summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorAlyssa Ross <hi@alyssa.is>2022-05-31 09:59:33 +0000
committerAlyssa Ross <hi@alyssa.is>2022-05-31 09:59:57 +0000
commit9ff36293d1e428cd7bf03e8d4b03611b6d361c28 (patch)
tree1ab51a42b868c55b83f6ccdb80371b9888739dd9 /nixos
parent1c4fcd0d4b0541e674ee56ace1053e23e562cc80 (diff)
parentddc3c396a51918043bb0faa6f676abd9562be62c (diff)
downloadnixpkgs-9ff36293d1e428cd7bf03e8d4b03611b6d361c28.tar
nixpkgs-9ff36293d1e428cd7bf03e8d4b03611b6d361c28.tar.gz
nixpkgs-9ff36293d1e428cd7bf03e8d4b03611b6d361c28.tar.bz2
nixpkgs-9ff36293d1e428cd7bf03e8d4b03611b6d361c28.tar.lz
nixpkgs-9ff36293d1e428cd7bf03e8d4b03611b6d361c28.tar.xz
nixpkgs-9ff36293d1e428cd7bf03e8d4b03611b6d361c28.tar.zst
nixpkgs-9ff36293d1e428cd7bf03e8d4b03611b6d361c28.zip
Last good Nixpkgs for Weston+nouveau? archive
I came this commit hash to terwiz[m] on IRC, who is trying to figure out
what the last version of Spectrum that worked on their NUC with Nvidia
graphics is.
Diffstat (limited to 'nixos')
-rw-r--r--nixos/COPYING18
-rw-r--r--nixos/README5
-rw-r--r--nixos/default.nix20
-rw-r--r--nixos/doc/manual/.gitignore2
-rw-r--r--nixos/doc/manual/Makefile30
-rw-r--r--nixos/doc/manual/README.md3
-rw-r--r--nixos/doc/manual/administration/boot-problems.section.md41
-rw-r--r--nixos/doc/manual/administration/cleaning-store.chapter.md62
-rw-r--r--nixos/doc/manual/administration/container-networking.section.md44
-rw-r--r--nixos/doc/manual/administration/containers.chapter.md28
-rw-r--r--nixos/doc/manual/administration/control-groups.chapter.md59
-rw-r--r--nixos/doc/manual/administration/declarative-containers.section.md48
-rw-r--r--nixos/doc/manual/administration/imperative-containers.section.md115
-rw-r--r--nixos/doc/manual/administration/logging.chapter.md38
-rw-r--r--nixos/doc/manual/administration/maintenance-mode.section.md11
-rw-r--r--nixos/doc/manual/administration/network-problems.section.md21
-rw-r--r--nixos/doc/manual/administration/rebooting.chapter.md30
-rw-r--r--nixos/doc/manual/administration/rollback.section.md38
-rw-r--r--nixos/doc/manual/administration/running.xml21
-rw-r--r--nixos/doc/manual/administration/service-mgmt.chapter.md120
-rw-r--r--nixos/doc/manual/administration/store-corruption.section.md28
-rw-r--r--nixos/doc/manual/administration/troubleshooting.chapter.md12
-rw-r--r--nixos/doc/manual/administration/user-sessions.chapter.md43
-rw-r--r--nixos/doc/manual/configuration/abstractions.section.md80
-rw-r--r--nixos/doc/manual/configuration/ad-hoc-network-config.section.md13
-rw-r--r--nixos/doc/manual/configuration/ad-hoc-packages.section.md51
-rw-r--r--nixos/doc/manual/configuration/adding-custom-packages.section.md74
-rw-r--r--nixos/doc/manual/configuration/config-file.section.md175
-rw-r--r--nixos/doc/manual/configuration/config-syntax.chapter.md19
-rw-r--r--nixos/doc/manual/configuration/configuration.xml31
-rw-r--r--nixos/doc/manual/configuration/customizing-packages.section.md74
-rw-r--r--nixos/doc/manual/configuration/declarative-packages.section.md46
-rw-r--r--nixos/doc/manual/configuration/file-systems.chapter.md42
-rw-r--r--nixos/doc/manual/configuration/firewall.section.md32
-rw-r--r--nixos/doc/manual/configuration/gpu-accel.chapter.md204
-rw-r--r--nixos/doc/manual/configuration/ipv4-config.section.md35
-rw-r--r--nixos/doc/manual/configuration/ipv6-config.section.md42
-rw-r--r--nixos/doc/manual/configuration/kubernetes.chapter.md104
-rw-r--r--nixos/doc/manual/configuration/linux-kernel.chapter.md140
-rw-r--r--nixos/doc/manual/configuration/luks-file-systems.section.md77
-rw-r--r--nixos/doc/manual/configuration/modularity.section.md133
-rw-r--r--nixos/doc/manual/configuration/network-manager.section.md42
-rw-r--r--nixos/doc/manual/configuration/networking.chapter.md16
-rw-r--r--nixos/doc/manual/configuration/package-mgmt.chapter.md18
-rw-r--r--nixos/doc/manual/configuration/profiles.chapter.md34
-rw-r--r--nixos/doc/manual/configuration/profiles/all-hardware.section.md11
-rw-r--r--nixos/doc/manual/configuration/profiles/base.section.md7
-rw-r--r--nixos/doc/manual/configuration/profiles/clone-config.section.md11
-rw-r--r--nixos/doc/manual/configuration/profiles/demo.section.md4
-rw-r--r--nixos/doc/manual/configuration/profiles/docker-container.section.md7
-rw-r--r--nixos/doc/manual/configuration/profiles/graphical.section.md10
-rw-r--r--nixos/doc/manual/configuration/profiles/hardened.section.md20
-rw-r--r--nixos/doc/manual/configuration/profiles/headless.section.md9
-rw-r--r--nixos/doc/manual/configuration/profiles/installation-device.section.md24
-rw-r--r--nixos/doc/manual/configuration/profiles/minimal.section.md9
-rw-r--r--nixos/doc/manual/configuration/profiles/qemu-guest.section.md7
-rw-r--r--nixos/doc/manual/configuration/renaming-interfaces.section.md51
-rw-r--r--nixos/doc/manual/configuration/ssh.section.md19
-rw-r--r--nixos/doc/manual/configuration/sshfs-file-systems.section.md104
-rw-r--r--nixos/doc/manual/configuration/subversion.chapter.md102
-rw-r--r--nixos/doc/manual/configuration/summary.section.md46
-rw-r--r--nixos/doc/manual/configuration/user-mgmt.chapter.md92
-rw-r--r--nixos/doc/manual/configuration/wayland.chapter.md27
-rw-r--r--nixos/doc/manual/configuration/wireless.section.md67
-rw-r--r--nixos/doc/manual/configuration/x-windows.chapter.md337
-rw-r--r--nixos/doc/manual/configuration/xfce.chapter.md52
-rw-r--r--nixos/doc/manual/contributing-to-this-manual.chapter.md13
-rw-r--r--nixos/doc/manual/default.nix264
-rw-r--r--nixos/doc/manual/development/activation-script.section.md72
-rw-r--r--nixos/doc/manual/development/assertions.section.md40
-rw-r--r--nixos/doc/manual/development/building-nixos.chapter.md46
-rw-r--r--nixos/doc/manual/development/building-parts.chapter.md74
-rw-r--r--nixos/doc/manual/development/development.xml20
-rw-r--r--nixos/doc/manual/development/freeform-modules.section.md79
-rw-r--r--nixos/doc/manual/development/importing-modules.section.md46
-rw-r--r--nixos/doc/manual/development/linking-nixos-tests-to-packages.section.md6
-rw-r--r--nixos/doc/manual/development/meta-attributes.section.md66
-rw-r--r--nixos/doc/manual/development/nixos-tests.chapter.md13
-rw-r--r--nixos/doc/manual/development/option-declarations.section.md221
-rw-r--r--nixos/doc/manual/development/option-def.section.md91
-rw-r--r--nixos/doc/manual/development/option-types.section.md583
-rw-r--r--nixos/doc/manual/development/replace-modules.section.md64
-rw-r--r--nixos/doc/manual/development/running-nixos-tests-interactively.section.md35
-rw-r--r--nixos/doc/manual/development/running-nixos-tests.section.md31
-rw-r--r--nixos/doc/manual/development/settings-options.section.md237
-rw-r--r--nixos/doc/manual/development/sources.chapter.md77
-rw-r--r--nixos/doc/manual/development/testing-installer.chapter.md18
-rw-r--r--nixos/doc/manual/development/unit-handling.section.md62
-rw-r--r--nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md53
-rw-r--r--nixos/doc/manual/development/writing-documentation.chapter.md93
-rw-r--r--nixos/doc/manual/development/writing-modules.chapter.md208
-rw-r--r--nixos/doc/manual/development/writing-nixos-tests.section.md366
-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.xml131
-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.xml80
-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.xml21
-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.xml239
-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.xml126
-rw-r--r--nixos/doc/manual/from_md/configuration/linux-kernel.chapter.xml157
-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/summary.section.xml332
-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.xml381
-rw-r--r--nixos/doc/manual/from_md/configuration/xfce.chapter.xml62
-rw-r--r--nixos/doc/manual/from_md/contributing-to-this-manual.chapter.xml22
-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/building-nixos.chapter.xml72
-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.xml327
-rw-r--r--nixos/doc/manual/from_md/development/option-def.section.xml104
-rw-r--r--nixos/doc/manual/from_md/development/option-types.section.xml1038
-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.xml39
-rw-r--r--nixos/doc/manual/from_md/development/running-nixos-tests.section.xml34
-rw-r--r--nixos/doc/manual/from_md/development/settings-options.section.xml389
-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.xml616
-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-pxe.section.xml42
-rw-r--r--nixos/doc/manual/from_md/installation/installing-usb.section.xml35
-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.xml645
-rw-r--r--nixos/doc/manual/from_md/installation/obtaining.chapter.xml48
-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.xml871
-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.xml2091
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2205.section.xml1630
-rw-r--r--nixos/doc/manual/installation/changing-config.chapter.md100
-rw-r--r--nixos/doc/manual/installation/installation.xml17
-rw-r--r--nixos/doc/manual/installation/installing-behind-a-proxy.section.md29
-rw-r--r--nixos/doc/manual/installation/installing-from-other-distro.section.md279
-rw-r--r--nixos/doc/manual/installation/installing-pxe.section.md32
-rw-r--r--nixos/doc/manual/installation/installing-usb.section.md31
-rw-r--r--nixos/doc/manual/installation/installing-virtualbox-guest.section.md59
-rw-r--r--nixos/doc/manual/installation/installing.chapter.md482
-rw-r--r--nixos/doc/manual/installation/obtaining.chapter.md26
-rw-r--r--nixos/doc/manual/installation/upgrading.chapter.md118
-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.xml690
-rw-r--r--nixos/doc/manual/man-nixos-version.xml137
-rw-r--r--nixos/doc/manual/man-pages.xml20
-rw-r--r--nixos/doc/manual/manual.xml24
-rwxr-xr-xnixos/doc/manual/md-to-db.sh49
-rw-r--r--nixos/doc/manual/preface.xml42
-rw-r--r--nixos/doc/manual/release-notes/release-notes.xml28
-rw-r--r--nixos/doc/manual/release-notes/rl-1310.section.md3
-rw-r--r--nixos/doc/manual/release-notes/rl-1404.section.md81
-rw-r--r--nixos/doc/manual/release-notes/rl-1412.section.md171
-rw-r--r--nixos/doc/manual/release-notes/rl-1509.section.md319
-rw-r--r--nixos/doc/manual/release-notes/rl-1603.section.md282
-rw-r--r--nixos/doc/manual/release-notes/rl-1609.section.md73
-rw-r--r--nixos/doc/manual/release-notes/rl-1703.section.md303
-rw-r--r--nixos/doc/manual/release-notes/rl-1709.section.md316
-rw-r--r--nixos/doc/manual/release-notes/rl-1803.section.md284
-rw-r--r--nixos/doc/manual/release-notes/rl-1809.section.md332
-rw-r--r--nixos/doc/manual/release-notes/rl-1903.section.md214
-rw-r--r--nixos/doc/manual/release-notes/rl-1909.section.md313
-rw-r--r--nixos/doc/manual/release-notes/rl-2003.section.md507
-rw-r--r--nixos/doc/manual/release-notes/rl-2009.section.md747
-rw-r--r--nixos/doc/manual/release-notes/rl-2105.section.md428
-rw-r--r--nixos/doc/manual/release-notes/rl-2111.section.md575
-rw-r--r--nixos/doc/manual/release-notes/rl-2205.section.md582
-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/build-vms.nix113
-rw-r--r--nixos/lib/default.nix33
-rw-r--r--nixos/lib/eval-cacheable-options.nix53
-rw-r--r--nixos/lib/eval-config-minimal.nix49
-rw-r--r--nixos/lib/eval-config.nix111
-rw-r--r--nixos/lib/from-env.nix4
-rw-r--r--nixos/lib/make-channel.nix31
-rw-r--r--nixos/lib/make-disk-image.nix443
-rw-r--r--nixos/lib/make-ext4-fs.nix86
-rw-r--r--nixos/lib/make-iso9660-image.nix65
-rw-r--r--nixos/lib/make-iso9660-image.sh139
-rw-r--r--nixos/lib/make-options-doc/default.nix169
-rw-r--r--nixos/lib/make-options-doc/generateAsciiDoc.py37
-rw-r--r--nixos/lib/make-options-doc/generateCommonMark.py27
-rw-r--r--nixos/lib/make-options-doc/mergeJSON.py93
-rw-r--r--nixos/lib/make-options-doc/options-to-docbook.xsl246
-rw-r--r--nixos/lib/make-options-doc/optionsJSONtoXML.nix6
-rw-r--r--nixos/lib/make-options-doc/postprocess-option-descriptions.xsl115
-rw-r--r--nixos/lib/make-options-doc/sortXML.py27
-rw-r--r--nixos/lib/make-squashfs.nix35
-rw-r--r--nixos/lib/make-system-tarball.nix56
-rw-r--r--nixos/lib/make-system-tarball.sh57
-rw-r--r--nixos/lib/make-zfs-image.nix333
-rw-r--r--nixos/lib/qemu-common.nix32
-rw-r--r--nixos/lib/systemd-lib.nix440
-rw-r--r--nixos/lib/systemd-unit-options.nix552
-rw-r--r--nixos/lib/test-driver/default.nix32
-rw-r--r--nixos/lib/test-driver/setup.py13
-rwxr-xr-xnixos/lib/test-driver/test_driver/__init__.py128
-rw-r--r--nixos/lib/test-driver/test_driver/driver.py225
-rw-r--r--nixos/lib/test-driver/test_driver/logger.py101
-rw-r--r--nixos/lib/test-driver/test_driver/machine.py988
-rw-r--r--nixos/lib/test-driver/test_driver/polling_condition.py77
-rw-r--r--nixos/lib/test-driver/test_driver/vlan.py58
-rw-r--r--nixos/lib/testing-python.nix251
-rw-r--r--nixos/lib/utils.nix201
-rw-r--r--nixos/maintainers/option-usages.nix192
-rw-r--r--nixos/maintainers/scripts/azure-new/.gitignore1
-rw-r--r--nixos/maintainers/scripts/azure-new/README.md42
-rwxr-xr-xnixos/maintainers/scripts/azure-new/boot-vm.sh36
-rw-r--r--nixos/maintainers/scripts/azure-new/common.sh7
-rw-r--r--nixos/maintainers/scripts/azure-new/examples/basic/image.nix10
-rw-r--r--nixos/maintainers/scripts/azure-new/examples/basic/system.nix34
-rw-r--r--nixos/maintainers/scripts/azure-new/shell.nix13
-rwxr-xr-xnixos/maintainers/scripts/azure-new/upload-image.sh58
-rwxr-xr-xnixos/maintainers/scripts/azure/create-azure.sh8
-rwxr-xr-xnixos/maintainers/scripts/azure/upload-azure.sh22
-rw-r--r--nixos/maintainers/scripts/cloudstack/cloudstack-image.nix22
-rw-r--r--nixos/maintainers/scripts/ec2/amazon-image-zfs.nix12
-rw-r--r--nixos/maintainers/scripts/ec2/amazon-image.nix165
-rwxr-xr-xnixos/maintainers/scripts/ec2/create-amis.sh342
-rwxr-xr-xnixos/maintainers/scripts/gce/create-gce.sh35
-rw-r--r--nixos/maintainers/scripts/lxd/lxd-image-inner.nix102
-rw-r--r--nixos/maintainers/scripts/lxd/lxd-image.nix34
-rw-r--r--nixos/maintainers/scripts/lxd/nix.tpl9
-rw-r--r--nixos/maintainers/scripts/openstack/openstack-image.nix26
-rw-r--r--nixos/modules/config/appstream.nix25
-rw-r--r--nixos/modules/config/console.nix203
-rw-r--r--nixos/modules/config/debug-info.nix45
-rw-r--r--nixos/modules/config/fonts/fontconfig.nix475
-rw-r--r--nixos/modules/config/fonts/fontdir.nix67
-rw-r--r--nixos/modules/config/fonts/fonts.nix81
-rw-r--r--nixos/modules/config/fonts/ghostscript.nix33
-rw-r--r--nixos/modules/config/gnu.nix44
-rw-r--r--nixos/modules/config/gtk/gtk-icon-cache.nix87
-rw-r--r--nixos/modules/config/i18n.nix97
-rw-r--r--nixos/modules/config/iproute2.nix32
-rw-r--r--nixos/modules/config/krb5/default.nix369
-rw-r--r--nixos/modules/config/ldap.nix303
-rw-r--r--nixos/modules/config/locale.nix94
-rw-r--r--nixos/modules/config/malloc.nix117
-rw-r--r--nixos/modules/config/networking.nix237
-rw-r--r--nixos/modules/config/no-x-libs.nix44
-rw-r--r--nixos/modules/config/nsswitch.nix133
-rw-r--r--nixos/modules/config/power-management.nix106
-rw-r--r--nixos/modules/config/pulseaudio.nix331
-rw-r--r--nixos/modules/config/qt5.nix104
-rw-r--r--nixos/modules/config/resolvconf.nix145
-rw-r--r--nixos/modules/config/shells-environment.nix224
-rw-r--r--nixos/modules/config/swap.nix249
-rw-r--r--nixos/modules/config/sysctl.nix63
-rw-r--r--nixos/modules/config/system-environment.nix104
-rw-r--r--nixos/modules/config/system-path.nix189
-rw-r--r--nixos/modules/config/terminfo.nix33
-rw-r--r--nixos/modules/config/unix-odbc-drivers.nix38
-rw-r--r--nixos/modules/config/update-users-groups.pl365
-rw-r--r--nixos/modules/config/users-groups.nix715
-rw-r--r--nixos/modules/config/vte.nix56
-rw-r--r--nixos/modules/config/xdg/autostart.nix26
-rw-r--r--nixos/modules/config/xdg/icons.nix42
-rw-r--r--nixos/modules/config/xdg/menus.nix29
-rw-r--r--nixos/modules/config/xdg/mime.nix102
-rw-r--r--nixos/modules/config/xdg/portal.nix81
-rw-r--r--nixos/modules/config/xdg/portals/wlr.nix67
-rw-r--r--nixos/modules/config/xdg/sounds.nix30
-rw-r--r--nixos/modules/config/zram.nix203
-rw-r--r--nixos/modules/hardware/acpilight.nix25
-rw-r--r--nixos/modules/hardware/all-firmware.nix93
-rw-r--r--nixos/modules/hardware/bladeRF.nix28
-rw-r--r--nixos/modules/hardware/brillo.nix22
-rw-r--r--nixos/modules/hardware/ckb-next.nix53
-rw-r--r--nixos/modules/hardware/corectrl.nix62
-rw-r--r--nixos/modules/hardware/cpu/amd-microcode.nix29
-rw-r--r--nixos/modules/hardware/cpu/intel-microcode.nix29
-rw-r--r--nixos/modules/hardware/cpu/intel-sgx.nix69
-rw-r--r--nixos/modules/hardware/device-tree.nix205
-rw-r--r--nixos/modules/hardware/digitalbitbox.nix30
-rw-r--r--nixos/modules/hardware/flirc.nix12
-rw-r--r--nixos/modules/hardware/gkraken.nix18
-rw-r--r--nixos/modules/hardware/gpgsmartcards.nix37
-rw-r--r--nixos/modules/hardware/hackrf.nix23
-rw-r--r--nixos/modules/hardware/i2c.nix43
-rw-r--r--nixos/modules/hardware/keyboard/teck.nix16
-rw-r--r--nixos/modules/hardware/keyboard/zsa.nix24
-rw-r--r--nixos/modules/hardware/ksm.nix38
-rw-r--r--nixos/modules/hardware/ledger.nix14
-rw-r--r--nixos/modules/hardware/logitech.nix96
-rw-r--r--nixos/modules/hardware/mcelog.nix35
-rw-r--r--nixos/modules/hardware/network/ath-user-regd.nix31
-rw-r--r--nixos/modules/hardware/network/b43.nix30
-rw-r--r--nixos/modules/hardware/network/broadcom-43xx.nix3
-rw-r--r--nixos/modules/hardware/network/intel-2200bg.nix30
-rw-r--r--nixos/modules/hardware/network/smc-2632w/default.nix9
-rw-r--r--nixos/modules/hardware/network/smc-2632w/firmware/cis/SMC2632W-v1.02.cis8
-rw-r--r--nixos/modules/hardware/network/zydas-zd1211.nix5
-rw-r--r--nixos/modules/hardware/nitrokey.nix27
-rw-r--r--nixos/modules/hardware/onlykey/default.nix33
-rw-r--r--nixos/modules/hardware/onlykey/onlykey.udev18
-rw-r--r--nixos/modules/hardware/opengl.nix157
-rw-r--r--nixos/modules/hardware/openrazer.nix146
-rw-r--r--nixos/modules/hardware/opentabletdriver.nix69
-rw-r--r--nixos/modules/hardware/pcmcia.nix60
-rw-r--r--nixos/modules/hardware/printers.nix130
-rw-r--r--nixos/modules/hardware/raid/hpsa.nix61
-rw-r--r--nixos/modules/hardware/rtl-sdr.nix23
-rw-r--r--nixos/modules/hardware/sata.nix100
-rw-r--r--nixos/modules/hardware/sensor/hddtemp.nix81
-rw-r--r--nixos/modules/hardware/sensor/iio.nix35
-rw-r--r--nixos/modules/hardware/steam-hardware.nix32
-rw-r--r--nixos/modules/hardware/system-76.nix89
-rw-r--r--nixos/modules/hardware/tuxedo-keyboard.nix35
-rw-r--r--nixos/modules/hardware/ubertooth.nix29
-rw-r--r--nixos/modules/hardware/uinput.nix19
-rw-r--r--nixos/modules/hardware/usb-wwan.nix39
-rw-r--r--nixos/modules/hardware/video/amdgpu-pro.nix70
-rw-r--r--nixos/modules/hardware/video/bumblebee.nix93
-rw-r--r--nixos/modules/hardware/video/capture/mwprocapture.nix56
-rw-r--r--nixos/modules/hardware/video/displaylink.nix76
-rw-r--r--nixos/modules/hardware/video/hidpi.nix16
-rw-r--r--nixos/modules/hardware/video/nvidia.nix391
-rw-r--r--nixos/modules/hardware/video/radeon.nix3
-rw-r--r--nixos/modules/hardware/video/switcheroo-control.nix18
-rw-r--r--nixos/modules/hardware/video/uvcvideo/default.nix64
-rw-r--r--nixos/modules/hardware/video/uvcvideo/uvcdynctrl-udev-rules.nix45
-rw-r--r--nixos/modules/hardware/video/webcam/facetimehd.nix44
-rw-r--r--nixos/modules/hardware/wooting.nix12
-rw-r--r--nixos/modules/hardware/xone.nix23
-rw-r--r--nixos/modules/hardware/xpadneo.nix29
-rw-r--r--nixos/modules/i18n/input-method/default.nix74
-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/hime.nix14
-rw-r--r--nixos/modules/i18n/input-method/ibus.nix86
-rw-r--r--nixos/modules/i18n/input-method/kime.nix51
-rw-r--r--nixos/modules/i18n/input-method/nabi.nix16
-rw-r--r--nixos/modules/i18n/input-method/uim.nix37
-rw-r--r--nixos/modules/installer/cd-dvd/channel.nix49
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-base.nix50
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix56
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix38
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5-new-kernel.nix7
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix50
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-minimal-new-kernel.nix7
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-minimal.nix14
-rw-r--r--nixos/modules/installer/cd-dvd/iso-image.nix811
-rw-r--r--nixos/modules/installer/cd-dvd/sd-image-aarch64-new-kernel.nix14
-rw-r--r--nixos/modules/installer/cd-dvd/sd-image-aarch64.nix14
-rw-r--r--nixos/modules/installer/cd-dvd/sd-image-armv7l-multiplatform.nix14
-rw-r--r--nixos/modules/installer/cd-dvd/sd-image-raspberrypi.nix14
-rw-r--r--nixos/modules/installer/cd-dvd/sd-image.nix14
-rw-r--r--nixos/modules/installer/cd-dvd/system-tarball-fuloong2f.nix160
-rw-r--r--nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt89
-rw-r--r--nixos/modules/installer/cd-dvd/system-tarball-pc.nix163
-rw-r--r--nixos/modules/installer/cd-dvd/system-tarball-sheevaplug.nix172
-rw-r--r--nixos/modules/installer/cd-dvd/system-tarball.nix93
-rw-r--r--nixos/modules/installer/netboot/netboot-base.nix17
-rw-r--r--nixos/modules/installer/netboot/netboot-minimal.nix10
-rw-r--r--nixos/modules/installer/netboot/netboot.nix120
-rw-r--r--nixos/modules/installer/scan/detected.nix12
-rw-r--r--nixos/modules/installer/scan/not-detected.nix6
-rw-r--r--nixos/modules/installer/sd-card/sd-image-aarch64-installer.nix10
-rw-r--r--nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel-installer.nix10
-rw-r--r--nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel.nix7
-rw-r--r--nixos/modules/installer/sd-card/sd-image-aarch64.nix74
-rw-r--r--nixos/modules/installer/sd-card/sd-image-armv7l-multiplatform-installer.nix10
-rw-r--r--nixos/modules/installer/sd-card/sd-image-armv7l-multiplatform.nix52
-rw-r--r--nixos/modules/installer/sd-card/sd-image-raspberrypi-installer.nix10
-rw-r--r--nixos/modules/installer/sd-card/sd-image-raspberrypi.nix41
-rw-r--r--nixos/modules/installer/sd-card/sd-image-riscv64-qemu-installer.nix10
-rw-r--r--nixos/modules/installer/sd-card/sd-image-riscv64-qemu.nix32
-rw-r--r--nixos/modules/installer/sd-card/sd-image-x86_64.nix27
-rw-r--r--nixos/modules/installer/sd-card/sd-image.nix269
-rw-r--r--nixos/modules/installer/tools/get-version-suffix22
-rw-r--r--nixos/modules/installer/tools/nix-fallback-paths.nix7
-rw-r--r--nixos/modules/installer/tools/nixos-build-vms/build-vms.nix31
-rw-r--r--nixos/modules/installer/tools/nixos-build-vms/nixos-build-vms.sh53
-rw-r--r--nixos/modules/installer/tools/nixos-enter.sh109
-rw-r--r--nixos/modules/installer/tools/nixos-generate-config.pl675
-rw-r--r--nixos/modules/installer/tools/nixos-install.sh218
-rw-r--r--nixos/modules/installer/tools/nixos-option/default.nix1
-rw-r--r--nixos/modules/installer/tools/nixos-version.sh24
-rw-r--r--nixos/modules/installer/tools/tools.nix235
-rw-r--r--nixos/modules/installer/virtualbox-demo.nix61
-rw-r--r--nixos/modules/misc/assertions.nix34
-rw-r--r--nixos/modules/misc/crashdump.nix76
-rw-r--r--nixos/modules/misc/documentation.nix346
-rw-r--r--nixos/modules/misc/extra-arguments.nix7
-rw-r--r--nixos/modules/misc/ids.nix677
-rw-r--r--nixos/modules/misc/label.nix72
-rw-r--r--nixos/modules/misc/lib.nix15
-rw-r--r--nixos/modules/misc/locate.nix313
-rw-r--r--nixos/modules/misc/man-db.nix73
-rw-r--r--nixos/modules/misc/mandoc.nix61
-rw-r--r--nixos/modules/misc/meta.nix76
-rw-r--r--nixos/modules/misc/nixops-autoluks.nix43
-rw-r--r--nixos/modules/misc/nixpkgs.nix259
-rw-r--r--nixos/modules/misc/nixpkgs/test.nix8
-rw-r--r--nixos/modules/misc/passthru.nix16
-rw-r--r--nixos/modules/misc/version.nix141
-rw-r--r--nixos/modules/misc/wordlist.nix59
-rw-r--r--nixos/modules/module-list.nix1237
-rw-r--r--nixos/modules/profiles/all-hardware.nix120
-rw-r--r--nixos/modules/profiles/base.nix58
-rw-r--r--nixos/modules/profiles/clone-config.nix109
-rw-r--r--nixos/modules/profiles/demo.nix21
-rw-r--r--nixos/modules/profiles/docker-container.nix61
-rw-r--r--nixos/modules/profiles/graphical.nix20
-rw-r--r--nixos/modules/profiles/hardened.nix118
-rw-r--r--nixos/modules/profiles/headless.nix25
-rw-r--r--nixos/modules/profiles/installation-device.nix117
-rw-r--r--nixos/modules/profiles/minimal.nix19
-rw-r--r--nixos/modules/profiles/qemu-guest.nix17
-rw-r--r--nixos/modules/programs/adb.nix30
-rw-r--r--nixos/modules/programs/appgate-sdp.nix25
-rw-r--r--nixos/modules/programs/atop.nix155
-rw-r--r--nixos/modules/programs/autojump.nix33
-rw-r--r--nixos/modules/programs/bandwhich.nix31
-rw-r--r--nixos/modules/programs/bash-my-aws.nix25
-rw-r--r--nixos/modules/programs/bash/bash-completion.nix37
-rw-r--r--nixos/modules/programs/bash/bash.nix217
-rw-r--r--nixos/modules/programs/bash/inputrc37
-rw-r--r--nixos/modules/programs/bash/ls-colors.nix20
-rw-r--r--nixos/modules/programs/bash/undistract-me.nix36
-rw-r--r--nixos/modules/programs/bcc.nix9
-rw-r--r--nixos/modules/programs/browserpass.nix32
-rw-r--r--nixos/modules/programs/calls.nix27
-rw-r--r--nixos/modules/programs/captive-browser.nix152
-rw-r--r--nixos/modules/programs/ccache.nix85
-rw-r--r--nixos/modules/programs/cdemu.nix62
-rw-r--r--nixos/modules/programs/chromium.nix112
-rw-r--r--nixos/modules/programs/clickshare.nix21
-rw-r--r--nixos/modules/programs/cnping.nix21
-rw-r--r--nixos/modules/programs/command-not-found/command-not-found.nix95
-rw-r--r--nixos/modules/programs/command-not-found/command-not-found.pl77
-rw-r--r--nixos/modules/programs/criu.nix27
-rw-r--r--nixos/modules/programs/dconf.nix66
-rw-r--r--nixos/modules/programs/digitalbitbox/default.nix39
-rw-r--r--nixos/modules/programs/digitalbitbox/doc.xml74
-rw-r--r--nixos/modules/programs/dmrconfig.nix38
-rw-r--r--nixos/modules/programs/droidcam.nix16
-rw-r--r--nixos/modules/programs/environment.nix67
-rw-r--r--nixos/modules/programs/evince.nix51
-rw-r--r--nixos/modules/programs/extra-container.nix17
-rw-r--r--nixos/modules/programs/feedbackd.nix33
-rw-r--r--nixos/modules/programs/file-roller.nix48
-rw-r--r--nixos/modules/programs/firejail.nix97
-rw-r--r--nixos/modules/programs/fish.nix320
-rw-r--r--nixos/modules/programs/fish_completion-generator.patch14
-rw-r--r--nixos/modules/programs/flashrom.nix26
-rw-r--r--nixos/modules/programs/flexoptix-app.nix25
-rw-r--r--nixos/modules/programs/freetds.nix61
-rw-r--r--nixos/modules/programs/fuse.nix37
-rw-r--r--nixos/modules/programs/gamemode.nix98
-rw-r--r--nixos/modules/programs/geary.nix24
-rw-r--r--nixos/modules/programs/git.nix69
-rw-r--r--nixos/modules/programs/gnome-disks.nix50
-rw-r--r--nixos/modules/programs/gnome-documents.nix54
-rw-r--r--nixos/modules/programs/gnome-terminal.nix38
-rw-r--r--nixos/modules/programs/gnupg.nix154
-rw-r--r--nixos/modules/programs/gpaste.nix36
-rw-r--r--nixos/modules/programs/gphoto2.nix30
-rw-r--r--nixos/modules/programs/hamster.nix15
-rw-r--r--nixos/modules/programs/htop.nix58
-rw-r--r--nixos/modules/programs/iftop.nix20
-rw-r--r--nixos/modules/programs/iotop.nix19
-rw-r--r--nixos/modules/programs/java.nix58
-rw-r--r--nixos/modules/programs/k40-whisperer.nix40
-rw-r--r--nixos/modules/programs/kbdlight.nix21
-rw-r--r--nixos/modules/programs/kclock.nix13
-rw-r--r--nixos/modules/programs/kdeconnect.nix35
-rw-r--r--nixos/modules/programs/less.nix134
-rw-r--r--nixos/modules/programs/liboping.nix24
-rw-r--r--nixos/modules/programs/light.nix27
-rw-r--r--nixos/modules/programs/mininet.nix39
-rw-r--r--nixos/modules/programs/mosh.nix43
-rw-r--r--nixos/modules/programs/msmtp.nix106
-rw-r--r--nixos/modules/programs/mtr.nix41
-rw-r--r--nixos/modules/programs/nano.nix42
-rw-r--r--nixos/modules/programs/nbd.nix19
-rw-r--r--nixos/modules/programs/neovim.nix166
-rw-r--r--nixos/modules/programs/nm-applet.nix31
-rw-r--r--nixos/modules/programs/noisetorch.nix28
-rw-r--r--nixos/modules/programs/npm.nix54
-rw-r--r--nixos/modules/programs/oblogout.nix11
-rw-r--r--nixos/modules/programs/pantheon-tweaks.nix19
-rw-r--r--nixos/modules/programs/partition-manager.nix19
-rw-r--r--nixos/modules/programs/phosh.nix162
-rw-r--r--nixos/modules/programs/plotinus.nix36
-rw-r--r--nixos/modules/programs/plotinus.xml30
-rw-r--r--nixos/modules/programs/proxychains.nix165
-rw-r--r--nixos/modules/programs/qt5ct.nix31
-rw-r--r--nixos/modules/programs/screen.nix33
-rw-r--r--nixos/modules/programs/seahorse.nix46
-rw-r--r--nixos/modules/programs/sedutil.nix18
-rw-r--r--nixos/modules/programs/shadow.nix129
-rw-r--r--nixos/modules/programs/singularity.nix34
-rw-r--r--nixos/modules/programs/slock.nix31
-rw-r--r--nixos/modules/programs/spacefm.nix55
-rw-r--r--nixos/modules/programs/ssh.nix346
-rw-r--r--nixos/modules/programs/ssmtp.nix190
-rw-r--r--nixos/modules/programs/starship.nix51
-rw-r--r--nixos/modules/programs/steam.nix63
-rw-r--r--nixos/modules/programs/sway.nix150
-rw-r--r--nixos/modules/programs/sysdig.nix14
-rw-r--r--nixos/modules/programs/system-config-printer.nix32
-rw-r--r--nixos/modules/programs/systemtap.nix29
-rw-r--r--nixos/modules/programs/thefuck.nix39
-rw-r--r--nixos/modules/programs/tmux.nix201
-rw-r--r--nixos/modules/programs/traceroute.nix28
-rw-r--r--nixos/modules/programs/tsm-client.nix287
-rw-r--r--nixos/modules/programs/turbovnc.nix54
-rw-r--r--nixos/modules/programs/udevil.nix19
-rw-r--r--nixos/modules/programs/usbtop.nix21
-rw-r--r--nixos/modules/programs/vim.nix33
-rw-r--r--nixos/modules/programs/virtualbox.nix8
-rw-r--r--nixos/modules/programs/wavemon.nix30
-rw-r--r--nixos/modules/programs/waybar.nix20
-rw-r--r--nixos/modules/programs/weylus.nix47
-rw-r--r--nixos/modules/programs/wireshark.nix42
-rw-r--r--nixos/modules/programs/wshowkeys.nix27
-rw-r--r--nixos/modules/programs/xfs_quota.nix110
-rw-r--r--nixos/modules/programs/xonsh.nix86
-rw-r--r--nixos/modules/programs/xss-lock.nix45
-rw-r--r--nixos/modules/programs/xwayland.nix50
-rw-r--r--nixos/modules/programs/yabar.nix163
-rw-r--r--nixos/modules/programs/zmap.nix18
-rw-r--r--nixos/modules/programs/zsh/oh-my-zsh.nix146
-rw-r--r--nixos/modules/programs/zsh/oh-my-zsh.xml155
-rw-r--r--nixos/modules/programs/zsh/zinputrc42
-rw-r--r--nixos/modules/programs/zsh/zsh-autoenv.nix28
-rw-r--r--nixos/modules/programs/zsh/zsh-autosuggestions.nix73
-rw-r--r--nixos/modules/programs/zsh/zsh-syntax-highlighting.nix107
-rw-r--r--nixos/modules/programs/zsh/zsh.nix303
-rw-r--r--nixos/modules/rename.nix97
-rw-r--r--nixos/modules/security/acme/default.nix921
-rw-r--r--nixos/modules/security/acme/doc.xml413
-rw-r--r--nixos/modules/security/acme/mk-cert-ownership-assertion.nix4
-rw-r--r--nixos/modules/security/apparmor.nix216
-rw-r--r--nixos/modules/security/apparmor/includes.nix317
-rw-r--r--nixos/modules/security/apparmor/profiles.nix11
-rw-r--r--nixos/modules/security/audit.nix123
-rw-r--r--nixos/modules/security/auditd.nix31
-rw-r--r--nixos/modules/security/ca.nix89
-rw-r--r--nixos/modules/security/chromium-suid-sandbox.nix38
-rw-r--r--nixos/modules/security/dhparams.nix177
-rw-r--r--nixos/modules/security/doas.nix288
-rw-r--r--nixos/modules/security/duosec.nix240
-rw-r--r--nixos/modules/security/google_oslogin.nix71
-rw-r--r--nixos/modules/security/lock-kernel-modules.nix58
-rw-r--r--nixos/modules/security/misc.nix155
-rw-r--r--nixos/modules/security/oath.nix50
-rw-r--r--nixos/modules/security/pam.nix1149
-rw-r--r--nixos/modules/security/pam_mount.nix102
-rw-r--r--nixos/modules/security/pam_usb.nix52
-rw-r--r--nixos/modules/security/polkit.nix112
-rw-r--r--nixos/modules/security/rngd.nix16
-rw-r--r--nixos/modules/security/rtkit.nix47
-rw-r--r--nixos/modules/security/sudo.nix265
-rw-r--r--nixos/modules/security/systemd-confinement.nix202
-rw-r--r--nixos/modules/security/tpm2.nix184
-rw-r--r--nixos/modules/security/wrappers/default.nix305
-rw-r--r--nixos/modules/security/wrappers/wrapper.c233
-rw-r--r--nixos/modules/security/wrappers/wrapper.nix21
-rw-r--r--nixos/modules/services/admin/meshcentral.nix53
-rw-r--r--nixos/modules/services/admin/oxidized.nix118
-rw-r--r--nixos/modules/services/admin/pgadmin.nix127
-rw-r--r--nixos/modules/services/admin/salt/master.nix63
-rw-r--r--nixos/modules/services/admin/salt/minion.nix67
-rw-r--r--nixos/modules/services/amqp/activemq/ActiveMQBroker.java19
-rw-r--r--nixos/modules/services/amqp/activemq/default.nix135
-rw-r--r--nixos/modules/services/amqp/rabbitmq.nix228
-rw-r--r--nixos/modules/services/audio/alsa.nix133
-rw-r--r--nixos/modules/services/audio/botamusique.nix115
-rw-r--r--nixos/modules/services/audio/hqplayerd.nix142
-rw-r--r--nixos/modules/services/audio/icecast.nix131
-rw-r--r--nixos/modules/services/audio/jack.nix294
-rw-r--r--nixos/modules/services/audio/jmusicbot.nix48
-rw-r--r--nixos/modules/services/audio/liquidsoap.nix69
-rw-r--r--nixos/modules/services/audio/mopidy.nix108
-rw-r--r--nixos/modules/services/audio/mpd.nix265
-rw-r--r--nixos/modules/services/audio/mpdscribble.nix213
-rw-r--r--nixos/modules/services/audio/navidrome.nix71
-rw-r--r--nixos/modules/services/audio/networkaudiod.nix19
-rw-r--r--nixos/modules/services/audio/roon-bridge.nix76
-rw-r--r--nixos/modules/services/audio/roon-server.nix79
-rw-r--r--nixos/modules/services/audio/slimserver.nix73
-rw-r--r--nixos/modules/services/audio/snapserver.nix315
-rw-r--r--nixos/modules/services/audio/spotifyd.nix68
-rw-r--r--nixos/modules/services/audio/squeezelite.nix46
-rw-r--r--nixos/modules/services/audio/ympd.nix57
-rw-r--r--nixos/modules/services/backup/automysqlbackup.nix119
-rw-r--r--nixos/modules/services/backup/bacula.nix578
-rw-r--r--nixos/modules/services/backup/borgbackup.nix730
-rw-r--r--nixos/modules/services/backup/borgbackup.xml209
-rw-r--r--nixos/modules/services/backup/borgmatic.nix57
-rw-r--r--nixos/modules/services/backup/btrbk.nix220
-rw-r--r--nixos/modules/services/backup/duplicati.nix86
-rw-r--r--nixos/modules/services/backup/duplicity.nix196
-rw-r--r--nixos/modules/services/backup/mysql-backup.nix130
-rw-r--r--nixos/modules/services/backup/postgresql-backup.nix164
-rw-r--r--nixos/modules/services/backup/postgresql-wal-receiver.nix204
-rw-r--r--nixos/modules/services/backup/restic-rest-server.nix111
-rw-r--r--nixos/modules/services/backup/restic.nix290
-rw-r--r--nixos/modules/services/backup/rsnapshot.nix75
-rw-r--r--nixos/modules/services/backup/sanoid.nix204
-rw-r--r--nixos/modules/services/backup/syncoid.nix421
-rw-r--r--nixos/modules/services/backup/tarsnap.nix408
-rw-r--r--nixos/modules/services/backup/tsm.nix125
-rw-r--r--nixos/modules/services/backup/zfs-replication.nix90
-rw-r--r--nixos/modules/services/backup/znapzend.nix469
-rw-r--r--nixos/modules/services/backup/zrepl.nix54
-rw-r--r--nixos/modules/services/blockchain/ethereum/geth.nix179
-rw-r--r--nixos/modules/services/cluster/corosync/default.nix112
-rw-r--r--nixos/modules/services/cluster/hadoop/conf.nix44
-rw-r--r--nixos/modules/services/cluster/hadoop/default.nix223
-rw-r--r--nixos/modules/services/cluster/hadoop/hdfs.nix204
-rw-r--r--nixos/modules/services/cluster/hadoop/yarn.nix200
-rw-r--r--nixos/modules/services/cluster/k3s/default.nix128
-rw-r--r--nixos/modules/services/cluster/kubernetes/addon-manager.nix171
-rw-r--r--nixos/modules/services/cluster/kubernetes/addons/dns.nix368
-rw-r--r--nixos/modules/services/cluster/kubernetes/apiserver.nix500
-rw-r--r--nixos/modules/services/cluster/kubernetes/controller-manager.nix176
-rw-r--r--nixos/modules/services/cluster/kubernetes/default.nix315
-rw-r--r--nixos/modules/services/cluster/kubernetes/flannel.nix100
-rw-r--r--nixos/modules/services/cluster/kubernetes/kubelet.nix398
-rw-r--r--nixos/modules/services/cluster/kubernetes/pki.nix406
-rw-r--r--nixos/modules/services/cluster/kubernetes/proxy.nix102
-rw-r--r--nixos/modules/services/cluster/kubernetes/scheduler.nix101
-rw-r--r--nixos/modules/services/cluster/pacemaker/default.nix52
-rw-r--r--nixos/modules/services/cluster/spark/default.nix162
-rw-r--r--nixos/modules/services/computing/boinc/client.nix131
-rw-r--r--nixos/modules/services/computing/foldingathome/client.nix91
-rw-r--r--nixos/modules/services/computing/slurm/slurm.nix437
-rw-r--r--nixos/modules/services/computing/torque/mom.nix63
-rw-r--r--nixos/modules/services/computing/torque/server.nix96
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/master.nix290
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/worker.nix198
-rw-r--r--nixos/modules/services/continuous-integration/buildkite-agents.nix280
-rw-r--r--nixos/modules/services/continuous-integration/github-runner.nix310
-rw-r--r--nixos/modules/services/continuous-integration/gitlab-runner.nix581
-rw-r--r--nixos/modules/services/continuous-integration/gocd-agent/default.nix218
-rw-r--r--nixos/modules/services/continuous-integration/gocd-server/default.nix212
-rw-r--r--nixos/modules/services/continuous-integration/hail.nix61
-rw-r--r--nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix266
-rw-r--r--nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix101
-rw-r--r--nixos/modules/services/continuous-integration/hydra/default.nix501
-rw-r--r--nixos/modules/services/continuous-integration/jenkins/default.nix249
-rw-r--r--nixos/modules/services/continuous-integration/jenkins/job-builder.nix241
-rw-r--r--nixos/modules/services/continuous-integration/jenkins/slave.nix68
-rw-r--r--nixos/modules/services/databases/aerospike.nix156
-rw-r--r--nixos/modules/services/databases/cassandra.nix563
-rw-r--r--nixos/modules/services/databases/clickhouse.nix78
-rw-r--r--nixos/modules/services/databases/cockroachdb.nix217
-rw-r--r--nixos/modules/services/databases/couchdb.nix225
-rw-r--r--nixos/modules/services/databases/firebird.nix168
-rw-r--r--nixos/modules/services/databases/foundationdb.nix429
-rw-r--r--nixos/modules/services/databases/foundationdb.xml443
-rw-r--r--nixos/modules/services/databases/hbase.nix149
-rw-r--r--nixos/modules/services/databases/influxdb.nix197
-rw-r--r--nixos/modules/services/databases/influxdb2.nix66
-rw-r--r--nixos/modules/services/databases/memcached.nix118
-rw-r--r--nixos/modules/services/databases/monetdb.nix100
-rw-r--r--nixos/modules/services/databases/mongodb.nix197
-rw-r--r--nixos/modules/services/databases/mysql.nix521
-rw-r--r--nixos/modules/services/databases/neo4j.nix673
-rw-r--r--nixos/modules/services/databases/openldap.nix325
-rw-r--r--nixos/modules/services/databases/opentsdb.nix108
-rw-r--r--nixos/modules/services/databases/pgmanage.nix207
-rw-r--r--nixos/modules/services/databases/postgresql.nix424
-rw-r--r--nixos/modules/services/databases/postgresql.xml214
-rw-r--r--nixos/modules/services/databases/redis.nix391
-rw-r--r--nixos/modules/services/databases/rethinkdb.nix108
-rw-r--r--nixos/modules/services/databases/riak.nix162
-rw-r--r--nixos/modules/services/databases/victoriametrics.nix78
-rw-r--r--nixos/modules/services/desktops/accountsservice.nix58
-rw-r--r--nixos/modules/services/desktops/bamf.nix27
-rw-r--r--nixos/modules/services/desktops/blueman.nix25
-rw-r--r--nixos/modules/services/desktops/cpupower-gui.nix56
-rw-r--r--nixos/modules/services/desktops/dleyna-renderer.nix28
-rw-r--r--nixos/modules/services/desktops/dleyna-server.nix28
-rw-r--r--nixos/modules/services/desktops/espanso.nix24
-rw-r--r--nixos/modules/services/desktops/flatpak.nix56
-rw-r--r--nixos/modules/services/desktops/flatpak.xml56
-rw-r--r--nixos/modules/services/desktops/geoclue2.nix270
-rw-r--r--nixos/modules/services/desktops/gnome/at-spi2-core.nix57
-rw-r--r--nixos/modules/services/desktops/gnome/chrome-gnome-shell.nix41
-rw-r--r--nixos/modules/services/desktops/gnome/evolution-data-server.nix71
-rw-r--r--nixos/modules/services/desktops/gnome/glib-networking.nix45
-rw-r--r--nixos/modules/services/desktops/gnome/gnome-initial-setup.nix98
-rw-r--r--nixos/modules/services/desktops/gnome/gnome-keyring.nix63
-rw-r--r--nixos/modules/services/desktops/gnome/gnome-online-accounts.nix51
-rw-r--r--nixos/modules/services/desktops/gnome/gnome-online-miners.nix51
-rw-r--r--nixos/modules/services/desktops/gnome/gnome-remote-desktop.nix32
-rw-r--r--nixos/modules/services/desktops/gnome/gnome-settings-daemon.nix70
-rw-r--r--nixos/modules/services/desktops/gnome/gnome-user-share.nix48
-rw-r--r--nixos/modules/services/desktops/gnome/rygel.nix44
-rw-r--r--nixos/modules/services/desktops/gnome/sushi.nix50
-rw-r--r--nixos/modules/services/desktops/gnome/tracker-miners.nix54
-rw-r--r--nixos/modules/services/desktops/gnome/tracker.nix76
-rw-r--r--nixos/modules/services/desktops/gsignond.nix45
-rw-r--r--nixos/modules/services/desktops/gvfs.nix64
-rw-r--r--nixos/modules/services/desktops/malcontent.nix40
-rw-r--r--nixos/modules/services/desktops/neard.nix23
-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/jack.conf.json38
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/minimal.conf.json118
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json99
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/pipewire.conf.json96
-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.nix136
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire.nix247
-rw-r--r--nixos/modules/services/desktops/pipewire/wireplumber.nix44
-rw-r--r--nixos/modules/services/desktops/profile-sync-daemon.nix77
-rw-r--r--nixos/modules/services/desktops/system-config-printer.nix41
-rw-r--r--nixos/modules/services/desktops/telepathy.nix48
-rw-r--r--nixos/modules/services/desktops/tumbler.nix52
-rw-r--r--nixos/modules/services/desktops/zeitgeist.nix31
-rw-r--r--nixos/modules/services/development/blackfire.nix60
-rw-r--r--nixos/modules/services/development/blackfire.xml46
-rw-r--r--nixos/modules/services/development/bloop.nix54
-rw-r--r--nixos/modules/services/development/distccd.nix155
-rw-r--r--nixos/modules/services/development/hoogle.nix81
-rw-r--r--nixos/modules/services/development/jupyter/default.nix201
-rw-r--r--nixos/modules/services/development/jupyter/kernel-options.nix60
-rw-r--r--nixos/modules/services/development/jupyterhub/default.nix202
-rw-r--r--nixos/modules/services/development/lorri.nix55
-rw-r--r--nixos/modules/services/development/rstudio-server/default.nix107
-rw-r--r--nixos/modules/services/development/zammad.nix323
-rw-r--r--nixos/modules/services/display-managers/greetd.nix111
-rw-r--r--nixos/modules/services/editors/emacs.nix103
-rw-r--r--nixos/modules/services/editors/emacs.xml580
-rw-r--r--nixos/modules/services/editors/infinoted.nix160
-rw-r--r--nixos/modules/services/finance/odoo.nix122
-rw-r--r--nixos/modules/services/games/asf.nix236
-rw-r--r--nixos/modules/services/games/crossfire-server.nix179
-rw-r--r--nixos/modules/services/games/deliantra-server.nix172
-rw-r--r--nixos/modules/services/games/factorio.nix268
-rw-r--r--nixos/modules/services/games/freeciv.nix187
-rw-r--r--nixos/modules/services/games/minecraft-server.nix257
-rw-r--r--nixos/modules/services/games/minetest-server.nix107
-rw-r--r--nixos/modules/services/games/openarena.nix56
-rw-r--r--nixos/modules/services/games/quake3-server.nix112
-rw-r--r--nixos/modules/services/games/teeworlds.nix119
-rw-r--r--nixos/modules/services/games/terraria.nix169
-rw-r--r--nixos/modules/services/hardware/acpid.nix155
-rw-r--r--nixos/modules/services/hardware/actkbd.nix133
-rw-r--r--nixos/modules/services/hardware/auto-cpufreq.nix24
-rw-r--r--nixos/modules/services/hardware/bluetooth.nix141
-rw-r--r--nixos/modules/services/hardware/bolt.nix34
-rw-r--r--nixos/modules/services/hardware/brltty.nix57
-rw-r--r--nixos/modules/services/hardware/ddccontrol.nix39
-rw-r--r--nixos/modules/services/hardware/fancontrol.nix48
-rw-r--r--nixos/modules/services/hardware/freefall.nix64
-rw-r--r--nixos/modules/services/hardware/fwupd.nix134
-rw-r--r--nixos/modules/services/hardware/illum.nix35
-rw-r--r--nixos/modules/services/hardware/interception-tools.nix62
-rw-r--r--nixos/modules/services/hardware/irqbalance.nix24
-rw-r--r--nixos/modules/services/hardware/joycond.nix40
-rw-r--r--nixos/modules/services/hardware/lcd.nix171
-rw-r--r--nixos/modules/services/hardware/lirc.nix100
-rw-r--r--nixos/modules/services/hardware/nvidia-optimus.nix43
-rw-r--r--nixos/modules/services/hardware/pcscd.nix73
-rw-r--r--nixos/modules/services/hardware/pommed.nix50
-rw-r--r--nixos/modules/services/hardware/power-profiles-daemon.nix55
-rw-r--r--nixos/modules/services/hardware/rasdaemon.nix170
-rw-r--r--nixos/modules/services/hardware/ratbagd.nix27
-rw-r--r--nixos/modules/services/hardware/sane.nix197
-rw-r--r--nixos/modules/services/hardware/sane_extra_backends/brscan4.nix112
-rw-r--r--nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix68
-rw-r--r--nixos/modules/services/hardware/sane_extra_backends/brscan5.nix110
-rw-r--r--nixos/modules/services/hardware/sane_extra_backends/brscan5_etc_files.nix77
-rw-r--r--nixos/modules/services/hardware/sane_extra_backends/dsseries.nix26
-rw-r--r--nixos/modules/services/hardware/spacenavd.nix24
-rw-r--r--nixos/modules/services/hardware/tcsd.nix162
-rw-r--r--nixos/modules/services/hardware/thermald.nix57
-rw-r--r--nixos/modules/services/hardware/thinkfan.nix224
-rw-r--r--nixos/modules/services/hardware/throttled.nix36
-rw-r--r--nixos/modules/services/hardware/tlp.nix124
-rw-r--r--nixos/modules/services/hardware/trezord.nix70
-rw-r--r--nixos/modules/services/hardware/trezord.xml26
-rw-r--r--nixos/modules/services/hardware/triggerhappy.nix122
-rw-r--r--nixos/modules/services/hardware/udev.nix341
-rw-r--r--nixos/modules/services/hardware/udisks2.nix46
-rw-r--r--nixos/modules/services/hardware/undervolt.nix190
-rw-r--r--nixos/modules/services/hardware/upower.nix239
-rw-r--r--nixos/modules/services/hardware/usbmuxd.nix76
-rw-r--r--nixos/modules/services/hardware/vdr.nix82
-rw-r--r--nixos/modules/services/hardware/xow.nix20
-rw-r--r--nixos/modules/services/home-automation/home-assistant.nix552
-rw-r--r--nixos/modules/services/home-automation/zigbee2mqtt.nix142
-rw-r--r--nixos/modules/services/logging/SystemdJournal2Gelf.nix60
-rw-r--r--nixos/modules/services/logging/awstats.nix257
-rw-r--r--nixos/modules/services/logging/filebeat.nix253
-rw-r--r--nixos/modules/services/logging/fluentd.nix58
-rw-r--r--nixos/modules/services/logging/graylog.nix169
-rw-r--r--nixos/modules/services/logging/heartbeat.nix74
-rw-r--r--nixos/modules/services/logging/journalbeat.nix94
-rw-r--r--nixos/modules/services/logging/journaldriver.nix112
-rw-r--r--nixos/modules/services/logging/journalwatch.nix265
-rw-r--r--nixos/modules/services/logging/klogd.nix38
-rw-r--r--nixos/modules/services/logging/logcheck.nix242
-rw-r--r--nixos/modules/services/logging/logrotate.nix179
-rw-r--r--nixos/modules/services/logging/logstash.nix194
-rw-r--r--nixos/modules/services/logging/promtail.nix91
-rw-r--r--nixos/modules/services/logging/rsyslogd.nix105
-rw-r--r--nixos/modules/services/logging/syslog-ng.nix101
-rw-r--r--nixos/modules/services/logging/syslogd.nix130
-rw-r--r--nixos/modules/services/logging/vector.nix64
-rw-r--r--nixos/modules/services/mail/clamsmtp.nix181
-rw-r--r--nixos/modules/services/mail/davmail.nix99
-rw-r--r--nixos/modules/services/mail/dkimproxy-out.nix120
-rw-r--r--nixos/modules/services/mail/dovecot.nix462
-rw-r--r--nixos/modules/services/mail/dspam.nix150
-rw-r--r--nixos/modules/services/mail/exim.nix132
-rw-r--r--nixos/modules/services/mail/maddy.nix273
-rw-r--r--nixos/modules/services/mail/mail.nix34
-rw-r--r--nixos/modules/services/mail/mailcatcher.nix68
-rw-r--r--nixos/modules/services/mail/mailhog.nix82
-rw-r--r--nixos/modules/services/mail/mailman.nix462
-rw-r--r--nixos/modules/services/mail/mailman.xml94
-rw-r--r--nixos/modules/services/mail/mlmmj.nix171
-rw-r--r--nixos/modules/services/mail/nullmailer.nix244
-rw-r--r--nixos/modules/services/mail/offlineimap.nix72
-rw-r--r--nixos/modules/services/mail/opendkim.nix167
-rw-r--r--nixos/modules/services/mail/opensmtpd.nix135
-rw-r--r--nixos/modules/services/mail/pfix-srsd.nix56
-rw-r--r--nixos/modules/services/mail/postfix.nix988
-rw-r--r--nixos/modules/services/mail/postfixadmin.nix199
-rw-r--r--nixos/modules/services/mail/postgrey.nix205
-rw-r--r--nixos/modules/services/mail/postsrsd.nix135
-rw-r--r--nixos/modules/services/mail/roundcube.nix249
-rw-r--r--nixos/modules/services/mail/rspamd.nix446
-rw-r--r--nixos/modules/services/mail/rss2email.nix135
-rw-r--r--nixos/modules/services/mail/spamassassin.nix191
-rw-r--r--nixos/modules/services/mail/sympa.nix590
-rw-r--r--nixos/modules/services/matrix/matrix-synapse-log_config.yaml25
-rw-r--r--nixos/modules/services/matrix/matrix-synapse.nix773
-rw-r--r--nixos/modules/services/matrix/matrix-synapse.xml231
-rw-r--r--nixos/modules/services/matrix/mjolnir.nix242
-rw-r--r--nixos/modules/services/matrix/mjolnir.xml134
-rw-r--r--nixos/modules/services/matrix/pantalaimon-options.nix70
-rw-r--r--nixos/modules/services/matrix/pantalaimon.nix70
-rw-r--r--nixos/modules/services/misc/airsonic.nix179
-rw-r--r--nixos/modules/services/misc/ananicy.nix107
-rw-r--r--nixos/modules/services/misc/ankisyncd.nix79
-rw-r--r--nixos/modules/services/misc/apache-kafka.nix151
-rw-r--r--nixos/modules/services/misc/autofs.nix100
-rw-r--r--nixos/modules/services/misc/autorandr.nix53
-rw-r--r--nixos/modules/services/misc/bazarr.nix77
-rw-r--r--nixos/modules/services/misc/beanstalkd.nix63
-rw-r--r--nixos/modules/services/misc/bees.nix132
-rw-r--r--nixos/modules/services/misc/bepasty.nix179
-rw-r--r--nixos/modules/services/misc/calibre-server.nix86
-rw-r--r--nixos/modules/services/misc/canto-daemon.nix37
-rw-r--r--nixos/modules/services/misc/cfdyndns.nix82
-rw-r--r--nixos/modules/services/misc/cgminer.nix148
-rw-r--r--nixos/modules/services/misc/clipcat.nix31
-rw-r--r--nixos/modules/services/misc/clipmenu.nix31
-rwxr-xr-xnixos/modules/services/misc/confd.nix90
-rw-r--r--nixos/modules/services/misc/cpuminer-cryptonight.nix66
-rw-r--r--nixos/modules/services/misc/dendrite.nix275
-rw-r--r--nixos/modules/services/misc/devmon.nix25
-rw-r--r--nixos/modules/services/misc/dictd.nix65
-rw-r--r--nixos/modules/services/misc/disnix.nix98
-rw-r--r--nixos/modules/services/misc/docker-registry.nix159
-rw-r--r--nixos/modules/services/misc/domoticz.nix51
-rw-r--r--nixos/modules/services/misc/duckling.nix39
-rw-r--r--nixos/modules/services/misc/dwm-status.nix73
-rw-r--r--nixos/modules/services/misc/dysnomia.nix265
-rw-r--r--nixos/modules/services/misc/errbot.nix104
-rw-r--r--nixos/modules/services/misc/etcd.nix205
-rw-r--r--nixos/modules/services/misc/etebase-server.nix226
-rw-r--r--nixos/modules/services/misc/etesync-dav.nix92
-rw-r--r--nixos/modules/services/misc/ethminer.nix117
-rw-r--r--nixos/modules/services/misc/exhibitor.nix422
-rw-r--r--nixos/modules/services/misc/felix.nix104
-rw-r--r--nixos/modules/services/misc/freeswitch.nix104
-rw-r--r--nixos/modules/services/misc/fstrim.nix46
-rw-r--r--nixos/modules/services/misc/gammu-smsd.nix253
-rw-r--r--nixos/modules/services/misc/geoipupdate.nix187
-rw-r--r--nixos/modules/services/misc/gitea.nix663
-rw-r--r--nixos/modules/services/misc/gitit.nix725
-rw-r--r--nixos/modules/services/misc/gitlab.nix1458
-rw-r--r--nixos/modules/services/misc/gitlab.xml151
-rw-r--r--nixos/modules/services/misc/gitolite.nix234
-rw-r--r--nixos/modules/services/misc/gitweb.nix60
-rw-r--r--nixos/modules/services/misc/gogs.nix274
-rw-r--r--nixos/modules/services/misc/gollum.nix121
-rw-r--r--nixos/modules/services/misc/gpsd.nix116
-rw-r--r--nixos/modules/services/misc/greenclip.nix31
-rw-r--r--nixos/modules/services/misc/headphones.nix89
-rw-r--r--nixos/modules/services/misc/heisenbridge.nix222
-rw-r--r--nixos/modules/services/misc/ihaskell.nix65
-rw-r--r--nixos/modules/services/misc/input-remapper.nix30
-rw-r--r--nixos/modules/services/misc/irkerd.nix67
-rw-r--r--nixos/modules/services/misc/jackett.nix82
-rw-r--r--nixos/modules/services/misc/jellyfin.nix122
-rw-r--r--nixos/modules/services/misc/klipper.nix117
-rw-r--r--nixos/modules/services/misc/leaps.nix62
-rw-r--r--nixos/modules/services/misc/libreddit.nix66
-rw-r--r--nixos/modules/services/misc/lidarr.nix89
-rw-r--r--nixos/modules/services/misc/lifecycled.nix164
-rw-r--r--nixos/modules/services/misc/logkeys.nix30
-rw-r--r--nixos/modules/services/misc/mame.nix69
-rw-r--r--nixos/modules/services/misc/matrix-appservice-discord.nix161
-rw-r--r--nixos/modules/services/misc/matrix-appservice-irc.nix232
-rw-r--r--nixos/modules/services/misc/matrix-conduit.nix149
-rw-r--r--nixos/modules/services/misc/mautrix-facebook.nix195
-rw-r--r--nixos/modules/services/misc/mautrix-telegram.nix181
-rw-r--r--nixos/modules/services/misc/mbpfan.nix107
-rw-r--r--nixos/modules/services/misc/mediatomb.nix394
-rw-r--r--nixos/modules/services/misc/metabase.nix103
-rw-r--r--nixos/modules/services/misc/moonraker.nix138
-rw-r--r--nixos/modules/services/misc/mx-puppet-discord.nix122
-rw-r--r--nixos/modules/services/misc/n8n.nix79
-rw-r--r--nixos/modules/services/misc/nitter.nix358
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix818
-rw-r--r--nixos/modules/services/misc/nix-gc.nix100
-rw-r--r--nixos/modules/services/misc/nix-optimise.nix51
-rw-r--r--nixos/modules/services/misc/nix-ssh-serve.nix69
-rw-r--r--nixos/modules/services/misc/novacomd.nix31
-rw-r--r--nixos/modules/services/misc/nzbget.nix117
-rw-r--r--nixos/modules/services/misc/nzbhydra2.nix78
-rw-r--r--nixos/modules/services/misc/octoprint.nix133
-rw-r--r--nixos/modules/services/misc/ombi.nix81
-rw-r--r--nixos/modules/services/misc/osrm.nix86
-rw-r--r--nixos/modules/services/misc/owncast.nix98
-rw-r--r--nixos/modules/services/misc/packagekit.nix74
-rw-r--r--nixos/modules/services/misc/paperless-ng.nix322
-rw-r--r--nixos/modules/services/misc/parsoid.nix129
-rw-r--r--nixos/modules/services/misc/pinnwand.nix103
-rw-r--r--nixos/modules/services/misc/plex.nix180
-rw-r--r--nixos/modules/services/misc/plikd.nix82
-rw-r--r--nixos/modules/services/misc/podgrab.nix50
-rw-r--r--nixos/modules/services/misc/prowlarr.nix41
-rw-r--r--nixos/modules/services/misc/pykms.nix92
-rw-r--r--nixos/modules/services/misc/radarr.nix75
-rw-r--r--nixos/modules/services/misc/redmine.nix384
-rw-r--r--nixos/modules/services/misc/ripple-data-api.nix195
-rw-r--r--nixos/modules/services/misc/rippled.nix438
-rw-r--r--nixos/modules/services/misc/rmfakecloud.nix147
-rw-r--r--nixos/modules/services/misc/safeeyes.nix51
-rw-r--r--nixos/modules/services/misc/sdrplay.nix35
-rw-r--r--nixos/modules/services/misc/serviio.nix87
-rw-r--r--nixos/modules/services/misc/sickbeard.nix95
-rw-r--r--nixos/modules/services/misc/signald.nix105
-rw-r--r--nixos/modules/services/misc/siproxd.nix179
-rw-r--r--nixos/modules/services/misc/snapper.nix187
-rw-r--r--nixos/modules/services/misc/sonarr.nix76
-rw-r--r--nixos/modules/services/misc/sourcehut/builds.nix236
-rw-r--r--nixos/modules/services/misc/sourcehut/default.nix1386
-rw-r--r--nixos/modules/services/misc/sourcehut/dispatch.nix127
-rw-r--r--nixos/modules/services/misc/sourcehut/git.nix217
-rw-r--r--nixos/modules/services/misc/sourcehut/hg.nix175
-rw-r--r--nixos/modules/services/misc/sourcehut/hub.nix120
-rw-r--r--nixos/modules/services/misc/sourcehut/lists.nix187
-rw-r--r--nixos/modules/services/misc/sourcehut/man.nix124
-rw-r--r--nixos/modules/services/misc/sourcehut/meta.nix213
-rw-r--r--nixos/modules/services/misc/sourcehut/paste.nix135
-rw-r--r--nixos/modules/services/misc/sourcehut/service.nix375
-rw-r--r--nixos/modules/services/misc/sourcehut/sourcehut.xml119
-rw-r--r--nixos/modules/services/misc/sourcehut/todo.nix163
-rw-r--r--nixos/modules/services/misc/spice-vdagentd.nix30
-rw-r--r--nixos/modules/services/misc/ssm-agent.nix73
-rw-r--r--nixos/modules/services/misc/sssd.nix97
-rw-r--r--nixos/modules/services/misc/subsonic.nix169
-rw-r--r--nixos/modules/services/misc/sundtek.nix33
-rw-r--r--nixos/modules/services/misc/svnserve.nix46
-rw-r--r--nixos/modules/services/misc/synergy.nix149
-rw-r--r--nixos/modules/services/misc/sysprof.nix19
-rw-r--r--nixos/modules/services/misc/taskserver/default.nix569
-rw-r--r--nixos/modules/services/misc/taskserver/doc.xml135
-rw-r--r--nixos/modules/services/misc/taskserver/helper-tool.py688
-rw-r--r--nixos/modules/services/misc/tautulli.nix81
-rw-r--r--nixos/modules/services/misc/tiddlywiki.nix52
-rw-r--r--nixos/modules/services/misc/tp-auto-kbbl.nix58
-rw-r--r--nixos/modules/services/misc/tzupdate.nix45
-rw-r--r--nixos/modules/services/misc/uhub.nix112
-rw-r--r--nixos/modules/services/misc/weechat.nix63
-rw-r--r--nixos/modules/services/misc/weechat.xml66
-rw-r--r--nixos/modules/services/misc/xmr-stak.nix93
-rw-r--r--nixos/modules/services/misc/xmrig.nix76
-rw-r--r--nixos/modules/services/misc/zoneminder.nix370
-rw-r--r--nixos/modules/services/misc/zookeeper.nix158
-rw-r--r--nixos/modules/services/monitoring/alerta.nix111
-rw-r--r--nixos/modules/services/monitoring/apcupsd.nix191
-rw-r--r--nixos/modules/services/monitoring/arbtt.nix62
-rw-r--r--nixos/modules/services/monitoring/bosun.nix165
-rw-r--r--nixos/modules/services/monitoring/cadvisor.nix142
-rw-r--r--nixos/modules/services/monitoring/collectd.nix162
-rw-r--r--nixos/modules/services/monitoring/das_watchdog.nix34
-rw-r--r--nixos/modules/services/monitoring/datadog-agent.nix296
-rw-r--r--nixos/modules/services/monitoring/dd-agent/dd-agent-defaults.nix8
-rw-r--r--nixos/modules/services/monitoring/dd-agent/dd-agent.nix236
-rwxr-xr-xnixos/modules/services/monitoring/dd-agent/update-dd-agent-defaults9
-rw-r--r--nixos/modules/services/monitoring/do-agent.nix25
-rw-r--r--nixos/modules/services/monitoring/fusion-inventory.nix63
-rw-r--r--nixos/modules/services/monitoring/grafana-image-renderer.nix150
-rw-r--r--nixos/modules/services/monitoring/grafana-reporter.nix67
-rw-r--r--nixos/modules/services/monitoring/grafana.nix723
-rw-r--r--nixos/modules/services/monitoring/graphite.nix582
-rw-r--r--nixos/modules/services/monitoring/hdaps.nix23
-rw-r--r--nixos/modules/services/monitoring/heapster.nix59
-rw-r--r--nixos/modules/services/monitoring/incron.nix103
-rw-r--r--nixos/modules/services/monitoring/kapacitor.nix188
-rw-r--r--nixos/modules/services/monitoring/loki.nix114
-rw-r--r--nixos/modules/services/monitoring/longview.nix160
-rw-r--r--nixos/modules/services/monitoring/mackerel-agent.nix110
-rw-r--r--nixos/modules/services/monitoring/metricbeat.nix151
-rw-r--r--nixos/modules/services/monitoring/monit.nix48
-rw-r--r--nixos/modules/services/monitoring/munin.nix404
-rw-r--r--nixos/modules/services/monitoring/nagios.nix213
-rw-r--r--nixos/modules/services/monitoring/netdata.nix310
-rw-r--r--nixos/modules/services/monitoring/parsedmarc.md113
-rw-r--r--nixos/modules/services/monitoring/parsedmarc.nix542
-rw-r--r--nixos/modules/services/monitoring/parsedmarc.xml125
-rw-r--r--nixos/modules/services/monitoring/prometheus/alertmanager.nix187
-rw-r--r--nixos/modules/services/monitoring/prometheus/default.nix1835
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.nix303
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.xml227
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/apcupsd.nix38
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/artifactory.nix59
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/bind.nix54
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/bird.nix50
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/bitcoin.nix82
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix70
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/buildkite-agent.nix64
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/collectd.nix77
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/dmarc.nix117
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/dnsmasq.nix38
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/domain.nix19
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix92
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/fastly.nix41
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/flow.nix50
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/fritzbox.nix38
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/influxdb.nix34
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/jitsi.nix40
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/json.nix43
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/kea.nix43
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/keylight.nix19
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/knot.nix54
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/lnd.nix46
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/mail.nix176
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/mikrotik.nix66
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/minio.nix64
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/modemmanager.nix37
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix58
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/nginx.nix68
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix51
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/node.nix49
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/openldap.nix67
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/openvpn.nix39
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/pihole.nix74
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/postfix.nix98
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/postgres.nix88
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/process.nix46
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/pve.nix118
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix53
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/redis.nix19
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix97
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix83
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/script.nix64
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix75
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix61
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/snmp.nix68
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/sql.nix108
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/surfboard.nix31
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/systemd.nix22
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/tor.nix44
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/unbound.nix63
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix34
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/unifi.nix66
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/varnish.nix88
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix71
-rw-r--r--nixos/modules/services/monitoring/prometheus/pushgateway.nix166
-rw-r--r--nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix55
-rw-r--r--nixos/modules/services/monitoring/riemann-dash.nix81
-rw-r--r--nixos/modules/services/monitoring/riemann-tools.nix70
-rw-r--r--nixos/modules/services/monitoring/riemann.nix105
-rw-r--r--nixos/modules/services/monitoring/scollector.nix134
-rw-r--r--nixos/modules/services/monitoring/smartd.nix253
-rw-r--r--nixos/modules/services/monitoring/statsd.nix149
-rw-r--r--nixos/modules/services/monitoring/sysstat.nix76
-rw-r--r--nixos/modules/services/monitoring/teamviewer.nix49
-rw-r--r--nixos/modules/services/monitoring/telegraf.nix90
-rw-r--r--nixos/modules/services/monitoring/thanos.nix838
-rw-r--r--nixos/modules/services/monitoring/tuptime.nix91
-rw-r--r--nixos/modules/services/monitoring/unifi-poller.nix318
-rw-r--r--nixos/modules/services/monitoring/ups.nix263
-rw-r--r--nixos/modules/services/monitoring/uptime.nix100
-rw-r--r--nixos/modules/services/monitoring/vnstat.nix60
-rw-r--r--nixos/modules/services/monitoring/zabbix-agent.nix178
-rw-r--r--nixos/modules/services/monitoring/zabbix-proxy.nix323
-rw-r--r--nixos/modules/services/monitoring/zabbix-server.nix320
-rw-r--r--nixos/modules/services/network-filesystems/cachefilesd.nix63
-rw-r--r--nixos/modules/services/network-filesystems/ceph.nix406
-rw-r--r--nixos/modules/services/network-filesystems/davfs2.nix93
-rw-r--r--nixos/modules/services/network-filesystems/diod.nix159
-rw-r--r--nixos/modules/services/network-filesystems/drbd.nix63
-rw-r--r--nixos/modules/services/network-filesystems/glusterfs.nix208
-rw-r--r--nixos/modules/services/network-filesystems/ipfs.nix311
-rw-r--r--nixos/modules/services/network-filesystems/kbfs.nix118
-rw-r--r--nixos/modules/services/network-filesystems/litestream/default.nix100
-rw-r--r--nixos/modules/services/network-filesystems/litestream/litestream.xml65
-rw-r--r--nixos/modules/services/network-filesystems/moosefs.nix249
-rw-r--r--nixos/modules/services/network-filesystems/netatalk.nix97
-rw-r--r--nixos/modules/services/network-filesystems/nfsd.nix175
-rw-r--r--nixos/modules/services/network-filesystems/openafs/client.nix252
-rw-r--r--nixos/modules/services/network-filesystems/openafs/lib.nix33
-rw-r--r--nixos/modules/services/network-filesystems/openafs/server.nix269
-rw-r--r--nixos/modules/services/network-filesystems/orangefs/client.nix96
-rw-r--r--nixos/modules/services/network-filesystems/orangefs/server.nix225
-rw-r--r--nixos/modules/services/network-filesystems/rsyncd.nix128
-rw-r--r--nixos/modules/services/network-filesystems/samba-wsdd.nix124
-rw-r--r--nixos/modules/services/network-filesystems/samba.nix252
-rw-r--r--nixos/modules/services/network-filesystems/tahoe.nix366
-rw-r--r--nixos/modules/services/network-filesystems/u9fs.nix78
-rw-r--r--nixos/modules/services/network-filesystems/webdav-server-rs.nix144
-rw-r--r--nixos/modules/services/network-filesystems/webdav.nix107
-rw-r--r--nixos/modules/services/network-filesystems/xtreemfs.nix495
-rw-r--r--nixos/modules/services/network-filesystems/yandex-disk.nix116
-rw-r--r--nixos/modules/services/networking/3proxy.nix413
-rw-r--r--nixos/modules/services/networking/adguardhome.nix140
-rw-r--r--nixos/modules/services/networking/amuled.nix83
-rw-r--r--nixos/modules/services/networking/antennas.nix80
-rw-r--r--nixos/modules/services/networking/aria2.nix131
-rw-r--r--nixos/modules/services/networking/asterisk.nix264
-rw-r--r--nixos/modules/services/networking/atftpd.nix65
-rw-r--r--nixos/modules/services/networking/autossh.nix113
-rw-r--r--nixos/modules/services/networking/avahi-daemon.nix286
-rw-r--r--nixos/modules/services/networking/babeld.nix144
-rw-r--r--nixos/modules/services/networking/bee-clef.nix107
-rw-r--r--nixos/modules/services/networking/bee.nix149
-rw-r--r--nixos/modules/services/networking/biboumi.nix270
-rw-r--r--nixos/modules/services/networking/bind.nix274
-rw-r--r--nixos/modules/services/networking/bird.nix103
-rw-r--r--nixos/modules/services/networking/bitcoind.nix261
-rw-r--r--nixos/modules/services/networking/bitlbee.nix189
-rw-r--r--nixos/modules/services/networking/blockbook-frontend.nix278
-rw-r--r--nixos/modules/services/networking/blocky.nix40
-rw-r--r--nixos/modules/services/networking/charybdis.nix114
-rw-r--r--nixos/modules/services/networking/cjdns.nix304
-rw-r--r--nixos/modules/services/networking/cntlm.nix126
-rw-r--r--nixos/modules/services/networking/connman.nix162
-rw-r--r--nixos/modules/services/networking/consul.nix259
-rw-r--r--nixos/modules/services/networking/coredns.nix50
-rw-r--r--nixos/modules/services/networking/corerad.nix82
-rw-r--r--nixos/modules/services/networking/coturn.nix365
-rw-r--r--nixos/modules/services/networking/croc.nix86
-rw-r--r--nixos/modules/services/networking/dante.nix62
-rw-r--r--nixos/modules/services/networking/ddclient.nix239
-rw-r--r--nixos/modules/services/networking/dhcpcd.nix250
-rw-r--r--nixos/modules/services/networking/dhcpd.nix221
-rw-r--r--nixos/modules/services/networking/dnscache.nix108
-rw-r--r--nixos/modules/services/networking/dnscrypt-proxy2.nix124
-rw-r--r--nixos/modules/services/networking/dnscrypt-wrapper.nix286
-rw-r--r--nixos/modules/services/networking/dnsdist.nix53
-rw-r--r--nixos/modules/services/networking/dnsmasq.nix130
-rw-r--r--nixos/modules/services/networking/doh-proxy-rust.nix60
-rw-r--r--nixos/modules/services/networking/ejabberd.nix157
-rw-r--r--nixos/modules/services/networking/epmd.nix72
-rw-r--r--nixos/modules/services/networking/ergo.nix143
-rw-r--r--nixos/modules/services/networking/ergochat.nix155
-rw-r--r--nixos/modules/services/networking/eternal-terminal.nix95
-rw-r--r--nixos/modules/services/networking/fakeroute.nix65
-rw-r--r--nixos/modules/services/networking/ferm.nix63
-rw-r--r--nixos/modules/services/networking/fireqos.nix52
-rw-r--r--nixos/modules/services/networking/firewall.nix584
-rw-r--r--nixos/modules/services/networking/flannel.nix192
-rw-r--r--nixos/modules/services/networking/freenet.nix64
-rw-r--r--nixos/modules/services/networking/freeradius.nix86
-rw-r--r--nixos/modules/services/networking/frr.nix211
-rw-r--r--nixos/modules/services/networking/gateone.nix59
-rw-r--r--nixos/modules/services/networking/gdomap.nix29
-rw-r--r--nixos/modules/services/networking/ghostunnel.nix242
-rw-r--r--nixos/modules/services/networking/git-daemon.nix131
-rw-r--r--nixos/modules/services/networking/globalprotect-vpn.nix43
-rw-r--r--nixos/modules/services/networking/gnunet.nix170
-rw-r--r--nixos/modules/services/networking/go-neb.nix79
-rw-r--r--nixos/modules/services/networking/go-shadowsocks2.nix30
-rw-r--r--nixos/modules/services/networking/gobgpd.nix64
-rw-r--r--nixos/modules/services/networking/gvpe.nix130
-rw-r--r--nixos/modules/services/networking/hans.nix145
-rw-r--r--nixos/modules/services/networking/haproxy.nix112
-rw-r--r--nixos/modules/services/networking/headscale.nix490
-rw-r--r--nixos/modules/services/networking/helpers.nix11
-rw-r--r--nixos/modules/services/networking/hostapd.nix219
-rw-r--r--nixos/modules/services/networking/htpdate.nix80
-rw-r--r--nixos/modules/services/networking/hylafax/default.nix31
-rw-r--r--nixos/modules/services/networking/hylafax/faxq-default.nix12
-rwxr-xr-xnixos/modules/services/networking/hylafax/faxq-wait.sh29
-rw-r--r--nixos/modules/services/networking/hylafax/hfaxd-default.nix10
-rw-r--r--nixos/modules/services/networking/hylafax/modem-default.nix22
-rw-r--r--nixos/modules/services/networking/hylafax/options.nix372
-rwxr-xr-xnixos/modules/services/networking/hylafax/spool.sh111
-rw-r--r--nixos/modules/services/networking/hylafax/systemd.nix249
-rw-r--r--nixos/modules/services/networking/i2p.nix34
-rw-r--r--nixos/modules/services/networking/i2pd.nix691
-rw-r--r--nixos/modules/services/networking/icecream/daemon.nix155
-rw-r--r--nixos/modules/services/networking/icecream/scheduler.nix101
-rw-r--r--nixos/modules/services/networking/inspircd.nix62
-rw-r--r--nixos/modules/services/networking/iodine.nix198
-rw-r--r--nixos/modules/services/networking/iperf3.nix97
-rw-r--r--nixos/modules/services/networking/ircd-hybrid/builder.sh31
-rw-r--r--nixos/modules/services/networking/ircd-hybrid/control.in26
-rw-r--r--nixos/modules/services/networking/ircd-hybrid/default.nix133
-rw-r--r--nixos/modules/services/networking/ircd-hybrid/ircd.conf1051
-rw-r--r--nixos/modules/services/networking/iscsi/initiator.nix84
-rw-r--r--nixos/modules/services/networking/iscsi/root-initiator.nix190
-rw-r--r--nixos/modules/services/networking/iscsi/target.nix53
-rw-r--r--nixos/modules/services/networking/iwd.nix62
-rw-r--r--nixos/modules/services/networking/jibri/default.nix417
-rw-r--r--nixos/modules/services/networking/jibri/logging.properties-journal32
-rw-r--r--nixos/modules/services/networking/jicofo.nix152
-rw-r--r--nixos/modules/services/networking/jitsi-videobridge.nix288
-rw-r--r--nixos/modules/services/networking/kea.nix383
-rw-r--r--nixos/modules/services/networking/keepalived/default.nix303
-rw-r--r--nixos/modules/services/networking/keepalived/virtual-ip-options.nix50
-rw-r--r--nixos/modules/services/networking/keepalived/vrrp-instance-options.nix133
-rw-r--r--nixos/modules/services/networking/keepalived/vrrp-script-options.nix64
-rw-r--r--nixos/modules/services/networking/keybase.nix47
-rw-r--r--nixos/modules/services/networking/knot.nix152
-rw-r--r--nixos/modules/services/networking/kresd.nix151
-rw-r--r--nixos/modules/services/networking/lambdabot.nix82
-rw-r--r--nixos/modules/services/networking/libreswan.nix160
-rw-r--r--nixos/modules/services/networking/lldpd.nix39
-rw-r--r--nixos/modules/services/networking/logmein-hamachi.nix50
-rw-r--r--nixos/modules/services/networking/lxd-image-server.nix137
-rw-r--r--nixos/modules/services/networking/magic-wormhole-mailbox-server.nix28
-rw-r--r--nixos/modules/services/networking/matterbridge.nix120
-rw-r--r--nixos/modules/services/networking/minidlna.nix193
-rw-r--r--nixos/modules/services/networking/miniupnpd.nix79
-rw-r--r--nixos/modules/services/networking/miredo.nix92
-rw-r--r--nixos/modules/services/networking/mjpg-streamer.nix80
-rw-r--r--nixos/modules/services/networking/monero.nix244
-rw-r--r--nixos/modules/services/networking/morty.nix98
-rw-r--r--nixos/modules/services/networking/mosquitto.md102
-rw-r--r--nixos/modules/services/networking/mosquitto.nix673
-rw-r--r--nixos/modules/services/networking/mosquitto.xml147
-rw-r--r--nixos/modules/services/networking/mstpd.nix33
-rw-r--r--nixos/modules/services/networking/mtprotoproxy.nix110
-rw-r--r--nixos/modules/services/networking/mtr-exporter.nix87
-rw-r--r--nixos/modules/services/networking/mullvad-vpn.nix50
-rw-r--r--nixos/modules/services/networking/multipath.nix557
-rw-r--r--nixos/modules/services/networking/murmur.nix318
-rw-r--r--nixos/modules/services/networking/mxisd.nix127
-rw-r--r--nixos/modules/services/networking/namecoind.nix199
-rw-r--r--nixos/modules/services/networking/nar-serve.nix55
-rw-r--r--nixos/modules/services/networking/nat.nix364
-rw-r--r--nixos/modules/services/networking/nats.nix158
-rw-r--r--nixos/modules/services/networking/nbd.nix146
-rw-r--r--nixos/modules/services/networking/ncdns.nix283
-rw-r--r--nixos/modules/services/networking/ndppd.nix189
-rw-r--r--nixos/modules/services/networking/nebula.nix217
-rw-r--r--nixos/modules/services/networking/networkmanager.nix568
-rw-r--r--nixos/modules/services/networking/nextdns.nix44
-rw-r--r--nixos/modules/services/networking/nftables.nix131
-rw-r--r--nixos/modules/services/networking/nghttpx/backend-params-submodule.nix131
-rw-r--r--nixos/modules/services/networking/nghttpx/backend-submodule.nix50
-rw-r--r--nixos/modules/services/networking/nghttpx/default.nix118
-rw-r--r--nixos/modules/services/networking/nghttpx/frontend-params-submodule.nix64
-rw-r--r--nixos/modules/services/networking/nghttpx/frontend-submodule.nix36
-rw-r--r--nixos/modules/services/networking/nghttpx/nghttpx-options.nix142
-rw-r--r--nixos/modules/services/networking/nghttpx/server-options.nix18
-rw-r--r--nixos/modules/services/networking/nghttpx/tls-submodule.nix21
-rw-r--r--nixos/modules/services/networking/ngircd.nix62
-rw-r--r--nixos/modules/services/networking/nix-serve.nix91
-rw-r--r--nixos/modules/services/networking/nix-store-gcs-proxy.nix75
-rw-r--r--nixos/modules/services/networking/nixops-dns.nix78
-rw-r--r--nixos/modules/services/networking/nntp-proxy.nix234
-rw-r--r--nixos/modules/services/networking/nomad.nix178
-rw-r--r--nixos/modules/services/networking/nsd.nix992
-rw-r--r--nixos/modules/services/networking/ntopng.nix160
-rw-r--r--nixos/modules/services/networking/ntp/chrony.nix178
-rw-r--r--nixos/modules/services/networking/ntp/ntpd.nix150
-rw-r--r--nixos/modules/services/networking/ntp/openntpd.nix85
-rw-r--r--nixos/modules/services/networking/nullidentdmod.nix34
-rw-r--r--nixos/modules/services/networking/nylon.nix166
-rw-r--r--nixos/modules/services/networking/ocserv.nix99
-rw-r--r--nixos/modules/services/networking/ofono.nix44
-rw-r--r--nixos/modules/services/networking/oidentd.nix44
-rw-r--r--nixos/modules/services/networking/onedrive.nix71
-rw-r--r--nixos/modules/services/networking/onedrive.xml34
-rw-r--r--nixos/modules/services/networking/openfire.nix56
-rw-r--r--nixos/modules/services/networking/openvpn.nix219
-rw-r--r--nixos/modules/services/networking/ostinato.nix104
-rw-r--r--nixos/modules/services/networking/owamp.nix45
-rw-r--r--nixos/modules/services/networking/pdns-recursor.nix206
-rw-r--r--nixos/modules/services/networking/pdnsd.nix91
-rw-r--r--nixos/modules/services/networking/pixiecore.nix135
-rw-r--r--nixos/modules/services/networking/pleroma.nix149
-rw-r--r--nixos/modules/services/networking/pleroma.xml188
-rw-r--r--nixos/modules/services/networking/polipo.nix112
-rw-r--r--nixos/modules/services/networking/powerdns.nix47
-rw-r--r--nixos/modules/services/networking/pppd.nix154
-rw-r--r--nixos/modules/services/networking/pptpd.nix124
-rw-r--r--nixos/modules/services/networking/prayer.nix90
-rw-r--r--nixos/modules/services/networking/privoxy.nix279
-rw-r--r--nixos/modules/services/networking/prosody.nix882
-rw-r--r--nixos/modules/services/networking/prosody.xml87
-rw-r--r--nixos/modules/services/networking/quassel.nix139
-rw-r--r--nixos/modules/services/networking/quicktun.nix118
-rw-r--r--nixos/modules/services/networking/quorum.nix231
-rw-r--r--nixos/modules/services/networking/radicale.nix204
-rw-r--r--nixos/modules/services/networking/radvd.nix77
-rw-r--r--nixos/modules/services/networking/rdnssd.nix82
-rw-r--r--nixos/modules/services/networking/redsocks.nix272
-rw-r--r--nixos/modules/services/networking/resilio.nix265
-rw-r--r--nixos/modules/services/networking/robustirc-bridge.nix47
-rw-r--r--nixos/modules/services/networking/rpcbind.nix46
-rw-r--r--nixos/modules/services/networking/rxe.nix52
-rw-r--r--nixos/modules/services/networking/sabnzbd.nix77
-rw-r--r--nixos/modules/services/networking/seafile.nix287
-rw-r--r--nixos/modules/services/networking/searx.nix231
-rw-r--r--nixos/modules/services/networking/shadowsocks.nix158
-rw-r--r--nixos/modules/services/networking/shairport-sync.nix112
-rw-r--r--nixos/modules/services/networking/shellhub-agent.nix91
-rw-r--r--nixos/modules/services/networking/shorewall.nix70
-rw-r--r--nixos/modules/services/networking/shorewall6.nix70
-rw-r--r--nixos/modules/services/networking/shout.nix115
-rw-r--r--nixos/modules/services/networking/skydns.nix93
-rw-r--r--nixos/modules/services/networking/smartdns.nix62
-rw-r--r--nixos/modules/services/networking/smokeping.nix362
-rw-r--r--nixos/modules/services/networking/sniproxy.nix88
-rw-r--r--nixos/modules/services/networking/snowflake-proxy.nix81
-rw-r--r--nixos/modules/services/networking/softether.nix163
-rw-r--r--nixos/modules/services/networking/soju.nix114
-rw-r--r--nixos/modules/services/networking/solanum.nix109
-rw-r--r--nixos/modules/services/networking/spacecookie.nix216
-rw-r--r--nixos/modules/services/networking/spiped.nix220
-rw-r--r--nixos/modules/services/networking/squid.nix176
-rw-r--r--nixos/modules/services/networking/ssh/lshd.nix189
-rw-r--r--nixos/modules/services/networking/ssh/sshd.nix571
-rw-r--r--nixos/modules/services/networking/sslh.nix168
-rw-r--r--nixos/modules/services/networking/strongswan-swanctl/module.nix84
-rw-r--r--nixos/modules/services/networking/strongswan-swanctl/param-constructors.nix162
-rw-r--r--nixos/modules/services/networking/strongswan-swanctl/param-lib.nix82
-rw-r--r--nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix1310
-rw-r--r--nixos/modules/services/networking/strongswan.nix170
-rw-r--r--nixos/modules/services/networking/stubby.nix89
-rw-r--r--nixos/modules/services/networking/stunnel.nix238
-rw-r--r--nixos/modules/services/networking/supplicant.nix240
-rw-r--r--nixos/modules/services/networking/supybot.nix163
-rw-r--r--nixos/modules/services/networking/syncplay.nix80
-rw-r--r--nixos/modules/services/networking/syncthing-relay.nix121
-rw-r--r--nixos/modules/services/networking/syncthing.nix601
-rw-r--r--nixos/modules/services/networking/tailscale.nix44
-rw-r--r--nixos/modules/services/networking/tcpcrypt.nix80
-rw-r--r--nixos/modules/services/networking/teamspeak3.nix160
-rw-r--r--nixos/modules/services/networking/tedicross.nix100
-rw-r--r--nixos/modules/services/networking/teleport.nix99
-rw-r--r--nixos/modules/services/networking/tetrd.nix96
-rw-r--r--nixos/modules/services/networking/tftpd.nix46
-rw-r--r--nixos/modules/services/networking/thelounge.nix106
-rw-r--r--nixos/modules/services/networking/tinc.nix439
-rw-r--r--nixos/modules/services/networking/tinydns.nix59
-rw-r--r--nixos/modules/services/networking/tox-bootstrapd.nix74
-rw-r--r--nixos/modules/services/networking/tox-node.nix90
-rw-r--r--nixos/modules/services/networking/toxvpn.nix70
-rw-r--r--nixos/modules/services/networking/trickster.nix113
-rw-r--r--nixos/modules/services/networking/tvheadend.nix63
-rw-r--r--nixos/modules/services/networking/ucarp.nix183
-rw-r--r--nixos/modules/services/networking/unbound.nix314
-rw-r--r--nixos/modules/services/networking/unifi.nix202
-rw-r--r--nixos/modules/services/networking/v2ray.nix95
-rw-r--r--nixos/modules/services/networking/vsftpd.nix329
-rw-r--r--nixos/modules/services/networking/wasabibackend.nix160
-rw-r--r--nixos/modules/services/networking/websockify.nix54
-rw-r--r--nixos/modules/services/networking/wg-netmanager.nix42
-rw-r--r--nixos/modules/services/networking/wg-quick.nix304
-rw-r--r--nixos/modules/services/networking/wireguard.nix503
-rw-r--r--nixos/modules/services/networking/wpa_supplicant.nix541
-rw-r--r--nixos/modules/services/networking/x2goserver.nix164
-rw-r--r--nixos/modules/services/networking/xandikos.nix148
-rw-r--r--nixos/modules/services/networking/xinetd.nix147
-rw-r--r--nixos/modules/services/networking/xl2tpd.nix143
-rw-r--r--nixos/modules/services/networking/xrdp.nix185
-rw-r--r--nixos/modules/services/networking/yggdrasil.nix201
-rw-r--r--nixos/modules/services/networking/yggdrasil.xml156
-rw-r--r--nixos/modules/services/networking/zerobin.nix102
-rw-r--r--nixos/modules/services/networking/zeronet.nix94
-rw-r--r--nixos/modules/services/networking/zerotierone.nix81
-rw-r--r--nixos/modules/services/networking/znc/default.nix335
-rw-r--r--nixos/modules/services/networking/znc/options.nix270
-rw-r--r--nixos/modules/services/printing/cupsd.nix460
-rw-r--r--nixos/modules/services/scheduling/atd.nix106
-rw-r--r--nixos/modules/services/scheduling/cron.nix138
-rw-r--r--nixos/modules/services/scheduling/fcron.nix170
-rw-r--r--nixos/modules/services/search/elasticsearch-curator.nix95
-rw-r--r--nixos/modules/services/search/elasticsearch.nix239
-rw-r--r--nixos/modules/services/search/hound.nix127
-rw-r--r--nixos/modules/services/search/kibana.nix213
-rw-r--r--nixos/modules/services/search/meilisearch.md39
-rw-r--r--nixos/modules/services/search/meilisearch.nix132
-rw-r--r--nixos/modules/services/search/meilisearch.xml85
-rw-r--r--nixos/modules/services/search/solr.nix110
-rw-r--r--nixos/modules/services/security/aesmd.nix236
-rw-r--r--nixos/modules/services/security/certmgr.nix201
-rw-r--r--nixos/modules/services/security/cfssl.nix222
-rw-r--r--nixos/modules/services/security/clamav.nix151
-rw-r--r--nixos/modules/services/security/fail2ban.nix340
-rw-r--r--nixos/modules/services/security/fprintd.nix64
-rw-r--r--nixos/modules/services/security/haka.nix156
-rw-r--r--nixos/modules/services/security/haveged.nix77
-rw-r--r--nixos/modules/services/security/hockeypuck.nix106
-rw-r--r--nixos/modules/services/security/hologram-agent.nix58
-rw-r--r--nixos/modules/services/security/hologram-server.nix130
-rw-r--r--nixos/modules/services/security/munge.nix68
-rw-r--r--nixos/modules/services/security/nginx-sso.nix67
-rw-r--r--nixos/modules/services/security/oauth2_proxy.nix591
-rw-r--r--nixos/modules/services/security/oauth2_proxy_nginx.nix66
-rw-r--r--nixos/modules/services/security/opensnitch.nix125
-rw-r--r--nixos/modules/services/security/physlock.nix139
-rw-r--r--nixos/modules/services/security/privacyidea.nix309
-rw-r--r--nixos/modules/services/security/shibboleth-sp.nix75
-rw-r--r--nixos/modules/services/security/sks.nix146
-rw-r--r--nixos/modules/services/security/sshguard.nix161
-rw-r--r--nixos/modules/services/security/step-ca.nix146
-rw-r--r--nixos/modules/services/security/tor.nix1067
-rw-r--r--nixos/modules/services/security/torify.nix80
-rw-r--r--nixos/modules/services/security/torsocks.nix121
-rw-r--r--nixos/modules/services/security/usbguard.nix214
-rw-r--r--nixos/modules/services/security/vault.nix204
-rw-r--r--nixos/modules/services/security/vaultwarden/backup.sh17
-rw-r--r--nixos/modules/services/security/vaultwarden/default.nix185
-rw-r--r--nixos/modules/services/security/yubikey-agent.nix66
-rw-r--r--nixos/modules/services/system/cachix-agent/default.nix57
-rw-r--r--nixos/modules/services/system/cloud-init.nix194
-rw-r--r--nixos/modules/services/system/dbus.nix139
-rw-r--r--nixos/modules/services/system/earlyoom.nix104
-rw-r--r--nixos/modules/services/system/kerberos/default.nix75
-rw-r--r--nixos/modules/services/system/kerberos/heimdal.nix68
-rw-r--r--nixos/modules/services/system/kerberos/mit.nix68
-rw-r--r--nixos/modules/services/system/localtime.nix49
-rw-r--r--nixos/modules/services/system/nscd.conf34
-rw-r--r--nixos/modules/services/system/nscd.nix87
-rw-r--r--nixos/modules/services/system/saslauthd.nix62
-rw-r--r--nixos/modules/services/system/self-deploy.nix173
-rw-r--r--nixos/modules/services/system/systembus-notify.nix27
-rw-r--r--nixos/modules/services/system/uptimed.nix60
-rw-r--r--nixos/modules/services/torrent/deluge.nix279
-rw-r--r--nixos/modules/services/torrent/flexget.nix100
-rw-r--r--nixos/modules/services/torrent/magnetico.nix220
-rw-r--r--nixos/modules/services/torrent/opentracker.nix45
-rw-r--r--nixos/modules/services/torrent/peerflix.nix71
-rw-r--r--nixos/modules/services/torrent/rtorrent.nix211
-rw-r--r--nixos/modules/services/torrent/transmission.nix487
-rw-r--r--nixos/modules/services/ttys/getty.nix162
-rw-r--r--nixos/modules/services/ttys/gpm.nix57
-rw-r--r--nixos/modules/services/ttys/kmscon.nix97
-rw-r--r--nixos/modules/services/video/epgstation/default.nix334
-rw-r--r--nixos/modules/services/video/epgstation/streaming.json140
-rw-r--r--nixos/modules/services/video/mirakurun.nix204
-rw-r--r--nixos/modules/services/video/replay-sorcery.nix72
-rw-r--r--nixos/modules/services/video/rtsp-simple-server.nix80
-rw-r--r--nixos/modules/services/video/unifi-video.nix267
-rw-r--r--nixos/modules/services/wayland/cage.nix104
-rw-r--r--nixos/modules/services/web-apps/atlassian/confluence.nix197
-rw-r--r--nixos/modules/services/web-apps/atlassian/crowd.nix164
-rw-r--r--nixos/modules/services/web-apps/atlassian/jira.nix204
-rw-r--r--nixos/modules/services/web-apps/baget.nix170
-rw-r--r--nixos/modules/services/web-apps/bookstack.nix449
-rw-r--r--nixos/modules/services/web-apps/calibre-web.nix165
-rw-r--r--nixos/modules/services/web-apps/code-server.nix139
-rw-r--r--nixos/modules/services/web-apps/convos.nix72
-rw-r--r--nixos/modules/services/web-apps/cryptpad.nix54
-rw-r--r--nixos/modules/services/web-apps/dex.nix118
-rw-r--r--nixos/modules/services/web-apps/discourse.nix1087
-rw-r--r--nixos/modules/services/web-apps/discourse.xml355
-rw-r--r--nixos/modules/services/web-apps/documize.nix150
-rw-r--r--nixos/modules/services/web-apps/dokuwiki.nix439
-rw-r--r--nixos/modules/services/web-apps/engelsystem.nix186
-rw-r--r--nixos/modules/services/web-apps/ethercalc.nix62
-rw-r--r--nixos/modules/services/web-apps/fluidd.nix66
-rw-r--r--nixos/modules/services/web-apps/galene.nix185
-rw-r--r--nixos/modules/services/web-apps/gerrit.nix242
-rw-r--r--nixos/modules/services/web-apps/gotify-server.nix49
-rw-r--r--nixos/modules/services/web-apps/grocy.nix172
-rw-r--r--nixos/modules/services/web-apps/grocy.xml77
-rw-r--r--nixos/modules/services/web-apps/hedgedoc.nix1038
-rw-r--r--nixos/modules/services/web-apps/hledger-web.nix142
-rw-r--r--nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix262
-rw-r--r--nixos/modules/services/web-apps/icingaweb2/module-monitoring.nix157
-rw-r--r--nixos/modules/services/web-apps/ihatemoney/default.nix153
-rw-r--r--nixos/modules/services/web-apps/invidious.nix264
-rw-r--r--nixos/modules/services/web-apps/invoiceplane.nix305
-rw-r--r--nixos/modules/services/web-apps/isso.nix69
-rw-r--r--nixos/modules/services/web-apps/jirafeau.nix173
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.nix452
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.xml55
-rw-r--r--nixos/modules/services/web-apps/keycloak.nix819
-rw-r--r--nixos/modules/services/web-apps/keycloak.xml222
-rw-r--r--nixos/modules/services/web-apps/lemmy.md34
-rw-r--r--nixos/modules/services/web-apps/lemmy.nix236
-rw-r--r--nixos/modules/services/web-apps/lemmy.xml56
-rw-r--r--nixos/modules/services/web-apps/limesurvey.nix280
-rw-r--r--nixos/modules/services/web-apps/mastodon.nix636
-rw-r--r--nixos/modules/services/web-apps/matomo-doc.xml107
-rw-r--r--nixos/modules/services/web-apps/matomo.nix335
-rw-r--r--nixos/modules/services/web-apps/mattermost.nix344
-rw-r--r--nixos/modules/services/web-apps/mediawiki.nix475
-rw-r--r--nixos/modules/services/web-apps/miniflux.nix127
-rw-r--r--nixos/modules/services/web-apps/moodle.nix315
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix933
-rw-r--r--nixos/modules/services/web-apps/nextcloud.xml291
-rw-r--r--nixos/modules/services/web-apps/nexus.nix156
-rw-r--r--nixos/modules/services/web-apps/node-red.nix149
-rw-r--r--nixos/modules/services/web-apps/openwebrx.nix34
-rw-r--r--nixos/modules/services/web-apps/peertube.nix475
-rw-r--r--nixos/modules/services/web-apps/pgpkeyserver-lite.nix78
-rw-r--r--nixos/modules/services/web-apps/pict-rs.md88
-rw-r--r--nixos/modules/services/web-apps/pict-rs.nix50
-rw-r--r--nixos/modules/services/web-apps/pict-rs.xml162
-rw-r--r--nixos/modules/services/web-apps/plantuml-server.nix140
-rw-r--r--nixos/modules/services/web-apps/plausible.nix292
-rw-r--r--nixos/modules/services/web-apps/plausible.xml51
-rw-r--r--nixos/modules/services/web-apps/powerdns-admin.nix152
-rw-r--r--nixos/modules/services/web-apps/prosody-filer.nix86
-rw-r--r--nixos/modules/services/web-apps/restya-board.nix380
-rw-r--r--nixos/modules/services/web-apps/rss-bridge.nix125
-rw-r--r--nixos/modules/services/web-apps/selfoss.nix164
-rw-r--r--nixos/modules/services/web-apps/shiori.nix96
-rw-r--r--nixos/modules/services/web-apps/sogo.nix271
-rw-r--r--nixos/modules/services/web-apps/timetagger.nix80
-rw-r--r--nixos/modules/services/web-apps/trilium.nix146
-rw-r--r--nixos/modules/services/web-apps/tt-rss.nix686
-rw-r--r--nixos/modules/services/web-apps/vikunja.nix145
-rw-r--r--nixos/modules/services/web-apps/virtlyst.nix73
-rw-r--r--nixos/modules/services/web-apps/whitebophir.nix52
-rw-r--r--nixos/modules/services/web-apps/wiki-js.nix139
-rw-r--r--nixos/modules/services/web-apps/wordpress.nix480
-rw-r--r--nixos/modules/services/web-apps/youtrack.nix181
-rw-r--r--nixos/modules/services/web-apps/zabbix.nix238
-rw-r--r--nixos/modules/services/web-servers/agate.nix148
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix839
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/location-options.nix54
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/vhost-options.nix295
-rw-r--r--nixos/modules/services/web-servers/caddy/default.nix339
-rw-r--r--nixos/modules/services/web-servers/caddy/vhost-options.nix79
-rw-r--r--nixos/modules/services/web-servers/darkhttpd.nix77
-rw-r--r--nixos/modules/services/web-servers/fcgiwrap.nix72
-rw-r--r--nixos/modules/services/web-servers/hitch/default.nix111
-rw-r--r--nixos/modules/services/web-servers/hydron.nix165
-rw-r--r--nixos/modules/services/web-servers/jboss/builder.sh72
-rw-r--r--nixos/modules/services/web-servers/jboss/default.nix88
-rw-r--r--nixos/modules/services/web-servers/lighttpd/cgit.nix93
-rw-r--r--nixos/modules/services/web-servers/lighttpd/collectd.nix62
-rw-r--r--nixos/modules/services/web-servers/lighttpd/default.nix268
-rw-r--r--nixos/modules/services/web-servers/lighttpd/gitweb.nix52
-rw-r--r--nixos/modules/services/web-servers/mighttpd2.nix132
-rw-r--r--nixos/modules/services/web-servers/minio.nix130
-rw-r--r--nixos/modules/services/web-servers/molly-brown.nix101
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix1005
-rw-r--r--nixos/modules/services/web-servers/nginx/gitweb.nix94
-rw-r--r--nixos/modules/services/web-servers/nginx/location-options.nix132
-rw-r--r--nixos/modules/services/web-servers/nginx/vhost-options.nix288
-rw-r--r--nixos/modules/services/web-servers/phpfpm/default.nix282
-rw-r--r--nixos/modules/services/web-servers/pomerium.nix135
-rw-r--r--nixos/modules/services/web-servers/tomcat.nix423
-rw-r--r--nixos/modules/services/web-servers/traefik.nix170
-rw-r--r--nixos/modules/services/web-servers/trafficserver/default.nix310
-rw-r--r--nixos/modules/services/web-servers/trafficserver/ip_allow.json36
-rw-r--r--nixos/modules/services/web-servers/trafficserver/logging.json37
-rw-r--r--nixos/modules/services/web-servers/ttyd.nix196
-rw-r--r--nixos/modules/services/web-servers/unit/default.nix155
-rw-r--r--nixos/modules/services/web-servers/uwsgi.nix229
-rw-r--r--nixos/modules/services/web-servers/varnish/default.nix115
-rw-r--r--nixos/modules/services/web-servers/zope2.nix262
-rw-r--r--nixos/modules/services/x11/clight.nix131
-rw-r--r--nixos/modules/services/x11/colord.nix41
-rw-r--r--nixos/modules/services/x11/desktop-managers/cde.nix73
-rw-r--r--nixos/modules/services/x11/desktop-managers/cinnamon.nix218
-rw-r--r--nixos/modules/services/x11/desktop-managers/default.nix99
-rw-r--r--nixos/modules/services/x11/desktop-managers/enlightenment.nix119
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome.nix608
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome.xml253
-rw-r--r--nixos/modules/services/x11/desktop-managers/kodi.nix41
-rw-r--r--nixos/modules/services/x11/desktop-managers/lumina.nix42
-rw-r--r--nixos/modules/services/x11/desktop-managers/lxqt.nix67
-rw-r--r--nixos/modules/services/x11/desktop-managers/mate.nix110
-rw-r--r--nixos/modules/services/x11/desktop-managers/none.nix7
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.nix316
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.xml120
-rw-r--r--nixos/modules/services/x11/desktop-managers/plasma5.nix579
-rw-r--r--nixos/modules/services/x11/desktop-managers/retroarch.nix40
-rw-r--r--nixos/modules/services/x11/desktop-managers/surf-display.nix128
-rw-r--r--nixos/modules/services/x11/desktop-managers/xfce.nix172
-rw-r--r--nixos/modules/services/x11/desktop-managers/xterm.nix38
-rw-r--r--nixos/modules/services/x11/display-managers/account-service-util.nix44
-rw-r--r--nixos/modules/services/x11/display-managers/default.nix503
-rw-r--r--nixos/modules/services/x11/display-managers/gdm.nix331
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/enso-os.nix140
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix174
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/mini.nix100
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix49
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/tiny.nix92
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm.nix328
-rw-r--r--nixos/modules/services/x11/display-managers/sddm.nix288
-rwxr-xr-xnixos/modules/services/x11/display-managers/set-session.py89
-rw-r--r--nixos/modules/services/x11/display-managers/slim.nix16
-rw-r--r--nixos/modules/services/x11/display-managers/startx.nix54
-rw-r--r--nixos/modules/services/x11/display-managers/sx.nix34
-rw-r--r--nixos/modules/services/x11/display-managers/xpra.nix252
-rw-r--r--nixos/modules/services/x11/extra-layouts.nix135
-rw-r--r--nixos/modules/services/x11/fractalart.nix36
-rw-r--r--nixos/modules/services/x11/gdk-pixbuf.nix45
-rw-r--r--nixos/modules/services/x11/hardware/cmt.nix59
-rw-r--r--nixos/modules/services/x11/hardware/digimend.nix38
-rw-r--r--nixos/modules/services/x11/hardware/libinput.nix291
-rw-r--r--nixos/modules/services/x11/hardware/synaptics.nix218
-rw-r--r--nixos/modules/services/x11/hardware/wacom.nix48
-rw-r--r--nixos/modules/services/x11/imwheel.nix71
-rw-r--r--nixos/modules/services/x11/picom.nix335
-rw-r--r--nixos/modules/services/x11/redshift.nix138
-rw-r--r--nixos/modules/services/x11/terminal-server.nix56
-rw-r--r--nixos/modules/services/x11/touchegg.nix38
-rw-r--r--nixos/modules/services/x11/unclutter-xfixes.nix58
-rw-r--r--nixos/modules/services/x11/unclutter.nix82
-rw-r--r--nixos/modules/services/x11/urserver.nix38
-rw-r--r--nixos/modules/services/x11/urxvtd.nix50
-rw-r--r--nixos/modules/services/x11/window-managers/2bwm.nix37
-rw-r--r--nixos/modules/services/x11/window-managers/afterstep.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/awesome.nix66
-rw-r--r--nixos/modules/services/x11/window-managers/berry.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/bspwm.nix77
-rw-r--r--nixos/modules/services/x11/window-managers/clfswm.nix34
-rw-r--r--nixos/modules/services/x11/window-managers/cwm.nix23
-rw-r--r--nixos/modules/services/x11/window-managers/default.nix88
-rw-r--r--nixos/modules/services/x11/window-managers/dwm.nix37
-rw-r--r--nixos/modules/services/x11/window-managers/e16.nix26
-rw-r--r--nixos/modules/services/x11/window-managers/evilwm.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/exwm.nix69
-rw-r--r--nixos/modules/services/x11/window-managers/fluxbox.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/fvwm.nix41
-rw-r--r--nixos/modules/services/x11/window-managers/herbstluftwm.nix47
-rw-r--r--nixos/modules/services/x11/window-managers/i3.nix78
-rw-r--r--nixos/modules/services/x11/window-managers/icewm.nix27
-rw-r--r--nixos/modules/services/x11/window-managers/jwm.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/leftwm.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/lwm.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/metacity.nix30
-rw-r--r--nixos/modules/services/x11/window-managers/mlvwm.nix41
-rw-r--r--nixos/modules/services/x11/window-managers/mwm.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/none.nix12
-rw-r--r--nixos/modules/services/x11/window-managers/notion.nix26
-rw-r--r--nixos/modules/services/x11/window-managers/openbox.nix24
-rw-r--r--nixos/modules/services/x11/window-managers/oroborus.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/pekwm.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/qtile.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/ratpoison.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/sawfish.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/smallwm.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/spectrwm.nix27
-rw-r--r--nixos/modules/services/x11/window-managers/stumpwm.nix24
-rw-r--r--nixos/modules/services/x11/window-managers/tinywm.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/twm.nix37
-rw-r--r--nixos/modules/services/x11/window-managers/windowlab.nix22
-rw-r--r--nixos/modules/services/x11/window-managers/windowmaker.nix25
-rw-r--r--nixos/modules/services/x11/window-managers/wmderland.nix61
-rw-r--r--nixos/modules/services/x11/window-managers/wmii.nix39
-rw-r--r--nixos/modules/services/x11/window-managers/xmonad.nix203
-rw-r--r--nixos/modules/services/x11/window-managers/yeahwm.nix25
-rw-r--r--nixos/modules/services/x11/xautolock.nix141
-rw-r--r--nixos/modules/services/x11/xbanish.nix31
-rw-r--r--nixos/modules/services/x11/xfs.conf15
-rw-r--r--nixos/modules/services/x11/xfs.nix46
-rw-r--r--nixos/modules/services/x11/xserver.nix867
-rw-r--r--nixos/modules/system/activation/activation-script.nix272
-rw-r--r--nixos/modules/system/activation/no-clone.nix8
-rwxr-xr-xnixos/modules/system/activation/switch-to-configuration.pl856
-rw-r--r--nixos/modules/system/activation/top-level.nix355
-rw-r--r--nixos/modules/system/boot/binfmt.nix325
-rw-r--r--nixos/modules/system/boot/emergency-mode.nix37
-rw-r--r--nixos/modules/system/boot/grow-partition.nix53
-rw-r--r--nixos/modules/system/boot/initrd-network.nix148
-rw-r--r--nixos/modules/system/boot/initrd-openvpn.nix81
-rw-r--r--nixos/modules/system/boot/initrd-ssh.nix215
-rw-r--r--nixos/modules/system/boot/kernel.nix350
-rw-r--r--nixos/modules/system/boot/kernel_config.nix117
-rw-r--r--nixos/modules/system/boot/kexec.nix32
-rw-r--r--nixos/modules/system/boot/loader/efi.nix20
-rw-r--r--nixos/modules/system/boot/loader/generations-dir/generations-dir-builder.sh106
-rw-r--r--nixos/modules/system/boot/loader/generations-dir/generations-dir.nix62
-rw-r--r--nixos/modules/system/boot/loader/generic-extlinux-compatible/default.nix82
-rw-r--r--nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.nix8
-rw-r--r--nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh157
-rw-r--r--nixos/modules/system/boot/loader/grub/grub.nix848
-rw-r--r--nixos/modules/system/boot/loader/grub/install-grub.pl780
-rw-r--r--nixos/modules/system/boot/loader/grub/ipxe.nix64
-rw-r--r--nixos/modules/system/boot/loader/grub/memtest.nix116
-rw-r--r--nixos/modules/system/boot/loader/init-script/init-script-builder.sh92
-rw-r--r--nixos/modules/system/boot/loader/init-script/init-script.nix51
-rw-r--r--nixos/modules/system/boot/loader/loader.nix20
-rw-r--r--nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.nix9
-rw-r--r--nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.sh143
-rw-r--r--nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix105
-rw-r--r--nixos/modules/system/boot/loader/raspberrypi/uboot-builder.nix37
-rw-r--r--nixos/modules/system/boot/loader/raspberrypi/uboot-builder.sh38
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py316
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix303
-rw-r--r--nixos/modules/system/boot/luksroot.nix941
-rw-r--r--nixos/modules/system/boot/modprobe.nix70
-rw-r--r--nixos/modules/system/boot/networkd.nix1797
-rw-r--r--nixos/modules/system/boot/pbkdf2-sha512.c38
-rw-r--r--nixos/modules/system/boot/plymouth.nix237
-rw-r--r--nixos/modules/system/boot/resolved.nix183
-rw-r--r--nixos/modules/system/boot/shutdown.nix27
-rw-r--r--nixos/modules/system/boot/stage-1-init.sh642
-rw-r--r--nixos/modules/system/boot/stage-1.nix717
-rwxr-xr-xnixos/modules/system/boot/stage-2-init.sh176
-rw-r--r--nixos/modules/system/boot/stage-2.nix108
-rw-r--r--nixos/modules/system/boot/systemd-nspawn.nix125
-rw-r--r--nixos/modules/system/boot/systemd.nix1064
-rw-r--r--nixos/modules/system/boot/timesyncd.nix74
-rw-r--r--nixos/modules/system/boot/tmp.nix64
-rw-r--r--nixos/modules/system/build.nix21
-rw-r--r--nixos/modules/system/etc/etc-activation.nix12
-rw-r--r--nixos/modules/system/etc/etc.nix201
-rw-r--r--nixos/modules/system/etc/setup-etc.pl146
-rw-r--r--nixos/modules/system/etc/test.nix70
-rw-r--r--nixos/modules/tasks/auto-upgrade.nix228
-rw-r--r--nixos/modules/tasks/bcache.nix13
-rw-r--r--nixos/modules/tasks/cpu-freq.nix90
-rw-r--r--nixos/modules/tasks/encrypted-devices.nix87
-rw-r--r--nixos/modules/tasks/filesystems.nix385
-rw-r--r--nixos/modules/tasks/filesystems/apfs.nix22
-rw-r--r--nixos/modules/tasks/filesystems/bcachefs.nix65
-rw-r--r--nixos/modules/tasks/filesystems/btrfs.nix149
-rw-r--r--nixos/modules/tasks/filesystems/cifs.nix25
-rw-r--r--nixos/modules/tasks/filesystems/ecryptfs.nix24
-rw-r--r--nixos/modules/tasks/filesystems/exfat.nix13
-rw-r--r--nixos/modules/tasks/filesystems/ext.nix22
-rw-r--r--nixos/modules/tasks/filesystems/f2fs.nix25
-rw-r--r--nixos/modules/tasks/filesystems/glusterfs.nix11
-rw-r--r--nixos/modules/tasks/filesystems/jfs.nix19
-rw-r--r--nixos/modules/tasks/filesystems/nfs.nix135
-rw-r--r--nixos/modules/tasks/filesystems/ntfs.nix11
-rw-r--r--nixos/modules/tasks/filesystems/reiserfs.nix25
-rw-r--r--nixos/modules/tasks/filesystems/unionfs-fuse.nix32
-rw-r--r--nixos/modules/tasks/filesystems/vboxsf.nix23
-rw-r--r--nixos/modules/tasks/filesystems/vfat.nix25
-rw-r--r--nixos/modules/tasks/filesystems/xfs.nix30
-rw-r--r--nixos/modules/tasks/filesystems/zfs.nix795
-rw-r--r--nixos/modules/tasks/lvm.nix84
-rw-r--r--nixos/modules/tasks/network-interfaces-scripted.nix625
-rw-r--r--nixos/modules/tasks/network-interfaces-systemd.nix406
-rw-r--r--nixos/modules/tasks/network-interfaces.nix1537
-rw-r--r--nixos/modules/tasks/powertop.nix29
-rw-r--r--nixos/modules/tasks/scsi-link-power-management.nix54
-rw-r--r--nixos/modules/tasks/snapraid.nix230
-rw-r--r--nixos/modules/tasks/swraid.nix17
-rw-r--r--nixos/modules/tasks/trackpoint.nix108
-rw-r--r--nixos/modules/tasks/tty-backgrounds-combine.sh32
-rw-r--r--nixos/modules/testing/minimal-kernel.nix28
-rw-r--r--nixos/modules/testing/service-runner.nix127
-rw-r--r--nixos/modules/testing/test-instrumentation.nix141
-rw-r--r--nixos/modules/virtualisation/amazon-ec2-amis.nix444
-rw-r--r--nixos/modules/virtualisation/amazon-image.nix180
-rw-r--r--nixos/modules/virtualisation/amazon-init.nix87
-rw-r--r--nixos/modules/virtualisation/amazon-options.nix74
-rw-r--r--nixos/modules/virtualisation/anbox.nix138
-rw-r--r--nixos/modules/virtualisation/azure-agent-entropy.patch17
-rw-r--r--nixos/modules/virtualisation/azure-agent.nix198
-rw-r--r--nixos/modules/virtualisation/azure-bootstrap-blobs.nix3
-rw-r--r--nixos/modules/virtualisation/azure-common.nix66
-rw-r--r--nixos/modules/virtualisation/azure-config-user.nix12
-rw-r--r--nixos/modules/virtualisation/azure-config.nix5
-rw-r--r--nixos/modules/virtualisation/azure-image.nix71
-rw-r--r--nixos/modules/virtualisation/azure-images.nix5
-rw-r--r--nixos/modules/virtualisation/brightbox-config.nix5
-rw-r--r--nixos/modules/virtualisation/brightbox-image.nix166
-rw-r--r--nixos/modules/virtualisation/build-vm.nix58
-rw-r--r--nixos/modules/virtualisation/cloudstack-config.nix40
-rw-r--r--nixos/modules/virtualisation/container-config.nix31
-rw-r--r--nixos/modules/virtualisation/containerd.nix101
-rw-r--r--nixos/modules/virtualisation/containers.nix156
-rw-r--r--nixos/modules/virtualisation/cri-o.nix163
-rw-r--r--nixos/modules/virtualisation/digital-ocean-config.nix197
-rw-r--r--nixos/modules/virtualisation/digital-ocean-image.nix70
-rw-r--r--nixos/modules/virtualisation/digital-ocean-init.nix95
-rw-r--r--nixos/modules/virtualisation/docker-image.nix57
-rw-r--r--nixos/modules/virtualisation/docker-rootless.nix102
-rw-r--r--nixos/modules/virtualisation/docker.nix252
-rw-r--r--nixos/modules/virtualisation/ec2-amis.nix9
-rw-r--r--nixos/modules/virtualisation/ec2-data.nix91
-rw-r--r--nixos/modules/virtualisation/ec2-metadata-fetcher.nix77
-rw-r--r--nixos/modules/virtualisation/ecs-agent.nix45
-rw-r--r--nixos/modules/virtualisation/gce-images.nix17
-rw-r--r--nixos/modules/virtualisation/google-compute-config.nix102
-rw-r--r--nixos/modules/virtualisation/google-compute-image.nix71
-rw-r--r--nixos/modules/virtualisation/grow-partition.nix3
-rw-r--r--nixos/modules/virtualisation/hyperv-guest.nix66
-rw-r--r--nixos/modules/virtualisation/hyperv-image.nix71
-rw-r--r--nixos/modules/virtualisation/kubevirt.nix30
-rw-r--r--nixos/modules/virtualisation/kvmgt.nix86
-rw-r--r--nixos/modules/virtualisation/libvirtd.nix392
-rw-r--r--nixos/modules/virtualisation/lxc-container.nix174
-rw-r--r--nixos/modules/virtualisation/lxc.nix86
-rw-r--r--nixos/modules/virtualisation/lxcfs.nix45
-rw-r--r--nixos/modules/virtualisation/lxd.nix182
-rw-r--r--nixos/modules/virtualisation/nixos-containers.nix866
-rw-r--r--nixos/modules/virtualisation/oci-containers.nix369
-rw-r--r--nixos/modules/virtualisation/openstack-config.nix58
-rw-r--r--nixos/modules/virtualisation/openstack-metadata-fetcher.nix22
-rw-r--r--nixos/modules/virtualisation/openvswitch.nix145
-rw-r--r--nixos/modules/virtualisation/parallels-guest.nix155
-rw-r--r--nixos/modules/virtualisation/podman/default.nix184
-rw-r--r--nixos/modules/virtualisation/podman/dnsname.nix36
-rw-r--r--nixos/modules/virtualisation/podman/network-socket-ghostunnel.nix34
-rw-r--r--nixos/modules/virtualisation/podman/network-socket.nix95
-rw-r--r--nixos/modules/virtualisation/proxmox-image.nix169
-rw-r--r--nixos/modules/virtualisation/proxmox-lxc.nix64
-rw-r--r--nixos/modules/virtualisation/qemu-guest-agent.nix45
-rw-r--r--nixos/modules/virtualisation/qemu-vm.nix1016
-rw-r--r--nixos/modules/virtualisation/railcar.nix124
-rw-r--r--nixos/modules/virtualisation/spice-usb-redirection.nix26
-rw-r--r--nixos/modules/virtualisation/vagrant-guest.nix58
-rw-r--r--nixos/modules/virtualisation/vagrant-virtualbox-image.nix60
-rw-r--r--nixos/modules/virtualisation/virtualbox-guest.nix93
-rw-r--r--nixos/modules/virtualisation/virtualbox-host.nix168
-rw-r--r--nixos/modules/virtualisation/virtualbox-image.nix215
-rw-r--r--nixos/modules/virtualisation/vmware-guest.nix86
-rw-r--r--nixos/modules/virtualisation/vmware-image.nix91
-rw-r--r--nixos/modules/virtualisation/waydroid.nix73
-rw-r--r--nixos/modules/virtualisation/xe-guest-utilities.nix52
-rw-r--r--nixos/modules/virtualisation/xen-dom0.nix453
-rw-r--r--nixos/modules/virtualisation/xen-domU.nix19
-rw-r--r--nixos/release-combined.nix160
-rw-r--r--nixos/release-small.nix127
-rw-r--r--nixos/release.nix386
-rw-r--r--nixos/tests/3proxy.nix189
-rw-r--r--nixos/tests/acme.nix597
-rw-r--r--nixos/tests/adguardhome.nix57
-rw-r--r--nixos/tests/aesmd.nix62
-rw-r--r--nixos/tests/agda.nix50
-rw-r--r--nixos/tests/airsonic.nix28
-rw-r--r--nixos/tests/all-tests.nix590
-rw-r--r--nixos/tests/amazon-init-shell.nix40
-rw-r--r--nixos/tests/apfs.nix54
-rw-r--r--nixos/tests/apparmor.nix82
-rw-r--r--nixos/tests/atd.nix31
-rw-r--r--nixos/tests/atop.nix234
-rw-r--r--nixos/tests/avahi.nix79
-rw-r--r--nixos/tests/babeld.nix142
-rw-r--r--nixos/tests/bazarr.nix26
-rw-r--r--nixos/tests/bcachefs.nix33
-rw-r--r--nixos/tests/beanstalkd.nix49
-rw-r--r--nixos/tests/bees.nix62
-rw-r--r--nixos/tests/bind.nix28
-rw-r--r--nixos/tests/bird.nix129
-rw-r--r--nixos/tests/bitcoind.nix46
-rw-r--r--nixos/tests/bittorrent.nix164
-rw-r--r--nixos/tests/blockbook-frontend.nix28
-rw-r--r--nixos/tests/blocky.nix34
-rw-r--r--nixos/tests/boot-stage1.nix164
-rw-r--r--nixos/tests/boot.nix149
-rw-r--r--nixos/tests/borgbackup.nix208
-rw-r--r--nixos/tests/botamusique.nix47
-rw-r--r--nixos/tests/bpf.nix29
-rw-r--r--nixos/tests/breitbandmessung.nix33
-rw-r--r--nixos/tests/brscan5.nix43
-rw-r--r--nixos/tests/btrbk.nix110
-rw-r--r--nixos/tests/buildbot.nix113
-rw-r--r--nixos/tests/buildkite-agents.nix31
-rw-r--r--nixos/tests/caddy.nix107
-rw-r--r--nixos/tests/cadvisor.nix34
-rw-r--r--nixos/tests/cage.nix36
-rw-r--r--nixos/tests/cagebreak.nix64
-rw-r--r--nixos/tests/calibre-web.nix43
-rw-r--r--nixos/tests/cassandra.nix132
-rw-r--r--nixos/tests/ceph-multi-node.nix233
-rw-r--r--nixos/tests/ceph-single-node-bluestore.nix196
-rw-r--r--nixos/tests/ceph-single-node.nix196
-rw-r--r--nixos/tests/certmgr.nix155
-rw-r--r--nixos/tests/cfssl.nix67
-rw-r--r--nixos/tests/charliecloud.nix43
-rw-r--r--nixos/tests/chromium.nix258
-rw-r--r--nixos/tests/cjdns.nix121
-rw-r--r--nixos/tests/clickhouse.nix32
-rw-r--r--nixos/tests/cloud-init.nix109
-rw-r--r--nixos/tests/cntr.nix75
-rw-r--r--nixos/tests/cockroachdb.nix124
-rw-r--r--nixos/tests/collectd.nix33
-rw-r--r--nixos/tests/common/acme/client/default.nix16
-rw-r--r--nixos/tests/common/acme/server/README.md21
-rw-r--r--nixos/tests/common/acme/server/acme.test.cert.pem19
-rw-r--r--nixos/tests/common/acme/server/acme.test.key.pem27
-rw-r--r--nixos/tests/common/acme/server/ca.cert.pem20
-rw-r--r--nixos/tests/common/acme/server/ca.key.pem27
-rw-r--r--nixos/tests/common/acme/server/default.nix141
-rw-r--r--nixos/tests/common/acme/server/generate-certs.nix29
-rw-r--r--nixos/tests/common/acme/server/snakeoil-certs.nix13
-rw-r--r--nixos/tests/common/auto.nix68
-rw-r--r--nixos/tests/common/ec2.nix66
-rw-r--r--nixos/tests/common/resolver.nix141
-rw-r--r--nixos/tests/common/user-account.nix15
-rw-r--r--nixos/tests/common/wayland-cage.nix13
-rw-r--r--nixos/tests/common/webroot/news-rss.xml27
-rw-r--r--nixos/tests/common/x11.nix17
-rw-r--r--nixos/tests/consul.nix229
-rw-r--r--nixos/tests/containers-bridge.nix99
-rw-r--r--nixos/tests/containers-custom-pkgs.nix34
-rw-r--r--nixos/tests/containers-ephemeral.nix54
-rw-r--r--nixos/tests/containers-extra_veth.nix91
-rw-r--r--nixos/tests/containers-hosts.nix49
-rw-r--r--nixos/tests/containers-imperative.nix166
-rw-r--r--nixos/tests/containers-ip.nix74
-rw-r--r--nixos/tests/containers-macvlans.nix82
-rw-r--r--nixos/tests/containers-names.nix37
-rw-r--r--nixos/tests/containers-nested.nix30
-rw-r--r--nixos/tests/containers-physical_interfaces.nix131
-rw-r--r--nixos/tests/containers-portforward.nix59
-rw-r--r--nixos/tests/containers-reloadable.nix71
-rw-r--r--nixos/tests/containers-restart_networking.nix113
-rw-r--r--nixos/tests/containers-tmpfs.nix90
-rw-r--r--nixos/tests/convos.nix30
-rw-r--r--nixos/tests/corerad.nix89
-rw-r--r--nixos/tests/coturn.nix29
-rw-r--r--nixos/tests/couchdb.nix63
-rw-r--r--nixos/tests/cri-o.nix19
-rw-r--r--nixos/tests/croc.nix51
-rw-r--r--nixos/tests/cryptpad.nix18
-rw-r--r--nixos/tests/custom-ca.nix179
-rw-r--r--nixos/tests/deluge.nix61
-rw-r--r--nixos/tests/dendrite.nix99
-rw-r--r--nixos/tests/dex-oidc.nix78
-rw-r--r--nixos/tests/dhparams.nix142
-rw-r--r--nixos/tests/disable-installer-tools.nix29
-rw-r--r--nixos/tests/discourse.nix201
-rw-r--r--nixos/tests/dnscrypt-proxy2.nix36
-rw-r--r--nixos/tests/dnscrypt-wrapper/default.nix72
-rw-r--r--nixos/tests/dnscrypt-wrapper/public.key1
-rw-r--r--nixos/tests/dnscrypt-wrapper/secret.key1
-rw-r--r--nixos/tests/dnsdist.nix48
-rw-r--r--nixos/tests/doas.nix98
-rw-r--r--nixos/tests/docker-edge.nix49
-rw-r--r--nixos/tests/docker-registry.nix61
-rw-r--r--nixos/tests/docker-rootless.nix41
-rw-r--r--nixos/tests/docker-tools-cross.nix76
-rw-r--r--nixos/tests/docker-tools-overlay.nix33
-rw-r--r--nixos/tests/docker-tools.nix423
-rw-r--r--nixos/tests/docker.nix52
-rw-r--r--nixos/tests/documize.nix62
-rw-r--r--nixos/tests/doh-proxy-rust.nix43
-rw-r--r--nixos/tests/dokuwiki.nix111
-rw-r--r--nixos/tests/domination.nix26
-rw-r--r--nixos/tests/dovecot.nix82
-rw-r--r--nixos/tests/drbd.nix87
-rw-r--r--nixos/tests/ec2.nix158
-rw-r--r--nixos/tests/ecryptfs.nix85
-rw-r--r--nixos/tests/elk.nix305
-rw-r--r--nixos/tests/emacs-daemon.nix48
-rw-r--r--nixos/tests/empty-file0
-rw-r--r--nixos/tests/engelsystem.nix41
-rw-r--r--nixos/tests/enlightenment.nix96
-rw-r--r--nixos/tests/env.nix36
-rw-r--r--nixos/tests/ergo.nix18
-rw-r--r--nixos/tests/ergochat.nix97
-rw-r--r--nixos/tests/etcd-cluster.nix154
-rw-r--r--nixos/tests/etcd.nix25
-rw-r--r--nixos/tests/etebase-server.nix50
-rw-r--r--nixos/tests/etesync-dav.nix21
-rw-r--r--nixos/tests/fancontrol.nix34
-rw-r--r--nixos/tests/fcitx/config12
-rw-r--r--nixos/tests/fcitx/default.nix141
-rw-r--r--nixos/tests/fcitx/profile4
-rw-r--r--nixos/tests/fenics.nix49
-rw-r--r--nixos/tests/ferm.nix75
-rw-r--r--nixos/tests/firefox.nix116
-rw-r--r--nixos/tests/firejail.nix91
-rw-r--r--nixos/tests/firewall.nix65
-rw-r--r--nixos/tests/fish.nix24
-rw-r--r--nixos/tests/flannel.nix57
-rw-r--r--nixos/tests/fluentd.nix49
-rw-r--r--nixos/tests/fluidd.nix21
-rw-r--r--nixos/tests/fontconfig-default-fonts.nix32
-rw-r--r--nixos/tests/freeswitch.nix29
-rw-r--r--nixos/tests/frr.nix104
-rw-r--r--nixos/tests/fsck.nix31
-rw-r--r--nixos/tests/ft2-clone.nix35
-rw-r--r--nixos/tests/gerrit.nix54
-rw-r--r--nixos/tests/geth.nix41
-rw-r--r--nixos/tests/ghostunnel.nix103
-rw-r--r--nixos/tests/git/hub.nix17
-rw-r--r--nixos/tests/gitdaemon.nix71
-rw-r--r--nixos/tests/gitea.nix110
-rw-r--r--nixos/tests/gitlab.nix159
-rw-r--r--nixos/tests/gitolite-fcgiwrap.nix93
-rw-r--r--nixos/tests/gitolite.nix138
-rw-r--r--nixos/tests/glusterfs.nix68
-rw-r--r--nixos/tests/gnome-xorg.nix95
-rw-r--r--nixos/tests/gnome.nix96
-rw-r--r--nixos/tests/go-neb.nix44
-rw-r--r--nixos/tests/gobgpd.nix71
-rw-r--r--nixos/tests/gocd-agent.nix48
-rw-r--r--nixos/tests/gocd-server.nix28
-rw-r--r--nixos/tests/google-oslogin/default.nix74
-rw-r--r--nixos/tests/google-oslogin/server.nix27
-rwxr-xr-xnixos/tests/google-oslogin/server.py135
-rw-r--r--nixos/tests/gotify-server.nix50
-rw-r--r--nixos/tests/grafana.nix109
-rw-r--r--nixos/tests/graphite.nix48
-rw-r--r--nixos/tests/graylog.nix115
-rw-r--r--nixos/tests/grocy.nix47
-rw-r--r--nixos/tests/grub.nix60
-rw-r--r--nixos/tests/gvisor.nix49
-rw-r--r--nixos/tests/hadoop/default.nix7
-rw-r--r--nixos/tests/hadoop/hadoop.nix255
-rw-r--r--nixos/tests/hadoop/hdfs.nix84
-rw-r--r--nixos/tests/hadoop/yarn.nix45
-rw-r--r--nixos/tests/haka.nix24
-rw-r--r--nixos/tests/haproxy.nix54
-rw-r--r--nixos/tests/hardened.nix101
-rw-r--r--nixos/tests/hedgedoc.nix60
-rw-r--r--nixos/tests/herbstluftwm.nix37
-rw-r--r--nixos/tests/hibernate.nix122
-rw-r--r--nixos/tests/hitch/default.nix33
-rw-r--r--nixos/tests/hitch/example.pem53
-rw-r--r--nixos/tests/hitch/example/index.txt1
-rw-r--r--nixos/tests/hledger-web.nix50
-rw-r--r--nixos/tests/hocker-fetchdocker/default.nix16
-rw-r--r--nixos/tests/hocker-fetchdocker/hello-world-container.nix19
-rw-r--r--nixos/tests/hocker-fetchdocker/machine.nix26
-rw-r--r--nixos/tests/hockeypuck.nix63
-rw-r--r--nixos/tests/home-assistant.nix156
-rw-r--r--nixos/tests/hostname.nix72
-rw-r--r--nixos/tests/hound.nix59
-rw-r--r--nixos/tests/hydra/common.nix48
-rwxr-xr-xnixos/tests/hydra/create-trivial-project.sh59
-rw-r--r--nixos/tests/hydra/default.nix59
-rw-r--r--nixos/tests/i3wm.nix46
-rw-r--r--nixos/tests/icingaweb2.nix71
-rw-r--r--nixos/tests/iftop.nix33
-rw-r--r--nixos/tests/ihatemoney/default.nix78
-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.nix51
-rw-r--r--nixos/tests/incron.nix52
-rw-r--r--nixos/tests/influxdb.nix40
-rw-r--r--nixos/tests/initrd-network-openvpn/default.nix145
-rw-r--r--nixos/tests/initrd-network-openvpn/initrd.ovpn29
-rw-r--r--nixos/tests/initrd-network-openvpn/shared.key21
-rw-r--r--nixos/tests/initrd-network-ssh/default.nix79
-rw-r--r--nixos/tests/initrd-network-ssh/generate-keys.nix10
-rw-r--r--nixos/tests/initrd-network-ssh/id_ed255197
-rw-r--r--nixos/tests/initrd-network-ssh/id_ed25519.pub1
-rw-r--r--nixos/tests/initrd-network-ssh/ssh_host_ed25519_key7
-rw-r--r--nixos/tests/initrd-network-ssh/ssh_host_ed25519_key.pub1
-rw-r--r--nixos/tests/initrd-network.nix33
-rw-r--r--nixos/tests/initrd-secrets.nix41
-rw-r--r--nixos/tests/input-remapper.nix52
-rw-r--r--nixos/tests/inspircd.nix93
-rw-r--r--nixos/tests/installed-tests/appstream-qt.nix9
-rw-r--r--nixos/tests/installed-tests/appstream.nix9
-rw-r--r--nixos/tests/installed-tests/colord.nix5
-rw-r--r--nixos/tests/installed-tests/default.nix111
-rw-r--r--nixos/tests/installed-tests/flatpak-builder.nix14
-rw-r--r--nixos/tests/installed-tests/flatpak.nix17
-rw-r--r--nixos/tests/installed-tests/fwupd.nix11
-rw-r--r--nixos/tests/installed-tests/gcab.nix5
-rw-r--r--nixos/tests/installed-tests/gdk-pixbuf.nix13
-rw-r--r--nixos/tests/installed-tests/gjs.nix6
-rw-r--r--nixos/tests/installed-tests/glib-networking.nix5
-rw-r--r--nixos/tests/installed-tests/glib-testing.nix5
-rw-r--r--nixos/tests/installed-tests/gnome-photos.nix35
-rw-r--r--nixos/tests/installed-tests/graphene.nix5
-rw-r--r--nixos/tests/installed-tests/gsconnect.nix7
-rw-r--r--nixos/tests/installed-tests/ibus.nix16
-rw-r--r--nixos/tests/installed-tests/libgdata.nix11
-rw-r--r--nixos/tests/installed-tests/libjcat.nix5
-rw-r--r--nixos/tests/installed-tests/librsvg.nix9
-rw-r--r--nixos/tests/installed-tests/libxmlb.nix5
-rw-r--r--nixos/tests/installed-tests/malcontent.nix5
-rw-r--r--nixos/tests/installed-tests/ostree.nix12
-rw-r--r--nixos/tests/installed-tests/pipewire.nix15
-rw-r--r--nixos/tests/installed-tests/power-profiles-daemon.nix9
-rw-r--r--nixos/tests/installed-tests/xdg-desktop-portal.nix9
-rw-r--r--nixos/tests/installer.nix811
-rw-r--r--nixos/tests/invidious.nix81
-rw-r--r--nixos/tests/invoiceplane.nix82
-rw-r--r--nixos/tests/iodine.nix64
-rw-r--r--nixos/tests/ipfs.nix39
-rw-r--r--nixos/tests/ipv6.nix130
-rw-r--r--nixos/tests/iscsi-multipath-root.nix267
-rw-r--r--nixos/tests/iscsi-root.nix161
-rw-r--r--nixos/tests/isso.nix30
-rw-r--r--nixos/tests/jackett.nix19
-rw-r--r--nixos/tests/jellyfin.nix155
-rw-r--r--nixos/tests/jenkins-cli.nix30
-rw-r--r--nixos/tests/jenkins.nix130
-rw-r--r--nixos/tests/jibri.nix69
-rw-r--r--nixos/tests/jirafeau.nix22
-rw-r--r--nixos/tests/jitsi-meet.nix49
-rw-r--r--nixos/tests/k3s-single-node-docker.nix84
-rw-r--r--nixos/tests/k3s-single-node.nix82
-rw-r--r--nixos/tests/kafka.nix79
-rw-r--r--nixos/tests/kbd-setfont-decompress.nix21
-rw-r--r--nixos/tests/kbd-update-search-paths-patch.nix19
-rw-r--r--nixos/tests/kea.nix73
-rw-r--r--nixos/tests/keepalived.nix42
-rw-r--r--nixos/tests/keepassxc.nix34
-rw-r--r--nixos/tests/kerberos/default.nix7
-rw-r--r--nixos/tests/kerberos/heimdal.nix42
-rw-r--r--nixos/tests/kerberos/mit.nix41
-rw-r--r--nixos/tests/kernel-generic.nix41
-rw-r--r--nixos/tests/kernel-latest-ath-user-regd.nix17
-rw-r--r--nixos/tests/kexec.nix22
-rw-r--r--nixos/tests/keycloak.nix160
-rw-r--r--nixos/tests/keymap.nix196
-rw-r--r--nixos/tests/knot.nix216
-rw-r--r--nixos/tests/krb5/default.nix5
-rw-r--r--nixos/tests/krb5/deprecated-config.nix50
-rw-r--r--nixos/tests/krb5/example-config.nix112
-rw-r--r--nixos/tests/ksm.nix22
-rw-r--r--nixos/tests/kubernetes/base.nix107
-rw-r--r--nixos/tests/kubernetes/default.nix15
-rw-r--r--nixos/tests/kubernetes/dns.nix151
-rw-r--r--nixos/tests/kubernetes/e2e.nix40
-rw-r--r--nixos/tests/kubernetes/rbac.nix164
-rw-r--r--nixos/tests/leaps.nix32
-rw-r--r--nixos/tests/libinput.nix38
-rw-r--r--nixos/tests/libreddit.nix19
-rw-r--r--nixos/tests/libresprite.nix30
-rw-r--r--nixos/tests/libreswan.nix134
-rw-r--r--nixos/tests/lidarr.nix20
-rw-r--r--nixos/tests/lightdm.nix28
-rw-r--r--nixos/tests/limesurvey.nix26
-rw-r--r--nixos/tests/litestream.nix93
-rw-r--r--nixos/tests/locate.nix62
-rw-r--r--nixos/tests/login.nix59
-rw-r--r--nixos/tests/logrotate.nix37
-rw-r--r--nixos/tests/loki.nix56
-rw-r--r--nixos/tests/lorri/builder.sh3
-rw-r--r--nixos/tests/lorri/default.nix26
-rw-r--r--nixos/tests/lorri/fake-shell.nix5
-rw-r--r--nixos/tests/lxd-image-server.nix127
-rw-r--r--nixos/tests/lxd-image.nix89
-rw-r--r--nixos/tests/lxd-nftables.nix51
-rw-r--r--nixos/tests/lxd.nix137
-rw-r--r--nixos/tests/maddy.nix58
-rw-r--r--nixos/tests/magic-wormhole-mailbox-server.nix38
-rw-r--r--nixos/tests/magnetico.nix41
-rw-r--r--nixos/tests/mailcatcher.nix30
-rw-r--r--nixos/tests/mailhog.nix24
-rw-r--r--nixos/tests/make-test-python.nix9
-rw-r--r--nixos/tests/man.nix100
-rw-r--r--nixos/tests/matomo.nix48
-rw-r--r--nixos/tests/matrix-appservice-irc.nix221
-rw-r--r--nixos/tests/matrix-conduit.nix95
-rw-r--r--nixos/tests/matrix-synapse.nix221
-rw-r--r--nixos/tests/matrix/mjolnir.nix170
-rw-r--r--nixos/tests/matrix/pantalaimon.nix88
-rw-r--r--nixos/tests/mattermost.nix124
-rw-r--r--nixos/tests/mediatomb.nix81
-rw-r--r--nixos/tests/mediawiki.nix28
-rw-r--r--nixos/tests/meilisearch.nix60
-rw-r--r--nixos/tests/memcached.nix24
-rw-r--r--nixos/tests/metabase.nix19
-rw-r--r--nixos/tests/minecraft-server.nix37
-rw-r--r--nixos/tests/minecraft.nix28
-rw-r--r--nixos/tests/minidlna.nix41
-rw-r--r--nixos/tests/miniflux.nix82
-rw-r--r--nixos/tests/minio.nix58
-rw-r--r--nixos/tests/misc.nix163
-rw-r--r--nixos/tests/mod_perl.nix53
-rw-r--r--nixos/tests/molly-brown.nix71
-rw-r--r--nixos/tests/mongodb.nix54
-rw-r--r--nixos/tests/moodle.nix22
-rw-r--r--nixos/tests/moosefs.nix89
-rw-r--r--nixos/tests/morty.nix30
-rw-r--r--nixos/tests/mosquitto.nix208
-rw-r--r--nixos/tests/mpd.nix134
-rw-r--r--nixos/tests/mpich-example.c21
-rw-r--r--nixos/tests/mpv.nix28
-rw-r--r--nixos/tests/mumble.nix85
-rw-r--r--nixos/tests/munin.nix44
-rw-r--r--nixos/tests/musescore.nix86
-rw-r--r--nixos/tests/mutable-users.nix73
-rw-r--r--nixos/tests/mxisd.nix21
-rw-r--r--nixos/tests/mysql/common.nix10
-rw-r--r--nixos/tests/mysql/mariadb-galera.nix250
-rw-r--r--nixos/tests/mysql/mysql-autobackup.nix53
-rw-r--r--nixos/tests/mysql/mysql-backup.nix72
-rw-r--r--nixos/tests/mysql/mysql-replication.nix101
-rw-r--r--nixos/tests/mysql/mysql.nix149
-rw-r--r--nixos/tests/mysql/testdb.sql11
-rw-r--r--nixos/tests/n8n.nix25
-rw-r--r--nixos/tests/nagios.nix116
-rw-r--r--nixos/tests/nar-serve.nix48
-rw-r--r--nixos/tests/nat.nix120
-rw-r--r--nixos/tests/nats.nix63
-rw-r--r--nixos/tests/navidrome.nix12
-rw-r--r--nixos/tests/nbd.nix87
-rw-r--r--nixos/tests/ncdns.nix96
-rw-r--r--nixos/tests/ndppd.nix60
-rw-r--r--nixos/tests/nebula.nix223
-rw-r--r--nixos/tests/neo4j.nix20
-rw-r--r--nixos/tests/netdata.nix38
-rw-r--r--nixos/tests/networking-proxy.nix134
-rw-r--r--nixos/tests/networking.nix925
-rw-r--r--nixos/tests/nextcloud/basic.nix112
-rw-r--r--nixos/tests/nextcloud/default.nix21
-rw-r--r--nixos/tests/nextcloud/with-mysql-and-memcached.nix110
-rw-r--r--nixos/tests/nextcloud/with-postgresql-and-redis.nix102
-rw-r--r--nixos/tests/nexus.nix32
-rw-r--r--nixos/tests/nfs/default.nix9
-rw-r--r--nixos/tests/nfs/kerberos.nix133
-rw-r--r--nixos/tests/nfs/simple.nix94
-rw-r--r--nixos/tests/nghttpx.nix61
-rw-r--r--nixos/tests/nginx-auth.nix47
-rw-r--r--nixos/tests/nginx-etag.nix88
-rw-r--r--nixos/tests/nginx-modsecurity.nix39
-rw-r--r--nixos/tests/nginx-pubhtml.nix21
-rw-r--r--nixos/tests/nginx-sandbox.nix65
-rw-r--r--nixos/tests/nginx-sso.nix48
-rw-r--r--nixos/tests/nginx-variants.nix33
-rw-r--r--nixos/tests/nginx.nix129
-rw-r--r--nixos/tests/nitter.nix18
-rw-r--r--nixos/tests/nix-serve-ssh.nix45
-rw-r--r--nixos/tests/nix-serve.nix22
-rw-r--r--nixos/tests/nixops/default.nix114
-rw-r--r--nixos/tests/nixops/legacy/base-configuration.nix31
-rw-r--r--nixos/tests/nixops/legacy/nixops.nix15
-rw-r--r--nixos/tests/nixos-generate-config.nix41
-rw-r--r--nixos/tests/node-red.nix31
-rw-r--r--nixos/tests/nomad.nix97
-rw-r--r--nixos/tests/noto-fonts.nix44
-rw-r--r--nixos/tests/novacomd.nix28
-rw-r--r--nixos/tests/nsd.nix109
-rw-r--r--nixos/tests/nzbget.nix46
-rw-r--r--nixos/tests/nzbhydra2.nix17
-rw-r--r--nixos/tests/oci-containers.nix43
-rw-r--r--nixos/tests/odoo.nix27
-rw-r--r--nixos/tests/oh-my-zsh.nix18
-rw-r--r--nixos/tests/ombi.nix18
-rw-r--r--nixos/tests/openarena.nix71
-rw-r--r--nixos/tests/openldap.nix130
-rw-r--r--nixos/tests/openresty-lua.nix55
-rw-r--r--nixos/tests/opensmtpd-rspamd.nix141
-rw-r--r--nixos/tests/opensmtpd.nix125
-rw-r--r--nixos/tests/openssh.nix112
-rw-r--r--nixos/tests/openstack-image.nix98
-rw-r--r--nixos/tests/opentabletdriver.nix30
-rw-r--r--nixos/tests/orangefs.nix82
-rw-r--r--nixos/tests/os-prober.nix121
-rw-r--r--nixos/tests/osrm-backend.nix57
-rw-r--r--nixos/tests/overlayfs.nix47
-rw-r--r--nixos/tests/owncast.nix42
-rw-r--r--nixos/tests/pacemaker.nix110
-rw-r--r--nixos/tests/packagekit.nix25
-rw-r--r--nixos/tests/pam/pam-file-contents.nix25
-rw-r--r--nixos/tests/pam/pam-oath-login.nix108
-rw-r--r--nixos/tests/pam/pam-u2f.nix25
-rw-r--r--nixos/tests/pam/test_chfn.py27
-rw-r--r--nixos/tests/pantheon.nix58
-rw-r--r--nixos/tests/paperless-ng.nix45
-rw-r--r--nixos/tests/parsedmarc/default.nix235
-rw-r--r--nixos/tests/pdns-recursor.nix12
-rw-r--r--nixos/tests/peerflix.nix23
-rw-r--r--nixos/tests/pgadmin4-standalone.nix43
-rw-r--r--nixos/tests/pgadmin4.nix142
-rw-r--r--nixos/tests/pgjwt.nix34
-rw-r--r--nixos/tests/pgmanage.nix41
-rw-r--r--nixos/tests/php/default.nix16
-rw-r--r--nixos/tests/php/fpm.nix59
-rw-r--r--nixos/tests/php/httpd.nix34
-rw-r--r--nixos/tests/php/pcre.nix42
-rw-r--r--nixos/tests/pict-rs.nix17
-rw-r--r--nixos/tests/pinnwand.nix94
-rw-r--r--nixos/tests/plasma5-systemd-start.nix42
-rw-r--r--nixos/tests/plasma5.nix61
-rw-r--r--nixos/tests/plausible.nix49
-rw-r--r--nixos/tests/pleroma.nix249
-rw-r--r--nixos/tests/plikd.nix27
-rw-r--r--nixos/tests/plotinus.nix28
-rw-r--r--nixos/tests/podgrab.nix34
-rw-r--r--nixos/tests/podman/default.nix144
-rw-r--r--nixos/tests/podman/dnsname.nix42
-rw-r--r--nixos/tests/podman/tls-ghostunnel.nix150
-rw-r--r--nixos/tests/pomerium.nix102
-rw-r--r--nixos/tests/postfix-raise-smtpd-tls-security-level.nix41
-rw-r--r--nixos/tests/postfix.nix77
-rw-r--r--nixos/tests/postfixadmin.nix31
-rw-r--r--nixos/tests/postgis.nix29
-rw-r--r--nixos/tests/postgresql-wal-receiver.nix119
-rw-r--r--nixos/tests/postgresql.nix137
-rw-r--r--nixos/tests/power-profiles-daemon.nix45
-rw-r--r--nixos/tests/powerdns-admin.nix117
-rw-r--r--nixos/tests/powerdns.nix65
-rw-r--r--nixos/tests/pppd.nix62
-rw-r--r--nixos/tests/predictable-interface-names.nix37
-rw-r--r--nixos/tests/printing.nix128
-rw-r--r--nixos/tests/privacyidea.nix43
-rw-r--r--nixos/tests/privoxy.nix113
-rw-r--r--nixos/tests/prometheus-exporters.nix1342
-rw-r--r--nixos/tests/prometheus.nix339
-rw-r--r--nixos/tests/prowlarr.nix18
-rw-r--r--nixos/tests/proxy.nix90
-rw-r--r--nixos/tests/pt2-clone.nix35
-rw-r--r--nixos/tests/pulseaudio.nix71
-rw-r--r--nixos/tests/qboot.nix13
-rw-r--r--nixos/tests/quorum.nix102
-rw-r--r--nixos/tests/rabbitmq.nix27
-rw-r--r--nixos/tests/radarr.nix18
-rw-r--r--nixos/tests/radicale.nix95
-rw-r--r--nixos/tests/rasdaemon.nix34
-rw-r--r--nixos/tests/redis.nix46
-rw-r--r--nixos/tests/redmine.nix44
-rw-r--r--nixos/tests/resolv.nix46
-rw-r--r--nixos/tests/restart-by-activation-script.nix73
-rw-r--r--nixos/tests/restic.nix96
-rw-r--r--nixos/tests/retroarch.nix49
-rw-r--r--nixos/tests/riak.nix18
-rw-r--r--nixos/tests/robustirc-bridge.nix29
-rw-r--r--nixos/tests/roundcube.nix31
-rw-r--r--nixos/tests/rspamd.nix313
-rw-r--r--nixos/tests/rss2email.nix66
-rw-r--r--nixos/tests/rstudio-server.nix30
-rw-r--r--nixos/tests/rsyncd.nix36
-rw-r--r--nixos/tests/rsyslogd.nix40
-rw-r--r--nixos/tests/rxe.nix47
-rw-r--r--nixos/tests/sabnzbd.nix22
-rw-r--r--nixos/tests/samba-wsdd.nix44
-rw-r--r--nixos/tests/samba.nix46
-rw-r--r--nixos/tests/sanoid.nix112
-rw-r--r--nixos/tests/sddm.nix69
-rw-r--r--nixos/tests/seafile.nix121
-rw-r--r--nixos/tests/searx.nix114
-rw-r--r--nixos/tests/service-runner.nix36
-rw-r--r--nixos/tests/shadow.nix119
-rw-r--r--nixos/tests/shadowsocks/common.nix84
-rw-r--r--nixos/tests/shadowsocks/default.nix16
-rw-r--r--nixos/tests/shattered-pixel-dungeon.nix30
-rw-r--r--nixos/tests/shiori.nix80
-rw-r--r--nixos/tests/signal-desktop.nix69
-rw-r--r--nixos/tests/simple.nix17
-rw-r--r--nixos/tests/slurm.nix168
-rw-r--r--nixos/tests/smokeping.nix34
-rw-r--r--nixos/tests/snapcast.nix89
-rw-r--r--nixos/tests/snapper.nix35
-rw-r--r--nixos/tests/soapui.nix24
-rw-r--r--nixos/tests/sogo.nix58
-rw-r--r--nixos/tests/solanum.nix97
-rw-r--r--nixos/tests/solr.nix56
-rw-r--r--nixos/tests/sonarr.nix18
-rw-r--r--nixos/tests/sourcehut.nix212
-rw-r--r--nixos/tests/spacecookie.nix56
-rw-r--r--nixos/tests/spark/default.nix27
-rw-r--r--nixos/tests/spark/spark_sample.py40
-rw-r--r--nixos/tests/specialisation.nix43
-rw-r--r--nixos/tests/ssh-keys.nix15
-rw-r--r--nixos/tests/sslh.nix83
-rw-r--r--nixos/tests/sssd-ldap.nix94
-rw-r--r--nixos/tests/sssd.nix17
-rw-r--r--nixos/tests/starship.nix42
-rw-r--r--nixos/tests/step-ca.nix76
-rw-r--r--nixos/tests/strongswan-swanctl.nix148
-rw-r--r--nixos/tests/sudo.nix106
-rw-r--r--nixos/tests/sway.nix138
-rw-r--r--nixos/tests/switch-test.nix1018
-rw-r--r--nixos/tests/sympa.nix35
-rw-r--r--nixos/tests/syncthing-init.nix31
-rw-r--r--nixos/tests/syncthing-relay.nix26
-rw-r--r--nixos/tests/syncthing.nix65
-rw-r--r--nixos/tests/systemd-analyze.nix46
-rw-r--r--nixos/tests/systemd-binfmt.nix90
-rw-r--r--nixos/tests/systemd-boot.nix254
-rw-r--r--nixos/tests/systemd-confinement.nix184
-rw-r--r--nixos/tests/systemd-cryptenroll.nix54
-rw-r--r--nixos/tests/systemd-escaping.nix45
-rw-r--r--nixos/tests/systemd-journal.nix22
-rw-r--r--nixos/tests/systemd-machinectl.nix85
-rw-r--r--nixos/tests/systemd-networkd-dhcpserver-static-leases.nix81
-rw-r--r--nixos/tests/systemd-networkd-dhcpserver.nix58
-rw-r--r--nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix284
-rw-r--r--nixos/tests/systemd-networkd-vrf.nix223
-rw-r--r--nixos/tests/systemd-networkd.nix113
-rw-r--r--nixos/tests/systemd-nspawn.nix60
-rw-r--r--nixos/tests/systemd-timesyncd.nix52
-rw-r--r--nixos/tests/systemd-unit-path.nix47
-rw-r--r--nixos/tests/systemd.nix196
-rw-r--r--nixos/tests/taskserver.nix282
-rw-r--r--nixos/tests/teeworlds.nix55
-rw-r--r--nixos/tests/telegraf.nix33
-rw-r--r--nixos/tests/teleport.nix99
-rw-r--r--nixos/tests/terminal-emulators.nix207
-rw-r--r--nixos/tests/thelounge.nix29
-rw-r--r--nixos/tests/tiddlywiki.nix69
-rw-r--r--nixos/tests/tigervnc.nix53
-rw-r--r--nixos/tests/timezone.nix50
-rw-r--r--nixos/tests/tinc/default.nix139
-rw-r--r--nixos/tests/tinc/snakeoil-keys.nix157
-rw-r--r--nixos/tests/tinydns.nix40
-rw-r--r--nixos/tests/tinywl.nix57
-rw-r--r--nixos/tests/tomcat.nix21
-rw-r--r--nixos/tests/tor.nix30
-rw-r--r--nixos/tests/traefik.nix89
-rw-r--r--nixos/tests/trafficserver.nix177
-rw-r--r--nixos/tests/transmission.nix23
-rw-r--r--nixos/tests/trezord.nix19
-rw-r--r--nixos/tests/trickster.nix37
-rw-r--r--nixos/tests/trilium-server.nix53
-rw-r--r--nixos/tests/tsm-client-gui.nix57
-rw-r--r--nixos/tests/tuptime.nix29
-rw-r--r--nixos/tests/turbovnc-headless-server.nix172
-rw-r--r--nixos/tests/tuxguitar.nix24
-rw-r--r--nixos/tests/txredisapi.nix29
-rw-r--r--nixos/tests/ucarp.nix66
-rw-r--r--nixos/tests/udisks2.nix69
-rw-r--r--nixos/tests/unbound.nix315
-rw-r--r--nixos/tests/unifi.nix36
-rw-r--r--nixos/tests/upnp.nix96
-rw-r--r--nixos/tests/usbguard.nix62
-rw-r--r--nixos/tests/user-activation-scripts.nix33
-rw-r--r--nixos/tests/uwsgi.nix81
-rw-r--r--nixos/tests/v2ray.nix83
-rw-r--r--nixos/tests/vault-postgresql.nix69
-rw-r--r--nixos/tests/vault.nix25
-rw-r--r--nixos/tests/vaultwarden.nix188
-rw-r--r--nixos/tests/vector.nix37
-rw-r--r--nixos/tests/vengi-tools.nix29
-rw-r--r--nixos/tests/victoriametrics.nix33
-rw-r--r--nixos/tests/vikunja.nix65
-rw-r--r--nixos/tests/virtualbox.nix531
-rw-r--r--nixos/tests/vscodium.nix78
-rw-r--r--nixos/tests/vsftpd.nix42
-rw-r--r--nixos/tests/wasabibackend.nix38
-rw-r--r--nixos/tests/web-apps/mastodon.nix170
-rw-r--r--nixos/tests/web-apps/peertube.nix130
-rw-r--r--nixos/tests/web-servers/agate.nix29
-rw-r--r--nixos/tests/web-servers/unit-php.nix47
-rw-r--r--nixos/tests/wiki-js.nix152
-rw-r--r--nixos/tests/wine.nix48
-rw-r--r--nixos/tests/wireguard/basic.nix74
-rw-r--r--nixos/tests/wireguard/default.nix27
-rw-r--r--nixos/tests/wireguard/generated.nix64
-rw-r--r--nixos/tests/wireguard/make-peer.nix23
-rw-r--r--nixos/tests/wireguard/namespaces.nix84
-rw-r--r--nixos/tests/wireguard/snakeoil-keys.nix11
-rw-r--r--nixos/tests/wireguard/wg-quick.nix67
-rw-r--r--nixos/tests/without-nix.nix23
-rw-r--r--nixos/tests/wmderland.nix54
-rw-r--r--nixos/tests/wordpress.nix90
-rw-r--r--nixos/tests/wpa_supplicant.nix96
-rw-r--r--nixos/tests/xandikos.nix70
-rw-r--r--nixos/tests/xautolock.nix24
-rw-r--r--nixos/tests/xfce.nix45
-rw-r--r--nixos/tests/xmonad.nix114
-rw-r--r--nixos/tests/xmpp/ejabberd.nix278
-rw-r--r--nixos/tests/xmpp/prosody-mysql.nix124
-rw-r--r--nixos/tests/xmpp/prosody.nix92
-rw-r--r--nixos/tests/xmpp/xmpp-sendmessage.nix87
-rw-r--r--nixos/tests/xrdp.nix47
-rw-r--r--nixos/tests/xss-lock.nix44
-rw-r--r--nixos/tests/xterm.nix23
-rw-r--r--nixos/tests/xxh.nix67
-rw-r--r--nixos/tests/yabar.nix33
-rw-r--r--nixos/tests/yggdrasil.nix162
-rw-r--r--nixos/tests/zammad.nix60
-rw-r--r--nixos/tests/zfs.nix130
-rw-r--r--nixos/tests/zigbee2mqtt.nix23
-rw-r--r--nixos/tests/zoneminder.nix23
-rw-r--r--nixos/tests/zookeeper.nix46
-rw-r--r--nixos/tests/zsh-history.nix35
2544 files changed, 337529 insertions, 0 deletions
diff --git a/nixos/COPYING b/nixos/COPYING
new file mode 100644
index 00000000000..c9b44cb8aae
--- /dev/null
+++ b/nixos/COPYING
@@ -0,0 +1,18 @@
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/nixos/README b/nixos/README
new file mode 100644
index 00000000000..ce4dd1988d2
--- /dev/null
+++ b/nixos/README
@@ -0,0 +1,5 @@
+*** NixOS ***
+
+NixOS is a Linux distribution based on the purely functional package
+management system Nix.  More information can be found at
+https://nixos.org/nixos and in the manual in doc/manual.
diff --git a/nixos/default.nix b/nixos/default.nix
new file mode 100644
index 00000000000..6beb4cd3a7d
--- /dev/null
+++ b/nixos/default.nix
@@ -0,0 +1,20 @@
+{ configuration ? import ./lib/from-env.nix "NIXOS_CONFIG" <nixos-config>
+, system ? builtins.currentSystem
+}:
+
+let
+
+  eval = import ./lib/eval-config.nix {
+    inherit system;
+    modules = [ configuration ];
+  };
+
+in
+
+{
+  inherit (eval) pkgs config options;
+
+  system = eval.config.system.build.toplevel;
+
+  inherit (eval.config.system.build) vm vmWithBootLoader;
+}
diff --git a/nixos/doc/manual/.gitignore b/nixos/doc/manual/.gitignore
new file mode 100644
index 00000000000..87928262421
--- /dev/null
+++ b/nixos/doc/manual/.gitignore
@@ -0,0 +1,2 @@
+generated
+manual-combined.xml
diff --git a/nixos/doc/manual/Makefile b/nixos/doc/manual/Makefile
new file mode 100644
index 00000000000..b2b6481b20c
--- /dev/null
+++ b/nixos/doc/manual/Makefile
@@ -0,0 +1,30 @@
+.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/README.md b/nixos/doc/manual/README.md
new file mode 100644
index 00000000000..bc649761df6
--- /dev/null
+++ b/nixos/doc/manual/README.md
@@ -0,0 +1,3 @@
+[Moved to ./contributing-to-this-manual.chapter.md](./contributing-to-this-manual.chapter.md). Link:
+
+https://nixos.org/manual/nixos/unstable/#chap-contributing
diff --git a/nixos/doc/manual/administration/boot-problems.section.md b/nixos/doc/manual/administration/boot-problems.section.md
new file mode 100644
index 00000000000..bca4fdc3fb3
--- /dev/null
+++ b/nixos/doc/manual/administration/boot-problems.section.md
@@ -0,0 +1,41 @@
+# Boot Problems {#sec-boot-problems}
+
+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 `linux`. The following are some useful kernel command line parameters that are recognised by the NixOS boot scripts or by systemd:
+
+`boot.shell_on_fail`
+
+: 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.
+
+`boot.debug1`
+
+: 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 `/proc` and `/sys`.
+
+`boot.debug1devices`
+
+: Like `boot.debug1`, but runs stage1 until kernel modules are loaded and device nodes are created. This may help with e.g. making the keyboard work.
+
+`boot.debug1mounts`
+
+: Like `boot.debug1` or `boot.debug1devices`, but runs stage1 until all filesystems that are mounted during initrd are mounted (see [neededForBoot](#opt-fileSystems._name_.neededForBoot)). As a motivating example, this could be useful if you've forgotten to set [neededForBoot](#opt-fileSystems._name_.neededForBoot) on a file system.
+
+`boot.trace`
+
+: Print every shell command executed by the stage 1 and 2 boot scripts.
+
+`single`
+
+: Boot into rescue mode (a.k.a. single user mode). This will cause systemd to start nothing but the unit `rescue.target`, which runs `sulogin` 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.
+
+`systemd.log_level=debug` `systemd.log_target=console`
+
+: Make systemd very verbose and send log messages to the console instead of the journal. For more parameters recognised by systemd, see systemd(1).
+
+In addition, these arguments are recognised by the live image only:
+
+`live.nixos.passwd=password`
+
+: Set the password for the `nixos` live user. This can be used for SSH access if there are issues using the terminal.
+
+Notice that for `boot.shell_on_fail`, `boot.debug1`, `boot.debug1devices`, and `boot.debug1mounts`, if you did **not** select "start the new shell as pid 1", and you `exit` from the new shell, boot will proceed normally from the point where it failed, as if you'd chosen "ignore the error and continue".
+
+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 `agetty` login prompts should appear eventually unless something is very wrong.)
diff --git a/nixos/doc/manual/administration/cleaning-store.chapter.md b/nixos/doc/manual/administration/cleaning-store.chapter.md
new file mode 100644
index 00000000000..c9140d0869c
--- /dev/null
+++ b/nixos/doc/manual/administration/cleaning-store.chapter.md
@@ -0,0 +1,62 @@
+# Cleaning the Nix Store {#sec-nix-gc}
+
+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 (`/nix/store`). You should
+periodically run Nix's *garbage collector* to remove old, unreferenced
+packages. This is easy:
+
+```ShellSession
+$ nix-collect-garbage
+```
+
+Alternatively, you can use a systemd unit that does the same in the
+background:
+
+```ShellSession
+# systemctl start nix-gc.service
+```
+
+You can tell NixOS in `configuration.nix` to run this unit automatically
+at certain points in time, for instance, every night at 03:15:
+
+```nix
+nix.gc.automatic = true;
+nix.gc.dates = "03:15";
+```
+
+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:
+
+```ShellSession
+$ nix-collect-garbage -d
+```
+
+You can also do this for specific profiles, e.g.
+
+```ShellSession
+$ nix-env -p /nix/var/nix/profiles/per-user/eelco/profile --delete-generations old
+```
+
+Note that NixOS system configurations are stored in the profile
+`/nix/var/nix/profiles/system`.
+
+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.
+
+```ShellSession
+$ nix-store --optimise
+```
+
+Since this command needs to read the entire Nix store, it can take quite
+a while to finish.
+
+## NixOS Boot Entries {#sect-nixos-gc-boot-entries}
+
+If your `/boot` partition runs out of space, after clearing old profiles
+you must rebuild your system with `nixos-rebuild boot` or `nixos-rebuild
+switch` to update the `/boot` partition and clear space.
diff --git a/nixos/doc/manual/administration/container-networking.section.md b/nixos/doc/manual/administration/container-networking.section.md
new file mode 100644
index 00000000000..0873768376c
--- /dev/null
+++ b/nixos/doc/manual/administration/container-networking.section.md
@@ -0,0 +1,44 @@
+# Container Networking {#sec-container-networking}
+
+When you create a container using `nixos-container create`, it gets it
+own private IPv4 address in the range `10.233.0.0/16`. You can get the
+container's IPv4 address as follows:
+
+```ShellSession
+# 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
+```
+
+Networking is implemented using a pair of virtual Ethernet devices. The
+network interface in the container is called `eth0`, while the matching
+interface in the host is called `ve-container-name` (e.g., `ve-foo`).
+The container has its own network namespace and the `CAP_NET_ADMIN`
+capability, so it can perform arbitrary network configuration such as
+setting up firewall rules, without affecting or having access to the
+host's network.
+
+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:
+
+```nix
+networking.nat.enable = true;
+networking.nat.internalInterfaces = ["ve-+"];
+networking.nat.externalInterface = "eth0";
+```
+
+where `eth0` should be replaced with the desired external interface.
+Note that `ve-+` is a wildcard that matches all container interfaces.
+
+If you are using Network Manager, you need to explicitly prevent it from
+managing container interfaces:
+
+```nix
+networking.networkmanager.unmanaged = [ "interface-name:ve-*" ];
+```
+
+You may need to restart your system for the changes to take effect.
diff --git a/nixos/doc/manual/administration/containers.chapter.md b/nixos/doc/manual/administration/containers.chapter.md
new file mode 100644
index 00000000000..ea51f91f698
--- /dev/null
+++ b/nixos/doc/manual/administration/containers.chapter.md
@@ -0,0 +1,28 @@
+# Container Management {#ch-containers}
+
+NixOS allows you to easily run other NixOS instances as *containers*.
+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.
+
+::: {.warning}
+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.
+:::
+
+NixOS containers can be created in two ways: imperatively, using the
+command `nixos-container`, and declaratively, by specifying them in your
+`configuration.nix`. The declarative approach implies that containers
+get upgraded along with your host system when you run `nixos-rebuild`,
+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" />
+```
diff --git a/nixos/doc/manual/administration/control-groups.chapter.md b/nixos/doc/manual/administration/control-groups.chapter.md
new file mode 100644
index 00000000000..abe8dd80b5a
--- /dev/null
+++ b/nixos/doc/manual/administration/control-groups.chapter.md
@@ -0,0 +1,59 @@
+# Control Groups {#sec-cgroups}
+
+To keep track of the processes in a running system, systemd uses
+*control groups* (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.
+
+The command `systemd-cgls` lists all control groups in the `systemd`
+hierarchy, which is what systemd uses to keep track of the processes
+belonging to each service or user session:
+
+```ShellSession
+$ 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
+  └─ ...
+```
+
+Similarly, `systemd-cgls cpu` 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 `httpd.service` cgroup cannot starve the CPU
+for one process in the `postgresql.service` 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
+`configuration.nix`:
+
+```nix
+systemd.services.httpd.serviceConfig.CPUShares = 512;
+```
+
+By default, every cgroup has 1024 CPU shares, so this will halve the CPU
+allocation of the `httpd.service` cgroup.
+
+There also is a `memory` 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 `configuration.nix`; for instance, to limit
+`httpd.service` to 512 MiB of RAM (excluding swap):
+
+```nix
+systemd.services.httpd.serviceConfig.MemoryLimit = "512M";
+```
+
+The command `systemd-cgtop` shows a continuously updated list of all
+cgroups with their CPU and memory usage.
diff --git a/nixos/doc/manual/administration/declarative-containers.section.md b/nixos/doc/manual/administration/declarative-containers.section.md
new file mode 100644
index 00000000000..0d9d4017ed8
--- /dev/null
+++ b/nixos/doc/manual/administration/declarative-containers.section.md
@@ -0,0 +1,48 @@
+# Declarative Container Specification {#sec-declarative-containers}
+
+You can also specify containers and their configuration in the host's
+`configuration.nix`. For example, the following specifies that there
+shall be a container named `database` running PostgreSQL:
+
+```nix
+containers.database =
+  { config =
+      { config, pkgs, ... }:
+      { services.postgresql.enable = true;
+      services.postgresql.package = pkgs.postgresql_10;
+      };
+  };
+```
+
+If you run `nixos-rebuild switch`, 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 `containers.database.autoStart = true` in its configuration.
+
+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:
+
+```nix
+containers.database = {
+  privateNetwork = true;
+  hostAddress = "192.168.100.10";
+  localAddress = "192.168.100.11";
+};
+```
+
+This gives the container a private virtual Ethernet interface with IP
+address `192.168.100.11`, which is hooked up to a virtual Ethernet
+interface on the host with IP address `192.168.100.10`. (See the next
+section for details on container networking.)
+
+To disable the container, just remove it from `configuration.nix` and
+run `nixos-rebuild
+  switch`. Note that this will not delete the root directory of the
+container in `/var/lib/containers`. Containers can be destroyed using
+the imperative method: `nixos-container destroy foo`.
+
+Declarative containers can be started and stopped using the
+corresponding systemd service, e.g.
+`systemctl start container@database`.
diff --git a/nixos/doc/manual/administration/imperative-containers.section.md b/nixos/doc/manual/administration/imperative-containers.section.md
new file mode 100644
index 00000000000..05196bf5d81
--- /dev/null
+++ b/nixos/doc/manual/administration/imperative-containers.section.md
@@ -0,0 +1,115 @@
+# Imperative Container Management {#sec-imperative-containers}
+
+We'll cover imperative container management using `nixos-container`
+first. Be aware that container management is currently only possible as
+`root`.
+
+You create a container with identifier `foo` as follows:
+
+```ShellSession
+# nixos-container create foo
+```
+
+This creates the container's root directory in `/var/lib/containers/foo`
+and a small configuration file in `/etc/containers/foo.conf`. It also
+builds the container's initial system configuration and stores it in
+`/nix/var/nix/profiles/per-container/foo/system`. You can modify the
+initial configuration of the container on the command line. For
+instance, to create a container that has `sshd` running, with the given
+public key for `root`:
+
+```ShellSession
+# nixos-container create foo --config '
+  services.openssh.enable = true;
+  users.users.root.openssh.authorizedKeys.keys = ["ssh-dss AAAAB3N…"];
+'
+```
+
+By default the next free address in the `10.233.0.0/16` subnet will be
+chosen as container IP. This behavior can be altered by setting
+`--host-address` and `--local-address`:
+
+```ShellSession
+# nixos-container create test --config-file test-container.nix \
+    --local-address 10.235.1.2 --host-address 10.235.1.1
+```
+
+Creating a container does not start it. To start the container, run:
+
+```ShellSession
+# nixos-container start foo
+```
+
+This command will return as soon as the container has booted and has
+reached `multi-user.target`. On the host, the container runs within a
+systemd unit called `container@container-name.service`. Thus, if
+something went wrong, you can get status info using `systemctl`:
+
+```ShellSession
+# systemctl status container@foo
+```
+
+If the container has started successfully, you can log in as root using
+the `root-login` operation:
+
+```ShellSession
+# nixos-container root-login foo
+[root@foo:~]#
+```
+
+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
+`login` operation, which is available to all users on the host:
+
+```ShellSession
+# nixos-container login foo
+foo login: alice
+Password: ***
+```
+
+With `nixos-container run`, you can execute arbitrary commands in the
+container:
+
+```ShellSession
+# 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
+```
+
+There are several ways to change the configuration of the container.
+First, on the host, you can edit
+`/var/lib/container/name/etc/nixos/configuration.nix`, and run
+
+```ShellSession
+# nixos-container update foo
+```
+
+This will build and activate the new configuration. You can also specify
+a new configuration on the command line:
+
+```ShellSession
+# nixos-container update foo --config '
+  services.httpd.enable = true;
+  services.httpd.adminAddr = "foo@example.org";
+  networking.firewall.allowedTCPPorts = [ 80 ];
+'
+
+# curl http://$(nixos-container show-ip foo)/
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">…
+```
+
+However, note that this will overwrite the container's
+`/etc/nixos/configuration.nix`.
+
+Alternatively, you can change the configuration from within the
+container itself by running `nixos-rebuild switch` inside the container.
+Note that the container by default does not have a copy of the NixOS
+channel, so you should run `nix-channel --update` first.
+
+Containers can be stopped and started using `nixos-container
+  stop` and `nixos-container start`, respectively, or by using
+`systemctl` on the container's service unit. To destroy a container,
+including its file system, do
+
+```ShellSession
+# nixos-container destroy foo
+```
diff --git a/nixos/doc/manual/administration/logging.chapter.md b/nixos/doc/manual/administration/logging.chapter.md
new file mode 100644
index 00000000000..4ce6f5e9fa7
--- /dev/null
+++ b/nixos/doc/manual/administration/logging.chapter.md
@@ -0,0 +1,38 @@
+# Logging {#sec-logging}
+
+System-wide logging is provided by systemd's *journal*, which subsumes
+traditional logging daemons such as syslogd and klogd. Log entries are
+kept in binary files in `/var/log/journal/`. The command `journalctl`
+allows you to see the contents of the journal. For example,
+
+```ShellSession
+$ journalctl -b
+```
+
+shows all journal entries since the last reboot. (The output of
+`journalctl` is piped into `less` 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:
+
+```ShellSession
+$ 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
+```
+
+Or to get all messages since the last reboot that have at least a
+"critical" severity level:
+
+```ShellSession
+$ 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)
+```
+
+The system journal is readable by root and by users in the `wheel` and
+`systemd-journal` groups. All users have a private journal that can be
+read using `journalctl`.
diff --git a/nixos/doc/manual/administration/maintenance-mode.section.md b/nixos/doc/manual/administration/maintenance-mode.section.md
new file mode 100644
index 00000000000..0aec013c0a9
--- /dev/null
+++ b/nixos/doc/manual/administration/maintenance-mode.section.md
@@ -0,0 +1,11 @@
+# Maintenance Mode {#sec-maintenance-mode}
+
+You can enter rescue mode by running:
+
+```ShellSession
+# systemctl rescue
+```
+
+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.
diff --git a/nixos/doc/manual/administration/network-problems.section.md b/nixos/doc/manual/administration/network-problems.section.md
new file mode 100644
index 00000000000..d360120d72d
--- /dev/null
+++ b/nixos/doc/manual/administration/network-problems.section.md
@@ -0,0 +1,21 @@
+# Network Problems {#sec-nix-network-issues}
+
+Nix uses a so-called *binary cache* to optimise building a package from
+source into downloading it as a pre-built binary. That is, whenever a
+command like `nixos-rebuild` 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 `https://cache.nixos.org/`. 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 `--option use-binary-caches false`, e.g.
+
+```ShellSession
+# nixos-rebuild switch --option use-binary-caches false
+```
+
+If you have an alternative binary cache at your disposal, you can use it
+instead:
+
+```ShellSession
+# nixos-rebuild switch --option binary-caches http://my-cache.example.org/
+```
diff --git a/nixos/doc/manual/administration/rebooting.chapter.md b/nixos/doc/manual/administration/rebooting.chapter.md
new file mode 100644
index 00000000000..ec4b889b164
--- /dev/null
+++ b/nixos/doc/manual/administration/rebooting.chapter.md
@@ -0,0 +1,30 @@
+# Rebooting and Shutting Down {#sec-rebooting}
+
+The system can be shut down (and automatically powered off) by doing:
+
+```ShellSession
+# shutdown
+```
+
+This is equivalent to running `systemctl poweroff`.
+
+To reboot the system, run
+
+```ShellSession
+# reboot
+```
+
+which is equivalent to `systemctl reboot`. Alternatively, you can
+quickly reboot the system using `kexec`, which bypasses the BIOS by
+directly loading the new kernel into memory:
+
+```ShellSession
+# systemctl kexec
+```
+
+The machine can be suspended to RAM (if supported) using `systemctl suspend`,
+and suspended to disk using `systemctl hibernate`.
+
+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.
diff --git a/nixos/doc/manual/administration/rollback.section.md b/nixos/doc/manual/administration/rollback.section.md
new file mode 100644
index 00000000000..290d685a2a1
--- /dev/null
+++ b/nixos/doc/manual/administration/rollback.section.md
@@ -0,0 +1,38 @@
+# Rolling Back Configuration Changes {#sec-rollback}
+
+After running `nixos-rebuild` 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.
+
+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 "NixOS - All configurations". 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:
+
+```ShellSession
+# /run/current-system/bin/switch-to-configuration boot
+```
+
+Second, you can switch to the previous configuration in a running
+system:
+
+```ShellSession
+# nixos-rebuild switch --rollback
+```
+
+This is equivalent to running:
+
+```ShellSession
+# /nix/var/nix/profiles/system-N-link/bin/switch-to-configuration switch
+```
+
+where `N` is the number of the NixOS system configuration. To get a
+list of the available configurations, do:
+
+```ShellSession
+$ ls -l /nix/var/nix/profiles/system-*-link
+...
+lrwxrwxrwx 1 root root 78 Aug 12 13:54 /nix/var/nix/profiles/system-268-link -> /nix/store/202b...-nixos-13.07pre4932_5a676e4-4be1055
+```
diff --git a/nixos/doc/manual/administration/running.xml b/nixos/doc/manual/administration/running.xml
new file mode 100644
index 00000000000..d9fcc1aee26
--- /dev/null
+++ b/nixos/doc/manual/administration/running.xml
@@ -0,0 +1,21 @@
+<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
new file mode 100644
index 00000000000..bb0f9b62e91
--- /dev/null
+++ b/nixos/doc/manual/administration/service-mgmt.chapter.md
@@ -0,0 +1,120 @@
+# Service Management {#sec-systemctl}
+
+In NixOS, all system services are started and monitored using the
+systemd program. systemd is the "init" process of the system (i.e. PID
+1), the parent of all other processes. It manages a set of so-called
+"units", which can be things like system services (programs), but also
+mount points, swap files, devices, targets (groups of units) and more.
+Units can have 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
+`default.target`; 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.
+
+## Interacting with a running systemd {#sect-nixos-systemd-general}
+
+The command `systemctl` is the main way to interact with `systemd`. The
+following paragraphs demonstrate ways to interact with any OS running
+systemd as init system. NixOS is of no exception. The [next section
+](#sect-nixos-systemd-nixos) explains NixOS specific things worth
+knowing.
+
+Without any arguments, `systemctl` the status of active units:
+
+```ShellSession
+$ 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
+...
+```
+
+You can ask for detailed status information about a unit, for instance,
+the PostgreSQL database service:
+
+```ShellSession
+$ 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.
+```
+
+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.
+
+Units can be stopped, started or restarted:
+
+```ShellSession
+# systemctl stop postgresql.service
+# systemctl start postgresql.service
+# systemctl restart postgresql.service
+```
+
+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).
+
+## systemd in NixOS {#sect-nixos-systemd-nixos}
+
+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
+users or the system.
+
+In order to enable a systemd *system* service with provided upstream
+package, use (e.g):
+
+```nix
+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
+`services.#name#.enable = true;`. These services are defined in
+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.
+
+*User* systemd services on the other hand, should be treated
+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
+[](#opt-systemd.packages) and then do this (e.g):
+
+```ShellSession
+$ 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
+```
+
+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
+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
+in turn will not make the service / timer start on login.
diff --git a/nixos/doc/manual/administration/store-corruption.section.md b/nixos/doc/manual/administration/store-corruption.section.md
new file mode 100644
index 00000000000..bd8a5772b37
--- /dev/null
+++ b/nixos/doc/manual/administration/store-corruption.section.md
@@ -0,0 +1,28 @@
+# Nix Store Corruption {#sec-nix-store-corruption}
+
+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 `sync` 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.
+
+If the corruption is in a path in the closure of the NixOS system
+configuration, you can fix it by doing
+
+```ShellSession
+# nixos-rebuild switch --repair
+```
+
+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.
+
+You can also scan the entire Nix store for corrupt paths:
+
+```ShellSession
+# nix-store --verify --check-contents --repair
+```
+
+Any corrupt paths will be redownloaded if they're available in a binary
+cache; otherwise, they cannot be repaired.
diff --git a/nixos/doc/manual/administration/troubleshooting.chapter.md b/nixos/doc/manual/administration/troubleshooting.chapter.md
new file mode 100644
index 00000000000..548456eaf6d
--- /dev/null
+++ b/nixos/doc/manual/administration/troubleshooting.chapter.md
@@ -0,0 +1,12 @@
+# Troubleshooting {#ch-troubleshooting}
+
+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" />
+```
diff --git a/nixos/doc/manual/administration/user-sessions.chapter.md b/nixos/doc/manual/administration/user-sessions.chapter.md
new file mode 100644
index 00000000000..5ff468b3012
--- /dev/null
+++ b/nixos/doc/manual/administration/user-sessions.chapter.md
@@ -0,0 +1,43 @@
+# User Sessions {#sec-user-sessions}
+
+Systemd keeps track of all users who are logged into the system (e.g. on
+a virtual console or remotely via SSH). The command `loginctl` allows
+querying and manipulating user sessions. For instance, to list all user
+sessions:
+
+```ShellSession
+$ loginctl
+   SESSION        UID USER             SEAT
+        c1        500 eelco            seat0
+        c3          0 root             seat0
+        c4        500 alice
+```
+
+This shows that two users are logged in locally, while another is logged
+in remotely. ("Seats" 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:
+
+```ShellSession
+$ 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
+```
+
+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:
+
+```ShellSession
+# loginctl terminate-session c3
+```
diff --git a/nixos/doc/manual/configuration/abstractions.section.md b/nixos/doc/manual/configuration/abstractions.section.md
new file mode 100644
index 00000000000..bf26e4c51ed
--- /dev/null
+++ b/nixos/doc/manual/configuration/abstractions.section.md
@@ -0,0 +1,80 @@
+# Abstractions {#sec-module-abstractions}
+
+If you find yourself repeating yourself over and over, it’s time to abstract. Take, for instance, this Apache HTTP Server configuration:
+
+```nix
+{
+  services.httpd.virtualHosts =
+    { "blog.example.org" = {
+        documentRoot = "/webroot/blog.example.org";
+        adminAddr = "alice@example.org";
+        forceSSL = true;
+        enableACME = true;
+        enablePHP = true;
+      };
+      "wiki.example.org" = {
+        documentRoot = "/webroot/wiki.example.org";
+        adminAddr = "alice@example.org";
+        forceSSL = true;
+        enableACME = true;
+        enablePHP = true;
+      };
+    };
+}
+```
+
+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 `let`:
+```nix
+let
+  commonConfig =
+    { adminAddr = "alice@example.org";
+      forceSSL = true;
+      enableACME = true;
+    };
+in
+{
+  services.httpd.virtualHosts =
+    { "blog.example.org" = (commonConfig // { documentRoot = "/webroot/blog.example.org"; });
+      "wiki.example.org" = (commonConfig // { documentRoot = "/webroot/wiki.example.com"; });
+    };
+}
+```
+
+The `let commonConfig = ...` defines a variable named `commonConfig`. The `//` operator merges two attribute sets, so the configuration of the second virtual host is the set `commonConfig` extended with the document root option.
+
+You can write a `let` wherever an expression is allowed. Thus, you also could have written:
+
+```nix
+{
+  services.httpd.virtualHosts =
+    let commonConfig = ...; in
+    { "blog.example.org" = (commonConfig // { ... })
+      "wiki.example.org" = (commonConfig // { ... })
+    };
+}
+```
+
+but not `{ let commonConfig = ...; in ...; }` since attributes (as opposed to attribute values) are not expressions.
+
+**Functions** 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:
+
+```nix
+{
+  services.httpd.virtualHosts =
+    let
+      makeVirtualHost = webroot:
+        { documentRoot = webroot;
+          adminAddr = "alice@example.org";
+          forceSSL = true;
+          enableACME = true;
+        };
+    in
+      { "example.org" = (makeVirtualHost "/webroot/example.org");
+        "example.com" = (makeVirtualHost "/webroot/example.com");
+        "example.gov" = (makeVirtualHost "/webroot/example.gov");
+        "example.nl" = (makeVirtualHost "/webroot/example.nl");
+      };
+}
+```
+
+Here, `makeVirtualHost` is a function that takes a single argument `webroot` and returns the configuration for a virtual host. That function is then called for several names to produce the list of virtual host configurations.
diff --git a/nixos/doc/manual/configuration/ad-hoc-network-config.section.md b/nixos/doc/manual/configuration/ad-hoc-network-config.section.md
new file mode 100644
index 00000000000..4478d77f361
--- /dev/null
+++ b/nixos/doc/manual/configuration/ad-hoc-network-config.section.md
@@ -0,0 +1,13 @@
+# Ad-Hoc Configuration {#ad-hoc-network-config}
+
+You can use [](#opt-networking.localCommands) to
+specify shell commands to be run at the end of `network-setup.service`. This
+is useful for doing network configuration not covered by the existing NixOS
+modules. For instance, to statically configure an IPv6 address:
+
+```nix
+networking.localCommands =
+  ''
+    ip -6 addr add 2001:610:685:1::1/64 dev eth0
+  '';
+```
diff --git a/nixos/doc/manual/configuration/ad-hoc-packages.section.md b/nixos/doc/manual/configuration/ad-hoc-packages.section.md
new file mode 100644
index 00000000000..e9d574903a1
--- /dev/null
+++ b/nixos/doc/manual/configuration/ad-hoc-packages.section.md
@@ -0,0 +1,51 @@
+# Ad-Hoc Package Management {#sec-ad-hoc-packages}
+
+With the command `nix-env`, you can install and uninstall packages from
+the command line. For instance, to install Mozilla Thunderbird:
+
+```ShellSession
+$ nix-env -iA nixos.thunderbird
+```
+
+If you invoke this as root, the package is installed in the Nix profile
+`/nix/var/nix/profiles/default` and visible to all users of the system;
+otherwise, the package ends up in
+`/nix/var/nix/profiles/per-user/username/profile` and is not visible to
+other users. The `-A` flag specifies the package by its attribute name;
+without it, the package is installed by matching against its package
+name (e.g. `thunderbird`). The latter is slower because it requires
+matching against all available Nix packages, and is ambiguous if there
+are multiple matching packages.
+
+Packages come from the NixOS channel. You typically upgrade a package by
+updating to the latest version of the NixOS channel:
+
+```ShellSession
+$ nix-channel --update nixos
+```
+
+and then running `nix-env -i` again. Other packages in the profile are
+*not* affected; this is the crucial difference with the declarative
+style of package management, where running `nixos-rebuild switch` 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:
+
+```ShellSession
+$ nix-env -u '*'
+```
+
+A package can be uninstalled using the `-e` flag:
+
+```ShellSession
+$ nix-env -e thunderbird
+```
+
+Finally, you can roll back an undesirable `nix-env` action:
+
+```ShellSession
+$ nix-env --rollback
+```
+
+`nix-env` has many more flags. For details, see the nix-env(1) manpage or
+the Nix manual.
diff --git a/nixos/doc/manual/configuration/adding-custom-packages.section.md b/nixos/doc/manual/configuration/adding-custom-packages.section.md
new file mode 100644
index 00000000000..5d1198fb0f4
--- /dev/null
+++ b/nixos/doc/manual/configuration/adding-custom-packages.section.md
@@ -0,0 +1,74 @@
+# Adding Custom Packages {#sec-custom-packages}
+
+It's possible that a package you need is not available in NixOS. In that
+case, you can do two things. First, you can clone the Nixpkgs
+repository, add the package to your clone, and (optionally) submit a
+patch or pull request to have it accepted into the main Nixpkgs repository.
+This is described in detail in the [Nixpkgs manual](https://nixos.org/nixpkgs/manual).
+In short, you clone Nixpkgs:
+
+```ShellSession
+$ git clone https://github.com/NixOS/nixpkgs
+$ cd nixpkgs
+```
+
+Then you write and test the package as described in the Nixpkgs manual.
+Finally, you add it to [](#opt-environment.systemPackages), e.g.
+
+```nix
+environment.systemPackages = [ pkgs.my-package ];
+```
+
+and you run `nixos-rebuild`, specifying your own Nixpkgs tree:
+
+```ShellSession
+# nixos-rebuild switch -I nixpkgs=/path/to/my/nixpkgs
+```
+
+The second possibility is to add the package outside of the Nixpkgs
+tree. For instance, here is how you specify a build of the
+[GNU Hello](https://www.gnu.org/software/hello/) package directly in
+`configuration.nix`:
+
+```nix
+environment.systemPackages =
+  let
+    my-hello = with pkgs; stdenv.mkDerivation rec {
+      name = "hello-2.8";
+      src = fetchurl {
+        url = "mirror://gnu/hello/${name}.tar.gz";
+        sha256 = "0wqd8sjmxfskrflaxywc7gqw7sfawrfvdxd9skxawzfgyy0pzdz6";
+      };
+    };
+  in
+  [ my-hello ];
+```
+
+Of course, you can also move the definition of `my-hello` into a
+separate Nix expression, e.g.
+
+```nix
+environment.systemPackages = [ (import ./my-hello.nix) ];
+```
+
+where `my-hello.nix` contains:
+
+```nix
+with import <nixpkgs> {}; # bring all of Nixpkgs into scope
+
+stdenv.mkDerivation rec {
+  name = "hello-2.8";
+  src = fetchurl {
+    url = "mirror://gnu/hello/${name}.tar.gz";
+    sha256 = "0wqd8sjmxfskrflaxywc7gqw7sfawrfvdxd9skxawzfgyy0pzdz6";
+  };
+}
+```
+
+This allows testing the package easily:
+
+```ShellSession
+$ nix-build my-hello.nix
+$ ./result/bin/hello
+Hello, world!
+```
diff --git a/nixos/doc/manual/configuration/config-file.section.md b/nixos/doc/manual/configuration/config-file.section.md
new file mode 100644
index 00000000000..f21ba113bf8
--- /dev/null
+++ b/nixos/doc/manual/configuration/config-file.section.md
@@ -0,0 +1,175 @@
+# NixOS Configuration File {#sec-configuration-file}
+
+The NixOS configuration file generally looks like this:
+
+```nix
+{ config, pkgs, ... }:
+
+{ option definitions
+}
+```
+
+The first line (`{ config, pkgs, ... }:`) denotes that this is actually
+a function that takes at least the two arguments `config` and `pkgs`.
+(These are explained later, in chapter [](#sec-writing-modules)) The
+function returns a *set* of option definitions (`{ ... }`).
+These definitions have the form `name = value`, where `name` is the
+name of an option and `value` is its value. For example,
+
+```nix
+{ config, pkgs, ... }:
+
+{ services.httpd.enable = true;
+  services.httpd.adminAddr = "alice@example.org";
+  services.httpd.virtualHosts.localhost.documentRoot = "/webroot";
+}
+```
+
+defines a configuration with three option definitions that together
+enable the Apache HTTP Server with `/webroot` as the document root.
+
+Sets can be nested, and in fact dots in option names are shorthand for
+defining a set containing another set. For instance,
+[](#opt-services.httpd.enable) defines a set named
+`services` that contains a set named `httpd`, which in turn contains an
+option definition named `enable` with value `true`. This means that the
+example above can also be written as:
+
+```nix
+{ config, pkgs, ... }:
+
+{ services = {
+    httpd = {
+      enable = true;
+      adminAddr = "alice@example.org";
+      virtualHosts = {
+        localhost = {
+          documentRoot = "/webroot";
+        };
+      };
+    };
+  };
+}
+```
+
+which may be more convenient if you have lots of option definitions that
+share the same prefix (such as `services.httpd`).
+
+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 *option declaration*), `nixos-rebuild` will give an error
+like:
+
+```plain
+The option `services.httpd.enable' defined in `/etc/nixos/configuration.nix' does not exist.
+```
+
+Likewise, values in option definitions must have a correct type. For
+instance, `services.httpd.enable` must be a Boolean (`true` or `false`).
+Trying to give it a value of another type, such as a string, will cause
+an error:
+
+```plain
+The option value `services.httpd.enable' in `/etc/nixos/configuration.nix' is not a boolean.
+```
+
+Options have various types of values. The most important are:
+
+Strings
+
+:   Strings are enclosed in double quotes, e.g.
+
+    ```nix
+    networking.hostName = "dexter";
+    ```
+
+    Special characters can be escaped by prefixing them with a backslash
+    (e.g. `\"`).
+
+    Multi-line strings can be enclosed in *double single quotes*, e.g.
+
+    ```nix
+    networking.extraHosts =
+      ''
+        127.0.0.2 other-localhost
+        10.0.0.1 server
+      '';
+    ```
+
+    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 `"` and `\` are not special (making it more convenient for
+    including things like shell code). See more info about this in the
+    Nix manual [here](https://nixos.org/nix/manual/#ssec-values).
+
+Booleans
+
+:   These can be `true` or `false`, e.g.
+
+    ```nix
+    networking.firewall.enable = true;
+    networking.firewall.allowPing = false;
+    ```
+
+Integers
+
+:   For example,
+
+    ```nix
+    boot.kernel.sysctl."net.ipv4.tcp_keepalive_time" = 60;
+    ```
+
+    (Note that here the attribute name `net.ipv4.tcp_keepalive_time` is
+    enclosed in quotes to prevent it from being interpreted as a set
+    named `net` containing a set named `ipv4`, and so on. This is
+    because it's not a NixOS option but the literal name of a Linux
+    kernel setting.)
+
+Sets
+
+:   Sets were introduced above. They are name/value pairs enclosed in
+    braces, as in the option definition
+
+    ```nix
+    fileSystems."/boot" =
+      { device = "/dev/sda1";
+        fsType = "ext4";
+        options = [ "rw" "data=ordered" "relatime" ];
+      };
+    ```
+
+Lists
+
+:   The important thing to note about lists is that list elements are
+    separated by whitespace, like this:
+
+    ```nix
+    boot.kernelModules = [ "fuse" "kvm-intel" "coretemp" ];
+    ```
+
+    List elements can be any other type, e.g. sets:
+
+    ```nix
+    swapDevices = [ { device = "/dev/disk/by-label/swap"; } ];
+    ```
+
+Packages
+
+:   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 `pkgs`. Typical uses:
+
+    ```nix
+    environment.systemPackages =
+      [ pkgs.thunderbird
+        pkgs.emacs
+      ];
+
+    services.postgresql.package = pkgs.postgresql_10;
+    ```
+
+    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
+    [](#sec-custom-packages).
diff --git a/nixos/doc/manual/configuration/config-syntax.chapter.md b/nixos/doc/manual/configuration/config-syntax.chapter.md
new file mode 100644
index 00000000000..56d093c0f6e
--- /dev/null
+++ b/nixos/doc/manual/configuration/config-syntax.chapter.md
@@ -0,0 +1,19 @@
+# Configuration Syntax {#sec-configuration-syntax}
+
+The NixOS configuration file `/etc/nixos/configuration.nix` is actually
+a *Nix expression*, 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 [Nix
+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" />
+<xi:include href="summary.section.xml" />
+```
diff --git a/nixos/doc/manual/configuration/configuration.xml b/nixos/doc/manual/configuration/configuration.xml
new file mode 100644
index 00000000000..b04316cfa48
--- /dev/null
+++ b/nixos/doc/manual/configuration/configuration.xml
@@ -0,0 +1,31 @@
+<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/customizing-packages.section.md b/nixos/doc/manual/configuration/customizing-packages.section.md
new file mode 100644
index 00000000000..bceeeb2d7a1
--- /dev/null
+++ b/nixos/doc/manual/configuration/customizing-packages.section.md
@@ -0,0 +1,74 @@
+# Customising Packages {#sec-customising-packages}
+
+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 `configuration.nix` as follows:
+`nixpkgs.config.firefox.enableGoogleTalkPlugin = true;`
+
+::: {.warning}
+Unfortunately, Nixpkgs currently lacks a way to query available
+configuration options.
+:::
+
+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:
+
+```nix
+environment.systemPackages = [ (pkgs.emacs.override { gtk = pkgs.gtk3; }) ];
+```
+
+The function `override` 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 `gtk` gets the
+value `pkgs.gtk3`, 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,
+[](#opt-environment.systemPackages)
+would be a list with two elements.)
+
+Even greater customisation is possible using the function
+`overrideAttrs`. While the `override` mechanism above overrides the
+arguments of a package function, `overrideAttrs` allows changing the
+*attributes* passed to `mkDerivation`. 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:
+
+```nix
+environment.systemPackages = [
+  (pkgs.emacs.overrideAttrs (oldAttrs: {
+    name = "emacs-25.0-pre";
+    src = /path/to/my/emacs/tree;
+  }))
+];
+```
+
+Here, `overrideAttrs` takes the Nix derivation specified by `pkgs.emacs`
+and produces a new derivation in which the original's `name` and `src`
+attribute have been replaced by the given values by re-calling
+`stdenv.mkDerivation`. The original attributes are accessible via the
+function argument, which is conventionally named `oldAttrs`.
+
+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 *global* override as follows:
+
+```nix
+nixpkgs.config.packageOverrides = pkgs:
+  { emacs = pkgs.emacs.override { gtk = pkgs.gtk3; };
+  };
+```
+
+The effect of this definition is essentially equivalent to modifying the
+`emacs` attribute in the Nixpkgs source tree. Any package in Nixpkgs
+that depends on `emacs` will be passed your customised instance.
+(However, the value `pkgs.emacs` in `nixpkgs.config.packageOverrides`
+refers to the original rather than overridden instance, to prevent an
+infinite recursion.)
diff --git a/nixos/doc/manual/configuration/declarative-packages.section.md b/nixos/doc/manual/configuration/declarative-packages.section.md
new file mode 100644
index 00000000000..337cdf8472e
--- /dev/null
+++ b/nixos/doc/manual/configuration/declarative-packages.section.md
@@ -0,0 +1,46 @@
+# Declarative Package Management {#sec-declarative-package-mgmt}
+
+With declarative package management, you specify which packages you want
+on your system by setting the option
+[](#opt-environment.systemPackages). For instance, adding the
+following line to `configuration.nix` enables the Mozilla Thunderbird
+email application:
+
+```nix
+environment.systemPackages = [ pkgs.thunderbird ];
+```
+
+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
+`nixos-rebuild switch`.
+
+::: {.note}
+Some packages require additional global configuration such as D-Bus or
+systemd service registration so adding them to
+[](#opt-environment.systemPackages) might not be sufficient. You are
+advised to check the [list of options](#ch-options) whether a NixOS
+module for the package does not exist.
+:::
+
+You can get a list of the available packages as follows:
+
+```ShellSession
+$ nix-env -qaP '*' --description
+nixos.firefox   firefox-23.0   Mozilla Firefox - the browser, reloaded
+...
+```
+
+The first column in the output is the *attribute name*, such as
+`nixos.thunderbird`.
+
+Note: the `nixos` prefix tells us that we want to get the package from
+the `nixos` channel and works only in CLI tools. In declarative
+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" />
+```
diff --git a/nixos/doc/manual/configuration/file-systems.chapter.md b/nixos/doc/manual/configuration/file-systems.chapter.md
new file mode 100644
index 00000000000..901e2e4f181
--- /dev/null
+++ b/nixos/doc/manual/configuration/file-systems.chapter.md
@@ -0,0 +1,42 @@
+# File Systems {#ch-file-systems}
+
+You can define file systems using the `fileSystems` configuration
+option. For instance, the following definition causes NixOS to mount the
+Ext4 file system on device `/dev/disk/by-label/data` onto the mount
+point `/data`:
+
+```nix
+fileSystems."/data" =
+  { device = "/dev/disk/by-label/data";
+    fsType = "ext4";
+  };
+```
+
+This will create an entry in `/etc/fstab`, which will generate a
+corresponding [systemd.mount](https://www.freedesktop.org/software/systemd/man/systemd.mount.html)
+unit via [systemd-fstab-generator](https://www.freedesktop.org/software/systemd/man/systemd-fstab-generator.html).
+The filesystem will be mounted automatically unless `"noauto"` is
+present in [options](#opt-fileSystems._name_.options). `"noauto"`
+filesystems can be mounted explicitly using `systemctl` e.g.
+`systemctl start data.mount`. Mount points are created automatically if they don't
+already exist. For `device`, it's best to use the topology-independent
+device aliases in `/dev/disk/by-label` and `/dev/disk/by-uuid`, as these
+don't change if the topology changes (e.g. if a disk is moved to another
+IDE controller).
+
+You can usually omit the file system type (`fsType`), since `mount` 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 `ext2`, `ext3` or `ext4`, then it's best
+to specify `fsType` to ensure that the kernel module is available.
+
+::: {.note}
+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 `options = [ "nofail" ];`.
+:::
+
+```{=docbook}
+<xi:include href="luks-file-systems.section.xml" />
+<xi:include href="sshfs-file-systems.section.xml" />
+```
diff --git a/nixos/doc/manual/configuration/firewall.section.md b/nixos/doc/manual/configuration/firewall.section.md
new file mode 100644
index 00000000000..dbf0ffb9273
--- /dev/null
+++ b/nixos/doc/manual/configuration/firewall.section.md
@@ -0,0 +1,32 @@
+# Firewall {#sec-firewall}
+
+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:
+
+```nix
+networking.firewall.enable = false;
+```
+
+If the firewall is enabled, you can open specific TCP ports to the
+outside world:
+
+```nix
+networking.firewall.allowedTCPPorts = [ 80 443 ];
+```
+
+Note that TCP port 22 (ssh) is opened automatically if the SSH daemon is
+enabled (`services.openssh.enable = true`). UDP ports can be opened through
+[](#opt-networking.firewall.allowedUDPPorts).
+
+To open ranges of TCP ports:
+
+```nix
+networking.firewall.allowedTCPPortRanges = [
+  { from = 4000; to = 4007; }
+  { from = 8000; to = 8010; }
+];
+```
+
+Similarly, UDP port ranges can be opened through
+[](#opt-networking.firewall.allowedUDPPortRanges).
diff --git a/nixos/doc/manual/configuration/gpu-accel.chapter.md b/nixos/doc/manual/configuration/gpu-accel.chapter.md
new file mode 100644
index 00000000000..08b6af5d98a
--- /dev/null
+++ b/nixos/doc/manual/configuration/gpu-accel.chapter.md
@@ -0,0 +1,204 @@
+# GPU acceleration {#sec-gpu-accel}
+
+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.
+
+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.
+
+## OpenCL {#sec-gpu-accel-opencl}
+
+[OpenCL](https://en.wikipedia.org/wiki/OpenCL) is a general compute API.
+It is used by various applications such as Blender and Darktable to
+accelerate certain operations.
+
+OpenCL applications load drivers through the *Installable Client Driver*
+(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 `OCL_ICD_VENDORS` environment variable. This variable can contain a
+directory which is scanned by the ICL loader for ICD files. For example:
+
+```ShellSession
+$ export \
+  OCL_ICD_VENDORS=`nix-build '<nixpkgs>' --no-out-link -A rocm-opencl-icd`/etc/OpenCL/vendors/
+```
+
+The second mechanism is to add the OpenCL driver package to
+[](#opt-hardware.opengl.extraPackages).
+This links the ICD file under `/run/opengl-driver`, where it will be visible
+to the ICD loader.
+
+The proper installation of OpenCL drivers can be verified through the
+`clinfo` command of the clinfo package. This command will report the
+number of hardware devices that is found and give detailed information
+for each device:
+
+```ShellSession
+$ clinfo | head -n3
+Number of platforms  1
+Platform Name        AMD Accelerated Parallel Processing
+Platform Vendor      Advanced Micro Devices, Inc.
+```
+
+### AMD {#sec-gpu-accel-opencl-amd}
+
+Modern AMD [Graphics Core
+Next](https://en.wikipedia.org/wiki/Graphics_Core_Next) (GCN) GPUs are
+supported through the rocm-opencl-icd package. Adding this package to
+[](#opt-hardware.opengl.extraPackages)
+enables OpenCL support:
+
+```nix
+hardware.opengl.extraPackages = [
+  rocm-opencl-icd
+];
+```
+
+### Intel {#sec-gpu-accel-opencl-intel}
+
+[Intel Gen8 and later
+GPUs](https://en.wikipedia.org/wiki/List_of_Intel_graphics_processing_units#Gen8)
+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.
+
+The intel-compute-runtime, beignet, or intel-ocl package can be added to
+[](#opt-hardware.opengl.extraPackages)
+to enable OpenCL support. For example, for Gen8 and later GPUs, the following
+configuration can be used:
+
+```nix
+hardware.opengl.extraPackages = [
+  intel-compute-runtime
+];
+```
+
+## Vulkan {#sec-gpu-accel-vulkan}
+
+[Vulkan](https://en.wikipedia.org/wiki/Vulkan_(API)) is a graphics and
+compute API for GPUs. It is used directly by games or indirectly though
+compatibility layers like
+[DXVK](https://github.com/doitsujin/dxvk/wiki).
+
+By default, if [](#opt-hardware.opengl.driSupport)
+is enabled, mesa is installed and provides Vulkan for supported hardware.
+
+Similar to OpenCL, Vulkan drivers are loaded through the *Installable
+Client Driver* (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.
+
+The first option is through the `VK_ICD_FILENAMES` environment variable.
+This variable can contain multiple JSON files, separated by `:`. For
+example:
+
+```ShellSession
+$ export \
+  VK_ICD_FILENAMES=`nix-build '<nixpkgs>' --no-out-link -A amdvlk`/share/vulkan/icd.d/amd_icd64.json
+```
+
+The second mechanism is to add the Vulkan driver package to
+[](#opt-hardware.opengl.extraPackages).
+This links the ICD file under `/run/opengl-driver`, where it will be
+visible to the ICD loader.
+
+The proper installation of Vulkan drivers can be verified through the
+`vulkaninfo` command of the vulkan-tools package. This command will
+report the hardware devices and drivers found, in this example output
+amdvlk and radv:
+
+```ShellSession
+$ 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
+```
+
+A simple graphical application that uses Vulkan is `vkcube` from the
+vulkan-tools package.
+
+### AMD {#sec-gpu-accel-vulkan-amd}
+
+Modern AMD [Graphics Core
+Next](https://en.wikipedia.org/wiki/Graphics_Core_Next) (GCN) GPUs are
+supported through either radv, which is part of mesa, or the amdvlk
+package. Adding the amdvlk package to
+[](#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:
+
+```nix
+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 = "RADV";
+# Or
+environment.variables.VK_ICD_FILENAMES =
+  "/run/opengl-driver/share/vulkan/icd.d/radeon_icd.x86_64.json";
+```
+
+## Common issues {#sec-gpu-accel-common-issues}
+
+### User permissions {#sec-gpu-accel-common-issues-permissions}
+
+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
+(`/dev/dri/renderD*`) or are tagged as `uaccess` (`/dev/dri/card*`). The
+access control lists of devices with the `uaccess` tag will be updated
+automatically when a user logs in through `systemd-logind`. For example,
+if the user *jane* is logged in, the access control list should look as
+follows:
+
+```ShellSession
+$ getfacl /dev/dri/card0
+# file: dev/dri/card0
+# owner: root
+# group: video
+user::rw-
+user:jane:rw-
+group::rw-
+mask::rw-
+other::---
+```
+
+If you disabled (this functionality of) `systemd-logind`, you may need
+to add the user to the `video` group and log in again.
+
+### Mixing different versions of nixpkgs {#sec-gpu-accel-common-issues-mixing-nixpkgs}
+
+The *Installable Client Driver* (ICD) mechanism used by OpenCL and
+Vulkan loads runtimes into its address space using `dlopen`. 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.
+
+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 `LD_DEBUG` variable set to get more diagnostic information. For
+example, OpenCL can be tested with `LD_DEBUG=files clinfo`, which should
+report missing symbols.
diff --git a/nixos/doc/manual/configuration/ipv4-config.section.md b/nixos/doc/manual/configuration/ipv4-config.section.md
new file mode 100644
index 00000000000..c73024b856d
--- /dev/null
+++ b/nixos/doc/manual/configuration/ipv4-config.section.md
@@ -0,0 +1,35 @@
+# IPv4 Configuration {#sec-ipv4}
+
+By default, NixOS uses DHCP (specifically, `dhcpcd`) to automatically
+configure network interfaces. However, you can configure an interface
+manually as follows:
+
+```nix
+networking.interfaces.eth0.ipv4.addresses = [ {
+  address = "192.168.1.2";
+  prefixLength = 24;
+} ];
+```
+
+Typically you'll also want to set a default gateway and set of name
+servers:
+
+```nix
+networking.defaultGateway = "192.168.1.1";
+networking.nameservers = [ "8.8.8.8" ];
+```
+
+::: {.note}
+Statically configured interfaces are set up by the systemd service
+`interface-name-cfg.service`. The default gateway and name server
+configuration is performed by `network-setup.service`.
+:::
+
+The host name is set using [](#opt-networking.hostName):
+
+```nix
+networking.hostName = "cartman";
+```
+
+The default host name is `nixos`. Set it to the empty string (`""`) to
+allow the DHCP server to provide the host name.
diff --git a/nixos/doc/manual/configuration/ipv6-config.section.md b/nixos/doc/manual/configuration/ipv6-config.section.md
new file mode 100644
index 00000000000..ce66f53ed47
--- /dev/null
+++ b/nixos/doc/manual/configuration/ipv6-config.section.md
@@ -0,0 +1,42 @@
+# IPv6 Configuration {#sec-ipv6}
+
+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 [](#opt-networking.tempAddresses). This option
+may be overridden on a per-interface basis by
+[](#opt-networking.interfaces._name_.tempAddress). You can disable
+IPv6 support globally by setting:
+
+```nix
+networking.enableIPv6 = false;
+```
+
+You can disable IPv6 on a single interface using a normal sysctl (in
+this example, we use interface `eth0`):
+
+```nix
+boot.kernel.sysctl."net.ipv6.conf.eth0.disable_ipv6" = true;
+```
+
+As with IPv4 networking interfaces are automatically configured via
+DHCPv6. You can configure an interface manually:
+
+```nix
+networking.interfaces.eth0.ipv6.addresses = [ {
+  address = "fe00:aa:bb:cc::2";
+  prefixLength = 64;
+} ];
+```
+
+For configuring a gateway, optionally with explicitly specified
+interface:
+
+```nix
+networking.defaultGateway6 = {
+  address = "fe00::1";
+  interface = "enp0s3";
+};
+```
+
+See [](#sec-ipv4) for similar examples and additional information.
diff --git a/nixos/doc/manual/configuration/kubernetes.chapter.md b/nixos/doc/manual/configuration/kubernetes.chapter.md
new file mode 100644
index 00000000000..93787577be9
--- /dev/null
+++ b/nixos/doc/manual/configuration/kubernetes.chapter.md
@@ -0,0 +1,104 @@
+# Kubernetes {#sec-kubernetes}
+
+The NixOS Kubernetes module is a collective term for a handful of
+individual submodules implementing the Kubernetes cluster components.
+
+There are generally two ways of enabling Kubernetes on NixOS. One way is
+to enable and configure cluster components appropriately by hand:
+
+```nix
+services.kubernetes = {
+  apiserver.enable = true;
+  controllerManager.enable = true;
+  scheduler.enable = true;
+  addonManager.enable = true;
+  proxy.enable = true;
+  flannel.enable = true;
+};
+```
+
+Another way is to assign cluster roles (\"master\" and/or \"node\") to
+the host. This enables apiserver, controllerManager, scheduler,
+addonManager, kube-proxy and etcd:
+
+```nix
+services.kubernetes.roles = [ "master" ];
+```
+
+While this will enable the kubelet and kube-proxy only:
+
+```nix
+services.kubernetes.roles = [ "node" ];
+```
+
+Assigning both the master and node roles is usable if you want a single
+node Kubernetes cluster for dev or testing purposes:
+
+```nix
+services.kubernetes.roles = [ "master" "node" ];
+```
+
+Note: Assigning either role will also default both
+[](#opt-services.kubernetes.flannel.enable)
+and [](#opt-services.kubernetes.easyCerts)
+to true. This sets up flannel as CNI and activates automatic PKI bootstrapping.
+
+As of kubernetes 1.10.X it has been deprecated to open non-tls-enabled
+ports on kubernetes components. Thus, from NixOS 19.03 all plain HTTP
+ports have been disabled by default. While opening insecure ports is
+still possible, it is recommended not to bind these to other interfaces
+than loopback. To re-enable the insecure port on the apiserver, see options:
+[](#opt-services.kubernetes.apiserver.insecurePort) and
+[](#opt-services.kubernetes.apiserver.insecureBindAddress)
+
+::: {.note}
+As of NixOS 19.03, it is mandatory to configure:
+[](#opt-services.kubernetes.masterAddress).
+The masterAddress must be resolveable and routeable by all cluster nodes.
+In single node clusters, this can be set to `localhost`.
+:::
+
+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: [ Using RBAC
+Authorization](https://kubernetes.io/docs/reference/access-authn-authz/rbac/).
+
+The NixOS kubernetes module provides an option for automatic certificate
+bootstrapping and configuration,
+[](#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 [](#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.
+
+To add new nodes to the cluster: On any (non-master) cluster node where
+[](#opt-services.kubernetes.easyCerts)
+is enabled, the helper script `nixos-kubernetes-node-join` 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.
+
+::: {.note}
+Multi-master (HA) clusters are not supported by the easyCerts module.
+:::
+
+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
+`/etc/kubernetes/cluster-admin.kubeconfig` as determined by
+[](#opt-services.kubernetes.pki.etcClusterAdminKubeconfig).
+`export KUBECONFIG=/etc/kubernetes/cluster-admin.kubeconfig` 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.
diff --git a/nixos/doc/manual/configuration/linux-kernel.chapter.md b/nixos/doc/manual/configuration/linux-kernel.chapter.md
new file mode 100644
index 00000000000..1d06543d4f1
--- /dev/null
+++ b/nixos/doc/manual/configuration/linux-kernel.chapter.md
@@ -0,0 +1,140 @@
+# Linux Kernel {#sec-kernel-config}
+
+You can override the Linux kernel and associated packages using the
+option `boot.kernelPackages`. For instance, this selects the Linux 3.10
+kernel:
+
+```nix
+boot.kernelPackages = pkgs.linuxKernel.packages.linux_3_10;
+```
+
+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.
+
+While `pkgs.linuxKernel.packages` contains all available kernel packages,
+you may want to use one of the unversioned `pkgs.linuxPackages_*` aliases
+such as `pkgs.linuxPackages_latest`, that are kept up to date with new
+versions.
+
+The default Linux kernel configuration should be fine for most users.
+You can see the configuration of your current kernel with the following
+command:
+
+```ShellSession
+zcat /proc/config.gz
+```
+
+If you want to change the kernel configuration, you can use the
+`packageOverrides` feature (see [](#sec-customising-packages)). For
+instance, to enable support for the kernel debugger KGDB:
+
+```nix
+nixpkgs.config.packageOverrides = pkgs: pkgs.lib.recursiveUpdate pkgs {
+  linuxKernel.kernels.linux_5_10 = pkgs.linuxKernel.kernels.linux_5_10.override {
+    extraConfig = ''
+      KGDB y
+    '';
+  };
+};
+```
+
+`extraConfig` takes a list of Linux kernel configuration options, one
+per line. The name of the option should not include the prefix
+`CONFIG_`. The option value is typically `y`, `n` or `m` (to build
+something as a kernel module).
+
+Kernel modules for hardware devices are generally loaded automatically
+by `udev`. You can force a module to be loaded via
+[](#opt-boot.kernelModules), e.g.
+
+```nix
+boot.kernelModules = [ "fuse" "kvm-intel" "coretemp" ];
+```
+
+If the module is required early during the boot (e.g. to mount the root
+file system), you can use [](#opt-boot.initrd.kernelModules):
+
+```nix
+boot.initrd.kernelModules = [ "cifs" ];
+```
+
+This causes the specified modules and their dependencies to be added to
+the initial ramdisk.
+
+Kernel runtime parameters can be set through
+[](#opt-boot.kernel.sysctl), e.g.
+
+```nix
+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}
+
+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`:
+
+```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;
+};
+```
+
+You can edit the config with this snippet (by default `make
+   menuconfig` won\'t work out of the box on nixos):
+
+```ShellSession
+nix-shell -E 'with import <nixpkgs> {}; kernelToOverride.overrideAttrs (o: {nativeBuildInputs=o.nativeBuildInputs ++ [ pkg-config ncurses ];})'
+```
+
+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`).
+
+```nix
+mptcp93.override ({
+  name="mptcp-local";
+
+  ignoreConfigErrors = true;
+  autoModules = false;
+  kernelPreferBuiltin = true;
+
+  enableParallelBuilding = true;
+
+  extraConfig = ''
+    DEBUG_KERNEL y
+    FRAME_POINTER y
+    KGDB y
+    KGDB_SERIAL_CONSOLE y
+    DEBUG_INFO y
+  '';
+});
+```
+
+## Developing kernel modules {#sec-linux-config-developing-modules}
+
+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.
+
+```ShellSession
+$ nix-build '<nixpkgs>' -A linuxPackages.kernel.dev
+$ nix-shell '<nixpkgs>' -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
+```
diff --git a/nixos/doc/manual/configuration/luks-file-systems.section.md b/nixos/doc/manual/configuration/luks-file-systems.section.md
new file mode 100644
index 00000000000..b5d0407d165
--- /dev/null
+++ b/nixos/doc/manual/configuration/luks-file-systems.section.md
@@ -0,0 +1,77 @@
+# LUKS-Encrypted File Systems {#sec-luks-file-systems}
+
+NixOS supports file systems that are encrypted using *LUKS* (Linux
+Unified Key Setup). For example, here is how you create an encrypted
+Ext4 file system on the device
+`/dev/disk/by-uuid/3f6b0024-3a44-4fde-a43a-767b872abe5d`:
+
+```ShellSession
+# 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
+```
+
+The LUKS volume should be automatically picked up by
+`nixos-generate-config`, but you might want to verify that your
+`hardware-configuration.nix` looks correct. To manually ensure that the
+system is automatically mounted at boot time as `/`, add the following
+to `configuration.nix`:
+
+```nix
+boot.initrd.luks.devices.crypted.device = "/dev/disk/by-uuid/3f6b0024-3a44-4fde-a43a-767b872abe5d";
+fileSystems."/".device = "/dev/mapper/crypted";
+```
+
+Should grub be used as bootloader, and `/boot` is located on an
+encrypted partition, it is necessary to add the following grub option:
+
+```nix
+boot.loader.grub.enableCryptodisk = true;
+```
+
+## FIDO2 {#sec-luks-file-systems-fido2}
+
+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
+`/dev/sda2`:
+
+```ShellSession
+# export FIDO2_LABEL="/dev/sda2 @ $HOSTNAME"
+# fido2luks credential "$FIDO2_LABEL"
+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
+```
+
+To ensure that this file system is decrypted using the FIDO2 compatible
+key, add the following to `configuration.nix`:
+
+```nix
+boot.initrd.luks.fido2Support = true;
+boot.initrd.luks.devices."/dev/sda2".fido2.credential = "f1d00200108b9d6e849a8b388da457688e3dd653b4e53770012d8f28e5d3b269865038c346802f36f3da7278b13ad6a3bb6a1452e24ebeeaa24ba40eef559b1b287d2a2f80b7";
+```
+
+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 [Trezor](https://trezor.io/).
+
+```nix
+boot.initrd.luks.devices."/dev/sda2".fido2.passwordLess = true;
+```
diff --git a/nixos/doc/manual/configuration/modularity.section.md b/nixos/doc/manual/configuration/modularity.section.md
new file mode 100644
index 00000000000..3395ace20c4
--- /dev/null
+++ b/nixos/doc/manual/configuration/modularity.section.md
@@ -0,0 +1,133 @@
+# Modularity {#sec-modularity}
+
+The NixOS configuration mechanism is modular. If your
+`configuration.nix` 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.
+
+Modules have exactly the same syntax as `configuration.nix`. In fact,
+`configuration.nix` is itself a module. You can use other modules by
+including them from `configuration.nix`, e.g.:
+
+```nix
+{ config, pkgs, ... }:
+
+{ imports = [ ./vpn.nix ./kde.nix ];
+  services.httpd.enable = true;
+  environment.systemPackages = [ pkgs.emacs ];
+  ...
+}
+```
+
+Here, we include two modules from the same directory, `vpn.nix` and
+`kde.nix`. The latter might look like this:
+
+```nix
+{ config, pkgs, ... }:
+
+{ services.xserver.enable = true;
+  services.xserver.displayManager.sddm.enable = true;
+  services.xserver.desktopManager.plasma5.enable = true;
+  environment.systemPackages = [ pkgs.vim ];
+}
+```
+
+Note that both `configuration.nix` and `kde.nix` define the option
+[](#opt-environment.systemPackages). When multiple modules define an
+option, NixOS will try to *merge* the definitions. In the case of
+[](#opt-environment.systemPackages), that's easy: the lists of
+packages can simply be concatenated. The value in `configuration.nix` 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 `mkBefore`:
+
+```nix
+boot.kernelModules = mkBefore [ "kvm-intel" ];
+```
+
+This causes the `kvm-intel` kernel module to be loaded before any other
+kernel modules.
+
+For other types of options, a merge may not be possible. For instance,
+if two modules define [](#opt-services.httpd.adminAddr),
+`nixos-rebuild` will give an error:
+
+```plain
+The unique option `services.httpd.adminAddr' is defined multiple times, in `/etc/nixos/httpd.nix' and `/etc/nixos/configuration.nix'.
+```
+
+When that happens, it's possible to force one definition take precedence
+over the others:
+
+```nix
+services.httpd.adminAddr = pkgs.lib.mkForce "bob@example.org";
+```
+
+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
+[](#opt-environment.systemPackages) only if
+[](#opt-services.xserver.enable) is set to `true` somewhere else:
+
+```nix
+{ config, pkgs, ... }:
+
+{ environment.systemPackages =
+    if config.services.xserver.enable then
+      [ pkgs.firefox
+        pkgs.thunderbird
+      ]
+    else
+      [ ];
+}
+```
+
+With multiple modules, it may not be obvious what the final value of a
+configuration option is. The command `nixos-option` allows you to find
+out:
+
+```ShellSession
+$ nixos-option services.xserver.enable
+true
+
+$ nixos-option boot.kernelModules
+[ "tun" "ipv6" "loop" ... ]
+```
+
+Interactive exploration of the configuration is possible using `nix
+  repl`, a read-eval-print loop for Nix expressions. A typical use:
+
+```ShellSession
+$ nix repl '<nixpkgs/nixos>'
+
+nix-repl> config.networking.hostName
+"mandark"
+
+nix-repl> map (x: x.hostName) config.services.httpd.virtualHosts
+[ "example.org" "example.gov" ]
+```
+
+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.
+
+```nix
+{ config, pkgs, ... }:
+
+let netConfig = hostName: {
+  networking.hostName = hostName;
+  networking.useDHCP = false;
+};
+
+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/network-manager.section.md b/nixos/doc/manual/configuration/network-manager.section.md
new file mode 100644
index 00000000000..4bda21d34a1
--- /dev/null
+++ b/nixos/doc/manual/configuration/network-manager.section.md
@@ -0,0 +1,42 @@
+# NetworkManager {#sec-networkmanager}
+
+To facilitate network configuration, some desktop environments use
+NetworkManager. You can enable NetworkManager by setting:
+
+```nix
+networking.networkmanager.enable = true;
+```
+
+some desktop managers (e.g., GNOME) enable NetworkManager automatically
+for you.
+
+All users that should have permission to change network settings must
+belong to the `networkmanager` group:
+
+```nix
+users.users.alice.extraGroups = [ "networkmanager" ];
+```
+
+NetworkManager is controlled using either `nmcli` or `nmtui`
+(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
+[](#opt-programs.nm-applet.enable), the graphical applet will be
+installed and will launch automatically when the graphical session is
+started.
+
+::: {.note}
+`networking.networkmanager` and `networking.wireless` (WPA Supplicant)
+can be used together if desired. To do this you need to instruct
+NetworkManager to ignore those interfaces like:
+
+```nix
+networking.networkmanager.unmanaged = [
+   "*" "except:type:wwan" "except:type:gsm"
+];
+```
+
+Refer to the option description for the exact syntax and references to
+external documentation.
+:::
diff --git a/nixos/doc/manual/configuration/networking.chapter.md b/nixos/doc/manual/configuration/networking.chapter.md
new file mode 100644
index 00000000000..529dc0610bd
--- /dev/null
+++ b/nixos/doc/manual/configuration/networking.chapter.md
@@ -0,0 +1,16 @@
+# Networking {#sec-networking}
+
+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" />
+```
+<!-- TODO: OpenVPN, NAT -->
diff --git a/nixos/doc/manual/configuration/package-mgmt.chapter.md b/nixos/doc/manual/configuration/package-mgmt.chapter.md
new file mode 100644
index 00000000000..a6c414be59a
--- /dev/null
+++ b/nixos/doc/manual/configuration/package-mgmt.chapter.md
@@ -0,0 +1,18 @@
+# Package Management {#sec-package-management}
+
+This section describes how to add additional packages to your system.
+NixOS has two distinct styles of package management:
+
+-   *Declarative*, where you declare what packages you want in your
+    `configuration.nix`. Every time you run `nixos-rebuild`, NixOS will
+    ensure that you get a consistent set of binaries corresponding to
+    your specification.
+
+-   *Ad hoc*, where you install, upgrade and uninstall packages via the
+    `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" />
+```
diff --git a/nixos/doc/manual/configuration/profiles.chapter.md b/nixos/doc/manual/configuration/profiles.chapter.md
new file mode 100644
index 00000000000..b4ae1b7d3fa
--- /dev/null
+++ b/nixos/doc/manual/configuration/profiles.chapter.md
@@ -0,0 +1,34 @@
+# Profiles {#ch-profiles}
+
+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 `<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:
+
+```nix
+imports = [
+  <nixpkgs/nixos/modules/profiles/profile-name.nix>
+];
+```
+
+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.
+
+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" />
+```
diff --git a/nixos/doc/manual/configuration/profiles/all-hardware.section.md b/nixos/doc/manual/configuration/profiles/all-hardware.section.md
new file mode 100644
index 00000000000..e2dd7c76089
--- /dev/null
+++ b/nixos/doc/manual/configuration/profiles/all-hardware.section.md
@@ -0,0 +1,11 @@
+# All Hardware {#sec-profile-all-hardware}
+
+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.
+
+The enabled kernel modules include support for SATA and PATA, SCSI
+(partially), USB, Firewire (untested), Virtio (QEMU, KVM, etc.), VMware, and
+Hyper-V. Additionally, [](#opt-hardware.enableAllFirmware) is
+enabled, and the firmware for the ZyDAS ZD1211 chipset is specifically
+installed.
diff --git a/nixos/doc/manual/configuration/profiles/base.section.md b/nixos/doc/manual/configuration/profiles/base.section.md
new file mode 100644
index 00000000000..59b3068fda3
--- /dev/null
+++ b/nixos/doc/manual/configuration/profiles/base.section.md
@@ -0,0 +1,7 @@
+# Base {#sec-profile-base}
+
+Defines the software packages included in the "minimal" 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).
diff --git a/nixos/doc/manual/configuration/profiles/clone-config.section.md b/nixos/doc/manual/configuration/profiles/clone-config.section.md
new file mode 100644
index 00000000000..e2583715e51
--- /dev/null
+++ b/nixos/doc/manual/configuration/profiles/clone-config.section.md
@@ -0,0 +1,11 @@
+# Clone Config {#sec-profile-clone-config}
+
+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.
+
+On images where the installation media also becomes an installation target,
+copying over `configuration.nix` should be disabled by
+setting `installer.cloneConfig` to `false`.
+For example, this is done in `sd-image-aarch64-installer.nix`.
diff --git a/nixos/doc/manual/configuration/profiles/demo.section.md b/nixos/doc/manual/configuration/profiles/demo.section.md
new file mode 100644
index 00000000000..0a0df483c12
--- /dev/null
+++ b/nixos/doc/manual/configuration/profiles/demo.section.md
@@ -0,0 +1,4 @@
+# Demo {#sec-profile-demo}
+
+This profile just enables a `demo` user, with password `demo`, uid `1000`, `wheel` group and
+[autologin in the SDDM display manager](#opt-services.xserver.displayManager.autoLogin).
diff --git a/nixos/doc/manual/configuration/profiles/docker-container.section.md b/nixos/doc/manual/configuration/profiles/docker-container.section.md
new file mode 100644
index 00000000000..f3e29b92f5e
--- /dev/null
+++ b/nixos/doc/manual/configuration/profiles/docker-container.section.md
@@ -0,0 +1,7 @@
+# Docker Container {#sec-profile-docker-container}
+
+This is the profile from which the Docker images are generated. It prepares a
+working system by importing the [Minimal](#sec-profile-minimal) and
+[Clone Config](#sec-profile-clone-config) profiles, and
+setting appropriate configuration options that are useful inside a container
+context, like [](#opt-boot.isContainer).
diff --git a/nixos/doc/manual/configuration/profiles/graphical.section.md b/nixos/doc/manual/configuration/profiles/graphical.section.md
new file mode 100644
index 00000000000..aaea5c8c028
--- /dev/null
+++ b/nixos/doc/manual/configuration/profiles/graphical.section.md
@@ -0,0 +1,10 @@
+# Graphical {#sec-profile-graphical}
+
+Defines a NixOS configuration with the Plasma 5 desktop. It's used by the
+graphical installation CD.
+
+It sets [](#opt-services.xserver.enable),
+[](#opt-services.xserver.displayManager.sddm.enable),
+[](#opt-services.xserver.desktopManager.plasma5.enable),
+and [](#opt-services.xserver.libinput.enable) to true. It also
+includes glxinfo and firefox in the system packages list.
diff --git a/nixos/doc/manual/configuration/profiles/hardened.section.md b/nixos/doc/manual/configuration/profiles/hardened.section.md
new file mode 100644
index 00000000000..9fb5e18c384
--- /dev/null
+++ b/nixos/doc/manual/configuration/profiles/hardened.section.md
@@ -0,0 +1,20 @@
+# Hardened {#sec-profile-hardened}
+
+A profile with most (vanilla) hardening options enabled by default,
+potentially at the cost of stability, features and performance.
+
+This includes a hardened kernel, and limiting the system information
+available to processes through the `/sys` and
+`/proc` 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 overriden via
+[](#opt-security.allowUserNamespaces)). See the
+[profile source](https://github.com/nixos/nixpkgs/tree/master/nixos/modules/profiles/hardened.nix)
+for further detail on which settings are altered.
+
+::: {.warning}
+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.
+:::
diff --git a/nixos/doc/manual/configuration/profiles/headless.section.md b/nixos/doc/manual/configuration/profiles/headless.section.md
new file mode 100644
index 00000000000..d185a9a774b
--- /dev/null
+++ b/nixos/doc/manual/configuration/profiles/headless.section.md
@@ -0,0 +1,9 @@
+# Headless {#sec-profile-headless}
+
+Common configuration for headless machines (e.g., Amazon EC2 instances).
+
+Disables [sound](#opt-sound.enable),
+[vesa](#opt-boot.vesa), serial consoles,
+[emergency mode](#opt-systemd.enableEmergencyMode),
+[grub splash images](#opt-boot.loader.grub.splashImage)
+and configures the kernel to reboot automatically on panic.
diff --git a/nixos/doc/manual/configuration/profiles/installation-device.section.md b/nixos/doc/manual/configuration/profiles/installation-device.section.md
new file mode 100644
index 00000000000..ae9f8fa7757
--- /dev/null
+++ b/nixos/doc/manual/configuration/profiles/installation-device.section.md
@@ -0,0 +1,24 @@
+# Installation Device {#sec-profile-installation-device}
+
+Provides a basic configuration for installation devices like CDs.
+This enables redistributable firmware, includes the
+[Clone Config profile](#sec-profile-clone-config)
+and a copy of the Nixpkgs channel, so `nixos-install`
+works out of the box.
+
+Documentation for [Nixpkgs](#opt-documentation.enable)
+and [NixOS](#opt-documentation.nixos.enable) are
+forcefully enabled (to override the
+[Minimal profile](#sec-profile-minimal) preference); the
+NixOS manual is shown automatically on TTY 8, udisks is disabled.
+Autologin is enabled as `nixos` user, while passwordless
+login as both `root` and `nixos` is possible.
+Passwordless `sudo` is enabled too.
+[wpa_supplicant](#opt-networking.wireless.enable) is
+enabled, but configured to not autostart.
+
+It is explained how to login, start the ssh server, and if available,
+how to start the display manager.
+
+Several settings are tweaked so that the installer has a better chance of
+succeeding under low-memory environments.
diff --git a/nixos/doc/manual/configuration/profiles/minimal.section.md b/nixos/doc/manual/configuration/profiles/minimal.section.md
new file mode 100644
index 00000000000..02a3b65ae42
--- /dev/null
+++ b/nixos/doc/manual/configuration/profiles/minimal.section.md
@@ -0,0 +1,9 @@
+# Minimal {#sec-profile-minimal}
+
+This profile defines a small NixOS configuration. It does not contain any
+graphical stuff. It's a very short file that enables
+[noXlibs](#opt-environment.noXlibs), sets
+[](#opt-i18n.supportedLocales) to
+only support the user-selected locale,
+[disables packages' documentation](#opt-documentation.enable),
+and [disables sound](#opt-sound.enable).
diff --git a/nixos/doc/manual/configuration/profiles/qemu-guest.section.md b/nixos/doc/manual/configuration/profiles/qemu-guest.section.md
new file mode 100644
index 00000000000..d7e3cae9cb0
--- /dev/null
+++ b/nixos/doc/manual/configuration/profiles/qemu-guest.section.md
@@ -0,0 +1,7 @@
+# QEMU Guest {#sec-profile-qemu-guest}
+
+This profile contains common configuration for virtual machines running under
+QEMU (using virtio).
+
+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.
diff --git a/nixos/doc/manual/configuration/renaming-interfaces.section.md b/nixos/doc/manual/configuration/renaming-interfaces.section.md
new file mode 100644
index 00000000000..18390c959b2
--- /dev/null
+++ b/nixos/doc/manual/configuration/renaming-interfaces.section.md
@@ -0,0 +1,51 @@
+# Renaming network interfaces {#sec-rename-ifs}
+
+NixOS uses the udev [predictable naming
+scheme](https://systemd.io/PREDICTABLE_INTERFACE_NAMES/) to assign names
+to network interfaces. This means that by default cards are not given
+the traditional names like `eth0` or `eth1`, whose order can change
+unpredictably across reboots. Instead, relying on physical locations and
+firmware information, the scheme produces names like `ens1`, `enp2s0`,
+etc.
+
+These names are predictable but less memorable and not necessarily
+stable: for example installing new hardware or changing firmware
+settings can result in a [name
+change](https://github.com/systemd/systemd/issues/3715#issue-165347602).
+If this is undesirable, for example if you have a single ethernet card,
+you can revert to the traditional scheme by setting
+[](#opt-networking.usePredictableInterfaceNames)
+to `false`.
+
+## Assigning custom names {#sec-custom-ifnames}
+
+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 `wan` to the interface with MAC address
+`52:54:00:12:01:01` using a netword link unit:
+
+```nix
+systemd.network.links."10-wan" = {
+  matchConfig.PermanentMACAddress = "52:54:00:12:01:01";
+  linkConfig.Name = "wan";
+};
+```
+
+Note that links are directly read by udev, *not networkd*, and will work
+even if networkd is disabled.
+
+Alternatively, we can use a plain old udev rule:
+
+```nix
+services.udev.initrdRules = ''
+  SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", \
+  ATTR{address}=="52:54:00:12:01:01", KERNEL=="eth*", NAME="wan"
+'';
+```
+
+::: {.warning}
+The rule must be installed in the initrd using
+`services.udev.initrdRules`, not the usual `services.udev.extraRules`
+option. This is to avoid race conditions with other programs controlling
+the interface.
+:::
diff --git a/nixos/doc/manual/configuration/ssh.section.md b/nixos/doc/manual/configuration/ssh.section.md
new file mode 100644
index 00000000000..cba81eb43f4
--- /dev/null
+++ b/nixos/doc/manual/configuration/ssh.section.md
@@ -0,0 +1,19 @@
+# Secure Shell Access {#sec-ssh}
+
+Secure shell (SSH) access to your machine can be enabled by setting:
+
+```nix
+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"`.
+
+You can declaratively specify authorised RSA/DSA public keys for a user
+as follows:
+
+```nix
+users.users.alice.openssh.authorizedKeys.keys =
+  [ "ssh-dss AAAAB3NzaC1kc3MAAACBAPIkGWVEt4..." ];
+```
diff --git a/nixos/doc/manual/configuration/sshfs-file-systems.section.md b/nixos/doc/manual/configuration/sshfs-file-systems.section.md
new file mode 100644
index 00000000000..4dd1b203a24
--- /dev/null
+++ b/nixos/doc/manual/configuration/sshfs-file-systems.section.md
@@ -0,0 +1,104 @@
+# SSHFS File Systems {#sec-sshfs-file-systems}
+
+[SSHFS][sshfs] is a [FUSE][fuse] 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.
+
+[sshfs]: https://github.com/libfuse/sshfs
+[fuse]: https://en.wikipedia.org/wiki/Filesystem_in_Userspace
+
+## Interactive mounting {#sec-sshfs-interactive}
+
+In NixOS, SSHFS is packaged as <package>sshfs</package>.
+Once installed, mounting a directory interactively is simple as running:
+```ShellSession
+$ sshfs my-user@example.com:/my-dir /mnt/my-dir
+```
+Like any other FUSE file system, the directory is unmounted using:
+```ShellSession
+$ fusermount -u /mnt/my-dir
+```
+
+## Non-interactive mounting {#sec-sshfs-non-interactive}
+
+Mounting non-interactively requires some precautions because `sshfs` 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:
+```ShellSession
+$ 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
+```
+To keep the key safe, change the ownership to `root:root` and make sure the permissions are `600`:
+OpenSSH normally refuses to use the key if it's not well-protected.
+
+The file system can be configured in NixOS via the usual [fileSystems](#opt-fileSystems) option.
+Here's a typical setup:
+```nix
+{
+  system.fsPackages = [ pkgs.sshfs ];
+
+  fileSystems."/mnt/my-dir" = {
+    device = "my-user@example.com:/my-dir/";
+    fsType = "sshfs";
+    options =
+      [ # Filesystem options
+        "allow_other"          # for non-root access
+        "_netdev"              # this is a network fs
+        "x-systemd.automount"  # mount on demand
+
+        # SSH options
+        "reconnect"              # handle connection drops
+        "ServerAliveInterval=15" # keep connections alive
+        "IdentityFile=/var/secrets/example-key"
+      ];
+  };
+}
+```
+More options from `ssh_config(5)` can be given as well, for example you can change the default SSH port or specify a jump proxy:
+```nix
+{
+  options =
+    [ "ProxyJump=bastion@example.com"
+      "Port=22"
+    ];
+}
+```
+It's also possible to change the `ssh` command used by SSHFS to connect to the server.
+For example:
+```nix
+{
+  options =
+    [ (builtins.replaceStrings [" "] ["\\040"]
+        "ssh_command=${pkgs.openssh}/bin/ssh -v -L 8080:localhost:80")
+    ];
+
+}
+```
+
+::: {.note}
+The escaping of spaces is needed because every option is written to the `/etc/fstab` file, which is a space-separated table.
+:::
+
+### Troubleshooting {#sec-sshfs-troubleshooting}
+
+If you're having a hard time figuring out why mounting is failing, you can add the option `"debug"`.
+This enables a verbose log in SSHFS that you can access via:
+```ShellSession
+$ 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 <ssh> <-x> <-a> <-oClearAllForwardings=yes> <-oServerAliveInterval=15> <-oIdentityFile=/var/secrets/wrong-key> <-2> <my-user@example.com> <-s> <sftp>
+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.
+```
+
+::: {.note}
+If the mount point contains special characters it needs to be escaped using `systemd-escape`.
+This is due to the way systemd converts paths into unit names.
+:::
diff --git a/nixos/doc/manual/configuration/subversion.chapter.md b/nixos/doc/manual/configuration/subversion.chapter.md
new file mode 100644
index 00000000000..84f9c270337
--- /dev/null
+++ b/nixos/doc/manual/configuration/subversion.chapter.md
@@ -0,0 +1,102 @@
+# Subversion {#module-services-subversion}
+
+[Subversion](https://subversion.apache.org/) is a centralized
+version-control system. It can use a [variety of
+protocols](http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.serverconfig.choosing)
+for communication between client and server.
+
+## Subversion inside Apache HTTP {#module-services-subversion-apache-httpd}
+
+This section focuses on configuring a web-based server on top of the
+Apache HTTP server, which uses
+[WebDAV](http://www.webdav.org/)/[DeltaV](http://www.webdav.org/deltav/WWW10/deltav-intro.htm)
+for communication.
+
+For more information on the general setup, please refer to the [the
+appropriate section of the Subversion
+book](http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.serverconfig.httpd).
+
+To configure, include in `/etc/nixos/configuration.nix` code to activate
+Apache HTTP, setting [](#opt-services.httpd.adminAddr)
+appropriately:
+
+```nix
+services.httpd.enable = true;
+services.httpd.adminAddr = ...;
+networking.firewall.allowedTCPPorts = [ 80 443 ];
+```
+
+For a simple Subversion server with basic authentication, configure the
+Subversion module for Apache as follows, setting `hostName` and
+`documentRoot` appropriately, and `SVNParentPath` to the parent
+directory of the repositories, `AuthzSVNAccessFile` to the location of
+the `.authz` file describing access permission, and `AuthUserFile` to
+the password file.
+
+```nix
+services.httpd.extraModules = [
+    # note that order is *super* important here
+    { name = "dav_svn"; path = "${pkgs.apacheHttpdPackages.subversion}/modules/mod_dav_svn.so"; }
+    { name = "authz_svn"; path = "${pkgs.apacheHttpdPackages.subversion}/modules/mod_authz_svn.so"; }
+  ];
+  services.httpd.virtualHosts = {
+    "svn" = {
+       hostName = HOSTNAME;
+       documentRoot = DOCUMENTROOT;
+       locations."/svn".extraConfig = ''
+           DAV svn
+           SVNParentPath REPO_PARENT
+           AuthzSVNAccessFile ACCESS_FILE
+           AuthName "SVN Repositories"
+           AuthType Basic
+           AuthUserFile PASSWORD_FILE
+           Require valid-user
+      '';
+    }
+```
+
+The key `"svn"` is just a symbolic name identifying the virtual host.
+The `"/svn"` in `locations."/svn".extraConfig` is the path underneath
+which the repositories will be served.
+
+[This page](https://wiki.archlinux.org/index.php/Subversion) explains
+how to set up the Subversion configuration itself. This boils down to
+the following:
+
+Underneath `REPO_PARENT` repositories can be set up as follows:
+
+```ShellSession
+$ svn create REPO_NAME
+```
+
+Repository files need to be accessible by `wwwrun`:
+
+```ShellSession
+$ chown -R wwwrun:wwwrun REPO_PARENT
+```
+
+The password file `PASSWORD_FILE` can be created as follows:
+
+```ShellSession
+$ htpasswd -cs PASSWORD_FILE USER_NAME
+```
+
+Additional users can be set up similarly, omitting the `c` flag:
+
+```ShellSession
+$ htpasswd -s PASSWORD_FILE USER_NAME
+```
+
+The file describing access permissions `ACCESS_FILE` will look something
+like the following:
+
+```nix
+[/]
+* = r
+
+[REPO_NAME:/]
+USER_NAME = rw
+```
+
+The Subversion repositories will be accessible as
+`http://HOSTNAME/svn/REPO_NAME`.
diff --git a/nixos/doc/manual/configuration/summary.section.md b/nixos/doc/manual/configuration/summary.section.md
new file mode 100644
index 00000000000..8abbbe257fd
--- /dev/null
+++ b/nixos/doc/manual/configuration/summary.section.md
@@ -0,0 +1,46 @@
+# Syntax Summary {#sec-nix-syntax-summary}
+
+Below is a summary of the most important syntactic constructs in the Nix
+expression language. It's not complete. In particular, there are many
+other built-in functions. See the [Nix
+manual](https://nixos.org/nix/manual/#chap-writing-nix-expressions) for
+the rest.
+
+| Example                                       | Description                                                                                                        |
+|-----------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
+| *Basic values*                                |                                                                                                                    |
+| `"Hello world"`                               | A string                                                                                                           |
+| `"${pkgs.bash}/bin/sh"`                       | A string containing an expression (expands to `"/nix/store/hash-bash-version/bin/sh"`)                             |
+| `true`, `false`                               | Booleans                                                                                                           |
+| `123`                                         | An integer                                                                                                         |
+| `./foo.png`                                   | A path (relative to the containing Nix expression)                                                                 |
+| *Compound values*                             |                                                                                                                    |
+| `{ x = 1; y = 2; }`                           | A set with attributes named `x` and `y`                                                                            |
+| `{ foo.bar = 1; }`                            | A nested set, equivalent to `{ foo = { bar = 1; }; }`                                                              |
+| `rec { x = "foo"; y = x + "bar"; }`           | A recursive set, equivalent to `{ x = "foo"; y = "foobar"; }`                                                      |
+| `[ "foo" "bar" ]`                             | A list with two elements                                                                                           |
+| *Operators*                                   |                                                                                                                    |
+| `"foo" + "bar"`                               | String concatenation                                                                                               |
+| `1 + 2`                                       | Integer addition                                                                                                   |
+| `"foo" == "f" + "oo"`                         | Equality test (evaluates to `true`)                                                                                |
+| `"foo" != "bar"`                              | Inequality test (evaluates to `true`)                                                                              |
+| `!true`                                       | Boolean negation                                                                                                   |
+| `{ x = 1; y = 2; }.x`                         | Attribute selection (evaluates to `1`)                                                                             |
+| `{ x = 1; y = 2; }.z or 3`                    | Attribute selection with default (evaluates to `3`)                                                                |
+| `{ x = 1; y = 2; } // { z = 3; }`             | Merge two sets (attributes in the right-hand set taking precedence)                                                |
+| *Control structures*                          |                                                                                                                    |
+| `if 1 + 1 == 2 then "yes!" else "no!"`        | Conditional expression                                                                                             |
+| `assert 1 + 1 == 2; "yes!"`                   | Assertion check (evaluates to `"yes!"`). See [](#sec-assertions) for using assertions in modules                   |
+| `let x = "foo"; y = "bar"; in x + y`          | Variable definition                                                                                                |
+| `with pkgs.lib; head [ 1 2 3 ]`               | Add all attributes from the given set to the scope (evaluates to `1`)                                              |
+| *Functions (lambdas)*                         |                                                                                                                    |
+| `x: x + 1`                                    | A function that expects an integer and returns it increased by 1                                                   |
+| `(x: x + 1) 100`                              | A function call (evaluates to 101)                                                                                 |
+| `let inc = x: x + 1; in inc (inc (inc 100))`  | A function bound to a variable and subsequently called by name (evaluates to 103)                                  |
+| `{ x, y }: x + y`                             | A function that expects a set with required attributes `x` and `y` and concatenates them                           |
+| `{ x, y ? "bar" }: x + y`                     | A function that expects a set with required attribute `x` and optional `y`, using `"bar"` as default value for `y` |
+| `{ x, y, ... }: x + y`                        | A function that expects a set with required attributes `x` and `y` and ignores any other attributes                |
+| `{ x, y } @ args: x + y`                      | A function that expects a set with required attributes `x` and `y`, and binds the whole set to `args`              |
+| *Built-in functions*                          |                                                                                                                    |
+| `import ./foo.nix`                            | Load and return Nix expression in given file                                                                       |
+| `map (x: x + x) [ 1 2 3 ]`                    | Apply a function to every element of a list (evaluates to `[ 2 4 6 ]`)                                             |
diff --git a/nixos/doc/manual/configuration/user-mgmt.chapter.md b/nixos/doc/manual/configuration/user-mgmt.chapter.md
new file mode 100644
index 00000000000..37990664a8f
--- /dev/null
+++ b/nixos/doc/manual/configuration/user-mgmt.chapter.md
@@ -0,0 +1,92 @@
+# User Management {#sec-user-management}
+
+NixOS supports both declarative and imperative styles of user
+management. In the declarative style, users are specified in
+`configuration.nix`. For instance, the following states that a user
+account named `alice` shall exist:
+
+```nix
+users.users.alice = {
+  isNormalUser = true;
+  home = "/home/alice";
+  description = "Alice Foobar";
+  extraGroups = [ "wheel" "networkmanager" ];
+  openssh.authorizedKeys.keys = [ "ssh-dss AAAAB3Nza... alice@foobar" ];
+};
+```
+
+Note that `alice` is a member of the `wheel` and `networkmanager`
+groups, which allows her to use `sudo` to execute commands as `root` 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
+`passwd` program to set a password, which is retained across invocations
+of `nixos-rebuild`.
+
+If you set [](#opt-users.mutableUsers) to
+false, then the contents of `/etc/passwd` and `/etc/group` will be congruent
+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
+[hashedPassword](#opt-users.users._name_.hashedPassword) option. A
+hashed password can be generated using `mkpasswd -m
+  sha-512`.
+
+A user ID (uid) is assigned automatically. You can also specify a uid
+manually by adding
+
+```nix
+uid = 1000;
+```
+
+to the user specification.
+
+Groups can be specified similarly. The following states that a group
+named `students` shall exist:
+
+```nix
+users.groups.students.gid = 1000;
+```
+
+As with users, the group ID (gid) is optional and will be assigned
+automatically if it's missing.
+
+In the imperative style, users and groups are managed by commands such
+as `useradd`, `groupmod` and so on. For instance, to create a user
+account named `alice`:
+
+```ShellSession
+# useradd -m alice
+```
+
+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:
+
+```ShellSession
+# su - alice -c "true"
+```
+
+The flag `-m` 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
+`passwd` utility:
+
+```ShellSession
+# passwd alice
+Enter new UNIX password: ***
+Retype new UNIX password: ***
+```
+
+A user can be deleted using `userdel`:
+
+```ShellSession
+# userdel -r alice
+```
+
+The flag `-r` deletes the user's home directory. Accounts can be
+modified using `usermod`. Unix groups can be managed using `groupadd`,
+`groupmod` and `groupdel`.
diff --git a/nixos/doc/manual/configuration/wayland.chapter.md b/nixos/doc/manual/configuration/wayland.chapter.md
new file mode 100644
index 00000000000..a3a46aa3da6
--- /dev/null
+++ b/nixos/doc/manual/configuration/wayland.chapter.md
@@ -0,0 +1,27 @@
+# Wayland {#sec-wayland}
+
+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
+a Wayland Compositor such as sway without separately enabling a Wayland
+server:
+
+```nix
+programs.sway.enable = true;
+```
+
+This installs the sway compositor along with some essential utilities.
+Now you can start sway from the TTY console.
+
+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:
+
+```nix
+xdg.portal.wlr.enable = true;
+```
+
+and configure Pipewire using
+[](#opt-services.pipewire.enable)
+and related options.
diff --git a/nixos/doc/manual/configuration/wireless.section.md b/nixos/doc/manual/configuration/wireless.section.md
new file mode 100644
index 00000000000..6b223d843ac
--- /dev/null
+++ b/nixos/doc/manual/configuration/wireless.section.md
@@ -0,0 +1,67 @@
+# Wireless Networks {#sec-wireless}
+
+For a desktop installation using NetworkManager (e.g., GNOME), you just
+have to make sure the user is in the `networkmanager` group and you can
+skip the rest of this section on wireless networks.
+
+NixOS will start wpa_supplicant for you if you enable this setting:
+
+```nix
+networking.wireless.enable = true;
+```
+
+NixOS lets you specify networks for wpa_supplicant declaratively:
+
+```nix
+networking.wireless.networks = {
+  echelon = {                # SSID with no spaces or special characters
+    psk = "abcdefgh";
+  };
+  "echelon's AP" = {         # SSID with spaces and/or special characters
+    psk = "ijklmnop";
+  };
+  echelon = {                # Hidden SSID
+    hidden = true;
+    psk = "qrstuvwx";
+  };
+  free.wifi = {};            # Public wireless network
+};
+```
+
+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
+`/etc/wpa_supplicant.conf`. You should edit this file yourself to define
+wireless networks, WPA keys and so on (see wpa_supplicant.conf(5)).
+
+If you are using WPA2 you can generate pskRaw key using
+`wpa_passphrase`:
+
+```ShellSession
+$ wpa_passphrase ESSID PSK
+network={
+        ssid="echelon"
+        #psk="abcdefgh"
+        psk=dca6d6ed41f4ab5a984c9f55f6f66d4efdc720ebf66959810f4329bb391c5435
+}
+```
+
+```nix
+networking.wireless.networks = {
+  echelon = {
+    pskRaw = "dca6d6ed41f4ab5a984c9f55f6f66d4efdc720ebf66959810f4329bb391c5435";
+  };
+}
+```
+
+or you can use it to directly generate the `wpa_supplicant.conf`:
+
+```ShellSession
+# wpa_passphrase ESSID PSK > /etc/wpa_supplicant.conf
+```
+
+After you have edited the `wpa_supplicant.conf`, you need to restart the
+wpa_supplicant service.
+
+```ShellSession
+# systemctl restart wpa_supplicant.service
+```
diff --git a/nixos/doc/manual/configuration/x-windows.chapter.md b/nixos/doc/manual/configuration/x-windows.chapter.md
new file mode 100644
index 00000000000..2c80b786b26
--- /dev/null
+++ b/nixos/doc/manual/configuration/x-windows.chapter.md
@@ -0,0 +1,337 @@
+# X Window System {#sec-x11}
+
+The X Window System (X11) provides the basis of NixOS' graphical user
+interface. It can be enabled as follows:
+
+```nix
+services.xserver.enable = true;
+```
+
+The X server will automatically detect and use the appropriate video
+driver from a set of X.org drivers (such as `vesa` and `intel`). You can
+also specify a driver manually, e.g.
+
+```nix
+services.xserver.videoDrivers = [ "r128" ];
+```
+
+to enable X.org's `xf86-video-r128` driver.
+
+You also need to enable at least one desktop or window manager.
+Otherwise, you can only log into a plain undecorated `xterm` window.
+Thus you should pick one or more of the following lines:
+
+```nix
+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;
+```
+
+NixOS's default *display manager* (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:
+
+```nix
+services.xserver.displayManager.sddm.enable = true;
+services.xserver.displayManager.gdm.enable = true;
+```
+
+You can set the keyboard layout (and optionally the layout variant):
+
+```nix
+services.xserver.layout = "de";
+services.xserver.xkbVariant = "neo";
+```
+
+The X server is started automatically at boot time. If you don't want
+this to happen, you can set:
+
+```nix
+services.xserver.autorun = false;
+```
+
+The X server can then be started manually:
+
+```ShellSession
+# systemctl start display-manager.service
+```
+
+On 64-bit systems, if you want OpenGL for 32-bit programs such as in
+Wine, you should also set the following:
+
+```nix
+hardware.opengl.driSupport32Bit = true;
+```
+
+## Auto-login {#sec-x11-auto-login .unnumbered}
+
+The x11 login screen can be skipped entirely, automatically logging you
+into your window manager and desktop environment when you boot your
+computer.
+
+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.
+
+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:
+
+```nix
+services.xserver.displayManager.defaultSession = "none+i3";
+```
+
+Every display manager in NixOS supports auto-login, here is an example
+using lightdm for a user `alice`:
+
+```nix
+services.xserver.displayManager.lightdm.enable = true;
+services.xserver.displayManager.autoLogin.enable = true;
+services.xserver.displayManager.autoLogin.user = "alice";
+```
+
+## Intel Graphics drivers {#sec-x11--graphics-cards-intel .unnumbered}
+
+There are two choices for Intel Graphics drivers in X.org: `modesetting`
+(included in the xorg-server itself) and `intel` (provided by the
+package xf86-video-intel).
+
+The default and recommended is `modesetting`. It is a generic driver
+which uses the kernel [mode
+setting](https://en.wikipedia.org/wiki/Mode_setting) (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).
+
+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
+officially updated since 2015.
+
+The results vary depending on the hardware, so you may have to try both
+drivers. Use the option
+[](#opt-services.xserver.videoDrivers)
+to set one. The recommended configuration for modern systems is:
+
+```nix
+services.xserver.videoDrivers = [ "modesetting" ];
+services.xserver.useGlamor = true;
+```
+
+If you experience screen tearing no matter what, this configuration was
+reported to resolve the issue:
+
+```nix
+services.xserver.videoDrivers = [ "intel" ];
+services.xserver.deviceSection = ''
+  Option "DRI" "2"
+  Option "TearFree" "true"
+'';
+```
+
+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}
+
+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:
+
+```nix
+services.xserver.videoDrivers = [ "nvidia" ];
+```
+
+Or if you have an older card, you may have to use one of the legacy
+drivers:
+
+```nix
+services.xserver.videoDrivers = [ "nvidiaLegacy390" ];
+services.xserver.videoDrivers = [ "nvidiaLegacy340" ];
+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}
+
+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:
+
+```nix
+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}
+
+Support for Synaptics touchpads (found in many laptops such as the Dell
+Latitude series) can be enabled as follows:
+
+```nix
+services.xserver.libinput.enable = true;
+```
+
+The driver has many options (see [](#ch-options)).
+For instance, the following disables tap-to-click behavior:
+
+```nix
+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 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";
+```
+
+## Custom XKB layouts {#custom-xkb-layouts .unnumbered}
+
+It is possible to install custom [ XKB
+](https://en.wikipedia.org/wiki/X_keyboard_extension) keyboard layouts
+using the option `services.xserver.extraLayouts`.
+
+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.
+
+Create a file called `us-greek` with the following content (under a
+directory called `symbols`; it\'s an XKB peculiarity that will help with
+testing):
+
+```nix
+xkb_symbols "us-greek"
+{
+  include "us(basic)"            // includes the base US keys
+  include "level3(ralt_switch)"  // configures right alt as a third level switch
+
+  key <LatA> { [ a, A, Greek_alpha ] };
+  key <LatB> { [ b, B, Greek_beta  ] };
+  key <LatG> { [ g, G, Greek_gamma ] };
+  key <LatD> { [ d, D, Greek_delta ] };
+  key <LatZ> { [ z, Z, Greek_zeta  ] };
+};
+```
+
+A minimal layout specification must include the following:
+
+```nix
+services.xserver.extraLayouts.us-greek = {
+  description = "US layout with alt-gr greek";
+  languages   = [ "eng" ];
+  symbolsFile = /yourpath/symbols/us-greek;
+};
+```
+
+::: {.note}
+The name (after `extraLayouts.`) should match the one given to the
+`xkb_symbols` block.
+:::
+
+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**:
+
+```ShellSession
+$ nix-shell -p xorg.xkbcomp
+$ setxkbmap -I/yourpath us-greek -print | xkbcomp -I/yourpath - $DISPLAY
+```
+
+You can inspect the predefined XKB files for examples:
+
+```ShellSession
+$ echo "$(nix-build --no-out-link '<nixpkgs>' -A xorg.xkeyboardconfig)/etc/X11/xkb/"
+```
+
+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
+`setxkbmap us-greek` and then type `<alt>+a` (it may not get applied in
+your terminal straight away). To change the default, the usual
+`services.xserver.layout` option can still be used.
+
+A layout can have several other components besides `xkb_symbols`, for
+example we will define new keycodes for some multimedia key and bind
+these to some symbol.
+
+Use the *xev* utility from `pkgs.xorg.xev` to find the codes of the keys
+of interest, then create a `media-key` file to hold the keycodes
+definitions
+
+```nix
+xkb_keycodes "media"
+{
+ <volUp>   = 123;
+ <volDown> = 456;
+}
+```
+
+Now use the newly define keycodes in `media-sym`:
+
+```nix
+xkb_symbols "media"
+{
+ key.type = "ONE_LEVEL";
+ key <volUp>   { [ XF86AudioLowerVolume ] };
+ key <volDown> { [ XF86AudioRaiseVolume ] };
+}
+```
+
+As before, to install the layout do
+
+```nix
+services.xserver.extraLayouts.media = {
+  description  = "Multimedia keys remapping";
+  languages    = [ "eng" ];
+  symbolsFile  = /path/to/media-key;
+  keycodesFile = /path/to/media-sym;
+};
+```
+
+::: {.note}
+The function `pkgs.writeText <filename> <content>` can be useful if you
+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
+workaround, you can set the keymap using `setxkbmap` at the start of the
+session with:
+
+```nix
+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
+example with `xinit` run
+
+```ShellSession
+$ xinit -- -xkbdir /etc/X11/xkb
+```
+
+To learn how to write layouts take a look at the XKB [documentation
+](https://www.x.org/releases/current/doc/xorg-docs/input/XKB-Enhancing.html#Defining_New_Layouts).
+More example layouts can also be found [here
+](https://wiki.archlinux.org/index.php/X_KeyBoard_extension#Basic_examples).
diff --git a/nixos/doc/manual/configuration/xfce.chapter.md b/nixos/doc/manual/configuration/xfce.chapter.md
new file mode 100644
index 00000000000..b0ef6682aae
--- /dev/null
+++ b/nixos/doc/manual/configuration/xfce.chapter.md
@@ -0,0 +1,52 @@
+# Xfce Desktop Environment {#sec-xfce}
+
+To enable the Xfce Desktop Environment, set
+
+```nix
+services.xserver.desktopManager.xfce.enable = true;
+services.xserver.displayManager.defaultSession = "xfce";
+```
+
+Optionally, *picom* can be enabled for nice graphical effects, some
+example settings:
+
+```nix
+services.picom = {
+  enable = true;
+  fade = true;
+  inactiveOpacity = 0.9;
+  shadow = true;
+  fadeDelta = 4;
+};
+```
+
+Some Xfce programs are not installed automatically. To install them
+manually (system wide), put them into your
+[](#opt-environment.systemPackages) from `pkgs.xfce`.
+
+## Thunar Plugins {#sec-xfce-thunar-plugins .unnumbered}
+
+If you\'d like to add extra plugins to Thunar, add them to
+[](#opt-services.xserver.desktopManager.xfce.thunarPlugins).
+You shouldn\'t just add them to [](#opt-environment.systemPackages).
+
+## Troubleshooting {#sec-xfce-troubleshooting .unnumbered}
+
+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 `journalctl --user -b`).
+
+```plain
+Thunar:2410): GVFS-RemoteVolumeMonitor-WARNING **: remote volume monitor with dbus name org.gtk.Private.UDisks2VolumeMonitor is not supported
+```
+
+This is caused by some needed GNOME services not running. This is all
+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.
+
+```ShellSession
+$ xfconf-query -c xfce4-session -p /compat/LaunchGNOME -s true
+```
+
+A log-out and re-log will be needed 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
new file mode 100644
index 00000000000..26813d1042d
--- /dev/null
+++ b/nixos/doc/manual/contributing-to-this-manual.chapter.md
@@ -0,0 +1,13 @@
+# Contributing to this manual {#chap-contributing}
+
+The DocBook and CommonMark sources of NixOS' manual are in the [nixos/doc/manual](https://github.com/NixOS/nixpkgs/tree/master/nixos/doc/manual) subdirectory of the [Nixpkgs](https://github.com/NixOS/nixpkgs) repository.
+
+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
+```
+
+If the build succeeds, the manual will be in `./result/share/doc/nixos/index.html`.
diff --git a/nixos/doc/manual/default.nix b/nixos/doc/manual/default.nix
new file mode 100644
index 00000000000..e96bc47b4a5
--- /dev/null
+++ b/nixos/doc/manual/default.nix
@@ -0,0 +1,264 @@
+{ pkgs
+, options
+, config
+, version
+, revision
+, extraSources ? []
+, baseOptionsJSON ? null
+, warningsAreErrors ? true
+, prefix ? ../../..
+}:
+
+with pkgs;
+
+let
+  lib = pkgs.lib;
+
+  # 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.
+  #
+  # E.g. if some `options` came from modules in ${pkgs.customModules}/nix,
+  # you'd need to include `extraSources = [ pkgs.customModules ]`
+  prefixesToStrip = map (p: "${toString p}/") ([ prefix ] ++ extraSources);
+  stripAnyPrefixes = lib.flip (lib.foldr lib.removePrefix) prefixesToStrip;
+
+  optionsDoc = buildPackages.nixosOptionsDoc {
+    inherit options revision baseOptionsJSON warningsAreErrors;
+    transformOptions = opt: opt // {
+      # Clean up declaration sites to not refer to the NixOS source tree.
+      declarations = map stripAnyPrefixes opt.declarations;
+    };
+  };
+
+  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
+    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">
+        <d:tocentry xmlns:d="http://docbook.org/ns/docbook" linkend="book-nixos-manual"><?dbhtml filename="index.html"?>
+          <d:tocentry linkend="ch-options"><?dbhtml filename="options.html"?></d:tocentry>
+          <d:tocentry linkend="ch-release-notes"><?dbhtml filename="release-notes.html"?></d:tocentry>
+        </d:tocentry>
+      </toc>
+    '';
+
+  manualXsltprocOptions = toString [
+    "--param section.autolabel 1"
+    "--param section.label.includes.component.label 1"
+    "--stringparam html.stylesheet 'style.css overrides.css highlightjs/mono-blue.css'"
+    "--stringparam html.script './highlightjs/highlight.pack.js ./highlightjs/loader.js'"
+    "--param xref.with.number.and.title 1"
+    "--param toc.section.depth 0"
+    "--param generate.consistent.ids 1"
+    "--stringparam admon.style ''"
+    "--stringparam callout.graphics.extension .svg"
+    "--stringparam current.docid manual"
+    "--param chunk.section.depth 0"
+    "--param chunk.first.sections 1"
+    "--param use.id.as.filename 1"
+    "--stringparam chunk.toc ${toc}"
+  ];
+
+  manual-combined = runCommand "nixos-manual-combined"
+    { inherit sources;
+      nativeBuildInputs = [ buildPackages.libxml2.bin buildPackages.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
+      }
+
+      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
+      }
+
+      lintrng manual-combined.xml
+      lintrng man-pages-combined.xml
+
+      mkdir $out
+      cp manual-combined.xml $out/
+      cp man-pages-combined.xml $out/
+    '';
+
+  olinkDB = runCommand "manual-olinkdb"
+    { inherit sources;
+      nativeBuildInputs = [ buildPackages.libxml2.bin buildPackages.libxslt.bin ];
+    }
+    ''
+      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
+
+      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
+    '';
+
+in rec {
+  inherit generatedSources;
+
+  inherit (optionsDoc) optionsJSON optionsNix optionsDocBook;
+
+  # Generate the NixOS manual.
+  manualHTML = runCommand "nixos-manual-html"
+    { inherit sources;
+      nativeBuildInputs = [ buildPackages.libxml2.bin buildPackages.libxslt.bin ];
+      meta.description = "The NixOS manual in HTML format";
+      allowedReferences = ["out"];
+    }
+    ''
+      # 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
+
+      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
+    ''; # */
+
+  # Alias for backward compatibility. TODO(@oxij): remove eventually.
+  manual = manualHTML;
+
+  # Index page of the NixOS manual.
+  manualHTMLIndex = "${manualHTML}/share/doc/nixos/index.html";
+
+  manualEpub = runCommand "nixos-manual-epub"
+    { inherit sources;
+      nativeBuildInputs = [ buildPackages.libxml2.bin buildPackages.libxslt.bin buildPackages.zip ];
+    }
+    ''
+      # Generate the epub manual.
+      dst=$out/share/doc/nixos
+
+      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
+
+      mkdir -p $dst/epub/OEBPS/images/callouts
+      cp -r ${docbook_xsl_ns}/xml/xsl/docbook/images/callouts/*.svg $dst/epub/OEBPS/images/callouts # */
+      echo "application/epub+zip" > mimetype
+      manual="$dst/nixos-manual.epub"
+      zip -0Xq "$manual" mimetype
+      cd $dst/epub && zip -Xr9D "$manual" *
+
+      rm -rf $dst/epub
+
+      mkdir -p $out/nix-support
+      echo "doc-epub manual $manual" >> $out/nix-support/hydra-build-products
+    '';
+
+
+  # Generate the NixOS manpages.
+  manpages = runCommand "nixos-manpages"
+    { inherit sources;
+      nativeBuildInputs = [ buildPackages.libxml2.bin buildPackages.libxslt.bin ];
+      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
+    '';
+
+}
diff --git a/nixos/doc/manual/development/activation-script.section.md b/nixos/doc/manual/development/activation-script.section.md
new file mode 100644
index 00000000000..df683662404
--- /dev/null
+++ b/nixos/doc/manual/development/activation-script.section.md
@@ -0,0 +1,72 @@
+# Activation script {#sec-activation-script}
+
+The activation script is a bash script called to activate the new
+configuration which resides in a NixOS system in `$out/activate`. Since its
+contents depend on your system configuration, the contents may differ.
+This chapter explains how the script works in general and some common NixOS
+snippets. Please be aware that the script is executed on every boot and system
+switch, so tasks that can be performed in other places should be performed
+there (for example letting a directory of a service be created by systemd using
+mechanisms like `StateDirectory`, `CacheDirectory`, ... or if that's not
+possible using `preStart` of the service).
+
+Activation scripts are defined as snippets using
+[](#opt-system.activationScripts). They can either be a simple multiline string
+or an attribute set that can depend on other snippets. The builder for the
+activation script will take these dependencies into account and order the
+snippets accordingly. As a simple example:
+
+```nix
+system.activationScripts.my-activation-script = {
+  deps = [ "etc" ];
+  # supportsDryActivation = true;
+  text = ''
+    echo "Hallo i bims"
+  '';
+};
+```
+
+This example creates an activation script snippet that is run after the `etc`
+snippet. The special variable `supportsDryActivation` can be set so the snippet
+is also run when `nixos-rebuild dry-activate` is run. To differentiate between
+real and dry activation, the `$NIXOS_ACTION` environment variable can be
+read which is set to `dry-activate` when a dry activation is done.
+
+An activation script can write to special files instructing
+`switch-to-configuration` to restart/reload units. The script will take these
+requests into account and will incorperate the unit configuration as described
+above. This means that the activation script will "fake" a modified unit file
+and `switch-to-configuration` will act accordingly. By doing so, configuration
+like [systemd.services.\<name\>.restartIfChanged](#opt-systemd.services) is
+respected. Since the activation script is run **after** services are already
+stopped, [systemd.services.\<name\>.stopIfChanged](#opt-systemd.services)
+cannot be taken into account anymore and the unit is always restarted instead
+of being stopped and started afterwards.
+
+The files that can be written to are `/run/nixos/activation-restart-list` and
+`/run/nixos/activation-reload-list` with their respective counterparts for
+dry activation being `/run/nixos/dry-activation-restart-list` and
+`/run/nixos/dry-activation-reload-list`. Those files can contain
+newline-separated lists of unit names where duplicates are being ignored. These
+files are not create automatically and activation scripts must take the
+possiblility into account that they have to create them first.
+
+## NixOS snippets {#sec-activation-script-nixos-snippets}
+
+There are some snippets NixOS enables by default because disabling them would
+most likely break you system. This section lists a few of them and what they
+do:
+
+- `binsh` creates `/bin/sh` which points to the runtime shell
+- `etc` sets up the contents of `/etc`, this includes systemd units and
+  excludes `/etc/passwd`, `/etc/group`, and `/etc/shadow` (which are managed by
+  the `users` snippet)
+- `hostname` sets the system's hostname in the kernel (not in `/etc`)
+- `modprobe` sets the path to the `modprobe` binary for module auto-loading
+- `nix` prepares the nix store and adds a default initial channel
+- `specialfs` is responsible for mounting filesystems like `/proc` and `sys`
+- `users` creates and removes users and groups by managing `/etc/passwd`,
+  `/etc/group` and `/etc/shadow`. This also creates home directories
+- `usrbinenv` creates `/usr/bin/env`
+- `var` creates some directories in `/var` that are not service-specific
+- `wrappers` creates setuid wrappers like `ping` and `sudo`
diff --git a/nixos/doc/manual/development/assertions.section.md b/nixos/doc/manual/development/assertions.section.md
new file mode 100644
index 00000000000..cc6d81e5699
--- /dev/null
+++ b/nixos/doc/manual/development/assertions.section.md
@@ -0,0 +1,40 @@
+# Warnings and Assertions {#sec-assertions}
+
+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.
+
+Although Nix has the `abort` and `builtins.trace` [functions](https://nixos.org/nix/manual/#ssec-builtins) 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.
+
+## Warnings {#sec-assertions-warnings}
+
+This is an example of using `warnings`.
+
+```nix
+{ 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 [];
+  }
+}
+```
+
+## Assertions {#sec-assertions-assetions}
+
+This example, extracted from the [`syslogd` module](https://github.com/NixOS/nixpkgs/blob/release-17.09/nixos/modules/services/logging/syslogd.nix) shows how to use `assertions`. 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.
+
+```nix
+{ config, lib, ... }:
+{
+  config = lib.mkIf config.services.syslogd.enable {
+    assertions =
+      [ { assertion = !config.services.rsyslogd.enable;
+          message = "rsyslogd conflicts with syslogd";
+        }
+      ];
+  }
+}
+```
diff --git a/nixos/doc/manual/development/building-nixos.chapter.md b/nixos/doc/manual/development/building-nixos.chapter.md
new file mode 100644
index 00000000000..3310dee98f9
--- /dev/null
+++ b/nixos/doc/manual/development/building-nixos.chapter.md
@@ -0,0 +1,46 @@
+# Building a NixOS (Live) ISO {#sec-building-image}
+
+Default live installer configurations are available inside `nixos/modules/installer/cd-dvd`.
+For building other system images, [nixos-generators] is a good place to start looking at.
+
+You have two options:
+
+- Use any of those default configurations as is
+- Combine them with (any of) your host config(s)
+
+System images, such as the live installer ones, know how to enforce configuration settings
+on wich they immediately depend in order to work correctly.
+
+However, if you are confident, you can opt to override those
+enforced values with `mkForce`.
+
+[nixos-generators]: https://github.com/nix-community/nixos-generators
+
+## Practical Instructions {#sec-building-image-instructions}
+
+```ShellSession
+$ git clone https://github.com/NixOS/nixpkgs.git
+$ cd nixpkgs/nixos
+$ nix-build -A config.system.build.isoImage -I nixos-config=modules/installer/cd-dvd/installation-cd-minimal.nix default.nix
+```
+
+To check the content of an ISO image, mount it like so:
+
+```ShellSession
+# mount -o loop -t iso9660 ./result/iso/cd.iso /mnt/iso
+```
+
+## Technical Notes {#sec-building-image-tech-notes}
+
+The config value enforcement is implemented via `mkImageMediaOverride = mkOverride 60;`
+and therefore primes over simple value assignments, but also yields to `mkForce`.
+
+This property allows image designers to implement in semantically correct ways those
+configuration values upon which the correct functioning of the image depends.
+
+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 befor 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.
diff --git a/nixos/doc/manual/development/building-parts.chapter.md b/nixos/doc/manual/development/building-parts.chapter.md
new file mode 100644
index 00000000000..79ddaa37140
--- /dev/null
+++ b/nixos/doc/manual/development/building-parts.chapter.md
@@ -0,0 +1,74 @@
+# Building Specific Parts of NixOS {#sec-building-parts}
+
+With the command `nix-build`, you can build specific parts of your NixOS
+configuration. This is done as follows:
+
+```ShellSession
+$ cd /path/to/nixpkgs/nixos
+$ nix-build -A config.option
+```
+
+where `option` is a NixOS option with type "derivation" (i.e. something
+that can be built). Attributes of interest include:
+
+`system.build.toplevel`
+
+:   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 `nixos-rebuild` builds and what `/run/current-system`
+    points to afterwards.
+
+    A shortcut to build this is:
+
+    ```ShellSession
+    $ nix-build -A system
+    ```
+
+`system.build.manual.manualHTML`
+
+:   The NixOS manual.
+
+`system.build.etc`
+
+:   A tree of symlinks that form the static parts of `/etc`.
+
+`system.build.initialRamdisk` , `system.build.kernel`
+
+:   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 `-kernel` and `-initrd` options:
+
+    ```ShellSession
+    $ 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
+    ```
+
+`system.build.nixos-rebuild` , `system.build.nixos-install` , `system.build.nixos-generate-config`
+
+:   These build the corresponding NixOS commands.
+
+`systemd.units.unit-name.unit`
+
+:   This builds the unit with the specified name. Note that since unit
+    names contain dots (e.g. `httpd.service`), you need to put them
+    between quotes, like this:
+
+    ```ShellSession
+    $ nix-build -A 'config.systemd.units."httpd.service".unit'
+    ```
+
+    You can also test individual units, without rebuilding the whole
+    system, by putting them in `/run/systemd/system`:
+
+    ```ShellSession
+    $ cp $(nix-build -A 'config.systemd.units."httpd.service".unit')/httpd.service \
+        /run/systemd/system/tmp-httpd.service
+    # systemctl daemon-reload
+    # systemctl start tmp-httpd.service
+    ```
+
+    Note that the unit must not have the same name as any unit in
+    `/etc/systemd/system` since those take precedence over
+    `/run/systemd/system`. That's why the unit is installed as
+    `tmp-httpd.service` here.
diff --git a/nixos/doc/manual/development/development.xml b/nixos/doc/manual/development/development.xml
new file mode 100644
index 00000000000..21286cdbd2b
--- /dev/null
+++ b/nixos/doc/manual/development/development.xml
@@ -0,0 +1,20 @@
+<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/what-happens-during-a-system-switch.chapter.xml" />
+ <xi:include href="../from_md/development/writing-documentation.chapter.xml" />
+ <xi:include href="../from_md/development/building-nixos.chapter.xml" />
+ <xi:include href="../from_md/development/nixos-tests.chapter.xml" />
+ <xi:include href="../from_md/development/testing-installer.chapter.xml" />
+</part>
diff --git a/nixos/doc/manual/development/freeform-modules.section.md b/nixos/doc/manual/development/freeform-modules.section.md
new file mode 100644
index 00000000000..10e876b96d5
--- /dev/null
+++ b/nixos/doc/manual/development/freeform-modules.section.md
@@ -0,0 +1,79 @@
+# Freeform modules {#sec-freeform-modules}
+
+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 `attrsOf` options in order to
+accept all attribute names.
+
+This feature can be enabled by using the attribute `freeformType` 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 `config` set. Since this feature nullifies name
+checking for entire option trees, it is only recommended for use in
+submodules.
+
+::: {#ex-freeform-module .example}
+::: {.title}
+**Example: 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
+type-checked and assign a default value. See
+[Example: Declaring a type-checked `settings` attribute](#ex-settings-typed-attrs)
+for a more complete example.
+
+```nix
+{ 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;
+      };
+
+    };
+  };
+}
+```
+
+And the following shows what such a module then allows
+
+```nix
+{
+  # Not a declared option, but the freeform type allows this
+  settings.logLevel = "debug";
+
+  # Not allowed because the the freeform type only allows strings
+  # settings.enable = true;
+
+  # Allowed because there is a port option declared
+  settings.port = 80;
+
+  # Not allowed because the port option doesn't allow strings
+  # settings.port = "443";
+}
+```
+:::
+
+::: {.note}
+Freeform attributes cannot depend on other attributes of the same set
+without infinite recursion:
+
+```nix
+{
+  # This throws infinite recursion encountered
+  settings.logLevel = lib.mkIf (config.settings.port == 80) "debug";
+}
+```
+
+To prevent this, declare options for all attributes that need to depend
+on others. For above example this means to declare `logLevel` to be an
+option.
+:::
diff --git a/nixos/doc/manual/development/importing-modules.section.md b/nixos/doc/manual/development/importing-modules.section.md
new file mode 100644
index 00000000000..65d78959b8e
--- /dev/null
+++ b/nixos/doc/manual/development/importing-modules.section.md
@@ -0,0 +1,46 @@
+# Importing Modules {#sec-importing-modules}
+
+Sometimes NixOS modules need to be used in configuration but exist
+outside of Nixpkgs. These modules can be imported:
+
+```nix
+{ config, lib, pkgs, ... }:
+
+{
+  imports =
+    [ # Use a locally-available module definition in
+      # ./example-module/default.nix
+        ./example-module
+    ];
+
+  services.exampleModule.enable = true;
+}
+```
+
+The environment variable `NIXOS_EXTRA_MODULE_PATH` 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:
+
+```nix
+# ./module-list/default.nix
+[
+  ./example-module1
+  ./example-module2
+]
+```
+
+```nix
+# ./extra-module/default.nix
+{ imports = import ./module-list.nix; }
+```
+
+```nix
+# NIXOS_EXTRA_MODULE_PATH=/absolute/path/to/extra-module
+{ config, lib, pkgs, ... }:
+
+{
+  # No `imports` needed
+
+  services.exampleModule1.enable = true;
+}
+```
diff --git a/nixos/doc/manual/development/linking-nixos-tests-to-packages.section.md b/nixos/doc/manual/development/linking-nixos-tests-to-packages.section.md
new file mode 100644
index 00000000000..38a64027f7c
--- /dev/null
+++ b/nixos/doc/manual/development/linking-nixos-tests-to-packages.section.md
@@ -0,0 +1,6 @@
+# Linking NixOS tests to packages {#sec-linking-nixos-tests-to-packages}
+
+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
+[described in the nixpkgs manual](https://nixos.org/manual/nixpkgs/stable/#ssec-nixos-tests-linking).
diff --git a/nixos/doc/manual/development/meta-attributes.section.md b/nixos/doc/manual/development/meta-attributes.section.md
new file mode 100644
index 00000000000..946c08efd0a
--- /dev/null
+++ b/nixos/doc/manual/development/meta-attributes.section.md
@@ -0,0 +1,66 @@
+# Meta Attributes {#sec-meta-attributes}
+
+Like Nix packages, NixOS modules can declare meta-attributes to provide
+extra information. Module meta attributes are defined in the `meta.nix`
+special module.
+
+`meta` is a top level attribute like `options` and `config`. Available
+meta-attributes are `maintainers`, `doc`, and `buildDocsInSandbox`.
+
+Each of the meta-attributes must be defined at most once per module
+file.
+
+```nix
+{ config, lib, pkgs, ... }:
+{
+  options = {
+    ...
+  };
+
+  config = {
+    ...
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ ericsagnes ];
+    doc = ./default.xml;
+    buildDocsInSandbox = true;
+  };
+}
+```
+
+-   `maintainers` contains a list of the module maintainers.
+
+-   `doc` points to a valid DocBook 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:
+
+    ```ShellSession
+    $ nix-build nixos/release.nix -A manual.x86_64-linux
+    ```
+
+-  `buildDocsInSandbox` 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
+   `NIXOS_EXTRA_MODULE_PATH` are always built outside of the sandbox, as has
+   been the case in previous releases.
+
+   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 `options` and `lib` module arguments and
+   the `pkgs.formats` attribute of the `pkgs` argument, `config` and the rest of
+   `pkgs` 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 `options` 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.
+
+   The default is `true` and should usually not be changed; set it to `false`
+   only if the module requires access to `pkgs` 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).
diff --git a/nixos/doc/manual/development/nixos-tests.chapter.md b/nixos/doc/manual/development/nixos-tests.chapter.md
new file mode 100644
index 00000000000..2a4fdddeaa6
--- /dev/null
+++ b/nixos/doc/manual/development/nixos-tests.chapter.md
@@ -0,0 +1,13 @@
+# NixOS Tests {#sec-nixos-tests}
+
+When you add some feature to NixOS, you should write a test for it.
+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" />
+```
diff --git a/nixos/doc/manual/development/option-declarations.section.md b/nixos/doc/manual/development/option-declarations.section.md
new file mode 100644
index 00000000000..53ecb9b3a62
--- /dev/null
+++ b/nixos/doc/manual/development/option-declarations.section.md
@@ -0,0 +1,221 @@
+# Option Declarations {#sec-option-declarations}
+
+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:
+
+```nix
+options = {
+  name = mkOption {
+    type = type specification;
+    default = default value;
+    example = example value;
+    description = "Description for use in the NixOS manual.";
+  };
+};
+```
+
+The attribute names within the `name` attribute path must be camel
+cased in general but should, as an exception, match the [ package
+attribute name](https://nixos.org/nixpkgs/manual/#sec-package-naming)
+when referencing a Nixpkgs package. For example, the option
+`services.nix-serve.bindAddress` references the `nix-serve` Nixpkgs
+package.
+
+The function `mkOption` accepts the following arguments.
+
+`type`
+
+:   The type of the option (see [](#sec-option-types)). This
+    argument is mandatory for nixpkgs modules. Setting this is highly
+    recommended for the sake of documentation and type checking. In case it is
+    not set, a fallback type with unspecified behavior is used.
+
+`default`
+
+:   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.
+
+`defaultText`
+
+:   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 `lib.literalExpression` for a Nix expression, `lib.literalDocBook` for
+    a plain English description in DocBook format.
+
+`example`
+
+:   An example value that will be shown in the NixOS manual.
+    You can use `lib.literalExpression` and `lib.literalDocBook` in the same way
+    as in `defaultText`.
+
+`description`
+
+:   A textual description of the option, in DocBook format, that will be
+    included in the NixOS manual.
+
+## Utility functions for common option patterns {#sec-option-declarations-util}
+
+### `mkEnableOption` {#sec-option-declarations-util-mkEnableOption}
+
+Creates an Option attribute set for a boolean value option i.e an
+option to be toggled on or off.
+
+This function takes a single string argument, the name of the thing to be toggled.
+
+The option's description is "Whether to enable \<name\>.".
+
+For example:
+
+::: {#ex-options-declarations-util-mkEnableOption-magic .example}
+```nix
+lib.mkEnableOption "magic"
+# is like
+lib.mkOption {
+  type = lib.types.bool;
+  default = false;
+  example = true;
+  description = "Whether to enable magic.";
+}
+```
+
+### `mkPackageOption` {#sec-option-declarations-util-mkPackageOption}
+
+Usage:
+
+```nix
+mkPackageOption pkgs "name" { default = [ "path" "in" "pkgs" ]; example = "literal example"; }
+```
+
+Creates an Option attribute set for an option that specifies the package a module should use for some purpose.
+
+**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 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.
+
+You can omit the default path if the name of the option is also attribute path in nixpkgs.
+
+::: {#ex-options-declarations-util-mkPackageOption .title}
+Examples:
+
+::: {#ex-options-declarations-util-mkPackageOption-hello .example}
+```nix
+lib.mkPackageOption pkgs "hello" { }
+# is like
+lib.mkOption {
+  type = lib.types.package;
+  default = pkgs.hello;
+  defaultText = lib.literalExpression "pkgs.hello";
+  description = "The hello package to use.";
+}
+```
+
+::: {#ex-options-declarations-util-mkPackageOption-ghc .example}
+```nix
+lib.mkPackageOption pkgs "GHC" {
+  default = [ "ghc" ];
+  example = "pkgs.haskell.package.ghc921.ghc.withPackages (hkgs: [ hkgs.primes ])";
+}
+# is like
+lib.mkOption {
+  type = lib.types.package;
+  default = pkgs.ghc;
+  defaultText = lib.literalExpression "pkgs.ghc";
+  example = lib.literalExpression "pkgs.haskell.package.ghc921.ghc.withPackages (hkgs: [ hkgs.primes ])";
+  description = "The GHC package to use.";
+}
+```
+
+## Extensible Option Types {#sec-option-declarations-eot}
+
+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 `enum` and `submodules` and any composed
+forms of them.
+
+Extensible option types can be used for `enum` options that affects
+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 \...).
+
+There are two approaches we could take with this module structure:
+
+-   Configuring the display managers independently by adding an enable
+    option to every display manager module backend. (NixOS)
+
+-   Configuring the display managers in the central module by adding
+    an option to select which display manager backend to use.
+
+Both approaches have problems.
+
+Making backends independent can quickly become hard to manage. For
+display managers, there can only be one enabled at a time, but the
+type system cannot enforce this restriction as there is no relation
+between each backend's `enable` option. As a result, this restriction
+has to be done explicitly by adding assertions in each display manager
+backend module.
+
+On the other hand, managing the display manager backends in the
+central module will require changing the central module option every
+time a new backend is added or removed.
+
+By using extensible option types, it is possible to create a placeholder
+option in the central module
+([Example: Extensible type placeholder in the service module](#ex-option-declaration-eot-service)),
+and to extend it in each backend module
+([Example: Extending `services.xserver.displayManager.enable` in the `gdm` module](#ex-option-declaration-eot-backend-gdm),
+[Example: Extending `services.xserver.displayManager.enable` in the `sddm` module](#ex-option-declaration-eot-backend-sddm)).
+
+As a result, `displayManager.enable` 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.
+
+::: {#ex-option-declaration-eot-service .example}
+::: {.title}
+**Example: Extensible type placeholder in the service module**
+:::
+```nix
+services.xserver.displayManager.enable = mkOption {
+  description = "Display manager to use";
+  type = with types; nullOr (enum [ ]);
+};
+```
+:::
+
+::: {#ex-option-declaration-eot-backend-gdm .example}
+::: {.title}
+**Example: Extending `services.xserver.displayManager.enable` in the `gdm` module**
+:::
+```nix
+services.xserver.displayManager.enable = mkOption {
+  type = with types; nullOr (enum [ "gdm" ]);
+};
+```
+:::
+
+::: {#ex-option-declaration-eot-backend-sddm .example}
+::: {.title}
+**Example: Extending `services.xserver.displayManager.enable` in the `sddm` module**
+:::
+```nix
+services.xserver.displayManager.enable = mkOption {
+  type = with types; nullOr (enum [ "sddm" ]);
+};
+```
+:::
+
+The placeholder declaration is a standard `mkOption` declaration, but it
+is important that extensible option declarations only use the `type`
+argument.
+
+Extensible option types work with any of the composed variants of `enum`
+such as `with types; nullOr (enum [ "foo" "bar" ])` or `with types;
+listOf (enum [ "foo" "bar" ])`.
diff --git a/nixos/doc/manual/development/option-def.section.md b/nixos/doc/manual/development/option-def.section.md
new file mode 100644
index 00000000000..91b24cd4a3a
--- /dev/null
+++ b/nixos/doc/manual/development/option-def.section.md
@@ -0,0 +1,91 @@
+# Option Definitions {#sec-option-definitions}
+
+Option definitions are generally straight-forward bindings of values to
+option names, like
+
+```nix
+config = {
+  services.httpd.enable = true;
+};
+```
+
+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}
+
+If a set of option definitions is conditional on the value of another
+option, you may need to use `mkIf`. Consider, for instance:
+
+```nix
+config = if config.services.httpd.enable then {
+  environment.systemPackages = [ ... ];
+  ...
+} else {};
+```
+
+This definition will cause Nix to fail with an "infinite recursion"
+error. Why? Because the value of `config.services.httpd.enable` depends
+on the value being constructed here. After all, you could also write the
+clearly circular and contradictory:
+
+```nix
+config = if config.services.httpd.enable then {
+  services.httpd.enable = false;
+} else {
+  services.httpd.enable = true;
+};
+```
+
+The solution is to write:
+
+```nix
+config = mkIf config.services.httpd.enable {
+  environment.systemPackages = [ ... ];
+  ...
+};
+```
+
+The special function `mkIf` causes the evaluation of the conditional to
+be "pushed down" into the individual definitions, as if you had written:
+
+```nix
+config = {
+  environment.systemPackages = if config.services.httpd.enable then [ ... ] else [];
+  ...
+};
+```
+
+## Setting Priorities {#sec-option-definitions-setting-priorities .unnumbered}
+
+A module can override the definitions of an option in other modules by
+setting a *priority*. All option definitions that do not have the lowest
+priority value are discarded. By default, option definitions have
+priority 1000. You can specify an explicit priority by using
+`mkOverride`, e.g.
+
+```nix
+services.openssh.enable = mkOverride 10 false;
+```
+
+This definition causes all other definitions with priorities above 10 to
+be discarded. The function `mkForce` is equal to `mkOverride 50`.
+
+## Merging Configurations {#sec-option-definitions-merging .unnumbered}
+
+In conjunction with `mkIf`, 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
+`mkMerge`:
+
+```nix
+config = mkMerge
+  [ # Unconditional stuff.
+    { environment.systemPackages = [ ... ];
+    }
+    # Conditional stuff.
+    (mkIf config.services.bla.enable {
+      environment.systemPackages = [ ... ];
+    })
+  ];
+```
diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md
new file mode 100644
index 00000000000..00f1d85bdb6
--- /dev/null
+++ b/nixos/doc/manual/development/option-types.section.md
@@ -0,0 +1,583 @@
+# Options Types {#sec-option-types}
+
+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.
+
+## Basic Types {#sec-option-types-basic}
+
+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.
+
+`types.bool`
+
+:   A boolean, its values can be `true` or `false`.
+
+`types.path`
+
+:   A filesystem path is anything that starts with a slash when
+    coerced to a string. Even if derivations can be considered as
+    paths, the more specific `types.package` should be preferred.
+
+`types.package`
+
+:   A top-level store path. This can be an attribute set pointing
+    to a store path, like a derivation or a flake input.
+
+`types.anything`
+
+:   A type that accepts any value and recursively merges attribute sets
+    together. This type is recommended when the option type is unknown.
+
+    ::: {#ex-types-anything .example}
+    ::: {.title}
+    **Example: `types.anything` Example**
+    :::
+    Two definitions of this type like
+
+    ```nix
+    {
+      str = lib.mkDefault "foo";
+      pkg.hello = pkgs.hello;
+      fun.fun = x: x + 1;
+    }
+    ```
+
+    ```nix
+    {
+      str = lib.mkIf true "bar";
+      pkg.gcc = pkgs.gcc;
+      fun.fun = lib.mkForce (x: x + 2);
+    }
+    ```
+
+    will get merged to
+
+    ```nix
+    {
+      str = "bar";
+      pkg.gcc = pkgs.gcc;
+      pkg.hello = pkgs.hello;
+      fun.fun = x: x + 2;
+    }
+    ```
+    :::
+
+`types.raw`
+
+:   A type which doesn't do any checking, merging or nested evaluation. It
+    accepts a single arbitrary value that is not recursed into, making it
+    useful for values coming from outside the module system, such as package
+    sets or arbitrary data. Options of this type are still evaluated according
+    to priorities and conditionals, so `mkForce`, `mkIf` and co. still work on
+    the option value itself, but not for any value nested within it. This type
+    should only be used when checking, merging and nested evaluation are not
+    desirable.
+
+`types.optionType`
+
+:   The type of an option's type. Its merging operation ensures that nested
+    options have the correct file location annotated, and that if possible,
+    multiple option definitions are correctly merged together. The main use
+    case is as the type of the `_module.freeformType` option.
+
+`types.attrs`
+
+:   A free-form attribute set.
+
+    ::: {.warning}
+    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`
+    and co. For allowing arbitrary attribute sets, prefer
+    `types.attrsOf types.anything` instead which doesn\'t have these
+    problems.
+    :::
+
+Integer-related types:
+
+`types.int`
+
+:   A signed integer.
+
+`types.ints.{s8, s16, s32}`
+
+:   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. `−128` to
+    `127` for 8 bits).
+
+`types.ints.unsigned`
+
+:   An unsigned integer (that is >= 0).
+
+`types.ints.{u8, u16, u32}`
+
+:   Unsigned integers with a fixed length (8, 16 or 32 bits). They go
+    from 0 to 2^n−1 respectively (e.g. `0`
+    to `255` for 8 bits).
+
+`types.ints.positive`
+
+:   A positive integer (that is > 0).
+
+`types.port`
+
+:   A port number. This type is an alias to
+    `types.ints.u16`.
+
+String-related types:
+
+`types.str`
+
+:   A string. Multiple definitions cannot be merged.
+
+`types.lines`
+
+:   A string. Multiple definitions are concatenated with a new line
+    `"\n"`.
+
+`types.commas`
+
+:   A string. Multiple definitions are concatenated with a comma `","`.
+
+`types.envVar`
+
+:   A string. Multiple definitions are concatenated with a collon `":"`.
+
+`types.strMatching`
+
+:   A string matching a specific regular expression. Multiple
+    definitions cannot be merged. The regular expression is processed
+    using `builtins.match`.
+
+## Value Types {#sec-option-types-value}
+
+Value types are types that take a value parameter.
+
+`types.enum` *`l`*
+
+:   One element of the list *`l`*, e.g. `types.enum [ "left" "right" ]`.
+    Multiple definitions cannot be merged.
+
+`types.separatedString` *`sep`*
+
+:   A string with a custom separator *`sep`*, e.g.
+    `types.separatedString "|"`.
+
+`types.ints.between` *`lowest highest`*
+
+:   An integer between *`lowest`* and *`highest`* (both inclusive). Useful
+    for creating types like `types.port`.
+
+`types.submodule` *`o`*
+
+:   A set of sub options *`o`*. *`o`* 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
+    `types.submoduleWith { modules = toList o; shorthandOnlyDefinesConfig = true; }`.
+    Submodules are detailed in [Submodule](#section-option-types-submodule).
+
+`types.submoduleWith` { *`modules`*, *`specialArgs`* ? {}, *`shorthandOnlyDefinesConfig`* ? false }
+
+:   Like `types.submodule`, but more flexible and with better defaults.
+    It has parameters
+
+    -   *`modules`* 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.
+
+        ::: {.note}
+        Only options defined with this argument are included in rendered
+        documentation.
+        :::
+
+    -   *`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
+        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
+        makes using `_module.args` to define it impossible.
+
+    -   *`shorthandOnlyDefinesConfig`* Whether definitions of this type
+        should default to the `config` section of a module (see
+        [Example: Structure of NixOS Modules](#ex-module-syntax))
+        if it is an attribute set. Enabling this only has a benefit
+        when the submodule defines an option named `config` or `options`.
+        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`
+        keys, all keys are interpreted as option definitions in the
+        `config` section. Enabling this option implicitly puts all
+        attributes in the `config` section.
+
+        With this option enabled, defining a non-`config` section
+        requires using a function:
+        `the-submodule = { ... }: { options = { ... }; }`.
+
+## Composed Types {#sec-option-types-composed}
+
+Composed types are types that take a type as parameter. `listOf
+   int` and `either int str` are examples of composed types.
+
+`types.listOf` *`t`*
+
+:   A list of *`t`* type, e.g. `types.listOf
+          int`. Multiple definitions are merged with list concatenation.
+
+`types.attrsOf` *`t`*
+
+:   An attribute set of where all the values are of *`t`* type. Multiple
+    definitions result in the joined attribute set.
+
+    ::: {.note}
+    This type is *strict* in its values, which in turn means attributes
+    cannot depend on other attributes. See `
+           types.lazyAttrsOf` for a lazy version.
+    :::
+
+`types.lazyAttrsOf` *`t`*
+
+:   An attribute set of where all the values are of *`t`* type. Multiple
+    definitions result in the joined attribute set. This is the lazy
+    version of `types.attrsOf
+          `, allowing attributes to depend on each other.
+
+    ::: {.warning}
+    This version does not fully support conditional definitions! With an
+    option `foo` of this type and a definition
+    `foo.attr = lib.mkIf false 10`, evaluating `foo ? attr` will return
+    `true` even though it should be false. Accessing the value will then
+    throw an error. For types *`t`* that have an `emptyValue` defined,
+    that value will be returned instead of throwing an error. So if the
+    type of `foo.attr` was `lazyAttrsOf (nullOr int)`, `null` would be
+    returned instead for the same `mkIf false` definition.
+    :::
+
+`types.nullOr` *`t`*
+
+:   `null` or type *`t`*. Multiple definitions are merged according to
+    type *`t`*.
+
+`types.uniq` *`t`*
+
+:   Ensures that type *`t`* cannot be merged. It is used to ensure option
+    definitions are declared only once.
+
+`types.unique` `{ message = m }` *`t`*
+
+:   Ensures that type *`t`* cannot be merged. Prints the message *`m`*, after
+    the line `The option <option path> is defined multiple times.` and before
+    a list of definition locations.
+
+`types.either` *`t1 t2`*
+
+:   Type *`t1`* or type *`t2`*, e.g. `with types; either int str`.
+    Multiple definitions cannot be merged.
+
+`types.oneOf` \[ *`t1 t2`* \... \]
+
+:   Type *`t1`* or type *`t2`* and so forth, e.g.
+    `with types; oneOf [ int str bool ]`. Multiple definitions cannot be
+    merged.
+
+`types.coercedTo` *`from f to`*
+
+:   Type *`to`* or type *`from`* which will be coerced to type *`to`* using
+    function *`f`* which takes an argument of type *`from`* and return a
+    value of type *`to`*. Can be used to preserve backwards compatibility
+    of an option if its type was changed.
+
+## Submodule {#section-option-types-submodule}
+
+`submodule` is a very powerful type that defines a set of sub-options
+that are handled like a separate module.
+
+It takes a parameter *`o`*, that should be a set, or a function returning
+a set with an `options` key defining the sub-options. Submodule option
+definitions are type-checked accordingly to the `options` declarations.
+Of course, you can nest submodule option definitons for even higher
+modularity.
+
+The option set can be defined directly
+([Example: Directly defined submodule](#ex-submodule-direct)) or as reference
+([Example: Submodule defined as a reference](#ex-submodule-reference)).
+
+::: {#ex-submodule-direct .example}
+::: {.title}
+**Example: Directly defined submodule**
+:::
+```nix
+options.mod = mkOption {
+  description = "submodule example";
+  type = with types; submodule {
+    options = {
+      foo = mkOption {
+        type = int;
+      };
+      bar = mkOption {
+        type = str;
+      };
+    };
+  };
+};
+```
+:::
+
+::: {#ex-submodule-reference .example}
+::: {.title}
+**Example: Submodule defined as a reference**
+:::
+```nix
+let
+  modOptions = {
+    options = {
+      foo = mkOption {
+        type = int;
+      };
+      bar = mkOption {
+        type = int;
+      };
+    };
+  };
+in
+options.mod = mkOption {
+  description = "submodule example";
+  type = with types; submodule modOptions;
+};
+```
+:::
+
+The `submodule` type is especially interesting when used with composed
+types like `attrsOf` or `listOf`. When composed with `listOf`
+([Example: Declaration of a list of submodules](#ex-submodule-listof-declaration)), `submodule` allows
+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**
+:::
+```nix
+options.mod = mkOption {
+  description = "submodule example";
+  type = with types; listOf (submodule {
+    options = {
+      foo = mkOption {
+        type = int;
+      };
+      bar = mkOption {
+        type = str;
+      };
+    };
+  });
+};
+```
+:::
+
+::: {#ex-submodule-listof-definition .example}
+::: {.title}
+**Example: Definition of a list of submodules**
+:::
+```nix
+config.mod = [
+  { foo = 1; bar = "one"; }
+  { foo = 2; bar = "two"; }
+];
+```
+:::
+
+When composed with `attrsOf`
+([Example: Declaration of attribute sets of submodules](#ex-submodule-attrsof-declaration)), `submodule` allows
+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**
+:::
+```nix
+options.mod = mkOption {
+  description = "submodule example";
+  type = with types; attrsOf (submodule {
+    options = {
+      foo = mkOption {
+        type = int;
+      };
+      bar = mkOption {
+        type = str;
+      };
+    };
+  });
+};
+```
+:::
+
+::: {#ex-submodule-attrsof-definition .example}
+::: {.title}
+**Example: Definition of attribute sets of submodules**
+:::
+```nix
+config.mod.one = { foo = 1; bar = "one"; };
+config.mod.two = { foo = 2; bar = "two"; };
+```
+:::
+
+## Extending types {#sec-option-types-extending}
+
+Types are mainly characterized by their `check` and `merge` functions.
+
+`check`
+
+:   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
+    `addCheck` function ([Example: Adding a type check](#ex-extending-type-check-1)),
+    or to fully override the check function
+    ([Example: Overriding a type check](#ex-extending-type-check-2)).
+
+    ::: {#ex-extending-type-check-1 .example}
+    ::: {.title}
+    **Example: Adding a type check**
+    :::
+    ```nix
+    byte = mkOption {
+      description = "An integer between 0 and 255.";
+      type = types.addCheck types.int (x: x >= 0 && x <= 255);
+    };
+    ```
+    :::
+
+    ::: {#ex-extending-type-check-2 .example}
+    ::: {.title}
+    **Example: Overriding a type check**
+    :::
+    ```nix
+    nixThings = mkOption {
+      description = "words that start with 'nix'";
+      type = types.str // {
+        check = (x: lib.hasPrefix "nix" x)
+      };
+    };
+    ```
+    :::
+
+`merge`
+
+:   Function to merge the options values when multiple values are set.
+    The function takes two parameters, `loc` the option path as a list
+    of strings, and `defs` the list of defined values as a list. It is
+    possible to override a type merge function for custom needs.
+
+## Custom Types {#sec-option-types-custom}
+
+Custom types can be created with the `mkOptionType` function. As type
+creation includes some more complex topics such as submodule handling,
+it is recommended to get familiar with `types.nix` code before creating
+a new type.
+
+The only required parameter is `name`.
+
+`name`
+
+:   A string representation of the type function name.
+
+`definition`
+
+:   Description of the type used in documentation. Give information of
+    the type and any of its arguments.
+
+`check`
+
+:   A function to type check the definition value. Takes the definition
+    value as a parameter and returns a boolean indicating the type check
+    result, `true` for success and `false` for failure.
+
+`merge`
+
+:   A function to merge multiple definitions values. Takes two
+    parameters:
+
+    *`loc`*
+
+    :   The option path as a list of strings, e.g. `["boot" "loader
+                 "grub" "enable"]`.
+
+    *`defs`*
+
+    :   The list of sets of defined `value` and `file` where the value
+        was defined, e.g. `[ {
+                 file = "/foo.nix"; value = 1; } { file = "/bar.nix"; value = 2 }
+                 ]`. The `merge` function should return the merged value
+        or throw an error in case the values are impossible or not meant
+        to be merged.
+
+`getSubOptions`
+
+:   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.
+    `prefix:
+          elemType.getSubOptions (prefix ++
+          ["prefix"])` where *`"prefix"`* is the newly added prefix.
+
+`getSubModules`
+
+:   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 `elemType`, the function should just recursively
+    look into submodules by returning `elemType.getSubModules;`.
+
+`substSubModules`
+
+:   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 `substSubModules`, e.g for a
+    type `composedType` that take an `elemtype` type parameter, this
+    function should be defined as `m:
+          composedType (elemType.substSubModules m)`.
+
+`typeMerge`
+
+:   A function to merge multiple type declarations. Takes the type to
+    merge `functor` as parameter. A `null` return value means that type
+    cannot be merged.
+
+    *`f`*
+
+    :   The type to merge `functor`.
+
+    Note: There is a generic `defaultTypeMerge` that work with most of
+    value and composed types.
+
+`functor`
+
+:   An attribute set representing the type. It is used for type
+    operations and has the following keys:
+
+    `type`
+
+    :   The type function.
+
+    `wrapped`
+
+    :   Holds the type parameter for composed types.
+
+    `payload`
+
+    :   Holds the value parameter for value types. The types that have a
+        `payload` are the `enum`, `separatedString` and `submodule`
+        types.
+
+    `binOp`
+
+    :   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.
diff --git a/nixos/doc/manual/development/replace-modules.section.md b/nixos/doc/manual/development/replace-modules.section.md
new file mode 100644
index 00000000000..0700a82004c
--- /dev/null
+++ b/nixos/doc/manual/development/replace-modules.section.md
@@ -0,0 +1,64 @@
+# Replace Modules {#sec-replace-modules}
+
+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.
+
+`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).
+
+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.
+
+```nix
+{ config, lib, pkgs, ... }:
+
+{
+  disabledModules = [ "services/databases/postgresql.nix" ];
+
+  imports =
+    [ # Use postgresql service from nixos-unstable channel.
+      # sudo nix-channel --add https://nixos.org/channels/nixos-unstable nixos-unstable
+      <nixos-unstable/nixos/modules/services/databases/postgresql.nix>
+    ];
+
+  services.postgresql.enable = true;
+}
+```
+
+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.
+
+```nix
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.man;
+in
+
+{
+  disabledModules = [ "services/programs/man.nix" ];
+
+  options = {
+    programs.man.enable = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Whether to enable manual pages.";
+    };
+  };
+
+  config = mkIf cfg.enabled {
+    warnings = [ "disabled manpages for production deployments." ];
+  };
+}
+```
diff --git a/nixos/doc/manual/development/running-nixos-tests-interactively.section.md b/nixos/doc/manual/development/running-nixos-tests-interactively.section.md
new file mode 100644
index 00000000000..a1431859ff5
--- /dev/null
+++ b/nixos/doc/manual/development/running-nixos-tests-interactively.section.md
@@ -0,0 +1,35 @@
+# Running Tests interactively {#sec-running-nixos-tests-interactively}
+
+The test itself can be run interactively. This is particularly useful
+when developing or debugging a test:
+
+```ShellSession
+$ nix-build . -A nixosTests.login.driverInteractive
+$ ./result/bin/nixos-test-driver
+[...]
+>>>
+```
+
+You can then take any Python statement, e.g.
+
+```py
+>>> start_all()
+>>> test_script()
+>>> machine.succeed("touch /tmp/foo")
+>>> print(machine.succeed("pwd")) # Show stdout of command
+```
+
+The function `test_script` 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).
+
+You can re-use the VM states coming from a previous run by setting the
+`--keep-vm-state` flag.
+
+```ShellSession
+$ ./result/bin/nixos-test-driver --keep-vm-state
+```
+
+The machine state is stored in the `$TMPDIR/vm-state-machinename`
+directory.
diff --git a/nixos/doc/manual/development/running-nixos-tests.section.md b/nixos/doc/manual/development/running-nixos-tests.section.md
new file mode 100644
index 00000000000..1bec023b613
--- /dev/null
+++ b/nixos/doc/manual/development/running-nixos-tests.section.md
@@ -0,0 +1,31 @@
+# Running Tests {#sec-running-nixos-tests}
+
+You can run tests using `nix-build`. For example, to run the test
+[`login.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix),
+you just do:
+
+```ShellSession
+$ nix-build '<nixpkgs/nixos/tests/login.nix>'
+```
+
+or, if you don't want to rely on `NIX_PATH`:
+
+```ShellSession
+$ cd /my/nixpkgs/nixos/tests
+$ nix-build login.nix
+…
+running the VM test script
+machine: QEMU running (pid 8841)
+…
+6 out of 6 tests succeeded
+```
+
+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:
+
+```ShellSession
+$ nix-store --read-log result
+```
diff --git a/nixos/doc/manual/development/settings-options.section.md b/nixos/doc/manual/development/settings-options.section.md
new file mode 100644
index 00000000000..f9bb6ff9cc4
--- /dev/null
+++ b/nixos/doc/manual/development/settings-options.section.md
@@ -0,0 +1,237 @@
+# Options for Program Settings {#sec-settings-options}
+
+Many programs have configuration files where program-specific settings
+can be declared. File formats can be separated into two categories:
+
+-   Nix-representable ones: These can trivially be mapped to a subset of
+    Nix syntax. E.g. JSON is an example, since its values like
+    `{"foo":{"bar":10}}` can be mapped directly to Nix:
+    `{ 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
+    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.
+
+    Currently there are no fixed conventions for these, but it is common
+    to have a `configFile` option for setting the configuration file
+    path directly. The default value of `configFile` can be an
+    auto-generated file, with convenient options for controlling the
+    contents. For example an option of type `attrsOf str` can be used
+    for representing environment variables which generates a section
+    like `export FOO="foo"`. Often it can also be useful to also include
+    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}
+
+By convention, formats like this are handled with a generic `settings`
+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
+`pkgs.formats`:
+
+`pkgs.formats.json` { }
+
+:   A function taking an empty attribute set (for future extensibility)
+    and returning a set with JSON-specific attributes `type` and
+    `generate` as specified [below](#pkgs-formats-result).
+
+`pkgs.formats.yaml` { }
+
+:   A function taking an empty attribute set (for future extensibility)
+    and returning a set with YAML-specific attributes `type` and
+    `generate` as specified [below](#pkgs-formats-result).
+
+`pkgs.formats.ini` { *`listsAsDuplicateKeys`* ? false, *`listToValue`* ? null, \... }
+
+:   A function taking an attribute set with values
+
+    `listsAsDuplicateKeys`
+
+    :   A boolean for controlling whether list values can be used to
+        represent duplicate INI keys
+
+    `listToValue`
+
+    :   A function for turning a list of values into a single value.
+
+    It returns a set with INI-specific attributes `type` and `generate`
+    as specified [below](#pkgs-formats-result).
+
+`pkgs.formats.toml` { }
+
+:   A function taking an empty attribute set (for future extensibility)
+    and returning a set with TOML-specific attributes `type` and
+    `generate` as specified [below](#pkgs-formats-result).
+
+`pkgs.formats.elixirConf { elixir ? pkgs.elixir }`
+
+:   A function taking an attribute set with values
+
+    `elixir`
+
+    :   The Elixir package which will be used to format the generated output
+
+    It returns a set with Elixir-Config-specific attributes `type`, `lib`, and
+    `generate` as specified [below](#pkgs-formats-result).
+
+    The `lib` attribute contains functions to be used in settings, for
+    generating special Elixir values:
+
+    `mkRaw elixirCode`
+
+    :   Outputs the given string as raw Elixir code
+
+    `mkGetEnv { envVariable, fallback ? null }`
+
+    :   Makes the configuration fetch an environment variable at runtime
+
+    `mkAtom atom`
+
+    :   Outputs the given string as an Elixir atom, instead of the default
+        Elixir binary string. Note: lowercase atoms still needs to be prefixed
+        with `:`
+
+    `mkTuple array`
+
+    :   Outputs the given array as an Elixir tuple, instead of the default
+        Elixir list
+
+    `mkMap attrset`
+
+    :   Outputs the given attribute set as an Elixir map, instead of the
+        default Elixir keyword list
+
+
+::: {#pkgs-formats-result}
+These functions all return an attribute set with these values:
+:::
+
+`type`
+
+:   A module system type representing a value of the format
+
+`lib`
+
+:   Utility functions for convenience, or special interactions with the format.
+    This attribute is optional. It may contain inside a `types` attribute
+    containing types specific to this format.
+
+`generate` *`filename jsonValue`*
+
+:   A function that can render a value of the format to a file. Returns
+    a file path.
+
+    ::: {.note}
+    This function puts the value contents in the Nix store. So this
+    should be avoided for secrets.
+    :::
+
+::: {#ex-settings-nix-representable .example}
+::: {.title}
+**Example: 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
+explanations.
+
+```nix
+{ options, config, lib, pkgs, ... }:
+let
+  cfg = config.services.foo;
+  # Define the settings format used for this program
+  settingsFormat = pkgs.formats.json {};
+in {
+
+  options.services.foo = {
+    enable = lib.mkEnableOption "foo service";
+
+    settings = lib.mkOption {
+      # Setting this type allows for correct merging behavior
+      type = settingsFormat.type;
+      default = {};
+      description = ''
+        Configuration for foo, see
+        <link xlink:href="https://example.com/docs/foo"/>
+        for supported settings.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    # We can assign some default settings here to make the service work by just
+    # enabling it. We use `mkDefault` for values that can be changed without
+    # problems
+    services.foo.settings = {
+      # Fails at runtime without any value set
+      log_level = lib.mkDefault "WARN";
+
+      # We assume systemd's `StateDirectory` is used, so we require this value,
+      # therefore no mkDefault
+      data_path = "/var/lib/foo";
+
+      # Since we use this to create a user we need to know the default value at
+      # eval time
+      user = lib.mkDefault "foo";
+    };
+
+    environment.etc."foo.json".source =
+      # The formats generator function takes a filename and the Nix value
+      # representing the format value and produces a filepath with that value
+      # rendered in the format
+      settingsFormat.generate "foo-config.json" cfg.settings;
+
+    # We know that the `user` attribute exists because we set a default value
+    # for it above, allowing us to use it without worries here
+    users.users.${cfg.settings.user} = { isSystemUser = true; };
+
+    # ...
+  };
+}
+```
+:::
+
+### Option declarations for attributes {#sec-settings-attrs-options}
+
+Some `settings` 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 [](#sec-freeform-modules).
+
+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.
+
+::: {#ex-settings-typed-attrs .example}
+::: {.title}
+**Example: Declaring a type-checked `settings` attribute**
+:::
+```nix
+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
+    <link xlink:href="https://example.com/docs/foo"/>
+    for supported values.
+  '';
+};
+```
+:::
diff --git a/nixos/doc/manual/development/sources.chapter.md b/nixos/doc/manual/development/sources.chapter.md
new file mode 100644
index 00000000000..88173f7135b
--- /dev/null
+++ b/nixos/doc/manual/development/sources.chapter.md
@@ -0,0 +1,77 @@
+# Getting the Sources {#sec-getting-sources}
+
+By default, NixOS's `nixos-rebuild` command uses the NixOS and Nixpkgs
+sources provided by the `nixos` channel (kept in
+`/nix/var/nix/profiles/per-user/root/channels/nixos`). To modify NixOS,
+however, you should check out the latest sources from Git. This is as
+follows:
+
+```ShellSession
+$ git clone https://github.com/NixOS/nixpkgs
+$ cd nixpkgs
+$ git remote update origin
+```
+
+This will check out the latest Nixpkgs sources to `./nixpkgs` the NixOS
+sources to `./nixpkgs/nixos`. (The NixOS source tree lives in a
+subdirectory of the Nixpkgs repository.) The `nixpkgs` repository has
+branches that correspond to each Nixpkgs/NixOS channel (see
+[](#sec-upgrading) for more information about channels). Thus, the
+Git branch `origin/nixos-17.03` will contain the latest built and tested
+version available in the `nixos-17.03` channel.
+
+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:
+
+```ShellSession
+$ nixos-version
+17.09pre104379.6e0b727 (Hummingbird)
+
+$ git checkout -b local 6e0b727
+```
+
+Or, to base your local branch on the latest version available in a NixOS
+channel:
+
+```ShellSession
+$ git remote update origin
+$ git checkout -b local origin/nixos-17.03
+```
+
+(Replace `nixos-17.03` with the name of the channel you want to use.)
+You can use `git merge` or `git
+  rebase` to keep your local branch in sync with the channel, e.g.
+
+```ShellSession
+$ git remote update origin
+$ git merge origin/nixos-17.03
+```
+
+You can use `git cherry-pick` to copy commits from your local branch to
+the upstream branch.
+
+If you want to rebuild your system using your (modified) sources, you
+need to tell `nixos-rebuild` about them using the `-I` flag:
+
+```ShellSession
+# nixos-rebuild switch -I nixpkgs=/my/sources/nixpkgs
+```
+
+If you want `nix-env` to use the expressions in `/my/sources`, use
+`nix-env -f
+  /my/sources/nixpkgs`, or change the default by adding a symlink in
+`~/.nix-defexpr`:
+
+```ShellSession
+$ ln -s /my/sources/nixpkgs ~/.nix-defexpr/nixpkgs
+```
+
+You may want to delete the symlink `~/.nix-defexpr/channels_root` 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 `~/.nix-defexpr` directory
+completely, log out and log in again and it should have been recreated
+with a link to the root channels.
diff --git a/nixos/doc/manual/development/testing-installer.chapter.md b/nixos/doc/manual/development/testing-installer.chapter.md
new file mode 100644
index 00000000000..2eaa0161492
--- /dev/null
+++ b/nixos/doc/manual/development/testing-installer.chapter.md
@@ -0,0 +1,18 @@
+# Testing the Installer {#ch-testing-installer}
+
+Building, burning, and booting from an installation CD is rather
+tedious, so here is a quick way to see if the installer works properly:
+
+```ShellSession
+# mount -t tmpfs none /mnt
+# nixos-generate-config --root /mnt
+$ nix-build '<nixpkgs/nixos>' -A config.system.build.nixos-install
+# ./result/bin/nixos-install
+```
+
+To start a login shell in the new NixOS installation in `/mnt`:
+
+```ShellSession
+$ nix-build '<nixpkgs/nixos>' -A config.system.build.nixos-enter
+# ./result/bin/nixos-enter
+```
diff --git a/nixos/doc/manual/development/unit-handling.section.md b/nixos/doc/manual/development/unit-handling.section.md
new file mode 100644
index 00000000000..a7ccb3dbd04
--- /dev/null
+++ b/nixos/doc/manual/development/unit-handling.section.md
@@ -0,0 +1,62 @@
+# Unit handling {#sec-unit-handling}
+
+To figure out what units need to be started/stopped/restarted/reloaded, the
+script first checks the current state of the system, similar to what `systemctl
+list-units` shows. For each of the units, the script goes through the following
+checks:
+
+- Is the unit file still in the new system? If not, **stop** the service unless
+  it sets `X-StopOnRemoval` in the `[Unit]` section to `false`.
+
+- Is it a `.target` unit? If so, **start** it unless it sets
+  `RefuseManualStart` in the `[Unit]` section to `true` or `X-OnlyManualStart`
+  in the `[Unit]` section to `true`. Also **stop** the unit again unless it
+  sets `X-StopOnReconfiguration` to `false`.
+
+- Are the contents of the unit files different? They are compared by parsing
+  them and comparing their contents. If they are different but only
+  `X-Reload-Triggers` in the `[Unit]` section is changed, **reload** the unit.
+  The NixOS module system allows setting these triggers with the option
+  [systemd.services.\<name\>.reloadTriggers](#opt-systemd.services). There are
+  some additional keys in the `[Unit]` section that are ignored as well. If the
+  unit files differ in any way, the following actions are performed:
+
+  - `.path` and `.slice` units are ignored. There is no need to restart them
+    since changes in their values are applied by systemd when systemd is
+    reloaded.
+
+  - `.mount` units are **reload**ed. These mostly come from the `/etc/fstab`
+    parser.
+
+  - `.socket` units are currently ignored. This is to be fixed at a later
+    point.
+
+  - The rest of the units (mostly `.service` units) are then **reload**ed if
+    `X-ReloadIfChanged` in the `[Service]` section is set to `true` (exposed
+    via [systemd.services.\<name\>.reloadIfChanged](#opt-systemd.services)).
+    A little exception is done for units that were deactivated in the meantime,
+    for example because they require a unit that got stopped before. These
+    are **start**ed instead of reloaded.
+
+  - If the reload flag is not set, some more flags decide if the unit is
+    skipped. These flags are `X-RestartIfChanged` in the `[Service]` section
+    (exposed via
+    [systemd.services.\<name\>.restartIfChanged](#opt-systemd.services)),
+    `RefuseManualStop` in the `[Unit]` section, and `X-OnlyManualStart` in the
+    `[Unit]` section.
+
+  - Further behavior depends on the unit having `X-StopIfChanged` in the
+    `[Service]` section set to `true` (exposed via
+    [systemd.services.\<name\>.stopIfChanged](#opt-systemd.services)). This is
+    set to `true` by default and must be explicitly turned off if not wanted.
+    If the flag is enabled, the unit is **stop**ped and then **start**ed. If
+    not, the unit is **restart**ed. The goal of the flag is to make sure that
+    the new unit never runs in the old environment which is still in place
+    before the activation script is run. This behavior is different when the
+    service is socket-activated, as outlined in the following steps.
+
+  - The last thing that is taken into account is whether the unit is a service
+    and socket-activated. If `X-StopIfChanged` is **not** set, the service
+    is **restart**ed with the others. If it is set, both the service and the
+    socket are **stop**ped and the socket is **start**ed, leaving socket
+    activation to start the service when it's needed.
diff --git a/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md b/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md
new file mode 100644
index 00000000000..aad82831a3c
--- /dev/null
+++ b/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md
@@ -0,0 +1,53 @@
+# What happens during a system switch? {#sec-switching-systems}
+
+Running `nixos-rebuild switch` is one of the more common tasks under NixOS.
+This chapter explains some of the internals of this command to make it simpler
+for new module developers to configure their units correctly and to make it
+easier to understand what is happening and why for curious administrators.
+
+`nixos-rebuild`, like many deployment solutions, calls `switch-to-configuration`
+which resides in a NixOS system at `$out/bin/switch-to-configuration`. The
+script is called with the action that is to be performed like `switch`, `test`,
+`boot`. There is also the `dry-activate` action which does not really perform
+the actions but rather prints what it would do if you called it with `test`.
+This feature can be used to check what service states would be changed if the
+configuration was switched to.
+
+If the action is `switch` or `boot`, the bootloader is updated first so the
+configuration will be the next one to boot. Unless `NIXOS_NO_SYNC` is set to
+`1`, `/nix/store` is synced to disk.
+
+If the action is `switch` or `test`, the currently running system is inspected
+and the actions to switch to the new system are calculated. This process takes
+two data sources into account: `/etc/fstab` and the current systemd status.
+Mounts and swaps are read from `/etc/fstab` and the corresponding actions are
+generated. If a new mount is added, for example, the proper `.mount` unit is
+marked to be started. The current systemd state is inspected, the difference
+between the current system and the desired configuration is calculated and
+actions are generated to get to this state. There are a lot of nuances that can
+be controlled by the units which are explained here.
+
+After calculating what should be done, the actions are carried out. The order
+of actions is always the same:
+- Stop units (`systemctl stop`)
+- Run activation script (`$out/activate`)
+- See if the activation script requested more units to restart
+- Restart systemd if needed (`systemd daemon-reexec`)
+- Forget about the failed state of units (`systemctl reset-failed`)
+- Reload systemd (`systemctl daemon-reload`)
+- Reload systemd user instances (`systemctl --user daemon-reload`)
+- Set up tmpfiles (`systemd-tmpfiles --create`)
+- Reload units (`systemctl reload`)
+- Restart units (`systemctl restart`)
+- Start units (`systemctl start`)
+- Inspect what changed during these actions and print units that failed and
+  that were newly started
+
+Most of these actions are either self-explaining but some of them have to do
+with our units or the activation script. For this reason, these topics are
+explained in the next sections.
+
+```{=docbook}
+<xi:include href="unit-handling.section.xml" />
+<xi:include href="activation-script.section.xml" />
+```
diff --git a/nixos/doc/manual/development/writing-documentation.chapter.md b/nixos/doc/manual/development/writing-documentation.chapter.md
new file mode 100644
index 00000000000..7c29f600d70
--- /dev/null
+++ b/nixos/doc/manual/development/writing-documentation.chapter.md
@@ -0,0 +1,93 @@
+# Writing NixOS Documentation {#sec-writing-documentation}
+
+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.
+
+## Building the Manual {#sec-writing-docs-building-the-manual}
+
+The DocBook sources of the [](#book-nixos-manual) are in the
+[`nixos/doc/manual`](https://github.com/NixOS/nixpkgs/tree/master/nixos/doc/manual)
+subdirectory of the Nixpkgs repository.
+
+You can quickly validate your edits with `make`:
+
+```ShellSession
+$ cd /path/to/nixpkgs/nixos/doc/manual
+$ nix-shell
+nix-shell$ make
+```
+
+Once you are done making modifications to the manual, it\'s important to
+build it before committing. You can do that as follows:
+
+```ShellSession
+nix-build nixos/release.nix -A manual.x86_64-linux
+```
+
+When this command successfully finishes, it will tell you where the
+manual got generated. The HTML will be accessible through the `result`
+symlink at `./result/share/doc/nixos/index.html`.
+
+## Editing DocBook XML {#sec-writing-docs-editing-docbook-xml}
+
+For general information on how to write in DocBook, see [DocBook 5: The
+Definitive Guide](http://www.docbook.org/tdg5/en/html/docbook.html).
+
+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 [](#sec-emacs-docbook-xml).
+
+[Pandoc](http://pandoc.org) 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:
+
+```ShellSession
+pandoc -f markdown_github -t docbook5 docs.md -o my-section.md
+```
+
+Pandoc can also quickly convert a single `section.xml` to HTML, which is
+helpful when drafting.
+
+Sometimes writing valid DocBook is simply too difficult. In this case,
+submit your documentation updates in a [GitHub
+Issue](https://github.com/NixOS/nixpkgs/issues/new) and someone will
+handle the conversion to XML for you.
+
+## Creating a Topic {#sec-writing-docs-creating-a-topic}
+
+You can use an existing topic as a basis for the new topic or create a
+topic from scratch.
+
+Keep the following guidelines in mind when you create and add a topic:
+
+-   The NixOS [`book`](http://www.docbook.org/tdg5/en/html/book.html)
+    element is in `nixos/doc/manual/manual.xml`. It includes several
+    [`parts`](http://www.docbook.org/tdg5/en/html/book.html) which are in
+    subdirectories.
+
+-   Store the topic file in the same directory as the `part` to which it
+    belongs. If your topic is about configuring a NixOS module, then the
+    XML file can be stored alongside the module definition `nix` file.
+
+-   If you include multiple words in the file name, separate the words
+    with a dash. For example: `ipv6-config.xml`.
+
+-   Make sure that the `xml:id` value is unique. You can use abbreviations
+    if the ID is too long. For example: `nixos-config`.
+
+-   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.
+
+## 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
+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.
+
+If the topic is about configuring a NixOS module, it can be
+automatically included in the manual by using the `meta.doc` attribute.
+See [](#sec-meta-attributes) for an explanation.
diff --git a/nixos/doc/manual/development/writing-modules.chapter.md b/nixos/doc/manual/development/writing-modules.chapter.md
new file mode 100644
index 00000000000..0c41cbd3cb7
--- /dev/null
+++ b/nixos/doc/manual/development/writing-modules.chapter.md
@@ -0,0 +1,208 @@
+# Writing NixOS Modules {#sec-writing-modules}
+
+NixOS has a modular system for declarative configuration. This system
+combines multiple *modules* to produce the full system configuration.
+One of the modules that constitute the configuration is
+`/etc/nixos/configuration.nix`. Most of the others live in the
+[`nixos/modules`](https://github.com/NixOS/nixpkgs/tree/master/nixos/modules)
+subdirectory of the Nixpkgs tree.
+
+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 *declare* options that
+can be used by other modules, and conversely can *define* options
+provided by other modules in its own implementation. For example, the
+module
+[`pam.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/security/pam.nix)
+declares the option `security.pam.services` that allows other modules (e.g.
+[`sshd.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/networking/ssh/sshd.nix))
+to define PAM services; and it defines the option `environment.etc` (declared by
+[`etc.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/system/etc/etc.nix))
+to cause files to be created in `/etc/pam.d`.
+
+In [](#sec-configuration-syntax), we saw the following structure of
+NixOS modules:
+
+```nix
+{ config, pkgs, ... }:
+
+{ option definitions
+}
+```
+
+This is actually an *abbreviated* form of module that only defines
+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**
+:::
+```nix
+{ config, pkgs, ... }:
+
+{
+  imports =
+    [ paths of other modules
+    ];
+
+  options = {
+    option declarations
+  };
+
+  config = {
+    option definitions
+  };
+}
+```
+:::
+
+The meaning of each part is as follows.
+
+-   The first line makes the current Nix expression a function. The variable
+    `pkgs` contains Nixpkgs (by default, it takes the `nixpkgs` entry of
+    `NIX_PATH`, see the [Nix manual](https://nixos.org/manual/nix/stable/#sec-common-env)
+    for further details), while `config` contains the full system
+    configuration. This line can be omitted if there is no reference to
+    `pkgs` and `config` inside the module.
+
+-   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.
+
+-   The attribute `options` is a nested set of *option declarations*
+    (described below).
+
+-   The attribute `config` is a nested set of *option definitions* (also
+    described below).
+
+[Example: NixOS Module for the "locate" Service](#locate-example)
+shows a module that handles the regular update of the "locate" 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
+`configuration.nix`): `services.locate.enable` (whether the database should
+be updated) and `services.locate.interval` (when the update should be done).
+It implements its functionality by defining two options declared by other
+modules: `systemd.services` (the set of all systemd services) and
+`systemd.timers` (the list of commands to be executed periodically by
+`systemd`).
+
+Care must be taken when writing systemd services using `Exec*` directives. By
+default systemd performs substitution on `%<char>` specifiers in these
+directives, expands environment variables from `$FOO` and `${FOO}`, splits
+arguments on whitespace, and splits commands on `;`. All of these must be escaped
+to avoid unexpected substitution or splitting when interpolating into an `Exec*`
+directive, e.g. when using an `extraArgs` option to pass additional arguments to
+the service. The functions `utils.escapeSystemdExecArg` and
+`utils.escapeSystemdExecArgs` are provided for this, see [Example: Escaping in
+Exec directives](#exec-escaping-example) for an example. When using these
+functions system environment substitution should *not* be disabled explicitly.
+
+::: {#locate-example .example}
+::: {.title}
+**Example: NixOS Module for the "locate" Service**
+:::
+```nix
+{ 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 = "02:15";
+      example = "hourly";
+      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 = "Update Locate Database";
+        path  = [ pkgs.su ];
+        script =
+          ''
+            mkdir -m 0755 -p $(dirname ${toString cfg.output})
+            exec updatedb \
+              --localuser=${cfg.localuser} \
+              ${optionalString (!cfg.includeStore) "--prunepaths='/nix/store'"} \
+              --output=${toString cfg.output} ${concatStringsSep " " cfg.extraFlags}
+          '';
+      };
+
+    systemd.timers.update-locatedb = mkIf cfg.enable
+      { description = "Update timer for locate database";
+        partOf      = [ "update-locatedb.service" ];
+        wantedBy    = [ "timers.target" ];
+        timerConfig.OnCalendar = cfg.interval;
+      };
+  };
+}
+```
+:::
+
+::: {#exec-escaping-example .example}
+::: {.title}
+**Example: Escaping in Exec directives**
+:::
+```nix
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+
+let
+  cfg = config.services.echo;
+  echoAll = pkgs.writeScript "echo-all" ''
+    #! ${pkgs.runtimeShell}
+    for s in "$@"; do
+      printf '%s\n' "$s"
+    done
+  '';
+  args = [ "a%Nything" "lang=\${LANG}" ";" "/bin/sh -c date" ];
+in {
+  systemd.services.echo =
+    { description = "Echo to the journal";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig.Type = "oneshot";
+      serviceConfig.ExecStart = ''
+        ${echoAll} ${utils.escapeSystemdExecArgs args}
+      '';
+    };
+}
+```
+:::
+
+```{=docbook}
+<xi:include href="option-declarations.section.xml" />
+<xi:include href="option-types.section.xml" />
+<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" />
+```
diff --git a/nixos/doc/manual/development/writing-nixos-tests.section.md b/nixos/doc/manual/development/writing-nixos-tests.section.md
new file mode 100644
index 00000000000..7de57d0d2a3
--- /dev/null
+++ b/nixos/doc/manual/development/writing-nixos-tests.section.md
@@ -0,0 +1,366 @@
+# Writing Tests {#sec-writing-nixos-tests}
+
+A NixOS test is a Nix expression that has the following structure:
+
+```nix
+import ./make-test-python.nix {
+
+  # Either the configuration of a single machine:
+  machine =
+    { config, pkgs, ... }:
+    { configuration…
+    };
+
+  # Or a set of machines:
+  nodes =
+    { machine1 =
+        { config, pkgs, ... }: { … };
+      machine2 =
+        { config, pkgs, ... }: { … };
+      …
+    };
+
+  testScript =
+    ''
+      Python code…
+    '';
+}
+```
+
+The attribute `testScript` is a bit of Python code that executes the
+test (described below). During the test, it will start one or more
+virtual machines, the configuration of which is described by the
+attribute `machine` (if you need only one machine in your test) or by
+the attribute `nodes` (if you need multiple machines). For instance,
+[`login.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix)
+only needs a single machine to test whether users can log in
+on the virtual console, whether device ownership is correctly maintained
+when switching between consoles, and so on. On the other hand,
+[`nfs/simple.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nfs/simple.nix),
+which tests NFS client and server functionality in the
+Linux kernel (including whether locks are maintained across server
+crashes), requires three machines: a server and two clients.
+
+There are a few special NixOS configuration options for test VMs:
+
+`virtualisation.memorySize`
+
+:   The memory of the VM in megabytes.
+
+`virtualisation.vlans`
+
+:   The virtual networks to which the VM is connected. See
+    [`nat.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nat.nix)
+    for an example.
+
+`virtualisation.writableStore`
+
+:   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.
+
+For more options, see the module
+[`qemu-vm.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/qemu-vm.nix).
+
+The test script is a sequence of Python statements that perform various
+actions, such as starting VMs, executing commands in the VMs, and so on.
+Each virtual machine is represented as an object stored in the variable
+`name` if this is also the identifier of the machine in the declarative
+config. If you didn\'t specify multiple machines using the `nodes`
+attribute, it is just `machine`. The following example starts the
+machine, waits until it has finished booting, then executes a command
+and checks that the output is more-or-less correct:
+
+```py
+machine.start()
+machine.wait_for_unit("default.target")
+if not "Linux" in machine.succeed("uname"):
+  raise Exception("Wrong OS")
+```
+
+The first line is actually unnecessary; machines are implicitly started
+when you first execute an action on them (such as `wait_for_unit` or
+`succeed`). If you have multiple machines, you can speed up the test by
+starting them in parallel:
+
+```py
+start_all()
+```
+
+## Machine objects {#ssec-machine-objects}
+
+The following methods are available on machine objects:
+
+`start`
+
+:   Start the virtual machine. This method is asynchronous --- it does
+    not wait for the machine to finish booting.
+
+`shutdown`
+
+:   Shut down the machine, waiting for the VM to exit.
+
+`crash`
+
+:   Simulate a sudden power failure, by telling the VM to exit
+    immediately.
+
+`block`
+
+:   Simulate unplugging the Ethernet cable that connects the machine to
+    the other machines.
+
+`unblock`
+
+:   Undo the effect of `block`.
+
+`screenshot`
+
+:   Take a picture of the display of the virtual machine, in PNG format.
+    The screenshot is linked from the HTML log.
+
+`get_screen_text_variants`
+
+:   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.
+
+    ::: {.note}
+    This requires passing `enableOCR` to the test attribute set.
+    :::
+
+`get_screen_text`
+
+:   Return a textual representation of what is currently visible on the
+    machine\'s screen using optical character recognition.
+
+    ::: {.note}
+    This requires passing `enableOCR` to the test attribute set.
+    :::
+
+`send_monitor_command`
+
+:   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.
+
+`send_key`
+
+:   Simulate pressing keys on the virtual keyboard, e.g.,
+    `send_key("ctrl-alt-delete")`.
+
+`send_chars`
+
+:   Simulate typing a sequence of characters on the virtual keyboard,
+    e.g., `send_chars("foobar\n")` will type the string `foobar`
+    followed by the Enter key.
+
+`execute`
+
+:   Execute a shell command, returning a list `(status, stdout)`.
+    If the command detaches, it must close stdout, as `execute` will wait
+    for this to consume all output reliably. This can be achieved by
+    redirecting stdout to stderr `>&2`, to `/dev/console`, `/dev/null` or
+    a file. Examples of detaching commands are `sleep 365d &`, where the
+    shell forks a new process that can write to stdout and `xclip -i`, where
+    the `xclip` command itself forks without closing stdout.
+    Takes an optional parameter `check_return` that defaults to `True`.
+    Setting this parameter to `False` will not check for the return code
+    and return -1 instead. This can be used for commands that shut down
+    the VM and would therefore break the pipe that would be used for
+    retrieving the return code.
+
+`succeed`
+
+:   Execute a shell command, raising an exception if the exit status is
+    not zero, otherwise returning the standard output. Commands are run
+    with `set -euo pipefail` set:
+
+    -   If several commands are separated by `;` and one fails, the
+        command as a whole will fail.
+
+    -   For pipelines, the last non-zero exit status will be returned
+        (if there is one, zero will be returned otherwise).
+
+    -   Dereferencing unset variables fail the command.
+
+    -   It will wait for stdout to be closed. See `execute` for the
+        implications.
+
+`fail`
+
+:   Like `succeed`, but raising an exception if the command returns a zero
+    status.
+
+`wait_until_succeeds`
+
+:   Repeat a shell command with 1-second intervals until it succeeds.
+
+`wait_until_fails`
+
+:   Repeat a shell command with 1-second intervals until it fails.
+
+`wait_for_unit`
+
+:   Wait until the specified systemd unit has reached the "active"
+    state.
+
+`wait_for_file`
+
+:   Wait until the specified file exists.
+
+`wait_for_open_port`
+
+:   Wait until a process is listening on the given TCP port (on
+    `localhost`, at least).
+
+`wait_for_closed_port`
+
+:   Wait until nobody is listening on the given TCP port.
+
+`wait_for_x`
+
+:   Wait until the X11 server is accepting connections.
+
+`wait_for_text`
+
+:   Wait until the supplied regular expressions matches the textual
+    contents of the screen by using optical character recognition (see
+    `get_screen_text` and `get_screen_text_variants`).
+
+    ::: {.note}
+    This requires passing `enableOCR` to the test attribute set.
+    :::
+
+`wait_for_console_text`
+
+:   Wait until the supplied regular expressions match a line of the
+    serial console output. This method is useful when OCR is not
+    possibile or accurate enough.
+
+`wait_for_window`
+
+:   Wait until an X11 window has appeared whose name matches the given
+    regular expression, e.g., `wait_for_window("Terminal")`.
+
+`copy_from_host`
+
+:   Copies a file from host to machine, e.g.,
+    `copy_from_host("myfile", "/etc/my/important/file")`.
+
+    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.
+
+`systemctl`
+
+:   Runs `systemctl` commands with optional support for
+    `systemctl --user`
+
+    ```py
+    machine.systemctl("list-jobs --no-pager") # runs `systemctl list-jobs --no-pager`
+    machine.systemctl("list-jobs --no-pager", "any-user") # spawns a shell for `any-user` and runs `systemctl --user list-jobs --no-pager`
+    ```
+
+`shell_interact`
+
+:   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 `Ctrl-d` or `Ctrl-c` also ends
+    the guest session.
+
+To test user units declared by `systemd.user.services` the optional
+`user` argument can be used:
+
+```py
+machine.start()
+machine.wait_for_x()
+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 commited though):
+
+```nix
+import ./make-test-python.nix {
+  skipLint = true;
+  machine =
+    { config, pkgs, ... }:
+    { configuration…
+    };
+
+  testScript =
+    ''
+      Python code…
+    '';
+}
+```
+
+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):
+
+```nix
+  testScript =
+    ''
+      # fmt: off
+      Python code…
+      # fmt: on
+    '';
+```
+
+## Failing tests early {#ssec-failing-tests-early}
+
+To fail tests early when certain invariables are no longer met (instead of waiting for the build to time out), the decorator `polling_condition` is provided. For example, if we are testing a program `foo` that should not quit after being started, we might write the following:
+
+```py
+@polling_condition
+def foo_running():
+    machine.succeed("pgrep -x foo")
+
+
+machine.succeed("foo --start")
+machine.wait_until_succeeds("pgrep -x foo")
+
+with foo_running:
+    ...  # Put `foo` through its paces
+```
+
+
+`polling_condition` takes the following (optional) arguments:
+
+`seconds_interval`
+
+:
+    specifies how often the condition should be polled:
+
+    ```py
+    @polling_condition(seconds_interval=10)
+    def foo_running():
+        machine.succeed("pgrep -x foo")
+    ```
+
+`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:
+
+    ```py
+    @polling_condition
+    def foo_running():
+        "check that foo is running"
+        machine.succeed("pgrep -x foo")
+    ```
+
+    ```py
+    @polling_condition(description="check that foo is running")
+    def foo_running():
+        machine.succeed("pgrep -x foo")
+    ```
diff --git a/nixos/doc/manual/from_md/README.md b/nixos/doc/manual/from_md/README.md
new file mode 100644
index 00000000000..cc6d08ca0a1
--- /dev/null
+++ b/nixos/doc/manual/from_md/README.md
@@ -0,0 +1,5 @@
+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
new file mode 100644
index 00000000000..144661c86eb
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/boot-problems.section.xml
@@ -0,0 +1,144 @@
+<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
new file mode 100644
index 00000000000..4243d2bf53f
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/cleaning-store.chapter.xml
@@ -0,0 +1,72 @@
+<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
new file mode 100644
index 00000000000..788a2b7b0ac
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/container-networking.section.xml
@@ -0,0 +1,54 @@
+<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
new file mode 100644
index 00000000000..afbd5b35aaa
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/containers.chapter.xml
@@ -0,0 +1,31 @@
+<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
new file mode 100644
index 00000000000..8dab2c9d44b
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/control-groups.chapter.xml
@@ -0,0 +1,67 @@
+<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
new file mode 100644
index 00000000000..7b35520d567
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/declarative-containers.section.xml
@@ -0,0 +1,60 @@
+<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_10;
+      };
+  };
+</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/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
new file mode 100644
index 00000000000..59ecfdee5af
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/imperative-containers.section.xml
@@ -0,0 +1,131 @@
+<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/containers/foo</literal> and a small configuration
+    file in <literal>/etc/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
new file mode 100644
index 00000000000..4da38c065a2
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/logging.chapter.xml
@@ -0,0 +1,45 @@
+<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
new file mode 100644
index 00000000000..c86b1911c11
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/maintenance-mode.section.xml
@@ -0,0 +1,14 @@
+<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
new file mode 100644
index 00000000000..4c0598ca94e
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/network-problems.section.xml
@@ -0,0 +1,25 @@
+<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
new file mode 100644
index 00000000000..78ee75afb64
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/rebooting.chapter.xml
@@ -0,0 +1,38 @@
+<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
new file mode 100644
index 00000000000..a8df053011c
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/rollback.section.xml
@@ -0,0 +1,42 @@
+<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
new file mode 100644
index 00000000000..8b01b8f896a
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/service-mgmt.chapter.xml
@@ -0,0 +1,141 @@
+<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
new file mode 100644
index 00000000000..9ed572d484d
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/store-corruption.section.xml
@@ -0,0 +1,34 @@
+<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
new file mode 100644
index 00000000000..8bbb8a1fe72
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/troubleshooting.chapter.xml
@@ -0,0 +1,12 @@
+<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
new file mode 100644
index 00000000000..e8c64f153fc
--- /dev/null
+++ b/nixos/doc/manual/from_md/administration/user-sessions.chapter.xml
@@ -0,0 +1,46 @@
+<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
new file mode 100644
index 00000000000..c71e23e34ad
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/abstractions.section.xml
@@ -0,0 +1,101 @@
+<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
new file mode 100644
index 00000000000..035ee3122e1
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/ad-hoc-network-config.section.xml
@@ -0,0 +1,16 @@
+<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
new file mode 100644
index 00000000000..c9a8d4f3f10
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/ad-hoc-packages.section.xml
@@ -0,0 +1,59 @@
+<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
new file mode 100644
index 00000000000..4fa40d61966
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/adding-custom-packages.section.xml
@@ -0,0 +1,80 @@
+<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. First, you can clone the Nixpkgs
+    repository, add the package to your clone, and (optionally) submit a
+    patch or pull request to have it accepted into the main Nixpkgs
+    repository. This is described in detail in the
+    <link xlink:href="https://nixos.org/nixpkgs/manual">Nixpkgs
+    manual</link>. In short, you clone Nixpkgs:
+  </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>
diff --git a/nixos/doc/manual/from_md/configuration/config-file.section.xml b/nixos/doc/manual/from_md/configuration/config-file.section.xml
new file mode 100644
index 00000000000..952c6e60030
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/config-file.section.xml
@@ -0,0 +1,231 @@
+<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_10;
+</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
new file mode 100644
index 00000000000..01446e53e38
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/config-syntax.chapter.xml
@@ -0,0 +1,21 @@
+<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" />
+  <xi:include href="summary.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
new file mode 100644
index 00000000000..f78b5dc5460
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/customizing-packages.section.xml
@@ -0,0 +1,90 @@
+<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
new file mode 100644
index 00000000000..da31f18d923
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/declarative-packages.section.xml
@@ -0,0 +1,53 @@
+<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
new file mode 100644
index 00000000000..71441d8b4a5
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/file-systems.chapter.xml
@@ -0,0 +1,55 @@
+<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
new file mode 100644
index 00000000000..24c19bb1c66
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/firewall.section.xml
@@ -0,0 +1,39 @@
+<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
new file mode 100644
index 00000000000..8e780c5dee9
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/gpu-accel.chapter.xml
@@ -0,0 +1,239 @@
+<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-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>jane</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:jane: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
new file mode 100644
index 00000000000..047ba2165f0
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/ipv4-config.section.xml
@@ -0,0 +1,43 @@
+<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
new file mode 100644
index 00000000000..137c3d772a8
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/ipv6-config.section.xml
@@ -0,0 +1,47 @@
+<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
new file mode 100644
index 00000000000..83a50d7c49d
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/kubernetes.chapter.xml
@@ -0,0 +1,126 @@
+<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>
+  <para>
+    As of kubernetes 1.10.X it has been deprecated to open
+    non-tls-enabled ports on kubernetes components. Thus, from NixOS
+    19.03 all plain HTTP ports have been disabled by default. While
+    opening insecure ports is still possible, it is recommended not to
+    bind these to other interfaces than loopback. To re-enable the
+    insecure port on the apiserver, see options:
+    <xref linkend="opt-services.kubernetes.apiserver.insecurePort" />
+    and
+    <xref linkend="opt-services.kubernetes.apiserver.insecureBindAddress" />
+  </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
new file mode 100644
index 00000000000..a1d6815af29
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/linux-kernel.chapter.xml
@@ -0,0 +1,157 @@
+<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>
+    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>
+</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
new file mode 100644
index 00000000000..42b766eba98
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/luks-file-systems.section.xml
@@ -0,0 +1,84 @@
+<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
new file mode 100644
index 00000000000..a7688090fcc
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/modularity.section.xml
@@ -0,0 +1,152 @@
+<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
new file mode 100644
index 00000000000..8f0d6d680ae
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/network-manager.section.xml
@@ -0,0 +1,49 @@
+<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
new file mode 100644
index 00000000000..2ed86ea3b58
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/networking.chapter.xml
@@ -0,0 +1,15 @@
+<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
new file mode 100644
index 00000000000..d3727edbe08
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/package-mgmt.chapter.xml
@@ -0,0 +1,28 @@
+<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
new file mode 100644
index 00000000000..6f5fc130c6a
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/profiles.chapter.xml
@@ -0,0 +1,38 @@
+<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
new file mode 100644
index 00000000000..43ac5edea7f
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/profiles/all-hardware.section.xml
@@ -0,0 +1,15 @@
+<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
new file mode 100644
index 00000000000..83d35bd2867
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/profiles/base.section.xml
@@ -0,0 +1,10 @@
+<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
new file mode 100644
index 00000000000..9430b49ea33
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/profiles/clone-config.section.xml
@@ -0,0 +1,16 @@
+<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
new file mode 100644
index 00000000000..09c2680a106
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/profiles/demo.section.xml
@@ -0,0 +1,10 @@
+<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
new file mode 100644
index 00000000000..97c2a92dcab
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/profiles/docker-container.section.xml
@@ -0,0 +1,12 @@
+<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
new file mode 100644
index 00000000000..1b109519d43
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/profiles/graphical.section.xml
@@ -0,0 +1,14 @@
+<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
new file mode 100644
index 00000000000..44c11786d94
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/profiles/hardened.section.xml
@@ -0,0 +1,25 @@
+<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 overriden 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
new file mode 100644
index 00000000000..0910b9ffaad
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/profiles/headless.section.xml
@@ -0,0 +1,15 @@
+<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
new file mode 100644
index 00000000000..837e69df06e
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/profiles/installation-device.section.xml
@@ -0,0 +1,32 @@
+<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
new file mode 100644
index 00000000000..a3fe30357df
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/profiles/minimal.section.xml
@@ -0,0 +1,13 @@
+<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
new file mode 100644
index 00000000000..f33464f9db4
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/profiles/qemu-guest.section.xml
@@ -0,0 +1,11 @@
+<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
new file mode 100644
index 00000000000..88c9e624c82
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/renaming-interfaces.section.xml
@@ -0,0 +1,62 @@
+<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
new file mode 100644
index 00000000000..037418d8ea4
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/ssh.section.xml
@@ -0,0 +1,23 @@
+<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
new file mode 100644
index 00000000000..5d74712f35d
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/sshfs-file-systems.section.xml
@@ -0,0 +1,139 @@
+<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
new file mode 100644
index 00000000000..794c2c34e39
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/subversion.chapter.xml
@@ -0,0 +1,121 @@
+<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/summary.section.xml b/nixos/doc/manual/from_md/configuration/summary.section.xml
new file mode 100644
index 00000000000..96a178c4930
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/summary.section.xml
@@ -0,0 +1,332 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-nix-syntax-summary">
+  <title>Syntax Summary</title>
+  <para>
+    Below is a summary of the most important syntactic constructs in the
+    Nix expression language. It’s not complete. In particular, there are
+    many other built-in functions. See the
+    <link xlink:href="https://nixos.org/nix/manual/#chap-writing-nix-expressions">Nix
+    manual</link> for the rest.
+  </para>
+  <informaltable>
+    <tgroup cols="2">
+      <colspec align="left" />
+      <colspec align="left" />
+      <thead>
+        <row>
+          <entry>
+            Example
+          </entry>
+          <entry>
+            Description
+          </entry>
+        </row>
+      </thead>
+      <tbody>
+        <row>
+          <entry>
+            <emphasis>Basic values</emphasis>
+          </entry>
+          <entry>
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>&quot;Hello world&quot;</literal>
+          </entry>
+          <entry>
+            A string
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>&quot;${pkgs.bash}/bin/sh&quot;</literal>
+          </entry>
+          <entry>
+            A string containing an expression (expands to
+            <literal>&quot;/nix/store/hash-bash-version/bin/sh&quot;</literal>)
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>true</literal>, <literal>false</literal>
+          </entry>
+          <entry>
+            Booleans
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>123</literal>
+          </entry>
+          <entry>
+            An integer
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>./foo.png</literal>
+          </entry>
+          <entry>
+            A path (relative to the containing Nix expression)
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <emphasis>Compound values</emphasis>
+          </entry>
+          <entry>
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>{ x = 1; y = 2; }</literal>
+          </entry>
+          <entry>
+            A set with attributes named <literal>x</literal> and
+            <literal>y</literal>
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>{ foo.bar = 1; }</literal>
+          </entry>
+          <entry>
+            A nested set, equivalent to
+            <literal>{ foo = { bar = 1; }; }</literal>
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>rec { x = &quot;foo&quot;; y = x + &quot;bar&quot;; }</literal>
+          </entry>
+          <entry>
+            A recursive set, equivalent to
+            <literal>{ x = &quot;foo&quot;; y = &quot;foobar&quot;; }</literal>
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>[ &quot;foo&quot; &quot;bar&quot; ]</literal>
+          </entry>
+          <entry>
+            A list with two elements
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <emphasis>Operators</emphasis>
+          </entry>
+          <entry>
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>&quot;foo&quot; + &quot;bar&quot;</literal>
+          </entry>
+          <entry>
+            String concatenation
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>1 + 2</literal>
+          </entry>
+          <entry>
+            Integer addition
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>&quot;foo&quot; == &quot;f&quot; + &quot;oo&quot;</literal>
+          </entry>
+          <entry>
+            Equality test (evaluates to <literal>true</literal>)
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>&quot;foo&quot; != &quot;bar&quot;</literal>
+          </entry>
+          <entry>
+            Inequality test (evaluates to <literal>true</literal>)
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>!true</literal>
+          </entry>
+          <entry>
+            Boolean negation
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>{ x = 1; y = 2; }.x</literal>
+          </entry>
+          <entry>
+            Attribute selection (evaluates to <literal>1</literal>)
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>{ x = 1; y = 2; }.z or 3</literal>
+          </entry>
+          <entry>
+            Attribute selection with default (evaluates to
+            <literal>3</literal>)
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>{ x = 1; y = 2; } // { z = 3; }</literal>
+          </entry>
+          <entry>
+            Merge two sets (attributes in the right-hand set taking
+            precedence)
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <emphasis>Control structures</emphasis>
+          </entry>
+          <entry>
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>if 1 + 1 == 2 then &quot;yes!&quot; else &quot;no!&quot;</literal>
+          </entry>
+          <entry>
+            Conditional expression
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>assert 1 + 1 == 2; &quot;yes!&quot;</literal>
+          </entry>
+          <entry>
+            Assertion check (evaluates to
+            <literal>&quot;yes!&quot;</literal>). See
+            <xref linkend="sec-assertions" /> for using assertions in
+            modules
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>let x = &quot;foo&quot;; y = &quot;bar&quot;; in x + y</literal>
+          </entry>
+          <entry>
+            Variable definition
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>with pkgs.lib; head [ 1 2 3 ]</literal>
+          </entry>
+          <entry>
+            Add all attributes from the given set to the scope
+            (evaluates to <literal>1</literal>)
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <emphasis>Functions (lambdas)</emphasis>
+          </entry>
+          <entry>
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>x: x + 1</literal>
+          </entry>
+          <entry>
+            A function that expects an integer and returns it increased
+            by 1
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>(x: x + 1) 100</literal>
+          </entry>
+          <entry>
+            A function call (evaluates to 101)
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>let inc = x: x + 1; in inc (inc (inc 100))</literal>
+          </entry>
+          <entry>
+            A function bound to a variable and subsequently called by
+            name (evaluates to 103)
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>{ x, y }: x + y</literal>
+          </entry>
+          <entry>
+            A function that expects a set with required attributes
+            <literal>x</literal> and <literal>y</literal> and
+            concatenates them
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>{ x, y ? &quot;bar&quot; }: x + y</literal>
+          </entry>
+          <entry>
+            A function that expects a set with required attribute
+            <literal>x</literal> and optional <literal>y</literal>,
+            using <literal>&quot;bar&quot;</literal> as default value
+            for <literal>y</literal>
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>{ x, y, ... }: x + y</literal>
+          </entry>
+          <entry>
+            A function that expects a set with required attributes
+            <literal>x</literal> and <literal>y</literal> and ignores
+            any other attributes
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>{ x, y } @ args: x + y</literal>
+          </entry>
+          <entry>
+            A function that expects a set with required attributes
+            <literal>x</literal> and <literal>y</literal>, and binds the
+            whole set to <literal>args</literal>
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <emphasis>Built-in functions</emphasis>
+          </entry>
+          <entry>
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>import ./foo.nix</literal>
+          </entry>
+          <entry>
+            Load and return Nix expression in given file
+          </entry>
+        </row>
+        <row>
+          <entry>
+            <literal>map (x: x + x) [ 1 2 3 ]</literal>
+          </entry>
+          <entry>
+            Apply a function to every element of a list (evaluates to
+            <literal>[ 2 4 6 ]</literal>)
+          </entry>
+        </row>
+      </tbody>
+    </tgroup>
+  </informaltable>
+</section>
diff --git a/nixos/doc/manual/from_md/configuration/user-mgmt.chapter.xml b/nixos/doc/manual/from_md/configuration/user-mgmt.chapter.xml
new file mode 100644
index 00000000000..06492d5c251
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/user-mgmt.chapter.xml
@@ -0,0 +1,105 @@
+<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 -m sha-512</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
new file mode 100644
index 00000000000..1e90d4f3117
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/wayland.chapter.xml
@@ -0,0 +1,31 @@
+<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
new file mode 100644
index 00000000000..82bc2013515
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/wireless.section.xml
@@ -0,0 +1,73 @@
+<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
new file mode 100644
index 00000000000..274d0d817bc
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/x-windows.chapter.xml
@@ -0,0 +1,381 @@
+<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; ];
+services.xserver.useGlamor = true;
+</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
new file mode 100644
index 00000000000..f96ef2e8c48
--- /dev/null
+++ b/nixos/doc/manual/from_md/configuration/xfce.chapter.xml
@@ -0,0 +1,62 @@
+<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 Plugins</title>
+    <para>
+      If you'd like to add extra plugins to Thunar, add them to
+      <xref linkend="opt-services.xserver.desktopManager.xfce.thunarPlugins" />.
+      You shouldn't just add them to
+      <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
new file mode 100644
index 00000000000..a9b0c6a5eef
--- /dev/null
+++ b/nixos/doc/manual/from_md/contributing-to-this-manual.chapter.xml
@@ -0,0 +1,22 @@
+<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="chap-contributing">
+  <title>Contributing to this manual</title>
+  <para>
+    The DocBook and CommonMark sources of 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>
+</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
new file mode 100644
index 00000000000..0d9e911216e
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/activation-script.section.xml
@@ -0,0 +1,150 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-activation-script">
+  <title>Activation script</title>
+  <para>
+    The activation script is a bash script called to activate the new
+    configuration which resides in a NixOS system in
+    <literal>$out/activate</literal>. Since its contents depend on your
+    system configuration, the contents may differ. This chapter explains
+    how the script works in general and some common NixOS snippets.
+    Please be aware that the script is executed on every boot and system
+    switch, so tasks that can be performed in other places should be
+    performed there (for example letting a directory of a service be
+    created by systemd using mechanisms like
+    <literal>StateDirectory</literal>,
+    <literal>CacheDirectory</literal>, … or if that’s not possible using
+    <literal>preStart</literal> of the service).
+  </para>
+  <para>
+    Activation scripts are defined as snippets using
+    <xref linkend="opt-system.activationScripts" />. They can either be
+    a simple multiline string or an attribute set that can depend on
+    other snippets. The builder for the activation script will take
+    these dependencies into account and order the snippets accordingly.
+    As a simple example:
+  </para>
+  <programlisting language="bash">
+system.activationScripts.my-activation-script = {
+  deps = [ &quot;etc&quot; ];
+  # supportsDryActivation = true;
+  text = ''
+    echo &quot;Hallo i bims&quot;
+  '';
+};
+</programlisting>
+  <para>
+    This example creates an activation script snippet that is run after
+    the <literal>etc</literal> snippet. The special variable
+    <literal>supportsDryActivation</literal> can be set so the snippet
+    is also run when <literal>nixos-rebuild dry-activate</literal> is
+    run. To differentiate between real and dry activation, the
+    <literal>$NIXOS_ACTION</literal> environment variable can be read
+    which is set to <literal>dry-activate</literal> when a dry
+    activation is done.
+  </para>
+  <para>
+    An activation script can write to special files instructing
+    <literal>switch-to-configuration</literal> to restart/reload units.
+    The script will take these requests into account and will
+    incorperate the unit configuration as described above. This means
+    that the activation script will <quote>fake</quote> a modified unit
+    file and <literal>switch-to-configuration</literal> will act
+    accordingly. By doing so, configuration like
+    <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.restartIfChanged</link>
+    is respected. Since the activation script is run
+    <emphasis role="strong">after</emphasis> services are already
+    stopped,
+    <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.stopIfChanged</link>
+    cannot be taken into account anymore and the unit is always
+    restarted instead of being stopped and started afterwards.
+  </para>
+  <para>
+    The files that can be written to are
+    <literal>/run/nixos/activation-restart-list</literal> and
+    <literal>/run/nixos/activation-reload-list</literal> with their
+    respective counterparts for dry activation being
+    <literal>/run/nixos/dry-activation-restart-list</literal> and
+    <literal>/run/nixos/dry-activation-reload-list</literal>. Those
+    files can contain newline-separated lists of unit names where
+    duplicates are being ignored. These files are not create
+    automatically and activation scripts must take the possiblility into
+    account that they have to create them first.
+  </para>
+  <section xml:id="sec-activation-script-nixos-snippets">
+    <title>NixOS snippets</title>
+    <para>
+      There are some snippets NixOS enables by default because disabling
+      them would most likely break you 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
new file mode 100644
index 00000000000..0844d484d60
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/assertions.section.xml
@@ -0,0 +1,58 @@
+<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/building-nixos.chapter.xml b/nixos/doc/manual/from_md/development/building-nixos.chapter.xml
new file mode 100644
index 00000000000..ad9349da068
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/building-nixos.chapter.xml
@@ -0,0 +1,72 @@
+<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 wich 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>
+    <programlisting>
+$ git clone https://github.com/NixOS/nixpkgs.git
+$ cd nixpkgs/nixos
+$ 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-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 befor
+      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/development/building-parts.chapter.xml b/nixos/doc/manual/from_md/development/building-parts.chapter.xml
new file mode 100644
index 00000000000..4df24cc9565
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/building-parts.chapter.xml
@@ -0,0 +1,124 @@
+<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
new file mode 100644
index 00000000000..86a9cf3140d
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/freeform-modules.section.xml
@@ -0,0 +1,87 @@
+<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
new file mode 100644
index 00000000000..cb04dde67c8
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/importing-modules.section.xml
@@ -0,0 +1,47 @@
+<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
new file mode 100644
index 00000000000..666bbec6162
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/linking-nixos-tests-to-packages.section.xml
@@ -0,0 +1,10 @@
+<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
new file mode 100644
index 00000000000..1eb6e0f3036
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/meta-attributes.section.xml
@@ -0,0 +1,95 @@
+<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
new file mode 100644
index 00000000000..b9ff2269676
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/nixos-tests.chapter.xml
@@ -0,0 +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" 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
new file mode 100644
index 00000000000..0ac5e0eeca2
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/option-declarations.section.xml
@@ -0,0 +1,327 @@
+<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 = &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.literalDocBook</literal> for a plain English
+          description in DocBook 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.literalDocBook</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 DocBook format, that
+          will be included in the NixOS manual.
+        </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 = &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 = &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.package.ghc921.ghc.withPackages (hkgs: [ hkgs.primes ])&quot;;
+}
+# is like
+lib.mkOption {
+  type = lib.types.package;
+  default = pkgs.ghc;
+  defaultText = lib.literalExpression &quot;pkgs.ghc&quot;;
+  example = lib.literalExpression &quot;pkgs.haskell.package.ghc921.ghc.withPackages (hkgs: [ hkgs.primes ])&quot;;
+  description = &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
new file mode 100644
index 00000000000..8c9ef181aff
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/option-def.section.xml
@@ -0,0 +1,104 @@
+<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 a <emphasis>priority</emphasis>. All option
+      definitions that do not have the lowest priority value are
+      discarded. By default, option definitions have priority 1000. 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>.
+    </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
new file mode 100644
index 00000000000..44472929270
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/option-types.section.xml
@@ -0,0 +1,1038 @@
+<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.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>
+    <para>
+      Integer-related types:
+    </para>
+    <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.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>
+    </variablelist>
+    <para>
+      String-related types:
+    </para>
+    <variablelist>
+      <varlistentry>
+        <term>
+          <literal>types.str</literal>
+        </term>
+        <listitem>
+          <para>
+            A string. Multiple definitions cannot be merged.
+          </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
+            collon <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 xml:id="sec-option-types-value">
+    <title>Value Types</title>
+    <para>
+      Value types are types that take a value parameter.
+    </para>
+    <variablelist>
+      <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.separatedString</literal>
+          <emphasis><literal>sep</literal></emphasis>
+        </term>
+        <listitem>
+          <para>
+            A string with a custom separator
+            <emphasis><literal>sep</literal></emphasis>, e.g.
+            <literal>types.separatedString &quot;|&quot;</literal>.
+          </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). Useful for creating types like
+            <literal>types.port</literal>.
+          </para>
+        </listitem>
+      </varlistentry>
+      <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>.
+            Submodules are detailed in
+            <link linkend="section-option-types-submodule">Submodule</link>.
+          </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>
+    </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 definitons 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>
+    <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
new file mode 100644
index 00000000000..cf8a39ba844
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/replace-modules.section.xml
@@ -0,0 +1,70 @@
+<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
new file mode 100644
index 00000000000..0e47350a0d2
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/running-nixos-tests-interactively.section.xml
@@ -0,0 +1,39 @@
+<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>
+  <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>
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
new file mode 100644
index 00000000000..da2e5076c95
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/running-nixos-tests.section.xml
@@ -0,0 +1,34 @@
+<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 just do:
+  </para>
+  <programlisting>
+$ nix-build '&lt;nixpkgs/nixos/tests/login.nix&gt;'
+</programlisting>
+  <para>
+    or, if you don’t want to rely on <literal>NIX_PATH</literal>:
+  </para>
+  <programlisting>
+$ cd /my/nixpkgs/nixos/tests
+$ nix-build login.nix
+…
+running the VM test script
+machine: QEMU running (pid 8841)
+…
+6 out of 6 tests succeeded
+</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
new file mode 100644
index 00000000000..746011a2d07
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/settings-options.section.xml
@@ -0,0 +1,389 @@
+<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.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
new file mode 100644
index 00000000000..aac18c9d06c
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/sources.chapter.xml
@@ -0,0 +1,90 @@
+<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
new file mode 100644
index 00000000000..286d49f3c29
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/testing-installer.chapter.xml
@@ -0,0 +1,22 @@
+<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
new file mode 100644
index 00000000000..4c980e1213a
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/unit-handling.section.xml
@@ -0,0 +1,131 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-unit-handling">
+  <title>Unit handling</title>
+  <para>
+    To figure out what units need to be
+    started/stopped/restarted/reloaded, the script first checks the
+    current state of the system, similar to what
+    <literal>systemctl list-units</literal> shows. For each of the
+    units, the script goes through the following checks:
+  </para>
+  <itemizedlist>
+    <listitem>
+      <para>
+        Is the unit file still in the new system? If not,
+        <emphasis role="strong">stop</emphasis> the service unless it
+        sets <literal>X-StopOnRemoval</literal> in the
+        <literal>[Unit]</literal> section to <literal>false</literal>.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Is it a <literal>.target</literal> unit? If so,
+        <emphasis role="strong">start</emphasis> it unless it sets
+        <literal>RefuseManualStart</literal> in the
+        <literal>[Unit]</literal> section to <literal>true</literal> or
+        <literal>X-OnlyManualStart</literal> in the
+        <literal>[Unit]</literal> section to <literal>true</literal>.
+        Also <emphasis role="strong">stop</emphasis> the unit again
+        unless it sets <literal>X-StopOnReconfiguration</literal> to
+        <literal>false</literal>.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Are the contents of the unit files different? They are compared
+        by parsing them and comparing their contents. If they are
+        different but only <literal>X-Reload-Triggers</literal> in the
+        <literal>[Unit]</literal> section is changed,
+        <emphasis role="strong">reload</emphasis> the unit. The NixOS
+        module system allows setting these triggers with the option
+        <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.reloadTriggers</link>.
+        There are some additional keys in the <literal>[Unit]</literal>
+        section that are ignored as well. If the unit files differ in
+        any way, the following actions are performed:
+      </para>
+      <itemizedlist>
+        <listitem>
+          <para>
+            <literal>.path</literal> and <literal>.slice</literal> units
+            are ignored. There is no need to restart them since changes
+            in their values are applied by systemd when systemd is
+            reloaded.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>.mount</literal> units are
+            <emphasis role="strong">reload</emphasis>ed. These mostly
+            come from the <literal>/etc/fstab</literal> parser.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>.socket</literal> units are currently ignored. This
+            is to be fixed at a later point.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            The rest of the units (mostly <literal>.service</literal>
+            units) are then <emphasis role="strong">reload</emphasis>ed
+            if <literal>X-ReloadIfChanged</literal> in the
+            <literal>[Service]</literal> section is set to
+            <literal>true</literal> (exposed via
+            <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.reloadIfChanged</link>).
+            A little exception is done for units that were deactivated
+            in the meantime, for example because they require a unit
+            that got stopped before. These are
+            <emphasis role="strong">start</emphasis>ed instead of
+            reloaded.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            If the reload flag is not set, some more flags decide if the
+            unit is skipped. These flags are
+            <literal>X-RestartIfChanged</literal> in the
+            <literal>[Service]</literal> section (exposed via
+            <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.restartIfChanged</link>),
+            <literal>RefuseManualStop</literal> in the
+            <literal>[Unit]</literal> section, and
+            <literal>X-OnlyManualStart</literal> in the
+            <literal>[Unit]</literal> section.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            Further behavior depends on the unit having
+            <literal>X-StopIfChanged</literal> in the
+            <literal>[Service]</literal> section set to
+            <literal>true</literal> (exposed via
+            <link linkend="opt-systemd.services">systemd.services.&lt;name&gt;.stopIfChanged</link>).
+            This is set to <literal>true</literal> by default and must
+            be explicitly turned off if not wanted. If the flag is
+            enabled, the unit is
+            <emphasis role="strong">stop</emphasis>ped and then
+            <emphasis role="strong">start</emphasis>ed. If not, the unit
+            is <emphasis role="strong">restart</emphasis>ed. The goal of
+            the flag is to make sure that the new unit never runs in the
+            old environment which is still in place before the
+            activation script is run. This behavior is different when
+            the service is socket-activated, as outlined in the
+            following steps.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            The last thing that is taken into account is whether the
+            unit is a service and socket-activated. If
+            <literal>X-StopIfChanged</literal> is
+            <emphasis role="strong">not</emphasis> set, the service is
+            <emphasis role="strong">restart</emphasis>ed with the
+            others. If it is set, both the service and the socket are
+            <emphasis role="strong">stop</emphasis>ped and the socket is
+            <emphasis role="strong">start</emphasis>ed, leaving socket
+            activation to start the service when it’s needed.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </listitem>
+  </itemizedlist>
+</section>
diff --git a/nixos/doc/manual/from_md/development/what-happens-during-a-system-switch.chapter.xml b/nixos/doc/manual/from_md/development/what-happens-during-a-system-switch.chapter.xml
new file mode 100644
index 00000000000..66ba792ddac
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/what-happens-during-a-system-switch.chapter.xml
@@ -0,0 +1,122 @@
+<chapter xmlns="http://docbook.org/ns/docbook"  xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xi="http://www.w3.org/2001/XInclude" xml:id="sec-switching-systems">
+  <title>What happens during a system switch?</title>
+  <para>
+    Running <literal>nixos-rebuild switch</literal> is one of the more
+    common tasks under NixOS. This chapter explains some of the
+    internals of this command to make it simpler for new module
+    developers to configure their units correctly and to make it easier
+    to understand what is happening and why for curious administrators.
+  </para>
+  <para>
+    <literal>nixos-rebuild</literal>, like many deployment solutions,
+    calls <literal>switch-to-configuration</literal> which resides in a
+    NixOS system at <literal>$out/bin/switch-to-configuration</literal>.
+    The script is called with the action that is to be performed like
+    <literal>switch</literal>, <literal>test</literal>,
+    <literal>boot</literal>. There is also the
+    <literal>dry-activate</literal> action which does not really perform
+    the actions but rather prints what it would do if you called it with
+    <literal>test</literal>. This feature can be used to check what
+    service states would be changed if the configuration was switched
+    to.
+  </para>
+  <para>
+    If the action is <literal>switch</literal> or
+    <literal>boot</literal>, the bootloader is updated first so the
+    configuration will be the next one to boot. Unless
+    <literal>NIXOS_NO_SYNC</literal> is set to <literal>1</literal>,
+    <literal>/nix/store</literal> is synced to disk.
+  </para>
+  <para>
+    If the action is <literal>switch</literal> or
+    <literal>test</literal>, the currently running system is inspected
+    and the actions to switch to the new system are calculated. This
+    process takes two data sources into account:
+    <literal>/etc/fstab</literal> and the current systemd status. Mounts
+    and swaps are read from <literal>/etc/fstab</literal> and the
+    corresponding actions are generated. If a new mount is added, for
+    example, the proper <literal>.mount</literal> unit is marked to be
+    started. The current systemd state is inspected, the difference
+    between the current system and the desired configuration is
+    calculated and actions are generated to get to this state. There are
+    a lot of nuances that can be controlled by the units which are
+    explained here.
+  </para>
+  <para>
+    After calculating what should be done, the actions are carried out.
+    The order of actions is always the same:
+  </para>
+  <itemizedlist spacing="compact">
+    <listitem>
+      <para>
+        Stop units (<literal>systemctl stop</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Run activation script (<literal>$out/activate</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        See if the activation script requested more units to restart
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Restart systemd if needed
+        (<literal>systemd daemon-reexec</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Forget about the failed state of units
+        (<literal>systemctl reset-failed</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Reload systemd (<literal>systemctl daemon-reload</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Reload systemd user instances
+        (<literal>systemctl --user daemon-reload</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Set up tmpfiles (<literal>systemd-tmpfiles --create</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Reload units (<literal>systemctl reload</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Restart units (<literal>systemctl restart</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Start units (<literal>systemctl start</literal>)
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Inspect what changed during these actions and print units that
+        failed and that were newly started
+      </para>
+    </listitem>
+  </itemizedlist>
+  <para>
+    Most of these actions are either self-explaining but some of them
+    have to do with our units or the activation script. For this reason,
+    these topics are explained in the next sections.
+  </para>
+  <xi:include href="unit-handling.section.xml" />
+  <xi:include href="activation-script.section.xml" />
+</chapter>
diff --git a/nixos/doc/manual/from_md/development/writing-documentation.chapter.xml b/nixos/doc/manual/from_md/development/writing-documentation.chapter.xml
new file mode 100644
index 00000000000..079c8006057
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/writing-documentation.chapter.xml
@@ -0,0 +1,144 @@
+<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
new file mode 100644
index 00000000000..367731eda09
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/writing-modules.chapter.xml
@@ -0,0 +1,245 @@
+<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
new file mode 100644
index 00000000000..45c9c40c609
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/writing-nixos-tests.section.xml
@@ -0,0 +1,616 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-writing-nixos-tests">
+  <title>Writing Tests</title>
+  <para>
+    A NixOS test is a Nix expression that has the following structure:
+  </para>
+  <programlisting language="bash">
+import ./make-test-python.nix {
+
+  # Either the configuration of a single machine:
+  machine =
+    { config, pkgs, ... }:
+    { configuration…
+    };
+
+  # Or a set of machines:
+  nodes =
+    { machine1 =
+        { config, pkgs, ... }: { … };
+      machine2 =
+        { config, pkgs, ... }: { … };
+      …
+    };
+
+  testScript =
+    ''
+      Python code…
+    '';
+}
+</programlisting>
+  <para>
+    The attribute <literal>testScript</literal> is a bit of Python code
+    that executes the test (described below). During the test, it will
+    start one or more virtual machines, the configuration of which is
+    described by the attribute <literal>machine</literal> (if you need
+    only one machine in your test) or by the attribute
+    <literal>nodes</literal> (if you need multiple machines). For
+    instance,
+    <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix"><literal>login.nix</literal></link>
+    only needs a single machine to test whether users can log in on the
+    virtual console, whether device ownership is correctly maintained
+    when switching between consoles, and so on. On the other hand,
+    <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nfs/simple.nix"><literal>nfs/simple.nix</literal></link>,
+    which tests NFS client and server functionality in the Linux kernel
+    (including whether locks are maintained across server crashes),
+    requires three machines: a server and two clients.
+  </para>
+  <para>
+    There are a few special NixOS configuration 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 didn't
+    specify multiple machines using the <literal>nodes</literal>
+    attribute, it is just <literal>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 actually 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 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 passing <literal>enableOCR</literal> to the
+              test attribute set.
+            </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 passing <literal>enableOCR</literal> to the
+              test attribute set.
+            </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>execute</literal>
+        </term>
+        <listitem>
+          <para>
+            Execute a shell command, returning a list
+            <literal>(status, stdout)</literal>. If the command
+            detaches, it must close stdout, as
+            <literal>execute</literal> will wait for this to consume all
+            output reliably. This can be achieved by redirecting stdout
+            to stderr <literal>&gt;&amp;2</literal>, to
+            <literal>/dev/console</literal>,
+            <literal>/dev/null</literal> or a file. Examples of
+            detaching commands are <literal>sleep 365d &amp;</literal>,
+            where the shell forks a new process that can write to stdout
+            and <literal>xclip -i</literal>, where the
+            <literal>xclip</literal> command itself forks without
+            closing stdout. Takes an optional parameter
+            <literal>check_return</literal> that defaults to
+            <literal>True</literal>. Setting this parameter to
+            <literal>False</literal> will not check for the return code
+            and return -1 instead. This can be used for commands that
+            shut down the VM and would therefore break the pipe that
+            would be used for retrieving the return code.
+          </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.
+            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, zero will be returned
+                otherwise).
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                Dereferencing unset variables fail the command.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                It will wait for stdout to be closed. See
+                <literal>execute</literal> for the implications.
+              </para>
+            </listitem>
+          </itemizedlist>
+        </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.
+          </para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
+          <literal>wait_until_fails</literal>
+        </term>
+        <listitem>
+          <para>
+            Repeat a shell command with 1-second intervals 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 passing <literal>enableOCR</literal> to the
+              test attribute set.
+            </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 possibile 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>
+    </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 commited though):
+    </para>
+    <programlisting language="bash">
+import ./make-test-python.nix {
+  skipLint = true;
+  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>
+  </section>
+  <section xml:id="ssec-failing-tests-early">
+    <title>Failing tests early</title>
+    <para>
+      To fail tests early when certain invariables 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>
+```py
+@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>
+```py
+@polling_condition
+def foo_running():
+    &quot;check that foo is running&quot;
+    machine.succeed(&quot;pgrep -x foo&quot;)
+```
+
+```py
+@polling_condition(description=&quot;check that foo is running&quot;)
+def foo_running():
+    machine.succeed(&quot;pgrep -x foo&quot;)
+```
+</programlisting>
+  </section>
+</section>
diff --git a/nixos/doc/manual/from_md/installation/changing-config.chapter.xml b/nixos/doc/manual/from_md/installation/changing-config.chapter.xml
new file mode 100644
index 00000000000..86f0b15b41c
--- /dev/null
+++ b/nixos/doc/manual/from_md/installation/changing-config.chapter.xml
@@ -0,0 +1,117 @@
+<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
new file mode 100644
index 00000000000..a551807cd47
--- /dev/null
+++ b/nixos/doc/manual/from_md/installation/installing-behind-a-proxy.section.xml
@@ -0,0 +1,41 @@
+<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
new file mode 100644
index 00000000000..525531a4781
--- /dev/null
+++ b/nixos/doc/manual/from_md/installation/installing-from-other-distro.section.xml
@@ -0,0 +1,388 @@
+<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` --root /
+</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 initalize 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-pxe.section.xml b/nixos/doc/manual/from_md/installation/installing-pxe.section.xml
new file mode 100644
index 00000000000..94172de65ea
--- /dev/null
+++ b/nixos/doc/manual/from_md/installation/installing-pxe.section.xml
@@ -0,0 +1,42 @@
+<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
new file mode 100644
index 00000000000..b46a1d56555
--- /dev/null
+++ b/nixos/doc/manual/from_md/installation/installing-usb.section.xml
@@ -0,0 +1,35 @@
+<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 Drive</title>
+  <para>
+    For systems without CD drive, the NixOS live CD can be booted from a
+    USB stick. You can use the <literal>dd</literal> utility to write
+    the image: <literal>dd if=path-to-image of=/dev/sdX</literal>. Be
+    careful about specifying the correct drive; you can use the
+    <literal>lsblk</literal> command to get a list of block devices.
+  </para>
+  <note>
+    <title>On macOS</title>
+    <programlisting>
+$ diskutil list
+[..]
+/dev/diskN (external, physical):
+   #:                       TYPE NAME                    SIZE       IDENTIFIER
+[..]
+$ diskutil unmountDisk diskN
+Unmount of all volumes on diskN was successful
+$ sudo dd if=nix.iso of=/dev/rdiskN
+</programlisting>
+    <para>
+      Using the 'raw' <literal>rdiskN</literal> device instead of
+      <literal>diskN</literal> completes in minutes instead of hours.
+      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>
+    The <literal>dd</literal> utility will write the image verbatim to
+    the drive, making it the recommended option for both UEFI and
+    non-UEFI installations.
+  </para>
+</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
new file mode 100644
index 00000000000..c8bb286c8f3
--- /dev/null
+++ b/nixos/doc/manual/from_md/installation/installing-virtualbox-guest.section.xml
@@ -0,0 +1,92 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-instaling-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
new file mode 100644
index 00000000000..db073fa8396
--- /dev/null
+++ b/nixos/doc/manual/from_md/installation/installing.chapter.xml
@@ -0,0 +1,645 @@
+<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 the system</title>
+    <para>
+      NixOS can be installed on BIOS or UEFI systems. The procedure for
+      a UEFI installation is by and large the same as a BIOS
+      installation. The differences are mentioned in the steps that
+      follow.
+    </para>
+    <para>
+      The installation media can be burned to a CD, or now more
+      commonly, <quote>burned</quote> to a USB drive (see
+      <xref linkend="sec-booting-from-usb" />).
+    </para>
+    <para>
+      The installation media contains a basic NixOS installation. When
+      it’s finished booting, it should have detected most of your
+      hardware.
+    </para>
+    <para>
+      The NixOS manual is available by running
+      <literal>nixos-help</literal>.
+    </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>
+      If you downloaded the graphical ISO image, you can run
+      <literal>systemctl start display-manager</literal> to start the
+      desktop environment. If you want to continue on the terminal, 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-booting-networking">
+      <title>Networking in the installer</title>
+      <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 perfomed 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>
+  <section xml:id="sec-installation-partitioning">
+    <title>Partitioning and formatting</title>
+    <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-partitioning-UEFI">
+      <title>UEFI (GPT)</title>
+      <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 512MiB -8GiB
+</programlisting>
+        </listitem>
+        <listitem>
+          <para>
+            Next, add a <emphasis>swap</emphasis> partition. The size
+            required will vary according to needs, here a 8GiB one is
+            created.
+          </para>
+          <programlisting>
+# parted /dev/sda -- mkpart primary linux-swap -8GiB 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 1MiB 512MiB
+# parted /dev/sda -- set 3 esp on
+</programlisting>
+        </listitem>
+      </orderedlist>
+      <para>
+        Once complete, you can follow with
+        <xref linkend="sec-installation-partitioning-formatting" />.
+      </para>
+    </section>
+    <section xml:id="sec-installation-partitioning-MBR">
+      <title>Legacy Boot (MBR)</title>
+      <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 1MiB -8GiB
+</programlisting>
+        </listitem>
+        <listitem>
+          <para>
+            Finally, add a <emphasis>swap</emphasis> partition. The size
+            required will vary according to needs, here a 8GiB one is
+            created.
+          </para>
+          <programlisting>
+# parted /dev/sda -- mkpart primary linux-swap -8GiB 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-partitioning-formatting" />.
+      </para>
+    </section>
+    <section xml:id="sec-installation-partitioning-formatting">
+      <title>Formatting</title>
+      <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-installing">
+    <title>Installing</title>
+    <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>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>
+              UEFI systems
+            </term>
+            <listitem>
+              <para>
+                You <emphasis>must</emphasis> 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>
+            </listitem>
+          </varlistentry>
+        </variablelist>
+        <para>
+          If there are other operating systems running on the machine
+          before installing NixOS, the
+          <xref linkend="opt-boot.loader.grub.useOSProber" /> option can
+          be set to <literal>true</literal> to automatically add them to
+          the grub menu.
+        </para>
+        <para>
+          If you need to configure networking for your machine the
+          configuration options are described in
+          <xref linkend="sec-networking" />. In particular, while wifi
+          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-summary">
+    <title>Installation summary</title>
+    <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 1MiB -8GiB
+# parted /dev/sda -- mkpart primary linux-swap -8GiB 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 512MiB -8GiB
+# parted /dev/sda -- mkpart primary linux-swap -8GiB 100%
+# parted /dev/sda -- mkpart ESP fat32 1MiB 512MiB
+# 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 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-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
new file mode 100644
index 00000000000..a922feda253
--- /dev/null
+++ b/nixos/doc/manual/from_md/installation/obtaining.chapter.xml
@@ -0,0 +1,48 @@
+<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/nixos/download.html">NixOS
+    download page</link>. There are a number of installation options. If
+    you happen to have an optical drive and a spare CD, burning the
+    image to CD and booting from that is probably the easiest option.
+    Most people will need to prepare a USB stick to boot from.
+    <xref linkend="sec-booting-from-usb" /> describes the preferred
+    method to prepare a USB stick. A number of alternative methods are
+    presented in the
+    <link xlink:href="https://nixos.wiki/wiki/NixOS_Installation_Guide#Making_the_installation_media">NixOS
+    Wiki</link>.
+  </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/nixos/download.html">NixOS
+        download page</link>.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        Using AMIs for Amazon’s EC2. To find one for your region and
+        instance type, please refer to the
+        <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/ec2-amis.nix">list
+        of most recent AMIs</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
new file mode 100644
index 00000000000..e3b77d4c365
--- /dev/null
+++ b/nixos/doc/manual/from_md/installation/upgrading.chapter.xml
@@ -0,0 +1,152 @@
+<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-21.11"><literal>nixos-21.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-21.11-small"><literal>nixos-21.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 21.11 ISO, you will be subscribed
+    to the <literal>nixos-21.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 21.11 stable channel:
+  </para>
+  <programlisting>
+# nix-channel --add https://nixos.org/channels/nixos-21.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-21.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-21.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
new file mode 100644
index 00000000000..b4f3657b4b8
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-1310.section.xml
@@ -0,0 +1,6 @@
+<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
new file mode 100644
index 00000000000..8771623b468
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-1404.section.xml
@@ -0,0 +1,189 @@
+<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
new file mode 100644
index 00000000000..3b6af73359d
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-1412.section.xml
@@ -0,0 +1,466 @@
+<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
new file mode 100644
index 00000000000..68d2ab389e8
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-1509.section.xml
@@ -0,0 +1,776 @@
+<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
new file mode 100644
index 00000000000..172b800b599
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-1603.section.xml
@@ -0,0 +1,695 @@
+<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 overriden 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
new file mode 100644
index 00000000000..0fba40a0e78
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-1609.section.xml
@@ -0,0 +1,273 @@
+<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
new file mode 100644
index 00000000000..1119ec53dfc
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-1703.section.xml
@@ -0,0 +1,818 @@
+<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
new file mode 100644
index 00000000000..8f0efe816e5
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-1709.section.xml
@@ -0,0 +1,922 @@
+<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 accomodate 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
new file mode 100644
index 00000000000..f54f6129e0d
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-1803.section.xml
@@ -0,0 +1,871 @@
+<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>
+    </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
new file mode 100644
index 00000000000..aa4637a99b6
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-1809.section.xml
@@ -0,0 +1,941 @@
+<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
new file mode 100644
index 00000000000..f26e68e1320
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-1903.section.xml
@@ -0,0 +1,790 @@
+<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 seperators. 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
new file mode 100644
index 00000000000..83cd649f4ea
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-1909.section.xml
@@ -0,0 +1,1197 @@
+<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 offical 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 seperate 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
new file mode 100644
index 00000000000..53e6e1329a9
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-2003.section.xml
@@ -0,0 +1,1497 @@
+<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
new file mode 100644
index 00000000000..edebd92b327
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-2009.section.xml
@@ -0,0 +1,2210 @@
+<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
new file mode 100644
index 00000000000..fb11b19229e
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-2105.section.xml
@@ -0,0 +1,1567 @@
+<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 accomodate new features to
+          reduce account rate limit issues. This will trigger new
+          account creation on the first rebuild following this update.
+          No issues are expected to arise from this, thanks to the new
+          account creation handling.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <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
new file mode 100644
index 00000000000..b61a0268dee
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
@@ -0,0 +1,2091 @@
+<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>
+    </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>,
+          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>gnat11</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>
+    </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>
+    </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
new file mode 100644
index 00000000000..348374026b4
--- /dev/null
+++ b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
@@ -0,0 +1,1630 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-22.05">
+  <title>Release 22.05 (“Quokkaâ€, 2022.05/??)</title>
+  <para>
+    In addition to numerous new and upgraded packages, this release has
+    the following highlights:
+  </para>
+  <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>
+    <itemizedlist>
+      <listitem>
+        <para>
+          <literal>security.acme.defaults</literal> has been added to
+          simplify configuring settings for many certificates at once.
+          This also opens up the the option to use DNS-01 validation
+          when using <literal>enableACME</literal> on web server virtual
+          hosts (e.g.
+          <literal>services.nginx.virtualHosts.*.enableACME</literal>).
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          PHP 8.1 is now available
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Mattermost has been updated to extended support release 6.3,
+          as the previously packaged extended support release 5.37 is
+          <link xlink:href="https://docs.mattermost.com/upgrade/extended-support-release.html">reaching
+          its end of life</link>. Migrations may take a while, see the
+          <link xlink:href="https://docs.mattermost.com/install/self-managed-changelog.html#release-v6-3-extended-support-release">changelog</link>
+          and
+          <link xlink:href="https://docs.mattermost.com/upgrade/important-upgrade-notes.html">important
+          upgrade notes</link>.
+        </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>
+          <link xlink:href="https://kops.sigs.k8s.io"><literal>kops</literal></link>
+          defaults to 1.22.4, 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 1.22. This will increase security by
+          default, but may break some types of workloads. See the
+          <link xlink:href="https://kops.sigs.k8s.io/releases/1.22-notes/">release
+          notes</link> for details.
+        </para>
+      </listitem>
+    </itemizedlist>
+  </section>
+  <section xml:id="sec-release-22.05-new-services">
+    <title>New Services</title>
+    <itemizedlist>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/intel/linux-sgx#install-the-intelr-sgx-psw">aesmd</link>,
+          the Intel SGX Architectural Enclave Service Manager. Available
+          as
+          <link linkend="opt-services.aesmd.enable">services.aesmd</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://docs.docker.com/engine/security/rootless/">rootless
+          Docker</link>, a <literal>systemd --user</literal> Docker
+          service which runs without root permissions. Available as
+          <link xlink:href="options.html#opt-virtualisation.docker.rootless.enable">virtualisation.docker.rootless.enable</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://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://www.elastic.co/guide/en/beats/filebeat/current/filebeat-overview.html">filebeat</link>,
+          a lightweight shipper for forwarding and centralizing log
+          data. Available as
+          <link linkend="opt-services.filebeat.enable">services.filebeat</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/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://frrouting.org/">FRRouting</link>, a
+          popular suite of Internet routing protocol daemons (BGP, BFD,
+          OSPF, IS-IS, VVRP and others). Available as
+          <link linkend="opt-services.frr.babel.enable">services.frr</link>
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/hifi/heisenbridge">heisenbridge</link>,
+          a bouncer-style Matrix IRC bridge. Available as
+          <link xlink:href="options.html#opt-services.heisenbridge.enable">services.heisenbridge</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://snowflake.torproject.org/">snowflake-proxy</link>,
+          a system to defeat internet censorship. Available as
+          <link xlink:href="options.html#opt-services.snowflake-proxy.enable">services.snowflake-proxy</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://ergo.chat">ergochat</link>, a modern
+          IRC with IRCv3 features. Available as
+          <link xlink:href="options.html#opt-services.ergochat.enable">services.ergochat</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/ngoduykhanh/PowerDNS-Admin">PowerDNS-Admin</link>,
+          a web interface for the PowerDNS server. Available at
+          <link xlink:href="options.html#opt-services.powerdns-admin.enable">services.powerdns-admin</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/postgres/pgadmin4">pgadmin4</link>,
+          an admin interface for the PostgreSQL database. Available at
+          <link xlink:href="options.html#opt-services.pgadmin.enable">services.pgadmin</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 xlink:href="options.html#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 xlink:href="options.html#opt-services.invoiceplane.enable">services.invoiceplane</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://maddy.email">maddy</link>, a
+          composable all-in-one mail server. Available as
+          <link xlink:href="options.html#opt-services.maddy.enable">services.maddy</link>.
+        </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 xlink:href="options.html#opt-programs.k4-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://github.com/mgumz/mtr-exporter">mtr-exporter</link>,
+          a Prometheus exporter for mtr metrics. Available as
+          <link xlink:href="options.html#opt-services.mtr-exporter.enable">services.mtr-exporter</link>.
+        </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 xlink:href="options.html#opt-services.prometheus.exporters.pve">services.prometheus.exporters.pve</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://github.com/mbrubeck/agate">agate</link>,
+          a very simple server for the Gemini hypertext protocol.
+          Available as
+          <link xlink:href="options.html#opt-services.agate.enable">services.agate</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/JustArchiNET/ArchiSteamFarm">ArchiSteamFarm</link>,
+          a C# application with primary purpose of idling Steam cards
+          from multiple accounts simultaneously. Available as
+          <link xlink:href="options.html#opt-services.archisteamfarm.enable">services.archisteamfarm</link>.
+        </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://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://moosefs.com">moosefs</link>, fault
+          tolerant petabyte distributed file system. Available as
+          <link linkend="opt-services.moosefs.client.enable">moosefs</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://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://github.com/audreyt/ethercalc">ethercalc</link>,
+          an online collaborative spreadsheet. Available as
+          <link xlink:href="options.html#opt-services.ethercalc.enable">services.ethercalc</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://nbd.sourceforge.io/">nbd</link>, a
+          Network Block Device server. Available as
+          <link xlink:href="options.html#opt-services.nbd.server.enable">services.nbd</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://timetagger.app">timetagger</link>,
+          an open source time-tracker with an intuitive user experience
+          and powerful reporting.
+          <link xlink:href="options.html#opt-services.timetagger.enable">services.timetagger</link>.
+        </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 xlink:href="options.html#opt-services.rstudio-server.enable">services.rstudio-server</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/juanfont/headscale">headscale</link>,
+          an Open Source implementation of the
+          <link xlink:href="https://tailscale.io">Tailscale</link>
+          Control Server. Available as
+          <link xlink:href="options.html#opt-services.headscale.enable">services.headscale</link>
+        </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.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://clusterlabs.org/pacemaker/">pacemaker</link>
+          cluster resource manager
+        </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 <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>
+          <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>
+          <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>
+          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>
+          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 = [
+      /run/keys/matrix-synapse/secrets.yaml
+    ];
+  };
+}
+</programlisting>
+        <para>
+          The secrets in your original config should be migrated into a
+          YAML file that is included via
+          <literal>extraConfigFiles</literal>.
+        </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>
+      </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>
+          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>autorestic</literal> package has been upgraded
+          from 1.3.0 to 1.5.0 which introduces breaking changes in
+          config file, check
+          <link xlink:href="https://autorestic.vercel.app/migration/1.4_1.5">their
+          migration guide</link> for more details.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          For <literal>pkgs.python3.pkgs.ipython</literal>, its direct
+          dependency
+          <literal>pkgs.python3.pkgs.matplotlib-inline</literal> (which
+          is really an adapter to integrate matplotlib in ipython if it
+          is installed) does not depend on
+          <literal>pkgs.python3.pkgs.matplotlib</literal> anymore. This
+          is closer to a non-Nix install of ipython. This has the added
+          benefit to reduce the closure size of
+          <literal>ipython</literal> from ~400MB to ~160MB (including
+          ~100MB for python itself).
+        </para>
+      </listitem>
+      <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>
+          <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>
+          MultiMC has been replaced with the fork PolyMC due to upstream
+          developers being hostile to 3rd party package maintainers.
+          PolyMC removes all MultiMC branding and is aimed at providing
+          proper 3rd party packages like the one contained in Nixpkgs.
+          This change affects the data folder where game instances and
+          other save and configuration files are stored. Users with
+          existing installations should rename
+          <literal>~/.local/share/multimc</literal> to
+          <literal>~/.local/share/polymc</literal>. The main config
+          file’s path has also moved from
+          <literal>~/.local/share/multimc/multimc.cfg</literal> to
+          <literal>~/.local/share/polymc/polymc.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>
+          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>
+          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>pkgs.pgadmin</literal> now refers to
+          <literal>pkgs.pgadmin4</literal>. If you still need pgadmin3,
+          use <literal>pkgs.pgadmin3</literal>.
+        </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>
+          <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>
+    </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>
+              but may be deprecated in the future.
+            </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>
+          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>
+          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>
+          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>services.unifi.openPorts</literal> option default
+          value of <literal>true</literal> is now deprecated and will be
+          changed to <literal>false</literal> in 22.11. Configurations
+          using this default will print a warning when rebuilt.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>security.acme</literal> certificates will now
+          correctly check for CA revokation before reaching their
+          minimum age.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Removing domains from
+          <literal>security.acme.certs._name_.extraDomainNames</literal>
+          will now correctly remove those domains during rebuild/renew.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          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
+          replace <literal>hostNames</literal>.
+          <literal>hostNames</literal> is deprecated, but still
+          available for now.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>services.stubby</literal> module was converted to
+          a
+          <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">settings-style</link>
+          configuration.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The option <literal>services.duplicati.dataDir</literal> has
+          been added to allow changing the location of duplicati’s
+          files.
+        </para>
+      </listitem>
+      <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.8.3-alpha.1.pre.0, which
+          introduces some breaking changes:
+        </para>
+        <itemizedlist spacing="compact">
+          <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>
+            </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>
+          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>
+          A new module was added for the
+          <link xlink:href="https://starship.rs/">Starship</link> shell
+          prompt, providing the options
+          <literal>programs.starship.enable</literal> and
+          <literal>programs.starship.settings</literal>.
+        </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>
+          <literal>services.logrotate.enable</literal> now defaults to
+          true if any rotate path has been defined, and some paths have
+          been added by default.
+        </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>
+          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>
+          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>
+          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>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>
+    </itemizedlist>
+  </section>
+</section>
diff --git a/nixos/doc/manual/installation/changing-config.chapter.md b/nixos/doc/manual/installation/changing-config.chapter.md
new file mode 100644
index 00000000000..8a404f085d7
--- /dev/null
+++ b/nixos/doc/manual/installation/changing-config.chapter.md
@@ -0,0 +1,100 @@
+# Changing the Configuration {#sec-changing-config}
+
+The file `/etc/nixos/configuration.nix` contains the current
+configuration of your machine. Whenever you've [changed
+something](#ch-configuration) in that file, you should do
+
+```ShellSession
+# nixos-rebuild switch
+```
+
+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).
+
+::: {.warning}
+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.
+:::
+
+::: {.warning}
+These commands must be executed as root, so you should either run them
+from a root shell or by prefixing them with `sudo -i`.
+:::
+
+You can also do
+
+```ShellSession
+# nixos-rebuild test
+```
+
+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.
+
+There is also
+
+```ShellSession
+# nixos-rebuild boot
+```
+
+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).
+
+You can make your configuration show up in a different submenu of the
+GRUB 2 boot screen by giving it a different *profile name*, e.g.
+
+```ShellSession
+# nixos-rebuild switch -p test
+```
+
+which causes the new configuration (and previous ones created using
+`-p test`) to show up in the GRUB submenu "NixOS - Profile \'test\'".
+This can be useful to separate test configurations from "stable"
+configurations.
+
+Finally, you can do
+
+```ShellSession
+$ nixos-rebuild build
+```
+
+to build the configuration but nothing more. This is useful to see
+whether everything compiles cleanly.
+
+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 *virtual machine* that contains the desired configuration. Just do
+
+```ShellSession
+$ nixos-rebuild build-vm
+$ ./result/bin/run-*-vm
+```
+
+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 `mutableUsers = false`. Another way is to temporarily add the
+following to your configuration:
+
+```nix
+users.users.your-user.initialHashedPassword = "test";
+```
+
+*Important:* 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):
+
+```ShellSession
+$ QEMU_NET_OPTS="hostfwd=tcp::2222-:22" ./result/bin/run-*-vm
+```
+
+allowing you to log in via SSH (assuming you have set the appropriate
+passwords or SSH authorized keys):
+
+```ShellSession
+$ ssh -p 2222 localhost
+```
diff --git a/nixos/doc/manual/installation/installation.xml b/nixos/doc/manual/installation/installation.xml
new file mode 100644
index 00000000000..1d443bbd0ee
--- /dev/null
+++ b/nixos/doc/manual/installation/installation.xml
@@ -0,0 +1,17 @@
+<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" />
+</part>
diff --git a/nixos/doc/manual/installation/installing-behind-a-proxy.section.md b/nixos/doc/manual/installation/installing-behind-a-proxy.section.md
new file mode 100644
index 00000000000..aca151531d0
--- /dev/null
+++ b/nixos/doc/manual/installation/installing-behind-a-proxy.section.md
@@ -0,0 +1,29 @@
+# Installing behind a proxy {#sec-installing-behind-proxy}
+
+To install NixOS behind a proxy, do the following before running
+`nixos-install`.
+
+1.  Update proxy configuration in `/mnt/etc/nixos/configuration.nix` to
+    keep the internet accessible after reboot.
+
+    ```nix
+    networking.proxy.default = "http://user:password@proxy:port/";
+    networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
+    ```
+
+1.  Setup the proxy environment variables in the shell where you are
+    running `nixos-install`.
+
+    ```ShellSession
+    # proxy_url="http://user:password@proxy:port/"
+    # export http_proxy="$proxy_url"
+    # export HTTP_PROXY="$proxy_url"
+    # export https_proxy="$proxy_url"
+    # export HTTPS_PROXY="$proxy_url"
+    ```
+
+::: {.note}
+If you are switching networks with different proxy configurations, use
+the `specialisation` option in `configuration.nix` to switch proxies at
+runtime. Refer to [](#ch-options) for more information.
+:::
diff --git a/nixos/doc/manual/installation/installing-from-other-distro.section.md b/nixos/doc/manual/installation/installing-from-other-distro.section.md
new file mode 100644
index 00000000000..d9060eb89c3
--- /dev/null
+++ b/nixos/doc/manual/installation/installing-from-other-distro.section.md
@@ -0,0 +1,279 @@
+# Installing from another Linux distribution {#sec-installing-from-other-distro}
+
+Because Nix (the package manager) & 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:
+
+1.  Install NixOS on another partition, from your existing Linux
+    distribution (without the use of a USB or optical device!)
+
+1.  Install NixOS on the same partition (in place!), from your existing
+    non-NixOS Linux distribution using `NIXOS_LUSTRATE`.
+
+1.  Install NixOS on your hard drive from the Live CD of any Linux
+    distribution.
+
+The first steps to all these are the same:
+
+1.  Install the Nix package manager:
+
+    Short version:
+
+    ```ShellSession
+    $ curl -L https://nixos.org/nix/install | sh
+    $ . $HOME/.nix-profile/etc/profile.d/nix.sh # …or open a fresh shell
+    ```
+
+    More details in the [ Nix
+    manual](https://nixos.org/nix/manual/#chap-quick-start)
+
+1.  Switch to the NixOS channel:
+
+    If you\'ve just installed Nix on a non-NixOS distribution, you will
+    be on the `nixpkgs` channel by default.
+
+    ```ShellSession
+    $ nix-channel --list
+    nixpkgs https://nixos.org/channels/nixpkgs-unstable
+    ```
+
+    As that channel gets released without running the NixOS tests, it
+    will be safer to use the `nixos-*` channels instead:
+
+    ```ShellSession
+    $ nix-channel --add https://nixos.org/channels/nixos-version nixpkgs
+    ```
+
+    You may want to throw in a `nix-channel --update` for good measure.
+
+1.  Install the NixOS installation tools:
+
+    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..
+
+    ```ShellSession
+    $ nix-env -f '<nixpkgs>' -iA nixos-install-tools
+    ```
+
+1.  ::: {.note}
+    The following 5 steps are only for installing NixOS to another
+    partition. For installing NixOS in place using `NIXOS_LUSTRATE`,
+    skip ahead.
+    :::
+
+    Prepare your target partition:
+
+    At this point it is time to prepare your target partition. Please
+    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`
+    there is nothing to do for this step.
+
+1.  Generate your NixOS configuration:
+
+    ```ShellSession
+    $ sudo `which nixos-generate-config` --root /mnt
+    ```
+
+    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
+    using GRUB and your existing distribution is running Ubuntu, you may
+    want to add something like this to your `configuration.nix`:
+
+    ```nix
+    boot.loader.grub.extraEntries = ''
+      menuentry "Ubuntu" {
+        search --set=ubuntu --fs-uuid 3cc3e652-0c1f-4800-8451-033754f68e6e
+        configfile "($ubuntu)/boot/grub/grub.cfg"
+      }
+    '';
+    ```
+
+    (You can find the appropriate UUID for your partition in
+    `/dev/disk/by-uuid`)
+
+1.  Create the `nixbld` group and user on your original distribution:
+
+    ```ShellSession
+    $ sudo groupadd -g 30000 nixbld
+    $ sudo useradd -u 30000 -g nixbld -G nixbld nixbld
+    ```
+
+1.  Download/build/install NixOS:
+
+    ::: {.warning}
+    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.
+    :::
+
+    ::: {.note}
+    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 `PATH="$PATH:/usr/sbin:/sbin"` in the following command.
+    :::
+
+    ```ShellSession
+    $ sudo PATH="$PATH" NIX_PATH="$NIX_PATH" `which nixos-install` --root /mnt
+    ```
+
+    Again, please refer to the `nixos-install` step in
+    [](#sec-installation) for more information.
+
+    That should be it for installation to another partition!
+
+1.  Optionally, you may want to clean up your non-NixOS distribution:
+
+    ```ShellSession
+    $ sudo userdel nixbld
+    $ sudo groupdel nixbld
+    ```
+
+    If you do not wish to keep the Nix package manager installed either,
+    run something like `sudo rm -rv ~/.nix-* /nix` and remove the line
+    that the Nix installer added to your `~/.profile`.
+
+1.  ::: {.note}
+    The following steps are only for installing NixOS in place using
+    `NIXOS_LUSTRATE`:
+    :::
+
+    Generate your NixOS configuration:
+
+    ```ShellSession
+    $ sudo `which nixos-generate-config` --root /
+    ```
+
+    Note that this will place the generated configuration files in
+    `/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
+    password until after you reboot. You can initalize 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
+    `sudo passwd -l root` if you use `sudo`)
+
+    ```nix
+    users.users.root.initialHashedPassword = "";
+    ```
+
+1.  Build the NixOS closure and install it in the `system` profile:
+
+    ```ShellSession
+    $ nix-env -p /nix/var/nix/profiles/system -f '<nixpkgs/nixos>' -I nixos-config=/etc/nixos/configuration.nix -iA system
+    ```
+
+1.  Change ownership of the `/nix` tree to root (since your Nix install
+    was probably single user):
+
+    ```ShellSession
+    $ sudo chown -R 0.0 /nix
+    ```
+
+1.  Set up the `/etc/NIXOS` and `/etc/NIXOS_LUSTRATE` files:
+
+    `/etc/NIXOS` officializes that this is now a NixOS partition (the
+    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
+    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:
+
+    -   The `/nix` directory
+
+    -   The `/boot` directory
+
+    -   Any file or directory listed in `/etc/NIXOS_LUSTRATE` (one per
+        line)
+
+    ::: {.note}
+    Support for `NIXOS_LUSTRATE` was added in NixOS 16.09. The act of
+    \"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.
+
+    lustrate /ˈlʌstreɪt/ verb.
+
+    purify by expiatory sacrifice, ceremonial washing, or some other
+    ritual action.
+    :::
+
+    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
+    reboot on NixOS:
+
+    ```ShellSession
+    $ echo etc/nixos | sudo tee -a /etc/NIXOS_LUSTRATE
+    ```
+
+1.  Finally, move the `/boot` 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:
+
+    ::: {.warning}
+    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.
+    :::
+
+    ```ShellSession
+    $ sudo mv -v /boot /boot.bak &&
+    sudo /nix/var/nix/profiles/system/bin/switch-to-configuration boot
+    ```
+
+    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
+    these lines:
+
+    ```ShellSession
+    # 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
+    ```
+
+    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
+    old distribution:
+
+    ```ShellSession
+    sudo rm -rf /old-root
+    ```
+
+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
+    lustrate process to convert Digital Ocean droplets to NixOS from
+    other distributions automatically.
diff --git a/nixos/doc/manual/installation/installing-pxe.section.md b/nixos/doc/manual/installation/installing-pxe.section.md
new file mode 100644
index 00000000000..4fbd6525f8c
--- /dev/null
+++ b/nixos/doc/manual/installation/installing-pxe.section.md
@@ -0,0 +1,32 @@
+# Booting from the "netboot" media (PXE) {#sec-booting-from-pxe}
+
+Advanced users may wish to install NixOS using an existing PXE or iPXE
+setup.
+
+These instructions assume that you have an existing PXE or iPXE
+infrastructure and simply want to add the NixOS installer as another
+option. To build the necessary files from your current version of nixpkgs,
+you can run:
+
+```ShellSession
+nix-build -A netboot.x86_64-linux '<nixpkgs/nixos/release.nix>'
+```
+
+This will create a `result` directory containing: \* `bzImage` -- the
+Linux kernel \* `initrd` -- the initrd file \* `netboot.ipxe` -- an
+example ipxe script demonstrating the appropriate kernel command line
+arguments for this image
+
+If you're using plain PXE, configure your boot loader to use the
+`bzImage` and `initrd` files and have it provide the same kernel command
+line arguments found in `netboot.ipxe`.
+
+If you're using iPXE, depending on how your HTTP/FTP/etc. server is
+configured you may be able to use `netboot.ipxe` unmodified, or you may
+need to update the paths to the files to match your server's directory
+layout.
+
+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.
diff --git a/nixos/doc/manual/installation/installing-usb.section.md b/nixos/doc/manual/installation/installing-usb.section.md
new file mode 100644
index 00000000000..ae58c08e523
--- /dev/null
+++ b/nixos/doc/manual/installation/installing-usb.section.md
@@ -0,0 +1,31 @@
+# Booting from a USB Drive {#sec-booting-from-usb}
+
+For systems without CD drive, the NixOS live CD can be booted from a USB
+stick. You can use the `dd` utility to write the image:
+`dd if=path-to-image of=/dev/sdX`. Be careful about specifying the correct
+drive; you can use the `lsblk` command to get a list of block devices.
+
+::: {.note}
+::: {.title}
+On macOS
+:::
+
+```ShellSession
+$ diskutil list
+[..]
+/dev/diskN (external, physical):
+   #:                       TYPE NAME                    SIZE       IDENTIFIER
+[..]
+$ diskutil unmountDisk diskN
+Unmount of all volumes on diskN was successful
+$ sudo dd if=nix.iso of=/dev/rdiskN
+```
+
+Using the \'raw\' `rdiskN` device instead of `diskN` completes in
+minutes instead of hours. After `dd` completes, a GUI dialog \"The disk
+you inserted was not readable by this computer\" will pop up, which can
+be ignored.
+:::
+
+The `dd` utility will write the image verbatim to the drive, making it
+the recommended option for both UEFI and non-UEFI installations.
diff --git a/nixos/doc/manual/installation/installing-virtualbox-guest.section.md b/nixos/doc/manual/installation/installing-virtualbox-guest.section.md
new file mode 100644
index 00000000000..e9c2a621c1b
--- /dev/null
+++ b/nixos/doc/manual/installation/installing-virtualbox-guest.section.md
@@ -0,0 +1,59 @@
+# Installing in a VirtualBox guest {#sec-instaling-virtualbox-guest}
+
+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 [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.  Base Memory Size: 768 MB or higher.
+
+1.  New Hard Disk of 8 GB or higher.
+
+1.  Mount the CD-ROM with the NixOS ISO (by clicking on CD/DVD-ROM)
+
+1.  Click on Settings / System / Processor and enable PAE/NX
+
+1.  Click on Settings / System / Acceleration and enable \"VT-x/AMD-V\"
+    acceleration
+
+1.  Click on Settings / Display / Screen and select VMSVGA as Graphics
+    Controller
+
+1.  Save the settings, start the virtual machine, and continue
+    installation like normal
+
+There are a few modifications you should make in configuration.nix.
+Enable booting:
+
+```nix
+boot.loader.grub.device = "/dev/sda";
+```
+
+Also remove the fsck that runs at startup. It will always fail to run,
+stopping your boot until you press `*`.
+
+```nix
+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
+`/etc/nixos/configuration.nix` to auto-mount them. If you do not add
+`"nofail"`, the system will not boot properly.
+
+```nix
+{ config, pkgs, ...} :
+{
+  fileSystems."/virtualboxshare" = {
+    fsType = "vboxsf";
+    device = "nameofthesharedfolder";
+    options = [ "rw" "nofail" ];
+  };
+}
+```
+
+The folder will be available directly under the root directory.
diff --git a/nixos/doc/manual/installation/installing.chapter.md b/nixos/doc/manual/installation/installing.chapter.md
new file mode 100644
index 00000000000..def4f37fbca
--- /dev/null
+++ b/nixos/doc/manual/installation/installing.chapter.md
@@ -0,0 +1,482 @@
+# Installing NixOS {#sec-installation}
+
+## Booting the system {#sec-installation-booting}
+
+NixOS can be installed on BIOS or UEFI systems. The procedure for a UEFI
+installation is by and large the same as a BIOS installation. The
+differences are mentioned in the steps that follow.
+
+The installation media can be burned to a CD, or now more commonly,
+"burned" to a USB drive (see [](#sec-booting-from-usb)).
+
+The installation media contains a basic NixOS installation. When it's
+finished booting, it should have detected most of your hardware.
+
+The NixOS manual is available by running `nixos-help`.
+
+You are logged-in automatically as `nixos`. The `nixos` user account has
+an empty password so you can use `sudo` without a password:
+```ShellSession
+$ sudo -i
+```
+
+If you downloaded the graphical ISO image, you can run `systemctl
+start display-manager` to start the desktop environment. If you want
+to continue on the terminal, you can use `loadkeys` to switch to your
+preferred keyboard layout. (We even provide neo2 via `loadkeys de
+neo`!)
+
+If the text is too small to be legible, try `setfont ter-v32n` to
+increase the font size.
+
+To install over a serial port connect with `115200n8` (e.g.
+`picocom -b 115200 /dev/ttyUSB0`). When the bootloader lists boot
+entries, select the serial console boot entry.
+
+### Networking in the installer {#sec-installation-booting-networking}
+
+The boot process should have brought up networking (check `ip
+a`). 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 `ifconfig`.
+
+On the graphical installer, you can configure the network, wifi
+included, through NetworkManager. Using the `nmtui` program, you can do
+so even in a non-graphical session. If you prefer to configure the
+network manually, disable NetworkManager with
+`systemctl stop NetworkManager`.
+
+On the minimal installer, NetworkManager is not available, so
+configuration must be perfomed manually. To configure the wifi, first
+start wpa_supplicant with `sudo systemctl start wpa_supplicant`, then
+run `wpa_cli`. For most home networks, you need to type in the following
+commands:
+
+```plain
+> add_network
+0
+> set_network 0 ssid "myhomenetwork"
+OK
+> set_network 0 psk "mypassword"
+OK
+> set_network 0 key_mgmt WPA-PSK
+OK
+> enable_network 0
+OK
+```
+
+For enterprise networks, for example *eduroam*, instead do:
+
+```plain
+> add_network
+0
+> set_network 0 ssid "eduroam"
+OK
+> set_network 0 identity "myname@example.com"
+OK
+> set_network 0 password "mypassword"
+OK
+> set_network 0 key_mgmt WPA-EAP
+OK
+> enable_network 0
+OK
+```
+
+When successfully connected, you should see a line such as this one
+
+```plain
+<3>CTRL-EVENT-CONNECTED - Connection to 32:85:ab:ef:24:5c completed [id=0 id_str=]
+```
+
+you can now leave `wpa_cli` by typing `quit`.
+
+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 `/home/nixos/.ssh/authorized_keys` or
+`/root/.ssh/authorized_keys` (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 `root` or `nixos` with `passwd` to be
+able to login.
+
+## Partitioning and formatting {#sec-installation-partitioning}
+
+The NixOS installer doesn't do any partitioning or formatting, so you
+need to do that yourself.
+
+The NixOS installer ships with multiple partitioning tools. The examples
+below use `parted`, but also provides `fdisk`, `gdisk`, `cfdisk`, and
+`cgdisk`.
+
+The recommended partition scheme differs depending if the computer uses
+*Legacy Boot* or *UEFI*.
+
+### UEFI (GPT) {#sec-installation-partitioning-UEFI}
+
+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
+update /etc/fstab.
+:::
+
+1.  Create a *GPT* partition table.
+
+    ```ShellSession
+    # parted /dev/sda -- mklabel gpt
+    ```
+
+2.  Add the *root* 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.
+
+    ```ShellSession
+    # parted /dev/sda -- mkpart primary 512MiB -8GiB
+    ```
+
+3.  Next, add a *swap* partition. The size required will vary according
+    to needs, here a 8GiB one is created.
+
+    ```ShellSession
+    # parted /dev/sda -- mkpart primary linux-swap -8GiB 100%
+    ```
+
+    ::: {.note}
+    The swap partition size rules are no different than for other Linux
+    distributions.
+    :::
+
+4.  Finally, the *boot* partition. NixOS by default uses the ESP (EFI
+    system partition) as its */boot* partition. It uses the initially
+    reserved 512MiB at the start of the disk.
+
+    ```ShellSession
+    # parted /dev/sda -- mkpart ESP fat32 1MiB 512MiB
+    # parted /dev/sda -- set 3 esp on
+    ```
+
+Once complete, you can follow with
+[](#sec-installation-partitioning-formatting).
+
+### Legacy Boot (MBR) {#sec-installation-partitioning-MBR}
+
+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
+update /etc/fstab.
+:::
+
+1.  Create a *MBR* partition table.
+
+    ```ShellSession
+    # parted /dev/sda -- mklabel msdos
+    ```
+
+2.  Add the *root* partition. This will fill the the disk except for the
+    end part, where the swap will live.
+
+    ```ShellSession
+    # parted /dev/sda -- mkpart primary 1MiB -8GiB
+    ```
+
+3.  Finally, add a *swap* partition. The size required will vary
+    according to needs, here a 8GiB one is created.
+
+    ```ShellSession
+    # parted /dev/sda -- mkpart primary linux-swap -8GiB 100%
+    ```
+
+    ::: {.note}
+    The swap partition size rules are no different than for other Linux
+    distributions.
+    :::
+
+Once complete, you can follow with
+[](#sec-installation-partitioning-formatting).
+
+### Formatting {#sec-installation-partitioning-formatting}
+
+Use the following commands:
+
+-   For initialising Ext4 partitions: `mkfs.ext4`. It is recommended
+    that you assign a unique symbolic label to the file system using the
+    option `-L label`, since this makes the file system configuration
+    independent from device changes. For example:
+
+    ```ShellSession
+    # mkfs.ext4 -L nixos /dev/sda1
+    ```
+
+-   For creating swap partitions: `mkswap`. Again it's recommended to
+    assign a label to the swap partition: `-L label`. For example:
+
+    ```ShellSession
+    # mkswap -L swap /dev/sda2
+    ```
+
+-   **UEFI systems**
+
+    For creating boot partitions: `mkfs.fat`. Again it's recommended
+    to assign a label to the boot partition: `-n label`. For
+    example:
+
+    ```ShellSession
+    # mkfs.fat -F 32 -n boot /dev/sda3
+    ```
+
+-   For creating LVM volumes, the LVM commands, e.g., `pvcreate`,
+    `vgcreate`, and `lvcreate`.
+
+-   For creating software RAID devices, use `mdadm`.
+
+## Installing {#sec-installation-installing}
+
+1.  Mount the target file system on which NixOS should be installed on
+    `/mnt`, e.g.
+
+    ```ShellSession
+    # mount /dev/disk/by-label/nixos /mnt
+    ```
+
+2.  **UEFI systems**
+
+    Mount the boot file system on `/mnt/boot`, e.g.
+
+    ```ShellSession
+    # mkdir -p /mnt/boot
+    # mount /dev/disk/by-label/boot /mnt/boot
+    ```
+
+3.  If your machine has a limited amount of memory, you may want to
+    activate swap devices now (`swapon device`).
+    The installer (or rather, the build actions that it
+    may spawn) may need quite a bit of RAM, depending on your
+    configuration.
+
+    ```ShellSession
+    # swapon /dev/sda2
+    ```
+
+4.  You now need to create a file `/mnt/etc/nixos/configuration.nix`
+    that specifies the intended configuration of the system. This is
+    because NixOS has a *declarative* 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 [](#sec-configuration-syntax),
+    while a list of available configuration options appears in
+    [](#ch-options). A minimal example is shown in
+    [Example: NixOS Configuration](#ex-config).
+
+    The command `nixos-generate-config` can generate an initial
+    configuration file for you:
+
+    ```ShellSession
+    # nixos-generate-config --root /mnt
+    ```
+
+    You should then edit `/mnt/etc/nixos/configuration.nix` to suit your
+    needs:
+
+    ```ShellSession
+    # nano /mnt/etc/nixos/configuration.nix
+    ```
+
+    If you're using the graphical ISO image, other editors may be
+    available (such as `vim`). If you have network access, you can also
+    install other editors -- for instance, you can install Emacs by
+    running `nix-env -f '<nixpkgs>' -iA emacs`.
+
+    BIOS systems
+
+    :   You *must* set the option [](#opt-boot.loader.grub.device) to
+        specify on which disk the GRUB boot loader is to be installed.
+        Without it, NixOS cannot boot.
+
+    UEFI systems
+
+    :   You *must* 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.
+
+        You may want to look at the options starting with
+        [`boot.loader.efi`](#opt-boot.loader.efi.canTouchEfiVariables) and
+        [`boot.loader.systemd-boot`](#opt-boot.loader.systemd-boot.enable)
+        as well.
+
+    If there are other operating systems running on the machine before
+    installing NixOS, the [](#opt-boot.loader.grub.useOSProber)
+    option can be set to `true` to automatically add them to the grub
+    menu.
+
+    If you need to configure networking for your machine the
+    configuration options are described in [](#sec-networking). In
+    particular, while wifi is supported on the installation image, it is
+    not enabled by default in the configuration generated by
+    `nixos-generate-config`.
+
+    Another critical option is `fileSystems`, specifying the file
+    systems that need to be mounted by NixOS. However, you typically
+    don't need to set it yourself, because `nixos-generate-config` sets
+    it automatically in `/mnt/etc/nixos/hardware-configuration.nix` from
+    your currently mounted file systems. (The configuration file
+    `hardware-configuration.nix` is included from `configuration.nix`
+    and will be overwritten by future invocations of
+    `nixos-generate-config`; thus, you generally should not modify it.)
+    Additionally, you may want to look at [Hardware configuration for
+    known-hardware](https://github.com/NixOS/nixos-hardware) at this
+    point or after installation.
+
+    ::: {.note}
+    Depending on your hardware configuration or type of file system, you
+    may need to set the option `boot.initrd.kernelModules` 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 `/mnt`, fix `/mnt/etc/nixos/configuration.nix`
+    and rerun `nixos-install`.) In most cases, `nixos-generate-config`
+    will figure out the required modules.
+    :::
+
+5.  Do the installation:
+
+    ```ShellSession
+    # nixos-install
+    ```
+
+    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 `nixos-install` after
+    fixing your `configuration.nix`.
+
+    As the last step, `nixos-install` will ask you to set the password
+    for the `root` user, e.g.
+
+    ```plain
+    setting root password...
+    New password: ***
+    Retype new password: ***
+    ```
+
+    ::: {.note}
+    For unattended installations, it is possible to use
+    `nixos-install --no-root-passwd` in order to disable the password
+    prompt entirely.
+    :::
+
+6.  If everything went well:
+
+    ```ShellSession
+    # reboot
+    ```
+
+7.  You should now be able to boot into the installed NixOS. The GRUB
+    boot menu shows a list of *available configurations* (initially just
+    one). Every time you change the NixOS configuration (see [Changing
+    Configuration](#sec-changing-config)), a new item is added to the
+    menu. This allows you to easily roll back to a previous
+    configuration if something goes wrong.
+
+    You should log in and change the `root` password with `passwd`.
+
+    You'll probably want to create some user accounts as well, which can
+    be done with `useradd`:
+
+    ```ShellSession
+    $ useradd -c 'Eelco Dolstra' -m eelco
+    $ passwd eelco
+    ```
+
+    You may also want to install some software. This will be covered in
+    [](#sec-package-management).
+
+## Installation summary {#sec-installation-summary}
+
+To summarise, [Example: Commands for Installing NixOS on `/dev/sda`](#ex-install-sequence)
+shows a typical sequence of commands for installing NixOS on an empty hard
+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)**
+:::
+```ShellSession
+# parted /dev/sda -- mklabel msdos
+# parted /dev/sda -- mkpart primary 1MiB -8GiB
+# parted /dev/sda -- mkpart primary linux-swap -8GiB 100%
+```
+:::
+
+::: {#ex-partition-scheme-UEFI .example}
+::: {.title}
+**Example: Example partition schemes for NixOS on `/dev/sda` (UEFI)**
+:::
+```ShellSession
+# parted /dev/sda -- mklabel gpt
+# parted /dev/sda -- mkpart primary 512MiB -8GiB
+# parted /dev/sda -- mkpart primary linux-swap -8GiB 100%
+# parted /dev/sda -- mkpart ESP fat32 1MiB 512MiB
+# parted /dev/sda -- set 3 esp on
+```
+:::
+
+::: {#ex-install-sequence .example}
+::: {.title}
+**Example: Commands for Installing NixOS on `/dev/sda`**
+:::
+With a partitioned disk.
+
+```ShellSession
+# 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
+```
+:::
+
+::: {#ex-config .example}
+::: {.title}
+**Example: NixOS Configuration**
+:::
+```ShellSession
+{ config, pkgs, ... }: {
+  imports = [
+    # Include the results of the hardware scan.
+    ./hardware-configuration.nix
+  ];
+
+  boot.loader.grub.device = "/dev/sda";   # (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."/".device = "/dev/disk/by-label/nixos";
+
+  # Enable the OpenSSH server.
+  services.sshd.enable = true;
+}
+```
+:::
+
+## 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-virtualbox-guest.section.xml" />
+<xi:include href="installing-from-other-distro.section.xml" />
+<xi:include href="installing-behind-a-proxy.section.xml" />
+```
diff --git a/nixos/doc/manual/installation/obtaining.chapter.md b/nixos/doc/manual/installation/obtaining.chapter.md
new file mode 100644
index 00000000000..832ec6146a9
--- /dev/null
+++ b/nixos/doc/manual/installation/obtaining.chapter.md
@@ -0,0 +1,26 @@
+# Obtaining NixOS {#sec-obtaining}
+
+NixOS ISO images can be downloaded from the [NixOS download
+page](https://nixos.org/nixos/download.html). There are a number of
+installation options. If you happen to have an optical drive and a spare
+CD, burning the image to CD and booting from that is probably the
+easiest option. Most people will need to prepare a USB stick to boot
+from. [](#sec-booting-from-usb) describes the preferred method to
+prepare a USB stick. A number of alternative methods are presented in
+the [NixOS Wiki](https://nixos.wiki/wiki/NixOS_Installation_Guide#Making_the_installation_media).
+
+As an alternative to installing NixOS yourself, you can get a running
+NixOS system through several other means:
+
+-   Using virtual appliances in Open Virtualization Format (OVF) that
+    can be imported into VirtualBox. These are available from the [NixOS
+    download page](https://nixos.org/nixos/download.html).
+
+-   Using AMIs for Amazon's EC2. To find one for your region and
+    instance type, please refer to the [list of most recent
+    AMIs](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/ec2-amis.nix).
+
+-   Using NixOps, the NixOS-based cloud deployment tool, which allows
+    you to provision VirtualBox and EC2 NixOS instances from declarative
+    specifications. Check out the [NixOps
+    homepage](https://nixos.org/nixops) for details.
diff --git a/nixos/doc/manual/installation/upgrading.chapter.md b/nixos/doc/manual/installation/upgrading.chapter.md
new file mode 100644
index 00000000000..faeefc4451d
--- /dev/null
+++ b/nixos/doc/manual/installation/upgrading.chapter.md
@@ -0,0 +1,118 @@
+# Upgrading NixOS {#sec-upgrading}
+
+The best way to keep your NixOS installation up to date is to use one of
+the NixOS *channels*. 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:
+
+-   *Stable channels*, such as [`nixos-21.11`](https://nixos.org/channels/nixos-21.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
+    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.
+
+-   The *unstable channel*, [`nixos-unstable`](https://nixos.org/channels/nixos-unstable).
+    This corresponds to NixOS's main development branch, and may thus see
+    radical changes between channel updates. It's not recommended for
+    production systems.
+
+-   *Small channels*, such as [`nixos-21.11-small`](https://nixos.org/channels/nixos-21.11-small)
+    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
+    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.
+
+To see what channels are available, go to <https://nixos.org/channels>.
+(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 <https://nixos.org/nixos/download.html> to find the
+newest supported stable release.
+
+When you first install NixOS, you're automatically subscribed to the
+NixOS channel that corresponds to your installation source. For
+instance, if you installed from a 21.11 ISO, you will be subscribed to
+the `nixos-21.11` channel. To see which NixOS channel you're subscribed
+to, run the following as root:
+
+```ShellSession
+# nix-channel --list | grep nixos
+nixos https://nixos.org/channels/nixos-unstable
+```
+
+To switch to a different NixOS channel, do
+
+```ShellSession
+# nix-channel --add https://nixos.org/channels/channel-name nixos
+```
+
+(Be sure to include the `nixos` parameter at the end.) For instance, to
+use the NixOS 21.11 stable channel:
+
+```ShellSession
+# nix-channel --add https://nixos.org/channels/nixos-21.11 nixos
+```
+
+If you have a server, you may want to use the "small" channel instead:
+
+```ShellSession
+# nix-channel --add https://nixos.org/channels/nixos-21.11-small nixos
+```
+
+And if you want to live on the bleeding edge:
+
+```ShellSession
+# nix-channel --add https://nixos.org/channels/nixos-unstable nixos
+```
+
+You can then upgrade NixOS to the latest version in your chosen channel
+by running
+
+```ShellSession
+# nixos-rebuild switch --upgrade
+```
+
+which is equivalent to the more verbose `nix-channel --update nixos; nixos-rebuild switch`.
+
+::: {.note}
+Channels are set per user. This means that running `nix-channel --add`
+as a non root user (or without sudo) will not affect
+configuration in `/etc/nixos/configuration.nix`
+:::
+
+::: {.warning}
+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.
+:::
+
+## Automatic Upgrades {#sec-upgrading-automatic}
+
+You can keep a NixOS system up-to-date automatically by adding the
+following to `configuration.nix`:
+
+```nix
+system.autoUpgrade.enable = true;
+system.autoUpgrade.allowReboot = true;
+```
+
+This enables a periodically executed systemd service named
+`nixos-upgrade.service`. If the `allowReboot` option is `false`, it runs
+`nixos-rebuild switch --upgrade` to upgrade NixOS to the latest version
+in the current channel. (To see when the service runs, see `systemctl list-timers`.)
+If `allowReboot` is `true`, 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.
+
+```nix
+system.autoUpgrade.channel = https://nixos.org/channels/nixos-21.11;
+```
diff --git a/nixos/doc/manual/man-configuration.xml b/nixos/doc/manual/man-configuration.xml
new file mode 100644
index 00000000000..ddb1408fdcf
--- /dev/null
+++ b/nixos/doc/manual/man-configuration.xml
@@ -0,0 +1,31 @@
+<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
new file mode 100644
index 00000000000..fa7c8c0c6d7
--- /dev/null
+++ b/nixos/doc/manual/man-nixos-build-vms.xml
@@ -0,0 +1,138 @@
+<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
new file mode 100644
index 00000000000..41f0e6b9751
--- /dev/null
+++ b/nixos/doc/manual/man-nixos-enter.xml
@@ -0,0 +1,154 @@
+<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
new file mode 100644
index 00000000000..9ac3b918ff6
--- /dev/null
+++ b/nixos/doc/manual/man-nixos-generate-config.xml
@@ -0,0 +1,214 @@
+<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
new file mode 100644
index 00000000000..eb6680b6567
--- /dev/null
+++ b/nixos/doc/manual/man-nixos-install.xml
@@ -0,0 +1,357 @@
+<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
new file mode 100644
index 00000000000..b921386d0df
--- /dev/null
+++ b/nixos/doc/manual/man-nixos-option.xml
@@ -0,0 +1,134 @@
+<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
new file mode 100644
index 00000000000..ab2a5d83a08
--- /dev/null
+++ b/nixos/doc/manual/man-nixos-rebuild.xml
@@ -0,0 +1,690 @@
+<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>--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>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>
+    </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>
+
+  </variablelist>
+
+  <para>
+   In addition, <command>nixos-rebuild</command> accepts various Nix-related
+   flags, including <option>--max-jobs</option> / <option>-j</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_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
new file mode 100644
index 00000000000..fae25721e39
--- /dev/null
+++ b/nixos/doc/manual/man-nixos-version.xml
@@ -0,0 +1,137 @@
+<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
new file mode 100644
index 00000000000..49acfe7330b
--- /dev/null
+++ b/nixos/doc/manual/man-pages.xml
@@ -0,0 +1,20 @@
+<reference xmlns="http://docbook.org/ns/docbook"
+           xmlns:xlink="http://www.w3.org/1999/xlink"
+           xmlns:xi="http://www.w3.org/2001/XInclude">
+ <title>NixOS Reference Pages</title>
+ <info>
+  <author><personname><firstname>Eelco</firstname><surname>Dolstra</surname></personname>
+   <contrib>Author</contrib>
+  </author>
+  <copyright><year>2007-2020</year><holder>Eelco Dolstra</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" />
+</reference>
diff --git a/nixos/doc/manual/manual.xml b/nixos/doc/manual/manual.xml
new file mode 100644
index 00000000000..158b3507a58
--- /dev/null
+++ b/nixos/doc/manual/manual.xml
@@ -0,0 +1,24 @@
+<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
new file mode 100755
index 00000000000..e0274f5619c
--- /dev/null
+++ b/nixos/doc/manual/md-to-db.sh
@@ -0,0 +1,49 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -I nixpkgs=channel:nixpkgs-unstable -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/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/preface.xml b/nixos/doc/manual/preface.xml
new file mode 100644
index 00000000000..c0d530c3d1b
--- /dev/null
+++ b/nixos/doc/manual/preface.xml
@@ -0,0 +1,42 @@
+<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.xml b/nixos/doc/manual/release-notes/release-notes.xml
new file mode 100644
index 00000000000..216fea67775
--- /dev/null
+++ b/nixos/doc/manual/release-notes/release-notes.xml
@@ -0,0 +1,28 @@
+<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-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-1310.section.md b/nixos/doc/manual/release-notes/rl-1310.section.md
new file mode 100644
index 00000000000..9efd8f6e8a1
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-1310.section.md
@@ -0,0 +1,3 @@
+# Release 13.10 ("Aardvark", 2013/10/31) {#sec-release-13.10}
+
+This is the first stable release branch of NixOS.
diff --git a/nixos/doc/manual/release-notes/rl-1404.section.md b/nixos/doc/manual/release-notes/rl-1404.section.md
new file mode 100644
index 00000000000..e0a70df3a63
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-1404.section.md
@@ -0,0 +1,81 @@
+# Release 14.04 ("Baboon", 2014/04/30) {#sec-release-14.04}
+
+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:
+
+- Installation on UEFI systems is now supported. See [](#sec-installation) for details.
+
+- Systemd has been updated to version 212, which has [numerous improvements](http://cgit.freedesktop.org/systemd/systemd/plain/NEWS?id=v212). NixOS now automatically starts systemd user instances when you log in. You can define global user units through the `systemd.unit.*` options.
+
+- NixOS is now based on Glibc 2.19 and GCC 4.8.
+
+- The default Linux kernel has been updated to 3.12.
+
+- KDE has been updated to 4.12.
+
+- GNOME 3.10 experimental support has been added.
+
+- Nix has been updated to 1.7 ([details](https://nixos.org/nix/manual/#ssec-relnotes-1.7)).
+
+- NixOS now supports fully declarative management of users and groups. If you set `users.mutableUsers` to `false`, then the contents of `/etc/passwd` and `/etc/group` will be [congruent](https://www.usenix.org/legacy/event/lisa02/tech/full_papers/traugott/traugott_html/) to your NixOS configuration. For instance, if you remove a user from `users.extraUsers` 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. If `users.mutableUsers` is `true` (the default), then behaviour is unchanged from NixOS 13.10.
+
+- 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 [](#ch-containers) for details.
+
+- Systemd units provided by packages can now be overridden from the NixOS configuration. For instance, if a package `foo` provides systemd units, you can say:
+
+  ```nix
+  {
+    systemd.packages = [ pkgs.foo ];
+  }
+  ```
+
+  to enable those units. You can then set or override unit options in the usual way, e.g.
+
+  ```nix
+  {
+    systemd.services.foo.wantedBy = [ "multi-user.target" ];
+    systemd.services.foo.serviceConfig.MemoryLimit = "512M";
+  }
+  ```
+
+  When upgrading from a previous release, please be aware of the following incompatible changes:
+
+- 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:
+
+  ```nix
+  {
+    nixpkgs.config.allowUnfree = true;
+  }
+  ```
+
+  Otherwise, you get an error message such as:
+
+  ```ShellSession
+      error: package ‘nvidia-x11-331.49-3.12.17’ in ‘…/nvidia-x11/default.nix:56’
+        has an unfree license, refusing to evaluate
+  ```
+
+- The Adobe Flash player is no longer enabled by default in the Firefox and Chromium wrappers. To enable it, you must set:
+
+  ```nix
+  {
+    nixpkgs.config.allowUnfree = true;
+    nixpkgs.config.firefox.enableAdobeFlash = true; # for Firefox
+    nixpkgs.config.chromium.enableAdobeFlash = true; # for Chromium
+  }
+  ```
+
+- The firewall is now enabled by default. If you don't want this, you need to disable it explicitly:
+
+  ```nix
+  {
+    networking.firewall.enable = false;
+  }
+  ```
+
+- The option `boot.loader.grub.memtest86` has been renamed to `boot.loader.grub.memtest86.enable`.
+
+- The `mysql55` service has been merged into the `mysql` service, which no longer sets a default for the option `services.mysql.package`.
+
+- Package variants are now differentiated by suffixing the name, rather than the version. For instance, `sqlite-3.8.4.3-interactive` is now called `sqlite-interactive-3.8.4.3`. This ensures that `nix-env -i sqlite` is unambiguous, and that `nix-env -u` won't "upgrade" `sqlite` to `sqlite-interactive` or vice versa. Notably, this change affects the Firefox wrapper (which provides plugins), as it is now called `firefox-wrapper`. So when using `nix-env`, you should do `nix-env -e firefox; nix-env -i firefox-wrapper` if you want to keep using the wrapper. This change does not affect declarative package management, since attribute names like `pkgs.firefoxWrapper` were already unambiguous.
+
+- The symlink `/etc/ca-bundle.crt` is gone. Programs should instead use the environment variable `OPENSSL_X509_CERT_FILE` (which points to `/etc/ssl/certs/ca-bundle.crt`).
diff --git a/nixos/doc/manual/release-notes/rl-1412.section.md b/nixos/doc/manual/release-notes/rl-1412.section.md
new file mode 100644
index 00000000000..683f1e45f09
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-1412.section.md
@@ -0,0 +1,171 @@
+# Release 14.12 ("Caterpillar", 2014/12/30) {#sec-release-14.12}
+
+In addition to numerous new and upgraded packages, this release has the following highlights:
+
+- Systemd has been updated to version 217, which has numerous [improvements.](http://lists.freedesktop.org/archives/systemd-devel/2014-October/024662.html)
+
+- [Nix has been updated to 1.8.](https://www.mail-archive.com/nix-dev@lists.science.uu.nl/msg13957.html)
+
+- NixOS is now based on Glibc 2.20.
+
+- KDE has been updated to 4.14.
+
+- The default Linux kernel has been updated to 3.14.
+
+- If `users.mutableUsers` is enabled (the default), changes made to the declaration of a user or group will be correctly realised when running `nixos-rebuild`. For instance, removing a user specification from `configuration.nix` will cause the actual user account to be deleted. If `users.mutableUsers` is disabled, it is no longer necessary to specify UIDs or GIDs; if omitted, they are allocated dynamically.
+
+Following new services were added since the last release:
+
+- `atftpd`
+
+- `bosun`
+
+- `bspwm`
+
+- `chronos`
+
+- `collectd`
+
+- `consul`
+
+- `cpuminer-cryptonight`
+
+- `crashplan`
+
+- `dnscrypt-proxy`
+
+- `docker-registry`
+
+- `docker`
+
+- `etcd`
+
+- `fail2ban`
+
+- `fcgiwrap`
+
+- `fleet`
+
+- `fluxbox`
+
+- `gdm`
+
+- `geoclue2`
+
+- `gitlab`
+
+- `gitolite`
+
+- `gnome3.gnome-documents`
+
+- `gnome3.gnome-online-miners`
+
+- `gnome3.gvfs`
+
+- `gnome3.seahorse`
+
+- `hbase`
+
+- `i2pd`
+
+- `influxdb`
+
+- `kubernetes`
+
+- `liquidsoap`
+
+- `lxc`
+
+- `mailpile`
+
+- `mesos`
+
+- `mlmmj`
+
+- `monetdb`
+
+- `mopidy`
+
+- `neo4j`
+
+- `nsd`
+
+- `openntpd`
+
+- `opentsdb`
+
+- `openvswitch`
+
+- `parallels-guest`
+
+- `peerflix`
+
+- `phd`
+
+- `polipo`
+
+- `prosody`
+
+- `radicale`
+
+- `redmine`
+
+- `riemann`
+
+- `scollector`
+
+- `seeks`
+
+- `siproxd`
+
+- `strongswan`
+
+- `tcsd`
+
+- `teamspeak3`
+
+- `thermald`
+
+- `torque/mrom`
+
+- `torque/server`
+
+- `uhub`
+
+- `unifi`
+
+- `znc`
+
+- `zookeeper`
+
+When upgrading from a previous release, please be aware of the following incompatible changes:
+
+- The default version of Apache httpd is now 2.4. If you use the `extraConfig` option to pass literal Apache configuration text, you may need to update it --- see [Apache's documentation](http://httpd.apache.org/docs/2.4/upgrading.html) for details. If you wish to continue to use httpd 2.2, add the following line to your NixOS configuration:
+
+  ```nix
+  {
+    services.httpd.package = pkgs.apacheHttpd_2_2;
+  }
+  ```
+
+- PHP 5.3 has been removed because it is no longer supported by the PHP project. A [migration guide](http://php.net/migration54) is available.
+
+- The host side of a container virtual Ethernet pair is now called `ve-container-name` rather than `c-container-name`.
+
+- GNOME 3.10 support has been dropped. The default GNOME version is now 3.12.
+
+- VirtualBox has been upgraded to 4.3.20 release. Users may be required to run `rm -rf /tmp/.vbox*`. The line `imports = [ <nixpkgs/nixos/modules/programs/virtualbox.nix> ]` is no longer necessary, use `services.virtualboxHost.enable = true` instead.
+
+  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 `vboxusers` group.
+
+- Chromium has been updated to 39.0.2171.65. `enablePepperPDF` is now enabled by default. `chromium*Wrapper` packages no longer exist, because upstream removed NSAPI support. `chromium-stable` has been renamed to `chromium`.
+
+- Python packaging documentation is now part of nixpkgs manual. To override the python packages available to a custom python you now use `pkgs.pythonFull.buildEnv.override` instead of `pkgs.pythonFull.override`.
+
+- `boot.resumeDevice = "8:6"` 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.
+
+- The system-wide default timezone for NixOS installations changed from `CET` to `UTC`. To choose a different timezone for your system, configure `time.timeZone` in `configuration.nix`. A fairly complete list of possible values for that setting is available at <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones>.
+
+- GNU screen has been updated to 4.2.1, which breaks the ability to connect to sessions created by older versions of screen.
+
+- The Intel GPU driver was updated to the 3.x prerelease version (used by most distributions) and supports DRI3 now.
diff --git a/nixos/doc/manual/release-notes/rl-1509.section.md b/nixos/doc/manual/release-notes/rl-1509.section.md
new file mode 100644
index 00000000000..55804ddb988
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-1509.section.md
@@ -0,0 +1,319 @@
+# Release 15.09 ("Dingo", 2015/09/30) {#sec-release-15.09}
+
+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.
+
+- Nix has been updated to version 1.10, which among other improvements enables cryptographic signatures on binary caches for improved security.
+
+- You can now keep your NixOS system up to date automatically by setting
+
+```nix
+{
+  system.autoUpgrade.enable = true;
+}
+```
+
+This will cause the system to periodically check for updates in your current channel and run `nixos-rebuild`.
+
+- This release is based on Glibc 2.21, GCC 4.9 and Linux 3.18.
+
+- GNOME has been upgraded to 3.16.
+
+- Xfce has been upgraded to 4.12.
+
+- 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.
+
+- E19 has been upgraded to 0.16.8.15.
+
+The following new services were added since the last release:
+
+- `services/mail/exim.nix`
+
+- `services/misc/apache-kafka.nix`
+
+- `services/misc/canto-daemon.nix`
+
+- `services/misc/confd.nix`
+
+- `services/misc/devmon.nix`
+
+- `services/misc/gitit.nix`
+
+- `services/misc/ihaskell.nix`
+
+- `services/misc/mbpfan.nix`
+
+- `services/misc/mediatomb.nix`
+
+- `services/misc/mwlib.nix`
+
+- `services/misc/parsoid.nix`
+
+- `services/misc/plex.nix`
+
+- `services/misc/ripple-rest.nix`
+
+- `services/misc/ripple-data-api.nix`
+
+- `services/misc/subsonic.nix`
+
+- `services/misc/sundtek.nix`
+
+- `services/monitoring/cadvisor.nix`
+
+- `services/monitoring/das_watchdog.nix`
+
+- `services/monitoring/grafana.nix`
+
+- `services/monitoring/riemann-tools.nix`
+
+- `services/monitoring/teamviewer.nix`
+
+- `services/network-filesystems/u9fs.nix`
+
+- `services/networking/aiccu.nix`
+
+- `services/networking/asterisk.nix`
+
+- `services/networking/bird.nix`
+
+- `services/networking/charybdis.nix`
+
+- `services/networking/docker-registry-server.nix`
+
+- `services/networking/fan.nix`
+
+- `services/networking/firefox/sync-server.nix`
+
+- `services/networking/gateone.nix`
+
+- `services/networking/heyefi.nix`
+
+- `services/networking/i2p.nix`
+
+- `services/networking/lambdabot.nix`
+
+- `services/networking/mstpd.nix`
+
+- `services/networking/nix-serve.nix`
+
+- `services/networking/nylon.nix`
+
+- `services/networking/racoon.nix`
+
+- `services/networking/skydns.nix`
+
+- `services/networking/shout.nix`
+
+- `services/networking/softether.nix`
+
+- `services/networking/sslh.nix`
+
+- `services/networking/tinc.nix`
+
+- `services/networking/tlsdated.nix`
+
+- `services/networking/tox-bootstrapd.nix`
+
+- `services/networking/tvheadend.nix`
+
+- `services/networking/zerotierone.nix`
+
+- `services/scheduling/marathon.nix`
+
+- `services/security/fprintd.nix`
+
+- `services/security/hologram.nix`
+
+- `services/security/munge.nix`
+
+- `services/system/cloud-init.nix`
+
+- `services/web-servers/shellinabox.nix`
+
+- `services/web-servers/uwsgi.nix`
+
+- `services/x11/unclutter.nix`
+
+- `services/x11/display-managers/sddm.nix`
+
+- `system/boot/coredump.nix`
+
+- `system/boot/loader/loader.nix`
+
+- `system/boot/loader/generic-extlinux-compatible`
+
+- `system/boot/networkd.nix`
+
+- `system/boot/resolved.nix`
+
+- `system/boot/timesyncd.nix`
+
+- `tasks/filesystems/exfat.nix`
+
+- `tasks/filesystems/ntfs.nix`
+
+- `tasks/filesystems/vboxsf.nix`
+
+- `virtualisation/virtualbox-host.nix`
+
+- `virtualisation/vmware-guest.nix`
+
+- `virtualisation/xen-dom0.nix`
+
+When upgrading from a previous release, please be aware of the following incompatible changes:
+
+- `sshd` 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
+
+```nix
+{
+  system.stateVersion = "14.12";
+}
+```
+
+The new option `system.stateVersion` ensures that certain configuration changes that could break existing systems (such as the `sshd` host key setting) will maintain compatibility with the specified NixOS release. NixOps sets the state version of existing deployments automatically.
+
+- `cron` is no longer enabled by default, unless you have a non-empty `services.cron.systemCronJobs`. To force `cron` to be enabled, set `services.cron.enable = true`.
+
+- 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`.
+
+- CMPlayer has been renamed to bomi upstream. Package `cmplayer` was accordingly renamed to `bomi`
+
+- Atom Shell has been renamed to Electron upstream. Package `atom-shell` was accordingly renamed to `electron`
+
+- Elm is not released on Hackage anymore. You should now use `elmPackages.elm` which contains the latest Elm platform.
+
+- The CUPS printing service has been updated to version `2.0.2`. Furthermore its systemd service has been renamed to `cups.service`.
+
+  Local printers are no longer shared or advertised by default. This behavior can be changed by enabling `services.printing.defaultShared` or `services.printing.browsing` respectively.
+
+- The VirtualBox host and guest options have been named more consistently. They can now found in `virtualisation.virtualbox.host.*` instead of `services.virtualboxHost.*` and `virtualisation.virtualbox.guest.*` instead of `services.virtualboxGuest.*`.
+
+  Also, there now is support for the `vboxsf` file system using the `fileSystems` configuration attribute. An example of how this can be used in a configuration:
+
+```nix
+{
+  fileSystems."/shiny" = {
+    device = "myshinysharedfolder";
+    fsType = "vboxsf";
+  };
+}
+```
+
+- \"`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
+```
+
+Executable programs written in Haskell can be installed with:
+
+```ShellSession
+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.
+
+- 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 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.
+
+- The `phpfpm` is now using the default PHP version (`pkgs.php`) instead of PHP 5.4 (`pkgs.php54`).
+
+- The `locate` 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 `services.locate.includeStore = true`.
+
+- The Nix expression search path (`NIX_PATH`) no longer contains `/etc/nixos/nixpkgs` by default. You can override `NIX_PATH` by setting `nix.nixPath`.
+
+- Python 2.6 has been marked as broken (as it no longer receives security updates from upstream).
+
+- Any use of module arguments such as `pkgs` to access library functions, or to define `imports` attributes will now lead to an infinite loop at the time of the evaluation.
+
+  In case of an infinite loop, use the `--show-trace` command line argument and read the line just above the error message.
+
+  ```ShellSession
+  $ nixos-rebuild build --show-trace
+  …
+  while evaluating the module argument `pkgs' in "/etc/nixos/my-module.nix":
+  infinite recursion encountered
+  ```
+
+  Any use of `pkgs.lib`, should be replaced by `lib`, after adding it as argument of the module. The following module
+
+  ```nix
+  { config, pkgs, ... }:
+
+  with pkgs.lib;
+
+  {
+    options = {
+      foo = mkOption { … };
+    };
+    config = mkIf config.foo { … };
+  }
+  ```
+
+  should be modified to look like:
+
+  ```nix
+  { config, pkgs, lib, ... }:
+
+  with lib;
+
+  {
+    options = {
+      foo = mkOption { option declaration };
+    };
+    config = mkIf config.foo { option definition };
+  }
+  ```
+
+  When `pkgs` is used to download other projects to import their modules, and only in such cases, it should be replaced by `(import <nixpkgs> {})`. The following module
+
+  ```nix
+  { config, pkgs, ... }:
+
+  let
+    myProject = pkgs.fetchurl {
+      src = url;
+      sha256 = hash;
+    };
+  in
+
+  {
+    imports = [ "${myProject}/module.nix" ];
+  }
+  ```
+
+  should be modified to look like:
+
+  ```nix
+  { config, pkgs, ... }:
+
+  let
+    myProject = (import <nixpkgs> {}).fetchurl {
+      src = url;
+      sha256 = hash;
+    };
+  in
+
+  {
+    imports = [ "${myProject}/module.nix" ];
+  }
+  ```
+
+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.
+
+- 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.
+
+- `buildEnv.env` on all Python interpreters is now available for nix-shell interoperability.
diff --git a/nixos/doc/manual/release-notes/rl-1603.section.md b/nixos/doc/manual/release-notes/rl-1603.section.md
new file mode 100644
index 00000000000..dce879ec16d
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-1603.section.md
@@ -0,0 +1,282 @@
+# Release 16.03 ("Emu", 2016/03/31) {#sec-release-16.03}
+
+In addition to numerous new and upgraded packages, this release has the following highlights:
+
+- Systemd 229, bringing [numerous improvements](https://github.com/systemd/systemd/blob/v229/NEWS) over 217.
+
+- Linux 4.4 (was 3.18).
+
+- GCC 5.3 (was 4.9). Note that GCC 5 [changes the C++ ABI in an incompatible way](https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_dual_abi.html); this may cause problems if you try to link objects compiled with different versions of GCC.
+
+- Glibc 2.23 (was 2.21).
+
+- Binutils 2.26 (was 2.23.1). See \#909
+
+- Improved support for ensuring [bitwise reproducible builds](https://reproducible-builds.org/). For example, `stdenv` now sets the environment variable `SOURCE_DATE_EPOCH` to a deterministic value, and Nix has [gained an option](https://nixos.org/nix/manual/#ssec-relnotes-1.11) 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).
+
+- Perl 5.22.
+
+The following new services were added since the last release:
+
+- `services/monitoring/longview.nix`
+
+- `hardware/video/webcam/facetimehd.nix`
+
+- `i18n/input-method/default.nix`
+
+- `i18n/input-method/fcitx.nix`
+
+- `i18n/input-method/ibus.nix`
+
+- `i18n/input-method/nabi.nix`
+
+- `i18n/input-method/uim.nix`
+
+- `programs/fish.nix`
+
+- `security/acme.nix`
+
+- `security/audit.nix`
+
+- `security/oath.nix`
+
+- `services/hardware/irqbalance.nix`
+
+- `services/mail/dspam.nix`
+
+- `services/mail/opendkim.nix`
+
+- `services/mail/postsrsd.nix`
+
+- `services/mail/rspamd.nix`
+
+- `services/mail/rmilter.nix`
+
+- `services/misc/autofs.nix`
+
+- `services/misc/bepasty.nix`
+
+- `services/misc/calibre-server.nix`
+
+- `services/misc/cfdyndns.nix`
+
+- `services/misc/gammu-smsd.nix`
+
+- `services/misc/mathics.nix`
+
+- `services/misc/matrix-synapse.nix`
+
+- `services/misc/octoprint.nix`
+
+- `services/monitoring/hdaps.nix`
+
+- `services/monitoring/heapster.nix`
+
+- `services/monitoring/longview.nix`
+
+- `services/network-filesystems/netatalk.nix`
+
+- `services/network-filesystems/xtreemfs.nix`
+
+- `services/networking/autossh.nix`
+
+- `services/networking/dnschain.nix`
+
+- `services/networking/gale.nix`
+
+- `services/networking/miniupnpd.nix`
+
+- `services/networking/namecoind.nix`
+
+- `services/networking/ostinato.nix`
+
+- `services/networking/pdnsd.nix`
+
+- `services/networking/shairport-sync.nix`
+
+- `services/networking/supplicant.nix`
+
+- `services/search/kibana.nix`
+
+- `services/security/haka.nix`
+
+- `services/security/physlock.nix`
+
+- `services/web-apps/pump.io.nix`
+
+- `services/x11/hardware/libinput.nix`
+
+- `services/x11/window-managers/windowlab.nix`
+
+- `system/boot/initrd-network.nix`
+
+- `system/boot/initrd-ssh.nix`
+
+- `system/boot/loader/loader.nix`
+
+- `system/boot/networkd.nix`
+
+- `system/boot/resolved.nix`
+
+- `virtualisation/lxd.nix`
+
+- `virtualisation/rkt.nix`
+
+When upgrading from a previous release, please be aware of the following incompatible changes:
+
+- We no longer produce graphical ISO images and VirtualBox images for `i686-linux`. A minimal ISO image is still provided.
+
+- Firefox and similar browsers are now _wrapped by default_. The package and attribute names are plain `firefox` or `midori`, etc. Backward-compatibility attributes were set up, but note that `nix-env -u` will _not_ update your current `firefox-with-plugins`; you have to uninstall it and install `firefox` instead.
+
+- `wmiiSnap` has been replaced with `wmii_hg`, but `services.xserver.windowManager.wmii.enable` has been updated respectively so this only affects you if you have explicitly installed `wmiiSnap`.
+
+- `jobs` NixOS option has been removed. It served as compatibility layer between Upstart jobs and SystemD services. All services have been rewritten to use `systemd.services`
+
+- `wmiimenu` is removed, as it has been removed by the developers upstream. Use `wimenu` from the `wmii-hg` package.
+
+- 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.
+
+  ```nix
+  {
+    imports = [ <nixpkgs/nixos/modules/services/misc/gitit.nix> ];
+  }
+  ```
+
+  will include the Gitit service configuration options.
+
+- `nginx` does not accept flags for enabling and disabling modules anymore. Instead it accepts `modules` argument, which is a list of modules to be built in. All modules now reside in `nginxModules` set. Example configuration:
+
+  ```nix
+  nginx.override {
+    modules = [ nginxModules.rtmp nginxModules.dav nginxModules.moreheaders ];
+  }
+  ```
+
+- `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.
+
+- `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.
+
+- `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.
+
+- 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:
+
+  ```nix
+  {
+    fileSystems."/example" = {
+      device = "/dev/sdc";
+      fsType = "btrfs";
+      options = [ "noatime" "compress=lzo" "space_cache" "autodefrag" ];
+    };
+  }
+  ```
+
+- 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.
+
+- `services.xserver.vaapiDrivers` has been removed. Use `hardware.opengl.extraPackages{,32}` instead. You can also specify VDPAU drivers there.
+
+- `programs.ibus` moved to `i18n.inputMethod.ibus`. The option `programs.ibus.plugins` changed to `i18n.inputMethod.ibus.engines` and the option to enable ibus changed from `programs.ibus.enable` to `i18n.inputMethod.enabled`. `i18n.inputMethod.enabled` should be set to the used input method name, `"ibus"` for ibus. An example of the new style:
+
+  ```nix
+  {
+    i18n.inputMethod.enabled = "ibus";
+    i18n.inputMethod.ibus.engines = with pkgs.ibus-engines; [ anthy mozc ];
+  }
+  ```
+
+  That is equivalent to the old version:
+
+  ```nix
+  {
+    programs.ibus.enable = true;
+    programs.ibus.plugins = with pkgs; [ ibus-anthy mozc ];
+  }
+  ```
+
+- `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 overriden 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.
+
+- The option `services.nsd.zones.<name>.data` no longer interpret the dollar sign (\$) as a shell variable, as such it should not be escaped anymore. Thus the following zone data:
+
+  ```dns-zone
+  $ORIGIN example.com.
+  $TTL 1800
+  @       IN      SOA     ns1.vpn.nbp.name.      admin.example.com. (
+
+  ```
+
+  Should modified to look like the actual file expected by nsd:
+
+  ```dns-zone
+  $ORIGIN example.com.
+  $TTL 1800
+  @       IN      SOA     ns1.vpn.nbp.name.      admin.example.com. (
+
+  ```
+
+- `service.syncthing.dataDir` options now has to point to exact folder where syncthing is writing to. Example configuration should look something like:
+
+  ```nix
+  {
+    services.syncthing = {
+        enable = true;
+        dataDir = "/home/somebody/.syncthing";
+        user = "somebody";
+    };
+  }
+  ```
+
+- `networking.firewall.allowPing` is now enabled by default. Users are encouraged to configure an appropriate rate limit for their machines using the Kernel interface at `/proc/sys/net/ipv4/icmp_ratelimit` and `/proc/sys/net/ipv6/icmp/ratelimit` or using the firewall itself, i.e. by setting the NixOS option `networking.firewall.pingLimit`.
+
+- Systems with some broadcom cards used to result into a generated config that is no longer accepted. If you get errors like
+
+  ```ShellSession
+  error: path ‘/nix/store/*-broadcom-sta-*’ does not exist and cannot be created
+  ```
+
+  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:
+
+  ```shell
+  GPG_TTY=$(tty)
+  export GPG_TTY
+  ```
+
+  If you want to use gpg-agent for SSH, too, add the following to your session initialization (e.g. `displayManager.sessionCommands`)
+
+  ```shell
+      gpg-connect-agent /bye
+      unset SSH_AGENT_PID
+      export SSH_AUTH_SOCK="''${HOME}/.gnupg/S.gpg-agent.ssh"
+  ```
+
+  and make sure that
+
+  ```conf
+      enable-ssh-support
+  ```
+
+  is included in your `~/.gnupg/gpg-agent.conf`. You will need to use `ssh-add` 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:
+
+  ```ShellSession
+      gpg --import ~/.gnupg/secring.gpg
+  ```
+
+  The `gpg-agent(1)` man page has more details about this subject, i.e. in the \"EXAMPLES\" section.
+
+Other notable improvements:
+
+- `ejabberd` module is brought back and now works on NixOS.
+
+- 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.
diff --git a/nixos/doc/manual/release-notes/rl-1609.section.md b/nixos/doc/manual/release-notes/rl-1609.section.md
new file mode 100644
index 00000000000..075f0cf52cd
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-1609.section.md
@@ -0,0 +1,73 @@
+# Release 16.09 ("Flounder", 2016/09/30) {#sec-release-16.09}
+
+In addition to numerous new and upgraded packages, this release has the following highlights:
+
+- Many NixOS configurations and Nix packages now use significantly less disk space, thanks to the [extensive work on closure size reduction](https://github.com/NixOS/nixpkgs/issues/7117). 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.
+
+- To improve security, packages are now [built using various hardening features](https://github.com/NixOS/nixpkgs/pull/12895). See the Nixpkgs manual for more information.
+
+- Support for PXE netboot. See [](#sec-booting-from-pxe) for documentation.
+
+- X.org server 1.18. If you use the `ati_unfree` driver, 1.17 is still used due to an ABI incompatibility.
+
+- This release is based on Glibc 2.24, GCC 5.4.0 and systemd 231. The default Linux kernel remains 4.4.
+
+The following new services were added since the last release:
+
+- `(this will get automatically generated at release time)`
+
+When upgrading from a previous release, please be aware of the following incompatible changes:
+
+- 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).
+
+- Shell aliases for systemd sub-commands [were dropped](https://github.com/NixOS/nixpkgs/pull/15598): `start`, `stop`, `restart`, `status`.
+
+- 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
+
+- `/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.
+
+- `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).
+
+- `fonts.fontconfig.ultimate.rendering` was removed because our presets were obsolete for some time. New presets are hardcoded into FreeType; you can select a preset via `fonts.fontconfig.ultimate.preset`. You can customize those presets via ordinary environment variables, using `environment.variables`.
+
+- The `audit` service is no longer enabled by default. Use `security.audit.enable = true` to explicitly enable it.
+
+- `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.
+
+- `services.mongodb.extraConfig` configuration format was changed to YAML.
+
+- PHP has been upgraded to 7.0
+
+Other notable improvements:
+
+- 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
+
+  ```nix
+  {
+    security.grsecurity.enable = true;
+  }
+  ```
+
+  to get a reasonably secure system without having to sacrifice too much functionality.
+
+- Special filesystems, like `/proc`, `/run` 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 `nixos-rebuild switch` if possible. One benefit from this is improved security --- most such filesystems are now mounted with `noexec`, `nodev` and/or `nosuid` options.
+
+- 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 (`networking.firewall.logReversePathDrops`) for easier debugging.
+
+- Containers configuration within `containers.<name>.config` is [now properly typed and checked](https://github.com/NixOS/nixpkgs/pull/17365). In particular, partial configurations are merged correctly.
+
+- The directory container setuid wrapper programs, `/var/setuid-wrappers`, [is now updated atomically to prevent failures if the switch to a new configuration is interrupted.](https://github.com/NixOS/nixpkgs/pull/18124)
+
+- `services.xserver.startGnuPGAgent` has been removed due to GnuPG 2.1.x bump. See [ how to achieve similar behavior](https://github.com/NixOS/nixpkgs/commit/5391882ebd781149e213e8817fba6ac3c503740c). You might need to `pkill gpg-agent` after the upgrade to prevent a stale agent being in the way.
+
+- [ Declarative users could share the uid due to the bug in the script handling conflict resolution. ](https://github.com/NixOS/nixpkgs/commit/e561edc322d275c3687fec431935095cfc717147)
+
+- Gummi boot has been replaced using systemd-boot.
+
+- Hydra package and NixOS module were added for convenience.
diff --git a/nixos/doc/manual/release-notes/rl-1703.section.md b/nixos/doc/manual/release-notes/rl-1703.section.md
new file mode 100644
index 00000000000..7f424f2a6ce
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-1703.section.md
@@ -0,0 +1,303 @@
+# Release 17.03 ("Gorilla", 2017/03/31) {#sec-release-17.03}
+
+## Highlights {#sec-release-17.03-highlights}
+
+In addition to numerous new and upgraded packages, this release has the following highlights:
+
+- Nixpkgs is now extensible through overlays. See the [Nixpkgs manual](https://nixos.org/nixpkgs/manual/#sec-overlays-install) for more information.
+
+- 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 setuid wrapper functionality now supports setting capabilities.
+
+- X.org server uses branch 1.19. Due to ABI incompatibilities, `ati_unfree` keeps forcing 1.17 and `amdgpu-pro` starts forcing 1.18.
+
+- Cross compilation has been rewritten. See the nixpkgs manual for details. The most obvious breaking change is that in derivations there is no `.nativeDrv` nor `.crossDrv` are now cross by default, not native.
+
+- The `overridePackages` function has been rewritten to be replaced by [ overlays](https://nixos.org/nixpkgs/manual/#sec-overlays-install)
+
+- Packages in nixpkgs can be marked as insecure through listed vulnerabilities. See the [Nixpkgs manual](https://nixos.org/nixpkgs/manual/#sec-allow-insecure) for more information.
+
+- PHP now defaults to PHP 7.1
+
+## New Services {#sec-release-17.03-new-services}
+
+The following new services were added since the last release:
+
+- `hardware/ckb.nix`
+
+- `hardware/mcelog.nix`
+
+- `hardware/usb-wwan.nix`
+
+- `hardware/video/capture/mwprocapture.nix`
+
+- `programs/adb.nix`
+
+- `programs/chromium.nix`
+
+- `programs/gphoto2.nix`
+
+- `programs/java.nix`
+
+- `programs/mtr.nix`
+
+- `programs/oblogout.nix`
+
+- `programs/vim.nix`
+
+- `programs/wireshark.nix`
+
+- `security/dhparams.nix`
+
+- `services/audio/ympd.nix`
+
+- `services/computing/boinc/client.nix`
+
+- `services/continuous-integration/buildbot/master.nix`
+
+- `services/continuous-integration/buildbot/worker.nix`
+
+- `services/continuous-integration/gitlab-runner.nix`
+
+- `services/databases/riak-cs.nix`
+
+- `services/databases/stanchion.nix`
+
+- `services/desktops/gnome3/gnome-terminal-server.nix`
+
+- `services/editors/infinoted.nix`
+
+- `services/hardware/illum.nix`
+
+- `services/hardware/trezord.nix`
+
+- `services/logging/journalbeat.nix`
+
+- `services/mail/offlineimap.nix`
+
+- `services/mail/postgrey.nix`
+
+- `services/misc/couchpotato.nix`
+
+- `services/misc/docker-registry.nix`
+
+- `services/misc/errbot.nix`
+
+- `services/misc/geoip-updater.nix`
+
+- `services/misc/gogs.nix`
+
+- `services/misc/leaps.nix`
+
+- `services/misc/nix-optimise.nix`
+
+- `services/misc/ssm-agent.nix`
+
+- `services/misc/sssd.nix`
+
+- `services/monitoring/arbtt.nix`
+
+- `services/monitoring/netdata.nix`
+
+- `services/monitoring/prometheus/default.nix`
+
+- `services/monitoring/prometheus/alertmanager.nix`
+
+- `services/monitoring/prometheus/blackbox-exporter.nix`
+
+- `services/monitoring/prometheus/json-exporter.nix`
+
+- `services/monitoring/prometheus/nginx-exporter.nix`
+
+- `services/monitoring/prometheus/node-exporter.nix`
+
+- `services/monitoring/prometheus/snmp-exporter.nix`
+
+- `services/monitoring/prometheus/unifi-exporter.nix`
+
+- `services/monitoring/prometheus/varnish-exporter.nix`
+
+- `services/monitoring/sysstat.nix`
+
+- `services/monitoring/telegraf.nix`
+
+- `services/monitoring/vnstat.nix`
+
+- `services/network-filesystems/cachefilesd.nix`
+
+- `services/network-filesystems/glusterfs.nix`
+
+- `services/network-filesystems/ipfs.nix`
+
+- `services/networking/dante.nix`
+
+- `services/networking/dnscrypt-wrapper.nix`
+
+- `services/networking/fakeroute.nix`
+
+- `services/networking/flannel.nix`
+
+- `services/networking/htpdate.nix`
+
+- `services/networking/miredo.nix`
+
+- `services/networking/nftables.nix`
+
+- `services/networking/powerdns.nix`
+
+- `services/networking/pdns-recursor.nix`
+
+- `services/networking/quagga.nix`
+
+- `services/networking/redsocks.nix`
+
+- `services/networking/wireguard.nix`
+
+- `services/system/cgmanager.nix`
+
+- `services/torrent/opentracker.nix`
+
+- `services/web-apps/atlassian/confluence.nix`
+
+- `services/web-apps/atlassian/crowd.nix`
+
+- `services/web-apps/atlassian/jira.nix`
+
+- `services/web-apps/frab.nix`
+
+- `services/web-apps/nixbot.nix`
+
+- `services/web-apps/selfoss.nix`
+
+- `services/web-apps/quassel-webserver.nix`
+
+- `services/x11/unclutter-xfixes.nix`
+
+- `services/x11/urxvtd.nix`
+
+- `system/boot/systemd-nspawn.nix`
+
+- `virtualisation/ecs-agent.nix`
+
+- `virtualisation/lxcfs.nix`
+
+- `virtualisation/openstack/keystone.nix`
+
+- `virtualisation/openstack/glance.nix`
+
+## Backward Incompatibilities {#sec-release-17.03-incompatibilities}
+
+When upgrading from a previous release, please be aware of the following incompatible changes:
+
+- Derivations have no `.nativeDrv` nor `.crossDrv` and are now cross by default, not native.
+
+- `stdenv.overrides` is now expected to take `self` and `super` arguments. See `lib.trivial.extends` for what those parameters represent.
+
+- `ansible` now defaults to ansible version 2 as version 1 has been removed due to a serious [ vulnerability](https://www.computest.nl/advisories/CT-2017-0109_Ansible.txt) unpatched by upstream.
+
+- `gnome` alias has been removed along with `gtk`, `gtkmm` and several others. Now you need to use versioned attributes, like `gnome3`.
+
+- The attribute name of the Radicale daemon has been changed from `pythonPackages.radicale` to `radicale`.
+
+- The `stripHash` bash function in `stdenv` changed according to its documentation; it now outputs the stripped name to `stdout` instead of putting it in the variable `strippedName`.
+
+- 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.
+
+- 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.
+
+- `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`.
+
+- `service.nylon` is now declared using named instances. As an example:
+
+  ```nix
+  {
+    services.nylon = {
+      enable = true;
+      acceptInterface = "br0";
+      bindInterface = "tun1";
+      port = 5912;
+    };
+  }
+  ```
+
+  should be replaced with:
+
+  ```nix
+  {
+    services.nylon.myvpn = {
+      enable = true;
+      acceptInterface = "br0";
+      bindInterface = "tun1";
+      port = 5912;
+    };
+  }
+  ```
+
+  this enables you to declare a SOCKS proxy for each uplink.
+
+- `overridePackages` function no longer exists. It is replaced by [ overlays](https://nixos.org/nixpkgs/manual/#sec-overlays-install). For example, the following code:
+
+  ```nix
+  let
+    pkgs = import <nixpkgs> {};
+  in
+    pkgs.overridePackages (self: super: ...)
+  ```
+
+  should be replaced by:
+
+  ```nix
+  let
+    pkgs = import <nixpkgs> {};
+  in
+    import pkgs.path { overlays = [(self: super: ...)]; }
+  ```
+
+- 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`.
+
+- 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 `fetch*` functions no longer support md5, please use sha256 instead.
+
+- The dnscrypt-proxy module interface has been streamlined around the `extraArgs` option. Where possible, legacy option declarations are mapped to `extraArgs` but will emit warnings. The `resolverList` has been outright removed: to use an unlisted resolver, use the `customResolver` option.
+
+- torbrowser now stores local state under `~/.local/share/tor-browser` by default. Any browser profile data from the old location, `~/.torbrowser4`, must be migrated manually.
+
+- The ihaskell, monetdb, offlineimap and sitecopy services have been removed.
+
+## Other Notable Changes {#sec-release-17.03-notable-changes}
+
+- 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`.
+
+- Python 2.6 interpreter and package set have been removed.
+
+- 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.
+
+- 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 `nix-shell`. See the Nixpkgs manual for details.
+
+- The Python package sets now use a fixed-point combinator and the sets are available as attributes of the interpreters.
+
+- The Python function `buildPythonPackage` has been improved and can be used to build from Setuptools source, Flit source, and precompiled Wheels.
+
+- When adding new or updating current Python libraries, the expressions should be put in separate files in `pkgs/development/python-modules` and called from `python-packages.nix`.
+
+- 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.
+
+- Containers using bridged networking no longer lose their connection after changes to the host networking.
+
+- ZFS supports pool auto scrubbing.
+
+- The bind DNS utilities (e.g. dig) have been split into their own output and are now also available in `pkgs.dnsutils` and it is no longer necessary to pull in all of `bind` to use them.
+
+- Per-user configuration was moved from `~/.nixpkgs` to `~/.config/nixpkgs`. The former is still valid for `config.nix` for backwards compatibility.
diff --git a/nixos/doc/manual/release-notes/rl-1709.section.md b/nixos/doc/manual/release-notes/rl-1709.section.md
new file mode 100644
index 00000000000..e5af22721b0
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-1709.section.md
@@ -0,0 +1,316 @@
+# Release 17.09 ("Hummingbird", 2017/09/??) {#sec-release-17.09}
+
+## Highlights {#sec-release-17.09-highlights}
+
+In addition to numerous new and upgraded packages, this release has the following highlights:
+
+- 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.
+
+- 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:
+
+  ```nix
+  { services.xserver.xrandrHeads = [
+      "HDMI-0"
+      {
+        output = "DVI-0";
+        primary = true;
+        monitorConfig = ''
+          Option "Rotate" "right"
+        '';
+      }
+    ];
+  }
+  ```
+
+  This will set the `DVI-0` output to be the primary head, even though `HDMI-0` is the first head in the list.
+
+- The handling of SSL in the `services.nginx` module has been cleaned up, renaming the misnamed `enableSSL` to `onlySSL` which reflects its original intention. This is not to be used with the already existing `forceSSL` 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 `enableSSL` option to keep the previous behaviour.
+
+  Another `addSSL` option has been introduced to configure both a non-SSL virtual host and an SSL virtual host with the same configuration.
+
+  Options to configure `resolver` options and `upstream` blocks have been introduced. See their information for further details.
+
+  The `port` option has been replaced by a more generic `listen` option which makes it possible to specify multiple addresses, ports and SSL configs dependant on the new SSL handling mentioned above.
+
+## New Services {#sec-release-17.09-new-services}
+
+The following new services were added since the last release:
+
+- `config/fonts/fontconfig-penultimate.nix`
+
+- `config/fonts/fontconfig-ultimate.nix`
+
+- `config/terminfo.nix`
+
+- `hardware/sensor/iio.nix`
+
+- `hardware/nitrokey.nix`
+
+- `hardware/raid/hpsa.nix`
+
+- `programs/browserpass.nix`
+
+- `programs/gnupg.nix`
+
+- `programs/qt5ct.nix`
+
+- `programs/slock.nix`
+
+- `programs/thefuck.nix`
+
+- `security/auditd.nix`
+
+- `security/lock-kernel-modules.nix`
+
+- `service-managers/docker.nix`
+
+- `service-managers/trivial.nix`
+
+- `services/admin/salt/master.nix`
+
+- `services/admin/salt/minion.nix`
+
+- `services/audio/slimserver.nix`
+
+- `services/cluster/kubernetes/default.nix`
+
+- `services/cluster/kubernetes/dns.nix`
+
+- `services/cluster/kubernetes/dashboard.nix`
+
+- `services/continuous-integration/hail.nix`
+
+- `services/databases/clickhouse.nix`
+
+- `services/databases/postage.nix`
+
+- `services/desktops/gnome3/gnome-disks.nix`
+
+- `services/desktops/gnome3/gpaste.nix`
+
+- `services/logging/SystemdJournal2Gelf.nix`
+
+- `services/logging/heartbeat.nix`
+
+- `services/logging/journalwatch.nix`
+
+- `services/logging/syslogd.nix`
+
+- `services/mail/mailhog.nix`
+
+- `services/mail/nullmailer.nix`
+
+- `services/misc/airsonic.nix`
+
+- `services/misc/autorandr.nix`
+
+- `services/misc/exhibitor.nix`
+
+- `services/misc/fstrim.nix`
+
+- `services/misc/gollum.nix`
+
+- `services/misc/irkerd.nix`
+
+- `services/misc/jackett.nix`
+
+- `services/misc/radarr.nix`
+
+- `services/misc/snapper.nix`
+
+- `services/monitoring/osquery.nix`
+
+- `services/monitoring/prometheus/collectd-exporter.nix`
+
+- `services/monitoring/prometheus/fritzbox-exporter.nix`
+
+- `services/network-filesystems/kbfs.nix`
+
+- `services/networking/dnscache.nix`
+
+- `services/networking/fireqos.nix`
+
+- `services/networking/iwd.nix`
+
+- `services/networking/keepalived/default.nix`
+
+- `services/networking/keybase.nix`
+
+- `services/networking/lldpd.nix`
+
+- `services/networking/matterbridge.nix`
+
+- `services/networking/squid.nix`
+
+- `services/networking/tinydns.nix`
+
+- `services/networking/xrdp.nix`
+
+- `services/security/shibboleth-sp.nix`
+
+- `services/security/sks.nix`
+
+- `services/security/sshguard.nix`
+
+- `services/security/torify.nix`
+
+- `services/security/usbguard.nix`
+
+- `services/security/vault.nix`
+
+- `services/system/earlyoom.nix`
+
+- `services/system/saslauthd.nix`
+
+- `services/web-apps/nexus.nix`
+
+- `services/web-apps/pgpkeyserver-lite.nix`
+
+- `services/web-apps/piwik.nix`
+
+- `services/web-servers/lighttpd/collectd.nix`
+
+- `services/web-servers/minio.nix`
+
+- `services/x11/display-managers/xpra.nix`
+
+- `services/x11/xautolock.nix`
+
+- `tasks/filesystems/bcachefs.nix`
+
+- `tasks/powertop.nix`
+
+## Backward Incompatibilities {#sec-release-17.09-incompatibilities}
+
+When upgrading from a previous release, please be aware of the following incompatible changes:
+
+- **In an Qemu-based virtualization environment, the network interface names changed from i.e. `enp0s3` to `ens3`.**
+
+  This is due to a kernel configuration change. The new naming is consistent with those of other Linux distributions with systemd. See [\#29197](https://github.com/NixOS/nixpkgs/issues/29197) for more information.
+
+  A machine is affected if the `virt-what` tool either returns `qemu` or `kvm` _and_ has interface names used in any part of its NixOS configuration, in particular if a static network configuration with `networking.interfaces` is used.
+
+  Before rebooting affected machines, please ensure:
+
+  - Change the interface names in your NixOS configuration. The first interface will be called `ens3`, the second one `ens8` and starting from there incremented by 1.
+
+  - After changing the interface names, rebuild your system with `nixos-rebuild boot` to activate the new configuration after a reboot. If you switch to the new configuration right away you might lose network connectivity! If using `nixops`, deploy with `nixops deploy --force-reboot`.
+
+- The following changes apply if the `stateVersion` is changed to 17.09 or higher. For `stateVersion = "17.03"` or lower the old behavior is preserved.
+
+  - The `postgres` default version was changed from 9.5 to 9.6.
+
+  - The `postgres` superuser name has changed from `root` to `postgres` to more closely follow what other Linux distributions are doing.
+
+  - The `postgres` default `dataDir` has changed from `/var/db/postgres` to `/var/lib/postgresql/$psqlSchema` where \$psqlSchema is 9.6 for example.
+
+  - 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.
+
+- The `aiccu` package was removed. This is due to SixXS [ sunsetting](https://www.sixxs.net/main/) its IPv6 tunnel.
+
+- The `fanctl` package and `fan` module have been removed due to the developers not upstreaming their iproute2 patches and lagging with compatibility to recent iproute2 versions.
+
+- 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.
+
+- 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>
+  mv /var/lib/ipfs/.ipfs/* $dataDir
+  rmdir /var/lib/ipfs/.ipfs
+  ```
+
+- The `caddy` service was previously using an extra `.caddy` directory in the data directory specified with the `dataDir` option. The contents of the `.caddy` directory are now expected to be in the `dataDir`.
+
+- The `ssh-agent` user service is not started by default anymore. Use `programs.ssh.startAgent` to enable it if needed. There is also a new `programs.gnupg.agent` module that creates a `gpg-agent` user service. It can also serve as a SSH agent if `enableSSHSupport` is set.
+
+- The `services.tinc.networks.<name>.listenAddress` 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 `services.tinc.networks.<name>.bindToAddress` instead. Refer to the description of the options for more details.
+
+- `tlsdate` package and module were removed. This is due to the project being dead and not building with openssl 1.1.
+
+- `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.
+
+- `services.firefox.syncserver` now runs by default as a non-root user. To accomodate 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.
+
+- The `compiz` window manager and package was removed. The system support had been broken for several years.
+
+- 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.
+
+- `services.mysql` now has declarative configuration of databases and users with the `ensureDatabases` and `ensureUsers` options.
+
+  These options will never delete existing databases and users, especially not when the value of the options are changed.
+
+  The MySQL users will be identified using [ Unix socket authentication](https://mariadb.com/kb/en/library/authentication-plugin-unix-socket/). This authenticates the Unix user with the same name only, and that without the need for a password.
+
+  If you have previously created a MySQL `root` user _with a password_, you will need to add `root` user for unix socket authentication before using the new options. This can be done by running the following SQL script:
+
+  ```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';
+  ```
+
+- `services.mysqlBackup` now works by default without any user setup, including for users other than `mysql`.
+
+  By default, the `mysql` user is no longer the user which performs the backup. Instead a system account `mysqlbackup` is used.
+
+  The `mysqlBackup` service is also now using systemd timers instead of `cron`.
+
+  Therefore, the `services.mysqlBackup.period` option no longer exists, and has been replaced with `services.mysqlBackup.calendar`, which is in the format of [systemd.time(7)](https://www.freedesktop.org/software/systemd/man/systemd.time.html#Calendar%20Events).
+
+  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.
+
+  You can check that backups still work by running `systemctl start mysql-backup` then `systemctl status mysql-backup`.
+
+- Templated systemd services e.g `container@name` are now handled currectly when switching to a new configuration, resulting in them being reloaded.
+
+- Steam: the `newStdcpp` parameter was removed and should not be needed anymore.
+
+- 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.
+
+## 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.
+
+- 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.
+
+- The `time.timeZone` option now allows the value `null` in addition to timezone strings. This value allows changing the timezone of a system imperatively using `timedatectl set-timezone`. The default timezone is still UTC.
+
+- Nixpkgs overlays may now be specified with a file as well as a directory. The value of `<nixpkgs-overlays>` may be a file, and `~/.config/nixpkgs/overlays.nix` can be used instead of the `~/.config/nixpkgs/overlays` directory.
+
+  See the overlays chapter of the Nixpkgs manual for more details.
+
+- Definitions for `/etc/hosts` can now be specified declaratively with `networking.hosts`.
+
+- 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.
+
+  This therefore leads to adding a new `debug` option to set the log level to the previous verbose mode, to make debugging easier, but still accessible easily.
+
+  Additionally a `copytoram` 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.
+
+- `services.gitlab-runner.configOptions` has been added to specify the configuration of gitlab-runners declaratively.
+
+- `services.jenkins.plugins` has been added to install plugins easily, this can be generated with jenkinsPlugins2nix.
+
+- `services.postfix.config` 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.
+
+- The GitLab package and module have been updated to the latest 10.0 release.
+
+- The `systemd-boot` boot loader now lists the NixOS version, kernel version and build date of all bootable generations.
+
+- 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 `services.dnscrypt-proxy.resolverName` option or setting it to `"random"`.
diff --git a/nixos/doc/manual/release-notes/rl-1803.section.md b/nixos/doc/manual/release-notes/rl-1803.section.md
new file mode 100644
index 00000000000..e4e46798104
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-1803.section.md
@@ -0,0 +1,284 @@
+# Release 18.03 ("Impala", 2018/04/04) {#sec-release-18.03}
+
+## Highlights {#sec-release-18.03-highlights}
+
+In addition to numerous new and upgraded packages, this release has the following highlights:
+
+- 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.
+
+- Nix now defaults to 2.0; see its [release notes](https://nixos.org/nix/manual/#ssec-relnotes-2.0).
+
+- Core version changes: linux: 4.9 -\> 4.14, glibc: 2.25 -\> 2.26, gcc: 6 -\> 7, systemd: 234 -\> 237.
+
+- Desktop version changes: gnome: 3.24 -\> 3.26, (KDE) plasma-desktop: 5.10 -\> 5.12.
+
+- 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:
+
+  - `libmysql` has been deprecated, please use `mysql.connector-c` instead, a compatibility passthru has been added to the MySQL packages.
+
+  - The `mysql57` package has a new `static` output containing the static libraries including `libmysqld.a`
+
+- PHP now defaults to PHP 7.2, updated from 7.1.
+
+## New Services {#sec-release-18.03-new-services}
+
+The following new services were added since the last release:
+
+- `./config/krb5/default.nix`
+
+- `./hardware/digitalbitbox.nix`
+
+- `./misc/label.nix`
+
+- `./programs/ccache.nix`
+
+- `./programs/criu.nix`
+
+- `./programs/digitalbitbox/default.nix`
+
+- `./programs/less.nix`
+
+- `./programs/npm.nix`
+
+- `./programs/plotinus.nix`
+
+- `./programs/rootston.nix`
+
+- `./programs/systemtap.nix`
+
+- `./programs/sway.nix`
+
+- `./programs/udevil.nix`
+
+- `./programs/way-cooler.nix`
+
+- `./programs/yabar.nix`
+
+- `./programs/zsh/zsh-autoenv.nix`
+
+- `./services/backup/borgbackup.nix`
+
+- `./services/backup/crashplan-small-business.nix`
+
+- `./services/desktops/dleyna-renderer.nix`
+
+- `./services/desktops/dleyna-server.nix`
+
+- `./services/desktops/pipewire.nix`
+
+- `./services/desktops/gnome3/chrome-gnome-shell.nix`
+
+- `./services/desktops/gnome3/tracker-miners.nix`
+
+- `./services/hardware/fwupd.nix`
+
+- `./services/hardware/interception-tools.nix`
+
+- `./services/hardware/u2f.nix`
+
+- `./services/hardware/usbmuxd.nix`
+
+- `./services/mail/clamsmtp.nix`
+
+- `./services/mail/dkimproxy-out.nix`
+
+- `./services/mail/pfix-srsd.nix`
+
+- `./services/misc/gitea.nix`
+
+- `./services/misc/home-assistant.nix`
+
+- `./services/misc/ihaskell.nix`
+
+- `./services/misc/logkeys.nix`
+
+- `./services/misc/novacomd.nix`
+
+- `./services/misc/osrm.nix`
+
+- `./services/misc/plexpy.nix`
+
+- `./services/misc/pykms.nix`
+
+- `./services/misc/tzupdate.nix`
+
+- `./services/monitoring/fusion-inventory.nix`
+
+- `./services/monitoring/prometheus/exporters.nix`
+
+- `./services/network-filesystems/beegfs.nix`
+
+- `./services/network-filesystems/davfs2.nix`
+
+- `./services/network-filesystems/openafs/client.nix`
+
+- `./services/network-filesystems/openafs/server.nix`
+
+- `./services/network-filesystems/ceph.nix`
+
+- `./services/networking/aria2.nix`
+
+- `./services/networking/monero.nix`
+
+- `./services/networking/nghttpx/default.nix`
+
+- `./services/networking/nixops-dns.nix`
+
+- `./services/networking/rxe.nix`
+
+- `./services/networking/stunnel.nix`
+
+- `./services/web-apps/matomo.nix`
+
+- `./services/web-apps/restya-board.nix`
+
+- `./services/web-servers/mighttpd2.nix`
+
+- `./services/x11/fractalart.nix`
+
+- `./system/boot/binfmt.nix`
+
+- `./system/boot/grow-partition.nix`
+
+- `./tasks/filesystems/ecryptfs.nix`
+
+- `./virtualisation/hyperv-guest.nix`
+
+## Backward Incompatibilities {#sec-release-18.03-incompatibilities}
+
+When upgrading from a previous release, please be aware of the following incompatible changes:
+
+- `sound.enable` now defaults to false.
+
+- Dollar signs in options under `services.postfix` 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.
+
+- The `postage` package (for web-based PostgreSQL administration) has been renamed to `pgmanage`. The corresponding module has also been renamed. To migrate please rename all `services.postage` options to `services.pgmanage`.
+
+- 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 `nix-env`. The change affects the following packages:
+
+  - `2048-in-terminal` → `_2048-in-terminal`
+
+  - `90secondportraits` → `_90secondportraits`
+
+  - `2bwm` → `_2bwm`
+
+  - `389-ds-base` → `_389-ds-base`
+
+- **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.**
+
+  DSA support was [deprecated in OpenSSH 7.0](https://www.openssh.com/legacy.html), due to it being too weak. To re-enable support, add `PubkeyAcceptedKeyTypes +ssh-dss` to the end of your `services.openssh.extraConfig`.
+
+  After updating the keys to be stronger, anyone still on a pre-17.03 version is safe to jump to 17.03, as vetted [here](https://search.nix.gsc.io/?q=stateVersion).
+
+- The `openssh` package now includes Kerberos support by default; the `openssh_with_kerberos` package is now a deprecated alias. If you do not want Kerberos support, you can do `openssh.override { withKerberos = false; }`. Note, this also applies to the `openssh_hpn` package.
+
+- `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.
+
+- `lib.addPassthru drv passthru` is removed. Use `lib.extendDerivation true passthru drv` instead.
+
+- The `memcached` service no longer accept dynamic socket paths via `services.memcached.socket`. Unix sockets can be still enabled by `services.memcached.enableUnixSocket` and will be accessible at `/run/memcached/memcached.sock`.
+
+- 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 following changes apply if the `stateVersion` is changed to 18.03 or higher. For `stateVersion = "17.09"` or lower the old behavior is preserved.
+
+  - `matrix-synapse` uses postgresql by default instead of sqlite. Migration instructions can be found [ here ](https://github.com/matrix-org/synapse/blob/master/docs/postgres.rst#porting-from-sqlite).
+
+- The `jid` package has been removed, due to maintenance overhead of a go package having non-versioned dependencies.
+
+- When using `services.xserver.libinput` (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, `Option "XkbRules" "base"` may result in broken keyboard layout.
+
+- The `attic` package was removed. A maintained fork called [Borg](https://www.borgbackup.org/) should be used instead. Migration instructions can be found [here](http://borgbackup.readthedocs.io/en/stable/usage/upgrade.html#attic-and-borg-0-xx-to-borg-1-x).
+
+- The Piwik analytics software was renamed to Matomo:
+
+  - The package `pkgs.piwik` was renamed to `pkgs.matomo`.
+
+  - The service `services.piwik` was renamed to `services.matomo`.
+
+  - 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 `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`.
+
+  - 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.
+
+- `nodejs-4_x` is end-of-life. `nodejs-4_x`, `nodejs-slim-4_x` and `nodePackages_4_x` are removed.
+
+- The `pump.io` NixOS module was removed. It is now maintained as an [external module](https://github.com/rvl/pump.io-nixos).
+
+- The Prosody XMPP server has received a major update. The following modules were renamed:
+
+  - `services.prosody.modules.httpserver` is now `services.prosody.modules.http_files`
+
+  - `services.prosody.modules.console` is now `services.prosody.modules.admin_telnet`
+
+  Many new modules are now core modules, most notably `services.prosody.modules.carbons` and `services.prosody.modules.mam`.
+
+  The better-performing `libevent` backend is now enabled by default.
+
+  `withCommunityModules` now passes through the modules to `services.prosody.extraModules`. Use `withOnlyInstalledCommunityModules` for modules that should not be enabled directly, e.g `lib_ldap`.
+
+- All prometheus exporter modules are now defined as submodules. The exporters are configured using `services.prometheus.exporters`.
+
+## Other Notable Changes {#sec-release-18.03-notable-changes}
+
+- ZNC option `services.znc.mutable` now defaults to `true`. That means that old configuration is not overwritten by default when update to the znc options are made.
+
+- The option `networking.wireless.networks.<name>.auth` has been added for wireless networks with WPA-Enterprise authentication. There is also a new `extraConfig` option to directly configure `wpa_supplicant` and `hidden` to connect to hidden networks.
+
+- In the module `networking.interfaces.<name>` the following options have been removed:
+
+  - `ipAddress`
+
+  - `ipv6Address`
+
+  - `prefixLength`
+
+  - `ipv6PrefixLength`
+
+  - `subnetMask`
+
+  To assign static addresses to an interface the options `ipv4.addresses` and `ipv6.addresses` should be used instead. The options `ip4` and `ip6` have been renamed to `ipv4.addresses` `ipv6.addresses` respectively. The new options `ipv4.routes` and `ipv6.routes` have been added to set up static routing.
+
+- 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.
+
+- `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.
+
+  If set to null the default clock format is used.
+
+- `displayManager.lightdm.greeters.gtk.indicators` has been added, a list of allowed indicator modules to use with the lightdm gtk greeter panel.
+
+  Built-in indicators include `~a11y`, `~language`, `~session`, `~power`, `~clock`, `~host`, `~spacer`. Unity indicators can be represented by short name (e.g. `sound`, `power`), service file name, or absolute path.
+
+  If set to `null` the default indicators are used.
+
+  In order to have the previous default configuration add
+
+  ```nix
+  {
+    services.xserver.displayManager.lightdm.greeters.gtk.indicators = [
+      "~host" "~spacer"
+      "~clock" "~spacer"
+      "~session"
+      "~language"
+      "~a11y"
+      "~power"
+    ];
+  }
+  ```
+
+  to your `configuration.nix`.
+
+- The NixOS test driver supports user services declared by `systemd.user.services`. The methods `waitForUnit`, `getUnitInfo`, `startJob` and `stopJob` provide an optional `$user` argument for that purpose.
+
+- Enabling bash completion on NixOS, `programs.bash.enableCompletion`, will now also enable completion for the Nix command line tools by installing the [nix-bash-completions](https://github.com/hedning/nix-bash-completions) package.
diff --git a/nixos/doc/manual/release-notes/rl-1809.section.md b/nixos/doc/manual/release-notes/rl-1809.section.md
new file mode 100644
index 00000000000..3443db37c97
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-1809.section.md
@@ -0,0 +1,332 @@
+# Release 18.09 ("Jellyfish", 2018/10/05) {#sec-release-18.09}
+
+## Highlights {#sec-release-18.09-highlights}
+
+In addition to numerous new and upgraded packages, this release has the following notable updates:
+
+- End of support is planned for end of April 2019, handing over to 19.03.
+
+- 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.
+
+- Nix has been updated to 2.1; see its [release notes](https://nixos.org/nix/manual/#ssec-relnotes-2.1).
+
+- Core versions: linux: 4.14 LTS (unchanged), glibc: 2.26 → 2.27, gcc: 7 (unchanged), systemd: 237 → 239.
+
+- Desktop version changes: gnome: 3.26 → 3.28, (KDE) plasma-desktop: 5.12 → 5.13.
+
+Notable changes and additions for 18.09 include:
+
+- Support for wrapping binaries using `firejail` has been added through `programs.firejail.wrappedBinaries`.
+
+  For example
+
+  ```nix
+  {
+    programs.firejail = {
+      enable = true;
+      wrappedBinaries = {
+        firefox = "${lib.getBin pkgs.firefox}/bin/firefox";
+        mpv = "${lib.getBin pkgs.mpv}/bin/mpv";
+      };
+    };
+  }
+  ```
+
+  This will place `firefox` and `mpv` binaries in the global path wrapped by firejail.
+
+- User channels are now in the default `NIX_PATH`, allowing users to use their personal `nix-channel` defined channels in `nix-build` and `nix-shell` commands, as well as in imports like `import <mychannel>`.
+
+  For example
+
+  ```ShellSession
+  $ nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgsunstable
+  $ nix-channel --update
+  $ nix-build '<nixpkgsunstable>' -A gitFull
+  $ nix run -f '<nixpkgsunstable>' gitFull
+  $ nix-instantiate -E '(import <nixpkgsunstable> {}).gitFull'
+  ```
+
+## New Services {#sec-release-18.09-new-services}
+
+A curated selection of new services that were added since the last release:
+
+- The `services.cassandra` 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 [Apache Cassandra](https://cassandra.apache.org/).
+
+- There is a new `services.foundationdb` module for deploying [FoundationDB](https://www.foundationdb.org) clusters.
+
+- When enabled the `iproute2` will copy the files expected by ip route (e.g., `rt_tables`) in `/etc/iproute2`. This allows to write aliases for routing tables for instance.
+
+- `services.strongswan-swanctl` is a modern replacement for `services.strongswan`. You can use either one of them to setup IPsec VPNs but not both at the same time.
+
+  `services.strongswan-swanctl` uses the [swanctl](https://wiki.strongswan.org/projects/strongswan/wiki/swanctl) command which uses the modern [vici](https://github.com/strongswan/strongswan/blob/master/src/libcharon/plugins/vici/README.md) _Versatile IKE Configuration Interface_. The deprecated `ipsec` command used in `services.strongswan` is using the legacy [stroke configuration interface](https://github.com/strongswan/strongswan/blob/master/README_LEGACY.md).
+
+- The new `services.elasticsearch-curator` service periodically curates or manages, your Elasticsearch indices and snapshots.
+
+Every new services:
+
+- `./config/xdg/autostart.nix`
+
+- `./config/xdg/icons.nix`
+
+- `./config/xdg/menus.nix`
+
+- `./config/xdg/mime.nix`
+
+- `./hardware/brightnessctl.nix`
+
+- `./hardware/onlykey.nix`
+
+- `./hardware/video/uvcvideo/default.nix`
+
+- `./misc/documentation.nix`
+
+- `./programs/firejail.nix`
+
+- `./programs/iftop.nix`
+
+- `./programs/sedutil.nix`
+
+- `./programs/singularity.nix`
+
+- `./programs/xss-lock.nix`
+
+- `./programs/zsh/zsh-autosuggestions.nix`
+
+- `./services/admin/oxidized.nix`
+
+- `./services/backup/duplicati.nix`
+
+- `./services/backup/restic.nix`
+
+- `./services/backup/restic-rest-server.nix`
+
+- `./services/cluster/hadoop/default.nix`
+
+- `./services/databases/aerospike.nix`
+
+- `./services/databases/monetdb.nix`
+
+- `./services/desktops/bamf.nix`
+
+- `./services/desktops/flatpak.nix`
+
+- `./services/desktops/zeitgeist.nix`
+
+- `./services/development/bloop.nix`
+
+- `./services/development/jupyter/default.nix`
+
+- `./services/hardware/lcd.nix`
+
+- `./services/hardware/undervolt.nix`
+
+- `./services/misc/clipmenu.nix`
+
+- `./services/misc/gitweb.nix`
+
+- `./services/misc/serviio.nix`
+
+- `./services/misc/safeeyes.nix`
+
+- `./services/misc/sysprof.nix`
+
+- `./services/misc/weechat.nix`
+
+- `./services/monitoring/datadog-agent.nix`
+
+- `./services/monitoring/incron.nix`
+
+- `./services/networking/dnsdist.nix`
+
+- `./services/networking/freeradius.nix`
+
+- `./services/networking/hans.nix`
+
+- `./services/networking/morty.nix`
+
+- `./services/networking/ndppd.nix`
+
+- `./services/networking/ocserv.nix`
+
+- `./services/networking/owamp.nix`
+
+- `./services/networking/quagga.nix`
+
+- `./services/networking/shadowsocks.nix`
+
+- `./services/networking/stubby.nix`
+
+- `./services/networking/zeronet.nix`
+
+- `./services/security/certmgr.nix`
+
+- `./services/security/cfssl.nix`
+
+- `./services/security/oauth2_proxy_nginx.nix`
+
+- `./services/web-apps/virtlyst.nix`
+
+- `./services/web-apps/youtrack.nix`
+
+- `./services/web-servers/hitch/default.nix`
+
+- `./services/web-servers/hydron.nix`
+
+- `./services/web-servers/meguca.nix`
+
+- `./services/web-servers/nginx/gitweb.nix`
+
+- `./virtualisation/kvmgt.nix`
+
+- `./virtualisation/qemu-guest-agent.nix`
+
+## Backward Incompatibilities {#sec-release-18.09-incompatibilities}
+
+When upgrading from a previous release, please be aware of the following incompatible changes:
+
+- Some licenses that were incorrectly not marked as unfree now are. This is the case for:
+
+  - cc-by-nc-sa-20: Creative Commons Attribution Non Commercial Share Alike 2.0
+
+  - cc-by-nc-sa-25: Creative Commons Attribution Non Commercial Share Alike 2.5
+
+  - cc-by-nc-sa-30: Creative Commons Attribution Non Commercial Share Alike 3.0
+
+  - cc-by-nc-sa-40: Creative Commons Attribution Non Commercial Share Alike 4.0
+
+  - cc-by-nd-30: Creative Commons Attribution-No Derivative Works v3.00
+
+  - msrla: Microsoft Research License Agreement
+
+- The deprecated `services.cassandra` module has seen a complete rewrite. (See above.)
+
+- `lib.strict` is removed. Use `builtins.seq` instead.
+
+- 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 `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`.
+
+- `services.munge` now runs as user (and group) `munge` instead of root. Make sure the key file is accessible to the daemon.
+
+- `dockerTools.buildImage` now uses `null` as default value for `tag`, which indicates that the nix output hash will be used as tag.
+
+- The ELK stack: `elasticsearch`, `logstash` and `kibana` has been upgraded from 2.\* to 6.3.\*. The 2.\* versions have been [unsupported since last year](https://www.elastic.co/support/eol) so they have been removed. You can still use the 5.\* versions under the names `elasticsearch5`, `logstash5` and `kibana5`.
+
+  The elastic beats: `filebeat`, `heartbeat`, `metricbeat` and `packetbeat` have had the same treatment: they now target 6.3.\* as well. The 5.\* versions are available under the names: `filebeat5`, `heartbeat5`, `metricbeat5` and `packetbeat5`
+
+  The ELK-6.3 stack now comes with [X-Pack by default](https://www.elastic.co/products/x-pack/open). Since X-Pack is licensed under the [Elastic License](https://github.com/elastic/elasticsearch/blob/master/licenses/ELASTIC-LICENSE.txt) the ELK packages now have an unfree license. To use them you need to specify `allowUnfree = true;` in your nixpkgs configuration.
+
+  Fortunately there is also a free variant of the ELK stack without X-Pack. The packages are available under the names: `elasticsearch-oss`, `logstash-oss` and `kibana-oss`.
+
+- Options `boot.initrd.luks.devices.name.yubikey.ramfsMountPoint` `boot.initrd.luks.devices.name.yubikey.storage.mountPoint` were removed. `luksroot.nix` 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.
+
+- `stdenv.system` and `system` 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 `stdenv.buildPlatform.system` can be used instead for the old behavior. They should be using that anyways for clarity.
+
+- Groups `kvm` and `render` are introduced now, as systemd requires them.
+
+## Other Notable Changes {#sec-release-18.09-notable-changes}
+
+- `dockerTools.pullImage` relies on image digest instead of image tag to download the image. The `sha256` of a pulled image has to be updated.
+
+- `lib.attrNamesToStr` has been deprecated. Use more specific concatenation (`lib.concat(Map)StringsSep`) instead.
+
+- `lib.addErrorContextToAttrs` has been deprecated. Use `builtins.addErrorContext` directly.
+
+- `lib.showVal` has been deprecated. Use `lib.traceSeqN` instead.
+
+- `lib.traceXMLVal` has been deprecated. Use `lib.traceValFn builtins.toXml` instead.
+
+- `lib.traceXMLValMarked` has been deprecated. Use `lib.traceValFn (x: str + builtins.toXML x)` instead.
+
+- The `pkgs` argument to NixOS modules can now be set directly using `nixpkgs.pkgs`. Previously, only the `system`, `config` and `overlays` arguments could be used to influence `pkgs`.
+
+- A NixOS system can now be constructed more easily based on a preexisting invocation of Nixpkgs. For example:
+
+  ```nix
+  {
+    inherit (pkgs.nixos {
+      boot.loader.grub.enable = false;
+      fileSystems."/".device = "/dev/xvda1";
+    }) toplevel kernel initialRamdisk manual;
+  }
+  ```
+
+  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.
+
+- `lib.traceValIfNot` has been deprecated. Use `if/then/else` and `lib.traceValSeq` instead.
+
+- `lib.traceCallXml` has been deprecated. Please complain if you use the function regularly.
+
+- The attribute `lib.nixpkgsVersion` has been deprecated in favor of `lib.version`. Please refer to the discussion in [NixOS/nixpkgs\#39416](https://github.com/NixOS/nixpkgs/pull/39416#discussion_r183845745) for further reference.
+
+- `lib.recursiveUpdateUntil` was not acting according to its specification. It has been fixed to act according to the docstring, and a test has been added.
+
+- The module for `security.dhparams` has two new options now:
+
+  `security.dhparams.stateless`
+
+  : Puts the generated Diffie-Hellman parameters into the Nix store instead of managing them in a stateful manner in `/var/lib/dhparams`.
+
+  `security.dhparams.defaultBitSize`
+
+  : The default bit size to use for the generated Diffie-Hellman parameters.
+
+  ::: {.note}
+  The path to the actual generated parameter files should now be queried using `config.security.dhparams.params.name.path` because it might be either in the Nix store or in a directory configured by `security.dhparams.path`.
+  :::
+
+  ::: {.note}
+  **For developers:**
+
+  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).
+
+  An example usage of this would be:
+
+  ```nix
+  { config, ... }:
+
+  {
+    security.dhparams.params.myservice = {};
+    environment.etc."myservice.conf".text = ''
+      dhparams = ${config.security.dhparams.params.myservice.path}
+    '';
+  }
+  ```
+
+  :::
+
+- `networking.networkmanager.useDnsmasq` has been deprecated. Use `networking.networkmanager.dns` instead.
+
+- The Kubernetes package has been bumped to major version 1.11. Please consult the [release notes](https://github.com/kubernetes/kubernetes/blob/release-1.11/CHANGELOG-1.11.md) for details on new features and api changes.
+
+- The option `services.kubernetes.apiserver.admissionControl` was renamed to `services.kubernetes.apiserver.enableAdmissionPlugins`.
+
+- 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.
+
+- The option `services.kubernetes.apiserver.address` was renamed to `services.kubernetes.apiserver.bindAddress`. Note that the default value has changed from 127.0.0.1 to 0.0.0.0.
+
+- The option `services.kubernetes.apiserver.publicAddress` was not used and thus has been removed.
+
+- The option `services.kubernetes.addons.dashboard.enableRBAC` was renamed to `services.kubernetes.addons.dashboard.rbac.enable`.
+
+- 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 module `services.networking.hostapd` now uses WPA2 by default.
+
+- `s6Dns`, `s6Networking`, `s6LinuxUtils` and `s6PortableUtils` renamed to `s6-dns`, `s6-networking`, `s6-linux-utils` and `s6-portable-utils` respectively.
+
+- The module option `nix.useSandbox` is now defaulted to `true`.
+
+- The config activation script of `nixos-rebuild` now [reloads](https://www.freedesktop.org/software/systemd/man/systemctl.html#Manager%20Lifecycle%20Commands) all user units for each authenticated user.
+
+- 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.
+
+- 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
new file mode 100644
index 00000000000..7637a70c1bf
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-1903.section.md
@@ -0,0 +1,214 @@
+# Release 19.03 ("Koi", 2019/04/11) {#sec-release-19.03}
+
+## Highlights {#sec-release-19.03-highlights}
+
+In addition to numerous new and upgraded packages, this release has the following highlights:
+
+- End of support is planned for end of October 2019, handing over to 19.09.
+
+- The default Python 3 interpreter is now CPython 3.7 instead of CPython 3.6.
+
+- 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.
+  :::
+
+  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.
+
+- There is now a set of `confinement` options for `systemd.services`, which allows to restrict services into a chroot 2 ed environment that only contains the store paths from the runtime closure of the service.
+
+## New Services {#sec-release-19.03-new-services}
+
+The following new services were added since the last release:
+
+- `./programs/nm-applet.nix`
+
+- There is a new `security.googleOsLogin` module for using [OS Login](https://cloud.google.com/compute/docs/instances/managing-instance-access) to manage SSH access to Google Compute Engine instances, which supersedes the imperative and broken `google-accounts-daemon` used in `nixos/modules/virtualisation/google-compute-config.nix`.
+
+- `./services/misc/beanstalkd.nix`
+
+- There is a new `services.cockroachdb` module for running CockroachDB databases. NixOS now ships with CockroachDB 2.1.x as well, available on `x86_64-linux` and `aarch64-linux`.
+
+- `./security/duosec.nix`
+
+- The [PAM module for Duo Security](https://duo.com/docs/duounix) has been enabled for use. One can configure it using the `security.duosec` options along with the corresponding PAM option in `security.pam.services.<name?>.duoSecurity.enable`.
+
+## Backward Incompatibilities {#sec-release-19.03-incompatibilities}
+
+When upgrading from a previous release, please be aware of the following incompatible changes:
+
+- The minimum version of Nix required to evaluate Nixpkgs is now 2.0.
+
+  - For users of NixOS 18.03 and 19.03, NixOS defaults to Nix 2.0, but supports using Nix 1.11 by setting `nix.package = pkgs.nix1;`. 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.
+
+  - For users of NixOS 17.09, you will first need to upgrade Nix by setting `nix.package = pkgs.nixStable2;` and run `nixos-rebuild switch` as the `root` user.
+
+  - For users of a daemon-less Nix installation on Linux or macOS, you can upgrade Nix by running `curl -L https://nixos.org/nix/install | sh`, or prior to doing a channel update, running `nix-env -iA nix`. 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.
+
+  - For users of the Nix daemon on macOS, you can upgrade Nix by running `sudo -i sh -c 'nix-channel --update && nix-env -iA nixpkgs.nix'; sudo launchctl stop org.nixos.nix-daemon; sudo launchctl start org.nixos.nix-daemon`.
+
+- The `buildPythonPackage` function now sets `strictDeps = true` to help distinguish between native and non-native dependencies in order to improve cross-compilation compatibility. Note however that this may break user expressions.
+
+- The `buildPythonPackage` function now sets `LANG = C.UTF-8` to enable Unicode support. The `glibcLocales` package is no longer needed as a build input.
+
+- 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 `[]`.
+
+- Package `rabbitmq_server` is renamed to `rabbitmq-server`.
+
+- The `light` module no longer uses setuid binaries, but udev rules. As a consequence users of that module have to belong to the `video` group in order to use the executable (i.e. `users.users.yourusername.extraGroups = ["video"];`).
+
+- Buildbot now supports Python 3 and its packages have been moved to `pythonPackages`. The options `services.buildbot-master.package` and `services.buildbot-worker.package` can be used to select the Python 2 or 3 version of the package.
+
+- Options `services.znc.confOptions.networks.name.userName` and `services.znc.confOptions.networks.name.modulePackages` were removed. They were never used for anything and can therefore safely be removed.
+
+- Package `wasm` has been renamed `proglodyte-wasm`. The package `wasm` will be pointed to `ocamlPackages.wasm` in 19.09, so make sure to update your configuration if you want to keep `proglodyte-wasm`
+
+- When the `nixpkgs.pkgs` option is set, NixOS will no longer ignore the `nixpkgs.overlays` option. The old behavior can be recovered by setting `nixpkgs.overlays = lib.mkForce [];`.
+
+- OpenSMTPD has been upgraded to version 6.4.0p1. This release makes backwards-incompatible changes to the configuration file format. See `man smtpd.conf` for more information on the new file format.
+
+- The versioned `postgresql` have been renamed to use underscore number seperators. For example, `postgresql96` has been renamed to `postgresql_9_6`.
+
+- Package `consul-ui` and passthrough `consul.ui` have been removed. The package `consul` now uses upstream releases that vendor the UI into the binary. See [\#48714](https://github.com/NixOS/nixpkgs/pull/48714#issuecomment-433454834) for details.
+
+- Slurm introduces the new option `services.slurm.stateSaveLocation`, which is now set to `/var/spool/slurm` by default (instead of `/var/spool`). Make sure to move all files to the new directory or to set the option accordingly.
+
+  The slurmctld now runs as user `slurm` instead of `root`. If you want to keep slurmctld running as `root`, set `services.slurm.user = root`.
+
+  The options `services.slurm.nodeName` and `services.slurm.partitionName` are now sets of strings to correctly reflect that fact that each of these options can occour more than once in the configuration.
+
+- The `solr` package has been upgraded from 4.10.3 to 7.5.0 and has undergone some major changes. The `services.solr` module has been updated to reflect these changes. Please review http://lucene.apache.org/solr/ carefully before upgrading.
+
+- Package `ckb` is renamed to `ckb-next`, and options `hardware.ckb.*` are renamed to `hardware.ckb-next.*`.
+
+- 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.
+
+- 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.
+
+  ```nix
+  {
+    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
+    '';
+  }
+  ```
+
+  See [\#50316](https://github.com/NixOS/nixpkgs/pull/50316) for details.
+
+- GitLab Shell previously used the nix store paths for the `gitlab-shell` command in its `authorized_keys` file, which might stop working after garbage collection. To circumvent that, we regenerated that file on each startup. As `gitlab-shell` has now been changed to use `/var/run/current-system/sw/bin/gitlab-shell`, this is not necessary anymore, but there might be leftover lines with a nix store path. Regenerate the `authorized_keys` file via `sudo -u git -H gitlab-rake gitlab:shell:setup` in that case.
+
+- 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.
+
+- `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.
+
+- The ibus-table input method has had a change in config format, which causes all previous settings to be lost. See [this commit message](https://github.com/mike-fabian/ibus-table/commit/f9195f877c5212fef0dfa446acb328c45ba5852b) for details.
+
+- 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.
+
+- `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.
+
+- `graylog` 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 [Upgrading Graylog](http://docs.graylog.org/en/3.0/pages/upgrade/graylog-3.0.html#simplified-http-interface-configuration) for details.
+
+- The option `users.ldap.bind.password` was renamed to `users.ldap.bind.passwordFile`, and needs to be readable by the `nslcd` user. Same applies to the new `users.ldap.daemon.rootpwmodpwFile` option.
+
+- `nodejs-6_x` is end-of-life. `nodejs-6_x`, `nodejs-slim-6_x` and `nodePackages_6_x` are removed.
+
+## Other Notable Changes {#sec-release-19.03-notable-changes}
+
+- The `services.matomo` module gained the option `services.matomo.package` which determines the used Matomo version.
+
+  The Matomo module now also 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`.
+
+  Additionally, you can 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.
+
+- `composableDerivation` along with supporting library functions has been removed.
+
+- The deprecated `truecrypt` package has been removed and `truecrypt` attribute is now an alias for `veracrypt`. VeraCrypt is backward-compatible with TrueCrypt volumes. Note that `cryptsetup` also supports loading TrueCrypt volumes.
+
+- 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 [Kubernetes v1.11](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG-1.11.md#sig-cluster-lifecycle). 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: `services.kubernetes.addons.dns.replicas`.
+
+- The quassel-webserver package and module was removed from nixpkgs due to the lack of maintainers.
+
+- The manual gained a [ new chapter on self-hosting `matrix-synapse` and `riot-web` ](#module-services-matrix), the most prevalent server and client implementations for the [Matrix](https://matrix.org/) federated communication network.
+
+- The astah-community package was removed from nixpkgs due to it being discontinued and the downloads not being available anymore.
+
+- The httpd service now saves log files with a .log file extension by default for easier integration with the logrotate service.
+
+- The owncloud server packages and httpd subservice module were removed from nixpkgs due to the lack of maintainers.
+
+- 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 `zramSwap.swapDevices` explicitly.
+
+  ZRAM algorithm can be changed now.
+
+  Changes to ZRAM algorithm are applied during `nixos-rebuild switch`, so make sure you have enough swap space on disk to survive ZRAM device rebuild. Alternatively, use `nixos-rebuild boot; reboot`.
+
+- Flat volumes are now disabled by default in `hardware.pulseaudio`. 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.
+
+  ::: {.note}
+  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.
+
+- 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.
+
+- The [Grafana module](options.html#opt-services.grafana.enable) now supports declarative [datasource and dashboard](http://docs.grafana.org/administration/provisioning/) provisioning.
+
+- The use of insecure ports on kubernetes has been deprecated. Thus options: `services.kubernetes.apiserver.port` and `services.kubernetes.controllerManager.port` has been renamed to `.insecurePort`, and default of both options has changed to 0 (disabled).
+
+- Note that the default value of `services.kubernetes.apiserver.bindAddress` 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: `services.kubernetes.apiserver.insecurebindAddress`.
+
+- The option `services.kubernetes.apiserver.allowPrivileged` and `services.kubernetes.kubelet.allowPrivileged` now defaults to false. Disallowing privileged containers on the cluster.
+
+- The kubernetes module does no longer add the kubernetes package to `environment.systemPackages` implicitly.
+
+- The `intel` driver has been removed from the default list of [X.org video drivers](options.html#opt-services.xserver.videoDrivers). The `modesetting` 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 `xrandr`. 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 `modesetting` driver, too). Kernel mode setting API does not support backlight control, so `xbacklight` tool will not work; backlight level can be controlled directly via `/sys/` or with `brightnessctl`. 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)
+
+- 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 [here](https://www.open-mpi.org/faq/?category=mpi-removed).
+
+  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 [services.nginx.sslProtocols](options.html#opt-services.nginx.sslProtocols).
+
+- A new subcommand `nixos-rebuild edit` was added.
diff --git a/nixos/doc/manual/release-notes/rl-1909.section.md b/nixos/doc/manual/release-notes/rl-1909.section.md
new file mode 100644
index 00000000000..572f1bf5a25
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-1909.section.md
@@ -0,0 +1,313 @@
+# Release 19.09 ("Loris", 2019/10/09) {#sec-release-19.09}
+
+## Highlights {#sec-release-19.09-highlights}
+
+In addition to numerous new and upgraded packages, this release has the following highlights:
+
+- End of support is planned for end of April 2020, handing over to 20.03.
+
+- Nix has been updated to 2.3; see its [release notes](https://nixos.org/nix/manual/#ssec-relnotes-2.3).
+
+- Core version changes:
+
+  systemd: 239 -\> 243
+
+  gcc: 7 -\> 8
+
+  glibc: 2.27 (unchanged)
+
+  linux: 4.19 LTS (unchanged)
+
+  openssl: 1.0 -\> 1.1
+
+- Desktop version changes:
+
+  plasma5: 5.14 -\> 5.16
+
+  gnome3: 3.30 -\> 3.32
+
+- PHP now defaults to PHP 7.3, updated from 7.2.
+
+- PHP 7.1 is no longer supported due to upstream not supporting this version for the entire lifecycle of the 19.09 release.
+
+- The binfmt module is now easier to use. Additional systems can be added through `boot.binfmt.emulatedSystems`. For instance, `boot.binfmt.emulatedSystems = [ "wasm32-wasi" "x86_64-windows" "aarch64-linux" ];` will set up binfmt interpreters for each of those listed systems.
+
+- 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.
+
+- The GNOME 3 desktop manager module sports an interface to enable/disable core services, applications, and optional GNOME packages like games.
+
+  - `services.gnome3.core-os-services.enable`
+
+  - `services.gnome3.core-shell.enable`
+
+  - `services.gnome3.core-utilities.enable`
+
+  - `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.
+
+- 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`**
+
+  - `accerciser`
+
+  - `dconf-editor`
+
+  - `evolution`
+
+  - `gnome-documents`
+
+  - `gnome-nettool`
+
+  - `gnome-power-manager`
+
+  - `gnome-todo`
+
+  - `gnome-tweaks`
+
+  - `gnome-usage`
+
+  - `gucharmap`
+
+  - `nautilus-sendto`
+
+  - `vinagre`
+
+  - `cheese`
+
+  - `geary`
+
+  **The following changes were enacted in `services.gnome3.core-shell.enable`**
+
+  - `gnome-color-manager`
+
+  - `orca`
+
+  - `services.avahi.enable`
+
+## New Services {#sec-release-19.09-new-services}
+
+The following new services were added since the last release:
+
+- `./programs/dwm-status.nix`
+
+- The new `hardware.printers` module allows to declaratively configure CUPS printers via the `ensurePrinters` and `ensureDefaultPrinter` options. `ensurePrinters` will never delete existing printers, but will make sure that the given printers are configured as declared.
+
+- There is a new [services.system-config-printer.enable](options.html#opt-services.system-config-printer.enable) and [programs.system-config-printer.enable](options.html#opt-programs.system-config-printer.enable) module for the program of the same name. If you previously had `system-config-printer` enabled through some other means you should migrate to using one of these modules.
+
+  - `services.xserver.desktopManager.plasma5`
+
+  - `services.xserver.desktopManager.gnome3`
+
+  - `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.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.
+
+## Backward Incompatibilities {#sec-release-19.09-incompatibilities}
+
+When upgrading from a previous release, please be aware of the following incompatible changes:
+
+- 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.
+
+- PostgreSQL now uses `/run/postgresql` as its socket directory instead of `/tmp`. 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.
+
+- 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 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.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.
+
+- The `services.emby.enable` module has been removed, see `services.jellyfin.enable` instead for a free software fork of Emby. See the Jellyfin documentation: [ Migrating from Emby to Jellyfin ](https://jellyfin.readthedocs.io/en/latest/administrator-docs/migrate-from-emby/)
+
+- IPv6 Privacy Extensions are now enabled by default for undeclared interfaces. The previous behaviour was quite misleading --- even though the default value for `networking.interfaces.*.preferTempAddress` was `true`, 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 `false` for the interface in question.
+
+- Since Bittorrent Sync was superseded by Resilio Sync in 2016, the `bittorrentSync`, `bittorrentSync14`, and `bittorrentSync16` packages have been removed in favor of `resilio-sync`.
+
+  The corresponding module, `services.btsync` has been replaced by the `services.resilio` module.
+
+- 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: `systemd.services.httpd.after = [ "postgresql.service" ];`
+
+  The option `services.httpd.extraSubservices` 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.
+
+  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 `services.limesurvey.enable`, `services.mediawiki.enable`, `services.wordpress.enable`, and `services.zabbixWeb.enable` options.
+
+- The option `systemd.network.networks.<name>.routes.*.routeConfig.GatewayOnlink` was renamed to `systemd.network.networks.<name>.routes.*.routeConfig.GatewayOnLink` (capital `L`). This follows [ upstreams renaming ](https://github.com/systemd/systemd/commit/9cb8c5593443d24c19e40bfd4fc06d672f8c554c) of the setting.
+
+- As of this release the NixOps feature `autoLuks` is deprecated. It no longer works with our systemd version without manual intervention.
+
+  Whenever the usage of the module is detected the evaluation will fail with a message explaining why and how to deal with the situation.
+
+  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.
+
+  If you are actively using the `autoLuks` module please let us know in [issue \#62211](https://github.com/NixOS/nixpkgs/issues/62211).
+
+- The setopt declarations will be evaluated at the end of `/etc/zshrc`, so any code in [programs.zsh.interactiveShellInit](options.html#opt-programs.zsh.interactiveShellInit), [programs.zsh.loginShellInit](options.html#opt-programs.zsh.loginShellInit) and [programs.zsh.promptInit](options.html#opt-programs.zsh.promptInit) may break if it relies on those options being set.
+
+- The `prometheus-nginx-exporter` package now uses the offical 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 [official repo](https://github.com/nginxinc/nginx-prometheus-exporter).
+
+- The `shibboleth-sp` package has been updated to version 3. It is largely backward compatible, for further information refer to the [release notes](https://wiki.shibboleth.net/confluence/display/SP3/ReleaseNotes) and [upgrade guide](https://wiki.shibboleth.net/confluence/display/SP3/UpgradingFromV2).
+
+  Nodejs 8 is scheduled EOL under the lifetime of 19.09 and has been dropped.
+
+- By default, prometheus exporters are now run with `DynamicUser` enabled. Exporters that need a real user, now run under a seperate user and group which follow the pattern `<exporter-name>-exporter`, instead of the previous default `nobody` and `nogroup`. Only some exporters are affected by the latter, namely the exporters `dovecot`, `node`, `postfix` and `varnish`.
+
+- The `ibus-qt` package is not installed by default anymore when [i18n.inputMethod.enabled](options.html#opt-i18n.inputMethod.enabled) is set to `ibus`. If IBus support in Qt 4.x applications is required, add the `ibus-qt` package to your [environment.systemPackages](options.html#opt-environment.systemPackages) manually.
+
+- The CUPS Printing service now uses socket-based activation by default, only starting when needed. The previous behavior can be restored by setting `services.cups.startWhenNeeded` to `false`.
+
+- The `services.systemhealth` module has been removed from nixpkgs due to lack of maintainer.
+
+- The `services.mantisbt` module has been removed from nixpkgs due to lack of maintainer.
+
+- Squid 3 has been removed and the `squid` derivation now refers to Squid 4.
+
+- The `services.pdns-recursor.extraConfig` option has been replaced by `services.pdns-recursor.settings`. The new option allows setting extra configuration while being better type-checked and mergeable.
+
+- No service depends on `keys.target` anymore which is a systemd target that indicates if all [NixOps keys](https://nixos.org/nixops/manual/#idm140737322342384) were successfully uploaded. Instead, `<key-name>-key.service` should be used to define a dependency of a key in a service. The full issue behind the `keys.target` dependency is described at [NixOS/nixpkgs\#67265](https://github.com/NixOS/nixpkgs/issues/67265).
+
+  The following services are affected by this:
+
+  - [`services.dovecot2`](options.html#opt-services.dovecot2.enable)
+
+  - [`services.nsd`](options.html#opt-services.nsd.enable)
+
+  - [`services.softether`](options.html#opt-services.softether.enable)
+
+  - [`services.strongswan`](options.html#opt-services.strongswan.enable)
+
+  - [`services.strongswan-swanctl`](options.html#opt-services.strongswan-swanctl.enable)
+
+  - [`services.httpd`](options.html#opt-services.httpd.enable)
+
+- The `security.acme.directory` option has been replaced by a read-only `security.acme.certs.<cert>.directory` option for each certificate you define. This will be a subdirectory of `/var/lib/acme`. You can use this read-only option to figure out where the certificates are stored for a specific certificate. For example, the `services.nginx.virtualhosts.<name>.enableACME` option will use this directory option to find the certs for the virtual host.
+
+  `security.acme.preDelay` and `security.acme.activationDelay` options have been removed. To execute a service before certificates are provisioned or renewed add a `RequiredBy=acme-${cert}.service` to any service.
+
+  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).
+
+- 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.
+
+- 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.
+
+- 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 `services.prometheus`.
+
+- 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 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`.
+
+- The `networking.useDHCP` option is unsupported in combination with `networking.useNetworkd` in anticipation of defaulting to it. It has to be set to `false` and enabled per interface with `networking.interfaces.<name>.useDHCP = true;`
+
+- 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.
+
+- 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.
+
+- Due to the short lifetime of non-LTS kernel releases package attributes like `linux_5_1`, `linux_5_2` and `linux_5_3` have been removed to discourage dependence on specific non-LTS kernel versions in stable NixOS releases. Going forward, versioned attributes like `linux_4_9` will exist for LTS versions only. Please use `linux_latest` or `linux_testing` if you depend on non-LTS releases. Keep in mind that `linux_latest` and `linux_testing` will change versions under the hood during the lifetime of a stable release and might include breaking changes.
+
+- Because of the systemd upgrade, some network interfaces might change their name. For details see [ upstream docs](https://www.freedesktop.org/software/systemd/man/systemd.net-naming-scheme.html#History) or [ our ticket](https://github.com/NixOS/nixpkgs/issues/71086).
+
+## Other Notable Changes {#sec-release-19.09-notable-changes}
+
+- The `documentation` module gained an option named `documentation.nixos.includeAllModules` which makes the generated configuration.nix 5 manual page include all options from all NixOS modules included in a given `configuration.nix` configuration file. Currently, it is set to `false` by default as enabling it frequently prevents evaluation. But the plan is to eventually have it set to `true` by default. Please set it to `true` now in your `configuration.nix` and fix all the bugs it uncovers.
+
+- The `vlc` package gained support for Chromecast streaming, enabled by default. TCP port 8010 must be open for it to work, so something like `networking.firewall.allowedTCPPorts = [ 8010 ];` may be required in your configuration. Also consider enabling [ Accelerated Video Playback](https://nixos.wiki/wiki/Accelerated_Video_Playback) for better transcoding performance.
+
+- The following changes apply if the `stateVersion` is changed to 19.09 or higher. For `stateVersion = "19.03"` or lower the old behavior is preserved.
+
+  - `solr.package` defaults to `pkgs.solr_8`.
+
+- The `hunspellDicts.fr-any` dictionary now ships with `fr_FR.{aff,dic}` which is linked to `fr-toutesvariantes.{aff,dic}`.
+
+- The `mysql` service now runs as `mysql` user. Previously, systemd did execute it as root, and mysql dropped privileges itself. This includes `ExecStartPre=` and `ExecStartPost=` phases. To accomplish that, runtime and data directory setup was delegated to RuntimeDirectory and tmpfiles.
+
+- With the upgrade to systemd version 242 the `systemd-timesyncd` service is no longer using `DynamicUser=yes`. 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 `19.09`) was `/var/lib/private/systemd/timesync`.
+
+  As long as the `system.config.stateVersion` is below `19.09` the state folder will migrated to its proper location (`/var/lib/systemd/timesync`), if required.
+
+- The package `avahi` is now built to look up service definitions from `/etc/avahi/services` instead of its output directory in the nix store. Accordingly the module `avahi` now supports custom service definitions via `services.avahi.extraServiceFiles`, which are then placed in the aforementioned directory. See avahi.service5 for more information on custom service definitions.
+
+- Since version 0.1.19, `cargo-vendor` honors package includes that are specified in the `Cargo.toml` file of Rust crates. `rustPlatform.buildRustPackage` uses `cargo-vendor` to collect and build dependent crates. Since this change in `cargo-vendor` changes the set of vendored files for most Rust packages, the hash that use used to verify the dependencies, `cargoSha256`, also changes.
+
+  The `cargoSha256` hashes of all in-tree derivations that use `buildRustPackage` have been updated to reflect this change. However, third-party derivations that use `buildRustPackage` may have to be updated as well.
+
+- 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 `phabricator` package and associated `httpd.extraSubservice`, as well as the `phd` service have been removed from nixpkgs due to lack of maintainer.
+
+- The `mercurial` `httpd.extraSubservice` has been removed from nixpkgs due to lack of maintainer.
+
+- The `trac` `httpd.extraSubservice` has been removed from nixpkgs because it was unmaintained.
+
+- The `foswiki` package and associated `httpd.extraSubservice` have been removed from nixpkgs due to lack of maintainer.
+
+- 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`.
+
+- 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.
+
+  Previous solution to this problem was either to use `lib.flip map` idiom or extract that anonymous mapping function to a named one. Both can still be used but `lib.forEach` is preferred over `lib.flip map`.
+
+  The `/etc/sysctl.d/nixos.conf` file containing all the options set via [boot.kernel.sysctl](options.html#opt-boot.kernel.sysctl) was moved to `/etc/sysctl.d/60-nixos.conf`, as sysctl.d5 recommends prefixing all filenames in `/etc/sysctl.d` with a two-digit number and a dash to simplify the ordering of the files.
+
+- We now install the sysctl snippets shipped with systemd.
+
+  - Loose reverse path filtering
+
+  - Source route filtering
+
+  - `fq_codel` as a packet scheduler (this helps to fight bufferbloat)
+
+  This also configures the kernel to pass core dumps to `systemd-coredump`, and restricts the SysRq key combinations to the sync command only. These sysctl snippets can be found in `/etc/sysctl.d/50-*.conf`, and overridden via [boot.kernel.sysctl](options.html#opt-boot.kernel.sysctl) (which will place the parameters in `/etc/sysctl.d/60-nixos.conf`).
+
+- Core dumps are now processed by `systemd-coredump` by default. `systemd-coredump` behaviour can still be modified via `systemd.coredump.extraConfig`. To stick to the old behaviour (having the kernel dump to a file called `core` in the working directory), without piping it through `systemd-coredump`, set `systemd.coredump.enable` to `false`.
+
+- `systemd.packages` option now also supports generators and shutdown scripts. Old `systemd.generator-packages` option has been removed.
+
+- The `rmilter` package was removed with associated module and options due deprecation by upstream developer. Use `rspamd` in proxy mode instead.
+
+- systemd cgroup accounting via the [systemd.enableCgroupAccounting](options.html#opt-systemd.enableCgroupAccounting) option is now enabled by default. It now also enables the more recent Block IO and IP accounting features.
+
+- We no longer enable custom font rendering settings with `fonts.fontconfig.penultimate.enable` by default. The defaults from fontconfig are sufficient.
+
+- The `crashplan` package and the `crashplan` service have been removed from nixpkgs due to crashplan shutting down the service, while the `crashplansb` package and `crashplan-small-business` service have been removed from nixpkgs due to lack of maintainer.
+
+  The [redis module](options.html#opt-services.redis.enable) was hardcoded to use the `redis` user, `/run/redis` as runtime directory and `/var/lib/redis` 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).
+
+- Using `fonts.enableDefaultFonts` adds a default emoji font `noto-fonts-emoji`.
+
+  - `services.xserver.enable`
+
+  - `programs.sway.enable`
+
+  - `programs.way-cooler.enable`
+
+  - `services.xrdp.enable`
+
+- 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.
+
+- `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
new file mode 100644
index 00000000000..b92c7f6634c
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-2003.section.md
@@ -0,0 +1,507 @@
+# Release 20.03 ("Markhor", 2020.04/20) {#sec-release-20.03}
+
+## Highlights {#sec-release-20.03-highlights}
+
+In addition to numerous new and upgraded packages, this release has the following highlights:
+
+- Support is planned until the end of October 2020, handing over to 20.09.
+
+- Core version changes:
+
+  gcc: 8.3.0 -\> 9.2.0
+
+  glibc: 2.27 -\> 2.30
+
+  linux: 4.19 -\> 5.4
+
+  mesa: 19.1.5 -\> 19.3.3
+
+  openssl: 1.0.2u -\> 1.1.1d
+
+- Desktop version changes:
+
+  plasma5: 5.16.5 -\> 5.17.5
+
+  kdeApplications: 19.08.2 -\> 19.12.3
+
+  gnome3: 3.32 -\> 3.34
+
+  pantheon: 5.0 -\> 5.1.3
+
+- Linux kernel is updated to branch 5.4 by default (from 4.19).
+
+- 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 [this discussion](https://github.com/NixOS/nixpkgs/issues/61718#issuecomment-617618503).
+
+- 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.
+
+- 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.
+
+- 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`.
+
+- `nixos-option` has been rewritten in C++, speeding it up, improving correctness, and adding a `-r` option which prints all options and their values recursively.
+
+- `services.xserver.desktopManager.default` and `services.xserver.windowManager.default` options were replaced by a single [services.xserver.displayManager.defaultSession](options.html#opt-services.xserver.displayManager.defaultSession) option to improve support for upstream session files. If you used something like:
+
+  ```nix
+  {
+    services.xserver.desktopManager.default = "xfce";
+    services.xserver.windowManager.default = "icewm";
+  }
+  ```
+
+  you should change it to:
+
+  ```nix
+  {
+    services.xserver.displayManager.defaultSession = "xfce+icewm";
+  }
+  ```
+
+- The testing driver implementation in NixOS is now in Python `make-test-python.nix`. This was done by Jacek Galowicz ([\@tfc](https://github.com/tfc)), and with the collaboration of Julian Stecklina ([\@blitz](https://github.com/blitz)) and Jana Traue ([\@jtraue](https://github.com/jtraue)). 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, `make-test.nix`, 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:
+
+  ```ShellSession
+  $ 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.
+  ```
+
+  API compatibility is planned to be kept for at least the next release with the perl driver.
+
+## New Services {#sec-release-20.03-new-services}
+
+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`.
+
+- 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.
+
+- `./config/console.nix`
+
+- `./hardware/brillo.nix`
+
+- `./hardware/tuxedo-keyboard.nix`
+
+- `./programs/bandwhich.nix`
+
+- `./programs/bash-my-aws.nix`
+
+- `./programs/liboping.nix`
+
+- `./programs/traceroute.nix`
+
+- `./services/backup/sanoid.nix`
+
+- `./services/backup/syncoid.nix`
+
+- `./services/backup/zfs-replication.nix`
+
+- `./services/continuous-integration/buildkite-agents.nix`
+
+- `./services/databases/victoriametrics.nix`
+
+- `./services/desktops/gnome3/gnome-initial-setup.nix`
+
+- `./services/desktops/neard.nix`
+
+- `./services/games/openarena.nix`
+
+- `./services/hardware/fancontrol.nix`
+
+- `./services/mail/sympa.nix`
+
+- `./services/misc/freeswitch.nix`
+
+- `./services/misc/mame.nix`
+
+- `./services/monitoring/do-agent.nix`
+
+- `./services/monitoring/prometheus/xmpp-alerts.nix`
+
+- `./services/network-filesystems/orangefs/server.nix`
+
+- `./services/network-filesystems/orangefs/client.nix`
+
+- `./services/networking/3proxy.nix`
+
+- `./services/networking/corerad.nix`
+
+- `./services/networking/go-shadowsocks2.nix`
+
+- `./services/networking/ntp/openntpd.nix`
+
+- `./services/networking/shorewall.nix`
+
+- `./services/networking/shorewall6.nix`
+
+- `./services/networking/spacecookie.nix`
+
+- `./services/networking/trickster.nix`
+
+- `./services/networking/v2ray.nix`
+
+- `./services/networking/xandikos.nix`
+
+- `./services/networking/yggdrasil.nix`
+
+- `./services/web-apps/dokuwiki.nix`
+
+- `./services/web-apps/gotify-server.nix`
+
+- `./services/web-apps/grocy.nix`
+
+- `./services/web-apps/ihatemoney`
+
+- `./services/web-apps/moinmoin.nix`
+
+- `./services/web-apps/trac.nix`
+
+- `./services/web-apps/trilium.nix`
+
+- `./services/web-apps/shiori.nix`
+
+- `./services/web-servers/ttyd.nix`
+
+- `./services/x11/picom.nix`
+
+- `./services/x11/hardware/digimend.nix`
+
+- `./services/x11/imwheel.nix`
+
+- `./virtualisation/cri-o.nix`
+
+## Backward Incompatibilities {#sec-release-20.03-incompatibilities}
+
+When upgrading from a previous release, please be aware of the following incompatible changes:
+
+- The dhcpcd package [ does not request IPv4 addresses for tap and bridge interfaces anymore by default](https://roy.marples.name/archives/dhcpcd-discuss/0002621.html). In order to still get an address on a bridge interface, one has to disable `networking.useDHCP` and explicitly enable `networking.interfaces.<name>.useDHCP` 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.
+
+- GnuPG is now built without support for a graphical passphrase entry by default. Please enable the `gpg-agent` user service via the NixOS option `programs.gnupg.agent.enable`. Note that upstream recommends using `gpg-agent` and will spawn a `gpg-agent` on the first invocation of GnuPG anyway.
+
+- The `dynamicHosts` option has been removed from the [NetworkManager](options.html#opt-networking.networkmanager.enable) 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 [networking.hosts](options.html#opt-networking.hosts), provide them via the DNS server in your network, or use [environment.etc](options.html#opt-environment.etc) to add a file into `/etc/NetworkManager/dnsmasq.d` reconfiguring `hostsdir`.
+
+- 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.
+
+- 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.
+
+- The Way Cooler wayland compositor has been removed, as the project has been officially canceled. There are no more `way-cooler` attribute and `programs.way-cooler` options.
+
+- 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).
+
+- 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.
+
+- KDE's old multimedia framework Phonon no longer supports Qt 4. For that reason, Plasma desktop also does not have `enableQt4Support` option any more.
+
+- The BeeGFS module has been removed.
+
+- The osquery module has been removed.
+
+- Going forward, `~/bin` in the users home directory will no longer be in `PATH` by default. If you depend on this you should set the option `environment.homeBinInPath` to `true`. The aforementioned option was added this release.
+
+- The `buildRustCrate` infrastructure now produces `lib` outputs in addition to the `out` output. This has led to drastically reduced closure sizes for some rust crates since development dependencies are now in the `lib` output.
+
+- 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 [ upstream issue](https://gitlab.gnome.org/GNOME/pango/issues/386) for more information.
+
+- The `roundcube` module has been hardened.
+
+  - The password of the database is not written world readable in the store any more. If `database.host` is set to `localhost`, 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 `database.passwordFile`, which should be set to the path of a file containing the password and readable by the user `nginx` only. The `database.password` option is insecure and deprecated. Usage of this option will print a warning.
+
+  - A random `des_key` 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.
+
+- 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 fourStore and fourStoreEndpoint modules have been removed.
+
+- 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).
+
+- 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 `<nixos/modules/profiles/minimal.nix>` module is included in container config.
+
+- The `kresd` services deprecates the `interfaces` option in favor of the `listenPlain` option which requires full [systemd.socket compatible](https://www.freedesktop.org/software/systemd/man/systemd.socket.html#ListenStream=) declaration which always include a port.
+
+- Virtual console options have been reorganized and can be found under a single top-level attribute: `console`. The full set of changes is as follows:
+
+  - `i18n.consoleFont` renamed to [console.font](options.html#opt-console.font)
+
+  - `i18n.consoleKeyMap` renamed to [console.keyMap](options.html#opt-console.keyMap)
+
+  - `i18n.consoleColors` renamed to [console.colors](options.html#opt-console.colors)
+
+  - `i18n.consolePackages` renamed to [console.packages](options.html#opt-console.packages)
+
+  - `i18n.consoleUseXkbConfig` renamed to [console.useXkbConfig](options.html#opt-console.useXkbConfig)
+
+  - `boot.earlyVconsoleSetup` renamed to [console.earlySetup](options.html#opt-console.earlySetup)
+
+  - `boot.extraTTYs` renamed to `console.extraTTYs`.
+
+- The [awstats](options.html#opt-services.awstats.enable) module has been rewritten to serve stats via static html pages, updated on a timer, over [nginx](options.html#opt-services.nginx.virtualHosts), instead of dynamic cgi pages over [apache](options.html#opt-services.httpd.enable).
+
+  Minor changes will be required to migrate existing configurations. Details of the required changes can seen by looking through the [awstats](options.html#opt-services.awstats.enable) module.
+
+- The httpd module no longer provides options to support serving web content without defining a virtual host. As a result of this the [services.httpd.logPerVirtualHost](options.html#opt-services.httpd.logPerVirtualHost) option now defaults to `true` instead of `false`. Please update your configuration to make use of [services.httpd.virtualHosts](options.html#opt-services.httpd.virtualHosts).
+
+  The [services.httpd.virtualHosts.\<name\>](options.html#opt-services.httpd.virtualHosts) option has changed type from a list of submodules to an attribute set of submodules, better matching [services.nginx.virtualHosts.\<name\>](options.html#opt-services.nginx.virtualHosts).
+
+  This change comes with the addition of the following options which mimic the functionality of their `nginx` counterparts: [services.httpd.virtualHosts.\<name\>.addSSL](options.html#opt-services.httpd.virtualHosts), [services.httpd.virtualHosts.\<name\>.forceSSL](options.html#opt-services.httpd.virtualHosts), [services.httpd.virtualHosts.\<name\>.onlySSL](options.html#opt-services.httpd.virtualHosts), [services.httpd.virtualHosts.\<name\>.enableACME](options.html#opt-services.httpd.virtualHosts), [services.httpd.virtualHosts.\<name\>.acmeRoot](options.html#opt-services.httpd.virtualHosts), and [services.httpd.virtualHosts.\<name\>.useACMEHost](options.html#opt-services.httpd.virtualHosts).
+
+- For NixOS configuration options, the `loaOf` type has been deprecated and will be removed in a future release. In nixpkgs, options of this type will be changed to `attrsOf` instead. If you were using one of these in your configuration, you will see a warning suggesting what changes will be required.
+
+  For example, [users.users](options.html#opt-users.users) is a `loaOf` option that is commonly used as follows:
+
+  ```nix
+  {
+    users.users =
+      [ { name = "me";
+          description = "My personal user.";
+          isNormalUser = true;
+        }
+      ];
+  }
+  ```
+
+  This should be rewritten by removing the list and using the value of `name` as the name of the attribute set:
+
+  ```nix
+  {
+    users.users.me =
+      { description = "My personal user.";
+        isNormalUser = true;
+      };
+  }
+  ```
+
+  For more information on this change have look at these links: [issue \#1800](https://github.com/NixOS/nixpkgs/issues/1800), [PR \#63103](https://github.com/NixOS/nixpkgs/pull/63103).
+
+- For NixOS modules, the types `types.submodule` and `types.submoduleWith` now support paths as allowed values, similar to how `imports` supports paths. Because of this, if you have a module that defines an option of type `either (submodule ...) path`, it will break since a path is now treated as the first type instead of the second. To fix this, change the type to `either path (submodule ...)`.
+
+- The [Buildkite Agent](options.html#opt-services.buildkite-agents) 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 `services.buildkite-agent` to `services.buildkite-agents.<name>`. Furthermore, the following options have been changed:
+
+  - `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.
+
+  - `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.
+
+  - [services.buildkite-agents.\<name\>.shell](options.html#opt-services.buildkite-agents) has been introduced, allowing to specify a custom shell to be used.
+
+- The `citrix_workspace_19_3_0` package has been removed as it will be EOLed within the lifespan of 20.03. For further information, please refer to the [support and maintenance information](https://www.citrix.com/de-de/support/product-lifecycle/milestones/receiver.html) from upstream.
+
+- 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:
+
+  ```nix
+  {
+    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
+    '';
+  }
+  ```
+
+  The difference is the:
+
+  ```
+  auth required pam_succeed_if.so quiet
+  ```
+
+  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.
+
+- There have been lots of improvements to the Mailman module. As a result,
+
+  - The `services.mailman.hyperkittyBaseUrl` option has been renamed to [services.mailman.hyperkitty.baseUrl](options.html#opt-services.mailman.hyperkitty.baseUrl).
+
+  - The `services.mailman.hyperkittyApiKey` 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 [services.mailman.hyperkitty.enable](options.html#opt-services.mailman.hyperkitty.enable) to `true`.
+
+  - Additionally, some Postfix configuration must now be set manually instead of automatically by the Mailman module:
+
+    ```nix
+    {
+      services.postfix.relayDomains = [ "hash:/var/lib/mailman/data/postfix_domains" ];
+      services.postfix.config.transport_maps = [ "hash:/var/lib/mailman/data/postfix_lmtp" ];
+      services.postfix.config.local_recipient_maps = [ "hash:/var/lib/mailman/data/postfix_lmtp" ];
+    }
+    ```
+
+    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.
+
+- The LLVM versions 3.5, 3.9 and 4 (including the corresponding CLang versions) have been dropped.
+
+- The `networking.interfaces.*.preferTempAddress` option has been replaced by `networking.interfaces.*.tempAddress`. The new option allows better control of the IPv6 temporary addresses, including completely disabling them for interfaces where they are not needed.
+
+- 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 `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
+  {
+    # Example configuration:
+    services.dnscrypt-proxy2.enable = true;
+    services.dnscrypt-proxy2.settings = {
+      listen_addresses = [ "127.0.0.1:43" ];
+      sources.public-resolvers = {
+        urls = [ "https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md" ];
+        cache_file = "public-resolvers.md";
+        minisign_key = "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3";
+        refresh_delay = 72;
+      };
+    };
+
+    services.dnsmasq.enable = true;
+    services.dnsmasq.servers = [ "127.0.0.1#43" ];
+  }
+  ```
+
+- `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).
+
+- 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 nixos-build-vms8 -script now uses the python test-driver.
+
+- 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 `builtins.fromJSON`.
+
+  The new default configuration also disables automatic guest account registration and analytics to improve privacy. The previous behavior can be restored by setting `config.riot-web.conf = { disable_guests = false; piwik = true; }`.
+
+- 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`.
+
+  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).
+
+  - 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.
+  :::
+
+- 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:
+
+    ```nix
+    { pkgs, ... }: {
+      services.hydra.package = pkgs.hydra-migration;
+    }
+    ```
+
+- Automatically fill the newly added ID columns on the server by running the following command:
+
+  ```ShellSession
+  $ hydra-backfill-ids
+  ```
+  ::: {.warning}
+  Please note that this process can take a while depending on your database-size!
+  :::
+
+- 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.
+  :::
+
+  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!
+  :::
+
+- 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/).
+
+## Other Notable Changes {#sec-release-20.03-notable-changes}
+
+- SD images are now compressed by default using `bzip2`.
+
+- The nginx web server previously started its master process as root privileged, then ran worker processes as a less privileged identity user (the `nginx` user). This was changed to start all of nginx as a less privileged user (defined by `services.nginx.user` and `services.nginx.group`). 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.
+
+  To continue to use the old approach, you can configure:
+
+  ```nix
+  {
+    services.nginx.appendConfig = let cfg = config.services.nginx; in ''user ${cfg.user} ${cfg.group};'';
+    systemd.services.nginx.serviceConfig.User = lib.mkForce "root";
+  }
+  ```
+
+- OpenSSH has been upgraded from 7.9 to 8.1, improving security and adding features but with potential incompatibilities. Consult the [ release announcement](https://www.openssh.com/txt/release-8.1) for more information.
+
+- `PRETTY_NAME` in `/etc/os-release` now uses the short rather than full version string.
+
+- The ACME module has switched from simp-le to [lego](https://github.com/go-acme/lego) which allows us to support DNS-01 challenges and wildcard certificates. The following options have been added: [security.acme.acceptTerms](options.html#opt-security.acme.acceptTerms), [security.acme.certs.\<name\>.dnsProvider](options.html#opt-security.acme.certs), [security.acme.certs.\<name\>.credentialsFile](options.html#opt-security.acme.certs), [security.acme.certs.\<name\>.dnsPropagationCheck](options.html#opt-security.acme.certs). As well as this, the options `security.acme.acceptTerms` and either `security.acme.email` or `security.acme.certs.<name>.email` 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.
+
+- It is now possible to unlock LUKS-Encrypted file systems using a FIDO2 token via `boot.initrd.luks.fido2Support`.
+
+- 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.
+
+  For further reference, please read [\#68953](https://github.com/NixOS/nixpkgs/pull/68953) or the corresponding [discourse thread](https://discourse.nixos.org/t/predictable-network-interface-names-in-initrd/4055).
+
+- The matrix-synapse-package has been updated to [v1.11.1](https://github.com/matrix-org/synapse/releases/tag/v1.11.1). Due to [stricter requirements](https://github.com/matrix-org/synapse/releases/tag/v1.10.0rc1) for database configuration when using postgresql, the automated database setup of the module has been removed to avoid any further edge-cases.
+
+  matrix-synapse expects `postgresql`-databases to have the options `LC_COLLATE` and `LC_CTYPE` set to [`'C'`](https://www.postgresql.org/docs/12/locale.html) which basically instructs `postgresql` to ignore any locale-based preferences.
+
+  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 `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:
+
+    ```nix
+    { ... }: {
+      services.matrix-synapse = {
+        enable = true;
+        /* and all the other config you've defined here */
+      };
+      services.postgresql.enable = true;
+    }
+    ```
+
+- 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).
+
+- 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.
+  :::
diff --git a/nixos/doc/manual/release-notes/rl-2009.section.md b/nixos/doc/manual/release-notes/rl-2009.section.md
new file mode 100644
index 00000000000..79be2a56a54
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-2009.section.md
@@ -0,0 +1,747 @@
+# Release 20.09 ("Nightingale", 2020.10/27) {#sec-release-20.09}
+
+Support is planned until the end of June 2021, handing over to 21.05. (Plans [ have shifted](https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md#core-changes) by two months since release of 20.09.)
+
+## Highlights {#sec-release-20.09-highlights}
+
+In addition to 7349 new, 14442 updated, and 8181 removed packages, this release has the following highlights:
+
+- Core version changes:
+
+  - gcc: 9.2.0 -\> 9.3.0
+
+  - glibc: 2.30 -\> 2.31
+
+  - linux: still defaults to 5.4.x, all supported kernels available
+
+  - mesa: 19.3.5 -\> 20.1.7
+
+- Desktop Environments:
+
+  - plasma5: 5.17.5 -\> 5.18.5
+
+  - kdeApplications: 19.12.3 -\> 20.08.1
+
+  - gnome3: 3.34 -\> 3.36, see its [release notes](https://help.gnome.org/misc/release-notes/3.36/)
+
+  - cinnamon: added at 4.6
+
+  - NixOS now distributes an official [GNOME ISO](https://nixos.org/download.html#nixos-iso)
+
+- Programming Languages and Frameworks:
+
+  - Agda ecosystem was heavily reworked (see more details below)
+
+  - PHP now defaults to PHP 7.4, updated from 7.3
+
+  - PHP 7.2 is no longer supported due to upstream not supporting this version for the entire lifecycle of the 20.09 release
+
+  - Python 3 now defaults to Python 3.8 instead of 3.7
+
+  - Python 3.5 reached its upstream EOL at the end of September 2020: it has been removed from the list of available packages
+
+- Databases and Service Monitoring:
+
+  - MariaDB has been updated to 10.4, MariaDB Galera to 26.4. Please read the related upgrade instructions under [backwards incompatibilities](#sec-release-20.09-incompatibilities) before upgrading.
+
+  - Zabbix now defaults to 5.0, updated from 4.4. Please read related sections under [backwards compatibilities](#sec-release-20.09-incompatibilities) before upgrading.
+
+- Major module changes:
+
+  - Quickly configure a complete, private, self-hosted video conferencing solution with the new Jitsi Meet module.
+
+  - Two new options, [authorizedKeysCommand](options.html#opt-services.openssh.authorizedKeysCommand) and [authorizedKeysCommandUser](options.html#opt-services.openssh.authorizedKeysCommandUser), have been added to the `openssh` module. If you have `AuthorizedKeysCommand` in your [services.openssh.extraConfig](options.html#opt-services.openssh.extraConfig) you should make use of these new options instead.
+
+  - There is a new module for Podman (`virtualisation.podman`), a drop-in replacement for the Docker command line.
+
+  - The new `virtualisation.containers` module manages configuration shared by the CRI-O and Podman modules.
+
+  - Declarative Docker containers are renamed from `docker-containers` to `virtualisation.oci-containers.containers`. This is to make it possible to use `podman` instead of `docker`.
+
+  - The new option [documentation.man.generateCaches](options.html#opt-documentation.man.generateCaches) has been added to automatically generate the `man-db` caches, which are needed by utilities like `whatis` and `apropos`. 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.
+
+  - `services.postfix.sslCACert` was replaced by `services.postfix.tlsTrustedAuthorities` which now defaults to system certificate authorities.
+
+  - The various documented workarounds to use steam have been converted to a module. `programs.steam.enable` enables steam, controller support and the workarounds.
+
+  - Support for built-in LCDs in various pieces of Logitech hardware (keyboards and USB speakers). `hardware.logitech.lcd.enable` enables support for all hardware supported by the [g15daemon project](https://sourceforge.net/projects/g15daemon/).
+
+  - 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 `boot.loader.grub.users`. Note: Password support is only available in GRUB version 2.
+
+- NixOS module changes:
+
+  - The NixOS module system now supports freeform modules as a mix between `types.attrsOf` and `types.submodule`. These allow you to explicitly declare a subset of options while still permitting definitions without an associated option. See [](#sec-freeform-modules) for how to use them.
+
+  - 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 `make-test.nix` or `testing.nix` needs to be ported to `make-test-python.nix` and `testing-python.nix` respectively.
+
+  - 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.
+
+- Starting with this release, the hydra-build-result `nixos-YY.MM` branches no longer exist in the [deprecated nixpkgs-channels repository](https://github.com/nixos/nixpkgs-channels). These branches are now in [the main nixpkgs repository](https://github.com/nixos/nixpkgs).
+
+## New Services {#sec-release-20.09-new-services}
+
+In addition to 1119 new, 118 updated, and 476 removed options; 61 new modules were added since the last release:
+
+- Hardware:
+
+  - [hardware.system76.firmware-daemon.enable](options.html#opt-hardware.system76.firmware-daemon.enable) adds easy support of system76 firmware
+
+  - [hardware.uinput.enable](options.html#opt-hardware.uinput.enable) loads uinput kernel module
+
+  - [hardware.video.hidpi.enable](options.html#opt-hardware.video.hidpi.enable) enable good defaults for HiDPI displays
+
+  - [hardware.wooting.enable](options.html#opt-hardware.wooting.enable) support for Wooting keyboards
+
+  - [hardware.xpadneo.enable](options.html#opt-hardware.xpadneo.enable) xpadneo driver for Xbox One wireless controllers
+
+- Programs:
+
+  - [programs.hamster.enable](options.html#opt-programs.hamster.enable) enable hamster time tracking
+
+  - [programs.steam.enable](options.html#opt-programs.steam.enable) adds easy enablement of steam and related system configuration
+
+- Security:
+
+  - [security.doas.enable](options.html#opt-security.doas.enable) alternative to sudo, allows non-root users to execute commands as root
+
+  - [security.tpm2.enable](options.html#opt-security.tpm2.enable) add Trusted Platform Module 2 support
+
+- System:
+
+  - [boot.initrd.network.openvpn.enable](options.html#opt-boot.initrd.network.openvpn.enable) start an OpenVPN client during initrd boot
+
+- Virtualization:
+
+  - [boot.enableContainers](options.html#opt-boot.enableContainers) use nixos-containers
+
+  - [virtualisation.oci-containers.containers](options.html#opt-virtualisation.oci-containers.containers) run OCI (Docker) containers
+
+  - [virtualisation.podman.enable](options.html#opt-virtualisation.podman.enable) daemonless container engine
+
+- Services:
+
+  - [services.ankisyncd.enable](options.html#opt-services.ankisyncd.enable) Anki sync server
+
+  - [services.bazarr.enable](options.html#opt-services.bazarr.enable) Subtitle manager for Sonarr and Radarr
+
+  - [services.biboumi.enable](options.html#opt-services.biboumi.enable) Biboumi XMPP gateway to IRC
+
+  - [services.blockbook-frontend](options.html#opt-services.blockbook-frontend) Blockbook-frontend, a service for the Trezor wallet
+
+  - [services.cage.enable](options.html#opt-services.cage.enable) Wayland cage service
+
+  - [services.convos.enable](options.html#opt-services.convos.enable) IRC daemon, which can be accessed throught the browser
+
+  - [services.engelsystem.enable](options.html#opt-services.engelsystem.enable) Tool for coordinating volunteers and shifts on large events
+
+  - [services.espanso.enable](options.html#opt-services.espanso.enable) text-expander written in rust
+
+  - [services.foldingathome.enable](options.html#opt-services.foldingathome.enable) Folding\@home client
+
+  - [services.gerrit.enable](options.html#opt-services.gerrit.enable) Web-based team code collaboration tool
+
+  - [services.go-neb.enable](options.html#opt-services.go-neb.enable) Matrix bot
+
+  - [services.hardware.xow.enable](options.html#opt-services.hardware.xow.enable) xow as a systemd service
+
+  - [services.hercules-ci-agent.enable](options.html#opt-services.hercules-ci-agent.enable) Hercules CI build agent
+
+  - [services.jicofo.enable](options.html#opt-services.jicofo.enable) Jitsi Conference Focus, component of Jitsi Meet
+
+  - [services.jirafeau.enable](options.html#opt-services.jirafeau.enable) A web file repository
+
+  - [services.jitsi-meet.enable](options.html#opt-services.jitsi-meet.enable) Secure, simple and scalable video conferences
+
+  - [services.jitsi-videobridge.enable](options.html#opt-services.jitsi-videobridge.enable) Jitsi Videobridge, a WebRTC compatible router
+
+  - [services.jupyterhub.enable](options.html#opt-services.jupyterhub.enable) Jupyterhub development server
+
+  - [services.k3s.enable](options.html#opt-services.k3s.enable) Lightweight Kubernetes distribution
+
+  - [services.magic-wormhole-mailbox-server.enable](options.html#opt-services.magic-wormhole-mailbox-server.enable) Magic Wormhole Mailbox Server
+
+  - [services.malcontent.enable](options.html#opt-services.malcontent.enable) Parental Control support
+
+  - [services.matrix-appservice-discord.enable](options.html#opt-services.matrix-appservice-discord.enable) Matrix and Discord bridge
+
+  - [services.mautrix-telegram.enable](options.html#opt-services.mautrix-telegram.enable) Matrix-Telegram puppeting/relaybot bridge
+
+  - [services.mirakurun.enable](options.html#opt-services.mirakurun.enable) Japanese DTV Tuner Server Service
+
+  - [services.molly-brown.enable](options.html#opt-services.molly-brown.enable) Molly-Brown Gemini server
+
+  - [services.mullvad-vpn.enable](options.html#opt-services.mullvad-vpn.enable) Mullvad VPN daemon
+
+  - [services.ncdns.enable](options.html#opt-services.ncdns.enable) Namecoin to DNS bridge
+
+  - [services.nextdns.enable](options.html#opt-services.nextdns.enable) NextDNS to DoH Proxy service
+
+  - [services.nix-store-gcs-proxy](options.html#opt-services.nix-store-gcs-proxy) Google storage bucket to be used as a nix store
+
+  - [services.onedrive.enable](options.html#opt-services.onedrive.enable) OneDrive sync service
+
+  - [services.pinnwand.enable](options.html#opt-services.pinnwand.enable) Pastebin-like service
+
+  - [services.pixiecore.enable](options.html#opt-services.pixiecore.enable) Manage network booting of machines
+
+  - [services.privacyidea.enable](options.html#opt-services.privacyidea.enable) Privacy authentication server
+
+  - [services.quorum.enable](options.html#opt-services.quorum.enable) Quorum blockchain daemon
+
+  - [services.robustirc-bridge.enable](options.html#opt-services.robustirc-bridge.enable) RobustIRC bridge
+
+  - [services.rss-bridge.enable](options.html#opt-services.rss-bridge.enable) Generate RSS and Atom feeds
+
+  - [services.rtorrent.enable](options.html#opt-services.rtorrent.enable) rTorrent service
+
+  - [services.smartdns.enable](options.html#opt-services.smartdns.enable) SmartDNS DNS server
+
+  - [services.sogo.enable](options.html#opt-services.sogo.enable) SOGo groupware
+
+  - [services.teeworlds.enable](options.html#opt-services.teeworlds.enable) Teeworlds game server
+
+  - [services.torque.mom.enable](options.html#opt-services.torque.mom.enable) torque computing node
+
+  - [services.torque.server.enable](options.html#opt-services.torque.server.enable) torque server
+
+  - [services.tuptime.enable](options.html#opt-services.tuptime.enable) A total uptime service
+
+  - [services.urserver.enable](options.html#opt-services.urserver.enable) X11 remote server
+
+  - [services.wasabibackend.enable](options.html#opt-services.wasabibackend.enable) Wasabi backend service
+
+  - [services.yubikey-agent.enable](options.html#opt-services.yubikey-agent.enable) Yubikey agent
+
+  - [services.zigbee2mqtt.enable](options.html#opt-services.zigbee2mqtt.enable) Zigbee to MQTT bridge
+
+## Backward Incompatibilities {#sec-release-20.09-incompatibilities}
+
+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:
+
+  ```nix
+  {
+  services.mysql.initialScript = pkgs.writeText "mariadb-init.sql" ''
+    ALTER USER root@localhost IDENTIFIED VIA mysql_native_password USING PASSWORD("verysecret");
+  '';
+  }
+  ```
+
+  When MariaDB data directory is just upgraded (not initialized), the users are not created or modified.
+
+- 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 `LOAD DATA INFILE or SELECT * INTO OUTFILE`. In this scenario a variant of the following may be required: - allow MySQL to read from /home and /tmp directories when using `LOAD DATA INFILE`
+
+  ```nix
+  {
+    systemd.services.mysql.serviceConfig.ProtectHome = lib.mkForce "read-only";
+  }
+  ```
+
+  \- allow MySQL to write to custom folder `/var/data` when using `SELECT * INTO OUTFILE`, assuming the mysql user has write access to `/var/data`
+
+  ```nix
+  {
+    systemd.services.mysql.serviceConfig.ReadWritePaths = [ "/var/data" ];
+  }
+  ```
+
+  The MySQL service no longer runs its `systemd` service startup script as `root` anymore. A dedicated non `root` 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:
+
+  ```SQL
+  CREATE USER IF NOT EXISTS 'mysql'@'localhost' identified with unix_socket;
+  GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'localhost' WITH GRANT OPTION;
+  ```
+
+  If you use MySQL instead of MariaDB please replace `unix_socket` with `auth_socket`. If you have changed the value of [services.mysql.user](options.html#opt-services.mysql.user) from the default of `mysql` to a different user please change `'mysql'@'localhost'` to the corresponding user instead.
+
+- Zabbix now defaults to 5.0, updated from 4.4. Please carefully read through [the upgrade guide](https://www.zabbix.com/documentation/current/manual/installation/upgrade/sources) and apply any changes required. Be sure to take special note of the section on [enabling extended range of numeric (float) values](https://www.zabbix.com/documentation/current/manual/installation/upgrade_notes_500#enabling_extended_range_of_numeric_float_values) as you will need to apply this database migration manually.
+
+  If you are using Zabbix Server with a MySQL or MariaDB database you should note that using a character set of `utf8` and a collate of `utf8_bin` has become mandatory with this release. See the upstream [issue](https://support.zabbix.com/browse/ZBX-17357) for further discussion. Before upgrading you should check the character set and collation used by your database and ensure they are correct:
+
+  ```SQL
+  SELECT
+    default_character_set_name,
+    default_collation_name
+  FROM
+    information_schema.schemata
+  WHERE
+    schema_name = 'zabbix';
+  ```
+
+  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 [example](https://www.zabbix.com/forum/zabbix-help/396573-reinstall-after-upgrade?p=396891#post396891) of how to do so, taken from the Zabbix forums:
+
+  ```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("ALTER TABLE ", TABLE_NAME," CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin;") AS ExecuteTheString
+  FROM information_schema.`COLUMNS`
+  WHERE table_schema = "zabbix" AND COLLATION_NAME = "utf8_general_ci";
+  ```
+
+- maxx package removed along with `services.xserver.desktopManager.maxx` module. Please migrate to cdesktopenv and `services.xserver.desktopManager.cde` module.
+
+- 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.
+
+- 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:
+
+  ```nix
+  {
+    services.grafana.package = pkgs.grafana.overrideAttrs (oldAttrs: rec {
+      phantomJsSupport = true;
+    });
+  }
+  ```
+
+- The [supybot](options.html#opt-services.supybot.enable) module now uses `/var/lib/supybot` as its default [stateDir](options.html#opt-services.supybot.stateDir) path if `stateVersion` is 20.09 or higher. It also enables a number of [systemd sandboxing options](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Sandboxing) which may possibly interfere with some plugins. If this is the case you can disable the options through attributes in `systemd.services.supybot.serviceConfig`.
+
+- The `security.duosec.skey` option, which stored a secret in the nix store, has been replaced by a new [security.duosec.secretKeyFile](options.html#opt-security.duosec.secretKeyFile) option for better security.
+
+  `security.duosec.ikey` has been renamed to [security.duosec.integrationKey](options.html#opt-security.duosec.integrationKey).
+
+- `vmware` has been removed from the `services.x11.videoDrivers` defaults. For VMWare guests set `virtualisation.vmware.guest.enable` to `true` which will include the appropriate drivers.
+
+- 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.
+
+- 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
+  {
+    environment.systemPackages = [
+      (pkgs.php.withExtensions
+        ({ all, ... }: with all; [
+          imagick
+          opcache
+          pdo
+          pdo_mysql
+        ])
+      )
+    ];
+  }
+  ```
+
+  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:
+
+- The updated `php` attribute is now easily customizable to your liking by using `php.withExtensions` or `php.buildEnv` instead of writing config files or changing configure flags.
+
+- The remaining configuration flags can now be set directly on the `php` attribute. For example, instead of
+
+  ```nix
+  {
+    php.override {
+      config.php.embed = true;
+      config.php.apxs2 = false;
+    }
+  }
+  ```
+
+  you should now write
+
+  ```nix
+  {
+    php.override {
+      embedSupport = true;
+      apxs2Support = false;
+    }
+  }
+  ```
+
+- The ACME module has been overhauled for simplicity and maintainability. Cert generation now implicitly uses the `acme` user, and the `security.acme.certs._name_.user` 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.
+
+- 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 [here](https://github.com/gollum/gollum/wiki/5.0-release-notes#migrating-your-wiki).
+
+- Deluge 2.x was added and is used as default for new NixOS installations where stateVersion is \>= 20.09. If you are upgrading from a previous NixOS version, you can set `service.deluge.package = pkgs.deluge-2_x` 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.
+
+- Nginx web server now starting with additional sandbox/hardening options. By default, write access to `/var/log/nginx` and `/var/cache/nginx` is allowed. To allow writing to other folders, use `systemd.services.nginx.serviceConfig.ReadWritePaths`
+
+  ```nix
+  {
+    systemd.services.nginx.serviceConfig.ReadWritePaths = [ "/var/www" ];
+  }
+  ```
+
+  Nginx is also started with the systemd option `ProtectHome = mkDefault true;` which forbids it to read anything from `/home`, `/root` and `/run/user` (see [ProtectHome docs](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHome=) for details). If you require serving files from home directories, you may choose to set e.g.
+
+  ```nix
+  {
+    systemd.services.nginx.serviceConfig.ProtectHome = "read-only";
+  }
+  ```
+
+- The NixOS options `nesting.clone` and `nesting.children` have been deleted, and replaced with named [specialisation](options.html#opt-specialisation) configurations.
+
+  Replace a `nesting.clone` entry with:
+
+  ```nix
+  {
+    specialisation.example-sub-configuration = {
+      configuration = {
+        ...
+      };
+  };
+  ```
+
+  Replace a `nesting.children` entry with:
+
+  ```nix
+  {
+    specialisation.example-sub-configuration = {
+      inheritParentConfig = false;
+      configuration = {
+        ...
+      };
+  };
+  ```
+
+  To switch to a specialised configuration at runtime you need to run:
+
+  ```ShellSession
+  $ sudo /run/current-system/specialisation/example-sub-configuration/bin/switch-to-configuration test
+  ```
+
+  Before you would have used:
+
+  ```ShellSession
+  $ sudo /run/current-system/fine-tune/child-1/bin/switch-to-configuration test
+  ```
+
+- 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.
+
+- 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 [services.httpd.user](options.html#opt-services.httpd.user) and [services.httpd.group](options.html#opt-services.httpd.group)). 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.
+
+  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_.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.
+
+- When using `buildBazelPackage` from Nixpkgs, `flat` hash mode is now used for dependencies instead of `recursive`. This is to better allow using hashed mirrors where needed. As a result, these hashes will have changed.
+
+- 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 `services.postgresql.checkConfig` to `false`.
+
+- The rkt module has been removed, it was archived by upstream.
+
+- The [Bazaar](https://bazaar.canonical.com) VCS is unmaintained and, as consequence of the Python 2 EOL, the packages `bazaar` and `bazaarTools` were removed. Breezy, the backward compatible fork of Bazaar (see the [announcement](https://www.jelmer.uk/breezy-intro.html)), was packaged as `breezy` and can be used instead.
+
+  Regarding Nixpkgs, `fetchbzr`, `nix-prefetch-bzr` and Bazaar support in Hydra will continue to work through Breezy.
+
+- In addition to the hostname, the fully qualified domain name (FQDN), which consists of `${networking.hostName}` and `${networking.domain}` is now added to `/etc/hosts`, to allow local FQDN resolution, as used by the `hostname --fqdn` 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 `127.0.0.2` instead of `127.0.1.1` to be consistent with what `nss-myhostname` (from systemd) returns. The old behaviour can e.g. be restored by using `networking.hosts = lib.mkForce { "127.0.1.1" = [ config.networking.hostName ]; };`.
+
+- The hostname (`networking.hostName`) 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 (`uname --nodename`) the option `boot.kernel.sysctl."kernel.hostname"` can be used as a workaround (but be aware of the 64 character limit).
+
+- 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).
+
+- 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`.
+
+- `sslh` has been updated to version `1.21`. The `ssl` probe must be renamed to `tls` in [services.sslh.appendConfig](options.html#opt-services.sslh.appendConfig).
+
+- Users of [OpenAFS 1.6](http://openafs.org) 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 `services.openafsClient.packages.module`, `services.openafsClient.packages.programs` and `services.openafsServer.package` to select a different OpenAFS package. OpenAFS 1.6 will be removed in the next release. The package `openafs` and the service options will then silently point to the OpenAFS 1.8 release.
+
+  See also the OpenAFS [Administrator Guide](http://docs.openafs.org/AdminGuide/index.html) for instructions. Beware of the following when updating servers:
+
+  - The storage format of the server key has changed and the key must be converted before running the new release.
+
+  - 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!
+
+  - 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.
+
+- `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`.
+
+- We now have a unified [services.xserver.displayManager.autoLogin](options.html#opt-services.xserver.displayManager.autoLogin) option interface to be used for every display-manager in NixOS.
+
+- The `bitcoind` 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.:
+
+  ```nix
+  {
+    services.bitcoind = {
+      enable = true;
+      extraConfig = "...";
+      ...
+    };
+  }
+  ```
+
+  To something similar:
+
+  ```nix
+  {
+    services.bitcoind.mainnet = {
+      enable = true;
+      dataDir = "/var/lib/bitcoind";
+      user = "bitcoin";
+      extraConfig = "...";
+      ...
+    };
+  }
+  ```
+
+  The key settings are:
+
+  - `dataDir` - to continue using the same data directory.
+
+  - `user` - to continue using the same user so that bitcoind maintains access to its files.
+
+- 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 [release info](https://www.graylog.org/post/announcing-graylog-v3-3-3) for information on how to avoid the issue.
+
+- The `dokuwiki` 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 `nginx.forceSSL` and `nginx.enableACME` are no longer set to `true`. To continue using your service with the original SSL settings, you have to adjust the original config, e.g.:
+
+  ```nix
+  {
+    services.dokuwiki = {
+      enable = true;
+      ...
+    };
+  }
+  ```
+
+  To something similar:
+
+  ```nix
+  {
+    services.dokuwiki."mywiki" = {
+      enable = true;
+      nginx = {
+        forceSSL = true;
+        enableACME = true;
+      };
+      ...
+    };
+  }
+  ```
+
+  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:
+
+  ```nix
+  {
+    services.postgresql.dataDir = "/var/db/postgresql";
+  }
+  ```
+
+  The postgresql module now expects there to be a database super user account called `postgres` 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 run the following SQL statements as a database super admin user before upgrading:
+
+  ```SQL
+  CREATE ROLE postgres LOGIN SUPERUSER;
+  ```
+
+- The USBGuard module now removes options and instead hardcodes values for `IPCAccessControlFiles`, `ruleFiles`, and `auditFilePath`. Audit logs can be found in the journal.
+
+- 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 [the PR that changed this](https://github.com/NixOS/nixpkgs/pull/82743#issuecomment-674520472) for more info.
+
+- For NixOS configuration options, the type `loaOf`, after its initial deprecation in release 20.03, has been removed. In NixOS and Nixpkgs options using this type have been converted to `attrsOf`. For more information on this change have look at these links: [issue \#1800](https://github.com/NixOS/nixpkgs/issues/1800), [PR \#63103](https://github.com/NixOS/nixpkgs/pull/63103).
+
+- `config.systemd.services.${name}.path` now returns a list of paths instead of a colon-separated string.
+
+- Caddy module now uses Caddy v2 by default. Caddy v1 can still be used by setting [services.caddy.package](options.html#opt-services.caddy.package) to `pkgs.caddy1`.
+
+  New option [services.caddy.adapter](options.html#opt-services.caddy.adapter) has been added.
+
+- 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 `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:
+
+  - 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.
+
+- 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.
+
+  ::: {.note}
+  Issue [\#98819](https://github.com/NixOS/nixpkgs/issues/98819) is now fixed and gnome3.epiphany is once again installed by default.
+  :::
+
+- If you want to manage the configuration of wpa_supplicant outside of NixOS you must ensure that none of [networking.wireless.networks](options.html#opt-networking.wireless.networks), [networking.wireless.extraConfig](options.html#opt-networking.wireless.extraConfig) or [networking.wireless.userControlled.enable](options.html#opt-networking.wireless.userControlled.enable) is being used or `true`. Using any of those options will cause wpa_supplicant to be started with a NixOS generated configuration file instead of your own.
+
+## Other Notable Changes {#sec-release-20.09-notable-changes}
+
+- SD images are now compressed by default using `zstd`. The compression for ISO images has also been changed to `zstd`, but ISO images are still not compressed by default.
+
+- `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.
+
+- 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.
+
+- The default output of `buildGoPackage` is now `$out` instead of `$bin`.
+
+- `buildGoModule` `doCheck` now defaults to `true`.
+
+- Packages built using `buildRustPackage` now use `release` mode for the `checkPhase` by default.
+
+  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.
+
+- Additional Git documentation (HTML and text files) is now available via the `git-doc` package.
+
+- Default algorithm for ZRAM swap was changed to `zstd`.
+
+- 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.
+
+- 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).
+
+- The `hardware.u2f` module, which was installing udev rules was removed, as udev gained native support to handle FIDO security tokens.
+
+- The `services.transmission` module was enhanced with the new options: [services.transmission.credentialsFile](options.html#opt-services.transmission.credentialsFile), [services.transmission.openFirewall](options.html#opt-services.transmission.openFirewall), and [services.transmission.performanceNetParameters](options.html#opt-services.transmission.performanceNetParameters).
+
+  `transmission-daemon` is now started with additional systemd sandbox/hardening options for better security. Please [report](https://github.com/NixOS/nixpkgs/issues) any use case where this is not working well. In particular, the `RootDirectory` option newly set forbids uploading or downloading a torrent outside of the default directory configured at [settings.download-dir](options.html#opt-services.transmission.settings). If you really need Transmission to access other directories, you must include those directories into the `BindPaths` of the service:
+
+  ```nix
+  {
+    systemd.services.transmission.serviceConfig.BindPaths = [ "/path/to/alternative/download-dir" ];
+  }
+  ```
+
+  Also, connection to the RPC (Remote Procedure Call) of `transmission-daemon` is now only available on the local network interface by default. Use:
+
+  ```nix
+  {
+    services.transmission.settings.rpc-bind-address = "0.0.0.0";
+  }
+  ```
+
+  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.
+
+  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.
+
+  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.
+
+  This means that a configuration like this
+
+  ```nix
+  {
+    services.dovecot2.mailboxes = [
+      { name = "Junk";
+        auto = "create";
+      }
+    ];
+  }
+  ```
+
+  should now look like this:
+
+  ```nix
+  {
+    services.dovecot2.mailboxes = {
+      Junk.auto = "create";
+    };
+  }
+  ```
+
+- 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.
+
+- 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.
+
+- 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.
+
+- The [nix.buildMachines](options.html#opt-nix.buildMachines) option is now type-checked. There are no functional changes, however this may require updating some configurations to use correct types for all attributes.
+
+- The `fontconfig` module stopped generating config and cache files for fontconfig 2.10.x, the `/etc/fonts/fonts.conf` now belongs to the latest fontconfig, just like on other Linux distributions, and we will [no longer](https://github.com/NixOS/nixpkgs/pull/95358) be versioning the config directories.
+
+  Fontconfig 2.10.x was removed from Nixpkgs since it hasn't been used in any Nixpkgs package for years now.
+
+- Nginx module `nginxModules.fastcgi-cache-purge` renamed to official name `nginxModules.cache-purge`. Nginx module `nginxModules.ngx_aws_auth` renamed to official name `nginxModules.aws-auth`.
+
+- The option `defaultPackages` was added. It installs the packages perl, rsync and strace for now. They were added unconditionally to `systemPackages` 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.
+
+- The `undervolt` option no longer needs to apply its settings every 30s. If they still become undone, open an issue and restore the previous behaviour using `undervolt.useTimer`.
+
+- Agda has been heavily reworked.
+
+  - `agda.mkDerivation` has been heavily changed and is now located at agdaPackages.mkDerivation.
+
+  - New top-level packages agda and `agda.withPackages` have been added, the second of which sets up agda with access to chosen libraries.
+
+  - All agda libraries now live under `agdaPackages`.
+
+  - Many broken libraries have been removed.
+
+  See the [new documentation](https://nixos.org/nixpkgs/manual/#agda) for more information.
+
+- The `deepin` package set has been removed from nixpkgs. It was a work in progress to package the [Deepin Desktop Environment (DDE)](https://www.deepin.org/en/dde/), 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 [issue \#94870](https://github.com/NixOS/nixpkgs/issues/94870). The package `netease-cloud-music` has also been removed, as it depends on libraries from deepin.
+
+- The `opendkim` module now uses systemd sandboxing features to limit the exposure of the system towards the opendkim service.
+
+- 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 [ the release notes for Kubernetes 1.19 carefully ](https://relnotes.k8s.io/?markdown=93264) before upgrading.
+
+- For AMD GPUs, Vulkan can now be used by adding `amdvlk` to `hardware.opengl.extraPackages`.
+
+- Similarly, still for AMD GPUs, the ROCm OpenCL stack can now be used by adding `rocm-opencl-icd` to `hardware.opengl.extraPackages`.
+
+## Contributions {#sec-release-20.09-contributions}
+
+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.
+
+1.  2288 Mario Rodas
+
+2.  1837 Frederik Rietdijk
+
+3.  946 Jörg Thalheim
+
+4.  925 Maximilian Bosch
+
+5.  687 Jonathan Ringer
+
+6.  651 Jan Tojnar
+
+7.  622 Daniël de Kok
+
+8.  605 WORLDofPEACE
+
+9.  597 Florian Klink
+
+10. 528 José Romildo Malaquias
+
+11. 281 volth
+
+12. 101 Robert Scott
+
+13. 86 Tim Steinbach
+
+14. 76 WORLDofPEACE
+
+15. 49 Maximilian Bosch
+
+16. 42 Thomas Tuegel
+
+17. 37 Doron Behar
+
+18. 36 Vladimír Čunát
+
+19. 27 Jonathan Ringer
+
+20. 27 Maciej Krüger
+
+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.
diff --git a/nixos/doc/manual/release-notes/rl-2105.section.md b/nixos/doc/manual/release-notes/rl-2105.section.md
new file mode 100644
index 00000000000..359f2e5b2e5
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-2105.section.md
@@ -0,0 +1,428 @@
+# Release 21.05 ("Okapi", 2021.05/31) {#sec-release-21.05}
+
+Support is planned until the end of December 2021, handing over to 21.11.
+
+## Highlights {#sec-release-21.05-highlights}
+
+In addition to numerous new and upgraded packages, this release has the following highlights:
+
+- Core version changes:
+
+  - gcc: 9.3.0 -\> 10.3.0
+
+  - glibc: 2.30 -\> 2.32
+
+  - default linux: 5.4 -\> 5.10, all supported kernels available
+
+  - mesa: 20.1.7 -\> 21.0.1
+
+- Desktop Environments:
+
+  - GNOME: 3.36 -\> 40, see its [release notes](https://help.gnome.org/misc/release-notes/40.0/)
+
+  - Plasma5: 5.18.5 -\> 5.21.3
+
+  - kdeApplications: 20.08.1 -\> 20.12.3
+
+  - cinnamon: 4.6 -\> 4.8.1
+
+- Programming Languages and Frameworks:
+
+  - Python optimizations were disabled again. Builds with optimizations enabled are not reproducible. Optimizations can now be enabled with an option.
+
+- 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).
+
+## New Services {#sec-release-21.05-new-services}
+
+The following new services were added since the last release:
+
+- [GNURadio](https://www.gnuradio.org/) 3.8 and 3.9 were [finally](https://github.com/NixOS/nixpkgs/issues/82263) 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 `gnuradio` (3.9), `gnuradio3_8` and `gnuradio3_7` 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 ([\#75478](https://github.com/NixOS/nixpkgs/issues/75478)).
+
+- [Keycloak](https://www.keycloak.org/), 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).
+
+  See the [Keycloak section of the NixOS manual](#module-services-keycloak) for more information.
+
+- [services.samba-wsdd.enable](options.html#opt-services.samba-wsdd.enable) Web Services Dynamic Discovery host daemon
+
+- [Discourse](https://www.discourse.org/), a modern and open source discussion platform.
+
+  See the [Discourse section of the NixOS manual](#module-services-discourse) for more information.
+
+- [services.nebula.networks](options.html#opt-services.nebula.networks) [Nebula VPN](https://github.com/slackhq/nebula)
+
+## Backward Incompatibilities {#sec-release-21.05-incompatibilities}
+
+When upgrading from a previous release, please be aware of the following incompatible changes:
+
+- GNOME desktop environment was upgraded to 40, see the release notes for [40.0](https://help.gnome.org/misc/release-notes/40.0/) and [3.38](https://help.gnome.org/misc/release-notes/3.38/). The `gnome3` attribute set has been renamed to `gnome` and so have been the NixOS options.
+
+- If you are using `services.udev.extraRules` 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 `services.udev.initrdRules` or see the new [Assigning custom names](#sec-custom-ifnames) section of the NixOS manual for an example using networkd links.
+
+- The `security.hideProcessInformation` module has been removed. It was broken since the switch to cgroups-v2.
+
+- The `linuxPackages.ati_drivers_x11` kernel modules have been removed. The drivers only supported kernels prior to 4.2, and thus have become obsolete.
+
+- The `systemConfig` 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.
+
+- `systemd-journal2gelf` no longer parses json and expects the receiving system to handle it. How to achieve this with Graylog is described in this [GitHub issue](https://github.com/parse-nl/SystemdJournal2Gelf/issues/10).
+
+- 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 {}`.
+
+- `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`!
+
+- The option `fonts.enableFontDir` has been renamed to [fonts.fontDir.enable](options.html#opt-fonts.fontDir.enable). The path of font directory has also been changed to `/run/current-system/sw/share/X11/fonts`, for consistency with other X11 resources.
+
+- A number of options have been renamed in the kicad interface. `oceSupport` has been renamed to `withOCE`, `withOCCT` has been renamed to `withOCC`, `ngspiceSupport` has been renamed to `withNgspice`, and `scriptingSupport` has been renamed to `withScripting`. Additionally, `kicad/base.nix` no longer provides default argument values since these are provided by `kicad/default.nix`.
+
+- The socket for the `pdns-recursor` module was moved from `/var/lib/pdns-recursor` to `/run/pdns-recursor` to match upstream.
+
+- 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 [this thread](https://forum.openpaper.work/t/paperwork-2-0/112/5) for more details.
+
+- PowerDNS has been updated from `4.2.x` to `4.3.x`. Please be sure to review the [Upgrade Notes](https://doc.powerdns.com/authoritative/upgrading.html#x-to-4-3-0) provided by upstream before upgrading. Worth specifically noting is that the service now runs entirely as a dedicated `pdns` user, instead of starting as `root` and dropping privileges, as well as the default `socket-dir` location changing from `/var/lib/powerdns` to `/run/pdns`.
+
+- The `mediatomb` service is now using by default the new and maintained fork `gerbera` package instead of the unmaintained `mediatomb` package. If you want to keep the old behavior, you must declare it with:
+
+  ```nix
+  {
+    services.mediatomb.package = pkgs.mediatomb;
+  }
+  ```
+
+  One new option `openFirewall` 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:
+
+  ```nix
+  {
+    services.mediatomb.openFirewall = true;
+  }
+  ```
+
+- 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 `mkfs.xfs -m reflink=0`.
+
+- 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 `uwsgi` user. Any additional capability can be added via the new option [services.uwsgi.capabilities](options.html#opt-services.uwsgi.capabilities). The previous behaviour can be restored by setting:
+
+  ```nix
+  {
+    services.uwsgi.user = "root";
+    services.uwsgi.group = "root";
+    services.uwsgi.instance =
+      {
+        uid = "uwsgi";
+        gid = "uwsgi";
+      };
+  }
+  ```
+
+  Another incompatibility from the previous release is that vassals running under a different user or group need to use `immediate-{uid,gid}` instead of the usual `uid,gid` options.
+
+- btc1 has been abandoned upstream, and removed.
+
+- cpp_ethereum (aleth) has been abandoned upstream, and removed.
+
+- riak-cs package removed along with `services.riak-cs` module.
+
+- stanchion package removed along with `services.stanchion` module.
+
+- mutt has been updated to a new major version (2.x), which comes with some backward incompatible changes that are described in the [release notes for Mutt 2.0](http://www.mutt.org/relnotes/2.0/).
+
+- `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).
+
+- [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.
+
+- openafs now points to openafs_1_8, which is the new stable release. OpenAFS 1.6 was removed.
+
+- The WireGuard module gained a new option `networking.wireguard.interfaces.<name>.peers.*.dynamicEndpointRefreshSeconds` that implements refreshing the IP of DNS-based endpoints periodically (which WireGuard itself [cannot do](https://lists.zx2c4.com/pipermail/wireguard/2017-November/002028.html)).
+
+- MariaDB has been updated to 10.5. Before you upgrade, it would be best to take a backup of your database and read [ Incompatible Changes Between 10.4 and 10.5](https://mariadb.com/kb/en/upgrading-from-mariadb-104-to-mariadb-105/#incompatible-changes-between-104-and-105). After the upgrade you will need to run `mysql_upgrade`.
+
+- The TokuDB storage engine dropped in mariadb 10.5 and removed in mariadb 10.6. It is recommended to switch to RocksDB. See also [TokuDB](https://mariadb.com/kb/en/tokudb/) and [MDEV-19780: Remove the TokuDB storage engine](https://jira.mariadb.org/browse/MDEV-19780).
+
+- The `openldap` module now has support for OLC-style configuration, users of the `configDir` option may wish to migrate. If you continue to use `configDir`, ensure that `olcPidFile` is set to `/run/slapd/slapd.pid`.
+
+  As a result, `extraConfig` and `extraDatabaseConfig` are removed. To help with migration, you can convert your `slapd.conf` file to OLC configuration with the following script (find the location of this configuration file by running `systemctl status openldap`, it is the `-f` option.
+
+  ```ShellSession
+  $ TMPDIR=$(mktemp -d)
+  $ slaptest -f /path/to/slapd.conf -F $TMPDIR
+  $ slapcat -F $TMPDIR -n0 -H 'ldap:///???(!(objectClass=olcSchemaConfig))'
+  ```
+
+  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 `slaptest` is buggy with schemas directly in the config file.
+
+- 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`.
+  :::
+
+- 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:
+
+  ```nix
+  {
+    opt-services.privoxy.enable = true;
+    opt-services.privoxy.enableTor = true;
+  }
+  ```
+
+- The `services.tor` module has a new exhaustively typed [services.tor.settings](options.html#opt-services.tor.settings) option following RFC 0042; backward compatibility with old options has been preserved when aliasing was possible. The corresponding systemd service has been hardened, but there is a chance that the service still requires more permissions, so please report any related trouble on the bugtracker. Onion services v3 are now supported in [services.tor.relay.onionServices](options.html#opt-services.tor.relay.onionServices). A new [services.tor.openFirewall](options.html#opt-services.tor.openFirewall) option as been introduced for allowing connections on all the TCP ports configured.
+
+- The options `services.slurm.dbdserver.storagePass` and `services.slurm.dbdserver.configFile` have been removed. Use `services.slurm.dbdserver.storagePassFile` instead to provide the database password. Extra config options can be given via the option `services.slurm.dbdserver.extraConfig`. 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.
+
+- The `wafHook` hook does not wrap Python anymore. Packages depending on `wafHook` need to include any Python into their `nativeBuildInputs`.
+
+- Starting with version 1.7.0, the project formerly named `CodiMD` is now named `HedgeDoc`. 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 [system.stateVersion](options.html#opt-system.stateVersion), existing installations will continue to work.
+
+- 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:
+
+  ```
+  http://some.json-exporter.host:7979/probe?target=https://example.com/some/json/endpoint
+  ```
+
+  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.
+
+  These changes also affect [services.prometheus.exporters.rspamd.enable](options.html#opt-services.prometheus.exporters.rspamd.enable), which is just a preconfigured instance of the json exporter.
+
+  For more information, take a look at the [ official documentation](https://github.com/prometheus-community/json_exporter) of the json_exporter.
+
+- Androidenv was updated, removing the `includeDocs` and `lldbVersions` 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.
+
+  Android packages are now loaded from a repo.json file created by parsing Android repo XML files. The arguments `repoJson` and `repoXmls` 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 `extraLicenses` argument has been added to accept more SDK licenses if your project requires it. See the androidenv documentation for more details.
+
+- The attribute `mpi` 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 `mpi ? null` in the input for optional MPI builds, have been changed to the boolean input paramater `useMpi` to enable building with MPI. Building all packages with `mpich` instead of the default `openmpi` can now be achived like this:
+
+  ```nix
+  self: super:
+  {
+    mpi = super.mpich;
+  }
+  ```
+
+- The Searx module has been updated with the ability to configure the service declaratively and uWSGI integration. The option `services.searx.configFile` has been renamed to [services.searx.settingsFile](options.html#opt-services.searx.settingsFile) for consistency with the new [services.searx.settings](options.html#opt-services.searx.settings). In addition, the `searx` uid and gid reservations have been removed since they were not necessary: the service is now running with a dynamically allocated uid.
+
+- The libinput module has been updated with the ability to configure mouse and touchpad settings separately. The options in `services.xserver.libinput` have been renamed to `services.xserver.libinput.touchpad`, while there is a new `services.xserver.libinput.mouse` for mouse related configuration.
+
+  Since touchpad options no longer apply to all devices, you may want to replicate your touchpad configuration in mouse section.
+
+- ALSA OSS emulation (`sound.enableOSSEmulation`) is now disabled by default.
+
+- Thinkfan as been updated to `1.2.x`, 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 [services.thinkfan.settings](options.html#opt-services.thinkfan.settings) option has been added.
+
+  Please read the [ thinkfan documentation](https://github.com/vmatare/thinkfan#readme) before updating.
+
+- Adobe Flash Player support has been dropped from the tree. In particular, the following packages no longer support it:
+
+  - chromium
+
+  - firefox
+
+  - qt48
+
+  - qt5.qtwebkit
+
+  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 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.
+
+- 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.
+
+- [services.xserver.videoDrivers](options.html#opt-services.xserver.videoDrivers) no longer uses the deprecated `cirrus` and `vesa` device dependent X drivers by default. It also enables both `amdgpu` and `nouveau` drivers by default now.
+
+- The `kindlegen` 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 [the discussion](https://github.com/NixOS/nixpkgs/issues/96439) for more info.
+
+- 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 [services.apache-kafka.jre](options.html#opt-services.apache-kafka.jre) option. Furthermore, the default list of [services.apache-kafka.jvmOptions](options.html#opt-services.apache-kafka.jvmOptions) have been removed. You should set your own according to the [upstream documentation](https://kafka.apache.org/documentation/#java) for your Kafka version.
+
+- 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:
+
+  ```nix
+  {
+    environment.systemPackages = [
+      pkgs.kodi
+    ];
+
+    nixpkgs.config.kodi = {
+      enableInputStreamAdaptive = true;
+      enableVFSSFTP = true;
+    };
+  }
+  ```
+
+  All Kodi `config` flags have been removed, and as a result the above configuration should now be written as:
+
+  ```nix
+  {
+    environment.systemPackages = [
+      (pkgs.kodi.withPackages (p: with p; [
+        inputstream-adaptive
+        vfs-sftp
+      ]))
+    ];
+  }
+  ```
+
+- `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.
+
+- 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 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`.
+
+- The `services.zigbee2mqtt.config` option has been renamed to `services.zigbee2mqtt.settings` and now follows [RFC 0042](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md).
+
+ 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.
+
+- 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.
+
+- 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.
+
+## Other Notable Changes {#sec-release-21.05-notable-changes}
+
+- `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.
+
+- [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.
+
+- 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.
+
+- [Nginx](https://nginx.org) has been updated to stable version 1.20.0. Now nginx uses the zlib-ng library by default.
+
+- KDE Gear (formerly KDE Applications) is upgraded to 21.04, see its [release notes](https://kde.org/announcements/gear/21.04/) for details.
+
+  The `kdeApplications` package set is now `kdeGear`, in keeping with the new name. The old name remains for compatibility, but it is deprecated.
+
+- [Libreswan](https://libreswan.org/) 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 `/etc/ipsec.d/ ` directory. In addition, two new options have been added to specify connection policies ([services.libreswan.policies](options.html#opt-services.libreswan.policies)) and disable send/receive redirects ([services.libreswan.disableRedirects](options.html#opt-services.libreswan.disableRedirects)).
+
+- The Mailman NixOS module (`services.mailman`) has a new option [services.mailman.enablePostfix](options.html#opt-services.mailman.enablePostfix), defaulting to true, that controls integration with Postfix.
+
+  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 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.
+
+  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.
+
+- The `mediatomb` service declares new options. It also adapts existing options so the configuration generation is now lazy. The existing option `customCfg` (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 `transcodingOption` (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 `mediaDirectories` allows the users to declare autoscan media directories from their nixos configuration:
+
+  ```nix
+  {
+    services.mediatomb.mediaDirectories = [
+      { path = "/var/lib/mediatomb/pictures"; recursive = false; hidden-files = false; }
+      { path = "/var/lib/mediatomb/audio"; recursive = true; hidden-files = false; }
+    ];
+  }
+  ```
+
+- The Unbound DNS resolver service (`services.unbound`) has been refactored to allow reloading, control sockets and to fix startup ordering issues.
+
+  It is now possible to enable a local UNIX control socket for unbound by setting the [services.unbound.localControlSocketPath](options.html#opt-services.unbound.localControlSocketPath) option.
+
+  Previously we just applied a very minimal set of restrictions and trusted unbound to properly drop root privs and capabilities.
+
+  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 `unbound` user with the required capabilities instead of letting unbound do the chroot & uid/gid changes.
+
+  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 `Type` set to `notify`, therefore we are now also using the `unbound-with-systemd` package here. Unbound will start up, read the configuration files and start listening on the configured ports before systemd will declare the unit `active (running)`. 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 `nss-lookup.target` or `unbound.service` are now be able to use unbound when those targets have been reached.
+
+  Additionally to the much stricter runtime environment the `/dev/urandom` 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.
+
+  The `preStart` script is now only required if we enabled the trust anchor updates (which are still enabled by default).
+
+  Another benefit of the refactoring is that we can now issue reloads via either `pkill -HUP unbound` and `systemctl reload unbound` 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 `/etc/unbound/unbound.conf` as that is the default in the CLI tooling which in turn enables us to use `unbound-control` without passing a custom configuration location.
+
+  The module has also been reworked to be [RFC 0042](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md) compliant. As such, `sevices.unbound.extraConfig` has been removed and replaced by [services.unbound.settings](options.html#opt-services.unbound.settings). `services.unbound.interfaces` has been renamed to `services.unbound.settings.server.interface`.
+
+  `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.
+
+- 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.
+
+  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 [systemd.enableUnifiedCgroupHierarchy](options.html#opt-systemd.enableUnifiedCgroupHierarchy) = `false`; and rebooting.
+
+- PulseAudio was upgraded to 14.0, with changes to the handling of default sinks. See its [release notes](https://www.freedesktop.org/wiki/Software/PulseAudio/Notes/14.0/).
+
+- 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.
+
+- In the ACME module, the data used to build the hash for the account directory has changed to accomodate new features to reduce account rate limit issues. This will trigger new account creation on the first rebuild following this update. No issues are expected to arise from this, thanks to the new account creation handling.
+
+- [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.
+
+- The `security.apparmor` module, for the [AppArmor](https://gitlab.com/apparmor/apparmor/-/wikis/Documentation) 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 [security.apparmor.policies](options.html#opt-security.apparmor.policies), replacing the previous `profiles` option to provide a way to disable a profile and to select whether to confine in enforce mode (default) or in complain mode (see `journalctl -b --grep apparmor`). Security-minded users may also want to enable [security.apparmor.killUnconfinedConfinables](options.html#opt-security.apparmor.killUnconfinedConfinables), at the cost of having some of their processes killed when updating to a NixOS version introducing new AppArmor profiles.
+
+- The GNOME desktop manager once again installs gnome.epiphany by default.
+
+- NixOS now generates empty `/etc/netgroup`. `/etc/netgroup` defines network-wide groups and may affect to setups using NIS.
+
+- Platforms, like `stdenv.hostPlatform`, no longer have a `platform` attribute. It has been (mostly) flattened away:
+
+  - `platform.gcc` is now `gcc`
+
+  - `platform.kernel*` is now `linux-kernel.*`
+
+  Additionally, `platform.kernelArch` moved to the top level as `linuxArch` to match the other `*Arch` variables.
+
+  The `platform` grouping of these things never meant anything, and was just a historial/implementation artifact that was overdue removal.
+
+- `services.restic` now uses a dedicated cache directory for every backup defined in `services.restic.backups`. The old global cache directory, `/root/.cache/restic`, is now unused and can be removed to free up disk space.
+
+- `isync`: The `isync` compatibility wrapper was removed and the Master/Slave terminology has been deprecated and should be replaced with Far/Near in the configuration file.
+
+- 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.
+
+- The `rustPlatform.buildRustPackage` 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 `buildRustPackage`. However, these changes also required several API changes to `buildRustPackage` itself:
+
+  - The `target` argument was removed. Instead, `buildRustPackage` will always use the same target as the C/C++ compiler that is used.
+
+  - The `cargoParallelTestThreads` argument was removed. Parallel tests are now disabled through `dontUseCargoParallelTests`.
+
+- The `rustPlatform.maturinBuildHook` hook was added. This hook can be used with `buildPythonPackage` to build Python packages that are written in Rust and use Maturin as their build tool.
+
+- Kubernetes has [deprecated docker](https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/) 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.
+
+- The GitLab module now has support for automatic backups. A schedule can be set with the [services.gitlab.backup.startAt](options.html#opt-services.gitlab.backup.startAt) option.
+
+- Prior to this release, systemd would also read system units from an undocumented `/etc/systemd-mutable/system` path. This path has been dropped from the defaults. That path (or others) can be re-enabled by adding it to the [boot.extraSystemdUnitPaths](options.html#opt-boot.extraSystemdUnitPaths) list.
+
+- PostgreSQL 9.5 is scheduled EOL during the 21.05 life cycle and has been removed.
+
+- [Xfce4](https://www.xfce.org/) relies on GIO/GVfs for userspace virtual filesystem access in applications like [thunar](https://docs.xfce.org/xfce/thunar/) and [gigolo](https://docs.xfce.org/apps/gigolo/). 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:
+
+  ```nix
+  {
+    services.gvfs.package = pkgs.gvfs.override { samba = null; };
+  }
+  ```
+
+- The newly enabled `systemd-pstore.service` now automatically evacuates crashdumps and panic logs from the persistent storage to `/var/lib/systemd/pstore`. This prevents NVRAM from filling up, which ensures the latest diagnostic data is always stored and alleviates problems with writing new boot configurations.
+
+- Nixpkgs now contains [automatically packaged GNOME Shell extensions](https://github.com/NixOS/nixpkgs/pull/118232) from the [GNOME Extensions](https://extensions.gnome.org/) portal. You can find them, filed by their UUID, under `gnome38Extensions` attribute for GNOME 3.38 and under `gnome40Extensions` for GNOME 40. Finally, the `gnomeExtensions` 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.
+
+- 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 `lfe_1_2` as it could not build with R21+. Moving forward, we expect to only support 3 yearly releases of OTP.
diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md
new file mode 100644
index 00000000000..310d32cfdd7
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-2111.section.md
@@ -0,0 +1,575 @@
+# Release 21.11 (“Porcupineâ€, 2021/11/30) {#sec-release-21.11}
+
+- Support is planned until the end of June 2022, handing over to 22.05.
+
+## Highlights {#sec-release-21.11-highlights}
+
+In addition to numerous new and upgraded packages, this release has the following highlights:
+
+- Nix has been updated to version 2.4, reference its [release notes](https://discourse.nixos.org/t/nix-2-4-released/15822) for more information on what has changed. The previous version of Nix, 2.3.16, remains available for the time being in the `nix_2_3` package.
+
+- `iptables` is now using `nf_tables` under the hood, by using `iptables-nft`,
+  similar to [Debian](https://wiki.debian.org/nftables#Current_status) and
+  [Fedora](https://fedoraproject.org/wiki/Changes/iptables-nft-default).
+  This means, `ip[6]tables`, `arptables` and `ebtables` commands  will actually
+  show rules from some specific tables in the `nf_tables` kernel subsystem.
+  In case you're migrating from an older release without rebooting, there might
+  be cases where you end up with iptable rules configured both in the legacy
+  `iptables` kernel backend, as well as in the `nf_tables` backend.
+  This can lead to confusing firewall behaviour. An `iptables-save` after
+  switching will complain about "iptables-legacy tables present".
+  It's probably best to reboot after the upgrade, or manually removing all
+  legacy iptables rules (via the `iptables-legacy` package).
+
+- systemd got an `nftables` backend, and configures (networkd) rules in their
+  own `io.systemd.*` tables. Check `nft list ruleset` to see these rules, not
+  `iptables-save` (which only shows `iptables`-created rules.
+
+- PHP now defaults to PHP 8.0, updated from 7.4.
+
+- kops now defaults to 1.21.1, which uses containerd as the default runtime.
+
+- `python3` now defaults to Python 3.9, updated from Python 3.8.
+
+- PostgreSQL now defaults to major version 13.
+
+- spark now defaults to spark 3, updated from 2. A [migration guide](https://spark.apache.org/docs/latest/core-migration-guide.html#upgrading-from-core-24-to-30) is available.
+
+- Improvements have been made to the Hadoop module and package:
+  - HDFS and YARN now support production-ready highly available deployments with automatic failover.
+  - Hadoop now defaults to Hadoop 3, updated from 2.
+  - JournalNode, ZKFS and HTTPFS services have been added.
+
+- Activation scripts can now, optionally, be run during a `nixos-rebuild dry-activate` and can detect the dry activation by reading `$NIXOS_ACTION`.
+  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.
+
+- KDE Plasma now finally works on Wayland.
+
+- bash now defaults to major version 5.
+
+- Systemd was updated to version 249 (from 247).
+
+- Pantheon desktop has been updated to version 6. Due to changes of screen locker, if locking doesn't work for you, please try `gsettings set org.gnome.desktop.lockdown disable-lock-screen false`.
+
+- `kubernetes-helm` now defaults to 3.7.0, which introduced some breaking changes to the experimental OCI manifest format. See [HIP 6](https://github.com/helm/community/blob/main/hips/hip-0006.md) for more details.
+  `helmfile` also defaults to 0.141.0, which is the minimum compatible version.
+
+- GNOME has been upgraded to 41. Please take a look at their [Release Notes](https://help.gnome.org/misc/release-notes/41.0/) for details.
+
+- LXD support was greatly improved:
+  - building LXD images from configurations is now directly possible with just nixpkgs
+  - hydra is now building nixOS LXD images that can be used standalone with full nixos-rebuild support
+
+- OpenSSH was updated to version 8.8p1
+  - 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
+  - These can be re-enabled, see the [OpenSSH changelog](https://www.openssh.com/txt/release-8.8) for details
+
+- ORY Kratos was updated to version 0.8.0-alpha.3
+  - This release requires you to run SQL migrations. Please, as always, create a backup of your database first!
+  - 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.
+  - 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.
+  - for more details, see [Release Notes](https://github.com/ory/kratos/releases/tag/v0.8.0-alpha.1).
+
+## New Services {#sec-release-21.11-new-services}
+
+- [btrbk](https://digint.ch/btrbk/index.html), 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 [services.btrbk](options.html#opt-services.brtbk.instances).
+
+- [clipcat](https://github.com/xrelkd/clipcat/), an X11 clipboard manager written in Rust. Available at [services.clipcat](options.html#opt-services.clipcat.enable).
+
+- [dex](https://github.com/dexidp/dex), an OpenID Connect (OIDC) identity and OAuth 2.0 provider. Available at [services.dex](options.html#opt-services.dex.enable).
+
+- [geoipupdate](https://github.com/maxmind/geoipupdate), a GeoIP database updater from MaxMind. Available as [services.geoipupdate](options.html#opt-services.geoipupdate.enable).
+
+- [Jibri](https://github.com/jitsi/jibri), a service for recording or streaming a Jitsi Meet conference. Available as [services.jibri](options.html#opt-services.jibri.enable).
+
+- [Kea](https://www.isc.org/kea/), ISCs 2nd generation DHCP and DDNS server suite. Available at [services.kea](options.html#opt-services.kea.dhcp4).
+
+- [owncast](https://owncast.online/), self-hosted video live streaming solution. Available at [services.owncast](options.html#opt-services.owncast.enable).
+
+- [PeerTube](https://joinpeertube.org/), developed by Framasoft, is the free and decentralized alternative to video platforms. Available at [services.peertube](options.html#opt-services.peertube.enable).
+
+- [sourcehut](https://sr.ht), a collection of tools useful for software development. Available as [services.sourcehut](options.html#opt-services.sourcehut.enable).
+
+- [ucarp](https://download.pureftpd.org/pub/ucarp/README), an userspace implementation of the Common Address Redundancy Protocol (CARP). Available as [networking.ucarp](options.html#opt-networking.ucarp.enable).
+
+- Users of flashrom should migrate to [programs.flashrom.enable](options.html#opt-programs.flashrom.enable) and add themselves to the `flashrom` group to be able to access programmers supported by flashrom.
+
+- [vikunja](https://vikunja.io), a to-do list app. Available as [services.vikunja](#opt-services.vikunja.enable).
+
+- [opensnitch](https://github.com/evilsocket/opensnitch), an application firewall. Available as [services.opensnitch](#opt-services.opensnitch.enable).
+
+- [snapraid](https://www.snapraid.it/), a backup program for disk arrays.
+  Available as [snapraid](#opt-snapraid.enable).
+
+- [Hockeypuck](https://github.com/hockeypuck/hockeypuck), a OpenPGP Key Server. Available as [services.hockeypuck](#opt-services.hockeypuck.enable).
+
+- [buildkite-agent-metrics](https://github.com/buildkite/buildkite-agent-metrics), a command-line tool for collecting Buildkite agent metrics, now has a Prometheus exporter available as [services.prometheus.exporters.buildkite-agent](#opt-services.prometheus.exporters.buildkite-agent.enable).
+
+- [influxdb-exporter](https://github.com/prometheus/influxdb_exporter) a Prometheus exporter that exports metrics received on an InfluxDB compatible endpoint is now available as [services.prometheus.exporters.influxdb](#opt-services.prometheus.exporters.influxdb.enable).
+
+- [mx-puppet-discord](https://github.com/matrix-discord/mx-puppet-discord), a discord puppeting bridge for matrix. Available as [services.mx-puppet-discord](#opt-services.mx-puppet-discord.enable).
+
+- [MeshCentral](https://www.meshcommander.com/meshcentral2/overview), a remote administration service ("TeamViewer but self-hosted and with more features") is now available with a package and a module: [services.meshcentral.enable](#opt-services.meshcentral.enable)
+
+- [moonraker](https://github.com/Arksine/moonraker), an API web server for Klipper.
+  Available as [moonraker](#opt-services.moonraker.enable).
+
+- [influxdb2](https://github.com/influxdata/influxdb), a Scalable datastore for metrics, events, and real-time analytics. Available as [services.influxdb2](#opt-services.influxdb2.enable).
+
+- [isso](https://posativ.org/isso/), a commenting server similar to Disqus.
+  Available as [isso](#opt-services.isso.enable)
+
+- [navidrome](https://www.navidrome.org/), a personal music streaming server with
+  subsonic-compatible api. Available as [navidrome](#opt-services.navidrome.enable).
+
+- [fluidd](https://docs.fluidd.xyz/), a Klipper web interface for managing 3d printers using moonraker. Available as [fluidd](#opt-services.fluidd.enable).
+
+- [sx](https://github.com/earnestly/sx), a simple alternative to both xinit and startx for starting a Xorg server. Available as [services.xserver.displayManager.sx](#opt-services.xserver.displayManager.sx.enable)
+
+- [postfixadmin](https://postfixadmin.sourceforge.io/), a web based virtual user administration interface for Postfix mail servers. Available as [postfixadmin](#opt-services.postfixadmin.enable).
+
+- [prowlarr](https://wiki.servarr.com/prowlarr), an indexer manager/proxy built on the popular arr .net/reactjs base stack [services.prowlarr](#opt-services.prowlarr.enable).
+
+- [soju](https://sr.ht/~emersion/soju), a user-friendly IRC bouncer. Available as [services.soju](options.html#opt-services.soju.enable).
+
+- [nats](https://nats.io/), a high performance cloud and edge messaging system. Available as [services.nats](#opt-services.nats.enable).
+
+- [git](https://git-scm.com), a distributed version control system. Available as [programs.git](options.html#opt-programs.git.enable).
+
+- [parsedmarc](https://domainaware.github.io/parsedmarc/), a service
+  which parses incoming [DMARC](https://dmarc.org/) reports and stores
+  or sends them to a downstream service for further analysis.
+  Documented in [its manual entry](#module-services-parsedmarc).
+
+- [spark](https://spark.apache.org/), a unified analytics engine for large-scale data processing.
+
+- [touchegg](https://github.com/JoseExposito/touchegg), a multi-touch gesture recognizer. Available as [services.touchegg](#opt-services.touchegg.enable).
+
+- [pantheon-tweaks](https://github.com/pantheon-tweaks/pantheon-tweaks), an unofficial system settings panel for Pantheon. Available as [programs.pantheon-tweaks](#opt-programs.pantheon-tweaks.enable).
+
+- [joycond](https://github.com/DanielOgorchock/joycond), a service that uses `hid-nintendo` to provide nintendo joycond pairing and better nintendo switch pro controller support.
+
+- [multipath](https://github.com/opensvc/multipath-tools), the device mapper multipath (DM-MP) daemon. Available as [services.multipath](#opt-services.multipath.enable).
+
+- [seafile](https://www.seafile.com/en/home/), an open source file syncing & sharing software. Available as [services.seafile](options.html#opt-services.seafile.enable).
+
+- [rasdaemon](https://github.com/mchehab/rasdaemon), a hardware error logging daemon. Available as [hardware.rasdaemon](#opt-hardware.rasdaemon.enable).
+
+- `code-server`-module now available
+
+- [xmrig](https://github.com/xmrig/xmrig), a high performance, open source, cross platform RandomX, KawPow, CryptoNight and AstroBWT unified CPU/GPU miner and RandomX benchmark.
+
+- Auto nice daemons [ananicy](https://github.com/Nefelim4ag/Ananicy) and [ananicy-cpp](https://gitlab.com/ananicy-cpp/ananicy-cpp/). Available as [services.ananicy](#opt-services.ananicy.enable).
+
+- [smartctl_exporter](https://github.com/prometheus-community/smartctl_exporter), a Prometheus exporter for [S.M.A.R.T.](https://en.wikipedia.org/wiki/S.M.A.R.T.) data. Available as [services.prometheus.exporters.smartctl](options.html#opt-services.prometheus.exporters.smartctl.enable).
+
+## Backward Incompatibilities {#sec-release-21.11-incompatibilities}
+
+- The NixOS VM test framework, `pkgs.nixosTest`/`make-test-python.nix`, now requires detaching commands such as `succeed("foo &")` and `succeed("foo | xclip -i")` to close stdout.
+  This can be done with a redirect such as `succeed("foo >&2 &")`. This breaking change was necessitated by a race condition causing tests to fail or hang.
+  It applies to all methods that invoke commands on the nodes, including `execute`, `succeed`, `fail`, `wait_until_succeeds`, `wait_until_fails`.
+
+- The `services.wakeonlan` option was removed, and replaced with `networking.interfaces.<name>.wakeOnLan`.
+
+- The `security.wrappers` 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.
+
+- Since `iptables` now uses `nf_tables` backend and `ipset` doesn't support it, some applications (ferm, shorewall, firehol) may have limited functionality.
+
+- The `paperless` module and package have been removed. All users should migrate to the
+  successor `paperless-ng` instead. The Paperless project [has been
+  archived](https://github.com/the-paperless-project/paperless/commit/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4)
+  and advises all users to use `paperless-ng` instead.
+
+  Users can use the `services.paperless-ng` module as a replacement while noting the following incompatibilities:
+
+  - `services.paperless.ocrLanguages` has no replacement. Users should migrate to [`services.paperless-ng.extraConfig`](options.html#opt-services.paperless-ng.extraConfig) instead:
+
+  ```nix
+  {
+    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 = "deu+eng+jpn"; # German & English & Japanse
+    };
+  }
+  ```
+
+  - If you previously specified `PAPERLESS_CONSUME_MAIL_*` settings in
+    `services.paperless.extraConfig` you should remove those options now. You
+    now _must_ define those settings in the admin interface of paperless-ng.
+
+  - Option `services.paperless.manage` no longer exists.
+    Use the script at `${services.paperless-ng.dataDir}/paperless-ng-manage` instead.
+    Note that this script only exists after the `paperless-ng` service has been
+    started at least once.
+
+  - 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.
+
+    To do so, enter the data directory (the value of
+    `services.paperless-ng.dataDir`, `/var/lib/paperless` by default), switch
+    to the paperless user and execute the management command like below:
+
+    ```
+    $ 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.
+    ```
+
+- The `staticjinja` package has been upgraded from 1.0.4 to 4.1.1
+
+- Firefox v91 does not support addons with invalid signature anymore. Firefox ESR needs to be used for nix addon support.
+
+- 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)).
+
+- [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
+  {
+    users.users.foo = {
+      isSystemUser = true;
+    };
+  }
+  ```
+  also create a group for your user:
+  ```nix
+  {
+    users.users.foo = {
+      isSystemUser = true;
+      group = "foo";
+    };
+    users.groups.foo = {};
+  }
+  ```
+
+- `services.geoip-updater` was broken and has been replaced by [services.geoipupdate](options.html#opt-services.geoipupdate.enable).
+
+- `ihatemoney` has been updated to version 5.1.1 ([release notes](https://github.com/spiral-project/ihatemoney/blob/5.1.1/CHANGELOG.rst)). If you serve ihatemoney by HTTP rather than HTTPS, you must set [services.ihatemoney.secureCookie](options.html#opt-services.ihatemoney.secureCookie) to `false`.
+
+- PHP 7.3 is no longer supported due to upstream not supporting this version for the entire lifecycle of the 21.11 release.
+
+- Those making use of `buildBazelPackage` will need to regenerate the fetch hashes (preferred), or set `fetchConfigured = false;`.
+
+- `consul` was upgraded to a new major release with breaking changes, see [upstream changelog](https://github.com/hashicorp/consul/releases/tag/v1.10.0).
+
+- fsharp41 has been removed in preference to use the latest dotnet-sdk
+
+- The following F#-related packages have been removed for being unmaintaned. Please use `fetchNuGet` for specific packages.
+
+  - ExtCore
+  - Fake
+  - Fantomas
+  - FsCheck
+  - FsCheck262
+  - FsCheckNunit
+  - FSharpAutoComplete
+  - FSharpCompilerCodeDom
+  - FSharpCompilerService
+  - FSharpCompilerTools
+  - FSharpCore302
+  - FSharpCore3125
+  - FSharpCore4001
+  - FSharpCore4117
+  - FSharpData
+  - FSharpData225
+  - FSharpDataSQLProvider
+  - FSharpFormatting
+  - FsLexYacc
+  - FsLexYacc706
+  - FsLexYaccRuntime
+  - FsPickler
+  - FsUnit
+  - Projekt
+  - Suave
+  - UnionArgParser
+  - ExcelDnaRegistration
+  - MathNetNumerics
+
+- `programs.x2goserver` is now `services.x2goserver`
+
+- The following dotnet-related packages have been removed for being unmaintaned. Please use `fetchNuGet` for specific packages.
+  - Autofac
+  - SystemValueTuple
+  - MicrosoftDiaSymReader
+  - MicrosoftDiaSymReaderPortablePdb
+  - SystemCollectionsImmutable
+  - SystemCollectionsImmutable131
+  - SystemReflectionMetadata
+  - NUnit350
+  - Deedle
+  - ExcelDna
+  - GitVersionTree
+  - NDeskOptions
+
+* The `antlr` package now defaults to the 4.x release instead of the
+  old 2.7.7 version.
+
+* The `pulseeffects` package updated to [version 4.x](https://github.com/wwmm/easyeffects/releases/tag/v6.0.0) and renamed to `easyeffects`.
+
+* The `libwnck` package now defaults to the 3.x release instead of the
+  old 2.31.0 version.
+
+* The `bitwarden_rs` packages and modules were renamed to `vaultwarden`
+  [following upstream](https://github.com/dani-garcia/vaultwarden/discussions/1642). More specifically,
+
+  - `pkgs.bitwarden_rs`, `pkgs.bitwarden_rs-sqlite`, `pkgs.bitwarden_rs-mysql` and
+    `pkgs.bitwarden_rs-postgresql` were renamed to `pkgs.vaultwarden`, `pkgs.vaultwarden-sqlite`,
+    `pkgs.vaultwarden-mysql` and `pkgs.vaultwarden-postgresql`, respectively.
+
+    - Old names are preserved as aliases for backwards compatibility, but may be removed in the future.
+    - The `bitwarden_rs` executable was also renamed to `vaultwarden` in all packages.
+
+  - `pkgs.bitwarden_rs-vault` was renamed to `pkgs.vaultwarden-vault`.
+
+    - `pkgs.bitwarden_rs-vault` is preserved as an alias for backwards compatibility, but may be removed in the future.
+    - The static files were moved from `/usr/share/bitwarden_rs` to `/usr/share/vaultwarden`.
+
+  - The `services.bitwarden_rs` config module was renamed to `services.vaultwarden`.
+
+    - `services.bitwarden_rs` is preserved as an alias for backwards compatibility, but may be removed in the future.
+
+  - `systemd.services.bitwarden_rs`, `systemd.services.backup-bitwarden_rs` and `systemd.timers.backup-bitwarden_rs`
+    were renamed to `systemd.services.vaultwarden`, `systemd.services.backup-vaultwarden` and
+    `systemd.timers.backup-vaultwarden`, respectively.
+
+    - Old names are preserved as aliases for backwards compatibility, but may be removed in the future.
+
+  - `users.users.bitwarden_rs` and `users.groups.bitwarden_rs` were renamed to `users.users.vaultwarden` and
+    `users.groups.vaultwarden`, respectively.
+
+  - The data directory remains located at `/var/lib/bitwarden_rs`, for backwards compatibility.
+
+- `yggdrasil` was upgraded to a new major release with breaking changes, see [upstream changelog](https://github.com/yggdrasil-network/yggdrasil-go/releases/tag/v0.4.0).
+
+- `icingaweb2` was upgraded to a new release which requires a manual database upgrade, see [upstream changelog](https://github.com/Icinga/icingaweb2/releases/tag/v2.9.0).
+
+- The `isabelle` package has been upgraded from 2020 to 2021
+
+- the `mingw-64` package has been upgraded from 6.0.0 to 9.0.0
+
+- `tt-rss` was upgraded to the commit on 2021-06-21, which has breaking changes. If you use `services.tt-rss.extraConfig` you should migrate to the `putenv`-style configuration. See [this Discourse post](https://community.tt-rss.org/t/rip-config-php-hello-classes-config-php/4337) in the tt-rss forums for more details.
+
+- The following Visual Studio Code extensions were renamed to keep the naming convention uniform.
+
+  - `bbenoist.Nix` -> `bbenoist.nix`
+  - `CoenraadS.bracket-pair-colorizer` -> `coenraads.bracket-pair-colorizer`
+  - `golang.Go` -> `golang.go`
+
+- `services.uptimed` now uses `/var/lib/uptimed` as its stateDirectory instead of `/var/spool/uptimed`. Make sure to move all files to the new directory.
+
+- Deprecated package aliases in `emacs.pkgs.*` have been removed. These aliases were remnants of the old Emacs package infrastructure. We now use exact upstream names wherever possible.
+
+- `programs.neovim.runtime` switched to a `linkFarm` internally, making it impossible to use wildcards in the `source` argument.
+
+- The `openrazer` and `openrazer-daemon` packages as well as the `hardware.openrazer` module now require users to be members of the `openrazer` group instead of `plugdev`. With this change, users no longer need be granted the entire set of `plugdev` group permissions, which can include permissions other than those required by `openrazer`. This is desirable from a security point of view. The setting [`harware.openrazer.users`](options.html#opt-services.hardware.openrazer.users) can be used to add users to the `openrazer` group.
+
+- 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 `services.xserver.dpi`.
+
+- The `yambar` package has been split into `yambar` and `yambar-wayland`, corresponding to the xorg and wayland backend respectively. Please switch to `yambar-wayland` if you are on wayland.
+
+- The `services.minio` module gained an additional option `consoleAddress`, that
+  configures the address and port the web UI is listening, it defaults to `:9001`.
+  To be able to access the web UI this port needs to be opened in the firewall.
+
+- The `varnish` package was upgraded from 6.3.x to 7.x. `varnish60` for the last LTS release is also still available.
+
+- The `kubernetes` package was upgraded to 1.22. The `kubernetes.apiserver.kubeletHttps` option was removed and HTTPS is always used.
+
+- The attribute `linuxPackages_latest_hardened` 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
+  `linuxPackages_5_10_hardened`.
+
+- The `nomad` package now defaults to a 1.1.x release instead of 1.0.x
+
+- If `exfat` is included in `boot.supportedFilesystems` and when using kernel 5.7
+  or later, the `exfatprogs` user-space utilities are used instead of `exfat`.
+
+- The `todoman` package was upgraded from 3.9.0 to 4.0.0. This introduces breaking changes in the [configuration file](https://todoman.readthedocs.io/en/stable/configure.html#configuration-file) format.
+
+- The `datadog-agent`, `datadog-integrations-core` and `datadog-process-agent` 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 `services.datadog-agent` has had breaking changes to the
+  configuration file. For details, see the [upstream changelog](https://github.com/DataDog/datadog-agent/blob/main/CHANGELOG.rst).
+
+- `opencv2` no longer includes the non-free libraries by default, and consequently `pfstools` no longer includes OpenCV support by default.  Both packages now support an `enableUnfree` option to re-enable this functionality.
+- `services.xserver.displayManager.defaultSession = "plasma5"` does not work anymore, instead use either `"plasma"` for the Plasma X11 session or `"plasmawayland"` for the Plasma Wayland sesison.
+
+- `boot.kernelParams` now only accepts one command line parameter per string. This change is aimed to reduce common mistakes like "param = 12", which would be parsed as 3 parameters.
+
+- `nix.daemonNiceLevel` and `nix.daemonIONiceLevel` have been removed in favour of the new options [`nix.daemonCPUSchedPolicy`](options.html#opt-nix.daemonCPUSchedPolicy), [`nix.daemonIOSchedClass`](options.html#opt-nix.daemonIOSchedClass) and [`nix.daemonIOSchedPriority`](options.html#opt-nix.daemonIOSchedPriority). Please refer to the options documentation and the `sched(7)` and `ioprio_set(2)` man pages for guidance on how to use them.
+
+- The `coursier` package's binary was renamed from `coursier` to `cs`. Completions which haven't worked for a while should now work with the renamed binary. To keep using `coursier`, you can create a shell alias.
+
+- The `services.mosquitto` 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.
+
+- The `fluidsynth_1` attribute has been removed, as this legacy version is no longer needed in nixpkgs. The actively maintained 2.x series is available as `fluidsynth` unchanged.
+
+- Nextcloud 20 (`pkgs.nextcloud20`) has been dropped because it was EOLed by upstream in 2021-10.
+
+- The `virtualisation.pathsInNixDB` option was renamed
+  [`virtualisation.additionalPaths`](options.html#opt-virtualisation.additionalPaths).
+
+- The `services.ddclient.password` option was removed, and replaced with `services.ddclient.passwordFile`.
+
+- The default GNAT version has been changed: The `gnat` attribute now points to `gnat11`
+  instead of `gnat9`.
+
+- `retroArchCores` has been removed. This means that using `nixpkgs.config.retroarch` to customize RetroArch cores is not supported anymore. Instead, use package overrides, for example: `retroarch.override { cores = with libretro; [ citra snes9x ]; };`. Also, `retroarchFull` derivation is available for those who want to have all RetroArch cores available.
+
+- The Linux kernel for security reasons now restricts access to BPF syscalls via `BPF_UNPRIV_DEFAULT_OFF=y`. Unprivileged access can be reenabled via the `kernel.unprivileged_bpf_disabled` sysctl knob.
+
+- `/usr` will always be included in the initial ramdisk. See the `fileSystems.<name>.neededForBoot` option.
+  If any files exist under `/usr` (which is not typical for NixOS), they will be included in the initial ramdisk, increasing its size to a possibly problematic extent.
+
+## Other Notable Changes {#sec-release-21.11-notable-changes}
+
+
+- The linux kernel package infrastructure was moved out of `all-packages.nix`, and restructured. Linux related functions and attributes now live under the `pkgs.linuxKernel` attribute set.
+  In particular the versioned `linuxPackages_*` package sets (such as `linuxPackages_5_4`) and kernels from `pkgs` were moved there and now live under `pkgs.linuxKernel.packages.*`. The unversioned ones (such as `linuxPackages_latest`) remain untouched.
+
+- In NixOS virtual machines (QEMU), the `virtualisation` module has been updated with new options:
+    - [`forwardPorts`](options.html#opt-virtualisation.forwardPorts) to configure IPv4 port forwarding,
+    - [`sharedDirectories`](options.html#opt-virtualisation.sharedDirectories) to set up shared host directories,
+    - [`resolution`](options.html#opt-virtualisation.resolution) to set the screen resolution,
+    - [`useNixStoreImage`](options.html#opt-virtualisation.useNixStoreImage) to use a disk image for the Nix store instead of 9P.
+
+  In addition, the default [`msize`](options.html#opt-virtualisation.msize) parameter in 9P filesystems (including /nix/store and all shared directories) has been increased to 16K for improved performance.
+
+- The setting [`services.openssh.logLevel`](options.html#opt-services.openssh.logLevel) `"VERBOSE"` `"INFO"`. This brings NixOS in line with upstream and other Linux distributions, and reduces log spam on servers due to bruteforcing botnets.
+
+  However, if [`services.fail2ban.enable`](options.html#opt-services.fail2ban.enable) is `true`, the `fail2ban` will override the verbosity to `"VERBOSE"`, so that `fail2ban` can observe the failed login attempts from the SSH logs.
+
+- The [`services.xserver.extraLayouts`](options.html#opt-services.xserver.extraLayouts) no longer cause additional rebuilds when a layout is added or modified.
+
+- Sway: The terminal emulator `rxvt-unicode` is no longer installed by default via `programs.sway.extraPackages`. The current default configuration uses `alacritty` (and soon `foot`) so this is only an issue when using a customized configuration and not installing `rxvt-unicode` explicitly.
+
+- `python3` now defaults to Python 3.9. Python 3.9 introduces many deprecation warnings, please look at the [What's New In Python 3.9 post](https://docs.python.org/3/whatsnew/3.9.html) for more information.
+
+- `qtile` hase been updated from '0.16.0' to '0.18.0', please check [qtile changelog](https://github.com/qtile/qtile/blob/master/CHANGELOG) for changes.
+
+- The `claws-mail` package now references the new GTK+ 3 release branch, major version 4. To use the GTK+ 2 releases, one can install the `claws-mail-gtk2` package.
+
+- The wordpress module provides a new interface which allows to use different webservers with the new option [`services.wordpress.webserver`](options.html#opt-services.wordpress.webserver).  Currently `httpd`, `caddy` and `nginx` are supported. The definitions of wordpress sites should now be set in [`services.wordpress.sites`](options.html#opt-services.wordpress.sites).
+
+  Sites definitions that use the old interface are automatically migrated in the new option. This backward compatibility will be removed in 22.05.
+
+- The dokuwiki module provides a new interface which allows to use different webservers with the new option [`services.dokuwiki.webserver`](options.html#opt-services.dokuwiki.webserver).  Currently `caddy` and `nginx` are supported. The definitions of dokuwiki sites should now be set in [`services.dokuwiki.sites`](options.html#opt-services.dokuwiki.sites).
+
+  Sites definitions that use the old interface are automatically migrated in the new option. This backward compatibility will be removed in 22.05.
+
+- The order of NSS (host) modules has been brought in line with upstream
+  recommendations:
+
+  - The `myhostname` module is placed before the `resolve` (optional) and `dns`
+    entries, but after `file` (to allow overriding via `/etc/hosts` /
+    `networking.extraHosts`, and prevent ISPs with catchall-DNS resolvers from
+    hijacking `.localhost` domains)
+  - The `mymachines` module, which provides hostname resolution for local
+    containers (registered with `systemd-machined`) is placed to the front, to
+    make sure its mappings are preferred over other resolvers.
+  - If systemd-networkd is enabled, the `resolve` module is placed before
+    `files` and `myhostname`, as it provides the same logic internally, with
+    caching.
+  - The `mdns(_minimal)` module has been updated to the new priorities.
+
+  If you use your own NSS host modules, make sure to update your priorities
+  according to these rules:
+
+  - NSS modules which should be queried before `resolved` DNS resolution should
+    use mkBefore.
+  - NSS modules which should be queried after `resolved`, `files` and
+    `myhostname`, but before `dns` should use the default priority
+  - NSS modules which should come after `dns` should use mkAfter.
+
+- The [networking.wireless](options.html#opt-networking.wireless.enable) module (based on wpa_supplicant) has been heavily reworked, solving a number of issues and adding useful features:
+  - The automatic discovery of wireless interfaces at boot has been made reliable again (issues [#101963](https://github.com/NixOS/nixpkgs/issues/101963), [#23196](https://github.com/NixOS/nixpkgs/issues/23196)).
+  - WPA3 and Fast BSS Transition (802.11r) are now enabled by default for all networks.
+  - Secrets like pre-shared keys and passwords can now be handled safely, meaning without including them in a world-readable file (`wpa_supplicant.conf` under /nix/store).
+    This is achieved by storing the secrets in a secured [environmentFile](options.html#opt-networking.wireless.environmentFile) and referring to them though environment variables that are expanded inside the configuration.
+  - With multiple interfaces declared, independent wpa_supplicant daemons are started, one for each interface (the services are named `wpa_supplicant-wlan0`, `wpa_supplicant-wlan1`, etc.).
+  - The generated `wpa_supplicant.conf` file is now formatted for easier reading.
+  - A new [scanOnLowSignal](options.html#opt-networking.wireless.scanOnLowSignal) option has been added to facilitate fast roaming between access points (enabled by default).
+  - A new [networks.&lt;name&gt;.authProtocols](options.html#opt-networking.wireless.networks._name_.authProtocols) option has been added to change the authentication protocols used when connecting to a network.
+
+- The [networking.wireless.iwd](options.html#opt-networking.wireless.iwd.enable) module has a new [networking.wireless.iwd.settings](options.html#opt-networking.wireless.iwd.settings) option.
+
+- The [services.smokeping.host](options.html#opt-services.smokeping.host) option was added and defaulted to `localhost`. Before, `smokeping` 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. [#100192](https://github.com/NixOS/nixpkgs/issues/100192). Further, the systemd service for `smokeping` got reworked defaults for increased operational stability, see [PR #144127](https://github.com/NixOS/nixpkgs/pull/144127) for details.
+
+- The [services.syncoid.enable](options.html#opt-services.syncoid.enable) 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 `zfs allow your-pool-name` and use `zfs unallow syncoid your-pool-name` to clean this up.
+
+- Zfs: `latestCompatibleLinuxPackages` is now exported on the zfs package. One can use `boot.kernelPackages = config.boot.zfs.package.latestCompatibleLinuxPackages;` to always track the latest compatible kernel with a given version of zfs.
+
+- Nginx will use the value of `sslTrustedCertificate` if provided for a virtual host, even if `enableACME` is set. This is useful for providers not using the same certificate to sign OCSP responses and server certificates.
+
+- `lib.formats.yaml`'s `generate` will not generate JSON anymore, but instead use more of the YAML-specific syntax.
+
+- MariaDB was upgraded from 10.5.x to 10.6.x. Please read the [upstream release notes](https://mariadb.com/kb/en/changes-improvements-in-mariadb-106/) for changes and upgrade instructions.
+
+- 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 [upstream release notes](https://mariadb.com/kb/en/changes-and-improvements-in-mariadb-connector-c-32/).
+
+- GNOME desktop environment now enables `QGnomePlatform` 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.
+
+- `rofi` has been updated from '1.6.1' to '1.7.0', one important thing is the removal of the old xresources based configuration setup. Read more [in rofi's changelog](https://github.com/davatorium/rofi/blob/cb12e6fc058f4a0f4f/Changelog#L1).
+
+- 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 `ipfs.localDiscovery = true;`. localDiscovery enables different instances to discover each other and share data.
+
+- `lua` and `luajit` interpreters have been patched to avoid looking into /usr/lib
+  directories, thus increasing the purity of the build.
+
+- Three new options, [xdg.mime.addedAssociations](#opt-xdg.mime.addedAssociations), [xdg.mime.defaultApplications](#opt-xdg.mime.defaultApplications), and [xdg.mime.removedAssociations](#opt-xdg.mime.removedAssociations) have been added to the [xdg.mime](#opt-xdg.mime.enable) module to allow the configuration of `/etc/xdg/mimeapps.list`.
+
+- Kopia was upgraded from 0.8.x to 0.9.x. Please read the [upstream release notes](https://github.com/kopia/kopia/releases/tag/v0.9.0) for changes and upgrade instructions.
+
+- The `systemd.network` module has gained support for the FooOverUDP link type.
+
+- The `networking` module has a new `networking.fooOverUDP` option to configure Foo-over-UDP encapsulations.
+
+- `networking.sits` now supports Foo-over-UDP encapsulation.
+
+-  The `virtualisation.libvirtd` module has been refactored and updated with new options:
+    - `virtualisation.libvirtd.qemu*` options (e.g.: `virtualisation.libvirtd.qemuRunAsRoot`) were moved to [`virtualisation.libvirtd.qemu`](options.html#opt-virtualisation.libvirtd.qemu) submodule,
+    - software TPM1/TPM2 support (e.g.: Windows 11 guests) ([`virtualisation.libvirtd.qemu.swtpm`](options.html#opt-virtualisation.libvirtd.qemu.swtpm)),
+    - custom OVMF package (e.g.: `pkgs.OVMFFull` with HTTP, CSM and Secure Boot support) ([`virtualisation.libvirtd.qemu.ovmf.package`](options.html#opt-virtualisation.libvirtd.qemu.ovmf.package)).
+
+- The `cawbird` 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.
+
+- A new option `services.prometheus.enableReload` has been added which can be enabled to reload the prometheus service when its config file changes instead of restarting.
+
+- The option `services.prometheus.environmentFile` has been removed since it was causing [issues](https://github.com/NixOS/nixpkgs/issues/126083) and Prometheus now has native support for secret files, i.e. `basic_auth.password_file` and `authorization.credentials_file`.
+
+- Dokuwiki now supports caddy! However
+  - the nginx option has been removed, in the new configuration, please use the `dokuwiki.webserver = "nginx"` instead.
+  - The "${hostname}" option has been deprecated, please use `dokuwiki.sites = [ "${hostname}" ]` instead
+
+- The [services.unifi](options.html#opt-services.unifi.enable) module has been reworked, solving a number of issues. This leads to several user facing changes:
+  - The `services.unifi.dataDir` option is removed and the data is now always located under `/var/lib/unifi/data`. This is done to make better use of systemd state direcotiry and thus making the service restart more reliable.
+  - The unifi logs can now be found under: `/var/log/unifi` instead of `/var/lib/unifi/logs`.
+  - The unifi run directory can now be found under: `/run/unifi` instead of `/var/lib/unifi/run`.
+
+- `security.pam.services.<name>.makeHomeDir` now uses `umask=0077` instead of `umask=0022` when creating the home directory.
+
+- 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 [the upgrade guide](https://grafana.com/docs/loki/latest/upgrading/#240).
+
+- `julia` now refers to `julia-stable` instead of `julia-lts`. In practice this means it has been upgraded from `1.0.4` to `1.5.4`.
+
+- RetroArch has been upgraded from version `1.8.5` to `1.9.13.2`. Since the previous release was quite old, if you're having issues after the upgrade, please delete your `$XDG_CONFIG_HOME/retroarch/retroarch.cfg` file.
+
+- hydrus has been upgraded from version `438` to `463`. Since upgrading between releases this old is advised against, be sure to have a backup of your data before upgrading. For details, see [the hydrus manual](https://hydrusnetwork.github.io/hydrus/help/getting_started_installing.html#big_updates).
+
+- More jdk and jre versions are now exposed via `java-packages.compiler`.
diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md
new file mode 100644
index 00000000000..37ff778dd9b
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-2205.section.md
@@ -0,0 +1,582 @@
+# Release 22.05 (“Quokkaâ€, 2022.05/??) {#sec-release-22.05}
+
+In addition to numerous new and upgraded packages, this release has the following highlights:
+
+- Support is planned until the end of December 2022, handing over to 22.11.
+
+## Highlights {#sec-release-22.05-highlights}
+
+- `security.acme.defaults` has been added to simplify configuring
+  settings for many certificates at once. This also opens up the
+  the option to use DNS-01 validation when using `enableACME` on
+  web server virtual hosts (e.g. `services.nginx.virtualHosts.*.enableACME`).
+
+- PHP 8.1 is now available
+
+- Mattermost has been updated to extended support release 6.3, as the previously packaged extended support release 5.37 is [reaching its end of life](https://docs.mattermost.com/upgrade/extended-support-release.html).
+  Migrations may take a while, see the [changelog](https://docs.mattermost.com/install/self-managed-changelog.html#release-v6-3-extended-support-release)
+  and [important upgrade notes](https://docs.mattermost.com/upgrade/important-upgrade-notes.html).
+
+- systemd services can now set [systemd.services.\<name\>.reloadTriggers](#opt-systemd.services) instead of `reloadIfChanged` for a more granular distinction between reloads and restarts.
+
+- [`kops`](https://kops.sigs.k8s.io) defaults to 1.22.4, 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. See the [release notes](https://kops.sigs.k8s.io/releases/1.22-notes/) for details.
+
+## New Services {#sec-release-22.05-new-services}
+
+- [aesmd](https://github.com/intel/linux-sgx#install-the-intelr-sgx-psw), the Intel SGX Architectural Enclave Service Manager. Available as [services.aesmd](#opt-services.aesmd.enable).
+
+- [rootless Docker](https://docs.docker.com/engine/security/rootless/), a `systemd --user` Docker service which runs without root permissions. Available as [virtualisation.docker.rootless.enable](options.html#opt-virtualisation.docker.rootless.enable).
+
+- [matrix-conduit](https://conduit.rs/), a simple, fast and reliable chat server powered by matrix. Available as [services.matrix-conduit](option.html#opt-services.matrix-conduit.enable).
+
+- [filebeat](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-overview.html), a lightweight shipper for forwarding and centralizing log data. Available as [services.filebeat](#opt-services.filebeat.enable).
+
+- [apfs](https://github.com/linux-apfs/linux-apfs-rw), a kernel module for mounting the Apple File System (APFS).
+
+- [FRRouting](https://frrouting.org/), a popular suite of Internet routing protocol daemons (BGP, BFD, OSPF, IS-IS, VVRP and others). Available as [services.frr](#opt-services.frr.babel.enable)
+
+- [heisenbridge](https://github.com/hifi/heisenbridge), a bouncer-style Matrix IRC bridge. Available as [services.heisenbridge](options.html#opt-services.heisenbridge.enable).
+
+- [snowflake-proxy](https://snowflake.torproject.org/), a system to defeat internet censorship. Available as [services.snowflake-proxy](options.html#opt-services.snowflake-proxy.enable).
+
+- [ergochat](https://ergo.chat), a modern IRC with IRCv3 features. Available as [services.ergochat](options.html#opt-services.ergochat.enable).
+
+- [PowerDNS-Admin](https://github.com/ngoduykhanh/PowerDNS-Admin), a web interface for the PowerDNS server. Available at [services.powerdns-admin](options.html#opt-services.powerdns-admin.enable).
+
+- [pgadmin4](https://github.com/postgres/pgadmin4), an admin interface for the PostgreSQL database. Available at [services.pgadmin](options.html#opt-services.pgadmin.enable).
+
+- [input-remapper](https://github.com/sezanzeb/input-remapper), an easy to use tool to change the mapping of your input device buttons. Available at [services.input-remapper](options.html#opt-services.input-remapper.enable).
+
+- [InvoicePlane](https://invoiceplane.com), web application for managing and creating invoices. Available at [services.invoiceplane](options.html#opt-services.invoiceplane.enable).
+
+- [maddy](https://maddy.email), a composable all-in-one mail server. Available as [services.maddy](options.html#opt-services.maddy.enable).
+
+- [K40-Whisperer](https://www.scorchworks.com/K40whisperer/k40whisperer.html), a program to control cheap Chinese laser cutters. Available as [programs.k40-whisperer.enable](options.html#opt-programs.k4-whisperer.enable). Users must add themselves to the `k40` group to be able to access the device.
+
+- [mtr-exporter](https://github.com/mgumz/mtr-exporter), a Prometheus exporter for mtr metrics. Available as [services.mtr-exporter](options.html#opt-services.mtr-exporter.enable).
+
+- [prometheus-pve-exporter](https://github.com/prometheus-pve/prometheus-pve-exporter), a tool that exposes information from the Proxmox VE API for use by Prometheus. Available as [services.prometheus.exporters.pve](options.html#opt-services.prometheus.exporters.pve).
+
+- [tetrd](https://tetrd.app), share your internet connection from your device to your PC and vice versa through a USB cable. Available at [services.tetrd](#opt-services.tetrd.enable).
+
+- [agate](https://github.com/mbrubeck/agate), a very simple server for the Gemini hypertext protocol. Available as [services.agate](options.html#opt-services.agate.enable).
+
+- [ArchiSteamFarm](https://github.com/JustArchiNET/ArchiSteamFarm), a C# application with primary purpose of idling Steam cards from multiple accounts simultaneously. Available as [services.archisteamfarm](options.html#opt-services.archisteamfarm.enable).
+
+- [teleport](https://goteleport.com), allows engineers and security professionals to unify access for SSH servers, Kubernetes clusters, web applications, and databases across all environments. Available at [services.teleport](#opt-services.teleport.enable).
+
+- [BaGet](https://loic-sharma.github.io/BaGet/), a lightweight NuGet and symbol server. Available at [services.baget](#opt-services.baget.enable).
+
+- [moosefs](https://moosefs.com), fault tolerant petabyte distributed file system.
+  Available as [moosefs](#opt-services.moosefs.client.enable).
+
+- [prosody-filer](https://github.com/ThomasLeister/prosody-filer), a server for handling XMPP HTTP Upload requests. Available at [services.prosody-filer](#opt-services.prosody-filer.enable).
+
+- [systembus-notify](https://github.com/rfjakob/systembus-notify), allow system level notifications to reach the users. Available as [services.systembus-notify](opt-services.systembus-notify.enable). Please keep in mind that this service should only be enabled on machines with fully trusted users, as any local user is able to DoS user sessions by spamming notifications.
+
+- [ethercalc](https://github.com/audreyt/ethercalc), an online collaborative
+  spreadsheet. Available as [services.ethercalc](options.html#opt-services.ethercalc.enable).
+
+- [nbd](https://nbd.sourceforge.io/), a Network Block Device server. Available as [services.nbd](options.html#opt-services.nbd.server.enable).
+
+- [timetagger](https://timetagger.app), an open source time-tracker with an intuitive user experience and powerful reporting. [services.timetagger](options.html#opt-services.timetagger.enable).
+
+- [rstudio-server](https://www.rstudio.com/products/rstudio/#rstudio-server), a browser-based version of the RStudio IDE for the R programming language. Available as [services.rstudio-server](options.html#opt-services.rstudio-server.enable).
+
+- [headscale](https://github.com/juanfont/headscale), an Open Source implementation of the [Tailscale](https://tailscale.io) Control Server. Available as [services.headscale](options.html#opt-services.headscale.enable)
+
+- [blocky](https://0xerr0r.github.io/blocky/), fast and lightweight DNS proxy as ad-blocker for local network with many features.
+
+- [pacemaker](https://clusterlabs.org/pacemaker/) cluster resource manager
+
+<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
+
+## Backward Incompatibilities {#sec-release-22.05-incompatibilities}
+
+- `pkgs.ghc` now refers to `pkgs.targetPackages.haskellPackages.ghc`.
+  This _only_ makes a difference if you are cross-compiling and will
+  ensure that `pkgs.ghc` always runs on the host platform and compiles
+  for the target platform (similar to `pkgs.gcc` for example).
+  `haskellPackages.ghc` still behaves as before, running on the build
+  platform and compiling for the host platform (similar to `stdenv.cc`).
+  This means you don't have to adjust your derivations if you use
+  `haskellPackages.callPackage`, but when using `pkgs.callPackage` and
+  taking `ghc` as an input, you should now use `buildPackages.ghc`
+  instead to ensure cross compilation keeps working (or switch to
+  `haskellPackages.callPackage`).
+
+- `pkgs.ghc.withPackages` as well as `haskellPackages.ghcWithPackages` etc.
+  now needs be overridden directly, as opposed to overriding the result of
+  calling it. Additionally, the `withLLVM` parameter has been renamed to
+  `useLLVM`. So instead of `(ghc.withPackages (p: [])).override { withLLVM = true; }`,
+  one needs to use `(ghc.withPackages.override { useLLVM = true; }) (p: [])`.
+
+- The `home-assistant` module now requires users that don't want their
+  configuration to be managed declaratively to set
+  `services.home-assistant.config = null;`. This is required
+  due to the way default settings are handled with the new settings style.
+
+  Additionally the default list of `extraComponents` now includes the minimal
+  dependencies to successfully complete the [onboarding](https://www.home-assistant.io/getting-started/onboarding/)
+  procedure.
+
+- `pkgs.emacsPackages.orgPackages` is removed because org elpa is deprecated.
+  The packages in the top level of `pkgs.emacsPackages`, such as org and
+  org-contrib, refer to the ones in `pkgs.emacsPackages.elpaPackages` and
+  `pkgs.emacsPackages.nongnuPackages` where the new versions will release.
+
+- `services.kubernetes.addons.dashboard` was removed due to it being an outdated version.
+
+- `services.kubernetes.scheduler.{port,address}` now set `--secure-port` and `--bind-address` instead of `--port` and `--address`, since the former have been deprecated and are no longer functional in kubernetes>=1.23. Ensure that you are not relying on the insecure behaviour before upgrading.
+
+- `services.k3s.enable` no longer implies `systemd.enableUnifiedCgroupHierarchy = false`, and will default to the 'systemd' cgroup driver when using `services.k3s.docker = true`.
+  This change may require a reboot to take effect, and k3s may not be able to run if the boot cgroup hierarchy does not match its configuration.
+  The previous behavior may be retained by explicitly setting `systemd.enableUnifiedCgroupHierarchy = false` in your configuration.
+
+- `fonts.fonts` no longer includes ancient bitmap fonts when both `config.services.xserver.enable` and `config.nixpkgs.config.allowUnfree` are enabled.
+  If you still want these fonts, use:
+
+  ```nix
+  {
+    fonts.fonts = [
+      pkgs.xorg.fontbhlucidatypewriter100dpi
+      pkgs.xorg.fontbhlucidatypewriter75dpi
+      pkgs.xorg.fontbh100dpi
+    ];
+  }
+  ```
+
+- The DHCP server (`services.dhcpd4`, `services.dhcpd6`) has been hardened.
+  The service is now using the systemd's `DynamicUser` mechanism to run as an unprivileged dynamically-allocated user with limited capabilities.
+  The dhcpd state files are now always stored in `/var/lib/dhcpd{4,6}` and the `services.dhcpd4.stateDir` and `service.dhcpd6.stateDir` options have been removed.
+  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. `systemd.services.dhcpd6.serviceConfig.AmbientCapabilities`.
+
+- The `mailpile` email webclient (`services.mailpile`) has been removed due to its reliance on python2.
+
+- The `matrix-synapse` service (`services.matrix-synapse`) has been converted to use the `settings` option defined in RFC42.
+  This means that options that are part of your `homeserver.yaml` configuration, and that were specified at the top-level of the
+  module (`services.matrix-synapse`) now need to be moved into `services.matrix-synapse.settings`. And while not all options you
+  may use are defined in there, they are still supported, because you can set arbitrary values in this freeform type.
+
+  The `listeners.*.bind_address` option was renamed to `bind_addresses` in order to match the upstream `homeserver.yaml` option
+  name. It is now also a list of strings instead of a string.
+
+  An example to make the required migration clearer:
+
+  Before:
+  ```nix
+  {
+    services.matrix-synapse = {
+      enable = true;
+
+      server_name = "example.com";
+      public_baseurl = "https://example.com:8448";
+
+      enable_registration = false;
+      registration_shared_secret = "xohshaeyui8jic7uutuDogahkee3aehuaf6ei3Xouz4iicie5thie6nohNahceut";
+      macaroon_secret_key = "xoo8eder9seivukaiPh1cheikohquuw8Yooreid0The4aifahth3Ou0aiShaiz4l";
+
+      tls_certificate_path = "/var/lib/acme/example.com/fullchain.pem";
+      tls_certificate_path = "/var/lib/acme/example.com/fullchain.pem";
+
+      listeners = [ {
+        port = 8448;
+        bind_address = "";
+        type = "http";
+        tls = true;
+        resources = [ {
+          names = [ "client" ];
+          compress = true;
+        } {
+          names = [ "federation" ];
+          compress = false;
+        } ];
+      } ];
+
+    };
+  }
+  ```
+
+  After:
+  ```nix
+  {
+    services.matrix-synapse = {
+      enable = true;
+
+      # this attribute set holds all values that go into your homeserver.yaml configuration
+      # See https://github.com/matrix-org/synapse/blob/develop/docs/sample_config.yaml for
+      # possible values.
+      settings = {
+        server_name = "example.com";
+        public_baseurl = "https://example.com:8448";
+
+        enable_registration = false;
+        # pass `registration_shared_secret` and `macaroon_secret_key` via `extraConfigFiles` instead
+
+        tls_certificate_path = "/var/lib/acme/example.com/fullchain.pem";
+        tls_certificate_path = "/var/lib/acme/example.com/fullchain.pem";
+
+        listeners = [ {
+          port = 8448;
+          bind_addresses = [
+            "::"
+            "0.0.0.0"
+          ];
+          type = "http";
+          tls = true;
+          resources = [ {
+            names = [ "client" ];
+            compress = true;
+          } {
+            names = [ "federation" ];
+            compress = false;
+          } ];
+        } ];
+      };
+
+      extraConfigFiles = [
+        /run/keys/matrix-synapse/secrets.yaml
+      ];
+    };
+  }
+  ```
+
+  The secrets in your original config should be migrated into a YAML file that is included via `extraConfigFiles`.
+
+  Additionally a few option defaults have been synced up with upstream default values, for example the `max_upload_size` grew from `10M` to `50M`. For the same reason, the default
+  `media_store_path` was changed from `${dataDir}/media` to `${dataDir}/media_store` if `system.stateVersion` is at least `22.05`. Files will need to be manually moved to the new
+  location if the `stateVersion` is updated.
+
+- The MoinMoin wiki engine (`services.moinmoin`) has been removed, because Python 2 is being retired from nixpkgs.
+
+- Services in the `hadoop` module previously set `openFirewall` to true by default.
+  This has now been changed to false. Node definitions for multi-node clusters would need
+  `openFirewall = true;` to be added to to hadoop services when upgrading from NixOS 21.11.
+
+- `services.hadoop.yarn.nodemanager` now uses cgroup-based CPU limit enforcement by default.
+  Additionally, the option `useCGroups` was added to nodemanagers as an easy way to switch
+  back to the old behavior.
+
+- The `wafHook` hook now honors `NIX_BUILD_CORES` when `enableParallelBuilding` is not set explicitly. Packages can restore the old behaviour by setting `enableParallelBuilding=false`.
+
+- `pkgs.claws-mail-gtk2`, representing Claws Mail's older release version three, was removed in order to get rid of Python 2.
+  Please switch to `claws-mail`, which is Claws Mail's latest release based on GTK+3 and Python 3.
+
+- The `writers.writePython2` and corresponding `writers.writePython2Bin` convenience functions to create executable Python 2 scripts in the store were removed in preparation of removal of the Python 2 interpreter.
+  Scripts have to be converted to Python 3 for use with `writers.writePython3` or `writers.writePyPy2` needs to be used.
+
+- `buildGoModule` was updated to use `go_1_17`, third party derivations that specify >= go 1.17 in the main `go.mod` will need to regenerate their `vendorSha256` hash.
+
+- The `gnome-passwordsafe` package updated to [version 6.x](https://gitlab.gnome.org/World/secrets/-/tags/6.0) and renamed to `gnome-secrets`.
+
+- If you previously used `/etc/docker/daemon.json`, you need to incorporate the changes into the new option `virtualisation.docker.daemon.settings`.
+
+- Ntopng (`services.ntopng`) is updated to 5.2.1 and uses a separate Redis instance if `system.stateVersion` is at least `22.05`. Existing setups shouldn't be affected.
+
+- The backward compatibility in `services.wordpress` to configure sites with
+  the old interface has been removed. Please use `services.wordpress.sites`
+  instead.
+
+- The backward compatibility in `services.dokuwiki` to configure sites with the
+  old interface has been removed. Please use `services.dokuwiki.sites` instead.
+
+- opensmtpd-extras is no longer build with python2 scripting support due to python2 deprecation in nixpkgs
+
+- `services.miniflux.adminCredentialFiles` is now required, instead of defaulting to `admin` and `password`.
+
+- The `autorestic` package has been upgraded from 1.3.0 to 1.5.0 which introduces breaking changes in config file, check [their migration guide](https://autorestic.vercel.app/migration/1.4_1.5) for more details.
+
+- For `pkgs.python3.pkgs.ipython`, its direct dependency `pkgs.python3.pkgs.matplotlib-inline`
+  (which is really an adapter to integrate matplotlib in ipython if it is installed) does
+  not depend on `pkgs.python3.pkgs.matplotlib` anymore.
+  This is closer to a non-Nix install of ipython.
+  This has the added benefit to reduce the closure size of `ipython` from ~400MB to ~160MB
+  (including ~100MB for python itself).
+
+- `documentation.man` has been refactored to support choosing a man implementation other than GNU's `man-db`. For this, `documentation.man.manualPages` has been renamed to `documentation.man.man-db.manualPages`. If you want to use the new alternative man implementation `mandoc`, add `documentation.man = { enable = true; man-db.enable = false; mandoc.enable = true; }` to your configuration.
+
+- Normal users (with `isNormalUser = true`) which have non-empty `subUidRanges` or `subGidRanges` set no longer have additional implicit ranges allocated. To enable automatic allocation back set `autoSubUidGidRange = true`.
+
+- `idris2` now requires `--package` when using packages `contrib` and `network`, while previously these idris2 packages were automatically loaded.
+
+- The iputils package, which is installed by default, no longer provides the
+  legacy tools `tftpd` and `traceroute6`. More tools (`ninfod`, `rarpd`, and
+  `rdisc`) are going to be removed in the next release. See
+  [upstream's release notes](https://github.com/iputils/iputils/releases/tag/20211215)
+  for more details and available replacements.
+
+- `services.thelounge.private` was removed in favor of `services.thelounge.public`, to follow with upstream changes.
+
+- `pkgs.docbookrx` was removed since it's unmaintained
+
+- `pkgs._7zz` is now correctly licensed as LGPL3+ and BSD3 with optional unfree unRAR licensed code
+
+- `tilp2` was removed together with its module
+
+- The F-PROT antivirus (`fprot` package) and its service module were removed because it
+  reached [end-of-life](https://kb.cyren.com/av-support/index.php?/Knowledgebase/Article/View/434/0/end-of-sale--end-of-life-for-f-prot-and-csam).
+
+- `bird1` and its modules `services.bird` as well as `services.bird6` have been removed. Upgrade to `services.bird2`.
+
+- The options `networking.interfaces.<name>.ipv4.routes` and `networking.interfaces.<name>.ipv6.routes` are no longer ignored when using networkd instead of the default scripted network backend by setting `networking.useNetworkd` to `true`.
+
+- MultiMC has been replaced with the fork PolyMC due to upstream developers being hostile to 3rd party package maintainers. PolyMC removes all MultiMC branding and is aimed at providing proper 3rd party packages like the one contained in Nixpkgs. This change affects the data folder where game instances and other save and configuration files are stored. Users with existing installations should rename `~/.local/share/multimc` to `~/.local/share/polymc`. The main config file's path has also moved from `~/.local/share/multimc/multimc.cfg` to `~/.local/share/polymc/polymc.cfg`.
+
+- `systemd-nspawn@.service` settings have been reverted to the default systemd behaviour. User namespaces are now activated by default. If you want to keep running nspawn containers without user namespaces you need to set `systemd.nspawn.<name>.execConfig.PrivateUsers = false`
+
+- The Tor SOCKS proxy is now actually disabled if `services.tor.client.enable` is set to `false` (the default). If you are using this functionality but didn't change the setting or set it to `false`, you now need to set it to `true`.
+
+- The terraform 0.12 compatibility has been removed and the `terraform.withPlugins` and `terraform-providers.mkProvider` implementations simplified. Providers now need to be stored under
+`$out/libexec/terraform-providers/<registry>/<owner>/<name>/<version>/<os>_<arch>/terraform-provider-<name>_v<version>` (which mkProvider does).
+
+  This breaks back-compat so it's not possible to mix-and-match with previous versions of nixpkgs. In exchange, it now becomes possible to use the providers from [nixpkgs-terraform-providers-bin](https://github.com/numtide/nixpkgs-terraform-providers-bin) directly.
+
+- The `dendrite` package has been upgraded from 0.5.1 to
+  [0.6.5](https://github.com/matrix-org/dendrite/releases/tag/v0.6.5). Instances
+  configured with split sqlite databases, which has been the default
+  in NixOS, require merging of the federation sender and signing key
+  databases. See upstream [release
+  notes](https://github.com/matrix-org/dendrite/releases/tag/v0.6.0)
+  on version 0.6.0 for details on database changes.
+
+- The existing `pkgs.opentelemetry-collector` has been moved to
+  `pkgs.opentelemetry-collector-contrib` to match the actual source being the
+  "contrib" edition. `pkgs.opentelemetry-collector` is now the actual core
+  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 `otelcontribcol` to `otelcorecol` and enjoy a 7x smaller binary.
+
+- `pkgs.pgadmin` now refers to `pkgs.pgadmin4`.
+  If you still need pgadmin3, use `pkgs.pgadmin3`.
+
+- `pkgs.noto-fonts-cjk` is now deprecated in favor of `pkgs.noto-fonts-cjk-sans`
+  and `pkgs.noto-fonts-cjk-serif` because they each have different release
+  schedules. To maintain compatibility with prior releases of Nixpkgs,
+  `pkgs.noto-fonts-cjk` is currently an alias of `pkgs.noto-fonts-cjk-sans` and
+  doesn't include serif fonts.
+
+- `pkgs.epgstation` has been upgraded from v1 to v2, resulting in incompatible
+  changes in the database scheme and configuration format.
+
+- Some top-level settings under [services.epgstation](#opt-services.epgstation.enable)
+  is now deprecated because it was redudant due to the same options being
+  present in [services.epgstation.settings](#opt-services.epgstation.settings).
+
+- The option `services.epgstation.basicAuth` was removed because basic
+  authentication support was dropped by upstream.
+
+- The option [services.epgstation.database.passwordFile](#opt-services.epgstation.database.passwordFile)
+  no longer has a default value. Make sure to set this option explicitly before
+  upgrading. Change the database password if necessary.
+
+- The [services.epgstation.settings](#opt-services.epgstation.settings)
+  option now expects options for `config.yml` in EPGStation v2.
+
+- Existing data for the [services.epgstation](#opt-services.epgstation.enable)
+  module would have to be backed up prior to the upgrade. To back up exising
+  data to `/tmp/epgstation.bak`, run
+  `sudo -u epgstation epgstation run backup /tmp/epgstation.bak`.
+  To import that data after to the upgrade, run
+  `sudo -u epgstation epgstation run v1migrate /tmp/epgstation.bak`
+
+- `switch-to-configuration` (the script that is run when running `nixos-rebuild switch` for example) has been reworked
+    * The interface that allows activation scripts to restart units has been streamlined. Restarting and reloading is now done by a single file `/run/nixos/activation-restart-list` that honors `restartIfChanged` and `reloadIfChanged` of the units.
+        * Preferring to reload instead of restarting can still be achieved using `/run/nixos/activation-reload-list`.
+    * The script now uses a proper ini-file parser to parse systemd units. Some values are now only searched in one section instead of in the entire unit. This is only relevant for units that don't use the NixOS systemd moule.
+        * `RefuseManualStop`, `X-OnlyManualStart`, `X-StopOnRemoval`, `X-StopOnReconfiguration` are only searched in the `[Unit]` section
+        * `X-ReloadIfChanged`, `X-RestartIfChanged`, `X-StopIfChanged` are only searched in the `[Service]` section
+
+- The `services.bookstack.cacheDir` option has been removed, since the
+  cache directory is now handled by systemd.
+
+- The `services.bookstack.extraConfig` option has been replaced by
+  `services.bookstack.config` which implements a
+  [settings-style](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md)
+  configuration.
+
+- `lib.assertMsg` and `lib.assertOneOf` no longer return `false` if the passed condition is `false`, `throw`ing the given error message instead (which makes the resulting error message less cluttered). This will not impact the behaviour of code using these functions as intended, namely as top-level wrapper for `assert` conditions.
+
+- The `vpnc` package has been changed to use GnuTLS instead of OpenSSL by default for licensing reasons.
+
+- `pkgs.vimPlugins.onedark-nvim` now refers to [navarasu/onedark.nvim](https://github.com/navarasu/onedark.nvim)
+  (formerly refers to [olimorris/onedarkpro.nvim](https://github.com/olimorris/onedarkpro.nvim)).
+
+- `services.pipewire.enable` will default to enabling the WirePlumber session manager instead of pipewire-media-session.
+  pipewire-media-session is deprecated by upstream and not recommended, but can still be manually enabled by setting
+  `services.pipewire.media-session.enable` to `true` and `services.pipewire.wireplumber.enable` to `false`.
+
+- `pkgs.makeDesktopItem` has been refactored to provide a more idiomatic API. Specifically:
+  - All valid options as of FDO Desktop Entry specification version 1.4 can now be passed in as explicit arguments
+  - `exec` can now be null, for entries that are not of type Application
+  - `mimeType` argument is renamed to `mimeTypes` for consistency
+  - `mimeTypes`, `categories`, `implements`, `keywords`, `onlyShowIn` and `notShowIn` take lists of strings instead of one string with semicolon separators
+  - `extraDesktopEntries` renamed to `extraConfig` for consistency
+  - Actions should now be provided as an attrset `actions`, the `Actions` line will be autogenerated.
+  - `extraEntries` is removed.
+  - Additional validation is added both at eval time and at build time.
+
+  See the `vscode` package for a more detailed example.
+
+<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
+
+## Other Notable Changes {#sec-release-22.05-notable-changes}
+
+- The option [services.redis.servers](#opt-services.redis.servers) was added
+  to support per-application `redis-server` which is more secure since Redis databases
+  are only mere key prefixes without any configuration or ACL of their own.
+  Backward-compatibility is preserved by mapping old `services.redis.settings`
+  to `services.redis.servers."".settings`, but you are strongly encouraged
+  to name each `redis-server` instance after the application using it,
+  instead of keeping that nameless one.
+  Except for the nameless `services.redis.servers.""`
+  still accessible at `127.0.0.1:6379`,
+  and to the members of the Unix group `redis`
+  through the Unix socket `/run/redis/redis.sock`,
+  all other `services.redis.servers.${serverName}`
+  are only accessible by default
+  to the members of the Unix group `redis-${serverName}`
+  through the Unix socket `/run/redis-${serverName}/redis.sock`.
+
+- The option [virtualisation.vmVariant](#opt-virtualisation.vmVariant) was added
+  to allow users to make changes to the `nixos-rebuild build-vm` configuration
+  that do not apply to their normal system.
+
+  The `config.system.build.vm` attribute now always exists and defaults to the
+  value from `vmVariant`. Configurations that import the `virtualisation/qemu-vm.nix`
+  module themselves will override this value, such that `vmVariant` is not used.
+
+  Similarly [virtualisation.vmVariantWithBootloader](#opt-virtualisation.vmVariantWithBootLoader) was added.
+
+- The configuration portion of the `nix-daemon` module has been reworked and exposed as [nix.settings](options.html#opt-nix-settings):
+  * Legacy options have been mapped to the corresponding options under under [nix.settings](options.html#opt-nix.settings) but may be deprecated in the future.
+  * [nix.buildMachines.publicHostKey](options.html#opt-nix.buildMachines.publicHostKey) has been added.
+
+- The `writers.writePyPy2`/`writers.writePyPy3` and corresponding `writers.writePyPy2Bin`/`writers.writePyPy3Bin` convenience functions to create executable Python 2/3 scripts using the PyPy interpreter were added.
+
+- Some improvements have been made to the `hadoop` module:
+  - A `gatewayRole` option has been added, for deploying hadoop cluster configuration files to a node that does not have any active services
+  - Support for older versions of hadoop have been added to the module
+  - Overriding and extending site XML files has been made easier
+
+- If you are using Wayland you can choose to use the Ozone Wayland support
+  in Chrome and several Electron apps by setting the environment variable
+  `NIXOS_OZONE_WL=1` (for example via
+  `environment.sessionVariables.NIXOS_OZONE_WL = "1"`).
+  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.
+
+- The `influxdb2` package was split into `influxdb2-server` and
+  `influxdb2-cli`, matching the split that took place upstream. A
+  combined `influxdb2` package is still provided in this release for
+  backwards compatibilty, but will be removed at a later date.
+
+- The `unifi` package was switched from `unifi6` to `unifi7`.
+  Direct downgrades from Unifi 7 to Unifi 6 are not possible and require restoring from a backup made by Unifi 6.
+
+- `programs.zsh.autosuggestions.strategy` now takes a list of strings instead of a string.
+
+- The `services.unifi.openPorts` option default value of `true` is now deprecated and will be changed to `false` in 22.11.
+  Configurations using this default will print a warning when rebuilt.
+
+- `security.acme` certificates will now correctly check for CA
+  revokation before reaching their minimum age.
+
+- Removing domains from `security.acme.certs._name_.extraDomainNames`
+  will now correctly remove those domains during rebuild/renew.
+
+- 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 `services.mysql.package = pkgs.mariadb_104;`.
+  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.
+
+- The option
+  [programs.ssh.enableAskPassword](#opt-programs.ssh.enableAskPassword) was
+  added, decoupling the setting of `SSH_ASKPASS` from
+  `services.xserver.enable`. This allows easy usage in non-X11 environments,
+  e.g. Wayland.
+
+- [programs.ssh.knownHosts](#opt-programs.ssh.knownHosts) has gained an `extraHostNames`
+  option to replace `hostNames`. `hostNames` is deprecated, but still available for now.
+
+- The `services.stubby` module was converted to a [settings-style](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md) configuration.
+
+- The option `services.duplicati.dataDir` has been added to allow changing the location of duplicati's files.
+
+- The options `boot.extraModprobeConfig` and `boot.blacklistedKernelModules` now also take effect in the initrd by copying the file `/etc/modprobe.d/nixos.conf` into the initrd.
+
+- `nixos-generate-config` now puts the dhcp configuration in `hardware-configuration.nix` instead of `configuration.nix`.
+
+- ORY Kratos was updated to version 0.8.3-alpha.1.pre.0, which introduces some breaking changes:
+  - If you are relying on the SQLite images, update your Docker Pull commands as follows:
+    - `docker pull oryd/kratos:{version}`
+  - Additionally, all passwords now have to be at least 8 characters long.
+  - For more details, see:
+    - [Release Notes for v0.8.1-alpha-1](https://github.com/ory/kratos/releases/tag/v0.8.1-alpha.1)
+    - [Release Notes for v0.8.2-alpha-1](https://github.com/ory/kratos/releases/tag/v0.8.2-alpha.1)
+
+- `fetchFromSourcehut` now allows fetching repositories recursively
+  using `fetchgit` or `fetchhg` if the argument `fetchSubmodules`
+  is set to `true`.
+
+- The `element-desktop` package now has an `useKeytar` option (defaults to `true`),
+  which allows disabling `keytar` and in turn `libsecret` usage
+  (which binds to native credential managers / keychain libraries).
+
+- The option `services.thelounge.plugins` has been added to allow installing plugins for The Lounge. Plugins can be found in `pkgs.theLoungePlugins.plugins` and `pkgs.theLoungePlugins.themes`.
+
+- The option `services.xserver.videoDriver = [ "nvidia" ];` will now also install [nvidia VA-API drivers](https://github.com/elFarto/nvidia-vaapi-driver) by default.
+
+- The `firmwareLinuxNonfree` package has been renamed to `linux-firmware`.
+
+- It is now possible to specify wordlists to include as handy to access environment variables using the `config.environment.wordlist` configuration options.
+
+- The `services.mbpfan` module was converted to a [RFC 0042](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md) configuration.
+
+- The default value for `programs.spacefm.settings.graphical_su` got unset. It previously pointed to `gksu` which has been removed.
+
+- A new module was added for the [Starship](https://starship.rs/) shell prompt,
+  providing the options `programs.starship.enable` and `programs.starship.settings`.
+
+- The [Dino](https://dino.im) XMPP client was updated to 0.3, adding support for audio and video calls.
+
+- `services.mattermost.plugins` has been added to allow the declarative installation of Mattermost plugins.
+  Plugins are automatically repackaged using autoPatchelf.
+
+- `services.logrotate.enable` now defaults to true if any rotate path has
+  been defined, and some paths have been added by default.
+
+- The `zrepl` package has been updated from 0.4.0 to 0.5:
+
+  - The RPC protocol version was bumped; all zrepl daemons in a setup must be updated and restarted before replication can resume.
+  - A bug involving encrypt-on-receive has been fixed. Read the [zrepl documentation](https://zrepl.github.io/configuration/sendrecvoptions.html#job-recv-options-placeholder) and check the output of `zfs get -r encryption,zrepl:placeholder PATH_TO_ROOTFS` on the receiver.
+
+- Renamed option `services.openssh.challengeResponseAuthentication` to `services.openssh.kbdInteractiveAuthentication`.
+  Reason is that the old name has been deprecated upstream.
+  Using the old option name will still work, but produce a warning.
+
+- The `pomerium-cli` command has been moved out of the `pomerium` package into
+  the `pomerium-cli` package, following upstream's repository split. If you are
+  using the `pomerium-cli` command, you should now install the `pomerium-cli`
+  package.
+
+- The option
+  [services.networking.networkmanager.enableFccUnlock](#opt-networking.networkmanager.enableFccUnlock)
+  was added to support FCC unlock procedures. Since release 1.18.4, the ModemManager
+  daemon no longer automatically performs the FCC unlock procedure by default. See
+  [the docs](https://modemmanager.org/docs/modemmanager/fcc-unlock/) for more details.
+
+- `programs.tmux` has a new option `plugins` that accepts a list of packages from the `tmuxPlugins` group. The specified packages are added to the system and loaded by `tmux`.
+
+- The polkit service, available at `security.polkit.enable`, is now disabled by default. It will automatically be enabled through services and desktop environments as needed.
+
+- The `hadoop` package has added support for `aarch64-linux` and `aarch64-darwin` as of 3.3.1 ([#158613](https://github.com/NixOS/nixpkgs/pull/158613)).
+
+- The `R` package now builds again on `aarch64-darwin` ([#158992](https://github.com/NixOS/nixpkgs/pull/158992)).
+
+- The `spark3` package has been updated from 3.1.2 to 3.2.1 ([#160075](https://github.com/NixOS/nixpkgs/pull/160075)):
+
+  - Testing has been enabled for `aarch64-linux` in addition to `x86_64-linux`.
+  - The `spark3` package is now usable on `aarch64-darwin` as a result of [#158613](https://github.com/NixOS/nixpkgs/pull/158613) and [#158992](https://github.com/NixOS/nixpkgs/pull/158992).
+
+<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
diff --git a/nixos/doc/manual/shell.nix b/nixos/doc/manual/shell.nix
new file mode 100644
index 00000000000..e5ec9b8f97f
--- /dev/null
+++ b/nixos/doc/manual/shell.nix
@@ -0,0 +1,8 @@
+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
new file mode 100755
index 00000000000..02168016b55
--- /dev/null
+++ b/nixos/doc/varlistentry-fixer.rb
@@ -0,0 +1,124 @@
+#!/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
new file mode 100644
index 00000000000..c3f39c7fd81
--- /dev/null
+++ b/nixos/doc/xmlformat.conf
@@ -0,0 +1,72 @@
+#
+# 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/build-vms.nix b/nixos/lib/build-vms.nix
new file mode 100644
index 00000000000..05d9ce89dbd
--- /dev/null
+++ b/nixos/lib/build-vms.nix
@@ -0,0 +1,113 @@
+{ system
+, # Use a minimal kernel?
+  minimal ? false
+, # Ignored
+  config ? null
+, # Nixpkgs, for qemu, lib and more
+  pkgs, lib
+, # !!! See comment about args in lib/modules.nix
+  specialArgs ? {}
+, # NixOS configuration to add to the VMs
+  extraConfigurations ? []
+}:
+
+with lib;
+
+rec {
+
+  inherit pkgs;
+
+  # Build a virtual network from an attribute set `{ machine1 =
+  # config1; ... machineN = configN; }', where `machineX' is the
+  # hostname and `configX' is a NixOS system configuration.  Each
+  # machine is given an arbitrary IP address in the virtual network.
+  buildVirtualNetwork =
+    nodes: let nodesOut = mapAttrs (n: buildVM nodesOut) (assignIPAddresses nodes); in nodesOut;
+
+
+  buildVM =
+    nodes: configurations:
+
+    import ./eval-config.nix {
+      inherit system specialArgs;
+      modules = configurations ++ extraConfigurations;
+      baseModules =  (import ../modules/module-list.nix) ++
+        [ ../modules/virtualisation/qemu-vm.nix
+          ../modules/testing/test-instrumentation.nix # !!! should only get added for automated test runs
+          { key = "no-manual"; documentation.nixos.enable = false; }
+          { key = "no-revision";
+            # Make the revision metadata constant, in order to avoid needless retesting.
+            # The human version (e.g. 21.05-pre) is left as is, because it is useful
+            # for external modules that test with e.g. nixosTest and rely on that
+            # version number.
+            config.system.nixos.revision = mkForce "constant-nixos-revision";
+          }
+          { key = "nodes"; _module.args.nodes = nodes; }
+        ] ++ optional minimal ../modules/testing/minimal-kernel.nix;
+    };
+
+
+  # Given an attribute set { machine1 = config1; ... machineN =
+  # configN; }, sequentially assign IP addresses in the 192.168.1.0/24
+  # range to each machine, and set the hostname to the attribute name.
+  assignIPAddresses = nodes:
+
+    let
+
+      machines = attrNames nodes;
+
+      machinesNumbered = zipLists machines (range 1 254);
+
+      nodes_ = forEach machinesNumbered (m: nameValuePair m.fst
+        [ ( { config, nodes, ... }:
+            let
+              interfacesNumbered = zipLists config.virtualisation.vlans (range 1 255);
+              interfaces = forEach interfacesNumbered ({ fst, snd }:
+                nameValuePair "eth${toString snd}" { ipv4.addresses =
+                  [ { address = "192.168.${toString fst}.${toString m.snd}";
+                      prefixLength = 24;
+                  } ];
+                });
+
+              networkConfig =
+                { networking.hostName = mkDefault m.fst;
+
+                  networking.interfaces = listToAttrs interfaces;
+
+                  networking.primaryIPAddress =
+                    optionalString (interfaces != []) (head (head interfaces).value.ipv4.addresses).address;
+
+                  # Put the IP addresses of all VMs in this machine's
+                  # /etc/hosts file.  If a machine has multiple
+                  # interfaces, use the IP address corresponding to
+                  # the first interface (i.e. the first network in its
+                  # virtualisation.vlans option).
+                  networking.extraHosts = flip concatMapStrings machines
+                    (m': let config = (getAttr m' nodes).config; in
+                      optionalString (config.networking.primaryIPAddress != "")
+                        ("${config.networking.primaryIPAddress} " +
+                         optionalString (config.networking.domain != null)
+                           "${config.networking.hostName}.${config.networking.domain} " +
+                         "${config.networking.hostName}\n"));
+
+                  virtualisation.qemu.options =
+                    let qemu-common = import ../lib/qemu-common.nix { inherit lib pkgs; };
+                    in flip concatMap interfacesNumbered
+                      ({ fst, snd }: qemu-common.qemuNICFlags snd fst m.snd);
+                };
+
+              in
+                { key = "ip-address";
+                  config = networkConfig // {
+                    # Expose the networkConfig items for tests like nixops
+                    # that need to recreate the network config.
+                    system.build.networkConfig = networkConfig;
+                  };
+                }
+          )
+          (getAttr m.fst nodes)
+        ] );
+
+    in listToAttrs nodes_;
+
+}
diff --git a/nixos/lib/default.nix b/nixos/lib/default.nix
new file mode 100644
index 00000000000..2b3056e0145
--- /dev/null
+++ b/nixos/lib/default.nix
@@ -0,0 +1,33 @@
+let
+  # The warning is in a top-level let binding so it is only printed once.
+  minimalModulesWarning = warn "lib.nixos.evalModules is experimental and subject to change. See nixos/lib/default.nix" null;
+  inherit (nonExtendedLib) warn;
+  nonExtendedLib = import ../../lib;
+in
+{ # Optional. Allows an extended `lib` to be used instead of the regular Nixpkgs lib.
+  lib ? nonExtendedLib,
+
+  # Feature flags allow you to opt in to unfinished code. These may change some
+  # behavior or disable warnings.
+  featureFlags ? {},
+
+  # This file itself is rather new, so we accept unknown parameters to be forward
+  # compatible. This is generally not recommended, because typos go undetected.
+  ...
+}:
+let
+  seqIf = cond: if cond then builtins.seq else a: b: b;
+  # If cond, force `a` before returning any attr
+  seqAttrsIf = cond: a: lib.mapAttrs (_: v: seqIf cond a v);
+
+  eval-config-minimal = import ./eval-config-minimal.nix { inherit lib; };
+in
+/*
+  This attribute set appears as lib.nixos in the flake, or can be imported
+  using a binding like `nixosLib = import (nixpkgs + "/nixos/lib") { }`.
+*/
+{
+  inherit (seqAttrsIf (!featureFlags?minimalModules) minimalModulesWarning eval-config-minimal)
+    evalModules
+    ;
+}
diff --git a/nixos/lib/eval-cacheable-options.nix b/nixos/lib/eval-cacheable-options.nix
new file mode 100644
index 00000000000..c3ba2ce6637
--- /dev/null
+++ b/nixos/lib/eval-cacheable-options.nix
@@ -0,0 +1,53 @@
+{ libPath
+, pkgsLibPath
+, nixosPath
+, modules
+, stateVersion
+, release
+}:
+
+let
+  lib = import libPath;
+  modulesPath = "${nixosPath}/modules";
+  # dummy pkgs set that contains no packages, only `pkgs.lib` from the full set.
+  # not having `pkgs.lib` causes all users of `pkgs.formats` to fail.
+  pkgs = import pkgsLibPath {
+    inherit lib;
+    pkgs = null;
+  };
+  utils = import "${nixosPath}/lib/utils.nix" {
+    inherit config lib;
+    pkgs = null;
+  };
+  # this is used both as a module and as specialArgs.
+  # as a module it sets the _module special values, as specialArgs it makes `config`
+  # unusable. this causes documentation attributes depending on `config` to fail.
+  config = {
+    _module.check = false;
+    _module.args = {};
+    system.stateVersion = stateVersion;
+  };
+  eval = lib.evalModules {
+    modules = (map (m: "${modulesPath}/${m}") modules) ++ [
+      config
+    ];
+    specialArgs = {
+      inherit config pkgs utils;
+    };
+  };
+  docs = import "${nixosPath}/doc/manual" {
+    pkgs = pkgs // {
+      inherit lib;
+      # duplicate of the declaration in all-packages.nix
+      buildPackages.nixosOptionsDoc = attrs:
+        (import "${nixosPath}/lib/make-options-doc")
+          ({ inherit pkgs lib; } // attrs);
+    };
+    config = config.config;
+    options = eval.options;
+    version = release;
+    revision = "release-${release}";
+    prefix = modulesPath;
+  };
+in
+  docs.optionsNix
diff --git a/nixos/lib/eval-config-minimal.nix b/nixos/lib/eval-config-minimal.nix
new file mode 100644
index 00000000000..d45b9ffd426
--- /dev/null
+++ b/nixos/lib/eval-config-minimal.nix
@@ -0,0 +1,49 @@
+
+# DO NOT IMPORT. Use nixpkgsFlake.lib.nixos, or import (nixpkgs + "/nixos/lib")
+{ lib }: # read -^
+
+let
+
+  /*
+    Invoke NixOS. Unlike traditional NixOS, this does not include all modules.
+    Any such modules have to be explicitly added via the `modules` parameter,
+    or imported using `imports` in a module.
+
+    A minimal module list improves NixOS evaluation performance and allows
+    modules to be independently usable, supporting new use cases.
+
+    Parameters:
+
+      modules:        A list of modules that constitute the configuration.
+
+      specialArgs:    An attribute set of module arguments. Unlike
+                      `config._module.args`, these are available for use in
+                      `imports`.
+                      `config._module.args` should be preferred when possible.
+
+    Return:
+
+      An attribute set containing `config.system.build.toplevel` among other
+      attributes. See `lib.evalModules` in the Nixpkgs library.
+
+   */
+  evalModules = {
+    prefix ? [],
+    modules ? [],
+    specialArgs ? {},
+  }:
+  # NOTE: Regular NixOS currently does use this function! Don't break it!
+  #       Ideally we don't diverge, unless we learn that we should.
+  #       In other words, only the public interface of nixos.evalModules
+  #       is experimental.
+  lib.evalModules {
+    inherit prefix modules;
+    specialArgs = {
+      modulesPath = builtins.toString ../modules;
+    } // specialArgs;
+  };
+
+in
+{
+  inherit evalModules;
+}
diff --git a/nixos/lib/eval-config.nix b/nixos/lib/eval-config.nix
new file mode 100644
index 00000000000..2daaa8a1186
--- /dev/null
+++ b/nixos/lib/eval-config.nix
@@ -0,0 +1,111 @@
+# From an end-user configuration file (`configuration.nix'), build a NixOS
+# configuration object (`config') from which we can retrieve option
+# values.
+
+# !!! Please think twice before adding to this argument list!
+# Ideally eval-config.nix would be an extremely thin wrapper
+# around lib.evalModules, so that modular systems that have nixos configs
+# as subcomponents (e.g. the container feature, or nixops if network
+# expressions are ever made modular at the top level) can just use
+# types.submodule instead of using eval-config.nix
+evalConfigArgs@
+{ # !!! system can be set modularly, would be nice to remove
+  system ? builtins.currentSystem
+, # !!! is this argument needed any more? The pkgs argument can
+  # be set modularly anyway.
+  pkgs ? null
+, # !!! what do we gain by making this configurable?
+  baseModules ? import ../modules/module-list.nix
+, # !!! See comment about args in lib/modules.nix
+  extraArgs ? {}
+, # !!! See comment about args in lib/modules.nix
+  specialArgs ? {}
+, modules
+, modulesLocation ? (builtins.unsafeGetAttrPos "modules" evalConfigArgs).file or null
+, # !!! See comment about check in lib/modules.nix
+  check ? true
+, prefix ? []
+, lib ? import ../../lib
+, extraModules ? let e = builtins.getEnv "NIXOS_EXTRA_MODULE_PATH";
+                 in if e == "" then [] else [(import e)]
+}:
+
+let pkgs_ = pkgs;
+in
+
+let
+  evalModulesMinimal = (import ./default.nix {
+    inherit lib;
+    # Implicit use of feature is noted in implementation.
+    featureFlags.minimalModules = { };
+  }).evalModules;
+
+  pkgsModule = rec {
+    _file = ./eval-config.nix;
+    key = _file;
+    config = {
+      # Explicit `nixpkgs.system` or `nixpkgs.localSystem` should override
+      # this.  Since the latter defaults to the former, the former should
+      # default to the argument. That way this new default could propagate all
+      # they way through, but has the last priority behind everything else.
+      nixpkgs.system = lib.mkDefault system;
+
+      # Stash the value of the `system` argument. When using `nesting.children`
+      # we want to have the same default value behavior (immediately above)
+      # without any interference from the user's configuration.
+      nixpkgs.initialSystem = system;
+
+      _module.args.pkgs = lib.mkIf (pkgs_ != null) (lib.mkForce pkgs_);
+    };
+  };
+
+  withWarnings = x:
+    lib.warnIf (evalConfigArgs?extraArgs) "The extraArgs argument to eval-config.nix is deprecated. Please set config._module.args instead."
+    lib.warnIf (evalConfigArgs?check) "The check argument to eval-config.nix is deprecated. Please set config._module.check instead."
+    x;
+
+  legacyModules =
+    lib.optional (evalConfigArgs?extraArgs) {
+      config = {
+        _module.args = extraArgs;
+      };
+    }
+    ++ lib.optional (evalConfigArgs?check) {
+      config = {
+        _module.check = lib.mkDefault check;
+      };
+    };
+
+  allUserModules =
+    let
+      # Add the invoking file (or specified modulesLocation) as error message location
+      # for modules that don't have their own locations; presumably inline modules.
+      locatedModules =
+        if modulesLocation == null then
+          modules
+        else
+          map (lib.setDefaultModuleLocation modulesLocation) modules;
+    in
+      locatedModules ++ legacyModules;
+
+  noUserModules = evalModulesMinimal ({
+    inherit prefix specialArgs;
+    modules = baseModules ++ extraModules ++ [ pkgsModule modulesModule ];
+  });
+
+  # Extra arguments that are useful for constructing a similar configuration.
+  modulesModule = {
+    config = {
+      _module.args = {
+        inherit noUserModules baseModules extraModules modules;
+      };
+    };
+  };
+
+  nixosWithUserModules = noUserModules.extendModules { modules = allUserModules; };
+
+in
+withWarnings nixosWithUserModules // {
+  inherit extraArgs;
+  inherit (nixosWithUserModules._module.args) pkgs;
+}
diff --git a/nixos/lib/from-env.nix b/nixos/lib/from-env.nix
new file mode 100644
index 00000000000..6bd71e40e9a
--- /dev/null
+++ b/nixos/lib/from-env.nix
@@ -0,0 +1,4 @@
+# TODO: remove this file. There is lib.maybeEnv now
+name: default:
+let value = builtins.getEnv name; in
+if value == "" then default else value
diff --git a/nixos/lib/make-channel.nix b/nixos/lib/make-channel.nix
new file mode 100644
index 00000000000..9b920b989fc
--- /dev/null
+++ b/nixos/lib/make-channel.nix
@@ -0,0 +1,31 @@
+/* Build a channel tarball. These contain, in addition to the nixpkgs
+ * expressions themselves, files that indicate the version of nixpkgs
+ * that they represent.
+ */
+{ pkgs, nixpkgs, version, versionSuffix }:
+
+pkgs.releaseTools.makeSourceTarball {
+  name = "nixos-channel";
+
+  src = nixpkgs;
+
+  officialRelease = false; # FIXME: fix this in makeSourceTarball
+  inherit version versionSuffix;
+
+  buildInputs = [ pkgs.nix ];
+
+  distPhase = ''
+    rm -rf .git
+    echo -n $VERSION_SUFFIX > .version-suffix
+    echo -n ${nixpkgs.rev or nixpkgs.shortRev} > .git-revision
+    releaseName=nixos-$VERSION$VERSION_SUFFIX
+    mkdir -p $out/tarballs
+    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
+    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
new file mode 100644
index 00000000000..15302ae8241
--- /dev/null
+++ b/nixos/lib/make-disk-image.nix
@@ -0,0 +1,443 @@
+{ pkgs
+, lib
+
+, # The NixOS configuration to be installed onto the disk image.
+  config
+
+, # The size of the disk, in megabytes.
+  # if "auto" size is calculated based on the contents copied to it and
+  #   additionalSpace is taken into account.
+  diskSize ? "auto"
+
+, # additional disk space to be added to the image if diskSize "auto"
+  # is used
+  additionalSpace ? "512M"
+
+, # size of the boot partition, is only used if partitionTableType is
+  # either "efi" or "hybrid"
+  # This will be undersized slightly, as this is actually the offset of
+  # the end of the partition. Generally it will be 1MiB smaller.
+  bootSize ? "256M"
+
+, # The files and directories to be placed in the target file system.
+  # This is a list of attribute sets {source, target, mode, user, group} where
+  # `source' is the file system object (regular file or directory) to be
+  # grafted in the file system at path `target', `mode' is a string containing
+  # the permissions that will be set (ex. "755"), `user' and `group' are the
+  # user and group name that will be set as owner of the files.
+  # `mode', `user', and `group' are optional.
+  # When setting one of `user' or `group', the other needs to be set too.
+  contents ? []
+
+, # Type of partition table to use; either "legacy", "efi", or "none".
+  # For "efi" images, the GPT partition table is used and a mandatory ESP
+  #   partition of reasonable size is created in addition to the root partition.
+  # For "legacy", the msdos partition table is used and a single large root
+  #   partition is created.
+  # For "legacy+gpt", the GPT partition table is used, a 1MiB no-fs partition for
+  #   use by the bootloader is created, and a single large root partition is
+  #   created.
+  # For "hybrid", the GPT partition table is used and a mandatory ESP
+  #   partition of reasonable size is created in addition to the root partition.
+  #   Also a legacy MBR will be present.
+  # For "none", no partition table is created. Enabling `installBootLoader`
+  #   most likely fails as GRUB will probably refuse to install.
+  partitionTableType ? "legacy"
+
+, # Whether to invoke `switch-to-configuration boot` during image creation
+  installBootLoader ? true
+
+, # The root file system type.
+  fsType ? "ext4"
+
+, # Filesystem label
+  label ? if onlyNixStore then "nix-store" else "nixos"
+
+, # The initial NixOS configuration file to be copied to
+  # /etc/nixos/configuration.nix.
+  configFile ? null
+
+, # Shell code executed after the VM has finished.
+  postVM ? ""
+
+, # Copy the contents of the Nix store to the root of the image and
+  # skip further setup. Incompatible with `contents`,
+  # `installBootLoader` and `configFile`.
+  onlyNixStore ? false
+
+, name ? "nixos-disk-image"
+
+, # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw.
+  format ? "raw"
+
+, # 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
+  # updated.
+  copyChannel ? true
+
+, # Additional store paths to copy to the image's store.
+  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";
+# Either both or none of {user,group} need to be set
+assert lib.all
+         (attrs: ((attrs.user  or null) == null)
+              == ((attrs.group or null) == null))
+         contents;
+assert onlyNixStore -> contents == [] && configFile == null && !installBootLoader;
+
+with lib;
+
+let format' = format; in let
+
+  format = if format' == "qcow2-compressed" then "qcow2" else format';
+
+  compress = optionalString (format' == "qcow2-compressed") "-c";
+
+  filename = "nixos." + {
+    qcow2 = "qcow2";
+    vdi   = "vdi";
+    vpc   = "vhd";
+    raw   = "img";
+  }.${format} or format;
+
+  rootPartition = { # switch-case
+    legacy = "1";
+    "legacy+gpt" = "2";
+    efi = "2";
+    hybrid = "3";
+  }.${partitionTableType};
+
+  partitionDiskScript = { # switch-case
+    legacy = ''
+      parted --script $diskImage -- \
+        mklabel msdos \
+        mkpart primary ext4 1MiB -1
+    '';
+    "legacy+gpt" = ''
+      parted --script $diskImage -- \
+        mklabel gpt \
+        mkpart no-fs 1MB 2MB \
+        set 1 bios_grub on \
+        align-check optimal 1 \
+        mkpart primary ext4 2MB -1 \
+        align-check optimal 2 \
+        print
+    '';
+    efi = ''
+      parted --script $diskImage -- \
+        mklabel gpt \
+        mkpart ESP fat32 8MiB ${bootSize} \
+        set 1 boot on \
+        mkpart primary ext4 ${bootSize} -1
+    '';
+    hybrid = ''
+      parted --script $diskImage -- \
+        mklabel gpt \
+        mkpart ESP fat32 8MiB ${bootSize} \
+        set 1 boot on \
+        mkpart no-fs 0 1024KiB \
+        set 2 bios_grub on \
+        mkpart primary ext4 ${bootSize} -1
+    '';
+    none = "";
+  }.${partitionTableType};
+
+  nixpkgs = cleanSource pkgs.path;
+
+  # FIXME: merge with channel.nix / make-channel.nix.
+  channelSources = pkgs.runCommand "nixos-${config.system.nixos.version}" {} ''
+    mkdir -p $out
+    cp -prd ${nixpkgs.outPath} $out/nixos
+    chmod -R u+w $out/nixos
+    if [ ! -e $out/nixos/nixpkgs ]; then
+      ln -s . $out/nixos/nixpkgs
+    fi
+    rm -rf $out/nixos/.git
+    echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
+  '';
+
+  binPath = with pkgs; makeBinPath (
+    [ rsync
+      util-linux
+      parted
+      e2fsprogs
+      lkl
+      config.system.build.nixos-install
+      config.system.build.nixos-enter
+      nix
+    ] ++ 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 :)
+  # !!! should use XML.
+  sources = map (x: x.source) contents;
+  targets = map (x: x.target) contents;
+  modes   = map (x: x.mode  or "''") contents;
+  users   = map (x: x.user  or "''") contents;
+  groups  = map (x: x.group or "''") contents;
+
+  basePaths = [ config.system.build.toplevel ]
+    ++ lib.optional copyChannel channelSources;
+
+  additionalPaths' = subtractLists basePaths additionalPaths;
+
+  closureInfo = pkgs.closureInfo {
+    rootPaths = basePaths ++ additionalPaths';
+  };
+
+  blockSize = toString (4 * 1024); # ext4fs block size (not block device sector size)
+
+  prepareImage = ''
+    export PATH=${binPath}
+
+    # Yes, mkfs.ext4 takes different units in different contexts. Fun.
+    sectorsToKilobytes() {
+      echo $(( ( "$1" * 512 ) / 1024 ))
+    }
+
+    sectorsToBytes() {
+      echo $(( "$1" * 512  ))
+    }
+
+    # Given lines of numbers, adds them together
+    sum_lines() {
+      local acc=0
+      while read -r number; do
+        acc=$((acc+number))
+      done
+      echo "$acc"
+    }
+
+    mebibyte=$(( 1024 * 1024 ))
+
+    # Approximative percentage of reserved space in an ext4 fs over 512MiB.
+    # 0.05208587646484375
+    #  × 1000, integer part: 52
+    compute_fudge() {
+      echo $(( $1 * 52 / 1000 ))
+    }
+
+    mkdir $out
+
+    root="$PWD/root"
+    mkdir -p $root
+
+    # Copy arbitrary other files into the image
+    # Semi-shamelessly copied from make-etc.sh. I (@copumpkin) shall factor this stuff out as part of
+    # https://github.com/NixOS/nixpkgs/issues/23052.
+    set -f
+    sources_=(${concatStringsSep " " sources})
+    targets_=(${concatStringsSep " " targets})
+    modes_=(${concatStringsSep " " modes})
+    set +f
+
+    for ((i = 0; i < ''${#targets_[@]}; i++)); do
+      source="''${sources_[$i]}"
+      target="''${targets_[$i]}"
+      mode="''${modes_[$i]}"
+
+      if [ -n "$mode" ]; then
+        rsync_chmod_flags="--chmod=$mode"
+      else
+        rsync_chmod_flags=""
+      fi
+      # Unfortunately cptofs only supports modes, not ownership, so we can't use
+      # rsync's --chown option. Instead, we change the ownerships in the
+      # VM script with chown.
+      rsync_flags="-a --no-o --no-g $rsync_chmod_flags"
+      if [[ "$source" =~ '*' ]]; then
+        # If the source name contains '*', perform globbing.
+        mkdir -p $root/$target
+        for fn in $source; do
+          rsync $rsync_flags "$fn" $root/$target/
+        done
+      else
+        mkdir -p $root/$(dirname $target)
+        if ! [ -e $root/$target ]; then
+          rsync $rsync_flags $source $root/$target
+        else
+          echo "duplicate entry $target -> $source"
+          exit 1
+        fi
+      fi
+    done
+
+    export HOME=$TMPDIR
+
+    # Provide a Nix database so that nixos-install can copy closures.
+    export NIX_STATE_DIR=$TMPDIR/state
+    nix-store --load-db < ${closureInfo}/registration
+
+    chmod 755 "$TMPDIR"
+    echo "running nixos-install..."
+    nixos-install --root $root --no-bootloader --no-root-passwd \
+      --system ${config.system.build.toplevel} \
+      ${if copyChannel then "--channel ${channelSources}" else "--no-channel-copy"} \
+      --substituters ""
+
+    ${optionalString (additionalPaths' != []) ''
+      nix --extra-experimental-features nix-command copy --to $root --no-check-sigs ${concatStringsSep " " additionalPaths'}
+    ''}
+
+    diskImage=nixos.raw
+
+    ${if diskSize == "auto" then ''
+      ${if partitionTableType == "efi" || partitionTableType == "hybrid" then ''
+        # Add the GPT at the end
+        gptSpace=$(( 512 * 34 * 1 ))
+        # Normally we'd need to account for alignment and things, if bootSize
+        # represented the actual size of the boot partition. But it instead
+        # represents the offset at which it ends.
+        # So we know bootSize is the reserved space in front of the partition.
+        reservedSpace=$(( gptSpace + $(numfmt --from=iec '${bootSize}') ))
+      '' else if partitionTableType == "legacy+gpt" then ''
+        # Add the GPT at the end
+        gptSpace=$(( 512 * 34 * 1 ))
+        # And include the bios_grub partition; the ext4 partition starts at 2MB exactly.
+        reservedSpace=$(( gptSpace + 2 * mebibyte ))
+      '' else if partitionTableType == "legacy" then ''
+        # Add the 1MiB aligned reserved space (includes MBR)
+        reservedSpace=$(( mebibyte ))
+      '' else ''
+        reservedSpace=0
+      ''}
+      additionalSpace=$(( $(numfmt --from=iec '${additionalSpace}') + reservedSpace ))
+
+      # Compute required space in filesystem blocks
+      diskUsage=$(find . ! -type d -print0 | du --files0-from=- --apparent-size --block-size "${blockSize}" | cut -f1 | sum_lines)
+      # Each inode takes space!
+      numInodes=$(find . | wc -l)
+      # Convert to bytes, inodes take two blocks each!
+      diskUsage=$(( (diskUsage + 2 * numInodes) * ${blockSize} ))
+      # Then increase the required space to account for the reserved blocks.
+      fudge=$(compute_fudge $diskUsage)
+      requiredFilesystemSpace=$(( diskUsage + fudge ))
+
+      diskSize=$(( requiredFilesystemSpace  + additionalSpace ))
+
+      # Round up to the nearest mebibyte.
+      # This ensures whole 512 bytes sector sizes in the disk image
+      # and helps towards aligning partitions optimally.
+      if (( diskSize % mebibyte )); then
+        diskSize=$(( ( diskSize / mebibyte + 1) * mebibyte ))
+      fi
+
+      truncate -s "$diskSize" $diskImage
+
+      printf "Automatic disk size...\n"
+      printf "  Closure space use: %d bytes\n" $diskUsage
+      printf "  fudge: %d bytes\n" $fudge
+      printf "  Filesystem size needed: %d bytes\n" $requiredFilesystemSpace
+      printf "  Additional space: %d bytes\n" $additionalSpace
+      printf "  Disk image size: %d bytes\n" $diskSize
+    '' else ''
+      truncate -s ${toString diskSize}M $diskImage
+    ''}
+
+    ${partitionDiskScript}
+
+    ${if partitionTableType != "none" then ''
+      # Get start & length of the root partition in sectors to $START and $SECTORS.
+      eval $(partx $diskImage -o START,SECTORS --nr ${rootPartition} --pairs)
+
+      mkfs.${fsType} -b ${blockSize} -F -L ${label} $diskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
+    '' else ''
+      mkfs.${fsType} -b ${blockSize} -F -L ${label} $diskImage
+    ''}
+
+    echo "copying staging root to image..."
+    cptofs -p ${optionalString (partitionTableType != "none") "-P ${rootPartition}"} \
+           -t ${fsType} \
+           -i $diskImage \
+           $root${optionalString onlyNixStore builtins.storeDir}/* / ||
+      (echo >&2 "ERROR: cptofs failed. diskSize might be too small for closure."; exit 1)
+  '';
+
+  moveOrConvertImage = ''
+    ${if format == "raw" then ''
+      mv $diskImage $out/${filename}
+    '' else ''
+      ${pkgs.qemu}/bin/qemu-img convert -f raw -O ${format} ${compress} $diskImage $out/${filename}
+    ''}
+    diskImage=$out/${filename}
+  '';
+
+  buildImage = pkgs.vmTools.runInLinuxVM (
+    pkgs.runCommand name {
+      preVM = prepareImage;
+      buildInputs = with pkgs; [ util-linux e2fsprogs dosfstools ];
+      postVM = moveOrConvertImage + postVM;
+      memSize = 1024;
+    } ''
+      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
+      # make systemd-boot find ESP without udev
+      mkdir /dev/block
+      ln -s /dev/vda1 /dev/block/254:1
+
+      mountPoint=/mnt
+      mkdir $mountPoint
+      mount $rootDisk $mountPoint
+
+      # Create the ESP and mount it. Unlike e2fsprogs, mkfs.vfat doesn't support an
+      # '-E offset=X' option, so we can't do this outside the VM.
+      ${optionalString (partitionTableType == "efi" || partitionTableType == "hybrid") ''
+        mkdir -p /mnt/boot
+        mkfs.vfat -n ESP /dev/vda1
+        mount /dev/vda1 /mnt/boot
+      ''}
+
+      # Install a configuration.nix
+      mkdir -p /mnt/etc/nixos
+      ${optionalString (configFile != null) ''
+        cp ${configFile} /mnt/etc/nixos/configuration.nix
+      ''}
+
+      ${lib.optionalString installBootLoader ''
+        # Set up core system link, GRUB, 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
+        rm -f $mountPoint/etc/machine-id
+      ''}
+
+      # Set the ownerships of the contents. The modes are set in preVM.
+      # No globbing on targets, so no need to set -f
+      targets_=(${concatStringsSep " " targets})
+      users_=(${concatStringsSep " " users})
+      groups_=(${concatStringsSep " " groups})
+      for ((i = 0; i < ''${#targets_[@]}; i++)); do
+        target="''${targets_[$i]}"
+        user="''${users_[$i]}"
+        group="''${groups_[$i]}"
+        if [ -n "$user$group" ]; then
+          # We have to nixos-enter since we need to use the user and group of the VM
+          nixos-enter --root $mountPoint -- chown -R "$user:$group" "$target"
+        fi
+      done
+
+      umount -R /mnt
+
+      # Make sure resize2fs works. Note that resize2fs has stricter criteria for resizing than a normal
+      # 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.
+      ${optionalString (fsType == "ext4") ''
+        tune2fs -T now -c 0 -i 0 $rootDisk
+      ''}
+    ''
+  );
+in
+  if onlyNixStore then
+    pkgs.runCommand name {}
+      (prepareImage + moveOrConvertImage + postVM)
+  else buildImage
diff --git a/nixos/lib/make-ext4-fs.nix b/nixos/lib/make-ext4-fs.nix
new file mode 100644
index 00000000000..416beeb32f2
--- /dev/null
+++ b/nixos/lib/make-ext4-fs.nix
@@ -0,0 +1,86 @@
+# Builds an ext4 image containing a populated /nix/store with the closure
+# of store paths passed in the storePaths parameter, in addition to the
+# contents of a directory that can be populated with commands. The
+# generated image is sized to only fit its contents, with the expectation
+# that a script resizes the filesystem at boot time.
+{ pkgs
+, lib
+# List of derivations to be included
+, storePaths
+# Whether or not to compress the resulting image with zstd
+, compressImage ? false, zstd
+# Shell commands to populate the ./files directory.
+# All files in that directory are copied to the root of the FS.
+, populateImageCommands ? ""
+, volumeLabel
+, uuid ? "44444444-4444-4444-8888-888888888888"
+, e2fsprogs
+, libfaketime
+, perl
+, fakeroot
+}:
+
+let
+  sdClosureInfo = pkgs.buildPackages.closureInfo { rootPaths = storePaths; };
+in
+pkgs.stdenv.mkDerivation {
+  name = "ext4-fs.img${lib.optionalString compressImage ".zst"}";
+
+  nativeBuildInputs = [ e2fsprogs.bin libfaketime perl fakeroot ]
+  ++ lib.optional compressImage zstd;
+
+  buildCommand =
+    ''
+      ${if compressImage then "img=temp.img" else "img=$out"}
+      (
+      mkdir -p ./files
+      ${populateImageCommands}
+      )
+
+      echo "Preparing store paths for image..."
+
+      # Create nix/store before copying path
+      mkdir -p ./rootImage/nix/store
+
+      xargs -I % cp -a --reflink=auto % -t ./rootImage/nix/store/ < ${sdClosureInfo}/store-paths
+      (
+        GLOBIGNORE=".:.."
+        shopt -u dotglob
+
+        for f in ./files/*; do
+            cp -a --reflink=auto -t ./rootImage/ "$f"
+        done
+      )
+
+      # Also include a manifest of the closures in a format suitable for nix-store --load-db
+      cp ${sdClosureInfo}/registration ./rootImage/nix-path-registration
+
+      # Make a crude approximation of the size of the target image.
+      # If the script starts failing, increase the fudge factors here.
+      numInodes=$(find ./rootImage | wc -l)
+      numDataBlocks=$(du -s -c -B 4096 --apparent-size ./rootImage | tail -1 | awk '{ print int($1 * 1.10) }')
+      bytes=$((2 * 4096 * $numInodes + 4096 * $numDataBlocks))
+      echo "Creating an EXT4 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks)"
+
+      truncate -s $bytes $img
+
+      faketime -f "1970-01-01 00:00:01" fakeroot mkfs.ext4 -L ${volumeLabel} -U ${uuid} -d ./rootImage $img
+
+      export EXT2FS_NO_MTAB_OK=yes
+      # I have ended up with corrupted images sometimes, I suspect that happens when the build machine's disk gets full during the build.
+      if ! fsck.ext4 -n -f $img; then
+        echo "--- Fsck failed for EXT4 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks) ---"
+        cat errorlog
+        return 1
+      fi
+
+      # We may want to shrink the file system and resize the image to
+      # get rid of the unnecessary slack here--but see
+      # https://github.com/NixOS/nixpkgs/issues/125121 for caveats.
+
+      if [ ${builtins.toString compressImage} ]; then
+        echo "Compressing image"
+        zstd -v --no-progress ./$img -o $out
+      fi
+    '';
+}
diff --git a/nixos/lib/make-iso9660-image.nix b/nixos/lib/make-iso9660-image.nix
new file mode 100644
index 00000000000..549530965f6
--- /dev/null
+++ b/nixos/lib/make-iso9660-image.nix
@@ -0,0 +1,65 @@
+{ stdenv, closureInfo, xorriso, syslinux, libossp_uuid
+
+, # The file name of the resulting ISO image.
+  isoName ? "cd.iso"
+
+, # The files and directories to be placed in the ISO file system.
+  # This is a list of attribute sets {source, target} where `source'
+  # is the file system object (regular file or directory) to be
+  # grafted in the file system at path `target'.
+  contents
+
+, # In addition to `contents', the closure of the store paths listed
+  # in `storeContents' are also placed in the Nix store of the CD.
+  # This is a list of attribute sets {object, symlink} where `object'
+  # is a store path whose closure will be copied, and `symlink' is a
+  # symlink to `object' that will be added to the CD.
+  storeContents ? []
+
+, # Whether this should be an El-Torito bootable CD.
+  bootable ? false
+
+, # Whether this should be an efi-bootable El-Torito CD.
+  efiBootable ? false
+
+, # Whether this should be an hybrid CD (bootable from USB as well as CD).
+  usbBootable ? false
+
+, # The path (in the ISO file system) of the boot image.
+  bootImage ? ""
+
+, # The path (in the ISO file system) of the efi boot image.
+  efiBootImage ? ""
+
+, # The path (outside the ISO file system) of the isohybrid-mbr image.
+  isohybridMbrImage ? ""
+
+, # Whether to compress the resulting ISO image with zstd.
+  compressImage ? false, zstd
+
+, # The volume ID.
+  volumeID ? ""
+}:
+
+assert bootable -> bootImage != "";
+assert efiBootable -> efiBootImage != "";
+assert usbBootable -> isohybridMbrImage != "";
+
+stdenv.mkDerivation {
+  name = isoName;
+  builder = ./make-iso9660-image.sh;
+  nativeBuildInputs = [ xorriso syslinux zstd libossp_uuid ];
+
+  inherit isoName bootable bootImage compressImage volumeID efiBootImage efiBootable isohybridMbrImage usbBootable;
+
+  # !!! should use XML.
+  sources = map (x: x.source) contents;
+  targets = map (x: x.target) contents;
+
+  # !!! should use XML.
+  objects = map (x: x.object) storeContents;
+  symlinks = map (x: x.symlink) storeContents;
+
+  # For obtaining the closure of `storeContents'.
+  closureInfo = closureInfo { rootPaths = map (x: x.object) storeContents; };
+}
diff --git a/nixos/lib/make-iso9660-image.sh b/nixos/lib/make-iso9660-image.sh
new file mode 100644
index 00000000000..9273b8d3db8
--- /dev/null
+++ b/nixos/lib/make-iso9660-image.sh
@@ -0,0 +1,139 @@
+source $stdenv/setup
+
+sources_=($sources)
+targets_=($targets)
+
+objects=($objects)
+symlinks=($symlinks)
+
+
+# Remove the initial slash from a path, since genisofs likes it that way.
+stripSlash() {
+    res="$1"
+    if test "${res:0:1}" = /; then res=${res:1}; fi
+}
+
+# Escape potential equal signs (=) with backslash (\=)
+escapeEquals() {
+    echo "$1" | sed -e 's/\\/\\\\/g' -e 's/=/\\=/g'
+}
+
+# Queues an file/directory to be placed on the ISO.
+# An entry consists of a local source path (2) and
+# a destination path on the ISO (1).
+addPath() {
+    target="$1"
+    source="$2"
+    echo "$(escapeEquals "$target")=$(escapeEquals "$source")" >> pathlist
+}
+
+stripSlash "$bootImage"; bootImage="$res"
+
+
+if test -n "$bootable"; then
+
+    # The -boot-info-table option modifies the $bootImage file, so
+    # find it in `contents' and make a copy of it (since the original
+    # is read-only in the Nix store...).
+    for ((i = 0; i < ${#targets_[@]}; i++)); do
+        stripSlash "${targets_[$i]}"
+        if test "$res" = "$bootImage"; then
+            echo "copying the boot image ${sources_[$i]}"
+            cp "${sources_[$i]}" boot.img
+            chmod u+w boot.img
+            sources_[$i]=boot.img
+        fi
+    done
+
+    isoBootFlags="-eltorito-boot ${bootImage}
+                  -eltorito-catalog .boot.cat
+                  -no-emul-boot -boot-load-size 4 -boot-info-table
+                  --sort-weight 1 /isolinux" # Make sure isolinux is near the beginning of the ISO
+fi
+
+if test -n "$usbBootable"; then
+    usbBootFlags="-isohybrid-mbr ${isohybridMbrImage}"
+fi
+
+if test -n "$efiBootable"; then
+    efiBootFlags="-eltorito-alt-boot
+                  -e $efiBootImage
+                  -no-emul-boot
+                  -isohybrid-gpt-basdat"
+fi
+
+touch pathlist
+
+
+# Add the individual files.
+for ((i = 0; i < ${#targets_[@]}; i++)); do
+    stripSlash "${targets_[$i]}"
+    addPath "$res" "${sources_[$i]}"
+done
+
+
+# Add the closures of the top-level store objects.
+for i in $(< $closureInfo/store-paths); do
+    addPath "${i:1}" "$i"
+done
+
+
+# Also include a manifest of the closures in a format suitable for
+# nix-store --load-db.
+if [[ ${#objects[*]} != 0 ]]; then
+    cp $closureInfo/registration nix-path-registration
+    addPath "nix-path-registration" "nix-path-registration"
+fi
+
+
+# Add symlinks to the top-level store objects.
+for ((n = 0; n < ${#objects[*]}; n++)); do
+    object=${objects[$n]}
+    symlink=${symlinks[$n]}
+    if test "$symlink" != "none"; then
+        mkdir -p $(dirname ./$symlink)
+        ln -s $object ./$symlink
+        addPath "$symlink" "./$symlink"
+    fi
+done
+
+mkdir -p $out/iso
+
+# daed2280-b91e-42c0-aed6-82c825ca41f3 is an arbitrary namespace, to prevent
+# independent applications from generating the same UUID for the same value.
+# (the chance of that being problematic seem pretty slim here, but that's how
+# version-5 UUID's work)
+xorriso="xorriso
+ -boot_image any gpt_disk_guid=$(uuid -v 5 daed2280-b91e-42c0-aed6-82c825ca41f3 $out | tr -d -)
+ -volume_date all_file_dates =$SOURCE_DATE_EPOCH
+ -as mkisofs
+ -iso-level 3
+ -volid ${volumeID}
+ -appid nixos
+ -publisher nixos
+ -graft-points
+ -full-iso9660-filenames
+ -joliet
+ ${isoBootFlags}
+ ${usbBootFlags}
+ ${efiBootFlags}
+ -r
+ -path-list pathlist
+ --sort-weight 0 /
+"
+
+$xorriso -output $out/iso/$isoName
+
+if test -n "$compressImage"; then
+    echo "Compressing image..."
+    zstd -T$NIX_BUILD_CORES --rm $out/iso/$isoName
+fi
+
+mkdir -p $out/nix-support
+echo $system > $out/nix-support/system
+
+if test -n "$compressImage"; then
+    echo "file iso $out/iso/$isoName.zst" >> $out/nix-support/hydra-build-products
+else
+    echo "file iso $out/iso/$isoName" >> $out/nix-support/hydra-build-products
+fi
diff --git a/nixos/lib/make-options-doc/default.nix b/nixos/lib/make-options-doc/default.nix
new file mode 100644
index 00000000000..57652dd5db1
--- /dev/null
+++ b/nixos/lib/make-options-doc/default.nix
@@ -0,0 +1,169 @@
+/* Generate JSON, XML and DocBook documentation for given NixOS options.
+
+   Minimal example:
+
+    { pkgs,  }:
+
+    let
+      eval = import (pkgs.path + "/nixos/lib/eval-config.nix") {
+        baseModules = [
+          ../module.nix
+        ];
+        modules = [];
+      };
+    in pkgs.nixosOptionsDoc {
+      options = eval.options;
+    }
+
+*/
+{ pkgs
+, lib
+, options
+, transformOptions ? lib.id  # function for additional tranformations of the options
+, revision ? "" # Specify revision for the options
+# a set of options the docs we are generating will be merged into, as if by recursiveUpdate.
+# used to split the options doc build into a static part (nixos/modules) and a dynamic part
+# (non-nixos modules imported via configuration.nix, other module sources).
+, baseOptionsJSON ? null
+# instead of printing warnings for eg options with missing descriptions (which may be lost
+# by nix build unless -L is given), emit errors instead and fail the build
+, warningsAreErrors ? true
+}:
+
+let
+  # Make a value safe for JSON. Functions are replaced by the string "<function>",
+  # derivations are replaced with an attrset
+  # { _type = "derivation"; name = <name of that derivation>; }.
+  # We need to handle derivations specially because consumers want to know about them,
+  # but we can't easily use the type,name subset of keys (since type is often used as
+  # a module option and might cause confusion). Use _type,name instead to the same
+  # effect, since _type is already used by the module system.
+  substSpecial = x:
+    if lib.isDerivation x then { _type = "derivation"; name = x.name; }
+    else if builtins.isAttrs x then lib.mapAttrs (name: substSpecial) x
+    else if builtins.isList x then map substSpecial x
+    else if lib.isFunction x then "<function>"
+    else x;
+
+  optionsList = lib.flip map optionsListVisible
+   (opt: transformOptions opt
+    // lib.optionalAttrs (opt ? example) { example = substSpecial opt.example; }
+    // lib.optionalAttrs (opt ? default) { default = substSpecial opt.default; }
+    // lib.optionalAttrs (opt ? type) { type = substSpecial opt.type; }
+    // lib.optionalAttrs (opt ? relatedPackages && opt.relatedPackages != []) { relatedPackages = genRelatedPackages opt.relatedPackages opt.name; }
+   );
+
+  # Generate DocBook documentation for a list of packages. This is
+  # what `relatedPackages` option of `mkOption` from
+  # ../../../lib/options.nix influences.
+  #
+  # Each element of `relatedPackages` can be either
+  # - a string:  that will be interpreted as an attribute name from `pkgs` and turned into a link
+  #              to search.nixos.org,
+  # - a list:    that will be interpreted as an attribute path from `pkgs` and turned into a link
+  #              to search.nixos.org,
+  # - an attrset: that can specify `name`, `path`, `comment`
+  #   (either of `name`, `path` is required, the rest are optional).
+  #
+  # NOTE: No checks against `pkgs` are made to ensure that the referenced package actually exists.
+  # Such checks are not compatible with option docs caching.
+  genRelatedPackages = packages: optName:
+    let
+      unpack = p: if lib.isString p then { name = p; }
+                  else if lib.isList p then { path = p; }
+                  else p;
+      describe = args:
+        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>
+        '';
+    in "<itemizedlist>${lib.concatStringsSep "\n" (map (p: describe (unpack p)) packages)}</itemizedlist>";
+
+  # Remove invisible and internal options.
+  optionsListVisible = lib.filter (opt: opt.visible && !opt.internal) (lib.optionAttrSetToDocList options);
+
+  optionsNix = builtins.listToAttrs (map (o: { name = o.name; value = removeAttrs o ["name" "visible" "internal"]; }) optionsList);
+
+in rec {
+  inherit optionsNix;
+
+  optionsAsciiDoc = pkgs.runCommand "options.adoc" {} ''
+    ${pkgs.python3Minimal}/bin/python ${./generateAsciiDoc.py} \
+      < ${optionsJSON}/share/doc/nixos/options.json \
+      > $out
+  '';
+
+  optionsCommonMark = pkgs.runCommand "options.md" {} ''
+    ${pkgs.python3Minimal}/bin/python ${./generateCommonMark.py} \
+      < ${optionsJSON}/share/doc/nixos/options.json \
+      > $out
+  '';
+
+  optionsJSON = pkgs.runCommand "options.json"
+    { meta.description = "List of NixOS options in JSON format";
+      buildInputs = [ pkgs.brotli ];
+      options = builtins.toFile "options.json"
+        (builtins.unsafeDiscardStringContext (builtins.toJSON optionsNix));
+    }
+    ''
+      # Export list of options in different format.
+      dst=$out/share/doc/nixos
+      mkdir -p $dst
+
+      ${
+        if baseOptionsJSON == null
+          then "cp $options $dst/options.json"
+          else ''
+            ${pkgs.python3Minimal}/bin/python ${./mergeJSON.py} \
+              ${lib.optionalString warningsAreErrors "--warnings-are-errors"} \
+              ${baseOptionsJSON} $options \
+              > $dst/options.json
+          ''
+      }
+
+      brotli -9 < $dst/options.json > $dst/options.json.br
+
+      mkdir -p $out/nix-support
+      echo "file json $dst/options.json" >> $out/nix-support/hydra-build-products
+      echo "file json-br $dst/options.json.br" >> $out/nix-support/hydra-build-products
+    '';
+
+  # Convert options.json into an XML file.
+  # The actual generation of the xml file is done in nix purely for the convenience
+  # of not having to generate the xml some other way
+  optionsXML = pkgs.runCommand "options.xml" {} ''
+    export NIX_STORE_DIR=$TMPDIR/store
+    export NIX_STATE_DIR=$TMPDIR/state
+    ${pkgs.nix}/bin/nix-instantiate \
+      --eval --xml --strict ${./optionsJSONtoXML.nix} \
+      --argstr file ${optionsJSON}/share/doc/nixos/options.json \
+      > "$out"
+  '';
+
+  optionsDocBook = pkgs.runCommand "options-docbook.xml" {} ''
+    optionsXML=${optionsXML}
+    if grep /nixpkgs/nixos/modules $optionsXML; 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"
+      echo "for hints about the offending path)."
+      exit 1
+    fi
+
+    ${pkgs.python3Minimal}/bin/python ${./sortXML.py} $optionsXML sorted.xml
+    ${pkgs.libxslt.bin}/bin/xsltproc \
+      --stringparam revision '${revision}' \
+      -o intermediate.xml ${./options-to-docbook.xsl} sorted.xml
+    ${pkgs.libxslt.bin}/bin/xsltproc \
+      -o "$out" ${./postprocess-option-descriptions.xsl} intermediate.xml
+  '';
+}
diff --git a/nixos/lib/make-options-doc/generateAsciiDoc.py b/nixos/lib/make-options-doc/generateAsciiDoc.py
new file mode 100644
index 00000000000..48eadd248c5
--- /dev/null
+++ b/nixos/lib/make-options-doc/generateAsciiDoc.py
@@ -0,0 +1,37 @@
+import json
+import sys
+
+options = json.load(sys.stdin)
+# TODO: declarations: link to github
+for (name, value) in options.items():
+    print(f'== {name}')
+    print()
+    print(value['description'])
+    print()
+    print('[discrete]')
+    print('=== details')
+    print()
+    print(f'Type:: {value["type"]}')
+    if 'default' in value:
+        print('Default::')
+        print('+')
+        print('----')
+        print(json.dumps(value['default'], ensure_ascii=False, separators=(',', ':')))
+        print('----')
+        print()
+    else:
+        print('No Default:: {blank}')
+    if value['readOnly']:
+        print('Read Only:: {blank}')
+    else:
+        print()
+    if 'example' in value:
+        print('Example::')
+        print('+')
+        print('----')
+        print(json.dumps(value['example'], ensure_ascii=False, separators=(',', ':')))
+        print('----')
+        print()
+    else:
+        print('No Example:: {blank}')
+    print()
diff --git a/nixos/lib/make-options-doc/generateCommonMark.py b/nixos/lib/make-options-doc/generateCommonMark.py
new file mode 100644
index 00000000000..404e53b0df9
--- /dev/null
+++ b/nixos/lib/make-options-doc/generateCommonMark.py
@@ -0,0 +1,27 @@
+import json
+import sys
+
+options = json.load(sys.stdin)
+for (name, value) in options.items():
+    print('##', name.replace('<', '\\<').replace('>', '\\>'))
+    print(value['description'])
+    print()
+    if 'type' in value:
+        print('*_Type_*:')
+        print(value['type'])
+        print()
+    print()
+    if 'default' in value:
+        print('*_Default_*')
+        print('```')
+        print(json.dumps(value['default'], ensure_ascii=False, separators=(',', ':')))
+        print('```')
+    print()
+    print()
+    if 'example' in value:
+        print('*_Example_*')
+        print('```')
+        print(json.dumps(value['example'], ensure_ascii=False, separators=(',', ':')))
+        print('```')
+    print()
+    print()
diff --git a/nixos/lib/make-options-doc/mergeJSON.py b/nixos/lib/make-options-doc/mergeJSON.py
new file mode 100644
index 00000000000..8e2ea322dc8
--- /dev/null
+++ b/nixos/lib/make-options-doc/mergeJSON.py
@@ -0,0 +1,93 @@
+import collections
+import json
+import sys
+from typing import Any, Dict, List
+
+JSON = Dict[str, Any]
+
+class Key:
+    def __init__(self, path: List[str]):
+        self.path = path
+    def __hash__(self):
+        result = 0
+        for id in self.path:
+            result ^= hash(id)
+        return result
+    def __eq__(self, other):
+        return type(self) is type(other) and self.path == other.path
+
+Option = collections.namedtuple('Option', ['name', 'value'])
+
+# pivot a dict of options keyed by their display name to a dict keyed by their path
+def pivot(options: Dict[str, JSON]) -> Dict[Key, Option]:
+    result: Dict[Key, Option] = dict()
+    for (name, opt) in options.items():
+        result[Key(opt['loc'])] = Option(name, opt)
+    return result
+
+# pivot back to indexed-by-full-name
+# like the docbook build we'll just fail if multiple options with differing locs
+# render to the same option name.
+def unpivot(options: Dict[Key, Option]) -> Dict[str, JSON]:
+    result: Dict[str, Dict] = dict()
+    for (key, opt) in options.items():
+        if opt.name in result:
+            raise RuntimeError(
+                'multiple options with colliding ids found',
+                opt.name,
+                result[opt.name]['loc'],
+                opt.value['loc'],
+            )
+        result[opt.name] = opt.value
+    return result
+
+warningsAreErrors = sys.argv[1] == "--warnings-are-errors"
+optOffset = 1 if warningsAreErrors else 0
+options = pivot(json.load(open(sys.argv[1 + optOffset], 'r')))
+overrides = pivot(json.load(open(sys.argv[2 + optOffset], 'r')))
+
+# fix up declaration paths in lazy options, since we don't eval them from a full nixpkgs dir
+for (k, v) in options.items():
+    v.value['declarations'] = list(map(lambda s: f'nixos/modules/{s}', v.value['declarations']))
+
+# merge both descriptions
+for (k, v) in overrides.items():
+    cur = options.setdefault(k, v).value
+    for (ok, ov) in v.value.items():
+        if ok == 'declarations':
+            decls = cur[ok]
+            for d in ov:
+                if d not in decls:
+                    decls += [d]
+        elif ok == "type":
+            # ignore types of placeholder options
+            if ov != "_unspecified" or cur[ok] == "_unspecified":
+                cur[ok] = ov
+        elif ov is not None or cur.get(ok, None) is None:
+            cur[ok] = ov
+
+severity = "error" if warningsAreErrors else "warning"
+
+# check that every option has a description
+hasWarnings = False
+for (k, v) in options.items():
+    if v.value.get('description', None) is None:
+        hasWarnings = True
+        print(f"\x1b[1;31m{severity}: option {v.name} has no description\x1b[0m", file=sys.stderr)
+        v.value['description'] = "This option has no description."
+    if v.value.get('type', "unspecified") == "unspecified":
+        hasWarnings = True
+        print(
+            f"\x1b[1;31m{severity}: option {v.name} has no type. Please specify a valid type, see " +
+            "https://nixos.org/manual/nixos/stable/index.html#sec-option-types\x1b[0m", file=sys.stderr)
+
+if hasWarnings and warningsAreErrors:
+    print(
+        "\x1b[1;31m" +
+        "Treating warnings as errors. Set documentation.nixos.options.warningsAreErrors " +
+        "to false to ignore these warnings." +
+        "\x1b[0m",
+        file=sys.stderr)
+    sys.exit(1)
+
+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
new file mode 100644
index 00000000000..b286f7b5e2c
--- /dev/null
+++ b/nixos/lib/make-options-doc/options-to-docbook.xsl
@@ -0,0 +1,246 @@
+<?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="program" />
+
+
+  <xsl:template match="/expr/list">
+    <appendix xml:id="appendix-configuration-options">
+      <title>Configuration Options</title>
+      <variablelist xml:id="configuration-variable-list">
+        <xsl:for-each select="attrs">
+          <xsl:variable name="id" select="concat('opt-', str:replace(str:replace(str:replace(str:replace(attr[@name = 'name']/string/@value, '*', '_'), '&lt;', '_'), '>', '_'), ':', '_'))" />
+          <varlistentry>
+            <term xlink:href="#{$id}">
+              <xsl:attribute name="xml:id"><xsl:value-of select="$id"/></xsl:attribute>
+              <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>
+    </appendix>
+  </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="string[contains(@value, '&#010;')]" mode="top">
+    <programlisting>
+      <xsl:text>''&#010;</xsl:text>
+      <xsl:value-of select='str:replace(str:replace(@value, "&apos;&apos;", "&apos;&apos;&apos;"), "${", "&apos;&apos;${")' />
+      <xsl:text>''</xsl:text>
+    </programlisting>
+  </xsl:template>
+
+
+  <xsl:template match="*" mode="top">
+    <literal><xsl:apply-templates select="." /></literal>
+  </xsl:template>
+
+
+  <xsl:template match="null">
+    <xsl:text>null</xsl:text>
+  </xsl:template>
+
+
+  <xsl:template match="string">
+    <xsl:choose>
+      <xsl:when test="(contains(@value, '&quot;') or contains(@value, '\')) and not(contains(@value, '&#010;'))">
+        <xsl:text>''</xsl:text><xsl:value-of select='str:replace(str:replace(@value, "&apos;&apos;", "&apos;&apos;&apos;"), "${", "&apos;&apos;${")' /><xsl:text>''</xsl:text>
+      </xsl:when>
+      <xsl:otherwise>
+        <xsl:text>"</xsl:text><xsl:value-of select="str:replace(str:replace(str:replace(str:replace(@value, '\', '\\'), '&quot;', '\&quot;'), '&#010;', '\n'), '${', '\${')" /><xsl:text>"</xsl:text>
+      </xsl:otherwise>
+    </xsl:choose>
+  </xsl:template>
+
+
+  <xsl:template match="int">
+    <xsl:value-of select="@value" />
+  </xsl:template>
+
+
+  <xsl:template match="bool[@value = 'true']">
+    <xsl:text>true</xsl:text>
+  </xsl:template>
+
+
+  <xsl:template match="bool[@value = 'false']">
+    <xsl:text>false</xsl:text>
+  </xsl:template>
+
+
+  <xsl:template match="list">
+    [
+    <xsl:for-each select="*">
+      <xsl:apply-templates select="." />
+      <xsl:text> </xsl:text>
+    </xsl:for-each>
+    ]
+  </xsl:template>
+
+
+  <xsl:template match="attrs[attr[@name = '_type' and string[@value = 'literalExpression']]]">
+    <xsl:value-of select="attr[@name = 'text']/string/@value" />
+  </xsl:template>
+
+
+  <xsl:template match="attrs">
+    {
+    <xsl:for-each select="attr">
+      <xsl:value-of select="@name" />
+      <xsl:text> = </xsl:text>
+      <xsl:apply-templates select="*" /><xsl:text>; </xsl:text>
+    </xsl:for-each>
+    }
+  </xsl:template>
+
+
+  <xsl:template match="attrs[attr[@name = '_type' and string[@value = 'derivation']]]">
+    <replaceable>(build of <xsl:value-of select="attr[@name = 'name']/string/@value" />)</replaceable>
+  </xsl:template>
+
+  <xsl:template match="attr[@name = 'declarations' or @name = 'definitions']">
+    <simplelist>
+      <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:template match="function">
+    <xsl:text>λ</xsl:text>
+  </xsl:template>
+
+
+</xsl:stylesheet>
diff --git a/nixos/lib/make-options-doc/optionsJSONtoXML.nix b/nixos/lib/make-options-doc/optionsJSONtoXML.nix
new file mode 100644
index 00000000000..ba50c5f898b
--- /dev/null
+++ b/nixos/lib/make-options-doc/optionsJSONtoXML.nix
@@ -0,0 +1,6 @@
+{ file }:
+
+builtins.attrValues
+  (builtins.mapAttrs
+    (name: def: def // { inherit name; })
+    (builtins.fromJSON (builtins.readFile file)))
diff --git a/nixos/lib/make-options-doc/postprocess-option-descriptions.xsl b/nixos/lib/make-options-doc/postprocess-option-descriptions.xsl
new file mode 100644
index 00000000000..1201c7612c2
--- /dev/null
+++ b/nixos/lib/make-options-doc/postprocess-option-descriptions.xsl
@@ -0,0 +1,115 @@
+<?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:exsl="http://exslt.org/common"
+                xmlns:db="http://docbook.org/ns/docbook"
+                xmlns:nixos="tag:nixos.org"
+                extension-element-prefixes="str exsl">
+  <xsl:output method='xml' encoding="UTF-8" />
+
+  <xsl:template match="@*|node()">
+    <xsl:copy>
+      <xsl:apply-templates select="@*|node()" />
+    </xsl:copy>
+  </xsl:template>
+
+  <xsl:template name="break-up-description">
+    <xsl:param name="input" />
+    <xsl:param name="buffer" />
+
+    <!-- Every time we have two newlines following each other, we want to
+         break it into </para><para>. -->
+    <xsl:variable name="parbreak" select="'&#xa;&#xa;'" />
+
+    <!-- Similar to "(head:tail) = input" in Haskell. -->
+    <xsl:variable name="head" select="$input[1]" />
+    <xsl:variable name="tail" select="$input[position() &gt; 1]" />
+
+    <xsl:choose>
+      <xsl:when test="$head/self::text() and contains($head, $parbreak)">
+        <!-- If the haystack provided to str:split() directly starts or
+             ends with $parbreak, it doesn't generate a <token/> for that,
+             so we are doing this here. -->
+        <xsl:variable name="splitted-raw">
+          <xsl:if test="starts-with($head, $parbreak)"><token /></xsl:if>
+          <xsl:for-each select="str:split($head, $parbreak)">
+            <token><xsl:value-of select="node()" /></token>
+          </xsl:for-each>
+          <!-- Something like ends-with($head, $parbreak), but there is
+               no ends-with() in XSLT, so we need to use substring(). -->
+          <xsl:if test="
+            substring($head, string-length($head) -
+                             string-length($parbreak) + 1) = $parbreak
+          "><token /></xsl:if>
+        </xsl:variable>
+        <xsl:variable name="splitted"
+                      select="exsl:node-set($splitted-raw)/token" />
+        <!-- The buffer we had so far didn't contain any text nodes that
+             contain a $parbreak, so we can put the buffer along with the
+             first token of $splitted into a para element. -->
+        <para xmlns="http://docbook.org/ns/docbook">
+          <xsl:apply-templates select="exsl:node-set($buffer)" />
+          <xsl:apply-templates select="$splitted[1]/node()" />
+        </para>
+        <!-- We have already emitted the first splitted result, so the
+             last result is going to be set as the new $buffer later
+             because its contents may not be directly followed up by a
+             $parbreak. -->
+        <xsl:for-each select="$splitted[position() &gt; 1
+                              and position() &lt; last()]">
+          <para xmlns="http://docbook.org/ns/docbook">
+            <xsl:apply-templates select="node()" />
+          </para>
+        </xsl:for-each>
+        <xsl:call-template name="break-up-description">
+          <xsl:with-param name="input" select="$tail" />
+          <xsl:with-param name="buffer" select="$splitted[last()]/node()" />
+        </xsl:call-template>
+      </xsl:when>
+      <!-- Either non-text node or one without $parbreak, which we just
+           want to buffer and continue recursing. -->
+      <xsl:when test="$input">
+        <xsl:call-template name="break-up-description">
+          <xsl:with-param name="input" select="$tail" />
+          <!-- This essentially appends $head to $buffer. -->
+          <xsl:with-param name="buffer">
+            <xsl:if test="$buffer">
+              <xsl:for-each select="exsl:node-set($buffer)">
+                <xsl:apply-templates select="." />
+              </xsl:for-each>
+            </xsl:if>
+            <xsl:apply-templates select="$head" />
+          </xsl:with-param>
+        </xsl:call-template>
+      </xsl:when>
+      <!-- No more $input, just put the remaining $buffer in a para. -->
+      <xsl:otherwise>
+        <para xmlns="http://docbook.org/ns/docbook">
+          <xsl:apply-templates select="exsl:node-set($buffer)" />
+        </para>
+      </xsl:otherwise>
+    </xsl:choose>
+  </xsl:template>
+
+  <xsl:template match="nixos:option-description">
+    <xsl:choose>
+      <!--
+        Only process nodes that are comprised of a single <para/> element,
+        because if that's not the case the description already contains
+        </para><para> in between and we need no further processing.
+      -->
+      <xsl:when test="count(db:para) > 1">
+        <xsl:apply-templates select="node()" />
+      </xsl:when>
+      <xsl:otherwise>
+        <xsl:call-template name="break-up-description">
+          <xsl:with-param name="input"
+                          select="exsl:node-set(db:para/node())" />
+        </xsl:call-template>
+      </xsl:otherwise>
+    </xsl:choose>
+  </xsl:template>
+
+</xsl:stylesheet>
diff --git a/nixos/lib/make-options-doc/sortXML.py b/nixos/lib/make-options-doc/sortXML.py
new file mode 100644
index 00000000000..e63ff3538b3
--- /dev/null
+++ b/nixos/lib/make-options-doc/sortXML.py
@@ -0,0 +1,27 @@
+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/make-squashfs.nix b/nixos/lib/make-squashfs.nix
new file mode 100644
index 00000000000..170d315fb75
--- /dev/null
+++ b/nixos/lib/make-squashfs.nix
@@ -0,0 +1,35 @@
+{ stdenv, squashfsTools, closureInfo
+
+, # The root directory of the squashfs filesystem is filled with the
+  # closures of the Nix store paths listed here.
+  storeContents ? []
+, # Compression parameters.
+  # For zstd compression you can use "zstd -Xcompression-level 6".
+  comp ? "xz -Xdict-size 100%"
+}:
+
+stdenv.mkDerivation {
+  name = "squashfs.img";
+
+  nativeBuildInputs = [ squashfsTools ];
+
+  buildCommand =
+    ''
+      closureInfo=${closureInfo { rootPaths = storeContents; }}
+
+      # Also include a manifest of the closures in a format suitable
+      # for nix-store --load-db.
+      cp $closureInfo/registration nix-path-registration
+
+      # 64 cores on i686 does not work
+      # fails with FATAL ERROR: mangle2:: xz compress failed with error code 5
+      if ((NIX_BUILD_CORES > 48)); then
+        NIX_BUILD_CORES=48
+      fi
+
+      # Generate the squashfs image.
+      mksquashfs nix-path-registration $(cat $closureInfo/store-paths) $out \
+        -no-hardlinks -keep-as-directory -all-root -b 1048576 -comp ${comp} \
+        -processors $NIX_BUILD_CORES
+    '';
+}
diff --git a/nixos/lib/make-system-tarball.nix b/nixos/lib/make-system-tarball.nix
new file mode 100644
index 00000000000..dab168f4a48
--- /dev/null
+++ b/nixos/lib/make-system-tarball.nix
@@ -0,0 +1,56 @@
+{ stdenv, closureInfo, pixz
+
+, # The file name of the resulting tarball
+  fileName ? "nixos-system-${stdenv.hostPlatform.system}"
+
+, # The files and directories to be placed in the tarball.
+  # This is a list of attribute sets {source, target} where `source'
+  # is the file system object (regular file or directory) to be
+  # grafted in the file system at path `target'.
+  contents
+
+, # In addition to `contents', the closure of the store paths listed
+  # in `packages' are also placed in the Nix store of the tarball.  This is
+  # a list of attribute sets {object, symlink} where `object' if a
+  # store path whose closure will be copied, and `symlink' is a
+  # symlink to `object' that will be added to the tarball.
+  storeContents ? []
+
+  # Extra commands to be executed before archiving files
+, extraCommands ? ""
+
+  # Extra tar arguments
+, extraArgs ? ""
+  # Command used for compression
+, compressCommand ? "pixz"
+  # Extension for the compressed tarball
+, compressionExtension ? ".xz"
+  # extra inputs, like the compressor to use
+, extraInputs ? [ pixz ]
+}:
+
+let
+  symlinks = map (x: x.symlink) storeContents;
+  objects = map (x: x.object) storeContents;
+in
+
+stdenv.mkDerivation {
+  name = "tarball";
+  builder = ./make-system-tarball.sh;
+  nativeBuildInputs = extraInputs;
+
+  inherit fileName extraArgs extraCommands compressCommand;
+
+  # !!! should use XML.
+  sources = map (x: x.source) contents;
+  targets = map (x: x.target) contents;
+
+  # !!! should use XML.
+  inherit symlinks objects;
+
+  closureInfo = closureInfo {
+    rootPaths = objects;
+  };
+
+  extension = compressionExtension;
+}
diff --git a/nixos/lib/make-system-tarball.sh b/nixos/lib/make-system-tarball.sh
new file mode 100644
index 00000000000..1a0017a1799
--- /dev/null
+++ b/nixos/lib/make-system-tarball.sh
@@ -0,0 +1,57 @@
+source $stdenv/setup
+
+sources_=($sources)
+targets_=($targets)
+
+objects=($objects)
+symlinks=($symlinks)
+
+
+# Remove the initial slash from a path, since genisofs likes it that way.
+stripSlash() {
+    res="$1"
+    if test "${res:0:1}" = /; then res=${res:1}; fi
+}
+
+# Add the individual files.
+for ((i = 0; i < ${#targets_[@]}; i++)); do
+    stripSlash "${targets_[$i]}"
+    mkdir -p "$(dirname "$res")"
+    cp -a "${sources_[$i]}" "$res"
+done
+
+
+# Add the closures of the top-level store objects.
+chmod +w .
+mkdir -p nix/store
+for i in $(< $closureInfo/store-paths); do
+    cp -a "$i" "${i:1}"
+done
+
+
+# TODO tar ruxo
+# Also include a manifest of the closures in a format suitable for
+# nix-store --load-db.
+cp $closureInfo/registration nix-path-registration
+
+# Add symlinks to the top-level store objects.
+for ((n = 0; n < ${#objects[*]}; n++)); do
+    object=${objects[$n]}
+    symlink=${symlinks[$n]}
+    if test "$symlink" != "none"; then
+        mkdir -p $(dirname ./$symlink)
+        ln -s $object ./$symlink
+    fi
+done
+
+$extraCommands
+
+mkdir -p $out/tarball
+
+rm env-vars
+
+time tar --sort=name --mtime='@1' --owner=0 --group=0 --numeric-owner -c * $extraArgs | $compressCommand > $out/tarball/$fileName.tar${extension}
+
+mkdir -p $out/nix-support
+echo $system > $out/nix-support/system
+echo "file system-tarball $out/tarball/$fileName.tar${extension}" > $out/nix-support/hydra-build-products
diff --git a/nixos/lib/make-zfs-image.nix b/nixos/lib/make-zfs-image.nix
new file mode 100644
index 00000000000..a84732aa117
--- /dev/null
+++ b/nixos/lib/make-zfs-image.nix
@@ -0,0 +1,333 @@
+# Note: This is a private API, internal to NixOS. Its interface is subject
+# to change without notice.
+#
+# The result of this builder is two disk images:
+#
+#  * `boot` - a small disk formatted with FAT to be used for /boot. FAT is
+#    chosen to support EFI.
+#  * `root` - a larger disk with a zpool taking the entire disk.
+#
+# This two-disk approach is taken to satisfy ZFS's requirements for
+# autoexpand.
+#
+# # Why doesn't autoexpand work with ZFS in a partition?
+#
+# When ZFS owns the whole disk doesn’t really use a partition: it has
+# a marker partition at the start and a marker partition at the end of
+# the disk.
+#
+# If ZFS is constrained to a partition, ZFS leaves expanding the partition
+# up to the user. Obviously, the user may not choose to do so.
+#
+# Once the user expands the partition, calling zpool online -e expands the
+# vdev to use the whole partition. It doesn’t happen automatically
+# presumably because zed doesn’t get an event saying it’s partition grew,
+# whereas it can and does get an event saying the whole disk it is on is
+# now larger.
+{ lib
+, pkgs
+, # The NixOS configuration to be installed onto the disk image.
+  config
+
+, # size of the FAT boot disk, in megabytes.
+  bootSize ? 1024
+
+, # The size of the root disk, in megabytes.
+  rootSize ? 2048
+
+, # The name of the ZFS pool
+  rootPoolName ? "tank"
+
+, # zpool properties
+  rootPoolProperties ? {
+    autoexpand = "on";
+  }
+, # pool-wide filesystem properties
+  rootPoolFilesystemProperties ? {
+    acltype = "posixacl";
+    atime = "off";
+    compression = "on";
+    mountpoint = "legacy";
+    xattr = "sa";
+  }
+
+, # datasets, with per-attribute options:
+  # mount: (optional) mount point in the VM
+  # properties: (optional) ZFS properties on the dataset, like filesystemProperties
+  # Notes:
+  # 1. datasets will be created from shorter to longer names as a simple topo-sort
+  # 2. you should define a root's dataset's mount for `/`
+  datasets ? { }
+
+, # The files and directories to be placed in the target file system.
+  # This is a list of attribute sets {source, target} where `source'
+  # is the file system object (regular file or directory) to be
+  # grafted in the file system at path `target'.
+  contents ? []
+
+, # The initial NixOS configuration file to be copied to
+  # /etc/nixos/configuration.nix. This configuration will be embedded
+  # inside a configuration which includes the described ZFS fileSystems.
+  configFile ? null
+
+, # Shell code executed after the VM has finished.
+  postVM ? ""
+
+, name ? "nixos-disk-image"
+
+, # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw.
+  format ? "raw"
+
+, # Include a copy of Nixpkgs in the disk image
+  includeChannel ? true
+}:
+let
+  formatOpt = if format == "qcow2-compressed" then "qcow2" else format;
+
+  compress = lib.optionalString (format == "qcow2-compressed") "-c";
+
+  filenameSuffix = "." + {
+    qcow2 = "qcow2";
+    vdi = "vdi";
+    vpc = "vhd";
+    raw = "img";
+  }.${formatOpt} or formatOpt;
+  bootFilename = "nixos.boot${filenameSuffix}";
+  rootFilename = "nixos.root${filenameSuffix}";
+
+  # FIXME: merge with channel.nix / make-channel.nix.
+  channelSources =
+    let
+      nixpkgs = lib.cleanSource pkgs.path;
+    in
+      pkgs.runCommand "nixos-${config.system.nixos.version}" {} ''
+        mkdir -p $out
+        cp -prd ${nixpkgs.outPath} $out/nixos
+        chmod -R u+w $out/nixos
+        if [ ! -e $out/nixos/nixpkgs ]; then
+          ln -s . $out/nixos/nixpkgs
+        fi
+        rm -rf $out/nixos/.git
+        echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
+      '';
+
+  closureInfo = pkgs.closureInfo {
+    rootPaths = [ config.system.build.toplevel ]
+    ++ (lib.optional includeChannel channelSources);
+  };
+
+  modulesTree = pkgs.aggregateModules
+    (with config.boot.kernelPackages; [ kernel zfs ]);
+
+  tools = lib.makeBinPath (
+    with pkgs; [
+      config.system.build.nixos-enter
+      config.system.build.nixos-install
+      dosfstools
+      e2fsprogs
+      gptfdisk
+      nix
+      parted
+      utillinux
+      zfs
+    ]
+  );
+
+  hasDefinedMount  = disk: ((disk.mount or null) != null);
+
+  stringifyProperties = prefix: properties: lib.concatStringsSep " \\\n" (
+    lib.mapAttrsToList
+      (
+        property: value: "${prefix} ${lib.escapeShellArg property}=${lib.escapeShellArg value}"
+      )
+      properties
+  );
+
+  featuresToProperties = features:
+    lib.listToAttrs
+      (builtins.map (feature: {
+        name = "feature@${feature}";
+        value = "enabled";
+      }) features);
+
+  createDatasets =
+    let
+      datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
+      sorted = lib.sort (left: right: (lib.stringLength left.name) < (lib.stringLength right.name)) datasetlist;
+      cmd = { name, value }:
+        let
+          properties = stringifyProperties "-o" (value.properties or {});
+        in
+          "zfs create -p ${properties} ${name}";
+    in
+      lib.concatMapStringsSep "\n" cmd sorted;
+
+  mountDatasets =
+    let
+      datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
+      mounts = lib.filter ({ value, ... }: hasDefinedMount value) datasetlist;
+      sorted = lib.sort (left: right: (lib.stringLength left.value.mount) < (lib.stringLength right.value.mount)) mounts;
+      cmd = { name, value }:
+        ''
+          mkdir -p /mnt${lib.escapeShellArg value.mount}
+          mount -t zfs ${name} /mnt${lib.escapeShellArg value.mount}
+        '';
+    in
+      lib.concatMapStringsSep "\n" cmd sorted;
+
+  unmountDatasets =
+    let
+      datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
+      mounts = lib.filter ({ value, ... }: hasDefinedMount value) datasetlist;
+      sorted = lib.sort (left: right: (lib.stringLength left.value.mount) > (lib.stringLength right.value.mount)) mounts;
+      cmd = { name, value }:
+        ''
+          umount /mnt${lib.escapeShellArg value.mount}
+        '';
+    in
+      lib.concatMapStringsSep "\n" cmd sorted;
+
+
+  fileSystemsCfgFile =
+    let
+      mountable = lib.filterAttrs (_: value: hasDefinedMount value) datasets;
+    in
+      pkgs.runCommand "filesystem-config.nix" {
+        buildInputs = with pkgs; [ jq nixpkgs-fmt ];
+        filesystems = builtins.toJSON {
+          fileSystems = lib.mapAttrs'
+            (
+              dataset: attrs:
+                {
+                  name = attrs.mount;
+                  value = {
+                    fsType = "zfs";
+                    device = "${dataset}";
+                  };
+                }
+            )
+            mountable;
+        };
+        passAsFile = [ "filesystems" ];
+      } ''
+      (
+        echo "builtins.fromJSON '''"
+        jq . < "$filesystemsPath"
+        echo "'''"
+      ) > $out
+
+      nixpkgs-fmt $out
+    '';
+
+  mergedConfig =
+    if configFile == null
+    then fileSystemsCfgFile
+    else
+      pkgs.runCommand "configuration.nix" {
+        buildInputs = with pkgs; [ nixpkgs-fmt ];
+      }
+        ''
+          (
+            echo '{ imports = ['
+            printf "(%s)\n" "$(cat ${fileSystemsCfgFile})";
+            printf "(%s)\n" "$(cat ${configFile})";
+            echo ']; }'
+          ) > $out
+
+          nixpkgs-fmt $out
+        '';
+
+  image = (
+    pkgs.vmTools.override {
+      rootModules =
+        [ "zfs" "9p" "9pnet_virtio" "virtio_pci" "virtio_blk" ] ++
+          (pkgs.lib.optional pkgs.stdenv.hostPlatform.isx86 "rtc_cmos");
+      kernel = modulesTree;
+    }
+  ).runInLinuxVM (
+    pkgs.runCommand name
+      {
+        QEMU_OPTS = "-drive file=$bootDiskImage,if=virtio,cache=unsafe,werror=report"
+         + " -drive file=$rootDiskImage,if=virtio,cache=unsafe,werror=report";
+        preVM = ''
+          PATH=$PATH:${pkgs.qemu_kvm}/bin
+          mkdir $out
+          bootDiskImage=boot.raw
+          qemu-img create -f raw $bootDiskImage ${toString bootSize}M
+
+          rootDiskImage=root.raw
+          qemu-img create -f raw $rootDiskImage ${toString rootSize}M
+        '';
+
+        postVM = ''
+          ${if formatOpt == "raw" then ''
+          mv $bootDiskImage $out/${bootFilename}
+          mv $rootDiskImage $out/${rootFilename}
+        '' else ''
+          ${pkgs.qemu}/bin/qemu-img convert -f raw -O ${formatOpt} ${compress} $bootDiskImage $out/${bootFilename}
+          ${pkgs.qemu}/bin/qemu-img convert -f raw -O ${formatOpt} ${compress} $rootDiskImage $out/${rootFilename}
+        ''}
+          bootDiskImage=$out/${bootFilename}
+          rootDiskImage=$out/${rootFilename}
+          set -x
+          ${postVM}
+        '';
+      } ''
+      export PATH=${tools}:$PATH
+      set -x
+
+      cp -sv /dev/vda /dev/sda
+      cp -sv /dev/vda /dev/xvda
+
+      parted --script /dev/vda -- \
+        mklabel gpt \
+        mkpart no-fs 1MiB 2MiB \
+        set 1 bios_grub on \
+        align-check optimal 1 \
+        mkpart ESP fat32 2MiB -1MiB \
+        align-check optimal 2 \
+        print
+
+      sfdisk --dump /dev/vda
+
+
+      zpool create \
+        ${stringifyProperties "  -o" rootPoolProperties} \
+        ${stringifyProperties "  -O" rootPoolFilesystemProperties} \
+        ${rootPoolName} /dev/vdb
+      parted --script /dev/vdb -- print
+
+      ${createDatasets}
+      ${mountDatasets}
+
+      mkdir -p /mnt/boot
+      mkfs.vfat -n ESP /dev/vda2
+      mount /dev/vda2 /mnt/boot
+
+      mount
+
+      # Install a configuration.nix
+      mkdir -p /mnt/etc/nixos
+      # `cat` so it is mutable on the fs
+      cat ${mergedConfig} > /mnt/etc/nixos/configuration.nix
+
+      export NIX_STATE_DIR=$TMPDIR/state
+      nix-store --load-db < ${closureInfo}/registration
+
+      nixos-install \
+        --root /mnt \
+        --no-root-passwd \
+        --system ${config.system.build.toplevel} \
+        --substituters "" \
+        ${lib.optionalString includeChannel ''--channel ${channelSources}''}
+
+      df -h
+
+      umount /mnt/boot
+      ${unmountDatasets}
+
+      zpool export ${rootPoolName}
+    ''
+  );
+in
+image
diff --git a/nixos/lib/qemu-common.nix b/nixos/lib/qemu-common.nix
new file mode 100644
index 00000000000..20bbe9ff5d9
--- /dev/null
+++ b/nixos/lib/qemu-common.nix
@@ -0,0 +1,32 @@
+# QEMU-related utilities shared between various Nix expressions.
+{ lib, pkgs }:
+
+let
+  zeroPad = n:
+    lib.optionalString (n < 16) "0" +
+      (if n > 255
+       then throw "Can't have more than 255 nets or nodes!"
+       else lib.toHexString n);
+in
+
+rec {
+  qemuNicMac = net: machine: "52:54:00:12:${zeroPad net}:${zeroPad machine}";
+
+  qemuNICFlags = nic: net: machine:
+    [ "-device virtio-net-pci,netdev=vlan${toString nic},mac=${qemuNicMac net machine}"
+      ''-netdev vde,id=vlan${toString nic},sock="$QEMU_VDE_SOCKET_${toString net}"''
+    ];
+
+  qemuSerialDevice = if pkgs.stdenv.hostPlatform.isx86 || pkgs.stdenv.hostPlatform.isRiscV then "ttyS0"
+        else if (with pkgs.stdenv.hostPlatform; isAarch32 || isAarch64 || isPower) then "ttyAMA0"
+        else throw "Unknown QEMU serial device for system '${pkgs.stdenv.hostPlatform.system}'";
+
+  qemuBinary = qemuPkg: {
+    x86_64-linux = "${qemuPkg}/bin/qemu-kvm -cpu max";
+    armv7l-linux = "${qemuPkg}/bin/qemu-system-arm -enable-kvm -machine virt -cpu host";
+    aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -enable-kvm -machine virt,gic-version=host -cpu host";
+    powerpc64le-linux = "${qemuPkg}/bin/qemu-system-ppc64 -machine powernv";
+    powerpc64-linux = "${qemuPkg}/bin/qemu-system-ppc64 -machine powernv";
+    x86_64-darwin = "${qemuPkg}/bin/qemu-kvm -cpu max";
+  }.${pkgs.stdenv.hostPlatform.system} or "${qemuPkg}/bin/qemu-kvm";
+}
diff --git a/nixos/lib/systemd-lib.nix b/nixos/lib/systemd-lib.nix
new file mode 100644
index 00000000000..a472d97f5cc
--- /dev/null
+++ b/nixos/lib/systemd-lib.nix
@@ -0,0 +1,440 @@
+{ config, lib, pkgs }:
+
+with lib;
+
+let
+  cfg = config.systemd;
+  lndir = "${pkgs.buildPackages.xorg.lndir}/bin/lndir";
+  systemd = cfg.package;
+in rec {
+
+  shellEscape = s: (replaceChars [ "\\" ] [ "\\\\" ] s);
+
+  mkPathSafeName = lib.replaceChars ["@" ":" "\\" "[" "]"] ["-" "-" "-" "" ""];
+
+  # a type for options that take a unit name
+  unitNameType = types.strMatching "[a-zA-Z0-9@%:_.\\-]+[.](service|socket|device|mount|automount|swap|target|path|timer|scope|slice)";
+
+  makeUnit = name: unit:
+    if unit.enable then
+      pkgs.runCommand "unit-${mkPathSafeName name}"
+        { preferLocalBuild = true;
+          allowSubstitutes = false;
+          inherit (unit) text;
+        }
+        ''
+          mkdir -p $out
+          echo -n "$text" > $out/${shellEscape name}
+        ''
+    else
+      pkgs.runCommand "unit-${mkPathSafeName name}-disabled"
+        { preferLocalBuild = true;
+          allowSubstitutes = false;
+        }
+        ''
+          mkdir -p $out
+          ln -s /dev/null $out/${shellEscape name}
+        '';
+
+  boolValues = [true false "yes" "no"];
+
+  digits = map toString (range 0 9);
+
+  isByteFormat = s:
+    let
+      l = reverseList (stringToCharacters s);
+      suffix = head l;
+      nums = tail l;
+    in elem suffix (["K" "M" "G" "T"] ++ digits)
+      && all (num: elem num digits) nums;
+
+  assertByteFormat = name: group: attr:
+    optional (attr ? ${name} && ! isByteFormat attr.${name})
+      "Systemd ${group} field `${name}' must be in byte format [0-9]+[KMGT].";
+
+  hexChars = stringToCharacters "0123456789abcdefABCDEF";
+
+  isMacAddress = s: stringLength s == 17
+    && flip all (splitString ":" s) (bytes:
+      all (byte: elem byte hexChars) (stringToCharacters bytes)
+    );
+
+  assertMacAddress = name: group: attr:
+    optional (attr ? ${name} && ! isMacAddress attr.${name})
+      "Systemd ${group} field `${name}' must be a valid mac address.";
+
+  isPort = i: i >= 0 && i <= 65535;
+
+  assertPort = name: group: attr:
+    optional (attr ? ${name} && ! isPort attr.${name})
+      "Error on the systemd ${group} field `${name}': ${attr.name} is not a valid port number.";
+
+  assertValueOneOf = name: values: group: attr:
+    optional (attr ? ${name} && !elem attr.${name} values)
+      "Systemd ${group} field `${name}' cannot have value `${toString attr.${name}}'.";
+
+  assertHasField = name: group: attr:
+    optional (!(attr ? ${name}))
+      "Systemd ${group} field `${name}' must exist.";
+
+  assertRange = name: min: max: group: attr:
+    optional (attr ? ${name} && !(min <= attr.${name} && max >= attr.${name}))
+      "Systemd ${group} field `${name}' is outside the range [${toString min},${toString max}]";
+
+  assertMinimum = name: min: group: attr:
+    optional (attr ? ${name} && attr.${name} < min)
+      "Systemd ${group} field `${name}' must be greater than or equal to ${toString min}";
+
+  assertOnlyFields = fields: group: attr:
+    let badFields = filter (name: ! elem name fields) (attrNames attr); in
+    optional (badFields != [ ])
+      "Systemd ${group} has extra fields [${concatStringsSep " " badFields}].";
+
+  assertInt = name: group: attr:
+    optional (attr ? ${name} && !isInt attr.${name})
+      "Systemd ${group} field `${name}' is not an integer";
+
+  checkUnitConfig = group: checks: attrs: let
+    # We're applied at the top-level type (attrsOf unitOption), so the actual
+    # unit options might contain attributes from mkOverride and mkIf that we need to
+    # convert into single values before checking them.
+    defs = mapAttrs (const (v:
+      if v._type or "" == "override" then v.content
+      else if v._type or "" == "if" then v.content
+      else v
+    )) attrs;
+    errors = concatMap (c: c group defs) checks;
+  in if errors == [] then true
+     else builtins.trace (concatStringsSep "\n" errors) false;
+
+  toOption = x:
+    if x == true then "true"
+    else if x == false then "false"
+    else toString x;
+
+  attrsToSection = as:
+    concatStrings (concatLists (mapAttrsToList (name: value:
+      map (x: ''
+          ${name}=${toOption x}
+        '')
+        (if isList value then value else [value]))
+        as));
+
+  generateUnits = generateUnits' true;
+
+  generateUnits' = allowCollisions: type: units: upstreamUnits: upstreamWants:
+    pkgs.runCommand "${type}-units"
+      { preferLocalBuild = true;
+        allowSubstitutes = false;
+      } ''
+      mkdir -p $out
+
+      # Copy the upstream systemd units we're interested in.
+      for i in ${toString upstreamUnits}; do
+        fn=${cfg.package}/example/systemd/${type}/$i
+        if ! [ -e $fn ]; then echo "missing $fn"; false; fi
+        if [ -L $fn ]; then
+          target="$(readlink "$fn")"
+          if [ ''${target:0:3} = ../ ]; then
+            ln -s "$(readlink -f "$fn")" $out/
+          else
+            cp -pd $fn $out/
+          fi
+        else
+          ln -s $fn $out/
+        fi
+      done
+
+      # Copy .wants links, but only those that point to units that
+      # we're interested in.
+      for i in ${toString upstreamWants}; do
+        fn=${cfg.package}/example/systemd/${type}/$i
+        if ! [ -e $fn ]; then echo "missing $fn"; false; fi
+        x=$out/$(basename $fn)
+        mkdir $x
+        for i in $fn/*; do
+          y=$x/$(basename $i)
+          cp -pd $i $y
+          if ! [ -e $y ]; then rm $y; fi
+        done
+      done
+
+      # Symlink all units provided listed in systemd.packages.
+      packages="${toString cfg.packages}"
+
+      # Filter duplicate directories
+      declare -A unique_packages
+      for k in $packages ; do unique_packages[$k]=1 ; done
+
+      for i in ''${!unique_packages[@]}; do
+        for fn in $i/etc/systemd/${type}/* $i/lib/systemd/${type}/*; do
+          if ! [[ "$fn" =~ .wants$ ]]; then
+            if [[ -d "$fn" ]]; then
+              targetDir="$out/$(basename "$fn")"
+              mkdir -p "$targetDir"
+              ${lndir} "$fn" "$targetDir"
+            else
+              ln -s $fn $out/
+            fi
+          fi
+        done
+      done
+
+      # Symlink all units defined by systemd.units. If these are also
+      # provided by systemd or systemd.packages, then add them as
+      # <unit-name>.d/overrides.conf, which makes them extend the
+      # upstream unit.
+      for i in ${toString (mapAttrsToList (n: v: v.unit) units)}; do
+        fn=$(basename $i/*)
+        if [ -e $out/$fn ]; then
+          if [ "$(readlink -f $i/$fn)" = /dev/null ]; then
+            ln -sfn /dev/null $out/$fn
+          else
+            ${if allowCollisions then ''
+              mkdir -p $out/$fn.d
+              ln -s $i/$fn $out/$fn.d/overrides.conf
+            '' else ''
+              echo "Found multiple derivations configuring $fn!"
+              exit 1
+            ''}
+          fi
+       else
+          ln -fs $i/$fn $out/
+        fi
+      done
+
+      # Create service aliases from aliases option.
+      ${concatStrings (mapAttrsToList (name: unit:
+          concatMapStrings (name2: ''
+            ln -sfn '${name}' $out/'${name2}'
+          '') unit.aliases) units)}
+
+      # Create .wants and .requires symlinks from the wantedBy and
+      # requiredBy options.
+      ${concatStrings (mapAttrsToList (name: unit:
+          concatMapStrings (name2: ''
+            mkdir -p $out/'${name2}.wants'
+            ln -sfn '../${name}' $out/'${name2}.wants'/
+          '') unit.wantedBy) units)}
+
+      ${concatStrings (mapAttrsToList (name: unit:
+          concatMapStrings (name2: ''
+            mkdir -p $out/'${name2}.requires'
+            ln -sfn '../${name}' $out/'${name2}.requires'/
+          '') unit.requiredBy) units)}
+
+      ${optionalString (type == "system") ''
+        # Stupid misc. symlinks.
+        ln -s ${cfg.defaultUnit} $out/default.target
+        ln -s ${cfg.ctrlAltDelUnit} $out/ctrl-alt-del.target
+        ln -s rescue.target $out/kbrequest.target
+
+        mkdir -p $out/getty.target.wants/
+        ln -s ../autovt@tty1.service $out/getty.target.wants/
+
+        ln -s ../remote-fs.target $out/multi-user.target.wants/
+      ''}
+    ''; # */
+
+  makeJobScript = name: text:
+    let
+      scriptName = replaceChars [ "\\" "@" ] [ "-" "_" ] (shellEscape name);
+      out = (pkgs.writeShellScriptBin scriptName ''
+        set -e
+        ${text}
+      '').overrideAttrs (_: {
+        # The derivation name is different from the script file name
+        # to keep the script file name short to avoid cluttering logs.
+        name = "unit-script-${scriptName}";
+      });
+    in "${out}/bin/${scriptName}";
+
+  unitConfig = { config, options, ... }: {
+    config = {
+      unitConfig =
+        optionalAttrs (config.requires != [])
+          { Requires = toString config.requires; }
+        // optionalAttrs (config.wants != [])
+          { Wants = toString config.wants; }
+        // optionalAttrs (config.after != [])
+          { After = toString config.after; }
+        // optionalAttrs (config.before != [])
+          { Before = toString config.before; }
+        // optionalAttrs (config.bindsTo != [])
+          { BindsTo = toString config.bindsTo; }
+        // optionalAttrs (config.partOf != [])
+          { PartOf = toString config.partOf; }
+        // optionalAttrs (config.conflicts != [])
+          { Conflicts = toString config.conflicts; }
+        // optionalAttrs (config.requisite != [])
+          { Requisite = toString config.requisite; }
+        // optionalAttrs (config.restartTriggers != [])
+          { X-Restart-Triggers = toString config.restartTriggers; }
+        // optionalAttrs (config.reloadTriggers != [])
+          { X-Reload-Triggers = toString config.reloadTriggers; }
+        // optionalAttrs (config.description != "") {
+          Description = config.description; }
+        // optionalAttrs (config.documentation != []) {
+          Documentation = toString config.documentation; }
+        // optionalAttrs (config.onFailure != []) {
+          OnFailure = toString config.onFailure; }
+        // optionalAttrs (options.startLimitIntervalSec.isDefined) {
+          StartLimitIntervalSec = toString config.startLimitIntervalSec;
+        } // optionalAttrs (options.startLimitBurst.isDefined) {
+          StartLimitBurst = toString config.startLimitBurst;
+        };
+    };
+  };
+
+  serviceConfig = { name, config, ... }: {
+    config = mkMerge
+      [ { # Default path for systemd services.  Should be quite minimal.
+          path = mkAfter
+            [ pkgs.coreutils
+              pkgs.findutils
+              pkgs.gnugrep
+              pkgs.gnused
+              systemd
+            ];
+          environment.PATH = "${makeBinPath config.path}:${makeSearchPathOutput "bin" "sbin" config.path}";
+        }
+        (mkIf (config.preStart != "")
+          { serviceConfig.ExecStartPre =
+              [ (makeJobScript "${name}-pre-start" config.preStart) ];
+          })
+        (mkIf (config.script != "")
+          { serviceConfig.ExecStart =
+              makeJobScript "${name}-start" config.script + " " + config.scriptArgs;
+          })
+        (mkIf (config.postStart != "")
+          { serviceConfig.ExecStartPost =
+              [ (makeJobScript "${name}-post-start" config.postStart) ];
+          })
+        (mkIf (config.reload != "")
+          { serviceConfig.ExecReload =
+              makeJobScript "${name}-reload" config.reload;
+          })
+        (mkIf (config.preStop != "")
+          { serviceConfig.ExecStop =
+              makeJobScript "${name}-pre-stop" config.preStop;
+          })
+        (mkIf (config.postStop != "")
+          { serviceConfig.ExecStopPost =
+              makeJobScript "${name}-post-stop" config.postStop;
+          })
+      ];
+  };
+
+  mountConfig = { config, ... }: {
+    config = {
+      mountConfig =
+        { What = config.what;
+          Where = config.where;
+        } // optionalAttrs (config.type != "") {
+          Type = config.type;
+        } // optionalAttrs (config.options != "") {
+          Options = config.options;
+        };
+    };
+  };
+
+  automountConfig = { config, ... }: {
+    config = {
+      automountConfig =
+        { Where = config.where;
+        };
+    };
+  };
+
+  commonUnitText = def: ''
+      [Unit]
+      ${attrsToSection def.unitConfig}
+    '';
+
+  targetToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text =
+        ''
+          [Unit]
+          ${attrsToSection def.unitConfig}
+        '';
+    };
+
+  serviceToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text = commonUnitText def +
+        ''
+          [Service]
+          ${let env = cfg.globalEnvironment // def.environment;
+            in concatMapStrings (n:
+              let s = optionalString (env.${n} != null)
+                "Environment=${builtins.toJSON "${n}=${env.${n}}"}\n";
+              # systemd max line length is now 1MiB
+              # https://github.com/systemd/systemd/commit/e6dde451a51dc5aaa7f4d98d39b8fe735f73d2af
+              in if stringLength s >= 1048576 then throw "The value of the environment variable ‘${n}’ in systemd service ‘${name}.service’ is too long." else s) (attrNames env)}
+          ${if def.reloadIfChanged then ''
+            X-ReloadIfChanged=true
+          '' else if !def.restartIfChanged then ''
+            X-RestartIfChanged=false
+          '' else ""}
+          ${optionalString (!def.stopIfChanged) "X-StopIfChanged=false"}
+          ${attrsToSection def.serviceConfig}
+        '';
+    };
+
+  socketToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text = commonUnitText def +
+        ''
+          [Socket]
+          ${attrsToSection def.socketConfig}
+          ${concatStringsSep "\n" (map (s: "ListenStream=${s}") def.listenStreams)}
+          ${concatStringsSep "\n" (map (s: "ListenDatagram=${s}") def.listenDatagrams)}
+        '';
+    };
+
+  timerToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text = commonUnitText def +
+        ''
+          [Timer]
+          ${attrsToSection def.timerConfig}
+        '';
+    };
+
+  pathToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text = commonUnitText def +
+        ''
+          [Path]
+          ${attrsToSection def.pathConfig}
+        '';
+    };
+
+  mountToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text = commonUnitText def +
+        ''
+          [Mount]
+          ${attrsToSection def.mountConfig}
+        '';
+    };
+
+  automountToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text = commonUnitText def +
+        ''
+          [Automount]
+          ${attrsToSection def.automountConfig}
+        '';
+    };
+
+  sliceToUnit = name: def:
+    { inherit (def) aliases wantedBy requiredBy enable;
+      text = commonUnitText def +
+        ''
+          [Slice]
+          ${attrsToSection def.sliceConfig}
+        '';
+    };
+}
diff --git a/nixos/lib/systemd-unit-options.nix b/nixos/lib/systemd-unit-options.nix
new file mode 100644
index 00000000000..8029ba0e3f6
--- /dev/null
+++ b/nixos/lib/systemd-unit-options.nix
@@ -0,0 +1,552 @@
+{ lib, systemdUtils }:
+
+with systemdUtils.lib;
+with lib;
+
+let
+  checkService = checkUnitConfig "Service" [
+    (assertValueOneOf "Type" [
+      "exec" "simple" "forking" "oneshot" "dbus" "notify" "idle"
+    ])
+    (assertValueOneOf "Restart" [
+      "no" "on-success" "on-failure" "on-abnormal" "on-abort" "always"
+    ])
+  ];
+
+in rec {
+
+  unitOption = mkOptionType {
+    name = "systemd option";
+    merge = loc: defs:
+      let
+        defs' = filterOverrides defs;
+        defs'' = getValues defs';
+      in
+        if isList (head defs'')
+        then concatLists defs''
+        else mergeEqualOption loc defs';
+  };
+
+  sharedOptions = {
+
+    enable = mkOption {
+      default = true;
+      type = types.bool;
+      description = ''
+        If set to false, this unit will be a symlink to
+        /dev/null. This is primarily useful to prevent specific
+        template instances
+        (e.g. <literal>serial-getty@ttyS0</literal>) from being
+        started. Note that <literal>enable=true</literal> does not
+        make a unit start by default at boot; if you want that, see
+        <literal>wantedBy</literal>.
+      '';
+    };
+
+    requiredBy = mkOption {
+      default = [];
+      type = types.listOf unitNameType;
+      description = ''
+        Units that require (i.e. depend on and need to go down with)
+        this unit. The discussion under <literal>wantedBy</literal>
+        applies here as well: inverse <literal>.requires</literal>
+        symlinks are established.
+      '';
+    };
+
+    wantedBy = mkOption {
+      default = [];
+      type = types.listOf unitNameType;
+      description = ''
+        Units that want (i.e. depend on) this unit. The standard way
+        to make a unit start by default at boot is to set this option
+        to <literal>[ "multi-user.target" ]</literal>. That's despite
+        the fact that the systemd.unit(5) manpage says this option
+        goes in the <literal>[Install]</literal> section that controls
+        the behaviour of <literal>systemctl enable</literal>. Since
+        such a process is stateful and thus contrary to the design of
+        NixOS, setting this option instead causes the equivalent
+        inverse <literal>.wants</literal> symlink to be present,
+        establishing the same desired relationship in a stateless way.
+      '';
+    };
+
+    aliases = mkOption {
+      default = [];
+      type = types.listOf unitNameType;
+      description = "Aliases of that unit.";
+    };
+
+  };
+
+  concreteUnitOptions = sharedOptions // {
+
+    text = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Text of this systemd unit.";
+    };
+
+    unit = mkOption {
+      internal = true;
+      description = "The generated unit.";
+    };
+
+  };
+
+  commonUnitOptions = sharedOptions // {
+
+    description = mkOption {
+      default = "";
+      type = types.singleLineStr;
+      description = "Description of this unit used in systemd messages and progress indicators.";
+    };
+
+    documentation = mkOption {
+      default = [];
+      type = types.listOf types.str;
+      description = "A list of URIs referencing documentation for this unit or its configuration.";
+    };
+
+    requires = mkOption {
+      default = [];
+      type = types.listOf unitNameType;
+      description = ''
+        Start the specified units when this unit is started, and stop
+        this unit when the specified units are stopped or fail.
+      '';
+    };
+
+    wants = mkOption {
+      default = [];
+      type = types.listOf unitNameType;
+      description = ''
+        Start the specified units when this unit is started.
+      '';
+    };
+
+    after = mkOption {
+      default = [];
+      type = types.listOf unitNameType;
+      description = ''
+        If the specified units are started at the same time as
+        this unit, delay this unit until they have started.
+      '';
+    };
+
+    before = mkOption {
+      default = [];
+      type = types.listOf unitNameType;
+      description = ''
+        If the specified units are started at the same time as
+        this unit, delay them until this unit has started.
+      '';
+    };
+
+    bindsTo = mkOption {
+      default = [];
+      type = types.listOf unitNameType;
+      description = ''
+        Like ‘requires’, but in addition, if the specified units
+        unexpectedly disappear, this unit will be stopped as well.
+      '';
+    };
+
+    partOf = mkOption {
+      default = [];
+      type = types.listOf unitNameType;
+      description = ''
+        If the specified units are stopped or restarted, then this
+        unit is stopped or restarted as well.
+      '';
+    };
+
+    conflicts = mkOption {
+      default = [];
+      type = types.listOf unitNameType;
+      description = ''
+        If the specified units are started, then this unit is stopped
+        and vice versa.
+      '';
+    };
+
+    requisite = mkOption {
+      default = [];
+      type = types.listOf unitNameType;
+      description = ''
+        Similar to requires. However if the units listed are not started,
+        they will not be started and the transaction will fail.
+      '';
+    };
+
+    unitConfig = mkOption {
+      default = {};
+      example = { RequiresMountsFor = "/data"; };
+      type = types.attrsOf unitOption;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Unit]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.unit</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    restartTriggers = mkOption {
+      default = [];
+      type = types.listOf types.unspecified;
+      description = ''
+        An arbitrary list of items such as derivations.  If any item
+        in the list changes between reconfigurations, the service will
+        be restarted.
+      '';
+    };
+
+    reloadTriggers = mkOption {
+      default = [];
+      type = types.listOf unitOption;
+      description = ''
+        An arbitrary list of items such as derivations.  If any item
+        in the list changes between reconfigurations, the service will
+        be reloaded.  If anything but a reload trigger changes in the
+        unit file, the unit will be restarted instead.
+      '';
+    };
+
+    onFailure = mkOption {
+      default = [];
+      type = types.listOf unitNameType;
+      description = ''
+        A list of one or more units that are activated when
+        this unit enters the "failed" state.
+      '';
+    };
+
+    startLimitBurst = mkOption {
+       type = types.int;
+       description = ''
+         Configure unit start rate limiting. Units which are started
+         more than startLimitBurst times within an interval time
+         interval are not permitted to start any more.
+       '';
+    };
+
+    startLimitIntervalSec = mkOption {
+       type = types.int;
+       description = ''
+         Configure unit start rate limiting. Units which are started
+         more than startLimitBurst times within an interval time
+         interval are not permitted to start any more.
+       '';
+    };
+
+  };
+
+
+  serviceOptions = commonUnitOptions // {
+
+    environment = mkOption {
+      default = {};
+      type = with types; attrsOf (nullOr (oneOf [ str path package ]));
+      example = { PATH = "/foo/bar/bin"; LANG = "nl_NL.UTF-8"; };
+      description = "Environment variables passed to the service's processes.";
+    };
+
+    path = mkOption {
+      default = [];
+      type = with types; listOf (oneOf [ package str ]);
+      description = ''
+        Packages added to the service's <envar>PATH</envar>
+        environment variable.  Both the <filename>bin</filename>
+        and <filename>sbin</filename> subdirectories of each
+        package are added.
+      '';
+    };
+
+    serviceConfig = mkOption {
+      default = {};
+      example =
+        { RestartSec = 5;
+        };
+      type = types.addCheck (types.attrsOf unitOption) checkService;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Service]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.service</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    script = mkOption {
+      type = types.lines;
+      default = "";
+      description = "Shell commands executed as the service's main process.";
+    };
+
+    scriptArgs = mkOption {
+      type = types.str;
+      default = "";
+      description = "Arguments passed to the main process script.";
+    };
+
+    preStart = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Shell commands executed before the service's main process
+        is started.
+      '';
+    };
+
+    postStart = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Shell commands executed after the service's main process
+        is started.
+      '';
+    };
+
+    reload = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Shell commands executed when the service's main process
+        is reloaded.
+      '';
+    };
+
+    preStop = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Shell commands executed to stop the service.
+      '';
+    };
+
+    postStop = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Shell commands executed after the service's main process
+        has exited.
+      '';
+    };
+
+    restartIfChanged = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether the service should be restarted during a NixOS
+        configuration switch if its definition has changed.
+      '';
+    };
+
+    reloadIfChanged = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether the service should be reloaded during a NixOS
+        configuration switch if its definition has changed.  If
+        enabled, the value of <option>restartIfChanged</option> is
+        ignored.
+
+        This option should not be used anymore in favor of
+        <option>reloadTriggers</option> which allows more granular
+        control of when a service is reloaded and when a service
+        is restarted.
+      '';
+    };
+
+    stopIfChanged = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        If set, a changed unit is restarted by calling
+        <command>systemctl stop</command> in the old configuration,
+        then <command>systemctl start</command> in the new one.
+        Otherwise, it is restarted in a single step using
+        <command>systemctl restart</command> in the new configuration.
+        The latter is less correct because it runs the
+        <literal>ExecStop</literal> commands from the new
+        configuration.
+      '';
+    };
+
+    startAt = mkOption {
+      type = with types; either str (listOf str);
+      default = [];
+      example = "Sun 14:00:00";
+      description = ''
+        Automatically start this unit at the given date/time, which
+        must be in the format described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.  This is equivalent
+        to adding a corresponding timer unit with
+        <option>OnCalendar</option> set to the value given here.
+      '';
+      apply = v: if isList v then v else [ v ];
+    };
+
+  };
+
+
+  socketOptions = commonUnitOptions // {
+
+    listenStreams = mkOption {
+      default = [];
+      type = types.listOf types.str;
+      example = [ "0.0.0.0:993" "/run/my-socket" ];
+      description = ''
+        For each item in this list, a <literal>ListenStream</literal>
+        option in the <literal>[Socket]</literal> section will be created.
+      '';
+    };
+
+    listenDatagrams = mkOption {
+      default = [];
+      type = types.listOf types.str;
+      example = [ "0.0.0.0:993" "/run/my-socket" ];
+      description = ''
+        For each item in this list, a <literal>ListenDatagram</literal>
+        option in the <literal>[Socket]</literal> section will be created.
+      '';
+    };
+
+    socketConfig = mkOption {
+      default = {};
+      example = { ListenStream = "/run/my-socket"; };
+      type = types.attrsOf unitOption;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Socket]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.socket</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+  };
+
+
+  timerOptions = commonUnitOptions // {
+
+    timerConfig = mkOption {
+      default = {};
+      example = { OnCalendar = "Sun 14:00:00"; Unit = "foo.service"; };
+      type = types.attrsOf unitOption;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Timer]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.timer</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> and
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry> for details.
+      '';
+    };
+
+  };
+
+
+  pathOptions = commonUnitOptions // {
+
+    pathConfig = mkOption {
+      default = {};
+      example = { PathChanged = "/some/path"; Unit = "changedpath.service"; };
+      type = types.attrsOf unitOption;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Path]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.path</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+  };
+
+
+  mountOptions = commonUnitOptions // {
+
+    what = mkOption {
+      example = "/dev/sda1";
+      type = types.str;
+      description = "Absolute path of device node, file or other resource. (Mandatory)";
+    };
+
+    where = mkOption {
+      example = "/mnt";
+      type = types.str;
+      description = ''
+        Absolute path of a directory of the mount point.
+        Will be created if it doesn't exist. (Mandatory)
+      '';
+    };
+
+    type = mkOption {
+      default = "";
+      example = "ext4";
+      type = types.str;
+      description = "File system type.";
+    };
+
+    options = mkOption {
+      default = "";
+      example = "noatime";
+      type = types.commas;
+      description = "Options used to mount the file system.";
+    };
+
+    mountConfig = mkOption {
+      default = {};
+      example = { DirectoryMode = "0775"; };
+      type = types.attrsOf unitOption;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Mount]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.mount</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+  };
+
+  automountOptions = commonUnitOptions // {
+
+    where = mkOption {
+      example = "/mnt";
+      type = types.str;
+      description = ''
+        Absolute path of a directory of the mount point.
+        Will be created if it doesn't exist. (Mandatory)
+      '';
+    };
+
+    automountConfig = mkOption {
+      default = {};
+      example = { DirectoryMode = "0775"; };
+      type = types.attrsOf unitOption;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Automount]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.automount</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+  };
+
+  targetOptions = commonUnitOptions;
+
+  sliceOptions = commonUnitOptions // {
+
+    sliceConfig = mkOption {
+      default = {};
+      example = { MemoryMax = "2G"; };
+      type = types.attrsOf unitOption;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Slice]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.slice</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+  };
+
+}
diff --git a/nixos/lib/test-driver/default.nix b/nixos/lib/test-driver/default.nix
new file mode 100644
index 00000000000..3aee9134318
--- /dev/null
+++ b/nixos/lib/test-driver/default.nix
@@ -0,0 +1,32 @@
+{ lib
+, python3Packages
+, enableOCR ? false
+, qemu_pkg ? qemu_test
+, coreutils
+, imagemagick_light
+, libtiff
+, netpbm
+, qemu_test
+, socat
+, tesseract4
+, vde2
+}:
+
+python3Packages.buildPythonApplication rec {
+  pname = "nixos-test-driver";
+  version = "1.1";
+  src = ./.;
+
+  propagatedBuildInputs = [ coreutils netpbm python3Packages.colorama python3Packages.ptpython qemu_pkg socat vde2 ]
+    ++ (lib.optionals enableOCR [ imagemagick_light tesseract4 ]);
+
+  doCheck = true;
+  checkInputs = with python3Packages; [ mypy pylint black ];
+  checkPhase = ''
+    mypy --disallow-untyped-defs \
+          --no-implicit-optional \
+          --ignore-missing-imports ${src}/test_driver
+    pylint --errors-only --enable=unused-import ${src}/test_driver
+    black --check --diff ${src}/test_driver
+  '';
+}
diff --git a/nixos/lib/test-driver/setup.py b/nixos/lib/test-driver/setup.py
new file mode 100644
index 00000000000..476c7b2dab2
--- /dev/null
+++ b/nixos/lib/test-driver/setup.py
@@ -0,0 +1,13 @@
+from setuptools import setup, find_packages
+
+setup(
+  name="nixos-test-driver",
+  version='1.1',
+  packages=find_packages(),
+  entry_points={
+    "console_scripts": [
+      "nixos-test-driver=test_driver:main",
+      "generate-driver-symbols=test_driver:generate_driver_symbols"
+    ]
+  },
+)
diff --git a/nixos/lib/test-driver/test_driver/__init__.py b/nixos/lib/test-driver/test_driver/__init__.py
new file mode 100755
index 00000000000..61d91c9ed65
--- /dev/null
+++ b/nixos/lib/test-driver/test_driver/__init__.py
@@ -0,0 +1,128 @@
+from pathlib import Path
+import argparse
+import ptpython.repl
+import os
+import time
+
+from test_driver.logger import rootlog
+from test_driver.driver import Driver
+
+
+class EnvDefault(argparse.Action):
+    """An argpars Action that takes values from the specified
+    environment variable as the flags default value.
+    """
+
+    def __init__(self, envvar, required=False, default=None, nargs=None, **kwargs):  # type: ignore
+        if not default and envvar:
+            if envvar in os.environ:
+                if nargs is not None and (nargs.isdigit() or nargs in ["*", "+"]):
+                    default = os.environ[envvar].split()
+                else:
+                    default = os.environ[envvar]
+                kwargs["help"] = (
+                    kwargs["help"] + f" (default from environment: {default})"
+                )
+        if required and default:
+            required = False
+        super(EnvDefault, self).__init__(
+            default=default, required=required, nargs=nargs, **kwargs
+        )
+
+    def __call__(self, parser, namespace, values, option_string=None):  # type: ignore
+        setattr(namespace, self.dest, values)
+
+
+def writeable_dir(arg: str) -> Path:
+    """Raises an ArgumentTypeError if the given argument isn't a writeable directory
+    Note: We want to fail as early as possible if a directory isn't writeable,
+    since an executed nixos-test could fail (very late) because of the test-driver
+    writing in a directory without proper permissions.
+    """
+    path = Path(arg)
+    if not path.is_dir():
+        raise argparse.ArgumentTypeError("{0} is not a directory".format(path))
+    if not os.access(path, os.W_OK):
+        raise argparse.ArgumentTypeError(
+            "{0} is not a writeable directory".format(path)
+        )
+    return path
+
+
+def main() -> None:
+    arg_parser = argparse.ArgumentParser(prog="nixos-test-driver")
+    arg_parser.add_argument(
+        "-K",
+        "--keep-vm-state",
+        help="re-use a VM state coming from a previous run",
+        action="store_true",
+    )
+    arg_parser.add_argument(
+        "-I",
+        "--interactive",
+        help="drop into a python repl and run the tests interactively",
+        action=argparse.BooleanOptionalAction,
+    )
+    arg_parser.add_argument(
+        "--start-scripts",
+        metavar="START-SCRIPT",
+        action=EnvDefault,
+        envvar="startScripts",
+        nargs="*",
+        help="start scripts for participating virtual machines",
+    )
+    arg_parser.add_argument(
+        "--vlans",
+        metavar="VLAN",
+        action=EnvDefault,
+        envvar="vlans",
+        nargs="*",
+        help="vlans to span by the driver",
+    )
+    arg_parser.add_argument(
+        "-o",
+        "--output_directory",
+        help="""The path to the directory where outputs copied from the VM will be placed.
+                By e.g. Machine.copy_from_vm or Machine.screenshot""",
+        default=Path.cwd(),
+        type=writeable_dir,
+    )
+    arg_parser.add_argument(
+        "testscript",
+        action=EnvDefault,
+        envvar="testScript",
+        help="the test script to run",
+        type=Path,
+    )
+
+    args = arg_parser.parse_args()
+
+    if not args.keep_vm_state:
+        rootlog.info("Machine state will be reset. To keep it, pass --keep-vm-state")
+
+    with Driver(
+        args.start_scripts,
+        args.vlans,
+        args.testscript.read_text(),
+        args.output_directory.resolve(),
+        args.keep_vm_state,
+    ) as driver:
+        if args.interactive:
+            ptpython.repl.embed(driver.test_symbols(), {})
+        else:
+            tic = time.time()
+            driver.run_tests()
+            toc = time.time()
+            rootlog.info(f"test script finished in {(toc-tic):.2f}s")
+
+
+def generate_driver_symbols() -> None:
+    """
+    This generates a file with symbols of the test-driver code that can be used
+    in user's test scripts. That list is then used by pyflakes to lint those
+    scripts.
+    """
+    d = Driver([], [], "", Path())
+    test_symbols = d.test_symbols()
+    with open("driver-symbols", "w") as fp:
+        fp.write(",".join(test_symbols.keys()))
diff --git a/nixos/lib/test-driver/test_driver/driver.py b/nixos/lib/test-driver/test_driver/driver.py
new file mode 100644
index 00000000000..880b1c5fdec
--- /dev/null
+++ b/nixos/lib/test-driver/test_driver/driver.py
@@ -0,0 +1,225 @@
+from contextlib import contextmanager
+from pathlib import Path
+from typing import Any, Dict, Iterator, List, Union, Optional, Callable, ContextManager
+import os
+import tempfile
+
+from test_driver.logger import rootlog
+from test_driver.machine import Machine, NixStartScript, retry
+from test_driver.vlan import VLan
+from test_driver.polling_condition import PollingCondition
+
+
+def get_tmp_dir() -> Path:
+    """Returns a temporary directory that is defined by TMPDIR, TEMP, TMP or CWD
+    Raises an exception in case the retrieved temporary directory is not writeable
+    See https://docs.python.org/3/library/tempfile.html#tempfile.gettempdir
+    """
+    tmp_dir = Path(tempfile.gettempdir())
+    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
+            )
+        )
+    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
+            )
+        )
+    return tmp_dir
+
+
+class Driver:
+    """A handle to the driver that sets up the environment
+    and runs the tests"""
+
+    tests: str
+    vlans: List[VLan]
+    machines: List[Machine]
+    polling_conditions: List[PollingCondition]
+
+    def __init__(
+        self,
+        start_scripts: List[str],
+        vlans: List[int],
+        tests: str,
+        out_dir: Path,
+        keep_vm_state: bool = False,
+    ):
+        self.tests = tests
+        self.out_dir = out_dir
+
+        tmp_dir = get_tmp_dir()
+
+        with rootlog.nested("start all VLans"):
+            self.vlans = [VLan(nr, tmp_dir) for nr in vlans]
+
+        def cmd(scripts: List[str]) -> Iterator[NixStartScript]:
+            for s in scripts:
+                yield NixStartScript(s)
+
+        self.polling_conditions = []
+
+        self.machines = [
+            Machine(
+                start_command=cmd,
+                keep_vm_state=keep_vm_state,
+                name=cmd.machine_name,
+                tmp_dir=tmp_dir,
+                callbacks=[self.check_polling_conditions],
+                out_dir=self.out_dir,
+            )
+            for cmd in cmd(start_scripts)
+        ]
+
+    def __enter__(self) -> "Driver":
+        return self
+
+    def __exit__(self, *_: Any) -> None:
+        with rootlog.nested("cleanup"):
+            for machine in self.machines:
+                machine.release()
+
+    def subtest(self, name: str) -> Iterator[None]:
+        """Group logs under a given test name"""
+        with rootlog.nested(name):
+            try:
+                yield
+                return True
+            except Exception as e:
+                rootlog.error(f'Test "{name}" failed with error: "{e}"')
+                raise e
+
+    def test_symbols(self) -> Dict[str, Any]:
+        @contextmanager
+        def subtest(name: str) -> Iterator[None]:
+            return self.subtest(name)
+
+        general_symbols = dict(
+            start_all=self.start_all,
+            test_script=self.test_script,
+            machines=self.machines,
+            vlans=self.vlans,
+            driver=self,
+            log=rootlog,
+            os=os,
+            create_machine=self.create_machine,
+            subtest=subtest,
+            run_tests=self.run_tests,
+            join_all=self.join_all,
+            retry=retry,
+            serial_stdout_off=self.serial_stdout_off,
+            serial_stdout_on=self.serial_stdout_on,
+            polling_condition=self.polling_condition,
+            Machine=Machine,  # for typing
+        )
+        machine_symbols = {m.name: m for m in self.machines}
+        # If there's exactly one machine, make it available under the name
+        # "machine", even if it's not called that.
+        if len(self.machines) == 1:
+            (machine_symbols["machine"],) = self.machines
+        vlan_symbols = {
+            f"vlan{v.nr}": self.vlans[idx] for idx, v in enumerate(self.vlans)
+        }
+        print(
+            "additionally exposed symbols:\n    "
+            + ", ".join(map(lambda m: m.name, self.machines))
+            + ",\n    "
+            + ", ".join(map(lambda v: f"vlan{v.nr}", self.vlans))
+            + ",\n    "
+            + ", ".join(list(general_symbols.keys()))
+        )
+        return {**general_symbols, **machine_symbols, **vlan_symbols}
+
+    def test_script(self) -> None:
+        """Run the test script"""
+        with rootlog.nested("run the VM test script"):
+            symbols = self.test_symbols()  # call eagerly
+            exec(self.tests, symbols, None)
+
+    def run_tests(self) -> None:
+        """Run the test script (for non-interactive test runs)"""
+        self.test_script()
+        # TODO: Collect coverage data
+        for machine in self.machines:
+            if machine.is_up():
+                machine.execute("sync")
+
+    def start_all(self) -> None:
+        """Start all machines"""
+        with rootlog.nested("start all VMs"):
+            for machine in self.machines:
+                machine.start()
+
+    def join_all(self) -> None:
+        """Wait for all machines to shut down"""
+        with rootlog.nested("wait for all VMs to finish"):
+            for machine in self.machines:
+                machine.wait_for_shutdown()
+
+    def create_machine(self, args: Dict[str, Any]) -> Machine:
+        rootlog.warning(
+            "Using legacy create_machine(), please instantiate the"
+            "Machine class directly, instead"
+        )
+
+        tmp_dir = get_tmp_dir()
+
+        if args.get("startCommand"):
+            start_command: str = args.get("startCommand", "")
+            cmd = NixStartScript(start_command)
+            name = args.get("name", cmd.machine_name)
+        else:
+            cmd = Machine.create_startcommand(args)  # type: ignore
+            name = args.get("name", "machine")
+
+        return Machine(
+            tmp_dir=tmp_dir,
+            out_dir=self.out_dir,
+            start_command=cmd,
+            name=name,
+            keep_vm_state=args.get("keep_vm_state", False),
+            allow_reboot=args.get("allow_reboot", False),
+        )
+
+    def serial_stdout_on(self) -> None:
+        rootlog._print_serial_logs = True
+
+    def serial_stdout_off(self) -> None:
+        rootlog._print_serial_logs = False
+
+    def check_polling_conditions(self) -> None:
+        for condition in self.polling_conditions:
+            condition.maybe_raise()
+
+    def polling_condition(
+        self,
+        fun_: Optional[Callable] = None,
+        *,
+        seconds_interval: float = 2.0,
+        description: Optional[str] = None,
+    ) -> Union[Callable[[Callable], ContextManager], ContextManager]:
+        driver = self
+
+        class Poll:
+            def __init__(self, fun: Callable):
+                self.condition = PollingCondition(
+                    fun,
+                    seconds_interval,
+                    description,
+                )
+
+            def __enter__(self) -> None:
+                driver.polling_conditions.append(self.condition)
+
+            def __exit__(self, a, b, c) -> None:  # type: ignore
+                res = driver.polling_conditions.pop()
+                assert res is self.condition
+
+        if fun_ is None:
+            return Poll
+        else:
+            return Poll(fun_)
diff --git a/nixos/lib/test-driver/test_driver/logger.py b/nixos/lib/test-driver/test_driver/logger.py
new file mode 100644
index 00000000000..5b3091a5129
--- /dev/null
+++ b/nixos/lib/test-driver/test_driver/logger.py
@@ -0,0 +1,101 @@
+from colorama import Style
+from contextlib import contextmanager
+from typing import Any, Dict, Iterator
+from queue import Queue, Empty
+from xml.sax.saxutils import XMLGenerator
+import codecs
+import os
+import sys
+import time
+import unicodedata
+
+
+class Logger:
+    def __init__(self) -> None:
+        self.logfile = os.environ.get("LOGFILE", "/dev/null")
+        self.logfile_handle = codecs.open(self.logfile, "wb")
+        self.xml = XMLGenerator(self.logfile_handle, encoding="utf-8")
+        self.queue: "Queue[Dict[str, str]]" = Queue()
+
+        self.xml.startDocument()
+        self.xml.startElement("logfile", attrs={})
+
+        self._print_serial_logs = True
+
+    @staticmethod
+    def _eprint(*args: object, **kwargs: Any) -> None:
+        print(*args, file=sys.stderr, **kwargs)
+
+    def close(self) -> None:
+        self.xml.endElement("logfile")
+        self.xml.endDocument()
+        self.logfile_handle.close()
+
+    def sanitise(self, message: str) -> str:
+        return "".join(ch for ch in message if unicodedata.category(ch)[0] != "C")
+
+    def maybe_prefix(self, message: str, attributes: Dict[str, str]) -> str:
+        if "machine" in attributes:
+            return "{}: {}".format(attributes["machine"], message)
+        return message
+
+    def log_line(self, message: str, attributes: Dict[str, str]) -> None:
+        self.xml.startElement("line", attributes)
+        self.xml.characters(message)
+        self.xml.endElement("line")
+
+    def info(self, *args, **kwargs) -> None:  # type: ignore
+        self.log(*args, **kwargs)
+
+    def warning(self, *args, **kwargs) -> None:  # type: ignore
+        self.log(*args, **kwargs)
+
+    def error(self, *args, **kwargs) -> None:  # type: ignore
+        self.log(*args, **kwargs)
+        sys.exit(1)
+
+    def log(self, message: str, attributes: Dict[str, str] = {}) -> None:
+        self._eprint(self.maybe_prefix(message, attributes))
+        self.drain_log_queue()
+        self.log_line(message, attributes)
+
+    def log_serial(self, message: str, machine: str) -> None:
+        self.enqueue({"msg": message, "machine": machine, "type": "serial"})
+        if self._print_serial_logs:
+            self._eprint(
+                Style.DIM + "{} # {}".format(machine, message) + Style.RESET_ALL
+            )
+
+    def enqueue(self, item: Dict[str, str]) -> None:
+        self.queue.put(item)
+
+    def drain_log_queue(self) -> None:
+        try:
+            while True:
+                item = self.queue.get_nowait()
+                msg = self.sanitise(item["msg"])
+                del item["msg"]
+                self.log_line(msg, item)
+        except Empty:
+            pass
+
+    @contextmanager
+    def nested(self, message: str, attributes: Dict[str, str] = {}) -> Iterator[None]:
+        self._eprint(self.maybe_prefix(message, attributes))
+
+        self.xml.startElement("nest", attrs={})
+        self.xml.startElement("head", attributes)
+        self.xml.characters(message)
+        self.xml.endElement("head")
+
+        tic = time.time()
+        self.drain_log_queue()
+        yield
+        self.drain_log_queue()
+        toc = time.time()
+        self.log("(finished: {}, in {:.2f} seconds)".format(message, toc - tic))
+
+        self.xml.endElement("nest")
+
+
+rootlog = Logger()
diff --git a/nixos/lib/test-driver/test_driver/machine.py b/nixos/lib/test-driver/test_driver/machine.py
new file mode 100644
index 00000000000..569a0f3c61e
--- /dev/null
+++ b/nixos/lib/test-driver/test_driver/machine.py
@@ -0,0 +1,988 @@
+from contextlib import _GeneratorContextManager
+from pathlib import Path
+from queue import Queue
+from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
+import base64
+import io
+import os
+import queue
+import re
+import shlex
+import shutil
+import socket
+import subprocess
+import sys
+import tempfile
+import threading
+import time
+
+from test_driver.logger import rootlog
+
+CHAR_TO_KEY = {
+    "A": "shift-a",
+    "N": "shift-n",
+    "-": "0x0C",
+    "_": "shift-0x0C",
+    "B": "shift-b",
+    "O": "shift-o",
+    "=": "0x0D",
+    "+": "shift-0x0D",
+    "C": "shift-c",
+    "P": "shift-p",
+    "[": "0x1A",
+    "{": "shift-0x1A",
+    "D": "shift-d",
+    "Q": "shift-q",
+    "]": "0x1B",
+    "}": "shift-0x1B",
+    "E": "shift-e",
+    "R": "shift-r",
+    ";": "0x27",
+    ":": "shift-0x27",
+    "F": "shift-f",
+    "S": "shift-s",
+    "'": "0x28",
+    '"': "shift-0x28",
+    "G": "shift-g",
+    "T": "shift-t",
+    "`": "0x29",
+    "~": "shift-0x29",
+    "H": "shift-h",
+    "U": "shift-u",
+    "\\": "0x2B",
+    "|": "shift-0x2B",
+    "I": "shift-i",
+    "V": "shift-v",
+    ",": "0x33",
+    "<": "shift-0x33",
+    "J": "shift-j",
+    "W": "shift-w",
+    ".": "0x34",
+    ">": "shift-0x34",
+    "K": "shift-k",
+    "X": "shift-x",
+    "/": "0x35",
+    "?": "shift-0x35",
+    "L": "shift-l",
+    "Y": "shift-y",
+    " ": "spc",
+    "M": "shift-m",
+    "Z": "shift-z",
+    "\n": "ret",
+    "!": "shift-0x02",
+    "@": "shift-0x03",
+    "#": "shift-0x04",
+    "$": "shift-0x05",
+    "%": "shift-0x06",
+    "^": "shift-0x07",
+    "&": "shift-0x08",
+    "*": "shift-0x09",
+    "(": "shift-0x0A",
+    ")": "shift-0x0B",
+}
+
+
+def make_command(args: list) -> str:
+    return " ".join(map(shlex.quote, (map(str, args))))
+
+
+def _perform_ocr_on_screenshot(
+    screenshot_path: str, model_ids: Iterable[int]
+) -> List[str]:
+    if shutil.which("tesseract") is None:
+        raise Exception("OCR requested but enableOCR is false")
+
+    magick_args = (
+        "-filter Catrom -density 72 -resample 300 "
+        + "-contrast -normalize -despeckle -type grayscale "
+        + "-sharpen 1 -posterize 3 -negate -gamma 100 "
+        + "-blur 1x65535"
+    )
+
+    tess_args = f"-c debug_file=/dev/null --psm 11"
+
+    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}"
+        ret = subprocess.run(cmd, shell=True, capture_output=True)
+        if ret.returncode != 0:
+            raise Exception(f"OCR failed with exit code {ret.returncode}")
+        model_results.append(ret.stdout.decode("utf-8"))
+
+    return model_results
+
+
+def retry(fn: Callable, timeout: int = 900) -> None:
+    """Call the given function repeatedly, with 1 second intervals,
+    until it returns True or a timeout is reached.
+    """
+
+    for _ in range(timeout):
+        if fn(False):
+            return
+        time.sleep(1)
+
+    if not fn(True):
+        raise Exception(f"action timed out after {timeout} seconds")
+
+
+class StartCommand:
+    """The Base Start Command knows how to append the necesary
+    runtime qemu options as determined by a particular test driver
+    run. Any such start command is expected to happily receive and
+    append additional qemu args.
+    """
+
+    _cmd: str
+
+    def cmd(
+        self,
+        monitor_socket_path: Path,
+        shell_socket_path: Path,
+        allow_reboot: bool = False,  # TODO: unused, legacy?
+    ) -> str:
+        display_opts = ""
+        display_available = any(x in os.environ for x in ["DISPLAY", "WAYLAND_DISPLAY"])
+        if not display_available:
+            display_opts += " -nographic"
+
+        # qemu options
+        qemu_opts = ""
+        qemu_opts += (
+            ""
+            if allow_reboot
+            else " -no-reboot"
+            " -device virtio-serial"
+            " -device virtconsole,chardev=shell"
+            " -device virtio-rng-pci"
+            " -serial stdio"
+        )
+        # TODO: qemu script already catpures this env variable, legacy?
+        qemu_opts += " " + os.environ.get("QEMU_OPTS", "")
+
+        return (
+            f"{self._cmd}"
+            f" -monitor unix:{monitor_socket_path}"
+            f" -chardev socket,id=shell,path={shell_socket_path}"
+            f"{qemu_opts}"
+            f"{display_opts}"
+        )
+
+    @staticmethod
+    def build_environment(
+        state_dir: Path,
+        shared_dir: Path,
+    ) -> dict:
+        # We make a copy to not update the current environment
+        env = dict(os.environ)
+        env.update(
+            {
+                "TMPDIR": str(state_dir),
+                "SHARED_DIR": str(shared_dir),
+                "USE_TMPDIR": "1",
+            }
+        )
+        return env
+
+    def run(
+        self,
+        state_dir: Path,
+        shared_dir: Path,
+        monitor_socket_path: Path,
+        shell_socket_path: Path,
+    ) -> subprocess.Popen:
+        return subprocess.Popen(
+            self.cmd(monitor_socket_path, shell_socket_path),
+            stdin=subprocess.DEVNULL,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            shell=True,
+            cwd=state_dir,
+            env=self.build_environment(state_dir, shared_dir),
+        )
+
+
+class NixStartScript(StartCommand):
+    """A start script from nixos/modules/virtualiation/qemu-vm.nix
+    that also satisfies the requirement of the BaseStartCommand.
+    These Nix commands have the particular charactersitic that the
+    machine name can be extracted out of them via a regex match.
+    (Admittedly a _very_ implicit contract, evtl. TODO fix)
+    """
+
+    def __init__(self, script: str):
+        self._cmd = script
+
+    @property
+    def machine_name(self) -> str:
+        match = re.search("run-(.+)-vm$", self._cmd)
+        name = "machine"
+        if match:
+            name = match.group(1)
+        return name
+
+
+class LegacyStartCommand(StartCommand):
+    """Used in some places to create an ad-hoc machine instead of
+    using nix test instrumentation + module system for that purpose.
+    Legacy.
+    """
+
+    def __init__(
+        self,
+        netBackendArgs: Optional[str] = None,
+        netFrontendArgs: Optional[str] = None,
+        hda: Optional[Tuple[Path, str]] = None,
+        cdrom: Optional[str] = None,
+        usb: Optional[str] = None,
+        bios: Optional[str] = None,
+        qemuBinary: Optional[str] = None,
+        qemuFlags: Optional[str] = None,
+    ):
+        if qemuBinary is not None:
+            self._cmd = qemuBinary
+        else:
+            self._cmd = "qemu-kvm"
+
+        self._cmd += " -m 384"
+
+        # networking
+        net_backend = "-netdev user,id=net0"
+        net_frontend = "-device virtio-net-pci,netdev=net0"
+        if netBackendArgs is not None:
+            net_backend += "," + netBackendArgs
+        if netFrontendArgs is not None:
+            net_frontend += "," + netFrontendArgs
+        self._cmd += f" {net_backend} {net_frontend}"
+
+        # hda
+        hda_cmd = ""
+        if hda is not None:
+            hda_path = hda[0].resolve()
+            hda_interface = hda[1]
+            if hda_interface == "scsi":
+                hda_cmd += (
+                    f" -drive id=hda,file={hda_path},werror=report,if=none"
+                    " -device scsi-hd,drive=hda"
+                )
+            else:
+                hda_cmd += f" -drive file={hda_path},if={hda_interface},werror=report"
+        self._cmd += hda_cmd
+
+        # cdrom
+        if cdrom is not None:
+            self._cmd += f" -cdrom {cdrom}"
+
+        # usb
+        usb_cmd = ""
+        if usb is not None:
+            # https://github.com/qemu/qemu/blob/master/docs/usb2.txt
+            usb_cmd += (
+                " -device usb-ehci"
+                f" -drive id=usbdisk,file={usb},if=none,readonly"
+                " -device usb-storage,drive=usbdisk "
+            )
+        self._cmd += usb_cmd
+
+        # bios
+        if bios is not None:
+            self._cmd += f" -bios {bios}"
+
+        # qemu flags
+        if qemuFlags is not None:
+            self._cmd += f" {qemuFlags}"
+
+
+class Machine:
+    """A handle to the machine with this name, that also knows how to manage
+    the machine lifecycle with the help of a start script / command."""
+
+    name: str
+    out_dir: Path
+    tmp_dir: Path
+    shared_dir: Path
+    state_dir: Path
+    monitor_path: Path
+    shell_path: Path
+
+    start_command: StartCommand
+    keep_vm_state: bool
+    allow_reboot: bool
+
+    process: Optional[subprocess.Popen]
+    pid: Optional[int]
+    monitor: Optional[socket.socket]
+    shell: Optional[socket.socket]
+    serial_thread: Optional[threading.Thread]
+
+    booted: bool
+    connected: bool
+    # Store last serial console lines for use
+    # of wait_for_console_text
+    last_lines: Queue = Queue()
+    callbacks: List[Callable]
+
+    def __repr__(self) -> str:
+        return f"<Machine '{self.name}'>"
+
+    def __init__(
+        self,
+        out_dir: Path,
+        tmp_dir: Path,
+        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 []
+
+        # set up directories
+        self.shared_dir = self.tmp_dir / "shared-xchg"
+        self.shared_dir.mkdir(mode=0o700, exist_ok=True)
+
+        self.state_dir = self.tmp_dir / f"vm-state-{self.name}"
+        self.monitor_path = self.state_dir / "monitor"
+        self.shell_path = self.state_dir / "shell"
+        if (not self.keep_vm_state) and self.state_dir.exists():
+            self.cleanup_statedir()
+        self.state_dir.mkdir(mode=0o700, exist_ok=True)
+
+        self.process = None
+        self.pid = None
+        self.monitor = None
+        self.shell = None
+        self.serial_thread = None
+
+        self.booted = False
+        self.connected = False
+
+    @staticmethod
+    def create_startcommand(args: Dict[str, str]) -> StartCommand:
+        rootlog.warning(
+            "Using legacy create_startcommand(),"
+            "please use proper nix test vm instrumentation, instead"
+            "to generate the appropriate nixos test vm qemu startup script"
+        )
+        hda = None
+        if args.get("hda"):
+            hda_arg: str = args.get("hda", "")
+            hda_arg_path: Path = Path(hda_arg)
+            hda = (hda_arg_path, args.get("hdaInterface", ""))
+        return LegacyStartCommand(
+            netBackendArgs=args.get("netBackendArgs"),
+            netFrontendArgs=args.get("netFrontendArgs"),
+            hda=hda,
+            cdrom=args.get("cdrom"),
+            usb=args.get("usb"),
+            bios=args.get("bios"),
+            qemuBinary=args.get("qemuBinary"),
+            qemuFlags=args.get("qemuFlags"),
+        )
+
+    def is_up(self) -> bool:
+        return self.booted and self.connected
+
+    def log(self, msg: str) -> None:
+        rootlog.log(msg, {"machine": self.name})
+
+    def log_serial(self, msg: str) -> None:
+        rootlog.log_serial(msg, self.name)
+
+    def nested(self, msg: str, attrs: Dict[str, str] = {}) -> _GeneratorContextManager:
+        my_attrs = {"machine": self.name}
+        my_attrs.update(attrs)
+        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
+
+    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()
+
+    def wait_for_unit(self, unit: str, user: Optional[str] = None) -> None:
+        """Wait for a systemd unit to get into "active" state.
+        Throws exceptions on "failed" and "inactive" states as well as
+        after timing out.
+        """
+
+        def check_active(_: Any) -> bool:
+            info = self.get_unit_info(unit, user)
+            state = info["ActiveState"]
+            if state == "failed":
+                raise Exception('unit "{}" reached state "{}"'.format(unit, state))
+
+            if state == "inactive":
+                status, jobs = self.systemctl("list-jobs --full 2>&1", user)
+                if "No jobs" in jobs:
+                    info = self.get_unit_info(unit, user)
+                    if info["ActiveState"] == state:
+                        raise Exception(
+                            (
+                                'unit "{}" is inactive and there ' "are no pending jobs"
+                            ).format(unit)
+                        )
+
+            return state == "active"
+
+        with self.nested(
+            "waiting for unit {}{}".format(
+                unit, f" with user {user}" if user is not None else ""
+            )
+        ):
+            retry(check_active)
+
+    def get_unit_info(self, unit: str, user: Optional[str] = None) -> Dict[str, str]:
+        status, lines = self.systemctl('--no-pager show "{}"'.format(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
+                )
+            )
+
+        line_pattern = re.compile(r"^([^=]+)=(.*)$")
+
+        def tuple_from_line(line: str) -> Tuple[str, str]:
+            match = line_pattern.match(line)
+            assert match is not None
+            return match[1], match[2]
+
+        return dict(
+            tuple_from_line(line)
+            for line in lines.split("\n")
+            if line_pattern.match(line)
+        )
+
+    def systemctl(self, q: str, user: Optional[str] = None) -> Tuple[int, str]:
+        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)
+            )
+        return self.execute("systemctl {}".format(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)
+        ):
+            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)
+                )
+
+    def _next_newline_closed_block_from_shell(self) -> str:
+        assert self.shell
+        output_buffer = []
+        while True:
+            # This receives up to 4096 bytes from the socket
+            chunk = self.shell.recv(4096)
+            if not chunk:
+                # Probably a broken pipe, return the output we have
+                break
+
+            decoded = chunk.decode()
+            output_buffer += [decoded]
+            if decoded[-1] == "\n":
+                break
+        return "".join(output_buffer)
+
+    def execute(
+        self, command: str, check_return: bool = True, timeout: Optional[int] = 900
+    ) -> Tuple[int, str]:
+        self.run_callbacks()
+        self.connect()
+
+        if timeout is not None:
+            command = "timeout {} sh -c {}".format(timeout, shlex.quote(command))
+
+        out_command = f"( set -euo pipefail; {command} ) | (base64 --wrap 0; echo)\n"
+        assert self.shell
+        self.shell.send(out_command.encode())
+
+        # Get the output
+        output = base64.b64decode(self._next_newline_closed_block_from_shell())
+
+        if not check_return:
+            return (-1, output.decode())
+
+        # Get the return code
+        self.shell.send("echo ${PIPESTATUS[0]}\n".encode())
+        rc = int(self._next_newline_closed_block_from_shell().strip())
+
+        return (rc, output.decode())
+
+    def shell_interact(self) -> None:
+        """Allows you to interact with the guest shell
+
+        Should only be used during test development, not in the production test."""
+        self.connect()
+        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()],
+        )
+
+    def succeed(self, *commands: str, timeout: Optional[int] = None) -> str:
+        """Execute each command and check that it succeeds."""
+        output = ""
+        for command in commands:
+            with self.nested("must succeed: {}".format(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)
+                    )
+                output += out
+        return output
+
+    def fail(self, *commands: str, timeout: Optional[int] = None) -> str:
+        """Execute each command and check that it fails."""
+        output = ""
+        for command in commands:
+            with self.nested("must fail: {}".format(command)):
+                (status, out) = self.execute(command, timeout=timeout)
+                if status == 0:
+                    raise Exception(
+                        "command `{}` unexpectedly succeeded".format(command)
+                    )
+                output += out
+        return output
+
+    def wait_until_succeeds(self, command: str, timeout: int = 900) -> str:
+        """Wait until a command returns success and return its output.
+        Throws an exception on timeout.
+        """
+        output = ""
+
+        def check_success(_: Any) -> bool:
+            nonlocal output
+            status, output = self.execute(command, timeout=timeout)
+            return status == 0
+
+        with self.nested("waiting for success: {}".format(command)):
+            retry(check_success, timeout)
+            return output
+
+    def wait_until_fails(self, command: str, timeout: int = 900) -> str:
+        """Wait until a command returns failure.
+        Throws an exception on timeout.
+        """
+        output = ""
+
+        def check_failure(_: Any) -> bool:
+            nonlocal output
+            status, output = self.execute(command, timeout=timeout)
+            return status != 0
+
+        with self.nested("waiting for failure: {}".format(command)):
+            retry(check_failure)
+            return output
+
+    def wait_for_shutdown(self) -> None:
+        if not self.booted:
+            return
+
+        with self.nested("waiting for the VM to power off"):
+            sys.stdout.flush()
+            assert self.process
+            self.process.wait()
+
+            self.pid = None
+            self.booted = False
+            self.connected = False
+
+    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)
+        )
+        return output
+
+    def wait_until_tty_matches(self, tty: str, regexp: str) -> None:
+        """Wait until the visible output on the chosen TTY matches regular
+        expression. Throws an exception on timeout.
+        """
+        matcher = re.compile(regexp)
+
+        def tty_matches(last: bool) -> bool:
+            text = self.get_tty_text(tty)
+            if last:
+                self.log(
+                    f"Last chance to match /{regexp}/ on TTY{tty}, "
+                    f"which currently contains: {text}"
+                )
+            return len(matcher.findall(text)) > 0
+
+        with self.nested("waiting for {} to appear on tty {}".format(regexp, tty)):
+            retry(tty_matches)
+
+    def send_chars(self, chars: List[str]) -> None:
+        with self.nested("sending keys ‘{}‘".format(chars)):
+            for char in chars:
+                self.send_key(char)
+
+    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))
+            return status == 0
+
+        with self.nested("waiting for file ‘{}‘".format(filename)):
+            retry(check_file)
+
+    def wait_for_open_port(self, port: int) -> None:
+        def port_is_open(_: Any) -> bool:
+            status, _ = self.execute("nc -z localhost {}".format(port))
+            return status == 0
+
+        with self.nested("waiting for TCP port {}".format(port)):
+            retry(port_is_open)
+
+    def wait_for_closed_port(self, port: int) -> None:
+        def port_is_closed(_: Any) -> bool:
+            status, _ = self.execute("nc -z localhost {}".format(port))
+            return status != 0
+
+        with self.nested("waiting for TCP port {} 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)
+
+    def stop_job(self, jobname: str, user: Optional[str] = None) -> Tuple[int, str]:
+        return self.systemctl("stop {}".format(jobname), user)
+
+    def wait_for_job(self, jobname: str) -> None:
+        self.wait_for_unit(jobname)
+
+    def connect(self) -> None:
+        if self.connected:
+            return
+
+        with self.nested("waiting for the VM to finish booting"):
+            self.start()
+
+            assert self.shell
+
+            tic = time.time()
+            self.shell.recv(1024)
+            # TODO: Timeout
+            toc = time.time()
+
+            self.log("connected to guest root shell")
+            self.log("(connecting took {:.2f} seconds)".format(toc - tic))
+            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)
+
+        with self.nested(
+            "making screenshot {}".format(filename),
+            {"image": os.path.basename(filename)},
+        ):
+            self.send_monitor_command("screendump {}".format(tmp))
+            ret = subprocess.run("pnmtopng {} > {}".format(tmp, filename), shell=True)
+            os.unlink(tmp)
+            if ret.returncode != 0:
+                raise Exception("Cannot convert screenshot")
+
+    def copy_from_host_via_shell(self, source: str, target: str) -> None:
+        """Copy a file from the host into the guest by piping it over the
+        shell into the destination file. Works without host-guest shared folder.
+        Prefer copy_from_host for whenever possible.
+        """
+        with open(source, "rb") as fh:
+            content_b64 = base64.b64encode(fh.read()).decode()
+            self.succeed(
+                f"mkdir -p $(dirname {target})",
+                f"echo -n {content_b64} | base64 -d > {target}",
+            )
+
+    def copy_from_host(self, source: str, target: str) -> None:
+        """Copy a file from the host into the guest via the `shared_dir` shared
+        among all the VMs (using a temporary directory).
+        """
+        host_src = Path(source)
+        vm_target = Path(target)
+        with tempfile.TemporaryDirectory(dir=self.shared_dir) as shared_td:
+            shared_temp = Path(shared_td)
+            host_intermediate = shared_temp / host_src.name
+            vm_shared_temp = Path("/tmp/shared") / shared_temp.name
+            vm_intermediate = vm_shared_temp / host_src.name
+
+            self.succeed(make_command(["mkdir", "-p", vm_shared_temp]))
+            if host_src.is_dir():
+                shutil.copytree(host_src, host_intermediate)
+            else:
+                shutil.copy(host_src, host_intermediate)
+            self.succeed(make_command(["mkdir", "-p", vm_target.parent]))
+            self.succeed(make_command(["cp", "-r", vm_intermediate, vm_target]))
+
+    def copy_from_vm(self, source: str, target_dir: str = "") -> None:
+        """Copy a file from the VM (specified by an in-VM source path) to a path
+        relative to `$out`. The file is copied via the `shared_dir` shared among
+        all the VMs (using a temporary directory).
+        """
+        # Compute the source, target, and intermediate shared file names
+        vm_src = Path(source)
+        with tempfile.TemporaryDirectory(dir=self.shared_dir) as shared_td:
+            shared_temp = Path(shared_td)
+            vm_shared_temp = Path("/tmp/shared") / shared_temp.name
+            vm_intermediate = vm_shared_temp / vm_src.name
+            intermediate = shared_temp / vm_src.name
+            # Copy the file to the shared directory inside VM
+            self.succeed(make_command(["mkdir", "-p", vm_shared_temp]))
+            self.succeed(make_command(["cp", "-r", vm_src, vm_intermediate]))
+            abs_target = self.out_dir / target_dir / vm_src.name
+            abs_target.parent.mkdir(exist_ok=True, parents=True)
+            # Copy the file from the shared directory outside VM
+            if intermediate.is_dir():
+                shutil.copytree(intermediate, abs_target)
+            else:
+                shutil.copy(intermediate, abs_target)
+
+    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))
+
+    def _get_screen_text_variants(self, model_ids: Iterable[int]) -> List[str]:
+        with tempfile.TemporaryDirectory() as tmpdir:
+            screenshot_path = os.path.join(tmpdir, "ppm")
+            self.send_monitor_command(f"screendump {screenshot_path}")
+            return _perform_ocr_on_screenshot(screenshot_path, model_ids)
+
+    def get_screen_text_variants(self) -> List[str]:
+        return self._get_screen_text_variants([0, 1, 2])
+
+    def get_screen_text(self) -> str:
+        return self._get_screen_text_variants([2])[0]
+
+    def wait_for_text(self, regex: str) -> None:
+        def screen_matches(last: bool) -> bool:
+            variants = self.get_screen_text_variants()
+            for text in variants:
+                if re.search(regex, text) is not None:
+                    return True
+
+            if last:
+                self.log("Last OCR attempt failed. Text was: {}".format(variants))
+
+            return False
+
+        with self.nested("waiting for {} to appear on screen".format(regex)):
+            retry(screen_matches)
+
+    def wait_for_console_text(self, regex: str) -> None:
+        with self.nested("waiting for {} to appear on console".format(regex)):
+            # Buffer the console output, this is needed
+            # to match multiline regexes.
+            console = io.StringIO()
+            while True:
+                try:
+                    console.write(self.last_lines.get())
+                except queue.Empty:
+                    self.sleep(1)
+                    continue
+                console.seek(0)
+                matches = re.search(regex, console.read())
+                if matches is not None:
+                    return
+
+    def send_key(self, key: str) -> None:
+        key = CHAR_TO_KEY.get(key, key)
+        self.send_monitor_command("sendkey {}".format(key))
+        time.sleep(0.01)
+
+    def start(self) -> None:
+        if self.booted:
+            return
+
+        self.log("starting vm")
+
+        def clear(path: Path) -> Path:
+            if path.exists():
+                path.unlink()
+            return path
+
+        def create_socket(path: Path) -> socket.socket:
+            s = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM)
+            s.bind(str(path))
+            s.listen(1)
+            return s
+
+        monitor_socket = create_socket(clear(self.monitor_path))
+        shell_socket = create_socket(clear(self.shell_path))
+        self.process = self.start_command.run(
+            self.state_dir,
+            self.shared_dir,
+            self.monitor_path,
+            self.shell_path,
+        )
+        self.monitor, _ = monitor_socket.accept()
+        self.shell, _ = shell_socket.accept()
+
+        # Store last serial console lines for use
+        # of wait_for_console_text
+        self.last_lines: Queue = Queue()
+
+        def process_serial_output() -> None:
+            assert self.process
+            assert self.process.stdout
+            for _line in self.process.stdout:
+                # Ignore undecodable bytes that may occur in boot menus
+                line = _line.decode(errors="ignore").replace("\r", "").rstrip()
+                self.last_lines.put(line)
+                self.log_serial(line)
+
+        self.serial_thread = threading.Thread(target=process_serial_output)
+        self.serial_thread.start()
+
+        self.wait_for_monitor_prompt()
+
+        self.pid = self.process.pid
+        self.booted = True
+
+        self.log("QEMU running (pid {})".format(self.pid))
+
+    def cleanup_statedir(self) -> None:
+        shutil.rmtree(self.state_dir)
+        rootlog.log(f"deleting VM state directory {self.state_dir}")
+        rootlog.log("if you want to keep the VM state, pass --keep-vm-state")
+
+    def shutdown(self) -> None:
+        if not self.booted:
+            return
+
+        assert self.shell
+        self.shell.send("poweroff\n".encode())
+        self.wait_for_shutdown()
+
+    def crash(self) -> None:
+        if not self.booted:
+            return
+
+        self.log("forced crash")
+        self.send_monitor_command("quit")
+        self.wait_for_shutdown()
+
+    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.
+        """
+
+        def check_x(_: Any) -> bool:
+            cmd = (
+                "journalctl -b SYSLOG_IDENTIFIER=systemd | "
+                + 'grep "Reached target Current graphical"'
+            )
+            status, _ = self.execute(cmd)
+            if status != 0:
+                return False
+            status, _ = self.execute("[ -e /tmp/.X11-unix/X0 ]")
+            return status == 0
+
+        with self.nested("waiting for the X11 server"):
+            retry(check_x)
+
+    def get_window_names(self) -> List[str]:
+        return self.succeed(
+            r"xwininfo -root -tree | sed 's/.*0x[0-9a-f]* \"\([^\"]*\)\".*/\1/; t; d'"
+        ).splitlines()
+
+    def wait_for_window(self, regexp: str) -> None:
+        pattern = re.compile(regexp)
+
+        def window_is_visible(last_try: bool) -> bool:
+            names = self.get_window_names()
+            if last_try:
+                self.log(
+                    "Last chance to match {} on the window list,".format(regexp)
+                    + " which currently contains: "
+                    + ", ".join(names)
+                )
+            return any(pattern.search(name) for name in names)
+
+        with self.nested("waiting for a window to appear"):
+            retry(window_is_visible)
+
+    def sleep(self, secs: int) -> None:
+        # We want to sleep in *guest* time, not *host* time.
+        self.succeed(f"sleep {secs}")
+
+    def forward_port(self, host_port: int = 8080, guest_port: int = 80) -> None:
+        """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)
+        )
+
+    def block(self) -> None:
+        """Make the machine unreachable by shutting down eth1 (the multicast
+        interface used to talk to the other VMs).  We keep eth0 up so that
+        the test driver can continue to talk to the machine.
+        """
+        self.send_monitor_command("set_link virtio-net-pci.1 off")
+
+    def unblock(self) -> None:
+        """Make the machine reachable."""
+        self.send_monitor_command("set_link virtio-net-pci.1 on")
+
+    def release(self) -> None:
+        if self.pid is None:
+            return
+        rootlog.info(f"kill machine (pid {self.pid})")
+        assert self.process
+        assert self.shell
+        assert self.monitor
+        assert self.serial_thread
+
+        self.process.terminate()
+        self.shell.close()
+        self.monitor.close()
+        self.serial_thread.join()
+
+    def run_callbacks(self) -> None:
+        for callback in self.callbacks:
+            callback()
diff --git a/nixos/lib/test-driver/test_driver/polling_condition.py b/nixos/lib/test-driver/test_driver/polling_condition.py
new file mode 100644
index 00000000000..459845452fa
--- /dev/null
+++ b/nixos/lib/test-driver/test_driver/polling_condition.py
@@ -0,0 +1,77 @@
+from typing import Callable, Optional
+import time
+
+from .logger import rootlog
+
+
+class PollingConditionFailed(Exception):
+    pass
+
+
+class PollingCondition:
+    condition: Callable[[], bool]
+    seconds_interval: float
+    description: Optional[str]
+
+    last_called: float
+    entered: bool
+
+    def __init__(
+        self,
+        condition: Callable[[], Optional[bool]],
+        seconds_interval: float = 2.0,
+        description: Optional[str] = None,
+    ):
+        self.condition = condition  # type: ignore
+        self.seconds_interval = seconds_interval
+
+        if description is None:
+            if condition.__doc__:
+                self.description = condition.__doc__
+            else:
+                self.description = condition.__name__
+        else:
+            self.description = str(description)
+
+        self.last_called = float("-inf")
+        self.entered = False
+
+    def check(self) -> bool:
+        if self.entered or not self.overdue:
+            return True
+
+        with self, rootlog.nested(self.nested_message):
+            rootlog.info(f"Time since last: {time.monotonic() - self.last_called:.2f}s")
+            try:
+                res = self.condition()  # type: ignore
+            except Exception:
+                res = False
+            res = res is None or res
+            rootlog.info(self.status_message(res))
+            return res
+
+    def maybe_raise(self) -> None:
+        if not self.check():
+            raise PollingConditionFailed(self.status_message(False))
+
+    def status_message(self, status: bool) -> str:
+        return f"Polling condition {'succeeded' if status else 'failed'}: {self.description}"
+
+    @property
+    def nested_message(self) -> str:
+        nested_message = ["Checking polling condition"]
+        if self.description is not None:
+            nested_message.append(repr(self.description))
+
+        return " ".join(nested_message)
+
+    @property
+    def overdue(self) -> bool:
+        return self.last_called + self.seconds_interval < time.monotonic()
+
+    def __enter__(self) -> None:
+        self.entered = True
+
+    def __exit__(self, exc_type, exc_value, traceback) -> None:  # type: ignore
+        self.entered = False
+        self.last_called = time.monotonic()
diff --git a/nixos/lib/test-driver/test_driver/vlan.py b/nixos/lib/test-driver/test_driver/vlan.py
new file mode 100644
index 00000000000..e5c8f07b4ed
--- /dev/null
+++ b/nixos/lib/test-driver/test_driver/vlan.py
@@ -0,0 +1,58 @@
+from pathlib import Path
+import io
+import os
+import pty
+import subprocess
+
+from test_driver.logger import rootlog
+
+
+class VLan:
+    """This class handles a VLAN that the run-vm scripts identify via its
+    number handles. The network's lifetime equals the object's lifetime.
+    """
+
+    nr: int
+    socket_dir: Path
+
+    process: subprocess.Popen
+    pid: int
+    fd: io.TextIOBase
+
+    def __repr__(self) -> str:
+        return f"<Vlan Nr. {self.nr}>"
+
+    def __init__(self, nr: int, tmp_dir: Path):
+        self.nr = nr
+        self.socket_dir = tmp_dir / f"vde{self.nr}.ctl"
+
+        # TODO: don't side-effect environment here
+        os.environ[f"QEMU_VDE_SOCKET_{self.nr}"] = str(self.socket_dir)
+
+        rootlog.info("start vlan")
+        pty_master, pty_slave = pty.openpty()
+
+        self.process = subprocess.Popen(
+            ["vde_switch", "-s", self.socket_dir, "--dirmode", "0700"],
+            stdin=pty_slave,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            shell=False,
+        )
+        self.pid = self.process.pid
+        self.fd = os.fdopen(pty_master, "w")
+        self.fd.write("version\n")
+
+        # TODO: perl version checks if this can be read from
+        # an if not, dies. we could hang here forever. Fix it.
+        assert self.process.stdout is not None
+        self.process.stdout.readline()
+        if not (self.socket_dir / "ctl").exists():
+            rootlog.error("cannot start vde_switch")
+
+        rootlog.info(f"running vlan (pid {self.pid})")
+
+    def __del__(self) -> None:
+        rootlog.info(f"kill vlan (pid {self.pid})")
+        self.fd.close()
+        self.process.terminate()
diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix
new file mode 100644
index 00000000000..0d3c3a89e78
--- /dev/null
+++ b/nixos/lib/testing-python.nix
@@ -0,0 +1,251 @@
+{ system
+, pkgs ? import ../.. { inherit system config; }
+  # Use a minimal kernel?
+, minimal ? false
+  # Ignored
+, config ? { }
+  # !!! See comment about args in lib/modules.nix
+, specialArgs ? { }
+  # Modules to add to each VM
+, extraConfigurations ? [ ]
+}:
+
+with pkgs;
+
+rec {
+
+  inherit pkgs;
+
+  # Run an automated test suite in the given virtual network.
+  runTests = { driver, driverInteractive, pos }:
+    stdenv.mkDerivation {
+      name = "vm-test-run-${driver.testName}";
+
+      requiredSystemFeatures = [ "kvm" "nixos-test" ];
+
+      buildCommand =
+        ''
+          mkdir -p $out
+
+          # effectively mute the XMLLogger
+          export LOGFILE=/dev/null
+
+          ${driver}/bin/nixos-test-driver -o $out
+        '';
+
+      passthru = driver.passthru // {
+        inherit driver driverInteractive;
+      };
+
+      inherit pos; # for better debugging
+    };
+
+  # Generate convenience wrappers for running the test driver
+  # has vlans, vms and test script defaulted through env variables
+  # also instantiates test script with nodes, if it's a function (contract)
+  setupDriverForTest = {
+      testScript
+    , testName
+    , nodes
+    , qemu_pkg ? pkgs.qemu_test
+    , enableOCR ? false
+    , skipLint ? false
+    , passthru ? {}
+    , interactive ? false
+  }:
+    let
+      # Reifies and correctly wraps the python test driver for
+      # the respective qemu version and with or without ocr support
+      testDriver = pkgs.callPackage ./test-driver {
+        inherit enableOCR;
+        qemu_pkg = qemu_test;
+        imagemagick_light = imagemagick_light.override { inherit libtiff; };
+        tesseract4 = tesseract4.override { enableLanguages = [ "eng" ]; };
+      };
+
+
+      testDriverName =
+        let
+          # A standard store path to the vm monitor is built like this:
+          #   /tmp/nix-build-vm-test-run-$name.drv-0/vm-state-machine/monitor
+          # The max filename length of a unix domain socket is 108 bytes.
+          # This means $name can at most be 50 bytes long.
+          maxTestNameLen = 50;
+          testNameLen = builtins.stringLength testName;
+        in with builtins;
+          if testNameLen > maxTestNameLen then
+            abort
+              ("The name of the test '${testName}' must not be longer than ${toString maxTestNameLen} " +
+                "it's currently ${toString testNameLen} characters long.")
+          else
+            "nixos-test-driver-${testName}";
+
+      vlans = map (m: m.config.virtualisation.vlans) (lib.attrValues nodes);
+      vms = map (m: m.config.system.build.vm) (lib.attrValues nodes);
+
+      nodeHostNames = let
+        nodesList = map (c: c.config.system.name) (lib.attrValues nodes);
+      in nodesList ++ lib.optional (lib.length nodesList == 1) "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;
+
+      testScript' =
+        # Call the test script with the computed nodes.
+        if lib.isFunction testScript
+        then testScript { inherit nodes; }
+        else testScript;
+
+    in
+    if lib.length invalidNodeNames > 0 then
+      throw ''
+        Cannot create machines out of (${lib.concatStringsSep ", " invalidNodeNames})!
+        All machines are referenced as python variables in the testing framework which will break the
+        script when special characters are used.
+
+        This is an IMPLEMENTATION ERROR and needs to be fixed. Meanwhile,
+        please stick to alphanumeric chars and underscores as separation.
+      ''
+    else lib.warnIf skipLint "Linting is disabled" (runCommand testDriverName
+      {
+        inherit testName;
+        nativeBuildInputs = [ makeWrapper ];
+        testScript = testScript';
+        preferLocalBuild = true;
+        passthru = passthru // {
+          inherit nodes;
+        };
+      }
+      ''
+        mkdir -p $out/bin
+
+        vmStartScripts=($(for i in ${toString vms}; do echo $i/bin/run-*-vm; done))
+        echo -n "$testScript" > $out/test-script
+        ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-test-driver
+
+        ${testDriver}/bin/generate-driver-symbols
+        ${lib.optionalString (!skipLint) ''
+          PYFLAKES_BUILTINS="$(
+            echo -n ${lib.escapeShellArg (lib.concatStringsSep "," nodeHostNames)},
+            < ${lib.escapeShellArg "driver-symbols"}
+          )" ${python3Packages.pyflakes}/bin/pyflakes $out/test-script
+        ''}
+
+        # set defaults through environment
+        # see: ./test-driver/test-driver.py argparse implementation
+        wrapProgram $out/bin/nixos-test-driver \
+          --set startScripts "''${vmStartScripts[*]}" \
+          --set testScript "$out/test-script" \
+          --set vlans '${toString vlans}' \
+          ${lib.optionalString (interactive) "--add-flags --interactive"}
+      '');
+
+  # Make a full-blown test
+  makeTest =
+    { testScript
+    , enableOCR ? false
+    , name ? "unnamed"
+      # Skip linting (mainly intended for faster dev cycles)
+    , skipLint ? false
+    , passthru ? {}
+    , # For meta.position
+      pos ? # position used in error messages and for meta.position
+        (if t.meta.description or null != null
+          then builtins.unsafeGetAttrPos "description" t.meta
+          else builtins.unsafeGetAttrPos "testScript" t)
+    , ...
+    } @ t:
+    let
+      nodes = qemu_pkg:
+        let
+          testScript' =
+            # Call the test script with the computed nodes.
+            if lib.isFunction testScript
+            then testScript { nodes = nodes qemu_pkg; }
+            else testScript;
+
+          build-vms = import ./build-vms.nix {
+            inherit system lib pkgs minimal specialArgs;
+            extraConfigurations = extraConfigurations ++ [(
+              { config, ... }:
+              {
+                virtualisation.qemu.package = qemu_pkg;
+
+                # Make sure all derivations referenced by the test
+                # script are available on the nodes. When the store is
+                # accessed through 9p, this isn't important, since
+                # everything in the store is available to the guest,
+                # but when building a root image it is, as all paths
+                # that should be available to the guest has to be
+                # copied to the image.
+                virtualisation.additionalPaths =
+                  lib.optional
+                    # A testScript may evaluate nodes, which has caused
+                    # infinite recursions. The demand cycle involves:
+                    #   testScript -->
+                    #   nodes -->
+                    #   toplevel -->
+                    #   additionalPaths -->
+                    #   hasContext testScript' -->
+                    #   testScript (ad infinitum)
+                    # If we don't need to build an image, we can break this
+                    # cycle by short-circuiting when useNixStoreImage is false.
+                    (config.virtualisation.useNixStoreImage && builtins.hasContext testScript')
+                    (pkgs.writeStringReferencesToFile testScript');
+
+                # Ensure we do not use aliases. Ideally this is only set
+                # when the test framework is used by Nixpkgs NixOS tests.
+                nixpkgs.config.allowAliases = false;
+              }
+            )];
+          };
+        in
+          build-vms.buildVirtualNetwork (
+              t.nodes or (if t ? machine then { machine = t.machine; } else { })
+          );
+
+      driver = setupDriverForTest {
+        inherit testScript enableOCR skipLint passthru;
+        testName = name;
+        qemu_pkg = pkgs.qemu_test;
+        nodes = nodes pkgs.qemu_test;
+      };
+      driverInteractive = setupDriverForTest {
+        inherit testScript enableOCR skipLint passthru;
+        testName = name;
+        qemu_pkg = pkgs.qemu;
+        nodes = nodes pkgs.qemu;
+        interactive = true;
+      };
+
+      test =
+        let
+          passMeta = drv: drv // lib.optionalAttrs (t ? meta) {
+            meta = (drv.meta or { }) // t.meta;
+          };
+        in passMeta (runTests { inherit driver pos driverInteractive; });
+
+    in
+      test // {
+        inherit test driver driverInteractive nodes;
+      };
+
+  abortForFunction = functionName: abort ''The ${functionName} function was
+    removed because it is not an essential part of the NixOS testing
+    infrastructure. It had no usage in NixOS or Nixpkgs and it had no designated
+    maintainer. You are free to reintroduce it by documenting it in the manual
+    and adding yourself as maintainer. It was removed in
+    https://github.com/NixOS/nixpkgs/pull/137013
+  '';
+
+  runInMachine = abortForFunction "runInMachine";
+
+  runInMachineWithX = abortForFunction "runInMachineWithX";
+
+  simpleTest = as: (makeTest as).test;
+
+}
diff --git a/nixos/lib/utils.nix b/nixos/lib/utils.nix
new file mode 100644
index 00000000000..ae68c3920c5
--- /dev/null
+++ b/nixos/lib/utils.nix
@@ -0,0 +1,201 @@
+{ lib, config, pkgs }: with lib;
+
+rec {
+
+  # Copy configuration files to avoid having the entire sources in the system closure
+  copyFile = filePath: pkgs.runCommand (builtins.unsafeDiscardStringContext (builtins.baseNameOf filePath)) {} ''
+    cp ${filePath} $out
+  '';
+
+  # Check whenever fileSystem is needed for boot.  NOTE: Make sure
+  # pathsNeededForBoot is closed under the parent relationship, i.e. if /a/b/c
+  # is in the list, put /a and /a/b in as well.
+  pathsNeededForBoot = [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/var/lib/nixos" "/etc" "/usr" ];
+  fsNeededForBoot = fs: fs.neededForBoot || elem fs.mountPoint pathsNeededForBoot;
+
+  # Check whenever `b` depends on `a` as a fileSystem
+  fsBefore = a: b:
+    let
+      # normalisePath adds a slash at the end of the path if it didn't already
+      # have one.
+      #
+      # The reason slashes are added at the end of each path is to prevent `b`
+      # from accidentally depending on `a` in cases like
+      #    a = { mountPoint = "/aaa"; ... }
+      #    b = { device     = "/aaaa"; ... }
+      # Here a.mountPoint *is* a prefix of b.device even though a.mountPoint is
+      # *not* a parent of b.device. If we add a slash at the end of each string,
+      # though, this is not a problem: "/aaa/" is not a prefix of "/aaaa/".
+      normalisePath = path: "${path}${optionalString (!(hasSuffix "/" path)) "/"}";
+      normalise = mount: mount // { device = normalisePath (toString mount.device);
+                                    mountPoint = normalisePath mount.mountPoint;
+                                    depends = map normalisePath mount.depends;
+                                  };
+
+      a' = normalise a;
+      b' = normalise b;
+
+    in hasPrefix a'.mountPoint b'.device
+    || hasPrefix a'.mountPoint b'.mountPoint
+    || any (hasPrefix a'.mountPoint) b'.depends;
+
+  # Escape a path according to the systemd rules, e.g. /dev/xyzzy
+  # becomes dev-xyzzy.  FIXME: slow.
+  escapeSystemdPath = s:
+   replaceChars ["/" "-" " "] ["-" "\\x2d" "\\x20"]
+   (removePrefix "/" s);
+
+  # Quotes an argument for use in Exec* service lines.
+  # systemd accepts "-quoted strings with escape sequences, toJSON produces
+  # a subset of these.
+  # Additionally we escape % to disallow expansion of % specifiers. Any lone ;
+  # in the input will be turned it ";" and thus lose its special meaning.
+  # Every $ is escaped to $$, this makes it unnecessary to disable environment
+  # substitution for the directive.
+  escapeSystemdExecArg = arg:
+    let
+      s = if builtins.isPath arg then "${arg}"
+        else if builtins.isString arg then arg
+        else if builtins.isInt arg || builtins.isFloat arg then toString arg
+        else throw "escapeSystemdExecArg only allows strings, paths and numbers";
+    in
+      replaceChars [ "%" "$" ] [ "%%" "$$" ] (builtins.toJSON s);
+
+  # Quotes a list of arguments into a single string for use in a Exec*
+  # line.
+  escapeSystemdExecArgs = concatMapStringsSep " " escapeSystemdExecArg;
+
+  # Returns a system path for a given shell package
+  toShellPath = shell:
+    if types.shellPackage.check shell then
+      "/run/current-system/sw${shell.shellPath}"
+    else if types.package.check shell then
+      throw "${shell} is not a shell package"
+    else
+      shell;
+
+  /* Recurse into a list or an attrset, searching for attrs named like
+     the value of the "attr" parameter, and return an attrset where the
+     names are the corresponding jq path where the attrs were found and
+     the values are the values of the attrs.
+
+     Example:
+       recursiveGetAttrWithJqPrefix {
+         example = [
+           {
+             irrelevant = "not interesting";
+           }
+           {
+             ignored = "ignored attr";
+             relevant = {
+               secret = {
+                 _secret = "/path/to/secret";
+               };
+             };
+           }
+         ];
+       } "_secret" -> { ".example[1].relevant.secret" = "/path/to/secret"; }
+  */
+  recursiveGetAttrWithJqPrefix = item: attr:
+    let
+      recurse = prefix: item:
+        if item ? ${attr} then
+          nameValuePair prefix item.${attr}
+        else if isAttrs item then
+          map (name: recurse (prefix + "." + name) item.${name}) (attrNames item)
+        else if isList item then
+          imap0 (index: item: recurse (prefix + "[${toString index}]") item) item
+        else
+          [];
+    in listToAttrs (flatten (recurse "" item));
+
+  /* Takes an attrset and a file path and generates a bash snippet that
+     outputs a JSON file at the file path with all instances of
+
+     { _secret = "/path/to/secret" }
+
+     in the attrset replaced with the contents of the file
+     "/path/to/secret" in the output JSON.
+
+     When a configuration option accepts an attrset that is finally
+     converted to JSON, this makes it possible to let the user define
+     arbitrary secret values.
+
+     Example:
+       If the file "/path/to/secret" contains the string
+       "topsecretpassword1234",
+
+       genJqSecretsReplacementSnippet {
+         example = [
+           {
+             irrelevant = "not interesting";
+           }
+           {
+             ignored = "ignored attr";
+             relevant = {
+               secret = {
+                 _secret = "/path/to/secret";
+               };
+             };
+           }
+         ];
+       } "/path/to/output.json"
+
+       would generate a snippet that, when run, outputs the following
+       JSON file at "/path/to/output.json":
+
+       {
+         "example": [
+           {
+             "irrelevant": "not interesting"
+           },
+           {
+             "ignored": "ignored attr",
+             "relevant": {
+               "secret": "topsecretpassword1234"
+             }
+           }
+         ]
+       }
+  */
+  genJqSecretsReplacementSnippet = genJqSecretsReplacementSnippet' "_secret";
+
+  # Like genJqSecretsReplacementSnippet, but allows the name of the
+  # attr which identifies the secret to be changed.
+  genJqSecretsReplacementSnippet' = attr: set: output:
+    let
+      secrets = recursiveGetAttrWithJqPrefix set attr;
+    in ''
+      if [[ -h '${output}' ]]; then
+        rm '${output}'
+      fi
+
+      inherit_errexit_enabled=0
+      shopt -pq inherit_errexit && inherit_errexit_enabled=1
+      shopt -s inherit_errexit
+    ''
+    + concatStringsSep
+        "\n"
+        (imap1 (index: name: ''
+                  secret${toString index}=$(<'${secrets.${name}}')
+                  export secret${toString index}
+                '')
+               (attrNames secrets))
+    + "\n"
+    + "${pkgs.jq}/bin/jq >'${output}' '"
+    + concatStringsSep
+      " | "
+      (imap1 (index: name: ''${name} = $ENV.secret${toString index}'')
+             (attrNames secrets))
+    + ''
+      ' <<'EOF'
+      ${builtins.toJSON set}
+      EOF
+      (( ! $inherit_errexit_enabled )) && shopt -u inherit_errexit
+    '';
+
+  systemdUtils = {
+    lib = import ./systemd-lib.nix { inherit lib config pkgs; };
+    unitOptions = import ./systemd-unit-options.nix { inherit lib systemdUtils; };
+  };
+}
diff --git a/nixos/maintainers/option-usages.nix b/nixos/maintainers/option-usages.nix
new file mode 100644
index 00000000000..11247666ecd
--- /dev/null
+++ b/nixos/maintainers/option-usages.nix
@@ -0,0 +1,192 @@
+{ configuration ? import ../lib/from-env.nix "NIXOS_CONFIG" <nixos-config>
+
+# provide an option name, as a string literal.
+, testOption ? null
+
+# provide a list of option names, as string literals.
+, testOptions ? [ ]
+}:
+
+# This file is made to be used as follow:
+#
+#   $ nix-instantiate ./option-usage.nix --argstr testOption service.xserver.enable -A txtContent --eval
+#
+# or
+#
+#   $ nix-build ./option-usage.nix --argstr testOption service.xserver.enable -A txt -o service.xserver.enable._txt
+#
+# Other targets exists such as `dotContent`, `dot`, and `pdf`.  If you are
+# looking for the option usage of multiple options, you can provide a list
+# as argument.
+#
+#   $ nix-build ./option-usage.nix --arg testOptions \
+#      '["boot.loader.gummiboot.enable" "boot.loader.gummiboot.timeout"]' \
+#      -A txt -o gummiboot.list
+#
+# Note, this script is slow as it has to evaluate all options of the system
+# once per queried option.
+#
+# This nix expression works by doing a first evaluation, which evaluates the
+# result of every option.
+#
+# Then, for each queried option, we evaluate the NixOS modules a second
+# time, except that we replace the `config` argument of all the modules with
+# the result of the original evaluation, except for the tested option which
+# value is replaced by a `throw` statement which is caught by the `tryEval`
+# evaluation of each option value.
+#
+# We then compare the result of the evaluation of the original module, with
+# the result of the second evaluation, and consider that the new failures are
+# caused by our mutation of the `config` argument.
+#
+# Doing so returns all option results which are directly using the
+# tested option result.
+
+with import ../../lib;
+
+let
+
+  evalFun = {
+    specialArgs ? {}
+  }: import ../lib/eval-config.nix {
+       modules = [ configuration ];
+       inherit specialArgs;
+     };
+
+  eval = evalFun {};
+  inherit (eval) pkgs;
+
+  excludedTestOptions = [
+    # We cannot evluate _module.args, as it is used during the computation
+    # of the modules list.
+    "_module.args"
+
+    # For some reasons which we yet have to investigate, some options cannot
+    # be replaced by a throw without causing a non-catchable failure.
+    "networking.bonds"
+    "networking.bridges"
+    "networking.interfaces"
+    "networking.macvlans"
+    "networking.sits"
+    "networking.vlans"
+    "services.openssh.startWhenNeeded"
+  ];
+
+  # for some reasons which we yet have to investigate, some options are
+  # time-consuming to compute, thus we filter them out at the moment.
+  excludedOptions = [
+    "boot.systemd.services"
+    "systemd.services"
+    "kde.extraPackages"
+  ];
+  excludeOptions = list:
+    filter (opt: !(elem (showOption opt.loc) excludedOptions)) list;
+
+
+  reportNewFailures = old: new:
+    let
+      filterChanges =
+        filter ({fst, snd}:
+          !(fst.success -> snd.success)
+        );
+
+      keepNames =
+        map ({fst, snd}:
+          /* assert fst.name == snd.name; */ snd.name
+        );
+
+      # Use  tryEval (strict ...)  to know if there is any failure while
+      # evaluating the option value.
+      #
+      # Note, the `strict` function is not strict enough, but using toXML
+      # builtins multiply by 4 the memory usage and the time used to compute
+      # each options.
+      tryCollectOptions = moduleResult:
+        forEach (excludeOptions (collect isOption moduleResult)) (opt:
+          { name = showOption opt.loc; } // builtins.tryEval (strict opt.value));
+     in
+       keepNames (
+         filterChanges (
+           zipLists (tryCollectOptions old) (tryCollectOptions new)
+         )
+       );
+
+
+  # Create a list of modules where each module contains only one failling
+  # options.
+  introspectionModules =
+    let
+      setIntrospection = opt: rec {
+        name = showOption opt.loc;
+        path = opt.loc;
+        config = setAttrByPath path
+          (throw "Usage introspection of '${name}' by forced failure.");
+      };
+    in
+      map setIntrospection (collect isOption eval.options);
+
+  overrideConfig = thrower:
+    recursiveUpdateUntil (path: old: new:
+      path == thrower.path
+    ) eval.config thrower.config;
+
+
+  graph =
+    map (thrower: {
+      option = thrower.name;
+      usedBy = assert __trace "Investigate ${thrower.name}" true;
+        reportNewFailures eval.options (evalFun {
+          specialArgs = {
+            config = overrideConfig thrower;
+          };
+        }).options;
+    }) introspectionModules;
+
+  displayOptionsGraph =
+     let
+       checkList =
+         if testOption != null then [ testOption ]
+         else testOptions;
+       checkAll = checkList == [];
+     in
+       flip filter graph ({option, ...}:
+         (checkAll || elem option checkList)
+         && !(elem option excludedTestOptions)
+       );
+
+  graphToDot = graph: ''
+    digraph "Option Usages" {
+      ${concatMapStrings ({option, usedBy}:
+          concatMapStrings (user: ''
+            "${option}" -> "${user}"''
+          ) usedBy
+        ) displayOptionsGraph}
+    }
+  '';
+
+  graphToText = graph:
+    concatMapStrings ({usedBy, ...}:
+        concatMapStrings (user: ''
+          ${user}
+        '') usedBy
+      ) displayOptionsGraph;
+
+in
+
+rec {
+  dotContent = graphToDot graph;
+  dot = pkgs.writeTextFile {
+    name = "option_usages.dot";
+    text = dotContent;
+  };
+
+  pdf = pkgs.texFunctions.dot2pdf {
+    dotGraph = dot;
+  };
+
+  txtContent = graphToText graph;
+  txt = pkgs.writeTextFile {
+    name = "option_usages.txt";
+    text = txtContent;
+  };
+}
diff --git a/nixos/maintainers/scripts/azure-new/.gitignore b/nixos/maintainers/scripts/azure-new/.gitignore
new file mode 100644
index 00000000000..9271abf14a0
--- /dev/null
+++ b/nixos/maintainers/scripts/azure-new/.gitignore
@@ -0,0 +1 @@
+azure
diff --git a/nixos/maintainers/scripts/azure-new/README.md b/nixos/maintainers/scripts/azure-new/README.md
new file mode 100644
index 00000000000..e5b69dacec0
--- /dev/null
+++ b/nixos/maintainers/scripts/azure-new/README.md
@@ -0,0 +1,42 @@
+# azure
+
+## Demo
+
+Here's a demo of this being used: https://asciinema.org/a/euXb9dIeUybE3VkstLWLbvhmp
+
+## Usage
+
+This is meant to be an example image that you can copy into your own
+project and modify to your own needs. Notice that the example image
+includes a built-in test user account, which by default uses your
+`~/.ssh/id_ed25519.pub` as an `authorized_key`.
+
+Build and upload the image
+```shell
+$ ./upload-image.sh ./examples/basic/image.nix
+
+...
++ attr=azbasic
++ nix-build ./examples/basic/image.nix --out-link azure
+/nix/store/qdpzknpskzw30vba92mb24xzll1dqsmd-azure-image
+...
+95.5 %, 0 Done, 0 Failed, 1 Pending, 0 Skipped, 1 Total, 2-sec Throughput (Mb/s): 932.9565
+...
+/subscriptions/aff271ee-e9be-4441-b9bb-42f5af4cbaeb/resourceGroups/nixos-images/providers/Microsoft.Compute/images/azure-image-todo-makethisbetter
+```
+
+Take the output, boot an Azure VM:
+
+```
+img="/subscriptions/.../..." # use output from last command
+./boot-vm.sh "${img}"
+...
+=> booted
+```
+
+## Future Work
+
+1. If the user specifies a hard-coded user, then the agent could be removed.
+   Probably has security benefits; definitely has closure-size benefits.
+   (It's likely the VM will need to be booted with a special flag. See:
+   https://github.com/Azure/azure-cli/issues/12775 for details.)
diff --git a/nixos/maintainers/scripts/azure-new/boot-vm.sh b/nixos/maintainers/scripts/azure-new/boot-vm.sh
new file mode 100755
index 00000000000..1ce3a5f9db1
--- /dev/null
+++ b/nixos/maintainers/scripts/azure-new/boot-vm.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+set -euo pipefail
+set -x
+
+image="${1}"
+location="westus2"
+group="nixos-test-vm"
+vm_size="Standard_D2s_v3";  os_size=42;
+
+# ensure group
+az group create --location "westus2" --name "${group}"
+group_id="$(az group show --name "${group}" -o tsv --query "[id]")"
+
+# (optional) identity
+if ! az identity show -n "${group}-identity" -g "${group}" &>/dev/stderr; then
+  az identity create --name "${group}-identity" --resource-group "${group}"
+fi
+
+# (optional) role assignment, to the resource group, bad but not really great alternatives
+identity_id="$(az identity show --name "${group}-identity" --resource-group "${group}" -o tsv --query "[id]")"
+principal_id="$(az identity show --name "${group}-identity" --resource-group "${group}" -o tsv --query "[principalId]")"
+until az role assignment create --assignee "${principal_id}" --role "Owner" --scope "${group_id}"; do sleep 1; done
+
+# boot vm
+az vm create \
+  --name "${group}-vm" \
+  --resource-group "${group}" \
+  --assign-identity "${identity_id}" \
+  --size "${vm_size}" \
+  --os-disk-size-gb "${os_size}" \
+  --image "${image}" \
+  --admin-username "${USER}" \
+  --location "westus2" \
+  --storage-sku "Premium_LRS" \
+  --ssh-key-values "$(ssh-add -L)"
+
diff --git a/nixos/maintainers/scripts/azure-new/common.sh b/nixos/maintainers/scripts/azure-new/common.sh
new file mode 100644
index 00000000000..eb87c3e0650
--- /dev/null
+++ b/nixos/maintainers/scripts/azure-new/common.sh
@@ -0,0 +1,7 @@
+export group="${AZURE_RESOURCE_GROUP:-"azure"}"
+export location="${AZURE_LOCATION:-"westus2"}"
+
+img_file=$(echo azure/*.vhd)
+img_name="$(basename "${img_file}")"
+img_name="${img_name%".vhd"}"
+export img_name="${img_name//[._]/-}"
diff --git a/nixos/maintainers/scripts/azure-new/examples/basic/image.nix b/nixos/maintainers/scripts/azure-new/examples/basic/image.nix
new file mode 100644
index 00000000000..310eba3621a
--- /dev/null
+++ b/nixos/maintainers/scripts/azure-new/examples/basic/image.nix
@@ -0,0 +1,10 @@
+let
+  pkgs = (import ../../../../../../default.nix {});
+  machine = import (pkgs.path + "/nixos/lib/eval-config.nix") {
+    system = "x86_64-linux";
+    modules = [
+      ({config, ...}: { imports = [ ./system.nix ]; })
+    ];
+  };
+in
+  machine.config.system.build.azureImage
diff --git a/nixos/maintainers/scripts/azure-new/examples/basic/system.nix b/nixos/maintainers/scripts/azure-new/examples/basic/system.nix
new file mode 100644
index 00000000000..d283742701d
--- /dev/null
+++ b/nixos/maintainers/scripts/azure-new/examples/basic/system.nix
@@ -0,0 +1,34 @@
+{ pkgs, modulesPath, ... }:
+
+let username = "azurenixosuser";
+in
+{
+  imports = [
+    "${modulesPath}/virtualisation/azure-common.nix"
+    "${modulesPath}/virtualisation/azure-image.nix"
+  ];
+
+  ## NOTE: This is just an example of how to hard-code a user.
+  ## The normal Azure agent IS included and DOES provision a user based
+  ## on the information passed at VM creation time.
+  users.users."${username}" = {
+    isNormalUser = true;
+    home = "/home/${username}";
+    description = "Azure NixOS Test User";
+    openssh.authorizedKeys.keys = [ (builtins.readFile ~/.ssh/id_ed25519.pub) ];
+  };
+  nix.settings.trusted-users = [ username ];
+
+  virtualisation.azureImage.diskSize = 2500;
+
+  system.stateVersion = "20.03";
+  boot.kernelPackages = pkgs.linuxPackages_latest;
+
+  # test user doesn't have a password
+  services.openssh.passwordAuthentication = false;
+  security.sudo.wheelNeedsPassword = false;
+
+  environment.systemPackages = with pkgs; [
+    git file htop wget curl
+  ];
+}
diff --git a/nixos/maintainers/scripts/azure-new/shell.nix b/nixos/maintainers/scripts/azure-new/shell.nix
new file mode 100644
index 00000000000..592f1bf9056
--- /dev/null
+++ b/nixos/maintainers/scripts/azure-new/shell.nix
@@ -0,0 +1,13 @@
+with (import ../../../../default.nix {});
+stdenv.mkDerivation {
+  name = "nixcfg-azure-devenv";
+
+  nativeBuildInputs = [
+    azure-cli
+    bash
+    cacert
+    azure-storage-azcopy
+  ];
+
+  AZURE_CONFIG_DIR="/tmp/azure-cli/.azure";
+}
diff --git a/nixos/maintainers/scripts/azure-new/upload-image.sh b/nixos/maintainers/scripts/azure-new/upload-image.sh
new file mode 100755
index 00000000000..143afbd7f96
--- /dev/null
+++ b/nixos/maintainers/scripts/azure-new/upload-image.sh
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+set -euo pipefail
+set -x
+
+image_nix="${1:-"./examples/basic/image.nix"}"
+
+nix-build "${image_nix}" --out-link "azure"
+
+group="nixos-images"
+location="westus2"
+img_name="nixos-image"
+img_file="$(readlink -f ./azure/disk.vhd)"
+
+if ! az group show -n "${group}" &>/dev/null; then
+  az group create --name "${group}" --location "${location}"
+fi
+
+# note: the disk access token song/dance is tedious
+# but allows us to upload direct to a disk image
+# thereby avoid storage accounts (and naming them) entirely!
+if ! az disk show -g "${group}" -n "${img_name}" &>/dev/null; then
+  bytes="$(stat -c %s ${img_file})"
+  size="30"
+  az disk create \
+    --resource-group "${group}" \
+    --name "${img_name}" \
+    --for-upload true --upload-size-bytes "${bytes}"
+
+  timeout=$(( 60 * 60 )) # disk access token timeout
+  sasurl="$(\
+    az disk grant-access \
+      --access-level Write \
+      --resource-group "${group}" \
+      --name "${img_name}" \
+      --duration-in-seconds ${timeout} \
+        | jq -r '.accessSas'
+  )"
+
+  azcopy copy "${img_file}" "${sasurl}" \
+    --blob-type PageBlob
+
+  az disk revoke-access \
+    --resource-group "${group}" \
+    --name "${img_name}"
+fi
+
+if ! az image show -g "${group}" -n "${img_name}" &>/dev/null; then
+  diskid="$(az disk show -g "${group}" -n "${img_name}" -o json | jq -r .id)"
+
+  az image create \
+    --resource-group "${group}" \
+    --name "${img_name}" \
+    --source "${diskid}" \
+    --os-type "linux" >/dev/null
+fi
+
+imageid="$(az image show -g "${group}" -n "${img_name}" -o json | jq -r .id)"
+echo "${imageid}"
diff --git a/nixos/maintainers/scripts/azure/create-azure.sh b/nixos/maintainers/scripts/azure/create-azure.sh
new file mode 100755
index 00000000000..0558f8dfffc
--- /dev/null
+++ b/nixos/maintainers/scripts/azure/create-azure.sh
@@ -0,0 +1,8 @@
+#! /bin/sh -eu
+
+export NIX_PATH=nixpkgs=$(dirname $(readlink -f $0))/../../../..
+export NIXOS_CONFIG=$(dirname $(readlink -f $0))/../../../modules/virtualisation/azure-image.nix
+export TIMESTAMP=$(date +%Y%m%d%H%M)
+
+nix-build '<nixpkgs/nixos>' \
+   -A config.system.build.azureImage --argstr system x86_64-linux -o azure -j 10
diff --git a/nixos/maintainers/scripts/azure/upload-azure.sh b/nixos/maintainers/scripts/azure/upload-azure.sh
new file mode 100755
index 00000000000..2ea35d1d4c3
--- /dev/null
+++ b/nixos/maintainers/scripts/azure/upload-azure.sh
@@ -0,0 +1,22 @@
+#! /bin/sh -e
+
+export STORAGE=${STORAGE:-nixos}
+export THREADS=${THREADS:-8}
+
+azure-vhd-utils-for-go  upload --localvhdpath azure/disk.vhd  --stgaccountname "$STORAGE"  --stgaccountkey "$KEY" \
+   --containername images --blobname nixos-unstable-nixops-updated.vhd --parallelism "$THREADS" --overwrite
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nixos/maintainers/scripts/cloudstack/cloudstack-image.nix b/nixos/maintainers/scripts/cloudstack/cloudstack-image.nix
new file mode 100644
index 00000000000..005f75476e9
--- /dev/null
+++ b/nixos/maintainers/scripts/cloudstack/cloudstack-image.nix
@@ -0,0 +1,22 @@
+# nix-build '<nixpkgs/nixos>' -A config.system.build.cloudstackImage --arg configuration "{ imports = [ ./nixos/maintainers/scripts/cloudstack/cloudstack-image.nix ]; }"
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  imports =
+    [ ../../../modules/virtualisation/cloudstack-config.nix ];
+
+  system.build.cloudstackImage = import ../../../lib/make-disk-image.nix {
+    inherit lib config pkgs;
+    format = "qcow2";
+    configFile = pkgs.writeText "configuration.nix"
+      ''
+        {
+          imports = [ <nixpkgs/nixos/modules/virtualisation/cloudstack-config.nix> ];
+        }
+      '';
+  };
+
+}
diff --git a/nixos/maintainers/scripts/ec2/amazon-image-zfs.nix b/nixos/maintainers/scripts/ec2/amazon-image-zfs.nix
new file mode 100644
index 00000000000..32dd96a7cb7
--- /dev/null
+++ b/nixos/maintainers/scripts/ec2/amazon-image-zfs.nix
@@ -0,0 +1,12 @@
+{
+  imports = [ ./amazon-image.nix ];
+  ec2.zfs = {
+    enable = true;
+    datasets = {
+      "tank/system/root".mount = "/";
+      "tank/system/var".mount = "/var";
+      "tank/local/nix".mount = "/nix";
+      "tank/user/home".mount = "/home";
+    };
+  };
+}
diff --git a/nixos/maintainers/scripts/ec2/amazon-image.nix b/nixos/maintainers/scripts/ec2/amazon-image.nix
new file mode 100644
index 00000000000..6358ec68f7c
--- /dev/null
+++ b/nixos/maintainers/scripts/ec2/amazon-image.nix
@@ -0,0 +1,165 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.amazonImage;
+  amiBootMode = if config.ec2.efi then "uefi" else "legacy-bios";
+
+in {
+
+  imports = [ ../../../modules/virtualisation/amazon-image.nix ];
+
+  # Amazon recomments setting this to the highest possible value for a good EBS
+  # experience, which prior to 4.15 was 255.
+  # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nvme-ebs-volumes.html#timeout-nvme-ebs-volumes
+  config.boot.kernelParams =
+    let timeout =
+      if pkgs.lib.versionAtLeast config.boot.kernelPackages.kernel.version "4.15"
+      then "4294967295"
+      else  "255";
+    in [ "nvme_core.io_timeout=${timeout}" ];
+
+  options.amazonImage = {
+    name = mkOption {
+      type = types.str;
+      description = "The name of the generated derivation";
+      default = "nixos-amazon-image-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}";
+    };
+
+    contents = mkOption {
+      example = literalExpression ''
+        [ { source = pkgs.memtest86 + "/memtest.bin";
+            target = "boot/memtest.bin";
+          }
+        ]
+      '';
+      default = [];
+      description = ''
+        This option lists files to be copied to fixed locations in the
+        generated image. Glob patterns work.
+      '';
+    };
+
+    sizeMB = mkOption {
+      type = with types; either (enum [ "auto" ]) int;
+      default = if config.ec2.hvm then 2048 else 8192;
+      example = 8192;
+      description = "The size in MB of the image";
+    };
+
+    format = mkOption {
+      type = types.enum [ "raw" "qcow2" "vpc" ];
+      default = "vpc";
+      description = "The image format to output";
+    };
+  };
+
+  config.system.build.amazonImage = let
+    configFile = pkgs.writeText "configuration.nix"
+      ''
+        { modulesPath, ... }: {
+          imports = [ "''${modulesPath}/virtualisation/amazon-image.nix" ];
+          ${optionalString config.ec2.hvm ''
+            ec2.hvm = true;
+          ''}
+          ${optionalString config.ec2.efi ''
+            ec2.efi = true;
+          ''}
+          ${optionalString config.ec2.zfs.enable ''
+            ec2.zfs.enable = true;
+            networking.hostId = "${config.networking.hostId}";
+          ''}
+        }
+      '';
+
+    zfsBuilder = import ../../../lib/make-zfs-image.nix {
+      inherit lib config configFile;
+      inherit (cfg) contents format name;
+      pkgs = import ../../../.. { inherit (pkgs) system; }; # ensure we use the regular qemu-kvm package
+
+      includeChannel = true;
+
+      bootSize = 1000; # 1G is the minimum EBS volume
+
+      rootSize = cfg.sizeMB;
+      rootPoolProperties = {
+        ashift = 12;
+        autoexpand = "on";
+      };
+
+      datasets = config.ec2.zfs.datasets;
+
+      postVM = ''
+        extension=''${rootDiskImage##*.}
+        friendlyName=$out/${cfg.name}
+        rootDisk="$friendlyName.root.$extension"
+        bootDisk="$friendlyName.boot.$extension"
+        mv "$rootDiskImage" "$rootDisk"
+        mv "$bootDiskImage" "$bootDisk"
+
+        mkdir -p $out/nix-support
+        echo "file ${cfg.format} $bootDisk" >> $out/nix-support/hydra-build-products
+        echo "file ${cfg.format} $rootDisk" >> $out/nix-support/hydra-build-products
+
+       ${pkgs.jq}/bin/jq -n \
+         --arg system_label ${lib.escapeShellArg config.system.nixos.label} \
+         --arg system ${lib.escapeShellArg pkgs.stdenv.hostPlatform.system} \
+         --arg root_logical_bytes "$(${pkgs.qemu}/bin/qemu-img info --output json "$rootDisk" | ${pkgs.jq}/bin/jq '."virtual-size"')" \
+         --arg boot_logical_bytes "$(${pkgs.qemu}/bin/qemu-img info --output json "$bootDisk" | ${pkgs.jq}/bin/jq '."virtual-size"')" \
+         --arg boot_mode "${amiBootMode}" \
+         --arg root "$rootDisk" \
+         --arg boot "$bootDisk" \
+        '{}
+          | .label = $system_label
+          | .boot_mode = $boot_mode
+          | .system = $system
+          | .disks.boot.logical_bytes = $boot_logical_bytes
+          | .disks.boot.file = $boot
+          | .disks.root.logical_bytes = $root_logical_bytes
+          | .disks.root.file = $root
+          ' > $out/nix-support/image-info.json
+      '';
+    };
+
+    extBuilder = import ../../../lib/make-disk-image.nix {
+      inherit lib config configFile;
+
+      inherit (cfg) contents format name;
+      pkgs = import ../../../.. { inherit (pkgs) system; }; # ensure we use the regular qemu-kvm package
+
+      fsType = "ext4";
+      partitionTableType = if config.ec2.efi then "efi"
+                           else if config.ec2.hvm then "legacy+gpt"
+                           else "none";
+
+      diskSize = cfg.sizeMB;
+
+      postVM = ''
+        extension=''${diskImage##*.}
+        friendlyName=$out/${cfg.name}.$extension
+        mv "$diskImage" "$friendlyName"
+        diskImage=$friendlyName
+
+        mkdir -p $out/nix-support
+        echo "file ${cfg.format} $diskImage" >> $out/nix-support/hydra-build-products
+
+       ${pkgs.jq}/bin/jq -n \
+         --arg system_label ${lib.escapeShellArg config.system.nixos.label} \
+         --arg system ${lib.escapeShellArg pkgs.stdenv.hostPlatform.system} \
+         --arg logical_bytes "$(${pkgs.qemu}/bin/qemu-img info --output json "$diskImage" | ${pkgs.jq}/bin/jq '."virtual-size"')" \
+         --arg boot_mode "${amiBootMode}" \
+         --arg file "$diskImage" \
+          '{}
+          | .label = $system_label
+          | .boot_mode = $boot_mode
+          | .system = $system
+          | .logical_bytes = $logical_bytes
+          | .file = $file
+          | .disks.root.logical_bytes = $logical_bytes
+          | .disks.root.file = $file
+          ' > $out/nix-support/image-info.json
+      '';
+    };
+  in if config.ec2.zfs.enable then zfsBuilder else extBuilder;
+}
diff --git a/nixos/maintainers/scripts/ec2/create-amis.sh b/nixos/maintainers/scripts/ec2/create-amis.sh
new file mode 100755
index 00000000000..797fe03e209
--- /dev/null
+++ b/nixos/maintainers/scripts/ec2/create-amis.sh
@@ -0,0 +1,342 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -p awscli -p jq -p qemu -i bash
+# shellcheck shell=bash
+#
+# Future Deprecation?
+# This entire thing should probably be replaced with a generic terraform config
+
+# Uploads and registers NixOS images built from the
+# <nixos/release.nix> amazonImage attribute. Images are uploaded and
+# registered via a home region, and then copied to other regions.
+
+# The home region requires an s3 bucket, and an IAM role named "vmimport"
+# (by default) with access to the S3 bucket. The name can be
+# configured with the "service_role_name" variable. Configuration of the
+# vmimport role is documented in
+# https://docs.aws.amazon.com/vm-import/latest/userguide/vmimport-image-import.html
+
+# set -x
+set -euo pipefail
+
+var () { true; }
+
+# configuration
+var ${state_dir:=$HOME/amis/ec2-images}
+var ${home_region:=eu-west-1}
+var ${bucket:=nixos-amis}
+var ${service_role_name:=vmimport}
+
+var ${regions:=eu-west-1 eu-west-2 eu-west-3 eu-central-1 eu-north-1
+         us-east-1 us-east-2 us-west-1 us-west-2
+         ca-central-1
+         ap-southeast-1 ap-southeast-2 ap-northeast-1 ap-northeast-2
+         ap-south-1 ap-east-1
+         sa-east-1}
+
+regions=($regions)
+
+log() {
+    echo "$@" >&2
+}
+
+if [ "$#" -ne 1 ]; then
+    log "Usage: ./upload-amazon-image.sh IMAGE_OUTPUT"
+    exit 1
+fi
+
+# result of the amazon-image from nixos/release.nix
+store_path=$1
+
+if [ ! -e "$store_path" ]; then
+    log "Store path: $store_path does not exist, fetching..."
+    nix-store --realise "$store_path"
+fi
+
+if [ ! -d "$store_path" ]; then
+    log "store_path: $store_path is not a directory. aborting"
+    exit 1
+fi
+
+read_image_info() {
+    if [ ! -e "$store_path/nix-support/image-info.json" ]; then
+        log "Image missing metadata"
+        exit 1
+    fi
+    jq -r "$1" "$store_path/nix-support/image-info.json"
+}
+
+# We handle a single image per invocation, store all attributes in
+# globals for convenience.
+zfs_disks=$(read_image_info .disks)
+is_zfs_image=
+if jq -e .boot <<< "$zfs_disks"; then
+  is_zfs_image=1
+  zfs_boot=".disks.boot"
+fi
+image_label="$(read_image_info .label)${is_zfs_image:+-ZFS}"
+image_system=$(read_image_info .system)
+image_files=( $(read_image_info ".disks.root.file") )
+
+image_logical_bytes=$(read_image_info "${zfs_boot:-.disks.root}.logical_bytes")
+
+if [[ -n "$is_zfs_image" ]]; then
+  image_files+=( $(read_image_info .disks.boot.file) )
+fi
+
+# Derived attributes
+
+image_logical_gigabytes=$(((image_logical_bytes-1)/1024/1024/1024+1)) # Round to the next GB
+
+case "$image_system" in
+    aarch64-linux)
+        amazon_arch=arm64
+        ;;
+    x86_64-linux)
+        amazon_arch=x86_64
+        ;;
+    *)
+        log "Unknown system: $image_system"
+        exit 1
+esac
+
+image_name="NixOS-${image_label}-${image_system}"
+image_description="NixOS ${image_label} ${image_system}"
+
+log "Image Details:"
+log " Name: $image_name"
+log " Description: $image_description"
+log " Size (gigabytes): $image_logical_gigabytes"
+log " System: $image_system"
+log " Amazon Arch: $amazon_arch"
+
+read_state() {
+    local state_key=$1
+    local type=$2
+
+    cat "$state_dir/$state_key.$type" 2>/dev/null || true
+}
+
+write_state() {
+    local state_key=$1
+    local type=$2
+    local val=$3
+
+    mkdir -p "$state_dir"
+    echo "$val" > "$state_dir/$state_key.$type"
+}
+
+wait_for_import() {
+    local region=$1
+    local task_id=$2
+    local state snapshot_id
+    log "Waiting for import task $task_id to be completed"
+    while true; do
+        read -r state message snapshot_id < <(
+            aws ec2 describe-import-snapshot-tasks --region "$region" --import-task-ids "$task_id" | \
+                jq -r '.ImportSnapshotTasks[].SnapshotTaskDetail | "\(.Status) \(.StatusMessage) \(.SnapshotId)"'
+        )
+        log " ... state=$state message=$message snapshot_id=$snapshot_id"
+        case "$state" in
+            active)
+                sleep 10
+                ;;
+            completed)
+                echo "$snapshot_id"
+                return
+                ;;
+            *)
+                log "Unexpected snapshot import state: '${state}'"
+                log "Full response: "
+                aws ec2 describe-import-snapshot-tasks --region "$region" --import-task-ids "$task_id" >&2
+                exit 1
+                ;;
+        esac
+    done
+}
+
+wait_for_image() {
+    local region=$1
+    local ami_id=$2
+    local state
+    log "Waiting for image $ami_id to be available"
+
+    while true; do
+        read -r state < <(
+            aws ec2 describe-images --image-ids "$ami_id" --region "$region" | \
+                jq -r ".Images[].State"
+        )
+        log " ... state=$state"
+        case "$state" in
+            pending)
+                sleep 10
+                ;;
+            available)
+                return
+                ;;
+            *)
+                log "Unexpected AMI state: '${state}'"
+                exit 1
+                ;;
+        esac
+    done
+}
+
+
+make_image_public() {
+    local region=$1
+    local ami_id=$2
+
+    wait_for_image "$region" "$ami_id"
+
+    log "Making image $ami_id public"
+
+    aws ec2 modify-image-attribute \
+        --image-id "$ami_id" --region "$region" --launch-permission 'Add={Group=all}' >&2
+}
+
+upload_image() {
+    local region=$1
+
+    for image_file in "${image_files[@]}"; do
+        local aws_path=${image_file#/}
+
+        if [[ -n "$is_zfs_image" ]]; then
+            local suffix=${image_file%.*}
+            suffix=${suffix##*.}
+        fi
+
+        local state_key="$region.$image_label${suffix:+.${suffix}}.$image_system"
+        local task_id
+        task_id=$(read_state "$state_key" task_id)
+        local snapshot_id
+        snapshot_id=$(read_state "$state_key" snapshot_id)
+        local ami_id
+        ami_id=$(read_state "$state_key" ami_id)
+
+        if [ -z "$task_id" ]; then
+            log "Checking for image on S3"
+            if ! aws s3 ls --region "$region" "s3://${bucket}/${aws_path}" >&2; then
+                log "Image missing from aws, uploading"
+                aws s3 cp --region "$region" "$image_file" "s3://${bucket}/${aws_path}" >&2
+            fi
+
+            log "Importing image from S3 path s3://$bucket/$aws_path"
+
+            task_id=$(aws ec2 import-snapshot --role-name "$service_role_name" --disk-container "{
+              \"Description\": \"nixos-image-${image_label}-${image_system}\",
+              \"Format\": \"vhd\",
+              \"UserBucket\": {
+                  \"S3Bucket\": \"$bucket\",
+                  \"S3Key\": \"$aws_path\"
+              }
+            }" --region "$region" | jq -r '.ImportTaskId')
+
+            write_state "$state_key" task_id "$task_id"
+        fi
+
+        if [ -z "$snapshot_id" ]; then
+            snapshot_id=$(wait_for_import "$region" "$task_id")
+            write_state "$state_key" snapshot_id "$snapshot_id"
+        fi
+    done
+
+    if [ -z "$ami_id" ]; then
+        log "Registering snapshot $snapshot_id as AMI"
+
+        local block_device_mappings=(
+            "DeviceName=/dev/xvda,Ebs={SnapshotId=$snapshot_id,VolumeSize=$image_logical_gigabytes,DeleteOnTermination=true,VolumeType=gp3}"
+        )
+
+        if [[ -n "$is_zfs_image" ]]; then
+            local root_snapshot_id=$(read_state "$region.$image_label.root.$image_system" snapshot_id)
+
+            local root_image_logical_bytes=$(read_image_info ".disks.root.logical_bytes")
+            local root_image_logical_gigabytes=$(((root_image_logical_bytes-1)/1024/1024/1024+1)) # Round to the next GB
+
+            block_device_mappings+=(
+                "DeviceName=/dev/xvdb,Ebs={SnapshotId=$root_snapshot_id,VolumeSize=$root_image_logical_gigabytes,DeleteOnTermination=true,VolumeType=gp3}"
+            )
+        fi
+
+
+        local extra_flags=(
+            --root-device-name /dev/xvda
+            --sriov-net-support simple
+            --ena-support
+            --virtualization-type hvm
+        )
+
+        block_device_mappings+=("DeviceName=/dev/sdb,VirtualName=ephemeral0")
+        block_device_mappings+=("DeviceName=/dev/sdc,VirtualName=ephemeral1")
+        block_device_mappings+=("DeviceName=/dev/sdd,VirtualName=ephemeral2")
+        block_device_mappings+=("DeviceName=/dev/sde,VirtualName=ephemeral3")
+
+        ami_id=$(
+            aws ec2 register-image \
+                --name "$image_name" \
+                --description "$image_description" \
+                --region "$region" \
+                --architecture $amazon_arch \
+                --block-device-mappings "${block_device_mappings[@]}" \
+                --boot-mode $(read_image_info .boot_mode) \
+                "${extra_flags[@]}" \
+                | jq -r '.ImageId'
+              )
+
+        write_state "$state_key" ami_id "$ami_id"
+    fi
+
+    [[ -v PRIVATE ]] || make_image_public "$region" "$ami_id"
+
+    echo "$ami_id"
+}
+
+copy_to_region() {
+    local region=$1
+    local from_region=$2
+    local from_ami_id=$3
+
+    state_key="$region.$image_label.$image_system"
+    ami_id=$(read_state "$state_key" ami_id)
+
+    if [ -z "$ami_id" ]; then
+        log "Copying $from_ami_id to $region"
+        ami_id=$(
+            aws ec2 copy-image \
+                --region "$region" \
+                --source-region "$from_region" \
+                --source-image-id "$from_ami_id" \
+                --name "$image_name" \
+                --description "$image_description" \
+                | jq -r '.ImageId'
+              )
+
+        write_state "$state_key" ami_id "$ami_id"
+    fi
+
+    [[ -v PRIVATE ]] || make_image_public "$region" "$ami_id"
+
+    echo "$ami_id"
+}
+
+upload_all() {
+    home_image_id=$(upload_image "$home_region")
+    jq -n \
+       --arg key "$home_region.$image_system" \
+       --arg value "$home_image_id" \
+       '$ARGS.named'
+
+    for region in "${regions[@]}"; do
+        if [ "$region" = "$home_region" ]; then
+            continue
+        fi
+        copied_image_id=$(copy_to_region "$region" "$home_region" "$home_image_id")
+
+        jq -n \
+           --arg key "$region.$image_system" \
+           --arg value "$copied_image_id" \
+           '$ARGS.named'
+    done
+}
+
+upload_all | jq --slurp from_entries
diff --git a/nixos/maintainers/scripts/gce/create-gce.sh b/nixos/maintainers/scripts/gce/create-gce.sh
new file mode 100755
index 00000000000..0eec4d04110
--- /dev/null
+++ b/nixos/maintainers/scripts/gce/create-gce.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env nix-shell
+#! nix-shell -i bash -p google-cloud-sdk
+
+set -euo pipefail
+
+BUCKET_NAME="${BUCKET_NAME:-nixos-cloud-images}"
+TIMESTAMP="$(date +%Y%m%d%H%M)"
+export TIMESTAMP
+
+nix-build '<nixpkgs/nixos/lib/eval-config.nix>' \
+   -A config.system.build.googleComputeImage \
+   --arg modules "[ <nixpkgs/nixos/modules/virtualisation/google-compute-image.nix> ]" \
+   --argstr system x86_64-linux \
+   -o gce \
+   -j 10
+
+img_path=$(echo gce/*.tar.gz)
+img_name=${IMAGE_NAME:-$(basename "$img_path")}
+img_id=$(echo "$img_name" | sed 's|.raw.tar.gz$||;s|\.|-|g;s|_|-|g')
+img_family=$(echo "$img_id" | cut -d - -f1-4)
+
+if ! gsutil ls "gs://${BUCKET_NAME}/$img_name"; then
+  gsutil cp "$img_path" "gs://${BUCKET_NAME}/$img_name"
+  gsutil acl ch -u AllUsers:R "gs://${BUCKET_NAME}/$img_name"
+
+  gcloud compute images create \
+    "$img_id" \
+    --source-uri "gs://${BUCKET_NAME}/$img_name" \
+    --family="$img_family"
+
+  gcloud compute images add-iam-policy-binding \
+    "$img_id" \
+    --member='allAuthenticatedUsers' \
+    --role='roles/compute.imageUser'
+fi
diff --git a/nixos/maintainers/scripts/lxd/lxd-image-inner.nix b/nixos/maintainers/scripts/lxd/lxd-image-inner.nix
new file mode 100644
index 00000000000..74634fd1671
--- /dev/null
+++ b/nixos/maintainers/scripts/lxd/lxd-image-inner.nix
@@ -0,0 +1,102 @@
+# 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’).
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+  imports =
+    [ # Include the default lxd configuration.
+      ../../../modules/virtualisation/lxc-container.nix
+      # Include the container-specific autogenerated configuration.
+      ./lxd.nix
+    ];
+
+  # networking.hostName = mkForce "nixos"; # Overwrite the hostname.
+  # networking.wireless.enable = true;  # Enables wireless support via wpa_supplicant.
+
+  # Set your time zone.
+  # time.timeZone = "Europe/Amsterdam";
+
+  # The global useDHCP flag is deprecated, therefore explicitly set to false here.
+  # Per-interface useDHCP will be mandatory in the future, so this generated config
+  # replicates the default behaviour.
+  networking.useDHCP = false;
+  networking.interfaces.eth0.useDHCP = true;
+
+  # Configure network proxy if necessary
+  # networking.proxy.default = "http://user:password@proxy:port/";
+  # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
+
+  # Select internationalisation properties.
+  # i18n.defaultLocale = "en_US.UTF-8";
+  # console = {
+  #   font = "Lat2-Terminus16";
+  #   keyMap = "us";
+  # };
+
+  # Enable the X11 windowing system.
+  # services.xserver.enable = true;
+
+  # Configure keymap in X11
+  # services.xserver.layout = "us";
+  # services.xserver.xkbOptions = "eurosign:e";
+
+  # Enable CUPS to print documents.
+  # services.printing.enable = true;
+
+  # Enable sound.
+  # sound.enable = true;
+  # hardware.pulseaudio.enable = true;
+
+  # Enable touchpad support (enabled default in most desktopManager).
+  # services.xserver.libinput.enable = true;
+
+  # Define a user account. Don't forget to set a password with ‘passwd’.
+  # users.users.jane = {
+  #   isNormalUser = true;
+  #   extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
+  # };
+
+  # List packages installed in system profile. To search, run:
+  # $ nix search wget
+  # environment.systemPackages = with pkgs; [
+  #   vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
+  #   wget
+  #   firefox
+  # ];
+
+  # Some programs need SUID wrappers, can be configured further or are
+  # started in user sessions.
+  # programs.mtr.enable = true;
+  # programs.gnupg.agent = {
+  #   enable = true;
+  #   enableSSHSupport = true;
+  # };
+
+  # List services that you want to enable:
+
+  # Enable the OpenSSH daemon.
+  # services.openssh.enable = true;
+
+  # Open ports in the firewall.
+  # networking.firewall.allowedTCPPorts = [ ... ];
+  # networking.firewall.allowedUDPPorts = [ ... ];
+  # Or disable the firewall altogether.
+  # networking.firewall.enable = false;
+
+  # 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
+  # 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).
+  system.stateVersion = "21.05"; # Did you read the comment?
+
+  # As this is intended as a stadalone image, undo some of the minimal profile stuff
+  documentation.enable = true;
+  documentation.nixos.enable = true;
+  environment.noXlibs = false;
+}
diff --git a/nixos/maintainers/scripts/lxd/lxd-image.nix b/nixos/maintainers/scripts/lxd/lxd-image.nix
new file mode 100644
index 00000000000..c76b9fcc7f7
--- /dev/null
+++ b/nixos/maintainers/scripts/lxd/lxd-image.nix
@@ -0,0 +1,34 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+
+{
+  imports = [
+    ../../../modules/virtualisation/lxc-container.nix
+  ];
+
+  virtualisation.lxc.templates.nix = {
+    enable = true;
+    target = "/etc/nixos/lxd.nix";
+    template = ./nix.tpl;
+    when = [ "create" "copy" ];
+  };
+
+  # copy the config for nixos-rebuild
+  system.activationScripts.config = ''
+    if [ ! -e /etc/nixos/configuration.nix ]; then
+      mkdir -p /etc/nixos
+      cat ${./lxd-image-inner.nix} > /etc/nixos/configuration.nix
+      sed 's|../../../modules/virtualisation/lxc-container.nix|<nixpkgs/nixos/modules/virtualisation/lxc-container.nix>|g' -i /etc/nixos/configuration.nix
+    fi
+  '';
+
+  # Network
+  networking.useDHCP = false;
+  networking.interfaces.eth0.useDHCP = true;
+
+  # As this is intended as a stadalone image, undo some of the minimal profile stuff
+  documentation.enable = true;
+  documentation.nixos.enable = true;
+  environment.noXlibs = false;
+}
diff --git a/nixos/maintainers/scripts/lxd/nix.tpl b/nixos/maintainers/scripts/lxd/nix.tpl
new file mode 100644
index 00000000000..307258ddc62
--- /dev/null
+++ b/nixos/maintainers/scripts/lxd/nix.tpl
@@ -0,0 +1,9 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+
+# WARNING: THIS CONFIGURATION IS AUTOGENERATED AND WILL BE OVERWRITTEN AUTOMATICALLY
+
+{
+  networking.hostName = "{{ container.name }}";
+}
diff --git a/nixos/maintainers/scripts/openstack/openstack-image.nix b/nixos/maintainers/scripts/openstack/openstack-image.nix
new file mode 100644
index 00000000000..3255e7f3d44
--- /dev/null
+++ b/nixos/maintainers/scripts/openstack/openstack-image.nix
@@ -0,0 +1,26 @@
+# nix-build '<nixpkgs/nixos>' -A config.system.build.openstackImage --arg configuration "{ imports = [ ./nixos/maintainers/scripts/openstack/openstack-image.nix ]; }"
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  imports =
+    [ ../../../modules/installer/cd-dvd/channel.nix
+      ../../../modules/virtualisation/openstack-config.nix
+    ];
+
+  system.build.openstackImage = import ../../../lib/make-disk-image.nix {
+    inherit lib config;
+    additionalSpace = "1024M";
+    pkgs = import ../../../.. { inherit (pkgs) system; }; # ensure we use the regular qemu-kvm package
+    format = "qcow2";
+    configFile = pkgs.writeText "configuration.nix"
+      ''
+        {
+          imports = [ <nixpkgs/nixos/modules/virtualisation/openstack-config.nix> ];
+        }
+      '';
+  };
+
+}
diff --git a/nixos/modules/config/appstream.nix b/nixos/modules/config/appstream.nix
new file mode 100644
index 00000000000..a72215c2f56
--- /dev/null
+++ b/nixos/modules/config/appstream.nix
@@ -0,0 +1,25 @@
+{ config, lib, ... }:
+
+with lib;
+{
+  options = {
+    appstream.enable = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to install files to support the
+        <link xlink:href="https://www.freedesktop.org/software/appstream/docs/index.html">AppStream metadata specification</link>.
+      '';
+    };
+  };
+
+  config = mkIf config.appstream.enable {
+    environment.pathsToLink = [
+      # per component metadata
+      "/share/metainfo"
+      # legacy path for above
+      "/share/appdata"
+    ];
+  };
+
+}
diff --git a/nixos/modules/config/console.nix b/nixos/modules/config/console.nix
new file mode 100644
index 00000000000..168bebd8d06
--- /dev/null
+++ b/nixos/modules/config/console.nix
@@ -0,0 +1,203 @@
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.console;
+
+  makeColor = i: concatMapStringsSep "," (x: "0x" + substring (2*i) 2 x);
+
+  isUnicode = hasSuffix "UTF-8" (toUpper config.i18n.defaultLocale);
+
+  optimizedKeymap = pkgs.runCommand "keymap" {
+    nativeBuildInputs = [ pkgs.buildPackages.kbd ];
+    LOADKEYS_KEYMAP_PATH = "${consoleEnv}/share/keymaps/**";
+    preferLocalBuild = true;
+  } ''
+    loadkeys -b ${optionalString isUnicode "-u"} "${cfg.keyMap}" > $out
+  '';
+
+  # Sadly, systemd-vconsole-setup doesn't support binary keymaps.
+  vconsoleConf = pkgs.writeText "vconsole.conf" ''
+    KEYMAP=${cfg.keyMap}
+    FONT=${cfg.font}
+  '';
+
+  consoleEnv = pkgs.buildEnv {
+    name = "console-env";
+    paths = [ pkgs.kbd ] ++ cfg.packages;
+    pathsToLink = [
+      "/share/consolefonts"
+      "/share/consoletrans"
+      "/share/keymaps"
+      "/share/unimaps"
+    ];
+  };
+
+  setVconsole = !config.boot.isContainer;
+in
+
+{
+  ###### interface
+
+  options.console  = {
+    font = mkOption {
+      type = with types; either str path;
+      default = "Lat2-Terminus16";
+      example = "LatArCyrHeb-16";
+      description = ''
+        The font used for the virtual consoles.  Leave empty to use
+        whatever the <command>setfont</command> program considers the
+        default font.
+        Can be either a font name or a path to a PSF font file.
+      '';
+    };
+
+    keyMap = mkOption {
+      type = with types; either str path;
+      default = "us";
+      example = "fr";
+      description = ''
+        The keyboard mapping table for the virtual consoles.
+      '';
+    };
+
+    colors = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [
+        "002b36" "dc322f" "859900" "b58900"
+        "268bd2" "d33682" "2aa198" "eee8d5"
+        "002b36" "cb4b16" "586e75" "657b83"
+        "839496" "6c71c4" "93a1a1" "fdf6e3"
+      ];
+      description = ''
+        The 16 colors palette used by the virtual consoles.
+        Leave empty to use the default colors.
+        Colors must be in hexadecimal format and listed in
+        order from color 0 to color 15.
+      '';
+
+    };
+
+    packages = mkOption {
+      type = types.listOf types.package;
+      default = [ ];
+      description = ''
+        List of additional packages that provide console fonts, keymaps and
+        other resources for virtual consoles use.
+      '';
+    };
+
+    useXkbConfig = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        If set, configure the virtual console keymap from the xserver
+        keyboard settings.
+      '';
+    };
+
+    earlySetup = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Enable setting virtual console options as early as possible (in initrd).
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkMerge [
+    { console.keyMap = with config.services.xserver;
+        mkIf cfg.useXkbConfig
+          (pkgs.runCommand "xkb-console-keymap" { preferLocalBuild = true; } ''
+            '${pkgs.buildPackages.ckbcomp}/bin/ckbcomp' \
+              ${optionalString (config.environment.sessionVariables ? XKB_CONFIG_ROOT)
+                "-I${config.environment.sessionVariables.XKB_CONFIG_ROOT}"
+              } \
+              -model '${xkbModel}' -layout '${layout}' \
+              -option '${xkbOptions}' -variant '${xkbVariant}' > "$out"
+          '');
+    }
+
+    (mkIf (!setVconsole) {
+      systemd.services.systemd-vconsole-setup.enable = false;
+    })
+
+    (mkIf setVconsole (mkMerge [
+      { environment.systemPackages = [ pkgs.kbd ];
+
+        # Let systemd-vconsole-setup.service do the work of setting up the
+        # virtual consoles.
+        environment.etc."vconsole.conf".source = vconsoleConf;
+        # Provide kbd with additional packages.
+        environment.etc.kbd.source = "${consoleEnv}/share";
+
+        boot.initrd.preLVMCommands = mkBefore ''
+          kbd_mode ${if isUnicode then "-u" else "-a"} -C /dev/console
+          printf "\033%%${if isUnicode then "G" else "@"}" >> /dev/console
+          loadkmap < ${optimizedKeymap}
+
+          ${optionalString cfg.earlySetup ''
+            setfont -C /dev/console $extraUtils/share/consolefonts/font.psf
+          ''}
+        '';
+
+        systemd.services.reload-systemd-vconsole-setup =
+          { description = "Reset console on configuration changes";
+            wantedBy = [ "multi-user.target" ];
+            restartTriggers = [ vconsoleConf consoleEnv ];
+            reloadIfChanged = true;
+            serviceConfig =
+              { RemainAfterExit = true;
+                ExecStart = "${pkgs.coreutils}/bin/true";
+                ExecReload = "/run/current-system/systemd/bin/systemctl restart systemd-vconsole-setup";
+              };
+          };
+      }
+
+      (mkIf (cfg.colors != []) {
+        boot.kernelParams = [
+          "vt.default_red=${makeColor 0 cfg.colors}"
+          "vt.default_grn=${makeColor 1 cfg.colors}"
+          "vt.default_blu=${makeColor 2 cfg.colors}"
+        ];
+      })
+
+      (mkIf cfg.earlySetup {
+        boot.initrd.extraUtilsCommands = ''
+          mkdir -p $out/share/consolefonts
+          ${if substring 0 1 cfg.font == "/" then ''
+            font="${cfg.font}"
+          '' else ''
+            font="$(echo ${consoleEnv}/share/consolefonts/${cfg.font}.*)"
+          ''}
+          if [[ $font == *.gz ]]; then
+            gzip -cd $font > $out/share/consolefonts/font.psf
+          else
+            cp -L $font $out/share/consolefonts/font.psf
+          fi
+        '';
+      })
+    ]))
+  ];
+
+  imports = [
+    (mkRenamedOptionModule [ "i18n" "consoleFont" ] [ "console" "font" ])
+    (mkRenamedOptionModule [ "i18n" "consoleKeyMap" ] [ "console" "keyMap" ])
+    (mkRenamedOptionModule [ "i18n" "consoleColors" ] [ "console" "colors" ])
+    (mkRenamedOptionModule [ "i18n" "consolePackages" ] [ "console" "packages" ])
+    (mkRenamedOptionModule [ "i18n" "consoleUseXkbConfig" ] [ "console" "useXkbConfig" ])
+    (mkRenamedOptionModule [ "boot" "earlyVconsoleSetup" ] [ "console" "earlySetup" ])
+    (mkRenamedOptionModule [ "boot" "extraTTYs" ] [ "console" "extraTTYs" ])
+    (mkRemovedOptionModule [ "console" "extraTTYs" ] ''
+      Since NixOS switched to systemd (circa 2012), TTYs have been spawned on
+      demand, so there is no need to configure them manually.
+    '')
+  ];
+}
diff --git a/nixos/modules/config/debug-info.nix b/nixos/modules/config/debug-info.nix
new file mode 100644
index 00000000000..2942ae5905d
--- /dev/null
+++ b/nixos/modules/config/debug-info.nix
@@ -0,0 +1,45 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+
+  options = {
+
+    environment.enableDebugInfo = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Some NixOS packages provide debug symbols. However, these are
+        not included in the system closure by default to save disk
+        space. Enabling this option causes the debug symbols to appear
+        in <filename>/run/current-system/sw/lib/debug/.build-id</filename>,
+        where tools such as <command>gdb</command> can find them.
+        If you need debug symbols for a package that doesn't
+        provide them by default, you can enable them as follows:
+        <programlisting>
+        nixpkgs.config.packageOverrides = pkgs: {
+          hello = pkgs.hello.overrideAttrs (oldAttrs: {
+            separateDebugInfo = true;
+          });
+        };
+        </programlisting>
+      '';
+    };
+
+  };
+
+
+  config = mkIf config.environment.enableDebugInfo {
+
+    # FIXME: currently disabled because /lib is already in
+    # environment.pathsToLink, and we can't have both.
+    #environment.pathsToLink = [ "/lib/debug/.build-id" ];
+
+    environment.extraOutputsToInstall = [ "debug" ];
+
+    environment.variables.NIX_DEBUG_INFO_DIRS = [ "/run/current-system/sw/lib/debug" ];
+
+  };
+
+}
diff --git a/nixos/modules/config/fonts/fontconfig.nix b/nixos/modules/config/fonts/fontconfig.nix
new file mode 100644
index 00000000000..1e68fef7ce7
--- /dev/null
+++ b/nixos/modules/config/fonts/fontconfig.nix
@@ -0,0 +1,475 @@
+/*
+
+Configuration files are linked to /etc/fonts/conf.d/
+
+This module generates a package containing configuration files and link it in /etc/fonts.
+
+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.
+
+*/
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.fonts.fontconfig;
+
+  fcBool = x: "<bool>" + (boolToString x) + "</bool>";
+  pkg = pkgs.fontconfig;
+
+  # configuration file to read fontconfig cache
+  # priority 0
+  cacheConf  = makeCacheConf {};
+
+  # generate the font cache setting file
+  # When cross-compiling, we can’t generate the cache, so we skip the
+  # <cachedir> part. fontconfig still works but is a little slower in
+  # looking things up.
+  makeCacheConf = { }:
+    let
+      makeCache = fontconfig: pkgs.makeFontsCache { inherit fontconfig; fontDirectories = config.fonts.fonts; };
+      cache     = makeCache pkgs.fontconfig;
+      cache32   = makeCache pkgs.pkgsi686Linux.fontconfig;
+    in
+    pkgs.writeText "fc-00-nixos-cache.conf" ''
+      <?xml version='1.0'?>
+      <!DOCTYPE fontconfig SYSTEM 'urn:fontconfig:fonts.dtd'>
+      <fontconfig>
+        <!-- Font directories -->
+        ${concatStringsSep "\n" (map (font: "<dir>${font}</dir>") config.fonts.fonts)}
+        ${optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
+        <!-- Pre-generated font caches -->
+        <cachedir>${cache}</cachedir>
+        ${optionalString (pkgs.stdenv.isx86_64 && cfg.cache32Bit) ''
+          <cachedir>${cache32}</cachedir>
+        ''}
+        ''}
+      </fontconfig>
+    '';
+
+  # rendering settings configuration file
+  # priority 10
+  renderConf = pkgs.writeText "fc-10-nixos-rendering.conf" ''
+    <?xml version='1.0'?>
+    <!DOCTYPE fontconfig SYSTEM 'urn:fontconfig:fonts.dtd'>
+    <fontconfig>
+
+      <!-- Default rendering settings -->
+      <match target="pattern">
+        <edit mode="append" name="hinting">
+          ${fcBool cfg.hinting.enable}
+        </edit>
+        <edit mode="append" name="autohint">
+          ${fcBool cfg.hinting.autohint}
+        </edit>
+        <edit mode="append" name="hintstyle">
+          <const>hintslight</const>
+        </edit>
+        <edit mode="append" name="antialias">
+          ${fcBool cfg.antialias}
+        </edit>
+        <edit mode="append" name="rgba">
+          <const>${cfg.subpixel.rgba}</const>
+        </edit>
+        <edit mode="append" name="lcdfilter">
+          <const>lcd${cfg.subpixel.lcdfilter}</const>
+        </edit>
+      </match>
+
+    </fontconfig>
+  '';
+
+  # local configuration file
+  localConf = pkgs.writeText "fc-local.conf" cfg.localConf;
+
+  # default fonts configuration file
+  # priority 52
+  defaultFontsConf =
+    let genDefault = fonts: name:
+      optionalString (fonts != []) ''
+        <alias binding="same">
+          <family>${name}</family>
+          <prefer>
+          ${concatStringsSep ""
+          (map (font: ''
+            <family>${font}</family>
+          '') fonts)}
+          </prefer>
+        </alias>
+      '';
+    in
+    pkgs.writeText "fc-52-nixos-default-fonts.conf" ''
+    <?xml version='1.0'?>
+    <!DOCTYPE fontconfig SYSTEM 'urn:fontconfig:fonts.dtd'>
+    <fontconfig>
+
+      <!-- Default fonts -->
+      ${genDefault cfg.defaultFonts.sansSerif "sans-serif"}
+
+      ${genDefault cfg.defaultFonts.serif     "serif"}
+
+      ${genDefault cfg.defaultFonts.monospace "monospace"}
+
+      ${genDefault cfg.defaultFonts.emoji "emoji"}
+
+    </fontconfig>
+  '';
+
+  # bitmap font options
+  # priority 53
+  rejectBitmaps = pkgs.writeText "fc-53-no-bitmaps.conf" ''
+    <?xml version="1.0"?>
+    <!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
+    <fontconfig>
+
+    ${optionalString (!cfg.allowBitmaps) ''
+    <!-- Reject bitmap fonts -->
+    <selectfont>
+      <rejectfont>
+        <pattern>
+          <patelt name="scalable"><bool>false</bool></patelt>
+        </pattern>
+      </rejectfont>
+    </selectfont>
+    ''}
+
+    <!-- Use embedded bitmaps in fonts like Calibri? -->
+    <match target="font">
+      <edit name="embeddedbitmap" mode="assign">
+        ${fcBool cfg.useEmbeddedBitmaps}
+      </edit>
+    </match>
+
+    </fontconfig>
+  '';
+
+  # reject Type 1 fonts
+  # priority 53
+  rejectType1 = pkgs.writeText "fc-53-nixos-reject-type1.conf" ''
+    <?xml version="1.0"?>
+    <!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
+    <fontconfig>
+
+    <!-- Reject Type 1 fonts -->
+    <selectfont>
+      <rejectfont>
+        <pattern>
+          <patelt name="fontformat"><string>Type 1</string></patelt>
+        </pattern>
+      </rejectfont>
+    </selectfont>
+
+    </fontconfig>
+  '';
+
+  # fontconfig configuration package
+  confPkg = pkgs.runCommand "fontconfig-conf" {
+    preferLocalBuild = true;
+  } ''
+    dst=$out/etc/fonts/conf.d
+    mkdir -p $dst
+
+    # fonts.conf
+    ln -s ${pkg.out}/etc/fonts/fonts.conf \
+          $dst/../fonts.conf
+    # TODO: remove this legacy symlink once people stop using packages built before #95358 was merged
+    mkdir -p $out/etc/fonts/2.11
+    ln -s /etc/fonts/fonts.conf \
+          $out/etc/fonts/2.11/fonts.conf
+
+    # fontconfig default config files
+    ln -s ${pkg.out}/etc/fonts/conf.d/*.conf \
+          $dst/
+
+    # 00-nixos-cache.conf
+    ln -s ${cacheConf}  $dst/00-nixos-cache.conf
+
+    # 10-nixos-rendering.conf
+    ln -s ${renderConf}       $dst/10-nixos-rendering.conf
+
+    # 50-user.conf
+    ${optionalString (!cfg.includeUserConf) ''
+    rm $dst/50-user.conf
+    ''}
+
+    # local.conf (indirect priority 51)
+    ${optionalString (cfg.localConf != "") ''
+    ln -s ${localConf}        $dst/../local.conf
+    ''}
+
+    # 52-nixos-default-fonts.conf
+    ln -s ${defaultFontsConf} $dst/52-nixos-default-fonts.conf
+
+    # 53-no-bitmaps.conf
+    ln -s ${rejectBitmaps} $dst/53-no-bitmaps.conf
+
+    ${optionalString (!cfg.allowType1) ''
+    # 53-nixos-reject-type1.conf
+    ln -s ${rejectType1} $dst/53-nixos-reject-type1.conf
+    ''}
+  '';
+
+  # Package with configuration files
+  # this merge all the packages in the fonts.fontconfig.confPackages list
+  fontconfigEtc = pkgs.buildEnv {
+    name  = "fontconfig-etc";
+    paths = cfg.confPackages;
+    ignoreCollisions = true;
+  };
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "fonts" "fontconfig" "ultimate" "allowBitmaps" ] [ "fonts" "fontconfig" "allowBitmaps" ])
+    (mkRenamedOptionModule [ "fonts" "fontconfig" "ultimate" "allowType1" ] [ "fonts" "fontconfig" "allowType1" ])
+    (mkRenamedOptionModule [ "fonts" "fontconfig" "ultimate" "useEmbeddedBitmaps" ] [ "fonts" "fontconfig" "useEmbeddedBitmaps" ])
+    (mkRenamedOptionModule [ "fonts" "fontconfig" "ultimate" "forceAutohint" ] [ "fonts" "fontconfig" "forceAutohint" ])
+    (mkRenamedOptionModule [ "fonts" "fontconfig" "ultimate" "renderMonoTTFAsBitmap" ] [ "fonts" "fontconfig" "renderMonoTTFAsBitmap" ])
+    (mkRemovedOptionModule [ "fonts" "fontconfig" "hinting" "style" ] "")
+    (mkRemovedOptionModule [ "fonts" "fontconfig" "forceAutohint" ] "")
+    (mkRemovedOptionModule [ "fonts" "fontconfig" "renderMonoTTFAsBitmap" ] "")
+    (mkRemovedOptionModule [ "fonts" "fontconfig" "dpi" ] "Use display server-specific options")
+  ] ++ lib.forEach [ "enable" "substitutions" "preset" ]
+     (opt: lib.mkRemovedOptionModule [ "fonts" "fontconfig" "ultimate" "${opt}" ] ''
+       The fonts.fontconfig.ultimate module and configuration is obsolete.
+       The repository has since been archived and activity has ceased.
+       https://github.com/bohoomil/fontconfig-ultimate/issues/171.
+       No action should be needed for font configuration, as the fonts.fontconfig
+       module is already used by default.
+     '');
+
+  options = {
+
+    fonts = {
+
+      fontconfig = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            If enabled, a Fontconfig configuration file will be built
+            pointing to a set of default fonts.  If you don't care about
+            running X11 applications or any other program that uses
+            Fontconfig, you can turn this option off and prevent a
+            dependency on all those fonts.
+          '';
+        };
+
+        confPackages = mkOption {
+          internal = true;
+          type     = with types; listOf path;
+          default  = [ ];
+          description = ''
+            Fontconfig configuration packages.
+          '';
+        };
+
+        antialias = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Enable font antialiasing. At high resolution (> 200 DPI),
+            antialiasing has no visible effect; users of such displays may want
+            to disable this option.
+          '';
+        };
+
+        localConf = mkOption {
+          type = types.lines;
+          default = "";
+          description = ''
+            System-wide customization file contents, has higher priority than
+            <literal>defaultFonts</literal> settings.
+          '';
+        };
+
+        defaultFonts = {
+          monospace = mkOption {
+            type = types.listOf types.str;
+            default = ["DejaVu Sans Mono"];
+            description = ''
+              System-wide default monospace font(s). Multiple fonts may be
+              listed in case multiple languages must be supported.
+            '';
+          };
+
+          sansSerif = mkOption {
+            type = types.listOf types.str;
+            default = ["DejaVu Sans"];
+            description = ''
+              System-wide default sans serif font(s). Multiple fonts may be
+              listed in case multiple languages must be supported.
+            '';
+          };
+
+          serif = mkOption {
+            type = types.listOf types.str;
+            default = ["DejaVu Serif"];
+            description = ''
+              System-wide default serif font(s). Multiple fonts may be listed
+              in case multiple languages must be supported.
+            '';
+          };
+
+          emoji = mkOption {
+            type = types.listOf types.str;
+            default = ["Noto Color Emoji"];
+            description = ''
+              System-wide default emoji font(s). Multiple fonts may be listed
+              in case a font does not support all emoji.
+
+              Note that fontconfig matches color emoji fonts preferentially,
+              so if you want to use a black and white font while having
+              a color font installed (eg. Noto Color Emoji installed alongside
+              Noto Emoji), fontconfig will still choose the color font even
+              when it is later in the list.
+            '';
+          };
+        };
+
+        hinting = {
+          enable = mkOption {
+            type = types.bool;
+            default = true;
+            description = ''
+              Enable font hinting. Hinting aligns glyphs to pixel boundaries to
+              improve rendering sharpness at low resolution. At high resolution
+              (> 200 dpi) hinting will do nothing (at best); users of such
+              displays may want to disable this option.
+            '';
+          };
+
+          autohint = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Enable the autohinter in place of the default interpreter.
+              The results are usually lower quality than correctly-hinted
+              fonts, but better than unhinted fonts.
+            '';
+          };
+        };
+
+        includeUserConf = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Include the user configuration from
+            <filename>~/.config/fontconfig/fonts.conf</filename> or
+            <filename>~/.config/fontconfig/conf.d</filename>.
+          '';
+        };
+
+        subpixel = {
+
+          rgba = mkOption {
+            default = "rgb";
+            type = types.enum ["rgb" "bgr" "vrgb" "vbgr" "none"];
+            description = ''
+              Subpixel order. The overwhelming majority of displays are
+              <literal>rgb</literal> in their normal orientation. Select
+              <literal>vrgb</literal> for mounting such a display 90 degrees
+              clockwise from its normal orientation or <literal>vbgr</literal>
+              for mounting 90 degrees counter-clockwise. Select
+              <literal>bgr</literal> in the unlikely event of mounting 180
+              degrees from the normal orientation. Reverse these directions in
+              the improbable event that the display's native subpixel order is
+              <literal>bgr</literal>.
+            '';
+          };
+
+          lcdfilter = mkOption {
+            default = "default";
+            type = types.enum ["none" "default" "light" "legacy"];
+            description = ''
+              FreeType LCD filter. At high resolution (> 200 DPI), LCD filtering
+              has no visible effect; users of such displays may want to select
+              <literal>none</literal>.
+            '';
+          };
+
+        };
+
+        cache32Bit = mkOption {
+          default = false;
+          type = types.bool;
+          description = ''
+            Generate system fonts cache for 32-bit applications.
+          '';
+        };
+
+        allowBitmaps = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Allow bitmap fonts. Set to <literal>false</literal> to ban all
+            bitmap fonts.
+          '';
+        };
+
+        allowType1 = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Allow Type-1 fonts. Default is <literal>false</literal> because of
+            poor rendering.
+          '';
+        };
+
+        useEmbeddedBitmaps = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Use embedded bitmaps in fonts like Calibri.";
+        };
+
+      };
+
+    };
+
+  };
+  config = mkMerge [
+    (mkIf cfg.enable {
+      environment.systemPackages    = [ pkgs.fontconfig ];
+      environment.etc.fonts.source  = "${fontconfigEtc}/etc/fonts/";
+      security.apparmor.includes."abstractions/fonts" = ''
+        # fonts.conf
+        r ${pkg.out}/etc/fonts/fonts.conf,
+
+        # fontconfig default config files
+        r ${pkg.out}/etc/fonts/conf.d/*.conf,
+
+        # 00-nixos-cache.conf
+        r ${cacheConf},
+
+        # 10-nixos-rendering.conf
+        r ${renderConf},
+
+        # 50-user.conf
+        ${optionalString cfg.includeUserConf ''
+        r ${pkg.out}/etc/fonts/conf.d.bak/50-user.conf,
+        ''}
+
+        # local.conf (indirect priority 51)
+        ${optionalString (cfg.localConf != "") ''
+        r ${localConf},
+        ''}
+
+        # 52-nixos-default-fonts.conf
+        r ${defaultFontsConf},
+
+        # 53-no-bitmaps.conf
+        r ${rejectBitmaps},
+
+        ${optionalString (!cfg.allowType1) ''
+        # 53-nixos-reject-type1.conf
+        r ${rejectType1},
+        ''}
+      '';
+    })
+    (mkIf cfg.enable {
+      fonts.fontconfig.confPackages = [ confPkg ];
+    })
+  ];
+
+}
diff --git a/nixos/modules/config/fonts/fontdir.nix b/nixos/modules/config/fonts/fontdir.nix
new file mode 100644
index 00000000000..560918302ca
--- /dev/null
+++ b/nixos/modules/config/fonts/fontdir.nix
@@ -0,0 +1,67 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.fonts.fontDir;
+
+  x11Fonts = pkgs.runCommand "X11-fonts" { preferLocalBuild = true; } ''
+    mkdir -p "$out/share/X11/fonts"
+    font_regexp='.*\.\(ttf\|ttc\|otf\|pcf\|pfa\|pfb\|bdf\)\(\.gz\)?'
+    find ${toString config.fonts.fonts} -regex "$font_regexp" \
+      -exec ln -sf -t "$out/share/X11/fonts" '{}' \;
+    cd "$out/share/X11/fonts"
+    ${optionalString cfg.decompressFonts ''
+      ${pkgs.gzip}/bin/gunzip -f *.gz
+    ''}
+    ${pkgs.xorg.mkfontscale}/bin/mkfontscale
+    ${pkgs.xorg.mkfontdir}/bin/mkfontdir
+    cat $(find ${pkgs.xorg.fontalias}/ -name fonts.alias) >fonts.alias
+  '';
+
+in
+
+{
+
+  options = {
+    fonts.fontDir = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to create a directory with links to all fonts in
+          <filename>/run/current-system/sw/share/X11/fonts</filename>.
+        '';
+      };
+
+      decompressFonts = mkOption {
+        type = types.bool;
+        default = config.programs.xwayland.enable;
+        defaultText = literalExpression "config.programs.xwayland.enable";
+        description = ''
+          Whether to decompress fonts in
+          <filename>/run/current-system/sw/share/X11/fonts</filename>.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ x11Fonts ];
+    environment.pathsToLink = [ "/share/X11/fonts" ];
+
+    services.xserver.filesSection = ''
+      FontPath "${x11Fonts}/share/X11/fonts"
+    '';
+
+  };
+
+  imports = [
+    (mkRenamedOptionModule [ "fonts" "enableFontDir" ] [ "fonts" "fontDir" "enable" ])
+  ];
+
+}
diff --git a/nixos/modules/config/fonts/fonts.nix b/nixos/modules/config/fonts/fonts.nix
new file mode 100644
index 00000000000..adc6654afc7
--- /dev/null
+++ b/nixos/modules/config/fonts/fonts.nix
@@ -0,0 +1,81 @@
+{ config, lib, pkgs, ... }:
+
+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;
+
+  defaultFonts =
+    [ pkgs.dejavu_fonts
+      pkgs.freefont_ttf
+      pkgs.gyre-fonts # TrueType substitutes for standard PostScript fonts
+      pkgs.liberation_ttf
+      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.")
+  ];
+
+  options = {
+
+    fonts = {
+
+      # TODO: find another name for it.
+      fonts = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        example = literalExpression "[ pkgs.dejavu_fonts ]";
+        description = "List of primary font paths.";
+      };
+
+      enableDefaultFonts = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable a basic set of fonts providing several font styles
+          and families and reasonable coverage of Unicode.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkMerge [
+    { fonts.fonts = mkIf config.fonts.enableDefaultFonts defaultFonts; }
+    { fonts.fonts = mkIf config.services.xserver.enable defaultXFonts; }
+  ];
+
+}
diff --git a/nixos/modules/config/fonts/ghostscript.nix b/nixos/modules/config/fonts/ghostscript.nix
new file mode 100644
index 00000000000..b1dd81bf2d2
--- /dev/null
+++ b/nixos/modules/config/fonts/ghostscript.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  options = {
+
+    fonts = {
+
+      enableGhostscriptFonts = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to add the fonts provided by Ghostscript (such as
+          various URW fonts and the “Base-14†Postscript fonts) to the
+          list of system fonts, making them available to X11
+          applications.
+        '';
+      };
+
+    };
+
+  };
+
+
+  config = mkIf config.fonts.enableGhostscriptFonts {
+
+    fonts.fonts = [ "${pkgs.ghostscript}/share/ghostscript/fonts" ];
+
+  };
+
+}
diff --git a/nixos/modules/config/gnu.nix b/nixos/modules/config/gnu.nix
new file mode 100644
index 00000000000..255d9741ba7
--- /dev/null
+++ b/nixos/modules/config/gnu.nix
@@ -0,0 +1,44 @@
+{ config, lib, pkgs, ... }:
+
+{
+  options = {
+    gnu = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = ''
+        When enabled, GNU software is chosen by default whenever a there is
+        a choice between GNU and non-GNU software (e.g., GNU lsh
+        vs. OpenSSH).
+      '';
+    };
+  };
+
+  config = lib.mkIf config.gnu {
+
+    environment.systemPackages = with pkgs;
+      # TODO: Adjust `requiredPackages' from `system-path.nix'.
+      # TODO: Add Inetutils once it has the new `ifconfig'.
+      [ parted
+        #fdisk  # XXX: GNU fdisk currently fails to build and it's redundant
+                # with the `parted' command.
+        nano zile
+        texinfo # for the stand-alone Info reader
+      ]
+      ++ lib.optional (!stdenv.isAarch32) grub2;
+
+
+    # GNU GRUB, where available.
+    boot.loader.grub.enable = !pkgs.stdenv.isAarch32;
+    boot.loader.grub.version = 2;
+
+    # GNU lsh.
+    services.openssh.enable = false;
+    services.lshd.enable = true;
+    programs.ssh.startAgent = false;
+    services.xserver.startGnuPGAgent = true;
+
+    # TODO: GNU dico.
+    # TODO: GNU Inetutils' inetd.
+    # TODO: GNU Pies.
+  };
+}
diff --git a/nixos/modules/config/gtk/gtk-icon-cache.nix b/nixos/modules/config/gtk/gtk-icon-cache.nix
new file mode 100644
index 00000000000..ff9aa7c6a04
--- /dev/null
+++ b/nixos/modules/config/gtk/gtk-icon-cache.nix
@@ -0,0 +1,87 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+{
+  options = {
+    gtk.iconCache.enable = mkOption {
+      type = types.bool;
+      default = config.services.xserver.enable;
+      defaultText = literalExpression "config.services.xserver.enable";
+      description = ''
+        Whether to build icon theme caches for GTK applications.
+      '';
+    };
+  };
+
+  config = mkIf config.gtk.iconCache.enable {
+
+    # (Re)build icon theme caches
+    # ---------------------------
+    # Each icon theme has its own cache. The difficult is that many
+    # packages may contribute with icons to the same theme by installing
+    # some icons.
+    #
+    # For instance, on my current NixOS system, the following packages
+    # (among many others) have icons installed into the hicolor icon
+    # theme: hicolor-icon-theme, psensor, wpa_gui, caja, etc.
+    #
+    # As another example, the mate icon theme has icons installed by the
+    # packages mate-icon-theme, mate-settings-daemon, and libmateweather.
+    #
+    # The HighContrast icon theme also has icons from different packages,
+    # like gnome-theme-extras and meld.
+
+    # When the cache is built all of its icons has to be known. How to
+    # implement this?
+    #
+    # I think that most themes have all icons installed by only one
+    # package. On my system there are 71 themes installed. Only 3 of them
+    # have icons installed from more than one package.
+    #
+    # If the main package of the theme provides a cache, presumably most
+    # of its icons will be available to applications without running this
+    # module. But additional icons offered by other packages will not be
+    # available. Therefore I think that it is good that the main theme
+    # package installs a cache (although it does not completely fixes the
+    # situation for packages installed with nix-env).
+    #
+    # The module solution presented here keeps the cache when there is
+    # only one package contributing with icons to the theme. Otherwise it
+    # rebuilds the cache taking into account the icons provided all
+    # packages.
+
+    environment.extraSetup = ''
+      # For each icon theme directory ...
+
+      find $out/share/icons -mindepth 1 -maxdepth 1 -print0 | while read -d $'\0' themedir
+      do
+
+        # In order to build the cache, the theme dir should be
+        # writable. When the theme dir is a symbolic link to somewhere
+        # in the nix store it is not writable and it means that only
+        # one package is contributing to the theme. If it already has
+        # a cache, no rebuild is needed. Otherwise a cache has to be
+        # built, and to be able to do that we first remove the
+        # symbolic link and make a directory, and then make symbolic
+        # links from the original directory into the new one.
+
+        if [ ! -w "$themedir" -a -L "$themedir" -a ! -r "$themedir"/icon-theme.cache ]; then
+          name=$(basename "$themedir")
+          path=$(readlink -f "$themedir")
+          rm "$themedir"
+          mkdir -p "$themedir"
+          ln -s "$path"/* "$themedir"/
+        fi
+
+        # (Re)build the cache if the theme dir is writable, replacing any
+        # existing cache for the theme
+
+        if [ -w "$themedir" ]; then
+          rm -f "$themedir"/icon-theme.cache
+          ${pkgs.buildPackages.gtk3.out}/bin/gtk-update-icon-cache --ignore-theme-index "$themedir"
+        fi
+      done
+    '';
+  };
+
+}
diff --git a/nixos/modules/config/i18n.nix b/nixos/modules/config/i18n.nix
new file mode 100644
index 00000000000..5b8d5b21449
--- /dev/null
+++ b/nixos/modules/config/i18n.nix
@@ -0,0 +1,97 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  ###### interface
+
+  options = {
+
+    i18n = {
+      glibcLocales = mkOption {
+        type = types.path;
+        default = pkgs.buildPackages.glibcLocales.override {
+          allLocales = any (x: x == "all") config.i18n.supportedLocales;
+          locales = config.i18n.supportedLocales;
+        };
+        defaultText = literalExpression ''
+          pkgs.buildPackages.glibcLocales.override {
+            allLocales = any (x: x == "all") config.i18n.supportedLocales;
+            locales = config.i18n.supportedLocales;
+          }
+        '';
+        example = literalExpression "pkgs.glibcLocales";
+        description = ''
+          Customized pkg.glibcLocales package.
+
+          Changing this option can disable handling of i18n.defaultLocale
+          and supportedLocale.
+        '';
+      };
+
+      defaultLocale = mkOption {
+        type = types.str;
+        default = "en_US.UTF-8";
+        example = "nl_NL.UTF-8";
+        description = ''
+          The default locale.  It determines the language for program
+          messages, the format for dates and times, sort order, and so on.
+          It also determines the character set, such as UTF-8.
+        '';
+      };
+
+      extraLocaleSettings = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        example = { LC_MESSAGES = "en_US.UTF-8"; LC_TIME = "de_DE.UTF-8"; };
+        description = ''
+          A set of additional system-wide locale settings other than
+          <literal>LANG</literal> which can be configured with
+          <option>i18n.defaultLocale</option>.
+        '';
+      };
+
+      supportedLocales = mkOption {
+        type = types.listOf types.str;
+        default = ["all"];
+        example = ["en_US.UTF-8/UTF-8" "nl_NL.UTF-8/UTF-8" "nl_NL/ISO-8859-1"];
+        description = ''
+          List of locales that the system should support.  The value
+          <literal>"all"</literal> means that all locales supported by
+          Glibc will be installed.  A full list of supported locales
+          can be found at <link
+          xlink:href="https://sourceware.org/git/?p=glibc.git;a=blob;f=localedata/SUPPORTED"/>.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = {
+
+    environment.systemPackages =
+      # We increase the priority a little, so that plain glibc in systemPackages can't win.
+      optional (config.i18n.supportedLocales != []) (lib.setPrio (-1) config.i18n.glibcLocales);
+
+    environment.sessionVariables =
+      { LANG = config.i18n.defaultLocale;
+        LOCALE_ARCHIVE = "/run/current-system/sw/lib/locale/locale-archive";
+      } // config.i18n.extraLocaleSettings;
+
+    systemd.globalEnvironment = mkIf (config.i18n.supportedLocales != []) {
+      LOCALE_ARCHIVE = "${config.i18n.glibcLocales}/lib/locale/locale-archive";
+    };
+
+    # ‘/etc/locale.conf’ is used by systemd.
+    environment.etc."locale.conf".source = pkgs.writeText "locale.conf"
+      ''
+        LANG=${config.i18n.defaultLocale}
+        ${concatStringsSep "\n" (mapAttrsToList (n: v: "${n}=${v}") config.i18n.extraLocaleSettings)}
+      '';
+
+  };
+}
diff --git a/nixos/modules/config/iproute2.nix b/nixos/modules/config/iproute2.nix
new file mode 100644
index 00000000000..5f41f3d21e4
--- /dev/null
+++ b/nixos/modules/config/iproute2.nix
@@ -0,0 +1,32 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.networking.iproute2;
+in
+{
+  options.networking.iproute2 = {
+    enable = mkEnableOption "copy IP route configuration files";
+    rttablesExtraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Verbatim lines to add to /etc/iproute2/rt_tables
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."iproute2/bpf_pinning" = { mode = "0644"; text = fileContents "${pkgs.iproute2}/etc/iproute2/bpf_pinning"; };
+    environment.etc."iproute2/ematch_map"  = { mode = "0644"; text = fileContents "${pkgs.iproute2}/etc/iproute2/ematch_map";  };
+    environment.etc."iproute2/group"       = { mode = "0644"; text = fileContents "${pkgs.iproute2}/etc/iproute2/group";       };
+    environment.etc."iproute2/nl_protos"   = { mode = "0644"; text = fileContents "${pkgs.iproute2}/etc/iproute2/nl_protos";   };
+    environment.etc."iproute2/rt_dsfield"  = { mode = "0644"; text = fileContents "${pkgs.iproute2}/etc/iproute2/rt_dsfield";  };
+    environment.etc."iproute2/rt_protos"   = { mode = "0644"; text = fileContents "${pkgs.iproute2}/etc/iproute2/rt_protos";   };
+    environment.etc."iproute2/rt_realms"   = { mode = "0644"; text = fileContents "${pkgs.iproute2}/etc/iproute2/rt_realms";   };
+    environment.etc."iproute2/rt_scopes"   = { mode = "0644"; text = fileContents "${pkgs.iproute2}/etc/iproute2/rt_scopes";   };
+    environment.etc."iproute2/rt_tables"   = { mode = "0644"; text = (fileContents "${pkgs.iproute2}/etc/iproute2/rt_tables")
+                                                                   + (optionalString (cfg.rttablesExtraConfig != "") "\n\n${cfg.rttablesExtraConfig}"); };
+  };
+}
diff --git a/nixos/modules/config/krb5/default.nix b/nixos/modules/config/krb5/default.nix
new file mode 100644
index 00000000000..911c5b629a9
--- /dev/null
+++ b/nixos/modules/config/krb5/default.nix
@@ -0,0 +1,369 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.krb5;
+
+  # This is to provide support for old configuration options (as much as is
+  # reasonable). This can be removed after 18.03 was released.
+  defaultConfig = {
+    libdefaults = optionalAttrs (cfg.defaultRealm != null)
+      { default_realm = cfg.defaultRealm; };
+
+    realms = optionalAttrs (lib.all (value: value != null) [
+      cfg.defaultRealm cfg.kdc cfg.kerberosAdminServer
+    ]) {
+      ${cfg.defaultRealm} = {
+        kdc = cfg.kdc;
+        admin_server = cfg.kerberosAdminServer;
+      };
+    };
+
+    domain_realm = optionalAttrs (lib.all (value: value != null) [
+      cfg.domainRealm cfg.defaultRealm
+    ]) {
+      ".${cfg.domainRealm}" = cfg.defaultRealm;
+      ${cfg.domainRealm} = cfg.defaultRealm;
+    };
+  };
+
+  mergedConfig = (recursiveUpdate defaultConfig {
+    inherit (config.krb5)
+      kerberos libdefaults realms domain_realm capaths appdefaults plugins
+      extraConfig config;
+  });
+
+  filterEmbeddedMetadata = value: if isAttrs value then
+    (filterAttrs
+      (attrName: attrValue: attrName != "_module" && attrValue != null)
+        value)
+    else value;
+
+  indent = "  ";
+
+  mkRelation = name: value:
+    if (isList value) then
+      concatMapStringsSep "\n" (mkRelation name) value
+    else "${name} = ${mkVal value}";
+
+  mkVal = value:
+    if (value == true) then "true"
+    else if (value == false) then "false"
+    else if (isInt value) then (toString value)
+    else if (isAttrs value) then
+      let configLines = concatLists
+        (map (splitString "\n")
+          (mapAttrsToList mkRelation value));
+      in
+      (concatStringsSep "\n${indent}"
+        ([ "{" ] ++ configLines))
+      + "\n}"
+    else value;
+
+  mkMappedAttrsOrString = value: concatMapStringsSep "\n"
+    (line: if builtins.stringLength line > 0
+      then "${indent}${line}"
+      else line)
+    (splitString "\n"
+      (if isAttrs value then
+        concatStringsSep "\n"
+            (mapAttrsToList mkRelation value)
+        else value));
+
+in {
+
+  ###### interface
+
+  options = {
+    krb5 = {
+      enable = mkEnableOption "building krb5.conf, configuration file for Kerberos V";
+
+      kerberos = mkOption {
+        type = types.package;
+        default = pkgs.krb5Full;
+        defaultText = literalExpression "pkgs.krb5Full";
+        example = literalExpression "pkgs.heimdal";
+        description = ''
+          The Kerberos implementation that will be present in
+          <literal>environment.systemPackages</literal> after enabling this
+          service.
+        '';
+      };
+
+      libdefaults = mkOption {
+        type = with types; either attrs lines;
+        default = {};
+        apply = attrs: filterEmbeddedMetadata attrs;
+        example = literalExpression ''
+          {
+            default_realm = "ATHENA.MIT.EDU";
+          };
+        '';
+        description = ''
+          Settings used by the Kerberos V5 library.
+        '';
+      };
+
+      realms = mkOption {
+        type = with types; either attrs lines;
+        default = {};
+        example = literalExpression ''
+          {
+            "ATHENA.MIT.EDU" = {
+              admin_server = "athena.mit.edu";
+              kdc = [
+                "athena01.mit.edu"
+                "athena02.mit.edu"
+              ];
+            };
+          };
+        '';
+        apply = attrs: filterEmbeddedMetadata attrs;
+        description = "Realm-specific contact information and settings.";
+      };
+
+      domain_realm = mkOption {
+        type = with types; either attrs lines;
+        default = {};
+        example = literalExpression ''
+          {
+            "example.com" = "EXAMPLE.COM";
+            ".example.com" = "EXAMPLE.COM";
+          };
+        '';
+        apply = attrs: filterEmbeddedMetadata attrs;
+        description = ''
+          Map of server hostnames to Kerberos realms.
+        '';
+      };
+
+      capaths = mkOption {
+        type = with types; either attrs lines;
+        default = {};
+        example = literalExpression ''
+          {
+            "ATHENA.MIT.EDU" = {
+              "EXAMPLE.COM" = ".";
+            };
+            "EXAMPLE.COM" = {
+              "ATHENA.MIT.EDU" = ".";
+            };
+          };
+        '';
+        apply = attrs: filterEmbeddedMetadata attrs;
+        description = ''
+          Authentication paths for non-hierarchical cross-realm authentication.
+        '';
+      };
+
+      appdefaults = mkOption {
+        type = with types; either attrs lines;
+        default = {};
+        example = literalExpression ''
+          {
+            pam = {
+              debug = false;
+              ticket_lifetime = 36000;
+              renew_lifetime = 36000;
+              max_timeout = 30;
+              timeout_shift = 2;
+              initial_timeout = 1;
+            };
+          };
+        '';
+        apply = attrs: filterEmbeddedMetadata attrs;
+        description = ''
+          Settings used by some Kerberos V5 applications.
+        '';
+      };
+
+      plugins = mkOption {
+        type = with types; either attrs lines;
+        default = {};
+        example = literalExpression ''
+          {
+            ccselect = {
+              disable = "k5identity";
+            };
+          };
+        '';
+        apply = attrs: filterEmbeddedMetadata attrs;
+        description = ''
+          Controls plugin module registration.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = with types; nullOr lines;
+        default = null;
+        example = ''
+          [logging]
+            kdc          = SYSLOG:NOTICE
+            admin_server = SYSLOG:NOTICE
+            default      = SYSLOG:NOTICE
+        '';
+        description = ''
+          These lines go to the end of <literal>krb5.conf</literal> verbatim.
+          <literal>krb5.conf</literal> may include any of the relations that are
+          valid for <literal>kdc.conf</literal> (see <literal>man
+          kdc.conf</literal>), but it is not a recommended practice.
+        '';
+      };
+
+      config = mkOption {
+        type = with types; nullOr lines;
+        default = null;
+        example = ''
+          [libdefaults]
+            default_realm = EXAMPLE.COM
+
+          [realms]
+            EXAMPLE.COM = {
+              admin_server = kerberos.example.com
+              kdc = kerberos.example.com
+              default_principal_flags = +preauth
+            }
+
+          [domain_realm]
+            example.com  = EXAMPLE.COM
+            .example.com = EXAMPLE.COM
+
+          [logging]
+            kdc          = SYSLOG:NOTICE
+            admin_server = SYSLOG:NOTICE
+            default      = SYSLOG:NOTICE
+        '';
+        description = ''
+          Verbatim <literal>krb5.conf</literal> configuration.  Note that this
+          is mutually exclusive with configuration via
+          <literal>libdefaults</literal>, <literal>realms</literal>,
+          <literal>domain_realm</literal>, <literal>capaths</literal>,
+          <literal>appdefaults</literal>, <literal>plugins</literal> and
+          <literal>extraConfig</literal> configuration options.  Consult
+          <literal>man krb5.conf</literal> for documentation.
+        '';
+      };
+
+      defaultRealm = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        example = "ATHENA.MIT.EDU";
+        description = ''
+          DEPRECATED, please use
+          <literal>krb5.libdefaults.default_realm</literal>.
+        '';
+      };
+
+      domainRealm = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        example = "athena.mit.edu";
+        description = ''
+          DEPRECATED, please create a map of server hostnames to Kerberos realms
+          in <literal>krb5.domain_realm</literal>.
+        '';
+      };
+
+      kdc = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        example = "kerberos.mit.edu";
+        description = ''
+          DEPRECATED, please pass a <literal>kdc</literal> attribute to a realm
+          in <literal>krb5.realms</literal>.
+        '';
+      };
+
+      kerberosAdminServer = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        example = "kerberos.mit.edu";
+        description = ''
+          DEPRECATED, please pass an <literal>admin_server</literal> attribute
+          to a realm in <literal>krb5.realms</literal>.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ cfg.kerberos ];
+
+    environment.etc."krb5.conf".text = if isString cfg.config
+      then cfg.config
+      else (''
+        [libdefaults]
+        ${mkMappedAttrsOrString mergedConfig.libdefaults}
+
+        [realms]
+        ${mkMappedAttrsOrString mergedConfig.realms}
+
+        [domain_realm]
+        ${mkMappedAttrsOrString mergedConfig.domain_realm}
+
+        [capaths]
+        ${mkMappedAttrsOrString mergedConfig.capaths}
+
+        [appdefaults]
+        ${mkMappedAttrsOrString mergedConfig.appdefaults}
+
+        [plugins]
+        ${mkMappedAttrsOrString mergedConfig.plugins}
+      '' + optionalString (mergedConfig.extraConfig != null)
+          ("\n" + mergedConfig.extraConfig));
+
+    warnings = flatten [
+      (optional (cfg.defaultRealm != null) ''
+        The option krb5.defaultRealm is deprecated, please use
+        krb5.libdefaults.default_realm.
+      '')
+      (optional (cfg.domainRealm != null) ''
+        The option krb5.domainRealm is deprecated, please use krb5.domain_realm.
+      '')
+      (optional (cfg.kdc != null) ''
+        The option krb5.kdc is deprecated, please pass a kdc attribute to a
+        realm in krb5.realms.
+      '')
+      (optional (cfg.kerberosAdminServer != null) ''
+        The option krb5.kerberosAdminServer is deprecated, please pass an
+        admin_server attribute to a realm in krb5.realms.
+      '')
+    ];
+
+    assertions = [
+      { assertion = !((builtins.any (value: value != null) [
+            cfg.defaultRealm cfg.domainRealm cfg.kdc cfg.kerberosAdminServer
+          ]) && ((builtins.any (value: value != {}) [
+              cfg.libdefaults cfg.realms cfg.domain_realm cfg.capaths
+              cfg.appdefaults cfg.plugins
+            ]) || (builtins.any (value: value != null) [
+              cfg.config cfg.extraConfig
+            ])));
+        message = ''
+          Configuration of krb5.conf by deprecated options is mutually exclusive
+          with configuration by section.  Please migrate your config using the
+          attributes suggested in the warnings.
+        '';
+      }
+      { assertion = !(cfg.config != null
+          && ((builtins.any (value: value != {}) [
+              cfg.libdefaults cfg.realms cfg.domain_realm cfg.capaths
+              cfg.appdefaults cfg.plugins
+            ]) || (builtins.any (value: value != null) [
+              cfg.extraConfig cfg.defaultRealm cfg.domainRealm cfg.kdc
+              cfg.kerberosAdminServer
+            ])));
+        message = ''
+          Configuration of krb5.conf using krb.config is mutually exclusive with
+          configuration by section.  If you want to mix the two, you can pass
+          lines to any configuration section or lines to krb5.extraConfig.
+        '';
+      }
+    ];
+  };
+}
diff --git a/nixos/modules/config/ldap.nix b/nixos/modules/config/ldap.nix
new file mode 100644
index 00000000000..85cad8b93d8
--- /dev/null
+++ b/nixos/modules/config/ldap.nix
@@ -0,0 +1,303 @@
+{ config, lib, pkgs, ... }:
+
+with pkgs;
+with lib;
+
+let
+
+  cfg = config.users.ldap;
+
+  # Careful: OpenLDAP seems to be very picky about the indentation of
+  # this file.  Directives HAVE to start in the first column!
+  ldapConfig = {
+    target = "ldap.conf";
+    source = writeText "ldap.conf" ''
+      uri ${config.users.ldap.server}
+      base ${config.users.ldap.base}
+      timelimit ${toString config.users.ldap.timeLimit}
+      bind_timelimit ${toString config.users.ldap.bind.timeLimit}
+      bind_policy ${config.users.ldap.bind.policy}
+      ${optionalString config.users.ldap.useTLS ''
+        ssl start_tls
+      ''}
+      ${optionalString (config.users.ldap.bind.distinguishedName != "") ''
+        binddn ${config.users.ldap.bind.distinguishedName}
+      ''}
+      ${optionalString (cfg.extraConfig != "") cfg.extraConfig }
+    '';
+  };
+
+  nslcdConfig = writeText "nslcd.conf" ''
+    uri ${cfg.server}
+    base ${cfg.base}
+    timelimit ${toString cfg.timeLimit}
+    bind_timelimit ${toString cfg.bind.timeLimit}
+    ${optionalString (cfg.bind.distinguishedName != "")
+      "binddn ${cfg.bind.distinguishedName}" }
+    ${optionalString (cfg.daemon.rootpwmoddn != "")
+      "rootpwmoddn ${cfg.daemon.rootpwmoddn}" }
+    ${optionalString (cfg.daemon.extraConfig != "") cfg.daemon.extraConfig }
+  '';
+
+  # nslcd normally reads configuration from /etc/nslcd.conf.
+  # this file might contain secrets. We append those at runtime,
+  # so redirect its location to something more temporary.
+  nslcdWrapped = runCommand "nslcd-wrapped" { nativeBuildInputs = [ makeWrapper ]; } ''
+    mkdir -p $out/bin
+    makeWrapper ${nss_pam_ldapd}/sbin/nslcd $out/bin/nslcd \
+      --set LD_PRELOAD    "${pkgs.libredirect}/lib/libredirect.so" \
+      --set NIX_REDIRECTS "/etc/nslcd.conf=/run/nslcd/nslcd.conf"
+  '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    users.ldap = {
+
+      enable = mkEnableOption "authentication against an LDAP server";
+
+      loginPam = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to include authentication against LDAP in login PAM.";
+      };
+
+      nsswitch = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to include lookup against LDAP in NSS.";
+      };
+
+      server = mkOption {
+        type = types.str;
+        example = "ldap://ldap.example.org/";
+        description = "The URL of the LDAP server.";
+      };
+
+      base = mkOption {
+        type = types.str;
+        example = "dc=example,dc=org";
+        description = "The distinguished name of the search base.";
+      };
+
+      useTLS = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If enabled, use TLS (encryption) over an LDAP (port 389)
+          connection.  The alternative is to specify an LDAPS server (port
+          636) in <option>users.ldap.server</option> or to forego
+          security.
+        '';
+      };
+
+      timeLimit = mkOption {
+        default = 0;
+        type = types.int;
+        description = ''
+          Specifies the time limit (in seconds) to use when performing
+          searches. A value of zero (0), which is the default, is to
+          wait indefinitely for searches to be completed.
+        '';
+      };
+
+      daemon = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether to let the nslcd daemon (nss-pam-ldapd) handle the
+            LDAP lookups for NSS and PAM. This can improve performance,
+            and if you need to bind to the LDAP server with a password,
+            it increases security, since only the nslcd user needs to
+            have access to the bindpw file, not everyone that uses NSS
+            and/or PAM. If this option is enabled, a local nscd user is
+            created automatically, and the nslcd service is started
+            automatically when the network get up.
+          '';
+        };
+
+        extraConfig = mkOption {
+          default =  "";
+          type = types.lines;
+          description = ''
+            Extra configuration options that will be added verbatim at
+            the end of the nslcd configuration file (<literal>nslcd.conf(5)</literal>).
+          '' ;
+        } ;
+
+        rootpwmoddn = mkOption {
+          default = "";
+          example = "cn=admin,dc=example,dc=com";
+          type = types.str;
+          description = ''
+            The distinguished name to use to bind to the LDAP server
+            when the root user tries to modify a user's password.
+          '';
+        };
+
+        rootpwmodpwFile = mkOption {
+          default = "";
+          example = "/run/keys/nslcd.rootpwmodpw";
+          type = types.str;
+          description = ''
+            The path to a file containing the credentials with which to bind to
+            the LDAP server if the root user tries to change a user's password.
+          '';
+        };
+      };
+
+      bind = {
+        distinguishedName = mkOption {
+          default = "";
+          example = "cn=admin,dc=example,dc=com";
+          type = types.str;
+          description = ''
+            The distinguished name to bind to the LDAP server with. If this
+            is not specified, an anonymous bind will be done.
+          '';
+        };
+
+        passwordFile = mkOption {
+          default = "/etc/ldap/bind.password";
+          type = types.str;
+          description = ''
+            The path to a file containing the credentials to use when binding
+            to the LDAP server (if not binding anonymously).
+          '';
+        };
+
+        timeLimit = mkOption {
+          default = 30;
+          type = types.int;
+          description = ''
+            Specifies the time limit (in seconds) to use when connecting
+            to the directory server. This is distinct from the time limit
+            specified in <option>users.ldap.timeLimit</option> and affects
+            the initial server connection only.
+          '';
+        };
+
+        policy = mkOption {
+          default = "hard_open";
+          type = types.enum [ "hard_open" "hard_init" "soft" ];
+          description = ''
+            Specifies the policy to use for reconnecting to an unavailable
+            LDAP server. The default is <literal>hard_open</literal>, which
+            reconnects if opening the connection to the directory server
+            failed. By contrast, <literal>hard_init</literal> reconnects if
+            initializing the connection failed. Initializing may not
+            actually contact the directory server, and it is possible that
+            a malformed configuration file will trigger reconnection. If
+            <literal>soft</literal> is specified, then
+            <package>nss_ldap</package> will return immediately on server
+            failure. All hard reconnect policies block with exponential
+            backoff before retrying.
+          '';
+        };
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra configuration options that will be added verbatim at
+          the end of the ldap configuration file (<literal>ldap.conf(5)</literal>).
+          If <option>users.ldap.daemon</option> is enabled, this
+          configuration will not be used. In that case, use
+          <option>users.ldap.daemon.extraConfig</option> instead.
+        '' ;
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.etc = optionalAttrs (!cfg.daemon.enable) {
+      "ldap.conf" = ldapConfig;
+    };
+
+    system.activationScripts = mkIf (!cfg.daemon.enable) {
+      ldap = stringAfter [ "etc" "groups" "users" ] ''
+        if test -f "${cfg.bind.passwordFile}" ; then
+          umask 0077
+          conf="$(mktemp)"
+          printf 'bindpw %s\n' "$(cat ${cfg.bind.passwordFile})" |
+          cat ${ldapConfig.source} - >"$conf"
+          mv -fT "$conf" /etc/ldap.conf
+        fi
+      '';
+    };
+
+    system.nssModules = mkIf cfg.nsswitch (singleton (
+      if cfg.daemon.enable then nss_pam_ldapd else nss_ldap
+    ));
+
+    system.nssDatabases.group = optional cfg.nsswitch "ldap";
+    system.nssDatabases.passwd = optional cfg.nsswitch "ldap";
+    system.nssDatabases.shadow = optional cfg.nsswitch "ldap";
+
+    users = mkIf cfg.daemon.enable {
+      groups.nslcd = {
+        gid = config.ids.gids.nslcd;
+      };
+
+      users.nslcd = {
+        uid = config.ids.uids.nslcd;
+        description = "nslcd user.";
+        group = "nslcd";
+      };
+    };
+
+    systemd.services = mkIf cfg.daemon.enable {
+      nslcd = {
+        wantedBy = [ "multi-user.target" ];
+
+        preStart = ''
+          umask 0077
+          conf="$(mktemp)"
+          {
+            cat ${nslcdConfig}
+            test -z '${cfg.bind.distinguishedName}' -o ! -f '${cfg.bind.passwordFile}' ||
+            printf 'bindpw %s\n' "$(cat '${cfg.bind.passwordFile}')"
+            test -z '${cfg.daemon.rootpwmoddn}' -o ! -f '${cfg.daemon.rootpwmodpwFile}' ||
+            printf 'rootpwmodpw %s\n' "$(cat '${cfg.daemon.rootpwmodpwFile}')"
+          } >"$conf"
+          mv -fT "$conf" /run/nslcd/nslcd.conf
+        '';
+
+        restartTriggers = [
+          nslcdConfig
+          cfg.bind.passwordFile
+          cfg.daemon.rootpwmodpwFile
+        ];
+
+        serviceConfig = {
+          ExecStart = "${nslcdWrapped}/bin/nslcd";
+          Type = "forking";
+          Restart = "always";
+          User = "nslcd";
+          Group = "nslcd";
+          RuntimeDirectory = [ "nslcd" ];
+          PIDFile = "/run/nslcd/nslcd.pid";
+          AmbientCapabilities = "CAP_SYS_RESOURCE";
+        };
+      };
+
+    };
+
+  };
+
+  imports =
+    [ (mkRenamedOptionModule [ "users" "ldap" "bind" "password"] [ "users" "ldap" "bind" "passwordFile"])
+    ];
+}
diff --git a/nixos/modules/config/locale.nix b/nixos/modules/config/locale.nix
new file mode 100644
index 00000000000..6f056588187
--- /dev/null
+++ b/nixos/modules/config/locale.nix
@@ -0,0 +1,94 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  tzdir = "${pkgs.tzdata}/share/zoneinfo";
+  nospace  = str: filter (c: c == " ") (stringToCharacters str) == [];
+  timezone = types.nullOr (types.addCheck types.str nospace)
+    // { description = "null or string without spaces"; };
+
+  lcfg = config.location;
+
+in
+
+{
+  options = {
+
+    time = {
+
+      timeZone = mkOption {
+        default = null;
+        type = timezone;
+        example = "America/New_York";
+        description = ''
+          The time zone used when displaying times and dates. See <link
+          xlink:href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones"/>
+          for a comprehensive list of possible values for this setting.
+
+          If null, the timezone will default to UTC and can be set imperatively
+          using timedatectl.
+        '';
+      };
+
+      hardwareClockInLocalTime = mkOption {
+        default = false;
+        type = types.bool;
+        description = "If set, keep the hardware clock in local time instead of UTC.";
+      };
+
+    };
+
+    location = {
+
+      latitude = mkOption {
+        type = types.float;
+        description = ''
+          Your current latitude, between
+          <literal>-90.0</literal> and <literal>90.0</literal>. Must be provided
+          along with longitude.
+        '';
+      };
+
+      longitude = mkOption {
+        type = types.float;
+        description = ''
+          Your current longitude, between
+          between <literal>-180.0</literal> and <literal>180.0</literal>. Must be
+          provided along with latitude.
+        '';
+      };
+
+      provider = mkOption {
+        type = types.enum [ "manual" "geoclue2" ];
+        default = "manual";
+        description = ''
+          The location provider to use for determining your location. If set to
+          <literal>manual</literal> you must also provide latitude/longitude.
+        '';
+      };
+
+    };
+  };
+
+  config = {
+
+    environment.sessionVariables.TZDIR = "/etc/zoneinfo";
+
+    services.geoclue2.enable = mkIf (lcfg.provider == "geoclue2") true;
+
+    # This way services are restarted when tzdata changes.
+    systemd.globalEnvironment.TZDIR = tzdir;
+
+    systemd.services.systemd-timedated.environment = lib.optionalAttrs (config.time.timeZone != null) { NIXOS_STATIC_TIMEZONE = "1"; };
+
+    environment.etc = {
+      zoneinfo.source = tzdir;
+    } // lib.optionalAttrs (config.time.timeZone != null) {
+        localtime.source = "/etc/zoneinfo/${config.time.timeZone}";
+        localtime.mode = "direct-symlink";
+      };
+  };
+
+}
diff --git a/nixos/modules/config/malloc.nix b/nixos/modules/config/malloc.nix
new file mode 100644
index 00000000000..a3fed33afa1
--- /dev/null
+++ b/nixos/modules/config/malloc.nix
@@ -0,0 +1,117 @@
+{ config, lib, pkgs, ... }:
+with lib;
+
+let
+  cfg = config.environment.memoryAllocator;
+
+  # The set of alternative malloc(3) providers.
+  providers = {
+    graphene-hardened = {
+      libPath = "${pkgs.graphene-hardened-malloc}/lib/libhardened_malloc.so";
+      description = ''
+        An allocator designed to mitigate memory corruption attacks, such as
+        those caused by use-after-free bugs.
+      '';
+    };
+
+    jemalloc = {
+      libPath = "${pkgs.jemalloc}/lib/libjemalloc.so";
+      description = ''
+        A general purpose allocator that emphasizes fragmentation avoidance
+        and scalable concurrency support.
+      '';
+    };
+
+    scudo = let
+      platformMap = {
+        aarch64-linux = "aarch64";
+        x86_64-linux  = "x86_64";
+      };
+
+      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";
+      description = ''
+        A user-mode allocator based on LLVM Sanitizer’s CombinedAllocator,
+        which aims at providing additional mitigations against heap based
+        vulnerabilities, while maintaining good performance.
+      '';
+    };
+
+    mimalloc = {
+      libPath = "${pkgs.mimalloc}/lib/libmimalloc.so";
+      description = ''
+        A compact and fast general purpose allocator, which may
+        optionally be built with mitigations against various heap
+        vulnerabilities.
+      '';
+    };
+  };
+
+  providerConf = providers.${cfg.provider};
+
+  # An output that contains only the shared library, to avoid
+  # needlessly bloating the system closure
+  mallocLib = pkgs.runCommand "malloc-provider-${cfg.provider}"
+    rec {
+      preferLocalBuild = true;
+      allowSubstitutes = false;
+      origLibPath = providerConf.libPath;
+      libName = baseNameOf origLibPath;
+    }
+    ''
+      mkdir -p $out/lib
+      cp -L $origLibPath $out/lib/$libName
+    '';
+
+  # The full path to the selected provider shlib.
+  providerLibPath = "${mallocLib}/lib/${mallocLib.libName}";
+in
+
+{
+  meta = {
+    maintainers = [ maintainers.joachifm ];
+  };
+
+  options = {
+    environment.memoryAllocator.provider = mkOption {
+      type = types.enum ([ "libc" ] ++ attrNames providers);
+      default = "libc";
+      description = ''
+        The system-wide memory allocator.
+
+        Briefly, the system-wide memory allocator providers are:
+        <itemizedlist>
+        <listitem><para><literal>libc</literal>: the standard allocator provided by libc</para></listitem>
+        ${toString (mapAttrsToList
+            (name: value: "<listitem><para><literal>${name}</literal>: ${value.description}</para></listitem>")
+            providers)}
+        </itemizedlist>
+
+        <warning>
+        <para>
+        Selecting an alternative allocator (i.e., anything other than
+        <literal>libc</literal>) may result in instability, data loss,
+        and/or service failure.
+        </para>
+        </warning>
+      '';
+    };
+  };
+
+  config = mkIf (cfg.provider != "libc") {
+    environment.etc."ld-nix.so.preload".text = ''
+      ${providerLibPath}
+    '';
+    security.apparmor.includes = {
+      "abstractions/base" = ''
+        r /etc/ld-nix.so.preload,
+        r ${config.environment.etc."ld-nix.so.preload".source},
+        include "${pkgs.apparmorRulesFromClosure {
+            name = "mallocLib";
+            baseRules = ["mr $path/lib/**.so*"];
+          } [ mallocLib ] }"
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/config/networking.nix b/nixos/modules/config/networking.nix
new file mode 100644
index 00000000000..bebfeb352c0
--- /dev/null
+++ b/nixos/modules/config/networking.nix
@@ -0,0 +1,237 @@
+# /etc files related to networking, such as /etc/services.
+
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.networking;
+  opt = options.networking;
+
+  localhostMultiple = any (elem "localhost") (attrValues (removeAttrs cfg.hosts [ "127.0.0.1" "::1" ]));
+
+in
+
+{
+  imports = [
+    (mkRemovedOptionModule [ "networking" "hostConf" ] "Use environment.etc.\"host.conf\" instead.")
+  ];
+
+  options = {
+
+    networking.hosts = lib.mkOption {
+      type = types.attrsOf (types.listOf types.str);
+      example = literalExpression ''
+        {
+          "127.0.0.1" = [ "foo.bar.baz" ];
+          "192.168.0.2" = [ "fileserver.local" "nameserver.local" ];
+        };
+      '';
+      description = ''
+        Locally defined maps of hostnames to IP addresses.
+      '';
+    };
+
+    networking.hostFiles = lib.mkOption {
+      type = types.listOf types.path;
+      defaultText = literalDocBook "Hosts from <option>networking.hosts</option> and <option>networking.extraHosts</option>";
+      example = literalExpression ''[ "''${pkgs.my-blocklist-package}/share/my-blocklist/hosts" ]'';
+      description = ''
+        Files that should be concatenated together to form <filename>/etc/hosts</filename>.
+      '';
+    };
+
+    networking.extraHosts = lib.mkOption {
+      type = types.lines;
+      default = "";
+      example = "192.168.0.1 lanlocalhost";
+      description = ''
+        Additional verbatim entries to be appended to <filename>/etc/hosts</filename>.
+        For adding hosts from derivation results, use <option>networking.hostFiles</option> instead.
+      '';
+    };
+
+    networking.timeServers = mkOption {
+      default = [
+        "0.nixos.pool.ntp.org"
+        "1.nixos.pool.ntp.org"
+        "2.nixos.pool.ntp.org"
+        "3.nixos.pool.ntp.org"
+      ];
+      type = types.listOf types.str;
+      description = ''
+        The set of NTP servers from which to synchronise.
+      '';
+    };
+
+    networking.proxy = {
+
+      default = lib.mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          This option specifies the default value for httpProxy, httpsProxy, ftpProxy and rsyncProxy.
+        '';
+        example = "http://127.0.0.1:3128";
+      };
+
+      httpProxy = lib.mkOption {
+        type = types.nullOr types.str;
+        default = cfg.proxy.default;
+        defaultText = literalExpression "config.${opt.proxy.default}";
+        description = ''
+          This option specifies the http_proxy environment variable.
+        '';
+        example = "http://127.0.0.1:3128";
+      };
+
+      httpsProxy = lib.mkOption {
+        type = types.nullOr types.str;
+        default = cfg.proxy.default;
+        defaultText = literalExpression "config.${opt.proxy.default}";
+        description = ''
+          This option specifies the https_proxy environment variable.
+        '';
+        example = "http://127.0.0.1:3128";
+      };
+
+      ftpProxy = lib.mkOption {
+        type = types.nullOr types.str;
+        default = cfg.proxy.default;
+        defaultText = literalExpression "config.${opt.proxy.default}";
+        description = ''
+          This option specifies the ftp_proxy environment variable.
+        '';
+        example = "http://127.0.0.1:3128";
+      };
+
+      rsyncProxy = lib.mkOption {
+        type = types.nullOr types.str;
+        default = cfg.proxy.default;
+        defaultText = literalExpression "config.${opt.proxy.default}";
+        description = ''
+          This option specifies the rsync_proxy environment variable.
+        '';
+        example = "http://127.0.0.1:3128";
+      };
+
+      allProxy = lib.mkOption {
+        type = types.nullOr types.str;
+        default = cfg.proxy.default;
+        defaultText = literalExpression "config.${opt.proxy.default}";
+        description = ''
+          This option specifies the all_proxy environment variable.
+        '';
+        example = "http://127.0.0.1:3128";
+      };
+
+      noProxy = lib.mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          This option specifies the no_proxy environment variable.
+          If a default proxy is used and noProxy is null,
+          then noProxy will be set to 127.0.0.1,localhost.
+        '';
+        example = "127.0.0.1,localhost,.localdomain";
+      };
+
+      envVars = lib.mkOption {
+        type = types.attrs;
+        internal = true;
+        default = {};
+        description = ''
+          Environment variables used for the network proxy.
+        '';
+      };
+    };
+  };
+
+  config = {
+
+    assertions = [{
+      assertion = !localhostMultiple;
+      message = ''
+        `networking.hosts` maps "localhost" to something other than "127.0.0.1"
+        or "::1". This will break some applications. Please use
+        `networking.extraHosts` if you really want to add such a mapping.
+      '';
+    }];
+
+    # These entries are required for "hostname -f" and to resolve both the
+    # hostname and FQDN correctly:
+    networking.hosts = let
+      hostnames = # Note: The FQDN (canonical hostname) has to come first:
+        optional (cfg.hostName != "" && cfg.domain != null) "${cfg.hostName}.${cfg.domain}"
+        ++ optional (cfg.hostName != "") cfg.hostName; # Then the hostname (without the domain)
+    in {
+      "127.0.0.2" = hostnames;
+    } // optionalAttrs cfg.enableIPv6 {
+      "::1" = hostnames;
+    };
+
+    networking.hostFiles = let
+      # Note: localhostHosts has to appear first in /etc/hosts so that 127.0.0.1
+      # resolves back to "localhost" (as some applications assume) instead of
+      # the FQDN! By default "networking.hosts" also contains entries for the
+      # FQDN so that e.g. "hostname -f" works correctly.
+      localhostHosts = pkgs.writeText "localhost-hosts" ''
+        127.0.0.1 localhost
+        ${optionalString cfg.enableIPv6 "::1 localhost"}
+      '';
+      stringHosts =
+        let
+          oneToString = set: ip: ip + " " + concatStringsSep " " set.${ip} + "\n";
+          allToString = set: concatMapStrings (oneToString set) (attrNames set);
+        in pkgs.writeText "string-hosts" (allToString (filterAttrs (_: v: v != []) cfg.hosts));
+      extraHosts = pkgs.writeText "extra-hosts" cfg.extraHosts;
+    in mkBefore [ localhostHosts stringHosts extraHosts ];
+
+    environment.etc =
+      { # /etc/services: TCP/UDP port assignments.
+        services.source = pkgs.iana-etc + "/etc/services";
+
+        # /etc/protocols: IP protocol numbers.
+        protocols.source  = pkgs.iana-etc + "/etc/protocols";
+
+        # /etc/hosts: Hostname-to-IP mappings.
+        hosts.source = pkgs.concatText "hosts" cfg.hostFiles;
+
+        # /etc/netgroup: Network-wide groups.
+        netgroup.text = mkDefault "";
+
+        # /etc/host.conf: resolver configuration file
+        "host.conf".text = ''
+          multi on
+        '';
+
+      } // optionalAttrs (pkgs.stdenv.hostPlatform.libc == "glibc") {
+        # /etc/rpc: RPC program numbers.
+        rpc.source = pkgs.stdenv.cc.libc.out + "/etc/rpc";
+      };
+
+      networking.proxy.envVars =
+        optionalAttrs (cfg.proxy.default != null) {
+          # other options already fallback to proxy.default
+          no_proxy = "127.0.0.1,localhost";
+        } // optionalAttrs (cfg.proxy.httpProxy != null) {
+          http_proxy  = cfg.proxy.httpProxy;
+        } // optionalAttrs (cfg.proxy.httpsProxy != null) {
+          https_proxy = cfg.proxy.httpsProxy;
+        } // optionalAttrs (cfg.proxy.rsyncProxy != null) {
+          rsync_proxy = cfg.proxy.rsyncProxy;
+        } // optionalAttrs (cfg.proxy.ftpProxy != null) {
+          ftp_proxy   = cfg.proxy.ftpProxy;
+        } // optionalAttrs (cfg.proxy.allProxy != null) {
+          all_proxy   = cfg.proxy.allProxy;
+        } // optionalAttrs (cfg.proxy.noProxy != null) {
+          no_proxy    = cfg.proxy.noProxy;
+        };
+
+    # Install the proxy environment variables
+    environment.sessionVariables = cfg.proxy.envVars;
+
+  };
+
+}
diff --git a/nixos/modules/config/no-x-libs.nix b/nixos/modules/config/no-x-libs.nix
new file mode 100644
index 00000000000..14fe180d0bc
--- /dev/null
+++ b/nixos/modules/config/no-x-libs.nix
@@ -0,0 +1,44 @@
+# This module gets rid of all dependencies on X11 client libraries
+# (including fontconfig).
+
+{ config, lib, ... }:
+
+with lib;
+
+{
+  options = {
+    environment.noXlibs = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Switch off the options in the default configuration that
+        require X11 libraries. This includes client-side font
+        configuration and SSH forwarding of X11 authentication
+        in. Thus, you probably do not want to enable this option if
+        you want to run X11 programs on this machine via SSH.
+      '';
+    };
+  };
+
+  config = mkIf config.environment.noXlibs {
+    programs.ssh.setXAuthLocation = false;
+    security.pam.services.su.forwardXAuth = lib.mkForce false;
+
+    fonts.fontconfig.enable = false;
+
+    nixpkgs.overlays = singleton (const (super: {
+      cairo = super.cairo.override { x11Support = false; };
+      dbus = super.dbus.override { x11Support = false; };
+      beam = super.beam_nox;
+      networkmanager-fortisslvpn = super.networkmanager-fortisslvpn.override { withGnome = false; };
+      networkmanager-iodine = super.networkmanager-iodine.override { withGnome = false; };
+      networkmanager-l2tp = super.networkmanager-l2tp.override { withGnome = false; };
+      networkmanager-openconnect = super.networkmanager-openconnect.override { withGnome = false; };
+      networkmanager-openvpn = super.networkmanager-openvpn.override { withGnome = false; };
+      networkmanager-sstp = super.networkmanager-vpnc.override { withGnome = false; };
+      networkmanager-vpnc = super.networkmanager-vpnc.override { withGnome = false; };
+      gobject-introspection = super.gobject-introspection.override { x11Support = false; };
+      qemu = super.qemu.override { gtkSupport = false; spiceSupport = false; sdlSupport = false; };
+    }));
+  };
+}
diff --git a/nixos/modules/config/nsswitch.nix b/nixos/modules/config/nsswitch.nix
new file mode 100644
index 00000000000..91a36cef10e
--- /dev/null
+++ b/nixos/modules/config/nsswitch.nix
@@ -0,0 +1,133 @@
+# Configuration for the Name Service Switch (/etc/nsswitch.conf).
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  options = {
+
+    # NSS modules.  Hacky!
+    # Only works with nscd!
+    system.nssModules = mkOption {
+      type = types.listOf types.path;
+      internal = true;
+      default = [];
+      description = ''
+        Search path for NSS (Name Service Switch) modules.  This allows
+        several DNS resolution methods to be specified via
+        <filename>/etc/nsswitch.conf</filename>.
+      '';
+      apply = list:
+        {
+          inherit list;
+          path = makeLibraryPath list;
+        };
+    };
+
+    system.nssDatabases = {
+      passwd = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          List of passwd entries to configure in <filename>/etc/nsswitch.conf</filename>.
+
+          Note that "files" is always prepended while "systemd" is appended if nscd is enabled.
+
+          This option only takes effect if nscd is enabled.
+        '';
+        default = [];
+      };
+
+      group = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          List of group entries to configure in <filename>/etc/nsswitch.conf</filename>.
+
+          Note that "files" is always prepended while "systemd" is appended if nscd is enabled.
+
+          This option only takes effect if nscd is enabled.
+        '';
+        default = [];
+      };
+
+      shadow = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          List of shadow entries to configure in <filename>/etc/nsswitch.conf</filename>.
+
+          Note that "files" is always prepended.
+
+          This option only takes effect if nscd is enabled.
+        '';
+        default = [];
+      };
+
+      hosts = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          List of hosts entries to configure in <filename>/etc/nsswitch.conf</filename>.
+
+          Note that "files" is always prepended, and "dns" and "myhostname" are always appended.
+
+          This option only takes effect if nscd is enabled.
+        '';
+        default = [];
+      };
+
+      services = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          List of services entries to configure in <filename>/etc/nsswitch.conf</filename>.
+
+          Note that "files" is always prepended.
+
+          This option only takes effect if nscd is enabled.
+        '';
+        default = [];
+      };
+    };
+  };
+
+  imports = [
+    (mkRenamedOptionModule [ "system" "nssHosts" ] [ "system" "nssDatabases" "hosts" ])
+  ];
+
+  config = {
+    assertions = [
+      {
+        # Prevent users from disabling nscd, with nssModules being set.
+        # If disabling nscd is really necessary, it's still possible to opt out
+        # by forcing config.system.nssModules to [].
+        assertion = config.system.nssModules.path != "" -> config.services.nscd.enable;
+        message = "Loading NSS modules from system.nssModules (${config.system.nssModules.path}), requires services.nscd.enable being set to true.";
+      }
+    ];
+
+    # Name Service Switch configuration file.  Required by the C
+    # library.
+    environment.etc."nsswitch.conf".text = ''
+      passwd:    ${concatStringsSep " " config.system.nssDatabases.passwd}
+      group:     ${concatStringsSep " " config.system.nssDatabases.group}
+      shadow:    ${concatStringsSep " " config.system.nssDatabases.shadow}
+
+      hosts:     ${concatStringsSep " " config.system.nssDatabases.hosts}
+      networks:  files
+
+      ethers:    files
+      services:  ${concatStringsSep " " config.system.nssDatabases.services}
+      protocols: files
+      rpc:       files
+    '';
+
+    system.nssDatabases = {
+      passwd = mkBefore [ "files" ];
+      group = mkBefore [ "files" ];
+      shadow = mkBefore [ "files" ];
+      hosts = mkMerge [
+        (mkOrder 998 [ "files" ])
+        (mkOrder 1499 [ "dns" ])
+      ];
+      services = mkBefore [ "files" ];
+    };
+  };
+}
diff --git a/nixos/modules/config/power-management.nix b/nixos/modules/config/power-management.nix
new file mode 100644
index 00000000000..710842e1503
--- /dev/null
+++ b/nixos/modules/config/power-management.nix
@@ -0,0 +1,106 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.powerManagement;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    powerManagement = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description =
+          ''
+            Whether to enable power management.  This includes support
+            for suspend-to-RAM and powersave features on laptops.
+          '';
+      };
+
+      resumeCommands = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Commands executed after the system resumes from suspend-to-RAM.";
+      };
+
+      powerUpCommands = mkOption {
+        type = types.lines;
+        default = "";
+        example = literalExpression ''
+          "''${pkgs.hdparm}/sbin/hdparm -B 255 /dev/sda"
+        '';
+        description =
+          ''
+            Commands executed when the machine powers up.  That is,
+            they're executed both when the system first boots and when
+            it resumes from suspend or hibernation.
+          '';
+      };
+
+      powerDownCommands = mkOption {
+        type = types.lines;
+        default = "";
+        example = literalExpression ''
+          "''${pkgs.hdparm}/sbin/hdparm -B 255 /dev/sda"
+        '';
+        description =
+          ''
+            Commands executed when the machine powers down.  That is,
+            they're executed both when the system shuts down and when
+            it goes to suspend or hibernation.
+          '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.targets.post-resume = {
+      description = "Post-Resume Actions";
+      requires = [ "post-resume.service" ];
+      after = [ "post-resume.service" ];
+      wantedBy = [ "sleep.target" ];
+      unitConfig.StopWhenUnneeded = true;
+    };
+
+    # Service executed before suspending/hibernating.
+    systemd.services.pre-sleep =
+      { description = "Pre-Sleep Actions";
+        wantedBy = [ "sleep.target" ];
+        before = [ "sleep.target" ];
+        script =
+          ''
+            ${cfg.powerDownCommands}
+          '';
+        serviceConfig.Type = "oneshot";
+      };
+
+    systemd.services.post-resume =
+      { description = "Post-Resume Actions";
+        after = [ "suspend.target" "hibernate.target" "hybrid-sleep.target" ];
+        script =
+          ''
+            /run/current-system/systemd/bin/systemctl try-restart post-resume.target
+            ${cfg.resumeCommands}
+            ${cfg.powerUpCommands}
+          '';
+        serviceConfig.Type = "oneshot";
+      };
+
+  };
+
+}
diff --git a/nixos/modules/config/pulseaudio.nix b/nixos/modules/config/pulseaudio.nix
new file mode 100644
index 00000000000..01555d28b73
--- /dev/null
+++ b/nixos/modules/config/pulseaudio.nix
@@ -0,0 +1,331 @@
+{ config, lib, pkgs, ... }:
+
+with pkgs;
+with lib;
+
+let
+
+  cfg = config.hardware.pulseaudio;
+  alsaCfg = config.sound;
+
+  systemWide = cfg.enable && cfg.systemWide;
+  nonSystemWide = cfg.enable && !cfg.systemWide;
+  hasZeroconf = let z = cfg.zeroconf; in z.publish.enable || z.discovery.enable;
+
+  overriddenPackage = cfg.package.override
+    (optionalAttrs hasZeroconf { zeroconfSupport = true; });
+  binary = "${getBin overriddenPackage}/bin/pulseaudio";
+  binaryNoDaemon = "${binary} --daemonize=no";
+
+  # Forces 32bit pulseaudio and alsa-plugins to be built/supported for apps
+  # using 32bit alsa on 64bit linux.
+  enable32BitAlsaPlugins = cfg.support32Bit && stdenv.isx86_64 && (pkgs.pkgsi686Linux.alsa-lib != null && pkgs.pkgsi686Linux.libpulseaudio != null);
+
+
+  myConfigFile =
+    let
+      addModuleIf = cond: mod: optionalString cond "load-module ${mod}";
+      allAnon = optional cfg.tcp.anonymousClients.allowAll "auth-anonymous=1";
+      ipAnon =  let a = cfg.tcp.anonymousClients.allowedIpRanges;
+                in optional (a != []) ''auth-ip-acl=${concatStringsSep ";" a}'';
+    in writeTextFile {
+      name = "default.pa";
+        text = ''
+        .include ${cfg.configFile}
+        ${addModuleIf cfg.zeroconf.publish.enable "module-zeroconf-publish"}
+        ${addModuleIf cfg.zeroconf.discovery.enable "module-zeroconf-discover"}
+        ${addModuleIf cfg.tcp.enable (concatStringsSep " "
+           ([ "module-native-protocol-tcp" ] ++ allAnon ++ ipAnon))}
+        ${addModuleIf config.services.jack.jackd.enable "module-jack-sink"}
+        ${addModuleIf config.services.jack.jackd.enable "module-jack-source"}
+        ${cfg.extraConfig}
+      '';
+    };
+
+  ids = config.ids;
+
+  uid = ids.uids.pulseaudio;
+  gid = ids.gids.pulseaudio;
+
+  stateDir = "/run/pulse";
+
+  # Create pulse/client.conf even if PulseAudio is disabled so
+  # that we can disable the autospawn feature in programs that
+  # are built with PulseAudio support (like KDE).
+  clientConf = writeText "client.conf" ''
+    autospawn=no
+    ${cfg.extraClientConf}
+  '';
+
+  # Write an /etc/asound.conf that causes all ALSA applications to
+  # be re-routed to the PulseAudio server through ALSA's Pulse
+  # plugin.
+  alsaConf = writeText "asound.conf" (''
+    pcm_type.pulse {
+      libs.native = ${pkgs.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_pulse.so ;
+      ${lib.optionalString enable32BitAlsaPlugins
+     "libs.32Bit = ${pkgs.pkgsi686Linux.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_pulse.so ;"}
+    }
+    pcm.!default {
+      type pulse
+      hint.description "Default Audio Device (via PulseAudio)"
+    }
+    ctl_type.pulse {
+      libs.native = ${pkgs.alsa-plugins}/lib/alsa-lib/libasound_module_ctl_pulse.so ;
+      ${lib.optionalString enable32BitAlsaPlugins
+     "libs.32Bit = ${pkgs.pkgsi686Linux.alsa-plugins}/lib/alsa-lib/libasound_module_ctl_pulse.so ;"}
+    }
+    ctl.!default {
+      type pulse
+    }
+    ${alsaCfg.extraConfig}
+  '');
+
+in {
+
+  options = {
+
+    hardware.pulseaudio = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the PulseAudio sound server.
+        '';
+      };
+
+      systemWide = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If false, a PulseAudio server is launched automatically for
+          each user that tries to use the sound system. The server runs
+          with user privileges. If true, one system-wide PulseAudio
+          server is launched on boot, running as the user "pulse", and
+          only users in the "audio" group will have access to the server.
+          Please read the PulseAudio documentation for more details.
+
+          Don't enable this option unless you know what you are doing.
+        '';
+      };
+
+      support32Bit = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to include the 32-bit pulseaudio libraries in the system or not.
+          This is only useful on 64-bit systems and currently limited to x86_64-linux.
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.nullOr types.path;
+        description = ''
+          The path to the default configuration options the PulseAudio server
+          should use. By default, the "default.pa" configuration
+          from the PulseAudio distribution is used.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Literal string to append to <literal>configFile</literal>
+          and the config file generated by the pulseaudio module.
+        '';
+      };
+
+      extraClientConf = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration appended to pulse/client.conf file.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = if config.services.jack.jackd.enable
+                  then pkgs.pulseaudioFull
+                  else pkgs.pulseaudio;
+        defaultText = literalExpression "pkgs.pulseaudio";
+        example = literalExpression "pkgs.pulseaudioFull";
+        description = ''
+          The PulseAudio derivation to use.  This can be used to enable
+          features (such as JACK support, Bluetooth) via the
+          <literal>pulseaudioFull</literal> package.
+        '';
+      };
+
+      extraModules = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "[ pkgs.pulseaudio-modules-bt ]";
+        description = ''
+          Extra pulseaudio modules to use. This is intended for out-of-tree
+          pulseaudio modules like extra bluetooth codecs.
+
+          Extra modules take precedence over built-in pulseaudio modules.
+        '';
+      };
+
+      daemon = {
+        logLevel = mkOption {
+          type = types.str;
+          default = "notice";
+          description = ''
+            The log level that the system-wide pulseaudio daemon should use,
+            if activated.
+          '';
+        };
+
+        config = mkOption {
+          type = types.attrsOf types.unspecified;
+          default = {};
+          description = "Config of the pulse daemon. See <literal>man pulse-daemon.conf</literal>.";
+          example = literalExpression ''{ realtime-scheduling = "yes"; }'';
+        };
+      };
+
+      zeroconf = {
+        discovery.enable =
+          mkEnableOption "discovery of pulseaudio sinks in the local network";
+        publish.enable =
+          mkEnableOption "publishing the pulseaudio sink in the local network";
+      };
+
+      # TODO: enable by default?
+      tcp = {
+        enable = mkEnableOption "tcp streaming support";
+
+        anonymousClients = {
+          allowAll = mkEnableOption "all anonymous clients to stream to the server";
+          allowedIpRanges = mkOption {
+            type = types.listOf types.str;
+            default = [];
+            example = literalExpression ''[ "127.0.0.1" "192.168.1.0/24" ]'';
+            description = ''
+              A list of IP subnets that are allowed to stream to the server.
+            '';
+          };
+        };
+      };
+
+    };
+
+  };
+
+
+  config = mkMerge [
+    {
+      environment.etc = {
+        "pulse/client.conf".source = clientConf;
+      };
+
+      hardware.pulseaudio.configFile = mkDefault "${getBin overriddenPackage}/etc/pulse/default.pa";
+    }
+
+    (mkIf cfg.enable {
+      environment.systemPackages = [ overriddenPackage ];
+
+      sound.enable = true;
+
+      environment.etc = {
+        "asound.conf".source = alsaConf;
+
+        "pulse/daemon.conf".source = writeText "daemon.conf"
+          (lib.generators.toKeyValue {} cfg.daemon.config);
+
+        "openal/alsoft.conf".source = writeText "alsoft.conf" "drivers=pulse";
+
+        "libao.conf".source = writeText "libao.conf" "default_driver=pulse";
+      };
+
+      # Disable flat volumes to enable relative ones
+      hardware.pulseaudio.daemon.config.flat-volumes = mkDefault "no";
+
+      # Upstream defaults to speex-float-1 which results in audible artifacts
+      hardware.pulseaudio.daemon.config.resample-method = mkDefault "speex-float-5";
+
+      # Allow PulseAudio to get realtime priority using rtkit.
+      security.rtkit.enable = true;
+
+      systemd.packages = [ overriddenPackage ];
+
+      # PulseAudio is packaged with udev rules to handle various audio device quirks
+      services.udev.packages = [ overriddenPackage ];
+    })
+
+    (mkIf (cfg.extraModules != []) {
+      hardware.pulseaudio.daemon.config.dl-search-path = let
+        overriddenModules = builtins.map
+          (drv: drv.override { pulseaudio = overriddenPackage; })
+          cfg.extraModules;
+        modulePaths = builtins.map
+          (drv: "${drv}/${overriddenPackage.pulseDir}/modules")
+          # User-provided extra modules take precedence
+          (overriddenModules ++ [ overriddenPackage ]);
+      in lib.concatStringsSep ":" modulePaths;
+    })
+
+    (mkIf hasZeroconf {
+      services.avahi.enable = true;
+    })
+    (mkIf cfg.zeroconf.publish.enable {
+      services.avahi.publish.enable = true;
+      services.avahi.publish.userServices = true;
+    })
+
+    (mkIf nonSystemWide {
+      environment.etc = {
+        "pulse/default.pa".source = myConfigFile;
+      };
+      systemd.user = {
+        services.pulseaudio = {
+          restartIfChanged = true;
+          serviceConfig = {
+            RestartSec = "500ms";
+            PassEnvironment = "DISPLAY";
+          };
+        } // optionalAttrs config.services.jack.jackd.enable {
+          environment.JACK_PROMISCUOUS_SERVER = "jackaudio";
+        };
+        sockets.pulseaudio = {
+          wantedBy = [ "sockets.target" ];
+        };
+      };
+    })
+
+    (mkIf systemWide {
+      users.users.pulse = {
+        # For some reason, PulseAudio wants UID == GID.
+        uid = assert uid == gid; uid;
+        group = "pulse";
+        extraGroups = [ "audio" ];
+        description = "PulseAudio system service user";
+        home = stateDir;
+        createHome = true;
+        isSystemUser = true;
+      };
+
+      users.groups.pulse.gid = gid;
+
+      systemd.services.pulseaudio = {
+        description = "PulseAudio System-Wide Server";
+        wantedBy = [ "sound.target" ];
+        before = [ "sound.target" ];
+        environment.PULSE_RUNTIME_PATH = stateDir;
+        serviceConfig = {
+          Type = "notify";
+          ExecStart = "${binaryNoDaemon} --log-level=${cfg.daemon.logLevel} --system -n --file=${myConfigFile}";
+          Restart = "on-failure";
+          RestartSec = "500ms";
+        };
+      };
+
+      environment.variables.PULSE_COOKIE = "${stateDir}/.config/pulse/cookie";
+    })
+  ];
+
+}
diff --git a/nixos/modules/config/qt5.nix b/nixos/modules/config/qt5.nix
new file mode 100644
index 00000000000..eabba9ad95f
--- /dev/null
+++ b/nixos/modules/config/qt5.nix
@@ -0,0 +1,104 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.qt5;
+
+  isQGnome = cfg.platformTheme == "gnome" && builtins.elem cfg.style ["adwaita" "adwaita-dark"];
+  isQtStyle = cfg.platformTheme == "gtk2" && !(builtins.elem cfg.style ["adwaita" "adwaita-dark"]);
+
+  packages = if isQGnome then [ pkgs.qgnomeplatform pkgs.adwaita-qt ]
+    else if isQtStyle then [ pkgs.libsForQt5.qtstyleplugins ]
+    else throw "`qt5.platformTheme` ${cfg.platformTheme} and `qt5.style` ${cfg.style} are not compatible.";
+
+in
+
+{
+
+  options = {
+    qt5 = {
+
+      enable = mkEnableOption "Qt5 theming configuration";
+
+      platformTheme = mkOption {
+        type = types.enum [
+          "gtk2"
+          "gnome"
+        ];
+        example = "gnome";
+        relatedPackages = [
+          "qgnomeplatform"
+          ["libsForQt5" "qtstyleplugins"]
+        ];
+        description = ''
+          Selects the platform theme to use for Qt5 applications.</para>
+          <para>The options are
+          <variablelist>
+            <varlistentry>
+              <term><literal>gtk</literal></term>
+              <listitem><para>Use GTK theme with
+                <link xlink:href="https://github.com/qt/qtstyleplugins">qtstyleplugins</link>
+              </para></listitem>
+            </varlistentry>
+            <varlistentry>
+              <term><literal>gnome</literal></term>
+              <listitem><para>Use GNOME theme with
+                <link xlink:href="https://github.com/FedoraQt/QGnomePlatform">qgnomeplatform</link>
+              </para></listitem>
+            </varlistentry>
+          </variablelist>
+        '';
+      };
+
+      style = mkOption {
+        type = types.enum [
+          "adwaita"
+          "adwaita-dark"
+          "cleanlooks"
+          "gtk2"
+          "motif"
+          "plastique"
+        ];
+        example = "adwaita";
+        relatedPackages = [
+          "adwaita-qt"
+          ["libsForQt5" "qtstyleplugins"]
+        ];
+        description = ''
+          Selects the style to use for Qt5 applications.</para>
+          <para>The options are
+          <variablelist>
+            <varlistentry>
+              <term><literal>adwaita</literal></term>
+              <term><literal>adwaita-dark</literal></term>
+              <listitem><para>Use Adwaita Qt style with
+                <link xlink:href="https://github.com/FedoraQt/adwaita-qt">adwaita</link>
+              </para></listitem>
+            </varlistentry>
+            <varlistentry>
+              <term><literal>cleanlooks</literal></term>
+              <term><literal>gtk2</literal></term>
+              <term><literal>motif</literal></term>
+              <term><literal>plastique</literal></term>
+              <listitem><para>Use styles from
+                <link xlink:href="https://github.com/qt/qtstyleplugins">qtstyleplugins</link>
+              </para></listitem>
+            </varlistentry>
+          </variablelist>
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.variables.QT_QPA_PLATFORMTHEME = cfg.platformTheme;
+
+    environment.variables.QT_STYLE_OVERRIDE = cfg.style;
+
+    environment.systemPackages = packages;
+
+  };
+}
diff --git a/nixos/modules/config/resolvconf.nix b/nixos/modules/config/resolvconf.nix
new file mode 100644
index 00000000000..4499481811f
--- /dev/null
+++ b/nixos/modules/config/resolvconf.nix
@@ -0,0 +1,145 @@
+# /etc files related to networking, such as /etc/services.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.networking.resolvconf;
+
+  resolvconfOptions = cfg.extraOptions
+    ++ optional cfg.dnsSingleRequest "single-request"
+    ++ optional cfg.dnsExtensionMechanism "edns0";
+
+  configText =
+    ''
+      # This is the default, but we must set it here to prevent
+      # a collision with an apparently unrelated environment
+      # variable with the same name exported by dhcpcd.
+      interface_order='lo lo[0-9]*'
+    '' + optionalString config.services.nscd.enable ''
+      # Invalidate the nscd cache whenever resolv.conf is
+      # regenerated.
+      libc_restart='/run/current-system/systemd/bin/systemctl try-restart --no-block nscd.service 2> /dev/null'
+    '' + optionalString (length resolvconfOptions > 0) ''
+      # Options as described in resolv.conf(5)
+      resolv_conf_options='${concatStringsSep " " resolvconfOptions}'
+    '' + optionalString cfg.useLocalResolver ''
+      # This hosts runs a full-blown DNS resolver.
+      name_servers='127.0.0.1'
+    '' + cfg.extraConfig;
+
+in
+
+{
+  imports = [
+    (mkRenamedOptionModule [ "networking" "dnsSingleRequest" ] [ "networking" "resolvconf" "dnsSingleRequest" ])
+    (mkRenamedOptionModule [ "networking" "dnsExtensionMechanism" ] [ "networking" "resolvconf" "dnsExtensionMechanism" ])
+    (mkRenamedOptionModule [ "networking" "extraResolvconfConf" ] [ "networking" "resolvconf" "extraConfig" ])
+    (mkRenamedOptionModule [ "networking" "resolvconfOptions" ] [ "networking" "resolvconf" "extraOptions" ])
+    (mkRemovedOptionModule [ "networking" "resolvconf" "useHostResolvConf" ] "This option was never used for anything anyways")
+  ];
+
+  options = {
+
+    networking.resolvconf = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = !(config.environment.etc ? "resolv.conf");
+        defaultText = literalExpression ''!(config.environment.etc ? "resolv.conf")'';
+        description = ''
+          DNS configuration is managed by resolvconf.
+        '';
+      };
+
+      dnsSingleRequest = lib.mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Recent versions of glibc will issue both ipv4 (A) and ipv6 (AAAA)
+          address queries at the same time, from the same port. Sometimes upstream
+          routers will systemically drop the ipv4 queries. The symptom of this problem is
+          that 'getent hosts example.com' only returns ipv6 (or perhaps only ipv4) addresses. The
+          workaround for this is to specify the option 'single-request' in
+          /etc/resolv.conf. This option enables that.
+        '';
+      };
+
+      dnsExtensionMechanism = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable the <code>edns0</code> option in <filename>resolv.conf</filename>. With
+          that option set, <code>glibc</code> supports use of the extension mechanisms for
+          DNS (EDNS) specified in RFC 2671. The most popular user of that feature is DNSSEC,
+          which does not work without it.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = "libc=NO";
+        description = ''
+          Extra configuration to append to <filename>resolvconf.conf</filename>.
+        '';
+      };
+
+      extraOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "ndots:1" "rotate" ];
+        description = ''
+          Set the options in <filename>/etc/resolv.conf</filename>.
+        '';
+      };
+
+      useLocalResolver = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Use local DNS server for resolving.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkMerge [
+    {
+      environment.etc."resolvconf.conf".text =
+        if !cfg.enable then
+          # Force-stop any attempts to use resolvconf
+          ''
+            echo "resolvconf is disabled on this system but was used anyway:" >&2
+            echo "$0 $*" >&2
+            exit 1
+          ''
+        else configText;
+    }
+
+    (mkIf cfg.enable {
+      environment.systemPackages = [ pkgs.openresolv ];
+
+      systemd.services.resolvconf = {
+        description = "resolvconf update";
+
+        before = [ "network-pre.target" ];
+        wants = [ "network-pre.target" ];
+        wantedBy = [ "multi-user.target" ];
+        restartTriggers = [ config.environment.etc."resolvconf.conf".source ];
+
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = "${pkgs.openresolv}/bin/resolvconf -u";
+          RemainAfterExit = true;
+        };
+      };
+
+    })
+  ];
+
+}
diff --git a/nixos/modules/config/shells-environment.nix b/nixos/modules/config/shells-environment.nix
new file mode 100644
index 00000000000..ae3f618e273
--- /dev/null
+++ b/nixos/modules/config/shells-environment.nix
@@ -0,0 +1,224 @@
+# This module defines a global environment configuration and
+# a common configuration for all shells.
+
+{ config, lib, utils, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.environment;
+
+  exportedEnvVars =
+    let
+      absoluteVariables =
+        mapAttrs (n: toList) cfg.variables;
+
+      suffixedVariables =
+        flip mapAttrs cfg.profileRelativeEnvVars (envVar: listSuffixes:
+          concatMap (profile: map (suffix: "${profile}${suffix}") listSuffixes) cfg.profiles
+        );
+
+      allVariables =
+        zipAttrsWith (n: concatLists) [ absoluteVariables suffixedVariables ];
+
+      exportVariables =
+        mapAttrsToList (n: v: ''export ${n}="${concatStringsSep ":" v}"'') allVariables;
+    in
+      concatStringsSep "\n" exportVariables;
+in
+
+{
+
+  options = {
+
+    environment.variables = mkOption {
+      default = {};
+      example = { EDITOR = "nvim"; VISUAL = "nvim"; };
+      description = ''
+        A set of environment variables used in the global environment.
+        These variables will be set on shell initialisation (e.g. in /etc/profile).
+        The value of each variable can be either a string or a list of
+        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);
+    };
+
+    environment.profiles = mkOption {
+      default = [];
+      description = ''
+        A list of profiles used to setup the global environment.
+      '';
+      type = types.listOf types.str;
+    };
+
+    environment.profileRelativeEnvVars = mkOption {
+      type = types.attrsOf (types.listOf types.str);
+      example = { PATH = [ "/bin" ]; MANPATH = [ "/man" "/share/man" ]; };
+      description = ''
+        Attribute set of environment variable.  Each attribute maps to a list
+        of relative paths.  Each relative path is appended to the each profile
+        of <option>environment.profiles</option> to form the content of the
+        corresponding environment variable.
+      '';
+    };
+
+    # !!! isn't there a better way?
+    environment.extraInit = mkOption {
+      default = "";
+      description = ''
+        Shell script code called during global environment initialisation
+        after all variables and profileVariables have been set.
+        This code is assumed to be shell-independent, which means you should
+        stick to pure sh without sh word split.
+      '';
+      type = types.lines;
+    };
+
+    environment.shellInit = mkOption {
+      default = "";
+      description = ''
+        Shell script code called during shell initialisation.
+        This code is assumed to be shell-independent, which means you should
+        stick to pure sh without sh word split.
+      '';
+      type = types.lines;
+    };
+
+    environment.loginShellInit = mkOption {
+      default = "";
+      description = ''
+        Shell script code called during login shell initialisation.
+        This code is assumed to be shell-independent, which means you should
+        stick to pure sh without sh word split.
+      '';
+      type = types.lines;
+    };
+
+    environment.interactiveShellInit = mkOption {
+      default = "";
+      description = ''
+        Shell script code called during interactive shell initialisation.
+        This code is assumed to be shell-independent, which means you should
+        stick to pure sh without sh word split.
+      '';
+      type = types.lines;
+    };
+
+    environment.shellAliases = mkOption {
+      example = { l = null; ll = "ls -l"; };
+      description = ''
+        An attribute set that maps aliases (the top level attribute names in
+        this option) to command strings or directly to build outputs. The
+        aliases are added to all users' shells.
+        Aliases mapped to <code>null</code> are ignored.
+      '';
+      type = with types; attrsOf (nullOr (either str path));
+    };
+
+    environment.homeBinInPath = mkOption {
+      description = ''
+        Include ~/bin/ in $PATH.
+      '';
+      default = false;
+      type = types.bool;
+    };
+
+    environment.localBinInPath = mkOption {
+      description = ''
+        Add ~/.local/bin/ to $PATH
+      '';
+      default = false;
+      type = types.bool;
+    };
+
+    environment.binsh = mkOption {
+      default = "${config.system.build.binsh}/bin/sh";
+      defaultText = literalExpression ''"''${config.system.build.binsh}/bin/sh"'';
+      example = literalExpression ''"''${pkgs.dash}/bin/dash"'';
+      type = types.path;
+      visible = false;
+      description = ''
+        The shell executable that is linked system-wide to
+        <literal>/bin/sh</literal>. Please note that NixOS assumes all
+        over the place that shell to be Bash, so override the default
+        setting only if you know exactly what you're doing.
+      '';
+    };
+
+    environment.shells = mkOption {
+      default = [];
+      example = literalExpression "[ pkgs.bashInteractive pkgs.zsh ]";
+      description = ''
+        A list of permissible login shells for user accounts.
+        No need to mention <literal>/bin/sh</literal>
+        here, it is placed into this list implicitly.
+      '';
+      type = types.listOf (types.either types.shellPackage types.path);
+    };
+
+  };
+
+  config = {
+
+    system.build.binsh = pkgs.bashInteractive;
+
+    # Set session variables in the shell as well. This is usually
+    # unnecessary, but it allows changes to session variables to take
+    # effect without restarting the session (e.g. by opening a new
+    # terminal instead of logging out of X11).
+    environment.variables = config.environment.sessionVariables;
+
+    environment.profileRelativeEnvVars = config.environment.profileRelativeSessionVariables;
+
+    environment.shellAliases = mapAttrs (name: mkDefault) {
+      ls = "ls --color=tty";
+      ll = "ls -l";
+      l  = "ls -alh";
+    };
+
+    environment.etc.shells.text =
+      ''
+        ${concatStringsSep "\n" (map utils.toShellPath cfg.shells)}
+        /bin/sh
+      '';
+
+    # For resetting environment with `. /etc/set-environment` when needed
+    # and discoverability (see motivation of #30418).
+    environment.etc.set-environment.source = config.system.build.setEnvironment;
+
+    system.build.setEnvironment = pkgs.writeText "set-environment"
+      ''
+        # DO NOT EDIT -- this file has been generated automatically.
+
+        # Prevent this file from being sourced by child shells.
+        export __NIXOS_SET_ENVIRONMENT_DONE=1
+
+        ${exportedEnvVars}
+
+        ${cfg.extraInit}
+
+        ${optionalString cfg.homeBinInPath ''
+          # ~/bin if it exists overrides other bin directories.
+          export PATH="$HOME/bin:$PATH"
+        ''}
+
+        ${optionalString cfg.localBinInPath ''
+          export PATH="$HOME/.local/bin:$PATH"
+        ''}
+      '';
+
+    system.activationScripts.binsh = stringAfter [ "stdio" ]
+      ''
+        # Create the required /bin/sh symlink; otherwise lots of things
+        # (notably the system() function) won't work.
+        mkdir -m 0755 -p /bin
+        ln -sfn "${cfg.binsh}" /bin/.sh.tmp
+        mv /bin/.sh.tmp /bin/sh # atomically replace /bin/sh
+      '';
+
+  };
+
+}
diff --git a/nixos/modules/config/swap.nix b/nixos/modules/config/swap.nix
new file mode 100644
index 00000000000..2b94b954cb8
--- /dev/null
+++ b/nixos/modules/config/swap.nix
@@ -0,0 +1,249 @@
+{ config, lib, pkgs, utils, ... }:
+
+with utils;
+with lib;
+
+let
+
+  randomEncryptionCoerce = enable: { inherit enable; };
+
+  randomEncryptionOpts = { ... }: {
+
+    options = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Encrypt swap device with a random key. This way you won't have a persistent swap device.
+
+          WARNING: Don't try to hibernate when you have at least one swap partition with
+          this option enabled! We have no way to set the partition into which hibernation image
+          is saved, so if your image ends up on an encrypted one you would lose it!
+
+          WARNING #2: Do not use /dev/disk/by-uuid/… or /dev/disk/by-label/… as your swap device
+          when using randomEncryption as the UUIDs and labels will get erased on every boot when
+          the partition is encrypted. Best to use /dev/disk/by-partuuid/…
+        '';
+      };
+
+      cipher = mkOption {
+        default = "aes-xts-plain64";
+        example = "serpent-xts-plain64";
+        type = types.str;
+        description = ''
+          Use specified cipher for randomEncryption.
+
+          Hint: Run "cryptsetup benchmark" to see which one is fastest on your machine.
+        '';
+      };
+
+      source = mkOption {
+        default = "/dev/urandom";
+        example = "/dev/random";
+        type = types.str;
+        description = ''
+          Define the source of randomness to obtain a random key for encryption.
+        '';
+      };
+
+      allowDiscards = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to allow TRIM requests to the underlying device. This option
+          has security implications; please read the LUKS documentation before
+          activating it.
+        '';
+      };
+    };
+
+  };
+
+  swapCfg = {config, options, ...}: {
+
+    options = {
+
+      device = mkOption {
+        example = "/dev/sda3";
+        type = types.str;
+        description = "Path of the device or swap file.";
+      };
+
+      label = mkOption {
+        example = "swap";
+        type = types.str;
+        description = ''
+          Label of the device.  Can be used instead of <varname>device</varname>.
+        '';
+      };
+
+      size = mkOption {
+        default = null;
+        example = 2048;
+        type = types.nullOr types.int;
+        description = ''
+          If this option is set, ‘device’ is interpreted as the
+          path of a swapfile that will be created automatically
+          with the indicated size (in megabytes).
+        '';
+      };
+
+      priority = mkOption {
+        default = null;
+        example = 2048;
+        type = types.nullOr types.int;
+        description = ''
+          Specify the priority of the swap device. Priority is a value between 0 and 32767.
+          Higher numbers indicate higher priority.
+          null lets the kernel choose a priority, which will show up as a negative value.
+        '';
+      };
+
+      randomEncryption = mkOption {
+        default = false;
+        example = {
+          enable = true;
+          cipher = "serpent-xts-plain64";
+          source = "/dev/random";
+        };
+        type = types.coercedTo types.bool randomEncryptionCoerce (types.submodule randomEncryptionOpts);
+        description = ''
+          Encrypt swap device with a random key. This way you won't have a persistent swap device.
+
+          HINT: run "cryptsetup benchmark" to test cipher performance on your machine.
+
+          WARNING: Don't try to hibernate when you have at least one swap partition with
+          this option enabled! We have no way to set the partition into which hibernation image
+          is saved, so if your image ends up on an encrypted one you would lose it!
+
+          WARNING #2: Do not use /dev/disk/by-uuid/… or /dev/disk/by-label/… as your swap device
+          when using randomEncryption as the UUIDs and labels will get erased on every boot when
+          the partition is encrypted. Best to use /dev/disk/by-partuuid/…
+        '';
+      };
+
+      discardPolicy = mkOption {
+        default = null;
+        example = "once";
+        type = types.nullOr (types.enum ["once" "pages" "both" ]);
+        description = ''
+          Specify the discard policy for the swap device. If "once", then the
+          whole swap space is discarded at swapon invocation. If "pages",
+          asynchronous discard on freed pages is performed, before returning to
+          the available pages pool. With "both", both policies are activated.
+          See swapon(8) for more information.
+        '';
+      };
+
+      options = mkOption {
+        default = [ "defaults" ];
+        example = [ "nofail" ];
+        type = types.listOf types.nonEmptyStr;
+        description = ''
+          Options used to mount the swap.
+        '';
+      };
+
+      deviceName = mkOption {
+        type = types.str;
+        internal = true;
+      };
+
+      realDevice = mkOption {
+        type = types.path;
+        internal = true;
+      };
+
+    };
+
+    config = rec {
+      device = mkIf options.label.isDefined
+        "/dev/disk/by-label/${config.label}";
+      deviceName = lib.replaceChars ["\\"] [""] (escapeSystemdPath config.device);
+      realDevice = if config.randomEncryption.enable then "/dev/mapper/${deviceName}" else config.device;
+    };
+
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    swapDevices = mkOption {
+      default = [];
+      example = [
+        { device = "/dev/hda7"; }
+        { device = "/var/swapfile"; }
+        { label = "bigswap"; }
+      ];
+      description = ''
+        The swap devices and swap files.  These must have been
+        initialised using <command>mkswap</command>.  Each element
+        should be an attribute set specifying either the path of the
+        swap device or file (<literal>device</literal>) or the label
+        of the swap device (<literal>label</literal>, see
+        <command>mkswap -L</command>).  Using a label is
+        recommended.
+      '';
+
+      type = types.listOf (types.submodule swapCfg);
+    };
+
+  };
+
+  config = mkIf ((length config.swapDevices) != 0) {
+
+    system.requiredKernelConfig = with config.lib.kernelConfig; [
+      (isYes "SWAP")
+    ];
+
+    # 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;
+
+            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}
+                    chmod 0600 ${sw.device}
+                    ${optionalString (!sw.randomEncryption.enable) "mkswap ${sw.realDevice}"}
+                  fi
+                ''}
+                ${optionalString sw.randomEncryption.enable ''
+                  cryptsetup plainOpen -c ${sw.randomEncryption.cipher} -d ${sw.randomEncryption.source} \
+                    ${optionalString sw.randomEncryption.allowDiscards "--allow-discards"} ${sw.device} ${sw.deviceName}
+                  mkswap ${sw.realDevice}
+                ''}
+              '';
+
+            unitConfig.RequiresMountsFor = [ "${dirOf sw.device}" ];
+            unitConfig.DefaultDependencies = false; # needed to prevent a cycle
+            serviceConfig.Type = "oneshot";
+            serviceConfig.RemainAfterExit = sw.randomEncryption.enable;
+            serviceConfig.ExecStop = optionalString sw.randomEncryption.enable "${pkgs.cryptsetup}/bin/cryptsetup luksClose ${sw.deviceName}";
+            restartIfChanged = false;
+          };
+
+      in listToAttrs (map createSwapDevice (filter (sw: sw.size != null || sw.randomEncryption.enable) config.swapDevices));
+
+  };
+
+}
diff --git a/nixos/modules/config/sysctl.nix b/nixos/modules/config/sysctl.nix
new file mode 100644
index 00000000000..db1f5284f50
--- /dev/null
+++ b/nixos/modules/config/sysctl.nix
@@ -0,0 +1,63 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+
+  sysctlOption = mkOptionType {
+    name = "sysctl option value";
+    check = val:
+      let
+        checkType = x: isBool x || isString x || isInt x || x == null;
+      in
+        checkType val || (val._type or "" == "override" && checkType val.content);
+    merge = loc: defs: mergeOneOption loc (filterOverrides defs);
+  };
+
+in
+
+{
+
+  options = {
+
+    boot.kernel.sysctl = mkOption {
+      default = {};
+      example = literalExpression ''
+        { "net.ipv4.tcp_syncookies" = false; "vm.swappiness" = 60; }
+      '';
+      type = types.attrsOf sysctlOption;
+      description = ''
+        Runtime parameters of the Linux kernel, as set by
+        <citerefentry><refentrytitle>sysctl</refentrytitle>
+        <manvolnum>8</manvolnum></citerefentry>.  Note that sysctl
+        parameters names must be enclosed in quotes
+        (e.g. <literal>"vm.swappiness"</literal> instead of
+        <literal>vm.swappiness</literal>).  The value of each
+        parameter may be a string, integer, boolean, or null
+        (signifying the option will not appear at all).
+      '';
+    };
+
+  };
+
+  config = {
+
+    environment.etc."sysctl.d/60-nixos.conf".text =
+      concatStrings (mapAttrsToList (n: v:
+        optionalString (v != null) "${n}=${if v == false then "0" else toString v}\n"
+      ) config.boot.kernel.sysctl);
+
+    systemd.services.systemd-sysctl =
+      { wantedBy = [ "multi-user.target" ];
+        restartTriggers = [ config.environment.etc."sysctl.d/60-nixos.conf".source ];
+      };
+
+    # Hide kernel pointers (e.g. in /proc/modules) for unprivileged
+    # users as these make it easier to exploit kernel vulnerabilities.
+    boot.kernel.sysctl."kernel.kptr_restrict" = mkDefault 1;
+
+    # Disable YAMA by default to allow easy debugging.
+    boot.kernel.sysctl."kernel.yama.ptrace_scope" = mkDefault 0;
+
+  };
+}
diff --git a/nixos/modules/config/system-environment.nix b/nixos/modules/config/system-environment.nix
new file mode 100644
index 00000000000..d2a66b8d932
--- /dev/null
+++ b/nixos/modules/config/system-environment.nix
@@ -0,0 +1,104 @@
+# This module defines a system-wide environment that will be
+# initialised by pam_env (that is, not only in shells).
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.environment;
+
+in
+
+{
+
+  options = {
+
+    environment.sessionVariables = mkOption {
+      default = {};
+      description = ''
+        A set of environment variables used in the global environment.
+        These variables will be set by PAM early in the login process.
+
+        The value of each session variable can be either a string or a
+        list of strings. The latter is concatenated, interspersed with
+        colon characters.
+
+        Note, due to limitations in the PAM format values may not
+        contain the <literal>"</literal> character.
+
+        Also, these variables are merged into
+        <xref linkend="opt-environment.variables"/> and it is
+        therefore not possible to use PAM style variables such as
+        <code>@{HOME}</code>.
+      '';
+      type = with types; attrsOf (either str (listOf str));
+      apply = mapAttrs (n: v: if isList v then concatStringsSep ":" v else v);
+    };
+
+    environment.profileRelativeSessionVariables = mkOption {
+      type = types.attrsOf (types.listOf types.str);
+      example = { PATH = [ "/bin" ]; MANPATH = [ "/man" "/share/man" ]; };
+      description = ''
+        Attribute set of environment variable used in the global
+        environment. These variables will be set by PAM early in the
+        login process.
+
+        Variable substitution is available as described in
+        <citerefentry>
+          <refentrytitle>pam_env.conf</refentrytitle>
+          <manvolnum>5</manvolnum>
+        </citerefentry>.
+
+        Each attribute maps to a list of relative paths. Each relative
+        path is appended to the each profile of
+        <option>environment.profiles</option> to form the content of
+        the corresponding environment variable.
+
+        Also, these variables are merged into
+        <xref linkend="opt-environment.profileRelativeEnvVars"/> and it is
+        therefore not possible to use PAM style variables such as
+        <code>@{HOME}</code>.
+      '';
+    };
+
+  };
+
+  config = {
+    environment.etc."pam/environment".text = let
+      suffixedVariables =
+        flip mapAttrs cfg.profileRelativeSessionVariables (envVar: suffixes:
+          flip concatMap cfg.profiles (profile:
+            map (suffix: "${profile}${suffix}") suffixes
+          )
+        );
+
+      # We're trying to use the same syntax for PAM variables and env variables.
+      # That means we need to map the env variables that people might use to their
+      # equivalent PAM variable.
+      replaceEnvVars = replaceStrings ["$HOME" "$USER"] ["@{HOME}" "@{PAM_USER}"];
+
+      pamVariable = n: v:
+        ''${n}   DEFAULT="${concatStringsSep ":" (map replaceEnvVars (toList v))}"'';
+
+      pamVariables =
+        concatStringsSep "\n"
+        (mapAttrsToList pamVariable
+        (zipAttrsWith (n: concatLists)
+          [
+            # Make sure security wrappers are prioritized without polluting
+            # shell environments with an extra entry. Sessions which depend on
+            # pam for its environment will otherwise have eg. broken sudo. In
+            # particular Gnome Shell sometimes fails to source a proper
+            # environment from a shell.
+            { PATH = [ config.security.wrapperDir ]; }
+
+            (mapAttrs (n: toList) cfg.sessionVariables)
+            suffixedVariables
+          ]));
+    in ''
+      ${pamVariables}
+    '';
+  };
+
+}
diff --git a/nixos/modules/config/system-path.nix b/nixos/modules/config/system-path.nix
new file mode 100644
index 00000000000..875c4c9c441
--- /dev/null
+++ b/nixos/modules/config/system-path.nix
@@ -0,0 +1,189 @@
+# This module defines the packages that appear in
+# /run/current-system/sw.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  requiredPackages = map (pkg: setPrio ((pkg.meta.priority or 5) + 3) pkg)
+    [ pkgs.acl
+      pkgs.attr
+      pkgs.bashInteractive # bash with ncurses support
+      pkgs.bzip2
+      pkgs.coreutils-full
+      pkgs.cpio
+      pkgs.curl
+      pkgs.diffutils
+      pkgs.findutils
+      pkgs.gawk
+      pkgs.stdenv.cc.libc
+      pkgs.getent
+      pkgs.getconf
+      pkgs.gnugrep
+      pkgs.gnupatch
+      pkgs.gnused
+      pkgs.gnutar
+      pkgs.gzip
+      pkgs.xz
+      pkgs.less
+      pkgs.libcap
+      pkgs.ncurses
+      pkgs.netcat
+      config.programs.ssh.package
+      pkgs.mkpasswd
+      pkgs.procps
+      pkgs.su
+      pkgs.time
+      pkgs.util-linux
+      pkgs.which
+      pkgs.zstd
+    ];
+
+  defaultPackageNames =
+    [ "nano"
+      "perl"
+      "rsync"
+      "strace"
+    ];
+  defaultPackages =
+    map
+      (n: let pkg = pkgs.${n}; in setPrio ((pkg.meta.priority or 5) + 3) pkg)
+      defaultPackageNames;
+  defaultPackagesText = "[ ${concatMapStringsSep " " (n: "pkgs.${n}") defaultPackageNames } ]";
+
+in
+
+{
+  options = {
+
+    environment = {
+
+      systemPackages = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "[ pkgs.firefox pkgs.thunderbird ]";
+        description = ''
+          The set of packages that appear in
+          /run/current-system/sw.  These packages are
+          automatically available to all users, and are
+          automatically updated every time you rebuild the system
+          configuration.  (The latter is the main difference with
+          installing them in the default profile,
+          <filename>/nix/var/nix/profiles/default</filename>.
+        '';
+      };
+
+      defaultPackages = mkOption {
+        type = types.listOf types.package;
+        default = defaultPackages;
+        defaultText = literalDocBook ''
+          these packages, with their <literal>meta.priority</literal> numerically increased
+          (thus lowering their installation priority):
+          <programlisting>${defaultPackagesText}</programlisting>
+        '';
+        example = [];
+        description = ''
+          Set of default packages that aren't strictly necessary
+          for a running system, entries can be removed for a more
+          minimal NixOS installation.
+
+          Note: If <package>pkgs.nano</package> is removed from this list,
+          make sure another editor is installed and the
+          <literal>EDITOR</literal> environment variable is set to it.
+          Environment variables can be set using
+          <option>environment.variables</option>.
+
+          Like with systemPackages, packages are installed to
+          <filename>/run/current-system/sw</filename>. They are
+          automatically available to all users, and are
+          automatically updated every time you rebuild the system
+          configuration.
+        '';
+      };
+
+      pathsToLink = mkOption {
+        type = types.listOf types.str;
+        # Note: We need `/lib' to be among `pathsToLink' for NSS modules
+        # to work.
+        default = [];
+        example = ["/"];
+        description = "List of directories to be symlinked in <filename>/run/current-system/sw</filename>.";
+      };
+
+      extraOutputsToInstall = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "doc" "info" "devdoc" ];
+        description = "List of additional package outputs to be symlinked into <filename>/run/current-system/sw</filename>.";
+      };
+
+      extraSetup = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Shell fragments to be run after the system environment has been created. This should only be used for things that need to modify the internals of the environment, e.g. generating MIME caches. The environment being built can be accessed at $out.";
+      };
+
+    };
+
+    system = {
+
+      path = mkOption {
+        internal = true;
+        description = ''
+          The packages you want in the boot environment.
+        '';
+      };
+
+    };
+
+  };
+
+  config = {
+
+    environment.systemPackages = requiredPackages ++ config.environment.defaultPackages;
+
+    environment.pathsToLink =
+      [ "/bin"
+        "/etc/xdg"
+        "/etc/gtk-2.0"
+        "/etc/gtk-3.0"
+        "/lib" # FIXME: remove and update debug-info.nix
+        "/sbin"
+        "/share/emacs"
+        "/share/hunspell"
+        "/share/nano"
+        "/share/org"
+        "/share/themes"
+        "/share/vim-plugins"
+        "/share/vulkan"
+        "/share/kservices5"
+        "/share/kservicetypes5"
+        "/share/kxmlgui5"
+        "/share/systemd"
+        "/share/thumbnailers"
+      ];
+
+    system.path = pkgs.buildEnv {
+      name = "system-path";
+      paths = config.environment.systemPackages;
+      inherit (config.environment) pathsToLink extraOutputsToInstall;
+      ignoreCollisions = true;
+      # !!! Hacky, should modularise.
+      # outputs TODO: note that the tools will often not be linked by default
+      postBuild =
+        ''
+          # Remove wrapped binaries, they shouldn't be accessible via PATH.
+          find $out/bin -maxdepth 1 -name ".*-wrapped" -type l -delete
+
+          if [ -x $out/bin/glib-compile-schemas -a -w $out/share/glib-2.0/schemas ]; then
+              $out/bin/glib-compile-schemas $out/share/glib-2.0/schemas
+          fi
+
+          ${config.environment.extraSetup}
+        '';
+    };
+
+  };
+}
diff --git a/nixos/modules/config/terminfo.nix b/nixos/modules/config/terminfo.nix
new file mode 100644
index 00000000000..1396640af67
--- /dev/null
+++ b/nixos/modules/config/terminfo.nix
@@ -0,0 +1,33 @@
+# This module manages the terminfo database
+# and its integration in the system.
+{ config, ... }:
+{
+  config = {
+
+    environment.pathsToLink = [
+      "/share/terminfo"
+    ];
+
+    environment.etc.terminfo = {
+      source = "${config.system.path}/share/terminfo";
+    };
+
+    environment.profileRelativeSessionVariables = {
+      TERMINFO_DIRS = [ "/share/terminfo" ];
+    };
+
+    environment.extraInit = ''
+
+      # reset TERM with new TERMINFO available (if any)
+      export TERM=$TERM
+    '';
+
+    security.sudo.extraConfig = ''
+
+      # Keep terminfo database for root and %wheel.
+      Defaults:root,%wheel env_keep+=TERMINFO_DIRS
+      Defaults:root,%wheel env_keep+=TERMINFO
+    '';
+
+  };
+}
diff --git a/nixos/modules/config/unix-odbc-drivers.nix b/nixos/modules/config/unix-odbc-drivers.nix
new file mode 100644
index 00000000000..055c3b2364e
--- /dev/null
+++ b/nixos/modules/config/unix-odbc-drivers.nix
@@ -0,0 +1,38 @@
+{ config, lib, ... }:
+
+with lib;
+
+# unixODBC drivers (this solution is not perfect.. Because the user has to
+# ask the admin to add a driver.. but it's simple and works
+
+let
+  iniDescription = pkg: ''
+    [${pkg.fancyName}]
+    Description = ${pkg.meta.description}
+    Driver = ${pkg}/${pkg.driver}
+  '';
+
+in {
+  ###### interface
+
+  options = {
+    environment.unixODBCDrivers = mkOption {
+      type = types.listOf types.package;
+      default = [];
+      example = literalExpression "with pkgs.unixODBCDrivers; [ sqlite psql ]";
+      description = ''
+        Specifies Unix ODBC drivers to be registered in
+        <filename>/etc/odbcinst.ini</filename>.  You may also want to
+        add <literal>pkgs.unixODBC</literal> to the system path to get
+        a command line client to connect to ODBC databases.
+      '';
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf (config.environment.unixODBCDrivers != []) {
+    environment.etc."odbcinst.ini".text = concatMapStringsSep "\n" iniDescription config.environment.unixODBCDrivers;
+  };
+
+}
diff --git a/nixos/modules/config/update-users-groups.pl b/nixos/modules/config/update-users-groups.pl
new file mode 100644
index 00000000000..26ce561013b
--- /dev/null
+++ b/nixos/modules/config/update-users-groups.pl
@@ -0,0 +1,365 @@
+use strict;
+use warnings;
+use File::Path qw(make_path);
+use File::Slurp;
+use Getopt::Long;
+use JSON;
+
+# Keep track of deleted uids and gids.
+my $uidMapFile = "/var/lib/nixos/uid-map";
+my $uidMap = -e $uidMapFile ? decode_json(read_file($uidMapFile)) : {};
+
+my $gidMapFile = "/var/lib/nixos/gid-map";
+my $gidMap = -e $gidMapFile ? decode_json(read_file($gidMapFile)) : {};
+
+my $is_dry = ($ENV{'NIXOS_ACTION'} // "") eq "dry-activate";
+GetOptions("dry-activate" => \$is_dry);
+make_path("/var/lib/nixos", { mode => 0755 }) unless $is_dry;
+
+sub updateFile {
+    my ($path, $contents, $perms) = @_;
+    return if $is_dry;
+    write_file($path, { atomic => 1, binmode => ':utf8', perms => $perms // 0644 }, $contents) or die;
+}
+
+sub nscdInvalidate {
+    system("nscd", "--invalidate", $_[0]) unless $is_dry;
+}
+
+sub hashPassword {
+    my ($password) = @_;
+    my $salt = "";
+    my @chars = ('.', '/', 0..9, 'A'..'Z', 'a'..'z');
+    $salt .= $chars[rand 64] for (1..8);
+    return crypt($password, '$6$' . $salt . '$');
+}
+
+sub dry_print {
+    if ($is_dry) {
+        print STDERR ("$_[1] $_[2]\n")
+    } else {
+        print STDERR ("$_[0] $_[2]\n")
+    }
+}
+
+
+# Functions for allocating free GIDs/UIDs. FIXME: respect ID ranges in
+# /etc/login.defs.
+sub allocId {
+    my ($used, $prevUsed, $idMin, $idMax, $up, $getid) = @_;
+    my $id = $up ? $idMin : $idMax;
+    while ($id >= $idMin && $id <= $idMax) {
+        if (!$used->{$id} && !$prevUsed->{$id} && !defined &$getid($id)) {
+            $used->{$id} = 1;
+            return $id;
+        }
+        $used->{$id} = 1;
+        if ($up) { $id++; } else { $id--; }
+    }
+    die "$0: out of free UIDs or GIDs\n";
+}
+
+my (%gidsUsed, %uidsUsed, %gidsPrevUsed, %uidsPrevUsed);
+
+sub allocGid {
+    my ($name) = @_;
+    my $prevGid = $gidMap->{$name};
+    if (defined $prevGid && !defined $gidsUsed{$prevGid}) {
+        dry_print("reviving", "would revive", "group '$name' with GID $prevGid");
+        $gidsUsed{$prevGid} = 1;
+        return $prevGid;
+    }
+    return allocId(\%gidsUsed, \%gidsPrevUsed, 400, 999, 0, sub { my ($gid) = @_; getgrgid($gid) });
+}
+
+sub allocUid {
+    my ($name, $isSystemUser) = @_;
+    my ($min, $max, $up) = $isSystemUser ? (400, 999, 0) : (1000, 29999, 1);
+    my $prevUid = $uidMap->{$name};
+    if (defined $prevUid && $prevUid >= $min && $prevUid <= $max && !defined $uidsUsed{$prevUid}) {
+        dry_print("reviving", "would revive", "user '$name' with UID $prevUid");
+        $uidsUsed{$prevUid} = 1;
+        return $prevUid;
+    }
+    return allocId(\%uidsUsed, \%uidsPrevUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) });
+}
+
+# Read the declared users/groups
+my $spec = decode_json(read_file($ARGV[0]));
+
+# Don't allocate UIDs/GIDs that are manually assigned.
+foreach my $g (@{$spec->{groups}}) {
+    $gidsUsed{$g->{gid}} = 1 if defined $g->{gid};
+}
+
+foreach my $u (@{$spec->{users}}) {
+    $uidsUsed{$u->{uid}} = 1 if defined $u->{uid};
+}
+
+# Likewise for previously used but deleted UIDs/GIDs.
+$uidsPrevUsed{$_} = 1 foreach values %{$uidMap};
+$gidsPrevUsed{$_} = 1 foreach values %{$gidMap};
+
+
+# Read the current /etc/group.
+sub parseGroup {
+    chomp;
+    my @f = split(':', $_, -4);
+    my $gid = $f[2] eq "" ? undef : int($f[2]);
+    $gidsUsed{$gid} = 1 if defined $gid;
+    return ($f[0], { name => $f[0], password => $f[1], gid => $gid, members => $f[3] });
+}
+
+my %groupsCur = -f "/etc/group" ? map { parseGroup } read_file("/etc/group", { binmode => ":utf8" }) : ();
+
+# Read the current /etc/passwd.
+sub parseUser {
+    chomp;
+    my @f = split(':', $_, -7);
+    my $uid = $f[2] eq "" ? undef : int($f[2]);
+    $uidsUsed{$uid} = 1 if defined $uid;
+    return ($f[0], { name => $f[0], fakePassword => $f[1], uid => $uid,
+        gid => $f[3], description => $f[4], home => $f[5], shell => $f[6] });
+}
+my %usersCur = -f "/etc/passwd" ? map { parseUser } read_file("/etc/passwd", { binmode => ":utf8" }) : ();
+
+# Read the groups that were created declaratively (i.e. not by groups)
+# in the past. These must be removed if they are no longer in the
+# current spec.
+my $declGroupsFile = "/var/lib/nixos/declarative-groups";
+my %declGroups;
+$declGroups{$_} = 1 foreach split / /, -e $declGroupsFile ? read_file($declGroupsFile, { binmode => ":utf8" }) : "";
+
+# Idem for the users.
+my $declUsersFile = "/var/lib/nixos/declarative-users";
+my %declUsers;
+$declUsers{$_} = 1 foreach split / /, -e $declUsersFile ? read_file($declUsersFile, { binmode => ":utf8" }) : "";
+
+
+# Generate a new /etc/group containing the declared groups.
+my %groupsOut;
+foreach my $g (@{$spec->{groups}}) {
+    my $name = $g->{name};
+    my $existing = $groupsCur{$name};
+
+    my %members = map { ($_, 1) } @{$g->{members}};
+
+    if (defined $existing) {
+        $g->{gid} = $existing->{gid} if !defined $g->{gid};
+        if ($g->{gid} != $existing->{gid}) {
+            dry_print("warning: not applying", "warning: would not apply", "GID change of group ‘$name’ ($existing->{gid} -> $g->{gid})");
+            $g->{gid} = $existing->{gid};
+        }
+        $g->{password} = $existing->{password}; # do we want this?
+        if ($spec->{mutableUsers}) {
+            # Merge in non-declarative group members.
+            foreach my $uname (split /,/, $existing->{members} // "") {
+                $members{$uname} = 1 if !defined $declUsers{$uname};
+            }
+        }
+    } else {
+        $g->{gid} = allocGid($name) if !defined $g->{gid};
+        $g->{password} = "x";
+    }
+
+    $g->{members} = join ",", sort(keys(%members));
+    $groupsOut{$name} = $g;
+
+    $gidMap->{$name} = $g->{gid};
+}
+
+# Update the persistent list of declarative groups.
+updateFile($declGroupsFile, join(" ", sort(keys %groupsOut)));
+
+# Merge in the existing /etc/group.
+foreach my $name (keys %groupsCur) {
+    my $g = $groupsCur{$name};
+    next if defined $groupsOut{$name};
+    if (!$spec->{mutableUsers} || defined $declGroups{$name}) {
+        dry_print("removing group", "would remove group", "‘$name’");
+    } else {
+        $groupsOut{$name} = $g;
+    }
+}
+
+
+# Rewrite /etc/group. FIXME: acquire lock.
+my @lines = map { join(":", $_->{name}, $_->{password}, $_->{gid}, $_->{members}) . "\n" }
+    (sort { $a->{gid} <=> $b->{gid} } values(%groupsOut));
+updateFile($gidMapFile, to_json($gidMap));
+updateFile("/etc/group", \@lines);
+nscdInvalidate("group");
+
+# Generate a new /etc/passwd containing the declared users.
+my %usersOut;
+foreach my $u (@{$spec->{users}}) {
+    my $name = $u->{name};
+
+    # Resolve the gid of the user.
+    if ($u->{group} =~ /^[0-9]$/) {
+        $u->{gid} = $u->{group};
+    } elsif (defined $groupsOut{$u->{group}}) {
+        $u->{gid} = $groupsOut{$u->{group}}->{gid} // die;
+    } else {
+        warn "warning: user ‘$name’ has unknown group ‘$u->{group}’\n";
+        $u->{gid} = 65534;
+    }
+
+    my $existing = $usersCur{$name};
+    if (defined $existing) {
+        $u->{uid} = $existing->{uid} if !defined $u->{uid};
+        if ($u->{uid} != $existing->{uid}) {
+            dry_print("warning: not applying", "warning: would not apply", "UID change of user ‘$name’ ($existing->{uid} -> $u->{uid})");
+            $u->{uid} = $existing->{uid};
+        }
+    } 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};
+        }
+    }
+
+    # Ensure home directory incl. ownership and permissions.
+    if ($u->{createHome}) {
+        make_path($u->{home}, { mode => 0700 }) if ! -e $u->{home} and ! $is_dry;
+        chown $u->{uid}, $u->{gid}, $u->{home};
+        chmod 0700, $u->{home};
+    }
+
+    if (defined $u->{passwordFile}) {
+        if (-e $u->{passwordFile}) {
+            $u->{hashedPassword} = read_file($u->{passwordFile});
+            chomp $u->{hashedPassword};
+        } else {
+            warn "warning: password file ‘$u->{passwordFile}’ does not exist\n";
+        }
+    } elsif (defined $u->{password}) {
+        $u->{hashedPassword} = hashPassword($u->{password});
+    }
+
+    if (!defined $u->{shell}) {
+        if (defined $existing) {
+            $u->{shell} = $existing->{shell};
+        } else {
+            warn "warning: no declarative or previous shell for ‘$name’, setting shell to nologin\n";
+            $u->{shell} = "/run/current-system/sw/bin/nologin";
+        }
+    }
+
+    $u->{fakePassword} = $existing->{fakePassword} // "x";
+    $usersOut{$name} = $u;
+
+    $uidMap->{$name} = $u->{uid};
+}
+
+# Update the persistent list of declarative users.
+updateFile($declUsersFile, join(" ", sort(keys %usersOut)));
+
+# Merge in the existing /etc/passwd.
+foreach my $name (keys %usersCur) {
+    my $u = $usersCur{$name};
+    next if defined $usersOut{$name};
+    if (!$spec->{mutableUsers} || defined $declUsers{$name}) {
+        dry_print("removing user", "would remove user", "‘$name’");
+    } else {
+        $usersOut{$name} = $u;
+    }
+}
+
+# Rewrite /etc/passwd. FIXME: acquire lock.
+@lines = map { join(":", $_->{name}, $_->{fakePassword}, $_->{uid}, $_->{gid}, $_->{description}, $_->{home}, $_->{shell}) . "\n" }
+    (sort { $a->{uid} <=> $b->{uid} } (values %usersOut));
+updateFile($uidMapFile, to_json($uidMap));
+updateFile("/etc/passwd", \@lines);
+nscdInvalidate("passwd");
+
+
+# Rewrite /etc/shadow to add new accounts or remove dead ones.
+my @shadowNew;
+my %shadowSeen;
+
+foreach my $line (-f "/etc/shadow" ? read_file("/etc/shadow", { binmode => ":utf8" }) : ()) {
+    chomp $line;
+    my ($name, $hashedPassword, @rest) = split(':', $line, -9);
+    my $u = $usersOut{$name};;
+    next if !defined $u;
+    $hashedPassword = "!" if !$spec->{mutableUsers};
+    $hashedPassword = $u->{hashedPassword} if defined $u->{hashedPassword} && !$spec->{mutableUsers}; # FIXME
+    chomp $hashedPassword;
+    push @shadowNew, join(":", $name, $hashedPassword, @rest) . "\n";
+    $shadowSeen{$name} = 1;
+}
+
+foreach my $u (values %usersOut) {
+    next if defined $shadowSeen{$u->{name}};
+    my $hashedPassword = "!";
+    $hashedPassword = $u->{hashedPassword} if defined $u->{hashedPassword};
+    # FIXME: set correct value for sp_lstchg.
+    push @shadowNew, join(":", $u->{name}, $hashedPassword, "1::::::") . "\n";
+}
+
+updateFile("/etc/shadow", \@shadowNew, 0640);
+{
+    my $uid = getpwnam "root";
+    my $gid = getgrnam "shadow";
+    my $path = "/etc/shadow";
+    (chown($uid, $gid, $path) || die "Failed to change ownership of $path: $!") unless $is_dry;
+}
+
+# Rewrite /etc/subuid & /etc/subgid to include default container mappings
+
+my $subUidMapFile = "/var/lib/nixos/auto-subuid-map";
+my $subUidMap = -e $subUidMapFile ? decode_json(read_file($subUidMapFile)) : {};
+
+my (%subUidsUsed, %subUidsPrevUsed);
+
+$subUidsPrevUsed{$_} = 1 foreach values %{$subUidMap};
+
+sub allocSubUid {
+    my ($name, @rest) = @_;
+
+    # TODO: No upper bounds?
+    my ($min, $max, $up) = (100000, 100000 * 100, 1);
+    my $prevId = $subUidMap->{$name};
+    if (defined $prevId && !defined $subUidsUsed{$prevId}) {
+        $subUidsUsed{$prevId} = 1;
+        return $prevId;
+    }
+
+    my $id = allocId(\%subUidsUsed, \%subUidsPrevUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) });
+    my $offset = $id - 100000;
+    my $count = $offset * 65536;
+    my $subordinate = 100000 + $count;
+    return $subordinate;
+}
+
+my @subGids;
+my @subUids;
+foreach my $u (values %usersOut) {
+    my $name = $u->{name};
+
+    foreach my $range (@{$u->{subUidRanges}}) {
+        my $value = join(":", ($name, $range->{startUid}, $range->{count}));
+        push @subUids, $value;
+    }
+
+    foreach my $range (@{$u->{subGidRanges}}) {
+        my $value = join(":", ($name, $range->{startGid}, $range->{count}));
+        push @subGids, $value;
+    }
+
+    if($u->{autoSubUidGidRange}) {
+        my $subordinate = allocSubUid($name);
+        $subUidMap->{$name} = $subordinate;
+        my $value = join(":", ($name, $subordinate, 65536));
+        push @subUids, $value;
+        push @subGids, $value;
+    }
+}
+
+updateFile("/etc/subuid", join("\n", @subUids) . "\n");
+updateFile("/etc/subgid", join("\n", @subGids) . "\n");
+updateFile($subUidMapFile, encode_json($subUidMap) . "\n");
diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix
new file mode 100644
index 00000000000..b0f96c754fa
--- /dev/null
+++ b/nixos/modules/config/users-groups.nix
@@ -0,0 +1,715 @@
+{ config, lib, utils, pkgs, ... }:
+
+with lib;
+
+let
+  ids = config.ids;
+  cfg = config.users;
+
+  isPasswdCompatible = str: !(hasInfix ":" str || hasInfix "\n" str);
+  passwdEntry = type: lib.types.addCheck type isPasswdCompatible // {
+    name = "passwdEntry ${type.name}";
+    description = "${type.description}, not containing newlines or colons";
+  };
+
+  # Check whether a password hash will allow login.
+  allowsLogin = hash:
+    hash == "" # login without password
+    || !(lib.elem hash
+      [ null   # password login disabled
+        "!"    # password login disabled
+        "!!"   # a variant of "!"
+        "*"    # password unset
+      ]);
+
+  passwordDescription = ''
+    The options <option>hashedPassword</option>,
+    <option>password</option> and <option>passwordFile</option>
+    controls what password is set for the user.
+    <option>hashedPassword</option> overrides both
+    <option>password</option> and <option>passwordFile</option>.
+    <option>password</option> overrides <option>passwordFile</option>.
+    If none of these three options are set, no password is assigned to
+    the user, and the user will not be able to do password logins.
+    If the option <option>users.mutableUsers</option> is true, the
+    password defined in one of the three options will only be set when
+    the user is created for the first time. After that, you are free to
+    change the password with the ordinary user management commands. If
+    <option>users.mutableUsers</option> is false, you cannot change
+    user passwords, they will always be set according to the password
+    options.
+  '';
+
+  hashedPasswordDescription = ''
+    To generate a hashed password run <literal>mkpasswd -m sha-512</literal>.
+
+    If set to an empty string (<literal>""</literal>), this user will
+    be able to log in without being asked for a password (but not via remote
+    services such as SSH, or indirectly via <command>su</command> or
+    <command>sudo</command>). This should only be used for e.g. bootable
+    live systems. Note: this is different from setting an empty password,
+    which ca be achieved using <option>users.users.&lt;name?&gt;.password</option>.
+
+    If set to <literal>null</literal> (default) this user will not
+    be able to log in using a password (i.e. via <command>login</command>
+    command).
+  '';
+
+  userOpts = { name, config, ... }: {
+
+    options = {
+
+      name = mkOption {
+        type = passwdEntry types.str;
+        apply = x: assert (builtins.stringLength x < 32 || abort "Username '${x}' is longer than 31 characters which is not allowed!"); x;
+        description = ''
+          The name of the user account. If undefined, the name of the
+          attribute set will be used.
+        '';
+      };
+
+      description = mkOption {
+        type = passwdEntry types.str;
+        default = "";
+        example = "Alice Q. User";
+        description = ''
+          A short description of the user account, typically the
+          user's full name.  This is actually the “GECOS†or “commentâ€
+          field in <filename>/etc/passwd</filename>.
+        '';
+      };
+
+      uid = mkOption {
+        type = with types; nullOr int;
+        default = null;
+        description = ''
+          The account UID. If the UID is null, a free UID is picked on
+          activation.
+        '';
+      };
+
+      isSystemUser = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Indicates if the user is a system user or not. This option
+          only has an effect if <option>uid</option> is
+          <option>null</option>, 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
+          1000).
+          Exactly one of <literal>isNormalUser</literal> and
+          <literal>isSystemUser</literal> must be true.
+        '';
+      };
+
+      isNormalUser = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Indicates whether this is an account for a “real†user. This
+          automatically sets <option>group</option> to
+          <literal>users</literal>, <option>createHome</option> to
+          <literal>true</literal>, <option>home</option> to
+          <filename>/home/<replaceable>username</replaceable></filename>,
+          <option>useDefaultShell</option> to <literal>true</literal>,
+          and <option>isSystemUser</option> to
+          <literal>false</literal>.
+          Exactly one of <literal>isNormalUser</literal> and
+          <literal>isSystemUser</literal> must be true.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        apply = x: assert (builtins.stringLength x < 32 || abort "Group name '${x}' is longer than 31 characters which is not allowed!"); x;
+        default = "";
+        description = "The user's primary group.";
+      };
+
+      extraGroups = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "The user's auxiliary groups.";
+      };
+
+      home = mkOption {
+        type = passwdEntry types.path;
+        default = "/var/empty";
+        description = "The user's home directory.";
+      };
+
+      cryptHomeLuks = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Path to encrypted luks device that contains
+          the user's home directory.
+        '';
+      };
+
+      pamMount = mkOption {
+        type = with types; attrsOf str;
+        default = {};
+        description = ''
+          Attributes for user's entry in
+          <filename>pam_mount.conf.xml</filename>.
+          Useful attributes might include <code>path</code>,
+          <code>options</code>, <code>fstype</code>, and <code>server</code>.
+          See <link
+          xlink:href="http://pam-mount.sourceforge.net/pam_mount.conf.5.html" />
+          for more information.
+        '';
+      };
+
+      shell = mkOption {
+        type = types.nullOr (types.either types.shellPackage (passwdEntry types.path));
+        default = pkgs.shadow;
+        defaultText = literalExpression "pkgs.shadow";
+        example = literalExpression "pkgs.bashInteractive";
+        description = ''
+          The path to the user's shell. Can use shell derivations,
+          like <literal>pkgs.bashInteractive</literal>. Don’t
+          forget to enable your shell in
+          <literal>programs</literal> if necessary,
+          like <code>programs.zsh.enable = true;</code>.
+        '';
+      };
+
+      subUidRanges = mkOption {
+        type = with types; listOf (submodule subordinateUidRange);
+        default = [];
+        example = [
+          { startUid = 1000; count = 1; }
+          { startUid = 100001; count = 65534; }
+        ];
+        description = ''
+          Subordinate user ids that user is allowed to use.
+          They are set into <filename>/etc/subuid</filename> and are used
+          by <literal>newuidmap</literal> for user namespaces.
+        '';
+      };
+
+      subGidRanges = mkOption {
+        type = with types; listOf (submodule subordinateGidRange);
+        default = [];
+        example = [
+          { startGid = 100; count = 1; }
+          { startGid = 1001; count = 999; }
+        ];
+        description = ''
+          Subordinate group ids that user is allowed to use.
+          They are set into <filename>/etc/subgid</filename> and are used
+          by <literal>newgidmap</literal> for user namespaces.
+        '';
+      };
+
+      autoSubUidGidRange = mkOption {
+        type = types.bool;
+        default = false;
+        example = true;
+        description = ''
+          Automatically allocate subordinate user and group ids for this user.
+          Allocated range is currently always of size 65536.
+        '';
+      };
+
+      createHome = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to create the home directory and ensure ownership as well as
+          permissions to match the user.
+        '';
+      };
+
+      useDefaultShell = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If true, the user's shell will be set to
+          <option>users.defaultUserShell</option>.
+        '';
+      };
+
+      hashedPassword = mkOption {
+        type = with types; nullOr (passwdEntry str);
+        default = null;
+        description = ''
+          Specifies the hashed password for the user.
+          ${passwordDescription}
+          ${hashedPasswordDescription}
+        '';
+      };
+
+      password = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Specifies the (clear text) password for the user.
+          Warning: do not set confidential information here
+          because it is world-readable in the Nix store. This option
+          should only be used for public accounts.
+          ${passwordDescription}
+        '';
+      };
+
+      passwordFile = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          The full path to a file that contains the user's password. The password
+          file is read on each system activation. The file should contain
+          exactly one line, which should be the password in an encrypted form
+          that is suitable for the <literal>chpasswd -e</literal> command.
+          ${passwordDescription}
+        '';
+      };
+
+      initialHashedPassword = mkOption {
+        type = with types; nullOr (passwdEntry str);
+        default = null;
+        description = ''
+          Specifies the initial hashed password for the user, i.e. the
+          hashed password assigned if the user does not already
+          exist. If <option>users.mutableUsers</option> is true, the
+          password can be changed subsequently using the
+          <command>passwd</command> command. Otherwise, it's
+          equivalent to setting the <option>hashedPassword</option> option.
+
+          ${hashedPasswordDescription}
+        '';
+      };
+
+      initialPassword = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Specifies the initial password for the user, i.e. the
+          password assigned if the user does not already exist. If
+          <option>users.mutableUsers</option> is true, the password
+          can be changed subsequently using the
+          <command>passwd</command> command. Otherwise, it's
+          equivalent to setting the <option>password</option>
+          option. The same caveat applies: the password specified here
+          is world-readable in the Nix store, so it should only be
+          used for guest accounts or passwords that will be changed
+          promptly.
+        '';
+      };
+
+      packages = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "[ pkgs.firefox pkgs.thunderbird ]";
+        description = ''
+          The set of packages that should be made available to the user.
+          This is in contrast to <option>environment.systemPackages</option>,
+          which adds packages to all users.
+        '';
+      };
+
+    };
+
+    config = mkMerge
+      [ { name = mkDefault name;
+          shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell);
+        }
+        (mkIf config.isNormalUser {
+          group = mkDefault "users";
+          createHome = mkDefault true;
+          home = mkDefault "/home/${config.name}";
+          useDefaultShell = mkDefault true;
+          isSystemUser = mkDefault false;
+        })
+        # If !mutableUsers, setting ‘initialPassword’ is equivalent to
+        # setting ‘password’ (and similarly for hashed passwords).
+        (mkIf (!cfg.mutableUsers && config.initialPassword != null) {
+          password = mkDefault config.initialPassword;
+        })
+        (mkIf (!cfg.mutableUsers && config.initialHashedPassword != null) {
+          hashedPassword = mkDefault config.initialHashedPassword;
+        })
+        (mkIf (config.isNormalUser && config.subUidRanges == [] && config.subGidRanges == []) {
+          autoSubUidGidRange = mkDefault true;
+        })
+      ];
+
+  };
+
+  groupOpts = { name, config, ... }: {
+
+    options = {
+
+      name = mkOption {
+        type = passwdEntry types.str;
+        description = ''
+          The name of the group. If undefined, the name of the attribute set
+          will be used.
+        '';
+      };
+
+      gid = mkOption {
+        type = with types; nullOr int;
+        default = null;
+        description = ''
+          The group GID. If the GID is null, a free GID is picked on
+          activation.
+        '';
+      };
+
+      members = mkOption {
+        type = with types; listOf (passwdEntry str);
+        default = [];
+        description = ''
+          The user names of the group members, added to the
+          <literal>/etc/group</literal> file.
+        '';
+      };
+
+    };
+
+    config = {
+      name = mkDefault name;
+
+      members = mapAttrsToList (n: u: u.name) (
+        filterAttrs (n: u: elem config.name u.extraGroups) cfg.users
+      );
+    };
+
+  };
+
+  subordinateUidRange = {
+    options = {
+      startUid = mkOption {
+        type = types.int;
+        description = ''
+          Start of the range of subordinate user ids that user is
+          allowed to use.
+        '';
+      };
+      count = mkOption {
+        type = types.int;
+        default = 1;
+        description = "Count of subordinate user ids";
+      };
+    };
+  };
+
+  subordinateGidRange = {
+    options = {
+      startGid = mkOption {
+        type = types.int;
+        description = ''
+          Start of the range of subordinate group ids that user is
+          allowed to use.
+        '';
+      };
+      count = mkOption {
+        type = types.int;
+        default = 1;
+        description = "Count of subordinate group ids";
+      };
+    };
+  };
+
+  idsAreUnique = set: idAttr: !(foldr (name: args@{ dup, acc }:
+    let
+      id = builtins.toString (builtins.getAttr idAttr (builtins.getAttr name set));
+      exists = builtins.hasAttr id acc;
+      newAcc = acc // (builtins.listToAttrs [ { name = id; value = true; } ]);
+    in if dup then args else if exists
+      then builtins.trace "Duplicate ${idAttr} ${id}" { dup = true; acc = null; }
+      else { dup = false; acc = newAcc; }
+    ) { dup = false; acc = {}; } (builtins.attrNames set)).dup;
+
+  uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid";
+  gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid";
+
+  spec = pkgs.writeText "users-groups.json" (builtins.toJSON {
+    inherit (cfg) mutableUsers;
+    users = mapAttrsToList (_: u:
+      { inherit (u)
+          name uid group description home createHome isSystemUser
+          password passwordFile hashedPassword
+          autoSubUidGidRange subUidRanges subGidRanges
+          initialPassword initialHashedPassword;
+        shell = utils.toShellPath u.shell;
+      }) cfg.users;
+    groups = attrValues cfg.groups;
+  });
+
+  systemShells =
+    let
+      shells = mapAttrsToList (_: u: u.shell) cfg.users;
+    in
+      filter types.shellPackage.check shells;
+
+in {
+  imports = [
+    (mkAliasOptionModule [ "users" "extraUsers" ] [ "users" "users" ])
+    (mkAliasOptionModule [ "users" "extraGroups" ] [ "users" "groups" ])
+    (mkRenamedOptionModule ["security" "initialRootPassword"] ["users" "users" "root" "initialHashedPassword"])
+  ];
+
+  ###### interface
+  options = {
+
+    users.mutableUsers = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        If set to <literal>true</literal>, you are free to add new users and groups to the system
+        with the ordinary <literal>useradd</literal> and
+        <literal>groupadd</literal> commands. On system activation, the
+        existing contents of the <literal>/etc/passwd</literal> and
+        <literal>/etc/group</literal> files will be merged with the
+        contents generated from the <literal>users.users</literal> and
+        <literal>users.groups</literal> options.
+        The initial password for a user will be set
+        according to <literal>users.users</literal>, but existing passwords
+        will not be changed.
+
+        <warning><para>
+        If set to <literal>false</literal>, the contents of the user and
+        group files will simply be replaced on system activation. This also
+        holds for the user passwords; all changed
+        passwords will be reset according to the
+        <literal>users.users</literal> configuration on activation.
+        </para></warning>
+      '';
+    };
+
+    users.enforceIdUniqueness = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to require that no two users/groups share the same uid/gid.
+      '';
+    };
+
+    users.users = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule userOpts);
+      example = {
+        alice = {
+          uid = 1234;
+          description = "Alice Q. User";
+          home = "/home/alice";
+          createHome = true;
+          group = "users";
+          extraGroups = ["wheel"];
+          shell = "/bin/sh";
+        };
+      };
+      description = ''
+        Additional user accounts to be created automatically by the system.
+        This can also be used to set options for root.
+      '';
+    };
+
+    users.groups = mkOption {
+      default = {};
+      example =
+        { students.gid = 1001;
+          hackers = { };
+        };
+      type = with types; attrsOf (submodule groupOpts);
+      description = ''
+        Additional groups to be created automatically by the system.
+      '';
+    };
+
+
+    users.allowNoPasswordLogin = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Disable checking that at least the <literal>root</literal> user or a user in the <literal>wheel</literal> group can log in using
+        a password or an SSH key.
+
+        WARNING: enabling this can lock you out of your system. Enable this only if you know what are you doing.
+      '';
+    };
+  };
+
+
+  ###### implementation
+
+  config = {
+
+    users.users = {
+      root = {
+        uid = ids.uids.root;
+        description = "System administrator";
+        home = "/root";
+        shell = mkDefault cfg.defaultUserShell;
+        group = "root";
+        initialHashedPassword = mkDefault "!";
+      };
+      nobody = {
+        uid = ids.uids.nobody;
+        isSystemUser = true;
+        description = "Unprivileged account (don't use!)";
+        group = "nogroup";
+      };
+    };
+
+    users.groups = {
+      root.gid = ids.gids.root;
+      wheel.gid = ids.gids.wheel;
+      disk.gid = ids.gids.disk;
+      kmem.gid = ids.gids.kmem;
+      tty.gid = ids.gids.tty;
+      floppy.gid = ids.gids.floppy;
+      uucp.gid = ids.gids.uucp;
+      lp.gid = ids.gids.lp;
+      cdrom.gid = ids.gids.cdrom;
+      tape.gid = ids.gids.tape;
+      audio.gid = ids.gids.audio;
+      video.gid = ids.gids.video;
+      dialout.gid = ids.gids.dialout;
+      nogroup.gid = ids.gids.nogroup;
+      users.gid = ids.gids.users;
+      nixbld.gid = ids.gids.nixbld;
+      utmp.gid = ids.gids.utmp;
+      adm.gid = ids.gids.adm;
+      input.gid = ids.gids.input;
+      kvm.gid = ids.gids.kvm;
+      render.gid = ids.gids.render;
+      sgx.gid = ids.gids.sgx;
+      shadow.gid = ids.gids.shadow;
+    };
+
+    system.activationScripts.users = {
+      supportsDryActivation = true;
+      text = ''
+        install -m 0700 -d /root
+        install -m 0755 -d /home
+
+        ${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON ])}/bin/perl \
+        -w ${./update-users-groups.pl} ${spec}
+      '';
+    };
+
+    # for backwards compatibility
+    system.activationScripts.groups = stringAfter [ "users" ] "";
+
+    # Install all the user shells
+    environment.systemPackages = systemShells;
+
+    environment.etc = (mapAttrs' (_: { packages, name, ... }: {
+      name = "profiles/per-user/${name}";
+      value.source = pkgs.buildEnv {
+        name = "user-environment";
+        paths = packages;
+        inherit (config.environment) pathsToLink extraOutputsToInstall;
+        inherit (config.system.path) ignoreCollisions postBuild;
+      };
+    }) (filterAttrs (_: u: u.packages != []) cfg.users));
+
+    environment.profiles = [
+      "$HOME/.nix-profile"
+      "/etc/profiles/per-user/$USER"
+    ];
+
+    assertions = [
+      { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique);
+        message = "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
+        # password or an SSH authorized key. Privileged accounts are
+        # root and users in the wheel group.
+        # The check does not apply when users.disableLoginPossibilityAssertion
+        # The check does not apply when users.mutableUsers
+        assertion = !cfg.mutableUsers -> !cfg.allowNoPasswordLogin ->
+          any id (mapAttrsToList (name: cfg:
+            (name == "root"
+             || cfg.group == "wheel"
+             || elem "wheel" cfg.extraGroups)
+            &&
+            (allowsLogin cfg.hashedPassword
+             || cfg.password != null
+             || cfg.passwordFile != null
+             || cfg.openssh.authorizedKeys.keys != []
+             || cfg.openssh.authorizedKeys.keyFiles != [])
+          ) cfg.users ++ [
+            config.security.googleOsLogin.enable
+          ]);
+        message = ''
+          Neither the root account nor any wheel user has a password or SSH authorized key.
+          You must set one to prevent being locked out of your system.
+          If you really want to be locked out of your system, set users.allowNoPasswordLogin = true;
+          However you are most probably better off by setting users.mutableUsers = true; and
+          manually running passwd root to set the root password.
+          '';
+      }
+    ] ++ flatten (flip mapAttrsToList cfg.users (name: user:
+      [
+        {
+        assertion = (user.hashedPassword != null)
+        -> (builtins.match ".*:.*" user.hashedPassword == null);
+        message = ''
+            The password hash of user "${user.name}" contains a ":" character.
+            This is invalid and would break the login system because the fields
+            of /etc/shadow (file where hashes are stored) are colon-separated.
+            Please check the value of option `users.users."${user.name}".hashedPassword`.'';
+          }
+          {
+            assertion = let
+              xor = a: b: a && !b || b && !a;
+              isEffectivelySystemUser = user.isSystemUser || (user.uid != null && user.uid < 500);
+            in xor isEffectivelySystemUser user.isNormalUser;
+            message = ''
+              Exactly one of users.users.${user.name}.isSystemUser and users.users.${user.name}.isNormalUser must be set.
+            '';
+          }
+          {
+            assertion = user.group != "";
+            message = ''
+              users.users.${user.name}.group is unset. This used to default to
+              nogroup, but this is unsafe. For example you can create a group
+              for this user with:
+              users.users.${user.name}.group = "${user.name}";
+              users.groups.${user.name} = {};
+            '';
+          }
+        ]
+    ));
+
+    warnings =
+      builtins.filter (x: x != null) (
+        flip mapAttrsToList cfg.users (_: user:
+        # This regex matches a subset of the Modular Crypto Format (MCF)[1]
+        # informal standard. Since this depends largely on the OS or the
+        # specific implementation of crypt(3) we only support the (sane)
+        # schemes implemented by glibc and BSDs. In particular the original
+        # DES hash is excluded since, having no structure, it would validate
+        # common mistakes like typing the plaintext password.
+        #
+        # [1]: https://en.wikipedia.org/wiki/Crypt_(C)
+        let
+          sep = "\\$";
+          base64 = "[a-zA-Z0-9./]+";
+          id = "[a-z0-9-]+";
+          value = "[a-zA-Z0-9/+.-]+";
+          options = "${id}(=${value})?(,${id}=${value})*";
+          scheme  = "${id}(${sep}${options})?";
+          content = "${base64}${sep}${base64}";
+          mcf = "^${sep}${scheme}${sep}${content}$";
+        in
+        if (allowsLogin user.hashedPassword
+            && user.hashedPassword != ""  # login without password
+            && builtins.match mcf user.hashedPassword == null)
+        then ''
+          The password hash of user "${user.name}" may be invalid. You must set a
+          valid hash or the user will be locked out of their account. Please
+          check the value of option `users.users."${user.name}".hashedPassword`.''
+        else null
+      ));
+
+  };
+
+}
diff --git a/nixos/modules/config/vte.nix b/nixos/modules/config/vte.nix
new file mode 100644
index 00000000000..24d32a00fd4
--- /dev/null
+++ b/nixos/modules/config/vte.nix
@@ -0,0 +1,56 @@
+# VTE
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  vteInitSnippet = ''
+    # Show current working directory in VTE terminals window title.
+    # Supports both bash and zsh, requires interactive shell.
+    . ${pkgs.vte}/etc/profile.d/vte.sh
+  '';
+
+in
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  options = {
+
+    programs.bash.vteIntegration = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to enable Bash integration for VTE terminals.
+        This allows it to preserve the current directory of the shell
+        across terminals.
+      '';
+    };
+
+    programs.zsh.vteIntegration = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to enable Zsh integration for VTE terminals.
+        This allows it to preserve the current directory of the shell
+        across terminals.
+      '';
+    };
+
+  };
+
+  config = mkMerge [
+    (mkIf config.programs.bash.vteIntegration {
+      programs.bash.interactiveShellInit = mkBefore vteInitSnippet;
+    })
+
+    (mkIf config.programs.zsh.vteIntegration {
+      programs.zsh.interactiveShellInit = vteInitSnippet;
+    })
+  ];
+}
diff --git a/nixos/modules/config/xdg/autostart.nix b/nixos/modules/config/xdg/autostart.nix
new file mode 100644
index 00000000000..40984cb5ec5
--- /dev/null
+++ b/nixos/modules/config/xdg/autostart.nix
@@ -0,0 +1,26 @@
+{ config, lib, ... }:
+
+with lib;
+{
+  meta = {
+    maintainers = teams.freedesktop.members;
+  };
+
+  options = {
+    xdg.autostart.enable = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to install files to support the
+        <link xlink:href="https://specifications.freedesktop.org/autostart-spec/autostart-spec-latest.html">XDG Autostart specification</link>.
+      '';
+    };
+  };
+
+  config = mkIf config.xdg.autostart.enable {
+    environment.pathsToLink = [
+      "/etc/xdg/autostart"
+    ];
+  };
+
+}
diff --git a/nixos/modules/config/xdg/icons.nix b/nixos/modules/config/xdg/icons.nix
new file mode 100644
index 00000000000..c83fdc251ef
--- /dev/null
+++ b/nixos/modules/config/xdg/icons.nix
@@ -0,0 +1,42 @@
+{ config, lib, ... }:
+
+with lib;
+{
+  meta = {
+    maintainers = teams.freedesktop.members;
+  };
+
+  options = {
+    xdg.icons.enable = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to install files to support the
+        <link xlink:href="https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html">XDG Icon Theme specification</link>.
+      '';
+    };
+  };
+
+  config = mkIf config.xdg.icons.enable {
+    environment.pathsToLink = [
+      "/share/icons"
+      "/share/pixmaps"
+    ];
+
+    # libXcursor looks for cursors in XCURSOR_PATH
+    # it mostly follows the spec for icons
+    # See: https://www.x.org/releases/current/doc/man/man3/Xcursor.3.xhtml Themes
+
+    # These are preferred so they come first in the list
+    environment.sessionVariables.XCURSOR_PATH = [
+      "$HOME/.icons"
+      "$HOME/.local/share/icons"
+    ];
+
+    environment.profileRelativeSessionVariables.XCURSOR_PATH = [
+      "/share/icons"
+      "/share/pixmaps"
+    ];
+  };
+
+}
diff --git a/nixos/modules/config/xdg/menus.nix b/nixos/modules/config/xdg/menus.nix
new file mode 100644
index 00000000000..6735a7a5c43
--- /dev/null
+++ b/nixos/modules/config/xdg/menus.nix
@@ -0,0 +1,29 @@
+{ config, lib, ... }:
+
+with lib;
+{
+  meta = {
+    maintainers = teams.freedesktop.members;
+  };
+
+  options = {
+    xdg.menus.enable = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to install files to support the
+        <link xlink:href="https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html">XDG Desktop Menu specification</link>.
+      '';
+    };
+  };
+
+  config = mkIf config.xdg.menus.enable {
+    environment.pathsToLink = [
+      "/share/applications"
+      "/share/desktop-directories"
+      "/etc/xdg/menus"
+      "/etc/xdg/menus/applications-merged"
+    ];
+  };
+
+}
diff --git a/nixos/modules/config/xdg/mime.nix b/nixos/modules/config/xdg/mime.nix
new file mode 100644
index 00000000000..9b6dd4cab5f
--- /dev/null
+++ b/nixos/modules/config/xdg/mime.nix
@@ -0,0 +1,102 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.xdg.mime;
+  associationOptions = with types; attrsOf (
+    coercedTo (either (listOf str) str) (x: concatStringsSep ";" (toList x)) str
+  );
+in
+
+{
+  meta = {
+    maintainers = teams.freedesktop.members ++ (with maintainers; [ figsoda ]);
+  };
+
+  options = {
+    xdg.mime.enable = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to install files to support the
+        <link xlink:href="https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html">XDG Shared MIME-info specification</link> and the
+        <link xlink:href="https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-latest.html">XDG MIME Applications specification</link>.
+      '';
+    };
+
+    xdg.mime.addedAssociations = mkOption {
+      type = associationOptions;
+      default = {};
+      example = {
+        "application/pdf" = "firefox.desktop";
+        "text/xml" = [ "nvim.desktop" "codium.desktop" ];
+      };
+      description = ''
+        Adds associations between mimetypes and applications. See the
+        <link xlink:href="https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-latest.html#associations">
+        specifications</link> for more information.
+      '';
+    };
+
+    xdg.mime.defaultApplications = mkOption {
+      type = associationOptions;
+      default = {};
+      example = {
+        "application/pdf" = "firefox.desktop";
+        "image/png" = [ "sxiv.desktop" "gimp.desktop" ];
+      };
+      description = ''
+        Sets the default applications for given mimetypes. See the
+        <link xlink:href="https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-latest.html#default">
+        specifications</link> for more information.
+      '';
+    };
+
+    xdg.mime.removedAssociations = mkOption {
+      type = associationOptions;
+      default = {};
+      example = {
+        "audio/mp3" = [ "mpv.desktop" "umpv.desktop" ];
+        "inode/directory" = "codium.desktop";
+      };
+      description = ''
+        Removes associations between mimetypes and applications. See the
+        <link xlink:href="https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-latest.html#associations">
+        specifications</link> for more information.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."xdg/mimeapps.list" = mkIf (
+      cfg.addedAssociations != {}
+      || cfg.defaultApplications != {}
+      || cfg.removedAssociations != {}
+    ) {
+      text = generators.toINI { } {
+        "Added Associations" = cfg.addedAssociations;
+        "Default Applications" = cfg.defaultApplications;
+        "Removed Associations" = cfg.removedAssociations;
+      };
+    };
+
+    environment.pathsToLink = [ "/share/mime" ];
+
+    environment.systemPackages = [
+      # this package also installs some useful data, as well as its utilities
+      pkgs.shared-mime-info
+    ];
+
+    environment.extraSetup = ''
+      if [ -w $out/share/mime ] && [ -d $out/share/mime/packages ]; then
+          XDG_DATA_DIRS=$out/share PKGSYSTEM_ENABLE_FSYNC=0 ${pkgs.buildPackages.shared-mime-info}/bin/update-mime-database -V $out/share/mime > /dev/null
+      fi
+
+      if [ -w $out/share/applications ]; then
+          ${pkgs.buildPackages.desktop-file-utils}/bin/update-desktop-database $out/share/applications
+      fi
+    '';
+  };
+
+}
diff --git a/nixos/modules/config/xdg/portal.nix b/nixos/modules/config/xdg/portal.nix
new file mode 100644
index 00000000000..088f2af59e2
--- /dev/null
+++ b/nixos/modules/config/xdg/portal.nix
@@ -0,0 +1,81 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "flatpak" "extraPortals" ] [ "xdg" "portal" "extraPortals" ])
+  ];
+
+  meta = {
+    maintainers = teams.freedesktop.members;
+  };
+
+  options.xdg.portal = {
+    enable =
+      mkEnableOption "<link xlink:href='https://github.com/flatpak/xdg-desktop-portal'>xdg desktop integration</link>" // {
+        default = false;
+      };
+
+    extraPortals = mkOption {
+      type = types.listOf types.package;
+      default = [ ];
+      description = ''
+        List of additional portals to add to path. Portals allow interaction
+        with system, like choosing files or taking screenshots. At minimum,
+        a desktop portal implementation should be listed. GNOME and KDE already
+        adds <package>xdg-desktop-portal-gtk</package>; and
+        <package>xdg-desktop-portal-kde</package> respectively. On other desktop
+        environments you probably want to add them yourself.
+      '';
+    };
+
+    gtkUsePortal = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Sets environment variable <literal>GTK_USE_PORTAL</literal> to <literal>1</literal>.
+        This is needed for packages ran outside Flatpak to respect and use XDG Desktop Portals.
+        For example, you'd need to set this for non-flatpak Firefox to use native filechoosers.
+        Defaults to <literal>false</literal> to respect its opt-in nature.
+      '';
+    };
+  };
+
+  config =
+    let
+      cfg = config.xdg.portal;
+      packages = [ pkgs.xdg-desktop-portal ] ++ cfg.extraPortals;
+      joinedPortals = pkgs.buildEnv {
+        name = "xdg-portals";
+        paths = packages;
+        pathsToLink = [ "/share/xdg-desktop-portal/portals" "/share/applications" ];
+      };
+
+    in
+    mkIf cfg.enable {
+
+      assertions = [
+        {
+          assertion = cfg.extraPortals != [ ];
+          message = "Setting xdg.portal.enable to true requires a portal implementation in xdg.portal.extraPortals such as xdg-desktop-portal-gtk or xdg-desktop-portal-kde.";
+        }
+      ];
+
+      services.dbus.packages = packages;
+      systemd.packages = packages;
+
+      environment = {
+        # fixes screen sharing on plasmawayland on non-chromium apps by linking
+        # share/applications/*.desktop files
+        # see https://github.com/NixOS/nixpkgs/issues/145174
+        systemPackages = [ joinedPortals ];
+        pathsToLink = [ "/share/applications" ];
+
+        sessionVariables = {
+          GTK_USE_PORTAL = mkIf cfg.gtkUsePortal "1";
+          XDG_DESKTOP_PORTAL_DIR = "${joinedPortals}/share/xdg-desktop-portal/portals";
+        };
+      };
+    };
+}
diff --git a/nixos/modules/config/xdg/portals/wlr.nix b/nixos/modules/config/xdg/portals/wlr.nix
new file mode 100644
index 00000000000..aba1d8dbc00
--- /dev/null
+++ b/nixos/modules/config/xdg/portals/wlr.nix
@@ -0,0 +1,67 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.xdg.portal.wlr;
+  package = pkgs.xdg-desktop-portal-wlr;
+  settingsFormat = pkgs.formats.ini { };
+  configFile = settingsFormat.generate "xdg-desktop-portal-wlr.ini" cfg.settings;
+in
+{
+  meta = {
+    maintainers = with maintainers; [ minijackson ];
+  };
+
+  options.xdg.portal.wlr = {
+    enable = mkEnableOption ''
+      desktop portal for wlroots-based desktops
+
+      This will add the <package>xdg-desktop-portal-wlr</package> package into
+      the <option>xdg.portal.extraPortals</option> option, and provide the
+      configuration file
+    '';
+
+    settings = mkOption {
+      description = ''
+        Configuration for <package>xdg-desktop-portal-wlr</package>.
+
+        See <literal>xdg-desktop-portal-wlr(5)</literal> for supported
+        values.
+      '';
+
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+      };
+
+      default = { };
+
+      # Example taken from the manpage
+      example = literalExpression ''
+        {
+          screencast = {
+            output_name = "HDMI-A-1";
+            max_fps = 30;
+            exec_before = "disable_notifications.sh";
+            exec_after = "enable_notifications.sh";
+            chooser_type = "simple";
+            chooser_cmd = "''${pkgs.slurp}/bin/slurp -f %o -or";
+          };
+        }
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    xdg.portal = {
+      enable = true;
+      extraPortals = [ package ];
+    };
+
+    systemd.user.services.xdg-desktop-portal-wlr.serviceConfig.ExecStart = [
+      # Empty ExecStart value to override the field
+      ""
+      "${package}/libexec/xdg-desktop-portal-wlr --config=${configFile}"
+    ];
+  };
+}
diff --git a/nixos/modules/config/xdg/sounds.nix b/nixos/modules/config/xdg/sounds.nix
new file mode 100644
index 00000000000..0b94f550929
--- /dev/null
+++ b/nixos/modules/config/xdg/sounds.nix
@@ -0,0 +1,30 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+{
+  meta = {
+    maintainers = teams.freedesktop.members;
+  };
+
+  options = {
+    xdg.sounds.enable = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to install files to support the
+        <link xlink:href="https://www.freedesktop.org/wiki/Specifications/sound-theme-spec/">XDG Sound Theme specification</link>.
+      '';
+    };
+  };
+
+  config = mkIf config.xdg.sounds.enable {
+    environment.systemPackages = [
+      pkgs.sound-theme-freedesktop
+    ];
+
+    environment.pathsToLink = [
+      "/share/sounds"
+    ];
+  };
+
+}
diff --git a/nixos/modules/config/zram.nix b/nixos/modules/config/zram.nix
new file mode 100644
index 00000000000..1f513b7e4da
--- /dev/null
+++ b/nixos/modules/config/zram.nix
@@ -0,0 +1,203 @@
+{ 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.
+    '')
+  ];
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    zramSwap = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable in-memory compressed devices and swap space provided by the zram
+          kernel module.
+          See <link xlink:href="https://www.kernel.org/doc/Documentation/blockdev/zram.txt">
+            https://www.kernel.org/doc/Documentation/blockdev/zram.txt
+          </link>.
+        '';
+      };
+
+      numDevices = mkOption {
+        default = 1;
+        type = types.int;
+        description = ''
+          Number of zram devices to create. See also
+          <literal>zramSwap.swapDevices</literal>
+        '';
+      };
+
+      swapDevices = mkOption {
+        default = null;
+        example = 1;
+        type = with types; nullOr int;
+        description = ''
+          Number of zram devices to be used as swap. Must be
+          <literal>&lt;= zramSwap.numDevices</literal>.
+          Default is same as <literal>zramSwap.numDevices</literal>, recommended is 1.
+        '';
+      };
+
+      memoryPercent = mkOption {
+        default = 50;
+        type = types.int;
+        description = ''
+          Maximum amount of memory that can be used by the zram swap devices
+          (as a percentage of your total memory). Defaults to 1/2 of your total
+          RAM. Run <literal>zramctl</literal> to check how good memory is
+          compressed.
+        '';
+      };
+
+      memoryMax = mkOption {
+        default = null;
+        type = with types; nullOr int;
+        description = ''
+          Maximum total amount of memory (in bytes) that can be used by the zram
+          swap devices.
+        '';
+      };
+
+      priority = mkOption {
+        default = 5;
+        type = types.int;
+        description = ''
+          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
+          fill the zram swap devices before falling back to disk swap).
+        '';
+      };
+
+      algorithm = mkOption {
+        default = "zstd";
+        example = "lz4";
+        type = with types; either (enum [ "lzo" "lz4" "zstd" ]) str;
+        description = ''
+          Compression algorithm. <literal>lzo</literal> has good compression,
+          but is slow. <literal>lz4</literal> has bad compression, but is fast.
+          <literal>zstd</literal> is both good compression and fast, but requires newer kernel.
+          You can check what other algorithms are supported by your zram device with
+          <programlisting>cat /sys/class/block/zram*/comp_algorithm</programlisting>
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    inherit warnings;
+
+    system.requiredKernelConfig = with config.lib.kernelConfig; [
+      (isModule "ZRAM")
+    ];
+
+    # Disabling this for the moment, as it would create and mkswap devices twice,
+    # 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}
+    '';
+
+    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;
+
+  };
+
+}
diff --git a/nixos/modules/hardware/acpilight.nix b/nixos/modules/hardware/acpilight.nix
new file mode 100644
index 00000000000..2de448a265c
--- /dev/null
+++ b/nixos/modules/hardware/acpilight.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.hardware.acpilight;
+in
+{
+  options = {
+    hardware.acpilight = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable acpilight.
+          This will allow brightness control via xbacklight from users in the video group
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ acpilight ];
+    services.udev.packages = with pkgs; [ acpilight ];
+  };
+}
diff --git a/nixos/modules/hardware/all-firmware.nix b/nixos/modules/hardware/all-firmware.nix
new file mode 100644
index 00000000000..5b60b17312f
--- /dev/null
+++ b/nixos/modules/hardware/all-firmware.nix
@@ -0,0 +1,93 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware;
+in {
+
+  imports = [
+    (mkRenamedOptionModule [ "networking" "enableRT73Firmware" ] [ "hardware" "enableRedistributableFirmware" ])
+    (mkRenamedOptionModule [ "networking" "enableIntel3945ABGFirmware" ] [ "hardware" "enableRedistributableFirmware" ])
+    (mkRenamedOptionModule [ "networking" "enableIntel2100BGFirmware" ] [ "hardware" "enableRedistributableFirmware" ])
+    (mkRenamedOptionModule [ "networking" "enableRalinkFirmware" ] [ "hardware" "enableRedistributableFirmware" ])
+    (mkRenamedOptionModule [ "networking" "enableRTL8192cFirmware" ] [ "hardware" "enableRedistributableFirmware" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    hardware.enableAllFirmware = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Turn on this option if you want to enable all the firmware.
+      '';
+    };
+
+    hardware.enableRedistributableFirmware = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Turn on this option if you want to enable all the firmware with a license allowing redistribution.
+      '';
+    };
+
+    hardware.wirelessRegulatoryDatabase = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Load the wireless regulatory database at boot.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkMerge [
+    (mkIf (cfg.enableAllFirmware || cfg.enableRedistributableFirmware) {
+      hardware.firmware = with pkgs; [
+        linux-firmware
+        intel2200BGFirmware
+        rtl8192su-firmware
+        rt5677-firmware
+        rtl8723bs-firmware
+        rtl8761b-firmware
+        rtw88-firmware
+        zd1211fw
+        alsa-firmware
+        sof-firmware
+        libreelec-dvb-firmware
+      ] ++ optional (pkgs.stdenv.hostPlatform.isAarch32 || pkgs.stdenv.hostPlatform.isAarch64) 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;
+    })
+    (mkIf cfg.enableAllFirmware {
+      assertions = [{
+        assertion = !cfg.enableAllFirmware || (config.nixpkgs.config.allowUnfree or false);
+        message = ''
+          the list of hardware.enableAllFirmware contains non-redistributable licensed firmware files.
+            This requires nixpkgs.config.allowUnfree to be true.
+            An alternative is to use the hardware.enableRedistributableFirmware option.
+        '';
+      }];
+      hardware.firmware = with pkgs; [
+        broadcom-bt-firmware
+        b43Firmware_5_1_138
+        b43Firmware_6_30_163_46
+        b43FirmwareCutter
+        xow_dongle-firmware
+      ] ++ optional pkgs.stdenv.hostPlatform.isx86 facetimehd-firmware;
+    })
+    (mkIf cfg.wirelessRegulatoryDatabase {
+      hardware.firmware = [ pkgs.wireless-regdb ];
+    })
+  ];
+}
diff --git a/nixos/modules/hardware/bladeRF.nix b/nixos/modules/hardware/bladeRF.nix
new file mode 100644
index 00000000000..35b74b8382e
--- /dev/null
+++ b/nixos/modules/hardware/bladeRF.nix
@@ -0,0 +1,28 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.bladeRF;
+
+in
+
+{
+  options.hardware.bladeRF = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enables udev rules for BladeRF devices. By default grants access
+        to users in the "bladerf" group. You may want to install the
+        libbladeRF package.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [ pkgs.libbladeRF ];
+    users.groups.bladerf = {};
+  };
+}
diff --git a/nixos/modules/hardware/brillo.nix b/nixos/modules/hardware/brillo.nix
new file mode 100644
index 00000000000..e970c948099
--- /dev/null
+++ b/nixos/modules/hardware/brillo.nix
@@ -0,0 +1,22 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.hardware.brillo;
+in
+{
+  options = {
+    hardware.brillo = {
+      enable = mkEnableOption ''
+        Enable brillo in userspace.
+        This will allow brightness control from users in the video group.
+      '';
+    };
+  };
+
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [ pkgs.brillo ];
+    environment.systemPackages = [ pkgs.brillo ];
+  };
+}
diff --git a/nixos/modules/hardware/ckb-next.nix b/nixos/modules/hardware/ckb-next.nix
new file mode 100644
index 00000000000..b2bbd77c9d7
--- /dev/null
+++ b/nixos/modules/hardware/ckb-next.nix
@@ -0,0 +1,53 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.ckb-next;
+
+in
+  {
+    imports = [
+      (mkRenamedOptionModule [ "hardware" "ckb" "enable" ] [ "hardware" "ckb-next" "enable" ])
+      (mkRenamedOptionModule [ "hardware" "ckb" "package" ] [ "hardware" "ckb-next" "package" ])
+    ];
+
+    options.hardware.ckb-next = {
+      enable = mkEnableOption "the Corsair keyboard/mouse driver";
+
+      gid = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        example = 100;
+        description = ''
+          Limit access to the ckb daemon to a particular group.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.ckb-next;
+        defaultText = literalExpression "pkgs.ckb-next";
+        description = ''
+          The package implementing the Corsair keyboard/mouse driver.
+        '';
+      };
+    };
+
+    config = mkIf cfg.enable {
+      environment.systemPackages = [ cfg.package ];
+
+      systemd.services.ckb-next = {
+        description = "Corsair Keyboards and Mice Daemon";
+        wantedBy = ["multi-user.target"];
+        serviceConfig = {
+          ExecStart = "${cfg.package}/bin/ckb-next-daemon ${optionalString (cfg.gid != null) "--gid=${builtins.toString cfg.gid}"}";
+          Restart = "on-failure";
+        };
+      };
+    };
+
+    meta = {
+      maintainers = with lib.maintainers; [ kierdavis ];
+    };
+  }
diff --git a/nixos/modules/hardware/corectrl.nix b/nixos/modules/hardware/corectrl.nix
new file mode 100644
index 00000000000..3185f6486c7
--- /dev/null
+++ b/nixos/modules/hardware/corectrl.nix
@@ -0,0 +1,62 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.corectrl;
+in
+{
+  options.programs.corectrl = {
+    enable = mkEnableOption ''
+      A tool to overclock amd graphics cards and processors.
+      Add your user to the corectrl group to run corectrl without needing to enter your password
+    '';
+
+    gpuOverclock = {
+      enable = mkEnableOption ''
+        true
+      '';
+      ppfeaturemask = mkOption {
+        type = types.str;
+        default = "0xfffd7fff";
+        example = "0xffffffff";
+        description = ''
+          Sets the `amdgpu.ppfeaturemask` kernel option.
+          In particular, it is used here to set the overdrive bit.
+          Default is `0xfffd7fff` as it is less likely to cause flicker issues.
+          Setting it to `0xffffffff` enables all features.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable (lib.mkMerge [
+    {
+      environment.systemPackages = [ pkgs.corectrl ];
+
+      services.dbus.packages = [ pkgs.corectrl ];
+
+      users.groups.corectrl = { };
+
+      security.polkit.extraConfig = ''
+        polkit.addRule(function(action, subject) {
+            if ((action.id == "org.corectrl.helper.init" ||
+                 action.id == "org.corectrl.helperkiller.init") &&
+                subject.local == true &&
+                subject.active == true &&
+                subject.isInGroup("corectrl")) {
+                    return polkit.Result.YES;
+            }
+        });
+      '';
+    }
+
+    (lib.mkIf cfg.gpuOverclock.enable {
+      # https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/gpu/drm/amd/include/amd_shared.h#n169
+      # The overdrive bit
+      boot.kernelParams = [ "amdgpu.ppfeaturemask=${cfg.gpuOverclock.ppfeaturemask}" ];
+    })
+  ]);
+
+  meta.maintainers = with lib.maintainers; [ artturin ];
+}
diff --git a/nixos/modules/hardware/cpu/amd-microcode.nix b/nixos/modules/hardware/cpu/amd-microcode.nix
new file mode 100644
index 00000000000..621c7066bfe
--- /dev/null
+++ b/nixos/modules/hardware/cpu/amd-microcode.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    hardware.cpu.amd.updateMicrocode = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Update the CPU microcode for AMD processors.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.hardware.cpu.amd.updateMicrocode {
+    # Microcode updates must be the first item prepended in the initrd
+    boot.initrd.prepend = mkOrder 1 [ "${pkgs.microcodeAmd}/amd-ucode.img" ];
+  };
+
+}
diff --git a/nixos/modules/hardware/cpu/intel-microcode.nix b/nixos/modules/hardware/cpu/intel-microcode.nix
new file mode 100644
index 00000000000..acce565fd80
--- /dev/null
+++ b/nixos/modules/hardware/cpu/intel-microcode.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    hardware.cpu.intel.updateMicrocode = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Update the CPU microcode for Intel processors.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.hardware.cpu.intel.updateMicrocode {
+    # Microcode updates must be the first item prepended in the initrd
+    boot.initrd.prepend = mkOrder 1 [ "${pkgs.microcodeIntel}/intel-ucode.img" ];
+  };
+
+}
diff --git a/nixos/modules/hardware/cpu/intel-sgx.nix b/nixos/modules/hardware/cpu/intel-sgx.nix
new file mode 100644
index 00000000000..1355ee753f0
--- /dev/null
+++ b/nixos/modules/hardware/cpu/intel-sgx.nix
@@ -0,0 +1,69 @@
+{ config, lib, ... }:
+with lib;
+let
+  cfg = config.hardware.cpu.intel.sgx;
+  defaultPrvGroup = "sgx_prv";
+in
+{
+  options.hardware.cpu.intel.sgx.enableDcapCompat = mkOption {
+    description = ''
+      Whether to enable backward compatibility for SGX software build for the
+      out-of-tree Intel SGX DCAP driver.
+
+      Creates symbolic links for the SGX devices <literal>/dev/sgx_enclave</literal>
+      and <literal>/dev/sgx_provision</literal> to make them available as
+      <literal>/dev/sgx/enclave</literal>  and <literal>/dev/sgx/provision</literal>,
+      respectively.
+    '';
+    type = types.bool;
+    default = true;
+  };
+
+  options.hardware.cpu.intel.sgx.provision = {
+    enable = mkEnableOption "access to the Intel SGX provisioning device";
+    user = mkOption {
+      description = "Owner to assign to the SGX provisioning device.";
+      type = types.str;
+      default = "root";
+    };
+    group = mkOption {
+      description = "Group to assign to the SGX provisioning device.";
+      type = types.str;
+      default = defaultPrvGroup;
+    };
+    mode = mkOption {
+      description = "Mode to set for the SGX provisioning device.";
+      type = types.str;
+      default = "0660";
+    };
+  };
+
+  config = mkMerge [
+    (mkIf cfg.provision.enable {
+      assertions = [
+        {
+          assertion = hasAttr cfg.provision.user config.users.users;
+          message = "Given user does not exist";
+        }
+        {
+          assertion = (cfg.provision.group == defaultPrvGroup) || (hasAttr cfg.provision.group config.users.groups);
+          message = "Given group does not exist";
+        }
+      ];
+
+      users.groups = optionalAttrs (cfg.provision.group == defaultPrvGroup) {
+        "${cfg.provision.group}" = { };
+      };
+
+      services.udev.extraRules = with cfg.provision; ''
+        SUBSYSTEM=="misc", KERNEL=="sgx_provision", OWNER="${user}", GROUP="${group}", MODE="${mode}"
+      '';
+    })
+    (mkIf cfg.enableDcapCompat {
+      services.udev.extraRules = ''
+        SUBSYSTEM=="misc", KERNEL=="sgx_enclave",   SYMLINK+="sgx/enclave"
+        SUBSYSTEM=="misc", KERNEL=="sgx_provision", SYMLINK+="sgx/provision"
+      '';
+    })
+  ];
+}
diff --git a/nixos/modules/hardware/device-tree.nix b/nixos/modules/hardware/device-tree.nix
new file mode 100644
index 00000000000..be67116ad50
--- /dev/null
+++ b/nixos/modules/hardware/device-tree.nix
@@ -0,0 +1,205 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.deviceTree;
+
+  overlayType = types.submodule {
+    options = {
+      name = mkOption {
+        type = types.str;
+        description = ''
+          Name of this overlay
+        '';
+      };
+
+      dtsFile = mkOption {
+        type = types.nullOr types.path;
+        description = ''
+          Path to .dts overlay file, overlay is applied to
+          each .dtb file matching "compatible" of the overlay.
+        '';
+        default = null;
+        example = literalExpression "./dts/overlays.dts";
+      };
+
+      dtsText = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Literal DTS contents, overlay is applied to
+          each .dtb file matching "compatible" of the overlay.
+        '';
+        example = ''
+          /dts-v1/;
+          /plugin/;
+          / {
+                  compatible = "raspberrypi";
+                  fragment@0 {
+                          target-path = "/soc";
+                          __overlay__ {
+                                  pps {
+                                          compatible = "pps-gpio";
+                                          status = "okay";
+                                  };
+                          };
+                  };
+          };
+        '';
+      };
+
+      dtboFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          Path to .dtbo compiled overlay file.
+        '';
+      };
+    };
+  };
+
+  # this requires kernel package
+  dtbsWithSymbols = pkgs.stdenv.mkDerivation {
+    name = "dtbs-with-symbols";
+    inherit (cfg.kernelPackage) src nativeBuildInputs depsBuildBuild;
+    patches = map (patch: patch.patch) cfg.kernelPackage.kernelPatches;
+    buildPhase = ''
+      patchShebangs scripts/*
+      substituteInPlace scripts/Makefile.lib \
+        --replace 'DTC_FLAGS += $(DTC_FLAGS_$(basetarget))' 'DTC_FLAGS += $(DTC_FLAGS_$(basetarget)) -@'
+      make ${pkgs.stdenv.hostPlatform.linux-kernel.baseConfig} ARCH="${pkgs.stdenv.hostPlatform.linuxArch}"
+      make dtbs ARCH="${pkgs.stdenv.hostPlatform.linuxArch}"
+    '';
+    installPhase = ''
+      make dtbs_install INSTALL_DTBS_PATH=$out/dtbs  ARCH="${pkgs.stdenv.hostPlatform.linuxArch}"
+    '';
+  };
+
+  filterDTBs = src: if isNull cfg.filter
+    then "${src}/dtbs"
+    else
+      pkgs.runCommand "dtbs-filtered" {} ''
+        mkdir -p $out
+        cd ${src}/dtbs
+        find . -type f -name '${cfg.filter}' -print0 \
+          | xargs -0 cp -v --no-preserve=mode --target-directory $out --parents
+      '';
+
+  # Compile single Device Tree overlay source
+  # file (.dts) into its compiled variant (.dtbo)
+  compileDTS = name: f: pkgs.callPackage({ dtc }: pkgs.stdenv.mkDerivation {
+    name = "${name}-dtbo";
+
+    nativeBuildInputs = [ dtc ];
+
+    buildCommand = ''
+      dtc -I dts ${f} -O dtb -@ -o $out
+    '';
+  }) {};
+
+  # 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
+      else compileDTS o.name (pkgs.writeText "dts" o.dtsText)
+    else o.dtboFile; } );
+
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "hardware" "deviceTree" "base" ] "Use hardware.deviceTree.kernelPackage instead")
+  ];
+
+  options = {
+      hardware.deviceTree = {
+        enable = mkOption {
+          default = pkgs.stdenv.hostPlatform.linux-kernel.DTB or false;
+          type = types.bool;
+          description = ''
+            Build device tree files. These are used to describe the
+            non-discoverable hardware of a system.
+          '';
+        };
+
+        kernelPackage = mkOption {
+          default = config.boot.kernelPackages.kernel;
+          defaultText = literalExpression "config.boot.kernelPackages.kernel";
+          example = literalExpression "pkgs.linux_latest";
+          type = types.path;
+          description = ''
+            Kernel package containing the base device-tree (.dtb) to boot. Uses
+            device trees bundled with the Linux kernel by default.
+          '';
+        };
+
+        name = mkOption {
+          default = null;
+          example = "some-dtb.dtb";
+          type = types.nullOr types.str;
+          description = ''
+            The name of an explicit dtb to be loaded, relative to the dtb base.
+            Useful in extlinux scenarios if the bootloader doesn't pick the
+            right .dtb file from FDTDIR.
+          '';
+        };
+
+        filter = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          example = "*rpi*.dtb";
+          description = ''
+            Only include .dtb files matching glob expression.
+          '';
+        };
+
+        overlays = mkOption {
+          default = [];
+          example = literalExpression ''
+            [
+              { name = "pps"; dtsFile = ./dts/pps.dts; }
+              { name = "spi";
+                dtsText = "...";
+              }
+              { name = "precompiled"; dtboFile = ./dtbos/example.dtbo; }
+            ]
+          '';
+          type = types.listOf (types.coercedTo types.path (path: {
+            name = baseNameOf path;
+            dtboFile = path;
+          }) overlayType);
+          description = ''
+            List of overlays to apply to base device-tree (.dtb) files.
+          '';
+        };
+
+        package = mkOption {
+          default = null;
+          type = types.nullOr types.path;
+          internal = true;
+          description = ''
+            A path containing the result of applying `overlays` to `kernelPackage`.
+          '';
+        };
+      };
+  };
+
+  config = mkIf (cfg.enable) {
+
+    assertions = let
+      invalidOverlay = o: isNull o.dtsFile && isNull o.dtsText && isNull o.dtboFile;
+    in lib.singleton {
+      assertion = lib.all (o: !invalidOverlay o) cfg.overlays;
+      message = ''
+        deviceTree overlay needs one of dtsFile, dtsText or dtboFile set.
+        Offending overlay(s):
+        ${toString (map (o: o.name) (builtins.filter invalidOverlay cfg.overlays))}
+      '';
+    };
+
+    hardware.deviceTree.package = if (cfg.overlays != [])
+      then pkgs.deviceTree.applyOverlays (filterDTBs dtbsWithSymbols) (withDTBOs cfg.overlays)
+      else (filterDTBs cfg.kernelPackage);
+  };
+}
diff --git a/nixos/modules/hardware/digitalbitbox.nix b/nixos/modules/hardware/digitalbitbox.nix
new file mode 100644
index 00000000000..097448a74f4
--- /dev/null
+++ b/nixos/modules/hardware/digitalbitbox.nix
@@ -0,0 +1,30 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.digitalbitbox;
+in
+
+{
+  options.hardware.digitalbitbox = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enables udev rules for Digital Bitbox devices.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.digitalbitbox;
+      defaultText = literalExpression "pkgs.digitalbitbox";
+      description = "The Digital Bitbox package to use. This can be used to install a package with udev rules that differ from the defaults.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/hardware/flirc.nix b/nixos/modules/hardware/flirc.nix
new file mode 100644
index 00000000000..94ec715b9fa
--- /dev/null
+++ b/nixos/modules/hardware/flirc.nix
@@ -0,0 +1,12 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.hardware.flirc;
+in
+{
+  options.hardware.flirc.enable = lib.mkEnableOption "software to configure a Flirc USB device";
+
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.flirc ];
+    services.udev.packages = [ pkgs.flirc ];
+  };
+}
diff --git a/nixos/modules/hardware/gkraken.nix b/nixos/modules/hardware/gkraken.nix
new file mode 100644
index 00000000000..97d15369db0
--- /dev/null
+++ b/nixos/modules/hardware/gkraken.nix
@@ -0,0 +1,18 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.gkraken;
+in
+{
+  options.hardware.gkraken = {
+    enable = mkEnableOption "gkraken's udev rules for NZXT AIO liquid coolers";
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = with pkgs; [
+      gkraken
+    ];
+  };
+}
diff --git a/nixos/modules/hardware/gpgsmartcards.nix b/nixos/modules/hardware/gpgsmartcards.nix
new file mode 100644
index 00000000000..6e5fcda6b85
--- /dev/null
+++ b/nixos/modules/hardware/gpgsmartcards.nix
@@ -0,0 +1,37 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  # gnupg's manual describes how to setup ccid udev rules:
+  #   https://www.gnupg.org/howtos/card-howto/en/ch02s03.html
+  # gnupg folks advised me (https://dev.gnupg.org/T5409) to look at debian's rules:
+  # https://salsa.debian.org/debian/gnupg2/-/blob/debian/main/debian/scdaemon.udev
+
+  # the latest rev of the entire debian gnupg2 repo as of 2021-04-28
+  # the scdaemon.udev file was last commited on 2021-01-05 (7817a03):
+  scdaemonUdevRev = "01898735a015541e3ffb43c7245ac1e612f40836";
+
+  scdaemonRules = pkgs.fetchurl {
+    url = "https://salsa.debian.org/debian/gnupg2/-/raw/${scdaemonUdevRev}/debian/scdaemon.udev";
+    sha256 = "08v0vp6950bz7galvc92zdss89y9vcwbinmbfcdldy8x72w6rqr3";
+  };
+
+  # per debian's udev deb hook (https://man7.org/linux/man-pages/man1/dh_installudev.1.html)
+  destination = "60-scdaemon.rules";
+
+  scdaemonUdevRulesPkg = pkgs.runCommandNoCC "scdaemon-udev-rules" {} ''
+    loc="$out/lib/udev/rules.d/"
+    mkdir -p "''${loc}"
+    cp "${scdaemonRules}" "''${loc}/${destination}"
+  '';
+
+  cfg = config.hardware.gpgSmartcards;
+in {
+  options.hardware.gpgSmartcards = {
+    enable = mkEnableOption "udev rules for gnupg smart cards";
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [ scdaemonUdevRulesPkg ];
+  };
+}
diff --git a/nixos/modules/hardware/hackrf.nix b/nixos/modules/hardware/hackrf.nix
new file mode 100644
index 00000000000..7f03b765bbd
--- /dev/null
+++ b/nixos/modules/hardware/hackrf.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.hardware.hackrf;
+
+in
+{
+  options.hardware.hackrf = {
+    enable = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = ''
+        Enables hackrf udev rules and ensures 'plugdev' group exists.
+        This is a prerequisite to using HackRF devices without being root, since HackRF USB descriptors will be owned by plugdev through udev.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.udev.packages = [ pkgs.hackrf ];
+    users.groups.plugdev = { };
+  };
+}
diff --git a/nixos/modules/hardware/i2c.nix b/nixos/modules/hardware/i2c.nix
new file mode 100644
index 00000000000..ff14b4b1c89
--- /dev/null
+++ b/nixos/modules/hardware/i2c.nix
@@ -0,0 +1,43 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.i2c;
+in
+
+{
+  options.hardware.i2c = {
+    enable = mkEnableOption ''
+      i2c devices support. By default access is granted to users in the "i2c"
+      group (will be created if non-existent) and any user with a seat, meaning
+      logged on the computer locally.
+    '';
+
+    group = mkOption {
+      type = types.str;
+      default = "i2c";
+      description = ''
+        Grant access to i2c devices (/dev/i2c-*) to users in this group.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    boot.kernelModules = [ "i2c-dev" ];
+
+    users.groups = mkIf (cfg.group == "i2c") {
+      i2c = { };
+    };
+
+    services.udev.extraRules = ''
+      # allow group ${cfg.group} and users with a seat use of i2c devices
+      ACTION=="add", KERNEL=="i2c-[0-9]*", TAG+="uaccess", GROUP="${cfg.group}", MODE="660"
+    '';
+
+  };
+
+  meta.maintainers = [ maintainers.rnhmjoj ];
+
+}
diff --git a/nixos/modules/hardware/keyboard/teck.nix b/nixos/modules/hardware/keyboard/teck.nix
new file mode 100644
index 00000000000..091ddb81962
--- /dev/null
+++ b/nixos/modules/hardware/keyboard/teck.nix
@@ -0,0 +1,16 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.hardware.keyboard.teck;
+in
+{
+  options.hardware.keyboard.teck = {
+    enable = mkEnableOption "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/zsa.nix b/nixos/modules/hardware/keyboard/zsa.nix
new file mode 100644
index 00000000000..bb69cfa0bf0
--- /dev/null
+++ b/nixos/modules/hardware/keyboard/zsa.nix
@@ -0,0 +1,24 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkOption mkIf types;
+  cfg = config.hardware.keyboard.zsa;
+in
+{
+  options.hardware.keyboard.zsa = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enables udev rules for keyboards from ZSA like the ErgoDox EZ, Planck EZ and Moonlander Mark I.
+        You need it when you want to flash a new configuration on the keyboard
+        or use their live training in the browser.
+        You may want to install the wally-cli package.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [ pkgs.zsa-udev-rules ];
+  };
+}
diff --git a/nixos/modules/hardware/ksm.nix b/nixos/modules/hardware/ksm.nix
new file mode 100644
index 00000000000..829c3532c45
--- /dev/null
+++ b/nixos/modules/hardware/ksm.nix
@@ -0,0 +1,38 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.ksm;
+
+in {
+  imports = [
+    (mkRenamedOptionModule [ "hardware" "enableKSM" ] [ "hardware" "ksm" "enable" ])
+  ];
+
+  options.hardware.ksm = {
+    enable = mkEnableOption "Kernel Same-Page Merging";
+    sleep = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      description = ''
+        How many milliseconds ksmd should sleep between scans.
+        Setting it to <literal>null</literal> uses the kernel's default time.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.enable-ksm = {
+      description = "Enable Kernel Same-Page Merging";
+      wantedBy = [ "multi-user.target" ];
+      script =
+        ''
+          echo 1 > /sys/kernel/mm/ksm/run
+        '' + optionalString (cfg.sleep != null)
+        ''
+          echo ${toString cfg.sleep} > /sys/kernel/mm/ksm/sleep_millisecs
+        '';
+    };
+  };
+}
diff --git a/nixos/modules/hardware/ledger.nix b/nixos/modules/hardware/ledger.nix
new file mode 100644
index 00000000000..41abe74315a
--- /dev/null
+++ b/nixos/modules/hardware/ledger.nix
@@ -0,0 +1,14 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.ledger;
+
+in {
+  options.hardware.ledger.enable = mkEnableOption "udev rules for Ledger devices";
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [ pkgs.ledger-udev-rules ];
+  };
+}
diff --git a/nixos/modules/hardware/logitech.nix b/nixos/modules/hardware/logitech.nix
new file mode 100644
index 00000000000..3ebe6aacf5d
--- /dev/null
+++ b/nixos/modules/hardware/logitech.nix
@@ -0,0 +1,96 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.logitech;
+
+  vendor = "046d";
+
+  daemon = "g15daemon";
+
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "hardware" "logitech" "enable" ] [ "hardware" "logitech" "wireless" "enable" ])
+    (mkRenamedOptionModule [ "hardware" "logitech" "enableGraphical" ] [ "hardware" "logitech" "wireless" "enableGraphical" ])
+  ];
+
+  options.hardware.logitech = {
+
+    lcd = {
+      enable = mkEnableOption "Logitech LCD Devices";
+
+      startWhenNeeded = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Only run the service when an actual supported device is plugged.
+        '';
+      };
+
+      devices = mkOption {
+        type = types.listOf types.str;
+        default = [ "0a07" "c222" "c225" "c227" "c251" ];
+        description = ''
+          List of USB device ids supported by g15daemon.
+          </para>
+          <para>
+          You most likely do not need to change this.
+        '';
+      };
+    };
+
+    wireless = {
+      enable = mkEnableOption "Logitech Wireless Devices";
+
+      enableGraphical = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable graphical support applications.";
+      };
+    };
+  };
+
+  config = lib.mkIf (cfg.wireless.enable || cfg.lcd.enable) {
+    environment.systemPackages = []
+      ++ lib.optional cfg.wireless.enable pkgs.ltunify
+      ++ lib.optional cfg.wireless.enableGraphical pkgs.solaar;
+
+    services.udev = {
+      # ltunifi and solaar both provide udev rules but the most up-to-date have been split
+      # out into a dedicated derivation
+
+      packages = []
+      ++ lib.optional cfg.wireless.enable pkgs.logitech-udev-rules
+      ++ lib.optional cfg.lcd.enable pkgs.g15daemon;
+
+      extraRules = ''
+        # nixos: hardware.logitech.lcd
+      '' + lib.concatMapStringsSep "\n" (
+        dev:
+          ''ACTION=="add", SUBSYSTEMS=="usb", ATTRS{idVendor}=="${vendor}", ATTRS{idProduct}=="${dev}", TAG+="systemd", ENV{SYSTEMD_WANTS}+="${daemon}.service"''
+      ) cfg.lcd.devices;
+    };
+
+    systemd.services."${daemon}" = lib.mkIf cfg.lcd.enable {
+      description = "Logitech LCD Support Daemon";
+      documentation = [ "man:g15daemon(1)" ];
+      wantedBy = lib.mkIf (! cfg.lcd.startWhenNeeded) "multi-user.target";
+
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${pkgs.g15daemon}/bin/g15daemon";
+        # we patch it to write to /run/g15daemon/g15daemon.pid instead of
+        # /run/g15daemon.pid so systemd will do the cleanup for us.
+        PIDFile = "/run/${daemon}/g15daemon.pid";
+        PrivateTmp = true;
+        PrivateNetwork = true;
+        ProtectHome = "tmpfs";
+        ProtectSystem = "full"; # strict doesn't work
+        RuntimeDirectory = daemon;
+        Restart = "on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/hardware/mcelog.nix b/nixos/modules/hardware/mcelog.nix
new file mode 100644
index 00000000000..13ad238870c
--- /dev/null
+++ b/nixos/modules/hardware/mcelog.nix
@@ -0,0 +1,35 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  meta.maintainers = with maintainers; [ grahamc ];
+  options = {
+
+    hardware.mcelog = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the Machine Check Exception logger.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf config.hardware.mcelog.enable {
+    systemd = {
+      packages = [ pkgs.mcelog ];
+
+      services.mcelog = {
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          ProtectHome = true;
+          PrivateNetwork = true;
+          PrivateTmp = true;
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/hardware/network/ath-user-regd.nix b/nixos/modules/hardware/network/ath-user-regd.nix
new file mode 100644
index 00000000000..b5ade5ed501
--- /dev/null
+++ b/nixos/modules/hardware/network/ath-user-regd.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  kernelVersion = config.boot.kernelPackages.kernel.version;
+  linuxKernelMinVersion = "5.8";
+  kernelPatch = pkgs.kernelPatches.ath_regd_optional // {
+    extraConfig = ''
+      ATH_USER_REGD y
+    '';
+  };
+in
+{
+  options.networking.wireless.athUserRegulatoryDomain = mkOption {
+    default = false;
+    type = types.bool;
+    description = ''
+      If enabled, sets the ATH_USER_REGD kernel config switch to true to
+      disable the enforcement of EEPROM regulatory restrictions for ath
+      drivers. Requires at least Linux ${linuxKernelMinVersion}.
+    '';
+  };
+
+  config = mkIf config.networking.wireless.athUserRegulatoryDomain {
+    assertions = singleton {
+      assertion = lessThan 0 (builtins.compareVersions kernelVersion linuxKernelMinVersion);
+      message = "ATH_USER_REGD patch for kernels older than ${linuxKernelMinVersion} not ported yet!";
+    };
+    boot.kernelPatches = [ kernelPatch ];
+  };
+}
diff --git a/nixos/modules/hardware/network/b43.nix b/nixos/modules/hardware/network/b43.nix
new file mode 100644
index 00000000000..eb03bf223cc
--- /dev/null
+++ b/nixos/modules/hardware/network/b43.nix
@@ -0,0 +1,30 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let kernelVersion = config.boot.kernelPackages.kernel.version; in
+
+{
+
+  ###### interface
+
+  options = {
+
+    networking.enableB43Firmware = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Turn on this option if you want firmware for the NICs supported by the b43 module.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.networking.enableB43Firmware {
+    hardware.firmware = [ pkgs.b43Firmware_5_1_138 ];
+  };
+
+}
diff --git a/nixos/modules/hardware/network/broadcom-43xx.nix b/nixos/modules/hardware/network/broadcom-43xx.nix
new file mode 100644
index 00000000000..c92b7a0509d
--- /dev/null
+++ b/nixos/modules/hardware/network/broadcom-43xx.nix
@@ -0,0 +1,3 @@
+{
+  hardware.enableRedistributableFirmware = true;
+}
diff --git a/nixos/modules/hardware/network/intel-2200bg.nix b/nixos/modules/hardware/network/intel-2200bg.nix
new file mode 100644
index 00000000000..17b973474c9
--- /dev/null
+++ b/nixos/modules/hardware/network/intel-2200bg.nix
@@ -0,0 +1,30 @@
+{ config, pkgs, lib, ... }:
+
+{
+
+  ###### interface
+
+  options = {
+
+    networking.enableIntel2200BGFirmware = lib.mkOption {
+      default = false;
+      type = lib.types.bool;
+      description = ''
+        Turn on this option if you want firmware for the Intel
+        PRO/Wireless 2200BG to be loaded automatically.  This is
+        required if you want to use this device.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = lib.mkIf config.networking.enableIntel2200BGFirmware {
+
+    hardware.firmware = [ pkgs.intel2200BGFirmware ];
+
+  };
+
+}
diff --git a/nixos/modules/hardware/network/smc-2632w/default.nix b/nixos/modules/hardware/network/smc-2632w/default.nix
new file mode 100644
index 00000000000..b00286464f3
--- /dev/null
+++ b/nixos/modules/hardware/network/smc-2632w/default.nix
@@ -0,0 +1,9 @@
+{lib, ...}:
+
+{
+  hardware = {
+    pcmcia = {
+      firmware = [ (lib.cleanSource ./firmware) ];
+    };
+  };
+}
diff --git a/nixos/modules/hardware/network/smc-2632w/firmware/cis/SMC2632W-v1.02.cis b/nixos/modules/hardware/network/smc-2632w/firmware/cis/SMC2632W-v1.02.cis
new file mode 100644
index 00000000000..5f13088c373
--- /dev/null
+++ b/nixos/modules/hardware/network/smc-2632w/firmware/cis/SMC2632W-v1.02.cis
@@ -0,0 +1,8 @@
+  vers_1 5.0, "SMC", "SMC2632W", "Version 01.02", ""
+  manfid 0x0156, 0x0002
+  funcid network_adapter
+  cftable_entry 0x01 [default]
+    Vcc Vmin 3000mV Vmax 3300mV Iavg 300mA Ipeak 300mA
+    Idown 10mA
+    io 0x0000-0x003f [lines=6] [16bit]
+    irq mask 0xffff [level] [pulse]
diff --git a/nixos/modules/hardware/network/zydas-zd1211.nix b/nixos/modules/hardware/network/zydas-zd1211.nix
new file mode 100644
index 00000000000..5dd7f30ed82
--- /dev/null
+++ b/nixos/modules/hardware/network/zydas-zd1211.nix
@@ -0,0 +1,5 @@
+{pkgs, ...}:
+
+{
+  hardware.firmware = [ pkgs.zd1211fw ];
+}
diff --git a/nixos/modules/hardware/nitrokey.nix b/nixos/modules/hardware/nitrokey.nix
new file mode 100644
index 00000000000..baa07203118
--- /dev/null
+++ b/nixos/modules/hardware/nitrokey.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.hardware.nitrokey;
+
+in
+
+{
+  options.hardware.nitrokey = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enables udev rules for Nitrokey devices. By default grants access
+        to users in the "nitrokey" group. You may want to install the
+        nitrokey-app package, depending on your device and needs.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [ pkgs.nitrokey-udev-rules ];
+  };
+}
diff --git a/nixos/modules/hardware/onlykey/default.nix b/nixos/modules/hardware/onlykey/default.nix
new file mode 100644
index 00000000000..07358c8a878
--- /dev/null
+++ b/nixos/modules/hardware/onlykey/default.nix
@@ -0,0 +1,33 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+
+  ####### interface
+
+  options = {
+
+    hardware.onlykey = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable OnlyKey device (https://crp.to/p/) support.
+        '';
+      };
+    };
+
+  };
+
+  ## As per OnlyKey's documentation piece (hhttps://docs.google.com/document/d/1Go_Rs218fKUx-j_JKhddbSVTqY6P0vQO831t2MKCJC8),
+  ## it is important to add udev rule for OnlyKey for it to work on Linux
+
+  ####### implementation
+
+  config = mkIf config.hardware.onlykey.enable {
+    services.udev.extraRules = builtins.readFile ./onlykey.udev;
+  };
+
+
+}
diff --git a/nixos/modules/hardware/onlykey/onlykey.udev b/nixos/modules/hardware/onlykey/onlykey.udev
new file mode 100644
index 00000000000..9c8873aafc9
--- /dev/null
+++ b/nixos/modules/hardware/onlykey/onlykey.udev
@@ -0,0 +1,18 @@
+# UDEV Rules for OnlyKey, https://docs.crp.to/linux.html
+ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="60fc", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="60fc", ENV{MTP_NO_PROBE}="1"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="60fc", MODE:="0666"
+KERNEL=="ttyACM*", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="60fc", MODE:="0666"
+
+
+# The udev rules were updated upstream without an explanation as you can
+# see in [this comment][commit]. Assuming that hey have changed the
+# idVendor/idProduct, I've kept the old values.
+# TODO: Contact them upstream.
+#
+# [commit]: https://github.com/trustcrypto/trustcrypto.github.io/commit/0bcf928adaea559e75efa02ebd1040f0a15f611d
+#
+ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789B]?", ENV{ID_MM_DEVICE_IGNORE}="1"
+ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789A]?", ENV{MTP_NO_PROBE}="1"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789ABCD]?", GROUP="plugdev"
+KERNEL=="ttyACM*", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789B]?", GROUP="plugdev"
diff --git a/nixos/modules/hardware/opengl.nix b/nixos/modules/hardware/opengl.nix
new file mode 100644
index 00000000000..0d8aaf73459
--- /dev/null
+++ b/nixos/modules/hardware/opengl.nix
@@ -0,0 +1,157 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.hardware.opengl;
+
+  kernelPackages = config.boot.kernelPackages;
+
+  videoDrivers = config.services.xserver.videoDrivers;
+
+  package = pkgs.buildEnv {
+    name = "opengl-drivers";
+    paths = [ cfg.package ] ++ cfg.extraPackages;
+  };
+
+  package32 = pkgs.buildEnv {
+    name = "opengl-drivers-32bit";
+    paths = [ cfg.package32 ] ++ cfg.extraPackages32;
+  };
+
+in
+
+{
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "xserver" "vaapiDrivers" ] [ "hardware" "opengl" "extraPackages" ])
+    (mkRemovedOptionModule [ "hardware" "opengl" "s3tcSupport" ] ''
+      S3TC support is now always enabled in Mesa.
+    '')
+  ];
+
+  options = {
+
+    hardware.opengl = {
+      enable = mkOption {
+        description = ''
+          Whether to enable OpenGL drivers. This is needed to enable
+          OpenGL support in X11 systems, as well as for Wayland compositors
+          like sway and Weston. It is enabled by default
+          by the corresponding modules, so you do not usually have to
+          set it yourself, only if there is no module for your wayland
+          compositor of choice. See services.xserver.enable and
+          programs.sway.enable.
+        '';
+        type = types.bool;
+        default = false;
+      };
+
+      driSupport = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable accelerated OpenGL rendering through the
+          Direct Rendering Interface (DRI).
+        '';
+      };
+
+      driSupport32Bit = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          On 64-bit systems, whether to support Direct Rendering for
+          32-bit applications (such as Wine).  This is currently only
+          supported for the <literal>nvidia</literal> as well as
+          <literal>Mesa</literal>.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        internal = true;
+        description = ''
+          The package that provides the OpenGL implementation.
+        '';
+      };
+
+      package32 = mkOption {
+        type = types.package;
+        internal = true;
+        description = ''
+          The package that provides the 32-bit OpenGL implementation on
+          64-bit systems. Used when <option>driSupport32Bit</option> is
+          set.
+        '';
+      };
+
+      extraPackages = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "with pkgs; [ vaapiIntel libvdpau-va-gl vaapiVdpau intel-ocl ]";
+        description = ''
+          Additional packages to add to OpenGL drivers. This can be used
+          to add OpenCL drivers, VA-API/VDPAU drivers etc.
+        '';
+      };
+
+      extraPackages32 = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "with pkgs.pkgsi686Linux; [ vaapiIntel libvdpau-va-gl vaapiVdpau ]";
+        description = ''
+          Additional packages to add to 32-bit OpenGL drivers on
+          64-bit systems. Used when <option>driSupport32Bit</option> is
+          set. This can be used to add OpenCL drivers, VA-API/VDPAU drivers etc.
+        '';
+      };
+
+      setLdLibraryPath = mkOption {
+        type = types.bool;
+        internal = true;
+        default = false;
+        description = ''
+          Whether the <literal>LD_LIBRARY_PATH</literal> environment variable
+          should be set to the locations of driver libraries. Drivers which
+          rely on overriding libraries should set this to true. Drivers which
+          support <literal>libglvnd</literal> and other dispatch libraries
+          instead of overriding libraries should not set this.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = cfg.driSupport32Bit -> pkgs.stdenv.isx86_64;
+        message = "Option driSupport32Bit only makes sense on a 64-bit system.";
+      }
+      { assertion = cfg.driSupport32Bit -> (config.boot.kernelPackages.kernel.features.ia32Emulation or false);
+        message = "Option driSupport32Bit requires a kernel that supports 32bit emulation";
+      }
+    ];
+
+    systemd.tmpfiles.rules = [
+      "L+ /run/opengl-driver - - - - ${package}"
+      (
+        if pkgs.stdenv.isi686 then
+          "L+ /run/opengl-driver-32 - - - - opengl-driver"
+        else if cfg.driSupport32Bit then
+          "L+ /run/opengl-driver-32 - - - - ${package32}"
+        else
+          "r /run/opengl-driver-32"
+      )
+    ];
+
+    environment.sessionVariables.LD_LIBRARY_PATH = mkIf cfg.setLdLibraryPath
+      ([ "/run/opengl-driver/lib" ] ++ optional cfg.driSupport32Bit "/run/opengl-driver-32/lib");
+
+    hardware.opengl.package = mkDefault pkgs.mesa.drivers;
+    hardware.opengl.package32 = mkDefault pkgs.pkgsi686Linux.mesa.drivers;
+
+    boot.extraModulePackages = optional (elem "virtualbox" videoDrivers) kernelPackages.virtualboxGuestAdditions;
+  };
+}
diff --git a/nixos/modules/hardware/openrazer.nix b/nixos/modules/hardware/openrazer.nix
new file mode 100644
index 00000000000..bd9fc485e17
--- /dev/null
+++ b/nixos/modules/hardware/openrazer.nix
@@ -0,0 +1,146 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.openrazer;
+  kernelPackages = config.boot.kernelPackages;
+
+  toPyBoolStr = b: if b then "True" else "False";
+
+  daemonExe = "${pkgs.openrazer-daemon}/bin/openrazer-daemon --config ${daemonConfFile}";
+
+  daemonConfFile = pkgs.writeTextFile {
+    name = "razer.conf";
+    text = ''
+      [General]
+      verbose_logging = ${toPyBoolStr cfg.verboseLogging}
+
+      [Startup]
+      sync_effects_enabled = ${toPyBoolStr cfg.syncEffectsEnabled}
+      devices_off_on_screensaver = ${toPyBoolStr cfg.devicesOffOnScreensaver}
+      mouse_battery_notifier = ${toPyBoolStr cfg.mouseBatteryNotifier}
+
+      [Statistics]
+      key_statistics = ${toPyBoolStr cfg.keyStatistics}
+    '';
+  };
+
+  dbusServiceFile = pkgs.writeTextFile rec {
+    name = "org.razer.service";
+    destination = "/share/dbus-1/services/${name}";
+    text = ''
+      [D-BUS Service]
+      Name=org.razer
+      Exec=${daemonExe}
+      SystemdService=openrazer-daemon.service
+    '';
+  };
+
+  drivers = [
+    "razerkbd"
+    "razermouse"
+    "razerfirefly"
+    "razerkraken"
+    "razermug"
+    "razercore"
+  ];
+in
+{
+  options = {
+    hardware.openrazer = {
+      enable = mkEnableOption ''
+        OpenRazer drivers and userspace daemon.
+      '';
+
+      verboseLogging = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable verbose logging. Logs debug messages.
+        '';
+      };
+
+      syncEffectsEnabled = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Set the sync effects flag to true so any assignment of
+          effects will work across devices.
+        '';
+      };
+
+      devicesOffOnScreensaver = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Turn off the devices when the systems screensaver kicks in.
+        '';
+      };
+
+      mouseBatteryNotifier = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Mouse battery notifier.
+        '';
+      };
+
+      keyStatistics = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Collects number of keypresses per hour per key used to
+          generate a heatmap.
+        '';
+      };
+
+      users = mkOption {
+        type = with types; listOf str;
+        default = [];
+        description = ''
+          Usernames to be added to the "openrazer" group, so that they
+          can start and interact with the OpenRazer userspace daemon.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    boot.extraModulePackages = [ kernelPackages.openrazer ];
+    boot.kernelModules = drivers;
+
+    # Makes the man pages available so you can succesfully run
+    # > systemctl --user help openrazer-daemon
+    environment.systemPackages = [ pkgs.python3Packages.openrazer-daemon.man ];
+
+    services.udev.packages = [ kernelPackages.openrazer ];
+    services.dbus.packages = [ dbusServiceFile ];
+
+    # A user must be a member of the openrazer group in order to start
+    # the openrazer-daemon. Therefore we make sure that the group
+    # exists.
+    users.groups.openrazer = {
+      members = cfg.users;
+    };
+
+    systemd.user.services.openrazer-daemon = {
+      description = "Daemon to manage razer devices in userspace";
+      unitConfig.Documentation = "man:openrazer-daemon(8)";
+        # Requires a graphical session so the daemon knows when the screensaver
+        # starts. See the 'devicesOffOnScreensaver' option.
+        wantedBy = [ "graphical-session.target" ];
+        partOf = [ "graphical-session.target" ];
+        serviceConfig = {
+          Type = "dbus";
+          BusName = "org.razer";
+          ExecStart = "${daemonExe} --foreground";
+          Restart = "always";
+      };
+    };
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ roelvandijk ];
+  };
+}
diff --git a/nixos/modules/hardware/opentabletdriver.nix b/nixos/modules/hardware/opentabletdriver.nix
new file mode 100644
index 00000000000..caba934ebe7
--- /dev/null
+++ b/nixos/modules/hardware/opentabletdriver.nix
@@ -0,0 +1,69 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.hardware.opentabletdriver;
+in
+{
+  meta.maintainers = with lib.maintainers; [ thiagokokada ];
+
+  options = {
+    hardware.opentabletdriver = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable OpenTabletDriver udev rules, user service and blacklist kernel
+          modules known to conflict with OpenTabletDriver.
+        '';
+      };
+
+      blacklistedKernelModules = mkOption {
+        type = types.listOf types.str;
+        default = [ "hid-uclogic" "wacom" ];
+        description = ''
+          Blacklist of kernel modules known to conflict with OpenTabletDriver.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.opentabletdriver;
+        defaultText = literalExpression "pkgs.opentabletdriver";
+        description = ''
+          OpenTabletDriver derivation to use.
+        '';
+      };
+
+      daemon = {
+        enable = mkOption {
+          default = true;
+          type = types.bool;
+          description = ''
+            Whether to start OpenTabletDriver daemon as a systemd user service.
+          '';
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    services.udev.packages = [ cfg.package ];
+
+    boot.blacklistedKernelModules = cfg.blacklistedKernelModules;
+
+    systemd.user.services.opentabletdriver = with pkgs; mkIf cfg.daemon.enable {
+      description = "Open source, cross-platform, user-mode tablet driver";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${cfg.package}/bin/otd-daemon -c ${cfg.package}/lib/OpenTabletDriver/Configurations";
+        Restart = "on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/hardware/pcmcia.nix b/nixos/modules/hardware/pcmcia.nix
new file mode 100644
index 00000000000..aef35a28e54
--- /dev/null
+++ b/nixos/modules/hardware/pcmcia.nix
@@ -0,0 +1,60 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  pcmciaUtils = pkgs.pcmciaUtils.passthru.function {
+    inherit (config.hardware.pcmcia) firmware config;
+  };
+
+in
+
+
+{
+  ###### interface
+
+  options = {
+
+    hardware.pcmcia = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable this option to support PCMCIA card.
+        '';
+      };
+
+      firmware = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          List of firmware used to handle specific PCMCIA card.
+        '';
+      };
+
+      config = mkOption {
+        default = null;
+        type = types.nullOr types.path;
+        description = ''
+          Path to the configuration file which maps the memory, IRQs
+          and ports used by the PCMCIA hardware.
+        '';
+      };
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf config.hardware.pcmcia.enable {
+
+    boot.kernelModules = [ "pcmcia" ];
+
+    services.udev.packages = [ pcmciaUtils ];
+
+    environment.systemPackages = [ pcmciaUtils ];
+
+  };
+
+}
diff --git a/nixos/modules/hardware/printers.nix b/nixos/modules/hardware/printers.nix
new file mode 100644
index 00000000000..ef07542950b
--- /dev/null
+++ b/nixos/modules/hardware/printers.nix
@@ -0,0 +1,130 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.hardware.printers;
+  ppdOptionsString = options: optionalString (options != {})
+    (concatStringsSep " "
+      (mapAttrsToList (name: value: "-o '${name}'='${value}'") options)
+    );
+  ensurePrinter = p: ''
+    ${pkgs.cups}/bin/lpadmin -p '${p.name}' -E \
+      ${optionalString (p.location != null) "-L '${p.location}'"} \
+      ${optionalString (p.description != null) "-D '${p.description}'"} \
+      -v '${p.deviceUri}' \
+      -m '${p.model}' \
+      ${ppdOptionsString p.ppdOptions}
+  '';
+  ensureDefaultPrinter = name: ''
+    ${pkgs.cups}/bin/lpadmin -d '${name}'
+  '';
+
+  # "graph but not # or /" can't be implemented as regex alone due to missing lookahead support
+  noInvalidChars = str: all (c: c != "#" && c != "/") (stringToCharacters str);
+  printerName = (types.addCheck (types.strMatching "[[:graph:]]+") noInvalidChars)
+    // { description = "printable string without spaces, # and /"; };
+
+
+in {
+  options = {
+    hardware.printers = {
+      ensureDefaultPrinter = mkOption {
+        type = types.nullOr printerName;
+        default = null;
+        description = ''
+          Ensures the named printer is the default CUPS printer / printer queue.
+        '';
+      };
+      ensurePrinters = mkOption {
+        description = ''
+          Will regularly ensure that the given CUPS printers are configured as declared here.
+          If a printer's options are manually changed afterwards, they will be overwritten eventually.
+          This option will never delete any printer, even if removed from this list.
+          You can check existing printers with <command>lpstat -s</command>
+          and remove printers with <command>lpadmin -x &lt;printer-name&gt;</command>.
+          Printers not listed here can still be manually configured.
+        '';
+        default = [];
+        type = types.listOf (types.submodule {
+          options = {
+            name = mkOption {
+              type = printerName;
+              example = "BrotherHL_Workroom";
+              description = ''
+                Name of the printer / printer queue.
+                May contain any printable characters except "/", "#", and space.
+              '';
+            };
+            location = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "Workroom";
+              description = ''
+                Optional human-readable location.
+              '';
+            };
+            description = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "Brother HL-5140";
+              description = ''
+                Optional human-readable description.
+              '';
+            };
+            deviceUri = mkOption {
+              type = types.str;
+              example = literalExpression ''
+                "ipp://printserver.local/printers/BrotherHL_Workroom"
+                "usb://HP/DESKJET%20940C?serial=CN16E6C364BH"
+              '';
+              description = ''
+                How to reach the printer.
+                <command>lpinfo -v</command> shows a list of supported device URIs and schemes.
+              '';
+            };
+            model = mkOption {
+              type = types.str;
+              example = literalExpression ''
+                "gutenprint.''${lib.versions.majorMinor (lib.getVersion pkgs.gutenprint)}://brother-hl-5140/expert"
+              '';
+              description = ''
+                Location of the ppd driver file for the printer.
+                <command>lpinfo -m</command> shows a list of supported models.
+              '';
+            };
+            ppdOptions = mkOption {
+              type = types.attrsOf types.str;
+              example = {
+                PageSize = "A4";
+                Duplex = "DuplexNoTumble";
+              };
+              default = {};
+              description = ''
+                Sets PPD options for the printer.
+                <command>lpoptions [-p printername] -l</command> shows suported PPD options for the given printer.
+              '';
+            };
+          };
+        });
+      };
+    };
+  };
+
+  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 {
+      description = "Ensure NixOS-configured CUPS printers";
+      wantedBy = [ "multi-user.target" ];
+      requires = [ cupsUnit ];
+      after = [ cupsUnit ];
+
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+      };
+
+      script = concatMapStringsSep "\n" ensurePrinter cfg.ensurePrinters
+        + optionalString (cfg.ensureDefaultPrinter != null) (ensureDefaultPrinter cfg.ensureDefaultPrinter);
+    };
+  };
+}
diff --git a/nixos/modules/hardware/raid/hpsa.nix b/nixos/modules/hardware/raid/hpsa.nix
new file mode 100644
index 00000000000..c4977e3fd70
--- /dev/null
+++ b/nixos/modules/hardware/raid/hpsa.nix
@@ -0,0 +1,61 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  hpssacli = pkgs.stdenv.mkDerivation rec {
+    pname = "hpssacli";
+    version = "2.40-13.0";
+
+    src = pkgs.fetchurl {
+      url = "https://downloads.linux.hpe.com/SDR/downloads/MCP/Ubuntu/pool/non-free/${pname}-${version}_amd64.deb";
+      sha256 = "11w7fwk93lmfw0yya4jpjwdmgjimqxx6412sqa166g1pz4jil4sw";
+    };
+
+    nativeBuildInputs = [ pkgs.dpkg ];
+
+    unpackPhase = "dpkg -x $src ./";
+
+    installPhase = ''
+      mkdir -p $out/bin $out/share/doc $out/share/man
+      mv opt/hp/hpssacli/bld/{hpssascripting,hprmstr,hpssacli} $out/bin/
+      mv opt/hp/hpssacli/bld/*.{license,txt}                   $out/share/doc/
+      mv usr/man                                               $out/share/
+
+      for file in $out/bin/*; do
+        chmod +w $file
+        patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
+                 --set-rpath ${lib.makeLibraryPath [ pkgs.stdenv.cc.cc ]} \
+                 $file
+      done
+    '';
+
+    dontStrip = true;
+
+    meta = with lib; {
+      description = "HP Smart Array CLI";
+      homepage = "https://downloads.linux.hpe.com/SDR/downloads/MCP/Ubuntu/pool/non-free/";
+      license = licenses.unfreeRedistributable;
+      platforms = [ "x86_64-linux" ];
+      maintainers = with maintainers; [ volth ];
+    };
+  };
+in {
+  ###### interface
+
+  options = {
+    hardware.raid.HPSmartArray = {
+      enable = mkEnableOption "HP Smart Array kernel modules and CLI utility";
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf config.hardware.raid.HPSmartArray.enable {
+
+    boot.initrd.kernelModules = [ "sg" ]; /* hpssacli wants it */
+    boot.initrd.availableKernelModules = [ "hpsa" ];
+
+    environment.systemPackages = [ hpssacli ];
+  };
+}
diff --git a/nixos/modules/hardware/rtl-sdr.nix b/nixos/modules/hardware/rtl-sdr.nix
new file mode 100644
index 00000000000..e85fc04e29b
--- /dev/null
+++ b/nixos/modules/hardware/rtl-sdr.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.hardware.rtl-sdr;
+
+in {
+  options.hardware.rtl-sdr = {
+    enable = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = ''
+        Enables rtl-sdr udev rules, ensures 'plugdev' group exists, and blacklists DVB kernel modules.
+        This is a prerequisite to using devices supported by rtl-sdr without being root, since rtl-sdr USB descriptors will be owned by plugdev through udev.
+       '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    boot.blacklistedKernelModules = [ "dvb_usb_rtl28xxu" "e4000" "rtl2832" ];
+    services.udev.packages = [ pkgs.rtl-sdr ];
+    users.groups.plugdev = {};
+  };
+}
diff --git a/nixos/modules/hardware/sata.nix b/nixos/modules/hardware/sata.nix
new file mode 100644
index 00000000000..81592997d6e
--- /dev/null
+++ b/nixos/modules/hardware/sata.nix
@@ -0,0 +1,100 @@
+{ config, lib, pkgs, ... }:
+let
+  inherit (lib) mkEnableOption mkIf mkOption types;
+
+  cfg = config.hardware.sata.timeout;
+
+  buildRule = d:
+    lib.concatStringsSep ", " [
+      ''ACTION=="add"''
+      ''SUBSYSTEM=="block"''
+      ''ENV{ID_${lib.toUpper d.idBy}}=="${d.name}"''
+      ''TAG+="systemd"''
+      ''ENV{SYSTEMD_WANTS}="${unitName d}"''
+    ];
+
+  devicePath = device:
+    "/dev/disk/by-${device.idBy}/${device.name}";
+
+  unitName = device:
+    "sata-timeout-${lib.strings.sanitizeDerivationName device.name}";
+
+  startScript =
+    pkgs.writeShellScript "sata-timeout.sh" ''
+      set -eEuo pipefail
+
+      device="$1"
+
+      ${pkgs.smartmontools}/bin/smartctl \
+        -l scterc,${toString cfg.deciSeconds},${toString cfg.deciSeconds} \
+        --quietmode errorsonly \
+        "$device"
+    '';
+
+in
+{
+  meta.maintainers = with lib.maintainers; [ peterhoeg ];
+
+  options.hardware.sata.timeout = {
+    enable = mkEnableOption "SATA drive timeouts";
+
+    deciSeconds = mkOption {
+      example = 70;
+      type = types.int;
+      description = ''
+        Set SCT Error Recovery Control timeout in deciseconds for use in RAID configurations.
+
+        Values are as follows:
+           0 = disable SCT ERT
+          70 = default in consumer drives (7 seconds)
+
+        Maximum is disk dependant but probably 60 seconds.
+      '';
+    };
+
+    drives = mkOption {
+      description = "List of drives for which to configure the timeout.";
+      type = types.listOf
+        (types.submodule {
+          options = {
+            name = mkOption {
+              description = "Drive name without the full path.";
+              type = types.str;
+            };
+
+            idBy = mkOption {
+              description = "The method to identify the drive.";
+              type = types.enum [ "path" "wwn" ];
+              default = "path";
+            };
+          };
+        });
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.extraRules = lib.concatMapStringsSep "\n" buildRule cfg.drives;
+
+    systemd.services = lib.listToAttrs (map
+      (e:
+        lib.nameValuePair (unitName e) {
+          description = "SATA timeout for ${e.name}";
+          wantedBy = [ "sata-timeout.target" ];
+          serviceConfig = {
+            Type = "oneshot";
+            ExecStart = "${startScript} '${devicePath e}'";
+            PrivateTmp = true;
+            PrivateNetwork = true;
+            ProtectHome = "tmpfs";
+            ProtectSystem = "strict";
+          };
+        }
+      )
+      cfg.drives);
+
+    systemd.targets.sata-timeout = {
+      description = "SATA timeout";
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/hardware/sensor/hddtemp.nix b/nixos/modules/hardware/sensor/hddtemp.nix
new file mode 100644
index 00000000000..df3f75e229a
--- /dev/null
+++ b/nixos/modules/hardware/sensor/hddtemp.nix
@@ -0,0 +1,81 @@
+{ config, lib, pkgs, ... }:
+let
+  inherit (lib) mkIf mkOption types;
+
+  cfg = config.hardware.sensor.hddtemp;
+
+  wrapper = pkgs.writeShellScript "hddtemp-wrapper" ''
+    set -eEuo pipefail
+
+    file=/var/lib/hddtemp/hddtemp.db
+
+    drives=(${toString (map (e: ''$(realpath ${lib.escapeShellArg e}) '') cfg.drives)})
+
+    cp ${pkgs.hddtemp}/share/hddtemp/hddtemp.db $file
+    ${lib.concatMapStringsSep "\n" (e: "echo ${lib.escapeShellArg e} >> $file") cfg.dbEntries}
+
+    exec ${pkgs.hddtemp}/bin/hddtemp ${lib.escapeShellArgs cfg.extraArgs} \
+      --daemon \
+      --unit=${cfg.unit} \
+      --file=$file \
+      ''${drives[@]}
+  '';
+
+in
+{
+  meta.maintainers = with lib.maintainers; [ peterhoeg ];
+
+  ###### interface
+
+  options = {
+    hardware.sensor.hddtemp = {
+      enable = mkOption {
+        description = ''
+          Enable this option to support HDD/SSD temperature sensors.
+        '';
+        type = types.bool;
+        default = false;
+      };
+
+      drives = mkOption {
+        description = "List of drives to monitor. If you pass /dev/disk/by-path/* entries the symlinks will be resolved as hddtemp doesn't like names with colons.";
+        type = types.listOf types.str;
+      };
+
+      unit = mkOption {
+        description = "Celcius or Fahrenheit";
+        type = types.enum [ "C" "F" ];
+        default = "C";
+      };
+
+      dbEntries = mkOption {
+        description = "Additional DB entries";
+        type = types.listOf types.str;
+        default = [ ];
+      };
+
+      extraArgs = mkOption {
+        description = "Additional arguments passed to the daemon.";
+        type = types.listOf types.str;
+        default = [ ];
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.hddtemp = {
+      description = "HDD/SSD temperature";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = wrapper;
+        StateDirectory = "hddtemp";
+        PrivateTmp = true;
+        ProtectHome = "tmpfs";
+        ProtectSystem = "strict";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/hardware/sensor/iio.nix b/nixos/modules/hardware/sensor/iio.nix
new file mode 100644
index 00000000000..8b3ba87a7d9
--- /dev/null
+++ b/nixos/modules/hardware/sensor/iio.nix
@@ -0,0 +1,35 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  ###### interface
+
+  options = {
+    hardware.sensor.iio = {
+      enable = mkOption {
+        description = ''
+          Enable this option to support IIO sensors with iio-sensor-proxy.
+
+          IIO sensors are used for orientation and ambient light
+          sensors on some mobile devices.
+        '';
+        type = types.bool;
+        default = false;
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf config.hardware.sensor.iio.enable {
+
+    boot.initrd.availableKernelModules = [ "hid-sensor-hub" ];
+
+    environment.systemPackages = with pkgs; [ iio-sensor-proxy ];
+
+    services.dbus.packages = with pkgs; [ iio-sensor-proxy ];
+    services.udev.packages = with pkgs; [ iio-sensor-proxy ];
+    systemd.packages = with pkgs; [ iio-sensor-proxy ];
+  };
+}
diff --git a/nixos/modules/hardware/steam-hardware.nix b/nixos/modules/hardware/steam-hardware.nix
new file mode 100644
index 00000000000..6218c9ffbb9
--- /dev/null
+++ b/nixos/modules/hardware/steam-hardware.nix
@@ -0,0 +1,32 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.hardware.steam-hardware;
+
+in
+
+{
+  options.hardware.steam-hardware = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable udev rules for Steam hardware such as the Steam Controller, other supported controllers and the HTC Vive";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [
+      pkgs.steamPackages.steam
+    ];
+
+    # The uinput module needs to be loaded in order to trigger the udev rules
+    # defined in the steam package for setting permissions on /dev/uinput.
+    #
+    # If the udev rules are not triggered, some controllers won't work with
+    # steam.
+    boot.kernelModules = [ "uinput" ];
+  };
+}
diff --git a/nixos/modules/hardware/system-76.nix b/nixos/modules/hardware/system-76.nix
new file mode 100644
index 00000000000..ca40ee0ebb3
--- /dev/null
+++ b/nixos/modules/hardware/system-76.nix
@@ -0,0 +1,89 @@
+{ config, lib, options, pkgs, ... }:
+
+let
+  inherit (lib) literalExpression mkOption mkEnableOption types mkIf mkMerge optional versionOlder;
+  cfg = config.hardware.system76;
+  opt = options.hardware.system76;
+
+  kpkgs = config.boot.kernelPackages;
+  modules = [ "system76" "system76-io" ] ++ (optional (versionOlder kpkgs.kernel.version "5.5") "system76-acpi");
+  modulePackages = map (m: kpkgs.${m}) modules;
+  moduleConfig = mkIf cfg.kernel-modules.enable {
+    boot.extraModulePackages = modulePackages;
+
+    boot.kernelModules = modules;
+
+    services.udev.packages = modulePackages;
+  };
+
+  firmware-pkg = pkgs.system76-firmware;
+  firmwareConfig = mkIf cfg.firmware-daemon.enable {
+    # Make system76-firmware-cli usable by root from the command line.
+    environment.systemPackages = [ firmware-pkg ];
+
+    services.dbus.packages = [ firmware-pkg ];
+
+    systemd.services.system76-firmware-daemon = {
+      description = "The System76 Firmware Daemon";
+
+      serviceConfig = {
+        ExecStart = "${firmware-pkg}/bin/system76-firmware-daemon";
+
+        Restart = "on-failure";
+      };
+
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+
+  power-pkg = config.boot.kernelPackages.system76-power;
+  powerConfig = mkIf cfg.power-daemon.enable {
+    # Make system76-power usable by root from the command line.
+    environment.systemPackages = [ power-pkg ];
+
+    services.dbus.packages = [ power-pkg ];
+
+    systemd.services.system76-power = {
+      description = "System76 Power Daemon";
+      serviceConfig = {
+        ExecStart = "${power-pkg}/bin/system76-power daemon";
+        Restart = "on-failure";
+        Type = "dbus";
+        BusName = "com.system76.PowerDaemon";
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+in {
+  options = {
+    hardware.system76 = {
+      enableAll = mkEnableOption "all recommended configuration for system76 systems";
+
+      firmware-daemon.enable = mkOption {
+        default = cfg.enableAll;
+        defaultText = literalExpression "config.${opt.enableAll}";
+        example = true;
+        description = "Whether to enable the system76 firmware daemon";
+        type = types.bool;
+      };
+
+      kernel-modules.enable = mkOption {
+        default = cfg.enableAll;
+        defaultText = literalExpression "config.${opt.enableAll}";
+        example = true;
+        description = "Whether to make the system76 out-of-tree kernel modules available";
+        type = types.bool;
+      };
+
+      power-daemon.enable = mkOption {
+        default = cfg.enableAll;
+        defaultText = literalExpression "config.${opt.enableAll}";
+        example = true;
+        description = "Whether to enable the system76 power daemon";
+        type = types.bool;
+      };
+    };
+  };
+
+  config = mkMerge [ moduleConfig firmwareConfig powerConfig ];
+}
diff --git a/nixos/modules/hardware/tuxedo-keyboard.nix b/nixos/modules/hardware/tuxedo-keyboard.nix
new file mode 100644
index 00000000000..97af7c61f3c
--- /dev/null
+++ b/nixos/modules/hardware/tuxedo-keyboard.nix
@@ -0,0 +1,35 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.tuxedo-keyboard;
+  tuxedo-keyboard = config.boot.kernelPackages.tuxedo-keyboard;
+in
+  {
+    options.hardware.tuxedo-keyboard = {
+      enable = mkEnableOption ''
+          Enables the tuxedo-keyboard driver.
+
+          To configure the driver, pass the options to the <option>boot.kernelParams</option> configuration.
+          There are several parameters you can change. It's best to check at the source code description which options are supported.
+          You can find all the supported parameters at: <link xlink:href="https://github.com/tuxedocomputers/tuxedo-keyboard#kernelparam" />
+
+          In order to use the <literal>custom</literal> lighting with the maximumg brightness and a color of <literal>0xff0a0a</literal> one would put pass <option>boot.kernelParams</option> like this:
+
+          <programlisting>
+          boot.kernelParams = [
+           "tuxedo_keyboard.mode=0"
+           "tuxedo_keyboard.brightness=255"
+           "tuxedo_keyboard.color_left=0xff0a0a"
+          ];
+          </programlisting>
+      '';
+    };
+
+    config = mkIf cfg.enable
+    {
+      boot.kernelModules = ["tuxedo_keyboard"];
+      boot.extraModulePackages = [ tuxedo-keyboard ];
+    };
+  }
diff --git a/nixos/modules/hardware/ubertooth.nix b/nixos/modules/hardware/ubertooth.nix
new file mode 100644
index 00000000000..637fddfb37d
--- /dev/null
+++ b/nixos/modules/hardware/ubertooth.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.ubertooth;
+
+  ubertoothPkg = pkgs.ubertooth.override {
+    udevGroup = cfg.group;
+  };
+in {
+  options.hardware.ubertooth = {
+    enable = mkEnableOption "Enable the Ubertooth software and its udev rules.";
+
+    group = mkOption {
+      type = types.str;
+      default = "ubertooth";
+      example = "wheel";
+      description = "Group for Ubertooth's udev rules.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ ubertoothPkg ];
+
+    services.udev.packages = [ ubertoothPkg ];
+    users.groups.${cfg.group} = {};
+  };
+}
diff --git a/nixos/modules/hardware/uinput.nix b/nixos/modules/hardware/uinput.nix
new file mode 100644
index 00000000000..55e86bfa6bd
--- /dev/null
+++ b/nixos/modules/hardware/uinput.nix
@@ -0,0 +1,19 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.hardware.uinput;
+in {
+  options.hardware.uinput = {
+    enable = lib.mkEnableOption "uinput support";
+  };
+
+  config = lib.mkIf cfg.enable {
+    boot.kernelModules = [ "uinput" ];
+
+    users.groups.uinput = {};
+
+    services.udev.extraRules = ''
+      SUBSYSTEM=="misc", KERNEL=="uinput", MODE="0660", GROUP="uinput", OPTIONS+="static_node=uinput"
+    '';
+  };
+}
diff --git a/nixos/modules/hardware/usb-wwan.nix b/nixos/modules/hardware/usb-wwan.nix
new file mode 100644
index 00000000000..679a6c6497c
--- /dev/null
+++ b/nixos/modules/hardware/usb-wwan.nix
@@ -0,0 +1,39 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  ###### interface
+
+  options = {
+
+    hardware.usbWwan = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable this option to support USB WWAN adapters.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf config.hardware.usbWwan.enable {
+    # Attaches device specific handlers.
+    services.udev.packages = with pkgs; [ usb-modeswitch-data ];
+
+    # Triggered by udev, usb-modeswitch creates systemd services via a
+    # template unit in the usb-modeswitch package.
+    systemd.packages = with pkgs; [ usb-modeswitch ];
+
+    # The systemd service requires the usb-modeswitch-data. The
+    # usb-modeswitch package intends to discover this via the
+    # filesystem at /usr/share/usb_modeswitch, and merge it with user
+    # configuration in /etc/usb_modeswitch.d. Configuring the correct
+    # path in the package is difficult, as it would cause a cyclic
+    # dependency.
+    environment.etc."usb_modeswitch.d".source = "${pkgs.usb-modeswitch-data}/share/usb_modeswitch";
+  };
+}
diff --git a/nixos/modules/hardware/video/amdgpu-pro.nix b/nixos/modules/hardware/video/amdgpu-pro.nix
new file mode 100644
index 00000000000..d784befc9b8
--- /dev/null
+++ b/nixos/modules/hardware/video/amdgpu-pro.nix
@@ -0,0 +1,70 @@
+# This module provides the proprietary AMDGPU-PRO drivers.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  drivers = config.services.xserver.videoDrivers;
+
+  enabled = elem "amdgpu-pro" drivers;
+
+  package = config.boot.kernelPackages.amdgpu-pro;
+  package32 = pkgs.pkgsi686Linux.linuxPackages.amdgpu-pro.override { kernel = null; };
+
+  opengl = config.hardware.opengl;
+
+in
+
+{
+
+  config = mkIf enabled {
+
+    nixpkgs.config.xorg.abiCompat = "1.20";
+
+    services.xserver.drivers = singleton
+      { name = "amdgpu"; modules = [ package ]; display = true; };
+
+    hardware.opengl.package = package;
+    hardware.opengl.package32 = package32;
+    hardware.opengl.setLdLibraryPath = true;
+
+    boot.extraModulePackages = [ package.kmod ];
+
+    boot.kernelPackages = pkgs.linuxKernel.packagesFor
+      (pkgs.linuxKernel.kernels.linux_5_10.override {
+        structuredExtraConfig = {
+          DEVICE_PRIVATE = kernel.yes;
+          KALLSYMS_ALL = kernel.yes;
+        };
+      });
+
+    hardware.firmware = [ package.fw ];
+
+    system.activationScripts.setup-amdgpu-pro = ''
+      ln -sfn ${package}/opt/amdgpu{,-pro} /run
+    '';
+
+    system.requiredKernelConfig = with config.lib.kernelConfig; [
+      (isYes "DEVICE_PRIVATE")
+      (isYes "KALLSYMS_ALL")
+    ];
+
+    boot.initrd.extraUdevRulesCommands = ''
+      cp -v ${package}/etc/udev/rules.d/*.rules $out/
+    '';
+
+    environment.systemPackages =
+      [ package.vulkan ] ++
+      # this isn't really DRI, but we'll reuse this option for now
+      optional config.hardware.opengl.driSupport32Bit package32.vulkan;
+
+    environment.etc = {
+      "modprobe.d/blacklist-radeon.conf".source = package + "/etc/modprobe.d/blacklist-radeon.conf";
+      amd.source = package + "/etc/amd";
+    };
+
+  };
+
+}
diff --git a/nixos/modules/hardware/video/bumblebee.nix b/nixos/modules/hardware/video/bumblebee.nix
new file mode 100644
index 00000000000..b6af4f80445
--- /dev/null
+++ b/nixos/modules/hardware/video/bumblebee.nix
@@ -0,0 +1,93 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.hardware.bumblebee;
+
+  kernel = config.boot.kernelPackages;
+
+  useNvidia = cfg.driver == "nvidia";
+
+  bumblebee = pkgs.bumblebee.override {
+    inherit useNvidia;
+    useDisplayDevice = cfg.connectDisplay;
+  };
+
+  useBbswitch = cfg.pmMethod == "bbswitch" || cfg.pmMethod == "auto" && useNvidia;
+
+  primus = pkgs.primus.override {
+    inherit useNvidia;
+  };
+
+in
+
+{
+
+  options = {
+    hardware.bumblebee = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable the bumblebee daemon to manage Optimus hybrid video cards.
+          This should power off secondary GPU until its use is requested
+          by running an application with optirun.
+        '';
+      };
+
+      group = mkOption {
+        default = "wheel";
+        example = "video";
+        type = types.str;
+        description = "Group for bumblebee socket";
+      };
+
+      connectDisplay = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Set to true if you intend to connect your discrete card to a
+          monitor. This option will set up your Nvidia card for EDID
+          discovery and to turn on the monitor signal.
+
+          Only nvidia driver is supported so far.
+        '';
+      };
+
+      driver = mkOption {
+        default = "nvidia";
+        type = types.enum [ "nvidia" "nouveau" ];
+        description = ''
+          Set driver used by bumblebeed. Supported are nouveau and nvidia.
+        '';
+      };
+
+      pmMethod = mkOption {
+        default = "auto";
+        type = types.enum [ "auto" "bbswitch" "switcheroo" "none" ];
+        description = ''
+          Set preferred power management method for unused card.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    boot.blacklistedKernelModules = [ "nvidia-drm" "nvidia" "nouveau" ];
+    boot.kernelModules = optional useBbswitch "bbswitch";
+    boot.extraModulePackages = optional useBbswitch kernel.bbswitch ++ optional useNvidia kernel.nvidia_x11.bin;
+
+    environment.systemPackages = [ bumblebee primus ];
+
+    systemd.services.bumblebeed = {
+      description = "Bumblebee Hybrid Graphics Switcher";
+      wantedBy = [ "multi-user.target" ];
+      before = [ "display-manager.service" ];
+      serviceConfig = {
+        ExecStart = "${bumblebee}/bin/bumblebeed --use-syslog -g ${cfg.group} --driver ${cfg.driver} --pm-method ${cfg.pmMethod}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/hardware/video/capture/mwprocapture.nix b/nixos/modules/hardware/video/capture/mwprocapture.nix
new file mode 100644
index 00000000000..76cb4c6ee9b
--- /dev/null
+++ b/nixos/modules/hardware/video/capture/mwprocapture.nix
@@ -0,0 +1,56 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.hardware.mwProCapture;
+
+  kernelPackages = config.boot.kernelPackages;
+
+in
+
+{
+
+  options.hardware.mwProCapture.enable = mkEnableOption "Magewell Pro Capture family kernel module";
+
+  config = mkIf cfg.enable {
+
+    boot.kernelModules = [ "ProCapture" ];
+
+    environment.systemPackages = [ kernelPackages.mwprocapture ];
+
+    boot.extraModulePackages = [ kernelPackages.mwprocapture ];
+
+    boot.extraModprobeConfig = ''
+      # Set the png picture to be displayed when no input signal is detected.
+      options ProCapture nosignal_file=${kernelPackages.mwprocapture}/res/NoSignal.png
+
+      # Set the png picture to be displayed when an unsupported input signal is detected.
+      options ProCapture unsupported_file=${kernelPackages.mwprocapture}/res/Unsupported.png
+
+      # Set the png picture to be displayed when an loking input signal is detected.
+      options ProCapture locking_file=${kernelPackages.mwprocapture}/res/Locking.png
+
+      # Message signaled interrupts switch
+      #options ProCapture disable_msi=0
+
+      # Set the debug level
+      #options ProCapture debug_level=0
+
+      # Force init switch eeprom
+      #options ProCapture init_switch_eeprom=0
+
+      # Min frame interval for VIDIOC_ENUM_FRAMEINTERVALS (default: 166666(100ns))
+      #options ProCapture enum_frameinterval_min=166666
+
+      # VIDIOC_ENUM_FRAMESIZES type (1: DISCRETE; 2: STEPWISE; otherwise: CONTINUOUS )
+      #options ProCapture enum_framesizes_type=0
+
+      # Parameters for internal usage
+      #options ProCapture internal_params=""
+    '';
+
+  };
+
+}
diff --git a/nixos/modules/hardware/video/displaylink.nix b/nixos/modules/hardware/video/displaylink.nix
new file mode 100644
index 00000000000..912f53da836
--- /dev/null
+++ b/nixos/modules/hardware/video/displaylink.nix
@@ -0,0 +1,76 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  enabled = elem "displaylink" config.services.xserver.videoDrivers;
+
+  evdi = config.boot.kernelPackages.evdi;
+
+  displaylink = pkgs.displaylink.override {
+    inherit evdi;
+  };
+
+in
+
+{
+
+  config = mkIf enabled {
+
+    boot.extraModulePackages = [ evdi ];
+    boot.kernelModules = [ "evdi" ];
+
+    environment.etc."X11/xorg.conf.d/40-displaylink.conf".text = ''
+      Section "OutputClass"
+        Identifier  "DisplayLink"
+        MatchDriver "evdi"
+        Driver      "modesetting"
+        Option      "AccelMethod" "none"
+      EndSection
+    '';
+
+    # make the device available
+    services.xserver.displayManager.sessionCommands = ''
+      ${lib.getBin pkgs.xorg.xrandr}/bin/xrandr --setprovideroutputsource 1 0
+    '';
+
+    # Those are taken from displaylink-installer.sh and from Arch Linux AUR package.
+
+    services.udev.packages = [ displaylink ];
+
+    powerManagement.powerDownCommands = ''
+      #flush any bytes in pipe
+      while read -n 1 -t 1 SUSPEND_RESULT < /tmp/PmMessagesPort_out; do : ; done;
+
+      #suspend DisplayLinkManager
+      echo "S" > /tmp/PmMessagesPort_in
+
+      #wait until suspend of DisplayLinkManager finish
+      if [ -f /tmp/PmMessagesPort_out ]; then
+        #wait until suspend of DisplayLinkManager finish
+        read -n 1 -t 10 SUSPEND_RESULT < /tmp/PmMessagesPort_out
+      fi
+    '';
+
+    powerManagement.resumeCommands = ''
+      #resume DisplayLinkManager
+      echo "R" > /tmp/PmMessagesPort_in
+    '';
+
+    systemd.services.dlm = {
+      description = "DisplayLink Manager Service";
+      after = [ "display-manager.service" ];
+      conflicts = [ "getty@tty7.service" ];
+
+      serviceConfig = {
+        ExecStart = "${displaylink}/bin/DisplayLinkManager";
+        Restart = "always";
+        RestartSec = 5;
+        LogsDirectory = "displaylink";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/hardware/video/hidpi.nix b/nixos/modules/hardware/video/hidpi.nix
new file mode 100644
index 00000000000..ac72b652504
--- /dev/null
+++ b/nixos/modules/hardware/video/hidpi.nix
@@ -0,0 +1,16 @@
+{ lib, pkgs, config, ...}:
+with lib;
+
+{
+  options.hardware.video.hidpi.enable = mkEnableOption "Font/DPI configuration optimized for HiDPI displays";
+
+  config = mkIf config.hardware.video.hidpi.enable {
+    console.font = lib.mkDefault "${pkgs.terminus_font}/share/consolefonts/ter-v32n.psf.gz";
+
+    # Needed when typing in passwords for full disk encryption
+    console.earlySetup = mkDefault true;
+    boot.loader.systemd-boot.consoleMode = mkDefault "1";
+
+    # TODO Find reasonable defaults X11 & wayland
+  };
+}
diff --git a/nixos/modules/hardware/video/nvidia.nix b/nixos/modules/hardware/video/nvidia.nix
new file mode 100644
index 00000000000..a81220a92a1
--- /dev/null
+++ b/nixos/modules/hardware/video/nvidia.nix
@@ -0,0 +1,391 @@
+# This module provides the proprietary NVIDIA X11 / OpenGL drivers.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  nvidia_x11 = let
+    drivers = config.services.xserver.videoDrivers;
+    isDeprecated = str: (hasPrefix "nvidia" str) && (str != "nvidia");
+    hasDeprecated = drivers: any isDeprecated drivers;
+  in if (hasDeprecated drivers) then
+    throw ''
+      Selecting an nvidia driver has been modified for NixOS 19.03. The version is now set using `hardware.nvidia.package`.
+    ''
+  else if (elem "nvidia" drivers) then cfg.package else null;
+
+  enabled = nvidia_x11 != null;
+  cfg = config.hardware.nvidia;
+
+  pCfg = cfg.prime;
+  syncCfg = pCfg.sync;
+  offloadCfg = pCfg.offload;
+  primeEnabled = syncCfg.enable || offloadCfg.enable;
+  nvidiaPersistencedEnabled =  cfg.nvidiaPersistenced;
+  nvidiaSettings = cfg.nvidiaSettings;
+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" "nvidiaBusId" ] [ "hardware" "nvidia" "prime" "nvidiaBusId" ])
+      (mkRenamedOptionModule [ "hardware" "nvidia" "optimus_prime" "intelBusId" ] [ "hardware" "nvidia" "prime" "intelBusId" ])
+    ];
+
+  options = {
+    hardware.nvidia.powerManagement.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Experimental power management through systemd. For more information, see
+        the NVIDIA docs, on Chapter 21. Configuring Power Management Support.
+      '';
+    };
+
+    hardware.nvidia.powerManagement.finegrained = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Experimental power management of PRIME offload. For more information, see
+        the NVIDIA docs, chapter 22. PCI-Express runtime power management.
+      '';
+    };
+
+    hardware.nvidia.modesetting.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable kernel modesetting when using the NVIDIA proprietary driver.
+
+        Enabling this fixes screen tearing when using Optimus via PRIME (see
+        <option>hardware.nvidia.prime.sync.enable</option>. This is not enabled
+        by default because it is not officially supported by NVIDIA and would not
+        work with SLI.
+      '';
+    };
+
+    hardware.nvidia.prime.nvidiaBusId = mkOption {
+      type = types.str;
+      default = "";
+      example = "PCI:1:0:0";
+      description = ''
+        Bus ID of the NVIDIA GPU. You can find it using lspci; for example if lspci
+        shows the NVIDIA GPU at "01:00.0", set this option to "PCI:1:0:0".
+      '';
+    };
+
+    hardware.nvidia.prime.intelBusId = mkOption {
+      type = types.str;
+      default = "";
+      example = "PCI:0:2:0";
+      description = ''
+        Bus ID of the Intel GPU. You can find it using lspci; for example if lspci
+        shows the Intel GPU at "00:02.0", set this option to "PCI:0:2:0".
+      '';
+    };
+
+    hardware.nvidia.prime.amdgpuBusId = mkOption {
+      type = types.str;
+      default = "";
+      example = "PCI:4:0:0";
+      description = ''
+        Bus ID of the AMD APU. You can find it using lspci; for example if lspci
+        shows the AMD APU at "04:00.0", set this option to "PCI:4:0:0".
+      '';
+    };
+
+    hardware.nvidia.prime.sync.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        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.
+
+        Note that this option only has any effect if the "nvidia" driver is specified
+        in <option>services.xserver.videoDrivers</option>, 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</option> and
+        <option>hardware.nvidia.prime.intelBusId</option>).
+
+        If you enable this, you may want to also enable kernel modesetting for the
+        NVIDIA driver (<option>hardware.nvidia.modesetting.enable</option>) 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>
+        option is supported is used.
+      '';
+    };
+
+    hardware.nvidia.prime.sync.allowExternalGpu = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Configure X to allow external NVIDIA GPUs when using optimus.
+      '';
+    };
+
+    hardware.nvidia.prime.offload.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        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</option> and
+        <option>hardware.nvidia.prime.intelBusId</option>).
+      '';
+    };
+
+    hardware.nvidia.nvidiaSettings = mkOption {
+      default = true;
+      type = types.bool;
+      description = ''
+        Whether to add nvidia-settings, NVIDIA's GUI configuration tool, to
+        systemPackages.
+      '';
+    };
+
+    hardware.nvidia.nvidiaPersistenced = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Update for NVIDA GPU headless mode, i.e. nvidia-persistenced. It ensures all
+        GPUs stay awake even during headless mode.
+      '';
+    };
+
+    hardware.nvidia.package = lib.mkOption {
+      type = lib.types.package;
+      default = config.boot.kernelPackages.nvidiaPackages.stable;
+      defaultText = literalExpression "config.boot.kernelPackages.nvidiaPackages.stable";
+      description = ''
+        The NVIDIA X11 derivation to use.
+      '';
+      example = literalExpression "config.boot.kernelPackages.nvidiaPackages.legacy_340";
+    };
+  };
+
+  config = let
+      igpuDriver = if pCfg.intelBusId != "" then "modesetting" else "amdgpu";
+      igpuBusId = if pCfg.intelBusId != "" then pCfg.intelBusId else pCfg.amdgpuBusId;
+  in mkIf enabled {
+    assertions = [
+      {
+        assertion = primeEnabled -> pCfg.intelBusId == "" || pCfg.amdgpuBusId == "";
+        message = ''
+          You cannot configure both an Intel iGPU and an AMD APU. Pick the one corresponding to your processor.
+        '';
+      }
+
+      {
+        assertion = primeEnabled -> pCfg.nvidiaBusId != "" && (pCfg.intelBusId != "" || pCfg.amdgpuBusId != "");
+        message = ''
+          When NVIDIA PRIME is enabled, the GPU bus IDs must configured.
+        '';
+      }
+
+      {
+        assertion = offloadCfg.enable -> versionAtLeast nvidia_x11.version "435.21";
+        message = "NVIDIA PRIME render offload is currently only supported on versions >= 435.21.";
+      }
+
+      {
+        assertion = !(syncCfg.enable && offloadCfg.enable);
+        message = "Only one NVIDIA PRIME solution may be used at a time.";
+      }
+
+      {
+        assertion = !(syncCfg.enable && cfg.powerManagement.finegrained);
+        message = "Sync precludes powering down the NVIDIA GPU.";
+      }
+
+      {
+        assertion = cfg.powerManagement.finegrained -> offloadCfg.enable;
+        message = "Fine-grained power management requires offload to be enabled.";
+      }
+
+      {
+        assertion = cfg.powerManagement.enable -> (
+          builtins.pathExists (cfg.package.out + "/bin/nvidia-sleep.sh") &&
+          builtins.pathExists (cfg.package.out + "/lib/systemd/system-sleep/nvidia")
+        );
+        message = "Required files for driver based power management don't exist.";
+      }
+    ];
+
+    # If Optimus/PRIME is enabled, we:
+    # - Specify the configured NVIDIA GPU bus ID in the Device section for the
+    #   "nvidia" driver.
+    # - Add the AllowEmptyInitialConfiguration option to the Screen section for the
+    #   "nvidia" driver, in order to allow the X server to start without any outputs.
+    # - Add a separate Device section for the Intel GPU, using the "modesetting"
+    #   driver and with the configured BusID.
+    # - OR add a separate Device section for the AMD APU, using the "amdgpu"
+    #   driver and with the configures BusID.
+    # - Reference that Device section from the ServerLayout section as an inactive
+    #   device.
+    # - Configure the display manager to run specific `xrandr` commands which will
+    #   configure/enable displays connected to the Intel iGPU / AMD APU.
+
+    services.xserver.useGlamor = mkDefault offloadCfg.enable;
+
+    services.xserver.drivers = let
+    in optional primeEnabled {
+      name = igpuDriver;
+      display = offloadCfg.enable;
+      modules = optional (igpuDriver == "amdgpu") [ pkgs.xorg.xf86videoamdgpu ];
+      deviceSection = ''
+        BusID "${igpuBusId}"
+        ${optionalString syncCfg.enable ''Option "AccelMethod" "none"''}
+      '';
+    } ++ singleton {
+      name = "nvidia";
+      modules = [ nvidia_x11.bin ];
+      display = !offloadCfg.enable;
+      deviceSection = optionalString primeEnabled
+        ''
+          BusID "${pCfg.nvidiaBusId}"
+          ${optionalString syncCfg.allowExternalGpu "Option \"AllowExternalGpus\""}
+          ${optionalString cfg.powerManagement.finegrained "Option \"NVreg_DynamicPowerManagement=0x02\""}
+        '';
+      screenSection =
+        ''
+          Option "RandRRotation" "on"
+          ${optionalString syncCfg.enable "Option \"AllowEmptyInitialConfiguration\""}
+        '';
+    };
+
+    services.xserver.serverLayoutSection = optionalString syncCfg.enable ''
+      Inactive "Device-${igpuDriver}[0]"
+    '' + optionalString offloadCfg.enable ''
+      Option "AllowNVIDIAGPUScreens"
+    '';
+
+    services.xserver.displayManager.setupCommands = optionalString syncCfg.enable ''
+      # Added by nvidia configuration module for Optimus/PRIME.
+      ${pkgs.xorg.xrandr}/bin/xrandr --setprovideroutputsource ${igpuDriver} NVIDIA-0
+      ${pkgs.xorg.xrandr}/bin/xrandr --auto
+    '';
+
+    environment.etc."nvidia/nvidia-application-profiles-rc" = mkIf nvidia_x11.useProfiles {
+      source = "${nvidia_x11.bin}/share/nvidia/nvidia-application-profiles-rc";
+    };
+
+    # 'nvidia_x11' installs it's files to /run/opengl-driver/...
+    environment.etc."egl/egl_external_platform.d".source =
+      "/run/opengl-driver/share/egl/egl_external_platform.d/";
+
+    hardware.opengl.package = mkIf (!offloadCfg.enable) nvidia_x11.out;
+    hardware.opengl.package32 = mkIf (!offloadCfg.enable) nvidia_x11.lib32;
+    hardware.opengl.extraPackages = [
+      pkgs.nvidia-vaapi-driver
+    ] ++ optional offloadCfg.enable nvidia_x11.out;
+    hardware.opengl.extraPackages32 = [
+      pkgs.pkgsi686Linux.nvidia-vaapi-driver
+    ] ++ optional offloadCfg.enable nvidia_x11.lib32;
+
+    environment.systemPackages = [ nvidia_x11.bin ]
+      ++ optionals cfg.nvidiaSettings [ nvidia_x11.settings ]
+      ++ optionals nvidiaPersistencedEnabled [ nvidia_x11.persistenced ];
+
+    systemd.packages = optional cfg.powerManagement.enable nvidia_x11.out;
+
+    systemd.services = let
+      baseNvidiaService = state: {
+        description = "NVIDIA system ${state} actions";
+
+        path = with pkgs; [ kbd ];
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = "${nvidia_x11.out}/bin/nvidia-sleep.sh '${state}'";
+        };
+      };
+
+      nvidiaService = sleepState: (baseNvidiaService sleepState) // {
+        before = [ "systemd-${sleepState}.service" ];
+        requiredBy = [ "systemd-${sleepState}.service" ];
+      };
+
+      services = (builtins.listToAttrs (map (t: nameValuePair "nvidia-${t}" (nvidiaService t)) ["hibernate" "suspend"]))
+        // {
+          nvidia-resume = (baseNvidiaService "resume") // {
+            after = [ "systemd-suspend.service" "systemd-hibernate.service" ];
+            requiredBy = [ "systemd-suspend.service" "systemd-hibernate.service" ];
+          };
+        };
+    in optionalAttrs cfg.powerManagement.enable services
+      // optionalAttrs nvidiaPersistencedEnabled {
+        "nvidia-persistenced" = mkIf nvidiaPersistencedEnabled {
+          description = "NVIDIA Persistence Daemon";
+          wantedBy = [ "multi-user.target" ];
+          serviceConfig = {
+            Type = "forking";
+            Restart = "always";
+            PIDFile = "/var/run/nvidia-persistenced/nvidia-persistenced.pid";
+            ExecStart = "${nvidia_x11.persistenced}/bin/nvidia-persistenced --verbose";
+            ExecStopPost = "${pkgs.coreutils}/bin/rm -rf /var/run/nvidia-persistenced";
+          };
+        };
+      };
+
+    systemd.tmpfiles.rules = optional config.virtualisation.docker.enableNvidia
+        "L+ /run/nvidia-docker/bin - - - - ${nvidia_x11.bin}/origBin"
+      ++ optional (nvidia_x11.persistenced != null && config.virtualisation.docker.enableNvidia)
+        "L+ /run/nvidia-docker/extras/bin/nvidia-persistenced - - - - ${nvidia_x11.persistenced}/origBin/nvidia-persistenced";
+
+    boot.extraModulePackages = [ nvidia_x11.bin ];
+
+    # nvidia-uvm is required by CUDA applications.
+    boot.kernelModules = [ "nvidia-uvm" ] ++
+      optionals config.services.xserver.enable [ "nvidia" "nvidia_modeset" "nvidia_drm" ];
+
+    # 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";
+
+    services.udev.extraRules =
+      ''
+        # Create /dev/nvidia-uvm when the nvidia-uvm module is loaded.
+        KERNEL=="nvidia", RUN+="${pkgs.runtimeShell} -c 'mknod -m 666 /dev/nvidiactl c $$(grep nvidia-frontend /proc/devices | cut -d \  -f 1) 255'"
+        KERNEL=="nvidia_modeset", RUN+="${pkgs.runtimeShell} -c 'mknod -m 666 /dev/nvidia-modeset c $$(grep nvidia-frontend /proc/devices | cut -d \  -f 1) 254'"
+        KERNEL=="card*", SUBSYSTEM=="drm", DRIVERS=="nvidia", RUN+="${pkgs.runtimeShell} -c 'mknod -m 666 /dev/nvidia%n c $$(grep nvidia-frontend /proc/devices | cut -d \  -f 1) %n'"
+        KERNEL=="nvidia_uvm", RUN+="${pkgs.runtimeShell} -c 'mknod -m 666 /dev/nvidia-uvm c $$(grep nvidia-uvm /proc/devices | cut -d \  -f 1) 0'"
+        KERNEL=="nvidia_uvm", RUN+="${pkgs.runtimeShell} -c 'mknod -m 666 /dev/nvidia-uvm-tools c $$(grep nvidia-uvm /proc/devices | cut -d \  -f 1) 0'"
+      '' + optionalString cfg.powerManagement.finegrained ''
+        # Remove NVIDIA USB xHCI Host Controller devices, if present
+        ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c0330", ATTR{remove}="1"
+
+        # Remove NVIDIA USB Type-C UCSI devices, if present
+        ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c8000", ATTR{remove}="1"
+
+        # Remove NVIDIA Audio devices, if present
+        ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x040300", ATTR{remove}="1"
+
+        # Enable runtime PM for NVIDIA VGA/3D controller devices on driver bind
+        ACTION=="bind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030000", TEST=="power/control", ATTR{power/control}="auto"
+        ACTION=="bind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030200", TEST=="power/control", ATTR{power/control}="auto"
+
+        # Disable runtime PM for NVIDIA VGA/3D controller devices on driver unbind
+        ACTION=="unbind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030000", TEST=="power/control", ATTR{power/control}="on"
+        ACTION=="unbind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030200", TEST=="power/control", ATTR{power/control}="on"
+      '';
+
+    boot.extraModprobeConfig = mkIf cfg.powerManagement.finegrained ''
+      options nvidia "NVreg_DynamicPowerManagement=0x02"
+    '';
+
+    boot.blacklistedKernelModules = [ "nouveau" "nvidiafb" ];
+
+    services.acpid.enable = true;
+
+  };
+
+}
diff --git a/nixos/modules/hardware/video/radeon.nix b/nixos/modules/hardware/video/radeon.nix
new file mode 100644
index 00000000000..c92b7a0509d
--- /dev/null
+++ b/nixos/modules/hardware/video/radeon.nix
@@ -0,0 +1,3 @@
+{
+  hardware.enableRedistributableFirmware = true;
+}
diff --git a/nixos/modules/hardware/video/switcheroo-control.nix b/nixos/modules/hardware/video/switcheroo-control.nix
new file mode 100644
index 00000000000..199adb2ad8f
--- /dev/null
+++ b/nixos/modules/hardware/video/switcheroo-control.nix
@@ -0,0 +1,18 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  pkg = [ pkgs.switcheroo-control ];
+  cfg = config.services.switcherooControl;
+in {
+  options.services.switcherooControl = {
+    enable = mkEnableOption "switcheroo-control, a D-Bus service to check the availability of dual-GPU";
+  };
+
+  config = mkIf cfg.enable {
+    services.dbus.packages = pkg;
+    environment.systemPackages = pkg;
+    systemd.packages = pkg;
+    systemd.targets.multi-user.wants = [ "switcheroo-control.service" ];
+  };
+}
diff --git a/nixos/modules/hardware/video/uvcvideo/default.nix b/nixos/modules/hardware/video/uvcvideo/default.nix
new file mode 100644
index 00000000000..338062cf69b
--- /dev/null
+++ b/nixos/modules/hardware/video/uvcvideo/default.nix
@@ -0,0 +1,64 @@
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.uvcvideo;
+
+  uvcdynctrl-udev-rules = packages: pkgs.callPackage ./uvcdynctrl-udev-rules.nix {
+    drivers = packages;
+    udevDebug = false;
+  };
+
+in
+
+{
+
+  options = {
+    services.uvcvideo.dynctrl = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable <command>uvcvideo</command> dynamic controls.
+
+          Note that enabling this brings the <command>uvcdynctrl</command> tool
+          into your environment and register all dynamic controls from
+          specified <command>packages</command> to the <command>uvcvideo</command> driver.
+        '';
+      };
+
+      packages = mkOption {
+        type = types.listOf types.path;
+        example = literalExpression "[ pkgs.tiscamera ]";
+        description = ''
+          List of packages containing <command>uvcvideo</command> dynamic controls
+          rules. All files found in
+          <filename><replaceable>pkg</replaceable>/share/uvcdynctrl/data</filename>
+          will be included.
+
+          Note that these will serve as input to the <command>libwebcam</command>
+          package which through its own <command>udev</command> rule will register
+          the dynamic controls from specified packages to the <command>uvcvideo</command>
+          driver.
+        '';
+        apply = map getBin;
+      };
+    };
+  };
+
+  config = mkIf cfg.dynctrl.enable {
+
+    services.udev.packages = [
+      (uvcdynctrl-udev-rules cfg.dynctrl.packages)
+    ];
+
+    environment.systemPackages = [
+      pkgs.libwebcam
+    ];
+
+  };
+}
diff --git a/nixos/modules/hardware/video/uvcvideo/uvcdynctrl-udev-rules.nix b/nixos/modules/hardware/video/uvcvideo/uvcdynctrl-udev-rules.nix
new file mode 100644
index 00000000000..a808429c999
--- /dev/null
+++ b/nixos/modules/hardware/video/uvcvideo/uvcdynctrl-udev-rules.nix
@@ -0,0 +1,45 @@
+{ buildEnv
+, libwebcam
+, makeWrapper
+, runCommand
+, drivers ? []
+, udevDebug ? false
+}:
+
+let
+  version = "0.0.0";
+
+  dataPath = buildEnv {
+    name = "uvcdynctrl-with-drivers-data-path";
+    paths = drivers ++ [ libwebcam ];
+    pathsToLink = [ "/share/uvcdynctrl/data" ];
+    ignoreCollisions = false;
+  };
+
+  dataDir = "${dataPath}/share/uvcdynctrl/data";
+  udevDebugVarValue = if udevDebug then "1" else "0";
+in
+
+runCommand "uvcdynctrl-udev-rules-${version}"
+{
+  inherit dataPath;
+  buildInputs = [
+    makeWrapper
+    libwebcam
+  ];
+  dontPatchELF = true;
+  dontStrip = true;
+  preferLocalBuild = true;
+}
+''
+  mkdir -p "$out/lib/udev"
+  makeWrapper "${libwebcam}/lib/udev/uvcdynctrl" "$out/lib/udev/uvcdynctrl" \
+    --set NIX_UVCDYNCTRL_DATA_DIR "${dataDir}" \
+    --set NIX_UVCDYNCTRL_UDEV_DEBUG "${udevDebugVarValue}"
+
+  mkdir -p "$out/lib/udev/rules.d"
+  cat "${libwebcam}/lib/udev/rules.d/80-uvcdynctrl.rules" | \
+    sed -r "s#RUN\+\=\"([^\"]+)\"#RUN\+\=\"$out/lib/udev/uvcdynctrl\"#g" > \
+    "$out/lib/udev/rules.d/80-uvcdynctrl.rules"
+''
+
diff --git a/nixos/modules/hardware/video/webcam/facetimehd.nix b/nixos/modules/hardware/video/webcam/facetimehd.nix
new file mode 100644
index 00000000000..d311f600c31
--- /dev/null
+++ b/nixos/modules/hardware/video/webcam/facetimehd.nix
@@ -0,0 +1,44 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.hardware.facetimehd;
+
+  kernelPackages = config.boot.kernelPackages;
+
+in
+
+{
+
+  options.hardware.facetimehd.enable = mkEnableOption "facetimehd kernel module";
+
+  config = mkIf cfg.enable {
+
+    assertions = singleton {
+      assertion = versionAtLeast kernelPackages.kernel.version "3.19";
+      message = "facetimehd is not supported for kernels older than 3.19";
+    };
+
+    boot.kernelModules = [ "facetimehd" ];
+
+    boot.blacklistedKernelModules = [ "bdc_pci" ];
+
+    boot.extraModulePackages = [ kernelPackages.facetimehd ];
+
+    hardware.firmware = [ pkgs.facetimehd-firmware ];
+
+    # unload module during suspend/hibernate as it crashes the whole system
+    powerManagement.powerDownCommands = ''
+      ${pkgs.kmod}/bin/lsmod | ${pkgs.gnugrep}/bin/grep -q "^facetimehd" && ${pkgs.kmod}/bin/rmmod -f -v facetimehd
+    '';
+
+    # and load it back on resume
+    powerManagement.resumeCommands = ''
+      ${pkgs.kmod}/bin/modprobe -v facetimehd
+    '';
+
+  };
+
+}
diff --git a/nixos/modules/hardware/wooting.nix b/nixos/modules/hardware/wooting.nix
new file mode 100644
index 00000000000..ee550cbbf6b
--- /dev/null
+++ b/nixos/modules/hardware/wooting.nix
@@ -0,0 +1,12 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+{
+  options.hardware.wooting.enable =
+    mkEnableOption "Enable support for Wooting keyboards";
+
+  config = mkIf config.hardware.wooting.enable {
+    environment.systemPackages = [ pkgs.wootility ];
+    services.udev.packages = [ pkgs.wooting-udev-rules ];
+  };
+}
diff --git a/nixos/modules/hardware/xone.nix b/nixos/modules/hardware/xone.nix
new file mode 100644
index 00000000000..89690d8c6fb
--- /dev/null
+++ b/nixos/modules/hardware/xone.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.hardware.xone;
+in
+{
+  options.hardware.xone = {
+    enable = mkEnableOption "the xone driver for Xbox One and Xbobx Series X|S accessories";
+  };
+
+  config = mkIf cfg.enable {
+    boot = {
+      blacklistedKernelModules = [ "xpad" "mt76x2u" ];
+      extraModulePackages = with config.boot.kernelPackages; [ xone ];
+    };
+    hardware.firmware = [ pkgs.xow_dongle-firmware ];
+  };
+
+  meta = {
+    maintainers = with maintainers; [ rhysmdnz ];
+  };
+}
diff --git a/nixos/modules/hardware/xpadneo.nix b/nixos/modules/hardware/xpadneo.nix
new file mode 100644
index 00000000000..dbc4ba21256
--- /dev/null
+++ b/nixos/modules/hardware/xpadneo.nix
@@ -0,0 +1,29 @@
+{ config, lib, ... }:
+
+with lib;
+let
+  cfg = config.hardware.xpadneo;
+in
+{
+  options.hardware.xpadneo = {
+    enable = mkEnableOption "the xpadneo driver for Xbox One wireless controllers";
+  };
+
+  config = mkIf cfg.enable {
+    boot = {
+      # Must disable Enhanced Retransmission Mode to support bluetooth pairing
+      # https://wiki.archlinux.org/index.php/Gamepad#Connect_Xbox_Wireless_Controller_with_Bluetooth
+      extraModprobeConfig =
+        mkIf
+          config.hardware.bluetooth.enable
+          "options bluetooth disable_ertm=1";
+
+      extraModulePackages = with config.boot.kernelPackages; [ xpadneo ];
+      kernelModules = [ "hid_xpadneo" ];
+    };
+  };
+
+  meta = {
+    maintainers = with maintainers; [ kira-bruneau ];
+  };
+}
diff --git a/nixos/modules/i18n/input-method/default.nix b/nixos/modules/i18n/input-method/default.nix
new file mode 100644
index 00000000000..bbc5783565a
--- /dev/null
+++ b/nixos/modules/i18n/input-method/default.nix
@@ -0,0 +1,74 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.i18n.inputMethod;
+
+  gtk2_cache = pkgs.runCommand "gtk2-immodule.cache"
+    { preferLocalBuild = true;
+      allowSubstitutes = false;
+      buildInputs = [ pkgs.gtk2 cfg.package ];
+    }
+    ''
+      mkdir -p $out/etc/gtk-2.0/
+      GTK_PATH=${cfg.package}/lib/gtk-2.0/ gtk-query-immodules-2.0 > $out/etc/gtk-2.0/immodules.cache
+    '';
+
+  gtk3_cache = pkgs.runCommand "gtk3-immodule.cache"
+    { preferLocalBuild = true;
+      allowSubstitutes = false;
+      buildInputs = [ pkgs.gtk3 cfg.package ];
+    }
+    ''
+      mkdir -p $out/etc/gtk-3.0/
+      GTK_PATH=${cfg.package}/lib/gtk-3.0/ gtk-query-immodules-3.0 > $out/etc/gtk-3.0/immodules.cache
+    '';
+
+in
+{
+  options.i18n = {
+    inputMethod = {
+      enabled = mkOption {
+        type    = types.nullOr (types.enum [ "ibus" "fcitx" "fcitx5" "nabi" "uim" "hime" "kime" ]);
+        default = null;
+        example = "fcitx";
+        description = ''
+          Select the enabled input method. Input methods is a software to input symbols that are not available on standard input devices.
+
+          Input methods are specially used to input Chinese, Japanese and Korean characters.
+
+          Currently the following input methods are available in NixOS:
+
+          <itemizedlist>
+          <listitem><para>ibus: The intelligent input bus, extra input engines can be added using <literal>i18n.inputMethod.ibus.engines</literal>.</para></listitem>
+          <listitem><para>fcitx: A customizable lightweight input method, extra input engines can be added using <literal>i18n.inputMethod.fcitx.engines</literal>.</para></listitem>
+          <listitem><para>fcitx5: The next generation of fcitx, addons (including engines, dictionaries, skins) can be added using <literal>i18n.inputMethod.fcitx5.addons</literal>.</para></listitem>
+          <listitem><para>nabi: A Korean input method based on XIM. Nabi doesn't support Qt 5.</para></listitem>
+          <listitem><para>uim: The universal input method, is a library with a XIM bridge. uim mainly support Chinese, Japanese and Korean.</para></listitem>
+          <listitem><para>hime: An extremely easy-to-use input method framework.</para></listitem>
+          <listitem><para>kime: Koream IME.</para></listitem>
+          </itemizedlist>
+        '';
+      };
+
+      package = mkOption {
+        internal = true;
+        type     = types.nullOr types.path;
+        default  = null;
+        description = ''
+          The input method method package.
+        '';
+      };
+    };
+  };
+
+  config = mkIf (cfg.enabled != null) {
+    environment.systemPackages = [ cfg.package gtk2_cache gtk3_cache ];
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ ericsagnes ];
+    doc = ./default.xml;
+  };
+
+}
diff --git a/nixos/modules/i18n/input-method/default.xml b/nixos/modules/i18n/input-method/default.xml
new file mode 100644
index 00000000000..dd66316c730
--- /dev/null
+++ b/nixos/modules/i18n/input-method/default.xml
@@ -0,0 +1,291 @@
+<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
new file mode 100644
index 00000000000..7738581b893
--- /dev/null
+++ b/nixos/modules/i18n/input-method/fcitx.nix
@@ -0,0 +1,46 @@
+{ 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: "<literal>${name}</literal>") (attrNames enginesDrv));
+          in
+            "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
new file mode 100644
index 00000000000..414aabbbaa7
--- /dev/null
+++ b/nixos/modules/i18n/input-method/fcitx5.nix
@@ -0,0 +1,38 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  im = config.i18n.inputMethod;
+  cfg = im.fcitx5;
+  fcitx5Package = pkgs.fcitx5-with-addons.override { inherit (cfg) addons; };
+in {
+  options = {
+    i18n.inputMethod.fcitx5 = {
+      addons = mkOption {
+        type = with types; listOf package;
+        default = [];
+        example = literalExpression "with pkgs; [ fcitx5-rime ]";
+        description = ''
+          Enabled Fcitx5 addons.
+        '';
+      };
+    };
+  };
+
+  config = mkIf (im.enabled == "fcitx5") {
+    i18n.inputMethod.package = fcitx5Package;
+
+    environment.variables = {
+      GTK_IM_MODULE = "fcitx";
+      QT_IM_MODULE = "fcitx";
+      XMODIFIERS = "@im=fcitx";
+    };
+
+    systemd.user.services.fcitx5-daemon = {
+      enable = true;
+      script = "${fcitx5Package}/bin/fcitx5";
+      wantedBy = [ "graphical-session.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/i18n/input-method/hime.nix b/nixos/modules/i18n/input-method/hime.nix
new file mode 100644
index 00000000000..8482130db3e
--- /dev/null
+++ b/nixos/modules/i18n/input-method/hime.nix
@@ -0,0 +1,14 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+{
+  config = mkIf (config.i18n.inputMethod.enabled == "hime") {
+    i18n.inputMethod.package = pkgs.hime;
+    environment.variables = {
+      GTK_IM_MODULE = "hime";
+      QT_IM_MODULE  = "hime";
+      XMODIFIERS    = "@im=hime";
+    };
+    services.xserver.displayManager.sessionCommands = "${pkgs.hime}/bin/hime &";
+  };
+}
diff --git a/nixos/modules/i18n/input-method/ibus.nix b/nixos/modules/i18n/input-method/ibus.nix
new file mode 100644
index 00000000000..c5b0cbc2150
--- /dev/null
+++ b/nixos/modules/i18n/input-method/ibus.nix
@@ -0,0 +1,86 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.i18n.inputMethod.ibus;
+  ibusPackage = pkgs.ibus-with-plugins.override { plugins = cfg.engines; };
+  ibusEngine = types.package // {
+    name  = "ibus-engine";
+    check = x: (lib.types.package.check x) && (attrByPath ["meta" "isIbusEngine"] false x);
+  };
+
+  impanel =
+    if cfg.panel != null
+    then "--panel=${cfg.panel}"
+    else "";
+
+  ibusAutostart = pkgs.writeTextFile {
+    name = "autostart-ibus-daemon";
+    destination = "/etc/xdg/autostart/ibus-daemon.desktop";
+    text = ''
+      [Desktop Entry]
+      Name=IBus
+      Type=Application
+      Exec=${ibusPackage}/bin/ibus-daemon --daemonize --xim ${impanel}
+    '';
+  };
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "programs" "ibus" "plugins" ] [ "i18n" "inputMethod" "ibus" "engines" ])
+  ];
+
+  options = {
+    i18n.inputMethod.ibus = {
+      engines = mkOption {
+        type    = with types; listOf ibusEngine;
+        default = [];
+        example = literalExpression "with pkgs.ibus-engines; [ mozc hangul ]";
+        description =
+          let
+            enginesDrv = filterAttrs (const isDerivation) pkgs.ibus-engines;
+            engines = concatStringsSep ", "
+              (map (name: "<literal>${name}</literal>") (attrNames enginesDrv));
+          in
+            "Enabled IBus engines. Available engines are: ${engines}.";
+      };
+      panel = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = literalExpression ''"''${pkgs.plasma5Packages.plasma-desktop}/lib/libexec/kimpanel-ibus-panel"'';
+        description = "Replace the IBus panel with another panel.";
+      };
+    };
+  };
+
+  config = mkIf (config.i18n.inputMethod.enabled == "ibus") {
+    i18n.inputMethod.package = ibusPackage;
+
+    environment.systemPackages = [
+      ibusAutostart
+    ];
+
+    # Without dconf enabled it is impossible to use IBus
+    programs.dconf.enable = true;
+
+    programs.dconf.packages = [ ibusPackage ];
+
+    services.dbus.packages = [
+      ibusAutostart
+    ];
+
+    environment.variables = {
+      GTK_IM_MODULE = "ibus";
+      QT_IM_MODULE = "ibus";
+      XMODIFIERS = "@im=ibus";
+    };
+
+    xdg.portal.extraPortals = mkIf config.xdg.portal.enable [
+      ibusPackage
+    ];
+  };
+
+  # uses attributes of the linked package
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/i18n/input-method/kime.nix b/nixos/modules/i18n/input-method/kime.nix
new file mode 100644
index 00000000000..729a665614a
--- /dev/null
+++ b/nixos/modules/i18n/input-method/kime.nix
@@ -0,0 +1,51 @@
+{ 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"];
+            };
+
+            indicator = {
+              icon_color = "White";
+            };
+
+            engine = {
+              hangul = {
+                layout = "dubeolsik";
+              };
+            };
+          }
+          '';
+        description = ''
+          kime configuration. Refer to <link xlink:href="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") {
+    i18n.inputMethod.package = pkgs.kime;
+
+    environment.variables = {
+      GTK_IM_MODULE = "kime";
+      QT_IM_MODULE  = "kime";
+      XMODIFIERS    = "@im=kime";
+    };
+
+    environment.etc."xdg/kime/config.yaml".text = replaceStrings [ "\\\\" ] [ "\\" ] (builtins.toJSON cfg.config);
+  };
+
+  # uses attributes of the linked package
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/i18n/input-method/nabi.nix b/nixos/modules/i18n/input-method/nabi.nix
new file mode 100644
index 00000000000..87620ae4e7b
--- /dev/null
+++ b/nixos/modules/i18n/input-method/nabi.nix
@@ -0,0 +1,16 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+{
+  config = mkIf (config.i18n.inputMethod.enabled == "nabi") {
+    i18n.inputMethod.package = pkgs.nabi;
+
+    environment.variables = {
+      GTK_IM_MODULE = "nabi";
+      QT_IM_MODULE  = "nabi";
+      XMODIFIERS    = "@im=nabi";
+    };
+
+    services.xserver.displayManager.sessionCommands = "${pkgs.nabi}/bin/nabi &";
+  };
+}
diff --git a/nixos/modules/i18n/input-method/uim.nix b/nixos/modules/i18n/input-method/uim.nix
new file mode 100644
index 00000000000..459294657e0
--- /dev/null
+++ b/nixos/modules/i18n/input-method/uim.nix
@@ -0,0 +1,37 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.i18n.inputMethod.uim;
+in
+{
+  options = {
+
+    i18n.inputMethod.uim = {
+      toolbar = mkOption {
+        type    = types.enum [ "gtk" "gtk3" "gtk-systray" "gtk3-systray" "qt4" ];
+        default = "gtk";
+        example = "gtk-systray";
+        description = ''
+          selected UIM toolbar.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf (config.i18n.inputMethod.enabled == "uim") {
+    i18n.inputMethod.package = pkgs.uim;
+
+    environment.variables = {
+      GTK_IM_MODULE = "uim";
+      QT_IM_MODULE  = "uim";
+      XMODIFIERS    = "@im=uim";
+    };
+    services.xserver.displayManager.sessionCommands = ''
+      ${pkgs.uim}/bin/uim-xim &
+      ${pkgs.uim}/bin/uim-toolbar-${cfg.toolbar} &
+    '';
+  };
+}
diff --git a/nixos/modules/installer/cd-dvd/channel.nix b/nixos/modules/installer/cd-dvd/channel.nix
new file mode 100644
index 00000000000..92164d65e53
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/channel.nix
@@ -0,0 +1,49 @@
+# Provide an initial copy of the NixOS channel so that the user
+# doesn't need to run "nix-channel --update" first.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  nixpkgs = lib.cleanSource pkgs.path;
+
+  # We need a copy of the Nix expressions for Nixpkgs and NixOS on the
+  # CD.  These are installed into the "nixos" channel of the root
+  # user, as expected by nixos-rebuild/nixos-install. FIXME: merge
+  # with make-channel.nix.
+  channelSources = pkgs.runCommand "nixos-${config.system.nixos.version}"
+    { preferLocalBuild = true; }
+    ''
+      mkdir -p $out
+      cp -prd ${nixpkgs.outPath} $out/nixos
+      chmod -R u+w $out/nixos
+      if [ ! -e $out/nixos/nixpkgs ]; then
+        ln -s . $out/nixos/nixpkgs
+      fi
+      ${optionalString (config.system.nixos.revision != null) ''
+        echo -n ${config.system.nixos.revision} > $out/nixos/.git-revision
+      ''}
+      echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
+      echo ${config.system.nixos.versionSuffix} | sed -e s/pre// > $out/nixos/svn-revision
+    '';
+
+in
+
+{
+  # Provide the NixOS/Nixpkgs sources in /etc/nixos.  This is required
+  # for nixos-install.
+  boot.postBootCommands = mkAfter
+    ''
+      if ! [ -e /var/lib/nixos/did-channel-init ]; then
+        echo "unpacking the NixOS/Nixpkgs sources..."
+        mkdir -p /nix/var/nix/profiles/per-user/root
+        ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/per-user/root/channels \
+          -i ${channelSources} --quiet --option build-use-substitutes false
+        mkdir -m 0700 -p /root/.nix-defexpr
+        ln -s /nix/var/nix/profiles/per-user/root/channels /root/.nix-defexpr/channels
+        mkdir -m 0755 -p /var/lib/nixos
+        touch /var/lib/nixos/did-channel-init
+      fi
+    '';
+}
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-base.nix b/nixos/modules/installer/cd-dvd/installation-cd-base.nix
new file mode 100644
index 00000000000..618057618d0
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/installation-cd-base.nix
@@ -0,0 +1,50 @@
+# This module contains the basic configuration for building a NixOS
+# installation CD.
+
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+{
+  imports =
+    [ ./iso-image.nix
+
+      # Profiles of this basic installation CD.
+      ../../profiles/all-hardware.nix
+      ../../profiles/base.nix
+      ../../profiles/installation-device.nix
+    ];
+
+  # Adds terminus_font for people with HiDPI displays
+  console.packages = options.console.packages.default ++ [ pkgs.terminus_font ];
+
+  # ISO naming.
+  isoImage.isoName = "${config.isoImage.isoBaseName}-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.iso";
+
+  # EFI booting
+  isoImage.makeEfiBootable = true;
+
+  # USB booting
+  isoImage.makeUsbBootable = true;
+
+  # Add Memtest86+ to the CD.
+  boot.loader.grub.memtest86.enable = true;
+
+  # An installation media cannot tolerate a host config defined file
+  # system layout on a fresh machine, before it has been formatted.
+  swapDevices = mkImageMediaOverride [ ];
+  fileSystems = mkImageMediaOverride config.lib.isoFileSystems;
+
+  boot.postBootCommands = ''
+    for o in $(</proc/cmdline); do
+      case "$o" in
+        live.nixos.passwd=*)
+          set -- $(IFS==; echo $o)
+          echo "nixos:$2" | ${pkgs.shadow}/bin/chpasswd
+          ;;
+      esac
+    done
+  '';
+
+  system.stateVersion = mkDefault "18.03";
+}
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix
new file mode 100644
index 00000000000..fa19daf1328
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix
@@ -0,0 +1,56 @@
+# This module contains the basic configuration for building a graphical NixOS
+# installation CD.
+
+{ lib, pkgs, ... }:
+
+with lib;
+
+{
+  imports = [ ./installation-cd-base.nix ];
+
+  # Whitelist wheel users to do anything
+  # This is useful for things like pkexec
+  #
+  # WARNING: this is dangerous for systems
+  # outside the installation-cd and shouldn't
+  # be used anywhere else.
+  security.polkit.extraConfig = ''
+    polkit.addRule(function(action, subject) {
+      if (subject.isInGroup("wheel")) {
+        return polkit.Result.YES;
+      }
+    });
+  '';
+
+  services.xserver.enable = true;
+
+  # Provide networkmanager for easy wireless configuration.
+  networking.networkmanager.enable = true;
+  networking.wireless.enable = mkForce false;
+
+  # KDE complains if power management is disabled (to be precise, if
+  # there is no power management backend such as upower).
+  powerManagement.enable = true;
+
+  # Enable sound in graphical iso's.
+  hardware.pulseaudio.enable = true;
+
+  environment.systemPackages = [
+    # Include gparted for partitioning disks.
+    pkgs.gparted
+
+    # Include some editors.
+    pkgs.vim
+    pkgs.bvi # binary editor
+    pkgs.joe
+
+    # Include some version control tools.
+    pkgs.git
+
+    # Firefox for reading the manual.
+    pkgs.firefox
+
+    pkgs.glxinfo
+  ];
+
+}
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
new file mode 100644
index 00000000000..303493741f3
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
@@ -0,0 +1,38 @@
+# This module defines a NixOS installation CD that contains GNOME.
+
+{ lib, ... }:
+
+with lib;
+
+{
+  imports = [ ./installation-cd-graphical-base.nix ];
+
+  isoImage.edition = "gnome";
+
+  services.xserver.desktopManager.gnome = {
+    # Add Firefox and other tools useful for installation to the launcher
+    favoriteAppsOverride = ''
+      [org.gnome.shell]
+      favorite-apps=[ 'firefox.desktop', 'nixos-manual.desktop', 'org.gnome.Terminal.desktop', 'org.gnome.Nautilus.desktop', 'gparted.desktop' ]
+    '';
+    enable = true;
+  };
+
+  services.xserver.displayManager = {
+    gdm = {
+      enable = true;
+      # autoSuspend makes the machine automatically suspend after inactivity.
+      # It's possible someone could/try to ssh'd into the machine and obviously
+      # have issues because it's inactive.
+      # See:
+      # * https://github.com/NixOS/nixpkgs/pull/63790
+      # * https://gitlab.gnome.org/GNOME/gnome-control-center/issues/22
+      autoSuspend = false;
+    };
+    autoLogin = {
+      enable = true;
+      user = "nixos";
+    };
+  };
+
+}
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5-new-kernel.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5-new-kernel.nix
new file mode 100644
index 00000000000..d98325a99ac
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5-new-kernel.nix
@@ -0,0 +1,7 @@
+{ pkgs, ... }:
+
+{
+  imports = [ ./installation-cd-graphical-plasma5.nix ];
+
+  boot.kernelPackages = pkgs.linuxPackages_latest;
+}
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix
new file mode 100644
index 00000000000..098c2b2870b
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix
@@ -0,0 +1,50 @@
+# This module defines a NixOS installation CD that contains X11 and
+# Plasma 5.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  imports = [ ./installation-cd-graphical-base.nix ];
+
+  isoImage.edition = "plasma5";
+
+  services.xserver = {
+    desktopManager.plasma5 = {
+      enable = true;
+    };
+
+    # Automatically login as nixos.
+    displayManager = {
+      sddm.enable = true;
+      autoLogin = {
+        enable = true;
+        user = "nixos";
+      };
+    };
+  };
+
+  environment.systemPackages = with pkgs; [
+    # Graphical text editor
+    kate
+  ];
+
+  system.activationScripts.installerDesktop = let
+
+    # Comes from documentation.nix when xserver and nixos.enable are true.
+    manualDesktopFile = "/run/current-system/sw/share/applications/nixos-manual.desktop";
+
+    homeDir = "/home/nixos/";
+    desktopDir = homeDir + "Desktop/";
+
+  in ''
+    mkdir -p ${desktopDir}
+    chown nixos ${homeDir} ${desktopDir}
+
+    ln -sfT ${manualDesktopFile} ${desktopDir + "nixos-manual.desktop"}
+    ln -sfT ${pkgs.gparted}/share/applications/gparted.desktop ${desktopDir + "gparted.desktop"}
+    ln -sfT ${pkgs.konsole}/share/applications/org.kde.konsole.desktop ${desktopDir + "org.kde.konsole.desktop"}
+  '';
+
+}
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-minimal-new-kernel.nix b/nixos/modules/installer/cd-dvd/installation-cd-minimal-new-kernel.nix
new file mode 100644
index 00000000000..3911a2b01b1
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/installation-cd-minimal-new-kernel.nix
@@ -0,0 +1,7 @@
+{ pkgs, ... }:
+
+{
+  imports = [ ./installation-cd-minimal.nix ];
+
+  boot.kernelPackages = pkgs.linuxPackages_latest;
+}
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix b/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix
new file mode 100644
index 00000000000..97506045e0e
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix
@@ -0,0 +1,14 @@
+# This module defines a small NixOS installation CD.  It does not
+# contain any graphical stuff.
+
+{ ... }:
+
+{
+  imports =
+    [ ./installation-cd-base.nix
+    ];
+
+  isoImage.edition = "minimal";
+
+  fonts.fontconfig.enable = false;
+}
diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix
new file mode 100644
index 00000000000..3ff1b3d670e
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/iso-image.nix
@@ -0,0 +1,811 @@
+# This module creates a bootable ISO image containing the given NixOS
+# configuration.  The derivation for the ISO image will be placed in
+# config.system.build.isoImage.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  /**
+   * Given a list of `options`, concats the result of mapping each options
+   * to a menuentry for use in grub.
+   *
+   *  * defaults: {name, image, params, initrd}
+   *  * options: [ option... ]
+   *  * option: {name, params, class}
+   */
+  menuBuilderGrub2 =
+  defaults: options: lib.concatStrings
+    (
+      map
+      (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 ""} {
+          linux ${defaults.image} \''${isoboot} ${defaults.params} ${
+            option.params or ""
+          }
+          initrd ${defaults.initrd}
+        }
+      '')
+      options
+    )
+  ;
+
+  /**
+   * Given a `config`, builds the default options.
+   */
+  buildMenuGrub2 = config:
+    buildMenuAdditionalParamsGrub2 config ""
+  ;
+
+  /**
+   * Given a `config` and params to add to `params`, build a set of default options.
+   * Use this one when creating a variant (e.g. hidpi)
+   */
+  buildMenuAdditionalParamsGrub2 = config: additional:
+  let
+    finalCfg = {
+      name = "NixOS ${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";
+    };
+  in
+    menuBuilderGrub2
+    finalCfg
+    [
+      { class = "installer"; }
+      { class = "nomodeset"; params = "nomodeset"; }
+      { class = "copytoram"; params = "copytoram"; }
+      { class = "debug";     params = "debug"; }
+    ]
+  ;
+
+  # Timeout in syslinux is in units of 1/10 of a second.
+  # 0 is used to disable timeouts.
+  syslinuxTimeout = if config.boot.loader.timeout == null then
+      0
+    else
+      max (config.boot.loader.timeout * 10) 1;
+
+
+  max = x: y: if x > y then x else y;
+
+  # The configuration file for syslinux.
+
+  # Notes on syslinux configuration and UNetbootin compatiblity:
+  #   * Do not use '/syslinux/syslinux.cfg' as the path for this
+  #     configuration. UNetbootin will not parse the file and use it as-is.
+  #     This results in a broken configuration if the partition label does
+  #     not match the specified config.isoImage.volumeID. For this reason
+  #     we're using '/isolinux/isolinux.cfg'.
+  #   * Use APPEND instead of adding command-line arguments directly after
+  #     the LINUX entries.
+  #   * COM32 entries (chainload, reboot, poweroff) are not recognized. They
+  #     result in incorrect boot entries.
+
+  baseIsolinuxCfg = ''
+    SERIAL 0 115200
+    TIMEOUT ${builtins.toString syslinuxTimeout}
+    UI vesamenu.c32
+    MENU TITLE NixOS
+    MENU BACKGROUND /isolinux/background.png
+    MENU RESOLUTION 800 600
+    MENU CLEAR
+    MENU ROWS 6
+    MENU CMDLINEROW -4
+    MENU TIMEOUTROW -3
+    MENU TABMSGROW  -2
+    MENU HELPMSGROW -1
+    MENU HELPMSGENDROW -1
+    MENU MARGIN 0
+
+    #                                FG:AARRGGBB  BG:AARRGGBB   shadow
+    MENU COLOR BORDER       30;44      #00000000    #00000000   none
+    MENU COLOR SCREEN       37;40      #FF000000    #00E2E8FF   none
+    MENU COLOR TABMSG       31;40      #80000000    #00000000   none
+    MENU COLOR TIMEOUT      1;37;40    #FF000000    #00000000   none
+    MENU COLOR TIMEOUT_MSG  37;40      #FF000000    #00000000   none
+    MENU COLOR CMDMARK      1;36;40    #FF000000    #00000000   none
+    MENU COLOR CMDLINE      37;40      #FF000000    #00000000   none
+    MENU COLOR TITLE        1;36;44    #00000000    #00000000   none
+    MENU COLOR UNSEL        37;44      #FF000000    #00000000   none
+    MENU COLOR SEL          7;37;40    #FFFFFFFF    #FF5277C3   std
+
+    DEFAULT boot
+
+    LABEL boot
+    MENU LABEL NixOS ${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)
+    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)
+    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)
+    LINUX /boot/${config.system.boot.loader.kernelFile}
+    APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} loglevel=7
+    INITRD /boot/${config.system.boot.loader.initrdFile}
+
+    # A variant to boot with a serial console enabled
+    LABEL boot-serial
+    MENU LABEL NixOS ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (serial console=ttyS0,115200n8)
+    LINUX /boot/${config.system.boot.loader.kernelFile}
+    APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} console=ttyS0,115200n8
+    INITRD /boot/${config.system.boot.loader.initrdFile}
+  '';
+
+  isolinuxMemtest86Entry = ''
+    LABEL memtest
+    MENU LABEL Memtest86+
+    LINUX /boot/memtest.bin
+    APPEND ${toString config.boot.loader.grub.memtest86.params}
+  '';
+
+  isolinuxCfg = concatStringsSep "\n"
+    ([ baseIsolinuxCfg ] ++ optional config.boot.loader.grub.memtest86.enable isolinuxMemtest86Entry);
+
+  refindBinary = if targetArch == "x64" || targetArch == "aa64" then "refind_${targetArch}.efi" else null;
+
+  # Setup instructions for rEFInd.
+  refind =
+    if refindBinary != null then
+      ''
+      # Adds rEFInd to the ISO.
+      cp -v ${pkgs.refind}/share/refind/${refindBinary} $out/EFI/boot/
+      ''
+    else
+      "# No refind for ${targetArch}"
+  ;
+
+  grubPkgs = if config.boot.loader.grub.forcei686 then pkgs.pkgsi686Linux else pkgs;
+
+  grubMenuCfg = ''
+    #
+    # Menu configuration
+    #
+
+    # Search using a "marker file"
+    search --set=root --file /EFI/nixos-installer-image
+
+    insmod gfxterm
+    insmod png
+    set gfxpayload=keep
+    set gfxmode=${concatStringsSep "," [
+      # GRUB will use the first valid mode listed here.
+      # `auto` will sometimes choose the smallest valid mode it detects.
+      # So instead we'll list a lot of possibly valid modes :/
+      #"3840x2160"
+      #"2560x1440"
+      "1920x1080"
+      "1366x768"
+      "1280x720"
+      "1024x768"
+      "800x600"
+      "auto"
+    ]}
+
+    # Fonts can be loaded?
+    # (This font is assumed to always be provided as a fallback by NixOS)
+    if loadfont (\$root)/EFI/boot/unicode.pf2; then
+      set with_fonts=true
+    fi
+    if [ "\$textmode" != "true" -a "\$with_fonts" == "true" ]; then
+      # Use graphical term, it can be either with background image or a theme.
+      # input is "console", while output is "gfxterm".
+      # This enables "serial" input and output only when possible.
+      # Otherwise the failure mode is to not even enable gfxterm.
+      if test "\$with_serial" == "yes"; then
+        terminal_output gfxterm serial
+        terminal_input  console serial
+      else
+        terminal_output gfxterm
+        terminal_input  console
+      fi
+    else
+      # Sets colors for the non-graphical term.
+      set menu_color_normal=cyan/blue
+      set menu_color_highlight=white/blue
+    fi
+
+    ${ # When there is a theme configured, use it, otherwise use the background image.
+    if config.isoImage.grubTheme != null then ''
+      # Sets theme.
+      set theme=(\$root)/EFI/boot/grub-theme/theme.txt
+      # Load theme fonts
+      $(find ${config.isoImage.grubTheme} -iname '*.pf2' -printf "loadfont (\$root)/EFI/boot/grub-theme/%P\n")
+    '' else ''
+      if background_image (\$root)/EFI/boot/efi-background.png; then
+        # Black background means transparent background when there
+        # is a background image set... This seems undocumented :(
+        set color_normal=black/black
+        set color_highlight=white/blue
+      else
+        # Falls back again to proper colors.
+        set menu_color_normal=cyan/blue
+        set menu_color_highlight=white/blue
+      fi
+    ''}
+  '';
+
+  # The EFI boot image.
+  # Notes about grub:
+  #  * Yes, the grubMenuCfg has to be repeated in all submenus. Otherwise you
+  #    will get white-on-black console-like text on sub-menus. *sigh*
+  efiDir = pkgs.runCommand "efi-directory" {
+    nativeBuildInputs = [ pkgs.buildPackages.grub2_efi ];
+    strictDeps = true;
+  } ''
+    mkdir -p $out/EFI/boot/
+
+    # Add a marker so GRUB can find the filesystem.
+    touch $out/EFI/nixos-installer-image
+
+    # ALWAYS required modules.
+    MODULES="fat iso9660 part_gpt part_msdos \
+             normal boot linux configfile loopback chain halt \
+             efifwsetup efi_gop \
+             ls search search_label search_fs_uuid search_fs_file \
+             gfxmenu gfxterm gfxterm_background gfxterm_menu test all_video loadenv \
+             exfat ext2 ntfs btrfs hfsplus udf \
+             videoinfo png \
+             echo serial \
+            "
+
+    echo "Building GRUB with modules:"
+    for mod in $MODULES; do
+      echo " - $mod"
+    done
+
+    # Modules that may or may not be available per-platform.
+    echo "Adding additional modules:"
+    for mod in efi_uga; do
+      if [ -f ${grubPkgs.grub2_efi}/lib/grub/${grubPkgs.grub2_efi.grubTarget}/$mod.mod ]; then
+        echo " - $mod"
+        MODULES+=" $mod"
+      fi
+    done
+
+    # Make our own efi program, we can't rely on "grub-install" since it seems to
+    # probe for devices, even with --skip-fs-probe.
+    grub-mkimage --directory=${grubPkgs.grub2_efi}/lib/grub/${grubPkgs.grub2_efi.grubTarget} -o $out/EFI/boot/boot${targetArch}.efi -p /EFI/boot -O ${grubPkgs.grub2_efi.grubTarget} \
+      $MODULES
+    cp ${grubPkgs.grub2_efi}/share/grub/unicode.pf2 $out/EFI/boot/
+
+    cat <<EOF > $out/EFI/boot/grub.cfg
+
+    set with_fonts=false
+    set textmode=false
+    # If you want to use serial for "terminal_*" commands, you need to set one up:
+    #   Example manual configuration:
+    #    → serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1
+    # This uses the defaults, and makes the serial terminal available.
+    set with_serial=no
+    if serial; then set with_serial=yes ;fi
+    export with_serial
+    clear
+    set timeout=10
+
+    # This message will only be viewable when "gfxterm" is not used.
+    echo ""
+    echo "Loading graphical boot menu..."
+    echo ""
+    echo "Press 't' to use the text boot menu on this console..."
+    echo ""
+
+    ${grubMenuCfg}
+
+    hiddenentry 'Text mode' --hotkey 't' {
+      loadfont (\$root)/EFI/boot/unicode.pf2
+      set textmode=true
+      terminal_output gfxterm console
+    }
+    hiddenentry 'GUI mode' --hotkey 'g' {
+      $(find ${config.isoImage.grubTheme} -iname '*.pf2' -printf "loadfont (\$root)/EFI/boot/grub-theme/%P\n")
+      set textmode=false
+      terminal_output gfxterm
+    }
+
+
+    # If the parameter iso_path is set, append the findiso parameter to the kernel
+    # line. We need this to allow the nixos iso to be booted from grub directly.
+    if [ \''${iso_path} ] ; then
+      set isoboot="findiso=\''${iso_path}"
+    fi
+
+    #
+    # Menu entries
+    #
+
+    ${buildMenuGrub2 config}
+    submenu "HiDPI, Quirks and Accessibility" --class hidpi --class submenu {
+      ${grubMenuCfg}
+      submenu "Suggests resolution @720p" --class hidpi-720p {
+        ${grubMenuCfg}
+        ${buildMenuAdditionalParamsGrub2 config "video=1280x720@60"}
+      }
+      submenu "Suggests resolution @1080p" --class hidpi-1080p {
+        ${grubMenuCfg}
+        ${buildMenuAdditionalParamsGrub2 config "video=1920x1080@60"}
+      }
+
+      # If we boot into a graphical environment where X is autoran
+      # and always crashes, it makes the media unusable. Allow the user
+      # to disable this.
+      submenu "Disable display-manager" --class quirk-disable-displaymanager {
+        ${grubMenuCfg}
+        ${buildMenuAdditionalParamsGrub2 config "systemd.mask=display-manager.service"}
+      }
+
+      # Some laptop and convertibles have the panel installed in an
+      # inconvenient way, rotated away from the keyboard.
+      # Those entries makes it easier to use the installer.
+      submenu "" {return}
+      submenu "Rotate framebuffer Clockwise" --class rotate-90cw {
+        ${grubMenuCfg}
+        ${buildMenuAdditionalParamsGrub2 config "fbcon=rotate:1"}
+      }
+      submenu "Rotate framebuffer Upside-Down" --class rotate-180 {
+        ${grubMenuCfg}
+        ${buildMenuAdditionalParamsGrub2 config "fbcon=rotate:2"}
+      }
+      submenu "Rotate framebuffer Counter-Clockwise" --class rotate-90ccw {
+        ${grubMenuCfg}
+        ${buildMenuAdditionalParamsGrub2 config "fbcon=rotate:3"}
+      }
+
+      # As a proof of concept, mainly. (Not sure it has accessibility merits.)
+      submenu "" {return}
+      submenu "Use black on white" --class accessibility-blakconwhite {
+        ${grubMenuCfg}
+        ${buildMenuAdditionalParamsGrub2 config "vt.default_red=0xFF,0xBC,0x4F,0xB4,0x56,0xBC,0x4F,0x00,0xA1,0xCF,0x84,0xCA,0x8D,0xB4,0x84,0x68 vt.default_grn=0xFF,0x55,0xBA,0xBA,0x4D,0x4D,0xB3,0x00,0xA0,0x8F,0xB3,0xCA,0x88,0x93,0xA4,0x68 vt.default_blu=0xFF,0x58,0x5F,0x58,0xC5,0xBD,0xC5,0x00,0xA8,0xBB,0xAB,0x97,0xBD,0xC7,0xC5,0x68"}
+      }
+
+      # Serial access is a must!
+      submenu "" {return}
+      submenu "Serial console=ttyS0,115200n8" --class serial {
+        ${grubMenuCfg}
+        ${buildMenuAdditionalParamsGrub2 config "console=ttyS0,115200n8"}
+      }
+    }
+
+    ${lib.optionalString (refindBinary != null) ''
+    # GRUB apparently cannot do "chainloader" operations on "CD".
+    if [ "\$root" != "cd0" ]; then
+      # Force root to be the FAT partition
+      # Otherwise it breaks rEFInd's boot
+      search --set=root --no-floppy --fs-uuid 1234-5678
+      menuentry 'rEFInd' --class refind {
+        chainloader (\$root)/EFI/boot/${refindBinary}
+      }
+    fi
+    ''}
+    menuentry 'Firmware Setup' --class settings {
+      fwsetup
+      clear
+      echo ""
+      echo "If you see this message, your EFI system doesn't support this feature."
+      echo ""
+    }
+    menuentry 'Shutdown' --class shutdown {
+      halt
+    }
+    EOF
+
+    ${refind}
+  '';
+
+  efiImg = pkgs.runCommand "efi-image_eltorito" {
+    nativeBuildInputs = [ pkgs.buildPackages.mtools pkgs.buildPackages.libfaketime pkgs.buildPackages.dosfstools ];
+    strictDeps = true;
+  }
+    # Be careful about determinism: du --apparent-size,
+    #   dates (cp -p, touch, mcopy -m, faketime for label), IDs (mkfs.vfat -i)
+    ''
+      mkdir ./contents && cd ./contents
+      cp -rp "${efiDir}"/EFI .
+      mkdir ./boot
+      cp -p "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}" \
+        "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}" ./boot/
+
+      # Rewrite dates for everything in the FS
+      find . -exec touch --date=2000-01-01 {} +
+
+      # Round up to the nearest multiple of 1MB, for more deterministic du output
+      usage_size=$(( $(du -s --block-size=1M --apparent-size . | tr -cd '[:digit:]') * 1024 * 1024 ))
+      # Make the image 110% as big as the files need to make up for FAT overhead
+      image_size=$(( ($usage_size * 110) / 100 ))
+      # Make the image fit blocks of 1M
+      block_size=$((1024*1024))
+      image_size=$(( ($image_size / $block_size + 1) * $block_size ))
+      echo "Usage size: $usage_size"
+      echo "Image size: $image_size"
+      truncate --size=$image_size "$out"
+      faketime "2000-01-01 00:00:00" mkfs.vfat -i 12345678 -n EFIBOOT "$out"
+
+      # Force a fixed order in mcopy for better determinism, and avoid file globbing
+      for d in $(find EFI boot -type d | sort); do
+        faketime "2000-01-01 00:00:00" mmd -i "$out" "::/$d"
+      done
+
+      for f in $(find EFI boot -type f | sort); do
+        mcopy -pvm -i "$out" "$f" "::/$f"
+      done
+
+      # Verify the FAT partition.
+      fsck.vfat -vn "$out"
+    ''; # */
+
+  # Name used by UEFI for architectures.
+  targetArch =
+    if pkgs.stdenv.isi686 || config.boot.loader.grub.forcei686 then
+      "ia32"
+    else if pkgs.stdenv.isx86_64 then
+      "x64"
+    else if pkgs.stdenv.isAarch32 then
+      "arm"
+    else if pkgs.stdenv.isAarch64 then
+      "aa64"
+    else
+      throw "Unsupported architecture";
+
+  # Syslinux (and isolinux) only supports x86-based architectures.
+  canx86BiosBoot = pkgs.stdenv.hostPlatform.isx86;
+
+in
+
+{
+  options = {
+
+    isoImage.isoName = mkOption {
+      default = "${config.isoImage.isoBaseName}.iso";
+      description = ''
+        Name of the generated ISO image file.
+      '';
+    };
+
+    isoImage.isoBaseName = mkOption {
+      default = "nixos";
+      description = ''
+        Prefix of the name of the generated ISO image file.
+      '';
+    };
+
+    isoImage.compressImage = mkOption {
+      default = false;
+      description = ''
+        Whether the ISO image should be compressed using
+        <command>zstd</command>.
+      '';
+    };
+
+    isoImage.squashfsCompression = mkOption {
+      default = with pkgs.stdenv.targetPlatform; "xz -Xdict-size 100% "
+                + lib.optionalString (isx86_32 || isx86_64) "-Xbcj x86"
+                # Untested but should also reduce size for these platforms
+                + lib.optionalString (isAarch32 || isAarch64) "-Xbcj arm"
+                + lib.optionalString (isPowerPC) "-Xbcj powerpc"
+                + lib.optionalString (isSparc) "-Xbcj sparc";
+      description = ''
+        Compression settings to use for the squashfs nix store.
+      '';
+      example = "zstd -Xcompression-level 6";
+    };
+
+    isoImage.edition = mkOption {
+      default = "";
+      description = ''
+        Specifies which edition string to use in the volume ID of the generated
+        ISO image.
+      '';
+    };
+
+    isoImage.volumeID = mkOption {
+      # nixos-$EDITION-$RELEASE-$ARCH
+      default = "nixos${optionalString (config.isoImage.edition != "") "-${config.isoImage.edition}"}-${config.system.nixos.release}-${pkgs.stdenv.hostPlatform.uname.processor}";
+      description = ''
+        Specifies the label or volume ID of the generated ISO image.
+        Note that the label is used by stage 1 of the boot process to
+        mount the CD, so it should be reasonably distinctive.
+      '';
+    };
+
+    isoImage.contents = mkOption {
+      example = literalExpression ''
+        [ { source = pkgs.memtest86 + "/memtest.bin";
+            target = "boot/memtest.bin";
+          }
+        ]
+      '';
+      description = ''
+        This option lists files to be copied to fixed locations in the
+        generated ISO image.
+      '';
+    };
+
+    isoImage.storeContents = mkOption {
+      example = literalExpression "[ pkgs.stdenv ]";
+      description = ''
+        This option lists additional derivations to be included in the
+        Nix store in the generated ISO image.
+      '';
+    };
+
+    isoImage.includeSystemBuildDependencies = mkOption {
+      default = false;
+      description = ''
+        Set this option to include all the needed sources etc in the
+        image. It significantly increases image size. Use that when
+        you want to be able to keep all the sources needed to build your
+        system or when you are going to install the system on a computer
+        with slow or non-existent network connection.
+      '';
+    };
+
+    isoImage.makeEfiBootable = mkOption {
+      default = false;
+      description = ''
+        Whether the ISO image should be an efi-bootable volume.
+      '';
+    };
+
+    isoImage.makeUsbBootable = mkOption {
+      default = false;
+      description = ''
+        Whether the ISO image should be bootable from CD as well as USB.
+      '';
+    };
+
+    isoImage.efiSplashImage = mkOption {
+      default = pkgs.fetchurl {
+          url = "https://raw.githubusercontent.com/NixOS/nixos-artwork/a9e05d7deb38a8e005a2b52575a3f59a63a4dba0/bootloader/efi-background.png";
+          sha256 = "18lfwmp8yq923322nlb9gxrh5qikj1wsk6g5qvdh31c4h5b1538x";
+        };
+      description = ''
+        The splash image to use in the EFI bootloader.
+      '';
+    };
+
+    isoImage.splashImage = mkOption {
+      default = pkgs.fetchurl {
+          url = "https://raw.githubusercontent.com/NixOS/nixos-artwork/a9e05d7deb38a8e005a2b52575a3f59a63a4dba0/bootloader/isolinux/bios-boot.png";
+          sha256 = "1wp822zrhbg4fgfbwkr7cbkr4labx477209agzc0hr6k62fr6rxd";
+        };
+      description = ''
+        The splash image to use in the legacy-boot bootloader.
+      '';
+    };
+
+    isoImage.grubTheme = mkOption {
+      default = pkgs.nixos-grub2-theme;
+      type = types.nullOr (types.either types.path types.package);
+      description = ''
+        The grub2 theme used for UEFI boot.
+      '';
+    };
+
+    isoImage.appendToMenuLabel = mkOption {
+      default = " Installer";
+      example = " Live System";
+      description = ''
+        The string to append after the menu label for the NixOS system.
+        This will be directly appended (without whitespace) to the NixOS version
+        string, like for example if it is set to <literal>XXX</literal>:
+
+        <para><literal>NixOS 99.99-pre666XXX</literal></para>
+      '';
+    };
+
+  };
+
+  # store them in lib so we can mkImageMediaOverride the
+  # entire file system layout in installation media (only)
+  config.lib.isoFileSystems = {
+    "/" = mkImageMediaOverride
+      {
+        fsType = "tmpfs";
+        options = [ "mode=0755" ];
+      };
+
+    # Note that /dev/root is a symlink to the actual root device
+    # specified on the kernel command line, created in the stage 1
+    # init script.
+    "/iso" = mkImageMediaOverride
+      { device = "/dev/root";
+        neededForBoot = true;
+        noCheck = true;
+      };
+
+    # In stage 1, mount a tmpfs on top of /nix/store (the squashfs
+    # image) to make this a live CD.
+    "/nix/.ro-store" = mkImageMediaOverride
+      { fsType = "squashfs";
+        device = "/iso/nix-store.squashfs";
+        options = [ "loop" ];
+        neededForBoot = true;
+      };
+
+    "/nix/.rw-store" = mkImageMediaOverride
+      { fsType = "tmpfs";
+        options = [ "mode=0755" ];
+        neededForBoot = true;
+      };
+
+    "/nix/store" = mkImageMediaOverride
+      { fsType = "overlay";
+        device = "overlay";
+        options = [
+          "lowerdir=/nix/.ro-store"
+          "upperdir=/nix/.rw-store/store"
+          "workdir=/nix/.rw-store/work"
+        ];
+        depends = [
+          "/nix/.ro-store"
+          "/nix/.rw-store/store"
+          "/nix/.rw-store/work"
+        ];
+      };
+  };
+
+  config = {
+    assertions = [
+      {
+        assertion = !(stringLength config.isoImage.volumeID > 32);
+        # https://wiki.osdev.org/ISO_9660#The_Primary_Volume_Descriptor
+        # Volume Identifier can only be 32 bytes
+        message = let
+          length = stringLength config.isoImage.volumeID;
+          howmany = toString length;
+          toomany = toString (length - 32);
+        in
+        "isoImage.volumeID ${config.isoImage.volumeID} is ${howmany} characters. That is ${toomany} characters longer than the limit of 32.";
+      }
+    ];
+
+    boot.loader.grub.version = 2;
+
+    # Don't build the GRUB menu builder script, since we don't need it
+    # here and it causes a cyclic dependency.
+    boot.loader.grub.enable = false;
+
+    environment.systemPackages =  [ grubPkgs.grub2 grubPkgs.grub2_efi ]
+      ++ optional canx86BiosBoot pkgs.syslinux
+    ;
+
+    # In stage 1 of the boot, mount the CD as the root FS by label so
+    # that we don't need to know its device.  We pass the label of the
+    # root filesystem on the kernel command line, rather than in
+    # `fileSystems' below.  This allows CD-to-USB converters such as
+    # UNetbootin to rewrite the kernel command line to pass the label or
+    # UUID of the USB stick.  It would be nicer to write
+    # `root=/dev/disk/by-label/...' here, but UNetbootin doesn't
+    # recognise that.
+    boot.kernelParams =
+      [ "root=LABEL=${config.isoImage.volumeID}"
+        "boot.shell_on_fail"
+      ];
+
+    fileSystems = config.lib.isoFileSystems;
+
+    boot.initrd.availableKernelModules = [ "squashfs" "iso9660" "uas" "overlay" ];
+
+    boot.initrd.kernelModules = [ "loop" "overlay" ];
+
+    # Closures to be copied to the Nix store on the CD, namely the init
+    # script and the top-level system configuration directory.
+    isoImage.storeContents =
+      [ config.system.build.toplevel ] ++
+      optional config.isoImage.includeSystemBuildDependencies
+        config.system.build.toplevel.drvPath;
+
+    # Create the squashfs image that contains the Nix store.
+    system.build.squashfsStore = pkgs.callPackage ../../../lib/make-squashfs.nix {
+      storeContents = config.isoImage.storeContents;
+      comp = config.isoImage.squashfsCompression;
+    };
+
+    # Individual files to be included on the CD, outside of the Nix
+    # store on the CD.
+    isoImage.contents =
+      [
+        { source = config.boot.kernelPackages.kernel + "/" + config.system.boot.loader.kernelFile;
+          target = "/boot/" + config.system.boot.loader.kernelFile;
+        }
+        { source = config.system.build.initialRamdisk + "/" + config.system.boot.loader.initrdFile;
+          target = "/boot/" + config.system.boot.loader.initrdFile;
+        }
+        { source = config.system.build.squashfsStore;
+          target = "/nix-store.squashfs";
+        }
+        { source = pkgs.writeText "version" config.system.nixos.label;
+          target = "/version.txt";
+        }
+      ] ++ optionals canx86BiosBoot [
+        { source = config.isoImage.splashImage;
+          target = "/isolinux/background.png";
+        }
+        { source = pkgs.substituteAll  {
+            name = "isolinux.cfg";
+            src = pkgs.writeText "isolinux.cfg-in" isolinuxCfg;
+            bootRoot = "/boot";
+          };
+          target = "/isolinux/isolinux.cfg";
+        }
+        { source = "${pkgs.syslinux}/share/syslinux";
+          target = "/isolinux";
+        }
+      ] ++ optionals config.isoImage.makeEfiBootable [
+        { source = efiImg;
+          target = "/boot/efi.img";
+        }
+        { source = "${efiDir}/EFI";
+          target = "/EFI";
+        }
+        { source = (pkgs.writeTextDir "grub/loopback.cfg" "source /EFI/boot/grub.cfg") + "/grub";
+          target = "/boot/grub";
+        }
+        { source = config.isoImage.efiSplashImage;
+          target = "/EFI/boot/efi-background.png";
+        }
+      ] ++ optionals (config.boot.loader.grub.memtest86.enable && canx86BiosBoot) [
+        { source = "${pkgs.memtest86plus}/memtest.bin";
+          target = "/boot/memtest.bin";
+        }
+      ] ++ optionals (config.isoImage.grubTheme != null) [
+        { source = config.isoImage.grubTheme;
+          target = "/EFI/boot/grub-theme";
+        }
+      ];
+
+    boot.loader.timeout = 10;
+
+    # Create the ISO image.
+    system.build.isoImage = pkgs.callPackage ../../../lib/make-iso9660-image.nix ({
+      inherit (config.isoImage) isoName compressImage volumeID contents;
+      bootable = canx86BiosBoot;
+      bootImage = "/isolinux/isolinux.bin";
+      syslinux = if canx86BiosBoot then pkgs.syslinux else null;
+    } // optionalAttrs (config.isoImage.makeUsbBootable && canx86BiosBoot) {
+      usbBootable = true;
+      isohybridMbrImage = "${pkgs.syslinux}/share/syslinux/isohdpfx.bin";
+    } // optionalAttrs config.isoImage.makeEfiBootable {
+      efiBootable = true;
+      efiBootImage = "boot/efi.img";
+    });
+
+    boot.postBootCommands =
+      ''
+        # After booting, register the contents of the Nix store on the
+        # CD in the Nix database in the tmpfs.
+        ${config.nix.package.out}/bin/nix-store --load-db < /nix/store/nix-path-registration
+
+        # nixos-rebuild also requires a "system" profile and an
+        # /etc/NIXOS tag.
+        touch /etc/NIXOS
+        ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
+      '';
+
+    # Add vfat support to the initrd to enable people to copy the
+    # contents of the CD to a bootable USB stick.
+    boot.initrd.supportedFilesystems = [ "vfat" ];
+
+  };
+
+}
diff --git a/nixos/modules/installer/cd-dvd/sd-image-aarch64-new-kernel.nix b/nixos/modules/installer/cd-dvd/sd-image-aarch64-new-kernel.nix
new file mode 100644
index 00000000000..a669d61571f
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/sd-image-aarch64-new-kernel.nix
@@ -0,0 +1,14 @@
+{ config, ... }:
+{
+  imports = [
+    ../sd-card/sd-image-aarch64-new-kernel-installer.nix
+  ];
+  config = {
+    warnings = [
+      ''
+      .../cd-dvd/sd-image-aarch64-new-kernel.nix is deprecated and will eventually be removed.
+      Please switch to .../sd-card/sd-image-aarch64-new-kernel-installer.nix, instead.
+      ''
+    ];
+  };
+}
diff --git a/nixos/modules/installer/cd-dvd/sd-image-aarch64.nix b/nixos/modules/installer/cd-dvd/sd-image-aarch64.nix
new file mode 100644
index 00000000000..76c1509b8f7
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/sd-image-aarch64.nix
@@ -0,0 +1,14 @@
+{ config, ... }:
+{
+  imports = [
+    ../sd-card/sd-image-aarch64-installer.nix
+  ];
+  config = {
+    warnings = [
+      ''
+      .../cd-dvd/sd-image-aarch64.nix is deprecated and will eventually be removed.
+      Please switch to .../sd-card/sd-image-aarch64-installer.nix, instead.
+      ''
+    ];
+  };
+}
diff --git a/nixos/modules/installer/cd-dvd/sd-image-armv7l-multiplatform.nix b/nixos/modules/installer/cd-dvd/sd-image-armv7l-multiplatform.nix
new file mode 100644
index 00000000000..6ee0eb9e9b8
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/sd-image-armv7l-multiplatform.nix
@@ -0,0 +1,14 @@
+{ config, ... }:
+{
+  imports = [
+    ../sd-card/sd-image-armv7l-multiplatform-installer.nix
+  ];
+  config = {
+    warnings = [
+      ''
+      .../cd-dvd/sd-image-armv7l-multiplatform.nix is deprecated and will eventually be removed.
+      Please switch to .../sd-card/sd-image-armv7l-multiplatform-installer.nix, instead.
+      ''
+    ];
+  };
+}
diff --git a/nixos/modules/installer/cd-dvd/sd-image-raspberrypi.nix b/nixos/modules/installer/cd-dvd/sd-image-raspberrypi.nix
new file mode 100644
index 00000000000..747440ba9c6
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/sd-image-raspberrypi.nix
@@ -0,0 +1,14 @@
+{ config, ... }:
+{
+  imports = [
+    ../sd-card/sd-image-raspberrypi-installer.nix
+  ];
+  config = {
+    warnings = [
+      ''
+      .../cd-dvd/sd-image-raspberrypi.nix is deprecated and will eventually be removed.
+      Please switch to .../sd-card/sd-image-raspberrypi-installer.nix, instead.
+      ''
+    ];
+  };
+}
diff --git a/nixos/modules/installer/cd-dvd/sd-image.nix b/nixos/modules/installer/cd-dvd/sd-image.nix
new file mode 100644
index 00000000000..e2d6dcb3fe3
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/sd-image.nix
@@ -0,0 +1,14 @@
+{ config, ... }:
+{
+  imports = [
+    ../sd-card/sd-image.nix
+  ];
+  config = {
+    warnings = [
+      ''
+      .../cd-dvd/sd-image.nix is deprecated and will eventually be removed.
+      Please switch to .../sd-card/sd-image.nix, instead.
+      ''
+    ];
+  };
+}
diff --git a/nixos/modules/installer/cd-dvd/system-tarball-fuloong2f.nix b/nixos/modules/installer/cd-dvd/system-tarball-fuloong2f.nix
new file mode 100644
index 00000000000..054c8c74a76
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/system-tarball-fuloong2f.nix
@@ -0,0 +1,160 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  # A dummy /etc/nixos/configuration.nix in the booted CD that
+  # rebuilds the CD's configuration (and allows the configuration to
+  # be modified, of course, providing a true live CD).  Problem is
+  # that we don't really know how the CD was built - the Nix
+  # expression language doesn't allow us to query the expression being
+  # evaluated.  So we'll just hope for the best.
+  dummyConfiguration = pkgs.writeText "configuration.nix"
+    ''
+      { config, pkgs, ... }:
+
+      { # Add your own options below, e.g.:
+        #   services.openssh.enable = true;
+        nixpkgs.config.platform = pkgs.platforms.fuloong2f_n32;
+      }
+    '';
+
+
+  pkgs2storeContents = l : map (x: { object = x; symlink = "none"; }) l;
+
+  # A clue for the kernel loading
+  kernelParams = pkgs.writeText "kernel-params.txt" ''
+    Kernel Parameters:
+      init=/boot/init ${toString config.boot.kernelParams}
+  '';
+
+  # System wide nixpkgs config
+  nixpkgsUserConfig = pkgs.writeText "config.nix" ''
+    pkgs:
+    {
+      platform = pkgs.platforms.fuloong2f_n32;
+    }
+  '';
+
+in
+
+{
+  imports = [ ./system-tarball.nix ];
+
+  # Disable some other stuff we don't need.
+  security.sudo.enable = false;
+
+  # Include only the en_US locale.  This saves 75 MiB or so compared to
+  # the full glibcLocales package.
+  i18n.supportedLocales = ["en_US.UTF-8/UTF-8" "en_US/ISO-8859-1"];
+
+  # Include some utilities that are useful for installing or repairing
+  # the system.
+  environment.systemPackages =
+    [ pkgs.w3m # needed for the manual anyway
+      pkgs.testdisk # useful for repairing boot problems
+      pkgs.ms-sys # for writing Microsoft boot sectors / MBRs
+      pkgs.parted
+      pkgs.ddrescue
+      pkgs.ccrypt
+      pkgs.cryptsetup # needed for dm-crypt volumes
+
+      # Some networking tools.
+      pkgs.sshfs-fuse
+      pkgs.socat
+      pkgs.screen
+      pkgs.wpa_supplicant # !!! should use the wpa module
+
+      # Hardware-related tools.
+      pkgs.sdparm
+      pkgs.hdparm
+      pkgs.dmraid
+
+      # Tools to create / manipulate filesystems.
+      pkgs.ntfsprogs # for resizing NTFS partitions
+      pkgs.btrfs-progs
+      pkgs.jfsutils
+
+      # Some compression/archiver tools.
+      pkgs.unzip
+      pkgs.zip
+      pkgs.xz
+      pkgs.dar # disk archiver
+
+      # Some editors.
+      pkgs.nvi
+      pkgs.bvi # binary editor
+      pkgs.joe
+    ];
+
+  # The initrd has to contain any module that might be necessary for
+  # mounting the CD/DVD.
+  boot.initrd.availableKernelModules =
+    [ "vfat" "reiserfs" ];
+
+  boot.kernelPackages = pkgs.linuxKernel.packages.linux_3_10;
+  boot.kernelParams = [ "console=tty1" ];
+
+  boot.postBootCommands =
+    ''
+      mkdir -p /mnt
+
+      cp ${dummyConfiguration} /etc/nixos/configuration.nix
+    '';
+
+  # Some more help text.
+  services.getty.helpLine =
+    ''
+
+      Log in as "root" with an empty password.  ${
+        if config.services.xserver.enable then
+          "Type `start xserver' to start\nthe graphical user interface."
+        else ""
+      }
+    '';
+
+  # Include the firmware for various wireless cards.
+  networking.enableRalinkFirmware = true;
+  networking.enableIntel2200BGFirmware = true;
+
+  # To speed up further installation of packages, include the complete stdenv
+  # in the Nix store of the tarball.
+  tarball.storeContents = pkgs2storeContents [ pkgs.stdenv ]
+    ++ [
+      {
+        object = config.system.build.bootStage2;
+        symlink = "/boot/init";
+      }
+      {
+        object = config.system.build.toplevel;
+        symlink = "/boot/system";
+      }
+    ];
+
+  tarball.contents = [
+    { source = kernelParams;
+      target = "/kernelparams.txt";
+    }
+    { source = config.boot.kernelPackages.kernel + "/" + config.system.boot.loader.kernelFile;
+      target = "/boot/" + config.system.boot.loader.kernelFile;
+    }
+    { source = nixpkgsUserConfig;
+      target = "/root/.nixpkgs/config.nix";
+    }
+  ];
+
+  # Allow sshd to be started manually through "start sshd".  It should
+  # not be started by default on the installation CD because the
+  # default root password is empty.
+  services.openssh.enable = true;
+  systemd.services.openssh.wantedBy = lib.mkOverride 50 [];
+
+  boot.loader.grub.enable = false;
+  boot.loader.generationsDir.enable = false;
+  system.boot.loader.kernelFile = "vmlinux";
+
+  nixpkgs.config = {
+    platform = pkgs.platforms.fuloong2f_n32;
+  };
+}
diff --git a/nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt b/nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt
new file mode 100644
index 00000000000..887bf60d0fb
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt
@@ -0,0 +1,89 @@
+Let all the files in the system tarball sit in a directory served by NFS (the
+NFS root) like this in exportfs:
+  /home/pcroot    192.168.1.0/24(rw,no_root_squash,no_all_squash)
+
+Run "exportfs -a" after editing /etc/exportfs, for the nfs server to be aware
+of the changes.
+
+Use a tftp server serving the root of boot/ (from the system tarball).
+
+In order to have PXE boot, use the boot/dhcpd.conf-example file for your dhcpd
+server, as it will point your PXE clients to pxelinux.0 from the tftp server.
+Adapt the configuration to your network.
+
+Adapt the pxelinux configuration (boot/pxelinux.cfg/default) to set the path to
+your nfrroot. If you use ip=dhcp in the kernel, the nfs server ip will be taken
+from dhcp and so you don't have to specify it.
+
+The linux in bzImage includes network drivers for some usual cards.
+
+
+QEMU Testing
+---------------
+
+You can test qemu pxe boot without having a DHCP server adapted, but having
+nfsroot, like this:
+  qemu-system-x86_64 -tftp /home/pcroot/boot -net nic -net user,bootfile=pxelinux.0 -boot n
+
+I don't know how to use NFS through the qemu '-net user' though.
+
+
+QEMU Testing with NFS root and bridged network
+-------------------------------------------------
+
+This allows testing with qemu as any other host in your LAN.
+
+Testing with the real dhcpd server requires setting up a bridge and having a
+tap device.
+  tunctl -t tap0
+  brctl addbr br0
+  brctl addif br0 eth0
+  brctl addif tap0 eth0
+  ifconfig eth0 0.0.0.0 up
+  ifconfig tap0 0.0.0.0 up
+  ifconfig br0 up # With your ip configuration
+
+Then you can run qemu:
+  qemu-system-x86_64 -boot n -net tap,ifname=tap0,script=no -net nic,model=e1000
+
+
+Using the system-tarball-pc in a chroot
+--------------------------------------------------
+
+Installation:
+  mkdir nixos-chroot && cd nixos-chroot
+  tar xf your-system-tarball.tar.xz
+  mkdir sys dev proc tmp root var run
+  mount --bind /sys sys
+  mount --bind /dev dev
+  mount --bind /proc proc
+
+Activate the system: look for a directory in nix/store similar to:
+    "/nix/store/y0d1lcj9fppli0hl3x0m0ba5g1ndjv2j-nixos-feb97bx-53f008"
+Having found it, activate that nixos system *twice*:
+  chroot . /nix/store/SOMETHING-nixos-SOMETHING/activate
+  chroot . /nix/store/SOMETHING-nixos-SOMETHING/activate
+
+This runs a 'hostname' command. Restore your old hostname with:
+  hostname OLDHOSTNAME
+
+Copy your system resolv.conf to the /etc/resolv.conf inside the chroot:
+  cp /etc/resolv.conf etc
+
+Then you can get an interactive shell in the nixos chroot. '*' means
+to run inside the chroot interactive shell
+  chroot . /bin/sh
+*  source /etc/profile
+
+Populate the nix database: that should be done in the init script if you
+had booted this nixos. Run:
+*  `grep local-cmds run/current-system/init`
+
+Then you can proceed normally subscribing to a nixos channel:
+  nix-channel --add https://nixos.org/channels/nixos-unstable
+  nix-channel --update
+
+Testing:
+  nix-env -i hello
+  which hello
+  hello
diff --git a/nixos/modules/installer/cd-dvd/system-tarball-pc.nix b/nixos/modules/installer/cd-dvd/system-tarball-pc.nix
new file mode 100644
index 00000000000..674fb6c8a33
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/system-tarball-pc.nix
@@ -0,0 +1,163 @@
+# This module contains the basic configuration for building a NixOS
+# tarball, that can directly boot, maybe using PXE or unpacking on a fs.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  pkgs2storeContents = l : map (x: { object = x; symlink = "none"; }) l;
+
+  # For PXE kernel loading
+  pxeconfig = pkgs.writeText "pxeconfig-default" ''
+    default menu.c32
+    prompt 0
+
+    label bootlocal
+      menu default
+      localboot 0
+      timeout 80
+      TOTALTIMEOUT 9000
+
+    label nixos
+      MENU LABEL ^NixOS using nfsroot
+      KERNEL bzImage
+      append ip=dhcp nfsroot=/home/pcroot init=${config.system.build.toplevel}/init rw
+
+    # I don't know how to make this boot with nfsroot (using the initrd)
+    label nixos_initrd
+      MENU LABEL NixOS booting the poor ^initrd.
+      KERNEL bzImage
+      append initrd=initrd ip=dhcp nfsroot=/home/pcroot init=${config.system.build.toplevel}/init rw
+
+    label memtest
+      MENU LABEL ^${pkgs.memtest86.name}
+      KERNEL memtest
+  '';
+
+  dhcpdExampleConfig = pkgs.writeText "dhcpd.conf-example" ''
+    # Example configuration for booting PXE.
+    allow booting;
+    allow bootp;
+
+    # Adapt this to your network configuration.
+    option domain-name "local";
+    option subnet-mask 255.255.255.0;
+    option broadcast-address 192.168.1.255;
+    option domain-name-servers 192.168.1.1;
+    option routers 192.168.1.1;
+
+    # PXE-specific configuration directives...
+    # Some BIOS don't accept slashes for paths inside the tftp servers,
+    # and will report Access Violation if they see slashes.
+    filename "pxelinux.0";
+    # For the TFTP and NFS root server. Set the IP of your server.
+    next-server 192.168.1.34;
+
+    subnet 192.168.1.0 netmask 255.255.255.0 {
+      range 192.168.1.50 192.168.1.55;
+    }
+  '';
+
+  readme = ./system-tarball-pc-readme.txt;
+
+in
+
+{
+  imports =
+    [ ./system-tarball.nix
+
+      # Profiles of this basic installation.
+      ../../profiles/all-hardware.nix
+      ../../profiles/base.nix
+      ../../profiles/installation-device.nix
+    ];
+
+  # To speed up further installation of packages, include the complete stdenv
+  # in the Nix store of the tarball.
+  tarball.storeContents = pkgs2storeContents [ pkgs.stdenv ];
+
+  tarball.contents =
+    [ { source = config.boot.kernelPackages.kernel + "/" + config.system.boot.loader.kernelFile;
+        target = "/boot/" + config.system.boot.loader.kernelFile;
+      }
+      { source = "${pkgs.syslinux}/share/syslinux/pxelinux.0";
+        target = "/boot/pxelinux.0";
+      }
+      { source = "${pkgs.syslinux}/share/syslinux/menu.c32";
+        target = "/boot/menu.c32";
+      }
+      { source = pxeconfig;
+        target = "/boot/pxelinux.cfg/default";
+      }
+      { source = readme;
+        target = "/readme.txt";
+      }
+      { source = dhcpdExampleConfig;
+        target = "/boot/dhcpd.conf-example";
+      }
+      { source = "${pkgs.memtest86}/memtest.bin";
+        # We can't leave '.bin', because pxelinux interprets this specially,
+        # and it would not load the image fine.
+        # http://forum.canardpc.com/threads/46464-0104-when-launched-via-pxe
+        target = "/boot/memtest";
+      }
+    ];
+
+  # Allow sshd to be started manually through "start sshd".  It should
+  # not be started by default on the installation CD because the
+  # default root password is empty.
+  services.openssh.enable = true;
+  systemd.services.openssh.wantedBy = lib.mkOverride 50 [];
+
+  # To be able to use the systemTarball to catch troubles.
+  boot.crashDump = {
+    enable = true;
+    kernelPackages = pkgs.linuxKernel.packages.linux_3_4;
+  };
+
+  # No grub for the tarball.
+  boot.loader.grub.enable = false;
+
+  /* fake entry, just to have a happy stage-1. Users
+     may boot without having stage-1 though */
+  fileSystems.fake =
+    { mountPoint = "/";
+      device = "/dev/something";
+    };
+
+  nixpkgs.config = {
+    packageOverrides = p: {
+      linux_3_4 = p.linux_3_4.override {
+        extraConfig = ''
+          # Enable drivers in kernel for most NICs.
+          E1000 y
+          # E1000E y
+          # ATH5K y
+          8139TOO y
+          NE2K_PCI y
+          ATL1 y
+          ATL1E y
+          ATL1C y
+          VORTEX y
+          VIA_RHINE y
+          R8169 y
+
+          # Enable nfs root boot
+          UNIX y # http://www.linux-mips.org/archives/linux-mips/2006-11/msg00113.html
+          IP_PNP y
+          IP_PNP_DHCP y
+          FSCACHE y
+          NFS_FS y
+          NFS_FSCACHE y
+          ROOT_NFS y
+
+          # Enable devtmpfs
+          DEVTMPFS y
+          DEVTMPFS_MOUNT y
+        '';
+      };
+    };
+  };
+}
diff --git a/nixos/modules/installer/cd-dvd/system-tarball-sheevaplug.nix b/nixos/modules/installer/cd-dvd/system-tarball-sheevaplug.nix
new file mode 100644
index 00000000000..458e313a3f7
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/system-tarball-sheevaplug.nix
@@ -0,0 +1,172 @@
+# This module contains the basic configuration for building a NixOS
+# tarball for the sheevaplug.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  # A dummy /etc/nixos/configuration.nix in the booted CD that
+  # rebuilds the CD's configuration (and allows the configuration to
+  # be modified, of course, providing a true live CD).  Problem is
+  # that we don't really know how the CD was built - the Nix
+  # expression language doesn't allow us to query the expression being
+  # evaluated.  So we'll just hope for the best.
+  dummyConfiguration = pkgs.writeText "configuration.nix"
+    ''
+      { config, pkgs, ... }:
+
+      {
+        # Add your own options below and run "nixos-rebuild switch".
+        # E.g.,
+        #   services.openssh.enable = true;
+      }
+    '';
+
+
+  pkgs2storeContents = l : map (x: { object = x; symlink = "none"; }) l;
+
+  # A clue for the kernel loading
+  kernelParams = pkgs.writeText "kernel-params.txt" ''
+    Kernel Parameters:
+      init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}
+  '';
+
+
+in
+
+{
+  imports = [ ./system-tarball.nix ];
+
+  # Disable some other stuff we don't need.
+  security.sudo.enable = false;
+
+  # Include only the en_US locale.  This saves 75 MiB or so compared to
+  # the full glibcLocales package.
+  i18n.supportedLocales = ["en_US.UTF-8/UTF-8" "en_US/ISO-8859-1"];
+
+  # Include some utilities that are useful for installing or repairing
+  # the system.
+  environment.systemPackages =
+    [ pkgs.w3m # needed for the manual anyway
+      pkgs.ddrescue
+      pkgs.ccrypt
+      pkgs.cryptsetup # needed for dm-crypt volumes
+
+      # Some networking tools.
+      pkgs.sshfs-fuse
+      pkgs.socat
+      pkgs.screen
+      pkgs.wpa_supplicant # !!! should use the wpa module
+
+      # Hardware-related tools.
+      pkgs.sdparm
+      pkgs.hdparm
+      pkgs.dmraid
+
+      # Tools to create / manipulate filesystems.
+      pkgs.btrfs-progs
+
+      # Some compression/archiver tools.
+      pkgs.unzip
+      pkgs.zip
+      pkgs.xz
+      pkgs.dar # disk archiver
+
+      # Some editors.
+      pkgs.nvi
+      pkgs.bvi # binary editor
+      pkgs.joe
+    ];
+
+  boot.loader.grub.enable = false;
+  boot.loader.generationsDir.enable = false;
+  system.boot.loader.kernelFile = "uImage";
+
+  boot.initrd.availableKernelModules =
+    [ "mvsdio" "reiserfs" "ext3" "ums-cypress" "rtc_mv" "ext4" ];
+
+  boot.postBootCommands =
+    ''
+      mkdir -p /mnt
+
+      cp ${dummyConfiguration} /etc/nixos/configuration.nix
+    '';
+
+  boot.initrd.extraUtilsCommands =
+    ''
+      copy_bin_and_libs ${pkgs.util-linux}/sbin/hwclock
+    '';
+
+  boot.initrd.postDeviceCommands =
+    ''
+      hwclock -s
+    '';
+
+  boot.kernelParams =
+    [
+      "selinux=0"
+      "console=tty1"
+      # "console=ttyS0,115200n8"  # serial console
+    ];
+
+  boot.kernelPackages = pkgs.linuxKernel.packages.linux_3_4;
+
+  boot.supportedFilesystems = [ "reiserfs" ];
+
+  /* fake entry, just to have a happy stage-1. Users
+     may boot without having stage-1 though */
+  fileSystems.fake =
+    { mountPoint = "/";
+      device = "/dev/something";
+    };
+
+  services.getty = {
+    # Some more help text.
+    helpLine = ''
+      Log in as "root" with an empty password.  ${
+        if config.services.xserver.enable then
+          "Type `start xserver' to start\nthe graphical user interface."
+        else ""
+      }
+    '';
+  };
+
+  # Setting vesa, we don't get the nvidia driver, which can't work in arm.
+  services.xserver.videoDrivers = [ "vesa" ];
+
+  documentation.nixos.enable = false;
+
+  # Include the firmware for various wireless cards.
+  networking.enableRalinkFirmware = true;
+  networking.enableIntel2200BGFirmware = true;
+
+  # To speed up further installation of packages, include the complete stdenv
+  # in the Nix store of the tarball.
+  tarball.storeContents = pkgs2storeContents [ pkgs.stdenv ];
+  tarball.contents = [
+    { source = kernelParams;
+      target = "/kernelparams.txt";
+    }
+    { source = config.boot.kernelPackages.kernel + "/" + config.system.boot.loader.kernelFile;
+      target = "/boot/" + config.system.boot.loader.kernelFile;
+    }
+    { source = pkgs.ubootSheevaplug;
+      target = "/boot/uboot";
+    }
+  ];
+
+  # Allow sshd to be started manually through "start sshd".  It should
+  # not be started by default on the installation CD because the
+  # default root password is empty.
+  services.openssh.enable = true;
+  systemd.services.openssh.wantedBy = lib.mkOverride 50 [];
+
+  # cpufrequtils fails to build on non-pc
+  powerManagement.enable = false;
+
+  nixpkgs.config = {
+    platform = pkgs.platforms.sheevaplug;
+  };
+}
diff --git a/nixos/modules/installer/cd-dvd/system-tarball.nix b/nixos/modules/installer/cd-dvd/system-tarball.nix
new file mode 100644
index 00000000000..362c555cc53
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/system-tarball.nix
@@ -0,0 +1,93 @@
+# This module creates a bootable ISO image containing the given NixOS
+# configuration.  The derivation for the ISO image will be placed in
+# config.system.build.tarball.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  versionFile = pkgs.writeText "nixos-label" config.system.nixos.label;
+
+in
+
+{
+  options = {
+    tarball.contents = mkOption {
+      example = literalExpression ''
+        [ { source = pkgs.memtest86 + "/memtest.bin";
+            target = "boot/memtest.bin";
+          }
+        ]
+      '';
+      description = ''
+        This option lists files to be copied to fixed locations in the
+        generated ISO image.
+      '';
+    };
+
+    tarball.storeContents = mkOption {
+      example = literalExpression "[ pkgs.stdenv ]";
+      description = ''
+        This option lists additional derivations to be included in the
+        Nix store in the generated ISO image.
+      '';
+    };
+
+  };
+
+  config = {
+
+    # In stage 1 of the boot, mount the CD/DVD as the root FS by label
+    # so that we don't need to know its device.
+    fileSystems = { };
+
+    # boot.initrd.availableKernelModules = [ "mvsdio" "reiserfs" "ext3" "ext4" ];
+
+    # boot.initrd.kernelModules = [ "rtc_mv" ];
+
+    # Closures to be copied to the Nix store on the CD, namely the init
+    # script and the top-level system configuration directory.
+    tarball.storeContents =
+      [ { object = config.system.build.toplevel;
+          symlink = "/run/current-system";
+        }
+      ];
+
+    # Individual files to be included on the CD, outside of the Nix
+    # store on the CD.
+    tarball.contents =
+      [ { source = config.system.build.initialRamdisk + "/" + config.system.boot.loader.initrdFile;
+          target = "/boot/" + config.system.boot.loader.initrdFile;
+        }
+        { source = versionFile;
+          target = "/nixos-version.txt";
+        }
+      ];
+
+    # Create the tarball
+    system.build.tarball = import ../../../lib/make-system-tarball.nix {
+      inherit (pkgs) stdenv closureInfo pixz;
+
+      inherit (config.tarball) contents storeContents;
+    };
+
+    boot.postBootCommands =
+      ''
+        # After booting, register the contents of the Nix store on the
+        # CD in the Nix database in the tmpfs.
+        if [ -f /nix-path-registration ]; then
+          ${config.nix.package.out}/bin/nix-store --load-db < /nix-path-registration &&
+          rm /nix-path-registration
+        fi
+
+        # nixos-rebuild also requires a "system" profile and an
+        # /etc/NIXOS tag.
+        touch /etc/NIXOS
+        ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
+      '';
+
+  };
+
+}
diff --git a/nixos/modules/installer/netboot/netboot-base.nix b/nixos/modules/installer/netboot/netboot-base.nix
new file mode 100644
index 00000000000..7e66a49c739
--- /dev/null
+++ b/nixos/modules/installer/netboot/netboot-base.nix
@@ -0,0 +1,17 @@
+# This module contains the basic configuration for building netboot
+# images
+
+{ lib, ... }:
+
+with lib;
+
+{
+  imports =
+    [ ./netboot.nix
+
+      # Profiles of this basic netboot media
+      ../../profiles/all-hardware.nix
+      ../../profiles/base.nix
+      ../../profiles/installation-device.nix
+    ];
+}
diff --git a/nixos/modules/installer/netboot/netboot-minimal.nix b/nixos/modules/installer/netboot/netboot-minimal.nix
new file mode 100644
index 00000000000..1563501a7e0
--- /dev/null
+++ b/nixos/modules/installer/netboot/netboot-minimal.nix
@@ -0,0 +1,10 @@
+# This module defines a small netboot environment.
+
+{ ... }:
+
+{
+  imports =
+    [ ./netboot-base.nix
+      ../../profiles/minimal.nix
+    ];
+}
diff --git a/nixos/modules/installer/netboot/netboot.nix b/nixos/modules/installer/netboot/netboot.nix
new file mode 100644
index 00000000000..a459e7304cd
--- /dev/null
+++ b/nixos/modules/installer/netboot/netboot.nix
@@ -0,0 +1,120 @@
+# This module creates netboot media containing the given NixOS
+# configuration.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  options = {
+
+    netboot.storeContents = mkOption {
+      example = literalExpression "[ pkgs.stdenv ]";
+      description = ''
+        This option lists additional derivations to be included in the
+        Nix store in the generated netboot image.
+      '';
+    };
+
+  };
+
+  config = {
+    # Don't build the GRUB menu builder script, since we don't need it
+    # here and it causes a cyclic dependency.
+    boot.loader.grub.enable = false;
+
+    # !!! Hack - attributes expected by other modules.
+    environment.systemPackages = [ pkgs.grub2_efi ]
+      ++ (if pkgs.stdenv.hostPlatform.system == "aarch64-linux"
+          then []
+          else [ pkgs.grub2 pkgs.syslinux ]);
+
+    fileSystems."/" = mkImageMediaOverride
+      { fsType = "tmpfs";
+        options = [ "mode=0755" ];
+      };
+
+    # In stage 1, mount a tmpfs on top of /nix/store (the squashfs
+    # image) to make this a live CD.
+    fileSystems."/nix/.ro-store" = mkImageMediaOverride
+      { fsType = "squashfs";
+        device = "../nix-store.squashfs";
+        options = [ "loop" ];
+        neededForBoot = true;
+      };
+
+    fileSystems."/nix/.rw-store" = mkImageMediaOverride
+      { fsType = "tmpfs";
+        options = [ "mode=0755" ];
+        neededForBoot = true;
+      };
+
+    fileSystems."/nix/store" = mkImageMediaOverride
+      { fsType = "overlay";
+        device = "overlay";
+        options = [
+          "lowerdir=/nix/.ro-store"
+          "upperdir=/nix/.rw-store/store"
+          "workdir=/nix/.rw-store/work"
+        ];
+
+        depends = [
+          "/nix/.ro-store"
+          "/nix/.rw-store/store"
+          "/nix/.rw-store/work"
+        ];
+      };
+
+    boot.initrd.availableKernelModules = [ "squashfs" "overlay" ];
+
+    boot.initrd.kernelModules = [ "loop" "overlay" ];
+
+    # Closures to be copied to the Nix store, namely the init
+    # script and the top-level system configuration directory.
+    netboot.storeContents =
+      [ config.system.build.toplevel ];
+
+    # Create the squashfs image that contains the Nix store.
+    system.build.squashfsStore = pkgs.callPackage ../../../lib/make-squashfs.nix {
+      storeContents = config.netboot.storeContents;
+    };
+
+
+    # Create the initrd
+    system.build.netbootRamdisk = pkgs.makeInitrd {
+      inherit (config.boot.initrd) compressor;
+      prepend = [ "${config.system.build.initialRamdisk}/initrd" ];
+
+      contents =
+        [ { object = config.system.build.squashfsStore;
+            symlink = "/nix-store.squashfs";
+          }
+        ];
+    };
+
+    system.build.netbootIpxeScript = pkgs.writeTextDir "netboot.ipxe" ''
+      #!ipxe
+      # Use the cmdline variable to allow the user to specify custom kernel params
+      # when chainloading this script from other iPXE scripts like netboot.xyz
+      kernel ${pkgs.stdenv.hostPlatform.linux-kernel.target} init=${config.system.build.toplevel}/init initrd=initrd ${toString config.boot.kernelParams} ''${cmdline}
+      initrd initrd
+      boot
+    '';
+
+    boot.loader.timeout = 10;
+
+    boot.postBootCommands =
+      ''
+        # After booting, register the contents of the Nix store
+        # in the Nix database in the tmpfs.
+        ${config.nix.package}/bin/nix-store --load-db < /nix/store/nix-path-registration
+
+        # nixos-rebuild also requires a "system" profile and an
+        # /etc/NIXOS tag.
+        touch /etc/NIXOS
+        ${config.nix.package}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
+      '';
+
+  };
+
+}
diff --git a/nixos/modules/installer/scan/detected.nix b/nixos/modules/installer/scan/detected.nix
new file mode 100644
index 00000000000..5c5fba56f51
--- /dev/null
+++ b/nixos/modules/installer/scan/detected.nix
@@ -0,0 +1,12 @@
+# List all devices which are detected by nixos-generate-config.
+# Common devices are enabled by default.
+{ lib, ... }:
+
+with lib;
+
+{
+  config = mkDefault {
+    # Common firmware, i.e. for wifi cards
+    hardware.enableRedistributableFirmware = true;
+  };
+}
diff --git a/nixos/modules/installer/scan/not-detected.nix b/nixos/modules/installer/scan/not-detected.nix
new file mode 100644
index 00000000000..baa068c08db
--- /dev/null
+++ b/nixos/modules/installer/scan/not-detected.nix
@@ -0,0 +1,6 @@
+# Enables non-free firmware on devices not recognized by `nixos-generate-config`.
+{ lib, ... }:
+
+{
+  hardware.enableRedistributableFirmware = lib.mkDefault true;
+}
diff --git a/nixos/modules/installer/sd-card/sd-image-aarch64-installer.nix b/nixos/modules/installer/sd-card/sd-image-aarch64-installer.nix
new file mode 100644
index 00000000000..2a6b6abdf91
--- /dev/null
+++ b/nixos/modules/installer/sd-card/sd-image-aarch64-installer.nix
@@ -0,0 +1,10 @@
+{
+  imports = [
+    ../../profiles/installation-device.nix
+    ./sd-image-aarch64.nix
+  ];
+
+  # the installation media is also the installation target,
+  # so we don't want to provide the installation configuration.nix.
+  installer.cloneConfig = false;
+}
diff --git a/nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel-installer.nix b/nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel-installer.nix
new file mode 100644
index 00000000000..1b6b55ff291
--- /dev/null
+++ b/nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel-installer.nix
@@ -0,0 +1,10 @@
+{
+  imports = [
+    ../../profiles/installation-device.nix
+    ./sd-image-aarch64-new-kernel.nix
+  ];
+
+  # the installation media is also the installation target,
+  # so we don't want to provide the installation configuration.nix.
+  installer.cloneConfig = false;
+}
diff --git a/nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel.nix b/nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel.nix
new file mode 100644
index 00000000000..2882fbcc730
--- /dev/null
+++ b/nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel.nix
@@ -0,0 +1,7 @@
+{ pkgs, ... }:
+
+{
+  imports = [ ./sd-image-aarch64.nix ];
+
+  boot.kernelPackages = pkgs.linuxPackages_latest;
+}
diff --git a/nixos/modules/installer/sd-card/sd-image-aarch64.nix b/nixos/modules/installer/sd-card/sd-image-aarch64.nix
new file mode 100644
index 00000000000..321793882f4
--- /dev/null
+++ b/nixos/modules/installer/sd-card/sd-image-aarch64.nix
@@ -0,0 +1,74 @@
+# To build, use:
+# nix-build nixos -I nixos-config=nixos/modules/installer/sd-card/sd-image-aarch64.nix -A config.system.build.sdImage
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ../../profiles/base.nix
+    ./sd-image.nix
+  ];
+
+  boot.loader.grub.enable = false;
+  boot.loader.generic-extlinux-compatible.enable = true;
+
+  boot.consoleLogLevel = lib.mkDefault 7;
+
+  # The serial ports listed here are:
+  # - ttyS0: for Tegra (Jetson TX1)
+  # - ttyAMA0: for QEMU's -machine virt
+  boot.kernelParams = ["console=ttyS0,115200n8" "console=ttyAMA0,115200n8" "console=tty0"];
+
+  sdImage = {
+    populateFirmwareCommands = let
+      configTxt = pkgs.writeText "config.txt" ''
+        [pi3]
+        kernel=u-boot-rpi3.bin
+
+        [pi02]
+        kernel=u-boot-rpi3.bin
+
+        [pi4]
+        kernel=u-boot-rpi4.bin
+        enable_gic=1
+        armstub=armstub8-gic.bin
+
+        # Otherwise the resolution will be weird in most cases, compared to
+        # what the pi3 firmware does by default.
+        disable_overscan=1
+
+        # Supported in newer board revisions
+        arm_boost=1
+
+        [all]
+        # Boot in 64-bit mode.
+        arm_64bit=1
+
+        # U-Boot needs this to work, regardless of whether UART is actually used or not.
+        # Look in arch/arm/mach-bcm283x/Kconfig in the U-Boot tree to see if this is still
+        # a requirement in the future.
+        enable_uart=1
+
+        # Prevent the firmware from smashing the framebuffer setup done by the mainline kernel
+        # when attempting to show low-voltage or overtemperature warnings.
+        avoid_warnings=1
+      '';
+      in ''
+        (cd ${pkgs.raspberrypifw}/share/raspberrypi/boot && cp bootcode.bin fixup*.dat start*.elf $NIX_BUILD_TOP/firmware/)
+
+        # Add the config
+        cp ${configTxt} firmware/config.txt
+
+        # Add pi3 specific files
+        cp ${pkgs.ubootRaspberryPi3_64bit}/u-boot.bin firmware/u-boot-rpi3.bin
+
+        # Add pi4 specific files
+        cp ${pkgs.ubootRaspberryPi4_64bit}/u-boot.bin firmware/u-boot-rpi4.bin
+        cp ${pkgs.raspberrypi-armstubs}/armstub8-gic.bin firmware/armstub8-gic.bin
+        cp ${pkgs.raspberrypifw}/share/raspberrypi/boot/bcm2711-rpi-4-b.dtb firmware/
+      '';
+    populateRootCommands = ''
+      mkdir -p ./files/boot
+      ${config.boot.loader.generic-extlinux-compatible.populateCmd} -c ${config.system.build.toplevel} -d ./files/boot
+    '';
+  };
+}
diff --git a/nixos/modules/installer/sd-card/sd-image-armv7l-multiplatform-installer.nix b/nixos/modules/installer/sd-card/sd-image-armv7l-multiplatform-installer.nix
new file mode 100644
index 00000000000..fbe04377d50
--- /dev/null
+++ b/nixos/modules/installer/sd-card/sd-image-armv7l-multiplatform-installer.nix
@@ -0,0 +1,10 @@
+{
+  imports = [
+    ../../profiles/installation-device.nix
+    ./sd-image-armv7l-multiplatform.nix
+  ];
+
+  # the installation media is also the installation target,
+  # so we don't want to provide the installation configuration.nix.
+  installer.cloneConfig = false;
+}
diff --git a/nixos/modules/installer/sd-card/sd-image-armv7l-multiplatform.nix b/nixos/modules/installer/sd-card/sd-image-armv7l-multiplatform.nix
new file mode 100644
index 00000000000..23ed9285129
--- /dev/null
+++ b/nixos/modules/installer/sd-card/sd-image-armv7l-multiplatform.nix
@@ -0,0 +1,52 @@
+# To build, use:
+# nix-build nixos -I nixos-config=nixos/modules/installer/sd-card/sd-image-armv7l-multiplatform.nix -A config.system.build.sdImage
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ../../profiles/base.nix
+    ./sd-image.nix
+  ];
+
+  boot.loader.grub.enable = false;
+  boot.loader.generic-extlinux-compatible.enable = true;
+
+  boot.consoleLogLevel = lib.mkDefault 7;
+  boot.kernelPackages = pkgs.linuxPackages_latest;
+  # The serial ports listed here are:
+  # - ttyS0: for Tegra (Jetson TK1)
+  # - ttymxc0: for i.MX6 (Wandboard)
+  # - ttyAMA0: for Allwinner (pcDuino3 Nano) and QEMU's -machine virt
+  # - ttyO0: for OMAP (BeagleBone Black)
+  # - ttySAC2: for Exynos (ODROID-XU3)
+  boot.kernelParams = ["console=ttyS0,115200n8" "console=ttymxc0,115200n8" "console=ttyAMA0,115200n8" "console=ttyO0,115200n8" "console=ttySAC2,115200n8" "console=tty0"];
+
+  sdImage = {
+    populateFirmwareCommands = let
+      configTxt = pkgs.writeText "config.txt" ''
+        # Prevent the firmware from smashing the framebuffer setup done by the mainline kernel
+        # when attempting to show low-voltage or overtemperature warnings.
+        avoid_warnings=1
+
+        [pi2]
+        kernel=u-boot-rpi2.bin
+
+        [pi3]
+        kernel=u-boot-rpi3.bin
+
+        # U-Boot used to need this to work, regardless of whether UART is actually used or not.
+        # TODO: check when/if this can be removed.
+        enable_uart=1
+      '';
+      in ''
+        (cd ${pkgs.raspberrypifw}/share/raspberrypi/boot && cp bootcode.bin fixup*.dat start*.elf $NIX_BUILD_TOP/firmware/)
+        cp ${pkgs.ubootRaspberryPi2}/u-boot.bin firmware/u-boot-rpi2.bin
+        cp ${pkgs.ubootRaspberryPi3_32bit}/u-boot.bin firmware/u-boot-rpi3.bin
+        cp ${configTxt} firmware/config.txt
+      '';
+    populateRootCommands = ''
+      mkdir -p ./files/boot
+      ${config.boot.loader.generic-extlinux-compatible.populateCmd} -c ${config.system.build.toplevel} -d ./files/boot
+    '';
+  };
+}
diff --git a/nixos/modules/installer/sd-card/sd-image-raspberrypi-installer.nix b/nixos/modules/installer/sd-card/sd-image-raspberrypi-installer.nix
new file mode 100644
index 00000000000..72ec7485b52
--- /dev/null
+++ b/nixos/modules/installer/sd-card/sd-image-raspberrypi-installer.nix
@@ -0,0 +1,10 @@
+{
+  imports = [
+    ../../profiles/installation-device.nix
+    ./sd-image-raspberrypi.nix
+  ];
+
+  # the installation media is also the installation target,
+  # so we don't want to provide the installation configuration.nix.
+  installer.cloneConfig = false;
+}
diff --git a/nixos/modules/installer/sd-card/sd-image-raspberrypi.nix b/nixos/modules/installer/sd-card/sd-image-raspberrypi.nix
new file mode 100644
index 00000000000..103d6787a03
--- /dev/null
+++ b/nixos/modules/installer/sd-card/sd-image-raspberrypi.nix
@@ -0,0 +1,41 @@
+# To build, use:
+# nix-build nixos -I nixos-config=nixos/modules/installer/sd-card/sd-image-raspberrypi.nix -A config.system.build.sdImage
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ../../profiles/base.nix
+    ./sd-image.nix
+  ];
+
+  boot.loader.grub.enable = false;
+  boot.loader.generic-extlinux-compatible.enable = true;
+
+  boot.consoleLogLevel = lib.mkDefault 7;
+  boot.kernelPackages = pkgs.linuxKernel.packages.linux_rpi1;
+
+  sdImage = {
+    populateFirmwareCommands = let
+      configTxt = pkgs.writeText "config.txt" ''
+        # Prevent the firmware from smashing the framebuffer setup done by the mainline kernel
+        # when attempting to show low-voltage or overtemperature warnings.
+        avoid_warnings=1
+
+        [pi0]
+        kernel=u-boot-rpi0.bin
+
+        [pi1]
+        kernel=u-boot-rpi1.bin
+      '';
+      in ''
+        (cd ${pkgs.raspberrypifw}/share/raspberrypi/boot && cp bootcode.bin fixup*.dat start*.elf $NIX_BUILD_TOP/firmware/)
+        cp ${pkgs.ubootRaspberryPiZero}/u-boot.bin firmware/u-boot-rpi0.bin
+        cp ${pkgs.ubootRaspberryPi}/u-boot.bin firmware/u-boot-rpi1.bin
+        cp ${configTxt} firmware/config.txt
+      '';
+    populateRootCommands = ''
+      mkdir -p ./files/boot
+      ${config.boot.loader.generic-extlinux-compatible.populateCmd} -c ${config.system.build.toplevel} -d ./files/boot
+    '';
+  };
+}
diff --git a/nixos/modules/installer/sd-card/sd-image-riscv64-qemu-installer.nix b/nixos/modules/installer/sd-card/sd-image-riscv64-qemu-installer.nix
new file mode 100644
index 00000000000..90c1b8413ad
--- /dev/null
+++ b/nixos/modules/installer/sd-card/sd-image-riscv64-qemu-installer.nix
@@ -0,0 +1,10 @@
+{
+  imports = [
+    ../../profiles/installation-device.nix
+    ./sd-image-riscv64-qemu.nix
+  ];
+
+  # the installation media is also the installation target,
+  # so we don't want to provide the installation configuration.nix.
+  installer.cloneConfig = false;
+}
diff --git a/nixos/modules/installer/sd-card/sd-image-riscv64-qemu.nix b/nixos/modules/installer/sd-card/sd-image-riscv64-qemu.nix
new file mode 100644
index 00000000000..a3e30768da4
--- /dev/null
+++ b/nixos/modules/installer/sd-card/sd-image-riscv64-qemu.nix
@@ -0,0 +1,32 @@
+# To build, use:
+# nix-build nixos -I nixos-config=nixos/modules/installer/sd-card/sd-image-riscv64-qemu.nix -A config.system.build.sdImage
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ../../profiles/base.nix
+    ./sd-image.nix
+  ];
+
+  boot.loader = {
+    grub.enable = false;
+    generic-extlinux-compatible = {
+      enable = true;
+
+      # Don't even specify FDTDIR - We do not have the correct DT
+      # The DTB is generated by QEMU at runtime
+      useGenerationDeviceTree = false;
+    };
+  };
+
+  boot.consoleLogLevel = lib.mkDefault 7;
+  boot.kernelParams = [ "console=tty0" "console=ttyS0,115200n8" ];
+
+  sdImage = {
+    populateFirmwareCommands = "";
+    populateRootCommands = ''
+      mkdir -p ./files/boot
+      ${config.boot.loader.generic-extlinux-compatible.populateCmd} -c ${config.system.build.toplevel} -d ./files/boot
+    '';
+  };
+}
diff --git a/nixos/modules/installer/sd-card/sd-image-x86_64.nix b/nixos/modules/installer/sd-card/sd-image-x86_64.nix
new file mode 100644
index 00000000000..b44c0a4eeca
--- /dev/null
+++ b/nixos/modules/installer/sd-card/sd-image-x86_64.nix
@@ -0,0 +1,27 @@
+# To build, use:
+# nix-build nixos -I nixos-config=nixos/modules/installer/sd-card/sd-image-x86_64.nix -A config.system.build.sdImage
+
+# This image is primarily used in NixOS tests (boot.nix) to test `boot.loader.generic-extlinux-compatible`.
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ../../profiles/base.nix
+    ./sd-image.nix
+  ];
+
+  boot.loader = {
+    grub.enable = false;
+    generic-extlinux-compatible.enable = true;
+  };
+
+  boot.consoleLogLevel = lib.mkDefault 7;
+
+  sdImage = {
+    populateFirmwareCommands = "";
+    populateRootCommands = ''
+      mkdir -p ./files/boot
+      ${config.boot.loader.generic-extlinux-compatible.populateCmd} -c ${config.system.build.toplevel} -d ./files/boot
+    '';
+  };
+}
diff --git a/nixos/modules/installer/sd-card/sd-image.nix b/nixos/modules/installer/sd-card/sd-image.nix
new file mode 100644
index 00000000000..7560c682517
--- /dev/null
+++ b/nixos/modules/installer/sd-card/sd-image.nix
@@ -0,0 +1,269 @@
+# This module creates a bootable SD card image containing the given NixOS
+# configuration. The generated image is MBR partitioned, with a FAT
+# /boot/firmware partition, and ext4 root partition. The generated image
+# is sized to fit its contents, and a boot script automatically resizes
+# the root partition to fit the device on the first boot.
+#
+# The firmware partition is built with expectation to hold the Raspberry
+# Pi firmware and bootloader, and be removed and replaced with a firmware
+# build for the target SoC for other board families.
+#
+# The derivation for the SD image will be placed in
+# config.system.build.sdImage
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  rootfsImage = pkgs.callPackage ../../../lib/make-ext4-fs.nix ({
+    inherit (config.sdImage) storePaths;
+    compressImage = true;
+    populateImageCommands = config.sdImage.populateRootCommands;
+    volumeLabel = "NIXOS_SD";
+  } // optionalAttrs (config.sdImage.rootPartitionUUID != null) {
+    uuid = config.sdImage.rootPartitionUUID;
+  });
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "sdImage" "bootPartitionID" ] "The FAT partition for SD image now only holds the Raspberry Pi firmware files. Use firmwarePartitionID to configure that partition's ID.")
+    (mkRemovedOptionModule [ "sdImage" "bootSize" ] "The boot files for SD image have been moved to the main ext4 partition. The FAT partition now only holds the Raspberry Pi firmware files. Changing its size may not be required.")
+    ../../profiles/all-hardware.nix
+  ];
+
+  options.sdImage = {
+    imageName = mkOption {
+      default = "${config.sdImage.imageBaseName}-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.img";
+      description = ''
+        Name of the generated image file.
+      '';
+    };
+
+    imageBaseName = mkOption {
+      default = "nixos-sd-image";
+      description = ''
+        Prefix of the name of the generated image file.
+      '';
+    };
+
+    storePaths = mkOption {
+      type = with types; listOf package;
+      example = literalExpression "[ pkgs.stdenv ]";
+      description = ''
+        Derivations to be included in the Nix store in the generated SD image.
+      '';
+    };
+
+    firmwarePartitionOffset = mkOption {
+      type = types.int;
+      default = 8;
+      description = ''
+        Gap in front of the /boot/firmware partition, in mebibytes (1024×1024
+        bytes).
+        Can be increased to make more space for boards requiring to dd u-boot
+        SPL before actual partitions.
+
+        Unless you are building your own images pre-configured with an
+        installed U-Boot, you can instead opt to delete the existing `FIRMWARE`
+        partition, which is used **only** for the Raspberry Pi family of
+        hardware.
+      '';
+    };
+
+    firmwarePartitionID = mkOption {
+      type = types.str;
+      default = "0x2178694e";
+      description = ''
+        Volume ID for the /boot/firmware partition on the SD card. This value
+        must be a 32-bit hexadecimal number.
+      '';
+    };
+
+    firmwarePartitionName = mkOption {
+      type = types.str;
+      default = "FIRMWARE";
+      description = ''
+        Name of the filesystem which holds the boot firmware.
+      '';
+    };
+
+    rootPartitionUUID = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "14e19a7b-0ae0-484d-9d54-43bd6fdc20c7";
+      description = ''
+        UUID for the filesystem on the main NixOS partition on the SD card.
+      '';
+    };
+
+    firmwareSize = mkOption {
+      type = types.int;
+      # As of 2019-08-18 the Raspberry pi firmware + u-boot takes ~18MiB
+      default = 30;
+      description = ''
+        Size of the /boot/firmware partition, in megabytes.
+      '';
+    };
+
+    populateFirmwareCommands = mkOption {
+      example = literalExpression "'' cp \${pkgs.myBootLoader}/u-boot.bin firmware/ ''";
+      description = ''
+        Shell commands to populate the ./firmware directory.
+        All files in that directory are copied to the
+        /boot/firmware partition on the SD image.
+      '';
+    };
+
+    populateRootCommands = mkOption {
+      example = literalExpression "''\${config.boot.loader.generic-extlinux-compatible.populateCmd} -c \${config.system.build.toplevel} -d ./files/boot''";
+      description = ''
+        Shell commands to populate the ./files directory.
+        All files in that directory are copied to the
+        root (/) partition on the SD image. Use this to
+        populate the ./files/boot (/boot) directory.
+      '';
+    };
+
+    postBuildCommands = mkOption {
+      example = literalExpression "'' dd if=\${pkgs.myBootLoader}/SPL of=$img bs=1024 seek=1 conv=notrunc ''";
+      default = "";
+      description = ''
+        Shell commands to run after the image is built.
+        Can be used for boards requiring to dd u-boot SPL before actual partitions.
+      '';
+    };
+
+    compressImage = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether the SD image should be compressed using
+        <command>zstd</command>.
+      '';
+    };
+
+    expandOnBoot = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to configure the sd image to expand it's partition on boot.
+      '';
+    };
+  };
+
+  config = {
+    fileSystems = {
+      "/boot/firmware" = {
+        device = "/dev/disk/by-label/${config.sdImage.firmwarePartitionName}";
+        fsType = "vfat";
+        # Alternatively, this could be removed from the configuration.
+        # The filesystem is not needed at runtime, it could be treated
+        # as an opaque blob instead of a discrete FAT32 filesystem.
+        options = [ "nofail" "noauto" ];
+      };
+      "/" = {
+        device = "/dev/disk/by-label/NIXOS_SD";
+        fsType = "ext4";
+      };
+    };
+
+    sdImage.storePaths = [ config.system.build.toplevel ];
+
+    system.build.sdImage = pkgs.callPackage ({ stdenv, dosfstools, e2fsprogs,
+    mtools, libfaketime, util-linux, zstd }: stdenv.mkDerivation {
+      name = config.sdImage.imageName;
+
+      nativeBuildInputs = [ dosfstools e2fsprogs mtools libfaketime util-linux zstd ];
+
+      inherit (config.sdImage) imageName compressImage;
+
+      buildCommand = ''
+        mkdir -p $out/nix-support $out/sd-image
+        export img=$out/sd-image/${config.sdImage.imageName}
+
+        echo "${pkgs.stdenv.buildPlatform.system}" > $out/nix-support/system
+        if test -n "$compressImage"; then
+          echo "file sd-image $img.zst" >> $out/nix-support/hydra-build-products
+        else
+          echo "file sd-image $img" >> $out/nix-support/hydra-build-products
+        fi
+
+        echo "Decompressing rootfs image"
+        zstd -d --no-progress "${rootfsImage}" -o ./root-fs.img
+
+        # Gap in front of the first partition, in MiB
+        gap=${toString config.sdImage.firmwarePartitionOffset}
+
+        # Create the image file sized to fit /boot/firmware and /, plus slack for the gap.
+        rootSizeBlocks=$(du -B 512 --apparent-size ./root-fs.img | awk '{ print $1 }')
+        firmwareSizeBlocks=$((${toString config.sdImage.firmwareSize} * 1024 * 1024 / 512))
+        imageSize=$((rootSizeBlocks * 512 + firmwareSizeBlocks * 512 + gap * 1024 * 1024))
+        truncate -s $imageSize $img
+
+        # type=b is 'W95 FAT32', type=83 is 'Linux'.
+        # The "bootable" partition is where u-boot will look file for the bootloader
+        # information (dtbs, extlinux.conf file).
+        sfdisk $img <<EOF
+            label: dos
+            label-id: ${config.sdImage.firmwarePartitionID}
+
+            start=''${gap}M, size=$firmwareSizeBlocks, type=b
+            start=$((gap + ${toString config.sdImage.firmwareSize}))M, type=83, bootable
+        EOF
+
+        # Copy the rootfs into the SD image
+        eval $(partx $img -o START,SECTORS --nr 2 --pairs)
+        dd conv=notrunc if=./root-fs.img of=$img seek=$START count=$SECTORS
+
+        # Create a FAT32 /boot/firmware partition of suitable size into firmware_part.img
+        eval $(partx $img -o START,SECTORS --nr 1 --pairs)
+        truncate -s $((SECTORS * 512)) firmware_part.img
+        faketime "1970-01-01 00:00:00" mkfs.vfat -i ${config.sdImage.firmwarePartitionID} -n ${config.sdImage.firmwarePartitionName} firmware_part.img
+
+        # Populate the files intended for /boot/firmware
+        mkdir firmware
+        ${config.sdImage.populateFirmwareCommands}
+
+        # Copy the populated /boot/firmware into the SD image
+        (cd firmware; mcopy -psvm -i ../firmware_part.img ./* ::)
+        # Verify the FAT partition before copying it.
+        fsck.vfat -vn firmware_part.img
+        dd conv=notrunc if=firmware_part.img of=$img seek=$START count=$SECTORS
+
+        ${config.sdImage.postBuildCommands}
+
+        if test -n "$compressImage"; then
+            zstd -T$NIX_BUILD_CORES --rm $img
+        fi
+      '';
+    }) {};
+
+    boot.postBootCommands = lib.mkIf config.sdImage.expandOnBoot ''
+      # On the first boot do some maintenance tasks
+      if [ -f /nix-path-registration ]; then
+        set -euo pipefail
+        set -x
+        # Figure out device names for the boot device and root filesystem.
+        rootPart=$(${pkgs.util-linux}/bin/findmnt -n -o SOURCE /)
+        bootDevice=$(lsblk -npo PKNAME $rootPart)
+        partNum=$(lsblk -npo MAJ:MIN $rootPart | ${pkgs.gawk}/bin/awk -F: '{print $2}')
+
+        # Resize the root partition and the filesystem to fit the disk
+        echo ",+," | sfdisk -N$partNum --no-reread $bootDevice
+        ${pkgs.parted}/bin/partprobe
+        ${pkgs.e2fsprogs}/bin/resize2fs $rootPart
+
+        # Register the contents of the initial Nix store
+        ${config.nix.package.out}/bin/nix-store --load-db < /nix-path-registration
+
+        # nixos-rebuild also requires a "system" profile and an /etc/NIXOS tag.
+        touch /etc/NIXOS
+        ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
+
+        # Prevents this from running on later boots.
+        rm -f /nix-path-registration
+      fi
+    '';
+  };
+}
diff --git a/nixos/modules/installer/tools/get-version-suffix b/nixos/modules/installer/tools/get-version-suffix
new file mode 100644
index 00000000000..b8972cd57d2
--- /dev/null
+++ b/nixos/modules/installer/tools/get-version-suffix
@@ -0,0 +1,22 @@
+getVersion() {
+    local dir="$1"
+    rev=
+    if [ -e "$dir/.git" ]; then
+        if [ -z "$(type -P git)" ]; then
+            echo "warning: Git not found; cannot figure out revision of $dir" >&2
+            return
+        fi
+        cd "$dir"
+        rev=$(git rev-parse --short HEAD)
+        if git describe --always --dirty | grep -q dirty; then
+            rev+=M
+        fi
+    fi
+}
+
+if nixpkgs=$(nix-instantiate --find-file nixpkgs "$@"); then
+    getVersion $nixpkgs
+    if [ -n "$rev" ]; then
+        echo ".git.$rev"
+    fi
+fi
diff --git a/nixos/modules/installer/tools/nix-fallback-paths.nix b/nixos/modules/installer/tools/nix-fallback-paths.nix
new file mode 100644
index 00000000000..dfafda77cb5
--- /dev/null
+++ b/nixos/modules/installer/tools/nix-fallback-paths.nix
@@ -0,0 +1,7 @@
+{
+  x86_64-linux = "/nix/store/0n2wfvi1i3fg97cjc54wslvk0804y0sn-nix-2.7.0";
+  i686-linux = "/nix/store/4p27c1k9z99pli6x8cxfph20yfyzn9nh-nix-2.7.0";
+  aarch64-linux = "/nix/store/r9yr8ijsb0gi9r7y92y3yzyld59yp0kj-nix-2.7.0";
+  x86_64-darwin = "/nix/store/hyfj5imsd0c4amlcjpf8l6w4q2draaj3-nix-2.7.0";
+  aarch64-darwin = "/nix/store/9l96qllhbb6xrsjaai76dn74ap7rq92n-nix-2.7.0";
+}
diff --git a/nixos/modules/installer/tools/nixos-build-vms/build-vms.nix b/nixos/modules/installer/tools/nixos-build-vms/build-vms.nix
new file mode 100644
index 00000000000..b4a94f62ad9
--- /dev/null
+++ b/nixos/modules/installer/tools/nixos-build-vms/build-vms.nix
@@ -0,0 +1,31 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, networkExpr
+}:
+
+let
+  nodes = builtins.mapAttrs (vm: module: {
+    _file = "${networkExpr}@node-${vm}";
+    imports = [ module ];
+  }) (import networkExpr);
+
+  pkgs = import ../../../../.. { inherit system config; };
+
+  testing = import ../../../../lib/testing-python.nix {
+    inherit system pkgs;
+  };
+
+  interactiveDriver = (testing.makeTest { inherit nodes; testScript = "start_all(); join_all();"; }).driverInteractive;
+in
+
+
+pkgs.runCommand "nixos-build-vms" { nativeBuildInputs = [ pkgs.makeWrapper ]; } ''
+  mkdir -p $out/bin
+  ln -s ${interactiveDriver}/bin/nixos-test-driver $out/bin/nixos-test-driver
+  ln -s ${interactiveDriver}/bin/nixos-test-driver $out/bin/nixos-run-vms
+  wrapProgram $out/bin/nixos-test-driver \
+    --add-flags "--interactive"
+  wrapProgram $out/bin/nixos-run-vms \
+     --set testScript "${pkgs.writeText "start-all" "start_all(); join_all();"}" \
+     --add-flags "--no-interactive"
+''
diff --git a/nixos/modules/installer/tools/nixos-build-vms/nixos-build-vms.sh b/nixos/modules/installer/tools/nixos-build-vms/nixos-build-vms.sh
new file mode 100644
index 00000000000..490ede04e6b
--- /dev/null
+++ b/nixos/modules/installer/tools/nixos-build-vms/nixos-build-vms.sh
@@ -0,0 +1,53 @@
+#! @runtimeShell@ -e
+# shellcheck shell=bash
+
+# Shows the usage of this command to the user
+
+showUsage() {
+    exec man nixos-build-vms
+    exit 1
+}
+
+# Parse valid argument options
+
+nixBuildArgs=()
+networkExpr=
+
+while [ $# -gt 0 ]; do
+    case "$1" in
+      --no-out-link)
+        nixBuildArgs+=("--no-out-link")
+        ;;
+      --show-trace)
+        nixBuildArgs+=("--show-trace")
+        ;;
+      -h|--help)
+        showUsage
+        exit 0
+        ;;
+      --option)
+        shift
+        nixBuildArgs+=("--option" "$1" "$2"); shift
+        ;;
+      *)
+        if [ -n "$networkExpr" ]; then
+          echo "Network expression already set!"
+          showUsage
+          exit 1
+        fi
+        networkExpr="$(readlink -f "$1")"
+        ;;
+    esac
+
+    shift
+done
+
+if [ -z "$networkExpr" ]
+then
+    echo "ERROR: A network expression must be specified!" >&2
+    exit 1
+fi
+
+# Build a network of VMs
+nix-build '<nixpkgs/nixos/modules/installer/tools/nixos-build-vms/build-vms.nix>' \
+    --argstr networkExpr "$networkExpr" "${nixBuildArgs[@]}"
diff --git a/nixos/modules/installer/tools/nixos-enter.sh b/nixos/modules/installer/tools/nixos-enter.sh
new file mode 100644
index 00000000000..89beeee7cf9
--- /dev/null
+++ b/nixos/modules/installer/tools/nixos-enter.sh
@@ -0,0 +1,109 @@
+#! @runtimeShell@
+# shellcheck shell=bash
+
+set -e
+
+# Re-exec ourselves in a private mount namespace so that our bind
+# mounts get cleaned up automatically.
+if [ -z "$NIXOS_ENTER_REEXEC" ]; then
+    export NIXOS_ENTER_REEXEC=1
+    if [ "$(id -u)" != 0 ]; then
+        extraFlags="-r"
+    fi
+    exec unshare --fork --mount --uts --mount-proc --pid $extraFlags -- "$0" "$@"
+else
+    mount --make-rprivate /
+fi
+
+mountPoint=/mnt
+system=/nix/var/nix/profiles/system
+command=("$system/sw/bin/bash" "--login")
+silent=0
+
+while [ "$#" -gt 0 ]; do
+    i="$1"; shift 1
+    case "$i" in
+        --root)
+            mountPoint="$1"; shift 1
+            ;;
+        --system)
+            system="$1"; shift 1
+            ;;
+        --help)
+            exec man nixos-enter
+            exit 1
+            ;;
+        --command|-c)
+            command=("$system/sw/bin/bash" "-c" "$1")
+            shift 1
+            ;;
+        --silent)
+            silent=1
+            ;;
+        --)
+            command=("$@")
+            break
+            ;;
+        *)
+            echo "$0: unknown option \`$i'"
+            exit 1
+            ;;
+    esac
+done
+
+if [[ ! -e $mountPoint/etc/NIXOS ]]; then
+    echo "$0: '$mountPoint' is not a NixOS installation" >&2
+    exit 126
+fi
+
+mkdir -p "$mountPoint/dev" "$mountPoint/sys"
+chmod 0755 "$mountPoint/dev" "$mountPoint/sys"
+mount --rbind /dev "$mountPoint/dev"
+mount --rbind /sys "$mountPoint/sys"
+
+# modified from https://github.com/archlinux/arch-install-scripts/blob/bb04ab435a5a89cd5e5ee821783477bc80db797f/arch-chroot.in#L26-L52
+chroot_add_resolv_conf() {
+    local chrootDir="$1" resolvConf="$1/etc/resolv.conf"
+
+    [[ -e /etc/resolv.conf ]] || return 0
+
+    # Handle resolv.conf as a symlink to somewhere else.
+    if [[ -L "$resolvConf" ]]; then
+      # readlink(1) should always give us *something* since we know at this point
+      # it's a symlink. For simplicity, ignore the case of nested symlinks.
+      # We also ignore the possibility of `../`s escaping the root.
+      resolvConf="$(readlink "$resolvConf")"
+      if [[ "$resolvConf" = /* ]]; then
+        resolvConf="$chrootDir$resolvConf"
+      else
+        resolvConf="$chrootDir/etc/$resolvConf"
+      fi
+    fi
+
+    # ensure file exists to bind mount over
+    if [[ ! -f "$resolvConf" ]]; then
+      install -Dm644 /dev/null "$resolvConf" || return 1
+    fi
+
+    mount --bind /etc/resolv.conf "$resolvConf"
+}
+
+chroot_add_resolv_conf "$mountPoint" || echo "$0: failed to set up resolv.conf" >&2
+
+(
+    # If silent, write both stdout and stderr of activation script to /dev/null
+    # otherwise, write both streams to stderr of this process
+    if [ "$silent" -eq 1 ]; then
+        exec 2>/dev/null
+    fi
+
+    # 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
+)
+
+unset TMPDIR
+
+exec chroot "$mountPoint" "${command[@]}"
diff --git a/nixos/modules/installer/tools/nixos-generate-config.pl b/nixos/modules/installer/tools/nixos-generate-config.pl
new file mode 100644
index 00000000000..57aef50a0f6
--- /dev/null
+++ b/nixos/modules/installer/tools/nixos-generate-config.pl
@@ -0,0 +1,675 @@
+#! @perl@
+
+use strict;
+use Cwd 'abs_path';
+use File::Spec;
+use File::Path;
+use File::Basename;
+use File::Slurp;
+use File::stat;
+
+umask(0022);
+
+sub uniq {
+    my %seen;
+    my @res = ();
+    foreach my $s (@_) {
+        if (!defined $seen{$s}) {
+            $seen{$s} = 1;
+            push @res, $s;
+        }
+    }
+    return @res;
+}
+
+sub runCommand {
+    my ($cmd) = @_;
+    open FILE, "$cmd 2>&1 |" or die "Failed to execute: $cmd\n";
+    my @ret = <FILE>;
+    close FILE;
+    return ($?, @ret);
+}
+
+# Process the command line.
+my $outDir = "/etc/nixos";
+my $rootDir = ""; # = /
+my $force = 0;
+my $noFilesystems = 0;
+my $showHardwareConfig = 0;
+
+for (my $n = 0; $n < scalar @ARGV; $n++) {
+    my $arg = $ARGV[$n];
+    if ($arg eq "--help") {
+        exec "man nixos-generate-config" or die;
+    }
+    elsif ($arg eq "--dir") {
+        $n++;
+        $outDir = $ARGV[$n];
+        die "$0: ‘--dir’ requires an argument\n" unless defined $outDir;
+    }
+    elsif ($arg eq "--root") {
+        $n++;
+        $rootDir = $ARGV[$n];
+        die "$0: ‘--root’ requires an argument\n" unless defined $rootDir;
+        $rootDir =~ s/\/*$//; # remove trailing slashes
+    }
+    elsif ($arg eq "--force") {
+        $force = 1;
+    }
+    elsif ($arg eq "--no-filesystems") {
+        $noFilesystems = 1;
+    }
+    elsif ($arg eq "--show-hardware-config") {
+        $showHardwareConfig = 1;
+    }
+    else {
+        die "$0: unrecognized argument ‘$arg’\n";
+    }
+}
+
+
+my @attrs = ();
+my @kernelModules = ();
+my @initrdKernelModules = ();
+my @initrdAvailableKernelModules = ();
+my @modulePackages = ();
+my @imports;
+
+
+sub debug {
+    return unless defined $ENV{"DEBUG"};
+    print STDERR @_;
+}
+
+
+my $cpuinfo = read_file "/proc/cpuinfo";
+
+
+sub hasCPUFeature {
+    my $feature = shift;
+    return $cpuinfo =~ /^flags\s*:.* $feature( |$)/m;
+}
+
+
+sub cpuManufacturer {
+    my $id = shift;
+    return $cpuinfo =~ /^vendor_id\s*:.* $id$/m;
+}
+
+
+# Determine CPU governor to use
+if (-e "/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors") {
+    my $governors = read_file("/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors");
+    # ondemand governor is not available on sandy bridge or later Intel CPUs
+    my @desired_governors = ("ondemand", "powersave");
+    my $e;
+
+    foreach $e (@desired_governors) {
+        if (index($governors, $e) != -1) {
+            last if (push @attrs, "powerManagement.cpuFreqGovernor = lib.mkDefault \"$e\";");
+        }
+    }
+}
+
+
+# Virtualization support?
+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.
+# However, some are needed in the initrd to boot the system.
+
+my $videoDriver;
+
+sub pciCheck {
+    my $path = shift;
+    my $vendor = read_file "$path/vendor"; chomp $vendor;
+    my $device = read_file "$path/device"; chomp $device;
+    my $class = read_file "$path/class"; chomp $class;
+
+    my $module;
+    if (-e "$path/driver/module") {
+        $module = basename `readlink -f $path/driver/module`;
+        chomp $module;
+    }
+
+    debug "$path: $vendor $device $class";
+    debug " $module" if defined $module;
+    debug "\n";
+
+    if (defined $module) {
+        # See the bottom of http://pciids.sourceforge.net/pci.ids for
+        # device classes.
+        if (# Mass-storage controller.  Definitely important.
+            $class =~ /^0x01/ ||
+
+            # Firewire controller.  A disk might be attached.
+            $class =~ /^0x0c00/ ||
+
+            # USB controller.  Needed if we want to use the
+            # keyboard when things go wrong in the initrd.
+            $class =~ /^0x0c03/
+            )
+        {
+            push @initrdAvailableKernelModules, $module;
+        }
+    }
+
+    # broadcom STA driver (wl.ko)
+    # list taken from http://www.broadcom.com/docs/linux_sta/README.txt
+    if ($vendor eq "0x14e4" &&
+        ($device eq "0x4311" || $device eq "0x4312" || $device eq "0x4313" ||
+         $device eq "0x4315" || $device eq "0x4327" || $device eq "0x4328" ||
+         $device eq "0x4329" || $device eq "0x432a" || $device eq "0x432b" ||
+         $device eq "0x432c" || $device eq "0x432d" || $device eq "0x4353" ||
+         $device eq "0x4357" || $device eq "0x4358" || $device eq "0x4359" ||
+         $device eq "0x4331" || $device eq "0x43a0" || $device eq "0x43b1"
+        ) )
+     {
+        push @modulePackages, "config.boot.kernelPackages.broadcom_sta";
+        push @kernelModules, "wl";
+     }
+
+    # broadcom FullMac driver
+    # list taken from
+    # https://wireless.wiki.kernel.org/en/users/Drivers/brcm80211#brcmfmac
+    if ($vendor eq "0x14e4" &&
+        ($device eq "0x43a3" || $device eq "0x43df" || $device eq "0x43ec" ||
+         $device eq "0x43d3" || $device eq "0x43d9" || $device eq "0x43e9" ||
+         $device eq "0x43ba" || $device eq "0x43bb" || $device eq "0x43bc" ||
+         $device eq "0xaa52" || $device eq "0x43ca" || $device eq "0x43cb" ||
+         $device eq "0x43cc" || $device eq "0x43c3" || $device eq "0x43c4" ||
+         $device eq "0x43c5"
+        ) )
+    {
+        # we need e.g. brcmfmac43602-pcie.bin
+        push @imports, "(modulesPath + \"/hardware/network/broadcom-43xx.nix\")";
+    }
+
+    # In case this is a virtio scsi device, we need to explicitly make this available.
+    if ($vendor eq "0x1af4" && $device eq "0x1004") {
+        push @initrdAvailableKernelModules, "virtio_scsi";
+    }
+
+    # Can't rely on $module here, since the module may not be loaded
+    # due to missing firmware.  Ideally we would check modules.pcimap
+    # here.
+    push @attrs, "networking.enableIntel2200BGFirmware = true;" if
+        $vendor eq "0x8086" &&
+        ($device eq "0x1043" || $device eq "0x104f" || $device eq "0x4220" ||
+         $device eq "0x4221" || $device eq "0x4223" || $device eq "0x4224");
+
+    push @attrs, "networking.enableIntel3945ABGFirmware = true;" if
+        $vendor eq "0x8086" &&
+        ($device eq "0x4229" || $device eq "0x4230" ||
+         $device eq "0x4222" || $device eq "0x4227");
+
+    # Assume that all NVIDIA cards are supported by the NVIDIA driver.
+    # There may be exceptions (e.g. old cards).
+    # FIXME: do we want to enable an unfree driver here?
+    #$videoDriver = "nvidia" if $vendor eq "0x10de" && $class =~ /^0x03/;
+}
+
+foreach my $path (glob "/sys/bus/pci/devices/*") {
+    pciCheck $path;
+}
+
+# Idem for USB devices.
+
+sub usbCheck {
+    my $path = shift;
+    my $class = read_file "$path/bInterfaceClass"; chomp $class;
+    my $subclass = read_file "$path/bInterfaceSubClass"; chomp $subclass;
+    my $protocol = read_file "$path/bInterfaceProtocol"; chomp $protocol;
+
+    my $module;
+    if (-e "$path/driver/module") {
+        $module = basename `readlink -f $path/driver/module`;
+        chomp $module;
+    }
+
+    debug "$path: $class $subclass $protocol";
+    debug " $module" if defined $module;
+    debug "\n";
+
+    if (defined $module) {
+        if (# Mass-storage controller.  Definitely important.
+            $class eq "08" ||
+
+            # Keyboard.  Needed if we want to use the
+            # keyboard when things go wrong in the initrd.
+            ($class eq "03" && $protocol eq "01")
+            )
+        {
+            push @initrdAvailableKernelModules, $module;
+        }
+    }
+}
+
+foreach my $path (glob "/sys/bus/usb/devices/*") {
+    if (-e "$path/bInterfaceClass") {
+        usbCheck $path;
+    }
+}
+
+
+# Add the modules for all block and MMC devices.
+foreach my $path (glob "/sys/class/{block,mmc_host}/*") {
+    my $module;
+    if (-e "$path/device/driver/module") {
+        $module = basename `readlink -f $path/device/driver/module`;
+        chomp $module;
+        push @initrdAvailableKernelModules, $module;
+    }
+}
+
+# Add bcache module, if needed.
+my @bcacheDevices = glob("/dev/bcache*");
+if (scalar @bcacheDevices > 0) {
+    push @initrdAvailableKernelModules, "bcache";
+}
+
+# Prevent unbootable systems if LVM snapshots are present at boot time.
+if (`lsblk -o TYPE` =~ "lvm") {
+    push @initrdKernelModules, "dm-snapshot";
+}
+
+my $virt = `@detectvirt@`;
+chomp $virt;
+
+
+# Check if we're a VirtualBox guest.  If so, enable the guest
+# additions.
+if ($virt eq "oracle") {
+    push @attrs, "virtualisation.virtualbox.guest.enable = true;"
+}
+
+
+# Likewise for QEMU.
+if ($virt eq "qemu" || $virt eq "kvm" || $virt eq "bochs") {
+    push @imports, "(modulesPath + \"/profiles/qemu-guest.nix\")";
+}
+
+# Also for Hyper-V.
+if ($virt eq "microsoft") {
+    push @attrs, "virtualisation.hypervGuest.enable = true;"
+}
+
+
+# Pull in NixOS configuration for containers.
+if ($virt eq "systemd-nspawn") {
+    push @attrs, "boot.isContainer = true;";
+}
+
+
+# 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";
+
+
+# For a device name like /dev/sda1, find a more stable path like
+# /dev/disk/by-uuid/X or /dev/disk/by-label/Y.
+sub findStableDevPath {
+    my ($dev) = @_;
+    return $dev if substr($dev, 0, 1) ne "/";
+    return $dev unless -e $dev;
+
+    my $st = stat($dev) or return $dev;
+
+    foreach my $dev2 (glob("/dev/disk/by-uuid/*"), glob("/dev/mapper/*"), glob("/dev/disk/by-label/*")) {
+        my $st2 = stat($dev2) or next;
+        return $dev2 if $st->rdev == $st2->rdev;
+    }
+
+    return $dev;
+}
+
+push @attrs, "services.xserver.videoDrivers = [ \"$videoDriver\" ];" if $videoDriver;
+
+# Generate the swapDevices option from the currently activated swap
+# devices.
+my @swaps = read_file("/proc/swaps", err_mode => 'carp');
+my @swapDevices;
+if (@swaps) {
+    shift @swaps;
+    foreach my $swap (@swaps) {
+        my @fields = split ' ', $swap;
+        my $swapFilename = $fields[0];
+        my $swapType = $fields[1];
+        next unless -e $swapFilename;
+        my $dev = findStableDevPath $swapFilename;
+        if ($swapType =~ "partition") {
+            # zram devices are more likely created by configuration.nix, so
+            # ignore them here
+            next if ($swapFilename =~ /^\/dev\/zram/);
+            push @swapDevices, "{ device = \"$dev\"; }";
+        } elsif ($swapType =~ "file") {
+            # swap *files* are more likely specified in configuration.nix, so
+            # ignore them here.
+        } else {
+            die "Unsupported swap type: $swapType\n";
+        }
+    }
+}
+
+
+# Generate the fileSystems option from the currently mounted
+# filesystems.
+sub in {
+    my ($d1, $d2) = @_;
+    return $d1 eq $d2 || substr($d1, 0, length($d2) + 1) eq "$d2/";
+}
+
+my $fileSystems;
+my %fsByDev;
+foreach my $fs (read_file("/proc/self/mountinfo")) {
+    chomp $fs;
+    my @fields = split / /, $fs;
+    my $mountPoint = $fields[4];
+    $mountPoint =~ s/\\040/ /g; # account for mount points with spaces in the name (\040 is the escape character)
+    $mountPoint =~ s/\\011/\t/g; # account for mount points with tabs in the name (\011 is the escape character)
+    next unless -d $mountPoint;
+    my @mountOptions = split /,/, $fields[5];
+
+    next if !in($mountPoint, $rootDir);
+    $mountPoint = substr($mountPoint, length($rootDir)); # strip the root directory (e.g. /mnt)
+    $mountPoint = "/" if $mountPoint eq "";
+
+    # Skip special filesystems.
+    next if in($mountPoint, "/proc") || in($mountPoint, "/dev") || in($mountPoint, "/sys") || in($mountPoint, "/run") || $mountPoint eq "/var/lib/nfs/rpc_pipefs";
+
+    # Skip the optional fields.
+    my $n = 6; $n++ while $fields[$n] ne "-"; $n++;
+    my $fsType = $fields[$n];
+    my $device = $fields[$n + 1];
+    my @superOptions = split /,/, $fields[$n + 2];
+    $device =~ s/\\040/ /g; # account for devices with spaces in the name (\040 is the escape character)
+    $device =~ s/\\011/\t/g; # account for mount points with tabs in the name (\011 is the escape character)
+
+    # Skip the read-only bind-mount on /nix/store.
+    next if $mountPoint eq "/nix/store" && (grep { $_ eq "rw" } @superOptions) && (grep { $_ eq "ro" } @mountOptions);
+
+    # Maybe this is a bind-mount of a filesystem we saw earlier?
+    if (defined $fsByDev{$fields[2]}) {
+        # Make sure this isn't a btrfs subvolume.
+        my $msg = `@btrfs@ subvol show $rootDir$mountPoint`;
+        if ($? != 0 || $msg =~ /ERROR:/s) {
+            my $path = $fields[3]; $path = "" if $path eq "/";
+            my $base = $fsByDev{$fields[2]};
+            $base = "" if $base eq "/";
+            $fileSystems .= <<EOF;
+  fileSystems.\"$mountPoint\" =
+    { device = \"$base$path\";
+      fsType = \"none\";
+      options = \[ \"bind\" \];
+    };
+
+EOF
+            next;
+        }
+    }
+    $fsByDev{$fields[2]} = $mountPoint;
+
+    # We don't know how to handle FUSE filesystems.
+    if ($fsType eq "fuseblk" || $fsType eq "fuse") {
+        print STDERR "warning: don't know how to emit ‘fileSystem’ option for FUSE filesystem ‘$mountPoint’\n";
+        next;
+    }
+
+    # Is this a mount of a loopback device?
+    my @extraOptions;
+    if ($device =~ /\/dev\/loop(\d+)/) {
+        my $loopnr = $1;
+        my $backer = read_file "/sys/block/loop$loopnr/loop/backing_file";
+        if (defined $backer) {
+            chomp $backer;
+            $device = $backer;
+            push @extraOptions, "loop";
+        }
+    }
+
+    # Is this a btrfs filesystem?
+    if ($fsType eq "btrfs") {
+        my ($status, @info) = runCommand("@btrfs@ subvol show $rootDir$mountPoint");
+        if ($status != 0 || join("", @info) =~ /ERROR:/) {
+            die "Failed to retrieve subvolume info for $mountPoint\n";
+        }
+        my @ids = join("\n", @info) =~ m/^(?!\/\n).*Subvolume ID:[ \t\n]*([0-9]+)/s;
+        if ($#ids > 0) {
+            die "Btrfs subvol name for $mountPoint listed multiple times in mount\n"
+        } elsif ($#ids == 0) {
+            my @paths = join("", @info) =~ m/^([^\n]*)/;
+            if ($#paths > 0) {
+                die "Btrfs returned multiple paths for a single subvolume id, mountpoint $mountPoint\n";
+            } elsif ($#paths != 0) {
+                die "Btrfs did not return a path for the subvolume at $mountPoint\n";
+            }
+            push @extraOptions, "subvol=$paths[0]";
+        }
+    }
+
+    # Don't emit tmpfs entry for /tmp, because it most likely comes from the
+    # boot.tmpOnTmpfs option in configuration.nix (managed declaratively).
+    next if ($mountPoint eq "/tmp" && $fsType eq "tmpfs");
+
+    # Emit the filesystem.
+    $fileSystems .= <<EOF;
+  fileSystems.\"$mountPoint\" =
+    { device = \"${\(findStableDevPath $device)}\";
+      fsType = \"$fsType\";
+EOF
+
+    if (scalar @extraOptions > 0) {
+        $fileSystems .= <<EOF;
+      options = \[ ${\join " ", map { "\"" . $_ . "\"" } uniq(@extraOptions)} \];
+EOF
+    }
+
+    $fileSystems .= <<EOF;
+    };
+
+EOF
+
+    # If this filesystem is on a LUKS device, then add a
+    # boot.initrd.luks.devices entry.
+    if (-e $device) {
+        my $deviceName = basename(abs_path($device));
+        if (-e "/sys/class/block/$deviceName"
+            && read_file("/sys/class/block/$deviceName/dm/uuid",  err_mode => 'quiet') =~ /^CRYPT-LUKS/)
+        {
+            my @slaves = glob("/sys/class/block/$deviceName/slaves/*");
+            if (scalar @slaves == 1) {
+                my $slave = "/dev/" . basename($slaves[0]);
+                if (-e $slave) {
+                    my $dmName = read_file("/sys/class/block/$deviceName/dm/name");
+                    chomp $dmName;
+                    # Ensure to add an entry only once
+                    my $luksDevice = "  boot.initrd.luks.devices.\"$dmName\".device";
+                    if ($fileSystems !~ /^\Q$luksDevice\E/m) {
+                        $fileSystems .= "$luksDevice = \"${\(findStableDevPath $slave)}\";\n\n";
+                    }
+                }
+            }
+        }
+    }
+}
+
+# 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.
+
+sub toNixStringList {
+    my $res = "";
+    foreach my $s (@_) {
+        $res .= " \"$s\"";
+    }
+    return $res;
+}
+sub toNixList {
+    my $res = "";
+    foreach my $s (@_) {
+        $res .= " $s";
+    }
+    return $res;
+}
+
+sub multiLineList {
+    my $indent = shift;
+    return " [ ]" if !@_;
+    my $res = "\n${indent}[ ";
+    my $first = 1;
+    foreach my $s (@_) {
+        $res .= "$indent  " if !$first;
+        $first = 0;
+        $res .= "$s\n";
+    }
+    $res .= "$indent]";
+    return $res;
+}
+
+my $initrdAvailableKernelModules = toNixStringList(uniq @initrdAvailableKernelModules);
+my $initrdKernelModules = toNixStringList(uniq @initrdKernelModules);
+my $kernelModules = toNixStringList(uniq @kernelModules);
+my $modulePackages = toNixList(uniq @modulePackages);
+
+my $fsAndSwap = "";
+if (!$noFilesystems) {
+    $fsAndSwap = "\n$fileSystems  ";
+    $fsAndSwap .= "swapDevices =" . multiLineList("    ", @swapDevices) . ";\n";
+}
+
+my $networkingDhcpConfig = generateNetworkingDhcpConfig();
+
+my $hwConfig = <<EOF;
+# 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, lib, pkgs, modulesPath, ... }:
+
+{
+  imports =${\multiLineList("    ", @imports)};
+
+  boot.initrd.availableKernelModules = [$initrdAvailableKernelModules ];
+  boot.initrd.kernelModules = [$initrdKernelModules ];
+  boot.kernelModules = [$kernelModules ];
+  boot.extraModulePackages = [$modulePackages ];
+$fsAndSwap
+$networkingDhcpConfig
+${\join "", (map { "  $_\n" } (uniq @attrs))}}
+EOF
+
+sub generateNetworkingDhcpConfig {
+    my $config = <<EOF;
+  # The global useDHCP flag is deprecated, therefore explicitly set to false here.
+  # Per-interface useDHCP will be mandatory in the future, so this generated config
+  # replicates the default behaviour.
+  networking.useDHCP = lib.mkDefault false;
+EOF
+
+    foreach my $path (glob "/sys/class/net/*") {
+        my $dev = basename($path);
+        if ($dev ne "lo") {
+            $config .= "  networking.interfaces.$dev.useDHCP = lib.mkDefault true;\n";
+        }
+    }
+
+    return $config;
+}
+
+sub generateXserverConfig {
+    my $xserverEnabled = "@xserverEnabled@";
+
+    my $config = "";
+    if ($xserverEnabled eq "1") {
+        $config = <<EOF;
+  # Enable the X11 windowing system.
+  services.xserver.enable = true;
+EOF
+    } else {
+        $config = <<EOF;
+  # Enable the X11 windowing system.
+  # services.xserver.enable = true;
+EOF
+    }
+}
+
+if ($showHardwareConfig) {
+    print STDOUT $hwConfig;
+} else {
+    $outDir = "$rootDir$outDir";
+
+    my $fn = "$outDir/hardware-configuration.nix";
+    print STDERR "writing $fn...\n";
+    mkpath($outDir, 0, 0755);
+    write_file($fn, $hwConfig);
+
+    # Generate a basic configuration.nix, unless one already exists.
+    $fn = "$outDir/configuration.nix";
+    if ($force || ! -e $fn) {
+        print STDERR "writing $fn...\n";
+
+        my $bootLoaderConfig = "";
+        if (-e "/sys/firmware/efi/efivars") {
+            $bootLoaderConfig = <<EOF;
+  # Use the systemd-boot EFI boot loader.
+  boot.loader.systemd-boot.enable = true;
+  boot.loader.efi.canTouchEfiVariables = true;
+EOF
+        } elsif (-e "/boot/extlinux") {
+            $bootLoaderConfig = <<EOF;
+  # Use the extlinux boot loader. (NixOS wants to enable GRUB by default)
+  boot.loader.grub.enable = false;
+  # Enables the generation of /boot/extlinux/extlinux.conf
+  boot.loader.generic-extlinux-compatible.enable = true;
+EOF
+        } elsif ($virt ne "systemd-nspawn") {
+            $bootLoaderConfig = <<EOF;
+  # Use the GRUB 2 boot loader.
+  boot.loader.grub.enable = true;
+  boot.loader.grub.version = 2;
+  # boot.loader.grub.efiSupport = true;
+  # boot.loader.grub.efiInstallAsRemovable = true;
+  # boot.loader.efi.efiSysMountPoint = "/boot/efi";
+  # Define on which hard drive you want to install Grub.
+  # boot.loader.grub.device = "/dev/sda"; # or "nodev" for efi only
+EOF
+        }
+
+        my $networkingDhcpConfig = generateNetworkingDhcpConfig();
+
+        my $xserverConfig = generateXserverConfig();
+
+        (my $desktopConfiguration = <<EOF)=~s/^/  /gm;
+@desktopConfiguration@
+EOF
+
+        write_file($fn, <<EOF);
+@configuration@
+EOF
+        print STDERR "For more hardware-specific settings, see https://github.com/NixOS/nixos-hardware.\n"
+    } else {
+        print STDERR "warning: not overwriting existing $fn\n";
+    }
+}
+
+# workaround for a bug in substituteAll
diff --git a/nixos/modules/installer/tools/nixos-install.sh b/nixos/modules/installer/tools/nixos-install.sh
new file mode 100644
index 00000000000..e7cf52f5e32
--- /dev/null
+++ b/nixos/modules/installer/tools/nixos-install.sh
@@ -0,0 +1,218 @@
+#! @runtimeShell@
+# shellcheck shell=bash
+
+set -e
+shopt -s nullglob
+
+export PATH=@path@:$PATH
+
+# Ensure a consistent umask.
+umask 0022
+
+# Parse the command line for the -I flag
+extraBuildFlags=()
+flakeFlags=()
+
+mountPoint=/mnt
+channelPath=
+system=
+verbosity=()
+
+while [ "$#" -gt 0 ]; do
+    i="$1"; shift 1
+    case "$i" in
+        --max-jobs|-j|--cores|-I|--substituters)
+            j="$1"; shift 1
+            extraBuildFlags+=("$i" "$j")
+            ;;
+        --option)
+            j="$1"; shift 1
+            k="$1"; shift 1
+            extraBuildFlags+=("$i" "$j" "$k")
+            ;;
+        --root)
+            mountPoint="$1"; shift 1
+            ;;
+        --system|--closure)
+            system="$1"; shift 1
+            ;;
+        --flake)
+          flake="$1"
+          flakeFlags=(--experimental-features 'nix-command flakes')
+          shift 1
+          ;;
+        --recreate-lock-file|--no-update-lock-file|--no-write-lock-file|--no-registries|--commit-lock-file)
+          lockFlags+=("$i")
+          ;;
+        --update-input)
+          j="$1"; shift 1
+          lockFlags+=("$i" "$j")
+          ;;
+        --override-input)
+          j="$1"; shift 1
+          k="$1"; shift 1
+          lockFlags+=("$i" "$j" "$k")
+          ;;
+        --channel)
+            channelPath="$1"; shift 1
+            ;;
+        --no-channel-copy)
+            noChannelCopy=1
+            ;;
+        --no-root-password|--no-root-passwd)
+            noRootPasswd=1
+            ;;
+        --no-bootloader)
+            noBootLoader=1
+            ;;
+        --show-trace|--impure|--keep-going)
+            extraBuildFlags+=("$i")
+            ;;
+        --help)
+            exec man nixos-install
+            exit 1
+            ;;
+        --debug)
+            set -x
+            ;;
+        -v*|--verbose)
+            verbosity+=("$i")
+            ;;
+        *)
+            echo "$0: unknown option \`$i'"
+            exit 1
+            ;;
+    esac
+done
+
+if ! test -e "$mountPoint"; then
+    echo "mount point $mountPoint doesn't exist"
+    exit 1
+fi
+
+# Verify permissions are okay-enough
+checkPath="$(realpath "$mountPoint")"
+while [[ "$checkPath" != "/" ]]; do
+    mode="$(stat -c '%a' "$checkPath")"
+    if [[ "${mode: -1}" -lt "5" ]]; then
+        echo "path $checkPath should have permissions 755, but had permissions $mode. Consider running 'chmod o+rx $checkPath'."
+        exit 1
+    fi
+    checkPath="$(dirname "$checkPath")"
+done
+
+# Get the path of the NixOS configuration file.
+if [[ -z $NIXOS_CONFIG ]]; then
+    NIXOS_CONFIG=$mountPoint/etc/nixos/configuration.nix
+fi
+
+if [[ ${NIXOS_CONFIG:0:1} != / ]]; then
+    echo "$0: \$NIXOS_CONFIG is not an absolute path"
+    exit 1
+fi
+
+if [[ -n $flake ]]; then
+    if [[ $flake =~ ^(.*)\#([^\#\"]*)$ ]]; then
+       flake="${BASH_REMATCH[1]}"
+       flakeAttr="${BASH_REMATCH[2]}"
+    fi
+    if [[ -z "$flakeAttr" ]]; then
+        echo "Please specify the name of the NixOS configuration to be installed, as a URI fragment in the flake-uri."
+        echo "For example, to use the output nixosConfigurations.foo from the flake.nix, append \"#foo\" to the flake-uri."
+        exit 1
+    fi
+    flakeAttr="nixosConfigurations.\"$flakeAttr\""
+fi
+
+# Resolve the flake.
+if [[ -n $flake ]]; then
+    flake=$(nix "${flakeFlags[@]}" flake metadata --json "${extraBuildFlags[@]}" "${lockFlags[@]}" -- "$flake" | jq -r .url)
+fi
+
+if [[ ! -e $NIXOS_CONFIG && -z $system && -z $flake ]]; then
+    echo "configuration file $NIXOS_CONFIG doesn't exist"
+    exit 1
+fi
+
+# A place to drop temporary stuff.
+tmpdir="$(mktemp -d -p "$mountPoint")"
+trap 'rm -rf $tmpdir' EXIT
+
+# store temporary files on target filesystem by default
+export TMPDIR=${TMPDIR:-$tmpdir}
+
+sub="auto?trusted=1"
+
+# Copy the NixOS/Nixpkgs sources to the target as the initial contents
+# of the NixOS channel.
+if [[ -z $noChannelCopy ]]; then
+    if [[ -z $channelPath ]]; then
+        channelPath="$(nix-env -p /nix/var/nix/profiles/per-user/root/channels -q nixos --no-name --out-path 2>/dev/null || echo -n "")"
+    fi
+    if [[ -n $channelPath ]]; then
+        echo "copying channel..."
+        mkdir -p "$mountPoint"/nix/var/nix/profiles/per-user/root
+        nix-env --store "$mountPoint" "${extraBuildFlags[@]}" --extra-substituters "$sub" \
+                -p "$mountPoint"/nix/var/nix/profiles/per-user/root/channels --set "$channelPath" --quiet \
+                "${verbosity[@]}"
+        install -m 0700 -d "$mountPoint"/root/.nix-defexpr
+        ln -sfn /nix/var/nix/profiles/per-user/root/channels "$mountPoint"/root/.nix-defexpr/channels
+    fi
+fi
+
+# Build the system configuration in the target filesystem.
+if [[ -z $system ]]; then
+    outLink="$tmpdir/system"
+    if [[ -z $flake ]]; then
+        echo "building the configuration in $NIXOS_CONFIG..."
+        nix-build --out-link "$outLink" --store "$mountPoint" "${extraBuildFlags[@]}" \
+            --extra-substituters "$sub" \
+            '<nixpkgs/nixos>' -A system -I "nixos-config=$NIXOS_CONFIG" "${verbosity[@]}"
+    else
+        echo "building the flake in $flake..."
+        nix "${flakeFlags[@]}" build "$flake#$flakeAttr.config.system.build.toplevel" \
+            --store "$mountPoint" --extra-substituters "$sub" "${verbosity[@]}" \
+            "${extraBuildFlags[@]}" "${lockFlags[@]}" --out-link "$outLink"
+    fi
+    system=$(readlink -f "$outLink")
+fi
+
+# Set the system profile to point to the configuration. TODO: combine
+# this with the previous step once we have a nix-env replacement with
+# a progress bar.
+nix-env --store "$mountPoint" "${extraBuildFlags[@]}" \
+        --extra-substituters "$sub" \
+        -p "$mountPoint"/nix/var/nix/profiles/system --set "$system" "${verbosity[@]}"
+
+# Mark the target as a NixOS installation, otherwise switch-to-configuration will chicken out.
+mkdir -m 0755 -p "$mountPoint/etc"
+touch "$mountPoint/etc/NIXOS"
+
+# Switch to the new system configuration.  This will install Grub with
+# a menu default pointing at the kernel/initrd/etc of the new
+# configuration.
+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
+fi
+
+# Ask the user to set a root password, but only if the passwd command
+# exists (i.e. when mutable user accounts are enabled).
+if [[ -z $noRootPasswd ]] && [ -t 0 ]; then
+    if nixos-enter --root "$mountPoint" -c 'test -e /nix/var/nix/profiles/system/sw/bin/passwd'; then
+        set +e
+        nixos-enter --root "$mountPoint" -c 'echo "setting root password..." && /nix/var/nix/profiles/system/sw/bin/passwd'
+        exit_code=$?
+        set -e
+
+        if [[ $exit_code != 0 ]]; then
+            echo "Setting a root password failed with the above printed error."
+            echo "You can set the root password manually by executing \`nixos-enter --root ${mountPoint@Q}\` and then running \`passwd\` in the shell of the new system."
+            exit $exit_code
+        fi
+    fi
+fi
+
+echo "installation finished!"
diff --git a/nixos/modules/installer/tools/nixos-option/default.nix b/nixos/modules/installer/tools/nixos-option/default.nix
new file mode 100644
index 00000000000..061460f38a3
--- /dev/null
+++ b/nixos/modules/installer/tools/nixos-option/default.nix
@@ -0,0 +1 @@
+{ pkgs, ... }: pkgs.nixos-option
diff --git a/nixos/modules/installer/tools/nixos-version.sh b/nixos/modules/installer/tools/nixos-version.sh
new file mode 100644
index 00000000000..59a9c572b41
--- /dev/null
+++ b/nixos/modules/installer/tools/nixos-version.sh
@@ -0,0 +1,24 @@
+#! @runtimeShell@
+# shellcheck shell=bash
+
+case "$1" in
+  -h|--help)
+    exec man nixos-version
+    exit 1
+    ;;
+  --hash|--revision)
+    if ! [[ @revision@ =~ ^[0-9a-f]+$ ]]; then
+      echo "$0: Nixpkgs commit hash is unknown"
+      exit 1
+    fi
+    echo "@revision@"
+    ;;
+  --json)
+    cat <<EOF
+@json@
+EOF
+    ;;
+  *)
+    echo "@version@ (@codeName@)"
+    ;;
+esac
diff --git a/nixos/modules/installer/tools/tools.nix b/nixos/modules/installer/tools/tools.nix
new file mode 100644
index 00000000000..71aaf7f253d
--- /dev/null
+++ b/nixos/modules/installer/tools/tools.nix
@@ -0,0 +1,235 @@
+# This module generates nixos-install, nixos-rebuild,
+# nixos-generate-config, etc.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  makeProg = args: pkgs.substituteAll (args // {
+    dir = "bin";
+    isExecutable = true;
+  });
+
+  nixos-build-vms = makeProg {
+    name = "nixos-build-vms";
+    src = ./nixos-build-vms/nixos-build-vms.sh;
+    inherit (pkgs) runtimeShell;
+  };
+
+  nixos-install = makeProg {
+    name = "nixos-install";
+    src = ./nixos-install.sh;
+    inherit (pkgs) runtimeShell;
+    nix = config.nix.package.out;
+    path = makeBinPath [
+      pkgs.jq
+      nixos-enter
+    ];
+  };
+
+  nixos-rebuild = pkgs.nixos-rebuild.override { nix = config.nix.package.out; };
+
+  nixos-generate-config = makeProg {
+    name = "nixos-generate-config";
+    src = ./nixos-generate-config.pl;
+    perl = "${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl";
+    detectvirt = "${pkgs.systemd}/bin/systemd-detect-virt";
+    btrfs = "${pkgs.btrfs-progs}/bin/btrfs";
+    inherit (config.system.nixos-generate-config) configuration desktopConfiguration;
+    xserverEnabled = config.services.xserver.enable;
+  };
+
+  nixos-option =
+    if lib.versionAtLeast (lib.getVersion config.nix.package) "2.4pre"
+    then null
+    else pkgs.nixos-option;
+
+  nixos-version = makeProg {
+    name = "nixos-version";
+    src = ./nixos-version.sh;
+    inherit (pkgs) runtimeShell;
+    inherit (config.system.nixos) version codeName revision;
+    inherit (config.system) configurationRevision;
+    json = builtins.toJSON ({
+      nixosVersion = config.system.nixos.version;
+    } // optionalAttrs (config.system.nixos.revision != null) {
+      nixpkgsRevision = config.system.nixos.revision;
+    } // optionalAttrs (config.system.configurationRevision != null) {
+      configurationRevision = config.system.configurationRevision;
+    });
+  };
+
+  nixos-enter = makeProg {
+    name = "nixos-enter";
+    src = ./nixos-enter.sh;
+    inherit (pkgs) runtimeShell;
+  };
+
+in
+
+{
+
+  options.system.nixos-generate-config = {
+    configuration = mkOption {
+      internal = true;
+      type = types.str;
+      description = ''
+        The NixOS module that <literal>nixos-generate-config</literal>
+        saves to <literal>/etc/nixos/configuration.nix</literal>.
+
+        This is an internal option. No backward compatibility is guaranteed.
+        Use at your own risk!
+
+        Note that this string gets spliced into a Perl script. The perl
+        variable <literal>$bootLoaderConfig</literal> can be used to
+        splice in the boot loader configuration.
+      '';
+    };
+
+    desktopConfiguration = mkOption {
+      internal = true;
+      type = types.listOf types.lines;
+      default = [];
+      description = ''
+        Text to preseed the desktop configuration that <literal>nixos-generate-config</literal>
+        saves to <literal>/etc/nixos/configuration.nix</literal>.
+
+        This is an internal option. No backward compatibility is guaranteed.
+        Use at your own risk!
+
+        Note that this string gets spliced into a Perl script. The perl
+        variable <literal>$bootLoaderConfig</literal> can be used to
+        splice in the boot loader configuration.
+      '';
+    };
+  };
+
+  options.system.disableInstallerTools = mkOption {
+    internal = true;
+    type = types.bool;
+    default = false;
+    description = ''
+      Disable nixos-rebuild, nixos-generate-config, nixos-installer
+      and other NixOS tools. This is useful to shrink embedded,
+      read-only systems which are not expected to be rebuild or
+      reconfigure themselves. Use at your own risk!
+    '';
+  };
+
+  config = lib.mkIf (!config.system.disableInstallerTools) {
+
+    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’).
+
+      { config, pkgs, ... }:
+
+      {
+        imports =
+          [ # Include the results of the hardware scan.
+            ./hardware-configuration.nix
+          ];
+
+      $bootLoaderConfig
+        # networking.hostName = "nixos"; # Define your hostname.
+        # Pick only one of the below networking options.
+        # networking.wireless.enable = true;  # Enables wireless support via wpa_supplicant.
+        # networking.networkmanager.enable = true;  # Easiest to use and most distros use this by default.
+
+        # Set your time zone.
+        # time.timeZone = "Europe/Amsterdam";
+
+        # Configure network proxy if necessary
+        # networking.proxy.default = "http://user:password\@proxy:port/";
+        # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
+
+        # Select internationalisation properties.
+        # i18n.defaultLocale = "en_US.UTF-8";
+        # console = {
+        #   font = "Lat2-Terminus16";
+        #   keyMap = "us";
+        #   useXkbConfig = true; # use xkbOptions in tty.
+        # };
+
+      $xserverConfig
+
+      $desktopConfiguration
+        # Configure keymap in X11
+        # services.xserver.layout = "us";
+        # services.xserver.xkbOptions = {
+        #   "eurosign:e";
+        #   "caps:escape" # map caps to escape.
+        # };
+
+        # Enable CUPS to print documents.
+        # services.printing.enable = true;
+
+        # Enable sound.
+        # sound.enable = true;
+        # hardware.pulseaudio.enable = true;
+
+        # Enable touchpad support (enabled default in most desktopManager).
+        # services.xserver.libinput.enable = true;
+
+        # Define a user account. Don't forget to set a password with ‘passwd’.
+        # users.users.jane = {
+        #   isNormalUser = true;
+        #   extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
+        # };
+
+        # List packages installed in system profile. To search, run:
+        # \$ nix search wget
+        # environment.systemPackages = with pkgs; [
+        #   vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
+        #   wget
+        #   firefox
+        # ];
+
+        # Some programs need SUID wrappers, can be configured further or are
+        # started in user sessions.
+        # programs.mtr.enable = true;
+        # programs.gnupg.agent = {
+        #   enable = true;
+        #   enableSSHSupport = true;
+        # };
+
+        # List services that you want to enable:
+
+        # Enable the OpenSSH daemon.
+        # services.openssh.enable = true;
+
+        # Open ports in the firewall.
+        # networking.firewall.allowedTCPPorts = [ ... ];
+        # networking.firewall.allowedUDPPorts = [ ... ];
+        # Or disable the firewall altogether.
+        # networking.firewall.enable = false;
+
+        # 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
+        # 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).
+        system.stateVersion = "${config.system.nixos.release}"; # Did you read the comment?
+
+      }
+    '';
+
+    environment.systemPackages =
+      [ nixos-build-vms
+        nixos-install
+        nixos-rebuild
+        nixos-generate-config
+        nixos-version
+        nixos-enter
+      ] ++ lib.optional (nixos-option != null) nixos-option;
+
+    system.build = {
+      inherit nixos-install nixos-generate-config nixos-option nixos-rebuild nixos-enter;
+    };
+
+  };
+
+}
diff --git a/nixos/modules/installer/virtualbox-demo.nix b/nixos/modules/installer/virtualbox-demo.nix
new file mode 100644
index 00000000000..27a7651382b
--- /dev/null
+++ b/nixos/modules/installer/virtualbox-demo.nix
@@ -0,0 +1,61 @@
+{ lib, ... }:
+
+with lib;
+
+{
+  imports =
+    [ ../virtualisation/virtualbox-image.nix
+      ../installer/cd-dvd/channel.nix
+      ../profiles/demo.nix
+      ../profiles/clone-config.nix
+    ];
+
+  # FIXME: UUID detection is currently broken
+  boot.loader.grub.fsIdentifier = "provided";
+
+  # Allow mounting of shared folders.
+  users.users.demo.extraGroups = [ "vboxsf" ];
+
+  # Add some more video drivers to give X11 a shot at working in
+  # VMware and QEMU.
+  services.xserver.videoDrivers = mkOverride 40 [ "virtualbox" "vmware" "cirrus" "vesa" "modesetting" ];
+
+  powerManagement.enable = false;
+  system.stateVersion = mkDefault "18.03";
+
+  installer.cloneConfigExtra = ''
+  # Let demo build as a trusted user.
+  # nix.settings.trusted-users = [ "demo" ];
+
+  # Mount a VirtualBox shared folder.
+  # This is configurable in the VirtualBox menu at
+  # Machine / Settings / Shared Folders.
+  # fileSystems."/mnt" = {
+  #   fsType = "vboxsf";
+  #   device = "nameofdevicetomount";
+  #   options = [ "rw" ];
+  # };
+
+  # By default, the NixOS VirtualBox demo image includes SDDM and Plasma.
+  # If you prefer another desktop manager or display manager, you may want
+  # to disable the default.
+  # services.xserver.desktopManager.plasma5.enable = lib.mkForce false;
+  # services.xserver.displayManager.sddm.enable = lib.mkForce false;
+
+  # Enable GDM/GNOME by uncommenting above two lines and two lines below.
+  # services.xserver.displayManager.gdm.enable = true;
+  # services.xserver.desktopManager.gnome.enable = true;
+
+  # Set your time zone.
+  # time.timeZone = "Europe/Amsterdam";
+
+  # List packages installed in system profile. To search, run:
+  # \$ nix search wget
+  # environment.systemPackages = with pkgs; [
+  #   wget vim
+  # ];
+
+  # Enable the OpenSSH daemon.
+  # services.openssh.enable = true;
+  '';
+}
diff --git a/nixos/modules/misc/assertions.nix b/nixos/modules/misc/assertions.nix
new file mode 100644
index 00000000000..550b3ac97f6
--- /dev/null
+++ b/nixos/modules/misc/assertions.nix
@@ -0,0 +1,34 @@
+{ lib, ... }:
+
+with lib;
+
+{
+
+  options = {
+
+    assertions = mkOption {
+      type = types.listOf types.unspecified;
+      internal = true;
+      default = [];
+      example = [ { assertion = false; message = "you can't enable this for that reason"; } ];
+      description = ''
+        This option allows modules to express conditions that must
+        hold for the evaluation of the system configuration to
+        succeed, along with associated error messages for the user.
+      '';
+    };
+
+    warnings = mkOption {
+      internal = true;
+      default = [];
+      type = types.listOf types.str;
+      example = [ "The `foo' service is deprecated and will go away soon!" ];
+      description = ''
+        This option allows modules to show warnings to users during
+        the evaluation of the system configuration.
+      '';
+    };
+
+  };
+  # impl of assertions is in <nixpkgs/nixos/modules/system/activation/top-level.nix>
+}
diff --git a/nixos/modules/misc/crashdump.nix b/nixos/modules/misc/crashdump.nix
new file mode 100644
index 00000000000..b0f75d9caaa
--- /dev/null
+++ b/nixos/modules/misc/crashdump.nix
@@ -0,0 +1,76 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  crashdump = config.boot.crashDump;
+
+  kernelParams = concatStringsSep " " crashdump.kernelParams;
+
+in
+###### interface
+{
+  options = {
+    boot = {
+      crashDump = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            If enabled, NixOS will set up a kernel that will
+            boot on crash, and leave the user in systemd rescue
+            to be able to save the crashed kernel dump at
+            /proc/vmcore.
+            It also activates the NMI watchdog.
+          '';
+        };
+        reservedMemory = mkOption {
+          default = "128M";
+          type = types.str;
+          description = ''
+            The amount of memory reserved for the crashdump kernel.
+            If you choose a too high value, dmesg will mention
+            "crashkernel reservation failed".
+          '';
+        };
+        kernelParams = mkOption {
+          type = types.listOf types.str;
+          default = [ "1" "boot.shell_on_fail" ];
+          description = ''
+            Parameters that will be passed to the kernel kexec-ed on crash.
+          '';
+        };
+      };
+    };
+  };
+
+###### implementation
+
+  config = mkIf crashdump.enable {
+    boot = {
+      postBootCommands = ''
+        echo "loading crashdump kernel...";
+        ${pkgs.kexec-tools}/sbin/kexec -p /run/current-system/kernel \
+        --initrd=/run/current-system/initrd \
+        --reset-vga --console-vga \
+        --command-line="init=$(readlink -f /run/current-system/init) irqpoll maxcpus=1 reset_devices ${kernelParams}"
+      '';
+      kernelParams = [
+       "crashkernel=${crashdump.reservedMemory}"
+       "nmi_watchdog=panic"
+       "softlockup_panic=1"
+      ];
+      kernelPatches = [ {
+        name = "crashdump-config";
+        patch = null;
+        extraConfig = ''
+                CRASH_DUMP y
+                DEBUG_INFO y
+                PROC_VMCORE y
+                LOCKUP_DETECTOR y
+                HARDLOCKUP_DETECTOR y
+              '';
+        } ];
+    };
+  };
+}
diff --git a/nixos/modules/misc/documentation.nix b/nixos/modules/misc/documentation.nix
new file mode 100644
index 00000000000..9304c307af2
--- /dev/null
+++ b/nixos/modules/misc/documentation.nix
@@ -0,0 +1,346 @@
+{ config, options, lib, pkgs, utils, modules, baseModules, extraModules, modulesPath, ... }:
+
+with lib;
+
+let
+
+  cfg = config.documentation;
+  allOpts = options;
+
+  /* Modules for which to show options even when not imported. */
+  extraDocModules = [ ../virtualisation/qemu-vm.nix ];
+
+  canCacheDocs = m:
+    let
+      f = import m;
+      instance = f (mapAttrs (n: _: abort "evaluating ${n} for `meta` failed") (functionArgs f));
+    in
+      cfg.nixos.options.splitBuild
+        && builtins.isPath m
+        && isFunction f
+        && instance ? options
+        && instance.meta.buildDocsInSandbox or true;
+
+  docModules =
+    let
+      p = partition canCacheDocs (baseModules ++ extraDocModules);
+    in
+      {
+        lazy = p.right;
+        eager = p.wrong ++ optionals cfg.nixos.includeAllModules (extraModules ++ modules);
+      };
+
+  manual = import ../../doc/manual rec {
+    inherit pkgs config;
+    version = config.system.nixos.release;
+    revision = "release-${version}";
+    extraSources = cfg.nixos.extraModuleSources;
+    options =
+      let
+        scrubbedEval = evalModules {
+          modules = [ {
+            _module.check = false;
+          } ] ++ docModules.eager;
+          specialArgs = {
+            pkgs = scrubDerivations "pkgs" pkgs;
+            # allow access to arbitrary options for eager modules, eg for getting
+            # option types from lazy modules
+            options = allOpts;
+            inherit modulesPath utils;
+          };
+        };
+        scrubDerivations = namePrefix: pkgSet: mapAttrs
+          (name: value:
+            let wholeName = "${namePrefix}.${name}"; in
+            if isAttrs value then
+              scrubDerivations wholeName value
+              // (optionalAttrs (isDerivation value) { outPath = "\${${wholeName}}"; })
+            else value
+          )
+          pkgSet;
+      in scrubbedEval.options;
+    baseOptionsJSON =
+      let
+        filter =
+          builtins.filterSource
+            (n: t:
+              (t == "directory" -> baseNameOf n != "tests")
+              && (t == "file" -> hasSuffix ".nix" n)
+            );
+      in
+        pkgs.runCommand "lazy-options.json" {
+          libPath = filter "${toString pkgs.path}/lib";
+          pkgsLibPath = filter "${toString pkgs.path}/pkgs/pkgs-lib";
+          nixosPath = filter "${toString pkgs.path}/nixos";
+          modules = map (p: ''"${removePrefix "${modulesPath}/" (toString p)}"'') docModules.lazy;
+        } ''
+          export NIX_STORE_DIR=$TMPDIR/store
+          export NIX_STATE_DIR=$TMPDIR/state
+          ${pkgs.buildPackages.nix}/bin/nix-instantiate \
+            --show-trace \
+            --eval --json --strict \
+            --argstr libPath "$libPath" \
+            --argstr pkgsLibPath "$pkgsLibPath" \
+            --argstr nixosPath "$nixosPath" \
+            --arg modules "[ $modules ]" \
+            --argstr stateVersion "${options.system.stateVersion.default}" \
+            --argstr release "${config.system.nixos.release}" \
+            $nixosPath/lib/eval-cacheable-options.nix > $out \
+            || {
+              echo -en "\e[1;31m"
+              echo 'Cacheable portion of option doc build failed.'
+              echo 'Usually this means that an option attribute that ends up in documentation (eg' \
+                '`default` or `description`) depends on the restricted module arguments' \
+                '`config` or `pkgs`.'
+              echo
+              echo 'Rebuild your configuration with `--show-trace` to find the offending' \
+                'location. Remove the references to restricted arguments (eg by escaping' \
+                'their antiquotations or adding a `defaultText`) or disable the sandboxed' \
+                'build for the failing module by setting `meta.buildDocsInSandbox = false`.'
+              echo -en "\e[0m"
+              exit 1
+            } >&2
+        '';
+    inherit (cfg.nixos.options) warningsAreErrors;
+  };
+
+
+  nixos-help = let
+    helpScript = pkgs.writeShellScriptBin "nixos-help" ''
+      # Finds first executable browser in a colon-separated list.
+      # (see how xdg-open defines BROWSER)
+      browser="$(
+        IFS=: ; for b in $BROWSER; do
+          [ -n "$(type -P "$b" || true)" ] && echo "$b" && break
+        done
+      )"
+      if [ -z "$browser" ]; then
+        browser="$(type -P xdg-open || true)"
+        if [ -z "$browser" ]; then
+          browser="${pkgs.w3m-nographics}/bin/w3m"
+        fi
+      fi
+      exec "$browser" ${manual.manualHTMLIndex}
+    '';
+
+    desktopItem = pkgs.makeDesktopItem {
+      name = "nixos-manual";
+      desktopName = "NixOS Manual";
+      genericName = "View NixOS documentation in a web browser";
+      icon = "nix-snowflake";
+      exec = "nixos-help";
+      categories = ["System"];
+    };
+
+    in pkgs.symlinkJoin {
+      name = "nixos-help";
+      paths = [
+        helpScript
+        desktopItem
+      ];
+    };
+
+in
+
+{
+  imports = [
+    (mkRenamedOptionModule [ "programs" "info" "enable" ] [ "documentation" "info" "enable" ])
+    (mkRenamedOptionModule [ "programs" "man"  "enable" ] [ "documentation" "man"  "enable" ])
+    (mkRenamedOptionModule [ "services" "nixosManual" "enable" ] [ "documentation" "nixos" "enable" ])
+  ];
+
+  options = {
+
+    documentation = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to install documentation of packages from
+          <option>environment.systemPackages</option> into the generated system path.
+
+          See "Multiple-output packages" chapter in the nixpkgs manual for more info.
+        '';
+        # which is at ../../../doc/multiple-output.chapter.md
+      };
+
+      man.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to install manual pages.
+          This also includes <literal>man</literal> outputs.
+        '';
+      };
+
+      man.generateCaches = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to generate the manual page index caches.
+          This allows searching for a page or
+          keyword using utilities like
+          <citerefentry>
+            <refentrytitle>apropos</refentrytitle>
+            <manvolnum>1</manvolnum>
+          </citerefentry>
+          and the <literal>-k</literal> option of
+          <citerefentry>
+            <refentrytitle>man</refentrytitle>
+            <manvolnum>1</manvolnum>
+          </citerefentry>.
+        '';
+      };
+
+      info.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to install info pages and the <command>info</command> command.
+          This also includes "info" outputs.
+        '';
+      };
+
+      doc.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to install documentation distributed in packages' <literal>/share/doc</literal>.
+          Usually plain text and/or HTML.
+          This also includes "doc" outputs.
+        '';
+      };
+
+      dev.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to install documentation targeted at developers.
+          <itemizedlist>
+          <listitem><para>This includes man pages targeted at developers if <option>documentation.man.enable</option> is
+                    set (this also includes "devman" outputs).</para></listitem>
+          <listitem><para>This includes info pages targeted at developers if <option>documentation.info.enable</option>
+                    is set (this also includes "devinfo" outputs).</para></listitem>
+          <listitem><para>This includes other pages targeted at developers if <option>documentation.doc.enable</option>
+                    is set (this also includes "devdoc" outputs).</para></listitem>
+          </itemizedlist>
+        '';
+      };
+
+      nixos.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to install NixOS's own documentation.
+          <itemizedlist>
+          <listitem><para>This includes man pages like
+                    <citerefentry><refentrytitle>configuration.nix</refentrytitle>
+                    <manvolnum>5</manvolnum></citerefentry> if <option>documentation.man.enable</option> is
+                    set.</para></listitem>
+          <listitem><para>This includes the HTML manual and the <command>nixos-help</command> command if
+                    <option>documentation.doc.enable</option> is set.</para></listitem>
+          </itemizedlist>
+        '';
+      };
+
+      nixos.options.splitBuild = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to split the option docs build into a cacheable and an uncacheable part.
+          Splitting the build can substantially decrease the amount of time needed to build
+          the manual, but some user modules may be incompatible with this splitting.
+        '';
+      };
+
+      nixos.options.warningsAreErrors = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Treat warning emitted during the option documentation build (eg for missing option
+          descriptions) as errors.
+        '';
+      };
+
+      nixos.includeAllModules = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether the generated NixOS's documentation should include documentation for all
+          the options from all the NixOS modules included in the current
+          <literal>configuration.nix</literal>. Disabling this will make the manual
+          generator to ignore options defined outside of <literal>baseModules</literal>.
+        '';
+      };
+
+      nixos.extraModuleSources = mkOption {
+        type = types.listOf (types.either types.path types.str);
+        default = [ ];
+        description = ''
+          Which extra NixOS module paths the generated NixOS's documentation should strip
+          from options.
+        '';
+        example = literalExpression ''
+          # e.g. with options from modules in ''${pkgs.customModules}/nix:
+          [ pkgs.customModules ]
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable (mkMerge [
+    {
+      assertions = [
+        {
+          assertion = !(cfg.man.man-db.enable && cfg.man.mandoc.enable);
+          message = ''
+            man-db and mandoc can't be used as the default man page viewer at the same time!
+          '';
+        }
+      ];
+    }
+
+    # The actual implementation for this lives in man-db.nix or mandoc.nix,
+    # depending on which backend is active.
+    (mkIf cfg.man.enable {
+      environment.pathsToLink = [ "/share/man" ];
+      environment.extraOutputsToInstall = [ "man" ] ++ optional cfg.dev.enable "devman";
+    })
+
+    (mkIf cfg.info.enable {
+      environment.systemPackages = [ pkgs.texinfoInteractive ];
+      environment.pathsToLink = [ "/share/info" ];
+      environment.extraOutputsToInstall = [ "info" ] ++ optional cfg.dev.enable "devinfo";
+      environment.extraSetup = ''
+        if [ -w $out/share/info ]; then
+          shopt -s nullglob
+          for i in $out/share/info/*.info $out/share/info/*.info.gz; do
+              ${pkgs.buildPackages.texinfo}/bin/install-info $i $out/share/info/dir
+          done
+        fi
+      '';
+    })
+
+    (mkIf cfg.doc.enable {
+      environment.pathsToLink = [ "/share/doc" ];
+      environment.extraOutputsToInstall = [ "doc" ] ++ optional cfg.dev.enable "devdoc";
+    })
+
+    (mkIf cfg.nixos.enable {
+      system.build.manual = manual;
+
+      environment.systemPackages = []
+        ++ optional cfg.man.enable manual.manpages
+        ++ optionals cfg.doc.enable [ manual.manualHTML nixos-help ];
+
+      services.getty.helpLine = mkIf cfg.doc.enable (
+          "\nRun 'nixos-help' for the NixOS manual."
+      );
+    })
+
+  ]);
+
+}
diff --git a/nixos/modules/misc/extra-arguments.nix b/nixos/modules/misc/extra-arguments.nix
new file mode 100644
index 00000000000..48891b44049
--- /dev/null
+++ b/nixos/modules/misc/extra-arguments.nix
@@ -0,0 +1,7 @@
+{ lib, config, pkgs, ... }:
+
+{
+  _module.args = {
+    utils = import ../../lib/utils.nix { inherit lib config pkgs; };
+  };
+}
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
new file mode 100644
index 00000000000..7d1faa50f4b
--- /dev/null
+++ b/nixos/modules/misc/ids.nix
@@ -0,0 +1,677 @@
+# This module defines the global list of uids and gids.  We keep a
+# central list to prevent id collisions.
+
+# IMPORTANT!
+# We only add static uids and gids for services where it is not feasible
+# to change uids/gids on service start, in example a service with a lot of
+# files. Please also check if the service is applicable for systemd's
+# DynamicUser option and does not need a uid/gid allocation at all.
+# Systemd can also change ownership of service directories using the
+# RuntimeDirectory/StateDirectory options.
+
+{ lib, ... }:
+
+let
+  inherit (lib) types;
+in
+{
+  options = {
+
+    ids.uids = lib.mkOption {
+      internal = true;
+      description = ''
+        The user IDs used in NixOS.
+      '';
+      type = types.attrsOf types.int;
+    };
+
+    ids.gids = lib.mkOption {
+      internal = true;
+      description = ''
+        The group IDs used in NixOS.
+      '';
+      type = types.attrsOf types.int;
+    };
+
+  };
+
+
+  config = {
+
+    ids.uids = {
+      root = 0;
+      #wheel = 1; # unused
+      #kmem = 2; # unused
+      #tty = 3; # unused
+      messagebus = 4; # D-Bus
+      haldaemon = 5;
+      #disk = 6; # unused
+      #vsftpd = 7; # dynamically allocated ass of 2021-09-14
+      ftp = 8;
+      # bitlbee = 9; # removed 2021-10-05 #139765
+      #avahi = 10; # removed 2019-05-22
+      nagios = 11;
+      atd = 12;
+      postfix = 13;
+      #postdrop = 14; # unused
+      dovecot = 15;
+      tomcat = 16;
+      #audio = 17; # unused
+      #floppy = 18; # unused
+      uucp = 19;
+      #lp = 20; # unused
+      #proc = 21; # unused
+      pulseaudio = 22; # must match `pulseaudio' GID
+      gpsd = 23;
+      #cdrom = 24; # unused
+      #tape = 25; # unused
+      #video = 26; # unused
+      #dialout = 27; # unused
+      polkituser = 28;
+      #utmp = 29; # unused
+      # ddclient = 30; # converted to DynamicUser = true
+      davfs2 = 31;
+      disnix = 33;
+      osgi = 34;
+      tor = 35;
+      cups = 36;
+      foldingathome = 37;
+      sabnzbd = 38;
+      #kdm = 39; # dropped in 17.03
+      #ghostone = 40; # dropped in 18.03
+      git = 41;
+      #fourstore = 42; # dropped in 20.03
+      #fourstorehttp = 43; # dropped in 20.03
+      #virtuoso = 44;  dropped module
+      #rtkit = 45; # dynamically allocated 2021-09-03
+      dovecot2 = 46;
+      dovenull2 = 47;
+      prayer = 49;
+      mpd = 50;
+      clamav = 51;
+      #fprot = 52; # unused
+      # bind = 53; #dynamically allocated as of 2021-09-03
+      wwwrun = 54;
+      #adm = 55; # unused
+      spamd = 56;
+      #networkmanager = 57; # unused
+      nslcd = 58;
+      scanner = 59;
+      nginx = 60;
+      chrony = 61;
+      #systemd-journal = 62; # unused
+      smtpd = 63;
+      smtpq = 64;
+      supybot = 65;
+      iodined = 66;
+      #libvirtd = 67; # unused
+      graphite = 68;
+      #statsd = 69; # removed 2018-11-14
+      transmission = 70;
+      postgres = 71;
+      #vboxusers = 72; # unused
+      #vboxsf = 73; # unused
+      smbguest = 74;  # unused
+      varnish = 75;
+      datadog = 76;
+      lighttpd = 77;
+      lightdm = 78;
+      freenet = 79;
+      ircd = 80;
+      bacula = 81;
+      #almir = 82; # removed 2018-03-25, the almir package was removed in 30291227f2411abaca097773eedb49b8f259e297 during 2017-08
+      deluge = 83;
+      mysql = 84;
+      rabbitmq = 85;
+      activemq = 86;
+      gnunet = 87;
+      oidentd = 88;
+      quassel = 89;
+      amule = 90;
+      minidlna = 91;
+      elasticsearch = 92;
+      tcpcryptd = 93; # tcpcryptd uses a hard-coded uid. We patch it in Nixpkgs to match this choice.
+      firebird = 95;
+      #keys = 96; # unused
+      #haproxy = 97; # dynamically allocated as of 2020-03-11
+      #mongodb = 98; #dynamically allocated as of 2021-09-03
+      #openldap = 99; # dynamically allocated as of PR#94610
+      #users = 100; # unused
+      # cgminer = 101; #dynamically allocated as of 2021-09-17
+      munin = 102;
+      #logcheck = 103; #dynamically allocated as of 2021-09-17
+      #nix-ssh = 104; #dynamically allocated as of 2021-09-03
+      dictd = 105;
+      couchdb = 106;
+      #searx = 107; # dynamically allocated as of 2020-10-27
+      #kippo = 108; # removed 2021-10-07, the kippo package was removed in 1b213f321cdbfcf868b96fd9959c24207ce1b66a during 2021-04
+      jenkins = 109;
+      systemd-journal-gateway = 110;
+      #notbit = 111; # unused
+      aerospike = 111;
+      #ngircd = 112; #dynamically allocated as of 2021-09-03
+      #btsync = 113; # unused
+      #minecraft = 114; #dynamically allocated as of 2021-09-03
+      vault = 115;
+      # rippled = 116; #dynamically allocated as of 2021-09-18
+      murmur = 117;
+      foundationdb = 118;
+      newrelic = 119;
+      starbound = 120;
+      hydra = 122;
+      spiped = 123;
+      teamspeak = 124;
+      influxdb = 125;
+      nsd = 126;
+      gitolite = 127;
+      znc = 128;
+      polipo = 129;
+      mopidy = 130;
+      #docker = 131; # unused
+      gdm = 132;
+      #dhcpd = 133; # dynamically allocated as of 2021-09-03
+      siproxd = 134;
+      mlmmj = 135;
+      #neo4j = 136;# dynamically allocated as of 2021-09-03
+      riemann = 137;
+      riemanndash = 138;
+      #radvd = 139;# dynamically allocated as of 2021-09-03
+      #zookeeper = 140;# dynamically allocated as of 2021-09-03
+      #dnsmasq = 141;# dynamically allocated as of 2021-09-03
+      #uhub = 142; # unused
+      yandexdisk = 143;
+      mxisd = 144; # was once collectd
+      #consul = 145;# dynamically allocated as of 2021-09-03
+      #mailpile = 146; # removed 2022-01-12
+      redmine = 147;
+      #seeks = 148; # removed 2020-06-21
+      prosody = 149;
+      i2pd = 150;
+      systemd-coredump = 151;
+      systemd-network = 152;
+      systemd-resolve = 153;
+      systemd-timesync = 154;
+      liquidsoap = 155;
+      #etcd = 156;# dynamically allocated as of 2021-09-03
+      hbase = 158;
+      opentsdb = 159;
+      scollector = 160;
+      bosun = 161;
+      kubernetes = 162;
+      peerflix = 163;
+      #chronos = 164; # removed 2020-08-15
+      gitlab = 165;
+      # tox-bootstrapd = 166; removed 2021-09-15
+      cadvisor = 167;
+      nylon = 168;
+      #apache-kafka = 169;# dynamically allocated as of 2021-09-03
+      #panamax = 170; # unused
+      exim = 172;
+      #fleet = 173; # unused
+      #input = 174; # unused
+      sddm = 175;
+      #tss = 176; # dynamically allocated as of 2021-09-17
+      #memcached = 177; removed 2018-01-03
+      #ntp = 179; # dynamically allocated as of 2021-09-17
+      zabbix = 180;
+      #redis = 181; removed 2018-01-03
+      #unifi = 183; dynamically allocated as of 2021-09-17
+      uptimed = 184;
+      #zope2 = 185; # dynamically allocated as of 2021-09-18
+      #ripple-data-api = 186; dynamically allocated as of 2021-09-17
+      mediatomb = 187;
+      #rdnssd = 188; #dynamically allocated as of 2021-09-18
+      ihaskell = 189;
+      i2p = 190;
+      lambdabot = 191;
+      asterisk = 192;
+      plex = 193;
+      plexpy = 195;
+      grafana = 196;
+      skydns = 197;
+      # ripple-rest = 198; # unused, removed 2017-08-12
+      # nix-serve = 199; # unused, removed 2020-12-12
+      #tvheadend = 200; # dynamically allocated as of 2021-09-18
+      uwsgi = 201;
+      gitit = 202;
+      riemanntools = 203;
+      subsonic = 204;
+      riak = 205;
+      #shout = 206; # dynamically allocated as of 2021-09-18
+      gateone = 207;
+      namecoin = 208;
+      #lxd = 210; # unused
+      #kibana = 211;# dynamically allocated as of 2021-09-03
+      xtreemfs = 212;
+      calibre-server = 213;
+      #heapster = 214; #dynamically allocated as of 2021-09-17
+      bepasty = 215;
+      # pumpio = 216; # unused, removed 2018-02-24
+      nm-openvpn = 217;
+      # mathics = 218; # unused, removed 2020-08-15
+      ejabberd = 219;
+      postsrsd = 220;
+      opendkim = 221;
+      dspam = 222;
+      # gale = 223; removed 2021-06-10
+      matrix-synapse = 224;
+      rspamd = 225;
+      # rmilter = 226; # unused, removed 2019-08-22
+      cfdyndns = 227;
+      # gammu-smsd = 228; #dynamically allocated as of 2021-09-17
+      pdnsd = 229;
+      octoprint = 230;
+      avahi-autoipd = 231;
+      # nntp-proxy = 232; #dynamically allocated as of 2021-09-17
+      mjpg-streamer = 233;
+      #radicale = 234;# dynamically allocated as of 2021-09-03
+      hydra-queue-runner = 235;
+      hydra-www = 236;
+      syncthing = 237;
+      caddy = 239;
+      taskd = 240;
+      # factorio = 241; # DynamicUser = true
+      # emby = 242; # unusued, removed 2019-05-01
+      #graylog = 243;# dynamically allocated as of 2021-09-03
+      sniproxy = 244;
+      nzbget = 245;
+      mosquitto = 246;
+      #toxvpn = 247; # dynamically allocated as of 2021-09-18
+      # squeezelite = 248; # DynamicUser = true
+      turnserver = 249;
+      #smokeping = 250;# dynamically allocated as of 2021-09-03
+      gocd-agent = 251;
+      gocd-server = 252;
+      terraria = 253;
+      mattermost = 254;
+      prometheus = 255;
+      telegraf = 256;
+      gitlab-runner = 257;
+      postgrey = 258;
+      hound = 259;
+      leaps = 260;
+      ipfs  = 261;
+      # stanchion = 262; # unused, removed 2020-10-14
+      # riak-cs = 263; # unused, removed 2020-10-14
+      infinoted = 264;
+      sickbeard = 265;
+      headphones = 266;
+      # couchpotato = 267; # unused, removed 2022-01-01
+      gogs = 268;
+      #pdns-recursor = 269; # dynamically allocated as of 2020-20-18
+      #kresd = 270; # switched to "knot-resolver" with dynamic ID
+      rpc = 271;
+      #geoip = 272; # new module uses DynamicUser
+      fcron = 273;
+      sonarr = 274;
+      radarr = 275;
+      jackett = 276;
+      aria2 = 277;
+      clickhouse = 278;
+      rslsync = 279;
+      minio = 280;
+      kanboard = 281;
+      # pykms = 282; # DynamicUser = true
+      kodi = 283;
+      restya-board = 284;
+      mighttpd2 = 285;
+      hass = 286;
+      #monero = 287; # dynamically allocated as of 2021-05-08
+      ceph = 288;
+      duplicati = 289;
+      monetdb = 290;
+      restic = 291;
+      openvpn = 292;
+      # meguca = 293; # removed 2020-08-21
+      yarn = 294;
+      hdfs = 295;
+      mapred = 296;
+      hadoop = 297;
+      hydron = 298;
+      cfssl = 299;
+      cassandra = 300;
+      qemu-libvirtd = 301;
+      # kvm = 302; # unused
+      # render = 303; # unused
+      # zeronet = 304; # removed 2019-01-03
+      lirc = 305;
+      lidarr = 306;
+      slurm = 307;
+      kapacitor = 308;
+      solr = 309;
+      alerta = 310;
+      minetest = 311;
+      rss2email = 312;
+      cockroachdb = 313;
+      zoneminder = 314;
+      paperless = 315;
+      #mailman = 316;  # removed 2019-08-30
+      zigbee2mqtt = 317;
+      # shadow = 318; # unused
+      hqplayer = 319;
+      moonraker = 320;
+      distcc = 321;
+      webdav = 322;
+      pipewire = 323;
+      rstudio-server = 324;
+
+      # When adding a uid, make sure it doesn't match an existing gid. And don't use uids above 399!
+
+      nixbld = 30000; # start of range of uids
+      nobody = 65534;
+    };
+
+    ids.gids = {
+      root = 0;
+      wheel = 1;
+      kmem = 2;
+      tty = 3;
+      messagebus = 4; # D-Bus
+      haldaemon = 5;
+      disk = 6;
+      #vsftpd = 7; # dynamically allocated as of 2021-09-14
+      ftp = 8;
+      # bitlbee = 9; # removed 2021-10-05 #139765
+      #avahi = 10; # removed 2019-05-22
+      #nagios = 11; # unused
+      atd = 12;
+      postfix = 13;
+      postdrop = 14;
+      dovecot = 15;
+      tomcat = 16;
+      audio = 17;
+      floppy = 18;
+      uucp = 19;
+      lp = 20;
+      proc = 21;
+      pulseaudio = 22; # must match `pulseaudio' UID
+      gpsd = 23;
+      cdrom = 24;
+      tape = 25;
+      video = 26;
+      dialout = 27;
+      #polkituser = 28; # currently unused, polkitd doesn't need a group
+      utmp = 29;
+      # ddclient = 30; # converted to DynamicUser = true
+      davfs2 = 31;
+      disnix = 33;
+      osgi = 34;
+      tor = 35;
+      #cups = 36; # unused
+      #foldingathome = 37; # unused
+      #sabnzd = 38; # unused
+      #kdm = 39; # unused, even before 17.03
+      #ghostone = 40; # dropped in 18.03
+      git = 41;
+      fourstore = 42;
+      fourstorehttp = 43;
+      virtuoso = 44;
+      #rtkit = 45; # unused
+      dovecot2 = 46;
+      dovenull2 = 47;
+      prayer = 49;
+      mpd = 50;
+      clamav = 51;
+      #fprot = 52; # unused
+      #bind = 53; # unused
+      wwwrun = 54;
+      adm = 55;
+      spamd = 56;
+      networkmanager = 57;
+      nslcd = 58;
+      scanner = 59;
+      nginx = 60;
+      chrony = 61;
+      systemd-journal = 62;
+      smtpd = 63;
+      smtpq = 64;
+      supybot = 65;
+      iodined = 66;
+      libvirtd = 67;
+      graphite = 68;
+      #statsd = 69; # removed 2018-11-14
+      transmission = 70;
+      postgres = 71;
+      vboxusers = 72;
+      vboxsf = 73;
+      smbguest = 74;  # unused
+      varnish = 75;
+      datadog = 76;
+      lighttpd = 77;
+      lightdm = 78;
+      freenet = 79;
+      ircd = 80;
+      bacula = 81;
+      #almir = 82; # removed 2018-03-25, the almir package was removed in 30291227f2411abaca097773eedb49b8f259e297 during 2017-08
+      deluge = 83;
+      mysql = 84;
+      rabbitmq = 85;
+      activemq = 86;
+      gnunet = 87;
+      oidentd = 88;
+      quassel = 89;
+      amule = 90;
+      minidlna = 91;
+      elasticsearch = 92;
+      #tcpcryptd = 93; # unused
+      firebird = 95;
+      keys = 96;
+      #haproxy = 97; # dynamically allocated as of 2020-03-11
+      #mongodb = 98; # unused
+      #openldap = 99; # dynamically allocated as of PR#94610
+      munin = 102;
+      #logcheck = 103; # unused
+      #nix-ssh = 104; # unused
+      dictd = 105;
+      couchdb = 106;
+      #searx = 107; # dynamically allocated as of 2020-10-27
+      #kippo = 108; # removed 2021-10-07, the kippo package was removed in 1b213f321cdbfcf868b96fd9959c24207ce1b66a during 2021-04
+      jenkins = 109;
+      systemd-journal-gateway = 110;
+      #notbit = 111; # unused
+      aerospike = 111;
+      #ngircd = 112; # unused
+      #btsync = 113; # unused
+      #minecraft = 114; # unused
+      vault = 115;
+      #ripped = 116; # unused
+      murmur = 117;
+      foundationdb = 118;
+      newrelic = 119;
+      starbound = 120;
+      hydra = 122;
+      spiped = 123;
+      teamspeak = 124;
+      influxdb = 125;
+      nsd = 126;
+      gitolite = 127;
+      znc = 128;
+      polipo = 129;
+      mopidy = 130;
+      docker = 131;
+      gdm = 132;
+      #dhcpcd = 133; # unused
+      siproxd = 134;
+      mlmmj = 135;
+      #neo4j = 136; # unused
+      riemann = 137;
+      riemanndash = 138;
+      #radvd = 139; # unused
+      #zookeeper = 140; # unused
+      #dnsmasq = 141; # unused
+      uhub = 142;
+      #yandexdisk = 143; # unused
+      mxisd = 144; # was once collectd
+      #consul = 145; # unused
+      #mailpile = 146; # removed 2022-01-12
+      redmine = 147;
+      #seeks = 148; # removed 2020-06-21
+      prosody = 149;
+      i2pd = 150;
+      systemd-network = 152;
+      systemd-resolve = 153;
+      systemd-timesync = 154;
+      liquidsoap = 155;
+      #etcd = 156; # unused
+      hbase = 158;
+      opentsdb = 159;
+      scollector = 160;
+      bosun = 161;
+      kubernetes = 162;
+      #peerflix = 163; # unused
+      #chronos = 164; # unused
+      gitlab = 165;
+      nylon = 168;
+      #panamax = 170; # unused
+      exim = 172;
+      #fleet = 173; # unused
+      input = 174;
+      sddm = 175;
+      #tss = 176; #dynamically allocateda as of 2021-09-20
+      #memcached = 177; # unused, removed 2018-01-03
+      #ntp = 179; # unused
+      zabbix = 180;
+      #redis = 181; # unused, removed 2018-01-03
+      #unifi = 183; # unused
+      #uptimed = 184; # unused
+      #zope2 = 185; # unused
+      #ripple-data-api = 186; #unused
+      mediatomb = 187;
+      #rdnssd = 188; # unused
+      ihaskell = 189;
+      i2p = 190;
+      lambdabot = 191;
+      asterisk = 192;
+      plex = 193;
+      sabnzbd = 194;
+      #grafana = 196; #unused
+      #skydns = 197; #unused
+      # ripple-rest = 198; # unused, removed 2017-08-12
+      #nix-serve = 199; #unused
+      #tvheadend = 200; #unused
+      uwsgi = 201;
+      gitit = 202;
+      riemanntools = 203;
+      subsonic = 204;
+      riak = 205;
+      #shout = 206; #unused
+      gateone = 207;
+      namecoin = 208;
+      #lxd = 210; # unused
+      #kibana = 211;
+      xtreemfs = 212;
+      calibre-server = 213;
+      bepasty = 215;
+      # pumpio = 216; # unused, removed 2018-02-24
+      nm-openvpn = 217;
+      mathics = 218;
+      ejabberd = 219;
+      postsrsd = 220;
+      opendkim = 221;
+      dspam = 222;
+      # gale = 223; removed 2021-06-10
+      matrix-synapse = 224;
+      rspamd = 225;
+      # rmilter = 226; # unused, removed 2019-08-22
+      cfdyndns = 227;
+      pdnsd = 229;
+      octoprint = 230;
+      #radicale = 234;# dynamically allocated as of 2021-09-03
+      syncthing = 237;
+      caddy = 239;
+      taskd = 240;
+      # factorio = 241; # unused
+      # emby = 242; # unused, removed 2019-05-01
+      sniproxy = 244;
+      nzbget = 245;
+      mosquitto = 246;
+      #toxvpn = 247; # unused
+      #squeezelite = 248; #unused
+      turnserver = 249;
+      #smokeping = 250;# dynamically allocated as of 2021-09-03
+      gocd-agent = 251;
+      gocd-server = 252;
+      terraria = 253;
+      mattermost = 254;
+      prometheus = 255;
+      #telegraf = 256; # unused
+      gitlab-runner = 257;
+      postgrey = 258;
+      hound = 259;
+      leaps = 260;
+      ipfs = 261;
+      # stanchion = 262; # unused, removed 2020-10-14
+      # riak-cs = 263; # unused, removed 2020-10-14
+      infinoted = 264;
+      sickbeard = 265;
+      headphones = 266;
+      # couchpotato = 267; # unused, removed 2022-01-01
+      gogs = 268;
+      #kresd = 270; # switched to "knot-resolver" with dynamic ID
+      #rpc = 271; # unused
+      #geoip = 272; # unused
+      fcron = 273;
+      sonarr = 274;
+      radarr = 275;
+      jackett = 276;
+      aria2 = 277;
+      clickhouse = 278;
+      rslsync = 279;
+      minio = 280;
+      kanboard = 281;
+      # pykms = 282; # DynamicUser = true
+      kodi = 283;
+      restya-board = 284;
+      mighttpd2 = 285;
+      hass = 286;
+      # monero = 287; # dynamically allocated as of 2021-05-08
+      ceph = 288;
+      duplicati = 289;
+      monetdb = 290;
+      restic = 291;
+      openvpn = 292;
+      # meguca = 293; # removed 2020-08-21
+      yarn = 294;
+      hdfs = 295;
+      mapred = 296;
+      hadoop = 297;
+      hydron = 298;
+      cfssl = 299;
+      cassandra = 300;
+      qemu-libvirtd = 301;
+      kvm = 302; # default udev rules from systemd requires these
+      render = 303; # default udev rules from systemd requires these
+      sgx = 304; # default udev rules from systemd requires these
+      lirc = 305;
+      lidarr = 306;
+      slurm = 307;
+      kapacitor = 308;
+      solr = 309;
+      alerta = 310;
+      minetest = 311;
+      rss2email = 312;
+      cockroachdb = 313;
+      zoneminder = 314;
+      paperless = 315;
+      #mailman = 316;  # removed 2019-08-30
+      zigbee2mqtt = 317;
+      shadow = 318;
+      hqplayer = 319;
+      moonraker = 320;
+      distcc = 321;
+      webdav = 322;
+      pipewire = 323;
+      rstudio-server = 324;
+
+      # When adding a gid, make sure it doesn't match an existing
+      # uid. Users and groups with the same name should have equal
+      # uids and gids. Also, don't use gids above 399!
+
+      users = 100;
+      nixbld = 30000;
+      nogroup = 65534;
+    };
+
+  };
+
+}
diff --git a/nixos/modules/misc/label.nix b/nixos/modules/misc/label.nix
new file mode 100644
index 00000000000..02b91555b3c
--- /dev/null
+++ b/nixos/modules/misc/label.nix
@@ -0,0 +1,72 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.system.nixos;
+in
+
+{
+
+  options.system = {
+
+    nixos.label = mkOption {
+      type = types.str;
+      description = ''
+        NixOS version name to be used in the names of generated
+        outputs and boot labels.
+
+        If you ever wanted to influence the labels in your GRUB menu,
+        this is the option for you.
+
+        The default is <option>system.nixos.tags</option> separated by
+        "-" + "-" + <envar>NIXOS_LABEL_VERSION</envar> environment
+        variable (defaults to the value of
+        <option>system.nixos.version</option>).
+
+        Can be overriden by setting <envar>NIXOS_LABEL</envar>.
+
+        Useful for not loosing track of configurations built from different
+        nixos branches/revisions, e.g.:
+
+        <screen>
+        #!/bin/sh
+        today=`date +%Y%m%d`
+        branch=`(cd nixpkgs ; git branch 2>/dev/null | sed -n '/^\* / { s|^\* ||; p; }')`
+        revision=`(cd nixpkgs ; git rev-parse HEAD)`
+        export NIXOS_LABEL_VERSION="$today.$branch-''${revision:0:7}"
+        nixos-rebuild switch</screen>
+      '';
+    };
+
+    nixos.tags = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "with-xen" ];
+      description = ''
+        Strings to prefix to the default
+        <option>system.nixos.label</option>.
+
+        Useful for not loosing track of configurations built with
+        different options, e.g.:
+
+        <screen>
+        {
+          system.nixos.tags = [ "with-xen" ];
+          virtualisation.xen.enable = true;
+        }
+        </screen>
+      '';
+    };
+
+  };
+
+  config = {
+    # This is set here rather than up there so that changing it would
+    # not rebuild the manual
+    system.nixos.label = mkDefault (maybeEnv "NIXOS_LABEL"
+                                             (concatStringsSep "-" ((sort (x: y: x < y) cfg.tags)
+                                              ++ [ (maybeEnv "NIXOS_LABEL_VERSION" cfg.version) ])));
+  };
+
+}
diff --git a/nixos/modules/misc/lib.nix b/nixos/modules/misc/lib.nix
new file mode 100644
index 00000000000..121f396701e
--- /dev/null
+++ b/nixos/modules/misc/lib.nix
@@ -0,0 +1,15 @@
+{ lib, ... }:
+
+{
+  options = {
+    lib = lib.mkOption {
+      default = {};
+
+      type = lib.types.attrsOf lib.types.attrs;
+
+      description = ''
+        This option allows modules to define helper functions, constants, etc.
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/misc/locate.nix b/nixos/modules/misc/locate.nix
new file mode 100644
index 00000000000..204a8914300
--- /dev/null
+++ b/nixos/modules/misc/locate.nix
@@ -0,0 +1,313 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.locate;
+  isMLocate = hasPrefix "mlocate" cfg.locate.name;
+  isPLocate = hasPrefix "plocate" cfg.locate.name;
+  isMorPLocate = (isMLocate || isPLocate);
+  isFindutils = hasPrefix "findutils" cfg.locate.name;
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "locate" "period" ] [ "services" "locate" "interval" ])
+    (mkRemovedOptionModule [ "services" "locate" "includeStore" ] "Use services.locate.prunePaths")
+  ];
+
+  options.services.locate = with types; {
+    enable = mkOption {
+      type = bool;
+      default = false;
+      description = ''
+        If enabled, NixOS will periodically update the database of
+        files used by the <command>locate</command> command.
+      '';
+    };
+
+    locate = mkOption {
+      type = package;
+      default = pkgs.findutils;
+      defaultText = literalExpression "pkgs.findutils";
+      example = literalExpression "pkgs.mlocate";
+      description = ''
+        The locate implementation to use
+      '';
+    };
+
+    interval = mkOption {
+      type = str;
+      default = "02:15";
+      example = "hourly";
+      description = ''
+        Update the locate database at this interval. Updates by
+        default at 2:15 AM every day.
+
+        The format is described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.
+
+        To disable automatic updates, set to <literal>"never"</literal>
+        and run <command>updatedb</command> manually.
+      '';
+    };
+
+    extraFlags = mkOption {
+      type = listOf str;
+      default = [ ];
+      description = ''
+        Extra flags to pass to <command>updatedb</command>.
+      '';
+    };
+
+    output = mkOption {
+      type = path;
+      default = "/var/cache/locatedb";
+      description = ''
+        The database file to build.
+      '';
+    };
+
+    localuser = mkOption {
+      type = nullOr str;
+      default = "nobody";
+      description = ''
+        The user to search non-network directories as, using
+        <command>su</command>.
+      '';
+    };
+
+    pruneFS = mkOption {
+      type = listOf str;
+      default = [
+        "afs"
+        "anon_inodefs"
+        "auto"
+        "autofs"
+        "bdev"
+        "binfmt"
+        "binfmt_misc"
+        "ceph"
+        "cgroup"
+        "cgroup2"
+        "cifs"
+        "coda"
+        "configfs"
+        "cramfs"
+        "cpuset"
+        "curlftpfs"
+        "debugfs"
+        "devfs"
+        "devpts"
+        "devtmpfs"
+        "ecryptfs"
+        "eventpollfs"
+        "exofs"
+        "futexfs"
+        "ftpfs"
+        "fuse"
+        "fusectl"
+        "fusesmb"
+        "fuse.ceph"
+        "fuse.glusterfs"
+        "fuse.gvfsd-fuse"
+        "fuse.mfs"
+        "fuse.rclone"
+        "fuse.rozofs"
+        "fuse.sshfs"
+        "gfs"
+        "gfs2"
+        "hostfs"
+        "hugetlbfs"
+        "inotifyfs"
+        "iso9660"
+        "jffs2"
+        "lustre"
+        "lustre_lite"
+        "misc"
+        "mfs"
+        "mqueue"
+        "ncpfs"
+        "nfs"
+        "NFS"
+        "nfs4"
+        "nfsd"
+        "nnpfs"
+        "ocfs"
+        "ocfs2"
+        "pipefs"
+        "proc"
+        "ramfs"
+        "rpc_pipefs"
+        "securityfs"
+        "selinuxfs"
+        "sfs"
+        "shfs"
+        "smbfs"
+        "sockfs"
+        "spufs"
+        "sshfs"
+        "subfs"
+        "supermount"
+        "sysfs"
+        "tmpfs"
+        "tracefs"
+        "ubifs"
+        "udev"
+        "udf"
+        "usbfs"
+        "vboxsf"
+        "vperfctrfs"
+      ];
+      description = ''
+        Which filesystem types to exclude from indexing
+      '';
+    };
+
+    prunePaths = mkOption {
+      type = listOf path;
+      default = [
+        "/tmp"
+        "/var/tmp"
+        "/var/cache"
+        "/var/lock"
+        "/var/run"
+        "/var/spool"
+        "/nix/store"
+        "/nix/var/log/nix"
+      ];
+      description = ''
+        Which paths to exclude from indexing
+      '';
+    };
+
+    pruneNames = mkOption {
+      type = listOf str;
+      default = lib.optionals (!isFindutils) [ ".bzr" ".cache" ".git" ".hg" ".svn" ];
+      defaultText = literalDocBook ''
+        <literal>[ ".bzr" ".cache" ".git" ".hg" ".svn" ]</literal>, if
+        supported by the locate implementation (i.e. mlocate or plocate).
+      '';
+      description = ''
+        Directory components which should exclude paths containing them from indexing
+      '';
+    };
+
+    pruneBindMounts = mkOption {
+      type = bool;
+      default = false;
+      description = ''
+        Whether not to index bind mounts
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    users.groups = mkMerge [
+      (mkIf isMLocate { mlocate = { }; })
+      (mkIf isPLocate { plocate = { }; })
+    ];
+
+    security.wrappers =
+      let
+        common = {
+          owner = "root";
+          permissions = "u+rx,g+x,o+x";
+          setgid = true;
+          setuid = false;
+        };
+        mlocate = (mkIf isMLocate {
+          group = "mlocate";
+          source = "${cfg.locate}/bin/locate";
+        });
+        plocate = (mkIf isPLocate {
+          group = "plocate";
+          source = "${cfg.locate}/bin/plocate";
+        });
+      in
+      mkIf isMorPLocate {
+        locate = mkMerge [ common mlocate plocate ];
+        plocate = (mkIf isPLocate (mkMerge [ common plocate ]));
+      };
+
+    nixpkgs.config = { locate.dbfile = cfg.output; };
+
+    environment.systemPackages = [ cfg.locate ];
+
+    environment.variables = mkIf (!isMorPLocate) { LOCATE_PATH = cfg.output; };
+
+    environment.etc = {
+      # write /etc/updatedb.conf for manual calls to `updatedb`
+      "updatedb.conf" = {
+        text = ''
+          PRUNEFS="${lib.concatStringsSep " " cfg.pruneFS}"
+          PRUNENAMES="${lib.concatStringsSep " " cfg.pruneNames}"
+          PRUNEPATHS="${lib.concatStringsSep " " cfg.prunePaths}"
+          PRUNE_BIND_MOUNTS="${if cfg.pruneBindMounts then "yes" else "no"}"
+        '';
+      };
+    };
+
+    warnings = optional (isMorPLocate && cfg.localuser != null)
+      "mlocate does not support the services.locate.localuser option; updatedb will run as root. (Silence with services.locate.localuser = null.)"
+    ++ optional (isFindutils && cfg.pruneNames != [ ])
+      "findutils locate does not support pruning by directory component"
+    ++ optional (isFindutils && cfg.pruneBindMounts)
+      "findutils locate does not support skipping bind mounts";
+
+    systemd.services.update-locatedb = {
+      description = "Update Locate Database";
+      path = mkIf (!isMorPLocate) [ pkgs.su ];
+
+      # mlocate's updatedb takes flags via a configuration file or
+      # on the command line, but not by environment variable.
+      script =
+        if isMorPLocate then
+          let
+            toFlags = x:
+              optional (cfg.${x} != [ ])
+                "--${lib.toLower x} '${concatStringsSep " " cfg.${x}}'";
+            args = concatLists (map toFlags [ "pruneFS" "pruneNames" "prunePaths" ]);
+          in
+          ''
+            exec ${cfg.locate}/bin/updatedb \
+              --output ${toString cfg.output} ${concatStringsSep " " args} \
+              --prune-bind-mounts ${if cfg.pruneBindMounts then "yes" else "no"} \
+              ${concatStringsSep " " cfg.extraFlags}
+          ''
+        else ''
+          exec ${cfg.locate}/bin/updatedb \
+            ${optionalString (cfg.localuser != null && !isMorPLocate) "--localuser=${cfg.localuser}"} \
+            --output=${toString cfg.output} ${concatStringsSep " " cfg.extraFlags}
+        '';
+      environment = optionalAttrs (!isMorPLocate) {
+        PRUNEFS = concatStringsSep " " cfg.pruneFS;
+        PRUNEPATHS = concatStringsSep " " cfg.prunePaths;
+        PRUNENAMES = concatStringsSep " " cfg.pruneNames;
+        PRUNE_BIND_MOUNTS = if cfg.pruneBindMounts then "yes" else "no";
+      };
+      serviceConfig.Nice = 19;
+      serviceConfig.IOSchedulingClass = "idle";
+      serviceConfig.PrivateTmp = "yes";
+      serviceConfig.PrivateNetwork = "yes";
+      serviceConfig.NoNewPrivileges = "yes";
+      serviceConfig.ReadOnlyPaths = "/";
+      # Use dirOf cfg.output because mlocate creates temporary files next to
+      # the actual database. We could specify and create them as well,
+      # but that would make this quite brittle when they change something.
+      # NOTE: If /var/cache does not exist, this leads to the misleading error message:
+      # update-locatedb.service: Failed at step NAMESPACE spawning …/update-locatedb-start: No such file or directory
+      serviceConfig.ReadWritePaths = dirOf cfg.output;
+    };
+
+    systemd.timers.update-locatedb = mkIf (cfg.interval != "never") {
+      description = "Update timer for locate database";
+      partOf = [ "update-locatedb.service" ];
+      wantedBy = [ "timers.target" ];
+      timerConfig.OnCalendar = cfg.interval;
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ SuperSandro2000 ];
+}
diff --git a/nixos/modules/misc/man-db.nix b/nixos/modules/misc/man-db.nix
new file mode 100644
index 00000000000..8bd329bc4e0
--- /dev/null
+++ b/nixos/modules/misc/man-db.nix
@@ -0,0 +1,73 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.documentation.man.man-db;
+in
+
+{
+  options = {
+    documentation.man.man-db = {
+      enable = lib.mkEnableOption "man-db as the default man page viewer" // {
+        default = config.documentation.man.enable;
+        defaultText = lib.literalExpression "config.documentation.man.enable";
+        example = false;
+      };
+
+      manualPages = lib.mkOption {
+        type = lib.types.path;
+        default = pkgs.buildEnv {
+          name = "man-paths";
+          paths = config.environment.systemPackages;
+          pathsToLink = [ "/share/man" ];
+          extraOutputsToInstall = [ "man" ]
+            ++ lib.optionals config.documentation.dev.enable [ "devman" ];
+          ignoreCollisions = true;
+        };
+        defaultText = lib.literalDocBook "all man pages in <option>config.environment.systemPackages</option>";
+        description = ''
+          The manual pages to generate caches for if <option>documentation.man.generateCaches</option>
+          is enabled. Must be a path to a directory with man pages under
+          <literal>/share/man</literal>; see the source for an example.
+          Advanced users can make this a content-addressed derivation to save a few rebuilds.
+        '';
+      };
+
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.man-db;
+        defaultText = lib.literalExpression "pkgs.man-db";
+        description = ''
+          The <literal>man-db</literal> derivation to use. Useful to override
+          configuration options used for the package.
+        '';
+      };
+    };
+  };
+
+  imports = [
+    (lib.mkRenamedOptionModule [ "documentation" "man" "manualPages" ] [ "documentation" "man" "man-db" "manualPages" ])
+  ];
+
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    environment.etc."man_db.conf".text =
+      let
+        manualCache = pkgs.runCommandLocal "man-cache" { } ''
+          echo "MANDB_MAP ${cfg.manualPages}/share/man $out" > man.conf
+          ${cfg.package}/bin/mandb -C man.conf -psc >/dev/null 2>&1
+        '';
+      in
+      ''
+        # Manual pages paths for NixOS
+        MANPATH_MAP /run/current-system/sw/bin /run/current-system/sw/share/man
+        MANPATH_MAP /run/wrappers/bin          /run/current-system/sw/share/man
+
+        ${lib.optionalString config.documentation.man.generateCaches ''
+        # Generated manual pages cache for NixOS (immutable)
+        MANDB_MAP /run/current-system/sw/share/man ${manualCache}
+        ''}
+        # Manual pages caches for NixOS
+        MANDB_MAP /run/current-system/sw/share/man /var/cache/man/nixos
+      '';
+  };
+}
diff --git a/nixos/modules/misc/mandoc.nix b/nixos/modules/misc/mandoc.nix
new file mode 100644
index 00000000000..3da60f2f8e6
--- /dev/null
+++ b/nixos/modules/misc/mandoc.nix
@@ -0,0 +1,61 @@
+{ config, lib, pkgs, ... }:
+
+let
+  makewhatis = "${lib.getBin cfg.package}/bin/makewhatis";
+
+  cfg = config.documentation.man.mandoc;
+
+in {
+  meta.maintainers = [ lib.maintainers.sternenseemann ];
+
+  options = {
+    documentation.man.mandoc = {
+      enable = lib.mkEnableOption "mandoc as the default man page viewer";
+
+      manPath = lib.mkOption {
+        type = with lib.types; listOf str;
+        default = [ "share/man" ];
+        example = lib.literalExpression "[ \"share/man\" \"share/man/fr\" ]";
+        description = ''
+          Change the manpath, i. e. the directories where
+          <citerefentry><refentrytitle>man</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+          looks for section-specific directories of man pages.
+          You only need to change this setting if you want extra man pages
+          (e. g. in non-english languages). All values must be strings that
+          are a valid path from the target prefix (without including it).
+          The first value given takes priority.
+        '';
+      };
+
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.mandoc;
+        defaultText = lib.literalExpression "pkgs.mandoc";
+        description = ''
+          The <literal>mandoc</literal> derivation to use. Useful to override
+          configuration options used for the package.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment = {
+      systemPackages = [ cfg.package ];
+
+      # tell mandoc about man pages
+      etc."man.conf".text = lib.concatMapStrings (path: ''
+        manpath /run/current-system/sw/${path}
+      '') cfg.manPath;
+
+      # create mandoc.db for whatis(1), apropos(1) and man(1) -k
+      # TODO(@sternenseemman): fix symlinked directories not getting indexed,
+      # see: https://inbox.vuxu.org/mandoc-tech/20210906171231.GF83680@athene.usta.de/T/#e85f773c1781e3fef85562b2794f9cad7b2909a3c
+      extraSetup = lib.mkIf config.documentation.man.generateCaches ''
+        ${makewhatis} -T utf8 ${
+          lib.concatMapStringsSep " " (path: "\"$out/${path}\"") cfg.manPath
+        }
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/misc/meta.nix b/nixos/modules/misc/meta.nix
new file mode 100644
index 00000000000..8e689a63f6b
--- /dev/null
+++ b/nixos/modules/misc/meta.nix
@@ -0,0 +1,76 @@
+{ lib, ... }:
+
+with lib;
+
+let
+  maintainer = mkOptionType {
+    name = "maintainer";
+    check = email: elem email (attrValues lib.maintainers);
+    merge = loc: defs: listToAttrs (singleton (nameValuePair (last defs).file (last defs).value));
+  };
+
+  listOfMaintainers = types.listOf maintainer // {
+    # Returns list of
+    #   { "module-file" = [
+    #        "maintainer1 <first@nixos.org>"
+    #        "maintainer2 <second@nixos.org>" ];
+    #   }
+    merge = loc: defs:
+      zipAttrs
+        (flatten (imap1 (n: def: imap1 (m: def':
+          maintainer.merge (loc ++ ["[${toString n}-${toString m}]"])
+            [{ inherit (def) file; value = def'; }]) def.value) defs));
+  };
+
+  docFile = types.path // {
+    # Returns tuples of
+    #   { file = "module location"; value = <path/to/doc.xml>; }
+    merge = loc: defs: defs;
+  };
+in
+
+{
+  options = {
+    meta = {
+
+      maintainers = mkOption {
+        type = listOfMaintainers;
+        internal = true;
+        default = [];
+        example = literalExpression ''[ lib.maintainers.all ]'';
+        description = ''
+          List of maintainers of each module.  This option should be defined at
+          most once per module.
+        '';
+      };
+
+      doc = mkOption {
+        type = docFile;
+        internal = true;
+        example = "./meta.chapter.xml";
+        description = ''
+          Documentation prologue for the set of options of each module.  This
+          option should be defined at most once per module.
+        '';
+      };
+
+      buildDocsInSandbox = mkOption {
+        type = types.bool // {
+          merge = loc: defs: defs;
+        };
+        internal = true;
+        default = true;
+        description = ''
+          Whether to include this module in the split options doc build.
+          Disable if the module references `config`, `pkgs` or other module
+          arguments that cannot be evaluated as constants.
+
+          This option should be defined at most once per module.
+        '';
+      };
+
+    };
+  };
+
+  meta.maintainers = singleton lib.maintainers.pierron;
+}
diff --git a/nixos/modules/misc/nixops-autoluks.nix b/nixos/modules/misc/nixops-autoluks.nix
new file mode 100644
index 00000000000..20c143286af
--- /dev/null
+++ b/nixos/modules/misc/nixops-autoluks.nix
@@ -0,0 +1,43 @@
+{ config, options, lib, ... }:
+let
+  path = [ "deployment" "autoLuks" ];
+  hasAutoLuksConfig = lib.hasAttrByPath path config && (lib.attrByPath path {} config) != {};
+
+  inherit (config.nixops) enableDeprecatedAutoLuks;
+in {
+  options.nixops.enableDeprecatedAutoLuks = lib.mkEnableOption "Enable the deprecated NixOps AutoLuks module";
+
+  config = {
+    assertions = [
+      {
+        assertion = if hasAutoLuksConfig then hasAutoLuksConfig && enableDeprecatedAutoLuks else true;
+        message = ''
+          âš ï¸  !!! WARNING !!! âš ï¸
+
+            NixOps autoLuks is deprecated. The feature was never widely used and the maintenance did outgrow the benefit.
+            If you still want to use the module:
+              a) Please raise your voice in the issue tracking usage of the module:
+                 https://github.com/NixOS/nixpkgs/issues/62211
+              b) make sure you set the `_netdev` option for each of the file
+                 systems referring to block devices provided by the autoLuks module.
+
+                 âš ï¸ If you do not set the option your system will not boot anymore! âš ï¸
+
+                  {
+                    fileSystems."/secret" = { options = [ "_netdev" ]; };
+                  }
+
+              b) set the option >nixops.enableDeprecatedAutoLuks = true< to remove this error.
+
+
+            For more details read through the following resources:
+              - https://github.com/NixOS/nixops/pull/1156
+              - https://github.com/NixOS/nixpkgs/issues/47550
+              - https://github.com/NixOS/nixpkgs/issues/62211
+              - https://github.com/NixOS/nixpkgs/pull/61321
+        '';
+      }
+    ];
+  };
+
+}
diff --git a/nixos/modules/misc/nixpkgs.nix b/nixos/modules/misc/nixpkgs.nix
new file mode 100644
index 00000000000..69967c8a760
--- /dev/null
+++ b/nixos/modules/misc/nixpkgs.nix
@@ -0,0 +1,259 @@
+{ config, options, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.nixpkgs;
+  opt = options.nixpkgs;
+
+  isConfig = x:
+    builtins.isAttrs x || lib.isFunction x;
+
+  optCall = f: x:
+    if lib.isFunction f
+    then f x
+    else f;
+
+  mergeConfig = lhs_: rhs_:
+    let
+      lhs = optCall lhs_ { inherit pkgs; };
+      rhs = optCall rhs_ { inherit pkgs; };
+    in
+    recursiveUpdate lhs rhs //
+    optionalAttrs (lhs ? packageOverrides) {
+      packageOverrides = pkgs:
+        optCall lhs.packageOverrides pkgs //
+        optCall (attrByPath ["packageOverrides"] ({}) rhs) pkgs;
+    } //
+    optionalAttrs (lhs ? perlPackageOverrides) {
+      perlPackageOverrides = pkgs:
+        optCall lhs.perlPackageOverrides pkgs //
+        optCall (attrByPath ["perlPackageOverrides"] ({}) rhs) pkgs;
+    };
+
+  configType = mkOptionType {
+    name = "nixpkgs-config";
+    description = "nixpkgs config";
+    check = x:
+      let traceXIfNot = c:
+            if c x then true
+            else lib.traceSeqN 1 x false;
+      in traceXIfNot isConfig;
+    merge = args: foldr (def: mergeConfig def.value) {};
+  };
+
+  overlayType = mkOptionType {
+    name = "nixpkgs-overlay";
+    description = "nixpkgs overlay";
+    check = lib.isFunction;
+    merge = lib.mergeOneOption;
+  };
+
+  pkgsType = mkOptionType {
+    name = "nixpkgs";
+    description = "An evaluation of Nixpkgs; the top level attribute set of packages";
+    check = builtins.isAttrs;
+  };
+
+  defaultPkgs = import ../../.. {
+    inherit (cfg) config overlays localSystem crossSystem;
+  };
+
+  finalPkgs = if opt.pkgs.isDefined then cfg.pkgs.appendOverlays cfg.overlays else defaultPkgs;
+
+in
+
+{
+  imports = [
+    ./assertions.nix
+    ./meta.nix
+  ];
+
+  options.nixpkgs = {
+
+    pkgs = mkOption {
+      defaultText = literalExpression ''
+        import "''${nixos}/.." {
+          inherit (cfg) config overlays localSystem crossSystem;
+        }
+      '';
+      type = pkgsType;
+      example = literalExpression "import <nixpkgs> {}";
+      description = ''
+        If set, the pkgs argument to all NixOS modules is the value of
+        this option, extended with <code>nixpkgs.overlays</code>, if
+        that is also set. Either <code>nixpkgs.crossSystem</code> or
+        <code>nixpkgs.localSystem</code> will be used in an assertion
+        to check that the NixOS and Nixpkgs architectures match. Any
+        other options in <code>nixpkgs.*</code>, notably <code>config</code>,
+        will be ignored.
+
+        If unset, the pkgs argument to all NixOS modules is determined
+        as shown in the default value for this option.
+
+        The default value imports the Nixpkgs source files
+        relative to the location of this NixOS module, because
+        NixOS and Nixpkgs are distributed together for consistency,
+        so the <code>nixos</code> in the default value is in fact a
+        relative path. The <code>config</code>, <code>overlays</code>,
+        <code>localSystem</code>, and <code>crossSystem</code> come
+        from this option's siblings.
+
+        This option can be used by applications like NixOps to increase
+        the performance of evaluation, or to create packages that depend
+        on a container that should be built with the exact same evaluation
+        of Nixpkgs, for example. Applications like this should set
+        their default value using <code>lib.mkDefault</code>, so
+        user-provided configuration can override it without using
+        <code>lib</code>.
+
+        Note that using a distinct version of Nixpkgs with NixOS may
+        be an unexpected source of problems. Use this option with care.
+      '';
+    };
+
+    config = mkOption {
+      default = {};
+      example = literalExpression
+        ''
+          { allowBroken = true; allowUnfree = true; }
+        '';
+      type = configType;
+      description = ''
+        The configuration of the Nix Packages collection.  (For
+        details, see the Nixpkgs documentation.)  It allows you to set
+        package configuration options.
+
+        Ignored when <code>nixpkgs.pkgs</code> is set.
+      '';
+    };
+
+    overlays = mkOption {
+      default = [];
+      example = literalExpression
+        ''
+          [
+            (self: super: {
+              openssh = super.openssh.override {
+                hpnSupport = true;
+                kerberos = self.libkrb5;
+              };
+            })
+          ]
+        '';
+      type = types.listOf overlayType;
+      description = ''
+        List of overlays to use with the Nix Packages collection.
+        (For details, see the Nixpkgs documentation.)  It allows
+        you to override packages globally. Each function in the list
+        takes as an argument the <emphasis>original</emphasis> Nixpkgs.
+        The first argument should be used for finding dependencies, and
+        the second should be used for overriding recipes.
+
+        If <code>nixpkgs.pkgs</code> is set, overlays specified here
+        will be applied after the overlays that were already present
+        in <code>nixpkgs.pkgs</code>.
+      '';
+    };
+
+    localSystem = mkOption {
+      type = types.attrs; # TODO utilize lib.systems.parsedPlatform
+      default = { inherit (cfg) system; };
+      example = { system = "aarch64-linux"; config = "aarch64-unknown-linux-gnu"; };
+      # Make sure that the final value has all fields for sake of other modules
+      # referring to this. TODO make `lib.systems` itself use the module system.
+      apply = lib.systems.elaborate;
+      defaultText = literalExpression
+        ''(import "''${nixos}/../lib").lib.systems.examples.aarch64-multiplatform'';
+      description = ''
+        Specifies the platform on which NixOS should be built. When
+        <code>nixpkgs.crossSystem</code> is unset, it also specifies
+        the platform <emphasis>for</emphasis> which NixOS should be
+        built.  If this option is unset, it defaults to the platform
+        type of the machine where evaluation happens. Specifying this
+        option is useful when doing distributed multi-platform
+        deployment, or when building virtual machines. See its
+        description in the Nixpkgs manual for more details.
+
+        Ignored when <code>nixpkgs.pkgs</code> is set.
+      '';
+    };
+
+    crossSystem = mkOption {
+      type = types.nullOr types.attrs; # TODO utilize lib.systems.parsedPlatform
+      default = null;
+      example = { system = "aarch64-linux"; config = "aarch64-unknown-linux-gnu"; };
+      description = ''
+        Specifies the platform for which NixOS should be
+        built. Specify this only if it is different from
+        <code>nixpkgs.localSystem</code>, the platform
+        <emphasis>on</emphasis> which NixOS should be built. In other
+        words, specify this to cross-compile NixOS. Otherwise it
+        should be set as null, the default. See its description in the
+        Nixpkgs manual for more details.
+
+        Ignored when <code>nixpkgs.pkgs</code> is set.
+      '';
+    };
+
+    system = mkOption {
+      type = types.str;
+      example = "i686-linux";
+      description = ''
+        Specifies the Nix platform type on which NixOS should be built.
+        It is better to specify <code>nixpkgs.localSystem</code> instead.
+        <programlisting>
+        {
+          nixpkgs.system = ..;
+        }
+        </programlisting>
+        is the same as
+        <programlisting>
+        {
+          nixpkgs.localSystem.system = ..;
+        }
+        </programlisting>
+        See <code>nixpkgs.localSystem</code> for more information.
+
+        Ignored when <code>nixpkgs.localSystem</code> is set.
+        Ignored when <code>nixpkgs.pkgs</code> is set.
+      '';
+    };
+
+    initialSystem = mkOption {
+      type = types.str;
+      internal = true;
+      description = ''
+        Preserved value of <literal>system</literal> passed to <literal>eval-config.nix</literal>.
+      '';
+    };
+  };
+
+  config = {
+    _module.args = {
+      pkgs = finalPkgs;
+    };
+
+    assertions = [
+      (
+        let
+          nixosExpectedSystem =
+            if config.nixpkgs.crossSystem != null
+            then config.nixpkgs.crossSystem.system or (lib.systems.parse.doubleFromSystem (lib.systems.parse.mkSystemFromString config.nixpkgs.crossSystem.config))
+            else config.nixpkgs.localSystem.system or (lib.systems.parse.doubleFromSystem (lib.systems.parse.mkSystemFromString config.nixpkgs.localSystem.config));
+          nixosOption =
+            if config.nixpkgs.crossSystem != null
+            then "nixpkgs.crossSystem"
+            else "nixpkgs.localSystem";
+          pkgsSystem = finalPkgs.stdenv.targetPlatform.system;
+        in {
+          assertion = nixosExpectedSystem == pkgsSystem;
+          message = "The NixOS nixpkgs.pkgs option was set to a Nixpkgs invocation that compiles to target system ${pkgsSystem} but NixOS was configured for system ${nixosExpectedSystem} via NixOS option ${nixosOption}. The NixOS system settings must match the Nixpkgs target system.";
+        }
+      )
+    ];
+  };
+
+  # needs a full nixpkgs path to import nixpkgs
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/misc/nixpkgs/test.nix b/nixos/modules/misc/nixpkgs/test.nix
new file mode 100644
index 00000000000..ec5fab9fb4a
--- /dev/null
+++ b/nixos/modules/misc/nixpkgs/test.nix
@@ -0,0 +1,8 @@
+{ evalMinimalConfig, pkgs, lib, stdenv }:
+lib.recurseIntoAttrs {
+  invokeNixpkgsSimple =
+    (evalMinimalConfig ({ config, modulesPath, ... }: {
+      imports = [ (modulesPath + "/misc/nixpkgs.nix") ];
+      nixpkgs.system = stdenv.hostPlatform.system;
+    }))._module.args.pkgs.hello;
+}
diff --git a/nixos/modules/misc/passthru.nix b/nixos/modules/misc/passthru.nix
new file mode 100644
index 00000000000..4e99631fdd8
--- /dev/null
+++ b/nixos/modules/misc/passthru.nix
@@ -0,0 +1,16 @@
+# This module allows you to export something from configuration
+# Use case: export kernel source expression for ease of configuring
+
+{ lib, ... }:
+
+{
+  options = {
+    passthru = lib.mkOption {
+      visible = false;
+      description = ''
+        This attribute set will be exported as a system attribute.
+        You can put whatever you want here.
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/misc/version.nix b/nixos/modules/misc/version.nix
new file mode 100644
index 00000000000..d825f4beb30
--- /dev/null
+++ b/nixos/modules/misc/version.nix
@@ -0,0 +1,141 @@
+{ config, lib, options, pkgs, ... }:
+
+let
+  cfg = config.system.nixos;
+  opt = options.system.nixos;
+
+  inherit (lib)
+    concatStringsSep mapAttrsToList toLower
+    literalExpression mkRenamedOptionModule mkDefault mkOption trivial types;
+
+  needsEscaping = s: null != builtins.match "[a-zA-Z0-9]+" s;
+  escapeIfNeccessary = s: if needsEscaping s then s else ''"${lib.escape [ "\$" "\"" "\\" "\`" ] s}"'';
+  attrsToText = attrs:
+    concatStringsSep "\n" (
+      mapAttrsToList (n: v: ''${n}=${escapeIfNeccessary (toString v)}'') attrs
+    );
+
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "system" "nixosVersion" ] [ "system" "nixos" "version" ])
+    (mkRenamedOptionModule [ "system" "nixosVersionSuffix" ] [ "system" "nixos" "versionSuffix" ])
+    (mkRenamedOptionModule [ "system" "nixosRevision" ] [ "system" "nixos" "revision" ])
+    (mkRenamedOptionModule [ "system" "nixosLabel" ] [ "system" "nixos" "label" ])
+  ];
+
+  options.system = {
+
+    nixos.version = mkOption {
+      internal = true;
+      type = types.str;
+      description = "The full NixOS version (e.g. <literal>16.03.1160.f2d4ee1</literal>).";
+    };
+
+    nixos.release = mkOption {
+      readOnly = true;
+      type = types.str;
+      default = trivial.release;
+      description = "The NixOS release (e.g. <literal>16.03</literal>).";
+    };
+
+    nixos.versionSuffix = mkOption {
+      internal = true;
+      type = types.str;
+      default = trivial.versionSuffix;
+      description = "The NixOS version suffix (e.g. <literal>1160.f2d4ee1</literal>).";
+    };
+
+    nixos.revision = mkOption {
+      internal = true;
+      type = types.nullOr types.str;
+      default = trivial.revisionWithDefault null;
+      description = "The Git revision from which this NixOS configuration was built.";
+    };
+
+    nixos.codeName = mkOption {
+      readOnly = true;
+      type = types.str;
+      default = trivial.codeName;
+      description = "The NixOS release code name (e.g. <literal>Emu</literal>).";
+    };
+
+    stateVersion = mkOption {
+      type = types.str;
+      default = cfg.release;
+      defaultText = literalExpression "config.${opt.release}";
+      description = ''
+        Every once in a while, a new NixOS release may change
+        configuration defaults in a way incompatible with stateful
+        data. For instance, if the default version of PostgreSQL
+        changes, the new version will probably be unable to read your
+        existing databases. To prevent such breakage, you should set the
+        value of this option to the NixOS release with which you want
+        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
+        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.
+        You should only bump this option, if you are sure that you can
+        or have migrated all state on your system which is affected
+        by this option.
+      '';
+    };
+
+    defaultChannel = mkOption {
+      internal = true;
+      type = types.str;
+      default = "https://nixos.org/channels/nixos-unstable";
+      description = "Default NixOS channel to which the root user is subscribed.";
+    };
+
+    configurationRevision = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "The Git revision of the top-level flake from which this configuration was built.";
+    };
+
+  };
+
+  config = {
+
+    system.nixos = {
+      # These defaults are set here rather than up there so that
+      # changing them would not rebuild the manual
+      version = mkDefault (cfg.release + cfg.versionSuffix);
+    };
+
+    # Generate /etc/os-release.  See
+    # https://www.freedesktop.org/software/systemd/man/os-release.html for the
+    # format.
+    environment.etc = {
+      "lsb-release".text = attrsToText {
+        LSB_VERSION = "${cfg.release} (${cfg.codeName})";
+        DISTRIB_ID = "nixos";
+        DISTRIB_RELEASE = cfg.release;
+        DISTRIB_CODENAME = toLower cfg.codeName;
+        DISTRIB_DESCRIPTION = "NixOS ${cfg.release} (${cfg.codeName})";
+      };
+
+      "os-release".text = attrsToText {
+        NAME = "NixOS";
+        ID = "nixos";
+        VERSION = "${cfg.release} (${cfg.codeName})";
+        VERSION_CODENAME = toLower cfg.codeName;
+        VERSION_ID = cfg.release;
+        BUILD_ID = cfg.version;
+        PRETTY_NAME = "NixOS ${cfg.release} (${cfg.codeName})";
+        LOGO = "nix-snowflake";
+        HOME_URL = "https://nixos.org/";
+        DOCUMENTATION_URL = "https://nixos.org/learn.html";
+        SUPPORT_URL = "https://nixos.org/community.html";
+        BUG_REPORT_URL = "https://github.com/NixOS/nixpkgs/issues";
+      };
+    };
+  };
+
+  # uses version info nixpkgs, which requires a full nixpkgs path
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/misc/wordlist.nix b/nixos/modules/misc/wordlist.nix
new file mode 100644
index 00000000000..988b522d743
--- /dev/null
+++ b/nixos/modules/misc/wordlist.nix
@@ -0,0 +1,59 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  concatAndSort = name: files: pkgs.runCommand name {} ''
+    awk 1 ${lib.escapeShellArgs files} | sed '{ /^\s*$/d; s/^\s\+//; s/\s\+$// }' | sort | uniq > $out
+  '';
+in
+{
+  options = {
+    environment.wordlist = {
+      enable = mkEnableOption "environment variables for lists of words";
+
+      lists = mkOption {
+        type = types.attrsOf (types.nonEmptyListOf types.path);
+
+        default = {
+          WORDLIST = [ "${pkgs.scowl}/share/dict/words.txt" ];
+        };
+
+        defaultText = literalExpression ''
+          {
+            WORDLIST = [ "''${pkgs.scowl}/share/dict/words.txt" ];
+          }
+        '';
+
+        description = ''
+          A set with the key names being the environment variable you'd like to
+          set and the values being a list of paths to text documents containing
+          lists of words. The various files will be merged, sorted, duplicates
+          removed, and extraneous spacing removed.
+
+          If you have a handful of words that you want to add to an already
+          existing wordlist, you may find `builtins.toFile` useful for this
+          task.
+        '';
+
+        example = literalExpression ''
+          {
+            WORDLIST = [ "''${pkgs.scowl}/share/dict/words.txt" ];
+            AUGMENTED_WORDLIST = [
+              "''${pkgs.scowl}/share/dict/words.txt"
+              "''${pkgs.scowl}/share/dict/words.variants.txt"
+              (builtins.toFile "extra-words" '''
+                desynchonization
+                oobleck''')
+            ];
+          }
+        '';
+      };
+    };
+  };
+
+  config = mkIf config.environment.wordlist.enable {
+    environment.variables =
+      lib.mapAttrs
+        (name: value: "${concatAndSort "wordlist-${name}" value}")
+        config.environment.wordlist.lists;
+  };
+}
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
new file mode 100644
index 00000000000..68f9c6c1227
--- /dev/null
+++ b/nixos/modules/module-list.nix
@@ -0,0 +1,1237 @@
+[
+  ./config/debug-info.nix
+  ./config/fonts/fontconfig.nix
+  ./config/fonts/fontdir.nix
+  ./config/fonts/fonts.nix
+  ./config/fonts/ghostscript.nix
+  ./config/xdg/autostart.nix
+  ./config/xdg/icons.nix
+  ./config/xdg/menus.nix
+  ./config/xdg/mime.nix
+  ./config/xdg/portal.nix
+  ./config/xdg/portals/wlr.nix
+  ./config/appstream.nix
+  ./config/console.nix
+  ./config/xdg/sounds.nix
+  ./config/gtk/gtk-icon-cache.nix
+  ./config/gnu.nix
+  ./config/i18n.nix
+  ./config/iproute2.nix
+  ./config/krb5/default.nix
+  ./config/ldap.nix
+  ./config/locale.nix
+  ./config/malloc.nix
+  ./config/networking.nix
+  ./config/no-x-libs.nix
+  ./config/nsswitch.nix
+  ./config/power-management.nix
+  ./config/pulseaudio.nix
+  ./config/qt5.nix
+  ./config/resolvconf.nix
+  ./config/shells-environment.nix
+  ./config/swap.nix
+  ./config/sysctl.nix
+  ./config/system-environment.nix
+  ./config/system-path.nix
+  ./config/terminfo.nix
+  ./config/unix-odbc-drivers.nix
+  ./config/users-groups.nix
+  ./config/vte.nix
+  ./config/zram.nix
+  ./hardware/acpilight.nix
+  ./hardware/all-firmware.nix
+  ./hardware/bladeRF.nix
+  ./hardware/brillo.nix
+  ./hardware/ckb-next.nix
+  ./hardware/cpu/amd-microcode.nix
+  ./hardware/cpu/intel-microcode.nix
+  ./hardware/cpu/intel-sgx.nix
+  ./hardware/corectrl.nix
+  ./hardware/digitalbitbox.nix
+  ./hardware/device-tree.nix
+  ./hardware/gkraken.nix
+  ./hardware/flirc.nix
+  ./hardware/gpgsmartcards.nix
+  ./hardware/i2c.nix
+  ./hardware/hackrf.nix
+  ./hardware/sensor/hddtemp.nix
+  ./hardware/sensor/iio.nix
+  ./hardware/keyboard/teck.nix
+  ./hardware/keyboard/zsa.nix
+  ./hardware/ksm.nix
+  ./hardware/ledger.nix
+  ./hardware/logitech.nix
+  ./hardware/mcelog.nix
+  ./hardware/network/ath-user-regd.nix
+  ./hardware/network/b43.nix
+  ./hardware/network/intel-2200bg.nix
+  ./hardware/nitrokey.nix
+  ./hardware/opengl.nix
+  ./hardware/openrazer.nix
+  ./hardware/pcmcia.nix
+  ./hardware/printers.nix
+  ./hardware/raid/hpsa.nix
+  ./hardware/rtl-sdr.nix
+  ./hardware/steam-hardware.nix
+  ./hardware/system-76.nix
+  ./hardware/tuxedo-keyboard.nix
+  ./hardware/ubertooth.nix
+  ./hardware/usb-wwan.nix
+  ./hardware/onlykey/default.nix
+  ./hardware/opentabletdriver.nix
+  ./hardware/sata.nix
+  ./hardware/wooting.nix
+  ./hardware/uinput.nix
+  ./hardware/video/amdgpu-pro.nix
+  ./hardware/video/capture/mwprocapture.nix
+  ./hardware/video/bumblebee.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/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
+  ./i18n/input-method/nabi.nix
+  ./i18n/input-method/uim.nix
+  ./i18n/input-method/kime.nix
+  ./installer/tools/tools.nix
+  ./misc/assertions.nix
+  ./misc/crashdump.nix
+  ./misc/documentation.nix
+  ./misc/extra-arguments.nix
+  ./misc/ids.nix
+  ./misc/lib.nix
+  ./misc/label.nix
+  ./misc/locate.nix
+  ./misc/man-db.nix
+  ./misc/mandoc.nix
+  ./misc/meta.nix
+  ./misc/nixpkgs.nix
+  ./misc/passthru.nix
+  ./misc/version.nix
+  ./misc/wordlist.nix
+  ./misc/nixops-autoluks.nix
+  ./programs/adb.nix
+  ./programs/appgate-sdp.nix
+  ./programs/atop.nix
+  ./programs/autojump.nix
+  ./programs/bandwhich.nix
+  ./programs/bash/bash.nix
+  ./programs/bash/bash-completion.nix
+  ./programs/bash/ls-colors.nix
+  ./programs/bash/undistract-me.nix
+  ./programs/bash-my-aws.nix
+  ./programs/bcc.nix
+  ./programs/browserpass.nix
+  ./programs/calls.nix
+  ./programs/captive-browser.nix
+  ./programs/ccache.nix
+  ./programs/cdemu.nix
+  ./programs/chromium.nix
+  ./programs/clickshare.nix
+  ./programs/cnping.nix
+  ./programs/command-not-found/command-not-found.nix
+  ./programs/criu.nix
+  ./programs/dconf.nix
+  ./programs/digitalbitbox/default.nix
+  ./programs/dmrconfig.nix
+  ./programs/droidcam.nix
+  ./programs/environment.nix
+  ./programs/evince.nix
+  ./programs/extra-container.nix
+  ./programs/feedbackd.nix
+  ./programs/file-roller.nix
+  ./programs/firejail.nix
+  ./programs/fish.nix
+  ./programs/flashrom.nix
+  ./programs/flexoptix-app.nix
+  ./programs/freetds.nix
+  ./programs/fuse.nix
+  ./programs/gamemode.nix
+  ./programs/geary.nix
+  ./programs/git.nix
+  ./programs/gnome-disks.nix
+  ./programs/gnome-documents.nix
+  ./programs/gnome-terminal.nix
+  ./programs/gpaste.nix
+  ./programs/gnupg.nix
+  ./programs/gphoto2.nix
+  ./programs/hamster.nix
+  ./programs/htop.nix
+  ./programs/iftop.nix
+  ./programs/iotop.nix
+  ./programs/java.nix
+  ./programs/k40-whisperer.nix
+  ./programs/kclock.nix
+  ./programs/kdeconnect.nix
+  ./programs/kbdlight.nix
+  ./programs/less.nix
+  ./programs/liboping.nix
+  ./programs/light.nix
+  ./programs/mosh.nix
+  ./programs/mininet.nix
+  ./programs/msmtp.nix
+  ./programs/mtr.nix
+  ./programs/nano.nix
+  ./programs/nbd.nix
+  ./programs/neovim.nix
+  ./programs/nm-applet.nix
+  ./programs/npm.nix
+  ./programs/noisetorch.nix
+  ./programs/oblogout.nix
+  ./programs/pantheon-tweaks.nix
+  ./programs/partition-manager.nix
+  ./programs/plotinus.nix
+  ./programs/proxychains.nix
+  ./programs/phosh.nix
+  ./programs/qt5ct.nix
+  ./programs/screen.nix
+  ./programs/sedutil.nix
+  ./programs/seahorse.nix
+  ./programs/slock.nix
+  ./programs/shadow.nix
+  ./programs/spacefm.nix
+  ./programs/singularity.nix
+  ./programs/ssh.nix
+  ./programs/ssmtp.nix
+  ./programs/sysdig.nix
+  ./programs/systemtap.nix
+  ./programs/starship.nix
+  ./programs/steam.nix
+  ./programs/sway.nix
+  ./programs/system-config-printer.nix
+  ./programs/thefuck.nix
+  ./programs/tmux.nix
+  ./programs/traceroute.nix
+  ./programs/tsm-client.nix
+  ./programs/turbovnc.nix
+  ./programs/udevil.nix
+  ./programs/usbtop.nix
+  ./programs/vim.nix
+  ./programs/wavemon.nix
+  ./programs/waybar.nix
+  ./programs/weylus.nix
+  ./programs/wireshark.nix
+  ./programs/wshowkeys.nix
+  ./programs/xfs_quota.nix
+  ./programs/xonsh.nix
+  ./programs/xss-lock.nix
+  ./programs/xwayland.nix
+  ./programs/yabar.nix
+  ./programs/zmap.nix
+  ./programs/zsh/oh-my-zsh.nix
+  ./programs/zsh/zsh.nix
+  ./programs/zsh/zsh-autoenv.nix
+  ./programs/zsh/zsh-autosuggestions.nix
+  ./programs/zsh/zsh-syntax-highlighting.nix
+  ./rename.nix
+  ./security/acme
+  ./security/apparmor.nix
+  ./security/audit.nix
+  ./security/auditd.nix
+  ./security/ca.nix
+  ./security/chromium-suid-sandbox.nix
+  ./security/dhparams.nix
+  ./security/duosec.nix
+  ./security/google_oslogin.nix
+  ./security/lock-kernel-modules.nix
+  ./security/misc.nix
+  ./security/oath.nix
+  ./security/pam.nix
+  ./security/pam_usb.nix
+  ./security/pam_mount.nix
+  ./security/polkit.nix
+  ./security/rngd.nix
+  ./security/rtkit.nix
+  ./security/wrappers/default.nix
+  ./security/sudo.nix
+  ./security/doas.nix
+  ./security/systemd-confinement.nix
+  ./security/tpm2.nix
+  ./services/admin/meshcentral.nix
+  ./services/admin/oxidized.nix
+  ./services/admin/pgadmin.nix
+  ./services/admin/salt/master.nix
+  ./services/admin/salt/minion.nix
+  ./services/amqp/activemq/default.nix
+  ./services/amqp/rabbitmq.nix
+  ./services/audio/alsa.nix
+  ./services/audio/botamusique.nix
+  ./services/audio/hqplayerd.nix
+  ./services/audio/icecast.nix
+  ./services/audio/jack.nix
+  ./services/audio/jmusicbot.nix
+  ./services/audio/liquidsoap.nix
+  ./services/audio/mpd.nix
+  ./services/audio/mpdscribble.nix
+  ./services/audio/mopidy.nix
+  ./services/audio/networkaudiod.nix
+  ./services/audio/roon-bridge.nix
+  ./services/audio/navidrome.nix
+  ./services/audio/roon-server.nix
+  ./services/audio/slimserver.nix
+  ./services/audio/snapserver.nix
+  ./services/audio/squeezelite.nix
+  ./services/audio/spotifyd.nix
+  ./services/audio/ympd.nix
+  ./services/backup/automysqlbackup.nix
+  ./services/backup/bacula.nix
+  ./services/backup/borgbackup.nix
+  ./services/backup/borgmatic.nix
+  ./services/backup/btrbk.nix
+  ./services/backup/duplicati.nix
+  ./services/backup/duplicity.nix
+  ./services/backup/mysql-backup.nix
+  ./services/backup/postgresql-backup.nix
+  ./services/backup/postgresql-wal-receiver.nix
+  ./services/backup/restic.nix
+  ./services/backup/restic-rest-server.nix
+  ./services/backup/rsnapshot.nix
+  ./services/backup/sanoid.nix
+  ./services/backup/syncoid.nix
+  ./services/backup/tarsnap.nix
+  ./services/backup/tsm.nix
+  ./services/backup/zfs-replication.nix
+  ./services/backup/znapzend.nix
+  ./services/blockchain/ethereum/geth.nix
+  ./services/backup/zrepl.nix
+  ./services/cluster/corosync/default.nix
+  ./services/cluster/hadoop/default.nix
+  ./services/cluster/k3s/default.nix
+  ./services/cluster/kubernetes/addons/dns.nix
+  ./services/cluster/kubernetes/addon-manager.nix
+  ./services/cluster/kubernetes/apiserver.nix
+  ./services/cluster/kubernetes/controller-manager.nix
+  ./services/cluster/kubernetes/default.nix
+  ./services/cluster/kubernetes/flannel.nix
+  ./services/cluster/kubernetes/kubelet.nix
+  ./services/cluster/kubernetes/pki.nix
+  ./services/cluster/kubernetes/proxy.nix
+  ./services/cluster/kubernetes/scheduler.nix
+  ./services/cluster/pacemaker/default.nix
+  ./services/cluster/spark/default.nix
+  ./services/computing/boinc/client.nix
+  ./services/computing/foldingathome/client.nix
+  ./services/computing/slurm/slurm.nix
+  ./services/computing/torque/mom.nix
+  ./services/computing/torque/server.nix
+  ./services/continuous-integration/buildbot/master.nix
+  ./services/continuous-integration/buildbot/worker.nix
+  ./services/continuous-integration/buildkite-agents.nix
+  ./services/continuous-integration/hail.nix
+  ./services/continuous-integration/hercules-ci-agent/default.nix
+  ./services/continuous-integration/hydra/default.nix
+  ./services/continuous-integration/github-runner.nix
+  ./services/continuous-integration/gitlab-runner.nix
+  ./services/continuous-integration/gocd-agent/default.nix
+  ./services/continuous-integration/gocd-server/default.nix
+  ./services/continuous-integration/jenkins/default.nix
+  ./services/continuous-integration/jenkins/job-builder.nix
+  ./services/continuous-integration/jenkins/slave.nix
+  ./services/databases/aerospike.nix
+  ./services/databases/cassandra.nix
+  ./services/databases/clickhouse.nix
+  ./services/databases/cockroachdb.nix
+  ./services/databases/couchdb.nix
+  ./services/databases/firebird.nix
+  ./services/databases/foundationdb.nix
+  ./services/databases/hbase.nix
+  ./services/databases/influxdb.nix
+  ./services/databases/influxdb2.nix
+  ./services/databases/memcached.nix
+  ./services/databases/monetdb.nix
+  ./services/databases/mongodb.nix
+  ./services/databases/mysql.nix
+  ./services/databases/neo4j.nix
+  ./services/databases/openldap.nix
+  ./services/databases/opentsdb.nix
+  ./services/databases/pgmanage.nix
+  ./services/databases/postgresql.nix
+  ./services/databases/redis.nix
+  ./services/databases/riak.nix
+  ./services/databases/victoriametrics.nix
+  ./services/desktops/accountsservice.nix
+  ./services/desktops/bamf.nix
+  ./services/desktops/blueman.nix
+  ./services/desktops/cpupower-gui.nix
+  ./services/desktops/dleyna-renderer.nix
+  ./services/desktops/dleyna-server.nix
+  ./services/desktops/espanso.nix
+  ./services/desktops/flatpak.nix
+  ./services/desktops/geoclue2.nix
+  ./services/desktops/gsignond.nix
+  ./services/desktops/gvfs.nix
+  ./services/desktops/malcontent.nix
+  ./services/desktops/pipewire/pipewire.nix
+  ./services/desktops/pipewire/pipewire-media-session.nix
+  ./services/desktops/pipewire/wireplumber.nix
+  ./services/desktops/gnome/at-spi2-core.nix
+  ./services/desktops/gnome/chrome-gnome-shell.nix
+  ./services/desktops/gnome/evolution-data-server.nix
+  ./services/desktops/gnome/glib-networking.nix
+  ./services/desktops/gnome/gnome-initial-setup.nix
+  ./services/desktops/gnome/gnome-keyring.nix
+  ./services/desktops/gnome/gnome-online-accounts.nix
+  ./services/desktops/gnome/gnome-online-miners.nix
+  ./services/desktops/gnome/gnome-remote-desktop.nix
+  ./services/desktops/gnome/gnome-settings-daemon.nix
+  ./services/desktops/gnome/gnome-user-share.nix
+  ./services/desktops/gnome/rygel.nix
+  ./services/desktops/gnome/sushi.nix
+  ./services/desktops/gnome/tracker.nix
+  ./services/desktops/gnome/tracker-miners.nix
+  ./services/desktops/neard.nix
+  ./services/desktops/profile-sync-daemon.nix
+  ./services/desktops/system-config-printer.nix
+  ./services/desktops/telepathy.nix
+  ./services/desktops/tumbler.nix
+  ./services/desktops/zeitgeist.nix
+  ./services/development/bloop.nix
+  ./services/development/blackfire.nix
+  ./services/development/distccd.nix
+  ./services/development/hoogle.nix
+  ./services/development/jupyter/default.nix
+  ./services/development/jupyterhub/default.nix
+  ./services/development/rstudio-server/default.nix
+  ./services/development/lorri.nix
+  ./services/development/zammad.nix
+  ./services/display-managers/greetd.nix
+  ./services/editors/emacs.nix
+  ./services/editors/infinoted.nix
+  ./services/finance/odoo.nix
+  ./services/games/asf.nix
+  ./services/games/crossfire-server.nix
+  ./services/games/deliantra-server.nix
+  ./services/games/factorio.nix
+  ./services/games/freeciv.nix
+  ./services/games/minecraft-server.nix
+  ./services/games/minetest-server.nix
+  ./services/games/openarena.nix
+  ./services/games/quake3-server.nix
+  ./services/games/teeworlds.nix
+  ./services/games/terraria.nix
+  ./services/hardware/acpid.nix
+  ./services/hardware/actkbd.nix
+  ./services/hardware/auto-cpufreq.nix
+  ./services/hardware/bluetooth.nix
+  ./services/hardware/bolt.nix
+  ./services/hardware/brltty.nix
+  ./services/hardware/ddccontrol.nix
+  ./services/hardware/fancontrol.nix
+  ./services/hardware/freefall.nix
+  ./services/hardware/fwupd.nix
+  ./services/hardware/illum.nix
+  ./services/hardware/interception-tools.nix
+  ./services/hardware/irqbalance.nix
+  ./services/hardware/joycond.nix
+  ./services/hardware/lcd.nix
+  ./services/hardware/lirc.nix
+  ./services/hardware/nvidia-optimus.nix
+  ./services/hardware/pcscd.nix
+  ./services/hardware/pommed.nix
+  ./services/hardware/power-profiles-daemon.nix
+  ./services/hardware/rasdaemon.nix
+  ./services/hardware/ratbagd.nix
+  ./services/hardware/sane.nix
+  ./services/hardware/sane_extra_backends/brscan4.nix
+  ./services/hardware/sane_extra_backends/brscan5.nix
+  ./services/hardware/sane_extra_backends/dsseries.nix
+  ./services/hardware/spacenavd.nix
+  ./services/hardware/tcsd.nix
+  ./services/hardware/tlp.nix
+  ./services/hardware/thinkfan.nix
+  ./services/hardware/throttled.nix
+  ./services/hardware/trezord.nix
+  ./services/hardware/triggerhappy.nix
+  ./services/hardware/udev.nix
+  ./services/hardware/udisks2.nix
+  ./services/hardware/upower.nix
+  ./services/hardware/usbmuxd.nix
+  ./services/hardware/thermald.nix
+  ./services/hardware/undervolt.nix
+  ./services/hardware/vdr.nix
+  ./services/hardware/xow.nix
+  ./services/home-automation/home-assistant.nix
+  ./services/home-automation/zigbee2mqtt.nix
+  ./services/logging/SystemdJournal2Gelf.nix
+  ./services/logging/awstats.nix
+  ./services/logging/filebeat.nix
+  ./services/logging/fluentd.nix
+  ./services/logging/graylog.nix
+  ./services/logging/heartbeat.nix
+  ./services/logging/journalbeat.nix
+  ./services/logging/journaldriver.nix
+  ./services/logging/journalwatch.nix
+  ./services/logging/klogd.nix
+  ./services/logging/logcheck.nix
+  ./services/logging/logrotate.nix
+  ./services/logging/logstash.nix
+  ./services/logging/promtail.nix
+  ./services/logging/rsyslogd.nix
+  ./services/logging/syslog-ng.nix
+  ./services/logging/syslogd.nix
+  ./services/logging/vector.nix
+  ./services/mail/clamsmtp.nix
+  ./services/mail/davmail.nix
+  ./services/mail/dkimproxy-out.nix
+  ./services/mail/dovecot.nix
+  ./services/mail/dspam.nix
+  ./services/mail/exim.nix
+  ./services/mail/maddy.nix
+  ./services/mail/mail.nix
+  ./services/mail/mailcatcher.nix
+  ./services/mail/mailhog.nix
+  ./services/mail/mailman.nix
+  ./services/mail/mlmmj.nix
+  ./services/mail/offlineimap.nix
+  ./services/mail/opendkim.nix
+  ./services/mail/opensmtpd.nix
+  ./services/mail/pfix-srsd.nix
+  ./services/mail/postfix.nix
+  ./services/mail/postfixadmin.nix
+  ./services/mail/postsrsd.nix
+  ./services/mail/postgrey.nix
+  ./services/mail/spamassassin.nix
+  ./services/mail/rspamd.nix
+  ./services/mail/rss2email.nix
+  ./services/mail/roundcube.nix
+  ./services/mail/sympa.nix
+  ./services/mail/nullmailer.nix
+  ./services/matrix/matrix-synapse.nix
+  ./services/matrix/mjolnir.nix
+  ./services/matrix/pantalaimon.nix
+  ./services/misc/ananicy.nix
+  ./services/misc/airsonic.nix
+  ./services/misc/ankisyncd.nix
+  ./services/misc/apache-kafka.nix
+  ./services/misc/autofs.nix
+  ./services/misc/autorandr.nix
+  ./services/misc/bazarr.nix
+  ./services/misc/beanstalkd.nix
+  ./services/misc/bees.nix
+  ./services/misc/bepasty.nix
+  ./services/misc/canto-daemon.nix
+  ./services/misc/calibre-server.nix
+  ./services/misc/cfdyndns.nix
+  ./services/misc/clipmenu.nix
+  ./services/misc/clipcat.nix
+  ./services/misc/cpuminer-cryptonight.nix
+  ./services/misc/cgminer.nix
+  ./services/misc/confd.nix
+  ./services/misc/dendrite.nix
+  ./services/misc/devmon.nix
+  ./services/misc/dictd.nix
+  ./services/misc/duckling.nix
+  ./services/misc/dwm-status.nix
+  ./services/misc/dysnomia.nix
+  ./services/misc/disnix.nix
+  ./services/misc/docker-registry.nix
+  ./services/misc/domoticz.nix
+  ./services/misc/errbot.nix
+  ./services/misc/etcd.nix
+  ./services/misc/etebase-server.nix
+  ./services/misc/etesync-dav.nix
+  ./services/misc/ethminer.nix
+  ./services/misc/exhibitor.nix
+  ./services/misc/felix.nix
+  ./services/misc/freeswitch.nix
+  ./services/misc/fstrim.nix
+  ./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
+  ./services/misc/gogs.nix
+  ./services/misc/gollum.nix
+  ./services/misc/gpsd.nix
+  ./services/misc/headphones.nix
+  ./services/misc/heisenbridge.nix
+  ./services/misc/greenclip.nix
+  ./services/misc/ihaskell.nix
+  ./services/misc/input-remapper.nix
+  ./services/misc/irkerd.nix
+  ./services/misc/jackett.nix
+  ./services/misc/jellyfin.nix
+  ./services/misc/klipper.nix
+  ./services/misc/logkeys.nix
+  ./services/misc/leaps.nix
+  ./services/misc/lidarr.nix
+  ./services/misc/libreddit.nix
+  ./services/misc/lifecycled.nix
+  ./services/misc/mame.nix
+  ./services/misc/matrix-appservice-discord.nix
+  ./services/misc/matrix-appservice-irc.nix
+  ./services/misc/matrix-conduit.nix
+  ./services/misc/mautrix-facebook.nix
+  ./services/misc/mautrix-telegram.nix
+  ./services/misc/mbpfan.nix
+  ./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
+  ./services/misc/nix-gc.nix
+  ./services/misc/nix-optimise.nix
+  ./services/misc/nix-ssh-serve.nix
+  ./services/misc/novacomd.nix
+  ./services/misc/nzbget.nix
+  ./services/misc/nzbhydra2.nix
+  ./services/misc/octoprint.nix
+  ./services/misc/ombi.nix
+  ./services/misc/osrm.nix
+  ./services/misc/owncast.nix
+  ./services/misc/packagekit.nix
+  ./services/misc/paperless-ng.nix
+  ./services/misc/parsoid.nix
+  ./services/misc/plex.nix
+  ./services/misc/plikd.nix
+  ./services/misc/podgrab.nix
+  ./services/misc/prowlarr.nix
+  ./services/misc/tautulli.nix
+  ./services/misc/pinnwand.nix
+  ./services/misc/pykms.nix
+  ./services/misc/radarr.nix
+  ./services/misc/redmine.nix
+  ./services/misc/rippled.nix
+  ./services/misc/ripple-data-api.nix
+  ./services/misc/rmfakecloud.nix
+  ./services/misc/serviio.nix
+  ./services/misc/safeeyes.nix
+  ./services/misc/sdrplay.nix
+  ./services/misc/sickbeard.nix
+  ./services/misc/signald.nix
+  ./services/misc/siproxd.nix
+  ./services/misc/snapper.nix
+  ./services/misc/sonarr.nix
+  ./services/misc/sourcehut
+  ./services/misc/spice-vdagentd.nix
+  ./services/misc/ssm-agent.nix
+  ./services/misc/sssd.nix
+  ./services/misc/subsonic.nix
+  ./services/misc/sundtek.nix
+  ./services/misc/svnserve.nix
+  ./services/misc/synergy.nix
+  ./services/misc/sysprof.nix
+  ./services/misc/taskserver
+  ./services/misc/tiddlywiki.nix
+  ./services/misc/tp-auto-kbbl.nix
+  ./services/misc/tzupdate.nix
+  ./services/misc/uhub.nix
+  ./services/misc/weechat.nix
+  ./services/misc/xmr-stak.nix
+  ./services/misc/xmrig.nix
+  ./services/misc/zoneminder.nix
+  ./services/misc/zookeeper.nix
+  ./services/monitoring/alerta.nix
+  ./services/monitoring/apcupsd.nix
+  ./services/monitoring/arbtt.nix
+  ./services/monitoring/bosun.nix
+  ./services/monitoring/cadvisor.nix
+  ./services/monitoring/collectd.nix
+  ./services/monitoring/das_watchdog.nix
+  ./services/monitoring/datadog-agent.nix
+  ./services/monitoring/dd-agent/dd-agent.nix
+  ./services/monitoring/do-agent.nix
+  ./services/monitoring/fusion-inventory.nix
+  ./services/monitoring/grafana.nix
+  ./services/monitoring/grafana-image-renderer.nix
+  ./services/monitoring/grafana-reporter.nix
+  ./services/monitoring/graphite.nix
+  ./services/monitoring/hdaps.nix
+  ./services/monitoring/heapster.nix
+  ./services/monitoring/incron.nix
+  ./services/monitoring/kapacitor.nix
+  ./services/monitoring/loki.nix
+  ./services/monitoring/longview.nix
+  ./services/monitoring/mackerel-agent.nix
+  ./services/monitoring/metricbeat.nix
+  ./services/monitoring/monit.nix
+  ./services/monitoring/munin.nix
+  ./services/monitoring/nagios.nix
+  ./services/monitoring/netdata.nix
+  ./services/monitoring/parsedmarc.nix
+  ./services/monitoring/prometheus/default.nix
+  ./services/monitoring/prometheus/alertmanager.nix
+  ./services/monitoring/prometheus/exporters.nix
+  ./services/monitoring/prometheus/pushgateway.nix
+  ./services/monitoring/prometheus/xmpp-alerts.nix
+  ./services/monitoring/riemann.nix
+  ./services/monitoring/riemann-dash.nix
+  ./services/monitoring/riemann-tools.nix
+  ./services/monitoring/scollector.nix
+  ./services/monitoring/smartd.nix
+  ./services/monitoring/sysstat.nix
+  ./services/monitoring/teamviewer.nix
+  ./services/monitoring/telegraf.nix
+  ./services/monitoring/thanos.nix
+  ./services/monitoring/tuptime.nix
+  ./services/monitoring/unifi-poller.nix
+  ./services/monitoring/ups.nix
+  ./services/monitoring/uptime.nix
+  ./services/monitoring/vnstat.nix
+  ./services/monitoring/zabbix-agent.nix
+  ./services/monitoring/zabbix-proxy.nix
+  ./services/monitoring/zabbix-server.nix
+  ./services/network-filesystems/cachefilesd.nix
+  ./services/network-filesystems/davfs2.nix
+  ./services/network-filesystems/drbd.nix
+  ./services/network-filesystems/glusterfs.nix
+  ./services/network-filesystems/kbfs.nix
+  ./services/network-filesystems/ipfs.nix
+  ./services/network-filesystems/litestream/default.nix
+  ./services/network-filesystems/netatalk.nix
+  ./services/network-filesystems/nfsd.nix
+  ./services/network-filesystems/moosefs.nix
+  ./services/network-filesystems/openafs/client.nix
+  ./services/network-filesystems/openafs/server.nix
+  ./services/network-filesystems/orangefs/server.nix
+  ./services/network-filesystems/orangefs/client.nix
+  ./services/network-filesystems/rsyncd.nix
+  ./services/network-filesystems/samba.nix
+  ./services/network-filesystems/samba-wsdd.nix
+  ./services/network-filesystems/tahoe.nix
+  ./services/network-filesystems/diod.nix
+  ./services/network-filesystems/u9fs.nix
+  ./services/network-filesystems/webdav.nix
+  ./services/network-filesystems/webdav-server-rs.nix
+  ./services/network-filesystems/yandex-disk.nix
+  ./services/network-filesystems/xtreemfs.nix
+  ./services/network-filesystems/ceph.nix
+  ./services/networking/3proxy.nix
+  ./services/networking/adguardhome.nix
+  ./services/networking/amuled.nix
+  ./services/networking/antennas.nix
+  ./services/networking/aria2.nix
+  ./services/networking/asterisk.nix
+  ./services/networking/atftpd.nix
+  ./services/networking/avahi-daemon.nix
+  ./services/networking/babeld.nix
+  ./services/networking/bee.nix
+  ./services/networking/bee-clef.nix
+  ./services/networking/biboumi.nix
+  ./services/networking/bind.nix
+  ./services/networking/bitcoind.nix
+  ./services/networking/autossh.nix
+  ./services/networking/bird.nix
+  ./services/networking/bitlbee.nix
+  ./services/networking/blockbook-frontend.nix
+  ./services/networking/blocky.nix
+  ./services/networking/charybdis.nix
+  ./services/networking/cjdns.nix
+  ./services/networking/cntlm.nix
+  ./services/networking/connman.nix
+  ./services/networking/consul.nix
+  ./services/networking/coredns.nix
+  ./services/networking/corerad.nix
+  ./services/networking/coturn.nix
+  ./services/networking/croc.nix
+  ./services/networking/dante.nix
+  ./services/networking/ddclient.nix
+  ./services/networking/dhcpcd.nix
+  ./services/networking/dhcpd.nix
+  ./services/networking/dnscache.nix
+  ./services/networking/dnscrypt-proxy2.nix
+  ./services/networking/dnscrypt-wrapper.nix
+  ./services/networking/dnsdist.nix
+  ./services/networking/dnsmasq.nix
+  ./services/networking/doh-proxy-rust.nix
+  ./services/networking/ncdns.nix
+  ./services/networking/nomad.nix
+  ./services/networking/ejabberd.nix
+  ./services/networking/epmd.nix
+  ./services/networking/ergo.nix
+  ./services/networking/ergochat.nix
+  ./services/networking/eternal-terminal.nix
+  ./services/networking/fakeroute.nix
+  ./services/networking/ferm.nix
+  ./services/networking/fireqos.nix
+  ./services/networking/firewall.nix
+  ./services/networking/flannel.nix
+  ./services/networking/freenet.nix
+  ./services/networking/freeradius.nix
+  ./services/networking/frr.nix
+  ./services/networking/gateone.nix
+  ./services/networking/gdomap.nix
+  ./services/networking/ghostunnel.nix
+  ./services/networking/git-daemon.nix
+  ./services/networking/globalprotect-vpn.nix
+  ./services/networking/gnunet.nix
+  ./services/networking/go-neb.nix
+  ./services/networking/go-shadowsocks2.nix
+  ./services/networking/gobgpd.nix
+  ./services/networking/gvpe.nix
+  ./services/networking/hans.nix
+  ./services/networking/haproxy.nix
+  ./services/networking/headscale.nix
+  ./services/networking/hostapd.nix
+  ./services/networking/htpdate.nix
+  ./services/networking/hylafax/default.nix
+  ./services/networking/i2pd.nix
+  ./services/networking/i2p.nix
+  ./services/networking/icecream/scheduler.nix
+  ./services/networking/icecream/daemon.nix
+  ./services/networking/inspircd.nix
+  ./services/networking/iodine.nix
+  ./services/networking/iperf3.nix
+  ./services/networking/ircd-hybrid/default.nix
+  ./services/networking/iscsi/initiator.nix
+  ./services/networking/iscsi/root-initiator.nix
+  ./services/networking/iscsi/target.nix
+  ./services/networking/iwd.nix
+  ./services/networking/jibri/default.nix
+  ./services/networking/jicofo.nix
+  ./services/networking/jitsi-videobridge.nix
+  ./services/networking/kea.nix
+  ./services/networking/keepalived/default.nix
+  ./services/networking/keybase.nix
+  ./services/networking/knot.nix
+  ./services/networking/kresd.nix
+  ./services/networking/lambdabot.nix
+  ./services/networking/libreswan.nix
+  ./services/networking/lldpd.nix
+  ./services/networking/logmein-hamachi.nix
+  ./services/networking/lxd-image-server.nix
+  ./services/networking/magic-wormhole-mailbox-server.nix
+  ./services/networking/matterbridge.nix
+  ./services/networking/mjpg-streamer.nix
+  ./services/networking/minidlna.nix
+  ./services/networking/miniupnpd.nix
+  ./services/networking/mosquitto.nix
+  ./services/networking/monero.nix
+  ./services/networking/morty.nix
+  ./services/networking/miredo.nix
+  ./services/networking/mstpd.nix
+  ./services/networking/mtprotoproxy.nix
+  ./services/networking/mtr-exporter.nix
+  ./services/networking/mullvad-vpn.nix
+  ./services/networking/multipath.nix
+  ./services/networking/murmur.nix
+  ./services/networking/mxisd.nix
+  ./services/networking/namecoind.nix
+  ./services/networking/nar-serve.nix
+  ./services/networking/nat.nix
+  ./services/networking/nats.nix
+  ./services/networking/nbd.nix
+  ./services/networking/ndppd.nix
+  ./services/networking/nebula.nix
+  ./services/networking/networkmanager.nix
+  ./services/networking/nextdns.nix
+  ./services/networking/nftables.nix
+  ./services/networking/ngircd.nix
+  ./services/networking/nghttpx/default.nix
+  ./services/networking/nix-serve.nix
+  ./services/networking/nix-store-gcs-proxy.nix
+  ./services/networking/nixops-dns.nix
+  ./services/networking/nntp-proxy.nix
+  ./services/networking/nsd.nix
+  ./services/networking/ntopng.nix
+  ./services/networking/ntp/chrony.nix
+  ./services/networking/ntp/ntpd.nix
+  ./services/networking/ntp/openntpd.nix
+  ./services/networking/nullidentdmod.nix
+  ./services/networking/nylon.nix
+  ./services/networking/ocserv.nix
+  ./services/networking/ofono.nix
+  ./services/networking/oidentd.nix
+  ./services/networking/onedrive.nix
+  ./services/networking/openfire.nix
+  ./services/networking/openvpn.nix
+  ./services/networking/ostinato.nix
+  ./services/networking/owamp.nix
+  ./services/networking/pdnsd.nix
+  ./services/networking/pixiecore.nix
+  ./services/networking/pleroma.nix
+  ./services/networking/polipo.nix
+  ./services/networking/powerdns.nix
+  ./services/networking/pdns-recursor.nix
+  ./services/networking/pppd.nix
+  ./services/networking/pptpd.nix
+  ./services/networking/prayer.nix
+  ./services/networking/privoxy.nix
+  ./services/networking/prosody.nix
+  ./services/networking/quassel.nix
+  ./services/networking/quorum.nix
+  ./services/networking/quicktun.nix
+  ./services/networking/radicale.nix
+  ./services/networking/radvd.nix
+  ./services/networking/rdnssd.nix
+  ./services/networking/redsocks.nix
+  ./services/networking/resilio.nix
+  ./services/networking/robustirc-bridge.nix
+  ./services/networking/rpcbind.nix
+  ./services/networking/rxe.nix
+  ./services/networking/sabnzbd.nix
+  ./services/networking/seafile.nix
+  ./services/networking/searx.nix
+  ./services/networking/skydns.nix
+  ./services/networking/shadowsocks.nix
+  ./services/networking/shairport-sync.nix
+  ./services/networking/shellhub-agent.nix
+  ./services/networking/shorewall.nix
+  ./services/networking/shorewall6.nix
+  ./services/networking/shout.nix
+  ./services/networking/sniproxy.nix
+  ./services/networking/snowflake-proxy.nix
+  ./services/networking/smartdns.nix
+  ./services/networking/smokeping.nix
+  ./services/networking/softether.nix
+  ./services/networking/solanum.nix
+  ./services/networking/soju.nix
+  ./services/networking/spacecookie.nix
+  ./services/networking/spiped.nix
+  ./services/networking/squid.nix
+  ./services/networking/sslh.nix
+  ./services/networking/ssh/lshd.nix
+  ./services/networking/ssh/sshd.nix
+  ./services/networking/strongswan.nix
+  ./services/networking/strongswan-swanctl/module.nix
+  ./services/networking/stunnel.nix
+  ./services/networking/stubby.nix
+  ./services/networking/supplicant.nix
+  ./services/networking/supybot.nix
+  ./services/networking/syncthing.nix
+  ./services/networking/syncthing-relay.nix
+  ./services/networking/syncplay.nix
+  ./services/networking/tailscale.nix
+  ./services/networking/tcpcrypt.nix
+  ./services/networking/teamspeak3.nix
+  ./services/networking/tedicross.nix
+  ./services/networking/tetrd.nix
+  ./services/networking/teleport.nix
+  ./services/networking/thelounge.nix
+  ./services/networking/tinc.nix
+  ./services/networking/tinydns.nix
+  ./services/networking/tftpd.nix
+  ./services/networking/trickster.nix
+  ./services/networking/tox-bootstrapd.nix
+  ./services/networking/tox-node.nix
+  ./services/networking/toxvpn.nix
+  ./services/networking/tvheadend.nix
+  ./services/networking/ucarp.nix
+  ./services/networking/unbound.nix
+  ./services/networking/unifi.nix
+  ./services/video/unifi-video.nix
+  ./services/video/rtsp-simple-server.nix
+  ./services/networking/v2ray.nix
+  ./services/networking/vsftpd.nix
+  ./services/networking/wasabibackend.nix
+  ./services/networking/websockify.nix
+  ./services/networking/wg-netmanager.nix
+  ./services/networking/wg-quick.nix
+  ./services/networking/wireguard.nix
+  ./services/networking/wpa_supplicant.nix
+  ./services/networking/xandikos.nix
+  ./services/networking/xinetd.nix
+  ./services/networking/xl2tpd.nix
+  ./services/networking/x2goserver.nix
+  ./services/networking/xrdp.nix
+  ./services/networking/yggdrasil.nix
+  ./services/networking/zerobin.nix
+  ./services/networking/zeronet.nix
+  ./services/networking/zerotierone.nix
+  ./services/networking/znc/default.nix
+  ./services/printing/cupsd.nix
+  ./services/scheduling/atd.nix
+  ./services/scheduling/cron.nix
+  ./services/scheduling/fcron.nix
+  ./services/search/elasticsearch.nix
+  ./services/search/elasticsearch-curator.nix
+  ./services/search/hound.nix
+  ./services/search/kibana.nix
+  ./services/search/meilisearch.nix
+  ./services/search/solr.nix
+  ./services/security/aesmd.nix
+  ./services/security/certmgr.nix
+  ./services/security/cfssl.nix
+  ./services/security/clamav.nix
+  ./services/security/fail2ban.nix
+  ./services/security/fprintd.nix
+  ./services/security/haka.nix
+  ./services/security/haveged.nix
+  ./services/security/hockeypuck.nix
+  ./services/security/hologram-server.nix
+  ./services/security/hologram-agent.nix
+  ./services/security/munge.nix
+  ./services/security/nginx-sso.nix
+  ./services/security/oauth2_proxy.nix
+  ./services/security/oauth2_proxy_nginx.nix
+  ./services/security/opensnitch.nix
+  ./services/security/privacyidea.nix
+  ./services/security/physlock.nix
+  ./services/security/shibboleth-sp.nix
+  ./services/security/sks.nix
+  ./services/security/sshguard.nix
+  ./services/security/step-ca.nix
+  ./services/security/tor.nix
+  ./services/security/torify.nix
+  ./services/security/torsocks.nix
+  ./services/security/usbguard.nix
+  ./services/security/vault.nix
+  ./services/security/vaultwarden/default.nix
+  ./services/security/yubikey-agent.nix
+  ./services/system/cachix-agent/default.nix
+  ./services/system/cloud-init.nix
+  ./services/system/dbus.nix
+  ./services/system/earlyoom.nix
+  ./services/system/localtime.nix
+  ./services/system/kerberos/default.nix
+  ./services/system/nscd.nix
+  ./services/system/saslauthd.nix
+  ./services/system/self-deploy.nix
+  ./services/system/systembus-notify.nix
+  ./services/system/uptimed.nix
+  ./services/torrent/deluge.nix
+  ./services/torrent/flexget.nix
+  ./services/torrent/magnetico.nix
+  ./services/torrent/opentracker.nix
+  ./services/torrent/peerflix.nix
+  ./services/torrent/rtorrent.nix
+  ./services/torrent/transmission.nix
+  ./services/ttys/getty.nix
+  ./services/ttys/gpm.nix
+  ./services/ttys/kmscon.nix
+  ./services/wayland/cage.nix
+  ./services/video/epgstation/default.nix
+  ./services/video/mirakurun.nix
+  ./services/video/replay-sorcery.nix
+  ./services/web-apps/atlassian/confluence.nix
+  ./services/web-apps/atlassian/crowd.nix
+  ./services/web-apps/atlassian/jira.nix
+  ./services/web-apps/bookstack.nix
+  ./services/web-apps/calibre-web.nix
+  ./services/web-apps/code-server.nix
+  ./services/web-apps/baget.nix
+  ./services/web-apps/convos.nix
+  ./services/web-apps/cryptpad.nix
+  ./services/web-apps/dex.nix
+  ./services/web-apps/discourse.nix
+  ./services/web-apps/documize.nix
+  ./services/web-apps/dokuwiki.nix
+  ./services/web-apps/engelsystem.nix
+  ./services/web-apps/ethercalc.nix
+  ./services/web-apps/fluidd.nix
+  ./services/web-apps/galene.nix
+  ./services/web-apps/gerrit.nix
+  ./services/web-apps/gotify-server.nix
+  ./services/web-apps/grocy.nix
+  ./services/web-apps/hedgedoc.nix
+  ./services/web-apps/hledger-web.nix
+  ./services/web-apps/icingaweb2/icingaweb2.nix
+  ./services/web-apps/icingaweb2/module-monitoring.nix
+  ./services/web-apps/ihatemoney
+  ./services/web-apps/isso.nix
+  ./services/web-apps/jirafeau.nix
+  ./services/web-apps/jitsi-meet.nix
+  ./services/web-apps/keycloak.nix
+  ./services/web-apps/lemmy.nix
+  ./services/web-apps/invidious.nix
+  ./services/web-apps/invoiceplane.nix
+  ./services/web-apps/limesurvey.nix
+  ./services/web-apps/mastodon.nix
+  ./services/web-apps/mattermost.nix
+  ./services/web-apps/mediawiki.nix
+  ./services/web-apps/miniflux.nix
+  ./services/web-apps/moodle.nix
+  ./services/web-apps/nextcloud.nix
+  ./services/web-apps/nexus.nix
+  ./services/web-apps/node-red.nix
+  ./services/web-apps/pict-rs.nix
+  ./services/web-apps/peertube.nix
+  ./services/web-apps/plantuml-server.nix
+  ./services/web-apps/plausible.nix
+  ./services/web-apps/pgpkeyserver-lite.nix
+  ./services/web-apps/powerdns-admin.nix
+  ./services/web-apps/prosody-filer.nix
+  ./services/web-apps/matomo.nix
+  ./services/web-apps/openwebrx.nix
+  ./services/web-apps/restya-board.nix
+  ./services/web-apps/sogo.nix
+  ./services/web-apps/rss-bridge.nix
+  ./services/web-apps/tt-rss.nix
+  ./services/web-apps/trilium.nix
+  ./services/web-apps/selfoss.nix
+  ./services/web-apps/shiori.nix
+  ./services/web-apps/vikunja.nix
+  ./services/web-apps/virtlyst.nix
+  ./services/web-apps/wiki-js.nix
+  ./services/web-apps/whitebophir.nix
+  ./services/web-apps/wordpress.nix
+  ./services/web-apps/youtrack.nix
+  ./services/web-apps/zabbix.nix
+  ./services/web-servers/agate.nix
+  ./services/web-servers/apache-httpd/default.nix
+  ./services/web-servers/caddy/default.nix
+  ./services/web-servers/darkhttpd.nix
+  ./services/web-servers/fcgiwrap.nix
+  ./services/web-servers/hitch/default.nix
+  ./services/web-servers/hydron.nix
+  ./services/web-servers/jboss/default.nix
+  ./services/web-servers/lighttpd/cgit.nix
+  ./services/web-servers/lighttpd/collectd.nix
+  ./services/web-servers/lighttpd/default.nix
+  ./services/web-servers/lighttpd/gitweb.nix
+  ./services/web-servers/mighttpd2.nix
+  ./services/web-servers/minio.nix
+  ./services/web-servers/molly-brown.nix
+  ./services/web-servers/nginx/default.nix
+  ./services/web-servers/nginx/gitweb.nix
+  ./services/web-servers/phpfpm/default.nix
+  ./services/web-servers/pomerium.nix
+  ./services/web-servers/unit/default.nix
+  ./services/web-servers/tomcat.nix
+  ./services/web-servers/traefik.nix
+  ./services/web-servers/trafficserver/default.nix
+  ./services/web-servers/ttyd.nix
+  ./services/web-servers/uwsgi.nix
+  ./services/web-servers/varnish/default.nix
+  ./services/web-servers/zope2.nix
+  ./services/x11/extra-layouts.nix
+  ./services/x11/clight.nix
+  ./services/x11/colord.nix
+  ./services/x11/picom.nix
+  ./services/x11/unclutter.nix
+  ./services/x11/unclutter-xfixes.nix
+  ./services/x11/desktop-managers/default.nix
+  ./services/x11/display-managers/default.nix
+  ./services/x11/display-managers/gdm.nix
+  ./services/x11/display-managers/lightdm.nix
+  ./services/x11/display-managers/sddm.nix
+  ./services/x11/display-managers/slim.nix
+  ./services/x11/display-managers/startx.nix
+  ./services/x11/display-managers/sx.nix
+  ./services/x11/display-managers/xpra.nix
+  ./services/x11/fractalart.nix
+  ./services/x11/hardware/libinput.nix
+  ./services/x11/hardware/synaptics.nix
+  ./services/x11/hardware/wacom.nix
+  ./services/x11/hardware/digimend.nix
+  ./services/x11/hardware/cmt.nix
+  ./services/x11/gdk-pixbuf.nix
+  ./services/x11/imwheel.nix
+  ./services/x11/redshift.nix
+  ./services/x11/touchegg.nix
+  ./services/x11/urserver.nix
+  ./services/x11/urxvtd.nix
+  ./services/x11/window-managers/awesome.nix
+  ./services/x11/window-managers/default.nix
+  ./services/x11/window-managers/clfswm.nix
+  ./services/x11/window-managers/fluxbox.nix
+  ./services/x11/window-managers/icewm.nix
+  ./services/x11/window-managers/bspwm.nix
+  ./services/x11/window-managers/metacity.nix
+  ./services/x11/window-managers/none.nix
+  ./services/x11/window-managers/twm.nix
+  ./services/x11/window-managers/windowlab.nix
+  ./services/x11/window-managers/wmii.nix
+  ./services/x11/window-managers/xmonad.nix
+  ./services/x11/xautolock.nix
+  ./services/x11/xbanish.nix
+  ./services/x11/xfs.nix
+  ./services/x11/xserver.nix
+  ./system/activation/activation-script.nix
+  ./system/activation/top-level.nix
+  ./system/boot/binfmt.nix
+  ./system/boot/emergency-mode.nix
+  ./system/boot/grow-partition.nix
+  ./system/boot/initrd-network.nix
+  ./system/boot/initrd-ssh.nix
+  ./system/boot/initrd-openvpn.nix
+  ./system/boot/kernel.nix
+  ./system/boot/kexec.nix
+  ./system/boot/loader/efi.nix
+  ./system/boot/loader/generations-dir/generations-dir.nix
+  ./system/boot/loader/generic-extlinux-compatible
+  ./system/boot/loader/grub/grub.nix
+  ./system/boot/loader/grub/ipxe.nix
+  ./system/boot/loader/grub/memtest.nix
+  ./system/boot/loader/init-script/init-script.nix
+  ./system/boot/loader/loader.nix
+  ./system/boot/loader/raspberrypi/raspberrypi.nix
+  ./system/boot/loader/systemd-boot/systemd-boot.nix
+  ./system/boot/luksroot.nix
+  ./system/boot/modprobe.nix
+  ./system/boot/networkd.nix
+  ./system/boot/plymouth.nix
+  ./system/boot/resolved.nix
+  ./system/boot/shutdown.nix
+  ./system/boot/stage-1.nix
+  ./system/boot/stage-2.nix
+  ./system/boot/systemd.nix
+  ./system/boot/systemd-nspawn.nix
+  ./system/boot/timesyncd.nix
+  ./system/boot/tmp.nix
+  ./system/etc/etc-activation.nix
+  ./tasks/auto-upgrade.nix
+  ./tasks/bcache.nix
+  ./tasks/cpu-freq.nix
+  ./tasks/encrypted-devices.nix
+  ./tasks/filesystems.nix
+  ./tasks/filesystems/apfs.nix
+  ./tasks/filesystems/bcachefs.nix
+  ./tasks/filesystems/btrfs.nix
+  ./tasks/filesystems/cifs.nix
+  ./tasks/filesystems/ecryptfs.nix
+  ./tasks/filesystems/exfat.nix
+  ./tasks/filesystems/ext.nix
+  ./tasks/filesystems/f2fs.nix
+  ./tasks/filesystems/jfs.nix
+  ./tasks/filesystems/nfs.nix
+  ./tasks/filesystems/ntfs.nix
+  ./tasks/filesystems/reiserfs.nix
+  ./tasks/filesystems/unionfs-fuse.nix
+  ./tasks/filesystems/vboxsf.nix
+  ./tasks/filesystems/vfat.nix
+  ./tasks/filesystems/xfs.nix
+  ./tasks/filesystems/zfs.nix
+  ./tasks/lvm.nix
+  ./tasks/network-interfaces.nix
+  ./tasks/network-interfaces-systemd.nix
+  ./tasks/network-interfaces-scripted.nix
+  ./tasks/scsi-link-power-management.nix
+  ./tasks/snapraid.nix
+  ./tasks/swraid.nix
+  ./tasks/trackpoint.nix
+  ./tasks/powertop.nix
+  ./testing/service-runner.nix
+  ./virtualisation/anbox.nix
+  ./virtualisation/build-vm.nix
+  ./virtualisation/container-config.nix
+  ./virtualisation/containerd.nix
+  ./virtualisation/containers.nix
+  ./virtualisation/nixos-containers.nix
+  ./virtualisation/oci-containers.nix
+  ./virtualisation/cri-o.nix
+  ./virtualisation/docker.nix
+  ./virtualisation/docker-rootless.nix
+  ./virtualisation/ecs-agent.nix
+  ./virtualisation/libvirtd.nix
+  ./virtualisation/lxc.nix
+  ./virtualisation/lxcfs.nix
+  ./virtualisation/lxd.nix
+  ./virtualisation/amazon-options.nix
+  ./virtualisation/hyperv-guest.nix
+  ./virtualisation/kvmgt.nix
+  ./virtualisation/openvswitch.nix
+  ./virtualisation/parallels-guest.nix
+  ./virtualisation/podman/default.nix
+  ./virtualisation/qemu-guest-agent.nix
+  ./virtualisation/railcar.nix
+  ./virtualisation/spice-usb-redirection.nix
+  ./virtualisation/virtualbox-guest.nix
+  ./virtualisation/virtualbox-host.nix
+  ./virtualisation/vmware-guest.nix
+  ./virtualisation/waydroid.nix
+  ./virtualisation/xen-dom0.nix
+  ./virtualisation/xe-guest-utilities.nix
+]
diff --git a/nixos/modules/profiles/all-hardware.nix b/nixos/modules/profiles/all-hardware.nix
new file mode 100644
index 00000000000..25f68123a1d
--- /dev/null
+++ b/nixos/modules/profiles/all-hardware.nix
@@ -0,0 +1,120 @@
+# This module 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.
+
+{ pkgs, lib,... }:
+let
+  platform = pkgs.stdenv.hostPlatform;
+in
+{
+
+  # The initrd has to contain any module that might be necessary for
+  # supporting the most important parts of HW like drives.
+  boot.initrd.availableKernelModules =
+    [ # SATA/PATA support.
+      "ahci"
+
+      "ata_piix"
+
+      "sata_inic162x" "sata_nv" "sata_promise" "sata_qstor"
+      "sata_sil" "sata_sil24" "sata_sis" "sata_svw" "sata_sx4"
+      "sata_uli" "sata_via" "sata_vsc"
+
+      "pata_ali" "pata_amd" "pata_artop" "pata_atiixp" "pata_efar"
+      "pata_hpt366" "pata_hpt37x" "pata_hpt3x2n" "pata_hpt3x3"
+      "pata_it8213" "pata_it821x" "pata_jmicron" "pata_marvell"
+      "pata_mpiix" "pata_netcell" "pata_ns87410" "pata_oldpiix"
+      "pata_pcmcia" "pata_pdc2027x" "pata_qdi" "pata_rz1000"
+      "pata_serverworks" "pata_sil680" "pata_sis"
+      "pata_sl82c105" "pata_triflex" "pata_via"
+      "pata_winbond"
+
+      # SCSI support (incomplete).
+      "3w-9xxx" "3w-xxxx" "aic79xx" "aic7xxx" "arcmsr"
+
+      # USB support, especially for booting from USB CD-ROM
+      # drives.
+      "uas"
+
+      # SD cards.
+      "sdhci_pci"
+
+      # Firewire support.  Not tested.
+      "ohci1394" "sbp2"
+
+      # Virtio (QEMU, KVM etc.) support.
+      "virtio_net" "virtio_pci" "virtio_mmio" "virtio_blk" "virtio_scsi" "virtio_balloon" "virtio_console"
+
+      # VMware support.
+      "mptspi" "vmxnet3" "vsock"
+    ] ++ lib.optional platform.isx86 "vmw_balloon"
+    ++ lib.optionals (pkgs.stdenv.isi686 || pkgs.stdenv.isx86_64) [
+      "vmw_vmci" "vmwgfx" "vmw_vsock_vmci_transport"
+
+      # Hyper-V support.
+      "hv_storvsc"
+    ] ++ lib.optionals (pkgs.stdenv.isAarch32 || pkgs.stdenv.isAarch64) [
+      # Most of the following falls into two categories:
+      #  - early KMS / early display
+      #  - early storage (e.g. USB) support
+
+      # Allows using framebuffer configured by the initial boot firmware
+      "simplefb"
+
+      # Allwinner support
+
+      # Required for early KMS
+      "sun4i-drm"
+      "sun8i-mixer" # Audio, but required for kms
+
+      # PWM for the backlight
+      "pwm-sun4i"
+
+      # Broadcom
+
+      "vc4"
+    ] ++ lib.optionals pkgs.stdenv.isAarch64 [
+      # Most of the following falls into two categories:
+      #  - early KMS / early display
+      #  - early storage (e.g. USB) support
+
+      # Broadcom
+
+      "pcie-brcmstb"
+
+      # Rockchip
+      "dw-hdmi"
+      "dw-mipi-dsi"
+      "rockchipdrm"
+      "rockchip-rga"
+      "phy-rockchip-pcie"
+      "pcie-rockchip-host"
+
+      # Misc. uncategorized hardware
+
+      # Used for some platform's integrated displays
+      "panel-simple"
+      "pwm-bl"
+
+      # Power supply drivers, some platforms need them for USB
+      "axp20x-ac-power"
+      "axp20x-battery"
+      "pinctrl-axp209"
+      "mp8859"
+
+      # USB drivers
+      "xhci-pci-renesas"
+
+      # Misc "weak" dependencies
+      "analogix-dp"
+      "analogix-anx6345" # For DP or eDP (e.g. integrated display)
+    ];
+
+  # Include lots of firmware.
+  hardware.enableRedistributableFirmware = true;
+
+  imports =
+    [ ../hardware/network/zydas-zd1211.nix ];
+
+}
diff --git a/nixos/modules/profiles/base.nix b/nixos/modules/profiles/base.nix
new file mode 100644
index 00000000000..33dd80d7c5a
--- /dev/null
+++ b/nixos/modules/profiles/base.nix
@@ -0,0 +1,58 @@
+# This module defines the software packages included in the "minimal"
+# installation CD.  It might be useful elsewhere.
+
+{ lib, pkgs, ... }:
+
+{
+  # Include some utilities that are useful for installing or repairing
+  # the system.
+  environment.systemPackages = [
+    pkgs.w3m-nographics # needed for the manual anyway
+    pkgs.testdisk # useful for repairing boot problems
+    pkgs.ms-sys # for writing Microsoft boot sectors / MBRs
+    pkgs.efibootmgr
+    pkgs.efivar
+    pkgs.parted
+    pkgs.gptfdisk
+    pkgs.ddrescue
+    pkgs.ccrypt
+    pkgs.cryptsetup # needed for dm-crypt volumes
+    pkgs.mkpasswd # for generating password files
+
+    # Some text editors.
+    pkgs.vim
+
+    # Some networking tools.
+    pkgs.fuse
+    pkgs.fuse3
+    pkgs.sshfs-fuse
+    pkgs.rsync
+    pkgs.socat
+    pkgs.screen
+
+    # Hardware-related tools.
+    pkgs.sdparm
+    pkgs.hdparm
+    pkgs.smartmontools # for diagnosing hard disks
+    pkgs.pciutils
+    pkgs.usbutils
+
+    # 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.
+  boot.supportedFilesystems = [ "btrfs" "reiserfs" "vfat" "f2fs" "xfs" "zfs" "ntfs" "cifs" ];
+
+  # Configure host id for ZFS to work
+  networking.hostId = lib.mkDefault "8425e349";
+}
diff --git a/nixos/modules/profiles/clone-config.nix b/nixos/modules/profiles/clone-config.nix
new file mode 100644
index 00000000000..3f669ba7d2e
--- /dev/null
+++ b/nixos/modules/profiles/clone-config.nix
@@ -0,0 +1,109 @@
+{ config, lib, pkgs, modules, ... }:
+
+with lib;
+
+let
+
+  # Location of the repository on the harddrive
+  nixosPath = toString ../..;
+
+  # Check if the path is from the NixOS repository
+  isNixOSFile = path:
+    let s = toString path; in
+      removePrefix nixosPath s != s;
+
+  # Copy modules given as extra configuration files.  Unfortunately, we
+  # cannot serialized attribute set given in the list of modules (that's why
+  # you should use files).
+  moduleFiles =
+    # FIXME: use typeOf (Nix 1.6.1).
+    filter (x: !isAttrs x && !lib.isFunction x) modules;
+
+  # Partition module files because between NixOS and non-NixOS files.  NixOS
+  # files may change if the repository is updated.
+  partitionedModuleFiles =
+    let p = partition isNixOSFile moduleFiles; in
+    { nixos = p.right; others = p.wrong; };
+
+  # Path transformed to be valid on the installation device.  Thus the
+  # device configuration could be rebuild.
+  relocatedModuleFiles =
+    let
+      relocateNixOS = path:
+        "<nixpkgs/nixos" + removePrefix nixosPath (toString path) + ">";
+    in
+      { nixos = map relocateNixOS partitionedModuleFiles.nixos;
+        others = []; # TODO: copy the modules to the install-device repository.
+      };
+
+  # A dummy /etc/nixos/configuration.nix in the booted CD that
+  # rebuilds the CD's configuration (and allows the configuration to
+  # be modified, of course, providing a true live CD).  Problem is
+  # that we don't really know how the CD was built - the Nix
+  # expression language doesn't allow us to query the expression being
+  # evaluated.  So we'll just hope for the best.
+  configClone = pkgs.writeText "configuration.nix"
+    ''
+      { config, pkgs, ... }:
+
+      {
+        imports = [ ${toString config.installer.cloneConfigIncludes} ];
+
+        ${config.installer.cloneConfigExtra}
+      }
+    '';
+
+in
+
+{
+
+  options = {
+
+    installer.cloneConfig = mkOption {
+      default = true;
+      description = ''
+        Try to clone the installation-device configuration by re-using it's
+        profile from the list of imported modules.
+      '';
+    };
+
+    installer.cloneConfigIncludes = mkOption {
+      default = [];
+      example = [ "./nixos/modules/hardware/network/rt73.nix" ];
+      description = ''
+        List of modules used to re-build this installation device profile.
+      '';
+    };
+
+    installer.cloneConfigExtra = mkOption {
+      default = "";
+      description = ''
+        Extra text to include in the cloned configuration.nix included in this
+        installer.
+      '';
+    };
+  };
+
+  config = {
+
+    installer.cloneConfigIncludes =
+      relocatedModuleFiles.nixos ++ relocatedModuleFiles.others;
+
+    boot.postBootCommands =
+      ''
+        # Provide a mount point for nixos-install.
+        mkdir -p /mnt
+
+        ${optionalString config.installer.cloneConfig ''
+          # Provide a configuration for the CD/DVD itself, to allow users
+          # to run nixos-rebuild to change the configuration of the
+          # running system on the CD/DVD.
+          if ! [ -e /etc/nixos/configuration.nix ]; then
+            cp ${configClone} /etc/nixos/configuration.nix
+          fi
+       ''}
+      '';
+
+  };
+
+}
diff --git a/nixos/modules/profiles/demo.nix b/nixos/modules/profiles/demo.nix
new file mode 100644
index 00000000000..4e8c74deedb
--- /dev/null
+++ b/nixos/modules/profiles/demo.nix
@@ -0,0 +1,21 @@
+{ ... }:
+
+{
+  imports = [ ./graphical.nix ];
+
+  users.users.demo =
+    { isNormalUser = true;
+      description = "Demo user account";
+      extraGroups = [ "wheel" ];
+      password = "demo";
+      uid = 1000;
+    };
+
+  services.xserver.displayManager = {
+    autoLogin = {
+      enable = true;
+      user = "demo";
+    };
+    sddm.autoLogin.relogin = true;
+  };
+}
diff --git a/nixos/modules/profiles/docker-container.nix b/nixos/modules/profiles/docker-container.nix
new file mode 100644
index 00000000000..183645de36f
--- /dev/null
+++ b/nixos/modules/profiles/docker-container.nix
@@ -0,0 +1,61 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let inherit (pkgs) writeScript; in
+
+let
+ pkgs2storeContents = l : map (x: { object = x; symlink = "none"; }) l;
+
+in {
+  # Docker image config.
+  imports = [
+    ../installer/cd-dvd/channel.nix
+    ./minimal.nix
+    ./clone-config.nix
+  ];
+
+  # Create the tarball
+  system.build.tarball = pkgs.callPackage ../../lib/make-system-tarball.nix {
+    contents = [
+      {
+        source = "${config.system.build.toplevel}/.";
+        target = "./";
+      }
+    ];
+    extraArgs = "--owner=0";
+
+    # Add init script to image
+    storeContents = pkgs2storeContents [
+      config.system.build.toplevel
+      pkgs.stdenv
+    ];
+
+    # Some container managers like lxc need these
+    extraCommands =
+      let script = writeScript "extra-commands.sh" ''
+            rm etc
+            mkdir -p proc sys dev etc
+          '';
+      in script;
+  };
+
+  boot.isContainer = true;
+  boot.postBootCommands =
+    ''
+      # After booting, register the contents of the Nix store in the Nix
+      # database.
+      if [ -f /nix-path-registration ]; then
+        ${config.nix.package.out}/bin/nix-store --load-db < /nix-path-registration &&
+        rm /nix-path-registration
+      fi
+
+      # nixos-rebuild also requires a "system" profile
+      ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
+    '';
+
+  # Install new init script
+  system.activationScripts.installInitScript = ''
+    ln -fs $systemConfig/init /init
+  '';
+}
diff --git a/nixos/modules/profiles/graphical.nix b/nixos/modules/profiles/graphical.nix
new file mode 100644
index 00000000000..d80456cede5
--- /dev/null
+++ b/nixos/modules/profiles/graphical.nix
@@ -0,0 +1,20 @@
+# This module defines a NixOS configuration with the Plasma 5 desktop.
+# It's used by the graphical installation CD.
+
+{ pkgs, ... }:
+
+{
+  services.xserver = {
+    enable = true;
+    displayManager.sddm.enable = true;
+    desktopManager.plasma5 = {
+      enable = true;
+    };
+    libinput.enable = true; # for touchpad support on many laptops
+  };
+
+  # Enable sound in virtualbox appliances.
+  hardware.pulseaudio.enable = true;
+
+  environment.systemPackages = [ pkgs.glxinfo pkgs.firefox ];
+}
diff --git a/nixos/modules/profiles/hardened.nix b/nixos/modules/profiles/hardened.nix
new file mode 100644
index 00000000000..856ee480fc0
--- /dev/null
+++ b/nixos/modules/profiles/hardened.nix
@@ -0,0 +1,118 @@
+# A profile with most (vanilla) hardening options enabled by default,
+# potentially at the cost of stability, features and performance.
+#
+# This profile enables options that are known to affect system
+# stability. If you experience any stability issues when using the
+# profile, try disabling it. If you report an issue and use this
+# profile, always mention that you do.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  meta = {
+    maintainers = [ maintainers.joachifm maintainers.emily ];
+  };
+
+  boot.kernelPackages = mkDefault pkgs.linuxPackages_hardened;
+
+  nix.settings.allowed-users = mkDefault [ "@users" ];
+
+  environment.memoryAllocator.provider = mkDefault "scudo";
+  environment.variables.SCUDO_OPTIONS = mkDefault "ZeroContents=1";
+
+  security.lockKernelModules = mkDefault true;
+
+  security.protectKernelImage = mkDefault true;
+
+  security.allowSimultaneousMultithreading = mkDefault false;
+
+  security.forcePageTableIsolation = mkDefault true;
+
+  # This is required by podman to run containers in rootless mode.
+  security.unprivilegedUsernsClone = mkDefault config.virtualisation.containers.enable;
+
+  security.virtualisation.flushL1DataCache = mkDefault "always";
+
+  security.apparmor.enable = mkDefault true;
+  security.apparmor.killUnconfinedConfinables = mkDefault true;
+
+  boot.kernelParams = [
+    # Slab/slub sanity checks, redzoning, and poisoning
+    "slub_debug=FZP"
+
+    # Overwrite free'd memory
+    "page_poison=1"
+
+    # Enable page allocator randomization
+    "page_alloc.shuffle=1"
+  ];
+
+  boot.blacklistedKernelModules = [
+    # Obscure network protocols
+    "ax25"
+    "netrom"
+    "rose"
+
+    # Old or rare or insufficiently audited filesystems
+    "adfs"
+    "affs"
+    "bfs"
+    "befs"
+    "cramfs"
+    "efs"
+    "erofs"
+    "exofs"
+    "freevxfs"
+    "f2fs"
+    "hfs"
+    "hpfs"
+    "jfs"
+    "minix"
+    "nilfs2"
+    "ntfs"
+    "omfs"
+    "qnx4"
+    "qnx6"
+    "sysv"
+    "ufs"
+  ];
+
+  # Restrict ptrace() usage to processes with a pre-defined relationship
+  # (e.g., parent/child)
+  boot.kernel.sysctl."kernel.yama.ptrace_scope" = mkOverride 500 1;
+
+  # Hide kptrs even for processes with CAP_SYSLOG
+  boot.kernel.sysctl."kernel.kptr_restrict" = mkOverride 500 2;
+
+  # Disable bpf() JIT (to eliminate spray attacks)
+  boot.kernel.sysctl."net.core.bpf_jit_enable" = mkDefault false;
+
+  # Disable ftrace debugging
+  boot.kernel.sysctl."kernel.ftrace_enabled" = mkDefault false;
+
+  # Enable strict reverse path filtering (that is, do not attempt to route
+  # packets that "obviously" do not belong to the iface's network; dropped
+  # packets are logged as martians).
+  boot.kernel.sysctl."net.ipv4.conf.all.log_martians" = mkDefault true;
+  boot.kernel.sysctl."net.ipv4.conf.all.rp_filter" = mkDefault "1";
+  boot.kernel.sysctl."net.ipv4.conf.default.log_martians" = mkDefault true;
+  boot.kernel.sysctl."net.ipv4.conf.default.rp_filter" = mkDefault "1";
+
+  # Ignore broadcast ICMP (mitigate SMURF)
+  boot.kernel.sysctl."net.ipv4.icmp_echo_ignore_broadcasts" = mkDefault true;
+
+  # Ignore incoming ICMP redirects (note: default is needed to ensure that the
+  # setting is applied to interfaces added after the sysctls are set)
+  boot.kernel.sysctl."net.ipv4.conf.all.accept_redirects" = mkDefault false;
+  boot.kernel.sysctl."net.ipv4.conf.all.secure_redirects" = mkDefault false;
+  boot.kernel.sysctl."net.ipv4.conf.default.accept_redirects" = mkDefault false;
+  boot.kernel.sysctl."net.ipv4.conf.default.secure_redirects" = mkDefault false;
+  boot.kernel.sysctl."net.ipv6.conf.all.accept_redirects" = mkDefault false;
+  boot.kernel.sysctl."net.ipv6.conf.default.accept_redirects" = mkDefault false;
+
+  # Ignore outgoing ICMP redirects (this is ipv4 only)
+  boot.kernel.sysctl."net.ipv4.conf.all.send_redirects" = mkDefault false;
+  boot.kernel.sysctl."net.ipv4.conf.default.send_redirects" = mkDefault false;
+}
diff --git a/nixos/modules/profiles/headless.nix b/nixos/modules/profiles/headless.nix
new file mode 100644
index 00000000000..c17cb287b72
--- /dev/null
+++ b/nixos/modules/profiles/headless.nix
@@ -0,0 +1,25 @@
+# Common configuration for headless machines (e.g., Amazon EC2
+# instances).
+
+{ lib, ... }:
+
+with lib;
+
+{
+  boot.vesa = false;
+
+  # Don't start a tty on the serial consoles.
+  systemd.services."serial-getty@ttyS0".enable = lib.mkDefault false;
+  systemd.services."serial-getty@hvc0".enable = false;
+  systemd.services."getty@tty1".enable = false;
+  systemd.services."autovt@".enable = false;
+
+  # Since we can't manually respond to a panic, just reboot.
+  boot.kernelParams = [ "panic=1" "boot.panic_on_fail" ];
+
+  # Don't allow emergency mode, because we don't have a console.
+  systemd.enableEmergencyMode = false;
+
+  # Being headless, we don't need a GRUB splash image.
+  boot.loader.grub.splashImage = null;
+}
diff --git a/nixos/modules/profiles/installation-device.nix b/nixos/modules/profiles/installation-device.nix
new file mode 100644
index 00000000000..3c503fba2a3
--- /dev/null
+++ b/nixos/modules/profiles/installation-device.nix
@@ -0,0 +1,117 @@
+# Provide a basic configuration for installation devices like CDs.
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+  imports =
+    [ # Enable devices which are usually scanned, because we don't know the
+      # target system.
+      ../installer/scan/detected.nix
+      ../installer/scan/not-detected.nix
+
+      # Allow "nixos-rebuild" to work properly by providing
+      # /etc/nixos/configuration.nix.
+      ./clone-config.nix
+
+      # Include a copy of Nixpkgs so that nixos-install works out of
+      # the box.
+      ../installer/cd-dvd/channel.nix
+    ];
+
+  config = {
+
+    # Enable in installer, even if the minimal profile disables it.
+    documentation.enable = mkForce true;
+
+    # Show the manual.
+    documentation.nixos.enable = mkForce true;
+
+    # Use less privileged nixos user
+    users.users.nixos = {
+      isNormalUser = true;
+      extraGroups = [ "wheel" "networkmanager" "video" ];
+      # Allow the graphical user to login without password
+      initialHashedPassword = "";
+    };
+
+    # Allow the user to log in as root without a password.
+    users.users.root.initialHashedPassword = "";
+
+    # Allow passwordless sudo from nixos user
+    security.sudo = {
+      enable = mkDefault true;
+      wheelNeedsPassword = mkForce false;
+    };
+
+    # Automatically log in at the virtual consoles.
+    services.getty.autologinUser = "nixos";
+
+    # Some more help text.
+    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.
+
+      If you need a wireless connection, type
+      `sudo systemctl start wpa_supplicant` and configure a
+      network using `wpa_cli`. See the NixOS manual for details.
+    '' + optionalString config.services.xserver.enable ''
+
+      Type `sudo systemctl start display-manager' to
+      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.
+    # 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";
+    };
+
+    # Enable wpa_supplicant, but don't start it by default.
+    networking.wireless.enable = mkDefault true;
+    networking.wireless.userControlled.enable = true;
+    systemd.services.wpa_supplicant.wantedBy = mkOverride 50 [];
+
+    # Tell the Nix evaluator to garbage collect more aggressively.
+    # This is desirable in memory-constrained environments that don't
+    # (yet) have swap set up.
+    environment.variables.GC_INITIAL_HEAP_SIZE = "1M";
+
+    # Make the installer more likely to succeed in low memory
+    # environments.  The kernel's overcommit heustistics bite us
+    # fairly often, preventing processes such as nix-worker or
+    # download-using-manifests.pl from forking even if there is
+    # plenty of free memory.
+    boot.kernel.sysctl."vm.overcommit_memory" = "1";
+
+    # To speed up installation a little bit, include the complete
+    # stdenv in the Nix store on the CD.
+    system.extraDependencies = with pkgs;
+      [
+        stdenv
+        stdenvNoCC # for runCommand
+        busybox
+        jq # for closureInfo
+      ];
+
+    # Show all debug messages from the kernel but don't log refused packets
+    # because we have the firewall enabled. This makes installs from the
+    # console less cumbersome if the machine has a public IP.
+    networking.firewall.logRefusedConnections = mkDefault false;
+
+    # Prevent installation media from evacuating persistent storage, as their
+    # var directory is not persistent and it would thus result in deletion of
+    # those entries.
+    environment.etc."systemd/pstore.conf".text = ''
+      [PStore]
+      Unlink=no
+    '';
+  };
+}
diff --git a/nixos/modules/profiles/minimal.nix b/nixos/modules/profiles/minimal.nix
new file mode 100644
index 00000000000..e79b9272384
--- /dev/null
+++ b/nixos/modules/profiles/minimal.nix
@@ -0,0 +1,19 @@
+# This module defines a small NixOS configuration.  It does not
+# contain any graphical stuff.
+
+{ config, lib, ... }:
+
+with lib;
+
+{
+  environment.noXlibs = mkDefault true;
+
+  # This isn't perfect, but let's expect the user specifies an UTF-8 defaultLocale
+  i18n.supportedLocales = [ (config.i18n.defaultLocale + "/UTF-8") ];
+
+  documentation.enable = mkDefault false;
+
+  documentation.nixos.enable = mkDefault false;
+
+  programs.command-not-found.enable = mkDefault false;
+}
diff --git a/nixos/modules/profiles/qemu-guest.nix b/nixos/modules/profiles/qemu-guest.nix
new file mode 100644
index 00000000000..d4335edfcf2
--- /dev/null
+++ b/nixos/modules/profiles/qemu-guest.nix
@@ -0,0 +1,17 @@
+# Common configuration for virtual machines running under QEMU (using
+# virtio).
+
+{ ... }:
+
+{
+  boot.initrd.availableKernelModules = [ "virtio_net" "virtio_pci" "virtio_mmio" "virtio_blk" "virtio_scsi" "9p" "9pnet_virtio" ];
+  boot.initrd.kernelModules = [ "virtio_balloon" "virtio_console" "virtio_rng" ];
+
+  boot.initrd.postDeviceCommands =
+    ''
+      # Set the system time from the hardware clock to work around a
+      # bug in qemu-kvm > 1.5.2 (where the VM clock is initialised
+      # to the *boot time* of the host).
+      hwclock -s
+    '';
+}
diff --git a/nixos/modules/programs/adb.nix b/nixos/modules/programs/adb.nix
new file mode 100644
index 00000000000..83bcfe886aa
--- /dev/null
+++ b/nixos/modules/programs/adb.nix
@@ -0,0 +1,30 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  meta.maintainers = [ maintainers.mic92 ];
+
+  ###### interface
+  options = {
+    programs.adb = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to configure system to use Android Debug Bridge (adb).
+          To grant access to a user, it must be part of adbusers group:
+          <code>users.users.alice.extraGroups = ["adbusers"];</code>
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf config.programs.adb.enable {
+    services.udev.packages = [ pkgs.android-udev-rules ];
+    # Give platform-tools lower priority so mke2fs+friends are taken from other packages first
+    environment.systemPackages = [ (lowPrio pkgs.androidenv.androidPkgs_9_0.platform-tools) ];
+    users.groups.adbusers = {};
+  };
+}
diff --git a/nixos/modules/programs/appgate-sdp.nix b/nixos/modules/programs/appgate-sdp.nix
new file mode 100644
index 00000000000..12cb542f4d0
--- /dev/null
+++ b/nixos/modules/programs/appgate-sdp.nix
@@ -0,0 +1,25 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+  options = {
+    programs.appgate-sdp = {
+      enable = mkEnableOption "AppGate SDP VPN client";
+    };
+  };
+
+  config = mkIf config.programs.appgate-sdp.enable {
+    boot.kernelModules = [ "tun" ];
+    environment.systemPackages = [ pkgs.appgate-sdp ];
+    services.dbus.packages = [ pkgs.appgate-sdp ];
+    systemd = {
+      packages = [ pkgs.appgate-sdp ];
+      # https://github.com/NixOS/nixpkgs/issues/81138
+      services.appgatedriver.wantedBy = [ "multi-user.target" ];
+      services.appgate-dumb-resolver.path = [ pkgs.e2fsprogs ];
+      services.appgate-resolver.path = [ pkgs.procps pkgs.e2fsprogs ];
+      services.appgatedriver.path = [ pkgs.e2fsprogs ];
+    };
+  };
+}
diff --git a/nixos/modules/programs/atop.nix b/nixos/modules/programs/atop.nix
new file mode 100644
index 00000000000..ad75ab27666
--- /dev/null
+++ b/nixos/modules/programs/atop.nix
@@ -0,0 +1,155 @@
+# Global configuration for atop.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.programs.atop;
+
+in
+{
+  ###### interface
+
+  options = {
+
+    programs.atop = rec {
+
+      enable = mkEnableOption "Atop";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.atop;
+        defaultText = literalExpression "pkgs.atop";
+        description = ''
+          Which package to use for Atop.
+        '';
+      };
+
+      netatop = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether to install and enable the netatop kernel module.
+            Note: this sets the kernel taint flag "O" for loading out-of-tree modules.
+          '';
+        };
+        package = mkOption {
+          type = types.package;
+          default = config.boot.kernelPackages.netatop;
+          defaultText = literalExpression "config.boot.kernelPackages.netatop";
+          description = ''
+            Which package to use for netatop.
+          '';
+        };
+      };
+
+      atopgpu.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to install and enable the atopgpud daemon to get information about
+          NVIDIA gpus.
+        '';
+      };
+
+      setuidWrapper.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to install a setuid wrapper for Atop. This is required to use some of
+          the features as non-root user (e.g.: ipc information, netatop, atopgpu).
+          Atop tries to drop the root privileges shortly after starting.
+        '';
+      };
+
+      atopService.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable the atop service responsible for storing statistics for
+          long-term analysis.
+        '';
+      };
+      atopRotateTimer.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable the atop-rotate timer, which restarts the atop service
+          daily to make sure the data files are rotate.
+        '';
+      };
+      atopacctService.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable the atopacct service which manages process accounting.
+          This allows Atop to gather data about processes that disappeared in between
+          two refresh intervals.
+        '';
+      };
+      settings = mkOption {
+        type = types.attrs;
+        default = { };
+        example = {
+          flags = "a1f";
+          interval = 5;
+        };
+        description = ''
+          Parameters to be written to <filename>/etc/atoprc</filename>.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable (
+    let
+      atop =
+        if cfg.atopgpu.enable then
+          (cfg.package.override { withAtopgpu = true; })
+        else
+          cfg.package;
+    in
+    {
+      environment.etc = mkIf (cfg.settings != { }) {
+        atoprc.text = concatStrings
+          (mapAttrsToList
+            (n: v: ''
+              ${n} ${toString v}
+            '')
+            cfg.settings);
+      };
+      environment.systemPackages = [ atop (lib.mkIf cfg.netatop.enable cfg.netatop.package) ];
+      boot.extraModulePackages = [ (lib.mkIf cfg.netatop.enable cfg.netatop.package) ];
+      systemd =
+        let
+          mkSystemd = type: cond: name: restartTriggers: {
+            ${name} = lib.mkIf cond {
+              inherit restartTriggers;
+              wantedBy = [ (if type == "services" then "multi-user.target" else if type == "timers" then "timers.target" else null) ];
+            };
+          };
+          mkService = mkSystemd "services";
+          mkTimer = mkSystemd "timers";
+        in
+        {
+          packages = [ atop (lib.mkIf cfg.netatop.enable cfg.netatop.package) ];
+          services =
+            mkService cfg.atopService.enable "atop" [ atop ]
+            // mkService cfg.atopacctService.enable "atopacct" [ atop ]
+            // mkService cfg.netatop.enable "netatop" [ cfg.netatop.package ]
+            // mkService cfg.atopgpu.enable "atopgpu" [ atop ];
+          timers = mkTimer cfg.atopRotateTimer.enable "atop-rotate" [ atop ];
+        };
+
+      security.wrappers = lib.mkIf cfg.setuidWrapper.enable {
+        atop =
+          { setuid = true;
+            owner = "root";
+            group = "root";
+            source = "${atop}/bin/atop";
+          };
+      };
+    }
+  );
+}
diff --git a/nixos/modules/programs/autojump.nix b/nixos/modules/programs/autojump.nix
new file mode 100644
index 00000000000..ecfc2f65807
--- /dev/null
+++ b/nixos/modules/programs/autojump.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.autojump;
+  prg = config.programs;
+in
+{
+  options = {
+    programs.autojump = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable autojump.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.pathsToLink = [ "/share/autojump" ];
+    environment.systemPackages = [ pkgs.autojump ];
+
+    programs.bash.interactiveShellInit = "source ${pkgs.autojump}/share/autojump/autojump.bash";
+    programs.zsh.interactiveShellInit = mkIf prg.zsh.enable "source ${pkgs.autojump}/share/autojump/autojump.zsh";
+    programs.fish.interactiveShellInit = mkIf prg.fish.enable "source ${pkgs.autojump}/share/autojump/autojump.fish";
+  };
+}
diff --git a/nixos/modules/programs/bandwhich.nix b/nixos/modules/programs/bandwhich.nix
new file mode 100644
index 00000000000..610d602ad2c
--- /dev/null
+++ b/nixos/modules/programs/bandwhich.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.programs.bandwhich;
+in {
+  meta.maintainers = with maintainers; [ Br1ght0ne ];
+
+  options = {
+    programs.bandwhich = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to add bandwhich to the global environment and configure a
+          setcap wrapper for it.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ bandwhich ];
+    security.wrappers.bandwhich = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_net_raw,cap_net_admin+ep";
+      source = "${pkgs.bandwhich}/bin/bandwhich";
+    };
+  };
+}
diff --git a/nixos/modules/programs/bash-my-aws.nix b/nixos/modules/programs/bash-my-aws.nix
new file mode 100644
index 00000000000..15e429a7549
--- /dev/null
+++ b/nixos/modules/programs/bash-my-aws.nix
@@ -0,0 +1,25 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  prg = config.programs;
+  cfg = prg.bash-my-aws;
+
+  initScript = ''
+    eval $(${pkgs.bash-my-aws}/bin/bma-init)
+  '';
+in
+  {
+    options = {
+      programs.bash-my-aws = {
+        enable = mkEnableOption "bash-my-aws";
+      };
+    };
+
+    config = mkIf cfg.enable {
+      environment.systemPackages = with pkgs; [ bash-my-aws ];
+
+      programs.bash.interactiveShellInit = initScript;
+    };
+  }
diff --git a/nixos/modules/programs/bash/bash-completion.nix b/nixos/modules/programs/bash/bash-completion.nix
new file mode 100644
index 00000000000..b8e5b1bfa33
--- /dev/null
+++ b/nixos/modules/programs/bash/bash-completion.nix
@@ -0,0 +1,37 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  enable = config.programs.bash.enableCompletion;
+in
+{
+  options = {
+    programs.bash.enableCompletion = mkEnableOption "Bash completion for all interactive bash shells" // {
+      default = true;
+    };
+  };
+
+  config = mkIf enable {
+    programs.bash.promptPluginInit = ''
+      # Check whether we're running a version of Bash that has support for
+      # programmable completion. If we do, enable all modules installed in
+      # the system and user profile in obsolete /etc/bash_completion.d/
+      # directories. Bash loads completions in all
+      # $XDG_DATA_DIRS/bash-completion/completions/
+      # on demand, so they do not need to be sourced here.
+      if shopt -q progcomp &>/dev/null; then
+        . "${pkgs.bash-completion}/etc/profile.d/bash_completion.sh"
+        nullglobStatus=$(shopt -p nullglob)
+        shopt -s nullglob
+        for p in $NIX_PROFILES; do
+          for m in "$p/etc/bash_completion.d/"*; do
+            . "$m"
+          done
+        done
+        eval "$nullglobStatus"
+        unset nullglobStatus p m
+      fi
+    '';
+  };
+}
diff --git a/nixos/modules/programs/bash/bash.nix b/nixos/modules/programs/bash/bash.nix
new file mode 100644
index 00000000000..7281126979e
--- /dev/null
+++ b/nixos/modules/programs/bash/bash.nix
@@ -0,0 +1,217 @@
+# This module defines global configuration for the Bash shell, in
+# particular /etc/bashrc and /etc/profile.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfge = config.environment;
+
+  cfg = config.programs.bash;
+
+  bashAliases = concatStringsSep "\n" (
+    mapAttrsFlatten (k: v: "alias ${k}=${escapeShellArg v}")
+      (filterAttrs (k: v: v != null) cfg.shellAliases)
+  );
+
+in
+
+{
+  imports = [
+    (mkRemovedOptionModule [ "programs" "bash" "enable" ] "")
+  ];
+
+  options = {
+
+    programs.bash = {
+
+      /*
+      enable = mkOption {
+        default = true;
+        description = ''
+          Whenever to configure Bash as an interactive shell.
+          Note that this tries to make Bash the default
+          <option>users.defaultUserShell</option>,
+          which in turn means that you might need to explicitly
+          set this variable if you have another shell configured
+          with NixOS.
+        '';
+        type = types.bool;
+      };
+      */
+
+      shellAliases = mkOption {
+        default = {};
+        description = ''
+          Set of aliases for bash shell, which overrides <option>environment.shellAliases</option>.
+          See <option>environment.shellAliases</option> for an option format description.
+        '';
+        type = with types; attrsOf (nullOr (either str path));
+      };
+
+      shellInit = mkOption {
+        default = "";
+        description = ''
+          Shell script code called during bash shell initialisation.
+        '';
+        type = types.lines;
+      };
+
+      loginShellInit = mkOption {
+        default = "";
+        description = ''
+          Shell script code called during login bash shell initialisation.
+        '';
+        type = types.lines;
+      };
+
+      interactiveShellInit = mkOption {
+        default = "";
+        description = ''
+          Shell script code called during interactive bash shell initialisation.
+        '';
+        type = types.lines;
+      };
+
+      promptInit = mkOption {
+        default = ''
+          # Provide a nice prompt if the terminal supports it.
+          if [ "$TERM" != "dumb" ] || [ -n "$INSIDE_EMACS" ]; then
+            PROMPT_COLOR="1;31m"
+            ((UID)) && PROMPT_COLOR="1;32m"
+            if [ -n "$INSIDE_EMACS" ] || [ "$TERM" = "eterm" ] || [ "$TERM" = "eterm-color" ]; then
+              # Emacs term mode doesn't support xterm title escape sequence (\e]0;)
+              PS1="\n\[\033[$PROMPT_COLOR\][\u@\h:\w]\\$\[\033[0m\] "
+            else
+              PS1="\n\[\033[$PROMPT_COLOR\][\[\e]0;\u@\h: \w\a\]\u@\h:\w]\\$\[\033[0m\] "
+            fi
+            if test "$TERM" = "xterm"; then
+              PS1="\[\033]2;\h:\u:\w\007\]$PS1"
+            fi
+          fi
+        '';
+        description = ''
+          Shell script code used to initialise the bash prompt.
+        '';
+        type = types.lines;
+      };
+
+      promptPluginInit = mkOption {
+        default = "";
+        description = ''
+          Shell script code used to initialise bash prompt plugins.
+        '';
+        type = types.lines;
+        internal = true;
+      };
+
+    };
+
+  };
+
+  config = /* mkIf cfg.enable */ {
+
+    programs.bash = {
+
+      shellAliases = mapAttrs (name: mkDefault) cfge.shellAliases;
+
+      shellInit = ''
+        if [ -z "$__NIXOS_SET_ENVIRONMENT_DONE" ]; then
+            . ${config.system.build.setEnvironment}
+        fi
+
+        ${cfge.shellInit}
+      '';
+
+      loginShellInit = cfge.loginShellInit;
+
+      interactiveShellInit = ''
+        # Check the window size after every command.
+        shopt -s checkwinsize
+
+        # Disable hashing (i.e. caching) of command lookups.
+        set +h
+
+        ${cfg.promptInit}
+        ${cfg.promptPluginInit}
+        ${bashAliases}
+
+        ${cfge.interactiveShellInit}
+      '';
+
+    };
+
+    environment.etc.profile.text =
+      ''
+        # /etc/profile: DO NOT EDIT -- this file has been generated automatically.
+        # This file is read for login shells.
+
+        # Only execute this file once per shell.
+        if [ -n "$__ETC_PROFILE_SOURCED" ]; then return; fi
+        __ETC_PROFILE_SOURCED=1
+
+        # Prevent this file from being sourced by interactive non-login child shells.
+        export __ETC_PROFILE_DONE=1
+
+        ${cfg.shellInit}
+        ${cfg.loginShellInit}
+
+        # Read system-wide modifications.
+        if test -f /etc/profile.local; then
+            . /etc/profile.local
+        fi
+
+        if [ -n "''${BASH_VERSION:-}" ]; then
+            . /etc/bashrc
+        fi
+      '';
+
+    environment.etc.bashrc.text =
+      ''
+        # /etc/bashrc: DO NOT EDIT -- this file has been generated automatically.
+
+        # Only execute this file once per shell.
+        if [ -n "$__ETC_BASHRC_SOURCED" ] || [ -n "$NOSYSBASHRC" ]; then return; fi
+        __ETC_BASHRC_SOURCED=1
+
+        # If the profile was not loaded in a parent process, source
+        # it.  But otherwise don't do it because we don't want to
+        # clobber overridden values of $PATH, etc.
+        if [ -z "$__ETC_PROFILE_DONE" ]; then
+            . /etc/profile
+        fi
+
+        # We are not always an interactive shell.
+        if [ -n "$PS1" ]; then
+            ${cfg.interactiveShellInit}
+        fi
+
+        # Read system-wide modifications.
+        if test -f /etc/bashrc.local; then
+            . /etc/bashrc.local
+        fi
+      '';
+
+    # Configuration for readline in bash. We use "option default"
+    # priority to allow user override using both .text and .source.
+    environment.etc.inputrc.source = mkOptionDefault ./inputrc;
+
+    users.defaultUserShell = mkDefault pkgs.bashInteractive;
+
+    environment.pathsToLink = optionals cfg.enableCompletion [
+      "/etc/bash_completion.d"
+      "/share/bash-completion"
+    ];
+
+    environment.shells =
+      [ "/run/current-system/sw/bin/bash"
+        "/run/current-system/sw/bin/sh"
+        "${pkgs.bashInteractive}/bin/bash"
+        "${pkgs.bashInteractive}/bin/sh"
+      ];
+
+  };
+
+}
diff --git a/nixos/modules/programs/bash/inputrc b/nixos/modules/programs/bash/inputrc
new file mode 100644
index 00000000000..f339eb649ed
--- /dev/null
+++ b/nixos/modules/programs/bash/inputrc
@@ -0,0 +1,37 @@
+# inputrc borrowed from CentOS (RHEL).
+
+set bell-style none
+
+set meta-flag on
+set input-meta on
+set convert-meta off
+set output-meta on
+set colored-stats on
+
+#set mark-symlinked-directories on
+
+$if mode=emacs
+
+# for linux console and RH/Debian xterm
+"\e[1~": beginning-of-line
+"\e[4~": end-of-line
+"\e[5~": beginning-of-history
+"\e[6~": end-of-history
+"\e[3~": delete-char
+"\e[2~": quoted-insert
+"\e[5C": forward-word
+"\e[5D": backward-word
+"\e[1;5C": forward-word
+"\e[1;5D": backward-word
+
+# for rxvt
+"\e[8~": end-of-line
+
+# for non RH/Debian xterm, can't hurt for RH/DEbian xterm
+"\eOH": beginning-of-line
+"\eOF": end-of-line
+
+# for freebsd console
+"\e[H": beginning-of-line
+"\e[F": end-of-line
+$endif
diff --git a/nixos/modules/programs/bash/ls-colors.nix b/nixos/modules/programs/bash/ls-colors.nix
new file mode 100644
index 00000000000..254ee14c477
--- /dev/null
+++ b/nixos/modules/programs/bash/ls-colors.nix
@@ -0,0 +1,20 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  enable = config.programs.bash.enableLsColors;
+in
+{
+  options = {
+    programs.bash.enableLsColors = mkEnableOption "extra colors in directory listings" // {
+      default = true;
+    };
+  };
+
+  config = mkIf enable {
+    programs.bash.promptPluginInit = ''
+      eval "$(${pkgs.coreutils}/bin/dircolors -b)"
+    '';
+  };
+}
diff --git a/nixos/modules/programs/bash/undistract-me.nix b/nixos/modules/programs/bash/undistract-me.nix
new file mode 100644
index 00000000000..0e6465e048a
--- /dev/null
+++ b/nixos/modules/programs/bash/undistract-me.nix
@@ -0,0 +1,36 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.bash.undistractMe;
+in
+{
+  options = {
+    programs.bash.undistractMe = {
+      enable = mkEnableOption "notifications when long-running terminal commands complete";
+
+      playSound = mkEnableOption "notification sounds when long-running terminal commands complete";
+
+      timeout = mkOption {
+        default = 10;
+        description = ''
+          Number of seconds it would take for a command to be considered long-running.
+        '';
+        type = types.int;
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    programs.bash.promptPluginInit = ''
+      export LONG_RUNNING_COMMAND_TIMEOUT=${toString cfg.timeout}
+      export UDM_PLAY_SOUND=${if cfg.playSound then "1" else "0"}
+      . "${pkgs.undistract-me}/etc/profile.d/undistract-me.sh"
+    '';
+  };
+
+  meta = {
+    maintainers = with maintainers; [ kira-bruneau ];
+  };
+}
diff --git a/nixos/modules/programs/bcc.nix b/nixos/modules/programs/bcc.nix
new file mode 100644
index 00000000000..e475c6ceaa6
--- /dev/null
+++ b/nixos/modules/programs/bcc.nix
@@ -0,0 +1,9 @@
+{ config, pkgs, lib, ... }:
+{
+  options.programs.bcc.enable = lib.mkEnableOption "bcc";
+
+  config = lib.mkIf config.programs.bcc.enable {
+    environment.systemPackages = [ pkgs.bcc ];
+    boot.extraModulePackages = [ pkgs.bcc ];
+  };
+}
diff --git a/nixos/modules/programs/browserpass.nix b/nixos/modules/programs/browserpass.nix
new file mode 100644
index 00000000000..e1456d3c184
--- /dev/null
+++ b/nixos/modules/programs/browserpass.nix
@@ -0,0 +1,32 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  options.programs.browserpass.enable = mkEnableOption "Browserpass native messaging host";
+
+  config = mkIf config.programs.browserpass.enable {
+    environment.etc = let
+      appId = "com.github.browserpass.native.json";
+      source = part: "${pkgs.browserpass}/lib/browserpass/${part}/${appId}";
+    in {
+      # chromium
+      "chromium/native-messaging-hosts/${appId}".source = source "hosts/chromium";
+      "chromium/policies/managed/${appId}".source = source "policies/chromium";
+
+      # chrome
+      "opt/chrome/native-messaging-hosts/${appId}".source = source "hosts/chromium";
+      "opt/chrome/policies/managed/${appId}".source = source "policies/chromium";
+
+      # vivaldi
+      "opt/vivaldi/native-messaging-hosts/${appId}".source = source "hosts/chromium";
+      "opt/vivaldi/policies/managed/${appId}".source = source "policies/chromium";
+
+      # brave
+      "opt/brave/native-messaging-hosts/${appId}".source = source "hosts/chromium";
+      "opt/brave/policies/managed/${appId}".source = source "policies/chromium";
+    };
+    nixpkgs.config.firefox.enableBrowserpass = true;
+  };
+}
diff --git a/nixos/modules/programs/calls.nix b/nixos/modules/programs/calls.nix
new file mode 100644
index 00000000000..08a223b408d
--- /dev/null
+++ b/nixos/modules/programs/calls.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.calls;
+in {
+  options = {
+    programs.calls = {
+      enable = mkEnableOption ''
+        Whether to enable GNOME calls: a phone dialer and call handler.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    programs.dconf.enable = true;
+
+    environment.systemPackages = [
+      pkgs.calls
+    ];
+
+    services.dbus.packages = [
+      pkgs.callaudiod
+    ];
+  };
+}
diff --git a/nixos/modules/programs/captive-browser.nix b/nixos/modules/programs/captive-browser.nix
new file mode 100644
index 00000000000..aad554c2bd6
--- /dev/null
+++ b/nixos/modules/programs/captive-browser.nix
@@ -0,0 +1,152 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.programs.captive-browser;
+
+  inherit (lib)
+    concatStringsSep escapeShellArgs optionalString
+    literalExpression mkEnableOption mkIf mkOption mkOptionDefault types;
+
+  browserDefault = chromium: concatStringsSep " " [
+    ''env XDG_CONFIG_HOME="$PREV_CONFIG_HOME"''
+    ''${chromium}/bin/chromium''
+    ''--user-data-dir=''${XDG_DATA_HOME:-$HOME/.local/share}/chromium-captive''
+    ''--proxy-server="socks5://$PROXY"''
+    ''--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE localhost"''
+    ''--no-first-run''
+    ''--new-window''
+    ''--incognito''
+    ''-no-default-browser-check''
+    ''http://cache.nixos.org/''
+  ];
+
+  desktopItem = pkgs.makeDesktopItem {
+    name = "captive-browser";
+    desktopName = "Captive Portal Browser";
+    exec = "/run/wrappers/bin/captive-browser";
+    icon = "nix-snowflake";
+    categories = [ "Network" ];
+  };
+
+in
+{
+  ###### interface
+
+  options = {
+    programs.captive-browser = {
+      enable = mkEnableOption "captive browser";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.captive-browser;
+        defaultText = literalExpression "pkgs.captive-browser";
+        description = "Which package to use for captive-browser";
+      };
+
+      interface = mkOption {
+        type = types.str;
+        description = "your public network interface (wlp3s0, wlan0, eth0, ...)";
+      };
+
+      # the options below are the same as in "captive-browser.toml"
+      browser = mkOption {
+        type = types.str;
+        default = browserDefault pkgs.chromium;
+        defaultText = literalExpression (browserDefault "\${pkgs.chromium}");
+        description = ''
+          The shell (/bin/sh) command executed once the proxy starts.
+          When browser exits, the proxy exits. An extra env var PROXY is available.
+
+          Here, we use a separate Chrome instance in Incognito mode, so that
+          it can run (and be waited for) alongside the default one, and that
+          it maintains no state across runs. To configure this browser open a
+          normal window in it, settings will be preserved.
+
+          @volth: chromium is to open a plain HTTP (not HTTPS nor redirect to HTTPS!) website.
+                  upstream uses http://example.com but I have seen captive portals whose DNS server resolves "example.com" to 127.0.0.1
+        '';
+      };
+
+      dhcp-dns = mkOption {
+        type = types.str;
+        description = ''
+          The shell (/bin/sh) command executed to obtain the DHCP
+          DNS server address. The first match of an IPv4 regex is used.
+          IPv4 only, because let's be real, it's a captive portal.
+        '';
+      };
+
+      socks5-addr = mkOption {
+        type = types.str;
+        default = "localhost:1666";
+        description = "the listen address for the SOCKS5 proxy server";
+      };
+
+      bindInterface = mkOption {
+        default = true;
+        type = types.bool;
+        description = ''
+          Binds <package>captive-browser</package> to the network interface declared in
+          <literal>cfg.interface</literal>. This can be used to avoid collisions
+          with private subnets.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [
+      (pkgs.runCommandNoCC "captive-browser-desktop-item" { } ''
+        install -Dm444 -t $out/share/applications ${desktopItem}/share/applications/*.desktop
+      '')
+    ];
+
+    programs.captive-browser.dhcp-dns =
+      let
+        iface = prefixes:
+          optionalString cfg.bindInterface (escapeShellArgs (prefixes ++ [ cfg.interface ]));
+      in
+      mkOptionDefault (
+        if config.networking.networkmanager.enable then
+          "${pkgs.networkmanager}/bin/nmcli dev show ${iface []} | ${pkgs.gnugrep}/bin/fgrep IP4.DNS"
+        else if config.networking.dhcpcd.enable then
+          "${pkgs.dhcpcd}/bin/dhcpcd ${iface ["-U"]} | ${pkgs.gnugrep}/bin/fgrep domain_name_servers"
+        else if config.networking.useNetworkd then
+          "${cfg.package}/bin/systemd-networkd-dns ${iface []}"
+        else
+          "${config.security.wrapperDir}/udhcpc --quit --now -f ${iface ["-i"]} -O dns --script ${
+          pkgs.writeShellScript "udhcp-script" ''
+            if [ "$1" = bound ]; then
+              echo "$dns"
+            fi
+          ''}"
+      );
+
+    security.wrappers.udhcpc = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_net_raw+p";
+      source = "${pkgs.busybox}/bin/udhcpc";
+    };
+
+    security.wrappers.captive-browser = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_net_raw+p";
+      source = pkgs.writeShellScript "captive-browser" ''
+        export PREV_CONFIG_HOME="$XDG_CONFIG_HOME"
+        export XDG_CONFIG_HOME=${pkgs.writeTextDir "captive-browser.toml" ''
+                                  browser = """${cfg.browser}"""
+                                  dhcp-dns = """${cfg.dhcp-dns}"""
+                                  socks5-addr = """${cfg.socks5-addr}"""
+                                  ${optionalString cfg.bindInterface ''
+                                    bind-device = """${cfg.interface}"""
+                                  ''}
+                                ''}
+        exec ${cfg.package}/bin/captive-browser
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/programs/ccache.nix b/nixos/modules/programs/ccache.nix
new file mode 100644
index 00000000000..0f7fd0a3683
--- /dev/null
+++ b/nixos/modules/programs/ccache.nix
@@ -0,0 +1,85 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.programs.ccache;
+in {
+  options.programs.ccache = {
+    # host configuration
+    enable = mkEnableOption "CCache";
+    cacheDir = mkOption {
+      type = types.path;
+      description = "CCache directory";
+      default = "/var/cache/ccache";
+    };
+    # target configuration
+    packageNames = mkOption {
+      type = types.listOf types.str;
+      description = "Nix top-level packages to be compiled using CCache";
+      default = [];
+      example = [ "wxGTK30" "ffmpeg" "libav_all" ];
+    };
+  };
+
+  config = mkMerge [
+    # host configuration
+    (mkIf cfg.enable {
+      systemd.tmpfiles.rules = [ "d ${cfg.cacheDir} 0770 root nixbld -" ];
+
+      # "nix-ccache --show-stats" and "nix-ccache --clear"
+      security.wrappers.nix-ccache = {
+        owner = "root";
+        group = "nixbld";
+        setuid = false;
+        setgid = true;
+        source = pkgs.writeScript "nix-ccache.pl" ''
+          #!${pkgs.perl}/bin/perl
+
+          %ENV=( CCACHE_DIR => '${cfg.cacheDir}' );
+          sub untaint {
+            my $v = shift;
+            return '-C' if $v eq '-C' || $v eq '--clear';
+            return '-V' if $v eq '-V' || $v eq '--version';
+            return '-s' if $v eq '-s' || $v eq '--show-stats';
+            return '-z' if $v eq '-z' || $v eq '--zero-stats';
+            exec('${pkgs.ccache}/bin/ccache', '-h');
+          }
+          exec('${pkgs.ccache}/bin/ccache', map { untaint $_ } @ARGV);
+        '';
+      };
+    })
+
+    # target configuration
+    (mkIf (cfg.packageNames != []) {
+      nixpkgs.overlays = [
+        (self: super: genAttrs cfg.packageNames (pn: super.${pn}.override { stdenv = builtins.trace "with ccache: ${pn}" self.ccacheStdenv; }))
+
+        (self: super: {
+          ccacheWrapper = super.ccacheWrapper.override {
+            extraConfig = ''
+              export CCACHE_COMPRESS=1
+              export CCACHE_DIR="${cfg.cacheDir}"
+              export CCACHE_UMASK=007
+              if [ ! -d "$CCACHE_DIR" ]; then
+                echo "====="
+                echo "Directory '$CCACHE_DIR' does not exist"
+                echo "Please create it with:"
+                echo "  sudo mkdir -m0770 '$CCACHE_DIR'"
+                echo "  sudo chown root:nixbld '$CCACHE_DIR'"
+                echo "====="
+                exit 1
+              fi
+              if [ ! -w "$CCACHE_DIR" ]; then
+                echo "====="
+                echo "Directory '$CCACHE_DIR' is not accessible for user $(whoami)"
+                echo "Please verify its access permissions"
+                echo "====="
+                exit 1
+              fi
+            '';
+          };
+        })
+      ];
+    })
+  ];
+}
diff --git a/nixos/modules/programs/cdemu.nix b/nixos/modules/programs/cdemu.nix
new file mode 100644
index 00000000000..142e2934240
--- /dev/null
+++ b/nixos/modules/programs/cdemu.nix
@@ -0,0 +1,62 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.programs.cdemu;
+in {
+
+  options = {
+    programs.cdemu = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          <command>cdemu</command> for members of
+          <option>programs.cdemu.group</option>.
+        '';
+      };
+      group = mkOption {
+        type = types.str;
+        default = "cdrom";
+        description = ''
+          Group that users must be in to use <command>cdemu</command>.
+        '';
+      };
+      gui = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to install the <command>cdemu</command> GUI (gCDEmu).
+        '';
+      };
+      image-analyzer = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to install the image analyzer.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    boot = {
+      extraModulePackages = [ config.boot.kernelPackages.vhba ];
+      kernelModules = [ "vhba" ];
+    };
+
+    services = {
+      udev.extraRules = ''
+        KERNEL=="vhba_ctl", MODE="0660", OWNER="root", GROUP="${cfg.group}"
+      '';
+      dbus.packages = [ pkgs.cdemu-daemon ];
+    };
+
+    environment.systemPackages =
+      [ pkgs.cdemu-daemon pkgs.cdemu-client ]
+      ++ optional cfg.gui pkgs.gcdemu
+      ++ optional cfg.image-analyzer pkgs.image-analyzer;
+  };
+
+}
diff --git a/nixos/modules/programs/chromium.nix b/nixos/modules/programs/chromium.nix
new file mode 100644
index 00000000000..8a1653318ab
--- /dev/null
+++ b/nixos/modules/programs/chromium.nix
@@ -0,0 +1,112 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.chromium;
+
+  defaultProfile = filterAttrs (k: v: v != null) {
+    HomepageLocation = cfg.homepageLocation;
+    DefaultSearchProviderEnabled = cfg.defaultSearchProviderEnabled;
+    DefaultSearchProviderSearchURL = cfg.defaultSearchProviderSearchURL;
+    DefaultSearchProviderSuggestURL = cfg.defaultSearchProviderSuggestURL;
+    ExtensionInstallForcelist = cfg.extensions;
+  };
+in
+
+{
+  ###### interface
+
+  options = {
+    programs.chromium = {
+      enable = mkEnableOption "<command>chromium</command> policies";
+
+      extensions = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          List of chromium extensions to install.
+          For list of plugins ids see id in url of extensions on
+          <link xlink:href="https://chrome.google.com/webstore/category/extensions">chrome web store</link>
+          page. To install a chromium extension not included in the chrome web
+          store, append to the extension id a semicolon ";" followed by a URL
+          pointing to an Update Manifest XML file. See
+          <link xlink:href="https://cloud.google.com/docs/chrome-enterprise/policies/?policy=ExtensionInstallForcelist">ExtensionInstallForcelist</link>
+          for additional details.
+        '';
+        default = [];
+        example = literalExpression ''
+          [
+            "chlffgpmiacpedhhbkiomidkjlcfhogd" # pushbullet
+            "mbniclmhobmnbdlbpiphghaielnnpgdp" # lightshot
+            "gcbommkclmclpchllfjekcdonpmejbdp" # https everywhere
+            "cjpalhdlnbpafiamejdnhcphjbkeiagm" # ublock origin
+          ]
+        '';
+      };
+
+      homepageLocation = mkOption {
+        type = types.nullOr types.str;
+        description = "Chromium default homepage";
+        default = null;
+        example = "https://nixos.org";
+      };
+
+      defaultSearchProviderEnabled = mkOption {
+        type = types.nullOr types.bool;
+        description = "Enable the default search provider.";
+        default = null;
+        example = true;
+      };
+
+      defaultSearchProviderSearchURL = mkOption {
+        type = types.nullOr types.str;
+        description = "Chromium default search provider url.";
+        default = null;
+        example =
+          "https://encrypted.google.com/search?q={searchTerms}&{google:RLZ}{google:originalQueryForSuggestion}{google:assistedQueryStats}{google:searchFieldtrialParameter}{google:searchClient}{google:sourceId}{google:instantExtendedEnabledParameter}ie={inputEncoding}";
+      };
+
+      defaultSearchProviderSuggestURL = mkOption {
+        type = types.nullOr types.str;
+        description = "Chromium default search provider url for suggestions.";
+        default = null;
+        example =
+          "https://encrypted.google.com/complete/search?output=chrome&q={searchTerms}";
+      };
+
+      extraOpts = mkOption {
+        type = types.attrs;
+        description = ''
+          Extra chromium policy options. A list of available policies
+          can be found in the Chrome Enterprise documentation:
+          <link xlink:href="https://cloud.google.com/docs/chrome-enterprise/policies/">https://cloud.google.com/docs/chrome-enterprise/policies/</link>
+          Make sure the selected policy is supported on Linux and your browser version.
+        '';
+        default = {};
+        example = literalExpression ''
+          {
+            "BrowserSignin" = 0;
+            "SyncDisabled" = true;
+            "PasswordManagerEnabled" = false;
+            "SpellcheckEnabled" = true;
+            "SpellcheckLanguage" = [
+                                     "de"
+                                     "en-US"
+                                   ];
+          }
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = lib.mkIf cfg.enable {
+    # for chromium
+    environment.etc."chromium/policies/managed/default.json".text = builtins.toJSON defaultProfile;
+    environment.etc."chromium/policies/managed/extra.json".text = builtins.toJSON cfg.extraOpts;
+    # for google-chrome https://www.chromium.org/administrators/linux-quick-start
+    environment.etc."opt/chrome/policies/managed/default.json".text = builtins.toJSON defaultProfile;
+    environment.etc."opt/chrome/policies/managed/extra.json".text = builtins.toJSON cfg.extraOpts;
+  };
+}
diff --git a/nixos/modules/programs/clickshare.nix b/nixos/modules/programs/clickshare.nix
new file mode 100644
index 00000000000..9980a7daf52
--- /dev/null
+++ b/nixos/modules/programs/clickshare.nix
@@ -0,0 +1,21 @@
+{ config, lib, pkgs, ... }:
+
+{
+
+  options.programs.clickshare-csc1.enable =
+    lib.options.mkEnableOption ''
+      Barco ClickShare CSC-1 driver/client.
+      This allows users in the <literal>clickshare</literal>
+      group to access and use a ClickShare USB dongle
+      that is connected to the machine
+    '';
+
+  config = lib.modules.mkIf config.programs.clickshare-csc1.enable {
+    environment.systemPackages = [ pkgs.clickshare-csc1 ];
+    services.udev.packages = [ pkgs.clickshare-csc1 ];
+    users.groups.clickshare = {};
+  };
+
+  meta.maintainers = [ lib.maintainers.yarny ];
+
+}
diff --git a/nixos/modules/programs/cnping.nix b/nixos/modules/programs/cnping.nix
new file mode 100644
index 00000000000..d208d2b0704
--- /dev/null
+++ b/nixos/modules/programs/cnping.nix
@@ -0,0 +1,21 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.cnping;
+in
+{
+  options = {
+    programs.cnping = {
+      enable = mkEnableOption "Whether to install a setcap wrapper for cnping";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    security.wrappers.cnping = {
+      source = "${pkgs.cnping}/bin/cnping";
+      capabilities = "cap_net_raw+ep";
+    };
+  };
+}
diff --git a/nixos/modules/programs/command-not-found/command-not-found.nix b/nixos/modules/programs/command-not-found/command-not-found.nix
new file mode 100644
index 00000000000..4d2a89b5158
--- /dev/null
+++ b/nixos/modules/programs/command-not-found/command-not-found.nix
@@ -0,0 +1,95 @@
+# This module provides suggestions of packages to install if the user
+# tries to run a missing command in Bash.  This is implemented using a
+# SQLite database that maps program names to Nix package names (e.g.,
+# "pdflatex" is mapped to "tetex").
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.command-not-found;
+  commandNotFound = pkgs.substituteAll {
+    name = "command-not-found";
+    dir = "bin";
+    src = ./command-not-found.pl;
+    isExecutable = true;
+    inherit (cfg) dbPath;
+    perl = pkgs.perl.withPackages (p: [ p.DBDSQLite p.StringShellQuote ]);
+  };
+
+in
+
+{
+  options.programs.command-not-found = {
+
+    enable = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether interactive shells should show which Nix package (if
+        any) provides a missing command.
+      '';
+    };
+
+    dbPath = mkOption {
+      default = "/nix/var/nix/profiles/per-user/root/channels/nixos/programs.sqlite" ;
+      description = ''
+        Absolute path to programs.sqlite.
+
+        By default this file will be provided by your channel
+        (nixexprs.tar.xz).
+      '';
+      type = types.path;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    programs.bash.interactiveShellInit =
+      ''
+        # This function is called whenever a command is not found.
+        command_not_found_handle() {
+          local p='${commandNotFound}/bin/command-not-found'
+          if [ -x "$p" ] && [ -f '${cfg.dbPath}' ]; then
+            # Run the helper program.
+            "$p" "$@"
+            # Retry the command if we just installed it.
+            if [ $? = 126 ]; then
+              "$@"
+            else
+              return 127
+            fi
+          else
+            echo "$1: command not found" >&2
+            return 127
+          fi
+        }
+      '';
+
+    programs.zsh.interactiveShellInit =
+      ''
+        # This function is called whenever a command is not found.
+        command_not_found_handler() {
+          local p='${commandNotFound}/bin/command-not-found'
+          if [ -x "$p" ] && [ -f '${cfg.dbPath}' ]; then
+            # Run the helper program.
+            "$p" "$@"
+
+            # Retry the command if we just installed it.
+            if [ $? = 126 ]; then
+              "$@"
+            else
+              return 127
+            fi
+          else
+            # Indicate than there was an error so ZSH falls back to its default handler
+            echo "$1: command not found" >&2
+            return 127
+          fi
+        }
+      '';
+
+    environment.systemPackages = [ commandNotFound ];
+  };
+
+}
diff --git a/nixos/modules/programs/command-not-found/command-not-found.pl b/nixos/modules/programs/command-not-found/command-not-found.pl
new file mode 100644
index 00000000000..72e246c81ae
--- /dev/null
+++ b/nixos/modules/programs/command-not-found/command-not-found.pl
@@ -0,0 +1,77 @@
+#! @perl@/bin/perl -w
+
+use strict;
+use DBI;
+use DBD::SQLite;
+use String::ShellQuote;
+use Config;
+
+my $program = $ARGV[0];
+
+my $dbPath = "@dbPath@";
+
+my $dbh = DBI->connect("dbi:SQLite:dbname=$dbPath", "", "")
+    or die "cannot open database `$dbPath'";
+$dbh->{RaiseError} = 0;
+$dbh->{PrintError} = 0;
+
+my $system = $ENV{"NIX_SYSTEM"} // $Config{myarchname};
+
+my $res = $dbh->selectall_arrayref(
+    "select package from Programs where system = ? and name = ?",
+    { Slice => {} }, $system, $program);
+
+my $len = !defined $res ? 0 : scalar @$res;
+
+if ($len == 0) {
+    print STDERR "$program: command not found\n";
+} elsif ($len == 1) {
+    my $package = @$res[0]->{package};
+    if ($ENV{"NIX_AUTO_RUN"} // "") {
+        if ($ENV{"NIX_AUTO_RUN_INTERACTIVE"} // "") {
+            while (1) {
+                print STDERR "'$program' from package '$package' will be run, confirm? [yn]: ";
+                chomp(my $comfirm = <STDIN>);
+                if (lc $comfirm eq "n") {
+                    exit 0;
+                } elsif (lc $comfirm eq "y") {
+                    last;
+                }
+            }
+        }
+        exec("nix-shell", "-p", $package, "--run", shell_quote("exec", @ARGV));
+    } else {
+        print STDERR <<EOF;
+The program '$program' is not in your PATH. You can make it available in an
+ephemeral shell by typing:
+  nix-shell -p $package
+EOF
+    }
+} else {
+    if ($ENV{"NIX_AUTO_RUN"} // "") {
+        print STDERR "Select a package that provides '$program':\n";
+        for my $i (0 .. $len - 1) {
+            print STDERR "  [", $i + 1, "]: @$res[$i]->{package}\n";
+        }
+        my $choice = 0;
+        while (1) { # exec will break this loop
+            no warnings "numeric";
+            print STDERR "Your choice [1-${len}]: ";
+            # 0 can be invalid user input like non-number string
+            # so we start from 1
+            $choice = <STDIN> + 0;
+            if (1 <= $choice && $choice <= $len) {
+                exec("nix-shell", "-p", @$res[$choice - 1]->{package},
+                    "--run", shell_quote("exec", @ARGV));
+            }
+        }
+    } else {
+        print STDERR <<EOF;
+The program '$program' is not in your PATH. It is provided by several packages.
+You can make it available in an ephemeral shell by typing one of the following:
+EOF
+        print STDERR "  nix-shell -p $_->{package}\n" foreach @$res;
+    }
+}
+
+exit 127;
diff --git a/nixos/modules/programs/criu.nix b/nixos/modules/programs/criu.nix
new file mode 100644
index 00000000000..1714e1331a4
--- /dev/null
+++ b/nixos/modules/programs/criu.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.programs.criu;
+in {
+
+  options = {
+    programs.criu = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Install <command>criu</command> along with necessary kernel options.
+        '';
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    system.requiredKernelConfig = with config.lib.kernelConfig; [
+      (isYes "CHECKPOINT_RESTORE")
+    ];
+    boot.kernel.features.criu = true;
+    environment.systemPackages = [ pkgs.criu ];
+  };
+
+}
diff --git a/nixos/modules/programs/dconf.nix b/nixos/modules/programs/dconf.nix
new file mode 100644
index 00000000000..265c41cbbbc
--- /dev/null
+++ b/nixos/modules/programs/dconf.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.dconf;
+  cfgDir = pkgs.symlinkJoin {
+    name = "dconf-system-config";
+    paths = map (x: "${x}/etc/dconf") cfg.packages;
+    postBuild = ''
+      mkdir -p $out/profile
+      mkdir -p $out/db
+    '' + (
+      concatStringsSep "\n" (
+        mapAttrsToList (
+          name: path: ''
+            ln -s ${path} $out/profile/${name}
+          ''
+        ) cfg.profiles
+      )
+    ) + ''
+      ${pkgs.dconf}/bin/dconf update $out/db
+    '';
+  };
+in
+{
+  ###### interface
+
+  options = {
+    programs.dconf = {
+      enable = mkEnableOption "dconf";
+
+      profiles = mkOption {
+        type = types.attrsOf types.path;
+        default = {};
+        description = "Set of dconf profile files, installed at <filename>/etc/dconf/profiles/<replaceable>name</replaceable></filename>.";
+        internal = true;
+      };
+
+      packages = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        description = "A list of packages which provide dconf profiles and databases in <filename>/etc/dconf</filename>.";
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf (cfg.profiles != {} || cfg.enable) {
+    environment.etc.dconf = mkIf (cfg.profiles != {} || cfg.packages != []) {
+      source = cfgDir;
+    };
+
+    services.dbus.packages = [ pkgs.dconf ];
+
+    systemd.packages = [ pkgs.dconf ];
+
+    # For dconf executable
+    environment.systemPackages = [ pkgs.dconf ];
+
+    # Needed for unwrapped applications
+    environment.sessionVariables.GIO_EXTRA_MODULES = mkIf cfg.enable [ "${pkgs.dconf.lib}/lib/gio/modules" ];
+  };
+
+}
diff --git a/nixos/modules/programs/digitalbitbox/default.nix b/nixos/modules/programs/digitalbitbox/default.nix
new file mode 100644
index 00000000000..cabdf260cda
--- /dev/null
+++ b/nixos/modules/programs/digitalbitbox/default.nix
@@ -0,0 +1,39 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.digitalbitbox;
+in
+
+{
+  options.programs.digitalbitbox = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Installs the Digital Bitbox application and enables the complementary hardware module.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.digitalbitbox;
+      defaultText = literalExpression "pkgs.digitalbitbox";
+      description = "The Digital Bitbox package to use. This can be used to install a package with udev rules that differ from the defaults.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    hardware.digitalbitbox = {
+      enable = true;
+      package = cfg.package;
+    };
+  };
+
+  meta = {
+    doc = ./doc.xml;
+    maintainers = with lib.maintainers; [ vidbina ];
+  };
+}
diff --git a/nixos/modules/programs/digitalbitbox/doc.xml b/nixos/modules/programs/digitalbitbox/doc.xml
new file mode 100644
index 00000000000..c63201628db
--- /dev/null
+++ b/nixos/modules/programs/digitalbitbox/doc.xml
@@ -0,0 +1,74 @@
+<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/dmrconfig.nix b/nixos/modules/programs/dmrconfig.nix
new file mode 100644
index 00000000000..d2a5117c48e
--- /dev/null
+++ b/nixos/modules/programs/dmrconfig.nix
@@ -0,0 +1,38 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.dmrconfig;
+
+in {
+  meta.maintainers = [ maintainers.etu ];
+
+  ###### interface
+  options = {
+    programs.dmrconfig = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to configure system to enable use of dmrconfig. This
+          enables the required udev rules and installs the program.
+        '';
+        relatedPackages = [ "dmrconfig" ];
+      };
+
+      package = mkOption {
+        default = pkgs.dmrconfig;
+        type = types.package;
+        defaultText = literalExpression "pkgs.dmrconfig";
+        description = "dmrconfig derivation to use";
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    services.udev.packages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/programs/droidcam.nix b/nixos/modules/programs/droidcam.nix
new file mode 100644
index 00000000000..9843a1f5be2
--- /dev/null
+++ b/nixos/modules/programs/droidcam.nix
@@ -0,0 +1,16 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+{
+  options.programs.droidcam = {
+    enable = mkEnableOption "DroidCam client";
+  };
+
+  config = lib.mkIf config.programs.droidcam.enable {
+    environment.systemPackages = [ pkgs.droidcam ];
+
+    boot.extraModulePackages = [ config.boot.kernelPackages.v4l2loopback ];
+    boot.kernelModules = [ "v4l2loopback" "snd-aloop" ];
+  };
+}
diff --git a/nixos/modules/programs/environment.nix b/nixos/modules/programs/environment.nix
new file mode 100644
index 00000000000..a448727be77
--- /dev/null
+++ b/nixos/modules/programs/environment.nix
@@ -0,0 +1,67 @@
+# This module defines a standard configuration for NixOS global environment.
+
+# Most of the stuff here should probably be moved elsewhere sometime.
+
+{ config, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.environment;
+
+in
+
+{
+
+  config = {
+
+    environment.variables =
+      { NIXPKGS_CONFIG = "/etc/nix/nixpkgs-config.nix";
+        # note: many programs exec() this directly, so default options for less must not
+        # be specified here; do so in the default value of programs.less.envVariables instead
+        PAGER = mkDefault "less";
+        EDITOR = mkDefault "nano";
+        XDG_CONFIG_DIRS = [ "/etc/xdg" ]; # needs to be before profile-relative paths to allow changes through environment.etc
+      };
+
+    # since we set PAGER to this above, make sure it's installed
+    programs.less.enable = true;
+
+    environment.profiles = mkAfter
+      [ "/nix/var/nix/profiles/default"
+        "/run/current-system/sw"
+      ];
+
+    # TODO: move most of these elsewhere
+    environment.profileRelativeSessionVariables =
+      { PATH = [ "/bin" ];
+        INFOPATH = [ "/info" "/share/info" ];
+        KDEDIRS = [ "" ];
+        QT_PLUGIN_PATH = [ "/lib/qt4/plugins" "/lib/kde4/plugins" ];
+        QTWEBKIT_PLUGIN_PATH = [ "/lib/mozilla/plugins/" ];
+        GTK_PATH = [ "/lib/gtk-2.0" "/lib/gtk-3.0" "/lib/gtk-4.0" ];
+        XDG_CONFIG_DIRS = [ "/etc/xdg" ];
+        XDG_DATA_DIRS = [ "/share" ];
+        MOZ_PLUGIN_PATH = [ "/lib/mozilla/plugins" ];
+        LIBEXEC_PATH = [ "/lib/libexec" ];
+      };
+
+    environment.pathsToLink = [ "/lib/gtk-2.0" "/lib/gtk-3.0" "/lib/gtk-4.0" ];
+
+    environment.extraInit =
+      ''
+         unset ASPELL_CONF
+         for i in ${concatStringsSep " " (reverseList cfg.profiles)} ; do
+           if [ -d "$i/lib/aspell" ]; then
+             export ASPELL_CONF="dict-dir $i/lib/aspell"
+           fi
+         done
+
+         export NIX_USER_PROFILE_DIR="/nix/var/nix/profiles/per-user/$USER"
+         export NIX_PROFILES="${concatStringsSep " " (reverseList cfg.profiles)}"
+      '';
+
+  };
+
+}
diff --git a/nixos/modules/programs/evince.nix b/nixos/modules/programs/evince.nix
new file mode 100644
index 00000000000..c033230afb1
--- /dev/null
+++ b/nixos/modules/programs/evince.nix
@@ -0,0 +1,51 @@
+# Evince.
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let cfg = config.programs.evince;
+
+in {
+
+  # Added 2019-08-09
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "evince" "enable" ]
+      [ "programs" "evince" "enable" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    programs.evince = {
+
+      enable = mkEnableOption
+        "Evince, the GNOME document viewer";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.evince;
+        defaultText = literalExpression "pkgs.evince";
+        description = "Evince derivation to use.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.programs.evince.enable {
+
+    environment.systemPackages = [ cfg.package ];
+
+    services.dbus.packages = [ cfg.package ];
+
+    systemd.packages = [ cfg.package ];
+
+  };
+
+}
diff --git a/nixos/modules/programs/extra-container.nix b/nixos/modules/programs/extra-container.nix
new file mode 100644
index 00000000000..c10ccd76916
--- /dev/null
+++ b/nixos/modules/programs/extra-container.nix
@@ -0,0 +1,17 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.programs.extra-container;
+in {
+  options = {
+    programs.extra-container.enable = mkEnableOption ''
+      extra-container, a tool for running declarative NixOS containers
+      without host system rebuilds
+    '';
+  };
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.extra-container ];
+    boot.extraSystemdUnitPaths = [ "/etc/systemd-mutable/system" ];
+  };
+}
diff --git a/nixos/modules/programs/feedbackd.nix b/nixos/modules/programs/feedbackd.nix
new file mode 100644
index 00000000000..4194080c8a7
--- /dev/null
+++ b/nixos/modules/programs/feedbackd.nix
@@ -0,0 +1,33 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.feedbackd;
+in {
+  options = {
+    programs.feedbackd = {
+      enable = mkEnableOption ''
+        Whether to enable the feedbackd D-BUS service and udev rules.
+
+        Your user needs to be in the `feedbackd` group to trigger effects.
+      '';
+      package = mkOption {
+        description = ''
+          Which feedbackd package to use.
+        '';
+        type = types.package;
+        default = pkgs.feedbackd;
+        defaultText = literalExpression "pkgs.feedbackd";
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    services.dbus.packages = [ cfg.package ];
+    services.udev.packages = [ cfg.package ];
+
+    users.groups.feedbackd = {};
+  };
+}
diff --git a/nixos/modules/programs/file-roller.nix b/nixos/modules/programs/file-roller.nix
new file mode 100644
index 00000000000..3c47d598165
--- /dev/null
+++ b/nixos/modules/programs/file-roller.nix
@@ -0,0 +1,48 @@
+# File Roller.
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let cfg = config.programs.file-roller;
+
+in {
+
+  # Added 2019-08-09
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "file-roller" "enable" ]
+      [ "programs" "file-roller" "enable" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    programs.file-roller = {
+
+      enable = mkEnableOption "File Roller, an archive manager for GNOME";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.gnome.file-roller;
+        defaultText = literalExpression "pkgs.gnome.file-roller";
+        description = "File Roller derivation to use.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ cfg.package ];
+
+    services.dbus.packages = [ cfg.package ];
+
+  };
+
+}
diff --git a/nixos/modules/programs/firejail.nix b/nixos/modules/programs/firejail.nix
new file mode 100644
index 00000000000..76b42168c19
--- /dev/null
+++ b/nixos/modules/programs/firejail.nix
@@ -0,0 +1,97 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.firejail;
+
+  wrappedBins = pkgs.runCommand "firejail-wrapped-binaries"
+    { preferLocalBuild = true;
+      allowSubstitutes = false;
+    }
+    ''
+      mkdir -p $out/bin
+      ${lib.concatStringsSep "\n" (lib.mapAttrsToList (command: value:
+      let
+        opts = if builtins.isAttrs value
+        then value
+        else { executable = value; profile = null; extraArgs = []; };
+        args = lib.escapeShellArgs (
+          opts.extraArgs
+          ++ (optional (opts.profile != null) "--profile=${toString opts.profile}")
+          );
+      in
+      ''
+        cat <<_EOF >$out/bin/${command}
+        #! ${pkgs.runtimeShell} -e
+        exec /run/wrappers/bin/firejail ${args} -- ${toString opts.executable} "\$@"
+        _EOF
+        chmod 0755 $out/bin/${command}
+      '') cfg.wrappedBinaries)}
+    '';
+
+in {
+  options.programs.firejail = {
+    enable = mkEnableOption "firejail";
+
+    wrappedBinaries = mkOption {
+      type = types.attrsOf (types.either types.path (types.submodule {
+        options = {
+          executable = mkOption {
+            type = types.path;
+            description = "Executable to run sandboxed";
+            example = literalExpression ''"''${lib.getBin pkgs.firefox}/bin/firefox"'';
+          };
+          profile = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            description = "Profile to use";
+            example = literalExpression ''"''${pkgs.firejail}/etc/firejail/firefox.profile"'';
+          };
+          extraArgs = mkOption {
+            type = types.listOf types.str;
+            default = [];
+            description = "Extra arguments to pass to firejail";
+            example = [ "--private=~/.firejail_home" ];
+          };
+        };
+      }));
+      default = {};
+      example = literalExpression ''
+        {
+          firefox = {
+            executable = "''${lib.getBin pkgs.firefox}/bin/firefox";
+            profile = "''${pkgs.firejail}/etc/firejail/firefox.profile";
+          };
+          mpv = {
+            executable = "''${lib.getBin pkgs.mpv}/bin/mpv";
+            profile = "''${pkgs.firejail}/etc/firejail/mpv.profile";
+          };
+        }
+      '';
+      description = ''
+        Wrap the binaries in firejail and place them in the global path.
+        </para>
+        <para>
+        You will get file collisions if you put the actual application binary in
+        the global environment (such as by adding the application package to
+        <code>environment.systemPackages</code>), and applications started via
+        .desktop files are not wrapped if they specify the absolute path to the
+        binary.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    security.wrappers.firejail =
+      { setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${lib.getBin pkgs.firejail}/bin/firejail";
+      };
+
+    environment.systemPackages = [ pkgs.firejail ] ++ [ wrappedBins ];
+  };
+
+  meta.maintainers = with maintainers; [ peterhoeg ];
+}
diff --git a/nixos/modules/programs/fish.nix b/nixos/modules/programs/fish.nix
new file mode 100644
index 00000000000..8dd7101947f
--- /dev/null
+++ b/nixos/modules/programs/fish.nix
@@ -0,0 +1,320 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfge = config.environment;
+
+  cfg = config.programs.fish;
+
+  fishAbbrs = concatStringsSep "\n" (
+    mapAttrsFlatten (k: v: "abbr -ag ${k} ${escapeShellArg v}")
+      cfg.shellAbbrs
+  );
+
+  fishAliases = concatStringsSep "\n" (
+    mapAttrsFlatten (k: v: "alias ${k} ${escapeShellArg v}")
+      (filterAttrs (k: v: v != null) cfg.shellAliases)
+  );
+
+  envShellInit = pkgs.writeText "shellInit" cfge.shellInit;
+
+  envLoginShellInit = pkgs.writeText "loginShellInit" cfge.loginShellInit;
+
+  envInteractiveShellInit = pkgs.writeText "interactiveShellInit" cfge.interactiveShellInit;
+
+  sourceEnv = file:
+  if cfg.useBabelfish then
+    "source /etc/fish/${file}.fish"
+  else
+    ''
+      set fish_function_path ${pkgs.fishPlugins.foreign-env}/share/fish/vendor_functions.d $fish_function_path
+      fenv source /etc/fish/foreign-env/${file} > /dev/null
+      set -e fish_function_path[1]
+    '';
+
+  babelfishTranslate = path: name:
+    pkgs.runCommand "${name}.fish" {
+      nativeBuildInputs = [ pkgs.babelfish ];
+    } "${pkgs.babelfish}/bin/babelfish < ${path} > $out;";
+
+in
+
+{
+
+  options = {
+
+    programs.fish = {
+
+      enable = mkOption {
+        default = false;
+        description = ''
+          Whether to configure fish as an interactive shell.
+        '';
+        type = types.bool;
+      };
+
+      useBabelfish = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If enabled, the configured environment will be translated to native fish using <link xlink:href="https://github.com/bouk/babelfish">babelfish</link>.
+          Otherwise, <link xlink:href="https://github.com/oh-my-fish/plugin-foreign-env">foreign-env</link> will be used.
+        '';
+      };
+
+      vendor.config.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether fish should source configuration snippets provided by other packages.
+        '';
+      };
+
+      vendor.completions.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether fish should use completion files provided by other packages.
+        '';
+      };
+
+      vendor.functions.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether fish should autoload fish functions provided by other packages.
+        '';
+      };
+
+      shellAbbrs = mkOption {
+        default = {};
+        example = {
+          gco = "git checkout";
+          npu = "nix-prefetch-url";
+        };
+        description = ''
+          Set of fish abbreviations.
+        '';
+        type = with types; attrsOf str;
+      };
+
+      shellAliases = mkOption {
+        default = {};
+        description = ''
+          Set of aliases for fish shell, which overrides <option>environment.shellAliases</option>.
+          See <option>environment.shellAliases</option> for an option format description.
+        '';
+        type = with types; attrsOf (nullOr (either str path));
+      };
+
+      shellInit = mkOption {
+        default = "";
+        description = ''
+          Shell script code called during fish shell initialisation.
+        '';
+        type = types.lines;
+      };
+
+      loginShellInit = mkOption {
+        default = "";
+        description = ''
+          Shell script code called during fish login shell initialisation.
+        '';
+        type = types.lines;
+      };
+
+      interactiveShellInit = mkOption {
+        default = "";
+        description = ''
+          Shell script code called during interactive fish shell initialisation.
+        '';
+        type = types.lines;
+      };
+
+      promptInit = mkOption {
+        default = "";
+        description = ''
+          Shell script code used to initialise fish prompt.
+        '';
+        type = types.lines;
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    programs.fish.shellAliases = mapAttrs (name: mkDefault) cfge.shellAliases;
+
+    # Required for man completions
+    documentation.man.generateCaches = lib.mkDefault true;
+
+    environment = mkMerge [
+      (mkIf cfg.useBabelfish
+      {
+        etc."fish/setEnvironment.fish".source = babelfishTranslate config.system.build.setEnvironment "setEnvironment";
+        etc."fish/shellInit.fish".source = babelfishTranslate envShellInit "shellInit";
+        etc."fish/loginShellInit.fish".source = babelfishTranslate envLoginShellInit "loginShellInit";
+        etc."fish/interactiveShellInit.fish".source = babelfishTranslate envInteractiveShellInit "interactiveShellInit";
+     })
+
+      (mkIf (!cfg.useBabelfish)
+      {
+        etc."fish/foreign-env/shellInit".source = envShellInit;
+        etc."fish/foreign-env/loginShellInit".source = envLoginShellInit;
+        etc."fish/foreign-env/interactiveShellInit".source = envInteractiveShellInit;
+      })
+
+      {
+        etc."fish/nixos-env-preinit.fish".text =
+        if cfg.useBabelfish
+        then ''
+          # source the NixOS environment config
+          if [ -z "$__NIXOS_SET_ENVIRONMENT_DONE" ]
+            source /etc/fish/setEnvironment.fish
+          end
+        ''
+        else ''
+          # This happens before $__fish_datadir/config.fish sets fish_function_path, so it is currently
+          # unset. We set it and then completely erase it, leaving its configuration to $__fish_datadir/config.fish
+          set fish_function_path ${pkgs.fishPlugins.foreign-env}/share/fish/vendor_functions.d $__fish_datadir/functions
+
+          # source the NixOS environment config
+          if [ -z "$__NIXOS_SET_ENVIRONMENT_DONE" ]
+            fenv source ${config.system.build.setEnvironment}
+          end
+
+          # clear fish_function_path so that it will be correctly set when we return to $__fish_datadir/config.fish
+          set -e fish_function_path
+        '';
+      }
+
+      {
+        etc."fish/config.fish".text = ''
+        # /etc/fish/config.fish: DO NOT EDIT -- this file has been generated automatically.
+
+        # if we haven't sourced the general config, do it
+        if not set -q __fish_nixos_general_config_sourced
+          ${sourceEnv "shellInit"}
+
+          ${cfg.shellInit}
+
+          # and leave a note so we don't source this config section again from
+          # this very shell (children will source the general config anew)
+          set -g __fish_nixos_general_config_sourced 1
+        end
+
+        # if we haven't sourced the login config, do it
+        status --is-login; and not set -q __fish_nixos_login_config_sourced
+        and begin
+          ${sourceEnv "loginShellInit"}
+
+          ${cfg.loginShellInit}
+
+          # and leave a note so we don't source this config section again from
+          # this very shell (children will source the general config anew)
+          set -g __fish_nixos_login_config_sourced 1
+        end
+
+        # if we haven't sourced the interactive config, do it
+        status --is-interactive; and not set -q __fish_nixos_interactive_config_sourced
+        and begin
+          ${fishAbbrs}
+          ${fishAliases}
+
+          ${sourceEnv "interactiveShellInit"}
+
+          ${cfg.promptInit}
+          ${cfg.interactiveShellInit}
+
+          # and leave a note so we don't source this config section again from
+          # this very shell (children will source the general config anew,
+          # allowing configuration changes in, e.g, aliases, to propagate)
+          set -g __fish_nixos_interactive_config_sourced 1
+        end
+      '';
+      }
+
+      {
+        etc."fish/generated_completions".source =
+        let
+          patchedGenerator = pkgs.stdenv.mkDerivation {
+            name = "fish_patched-completion-generator";
+            srcs = [
+              "${pkgs.fish}/share/fish/tools/create_manpage_completions.py"
+              "${pkgs.fish}/share/fish/tools/deroff.py"
+            ];
+            unpackCmd = "cp $curSrc $(basename $curSrc)";
+            sourceRoot = ".";
+            patches = [ ./fish_completion-generator.patch ]; # to prevent collisions of identical completion files
+            dontBuild = true;
+            installPhase = ''
+              mkdir -p $out
+              cp * $out/
+            '';
+            preferLocalBuild = true;
+            allowSubstitutes = false;
+          };
+          generateCompletions = package: pkgs.runCommand
+            "${package.name}_fish-completions"
+            (
+              {
+                inherit package;
+                preferLocalBuild = true;
+                allowSubstitutes = false;
+              }
+              // optionalAttrs (package ? meta.priority) { meta.priority = package.meta.priority; }
+            )
+            ''
+              mkdir -p $out
+              if [ -d $package/share/man ]; then
+                find $package/share/man -type f | xargs ${pkgs.python3.interpreter} ${patchedGenerator}/create_manpage_completions.py --directory $out >/dev/null
+              fi
+            '';
+        in
+          pkgs.buildEnv {
+            name = "system_fish-completions";
+            ignoreCollisions = true;
+            paths = map generateCompletions config.environment.systemPackages;
+          };
+      }
+
+      # include programs that bring their own completions
+      {
+        pathsToLink = []
+        ++ optional cfg.vendor.config.enable "/share/fish/vendor_conf.d"
+        ++ optional cfg.vendor.completions.enable "/share/fish/vendor_completions.d"
+        ++ optional cfg.vendor.functions.enable "/share/fish/vendor_functions.d";
+      }
+
+      { systemPackages = [ pkgs.fish ]; }
+
+      {
+        shells = [
+          "/run/current-system/sw/bin/fish"
+          "${pkgs.fish}/bin/fish"
+        ];
+      }
+    ];
+
+    programs.fish.interactiveShellInit = ''
+      # add completions generated by NixOS to $fish_complete_path
+      begin
+        # joins with null byte to acommodate all characters in paths, then respectively gets all paths before (exclusive) / after (inclusive) the first one including "generated_completions",
+        # splits by null byte, and then removes all empty lines produced by using 'string'
+        set -l prev (string join0 $fish_complete_path | string match --regex "^.*?(?=\x00[^\x00]*generated_completions.*)" | string split0 | string match -er ".")
+        set -l post (string join0 $fish_complete_path | string match --regex "[^\x00]*generated_completions.*" | string split0 | string match -er ".")
+        set fish_complete_path $prev "/etc/fish/generated_completions" $post
+      end
+      # prevent fish from generating completions on first run
+      if not test -d $__fish_user_data_dir/generated_completions
+        ${pkgs.coreutils}/bin/mkdir $__fish_user_data_dir/generated_completions
+      end
+    '';
+
+  };
+
+}
diff --git a/nixos/modules/programs/fish_completion-generator.patch b/nixos/modules/programs/fish_completion-generator.patch
new file mode 100644
index 00000000000..fa207e484c9
--- /dev/null
+++ b/nixos/modules/programs/fish_completion-generator.patch
@@ -0,0 +1,14 @@
+--- a/create_manpage_completions.py
++++ b/create_manpage_completions.py
+@@ -879,10 +879,6 @@ def parse_manpage_at_path(manpage_path, output_directory):
+                 )
+                 return False
+ 
+-        # Output the magic word Autogenerated so we can tell if we can overwrite this
+-        built_command_output.insert(
+-            0, "# " + CMDNAME + "\n# Autogenerated from man page " + manpage_path
+-        )
+         # built_command_output.insert(2, "# using " + parser.__class__.__name__) # XXX MISATTRIBUTES THE CULPABLE PARSER! Was really using Type2 but reporting TypeDeroffManParser
+ 
+         for line in built_command_output:
+
diff --git a/nixos/modules/programs/flashrom.nix b/nixos/modules/programs/flashrom.nix
new file mode 100644
index 00000000000..f026c2e31cd
--- /dev/null
+++ b/nixos/modules/programs/flashrom.nix
@@ -0,0 +1,26 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.flashrom;
+in
+{
+  options.programs.flashrom = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Installs flashrom and configures udev rules for programmers
+        used by flashrom. Grants access to users in the "flashrom"
+        group.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [ pkgs.flashrom ];
+    environment.systemPackages = [ pkgs.flashrom ];
+    users.groups.flashrom = { };
+  };
+}
diff --git a/nixos/modules/programs/flexoptix-app.nix b/nixos/modules/programs/flexoptix-app.nix
new file mode 100644
index 00000000000..5e169be2d89
--- /dev/null
+++ b/nixos/modules/programs/flexoptix-app.nix
@@ -0,0 +1,25 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.flexoptix-app;
+in {
+  options = {
+    programs.flexoptix-app = {
+      enable = mkEnableOption "FLEXOPTIX app + udev rules";
+
+      package = mkOption {
+        description = "FLEXOPTIX app package to use";
+        type = types.package;
+        default = pkgs.flexoptix-app;
+        defaultText = literalExpression "pkgs.flexoptix-app";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    services.udev.packages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/programs/freetds.nix b/nixos/modules/programs/freetds.nix
new file mode 100644
index 00000000000..d95c44d756a
--- /dev/null
+++ b/nixos/modules/programs/freetds.nix
@@ -0,0 +1,61 @@
+# Global configuration for freetds environment.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.environment.freetds;
+
+in
+{
+  ###### interface
+
+  options = {
+
+    environment.freetds = mkOption {
+      type = types.attrsOf types.str;
+      default = {};
+      example = literalExpression ''
+        { MYDATABASE = '''
+            host = 10.0.2.100
+            port = 1433
+            tds version = 7.2
+          ''';
+        }
+      '';
+      description =
+        ''
+        Configure freetds database entries. Each attribute denotes
+        a section within freetds.conf, and the value (a string) is the config
+        content for that section. When at least one entry is configured
+        the global environment variables FREETDSCONF, FREETDS and SYBASE
+        will be configured to allow the programs that use freetds to find the
+        library and config.
+        '';
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf (length (attrNames cfg) > 0) {
+
+    environment.variables.FREETDSCONF = "/etc/freetds.conf";
+    environment.variables.FREETDS = "/etc/freetds.conf";
+    environment.variables.SYBASE = "${pkgs.freetds}";
+
+    environment.etc."freetds.conf" = { text =
+      (concatStrings (mapAttrsToList (name: value:
+        ''
+        [${name}]
+        ${value}
+        ''
+      ) cfg));
+    };
+
+  };
+
+}
diff --git a/nixos/modules/programs/fuse.nix b/nixos/modules/programs/fuse.nix
new file mode 100644
index 00000000000..c15896efbb5
--- /dev/null
+++ b/nixos/modules/programs/fuse.nix
@@ -0,0 +1,37 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.fuse;
+in {
+  meta.maintainers = with maintainers; [ primeos ];
+
+  options.programs.fuse = {
+    mountMax = mkOption {
+      # In the C code it's an "int" (i.e. signed and at least 16 bit), but
+      # negative numbers obviously make no sense:
+      type = types.ints.between 0 32767; # 2^15 - 1
+      default = 1000;
+      description = ''
+        Set the maximum number of FUSE mounts allowed to non-root users.
+      '';
+    };
+
+    userAllowOther = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Allow non-root users to specify the allow_other or allow_root mount
+        options, see mount.fuse3(8).
+      '';
+    };
+  };
+
+  config =  {
+    environment.etc."fuse.conf".text = ''
+      ${optionalString (!cfg.userAllowOther) "#"}user_allow_other
+      mount_max = ${toString cfg.mountMax}
+    '';
+  };
+}
diff --git a/nixos/modules/programs/gamemode.nix b/nixos/modules/programs/gamemode.nix
new file mode 100644
index 00000000000..a377a1619aa
--- /dev/null
+++ b/nixos/modules/programs/gamemode.nix
@@ -0,0 +1,98 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.gamemode;
+  settingsFormat = pkgs.formats.ini { };
+  configFile = settingsFormat.generate "gamemode.ini" cfg.settings;
+in
+{
+  options = {
+    programs.gamemode = {
+      enable = mkEnableOption "GameMode to optimise system performance on demand";
+
+      enableRenice = mkEnableOption "CAP_SYS_NICE on gamemoded to support lowering process niceness" // {
+        default = true;
+      };
+
+      settings = mkOption {
+        type = settingsFormat.type;
+        default = {};
+        description = ''
+          System-wide configuration for GameMode (/etc/gamemode.ini).
+          See gamemoded(8) man page for available settings.
+        '';
+        example = literalExpression ''
+          {
+            general = {
+              renice = 10;
+            };
+
+            # Warning: GPU optimisations have the potential to damage hardware
+            gpu = {
+              apply_gpu_optimisations = "accept-responsibility";
+              gpu_device = 0;
+              amd_performance_level = "high";
+            };
+
+            custom = {
+              start = "''${pkgs.libnotify}/bin/notify-send 'GameMode started'";
+              end = "''${pkgs.libnotify}/bin/notify-send 'GameMode ended'";
+            };
+          }
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment = {
+      systemPackages = [ pkgs.gamemode ];
+      etc."gamemode.ini".source = configFile;
+    };
+
+    security = {
+      polkit.enable = true;
+      wrappers = mkIf cfg.enableRenice {
+        gamemoded = {
+          owner = "root";
+          group = "root";
+          source = "${pkgs.gamemode}/bin/gamemoded";
+          capabilities = "cap_sys_nice+ep";
+        };
+      };
+    };
+
+    systemd = {
+      packages = [ pkgs.gamemode ];
+      user.services.gamemoded = {
+        # The upstream service already defines this, but doesn't get applied.
+        # See https://github.com/NixOS/nixpkgs/issues/81138
+        wantedBy = [ "default.target" ];
+
+        # Use pkexec from the security wrappers to allow users to
+        # run libexec/cpugovctl & libexec/gpuclockctl as root with
+        # the the actions defined in share/polkit-1/actions.
+        #
+        # This uses a link farm to make sure other wrapped executables
+        # aren't included in PATH.
+        environment.PATH = mkForce (pkgs.linkFarm "pkexec" [
+          {
+            name = "pkexec";
+            path = "${config.security.wrapperDir}/pkexec";
+          }
+        ]);
+
+        serviceConfig.ExecStart = mkIf cfg.enableRenice [
+          "" # Tell systemd to clear the existing ExecStart list, to prevent appending to it.
+          "${config.security.wrapperDir}/gamemoded"
+        ];
+      };
+    };
+  };
+
+  meta = {
+    maintainers = with maintainers; [ kira-bruneau ];
+  };
+}
diff --git a/nixos/modules/programs/geary.nix b/nixos/modules/programs/geary.nix
new file mode 100644
index 00000000000..407680c30dc
--- /dev/null
+++ b/nixos/modules/programs/geary.nix
@@ -0,0 +1,24 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.geary;
+
+in {
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  options = {
+    programs.geary.enable = mkEnableOption "Geary, a Mail client for GNOME 3";
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.gnome.geary ];
+    programs.dconf.enable = true;
+    services.gnome.gnome-keyring.enable = true;
+    services.gnome.gnome-online-accounts.enable = true;
+  };
+}
+
diff --git a/nixos/modules/programs/git.nix b/nixos/modules/programs/git.nix
new file mode 100644
index 00000000000..06ce374b199
--- /dev/null
+++ b/nixos/modules/programs/git.nix
@@ -0,0 +1,69 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.git;
+in
+
+{
+  options = {
+    programs.git = {
+      enable = mkEnableOption "git";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.git;
+        defaultText = literalExpression "pkgs.git";
+        example = literalExpression "pkgs.gitFull";
+        description = "The git package to use";
+      };
+
+      config = mkOption {
+        type = with types; attrsOf (attrsOf anything);
+        default = { };
+        example = {
+          init.defaultBranch = "main";
+          url."https://github.com/".insteadOf = [ "gh:" "github:" ];
+        };
+        description = ''
+          Configuration to write to /etc/gitconfig. See the CONFIGURATION FILE
+          section of git-config(1) for more information.
+        '';
+      };
+
+      lfs = {
+        enable = mkEnableOption "git-lfs";
+
+        package = mkOption {
+          type = types.package;
+          default = pkgs.git-lfs;
+          defaultText = literalExpression "pkgs.git-lfs";
+          description = "The git-lfs package to use";
+        };
+      };
+    };
+  };
+
+  config = mkMerge [
+    (mkIf cfg.enable {
+      environment.systemPackages = [ cfg.package ];
+      environment.etc.gitconfig = mkIf (cfg.config != {}) {
+        text = generators.toGitINI cfg.config;
+      };
+    })
+    (mkIf (cfg.enable && cfg.lfs.enable) {
+      environment.systemPackages = [ cfg.lfs.package ];
+      programs.git.config = {
+        filter.lfs = {
+          clean = "git-lfs clean -- %f";
+          smudge = "git-lfs smudge -- %f";
+          process = "git-lfs filter-process";
+          required = true;
+        };
+      };
+    })
+  ];
+
+  meta.maintainers = with maintainers; [ figsoda ];
+}
diff --git a/nixos/modules/programs/gnome-disks.nix b/nixos/modules/programs/gnome-disks.nix
new file mode 100644
index 00000000000..4b128b47126
--- /dev/null
+++ b/nixos/modules/programs/gnome-disks.nix
@@ -0,0 +1,50 @@
+# GNOME Disks.
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  # Added 2019-08-09
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-disks" "enable" ]
+      [ "programs" "gnome-disks" "enable" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    programs.gnome-disks = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable GNOME Disks daemon, a program designed to
+          be a UDisks2 graphical front-end.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.programs.gnome-disks.enable {
+
+    environment.systemPackages = [ pkgs.gnome.gnome-disk-utility ];
+
+    services.dbus.packages = [ pkgs.gnome.gnome-disk-utility ];
+
+  };
+
+}
diff --git a/nixos/modules/programs/gnome-documents.nix b/nixos/modules/programs/gnome-documents.nix
new file mode 100644
index 00000000000..43ad3163efd
--- /dev/null
+++ b/nixos/modules/programs/gnome-documents.nix
@@ -0,0 +1,54 @@
+# 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 = ''
+          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/gnome-terminal.nix b/nixos/modules/programs/gnome-terminal.nix
new file mode 100644
index 00000000000..71a6b217880
--- /dev/null
+++ b/nixos/modules/programs/gnome-terminal.nix
@@ -0,0 +1,38 @@
+# GNOME Terminal.
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.programs.gnome-terminal;
+
+in
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  # Added 2019-08-19
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-terminal-server" "enable" ]
+      [ "programs" "gnome-terminal" "enable" ])
+  ];
+
+  options = {
+    programs.gnome-terminal.enable = mkEnableOption "GNOME Terminal";
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.gnome.gnome-terminal ];
+    services.dbus.packages = [ pkgs.gnome.gnome-terminal ];
+    systemd.packages = [ pkgs.gnome.gnome-terminal ];
+
+    programs.bash.vteIntegration = true;
+    programs.zsh.vteIntegration = true;
+  };
+}
diff --git a/nixos/modules/programs/gnupg.nix b/nixos/modules/programs/gnupg.nix
new file mode 100644
index 00000000000..b41f30287ea
--- /dev/null
+++ b/nixos/modules/programs/gnupg.nix
@@ -0,0 +1,154 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.programs.gnupg;
+
+  xserverCfg = config.services.xserver;
+
+  defaultPinentryFlavor =
+    if xserverCfg.desktopManager.lxqt.enable
+    || xserverCfg.desktopManager.plasma5.enable then
+      "qt"
+    else if xserverCfg.desktopManager.xfce.enable then
+      "gtk2"
+    else if xserverCfg.enable || config.programs.sway.enable then
+      "gnome3"
+    else
+      null;
+
+in
+
+{
+
+  options.programs.gnupg = {
+    package = mkOption {
+      type = types.package;
+      default = pkgs.gnupg;
+      defaultText = literalExpression "pkgs.gnupg";
+      description = ''
+        The gpg package that should be used.
+      '';
+    };
+
+    agent.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enables GnuPG agent with socket-activation for every user session.
+      '';
+    };
+
+    agent.enableSSHSupport = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable SSH agent support in GnuPG agent. Also sets SSH_AUTH_SOCK
+        environment variable correctly. This will disable socket-activation
+        and thus always start a GnuPG agent per user session.
+      '';
+    };
+
+    agent.enableExtraSocket = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable extra socket for GnuPG agent.
+      '';
+    };
+
+    agent.enableBrowserSocket = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable browser socket for GnuPG agent.
+      '';
+    };
+
+    agent.pinentryFlavor = mkOption {
+      type = types.nullOr (types.enum pkgs.pinentry.flavors);
+      example = "gnome3";
+      default = defaultPinentryFlavor;
+      defaultText = literalDocBook ''matching the configured desktop environment'';
+      description = ''
+        Which pinentry interface to use. If not null, the path to the
+        pinentry binary will be passed to gpg-agent via commandline and
+        thus overrides the pinentry option in gpg-agent.conf in the user's
+        home directory.
+        If not set at all, it'll pick an appropriate flavor depending on the
+        system configuration (qt flavor for lxqt and plasma5, gtk2 for xfce
+        4.12, gnome3 on all other systems with X enabled, ncurses otherwise).
+      '';
+    };
+
+    dirmngr.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enables GnuPG network certificate management daemon with socket-activation for every user session.
+      '';
+    };
+  };
+
+  config = mkIf cfg.agent.enable {
+    # This overrides the systemd user unit shipped with the gnupg package
+    systemd.user.services.gpg-agent = mkIf (cfg.agent.pinentryFlavor != null) {
+      serviceConfig.ExecStart = [ "" ''
+        ${cfg.package}/bin/gpg-agent --supervised \
+          --pinentry-program ${pkgs.pinentry.${cfg.agent.pinentryFlavor}}/bin/pinentry
+      '' ];
+    };
+
+    systemd.user.sockets.gpg-agent = {
+      wantedBy = [ "sockets.target" ];
+    };
+
+    systemd.user.sockets.gpg-agent-ssh = mkIf cfg.agent.enableSSHSupport {
+      wantedBy = [ "sockets.target" ];
+    };
+
+    systemd.user.sockets.gpg-agent-extra = mkIf cfg.agent.enableExtraSocket {
+      wantedBy = [ "sockets.target" ];
+    };
+
+    systemd.user.sockets.gpg-agent-browser = mkIf cfg.agent.enableBrowserSocket {
+      wantedBy = [ "sockets.target" ];
+    };
+
+    systemd.user.sockets.dirmngr = mkIf cfg.dirmngr.enable {
+      wantedBy = [ "sockets.target" ];
+    };
+
+    services.dbus.packages = mkIf (cfg.agent.pinentryFlavor == "gnome3") [ pkgs.gcr ];
+
+    environment.systemPackages = with pkgs; [ cfg.package ];
+    systemd.packages = [ cfg.package ];
+
+    environment.interactiveShellInit = ''
+      # Bind gpg-agent to this TTY if gpg commands are used.
+      export GPG_TTY=$(tty)
+
+    '' + (optionalString cfg.agent.enableSSHSupport ''
+      # SSH agent protocol doesn't support changing TTYs, so bind the agent
+      # to every new TTY.
+      ${cfg.package}/bin/gpg-connect-agent --quiet updatestartuptty /bye > /dev/null
+    '');
+
+    environment.extraInit = mkIf cfg.agent.enableSSHSupport ''
+      if [ -z "$SSH_AUTH_SOCK" ]; then
+        export SSH_AUTH_SOCK=$(${cfg.package}/bin/gpgconf --list-dirs agent-ssh-socket)
+      fi
+    '';
+
+    assertions = [
+      { assertion = cfg.agent.enableSSHSupport -> !config.programs.ssh.startAgent;
+        message = "You can't use ssh-agent and GnuPG agent with SSH support enabled at the same time!";
+      }
+    ];
+  };
+
+  # uses attributes of the linked package
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/programs/gpaste.nix b/nixos/modules/programs/gpaste.nix
new file mode 100644
index 00000000000..cff2fb8d003
--- /dev/null
+++ b/nixos/modules/programs/gpaste.nix
@@ -0,0 +1,36 @@
+# GPaste.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  # Added 2019-08-09
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gpaste" "enable" ]
+      [ "programs" "gpaste" "enable" ])
+  ];
+
+  ###### interface
+  options = {
+     programs.gpaste = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable GPaste, a clipboard manager.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf config.programs.gpaste.enable {
+    environment.systemPackages = [ pkgs.gnome.gpaste ];
+    services.dbus.packages = [ pkgs.gnome.gpaste ];
+    systemd.packages = [ pkgs.gnome.gpaste ];
+    # gnome-control-center crashes in Keyboard Shortcuts pane without the GSettings schemas.
+    services.xserver.desktopManager.gnome.sessionPath = [ pkgs.gnome.gpaste ];
+  };
+}
diff --git a/nixos/modules/programs/gphoto2.nix b/nixos/modules/programs/gphoto2.nix
new file mode 100644
index 00000000000..93923ff3133
--- /dev/null
+++ b/nixos/modules/programs/gphoto2.nix
@@ -0,0 +1,30 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  meta.maintainers = [ maintainers.league ];
+
+  ###### interface
+  options = {
+    programs.gphoto2 = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to configure system to use gphoto2.
+          To grant digital camera access to a user, the user must
+          be part of the camera group:
+          <code>users.users.alice.extraGroups = ["camera"];</code>
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf config.programs.gphoto2.enable {
+    services.udev.packages = [ pkgs.libgphoto2 ];
+    environment.systemPackages = [ pkgs.gphoto2 ];
+    users.groups.camera = {};
+  };
+}
diff --git a/nixos/modules/programs/hamster.nix b/nixos/modules/programs/hamster.nix
new file mode 100644
index 00000000000..0bb56ad7ff3
--- /dev/null
+++ b/nixos/modules/programs/hamster.nix
@@ -0,0 +1,15 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  meta.maintainers = pkgs.hamster.meta.maintainers;
+
+  options.programs.hamster.enable =
+    mkEnableOption "hamster, a time tracking program";
+
+  config = lib.mkIf config.programs.hamster.enable {
+    environment.systemPackages = [ pkgs.hamster ];
+    services.dbus.packages = [ pkgs.hamster ];
+  };
+}
diff --git a/nixos/modules/programs/htop.nix b/nixos/modules/programs/htop.nix
new file mode 100644
index 00000000000..5c197838e47
--- /dev/null
+++ b/nixos/modules/programs/htop.nix
@@ -0,0 +1,58 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.programs.htop;
+
+  fmt = value:
+    if isList value then concatStringsSep " " (map fmt value) else
+    if isString value then value else
+    if isBool value || isInt value then toString value else
+    throw "Unrecognized type ${typeOf value} in htop settings";
+
+in
+
+{
+
+  options.programs.htop = {
+    package = mkOption {
+      type = types.package;
+      default = pkgs.htop;
+      defaultText = "pkgs.htop";
+      description = ''
+        The htop package that should be used.
+      '';
+    };
+
+    enable = mkEnableOption "htop process monitor";
+
+    settings = mkOption {
+      type = with types; attrsOf (oneOf [ str int bool (listOf (oneOf [ str int bool ])) ]);
+      default = {};
+      example = {
+        hide_kernel_threads = true;
+        hide_userland_threads = true;
+      };
+      description = ''
+        Extra global default configuration for htop
+        which is read on first startup only.
+        Htop subsequently uses ~/.config/htop/htoprc
+        as configuration source.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [
+      cfg.package
+    ];
+
+    environment.etc."htoprc".text = ''
+      # Global htop configuration
+      # To change set: programs.htop.settings.KEY = VALUE;
+    '' + concatStringsSep "\n" (mapAttrsToList (key: value: "${key}=${fmt value}") cfg.settings);
+  };
+
+}
diff --git a/nixos/modules/programs/iftop.nix b/nixos/modules/programs/iftop.nix
new file mode 100644
index 00000000000..c74714a9a6d
--- /dev/null
+++ b/nixos/modules/programs/iftop.nix
@@ -0,0 +1,20 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.iftop;
+in {
+  options = {
+    programs.iftop.enable = mkEnableOption "iftop + setcap wrapper";
+  };
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.iftop ];
+    security.wrappers.iftop = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_net_raw+p";
+      source = "${pkgs.iftop}/bin/iftop";
+    };
+  };
+}
diff --git a/nixos/modules/programs/iotop.nix b/nixos/modules/programs/iotop.nix
new file mode 100644
index 00000000000..b7c1c69f9dd
--- /dev/null
+++ b/nixos/modules/programs/iotop.nix
@@ -0,0 +1,19 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.iotop;
+in {
+  options = {
+    programs.iotop.enable = mkEnableOption "iotop + setcap wrapper";
+  };
+  config = mkIf cfg.enable {
+    security.wrappers.iotop = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_net_admin+p";
+      source = "${pkgs.iotop}/bin/iotop";
+    };
+  };
+}
diff --git a/nixos/modules/programs/java.nix b/nixos/modules/programs/java.nix
new file mode 100644
index 00000000000..4e4e0629e5d
--- /dev/null
+++ b/nixos/modules/programs/java.nix
@@ -0,0 +1,58 @@
+# This module provides JAVA_HOME, with a different way to install java
+# system-wide.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.java;
+in
+
+{
+
+  options = {
+
+    programs.java = {
+
+      enable = mkEnableOption "java" // {
+        description = ''
+          Install and setup the Java development kit.
+          <note>
+          <para>This adds JAVA_HOME to the global environment, by sourcing the
+            jdk's setup-hook on shell init. It is equivalent to starting a shell
+            through 'nix-shell -p jdk', or roughly the following system-wide
+            configuration:
+          </para>
+          <programlisting>
+            environment.variables.JAVA_HOME = ''${pkgs.jdk.home}/lib/openjdk;
+            environment.systemPackages = [ pkgs.jdk ];
+          </programlisting>
+          </note>
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.jdk;
+        defaultText = literalExpression "pkgs.jdk";
+        description = ''
+          Java package to install. Typical values are pkgs.jdk or pkgs.jre.
+        '';
+        type = types.package;
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ cfg.package ];
+
+    environment.shellInit = ''
+      test -e ${cfg.package}/nix-support/setup-hook && source ${cfg.package}/nix-support/setup-hook
+    '';
+
+  };
+
+}
diff --git a/nixos/modules/programs/k40-whisperer.nix b/nixos/modules/programs/k40-whisperer.nix
new file mode 100644
index 00000000000..3163e45f57e
--- /dev/null
+++ b/nixos/modules/programs/k40-whisperer.nix
@@ -0,0 +1,40 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.k40-whisperer;
+  pkg = cfg.package.override {
+    udevGroup = cfg.group;
+  };
+in
+{
+  options.programs.k40-whisperer = {
+    enable = mkEnableOption "K40-Whisperer";
+
+    group = mkOption {
+      type = types.str;
+      description = ''
+        Group assigned to the device when connected.
+      '';
+      default = "k40";
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.k40-whisperer;
+      defaultText = literalExpression "pkgs.k40-whisperer";
+      example = literalExpression "pkgs.k40-whisperer";
+      description = ''
+        K40 Whisperer package to use.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups.${cfg.group} = {};
+
+    environment.systemPackages = [ pkg ];
+    services.udev.packages = [ pkg ];
+  };
+}
diff --git a/nixos/modules/programs/kbdlight.nix b/nixos/modules/programs/kbdlight.nix
new file mode 100644
index 00000000000..8a2a0057cf2
--- /dev/null
+++ b/nixos/modules/programs/kbdlight.nix
@@ -0,0 +1,21 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.kbdlight;
+
+in
+{
+  options.programs.kbdlight.enable = mkEnableOption "kbdlight";
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.kbdlight ];
+    security.wrappers.kbdlight =
+      { setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${pkgs.kbdlight.out}/bin/kbdlight";
+      };
+  };
+}
diff --git a/nixos/modules/programs/kclock.nix b/nixos/modules/programs/kclock.nix
new file mode 100644
index 00000000000..42d81d2798b
--- /dev/null
+++ b/nixos/modules/programs/kclock.nix
@@ -0,0 +1,13 @@
+{ lib, pkgs, config, ... }:
+with lib;
+let
+  cfg = config.programs.kclock;
+  kclockPkg = pkgs.libsForQt5.kclock;
+in {
+  options.programs.kclock = { enable = mkEnableOption "Enable KClock"; };
+
+  config = mkIf cfg.enable {
+    services.dbus.packages = [ kclockPkg ];
+    environment.systemPackages = [ kclockPkg ];
+  };
+}
diff --git a/nixos/modules/programs/kdeconnect.nix b/nixos/modules/programs/kdeconnect.nix
new file mode 100644
index 00000000000..df698e84dd7
--- /dev/null
+++ b/nixos/modules/programs/kdeconnect.nix
@@ -0,0 +1,35 @@
+{ config, pkgs, lib, ... }:
+with lib;
+{
+  options.programs.kdeconnect = {
+    enable = mkEnableOption ''
+      kdeconnect.
+
+      Note that it will open the TCP and UDP port from
+      1714 to 1764 as they are needed for it to function properly.
+      You can use the <option>package</option> to use
+      <code>gnomeExtensions.gsconnect</code> as an alternative
+      implementation if you use Gnome.
+    '';
+    package = mkOption {
+      default = pkgs.kdeconnect;
+      defaultText = literalExpression "pkgs.kdeconnect";
+      type = types.package;
+      example = literalExpression "pkgs.gnomeExtensions.gsconnect";
+      description = ''
+        The package providing the implementation for kdeconnect.
+      '';
+    };
+  };
+  config =
+    let
+      cfg = config.programs.kdeconnect;
+    in
+      mkIf cfg.enable {
+        environment.systemPackages = [ cfg.package ];
+        networking.firewall = rec {
+          allowedTCPPortRanges = [ { from = 1714; to = 1764; } ];
+          allowedUDPPortRanges = allowedTCPPortRanges;
+        };
+      };
+}
diff --git a/nixos/modules/programs/less.nix b/nixos/modules/programs/less.nix
new file mode 100644
index 00000000000..794146b19fa
--- /dev/null
+++ b/nixos/modules/programs/less.nix
@@ -0,0 +1,134 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.programs.less;
+
+  configText = if (cfg.configFile != null) then (builtins.readFile cfg.configFile) else ''
+    #command
+    ${concatStringsSep "\n"
+      (mapAttrsToList (command: action: "${command} ${action}") cfg.commands)
+    }
+    ${if cfg.clearDefaultCommands then "#stop" else ""}
+
+    #line-edit
+    ${concatStringsSep "\n"
+      (mapAttrsToList (command: action: "${command} ${action}") cfg.lineEditingKeys)
+    }
+
+    #env
+    ${concatStringsSep "\n"
+      (mapAttrsToList (variable: values: "${variable}=${values}") cfg.envVariables)
+    }
+  '';
+
+  lessKey = pkgs.writeText "lessconfig" configText;
+
+in
+
+{
+  options = {
+
+    programs.less = {
+
+      # note that environment.nix sets PAGER=less, and
+      # therefore also enables this module
+      enable = mkEnableOption "less";
+
+      configFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = literalExpression ''"''${pkgs.my-configs}/lesskey"'';
+        description = ''
+          Path to lesskey configuration file.
+
+          <option>configFile</option> takes precedence over <option>commands</option>,
+          <option>clearDefaultCommands</option>, <option>lineEditingKeys</option>, and
+          <option>envVariables</option>.
+        '';
+      };
+
+      commands = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        example = {
+          h = "noaction 5\\e(";
+          l = "noaction 5\\e)";
+        };
+        description = "Defines new command keys.";
+      };
+
+      clearDefaultCommands = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Clear all default commands.
+          You should remember to set the quit key.
+          Otherwise you will not be able to leave less without killing it.
+        '';
+      };
+
+      lineEditingKeys = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        example = {
+          e = "abort";
+        };
+        description = "Defines new line-editing keys.";
+      };
+
+      envVariables = mkOption {
+        type = types.attrsOf types.str;
+        default = {
+          LESS = "-R";
+        };
+        example = {
+          LESS = "--quit-if-one-screen";
+        };
+        description = "Defines environment variables.";
+      };
+
+      lessopen = mkOption {
+        type = types.nullOr types.str;
+        default = "|${pkgs.lesspipe}/bin/lesspipe.sh %s";
+        defaultText = literalExpression ''"|''${pkgs.lesspipe}/bin/lesspipe.sh %s"'';
+        description = ''
+          Before less opens a file, it first gives your input preprocessor a chance to modify the way the contents of the file are displayed.
+        '';
+      };
+
+      lessclose = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          When less closes a file opened in such a way, it will call another program, called the input postprocessor, which may  perform  any  desired  clean-up  action (such  as deleting the replacement file created by LESSOPEN).
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.less ];
+
+    environment.variables = {
+      LESSKEYIN_SYSTEM = toString lessKey;
+    } // optionalAttrs (cfg.lessopen != null) {
+      LESSOPEN = cfg.lessopen;
+    } // optionalAttrs (cfg.lessclose != null) {
+      LESSCLOSE = cfg.lessclose;
+    };
+
+    warnings = optional (
+      cfg.clearDefaultCommands && (all (x: x != "quit") (attrValues cfg.commands))
+    ) ''
+      config.programs.less.clearDefaultCommands clears all default commands of less but there is no alternative binding for exiting.
+      Consider adding a binding for 'quit'.
+    '';
+  };
+
+  meta.maintainers = with maintainers; [ johnazoidberg ];
+
+}
diff --git a/nixos/modules/programs/liboping.nix b/nixos/modules/programs/liboping.nix
new file mode 100644
index 00000000000..4433f9767d6
--- /dev/null
+++ b/nixos/modules/programs/liboping.nix
@@ -0,0 +1,24 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.liboping;
+in {
+  options.programs.liboping = {
+    enable = mkEnableOption "liboping";
+  };
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ liboping ];
+    security.wrappers = mkMerge (map (
+      exec: {
+        "${exec}" = {
+          owner = "root";
+          group = "root";
+          capabilities = "cap_net_raw+p";
+          source = "${pkgs.liboping}/bin/${exec}";
+        };
+      }
+    ) [ "oping" "noping" ]);
+  };
+}
diff --git a/nixos/modules/programs/light.nix b/nixos/modules/programs/light.nix
new file mode 100644
index 00000000000..9f2a03e7e76
--- /dev/null
+++ b/nixos/modules/programs/light.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.light;
+
+in
+{
+  options = {
+    programs.light = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to install Light backlight control command
+          and udev rules granting access to members of the "video" group.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.light ];
+    services.udev.packages = [ pkgs.light ];
+  };
+}
diff --git a/nixos/modules/programs/mininet.nix b/nixos/modules/programs/mininet.nix
new file mode 100644
index 00000000000..6e90e7669ac
--- /dev/null
+++ b/nixos/modules/programs/mininet.nix
@@ -0,0 +1,39 @@
+# Global configuration for mininet
+# kernel must have NETNS/VETH/SCHED
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg  = config.programs.mininet;
+
+  generatedPath = with pkgs; makeSearchPath "bin"  [
+    iperf ethtool iproute2 socat
+  ];
+
+  pyEnv = pkgs.python.withPackages(ps: [ ps.mininet-python ]);
+
+  mnexecWrapped = pkgs.runCommand "mnexec-wrapper"
+    { buildInputs = [ pkgs.makeWrapper pkgs.pythonPackages.wrapPython ]; }
+    ''
+      makeWrapper ${pkgs.mininet}/bin/mnexec \
+        $out/bin/mnexec \
+        --prefix PATH : "${generatedPath}"
+
+      ln -s ${pyEnv}/bin/mn $out/bin/mn
+
+      # mn errors out without a telnet binary
+      # pkgs.telnet brings an undesired ifconfig into PATH see #43105
+      ln -s ${pkgs.telnet}/bin/telnet $out/bin/telnet
+    '';
+in
+{
+  options.programs.mininet.enable = mkEnableOption "Mininet";
+
+  config = mkIf cfg.enable {
+
+    virtualisation.vswitch.enable = true;
+
+    environment.systemPackages = [ mnexecWrapped ];
+  };
+}
diff --git a/nixos/modules/programs/mosh.nix b/nixos/modules/programs/mosh.nix
new file mode 100644
index 00000000000..e08099e21a0
--- /dev/null
+++ b/nixos/modules/programs/mosh.nix
@@ -0,0 +1,43 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg  = config.programs.mosh;
+
+in
+{
+  options.programs.mosh = {
+    enable = mkOption {
+      description = ''
+        Whether to enable mosh. Note, this will open ports in your firewall!
+      '';
+      default = false;
+      type = lib.types.bool;
+    };
+    withUtempter = mkOption {
+      description = ''
+        Whether to enable libutempter for mosh.
+        This is required so that mosh can write to /var/run/utmp (which can be queried with `who` to display currently connected user sessions).
+        Note, this will add a guid wrapper for the group utmp!
+      '';
+      default = true;
+      type = lib.types.bool;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ mosh ];
+    networking.firewall.allowedUDPPortRanges = [ { from = 60000; to = 61000; } ];
+    security.wrappers = mkIf cfg.withUtempter {
+      utempter = {
+        source = "${pkgs.libutempter}/lib/utempter/utempter";
+        owner = "root";
+        group = "utmp";
+        setuid = false;
+        setgid = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/programs/msmtp.nix b/nixos/modules/programs/msmtp.nix
new file mode 100644
index 00000000000..9c067bdc969
--- /dev/null
+++ b/nixos/modules/programs/msmtp.nix
@@ -0,0 +1,106 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.msmtp;
+
+in {
+  meta.maintainers = with maintainers; [ pacien ];
+
+  options = {
+    programs.msmtp = {
+      enable = mkEnableOption "msmtp - an SMTP client";
+
+      setSendmail = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to set the system sendmail to msmtp's.
+        '';
+      };
+
+      defaults = mkOption {
+        type = types.attrs;
+        default = {};
+        example = {
+          aliases = "/etc/aliases";
+          port = 587;
+          tls = true;
+        };
+        description = ''
+          Default values applied to all accounts.
+          See msmtp(1) for the available options.
+        '';
+      };
+
+      accounts = mkOption {
+        type = with types; attrsOf attrs;
+        default = {};
+        example = {
+          "default" = {
+            host = "smtp.example";
+            auth = true;
+            user = "someone";
+            passwordeval = "cat /secrets/password.txt";
+          };
+        };
+        description = ''
+          Named accounts and their respective configurations.
+          The special name "default" allows a default account to be defined.
+          See msmtp(1) for the available options.
+
+          Use `programs.msmtp.extraConfig` instead of this attribute set-based
+          option if ordered account inheritance is needed.
+
+          It is advised to use the `passwordeval` setting to read the password
+          from a secret file to avoid having it written in the world-readable
+          nix store. The password file must end with a newline (`\n`).
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra lines to add to the msmtp configuration verbatim.
+          See msmtp(1) for the syntax and available options.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.msmtp ];
+
+    services.mail.sendmailSetuidWrapper = mkIf cfg.setSendmail {
+      program = "sendmail";
+      source = "${pkgs.msmtp}/bin/sendmail";
+      setuid = false;
+      setgid = false;
+      owner = "root";
+      group = "root";
+    };
+
+    environment.etc."msmtprc".text = let
+      mkValueString = v:
+        if v == true then "on"
+        else if v == false then "off"
+        else generators.mkValueStringDefault {} v;
+      mkKeyValueString = k: v: "${k} ${mkValueString v}";
+      mkInnerSectionString =
+        attrs: concatStringsSep "\n" (mapAttrsToList mkKeyValueString attrs);
+      mkAccountString = name: attrs: ''
+        account ${name}
+        ${mkInnerSectionString attrs}
+      '';
+    in ''
+      defaults
+      ${mkInnerSectionString cfg.defaults}
+
+      ${concatStringsSep "\n" (mapAttrsToList mkAccountString cfg.accounts)}
+
+      ${cfg.extraConfig}
+    '';
+  };
+}
diff --git a/nixos/modules/programs/mtr.nix b/nixos/modules/programs/mtr.nix
new file mode 100644
index 00000000000..3cffe0fd8b2
--- /dev/null
+++ b/nixos/modules/programs/mtr.nix
@@ -0,0 +1,41 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.mtr;
+
+in {
+  options = {
+    programs.mtr = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to add mtr to the global environment and configure a
+          setcap wrapper for it.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.mtr;
+        defaultText = literalExpression "pkgs.mtr";
+        description = ''
+          The package to use.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ cfg.package ];
+
+    security.wrappers.mtr-packet = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_net_raw+p";
+      source = "${cfg.package}/bin/mtr-packet";
+    };
+  };
+}
diff --git a/nixos/modules/programs/nano.nix b/nixos/modules/programs/nano.nix
new file mode 100644
index 00000000000..5837dd46d7c
--- /dev/null
+++ b/nixos/modules/programs/nano.nix
@@ -0,0 +1,42 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.programs.nano;
+  LF = "\n";
+in
+
+{
+  ###### interface
+
+  options = {
+    programs.nano = {
+
+      nanorc = lib.mkOption {
+        type = lib.types.lines;
+        default = "";
+        description = ''
+          The system-wide nano configuration.
+          See <citerefentry><refentrytitle>nanorc</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
+        '';
+        example = ''
+          set nowrap
+          set tabstospaces
+          set tabsize 2
+        '';
+      };
+      syntaxHighlight = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = "Whether to enable syntax highlight for various languages.";
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = lib.mkIf (cfg.nanorc != "" || cfg.syntaxHighlight) {
+    environment.etc.nanorc.text = lib.concatStrings [ cfg.nanorc
+      (lib.optionalString cfg.syntaxHighlight ''${LF}include "${pkgs.nano}/share/nano/*.nanorc"'') ];
+  };
+
+}
diff --git a/nixos/modules/programs/nbd.nix b/nixos/modules/programs/nbd.nix
new file mode 100644
index 00000000000..fea9bc1ff71
--- /dev/null
+++ b/nixos/modules/programs/nbd.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.nbd;
+in
+{
+  options = {
+    programs.nbd = {
+      enable = mkEnableOption "Network Block Device (nbd) support";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ nbd ];
+    boot.kernelModules = [ "nbd" ];
+  };
+}
diff --git a/nixos/modules/programs/neovim.nix b/nixos/modules/programs/neovim.nix
new file mode 100644
index 00000000000..4649662542d
--- /dev/null
+++ b/nixos/modules/programs/neovim.nix
@@ -0,0 +1,166 @@
+{ config, lib, pkgs, ... }:
+
+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 {
+  options.programs.neovim = {
+    enable = mkEnableOption "Neovim";
+
+    defaultEditor = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        When enabled, installs neovim and configures neovim to be the default editor
+        using the EDITOR environment variable.
+      '';
+    };
+
+    viAlias = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Symlink <command>vi</command> to <command>nvim</command> binary.
+      '';
+    };
+
+    vimAlias = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Symlink <command>vim</command> to <command>nvim</command> binary.
+      '';
+    };
+
+    withRuby = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Enable Ruby provider.";
+    };
+
+    withPython3 = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Enable Python 3 provider.";
+    };
+
+    withNodeJs = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable Node provider.";
+    };
+
+    configure = mkOption {
+      type = types.attrs;
+      default = {};
+      example = literalExpression ''
+        {
+          customRC = '''
+            " here your custom configuration goes!
+          ''';
+          packages.myVimPackage = with pkgs.vimPlugins; {
+            # loaded on launch
+            start = [ fugitive ];
+            # manually loadable by calling `:packadd $plugin-name`
+            opt = [ ];
+          };
+        }
+      '';
+      description = ''
+        Generate your init file from your list of plugins and custom commands.
+        Neovim will then be wrapped to load <command>nvim -u /nix/store/<replaceable>hash</replaceable>-vimrc</command>
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.neovim-unwrapped;
+      defaultText = literalExpression "pkgs.neovim-unwrapped";
+      description = "The package to use for the neovim binary.";
+    };
+
+    finalPackage = mkOption {
+      type = types.package;
+      visible = false;
+      readOnly = true;
+      description = "Resulting customized neovim package.";
+    };
+
+    runtime = mkOption {
+      default = {};
+      example = literalExpression ''
+        { "ftplugin/c.vim".text = "setlocal omnifunc=v:lua.vim.lsp.omnifunc"; }
+      '';
+      description = ''
+        Set of files that have to be linked in <filename>runtime</filename>.
+      '';
+
+      type = with types; attrsOf (submodule (
+        { name, config, ... }:
+        { options = {
+
+            enable = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                Whether this /etc file should be generated.  This
+                option allows specific /etc files to be disabled.
+              '';
+            };
+
+            target = mkOption {
+              type = types.str;
+              description = ''
+                Name of symlink.  Defaults to the attribute
+                name.
+              '';
+            };
+
+            text = mkOption {
+              default = null;
+              type = types.nullOr types.lines;
+              description = "Text of the file.";
+            };
+
+            source = mkOption {
+              type = types.path;
+              description = "Path of the source file.";
+            };
+
+          };
+
+          config = {
+            target = mkDefault name;
+            source = mkIf (config.text != null) (
+              let name' = "neovim-runtime" + baseNameOf name;
+              in mkDefault (pkgs.writeText name' config.text));
+          };
+
+        }));
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [
+      cfg.finalPackage
+    ];
+    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 // {
+
+        customRC = (cfg.configure.customRC or "") + ''
+          set runtimepath^=${runtime}/etc
+        '';
+      };
+    };
+  };
+}
diff --git a/nixos/modules/programs/nm-applet.nix b/nixos/modules/programs/nm-applet.nix
new file mode 100644
index 00000000000..5bcee30125b
--- /dev/null
+++ b/nixos/modules/programs/nm-applet.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+{
+  meta = {
+    maintainers = lib.teams.freedesktop.members;
+  };
+
+  options.programs.nm-applet = {
+    enable = lib.mkEnableOption "nm-applet";
+
+    indicator = lib.mkOption {
+      type = lib.types.bool;
+      default = true;
+      description = ''
+        Whether to use indicator instead of status icon.
+        It is needed for Appindicator environments, like Enlightenment.
+      '';
+    };
+  };
+
+  config = lib.mkIf config.programs.nm-applet.enable {
+    systemd.user.services.nm-applet = {
+      description = "Network manager applet";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+      serviceConfig.ExecStart = "${pkgs.networkmanagerapplet}/bin/nm-applet ${lib.optionalString config.programs.nm-applet.indicator "--indicator"}";
+    };
+
+    services.dbus.packages = [ pkgs.gcr ];
+  };
+}
diff --git a/nixos/modules/programs/noisetorch.nix b/nixos/modules/programs/noisetorch.nix
new file mode 100644
index 00000000000..f76555289f1
--- /dev/null
+++ b/nixos/modules/programs/noisetorch.nix
@@ -0,0 +1,28 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let cfg = config.programs.noisetorch;
+in {
+  options.programs.noisetorch = {
+    enable = mkEnableOption "noisetorch + setcap wrapper";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.noisetorch;
+      defaultText = literalExpression "pkgs.noisetorch";
+      description = ''
+        The noisetorch package to use.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    security.wrappers.noisetorch = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_sys_resource=+ep";
+      source = "${cfg.package}/bin/noisetorch";
+    };
+  };
+}
diff --git a/nixos/modules/programs/npm.nix b/nixos/modules/programs/npm.nix
new file mode 100644
index 00000000000..d79c6c73400
--- /dev/null
+++ b/nixos/modules/programs/npm.nix
@@ -0,0 +1,54 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.npm;
+in
+
+{
+  ###### interface
+
+  options = {
+    programs.npm = {
+      enable = mkEnableOption "<command>npm</command> global config";
+
+      package = mkOption {
+        type = types.package;
+        description = "The npm package version / flavor to use";
+        default = pkgs.nodePackages.npm;
+        defaultText = literalExpression "pkgs.nodePackages.npm";
+        example = literalExpression "pkgs.nodePackages_13_x.npm";
+      };
+
+      npmrc = mkOption {
+        type = lib.types.lines;
+        description = ''
+          The system-wide npm configuration.
+          See <link xlink:href="https://docs.npmjs.com/misc/config"/>.
+        '';
+        default = ''
+          prefix = ''${HOME}/.npm
+        '';
+        example = ''
+          prefix = ''${HOME}/.npm
+          https-proxy=proxy.example.com
+          init-license=MIT
+          init-author-url=http://npmjs.org
+          color=true
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = lib.mkIf cfg.enable {
+    environment.etc.npmrc.text = cfg.npmrc;
+
+    environment.variables.NPM_CONFIG_GLOBALCONFIG = "/etc/npmrc";
+
+    environment.systemPackages = [ cfg.package ];
+  };
+
+}
diff --git a/nixos/modules/programs/oblogout.nix b/nixos/modules/programs/oblogout.nix
new file mode 100644
index 00000000000..a039b0623b5
--- /dev/null
+++ b/nixos/modules/programs/oblogout.nix
@@ -0,0 +1,11 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  imports = [
+    (mkRemovedOptionModule [ "programs" "oblogout" ] "programs.oblogout has been removed from NixOS. This is because the oblogout repository has been archived upstream.")
+  ];
+
+}
diff --git a/nixos/modules/programs/pantheon-tweaks.nix b/nixos/modules/programs/pantheon-tweaks.nix
new file mode 100644
index 00000000000..0b8a19ea22c
--- /dev/null
+++ b/nixos/modules/programs/pantheon-tweaks.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  meta = {
+    maintainers = teams.pantheon.members;
+  };
+
+  ###### interface
+  options = {
+    programs.pantheon-tweaks.enable = mkEnableOption "Pantheon Tweaks, an unofficial system settings panel for Pantheon";
+  };
+
+  ###### implementation
+  config = mkIf config.programs.pantheon-tweaks.enable {
+    services.xserver.desktopManager.pantheon.extraSwitchboardPlugs = [ pkgs.pantheon-tweaks ];
+  };
+}
diff --git a/nixos/modules/programs/partition-manager.nix b/nixos/modules/programs/partition-manager.nix
new file mode 100644
index 00000000000..1be2f0a69a1
--- /dev/null
+++ b/nixos/modules/programs/partition-manager.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  meta.maintainers = [ maintainers.oxalica ];
+
+  ###### interface
+  options = {
+    programs.partition-manager.enable = mkEnableOption "KDE Partition Manager";
+  };
+
+  ###### implementation
+  config = mkIf config.programs.partition-manager.enable {
+    services.dbus.packages = [ pkgs.libsForQt5.kpmcore ];
+    # `kpmcore` need to be installed to pull in polkit actions.
+    environment.systemPackages = [ pkgs.libsForQt5.kpmcore pkgs.partition-manager ];
+  };
+}
diff --git a/nixos/modules/programs/phosh.nix b/nixos/modules/programs/phosh.nix
new file mode 100644
index 00000000000..ad875616ac9
--- /dev/null
+++ b/nixos/modules/programs/phosh.nix
@@ -0,0 +1,162 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.phosh;
+
+  # Based on https://source.puri.sm/Librem5/librem5-base/-/blob/4596c1056dd75ac7f043aede07887990fd46f572/default/sm.puri.OSK0.desktop
+  oskItem = pkgs.makeDesktopItem {
+    name = "sm.puri.OSK0";
+    desktopName = "On-screen keyboard";
+    exec = "${pkgs.squeekboard}/bin/squeekboard";
+    categories = [ "GNOME" "Core" ];
+    onlyShowIn = [ "GNOME" ];
+    noDisplay = true;
+    extraConfig = {
+      X-GNOME-Autostart-Phase = "Panel";
+      X-GNOME-Provides = "inputmethod";
+      X-GNOME-Autostart-Notify = "true";
+      X-GNOME-AutoRestart = "true";
+    };
+  };
+
+  phocConfigType = types.submodule {
+    options = {
+      xwayland = mkOption {
+        description = ''
+          Whether to enable XWayland support.
+
+          To start XWayland immediately, use `immediate`.
+        '';
+        type = types.enum [ "true" "false" "immediate" ];
+        default = "false";
+      };
+      cursorTheme = mkOption {
+        description = ''
+          Cursor theme to use in Phosh.
+        '';
+        type = types.str;
+        default = "default";
+      };
+      outputs = mkOption {
+        description = ''
+          Output configurations.
+        '';
+        type = types.attrsOf phocOutputType;
+        default = {
+          DSI-1 = {
+            scale = 2;
+          };
+        };
+      };
+    };
+  };
+
+  phocOutputType = types.submodule {
+    options = {
+      modeline = mkOption {
+        description = ''
+          One or more modelines.
+        '';
+        type = types.either types.str (types.listOf types.str);
+        default = [];
+        example = [
+          "87.25 720 776 848  976 1440 1443 1453 1493 -hsync +vsync"
+          "65.13 768 816 896 1024 1024 1025 1028 1060 -HSync +VSync"
+        ];
+      };
+      mode = mkOption {
+        description = ''
+          Default video mode.
+        '';
+        type = types.nullOr types.str;
+        default = null;
+        example = "768x1024";
+      };
+      scale = mkOption {
+        description = ''
+          Display scaling factor.
+        '';
+        type = types.nullOr types.ints.unsigned;
+        default = null;
+        example = 2;
+      };
+      rotate = mkOption {
+        description = ''
+          Screen transformation.
+        '';
+        type = types.enum [
+          "90" "180" "270" "flipped" "flipped-90" "flipped-180" "flipped-270" null
+        ];
+        default = null;
+      };
+    };
+  };
+
+  optionalKV = k: v: if v == null then "" else "${k} = ${builtins.toString v}";
+
+  renderPhocOutput = name: output: let
+    modelines = if builtins.isList output.modeline
+      then output.modeline
+      else [ output.modeline ];
+    renderModeline = l: "modeline = ${l}";
+  in ''
+    [output:${name}]
+    ${concatStringsSep "\n" (map renderModeline modelines)}
+    ${optionalKV "mode" output.mode}
+    ${optionalKV "scale" output.scale}
+    ${optionalKV "rotate" output.rotate}
+  '';
+
+  renderPhocConfig = phoc: let
+    outputs = mapAttrsToList renderPhocOutput phoc.outputs;
+  in ''
+    [core]
+    xwayland = ${phoc.xwayland}
+    ${concatStringsSep "\n" outputs}
+    [cursor]
+    theme = ${phoc.cursorTheme}
+  '';
+in {
+  options = {
+    programs.phosh = {
+      enable = mkEnableOption ''
+        Whether to enable, Phosh, related packages and default configurations.
+      '';
+      phocConfig = mkOption {
+        description = ''
+          Configurations for the Phoc compositor.
+        '';
+        type = types.oneOf [ types.lines types.path phocConfigType ];
+        default = {};
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [
+      pkgs.phoc
+      pkgs.phosh
+      pkgs.squeekboard
+      oskItem
+    ];
+
+    systemd.packages = [ pkgs.phosh ];
+
+    programs.feedbackd.enable = true;
+
+    security.pam.services.phosh = {};
+
+    hardware.opengl.enable = mkDefault true;
+
+    services.gnome.core-shell.enable = true;
+    services.gnome.core-os-services.enable = true;
+    services.xserver.displayManager.sessionPackages = [ pkgs.phosh ];
+
+    environment.etc."phosh/phoc.ini".source =
+      if builtins.isPath cfg.phocConfig then cfg.phocConfig
+      else if builtins.isString cfg.phocConfig then pkgs.writeText "phoc.ini" cfg.phocConfig
+      else pkgs.writeText "phoc.ini" (renderPhocConfig cfg.phocConfig);
+  };
+}
diff --git a/nixos/modules/programs/plotinus.nix b/nixos/modules/programs/plotinus.nix
new file mode 100644
index 00000000000..2c90a41ba02
--- /dev/null
+++ b/nixos/modules/programs/plotinus.nix
@@ -0,0 +1,36 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.plotinus;
+in
+{
+  meta = {
+    maintainers = pkgs.plotinus.meta.maintainers;
+    doc = ./plotinus.xml;
+  };
+
+  ###### interface
+
+  options = {
+    programs.plotinus = {
+      enable = mkOption {
+        default = false;
+        description = ''
+          Whether to enable the Plotinus GTK 3 plugin. Plotinus provides a
+          popup (triggered by Ctrl-Shift-P) to search the menus of a
+          compatible application.
+        '';
+        type = types.bool;
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.sessionVariables.XDG_DATA_DIRS = [ "${pkgs.plotinus}/share/gsettings-schemas/${pkgs.plotinus.name}" ];
+    environment.variables.GTK3_MODULES = [ "${pkgs.plotinus}/lib/libplotinus.so" ];
+  };
+}
diff --git a/nixos/modules/programs/plotinus.xml b/nixos/modules/programs/plotinus.xml
new file mode 100644
index 00000000000..8fc8c22c6d7
--- /dev/null
+++ b/nixos/modules/programs/plotinus.xml
@@ -0,0 +1,30 @@
+<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
new file mode 100644
index 00000000000..3f44e23a93e
--- /dev/null
+++ b/nixos/modules/programs/proxychains.nix
@@ -0,0 +1,165 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+
+  cfg = config.programs.proxychains;
+
+  configFile = ''
+    ${cfg.chain.type}_chain
+    ${optionalString (cfg.chain.type == "random")
+    "chain_len = ${builtins.toString cfg.chain.length}"}
+    ${optionalString cfg.proxyDNS "proxy_dns"}
+    ${optionalString cfg.quietMode "quiet_mode"}
+    remote_dns_subnet ${builtins.toString cfg.remoteDNSSubnet}
+    tcp_read_time_out ${builtins.toString cfg.tcpReadTimeOut}
+    tcp_connect_time_out ${builtins.toString cfg.tcpConnectTimeOut}
+    localnet ${cfg.localnet}
+    [ProxyList]
+    ${builtins.concatStringsSep "\n"
+      (lib.mapAttrsToList (k: v: "${v.type} ${v.host} ${builtins.toString v.port}")
+        (lib.filterAttrs (k: v: v.enable) cfg.proxies))}
+  '';
+
+  proxyOptions = {
+    options = {
+      enable = mkEnableOption "this proxy";
+
+      type = mkOption {
+        type = types.enum [ "http" "socks4" "socks5" ];
+        description = "Proxy type.";
+      };
+
+      host = mkOption {
+        type = types.str;
+        description = "Proxy host or IP address.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        description = "Proxy port";
+      };
+    };
+  };
+
+in {
+
+  ###### interface
+
+  options = {
+
+    programs.proxychains = {
+
+      enable = mkEnableOption "installing proxychains configuration";
+
+      chain = {
+        type = mkOption {
+          type = types.enum [ "dynamic" "strict" "random" ];
+          default = "strict";
+          description = ''
+            <literal>dynamic</literal> - Each connection will be done via chained proxies
+            all proxies chained in the order as they appear in the list
+            at least one proxy must be online to play in chain
+            (dead proxies are skipped)
+            otherwise <literal>EINTR</literal> is returned to the app.
+
+            <literal>strict</literal> - Each connection will be done via chained proxies
+            all proxies chained in the order as they appear in the list
+            all proxies must be online to play in chain
+            otherwise <literal>EINTR</literal> is returned to the app.
+
+            <literal>random</literal> - Each connection will be done via random proxy
+            (or proxy chain, see <option>programs.proxychains.chain.length</option>) from the list.
+          '';
+        };
+        length = mkOption {
+          type = types.nullOr types.int;
+          default = null;
+          description = ''
+            Chain length for random chain.
+          '';
+        };
+      };
+
+      proxyDNS = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Proxy DNS requests - no leak for DNS data.";
+      };
+
+      quietMode = mkEnableOption "Quiet mode (no output from the library).";
+
+      remoteDNSSubnet = mkOption {
+        type = types.enum [ 10 127 224 ];
+        default = 224;
+        description = ''
+          Set the class A subnet number to use for the internal remote DNS mapping, uses the reserved 224.x.x.x range by default.
+        '';
+      };
+
+      tcpReadTimeOut = mkOption {
+        type = types.int;
+        default = 15000;
+        description = "Connection read time-out in milliseconds.";
+      };
+
+      tcpConnectTimeOut = mkOption {
+        type = types.int;
+        default = 8000;
+        description = "Connection time-out in milliseconds.";
+      };
+
+      localnet = mkOption {
+        type = types.str;
+        default = "127.0.0.0/255.0.0.0";
+        description = "By default enable localnet for loopback address ranges.";
+      };
+
+      proxies = mkOption {
+        type = types.attrsOf (types.submodule proxyOptions);
+        description = ''
+          Proxies to be used by proxychains.
+        '';
+
+        example = literalExpression ''
+          { myproxy =
+            { type = "socks4";
+              host = "127.0.0.1";
+              port = 1337;
+            };
+          }
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  meta.maintainers = with maintainers; [ sorki ];
+
+  config = mkIf cfg.enable {
+
+    assertions = singleton {
+      assertion = cfg.chain.type != "random" && cfg.chain.length == null;
+      message = ''
+        Option `programs.proxychains.chain.length`
+        only makes sense with `programs.proxychains.chain.type` = "random".
+      '';
+    };
+
+    programs.proxychains.proxies = mkIf config.services.tor.client.enable
+      {
+        torproxy = mkDefault {
+          enable = true;
+          type = "socks4";
+          host = "127.0.0.1";
+          port = 9050;
+        };
+      };
+
+    environment.etc."proxychains.conf".text = configFile;
+    environment.systemPackages = [ pkgs.proxychains ];
+  };
+
+}
diff --git a/nixos/modules/programs/qt5ct.nix b/nixos/modules/programs/qt5ct.nix
new file mode 100644
index 00000000000..88e861bf403
--- /dev/null
+++ b/nixos/modules/programs/qt5ct.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  meta.maintainers = [ maintainers.romildo ];
+
+  ###### interface
+  options = {
+    programs.qt5ct = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to enable the Qt5 Configuration Tool (qt5ct), a
+          program that allows users to configure Qt5 settings (theme,
+          font, icons, etc.) under desktop environments or window
+          manager without Qt integration.
+
+          Official home page: <link xlink:href="https://sourceforge.net/projects/qt5ct/">https://sourceforge.net/projects/qt5ct/</link>
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf config.programs.qt5ct.enable {
+    environment.variables.QT_QPA_PLATFORMTHEME = "qt5ct";
+    environment.systemPackages = with pkgs; [ libsForQt5.qt5ct ];
+  };
+}
diff --git a/nixos/modules/programs/screen.nix b/nixos/modules/programs/screen.nix
new file mode 100644
index 00000000000..728a0eb8cea
--- /dev/null
+++ b/nixos/modules/programs/screen.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkOption mkIf types;
+  cfg = config.programs.screen;
+in
+
+{
+  ###### interface
+
+  options = {
+    programs.screen = {
+
+      screenrc = mkOption {
+        default = "";
+        description = ''
+          The contents of /etc/screenrc file.
+        '';
+        type = types.lines;
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf (cfg.screenrc != "") {
+    environment.etc.screenrc.text = cfg.screenrc;
+
+    environment.systemPackages = [ pkgs.screen ];
+    security.pam.services.screen = {};
+  };
+
+}
diff --git a/nixos/modules/programs/seahorse.nix b/nixos/modules/programs/seahorse.nix
new file mode 100644
index 00000000000..c0a356bff57
--- /dev/null
+++ b/nixos/modules/programs/seahorse.nix
@@ -0,0 +1,46 @@
+# Seahorse.
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+ # Added 2019-08-27
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "seahorse" "enable" ]
+      [ "programs" "seahorse" "enable" ])
+  ];
+
+
+  ###### interface
+
+  options = {
+
+    programs.seahorse = {
+
+      enable = mkEnableOption "Seahorse, a GNOME application for managing encryption keys and passwords in the GNOME Keyring";
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.programs.seahorse.enable {
+
+    programs.ssh.askPassword = mkDefault "${pkgs.gnome.seahorse}/libexec/seahorse/ssh-askpass";
+
+    environment.systemPackages = [
+      pkgs.gnome.seahorse
+    ];
+
+    services.dbus.packages = [
+      pkgs.gnome.seahorse
+    ];
+
+  };
+
+}
diff --git a/nixos/modules/programs/sedutil.nix b/nixos/modules/programs/sedutil.nix
new file mode 100644
index 00000000000..7efc80f4abb
--- /dev/null
+++ b/nixos/modules/programs/sedutil.nix
@@ -0,0 +1,18 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.sedutil;
+
+in {
+  options.programs.sedutil.enable = mkEnableOption "sedutil";
+
+  config = mkIf cfg.enable {
+    boot.kernelParams = [
+      "libata.allow_tpm=1"
+    ];
+
+    environment.systemPackages = with pkgs; [ sedutil ];
+  };
+}
diff --git a/nixos/modules/programs/shadow.nix b/nixos/modules/programs/shadow.nix
new file mode 100644
index 00000000000..963cd8853db
--- /dev/null
+++ b/nixos/modules/programs/shadow.nix
@@ -0,0 +1,129 @@
+# Configuration for the pwdutils suite of tools: passwd, useradd, etc.
+
+{ config, lib, utils, pkgs, ... }:
+
+with lib;
+
+let
+
+  /*
+  There are three different sources for user/group id ranges, each of which gets
+  used by different programs:
+  - The login.defs file, used by the useradd, groupadd and newusers commands
+  - The update-users-groups.pl file, used by NixOS in the activation phase to
+    decide on which ids to use for declaratively defined users without a static
+    id
+  - Systemd compile time options -Dsystem-uid-max= and -Dsystem-gid-max=, used
+    by systemd for features like ConditionUser=@system and systemd-sysusers
+  */
+  loginDefs =
+    ''
+      DEFAULT_HOME yes
+
+      SYS_UID_MIN  400
+      SYS_UID_MAX  999
+      UID_MIN      1000
+      UID_MAX      29999
+
+      SYS_GID_MIN  400
+      SYS_GID_MAX  999
+      GID_MIN      1000
+      GID_MAX      29999
+
+      TTYGROUP     tty
+      TTYPERM      0620
+
+      # Ensure privacy for newly created home directories.
+      UMASK        077
+
+      # Uncomment this and install chfn SUID to allow non-root
+      # users to change their account GECOS information.
+      # This should be made configurable.
+      #CHFN_RESTRICT frwh
+
+    '';
+
+  mkSetuidRoot = source:
+    { setuid = true;
+      owner = "root";
+      group = "root";
+      inherit source;
+    };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    users.defaultUserShell = lib.mkOption {
+      description = ''
+        This option defines the default shell assigned to user
+        accounts. This can be either a full system path or a shell package.
+
+        This must not be a store path, since the path is
+        used outside the store (in particular in /etc/passwd).
+      '';
+      example = literalExpression "pkgs.zsh";
+      type = types.either types.path types.shellPackage;
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = {
+
+    environment.systemPackages =
+      lib.optional config.users.mutableUsers pkgs.shadow ++
+      lib.optional (types.shellPackage.check config.users.defaultUserShell)
+        config.users.defaultUserShell;
+
+    environment.etc =
+      { # /etc/login.defs: global configuration for pwdutils.  You
+        # cannot login without it!
+        "login.defs".source = pkgs.writeText "login.defs" loginDefs;
+
+        # /etc/default/useradd: configuration for useradd.
+        "default/useradd".source = pkgs.writeText "useradd"
+          ''
+            GROUP=100
+            HOME=/home
+            SHELL=${utils.toShellPath config.users.defaultUserShell}
+          '';
+      };
+
+    security.pam.services =
+      { chsh = { rootOK = true; };
+        chfn = { rootOK = true; };
+        su = { rootOK = true; forwardXAuth = true; logFailures = true; };
+        passwd = {};
+        # Note: useradd, groupadd etc. aren't setuid root, so it
+        # doesn't really matter what the PAM config says as long as it
+        # lets root in.
+        useradd = { rootOK = true; };
+        usermod = { rootOK = true; };
+        userdel = { rootOK = true; };
+        groupadd = { rootOK = true; };
+        groupmod = { rootOK = true; };
+        groupmems = { rootOK = true; };
+        groupdel = { rootOK = true; };
+        login = { startSession = true; allowNullPassword = true; showMotd = true; updateWtmp = true; };
+        chpasswd = { rootOK = true; };
+      };
+
+    security.wrappers = {
+      su        = mkSetuidRoot "${pkgs.shadow.su}/bin/su";
+      sg        = mkSetuidRoot "${pkgs.shadow.out}/bin/sg";
+      newgrp    = mkSetuidRoot "${pkgs.shadow.out}/bin/newgrp";
+      newuidmap = mkSetuidRoot "${pkgs.shadow.out}/bin/newuidmap";
+      newgidmap = mkSetuidRoot "${pkgs.shadow.out}/bin/newgidmap";
+    } // lib.optionalAttrs config.users.mutableUsers {
+      chsh   = mkSetuidRoot "${pkgs.shadow.out}/bin/chsh";
+      passwd = mkSetuidRoot "${pkgs.shadow.out}/bin/passwd";
+    };
+  };
+}
diff --git a/nixos/modules/programs/singularity.nix b/nixos/modules/programs/singularity.nix
new file mode 100644
index 00000000000..db935abe4bb
--- /dev/null
+++ b/nixos/modules/programs/singularity.nix
@@ -0,0 +1,34 @@
+{ config, pkgs, lib, ... }:
+
+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 {
+  options.programs.singularity = {
+    enable = mkEnableOption "Singularity";
+  };
+
+  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 -"
+      ];
+  };
+
+}
diff --git a/nixos/modules/programs/slock.nix b/nixos/modules/programs/slock.nix
new file mode 100644
index 00000000000..ce80fcc5d4a
--- /dev/null
+++ b/nixos/modules/programs/slock.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.slock;
+
+in
+{
+  options = {
+    programs.slock = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to install slock screen locker with setuid wrapper.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.slock ];
+    security.wrappers.slock =
+      { setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${pkgs.slock.out}/bin/slock";
+      };
+  };
+}
diff --git a/nixos/modules/programs/spacefm.nix b/nixos/modules/programs/spacefm.nix
new file mode 100644
index 00000000000..f71abcaa332
--- /dev/null
+++ b/nixos/modules/programs/spacefm.nix
@@ -0,0 +1,55 @@
+# Global configuration for spacefm.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.programs.spacefm;
+
+in
+{
+  ###### interface
+
+  options = {
+
+    programs.spacefm = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to install SpaceFM and create <filename>/etc/spacefm/spacefm.conf</filename>.
+        '';
+      };
+
+      settings = mkOption {
+        type = types.attrs;
+        default = {
+          tmp_dir = "/tmp";
+          terminal_su = "${pkgs.sudo}/bin/sudo";
+        };
+        defaultText = literalExpression ''
+          {
+            tmp_dir = "/tmp";
+            terminal_su = "''${pkgs.sudo}/bin/sudo";
+          }
+        '';
+        description = ''
+          The system-wide spacefm configuration.
+          Parameters to be written to <filename>/etc/spacefm/spacefm.conf</filename>.
+          Refer to the <link xlink:href="https://ignorantguru.github.io/spacefm/spacefm-manual-en.html#programfiles-etc">relevant entry</link> in the SpaceFM manual.
+        '';
+      };
+
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.spaceFM ];
+
+    environment.etc."spacefm/spacefm.conf".text =
+      concatStrings (mapAttrsToList (n: v: "${n}=${toString v}\n") cfg.settings);
+  };
+}
diff --git a/nixos/modules/programs/ssh.nix b/nixos/modules/programs/ssh.nix
new file mode 100644
index 00000000000..b31fce91524
--- /dev/null
+++ b/nixos/modules/programs/ssh.nix
@@ -0,0 +1,346 @@
+# Global configuration for the SSH client.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg  = config.programs.ssh;
+
+  askPassword = cfg.askPassword;
+
+  askPasswordWrapper = pkgs.writeScript "ssh-askpass-wrapper"
+    ''
+      #! ${pkgs.runtimeShell} -e
+      export DISPLAY="$(systemctl --user show-environment | ${pkgs.gnused}/bin/sed 's/^DISPLAY=\(.*\)/\1/; t; d')"
+      exec ${askPassword} "$@"
+    '';
+
+  knownHosts = attrValues cfg.knownHosts;
+
+  knownHostsText = (flip (concatMapStringsSep "\n") knownHosts
+    (h: assert h.hostNames != [];
+      optionalString h.certAuthority "@cert-authority " + concatStringsSep "," h.hostNames + " "
+      + (if h.publicKey != null then h.publicKey else readFile h.publicKeyFile)
+    )) + "\n";
+
+  knownHostsFiles = [ "/etc/ssh/ssh_known_hosts" "/etc/ssh/ssh_known_hosts2" ]
+    ++ map pkgs.copyPathToStore cfg.knownHostsFiles;
+
+in
+{
+  ###### interface
+
+  options = {
+
+    programs.ssh = {
+
+      enableAskPassword = mkOption {
+        type = types.bool;
+        default = config.services.xserver.enable;
+        defaultText = literalExpression "config.services.xserver.enable";
+        description = "Whether to configure SSH_ASKPASS in the environment.";
+      };
+
+      askPassword = mkOption {
+        type = types.str;
+        default = "${pkgs.x11_ssh_askpass}/libexec/x11-ssh-askpass";
+        defaultText = literalExpression ''"''${pkgs.x11_ssh_askpass}/libexec/x11-ssh-askpass"'';
+        description = "Program used by SSH to ask for passwords.";
+      };
+
+      forwardX11 = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to request X11 forwarding on outgoing connections by default.
+          This is useful for running graphical programs on the remote machine and have them display to your local X11 server.
+          Historically, this value has depended on the value used by the local sshd daemon, but there really isn't a relation between the two.
+          Note: there are some security risks to forwarding an X11 connection.
+          NixOS's X server is built with the SECURITY extension, which prevents some obvious attacks.
+          To enable or disable forwarding on a per-connection basis, see the -X and -x options to ssh.
+          The -Y option to ssh enables trusted forwarding, which bypasses the SECURITY extension.
+        '';
+      };
+
+      setXAuthLocation = mkOption {
+        type = types.bool;
+        description = ''
+          Whether to set the path to <command>xauth</command> for X11-forwarded connections.
+          This causes a dependency on X11 packages.
+        '';
+      };
+
+      pubkeyAcceptedKeyTypes = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "ssh-ed25519" "ssh-rsa" ];
+        description = ''
+          Specifies the key types that will be used for public key authentication.
+        '';
+      };
+
+      hostKeyAlgorithms = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "ssh-ed25519" "ssh-rsa" ];
+        description = ''
+          Specifies the host key algorithms that the client wants to use in order of preference.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration text prepended to <filename>ssh_config</filename>. Other generated
+          options will be added after a <code>Host *</code> pattern.
+          See <citerefentry><refentrytitle>ssh_config</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+          for help.
+        '';
+      };
+
+      startAgent = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to start the OpenSSH agent when you log in.  The OpenSSH agent
+          remembers private keys for you so that you don't have to type in
+          passphrases every time you make an SSH connection.  Use
+          <command>ssh-add</command> to add a key to the agent.
+        '';
+      };
+
+      agentTimeout = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "1h";
+        description = ''
+          How long to keep the private keys in memory. Use null to keep them forever.
+        '';
+      };
+
+      agentPKCS11Whitelist = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = literalExpression ''"''${pkgs.opensc}/lib/opensc-pkcs11.so"'';
+        description = ''
+          A pattern-list of acceptable paths for PKCS#11 shared libraries
+          that may be used with the -s option to ssh-add.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.openssh;
+        defaultText = literalExpression "pkgs.openssh";
+        description = ''
+          The package used for the openssh client and daemon.
+        '';
+      };
+
+      knownHosts = mkOption {
+        default = {};
+        type = types.attrsOf (types.submodule ({ name, config, options, ... }: {
+          options = {
+            certAuthority = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                This public key is an SSH certificate authority, rather than an
+                individual host's key.
+              '';
+            };
+            hostNames = mkOption {
+              type = types.listOf types.str;
+              default = [ name ] ++ config.extraHostNames;
+              defaultText = literalExpression "[ ${name} ] ++ config.${options.extraHostNames}";
+              description = ''
+                DEPRECATED, please use <literal>extraHostNames</literal>.
+                A list of host names and/or IP numbers used for accessing
+                the host's ssh service.
+              '';
+            };
+            extraHostNames = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = ''
+                A list of additional host names and/or IP numbers used for
+                accessing the host's ssh service.
+              '';
+            };
+            publicKey = mkOption {
+              default = null;
+              type = types.nullOr types.str;
+              example = "ecdsa-sha2-nistp521 AAAAE2VjZHN...UEPg==";
+              description = ''
+                The public key data for the host. You can fetch a public key
+                from a running SSH server with the <command>ssh-keyscan</command>
+                command. The public key should not include any host names, only
+                the key type and the key itself.
+              '';
+            };
+            publicKeyFile = mkOption {
+              default = null;
+              type = types.nullOr types.path;
+              description = ''
+                The path to the public key file for the host. The public
+                key file is read at build time and saved in the Nix store.
+                You can fetch a public key file from a running SSH server
+                with the <command>ssh-keyscan</command> command. The content
+                of the file should follow the same format as described for
+                the <literal>publicKey</literal> option. Only a single key
+                is supported. If a host has multiple keys, use
+                <option>programs.ssh.knownHostsFiles</option> instead.
+              '';
+            };
+          };
+        }));
+        description = ''
+          The set of system-wide known SSH hosts.
+        '';
+        example = literalExpression ''
+          {
+            myhost = {
+              extraHostNames = [ "myhost.mydomain.com" "10.10.1.4" ];
+              publicKeyFile = ./pubkeys/myhost_ssh_host_dsa_key.pub;
+            };
+            "myhost2.net".publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILIRuJ8p1Fi+m6WkHV0KWnRfpM1WxoW8XAS+XvsSKsTK";
+          }
+        '';
+      };
+
+      knownHostsFiles = mkOption {
+        default = [];
+        type = with types; listOf path;
+        description = ''
+          Files containing SSH host keys to set as global known hosts.
+          <literal>/etc/ssh/ssh_known_hosts</literal> (which is
+          generated by <option>programs.ssh.knownHosts</option>) and
+          <literal>/etc/ssh/ssh_known_hosts2</literal> are always
+          included.
+        '';
+        example = literalExpression ''
+          [
+            ./known_hosts
+            (writeText "github.keys" '''
+              github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+              github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
+              github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
+            ''')
+          ]
+        '';
+      };
+
+      kexAlgorithms = mkOption {
+        type = types.nullOr (types.listOf types.str);
+        default = null;
+        example = [ "curve25519-sha256@libssh.org" "diffie-hellman-group-exchange-sha256" ];
+        description = ''
+          Specifies the available KEX (Key Exchange) algorithms.
+        '';
+      };
+
+      ciphers = mkOption {
+        type = types.nullOr (types.listOf types.str);
+        default = null;
+        example = [ "chacha20-poly1305@openssh.com" "aes256-gcm@openssh.com" ];
+        description = ''
+          Specifies the ciphers allowed and their order of preference.
+        '';
+      };
+
+      macs = mkOption {
+        type = types.nullOr (types.listOf types.str);
+        default = null;
+        example = [ "hmac-sha2-512-etm@openssh.com" "hmac-sha1" ];
+        description = ''
+          Specifies the MAC (message authentication code) algorithms in order of preference. The MAC algorithm is used
+          for data integrity protection.
+        '';
+      };
+    };
+
+  };
+
+  config = {
+
+    programs.ssh.setXAuthLocation =
+      mkDefault (config.services.xserver.enable || config.programs.ssh.forwardX11 || config.services.openssh.forwardX11);
+
+    assertions =
+      [ { assertion = cfg.forwardX11 -> cfg.setXAuthLocation;
+          message = "cannot enable X11 forwarding without setting XAuth location";
+        }
+      ] ++ flip mapAttrsToList cfg.knownHosts (name: data: {
+        assertion = (data.publicKey == null && data.publicKeyFile != null) ||
+                    (data.publicKey != null && data.publicKeyFile == null);
+        message = "knownHost ${name} must contain either a publicKey or publicKeyFile";
+      });
+
+    warnings = mapAttrsToList (name: _: ''programs.ssh.knownHosts.${name}.hostNames is deprecated, use programs.ssh.knownHosts.${name}.extraHostNames'')
+      (filterAttrs (name: {hostNames, extraHostNames, ...}: hostNames != [ name ] ++ extraHostNames) cfg.knownHosts);
+
+    # SSH configuration. Slight duplication of the sshd_config
+    # generation in the sshd service.
+    environment.etc."ssh/ssh_config".text =
+      ''
+        # Custom options from `extraConfig`, to override generated options
+        ${cfg.extraConfig}
+
+        # Generated options from other settings
+        Host *
+        AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"}
+        GlobalKnownHostsFile ${concatStringsSep " " knownHostsFiles}
+
+        ${optionalString cfg.setXAuthLocation ''
+          XAuthLocation ${pkgs.xorg.xauth}/bin/xauth
+        ''}
+
+        ForwardX11 ${if cfg.forwardX11 then "yes" else "no"}
+
+        ${optionalString (cfg.pubkeyAcceptedKeyTypes != []) "PubkeyAcceptedKeyTypes ${concatStringsSep "," cfg.pubkeyAcceptedKeyTypes}"}
+        ${optionalString (cfg.hostKeyAlgorithms != []) "HostKeyAlgorithms ${concatStringsSep "," cfg.hostKeyAlgorithms}"}
+        ${optionalString (cfg.kexAlgorithms != null) "KexAlgorithms ${concatStringsSep "," cfg.kexAlgorithms}"}
+        ${optionalString (cfg.ciphers != null) "Ciphers ${concatStringsSep "," cfg.ciphers}"}
+        ${optionalString (cfg.macs != null) "MACs ${concatStringsSep "," cfg.macs}"}
+      '';
+
+    environment.etc."ssh/ssh_known_hosts".text = knownHostsText;
+
+    # FIXME: this should really be socket-activated for über-awesomeness.
+    systemd.user.services.ssh-agent = mkIf cfg.startAgent
+      { description = "SSH Agent";
+        wantedBy = [ "default.target" ];
+        unitConfig.ConditionUser = "!@system";
+        serviceConfig =
+          { ExecStartPre = "${pkgs.coreutils}/bin/rm -f %t/ssh-agent";
+            ExecStart =
+                "${cfg.package}/bin/ssh-agent " +
+                optionalString (cfg.agentTimeout != null) ("-t ${cfg.agentTimeout} ") +
+                optionalString (cfg.agentPKCS11Whitelist != null) ("-P ${cfg.agentPKCS11Whitelist} ") +
+                "-a %t/ssh-agent";
+            StandardOutput = "null";
+            Type = "forking";
+            Restart = "on-failure";
+            SuccessExitStatus = "0 2";
+          };
+        # Allow ssh-agent to ask for confirmation. This requires the
+        # unit to know about the user's $DISPLAY (via ‘systemctl
+        # import-environment’).
+        environment.SSH_ASKPASS = optionalString cfg.enableAskPassword askPasswordWrapper;
+        environment.DISPLAY = "fake"; # required to make ssh-agent start $SSH_ASKPASS
+      };
+
+    environment.extraInit = optionalString cfg.startAgent
+      ''
+        if [ -z "$SSH_AUTH_SOCK" -a -n "$XDG_RUNTIME_DIR" ]; then
+          export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent"
+        fi
+      '';
+
+    environment.variables.SSH_ASKPASS = optionalString cfg.enableAskPassword askPassword;
+
+  };
+}
diff --git a/nixos/modules/programs/ssmtp.nix b/nixos/modules/programs/ssmtp.nix
new file mode 100644
index 00000000000..b454bf35229
--- /dev/null
+++ b/nixos/modules/programs/ssmtp.nix
@@ -0,0 +1,190 @@
+# Configuration for `ssmtp', a trivial mail transfer agent that can
+# replace sendmail/postfix on simple systems.  It delivers email
+# directly to an SMTP server defined in its configuration file, without
+# queueing mail locally.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ssmtp;
+
+in
+{
+
+  imports = [
+    (mkRenamedOptionModule [ "networking" "defaultMailServer" "directDelivery" ] [ "services" "ssmtp" "enable" ])
+    (mkRenamedOptionModule [ "networking" "defaultMailServer" "hostName" ] [ "services" "ssmtp" "hostName" ])
+    (mkRenamedOptionModule [ "networking" "defaultMailServer" "domain" ] [ "services" "ssmtp" "domain" ])
+    (mkRenamedOptionModule [ "networking" "defaultMailServer" "root" ] [ "services" "ssmtp" "root" ])
+    (mkRenamedOptionModule [ "networking" "defaultMailServer" "useTLS" ] [ "services" "ssmtp" "useTLS" ])
+    (mkRenamedOptionModule [ "networking" "defaultMailServer" "useSTARTTLS" ] [ "services" "ssmtp" "useSTARTTLS" ])
+    (mkRenamedOptionModule [ "networking" "defaultMailServer" "authUser" ] [ "services" "ssmtp" "authUser" ])
+    (mkRenamedOptionModule [ "networking" "defaultMailServer" "authPassFile" ] [ "services" "ssmtp" "authPassFile" ])
+    (mkRenamedOptionModule [ "networking" "defaultMailServer" "setSendmail" ] [ "services" "ssmtp" "setSendmail" ])
+
+    (mkRemovedOptionModule [ "networking" "defaultMailServer" "authPass" ] "authPass has been removed since it leaks the clear-text password into the world-readable store. Use authPassFile instead and make sure it's not a store path")
+    (mkRemovedOptionModule [ "services" "ssmtp" "authPass" ] "authPass has been removed since it leaks the clear-text password into the world-readable store. Use authPassFile instead and make sure it's not a store path")
+  ];
+
+  options = {
+
+    services.ssmtp = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Use the trivial Mail Transfer Agent (MTA)
+          <command>ssmtp</command> package to allow programs to send
+          e-mail.  If you don't want to run a “real†MTA like
+          <command>sendmail</command> or <command>postfix</command> on
+          your machine, set this option to <literal>true</literal>, and
+          set the option
+          <option>services.ssmtp.hostName</option> to the
+          host name of your preferred mail server.
+        '';
+      };
+
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ bool str ]);
+        default = {};
+        description = ''
+          <citerefentry><refentrytitle>ssmtp</refentrytitle><manvolnum>5</manvolnum></citerefentry> configuration. Refer
+          to <link xlink:href="https://linux.die.net/man/5/ssmtp.conf"/> for details on supported values.
+        '';
+        example = literalExpression ''
+          {
+            Debug = true;
+            FromLineOverride = false;
+          }
+        '';
+      };
+
+      hostName = mkOption {
+        type = types.str;
+        example = "mail.example.org";
+        description = ''
+          The host name of the default mail server to use to deliver
+          e-mail. Can also contain a port number (ex: mail.example.org:587),
+          defaults to port 25 if no port is given.
+        '';
+      };
+
+      root = mkOption {
+        type = types.str;
+        default = "";
+        example = "root@example.org";
+        description = ''
+          The e-mail to which mail for users with UID &lt; 1000 is forwarded.
+        '';
+      };
+
+      domain = mkOption {
+        type = types.str;
+        default = "";
+        example = "example.org";
+        description = ''
+          The domain from which mail will appear to be sent.
+        '';
+      };
+
+      useTLS = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether TLS should be used to connect to the default mail
+          server.
+        '';
+      };
+
+      useSTARTTLS = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether the STARTTLS should be used to connect to the default
+          mail server.  (This is needed for TLS-capable mail servers
+          running on the default SMTP port 25.)
+        '';
+      };
+
+      authUser = mkOption {
+        type = types.str;
+        default = "";
+        example = "foo@example.org";
+        description = ''
+          Username used for SMTP auth. Leave blank to disable.
+        '';
+      };
+
+      authPassFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/run/keys/ssmtp-authpass";
+        description = ''
+          Path to a file that contains the password used for SMTP auth. The file
+          should not contain a trailing newline, if the password does not contain one
+          (e.g. use <command>echo -n "password" > file</command>).
+          This file should be readable by the users that need to execute ssmtp.
+        '';
+      };
+
+      setSendmail = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to set the system sendmail to ssmtp's.";
+      };
+
+    };
+
+  };
+
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = cfg.useSTARTTLS -> cfg.useTLS;
+        message = "services.ssmtp.useSTARTTLS has no effect without services.ssmtp.useTLS";
+      }
+    ];
+
+    services.ssmtp.settings = mkMerge [
+      ({
+        MailHub = cfg.hostName;
+        FromLineOverride = mkDefault true;
+        UseTLS = cfg.useTLS;
+        UseSTARTTLS = cfg.useSTARTTLS;
+      })
+      (mkIf (cfg.root != "") { root = cfg.root; })
+      (mkIf (cfg.domain != "") { rewriteDomain = cfg.domain; })
+      (mkIf (cfg.authUser != "") { AuthUser = cfg.authUser; })
+      (mkIf (cfg.authPassFile != null) { AuthPassFile = cfg.authPassFile; })
+    ];
+
+    # careful here: ssmtp REQUIRES all config lines to end with a newline char!
+    environment.etc."ssmtp/ssmtp.conf".text = with generators; toKeyValue {
+      mkKeyValue = mkKeyValueDefault {
+        mkValueString = value:
+          if value == true then "YES"
+          else if value == false then "NO"
+          else mkValueStringDefault {} value
+        ;
+      } "=";
+    } cfg.settings;
+
+    environment.systemPackages = [pkgs.ssmtp];
+
+    services.mail.sendmailSetuidWrapper = mkIf cfg.setSendmail {
+      program = "sendmail";
+      source = "${pkgs.ssmtp}/bin/sendmail";
+      setuid = false;
+      setgid = false;
+      owner = "root";
+      group = "root";
+    };
+
+  };
+
+}
diff --git a/nixos/modules/programs/starship.nix b/nixos/modules/programs/starship.nix
new file mode 100644
index 00000000000..83d2272003c
--- /dev/null
+++ b/nixos/modules/programs/starship.nix
@@ -0,0 +1,51 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.starship;
+
+  settingsFormat = pkgs.formats.toml { };
+
+  settingsFile = settingsFormat.generate "starship.toml" cfg.settings;
+
+in {
+  options.programs.starship = {
+    enable = mkEnableOption "the Starship shell prompt";
+
+    settings = mkOption {
+      inherit (settingsFormat) type;
+      default = { };
+      description = ''
+        Configuration included in <literal>starship.toml</literal>.
+
+        See https://starship.rs/config/#prompt for documentation.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    programs.bash.promptInit = ''
+      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 = ''
+      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 = ''
+      if [[ $TERM != "dumb" && (-z $INSIDE_EMACS || $INSIDE_EMACS == "vterm") ]]; then
+        export STARSHIP_CONFIG=${settingsFile}
+        eval "$(${pkgs.starship}/bin/starship init zsh)"
+      fi
+    '';
+  };
+
+  meta.maintainers = pkgs.starship.meta.maintainers;
+}
diff --git a/nixos/modules/programs/steam.nix b/nixos/modules/programs/steam.nix
new file mode 100644
index 00000000000..ff4deba2bf0
--- /dev/null
+++ b/nixos/modules/programs/steam.nix
@@ -0,0 +1,63 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.steam;
+
+  steam = pkgs.steam.override {
+    extraLibraries = pkgs: with config.hardware.opengl;
+      if pkgs.hostPlatform.is64bit
+      then [ package ] ++ extraPackages
+      else [ package32 ] ++ extraPackages32;
+  };
+in {
+  options.programs.steam = {
+    enable = mkEnableOption "steam";
+
+    remotePlay.openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Open ports in the firewall for Steam Remote Play.
+      '';
+    };
+
+    dedicatedServer.openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Open ports in the firewall for Source Dedicated Server.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    hardware.opengl = { # this fixes the "glXChooseVisual failed" bug, context: https://github.com/NixOS/nixpkgs/issues/47932
+      enable = true;
+      driSupport = true;
+      driSupport32Bit = true;
+    };
+
+    # optionally enable 32bit pulseaudio support if pulseaudio is enabled
+    hardware.pulseaudio.support32Bit = config.hardware.pulseaudio.enable;
+
+    hardware.steam-hardware.enable = true;
+
+    environment.systemPackages = [ steam steam.run ];
+
+    networking.firewall = lib.mkMerge [
+      (mkIf cfg.remotePlay.openFirewall {
+        allowedTCPPorts = [ 27036 ];
+        allowedUDPPortRanges = [ { from = 27031; to = 27036; } ];
+      })
+
+      (mkIf cfg.dedicatedServer.openFirewall {
+        allowedTCPPorts = [ 27015 ]; # SRCDS Rcon port
+        allowedUDPPorts = [ 27015 ]; # Gameplay traffic
+      })
+    ];
+  };
+
+  meta.maintainers = with maintainers; [ mkg20001 ];
+}
diff --git a/nixos/modules/programs/sway.nix b/nixos/modules/programs/sway.nix
new file mode 100644
index 00000000000..01b04728134
--- /dev/null
+++ b/nixos/modules/programs/sway.nix
@@ -0,0 +1,150 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.sway;
+
+  wrapperOptions = types.submodule {
+    options =
+      let
+        mkWrapperFeature  = default: description: mkOption {
+          type = types.bool;
+          inherit default;
+          example = !default;
+          description = "Whether to make use of the ${description}";
+        };
+      in {
+        base = mkWrapperFeature true ''
+          base wrapper to execute extra session commands and prepend a
+          dbus-run-session to the sway command.
+        '';
+        gtk = mkWrapperFeature false ''
+          wrapGAppsHook wrapper to execute sway with required environment
+          variables for GTK applications.
+        '';
+    };
+  };
+
+  swayPackage = pkgs.sway.override {
+    extraSessionCommands = cfg.extraSessionCommands;
+    extraOptions = cfg.extraOptions;
+    withBaseWrapper = cfg.wrapperFeatures.base;
+    withGtkWrapper = cfg.wrapperFeatures.gtk;
+    isNixOS = true;
+  };
+in {
+  options.programs.sway = {
+    enable = mkEnableOption ''
+      Sway, the i3-compatible tiling Wayland compositor. You can manually launch
+      Sway by executing "exec sway" on a TTY. Copy /etc/sway/config to
+      ~/.config/sway/config to modify the default configuration. See
+      <link xlink:href="https://github.com/swaywm/sway/wiki" /> and
+      "man 5 sway" for more information'';
+
+    wrapperFeatures = mkOption {
+      type = wrapperOptions;
+      default = { };
+      example = { gtk = true; };
+      description = ''
+        Attribute set of features to enable in the wrapper.
+      '';
+    };
+
+    extraSessionCommands = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        # SDL:
+        export SDL_VIDEODRIVER=wayland
+        # QT (needs qt5.qtwayland in systemPackages):
+        export QT_QPA_PLATFORM=wayland-egl
+        export QT_WAYLAND_DISABLE_WINDOWDECORATION="1"
+        # Fix for some Java AWT applications (e.g. Android Studio),
+        # use this if they aren't displayed properly:
+        export _JAVA_AWT_WM_NONREPARENTING=1
+      '';
+      description = ''
+        Shell commands executed just before Sway is started. See
+        <link xlink:href="https://github.com/swaywm/sway/wiki/Running-programs-natively-under-wayland" />
+        and <link xlink:href="https://github.com/swaywm/wlroots/blob/master/docs/env_vars.md" />
+        for some useful environment variables.
+      '';
+    };
+
+    extraOptions = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [
+        "--verbose"
+        "--debug"
+        "--unsupported-gpu"
+        "--my-next-gpu-wont-be-nvidia"
+      ];
+      description = ''
+        Command line arguments passed to launch Sway. Please DO NOT report
+        issues if you use an unsupported GPU (proprietary drivers).
+      '';
+    };
+
+    extraPackages = mkOption {
+      type = with types; listOf package;
+      default = with pkgs; [
+        swaylock swayidle foot dmenu
+      ];
+      defaultText = literalExpression ''
+        with pkgs; [ swaylock swayidle foot dmenu ];
+      '';
+      example = literalExpression ''
+        with pkgs; [
+          i3status i3status-rust
+          termite rofi light
+        ]
+      '';
+      description = ''
+        Extra packages to be installed system wide. See
+        <link xlink:href="https://github.com/swaywm/sway/wiki/Useful-add-ons-for-sway" /> and
+        <link xlink:href="https://github.com/swaywm/sway/wiki/i3-Migration-Guide#common-x11-apps-used-on-i3-with-wayland-alternatives" />
+        for a list of useful software.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.extraSessionCommands != "" -> cfg.wrapperFeatures.base;
+        message = ''
+          The extraSessionCommands for Sway will not be run if
+          wrapperFeatures.base is disabled.
+        '';
+      }
+    ];
+    environment = {
+      systemPackages = [ swayPackage ] ++ cfg.extraPackages;
+      # Needed for the default wallpaper:
+      pathsToLink = [ "/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
+        '';
+      };
+    };
+    security.polkit.enable = true;
+    security.pam.services.swaylock = {};
+    hardware.opengl.enable = mkDefault true;
+    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 ];
+    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 ];
+  };
+
+  meta.maintainers = with lib.maintainers; [ primeos colemickens ];
+}
diff --git a/nixos/modules/programs/sysdig.nix b/nixos/modules/programs/sysdig.nix
new file mode 100644
index 00000000000..fbbf2906556
--- /dev/null
+++ b/nixos/modules/programs/sysdig.nix
@@ -0,0 +1,14 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.sysdig;
+in {
+  options.programs.sysdig.enable = mkEnableOption "sysdig";
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.sysdig ];
+    boot.extraModulePackages = [ config.boot.kernelPackages.sysdig ];
+  };
+}
diff --git a/nixos/modules/programs/system-config-printer.nix b/nixos/modules/programs/system-config-printer.nix
new file mode 100644
index 00000000000..34592dd7064
--- /dev/null
+++ b/nixos/modules/programs/system-config-printer.nix
@@ -0,0 +1,32 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    programs.system-config-printer = {
+
+      enable = mkEnableOption "system-config-printer, a Graphical user interface for CUPS administration";
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.programs.system-config-printer.enable {
+
+    environment.systemPackages = [
+      pkgs.system-config-printer
+    ];
+
+    services.system-config-printer.enable = true;
+
+  };
+
+}
diff --git a/nixos/modules/programs/systemtap.nix b/nixos/modules/programs/systemtap.nix
new file mode 100644
index 00000000000..360e106678e
--- /dev/null
+++ b/nixos/modules/programs/systemtap.nix
@@ -0,0 +1,29 @@
+{ config, lib, ... }:
+
+with lib;
+
+let cfg = config.programs.systemtap;
+in {
+
+  options = {
+    programs.systemtap = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Install <command>systemtap</command> along with necessary kernel options.
+        '';
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    system.requiredKernelConfig = with config.lib.kernelConfig; [
+      (isYes "DEBUG")
+    ];
+    boot.kernel.features.debug = true;
+    environment.systemPackages = [
+      config.boot.kernelPackages.systemtap
+    ];
+  };
+
+}
diff --git a/nixos/modules/programs/thefuck.nix b/nixos/modules/programs/thefuck.nix
new file mode 100644
index 00000000000..b909916158d
--- /dev/null
+++ b/nixos/modules/programs/thefuck.nix
@@ -0,0 +1,39 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  prg = config.programs;
+  cfg = prg.thefuck;
+
+  initScript = ''
+    eval $(${pkgs.thefuck}/bin/thefuck --alias ${cfg.alias})
+  '';
+in
+  {
+    options = {
+      programs.thefuck = {
+        enable = mkEnableOption "thefuck";
+
+        alias = mkOption {
+          default = "fuck";
+          type = types.str;
+
+          description = ''
+            `thefuck` needs an alias to be configured.
+            The default value is `fuck`, but you can use anything else as well.
+          '';
+        };
+      };
+    };
+
+    config = mkIf cfg.enable {
+      environment.systemPackages = with pkgs; [ thefuck ];
+
+      programs.bash.interactiveShellInit = initScript;
+      programs.zsh.interactiveShellInit = mkIf prg.zsh.enable initScript;
+      programs.fish.interactiveShellInit = mkIf prg.fish.enable ''
+        ${pkgs.thefuck}/bin/thefuck --alias | source
+      '';
+    };
+  }
diff --git a/nixos/modules/programs/tmux.nix b/nixos/modules/programs/tmux.nix
new file mode 100644
index 00000000000..74b3fbd9ac0
--- /dev/null
+++ b/nixos/modules/programs/tmux.nix
@@ -0,0 +1,201 @@
+{ config, pkgs, lib, ... }:
+
+let
+  inherit (lib) mkOption mkIf types;
+
+  cfg = config.programs.tmux;
+
+  defaultKeyMode  = "emacs";
+  defaultResize   = 5;
+  defaultShortcut = "b";
+  defaultTerminal = "screen";
+
+  boolToStr = value: if value then "on" else "off";
+
+  tmuxConf = ''
+    set  -g default-terminal "${cfg.terminal}"
+    set  -g base-index      ${toString cfg.baseIndex}
+    setw -g pane-base-index ${toString cfg.baseIndex}
+
+    ${if cfg.newSession then "new-session" else ""}
+
+    ${if cfg.reverseSplit then ''
+    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 ''
+    bind h select-pane -L
+    bind j select-pane -D
+    bind k select-pane -U
+    bind l select-pane -R
+
+    bind -r H resize-pane -L ${toString cfg.resizeAmount}
+    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 ''
+    # 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"}
+    set  -s escape-time       ${toString cfg.escapeTime}
+    set  -g history-limit     ${toString cfg.historyLimit}
+
+    ${lib.optionalString (cfg.plugins != []) ''
+    # Run plugins
+    ${lib.concatMapStringsSep "\n" (x: "run-shell ${x.rtp}") cfg.plugins}
+
+    ''}
+
+    ${cfg.extraConfig}
+  '';
+
+in {
+  ###### interface
+
+  options = {
+    programs.tmux = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whenever to configure <command>tmux</command> system-wide.";
+        relatedPackages = [ "tmux" ];
+      };
+
+      aggressiveResize = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Resize the window to the size of the smallest session for which it is the current window.
+        '';
+      };
+
+      baseIndex = mkOption {
+        default = 0;
+        example = 1;
+        type = types.int;
+        description = "Base index for windows and panes.";
+      };
+
+      clock24 = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Use 24 hour clock.";
+      };
+
+      customPaneNavigationAndResize = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Override the hjkl and HJKL bindings for pane navigation and resizing in VI mode.";
+      };
+
+      escapeTime = mkOption {
+        default = 500;
+        example = 0;
+        type = types.int;
+        description = "Time in milliseconds for which tmux waits after an escape is input.";
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        description = ''
+          Additional contents of /etc/tmux.conf
+        '';
+        type = types.lines;
+      };
+
+      historyLimit = mkOption {
+        default = 2000;
+        example = 5000;
+        type = types.int;
+        description = "Maximum number of lines held in window history.";
+      };
+
+      keyMode = mkOption {
+        default = defaultKeyMode;
+        example = "vi";
+        type = types.enum [ "emacs" "vi" ];
+        description = "VI or Emacs style shortcuts.";
+      };
+
+      newSession = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Automatically spawn a session if trying to attach and none are running.";
+      };
+
+      reverseSplit = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Reverse the window split shortcuts.";
+      };
+
+      resizeAmount = mkOption {
+        default = defaultResize;
+        example = 10;
+        type = types.int;
+        description = "Number of lines/columns when resizing.";
+      };
+
+      shortcut = mkOption {
+        default = defaultShortcut;
+        example = "a";
+        type = types.str;
+        description = "Ctrl following by this key is used as the main shortcut.";
+      };
+
+      terminal = mkOption {
+        default = defaultTerminal;
+        example = "screen-256color";
+        type = types.str;
+        description = "Set the $TERM variable.";
+      };
+
+      secureSocket = mkOption {
+        default = true;
+        type = types.bool;
+        description = ''
+          Store tmux socket under /run, which is more secure than /tmp, but as a
+          downside it doesn't survive user logout.
+        '';
+      };
+
+      plugins = mkOption {
+        default = [];
+        type = types.listOf types.package;
+        description = "List of plugins to install.";
+        example = lib.literalExpression "[ pkgs.tmuxPlugins.nord ]";
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment = {
+      etc."tmux.conf".text = tmuxConf;
+
+      systemPackages = [ pkgs.tmux ] ++ cfg.plugins;
+
+      variables = {
+        TMUX_TMPDIR = lib.optional cfg.secureSocket ''''${XDG_RUNTIME_DIR:-"/run/user/$(id -u)"}'';
+      };
+    };
+  };
+
+  imports = [
+    (lib.mkRenamedOptionModule [ "programs" "tmux" "extraTmuxConf" ] [ "programs" "tmux" "extraConfig" ])
+  ];
+}
diff --git a/nixos/modules/programs/traceroute.nix b/nixos/modules/programs/traceroute.nix
new file mode 100644
index 00000000000..6e04057ac50
--- /dev/null
+++ b/nixos/modules/programs/traceroute.nix
@@ -0,0 +1,28 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.traceroute;
+in {
+  options = {
+    programs.traceroute = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to configure a setcap wrapper for traceroute.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    security.wrappers.traceroute = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_net_raw+p";
+      source = "${pkgs.traceroute}/bin/traceroute";
+    };
+  };
+}
diff --git a/nixos/modules/programs/tsm-client.nix b/nixos/modules/programs/tsm-client.nix
new file mode 100644
index 00000000000..28db9625387
--- /dev/null
+++ b/nixos/modules/programs/tsm-client.nix
@@ -0,0 +1,287 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+  inherit (builtins) length map;
+  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.types) addCheck attrsOf lines nonEmptyStr nullOr package path port str strMatching submodule;
+
+  # Checks if given list of strings contains unique
+  # elements when compared without considering case.
+  # Type: checkIUnique :: [string] -> bool
+  # Example: checkIUnique ["foo" "Foo"] => false
+  checkIUnique = lst:
+    let
+      lenUniq = l: length (lib.lists.unique l);
+    in
+      lenUniq lst == lenUniq (map toLower lst);
+
+  # TSM rejects servername strings longer than 64 chars.
+  servernameType = strMatching ".{1,64}";
+
+  serverOptions = { name, config, ... }: {
+    options.name = mkOption {
+      type = servernameType;
+      example = "mainTsmServer";
+      description = ''
+        Local name of the IBM TSM server,
+        must be uncapitalized and no longer than 64 chars.
+        The value will be used for the
+        <literal>server</literal>
+        directive in <filename>dsm.sys</filename>.
+      '';
+    };
+    options.server = mkOption {
+      type = nonEmptyStr;
+      example = "tsmserver.company.com";
+      description = ''
+        Host/domain name or IP address of the IBM TSM server.
+        The value will be used for the
+        <literal>tcpserveraddress</literal>
+        directive in <filename>dsm.sys</filename>.
+      '';
+    };
+    options.port = mkOption {
+      type = addCheck port (p: p<=32767);
+      default = 1500;  # official default
+      description = ''
+        TCP port of the IBM TSM server.
+        The value will be used for the
+        <literal>tcpport</literal>
+        directive in <filename>dsm.sys</filename>.
+        TSM does not support ports above 32767.
+      '';
+    };
+    options.node = mkOption {
+      type = nonEmptyStr;
+      example = "MY-TSM-NODE";
+      description = ''
+        Target node name on the IBM TSM server.
+        The value will be used for the
+        <literal>nodename</literal>
+        directive in <filename>dsm.sys</filename>.
+      '';
+    };
+    options.genPasswd = mkEnableOption ''
+      automatic client password generation.
+      This option influences the
+      <literal>passwordaccess</literal>
+      directive in <filename>dsm.sys</filename>.
+      The password will be stored in the directory
+      given by the option <option>passwdDir</option>.
+      <emphasis>Caution</emphasis>:
+      If this option is enabled and the server forces
+      to renew the password (e.g. on first connection),
+      a random password will be generated and stored
+    '';
+    options.passwdDir = mkOption {
+      type = path;
+      example = "/home/alice/tsm-password";
+      description = ''
+        Directory that holds the TSM
+        node's password information.
+        The value will be used for the
+        <literal>passworddir</literal>
+        directive in <filename>dsm.sys</filename>.
+      '';
+    };
+    options.includeExclude = mkOption {
+      type = lines;
+      default = "";
+      example = ''
+        exclude.dir     /nix/store
+        include.encrypt /home/.../*
+      '';
+      description = ''
+        <literal>include.*</literal> and
+        <literal>exclude.*</literal> directives to be
+        used when sending files to the IBM TSM server.
+        The lines will be written into a file that the
+        <literal>inclexcl</literal>
+        directive in <filename>dsm.sys</filename> points to.
+      '';
+    };
+    options.extraConfig = mkOption {
+      # TSM option keys are case insensitive;
+      # we have to ensure there are no keys that
+      # differ only by upper and lower case.
+      type = addCheck
+        (attrsOf (nullOr str))
+        (attrs: checkIUnique (attrNames attrs));
+      default = {};
+      example.compression = "yes";
+      example.passwordaccess = null;
+      description = ''
+        Additional key-value pairs for the server stanza.
+        Values must be strings, or <literal>null</literal>
+        for the key not to be used in the stanza
+        (e.g. to overrule values generated by other options).
+      '';
+    };
+    options.text = mkOption {
+      type = lines;
+      example = literalExpression
+        ''lib.modules.mkAfter "compression no"'';
+      description = ''
+        Additional text lines for the server stanza.
+        This option can be used if certion configuration keys
+        must be used multiple times or ordered in a certain way
+        as the <option>extraConfig</option> option can't
+        control the order of lines in the resulting stanza.
+        Note that the <literal>server</literal>
+        line at the beginning of the stanza is
+        not part of this option's value.
+      '';
+    };
+    options.stanza = mkOption {
+      type = str;
+      internal = true;
+      visible = false;
+      description = "Server stanza text generated from the options.";
+    };
+    config.name = mkDefault name;
+    # Client system-options file directives are explained here:
+    # https://www.ibm.com/docs/en/spectrum-protect/8.1.13?topic=commands-processing-options
+    config.extraConfig =
+      mapAttrs (lib.trivial.const mkDefault) (
+        {
+          commmethod = "v6tcpip";  # uses v4 or v6, based on dns lookup result
+          tcpserveraddress = config.server;
+          tcpport = builtins.toString config.port;
+          nodename = config.node;
+          passwordaccess = if config.genPasswd then "generate" else "prompt";
+          passworddir = ''"${config.passwdDir}"'';
+        } // optionalAttrs (config.includeExclude!="") {
+          inclexcl = ''"${pkgs.writeText "inclexcl.dsm.sys" config.includeExclude}"'';
+        }
+      );
+    config.text =
+      let
+        attrset = filterAttrs (k: v: v!=null) config.extraConfig;
+        mkLine = k: v: k + optionalString (v!="") "  ${v}";
+        lines = mapAttrsToList mkLine attrset;
+      in
+        concatStringsSep "\n" lines;
+    config.stanza = ''
+      server  ${config.name}
+      ${config.text}
+    '';
+  };
+
+  options.programs.tsmClient = {
+    enable = mkEnableOption ''
+      IBM Spectrum Protect (Tivoli Storage Manager, TSM)
+      client command line applications with a
+      client system-options file "dsm.sys"
+    '';
+    servers = mkOption {
+      type = attrsOf (submodule [ serverOptions ]);
+      default = {};
+      example.mainTsmServer = {
+        server = "tsmserver.company.com";
+        node = "MY-TSM-NODE";
+        extraConfig.compression = "yes";
+      };
+      description = ''
+        Server definitions ("stanzas")
+        for the client system-options file.
+      '';
+    };
+    defaultServername = mkOption {
+      type = nullOr servernameType;
+      default = null;
+      example = "mainTsmServer";
+      description = ''
+        If multiple server stanzas are declared with
+        <option>programs.tsmClient.servers</option>,
+        this option may be used to name a default
+        server stanza that IBM TSM uses in the absence of
+        a user-defined <filename>dsm.opt</filename> file.
+        This option translates to a
+        <literal>defaultserver</literal> configuration line.
+      '';
+    };
+    dsmSysText = mkOption {
+      type = lines;
+      readOnly = true;
+      description = ''
+        This configuration key contains the effective text
+        of the client system-options file "dsm.sys".
+        It should not be changed, but may be
+        used to feed the configuration into other
+        TSM-depending packages used on the system.
+      '';
+    };
+    package = mkOption {
+      type = package;
+      default = pkgs.tsm-client;
+      defaultText = literalExpression "pkgs.tsm-client";
+      example = literalExpression "pkgs.tsm-client-withGui";
+      description = ''
+        The TSM client derivation to be
+        added to the system environment.
+        It will called with <literal>.override</literal>
+        to add paths to the client system-options file.
+      '';
+    };
+    wrappedPackage = mkOption {
+      type = package;
+      readOnly = true;
+      description = ''
+        The TSM client derivation, wrapped with the path
+        to the client system-options file "dsm.sys".
+        This option is to provide the effective derivation
+        for other modules that want to call TSM executables.
+      '';
+    };
+  };
+
+  cfg = config.programs.tsmClient;
+
+  assertions = [
+    {
+      assertion = checkIUnique (mapAttrsToList (k: v: v.name) cfg.servers);
+      message = ''
+        TSM servernames contain duplicate name
+        (note that case doesn't matter!)
+      '';
+    }
+    {
+      assertion = (cfg.defaultServername!=null)->(hasAttr cfg.defaultServername cfg.servers);
+      message = "TSM defaultServername not found in list of servers";
+    }
+  ];
+
+  dsmSysText = ''
+    ****  IBM Spectrum Protect (Tivoli Storage Manager)
+    ****  client system-options file "dsm.sys".
+    ****  Do not edit!
+    ****  This file is generated by NixOS configuration.
+
+    ${optionalString (cfg.defaultServername!=null) "defaultserver  ${cfg.defaultServername}"}
+
+    ${concatStringsSep "\n" (mapAttrsToList (k: v: v.stanza) cfg.servers)}
+  '';
+
+in
+
+{
+
+  inherit options;
+
+  config = mkIf cfg.enable {
+    inherit assertions;
+    programs.tsmClient.dsmSysText = dsmSysText;
+    programs.tsmClient.wrappedPackage = cfg.package.override rec {
+      dsmSysCli = pkgs.writeText "dsm.sys" cfg.dsmSysText;
+      dsmSysApi = dsmSysCli;
+    };
+    environment.systemPackages = [ cfg.wrappedPackage ];
+  };
+
+  meta.maintainers = [ lib.maintainers.yarny ];
+
+}
diff --git a/nixos/modules/programs/turbovnc.nix b/nixos/modules/programs/turbovnc.nix
new file mode 100644
index 00000000000..e6f8836aa36
--- /dev/null
+++ b/nixos/modules/programs/turbovnc.nix
@@ -0,0 +1,54 @@
+# Global configuration for the SSH client.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.turbovnc;
+in
+{
+  options = {
+
+    programs.turbovnc = {
+
+      ensureHeadlessSoftwareOpenGL = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to set up NixOS such that TurboVNC's built-in software OpenGL
+          implementation works.
+
+          This will enable <option>hardware.opengl.enable</option> so that OpenGL
+          programs can find Mesa's llvmpipe drivers.
+
+          Setting this option to <code>false</code> does not mean that software
+          OpenGL won't work; it may still work depending on your system
+          configuration.
+
+          This option is also intended to generate warnings if you are using some
+          configuration that's incompatible with using headless software OpenGL
+          in TurboVNC.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.ensureHeadlessSoftwareOpenGL {
+
+    # TurboVNC has builtin support for Mesa llvmpipe's `swrast`
+    # software rendering to implemnt GLX (OpenGL on Xorg).
+    # However, just building TurboVNC with support for that is not enough
+    # (it only takes care of the X server side part of OpenGL);
+    # the indiviudual applications (e.g. `glxgears`) also need to directly load
+    # the OpenGL libs.
+    # Thus, this creates `/run/opengl-driver` populated by Mesa so that the applications
+    # can find the llvmpipe `swrast.so` software rendering DRI lib via `libglvnd`.
+    # This comment exists to explain why `hardware.` is involved,
+    # even though 100% software rendering is used.
+    hardware.opengl.enable = true;
+
+  };
+}
diff --git a/nixos/modules/programs/udevil.nix b/nixos/modules/programs/udevil.nix
new file mode 100644
index 00000000000..0dc08c435df
--- /dev/null
+++ b/nixos/modules/programs/udevil.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.udevil;
+
+in {
+  options.programs.udevil.enable = mkEnableOption "udevil";
+
+  config = mkIf cfg.enable {
+    security.wrappers.udevil =
+      { setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${lib.getBin pkgs.udevil}/bin/udevil";
+      };
+  };
+}
diff --git a/nixos/modules/programs/usbtop.nix b/nixos/modules/programs/usbtop.nix
new file mode 100644
index 00000000000..c1b6ee38caa
--- /dev/null
+++ b/nixos/modules/programs/usbtop.nix
@@ -0,0 +1,21 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.usbtop;
+in {
+  options = {
+    programs.usbtop.enable = mkEnableOption "usbtop and required kernel module";
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [
+      usbtop
+    ];
+
+    boot.kernelModules = [
+      "usbmon"
+    ];
+  };
+}
diff --git a/nixos/modules/programs/vim.nix b/nixos/modules/programs/vim.nix
new file mode 100644
index 00000000000..1695bc99473
--- /dev/null
+++ b/nixos/modules/programs/vim.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.vim;
+in {
+  options.programs.vim = {
+    defaultEditor = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        When enabled, installs vim and configures vim to be the default editor
+        using the EDITOR environment variable.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.vim;
+      defaultText = literalExpression "pkgs.vim";
+      example = literalExpression "pkgs.vimHugeX";
+      description = ''
+        vim package to use.
+      '';
+    };
+  };
+
+  config = mkIf cfg.defaultEditor {
+    environment.systemPackages = [ cfg.package ];
+    environment.variables = { EDITOR = mkOverride 900 "vim"; };
+  };
+}
diff --git a/nixos/modules/programs/virtualbox.nix b/nixos/modules/programs/virtualbox.nix
new file mode 100644
index 00000000000..be96cf23b39
--- /dev/null
+++ b/nixos/modules/programs/virtualbox.nix
@@ -0,0 +1,8 @@
+let
+  msg = "Importing <nixpkgs/nixos/modules/programs/virtualbox.nix> is "
+      + "deprecated, please use `virtualisation.virtualbox.host.enable = true' "
+      + "instead.";
+in {
+  config.warnings = [ msg ];
+  config.virtualisation.virtualbox.host.enable = true;
+}
diff --git a/nixos/modules/programs/wavemon.nix b/nixos/modules/programs/wavemon.nix
new file mode 100644
index 00000000000..e5ccacba75d
--- /dev/null
+++ b/nixos/modules/programs/wavemon.nix
@@ -0,0 +1,30 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.wavemon;
+in {
+  options = {
+    programs.wavemon = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to add wavemon to the global environment and configure a
+          setcap wrapper for it.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ wavemon ];
+    security.wrappers.wavemon = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_net_admin+ep";
+      source = "${pkgs.wavemon}/bin/wavemon";
+    };
+  };
+}
diff --git a/nixos/modules/programs/waybar.nix b/nixos/modules/programs/waybar.nix
new file mode 100644
index 00000000000..22530e6c7d4
--- /dev/null
+++ b/nixos/modules/programs/waybar.nix
@@ -0,0 +1,20 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+{
+  options.programs.waybar = {
+    enable = mkEnableOption "waybar";
+  };
+
+  config = mkIf config.programs.waybar.enable {
+    systemd.user.services.waybar = {
+      description = "Waybar as systemd service";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+      script = "${pkgs.waybar}/bin/waybar";
+    };
+  };
+
+  meta.maintainers = [ maintainers.FlorianFranzen ];
+}
diff --git a/nixos/modules/programs/weylus.nix b/nixos/modules/programs/weylus.nix
new file mode 100644
index 00000000000..ea92c77e7c3
--- /dev/null
+++ b/nixos/modules/programs/weylus.nix
@@ -0,0 +1,47 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.weylus;
+in
+{
+  options.programs.weylus = with types; {
+    enable = mkEnableOption "weylus";
+
+    openFirewall = mkOption {
+      type = bool;
+      default = false;
+      description = ''
+        Open ports needed for the functionality of the program.
+      '';
+    };
+
+     users = mkOption {
+      type = listOf str;
+      default = [ ];
+      description = ''
+        To enable stylus and multi-touch support, the user you're going to use must be added to this list.
+        These users can synthesize input events system-wide, even when another user is logged in - untrusted users should not be added.
+      '';
+    };
+
+    package = mkOption {
+      type = package;
+      default = pkgs.weylus;
+      defaultText = "pkgs.weylus";
+      description = "Weylus package to install.";
+    };
+  };
+  config = mkIf cfg.enable {
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 1701 9001 ];
+    };
+
+    hardware.uinput.enable = true;
+
+    users.groups.uinput.members = cfg.users;
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/programs/wireshark.nix b/nixos/modules/programs/wireshark.nix
new file mode 100644
index 00000000000..f7b0727cb2b
--- /dev/null
+++ b/nixos/modules/programs/wireshark.nix
@@ -0,0 +1,42 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.wireshark;
+  wireshark = cfg.package;
+in {
+  options = {
+    programs.wireshark = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to add Wireshark to the global environment and configure a
+          setcap wrapper for 'dumpcap' for users in the 'wireshark' group.
+        '';
+      };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.wireshark-cli;
+        defaultText = literalExpression "pkgs.wireshark-cli";
+        description = ''
+          Which Wireshark package to install in the global environment.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ wireshark ];
+    users.groups.wireshark = {};
+
+    security.wrappers.dumpcap = {
+      source = "${wireshark}/bin/dumpcap";
+      capabilities = "cap_net_raw+p";
+      owner = "root";
+      group = "wireshark";
+      permissions = "u+rx,g+x";
+    };
+  };
+}
diff --git a/nixos/modules/programs/wshowkeys.nix b/nixos/modules/programs/wshowkeys.nix
new file mode 100644
index 00000000000..f7b71d2bb0c
--- /dev/null
+++ b/nixos/modules/programs/wshowkeys.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.wshowkeys;
+in {
+  meta.maintainers = with maintainers; [ primeos ];
+
+  options = {
+    programs.wshowkeys = {
+      enable = mkEnableOption ''
+        wshowkeys (displays keypresses on screen on supported Wayland
+        compositors). It requires root permissions to read input events, but
+        these permissions are dropped after startup'';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    security.wrappers.wshowkeys =
+      { setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${pkgs.wshowkeys}/bin/wshowkeys";
+      };
+  };
+}
diff --git a/nixos/modules/programs/xfs_quota.nix b/nixos/modules/programs/xfs_quota.nix
new file mode 100644
index 00000000000..c03e59a5b4a
--- /dev/null
+++ b/nixos/modules/programs/xfs_quota.nix
@@ -0,0 +1,110 @@
+# Configuration for the xfs_quota command
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.programs.xfs_quota;
+
+  limitOptions = opts: concatStringsSep " " [
+    (optionalString (opts.sizeSoftLimit != null) "bsoft=${opts.sizeSoftLimit}")
+    (optionalString (opts.sizeHardLimit != null) "bhard=${opts.sizeHardLimit}")
+  ];
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    programs.xfs_quota = {
+      projects = mkOption {
+        default = {};
+        type = types.attrsOf (types.submodule {
+          options = {
+            id = mkOption {
+              type = types.int;
+              description = "Project ID.";
+            };
+
+            fileSystem = mkOption {
+              type = types.str;
+              description = "XFS filesystem hosting the xfs_quota project.";
+              default = "/";
+            };
+
+            path = mkOption {
+              type = types.str;
+              description = "Project directory.";
+            };
+
+            sizeSoftLimit = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "30g";
+              description = "Soft limit of the project size";
+            };
+
+            sizeHardLimit = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "50g";
+              description = "Hard limit of the project size.";
+            };
+          };
+        });
+
+        description = "Setup of xfs_quota projects. Make sure the filesystem is mounted with the pquota option.";
+
+        example = {
+          projname = {
+            id = 50;
+            path = "/xfsprojects/projname";
+            sizeHardLimit = "50g";
+          };
+        };
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf (cfg.projects != {}) {
+
+    environment.etc.projects.source = pkgs.writeText "etc-project"
+      (concatStringsSep "\n" (mapAttrsToList
+        (name: opts: "${toString opts.id}:${opts.path}") cfg.projects));
+
+    environment.etc.projid.source = pkgs.writeText "etc-projid"
+      (concatStringsSep "\n" (mapAttrsToList
+        (name: opts: "${name}:${toString opts.id}") cfg.projects));
+
+    systemd.services = mapAttrs' (name: opts:
+      nameValuePair "xfs_quota-${name}" {
+        description = "Setup xfs_quota for project ${name}";
+        script = ''
+          ${pkgs.xfsprogs.bin}/bin/xfs_quota -x -c 'project -s ${name}' ${opts.fileSystem}
+          ${pkgs.xfsprogs.bin}/bin/xfs_quota -x -c 'limit -p ${limitOptions opts} ${name}' ${opts.fileSystem}
+        '';
+
+        wantedBy = [ "multi-user.target" ];
+        after = [ ((replaceChars [ "/" ] [ "-" ] opts.fileSystem) + ".mount") ];
+
+        restartTriggers = [ config.environment.etc.projects.source ];
+
+        serviceConfig = {
+          Type = "oneshot";
+          RemainAfterExit = true;
+        };
+      }
+    ) cfg.projects;
+
+  };
+
+}
diff --git a/nixos/modules/programs/xonsh.nix b/nixos/modules/programs/xonsh.nix
new file mode 100644
index 00000000000..6e40db51cdb
--- /dev/null
+++ b/nixos/modules/programs/xonsh.nix
@@ -0,0 +1,86 @@
+# This module defines global configuration for the xonsh.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.programs.xonsh;
+
+in
+
+{
+
+  options = {
+
+    programs.xonsh = {
+
+      enable = mkOption {
+        default = false;
+        description = ''
+          Whether to configure xonsh as an interactive shell.
+        '';
+        type = types.bool;
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.xonsh;
+        defaultText = literalExpression "pkgs.xonsh";
+        example = literalExpression "pkgs.xonsh.override { configFile = \"/path/to/xonshrc\"; }";
+        description = ''
+          xonsh package to use.
+        '';
+      };
+
+      config = mkOption {
+        default = "";
+        description = "Control file to customize your shell behavior.";
+        type = types.lines;
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.etc.xonshrc.text = ''
+      # /etc/xonshrc: DO NOT EDIT -- this file has been generated automatically.
+
+
+      if not ''${...}.get('__NIXOS_SET_ENVIRONMENT_DONE'):
+          # The NixOS environment and thereby also $PATH
+          # haven't been fully set up at this point. But
+          # `source-bash` below requires `bash` to be on $PATH,
+          # so add an entry with bash's location:
+          $PATH.add('${pkgs.bash}/bin')
+
+          # Stash xonsh's ls alias, so that we don't get a collision
+          # with Bash's ls alias from environment.shellAliases:
+          _ls_alias = aliases.pop('ls', None)
+
+          # Source the NixOS environment config.
+          source-bash "${config.system.build.setEnvironment}"
+
+          # Restore xonsh's ls alias, overriding that from Bash (if any).
+          if _ls_alias is not None:
+              aliases['ls'] = _ls_alias
+          del _ls_alias
+
+
+      ${cfg.config}
+    '';
+
+    environment.systemPackages = [ cfg.package ];
+
+    environment.shells =
+      [ "/run/current-system/sw/bin/xonsh"
+        "${cfg.package}/bin/xonsh"
+      ];
+
+  };
+
+}
+
diff --git a/nixos/modules/programs/xss-lock.nix b/nixos/modules/programs/xss-lock.nix
new file mode 100644
index 00000000000..aba76133e5e
--- /dev/null
+++ b/nixos/modules/programs/xss-lock.nix
@@ -0,0 +1,45 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.xss-lock;
+in
+{
+  options.programs.xss-lock = {
+    enable = mkEnableOption "xss-lock";
+
+    lockerCommand = mkOption {
+      default = "${pkgs.i3lock}/bin/i3lock";
+      defaultText = literalExpression ''"''${pkgs.i3lock}/bin/i3lock"'';
+      example = literalExpression ''"''${pkgs.i3lock-fancy}/bin/i3lock-fancy"'';
+      type = types.separatedString " ";
+      description = "Locker to be used with xsslock";
+    };
+
+    extraOptions = mkOption {
+      default = [ ];
+      example = [ "--ignore-sleep" ];
+      type = types.listOf types.str;
+      description = ''
+        Additional command-line arguments to pass to
+        <command>xss-lock</command>.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.xss-lock = {
+      description = "XSS Lock Daemon";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+      serviceConfig.ExecStart = with lib;
+        strings.concatStringsSep " " ([
+            "${pkgs.xss-lock}/bin/xss-lock" "--session \${XDG_SESSION_ID}"
+          ] ++ (map escapeShellArg cfg.extraOptions) ++ [
+            "--"
+            cfg.lockerCommand
+        ]);
+    };
+  };
+}
diff --git a/nixos/modules/programs/xwayland.nix b/nixos/modules/programs/xwayland.nix
new file mode 100644
index 00000000000..3a8080fa4c4
--- /dev/null
+++ b/nixos/modules/programs/xwayland.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.xwayland;
+
+in
+
+{
+  options.programs.xwayland = {
+
+    enable = mkEnableOption "Xwayland (an X server for interfacing X11 apps with the Wayland protocol)";
+
+    defaultFontPath = mkOption {
+      type = types.str;
+      default = optionalString config.fonts.fontDir.enable
+        "/run/current-system/sw/share/X11/fonts";
+      defaultText = literalExpression ''
+        optionalString config.fonts.fontDir.enable "/run/current-system/sw/share/X11/fonts"
+      '';
+      description = ''
+        Default font path. Setting this option causes Xwayland to be rebuilt.
+      '';
+    };
+
+    package = mkOption {
+      type = types.path;
+      default = pkgs.xwayland.override (oldArgs: {
+        inherit (cfg) defaultFontPath;
+      });
+      defaultText = literalExpression ''
+        pkgs.xwayland.override (oldArgs: {
+          inherit (config.programs.xwayland) defaultFontPath;
+        })
+      '';
+      description = "The Xwayland package to use.";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    # Needed by some applications for fonts and default settings
+    environment.pathsToLink = [ "/share/X11" ];
+
+    environment.systemPackages = [ cfg.package ];
+
+  };
+}
diff --git a/nixos/modules/programs/yabar.nix b/nixos/modules/programs/yabar.nix
new file mode 100644
index 00000000000..a8fac41e899
--- /dev/null
+++ b/nixos/modules/programs/yabar.nix
@@ -0,0 +1,163 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.yabar;
+
+  mapExtra = v: lib.concatStringsSep "\n" (mapAttrsToList (
+    key: val: "${key} = ${if (isString val) then "\"${val}\"" else "${builtins.toString val}"};"
+  ) v);
+
+  listKeys = r: concatStringsSep "," (map (n: "\"${n}\"") (attrNames r));
+
+  configFile = let
+    bars = mapAttrsToList (
+      name: cfg: ''
+        ${name}: {
+          font: "${cfg.font}";
+          position: "${cfg.position}";
+
+          ${mapExtra cfg.extra}
+
+          block-list: [${listKeys cfg.indicators}]
+
+          ${concatStringsSep "\n" (mapAttrsToList (
+            name: cfg: ''
+              ${name}: {
+                exec: "${cfg.exec}";
+                align: "${cfg.align}";
+                ${mapExtra cfg.extra}
+              };
+            ''
+          ) cfg.indicators)}
+        };
+      ''
+    ) cfg.bars;
+  in pkgs.writeText "yabar.conf" ''
+    bar-list = [${listKeys cfg.bars}];
+    ${concatStringsSep "\n" bars}
+  '';
+in
+  {
+    options.programs.yabar = {
+      enable = mkEnableOption "yabar";
+
+      package = mkOption {
+        default = pkgs.yabar-unstable;
+        defaultText = literalExpression "pkgs.yabar-unstable";
+        example = literalExpression "pkgs.yabar";
+        type = types.package;
+
+        # `yabar-stable` segfaults under certain conditions.
+        apply = x: if x == pkgs.yabar-unstable then x else flip warn x ''
+          It's not recommended to use `yabar' with `programs.yabar', the (old) stable release
+          tends to segfault under certain circumstances:
+
+          * https://github.com/geommer/yabar/issues/86
+          * https://github.com/geommer/yabar/issues/68
+          * https://github.com/geommer/yabar/issues/143
+
+          Most of them don't occur on master anymore, until a new release is published, it's recommended
+          to use `yabar-unstable'.
+        '';
+
+        description = ''
+          The package which contains the `yabar` binary.
+
+          Nixpkgs provides the `yabar` and `yabar-unstable`
+          derivations since 18.03, so it's possible to choose.
+        '';
+      };
+
+      bars = mkOption {
+        default = {};
+        type = types.attrsOf(types.submodule {
+          options = {
+            font = mkOption {
+              default = "sans bold 9";
+              example = "Droid Sans, FontAwesome Bold 9";
+              type = types.str;
+
+              description = ''
+                The font that will be used to draw the status bar.
+              '';
+            };
+
+            position = mkOption {
+              default = "top";
+              example = "bottom";
+              type = types.enum [ "top" "bottom" ];
+
+              description = ''
+                The position where the bar will be rendered.
+              '';
+            };
+
+            extra = mkOption {
+              default = {};
+              type = types.attrsOf types.str;
+
+              description = ''
+                An attribute set which contains further attributes of a bar.
+              '';
+            };
+
+            indicators = mkOption {
+              default = {};
+              type = types.attrsOf(types.submodule {
+                options.exec = mkOption {
+                  example = "YABAR_DATE";
+                  type = types.str;
+                  description = ''
+                     The type of the indicator to be executed.
+                  '';
+                };
+
+                options.align = mkOption {
+                  default = "left";
+                  example = "right";
+                  type = types.enum [ "left" "center" "right" ];
+
+                  description = ''
+                    Whether to align the indicator at the left or right of the bar.
+                  '';
+                };
+
+                options.extra = mkOption {
+                  default = {};
+                  type = types.attrsOf (types.either types.str types.int);
+
+                  description = ''
+                    An attribute set which contains further attributes of a indicator.
+                  '';
+                };
+              });
+
+              description = ''
+                Indicators that should be rendered by yabar.
+              '';
+            };
+          };
+        });
+
+        description = ''
+          List of bars that should be rendered by yabar.
+        '';
+      };
+    };
+
+    config = mkIf cfg.enable {
+      systemd.user.services.yabar = {
+        description = "yabar service";
+        wantedBy = [ "graphical-session.target" ];
+        partOf = [ "graphical-session.target" ];
+
+        script = ''
+          ${cfg.package}/bin/yabar -c ${configFile}
+        '';
+
+        serviceConfig.Restart = "always";
+      };
+    };
+  }
diff --git a/nixos/modules/programs/zmap.nix b/nixos/modules/programs/zmap.nix
new file mode 100644
index 00000000000..2e27fce4d7c
--- /dev/null
+++ b/nixos/modules/programs/zmap.nix
@@ -0,0 +1,18 @@
+{ pkgs, config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.zmap;
+in {
+  options.programs.zmap = {
+    enable = mkEnableOption "ZMap";
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.zmap ];
+
+    environment.etc."zmap/blacklist.conf".source = "${pkgs.zmap}/etc/zmap/blacklist.conf";
+    environment.etc."zmap/zmap.conf".source = "${pkgs.zmap}/etc/zmap.conf";
+  };
+}
diff --git a/nixos/modules/programs/zsh/oh-my-zsh.nix b/nixos/modules/programs/zsh/oh-my-zsh.nix
new file mode 100644
index 00000000000..9d7622bd328
--- /dev/null
+++ b/nixos/modules/programs/zsh/oh-my-zsh.nix
@@ -0,0 +1,146 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.programs.zsh.ohMyZsh;
+
+  mkLinkFarmEntry = name: dir:
+    let
+      env = pkgs.buildEnv {
+        name = "zsh-${name}-env";
+        paths = cfg.customPkgs;
+        pathsToLink = "/share/zsh/${dir}";
+      };
+    in
+      { inherit name; path = "${env}/share/zsh/${dir}"; };
+
+  mkLinkFarmEntry' = name: mkLinkFarmEntry name name;
+
+  custom =
+    if cfg.custom != null then cfg.custom
+    else if length cfg.customPkgs == 0 then null
+    else pkgs.linkFarm "oh-my-zsh-custom" [
+      (mkLinkFarmEntry' "themes")
+      (mkLinkFarmEntry "completions" "site-functions")
+      (mkLinkFarmEntry' "plugins")
+    ];
+
+in
+  {
+    imports = [
+      (mkRenamedOptionModule [ "programs" "zsh" "oh-my-zsh" "enable" ] [ "programs" "zsh" "ohMyZsh" "enable" ])
+      (mkRenamedOptionModule [ "programs" "zsh" "oh-my-zsh" "theme" ] [ "programs" "zsh" "ohMyZsh" "theme" ])
+      (mkRenamedOptionModule [ "programs" "zsh" "oh-my-zsh" "custom" ] [ "programs" "zsh" "ohMyZsh" "custom" ])
+      (mkRenamedOptionModule [ "programs" "zsh" "oh-my-zsh" "plugins" ] [ "programs" "zsh" "ohMyZsh" "plugins" ])
+    ];
+
+    options = {
+      programs.zsh.ohMyZsh = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Enable oh-my-zsh.
+          '';
+        };
+
+        package = mkOption {
+          default = pkgs.oh-my-zsh;
+          defaultText = literalExpression "pkgs.oh-my-zsh";
+          description = ''
+            Package to install for `oh-my-zsh` usage.
+          '';
+
+          type = types.package;
+        };
+
+        plugins = mkOption {
+          default = [];
+          type = types.listOf(types.str);
+          description = ''
+            List of oh-my-zsh plugins
+          '';
+        };
+
+        custom = mkOption {
+          default = null;
+          type = with types; nullOr str;
+          description = ''
+            Path to a custom oh-my-zsh package to override config of oh-my-zsh.
+            (Can't be used along with `customPkgs`).
+          '';
+        };
+
+        customPkgs = mkOption {
+          default = [];
+          type = types.listOf types.package;
+          description = ''
+            List of custom packages that should be loaded into `oh-my-zsh`.
+          '';
+        };
+
+        theme = mkOption {
+          default = "";
+          type = types.str;
+          description = ''
+            Name of the theme to be used by oh-my-zsh.
+          '';
+        };
+
+        cacheDir = mkOption {
+          default = "$HOME/.cache/oh-my-zsh";
+          type = types.str;
+          description = ''
+            Cache directory to be used by `oh-my-zsh`.
+            Without this option it would default to the read-only nix store.
+          '';
+        };
+      };
+    };
+
+    config = mkIf cfg.enable {
+
+      # Prevent zsh from overwriting oh-my-zsh's prompt
+      programs.zsh.promptInit = mkDefault "";
+
+      environment.systemPackages = [ cfg.package ];
+
+      programs.zsh.interactiveShellInit = ''
+        # oh-my-zsh configuration generated by NixOS
+        export ZSH=${cfg.package}/share/oh-my-zsh
+
+        ${optionalString (length(cfg.plugins) > 0)
+          "plugins=(${concatStringsSep " " cfg.plugins})"
+        }
+
+        ${optionalString (custom != null)
+          "ZSH_CUSTOM=\"${custom}\""
+        }
+
+        ${optionalString (stringLength(cfg.theme) > 0)
+          "ZSH_THEME=\"${cfg.theme}\""
+        }
+
+        ${optionalString (cfg.cacheDir != null) ''
+          if [[ ! -d "${cfg.cacheDir}" ]]; then
+            mkdir -p "${cfg.cacheDir}"
+          fi
+          ZSH_CACHE_DIR=${cfg.cacheDir}
+        ''}
+
+        source $ZSH/oh-my-zsh.sh
+      '';
+
+      assertions = [
+        {
+          assertion = cfg.custom != null -> cfg.customPkgs == [];
+          message = "If `cfg.custom` is set for `ZSH_CUSTOM`, `customPkgs` can't be used!";
+        }
+      ];
+
+    };
+
+    meta.doc = ./oh-my-zsh.xml;
+  }
diff --git a/nixos/modules/programs/zsh/oh-my-zsh.xml b/nixos/modules/programs/zsh/oh-my-zsh.xml
new file mode 100644
index 00000000000..14a7228ad9b
--- /dev/null
+++ b/nixos/modules/programs/zsh/oh-my-zsh.xml
@@ -0,0 +1,155 @@
+<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/zinputrc b/nixos/modules/programs/zsh/zinputrc
new file mode 100644
index 00000000000..6121f3e21f1
--- /dev/null
+++ b/nixos/modules/programs/zsh/zinputrc
@@ -0,0 +1,42 @@
+# Stolen from ArchWiki
+
+# create a zkbd compatible hash;
+# to add other keys to this hash, see: man 5 terminfo
+typeset -A key
+
+key[Home]=${terminfo[khome]}
+
+key[End]=${terminfo[kend]}
+key[Insert]=${terminfo[kich1]}
+key[Delete]=${terminfo[kdch1]}
+key[Up]=${terminfo[kcuu1]}
+key[Down]=${terminfo[kcud1]}
+key[Left]=${terminfo[kcub1]}
+key[Right]=${terminfo[kcuf1]}
+key[PageUp]=${terminfo[kpp]}
+key[PageDown]=${terminfo[knp]}
+
+# setup key accordingly
+[[ -n "${key[Home]}"     ]]  && bindkey  "${key[Home]}"     beginning-of-line
+[[ -n "${key[End]}"      ]]  && bindkey  "${key[End]}"      end-of-line
+[[ -n "${key[Insert]}"   ]]  && bindkey  "${key[Insert]}"   overwrite-mode
+[[ -n "${key[Delete]}"   ]]  && bindkey  "${key[Delete]}"   delete-char
+[[ -n "${key[Up]}"       ]]  && bindkey  "${key[Up]}"       up-line-or-history
+[[ -n "${key[Down]}"     ]]  && bindkey  "${key[Down]}"     down-line-or-history
+[[ -n "${key[Left]}"     ]]  && bindkey  "${key[Left]}"     backward-char
+[[ -n "${key[Right]}"    ]]  && bindkey  "${key[Right]}"    forward-char
+[[ -n "${key[PageUp]}"   ]]  && bindkey  "${key[PageUp]}"   beginning-of-buffer-or-history
+[[ -n "${key[PageDown]}" ]]  && bindkey  "${key[PageDown]}" end-of-buffer-or-history
+
+# Finally, make sure the terminal is in application mode, when zle is
+# active. Only then are the values from $terminfo valid.
+if (( ${+terminfo[smkx]} )) && (( ${+terminfo[rmkx]} )); then
+    function zle-line-init () {
+        printf '%s' "${terminfo[smkx]}"
+    }
+    function zle-line-finish () {
+        printf '%s' "${terminfo[rmkx]}"
+    }
+    zle -N zle-line-init
+    zle -N zle-line-finish
+fi
diff --git a/nixos/modules/programs/zsh/zsh-autoenv.nix b/nixos/modules/programs/zsh/zsh-autoenv.nix
new file mode 100644
index 00000000000..62f497a45dd
--- /dev/null
+++ b/nixos/modules/programs/zsh/zsh-autoenv.nix
@@ -0,0 +1,28 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.zsh.zsh-autoenv;
+in {
+  options = {
+    programs.zsh.zsh-autoenv = {
+      enable = mkEnableOption "zsh-autoenv";
+      package = mkOption {
+        default = pkgs.zsh-autoenv;
+        defaultText = literalExpression "pkgs.zsh-autoenv";
+        description = ''
+          Package to install for `zsh-autoenv` usage.
+        '';
+
+        type = types.package;
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    programs.zsh.interactiveShellInit = ''
+      source ${cfg.package}/share/zsh-autoenv/autoenv.zsh
+    '';
+  };
+}
diff --git a/nixos/modules/programs/zsh/zsh-autosuggestions.nix b/nixos/modules/programs/zsh/zsh-autosuggestions.nix
new file mode 100644
index 00000000000..2e53e907d54
--- /dev/null
+++ b/nixos/modules/programs/zsh/zsh-autosuggestions.nix
@@ -0,0 +1,73 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.zsh.autosuggestions;
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "programs" "zsh" "enableAutosuggestions" ] [ "programs" "zsh" "autosuggestions" "enable" ])
+  ];
+
+  options.programs.zsh.autosuggestions = {
+
+    enable = mkEnableOption "zsh-autosuggestions";
+
+    highlightStyle = mkOption {
+      type = types.str;
+      default = "fg=8"; # https://github.com/zsh-users/zsh-autosuggestions/tree/v0.4.3#suggestion-highlight-style
+      description = "Highlight style for suggestions ({fore,back}ground color)";
+      example = "fg=cyan";
+    };
+
+    strategy = mkOption {
+      type = types.listOf (types.enum [ "history" "completion" "match_prev_cmd" ]);
+      default = [ "history" ];
+      description = ''
+        `ZSH_AUTOSUGGEST_STRATEGY` is an array that specifies how suggestions should be generated.
+        The strategies in the array are tried successively until a suggestion is found.
+        There are currently three built-in strategies to choose from:
+
+        - `history`: Chooses the most recent match from history.
+        - `completion`: Chooses a suggestion based on what tab-completion would suggest. (requires `zpty` module)
+        - `match_prev_cmd`: Like `history`, but chooses the most recent match whose preceding history item matches
+            the most recently executed command. Note that this strategy won't work as expected with ZSH options that
+            don't preserve the history order such as `HIST_IGNORE_ALL_DUPS` or `HIST_EXPIRE_DUPS_FIRST`.
+      '';
+    };
+
+    async = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Whether to fetch suggestions asynchronously";
+      example = false;
+    };
+
+    extraConfig = mkOption {
+      type = with types; attrsOf str;
+      default = {};
+      description = "Attribute set with additional configuration values";
+      example = literalExpression ''
+        {
+          "ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" = "20";
+        }
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    programs.zsh.interactiveShellInit = ''
+      source ${pkgs.zsh-autosuggestions}/share/zsh-autosuggestions/zsh-autosuggestions.zsh
+
+      export ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE="${cfg.highlightStyle}"
+      export ZSH_AUTOSUGGEST_STRATEGY=(${concatStringsSep " " cfg.strategy})
+      ${optionalString (!cfg.async) "unset ZSH_AUTOSUGGEST_USE_ASYNC"}
+
+      ${concatStringsSep "\n" (mapAttrsToList (key: value: ''export ${key}="${value}"'') cfg.extraConfig)}
+    '';
+
+  };
+}
diff --git a/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix b/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
new file mode 100644
index 00000000000..1eb53ccae52
--- /dev/null
+++ b/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
@@ -0,0 +1,107 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.zsh.syntaxHighlighting;
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "programs" "zsh" "enableSyntaxHighlighting" ] [ "programs" "zsh" "syntaxHighlighting" "enable" ])
+    (mkRenamedOptionModule [ "programs" "zsh" "syntax-highlighting" "enable" ] [ "programs" "zsh" "syntaxHighlighting" "enable" ])
+    (mkRenamedOptionModule [ "programs" "zsh" "syntax-highlighting" "highlighters" ] [ "programs" "zsh" "syntaxHighlighting" "highlighters" ])
+    (mkRenamedOptionModule [ "programs" "zsh" "syntax-highlighting" "patterns" ] [ "programs" "zsh" "syntaxHighlighting" "patterns" ])
+  ];
+
+  options = {
+    programs.zsh.syntaxHighlighting = {
+      enable = mkEnableOption "zsh-syntax-highlighting";
+
+      highlighters = mkOption {
+        default = [ "main" ];
+
+        # https://github.com/zsh-users/zsh-syntax-highlighting/blob/master/docs/highlighters.md
+        type = types.listOf(types.enum([
+          "main"
+          "brackets"
+          "pattern"
+          "cursor"
+          "root"
+          "line"
+        ]));
+
+        description = ''
+          Specifies the highlighters to be used by zsh-syntax-highlighting.
+
+          The following defined options can be found here:
+          https://github.com/zsh-users/zsh-syntax-highlighting/blob/master/docs/highlighters.md
+        '';
+      };
+
+      patterns = mkOption {
+        default = {};
+        type = types.attrsOf types.str;
+
+        example = literalExpression ''
+          {
+            "rm -rf *" = "fg=white,bold,bg=red";
+          }
+        '';
+
+        description = ''
+          Specifies custom patterns to be highlighted by zsh-syntax-highlighting.
+
+          Please refer to the docs for more information about the usage:
+          https://github.com/zsh-users/zsh-syntax-highlighting/blob/master/docs/highlighters/pattern.md
+        '';
+      };
+      styles = mkOption {
+        default = {};
+        type = types.attrsOf types.str;
+
+        example = literalExpression ''
+          {
+            "alias" = "fg=magenta,bold";
+          }
+        '';
+
+        description = ''
+          Specifies custom styles to be highlighted by zsh-syntax-highlighting.
+
+          Please refer to the docs for more information about the usage:
+          https://github.com/zsh-users/zsh-syntax-highlighting/blob/master/docs/highlighters/main.md
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ zsh-syntax-highlighting ];
+
+    assertions = [
+      {
+        assertion = length(attrNames cfg.patterns) > 0 -> elem "pattern" cfg.highlighters;
+        message = ''
+          When highlighting patterns, "pattern" needs to be included in the list of highlighters.
+        '';
+      }
+    ];
+
+    programs.zsh.interactiveShellInit = with pkgs;
+      lib.mkAfter (lib.concatStringsSep "\n" ([
+        "source ${zsh-syntax-highlighting}/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh"
+      ] ++ optional (length(cfg.highlighters) > 0)
+        "ZSH_HIGHLIGHT_HIGHLIGHTERS=(${concatStringsSep " " cfg.highlighters})"
+        ++ optionals (length(attrNames cfg.patterns) > 0)
+          (mapAttrsToList (
+            pattern: design:
+            "ZSH_HIGHLIGHT_PATTERNS+=('${pattern}' '${design}')"
+          ) cfg.patterns)
+        ++ optionals (length(attrNames cfg.styles) > 0)
+          (mapAttrsToList (
+            styles: design:
+            "ZSH_HIGHLIGHT_STYLES[${styles}]='${design}'"
+          ) cfg.styles)
+      ));
+  };
+}
diff --git a/nixos/modules/programs/zsh/zsh.nix b/nixos/modules/programs/zsh/zsh.nix
new file mode 100644
index 00000000000..5fe98b6801b
--- /dev/null
+++ b/nixos/modules/programs/zsh/zsh.nix
@@ -0,0 +1,303 @@
+# This module defines global configuration for the zshell.
+
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfge = config.environment;
+
+  cfg = config.programs.zsh;
+  opt = options.programs.zsh;
+
+  zshAliases = concatStringsSep "\n" (
+    mapAttrsFlatten (k: v: "alias ${k}=${escapeShellArg v}")
+      (filterAttrs (k: v: v != null) cfg.shellAliases)
+  );
+
+  zshStartupNotes = ''
+    # Note that generated /etc/zprofile and /etc/zshrc files do a lot of
+    # non-standard setup to make zsh usable with no configuration by default.
+    #
+    # Which means that unless you explicitly meticulously override everything
+    # generated, interactions between your ~/.zshrc and these files are likely
+    # to be rather surprising.
+    #
+    # Note however, that you can disable loading of the generated /etc/zprofile
+    # and /etc/zshrc (you can't disable loading of /etc/zshenv, but it is
+    # designed to not set anything surprising) by setting `no_global_rcs` option
+    # in ~/.zshenv:
+    #
+    #   echo setopt no_global_rcs >> ~/.zshenv
+    #
+    # See "STARTUP/SHUTDOWN FILES" section of zsh(1) for more info.
+  '';
+
+in
+
+{
+
+  options = {
+
+    programs.zsh = {
+
+      enable = mkOption {
+        default = false;
+        description = ''
+          Whether to configure zsh as an interactive shell. To enable zsh for
+          a particular user, use the <option>users.users.&lt;name?&gt;.shell</option>
+          option for that user. To enable zsh system-wide use the
+          <option>users.defaultUserShell</option> option.
+        '';
+        type = types.bool;
+      };
+
+      shellAliases = mkOption {
+        default = { };
+        description = ''
+          Set of aliases for zsh shell, which overrides <option>environment.shellAliases</option>.
+          See <option>environment.shellAliases</option> for an option format description.
+        '';
+        type = with types; attrsOf (nullOr (either str path));
+      };
+
+      shellInit = mkOption {
+        default = "";
+        description = ''
+          Shell script code called during zsh shell initialisation.
+        '';
+        type = types.lines;
+      };
+
+      loginShellInit = mkOption {
+        default = "";
+        description = ''
+          Shell script code called during zsh login shell initialisation.
+        '';
+        type = types.lines;
+      };
+
+      interactiveShellInit = mkOption {
+        default = "";
+        description = ''
+          Shell script code called during interactive zsh shell initialisation.
+        '';
+        type = types.lines;
+      };
+
+      promptInit = mkOption {
+        default = ''
+          # Note that to manually override this in ~/.zshrc you should run `prompt off`
+          # before setting your PS1 and etc. Otherwise this will likely to interact with
+          # your ~/.zshrc configuration in unexpected ways as the default prompt sets
+          # a lot of different prompt variables.
+          autoload -U promptinit && promptinit && prompt suse && setopt prompt_sp
+        '';
+        description = ''
+          Shell script code used to initialise the zsh prompt.
+        '';
+        type = types.lines;
+      };
+
+      histSize = mkOption {
+        default = 2000;
+        description = ''
+          Change history size.
+        '';
+        type = types.int;
+      };
+
+      histFile = mkOption {
+        default = "$HOME/.zsh_history";
+        description = ''
+          Change history file.
+        '';
+        type = types.str;
+      };
+
+      setOptions = mkOption {
+        type = types.listOf types.str;
+        default = [
+          "HIST_IGNORE_DUPS"
+          "SHARE_HISTORY"
+          "HIST_FCNTL_LOCK"
+        ];
+        example = [ "EXTENDED_HISTORY" "RM_STAR_WAIT" ];
+        description = ''
+          Configure zsh options. See
+          <citerefentry><refentrytitle>zshoptions</refentrytitle><manvolnum>1</manvolnum></citerefentry>.
+        '';
+      };
+
+      enableCompletion = mkOption {
+        default = true;
+        description = ''
+          Enable zsh completion for all interactive zsh shells.
+        '';
+        type = types.bool;
+      };
+
+      enableBashCompletion = mkOption {
+        default = false;
+        description = ''
+          Enable compatibility with bash's programmable completion system.
+        '';
+        type = types.bool;
+      };
+
+      enableGlobalCompInit = mkOption {
+        default = cfg.enableCompletion;
+        defaultText = literalExpression "config.${opt.enableCompletion}";
+        description = ''
+          Enable execution of compinit call for all interactive zsh shells.
+
+          This option can be disabled if the user wants to extend its
+          <literal>fpath</literal> and a custom <literal>compinit</literal>
+          call in the local config is required.
+        '';
+        type = types.bool;
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    programs.zsh.shellAliases = mapAttrs (name: mkDefault) cfge.shellAliases;
+
+    environment.etc.zshenv.text =
+      ''
+        # /etc/zshenv: DO NOT EDIT -- this file has been generated automatically.
+        # This file is read for all shells.
+
+        # Only execute this file once per shell.
+        if [ -n "$__ETC_ZSHENV_SOURCED" ]; then return; fi
+        __ETC_ZSHENV_SOURCED=1
+
+        if [ -z "$__NIXOS_SET_ENVIRONMENT_DONE" ]; then
+            . ${config.system.build.setEnvironment}
+        fi
+
+        HELPDIR="${pkgs.zsh}/share/zsh/$ZSH_VERSION/help"
+
+        # Tell zsh how to find installed completions.
+        for p in ''${(z)NIX_PROFILES}; do
+            fpath+=($p/share/zsh/site-functions $p/share/zsh/$ZSH_VERSION/functions $p/share/zsh/vendor-completions)
+        done
+
+        # Setup custom shell init stuff.
+        ${cfge.shellInit}
+
+        ${cfg.shellInit}
+
+        # Read system-wide modifications.
+        if test -f /etc/zshenv.local; then
+            . /etc/zshenv.local
+        fi
+      '';
+
+    environment.etc.zprofile.text =
+      ''
+        # /etc/zprofile: DO NOT EDIT -- this file has been generated automatically.
+        # This file is read for login shells.
+        #
+        ${zshStartupNotes}
+
+        # Only execute this file once per shell.
+        if [ -n "$__ETC_ZPROFILE_SOURCED" ]; then return; fi
+        __ETC_ZPROFILE_SOURCED=1
+
+        # Setup custom login shell init stuff.
+        ${cfge.loginShellInit}
+
+        ${cfg.loginShellInit}
+
+        # Read system-wide modifications.
+        if test -f /etc/zprofile.local; then
+            . /etc/zprofile.local
+        fi
+      '';
+
+    environment.etc.zshrc.text =
+      ''
+        # /etc/zshrc: DO NOT EDIT -- this file has been generated automatically.
+        # This file is read for interactive shells.
+        #
+        ${zshStartupNotes}
+
+        # Only execute this file once per shell.
+        if [ -n "$__ETC_ZSHRC_SOURCED" -o -n "$NOSYSZSHRC" ]; then return; fi
+        __ETC_ZSHRC_SOURCED=1
+
+        ${optionalString (cfg.setOptions != []) ''
+          # Set zsh options.
+          setopt ${concatStringsSep " " cfg.setOptions}
+        ''}
+
+        # Setup command line history.
+        # Don't export these, otherwise other shells (bash) will try to use same HISTFILE.
+        SAVEHIST=${toString cfg.histSize}
+        HISTSIZE=${toString cfg.histSize}
+        HISTFILE=${cfg.histFile}
+
+        # Configure sane keyboard defaults.
+        . /etc/zinputrc
+
+        ${optionalString cfg.enableGlobalCompInit ''
+          # Enable autocompletion.
+          autoload -U compinit && compinit
+        ''}
+
+        ${optionalString cfg.enableBashCompletion ''
+          # Enable compatibility with bash's completion system.
+          autoload -U bashcompinit && bashcompinit
+        ''}
+
+        # Setup custom interactive shell init stuff.
+        ${cfge.interactiveShellInit}
+
+        ${cfg.interactiveShellInit}
+
+        # Setup aliases.
+        ${zshAliases}
+
+        # Setup prompt.
+        ${cfg.promptInit}
+
+        # Disable some features to support TRAMP.
+        if [ "$TERM" = dumb ]; then
+            unsetopt zle prompt_cr prompt_subst
+            unset RPS1 RPROMPT
+            PS1='$ '
+            PROMPT='$ '
+        fi
+
+        # Read system-wide modifications.
+        if test -f /etc/zshrc.local; then
+            . /etc/zshrc.local
+        fi
+      '';
+
+    # Bug in nix flakes:
+    # If we use `.source` here the path is garbage collected also we point to it with a symlink
+    # see https://github.com/NixOS/nixpkgs/issues/132732
+    environment.etc.zinputrc.text = builtins.readFile ./zinputrc;
+
+    environment.systemPackages = [ pkgs.zsh ]
+      ++ optional cfg.enableCompletion pkgs.nix-zsh-completions;
+
+    environment.pathsToLink = optional cfg.enableCompletion "/share/zsh";
+
+    #users.defaultUserShell = mkDefault "/run/current-system/sw/bin/zsh";
+
+    environment.shells =
+      [
+        "/run/current-system/sw/bin/zsh"
+        "${pkgs.zsh}/bin/zsh"
+      ];
+
+  };
+
+}
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
new file mode 100644
index 00000000000..195cf87e6a8
--- /dev/null
+++ b/nixos/modules/rename.nix
@@ -0,0 +1,97 @@
+{ lib, pkgs, ... }:
+
+with lib;
+
+{
+  imports = [
+    /*
+    This file defines some renaming/removing options for backwards compatibility
+
+    It should ONLY be used when the relevant module can't define these imports
+    itself, such as when the module was removed completely.
+    See https://github.com/NixOS/nixpkgs/pull/61570 for explanation
+    */
+
+    # 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" ])
+
+    # Completely removed modules
+    (mkRemovedOptionModule [ "environment" "blcr" "enable" ] "The BLCR module has been removed")
+    (mkRemovedOptionModule [ "fonts" "fontconfig" "penultimate" ] "The corresponding package has removed from nixpkgs.")
+    (mkRemovedOptionModule [ "hardware" "brightnessctl" ] ''
+      The brightnessctl module was removed because newer versions of
+      brightnessctl don't require the udev rules anymore (they can use the
+      systemd-logind API). Instead of using the module you can now
+      simply add the brightnessctl package to environment.systemPackages.
+    '')
+    (mkRemovedOptionModule [ "hardware" "u2f" ] ''
+      The U2F modules module was removed, as all it did was adding the
+      udev rules from libu2f-host to the system. Udev gained native support
+      to handle FIDO security tokens, so this isn't necessary anymore.
+    '')
+    (mkRemovedOptionModule [ "networking" "vpnc" ] "Use environment.etc.\"vpnc/service.conf\" instead.")
+    (mkRemovedOptionModule [ "networking" "wicd" ] "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"))
+    (mkRemovedOptionModule [ "security" "hideProcessInformation" ] ''
+        The hidepid module was removed, since the underlying machinery
+        is broken when using cgroups-v2.
+    '')
+    (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" "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.")
+    (mkRemovedOptionModule [ "services" "fourStore" ] "The fourStore module has been removed")
+    (mkRemovedOptionModule [ "services" "fourStoreEndpoint" ] "The fourStoreEndpoint module has been removed")
+    (mkRemovedOptionModule [ "services" "fprot" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "frab" ] "The frab module has been removed")
+    (mkRemovedOptionModule [ "services" "kippo" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "mailpile" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "marathon" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "mathics" ] "The Mathics module has been removed")
+    (mkRemovedOptionModule [ "services" "meguca" ] "Use meguca has been removed from nixpkgs")
+    (mkRemovedOptionModule [ "services" "mesos" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "moinmoin" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "mwlib" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "osquery" ] "The osquery module has been removed")
+    (mkRemovedOptionModule [ "services" "pantheon" "files" ] ''
+      This module was removed, please add pkgs.pantheon.elementary-files to environment.systemPackages directly.
+    '')
+    (mkRemovedOptionModule [ "services" "prey" ] ''
+      prey-bash-client is deprecated upstream
+    '')
+    (mkRemovedOptionModule [ "services" "quagga" ] "the corresponding package has been removed from nixpkgs")
+    (mkRemovedOptionModule [ "services" "seeks" ] "")
+    (mkRemovedOptionModule [ "services" "venus" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "wakeonlan"] "This module was removed in favor of enabling it with networking.interfaces.<name>.wakeOnLan")
+    (mkRemovedOptionModule [ "services" "winstone" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "xserver" "displayManager" "auto" ] ''
+      The services.xserver.displayManager.auto module has been removed
+      because it was only intended for use in internal NixOS tests, and gave the
+      false impression of it being a special display manager when it's actually
+      LightDM. Please use the services.xserver.displayManager.autoLogin options
+      instead, or any other display manager in NixOS as they all support auto-login.
+    '')
+    (mkRemovedOptionModule [ "services" "xserver" "multitouch" ] ''
+      services.xserver.multitouch (which uses xf86_input_mtrack) has been removed
+      as the underlying package isn't being maintained. Working alternatives are
+      libinput and synaptics.
+    '')
+    (mkRemovedOptionModule [ "virtualisation" "rkt" ] "The rkt module has been removed, it was archived by upstream")
+    (mkRemovedOptionModule [ "services" "racoon" ] ''
+      The racoon module has been removed, because the software project was abandoned upstream.
+    '')
+    (mkRemovedOptionModule [ "services" "shellinabox" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "gogoclient" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "virtuoso" ] "The corresponding package was removed from nixpkgs.")
+
+    # Do NOT add any option renames here, see top of the file
+  ];
+}
diff --git a/nixos/modules/security/acme/default.nix b/nixos/modules/security/acme/default.nix
new file mode 100644
index 00000000000..d827c448055
--- /dev/null
+++ b/nixos/modules/security/acme/default.nix
@@ -0,0 +1,921 @@
+{ config, lib, pkgs, options, ... }:
+with lib;
+let
+  cfg = config.security.acme;
+  opt = options.security.acme;
+  user = if cfg.useRoot then "root" else "acme";
+
+  # Used to calculate timer accuracy for coalescing
+  numCerts = length (builtins.attrNames cfg.certs);
+  _24hSecs = 60 * 60 * 24;
+
+  # Used to make unique paths for each cert/account config set
+  mkHash = with builtins; val: substring 0 20 (hashString "sha256" val);
+  mkAccountHash = acmeServer: data: mkHash "${toString acmeServer} ${data.keyType} ${data.email}";
+  accountDirRoot = "/var/lib/acme/.lego/accounts/";
+
+  # There are many services required to make cert renewals work.
+  # They all follow a common structure:
+  #   - They inherit this commonServiceConfig
+  #   - They all run as the acme user
+  #   - They all use BindPath and StateDirectory where possible
+  #     to set up a sort of build environment in /tmp
+  # The Group can vary depending on what the user has specified in
+  # security.acme.certs.<cert>.group on some of the services.
+  commonServiceConfig = {
+    Type = "oneshot";
+    User = user;
+    Group = mkDefault "acme";
+    UMask = 0022;
+    StateDirectoryMode = 750;
+    ProtectSystem = "strict";
+    ReadWritePaths = [
+      "/var/lib/acme"
+    ];
+    PrivateTmp = true;
+
+    WorkingDirectory = "/tmp";
+
+    CapabilityBoundingSet = [ "" ];
+    DevicePolicy = "closed";
+    LockPersonality = true;
+    MemoryDenyWriteExecute = true;
+    NoNewPrivileges = true;
+    PrivateDevices = true;
+    ProtectClock = true;
+    ProtectHome = true;
+    ProtectHostname = true;
+    ProtectControlGroups = true;
+    ProtectKernelLogs = true;
+    ProtectKernelModules = true;
+    ProtectKernelTunables = true;
+    ProtectProc = "invisible";
+    ProcSubset = "pid";
+    RemoveIPC = true;
+    RestrictAddressFamilies = [
+      "AF_INET"
+      "AF_INET6"
+    ];
+    RestrictNamespaces = true;
+    RestrictRealtime = true;
+    RestrictSUIDSGID = true;
+    SystemCallArchitectures = "native";
+    SystemCallFilter = [
+      # 1. allow a reasonable set of syscalls
+      "@system-service"
+      # 2. and deny unreasonable ones
+      "~@privileged @resources"
+      # 3. then allow the required subset within denied groups
+      "@chown"
+    ];
+  };
+
+  # In order to avoid race conditions creating the CA for selfsigned certs,
+  # we have a separate service which will create the necessary files.
+  selfsignCAService = {
+    description = "Generate self-signed certificate authority";
+
+    path = with pkgs; [ minica ];
+
+    unitConfig = {
+      ConditionPathExists = "!/var/lib/acme/.minica/key.pem";
+      StartLimitIntervalSec = 0;
+    };
+
+    serviceConfig = commonServiceConfig // {
+      StateDirectory = "acme/.minica";
+      BindPaths = "/var/lib/acme/.minica:/tmp/ca";
+      UMask = 0077;
+    };
+
+    # Working directory will be /tmp
+    script = ''
+      minica \
+        --ca-key ca/key.pem \
+        --ca-cert ca/cert.pem \
+        --domains selfsigned.local
+    '';
+  };
+
+  # Ensures that directories which are shared across all certs
+  # exist and have the correct user and group, since group
+  # is configurable on a per-cert basis.
+  userMigrationService = let
+    script = with builtins; ''
+      chown -R ${user} .lego/accounts
+    '' + (concatStringsSep "\n" (mapAttrsToList (cert: data: ''
+      for fixpath in ${escapeShellArg cert} .lego/${escapeShellArg cert}; do
+        if [ -d "$fixpath" ]; then
+          chmod -R u=rwX,g=rX,o= "$fixpath"
+          chown -R ${user}:${data.group} "$fixpath"
+        fi
+      done
+    '') certConfigs));
+  in {
+    description = "Fix owner and group of all ACME certificates";
+
+    serviceConfig = commonServiceConfig // {
+      # We don't want this to run every time a renewal happens
+      RemainAfterExit = true;
+
+      # These StateDirectory entries negate the need for tmpfiles
+      StateDirectory = [ "acme" "acme/.lego" "acme/.lego/accounts" ];
+      StateDirectoryMode = 755;
+      WorkingDirectory = "/var/lib/acme";
+
+      # Run the start script as root
+      ExecStart = "+" + (pkgs.writeShellScript "acme-fixperms" script);
+    };
+  };
+
+  certToConfig = cert: data: let
+    acmeServer = data.server;
+    useDns = data.dnsProvider != null;
+    destPath = "/var/lib/acme/${cert}";
+    selfsignedDeps = optionals (cfg.preliminarySelfsigned) [ "acme-selfsigned-${cert}.service" ];
+
+    # Minica and lego have a "feature" which replaces * with _. We need
+    # to make this substitution to reference the output files from both programs.
+    # End users never see this since we rename the certs.
+    keyName = builtins.replaceStrings ["*"] ["_"] data.domain;
+
+    # FIXME when mkChangedOptionModule supports submodules, change to that.
+    # This is a workaround
+    extraDomains = data.extraDomainNames ++ (
+      optionals
+      (data.extraDomains != "_mkMergedOptionModule")
+      (builtins.attrNames data.extraDomains)
+    );
+
+    # Create hashes for cert data directories based on configuration
+    # Flags are separated to avoid collisions
+    hashData = with builtins; ''
+      ${concatStringsSep " " data.extraLegoFlags} -
+      ${concatStringsSep " " data.extraLegoRunFlags} -
+      ${concatStringsSep " " data.extraLegoRenewFlags} -
+      ${toString acmeServer} ${toString data.dnsProvider}
+      ${toString data.ocspMustStaple} ${data.keyType}
+    '';
+    certDir = mkHash hashData;
+    # TODO remove domainHash usage entirely. Waiting on go-acme/lego#1532
+    domainHash = mkHash "${concatStringsSep " " extraDomains} ${data.domain}";
+    accountHash = (mkAccountHash acmeServer data);
+    accountDir = accountDirRoot + accountHash;
+
+    protocolOpts = if useDns then (
+      [ "--dns" data.dnsProvider ]
+      ++ optionals (!data.dnsPropagationCheck) [ "--dns.disable-cp" ]
+      ++ optionals (data.dnsResolver != null) [ "--dns.resolvers" data.dnsResolver ]
+    ) else if data.listenHTTP != null then [ "--http" "--http.port" data.listenHTTP ]
+    else [ "--http" "--http.webroot" data.webroot ];
+
+    commonOpts = [
+      "--accept-tos" # Checking the option is covered by the assertions
+      "--path" "."
+      "-d" data.domain
+      "--email" data.email
+      "--key-type" data.keyType
+    ] ++ protocolOpts
+      ++ optionals (acmeServer != null) [ "--server" acmeServer ]
+      ++ concatMap (name: [ "-d" name ]) extraDomains
+      ++ data.extraLegoFlags;
+
+    # Although --must-staple is common to both modes, it is not declared as a
+    # mode-agnostic argument in lego and thus must come after the mode.
+    runOpts = escapeShellArgs (
+      commonOpts
+      ++ [ "run" ]
+      ++ optionals data.ocspMustStaple [ "--must-staple" ]
+      ++ data.extraLegoRunFlags
+    );
+    renewOpts = escapeShellArgs (
+      commonOpts
+      ++ [ "renew" ]
+      ++ optionals data.ocspMustStaple [ "--must-staple" ]
+      ++ data.extraLegoRenewFlags
+    );
+
+    # We need to collect all the ACME webroots to grant them write
+    # access in the systemd service.
+    webroots =
+      lib.remove null
+        (lib.unique
+            (builtins.map
+            (certAttrs: certAttrs.webroot)
+            (lib.attrValues config.security.acme.certs)));
+  in {
+    inherit accountHash cert selfsignedDeps;
+
+    group = data.group;
+
+    renewTimer = {
+      description = "Renew ACME Certificate for ${cert}";
+      wantedBy = [ "timers.target" ];
+      timerConfig = {
+        OnCalendar = data.renewInterval;
+        Unit = "acme-${cert}.service";
+        Persistent = "yes";
+
+        # Allow systemd to pick a convenient time within the day
+        # to run the check.
+        # This allows the coalescing of multiple timer jobs.
+        # We divide by the number of certificates so that if you
+        # have many certificates, the renewals are distributed over
+        # the course of the day to avoid rate limits.
+        AccuracySec = "${toString (_24hSecs / numCerts)}s";
+
+        # Skew randomly within the day, per https://letsencrypt.org/docs/integration-guide/.
+        RandomizedDelaySec = "24h";
+      };
+    };
+
+    selfsignService = {
+      description = "Generate self-signed certificate for ${cert}";
+      after = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ];
+      requires = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ];
+
+      path = with pkgs; [ minica ];
+
+      unitConfig = {
+        ConditionPathExists = "!/var/lib/acme/${cert}/key.pem";
+        StartLimitIntervalSec = 0;
+      };
+
+      serviceConfig = commonServiceConfig // {
+        Group = data.group;
+        UMask = 0027;
+
+        StateDirectory = "acme/${cert}";
+
+        BindPaths = [
+          "/var/lib/acme/.minica:/tmp/ca"
+          "/var/lib/acme/${cert}:/tmp/${keyName}"
+        ];
+      };
+
+      # Working directory will be /tmp
+      # minica will output to a folder sharing the name of the first domain
+      # in the list, which will be ${data.domain}
+      script = ''
+        minica \
+          --ca-key ca/key.pem \
+          --ca-cert ca/cert.pem \
+          --domains ${escapeShellArg (builtins.concatStringsSep "," ([ data.domain ] ++ extraDomains))}
+
+        # Create files to match directory layout for real certificates
+        cd '${keyName}'
+        cp ../ca/cert.pem chain.pem
+        cat cert.pem chain.pem > fullchain.pem
+        cat key.pem fullchain.pem > full.pem
+
+        # Group might change between runs, re-apply it
+        chown '${user}:${data.group}' *
+
+        # Default permissions make the files unreadable by group + anon
+        # Need to be readable by group
+        chmod 640 *
+      '';
+    };
+
+    renewService = {
+      description = "Renew ACME certificate for ${cert}";
+      after = [ "network.target" "network-online.target" "acme-fixperms.service" "nss-lookup.target" ] ++ selfsignedDeps;
+      wants = [ "network-online.target" "acme-fixperms.service" ] ++ selfsignedDeps;
+
+      # https://github.com/NixOS/nixpkgs/pull/81371#issuecomment-605526099
+      wantedBy = optionals (!config.boot.isContainer) [ "multi-user.target" ];
+
+      path = with pkgs; [ lego coreutils diffutils openssl ];
+
+      serviceConfig = commonServiceConfig // {
+        Group = data.group;
+
+        # Keep in mind that these directories will be deleted if the user runs
+        # systemctl clean --what=state
+        # acme/.lego/${cert} is listed for this reason.
+        StateDirectory = [
+          "acme/${cert}"
+          "acme/.lego/${cert}"
+          "acme/.lego/${cert}/${certDir}"
+          "acme/.lego/accounts/${accountHash}"
+        ];
+
+        ReadWritePaths = commonServiceConfig.ReadWritePaths ++ webroots;
+
+        # Needs to be space separated, but can't use a multiline string because that'll include newlines
+        BindPaths = [
+          "${accountDir}:/tmp/accounts"
+          "/var/lib/acme/${cert}:/tmp/out"
+          "/var/lib/acme/.lego/${cert}/${certDir}:/tmp/certificates"
+        ];
+
+        # Only try loading the credentialsFile if the dns challenge is enabled
+        EnvironmentFile = mkIf useDns data.credentialsFile;
+
+        # Run as root (Prefixed with +)
+        ExecStartPost = "+" + (pkgs.writeShellScript "acme-postrun" ''
+          cd /var/lib/acme/${escapeShellArg cert}
+          if [ -e renewed ]; then
+            rm renewed
+            ${data.postRun}
+            ${optionalString (data.reloadServices != [])
+                "systemctl --no-block try-reload-or-restart ${escapeShellArgs data.reloadServices}"
+            }
+          fi
+        '');
+      } // optionalAttrs (data.listenHTTP != null && toInt (elemAt (splitString ":" data.listenHTTP) 1) < 1024) {
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+      };
+
+      # Working directory will be /tmp
+      script = ''
+        ${optionalString data.enableDebugLogs "set -x"}
+        set -euo pipefail
+
+        # This reimplements the expiration date check, but without querying
+        # the acme server first. By doing this offline, we avoid errors
+        # when the network or DNS are unavailable, which can happen during
+        # nixos-rebuild switch.
+        is_expiration_skippable() {
+          pem=$1
+
+          # This function relies on set -e to exit early if any of the
+          # conditions or programs fail.
+
+          [[ -e $pem ]]
+
+          expiration_line="$(
+            set -euxo pipefail
+            openssl x509 -noout -enddate <$pem \
+                  | grep notAfter \
+                  | sed -e 's/^notAfter=//'
+          )"
+          [[ -n "$expiration_line" ]]
+
+          expiration_date="$(date -d "$expiration_line" +%s)"
+          now="$(date +%s)"
+          expiration_s=$[expiration_date - now]
+          expiration_days=$[expiration_s / (3600 * 24)]   # rounds down
+
+          [[ $expiration_days -gt ${toString data.validMinDays} ]]
+        }
+
+        ${optionalString (data.webroot != null) ''
+          # Ensure the webroot exists. Fixing group is required in case configuration was changed between runs.
+          # Lego will fail if the webroot does not exist at all.
+          (
+            mkdir -p '${data.webroot}/.well-known/acme-challenge' \
+            && chgrp '${data.group}' ${data.webroot}/.well-known/acme-challenge
+          ) || (
+            echo 'Please ensure ${data.webroot}/.well-known/acme-challenge exists and is writable by acme:${data.group}' \
+            && exit 1
+          )
+        ''}
+
+        echo '${domainHash}' > domainhash.txt
+
+        # Check if we can renew.
+        # We can only renew if the list of domains has not changed.
+        if cmp -s domainhash.txt certificates/domainhash.txt && [ -e 'certificates/${keyName}.key' -a -e 'certificates/${keyName}.crt' -a -n "$(ls -1 accounts)" ]; then
+
+          # Even if a cert is not expired, it may be revoked by the CA.
+          # Try to renew, and silently fail if the cert is not expired.
+          # Avoids #85794 and resolves #129838
+          if ! lego ${renewOpts} --days ${toString data.validMinDays}; then
+            if is_expiration_skippable out/full.pem; then
+              echo 1>&2 "nixos-acme: Ignoring failed renewal because expiration isn't within the coming ${toString data.validMinDays} days"
+            else
+              # High number to avoid Systemd reserved codes.
+              exit 11
+            fi
+          fi
+
+        # Otherwise do a full run
+        elif ! lego ${runOpts}; then
+          # Produce a nice error for those doing their first nixos-rebuild with these certs
+          echo Failed to fetch certificates. \
+            This may mean your DNS records are set up incorrectly. \
+            ${optionalString (cfg.preliminarySelfsigned) "Selfsigned certs are in place and dependant services will still start."}
+          # Exit 10 so that users can potentially amend SuccessExitStatus to ignore this error.
+          # High number to avoid Systemd reserved codes.
+          exit 10
+        fi
+
+        mv domainhash.txt certificates/
+
+        # Group might change between runs, re-apply it
+        chown '${user}:${data.group}' certificates/*
+
+        # Copy all certs to the "real" certs directory
+        if ! cmp -s 'certificates/${keyName}.crt' out/fullchain.pem; then
+          touch out/renewed
+          echo Installing new certificate
+          cp -vp 'certificates/${keyName}.crt' out/fullchain.pem
+          cp -vp 'certificates/${keyName}.key' out/key.pem
+          cp -vp 'certificates/${keyName}.issuer.crt' out/chain.pem
+          ln -sf fullchain.pem out/cert.pem
+          cat out/key.pem out/fullchain.pem > out/full.pem
+        fi
+
+        # By default group will have no access to the cert files.
+        # This chmod will fix that.
+        chmod 640 out/*
+      '';
+    };
+  };
+
+  certConfigs = mapAttrs certToConfig cfg.certs;
+
+  # These options can be specified within
+  # security.acme.defaults or security.acme.certs.<name>
+  inheritableModule = isDefaults: { config, ... }: let
+    defaultAndText = name: default: {
+      # When ! isDefaults then this is the option declaration for the
+      # security.acme.certs.<name> path, which has the extra inheritDefaults
+      # option, which if disabled means that we can't inherit it
+      default = if isDefaults || ! config.inheritDefaults then default else cfg.defaults.${name};
+      # The docs however don't need to depend on inheritDefaults, they should
+      # stay constant. Though notably it wouldn't matter much, because to get
+      # the option information, a submodule with name `<name>` is evaluated
+      # without any definitions.
+      defaultText = if isDefaults then default else literalExpression "config.security.acme.defaults.${name}";
+    };
+  in {
+    options = {
+      validMinDays = mkOption {
+        type = types.int;
+        inherit (defaultAndText "validMinDays" 30) default defaultText;
+        description = "Minimum remaining validity before renewal in days.";
+      };
+
+      renewInterval = mkOption {
+        type = types.str;
+        inherit (defaultAndText "renewInterval" "daily") default defaultText;
+        description = ''
+          Systemd calendar expression when to check for renewal. See
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+
+      enableDebugLogs = mkEnableOption "debug logging for this certificate" // {
+        inherit (defaultAndText "enableDebugLogs" true) default defaultText;
+      };
+
+      webroot = mkOption {
+        type = types.nullOr types.str;
+        inherit (defaultAndText "webroot" null) default defaultText;
+        example = "/var/lib/acme/acme-challenge";
+        description = ''
+          Where the webroot of the HTTP vhost is located.
+          <filename>.well-known/acme-challenge/</filename> directory
+          will be created below the webroot if it doesn't exist.
+          <literal>http://example.org/.well-known/acme-challenge/</literal> must also
+          be available (notice unencrypted HTTP).
+        '';
+      };
+
+      server = mkOption {
+        type = types.nullOr types.str;
+        inherit (defaultAndText "server" null) default defaultText;
+        description = ''
+          ACME Directory Resource URI. Defaults to Let's Encrypt's
+          production endpoint,
+          <link xlink:href="https://acme-v02.api.letsencrypt.org/directory"/>, if unset.
+        '';
+      };
+
+      email = mkOption {
+        type = types.str;
+        inherit (defaultAndText "email" null) default defaultText;
+        description = ''
+          Email address for account creation and correspondence from the CA.
+          It is recommended to use the same email for all certs to avoid account
+          creation limits.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        inherit (defaultAndText "group" "acme") default defaultText;
+        description = "Group running the ACME client.";
+      };
+
+      reloadServices = mkOption {
+        type = types.listOf types.str;
+        inherit (defaultAndText "reloadServices" []) default defaultText;
+        description = ''
+          The list of systemd services to call <code>systemctl try-reload-or-restart</code>
+          on.
+        '';
+      };
+
+      postRun = mkOption {
+        type = types.lines;
+        inherit (defaultAndText "postRun" "") default defaultText;
+        example = "cp full.pem backup.pem";
+        description = ''
+          Commands to run after new certificates go live. Note that
+          these commands run as the root user.
+
+          Executed in the same directory with the new certificate.
+        '';
+      };
+
+      keyType = mkOption {
+        type = types.str;
+        inherit (defaultAndText "keyType" "ec256") default defaultText;
+        description = ''
+          Key type to use for private keys.
+          For an up to date list of supported values check the --key-type option
+          at <link xlink:href="https://go-acme.github.io/lego/usage/cli/#usage"/>.
+        '';
+      };
+
+      dnsProvider = mkOption {
+        type = types.nullOr types.str;
+        inherit (defaultAndText "dnsProvider" null) default defaultText;
+        example = "route53";
+        description = ''
+          DNS Challenge provider. For a list of supported providers, see the "code"
+          field of the DNS providers listed at <link xlink:href="https://go-acme.github.io/lego/dns/"/>.
+        '';
+      };
+
+      dnsResolver = mkOption {
+        type = types.nullOr types.str;
+        inherit (defaultAndText "dnsResolver" null) default defaultText;
+        example = "1.1.1.1:53";
+        description = ''
+          Set the resolver to use for performing recursive DNS queries. Supported:
+          host:port. The default is to use the system resolvers, or Google's DNS
+          resolvers if the system's cannot be determined.
+        '';
+      };
+
+      credentialsFile = mkOption {
+        type = types.path;
+        inherit (defaultAndText "credentialsFile" null) default defaultText;
+        description = ''
+          Path to an EnvironmentFile for the cert's service containing any required and
+          optional environment variables for your selected dnsProvider.
+          To find out what values you need to set, consult the documentation at
+          <link xlink:href="https://go-acme.github.io/lego/dns/"/> for the corresponding dnsProvider.
+        '';
+        example = "/var/src/secrets/example.org-route53-api-token";
+      };
+
+      dnsPropagationCheck = mkOption {
+        type = types.bool;
+        inherit (defaultAndText "dnsPropagationCheck" true) default defaultText;
+        description = ''
+          Toggles lego DNS propagation check, which is used alongside DNS-01
+          challenge to ensure the DNS entries required are available.
+        '';
+      };
+
+      ocspMustStaple = mkOption {
+        type = types.bool;
+        inherit (defaultAndText "ocspMustStaple" false) default defaultText;
+        description = ''
+          Turns on the OCSP Must-Staple TLS extension.
+          Make sure you know what you're doing! See:
+          <itemizedlist>
+            <listitem><para><link xlink:href="https://blog.apnic.net/2019/01/15/is-the-web-ready-for-ocsp-must-staple/" /></para></listitem>
+            <listitem><para><link xlink:href="https://blog.hboeck.de/archives/886-The-Problem-with-OCSP-Stapling-and-Must-Staple-and-why-Certificate-Revocation-is-still-broken.html" /></para></listitem>
+          </itemizedlist>
+        '';
+      };
+
+      extraLegoFlags = mkOption {
+        type = types.listOf types.str;
+        inherit (defaultAndText "extraLegoFlags" []) default defaultText;
+        description = ''
+          Additional global flags to pass to all lego commands.
+        '';
+      };
+
+      extraLegoRenewFlags = mkOption {
+        type = types.listOf types.str;
+        inherit (defaultAndText "extraLegoRenewFlags" []) default defaultText;
+        description = ''
+          Additional flags to pass to lego renew.
+        '';
+      };
+
+      extraLegoRunFlags = mkOption {
+        type = types.listOf types.str;
+        inherit (defaultAndText "extraLegoRunFlags" []) default defaultText;
+        description = ''
+          Additional flags to pass to lego run.
+        '';
+      };
+    };
+  };
+
+  certOpts = { name, config, ... }: {
+    options = {
+      # user option has been removed
+      user = mkOption {
+        visible = false;
+        default = "_mkRemovedOptionModule";
+      };
+
+      # allowKeysForGroup option has been removed
+      allowKeysForGroup = mkOption {
+        visible = false;
+        default = "_mkRemovedOptionModule";
+      };
+
+      # extraDomains was replaced with extraDomainNames
+      extraDomains = mkOption {
+        visible = false;
+        default = "_mkMergedOptionModule";
+      };
+
+      directory = mkOption {
+        type = types.str;
+        readOnly = true;
+        default = "/var/lib/acme/${name}";
+        description = "Directory where certificate and other state is stored.";
+      };
+
+      domain = mkOption {
+        type = types.str;
+        default = name;
+        description = "Domain to fetch certificate for (defaults to the entry name).";
+      };
+
+      extraDomainNames = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = literalExpression ''
+          [
+            "example.org"
+            "mydomain.org"
+          ]
+        '';
+        description = ''
+          A list of extra domain names, which are included in the one certificate to be issued.
+        '';
+      };
+
+      # This setting must be different for each configured certificate, otherwise
+      # two or more renewals may fail to bind to the address. Hence, it is not in
+      # the inheritableOpts.
+      listenHTTP = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = ":1360";
+        description = ''
+          Interface and port to listen on to solve HTTP challenges
+          in the form [INTERFACE]:PORT.
+          If you use a port other than 80, you must proxy port 80 to this port.
+        '';
+      };
+
+      inheritDefaults = mkOption {
+        default = true;
+        example = true;
+        description = "Whether to inherit values set in `security.acme.defaults` or not.";
+        type = lib.types.bool;
+      };
+    };
+  };
+
+in {
+
+  options = {
+    security.acme = {
+      preliminarySelfsigned = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether a preliminary self-signed certificate should be generated before
+          doing ACME requests. This can be useful when certificates are required in
+          a webserver, but ACME needs the webserver to make its requests.
+
+          With preliminary self-signed certificate the webserver can be started and
+          can later reload the correct ACME certificates.
+        '';
+      };
+
+      acceptTerms = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Accept the CA's terms of service. The default provider is Let's Encrypt,
+          you can find their ToS at <link xlink:href="https://letsencrypt.org/repository/"/>.
+        '';
+      };
+
+      useRoot = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to use the root user when generating certs. This is not recommended
+          for security + compatiblity reasons. If a service requires root owned certificates
+          consider following the guide on "Using ACME with services demanding root
+          owned certificates" in the NixOS manual, and only using this as a fallback
+          or for testing.
+        '';
+      };
+
+      defaults = mkOption {
+        type = types.submodule (inheritableModule true);
+        description = ''
+          Default values inheritable by all configured certs. You can
+          use this to define options shared by all your certs. These defaults
+          can also be ignored on a per-cert basis using the
+          `security.acme.certs.''${cert}.inheritDefaults' option.
+        '';
+      };
+
+      certs = mkOption {
+        default = { };
+        type = with types; attrsOf (submodule [ (inheritableModule false) certOpts ]);
+        description = ''
+          Attribute set of certificates to get signed and renewed. Creates
+          <literal>acme-''${cert}.{service,timer}</literal> systemd units for
+          each certificate defined here. Other services can add dependencies
+          to those units if they rely on the certificates being present,
+          or trigger restarts of the service if certificates get renewed.
+        '';
+        example = literalExpression ''
+          {
+            "example.com" = {
+              webroot = "/var/lib/acme/acme-challenge/";
+              email = "foo@example.com";
+              extraDomainNames = [ "www.example.com" "foo.example.com" ];
+            };
+            "bar.example.com" = {
+              webroot = "/var/lib/acme/acme-challenge/";
+              email = "bar@example.com";
+            };
+          }
+        '';
+      };
+    };
+  };
+
+  imports = [
+    (mkRemovedOptionModule [ "security" "acme" "production" ] ''
+      Use security.acme.server to define your staging ACME server URL instead.
+
+      To use the let's encrypt staging server, use security.acme.server =
+      "https://acme-staging-v02.api.letsencrypt.org/directory".
+    '')
+    (mkRemovedOptionModule [ "security" "acme" "directory" ] "ACME Directory is now hardcoded to /var/lib/acme and its permisisons are managed by systemd. See https://github.com/NixOS/nixpkgs/issues/53852 for more info.")
+    (mkRemovedOptionModule [ "security" "acme" "preDelay" ] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
+    (mkRemovedOptionModule [ "security" "acme" "activationDelay" ] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
+    (mkChangedOptionModule [ "security" "acme" "validMin" ] [ "security" "acme" "defaults" "validMinDays" ] (config: config.security.acme.validMin / (24 * 3600)))
+    (mkChangedOptionModule [ "security" "acme" "validMinDays" ] [ "security" "acme" "defaults" "validMinDays" ] (config: config.security.acme.validMinDays))
+    (mkChangedOptionModule [ "security" "acme" "renewInterval" ] [ "security" "acme" "defaults" "renewInterval" ] (config: config.security.acme.renewInterval))
+    (mkChangedOptionModule [ "security" "acme" "email" ] [ "security" "acme" "defaults" "email" ] (config: config.security.acme.email))
+    (mkChangedOptionModule [ "security" "acme" "server" ] [ "security" "acme" "defaults" "server" ] (config: config.security.acme.server))
+    (mkChangedOptionModule [ "security" "acme" "enableDebugLogs" ] [ "security" "acme" "defaults" "enableDebugLogs" ] (config: config.security.acme.enableDebugLogs))
+  ];
+
+  config = mkMerge [
+    (mkIf (cfg.certs != { }) {
+
+      # 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 ''
+        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);
+
+      assertions = let
+        certs = attrValues cfg.certs;
+      in [
+        {
+          assertion = cfg.email != null || all (certOpts: certOpts.email != null) certs;
+          message = ''
+            You must define `security.acme.certs.<name>.email` or
+            `security.acme.email` to register with the CA. Note that using
+            many different addresses for certs may trigger account rate limits.
+          '';
+        }
+        {
+          assertion = cfg.acceptTerms;
+          message = ''
+            You must accept the CA's terms of service before using
+            the ACME module by setting `security.acme.acceptTerms`
+            to `true`. For Let's Encrypt's ToS see https://letsencrypt.org/repository/
+          '';
+        }
+      ] ++ (builtins.concatLists (mapAttrsToList (cert: data: [
+        {
+          assertion = data.user == "_mkRemovedOptionModule";
+          message = ''
+            The option definition `security.acme.certs.${cert}.user' no longer has any effect; Please remove it.
+            Certificate user is now hard coded to the "acme" user. If you would
+            like another user to have access, consider adding them to the
+            "acme" group or changing security.acme.certs.${cert}.group.
+          '';
+        }
+        {
+          assertion = data.allowKeysForGroup == "_mkRemovedOptionModule";
+          message = ''
+            The option definition `security.acme.certs.${cert}.allowKeysForGroup' no longer has any effect; Please remove it.
+            All certs are readable by the configured group. If this is undesired,
+            consider changing security.acme.certs.${cert}.group to an unused group.
+          '';
+        }
+        # * in the cert value breaks building of systemd services, and makes
+        # referencing them as a user quite weird too. Best practice is to use
+        # the domain option.
+        {
+          assertion = ! hasInfix "*" cert;
+          message = ''
+            The cert option path `security.acme.certs.${cert}.dnsProvider`
+            cannot contain a * character.
+            Instead, set `security.acme.certs.${cert}.domain = "${cert}";`
+            and remove the wildcard from the path.
+          '';
+        }
+        {
+          assertion = data.dnsProvider == null || data.webroot == null;
+          message = ''
+            Options `security.acme.certs.${cert}.dnsProvider` and
+            `security.acme.certs.${cert}.webroot` are mutually exclusive.
+          '';
+        }
+        {
+          assertion = data.webroot == null || data.listenHTTP == null;
+          message = ''
+            Options `security.acme.certs.${cert}.webroot` and
+            `security.acme.certs.${cert}.listenHTTP` are mutually exclusive.
+          '';
+        }
+        {
+          assertion = data.listenHTTP == null || data.dnsProvider == null;
+          message = ''
+            Options `security.acme.certs.${cert}.listenHTTP` and
+            `security.acme.certs.${cert}.dnsProvider` are mutually exclusive.
+          '';
+        }
+        {
+          assertion = data.dnsProvider != null || data.webroot != null || data.listenHTTP != null;
+          message = ''
+            One of `security.acme.certs.${cert}.dnsProvider`,
+            `security.acme.certs.${cert}.webroot`, or
+            `security.acme.certs.${cert}.listenHTTP` must be provided.
+          '';
+        }
+      ]) cfg.certs));
+
+      users.users.acme = {
+        home = "/var/lib/acme";
+        group = "acme";
+        isSystemUser = true;
+      };
+
+      users.groups.acme = {};
+
+      systemd.services = {
+        "acme-fixperms" = userMigrationService;
+      } // (mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewService) certConfigs)
+        // (optionalAttrs (cfg.preliminarySelfsigned) ({
+        "acme-selfsigned-ca" = selfsignCAService;
+      } // (mapAttrs' (cert: conf: nameValuePair "acme-selfsigned-${cert}" conf.selfsignService) certConfigs)));
+
+      systemd.timers = mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewTimer) certConfigs;
+
+      systemd.targets = let
+        # Create some targets which can be depended on to be "active" after cert renewals
+        finishedTargets = mapAttrs' (cert: conf: nameValuePair "acme-finished-${cert}" {
+          wantedBy = [ "default.target" ];
+          requires = [ "acme-${cert}.service" ];
+          after = [ "acme-${cert}.service" ];
+        }) certConfigs;
+
+        # Create targets to limit the number of simultaneous account creations
+        # How it works:
+        # - Pick a "leader" cert service, which will be in charge of creating the account,
+        #   and run first (requires + after)
+        # - Make all other cert services sharing the same account wait for the leader to
+        #   finish before starting (requiredBy + before).
+        # Using a target here is fine - account creation is a one time event. Even if
+        # systemd clean --what=state is used to delete the account, so long as the user
+        # then runs one of the cert services, there won't be any issues.
+        accountTargets = mapAttrs' (hash: confs: let
+          leader = "acme-${(builtins.head confs).cert}.service";
+          dependantServices = map (conf: "acme-${conf.cert}.service") (builtins.tail confs);
+        in nameValuePair "acme-account-${hash}" {
+          requiredBy = dependantServices;
+          before = dependantServices;
+          requires = [ leader ];
+          after = [ leader ];
+        }) (groupBy (conf: conf.accountHash) (attrValues certConfigs));
+      in finishedTargets // accountTargets;
+    })
+  ];
+
+  meta = {
+    maintainers = lib.teams.acme.members;
+    doc = ./doc.xml;
+  };
+}
diff --git a/nixos/modules/security/acme/doc.xml b/nixos/modules/security/acme/doc.xml
new file mode 100644
index 00000000000..f623cc509be
--- /dev/null
+++ b/nixos/modules/security/acme/doc.xml
@@ -0,0 +1,413 @@
+<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.
+    <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
+    tsig-keygen rfc2136key.example.com &gt; /var/lib/secrets/dnskeys.conf
+    chown named:root /var/lib/secrets/dnskeys.conf
+    chmod 400 /var/lib/secrets/dnskeys.conf
+
+    # Copy the secret value from the dnskeys.conf, and put it in
+    # RFC2136_TSIG_SECRET below
+
+    cat &gt; /var/lib/secrets/certs.secret &lt;&lt; EOF
+    RFC2136_NAMESERVER='127.0.0.1:53'
+    RFC2136_TSIG_ALGORITHM='hmac-sha256.'
+    RFC2136_TSIG_KEY='rfc2136key.example.com'
+    RFC2136_TSIG_SECRET='your secret key'
+    EOF
+    chmod 400 /var/lib/secrets/certs.secret
+  '';
+};
+</programlisting>
+
+  <para>
+   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/acme/mk-cert-ownership-assertion.nix b/nixos/modules/security/acme/mk-cert-ownership-assertion.nix
new file mode 100644
index 00000000000..b80d89aeb9f
--- /dev/null
+++ b/nixos/modules/security/acme/mk-cert-ownership-assertion.nix
@@ -0,0 +1,4 @@
+{ cert, group, groups, user }: {
+  assertion = cert.group == group || builtins.any (u: u == user) groups.${cert.group}.members;
+  message = "Group for certificate ${cert.domain} must be ${group}, or user ${user} must be a member of group ${cert.group}";
+}
diff --git a/nixos/modules/security/apparmor.nix b/nixos/modules/security/apparmor.nix
new file mode 100644
index 00000000000..be1b0362fc1
--- /dev/null
+++ b/nixos/modules/security/apparmor.nix
@@ -0,0 +1,216 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  inherit (builtins) attrNames head map match readFile;
+  inherit (lib) types;
+  inherit (config.environment) etc;
+  cfg = config.security.apparmor;
+  mkDisableOption = name: mkEnableOption name // {
+    default = true;
+    example = false;
+  };
+  enabledPolicies = filterAttrs (n: p: p.enable) cfg.policies;
+in
+
+{
+  imports = [
+    (mkRemovedOptionModule [ "security" "apparmor" "confineSUIDApplications" ] "Please use the new options: `security.apparmor.policies.<policy>.enable'.")
+    (mkRemovedOptionModule [ "security" "apparmor" "profiles" ] "Please use the new option: `security.apparmor.policies'.")
+    apparmor/includes.nix
+    apparmor/profiles.nix
+  ];
+
+  options = {
+    security.apparmor = {
+      enable = mkEnableOption ''
+        the AppArmor Mandatory Access Control system.
+
+        If you're enabling this module on a running system,
+        note that a reboot will be required to activate AppArmor in the kernel.
+
+        Also, beware that enabling this module privileges stability over security
+        by not trying to kill unconfined but newly confinable running processes by default,
+        though it would be needed because AppArmor can only confine new
+        or already confined processes of an executable.
+        This killing would for instance be necessary when upgrading to a NixOS revision
+        introducing for the first time an AppArmor profile for the executable
+        of a running process.
+
+        Enable <xref linkend="opt-security.apparmor.killUnconfinedConfinables"/>
+        if you want this service to do such killing
+        by sending a <literal>SIGTERM</literal> to those running processes'';
+      policies = mkOption {
+        description = ''
+          AppArmor policies.
+        '';
+        type = types.attrsOf (types.submodule ({ name, config, ... }: {
+          options = {
+            enable = mkDisableOption "loading of the profile into the kernel";
+            enforce = mkDisableOption "enforcing of the policy or only complain in the logs";
+            profile = mkOption {
+              description = "The policy of the profile.";
+              type = types.lines;
+              apply = pkgs.writeText name;
+            };
+          };
+        }));
+        default = {};
+      };
+      includes = mkOption {
+        type = types.attrsOf types.lines;
+        default = {};
+        description = ''
+          List of paths to be added to AppArmor's searched paths
+          when resolving <literal>include</literal> directives.
+        '';
+        apply = mapAttrs pkgs.writeText;
+      };
+      packages = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        description = "List of packages to be added to AppArmor's include path";
+      };
+      enableCache = mkEnableOption ''
+        caching of AppArmor policies
+        in <literal>/var/cache/apparmor/</literal>.
+
+        Beware that AppArmor policies almost always contain Nix store paths,
+        and thus produce at each change of these paths
+        a new cached version accumulating in the cache'';
+      killUnconfinedConfinables = mkEnableOption ''
+        killing of processes which have an AppArmor profile enabled
+        (in <xref linkend="opt-security.apparmor.policies"/>)
+        but are not confined (because AppArmor can only confine new processes).
+
+        This is only sending a gracious <literal>SIGTERM</literal> signal to the processes,
+        not a <literal>SIGKILL</literal>.
+
+        Beware that due to a current limitation of AppArmor,
+        only profiles with exact paths (and no name) can enable such kills'';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = map (policy:
+      { assertion = match ".*/.*" policy == null;
+        message = "`security.apparmor.policies.\"${policy}\"' must not contain a slash.";
+        # Because, for instance, aa-remove-unknown uses profiles_names_list() in rc.apparmor.functions
+        # which does not recurse into sub-directories.
+      }
+    ) (attrNames cfg.policies);
+
+    environment.systemPackages = [
+      pkgs.apparmor-utils
+      pkgs.apparmor-bin-utils
+    ];
+    environment.etc."apparmor.d".source = pkgs.linkFarm "apparmor.d" (
+      # It's important to put only enabledPolicies here and not all cfg.policies
+      # because aa-remove-unknown reads profiles from all /etc/apparmor.d/*
+      mapAttrsToList (name: p: { inherit name; path = p.profile; }) enabledPolicies ++
+      mapAttrsToList (name: path: { inherit name path; }) cfg.includes
+    );
+    environment.etc."apparmor/parser.conf".text = ''
+        ${if cfg.enableCache then "write-cache" else "skip-cache"}
+        cache-loc /var/cache/apparmor
+        Include /etc/apparmor.d
+      '' +
+      concatMapStrings (p: "Include ${p}/etc/apparmor.d\n") cfg.packages;
+    # For aa-logprof
+    environment.etc."apparmor/apparmor.conf".text = ''
+    '';
+    # For aa-logprof
+    environment.etc."apparmor/severity.db".source = pkgs.apparmor-utils + "/etc/apparmor/severity.db";
+    environment.etc."apparmor/logprof.conf".source = pkgs.runCommand "logprof.conf" {
+      header = ''
+        [settings]
+          # /etc/apparmor.d/ is read-only on NixOS
+          profiledir = /var/cache/apparmor/logprof
+          inactive_profiledir = /etc/apparmor.d/disable
+          # Use: journalctl -b --since today --grep audit: | aa-logprof
+          logfiles = /dev/stdin
+
+          parser = ${pkgs.apparmor-parser}/bin/apparmor_parser
+          ldd = ${pkgs.glibc.bin}/bin/ldd
+          logger = ${pkgs.util-linux}/bin/logger
+
+          # customize how file ownership permissions are presented
+          # 0 - off
+          # 1 - default of what ever mode the log reported
+          # 2 - force the new permissions to be user
+          # 3 - force all perms on the rule to be user
+          default_owner_prompt = 1
+
+          custom_includes = /etc/apparmor.d ${concatMapStringsSep " " (p: "${p}/etc/apparmor.d") cfg.packages}
+
+        [qualifiers]
+          ${pkgs.runtimeShell} = icnu
+          ${pkgs.bashInteractive}/bin/sh = icnu
+          ${pkgs.bashInteractive}/bin/bash = icnu
+          ${config.users.defaultUserShell} = icnu
+      '';
+      footer = "${pkgs.apparmor-utils}/etc/apparmor/logprof.conf";
+      passAsFile = [ "header" ];
+    } ''
+      cp $headerPath $out
+      sed '1,/\[qualifiers\]/d' $footer >> $out
+    '';
+
+    boot.kernelParams = [ "apparmor=1" "security=apparmor" ];
+
+    systemd.services.apparmor = {
+      after = [
+        "local-fs.target"
+        "systemd-journald-audit.socket"
+      ];
+      before = [ "sysinit.target" ];
+      wantedBy = [ "multi-user.target" ];
+      unitConfig = {
+        Description="Load AppArmor policies";
+        DefaultDependencies = "no";
+        ConditionSecurity = "apparmor";
+      };
+      # Reloading instead of restarting enables to load new AppArmor profiles
+      # without necessarily restarting all services which have Requires=apparmor.service
+      reloadIfChanged = true;
+      restartTriggers = [
+        etc."apparmor/parser.conf".source
+        etc."apparmor.d".source
+      ];
+      serviceConfig = let
+        killUnconfinedConfinables = pkgs.writeShellScript "apparmor-kill" ''
+          set -eu
+          ${pkgs.apparmor-bin-utils}/bin/aa-status --json |
+          ${pkgs.jq}/bin/jq --raw-output '.processes | .[] | .[] | select (.status == "unconfined") | .pid' |
+          xargs --verbose --no-run-if-empty --delimiter='\n' \
+          kill
+        '';
+        commonOpts = p: "--verbose --show-cache ${optionalString (!p.enforce) "--complain "}${p.profile}";
+        in {
+        Type = "oneshot";
+        RemainAfterExit = "yes";
+        ExecStartPre = "${pkgs.apparmor-utils}/bin/aa-teardown";
+        ExecStart = mapAttrsToList (n: p: "${pkgs.apparmor-parser}/bin/apparmor_parser --add ${commonOpts p}") enabledPolicies;
+        ExecStartPost = optional cfg.killUnconfinedConfinables killUnconfinedConfinables;
+        ExecReload =
+          # Add or replace into the kernel profiles in enabledPolicies
+          # (because AppArmor can do that without stopping the processes already confined).
+          mapAttrsToList (n: p: "${pkgs.apparmor-parser}/bin/apparmor_parser --replace ${commonOpts p}") enabledPolicies ++
+          # Remove from the kernel any profile whose name is not
+          # one of the names within the content of the profiles in enabledPolicies
+          # (indirectly read from /etc/apparmor.d/*, without recursing into sub-directory).
+          # Note that this does not remove profiles dynamically generated by libvirt.
+          [ "${pkgs.apparmor-utils}/bin/aa-remove-unknown" ] ++
+          # Optionaly kill the processes which are unconfined but now have a profile loaded
+          # (because AppArmor can only start to confine new processes).
+          optional cfg.killUnconfinedConfinables killUnconfinedConfinables;
+        ExecStop = "${pkgs.apparmor-utils}/bin/aa-teardown";
+        CacheDirectory = [ "apparmor" "apparmor/logprof" ];
+        CacheDirectoryMode = "0700";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ julm ];
+}
diff --git a/nixos/modules/security/apparmor/includes.nix b/nixos/modules/security/apparmor/includes.nix
new file mode 100644
index 00000000000..f290e95a296
--- /dev/null
+++ b/nixos/modules/security/apparmor/includes.nix
@@ -0,0 +1,317 @@
+{ config, lib, pkgs, ... }:
+let
+  inherit (builtins) attrNames hasAttr isAttrs;
+  inherit (lib) getLib;
+  inherit (config.environment) etc;
+  # Utility to generate an AppArmor rule
+  # only when the given path exists in config.environment.etc
+  etcRule = arg:
+    let go = { path ? null, mode ? "r", trail ? "" }:
+      lib.optionalString (hasAttr path etc)
+        "${mode} ${config.environment.etc.${path}.source}${trail},";
+    in if isAttrs arg
+    then go arg
+    else go { path = arg; };
+in
+{
+# FIXME: most of the etcRule calls below have been
+# written systematically by converting from apparmor-profiles's profiles
+# without testing nor deep understanding of their uses,
+# and thus may need more rules or can have less rules;
+# this remains to be determined case by case,
+# some may even be completely useless.
+config.security.apparmor.includes = {
+  # This one is included by <tunables/global>
+  # which is usualy included before any profile.
+  "abstractions/tunables/alias" = ''
+    alias /bin -> /run/current-system/sw/bin,
+    alias /lib/modules -> /run/current-system/kernel/lib/modules,
+    alias /sbin -> /run/current-system/sw/sbin,
+    alias /usr -> /run/current-system/sw,
+  '';
+  "abstractions/audio" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/audio"
+    '' + lib.concatMapStringsSep "\n" etcRule [
+      "asound.conf"
+      "esound/esd.conf"
+      "libao.conf"
+      { path = "pulse";  trail = "/"; }
+      { path = "pulse";  trail = "/**"; }
+      { path = "sound";  trail = "/"; }
+      { path = "sound";  trail = "/**"; }
+      { path = "alsa/conf.d";  trail = "/"; }
+      { path = "alsa/conf.d";  trail = "/*"; }
+      "openal/alsoft.conf"
+      "wildmidi/wildmidi.conf"
+    ];
+  "abstractions/authentication" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/authentication"
+    # Defined in security.pam
+    include <abstractions/pam>
+    '' + lib.concatMapStringsSep "\n" etcRule [
+      "nologin"
+      "securetty"
+      { path = "security";  trail = "/*"; }
+      "shadow"
+      "gshadow"
+      "pwdb.conf"
+      "default/passwd"
+      "login.defs"
+    ];
+  "abstractions/base" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/base"
+    r ${pkgs.stdenv.cc.libc}/share/locale/**,
+    r ${pkgs.stdenv.cc.libc}/share/locale.alias,
+    ${lib.optionalString (pkgs.glibcLocales != null) "r ${pkgs.glibcLocales}/lib/locale/locale-archive,"}
+    ${etcRule "localtime"}
+    r ${pkgs.tzdata}/share/zoneinfo/**,
+    r ${pkgs.stdenv.cc.libc}/share/i18n/**,
+  '';
+  "abstractions/bash" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/bash"
+
+    # bash inspects filesystems at startup
+    # and /etc/mtab is linked to /proc/mounts
+    @{PROC}/mounts
+
+    # system-wide bash configuration
+    '' + lib.concatMapStringsSep "\n" etcRule [
+      "profile.dos"
+      "profile"
+      "profile.d"
+      { path = "profile.d";  trail = "/*"; }
+      "bashrc"
+      "bash.bashrc"
+      "bash.bashrc.local"
+      "bash_completion"
+      "bash_completion.d"
+      { path = "bash_completion.d";  trail = "/*"; }
+      # bash relies on system-wide readline configuration
+      "inputrc"
+      # run out of /etc/bash.bashrc
+      "DIR_COLORS"
+    ];
+  "abstractions/consoles" = ''
+     include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/consoles"
+  '';
+  "abstractions/cups-client" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/cups-client"
+    ${etcRule "cups/cups-client.conf"}
+  '';
+  "abstractions/dbus-session-strict" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/dbus-session-strict"
+    ${etcRule "machine-id"}
+  '';
+  "abstractions/dconf" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/dconf"
+    ${etcRule { path = "dconf";  trail = "/**"; }}
+  '';
+  "abstractions/dri-common" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/dri-common"
+    ${etcRule "drirc"}
+  '';
+  # The config.fonts.fontconfig NixOS module adds many files to /etc/fonts/
+  # by symlinking them but without exporting them outside of its NixOS module,
+  # those are therefore added there to this "abstractions/fonts".
+  "abstractions/fonts" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/fonts"
+    ${etcRule { path = "fonts";  trail = "/**"; }}
+  '';
+  "abstractions/gnome" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/gnome"
+    include <abstractions/fonts>
+    '' + lib.concatMapStringsSep "\n" etcRule [
+      { path = "gnome";  trail = "/gtkrc*"; }
+      { path = "gtk";  trail = "/*"; }
+      { path = "gtk-2.0";  trail = "/*"; }
+      { path = "gtk-3.0";  trail = "/*"; }
+      "orbitrc"
+      { path = "pango";  trail = "/*"; }
+      { path = "/etc/gnome-vfs-2.0";  trail = "/modules/"; }
+      { path = "/etc/gnome-vfs-2.0";  trail = "/modules/*"; }
+      "papersize"
+      { path = "cups";  trail = "/lpoptions"; }
+      { path = "gnome";  trail = "/defaults.list"; }
+      { path = "xdg";  trail = "/{,*-}mimeapps.list"; }
+      "xdg/mimeapps.list"
+    ];
+  "abstractions/kde" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/kde"
+    '' + lib.concatMapStringsSep "\n" etcRule [
+      { path = "qt3";  trail = "/kstylerc"; }
+      { path = "qt3";  trail = "/qt_plugins_3.3rc"; }
+      { path = "qt3";  trail = "/qtrc"; }
+      "kderc"
+      { path = "kde3";  trail = "/*"; }
+      "kde4rc"
+      { path = "xdg";  trail = "/kdeglobals"; }
+      { path = "xdg";  trail = "/Trolltech.conf"; }
+    ];
+  "abstractions/kerberosclient" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/kerberosclient"
+    '' + lib.concatMapStringsSep "\n" etcRule [
+    { path = "krb5.keytab"; mode="rk"; }
+    "krb5.conf"
+    "krb5.conf.d"
+    { path = "krb5.conf.d";  trail = "/*"; }
+
+    # config files found via strings on libs
+    "krb.conf"
+    "krb.realms"
+    "srvtab"
+    ];
+  "abstractions/ldapclient" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/ldapclient"
+    '' + lib.concatMapStringsSep "\n" etcRule [
+      "ldap.conf"
+      "ldap.secret"
+      { path = "openldap";  trail = "/*"; }
+      { path = "openldap";  trail = "/cacerts/*"; }
+      { path = "sasl2";  trail = "/*"; }
+    ];
+  "abstractions/likewise" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/likewise"
+  '';
+  "abstractions/mdns" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/mdns"
+    ${etcRule "nss_mdns.conf"}
+  '';
+  "abstractions/nameservice" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/nameservice"
+
+    # Many programs wish to perform nameservice-like operations, such as
+    # looking up users by name or id, groups by name or id, hosts by name
+    # or IP, etc. These operations may be performed through files, dns,
+    # NIS, NIS+, LDAP, hesiod, wins, etc. Allow them all here.
+    mr ${getLib pkgs.nss}/lib/libnss_*.so*,
+    mr ${getLib pkgs.nss}/lib64/libnss_*.so*,
+    '' + lib.concatMapStringsSep "\n" etcRule [
+      "group"
+      "host.conf"
+      "hosts"
+      "nsswitch.conf"
+      "gai.conf"
+      "passwd"
+      "protocols"
+
+      # libtirpc (used for NIS/YP login) needs this
+      "netconfig"
+
+      "resolv.conf"
+
+      { path = "samba";  trail = "/lmhosts"; }
+      "services"
+
+      "default/nss"
+
+      # libnl-3-200 via libnss-gw-name
+      { path = "libnl";  trail = "/classid"; }
+      { path = "libnl-3";  trail = "/classid"; }
+    ];
+  "abstractions/nis" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/nis"
+  '';
+  "abstractions/nvidia" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/nvidia"
+    ${etcRule "vdpau_wrapper.cfg"}
+  '';
+  "abstractions/opencl-common" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/opencl-common"
+    ${etcRule { path = "OpenCL";  trail = "/**"; }}
+  '';
+  "abstractions/opencl-mesa" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/opencl-mesa"
+    ${etcRule "default/drirc"}
+  '';
+  "abstractions/openssl" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/openssl"
+    ${etcRule { path = "ssl";  trail = "/openssl.cnf"; }}
+  '';
+  "abstractions/p11-kit" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/p11-kit"
+    '' + lib.concatMapStringsSep "\n" etcRule [
+      { path = "pkcs11";  trail = "/"; }
+      { path = "pkcs11";  trail = "/pkcs11.conf"; }
+      { path = "pkcs11";  trail = "/modules/"; }
+      { path = "pkcs11";  trail = "/modules/*"; }
+    ];
+  "abstractions/perl" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/perl"
+    ${etcRule { path = "perl";  trail = "/**"; }}
+  '';
+  "abstractions/php" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/php"
+    '' + lib.concatMapStringsSep "\n" etcRule [
+      { path = "php";  trail = "/**/"; }
+      { path = "php5";  trail = "/**/"; }
+      { path = "php7";  trail = "/**/"; }
+      { path = "php";  trail = "/**.ini"; }
+      { path = "php5";  trail = "/**.ini"; }
+      { path = "php7";  trail = "/**.ini"; }
+    ];
+  "abstractions/postfix-common" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/postfix-common"
+    '' + lib.concatMapStringsSep "\n" etcRule [
+      "mailname"
+      { path = "postfix";  trail = "/*.cf"; }
+      "postfix/main.cf"
+      "postfix/master.cf"
+    ];
+  "abstractions/python" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/python"
+  '';
+  "abstractions/qt5" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/qt5"
+    '' + lib.concatMapStringsSep "\n" etcRule [
+      { path = "xdg";  trail = "/QtProject/qtlogging.ini"; }
+      { path = "xdg/QtProject";  trail = "/qtlogging.ini"; }
+      "xdg/QtProject/qtlogging.ini"
+    ];
+  "abstractions/samba" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/samba"
+    ${etcRule { path = "samba";  trail = "/*"; }}
+  '';
+  "abstractions/ssl_certs" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/ssl_certs"
+
+    # For the NixOS module: security.acme
+    r /var/lib/acme/*/cert.pem,
+    r /var/lib/acme/*/chain.pem,
+    r /var/lib/acme/*/fullchain.pem,
+
+    '' + lib.concatMapStringsSep "\n" etcRule [
+      "ssl/certs/ca-certificates.crt"
+      "ssl/certs/ca-bundle.crt"
+      "pki/tls/certs/ca-bundle.crt"
+
+      { path = "ssl/trust";  trail = "/"; }
+      { path = "ssl/trust";  trail = "/*"; }
+      { path = "ssl/trust/anchors";  trail = "/"; }
+      { path = "ssl/trust/anchors";  trail = "/**"; }
+      { path = "pki/trust";  trail = "/"; }
+      { path = "pki/trust";  trail = "/*"; }
+      { path = "pki/trust/anchors";  trail = "/"; }
+      { path = "pki/trust/anchors";  trail = "/**"; }
+    ];
+  "abstractions/ssl_keys" = ''
+    # security.acme NixOS module
+    r /var/lib/acme/*/full.pem,
+    r /var/lib/acme/*/key.pem,
+  '';
+  "abstractions/vulkan" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/vulkan"
+    ${etcRule { path = "vulkan/icd.d";  trail = "/"; }}
+    ${etcRule { path = "vulkan/icd.d";  trail = "/*.json"; }}
+  '';
+  "abstractions/winbind" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/winbind"
+    ${etcRule { path = "samba";  trail = "/smb.conf"; }}
+    ${etcRule { path = "samba";  trail = "/dhcp.conf"; }}
+  '';
+  "abstractions/X" = ''
+    include "${pkgs.apparmor-profiles}/etc/apparmor.d/abstractions/X"
+    ${etcRule { path = "X11/cursors";  trail = "/"; }}
+    ${etcRule { path = "X11/cursors";  trail = "/**"; }}
+  '';
+};
+}
diff --git a/nixos/modules/security/apparmor/profiles.nix b/nixos/modules/security/apparmor/profiles.nix
new file mode 100644
index 00000000000..8eb630b5a48
--- /dev/null
+++ b/nixos/modules/security/apparmor/profiles.nix
@@ -0,0 +1,11 @@
+{ config, lib, pkgs, ... }:
+let apparmor = config.security.apparmor; in
+{
+config.security.apparmor.packages = [ pkgs.apparmor-profiles ];
+config.security.apparmor.policies."bin.ping".profile = lib.mkIf apparmor.policies."bin.ping".enable ''
+  include "${pkgs.iputils.apparmor}/bin.ping"
+  include "${pkgs.inetutils.apparmor}/bin.ping"
+  # Note that including those two profiles in the same profile
+  # would not work if the second one were to re-include <tunables/global>.
+'';
+}
diff --git a/nixos/modules/security/audit.nix b/nixos/modules/security/audit.nix
new file mode 100644
index 00000000000..2b22bdd9f0a
--- /dev/null
+++ b/nixos/modules/security/audit.nix
@@ -0,0 +1,123 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.security.audit;
+  enabled = cfg.enable == "lock" || cfg.enable;
+
+  failureModes = {
+    silent = 0;
+    printk = 1;
+    panic  = 2;
+  };
+
+  disableScript = pkgs.writeScript "audit-disable" ''
+    #!${pkgs.runtimeShell} -eu
+    # Explicitly disable everything, as otherwise journald might start it.
+    auditctl -D
+    auditctl -e 0 -a task,never
+  '';
+
+  # TODO: it seems like people like their rules to be somewhat secret, yet they will not be if
+  # put in the store like this. At the same time, it doesn't feel like a huge deal and working
+  # around that is a pain so I'm leaving it like this for now.
+  startScript = pkgs.writeScript "audit-start" ''
+    #!${pkgs.runtimeShell} -eu
+    # Clear out any rules we may start with
+    auditctl -D
+
+    # Put the rules in a temporary file owned and only readable by root
+    rulesfile="$(mktemp)"
+    ${concatMapStrings (x: "echo '${x}' >> $rulesfile\n") cfg.rules}
+
+    # Apply the requested rules
+    auditctl -R "$rulesfile"
+
+    # Enable and configure auditing
+    auditctl \
+      -e ${if cfg.enable == "lock" then "2" else "1"} \
+      -b ${toString cfg.backlogLimit} \
+      -f ${toString failureModes.${cfg.failureMode}} \
+      -r ${toString cfg.rateLimit}
+  '';
+
+  stopScript = pkgs.writeScript "audit-stop" ''
+    #!${pkgs.runtimeShell} -eu
+    # Clear the rules
+    auditctl -D
+
+    # Disable auditing
+    auditctl -e 0
+  '';
+in {
+  options = {
+    security.audit = {
+      enable = mkOption {
+        type        = types.enum [ false true "lock" ];
+        default     = false;
+        description = ''
+          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.
+        '';
+      };
+
+      failureMode = mkOption {
+        type        = types.enum [ "silent" "printk" "panic" ];
+        default     = "printk";
+        description = "How to handle critical errors in the auditing system";
+      };
+
+      backlogLimit = mkOption {
+        type        = types.int;
+        default     = 64; # Apparently the kernel default
+        description = ''
+          The maximum number of outstanding audit buffers allowed; exceeding this is
+          considered a failure and handled in a manner specified by failureMode.
+        '';
+      };
+
+      rateLimit = mkOption {
+        type        = types.int;
+        default     = 0;
+        description = ''
+          The maximum messages per second permitted before triggering a failure as
+          specified by failureMode. Setting it to zero disables the limit.
+        '';
+      };
+
+      rules = mkOption {
+        type        = types.listOf types.str; # (types.either types.str (types.submodule rule));
+        default     = [];
+        example     = [ "-a exit,always -F arch=b64 -S execve" ];
+        description = ''
+          The ordered audit rules, with each string appearing as one line of the audit.rules file.
+        '';
+      };
+    };
+  };
+
+  config = {
+    systemd.services.audit = {
+      description = "Kernel Auditing";
+      wantedBy = [ "basic.target" ];
+
+      unitConfig = {
+        ConditionVirtualization = "!container";
+        ConditionSecurity = [ "audit" ];
+      };
+
+
+      path = [ pkgs.audit ];
+
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStart = "@${if enabled then startScript else disableScript} audit-start";
+        ExecStop  = "@${stopScript} audit-stop";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/security/auditd.nix b/nixos/modules/security/auditd.nix
new file mode 100644
index 00000000000..9d26cfbcfb1
--- /dev/null
+++ b/nixos/modules/security/auditd.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  options.security.auditd.enable = mkEnableOption "the Linux Audit daemon";
+
+  config = mkIf config.security.auditd.enable {
+    boot.kernelParams = [ "audit=1" ];
+
+    environment.systemPackages = [ pkgs.audit ];
+
+    systemd.services.auditd = {
+      description = "Linux Audit daemon";
+      wantedBy = [ "basic.target" ];
+
+      unitConfig = {
+        ConditionVirtualization = "!container";
+        ConditionSecurity = [ "audit" ];
+        DefaultDependencies = false;
+      };
+
+      path = [ pkgs.audit ];
+
+      serviceConfig = {
+        ExecStartPre="${pkgs.coreutils}/bin/mkdir -p /var/log/audit";
+        ExecStart = "${pkgs.audit}/bin/auditd -l -n -s nochange";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/security/ca.nix b/nixos/modules/security/ca.nix
new file mode 100644
index 00000000000..f71d9d90ec5
--- /dev/null
+++ b/nixos/modules/security/ca.nix
@@ -0,0 +1,89 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.security.pki;
+
+  cacertPackage = pkgs.cacert.override {
+    blacklist = cfg.caCertificateBlacklist;
+    extraCertificateFiles = cfg.certificateFiles;
+    extraCertificateStrings = cfg.certificates;
+  };
+  caBundle = "${cacertPackage}/etc/ssl/certs/ca-bundle.crt";
+
+in
+
+{
+
+  options = {
+
+    security.pki.certificateFiles = mkOption {
+      type = types.listOf types.path;
+      default = [];
+      example = literalExpression ''[ "''${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" ]'';
+      description = ''
+        A list of files containing trusted root certificates in PEM
+        format. These are concatenated to form
+        <filename>/etc/ssl/certs/ca-certificates.crt</filename>, which is
+        used by many programs that use OpenSSL, such as
+        <command>curl</command> and <command>git</command>.
+      '';
+    };
+
+    security.pki.certificates = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = literalExpression ''
+        [ '''
+            NixOS.org
+            =========
+            -----BEGIN CERTIFICATE-----
+            MIIGUDCCBTigAwIBAgIDD8KWMA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ
+            TDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0
+            ...
+            -----END CERTIFICATE-----
+          '''
+        ]
+      '';
+      description = ''
+        A list of trusted root certificates in PEM format.
+      '';
+    };
+
+    security.pki.caCertificateBlacklist = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [
+        "WoSign" "WoSign China"
+        "CA WoSign ECC Root"
+        "Certification Authority of WoSign G2"
+      ];
+      description = ''
+        A list of blacklisted CA certificate names that won't be imported from
+        the Mozilla Trust Store into
+        <filename>/etc/ssl/certs/ca-certificates.crt</filename>. Use the
+        names from that file.
+      '';
+    };
+
+  };
+
+  config = {
+
+    # NixOS canonical location + Debian/Ubuntu/Arch/Gentoo compatibility.
+    environment.etc."ssl/certs/ca-certificates.crt".source = caBundle;
+
+    # Old NixOS compatibility.
+    environment.etc."ssl/certs/ca-bundle.crt".source = caBundle;
+
+    # CentOS/Fedora compatibility.
+    environment.etc."pki/tls/certs/ca-bundle.crt".source = caBundle;
+
+    # P11-Kit trust source.
+    environment.etc."ssl/trust-source".source = "${cacertPackage.p11kit}/etc/ssl/trust-source";
+
+  };
+
+}
diff --git a/nixos/modules/security/chromium-suid-sandbox.nix b/nixos/modules/security/chromium-suid-sandbox.nix
new file mode 100644
index 00000000000..bb99c053f71
--- /dev/null
+++ b/nixos/modules/security/chromium-suid-sandbox.nix
@@ -0,0 +1,38 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg     = config.security.chromiumSuidSandbox;
+  sandbox = pkgs.chromium.sandbox;
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "programs" "unity3d" "enable" ] [ "security" "chromiumSuidSandbox" "enable" ])
+  ];
+
+  options.security.chromiumSuidSandbox.enable = mkOption {
+    type = types.bool;
+    default = false;
+    description = ''
+      Whether to install the Chromium SUID sandbox which is an executable that
+      Chromium may use in order to achieve sandboxing.
+
+      If you get the error "The SUID sandbox helper binary was found, but is not
+      configured correctly.", turning this on might help.
+
+      Also, if the URL chrome://sandbox tells you that "You are not adequately
+      sandboxed!", turning this on might resolve the issue.
+    '';
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ sandbox ];
+    security.wrappers.${sandbox.passthru.sandboxExecutableName} =
+      { setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${sandbox}/bin/${sandbox.passthru.sandboxExecutableName}";
+      };
+  };
+}
diff --git a/nixos/modules/security/dhparams.nix b/nixos/modules/security/dhparams.nix
new file mode 100644
index 00000000000..cfa9003f12f
--- /dev/null
+++ b/nixos/modules/security/dhparams.nix
@@ -0,0 +1,177 @@
+{ config, lib, options, pkgs, ... }:
+
+let
+  inherit (lib) literalExpression mkOption types;
+  cfg = config.security.dhparams;
+  opt = options.security.dhparams;
+
+  bitType = types.addCheck types.int (b: b >= 16) // {
+    name = "bits";
+    description = "integer of at least 16 bits";
+  };
+
+  paramsSubmodule = { name, config, ... }: {
+    options.bits = mkOption {
+      type = bitType;
+      default = cfg.defaultBitSize;
+      defaultText = literalExpression "config.${opt.defaultBitSize}";
+      description = ''
+        The bit size for the prime that is used during a Diffie-Hellman
+        key exchange.
+      '';
+    };
+
+    options.path = mkOption {
+      type = types.path;
+      readOnly = true;
+      description = ''
+        The resulting path of the generated Diffie-Hellman parameters
+        file for other services to reference. This could be either a
+        store path or a file inside the directory specified by
+        <option>security.dhparams.path</option>.
+      '';
+    };
+
+    config.path = let
+      generated = pkgs.runCommand "dhparams-${name}.pem" {
+        nativeBuildInputs = [ pkgs.openssl ];
+      } "openssl dhparam -out \"$out\" ${toString config.bits}";
+    in if cfg.stateful then "${cfg.path}/${name}.pem" else generated;
+  };
+
+in {
+  options = {
+    security.dhparams = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to generate new DH params and clean up old DH params.
+        '';
+      };
+
+      params = mkOption {
+        type = with types; let
+          coerce = bits: { inherit bits; };
+        in attrsOf (coercedTo int coerce (submodule paramsSubmodule));
+        default = {};
+        example = lib.literalExpression "{ nginx.bits = 3072; }";
+        description = ''
+          Diffie-Hellman parameters to generate.
+
+          The value is the size (in bits) of the DH params to generate. The
+          generated DH params path can be found in
+          <literal>config.security.dhparams.params.<replaceable>name</replaceable>.path</literal>.
+
+          <note><para>The name of the DH params is taken as being the name of
+          the service it serves and the params will be generated before the
+          said service is started.</para></note>
+
+          <warning><para>If you are removing all dhparams from this list, you
+          have to leave <option>security.dhparams.enable</option> for at
+          least one activation in order to have them be cleaned up. This also
+          means if you rollback to a version without any dhparams the
+          existing ones won't be cleaned up. Of course this only applies if
+          <option>security.dhparams.stateful</option> is
+          <literal>true</literal>.</para></warning>
+
+          <note><title>For module implementers:</title><para>It's recommended
+          to not set a specific bit size here, so that users can easily
+          override this by setting
+          <option>security.dhparams.defaultBitSize</option>.</para></note>
+        '';
+      };
+
+      stateful = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether generation of Diffie-Hellman parameters should be stateful or
+          not. If this is enabled, PEM-encoded files for Diffie-Hellman
+          parameters are placed in the directory specified by
+          <option>security.dhparams.path</option>. Otherwise the files are
+          created within the Nix store.
+
+          <note><para>If this is <literal>false</literal> the resulting store
+          path will be non-deterministic and will be rebuilt every time the
+          <package>openssl</package> package changes.</para></note>
+        '';
+      };
+
+      defaultBitSize = mkOption {
+        type = bitType;
+        default = 2048;
+        description = ''
+          This allows to override the default bit size for all of the
+          Diffie-Hellman parameters set in
+          <option>security.dhparams.params</option>.
+        '';
+      };
+
+      path = mkOption {
+        type = types.str;
+        default = "/var/lib/dhparams";
+        description = ''
+          Path to the directory in which Diffie-Hellman parameters will be
+          stored. This only is relevant if
+          <option>security.dhparams.stateful</option> is
+          <literal>true</literal>.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf (cfg.enable && cfg.stateful) {
+    systemd.services = {
+      dhparams-init = {
+        description = "Clean Up Old Diffie-Hellman Parameters";
+
+        # Clean up even when no DH params is set
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig.RemainAfterExit = true;
+        serviceConfig.Type = "oneshot";
+
+        script = ''
+          if [ ! -d ${cfg.path} ]; then
+            mkdir -p ${cfg.path}
+          fi
+
+          # Remove old dhparams
+          for file in ${cfg.path}/*; do
+            if [ ! -f "$file" ]; then
+              continue
+            fi
+            ${lib.concatStrings (lib.mapAttrsToList (name: { bits, path, ... }: ''
+              if [ "$file" = ${lib.escapeShellArg path} ] && \
+                 ${pkgs.openssl}/bin/openssl dhparam -in "$file" -text \
+                 | head -n 1 | grep "(${toString bits} bit)" > /dev/null; then
+                continue
+              fi
+            '') cfg.params)}
+            rm $file
+          done
+
+          # TODO: Ideally this would be removing the *former* cfg.path, though
+          # this does not seem really important as changes to it are quite
+          # unlikely
+          rmdir --ignore-fail-on-non-empty ${cfg.path}
+        '';
+      };
+    } // lib.mapAttrs' (name: { bits, path, ... }: lib.nameValuePair "dhparams-gen-${name}" {
+      description = "Generate Diffie-Hellman Parameters for ${name}";
+      after = [ "dhparams-init.service" ];
+      before = [ "${name}.service" ];
+      wantedBy = [ "multi-user.target" ];
+      unitConfig.ConditionPathExists = "!${path}";
+      serviceConfig.Type = "oneshot";
+      script = ''
+        mkdir -p ${lib.escapeShellArg cfg.path}
+        ${pkgs.openssl}/bin/openssl dhparam -out ${lib.escapeShellArg path} \
+          ${toString bits}
+      '';
+    }) cfg.params;
+  };
+
+  meta.maintainers = with lib.maintainers; [ ekleog ];
+}
diff --git a/nixos/modules/security/doas.nix b/nixos/modules/security/doas.nix
new file mode 100644
index 00000000000..2a814f17e45
--- /dev/null
+++ b/nixos/modules/security/doas.nix
@@ -0,0 +1,288 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.security.doas;
+
+  inherit (pkgs) doas;
+
+  mkUsrString = user: toString user;
+
+  mkGrpString = group: ":${toString group}";
+
+  mkOpts = rule: concatStringsSep " " [
+    (optionalString rule.noPass "nopass")
+    (optionalString rule.noLog "nolog")
+    (optionalString rule.persist "persist")
+    (optionalString rule.keepEnv "keepenv")
+    "setenv { SSH_AUTH_SOCK TERMINFO TERMINFO_DIRS ${concatStringsSep " " rule.setEnv} }"
+  ];
+
+  mkArgs = rule:
+    if (isNull rule.args) then ""
+    else if (length rule.args == 0) then "args"
+    else "args ${concatStringsSep " " rule.args}";
+
+  mkRule = rule:
+    let
+      opts = mkOpts rule;
+
+      as = optionalString (!isNull rule.runAs) "as ${rule.runAs}";
+
+      cmd = optionalString (!isNull rule.cmd) "cmd ${rule.cmd}";
+
+      args = mkArgs rule;
+    in
+    optionals (length cfg.extraRules > 0) [
+      (
+        optionalString (length rule.users > 0)
+          (map (usr: "permit ${opts} ${mkUsrString usr} ${as} ${cmd} ${args}") rule.users)
+      )
+      (
+        optionalString (length rule.groups > 0)
+          (map (grp: "permit ${opts} ${mkGrpString grp} ${as} ${cmd} ${args}") rule.groups)
+      )
+    ];
+in
+{
+
+  ###### interface
+
+  options.security.doas = {
+
+    enable = mkOption {
+      type = with types; bool;
+      default = false;
+      description = ''
+        Whether to enable the <command>doas</command> command, which allows
+        non-root users to execute commands as root.
+      '';
+    };
+
+    wheelNeedsPassword = mkOption {
+      type = with types; bool;
+      default = true;
+      description = ''
+        Whether users of the <code>wheel</code> group must provide a password to
+        run commands as super user via <command>doas</command>.
+      '';
+    };
+
+    extraRules = mkOption {
+      default = [];
+      description = ''
+        Define specific rules to be set in the
+        <filename>/etc/doas.conf</filename> file. More specific rules should
+        come after more general ones in order to yield the expected behavior.
+        You can use <code>mkBefore</code> and/or <code>mkAfter</code> to ensure
+        this is the case when configuration options are merged.
+      '';
+      example = literalExpression ''
+        [
+          # Allow execution of any command by any user in group doas, requiring
+          # a password and keeping any previously-defined environment variables.
+          { groups = [ "doas" ]; noPass = false; keepEnv = true; }
+
+          # Allow execution of "/home/root/secret.sh" by user `backup` OR user
+          # `database` OR any member of the group with GID `1006`, without a
+          # password.
+          { users = [ "backup" "database" ]; groups = [ 1006 ];
+            cmd = "/home/root/secret.sh"; noPass = true; }
+
+          # Allow any member of group `bar` to run `/home/baz/cmd1.sh` as user
+          # `foo` with argument `hello-doas`.
+          { groups = [ "bar" ]; runAs = "foo";
+            cmd = "/home/baz/cmd1.sh"; args = [ "hello-doas" ]; }
+
+          # Allow any member of group `bar` to run `/home/baz/cmd2.sh` as user
+          # `foo` with no arguments.
+          { groups = [ "bar" ]; runAs = "foo";
+            cmd = "/home/baz/cmd2.sh"; args = [ ]; }
+
+          # Allow user `abusers` to execute "nano" and unset the value of
+          # SSH_AUTH_SOCK, override the value of ALPHA to 1, and inherit the
+          # value of BETA from the current environment.
+          { users = [ "abusers" ]; cmd = "nano";
+            setEnv = [ "-SSH_AUTH_SOCK" "ALPHA=1" "BETA" ]; }
+        ]
+      '';
+      type = with types; listOf (
+        submodule {
+          options = {
+
+            noPass = mkOption {
+              type = with types; bool;
+              default = false;
+              description = ''
+                If <code>true</code>, the user is not required to enter a
+                password.
+              '';
+            };
+
+            noLog = mkOption {
+              type = with types; bool;
+              default = false;
+              description = ''
+                If <code>true</code>, successful executions will not be logged
+                to
+                <citerefentry><refentrytitle>syslogd</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
+              '';
+            };
+
+            persist = mkOption {
+              type = with types; bool;
+              default = false;
+              description = ''
+                If <code>true</code>, do not ask for a password again for some
+                time after the user successfully authenticates.
+              '';
+            };
+
+            keepEnv = mkOption {
+              type = with types; bool;
+              default = false;
+              description = ''
+                If <code>true</code>, environment variables other than those
+                listed in
+                <citerefentry><refentrytitle>doas</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+                are kept when creating the environment for the new process.
+              '';
+            };
+
+            setEnv = mkOption {
+              type = with types; listOf str;
+              default = [];
+              description = ''
+                Keep or set the specified variables. Variables may also be
+                removed with a leading '-' or set using
+                <code>variable=value</code>. If the first character of
+                <code>value</code> is a '$', the value to be set is taken from
+                the existing environment variable of the indicated name. This
+                option is processed after the default environment has been
+                created.
+
+                NOTE: All rules have <code>setenv { SSH_AUTH_SOCK }</code> by
+                default. To prevent <code>SSH_AUTH_SOCK</code> from being
+                inherited, add <code>"-SSH_AUTH_SOCK"</code> anywhere in this
+                list.
+              '';
+            };
+
+            users = mkOption {
+              type = with types; listOf (either str int);
+              default = [];
+              description = "The usernames / UIDs this rule should apply for.";
+            };
+
+            groups = mkOption {
+              type = with types; listOf (either str int);
+              default = [];
+              description = "The groups / GIDs this rule should apply for.";
+            };
+
+            runAs = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Which user or group the specified command is allowed to run as.
+                When set to <code>null</code> (the default), all users are
+                allowed.
+
+                A user can be specified using just the username:
+                <code>"foo"</code>. It is also possible to only allow running as
+                a specific group with <code>":bar"</code>.
+              '';
+            };
+
+            cmd = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                The command the user is allowed to run. When set to
+                <code>null</code> (the default), all commands are allowed.
+
+                NOTE: It is best practice to specify absolute paths. If a
+                relative path is specified, only a restricted PATH will be
+                searched.
+              '';
+            };
+
+            args = mkOption {
+              type = with types; nullOr (listOf str);
+              default = null;
+              description = ''
+                Arguments that must be provided to the command. When set to
+                <code>[]</code>, the command must be run without any arguments.
+              '';
+            };
+          };
+        }
+      );
+    };
+
+    extraConfig = mkOption {
+      type = with types; lines;
+      default = "";
+      description = ''
+        Extra configuration text appended to <filename>doas.conf</filename>.
+      '';
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    security.doas.extraRules = mkOrder 600 [
+      {
+        groups = [ "wheel" ];
+        noPass = !cfg.wheelNeedsPassword;
+      }
+    ];
+
+    security.wrappers.doas =
+      { setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${doas}/bin/doas";
+      };
+
+    environment.systemPackages = [
+      doas
+    ];
+
+    security.pam.services.doas = {
+      allowNullPassword = true;
+      sshAgentAuth = true;
+    };
+
+    environment.etc."doas.conf" = {
+      source = pkgs.runCommand "doas-conf"
+        {
+          src = pkgs.writeText "doas-conf-in" ''
+            # To modify this file, set the NixOS options
+            # `security.doas.extraRules` or `security.doas.extraConfig`. To
+            # 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}
+          '';
+          preferLocalBuild = true;
+        }
+        # Make sure that the doas.conf file is syntactically valid.
+        "${pkgs.buildPackages.doas}/bin/doas -C $src && cp $src $out";
+      mode = "0440";
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ cole-h ];
+}
diff --git a/nixos/modules/security/duosec.nix b/nixos/modules/security/duosec.nix
new file mode 100644
index 00000000000..bbe246fe229
--- /dev/null
+++ b/nixos/modules/security/duosec.nix
@@ -0,0 +1,240 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.security.duosec;
+
+  boolToStr = b: if b then "yes" else "no";
+
+  configFilePam = ''
+    [duo]
+    ikey=${cfg.integrationKey}
+    host=${cfg.host}
+    ${optionalString (cfg.groups != "") ("groups="+cfg.groups)}
+    failmode=${cfg.failmode}
+    pushinfo=${boolToStr cfg.pushinfo}
+    autopush=${boolToStr cfg.autopush}
+    prompts=${toString cfg.prompts}
+    fallback_local_ip=${boolToStr cfg.fallbackLocalIP}
+  '';
+
+  configFileLogin = configFilePam + ''
+    motd=${boolToStr cfg.motd}
+    accept_env_factor=${boolToStr cfg.acceptEnvFactor}
+  '';
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "security" "duosec" "group" ] [ "security" "duosec" "groups" ])
+    (mkRenamedOptionModule [ "security" "duosec" "ikey" ] [ "security" "duosec" "integrationKey" ])
+    (mkRemovedOptionModule [ "security" "duosec" "skey" ] "The insecure security.duosec.skey option has been replaced by a new security.duosec.secretKeyFile option. Use this new option to store a secure copy of your key instead.")
+  ];
+
+  options = {
+    security.duosec = {
+      ssh.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "If enabled, protect SSH logins with Duo Security.";
+      };
+
+      pam.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "If enabled, protect logins with Duo Security using PAM support.";
+      };
+
+      integrationKey = mkOption {
+        type = types.str;
+        description = "Integration key.";
+      };
+
+      secretKeyFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          A file containing your secret key. The security of your Duo application is tied to the security of your secret key.
+        '';
+        example = "/run/keys/duo-skey";
+      };
+
+      host = mkOption {
+        type = types.str;
+        description = "Duo API hostname.";
+      };
+
+      groups = mkOption {
+        type = types.str;
+        default = "";
+        example = "users,!wheel,!*admin guests";
+        description = ''
+          If specified, Duo authentication is required only for users
+          whose primary group or supplementary group list matches one
+          of the space-separated pattern lists. Refer to
+          <link xlink:href="https://duo.com/docs/duounix"/> for details.
+        '';
+      };
+
+      failmode = mkOption {
+        type = types.enum [ "safe" "secure" ];
+        default = "safe";
+        description = ''
+          On service or configuration errors that prevent Duo
+          authentication, fail "safe" (allow access) or "secure" (deny
+          access). The default is "safe".
+        '';
+      };
+
+      pushinfo = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Include information such as the command to be executed in
+          the Duo Push message.
+        '';
+      };
+
+      autopush = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If <literal>true</literal>, Duo Unix will automatically send
+          a push login request to the user’s phone, falling back on a
+          phone call if push is unavailable. If
+          <literal>false</literal>, the user will be prompted to
+          choose an authentication method. When configured with
+          <literal>autopush = yes</literal>, we recommend setting
+          <literal>prompts = 1</literal>.
+        '';
+      };
+
+      motd = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Print the contents of <literal>/etc/motd</literal> to screen
+          after a successful login.
+        '';
+      };
+
+      prompts = mkOption {
+        type = types.enum [ 1 2 3 ];
+        default = 3;
+        description = ''
+          If a user fails to authenticate with a second factor, Duo
+          Unix will prompt the user to authenticate again. This option
+          sets the maximum number of prompts that Duo Unix will
+          display before denying access. Must be 1, 2, or 3. Default
+          is 3.
+
+          For example, when <literal>prompts = 1</literal>, the user
+          will have to successfully authenticate on the first prompt,
+          whereas if <literal>prompts = 2</literal>, if the user
+          enters incorrect information at the initial prompt, he/she
+          will be prompted to authenticate again.
+
+          When configured with <literal>autopush = true</literal>, we
+          recommend setting <literal>prompts = 1</literal>.
+        '';
+      };
+
+      acceptEnvFactor = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Look for factor selection or passcode in the
+          <literal>$DUO_PASSCODE</literal> environment variable before
+          prompting the user for input.
+
+          When $DUO_PASSCODE is non-empty, it will override
+          autopush. The SSH client will need SendEnv DUO_PASSCODE in
+          its configuration, and the SSH server will similarly need
+          AcceptEnv DUO_PASSCODE.
+        '';
+      };
+
+      fallbackLocalIP = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Duo Unix reports the IP address of the authorizing user, for
+          the purposes of authorization and whitelisting. If Duo Unix
+          cannot detect the IP address of the client, setting
+          <literal>fallbackLocalIP = yes</literal> will cause Duo Unix
+          to send the IP address of the server it is running on.
+
+          If you are using IP whitelisting, enabling this option could
+          cause unauthorized logins if the local IP is listed in the
+          whitelist.
+        '';
+      };
+
+      allowTcpForwarding = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          By default, when SSH forwarding, enabling Duo Security will
+          disable TCP forwarding. By enabling this, you potentially
+          undermine some of the SSH based login security. Note this is
+          not needed if you use PAM.
+        '';
+      };
+    };
+  };
+
+  config = mkIf (cfg.ssh.enable || cfg.pam.enable) {
+    environment.systemPackages = [ pkgs.duo-unix ];
+
+    security.wrappers.login_duo =
+      { setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${pkgs.duo-unix.out}/bin/login_duo";
+      };
+
+    system.activationScripts = {
+      login_duo = mkIf cfg.ssh.enable ''
+        if test -f "${cfg.secretKeyFile}"; then
+          mkdir -m 0755 -p /etc/duo
+
+          umask 0077
+          conf="$(mktemp)"
+          {
+            cat ${pkgs.writeText "login_duo.conf" configFileLogin}
+            printf 'skey = %s\n' "$(cat ${cfg.secretKeyFile})"
+          } >"$conf"
+
+          chown sshd "$conf"
+          mv -fT "$conf" /etc/duo/login_duo.conf
+        fi
+      '';
+      pam_duo = mkIf cfg.pam.enable ''
+        if test -f "${cfg.secretKeyFile}"; then
+          mkdir -m 0755 -p /etc/duo
+
+          umask 0077
+          conf="$(mktemp)"
+          {
+            cat ${pkgs.writeText "login_duo.conf" configFilePam}
+            printf 'skey = %s\n' "$(cat ${cfg.secretKeyFile})"
+          } >"$conf"
+
+          mv -fT "$conf" /etc/duo/pam_duo.conf
+        fi
+      '';
+    };
+
+    /* If PAM *and* SSH are enabled, then don't do anything special.
+    If PAM isn't used, set the default SSH-only options. */
+    services.openssh.extraConfig = mkIf (cfg.ssh.enable || cfg.pam.enable) (
+    if cfg.pam.enable then "UseDNS no" else ''
+      # Duo Security configuration
+      ForceCommand ${config.security.wrapperDir}/login_duo
+      PermitTunnel no
+      ${optionalString (!cfg.allowTcpForwarding) ''
+        AllowTcpForwarding no
+      ''}
+    '');
+  };
+}
diff --git a/nixos/modules/security/google_oslogin.nix b/nixos/modules/security/google_oslogin.nix
new file mode 100644
index 00000000000..cf416035ef6
--- /dev/null
+++ b/nixos/modules/security/google_oslogin.nix
@@ -0,0 +1,71 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.security.googleOsLogin;
+  package = pkgs.google-guest-oslogin;
+
+in
+
+{
+
+  options = {
+
+    security.googleOsLogin.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable Google OS Login.
+
+        The OS Login package enables the following components:
+        AuthorizedKeysCommand to query valid SSH keys from the user's OS Login
+        profile during ssh authentication phase.
+        NSS Module to provide user and group information
+        PAM Module for the sshd service, providing authorization and
+        authentication support, allowing the system to use data stored in
+        Google Cloud IAM permissions to control both, the ability to log into
+        an instance, and to perform operations as root (sudo).
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    security.pam.services.sshd = {
+      makeHomeDir = true;
+      googleOsLoginAccountVerification = true;
+      googleOsLoginAuthentication = true;
+    };
+
+    security.sudo.extraConfig = ''
+      #includedir /run/google-sudoers.d
+    '';
+    systemd.tmpfiles.rules = [
+      "d /run/google-sudoers.d 750 root root -"
+      "d /var/google-users.d 750 root root -"
+    ];
+
+    systemd.packages = [ package ];
+    systemd.timers.google-oslogin-cache.wantedBy = [ "timers.target" ];
+
+    # enable the nss module, so user lookups etc. work
+    system.nssModules = [ package ];
+    system.nssDatabases.passwd = [ "cache_oslogin" "oslogin" ];
+    system.nssDatabases.group = [ "cache_oslogin" "oslogin" ];
+
+    # Ugly: sshd refuses to start if a store path is given because /nix/store is group-writable.
+    # So indirect by a symlink.
+    environment.etc."ssh/authorized_keys_command_google_oslogin" = {
+      mode = "0755";
+      text = ''
+        #!/bin/sh
+        exec ${package}/bin/google_authorized_keys "$@"
+      '';
+    };
+    services.openssh.authorizedKeysCommand = "/etc/ssh/authorized_keys_command_google_oslogin %u";
+    services.openssh.authorizedKeysCommandUser = "nobody";
+  };
+
+}
diff --git a/nixos/modules/security/lock-kernel-modules.nix b/nixos/modules/security/lock-kernel-modules.nix
new file mode 100644
index 00000000000..065587bc286
--- /dev/null
+++ b/nixos/modules/security/lock-kernel-modules.nix
@@ -0,0 +1,58 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+  meta = {
+    maintainers = [ maintainers.joachifm ];
+  };
+
+  options = {
+    security.lockKernelModules = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Disable kernel module loading once the system is fully initialised.
+        Module loading is disabled until the next reboot. Problems caused
+        by delayed module loading can be fixed by adding the module(s) in
+        question to <option>boot.kernelModules</option>.
+      '';
+    };
+  };
+
+  config = mkIf config.security.lockKernelModules {
+    boot.kernelModules = concatMap (x:
+      if x.device != null
+        then
+          if x.fsType == "vfat"
+            then [ "vfat" "nls-cp437" "nls-iso8859-1" ]
+            else [ x.fsType ]
+        else []) config.system.build.fileSystems;
+
+    systemd.services.disable-kernel-module-loading = {
+      description = "Disable kernel module loading";
+
+      wants = [ "systemd-udevd.service" ];
+      wantedBy = [ config.systemd.defaultUnit ];
+
+      after =
+        [ "firewall.service"
+          "systemd-modules-load.service"
+           config.systemd.defaultUnit
+        ];
+
+      unitConfig.ConditionPathIsReadWrite = "/proc/sys/kernel";
+
+      serviceConfig =
+        { Type = "oneshot";
+          RemainAfterExit = true;
+          TimeoutSec = 180;
+        };
+
+      script = ''
+        ${pkgs.udev}/bin/udevadm settle
+        echo -n 1 >/proc/sys/kernel/modules_disabled
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/security/misc.nix b/nixos/modules/security/misc.nix
new file mode 100644
index 00000000000..c20e067b8cc
--- /dev/null
+++ b/nixos/modules/security/misc.nix
@@ -0,0 +1,155 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+  meta = {
+    maintainers = [ maintainers.joachifm ];
+  };
+
+  imports = [
+    (lib.mkRenamedOptionModule [ "security" "virtualization" "flushL1DataCache" ] [ "security" "virtualisation" "flushL1DataCache" ])
+  ];
+
+  options = {
+    security.allowUserNamespaces = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to allow creation of user namespaces.
+
+        The motivation for disabling user namespaces is the potential
+        presence of code paths where the kernel's permission checking
+        logic fails to account for namespacing, instead permitting a
+        namespaced process to act outside the namespace with the same
+        privileges as it would have inside it.  This is particularly
+        damaging in the common case of running as root within the namespace.
+
+        When user namespace creation is disallowed, attempting to create a
+        user namespace fails with "no space left on device" (ENOSPC).
+        root may re-enable user namespace creation at runtime.
+      '';
+    };
+
+    security.unprivilegedUsernsClone = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        When disabled, unprivileged users will not be able to create new namespaces.
+        By default unprivileged user namespaces are disabled.
+        This option only works in a hardened profile.
+      '';
+    };
+
+    security.protectKernelImage = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to prevent replacing the running kernel image.
+      '';
+    };
+
+    security.allowSimultaneousMultithreading = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to allow SMT/hyperthreading.  Disabling SMT means that only
+        physical CPU cores will be usable at runtime, potentially at
+        significant performance cost.
+
+        The primary motivation for disabling SMT is to mitigate the risk of
+        leaking data between threads running on the same CPU core (due to
+        e.g., shared caches).  This attack vector is unproven.
+
+        Disabling SMT is a supplement to the L1 data cache flushing mitigation
+        (see <xref linkend="opt-security.virtualisation.flushL1DataCache"/>)
+        versus malicious VM guests (SMT could "bring back" previously flushed
+        data).
+      '';
+    };
+
+    security.forcePageTableIsolation = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to force-enable the Page Table Isolation (PTI) Linux kernel
+        feature even on CPU models that claim to be safe from Meltdown.
+
+        This hardening feature is most beneficial to systems that run untrusted
+        workloads that rely on address space isolation for security.
+      '';
+    };
+
+    security.virtualisation.flushL1DataCache = mkOption {
+      type = types.nullOr (types.enum [ "never" "cond" "always" ]);
+      default = null;
+      description = ''
+        Whether the hypervisor should flush the L1 data cache before
+        entering guests.
+        See also <xref linkend="opt-security.allowSimultaneousMultithreading"/>.
+
+        <variablelist>
+          <varlistentry>
+            <term><literal>null</literal></term>
+            <listitem><para>uses the kernel default</para></listitem>
+          </varlistentry>
+          <varlistentry>
+            <term><literal>"never"</literal></term>
+            <listitem><para>disables L1 data cache flushing entirely.
+            May be appropriate if all guests are trusted.</para></listitem>
+          </varlistentry>
+          <varlistentry>
+            <term><literal>"cond"</literal></term>
+            <listitem><para>flushes L1 data cache only for pre-determined
+            code paths.  May leak information about the host address space
+            layout.</para></listitem>
+          </varlistentry>
+          <varlistentry>
+            <term><literal>"always"</literal></term>
+            <listitem><para>flushes L1 data cache every time the hypervisor
+            enters the guest.  May incur significant performance cost.
+            </para></listitem>
+          </varlistentry>
+        </variablelist>
+      '';
+    };
+  };
+
+  config = mkMerge [
+    (mkIf (!config.security.allowUserNamespaces) {
+      # Setting the number of allowed user namespaces to 0 effectively disables
+      # the feature at runtime.  Note that root may raise the limit again
+      # at any time.
+      boot.kernel.sysctl."user.max_user_namespaces" = 0;
+
+      assertions = [
+        { assertion = config.nix.settings.sandbox -> config.security.allowUserNamespaces;
+          message = "`nix.settings.sandbox = true` conflicts with `!security.allowUserNamespaces`.";
+        }
+      ];
+    })
+
+    (mkIf config.security.unprivilegedUsernsClone {
+      boot.kernel.sysctl."kernel.unprivileged_userns_clone" = mkDefault true;
+    })
+
+    (mkIf config.security.protectKernelImage {
+      # Disable hibernation (allows replacing the running kernel)
+      boot.kernelParams = [ "nohibernate" ];
+      # Prevent replacing the running kernel image w/o reboot
+      boot.kernel.sysctl."kernel.kexec_load_disabled" = mkDefault true;
+    })
+
+    (mkIf (!config.security.allowSimultaneousMultithreading) {
+      boot.kernelParams = [ "nosmt" ];
+    })
+
+    (mkIf config.security.forcePageTableIsolation {
+      boot.kernelParams = [ "pti=on" ];
+    })
+
+    (mkIf (config.security.virtualisation.flushL1DataCache != null) {
+      boot.kernelParams = [ "kvm-intel.vmentry_l1d_flush=${config.security.virtualisation.flushL1DataCache}" ];
+    })
+  ];
+}
diff --git a/nixos/modules/security/oath.nix b/nixos/modules/security/oath.nix
new file mode 100644
index 00000000000..93bdc851117
--- /dev/null
+++ b/nixos/modules/security/oath.nix
@@ -0,0 +1,50 @@
+# This module provides configuration for the OATH PAM modules.
+
+{ lib, ... }:
+
+with lib;
+
+{
+  options = {
+
+    security.pam.oath = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the OATH (one-time password) PAM module.
+        '';
+      };
+
+      digits = mkOption {
+        type = types.enum [ 6 7 8 ];
+        default = 6;
+        description = ''
+          Specify the length of the one-time password in number of
+          digits.
+        '';
+      };
+
+      window = mkOption {
+        type = types.int;
+        default = 5;
+        description = ''
+          Specify the number of one-time passwords to check in order
+          to accommodate for situations where the system and the
+          client are slightly out of sync (iteration for HOTP or time
+          steps for TOTP).
+        '';
+      };
+
+      usersFile = mkOption {
+        type = types.path;
+        default = "/etc/users.oath";
+        description = ''
+          Set the path to file where the user's credentials are
+          stored. This file must not be world readable!
+        '';
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix
new file mode 100644
index 00000000000..c0ef8b5f30b
--- /dev/null
+++ b/nixos/modules/security/pam.nix
@@ -0,0 +1,1149 @@
+# This module provides configuration for the PAM (Pluggable
+# Authentication Modules) system.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  parentConfig = config;
+
+  pamOpts = { config, name, ... }: let cfg = config; in let config = parentConfig; in {
+
+    options = {
+
+      name = mkOption {
+        example = "sshd";
+        type = types.str;
+        description = "Name of the PAM service.";
+      };
+
+      unixAuth = mkOption {
+        default = true;
+        type = types.bool;
+        description = ''
+          Whether users can log in with passwords defined in
+          <filename>/etc/shadow</filename>.
+        '';
+      };
+
+      rootOK = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If set, root doesn't need to authenticate (e.g. for the
+          <command>useradd</command> service).
+        '';
+      };
+
+      p11Auth = mkOption {
+        default = config.security.pam.p11.enable;
+        defaultText = literalExpression "config.security.pam.p11.enable";
+        type = types.bool;
+        description = ''
+          If set, keys listed in
+          <filename>~/.ssh/authorized_keys</filename> and
+          <filename>~/.eid/authorized_certificates</filename>
+          can be used to log in with the associated PKCS#11 tokens.
+        '';
+      };
+
+      u2fAuth = mkOption {
+        default = config.security.pam.u2f.enable;
+        defaultText = literalExpression "config.security.pam.u2f.enable";
+        type = types.bool;
+        description = ''
+          If set, users listed in
+          <filename>$XDG_CONFIG_HOME/Yubico/u2f_keys</filename> (or
+          <filename>$HOME/.config/Yubico/u2f_keys</filename> if XDG variable is
+          not set) are able to log in with the associated U2F key. Path can be
+          changed using <option>security.pam.u2f.authFile</option> option.
+        '';
+      };
+
+      yubicoAuth = mkOption {
+        default = config.security.pam.yubico.enable;
+        defaultText = literalExpression "config.security.pam.yubico.enable";
+        type = types.bool;
+        description = ''
+          If set, users listed in
+          <filename>~/.yubico/authorized_yubikeys</filename>
+          are able to log in with the associated Yubikey tokens.
+        '';
+      };
+
+      googleAuthenticator = {
+        enable = mkOption {
+          default = false;
+          type = types.bool;
+          description = ''
+            If set, users with enabled Google Authenticator (created
+            <filename>~/.google_authenticator</filename>) will be required
+            to provide Google Authenticator token to log in.
+          '';
+        };
+      };
+
+      usbAuth = mkOption {
+        default = config.security.pam.usb.enable;
+        defaultText = literalExpression "config.security.pam.usb.enable";
+        type = types.bool;
+        description = ''
+          If set, users listed in
+          <filename>/etc/pamusb.conf</filename> are able to log in
+          with the associated USB key.
+        '';
+      };
+
+      otpwAuth = mkOption {
+        default = config.security.pam.enableOTPW;
+        defaultText = literalExpression "config.security.pam.enableOTPW";
+        type = types.bool;
+        description = ''
+          If set, the OTPW system will be used (if
+          <filename>~/.otpw</filename> exists).
+        '';
+      };
+
+      googleOsLoginAccountVerification = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If set, will use the Google OS Login PAM modules
+          (<literal>pam_oslogin_login</literal>,
+          <literal>pam_oslogin_admin</literal>) to verify possible OS Login
+          users and set sudoers configuration accordingly.
+          This only makes sense to enable for the <literal>sshd</literal> PAM
+          service.
+        '';
+      };
+
+      googleOsLoginAuthentication = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If set, will use the <literal>pam_oslogin_login</literal>'s user
+          authentication methods to authenticate users using 2FA.
+          This only makes sense to enable for the <literal>sshd</literal> PAM
+          service.
+        '';
+      };
+
+      fprintAuth = mkOption {
+        default = config.services.fprintd.enable;
+        defaultText = literalExpression "config.services.fprintd.enable";
+        type = types.bool;
+        description = ''
+          If set, fingerprint reader will be used (if exists and
+          your fingerprints are enrolled).
+        '';
+      };
+
+      oathAuth = mkOption {
+        default = config.security.pam.oath.enable;
+        defaultText = literalExpression "config.security.pam.oath.enable";
+        type = types.bool;
+        description = ''
+          If set, the OATH Toolkit will be used.
+        '';
+      };
+
+      sshAgentAuth = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If set, the calling user's SSH agent is used to authenticate
+          against the keys in the calling user's
+          <filename>~/.ssh/authorized_keys</filename>.  This is useful
+          for <command>sudo</command> on password-less remote systems.
+        '';
+      };
+
+      duoSecurity = {
+        enable = mkOption {
+          default = false;
+          type = types.bool;
+          description = ''
+            If set, use the Duo Security pam module
+            <literal>pam_duo</literal> for authentication.  Requires
+            configuration of <option>security.duosec</option> options.
+          '';
+        };
+      };
+
+      startSession = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If set, the service will register a new session with
+          systemd's login manager.  For local sessions, this will give
+          the user access to audio devices, CD-ROM drives.  In the
+          default PolicyKit configuration, it also allows the user to
+          reboot the system.
+        '';
+      };
+
+      setEnvironment = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether the service should set the environment variables
+          listed in <option>environment.sessionVariables</option>
+          using <literal>pam_env.so</literal>.
+        '';
+      };
+
+      setLoginUid = mkOption {
+        type = types.bool;
+        description = ''
+          Set the login uid of the process
+          (<filename>/proc/self/loginuid</filename>) for auditing
+          purposes.  The login uid is only set by ‘entry points’ like
+          <command>login</command> and <command>sshd</command>, not by
+          commands like <command>sudo</command>.
+        '';
+      };
+
+      ttyAudit = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Enable or disable TTY auditing for specified users
+          '';
+        };
+
+        enablePattern = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            For each user matching one of comma-separated
+            glob patterns, enable TTY auditing
+          '';
+        };
+
+        disablePattern = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            For each user matching one of comma-separated
+            glob patterns, disable TTY auditing
+          '';
+        };
+
+        openOnly = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Set the TTY audit flag when opening the session,
+            but do not restore it when closing the session.
+            Using this option is necessary for some services
+            that don't fork() to run the authenticated session,
+            such as sudo.
+          '';
+        };
+      };
+
+      forwardXAuth = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether X authentication keys should be passed from the
+          calling user to the target user (e.g. for
+          <command>su</command>)
+        '';
+      };
+
+      pamMount = mkOption {
+        default = config.security.pam.mount.enable;
+        defaultText = literalExpression "config.security.pam.mount.enable";
+        type = types.bool;
+        description = ''
+          Enable PAM mount (pam_mount) system to mount fileystems on user login.
+        '';
+      };
+
+      allowNullPassword = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to allow logging into accounts that have no password
+          set (i.e., have an empty password field in
+          <filename>/etc/passwd</filename> or
+          <filename>/etc/group</filename>).  This does not enable
+          logging into disabled accounts (i.e., that have the password
+          field set to <literal>!</literal>).  Note that regardless of
+          what the pam_unix documentation says, accounts with hashed
+          empty passwords are always allowed to log in.
+        '';
+      };
+
+      nodelay = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Wheather the delay after typing a wrong password should be disabled.
+        '';
+      };
+
+      requireWheel = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to permit root access only to members of group wheel.
+        '';
+      };
+
+      limits = mkOption {
+        default = [];
+        type = limitsType;
+        description = ''
+          Attribute set describing resource limits.  Defaults to the
+          value of <option>security.pam.loginLimits</option>.
+          The meaning of the values is explained in <citerefentry>
+          <refentrytitle>limits.conf</refentrytitle><manvolnum>5</manvolnum>
+          </citerefentry>.
+        '';
+      };
+
+      showMotd = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to show the message of the day.";
+      };
+
+      makeHomeDir = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to try to create home directories for users
+          with <literal>$HOME</literal>s pointing to nonexistent
+          locations on session login.
+        '';
+      };
+
+      updateWtmp = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to update <filename>/var/log/wtmp</filename>.";
+      };
+
+      logFailures = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to log authentication failures in <filename>/var/log/faillog</filename>.";
+      };
+
+      enableAppArmor = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable support for attaching AppArmor profiles at the
+          user/group level, e.g., as part of a role based access
+          control scheme.
+        '';
+      };
+
+      enableKwallet = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If enabled, pam_wallet will attempt to automatically unlock the
+          user's default KDE wallet upon login. If the user has no wallet named
+          "kdewallet", or the login password does not match their wallet
+          password, KDE will prompt separately after login.
+        '';
+      };
+      sssdStrictAccess = mkOption {
+        default = false;
+        type = types.bool;
+        description = "enforce sssd access control";
+      };
+
+      enableGnomeKeyring = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If enabled, pam_gnome_keyring will attempt to automatically unlock the
+          user's default Gnome keyring upon login. If the user login password does
+          not match their keyring password, Gnome Keyring will prompt separately
+          after login.
+        '';
+      };
+
+      gnupg = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            If enabled, pam_gnupg will attempt to automatically unlock the
+            user's GPG keys with the login password via
+            <command>gpg-agent</command>. The keygrips of all keys to be
+            unlocked should be written to <filename>~/.pam-gnupg</filename>,
+            and can be queried with <command>gpg -K --with-keygrip</command>.
+            Presetting passphrases must be enabled by adding
+            <literal>allow-preset-passphrase</literal> in
+            <filename>~/.gnupg/gpg-agent.conf</filename>.
+          '';
+        };
+
+        noAutostart = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Don't start <command>gpg-agent</command> if it is not running.
+            Useful in conjunction with starting <command>gpg-agent</command> as
+            a systemd user service.
+          '';
+        };
+
+        storeOnly = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Don't send the password immediately after login, but store for PAM
+            <literal>session</literal>.
+          '';
+        };
+      };
+
+      text = mkOption {
+        type = types.nullOr types.lines;
+        description = "Contents of the PAM service file.";
+      };
+
+    };
+
+    # The resulting /etc/pam.d/* file contents are verified in
+    # nixos/tests/pam/pam-file-contents.nix. Please update tests there when
+    # changing the derivation.
+    config = {
+      name = mkDefault name;
+      setLoginUid = mkDefault cfg.startSession;
+      limits = mkDefault config.security.pam.loginLimits;
+
+      # !!! TODO: move the LDAP stuff to the LDAP module, and the
+      # Samba stuff to the Samba module.  This requires that the PAM
+      # module provides the right hooks.
+      text = mkDefault
+        (
+          ''
+            # Account management.
+            account required pam_unix.so
+          '' +
+          optionalString use_ldap ''
+            account sufficient ${pam_ldap}/lib/security/pam_ldap.so
+          '' +
+          optionalString (config.services.sssd.enable && cfg.sssdStrictAccess==false) ''
+            account sufficient ${pkgs.sssd}/lib/security/pam_sss.so
+          '' +
+          optionalString (config.services.sssd.enable && cfg.sssdStrictAccess) ''
+            account [default=bad success=ok user_unknown=ignore] ${pkgs.sssd}/lib/security/pam_sss.so
+          '' +
+          optionalString config.krb5.enable ''
+            account sufficient ${pam_krb5}/lib/security/pam_krb5.so
+          '' +
+          optionalString cfg.googleOsLoginAccountVerification ''
+            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
+          '' +
+          ''
+
+            # Authentication management.
+          '' +
+          optionalString cfg.googleOsLoginAuthentication ''
+            auth [success=done perm_denied=die default=ignore] ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_login.so
+          '' +
+          optionalString cfg.rootOK ''
+            auth sufficient pam_rootok.so
+          '' +
+          optionalString cfg.requireWheel ''
+            auth required pam_wheel.so use_uid
+          '' +
+          optionalString cfg.logFailures ''
+            auth required pam_faillock.so
+          '' +
+          optionalString (config.security.pam.enableSSHAgentAuth && cfg.sshAgentAuth) ''
+            auth sufficient ${pkgs.pam_ssh_agent_auth}/libexec/pam_ssh_agent_auth.so file=${lib.concatStringsSep ":" config.services.openssh.authorizedKeysFiles}
+          '' +
+          (let p11 = config.security.pam.p11; in optionalString cfg.p11Auth ''
+            auth ${p11.control} ${pkgs.pam_p11}/lib/security/pam_p11.so ${pkgs.opensc}/lib/opensc-pkcs11.so
+          '') +
+          (let u2f = config.security.pam.u2f; in optionalString cfg.u2fAuth ''
+            auth ${u2f.control} ${pkgs.pam_u2f}/lib/security/pam_u2f.so ${optionalString u2f.debug "debug"} ${optionalString (u2f.authFile != null) "authfile=${u2f.authFile}"} ${optionalString u2f.interactive "interactive"} ${optionalString u2f.cue "cue"} ${optionalString (u2f.appId != null) "appid=${u2f.appId}"}
+          '') +
+          optionalString cfg.usbAuth ''
+            auth sufficient ${pkgs.pam_usb}/lib/security/pam_usb.so
+          '' +
+          (let oath = config.security.pam.oath; in optionalString cfg.oathAuth ''
+            auth requisite ${pkgs.oathToolkit}/lib/security/pam_oath.so window=${toString oath.window} usersfile=${toString oath.usersFile} digits=${toString oath.digits}
+          '') +
+          (let yubi = config.security.pam.yubico; in optionalString cfg.yubicoAuth ''
+            auth ${yubi.control} ${pkgs.yubico-pam}/lib/security/pam_yubico.so mode=${toString yubi.mode} ${optionalString (yubi.challengeResponsePath != null) "chalresp_path=${yubi.challengeResponsePath}"} ${optionalString (yubi.mode == "client") "id=${toString yubi.id}"} ${optionalString yubi.debug "debug"}
+          '') +
+          optionalString cfg.fprintAuth ''
+            auth sufficient ${pkgs.fprintd}/lib/security/pam_fprintd.so
+          '' +
+          # Modules in this block require having the password set in PAM_AUTHTOK.
+          # pam_unix is marked as 'sufficient' on NixOS which means nothing will run
+          # after it succeeds. Certain modules need to run after pam_unix
+          # prompts the user for password so we run it once with 'required' 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 &&
+            (config.security.pam.enableEcryptfs
+              || cfg.pamMount
+              || cfg.enableKwallet
+              || cfg.enableGnomeKeyring
+              || cfg.googleAuthenticator.enable
+              || cfg.gnupg.enable
+              || cfg.duoSecurity.enable))
+            (
+              ''
+                auth required pam_unix.so ${optionalString cfg.allowNullPassword "nullok"} ${optionalString cfg.nodelay "nodelay"} likeauth
+              '' +
+              optionalString config.security.pam.enableEcryptfs ''
+                auth optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so unwrap
+              '' +
+              optionalString cfg.pamMount ''
+                auth optional ${pkgs.pam_mount}/lib/security/pam_mount.so disable_interactive
+              '' +
+              optionalString cfg.enableKwallet ''
+               auth optional ${pkgs.plasma5Packages.kwallet-pam}/lib/security/pam_kwallet5.so kwalletd=${pkgs.plasma5Packages.kwallet.bin}/bin/kwalletd5
+              '' +
+              optionalString cfg.enableGnomeKeyring ''
+                auth optional ${pkgs.gnome.gnome-keyring}/lib/security/pam_gnome_keyring.so
+              '' +
+              optionalString cfg.gnupg.enable ''
+                auth optional ${pkgs.pam_gnupg}/lib/security/pam_gnupg.so ${optionalString cfg.gnupg.storeOnly " store-only"}
+              '' +
+              optionalString cfg.googleAuthenticator.enable ''
+                auth required ${pkgs.google-authenticator}/lib/security/pam_google_authenticator.so no_increment_hotp
+              '' +
+              optionalString cfg.duoSecurity.enable ''
+                auth required ${pkgs.duo-unix}/lib/security/pam_duo.so
+              ''
+            )) +
+          optionalString cfg.unixAuth ''
+            auth sufficient pam_unix.so ${optionalString cfg.allowNullPassword "nullok"} ${optionalString cfg.nodelay "nodelay"} likeauth try_first_pass
+          '' +
+          optionalString cfg.otpwAuth ''
+            auth sufficient ${pkgs.otpw}/lib/security/pam_otpw.so
+          '' +
+          optionalString use_ldap ''
+            auth sufficient ${pam_ldap}/lib/security/pam_ldap.so use_first_pass
+          '' +
+          optionalString config.services.sssd.enable ''
+            auth sufficient ${pkgs.sssd}/lib/security/pam_sss.so use_first_pass
+          '' +
+          optionalString config.krb5.enable ''
+            auth [default=ignore success=1 service_err=reset] ${pam_krb5}/lib/security/pam_krb5.so use_first_pass
+            auth [default=die success=done] ${pam_ccreds}/lib/security/pam_ccreds.so action=validate use_first_pass
+            auth sufficient ${pam_ccreds}/lib/security/pam_ccreds.so action=store use_first_pass
+          '' +
+          ''
+            auth required pam_deny.so
+
+            # Password management.
+            password sufficient pam_unix.so nullok sha512
+          '' +
+          optionalString config.security.pam.enableEcryptfs ''
+            password optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so
+          '' +
+          optionalString cfg.pamMount ''
+            password optional ${pkgs.pam_mount}/lib/security/pam_mount.so
+          '' +
+          optionalString use_ldap ''
+            password sufficient ${pam_ldap}/lib/security/pam_ldap.so
+          '' +
+          optionalString config.services.sssd.enable ''
+            password sufficient ${pkgs.sssd}/lib/security/pam_sss.so use_authtok
+          '' +
+          optionalString config.krb5.enable ''
+            password sufficient ${pam_krb5}/lib/security/pam_krb5.so use_first_pass
+          '' +
+          optionalString cfg.enableGnomeKeyring ''
+            password optional ${pkgs.gnome.gnome-keyring}/lib/security/pam_gnome_keyring.so use_authtok
+          '' +
+          ''
+
+            # Session management.
+          '' +
+          optionalString cfg.setEnvironment ''
+            session required pam_env.so conffile=/etc/pam/environment readenv=0
+          '' +
+          ''
+            session required pam_unix.so
+          '' +
+          optionalString cfg.setLoginUid ''
+            session ${if config.boot.isContainer then "optional" else "required"} pam_loginuid.so
+          '' +
+          optionalString cfg.ttyAudit.enable ''
+            session required ${pkgs.pam}/lib/security/pam_tty_audit.so
+                open_only=${toString cfg.ttyAudit.openOnly}
+                ${optionalString (cfg.ttyAudit.enablePattern != null) "enable=${cfg.ttyAudit.enablePattern}"}
+                ${optionalString (cfg.ttyAudit.disablePattern != null) "disable=${cfg.ttyAudit.disablePattern}"}
+          '' +
+          optionalString cfg.makeHomeDir ''
+            session required ${pkgs.pam}/lib/security/pam_mkhomedir.so silent skel=${config.security.pam.makeHomeDir.skelDirectory} umask=0077
+          '' +
+          optionalString cfg.updateWtmp ''
+            session required ${pkgs.pam}/lib/security/pam_lastlog.so silent
+          '' +
+          optionalString config.security.pam.enableEcryptfs ''
+            session optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so
+          '' +
+          optionalString cfg.pamMount ''
+            session optional ${pkgs.pam_mount}/lib/security/pam_mount.so disable_interactive
+          '' +
+          optionalString use_ldap ''
+            session optional ${pam_ldap}/lib/security/pam_ldap.so
+          '' +
+          optionalString config.services.sssd.enable ''
+            session optional ${pkgs.sssd}/lib/security/pam_sss.so
+          '' +
+          optionalString config.krb5.enable ''
+            session optional ${pam_krb5}/lib/security/pam_krb5.so
+          '' +
+          optionalString cfg.otpwAuth ''
+            session optional ${pkgs.otpw}/lib/security/pam_otpw.so
+          '' +
+          optionalString cfg.startSession ''
+            session optional ${pkgs.systemd}/lib/security/pam_systemd.so
+          '' +
+          optionalString cfg.forwardXAuth ''
+            session optional pam_xauth.so xauthpath=${pkgs.xorg.xauth}/bin/xauth systemuser=99
+          '' +
+          optionalString (cfg.limits != []) ''
+            session required ${pkgs.pam}/lib/security/pam_limits.so conf=${makeLimitsConf cfg.limits}
+          '' +
+          optionalString (cfg.showMotd && config.users.motd != null) ''
+            session optional ${pkgs.pam}/lib/security/pam_motd.so motd=${motd}
+          '' +
+          optionalString (cfg.enableAppArmor && config.security.apparmor.enable) ''
+            session optional ${pkgs.apparmor-pam}/lib/security/pam_apparmor.so order=user,group,default debug
+          '' +
+          optionalString (cfg.enableKwallet) ''
+            session optional ${pkgs.plasma5Packages.kwallet-pam}/lib/security/pam_kwallet5.so kwalletd=${pkgs.plasma5Packages.kwallet.bin}/bin/kwalletd5
+          '' +
+          optionalString (cfg.enableGnomeKeyring) ''
+            session optional ${pkgs.gnome.gnome-keyring}/lib/security/pam_gnome_keyring.so auto_start
+          '' +
+          optionalString cfg.gnupg.enable ''
+            session optional ${pkgs.pam_gnupg}/lib/security/pam_gnupg.so ${optionalString cfg.gnupg.noAutostart " no-autostart"}
+          '' +
+          optionalString (config.virtualisation.lxc.lxcfs.enable) ''
+            session optional ${pkgs.lxc}/lib/security/pam_cgfs.so -c all
+          ''
+        );
+    };
+
+  };
+
+
+  inherit (pkgs) pam_krb5 pam_ccreds;
+
+  use_ldap = (config.users.ldap.enable && config.users.ldap.loginPam);
+  pam_ldap = if config.users.ldap.daemon.enable then pkgs.nss_pam_ldapd else pkgs.pam_ldap;
+
+  # Create a limits.conf(5) file.
+  makeLimitsConf = limits:
+    pkgs.writeText "limits.conf"
+       (concatMapStrings ({ domain, type, item, value }:
+         "${domain} ${type} ${item} ${toString value}\n")
+         limits);
+
+  limitsType = with lib.types; listOf (submodule ({ ... }: {
+    options = {
+      domain = mkOption {
+        description = "Username, groupname, or wildcard this limit applies to";
+        example = "@wheel";
+        type = str;
+      };
+
+      type = mkOption {
+        description = "Type of this limit";
+        type = enum [ "-" "hard" "soft" ];
+        default = "-";
+      };
+
+      item = mkOption {
+        description = "Item this limit applies to";
+        type = enum [
+          "core"
+          "data"
+          "fsize"
+          "memlock"
+          "nofile"
+          "rss"
+          "stack"
+          "cpu"
+          "nproc"
+          "as"
+          "maxlogins"
+          "maxsyslogins"
+          "priority"
+          "locks"
+          "sigpending"
+          "msgqueue"
+          "nice"
+          "rtprio"
+        ];
+      };
+
+      value = mkOption {
+        description = "Value of this limit";
+        type = oneOf [ str int ];
+      };
+    };
+  }));
+
+  motd = pkgs.writeText "motd" config.users.motd;
+
+  makePAMService = name: service:
+    { name = "pam.d/${name}";
+      value.source = pkgs.writeText "${name}.pam" service.text;
+    };
+
+in
+
+{
+
+  imports = [
+    (mkRenamedOptionModule [ "security" "pam" "enableU2F" ] [ "security" "pam" "u2f" "enable" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    security.pam.loginLimits = mkOption {
+      default = [];
+      type = limitsType;
+      example =
+        [ { domain = "ftp";
+            type   = "hard";
+            item   = "nproc";
+            value  = "0";
+          }
+          { domain = "@student";
+            type   = "-";
+            item   = "maxlogins";
+            value  = "4";
+          }
+       ];
+
+     description =
+       '' Define resource limits that should apply to users or groups.
+          Each item in the list should be an attribute set with a
+          <varname>domain</varname>, <varname>type</varname>,
+          <varname>item</varname>, and <varname>value</varname>
+          attribute.  The syntax and semantics of these attributes
+          must be that described in <citerefentry><refentrytitle>limits.conf</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry>.
+
+          Note that these limits do not apply to systemd services,
+          whose limits can be changed via <option>systemd.extraConfig</option>
+          instead.
+       '';
+    };
+
+    security.pam.services = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule pamOpts);
+      description =
+        ''
+          This option defines the PAM services.  A service typically
+          corresponds to a program that uses PAM,
+          e.g. <command>login</command> or <command>passwd</command>.
+          Each attribute of this set defines a PAM service, with the attribute name
+          defining the name of the service.
+        '';
+    };
+
+    security.pam.makeHomeDir.skelDirectory = mkOption {
+      type = types.str;
+      default = "/var/empty";
+      example =  "/etc/skel";
+      description = ''
+        Path to skeleton directory whose contents are copied to home
+        directories newly created by <literal>pam_mkhomedir</literal>.
+      '';
+    };
+
+    security.pam.enableSSHAgentAuth = mkOption {
+      type = types.bool;
+      default = false;
+      description =
+        ''
+          Enable sudo logins if the user's SSH agent provides a key
+          present in <filename>~/.ssh/authorized_keys</filename>.
+          This allows machines to exclusively use SSH keys instead of
+          passwords.
+        '';
+    };
+
+    security.pam.enableOTPW = mkEnableOption "the OTPW (one-time password) PAM module";
+
+    security.pam.p11 = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enables P11 PAM (<literal>pam_p11</literal>) module.
+
+          If set, users can log in with SSH keys and PKCS#11 tokens.
+
+          More information can be found <link
+          xlink:href="https://github.com/OpenSC/pam_p11">here</link>.
+        '';
+      };
+
+      control = mkOption {
+        default = "sufficient";
+        type = types.enum [ "required" "requisite" "sufficient" "optional" ];
+        description = ''
+          This option sets pam "control".
+          If you want to have multi factor authentication, use "required".
+          If you want to use the PKCS#11 device instead of the regular password,
+          use "sufficient".
+
+          Read
+          <citerefentry>
+            <refentrytitle>pam.conf</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry>
+          for better understanding of this option.
+        '';
+      };
+    };
+
+    security.pam.u2f = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enables U2F PAM (<literal>pam-u2f</literal>) module.
+
+          If set, users listed in
+          <filename>$XDG_CONFIG_HOME/Yubico/u2f_keys</filename> (or
+          <filename>$HOME/.config/Yubico/u2f_keys</filename> if XDG variable is
+          not set) are able to log in with the associated U2F key. The path can
+          be changed using <option>security.pam.u2f.authFile</option> option.
+
+          File format is:
+          <literal>username:first_keyHandle,first_public_key: second_keyHandle,second_public_key</literal>
+          This file can be generated using <command>pamu2fcfg</command> command.
+
+          More information can be found <link
+          xlink:href="https://developers.yubico.com/pam-u2f/">here</link>.
+        '';
+      };
+
+      authFile = mkOption {
+        default = null;
+        type = with types; nullOr path;
+        description = ''
+          By default <literal>pam-u2f</literal> module reads the keys from
+          <filename>$XDG_CONFIG_HOME/Yubico/u2f_keys</filename> (or
+          <filename>$HOME/.config/Yubico/u2f_keys</filename> if XDG variable is
+          not set).
+
+          If you want to change auth file locations or centralize database (for
+          example use <filename>/etc/u2f-mappings</filename>) you can set this
+          option.
+
+          File format is:
+          <literal>username:first_keyHandle,first_public_key: second_keyHandle,second_public_key</literal>
+          This file can be generated using <command>pamu2fcfg</command> command.
+
+          More information can be found <link
+          xlink:href="https://developers.yubico.com/pam-u2f/">here</link>.
+        '';
+      };
+
+      appId = mkOption {
+        default = null;
+        type = with types; nullOr str;
+        description = ''
+            By default <literal>pam-u2f</literal> module sets the application
+            ID to <literal>pam://$HOSTNAME</literal>.
+
+            When using <command>pamu2fcfg</command>, you can specify your
+            application ID with the <literal>-i</literal> flag.
+
+            More information can be found <link
+            xlink:href="https://developers.yubico.com/pam-u2f/Manuals/pam_u2f.8.html">
+            here</link>
+        '';
+      };
+
+      control = mkOption {
+        default = "sufficient";
+        type = types.enum [ "required" "requisite" "sufficient" "optional" ];
+        description = ''
+          This option sets pam "control".
+          If you want to have multi factor authentication, use "required".
+          If you want to use U2F device instead of regular password, use "sufficient".
+
+          Read
+          <citerefentry>
+            <refentrytitle>pam.conf</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry>
+          for better understanding of this option.
+        '';
+      };
+
+      debug = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Debug output to stderr.
+        '';
+      };
+
+      interactive = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Set to prompt a message and wait before testing the presence of a U2F device.
+          Recommended if your device doesn’t have a tactile trigger.
+        '';
+      };
+
+      cue = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          By default <literal>pam-u2f</literal> module does not inform user
+          that he needs to use the u2f device, it just waits without a prompt.
+
+          If you set this option to <literal>true</literal>,
+          <literal>cue</literal> option is added to <literal>pam-u2f</literal>
+          module and reminder message will be displayed.
+        '';
+      };
+    };
+
+    security.pam.yubico = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enables Yubico PAM (<literal>yubico-pam</literal>) module.
+
+          If set, users listed in
+          <filename>~/.yubico/authorized_yubikeys</filename>
+          are able to log in with the associated Yubikey tokens.
+
+          The file must have only one line:
+          <literal>username:yubikey_token_id1:yubikey_token_id2</literal>
+          More information can be found <link
+          xlink:href="https://developers.yubico.com/yubico-pam/">here</link>.
+        '';
+      };
+      control = mkOption {
+        default = "sufficient";
+        type = types.enum [ "required" "requisite" "sufficient" "optional" ];
+        description = ''
+          This option sets pam "control".
+          If you want to have multi factor authentication, use "required".
+          If you want to use Yubikey instead of regular password, use "sufficient".
+
+          Read
+          <citerefentry>
+            <refentrytitle>pam.conf</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry>
+          for better understanding of this option.
+        '';
+      };
+      id = mkOption {
+        example = "42";
+        type = types.str;
+        description = "client id";
+      };
+
+      debug = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Debug output to stderr.
+        '';
+      };
+      mode = mkOption {
+        default = "client";
+        type = types.enum [ "client" "challenge-response" ];
+        description = ''
+          Mode of operation.
+
+          Use "client" for online validation with a YubiKey validation service such as
+          the YubiCloud.
+
+          Use "challenge-response" for offline validation using YubiKeys with HMAC-SHA-1
+          Challenge-Response configurations. See the man-page ykpamcfg(1) for further
+          details on how to configure offline Challenge-Response validation.
+
+          More information can be found <link
+          xlink:href="https://developers.yubico.com/yubico-pam/Authentication_Using_Challenge-Response.html">here</link>.
+        '';
+      };
+      challengeResponsePath = mkOption {
+        default = null;
+        type = types.nullOr types.path;
+        description = ''
+          If not null, set the path used by yubico pam module where the challenge expected response is stored.
+
+          More information can be found <link
+          xlink:href="https://developers.yubico.com/yubico-pam/Authentication_Using_Challenge-Response.html">here</link>.
+        '';
+      };
+    };
+
+    security.pam.enableEcryptfs = mkEnableOption "eCryptfs PAM module (mounting ecryptfs home directory on login)";
+
+    users.motd = mkOption {
+      default = null;
+      example = "Today is Sweetmorn, the 4th day of The Aftermath in the YOLD 3178.";
+      type = types.nullOr types.lines;
+      description = "Message of the day shown to users when they log in.";
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = {
+
+    environment.systemPackages =
+      # Include the PAM modules in the system path mostly for the manpages.
+      [ pkgs.pam ]
+      ++ optional config.users.ldap.enable pam_ldap
+      ++ optional config.services.sssd.enable pkgs.sssd
+      ++ optionals config.krb5.enable [pam_krb5 pam_ccreds]
+      ++ optionals config.security.pam.enableOTPW [ pkgs.otpw ]
+      ++ optionals config.security.pam.oath.enable [ pkgs.oathToolkit ]
+      ++ optionals config.security.pam.p11.enable [ pkgs.pam_p11 ]
+      ++ optionals config.security.pam.u2f.enable [ pkgs.pam_u2f ];
+
+    boot.supportedFilesystems = optionals config.security.pam.enableEcryptfs [ "ecryptfs" ];
+
+    security.wrappers = {
+      unix_chkpwd = {
+        setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${pkgs.pam}/bin/unix_chkpwd";
+      };
+    };
+
+    environment.etc = mapAttrs' makePAMService config.security.pam.services;
+
+    security.pam.services =
+      { other.text =
+          ''
+            auth     required pam_warn.so
+            auth     required pam_deny.so
+            account  required pam_warn.so
+            account  required pam_deny.so
+            password required pam_warn.so
+            password required pam_deny.so
+            session  required pam_warn.so
+            session  required pam_deny.so
+          '';
+
+        # Most of these should be moved to specific modules.
+        i3lock = {};
+        i3lock-color = {};
+        vlock = {};
+        xlock = {};
+        xscreensaver = {};
+
+        runuser = { rootOK = true; unixAuth = false; setEnvironment = false; };
+
+        /* FIXME: should runuser -l start a systemd session? Currently
+           it complains "Cannot create session: Already running in a
+           session". */
+        runuser-l = { rootOK = true; unixAuth = false; };
+      };
+
+    security.apparmor.includes."abstractions/pam" = let
+      isEnabled = test: fold or false (map test (attrValues config.security.pam.services));
+      in
+      lib.concatMapStrings
+        (name: "r ${config.environment.etc."pam.d/${name}".source},\n")
+        (attrNames config.security.pam.services) +
+      ''
+      mr ${getLib pkgs.pam}/lib/security/pam_filter/*,
+      mr ${getLib pkgs.pam}/lib/security/pam_*.so,
+      r ${getLib pkgs.pam}/lib/security/,
+      '' +
+      optionalString use_ldap ''
+         mr ${pam_ldap}/lib/security/pam_ldap.so,
+      '' +
+      optionalString config.services.sssd.enable ''
+        mr ${pkgs.sssd}/lib/security/pam_sss.so,
+      '' +
+      optionalString config.krb5.enable ''
+        mr ${pam_krb5}/lib/security/pam_krb5.so,
+        mr ${pam_ccreds}/lib/security/pam_ccreds.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.googleOsLoginAccountVerification)) ''
+        mr ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_login.so,
+        mr ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_admin.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.googleOsLoginAuthentication)) ''
+        mr ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_login.so,
+      '' +
+      optionalString (config.security.pam.enableSSHAgentAuth
+                     && isEnabled (cfg: cfg.sshAgentAuth)) ''
+        mr ${pkgs.pam_ssh_agent_auth}/libexec/pam_ssh_agent_auth.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.fprintAuth)) ''
+        mr ${pkgs.fprintd}/lib/security/pam_fprintd.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.u2fAuth)) ''
+        mr ${pkgs.pam_u2f}/lib/security/pam_u2f.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.usbAuth)) ''
+        mr ${pkgs.pam_usb}/lib/security/pam_usb.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.oathAuth)) ''
+        "mr ${pkgs.oathToolkit}/lib/security/pam_oath.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.yubicoAuth)) ''
+        mr ${pkgs.yubico-pam}/lib/security/pam_yubico.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.duoSecurity.enable)) ''
+        mr ${pkgs.duo-unix}/lib/security/pam_duo.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.otpwAuth)) ''
+        mr ${pkgs.otpw}/lib/security/pam_otpw.so,
+      '' +
+      optionalString config.security.pam.enableEcryptfs ''
+        mr ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.pamMount)) ''
+        mr ${pkgs.pam_mount}/lib/security/pam_mount.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.enableGnomeKeyring)) ''
+        mr ${pkgs.gnome3.gnome-keyring}/lib/security/pam_gnome_keyring.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.startSession)) ''
+        mr ${pkgs.systemd}/lib/security/pam_systemd.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.enableAppArmor)
+                     && config.security.apparmor.enable) ''
+        mr ${pkgs.apparmor-pam}/lib/security/pam_apparmor.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.enableKwallet)) ''
+        mr ${pkgs.plasma5Packages.kwallet-pam}/lib/security/pam_kwallet5.so,
+      '' +
+      optionalString config.virtualisation.lxc.lxcfs.enable ''
+        mr ${pkgs.lxc}/lib/security/pam_cgfs.so
+      '';
+  };
+
+}
diff --git a/nixos/modules/security/pam_mount.nix b/nixos/modules/security/pam_mount.nix
new file mode 100644
index 00000000000..462b7f89e2f
--- /dev/null
+++ b/nixos/modules/security/pam_mount.nix
@@ -0,0 +1,102 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.security.pam.mount;
+
+  anyPamMount = any (attrByPath ["pamMount"] false) (attrValues config.security.pam.services);
+in
+
+{
+  options = {
+
+    security.pam.mount = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable PAM mount system to mount fileystems on user login.
+        '';
+      };
+
+      extraVolumes = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          List of volume definitions for pam_mount.
+          For more information, visit <link
+          xlink:href="http://pam-mount.sourceforge.net/pam_mount.conf.5.html" />.
+        '';
+      };
+
+      additionalSearchPaths = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "[ pkgs.bindfs ]";
+        description = ''
+          Additional programs to include in the search path of pam_mount.
+          Useful for example if you want to use some FUSE filesystems like bindfs.
+        '';
+      };
+
+      fuseMountOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = literalExpression ''
+          [ "nodev" "nosuid" "force-user=%(USER)" "gid=%(USERGID)" "perms=0700" "chmod-deny" "chown-deny" "chgrp-deny" ]
+        '';
+        description = ''
+          Global mount options that apply to every FUSE volume.
+          You can define volume-specific options in the volume definitions.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf (cfg.enable || anyPamMount) {
+
+    environment.systemPackages = [ pkgs.pam_mount ];
+    environment.etc."security/pam_mount.conf.xml" = {
+      source =
+        let
+          extraUserVolumes = filterAttrs (n: u: u.cryptHomeLuks != null || u.pamMount != {}) config.users.users;
+          mkAttr = k: v: ''${k}="${v}"'';
+          userVolumeEntry = user: let
+            attrs = {
+              user = user.name;
+              path = user.cryptHomeLuks;
+              mountpoint = user.home;
+            } // user.pamMount;
+          in
+            "<volume ${concatStringsSep " " (mapAttrsToList mkAttr attrs)} />\n";
+        in
+         pkgs.writeText "pam_mount.conf.xml" ''
+          <?xml version="1.0" encoding="utf-8" ?>
+          <!DOCTYPE pam_mount SYSTEM "pam_mount.conf.xml.dtd">
+          <!-- auto generated from Nixos: modules/config/users-groups.nix -->
+          <pam_mount>
+          <debug enable="0" />
+
+          <!-- if activated, requires ofl from hxtools to be present -->
+          <logout wait="0" hup="no" term="no" kill="no" />
+          <!-- set PATH variable for pam_mount module -->
+          <path>${makeBinPath ([ pkgs.util-linux ] ++ cfg.additionalSearchPaths)}</path>
+          <!-- create mount point if not present -->
+          <mkmountpoint enable="1" remove="true" />
+
+          <!-- specify the binaries to be called -->
+          <fusemount>${pkgs.fuse}/bin/mount.fuse %(VOLUME) %(MNTPT) -o ${concatStringsSep "," (cfg.fuseMountOptions ++ [ "%(OPTIONS)" ])}</fusemount>
+          <cryptmount>${pkgs.pam_mount}/bin/mount.crypt %(VOLUME) %(MNTPT)</cryptmount>
+          <cryptumount>${pkgs.pam_mount}/bin/umount.crypt %(MNTPT)</cryptumount>
+          <pmvarrun>${pkgs.pam_mount}/bin/pmvarrun -u %(USER) -o %(OPERATION)</pmvarrun>
+
+          ${concatStrings (map userVolumeEntry (attrValues extraUserVolumes))}
+          ${concatStringsSep "\n" cfg.extraVolumes}
+          </pam_mount>
+          '';
+    };
+
+  };
+}
diff --git a/nixos/modules/security/pam_usb.nix b/nixos/modules/security/pam_usb.nix
new file mode 100644
index 00000000000..51d81e823f8
--- /dev/null
+++ b/nixos/modules/security/pam_usb.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.security.pam.usb;
+
+  anyUsbAuth = any (attrByPath ["usbAuth"] false) (attrValues config.security.pam.services);
+
+in
+
+{
+  options = {
+
+    security.pam.usb = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable USB login for all login systems that support it.  For
+          more information, visit <link
+          xlink:href="https://github.com/aluzzardi/pam_usb/wiki/Getting-Started#setting-up-devices-and-users" />.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf (cfg.enable || anyUsbAuth) {
+
+    # Make sure pmount and pumount are setuid wrapped.
+    security.wrappers = {
+      pmount =
+        { setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${pkgs.pmount.out}/bin/pmount";
+        };
+      pumount =
+        { setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${pkgs.pmount.out}/bin/pumount";
+        };
+    };
+
+    environment.systemPackages = [ pkgs.pmount ];
+
+  };
+}
diff --git a/nixos/modules/security/polkit.nix b/nixos/modules/security/polkit.nix
new file mode 100644
index 00000000000..1ba149745c6
--- /dev/null
+++ b/nixos/modules/security/polkit.nix
@@ -0,0 +1,112 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.security.polkit;
+
+in
+
+{
+
+  options = {
+
+    security.polkit.enable = mkEnableOption "polkit";
+
+    security.polkit.extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example =
+        ''
+          /* Log authorization checks. */
+          polkit.addRule(function(action, subject) {
+            polkit.log("user " +  subject.user + " is attempting action " + action.id + " from PID " + subject.pid);
+          });
+
+          /* Allow any local user to do anything (dangerous!). */
+          polkit.addRule(function(action, subject) {
+            if (subject.local) return "yes";
+          });
+        '';
+      description =
+        ''
+          Any polkit rules to be added to config (in JavaScript ;-). See:
+          http://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html#polkit-rules
+        '';
+    };
+
+    security.polkit.adminIdentities = mkOption {
+      type = types.listOf types.str;
+      default = [ "unix-group:wheel" ];
+      example = [ "unix-user:alice" "unix-group:admin" ];
+      description =
+        ''
+          Specifies which users are considered “administratorsâ€, for those
+          actions that require the user to authenticate as an
+          administrator (i.e. have an <literal>auth_admin</literal>
+          value).  By default, this is all users in the <literal>wheel</literal> group.
+        '';
+    };
+
+  };
+
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.polkit.bin pkgs.polkit.out ];
+
+    systemd.packages = [ pkgs.polkit.out ];
+
+    systemd.services.polkit.restartTriggers = [ config.system.path ];
+    systemd.services.polkit.stopIfChanged = false;
+
+    # The polkit daemon reads action/rule files
+    environment.pathsToLink = [ "/share/polkit-1" ];
+
+    # PolKit rules for NixOS.
+    environment.etc."polkit-1/rules.d/10-nixos.rules".text =
+      ''
+        polkit.addAdminRule(function(action, subject) {
+          return [${concatStringsSep ", " (map (i: "\"${i}\"") cfg.adminIdentities)}];
+        });
+
+        ${cfg.extraConfig}
+      ''; #TODO: validation on compilation (at least against typos)
+
+    services.dbus.packages = [ pkgs.polkit.out ];
+
+    security.pam.services.polkit-1 = {};
+
+    security.wrappers = {
+      pkexec =
+        { setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${pkgs.polkit.bin}/bin/pkexec";
+        };
+      polkit-agent-helper-1 =
+        { setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${pkgs.polkit.out}/lib/polkit-1/polkit-agent-helper-1";
+        };
+    };
+
+    systemd.tmpfiles.rules = [
+      # Probably no more needed, clean up
+      "R /var/lib/polkit-1"
+      "R /var/lib/PolicyKit"
+    ];
+
+    users.users.polkituser = {
+      description = "PolKit daemon";
+      uid = config.ids.uids.polkituser;
+      group = "polkituser";
+    };
+
+    users.groups.polkituser = {};
+  };
+
+}
+
diff --git a/nixos/modules/security/rngd.nix b/nixos/modules/security/rngd.nix
new file mode 100644
index 00000000000..8cca1c26d68
--- /dev/null
+++ b/nixos/modules/security/rngd.nix
@@ -0,0 +1,16 @@
+{ lib, ... }:
+let
+  removed = k: lib.mkRemovedOptionModule [ "security" "rngd" k ];
+in
+{
+  imports = [
+    (removed "enable" ''
+       rngd is not necessary for any device that the kernel recognises
+       as an hardware RNG, as it will automatically run the krngd task
+       to periodically collect random data from the device and mix it
+       into the kernel's RNG.
+    '')
+    (removed "debug"
+      "The rngd module was removed, so its debug option does nothing.")
+  ];
+}
diff --git a/nixos/modules/security/rtkit.nix b/nixos/modules/security/rtkit.nix
new file mode 100644
index 00000000000..ad8746808e8
--- /dev/null
+++ b/nixos/modules/security/rtkit.nix
@@ -0,0 +1,47 @@
+# A module for ‘rtkit’, a DBus system service that hands out realtime
+# scheduling priority to processes that ask for it.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  options = {
+
+    security.rtkit.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable the RealtimeKit system service, which hands
+        out realtime scheduling priority to user processes on
+        demand. For example, the PulseAudio server uses this to
+        acquire realtime priority.
+      '';
+    };
+
+  };
+
+
+  config = mkIf config.security.rtkit.enable {
+
+    security.polkit.enable = true;
+
+    # To make polkit pickup rtkit policies
+    environment.systemPackages = [ pkgs.rtkit ];
+
+    systemd.packages = [ pkgs.rtkit ];
+
+    services.dbus.packages = [ pkgs.rtkit ];
+
+    users.users.rtkit =
+      {
+        isSystemUser = true;
+        group = "rtkit";
+        description = "RealtimeKit daemon";
+      };
+    users.groups.rtkit = {};
+
+  };
+
+}
diff --git a/nixos/modules/security/sudo.nix b/nixos/modules/security/sudo.nix
new file mode 100644
index 00000000000..99e578f8ada
--- /dev/null
+++ b/nixos/modules/security/sudo.nix
@@ -0,0 +1,265 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.security.sudo;
+
+  inherit (pkgs) sudo;
+
+  toUserString = user: if (isInt user) then "#${toString user}" else "${user}";
+  toGroupString = group: if (isInt group) then "%#${toString group}" else "%${group}";
+
+  toCommandOptionsString = options:
+    "${concatStringsSep ":" options}${optionalString (length options != 0) ":"} ";
+
+  toCommandsString = commands:
+    concatStringsSep ", " (
+      map (command:
+        if (isString command) then
+          command
+        else
+          "${toCommandOptionsString command.options}${command.command}"
+      ) commands
+    );
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    security.sudo.enable = mkOption {
+      type = types.bool;
+      default = true;
+      description =
+        ''
+          Whether to enable the <command>sudo</command> command, which
+          allows non-root users to execute commands as root.
+        '';
+    };
+
+    security.sudo.package = mkOption {
+      type = types.package;
+      default = pkgs.sudo;
+      defaultText = literalExpression "pkgs.sudo";
+      description = ''
+        Which package to use for `sudo`.
+      '';
+    };
+
+    security.sudo.wheelNeedsPassword = mkOption {
+      type = types.bool;
+      default = true;
+      description =
+        ''
+          Whether users of the <code>wheel</code> group must
+          provide a password to run commands as super user via <command>sudo</command>.
+        '';
+      };
+
+    security.sudo.execWheelOnly = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Only allow members of the <code>wheel</code> group to execute sudo by
+        setting the executable's permissions accordingly.
+        This prevents users that are not members of <code>wheel</code> from
+        exploiting vulnerabilities in sudo such as CVE-2021-3156.
+      '';
+    };
+
+    security.sudo.configFile = mkOption {
+      type = types.lines;
+      # Note: if syntax errors are detected in this file, the NixOS
+      # configuration will fail to build.
+      description =
+        ''
+          This string contains the contents of the
+          <filename>sudoers</filename> file.
+        '';
+    };
+
+    security.sudo.extraRules = mkOption {
+      description = ''
+        Define specific rules to be in the <filename>sudoers</filename> file.
+        More specific rules should come after more general ones in order to
+        yield the expected behavior. You can use mkBefore/mkAfter to ensure
+        this is the case when configuration options are merged.
+      '';
+      default = [];
+      example = literalExpression ''
+        [
+          # Allow execution of any command by all users in group sudo,
+          # requiring a password.
+          { groups = [ "sudo" ]; commands = [ "ALL" ]; }
+
+          # Allow execution of "/home/root/secret.sh" by user `backup`, `database`
+          # and the group with GID `1006` without a password.
+          { users = [ "backup" "database" ]; groups = [ 1006 ];
+            commands = [ { command = "/home/root/secret.sh"; options = [ "SETENV" "NOPASSWD" ]; } ]; }
+
+          # Allow all users of group `bar` to run two executables as user `foo`
+          # with arguments being pre-set.
+          { groups = [ "bar" ]; runAs = "foo";
+            commands =
+              [ "/home/baz/cmd1.sh hello-sudo"
+                  { command = '''/home/baz/cmd2.sh ""'''; options = [ "SETENV" ]; } ]; }
+        ]
+      '';
+      type = with types; listOf (submodule {
+        options = {
+          users = mkOption {
+            type = with types; listOf (either str int);
+            description = ''
+              The usernames / UIDs this rule should apply for.
+            '';
+            default = [];
+          };
+
+          groups = mkOption {
+            type = with types; listOf (either str int);
+            description = ''
+              The groups / GIDs this rule should apply for.
+            '';
+            default = [];
+          };
+
+          host = mkOption {
+            type = types.str;
+            default = "ALL";
+            description = ''
+              For what host this rule should apply.
+            '';
+          };
+
+          runAs = mkOption {
+            type = with types; str;
+            default = "ALL:ALL";
+            description = ''
+              Under which user/group the specified command is allowed to run.
+
+              A user can be specified using just the username: <code>"foo"</code>.
+              It is also possible to specify a user/group combination using <code>"foo:bar"</code>
+              or to only allow running as a specific group with <code>":bar"</code>.
+            '';
+          };
+
+          commands = mkOption {
+            description = ''
+              The commands for which the rule should apply.
+            '';
+            type = with types; listOf (either str (submodule {
+
+              options = {
+                command = mkOption {
+                  type = with types; str;
+                  description = ''
+                    A command being either just a path to a binary to allow any arguments,
+                    the full command with arguments pre-set or with <code>""</code> used as the argument,
+                    not allowing arguments to the command at all.
+                  '';
+                };
+
+                options = mkOption {
+                  type = with types; listOf (enum [ "NOPASSWD" "PASSWD" "NOEXEC" "EXEC" "SETENV" "NOSETENV" "LOG_INPUT" "NOLOG_INPUT" "LOG_OUTPUT" "NOLOG_OUTPUT" ]);
+                  description = ''
+                    Options for running the command. Refer to the <a href="https://www.sudo.ws/man/1.7.10/sudoers.man.html">sudo manual</a>.
+                  '';
+                  default = [];
+                };
+              };
+
+            }));
+          };
+        };
+      });
+    };
+
+    security.sudo.extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Extra configuration text appended to <filename>sudoers</filename>.
+      '';
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    # We `mkOrder 600` so that the default rule shows up first, but there is
+    # still enough room for a user to `mkBefore` it.
+    security.sudo.extraRules = mkOrder 600 [
+      { groups = [ "wheel" ];
+        commands = [ { command = "ALL"; options = (if cfg.wheelNeedsPassword then [ "SETENV" ] else [ "NOPASSWD" "SETENV" ]); } ];
+      }
+    ];
+
+    security.sudo.configFile =
+      ''
+        # Don't edit this file. Set the NixOS options ‘security.sudo.configFile’
+        # or ‘security.sudo.extraRules’ instead.
+
+        # Keep SSH_AUTH_SOCK so that pam_ssh_agent_auth.so can do its magic.
+        Defaults env_keep+=SSH_AUTH_SOCK
+
+        # "root" is allowed to do anything.
+        root        ALL=(ALL:ALL) SETENV: ALL
+
+        # extraRules
+        ${concatStringsSep "\n" (
+          lists.flatten (
+            map (
+              rule: if (length rule.commands != 0) then [
+                (map (user: "${toUserString user}	${rule.host}=(${rule.runAs})	${toCommandsString rule.commands}") rule.users)
+                (map (group: "${toGroupString group}	${rule.host}=(${rule.runAs})	${toCommandsString rule.commands}") rule.groups)
+              ] else []
+            ) cfg.extraRules
+          )
+        )}
+
+        ${cfg.extraConfig}
+      '';
+
+    security.wrappers = let
+      owner = "root";
+      group = if cfg.execWheelOnly then "wheel" else "root";
+      setuid = true;
+      permissions = if cfg.execWheelOnly then "u+rx,g+x" else "u+rx,g+x,o+x";
+    in {
+      sudo = {
+        source = "${cfg.package.out}/bin/sudo";
+        inherit owner group setuid permissions;
+      };
+      sudoedit = {
+        source = "${cfg.package.out}/bin/sudoedit";
+        inherit owner group setuid permissions;
+      };
+    };
+
+    environment.systemPackages = [ sudo ];
+
+    security.pam.services.sudo = { sshAgentAuth = true; };
+
+    environment.etc.sudoers =
+      { source =
+          pkgs.runCommand "sudoers"
+          {
+            src = pkgs.writeText "sudoers-in" cfg.configFile;
+            preferLocalBuild = true;
+          }
+          # Make sure that the sudoers file is syntactically valid.
+          # (currently disabled - NIXOS-66)
+          "${pkgs.buildPackages.sudo}/sbin/visudo -f $src -c && cp $src $out";
+        mode = "0440";
+      };
+
+  };
+
+}
diff --git a/nixos/modules/security/systemd-confinement.nix b/nixos/modules/security/systemd-confinement.nix
new file mode 100644
index 00000000000..f3a2de3bf87
--- /dev/null
+++ b/nixos/modules/security/systemd-confinement.nix
@@ -0,0 +1,202 @@
+{ config, pkgs, lib, utils, ... }:
+
+let
+  toplevelConfig = config;
+  inherit (lib) types;
+  inherit (utils.systemdUtils.lib) mkPathSafeName;
+in {
+  options.systemd.services = lib.mkOption {
+    type = types.attrsOf (types.submodule ({ name, config, ... }: {
+      options.confinement.enable = lib.mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If set, all the required runtime store paths for this service are
+          bind-mounted into a <literal>tmpfs</literal>-based <citerefentry>
+            <refentrytitle>chroot</refentrytitle>
+            <manvolnum>2</manvolnum>
+          </citerefentry>.
+        '';
+      };
+
+      options.confinement.fullUnit = lib.mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to include the full closure of the systemd unit file into the
+          chroot, instead of just the dependencies for the executables.
+
+          <warning><para>While it may be tempting to just enable this option to
+          make things work quickly, please be aware that this might add paths
+          to the closure of the chroot that you didn't anticipate. It's better
+          to use <option>confinement.packages</option> to <emphasis
+          role="strong">explicitly</emphasis> add additional store paths to the
+          chroot.</para></warning>
+        '';
+      };
+
+      options.confinement.packages = lib.mkOption {
+        type = types.listOf (types.either types.str types.package);
+        default = [];
+        description = let
+          mkScOption = optName: "<option>serviceConfig.${optName}</option>";
+        in ''
+          Additional packages or strings with context to add to the closure of
+          the chroot. By default, this includes all the packages from the
+          ${lib.concatMapStringsSep ", " mkScOption [
+            "ExecReload" "ExecStartPost" "ExecStartPre" "ExecStop"
+            "ExecStopPost"
+          ]} and ${mkScOption "ExecStart"} options. If you want to have all the
+          dependencies of this systemd unit, you can use
+          <option>confinement.fullUnit</option>.
+
+          <note><para>The store paths listed in <option>path</option> are
+          <emphasis role="strong">not</emphasis> included in the closure as
+          well as paths from other options except those listed
+          above.</para></note>
+        '';
+      };
+
+      options.confinement.binSh = lib.mkOption {
+        type = types.nullOr types.path;
+        default = toplevelConfig.environment.binsh;
+        defaultText = lib.literalExpression "config.environment.binsh";
+        example = lib.literalExpression ''"''${pkgs.dash}/bin/dash"'';
+        description = ''
+          The program to make available as <filename>/bin/sh</filename> inside
+          the chroot. If this is set to <literal>null</literal>, no
+          <filename>/bin/sh</filename> is provided at all.
+
+          This is useful for some applications, which for example use the
+          <citerefentry>
+            <refentrytitle>system</refentrytitle>
+            <manvolnum>3</manvolnum>
+          </citerefentry> library function to execute commands.
+        '';
+      };
+
+      options.confinement.mode = lib.mkOption {
+        type = types.enum [ "full-apivfs" "chroot-only" ];
+        default = "full-apivfs";
+        description = ''
+          The value <literal>full-apivfs</literal> (the default) sets up
+          private <filename class="directory">/dev</filename>, <filename
+          class="directory">/proc</filename>, <filename
+          class="directory">/sys</filename> and <filename
+          class="directory">/tmp</filename> file systems in a separate user
+          name space.
+
+          If this is set to <literal>chroot-only</literal>, only the file
+          system name space is set up along with the call to <citerefentry>
+            <refentrytitle>chroot</refentrytitle>
+            <manvolnum>2</manvolnum>
+          </citerefentry>.
+
+          <note><para>This doesn't cover network namespaces and is solely for
+          file system level isolation.</para></note>
+        '';
+      };
+
+      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 {
+        serviceConfig = {
+          RootDirectory = "/var/empty";
+          TemporaryFileSystem = "/";
+          PrivateMounts = lib.mkDefault true;
+
+          # https://github.com/NixOS/nixpkgs/issues/14645 is a future attempt
+          # to change some of these to default to true.
+          #
+          # If we run in chroot-only mode, having something like PrivateDevices
+          # set to true by default will mount /dev within the chroot, whereas
+          # with "chroot-only" it's expected that there are no /dev, /proc and
+          # /sys file systems available.
+          #
+          # However, if this suddenly becomes true, the attack surface will
+          # increase, so let's explicitly set these options to true/false
+          # depending on the mode.
+          MountAPIVFS = wantsAPIVFS;
+          PrivateDevices = wantsAPIVFS;
+          PrivateTmp = wantsAPIVFS;
+          PrivateUsers = wantsAPIVFS;
+          ProtectControlGroups = wantsAPIVFS;
+          ProtectKernelModules = wantsAPIVFS;
+          ProtectKernelTunables = wantsAPIVFS;
+        };
+        confinement.packages = let
+          execOpts = [
+            "ExecReload" "ExecStart" "ExecStartPost" "ExecStartPre" "ExecStop"
+            "ExecStopPost"
+          ];
+          execPkgs = lib.concatMap (opt: let
+            isSet = config.serviceConfig ? ${opt};
+          in lib.flatten (lib.optional isSet config.serviceConfig.${opt})) execOpts;
+          unitAttrs = toplevelConfig.systemd.units."${name}.service";
+          allPkgs = lib.singleton (builtins.toJSON unitAttrs);
+          unitPkgs = if fullUnit then allPkgs else execPkgs;
+        in unitPkgs ++ lib.optional (binSh != null) binSh;
+      };
+    }));
+  };
+
+  config.assertions = lib.concatLists (lib.mapAttrsToList (name: cfg: let
+    whatOpt = optName: "The 'serviceConfig' option '${optName}' for"
+                    + " service '${name}' is enabled in conjunction with"
+                    + " 'confinement.enable'";
+  in lib.optionals cfg.confinement.enable [
+    { assertion = !cfg.serviceConfig.RootDirectoryStartOnly or false;
+      message = "${whatOpt "RootDirectoryStartOnly"}, but right now systemd"
+              + " doesn't support restricting bind-mounts to 'ExecStart'."
+              + " Please either define a separate service or find a way to run"
+              + " commands other than ExecStart within the chroot.";
+    }
+    { assertion = !cfg.serviceConfig.DynamicUser or false;
+      message = "${whatOpt "DynamicUser"}. Please create a dedicated user via"
+              + " the 'users.users' option instead as this combination is"
+              + " currently not supported.";
+    }
+    { assertion = cfg.serviceConfig ? ProtectSystem -> cfg.serviceConfig.ProtectSystem == false;
+      message = "${whatOpt "ProtectSystem"}. ProtectSystem is not compatible"
+              + " with service confinement as it fails to remount /usr within"
+              + " our chroot. Please disable the option.";
+    }
+  ]) config.systemd.services);
+
+  config.systemd.packages = lib.concatLists (lib.mapAttrsToList (name: cfg: let
+    rootPaths = let
+      contents = lib.concatStringsSep "\n" cfg.confinement.packages;
+    in pkgs.writeText "${mkPathSafeName name}-string-contexts.txt" contents;
+
+    chrootPaths = pkgs.runCommand "${mkPathSafeName name}-chroot-paths" {
+      closureInfo = pkgs.closureInfo { inherit rootPaths; };
+      serviceName = "${name}.service";
+      excludedPath = rootPaths;
+    } ''
+      mkdir -p "$out/lib/systemd/system/$serviceName.d"
+      serviceFile="$out/lib/systemd/system/$serviceName.d/confinement.conf"
+
+      echo '[Service]' > "$serviceFile"
+
+      # /bin/sh is special here, because the option value could contain a
+      # symlink and we need to properly resolve it.
+      ${lib.optionalString (cfg.confinement.binSh != null) ''
+        binsh=${lib.escapeShellArg cfg.confinement.binSh}
+        realprog="$(readlink -e "$binsh")"
+        echo "BindReadOnlyPaths=$realprog:/bin/sh" >> "$serviceFile"
+      ''}
+
+      while read storePath; do
+        if [ -L "$storePath" ]; then
+          # Currently, systemd can't cope with symlinks in Bind(ReadOnly)Paths,
+          # so let's just bind-mount the target to that location.
+          echo "BindReadOnlyPaths=$(readlink -e "$storePath"):$storePath"
+        elif [ "$storePath" != "$excludedPath" ]; then
+          echo "BindReadOnlyPaths=$storePath"
+        fi
+      done < "$closureInfo/store-paths" >> "$serviceFile"
+    '';
+  in lib.optional cfg.confinement.enable chrootPaths) config.systemd.services);
+}
diff --git a/nixos/modules/security/tpm2.nix b/nixos/modules/security/tpm2.nix
new file mode 100644
index 00000000000..be85fd246e3
--- /dev/null
+++ b/nixos/modules/security/tpm2.nix
@@ -0,0 +1,184 @@
+{ lib, pkgs, config, ... }:
+let
+  cfg = config.security.tpm2;
+
+  # This snippet is taken from tpm2-tss/dist/tpm-udev.rules, but modified to allow custom user/groups
+  # The idea is that the tssUser is allowed to acess the TPM and kernel TPM resource manager, while
+  # the tssGroup is only allowed to access the kernel resource manager
+  # Therefore, if either of the two are null, the respective part isn't generated
+  udevRules = tssUser: tssGroup: ''
+    ${lib.optionalString (tssUser != null) ''KERNEL=="tpm[0-9]*", MODE="0660", OWNER="${tssUser}"''}
+    ${lib.optionalString (tssUser != null || tssGroup != null)
+      ''KERNEL=="tpmrm[0-9]*", MODE="0660"''
+      + lib.optionalString (tssUser != null) '', OWNER="${tssUser}"''
+      + lib.optionalString (tssGroup != null) '', GROUP="${tssGroup}"''
+     }
+  '';
+
+in {
+  options.security.tpm2 = {
+    enable = lib.mkEnableOption "Trusted Platform Module 2 support";
+
+    tssUser = lib.mkOption {
+      description = ''
+        Name of the tpm device-owner and service user, set if applyUdevRules is
+        set.
+      '';
+      type = lib.types.nullOr lib.types.str;
+      default = if cfg.abrmd.enable then "tss" else "root";
+      defaultText = lib.literalExpression ''if config.security.tpm2.abrmd.enable then "tss" else "root"'';
+    };
+
+    tssGroup = lib.mkOption {
+      description = ''
+        Group of the tpm kernel resource manager (tpmrm) device-group, set if
+        applyUdevRules is set.
+      '';
+      type = lib.types.nullOr lib.types.str;
+      default = "tss";
+    };
+
+    applyUdevRules = lib.mkOption {
+      description = ''
+        Whether to make the /dev/tpm[0-9] devices accessible by the tssUser, or
+        the /dev/tpmrm[0-9] by tssGroup respectively
+      '';
+      type = lib.types.bool;
+      default = true;
+    };
+
+    abrmd = {
+      enable = lib.mkEnableOption ''
+        Trusted Platform 2 userspace resource manager daemon
+      '';
+
+      package = lib.mkOption {
+        description = "tpm2-abrmd package to use";
+        type = lib.types.package;
+        default = pkgs.tpm2-abrmd;
+        defaultText = lib.literalExpression "pkgs.tpm2-abrmd";
+      };
+    };
+
+    pkcs11 = {
+      enable = lib.mkEnableOption ''
+        TPM2 PKCS#11 tool and shared library in system path
+        (<literal>/run/current-system/sw/lib/libtpm2_pkcs11.so</literal>)
+      '';
+
+      package = lib.mkOption {
+        description = "tpm2-pkcs11 package to use";
+        type = lib.types.package;
+        default = pkgs.tpm2-pkcs11;
+        defaultText = lib.literalExpression "pkgs.tpm2-pkcs11";
+      };
+    };
+
+    tctiEnvironment = {
+      enable = lib.mkOption {
+        description = ''
+          Set common TCTI environment variables to the specified value.
+          The variables are
+          <itemizedlist>
+            <listitem>
+              <para>
+                <literal>TPM2TOOLS_TCTI</literal>
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>TPM2_PKCS11_TCTI</literal>
+              </para>
+            </listitem>
+          </itemizedlist>
+        '';
+        type = lib.types.bool;
+        default = false;
+      };
+
+      interface = lib.mkOption {
+        description = ''
+          The name of the TPM command transmission interface (TCTI) library to
+          use.
+        '';
+        type = lib.types.enum [ "tabrmd" "device" ];
+        default = "device";
+      };
+
+      deviceConf = lib.mkOption {
+        description = ''
+          Configuration part of the device TCTI, e.g. the path to the TPM device.
+          Applies if interface is set to "device".
+          The format is specified in the
+          <link xlink:href="https://github.com/tpm2-software/tpm2-tools/blob/master/man/common/tcti.md#tcti-options">
+          tpm2-tools repository</link>.
+        '';
+        type = lib.types.str;
+        default = "/dev/tpmrm0";
+      };
+
+      tabrmdConf = lib.mkOption {
+        description = ''
+          Configuration part of the tabrmd TCTI, like the D-Bus bus name.
+          Applies if interface is set to "tabrmd".
+          The format is specified in the
+          <link xlink:href="https://github.com/tpm2-software/tpm2-tools/blob/master/man/common/tcti.md#tcti-options">
+          tpm2-tools repository</link>.
+        '';
+        type = lib.types.str;
+        default = "bus_name=com.intel.tss2.Tabrmd";
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable (lib.mkMerge [
+    {
+      # PKCS11 tools and library
+      environment.systemPackages = lib.mkIf cfg.pkcs11.enable [
+        (lib.getBin cfg.pkcs11.package)
+        (lib.getLib cfg.pkcs11.package)
+      ];
+
+      services.udev.extraRules = lib.mkIf cfg.applyUdevRules
+        (udevRules cfg.tssUser cfg.tssGroup);
+
+      # Create the tss user and group only if the default value is used
+      users.users.${cfg.tssUser} = lib.mkIf (cfg.tssUser == "tss") {
+        isSystemUser = true;
+        group = "tss";
+      };
+      users.groups.${cfg.tssGroup} = lib.mkIf (cfg.tssGroup == "tss") {};
+
+      environment.variables = lib.mkIf cfg.tctiEnvironment.enable (
+        lib.attrsets.genAttrs [
+          "TPM2TOOLS_TCTI"
+          "TPM2_PKCS11_TCTI"
+        ] (_: ''${cfg.tctiEnvironment.interface}:${
+          if cfg.tctiEnvironment.interface == "tabrmd" then
+            cfg.tctiEnvironment.tabrmdConf
+          else
+            cfg.tctiEnvironment.deviceConf
+        }'')
+      );
+    }
+
+    (lib.mkIf cfg.abrmd.enable {
+      systemd.services."tpm2-abrmd" = {
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          Type = "dbus";
+          Restart = "always";
+          RestartSec = 30;
+          BusName = "com.intel.tss2.Tabrmd";
+          ExecStart = "${cfg.abrmd.package}/bin/tpm2-abrmd";
+          User = "tss";
+          Group = "tss";
+        };
+      };
+
+      services.dbus.packages = lib.singleton cfg.abrmd.package;
+    })
+  ]);
+
+  meta.maintainers = with lib.maintainers; [ lschuermann ];
+}
diff --git a/nixos/modules/security/wrappers/default.nix b/nixos/modules/security/wrappers/default.nix
new file mode 100644
index 00000000000..e63f19010de
--- /dev/null
+++ b/nixos/modules/security/wrappers/default.nix
@@ -0,0 +1,305 @@
+{ config, lib, pkgs, ... }:
+let
+
+  inherit (config.security) wrapperDir wrappers;
+
+  parentWrapperDir = dirOf wrapperDir;
+
+  securityWrapper = pkgs.callPackage ./wrapper.nix {
+    inherit parentWrapperDir;
+  };
+
+  fileModeType =
+    let
+      # taken from the chmod(1) man page
+      symbolic = "[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+";
+      numeric = "[-+=]?[0-7]{0,4}";
+      mode = "((${symbolic})(,${symbolic})*)|(${numeric})";
+    in
+     lib.types.strMatching mode
+     // { description = "file mode string"; };
+
+  wrapperType = lib.types.submodule ({ name, config, ... }: {
+    options.source = lib.mkOption
+      { type = lib.types.path;
+        description = "The absolute path to the program to be wrapped.";
+      };
+    options.program = lib.mkOption
+      { type = with lib.types; nullOr str;
+        default = name;
+        description = ''
+          The name of the wrapper program. Defaults to the attribute name.
+        '';
+      };
+    options.owner = lib.mkOption
+      { type = lib.types.str;
+        description = "The owner of the wrapper program.";
+      };
+    options.group = lib.mkOption
+      { type = lib.types.str;
+        description = "The group of the wrapper program.";
+      };
+    options.permissions = lib.mkOption
+      { type = fileModeType;
+        default  = "u+rx,g+x,o+x";
+        example = "a+rx";
+        description = ''
+          The permissions of the wrapper program. The format is that of a
+          symbolic or numeric file mode understood by <command>chmod</command>.
+        '';
+      };
+    options.capabilities = lib.mkOption
+      { type = lib.types.commas;
+        default = "";
+        description = ''
+          A comma-separated list of capabilities to be given to the wrapper
+          program. For capabilities supported by the system check the
+          <citerefentry>
+            <refentrytitle>capabilities</refentrytitle>
+            <manvolnum>7</manvolnum>
+          </citerefentry>
+          manual page.
+
+          <note><para>
+            <literal>cap_setpcap</literal>, which is required for the wrapper
+            program to be able to raise caps into the Ambient set is NOT raised
+            to the Ambient set so that the real program cannot modify its own
+            capabilities!! This may be too restrictive for cases in which the
+            real program needs cap_setpcap but it at least leans on the side
+            security paranoid vs. too relaxed.
+          </para></note>
+        '';
+      };
+    options.setuid = lib.mkOption
+      { type = lib.types.bool;
+        default = false;
+        description = "Whether to add the setuid bit the wrapper program.";
+      };
+    options.setgid = lib.mkOption
+      { type = lib.types.bool;
+        default = false;
+        description = "Whether to add the setgid bit the wrapper program.";
+      };
+  });
+
+  ###### Activation script for the setcap wrappers
+  mkSetcapProgram =
+    { program
+    , capabilities
+    , source
+    , owner
+    , group
+    , permissions
+    , ...
+    }:
+    ''
+      cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}"
+      echo -n "${source}" > "$wrapperDir/${program}.real"
+
+      # Prevent races
+      chmod 0000 "$wrapperDir/${program}"
+      chown ${owner}.${group} "$wrapperDir/${program}"
+
+      # Set desired capabilities on the file plus cap_setpcap so
+      # the wrapper program can elevate the capabilities set on
+      # its file into the Ambient set.
+      ${pkgs.libcap.out}/bin/setcap "cap_setpcap,${capabilities}" "$wrapperDir/${program}"
+
+      # Set the executable bit
+      chmod ${permissions} "$wrapperDir/${program}"
+    '';
+
+  ###### Activation script for the setuid wrappers
+  mkSetuidProgram =
+    { program
+    , source
+    , owner
+    , group
+    , setuid
+    , setgid
+    , permissions
+    , ...
+    }:
+    ''
+      cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}"
+      echo -n "${source}" > "$wrapperDir/${program}.real"
+
+      # Prevent races
+      chmod 0000 "$wrapperDir/${program}"
+      chown ${owner}.${group} "$wrapperDir/${program}"
+
+      chmod "u${if setuid then "+" else "-"}s,g${if setgid then "+" else "-"}s,${permissions}" "$wrapperDir/${program}"
+    '';
+
+  mkWrappedPrograms =
+    builtins.map
+      (opts:
+        if opts.capabilities != ""
+        then mkSetcapProgram opts
+        else mkSetuidProgram opts
+      ) (lib.attrValues wrappers);
+in
+{
+  imports = [
+    (lib.mkRemovedOptionModule [ "security" "setuidOwners" ] "Use security.wrappers instead")
+    (lib.mkRemovedOptionModule [ "security" "setuidPrograms" ] "Use security.wrappers instead")
+  ];
+
+  ###### interface
+
+  options = {
+    security.wrappers = lib.mkOption {
+      type = lib.types.attrsOf wrapperType;
+      default = {};
+      example = lib.literalExpression
+        ''
+          {
+            # a setuid root program
+            doas =
+              { setuid = true;
+                owner = "root";
+                group = "root";
+                source = "''${pkgs.doas}/bin/doas";
+              };
+
+            # a setgid program
+            locate =
+              { setgid = true;
+                owner = "root";
+                group = "mlocate";
+                source = "''${pkgs.locate}/bin/locate";
+              };
+
+            # a program with the CAP_NET_RAW capability
+            ping =
+              { owner = "root";
+                group = "root";
+                capabilities = "cap_net_raw+ep";
+                source = "''${pkgs.iputils.out}/bin/ping";
+              };
+          }
+        '';
+      description = ''
+        This option effectively allows adding setuid/setgid bits, capabilities,
+        changing file ownership and permissions of a program without directly
+        modifying it. This works by creating a wrapper program under the
+        <option>security.wrapperDir</option> directory, which is then added to
+        the shell <literal>PATH</literal>.
+      '';
+    };
+
+    security.wrapperDir = lib.mkOption {
+      type        = lib.types.path;
+      default     = "/run/wrappers/bin";
+      internal    = true;
+      description = ''
+        This option defines the path to the wrapper programs. It
+        should not be overriden.
+      '';
+    };
+  };
+
+  ###### implementation
+  config = {
+
+    assertions = lib.mapAttrsToList
+      (name: opts:
+        { assertion = opts.setuid || opts.setgid -> opts.capabilities == "";
+          message = ''
+            The security.wrappers.${name} wrapper is not valid:
+                setuid/setgid and capabilities are mutually exclusive.
+          '';
+        }
+      ) wrappers;
+
+    security.wrappers =
+      let
+        mkSetuidRoot = source:
+          { setuid = true;
+            owner = "root";
+            group = "root";
+            inherit source;
+          };
+      in
+      { # These are mount related wrappers that require the +s permission.
+        fusermount  = mkSetuidRoot "${pkgs.fuse}/bin/fusermount";
+        fusermount3 = mkSetuidRoot "${pkgs.fuse3}/bin/fusermount3";
+        mount  = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/mount";
+        umount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/umount";
+      };
+
+    boot.specialFileSystems.${parentWrapperDir} = {
+      fsType = "tmpfs";
+      options = [ "nodev" "mode=755" ];
+    };
+
+    # Make sure our wrapperDir exports to the PATH env variable when
+    # initializing the shell
+    environment.extraInit = ''
+      # Wrappers override other bin directories.
+      export PATH="${wrapperDir}:$PATH"
+    '';
+
+    security.apparmor.includes."nixos/security.wrappers" = ''
+      include "${pkgs.apparmorRulesFromClosure { name="security.wrappers"; } [
+        securityWrapper
+      ]}"
+    '';
+
+    ###### wrappers activation script
+    system.activationScripts.wrappers =
+      lib.stringAfter [ "specialfs" "users" ]
+        ''
+          chmod 755 "${parentWrapperDir}"
+
+          # We want to place the tmpdirs for the wrappers to the parent dir.
+          wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
+          chmod a+rx "$wrapperDir"
+
+          ${lib.concatStringsSep "\n" mkWrappedPrograms}
+
+          if [ -L ${wrapperDir} ]; then
+            # Atomically replace the symlink
+            # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
+            old=$(readlink -f ${wrapperDir})
+            if [ -e "${wrapperDir}-tmp" ]; then
+              rm --force --recursive "${wrapperDir}-tmp"
+            fi
+            ln --symbolic --force --no-dereference "$wrapperDir" "${wrapperDir}-tmp"
+            mv --no-target-directory "${wrapperDir}-tmp" "${wrapperDir}"
+            rm --force --recursive "$old"
+          else
+            # For initial setup
+            ln --symbolic "$wrapperDir" "${wrapperDir}"
+          fi
+        '';
+
+    ###### wrappers consistency checks
+    system.extraDependencies = lib.singleton (pkgs.runCommandLocal
+      "ensure-all-wrappers-paths-exist" { }
+      ''
+        # make sure we produce output
+        mkdir -p $out
+
+        echo -n "Checking that Nix store paths of all wrapped programs exist... "
+
+        declare -A wrappers
+        ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v:
+          "wrappers['${n}']='${v.source}'") wrappers)}
+
+        for name in "''${!wrappers[@]}"; do
+          path="''${wrappers[$name]}"
+          if [[ "$path" =~ /nix/store ]] && [ ! -e "$path" ]; then
+            test -t 1 && echo -ne '\033[1;31m'
+            echo "FAIL"
+            echo "The path $path does not exist!"
+            echo 'Please, check the value of `security.wrappers."'$name'".source`.'
+            test -t 1 && echo -ne '\033[0m'
+            exit 1
+          fi
+        done
+
+        echo "OK"
+      '');
+  };
+}
diff --git a/nixos/modules/security/wrappers/wrapper.c b/nixos/modules/security/wrappers/wrapper.c
new file mode 100644
index 00000000000..529669facda
--- /dev/null
+++ b/nixos/modules/security/wrappers/wrapper.c
@@ -0,0 +1,233 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/xattr.h>
+#include <fcntl.h>
+#include <dirent.h>
+#include <assert.h>
+#include <errno.h>
+#include <linux/capability.h>
+#include <sys/prctl.h>
+#include <limits.h>
+#include <stdint.h>
+#include <syscall.h>
+#include <byteswap.h>
+
+// Make sure assertions are not compiled out, we use them to codify
+// invariants about this program and we want it to fail fast and
+// loudly if they are violated.
+#undef NDEBUG
+
+extern char **environ;
+
+// The WRAPPER_DIR macro is supplied at compile time so that it cannot
+// be changed at runtime
+static char *wrapper_dir = WRAPPER_DIR;
+
+// Wrapper debug variable name
+static char *wrapper_debug = "WRAPPER_DEBUG";
+
+#define CAP_SETPCAP 8
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+#define LE32_TO_H(x) bswap_32(x)
+#else
+#define LE32_TO_H(x) (x)
+#endif
+
+int get_last_cap(unsigned *last_cap) {
+    FILE* file = fopen("/proc/sys/kernel/cap_last_cap", "r");
+    if (file == NULL) {
+        int saved_errno = errno;
+        fprintf(stderr, "failed to open /proc/sys/kernel/cap_last_cap: %s\n", strerror(errno));
+        return -saved_errno;
+    }
+    int res = fscanf(file, "%u", last_cap);
+    if (res == EOF) {
+        int saved_errno = errno;
+        fprintf(stderr, "could not read number from /proc/sys/kernel/cap_last_cap: %s\n", strerror(errno));
+        return -saved_errno;
+    }
+    fclose(file);
+    return 0;
+}
+
+// Given the path to this program, fetch its configured capability set
+// (as set by `setcap ... /path/to/file`) and raise those capabilities
+// into the Ambient set.
+static int make_caps_ambient(const char *self_path) {
+    struct vfs_ns_cap_data data = {};
+    int r = getxattr(self_path, "security.capability", &data, sizeof(data));
+
+    if (r < 0) {
+        if (errno == ENODATA) {
+            // no capabilities set
+            return 0;
+        }
+        fprintf(stderr, "cannot get capabilities for %s: %s", self_path, strerror(errno));
+        return 1;
+    }
+
+    size_t size;
+    uint32_t version = LE32_TO_H(data.magic_etc) & VFS_CAP_REVISION_MASK;
+    switch (version) {
+        case VFS_CAP_REVISION_1:
+            size = VFS_CAP_U32_1;
+            break;
+        case VFS_CAP_REVISION_2:
+        case VFS_CAP_REVISION_3:
+            size = VFS_CAP_U32_3;
+            break;
+        default:
+            fprintf(stderr, "BUG! Unsupported capability version 0x%x on %s. Report to NixOS bugtracker\n", version, self_path);
+            return 1;
+    }
+
+    const struct __user_cap_header_struct header = {
+      .version = _LINUX_CAPABILITY_VERSION_3,
+      .pid = getpid(),
+    };
+    struct __user_cap_data_struct user_data[2] = {};
+
+    for (size_t i = 0; i < size; i++) {
+        // merge inheritable & permitted into one
+        user_data[i].permitted = user_data[i].inheritable =
+            LE32_TO_H(data.data[i].inheritable) | LE32_TO_H(data.data[i].permitted);
+    }
+
+    if (syscall(SYS_capset, &header, &user_data) < 0) {
+        fprintf(stderr, "failed to inherit capabilities: %s", strerror(errno));
+        return 1;
+    }
+    unsigned last_cap;
+    r = get_last_cap(&last_cap);
+    if (r < 0) {
+        return 1;
+    }
+    uint64_t set = user_data[0].permitted | (uint64_t)user_data[1].permitted << 32;
+    for (unsigned cap = 0; cap < last_cap; cap++) {
+        if (!(set & (1ULL << cap))) {
+            continue;
+        }
+
+        // Check for the cap_setpcap capability, we set this on the
+        // wrapper so it can elevate the capabilities to the Ambient
+        // set but we do not want to propagate it down into the
+        // wrapped program.
+        //
+        // TODO: what happens if that's the behavior you want
+        // though???? I'm preferring a strict vs. loose policy here.
+        if (cap == CAP_SETPCAP) {
+            if(getenv(wrapper_debug)) {
+                fprintf(stderr, "cap_setpcap in set, skipping it\n");
+            }
+            continue;
+        }
+        if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, (unsigned long) cap, 0, 0)) {
+            fprintf(stderr, "cannot raise the capability %d into the ambient set: %s\n", cap, strerror(errno));
+            return 1;
+        }
+        if (getenv(wrapper_debug)) {
+            fprintf(stderr, "raised %d into the ambient capability set\n", cap);
+        }
+    }
+
+    return 0;
+}
+
+int readlink_malloc(const char *p, char **ret) {
+    size_t l = FILENAME_MAX+1;
+    int r;
+
+    for (;;) {
+        char *c = calloc(l, sizeof(char));
+        if (!c) {
+            return -ENOMEM;
+        }
+
+        ssize_t n = readlink(p, c, l-1);
+        if (n < 0) {
+            r = -errno;
+            free(c);
+            return r;
+        }
+
+        if ((size_t) n < l-1) {
+            c[n] = 0;
+            *ret = c;
+            return 0;
+        }
+
+        free(c);
+        l *= 2;
+    }
+}
+
+int main(int argc, char **argv) {
+    char *self_path = NULL;
+    int self_path_size = readlink_malloc("/proc/self/exe", &self_path);
+    if (self_path_size < 0) {
+        fprintf(stderr, "cannot readlink /proc/self/exe: %s", strerror(-self_path_size));
+    }
+
+    // Make sure that we are being executed from the right location,
+    // i.e., `safe_wrapper_dir'.  This is to prevent someone from creating
+    // hard link `X' from some other location, along with a false
+    // `X.real' file, to allow arbitrary programs from being executed
+    // with elevated capabilities.
+    int len = strlen(wrapper_dir);
+    if (len > 0 && '/' == wrapper_dir[len - 1])
+      --len;
+    assert(!strncmp(self_path, wrapper_dir, len));
+    assert('/' == wrapper_dir[0]);
+    assert('/' == self_path[len]);
+
+    // Make *really* *really* sure that we were executed as
+    // `self_path', and not, say, as some other setuid program. That
+    // is, our effective uid/gid should match the uid/gid of
+    // `self_path'.
+    struct stat st;
+    assert(lstat(self_path, &st) != -1);
+
+    assert(!(st.st_mode & S_ISUID) || (st.st_uid == geteuid()));
+    assert(!(st.st_mode & S_ISGID) || (st.st_gid == getegid()));
+
+    // And, of course, we shouldn't be writable.
+    assert(!(st.st_mode & (S_IWGRP | S_IWOTH)));
+
+    // Read the path of the real (wrapped) program from <self>.real.
+    char real_fn[PATH_MAX + 10];
+    int real_fn_size = snprintf(real_fn, sizeof(real_fn), "%s.real", self_path);
+    assert(real_fn_size < sizeof(real_fn));
+
+    int fd_self = open(real_fn, O_RDONLY);
+    assert(fd_self != -1);
+
+    char source_prog[PATH_MAX];
+    len = read(fd_self, source_prog, PATH_MAX);
+    assert(len != -1);
+    assert(len < sizeof(source_prog));
+    assert(len > 0);
+    source_prog[len] = 0;
+
+    close(fd_self);
+
+    // Read the capabilities set on the wrapper and raise them in to
+    // the ambient set so the program we're wrapping receives the
+    // capabilities too!
+    if (make_caps_ambient(self_path) != 0) {
+        free(self_path);
+        return 1;
+    }
+    free(self_path);
+
+    execve(source_prog, argv, environ);
+    
+    fprintf(stderr, "%s: cannot run `%s': %s\n",
+        argv[0], source_prog, strerror(errno));
+
+    return 1;
+}
diff --git a/nixos/modules/security/wrappers/wrapper.nix b/nixos/modules/security/wrappers/wrapper.nix
new file mode 100644
index 00000000000..e3620fb222d
--- /dev/null
+++ b/nixos/modules/security/wrappers/wrapper.nix
@@ -0,0 +1,21 @@
+{ stdenv, linuxHeaders, parentWrapperDir, debug ? false }:
+# For testing:
+# $ nix-build -E 'with import <nixpkgs> {}; pkgs.callPackage ./wrapper.nix { parentWrapperDir = "/run/wrappers"; debug = true; }'
+stdenv.mkDerivation {
+  name = "security-wrapper";
+  buildInputs = [ linuxHeaders ];
+  dontUnpack = true;
+  hardeningEnable = [ "pie" ];
+  CFLAGS = [
+    ''-DWRAPPER_DIR="${parentWrapperDir}"''
+  ] ++ (if debug then [
+    "-Werror" "-Og" "-g"
+  ] else [
+    "-Wall" "-O2"
+  ]);
+  dontStrip = debug;
+  installPhase = ''
+    mkdir -p $out/bin
+    $CC $CFLAGS ${./wrapper.c} -o $out/bin/security-wrapper
+  '';
+}
diff --git a/nixos/modules/services/admin/meshcentral.nix b/nixos/modules/services/admin/meshcentral.nix
new file mode 100644
index 00000000000..92762d2037c
--- /dev/null
+++ b/nixos/modules/services/admin/meshcentral.nix
@@ -0,0 +1,53 @@
+{ config, pkgs, lib, ... }:
+let
+  cfg = config.services.meshcentral;
+  configFormat = pkgs.formats.json {};
+  configFile = configFormat.generate "meshcentral-config.json" cfg.settings;
+in with lib; {
+  options.services.meshcentral = with types; {
+    enable = mkEnableOption "MeshCentral computer management server";
+    package = mkOption {
+      description = "MeshCentral package to use. Replacing this may be necessary to add dependencies for extra functionality.";
+      type = types.package;
+      default = pkgs.meshcentral;
+      defaultText = literalExpression "pkgs.meshcentral";
+    };
+    settings = mkOption {
+      description = ''
+        Settings for MeshCentral. Refer to upstream documentation for details:
+
+        <itemizedlist>
+          <listitem><para><link xlink:href="https://github.com/Ylianst/MeshCentral/blob/master/meshcentral-config-schema.json">JSON Schema definition</link></para></listitem>
+          <listitem><para><link xlink:href="https://github.com/Ylianst/MeshCentral/blob/master/sample-config.json">simple sample configuration</link></para></listitem>
+          <listitem><para><link xlink:href="https://github.com/Ylianst/MeshCentral/blob/master/sample-config-advanced.json">complex sample configuration</link></para></listitem>
+          <listitem><para><link xlink:href="https://www.meshcommander.com/meshcentral2">Old homepage) with documentation link</link></para></listitem>
+        </itemizedlist>
+      '';
+      type = types.submodule {
+        freeformType = configFormat.type;
+      };
+      example = {
+        settings = {
+          WANonly = true;
+          Cert = "meshcentral.example.com";
+          TlsOffload = "10.0.0.2,fd42::2";
+          Port = 4430;
+        };
+        domains."".certUrl = "https://meshcentral.example.com/";
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    services.meshcentral.settings.settings.autoBackup.backupPath = lib.mkDefault "/var/lib/meshcentral/backups";
+    systemd.services.meshcentral = {
+      wantedBy = ["multi-user.target"];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/meshcentral --datapath /var/lib/meshcentral --configfile ${configFile}";
+        DynamicUser = true;
+        StateDirectory = "meshcentral";
+        CacheDirectory = "meshcentral";
+      };
+    };
+  };
+  meta.maintainers = [ maintainers.lheckemann ];
+}
diff --git a/nixos/modules/services/admin/oxidized.nix b/nixos/modules/services/admin/oxidized.nix
new file mode 100644
index 00000000000..49ea3ced76a
--- /dev/null
+++ b/nixos/modules/services/admin/oxidized.nix
@@ -0,0 +1,118 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.oxidized;
+in
+{
+  options.services.oxidized = {
+    enable = mkEnableOption "the oxidized configuration backup service";
+
+    user = mkOption {
+      type = types.str;
+      default = "oxidized";
+      description = ''
+        User under which the oxidized service runs.
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "oxidized";
+      description = ''
+        Group under which the oxidized service runs.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/oxidized";
+      description = "State directory for the oxidized service.";
+    };
+
+    configFile = mkOption {
+      type = types.path;
+      example = literalExpression ''
+        pkgs.writeText "oxidized-config.yml" '''
+          ---
+          debug: true
+          use_syslog: true
+          input:
+            default: ssh
+            ssh:
+              secure: true
+          interval: 3600
+          model_map:
+            dell: powerconnect
+            hp: procurve
+          source:
+            default: csv
+            csv:
+              delimiter: !ruby/regexp /:/
+              file: "/var/lib/oxidized/.config/oxidized/router.db"
+              map:
+                name: 0
+                model: 1
+                username: 2
+                password: 3
+          pid: "/var/lib/oxidized/.config/oxidized/pid"
+          rest: 127.0.0.1:8888
+          retries: 3
+          # ... additional config
+        ''';
+      '';
+      description = ''
+        Path to the oxidized configuration file.
+      '';
+    };
+
+    routerDB = mkOption {
+      type = types.path;
+      example = literalExpression ''
+        pkgs.writeText "oxidized-router.db" '''
+          hostname-sw1:powerconnect:username1:password2
+          hostname-sw2:procurve:username2:password2
+          # ... additional hosts
+        '''
+      '';
+      description = ''
+        Path to the file/database which contains the targets for oxidized.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups.${cfg.group} = { };
+    users.users.${cfg.user} = {
+      description = "Oxidized service user";
+      group = cfg.group;
+      home = cfg.dataDir;
+      createHome = true;
+      isSystemUser = true;
+    };
+
+    systemd.services.oxidized = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      preStart = ''
+        mkdir -p ${cfg.dataDir}/.config/oxidized
+        ln -f -s ${cfg.routerDB} ${cfg.dataDir}/.config/oxidized/router.db
+        ln -f -s ${cfg.configFile} ${cfg.dataDir}/.config/oxidized/config
+      '';
+
+      serviceConfig = {
+        ExecStart = "${pkgs.oxidized}/bin/oxidized";
+        User = cfg.user;
+        Group = cfg.group;
+        UMask = "0077";
+        NoNewPrivileges = true;
+        Restart  = "always";
+        WorkingDirectory = cfg.dataDir;
+        KillSignal = "SIGKILL";
+        PIDFile = "${cfg.dataDir}/.config/oxidized/pid";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/admin/pgadmin.nix b/nixos/modules/services/admin/pgadmin.nix
new file mode 100644
index 00000000000..80b68145410
--- /dev/null
+++ b/nixos/modules/services/admin/pgadmin.nix
@@ -0,0 +1,127 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  pkg = pkgs.pgadmin4;
+  cfg = config.services.pgadmin;
+
+  _base = with types; [ int bool str ];
+  base = with types; oneOf ([ (listOf (oneOf _base)) (attrsOf (oneOf _base)) ] ++ _base);
+
+  formatAttrset = attr:
+    "{${concatStringsSep "\n" (mapAttrsToList (key: value: "${builtins.toJSON key}: ${formatPyValue value},") attr)}}";
+
+  formatPyValue = value:
+    if builtins.isString value then builtins.toJSON value
+    else if value ? _expr then value._expr
+    else if builtins.isInt value then toString value
+    else if builtins.isBool value then (if value then "True" else "False")
+    else if builtins.isAttrs value then (formatAttrset value)
+    else if builtins.isList value then "[${concatStringsSep "\n" (map (v: "${formatPyValue v},") value)}]"
+    else throw "Unrecognized type";
+
+  formatPy = attrs:
+    concatStringsSep "\n" (mapAttrsToList (key: value: "${key} = ${formatPyValue value}") attrs);
+
+  pyType = with types; attrsOf (oneOf [ (attrsOf base) (listOf base) base ]);
+in
+{
+  options.services.pgadmin = {
+    enable = mkEnableOption "PostgreSQL Admin 4";
+
+    port = mkOption {
+      description = "Port for pgadmin4 to run on";
+      type = types.port;
+      default = 5050;
+    };
+
+    initialEmail = mkOption {
+      description = "Initial email for the pgAdmin account.";
+      type = types.str;
+    };
+
+    initialPasswordFile = mkOption {
+      description = ''
+        Initial password file for the pgAdmin account.
+        NOTE: Should be string not a store path, to prevent the password from being world readable.
+      '';
+      type = types.path;
+    };
+
+    openFirewall = mkEnableOption "firewall passthrough for pgadmin4";
+
+    settings = mkOption {
+      description = ''
+        Settings for pgadmin4.
+        <link xlink:href="https://www.pgadmin.org/docs/pgadmin4/development/config_py.html">Documentation</link>.
+      '';
+      type = pyType;
+      default= {};
+    };
+  };
+
+  config = mkIf (cfg.enable) {
+    networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall) [ cfg.port ];
+
+    services.pgadmin.settings = {
+      DEFAULT_SERVER_PORT = cfg.port;
+      SERVER_MODE = true;
+    } // (optionalAttrs cfg.openFirewall {
+      DEFAULT_SERVER = mkDefault "::";
+    });
+
+    systemd.services.pgadmin = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      requires = [ "network.target" ];
+      # we're adding this optionally so just in case there's any race it'll be caught
+      # in case postgres doesn't start, pgadmin will just start normally
+      wants = [ "postgresql.service" ];
+
+      path = [ config.services.postgresql.package pkgs.coreutils pkgs.bash ];
+
+      preStart = ''
+        # NOTE: this is idempotent (aka running it twice has no effect)
+        (
+          # Email address:
+          echo ${escapeShellArg cfg.initialEmail}
+
+          # file might not contain newline. echo hack fixes that.
+          PW=$(cat ${escapeShellArg cfg.initialPasswordFile})
+
+          # Password:
+          echo "$PW"
+          # Retype password:
+          echo "$PW"
+        ) | ${pkg}/bin/pgadmin4-setup
+      '';
+
+      restartTriggers = [
+        "/etc/pgadmin/config_system.py"
+      ];
+
+      serviceConfig = {
+        User = "pgadmin";
+        DynamicUser = true;
+        LogsDirectory = "pgadmin";
+        StateDirectory = "pgadmin";
+        ExecStart = "${pkg}/bin/pgadmin4";
+      };
+    };
+
+    users.users.pgadmin = {
+      isSystemUser = true;
+      group = "pgadmin";
+    };
+
+    users.groups.pgadmin = {};
+
+    environment.etc."pgadmin/config_system.py" = {
+      text = formatPy cfg.settings;
+      mode = "0600";
+      user = "pgadmin";
+      group = "pgadmin";
+    };
+  };
+}
diff --git a/nixos/modules/services/admin/salt/master.nix b/nixos/modules/services/admin/salt/master.nix
new file mode 100644
index 00000000000..a3069c81c19
--- /dev/null
+++ b/nixos/modules/services/admin/salt/master.nix
@@ -0,0 +1,63 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg  = config.services.salt.master;
+
+  fullConfig = lib.recursiveUpdate {
+    # Provide defaults for some directories to allow an immutable config dir
+
+    # Default is equivalent to /etc/salt/master.d/*.conf
+    default_include = "/var/lib/salt/master.d/*.conf";
+    # Default is in /etc/salt/pki/master
+    pki_dir = "/var/lib/salt/pki/master";
+  } cfg.configuration;
+
+in
+
+{
+  options = {
+    services.salt.master = {
+      enable = mkEnableOption "Salt master service";
+      configuration = mkOption {
+        type = types.attrs;
+        default = {};
+        description = "Salt master configuration as Nix attribute set.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment = {
+      # Set this up in /etc/salt/master so `salt`, `salt-key`, etc. work.
+      # The alternatives are
+      # - passing --config-dir to all salt commands, not just the master unit,
+      # - setting a global environment variable,
+      etc."salt/master".source = pkgs.writeText "master" (
+        builtins.toJSON fullConfig
+      );
+      systemPackages = with pkgs; [ salt ];
+    };
+    systemd.services.salt-master = {
+      description = "Salt Master";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = with pkgs; [
+        util-linux  # for dmesg
+      ];
+      serviceConfig = {
+        ExecStart = "${pkgs.salt}/bin/salt-master";
+        LimitNOFILE = 16384;
+        Type = "notify";
+        NotifyAccess = "all";
+      };
+      restartTriggers = [
+        config.environment.etc."salt/master".source
+      ];
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ Flakebi ];
+}
diff --git a/nixos/modules/services/admin/salt/minion.nix b/nixos/modules/services/admin/salt/minion.nix
new file mode 100644
index 00000000000..ac124c570d8
--- /dev/null
+++ b/nixos/modules/services/admin/salt/minion.nix
@@ -0,0 +1,67 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg  = config.services.salt.minion;
+
+  fullConfig = lib.recursiveUpdate {
+    # Provide defaults for some directories to allow an immutable config dir
+    # NOTE: the config dir being immutable prevents `minion_id` caching
+
+    # Default is equivalent to /etc/salt/minion.d/*.conf
+    default_include = "/var/lib/salt/minion.d/*.conf";
+    # Default is in /etc/salt/pki/minion
+    pki_dir = "/var/lib/salt/pki/minion";
+  } cfg.configuration;
+
+in
+
+{
+  options = {
+    services.salt.minion = {
+      enable = mkEnableOption "Salt minion service";
+      configuration = mkOption {
+        type = types.attrs;
+        default = {};
+        description = ''
+          Salt minion configuration as Nix attribute set.
+          See <link xlink:href="https://docs.saltstack.com/en/latest/ref/configuration/minion.html"/>
+          for details.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment = {
+      # Set this up in /etc/salt/minion so `salt-call`, etc. work.
+      # The alternatives are
+      # - passing --config-dir to all salt commands, not just the minion unit,
+      # - setting aglobal environment variable.
+      etc."salt/minion".source = pkgs.writeText "minion" (
+        builtins.toJSON fullConfig
+      );
+      systemPackages = with pkgs; [ salt ];
+    };
+    systemd.services.salt-minion = {
+      description = "Salt Minion";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = with pkgs; [
+        util-linux
+      ];
+      serviceConfig = {
+        ExecStart = "${pkgs.salt}/bin/salt-minion";
+        LimitNOFILE = 8192;
+        Type = "notify";
+        NotifyAccess = "all";
+      };
+      restartTriggers = [
+        config.environment.etc."salt/minion".source
+      ];
+    };
+  };
+}
+
diff --git a/nixos/modules/services/amqp/activemq/ActiveMQBroker.java b/nixos/modules/services/amqp/activemq/ActiveMQBroker.java
new file mode 100644
index 00000000000..c0f5d16ea11
--- /dev/null
+++ b/nixos/modules/services/amqp/activemq/ActiveMQBroker.java
@@ -0,0 +1,19 @@
+import org.apache.activemq.broker.BrokerService;
+import org.apache.activemq.broker.BrokerFactory;
+import java.net.URI;
+
+public class ActiveMQBroker {
+
+  public static void main(String[] args) throws Throwable {
+    URI uri = new URI((args.length > 0) ? args[0] : "xbean:activemq.xml");
+    BrokerService broker = BrokerFactory.createBroker(uri);
+    broker.start();
+    if (broker.waitUntilStarted()) {
+      broker.waitUntilStopped();
+    } else {
+      System.out.println("Failed starting broker");
+      System.exit(-1);
+    };
+  }
+
+}
diff --git a/nixos/modules/services/amqp/activemq/default.nix b/nixos/modules/services/amqp/activemq/default.nix
new file mode 100644
index 00000000000..47669b05aa9
--- /dev/null
+++ b/nixos/modules/services/amqp/activemq/default.nix
@@ -0,0 +1,135 @@
+{ config, lib, pkgs, ... }:
+
+with pkgs;
+with lib;
+
+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
+    '';
+  };
+
+in {
+
+  options = {
+    services.activemq = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the Apache ActiveMQ message broker service.
+        '';
+      };
+      configurationDir = mkOption {
+        default = "${activemq}/conf";
+        defaultText = literalExpression ''"''${pkgs.activemq}/conf"'';
+        type = types.str;
+        description = ''
+          The base directory for ActiveMQ's configuration.
+          By default, this directory is searched for a file named activemq.xml,
+          which should contain the configuration for the broker service.
+        '';
+      };
+      configurationURI = mkOption {
+        type = types.str;
+        default = "xbean:activemq.xml";
+        description = ''
+          The URI that is passed along to the BrokerFactory to
+          set up the configuration of the ActiveMQ broker service.
+          You should not need to change this. For custom configuration,
+          set the <literal>configurationDir</literal> instead, and create
+          an activemq.xml configuration file in it.
+        '';
+      };
+      baseDir = mkOption {
+        type = types.str;
+        default = "/var/activemq";
+        description = ''
+          The base directory where ActiveMQ stores its persistent data and logs.
+          This will be overridden if you set "activemq.base" and "activemq.data"
+          in the <literal>javaProperties</literal> option. You can also override
+          this in activemq.xml.
+        '';
+      };
+      javaProperties = mkOption {
+        type = types.attrs;
+        default = { };
+        example = literalExpression ''
+          {
+            "java.net.preferIPv4Stack" = "true";
+          }
+        '';
+        apply = attrs: {
+          "activemq.base" = "${cfg.baseDir}";
+          "activemq.data" = "${cfg.baseDir}/data";
+          "activemq.conf" = "${cfg.configurationDir}";
+          "activemq.home" = "${activemq}";
+        } // attrs;
+        description = ''
+          Specifies Java properties that are sent to the ActiveMQ
+          broker service with the "-D" option. You can set properties
+          here to change the behaviour and configuration of the broker.
+          All essential properties that are not set here are automatically
+          given reasonable defaults.
+        '';
+      };
+      extraJavaOptions = mkOption {
+        type = types.separatedString " ";
+        default = "";
+        example = "-Xmx2G -Xms2G -XX:MaxPermSize=512M";
+        description = ''
+          Add extra options here that you want to be sent to the
+          Java runtime when the broker service is started.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.activemq = {
+      description = "ActiveMQ server user";
+      group = "activemq";
+      uid = config.ids.uids.activemq;
+    };
+
+    users.groups.activemq.gid = config.ids.gids.activemq;
+
+    systemd.services.activemq_init = {
+      wantedBy = [ "activemq.service" ];
+      partOf = [ "activemq.service" ];
+      before = [ "activemq.service" ];
+      serviceConfig.Type = "oneshot";
+      script = ''
+        mkdir -p "${cfg.javaProperties."activemq.data"}"
+        chown -R activemq "${cfg.javaProperties."activemq.data"}"
+      '';
+    };
+
+    systemd.services.activemq = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = [ jre ];
+      serviceConfig.User = "activemq";
+      script = ''
+        source ${activemq}/lib/classpath.env
+        export CLASSPATH=${activemqBroker}/lib:${cfg.configurationDir}:$CLASSPATH
+        exec java \
+          ${concatStringsSep " \\\n" (mapAttrsToList (name: value: "-D${name}=${value}") cfg.javaProperties)} \
+          ${cfg.extraJavaOptions} ActiveMQBroker "${cfg.configurationURI}"
+      '';
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/amqp/rabbitmq.nix b/nixos/modules/services/amqp/rabbitmq.nix
new file mode 100644
index 00000000000..3255942fe43
--- /dev/null
+++ b/nixos/modules/services/amqp/rabbitmq.nix
@@ -0,0 +1,228 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.rabbitmq;
+
+  inherit (builtins) concatStringsSep;
+
+  config_file_content = lib.generators.toKeyValue { } cfg.configItems;
+  config_file = pkgs.writeText "rabbitmq.conf" config_file_content;
+
+  advanced_config_file = pkgs.writeText "advanced.config" cfg.config;
+
+in
+{
+  ###### interface
+  options = {
+    services.rabbitmq = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the RabbitMQ server, an Advanced Message
+          Queuing Protocol (AMQP) broker.
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.rabbitmq-server;
+        type = types.package;
+        defaultText = literalExpression "pkgs.rabbitmq-server";
+        description = ''
+          Which rabbitmq package to use.
+        '';
+      };
+
+      listenAddress = mkOption {
+        default = "127.0.0.1";
+        example = "";
+        description = ''
+          IP address on which RabbitMQ will listen for AMQP
+          connections.  Set to the empty string to listen on all
+          interfaces.  Note that RabbitMQ creates a user named
+          <literal>guest</literal> with password
+          <literal>guest</literal> by default, so you should delete
+          this user if you intend to allow external access.
+
+          Together with 'port' setting it's mostly an alias for
+          configItems."listeners.tcp.1" and it's left for backwards
+          compatibility with previous version of this module.
+        '';
+        type = types.str;
+      };
+
+      port = mkOption {
+        default = 5672;
+        description = ''
+          Port on which RabbitMQ will listen for AMQP connections.
+        '';
+        type = types.port;
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/rabbitmq";
+        description = ''
+          Data directory for rabbitmq.
+        '';
+      };
+
+      cookie = mkOption {
+        default = "";
+        type = types.str;
+        description = ''
+          Erlang cookie is a string of arbitrary length which must
+          be the same for several nodes to be allowed to communicate.
+          Leave empty to generate automatically.
+        '';
+      };
+
+      configItems = mkOption {
+        default = { };
+        type = types.attrsOf types.str;
+        example = literalExpression ''
+          {
+            "auth_backends.1.authn" = "rabbit_auth_backend_ldap";
+            "auth_backends.1.authz" = "rabbit_auth_backend_internal";
+          }
+        '';
+        description = ''
+          Configuration options in RabbitMQ's new config file format,
+          which is a simple key-value format that can not express nested
+          data structures. This is known as the <literal>rabbitmq.conf</literal> file,
+          although outside NixOS that filename may have Erlang syntax, particularly
+          prior to RabbitMQ 3.7.0.
+
+          If you do need to express nested data structures, you can use
+          <literal>config</literal> option. Configuration from <literal>config</literal>
+          will be merged into these options by RabbitMQ at runtime to
+          form the final configuration.
+
+          See https://www.rabbitmq.com/configure.html#config-items
+          For the distinct formats, see https://www.rabbitmq.com/configure.html#config-file-formats
+        '';
+      };
+
+      config = mkOption {
+        default = "";
+        type = types.str;
+        description = ''
+          Verbatim advanced configuration file contents using the Erlang syntax.
+          This is also known as the <literal>advanced.config</literal> file or the old config format.
+
+          <literal>configItems</literal> is preferred whenever possible. However, nested
+          data structures can only be expressed properly using the <literal>config</literal> option.
+
+          The contents of this option will be merged into the <literal>configItems</literal>
+          by RabbitMQ at runtime to form the final configuration.
+
+          See the second table on https://www.rabbitmq.com/configure.html#config-items
+          For the distinct formats, see https://www.rabbitmq.com/configure.html#config-file-formats
+        '';
+      };
+
+      plugins = mkOption {
+        default = [ ];
+        type = types.listOf types.str;
+        description = "The names of plugins to enable";
+      };
+
+      pluginDirs = mkOption {
+        default = [ ];
+        type = types.listOf types.path;
+        description = "The list of directories containing external plugins";
+      };
+
+      managementPlugin = {
+        enable = mkEnableOption "the management plugin";
+        port = mkOption {
+          default = 15672;
+          type = types.port;
+          description = ''
+            On which port to run the management plugin
+          '';
+        };
+      };
+    };
+  };
+
+
+  ###### implementation
+  config = mkIf cfg.enable {
+
+    # This is needed so we will have 'rabbitmqctl' in our PATH
+    environment.systemPackages = [ cfg.package ];
+
+    services.epmd.enable = true;
+
+    users.users.rabbitmq = {
+      description = "RabbitMQ server user";
+      home = "${cfg.dataDir}";
+      createHome = true;
+      group = "rabbitmq";
+      uid = config.ids.uids.rabbitmq;
+    };
+
+    users.groups.rabbitmq.gid = config.ids.gids.rabbitmq;
+
+    services.rabbitmq.configItems = {
+      "listeners.tcp.1" = mkDefault "${cfg.listenAddress}:${toString cfg.port}";
+    } // optionalAttrs cfg.managementPlugin.enable {
+      "management.tcp.port" = toString cfg.managementPlugin.port;
+      "management.tcp.ip" = cfg.listenAddress;
+    };
+
+    services.rabbitmq.plugins = optional cfg.managementPlugin.enable "rabbitmq_management";
+
+    systemd.services.rabbitmq = {
+      description = "RabbitMQ Server";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "epmd.socket" ];
+      wants = [ "network.target" "epmd.socket" ];
+
+      path = [
+        cfg.package
+        pkgs.coreutils # mkdir/chown/chmod for preStart
+      ];
+
+      environment = {
+        RABBITMQ_MNESIA_BASE = "${cfg.dataDir}/mnesia";
+        RABBITMQ_LOGS = "-";
+        SYS_PREFIX = "";
+        RABBITMQ_CONFIG_FILE = config_file;
+        RABBITMQ_PLUGINS_DIR = concatStringsSep ":" cfg.pluginDirs;
+        RABBITMQ_ENABLED_PLUGINS_FILE = pkgs.writeText "enabled_plugins" ''
+          [ ${concatStringsSep "," cfg.plugins} ].
+        '';
+      } // optionalAttrs (cfg.config != "") { RABBITMQ_ADVANCED_CONFIG_FILE = advanced_config_file; };
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/sbin/rabbitmq-server";
+        ExecStop = "${cfg.package}/sbin/rabbitmqctl shutdown";
+        User = "rabbitmq";
+        Group = "rabbitmq";
+        LogsDirectory = "rabbitmq";
+        WorkingDirectory = cfg.dataDir;
+        Type = "notify";
+        NotifyAccess = "all";
+        UMask = "0027";
+        LimitNOFILE = "100000";
+        Restart = "on-failure";
+        RestartSec = "10";
+        TimeoutStartSec = "3600";
+      };
+
+      preStart = ''
+        ${optionalString (cfg.cookie != "") ''
+            echo -n ${cfg.cookie} > ${cfg.dataDir}/.erlang.cookie
+            chmod 600 ${cfg.dataDir}/.erlang.cookie
+        ''}
+      '';
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/audio/alsa.nix b/nixos/modules/services/audio/alsa.nix
new file mode 100644
index 00000000000..0d743ed31da
--- /dev/null
+++ b/nixos/modules/services/audio/alsa.nix
@@ -0,0 +1,133 @@
+# ALSA sound support.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inherit (pkgs) alsa-utils;
+
+  pulseaudioEnabled = config.hardware.pulseaudio.enable;
+
+in
+
+{
+  imports = [
+    (mkRenamedOptionModule [ "sound" "enableMediaKeys" ] [ "sound" "mediaKeys" "enable" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    sound = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable ALSA sound.
+        '';
+      };
+
+      enableOSSEmulation = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable ALSA OSS emulation (with certain cards sound mixing may not work!).
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          defaults.pcm.!card 3
+        '';
+        description = ''
+          Set addition configuration for system-wide alsa.
+        '';
+      };
+
+      mediaKeys = {
+
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether to enable volume and capture control with keyboard media keys.
+
+            You want to leave this disabled if you run a desktop environment
+            like KDE, Gnome, Xfce, etc, as those handle such things themselves.
+            You might want to enable this if you run a minimalistic desktop
+            environment or work from bare linux ttys/framebuffers.
+
+            Enabling this will turn on <option>services.actkbd</option>.
+          '';
+        };
+
+        volumeStep = mkOption {
+          type = types.str;
+          default = "1";
+          example = "1%";
+          description = ''
+            The value by which to increment/decrement volume on media keys.
+
+            See amixer(1) for allowed values.
+          '';
+        };
+
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.sound.enable {
+
+    environment.systemPackages = [ alsa-utils ];
+
+    environment.etc = mkIf (!pulseaudioEnabled && config.sound.extraConfig != "")
+      { "asound.conf".text = config.sound.extraConfig; };
+
+    # ALSA provides a udev rule for restoring volume settings.
+    services.udev.packages = [ alsa-utils ];
+
+    boot.kernelModules = optional config.sound.enableOSSEmulation "snd_pcm_oss";
+
+    systemd.services.alsa-store =
+      { description = "Store Sound Card State";
+        wantedBy = [ "multi-user.target" ];
+        unitConfig.RequiresMountsFor = "/var/lib/alsa";
+        unitConfig.ConditionVirtualization = "!systemd-nspawn";
+        serviceConfig = {
+          Type = "oneshot";
+          RemainAfterExit = true;
+          ExecStart = "${pkgs.coreutils}/bin/mkdir -p /var/lib/alsa";
+          ExecStop = "${alsa-utils}/sbin/alsactl store --ignore";
+        };
+      };
+
+    services.actkbd = mkIf config.sound.mediaKeys.enable {
+      enable = true;
+      bindings = [
+        # "Mute" media key
+        { keys = [ 113 ]; events = [ "key" ];       command = "${alsa-utils}/bin/amixer -q set Master toggle"; }
+
+        # "Lower Volume" media key
+        { keys = [ 114 ]; events = [ "key" "rep" ]; command = "${alsa-utils}/bin/amixer -q set Master ${config.sound.mediaKeys.volumeStep}- unmute"; }
+
+        # "Raise Volume" media key
+        { keys = [ 115 ]; events = [ "key" "rep" ]; command = "${alsa-utils}/bin/amixer -q set Master ${config.sound.mediaKeys.volumeStep}+ unmute"; }
+
+        # "Mic Mute" media key
+        { keys = [ 190 ]; events = [ "key" ];       command = "${alsa-utils}/bin/amixer -q set Capture toggle"; }
+      ];
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/audio/botamusique.nix b/nixos/modules/services/audio/botamusique.nix
new file mode 100644
index 00000000000..f4fa0ead4f0
--- /dev/null
+++ b/nixos/modules/services/audio/botamusique.nix
@@ -0,0 +1,115 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.botamusique;
+
+  format = pkgs.formats.ini {};
+  configFile = format.generate "botamusique.ini" cfg.settings;
+in
+{
+  meta.maintainers = with lib.maintainers; [ hexa ];
+
+  options.services.botamusique = {
+    enable = mkEnableOption "botamusique, a bot to play audio streams on mumble";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.botamusique;
+      defaultText = literalExpression "pkgs.botamusique";
+      description = "The botamusique package to use.";
+    };
+
+    settings = mkOption {
+      type = with types; submodule {
+        freeformType = format.type;
+        options = {
+          server.host = mkOption {
+            type = types.str;
+            default = "localhost";
+            example = "mumble.example.com";
+            description = "Hostname of the mumble server to connect to.";
+          };
+
+          server.port = mkOption {
+            type = types.port;
+            default = 64738;
+            description = "Port of the mumble server to connect to.";
+          };
+
+          bot.username = mkOption {
+            type = types.str;
+            default = "botamusique";
+            description = "Name the bot should appear with.";
+          };
+
+          bot.comment = mkOption {
+            type = types.str;
+            default = "Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun!";
+            description = "Comment displayed for the bot.";
+          };
+        };
+      };
+      default = {};
+      description = ''
+        Your <filename>configuration.ini</filename> as a Nix attribute set. Look up
+        possible options in the <link xlink:href="https://github.com/azlux/botamusique/blob/master/configuration.example.ini">configuration.example.ini</link>.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.botamusique = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      unitConfig.Documentation = "https://github.com/azlux/botamusique/wiki";
+
+      environment.HOME = "/var/lib/botamusique";
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/botamusique --config ${configFile}";
+        Restart = "always"; # the bot exits when the server connection is lost
+
+        # Hardening
+        CapabilityBoundingSet = [ "" ];
+        DynamicUser = true;
+        IPAddressDeny = [
+          "link-local"
+          "multicast"
+        ];
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        ProcSubset = "pid";
+        PrivateDevices = true;
+        PrivateUsers = true;
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+        ];
+        StateDirectory = "botamusique";
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+          "~@resources"
+        ];
+        UMask = "0077";
+        WorkingDirectory = "/var/lib/botamusique";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/audio/hqplayerd.nix b/nixos/modules/services/audio/hqplayerd.nix
new file mode 100644
index 00000000000..416d12ce217
--- /dev/null
+++ b/nixos/modules/services/audio/hqplayerd.nix
@@ -0,0 +1,142 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.hqplayerd;
+  pkg = pkgs.hqplayerd;
+  # XXX: This is hard-coded in the distributed binary, don't try to change it.
+  stateDir = "/var/lib/hqplayer";
+  configDir = "/etc/hqplayer";
+in
+{
+  options = {
+    services.hqplayerd = {
+      enable = mkEnableOption "HQPlayer Embedded";
+
+      auth = {
+        username = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            Username used for HQPlayer's WebUI.
+
+            Without this you will need to manually create the credentials after
+            first start by going to http://your.ip/8088/auth
+          '';
+        };
+
+        password = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            Password used for HQPlayer's WebUI.
+
+            Without this you will need to manually create the credentials after
+            first start by going to http://your.ip/8088/auth
+          '';
+        };
+      };
+
+      licenseFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          Path to the HQPlayer license key file.
+
+          Without this, the service will run in trial mode and restart every 30
+          minutes.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Opens ports needed for the WebUI and controller API.
+        '';
+      };
+
+      config = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = ''
+          HQplayer daemon configuration, written to /etc/hqplayer/hqplayerd.xml.
+
+          Refer to share/doc/hqplayerd/readme.txt in the hqplayerd derivation for possible values.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = (cfg.auth.username != null -> cfg.auth.password != null)
+                 && (cfg.auth.password != null -> cfg.auth.username != null);
+        message = "You must set either both services.hqplayer.auth.username and password, or neither.";
+      }
+    ];
+
+    environment = {
+      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 ];
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 8088 4321 ];
+    };
+
+    services.udev.packages = [ pkg ];
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${configDir}      0755 hqplayer hqplayer - -"
+        "d ${stateDir}       0755 hqplayer hqplayer - -"
+        "d ${stateDir}/home  0755 hqplayer hqplayer - -"
+      ];
+
+      packages = [ pkg ];
+
+      services.hqplayerd = {
+        wantedBy = [ "multi-user.target" ];
+        after = [ "systemd-tmpfiles-setup.service" ];
+
+        environment.HOME = "${stateDir}/home";
+
+        unitConfig.ConditionPathExists = [ configDir stateDir ];
+
+        restartTriggers = optionals (cfg.config != null) [ config.environment.etc."hqplayer/hqplayerd.xml".source ];
+
+        preStart = ''
+          cp -r "${pkg}/var/lib/hqplayer/web" "${stateDir}"
+          chmod -R u+wX "${stateDir}/web"
+
+          if [ ! -f "${configDir}/hqplayerd.xml" ]; then
+            echo "creating initial config file"
+            install -m 0644 "${pkg}/etc/hqplayer/hqplayerd.xml" "${configDir}/hqplayerd.xml"
+          fi
+        '' + optionalString (cfg.auth.username != null && cfg.auth.password != null) ''
+          ${pkg}/bin/hqplayerd -s ${cfg.auth.username} ${cfg.auth.password}
+        '';
+      };
+    };
+
+    users.groups = {
+      hqplayer.gid = config.ids.gids.hqplayer;
+    };
+
+    users.users = {
+      hqplayer = {
+        description = "hqplayer daemon user";
+        extraGroups = [ "audio" ];
+        group = "hqplayer";
+        uid = config.ids.uids.hqplayer;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/audio/icecast.nix b/nixos/modules/services/audio/icecast.nix
new file mode 100644
index 00000000000..5ee5bd745f9
--- /dev/null
+++ b/nixos/modules/services/audio/icecast.nix
@@ -0,0 +1,131 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.icecast;
+  configFile = pkgs.writeText "icecast.xml" ''
+    <icecast>
+      <hostname>${cfg.hostname}</hostname>
+
+      <authentication>
+        <admin-user>${cfg.admin.user}</admin-user>
+        <admin-password>${cfg.admin.password}</admin-password>
+      </authentication>
+
+      <paths>
+        <logdir>${cfg.logDir}</logdir>
+        <adminroot>${pkgs.icecast}/share/icecast/admin</adminroot>
+        <webroot>${pkgs.icecast}/share/icecast/web</webroot>
+        <alias source="/" dest="/status.xsl"/>
+      </paths>
+
+      <listen-socket>
+        <port>${toString cfg.listen.port}</port>
+        <bind-address>${cfg.listen.address}</bind-address>
+      </listen-socket>
+
+      <security>
+        <chroot>0</chroot>
+        <changeowner>
+            <user>${cfg.user}</user>
+            <group>${cfg.group}</group>
+        </changeowner>
+      </security>
+
+      ${cfg.extraConf}
+    </icecast>
+  '';
+in {
+
+  ###### interface
+
+  options = {
+
+    services.icecast = {
+
+      enable = mkEnableOption "Icecast server";
+
+      hostname = mkOption {
+        type = types.nullOr types.str;
+        description = "DNS name or IP address that will be used for the stream directory lookups or possibily the playlist generation if a Host header is not provided.";
+        default = config.networking.domain;
+        defaultText = literalExpression "config.networking.domain";
+      };
+
+      admin = {
+        user = mkOption {
+          type = types.str;
+          description = "Username used for all administration functions.";
+          default = "admin";
+        };
+
+        password = mkOption {
+          type = types.str;
+          description = "Password used for all administration functions.";
+        };
+      };
+
+      logDir = mkOption {
+        type = types.path;
+        description = "Base directory used for logging.";
+        default = "/var/log/icecast";
+      };
+
+      listen = {
+        port = mkOption {
+          type = types.int;
+          description = "TCP port that will be used to accept client connections.";
+          default = 8000;
+        };
+
+        address = mkOption {
+          type = types.str;
+          description = "Address Icecast will listen on.";
+          default = "::";
+        };
+      };
+
+      user = mkOption {
+        type = types.str;
+        description = "User privileges for the server.";
+        default = "nobody";
+      };
+
+      group = mkOption {
+        type = types.str;
+        description = "Group privileges for the server.";
+        default = "nogroup";
+      };
+
+      extraConf = mkOption {
+        type = types.lines;
+        description = "icecast.xml content.";
+        default = "";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.icecast = {
+      after = [ "network.target" ];
+      description = "Icecast Network Audio Streaming Server";
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = "mkdir -p ${cfg.logDir} && chown ${cfg.user}:${cfg.group} ${cfg.logDir}";
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${pkgs.icecast}/bin/icecast -c ${configFile}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/audio/jack.nix b/nixos/modules/services/audio/jack.nix
new file mode 100644
index 00000000000..84fc9957b87
--- /dev/null
+++ b/nixos/modules/services/audio/jack.nix
@@ -0,0 +1,294 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.jack;
+
+  pcmPlugin = cfg.jackd.enable && cfg.alsa.enable;
+  loopback = cfg.jackd.enable && cfg.loopback.enable;
+
+  enable32BitAlsaPlugins = cfg.alsa.support32Bit && pkgs.stdenv.isx86_64 && pkgs.pkgsi686Linux.alsa-lib != null;
+
+  umaskNeeded = versionOlder cfg.jackd.package.version "1.9.12";
+  bridgeNeeded = versionAtLeast cfg.jackd.package.version "1.9.12";
+in {
+  options = {
+    services.jack = {
+      jackd = {
+        enable = mkEnableOption ''
+          JACK Audio Connection Kit. You need to add yourself to the "jackaudio" group
+        '';
+
+        package = mkOption {
+          # until jack1 promiscuous mode is fixed
+          internal = true;
+          type = types.package;
+          default = pkgs.jack2;
+          defaultText = literalExpression "pkgs.jack2";
+          example = literalExpression "pkgs.jack1";
+          description = ''
+            The JACK package to use.
+          '';
+        };
+
+        extraOptions = mkOption {
+          type = types.listOf types.str;
+          default = [
+            "-dalsa"
+          ];
+          example = literalExpression ''
+            [ "-dalsa" "--device" "hw:1" ];
+          '';
+          description = ''
+            Specifies startup command line arguments to pass to JACK server.
+          '';
+        };
+
+        session = mkOption {
+          type = types.lines;
+          description = ''
+            Commands to run after JACK is started.
+          '';
+        };
+
+      };
+
+      alsa = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Route audio to/from generic ALSA-using applications using ALSA JACK PCM plugin.
+          '';
+        };
+
+        support32Bit = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether to support sound for 32-bit ALSA applications on 64-bit system.
+          '';
+        };
+      };
+
+      loopback = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Create ALSA loopback device, instead of using PCM plugin. Has broader
+            application support (things like Steam will work), but may need fine-tuning
+            for concrete hardware.
+          '';
+        };
+
+        index = mkOption {
+          type = types.int;
+          default = 10;
+          description = ''
+            Index of an ALSA loopback device.
+          '';
+        };
+
+        config = mkOption {
+          type = types.lines;
+          description = ''
+            ALSA config for loopback device.
+          '';
+        };
+
+        dmixConfig = mkOption {
+          type = types.lines;
+          default = "";
+          example = ''
+            period_size 2048
+            periods 2
+          '';
+          description = ''
+            For music production software that still doesn't support JACK natively you
+            would like to put buffer/period adjustments here
+            to decrease dmix device latency.
+          '';
+        };
+
+        session = mkOption {
+          type = types.lines;
+          description = ''
+            Additional commands to run to setup loopback device.
+          '';
+        };
+      };
+
+    };
+
+  };
+
+  config = mkMerge [
+
+    (mkIf pcmPlugin {
+      sound.extraConfig = ''
+        pcm_type.jack {
+          libs.native = ${pkgs.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_jack.so ;
+          ${lib.optionalString enable32BitAlsaPlugins
+          "libs.32Bit = ${pkgs.pkgsi686Linux.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_jack.so ;"}
+        }
+        pcm.!default {
+          @func getenv
+          vars [ PCM ]
+          default "plug:jack"
+        }
+      '';
+    })
+
+    (mkIf loopback {
+      boot.kernelModules = [ "snd-aloop" ];
+      boot.kernelParams = [ "snd-aloop.index=${toString cfg.loopback.index}" ];
+      sound.extraConfig = cfg.loopback.config;
+    })
+
+    (mkIf cfg.jackd.enable {
+      services.jack.jackd.session = ''
+        ${lib.optionalString bridgeNeeded "${pkgs.a2jmidid}/bin/a2jmidid -e &"}
+      '';
+      # https://alsa.opensrc.org/Jack_and_Loopback_device_as_Alsa-to-Jack_bridge#id06
+      services.jack.loopback.config = ''
+        pcm.loophw00 {
+          type hw
+          card ${toString cfg.loopback.index}
+          device 0
+          subdevice 0
+        }
+        pcm.amix {
+          type dmix
+          ipc_key 219345
+          slave {
+            pcm loophw00
+            ${cfg.loopback.dmixConfig}
+          }
+        }
+        pcm.asoftvol {
+          type softvol
+          slave.pcm "amix"
+          control { name Master }
+        }
+        pcm.cloop {
+          type hw
+          card ${toString cfg.loopback.index}
+          device 1
+          subdevice 0
+          format S32_LE
+        }
+        pcm.loophw01 {
+          type hw
+          card ${toString cfg.loopback.index}
+          device 0
+          subdevice 1
+        }
+        pcm.ploop {
+          type hw
+          card ${toString cfg.loopback.index}
+          device 1
+          subdevice 1
+          format S32_LE
+        }
+        pcm.aduplex {
+          type asym
+          playback.pcm "asoftvol"
+          capture.pcm "loophw01"
+        }
+        pcm.!default {
+          type plug
+          slave.pcm aduplex
+        }
+      '';
+      services.jack.loopback.session = ''
+        alsa_in -j cloop -dcloop &
+        alsa_out -j ploop -dploop &
+        while [ "$(jack_lsp cloop)" == "" ] || [ "$(jack_lsp ploop)" == "" ]; do sleep 1; done
+        jack_connect cloop:capture_1 system:playback_1
+        jack_connect cloop:capture_2 system:playback_2
+        jack_connect system:capture_1 ploop:playback_1
+        jack_connect system:capture_2 ploop:playback_2
+      '';
+
+      assertions = [
+        {
+          assertion = !(cfg.alsa.enable && cfg.loopback.enable);
+          message = "For JACK both alsa and loopback options shouldn't be used at the same time.";
+        }
+      ];
+
+      users.users.jackaudio = {
+        group = "jackaudio";
+        extraGroups = [ "audio" ];
+        description = "JACK Audio system service user";
+        isSystemUser = true;
+      };
+      # http://jackaudio.org/faq/linux_rt_config.html
+      security.pam.loginLimits = [
+        { domain = "@jackaudio"; type = "-"; item = "rtprio"; value = "99"; }
+        { domain = "@jackaudio"; type = "-"; item = "memlock"; value = "unlimited"; }
+      ];
+      users.groups.jackaudio = {};
+
+      environment = {
+        systemPackages = [ cfg.jackd.package ];
+        etc."alsa/conf.d/50-jack.conf".source = "${pkgs.alsa-plugins}/etc/alsa/conf.d/50-jack.conf";
+        variables.JACK_PROMISCUOUS_SERVER = "jackaudio";
+      };
+
+      services.udev.extraRules = ''
+        ACTION=="add", SUBSYSTEM=="sound", ATTRS{id}!="Loopback", TAG+="systemd", ENV{SYSTEMD_WANTS}="jack.service"
+      '';
+
+      systemd.services.jack = {
+        description = "JACK Audio Connection Kit";
+        serviceConfig = {
+          User = "jackaudio";
+          SupplementaryGroups = lib.optional
+            (config.hardware.pulseaudio.enable
+            && !config.hardware.pulseaudio.systemWide) "users";
+          ExecStart = "${cfg.jackd.package}/bin/jackd ${lib.escapeShellArgs cfg.jackd.extraOptions}";
+          LimitRTPRIO = 99;
+          LimitMEMLOCK = "infinity";
+        } // optionalAttrs umaskNeeded {
+          UMask = "007";
+        };
+        path = [ cfg.jackd.package ];
+        environment = {
+          JACK_PROMISCUOUS_SERVER = "jackaudio";
+          JACK_NO_AUDIO_RESERVATION = "1";
+        };
+        restartIfChanged = false;
+      };
+      systemd.services.jack-session = {
+        description = "JACK session";
+        script = ''
+          jack_wait -w
+          ${cfg.jackd.session}
+          ${lib.optionalString cfg.loopback.enable cfg.loopback.session}
+        '';
+        serviceConfig = {
+          RemainAfterExit = true;
+          User = "jackaudio";
+          StateDirectory = "jack";
+          LimitRTPRIO = 99;
+          LimitMEMLOCK = "infinity";
+        };
+        path = [ cfg.jackd.package ];
+        environment = {
+          JACK_PROMISCUOUS_SERVER = "jackaudio";
+          HOME = "/var/lib/jack";
+        };
+        wantedBy = [ "jack.service" ];
+        partOf = [ "jack.service" ];
+        after = [ "jack.service" ];
+        restartIfChanged = false;
+      };
+    })
+
+  ];
+
+  meta.maintainers = [ ];
+}
diff --git a/nixos/modules/services/audio/jmusicbot.nix b/nixos/modules/services/audio/jmusicbot.nix
new file mode 100644
index 00000000000..e0f8d461af0
--- /dev/null
+++ b/nixos/modules/services/audio/jmusicbot.nix
@@ -0,0 +1,48 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.jmusicbot;
+in
+{
+  options = {
+    services.jmusicbot = {
+      enable = mkEnableOption "jmusicbot, a Discord music bot that's easy to set up and run yourself";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.jmusicbot;
+        defaultText = literalExpression "pkgs.jmusicbot";
+        description = "JMusicBot package to use";
+      };
+
+      stateDir = mkOption {
+        type = types.path;
+        description = ''
+          The directory where config.txt and serversettings.json is saved.
+          If left as the default value this directory will automatically be created before JMusicBot starts, otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership and permissions.
+          Untouched by the value of this option config.txt needs to be placed manually into this directory.
+        '';
+        default = "/var/lib/jmusicbot/";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.jmusicbot = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      description = "Discord music bot that's easy to set up and run yourself!";
+      serviceConfig = mkMerge [{
+        ExecStart = "${cfg.package}/bin/JMusicBot";
+        WorkingDirectory = cfg.stateDir;
+        Restart = "always";
+        RestartSec = 20;
+        DynamicUser = true;
+      }
+        (mkIf (cfg.stateDir == "/var/lib/jmusicbot") { StateDirectory = "jmusicbot"; })];
+    };
+  };
+
+  meta.maintainers = with maintainers; [ SuperSandro2000 ];
+}
diff --git a/nixos/modules/services/audio/liquidsoap.nix b/nixos/modules/services/audio/liquidsoap.nix
new file mode 100644
index 00000000000..ffeefc0f988
--- /dev/null
+++ b/nixos/modules/services/audio/liquidsoap.nix
@@ -0,0 +1,69 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  streams = builtins.attrNames config.services.liquidsoap.streams;
+
+  streamService =
+    name:
+    let stream = builtins.getAttr name config.services.liquidsoap.streams; in
+    { inherit name;
+      value = {
+        after = [ "network-online.target" "sound.target" ];
+        description = "${name} liquidsoap stream";
+        wantedBy = [ "multi-user.target" ];
+        path = [ pkgs.wget ];
+        serviceConfig = {
+          ExecStart = "${pkgs.liquidsoap}/bin/liquidsoap ${stream}";
+          User = "liquidsoap";
+          LogsDirectory = "liquidsoap";
+        };
+      };
+    };
+in
+{
+
+  ##### interface
+
+  options = {
+
+    services.liquidsoap.streams = mkOption {
+
+      description =
+        ''
+          Set of Liquidsoap streams to start,
+          one systemd service per stream.
+        '';
+
+      default = {};
+
+      example = {
+        myStream1 = "/etc/liquidsoap/myStream1.liq";
+        myStream2 = literalExpression "./myStream2.liq";
+        myStream3 = "out(playlist(\"/srv/music/\"))";
+      };
+
+      type = types.attrsOf (types.either types.path types.str);
+    };
+
+  };
+  ##### implementation
+
+  config = mkIf (builtins.length streams != 0) {
+
+    users.users.liquidsoap = {
+      uid = config.ids.uids.liquidsoap;
+      group = "liquidsoap";
+      extraGroups = [ "audio" ];
+      description = "Liquidsoap streaming user";
+      home = "/var/lib/liquidsoap";
+      createHome = true;
+    };
+
+    users.groups.liquidsoap.gid = config.ids.gids.liquidsoap;
+
+    systemd.services = builtins.listToAttrs ( map streamService streams );
+  };
+
+}
diff --git a/nixos/modules/services/audio/mopidy.nix b/nixos/modules/services/audio/mopidy.nix
new file mode 100644
index 00000000000..9937feadaeb
--- /dev/null
+++ b/nixos/modules/services/audio/mopidy.nix
@@ -0,0 +1,108 @@
+{ config, lib, pkgs, ... }:
+
+with pkgs;
+with lib;
+
+let
+  uid = config.ids.uids.mopidy;
+  gid = config.ids.gids.mopidy;
+  cfg = config.services.mopidy;
+
+  mopidyConf = writeText "mopidy.conf" cfg.configuration;
+
+  mopidyEnv = buildEnv {
+    name = "mopidy-with-extensions-${mopidy.version}";
+    paths = closePropagation cfg.extensionPackages;
+    pathsToLink = [ "/${mopidyPackages.python.sitePackages}" ];
+    buildInputs = [ makeWrapper ];
+    postBuild = ''
+      makeWrapper ${mopidy}/bin/mopidy $out/bin/mopidy \
+        --prefix PYTHONPATH : $out/${mopidyPackages.python.sitePackages}
+    '';
+  };
+in {
+
+  options = {
+
+    services.mopidy = {
+
+      enable = mkEnableOption "Mopidy, a music player daemon";
+
+      dataDir = mkOption {
+        default = "/var/lib/mopidy";
+        type = types.str;
+        description = ''
+          The directory where Mopidy stores its state.
+        '';
+      };
+
+      extensionPackages = mkOption {
+        default = [];
+        type = types.listOf types.package;
+        example = literalExpression "[ pkgs.mopidy-spotify ]";
+        description = ''
+          Mopidy extensions that should be loaded by the service.
+        '';
+      };
+
+      configuration = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          The configuration that Mopidy should use.
+        '';
+      };
+
+      extraConfigFiles = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        description = ''
+          Extra config file read by Mopidy when the service starts.
+          Later files in the list overrides earlier configuration.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - mopidy mopidy - -"
+    ];
+
+    systemd.services.mopidy = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "sound.target" ];
+      description = "mopidy music player daemon";
+      serviceConfig = {
+        ExecStart = "${mopidyEnv}/bin/mopidy --config ${concatStringsSep ":" ([mopidyConf] ++ cfg.extraConfigFiles)}";
+        User = "mopidy";
+      };
+    };
+
+    systemd.services.mopidy-scan = {
+      description = "mopidy local files scanner";
+      serviceConfig = {
+        ExecStart = "${mopidyEnv}/bin/mopidy --config ${concatStringsSep ":" ([mopidyConf] ++ cfg.extraConfigFiles)} local scan";
+        User = "mopidy";
+        Type = "oneshot";
+      };
+    };
+
+    users.users.mopidy = {
+      inherit uid;
+      group = "mopidy";
+      extraGroups = [ "audio" ];
+      description = "Mopidy daemon user";
+      home = cfg.dataDir;
+    };
+
+    users.groups.mopidy.gid = gid;
+
+  };
+
+}
diff --git a/nixos/modules/services/audio/mpd.nix b/nixos/modules/services/audio/mpd.nix
new file mode 100644
index 00000000000..586b9ffa688
--- /dev/null
+++ b/nixos/modules/services/audio/mpd.nix
@@ -0,0 +1,265 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  name = "mpd";
+
+  uid = config.ids.uids.mpd;
+  gid = config.ids.gids.mpd;
+  cfg = config.services.mpd;
+
+  credentialsPlaceholder = (creds:
+    let
+      placeholders = (imap0
+        (i: c: ''password "{{password-${toString i}}}@${concatStringsSep "," c.permissions}"'')
+        creds);
+    in
+      concatStringsSep "\n" placeholders);
+
+  mpdConf = pkgs.writeText "mpd.conf" ''
+    # This file was automatically generated by NixOS. Edit mpd's configuration
+    # via NixOS' configuration.nix, as this file will be rewritten upon mpd's
+    # restart.
+
+    music_directory     "${cfg.musicDirectory}"
+    playlist_directory  "${cfg.playlistDirectory}"
+    ${lib.optionalString (cfg.dbFile != null) ''
+      db_file             "${cfg.dbFile}"
+    ''}
+    state_file          "${cfg.dataDir}/state"
+    sticker_file        "${cfg.dataDir}/sticker.sql"
+
+    ${optionalString (cfg.network.listenAddress != "any") ''bind_to_address "${cfg.network.listenAddress}"''}
+    ${optionalString (cfg.network.port != 6600)  ''port "${toString cfg.network.port}"''}
+    ${optionalString (cfg.fluidsynth) ''
+      decoder {
+              plugin "fluidsynth"
+              soundfont "${pkgs.soundfont-fluid}/share/soundfonts/FluidR3_GM2-2.sf2"
+      }
+    ''}
+
+    ${optionalString (cfg.credentials != []) (credentialsPlaceholder cfg.credentials)}
+
+    ${cfg.extraConfig}
+  '';
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.mpd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable MPD, the music player daemon.
+        '';
+      };
+
+      startWhenNeeded = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If set, <command>mpd</command> is socket-activated; that
+          is, instead of having it permanently running as a daemon,
+          systemd will start it on the first incoming connection.
+        '';
+      };
+
+      musicDirectory = mkOption {
+        type = with types; either path (strMatching "(http|https|nfs|smb)://.+");
+        default = "${cfg.dataDir}/music";
+        defaultText = literalExpression ''"''${dataDir}/music"'';
+        description = ''
+          The directory or NFS/SMB network share where MPD reads music from. If left
+          as the default value this directory will automatically be created before
+          the MPD server starts, otherwise the sysadmin is responsible for ensuring
+          the directory exists with appropriate ownership and permissions.
+        '';
+      };
+
+      playlistDirectory = mkOption {
+        type = types.path;
+        default = "${cfg.dataDir}/playlists";
+        defaultText = literalExpression ''"''${dataDir}/playlists"'';
+        description = ''
+          The directory where MPD stores playlists. If left as the default value
+          this directory will automatically be created before the MPD server starts,
+          otherwise the sysadmin is responsible for ensuring the directory exists
+          with appropriate ownership and permissions.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          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 <literal>man 5 mpd.conf</literal>'.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/${name}";
+        description = ''
+          The directory where MPD stores its state, tag cache, playlists etc. If
+          left as the default value this directory will automatically be created
+          before the MPD server starts, otherwise the sysadmin is responsible for
+          ensuring the directory exists with appropriate ownership and permissions.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = name;
+        description = "User account under which MPD runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = name;
+        description = "Group account under which MPD runs.";
+      };
+
+      network = {
+
+        listenAddress = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          example = "any";
+          description = ''
+            The address for the daemon to listen on.
+            Use <literal>any</literal> to listen on all addresses.
+          '';
+        };
+
+        port = mkOption {
+          type = types.int;
+          default = 6600;
+          description = ''
+            This setting is the TCP port that is desired for the daemon to get assigned
+            to.
+          '';
+        };
+
+      };
+
+      dbFile = mkOption {
+        type = types.nullOr types.str;
+        default = "${cfg.dataDir}/tag_cache";
+        defaultText = literalExpression ''"''${dataDir}/tag_cache"'';
+        description = ''
+          The path to MPD's database. If set to <literal>null</literal> the
+          parameter is omitted from the configuration.
+        '';
+      };
+
+      credentials = mkOption {
+        type = types.listOf (types.submodule {
+          options = {
+            passwordFile = mkOption {
+              type = types.path;
+              description = ''
+                Path to file containing the password.
+              '';
+            };
+            permissions = let
+              perms = ["read" "add" "control" "admin"];
+            in mkOption {
+              type = types.listOf (types.enum perms);
+              default = [ "read" ];
+              description = ''
+                List of permissions that are granted with this password.
+                Permissions can be "${concatStringsSep "\", \"" perms}".
+              '';
+            };
+          };
+        });
+        description = ''
+          Credentials and permissions for accessing the mpd server.
+        '';
+        default = [];
+        example = [
+          {passwordFile = "/var/lib/secrets/mpd_readonly_password"; permissions = [ "read" ];}
+          {passwordFile = "/var/lib/secrets/mpd_admin_password"; permissions = ["read" "add" "control" "admin"];}
+        ];
+      };
+
+      fluidsynth = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If set, add fluidsynth soundfont and configure the plugin.
+        '';
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    # install mpd units
+    systemd.packages = [ pkgs.mpd ];
+
+    systemd.sockets.mpd = mkIf cfg.startWhenNeeded {
+      wantedBy = [ "sockets.target" ];
+      listenStreams = [
+        (if pkgs.lib.hasPrefix "/" cfg.network.listenAddress
+          then cfg.network.listenAddress
+          else "${optionalString (cfg.network.listenAddress != "any") "${cfg.network.listenAddress}:"}${toString cfg.network.port}")
+      ];
+    };
+
+    systemd.services.mpd = {
+      wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
+
+      preStart =
+        ''
+          set -euo pipefail
+          install -m 600 ${mpdConf} /run/mpd/mpd.conf
+        '' + optionalString (cfg.credentials != [])
+        (concatStringsSep "\n"
+          (imap0
+            (i: c: ''${pkgs.replace-secret}/bin/replace-secret '{{password-${toString i}}}' '${c.passwordFile}' /run/mpd/mpd.conf'')
+            cfg.credentials));
+
+      serviceConfig =
+        {
+          User = "${cfg.user}";
+          # Note: the first "" overrides the ExecStart from the upstream unit
+          ExecStart = [ "" "${pkgs.mpd}/bin/mpd --systemd /run/mpd/mpd.conf" ];
+          RuntimeDirectory = "mpd";
+          StateDirectory = []
+            ++ optionals (cfg.dataDir == "/var/lib/${name}") [ name ]
+            ++ optionals (cfg.playlistDirectory == "/var/lib/${name}/playlists") [ name "${name}/playlists" ]
+            ++ optionals (cfg.musicDirectory == "/var/lib/${name}/music")        [ name "${name}/music" ];
+        };
+    };
+
+    users.users = optionalAttrs (cfg.user == name) {
+      ${name} = {
+        inherit uid;
+        group = cfg.group;
+        extraGroups = [ "audio" ];
+        description = "Music Player Daemon user";
+        home = "${cfg.dataDir}";
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == name) {
+      ${name}.gid = gid;
+    };
+  };
+
+}
diff --git a/nixos/modules/services/audio/mpdscribble.nix b/nixos/modules/services/audio/mpdscribble.nix
new file mode 100644
index 00000000000..333ffb70941
--- /dev/null
+++ b/nixos/modules/services/audio/mpdscribble.nix
@@ -0,0 +1,213 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mpdscribble;
+  mpdCfg = config.services.mpd;
+  mpdOpt = options.services.mpd;
+
+  endpointUrls = {
+    "last.fm" = "http://post.audioscrobbler.com";
+    "libre.fm" = "http://turtle.libre.fm";
+    "jamendo" = "http://postaudioscrobbler.jamendo.com";
+    "listenbrainz" = "http://proxy.listenbrainz.org";
+  };
+
+  mkSection = secname: secCfg: ''
+    [${secname}]
+    url      = ${secCfg.url}
+    username = ${secCfg.username}
+    password = {{${secname}_PASSWORD}}
+    journal  = /var/lib/mpdscribble/${secname}.journal
+  '';
+
+  endpoints = concatStringsSep "\n" (mapAttrsToList mkSection cfg.endpoints);
+  cfgTemplate = pkgs.writeText "mpdscribble.conf" ''
+    ## This file was automatically genenrated by NixOS and will be overwritten.
+    ## Do not edit. Edit your NixOS configuration instead.
+
+    ## mpdscribble - an audioscrobbler for the Music Player Daemon.
+    ## http://mpd.wikia.com/wiki/Client:mpdscribble
+
+    # HTTP proxy URL.
+    ${optionalString (cfg.proxy != null) "proxy = ${cfg.proxy}"}
+
+    # The location of the mpdscribble log file.  The special value
+    # "syslog" makes mpdscribble use the local syslog daemon.  On most
+    # systems, log messages will appear in /var/log/daemon.log then.
+    # "-" means log to stderr (the current terminal).
+    log = -
+
+    # How verbose mpdscribble's logging should be.  Default is 1.
+    verbose = ${toString cfg.verbose}
+
+    # How often should mpdscribble save the journal file? [seconds]
+    journal_interval = ${toString cfg.journalInterval}
+
+    # The host running MPD, possibly protected by a password
+    # ([PASSWORD@]HOSTNAME).
+    host = ${(optionalString (cfg.passwordFile != null) "{{MPD_PASSWORD}}@") + cfg.host}
+
+    # The port that the MPD listens on and mpdscribble should try to
+    # connect to.
+    port = ${toString cfg.port}
+
+    ${endpoints}
+  '';
+
+  cfgFile = "/run/mpdscribble/mpdscribble.conf";
+
+  replaceSecret = secretFile: placeholder: targetFile:
+    optionalString (secretFile != null) ''
+      ${pkgs.replace-secret}/bin/replace-secret '${placeholder}' '${secretFile}' '${targetFile}' '';
+
+  preStart = pkgs.writeShellScript "mpdscribble-pre-start" ''
+    cp -f "${cfgTemplate}" "${cfgFile}"
+    ${replaceSecret cfg.passwordFile "{{MPD_PASSWORD}}" cfgFile}
+    ${concatStringsSep "\n" (mapAttrsToList (secname: cfg:
+      replaceSecret cfg.passwordFile "{{${secname}_PASSWORD}}" cfgFile)
+      cfg.endpoints)}
+  '';
+
+  localMpd = (cfg.host == "localhost" || cfg.host == "127.0.0.1");
+
+in {
+  ###### interface
+
+  options.services.mpdscribble = {
+
+    enable = mkEnableOption "mpdscribble";
+
+    proxy = mkOption {
+      default = null;
+      type = types.nullOr types.str;
+      description = ''
+        HTTP proxy URL.
+      '';
+    };
+
+    verbose = mkOption {
+      default = 1;
+      type = types.int;
+      description = ''
+        Log level for the mpdscribble daemon.
+      '';
+    };
+
+    journalInterval = mkOption {
+      default = 600;
+      example = 60;
+      type = types.int;
+      description = ''
+        How often should mpdscribble save the journal file? [seconds]
+      '';
+    };
+
+    host = mkOption {
+      default = (if mpdCfg.network.listenAddress != "any" then
+        mpdCfg.network.listenAddress
+      else
+        "localhost");
+      defaultText = literalExpression ''
+        if config.${mpdOpt.network.listenAddress} != "any"
+        then config.${mpdOpt.network.listenAddress}
+        else "localhost"
+      '';
+      type = types.str;
+      description = ''
+        Host for the mpdscribble daemon to search for a mpd daemon on.
+      '';
+    };
+
+    passwordFile = mkOption {
+      default = if localMpd then
+        (findFirst
+          (c: any (x: x == "read") c.permissions)
+          { passwordFile = null; }
+          mpdCfg.credentials).passwordFile
+      else
+        null;
+      defaultText = literalDocBook ''
+        The first password file with read access configured for MPD when using a local instance,
+        otherwise <literal>null</literal>.
+      '';
+      type = types.nullOr types.str;
+      description = ''
+        File containing the password for the mpd daemon.
+        If there is a local mpd configured using <option>services.mpd.credentials</option>
+        the default is automatically set to a matching passwordFile of the local mpd.
+      '';
+    };
+
+    port = mkOption {
+      default = mpdCfg.network.port;
+      defaultText = literalExpression "config.${mpdOpt.network.port}";
+      type = types.port;
+      description = ''
+        Port for the mpdscribble daemon to search for a mpd daemon on.
+      '';
+    };
+
+    endpoints = mkOption {
+      type = (let
+        endpoint = { name, ... }: {
+          options = {
+            url = mkOption {
+              type = types.str;
+              default = endpointUrls.${name} or "";
+              description =
+                "The url endpoint where the scrobble API is listening.";
+            };
+            username = mkOption {
+              type = types.str;
+              description = ''
+                Username for the scrobble service.
+              '';
+            };
+            passwordFile = mkOption {
+              type = types.nullOr types.str;
+              description =
+                "File containing the password, either as MD5SUM or cleartext.";
+            };
+          };
+        };
+      in types.attrsOf (types.submodule endpoint));
+      default = { };
+      example = {
+        "last.fm" = {
+          username = "foo";
+          passwordFile = "/run/secrets/lastfm_password";
+        };
+      };
+      description = ''
+        Endpoints to scrobble to.
+        If the endpoint is one of "${
+          concatStringsSep "\", \"" (attrNames endpointUrls)
+        }" the url is set automatically.
+      '';
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.mpdscribble = {
+      after = [ "network.target" ] ++ (optional localMpd "mpd.service");
+      description = "mpdscribble mpd scrobble client";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "mpdscribble";
+        RuntimeDirectory = "mpdscribble";
+        RuntimeDirectoryMode = "700";
+        # TODO use LoadCredential= instead of running preStart with full privileges?
+        ExecStartPre = "+${preStart}";
+        ExecStart =
+          "${pkgs.mpdscribble}/bin/mpdscribble --no-daemon --conf ${cfgFile}";
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/services/audio/navidrome.nix b/nixos/modules/services/audio/navidrome.nix
new file mode 100644
index 00000000000..3660e05310b
--- /dev/null
+++ b/nixos/modules/services/audio/navidrome.nix
@@ -0,0 +1,71 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.navidrome;
+  settingsFormat = pkgs.formats.json {};
+in {
+  options = {
+    services.navidrome = {
+
+      enable = mkEnableOption "Navidrome music server";
+
+      settings = mkOption rec {
+        type = settingsFormat.type;
+        apply = recursiveUpdate default;
+        default = {
+          Address = "127.0.0.1";
+          Port = 4533;
+        };
+        example = {
+          MusicFolder = "/mnt/music";
+        };
+        description = ''
+          Configuration for Navidrome, see <link xlink:href="https://www.navidrome.org/docs/usage/configuration-options/"/> for supported values.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.navidrome = {
+      description = "Navidrome Media Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.navidrome}/bin/navidrome --configfile ${settingsFormat.generate "navidrome.json" cfg.settings}
+        '';
+        DynamicUser = true;
+        StateDirectory = "navidrome";
+        WorkingDirectory = "/var/lib/navidrome";
+        RuntimeDirectory = "navidrome";
+        RootDirectory = "/run/navidrome";
+        ReadWritePaths = "";
+        BindReadOnlyPaths = [
+          builtins.storeDir
+        ] ++ lib.optional (cfg.settings ? MusicFolder) cfg.settings.MusicFolder;
+        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" "~@resources" ];
+        RestrictRealtime = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        UMask = "0066";
+        ProtectHostname = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/audio/networkaudiod.nix b/nixos/modules/services/audio/networkaudiod.nix
new file mode 100644
index 00000000000..265a4e1d95d
--- /dev/null
+++ b/nixos/modules/services/audio/networkaudiod.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  name = "networkaudiod";
+  cfg = config.services.networkaudiod;
+in {
+  options = {
+    services.networkaudiod = {
+      enable = mkEnableOption "Networkaudiod (NAA)";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.packages = [ pkgs.networkaudiod ];
+    systemd.services.networkaudiod.wantedBy = [ "multi-user.target" ];
+  };
+}
diff --git a/nixos/modules/services/audio/roon-bridge.nix b/nixos/modules/services/audio/roon-bridge.nix
new file mode 100644
index 00000000000..e08f8a4f9e7
--- /dev/null
+++ b/nixos/modules/services/audio/roon-bridge.nix
@@ -0,0 +1,76 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  name = "roon-bridge";
+  cfg = config.services.roon-bridge;
+in {
+  options = {
+    services.roon-bridge = {
+      enable = mkEnableOption "Roon Bridge";
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for the bridge.
+        '';
+      };
+      user = mkOption {
+        type = types.str;
+        default = "roon-bridge";
+        description = ''
+          User to run the Roon bridge as.
+        '';
+      };
+      group = mkOption {
+        type = types.str;
+        default = "roon-bridge";
+        description = ''
+          Group to run the Roon Bridge as.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.roon-bridge = {
+      after = [ "network.target" ];
+      description = "Roon Bridge";
+      wantedBy = [ "multi-user.target" ];
+
+      environment.ROON_DATAROOT = "/var/lib/${name}";
+
+      serviceConfig = {
+        ExecStart = "${pkgs.roon-bridge}/start.sh";
+        LimitNOFILE = 8192;
+        User = cfg.user;
+        Group = cfg.group;
+        StateDirectory = name;
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPortRanges = [{ from = 9100; to = 9200; }];
+      allowedUDPPorts = [ 9003 ];
+      extraCommands = ''
+        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
+      '';
+    };
+
+
+    users.groups.${cfg.group} = {};
+    users.users.${cfg.user} =
+      if cfg.user == "roon-bridge" then {
+        isSystemUser = true;
+        description = "Roon Bridge user";
+        group = cfg.group;
+        extraGroups = [ "audio" ];
+      }
+      else {};
+  };
+}
diff --git a/nixos/modules/services/audio/roon-server.nix b/nixos/modules/services/audio/roon-server.nix
new file mode 100644
index 00000000000..de1f61c8e73
--- /dev/null
+++ b/nixos/modules/services/audio/roon-server.nix
@@ -0,0 +1,79 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  name = "roon-server";
+  cfg = config.services.roon-server;
+in {
+  options = {
+    services.roon-server = {
+      enable = mkEnableOption "Roon Server";
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for the server.
+        '';
+      };
+      user = mkOption {
+        type = types.str;
+        default = "roon-server";
+        description = ''
+          User to run the Roon Server as.
+        '';
+      };
+      group = mkOption {
+        type = types.str;
+        default = "roon-server";
+        description = ''
+          Group to run the Roon Server as.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.roon-server = {
+      after = [ "network.target" ];
+      description = "Roon Server";
+      wantedBy = [ "multi-user.target" ];
+
+      environment.ROON_DATAROOT = "/var/lib/${name}";
+
+      serviceConfig = {
+        ExecStart = "${pkgs.roon-server}/bin/RoonServer";
+        LimitNOFILE = 8192;
+        User = cfg.user;
+        Group = cfg.group;
+        StateDirectory = name;
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPortRanges = [
+        { from = 9100; to = 9200; }
+        { from = 9330; to = 9332; }
+      ];
+      allowedUDPPorts = [ 9003 ];
+      extraCommands = ''
+        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
+      '';
+    };
+
+
+    users.groups.${cfg.group} = {};
+    users.users.${cfg.user} =
+      if cfg.user == "roon-server" then {
+        isSystemUser = true;
+        description = "Roon Server user";
+        group = cfg.group;
+        extraGroups = [ "audio" ];
+      }
+      else {};
+  };
+}
diff --git a/nixos/modules/services/audio/slimserver.nix b/nixos/modules/services/audio/slimserver.nix
new file mode 100644
index 00000000000..ecd26528499
--- /dev/null
+++ b/nixos/modules/services/audio/slimserver.nix
@@ -0,0 +1,73 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.slimserver;
+
+in {
+  options = {
+
+    services.slimserver = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable slimserver.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.slimserver;
+        defaultText = literalExpression "pkgs.slimserver";
+        description = "Slimserver package to use.";
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/slimserver";
+        description = ''
+          The directory where slimserver stores its state, tag cache,
+          playlists etc.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - slimserver slimserver - -"
+    ];
+
+    systemd.services.slimserver = {
+      after = [ "network.target" ];
+      description = "Slim Server for Logitech Squeezebox Players";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        User = "slimserver";
+        # Issue 40589: Disable broken image/video support (audio still works!)
+        ExecStart = "${cfg.package}/slimserver.pl --logdir ${cfg.dataDir}/logs --prefsdir ${cfg.dataDir}/prefs --cachedir ${cfg.dataDir}/cache --noimage --novideo";
+      };
+    };
+
+    users = {
+      users.slimserver = {
+        description = "Slimserver daemon user";
+        home = cfg.dataDir;
+        group = "slimserver";
+        isSystemUser = true;
+      };
+      groups.slimserver = {};
+    };
+  };
+
+}
+
diff --git a/nixos/modules/services/audio/snapserver.nix b/nixos/modules/services/audio/snapserver.nix
new file mode 100644
index 00000000000..6d5ce98df89
--- /dev/null
+++ b/nixos/modules/services/audio/snapserver.nix
@@ -0,0 +1,315 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  name = "snapserver";
+
+  cfg = config.services.snapserver;
+
+  # Using types.nullOr to inherit upstream defaults.
+  sampleFormat = mkOption {
+    type = with types; nullOr str;
+    default = null;
+    description = ''
+      Default sample format.
+    '';
+    example = "48000:16:2";
+  };
+
+  codec = mkOption {
+    type = with types; nullOr str;
+    default = null;
+    description = ''
+      Default audio compression method.
+    '';
+    example = "flac";
+  };
+
+  streamToOption = name: opt:
+    let
+      os = val:
+        optionalString (val != null) "${val}";
+      os' = prefix: val:
+        optionalString (val != null) (prefix + "${val}");
+      flatten = key: value:
+        "&${key}=${value}";
+    in
+      "--stream.stream=\"${opt.type}://" + os opt.location + "?" + os' "name=" name
+        + concatStrings (mapAttrsToList flatten opt.query) + "\"";
+
+  optionalNull = val: ret:
+    optional (val != null) ret;
+
+  optionString = concatStringsSep " " (mapAttrsToList streamToOption cfg.streams
+    # global options
+    ++ [ "--stream.bind_to_address=${cfg.listenAddress}" ]
+    ++ [ "--stream.port=${toString cfg.port}" ]
+    ++ optionalNull cfg.sampleFormat "--stream.sampleformat=${cfg.sampleFormat}"
+    ++ optionalNull cfg.codec "--stream.codec=${cfg.codec}"
+    ++ optionalNull cfg.streamBuffer "--stream.stream_buffer=${toString cfg.streamBuffer}"
+    ++ optionalNull cfg.buffer "--stream.buffer=${toString cfg.buffer}"
+    ++ optional cfg.sendToMuted "--stream.send_to_muted"
+    # tcp json rpc
+    ++ [ "--tcp.enabled=${toString cfg.tcp.enable}" ]
+    ++ optionals cfg.tcp.enable [
+      "--tcp.bind_to_address=${cfg.tcp.listenAddress}"
+      "--tcp.port=${toString cfg.tcp.port}" ]
+     # http json rpc
+    ++ [ "--http.enabled=${toString cfg.http.enable}" ]
+    ++ optionals cfg.http.enable [
+      "--http.bind_to_address=${cfg.http.listenAddress}"
+      "--http.port=${toString cfg.http.port}"
+    ] ++ optional (cfg.http.docRoot != null) "--http.doc_root=\"${toString cfg.http.docRoot}\"");
+
+in {
+  imports = [
+    (mkRenamedOptionModule [ "services" "snapserver" "controlPort" ] [ "services" "snapserver" "tcp" "port" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.snapserver = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable snapserver.
+        '';
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "::";
+        example = "0.0.0.0";
+        description = ''
+          The address where snapclients can connect.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 1704;
+        description = ''
+          The port that snapclients can connect to.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to automatically open the specified ports in the firewall.
+        '';
+      };
+
+      inherit sampleFormat;
+      inherit codec;
+
+      streamBuffer = mkOption {
+        type = with types; nullOr int;
+        default = null;
+        description = ''
+          Stream read (input) buffer in ms.
+        '';
+        example = 20;
+      };
+
+      buffer = mkOption {
+        type = with types; nullOr int;
+        default = null;
+        description = ''
+          Network buffer in ms.
+        '';
+        example = 1000;
+      };
+
+      sendToMuted = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Send audio to muted clients.
+        '';
+      };
+
+      tcp.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable the JSON-RPC via TCP.
+        '';
+      };
+
+      tcp.listenAddress = mkOption {
+        type = types.str;
+        default = "::";
+        example = "0.0.0.0";
+        description = ''
+          The address where the TCP JSON-RPC listens on.
+        '';
+      };
+
+      tcp.port = mkOption {
+        type = types.port;
+        default = 1705;
+        description = ''
+          The port where the TCP JSON-RPC listens on.
+        '';
+      };
+
+      http.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable the JSON-RPC via HTTP.
+        '';
+      };
+
+      http.listenAddress = mkOption {
+        type = types.str;
+        default = "::";
+        example = "0.0.0.0";
+        description = ''
+          The address where the HTTP JSON-RPC listens on.
+        '';
+      };
+
+      http.port = mkOption {
+        type = types.port;
+        default = 1780;
+        description = ''
+          The port where the HTTP JSON-RPC listens on.
+        '';
+      };
+
+      http.docRoot = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = ''
+          Path to serve from the HTTP servers root.
+        '';
+      };
+
+      streams = mkOption {
+        type = with types; attrsOf (submodule {
+          options = {
+            location = mkOption {
+              type = types.oneOf [ types.path types.str ];
+              description = ''
+                For type <literal>pipe</literal> or <literal>file</literal>, the path to the pipe or file.
+                For type <literal>librespot</literal>, <literal>airplay</literal> or <literal>process</literal>, the path to the corresponding binary.
+                For type <literal>tcp</literal>, the <literal>host:port</literal> address to connect to or listen on.
+                For type <literal>meta</literal>, a list of stream names in the form <literal>/one/two/...</literal>. Don't forget the leading slash.
+                For type <literal>alsa</literal>, use an empty string.
+              '';
+              example = literalExpression ''
+                "/path/to/pipe"
+                "/path/to/librespot"
+                "192.168.1.2:4444"
+                "/MyTCP/Spotify/MyPipe"
+              '';
+            };
+            type = mkOption {
+              type = types.enum [ "pipe" "librespot" "airplay" "file" "process" "tcp" "alsa" "spotify" "meta" ];
+              default = "pipe";
+              description = ''
+                The type of input stream.
+              '';
+            };
+            query = mkOption {
+              type = attrsOf str;
+              default = {};
+              description = ''
+                Key-value pairs that convey additional parameters about a stream.
+              '';
+              example = literalExpression ''
+                # for type == "pipe":
+                {
+                  mode = "create";
+                };
+                # for type == "process":
+                {
+                  params = "--param1 --param2";
+                  logStderr = "true";
+                };
+                # for type == "tcp":
+                {
+                  mode = "client";
+                }
+                # for type == "alsa":
+                {
+                  device = "hw:0,0";
+                }
+              '';
+            };
+            inherit sampleFormat;
+            inherit codec;
+          };
+        });
+        default = { default = {}; };
+        description = ''
+          The definition for an input source.
+        '';
+        example = literalExpression ''
+          {
+            mpd = {
+              type = "pipe";
+              location = "/run/snapserver/mpd";
+              sampleFormat = "48000:16:2";
+              codec = "pcm";
+            };
+          };
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    # https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85
+    warnings = filter (w: w != "") (mapAttrsToList (k: v: if v.type == "spotify" then ''
+      services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead.
+    '' else "") cfg.streams);
+
+    systemd.services.snapserver = {
+      after = [ "network.target" ];
+      description = "Snapserver";
+      wantedBy = [ "multi-user.target" ];
+      before = [ "mpd.service" "mopidy.service" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${pkgs.snapcast}/bin/snapserver --daemon ${optionString}";
+        Type = "forking";
+        LimitRTPRIO = 50;
+        LimitRTTIME = "infinity";
+        NoNewPrivileges = true;
+        PIDFile = "/run/${name}/pid";
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
+        RestrictNamespaces = true;
+        RuntimeDirectory = name;
+        StateDirectory = name;
+      };
+    };
+
+    networking.firewall.allowedTCPPorts =
+      optionals cfg.openFirewall [ cfg.port ]
+      ++ optional cfg.tcp.enable cfg.tcp.port
+      ++ optional cfg.http.enable cfg.http.port;
+  };
+
+  meta = {
+    maintainers = with maintainers; [ tobim ];
+  };
+
+}
diff --git a/nixos/modules/services/audio/spotifyd.nix b/nixos/modules/services/audio/spotifyd.nix
new file mode 100644
index 00000000000..22848ed9800
--- /dev/null
+++ b/nixos/modules/services/audio/spotifyd.nix
@@ -0,0 +1,68 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.spotifyd;
+  toml = pkgs.formats.toml {};
+  warnConfig =
+    if cfg.config != ""
+    then lib.trace "Using the stringly typed .config attribute is discouraged. Use the TOML typed .settings attribute instead."
+    else id;
+  spotifydConf =
+    if cfg.settings != {}
+    then toml.generate "spotify.conf" cfg.settings
+    else warnConfig (pkgs.writeText "spotifyd.conf" cfg.config);
+in
+{
+  options = {
+    services.spotifyd = {
+      enable = mkEnableOption "spotifyd, a Spotify playing daemon";
+
+      config = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          (Deprecated) Configuration for Spotifyd. For syntax and directives, see
+          <link xlink:href="https://github.com/Spotifyd/spotifyd#Configuration"/>.
+        '';
+      };
+
+      settings = mkOption {
+        default = {};
+        type = toml.type;
+        example = { global.bitrate = 320; };
+        description = ''
+          Configuration for Spotifyd. For syntax and directives, see
+          <link xlink:href="https://github.com/Spotifyd/spotifyd#Configuration"/>.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.config == "" || cfg.settings == {};
+        message = "At most one of the .config attribute and the .settings attribute may be set";
+      }
+    ];
+
+    systemd.services.spotifyd = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" "sound.target" ];
+      description = "spotifyd, a Spotify playing daemon";
+      environment.SHELL = "/bin/sh";
+      serviceConfig = {
+        ExecStart = "${pkgs.spotifyd}/bin/spotifyd --no-daemon --cache-path /var/cache/spotifyd --config-path ${spotifydConf}";
+        Restart = "always";
+        RestartSec = 12;
+        DynamicUser = true;
+        CacheDirectory = "spotifyd";
+        SupplementaryGroups = ["audio"];
+      };
+    };
+  };
+
+  meta.maintainers = [ maintainers.anderslundstedt ];
+}
diff --git a/nixos/modules/services/audio/squeezelite.nix b/nixos/modules/services/audio/squeezelite.nix
new file mode 100644
index 00000000000..36295e21c60
--- /dev/null
+++ b/nixos/modules/services/audio/squeezelite.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkEnableOption mkIf mkOption optionalString types;
+
+  dataDir = "/var/lib/squeezelite";
+  cfg = config.services.squeezelite;
+  pkg = if cfg.pulseAudio then pkgs.squeezelite-pulse else pkgs.squeezelite;
+  bin = "${pkg}/bin/${pkg.pname}";
+
+in
+{
+
+  ###### interface
+
+  options.services.squeezelite = {
+    enable = mkEnableOption "Squeezelite, a software Squeezebox emulator";
+
+    pulseAudio = mkEnableOption "pulseaudio support";
+
+    extraArguments = mkOption {
+      default = "";
+      type = types.str;
+      description = ''
+        Additional command line arguments to pass to Squeezelite.
+      '';
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.squeezelite = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "sound.target" ];
+      description = "Software Squeezebox emulator";
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${bin} -N ${dataDir}/player-name ${cfg.extraArguments}";
+        StateDirectory = builtins.baseNameOf dataDir;
+        SupplementaryGroups = "audio";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/audio/ympd.nix b/nixos/modules/services/audio/ympd.nix
new file mode 100644
index 00000000000..84b72d14251
--- /dev/null
+++ b/nixos/modules/services/audio/ympd.nix
@@ -0,0 +1,57 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ympd;
+in {
+
+  ###### interface
+
+  options = {
+
+    services.ympd = {
+
+      enable = mkEnableOption "ympd, the MPD Web GUI";
+
+      webPort = mkOption {
+        type = types.either types.str types.port; # string for backwards compat
+        default = "8080";
+        description = "The port where ympd's web interface will be available.";
+        example = "ssl://8080:/path/to/ssl-private-key.pem";
+      };
+
+      mpd = {
+        host = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = "The host where MPD is listening.";
+        };
+
+        port = mkOption {
+          type = types.int;
+          default = config.services.mpd.network.port;
+          defaultText = literalExpression "config.services.mpd.network.port";
+          description = "The port where MPD is listening.";
+          example = 6600;
+        };
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    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";
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/backup/automysqlbackup.nix b/nixos/modules/services/backup/automysqlbackup.nix
new file mode 100644
index 00000000000..fd2764a40ad
--- /dev/null
+++ b/nixos/modules/services/backup/automysqlbackup.nix
@@ -0,0 +1,119 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+  inherit (lib) concatMapStringsSep concatStringsSep isInt isList literalExpression;
+  inherit (lib) mapAttrs mapAttrsToList mkDefault mkEnableOption mkIf mkOption optional types;
+
+  cfg = config.services.automysqlbackup;
+  pkg = pkgs.automysqlbackup;
+  user = "automysqlbackup";
+  group = "automysqlbackup";
+
+  toStr = val:
+    if isList val then "( ${concatMapStringsSep " " (val: "'${val}'") val} )"
+    else if isInt val then toString val
+    else if true == val then "'yes'"
+    else if false == val then "'no'"
+    else "'${toString val}'";
+
+  configFile = pkgs.writeText "automysqlbackup.conf" ''
+    #version=${pkg.version}
+    # DONT'T REMOVE THE PREVIOUS VERSION LINE!
+    #
+    ${concatStringsSep "\n" (mapAttrsToList (name: value: "CONFIG_${name}=${toStr value}") cfg.config)}
+  '';
+
+in
+{
+  # interface
+  options = {
+    services.automysqlbackup = {
+
+      enable = mkEnableOption "AutoMySQLBackup";
+
+      calendar = mkOption {
+        type = types.str;
+        default = "01:15:00";
+        description = ''
+          Configured when to run the backup service systemd unit (DayOfWeek Year-Month-Day Hour:Minute:Second).
+        '';
+      };
+
+      config = mkOption {
+        type = with types; attrsOf (oneOf [ str int bool (listOf str) ]);
+        default = {};
+        description = ''
+          automysqlbackup configuration. Refer to
+          <filename>''${pkgs.automysqlbackup}/etc/automysqlbackup.conf</filename>
+          for details on supported values.
+        '';
+        example = literalExpression ''
+          {
+            db_names = [ "nextcloud" "matomo" ];
+            table_exclude = [ "nextcloud.oc_users" "nextcloud.oc_whats_new" ];
+            mailcontent = "log";
+            mail_address = "admin@example.org";
+          }
+        '';
+      };
+
+    };
+  };
+
+  # implementation
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = !config.services.mysqlBackup.enable;
+        message = "Please choose one of services.mysqlBackup or services.automysqlbackup.";
+      }
+    ];
+
+    services.automysqlbackup.config = mapAttrs (name: mkDefault) {
+      mysql_dump_username = user;
+      mysql_dump_host = "localhost";
+      mysql_dump_socket = "/run/mysqld/mysqld.sock";
+      backup_dir = "/var/backup/mysql";
+      db_exclude = [ "information_schema" "performance_schema" ];
+      mailcontent = "stdout";
+      mysql_dump_single_transaction = true;
+    };
+
+    systemd.timers.automysqlbackup = {
+      description = "automysqlbackup timer";
+      wantedBy = [ "timers.target" ];
+      timerConfig = {
+        OnCalendar = cfg.calendar;
+        AccuracySec = "5m";
+      };
+    };
+
+    systemd.services.automysqlbackup = {
+      description = "automysqlbackup service";
+      serviceConfig = {
+        User = user;
+        Group = group;
+        ExecStart = "${pkg}/bin/automysqlbackup ${configFile}";
+      };
+    };
+
+    environment.systemPackages = [ pkg ];
+
+    users.users.${user} = {
+      group = group;
+      isSystemUser = true;
+    };
+    users.groups.${group} = { };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.config.backup_dir}' 0750 ${user} ${group} - -"
+    ];
+
+    services.mysql.ensureUsers = optional (config.services.mysql.enable && cfg.config.mysql_dump_host == "localhost") {
+      name = user;
+      ensurePermissions = { "*.*" = "SELECT, SHOW VIEW, TRIGGER, LOCK TABLES"; };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/backup/bacula.nix b/nixos/modules/services/backup/bacula.nix
new file mode 100644
index 00000000000..59890204234
--- /dev/null
+++ b/nixos/modules/services/backup/bacula.nix
@@ -0,0 +1,578 @@
+{ config, lib, pkgs, ... }:
+
+
+# TODO: test configuration when building nixexpr (use -t parameter)
+# TODO: support sqlite3 (it's deprecate?) and mysql
+
+with lib;
+
+let
+  libDir = "/var/lib/bacula";
+
+  fd_cfg = config.services.bacula-fd;
+  fd_conf = pkgs.writeText "bacula-fd.conf"
+    ''
+      Client {
+        Name = "${fd_cfg.name}";
+        FDPort = ${toString fd_cfg.port};
+        WorkingDirectory = "${libDir}";
+        Pid Directory = "/run";
+        ${fd_cfg.extraClientConfig}
+      }
+
+      ${concatStringsSep "\n" (mapAttrsToList (name: value: ''
+      Director {
+        Name = "${name}";
+        Password = "${value.password}";
+        Monitor = "${value.monitor}";
+      }
+      '') fd_cfg.director)}
+
+      Messages {
+        Name = Standard;
+        syslog = all, !skipped, !restored
+        ${fd_cfg.extraMessagesConfig}
+      }
+    '';
+
+  sd_cfg = config.services.bacula-sd;
+  sd_conf = pkgs.writeText "bacula-sd.conf"
+    ''
+      Storage {
+        Name = "${sd_cfg.name}";
+        SDPort = ${toString sd_cfg.port};
+        WorkingDirectory = "${libDir}";
+        Pid Directory = "/run";
+        ${sd_cfg.extraStorageConfig}
+      }
+
+      ${concatStringsSep "\n" (mapAttrsToList (name: value: ''
+      Autochanger {
+        Name = "${name}";
+        Device = ${concatStringsSep ", " (map (a: "\"${a}\"") value.devices)};
+        Changer Device =  "${value.changerDevice}";
+        Changer Command = "${value.changerCommand}";
+        ${value.extraAutochangerConfig}
+      }
+      '') sd_cfg.autochanger)}
+
+      ${concatStringsSep "\n" (mapAttrsToList (name: value: ''
+      Device {
+        Name = "${name}";
+        Archive Device = "${value.archiveDevice}";
+        Media Type = "${value.mediaType}";
+        ${value.extraDeviceConfig}
+      }
+      '') sd_cfg.device)}
+
+      ${concatStringsSep "\n" (mapAttrsToList (name: value: ''
+      Director {
+        Name = "${name}";
+        Password = "${value.password}";
+        Monitor = "${value.monitor}";
+      }
+      '') sd_cfg.director)}
+
+      Messages {
+        Name = Standard;
+        syslog = all, !skipped, !restored
+        ${sd_cfg.extraMessagesConfig}
+      }
+    '';
+
+  dir_cfg = config.services.bacula-dir;
+  dir_conf = pkgs.writeText "bacula-dir.conf"
+    ''
+    Director {
+      Name = "${dir_cfg.name}";
+      Password = "${dir_cfg.password}";
+      DirPort = ${toString dir_cfg.port};
+      Working Directory = "${libDir}";
+      Pid Directory = "/run/";
+      QueryFile = "${pkgs.bacula}/etc/query.sql";
+      ${dir_cfg.extraDirectorConfig}
+    }
+
+    Catalog {
+      Name = "PostgreSQL";
+      dbname = "bacula";
+      user = "bacula";
+    }
+
+    Messages {
+      Name = Standard;
+      syslog = all, !skipped, !restored
+      ${dir_cfg.extraMessagesConfig}
+    }
+
+    ${dir_cfg.extraConfig}
+    '';
+
+  directorOptions = {...}:
+  {
+    options = {
+      password = mkOption {
+        type = types.str;
+        # TODO: required?
+        description = ''
+          Specifies the password that must be supplied for the default Bacula
+          Console to be authorized. The same password must appear in the
+          Director resource of the Console configuration file. For added
+          security, the password is never passed across the network but instead
+          a challenge response hash code created with the password. This
+          directive is required. If you have either /dev/random or bc on your
+          machine, Bacula will generate a random password during the
+          configuration process, otherwise it will be left blank and you must
+          manually supply it.
+
+          The password is plain text. It is not generated through any special
+          process but as noted above, it is better to use random text for
+          security reasons.
+        '';
+      };
+
+      monitor = mkOption {
+        type = types.enum [ "no" "yes" ];
+        default = "no";
+        example = "yes";
+        description = ''
+          If Monitor is set to <literal>no</literal>, this director will have
+          full access to this Storage daemon. If Monitor is set to
+          <literal>yes</literal>, this director will only be able to fetch the
+          current status of this Storage daemon.
+
+          Please note that if this director is being used by a Monitor, we
+          highly recommend to set this directive to yes to avoid serious
+          security problems.
+        '';
+      };
+    };
+  };
+
+  autochangerOptions = {...}:
+  {
+    options = {
+      changerDevice = mkOption {
+        type = types.str;
+        description = ''
+          The specified name-string must be the generic SCSI device name of the
+          autochanger that corresponds to the normal read/write Archive Device
+          specified in the Device resource. This generic SCSI device name
+          should be specified if you have an autochanger or if you have a
+          standard tape drive and want to use the Alert Command (see below).
+          For example, on Linux systems, for an Archive Device name of
+          <literal>/dev/nst0</literal>, you would specify
+          <literal>/dev/sg0</literal> for the Changer Device name.  Depending
+          on your exact configuration, and the number of autochangers or the
+          type of autochanger, what you specify here can vary. This directive
+          is optional. See the Using AutochangersAutochangersChapter chapter of
+          this manual for more details of using this and the following
+          autochanger directives.
+          '';
+      };
+
+      changerCommand = mkOption {
+        type = types.str;
+        description = ''
+          The name-string specifies an external program to be called that will
+          automatically change volumes as required by Bacula. Normally, this
+          directive will be specified only in the AutoChanger resource, which
+          is then used for all devices. However, you may also specify the
+          different Changer Command in each Device resource. Most frequently,
+          you will specify the Bacula supplied mtx-changer script as follows:
+
+          <literal>"/path/mtx-changer %c %o %S %a %d"</literal>
+
+          and you will install the mtx on your system (found in the depkgs
+          release). An example of this command is in the default bacula-sd.conf
+          file. For more details on the substitution characters that may be
+          specified to configure your autochanger please see the
+          AutochangersAutochangersChapter chapter of this manual. For FreeBSD
+          users, you might want to see one of the several chio scripts in
+          examples/autochangers.
+          '';
+        default = "/etc/bacula/mtx-changer %c %o %S %a %d";
+      };
+
+      devices = mkOption {
+        description = "";
+        type = types.listOf types.str;
+      };
+
+      extraAutochangerConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra configuration to be passed in Autochanger directive.
+        '';
+        example = ''
+
+        '';
+      };
+    };
+  };
+
+
+  deviceOptions = {...}:
+  {
+    options = {
+      archiveDevice = mkOption {
+        # TODO: required?
+        type = types.str;
+        description = ''
+          The specified name-string gives the system file name of the storage
+          device managed by this storage daemon. This will usually be the
+          device file name of a removable storage device (tape drive), for
+          example <literal>/dev/nst0</literal> or
+          <literal>/dev/rmt/0mbn</literal>. For a DVD-writer, it will be for
+          example <literal>/dev/hdc</literal>. It may also be a directory name
+          if you are archiving to disk storage. In this case, you must supply
+          the full absolute path to the directory. When specifying a tape
+          device, it is preferable that the "non-rewind" variant of the device
+          file name be given.
+        '';
+      };
+
+      mediaType = mkOption {
+        # TODO: required?
+        type = types.str;
+        description = ''
+          The specified name-string names the type of media supported by this
+          device, for example, <literal>DLT7000</literal>. Media type names are
+          arbitrary in that you set them to anything you want, but they must be
+          known to the volume database to keep track of which storage daemons
+          can read which volumes. In general, each different storage type
+          should have a unique Media Type associated with it. The same
+          name-string must appear in the appropriate Storage resource
+          definition in the Director's configuration file.
+
+          Even though the names you assign are arbitrary (i.e. you choose the
+          name you want), you should take care in specifying them because the
+          Media Type is used to determine which storage device Bacula will
+          select during restore. Thus you should probably use the same Media
+          Type specification for all drives where the Media can be freely
+          interchanged. This is not generally an issue if you have a single
+          Storage daemon, but it is with multiple Storage daemons, especially
+          if they have incompatible media.
+
+          For example, if you specify a Media Type of <literal>DDS-4</literal>
+          then during the restore, Bacula will be able to choose any Storage
+          Daemon that handles <literal>DDS-4</literal>. If you have an
+          autochanger, you might want to name the Media Type in a way that is
+          unique to the autochanger, unless you wish to possibly use the
+          Volumes in other drives. You should also ensure to have unique Media
+          Type names if the Media is not compatible between drives. This
+          specification is required for all devices.
+
+          In addition, if you are using disk storage, each Device resource will
+          generally have a different mount point or directory. In order for
+          Bacula to select the correct Device resource, each one must have a
+          unique Media Type.
+        '';
+      };
+
+      extraDeviceConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra configuration to be passed in Device directive.
+        '';
+        example = ''
+          LabelMedia = yes
+          Random Access = no
+          AutomaticMount = no
+          RemovableMedia = no
+          MaximumOpenWait = 60
+          AlwaysOpen = no
+        '';
+      };
+    };
+  };
+
+in {
+  options = {
+    services.bacula-fd = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the Bacula File Daemon.
+        '';
+      };
+
+      name = mkOption {
+        default = "${config.networking.hostName}-fd";
+        defaultText = literalExpression ''"''${config.networking.hostName}-fd"'';
+        type = types.str;
+        description = ''
+          The client name that must be used by the Director when connecting.
+          Generally, it is a good idea to use a name related to the machine so
+          that error messages can be easily identified if you have multiple
+          Clients. This directive is required.
+        '';
+      };
+
+      port = mkOption {
+        default = 9102;
+        type = types.int;
+        description = ''
+          This specifies the port number on which the Client listens for
+          Director connections. It must agree with the FDPort specified in
+          the Client resource of the Director's configuration file.
+        '';
+      };
+
+      director = mkOption {
+        default = {};
+        description = ''
+          This option defines director resources in Bacula File Daemon.
+        '';
+        type = with types; attrsOf (submodule directorOptions);
+      };
+
+      extraClientConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra configuration to be passed in Client directive.
+        '';
+        example = ''
+          Maximum Concurrent Jobs = 20;
+          Heartbeat Interval = 30;
+        '';
+      };
+
+      extraMessagesConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra configuration to be passed in Messages directive.
+        '';
+        example = ''
+          console = all
+        '';
+      };
+    };
+
+    services.bacula-sd = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Bacula Storage Daemon.
+        '';
+      };
+
+      name = mkOption {
+        default = "${config.networking.hostName}-sd";
+        defaultText = literalExpression ''"''${config.networking.hostName}-sd"'';
+        type = types.str;
+        description = ''
+          Specifies the Name of the Storage daemon.
+        '';
+      };
+
+      port = mkOption {
+        default = 9103;
+        type = types.int;
+        description = ''
+          Specifies port number on which the Storage daemon listens for
+          Director connections.
+        '';
+      };
+
+      director = mkOption {
+        default = {};
+        description = ''
+          This option defines Director resources in Bacula Storage Daemon.
+        '';
+        type = with types; attrsOf (submodule directorOptions);
+      };
+
+      device = mkOption {
+        default = {};
+        description = ''
+          This option defines Device resources in Bacula Storage Daemon.
+        '';
+        type = with types; attrsOf (submodule deviceOptions);
+      };
+
+      autochanger = mkOption {
+        default = {};
+        description = ''
+          This option defines Autochanger resources in Bacula Storage Daemon.
+        '';
+        type = with types; attrsOf (submodule autochangerOptions);
+      };
+
+      extraStorageConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra configuration to be passed in Storage directive.
+        '';
+        example = ''
+          Maximum Concurrent Jobs = 20;
+          Heartbeat Interval = 30;
+        '';
+      };
+
+      extraMessagesConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra configuration to be passed in Messages directive.
+        '';
+        example = ''
+          console = all
+        '';
+      };
+
+    };
+
+    services.bacula-dir = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Bacula Director Daemon.
+        '';
+      };
+
+      name = mkOption {
+        default = "${config.networking.hostName}-dir";
+        defaultText = literalExpression ''"''${config.networking.hostName}-dir"'';
+        type = types.str;
+        description = ''
+          The director name used by the system administrator. This directive is
+          required.
+        '';
+      };
+
+      port = mkOption {
+        default = 9101;
+        type = types.int;
+        description = ''
+          Specify the port (a positive integer) on which the Director daemon
+          will listen for Bacula Console connections. This same port number
+          must be specified in the Director resource of the Console
+          configuration file. The default is 9101, so normally this directive
+          need not be specified. This directive should not be used if you
+          specify DirAddresses (N.B plural) directive.
+        '';
+      };
+
+      password = mkOption {
+        # TODO: required?
+        type = types.str;
+        description = ''
+           Specifies the password that must be supplied for a Director.
+        '';
+      };
+
+      extraMessagesConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra configuration to be passed in Messages directive.
+        '';
+        example = ''
+          console = all
+        '';
+      };
+
+      extraDirectorConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra configuration to be passed in Director directive.
+        '';
+        example = ''
+          Maximum Concurrent Jobs = 20;
+          Heartbeat Interval = 30;
+        '';
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra configuration for Bacula Director Daemon.
+        '';
+        example = ''
+          TODO
+        '';
+      };
+    };
+  };
+
+  config = mkIf (fd_cfg.enable || sd_cfg.enable || dir_cfg.enable) {
+    systemd.services.bacula-fd = mkIf fd_cfg.enable {
+      after = [ "network.target" ];
+      description = "Bacula File Daemon";
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.bacula ];
+      serviceConfig = {
+        ExecStart = "${pkgs.bacula}/sbin/bacula-fd -f -u root -g bacula -c ${fd_conf}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        LogsDirectory = "bacula";
+        StateDirectory = "bacula";
+      };
+    };
+
+    systemd.services.bacula-sd = mkIf sd_cfg.enable {
+      after = [ "network.target" ];
+      description = "Bacula Storage Daemon";
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.bacula ];
+      serviceConfig = {
+        ExecStart = "${pkgs.bacula}/sbin/bacula-sd -f -u bacula -g bacula -c ${sd_conf}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        LogsDirectory = "bacula";
+        StateDirectory = "bacula";
+      };
+    };
+
+    services.postgresql.enable = dir_cfg.enable == true;
+
+    systemd.services.bacula-dir = mkIf dir_cfg.enable {
+      after = [ "network.target" "postgresql.service" ];
+      description = "Bacula Director Daemon";
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.bacula ];
+      serviceConfig = {
+        ExecStart = "${pkgs.bacula}/sbin/bacula-dir -f -u bacula -g bacula -c ${dir_conf}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        LogsDirectory = "bacula";
+        StateDirectory = "bacula";
+      };
+      preStart = ''
+        if ! test -e "${libDir}/db-created"; then
+            ${pkgs.postgresql}/bin/createuser --no-superuser --no-createdb --no-createrole bacula
+            #${pkgs.postgresql}/bin/createdb --owner bacula bacula
+
+            # populate DB
+            ${pkgs.bacula}/etc/create_bacula_database postgresql
+            ${pkgs.bacula}/etc/make_bacula_tables postgresql
+            ${pkgs.bacula}/etc/grant_bacula_privileges postgresql
+            touch "${libDir}/db-created"
+        else
+            ${pkgs.bacula}/etc/update_bacula_tables postgresql || true
+        fi
+      '';
+    };
+
+    environment.systemPackages = [ pkgs.bacula ];
+
+    users.users.bacula = {
+      group = "bacula";
+      uid = config.ids.uids.bacula;
+      home = "${libDir}";
+      createHome = true;
+      description = "Bacula Daemons user";
+      shell = "${pkgs.bash}/bin/bash";
+    };
+
+    users.groups.bacula.gid = config.ids.gids.bacula;
+  };
+}
diff --git a/nixos/modules/services/backup/borgbackup.nix b/nixos/modules/services/backup/borgbackup.nix
new file mode 100644
index 00000000000..4c9ddfe4674
--- /dev/null
+++ b/nixos/modules/services/backup/borgbackup.nix
@@ -0,0 +1,730 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  isLocalPath = x:
+    builtins.substring 0 1 x == "/"      # absolute path
+    || builtins.substring 0 1 x == "."   # relative path
+    || builtins.match "[.*:.*]" == null; # not machine:path
+
+  mkExcludeFile = cfg:
+    # Write each exclude pattern to a new line
+    pkgs.writeText "excludefile" (concatStringsSep "\n" cfg.exclude);
+
+  mkKeepArgs = cfg:
+    # If cfg.prune.keep e.g. has a yearly attribute,
+    # its content is passed on as --keep-yearly
+    concatStringsSep " "
+      (mapAttrsToList (x: y: "--keep-${x}=${toString y}") cfg.prune.keep);
+
+  mkBackupScript = cfg: ''
+    on_exit()
+    {
+      exitStatus=$?
+      # Reset the EXIT handler, or else we're called again on 'exit' below
+      trap - EXIT
+      ${cfg.postHook}
+      exit $exitStatus
+    }
+    trap 'on_exit' INT TERM QUIT EXIT
+
+    archiveName="${if cfg.archiveBaseName == null then "" else cfg.archiveBaseName + "-"}$(date ${cfg.dateFormat})"
+    archiveSuffix="${optionalString cfg.appendFailedSuffix ".failed"}"
+    ${cfg.preHook}
+  '' + optionalString cfg.doInit ''
+    # Run borg init if the repo doesn't exist yet
+    if ! borg list $extraArgs > /dev/null; then
+      borg init $extraArgs \
+        --encryption ${cfg.encryption.mode} \
+        $extraInitArgs
+      ${cfg.postInit}
+    fi
+  '' + ''
+    (
+      set -o pipefail
+      ${optionalString (cfg.dumpCommand != null) ''${escapeShellArg cfg.dumpCommand} | \''}
+      borg create $extraArgs \
+        --compression ${cfg.compression} \
+        --exclude-from ${mkExcludeFile cfg} \
+        $extraCreateArgs \
+        "::$archiveName$archiveSuffix" \
+        ${if cfg.paths == null then "-" else escapeShellArgs cfg.paths}
+    )
+  '' + optionalString cfg.appendFailedSuffix ''
+    borg rename $extraArgs \
+      "::$archiveName$archiveSuffix" "$archiveName"
+  '' + ''
+    ${cfg.postCreate}
+  '' + optionalString (cfg.prune.keep != { }) ''
+    borg prune $extraArgs \
+      ${mkKeepArgs cfg} \
+      ${optionalString (cfg.prune.prefix != null) "--prefix ${escapeShellArg cfg.prune.prefix} \\"}
+      $extraPruneArgs
+    ${cfg.postPrune}
+  '';
+
+  mkPassEnv = cfg: with cfg.encryption;
+    if passCommand != null then
+      { BORG_PASSCOMMAND = passCommand; }
+    else if passphrase != null then
+      { BORG_PASSPHRASE = passphrase; }
+    else { };
+
+  mkBackupService = name: cfg:
+    let
+      userHome = config.users.users.${cfg.user}.home;
+    in nameValuePair "borgbackup-job-${name}" {
+      description = "BorgBackup job ${name}";
+      path = with pkgs; [
+        borgbackup openssh
+      ];
+      script = mkBackupScript cfg;
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        # Only run when no other process is using CPU or disk
+        CPUSchedulingPolicy = "idle";
+        IOSchedulingClass = "idle";
+        ProtectSystem = "strict";
+        ReadWritePaths =
+          [ "${userHome}/.config/borg" "${userHome}/.cache/borg" ]
+          ++ cfg.readWritePaths
+          # Borg needs write access to repo if it is not remote
+          ++ optional (isLocalPath cfg.repo) cfg.repo;
+        PrivateTmp = cfg.privateTmp;
+      };
+      environment = {
+        BORG_REPO = cfg.repo;
+        inherit (cfg) extraArgs extraInitArgs extraCreateArgs extraPruneArgs;
+      } // (mkPassEnv cfg) // cfg.environment;
+    };
+
+  mkBackupTimers = name: cfg:
+    nameValuePair "borgbackup-job-${name}" {
+      description = "BorgBackup job ${name} timer";
+      wantedBy = [ "timers.target" ];
+      timerConfig = {
+        Persistent = cfg.persistentTimer;
+        OnCalendar = cfg.startAt;
+      };
+      # if remote-backup wait for network
+      after = optional (cfg.persistentTimer && !isLocalPath cfg.repo) "network-online.target";
+    };
+
+  # utility function around makeWrapper
+  mkWrapperDrv = {
+      original, name, set ? {}
+    }:
+    pkgs.runCommand "${name}-wrapper" {
+      buildInputs = [ pkgs.makeWrapper ];
+    } (with lib; ''
+      makeWrapper "${original}" "$out/bin/${name}" \
+        ${concatStringsSep " \\\n " (mapAttrsToList (name: value: ''--set ${name} "${value}"'') set)}
+    '');
+
+  mkBorgWrapper = name: cfg: mkWrapperDrv {
+    original = "${pkgs.borgbackup}/bin/borg";
+    name = "borg-job-${name}";
+    set = { BORG_REPO = cfg.repo; } // (mkPassEnv cfg) // cfg.environment;
+  };
+
+  # Paths listed in ReadWritePaths must exist before service is started
+  mkActivationScript = name: cfg:
+    let
+      install = "install -o ${cfg.user} -g ${cfg.group}";
+    in
+      nameValuePair "borgbackup-job-${name}" (stringAfter [ "users" ] (''
+        # 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
+      '' + optionalString (isLocalPath cfg.repo && !cfg.removableDevice) ''
+        ${install} -d ${escapeShellArg cfg.repo}
+      ''));
+
+  mkPassAssertion = name: cfg: {
+    assertion = with cfg.encryption;
+      mode != "none" -> passCommand != null || passphrase != null;
+    message =
+      "passCommand or passphrase has to be specified because"
+      + '' borgbackup.jobs.${name}.encryption != "none"'';
+  };
+
+  mkRepoService = name: cfg:
+    nameValuePair "borgbackup-repo-${name}" {
+      description = "Create BorgBackup repository ${name} directory";
+      script = ''
+        mkdir -p ${escapeShellArg cfg.path}
+        chown ${cfg.user}:${cfg.group} ${escapeShellArg cfg.path}
+      '';
+      serviceConfig = {
+        # The service's only task is to ensure that the specified path exists
+        Type = "oneshot";
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+
+  mkAuthorizedKey = cfg: appendOnly: key:
+    let
+      # Because of the following line, clients do not need to specify an absolute repo path
+      cdCommand = "cd ${escapeShellArg cfg.path}";
+      restrictedArg = "--restrict-to-${if cfg.allowSubRepos then "path" else "repository"} .";
+      appendOnlyArg = optionalString appendOnly "--append-only";
+      quotaArg = optionalString (cfg.quota != null) "--storage-quota ${cfg.quota}";
+      serveCommand = "borg serve ${restrictedArg} ${appendOnlyArg} ${quotaArg}";
+    in
+      ''command="${cdCommand} && ${serveCommand}",restrict ${key}'';
+
+  mkUsersConfig = name: cfg: {
+    users.${cfg.user} = {
+      openssh.authorizedKeys.keys =
+        (map (mkAuthorizedKey cfg false) cfg.authorizedKeys
+        ++ map (mkAuthorizedKey cfg true) cfg.authorizedKeysAppendOnly);
+      useDefaultShell = true;
+      group = cfg.group;
+      isSystemUser = true;
+    };
+    groups.${cfg.group} = { };
+  };
+
+  mkKeysAssertion = name: cfg: {
+    assertion = cfg.authorizedKeys != [ ] || cfg.authorizedKeysAppendOnly != [ ];
+    message =
+      "borgbackup.repos.${name} does not make sense"
+      + " without at least one public key";
+  };
+
+  mkSourceAssertions = name: cfg: {
+    assertion = count isNull [ cfg.dumpCommand cfg.paths ] == 1;
+    message = ''
+      Exactly one of borgbackup.jobs.${name}.paths or borgbackup.jobs.${name}.dumpCommand
+      must be set.
+    '';
+  };
+
+  mkRemovableDeviceAssertions = name: cfg: {
+    assertion = !(isLocalPath cfg.repo) -> !cfg.removableDevice;
+    message = ''
+      borgbackup.repos.${name}: repo isn't a local path, thus it can't be a removable device!
+    '';
+  };
+
+in {
+  meta.maintainers = with maintainers; [ dotlambda ];
+  meta.doc = ./borgbackup.xml;
+
+  ###### interface
+
+  options.services.borgbackup.jobs = mkOption {
+    description = ''
+      Deduplicating backups using BorgBackup.
+      Adding a job will cause a borg-job-NAME wrapper to be added
+      to your system path, so that you can perform maintenance easily.
+      See also the chapter about BorgBackup in the NixOS manual.
+    '';
+    default = { };
+    example = literalExpression ''
+      { # for a local backup
+        rootBackup = {
+          paths = "/";
+          exclude = [ "/nix" ];
+          repo = "/path/to/local/repo";
+          encryption = {
+            mode = "repokey";
+            passphrase = "secret";
+          };
+          compression = "auto,lzma";
+          startAt = "weekly";
+        };
+      }
+      { # Root backing each day up to a remote backup server. We assume that you have
+        #   * created a password less key: ssh-keygen -N "" -t ed25519 -f /path/to/ssh_key
+        #     best practices are: use -t ed25519, /path/to = /run/keys
+        #   * the passphrase is in the file /run/keys/borgbackup_passphrase
+        #   * you have initialized the repository manually
+        paths = [ "/etc" "/home" ];
+        exclude = [ "/nix" "'**/.cache'" ];
+        doInit = false;
+        repo =  "user3@arep.repo.borgbase.com:repo";
+        encryption = {
+          mode = "repokey-blake2";
+          passCommand = "cat /path/to/passphrase";
+        };
+        environment = { BORG_RSH = "ssh -i /path/to/ssh_key"; };
+        compression = "auto,lzma";
+        startAt = "daily";
+    };
+    '';
+    type = types.attrsOf (types.submodule (let globalConfig = config; in
+      { name, config, ... }: {
+        options = {
+
+          paths = mkOption {
+            type = with types; nullOr (coercedTo str lib.singleton (listOf str));
+            default = null;
+            description = ''
+              Path(s) to back up.
+              Mutually exclusive with <option>dumpCommand</option>.
+            '';
+            example = "/home/user";
+          };
+
+          dumpCommand = mkOption {
+            type = with types; nullOr path;
+            default = null;
+            description = ''
+              Backup the stdout of this program instead of filesystem paths.
+              Mutually exclusive with <option>paths</option>.
+            '';
+            example = "/path/to/createZFSsend.sh";
+          };
+
+          repo = mkOption {
+            type = types.str;
+            description = "Remote or local repository to back up to.";
+            example = "user@machine:/path/to/repo";
+          };
+
+          removableDevice = mkOption {
+            type = types.bool;
+            default = false;
+            description = "Whether the repo (which must be local) is a removable device.";
+          };
+
+          archiveBaseName = mkOption {
+            type = types.nullOr (types.strMatching "[^/{}]+");
+            default = "${globalConfig.networking.hostName}-${name}";
+            defaultText = literalExpression ''"''${config.networking.hostName}-<name>"'';
+            description = ''
+              How to name the created archives. A timestamp, whose format is
+              determined by <option>dateFormat</option>, will be appended. The full
+              name can be modified at runtime (<literal>$archiveName</literal>).
+              Placeholders like <literal>{hostname}</literal> must not be used.
+              Use <literal>null</literal> for no base name.
+            '';
+          };
+
+          dateFormat = mkOption {
+            type = types.str;
+            description = ''
+              Arguments passed to <command>date</command>
+              to create a timestamp suffix for the archive name.
+            '';
+            default = "+%Y-%m-%dT%H:%M:%S";
+            example = "-u +%s";
+          };
+
+          startAt = mkOption {
+            type = with types; either str (listOf str);
+            default = "daily";
+            description = ''
+              When or how often the backup should run.
+              Must be in the format described in
+              <citerefentry><refentrytitle>systemd.time</refentrytitle>
+              <manvolnum>7</manvolnum></citerefentry>.
+              If you do not want the backup to start
+              automatically, use <literal>[ ]</literal>.
+              It will generate a systemd service borgbackup-job-NAME.
+              You may trigger it manually via systemctl restart borgbackup-job-NAME.
+            '';
+          };
+
+          persistentTimer = mkOption {
+            default = false;
+            type = types.bool;
+            example = true;
+            description = ''
+              Set the <literal>persistentTimer</literal> option for the
+              <citerefentry><refentrytitle>systemd.timer</refentrytitle>
+              <manvolnum>5</manvolnum></citerefentry>
+              which triggers the backup immediately if the last trigger
+              was missed (e.g. if the system was powered down).
+            '';
+          };
+
+          user = mkOption {
+            type = types.str;
+            description = ''
+              The user <command>borg</command> is run as.
+              User or group need read permission
+              for the specified <option>paths</option>.
+            '';
+            default = "root";
+          };
+
+          group = mkOption {
+            type = types.str;
+            description = ''
+              The group borg is run as. User or group needs read permission
+              for the specified <option>paths</option>.
+            '';
+            default = "root";
+          };
+
+          encryption.mode = mkOption {
+            type = types.enum [
+              "repokey" "keyfile"
+              "repokey-blake2" "keyfile-blake2"
+              "authenticated" "authenticated-blake2"
+              "none"
+            ];
+            description = ''
+              Encryption mode to use. Setting a mode
+              other than <literal>"none"</literal> requires
+              you to specify a <option>passCommand</option>
+              or a <option>passphrase</option>.
+            '';
+            example = "repokey-blake2";
+          };
+
+          encryption.passCommand = mkOption {
+            type = with types; nullOr str;
+            description = ''
+              A command which prints the passphrase to stdout.
+              Mutually exclusive with <option>passphrase</option>.
+            '';
+            default = null;
+            example = "cat /path/to/passphrase_file";
+          };
+
+          encryption.passphrase = mkOption {
+            type = with types; nullOr str;
+            description = ''
+              The passphrase the backups are encrypted with.
+              Mutually exclusive with <option>passCommand</option>.
+              If you do not want the passphrase to be stored in the
+              world-readable Nix store, use <option>passCommand</option>.
+            '';
+            default = null;
+          };
+
+          compression = mkOption {
+            # "auto" is optional,
+            # compression mode must be given,
+            # compression level is optional
+            type = types.strMatching "none|(auto,)?(lz4|zstd|zlib|lzma)(,[[:digit:]]{1,2})?";
+            description = ''
+              Compression method to use. Refer to
+              <command>borg help compression</command>
+              for all available options.
+            '';
+            default = "lz4";
+            example = "auto,lzma";
+          };
+
+          exclude = mkOption {
+            type = with types; listOf str;
+            description = ''
+              Exclude paths matching any of the given patterns. See
+              <command>borg help patterns</command> for pattern syntax.
+            '';
+            default = [ ];
+            example = [
+              "/home/*/.cache"
+              "/nix"
+            ];
+          };
+
+          readWritePaths = mkOption {
+            type = with types; listOf path;
+            description = ''
+              By default, borg cannot write anywhere on the system but
+              <literal>$HOME/.config/borg</literal> and <literal>$HOME/.cache/borg</literal>.
+              If, for example, your preHook script needs to dump files
+              somewhere, put those directories here.
+            '';
+            default = [ ];
+            example = [
+              "/var/backup/mysqldump"
+            ];
+          };
+
+          privateTmp = mkOption {
+            type = types.bool;
+            description = ''
+              Set the <literal>PrivateTmp</literal> option for
+              the systemd-service. Set to false if you need sockets
+              or other files from global /tmp.
+            '';
+            default = true;
+          };
+
+          doInit = mkOption {
+            type = types.bool;
+            description = ''
+              Run <command>borg init</command> if the
+              specified <option>repo</option> does not exist.
+              You should set this to <literal>false</literal>
+              if the repository is located on an external drive
+              that might not always be mounted.
+            '';
+            default = true;
+          };
+
+          appendFailedSuffix = mkOption {
+            type = types.bool;
+            description = ''
+              Append a <literal>.failed</literal> suffix
+              to the archive name, which is only removed if
+              <command>borg create</command> has a zero exit status.
+            '';
+            default = true;
+          };
+
+          prune.keep = mkOption {
+            # Specifying e.g. `prune.keep.yearly = -1`
+            # means there is no limit of yearly archives to keep
+            # The regex is for use with e.g. --keep-within 1y
+            type = with types; attrsOf (either int (strMatching "[[:digit:]]+[Hdwmy]"));
+            description = ''
+              Prune a repository by deleting all archives not matching any of the
+              specified retention options. See <command>borg help prune</command>
+              for the available options.
+            '';
+            default = { };
+            example = literalExpression ''
+              {
+                within = "1d"; # Keep all archives from the last day
+                daily = 7;
+                weekly = 4;
+                monthly = -1;  # Keep at least one archive for each month
+              }
+            '';
+          };
+
+          prune.prefix = mkOption {
+            type = types.nullOr (types.str);
+            description = ''
+              Only consider archive names starting with this prefix for pruning.
+              By default, only archives created by this job are considered.
+              Use <literal>""</literal> or <literal>null</literal> to consider all archives.
+            '';
+            default = config.archiveBaseName;
+            defaultText = literalExpression "archiveBaseName";
+          };
+
+          environment = mkOption {
+            type = with types; attrsOf str;
+            description = ''
+              Environment variables passed to the backup script.
+              You can for example specify which SSH key to use.
+            '';
+            default = { };
+            example = { BORG_RSH = "ssh -i /path/to/key"; };
+          };
+
+          preHook = mkOption {
+            type = types.lines;
+            description = ''
+              Shell commands to run before the backup.
+              This can for example be used to mount file systems.
+            '';
+            default = "";
+            example = ''
+              # To add excluded paths at runtime
+              extraCreateArgs="$extraCreateArgs --exclude /some/path"
+            '';
+          };
+
+          postInit = mkOption {
+            type = types.lines;
+            description = ''
+              Shell commands to run after <command>borg init</command>.
+            '';
+            default = "";
+          };
+
+          postCreate = mkOption {
+            type = types.lines;
+            description = ''
+              Shell commands to run after <command>borg create</command>. The name
+              of the created archive is stored in <literal>$archiveName</literal>.
+            '';
+            default = "";
+          };
+
+          postPrune = mkOption {
+            type = types.lines;
+            description = ''
+              Shell commands to run after <command>borg prune</command>.
+            '';
+            default = "";
+          };
+
+          postHook = mkOption {
+            type = types.lines;
+            description = ''
+              Shell commands to run just before exit. They are executed
+              even if a previous command exits with a non-zero exit code.
+              The latter is available as <literal>$exitStatus</literal>.
+            '';
+            default = "";
+          };
+
+          extraArgs = mkOption {
+            type = types.str;
+            description = ''
+              Additional arguments for all <command>borg</command> calls the
+              service has. Handle with care.
+            '';
+            default = "";
+            example = "--remote-path=/path/to/borg";
+          };
+
+          extraInitArgs = mkOption {
+            type = types.str;
+            description = ''
+              Additional arguments for <command>borg init</command>.
+              Can also be set at runtime using <literal>$extraInitArgs</literal>.
+            '';
+            default = "";
+            example = "--append-only";
+          };
+
+          extraCreateArgs = mkOption {
+            type = types.str;
+            description = ''
+              Additional arguments for <command>borg create</command>.
+              Can also be set at runtime using <literal>$extraCreateArgs</literal>.
+            '';
+            default = "";
+            example = "--stats --checkpoint-interval 600";
+          };
+
+          extraPruneArgs = mkOption {
+            type = types.str;
+            description = ''
+              Additional arguments for <command>borg prune</command>.
+              Can also be set at runtime using <literal>$extraPruneArgs</literal>.
+            '';
+            default = "";
+            example = "--save-space";
+          };
+
+        };
+      }
+    ));
+  };
+
+  options.services.borgbackup.repos = mkOption {
+    description = ''
+      Serve BorgBackup repositories to given public SSH keys,
+      restricting their access to the repository only.
+      See also the chapter about BorgBackup in the NixOS manual.
+      Also, clients do not need to specify the absolute path when accessing the repository,
+      i.e. <literal>user@machine:.</literal> is enough. (Note colon and dot.)
+    '';
+    default = { };
+    type = types.attrsOf (types.submodule (
+      { ... }: {
+        options = {
+          path = mkOption {
+            type = types.path;
+            description = ''
+              Where to store the backups. Note that the directory
+              is created automatically, with correct permissions.
+            '';
+            default = "/var/lib/borgbackup";
+          };
+
+          user = mkOption {
+            type = types.str;
+            description = ''
+              The user <command>borg serve</command> is run as.
+              User or group needs write permission
+              for the specified <option>path</option>.
+            '';
+            default = "borg";
+          };
+
+          group = mkOption {
+            type = types.str;
+            description = ''
+              The group <command>borg serve</command> is run as.
+              User or group needs write permission
+              for the specified <option>path</option>.
+            '';
+            default = "borg";
+          };
+
+          authorizedKeys = mkOption {
+            type = with types; listOf str;
+            description = ''
+              Public SSH keys that are given full write access to this repository.
+              You should use a different SSH key for each repository you write to, because
+              the specified keys are restricted to running <command>borg serve</command>
+              and can only access this single repository.
+            '';
+            default = [ ];
+          };
+
+          authorizedKeysAppendOnly = mkOption {
+            type = with types; listOf str;
+            description = ''
+              Public SSH keys that can only be used to append new data (archives) to the repository.
+              Note that archives can still be marked as deleted and are subsequently removed from disk
+              upon accessing the repo with full write access, e.g. when pruning.
+            '';
+            default = [ ];
+          };
+
+          allowSubRepos = mkOption {
+            type = types.bool;
+            description = ''
+              Allow clients to create repositories in subdirectories of the
+              specified <option>path</option>. These can be accessed using
+              <literal>user@machine:path/to/subrepo</literal>. Note that a
+              <option>quota</option> applies to repositories independently.
+              Therefore, if this is enabled, clients can create multiple
+              repositories and upload an arbitrary amount of data.
+            '';
+            default = false;
+          };
+
+          quota = mkOption {
+            # See the definition of parse_file_size() in src/borg/helpers/parseformat.py
+            type = with types; nullOr (strMatching "[[:digit:].]+[KMGTP]?");
+            description = ''
+              Storage quota for the repository. This quota is ensured for all
+              sub-repositories if <option>allowSubRepos</option> is enabled
+              but not for the overall storage space used.
+            '';
+            default = null;
+            example = "100G";
+          };
+
+        };
+      }
+    ));
+  };
+
+  ###### implementation
+
+  config = mkIf (with config.services.borgbackup; jobs != { } || repos != { })
+    (with config.services.borgbackup; {
+      assertions =
+        mapAttrsToList mkPassAssertion jobs
+        ++ mapAttrsToList mkKeysAssertion repos
+        ++ mapAttrsToList mkSourceAssertions jobs
+        ++ mapAttrsToList mkRemovableDeviceAssertions jobs;
+
+      system.activationScripts = mapAttrs' mkActivationScript jobs;
+
+      systemd.services =
+        # A job named "foo" is mapped to systemd.services.borgbackup-job-foo
+        mapAttrs' mkBackupService jobs
+        # A repo named "foo" is mapped to systemd.services.borgbackup-repo-foo
+        // mapAttrs' mkRepoService repos;
+
+      # A job named "foo" is mapped to systemd.timers.borgbackup-job-foo
+      # only generate the timer if interval (startAt) is set
+      systemd.timers = mapAttrs' mkBackupTimers (filterAttrs (_: cfg: cfg.startAt != []) jobs);
+
+      users = mkMerge (mapAttrsToList mkUsersConfig repos);
+
+      environment.systemPackages = with pkgs; [ borgbackup ] ++ (mapAttrsToList mkBorgWrapper jobs);
+    });
+}
diff --git a/nixos/modules/services/backup/borgbackup.xml b/nixos/modules/services/backup/borgbackup.xml
new file mode 100644
index 00000000000..8f623c93656
--- /dev/null
+++ b/nixos/modules/services/backup/borgbackup.xml
@@ -0,0 +1,209 @@
+<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";
+        };
+        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
new file mode 100644
index 00000000000..5e5c0bbeccc
--- /dev/null
+++ b/nixos/modules/services/backup/borgmatic.nix
@@ -0,0 +1,57 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.borgmatic;
+  cfgfile = pkgs.writeText "config.yaml" (builtins.toJSON cfg.settings);
+in {
+  options.services.borgmatic = {
+    enable = mkEnableOption "borgmatic";
+
+    settings = mkOption {
+      description = ''
+        See https://torsion.org/borgmatic/docs/reference/configuration/
+      '';
+      type = types.submodule {
+        freeformType = with lib.types; attrsOf anything;
+        options.location = {
+          source_directories = mkOption {
+            type = types.listOf types.str;
+            description = ''
+              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 = ''
+              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}"
+            ];
+          };
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.borgmatic ];
+
+    environment.etc."borgmatic/config.yaml".source = cfgfile;
+
+    systemd.packages = [ pkgs.borgmatic ];
+
+  };
+}
diff --git a/nixos/modules/services/backup/btrbk.nix b/nixos/modules/services/backup/btrbk.nix
new file mode 100644
index 00000000000..0c00b934405
--- /dev/null
+++ b/nixos/modules/services/backup/btrbk.nix
@@ -0,0 +1,220 @@
+{ config, pkgs, lib, ... }:
+let
+  cfg = config.services.btrbk;
+  sshEnabled = cfg.sshAccess != [ ];
+  serviceEnabled = cfg.instances != { };
+  attr2Lines = attr:
+    let
+      pairs = lib.attrsets.mapAttrsToList (name: value: { inherit name value; }) attr;
+      isSubsection = value:
+        if builtins.isAttrs value then true
+        else if builtins.isString value then false
+        else throw "invalid type in btrbk config ${builtins.typeOf value}";
+      sortedPairs = lib.lists.partition (x: isSubsection x.value) pairs;
+    in
+    lib.flatten (
+      # non subsections go first
+      (
+        map (pair: [ "${pair.name} ${pair.value}" ]) sortedPairs.wrong
+      )
+      ++ # subsections go last
+      (
+        map
+          (
+            pair:
+            lib.mapAttrsToList
+              (
+                childname: value:
+                  [ "${pair.name} ${childname}" ] ++ (map (x: " " + x) (attr2Lines value))
+              )
+              pair.value
+          )
+          sortedPairs.right
+      )
+    )
+  ;
+  addDefaults = settings: { backend = "btrfs-progs-sudo"; } // settings;
+  mkConfigFile = settings: lib.concatStringsSep "\n" (attr2Lines (addDefaults settings));
+  mkTestedConfigFile = name: settings:
+    let
+      configFile = pkgs.writeText "btrbk-${name}.conf" (mkConfigFile settings);
+    in
+    pkgs.runCommand "btrbk-${name}-tested.conf" { } ''
+      mkdir foo
+      cp ${configFile} $out
+      if (set +o pipefail; ${pkgs.btrbk}/bin/btrbk -c $out ls foo 2>&1 | grep $out);
+      then
+      echo btrbk configuration is invalid
+      cat $out
+      exit 1
+      fi;
+    '';
+in
+{
+  options = {
+    services.btrbk = {
+      extraPackages = lib.mkOption {
+        description = "Extra packages for btrbk, like compression utilities for <literal>stream_compress</literal>";
+        type = lib.types.listOf lib.types.package;
+        default = [ ];
+        example = lib.literalExpression "[ pkgs.xz ]";
+      };
+      niceness = lib.mkOption {
+        description = "Niceness for local instances of btrbk. Also applies to remote ones connecting via ssh when positive.";
+        type = lib.types.ints.between (-20) 19;
+        default = 10;
+      };
+      ioSchedulingClass = lib.mkOption {
+        description = "IO scheduling class for btrbk (see ionice(1) for a quick description). Applies to local instances, and remote ones connecting by ssh if set to idle.";
+        type = lib.types.enum [ "idle" "best-effort" "realtime" ];
+        default = "best-effort";
+      };
+      instances = lib.mkOption {
+        description = "Set of btrbk instances. The instance named <literal>btrbk</literal> is the default one.";
+        type = with lib.types;
+          attrsOf (
+            submodule {
+              options = {
+                onCalendar = lib.mkOption {
+                  type = lib.types.str;
+                  default = "daily";
+                  description = "How often this btrbk instance is started. See systemd.time(7) for more information about the format.";
+                };
+                settings = lib.mkOption {
+                  type = let t = lib.types.attrsOf (lib.types.either lib.types.str (t // { description = "instances of this type recursively"; })); in t;
+                  default = { };
+                  example = {
+                    snapshot_preserve_min = "2d";
+                    snapshot_preserve = "14d";
+                    volume = {
+                      "/mnt/btr_pool" = {
+                        target = "/mnt/btr_backup/mylaptop";
+                        subvolume = {
+                          "rootfs" = { };
+                          "home" = { snapshot_create = "always"; };
+                        };
+                      };
+                    };
+                  };
+                  description = "configuration options for btrbk. Nested attrsets translate to subsections.";
+                };
+              };
+            }
+          );
+        default = { };
+      };
+      sshAccess = lib.mkOption {
+        description = "SSH keys that should be able to make or push snapshots on this system remotely with btrbk";
+        type = with lib.types; listOf (
+          submodule {
+            options = {
+              key = lib.mkOption {
+                type = str;
+                description = "SSH public key allowed to login as user <literal>btrbk</literal> to run remote backups.";
+              };
+              roles = lib.mkOption {
+                type = listOf (enum [ "info" "source" "target" "delete" "snapshot" "send" "receive" ]);
+                example = [ "source" "info" "send" ];
+                description = "What actions can be performed with this SSH key. See ssh_filter_btrbk(1) for details";
+              };
+            };
+          }
+        );
+        default = [ ];
+      };
+    };
+
+  };
+  config = lib.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" ]; }
+        ];
+      }
+    ];
+    users.users.btrbk = {
+      isSystemUser = true;
+      # ssh needs a home directory
+      home = "/var/lib/btrbk";
+      createHome = true;
+      shell = "${pkgs.bash}/bin/bash";
+      group = "btrbk";
+      openssh.authorizedKeys.keys = map
+        (
+          v:
+          let
+            options = lib.concatMapStringsSep " " (x: "--" + x) v.roles;
+            ioniceClass = {
+              "idle" = 3;
+              "best-effort" = 2;
+              "realtime" = 1;
+            }.${cfg.ioSchedulingClass};
+          in
+          ''command="${pkgs.util-linux}/bin/ionice -t -c ${toString ioniceClass} ${lib.optionalString (cfg.niceness >= 1) "${pkgs.coreutils}/bin/nice -n ${toString cfg.niceness}"} ${pkgs.btrbk}/share/btrbk/scripts/ssh_filter_btrbk.sh --sudo ${options}" ${v.key}''
+        )
+        cfg.sshAccess;
+    };
+    users.groups.btrbk = { };
+    systemd.tmpfiles.rules = [
+      "d /var/lib/btrbk 0750 btrbk btrbk"
+      "d /var/lib/btrbk/.ssh 0700 btrbk btrbk"
+      "f /var/lib/btrbk/.ssh/config 0700 btrbk btrbk - StrictHostKeyChecking=accept-new"
+    ];
+    environment.etc = lib.mapAttrs'
+      (
+        name: instance: {
+          name = "btrbk/${name}.conf";
+          value.source = mkTestedConfigFile name instance.settings;
+        }
+      )
+      cfg.instances;
+    systemd.services = lib.mapAttrs'
+      (
+        name: _: {
+          name = "btrbk-${name}";
+          value = {
+            description = "Takes BTRFS snapshots and maintains retention policies.";
+            unitConfig.Documentation = "man:btrbk(1)";
+            path = [ "/run/wrappers" ] ++ cfg.extraPackages;
+            serviceConfig = {
+              User = "btrbk";
+              Group = "btrbk";
+              Type = "oneshot";
+              ExecStart = "${pkgs.btrbk}/bin/btrbk -c /etc/btrbk/${name}.conf run";
+              Nice = cfg.niceness;
+              IOSchedulingClass = cfg.ioSchedulingClass;
+              StateDirectory = "btrbk";
+            };
+          };
+        }
+      )
+      cfg.instances;
+
+    systemd.timers = lib.mapAttrs'
+      (
+        name: instance: {
+          name = "btrbk-${name}";
+          value = {
+            description = "Timer to take BTRFS snapshots and maintain retention policies.";
+            wantedBy = [ "timers.target" ];
+            timerConfig = {
+              OnCalendar = instance.onCalendar;
+              AccuracySec = "10min";
+              Persistent = true;
+            };
+          };
+        }
+      )
+      cfg.instances;
+  };
+
+}
diff --git a/nixos/modules/services/backup/duplicati.nix b/nixos/modules/services/backup/duplicati.nix
new file mode 100644
index 00000000000..97864c44691
--- /dev/null
+++ b/nixos/modules/services/backup/duplicati.nix
@@ -0,0 +1,86 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.duplicati;
+in
+{
+  options = {
+    services.duplicati = {
+      enable = mkEnableOption "Duplicati";
+
+      port = mkOption {
+        default = 8200;
+        type = types.int;
+        description = ''
+          Port serving the web interface
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/duplicati";
+        description = ''
+          The directory where Duplicati stores its data files.
+
+          <note><para>
+            If left as the default value this directory will automatically be created
+            before the Duplicati server starts, otherwise you are responsible for ensuring
+            the directory exists with appropriate ownership and permissions.
+          </para></note>
+        '';
+      };
+
+      interface = mkOption {
+        default = "127.0.0.1";
+        type = types.str;
+        description = ''
+          Listening interface for the web UI
+          Set it to "any" to listen on all available interfaces
+        '';
+      };
+
+      user = mkOption {
+        default = "duplicati";
+        type = types.str;
+        description = ''
+          Duplicati runs as it's own user. It will only be able to backup world-readable files.
+          Run as root with special care.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.duplicati ];
+
+    systemd.services.duplicati = {
+      description = "Duplicati backup";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = mkMerge [
+        {
+          User = cfg.user;
+          Group = "duplicati";
+          ExecStart = "${pkgs.duplicati}/bin/duplicati-server --webservice-interface=${cfg.interface} --webservice-port=${toString cfg.port} --server-datafolder=${cfg.dataDir}";
+          Restart = "on-failure";
+        }
+        (mkIf (cfg.dataDir == "/var/lib/duplicati") {
+          StateDirectory = "duplicati";
+        })
+      ];
+    };
+
+    users.users = lib.optionalAttrs (cfg.user == "duplicati") {
+      duplicati = {
+        uid = config.ids.uids.duplicati;
+        home = cfg.dataDir;
+        group = "duplicati";
+      };
+    };
+    users.groups.duplicati.gid = config.ids.gids.duplicati;
+
+  };
+}
+
diff --git a/nixos/modules/services/backup/duplicity.nix b/nixos/modules/services/backup/duplicity.nix
new file mode 100644
index 00000000000..6949fa8b995
--- /dev/null
+++ b/nixos/modules/services/backup/duplicity.nix
@@ -0,0 +1,196 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.duplicity;
+
+  stateDirectory = "/var/lib/duplicity";
+
+  localTarget =
+    if hasPrefix "file://" cfg.targetUrl
+    then removePrefix "file://" cfg.targetUrl else null;
+
+in
+{
+  options.services.duplicity = {
+    enable = mkEnableOption "backups with duplicity";
+
+    root = mkOption {
+      type = types.path;
+      default = "/";
+      description = ''
+        Root directory to backup.
+      '';
+    };
+
+    include = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = [ "/home" ];
+      description = ''
+        List of paths to include into the backups. See the FILE SELECTION
+        section in <citerefentry><refentrytitle>duplicity</refentrytitle>
+        <manvolnum>1</manvolnum></citerefentry> for details on the syntax.
+      '';
+    };
+
+    exclude = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      description = ''
+        List of paths to exclude from backups. See the FILE SELECTION section in
+        <citerefentry><refentrytitle>duplicity</refentrytitle>
+        <manvolnum>1</manvolnum></citerefentry> for details on the syntax.
+      '';
+    };
+
+    targetUrl = mkOption {
+      type = types.str;
+      example = "s3://host:port/prefix";
+      description = ''
+        Target url to backup to. See the URL FORMAT section in
+        <citerefentry><refentrytitle>duplicity</refentrytitle>
+        <manvolnum>1</manvolnum></citerefentry> for supported urls.
+      '';
+    };
+
+    secretFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Path of a file containing secrets (gpg passphrase, access key...) in
+        the format of EnvironmentFile as described by
+        <citerefentry><refentrytitle>systemd.exec</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry>. For example:
+        <programlisting>
+        PASSPHRASE=<replaceable>...</replaceable>
+        AWS_ACCESS_KEY_ID=<replaceable>...</replaceable>
+        AWS_SECRET_ACCESS_KEY=<replaceable>...</replaceable>
+        </programlisting>
+      '';
+    };
+
+    frequency = mkOption {
+      type = types.nullOr types.str;
+      default = "daily";
+      description = ''
+        Run duplicity with the given frequency (see
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry> for the format).
+        If null, do not run automatically.
+      '';
+    };
+
+    extraFlags = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = [ "--backend-retry-delay" "100" ];
+      description = ''
+        Extra command-line flags passed to duplicity. See
+        <citerefentry><refentrytitle>duplicity</refentrytitle>
+        <manvolnum>1</manvolnum></citerefentry>.
+      '';
+    };
+
+    fullIfOlderThan = mkOption {
+      type = types.str;
+      default = "never";
+      example = "1M";
+      description = ''
+        If <literal>"never"</literal> (the default) always do incremental
+        backups (the first backup will be a full backup, of course).  If
+        <literal>"always"</literal> always do full backups.  Otherwise, this
+        must be a string representing a duration. Full backups will be made
+        when the latest full backup is older than this duration. If this is not
+        the case, an incremental backup is performed.
+      '';
+    };
+
+    cleanup = {
+      maxAge = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "6M";
+        description = ''
+          If non-null, delete all backup sets older than the given time.  Old backup sets
+          will not be deleted if backup sets newer than time depend on them.
+        '';
+      };
+      maxFull = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        example = 2;
+        description = ''
+          If non-null, delete all backups sets that are older than the count:th last full
+          backup (in other words, keep the last count full backups and
+          associated incremental sets).
+        '';
+      };
+      maxIncr = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        example = 1;
+        description = ''
+          If non-null, delete incremental sets of all backups sets that are
+          older than the count:th last full backup (in other words, keep only
+          old full backups and not their increments).
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd = {
+      services.duplicity = {
+        description = "backup files with duplicity";
+
+        environment.HOME = stateDirectory;
+
+        script =
+          let
+            target = escapeShellArg cfg.targetUrl;
+            extra = escapeShellArgs ([ "--archive-dir" stateDirectory ] ++ cfg.extraFlags);
+            dup = "${pkgs.duplicity}/bin/duplicity";
+          in
+          ''
+            set -x
+            ${dup} cleanup ${target} --force ${extra}
+            ${lib.optionalString (cfg.cleanup.maxAge != null) "${dup} remove-older-than ${lib.escapeShellArg cfg.cleanup.maxAge} ${target} --force ${extra}"}
+            ${lib.optionalString (cfg.cleanup.maxFull != null) "${dup} remove-all-but-n-full ${toString cfg.cleanup.maxFull} ${target} --force ${extra}"}
+            ${lib.optionalString (cfg.cleanup.maxIncr != null) "${dup} remove-all-inc-of-but-n-full ${toString cfg.cleanup.maxIncr} ${target} --force ${extra}"}
+            exec ${dup} ${if cfg.fullIfOlderThan == "always" then "full" else "incr"} ${lib.escapeShellArgs (
+              [ cfg.root cfg.targetUrl ]
+              ++ concatMap (p: [ "--include" p ]) cfg.include
+              ++ concatMap (p: [ "--exclude" p ]) cfg.exclude
+              ++ (lib.optionals (cfg.fullIfOlderThan != "never" && cfg.fullIfOlderThan != "always") [ "--full-if-older-than" cfg.fullIfOlderThan ])
+              )} ${extra}
+          '';
+        serviceConfig = {
+          PrivateTmp = true;
+          ProtectSystem = "strict";
+          ProtectHome = "read-only";
+          StateDirectory = baseNameOf stateDirectory;
+        } // optionalAttrs (localTarget != null) {
+          ReadWritePaths = localTarget;
+        } // optionalAttrs (cfg.secretFile != null) {
+          EnvironmentFile = cfg.secretFile;
+        };
+      } // optionalAttrs (cfg.frequency != null) {
+        startAt = cfg.frequency;
+      };
+
+      tmpfiles.rules = optional (localTarget != null) "d ${localTarget} 0700 root root -";
+    };
+
+    assertions = singleton {
+      # Duplicity will fail if the last file selection option is an include. It
+      # is not always possible to detect but this simple case can be caught.
+      assertion = cfg.include != [ ] -> cfg.exclude != [ ] || cfg.extraFlags != [ ];
+      message = ''
+        Duplicity will fail if you only specify included paths ("Because the
+        default is to include all files, the expression is redundant. Exiting
+        because this probably isn't what you meant.")
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/backup/mysql-backup.nix b/nixos/modules/services/backup/mysql-backup.nix
new file mode 100644
index 00000000000..c40a0b5abc4
--- /dev/null
+++ b/nixos/modules/services/backup/mysql-backup.nix
@@ -0,0 +1,130 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inherit (pkgs) mariadb gzip;
+
+  cfg = config.services.mysqlBackup;
+  defaultUser = "mysqlbackup";
+
+  backupScript = ''
+    set -o pipefail
+    failed=""
+    ${concatMapStringsSep "\n" backupDatabaseScript cfg.databases}
+    if [ -n "$failed" ]; then
+      echo "Backup of database(s) failed:$failed"
+      exit 1
+    fi
+  '';
+  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
+      mv $dest.tmp $dest
+      echo "Backed up to $dest"
+    else
+      echo "Failed to back up to $dest"
+      rm -f $dest.tmp
+      failed="$failed ${db}"
+    fi
+  '';
+
+in
+
+{
+  options = {
+
+    services.mysqlBackup = {
+
+      enable = mkEnableOption "MySQL backups";
+
+      calendar = mkOption {
+        type = types.str;
+        default = "01:15:00";
+        description = ''
+          Configured when to run the backup service systemd unit (DayOfWeek Year-Month-Day Hour:Minute:Second).
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = defaultUser;
+        description = ''
+          User to be used to perform backup.
+        '';
+      };
+
+      databases = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        description = ''
+          List of database names to dump.
+        '';
+      };
+
+      location = mkOption {
+        type = types.path;
+        default = "/var/backup/mysql";
+        description = ''
+          Location to put the gzipped MySQL database dumps.
+        '';
+      };
+
+      singleTransaction = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to create database dump in a single transaction
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    users.users = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} = {
+        isSystemUser = true;
+        createHome = false;
+        home = cfg.location;
+        group = "nogroup";
+      };
+    };
+
+    services.mysql.ensureUsers = [{
+      name = cfg.user;
+      ensurePermissions = with lib;
+        let
+          privs = "SELECT, SHOW VIEW, TRIGGER, LOCK TABLES";
+          grant = db: nameValuePair "${db}.*" privs;
+        in
+          listToAttrs (map grant cfg.databases);
+    }];
+
+    systemd = {
+      timers.mysql-backup = {
+        description = "Mysql backup timer";
+        wantedBy = [ "timers.target" ];
+        timerConfig = {
+          OnCalendar = cfg.calendar;
+          AccuracySec = "5m";
+          Unit = "mysql-backup.service";
+        };
+      };
+      services.mysql-backup = {
+        description = "MySQL backup service";
+        enable = true;
+        serviceConfig = {
+          Type = "oneshot";
+          User = cfg.user;
+        };
+        script = backupScript;
+      };
+      tmpfiles.rules = [
+        "d ${cfg.location} 0700 ${cfg.user} - - -"
+      ];
+    };
+  };
+
+}
diff --git a/nixos/modules/services/backup/postgresql-backup.nix b/nixos/modules/services/backup/postgresql-backup.nix
new file mode 100644
index 00000000000..562458eb457
--- /dev/null
+++ b/nixos/modules/services/backup/postgresql-backup.nix
@@ -0,0 +1,164 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.postgresqlBackup;
+
+  postgresqlBackupService = db: dumpCmd:
+    let
+      compressSuffixes = {
+        "none" = "";
+        "gzip" = ".gz";
+        "zstd" = ".zstd";
+      };
+      compressSuffix = getAttr cfg.compression compressSuffixes;
+
+      compressCmd = getAttr cfg.compression {
+        "none" = "cat";
+        "gzip" = "${pkgs.gzip}/bin/gzip -c";
+        "zstd" = "${pkgs.zstd}/bin/zstd -c";
+      };
+
+      mkSqlPath = prefix: suffix: "${cfg.location}/${db}${prefix}.sql${suffix}";
+      curFile = mkSqlPath "" compressSuffix;
+      prevFile = mkSqlPath ".prev" compressSuffix;
+      prevFiles = map (mkSqlPath ".prev") (attrValues compressSuffixes);
+      inProgressFile = mkSqlPath ".in-progress" compressSuffix;
+    in {
+      enable = true;
+
+      description = "Backup of ${db} database(s)";
+
+      requires = [ "postgresql.service" ];
+
+      path = [ pkgs.coreutils config.services.postgresql.package ];
+
+      script = ''
+        set -e -o pipefail
+
+        umask 0077 # ensure backup is only readable by postgres user
+
+        if [ -e ${curFile} ]; then
+          rm -f ${toString prevFiles}
+          mv ${curFile} ${prevFile}
+        fi
+
+        ${dumpCmd} \
+          | ${compressCmd} \
+          > ${inProgressFile}
+
+        mv ${inProgressFile} ${curFile}
+      '';
+
+      serviceConfig = {
+        Type = "oneshot";
+        User = "postgres";
+      };
+
+      startAt = cfg.startAt;
+    };
+
+in {
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "postgresqlBackup" "period" ] ''
+       A systemd timer is now used instead of cron.
+       The starting time can be configured via <literal>services.postgresqlBackup.startAt</literal>.
+    '')
+  ];
+
+  options = {
+    services.postgresqlBackup = {
+      enable = mkEnableOption "PostgreSQL dumps";
+
+      startAt = mkOption {
+        default = "*-*-* 01:15:00";
+        type = with types; either (listOf str) str;
+        description = ''
+          This option defines (see <literal>systemd.time</literal> for format) when the
+          databases should be dumped.
+          The default is to update at 01:15 (at night) every day.
+        '';
+      };
+
+      backupAll = mkOption {
+        default = cfg.databases == [];
+        defaultText = literalExpression "services.postgresqlBackup.databases == []";
+        type = lib.types.bool;
+        description = ''
+          Backup all databases using pg_dumpall.
+          This option is mutual exclusive to
+          <literal>services.postgresqlBackup.databases</literal>.
+          The resulting backup dump will have the name all.sql.gz.
+          This option is the default if no databases are specified.
+        '';
+      };
+
+      databases = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        description = ''
+          List of database names to dump.
+        '';
+      };
+
+      location = mkOption {
+        default = "/var/backup/postgresql";
+        type = types.path;
+        description = ''
+          Path of directory where the PostgreSQL database dumps will be placed.
+        '';
+      };
+
+      pgdumpOptions = mkOption {
+        type = types.separatedString " ";
+        default = "-C";
+        description = ''
+          Command line options for pg_dump. This options is not used
+          if <literal>config.services.postgresqlBackup.backupAll</literal> is enabled.
+          Note that config.services.postgresqlBackup.backupAll is also active,
+          when no databases where specified.
+        '';
+      };
+
+      compression = mkOption {
+        type = types.enum ["none" "gzip" "zstd"];
+        default = "gzip";
+        description = ''
+          The type of compression to use on the generated database dump.
+        '';
+      };
+    };
+
+  };
+
+  config = mkMerge [
+    {
+      assertions = [{
+        assertion = cfg.backupAll -> cfg.databases == [];
+        message = "config.services.postgresqlBackup.backupAll cannot be used together with config.services.postgresqlBackup.databases";
+      }];
+    }
+    (mkIf cfg.enable {
+      systemd.tmpfiles.rules = [
+        "d '${cfg.location}' 0700 postgres - - -"
+      ];
+    })
+    (mkIf (cfg.enable && cfg.backupAll) {
+      systemd.services.postgresqlBackup =
+        postgresqlBackupService "all" "pg_dumpall";
+    })
+    (mkIf (cfg.enable && !cfg.backupAll) {
+      systemd.services = listToAttrs (map (db:
+        let
+          cmd = "pg_dump ${cfg.pgdumpOptions} ${db}";
+        in {
+          name = "postgresqlBackup-${db}";
+          value = postgresqlBackupService db cmd;
+        }) cfg.databases);
+    })
+  ];
+
+}
diff --git a/nixos/modules/services/backup/postgresql-wal-receiver.nix b/nixos/modules/services/backup/postgresql-wal-receiver.nix
new file mode 100644
index 00000000000..32643adfdae
--- /dev/null
+++ b/nixos/modules/services/backup/postgresql-wal-receiver.nix
@@ -0,0 +1,204 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  receiverSubmodule = {
+    options = {
+      postgresqlPackage = mkOption {
+        type = types.package;
+        example = literalExpression "pkgs.postgresql_11";
+        description = ''
+          PostgreSQL package to use.
+        '';
+      };
+
+      directory = mkOption {
+        type = types.path;
+        example = literalExpression "/mnt/pg_wal/main/";
+        description = ''
+          Directory to write the output to.
+        '';
+      };
+
+      statusInterval = mkOption {
+        type = types.int;
+        default = 10;
+        description = ''
+          Specifies the number of seconds between status packets sent back to the server.
+          This allows for easier monitoring of the progress from server.
+          A value of zero disables the periodic status updates completely,
+          although an update will still be sent when requested by the server, to avoid timeout disconnect.
+        '';
+      };
+
+      slot = mkOption {
+        type = types.str;
+        default = "";
+        example = "some_slot_name";
+        description = ''
+          Require <command>pg_receivewal</command> to use an existing replication slot (see
+          <link xlink:href="https://www.postgresql.org/docs/current/warm-standby.html#STREAMING-REPLICATION-SLOTS">Section 26.2.6 of the PostgreSQL manual</link>).
+          When this option is used, <command>pg_receivewal</command> will report a flush position to the server,
+          indicating when each segment has been synchronized to disk so that the server can remove that segment if it is not otherwise needed.
+
+          When the replication client of <command>pg_receivewal</command> is configured on the server as a synchronous standby,
+          then using a replication slot will report the flush position to the server, but only when a WAL file is closed.
+          Therefore, that configuration will cause transactions on the primary to wait for a long time and effectively not work satisfactorily.
+          The option <option>synchronous</option> must be specified in addition to make this work correctly.
+        '';
+      };
+
+      synchronous = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Flush the WAL data to disk immediately after it has been received.
+          Also send a status packet back to the server immediately after flushing, regardless of <option>statusInterval</option>.
+
+          This option should be specified if the replication client of <command>pg_receivewal</command> is configured on the server as a synchronous standby,
+          to ensure that timely feedback is sent to the server.
+        '';
+      };
+
+      compress = mkOption {
+        type = types.ints.between 0 9;
+        default = 0;
+        description = ''
+          Enables gzip compression of write-ahead logs, and specifies the compression level
+          (<literal>0</literal> through <literal>9</literal>, <literal>0</literal> being no compression and <literal>9</literal> being best compression).
+          The suffix <literal>.gz</literal> will automatically be added to all filenames.
+
+          This option requires PostgreSQL >= 10.
+        '';
+      };
+
+      connection = mkOption {
+        type = types.str;
+        example = "postgresql://user@somehost";
+        description = ''
+          Specifies parameters used to connect to the server, as a connection string.
+          See <link xlink:href="https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING">Section 34.1.1 of the PostgreSQL manual</link> for more information.
+
+          Because <command>pg_receivewal</command> doesn't connect to any particular database in the cluster,
+          database name in the connection string will be ignored.
+        '';
+      };
+
+      extraArgs = mkOption {
+        type = with types; listOf str;
+        default = [ ];
+        example = literalExpression ''
+          [
+            "--no-sync"
+          ]
+        '';
+        description = ''
+          A list of extra arguments to pass to the <command>pg_receivewal</command> command.
+        '';
+      };
+
+      environment = mkOption {
+        type = with types; attrsOf str;
+        default = { };
+        example = literalExpression ''
+          {
+            PGPASSFILE = "/private/passfile";
+            PGSSLMODE = "require";
+          }
+        '';
+        description = ''
+          Environment variables passed to the service.
+          Usable parameters are listed in <link xlink:href="https://www.postgresql.org/docs/current/libpq-envars.html">Section 34.14 of the PostgreSQL manual</link>.
+        '';
+      };
+    };
+  };
+
+in {
+  options = {
+    services.postgresqlWalReceiver = {
+      receivers = mkOption {
+        type = with types; attrsOf (submodule receiverSubmodule);
+        default = { };
+        example = literalExpression ''
+          {
+            main = {
+              postgresqlPackage = pkgs.postgresql_11;
+              directory = /mnt/pg_wal/main/;
+              slot = "main_wal_receiver";
+              connection = "postgresql://user@somehost";
+            };
+          }
+        '';
+        description = ''
+          PostgreSQL WAL receivers.
+          Stream write-ahead logs from a PostgreSQL server using <command>pg_receivewal</command> (formerly <command>pg_receivexlog</command>).
+          See <link xlink:href="https://www.postgresql.org/docs/current/app-pgreceivewal.html">the man page</link> for more information.
+        '';
+      };
+    };
+  };
+
+  config = let
+    receivers = config.services.postgresqlWalReceiver.receivers;
+  in mkIf (receivers != { }) {
+    users = {
+      users.postgres = {
+        uid = config.ids.uids.postgres;
+        group = "postgres";
+        description = "PostgreSQL server user";
+      };
+
+      groups.postgres = {
+        gid = config.ids.gids.postgres;
+      };
+    };
+
+    assertions = concatLists (attrsets.mapAttrsToList (name: config: [
+      {
+        assertion = config.compress > 0 -> versionAtLeast config.postgresqlPackage.version "10";
+        message = "Invalid configuration for WAL receiver \"${name}\": compress requires PostgreSQL version >= 10.";
+      }
+    ]) receivers);
+
+    systemd.tmpfiles.rules = mapAttrsToList (name: config: ''
+      d ${escapeShellArg config.directory} 0750 postgres postgres - -
+    '') receivers;
+
+    systemd.services = with attrsets; mapAttrs' (name: config: nameValuePair "postgresql-wal-receiver-${name}" {
+      description = "PostgreSQL WAL receiver (${name})";
+      wantedBy = [ "multi-user.target" ];
+      startLimitIntervalSec = 0; # retry forever, useful in case of network disruption
+
+      serviceConfig = {
+        User = "postgres";
+        Group = "postgres";
+        KillSignal = "SIGINT";
+        Restart = "always";
+        RestartSec = 60;
+      };
+
+      inherit (config) environment;
+
+      script = let
+        receiverCommand = postgresqlPackage:
+         if (versionAtLeast postgresqlPackage.version "10")
+           then "${postgresqlPackage}/bin/pg_receivewal"
+           else "${postgresqlPackage}/bin/pg_receivexlog";
+      in ''
+        ${receiverCommand config.postgresqlPackage} \
+          --no-password \
+          --directory=${escapeShellArg config.directory} \
+          --status-interval=${toString config.statusInterval} \
+          --dbname=${escapeShellArg config.connection} \
+          ${optionalString (config.compress > 0) "--compress=${toString config.compress}"} \
+          ${optionalString (config.slot != "") "--slot=${escapeShellArg config.slot}"} \
+          ${optionalString config.synchronous "--synchronous"} \
+          ${concatStringsSep " " config.extraArgs}
+      '';
+    }) receivers;
+  };
+
+  meta.maintainers = with maintainers; [ pacien ];
+}
diff --git a/nixos/modules/services/backup/restic-rest-server.nix b/nixos/modules/services/backup/restic-rest-server.nix
new file mode 100644
index 00000000000..4717119f178
--- /dev/null
+++ b/nixos/modules/services/backup/restic-rest-server.nix
@@ -0,0 +1,111 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.restic.server;
+in
+{
+  meta.maintainers = [ maintainers.bachp ];
+
+  options.services.restic.server = {
+    enable = mkEnableOption "Restic REST Server";
+
+    listenAddress = mkOption {
+      default = ":8000";
+      example = "127.0.0.1:8080";
+      type = types.str;
+      description = "Listen on a specific IP address and port.";
+    };
+
+    dataDir = mkOption {
+      default = "/var/lib/restic";
+      type = types.path;
+      description = "The directory for storing the restic repository.";
+    };
+
+    appendOnly = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Enable append only mode.
+        This mode allows creation of new backups but prevents deletion and modification of existing backups.
+        This can be useful when backing up systems that have a potential of being hacked.
+      '';
+    };
+
+    privateRepos = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Enable private repos.
+        Grants access only when a subdirectory with the same name as the user is specified in the repository URL.
+      '';
+    };
+
+    prometheus = mkOption {
+      default = false;
+      type = types.bool;
+      description = "Enable Prometheus metrics at /metrics.";
+    };
+
+    extraFlags = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Extra commandline options to pass to Restic REST server.
+      '';
+    };
+
+    package = mkOption {
+      default = pkgs.restic-rest-server;
+      defaultText = literalExpression "pkgs.restic-rest-server";
+      type = types.package;
+      description = "Restic REST server package to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.restic-rest-server = {
+      description = "Restic REST Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.package}/bin/rest-server \
+          --listen ${cfg.listenAddress} \
+          --path ${cfg.dataDir} \
+          ${optionalString cfg.appendOnly "--append-only"} \
+          ${optionalString cfg.privateRepos "--private-repos"} \
+          ${optionalString cfg.prometheus "--prometheus"} \
+          ${escapeShellArgs cfg.extraFlags} \
+        '';
+        Type = "simple";
+        User = "restic";
+        Group = "restic";
+
+        # Security hardening
+        ReadWritePaths = [ cfg.dataDir ];
+        PrivateTmp = true;
+        ProtectSystem = "strict";
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        PrivateDevices = true;
+      };
+    };
+
+    systemd.tmpfiles.rules = mkIf cfg.privateRepos [
+        "f ${cfg.dataDir}/.htpasswd 0700 restic restic -"
+    ];
+
+    users.users.restic = {
+      group = "restic";
+      home = cfg.dataDir;
+      createHome = true;
+      uid = config.ids.uids.restic;
+    };
+
+    users.groups.restic.gid = config.ids.uids.restic;
+  };
+}
diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix
new file mode 100644
index 00000000000..8ff8e31864b
--- /dev/null
+++ b/nixos/modules/services/backup/restic.nix
@@ -0,0 +1,290 @@
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+
+let
+  # Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers"
+  inherit (utils.systemdUtils.unitOptions) unitOption;
+in
+{
+  options.services.restic.backups = mkOption {
+    description = ''
+      Periodic backups to create with Restic.
+    '';
+    type = types.attrsOf (types.submodule ({ config, name, ... }: {
+      options = {
+        passwordFile = mkOption {
+          type = types.str;
+          description = ''
+            Read the repository password from a file.
+          '';
+          example = "/etc/nixos/restic-password";
+        };
+
+        environmentFile = mkOption {
+          type = with types; nullOr str;
+          # added on 2021-08-28, s3CredentialsFile should
+          # be removed in the future (+ remember the warning)
+          default = config.s3CredentialsFile;
+          description = ''
+            file containing the credentials to access the repository, in the
+            format of an EnvironmentFile as described by systemd.exec(5)
+          '';
+        };
+
+        s3CredentialsFile = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = ''
+            file containing the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
+            for an S3-hosted repository, in the format of an EnvironmentFile
+            as described by systemd.exec(5)
+          '';
+        };
+
+        rcloneOptions = mkOption {
+          type = with types; nullOr (attrsOf (oneOf [ str bool ]));
+          default = null;
+          description = ''
+            Options to pass to rclone to control its behavior.
+            See <link xlink:href="https://rclone.org/docs/#options"/> for
+            available options. When specifying option names, strip the
+            leading <literal>--</literal>. To set a flag such as
+            <literal>--drive-use-trash</literal>, which does not take a value,
+            set the value to the Boolean <literal>true</literal>.
+          '';
+          example = {
+            bwlimit = "10M";
+            drive-use-trash = "true";
+          };
+        };
+
+        rcloneConfig = mkOption {
+          type = with types; nullOr (attrsOf (oneOf [ str bool ]));
+          default = null;
+          description = ''
+            Configuration for the rclone remote being used for backup.
+            See the remote's specific options under rclone's docs at
+            <link xlink:href="https://rclone.org/docs/"/>. When specifying
+            option names, use the "config" name specified in the docs.
+            For example, to set <literal>--b2-hard-delete</literal> for a B2
+            remote, use <literal>hard_delete = true</literal> in the
+            attribute set.
+            Warning: Secrets set in here will be world-readable in the Nix
+            store! Consider using the <literal>rcloneConfigFile</literal>
+            option instead to specify secret values separately. Note that
+            options set here will override those set in the config file.
+          '';
+          example = {
+            type = "b2";
+            account = "xxx";
+            key = "xxx";
+            hard_delete = true;
+          };
+        };
+
+        rcloneConfigFile = mkOption {
+          type = with types; nullOr path;
+          default = null;
+          description = ''
+            Path to the file containing rclone configuration. This file
+            must contain configuration for the remote specified in this backup
+            set and also must be readable by root. Options set in
+            <literal>rcloneConfig</literal> will override those set in this
+            file.
+          '';
+        };
+
+        repository = mkOption {
+          type = types.str;
+          description = ''
+            repository to backup to.
+          '';
+          example = "sftp:backup@192.168.1.100:/backups/${name}";
+        };
+
+        paths = mkOption {
+          type = types.nullOr (types.listOf types.str);
+          default = null;
+          description = ''
+            Which paths to backup.  If null or an empty array, no
+            backup command will be run.  This can be used to create a
+            prune-only job.
+          '';
+          example = [
+            "/var/lib/postgresql"
+            "/home/user/backup"
+          ];
+        };
+
+        timerConfig = mkOption {
+          type = types.attrsOf unitOption;
+          default = {
+            OnCalendar = "daily";
+          };
+          description = ''
+            When to run the backup. See man systemd.timer for details.
+          '';
+          example = {
+            OnCalendar = "00:05";
+            RandomizedDelaySec = "5h";
+          };
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "root";
+          description = ''
+            As which user the backup should run.
+          '';
+          example = "postgresql";
+        };
+
+        extraBackupArgs = mkOption {
+          type = types.listOf types.str;
+          default = [];
+          description = ''
+            Extra arguments passed to restic backup.
+          '';
+          example = [
+            "--exclude-file=/etc/nixos/restic-ignore"
+          ];
+        };
+
+        extraOptions = mkOption {
+          type = types.listOf types.str;
+          default = [];
+          description = ''
+            Extra extended options to be passed to the restic --option flag.
+          '';
+          example = [
+            "sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'"
+          ];
+        };
+
+        initialize = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Create the repository if it doesn't exist.
+          '';
+        };
+
+        pruneOpts = mkOption {
+          type = types.listOf types.str;
+          default = [];
+          description = ''
+            A list of options (--keep-* et al.) for 'restic forget
+            --prune', to automatically prune old snapshots.  The
+            'forget' command is run *after* the 'backup' command, so
+            keep that in mind when constructing the --keep-* options.
+          '';
+          example = [
+            "--keep-daily 7"
+            "--keep-weekly 5"
+            "--keep-monthly 12"
+            "--keep-yearly 75"
+          ];
+        };
+
+        dynamicFilesFrom = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = ''
+            A script that produces a list of files to back up.  The
+            results of this command are given to the '--files-from'
+            option.
+          '';
+          example = "find /home/matt/git -type d -name .git";
+        };
+      };
+    }));
+    default = {};
+    example = {
+      localbackup = {
+        paths = [ "/home" ];
+        repository = "/mnt/backup-hdd";
+        passwordFile = "/etc/nixos/secrets/restic-password";
+        initialize = true;
+      };
+      remotebackup = {
+        paths = [ "/home" ];
+        repository = "sftp:backup@host:/backups/home";
+        passwordFile = "/etc/nixos/secrets/restic-password";
+        extraOptions = [
+          "sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'"
+        ];
+        timerConfig = {
+          OnCalendar = "00:05";
+          RandomizedDelaySec = "5h";
+        };
+      };
+    };
+  };
+
+  config = {
+    warnings = mapAttrsToList (n: v: "services.restic.backups.${n}.s3CredentialsFile is deprecated, please use services.restic.backups.${n}.environmentFile instead.") (filterAttrs (n: v: v.s3CredentialsFile != null) config.services.restic.backups);
+    systemd.services =
+      mapAttrs' (name: backup:
+        let
+          extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
+          resticCmd = "${pkgs.restic}/bin/restic${extraOptions}";
+          filesFromTmpFile = "/run/restic-backups-${name}/includes";
+          backupPaths = if (backup.dynamicFilesFrom == null)
+                        then if (backup.paths != null) then concatStringsSep " " backup.paths else ""
+                        else "--files-from ${filesFromTmpFile}";
+          pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [
+            ( resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts) )
+            ( resticCmd + " check" )
+          ];
+          # Helper functions for rclone remotes
+          rcloneRemoteName = builtins.elemAt (splitString ":" backup.repository) 1;
+          rcloneAttrToOpt = v: "RCLONE_" + toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v);
+          rcloneAttrToConf = v: "RCLONE_CONFIG_" + toUpper (rcloneRemoteName + "_" + v);
+          toRcloneVal = v: if lib.isBool v then lib.boolToString v else v;
+        in nameValuePair "restic-backups-${name}" ({
+          environment = {
+            RESTIC_PASSWORD_FILE = backup.passwordFile;
+            RESTIC_REPOSITORY = backup.repository;
+          } // optionalAttrs (backup.rcloneOptions != null) (mapAttrs' (name: value:
+            nameValuePair (rcloneAttrToOpt name) (toRcloneVal value)
+          ) backup.rcloneOptions) // optionalAttrs (backup.rcloneConfigFile != null) {
+            RCLONE_CONFIG = backup.rcloneConfigFile;
+          } // optionalAttrs (backup.rcloneConfig != null) (mapAttrs' (name: value:
+            nameValuePair (rcloneAttrToConf name) (toRcloneVal value)
+          ) backup.rcloneConfig);
+          path = [ pkgs.openssh ];
+          restartIfChanged = false;
+          serviceConfig = {
+            Type = "oneshot";
+            ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup --cache-dir=%C/restic-backups-${name} ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ])
+                        ++ pruneCmd;
+            User = backup.user;
+            RuntimeDirectory = "restic-backups-${name}";
+            CacheDirectory = "restic-backups-${name}";
+            CacheDirectoryMode = "0700";
+          } // optionalAttrs (backup.environmentFile != null) {
+            EnvironmentFile = backup.environmentFile;
+          };
+        } // optionalAttrs (backup.initialize || backup.dynamicFilesFrom != null) {
+          preStart = ''
+            ${optionalString (backup.initialize) ''
+              ${resticCmd} snapshots || ${resticCmd} init
+            ''}
+            ${optionalString (backup.dynamicFilesFrom != null) ''
+              ${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} > ${filesFromTmpFile}
+            ''}
+          '';
+        } // optionalAttrs (backup.dynamicFilesFrom != null) {
+          postStart = ''
+            rm ${filesFromTmpFile}
+          '';
+        })
+      ) config.services.restic.backups;
+    systemd.timers =
+      mapAttrs' (name: backup: nameValuePair "restic-backups-${name}" {
+        wantedBy = [ "timers.target" ];
+        timerConfig = backup.timerConfig;
+      }) config.services.restic.backups;
+  };
+}
diff --git a/nixos/modules/services/backup/rsnapshot.nix b/nixos/modules/services/backup/rsnapshot.nix
new file mode 100644
index 00000000000..6635a51ec2c
--- /dev/null
+++ b/nixos/modules/services/backup/rsnapshot.nix
@@ -0,0 +1,75 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.rsnapshot;
+  cfgfile = pkgs.writeText "rsnapshot.conf" ''
+    config_version	1.2
+    cmd_cp	${pkgs.coreutils}/bin/cp
+    cmd_rm	${pkgs.coreutils}/bin/rm
+    cmd_rsync	${pkgs.rsync}/bin/rsync
+    cmd_ssh	${pkgs.openssh}/bin/ssh
+    cmd_logger	${pkgs.inetutils}/bin/logger
+    cmd_du	${pkgs.coreutils}/bin/du
+    cmd_rsnapshot_diff	${pkgs.rsnapshot}/bin/rsnapshot-diff
+    lockfile	/run/rsnapshot.pid
+    link_dest	1
+
+    ${cfg.extraConfig}
+  '';
+in
+{
+  options = {
+    services.rsnapshot = {
+      enable = mkEnableOption "rsnapshot backups";
+      enableManualRsnapshot = mkOption {
+        description = "Whether to enable manual usage of the rsnapshot command with this module.";
+        default = true;
+        type = types.bool;
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        example = ''
+          retains	hourly	24
+          retain	daily	365
+          backup	/home/	localhost/
+        '';
+        type = types.lines;
+        description = ''
+          rsnapshot configuration option in addition to the defaults from
+          rsnapshot and this module.
+
+          Note that tabs are required to separate option arguments, and
+          directory names require trailing slashes.
+
+          The "extra" in the option name might be a little misleading right
+          now, as it is required to get a functional configuration.
+        '';
+      };
+
+      cronIntervals = mkOption {
+        default = {};
+        example = { hourly = "0 * * * *"; daily = "50 21 * * *"; };
+        type = types.attrsOf types.str;
+        description = ''
+          Periodicity at which intervals should be run by cron.
+          Note that the intervals also have to exist in configuration
+          as retain options.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable (mkMerge [
+    {
+      services.cron.systemCronJobs =
+        mapAttrsToList (interval: time: "${time} root ${pkgs.rsnapshot}/bin/rsnapshot -c ${cfgfile} ${interval}") cfg.cronIntervals;
+    }
+    (mkIf cfg.enableManualRsnapshot {
+      environment.systemPackages = [ pkgs.rsnapshot ];
+      environment.etc."rsnapshot.conf".source = cfgfile;
+    })
+  ]);
+}
diff --git a/nixos/modules/services/backup/sanoid.nix b/nixos/modules/services/backup/sanoid.nix
new file mode 100644
index 00000000000..5eb031b2e9f
--- /dev/null
+++ b/nixos/modules/services/backup/sanoid.nix
@@ -0,0 +1,204 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.sanoid;
+
+  datasetSettingsType = with types;
+    (attrsOf (nullOr (oneOf [ str int bool (listOf str) ]))) // {
+      description = "dataset/template options";
+    };
+
+  commonOptions = {
+    hourly = mkOption {
+      description = "Number of hourly snapshots.";
+      type = with types; nullOr ints.unsigned;
+      default = null;
+    };
+
+    daily = mkOption {
+      description = "Number of daily snapshots.";
+      type = with types; nullOr ints.unsigned;
+      default = null;
+    };
+
+    monthly = mkOption {
+      description = "Number of monthly snapshots.";
+      type = with types; nullOr ints.unsigned;
+      default = null;
+    };
+
+    yearly = mkOption {
+      description = "Number of yearly snapshots.";
+      type = with types; nullOr ints.unsigned;
+      default = null;
+    };
+
+    autoprune = mkOption {
+      description = "Whether to automatically prune old snapshots.";
+      type = with types; nullOr bool;
+      default = null;
+    };
+
+    autosnap = mkOption {
+      description = "Whether to automatically take snapshots.";
+      type = with types; nullOr bool;
+      default = null;
+    };
+  };
+
+  datasetOptions = rec {
+    use_template = mkOption {
+      description = "Names of the templates to use for this dataset.";
+      type = types.listOf (types.str // {
+        check = (types.enum (attrNames cfg.templates)).check;
+        description = "configured template name";
+      });
+      default = [ ];
+    };
+    useTemplate = use_template;
+
+    recursive = mkOption {
+      description = ''
+        Whether to recursively snapshot dataset children.
+        You can also set this to <literal>"zfs"</literal> to handle datasets
+        recursively in an atomic way without the possibility to
+        override settings for child datasets.
+      '';
+      type = with types; oneOf [ bool (enum [ "zfs" ]) ];
+      default = false;
+    };
+
+    process_children_only = mkOption {
+      description = "Whether to only snapshot child datasets if recursing.";
+      type = types.bool;
+      default = false;
+    };
+    processChildrenOnly = process_children_only;
+  };
+
+  # Extract unique dataset names
+  datasets = unique (attrNames cfg.datasets);
+
+  # Function to build "zfs allow" and "zfs unallow" commands for the
+  # filesystems we've delegated permissions to.
+  buildAllowCommand = zfsAction: permissions: dataset: lib.escapeShellArgs [
+    # Here we explicitly use the booted system to guarantee the stable API needed by ZFS
+    "-+/run/booted-system/sw/bin/zfs"
+    zfsAction
+    "sanoid"
+    (concatStringsSep "," permissions)
+    dataset
+  ];
+
+  configFile =
+    let
+      mkValueString = v:
+        if builtins.isList v then concatStringsSep "," v
+        else generators.mkValueStringDefault { } v;
+
+      mkKeyValue = k: v:
+        if v == null then ""
+        else if k == "processChildrenOnly" then ""
+        else if k == "useTemplate" then ""
+        else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v;
+    in
+    generators.toINI { inherit mkKeyValue; } cfg.settings;
+
+in
+{
+
+  # Interface
+
+  options.services.sanoid = {
+    enable = mkEnableOption "Sanoid ZFS snapshotting service";
+
+    interval = mkOption {
+      type = types.str;
+      default = "hourly";
+      example = "daily";
+      description = ''
+        Run sanoid at this interval. The default is to run hourly.
+
+        The format is described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.
+      '';
+    };
+
+    datasets = mkOption {
+      type = types.attrsOf (types.submodule ({ config, options, ... }: {
+        freeformType = datasetSettingsType;
+        options = commonOptions // datasetOptions;
+        config.use_template = mkAliasDefinitions (mkDefault options.useTemplate or { });
+        config.process_children_only = mkAliasDefinitions (mkDefault options.processChildrenOnly or { });
+      }));
+      default = { };
+      description = "Datasets to snapshot.";
+    };
+
+    templates = mkOption {
+      type = types.attrsOf (types.submodule {
+        freeformType = datasetSettingsType;
+        options = commonOptions;
+      });
+      default = { };
+      description = "Templates for datasets.";
+    };
+
+    settings = mkOption {
+      type = types.attrsOf datasetSettingsType;
+      description = ''
+        Free-form settings written directly to the config file. See
+        <link xlink:href="https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf"/>
+        for allowed values.
+      '';
+    };
+
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = [ "--verbose" "--readonly" "--debug" ];
+      description = ''
+        Extra arguments to pass to sanoid. See
+        <link xlink:href="https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options"/>
+        for allowed options.
+      '';
+    };
+  };
+
+  # Implementation
+
+  config = mkIf cfg.enable {
+    services.sanoid.settings = mkMerge [
+      (mapAttrs' (d: v: nameValuePair ("template_" + d) v) cfg.templates)
+      (mapAttrs (d: v: v) cfg.datasets)
+    ];
+
+    systemd.services.sanoid = {
+      description = "Sanoid snapshot service";
+      serviceConfig = {
+        ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets);
+        ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets);
+        ExecStart = lib.escapeShellArgs ([
+          "${pkgs.sanoid}/bin/sanoid"
+          "--cron"
+          "--configdir"
+          (pkgs.writeTextDir "sanoid.conf" configFile)
+        ] ++ cfg.extraArgs);
+        User = "sanoid";
+        Group = "sanoid";
+        DynamicUser = true;
+        RuntimeDirectory = "sanoid";
+        CacheDirectory = "sanoid";
+      };
+      # Prevents missing snapshots during DST changes
+      environment.TZ = "UTC";
+      after = [ "zfs.target" ];
+      startAt = cfg.interval;
+    };
+  };
+
+  meta.maintainers = with maintainers; [ lopsided98 ];
+}
diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix
new file mode 100644
index 00000000000..4df10f5ee02
--- /dev/null
+++ b/nixos/modules/services/backup/syncoid.nix
@@ -0,0 +1,421 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.syncoid;
+
+  # Extract local dasaset names (so no datasets containing "@")
+  localDatasetName = d: optionals (d != null) (
+    let m = builtins.match "([^/@]+[^@]*)" d; in
+    optionals (m != null) m
+  );
+
+  # Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
+  escapeUnitName = name:
+    lib.concatMapStrings (s: if lib.isList s then "-" else s)
+      (builtins.split "[^a-zA-Z0-9_.\\-]+" name);
+
+  # Function to build "zfs allow" commands for the filesystems we've
+  # delegated permissions to. It also checks if the target dataset
+  # exists before delegating permissions, if it doesn't exist we
+  # delegate it to the parent dataset. This should solve the case of
+  # provisoning new datasets.
+  buildAllowCommand = permissions: dataset: (
+    "-+${pkgs.writeShellScript "zfs-allow-${dataset}" ''
+      # Here we explicitly use the booted system to guarantee the stable API needed by ZFS
+
+      # Run a ZFS list on the dataset to check if it exists
+      if ${lib.escapeShellArgs [
+        "/run/booted-system/sw/bin/zfs"
+        "list"
+        dataset
+      ]} 2> /dev/null; then
+        ${lib.escapeShellArgs [
+          "/run/booted-system/sw/bin/zfs"
+          "allow"
+          cfg.user
+          (concatStringsSep "," permissions)
+          dataset
+        ]}
+      else
+        ${lib.escapeShellArgs [
+          "/run/booted-system/sw/bin/zfs"
+          "allow"
+          cfg.user
+          (concatStringsSep "," permissions)
+          # Remove the last part of the path
+          (builtins.dirOf dataset)
+        ]}
+      fi
+    ''}"
+  );
+
+  # Function to build "zfs unallow" commands for the filesystems we've
+  # delegated permissions to. Here we unallow both the target but also
+  # on the parent dataset because at this stage we have no way of
+  # knowing if the allow command did execute on the parent dataset or
+  # not in the pre-hook. We can't run the same if in the post hook
+  # since the dataset should have been created at this point.
+  buildUnallowCommand = permissions: dataset: (
+    "-+${pkgs.writeShellScript "zfs-unallow-${dataset}" ''
+      # Here we explicitly use the booted system to guarantee the stable API needed by ZFS
+      ${lib.escapeShellArgs [
+        "/run/booted-system/sw/bin/zfs"
+        "unallow"
+        cfg.user
+        (concatStringsSep "," permissions)
+        dataset
+      ]}
+      ${lib.escapeShellArgs [
+        "/run/booted-system/sw/bin/zfs"
+        "unallow"
+        cfg.user
+        (concatStringsSep "," permissions)
+        # Remove the last part of the path
+        (builtins.dirOf dataset)
+      ]}
+    ''}"
+  );
+in
+{
+
+  # Interface
+
+  options.services.syncoid = {
+    enable = mkEnableOption "Syncoid ZFS synchronization service";
+
+    interval = mkOption {
+      type = types.str;
+      default = "hourly";
+      example = "*-*-* *:15:00";
+      description = ''
+        Run syncoid at this interval. The default is to run hourly.
+
+        The format is described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.
+      '';
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = "syncoid";
+      example = "backup";
+      description = ''
+        The user for the service. ZFS privilege delegation will be
+        automatically configured for any local pools used by syncoid if this
+        option is set to a user other than root. The user will be given the
+        "hold" and "send" privileges on any pool that has datasets being sent
+        and the "create", "mount", "receive", and "rollback" privileges on
+        any pool that has datasets being received.
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "syncoid";
+      example = "backup";
+      description = "The group for the service.";
+    };
+
+    sshKey = mkOption {
+      type = types.nullOr types.path;
+      # Prevent key from being copied to store
+      apply = mapNullable toString;
+      default = null;
+      description = ''
+        SSH private key file to use to login to the remote system. Can be
+        overridden in individual commands.
+      '';
+    };
+
+    localSourceAllow = mkOption {
+      type = types.listOf types.str;
+      # Permissions snapshot and destroy are in case --no-sync-snap is not used
+      default = [ "bookmark" "hold" "send" "snapshot" "destroy" ];
+      description = ''
+        Permissions granted for the <option>services.syncoid.user</option> user
+        for local source datasets. See
+        <link xlink:href="https://openzfs.github.io/openzfs-docs/man/8/zfs-allow.8.html"/>
+        for available permissions.
+      '';
+    };
+
+    localTargetAllow = mkOption {
+      type = types.listOf types.str;
+      default = [ "change-key" "compression" "create" "mount" "mountpoint" "receive" "rollback" ];
+      example = [ "create" "mount" "receive" "rollback" ];
+      description = ''
+        Permissions granted for the <option>services.syncoid.user</option> user
+        for local target datasets. See
+        <link xlink:href="https://openzfs.github.io/openzfs-docs/man/8/zfs-allow.8.html"/>
+        for available permissions.
+        Make sure to include the <literal>change-key</literal> permission if you send raw encrypted datasets,
+        the <literal>compression</literal> permission if you send raw compressed datasets, and so on.
+        For remote target datasets you'll have to set your remote user permissions by yourself.
+      '';
+    };
+
+    commonArgs = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = [ "--no-sync-snap" ];
+      description = ''
+        Arguments to add to every syncoid command, unless disabled for that
+        command. See
+        <link xlink:href="https://github.com/jimsalterjrs/sanoid/#syncoid-command-line-options"/>
+        for available options.
+      '';
+    };
+
+    service = mkOption {
+      type = types.attrs;
+      default = { };
+      description = ''
+        Systemd configuration common to all syncoid services.
+      '';
+    };
+
+    commands = mkOption {
+      type = types.attrsOf (types.submodule ({ name, ... }: {
+        options = {
+          source = mkOption {
+            type = types.str;
+            example = "pool/dataset";
+            description = ''
+              Source ZFS dataset. Can be either local or remote. Defaults to
+              the attribute name.
+            '';
+          };
+
+          target = mkOption {
+            type = types.str;
+            example = "user@server:pool/dataset";
+            description = ''
+              Target ZFS dataset. Can be either local
+              (<replaceable>pool/dataset</replaceable>) or remote
+              (<replaceable>user@server:pool/dataset</replaceable>).
+            '';
+          };
+
+          recursive = mkEnableOption ''the transfer of child datasets'';
+
+          sshKey = mkOption {
+            type = types.nullOr types.path;
+            # Prevent key from being copied to store
+            apply = mapNullable toString;
+            description = ''
+              SSH private key file to use to login to the remote system.
+              Defaults to <option>services.syncoid.sshKey</option> option.
+            '';
+          };
+
+          localSourceAllow = mkOption {
+            type = types.listOf types.str;
+            description = ''
+              Permissions granted for the <option>services.syncoid.user</option> user
+              for local source datasets. See
+              <link xlink:href="https://openzfs.github.io/openzfs-docs/man/8/zfs-allow.8.html"/>
+              for available permissions.
+              Defaults to <option>services.syncoid.localSourceAllow</option> option.
+            '';
+          };
+
+          localTargetAllow = mkOption {
+            type = types.listOf types.str;
+            description = ''
+              Permissions granted for the <option>services.syncoid.user</option> user
+              for local target datasets. See
+              <link xlink:href="https://openzfs.github.io/openzfs-docs/man/8/zfs-allow.8.html"/>
+              for available permissions.
+              Make sure to include the <literal>change-key</literal> permission if you send raw encrypted datasets,
+              the <literal>compression</literal> permission if you send raw compressed datasets, and so on.
+              For remote target datasets you'll have to set your remote user permissions by yourself.
+            '';
+          };
+
+          sendOptions = mkOption {
+            type = types.separatedString " ";
+            default = "";
+            example = "Lc e";
+            description = ''
+              Advanced options to pass to zfs send. Options are specified
+              without their leading dashes and separated by spaces.
+            '';
+          };
+
+          recvOptions = mkOption {
+            type = types.separatedString " ";
+            default = "";
+            example = "ux recordsize o compression=lz4";
+            description = ''
+              Advanced options to pass to zfs recv. Options are specified
+              without their leading dashes and separated by spaces.
+            '';
+          };
+
+          useCommonArgs = mkOption {
+            type = types.bool;
+            default = true;
+            description = ''
+              Whether to add the configured common arguments to this command.
+            '';
+          };
+
+          service = mkOption {
+            type = types.attrs;
+            default = { };
+            description = ''
+              Systemd configuration specific to this syncoid service.
+            '';
+          };
+
+          extraArgs = mkOption {
+            type = types.listOf types.str;
+            default = [ ];
+            example = [ "--sshport 2222" ];
+            description = "Extra syncoid arguments for this command.";
+          };
+        };
+        config = {
+          source = mkDefault name;
+          sshKey = mkDefault cfg.sshKey;
+          localSourceAllow = mkDefault cfg.localSourceAllow;
+          localTargetAllow = mkDefault cfg.localTargetAllow;
+        };
+      }));
+      default = { };
+      example = literalExpression ''
+        {
+          "pool/test".target = "root@target:pool/test";
+        }
+      '';
+      description = "Syncoid commands to run.";
+    };
+  };
+
+  # Implementation
+
+  config = mkIf cfg.enable {
+    users = {
+      users = mkIf (cfg.user == "syncoid") {
+        syncoid = {
+          group = cfg.group;
+          isSystemUser = true;
+          # For syncoid to be able to create /var/lib/syncoid/.ssh/
+          # and to use custom ssh_config or known_hosts.
+          home = "/var/lib/syncoid";
+          createHome = false;
+        };
+      };
+      groups = mkIf (cfg.group == "syncoid") {
+        syncoid = { };
+      };
+    };
+
+    systemd.services = mapAttrs'
+      (name: c:
+        nameValuePair "syncoid-${escapeUnitName name}" (mkMerge [
+          {
+            description = "Syncoid ZFS synchronization from ${c.source} to ${c.target}";
+            after = [ "zfs.target" ];
+            startAt = cfg.interval;
+            # syncoid may need zpool to get feature@extensible_dataset
+            path = [ "/run/booted-system/sw/bin/" ];
+            serviceConfig = {
+              ExecStartPre =
+                (map (buildAllowCommand c.localSourceAllow) (localDatasetName c.source)) ++
+                (map (buildAllowCommand c.localTargetAllow) (localDatasetName c.target));
+              ExecStopPost =
+                (map (buildUnallowCommand c.localSourceAllow) (localDatasetName c.source)) ++
+                (map (buildUnallowCommand c.localTargetAllow) (localDatasetName c.target));
+              ExecStart = lib.escapeShellArgs ([ "${pkgs.sanoid}/bin/syncoid" ]
+                ++ optionals c.useCommonArgs cfg.commonArgs
+                ++ optional c.recursive "-r"
+                ++ optionals (c.sshKey != null) [ "--sshkey" c.sshKey ]
+                ++ c.extraArgs
+                ++ [
+                "--sendoptions"
+                c.sendOptions
+                "--recvoptions"
+                c.recvOptions
+                "--no-privilege-elevation"
+                c.source
+                c.target
+              ]);
+              User = cfg.user;
+              Group = cfg.group;
+              StateDirectory = [ "syncoid" ];
+              StateDirectoryMode = "700";
+              # Prevent SSH control sockets of different syncoid services from interfering
+              PrivateTmp = true;
+              # Permissive access to /proc because syncoid
+              # calls ps(1) to detect ongoing `zfs receive`.
+              ProcSubset = "all";
+              ProtectProc = "default";
+
+              # The following options are only for optimizing:
+              # systemd-analyze security | grep syncoid-'*'
+              AmbientCapabilities = "";
+              CapabilityBoundingSet = "";
+              DeviceAllow = [ "/dev/zfs" ];
+              LockPersonality = true;
+              MemoryDenyWriteExecute = true;
+              NoNewPrivileges = true;
+              PrivateDevices = true;
+              PrivateMounts = true;
+              PrivateNetwork = mkDefault false;
+              PrivateUsers = true;
+              ProtectClock = true;
+              ProtectControlGroups = true;
+              ProtectHome = true;
+              ProtectHostname = true;
+              ProtectKernelLogs = true;
+              ProtectKernelModules = true;
+              ProtectKernelTunables = true;
+              ProtectSystem = "strict";
+              RemoveIPC = true;
+              RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+              RestrictNamespaces = true;
+              RestrictRealtime = true;
+              RestrictSUIDSGID = true;
+              RootDirectory = "/run/syncoid/${escapeUnitName name}";
+              RootDirectoryStartOnly = true;
+              BindPaths = [ "/dev/zfs" ];
+              BindReadOnlyPaths = [ builtins.storeDir "/etc" "/run" "/bin/sh" ];
+              # Avoid useless mounting of RootDirectory= in the own RootDirectory= of ExecStart='s mount namespace.
+              InaccessiblePaths = [ "-+/run/syncoid/${escapeUnitName name}" ];
+              MountAPIVFS = true;
+              # Create RootDirectory= in the host's mount namespace.
+              RuntimeDirectory = [ "syncoid/${escapeUnitName name}" ];
+              RuntimeDirectoryMode = "700";
+              SystemCallFilter = [
+                "@system-service"
+                # Groups in @system-service which do not contain a syscall listed by:
+                # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' syncoid …
+                # awk >perf.syscalls -F "," '$1 > 0 {sub("syscalls:sys_enter_","",$3); print $3}' perf.log
+                # systemd-analyze syscall-filter | grep -v -e '#' | sed -e ':loop; /^[^ ]/N; s/\n //; t loop' | grep $(printf ' -e \\<%s\\>' $(cat perf.syscalls)) | cut -f 1 -d ' '
+                "~@aio"
+                "~@chown"
+                "~@keyring"
+                "~@memlock"
+                "~@privileged"
+                "~@resources"
+                "~@setuid"
+                "~@timer"
+              ];
+              SystemCallArchitectures = "native";
+              # This is for BindPaths= and BindReadOnlyPaths=
+              # to allow traversal of directories they create in RootDirectory=.
+              UMask = "0066";
+            };
+          }
+          cfg.service
+          c.service
+        ]))
+      cfg.commands;
+  };
+
+  meta.maintainers = with maintainers; [ julm lopsided98 ];
+}
diff --git a/nixos/modules/services/backup/tarsnap.nix b/nixos/modules/services/backup/tarsnap.nix
new file mode 100644
index 00000000000..9b5fd90012e
--- /dev/null
+++ b/nixos/modules/services/backup/tarsnap.nix
@@ -0,0 +1,408 @@
+{ config, lib, options, pkgs, utils, ... }:
+
+with lib;
+
+let
+  gcfg = config.services.tarsnap;
+  opt = options.services.tarsnap;
+
+  configFile = name: cfg: ''
+    keyfile ${cfg.keyfile}
+    ${optionalString (cfg.cachedir != null) "cachedir ${cfg.cachedir}"}
+    ${optionalString cfg.nodump "nodump"}
+    ${optionalString cfg.printStats "print-stats"}
+    ${optionalString cfg.printStats "humanize-numbers"}
+    ${optionalString (cfg.checkpointBytes != null) ("checkpoint-bytes "+cfg.checkpointBytes)}
+    ${optionalString cfg.aggressiveNetworking "aggressive-networking"}
+    ${concatStringsSep "\n" (map (v: "exclude ${v}") cfg.excludes)}
+    ${concatStringsSep "\n" (map (v: "include ${v}") cfg.includes)}
+    ${optionalString cfg.lowmem "lowmem"}
+    ${optionalString cfg.verylowmem "verylowmem"}
+    ${optionalString (cfg.maxbw != null) "maxbw ${toString cfg.maxbw}"}
+    ${optionalString (cfg.maxbwRateUp != null) "maxbw-rate-up ${toString cfg.maxbwRateUp}"}
+    ${optionalString (cfg.maxbwRateDown != null) "maxbw-rate-down ${toString cfg.maxbwRateDown}"}
+  '';
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "tarsnap" "cachedir" ] "Use services.tarsnap.archives.<name>.cachedir")
+  ];
+
+  options = {
+    services.tarsnap = {
+      enable = mkEnableOption "periodic tarsnap backups";
+
+      keyfile = mkOption {
+        type = types.str;
+        default = "/root/tarsnap.key";
+        description = ''
+          The keyfile which associates this machine with your tarsnap
+          account.
+          Create the keyfile with <command>tarsnap-keygen</command>.
+
+          Note that each individual archive (specified below) may also have its
+          own individual keyfile specified. Tarsnap does not allow multiple
+          concurrent backups with the same cache directory and key (starting a
+          new backup will cause another one to fail). If you have multiple
+          archives specified, you should either spread out your backups to be
+          far apart, or specify a separate key for each archive. By default
+          every archive defaults to using
+          <literal>"/root/tarsnap.key"</literal>.
+
+          It's recommended for backups that you generate a key for every archive
+          using <literal>tarsnap-keygen(1)</literal>, and then generate a
+          write-only tarsnap key using <literal>tarsnap-keymgmt(1)</literal>,
+          and keep your master key(s) for a particular machine off-site.
+
+          The keyfile name should be given as a string and not a path, to
+          avoid the key being copied into the Nix store.
+        '';
+      };
+
+      archives = mkOption {
+        type = types.attrsOf (types.submodule ({ config, options, ... }:
+          {
+            options = {
+              keyfile = mkOption {
+                type = types.str;
+                default = gcfg.keyfile;
+                defaultText = literalExpression "config.${opt.keyfile}";
+                description = ''
+                  Set a specific keyfile for this archive. This defaults to
+                  <literal>"/root/tarsnap.key"</literal> if left unspecified.
+
+                  Use this option if you want to run multiple backups
+                  concurrently - each archive must have a unique key. You can
+                  generate a write-only key derived from your master key (which
+                  is recommended) using <literal>tarsnap-keymgmt(1)</literal>.
+
+                  Note: every archive must have an individual master key. You
+                  must generate multiple keys with
+                  <literal>tarsnap-keygen(1)</literal>, and then generate write
+                  only keys from those.
+
+                  The keyfile name should be given as a string and not a path, to
+                  avoid the key being copied into the Nix store.
+                '';
+              };
+
+              cachedir = mkOption {
+                type = types.nullOr types.path;
+                default = "/var/cache/tarsnap/${utils.escapeSystemdPath config.keyfile}";
+                defaultText = literalExpression ''
+                  "/var/cache/tarsnap/''${utils.escapeSystemdPath config.${options.keyfile}}"
+                '';
+                description = ''
+                  The cache allows tarsnap to identify previously stored data
+                  blocks, reducing archival time and bandwidth usage.
+
+                  Should the cache become desynchronized or corrupted, tarsnap
+                  will refuse to run until you manually rebuild the cache with
+                  <command>tarsnap --fsck</command>.
+
+                  Set to <literal>null</literal> to disable caching.
+                '';
+              };
+
+              nodump = mkOption {
+                type = types.bool;
+                default = true;
+                description = ''
+                  Exclude files with the <literal>nodump</literal> flag.
+                '';
+              };
+
+              printStats = mkOption {
+                type = types.bool;
+                default = true;
+                description = ''
+                  Print global archive statistics upon completion.
+                  The output is available via
+                  <command>systemctl status tarsnap-archive-name</command>.
+                '';
+              };
+
+              checkpointBytes = mkOption {
+                type = types.nullOr types.str;
+                default = "1GB";
+                description = ''
+                  Create a checkpoint every <literal>checkpointBytes</literal>
+                  of uploaded data (optionally specified using an SI prefix).
+
+                  1GB is the minimum value. A higher value is recommended,
+                  as checkpointing is expensive.
+
+                  Set to <literal>null</literal> to disable checkpointing.
+                '';
+              };
+
+              period = mkOption {
+                type = types.str;
+                default = "01:15";
+                example = "hourly";
+                description = ''
+                  Create archive at this interval.
+
+                  The format is described in
+                  <citerefentry><refentrytitle>systemd.time</refentrytitle>
+                  <manvolnum>7</manvolnum></citerefentry>.
+                '';
+              };
+
+              aggressiveNetworking = mkOption {
+                type = types.bool;
+                default = false;
+                description = ''
+                  Upload data over multiple TCP connections, potentially
+                  increasing tarsnap's bandwidth utilisation at the cost
+                  of slowing down all other network traffic. Not
+                  recommended unless TCP congestion is the dominant
+                  limiting factor.
+                '';
+              };
+
+              directories = mkOption {
+                type = types.listOf types.path;
+                default = [];
+                description = "List of filesystem paths to archive.";
+              };
+
+              excludes = mkOption {
+                type = types.listOf types.str;
+                default = [];
+                description = ''
+                  Exclude files and directories matching these patterns.
+                '';
+              };
+
+              includes = mkOption {
+                type = types.listOf types.str;
+                default = [];
+                description = ''
+                  Include only files and directories matching these
+                  patterns (the empty list includes everything).
+
+                  Exclusions have precedence over inclusions.
+                '';
+              };
+
+              lowmem = mkOption {
+                type = types.bool;
+                default = false;
+                description = ''
+                  Reduce memory consumption by not caching small files.
+                  Possibly beneficial if the average file size is smaller
+                  than 1 MB and the number of files is lower than the
+                  total amount of RAM in KB.
+                '';
+              };
+
+              verylowmem = mkOption {
+                type = types.bool;
+                default = false;
+                description = ''
+                  Reduce memory consumption by a factor of 2 beyond what
+                  <literal>lowmem</literal> does, at the cost of significantly
+                  slowing down the archiving process.
+                '';
+              };
+
+              maxbw = mkOption {
+                type = types.nullOr types.int;
+                default = null;
+                description = ''
+                  Abort archival if upstream bandwidth usage in bytes
+                  exceeds this threshold.
+                '';
+              };
+
+              maxbwRateUp = mkOption {
+                type = types.nullOr types.int;
+                default = null;
+                example = literalExpression "25 * 1000";
+                description = ''
+                  Upload bandwidth rate limit in bytes.
+                '';
+              };
+
+              maxbwRateDown = mkOption {
+                type = types.nullOr types.int;
+                default = null;
+                example = literalExpression "50 * 1000";
+                description = ''
+                  Download bandwidth rate limit in bytes.
+                '';
+              };
+
+              verbose = mkOption {
+                type = types.bool;
+                default = false;
+                description = ''
+                  Whether to produce verbose logging output.
+                '';
+              };
+              explicitSymlinks = mkOption {
+                type = types.bool;
+                default = false;
+                description = ''
+                  Whether to follow symlinks specified as archives.
+                '';
+              };
+              followSymlinks = mkOption {
+                type = types.bool;
+                default = false;
+                description = ''
+                  Whether to follow all symlinks in archive trees.
+                '';
+              };
+            };
+          }
+        ));
+
+        default = {};
+
+        example = literalExpression ''
+          {
+            nixos =
+              { directories = [ "/home" "/root/ssl" ];
+              };
+
+            gamedata =
+              { directories = [ "/var/lib/minecraft" ];
+                period      = "*:30";
+              };
+          }
+        '';
+
+        description = ''
+          Tarsnap archive configurations. Each attribute names an archive
+          to be created at a given time interval, according to the options
+          associated with it. When uploading to the tarsnap server,
+          archive names are suffixed by a 1 second resolution timestamp,
+          with the format <literal>%Y%m%d%H%M%S</literal>.
+
+          For each member of the set is created a timer which triggers the
+          instanced <literal>tarsnap-archive-name</literal> service unit. You may use
+          <command>systemctl start tarsnap-archive-name</command> to
+          manually trigger creation of <literal>archive-name</literal> at
+          any time.
+        '';
+      };
+    };
+  };
+
+  config = mkIf gcfg.enable {
+    assertions =
+      (mapAttrsToList (name: cfg:
+        { assertion = cfg.directories != [];
+          message = "Must specify paths for tarsnap to back up";
+        }) gcfg.archives) ++
+      (mapAttrsToList (name: cfg:
+        { assertion = !(cfg.lowmem && cfg.verylowmem);
+          message = "You cannot set both lowmem and verylowmem";
+        }) gcfg.archives);
+
+    systemd.services =
+      (mapAttrs' (name: cfg: nameValuePair "tarsnap-${name}" {
+        description = "Tarsnap archive '${name}'";
+        requires    = [ "network-online.target" ];
+        after       = [ "network-online.target" ];
+
+        path = with pkgs; [ iputils tarsnap util-linux ];
+
+        # In order for the persistent tarsnap timer to work reliably, we have to
+        # make sure that the tarsnap server is reachable after systemd starts up
+        # the service - therefore we sleep in a loop until we can ping the
+        # endpoint.
+        preStart = ''
+          while ! ping -4 -q -c 1 v1-0-0-server.tarsnap.com &> /dev/null; do sleep 3; done
+        '';
+
+        script = let
+          tarsnap = ''tarsnap --configfile "/etc/tarsnap/${name}.conf"'';
+          run = ''${tarsnap} -c -f "${name}-$(date +"%Y%m%d%H%M%S")" \
+                        ${optionalString cfg.verbose "-v"} \
+                        ${optionalString cfg.explicitSymlinks "-H"} \
+                        ${optionalString cfg.followSymlinks "-L"} \
+                        ${concatStringsSep " " cfg.directories}'';
+          cachedir = escapeShellArg cfg.cachedir;
+          in if (cfg.cachedir != null) then ''
+            mkdir -p ${cachedir}
+            chmod 0700 ${cachedir}
+
+            ( flock 9
+              if [ ! -e ${cachedir}/firstrun ]; then
+                ( flock 10
+                  flock -u 9
+                  ${tarsnap} --fsck
+                  flock 9
+                ) 10>${cachedir}/firstrun
+              fi
+            ) 9>${cachedir}/lockf
+
+             exec flock ${cachedir}/firstrun ${run}
+          '' else "exec ${run}";
+
+        serviceConfig = {
+          Type = "oneshot";
+          IOSchedulingClass = "idle";
+          NoNewPrivileges = "true";
+          CapabilityBoundingSet = [ "CAP_DAC_READ_SEARCH" ];
+          PermissionsStartOnly = "true";
+        };
+      }) gcfg.archives) //
+
+      (mapAttrs' (name: cfg: nameValuePair "tarsnap-restore-${name}"{
+        description = "Tarsnap restore '${name}'";
+        requires    = [ "network-online.target" ];
+
+        path = with pkgs; [ iputils tarsnap util-linux ];
+
+        script = let
+          tarsnap = ''tarsnap --configfile "/etc/tarsnap/${name}.conf"'';
+          lastArchive = "$(${tarsnap} --list-archives | sort | tail -1)";
+          run = ''${tarsnap} -x -f "${lastArchive}" ${optionalString cfg.verbose "-v"}'';
+          cachedir = escapeShellArg cfg.cachedir;
+
+        in if (cfg.cachedir != null) then ''
+          mkdir -p ${cachedir}
+          chmod 0700 ${cachedir}
+
+          ( flock 9
+            if [ ! -e ${cachedir}/firstrun ]; then
+              ( flock 10
+                flock -u 9
+                ${tarsnap} --fsck
+                flock 9
+              ) 10>${cachedir}/firstrun
+            fi
+          ) 9>${cachedir}/lockf
+
+           exec flock ${cachedir}/firstrun ${run}
+        '' else "exec ${run}";
+
+        serviceConfig = {
+          Type = "oneshot";
+          IOSchedulingClass = "idle";
+          NoNewPrivileges = "true";
+          CapabilityBoundingSet = [ "CAP_DAC_READ_SEARCH" ];
+          PermissionsStartOnly = "true";
+        };
+      }) gcfg.archives);
+
+    # Note: the timer must be Persistent=true, so that systemd will start it even
+    # if e.g. your laptop was asleep while the latest interval occurred.
+    systemd.timers = mapAttrs' (name: cfg: nameValuePair "tarsnap-${name}"
+      { timerConfig.OnCalendar = cfg.period;
+        timerConfig.Persistent = "true";
+        wantedBy = [ "timers.target" ];
+      }) gcfg.archives;
+
+    environment.etc =
+      mapAttrs' (name: cfg: nameValuePair "tarsnap/${name}.conf"
+        { text = configFile name cfg;
+        }) gcfg.archives;
+
+    environment.systemPackages = [ pkgs.tarsnap ];
+  };
+}
diff --git a/nixos/modules/services/backup/tsm.nix b/nixos/modules/services/backup/tsm.nix
new file mode 100644
index 00000000000..4e690ac6ecd
--- /dev/null
+++ b/nixos/modules/services/backup/tsm.nix
@@ -0,0 +1,125 @@
+{ config, lib, ... }:
+
+let
+
+  inherit (lib.attrsets) hasAttr;
+  inherit (lib.modules) mkDefault mkIf;
+  inherit (lib.options) mkEnableOption mkOption;
+  inherit (lib.types) nonEmptyStr nullOr;
+
+  options.services.tsmBackup = {
+    enable = mkEnableOption ''
+      automatic backups with the
+      IBM Spectrum Protect (Tivoli Storage Manager, TSM) client.
+      This also enables
+      <option>programs.tsmClient.enable</option>
+    '';
+    command = mkOption {
+      type = nonEmptyStr;
+      default = "backup";
+      example = "incr";
+      description = ''
+        The actual command passed to the
+        <literal>dsmc</literal> executable to start the backup.
+      '';
+    };
+    servername = mkOption {
+      type = nonEmptyStr;
+      example = "mainTsmServer";
+      description = ''
+        Create a systemd system service
+        <literal>tsm-backup.service</literal> that starts
+        a backup based on the given servername's stanza.
+        Note that this server's
+        <option>passwdDir</option> will default to
+        <filename>/var/lib/tsm-backup/password</filename>
+        (but may be overridden);
+        also, the service will use
+        <filename>/var/lib/tsm-backup</filename> as
+        <literal>HOME</literal> when calling
+        <literal>dsmc</literal>.
+      '';
+    };
+    autoTime = mkOption {
+      type = nullOr nonEmptyStr;
+      default = null;
+      example = "12:00";
+      description = ''
+        The backup service will be invoked
+        automatically at the given date/time,
+        which must be in the format described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
+        The default <literal>null</literal>
+        disables automatic backups.
+      '';
+    };
+  };
+
+  cfg = config.services.tsmBackup;
+  cfgPrg = config.programs.tsmClient;
+
+  assertions = [
+    {
+      assertion = hasAttr cfg.servername cfgPrg.servers;
+      message = "TSM service servername not found in list of servers";
+    }
+    {
+      assertion = cfgPrg.servers.${cfg.servername}.genPasswd;
+      message = "TSM service requires automatic password generation";
+    }
+  ];
+
+in
+
+{
+
+  inherit options;
+
+  config = mkIf cfg.enable {
+    inherit assertions;
+    programs.tsmClient.enable = true;
+    programs.tsmClient.servers.${cfg.servername}.passwdDir =
+      mkDefault "/var/lib/tsm-backup/password";
+    systemd.services.tsm-backup = {
+      description = "IBM Spectrum Protect (Tivoli Storage Manager) Backup";
+      # DSM_LOG needs a trailing slash to have it treated as a directory.
+      # `/var/log` would be littered with TSM log files otherwise.
+      environment.DSM_LOG = "/var/log/tsm-backup/";
+      # TSM needs a HOME dir to store certificates.
+      environment.HOME = "/var/lib/tsm-backup";
+      serviceConfig = {
+        # for exit status description see
+        # https://www.ibm.com/docs/en/spectrum-protect/8.1.13?topic=clients-client-return-codes
+        SuccessExitStatus = "4 8";
+        # The `-se` option must come after the command.
+        # The `-optfile` option suppresses a `dsm.opt`-not-found warning.
+        ExecStart =
+          "${cfgPrg.wrappedPackage}/bin/dsmc ${cfg.command} -se='${cfg.servername}' -optfile=/dev/null";
+        LogsDirectory = "tsm-backup";
+        StateDirectory = "tsm-backup";
+        StateDirectoryMode = "0750";
+        # systemd sandboxing
+        LockPersonality = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        #PrivateTmp = true;  # would break backup of {/var,}/tmp
+        #PrivateUsers = true;  # would block backup of /home/*
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = "read-only";
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "noaccess";
+        ProtectSystem = "strict";
+        RestrictNamespaces = true;
+        RestrictSUIDSGID = true;
+      };
+      startAt = mkIf (cfg.autoTime!=null) cfg.autoTime;
+    };
+  };
+
+  meta.maintainers = [ lib.maintainers.yarny ];
+
+}
diff --git a/nixos/modules/services/backup/zfs-replication.nix b/nixos/modules/services/backup/zfs-replication.nix
new file mode 100644
index 00000000000..6d75774c78f
--- /dev/null
+++ b/nixos/modules/services/backup/zfs-replication.nix
@@ -0,0 +1,90 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.zfs.autoReplication;
+  recursive = optionalString cfg.recursive " --recursive";
+  followDelete = optionalString cfg.followDelete " --follow-delete";
+in {
+  options = {
+    services.zfs.autoReplication = {
+      enable = mkEnableOption "ZFS snapshot replication.";
+
+      followDelete = mkOption {
+        description = "Remove remote snapshots that don't have a local correspondant.";
+        default = true;
+        type = types.bool;
+      };
+
+      host = mkOption {
+        description = "Remote host where snapshots should be sent. <literal>lz4</literal> is expected to be installed on this host.";
+        example = "example.com";
+        type = types.str;
+      };
+
+      identityFilePath = mkOption {
+        description = "Path to SSH key used to login to host.";
+        example = "/home/username/.ssh/id_rsa";
+        type = types.path;
+      };
+
+      localFilesystem = mkOption {
+        description = "Local ZFS fileystem from which snapshots should be sent.  Defaults to the attribute name.";
+        example = "pool/file/path";
+        type = types.str;
+      };
+
+      remoteFilesystem = mkOption {
+        description = "Remote ZFS filesystem where snapshots should be sent.";
+        example = "pool/file/path";
+        type = types.str;
+      };
+
+      recursive = mkOption {
+        description = "Recursively discover snapshots to send.";
+        default = true;
+        type = types.bool;
+      };
+
+      username = mkOption {
+        description = "Username used by SSH to login to remote host.";
+        example = "username";
+        type = types.str;
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = [
+      pkgs.lz4
+    ];
+
+    systemd.services.zfs-replication = {
+      after = [
+        "zfs-snapshot-daily.service"
+        "zfs-snapshot-frequent.service"
+        "zfs-snapshot-hourly.service"
+        "zfs-snapshot-monthly.service"
+        "zfs-snapshot-weekly.service"
+      ];
+      description = "ZFS Snapshot Replication";
+      documentation = [
+        "https://github.com/alunduil/zfs-replicate"
+      ];
+      restartIfChanged = false;
+      serviceConfig.ExecStart = "${pkgs.zfs-replicate}/bin/zfs-replicate${recursive} -l ${escapeShellArg cfg.username} -i ${escapeShellArg cfg.identityFilePath}${followDelete} ${escapeShellArg cfg.host} ${escapeShellArg cfg.remoteFilesystem} ${escapeShellArg cfg.localFilesystem}";
+      wantedBy = [
+        "zfs-snapshot-daily.service"
+        "zfs-snapshot-frequent.service"
+        "zfs-snapshot-hourly.service"
+        "zfs-snapshot-monthly.service"
+        "zfs-snapshot-weekly.service"
+      ];
+    };
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ alunduil ];
+  };
+}
diff --git a/nixos/modules/services/backup/znapzend.nix b/nixos/modules/services/backup/znapzend.nix
new file mode 100644
index 00000000000..09e60177c39
--- /dev/null
+++ b/nixos/modules/services/backup/znapzend.nix
@@ -0,0 +1,469 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+with types;
+
+let
+
+  planDescription = ''
+      The znapzend backup plan to use for the source.
+
+      The plan specifies how often to backup and for how long to keep the
+      backups. It consists of a series of retention periodes to interval
+      associations:
+
+      <literal>
+        retA=>intA,retB=>intB,...
+      </literal>
+
+      Both intervals and retention periods are expressed in standard units
+      of time or multiples of them. You can use both the full name or a
+      shortcut according to the following listing:
+
+      <literal>
+        second|sec|s, minute|min, hour|h, day|d, week|w, month|mon|m, year|y
+      </literal>
+
+      See <citerefentry><refentrytitle>znapzendzetup</refentrytitle><manvolnum>1</manvolnum></citerefentry> for more info.
+  '';
+  planExample = "1h=>10min,1d=>1h,1w=>1d,1m=>1w,1y=>1m";
+
+  # A type for a string of the form number{b|k|M|G}
+  mbufferSizeType = str // {
+    check = x: str.check x && builtins.isList (builtins.match "^[0-9]+[bkMG]$" x);
+    description = "string of the form number{b|k|M|G}";
+  };
+
+  enabledFeatures = concatLists (mapAttrsToList (name: enabled: optional enabled name) cfg.features);
+
+  # Type for a string that must contain certain other strings (the list parameter).
+  # Note that these would need regex escaping.
+  stringContainingStrings = list: let
+    matching = s: map (str: builtins.match ".*${str}.*" s) list;
+  in str // {
+    check = x: str.check x && all isList (matching x);
+    description = "string containing all of the characters ${concatStringsSep ", " list}";
+  };
+
+  timestampType = stringContainingStrings [ "%Y" "%m" "%d" "%H" "%M" "%S" ];
+
+  destType = srcConfig: submodule ({ name, ... }: {
+    options = {
+
+      label = mkOption {
+        type = str;
+        description = "Label for this destination. Defaults to the attribute name.";
+      };
+
+      plan = mkOption {
+        type = str;
+        description = planDescription;
+        example = planExample;
+      };
+
+      dataset = mkOption {
+        type = str;
+        description = "Dataset name to send snapshots to.";
+        example = "tank/main";
+      };
+
+      host = mkOption {
+        type = nullOr str;
+        description = ''
+          Host to use for the destination dataset. Can be prefixed with
+          <literal>user@</literal> to specify the ssh user.
+        '';
+        default = null;
+        example = "john@example.com";
+      };
+
+      presend = mkOption {
+        type = nullOr str;
+        description = ''
+          Command to run before sending the snapshot to the destination.
+          Intended to run a remote script via <command>ssh</command> on the
+          destination, e.g. to bring up a backup disk or server or to put a
+          zpool online/offline. See also <option>postsend</option>.
+        '';
+        default = null;
+        example = "ssh root@bserv zpool import -Nf tank";
+      };
+
+      postsend = mkOption {
+        type = nullOr str;
+        description = ''
+          Command to run after sending the snapshot to the destination.
+          Intended to run a remote script via <command>ssh</command> on the
+          destination, e.g. to bring up a backup disk or server or to put a
+          zpool online/offline. See also <option>presend</option>.
+        '';
+        default = null;
+        example = "ssh root@bserv zpool export tank";
+      };
+    };
+
+    config = {
+      label = mkDefault name;
+      plan = mkDefault srcConfig.plan;
+    };
+  });
+
+
+
+  srcType = submodule ({ name, config, ... }: {
+    options = {
+
+      enable = mkOption {
+        type = bool;
+        description = "Whether to enable this source.";
+        default = true;
+      };
+
+      recursive = mkOption {
+        type = bool;
+        description = "Whether to do recursive snapshots.";
+        default = false;
+      };
+
+      mbuffer = {
+        enable = mkOption {
+          type = bool;
+          description = "Whether to use <command>mbuffer</command>.";
+          default = false;
+        };
+
+        port = mkOption {
+          type = nullOr ints.u16;
+          description = ''
+              Port to use for <command>mbuffer</command>.
+
+              If this is null, it will run <command>mbuffer</command> through
+              ssh.
+
+              If this is not null, it will run <command>mbuffer</command>
+              directly through TCP, which is not encrypted but faster. In that
+              case the given port needs to be open on the destination host.
+          '';
+          default = null;
+        };
+
+        size = mkOption {
+          type = mbufferSizeType;
+          description = ''
+            The size for <command>mbuffer</command>.
+            Supports the units b, k, M, G.
+          '';
+          default = "1G";
+          example = "128M";
+        };
+      };
+
+      presnap = mkOption {
+        type = nullOr str;
+        description = ''
+          Command to run before snapshots are taken on the source dataset,
+          e.g. for database locking/flushing. See also
+          <option>postsnap</option>.
+        '';
+        default = null;
+        example = literalExpression ''
+          '''''${pkgs.mariadb}/bin/mysql -e "set autocommit=0;flush tables with read lock;\\! ''${pkgs.coreutils}/bin/sleep 600" &  ''${pkgs.coreutils}/bin/echo $! > /tmp/mariadblock.pid ; sleep 10'''
+        '';
+      };
+
+      postsnap = mkOption {
+        type = nullOr str;
+        description = ''
+          Command to run after snapshots are taken on the source dataset,
+          e.g. for database unlocking. See also <option>presnap</option>.
+        '';
+        default = null;
+        example = literalExpression ''
+          "''${pkgs.coreutils}/bin/kill `''${pkgs.coreutils}/bin/cat /tmp/mariadblock.pid`;''${pkgs.coreutils}/bin/rm /tmp/mariadblock.pid"
+        '';
+      };
+
+      timestampFormat = mkOption {
+        type = timestampType;
+        description = ''
+          The timestamp format to use for constructing snapshot names.
+          The syntax is <literal>strftime</literal>-like. The string must
+          consist of the mandatory <literal>%Y %m %d %H %M %S</literal>.
+          Optionally  <literal>- _ . :</literal>  characters as well as any
+          alphanumeric character are allowed. If suffixed by a
+          <literal>Z</literal>, times will be in UTC.
+        '';
+        default = "%Y-%m-%d-%H%M%S";
+        example = "znapzend-%m.%d.%Y-%H%M%SZ";
+      };
+
+      sendDelay = mkOption {
+        type = int;
+        description = ''
+          Specify delay (in seconds) before sending snaps to the destination.
+          May be useful if you want to control sending time.
+        '';
+        default = 0;
+        example = 60;
+      };
+
+      plan = mkOption {
+        type = str;
+        description = planDescription;
+        example = planExample;
+      };
+
+      dataset = mkOption {
+        type = str;
+        description = "The dataset to use for this source.";
+        example = "tank/home";
+      };
+
+      destinations = mkOption {
+        type = attrsOf (destType config);
+        description = "Additional destinations.";
+        default = {};
+        example = literalExpression ''
+          {
+            local = {
+              dataset = "btank/backup";
+              presend = "zpool import -N btank";
+              postsend = "zpool export btank";
+            };
+            remote = {
+              host = "john@example.com";
+              dataset = "tank/john";
+            };
+          };
+        '';
+      };
+    };
+
+    config = {
+      dataset = mkDefault name;
+    };
+
+  });
+
+  ### Generating the configuration from here
+
+  cfg = config.services.znapzend;
+
+  onOff = b: if b then "on" else "off";
+  nullOff = b: if b == null then "off" else toString b;
+  stripSlashes = replaceStrings [ "/" ] [ "." ];
+
+  attrsToFile = config: concatStringsSep "\n" (builtins.attrValues (
+    mapAttrs (n: v: "${n}=${v}") config));
+
+  mkDestAttrs = dst: with dst;
+    mapAttrs' (n: v: nameValuePair "dst_${label}${n}" v) ({
+      "" = optionalString (host != null) "${host}:" + dataset;
+      _plan = plan;
+    } // optionalAttrs (presend != null) {
+      _precmd = presend;
+    } // optionalAttrs (postsend != null) {
+      _pstcmd = postsend;
+    });
+
+  mkSrcAttrs = srcCfg: with srcCfg; {
+    enabled = onOff enable;
+    # mbuffer is not referenced by its full path to accomodate non-NixOS systems or differing mbuffer versions between source and target
+    mbuffer = with mbuffer; if enable then "mbuffer"
+        + optionalString (port != null) ":${toString port}" else "off";
+    mbuffer_size = mbuffer.size;
+    post_znap_cmd = nullOff postsnap;
+    pre_znap_cmd = nullOff presnap;
+    recursive = onOff recursive;
+    src = dataset;
+    src_plan = plan;
+    tsformat = timestampFormat;
+    zend_delay = toString sendDelay;
+  } // foldr (a: b: a // b) {} (
+    map mkDestAttrs (builtins.attrValues destinations)
+  );
+
+  files = mapAttrs' (n: srcCfg: let
+    fileText = attrsToFile (mkSrcAttrs srcCfg);
+  in {
+    name = srcCfg.dataset;
+    value = pkgs.writeText (stripSlashes srcCfg.dataset) fileText;
+  }) cfg.zetup;
+
+in
+{
+  options = {
+    services.znapzend = {
+      enable = mkEnableOption "ZnapZend ZFS backup daemon";
+
+      logLevel = mkOption {
+        default = "debug";
+        example = "warning";
+        type = enum ["debug" "info" "warning" "err" "alert"];
+        description = ''
+          The log level when logging to file. Any of debug, info, warning, err,
+          alert. Default in daemonized form is debug.
+        '';
+      };
+
+      logTo = mkOption {
+        type = str;
+        default = "syslog::daemon";
+        example = "/var/log/znapzend.log";
+        description = ''
+          Where to log to (syslog::&lt;facility&gt; or &lt;filepath&gt;).
+        '';
+      };
+
+      noDestroy = mkOption {
+        type = bool;
+        default = false;
+        description = "Does all changes to the filesystem except destroy.";
+      };
+
+      autoCreation = mkOption {
+        type = bool;
+        default = false;
+        description = "Automatically create the destination dataset if it does not exist.";
+      };
+
+      zetup = mkOption {
+        type = attrsOf srcType;
+        description = "Znapzend configuration.";
+        default = {};
+        example = literalExpression ''
+          {
+            "tank/home" = {
+              # Make snapshots of tank/home every hour, keep those for 1 day,
+              # keep every days snapshot for 1 month, etc.
+              plan = "1d=>1h,1m=>1d,1y=>1m";
+              recursive = true;
+              # Send all those snapshots to john@example.com:rtank/john as well
+              destinations.remote = {
+                host = "john@example.com";
+                dataset = "rtank/john";
+              };
+            };
+          };
+        '';
+      };
+
+      pure = mkOption {
+        type = bool;
+        description = ''
+          Do not persist any stateful znapzend setups. If this option is
+          enabled, your previously set znapzend setups will be cleared and only
+          the ones defined with this module will be applied.
+        '';
+        default = false;
+      };
+
+      features.oracleMode = mkEnableOption ''
+        Destroy snapshots one by one instead of using one long argument list.
+        If source and destination are out of sync for a long time, you may have
+        so many snapshots to destroy that the argument gets is too long and the
+        command fails.
+      '';
+      features.recvu = mkEnableOption ''
+        recvu feature which uses <literal>-u</literal> on the receiving end to keep the destination
+        filesystem unmounted.
+      '';
+      features.compressed = mkEnableOption ''
+        compressed feature which adds the options <literal>-Lce</literal> to
+        the <command>zfs send</command> command. When this is enabled, make
+        sure that both the sending and receiving pool have the same relevant
+        features enabled. Using <literal>-c</literal> will skip unneccessary
+        decompress-compress stages, <literal>-L</literal> is for large block
+        support and -e is for embedded data support. see
+        <citerefentry><refentrytitle>znapzend</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+        and <citerefentry><refentrytitle>zfs</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+        for more info.
+      '';
+      features.sendRaw = mkEnableOption ''
+        sendRaw feature which adds the options <literal>-w</literal> to the
+        <command>zfs send</command> command. For encrypted source datasets this
+        instructs zfs not to decrypt before sending which results in a remote
+        backup that can't be read without the encryption key/passphrase, useful
+        when the remote isn't fully trusted or not physically secure. This
+        option must be used consistently, raw incrementals cannot be based on
+        non-raw snapshots and vice versa.
+      '';
+      features.skipIntermediates = mkEnableOption ''
+        Enable the skipIntermediates feature to send a single increment
+        between latest common snapshot and the newly made one. It may skip
+        several source snaps if the destination was offline for some time, and
+        it should skip snapshots not managed by znapzend. Normally for online
+        destinations, the new snapshot is sent as soon as it is created on the
+        source, so there are no automatic increments to skip.
+      '';
+      features.lowmemRecurse = mkEnableOption ''
+        use lowmemRecurse on systems where you have too many datasets, so a
+        recursive listing of attributes to find backup plans exhausts the
+        memory available to <command>znapzend</command>: instead, go the slower
+        way to first list all impacted dataset names, and then query their
+        configs one by one.
+      '';
+      features.zfsGetType = mkEnableOption ''
+        use zfsGetType if your <command>zfs get</command> supports a
+        <literal>-t</literal> argument for filtering by dataset type at all AND
+        lists properties for snapshots by default when recursing, so that there
+        is too much data to process while searching for backup plans.
+        If these two conditions apply to your system, the time needed for a
+        <literal>--recursive</literal> search for backup plans can literally
+        differ by hundreds of times (depending on the amount of snapshots in
+        that dataset tree... and a decent backup plan will ensure you have a lot
+        of those), so you would benefit from requesting this feature.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.znapzend ];
+
+    systemd.services = {
+      znapzend = {
+        description = "ZnapZend - ZFS Backup System";
+        wantedBy    = [ "zfs.target" ];
+        after       = [ "zfs.target" ];
+
+        path = with pkgs; [ zfs mbuffer openssh ];
+
+        preStart = optionalString cfg.pure ''
+          echo Resetting znapzend zetups
+          ${pkgs.znapzend}/bin/znapzendzetup list \
+            | grep -oP '(?<=\*\*\* backup plan: ).*(?= \*\*\*)' \
+            | xargs -I{} ${pkgs.znapzend}/bin/znapzendzetup delete "{}"
+        '' + concatStringsSep "\n" (mapAttrsToList (dataset: config: ''
+          echo Importing znapzend zetup ${config} for dataset ${dataset}
+          ${pkgs.znapzend}/bin/znapzendzetup import --write ${dataset} ${config} &
+        '') files) + ''
+          wait
+        '';
+
+        serviceConfig = {
+          # znapzendzetup --import apparently tries to connect to the backup
+          # host 3 times with a timeout of 30 seconds, leading to a startup
+          # delay of >90s when the host is down, which is just above the default
+          # service timeout of 90 seconds. Increase the timeout so it doesn't
+          # make the service fail in that case.
+          TimeoutStartSec = 180;
+          # Needs to have write access to ZFS
+          User = "root";
+          ExecStart = let
+            args = concatStringsSep " " [
+              "--logto=${cfg.logTo}"
+              "--loglevel=${cfg.logLevel}"
+              (optionalString cfg.noDestroy "--nodestroy")
+              (optionalString cfg.autoCreation "--autoCreation")
+              (optionalString (enabledFeatures != [])
+                "--features=${concatStringsSep "," enabledFeatures}")
+            ]; in "${pkgs.znapzend}/bin/znapzend ${args}";
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          Restart = "on-failure";
+        };
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ infinisil SlothOfAnarchy ];
+}
diff --git a/nixos/modules/services/backup/zrepl.nix b/nixos/modules/services/backup/zrepl.nix
new file mode 100644
index 00000000000..4356479b663
--- /dev/null
+++ b/nixos/modules/services/backup/zrepl.nix
@@ -0,0 +1,54 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.zrepl;
+  format = pkgs.formats.yaml { };
+  configFile = format.generate "zrepl.yml" cfg.settings;
+in
+{
+  meta.maintainers = with maintainers; [ cole-h ];
+
+  options = {
+    services.zrepl = {
+      enable = mkEnableOption "zrepl";
+
+      settings = mkOption {
+        default = { };
+        description = ''
+          Configuration for zrepl. See <link
+          xlink:href="https://zrepl.github.io/configuration.html"/>
+          for more information.
+        '';
+        type = types.submodule {
+          freeformType = format.type;
+        };
+      };
+    };
+  };
+
+  ### Implementation ###
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.zrepl ];
+
+    # zrepl looks for its config in this location by default. This
+    # allows the use of e.g. `zrepl signal wakeup <job>` without having
+    # to specify the storepath of the config.
+    environment.etc."zrepl/zrepl.yml".source = configFile;
+
+    systemd.packages = [ pkgs.zrepl ];
+    systemd.services.zrepl = {
+      requires = [ "local-fs.target" ];
+      wantedBy = [ "zfs.target" ];
+      after = [ "zfs.target" ];
+
+      path = [ config.boot.zfs.package ];
+      restartTriggers = [ configFile ];
+
+      serviceConfig = {
+        Restart = "on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/blockchain/ethereum/geth.nix b/nixos/modules/services/blockchain/ethereum/geth.nix
new file mode 100644
index 00000000000..bf2cf1edd4d
--- /dev/null
+++ b/nixos/modules/services/blockchain/ethereum/geth.nix
@@ -0,0 +1,179 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  eachGeth = config.services.geth;
+
+  gethOpts = { config, lib, name, ...}: {
+
+    options = {
+
+      enable = lib.mkEnableOption "Go Ethereum Node";
+
+      port = mkOption {
+        type = types.port;
+        default = 30303;
+        description = "Port number Go Ethereum will be listening on, both TCP and UDP.";
+      };
+
+      http = {
+        enable = lib.mkEnableOption "Go Ethereum HTTP API";
+        address = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "Listen address of Go Ethereum HTTP API.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8545;
+          description = "Port number of Go Ethereum HTTP API.";
+        };
+
+        apis = mkOption {
+          type = types.nullOr (types.listOf types.str);
+          default = null;
+          description = "APIs to enable over WebSocket";
+          example = ["net" "eth"];
+        };
+      };
+
+      websocket = {
+        enable = lib.mkEnableOption "Go Ethereum WebSocket API";
+        address = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "Listen address of Go Ethereum WebSocket API.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8546;
+          description = "Port number of Go Ethereum WebSocket API.";
+        };
+
+        apis = mkOption {
+          type = types.nullOr (types.listOf types.str);
+          default = null;
+          description = "APIs to enable over WebSocket";
+          example = ["net" "eth"];
+        };
+      };
+
+      metrics = {
+        enable = lib.mkEnableOption "Go Ethereum prometheus metrics";
+        address = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "Listen address of Go Ethereum metrics service.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 6060;
+          description = "Port number of Go Ethereum metrics service.";
+        };
+      };
+
+      network = mkOption {
+        type = types.nullOr (types.enum [ "goerli" "rinkeby" "yolov2" "ropsten" ]);
+        default = null;
+        description = "The network to connect to. Mainnet (null) is the default ethereum network.";
+      };
+
+      syncmode = mkOption {
+        type = types.enum [ "snap" "fast" "full" "light" ];
+        default = "snap";
+        description = "Blockchain sync mode.";
+      };
+
+      gcmode = mkOption {
+        type = types.enum [ "full" "archive" ];
+        default = "full";
+        description = "Blockchain garbage collection mode.";
+      };
+
+      maxpeers = mkOption {
+        type = types.int;
+        default = 50;
+        description = "Maximum peers to connect to.";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        description = "Additional arguments passed to Go Ethereum.";
+        default = [];
+      };
+
+      package = mkOption {
+        default = pkgs.go-ethereum.geth;
+        defaultText = literalExpression "pkgs.go-ethereum.geth";
+        type = types.package;
+        description = "Package to use as Go Ethereum node.";
+      };
+    };
+  };
+in
+
+{
+
+  ###### interface
+
+  options = {
+    services.geth = mkOption {
+      type = types.attrsOf (types.submodule gethOpts);
+      default = {};
+      description = "Specification of one or more geth instances.";
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf (eachGeth != {}) {
+
+    environment.systemPackages = flatten (mapAttrsToList (gethName: cfg: [
+      cfg.package
+    ]) eachGeth);
+
+    systemd.services = mapAttrs' (gethName: cfg: (
+      nameValuePair "geth-${gethName}" (mkIf cfg.enable {
+      description = "Go Ethereum node (${gethName})";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+        StateDirectory = "goethereum/${gethName}/${if (cfg.network == null) then "mainnet" else cfg.network}";
+
+        # Hardening measures
+        PrivateTmp = "true";
+        ProtectSystem = "full";
+        NoNewPrivileges = "true";
+        PrivateDevices = "true";
+        MemoryDenyWriteExecute = "true";
+      };
+
+      script = ''
+        ${cfg.package}/bin/geth \
+          --nousb \
+          --ipcdisable \
+          ${optionalString (cfg.network != null) ''--${cfg.network}''} \
+          --syncmode ${cfg.syncmode} \
+          --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.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.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}''} \
+          ${lib.escapeShellArgs cfg.extraArgs} \
+          --datadir /var/lib/goethereum/${gethName}/${if (cfg.network == null) then "mainnet" else cfg.network}
+      '';
+    }))) eachGeth;
+
+  };
+
+}
diff --git a/nixos/modules/services/cluster/corosync/default.nix b/nixos/modules/services/cluster/corosync/default.nix
new file mode 100644
index 00000000000..b4144917fee
--- /dev/null
+++ b/nixos/modules/services/cluster/corosync/default.nix
@@ -0,0 +1,112 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.corosync;
+in
+{
+  # interface
+  options.services.corosync = {
+    enable = mkEnableOption "corosync";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.corosync;
+      defaultText = literalExpression "pkgs.corosync";
+      description = "Package that should be used for corosync.";
+    };
+
+    clusterName = mkOption {
+      type = types.str;
+      default = "nixcluster";
+      description = "Name of the corosync cluster.";
+    };
+
+    extraOptions = mkOption {
+      type = with types; listOf str;
+      default = [];
+      description = "Additional options with which to start corosync.";
+    };
+
+    nodelist = mkOption {
+      description = "Corosync nodelist: all cluster members.";
+      default = [];
+      type = with types; listOf (submodule {
+        options = {
+          nodeid = mkOption {
+            type = int;
+            description = "Node ID number";
+          };
+          name = mkOption {
+            type = str;
+            description = "Node name";
+          };
+          ring_addrs = mkOption {
+            type = listOf str;
+            description = "List of addresses, one for each ring.";
+          };
+        };
+      });
+    };
+  };
+
+  # implementation
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    environment.etc."corosync/corosync.conf".text = ''
+      totem {
+        version: 2
+        secauth: on
+        cluster_name: ${cfg.clusterName}
+        transport: knet
+      }
+
+      nodelist {
+        ${concatMapStrings ({ nodeid, name, ring_addrs }: ''
+          node {
+            nodeid: ${toString nodeid}
+            name: ${name}
+            ${concatStrings (imap0 (i: addr: ''
+              ring${toString i}_addr: ${addr}
+            '') ring_addrs)}
+          }
+        '') cfg.nodelist}
+      }
+
+      quorum {
+        # only corosync_votequorum is supported
+        provider: corosync_votequorum
+        wait_for_all: 0
+        ${optionalString (builtins.length cfg.nodelist < 3) ''
+          two_node: 1
+        ''}
+      }
+
+      logging {
+        to_syslog: yes
+      }
+    '';
+
+    environment.etc."corosync/uidgid.d/root".text = ''
+      # allow pacemaker connection by root
+      uidgid {
+        uid: 0
+        gid: 0
+      }
+    '';
+
+    systemd.packages = [ cfg.package ];
+    systemd.services.corosync = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        StateDirectory = "corosync";
+        StateDirectoryMode = "0700";
+      };
+    };
+
+    environment.etc."sysconfig/corosync".text = lib.optionalString (cfg.extraOptions != []) ''
+      COROSYNC_OPTIONS="${lib.escapeShellArgs cfg.extraOptions}"
+    '';
+  };
+}
diff --git a/nixos/modules/services/cluster/hadoop/conf.nix b/nixos/modules/services/cluster/hadoop/conf.nix
new file mode 100644
index 00000000000..e3c26a0d550
--- /dev/null
+++ b/nixos/modules/services/cluster/hadoop/conf.nix
@@ -0,0 +1,44 @@
+{ cfg, pkgs, lib }:
+let
+  propertyXml = name: value: lib.optionalString (value != null) ''
+    <property>
+      <name>${name}</name>
+      <value>${builtins.toString value}</value>
+    </property>
+  '';
+  siteXml = fileName: properties: pkgs.writeTextDir fileName ''
+    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
+    <!-- generated by NixOS -->
+    <configuration>
+      ${builtins.concatStringsSep "\n" (pkgs.lib.mapAttrsToList propertyXml properties)}
+    </configuration>
+  '';
+  cfgLine = name: value: ''
+    ${name}=${builtins.toString value}
+  '';
+  cfgFile = fileName: properties: pkgs.writeTextDir fileName ''
+    # generated by NixOS
+    ${builtins.concatStringsSep "" (pkgs.lib.mapAttrsToList cfgLine properties)}
+  '';
+  userFunctions = ''
+    hadoop_verify_logdir() {
+      echo Skipping verification of log directory
+    }
+  '';
+  hadoopEnv = ''
+    export HADOOP_LOG_DIR=/tmp/hadoop/$USER
+  '';
+in
+pkgs.runCommand "hadoop-conf" {} (with cfg; ''
+  mkdir -p $out/
+  cp ${siteXml "core-site.xml" (coreSite // coreSiteInternal)}/* $out/
+  cp ${siteXml "hdfs-site.xml" (hdfsSiteDefault // hdfsSite // hdfsSiteInternal)}/* $out/
+  cp ${siteXml "mapred-site.xml" (mapredSiteDefault // mapredSite)}/* $out/
+  cp ${siteXml "yarn-site.xml" (yarnSiteDefault // yarnSite // yarnSiteInternal)}/* $out/
+  cp ${siteXml "httpfs-site.xml" httpfsSite}/* $out/
+  cp ${cfgFile "container-executor.cfg" containerExecutorCfg}/* $out/
+  cp ${pkgs.writeTextDir "hadoop-user-functions.sh" userFunctions}/* $out/
+  cp ${pkgs.writeTextDir "hadoop-env.sh" hadoopEnv}/* $out/
+  cp ${log4jProperties} $out/log4j.properties
+  ${lib.concatMapStringsSep "\n" (dir: "cp -r ${dir}/* $out/") extraConfDirs}
+'')
diff --git a/nixos/modules/services/cluster/hadoop/default.nix b/nixos/modules/services/cluster/hadoop/default.nix
new file mode 100644
index 00000000000..a4fdea81037
--- /dev/null
+++ b/nixos/modules/services/cluster/hadoop/default.nix
@@ -0,0 +1,223 @@
+{ config, lib, options, pkgs, ...}:
+let
+  cfg = config.services.hadoop;
+  opt = options.services.hadoop;
+in
+with lib;
+{
+  imports = [ ./yarn.nix ./hdfs.nix ];
+
+  options.services.hadoop = {
+    coreSite = mkOption {
+      default = {};
+      type = types.attrsOf types.anything;
+      example = literalExpression ''
+        {
+          "fs.defaultFS" = "hdfs://localhost";
+        }
+      '';
+      description = ''
+        Hadoop core-site.xml definition
+        <link xlink:href="https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-common/core-default.xml"/>
+      '';
+    };
+    coreSiteInternal = mkOption {
+      default = {};
+      type = types.attrsOf types.anything;
+      internal = true;
+      description = ''
+        Internal option to add configs to core-site.xml based on module options
+      '';
+    };
+
+    hdfsSiteDefault = mkOption {
+      default = {
+        "dfs.namenode.rpc-bind-host" = "0.0.0.0";
+        "dfs.namenode.http-address" = "0.0.0.0:9870";
+        "dfs.namenode.servicerpc-bind-host" = "0.0.0.0";
+        "dfs.namenode.http-bind-host" = "0.0.0.0";
+      };
+      type = types.attrsOf types.anything;
+      description = ''
+        Default options for hdfs-site.xml
+      '';
+    };
+    hdfsSite = mkOption {
+      default = {};
+      type = types.attrsOf types.anything;
+      example = literalExpression ''
+        {
+          "dfs.nameservices" = "namenode1";
+        }
+      '';
+      description = ''
+        Additional options and overrides for hdfs-site.xml
+        <link xlink:href="https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/hdfs-default.xml"/>
+      '';
+    };
+    hdfsSiteInternal = mkOption {
+      default = {};
+      type = types.attrsOf types.anything;
+      internal = true;
+      description = ''
+        Internal option to add configs to hdfs-site.xml based on module options
+      '';
+    };
+
+    mapredSiteDefault = mkOption {
+      default = {
+        "mapreduce.framework.name" = "yarn";
+        "yarn.app.mapreduce.am.env" = "HADOOP_MAPRED_HOME=${cfg.package}/lib/${cfg.package.untarDir}";
+        "mapreduce.map.env" = "HADOOP_MAPRED_HOME=${cfg.package}/lib/${cfg.package.untarDir}";
+        "mapreduce.reduce.env" = "HADOOP_MAPRED_HOME=${cfg.package}/lib/${cfg.package.untarDir}";
+      };
+      defaultText = literalExpression ''
+        {
+          "mapreduce.framework.name" = "yarn";
+          "yarn.app.mapreduce.am.env" = "HADOOP_MAPRED_HOME=''${config.${opt.package}}/lib/''${config.${opt.package}.untarDir}";
+          "mapreduce.map.env" = "HADOOP_MAPRED_HOME=''${config.${opt.package}}/lib/''${config.${opt.package}.untarDir}";
+          "mapreduce.reduce.env" = "HADOOP_MAPRED_HOME=''${config.${opt.package}}/lib/''${config.${opt.package}.untarDir}";
+        }
+      '';
+      type = types.attrsOf types.anything;
+      description = ''
+        Default options for mapred-site.xml
+      '';
+    };
+    mapredSite = mkOption {
+      default = {};
+      type = types.attrsOf types.anything;
+      example = literalExpression ''
+        {
+          "mapreduce.map.java.opts" = "-Xmx900m -XX:+UseParallelGC";
+        }
+      '';
+      description = ''
+        Additional options and overrides for mapred-site.xml
+        <link xlink:href="https://hadoop.apache.org/docs/current/hadoop-mapreduce-client/hadoop-mapreduce-client-core/mapred-default.xml"/>
+      '';
+    };
+
+    yarnSiteDefault = mkOption {
+      default = {
+        "yarn.nodemanager.admin-env" = "PATH=$PATH";
+        "yarn.nodemanager.aux-services" = "mapreduce_shuffle";
+        "yarn.nodemanager.aux-services.mapreduce_shuffle.class" = "org.apache.hadoop.mapred.ShuffleHandler";
+        "yarn.nodemanager.bind-host" = "0.0.0.0";
+        "yarn.nodemanager.container-executor.class" = "org.apache.hadoop.yarn.server.nodemanager.LinuxContainerExecutor";
+        "yarn.nodemanager.env-whitelist" = "JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_HOME,LANG,TZ";
+        "yarn.nodemanager.linux-container-executor.group" = "hadoop";
+        "yarn.nodemanager.linux-container-executor.path" = "/run/wrappers/yarn-nodemanager/bin/container-executor";
+        "yarn.nodemanager.log-dirs" = "/var/log/hadoop/yarn/nodemanager";
+        "yarn.resourcemanager.bind-host" = "0.0.0.0";
+        "yarn.resourcemanager.scheduler.class" = "org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler";
+      };
+      type = types.attrsOf types.anything;
+      description = ''
+        Default options for yarn-site.xml
+      '';
+    };
+    yarnSite = mkOption {
+      default = {};
+      type = types.attrsOf types.anything;
+      example = literalExpression ''
+        {
+          "yarn.resourcemanager.hostname" = "''${config.networking.hostName}";
+        }
+      '';
+      description = ''
+        Additional options and overrides for yarn-site.xml
+        <link xlink:href="https://hadoop.apache.org/docs/current/hadoop-yarn/hadoop-yarn-common/yarn-default.xml"/>
+      '';
+    };
+    yarnSiteInternal = mkOption {
+      default = {};
+      type = types.attrsOf types.anything;
+      internal = true;
+      description = ''
+        Internal option to add configs to yarn-site.xml based on module options
+      '';
+    };
+
+    httpfsSite = mkOption {
+      default = { };
+      type = types.attrsOf types.anything;
+      example = literalExpression ''
+        {
+          "hadoop.http.max.threads" = 500;
+        }
+      '';
+      description = ''
+        Hadoop httpfs-site.xml definition
+        <link xlink:href="https://hadoop.apache.org/docs/current/hadoop-hdfs-httpfs/httpfs-default.html"/>
+      '';
+    };
+
+    log4jProperties = mkOption {
+      default = "${cfg.package}/lib/${cfg.package.untarDir}/etc/hadoop/log4j.properties";
+      defaultText = literalExpression ''
+        "''${config.${opt.package}}/lib/''${config.${opt.package}.untarDir}/etc/hadoop/log4j.properties"
+      '';
+      type = types.path;
+      example = literalExpression ''
+        "''${pkgs.hadoop}/lib/''${pkgs.hadoop.untarDir}/etc/hadoop/log4j.properties";
+      '';
+      description = "log4j.properties file added to HADOOP_CONF_DIR";
+    };
+
+    containerExecutorCfg = mkOption {
+      default = {
+        # must be the same as yarn.nodemanager.linux-container-executor.group in yarnSite
+        "yarn.nodemanager.linux-container-executor.group"="hadoop";
+        "min.user.id"=1000;
+        "feature.terminal.enabled"=1;
+        "feature.mount-cgroup.enabled" = 1;
+      };
+      type = types.attrsOf types.anything;
+      example = literalExpression ''
+        options.services.hadoop.containerExecutorCfg.default // {
+          "feature.terminal.enabled" = 0;
+        }
+      '';
+      description = ''
+        Yarn container-executor.cfg definition
+        <link xlink:href="https://hadoop.apache.org/docs/r2.7.2/hadoop-yarn/hadoop-yarn-site/SecureContainer.html"/>
+      '';
+    };
+
+    extraConfDirs = mkOption {
+      default = [];
+      type = types.listOf types.path;
+      example = literalExpression ''
+        [
+          ./extraHDFSConfs
+          ./extraYARNConfs
+        ]
+      '';
+      description = "Directories containing additional config files to be added to HADOOP_CONF_DIR";
+    };
+
+    gatewayRole.enable = mkEnableOption "gateway role for deploying hadoop configs";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.hadoop;
+      defaultText = literalExpression "pkgs.hadoop";
+      description = "";
+    };
+  };
+
+
+  config = mkIf cfg.gatewayRole.enable {
+    users.groups.hadoop = {
+      gid = config.ids.gids.hadoop;
+    };
+    environment = {
+      systemPackages = [ cfg.package ];
+      etc."hadoop-conf".source = let
+        hadoopConf = "${import ./conf.nix { inherit cfg pkgs lib; }}/";
+      in "${hadoopConf}";
+      variables.HADOOP_CONF_DIR = "/etc/hadoop-conf/";
+    };
+  };
+}
diff --git a/nixos/modules/services/cluster/hadoop/hdfs.nix b/nixos/modules/services/cluster/hadoop/hdfs.nix
new file mode 100644
index 00000000000..325a002ad32
--- /dev/null
+++ b/nixos/modules/services/cluster/hadoop/hdfs.nix
@@ -0,0 +1,204 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.hadoop;
+
+  # Config files for hadoop services
+  hadoopConf = "${import ./conf.nix { inherit cfg pkgs lib; }}/";
+
+  # Generator for HDFS service options
+  hadoopServiceOption = { serviceName, firewallOption ? true, extraOpts ? null }: {
+    enable = mkEnableOption serviceName;
+    restartIfChanged = mkOption {
+      type = types.bool;
+      description = ''
+        Automatically restart the service on config change.
+        This can be set to false to defer restarts on clusters running critical applications.
+        Please consider the security implications of inadvertently running an older version,
+        and the possibility of unexpected behavior caused by inconsistent versions across a cluster when disabling this option.
+      '';
+      default = false;
+    };
+    extraFlags = mkOption{
+      type = with types; listOf str;
+      default = [];
+      description = "Extra command line flags to pass to ${serviceName}";
+      example = [
+        "-Dcom.sun.management.jmxremote"
+        "-Dcom.sun.management.jmxremote.port=8010"
+      ];
+    };
+    extraEnv = mkOption{
+      type = with types; attrsOf str;
+      default = {};
+      description = "Extra environment variables for ${serviceName}";
+    };
+  } // (optionalAttrs firewallOption {
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Open firewall ports for ${serviceName}.";
+    };
+  }) // (optionalAttrs (extraOpts != null) extraOpts);
+
+  # Generator for HDFS service configs
+  hadoopServiceConfig =
+    { name
+    , serviceOptions ? cfg.hdfs."${toLower name}"
+    , description ? "Hadoop HDFS ${name}"
+    , User ? "hdfs"
+    , allowedTCPPorts ? [ ]
+    , preStart ? ""
+    , environment ? { }
+    , extraConfig ? { }
+    }: (
+
+      mkIf serviceOptions.enable ( mkMerge [{
+        systemd.services."hdfs-${toLower name}" = {
+          inherit description preStart;
+          environment = environment // serviceOptions.extraEnv;
+          wantedBy = [ "multi-user.target" ];
+          inherit (serviceOptions) restartIfChanged;
+          serviceConfig = {
+            inherit User;
+            SyslogIdentifier = "hdfs-${toLower name}";
+            ExecStart = "${cfg.package}/bin/hdfs --config ${hadoopConf} ${toLower name} ${escapeShellArgs serviceOptions.extraFlags}";
+            Restart = "always";
+          };
+        };
+
+        services.hadoop.gatewayRole.enable = true;
+
+        networking.firewall.allowedTCPPorts = mkIf
+          ((builtins.hasAttr "openFirewall" serviceOptions) && serviceOptions.openFirewall)
+          allowedTCPPorts;
+      } extraConfig])
+    );
+
+in
+{
+  options.services.hadoop.hdfs = {
+
+    namenode = hadoopServiceOption { serviceName = "HDFS NameNode"; } // {
+      formatOnInit = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Format HDFS namenode on first start. This is useful for quickly spinning up
+          ephemeral HDFS clusters with a single namenode.
+          For HA clusters, initialization involves multiple steps across multiple nodes.
+          Follow this guide to initialize an HA cluster manually:
+          <link xlink:href="https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HDFSHighAvailabilityWithQJM.html"/>
+        '';
+      };
+    };
+
+    datanode = hadoopServiceOption { serviceName = "HDFS DataNode"; } // {
+      dataDirs = mkOption {
+        default = null;
+        description = "Tier and path definitions for datanode storage.";
+        type = with types; nullOr (listOf (submodule {
+          options = {
+            type = mkOption {
+              type = enum [ "SSD" "DISK" "ARCHIVE" "RAM_DISK" ];
+              description = ''
+                Storage types ([SSD]/[DISK]/[ARCHIVE]/[RAM_DISK]) for HDFS storage policies.
+              '';
+            };
+            path = mkOption {
+              type = path;
+              example = [ "/var/lib/hadoop/hdfs/dn" ];
+              description = "Determines where on the local filesystem a data node should store its blocks.";
+            };
+          };
+        }));
+      };
+    };
+
+    journalnode = hadoopServiceOption { serviceName = "HDFS JournalNode"; };
+
+    zkfc = hadoopServiceOption {
+      serviceName = "HDFS ZooKeeper failover controller";
+      firewallOption = false;
+    };
+
+    httpfs = hadoopServiceOption { serviceName = "HDFS JournalNode"; } // {
+      tempPath = mkOption {
+        type = types.path;
+        default = "/tmp/hadoop/httpfs";
+        description = "HTTPFS_TEMP path used by HTTPFS";
+      };
+    };
+
+  };
+
+  config = mkMerge [
+    (hadoopServiceConfig {
+      name = "NameNode";
+      allowedTCPPorts = [
+        9870 # namenode.http-address
+        8020 # namenode.rpc-address
+        8022 # namenode.servicerpc-address
+        8019 # dfs.ha.zkfc.port
+      ];
+      preStart = (mkIf cfg.hdfs.namenode.formatOnInit
+        "${cfg.package}/bin/hdfs --config ${hadoopConf} namenode -format -nonInteractive || true"
+      );
+    })
+
+    (hadoopServiceConfig {
+      name = "DataNode";
+      # port numbers for datanode changed between hadoop 2 and 3
+      allowedTCPPorts = if versionAtLeast cfg.package.version "3" then [
+        9864 # datanode.http.address
+        9866 # datanode.address
+        9867 # datanode.ipc.address
+      ] else [
+        50075 # datanode.http.address
+        50010 # datanode.address
+        50020 # datanode.ipc.address
+      ];
+      extraConfig.services.hadoop.hdfsSiteInternal."dfs.datanode.data.dir" = let d = cfg.hdfs.datanode.dataDirs; in
+        if (d!= null) then (concatMapStringsSep "," (x: "["+x.type+"]file://"+x.path) cfg.hdfs.datanode.dataDirs) else d;
+    })
+
+    (hadoopServiceConfig {
+      name = "JournalNode";
+      allowedTCPPorts = [
+        8480 # dfs.journalnode.http-address
+        8485 # dfs.journalnode.rpc-address
+      ];
+    })
+
+    (hadoopServiceConfig {
+      name = "zkfc";
+      description = "Hadoop HDFS ZooKeeper failover controller";
+    })
+
+    (hadoopServiceConfig {
+      name = "HTTPFS";
+      environment.HTTPFS_TEMP = cfg.hdfs.httpfs.tempPath;
+      preStart = "mkdir -p $HTTPFS_TEMP";
+      User = "httpfs";
+      allowedTCPPorts = [
+        14000 # httpfs.http.port
+      ];
+    })
+
+    (mkIf cfg.gatewayRole.enable {
+      users.users.hdfs = {
+        description = "Hadoop HDFS user";
+        group = "hadoop";
+        uid = config.ids.uids.hdfs;
+      };
+    })
+    (mkIf cfg.hdfs.httpfs.enable {
+      users.users.httpfs = {
+        description = "Hadoop HTTPFS user";
+        group = "hadoop";
+        isSystemUser = true;
+      };
+    })
+
+  ];
+}
diff --git a/nixos/modules/services/cluster/hadoop/yarn.nix b/nixos/modules/services/cluster/hadoop/yarn.nix
new file mode 100644
index 00000000000..74e16bdec68
--- /dev/null
+++ b/nixos/modules/services/cluster/hadoop/yarn.nix
@@ -0,0 +1,200 @@
+{ config, lib, pkgs, ...}:
+with lib;
+let
+  cfg = config.services.hadoop;
+  hadoopConf = "${import ./conf.nix { inherit cfg pkgs lib; }}/";
+  restartIfChanged  = mkOption {
+    type = types.bool;
+    description = ''
+      Automatically restart the service on config change.
+      This can be set to false to defer restarts on clusters running critical applications.
+      Please consider the security implications of inadvertently running an older version,
+      and the possibility of unexpected behavior caused by inconsistent versions across a cluster when disabling this option.
+    '';
+    default = false;
+  };
+  extraFlags = mkOption{
+    type = with types; listOf str;
+    default = [];
+    description = "Extra command line flags to pass to the service";
+    example = [
+      "-Dcom.sun.management.jmxremote"
+      "-Dcom.sun.management.jmxremote.port=8010"
+    ];
+  };
+  extraEnv = mkOption{
+    type = with types; attrsOf str;
+    default = {};
+    description = "Extra environment variables";
+  };
+in
+{
+  options.services.hadoop.yarn = {
+    resourcemanager = {
+      enable = mkEnableOption "Hadoop YARN ResourceManager";
+      inherit restartIfChanged extraFlags extraEnv;
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open firewall ports for resourcemanager
+        '';
+      };
+    };
+    nodemanager = {
+      enable = mkEnableOption "Hadoop YARN NodeManager";
+      inherit restartIfChanged extraFlags extraEnv;
+
+      resource = {
+        cpuVCores = mkOption {
+          description = "Number of vcores that can be allocated for containers.";
+          type = with types; nullOr ints.positive;
+          default = null;
+        };
+        maximumAllocationVCores = mkOption {
+          description = "The maximum virtual CPU cores any container can be allocated.";
+          type = with types; nullOr ints.positive;
+          default = null;
+        };
+        memoryMB = mkOption {
+          description = "Amount of physical memory, in MB, that can be allocated for containers.";
+          type = with types; nullOr ints.positive;
+          default = null;
+        };
+        maximumAllocationMB = mkOption {
+          description = "The maximum physical memory any container can be allocated.";
+          type = with types; nullOr ints.positive;
+          default = null;
+        };
+      };
+
+      useCGroups = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Use cgroups to enforce resource limits on containers
+        '';
+      };
+
+      localDir = mkOption {
+        description = "List of directories to store localized files in.";
+        type = with types; nullOr (listOf path);
+        example = [ "/var/lib/hadoop/yarn/nm" ];
+        default = null;
+      };
+
+      addBinBash = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Add /bin/bash. This is needed by the linux container executor's launch script.
+        '';
+      };
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open firewall ports for nodemanager.
+          Because containers can listen on any ephemeral port, TCP ports 1024–65535 will be opened.
+        '';
+      };
+    };
+  };
+
+  config = mkMerge [
+    (mkIf cfg.gatewayRole.enable {
+      users.users.yarn = {
+        description = "Hadoop YARN user";
+        group = "hadoop";
+        uid = config.ids.uids.yarn;
+      };
+    })
+
+    (mkIf cfg.yarn.resourcemanager.enable {
+      systemd.services.yarn-resourcemanager = {
+        description = "Hadoop YARN ResourceManager";
+        wantedBy = [ "multi-user.target" ];
+        inherit (cfg.yarn.resourcemanager) restartIfChanged;
+        environment = cfg.yarn.resourcemanager.extraEnv;
+
+        serviceConfig = {
+          User = "yarn";
+          SyslogIdentifier = "yarn-resourcemanager";
+          ExecStart = "${cfg.package}/bin/yarn --config ${hadoopConf} " +
+                      " resourcemanager ${escapeShellArgs cfg.yarn.resourcemanager.extraFlags}";
+          Restart = "always";
+        };
+      };
+
+      services.hadoop.gatewayRole.enable = true;
+
+      networking.firewall.allowedTCPPorts = (mkIf cfg.yarn.resourcemanager.openFirewall [
+        8088 # resourcemanager.webapp.address
+        8030 # resourcemanager.scheduler.address
+        8031 # resourcemanager.resource-tracker.address
+        8032 # resourcemanager.address
+        8033 # resourcemanager.admin.address
+      ]);
+    })
+
+    (mkIf cfg.yarn.nodemanager.enable {
+      # Needed because yarn hardcodes /bin/bash in container start scripts
+      # These scripts can't be patched, they are generated at runtime
+      systemd.tmpfiles.rules = [
+        (mkIf cfg.yarn.nodemanager.addBinBash "L /bin/bash - - - - /run/current-system/sw/bin/bash")
+      ];
+
+      systemd.services.yarn-nodemanager = {
+        description = "Hadoop YARN NodeManager";
+        wantedBy = [ "multi-user.target" ];
+        inherit (cfg.yarn.nodemanager) restartIfChanged;
+        environment = cfg.yarn.nodemanager.extraEnv;
+
+        preStart = ''
+          # create log dir
+          mkdir -p /var/log/hadoop/yarn/nodemanager
+          chown yarn:hadoop /var/log/hadoop/yarn/nodemanager
+
+          # set up setuid container executor binary
+          umount /run/wrappers/yarn-nodemanager/cgroup/cpu || true
+          rm -rf /run/wrappers/yarn-nodemanager/ || true
+          mkdir -p /run/wrappers/yarn-nodemanager/{bin,etc/hadoop,cgroup/cpu}
+          cp ${cfg.package}/lib/${cfg.package.untarDir}/bin/container-executor /run/wrappers/yarn-nodemanager/bin/
+          chgrp hadoop /run/wrappers/yarn-nodemanager/bin/container-executor
+          chmod 6050 /run/wrappers/yarn-nodemanager/bin/container-executor
+          cp ${hadoopConf}/container-executor.cfg /run/wrappers/yarn-nodemanager/etc/hadoop/
+        '';
+
+        serviceConfig = {
+          User = "yarn";
+          SyslogIdentifier = "yarn-nodemanager";
+          PermissionsStartOnly = true;
+          ExecStart = "${cfg.package}/bin/yarn --config ${hadoopConf} " +
+                      " nodemanager ${escapeShellArgs cfg.yarn.nodemanager.extraFlags}";
+          Restart = "always";
+        };
+      };
+
+      services.hadoop.gatewayRole.enable = true;
+
+      services.hadoop.yarnSiteInternal = with cfg.yarn.nodemanager; {
+        "yarn.nodemanager.local-dirs" = localDir;
+        "yarn.scheduler.maximum-allocation-vcores" = resource.maximumAllocationVCores;
+        "yarn.scheduler.maximum-allocation-mb" = resource.maximumAllocationMB;
+        "yarn.nodemanager.resource.cpu-vcores" = resource.cpuVCores;
+        "yarn.nodemanager.resource.memory-mb" = resource.memoryMB;
+      } // mkIf useCGroups {
+        "yarn.nodemanager.linux-container-executor.cgroups.hierarchy" = "/hadoop-yarn";
+        "yarn.nodemanager.linux-container-executor.resources-handler.class" = "org.apache.hadoop.yarn.server.nodemanager.util.CgroupsLCEResourcesHandler";
+        "yarn.nodemanager.linux-container-executor.cgroups.mount" = "true";
+        "yarn.nodemanager.linux-container-executor.cgroups.mount-path" = "/run/wrappers/yarn-nodemanager/cgroup";
+      };
+
+      networking.firewall.allowedTCPPortRanges = [
+        (mkIf (cfg.yarn.nodemanager.openFirewall) {from = 1024; to = 65535;})
+      ];
+    })
+
+  ];
+}
diff --git a/nixos/modules/services/cluster/k3s/default.nix b/nixos/modules/services/cluster/k3s/default.nix
new file mode 100644
index 00000000000..3a36cfa3f37
--- /dev/null
+++ b/nixos/modules/services/cluster/k3s/default.nix
@@ -0,0 +1,128 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.k3s;
+in
+{
+  # interface
+  options.services.k3s = {
+    enable = mkEnableOption "k3s";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.k3s;
+      defaultText = literalExpression "pkgs.k3s";
+      description = "Package that should be used for k3s";
+    };
+
+    role = mkOption {
+      description = ''
+        Whether k3s should run as a server or agent.
+        Note that the server, by default, also runs as an agent.
+      '';
+      default = "server";
+      type = types.enum [ "server" "agent" ];
+    };
+
+    serverAddr = mkOption {
+      type = types.str;
+      description = "The k3s server to connect to. This option only makes sense for an agent.";
+      example = "https://10.0.0.10:6443";
+      default = "";
+    };
+
+    token = mkOption {
+      type = types.str;
+      description = ''
+        The k3s token to use when connecting to the server. This option only makes sense for an agent.
+        WARNING: This option will expose store your token unencrypted world-readable in the nix store.
+        If this is undesired use the tokenFile option instead.
+      '';
+      default = "";
+    };
+
+    tokenFile = mkOption {
+      type = types.nullOr types.path;
+      description = "File path containing k3s token to use when connecting to the server. This option only makes sense for an agent.";
+      default = null;
+    };
+
+    docker = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Use docker to run containers rather than the built-in containerd.";
+    };
+
+    extraFlags = mkOption {
+      description = "Extra flags to pass to the k3s command.";
+      type = types.str;
+      default = "";
+      example = "--no-deploy traefik --cluster-cidr 10.24.0.0/16";
+    };
+
+    disableAgent = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Only run the server. This option only makes sense for a server.";
+    };
+
+    configPath = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = "File path containing the k3s YAML config. This is useful when the config is generated (for example on boot).";
+    };
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.role == "agent" -> (cfg.configPath != null || cfg.serverAddr != "");
+        message = "serverAddr or configPath (with 'server' key) should be set if role is 'agent'";
+      }
+      {
+        assertion = cfg.role == "agent" -> cfg.configPath != null || cfg.tokenFile != null || cfg.token != "";
+        message = "token or tokenFile or configPath (with 'token' or 'token-file' keys) should be set if role is 'agent'";
+      }
+    ];
+
+    virtualisation.docker = mkIf cfg.docker {
+      enable = mkDefault true;
+    };
+    environment.systemPackages = [ config.services.k3s.package ];
+
+    systemd.services.k3s = {
+      description = "k3s service";
+      after = [ "network.service" "firewall.service" ] ++ (optional cfg.docker "docker.service");
+      wants = [ "network.service" "firewall.service" ];
+      wantedBy = [ "multi-user.target" ];
+      path = optional config.boot.zfs.enabled config.boot.zfs.package;
+      serviceConfig = {
+        # See: https://github.com/rancher/k3s/blob/dddbd16305284ae4bd14c0aade892412310d7edc/install.sh#L197
+        Type = if cfg.role == "agent" then "exec" else "notify";
+        KillMode = "process";
+        Delegate = "yes";
+        Restart = "always";
+        RestartSec = "5s";
+        LimitNOFILE = 1048576;
+        LimitNPROC = "infinity";
+        LimitCORE = "infinity";
+        TasksMax = "infinity";
+        ExecStart = concatStringsSep " \\\n " (
+          [
+            "${cfg.package}/bin/k3s ${cfg.role}"
+          ] ++ (optional cfg.docker "--docker")
+          ++ (optional (cfg.docker && config.systemd.enableUnifiedCgroupHierarchy) "--kubelet-arg=cgroup-driver=systemd")
+          ++ (optional cfg.disableAgent "--disable-agent")
+          ++ (optional (cfg.serverAddr != "") "--server ${cfg.serverAddr}")
+          ++ (optional (cfg.token != "") "--token ${cfg.token}")
+          ++ (optional (cfg.tokenFile != null) "--token-file ${cfg.tokenFile}")
+          ++ (optional (cfg.configPath != null) "--config ${cfg.configPath}")
+          ++ [ cfg.extraFlags ]
+        );
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/cluster/kubernetes/addon-manager.nix b/nixos/modules/services/cluster/kubernetes/addon-manager.nix
new file mode 100644
index 00000000000..b677d900ff5
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/addon-manager.nix
@@ -0,0 +1,171 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  cfg = top.addonManager;
+
+  isRBACEnabled = elem "RBAC" top.apiserver.authorizationMode;
+
+  addons = pkgs.runCommand "kubernetes-addons" { } ''
+    mkdir -p $out
+    # since we are mounting the addons to the addon manager, they need to be copied
+    ${concatMapStringsSep ";" (a: "cp -v ${a}/* $out/") (mapAttrsToList (name: addon:
+      pkgs.writeTextDir "${name}.json" (builtins.toJSON addon)
+    ) (cfg.addons))}
+  '';
+in
+{
+  ###### interface
+  options.services.kubernetes.addonManager = with lib.types; {
+
+    bootstrapAddons = mkOption {
+      description = ''
+        Bootstrap addons are like regular addons, but they are applied with cluster-admin rigths.
+        They are applied at addon-manager startup only.
+      '';
+      default = { };
+      type = attrsOf attrs;
+      example = literalExpression ''
+        {
+          "my-service" = {
+            "apiVersion" = "v1";
+            "kind" = "Service";
+            "metadata" = {
+              "name" = "my-service";
+              "namespace" = "default";
+            };
+            "spec" = { ... };
+          };
+        }
+      '';
+    };
+
+    addons = mkOption {
+      description = "Kubernetes addons (any kind of Kubernetes resource can be an addon).";
+      default = { };
+      type = attrsOf (either attrs (listOf attrs));
+      example = literalExpression ''
+        {
+          "my-service" = {
+            "apiVersion" = "v1";
+            "kind" = "Service";
+            "metadata" = {
+              "name" = "my-service";
+              "namespace" = "default";
+            };
+            "spec" = { ... };
+          };
+        }
+        // import <nixpkgs/nixos/modules/services/cluster/kubernetes/dns.nix> { cfg = config.services.kubernetes; };
+      '';
+    };
+
+    enable = mkEnableOption "Kubernetes addon manager.";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    environment.etc."kubernetes/addons".source = "${addons}/";
+
+    systemd.services.kube-addon-manager = {
+      description = "Kubernetes addon manager";
+      wantedBy = [ "kubernetes.target" ];
+      after = [ "kube-apiserver.service" ];
+      environment.ADDON_PATH = "/etc/kubernetes/addons/";
+      path = [ pkgs.gawk ];
+      serviceConfig = {
+        Slice = "kubernetes.slice";
+        ExecStart = "${top.package}/bin/kube-addons";
+        WorkingDirectory = top.dataDir;
+        User = "kubernetes";
+        Group = "kubernetes";
+        Restart = "on-failure";
+        RestartSec = 10;
+      };
+      unitConfig = {
+        StartLimitIntervalSec = 0;
+      };
+    };
+
+    services.kubernetes.addonManager.bootstrapAddons = mkIf isRBACEnabled
+    (let
+      name = "system:kube-addon-manager";
+      namespace = "kube-system";
+    in
+    {
+
+      kube-addon-manager-r = {
+        apiVersion = "rbac.authorization.k8s.io/v1";
+        kind = "Role";
+        metadata = {
+          inherit name namespace;
+        };
+        rules = [{
+          apiGroups = ["*"];
+          resources = ["*"];
+          verbs = ["*"];
+        }];
+      };
+
+      kube-addon-manager-rb = {
+        apiVersion = "rbac.authorization.k8s.io/v1";
+        kind = "RoleBinding";
+        metadata = {
+          inherit name namespace;
+        };
+        roleRef = {
+          apiGroup = "rbac.authorization.k8s.io";
+          kind = "Role";
+          inherit name;
+        };
+        subjects = [{
+          apiGroup = "rbac.authorization.k8s.io";
+          kind = "User";
+          inherit name;
+        }];
+      };
+
+      kube-addon-manager-cluster-lister-cr = {
+        apiVersion = "rbac.authorization.k8s.io/v1";
+        kind = "ClusterRole";
+        metadata = {
+          name = "${name}:cluster-lister";
+        };
+        rules = [{
+          apiGroups = ["*"];
+          resources = ["*"];
+          verbs = ["list"];
+        }];
+      };
+
+      kube-addon-manager-cluster-lister-crb = {
+        apiVersion = "rbac.authorization.k8s.io/v1";
+        kind = "ClusterRoleBinding";
+        metadata = {
+          name = "${name}:cluster-lister";
+        };
+        roleRef = {
+          apiGroup = "rbac.authorization.k8s.io";
+          kind = "ClusterRole";
+          name = "${name}:cluster-lister";
+        };
+        subjects = [{
+          kind = "User";
+          inherit name;
+        }];
+      };
+    });
+
+    services.kubernetes.pki.certs = {
+      addonManager = top.lib.mkCert {
+        name = "kube-addon-manager";
+        CN = "system:kube-addon-manager";
+        action = "systemctl restart kube-addon-manager.service";
+      };
+    };
+  };
+
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/cluster/kubernetes/addons/dns.nix b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
new file mode 100644
index 00000000000..7bd4991f43f
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
@@ -0,0 +1,368 @@
+{ config, options, pkgs, lib, ... }:
+
+with lib;
+
+let
+  version = "1.7.1";
+  cfg = config.services.kubernetes.addons.dns;
+  ports = {
+    dns = 10053;
+    health = 10054;
+    metrics = 10055;
+  };
+in {
+  options.services.kubernetes.addons.dns = {
+    enable = mkEnableOption "kubernetes dns addon";
+
+    clusterIp = mkOption {
+      description = "Dns addon clusterIP";
+
+      # this default is also what kubernetes users
+      default = (
+        concatStringsSep "." (
+          take 3 (splitString "." config.services.kubernetes.apiserver.serviceClusterIpRange
+        ))
+      ) + ".254";
+      defaultText = literalDocBook ''
+        The <literal>x.y.z.254</literal> IP of
+        <literal>config.${options.services.kubernetes.apiserver.serviceClusterIpRange}</literal>.
+      '';
+      type = types.str;
+    };
+
+    clusterDomain = mkOption {
+      description = "Dns cluster domain";
+      default = "cluster.local";
+      type = types.str;
+    };
+
+    replicas = mkOption {
+      description = "Number of DNS pod replicas to deploy in the cluster.";
+      default = 2;
+      type = types.int;
+    };
+
+    reconcileMode = mkOption {
+      description = ''
+        Controls the addon manager reconciliation mode for the DNS addon.
+
+        Setting reconcile mode to EnsureExists makes it possible to tailor DNS behavior by editing the coredns ConfigMap.
+
+        See: <link xlink:href="https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/addon-manager/README.md"/>.
+      '';
+      default = "Reconcile";
+      type = types.enum [ "Reconcile" "EnsureExists" ];
+    };
+
+    coredns = mkOption {
+      description = "Docker image to seed for the CoreDNS container.";
+      type = types.attrs;
+      default = {
+        imageName = "coredns/coredns";
+        imageDigest = "sha256:4a6e0769130686518325b21b0c1d0688b54e7c79244d48e1b15634e98e40c6ef";
+        finalImageTag = version;
+        sha256 = "02r440xcdsgi137k5lmmvp0z5w5fmk8g9mysq5pnysq1wl8sj6mw";
+      };
+    };
+
+    corefile = mkOption {
+      description = ''
+        Custom coredns corefile configuration.
+
+        See: <link xlink:href="https://coredns.io/manual/toc/#configuration"/>.
+      '';
+      type = types.str;
+      default = ''
+        .:${toString ports.dns} {
+          errors
+          health :${toString ports.health}
+          kubernetes ${cfg.clusterDomain} in-addr.arpa ip6.arpa {
+            pods insecure
+            fallthrough in-addr.arpa ip6.arpa
+          }
+          prometheus :${toString ports.metrics}
+          forward . /etc/resolv.conf
+          cache 30
+          loop
+          reload
+          loadbalance
+        }'';
+      defaultText = literalExpression ''
+        '''
+          .:${toString ports.dns} {
+            errors
+            health :${toString ports.health}
+            kubernetes ''${config.services.kubernetes.addons.dns.clusterDomain} in-addr.arpa ip6.arpa {
+              pods insecure
+              fallthrough in-addr.arpa ip6.arpa
+            }
+            prometheus :${toString ports.metrics}
+            forward . /etc/resolv.conf
+            cache 30
+            loop
+            reload
+            loadbalance
+          }
+        '''
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.kubernetes.kubelet.seedDockerImages =
+      singleton (pkgs.dockerTools.pullImage cfg.coredns);
+
+    services.kubernetes.addonManager.bootstrapAddons = {
+      coredns-cr = {
+        apiVersion = "rbac.authorization.k8s.io/v1";
+        kind = "ClusterRole";
+        metadata = {
+          labels = {
+            "addonmanager.kubernetes.io/mode" = "Reconcile";
+            k8s-app = "kube-dns";
+            "kubernetes.io/cluster-service" = "true";
+            "kubernetes.io/bootstrapping" = "rbac-defaults";
+          };
+          name = "system:coredns";
+        };
+        rules = [
+          {
+            apiGroups = [ "" ];
+            resources = [ "endpoints" "services" "pods" "namespaces" ];
+            verbs = [ "list" "watch" ];
+          }
+          {
+            apiGroups = [ "" ];
+            resources = [ "nodes" ];
+            verbs = [ "get" ];
+          }
+        ];
+      };
+
+      coredns-crb = {
+        apiVersion = "rbac.authorization.k8s.io/v1";
+        kind = "ClusterRoleBinding";
+        metadata = {
+          annotations = {
+            "rbac.authorization.kubernetes.io/autoupdate" = "true";
+          };
+          labels = {
+            "addonmanager.kubernetes.io/mode" = "Reconcile";
+            k8s-app = "kube-dns";
+            "kubernetes.io/cluster-service" = "true";
+            "kubernetes.io/bootstrapping" = "rbac-defaults";
+          };
+          name = "system:coredns";
+        };
+        roleRef = {
+          apiGroup = "rbac.authorization.k8s.io";
+          kind = "ClusterRole";
+          name = "system:coredns";
+        };
+        subjects = [
+          {
+            kind = "ServiceAccount";
+            name = "coredns";
+            namespace = "kube-system";
+          }
+        ];
+      };
+    };
+
+    services.kubernetes.addonManager.addons = {
+      coredns-sa = {
+        apiVersion = "v1";
+        kind = "ServiceAccount";
+        metadata = {
+          labels = {
+            "addonmanager.kubernetes.io/mode" = "Reconcile";
+            k8s-app = "kube-dns";
+            "kubernetes.io/cluster-service" = "true";
+          };
+          name = "coredns";
+          namespace = "kube-system";
+        };
+      };
+
+      coredns-cm = {
+        apiVersion = "v1";
+        kind = "ConfigMap";
+        metadata = {
+          labels = {
+            "addonmanager.kubernetes.io/mode" = cfg.reconcileMode;
+            k8s-app = "kube-dns";
+            "kubernetes.io/cluster-service" = "true";
+          };
+          name = "coredns";
+          namespace = "kube-system";
+        };
+        data = {
+          Corefile = cfg.corefile;
+        };
+      };
+
+      coredns-deploy = {
+        apiVersion = "apps/v1";
+        kind = "Deployment";
+        metadata = {
+          labels = {
+            "addonmanager.kubernetes.io/mode" = cfg.reconcileMode;
+            k8s-app = "kube-dns";
+            "kubernetes.io/cluster-service" = "true";
+            "kubernetes.io/name" = "CoreDNS";
+          };
+          name = "coredns";
+          namespace = "kube-system";
+        };
+        spec = {
+          replicas = cfg.replicas;
+          selector = {
+            matchLabels = { k8s-app = "kube-dns"; };
+          };
+          strategy = {
+            rollingUpdate = { maxUnavailable = 1; };
+            type = "RollingUpdate";
+          };
+          template = {
+            metadata = {
+              labels = {
+                k8s-app = "kube-dns";
+              };
+            };
+            spec = {
+              containers = [
+                {
+                  args = [ "-conf" "/etc/coredns/Corefile" ];
+                  image = with cfg.coredns; "${imageName}:${finalImageTag}";
+                  imagePullPolicy = "Never";
+                  livenessProbe = {
+                    failureThreshold = 5;
+                    httpGet = {
+                      path = "/health";
+                      port = ports.health;
+                      scheme = "HTTP";
+                    };
+                    initialDelaySeconds = 60;
+                    successThreshold = 1;
+                    timeoutSeconds = 5;
+                  };
+                  name = "coredns";
+                  ports = [
+                    {
+                      containerPort = ports.dns;
+                      name = "dns";
+                      protocol = "UDP";
+                    }
+                    {
+                      containerPort = ports.dns;
+                      name = "dns-tcp";
+                      protocol = "TCP";
+                    }
+                    {
+                      containerPort = ports.metrics;
+                      name = "metrics";
+                      protocol = "TCP";
+                    }
+                  ];
+                  resources = {
+                    limits = {
+                      memory = "170Mi";
+                    };
+                    requests = {
+                      cpu = "100m";
+                      memory = "70Mi";
+                    };
+                  };
+                  securityContext = {
+                    allowPrivilegeEscalation = false;
+                    capabilities = {
+                      drop = [ "all" ];
+                    };
+                    readOnlyRootFilesystem = true;
+                  };
+                  volumeMounts = [
+                    {
+                      mountPath = "/etc/coredns";
+                      name = "config-volume";
+                      readOnly = true;
+                    }
+                  ];
+                }
+              ];
+              dnsPolicy = "Default";
+              nodeSelector = {
+                "beta.kubernetes.io/os" = "linux";
+              };
+              serviceAccountName = "coredns";
+              tolerations = [
+                {
+                  effect = "NoSchedule";
+                  key = "node-role.kubernetes.io/master";
+                }
+                {
+                  key = "CriticalAddonsOnly";
+                  operator = "Exists";
+                }
+              ];
+              volumes = [
+                {
+                  configMap = {
+                    items = [
+                      {
+                        key = "Corefile";
+                        path = "Corefile";
+                      }
+                    ];
+                    name = "coredns";
+                  };
+                  name = "config-volume";
+                }
+              ];
+            };
+          };
+        };
+      };
+
+      coredns-svc = {
+        apiVersion = "v1";
+        kind = "Service";
+        metadata = {
+          annotations = {
+            "prometheus.io/port" = toString ports.metrics;
+            "prometheus.io/scrape" = "true";
+          };
+          labels = {
+            "addonmanager.kubernetes.io/mode" = "Reconcile";
+            k8s-app = "kube-dns";
+            "kubernetes.io/cluster-service" = "true";
+            "kubernetes.io/name" = "CoreDNS";
+          };
+          name = "kube-dns";
+          namespace = "kube-system";
+        };
+        spec = {
+          clusterIP = cfg.clusterIp;
+          ports = [
+            {
+              name = "dns";
+              port = 53;
+              targetPort = ports.dns;
+              protocol = "UDP";
+            }
+            {
+              name = "dns-tcp";
+              port = 53;
+              targetPort = ports.dns;
+              protocol = "TCP";
+            }
+          ];
+          selector = { k8s-app = "kube-dns"; };
+        };
+      };
+    };
+
+    services.kubernetes.kubelet.clusterDns = mkDefault cfg.clusterIp;
+  };
+
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/cluster/kubernetes/apiserver.nix b/nixos/modules/services/cluster/kubernetes/apiserver.nix
new file mode 100644
index 00000000000..a192e93badc
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/apiserver.nix
@@ -0,0 +1,500 @@
+  { config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  otop = options.services.kubernetes;
+  cfg = top.apiserver;
+
+  isRBACEnabled = elem "RBAC" cfg.authorizationMode;
+
+  apiserverServiceIP = (concatStringsSep "." (
+    take 3 (splitString "." cfg.serviceClusterIpRange
+  )) + ".1");
+in
+{
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "kubernetes" "apiserver" "admissionControl" ] [ "services" "kubernetes" "apiserver" "enableAdmissionPlugins" ])
+    (mkRenamedOptionModule [ "services" "kubernetes" "apiserver" "address" ] ["services" "kubernetes" "apiserver" "bindAddress"])
+    (mkRenamedOptionModule [ "services" "kubernetes" "apiserver" "port" ] ["services" "kubernetes" "apiserver" "insecurePort"])
+    (mkRemovedOptionModule [ "services" "kubernetes" "apiserver" "publicAddress" ] "")
+    (mkRenamedOptionModule [ "services" "kubernetes" "etcd" "servers" ] [ "services" "kubernetes" "apiserver" "etcd" "servers" ])
+    (mkRenamedOptionModule [ "services" "kubernetes" "etcd" "keyFile" ] [ "services" "kubernetes" "apiserver" "etcd" "keyFile" ])
+    (mkRenamedOptionModule [ "services" "kubernetes" "etcd" "certFile" ] [ "services" "kubernetes" "apiserver" "etcd" "certFile" ])
+    (mkRenamedOptionModule [ "services" "kubernetes" "etcd" "caFile" ] [ "services" "kubernetes" "apiserver" "etcd" "caFile" ])
+  ];
+
+  ###### interface
+  options.services.kubernetes.apiserver = with lib.types; {
+
+    advertiseAddress = mkOption {
+      description = ''
+        Kubernetes apiserver IP address on which to advertise the apiserver
+        to members of the cluster. This address must be reachable by the rest
+        of the cluster.
+      '';
+      default = null;
+      type = nullOr str;
+    };
+
+    allowPrivileged = mkOption {
+      description = "Whether to allow privileged containers on Kubernetes.";
+      default = false;
+      type = bool;
+    };
+
+    authorizationMode = mkOption {
+      description = ''
+        Kubernetes apiserver authorization mode (AlwaysAllow/AlwaysDeny/ABAC/Webhook/RBAC/Node). See
+        <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authorization/"/>
+      '';
+      default = ["RBAC" "Node"]; # Enabling RBAC by default, although kubernetes default is AllowAllow
+      type = listOf (enum ["AlwaysAllow" "AlwaysDeny" "ABAC" "Webhook" "RBAC" "Node"]);
+    };
+
+    authorizationPolicy = mkOption {
+      description = ''
+        Kubernetes apiserver authorization policy file. See
+        <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authorization/"/>
+      '';
+      default = [];
+      type = listOf attrs;
+    };
+
+    basicAuthFile = mkOption {
+      description = ''
+        Kubernetes apiserver basic authentication file. See
+        <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authentication"/>
+      '';
+      default = null;
+      type = nullOr path;
+    };
+
+    bindAddress = mkOption {
+      description = ''
+        The IP address on which to listen for the --secure-port port.
+        The associated interface(s) must be reachable by the rest
+        of the cluster, and by CLI/web clients.
+      '';
+      default = "0.0.0.0";
+      type = str;
+    };
+
+    clientCaFile = mkOption {
+      description = "Kubernetes apiserver CA file for client auth.";
+      default = top.caFile;
+      defaultText = literalExpression "config.${otop.caFile}";
+      type = nullOr path;
+    };
+
+    disableAdmissionPlugins = mkOption {
+      description = ''
+        Kubernetes admission control plugins to disable. See
+        <link xlink:href="https://kubernetes.io/docs/admin/admission-controllers/"/>
+      '';
+      default = [];
+      type = listOf str;
+    };
+
+    enable = mkEnableOption "Kubernetes apiserver";
+
+    enableAdmissionPlugins = mkOption {
+      description = ''
+        Kubernetes admission control plugins to enable. See
+        <link xlink:href="https://kubernetes.io/docs/admin/admission-controllers/"/>
+      '';
+      default = [
+        "NamespaceLifecycle" "LimitRanger" "ServiceAccount"
+        "ResourceQuota" "DefaultStorageClass" "DefaultTolerationSeconds"
+        "NodeRestriction"
+      ];
+      example = [
+        "NamespaceLifecycle" "NamespaceExists" "LimitRanger"
+        "SecurityContextDeny" "ServiceAccount" "ResourceQuota"
+        "PodSecurityPolicy" "NodeRestriction" "DefaultStorageClass"
+      ];
+      type = listOf str;
+    };
+
+    etcd = {
+      servers = mkOption {
+        description = "List of etcd servers.";
+        default = ["http://127.0.0.1:2379"];
+        type = types.listOf types.str;
+      };
+
+      keyFile = mkOption {
+        description = "Etcd key file.";
+        default = null;
+        type = types.nullOr types.path;
+      };
+
+      certFile = mkOption {
+        description = "Etcd cert file.";
+        default = null;
+        type = types.nullOr types.path;
+      };
+
+      caFile = mkOption {
+        description = "Etcd ca file.";
+        default = top.caFile;
+        defaultText = literalExpression "config.${otop.caFile}";
+        type = types.nullOr types.path;
+      };
+    };
+
+    extraOpts = mkOption {
+      description = "Kubernetes apiserver extra command line options.";
+      default = "";
+      type = separatedString " ";
+    };
+
+    extraSANs = mkOption {
+      description = "Extra x509 Subject Alternative Names to be added to the kubernetes apiserver tls cert.";
+      default = [];
+      type = listOf str;
+    };
+
+    featureGates = mkOption {
+      description = "List set of feature gates";
+      default = top.featureGates;
+      defaultText = literalExpression "config.${otop.featureGates}";
+      type = listOf str;
+    };
+
+    insecureBindAddress = mkOption {
+      description = "The IP address on which to serve the --insecure-port.";
+      default = "127.0.0.1";
+      type = str;
+    };
+
+    insecurePort = mkOption {
+      description = "Kubernetes apiserver insecure listening port. (0 = disabled)";
+      default = 0;
+      type = int;
+    };
+
+    kubeletClientCaFile = mkOption {
+      description = "Path to a cert file for connecting to kubelet.";
+      default = top.caFile;
+      defaultText = literalExpression "config.${otop.caFile}";
+      type = nullOr path;
+    };
+
+    kubeletClientCertFile = mkOption {
+      description = "Client certificate to use for connections to kubelet.";
+      default = null;
+      type = nullOr path;
+    };
+
+    kubeletClientKeyFile = mkOption {
+      description = "Key to use for connections to kubelet.";
+      default = null;
+      type = nullOr path;
+    };
+
+    preferredAddressTypes = mkOption {
+      description = "List of the preferred NodeAddressTypes to use for kubelet connections.";
+      type = nullOr str;
+      default = null;
+    };
+
+    proxyClientCertFile = mkOption {
+      description = "Client certificate to use for connections to proxy.";
+      default = null;
+      type = nullOr path;
+    };
+
+    proxyClientKeyFile = mkOption {
+      description = "Key to use for connections to proxy.";
+      default = null;
+      type = nullOr path;
+    };
+
+    runtimeConfig = mkOption {
+      description = ''
+        Api runtime configuration. See
+        <link xlink:href="https://kubernetes.io/docs/tasks/administer-cluster/cluster-management/"/>
+      '';
+      default = "authentication.k8s.io/v1beta1=true";
+      example = "api/all=false,api/v1=true";
+      type = str;
+    };
+
+    storageBackend = mkOption {
+      description = ''
+        Kubernetes apiserver storage backend.
+      '';
+      default = "etcd3";
+      type = enum ["etcd2" "etcd3"];
+    };
+
+    securePort = mkOption {
+      description = "Kubernetes apiserver secure port.";
+      default = 6443;
+      type = int;
+    };
+
+    apiAudiences = mkOption {
+      description = ''
+        Kubernetes apiserver ServiceAccount issuer.
+      '';
+      default = "api,https://kubernetes.default.svc";
+      type = str;
+    };
+
+    serviceAccountIssuer = mkOption {
+      description = ''
+        Kubernetes apiserver ServiceAccount issuer.
+      '';
+      default = "https://kubernetes.default.svc";
+      type = str;
+    };
+
+    serviceAccountSigningKeyFile = mkOption {
+      description = ''
+        Path to the file that contains the current private key of the service
+        account token issuer. The issuer will sign issued ID tokens with this
+        private key.
+      '';
+      type = path;
+    };
+
+    serviceAccountKeyFile = mkOption {
+      description = ''
+        File containing PEM-encoded x509 RSA or ECDSA private or public keys,
+        used to verify ServiceAccount tokens. The specified file can contain
+        multiple keys, and the flag can be specified multiple times with
+        different files. If unspecified, --tls-private-key-file is used.
+        Must be specified when --service-account-signing-key is provided
+      '';
+      type = path;
+    };
+
+    serviceClusterIpRange = mkOption {
+      description = ''
+        A CIDR notation IP range from which to assign service cluster IPs.
+        This must not overlap with any IP ranges assigned to nodes for pods.
+      '';
+      default = "10.0.0.0/24";
+      type = str;
+    };
+
+    tlsCertFile = mkOption {
+      description = "Kubernetes apiserver certificate file.";
+      default = null;
+      type = nullOr path;
+    };
+
+    tlsKeyFile = mkOption {
+      description = "Kubernetes apiserver private key file.";
+      default = null;
+      type = nullOr path;
+    };
+
+    tokenAuthFile = mkOption {
+      description = ''
+        Kubernetes apiserver token authentication file. See
+        <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authentication"/>
+      '';
+      default = null;
+      type = nullOr path;
+    };
+
+    verbosity = mkOption {
+      description = ''
+        Optional glog verbosity level for logging statements. See
+        <link xlink:href="https://github.com/kubernetes/community/blob/master/contributors/devel/logging.md"/>
+      '';
+      default = null;
+      type = nullOr int;
+    };
+
+    webhookConfig = mkOption {
+      description = ''
+        Kubernetes apiserver Webhook config file. It uses the kubeconfig file format.
+        See <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/webhook/"/>
+      '';
+      default = null;
+      type = nullOr path;
+    };
+
+  };
+
+
+  ###### implementation
+  config = mkMerge [
+
+    (mkIf cfg.enable {
+        systemd.services.kube-apiserver = {
+          description = "Kubernetes APIServer Service";
+          wantedBy = [ "kubernetes.target" ];
+          after = [ "network.target" ];
+          serviceConfig = {
+            Slice = "kubernetes.slice";
+            ExecStart = ''${top.package}/bin/kube-apiserver \
+              --allow-privileged=${boolToString cfg.allowPrivileged} \
+              --authorization-mode=${concatStringsSep "," cfg.authorizationMode} \
+                ${optionalString (elem "ABAC" cfg.authorizationMode)
+                  "--authorization-policy-file=${
+                    pkgs.writeText "kube-auth-policy.jsonl"
+                    (concatMapStringsSep "\n" (l: builtins.toJSON l) cfg.authorizationPolicy)
+                  }"
+                } \
+                ${optionalString (elem "Webhook" cfg.authorizationMode)
+                  "--authorization-webhook-config-file=${cfg.webhookConfig}"
+                } \
+              --bind-address=${cfg.bindAddress} \
+              ${optionalString (cfg.advertiseAddress != null)
+                "--advertise-address=${cfg.advertiseAddress}"} \
+              ${optionalString (cfg.clientCaFile != null)
+                "--client-ca-file=${cfg.clientCaFile}"} \
+              --disable-admission-plugins=${concatStringsSep "," cfg.disableAdmissionPlugins} \
+              --enable-admission-plugins=${concatStringsSep "," cfg.enableAdmissionPlugins} \
+              --etcd-servers=${concatStringsSep "," cfg.etcd.servers} \
+              ${optionalString (cfg.etcd.caFile != null)
+                "--etcd-cafile=${cfg.etcd.caFile}"} \
+              ${optionalString (cfg.etcd.certFile != null)
+                "--etcd-certfile=${cfg.etcd.certFile}"} \
+              ${optionalString (cfg.etcd.keyFile != null)
+                "--etcd-keyfile=${cfg.etcd.keyFile}"} \
+              ${optionalString (cfg.featureGates != [])
+                "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
+              ${optionalString (cfg.basicAuthFile != null)
+                "--basic-auth-file=${cfg.basicAuthFile}"} \
+              ${optionalString (cfg.kubeletClientCaFile != null)
+                "--kubelet-certificate-authority=${cfg.kubeletClientCaFile}"} \
+              ${optionalString (cfg.kubeletClientCertFile != null)
+                "--kubelet-client-certificate=${cfg.kubeletClientCertFile}"} \
+              ${optionalString (cfg.kubeletClientKeyFile != null)
+                "--kubelet-client-key=${cfg.kubeletClientKeyFile}"} \
+              ${optionalString (cfg.preferredAddressTypes != null)
+                "--kubelet-preferred-address-types=${cfg.preferredAddressTypes}"} \
+              ${optionalString (cfg.proxyClientCertFile != null)
+                "--proxy-client-cert-file=${cfg.proxyClientCertFile}"} \
+              ${optionalString (cfg.proxyClientKeyFile != null)
+                "--proxy-client-key-file=${cfg.proxyClientKeyFile}"} \
+              --insecure-bind-address=${cfg.insecureBindAddress} \
+              --insecure-port=${toString cfg.insecurePort} \
+              ${optionalString (cfg.runtimeConfig != "")
+                "--runtime-config=${cfg.runtimeConfig}"} \
+              --secure-port=${toString cfg.securePort} \
+              --api-audiences=${toString cfg.apiAudiences} \
+              --service-account-issuer=${toString cfg.serviceAccountIssuer} \
+              --service-account-signing-key-file=${cfg.serviceAccountSigningKeyFile} \
+              --service-account-key-file=${cfg.serviceAccountKeyFile} \
+              --service-cluster-ip-range=${cfg.serviceClusterIpRange} \
+              --storage-backend=${cfg.storageBackend} \
+              ${optionalString (cfg.tlsCertFile != null)
+                "--tls-cert-file=${cfg.tlsCertFile}"} \
+              ${optionalString (cfg.tlsKeyFile != null)
+                "--tls-private-key-file=${cfg.tlsKeyFile}"} \
+              ${optionalString (cfg.tokenAuthFile != null)
+                "--token-auth-file=${cfg.tokenAuthFile}"} \
+              ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
+              ${cfg.extraOpts}
+            '';
+            WorkingDirectory = top.dataDir;
+            User = "kubernetes";
+            Group = "kubernetes";
+            AmbientCapabilities = "cap_net_bind_service";
+            Restart = "on-failure";
+            RestartSec = 5;
+          };
+
+          unitConfig = {
+            StartLimitIntervalSec = 0;
+          };
+        };
+
+        services.etcd = {
+          clientCertAuth = mkDefault true;
+          peerClientCertAuth = mkDefault true;
+          listenClientUrls = mkDefault ["https://0.0.0.0:2379"];
+          listenPeerUrls = mkDefault ["https://0.0.0.0:2380"];
+          advertiseClientUrls = mkDefault ["https://${top.masterAddress}:2379"];
+          initialCluster = mkDefault ["${top.masterAddress}=https://${top.masterAddress}:2380"];
+          name = mkDefault top.masterAddress;
+          initialAdvertisePeerUrls = mkDefault ["https://${top.masterAddress}:2380"];
+        };
+
+        services.kubernetes.addonManager.bootstrapAddons = mkIf isRBACEnabled {
+
+          apiserver-kubelet-api-admin-crb = {
+            apiVersion = "rbac.authorization.k8s.io/v1";
+            kind = "ClusterRoleBinding";
+            metadata = {
+              name = "system:kube-apiserver:kubelet-api-admin";
+            };
+            roleRef = {
+              apiGroup = "rbac.authorization.k8s.io";
+              kind = "ClusterRole";
+              name = "system:kubelet-api-admin";
+            };
+            subjects = [{
+              kind = "User";
+              name = "system:kube-apiserver";
+            }];
+          };
+
+        };
+
+      services.kubernetes.pki.certs = with top.lib; {
+        apiServer = mkCert {
+          name = "kube-apiserver";
+          CN = "kubernetes";
+          hosts = [
+                    "kubernetes.default.svc"
+                    "kubernetes.default.svc.${top.addons.dns.clusterDomain}"
+                    cfg.advertiseAddress
+                    top.masterAddress
+                    apiserverServiceIP
+                    "127.0.0.1"
+                  ] ++ cfg.extraSANs;
+          action = "systemctl restart kube-apiserver.service";
+        };
+        apiserverProxyClient = mkCert {
+          name = "kube-apiserver-proxy-client";
+          CN = "front-proxy-client";
+          action = "systemctl restart kube-apiserver.service";
+        };
+        apiserverKubeletClient = mkCert {
+          name = "kube-apiserver-kubelet-client";
+          CN = "system:kube-apiserver";
+          action = "systemctl restart kube-apiserver.service";
+        };
+        apiserverEtcdClient = mkCert {
+          name = "kube-apiserver-etcd-client";
+          CN = "etcd-client";
+          action = "systemctl restart kube-apiserver.service";
+        };
+        clusterAdmin = mkCert {
+          name = "cluster-admin";
+          CN = "cluster-admin";
+          fields = {
+            O = "system:masters";
+          };
+          privateKeyOwner = "root";
+        };
+        etcd = mkCert {
+          name = "etcd";
+          CN = top.masterAddress;
+          hosts = [
+                    "etcd.local"
+                    "etcd.${top.addons.dns.clusterDomain}"
+                    top.masterAddress
+                    cfg.advertiseAddress
+                  ];
+          privateKeyOwner = "etcd";
+          action = "systemctl restart etcd.service";
+        };
+      };
+
+    })
+
+  ];
+
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/cluster/kubernetes/controller-manager.nix b/nixos/modules/services/cluster/kubernetes/controller-manager.nix
new file mode 100644
index 00000000000..7c317e94dee
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/controller-manager.nix
@@ -0,0 +1,176 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  otop = options.services.kubernetes;
+  cfg = top.controllerManager;
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "kubernetes" "controllerManager" "address" ] ["services" "kubernetes" "controllerManager" "bindAddress"])
+    (mkRenamedOptionModule [ "services" "kubernetes" "controllerManager" "port" ] ["services" "kubernetes" "controllerManager" "insecurePort"])
+  ];
+
+  ###### interface
+  options.services.kubernetes.controllerManager = with lib.types; {
+
+    allocateNodeCIDRs = mkOption {
+      description = "Whether to automatically allocate CIDR ranges for cluster nodes.";
+      default = true;
+      type = bool;
+    };
+
+    bindAddress = mkOption {
+      description = "Kubernetes controller manager listening address.";
+      default = "127.0.0.1";
+      type = str;
+    };
+
+    clusterCidr = mkOption {
+      description = "Kubernetes CIDR Range for Pods in cluster.";
+      default = top.clusterCidr;
+      defaultText = literalExpression "config.${otop.clusterCidr}";
+      type = str;
+    };
+
+    enable = mkEnableOption "Kubernetes controller manager";
+
+    extraOpts = mkOption {
+      description = "Kubernetes controller manager extra command line options.";
+      default = "";
+      type = separatedString " ";
+    };
+
+    featureGates = mkOption {
+      description = "List set of feature gates";
+      default = top.featureGates;
+      defaultText = literalExpression "config.${otop.featureGates}";
+      type = listOf str;
+    };
+
+    insecurePort = mkOption {
+      description = "Kubernetes controller manager insecure listening port.";
+      default = 0;
+      type = int;
+    };
+
+    kubeconfig = top.lib.mkKubeConfigOptions "Kubernetes controller manager";
+
+    leaderElect = mkOption {
+      description = "Whether to start leader election before executing main loop.";
+      type = bool;
+      default = true;
+    };
+
+    rootCaFile = mkOption {
+      description = ''
+        Kubernetes controller manager certificate authority file included in
+        service account's token secret.
+      '';
+      default = top.caFile;
+      defaultText = literalExpression "config.${otop.caFile}";
+      type = nullOr path;
+    };
+
+    securePort = mkOption {
+      description = "Kubernetes controller manager secure listening port.";
+      default = 10252;
+      type = int;
+    };
+
+    serviceAccountKeyFile = mkOption {
+      description = ''
+        Kubernetes controller manager PEM-encoded private RSA key file used to
+        sign service account tokens
+      '';
+      default = null;
+      type = nullOr path;
+    };
+
+    tlsCertFile = mkOption {
+      description = "Kubernetes controller-manager certificate file.";
+      default = null;
+      type = nullOr path;
+    };
+
+    tlsKeyFile = mkOption {
+      description = "Kubernetes controller-manager private key file.";
+      default = null;
+      type = nullOr path;
+    };
+
+    verbosity = mkOption {
+      description = ''
+        Optional glog verbosity level for logging statements. See
+        <link xlink:href="https://github.com/kubernetes/community/blob/master/contributors/devel/logging.md"/>
+      '';
+      default = null;
+      type = nullOr int;
+    };
+
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    systemd.services.kube-controller-manager = {
+      description = "Kubernetes Controller Manager Service";
+      wantedBy = [ "kubernetes.target" ];
+      after = [ "kube-apiserver.service" ];
+      serviceConfig = {
+        RestartSec = "30s";
+        Restart = "on-failure";
+        Slice = "kubernetes.slice";
+        ExecStart = ''${top.package}/bin/kube-controller-manager \
+          --allocate-node-cidrs=${boolToString cfg.allocateNodeCIDRs} \
+          --bind-address=${cfg.bindAddress} \
+          ${optionalString (cfg.clusterCidr!=null)
+            "--cluster-cidr=${cfg.clusterCidr}"} \
+          ${optionalString (cfg.featureGates != [])
+            "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
+          --kubeconfig=${top.lib.mkKubeConfig "kube-controller-manager" cfg.kubeconfig} \
+          --leader-elect=${boolToString cfg.leaderElect} \
+          ${optionalString (cfg.rootCaFile!=null)
+            "--root-ca-file=${cfg.rootCaFile}"} \
+          --port=${toString cfg.insecurePort} \
+          --secure-port=${toString cfg.securePort} \
+          ${optionalString (cfg.serviceAccountKeyFile!=null)
+            "--service-account-private-key-file=${cfg.serviceAccountKeyFile}"} \
+          ${optionalString (cfg.tlsCertFile!=null)
+            "--tls-cert-file=${cfg.tlsCertFile}"} \
+          ${optionalString (cfg.tlsKeyFile!=null)
+            "--tls-private-key-file=${cfg.tlsKeyFile}"} \
+          ${optionalString (elem "RBAC" top.apiserver.authorizationMode)
+            "--use-service-account-credentials"} \
+          ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
+          ${cfg.extraOpts}
+        '';
+        WorkingDirectory = top.dataDir;
+        User = "kubernetes";
+        Group = "kubernetes";
+      };
+      unitConfig = {
+        StartLimitIntervalSec = 0;
+      };
+      path = top.path;
+    };
+
+    services.kubernetes.pki.certs = with top.lib; {
+      controllerManager = mkCert {
+        name = "kube-controller-manager";
+        CN = "kube-controller-manager";
+        action = "systemctl restart kube-controller-manager.service";
+      };
+      controllerManagerClient = mkCert {
+        name = "kube-controller-manager-client";
+        CN = "system:kube-controller-manager";
+        action = "systemctl restart kube-controller-manager.service";
+      };
+    };
+
+    services.kubernetes.controllerManager.kubeconfig.server = mkDefault top.apiserverAddress;
+  };
+
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/cluster/kubernetes/default.nix b/nixos/modules/services/cluster/kubernetes/default.nix
new file mode 100644
index 00000000000..35ec99d83c8
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/default.nix
@@ -0,0 +1,315 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.kubernetes;
+  opt = options.services.kubernetes;
+
+  defaultContainerdSettings = {
+    version = 2;
+    root = "/var/lib/containerd";
+    state = "/run/containerd";
+    oom_score = 0;
+
+    grpc = {
+      address = "/run/containerd/containerd.sock";
+    };
+
+    plugins."io.containerd.grpc.v1.cri" = {
+      sandbox_image = "pause:latest";
+
+      cni = {
+        bin_dir = "/opt/cni/bin";
+        max_conf_num = 0;
+      };
+
+      containerd.runtimes.runc = {
+        runtime_type = "io.containerd.runc.v2";
+        options.SystemdCgroup = true;
+      };
+    };
+  };
+
+  mkKubeConfig = name: conf: pkgs.writeText "${name}-kubeconfig" (builtins.toJSON {
+    apiVersion = "v1";
+    kind = "Config";
+    clusters = [{
+      name = "local";
+      cluster.certificate-authority = conf.caFile or cfg.caFile;
+      cluster.server = conf.server;
+    }];
+    users = [{
+      inherit name;
+      user = {
+        client-certificate = conf.certFile;
+        client-key = conf.keyFile;
+      };
+    }];
+    contexts = [{
+      context = {
+        cluster = "local";
+        user = name;
+      };
+      name = "local";
+    }];
+    current-context = "local";
+  });
+
+  caCert = secret "ca";
+
+  etcdEndpoints = ["https://${cfg.masterAddress}:2379"];
+
+  mkCert = { name, CN, hosts ? [], fields ? {}, action ? "",
+             privateKeyOwner ? "kubernetes" }: rec {
+    inherit name caCert CN hosts fields action;
+    cert = secret name;
+    key = secret "${name}-key";
+    privateKeyOptions = {
+      owner = privateKeyOwner;
+      group = "nogroup";
+      mode = "0600";
+      path = key;
+    };
+  };
+
+  secret = name: "${cfg.secretsPath}/${name}.pem";
+
+  mkKubeConfigOptions = prefix: {
+    server = mkOption {
+      description = "${prefix} kube-apiserver server address.";
+      type = types.str;
+    };
+
+    caFile = mkOption {
+      description = "${prefix} certificate authority file used to connect to kube-apiserver.";
+      type = types.nullOr types.path;
+      default = cfg.caFile;
+      defaultText = literalExpression "config.${opt.caFile}";
+    };
+
+    certFile = mkOption {
+      description = "${prefix} client certificate file used to connect to kube-apiserver.";
+      type = types.nullOr types.path;
+      default = null;
+    };
+
+    keyFile = mkOption {
+      description = "${prefix} client key file used to connect to kube-apiserver.";
+      type = types.nullOr types.path;
+      default = null;
+    };
+  };
+in {
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "kubernetes" "addons" "dashboard" ] "Removed due to it being an outdated version")
+    (mkRemovedOptionModule [ "services" "kubernetes" "verbose" ] "")
+  ];
+
+  ###### interface
+
+  options.services.kubernetes = {
+    roles = mkOption {
+      description = ''
+        Kubernetes role that this machine should take.
+
+        Master role will enable etcd, apiserver, scheduler, controller manager
+        addon manager, flannel and proxy services.
+        Node role will enable flannel, docker, kubelet and proxy services.
+      '';
+      default = [];
+      type = types.listOf (types.enum ["master" "node"]);
+    };
+
+    package = mkOption {
+      description = "Kubernetes package to use.";
+      type = types.package;
+      default = pkgs.kubernetes;
+      defaultText = literalExpression "pkgs.kubernetes";
+    };
+
+    kubeconfig = mkKubeConfigOptions "Default kubeconfig";
+
+    apiserverAddress = mkOption {
+      description = ''
+        Clusterwide accessible address for the kubernetes apiserver,
+        including protocol and optional port.
+      '';
+      example = "https://kubernetes-apiserver.example.com:6443";
+      type = types.str;
+    };
+
+    caFile = mkOption {
+      description = "Default kubernetes certificate authority";
+      type = types.nullOr types.path;
+      default = null;
+    };
+
+    dataDir = mkOption {
+      description = "Kubernetes root directory for managing kubelet files.";
+      default = "/var/lib/kubernetes";
+      type = types.path;
+    };
+
+    easyCerts = mkOption {
+      description = "Automatically setup x509 certificates and keys for the entire cluster.";
+      default = false;
+      type = types.bool;
+    };
+
+    featureGates = mkOption {
+      description = "List set of feature gates.";
+      default = [];
+      type = types.listOf types.str;
+    };
+
+    masterAddress = mkOption {
+      description = "Clusterwide available network address or hostname for the kubernetes master server.";
+      example = "master.example.com";
+      type = types.str;
+    };
+
+    path = mkOption {
+      description = "Packages added to the services' PATH environment variable. Both the bin and sbin subdirectories of each package are added.";
+      type = types.listOf types.package;
+      default = [];
+    };
+
+    clusterCidr = mkOption {
+      description = "Kubernetes controller manager and proxy CIDR Range for Pods in cluster.";
+      default = "10.1.0.0/16";
+      type = types.nullOr types.str;
+    };
+
+    lib = mkOption {
+      description = "Common functions for the kubernetes modules.";
+      default = {
+        inherit mkCert;
+        inherit mkKubeConfig;
+        inherit mkKubeConfigOptions;
+      };
+      type = types.attrs;
+    };
+
+    secretsPath = mkOption {
+      description = "Default location for kubernetes secrets. Not a store location.";
+      type = types.path;
+      default = cfg.dataDir + "/secrets";
+      defaultText = literalExpression ''
+        config.${opt.dataDir} + "/secrets"
+      '';
+    };
+  };
+
+  ###### implementation
+
+  config = mkMerge [
+
+    (mkIf cfg.easyCerts {
+      services.kubernetes.pki.enable = mkDefault true;
+      services.kubernetes.caFile = caCert;
+    })
+
+    (mkIf (elem "master" cfg.roles) {
+      services.kubernetes.apiserver.enable = mkDefault true;
+      services.kubernetes.scheduler.enable = mkDefault true;
+      services.kubernetes.controllerManager.enable = mkDefault true;
+      services.kubernetes.addonManager.enable = mkDefault true;
+      services.kubernetes.proxy.enable = mkDefault true;
+      services.etcd.enable = true; # Cannot mkDefault because of flannel default options
+      services.kubernetes.kubelet = {
+        enable = mkDefault true;
+        taints = mkIf (!(elem "node" cfg.roles)) {
+          master = {
+            key = "node-role.kubernetes.io/master";
+            value = "true";
+            effect = "NoSchedule";
+          };
+        };
+      };
+    })
+
+
+    (mkIf (all (el: el == "master") cfg.roles) {
+      # if this node is only a master make it unschedulable by default
+      services.kubernetes.kubelet.unschedulable = mkDefault true;
+    })
+
+    (mkIf (elem "node" cfg.roles) {
+      services.kubernetes.kubelet.enable = mkDefault true;
+      services.kubernetes.proxy.enable = mkDefault true;
+    })
+
+    # Using "services.kubernetes.roles" will automatically enable easyCerts and flannel
+    (mkIf (cfg.roles != []) {
+      services.kubernetes.flannel.enable = mkDefault true;
+      services.flannel.etcd.endpoints = mkDefault etcdEndpoints;
+      services.kubernetes.easyCerts = mkDefault true;
+    })
+
+    (mkIf cfg.apiserver.enable {
+      services.kubernetes.pki.etcClusterAdminKubeconfig = mkDefault "kubernetes/cluster-admin.kubeconfig";
+      services.kubernetes.apiserver.etcd.servers = mkDefault etcdEndpoints;
+    })
+
+    (mkIf cfg.kubelet.enable {
+      virtualisation.containerd = {
+        enable = mkDefault true;
+        settings = mapAttrsRecursive (name: mkDefault) defaultContainerdSettings;
+      };
+    })
+
+    (mkIf (cfg.apiserver.enable || cfg.controllerManager.enable) {
+      services.kubernetes.pki.certs = {
+        serviceAccount = mkCert {
+          name = "service-account";
+          CN = "system:service-account-signer";
+          action = ''
+            systemctl reload \
+              kube-apiserver.service \
+              kube-controller-manager.service
+          '';
+        };
+      };
+    })
+
+    (mkIf (
+        cfg.apiserver.enable ||
+        cfg.scheduler.enable ||
+        cfg.controllerManager.enable ||
+        cfg.kubelet.enable ||
+        cfg.proxy.enable ||
+        cfg.addonManager.enable
+    ) {
+      systemd.targets.kubernetes = {
+        description = "Kubernetes";
+        wantedBy = [ "multi-user.target" ];
+      };
+
+      systemd.tmpfiles.rules = [
+        "d /opt/cni/bin 0755 root root -"
+        "d /run/kubernetes 0755 kubernetes kubernetes -"
+        "d /var/lib/kubernetes 0755 kubernetes kubernetes -"
+      ];
+
+      users.users.kubernetes = {
+        uid = config.ids.uids.kubernetes;
+        description = "Kubernetes user";
+        group = "kubernetes";
+        home = cfg.dataDir;
+        createHome = true;
+      };
+      users.groups.kubernetes.gid = config.ids.gids.kubernetes;
+
+      # dns addon is enabled by default
+      services.kubernetes.addons.dns.enable = mkDefault true;
+
+      services.kubernetes.apiserverAddress = mkDefault ("https://${if cfg.apiserver.advertiseAddress != null
+                          then cfg.apiserver.advertiseAddress
+                          else "${cfg.masterAddress}:${toString cfg.apiserver.securePort}"}");
+    })
+  ];
+
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/cluster/kubernetes/flannel.nix b/nixos/modules/services/cluster/kubernetes/flannel.nix
new file mode 100644
index 00000000000..cb81eaaf016
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/flannel.nix
@@ -0,0 +1,100 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  cfg = top.flannel;
+
+  # we want flannel to use kubernetes itself as configuration backend, not direct etcd
+  storageBackend = "kubernetes";
+in
+{
+  ###### interface
+  options.services.kubernetes.flannel = {
+    enable = mkEnableOption "enable flannel networking";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.flannel = {
+
+      enable = mkDefault true;
+      network = mkDefault top.clusterCidr;
+      inherit storageBackend;
+      nodeName = config.services.kubernetes.kubelet.hostname;
+    };
+
+    services.kubernetes.kubelet = {
+      networkPlugin = mkDefault "cni";
+      cni.config = mkDefault [{
+        name = "mynet";
+        type = "flannel";
+        cniVersion = "0.3.1";
+        delegate = {
+          isDefaultGateway = true;
+          bridge = "mynet";
+        };
+      }];
+    };
+
+    networking = {
+      firewall.allowedUDPPorts = [
+        8285  # flannel udp
+        8472  # flannel vxlan
+      ];
+      dhcpcd.denyInterfaces = [ "mynet*" "flannel*" ];
+    };
+
+    services.kubernetes.pki.certs = {
+      flannelClient = top.lib.mkCert {
+        name = "flannel-client";
+        CN = "flannel-client";
+        action = "systemctl restart flannel.service";
+      };
+    };
+
+    # give flannel som kubernetes rbac permissions if applicable
+    services.kubernetes.addonManager.bootstrapAddons = mkIf ((storageBackend == "kubernetes") && (elem "RBAC" top.apiserver.authorizationMode)) {
+
+      flannel-cr = {
+        apiVersion = "rbac.authorization.k8s.io/v1";
+        kind = "ClusterRole";
+        metadata = { name = "flannel"; };
+        rules = [{
+          apiGroups = [ "" ];
+          resources = [ "pods" ];
+          verbs = [ "get" ];
+        }
+        {
+          apiGroups = [ "" ];
+          resources = [ "nodes" ];
+          verbs = [ "list" "watch" ];
+        }
+        {
+          apiGroups = [ "" ];
+          resources = [ "nodes/status" ];
+          verbs = [ "patch" ];
+        }];
+      };
+
+      flannel-crb = {
+        apiVersion = "rbac.authorization.k8s.io/v1";
+        kind = "ClusterRoleBinding";
+        metadata = { name = "flannel"; };
+        roleRef = {
+          apiGroup = "rbac.authorization.k8s.io";
+          kind = "ClusterRole";
+          name = "flannel";
+        };
+        subjects = [{
+          kind = "User";
+          name = "flannel-client";
+        }];
+      };
+
+    };
+  };
+
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/cluster/kubernetes/kubelet.nix b/nixos/modules/services/cluster/kubernetes/kubelet.nix
new file mode 100644
index 00000000000..af3a5062feb
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/kubelet.nix
@@ -0,0 +1,398 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  otop = options.services.kubernetes;
+  cfg = top.kubelet;
+
+  cniConfig =
+    if cfg.cni.config != [] && cfg.cni.configDir != null then
+      throw "Verbatim CNI-config and CNI configDir cannot both be set."
+    else if cfg.cni.configDir != null then
+      cfg.cni.configDir
+    else
+      (pkgs.buildEnv {
+        name = "kubernetes-cni-config";
+        paths = imap (i: entry:
+          pkgs.writeTextDir "${toString (10+i)}-${entry.type}.conf" (builtins.toJSON entry)
+        ) cfg.cni.config;
+      });
+
+  infraContainer = pkgs.dockerTools.buildImage {
+    name = "pause";
+    tag = "latest";
+    contents = top.package.pause;
+    config.Cmd = ["/bin/pause"];
+  };
+
+  kubeconfig = top.lib.mkKubeConfig "kubelet" cfg.kubeconfig;
+
+  manifestPath = "kubernetes/manifests";
+
+  taintOptions = with lib.types; { name, ... }: {
+    options = {
+      key = mkOption {
+        description = "Key of taint.";
+        default = name;
+        defaultText = literalDocBook "Name of this submodule.";
+        type = str;
+      };
+      value = mkOption {
+        description = "Value of taint.";
+        type = str;
+      };
+      effect = mkOption {
+        description = "Effect of taint.";
+        example = "NoSchedule";
+        type = enum ["NoSchedule" "PreferNoSchedule" "NoExecute"];
+      };
+    };
+  };
+
+  taints = concatMapStringsSep "," (v: "${v.key}=${v.value}:${v.effect}") (mapAttrsToList (n: v: v) cfg.taints);
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "applyManifests" ] "")
+    (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "cadvisorPort" ] "")
+    (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "allowPrivileged" ] "")
+  ];
+
+  ###### interface
+  options.services.kubernetes.kubelet = with lib.types; {
+
+    address = mkOption {
+      description = "Kubernetes kubelet info server listening address.";
+      default = "0.0.0.0";
+      type = str;
+    };
+
+    clusterDns = mkOption {
+      description = "Use alternative DNS.";
+      default = "10.1.0.1";
+      type = str;
+    };
+
+    clusterDomain = mkOption {
+      description = "Use alternative domain.";
+      default = config.services.kubernetes.addons.dns.clusterDomain;
+      defaultText = literalExpression "config.${options.services.kubernetes.addons.dns.clusterDomain}";
+      type = str;
+    };
+
+    clientCaFile = mkOption {
+      description = "Kubernetes apiserver CA file for client authentication.";
+      default = top.caFile;
+      defaultText = literalExpression "config.${otop.caFile}";
+      type = nullOr path;
+    };
+
+    cni = {
+      packages = mkOption {
+        description = "List of network plugin packages to install.";
+        type = listOf package;
+        default = [];
+      };
+
+      config = mkOption {
+        description = "Kubernetes CNI configuration.";
+        type = listOf attrs;
+        default = [];
+        example = literalExpression ''
+          [{
+            "cniVersion": "0.3.1",
+            "name": "mynet",
+            "type": "bridge",
+            "bridge": "cni0",
+            "isGateway": true,
+            "ipMasq": true,
+            "ipam": {
+                "type": "host-local",
+                "subnet": "10.22.0.0/16",
+                "routes": [
+                    { "dst": "0.0.0.0/0" }
+                ]
+            }
+          } {
+            "cniVersion": "0.3.1",
+            "type": "loopback"
+          }]
+        '';
+      };
+
+      configDir = mkOption {
+        description = "Path to Kubernetes CNI configuration directory.";
+        type = nullOr path;
+        default = null;
+      };
+    };
+
+    containerRuntime = mkOption {
+      description = "Which container runtime type to use";
+      type = enum ["docker" "remote"];
+      default = "remote";
+    };
+
+    containerRuntimeEndpoint = mkOption {
+      description = "Endpoint at which to find the container runtime api interface/socket";
+      type = str;
+      default = "unix:///run/containerd/containerd.sock";
+    };
+
+    enable = mkEnableOption "Kubernetes kubelet.";
+
+    extraOpts = mkOption {
+      description = "Kubernetes kubelet extra command line options.";
+      default = "";
+      type = separatedString " ";
+    };
+
+    featureGates = mkOption {
+      description = "List set of feature gates";
+      default = top.featureGates;
+      defaultText = literalExpression "config.${otop.featureGates}";
+      type = listOf str;
+    };
+
+    healthz = {
+      bind = mkOption {
+        description = "Kubernetes kubelet healthz listening address.";
+        default = "127.0.0.1";
+        type = str;
+      };
+
+      port = mkOption {
+        description = "Kubernetes kubelet healthz port.";
+        default = 10248;
+        type = int;
+      };
+    };
+
+    hostname = mkOption {
+      description = "Kubernetes kubelet hostname override.";
+      default = config.networking.hostName;
+      defaultText = literalExpression "config.networking.hostName";
+      type = str;
+    };
+
+    kubeconfig = top.lib.mkKubeConfigOptions "Kubelet";
+
+    manifests = mkOption {
+      description = "List of manifests to bootstrap with kubelet (only pods can be created as manifest entry)";
+      type = attrsOf attrs;
+      default = {};
+    };
+
+    networkPlugin = mkOption {
+      description = "Network plugin to use by Kubernetes.";
+      type = nullOr (enum ["cni" "kubenet"]);
+      default = "kubenet";
+    };
+
+    nodeIp = mkOption {
+      description = "IP address of the node. If set, kubelet will use this IP address for the node.";
+      default = null;
+      type = nullOr str;
+    };
+
+    registerNode = mkOption {
+      description = "Whether to auto register kubelet with API server.";
+      default = true;
+      type = bool;
+    };
+
+    port = mkOption {
+      description = "Kubernetes kubelet info server listening port.";
+      default = 10250;
+      type = int;
+    };
+
+    seedDockerImages = mkOption {
+      description = "List of docker images to preload on system";
+      default = [];
+      type = listOf package;
+    };
+
+    taints = mkOption {
+      description = "Node taints (https://kubernetes.io/docs/concepts/configuration/assign-pod-node/).";
+      default = {};
+      type = attrsOf (submodule [ taintOptions ]);
+    };
+
+    tlsCertFile = mkOption {
+      description = "File containing x509 Certificate for HTTPS.";
+      default = null;
+      type = nullOr path;
+    };
+
+    tlsKeyFile = mkOption {
+      description = "File containing x509 private key matching tlsCertFile.";
+      default = null;
+      type = nullOr path;
+    };
+
+    unschedulable = mkOption {
+      description = "Whether to set node taint to unschedulable=true as it is the case of node that has only master role.";
+      default = false;
+      type = bool;
+    };
+
+    verbosity = mkOption {
+      description = ''
+        Optional glog verbosity level for logging statements. See
+        <link xlink:href="https://github.com/kubernetes/community/blob/master/contributors/devel/logging.md"/>
+      '';
+      default = null;
+      type = nullOr int;
+    };
+
+  };
+
+  ###### implementation
+  config = mkMerge [
+    (mkIf cfg.enable {
+
+      environment.etc."cni/net.d".source = cniConfig;
+
+      services.kubernetes.kubelet.seedDockerImages = [infraContainer];
+
+      boot.kernel.sysctl = {
+        "net.bridge.bridge-nf-call-iptables"  = 1;
+        "net.ipv4.ip_forward"                 = 1;
+        "net.bridge.bridge-nf-call-ip6tables" = 1;
+      };
+
+      systemd.services.kubelet = {
+        description = "Kubernetes Kubelet Service";
+        wantedBy = [ "kubernetes.target" ];
+        after = [ "containerd.service" "network.target" "kube-apiserver.service" ];
+        path = with pkgs; [
+          gitMinimal
+          openssh
+          util-linux
+          iproute2
+          ethtool
+          thin-provisioning-tools
+          iptables
+          socat
+        ] ++ lib.optional config.boot.zfs.enabled config.boot.zfs.package ++ top.path;
+        preStart = ''
+          ${concatMapStrings (img: ''
+            echo "Seeding container image: ${img}"
+            ${if (lib.hasSuffix "gz" img) then
+              ''${pkgs.gzip}/bin/zcat "${img}" | ${pkgs.containerd}/bin/ctr -n k8s.io image import --all-platforms -''
+            else
+              ''${pkgs.coreutils}/bin/cat "${img}" | ${pkgs.containerd}/bin/ctr -n k8s.io image import --all-platforms -''
+            }
+          '') cfg.seedDockerImages}
+
+          rm /opt/cni/bin/* || true
+          ${concatMapStrings (package: ''
+            echo "Linking cni package: ${package}"
+            ln -fs ${package}/bin/* /opt/cni/bin
+          '') cfg.cni.packages}
+        '';
+        serviceConfig = {
+          Slice = "kubernetes.slice";
+          CPUAccounting = true;
+          MemoryAccounting = true;
+          Restart = "on-failure";
+          RestartSec = "1000ms";
+          ExecStart = ''${top.package}/bin/kubelet \
+            --address=${cfg.address} \
+            --authentication-token-webhook \
+            --authentication-token-webhook-cache-ttl="10s" \
+            --authorization-mode=Webhook \
+            ${optionalString (cfg.clientCaFile != null)
+              "--client-ca-file=${cfg.clientCaFile}"} \
+            ${optionalString (cfg.clusterDns != "")
+              "--cluster-dns=${cfg.clusterDns}"} \
+            ${optionalString (cfg.clusterDomain != "")
+              "--cluster-domain=${cfg.clusterDomain}"} \
+            --cni-conf-dir=${cniConfig} \
+            ${optionalString (cfg.featureGates != [])
+              "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
+            --hairpin-mode=hairpin-veth \
+            --healthz-bind-address=${cfg.healthz.bind} \
+            --healthz-port=${toString cfg.healthz.port} \
+            --hostname-override=${cfg.hostname} \
+            --kubeconfig=${kubeconfig} \
+            ${optionalString (cfg.networkPlugin != null)
+              "--network-plugin=${cfg.networkPlugin}"} \
+            ${optionalString (cfg.nodeIp != null)
+              "--node-ip=${cfg.nodeIp}"} \
+            --pod-infra-container-image=pause \
+            ${optionalString (cfg.manifests != {})
+              "--pod-manifest-path=/etc/${manifestPath}"} \
+            --port=${toString cfg.port} \
+            --register-node=${boolToString cfg.registerNode} \
+            ${optionalString (taints != "")
+              "--register-with-taints=${taints}"} \
+            --root-dir=${top.dataDir} \
+            ${optionalString (cfg.tlsCertFile != null)
+              "--tls-cert-file=${cfg.tlsCertFile}"} \
+            ${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}
+          '';
+          WorkingDirectory = top.dataDir;
+        };
+        unitConfig = {
+          StartLimitIntervalSec = 0;
+        };
+      };
+
+      # Allways include cni plugins
+      services.kubernetes.kubelet.cni.packages = [pkgs.cni-plugins pkgs.cni-plugin-flannel];
+
+      boot.kernelModules = ["br_netfilter" "overlay"];
+
+      services.kubernetes.kubelet.hostname = with config.networking;
+        mkDefault (hostName + optionalString (domain != null) ".${domain}");
+
+      services.kubernetes.pki.certs = with top.lib; {
+        kubelet = mkCert {
+          name = "kubelet";
+          CN = top.kubelet.hostname;
+          action = "systemctl restart kubelet.service";
+
+        };
+        kubeletClient = mkCert {
+          name = "kubelet-client";
+          CN = "system:node:${top.kubelet.hostname}";
+          fields = {
+            O = "system:nodes";
+          };
+          action = "systemctl restart kubelet.service";
+        };
+      };
+
+      services.kubernetes.kubelet.kubeconfig.server = mkDefault top.apiserverAddress;
+    })
+
+    (mkIf (cfg.enable && cfg.manifests != {}) {
+      environment.etc = mapAttrs' (name: manifest:
+        nameValuePair "${manifestPath}/${name}.json" {
+          text = builtins.toJSON manifest;
+          mode = "0755";
+        }
+      ) cfg.manifests;
+    })
+
+    (mkIf (cfg.unschedulable && cfg.enable) {
+      services.kubernetes.kubelet.taints.unschedulable = {
+        value = "true";
+        effect = "NoSchedule";
+      };
+    })
+
+  ];
+
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/cluster/kubernetes/pki.nix b/nixos/modules/services/cluster/kubernetes/pki.nix
new file mode 100644
index 00000000000..7d9198d20e8
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/pki.nix
@@ -0,0 +1,406 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  cfg = top.pki;
+
+  csrCA = pkgs.writeText "kube-pki-cacert-csr.json" (builtins.toJSON {
+    key = {
+        algo = "rsa";
+        size = 2048;
+    };
+    names = singleton cfg.caSpec;
+  });
+
+  csrCfssl = pkgs.writeText "kube-pki-cfssl-csr.json" (builtins.toJSON {
+    key = {
+        algo = "rsa";
+        size = 2048;
+    };
+    CN = top.masterAddress;
+    hosts = [top.masterAddress] ++ cfg.cfsslAPIExtraSANs;
+  });
+
+  cfsslAPITokenBaseName = "apitoken.secret";
+  cfsslAPITokenPath = "${config.services.cfssl.dataDir}/${cfsslAPITokenBaseName}";
+  certmgrAPITokenPath = "${top.secretsPath}/${cfsslAPITokenBaseName}";
+  cfsslAPITokenLength = 32;
+
+  clusterAdminKubeconfig = with cfg.certs.clusterAdmin;
+    top.lib.mkKubeConfig "cluster-admin" {
+        server = top.apiserverAddress;
+        certFile = cert;
+        keyFile = key;
+    };
+
+  remote = with config.services; "https://${kubernetes.masterAddress}:${toString cfssl.port}";
+in
+{
+  ###### interface
+  options.services.kubernetes.pki = with lib.types; {
+
+    enable = mkEnableOption "easyCert issuer service";
+
+    certs = mkOption {
+      description = "List of certificate specs to feed to cert generator.";
+      default = {};
+      type = attrs;
+    };
+
+    genCfsslCACert = mkOption {
+      description = ''
+        Whether to automatically generate cfssl CA certificate and key,
+        if they don't exist.
+      '';
+      default = true;
+      type = bool;
+    };
+
+    genCfsslAPICerts = mkOption {
+      description = ''
+        Whether to automatically generate cfssl API webserver TLS cert and key,
+        if they don't exist.
+      '';
+      default = true;
+      type = bool;
+    };
+
+    cfsslAPIExtraSANs = mkOption {
+      description = ''
+        Extra x509 Subject Alternative Names to be added to the cfssl API webserver TLS cert.
+      '';
+      default = [];
+      example = [ "subdomain.example.com" ];
+      type = listOf str;
+    };
+
+    genCfsslAPIToken = mkOption {
+      description = ''
+        Whether to automatically generate cfssl API-token secret,
+        if they doesn't exist.
+      '';
+      default = true;
+      type = bool;
+    };
+
+    pkiTrustOnBootstrap = mkOption {
+      description = "Whether to always trust remote cfssl server upon initial PKI bootstrap.";
+      default = true;
+      type = bool;
+    };
+
+    caCertPathPrefix = mkOption {
+      description = ''
+        Path-prefrix for the CA-certificate to be used for cfssl signing.
+        Suffixes ".pem" and "-key.pem" will be automatically appended for
+        the public and private keys respectively.
+      '';
+      default = "${config.services.cfssl.dataDir}/ca";
+      defaultText = literalExpression ''"''${config.services.cfssl.dataDir}/ca"'';
+      type = str;
+    };
+
+    caSpec = mkOption {
+      description = "Certificate specification for the auto-generated CAcert.";
+      default = {
+        CN = "kubernetes-cluster-ca";
+        O = "NixOS";
+        OU = "services.kubernetes.pki.caSpec";
+        L = "auto-generated";
+      };
+      type = attrs;
+    };
+
+    etcClusterAdminKubeconfig = mkOption {
+      description = ''
+        Symlink a kubeconfig with cluster-admin privileges to environment path
+        (/etc/&lt;path&gt;).
+      '';
+      default = null;
+      type = nullOr str;
+    };
+
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable
+  (let
+    cfsslCertPathPrefix = "${config.services.cfssl.dataDir}/cfssl";
+    cfsslCert = "${cfsslCertPathPrefix}.pem";
+    cfsslKey = "${cfsslCertPathPrefix}-key.pem";
+  in
+  {
+
+    services.cfssl = mkIf (top.apiserver.enable) {
+      enable = true;
+      address = "0.0.0.0";
+      tlsCert = cfsslCert;
+      tlsKey = cfsslKey;
+      configFile = toString (pkgs.writeText "cfssl-config.json" (builtins.toJSON {
+        signing = {
+          profiles = {
+            default = {
+              usages = ["digital signature"];
+              auth_key = "default";
+              expiry = "720h";
+            };
+          };
+        };
+        auth_keys = {
+          default = {
+            type = "standard";
+            key = "file:${cfsslAPITokenPath}";
+          };
+        };
+      }));
+    };
+
+    systemd.services.cfssl.preStart = with pkgs; with config.services.cfssl; mkIf (top.apiserver.enable)
+    (concatStringsSep "\n" [
+      "set -e"
+      (optionalString cfg.genCfsslCACert ''
+        if [ ! -f "${cfg.caCertPathPrefix}.pem" ]; then
+          ${cfssl}/bin/cfssl genkey -initca ${csrCA} | \
+            ${cfssl}/bin/cfssljson -bare ${cfg.caCertPathPrefix}
+        fi
+      '')
+      (optionalString cfg.genCfsslAPICerts ''
+        if [ ! -f "${dataDir}/cfssl.pem" ]; then
+          ${cfssl}/bin/cfssl gencert -ca "${cfg.caCertPathPrefix}.pem" -ca-key "${cfg.caCertPathPrefix}-key.pem" ${csrCfssl} | \
+            ${cfssl}/bin/cfssljson -bare ${cfsslCertPathPrefix}
+        fi
+      '')
+      (optionalString cfg.genCfsslAPIToken ''
+        if [ ! -f "${cfsslAPITokenPath}" ]; then
+          head -c ${toString (cfsslAPITokenLength / 2)} /dev/urandom | od -An -t x | tr -d ' ' >"${cfsslAPITokenPath}"
+        fi
+        chown cfssl "${cfsslAPITokenPath}" && chmod 400 "${cfsslAPITokenPath}"
+      '')]);
+
+    systemd.services.kube-certmgr-bootstrap = {
+      description = "Kubernetes certmgr bootstrapper";
+      wantedBy = [ "certmgr.service" ];
+      after = [ "cfssl.target" ];
+      script = concatStringsSep "\n" [''
+        set -e
+
+        # If there's a cfssl (cert issuer) running locally, then don't rely on user to
+        # manually paste it in place. Just symlink.
+        # otherwise, create the target file, ready for users to insert the token
+
+        mkdir -p "$(dirname "${certmgrAPITokenPath}")"
+        if [ -f "${cfsslAPITokenPath}" ]; then
+          ln -fs "${cfsslAPITokenPath}" "${certmgrAPITokenPath}"
+        else
+          touch "${certmgrAPITokenPath}" && chmod 600 "${certmgrAPITokenPath}"
+        fi
+      ''
+      (optionalString (cfg.pkiTrustOnBootstrap) ''
+        if [ ! -f "${top.caFile}" ] || [ $(cat "${top.caFile}" | wc -c) -lt 1 ]; then
+          ${pkgs.curl}/bin/curl --fail-early -f -kd '{}' ${remote}/api/v1/cfssl/info | \
+            ${pkgs.cfssl}/bin/cfssljson -stdout >${top.caFile}
+        fi
+      '')
+      ];
+      serviceConfig = {
+        RestartSec = "10s";
+        Restart = "on-failure";
+      };
+    };
+
+    services.certmgr = {
+      enable = true;
+      package = pkgs.certmgr-selfsigned;
+      svcManager = "command";
+      specs =
+        let
+          mkSpec = _: cert: {
+            inherit (cert) action;
+            authority = {
+              inherit remote;
+              file.path = cert.caCert;
+              root_ca = cert.caCert;
+              profile = "default";
+              auth_key_file = certmgrAPITokenPath;
+            };
+            certificate = {
+              path = cert.cert;
+            };
+            private_key = cert.privateKeyOptions;
+            request = {
+              hosts = [cert.CN] ++ cert.hosts;
+              inherit (cert) CN;
+              key = {
+                algo = "rsa";
+                size = 2048;
+              };
+              names = [ cert.fields ];
+            };
+          };
+        in
+          mapAttrs mkSpec cfg.certs;
+      };
+
+      #TODO: Get rid of kube-addon-manager in the future for the following reasons
+      # - it is basically just a shell script wrapped around kubectl
+      # - it assumes that it is clusterAdmin or can gain clusterAdmin rights through serviceAccount
+      # - it is designed to be used with k8s system components only
+      # - it would be better with a more Nix-oriented way of managing addons
+      systemd.services.kube-addon-manager = mkIf top.addonManager.enable (mkMerge [{
+        environment.KUBECONFIG = with cfg.certs.addonManager;
+          top.lib.mkKubeConfig "addon-manager" {
+            server = top.apiserverAddress;
+            certFile = cert;
+            keyFile = key;
+          };
+        }
+
+        (optionalAttrs (top.addonManager.bootstrapAddons != {}) {
+          serviceConfig.PermissionsStartOnly = true;
+          preStart = with pkgs;
+          let
+            files = mapAttrsToList (n: v: writeText "${n}.json" (builtins.toJSON v))
+              top.addonManager.bootstrapAddons;
+          in
+          ''
+            export KUBECONFIG=${clusterAdminKubeconfig}
+            ${kubernetes}/bin/kubectl apply -f ${concatStringsSep " \\\n -f " files}
+          '';
+        })]);
+
+      environment.etc.${cfg.etcClusterAdminKubeconfig}.source = mkIf (!isNull cfg.etcClusterAdminKubeconfig)
+        clusterAdminKubeconfig;
+
+      environment.systemPackages = mkIf (top.kubelet.enable || top.proxy.enable) [
+      (pkgs.writeScriptBin "nixos-kubernetes-node-join" ''
+        set -e
+        exec 1>&2
+
+        if [ $# -gt 0 ]; then
+          echo "Usage: $(basename $0)"
+          echo ""
+          echo "No args. Apitoken must be provided on stdin."
+          echo "To get the apitoken, execute: 'sudo cat ${certmgrAPITokenPath}' on the master node."
+          exit 1
+        fi
+
+        if [ $(id -u) != 0 ]; then
+          echo "Run as root please."
+          exit 1
+        fi
+
+        read -r token
+        if [ ''${#token} != ${toString cfsslAPITokenLength} ]; then
+          echo "Token must be of length ${toString cfsslAPITokenLength}."
+          exit 1
+        fi
+
+        echo $token > ${certmgrAPITokenPath}
+        chmod 600 ${certmgrAPITokenPath}
+
+        echo "Restarting certmgr..." >&1
+        systemctl restart certmgr
+
+        echo "Waiting for certs to appear..." >&1
+
+        ${optionalString top.kubelet.enable ''
+          while [ ! -f ${cfg.certs.kubelet.cert} ]; do sleep 1; done
+          echo "Restarting kubelet..." >&1
+          systemctl restart kubelet
+        ''}
+
+        ${optionalString top.proxy.enable ''
+          while [ ! -f ${cfg.certs.kubeProxyClient.cert} ]; do sleep 1; done
+          echo "Restarting kube-proxy..." >&1
+          systemctl restart kube-proxy
+        ''}
+
+        ${optionalString top.flannel.enable ''
+          while [ ! -f ${cfg.certs.flannelClient.cert} ]; do sleep 1; done
+          echo "Restarting flannel..." >&1
+          systemctl restart flannel
+        ''}
+
+        echo "Node joined succesfully"
+      '')];
+
+      # isolate etcd on loopback at the master node
+      # easyCerts doesn't support multimaster clusters anyway atm.
+      services.etcd = with cfg.certs.etcd; {
+        listenClientUrls = ["https://127.0.0.1:2379"];
+        listenPeerUrls = ["https://127.0.0.1:2380"];
+        advertiseClientUrls = ["https://etcd.local:2379"];
+        initialCluster = ["${top.masterAddress}=https://etcd.local:2380"];
+        initialAdvertisePeerUrls = ["https://etcd.local:2380"];
+        certFile = mkDefault cert;
+        keyFile = mkDefault key;
+        trustedCaFile = mkDefault caCert;
+      };
+      networking.extraHosts = mkIf (config.services.etcd.enable) ''
+        127.0.0.1 etcd.${top.addons.dns.clusterDomain} etcd.local
+      '';
+
+      services.flannel = with cfg.certs.flannelClient; {
+        kubeconfig = top.lib.mkKubeConfig "flannel" {
+          server = top.apiserverAddress;
+          certFile = cert;
+          keyFile = key;
+        };
+      };
+
+      services.kubernetes = {
+
+        apiserver = mkIf top.apiserver.enable (with cfg.certs.apiServer; {
+          etcd = with cfg.certs.apiserverEtcdClient; {
+            servers = ["https://etcd.local:2379"];
+            certFile = mkDefault cert;
+            keyFile = mkDefault key;
+            caFile = mkDefault caCert;
+          };
+          clientCaFile = mkDefault caCert;
+          tlsCertFile = mkDefault cert;
+          tlsKeyFile = mkDefault key;
+          serviceAccountKeyFile = mkDefault cfg.certs.serviceAccount.cert;
+          serviceAccountSigningKeyFile = mkDefault cfg.certs.serviceAccount.key;
+          kubeletClientCaFile = mkDefault caCert;
+          kubeletClientCertFile = mkDefault cfg.certs.apiserverKubeletClient.cert;
+          kubeletClientKeyFile = mkDefault cfg.certs.apiserverKubeletClient.key;
+          proxyClientCertFile = mkDefault cfg.certs.apiserverProxyClient.cert;
+          proxyClientKeyFile = mkDefault cfg.certs.apiserverProxyClient.key;
+        });
+        controllerManager = mkIf top.controllerManager.enable {
+          serviceAccountKeyFile = mkDefault cfg.certs.serviceAccount.key;
+          rootCaFile = cfg.certs.controllerManagerClient.caCert;
+          kubeconfig = with cfg.certs.controllerManagerClient; {
+            certFile = mkDefault cert;
+            keyFile = mkDefault key;
+          };
+        };
+        scheduler = mkIf top.scheduler.enable {
+          kubeconfig = with cfg.certs.schedulerClient; {
+            certFile = mkDefault cert;
+            keyFile = mkDefault key;
+          };
+        };
+        kubelet = mkIf top.kubelet.enable {
+          clientCaFile = mkDefault cfg.certs.kubelet.caCert;
+          tlsCertFile = mkDefault cfg.certs.kubelet.cert;
+          tlsKeyFile = mkDefault cfg.certs.kubelet.key;
+          kubeconfig = with cfg.certs.kubeletClient; {
+            certFile = mkDefault cert;
+            keyFile = mkDefault key;
+          };
+        };
+        proxy = mkIf top.proxy.enable {
+          kubeconfig = with cfg.certs.kubeProxyClient; {
+            certFile = mkDefault cert;
+            keyFile = mkDefault key;
+          };
+        };
+      };
+    });
+
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/cluster/kubernetes/proxy.nix b/nixos/modules/services/cluster/kubernetes/proxy.nix
new file mode 100644
index 00000000000..0fd98d1c157
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/proxy.nix
@@ -0,0 +1,102 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  otop = options.services.kubernetes;
+  cfg = top.proxy;
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "kubernetes" "proxy" "address" ] ["services" "kubernetes" "proxy" "bindAddress"])
+  ];
+
+  ###### interface
+  options.services.kubernetes.proxy = with lib.types; {
+
+    bindAddress = mkOption {
+      description = "Kubernetes proxy listening address.";
+      default = "0.0.0.0";
+      type = str;
+    };
+
+    enable = mkEnableOption "Kubernetes proxy";
+
+    extraOpts = mkOption {
+      description = "Kubernetes proxy extra command line options.";
+      default = "";
+      type = separatedString " ";
+    };
+
+    featureGates = mkOption {
+      description = "List set of feature gates";
+      default = top.featureGates;
+      defaultText = literalExpression "config.${otop.featureGates}";
+      type = listOf str;
+    };
+
+    hostname = mkOption {
+      description = "Kubernetes proxy hostname override.";
+      default = config.networking.hostName;
+      defaultText = literalExpression "config.networking.hostName";
+      type = str;
+    };
+
+    kubeconfig = top.lib.mkKubeConfigOptions "Kubernetes proxy";
+
+    verbosity = mkOption {
+      description = ''
+        Optional glog verbosity level for logging statements. See
+        <link xlink:href="https://github.com/kubernetes/community/blob/master/contributors/devel/logging.md"/>
+      '';
+      default = null;
+      type = nullOr int;
+    };
+
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    systemd.services.kube-proxy = {
+      description = "Kubernetes Proxy Service";
+      wantedBy = [ "kubernetes.target" ];
+      after = [ "kube-apiserver.service" ];
+      path = with pkgs; [ iptables conntrack-tools ];
+      serviceConfig = {
+        Slice = "kubernetes.slice";
+        ExecStart = ''${top.package}/bin/kube-proxy \
+          --bind-address=${cfg.bindAddress} \
+          ${optionalString (top.clusterCidr!=null)
+            "--cluster-cidr=${top.clusterCidr}"} \
+          ${optionalString (cfg.featureGates != [])
+            "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
+          --hostname-override=${cfg.hostname} \
+          --kubeconfig=${top.lib.mkKubeConfig "kube-proxy" cfg.kubeconfig} \
+          ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
+          ${cfg.extraOpts}
+        '';
+        WorkingDirectory = top.dataDir;
+        Restart = "on-failure";
+        RestartSec = 5;
+      };
+      unitConfig = {
+        StartLimitIntervalSec = 0;
+      };
+    };
+
+    services.kubernetes.proxy.hostname = with config.networking; mkDefault hostName;
+
+    services.kubernetes.pki.certs = {
+      kubeProxyClient = top.lib.mkCert {
+        name = "kube-proxy-client";
+        CN = "system:kube-proxy";
+        action = "systemctl restart kube-proxy.service";
+      };
+    };
+
+    services.kubernetes.proxy.kubeconfig.server = mkDefault top.apiserverAddress;
+  };
+
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/cluster/kubernetes/scheduler.nix b/nixos/modules/services/cluster/kubernetes/scheduler.nix
new file mode 100644
index 00000000000..2d95528a6ea
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/scheduler.nix
@@ -0,0 +1,101 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  otop = options.services.kubernetes;
+  cfg = top.scheduler;
+in
+{
+  ###### interface
+  options.services.kubernetes.scheduler = with lib.types; {
+
+    address = mkOption {
+      description = "Kubernetes scheduler listening address.";
+      default = "127.0.0.1";
+      type = str;
+    };
+
+    enable = mkEnableOption "Kubernetes scheduler";
+
+    extraOpts = mkOption {
+      description = "Kubernetes scheduler extra command line options.";
+      default = "";
+      type = separatedString " ";
+    };
+
+    featureGates = mkOption {
+      description = "List set of feature gates";
+      default = top.featureGates;
+      defaultText = literalExpression "config.${otop.featureGates}";
+      type = listOf str;
+    };
+
+    kubeconfig = top.lib.mkKubeConfigOptions "Kubernetes scheduler";
+
+    leaderElect = mkOption {
+      description = "Whether to start leader election before executing main loop.";
+      type = bool;
+      default = true;
+    };
+
+    port = mkOption {
+      description = "Kubernetes scheduler listening port.";
+      default = 10251;
+      type = int;
+    };
+
+    verbosity = mkOption {
+      description = ''
+        Optional glog verbosity level for logging statements. See
+        <link xlink:href="https://github.com/kubernetes/community/blob/master/contributors/devel/logging.md"/>
+      '';
+      default = null;
+      type = nullOr int;
+    };
+
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    systemd.services.kube-scheduler = {
+      description = "Kubernetes Scheduler Service";
+      wantedBy = [ "kubernetes.target" ];
+      after = [ "kube-apiserver.service" ];
+      serviceConfig = {
+        Slice = "kubernetes.slice";
+        ExecStart = ''${top.package}/bin/kube-scheduler \
+          --bind-address=${cfg.address} \
+          ${optionalString (cfg.featureGates != [])
+            "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
+          --kubeconfig=${top.lib.mkKubeConfig "kube-scheduler" cfg.kubeconfig} \
+          --leader-elect=${boolToString cfg.leaderElect} \
+          --secure-port=${toString cfg.port} \
+          ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
+          ${cfg.extraOpts}
+        '';
+        WorkingDirectory = top.dataDir;
+        User = "kubernetes";
+        Group = "kubernetes";
+        Restart = "on-failure";
+        RestartSec = 5;
+      };
+      unitConfig = {
+        StartLimitIntervalSec = 0;
+      };
+    };
+
+    services.kubernetes.pki.certs = {
+      schedulerClient = top.lib.mkCert {
+        name = "kube-scheduler-client";
+        CN = "system:kube-scheduler";
+        action = "systemctl restart kube-scheduler.service";
+      };
+    };
+
+    services.kubernetes.scheduler.kubeconfig.server = mkDefault top.apiserverAddress;
+  };
+
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/cluster/pacemaker/default.nix b/nixos/modules/services/cluster/pacemaker/default.nix
new file mode 100644
index 00000000000..7eeadffcc58
--- /dev/null
+++ b/nixos/modules/services/cluster/pacemaker/default.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.pacemaker;
+in
+{
+  # interface
+  options.services.pacemaker = {
+    enable = mkEnableOption "pacemaker";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.pacemaker;
+      defaultText = literalExpression "pkgs.pacemaker";
+      description = "Package that should be used for pacemaker.";
+    };
+  };
+
+  # implementation
+  config = mkIf cfg.enable {
+    assertions = [ {
+      assertion = config.services.corosync.enable;
+      message = ''
+        Enabling services.pacemaker requires a services.corosync configuration.
+      '';
+    } ];
+
+    environment.systemPackages = [ cfg.package ];
+
+    # required by pacemaker
+    users.users.hacluster = {
+      isSystemUser = true;
+      group = "pacemaker";
+      home = "/var/lib/pacemaker";
+    };
+    users.groups.pacemaker = {};
+
+    systemd.tmpfiles.rules = [
+      "d /var/log/pacemaker 0700 hacluster pacemaker -"
+    ];
+
+    systemd.packages = [ cfg.package ];
+    systemd.services.pacemaker = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        StateDirectory = "pacemaker";
+        StateDirectoryMode = "0700";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/cluster/spark/default.nix b/nixos/modules/services/cluster/spark/default.nix
new file mode 100644
index 00000000000..e6b44e130a3
--- /dev/null
+++ b/nixos/modules/services/cluster/spark/default.nix
@@ -0,0 +1,162 @@
+{config, pkgs, lib, ...}:
+let
+  cfg = config.services.spark;
+in
+with lib;
+{
+  options = {
+    services.spark = {
+      master = {
+        enable = mkEnableOption "Spark master service";
+        bind = mkOption {
+          type = types.str;
+          description = "Address the spark master binds to.";
+          default = "127.0.0.1";
+          example = "0.0.0.0";
+        };
+        restartIfChanged  = mkOption {
+          type = types.bool;
+          description = ''
+            Automatically restart master service on config change.
+            This can be set to false to defer restarts on clusters running critical applications.
+            Please consider the security implications of inadvertently running an older version,
+            and the possibility of unexpected behavior caused by inconsistent versions across a cluster when disabling this option.
+          '';
+          default = true;
+        };
+        extraEnvironment = mkOption {
+          type = types.attrsOf types.str;
+          description = "Extra environment variables to pass to spark master. See spark-standalone documentation.";
+          default = {};
+          example = {
+            SPARK_MASTER_WEBUI_PORT = 8181;
+            SPARK_MASTER_OPTS = "-Dspark.deploy.defaultCores=5";
+          };
+        };
+      };
+      worker = {
+        enable = mkEnableOption "Spark worker service";
+        workDir = mkOption {
+          type = types.path;
+          description = "Spark worker work dir.";
+          default = "/var/lib/spark";
+        };
+        master = mkOption {
+          type = types.str;
+          description = "Address of the spark master.";
+          default = "127.0.0.1:7077";
+        };
+        restartIfChanged  = mkOption {
+          type = types.bool;
+          description = ''
+            Automatically restart worker service on config change.
+            This can be set to false to defer restarts on clusters running critical applications.
+            Please consider the security implications of inadvertently running an older version,
+            and the possibility of unexpected behavior caused by inconsistent versions across a cluster when disabling this option.
+          '';
+          default = true;
+        };
+        extraEnvironment = mkOption {
+          type = types.attrsOf types.str;
+          description = "Extra environment variables to pass to spark worker.";
+          default = {};
+          example = {
+            SPARK_WORKER_CORES = 5;
+            SPARK_WORKER_MEMORY = "2g";
+          };
+        };
+      };
+      confDir = mkOption {
+        type = types.path;
+        description = "Spark configuration directory. Spark will use the configuration files (spark-defaults.conf, spark-env.sh, log4j.properties, etc) from this directory.";
+        default = "${cfg.package}/lib/${cfg.package.untarDir}/conf";
+        defaultText = literalExpression ''"''${package}/lib/''${package.untarDir}/conf"'';
+      };
+      logDir = mkOption {
+        type = types.path;
+        description = "Spark log directory.";
+        default = "/var/log/spark";
+      };
+      package = mkOption {
+        type = types.package;
+        description = "Spark package.";
+        default = pkgs.spark;
+        defaultText = literalExpression "pkgs.spark";
+        example = literalExpression ''pkgs.spark.overrideAttrs (super: rec {
+          pname = "spark";
+          version = "2.4.4";
+
+          src = pkgs.fetchzip {
+            url    = "mirror://apache/spark/"''${pname}-''${version}/''${pname}-''${version}-bin-without-hadoop.tgz";
+            sha256 = "1a9w5k0207fysgpxx6db3a00fs5hdc2ncx99x4ccy2s0v5ndc66g";
+          };
+        })'';
+      };
+    };
+  };
+  config = lib.mkIf (cfg.worker.enable || cfg.master.enable) {
+    environment.systemPackages = [ cfg.package ];
+    systemd = {
+      services = {
+        spark-master = lib.mkIf cfg.master.enable {
+          path = with pkgs; [ procps openssh nettools ];
+          description = "spark master service.";
+          after = [ "network.target" ];
+          wantedBy = [ "multi-user.target" ];
+          restartIfChanged = cfg.master.restartIfChanged;
+          environment = cfg.master.extraEnvironment // {
+            SPARK_MASTER_HOST = cfg.master.bind;
+            SPARK_CONF_DIR = cfg.confDir;
+            SPARK_LOG_DIR = cfg.logDir;
+          };
+          serviceConfig = {
+            Type = "forking";
+            User = "spark";
+            Group = "spark";
+            WorkingDirectory = "${cfg.package}/lib/${cfg.package.untarDir}";
+            ExecStart = "${cfg.package}/lib/${cfg.package.untarDir}/sbin/start-master.sh";
+            ExecStop  = "${cfg.package}/lib/${cfg.package.untarDir}/sbin/stop-master.sh";
+            TimeoutSec = 300;
+            StartLimitBurst=10;
+            Restart = "always";
+          };
+        };
+        spark-worker = lib.mkIf cfg.worker.enable {
+          path = with pkgs; [ procps openssh nettools rsync ];
+          description = "spark master service.";
+          after = [ "network.target" ];
+          wantedBy = [ "multi-user.target" ];
+          restartIfChanged = cfg.worker.restartIfChanged;
+          environment = cfg.worker.extraEnvironment // {
+            SPARK_MASTER = cfg.worker.master;
+            SPARK_CONF_DIR = cfg.confDir;
+            SPARK_LOG_DIR = cfg.logDir;
+            SPARK_WORKER_DIR = cfg.worker.workDir;
+          };
+          serviceConfig = {
+            Type = "forking";
+            User = "spark";
+            WorkingDirectory = "${cfg.package}/lib/${cfg.package.untarDir}";
+            ExecStart = "${cfg.package}/lib/${cfg.package.untarDir}/sbin/start-worker.sh spark://${cfg.worker.master}";
+            ExecStop  = "${cfg.package}/lib/${cfg.package.untarDir}/sbin/stop-worker.sh";
+            TimeoutSec = 300;
+            StartLimitBurst=10;
+            Restart = "always";
+          };
+        };
+      };
+      tmpfiles.rules = [
+        "d '${cfg.worker.workDir}' - spark spark - -"
+        "d '${cfg.logDir}' - spark spark - -"
+      ];
+    };
+    users = {
+      users.spark = {
+        description = "spark user.";
+        group = "spark";
+        isSystemUser = true;
+      };
+      groups.spark = { };
+    };
+  };
+}
diff --git a/nixos/modules/services/computing/boinc/client.nix b/nixos/modules/services/computing/boinc/client.nix
new file mode 100644
index 00000000000..52249455fd4
--- /dev/null
+++ b/nixos/modules/services/computing/boinc/client.nix
@@ -0,0 +1,131 @@
+{config, lib, pkgs, ...}:
+
+with lib;
+
+let
+  cfg = config.services.boinc;
+  allowRemoteGuiRpcFlag = optionalString cfg.allowRemoteGuiRpc "--allow_remote_gui_rpc";
+
+  fhsEnv = pkgs.buildFHSUserEnv {
+    name = "boinc-fhs-env";
+    targetPkgs = pkgs': [ cfg.package ] ++ cfg.extraEnvPackages;
+    runScript = "/bin/boinc_client";
+  };
+  fhsEnvExecutable = "${fhsEnv}/bin/${fhsEnv.name}";
+
+in
+  {
+    options.services.boinc = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the BOINC distributed computing client. If this
+          option is set to true, the boinc_client daemon will be run as a
+          background service. The boinccmd command can be used to control the
+          daemon.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.boinc;
+        defaultText = literalExpression "pkgs.boinc";
+        description = ''
+          Which BOINC package to use.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/boinc";
+        description = ''
+          The directory in which to store BOINC's configuration and data files.
+        '';
+      };
+
+      allowRemoteGuiRpc = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If set to true, any remote host can connect to and control this BOINC
+          client (subject to password authentication). If instead set to false,
+          only the hosts listed in <varname>dataDir</varname>/remote_hosts.cfg will be allowed to
+          connect.
+
+          See also: <link xlink:href="http://boinc.berkeley.edu/wiki/Controlling_BOINC_remotely#Remote_access"/>
+        '';
+      };
+
+      extraEnvPackages = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "[ pkgs.virtualbox ]";
+        description = ''
+          Additional packages to make available in the environment in which
+          BOINC will run. Common choices are:
+          <variablelist>
+            <varlistentry>
+              <term><varname>pkgs.virtualbox</varname></term>
+              <listitem><para>
+                The VirtualBox virtual machine framework. Required by some BOINC
+                projects, such as ATLAS@home.
+              </para></listitem>
+            </varlistentry>
+            <varlistentry>
+              <term><varname>pkgs.ocl-icd</varname></term>
+              <listitem><para>
+                OpenCL infrastructure library. Required by BOINC projects that
+                use OpenCL, in addition to a device-specific OpenCL driver.
+              </para></listitem>
+            </varlistentry>
+            <varlistentry>
+              <term><varname>pkgs.linuxPackages.nvidia_x11</varname></term>
+              <listitem><para>
+                Provides CUDA libraries. Required by BOINC projects that use
+                CUDA. Note that this requires an NVIDIA graphics device to be
+                present on the system.
+              </para><para>
+                Also provides OpenCL drivers for NVIDIA GPUs;
+                <varname>pkgs.ocl-icd</varname> is also needed in this case.
+              </para></listitem>
+            </varlistentry>
+          </variablelist>
+        '';
+      };
+    };
+
+    config = mkIf cfg.enable {
+      environment.systemPackages = [cfg.package];
+
+      users.users.boinc = {
+        group = "boinc";
+        createHome = false;
+        description = "BOINC Client";
+        home = cfg.dataDir;
+        isSystemUser = true;
+      };
+      users.groups.boinc = {};
+
+      systemd.tmpfiles.rules = [
+        "d '${cfg.dataDir}' - boinc boinc - -"
+      ];
+
+      systemd.services.boinc = {
+        description = "BOINC Client";
+        after = ["network.target"];
+        wantedBy = ["multi-user.target"];
+        script = ''
+          ${fhsEnvExecutable} --dir ${cfg.dataDir} ${allowRemoteGuiRpcFlag}
+        '';
+        serviceConfig = {
+          User = "boinc";
+          Nice = 10;
+        };
+      };
+    };
+
+    meta = {
+      maintainers = with lib.maintainers; [kierdavis];
+    };
+  }
diff --git a/nixos/modules/services/computing/foldingathome/client.nix b/nixos/modules/services/computing/foldingathome/client.nix
new file mode 100644
index 00000000000..aa9d0a5218f
--- /dev/null
+++ b/nixos/modules/services/computing/foldingathome/client.nix
@@ -0,0 +1,91 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.foldingathome;
+
+  args =
+    ["--team" "${toString cfg.team}"]
+    ++ lib.optionals (cfg.user != null) ["--user" cfg.user]
+    ++ cfg.extraArgs
+    ;
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "foldingAtHome" ] [ "services" "foldingathome" ])
+    (mkRenamedOptionModule [ "services" "foldingathome" "nickname" ] [ "services" "foldingathome" "user" ])
+    (mkRemovedOptionModule [ "services" "foldingathome" "config" ] ''
+      Use <literal>services.foldingathome.extraArgs instead<literal>
+    '')
+  ];
+  options.services.foldingathome = {
+    enable = mkEnableOption "Enable the Folding@home client";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.fahclient;
+      defaultText = literalExpression "pkgs.fahclient";
+      description = ''
+        Which Folding@home client to use.
+      '';
+    };
+
+    user = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The user associated with the reported computation results. This will
+        be used in the ranking statistics.
+      '';
+    };
+
+    team = mkOption {
+      type = types.int;
+      default = 236565;
+      description = ''
+        The team ID associated with the reported computation results. This
+        will be used in the ranking statistics.
+
+        By default, use the NixOS folding@home team ID is being used.
+      '';
+    };
+
+    daemonNiceLevel = mkOption {
+      type = types.ints.between (-20) 19;
+      default = 0;
+      description = ''
+        Daemon process priority for FAHClient.
+        0 is the default Unix process priority, 19 is the lowest.
+      '';
+    };
+
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Extra startup options for the FAHClient. Run
+        <literal>FAHClient --help</literal> to find all the available options.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.foldingathome = {
+      description = "Folding@home client";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      script = ''
+        exec ${cfg.package}/bin/FAHClient ${lib.escapeShellArgs args}
+      '';
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "foldingathome";
+        Nice = cfg.daemonNiceLevel;
+        WorkingDirectory = "%S/foldingathome";
+      };
+    };
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ zimbatm ];
+  };
+}
diff --git a/nixos/modules/services/computing/slurm/slurm.nix b/nixos/modules/services/computing/slurm/slurm.nix
new file mode 100644
index 00000000000..8cbe54c6060
--- /dev/null
+++ b/nixos/modules/services/computing/slurm/slurm.nix
@@ -0,0 +1,437 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.slurm;
+  opt = options.services.slurm;
+  # configuration file can be generated by http://slurm.schedmd.com/configurator.html
+
+  defaultUser = "slurm";
+
+  configFile = pkgs.writeTextDir "slurm.conf"
+    ''
+      ClusterName=${cfg.clusterName}
+      StateSaveLocation=${cfg.stateSaveLocation}
+      SlurmUser=${cfg.user}
+      ${optionalString (cfg.controlMachine != null) "controlMachine=${cfg.controlMachine}"}
+      ${optionalString (cfg.controlAddr != null) "controlAddr=${cfg.controlAddr}"}
+      ${toString (map (x: "NodeName=${x}\n") cfg.nodeName)}
+      ${toString (map (x: "PartitionName=${x}\n") cfg.partitionName)}
+      PlugStackConfig=${plugStackConfig}/plugstack.conf
+      ProctrackType=${cfg.procTrackType}
+      ${cfg.extraConfig}
+    '';
+
+  plugStackConfig = pkgs.writeTextDir "plugstack.conf"
+    ''
+      ${optionalString cfg.enableSrunX11 "optional ${pkgs.slurm-spank-x11}/lib/x11.so"}
+      ${cfg.extraPlugstackConfig}
+    '';
+
+  cgroupConfig = pkgs.writeTextDir "cgroup.conf"
+   ''
+     ${cfg.extraCgroupConfig}
+   '';
+
+  slurmdbdConf = pkgs.writeText "slurmdbd.conf"
+   ''
+     DbdHost=${cfg.dbdserver.dbdHost}
+     SlurmUser=${cfg.user}
+     StorageType=accounting_storage/mysql
+     StorageUser=${cfg.dbdserver.storageUser}
+     ${cfg.dbdserver.extraConfig}
+   '';
+
+  # slurm expects some additional config files to be
+  # in the same directory as slurm.conf
+  etcSlurm = pkgs.symlinkJoin {
+    name = "etc-slurm";
+    paths = [ configFile cgroupConfig plugStackConfig ] ++ cfg.extraConfigPaths;
+  };
+in
+
+{
+
+  ###### interface
+
+  meta.maintainers = [ maintainers.markuskowa ];
+
+  options = {
+
+    services.slurm = {
+
+      server = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether to enable the slurm control daemon.
+            Note that the standard authentication method is "munge".
+            The "munge" service needs to be provided with a password file in order for
+            slurm to work properly (see <literal>services.munge.password</literal>).
+          '';
+        };
+      };
+
+      dbdserver = {
+        enable = mkEnableOption "SlurmDBD service";
+
+        dbdHost = mkOption {
+          type = types.str;
+          default = config.networking.hostName;
+          defaultText = literalExpression "config.networking.hostName";
+          description = ''
+            Hostname of the machine where <literal>slurmdbd</literal>
+            is running (i.e. name returned by <literal>hostname -s</literal>).
+          '';
+        };
+
+        storageUser = mkOption {
+          type = types.str;
+          default = cfg.user;
+          defaultText = literalExpression "config.${opt.user}";
+          description = ''
+            Database user name.
+          '';
+        };
+
+        storagePassFile = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = ''
+            Path to file with database password. The content of this will be used to
+            create the password for the <literal>StoragePass</literal> option.
+          '';
+        };
+
+        extraConfig = mkOption {
+          type = types.lines;
+          default = "";
+          description = ''
+            Extra configuration for <literal>slurmdbd.conf</literal> See also:
+            <citerefentry><refentrytitle>slurmdbd.conf</refentrytitle>
+            <manvolnum>8</manvolnum></citerefentry>.
+          '';
+        };
+      };
+
+      client = {
+        enable = mkEnableOption "slurm client daemon";
+      };
+
+      enableStools = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to provide a slurm.conf file.
+          Enable this option if you do not run a slurm daemon on this host
+          (i.e. <literal>server.enable</literal> and <literal>client.enable</literal> are <literal>false</literal>)
+          but you still want to run slurm commands from this host.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.slurm.override { enableX11 = ! cfg.enableSrunX11; };
+        defaultText = literalExpression "pkgs.slurm";
+        example = literalExpression "pkgs.slurm-full";
+        description = ''
+          The package to use for slurm binaries.
+        '';
+      };
+
+      controlMachine = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = null;
+        description = ''
+          The short hostname of the machine where SLURM control functions are
+          executed (i.e. the name returned by the command "hostname -s", use "tux001"
+          rather than "tux001.my.com").
+        '';
+      };
+
+      controlAddr = mkOption {
+        type = types.nullOr types.str;
+        default = cfg.controlMachine;
+        defaultText = literalExpression "config.${opt.controlMachine}";
+        example = null;
+        description = ''
+          Name that ControlMachine should be referred to in establishing a
+          communications path.
+        '';
+      };
+
+      clusterName = mkOption {
+        type = types.str;
+        default = "default";
+        example = "myCluster";
+        description = ''
+          Necessary to distinguish accounting records in a multi-cluster environment.
+        '';
+      };
+
+      nodeName = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = literalExpression ''[ "linux[1-32] CPUs=1 State=UNKNOWN" ];'';
+        description = ''
+          Name that SLURM uses to refer to a node (or base partition for BlueGene
+          systems). Typically this would be the string that "/bin/hostname -s"
+          returns. Note that now you have to write node's parameters after the name.
+        '';
+      };
+
+      partitionName = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = literalExpression ''[ "debug Nodes=linux[1-32] Default=YES MaxTime=INFINITE State=UP" ];'';
+        description = ''
+          Name by which the partition may be referenced. Note that now you have
+          to write the partition's parameters after the name.
+        '';
+      };
+
+      enableSrunX11 = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If enabled srun will accept the option "--x11" to allow for X11 forwarding
+          from within an interactive session or a batch job. This activates the
+          slurm-spank-x11 module. Note that this option also enables
+          <option>services.openssh.forwardX11</option> on the client.
+
+          This option requires slurm to be compiled without native X11 support.
+          The default behavior is to re-compile the slurm package with native X11
+          support disabled if this option is set to true.
+
+          To use the native X11 support add <literal>PrologFlags=X11</literal> in <option>extraConfig</option>.
+          Note that this method will only work RSA SSH host keys.
+        '';
+      };
+
+      procTrackType = mkOption {
+        type = types.str;
+        default = "proctrack/linuxproc";
+        description = ''
+          Plugin to be used for process tracking on a job step basis.
+          The slurmd daemon uses this mechanism to identify all processes
+          which are children of processes it spawns for a user job step.
+        '';
+      };
+
+      stateSaveLocation = mkOption {
+        type = types.str;
+        default = "/var/spool/slurmctld";
+        description = ''
+          Directory into which the Slurm controller, slurmctld, saves its state.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = defaultUser;
+        description = ''
+          Set this option when you want to run the slurmctld daemon
+          as something else than the default slurm user "slurm".
+          Note that the UID of this user needs to be the same
+          on all nodes.
+        '';
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra configuration options that will be added verbatim at
+          the end of the slurm configuration file.
+        '';
+      };
+
+      extraPlugstackConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra configuration that will be added to the end of <literal>plugstack.conf</literal>.
+        '';
+      };
+
+      extraCgroupConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra configuration for <literal>cgroup.conf</literal>. This file is
+          used when <literal>procTrackType=proctrack/cgroup</literal>.
+        '';
+      };
+
+      extraConfigPaths = mkOption {
+        type = with types; listOf path;
+        default = [];
+        description = ''
+          Slurm expects config files for plugins in the same path
+          as <literal>slurm.conf</literal>. Add extra nix store
+          paths that should be merged into same directory as
+          <literal>slurm.conf</literal>.
+        '';
+      };
+
+      etcSlurm = mkOption {
+        type = types.path;
+        internal = true;
+        default = etcSlurm;
+        defaultText = literalDocBook ''
+          Directory created from generated config files and
+          <literal>config.${opt.extraConfigPaths}</literal>.
+        '';
+        description = ''
+          Path to directory with slurm config files. This option is set by default from the
+          Slurm module and is meant to make the Slurm config file available to other modules.
+        '';
+      };
+
+    };
+
+  };
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "slurm" "dbdserver" "storagePass" ] ''
+      This option has been removed so that the database password is not exposed via the nix store.
+      Use services.slurm.dbdserver.storagePassFile to provide the database password.
+    '')
+    (mkRemovedOptionModule [ "services" "slurm" "dbdserver" "configFile" ] ''
+      This option has been removed. Use services.slurm.dbdserver.storagePassFile
+      and services.slurm.dbdserver.extraConfig instead.
+    '')
+  ];
+
+  ###### implementation
+
+  config =
+    let
+      wrappedSlurm = pkgs.stdenv.mkDerivation {
+        name = "wrappedSlurm";
+
+        builder = pkgs.writeText "builder.sh" ''
+          source $stdenv/setup
+          mkdir -p $out/bin
+          find  ${getBin cfg.package}/bin -type f -executable | while read EXE
+          do
+            exename="$(basename $EXE)"
+            wrappername="$out/bin/$exename"
+            cat > "$wrappername" <<EOT
+          #!/bin/sh
+          if [ -z "$SLURM_CONF" ]
+          then
+            SLURM_CONF="${cfg.etcSlurm}/slurm.conf" "$EXE" "\$@"
+          else
+            "$EXE" "\$0"
+          fi
+          EOT
+            chmod +x "$wrappername"
+          done
+
+          mkdir -p $out/share
+          ln -s ${getBin cfg.package}/share/man $out/share/man
+        '';
+      };
+
+  in mkIf ( cfg.enableStools ||
+            cfg.client.enable ||
+            cfg.server.enable ||
+            cfg.dbdserver.enable ) {
+
+    environment.systemPackages = [ wrappedSlurm ];
+
+    services.munge.enable = mkDefault true;
+
+    # use a static uid as default to ensure it is the same on all nodes
+    users.users.slurm = mkIf (cfg.user == defaultUser) {
+      name = defaultUser;
+      group = "slurm";
+      uid = config.ids.uids.slurm;
+    };
+
+    users.groups.slurm.gid = config.ids.uids.slurm;
+
+    systemd.services.slurmd = mkIf (cfg.client.enable) {
+      path = with pkgs; [ wrappedSlurm coreutils ]
+        ++ lib.optional cfg.enableSrunX11 slurm-spank-x11;
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "systemd-tmpfiles-clean.service" ];
+      requires = [ "network.target" ];
+
+      serviceConfig = {
+        Type = "forking";
+        KillMode = "process";
+        ExecStart = "${wrappedSlurm}/bin/slurmd";
+        PIDFile = "/run/slurmd.pid";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        LimitMEMLOCK = "infinity";
+      };
+    };
+
+    systemd.tmpfiles.rules = mkIf cfg.client.enable [
+      "d /var/spool/slurmd 755 root root -"
+    ];
+
+    services.openssh.forwardX11 = mkIf cfg.client.enable (mkDefault true);
+
+    systemd.services.slurmctld = mkIf (cfg.server.enable) {
+      path = with pkgs; [ wrappedSlurm munge coreutils ]
+        ++ lib.optional cfg.enableSrunX11 slurm-spank-x11;
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "munged.service" ];
+      requires = [ "munged.service" ];
+
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${wrappedSlurm}/bin/slurmctld";
+        PIDFile = "/run/slurmctld.pid";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+      };
+
+      preStart = ''
+        mkdir -p ${cfg.stateSaveLocation}
+        chown -R ${cfg.user}:slurm ${cfg.stateSaveLocation}
+      '';
+    };
+
+    systemd.services.slurmdbd = let
+      # slurm strips the last component off the path
+      configPath = "$RUNTIME_DIRECTORY/slurmdbd.conf";
+    in mkIf (cfg.dbdserver.enable) {
+      path = with pkgs; [ wrappedSlurm munge coreutils ];
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "munged.service" "mysql.service" ];
+      requires = [ "munged.service" "mysql.service" ];
+
+      preStart = ''
+        install -m 600 -o ${cfg.user} -T ${slurmdbdConf} ${configPath}
+        ${optionalString (cfg.dbdserver.storagePassFile != null) ''
+          echo "StoragePass=$(cat ${cfg.dbdserver.storagePassFile})" \
+            >> ${configPath}
+        ''}
+      '';
+
+      script = ''
+        export SLURM_CONF=${configPath}
+        exec ${cfg.package}/bin/slurmdbd -D
+      '';
+
+      serviceConfig = {
+        RuntimeDirectory = "slurmdbd";
+        Type = "simple";
+        PIDFile = "/run/slurmdbd.pid";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/computing/torque/mom.nix b/nixos/modules/services/computing/torque/mom.nix
new file mode 100644
index 00000000000..6747bd4b0d5
--- /dev/null
+++ b/nixos/modules/services/computing/torque/mom.nix
@@ -0,0 +1,63 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.torque.mom;
+  torque = pkgs.torque;
+
+  momConfig = pkgs.writeText "torque-mom-config" ''
+    $pbsserver ${cfg.serverNode}
+    $logevent 225
+  '';
+
+in
+{
+  options = {
+
+    services.torque.mom = {
+      enable = mkEnableOption "torque computing node";
+
+      serverNode = mkOption {
+        type = types.str;
+        description = "Hostname running pbs server.";
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.torque ];
+
+    systemd.services.torque-mom-init = {
+      path = with pkgs; [ torque util-linux procps inetutils ];
+
+      script = ''
+        pbs_mkdirs -v aux
+        pbs_mkdirs -v mom
+        hostname > /var/spool/torque/server_name
+        cp -v ${momConfig} /var/spool/torque/mom_priv/config
+      '';
+
+      serviceConfig.Type = "oneshot";
+      unitConfig.ConditionPathExists = "!/var/spool/torque";
+    };
+
+    systemd.services.torque-mom = {
+      path = [ torque ];
+
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "torque-mom-init.service" ];
+      after = [ "torque-mom-init.service" "network.target" ];
+
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${torque}/bin/pbs_mom";
+        PIDFile = "/var/spool/torque/mom_priv/mom.lock";
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/computing/torque/server.nix b/nixos/modules/services/computing/torque/server.nix
new file mode 100644
index 00000000000..8d923fc04d4
--- /dev/null
+++ b/nixos/modules/services/computing/torque/server.nix
@@ -0,0 +1,96 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.torque.server;
+  torque = pkgs.torque;
+in
+{
+  options = {
+
+    services.torque.server = {
+
+      enable = mkEnableOption "torque server";
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.torque ];
+
+    systemd.services.torque-server-init = {
+      path = with pkgs; [ torque util-linux procps inetutils ];
+
+      script = ''
+        tmpsetup=$(mktemp -t torque-XXXX)
+        cp -p ${torque}/bin/torque.setup $tmpsetup
+        sed -i $tmpsetup -e 's/pbs_server -t create/pbs_server -f -t create/'
+
+        pbs_mkdirs -v aux
+        pbs_mkdirs -v server
+        hostname > /var/spool/torque/server_name
+        cp -prv ${torque}/var/spool/torque/* /var/spool/torque/
+        $tmpsetup root
+
+        sleep 1
+        rm -f $tmpsetup
+        kill $(pgrep pbs_server) 2>/dev/null
+        kill $(pgrep trqauthd) 2>/dev/null
+      '';
+
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+      };
+
+      unitConfig = {
+        ConditionPathExists = "!/var/spool/torque";
+      };
+    };
+
+    systemd.services.trqauthd = {
+      path = [ torque ];
+
+      requires = [ "torque-server-init.service" ];
+      after = [ "torque-server-init.service" ];
+
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${torque}/bin/trqauthd";
+      };
+    };
+
+    systemd.services.torque-server = {
+      path = [ torque ];
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "torque-scheduler.service" "trqauthd.service" ];
+      before = [ "trqauthd.service" ];
+      requires = [ "torque-server-init.service" ];
+      after = [ "torque-server-init.service" "network.target" ];
+
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${torque}/bin/pbs_server";
+        ExecStop = "${torque}/bin/qterm";
+        PIDFile = "/var/spool/torque/server_priv/server.lock";
+      };
+    };
+
+    systemd.services.torque-scheduler = {
+      path = [ torque ];
+
+      requires = [ "torque-server-init.service" ];
+      after = [ "torque-server-init.service" ];
+
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${torque}/bin/pbs_sched";
+        PIDFile = "/var/spool/torque/sched_priv/sched.lock";
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/buildbot/master.nix b/nixos/modules/services/continuous-integration/buildbot/master.nix
new file mode 100644
index 00000000000..aaa159d3cb1
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/buildbot/master.nix
@@ -0,0 +1,290 @@
+# NixOS module for Buildbot continous integration server.
+
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.buildbot-master;
+  opt = options.services.buildbot-master;
+
+  python = cfg.package.pythonModule;
+
+  escapeStr = s: escape ["'"] s;
+
+  defaultMasterCfg = pkgs.writeText "master.cfg" ''
+    from buildbot.plugins import *
+    factory = util.BuildFactory()
+    c = BuildmasterConfig = dict(
+     workers       = [${concatStringsSep "," cfg.workers}],
+     protocols     = { 'pb': {'port': ${toString cfg.pbPort} } },
+     title         = '${escapeStr cfg.title}',
+     titleURL      = '${escapeStr cfg.titleUrl}',
+     buildbotURL   = '${escapeStr cfg.buildbotUrl}',
+     db            = dict(db_url='${escapeStr cfg.dbUrl}'),
+     www           = dict(port=${toString cfg.port}),
+     change_source = [ ${concatStringsSep "," cfg.changeSource} ],
+     schedulers    = [ ${concatStringsSep "," cfg.schedulers} ],
+     builders      = [ ${concatStringsSep "," cfg.builders} ],
+     services      = [ ${concatStringsSep "," cfg.reporters} ],
+    )
+    for step in [ ${concatStringsSep "," cfg.factorySteps} ]:
+      factory.addStep(step)
+
+    ${cfg.extraConfig}
+  '';
+
+  tacFile = pkgs.writeText "buildbot-master.tac" ''
+    import os
+
+    from twisted.application import service
+    from buildbot.master import BuildMaster
+
+    basedir = '${cfg.buildbotDir}'
+
+    configfile = '${cfg.masterCfg}'
+
+    # Default umask for server
+    umask = None
+
+    # note: this line is matched against to check that this is a buildmaster
+    # directory; do not edit it.
+    application = service.Application('buildmaster')
+
+    m = BuildMaster(basedir, configfile, umask)
+    m.setServiceParent(application)
+  '';
+
+in {
+  options = {
+    services.buildbot-master = {
+
+      factorySteps = mkOption {
+        type = types.listOf types.str;
+        description = "Factory Steps";
+        default = [];
+        example = [
+          "steps.Git(repourl='git://github.com/buildbot/pyflakes.git', mode='incremental')"
+          "steps.ShellCommand(command=['trial', 'pyflakes'])"
+        ];
+      };
+
+      changeSource = mkOption {
+        type = types.listOf types.str;
+        description = "List of Change Sources.";
+        default = [];
+        example = [
+          "changes.GitPoller('git://github.com/buildbot/pyflakes.git', workdir='gitpoller-workdir', branch='master', pollinterval=300)"
+        ];
+      };
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the Buildbot continuous integration server.";
+      };
+
+      extraConfig = mkOption {
+        type = types.str;
+        description = "Extra configuration to append to master.cfg";
+        default = "c['buildbotNetUsageData'] = None";
+      };
+
+      masterCfg = mkOption {
+        type = types.path;
+        description = "Optionally pass master.cfg path. Other options in this configuration will be ignored.";
+        default = defaultMasterCfg;
+        defaultText = literalDocBook ''generated configuration file'';
+        example = "/etc/nixos/buildbot/master.cfg";
+      };
+
+      schedulers = mkOption {
+        type = types.listOf types.str;
+        description = "List of Schedulers.";
+        default = [
+          "schedulers.SingleBranchScheduler(name='all', change_filter=util.ChangeFilter(branch='master'), treeStableTimer=None, builderNames=['runtests'])"
+          "schedulers.ForceScheduler(name='force',builderNames=['runtests'])"
+        ];
+      };
+
+      builders = mkOption {
+        type = types.listOf types.str;
+        description = "List of Builders.";
+        default = [
+          "util.BuilderConfig(name='runtests',workernames=['example-worker'],factory=factory)"
+        ];
+      };
+
+      workers = mkOption {
+        type = types.listOf types.str;
+        description = "List of Workers.";
+        default = [ "worker.Worker('example-worker', 'pass')" ];
+      };
+
+      reporters = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        description = "List of reporter objects used to present build status to various users.";
+      };
+
+      user = mkOption {
+        default = "buildbot";
+        type = types.str;
+        description = "User the buildbot server should execute under.";
+      };
+
+      group = mkOption {
+        default = "buildbot";
+        type = types.str;
+        description = "Primary group of buildbot user.";
+      };
+
+      extraGroups = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "List of extra groups that the buildbot user should be a part of.";
+      };
+
+      home = mkOption {
+        default = "/home/buildbot";
+        type = types.path;
+        description = "Buildbot home directory.";
+      };
+
+      buildbotDir = mkOption {
+        default = "${cfg.home}/master";
+        defaultText = literalExpression ''"''${config.${opt.home}}/master"'';
+        type = types.path;
+        description = "Specifies the Buildbot directory.";
+      };
+
+      pbPort = mkOption {
+        default = 9989;
+        type = types.either types.str types.int;
+        example = "'tcp:9990:interface=127.0.0.1'";
+        description = ''
+          The buildmaster will listen on a TCP port of your choosing
+          for connections from workers.
+          It can also use this port for connections from remote Change Sources,
+          status clients, and debug tools.
+          This port should be visible to the outside world, and you’ll need to tell
+          your worker admins about your choice.
+          If put in (single) quotes, this can also be used as a connection string,
+          as defined in the <link xlink:href="https://twistedmatrix.com/documents/current/core/howto/endpoints.html">ConnectionStrings guide</link>.
+        '';
+      };
+
+      listenAddress = mkOption {
+        default = "0.0.0.0";
+        type = types.str;
+        description = "Specifies the bind address on which the buildbot HTTP interface listens.";
+      };
+
+      buildbotUrl = mkOption {
+        default = "http://localhost:8010/";
+        type = types.str;
+        description = "Specifies the Buildbot URL.";
+      };
+
+      title = mkOption {
+        default = "Buildbot";
+        type = types.str;
+        description = "Specifies the Buildbot Title.";
+      };
+
+      titleUrl = mkOption {
+        default = "Buildbot";
+        type = types.str;
+        description = "Specifies the Buildbot TitleURL.";
+      };
+
+      dbUrl = mkOption {
+        default = "sqlite:///state.sqlite";
+        type = types.str;
+        description = "Specifies the database connection string.";
+      };
+
+      port = mkOption {
+        default = 8010;
+        type = types.int;
+        description = "Specifies port number on which the buildbot HTTP interface listens.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.python3Packages.buildbot-full;
+        defaultText = literalExpression "pkgs.python3Packages.buildbot-full";
+        description = "Package to use for buildbot.";
+        example = literalExpression "pkgs.python3Packages.buildbot";
+      };
+
+      packages = mkOption {
+        default = [ pkgs.git ];
+        defaultText = literalExpression "[ pkgs.git ]";
+        type = types.listOf types.package;
+        description = "Packages to add to PATH for the buildbot process.";
+      };
+
+      pythonPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
+        default = pythonPackages: with pythonPackages; [ ];
+        defaultText = literalExpression "pythonPackages: with pythonPackages; [ ]";
+        description = "Packages to add the to the PYTHONPATH of the buildbot process.";
+        example = literalExpression "pythonPackages: with pythonPackages; [ requests ]";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups = optionalAttrs (cfg.group == "buildbot") {
+      buildbot = { };
+    };
+
+    users.users = optionalAttrs (cfg.user == "buildbot") {
+      buildbot = {
+        description = "Buildbot User.";
+        isNormalUser = true;
+        createHome = true;
+        home = cfg.home;
+        group = cfg.group;
+        extraGroups = cfg.extraGroups;
+        useDefaultShell = true;
+      };
+    };
+
+    systemd.services.buildbot-master = {
+      description = "Buildbot Continuous Integration Server.";
+      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}";
+
+      preStart = ''
+        mkdir -vp "${cfg.buildbotDir}"
+        # Link the tac file so buildbot command line tools recognize the directory
+        ln -sf "${tacFile}" "${cfg.buildbotDir}/buildbot.tac"
+        ${cfg.package}/bin/buildbot create-master --db "${cfg.dbUrl}" "${cfg.buildbotDir}"
+        rm -f buildbot.tac.new master.cfg.sample
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        WorkingDirectory = cfg.home;
+        # NOTE: call twistd directly with stdout logging for systemd
+        ExecStart = "${python.pkgs.twisted}/bin/twistd -o --nodaemon --pidfile= --logfile - --python ${tacFile}";
+      };
+    };
+  };
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "buildbot-master" "bpPort" ] [ "services" "buildbot-master" "pbPort" ])
+    (mkRemovedOptionModule [ "services" "buildbot-master" "status" ] ''
+      Since Buildbot 0.9.0, status targets are deprecated and ignored.
+      Review your configuration and migrate to reporters (available at services.buildbot-master.reporters).
+    '')
+  ];
+
+  meta.maintainers = with lib.maintainers; [ mic92 lopsided98 ];
+}
diff --git a/nixos/modules/services/continuous-integration/buildbot/worker.nix b/nixos/modules/services/continuous-integration/buildbot/worker.nix
new file mode 100644
index 00000000000..1d7f53bb655
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/buildbot/worker.nix
@@ -0,0 +1,198 @@
+# NixOS module for Buildbot Worker.
+
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.buildbot-worker;
+  opt = options.services.buildbot-worker;
+
+  python = cfg.package.pythonModule;
+
+  tacFile = pkgs.writeText "aur-buildbot-worker.tac" ''
+    import os
+    from io import open
+
+    from buildbot_worker.bot import Worker
+    from twisted.application import service
+
+    basedir = '${cfg.buildbotDir}'
+
+    # note: this line is matched against to check that this is a worker
+    # directory; do not edit it.
+    application = service.Application('buildbot-worker')
+
+    master_url_split = '${cfg.masterUrl}'.split(':')
+    buildmaster_host = master_url_split[0]
+    port = int(master_url_split[1])
+    workername = '${cfg.workerUser}'
+
+    with open('${cfg.workerPassFile}', 'r', encoding='utf-8') as passwd_file:
+        passwd = passwd_file.read().strip('\r\n')
+    keepalive = ${toString cfg.keepalive}
+    umask = None
+    maxdelay = 300
+    numcpus = None
+    allow_shutdown = None
+
+    s = Worker(buildmaster_host, port, workername, passwd, basedir,
+               keepalive, umask=umask, maxdelay=maxdelay,
+               numcpus=numcpus, allow_shutdown=allow_shutdown)
+    s.setServiceParent(application)
+  '';
+
+in {
+  options = {
+    services.buildbot-worker = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the Buildbot Worker.";
+      };
+
+      user = mkOption {
+        default = "bbworker";
+        type = types.str;
+        description = "User the buildbot Worker should execute under.";
+      };
+
+      group = mkOption {
+        default = "bbworker";
+        type = types.str;
+        description = "Primary group of buildbot Worker user.";
+      };
+
+      extraGroups = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "List of extra groups that the Buildbot Worker user should be a part of.";
+      };
+
+      home = mkOption {
+        default = "/home/bbworker";
+        type = types.path;
+        description = "Buildbot home directory.";
+      };
+
+      buildbotDir = mkOption {
+        default = "${cfg.home}/worker";
+        defaultText = literalExpression ''"''${config.${opt.home}}/worker"'';
+        type = types.path;
+        description = "Specifies the Buildbot directory.";
+      };
+
+      workerUser = mkOption {
+        default = "example-worker";
+        type = types.str;
+        description = "Specifies the Buildbot Worker user.";
+      };
+
+      workerPass = mkOption {
+        default = "pass";
+        type = types.str;
+        description = "Specifies the Buildbot Worker password.";
+      };
+
+      workerPassFile = mkOption {
+        type = types.path;
+        description = "File used to store the Buildbot Worker password";
+      };
+
+      hostMessage = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description = "Description of this worker";
+      };
+
+      adminMessage = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description = "Name of the administrator of this worker";
+      };
+
+      masterUrl = mkOption {
+        default = "localhost:9989";
+        type = types.str;
+        description = "Specifies the Buildbot Worker connection string.";
+      };
+
+      keepalive = mkOption {
+        default = 600;
+        type = types.int;
+        description = "
+          This is a number that indicates how frequently keepalive messages should be sent
+          from the worker to the buildmaster, expressed in seconds.
+        ";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.python3Packages.buildbot-worker;
+        defaultText = literalExpression "pkgs.python3Packages.buildbot-worker";
+        description = "Package to use for buildbot worker.";
+        example = literalExpression "pkgs.python2Packages.buildbot-worker";
+      };
+
+      packages = mkOption {
+        default = with pkgs; [ git ];
+        defaultText = literalExpression "[ pkgs.git ]";
+        type = types.listOf types.package;
+        description = "Packages to add to PATH for the buildbot process.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.buildbot-worker.workerPassFile = mkDefault (pkgs.writeText "buildbot-worker-password" cfg.workerPass);
+
+    users.groups = optionalAttrs (cfg.group == "bbworker") {
+      bbworker = { };
+    };
+
+    users.users = optionalAttrs (cfg.user == "bbworker") {
+      bbworker = {
+        description = "Buildbot Worker User.";
+        isNormalUser = true;
+        createHome = true;
+        home = cfg.home;
+        group = cfg.group;
+        extraGroups = cfg.extraGroups;
+        useDefaultShell = true;
+      };
+    };
+
+    systemd.services.buildbot-worker = {
+      description = "Buildbot Worker.";
+      after = [ "network.target" "buildbot-master.service" ];
+      wantedBy = [ "multi-user.target" ];
+      path = cfg.packages;
+      environment.PYTHONPATH = "${python.withPackages (p: [ cfg.package ])}/${python.sitePackages}";
+
+      preStart = ''
+        mkdir -vp "${cfg.buildbotDir}/info"
+        ${optionalString (cfg.hostMessage != null) ''
+          ln -sf "${pkgs.writeText "buildbot-worker-host" cfg.hostMessage}" "${cfg.buildbotDir}/info/host"
+        ''}
+        ${optionalString (cfg.adminMessage != null) ''
+          ln -sf "${pkgs.writeText "buildbot-worker-admin" cfg.adminMessage}" "${cfg.buildbotDir}/info/admin"
+        ''}
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        WorkingDirectory = cfg.home;
+
+        # NOTE: call twistd directly with stdout logging for systemd
+        ExecStart = "${python.pkgs.twisted}/bin/twistd --nodaemon --pidfile= --logfile - --python ${tacFile}";
+      };
+
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ ];
+
+}
diff --git a/nixos/modules/services/continuous-integration/buildkite-agents.nix b/nixos/modules/services/continuous-integration/buildkite-agents.nix
new file mode 100644
index 00000000000..1872567c9f1
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/buildkite-agents.nix
@@ -0,0 +1,280 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.buildkite-agents;
+
+  mkHookOption = { name, description, example ? null }: {
+    inherit name;
+    value = mkOption {
+      default = null;
+      inherit description;
+      type = types.nullOr types.lines;
+    } // (if example == null then {} else { inherit example; });
+  };
+  mkHookOptions = hooks: listToAttrs (map mkHookOption hooks);
+
+  hooksDir = cfg: let
+    mkHookEntry = name: value: ''
+      cat > $out/${name} <<'EOF'
+      #! ${pkgs.runtimeShell}
+      set -e
+      ${value}
+      EOF
+      chmod 755 $out/${name}
+    '';
+  in pkgs.runCommand "buildkite-agent-hooks" { preferLocalBuild = true; } ''
+    mkdir $out
+    ${concatStringsSep "\n" (mapAttrsToList mkHookEntry (filterAttrs (n: v: v != null) cfg.hooks))}
+  '';
+
+  buildkiteOptions = { name ? "", config, ... }: {
+    options = {
+      enable = mkOption {
+        default = true;
+        type = types.bool;
+        description = "Whether to enable this buildkite agent";
+      };
+
+      package = mkOption {
+        default = pkgs.buildkite-agent;
+        defaultText = literalExpression "pkgs.buildkite-agent";
+        description = "Which buildkite-agent derivation to use";
+        type = types.package;
+      };
+
+      dataDir = mkOption {
+        default = "/var/lib/buildkite-agent-${name}";
+        description = "The workdir for the agent";
+        type = types.str;
+      };
+
+      runtimePackages = mkOption {
+        default = [ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ];
+        defaultText = literalExpression "[ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ]";
+        description = "Add programs to the buildkite-agent environment";
+        type = types.listOf types.package;
+      };
+
+      tokenPath = mkOption {
+        type = types.path;
+        description = ''
+          The token from your Buildkite "Agents" page.
+
+          A run-time path to the token file, which is supposed to be provisioned
+          outside of Nix store.
+        '';
+      };
+
+      name = mkOption {
+        type = types.str;
+        default = "%hostname-${name}-%n";
+        description = ''
+          The name of the agent as seen in the buildkite dashboard.
+        '';
+      };
+
+      tags = mkOption {
+        type = types.attrsOf (types.either types.str (types.listOf types.str));
+        default = {};
+        example = { queue = "default"; docker = "true"; ruby2 ="true"; };
+        description = ''
+          Tags for the agent.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = "debug=true";
+        description = ''
+          Extra lines to be added verbatim to the configuration file.
+        '';
+      };
+
+      privateSshKeyPath = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        ## maximum care is taken so that secrets (ssh keys and the CI token)
+        ## don't end up in the Nix store.
+        apply = final: if final == null then null else toString final;
+
+        description = ''
+          OpenSSH private key
+
+          A run-time path to the key file, which is supposed to be provisioned
+          outside of Nix store.
+        '';
+      };
+
+      hooks = mkHookOptions [
+        { name = "checkout";
+          description = ''
+            The `checkout` hook script will replace the default checkout routine of the
+            bootstrap.sh script. You can use this hook to do your own SCM checkout
+            behaviour
+          ''; }
+        { name = "command";
+          description = ''
+            The `command` hook script will replace the default implementation of running
+            the build command.
+          ''; }
+        { name = "environment";
+          description = ''
+            The `environment` hook will run before all other commands, and can be used
+            to set up secrets, data, etc. Anything exported in hooks will be available
+            to the build script.
+
+            Note: the contents of this file will be copied to the world-readable
+            Nix store.
+          '';
+          example = ''
+            export SECRET_VAR=`head -1 /run/keys/secret`
+          ''; }
+        { name = "post-artifact";
+          description = ''
+            The `post-artifact` hook will run just after artifacts are uploaded
+          ''; }
+        { name = "post-checkout";
+          description = ''
+            The `post-checkout` hook will run after the bootstrap script has checked out
+            your projects source code.
+          ''; }
+        { name = "post-command";
+          description = ''
+            The `post-command` hook will run after the bootstrap script has run your
+            build commands
+          ''; }
+        { name = "pre-artifact";
+          description = ''
+            The `pre-artifact` hook will run just before artifacts are uploaded
+          ''; }
+        { name = "pre-checkout";
+          description = ''
+            The `pre-checkout` hook will run just before your projects source code is
+            checked out from your SCM provider
+          ''; }
+        { name = "pre-command";
+          description = ''
+            The `pre-command` hook will run just before your build command runs
+          ''; }
+        { name = "pre-exit";
+          description = ''
+            The `pre-exit` hook will run just before your build job finishes
+          ''; }
+      ];
+
+      hooksPath = mkOption {
+        type = types.path;
+        default = hooksDir config;
+        defaultText = literalDocBook "generated from <option>services.buildkite-agents.&lt;name&gt;.hooks</option>";
+        description = ''
+          Path to the directory storing the hooks.
+          Consider using <option>services.buildkite-agents.&lt;name&gt;.hooks.&lt;name&gt;</option>
+          instead.
+        '';
+      };
+
+      shell = mkOption {
+        type = types.str;
+        default = "${pkgs.bash}/bin/bash -e -c";
+        defaultText = literalExpression ''"''${pkgs.bash}/bin/bash -e -c"'';
+        description = ''
+          Command that buildkite-agent 3 will execute when it spawns a shell.
+        '';
+      };
+    };
+  };
+  enabledAgents = lib.filterAttrs (n: v: v.enable) cfg;
+  mapAgents = function: lib.mkMerge (lib.mapAttrsToList function enabledAgents);
+in
+{
+  options.services.buildkite-agents = mkOption {
+    type = types.attrsOf (types.submodule buildkiteOptions);
+    default = {};
+    description = ''
+      Attribute set of buildkite agents.
+      The attribute key is combined with the hostname and a unique integer to
+      create the final agent name. This can be overridden by setting the `name`
+      attribute.
+    '';
+  };
+
+  config.users.users = mapAgents (name: cfg: {
+    "buildkite-agent-${name}" = {
+      name = "buildkite-agent-${name}";
+      home = cfg.dataDir;
+      createHome = true;
+      description = "Buildkite agent user";
+      extraGroups = [ "keys" ];
+      isSystemUser = true;
+      group = "buildkite-agent-${name}";
+    };
+  });
+  config.users.groups = mapAgents (name: cfg: {
+    "buildkite-agent-${name}" = {};
+  });
+
+  config.systemd.services = mapAgents (name: cfg: {
+    "buildkite-agent-${name}" =
+      { description = "Buildkite Agent";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        path = cfg.runtimePackages ++ [ cfg.package pkgs.coreutils ];
+        environment = config.networking.proxy.envVars // {
+          HOME = cfg.dataDir;
+          NIX_REMOTE = "daemon";
+        };
+
+        ## NB: maximum care is taken so that secrets (ssh keys and the CI token)
+        ##     don't end up in the Nix store.
+        preStart = let
+          sshDir = "${cfg.dataDir}/.ssh";
+          tagStr = name: value:
+            if lib.isList value
+            then lib.concatStringsSep "," (builtins.map (v: "${name}=${v}") value)
+            else "${name}=${value}";
+          tagsStr = lib.concatStringsSep "," (lib.mapAttrsToList tagStr cfg.tags);
+        in
+          optionalString (cfg.privateSshKeyPath != null) ''
+            mkdir -m 0700 -p "${sshDir}"
+            install -m600 "${toString cfg.privateSshKeyPath}" "${sshDir}/id_rsa"
+          '' + ''
+            cat > "${cfg.dataDir}/buildkite-agent.cfg" <<EOF
+            token="$(cat ${toString cfg.tokenPath})"
+            name="${cfg.name}"
+            shell="${cfg.shell}"
+            tags="${tagsStr}"
+            build-path="${cfg.dataDir}/builds"
+            hooks-path="${cfg.hooksPath}"
+            ${cfg.extraConfig}
+            EOF
+          '';
+
+        serviceConfig =
+          { ExecStart = "${cfg.package}/bin/buildkite-agent start --config ${cfg.dataDir}/buildkite-agent.cfg";
+            User = "buildkite-agent-${name}";
+            RestartSec = 5;
+            Restart = "on-failure";
+            TimeoutSec = 10;
+            # set a long timeout to give buildkite-agent a chance to finish current builds
+            TimeoutStopSec = "2 min";
+            KillMode = "mixed";
+          };
+      };
+  });
+
+  config.assertions = mapAgents (name: cfg: [
+      { assertion = cfg.hooksPath == (hooksDir cfg) || all (v: v == null) (attrValues cfg.hooks);
+        message = ''
+          Options `services.buildkite-agents.${name}.hooksPath' and
+          `services.buildkite-agents.${name}.hooks.<name>' are mutually exclusive.
+        '';
+      }
+  ]);
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "buildkite-agent"] "services.buildkite-agent has been upgraded from version 2 to version 3 and moved to an attribute set at services.buildkite-agents. Please consult the 20.03 release notes for more information.")
+  ];
+}
diff --git a/nixos/modules/services/continuous-integration/github-runner.nix b/nixos/modules/services/continuous-integration/github-runner.nix
new file mode 100644
index 00000000000..a7645e1f56e
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/github-runner.nix
@@ -0,0 +1,310 @@
+{ config, pkgs, lib, ... }:
+with lib;
+let
+  cfg = config.services.github-runner;
+  svcName = "github-runner";
+  systemdDir = "${svcName}/${cfg.name}";
+  # %t: Runtime directory root (usually /run); see systemd.unit(5)
+  runtimeDir = "%t/${systemdDir}";
+  # %S: State directory root (usually /var/lib); see systemd.unit(5)
+  stateDir = "%S/${systemdDir}";
+  # %L: Log directory root (usually /var/log); see systemd.unit(5)
+  logsDir = "%L/${systemdDir}";
+  # Name of file stored in service state directory
+  currentConfigTokenFilename = ".current-token";
+in
+{
+  options.services.github-runner = {
+    enable = mkOption {
+      default = false;
+      example = true;
+      description = ''
+        Whether to enable GitHub Actions runner.
+
+        Note: GitHub recommends using self-hosted runners with private repositories only. Learn more here:
+        <link xlink:href="https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners"
+        >About self-hosted runners</link>.
+      '';
+      type = lib.types.bool;
+    };
+
+    url = mkOption {
+      type = types.str;
+      description = ''
+        Repository to add the runner to.
+
+        Changing this option triggers a new runner registration.
+
+        IMPORTANT: If your token is org-wide (not per repository), you need to
+        provide a github org link, not a single repository, so do it like this
+        <literal>https://github.com/nixos</literal>, not like this
+        <literal>https://github.com/nixos/nixpkgs</literal>.
+        Otherwise, you are going to get a <literal>404 NotFound</literal>
+        from <literal>POST https://api.github.com/actions/runner-registration</literal>
+        in the configure script.
+      '';
+      example = "https://github.com/nixos/nixpkgs";
+    };
+
+    tokenFile = mkOption {
+      type = types.path;
+      description = ''
+        The full path to a file which contains the runner registration token.
+        The file should contain exactly one line with the token without any newline.
+        The token can be used to re-register a runner of the same name but is time-limited.
+
+        Changing this option or the file's content triggers a new runner registration.
+      '';
+      example = "/run/secrets/github-runner/nixos.token";
+    };
+
+    name = mkOption {
+      # Same pattern as for `networking.hostName`
+      type = types.strMatching "^$|^[[:alnum:]]([[:alnum:]_-]{0,61}[[:alnum:]])?$";
+      description = ''
+        Name of the runner to configure. Defaults to the hostname.
+
+        Changing this option triggers a new runner registration.
+      '';
+      example = "nixos";
+      default = config.networking.hostName;
+      defaultText = literalExpression "config.networking.hostName";
+    };
+
+    runnerGroup = mkOption {
+      type = types.nullOr types.str;
+      description = ''
+        Name of the runner group to add this runner to (defaults to the default runner group).
+
+        Changing this option triggers a new runner registration.
+      '';
+      default = null;
+    };
+
+    extraLabels = mkOption {
+      type = types.listOf types.str;
+      description = ''
+        Extra labels in addition to the default (<literal>["self-hosted", "Linux", "X64"]</literal>).
+
+        Changing this option triggers a new runner registration.
+      '';
+      example = literalExpression ''[ "nixos" ]'';
+      default = [ ];
+    };
+
+    replace = mkOption {
+      type = types.bool;
+      description = ''
+        Replace any existing runner with the same name.
+
+        Without this flag, registering a new runner with the same name fails.
+      '';
+      default = false;
+    };
+
+    extraPackages = mkOption {
+      type = types.listOf types.package;
+      description = ''
+        Extra packages to add to <literal>PATH</literal> of the service to make them available to workflows.
+      '';
+      default = [ ];
+    };
+
+    package = mkOption {
+      type = types.package;
+      description = ''
+        Which github-runner derivation to use.
+      '';
+      default = pkgs.github-runner;
+      defaultText = literalExpression "pkgs.github-runner";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    warnings = optionals (isStorePath cfg.tokenFile) [
+      ''
+        `services.github-runner.tokenFile` points to the Nix store and, therefore, is world-readable.
+        Consider using a path outside of the Nix store to keep the token private.
+      ''
+    ];
+
+    systemd.services.${svcName} = {
+      description = "GitHub Actions runner";
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network.target" "network-online.target" ];
+
+      environment = {
+        HOME = runtimeDir;
+        RUNNER_ROOT = runtimeDir;
+      };
+
+      path = (with pkgs; [
+        bash
+        coreutils
+        git
+        gnutar
+        gzip
+      ]) ++ [
+        config.nix.package
+      ] ++ cfg.extraPackages;
+
+      serviceConfig = rec {
+        ExecStart = "${cfg.package}/bin/runsvc.sh";
+
+        # 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
+
+              STATE_DIRECTORY="$1"
+              RUNTIME_DIRECTORY="$2"
+              LOGS_DIRECTORY="$3"
+
+              ${lines}
+            '';
+            currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json";
+            runnerRegistrationConfig = getAttrs [ "name" "tokenFile" "url" "runnerGroup" "extraLabels" ] cfg;
+            newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig);
+            newConfigTokenFilename = ".new-token";
+            runnerCredFiles = [
+              ".credentials"
+              ".credentials_rsaparams"
+              ".runner"
+            ];
+            unconfigureRunner = writeScript "unconfigure" ''
+              differs=
+              # Set `differs = 1` if current and new runner config differ or if `currentConfigPath` does not exist
+              ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 || differs=1
+              # Also trigger a registration if the token content changed
+              ${pkgs.diffutils}/bin/diff -q \
+                "$STATE_DIRECTORY"/${currentConfigTokenFilename} \
+                ${escapeShellArg cfg.tokenFile} \
+                >/dev/null 2>&1 || differs=1
+
+              if [[ -n "$differs" ]]; 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."
+                find "$STATE_DIRECTORY/" -mindepth 1 -delete
+
+                # Copy the configured token file to the state dir and allow the service user to read the file
+                install --mode=666 ${escapeShellArg cfg.tokenFile} "$STATE_DIRECTORY/${newConfigTokenFilename}"
+                # Also copy current file to allow for a diff on the next start
+                install --mode=600 ${escapeShellArg cfg.tokenFile} "$STATE_DIRECTORY/${currentConfigTokenFilename}"
+              fi
+            '';
+            configureRunner = writeScript "configure" ''
+              if [[ -e "$STATE_DIRECTORY/${newConfigTokenFilename}" ]]; then
+                echo "Configuring GitHub Actions Runner"
+
+                token=$(< "$STATE_DIRECTORY"/${newConfigTokenFilename})
+                RUNNER_ROOT="$STATE_DIRECTORY" ${cfg.package}/bin/config.sh \
+                  --unattended \
+                  --disableupdate \
+                  --work "$RUNTIME_DIRECTORY" \
+                  --url ${escapeShellArg cfg.url} \
+                  --token "$token" \
+                  --labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)} \
+                  --name ${escapeShellArg cfg.name} \
+                  ${optionalString cfg.replace "--replace"} \
+                  ${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"}
+
+                # 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 "$STATE_DIRECTORY/${newConfigTokenFilename}"
+
+                # Symlink to new config
+                ln -s '${newConfigPath}' "${currentConfigPath}"
+              fi
+            '';
+            setupRuntimeDir = writeScript "setup-runtime-dirs" ''
+              # Link _diag dir
+              ln -s "$LOGS_DIRECTORY" "$RUNTIME_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 ]}") [
+            "+${unconfigureRunner}" # runs as root
+            configureRunner
+            setupRuntimeDir
+          ];
+
+        # 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
+          cfg.tokenFile
+          # Token file in the state directory
+          "${stateDir}/${currentConfigTokenFilename}"
+        ];
+
+        # By default, use a dynamically allocated user
+        DynamicUser = true;
+
+        KillMode = "process";
+        KillSignal = "SIGTERM";
+
+        # 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 = "";
+        LockPersonality = true;
+        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";
+
+        # Needs network access
+        PrivateNetwork = false;
+        # Cannot be true due to Node
+        MemoryDenyWriteExecute = false;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/gitlab-runner.nix b/nixos/modules/services/continuous-integration/gitlab-runner.nix
new file mode 100644
index 00000000000..dc58c634523
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/gitlab-runner.nix
@@ -0,0 +1,581 @@
+{ config, lib, pkgs, ... }:
+with builtins;
+with lib;
+let
+  cfg = config.services.gitlab-runner;
+  hasDocker = config.virtualisation.docker.enable;
+  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})
+      cp ${cfg.configFile} ${configPath}
+      # make config file readable by service
+      chown -R --reference=$HOME $(dirname ${configPath})
+    '' else ''
+      export CONFIG_FILE=${configPath}
+
+      mkdir -p $(dirname ${configPath})
+
+      # 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 }')
+
+      # 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)
+
+      # register new services
+      ${concatStringsSep "\n" (mapAttrsToList (name: service: ''
+        if echo "$NEW_SERVICES" | grep -xq ${name}; then
+          bash -c ${escapeShellArg (concatStringsSep " \\\n " ([
+            "set -a && source ${service.registrationConfigFile} &&"
+            "gitlab-runner register"
+            "--non-interactive"
+            "--name ${name}"
+            "--executor ${service.executor}"
+            "--limit ${toString service.limit}"
+            "--request-concurrency ${toString service.requestConcurrency}"
+            "--maximum-timeout ${toString service.maximumTimeout}"
+          ] ++ service.registrationFlags
+            ++ optional (service.buildsDir != null)
+            "--builds-dir ${service.buildsDir}"
+            ++ optional (service.cloneUrl != null)
+            "--clone-url ${service.cloneUrl}"
+            ++ optional (service.preCloneScript != null)
+            "--pre-clone-script ${service.preCloneScript}"
+            ++ optional (service.preBuildScript != null)
+            "--pre-build-script ${service.preBuildScript}"
+            ++ optional (service.postBuildScript != null)
+            "--post-build-script ${service.postBuildScript}"
+            ++ optional (service.tagList != [ ])
+            "--tag-list ${concatStringsSep "," service.tagList}"
+            ++ optional service.runUntagged
+            "--run-untagged"
+            ++ optional service.protected
+            "--access-level ref_protected"
+            ++ optional service.debugTraceDisabled
+            "--debug-trace-disabled"
+            ++ map (e: "--env ${escapeShellArg e}") (mapAttrsToList (name: value: "${name}=${value}") service.environmentVariables)
+            ++ optionals (hasPrefix "docker" service.executor) (
+              assert (
+                assertMsg (service.dockerImage != null)
+                  "dockerImage option is required for ${service.executor} executor (${name})");
+              [ "--docker-image ${service.dockerImage}" ]
+              ++ optional service.dockerDisableCache
+              "--docker-disable-cache"
+              ++ optional service.dockerPrivileged
+              "--docker-privileged"
+              ++ map (v: "--docker-volumes ${escapeShellArg v}") service.dockerVolumes
+              ++ map (v: "--docker-extra-hosts ${escapeShellArg v}") service.dockerExtraHosts
+              ++ map (v: "--docker-allowed-images ${escapeShellArg v}") service.dockerAllowedImages
+              ++ map (v: "--docker-allowed-services ${escapeShellArg v}") service.dockerAllowedServices
+            )
+          ))} && sleep 1 || exit 1
+        fi
+      '') hashedServices)}
+
+      # unregister old services
+      for NAME in $(echo "$OLD_SERVICES")
+      do
+        [ ! -z "$NAME" ] && gitlab-runner unregister \
+          --name "$NAME" && sleep 1
+      done
+
+      # update global options
+      remarshal --if toml --of json ${configPath} \
+        | jq -cM ${escapeShellArg (concatStringsSep " | " [
+            ".check_interval = ${toJSON cfg.checkInterval}"
+            ".concurrent = ${toJSON cfg.concurrent}"
+            ".sentry_dsn = ${toJSON cfg.sentryDSN}"
+            ".listen_address = ${toJSON cfg.prometheusListenAddress}"
+            ".session_server.listen_address = ${toJSON cfg.sessionServer.listenAddress}"
+            ".session_server.advertise_address = ${toJSON cfg.sessionServer.advertiseAddress}"
+            ".session_server.session_timeout = ${toJSON cfg.sessionServer.sessionTimeout}"
+            "del(.[] | nulls)"
+            "del(.session_server[] | nulls)"
+          ])} \
+        | remarshal --if json --of toml \
+        | sponge ${configPath}
+
+      # make config file readable by service
+      chown -R --reference=$HOME $(dirname ${configPath})
+    '');
+  startScript = pkgs.writeShellScriptBin "gitlab-runner-start" ''
+    export CONFIG_FILE=${configPath}
+    exec gitlab-runner run --working-directory $HOME
+  '';
+in
+{
+  options.services.gitlab-runner = {
+    enable = mkEnableOption "Gitlab Runner";
+    configFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Configuration file for gitlab-runner.
+
+        <option>configFile</option> takes precedence over <option>services</option>.
+        <option>checkInterval</option> and <option>concurrent</option> will be ignored too.
+
+        This option is deprecated, please use <option>services</option> instead.
+        You can use <option>registrationConfigFile</option> and
+        <option>registrationFlags</option>
+        for settings not covered by this module.
+      '';
+    };
+    checkInterval = mkOption {
+      type = types.int;
+      default = 0;
+      example = literalExpression "with lib; (length (attrNames config.services.gitlab-runner.services)) * 3";
+      description = ''
+        Defines the interval length, in seconds, between new jobs check.
+        The default value is 3;
+        if set to 0 or lower, the default value will be used.
+        See <link xlink:href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html#how-check_interval-works">runner documentation</link> for more information.
+      '';
+    };
+    concurrent = mkOption {
+      type = types.int;
+      default = 1;
+      example = literalExpression "config.nix.settings.max-jobs";
+      description = ''
+        Limits how many jobs globally can be run concurrently.
+        The most upper limit of jobs using all defined runners.
+        0 does not mean unlimited.
+      '';
+    };
+    sentryDSN = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "https://public:private@host:port/1";
+      description = ''
+        Data Source Name for tracking of all system level errors to Sentry.
+      '';
+    };
+    prometheusListenAddress = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "localhost:8080";
+      description = ''
+        Address (&lt;host&gt;:&lt;port&gt;) on which the Prometheus metrics HTTP server
+        should be listening.
+      '';
+    };
+    sessionServer = mkOption {
+      type = types.submodule {
+        options = {
+          listenAddress = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "0.0.0.0:8093";
+            description = ''
+              An internal URL to be used for the session server.
+            '';
+          };
+          advertiseAddress = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "runner-host-name.tld:8093";
+            description = ''
+              The URL that the Runner will expose to GitLab to be used
+              to access the session server.
+              Fallbacks to <option>listenAddress</option> if not defined.
+            '';
+          };
+          sessionTimeout = mkOption {
+            type = types.int;
+            default = 1800;
+            description = ''
+              How long in seconds the session can stay active after
+              the job completes (which will block the job from finishing).
+            '';
+          };
+        };
+      };
+      default = { };
+      example = literalExpression ''
+        {
+          listenAddress = "0.0.0.0:8093";
+        }
+      '';
+      description = ''
+        The session server allows the user to interact with jobs
+        that the Runner is responsible for. A good example of this is the
+        <link xlink:href="https://docs.gitlab.com/ee/ci/interactive_web_terminal/index.html">interactive web terminal</link>.
+      '';
+    };
+    gracefulTermination = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Finish all remaining jobs before stopping.
+        If not set gitlab-runner will stop immediatly without waiting
+        for jobs to finish, which will lead to failed builds.
+      '';
+    };
+    gracefulTimeout = mkOption {
+      type = types.str;
+      default = "infinity";
+      example = "5min 20s";
+      description = ''
+        Time to wait until a graceful shutdown is turned into a forceful one.
+      '';
+    };
+    package = mkOption {
+      type = types.package;
+      default = pkgs.gitlab-runner;
+      defaultText = literalExpression "pkgs.gitlab-runner";
+      example = literalExpression "pkgs.gitlab-runner_1_11";
+      description = "Gitlab Runner package to use.";
+    };
+    extraPackages = mkOption {
+      type = types.listOf types.package;
+      default = [ ];
+      description = ''
+        Extra packages to add to PATH for the gitlab-runner process.
+      '';
+    };
+    services = mkOption {
+      description = "GitLab Runner services.";
+      default = { };
+      example = literalExpression ''
+        {
+          # runner for building in docker via host's nix-daemon
+          # nix store will be readable in runner, might be insecure
+          nix = {
+            # File should contain at least these two variables:
+            # `CI_SERVER_URL`
+            # `REGISTRATION_TOKEN`
+            registrationConfigFile = "/run/secrets/gitlab-runner-registration";
+            dockerImage = "alpine";
+            dockerVolumes = [
+              "/nix/store:/nix/store:ro"
+              "/nix/var/nix/db:/nix/var/nix/db:ro"
+              "/nix/var/nix/daemon-socket:/nix/var/nix/daemon-socket:ro"
+            ];
+            dockerDisableCache = true;
+            preBuildScript = pkgs.writeScript "setup-container" '''
+              mkdir -p -m 0755 /nix/var/log/nix/drvs
+              mkdir -p -m 0755 /nix/var/nix/gcroots
+              mkdir -p -m 0755 /nix/var/nix/profiles
+              mkdir -p -m 0755 /nix/var/nix/temproots
+              mkdir -p -m 0755 /nix/var/nix/userpool
+              mkdir -p -m 1777 /nix/var/nix/gcroots/per-user
+              mkdir -p -m 1777 /nix/var/nix/profiles/per-user
+              mkdir -p -m 0755 /nix/var/nix/profiles/per-user/root
+              mkdir -p -m 0700 "$HOME/.nix-defexpr"
+
+              . ''${pkgs.nix}/etc/profile.d/nix.sh
+
+              ''${pkgs.nix}/bin/nix-env -i ''${concatStringsSep " " (with pkgs; [ nix cacert git openssh ])}
+
+              ''${pkgs.nix}/bin/nix-channel --add https://nixos.org/channels/nixpkgs-unstable
+              ''${pkgs.nix}/bin/nix-channel --update nixpkgs
+            ''';
+            environmentVariables = {
+              ENV = "/etc/profile";
+              USER = "root";
+              NIX_REMOTE = "daemon";
+              PATH = "/nix/var/nix/profiles/default/bin:/nix/var/nix/profiles/default/sbin:/bin:/sbin:/usr/bin:/usr/sbin";
+              NIX_SSL_CERT_FILE = "/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt";
+            };
+            tagList = [ "nix" ];
+          };
+          # runner for building docker images
+          docker-images = {
+            # File should contain at least these two variables:
+            # `CI_SERVER_URL`
+            # `REGISTRATION_TOKEN`
+            registrationConfigFile = "/run/secrets/gitlab-runner-registration";
+            dockerImage = "docker:stable";
+            dockerVolumes = [
+              "/var/run/docker.sock:/var/run/docker.sock"
+            ];
+            tagList = [ "docker-images" ];
+          };
+          # runner for executing stuff on host system (very insecure!)
+          # make sure to add required packages (including git!)
+          # to `environment.systemPackages`
+          shell = {
+            # File should contain at least these two variables:
+            # `CI_SERVER_URL`
+            # `REGISTRATION_TOKEN`
+            registrationConfigFile = "/run/secrets/gitlab-runner-registration";
+            executor = "shell";
+            tagList = [ "shell" ];
+          };
+          # runner for everything else
+          default = {
+            # File should contain at least these two variables:
+            # `CI_SERVER_URL`
+            # `REGISTRATION_TOKEN`
+            registrationConfigFile = "/run/secrets/gitlab-runner-registration";
+            dockerImage = "debian:stable";
+          };
+        }
+      '';
+      type = types.attrsOf (types.submodule {
+        options = {
+          registrationConfigFile = mkOption {
+            type = types.path;
+            description = ''
+              Absolute path to a file with environment variables
+              used for gitlab-runner registration.
+              A list of all supported environment variables can be found in
+              <literal>gitlab-runner register --help</literal>.
+
+              Ones that you probably want to set is
+
+              <literal>CI_SERVER_URL=&lt;CI server URL&gt;</literal>
+
+              <literal>REGISTRATION_TOKEN=&lt;registration secret&gt;</literal>
+
+              WARNING: make sure to use quoted absolute path,
+              or it is going to be copied to Nix Store.
+            '';
+          };
+          registrationFlags = mkOption {
+            type = types.listOf types.str;
+            default = [ ];
+            example = [ "--docker-helper-image my/gitlab-runner-helper" ];
+            description = ''
+              Extra command-line flags passed to
+              <literal>gitlab-runner register</literal>.
+              Execute <literal>gitlab-runner register --help</literal>
+              for a list of supported flags.
+            '';
+          };
+          environmentVariables = mkOption {
+            type = types.attrsOf types.str;
+            default = { };
+            example = { NAME = "value"; };
+            description = ''
+              Custom environment variables injected to build environment.
+              For secrets you can use <option>registrationConfigFile</option>
+              with <literal>RUNNER_ENV</literal> variable set.
+            '';
+          };
+          executor = mkOption {
+            type = types.str;
+            default = "docker";
+            description = ''
+              Select executor, eg. shell, docker, etc.
+              See <link xlink:href="https://docs.gitlab.com/runner/executors/README.html">runner documentation</link> for more information.
+            '';
+          };
+          buildsDir = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            example = "/var/lib/gitlab-runner/builds";
+            description = ''
+              Absolute path to a directory where builds will be stored
+              in context of selected executor (Locally, Docker, SSH).
+            '';
+          };
+          cloneUrl = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "http://gitlab.example.local";
+            description = ''
+              Overwrite the URL for the GitLab instance. Used if the Runner can’t connect to GitLab on the URL GitLab exposes itself.
+            '';
+          };
+          dockerImage = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            description = ''
+              Docker image to be used.
+            '';
+          };
+          dockerVolumes = mkOption {
+            type = types.listOf types.str;
+            default = [ ];
+            example = [ "/var/run/docker.sock:/var/run/docker.sock" ];
+            description = ''
+              Bind-mount a volume and create it
+              if it doesn't exist prior to mounting.
+            '';
+          };
+          dockerDisableCache = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Disable all container caching.
+            '';
+          };
+          dockerPrivileged = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Give extended privileges to container.
+            '';
+          };
+          dockerExtraHosts = mkOption {
+            type = types.listOf types.str;
+            default = [ ];
+            example = [ "other-host:127.0.0.1" ];
+            description = ''
+              Add a custom host-to-IP mapping.
+            '';
+          };
+          dockerAllowedImages = mkOption {
+            type = types.listOf types.str;
+            default = [ ];
+            example = [ "ruby:*" "python:*" "php:*" "my.registry.tld:5000/*:*" ];
+            description = ''
+              Whitelist allowed images.
+            '';
+          };
+          dockerAllowedServices = mkOption {
+            type = types.listOf types.str;
+            default = [ ];
+            example = [ "postgres:9" "redis:*" "mysql:*" ];
+            description = ''
+              Whitelist allowed services.
+            '';
+          };
+          preCloneScript = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            description = ''
+              Runner-specific command script executed before code is pulled.
+            '';
+          };
+          preBuildScript = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            description = ''
+              Runner-specific command script executed after code is pulled,
+              just before build executes.
+            '';
+          };
+          postBuildScript = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            description = ''
+              Runner-specific command script executed after code is pulled
+              and just after build executes.
+            '';
+          };
+          tagList = mkOption {
+            type = types.listOf types.str;
+            default = [ ];
+            description = ''
+              Tag list.
+            '';
+          };
+          runUntagged = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Register to run untagged builds; defaults to
+              <literal>true</literal> when <option>tagList</option> is empty.
+            '';
+          };
+          limit = mkOption {
+            type = types.int;
+            default = 0;
+            description = ''
+              Limit how many jobs can be handled concurrently by this service.
+              0 (default) simply means don't limit.
+            '';
+          };
+          requestConcurrency = mkOption {
+            type = types.int;
+            default = 0;
+            description = ''
+              Limit number of concurrent requests for new jobs from GitLab.
+            '';
+          };
+          maximumTimeout = mkOption {
+            type = types.int;
+            default = 0;
+            description = ''
+              What is the maximum timeout (in seconds) that will be set for
+              job when using this Runner. 0 (default) simply means don't limit.
+            '';
+          };
+          protected = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              When set to true Runner will only run on pipelines
+              triggered on protected branches.
+            '';
+          };
+          debugTraceDisabled = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              When set to true Runner will disable the possibility of
+              using the <literal>CI_DEBUG_TRACE</literal> feature.
+            '';
+          };
+        };
+      });
+    };
+  };
+  config = mkIf cfg.enable {
+    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))
+    ++ optional (cfg.configFile != null) "services.gitlab-runner.`configFile` is deprecated, please use services.gitlab-runner.`services`.";
+    environment.systemPackages = [ cfg.package ];
+    systemd.services.gitlab-runner = {
+      description = "Gitlab Runner";
+      documentation = [ "https://docs.gitlab.com/runner/" ];
+      after = [ "network.target" ]
+        ++ optional hasDocker "docker.service";
+      requires = optional hasDocker "docker.service";
+      wantedBy = [ "multi-user.target" ];
+      environment = config.networking.proxy.envVars // {
+        HOME = "/var/lib/gitlab-runner";
+      };
+      path = with pkgs; [
+        bash
+        gawk
+        jq
+        moreutils
+        remarshal
+        util-linux
+        cfg.package
+      ] ++ cfg.extraPackages;
+      reloadIfChanged = true;
+      serviceConfig = {
+        # Set `DynamicUser` under `systemd.services.gitlab-runner.serviceConfig`
+        # to `lib.mkForce false` in your configuration to run this service as root.
+        # You can also set `User` and `Group` options to run this service as desired user.
+        # Make sure to restart service or changes won't apply.
+        DynamicUser = true;
+        StateDirectory = "gitlab-runner";
+        SupplementaryGroups = optional hasDocker "docker";
+        ExecStartPre = "!${configureScript}/bin/gitlab-runner-configure";
+        ExecStart = "${startScript}/bin/gitlab-runner-start";
+        ExecReload = "!${configureScript}/bin/gitlab-runner-configure";
+      } // optionalAttrs (cfg.gracefulTermination) {
+        TimeoutStopSec = "${cfg.gracefulTimeout}";
+        KillSignal = "SIGQUIT";
+        KillMode = "process";
+      };
+    };
+    # Enable docker if `docker` executor is used in any service
+    virtualisation.docker.enable = mkIf (
+      any (s: s.executor == "docker") (attrValues cfg.services)
+    ) (mkDefault true);
+  };
+  imports = [
+    (mkRenamedOptionModule [ "services" "gitlab-runner" "packages" ] [ "services" "gitlab-runner" "extraPackages" ] )
+    (mkRemovedOptionModule [ "services" "gitlab-runner" "configOptions" ] "Use services.gitlab-runner.services option instead" )
+    (mkRemovedOptionModule [ "services" "gitlab-runner" "workDir" ] "You should move contents of workDir (if any) to /var/lib/gitlab-runner" )
+  ];
+}
diff --git a/nixos/modules/services/continuous-integration/gocd-agent/default.nix b/nixos/modules/services/continuous-integration/gocd-agent/default.nix
new file mode 100644
index 00000000000..c63998c6736
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/gocd-agent/default.nix
@@ -0,0 +1,218 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gocd-agent;
+  opt = options.services.gocd-agent;
+in {
+  options = {
+    services.gocd-agent = {
+      enable = mkEnableOption "gocd-agent";
+
+      user = mkOption {
+        default = "gocd-agent";
+        type = types.str;
+        description = ''
+          User the Go.CD agent should execute under.
+        '';
+      };
+
+      group = mkOption {
+        default = "gocd-agent";
+        type = types.str;
+        description = ''
+          If the default user "gocd-agent" is configured then this is the primary
+          group of that user.
+        '';
+      };
+
+      extraGroups = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "wheel" "docker" ];
+        description = ''
+          List of extra groups that the "gocd-agent" user should be a part of.
+        '';
+      };
+
+      packages = mkOption {
+        default = [ pkgs.stdenv pkgs.jre pkgs.git config.programs.ssh.package pkgs.nix ];
+        defaultText = literalExpression "[ pkgs.stdenv pkgs.jre pkgs.git config.programs.ssh.package pkgs.nix ]";
+        type = types.listOf types.package;
+        description = ''
+          Packages to add to PATH for the Go.CD agent process.
+        '';
+      };
+
+      agentConfig = mkOption {
+        default = "";
+        type = types.str;
+        example = ''
+          agent.auto.register.resources=ant,java
+          agent.auto.register.environments=QA,Performance
+          agent.auto.register.hostname=Agent01
+        '';
+        description = ''
+          Agent registration configuration.
+        '';
+      };
+
+      goServer = mkOption {
+        default = "https://127.0.0.1:8154/go";
+        type = types.str;
+        description = ''
+          URL of the GoCD Server to attach the Go.CD Agent to.
+        '';
+      };
+
+      workDir = mkOption {
+        default = "/var/lib/go-agent";
+        type = types.str;
+        description = ''
+          Specifies the working directory in which the Go.CD agent java archive resides.
+        '';
+      };
+
+      initialJavaHeapSize = mkOption {
+        default = "128m";
+        type = types.str;
+        description = ''
+          Specifies the initial java heap memory size for the Go.CD agent java process.
+        '';
+      };
+
+      maxJavaHeapMemory = mkOption {
+        default = "256m";
+        type = types.str;
+        description = ''
+          Specifies the java maximum heap memory size for the Go.CD agent java process.
+        '';
+      };
+
+      startupOptions = mkOption {
+        type = types.listOf types.str;
+        default = [
+          "-Xms${cfg.initialJavaHeapSize}"
+          "-Xmx${cfg.maxJavaHeapMemory}"
+          "-Djava.io.tmpdir=/tmp"
+          "-Dcruise.console.publish.interval=10"
+          "-Djava.security.egd=file:/dev/./urandom"
+        ];
+        defaultText = literalExpression ''
+          [
+            "-Xms''${config.${opt.initialJavaHeapSize}}"
+            "-Xmx''${config.${opt.maxJavaHeapMemory}}"
+            "-Djava.io.tmpdir=/tmp"
+            "-Dcruise.console.publish.interval=10"
+            "-Djava.security.egd=file:/dev/./urandom"
+          ]
+        '';
+        description = ''
+          Specifies startup command line arguments to pass to Go.CD agent
+          java process.
+        '';
+      };
+
+      extraOptions = mkOption {
+        default = [ ];
+        type = types.listOf types.str;
+        example = [
+          "-X debug"
+          "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5006"
+          "-verbose:gc"
+          "-Xloggc:go-agent-gc.log"
+          "-XX:+PrintGCTimeStamps"
+          "-XX:+PrintTenuringDistribution"
+          "-XX:+PrintGCDetails"
+          "-XX:+PrintGC"
+        ];
+        description = ''
+          Specifies additional command line arguments to pass to Go.CD agent
+          java process.  Example contains debug and gcLog arguments.
+        '';
+      };
+
+      environment = mkOption {
+        default = { };
+        type = with types; attrsOf str;
+        description = ''
+          Additional environment variables to be passed to the Go.CD agent process.
+          As a base environment, Go.CD agent receives NIX_PATH from
+          <option>environment.sessionVariables</option>, NIX_REMOTE is set to
+          "daemon".
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups = optionalAttrs (cfg.group == "gocd-agent") {
+      gocd-agent.gid = config.ids.gids.gocd-agent;
+    };
+
+    users.users = optionalAttrs (cfg.user == "gocd-agent") {
+      gocd-agent = {
+        description = "gocd-agent user";
+        createHome = true;
+        home = cfg.workDir;
+        group = cfg.group;
+        extraGroups = cfg.extraGroups;
+        useDefaultShell = true;
+        uid = config.ids.uids.gocd-agent;
+      };
+    };
+
+    systemd.services.gocd-agent = {
+      description = "GoCD Agent";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment =
+        let
+          selectedSessionVars =
+            lib.filterAttrs (n: v: builtins.elem n [ "NIX_PATH" ])
+              config.environment.sessionVariables;
+        in
+          selectedSessionVars //
+            {
+              NIX_REMOTE = "daemon";
+              AGENT_WORK_DIR = cfg.workDir;
+              AGENT_STARTUP_ARGS = ''${concatStringsSep " "  cfg.startupOptions}'';
+              LOG_DIR = cfg.workDir;
+              LOG_FILE = "${cfg.workDir}/go-agent-start.log";
+            } //
+            cfg.environment;
+
+      path = cfg.packages;
+
+      script = ''
+        MPATH="''${PATH}";
+        source /etc/profile
+        export PATH="''${MPATH}:''${PATH}";
+
+        if ! test -f ~/.nixpkgs/config.nix; then
+          mkdir -p ~/.nixpkgs/
+          echo "{ allowUnfree = true; }" > ~/.nixpkgs/config.nix
+        fi
+
+        mkdir -p config
+        rm -f config/autoregister.properties
+        ln -s "${pkgs.writeText "autoregister.properties" cfg.agentConfig}" config/autoregister.properties
+
+        ${pkgs.git}/bin/git config --global --add http.sslCAinfo /etc/ssl/certs/ca-certificates.crt
+        ${pkgs.jre}/bin/java ${concatStringsSep " " cfg.startupOptions} \
+                        ${concatStringsSep " " cfg.extraOptions} \
+                              -jar ${pkgs.gocd-agent}/go-agent/agent-bootstrapper.jar \
+                              -serverUrl ${cfg.goServer}
+      '';
+
+      serviceConfig = {
+        User = cfg.user;
+        WorkingDirectory = cfg.workDir;
+        RestartSec = 30;
+        Restart = "on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/gocd-server/default.nix b/nixos/modules/services/continuous-integration/gocd-server/default.nix
new file mode 100644
index 00000000000..3540656f934
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/gocd-server/default.nix
@@ -0,0 +1,212 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gocd-server;
+  opt = options.services.gocd-server;
+in {
+  options = {
+    services.gocd-server = {
+      enable = mkEnableOption "gocd-server";
+
+      user = mkOption {
+        default = "gocd-server";
+        type = types.str;
+        description = ''
+          User the Go.CD server should execute under.
+        '';
+      };
+
+      group = mkOption {
+        default = "gocd-server";
+        type = types.str;
+        description = ''
+          If the default user "gocd-server" is configured then this is the primary group of that user.
+        '';
+      };
+
+      extraGroups = mkOption {
+        default = [ ];
+        type = types.listOf types.str;
+        example = [ "wheel" "docker" ];
+        description = ''
+          List of extra groups that the "gocd-server" user should be a part of.
+        '';
+      };
+
+      listenAddress = mkOption {
+        default = "0.0.0.0";
+        example = "localhost";
+        type = types.str;
+        description = ''
+          Specifies the bind address on which the Go.CD server HTTP interface listens.
+        '';
+      };
+
+      port = mkOption {
+        default = 8153;
+        type = types.int;
+        description = ''
+          Specifies port number on which the Go.CD server HTTP interface listens.
+        '';
+      };
+
+      sslPort = mkOption {
+        default = 8154;
+        type = types.int;
+        description = ''
+          Specifies port number on which the Go.CD server HTTPS interface listens.
+        '';
+      };
+
+      workDir = mkOption {
+        default = "/var/lib/go-server";
+        type = types.str;
+        description = ''
+          Specifies the working directory in which the Go.CD server java archive resides.
+        '';
+      };
+
+      packages = mkOption {
+        default = [ pkgs.stdenv pkgs.jre pkgs.git config.programs.ssh.package pkgs.nix ];
+        defaultText = literalExpression "[ pkgs.stdenv pkgs.jre pkgs.git config.programs.ssh.package pkgs.nix ]";
+        type = types.listOf types.package;
+        description = ''
+          Packages to add to PATH for the Go.CD server's process.
+        '';
+      };
+
+      initialJavaHeapSize = mkOption {
+        default = "512m";
+        type = types.str;
+        description = ''
+          Specifies the initial java heap memory size for the Go.CD server's java process.
+        '';
+      };
+
+      maxJavaHeapMemory = mkOption {
+        default = "1024m";
+        type = types.str;
+        description = ''
+          Specifies the java maximum heap memory size for the Go.CD server's java process.
+        '';
+      };
+
+      startupOptions = mkOption {
+        type = types.listOf types.str;
+        default = [
+          "-Xms${cfg.initialJavaHeapSize}"
+          "-Xmx${cfg.maxJavaHeapMemory}"
+          "-Dcruise.listen.host=${cfg.listenAddress}"
+          "-Duser.language=en"
+          "-Djruby.rack.request.size.threshold.bytes=30000000"
+          "-Duser.country=US"
+          "-Dcruise.config.dir=${cfg.workDir}/conf"
+          "-Dcruise.config.file=${cfg.workDir}/conf/cruise-config.xml"
+          "-Dcruise.server.port=${toString cfg.port}"
+          "-Dcruise.server.ssl.port=${toString cfg.sslPort}"
+        ];
+        defaultText = literalExpression ''
+          [
+            "-Xms''${config.${opt.initialJavaHeapSize}}"
+            "-Xmx''${config.${opt.maxJavaHeapMemory}}"
+            "-Dcruise.listen.host=''${config.${opt.listenAddress}}"
+            "-Duser.language=en"
+            "-Djruby.rack.request.size.threshold.bytes=30000000"
+            "-Duser.country=US"
+            "-Dcruise.config.dir=''${config.${opt.workDir}}/conf"
+            "-Dcruise.config.file=''${config.${opt.workDir}}/conf/cruise-config.xml"
+            "-Dcruise.server.port=''${toString config.${opt.port}}"
+            "-Dcruise.server.ssl.port=''${toString config.${opt.sslPort}}"
+          ]
+        '';
+
+        description = ''
+          Specifies startup command line arguments to pass to Go.CD server
+          java process.
+        '';
+      };
+
+      extraOptions = mkOption {
+        default = [ ];
+        type = types.listOf types.str;
+        example = [
+          "-X debug"
+          "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"
+          "-verbose:gc"
+          "-Xloggc:go-server-gc.log"
+          "-XX:+PrintGCTimeStamps"
+          "-XX:+PrintTenuringDistribution"
+          "-XX:+PrintGCDetails"
+          "-XX:+PrintGC"
+        ];
+        description = ''
+          Specifies additional command line arguments to pass to Go.CD server's
+          java process.  Example contains debug and gcLog arguments.
+        '';
+      };
+
+      environment = mkOption {
+        default = { };
+        type = with types; attrsOf str;
+        description = ''
+          Additional environment variables to be passed to the gocd-server process.
+          As a base environment, gocd-server receives NIX_PATH from
+          <option>environment.sessionVariables</option>, NIX_REMOTE is set to
+          "daemon".
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups = optionalAttrs (cfg.group == "gocd-server") {
+      gocd-server.gid = config.ids.gids.gocd-server;
+    };
+
+    users.users = optionalAttrs (cfg.user == "gocd-server") {
+      gocd-server = {
+        description = "gocd-server user";
+        createHome = true;
+        home = cfg.workDir;
+        group = cfg.group;
+        extraGroups = cfg.extraGroups;
+        useDefaultShell = true;
+        uid = config.ids.uids.gocd-server;
+      };
+    };
+
+    systemd.services.gocd-server = {
+      description = "GoCD Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment =
+        let
+          selectedSessionVars =
+            lib.filterAttrs (n: v: builtins.elem n [ "NIX_PATH" ])
+              config.environment.sessionVariables;
+        in
+          selectedSessionVars //
+            { NIX_REMOTE = "daemon";
+            } //
+            cfg.environment;
+
+      path = cfg.packages;
+
+      script = ''
+        ${pkgs.git}/bin/git config --global --add http.sslCAinfo /etc/ssl/certs/ca-certificates.crt
+        ${pkgs.jre}/bin/java -server ${concatStringsSep " " cfg.startupOptions} \
+                               ${concatStringsSep " " cfg.extraOptions}  \
+                              -jar ${pkgs.gocd-server}/go-server/go.jar
+      '';
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        WorkingDirectory = cfg.workDir;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/hail.nix b/nixos/modules/services/continuous-integration/hail.nix
new file mode 100644
index 00000000000..4070a3425c4
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/hail.nix
@@ -0,0 +1,61 @@
+{ config, lib, pkgs, ...}:
+
+with lib;
+
+let
+  cfg = config.services.hail;
+in {
+
+
+  ###### interface
+
+  options.services.hail = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enables the Hail Auto Update Service. Hail can automatically deploy artifacts
+        built by a Hydra Continous Integration server. A common use case is to provide
+        continous deployment for single services or a full NixOS configuration.'';
+    };
+    profile = mkOption {
+      type = types.str;
+      default = "hail-profile";
+      description = "The name of the Nix profile used by Hail.";
+    };
+    hydraJobUri = mkOption {
+      type = types.str;
+      description = "The URI of the Hydra Job.";
+    };
+    netrc = mkOption {
+      type = types.nullOr types.path;
+      description = "The netrc file to use when fetching data from Hydra.";
+      default = null;
+    };
+    package = mkOption {
+      type = types.package;
+      default = pkgs.haskellPackages.hail;
+      defaultText = literalExpression "pkgs.haskellPackages.hail";
+      description = "Hail package to use.";
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.hail = {
+      description = "Hail Auto Update Service";
+      wants = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = with pkgs; [ nix ];
+      environment = {
+        HOME = "/var/lib/empty";
+      };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/hail --profile ${cfg.profile} --job-uri ${cfg.hydraJobUri}"
+          + lib.optionalString (cfg.netrc != null) " --netrc-file ${cfg.netrc}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix b/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix
new file mode 100644
index 00000000000..80c88714bfc
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix
@@ -0,0 +1,266 @@
+/*
+
+  This file is for options that NixOS and nix-darwin have in common.
+
+  Platform-specific code is in the respective default.nix files.
+
+*/
+
+{ config, lib, options, pkgs, ... }:
+let
+  inherit (lib)
+    filterAttrs
+    literalDocBook
+    literalExpression
+    mkIf
+    mkOption
+    mkRemovedOptionModule
+    mkRenamedOptionModule
+    types
+    ;
+
+  cfg =
+    config.services.hercules-ci-agent;
+
+  format = pkgs.formats.toml { };
+
+  settingsModule = { config, ... }: {
+    freeformType = format.type;
+    options = {
+      apiBaseUrl = mkOption {
+        description = ''
+          API base URL that the agent will connect to.
+
+          When using Hercules CI Enterprise, set this to the URL where your
+          Hercules CI server is reachable.
+        '';
+        type = types.str;
+        default = "https://hercules-ci.com";
+      };
+      baseDirectory = mkOption {
+        type = types.path;
+        default = "/var/lib/hercules-ci-agent";
+        description = ''
+          State directory (secrets, work directory, etc) for agent
+        '';
+      };
+      concurrentTasks = mkOption {
+        description = ''
+          Number of tasks to perform simultaneously.
+
+          A task is a single derivation build, an evaluation or an effect run.
+          At minimum, you need 2 concurrent tasks for <literal>x86_64-linux</literal>
+          in your cluster, to allow for import from derivation.
+
+          <literal>concurrentTasks</literal> can be around the CPU core count or lower if memory is
+          the bottleneck.
+
+          The optimal value depends on the resource consumption characteristics of your workload,
+          including memory usage and in-task parallelism. This is typically determined empirically.
+
+          When scaling, it is generally better to have a double-size machine than two machines,
+          because each split of resources causes inefficiencies; particularly with regards
+          to build latency because of extra downloads.
+        '';
+        type = types.either types.ints.positive (types.enum [ "auto" ]);
+        default = "auto";
+      };
+      labels = mkOption {
+        description = ''
+          A key-value map of user data.
+
+          This data will be available to organization members in the dashboard and API.
+
+          The values can be of any TOML type that corresponds to a JSON type, but arrays
+          can not contain tables/objects due to limitations of the TOML library. Values
+          involving arrays of non-primitive types may not be representable currently.
+        '';
+        type = format.type;
+        defaultText = literalExpression ''
+          {
+            agent.source = "..."; # One of "nixpkgs", "flake", "override"
+            lib.version = "...";
+            pkgs.version = "...";
+          }
+        '';
+      };
+      workDirectory = mkOption {
+        description = ''
+          The directory in which temporary subdirectories are created for task state. This includes sources for Nix evaluation.
+        '';
+        type = types.path;
+        default = config.baseDirectory + "/work";
+        defaultText = literalExpression ''baseDirectory + "/work"'';
+      };
+      staticSecretsDirectory = mkOption {
+        description = ''
+          This is the default directory to look for statically configured secrets like <literal>cluster-join-token.key</literal>.
+
+          See also <literal>clusterJoinTokenPath</literal> and <literal>binaryCachesPath</literal> for fine-grained configuration.
+        '';
+        type = types.path;
+        default = config.baseDirectory + "/secrets";
+        defaultText = literalExpression ''baseDirectory + "/secrets"'';
+      };
+      clusterJoinTokenPath = mkOption {
+        description = ''
+          Location of the cluster-join-token.key file.
+
+          You can retrieve the contents of the file when creating a new agent via
+          <link xlink:href="https://hercules-ci.com/dashboard">https://hercules-ci.com/dashboard</link>.
+
+          As this value is confidential, it should not be in the store, but
+          installed using other means, such as agenix, NixOps
+          <literal>deployment.keys</literal>, or manual installation.
+
+          The contents of the file are used for authentication between the agent and the API.
+        '';
+        type = types.path;
+        default = config.staticSecretsDirectory + "/cluster-join-token.key";
+        defaultText = literalExpression ''staticSecretsDirectory + "/cluster-join-token.key"'';
+      };
+      binaryCachesPath = mkOption {
+        description = ''
+          Path to a JSON file containing binary cache secret keys.
+
+          As these values are confidential, they should not be in the store, but
+          copied over using other means, such as agenix, NixOps
+          <literal>deployment.keys</literal>, or manual installation.
+
+          The format is described on <link xlink:href="https://docs.hercules-ci.com/hercules-ci-agent/binary-caches-json/">https://docs.hercules-ci.com/hercules-ci-agent/binary-caches-json/</link>.
+        '';
+        type = types.path;
+        default = config.staticSecretsDirectory + "/binary-caches.json";
+        defaultText = literalExpression ''staticSecretsDirectory + "/binary-caches.json"'';
+      };
+      secretsJsonPath = mkOption {
+        description = ''
+          Path to a JSON file containing secrets for effects.
+
+          As these values are confidential, they should not be in the store, but
+          copied over using other means, such as agenix, NixOps
+          <literal>deployment.keys</literal>, or manual installation.
+
+          The format is described on <link xlink:href="https://docs.hercules-ci.com/hercules-ci-agent/secrets-json/">https://docs.hercules-ci.com/hercules-ci-agent/secrets-json/</link>.
+
+        '';
+        type = types.path;
+        default = config.staticSecretsDirectory + "/secrets.json";
+        defaultText = literalExpression ''staticSecretsDirectory + "/secrets.json"'';
+      };
+    };
+  };
+
+  # TODO (roberth, >=2022) remove
+  checkNix =
+    if !cfg.checkNix
+    then ""
+    else if lib.versionAtLeast config.nix.package.version "2.3.10"
+    then ""
+    else
+      pkgs.stdenv.mkDerivation {
+        name = "hercules-ci-check-system-nix-src";
+        inherit (config.nix.package) src patches;
+        dontConfigure = true;
+        buildPhase = ''
+          echo "Checking in-memory pathInfoCache expiry"
+          if ! grep 'PathInfoCacheValue' src/libstore/store-api.hh >/dev/null; then
+            cat 1>&2 <<EOF
+
+            You are deploying Hercules CI Agent on a system with an incompatible
+            nix-daemon. Please make sure nix.package is set to a Nix version of at
+            least 2.3.10 or a master version more recent than Mar 12, 2020.
+          EOF
+            exit 1
+          fi
+        '';
+        installPhase = "touch $out";
+      };
+
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "hercules-ci-agent" "extraOptions" ] [ "services" "hercules-ci-agent" "settings" ])
+    (mkRenamedOptionModule [ "services" "hercules-ci-agent" "baseDirectory" ] [ "services" "hercules-ci-agent" "settings" "baseDirectory" ])
+    (mkRenamedOptionModule [ "services" "hercules-ci-agent" "concurrentTasks" ] [ "services" "hercules-ci-agent" "settings" "concurrentTasks" ])
+    (mkRemovedOptionModule [ "services" "hercules-ci-agent" "patchNix" ] "Nix versions packaged in this version of Nixpkgs don't need a patched nix-daemon to work correctly in Hercules CI Agent clusters.")
+  ];
+
+  options.services.hercules-ci-agent = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable to run Hercules CI Agent as a system service.
+
+        <link xlink:href="https://hercules-ci.com">Hercules CI</link> is a
+        continuous integation service that is centered around Nix.
+
+        Support is available at <link xlink:href="mailto:help@hercules-ci.com">help@hercules-ci.com</link>.
+      '';
+    };
+    checkNix = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to make sure that the system's Nix (nix-daemon) is compatible.
+
+        If you set this to false, please keep up with the change log.
+      '';
+    };
+    package = mkOption {
+      description = ''
+        Package containing the bin/hercules-ci-agent executable.
+      '';
+      type = types.package;
+      default = pkgs.hercules-ci-agent;
+      defaultText = literalExpression "pkgs.hercules-ci-agent";
+    };
+    settings = mkOption {
+      description = ''
+        These settings are written to the <literal>agent.toml</literal> file.
+
+        Not all settings are listed as options, can be set nonetheless.
+
+        For the exhaustive list of settings, see <link xlink:href="https://docs.hercules-ci.com/hercules-ci/reference/agent-config/"/>.
+      '';
+      type = types.submoduleWith { modules = [ settingsModule ]; };
+    };
+
+    /*
+      Internal and/or computed values.
+
+      These are written as options instead of let binding to allow sharing with
+      default.nix on both NixOS and nix-darwin.
+    */
+    tomlFile = mkOption {
+      type = types.path;
+      internal = true;
+      defaultText = literalDocBook "generated <literal>hercules-ci-agent.toml</literal>";
+      description = ''
+        The fully assembled config file.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    nix.extraOptions = lib.addContextFrom checkNix ''
+      # A store path that was missing at first may well have finished building,
+      # even shortly after the previous lookup. This *also* applies to the daemon.
+      narinfo-cache-negative-ttl = 0
+    '';
+    services.hercules-ci-agent = {
+      tomlFile =
+        format.generate "hercules-ci-agent.toml" cfg.settings;
+
+      settings.labels = {
+        agent.source =
+          if options.services.hercules-ci-agent.package.highestPrio == (lib.modules.mkOptionDefault { }).priority
+          then "nixpkgs"
+          else lib.mkOptionDefault "override";
+        pkgs.version = pkgs.lib.version;
+        lib.version = lib.version;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix b/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
new file mode 100644
index 00000000000..ef1933e1228
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
@@ -0,0 +1,101 @@
+/*
+
+  This file is for NixOS-specific options and configs.
+
+  Code that is shared with nix-darwin goes in common.nix.
+
+*/
+
+{ pkgs, config, lib, ... }:
+let
+  inherit (lib) mkIf mkDefault;
+
+  cfg = config.services.hercules-ci-agent;
+
+  command = "${cfg.package}/bin/hercules-ci-agent --config ${cfg.tomlFile}";
+  testCommand = "${command} --test-configuration";
+
+in
+{
+  imports = [
+    ./common.nix
+    (lib.mkRenamedOptionModule [ "services" "hercules-ci-agent" "user" ] [ "systemd" "services" "hercules-ci-agent" "serviceConfig" "User" ])
+  ];
+
+  config = mkIf cfg.enable {
+    systemd.services.hercules-ci-agent = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
+      path = [ config.nix.package ];
+      startLimitBurst = 30 * 1000000; # practically infinite
+      serviceConfig = {
+        User = "hercules-ci-agent";
+        ExecStart = command;
+        ExecStartPre = testCommand;
+        Restart = "on-failure";
+        RestartSec = 120;
+      };
+    };
+
+    # Changes in the secrets do not affect the unit in any way that would cause
+    # a restart, which is currently necessary to reload the secrets.
+    systemd.paths.hercules-ci-agent-restart-files = {
+      wantedBy = [ "hercules-ci-agent.service" ];
+      pathConfig = {
+        Unit = "hercules-ci-agent-restarter.service";
+        PathChanged = [ cfg.settings.clusterJoinTokenPath cfg.settings.binaryCachesPath ];
+      };
+    };
+    systemd.services.hercules-ci-agent-restarter = {
+      serviceConfig.Type = "oneshot";
+      script = ''
+        # Wait a bit, with the effect of bundling up file changes into a single
+        # run of this script and hopefully a single restart.
+        sleep 10
+        if systemctl is-active --quiet hercules-ci-agent.service; then
+          if ${testCommand}; then
+            systemctl restart hercules-ci-agent.service
+          else
+            echo 1>&2 "WARNING: Not restarting agent because config is not valid at this time."
+          fi
+        else
+          echo 1>&2 "Not restarting hercules-ci-agent despite config file update, because it is not already active."
+        fi
+      '';
+    };
+
+    # Trusted user allows simplified configuration and better performance
+    # when operating in a cluster.
+    nix.settings.trusted-users = [ config.systemd.services.hercules-ci-agent.serviceConfig.User ];
+    services.hercules-ci-agent = {
+      settings = {
+        nixUserIsTrusted = true;
+        labels =
+          let
+            mkIfNotNull = x: mkIf (x != null) x;
+          in
+          {
+            nixos.configurationRevision = mkIfNotNull config.system.configurationRevision;
+            nixos.release = config.system.nixos.release;
+            nixos.label = mkIfNotNull config.system.nixos.label;
+            nixos.codeName = config.system.nixos.codeName;
+            nixos.tags = config.system.nixos.tags;
+            nixos.systemName = mkIfNotNull config.system.name;
+          };
+      };
+    };
+
+    users.users.hercules-ci-agent = {
+      home = cfg.settings.baseDirectory;
+      createHome = true;
+      group = "hercules-ci-agent";
+      description = "Hercules CI Agent system user";
+      isSystemUser = true;
+    };
+
+    users.groups.hercules-ci-agent = { };
+  };
+
+  meta.maintainers = [ lib.maintainers.roberth ];
+}
diff --git a/nixos/modules/services/continuous-integration/hydra/default.nix b/nixos/modules/services/continuous-integration/hydra/default.nix
new file mode 100644
index 00000000000..cc5de97d6d1
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/hydra/default.nix
@@ -0,0 +1,501 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.hydra;
+
+  baseDir = "/var/lib/hydra";
+
+  hydraConf = pkgs.writeScript "hydra.conf" cfg.extraConfig;
+
+  hydraEnv =
+    { HYDRA_DBI = cfg.dbi;
+      HYDRA_CONFIG = "${baseDir}/hydra.conf";
+      HYDRA_DATA = "${baseDir}";
+    };
+
+  env =
+    { NIX_REMOTE = "daemon";
+      SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt"; # Remove in 16.03
+      PGPASSFILE = "${baseDir}/pgpass";
+      NIX_REMOTE_SYSTEMS = concatStringsSep ":" cfg.buildMachinesFiles;
+    } // optionalAttrs (cfg.smtpHost != null) {
+      EMAIL_SENDER_TRANSPORT = "SMTP";
+      EMAIL_SENDER_TRANSPORT_host = cfg.smtpHost;
+    } // hydraEnv // cfg.extraEnv;
+
+  serverEnv = env //
+    { HYDRA_TRACKER = cfg.tracker;
+      XDG_CACHE_HOME = "${baseDir}/www/.cache";
+      COLUMNS = "80";
+      PGPASSFILE = "${baseDir}/pgpass-www"; # grrr
+    } // (optionalAttrs cfg.debugServer { DBIC_TRACE = "1"; });
+
+  localDB = "dbi:Pg:dbname=hydra;user=hydra;";
+
+  haveLocalDB = cfg.dbi == localDB;
+
+  hydra-package =
+  let
+    makeWrapperArgs = concatStringsSep " " (mapAttrsToList (key: value: "--set \"${key}\" \"${value}\"") hydraEnv);
+  in pkgs.buildEnv rec {
+    name = "hydra-env";
+    buildInputs = [ pkgs.makeWrapper ];
+    paths = [ cfg.package ];
+
+    postBuild = ''
+      if [ -L "$out/bin" ]; then
+          unlink "$out/bin"
+      fi
+      mkdir -p "$out/bin"
+
+      for path in ${concatStringsSep " " paths}; do
+        if [ -d "$path/bin" ]; then
+          cd "$path/bin"
+          for prg in *; do
+            if [ -f "$prg" ]; then
+              rm -f "$out/bin/$prg"
+              if [ -x "$prg" ]; then
+                makeWrapper "$path/bin/$prg" "$out/bin/$prg" ${makeWrapperArgs}
+              fi
+            fi
+          done
+        fi
+      done
+   '';
+  };
+
+in
+
+{
+  ###### interface
+  options = {
+
+    services.hydra = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run Hydra services.
+        '';
+      };
+
+      dbi = mkOption {
+        type = types.str;
+        default = localDB;
+        example = "dbi:Pg:dbname=hydra;host=postgres.example.org;user=foo;";
+        description = ''
+          The DBI string for Hydra database connection.
+
+          NOTE: Attempts to set `application_name` will be overridden by
+          `hydra-TYPE` (where TYPE is e.g. `evaluator`, `queue-runner`,
+          etc.) in all hydra services to more easily distinguish where
+          queries are coming from.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.hydra-unstable;
+        defaultText = literalExpression "pkgs.hydra-unstable";
+        description = "The Hydra package.";
+      };
+
+      hydraURL = mkOption {
+        type = types.str;
+        description = ''
+          The base URL for the Hydra webserver instance. Used for links in emails.
+        '';
+      };
+
+      listenHost = mkOption {
+        type = types.str;
+        default = "*";
+        example = "localhost";
+        description = ''
+          The hostname or address to listen on or <literal>*</literal> to listen
+          on all interfaces.
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 3000;
+        description = ''
+          TCP port the web server should listen to.
+        '';
+      };
+
+      minimumDiskFree = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          Threshold of minimum disk space (GiB) to determine if the queue runner should run or not.
+        '';
+      };
+
+      minimumDiskFreeEvaluator = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          Threshold of minimum disk space (GiB) to determine if the evaluator should run or not.
+        '';
+      };
+
+      notificationSender = mkOption {
+        type = types.str;
+        description = ''
+          Sender email address used for email notifications.
+        '';
+      };
+
+      smtpHost = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "localhost";
+        description = ''
+          Hostname of the SMTP server to use to send email.
+        '';
+      };
+
+      tracker = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Piece of HTML that is included on all pages.
+        '';
+      };
+
+      logo = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          Path to a file containing the logo of your Hydra instance.
+        '';
+      };
+
+      debugServer = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to run the server in debug mode.";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        description = "Extra lines for the Hydra configuration.";
+      };
+
+      extraEnv = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        description = "Extra environment variables for Hydra.";
+      };
+
+      gcRootsDir = mkOption {
+        type = types.path;
+        default = "/nix/var/nix/gcroots/hydra";
+        description = "Directory that holds Hydra garbage collector roots.";
+      };
+
+      buildMachinesFiles = mkOption {
+        type = types.listOf types.path;
+        default = optional (config.nix.buildMachines != []) "/etc/nix/machines";
+        defaultText = literalExpression ''optional (config.nix.buildMachines != []) "/etc/nix/machines"'';
+        example = [ "/etc/nix/machines" "/var/lib/hydra/provisioner/machines" ];
+        description = "List of files containing build machines.";
+      };
+
+      useSubstitutes = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to use binary caches for downloading store paths. Note that
+          binary substitutions trigger (a potentially large number of) additional
+          HTTP requests that slow down the queue monitor thread significantly.
+          Also, this Hydra instance will serve those downloaded store paths to
+          its users with its own signature attached as if it had built them
+          itself, so don't enable this feature unless your active binary caches
+          are absolute trustworthy.
+        '';
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.groups.hydra = {
+      gid = config.ids.gids.hydra;
+    };
+
+    users.users.hydra =
+      { description = "Hydra";
+        group = "hydra";
+        # We don't enable `createHome` here because the creation of the home directory is handled by the hydra-init service below.
+        home = baseDir;
+        useDefaultShell = true;
+        uid = config.ids.uids.hydra;
+      };
+
+    users.users.hydra-queue-runner =
+      { description = "Hydra queue runner";
+        group = "hydra";
+        useDefaultShell = true;
+        home = "${baseDir}/queue-runner"; # really only to keep SSH happy
+        uid = config.ids.uids.hydra-queue-runner;
+      };
+
+    users.users.hydra-www =
+      { description = "Hydra web server";
+        group = "hydra";
+        useDefaultShell = true;
+        uid = config.ids.uids.hydra-www;
+      };
+
+    services.hydra.extraConfig =
+      ''
+        using_frontend_proxy = 1
+        base_uri = ${cfg.hydraURL}
+        notification_sender = ${cfg.notificationSender}
+        max_servers = 25
+        ${optionalString (cfg.logo != null) ''
+          hydra_logo = ${cfg.logo}
+        ''}
+        gc_roots_dir = ${cfg.gcRootsDir}
+        use-substitutes = ${if cfg.useSubstitutes then "1" else "0"}
+      '';
+
+    environment.systemPackages = [ hydra-package ];
+
+    environment.variables = hydraEnv;
+
+    nix.settings = mkMerge [
+      {
+        keep-outputs = true;
+        keep-derivations = true;
+        trusted-users = [ "hydra-queue-runner" ];
+      }
+
+      (mkIf (versionOlder (getVersion config.nix.package.out) "2.4pre")
+        {
+          # The default (`true') slows Nix down a lot since the build farm
+          # has so many GC roots.
+          gc-check-reachability = false;
+        }
+      )
+    ];
+
+    systemd.services.hydra-init =
+      { wantedBy = [ "multi-user.target" ];
+        requires = optional haveLocalDB "postgresql.service";
+        after = optional haveLocalDB "postgresql.service";
+        environment = env // {
+          HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-init";
+        };
+        preStart = ''
+          mkdir -p ${baseDir}
+          chown hydra.hydra ${baseDir}
+          chmod 0750 ${baseDir}
+
+          ln -sf ${hydraConf} ${baseDir}/hydra.conf
+
+          mkdir -m 0700 -p ${baseDir}/www
+          chown hydra-www.hydra ${baseDir}/www
+
+          mkdir -m 0700 -p ${baseDir}/queue-runner
+          mkdir -m 0750 -p ${baseDir}/build-logs
+          chown hydra-queue-runner.hydra ${baseDir}/queue-runner ${baseDir}/build-logs
+
+          ${optionalString haveLocalDB ''
+            if ! [ -e ${baseDir}/.db-created ]; then
+              ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createuser hydra
+              ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createdb -O hydra hydra
+              touch ${baseDir}/.db-created
+            fi
+            echo "create extension if not exists pg_trgm" | ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} -- ${config.services.postgresql.package}/bin/psql hydra
+          ''}
+
+          if [ ! -e ${cfg.gcRootsDir} ]; then
+
+            # Move legacy roots directory.
+            if [ -e /nix/var/nix/gcroots/per-user/hydra/hydra-roots ]; then
+              mv /nix/var/nix/gcroots/per-user/hydra/hydra-roots ${cfg.gcRootsDir}
+            fi
+
+            mkdir -p ${cfg.gcRootsDir}
+          fi
+
+          # Move legacy hydra-www roots.
+          if [ -e /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots ]; then
+            find /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots/ -type f \
+              | xargs -r mv -f -t ${cfg.gcRootsDir}/
+            rmdir /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots
+          fi
+
+          chown hydra.hydra ${cfg.gcRootsDir}
+          chmod 2775 ${cfg.gcRootsDir}
+        '';
+        serviceConfig.ExecStart = "${hydra-package}/bin/hydra-init";
+        serviceConfig.PermissionsStartOnly = true;
+        serviceConfig.User = "hydra";
+        serviceConfig.Type = "oneshot";
+        serviceConfig.RemainAfterExit = true;
+      };
+
+    systemd.services.hydra-server =
+      { wantedBy = [ "multi-user.target" ];
+        requires = [ "hydra-init.service" ];
+        after = [ "hydra-init.service" ];
+        environment = serverEnv // {
+          HYDRA_DBI = "${serverEnv.HYDRA_DBI};application_name=hydra-server";
+        };
+        restartTriggers = [ hydraConf ];
+        serviceConfig =
+          { ExecStart =
+              "@${hydra-package}/bin/hydra-server hydra-server -f -h '${cfg.listenHost}' "
+              + "-p ${toString cfg.port} --max_spare_servers 5 --max_servers 25 "
+              + "--max_requests 100 ${optionalString cfg.debugServer "-d"}";
+            User = "hydra-www";
+            PermissionsStartOnly = true;
+            Restart = "always";
+          };
+      };
+
+    systemd.services.hydra-queue-runner =
+      { wantedBy = [ "multi-user.target" ];
+        requires = [ "hydra-init.service" ];
+        after = [ "hydra-init.service" "network.target" ];
+        path = [ hydra-package pkgs.nettools pkgs.openssh pkgs.bzip2 config.nix.package ];
+        restartTriggers = [ hydraConf ];
+        environment = env // {
+          PGPASSFILE = "${baseDir}/pgpass-queue-runner"; # grrr
+          IN_SYSTEMD = "1"; # to get log severity levels
+          HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-queue-runner";
+        };
+        serviceConfig =
+          { ExecStart = "@${hydra-package}/bin/hydra-queue-runner hydra-queue-runner -v";
+            ExecStopPost = "${hydra-package}/bin/hydra-queue-runner --unlock";
+            User = "hydra-queue-runner";
+            Restart = "always";
+
+            # Ensure we can get core dumps.
+            LimitCORE = "infinity";
+            WorkingDirectory = "${baseDir}/queue-runner";
+          };
+      };
+
+    systemd.services.hydra-evaluator =
+      { wantedBy = [ "multi-user.target" ];
+        requires = [ "hydra-init.service" ];
+        after = [ "hydra-init.service" "network.target" ];
+        path = with pkgs; [ hydra-package nettools jq ];
+        restartTriggers = [ hydraConf ];
+        environment = env // {
+          HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-evaluator";
+        };
+        serviceConfig =
+          { ExecStart = "@${hydra-package}/bin/hydra-evaluator hydra-evaluator";
+            User = "hydra";
+            Restart = "always";
+            WorkingDirectory = baseDir;
+          };
+      };
+
+    systemd.services.hydra-update-gc-roots =
+      { requires = [ "hydra-init.service" ];
+        after = [ "hydra-init.service" ];
+        environment = env // {
+          HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-update-gc-roots";
+        };
+        serviceConfig =
+          { ExecStart = "@${hydra-package}/bin/hydra-update-gc-roots hydra-update-gc-roots";
+            User = "hydra";
+          };
+        startAt = "2,14:15";
+      };
+
+    systemd.services.hydra-send-stats =
+      { wantedBy = [ "multi-user.target" ];
+        after = [ "hydra-init.service" ];
+        environment = env // {
+          HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-send-stats";
+        };
+        serviceConfig =
+          { ExecStart = "@${hydra-package}/bin/hydra-send-stats hydra-send-stats";
+            User = "hydra";
+          };
+      };
+
+    systemd.services.hydra-notify =
+      { wantedBy = [ "multi-user.target" ];
+        requires = [ "hydra-init.service" ];
+        after = [ "hydra-init.service" ];
+        restartTriggers = [ hydraConf ];
+        environment = env // {
+          PGPASSFILE = "${baseDir}/pgpass-queue-runner";
+          HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-notify";
+        };
+        serviceConfig =
+          { ExecStart = "@${hydra-package}/bin/hydra-notify hydra-notify";
+            # FIXME: run this under a less privileged user?
+            User = "hydra-queue-runner";
+            Restart = "always";
+            RestartSec = 5;
+          };
+      };
+
+    # If there is less than a certain amount of free disk space, stop
+    # the queue/evaluator to prevent builds from failing or aborting.
+    systemd.services.hydra-check-space =
+      { script =
+          ''
+            if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFree} * 1024**3)) ]; then
+                echo "stopping Hydra queue runner due to lack of free space..."
+                systemctl stop hydra-queue-runner
+            fi
+            if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFreeEvaluator} * 1024**3)) ]; then
+                echo "stopping Hydra evaluator due to lack of free space..."
+                systemctl stop hydra-evaluator
+            fi
+          '';
+        startAt = "*:0/5";
+      };
+
+    # Periodically compress build logs. The queue runner compresses
+    # logs automatically after a step finishes, but this doesn't work
+    # if the queue runner is stopped prematurely.
+    systemd.services.hydra-compress-logs =
+      { path = [ pkgs.bzip2 ];
+        script =
+          ''
+            find /var/lib/hydra/build-logs -type f -name "*.drv" -mtime +3 -size +0c | xargs -r bzip2 -v -f
+          '';
+        startAt = "Sun 01:45";
+      };
+
+    services.postgresql.enable = mkIf haveLocalDB true;
+
+    services.postgresql.identMap = optionalString haveLocalDB
+      ''
+        hydra-users hydra hydra
+        hydra-users hydra-queue-runner hydra
+        hydra-users hydra-www hydra
+        hydra-users root hydra
+        # The postgres user is used to create the pg_trgm extension for the hydra database
+        hydra-users postgres postgres
+      '';
+
+    services.postgresql.authentication = optionalString haveLocalDB
+      ''
+        local hydra all ident map=hydra-users
+      '';
+
+  };
+
+}
diff --git a/nixos/modules/services/continuous-integration/jenkins/default.nix b/nixos/modules/services/continuous-integration/jenkins/default.nix
new file mode 100644
index 00000000000..d37dcb5519d
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/jenkins/default.nix
@@ -0,0 +1,249 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.jenkins;
+  jenkinsUrl = "http://${cfg.listenAddress}:${toString cfg.port}${cfg.prefix}";
+in {
+  options = {
+    services.jenkins = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the jenkins continuous integration server.
+        '';
+      };
+
+      user = mkOption {
+        default = "jenkins";
+        type = types.str;
+        description = ''
+          User the jenkins server should execute under.
+        '';
+      };
+
+      group = mkOption {
+        default = "jenkins";
+        type = types.str;
+        description = ''
+          If the default user "jenkins" is configured then this is the primary
+          group of that user.
+        '';
+      };
+
+      extraGroups = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "wheel" "dialout" ];
+        description = ''
+          List of extra groups that the "jenkins" user should be a part of.
+        '';
+      };
+
+      home = mkOption {
+        default = "/var/lib/jenkins";
+        type = types.path;
+        description = ''
+          The path to use as JENKINS_HOME. If the default user "jenkins" is configured then
+          this is the home of the "jenkins" user.
+        '';
+      };
+
+      listenAddress = mkOption {
+        default = "0.0.0.0";
+        example = "localhost";
+        type = types.str;
+        description = ''
+          Specifies the bind address on which the jenkins HTTP interface listens.
+          The default is the wildcard address.
+        '';
+      };
+
+      port = mkOption {
+        default = 8080;
+        type = types.port;
+        description = ''
+          Specifies port number on which the jenkins HTTP interface listens.
+          The default is 8080.
+        '';
+      };
+
+      prefix = mkOption {
+        default = "";
+        example = "/jenkins";
+        type = types.str;
+        description = ''
+          Specifies a urlPrefix to use with jenkins.
+          If the example /jenkins is given, the jenkins server will be
+          accessible using localhost:8080/jenkins.
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.jenkins;
+        defaultText = literalExpression "pkgs.jenkins";
+        type = types.package;
+        description = "Jenkins package to use.";
+      };
+
+      packages = mkOption {
+        default = [ pkgs.stdenv pkgs.git pkgs.jdk11 config.programs.ssh.package pkgs.nix ];
+        defaultText = literalExpression "[ pkgs.stdenv pkgs.git pkgs.jdk11 config.programs.ssh.package pkgs.nix ]";
+        type = types.listOf types.package;
+        description = ''
+          Packages to add to PATH for the jenkins process.
+        '';
+      };
+
+      environment = mkOption {
+        default = { };
+        type = with types; attrsOf str;
+        description = ''
+          Additional environment variables to be passed to the jenkins process.
+          As a base environment, jenkins receives NIX_PATH from
+          <option>environment.sessionVariables</option>, NIX_REMOTE is set to
+          "daemon" and JENKINS_HOME is set to the value of
+          <option>services.jenkins.home</option>.
+          This option has precedence and can be used to override those
+          mentioned variables.
+        '';
+      };
+
+      plugins = mkOption {
+        default = null;
+        type = types.nullOr (types.attrsOf types.package);
+        description = ''
+          A set of plugins to activate. Note that this will completely
+          remove and replace any previously installed plugins. If you
+          have manually-installed plugins that you want to keep while
+          using this module, set this option to
+          <literal>null</literal>. You can generate this set with a
+          tool such as <literal>jenkinsPlugins2nix</literal>.
+        '';
+        example = literalExpression ''
+          import path/to/jenkinsPlugins2nix-generated-plugins.nix { inherit (pkgs) fetchurl stdenv; }
+        '';
+      };
+
+      extraOptions = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "--debug=9" ];
+        description = ''
+          Additional command line arguments to pass to Jenkins.
+        '';
+      };
+
+      extraJavaOptions = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "-Xmx80m" ];
+        description = ''
+          Additional command line arguments to pass to the Java run time (as opposed to Jenkins).
+        '';
+      };
+
+      withCLI = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to make the CLI available.
+
+          More info about the CLI available at
+          <link xlink:href="https://www.jenkins.io/doc/book/managing/cli">
+          https://www.jenkins.io/doc/book/managing/cli</link> .
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment = {
+      # server references the dejavu fonts
+      systemPackages = [
+        pkgs.dejavu_fonts
+      ] ++ optional cfg.withCLI cfg.package;
+
+      variables = {}
+        // optionalAttrs cfg.withCLI {
+          # Make it more convenient to use the `jenkins-cli`.
+          JENKINS_URL = jenkinsUrl;
+        };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "jenkins") {
+      jenkins.gid = config.ids.gids.jenkins;
+    };
+
+    users.users = optionalAttrs (cfg.user == "jenkins") {
+      jenkins = {
+        description = "jenkins user";
+        createHome = true;
+        home = cfg.home;
+        group = cfg.group;
+        extraGroups = cfg.extraGroups;
+        useDefaultShell = true;
+        uid = config.ids.uids.jenkins;
+      };
+    };
+
+    systemd.services.jenkins = {
+      description = "Jenkins Continuous Integration Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment =
+        let
+          selectedSessionVars =
+            lib.filterAttrs (n: v: builtins.elem n [ "NIX_PATH" ])
+              config.environment.sessionVariables;
+        in
+          selectedSessionVars //
+          { JENKINS_HOME = cfg.home;
+            NIX_REMOTE = "daemon";
+          } //
+          cfg.environment;
+
+      path = cfg.packages;
+
+      # Force .war (re)extraction, or else we might run stale Jenkins.
+
+      preStart =
+        let replacePlugins =
+              if cfg.plugins == null
+              then ""
+              else
+                let pluginCmds = lib.attrsets.mapAttrsToList
+                      (n: v: "cp ${v} ${cfg.home}/plugins/${n}.jpi")
+                      cfg.plugins;
+                in ''
+                  rm -r ${cfg.home}/plugins || true
+                  mkdir -p ${cfg.home}/plugins
+                  ${lib.strings.concatStringsSep "\n" pluginCmds}
+                '';
+        in ''
+          rm -rf ${cfg.home}/war
+          ${replacePlugins}
+        '';
+
+      # For reference: https://wiki.jenkins.io/display/JENKINS/JenkinsLinuxStartupScript
+      script = ''
+        ${pkgs.jdk11}/bin/java ${concatStringsSep " " cfg.extraJavaOptions} -jar ${cfg.package}/webapps/jenkins.war --httpListenAddress=${cfg.listenAddress} \
+                                                  --httpPort=${toString cfg.port} \
+                                                  --prefix=${cfg.prefix} \
+                                                  -Djava.awt.headless=true \
+                                                  ${concatStringsSep " " cfg.extraOptions}
+      '';
+
+      postStart = ''
+        until [[ $(${pkgs.curl.bin}/bin/curl -L -s --head -w '\n%{http_code}' ${jenkinsUrl} | tail -n1) =~ ^(200|403)$ ]]; do
+          sleep 1
+        done
+      '';
+
+      serviceConfig = {
+        User = cfg.user;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
new file mode 100644
index 00000000000..3ca1542c18f
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
@@ -0,0 +1,241 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  jenkinsCfg = config.services.jenkins;
+  cfg = config.services.jenkins.jobBuilder;
+
+in {
+  options = {
+    services.jenkins.jobBuilder = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether or not to enable the Jenkins Job Builder (JJB) service. It
+          allows defining jobs for Jenkins in a declarative manner.
+
+          Jobs managed through the Jenkins WebUI (or by other means) are left
+          unchanged.
+
+          Note that it really is declarative configuration; if you remove a
+          previously defined job, the corresponding job directory will be
+          deleted.
+
+          Please see the Jenkins Job Builder documentation for more info:
+          <link xlink:href="http://docs.openstack.org/infra/jenkins-job-builder/">
+          http://docs.openstack.org/infra/jenkins-job-builder/</link>
+        '';
+      };
+
+      accessUser = mkOption {
+        default = "";
+        type = types.str;
+        description = ''
+          User id in Jenkins used to reload config.
+        '';
+      };
+
+      accessToken = mkOption {
+        default = "";
+        type = types.str;
+        description = ''
+          User token in Jenkins used to reload config.
+          WARNING: This token will be world readable in the Nix store. To keep
+          it secret, use the <option>accessTokenFile</option> option instead.
+        '';
+      };
+
+      accessTokenFile = mkOption {
+        default = "";
+        type = types.str;
+        example = "/run/keys/jenkins-job-builder-access-token";
+        description = ''
+          File containing the API token for the <option>accessUser</option>
+          user.
+        '';
+      };
+
+      yamlJobs = mkOption {
+        default = "";
+        type = types.lines;
+        example = ''
+          - job:
+              name: jenkins-job-test-1
+              builders:
+                - shell: echo 'Hello world!'
+        '';
+        description = ''
+          Job descriptions for Jenkins Job Builder in YAML format.
+        '';
+      };
+
+      jsonJobs = mkOption {
+        default = [ ];
+        type = types.listOf types.str;
+        example = literalExpression ''
+          [
+            '''
+              [ { "job":
+                  { "name": "jenkins-job-test-2",
+                    "builders": [ "shell": "echo 'Hello world!'" ]
+                  }
+                }
+              ]
+            '''
+          ]
+        '';
+        description = ''
+          Job descriptions for Jenkins Job Builder in JSON format.
+        '';
+      };
+
+      nixJobs = mkOption {
+        default = [ ];
+        type = types.listOf types.attrs;
+        example = literalExpression ''
+          [ { job =
+              { name = "jenkins-job-test-3";
+                builders = [
+                  { shell = "echo 'Hello world!'"; }
+                ];
+              };
+            }
+          ]
+        '';
+        description = ''
+          Job descriptions for Jenkins Job Builder in Nix format.
+
+          This is a trivial wrapper around jsonJobs, using builtins.toJSON
+          behind the scene.
+        '';
+      };
+    };
+  };
+
+  config = mkIf (jenkinsCfg.enable && cfg.enable) {
+    assertions = [
+      { assertion =
+          if cfg.accessUser != ""
+          then (cfg.accessToken != "" && cfg.accessTokenFile == "") ||
+               (cfg.accessToken == "" && cfg.accessTokenFile != "")
+          else true;
+        message = ''
+          One of accessToken and accessTokenFile options must be non-empty
+          strings, but not both. Current values:
+            services.jenkins.jobBuilder.accessToken = "${cfg.accessToken}"
+            services.jenkins.jobBuilder.accessTokenFile = "${cfg.accessTokenFile}"
+        '';
+      }
+    ];
+
+    systemd.services.jenkins-job-builder = {
+      description = "Jenkins Job Builder Service";
+      # JJB can run either before or after jenkins. We chose after, so we can
+      # always use curl to notify (running) jenkins to reload its config.
+      after = [ "jenkins.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      path = with pkgs; [ jenkins-job-builder curl ];
+
+      # Q: Why manipulate files directly instead of using "jenkins-jobs upload [...]"?
+      # A: Because this module is for administering a local jenkins install,
+      #    and using local file copy allows us to not worry about
+      #    authentication.
+      script =
+        let
+          yamlJobsFile = builtins.toFile "jobs.yaml" cfg.yamlJobs;
+          jsonJobsFiles =
+            map (x: (builtins.toFile "jobs.json" x))
+              (cfg.jsonJobs ++ [(builtins.toJSON cfg.nixJobs)]);
+          jobBuilderOutputDir = "/run/jenkins-job-builder/output";
+          # Stamp file is placed in $JENKINS_HOME/jobs/$JOB_NAME/ to indicate
+          # ownership. Enables tracking and removal of stale jobs.
+          ownerStamp = ".config-xml-managed-by-nixos-jenkins-job-builder";
+          reloadScript = ''
+            echo "Asking Jenkins to reload config"
+            curl_opts="--silent --fail --show-error"
+            access_token=${if cfg.accessTokenFile != ""
+                           then "$(cat '${cfg.accessTokenFile}')"
+                           else cfg.accessToken}
+            jenkins_url="http://${cfg.accessUser}:$access_token@${jenkinsCfg.listenAddress}:${toString jenkinsCfg.port}${jenkinsCfg.prefix}"
+            crumb=$(curl $curl_opts "$jenkins_url"'/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)')
+            curl $curl_opts -X POST -H "$crumb" "$jenkins_url"/reload
+          '';
+        in
+          ''
+            joinByString()
+            {
+                local separator="$1"
+                shift
+                local first="$1"
+                shift
+                printf "%s" "$first" "''${@/#/$separator}"
+            }
+
+            # Map a relative directory path in the output from
+            # jenkins-job-builder (jobname) to the layout expected by jenkins:
+            # each directory level gets prepended "jobs/".
+            getJenkinsJobDir()
+            {
+                IFS='/' read -ra input_dirs <<< "$1"
+                printf "jobs/"
+                joinByString "/jobs/" "''${input_dirs[@]}"
+            }
+
+            # The inverse of getJenkinsJobDir (remove the "jobs/" prefixes)
+            getJobname()
+            {
+                IFS='/' read -ra input_dirs <<< "$1"
+                local i=0
+                local nelem=''${#input_dirs[@]}
+                for e in "''${input_dirs[@]}"; do
+                    if [ $((i % 2)) -eq 1 ]; then
+                        printf "$e"
+                        if [ $i -lt $(( nelem - 1 )) ]; then
+                            printf "/"
+                        fi
+                    fi
+                    i=$((i + 1))
+                done
+            }
+
+            rm -rf ${jobBuilderOutputDir}
+            cur_decl_jobs=/run/jenkins-job-builder/declarative-jobs
+            rm -f "$cur_decl_jobs"
+
+            # Create / update jobs
+            mkdir -p ${jobBuilderOutputDir}
+            for inputFile in ${yamlJobsFile} ${concatStringsSep " " jsonJobsFiles}; do
+                HOME="${jenkinsCfg.home}" "${pkgs.jenkins-job-builder}/bin/jenkins-jobs" --ignore-cache test --config-xml -o "${jobBuilderOutputDir}" "$inputFile"
+            done
+
+            find "${jobBuilderOutputDir}" -type f -name config.xml | while read -r f; do echo "$(dirname "$f")"; done | sort | while read -r dir; do
+                jobname="$(realpath --relative-to="${jobBuilderOutputDir}" "$dir")"
+                jenkinsjobname=$(getJenkinsJobDir "$jobname")
+                jenkinsjobdir="${jenkinsCfg.home}/$jenkinsjobname"
+                echo "Creating / updating job \"$jobname\""
+                mkdir -p "$jenkinsjobdir"
+                touch "$jenkinsjobdir/${ownerStamp}"
+                cp "$dir"/config.xml "$jenkinsjobdir/config.xml"
+                echo "$jenkinsjobname" >> "$cur_decl_jobs"
+            done
+
+            # Remove stale jobs
+            find "${jenkinsCfg.home}" -type f -name "${ownerStamp}" | while read -r f; do echo "$(dirname "$f")"; done | sort --reverse | while read -r dir; do
+                jenkinsjobname="$(realpath --relative-to="${jenkinsCfg.home}" "$dir")"
+                grep --quiet --line-regexp "$jenkinsjobname" "$cur_decl_jobs" 2>/dev/null && continue
+                jobname=$(getJobname "$jenkinsjobname")
+                echo "Deleting stale job \"$jobname\""
+                jobdir="${jenkinsCfg.home}/$jenkinsjobname"
+                rm -rf "$jobdir"
+            done
+          '' + (if cfg.accessUser != "" then reloadScript else "");
+      serviceConfig = {
+        User = jenkinsCfg.user;
+        RuntimeDirectory = "jenkins-job-builder";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/jenkins/slave.nix b/nixos/modules/services/continuous-integration/jenkins/slave.nix
new file mode 100644
index 00000000000..3c0e6f78e74
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/jenkins/slave.nix
@@ -0,0 +1,68 @@
+{ config, lib, ... }:
+with lib;
+let
+  cfg = config.services.jenkinsSlave;
+  masterCfg = config.services.jenkins;
+in {
+  options = {
+    services.jenkinsSlave = {
+      # todo:
+      # * assure the profile of the jenkins user has a JRE and any specified packages. This would
+      # enable ssh slaves.
+      # * Optionally configure the node as a jenkins ad-hoc slave. This would imply configuration
+      # properties for the master node.
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If true the system will be configured to work as a jenkins slave.
+          If the system is also configured to work as a jenkins master then this has no effect.
+          In progress: Currently only assures the jenkins user is configured.
+        '';
+      };
+
+      user = mkOption {
+        default = "jenkins";
+        type = types.str;
+        description = ''
+          User the jenkins slave agent should execute under.
+        '';
+      };
+
+      group = mkOption {
+        default = "jenkins";
+        type = types.str;
+        description = ''
+          If the default slave agent user "jenkins" is configured then this is
+          the primary group of that user.
+        '';
+      };
+
+      home = mkOption {
+        default = "/var/lib/jenkins";
+        type = types.path;
+        description = ''
+          The path to use as JENKINS_HOME. If the default user "jenkins" is configured then
+          this is the home of the "jenkins" user.
+        '';
+      };
+    };
+  };
+
+  config = mkIf (cfg.enable && !masterCfg.enable) {
+    users.groups = optionalAttrs (cfg.group == "jenkins") {
+      jenkins.gid = config.ids.gids.jenkins;
+    };
+
+    users.users = optionalAttrs (cfg.user == "jenkins") {
+      jenkins = {
+        description = "jenkins user";
+        createHome = true;
+        home = cfg.home;
+        group = cfg.group;
+        useDefaultShell = true;
+        uid = config.ids.uids.jenkins;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/databases/aerospike.nix b/nixos/modules/services/databases/aerospike.nix
new file mode 100644
index 00000000000..8109762aea7
--- /dev/null
+++ b/nixos/modules/services/databases/aerospike.nix
@@ -0,0 +1,156 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.aerospike;
+
+  aerospikeConf = pkgs.writeText "aerospike.conf" ''
+    # This stanza must come first.
+    service {
+      user aerospike
+      group aerospike
+      paxos-single-replica-limit 1 # Number of nodes where the replica count is automatically reduced to 1.
+      proto-fd-max 15000
+      work-directory ${cfg.workDir}
+    }
+    logging {
+      console {
+        context any info
+      }
+    }
+    mod-lua {
+      system-path ${cfg.package}/share/udf/lua
+      user-path ${cfg.workDir}/udf/lua
+    }
+    network {
+      ${cfg.networkConfig}
+    }
+    ${cfg.extraConfig}
+  '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.aerospike = {
+      enable = mkEnableOption "Aerospike server";
+
+      package = mkOption {
+        default = pkgs.aerospike;
+        defaultText = literalExpression "pkgs.aerospike";
+        type = types.package;
+        description = "Which Aerospike derivation to use";
+      };
+
+      workDir = mkOption {
+        type = types.str;
+        default = "/var/lib/aerospike";
+        description = "Location where Aerospike stores its files";
+      };
+
+      networkConfig = mkOption {
+        type = types.lines;
+        default = ''
+          service {
+            address any
+            port 3000
+          }
+
+          heartbeat {
+            address any
+            mode mesh
+            port 3002
+            interval 150
+            timeout 10
+          }
+
+          fabric {
+            address any
+            port 3001
+          }
+
+          info {
+            address any
+            port 3003
+          }
+        '';
+        description = "network section of configuration file";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          namespace test {
+            replication-factor 2
+            memory-size 4G
+            default-ttl 30d
+            storage-engine memory
+          }
+        '';
+        description = "Extra configuration";
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.aerospike.enable {
+
+    users.users.aerospike = {
+      name = "aerospike";
+      group = "aerospike";
+      uid = config.ids.uids.aerospike;
+      description = "Aerospike server user";
+    };
+    users.groups.aerospike.gid = config.ids.gids.aerospike;
+
+    systemd.services.aerospike = rec {
+      description = "Aerospike server";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/asd --fgdaemon --config-file ${aerospikeConf}";
+        User = "aerospike";
+        Group = "aerospike";
+        LimitNOFILE = 100000;
+        PermissionsStartOnly = true;
+      };
+
+      preStart = ''
+        if [ $(echo "$(${pkgs.procps}/bin/sysctl -n kernel.shmall) < 4294967296" | ${pkgs.bc}/bin/bc) == "1"  ]; then
+          echo "kernel.shmall too low, setting to 4G pages"
+          ${pkgs.procps}/bin/sysctl -w kernel.shmall=4294967296
+        fi
+        if [ $(echo "$(${pkgs.procps}/bin/sysctl -n kernel.shmmax) < 1073741824" | ${pkgs.bc}/bin/bc) == "1"  ]; then
+          echo "kernel.shmmax too low, setting to 1GB"
+          ${pkgs.procps}/bin/sysctl -w kernel.shmmax=1073741824
+        fi
+        if [ $(echo "$(cat /proc/sys/net/core/rmem_max) < 15728640" | ${pkgs.bc}/bin/bc) == "1" ]; then
+          echo "increasing socket buffer limit (/proc/sys/net/core/rmem_max): $(cat /proc/sys/net/core/rmem_max) -> 15728640"
+          echo 15728640 > /proc/sys/net/core/rmem_max
+        fi
+        if [ $(echo "$(cat /proc/sys/net/core/wmem_max) <  5242880" | ${pkgs.bc}/bin/bc) == "1"  ]; then
+          echo "increasing socket buffer limit (/proc/sys/net/core/wmem_max): $(cat /proc/sys/net/core/wmem_max) -> 5242880"
+          echo  5242880 > /proc/sys/net/core/wmem_max
+        fi
+        install -d -m0700 -o ${serviceConfig.User} -g ${serviceConfig.Group} "${cfg.workDir}"
+        install -d -m0700 -o ${serviceConfig.User} -g ${serviceConfig.Group} "${cfg.workDir}/smd"
+        install -d -m0700 -o ${serviceConfig.User} -g ${serviceConfig.Group} "${cfg.workDir}/udf"
+        install -d -m0700 -o ${serviceConfig.User} -g ${serviceConfig.Group} "${cfg.workDir}/udf/lua"
+      '';
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/databases/cassandra.nix b/nixos/modules/services/databases/cassandra.nix
new file mode 100644
index 00000000000..b36cac35e7c
--- /dev/null
+++ b/nixos/modules/services/databases/cassandra.nix
@@ -0,0 +1,563 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib)
+    concatStringsSep
+    flip
+    literalDocBook
+    literalExpression
+    optionalAttrs
+    optionals
+    recursiveUpdate
+    mkEnableOption
+    mkIf
+    mkOption
+    types
+    versionAtLeast
+    ;
+
+  cfg = config.services.cassandra;
+
+  defaultUser = "cassandra";
+
+  cassandraConfig = flip recursiveUpdate cfg.extraConfig (
+    {
+      commitlog_sync = "batch";
+      commitlog_sync_batch_window_in_ms = 2;
+      start_native_transport = cfg.allowClients;
+      cluster_name = cfg.clusterName;
+      partitioner = "org.apache.cassandra.dht.Murmur3Partitioner";
+      endpoint_snitch = "SimpleSnitch";
+      data_file_directories = [ "${cfg.homeDir}/data" ];
+      commitlog_directory = "${cfg.homeDir}/commitlog";
+      saved_caches_directory = "${cfg.homeDir}/saved_caches";
+    } // optionalAttrs (cfg.seedAddresses != [ ]) {
+      seed_provider = [
+        {
+          class_name = "org.apache.cassandra.locator.SimpleSeedProvider";
+          parameters = [{ seeds = concatStringsSep "," cfg.seedAddresses; }];
+        }
+      ];
+    } // optionalAttrs (versionAtLeast cfg.package.version "3") {
+      hints_directory = "${cfg.homeDir}/hints";
+    }
+  );
+
+  cassandraConfigWithAddresses = cassandraConfig // (
+    if cfg.listenAddress == null
+    then { listen_interface = cfg.listenInterface; }
+    else { listen_address = cfg.listenAddress; }
+  ) // (
+    if cfg.rpcAddress == null
+    then { rpc_interface = cfg.rpcInterface; }
+    else { rpc_address = cfg.rpcAddress; }
+  );
+
+  cassandraEtc = pkgs.stdenv.mkDerivation {
+    name = "cassandra-etc";
+
+    cassandraYaml = builtins.toJSON cassandraConfigWithAddresses;
+    cassandraEnvPkg = "${cfg.package}/conf/cassandra-env.sh";
+    cassandraLogbackConfig = pkgs.writeText "logback.xml" cfg.logbackConfig;
+
+    passAsFile = [ "extraEnvSh" ];
+    inherit (cfg) extraEnvSh;
+
+    buildCommand = ''
+      mkdir -p "$out"
+
+      echo "$cassandraYaml" > "$out/cassandra.yaml"
+      ln -s "$cassandraLogbackConfig" "$out/logback.xml"
+
+      ( cat "$cassandraEnvPkg"
+        echo "# lines from services.cassandra.extraEnvSh: "
+        cat "$extraEnvShPath"
+      ) > "$out/cassandra-env.sh"
+
+      # Delete default JMX Port, otherwise we can't set it using env variable
+      sed -i '/JMX_PORT="7199"/d' "$out/cassandra-env.sh"
+
+      # Delete default password file
+      sed -i '/-Dcom.sun.management.jmxremote.password.file=\/etc\/cassandra\/jmxremote.password/d' "$out/cassandra-env.sh"
+    '';
+  };
+
+  defaultJmxRolesFile =
+    builtins.foldl'
+      (left: right: left + right) ""
+      (map (role: "${role.username} ${role.password}") cfg.jmxRoles);
+
+  fullJvmOptions =
+    cfg.jvmOpts
+    ++ optionals (cfg.jmxRoles != [ ]) [
+      "-Dcom.sun.management.jmxremote.authenticate=true"
+      "-Dcom.sun.management.jmxremote.password.file=${cfg.jmxRolesFile}"
+    ] ++ optionals cfg.remoteJmx [
+      "-Djava.rmi.server.hostname=${cfg.rpcAddress}"
+    ];
+
+in
+{
+  options.services.cassandra = {
+
+    enable = mkEnableOption ''
+      Apache Cassandra – Scalable and highly available database.
+    '';
+
+    clusterName = mkOption {
+      type = types.str;
+      default = "Test Cluster";
+      description = ''
+        The name of the cluster.
+        This setting prevents nodes in one logical cluster from joining
+        another. All nodes in a cluster must have the same value.
+      '';
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = "Run Apache Cassandra under this user.";
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = "Run Apache Cassandra under this group.";
+    };
+
+    homeDir = mkOption {
+      type = types.path;
+      default = "/var/lib/cassandra";
+      description = ''
+        Home directory for Apache Cassandra.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.cassandra;
+      defaultText = literalExpression "pkgs.cassandra";
+      example = literalExpression "pkgs.cassandra_3_11";
+      description = ''
+        The Apache Cassandra package to use.
+      '';
+    };
+
+    jvmOpts = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      description = ''
+        Populate the JVM_OPT environment variable.
+      '';
+    };
+
+    listenAddress = mkOption {
+      type = types.nullOr types.str;
+      default = "127.0.0.1";
+      example = null;
+      description = ''
+        Address or interface to bind to and tell other Cassandra nodes
+        to connect to. You _must_ change this if you want multiple
+        nodes to be able to communicate!
+
+        Set listenAddress OR listenInterface, not both.
+
+        Leaving it blank leaves it up to
+        InetAddress.getLocalHost(). This will always do the Right
+        Thing _if_ the node is properly configured (hostname, name
+        resolution, etc), and the Right Thing is to use the address
+        associated with the hostname (it might not be).
+
+        Setting listen_address to 0.0.0.0 is always wrong.
+      '';
+    };
+
+    listenInterface = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "eth1";
+      description = ''
+        Set listenAddress OR listenInterface, not both. Interfaces
+        must correspond to a single address, IP aliasing is not
+        supported.
+      '';
+    };
+
+    rpcAddress = mkOption {
+      type = types.nullOr types.str;
+      default = "127.0.0.1";
+      example = null;
+      description = ''
+        The address or interface to bind the native transport server to.
+
+        Set rpcAddress OR rpcInterface, not both.
+
+        Leaving rpcAddress blank has the same effect as on
+        listenAddress (i.e. it will be based on the configured hostname
+        of the node).
+
+        Note that unlike listenAddress, you can specify 0.0.0.0, but you
+        must also set extraConfig.broadcast_rpc_address to a value other
+        than 0.0.0.0.
+
+        For security reasons, you should not expose this port to the
+        internet. Firewall it if needed.
+      '';
+    };
+
+    rpcInterface = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "eth1";
+      description = ''
+        Set rpcAddress OR rpcInterface, not both. Interfaces must
+        correspond to a single address, IP aliasing is not supported.
+      '';
+    };
+
+    logbackConfig = mkOption {
+      type = types.lines;
+      default = ''
+        <configuration scan="false">
+          <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+            <encoder>
+              <pattern>%-5level %date{HH:mm:ss,SSS} %msg%n</pattern>
+            </encoder>
+          </appender>
+
+          <root level="INFO">
+            <appender-ref ref="STDOUT" />
+          </root>
+
+          <logger name="com.thinkaurelius.thrift" level="ERROR"/>
+        </configuration>
+      '';
+      description = ''
+        XML logback configuration for cassandra
+      '';
+    };
+
+    seedAddresses = mkOption {
+      type = types.listOf types.str;
+      default = [ "127.0.0.1" ];
+      description = ''
+        The addresses of hosts designated as contact points in the cluster. A
+        joining node contacts one of the nodes in the seeds list to learn the
+        topology of the ring.
+        Set to 127.0.0.1 for a single node cluster.
+      '';
+    };
+
+    allowClients = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Enables or disables the native transport server (CQL binary protocol).
+        This server uses the same address as the <literal>rpcAddress</literal>,
+        but the port it uses is not <literal>rpc_port</literal> but
+        <literal>native_transport_port</literal>. See the official Cassandra
+        docs for more information on these variables and set them using
+        <literal>extraConfig</literal>.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.attrs;
+      default = { };
+      example =
+        {
+          commitlog_sync_batch_window_in_ms = 3;
+        };
+      description = ''
+        Extra options to be merged into cassandra.yaml as nix attribute set.
+      '';
+    };
+
+    extraEnvSh = mkOption {
+      type = types.lines;
+      default = "";
+      example = literalExpression ''"CLASSPATH=$CLASSPATH:''${extraJar}"'';
+      description = ''
+        Extra shell lines to be appended onto cassandra-env.sh.
+      '';
+    };
+
+    fullRepairInterval = mkOption {
+      type = types.nullOr types.str;
+      default = "3w";
+      example = null;
+      description = ''
+        Set the interval how often full repairs are run, i.e.
+        <literal>nodetool repair --full</literal> is executed. See
+        https://cassandra.apache.org/doc/latest/operating/repair.html
+        for more information.
+
+        Set to <literal>null</literal> to disable full repairs.
+      '';
+    };
+
+    fullRepairOptions = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = [ "--partitioner-range" ];
+      description = ''
+        Options passed through to the full repair command.
+      '';
+    };
+
+    incrementalRepairInterval = mkOption {
+      type = types.nullOr types.str;
+      default = "3d";
+      example = null;
+      description = ''
+        Set the interval how often incremental repairs are run, i.e.
+        <literal>nodetool repair</literal> is executed. See
+        https://cassandra.apache.org/doc/latest/operating/repair.html
+        for more information.
+
+        Set to <literal>null</literal> to disable incremental repairs.
+      '';
+    };
+
+    incrementalRepairOptions = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = [ "--partitioner-range" ];
+      description = ''
+        Options passed through to the incremental repair command.
+      '';
+    };
+
+    maxHeapSize = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "4G";
+      description = ''
+        Must be left blank or set together with heapNewSize.
+        If left blank a sensible value for the available amount of RAM and CPU
+        cores is calculated.
+
+        Override to set the amount of memory to allocate to the JVM at
+        start-up. For production use you may wish to adjust this for your
+        environment. MAX_HEAP_SIZE is the total amount of memory dedicated
+        to the Java heap. HEAP_NEWSIZE refers to the size of the young
+        generation.
+
+        The main trade-off for the young generation is that the larger it
+        is, the longer GC pause times will be. The shorter it is, the more
+        expensive GC will be (usually).
+      '';
+    };
+
+    heapNewSize = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "800M";
+      description = ''
+        Must be left blank or set together with heapNewSize.
+        If left blank a sensible value for the available amount of RAM and CPU
+        cores is calculated.
+
+        Override to set the amount of memory to allocate to the JVM at
+        start-up. For production use you may wish to adjust this for your
+        environment. HEAP_NEWSIZE refers to the size of the young
+        generation.
+
+        The main trade-off for the young generation is that the larger it
+        is, the longer GC pause times will be. The shorter it is, the more
+        expensive GC will be (usually).
+
+        The example HEAP_NEWSIZE assumes a modern 8-core+ machine for decent pause
+        times. If in doubt, and if you do not particularly want to tweak, go with
+        100 MB per physical CPU core.
+      '';
+    };
+
+    mallocArenaMax = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      example = 4;
+      description = ''
+        Set this to control the amount of arenas per-thread in glibc.
+      '';
+    };
+
+    remoteJmx = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Cassandra ships with JMX accessible *only* from localhost.
+        To enable remote JMX connections set to true.
+
+        Be sure to also enable authentication and/or TLS.
+        See: https://wiki.apache.org/cassandra/JmxSecurity
+      '';
+    };
+
+    jmxPort = mkOption {
+      type = types.int;
+      default = 7199;
+      description = ''
+        Specifies the default port over which Cassandra will be available for
+        JMX connections.
+        For security reasons, you should not expose this port to the internet.
+        Firewall it if needed.
+      '';
+    };
+
+    jmxRoles = mkOption {
+      default = [ ];
+      description = ''
+        Roles that are allowed to access the JMX (e.g. nodetool)
+        BEWARE: The passwords will be stored world readable in the nix-store.
+                It's recommended to use your own protected file using
+                <literal>jmxRolesFile</literal>
+
+        Doesn't work in versions older than 3.11 because they don't like that
+        it's world readable.
+      '';
+      type = types.listOf (types.submodule {
+        options = {
+          username = mkOption {
+            type = types.str;
+            description = "Username for JMX";
+          };
+          password = mkOption {
+            type = types.str;
+            description = "Password for JMX";
+          };
+        };
+      });
+    };
+
+    jmxRolesFile = mkOption {
+      type = types.nullOr types.path;
+      default =
+        if versionAtLeast cfg.package.version "3.11"
+        then pkgs.writeText "jmx-roles-file" defaultJmxRolesFile
+        else null;
+      defaultText = literalDocBook ''generated configuration file if version is at least 3.11, otherwise <literal>null</literal>'';
+      example = "/var/lib/cassandra/jmx.password";
+      description = ''
+        Specify your own jmx roles file.
+
+        Make sure the permissions forbid "others" from reading the file if
+        you're using Cassandra below version 3.11.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = (cfg.listenAddress == null) != (cfg.listenInterface == null);
+        message = "You have to set either listenAddress or listenInterface";
+      }
+      {
+        assertion = (cfg.rpcAddress == null) != (cfg.rpcInterface == null);
+        message = "You have to set either rpcAddress or rpcInterface";
+      }
+      {
+        assertion = (cfg.maxHeapSize == null) == (cfg.heapNewSize == null);
+        message = "If you set either of maxHeapSize or heapNewSize you have to set both";
+      }
+      {
+        assertion = cfg.remoteJmx -> cfg.jmxRolesFile != null;
+        message = ''
+          If you want JMX available remotely you need to set a password using
+          <literal>jmxRoles</literal> or <literal>jmxRolesFile</literal> if
+          using Cassandra older than v3.11.
+        '';
+      }
+    ];
+    users = mkIf (cfg.user == defaultUser) {
+      users.${defaultUser} = {
+        group = cfg.group;
+        home = cfg.homeDir;
+        createHome = true;
+        uid = config.ids.uids.cassandra;
+        description = "Cassandra service user";
+      };
+      groups.${defaultUser}.gid = config.ids.gids.cassandra;
+    };
+
+    systemd.services.cassandra = {
+      description = "Apache Cassandra service";
+      after = [ "network.target" ];
+      environment = {
+        CASSANDRA_CONF = "${cassandraEtc}";
+        JVM_OPTS = builtins.concatStringsSep " " fullJvmOptions;
+        MAX_HEAP_SIZE = toString cfg.maxHeapSize;
+        HEAP_NEWSIZE = toString cfg.heapNewSize;
+        MALLOC_ARENA_MAX = toString cfg.mallocArenaMax;
+        LOCAL_JMX = if cfg.remoteJmx then "no" else "yes";
+        JMX_PORT = toString cfg.jmxPort;
+      };
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.package}/bin/cassandra -f";
+        SuccessExitStatus = 143;
+      };
+    };
+
+    systemd.services.cassandra-full-repair = {
+      description = "Perform a full repair on this Cassandra node";
+      after = [ "cassandra.service" ];
+      requires = [ "cassandra.service" ];
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart =
+          concatStringsSep " "
+            ([
+              "${cfg.package}/bin/nodetool"
+              "repair"
+              "--full"
+            ] ++ cfg.fullRepairOptions);
+      };
+    };
+
+    systemd.timers.cassandra-full-repair =
+      mkIf (cfg.fullRepairInterval != null) {
+        description = "Schedule full repairs on Cassandra";
+        wantedBy = [ "timers.target" ];
+        timerConfig = {
+          OnBootSec = cfg.fullRepairInterval;
+          OnUnitActiveSec = cfg.fullRepairInterval;
+          Persistent = true;
+        };
+      };
+
+    systemd.services.cassandra-incremental-repair = {
+      description = "Perform an incremental repair on this cassandra node.";
+      after = [ "cassandra.service" ];
+      requires = [ "cassandra.service" ];
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart =
+          concatStringsSep " "
+            ([
+              "${cfg.package}/bin/nodetool"
+              "repair"
+            ] ++ cfg.incrementalRepairOptions);
+      };
+    };
+
+    systemd.timers.cassandra-incremental-repair =
+      mkIf (cfg.incrementalRepairInterval != null) {
+        description = "Schedule incremental repairs on Cassandra";
+        wantedBy = [ "timers.target" ];
+        timerConfig = {
+          OnBootSec = cfg.incrementalRepairInterval;
+          OnUnitActiveSec = cfg.incrementalRepairInterval;
+          Persistent = true;
+        };
+      };
+  };
+
+  meta.maintainers = with lib.maintainers; [ roberth ];
+}
diff --git a/nixos/modules/services/databases/clickhouse.nix b/nixos/modules/services/databases/clickhouse.nix
new file mode 100644
index 00000000000..3a161d56107
--- /dev/null
+++ b/nixos/modules/services/databases/clickhouse.nix
@@ -0,0 +1,78 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.clickhouse;
+in
+with lib;
+{
+
+  ###### interface
+
+  options = {
+
+    services.clickhouse = {
+
+      enable = mkEnableOption "ClickHouse database server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.clickhouse;
+        defaultText = "pkgs.clickhouse";
+        description = ''
+          ClickHouse package to use.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.clickhouse = {
+      name = "clickhouse";
+      uid = config.ids.uids.clickhouse;
+      group = "clickhouse";
+      description = "ClickHouse server user";
+    };
+
+    users.groups.clickhouse.gid = config.ids.gids.clickhouse;
+
+    systemd.services.clickhouse = {
+      description = "ClickHouse server";
+
+      wantedBy = [ "multi-user.target" ];
+
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        User = "clickhouse";
+        Group = "clickhouse";
+        ConfigurationDirectory = "clickhouse-server";
+        AmbientCapabilities = "CAP_SYS_NICE";
+        StateDirectory = "clickhouse";
+        LogsDirectory = "clickhouse";
+        ExecStart = "${cfg.package}/bin/clickhouse-server --config-file=${cfg.package}/etc/clickhouse-server/config.xml";
+      };
+    };
+
+    environment.etc = {
+      "clickhouse-server/config.xml" = {
+        source = "${cfg.package}/etc/clickhouse-server/config.xml";
+      };
+
+      "clickhouse-server/users.xml" = {
+        source = "${cfg.package}/etc/clickhouse-server/users.xml";
+      };
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    # startup requires a `/etc/localtime` which only if exists if `time.timeZone != null`
+    time.timeZone = mkDefault "UTC";
+
+  };
+
+}
diff --git a/nixos/modules/services/databases/cockroachdb.nix b/nixos/modules/services/databases/cockroachdb.nix
new file mode 100644
index 00000000000..eb061af9262
--- /dev/null
+++ b/nixos/modules/services/databases/cockroachdb.nix
@@ -0,0 +1,217 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cockroachdb;
+  crdb = cfg.package;
+
+  escape    = builtins.replaceStrings ["%"] ["%%"];
+  ifNotNull = v: s: optionalString (v != null) s;
+
+  startupCommand = lib.concatStringsSep " "
+    [ # Basic startup
+      "${crdb}/bin/cockroach start"
+      "--logtostderr"
+      "--store=/var/lib/cockroachdb"
+      (ifNotNull cfg.locality "--locality='${cfg.locality}'")
+
+      # WebUI settings
+      "--http-addr='${cfg.http.address}:${toString cfg.http.port}'"
+
+      # Cluster listen address
+      "--listen-addr='${cfg.listen.address}:${toString cfg.listen.port}'"
+
+      # Cluster configuration
+      (ifNotNull cfg.join "--join=${cfg.join}")
+
+      # Cache and memory settings. Must be escaped.
+      "--cache='${escape cfg.cache}'"
+      "--max-sql-memory='${escape cfg.maxSqlMemory}'"
+
+      # Certificate/security settings.
+      (if cfg.insecure then "--insecure" else "--certs-dir=${cfg.certsDir}")
+    ];
+
+    addressOption = descr: defaultPort: {
+      address = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Address to bind to for ${descr}";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = defaultPort;
+        description = "Port to bind to for ${descr}";
+      };
+    };
+in
+
+{
+  options = {
+    services.cockroachdb = {
+      enable = mkEnableOption "CockroachDB Server";
+
+      listen = addressOption "intra-cluster communication" 26257;
+
+      http = addressOption "http-based Admin UI" 8080;
+
+      locality = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          An ordered, comma-separated list of key-value pairs that describe the
+          topography of the machine. Topography might include country,
+          datacenter or rack designations. Data is automatically replicated to
+          maximize diversities of each tier. The order of tiers is used to
+          determine the priority of the diversity, so the more inclusive
+          localities like country should come before less inclusive localities
+          like datacenter.  The tiers and order must be the same on all nodes.
+          Including more tiers is better than including fewer. For example:
+
+          <literal>
+              country=us,region=us-west,datacenter=us-west-1b,rack=12
+              country=ca,region=ca-east,datacenter=ca-east-2,rack=4
+
+              planet=earth,province=manitoba,colo=secondary,power=3
+          </literal>
+        '';
+      };
+
+      join = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "The addresses for connecting the node to a cluster.";
+      };
+
+      insecure = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Run in insecure mode.";
+      };
+
+      certsDir = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = "The path to the certificate directory.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "cockroachdb";
+        description = "User account under which CockroachDB runs";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "cockroachdb";
+        description = "User account under which CockroachDB runs";
+      };
+
+      openPorts = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open firewall ports for cluster communication by default";
+      };
+
+      cache = mkOption {
+        type = types.str;
+        default = "25%";
+        description = ''
+          The total size for caches.
+
+          This can be a percentage, expressed with a fraction sign or as a
+          decimal-point number, or any bytes-based unit. For example,
+          <literal>"25%"</literal>, <literal>"0.25"</literal> both represent
+          25% of the available system memory. The values
+          <literal>"1000000000"</literal> and <literal>"1GB"</literal> both
+          represent 1 gigabyte of memory.
+
+        '';
+      };
+
+      maxSqlMemory = mkOption {
+        type = types.str;
+        default = "25%";
+        description = ''
+          The maximum in-memory storage capacity available to store temporary
+          data for SQL queries.
+
+          This can be a percentage, expressed with a fraction sign or as a
+          decimal-point number, or any bytes-based unit. For example,
+          <literal>"25%"</literal>, <literal>"0.25"</literal> both represent
+          25% of the available system memory. The values
+          <literal>"1000000000"</literal> and <literal>"1GB"</literal> both
+          represent 1 gigabyte of memory.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.cockroachdb;
+        defaultText = literalExpression "pkgs.cockroachdb";
+        description = ''
+          The CockroachDB derivation to use for running the service.
+
+          This would primarily be useful to enable Enterprise Edition features
+          in your own custom CockroachDB build (Nixpkgs CockroachDB binaries
+          only contain open source features and open source code).
+        '';
+      };
+    };
+  };
+
+  config = mkIf config.services.cockroachdb.enable {
+    assertions = [
+      { assertion = !cfg.insecure -> cfg.certsDir != null;
+        message = "CockroachDB must have a set of SSL certificates (.certsDir), or run in Insecure Mode (.insecure = true)";
+      }
+    ];
+
+    environment.systemPackages = [ crdb ];
+
+    users.users = optionalAttrs (cfg.user == "cockroachdb") {
+      cockroachdb = {
+        description = "CockroachDB Server User";
+        uid         = config.ids.uids.cockroachdb;
+        group       = cfg.group;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "cockroachdb") {
+      cockroachdb.gid = config.ids.gids.cockroachdb;
+    };
+
+    networking.firewall.allowedTCPPorts = lib.optionals cfg.openPorts
+      [ cfg.http.port cfg.listen.port ];
+
+    systemd.services.cockroachdb =
+      { description   = "CockroachDB Server";
+        documentation = [ "man:cockroach(1)" "https://www.cockroachlabs.com" ];
+
+        after    = [ "network.target" "time-sync.target" ];
+        requires = [ "time-sync.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        unitConfig.RequiresMountsFor = "/var/lib/cockroachdb";
+
+        serviceConfig =
+          { ExecStart = startupCommand;
+            Type = "notify";
+            User = cfg.user;
+            StateDirectory = "cockroachdb";
+            StateDirectoryMode = "0700";
+
+            Restart = "always";
+
+            # A conservative-ish timeout is alright here, because for Type=notify
+            # cockroach will send systemd pings during startup to keep it alive
+            TimeoutStopSec = 60;
+            RestartSec = 10;
+          };
+      };
+  };
+
+  meta.maintainers = with lib.maintainers; [ thoughtpolice ];
+}
diff --git a/nixos/modules/services/databases/couchdb.nix b/nixos/modules/services/databases/couchdb.nix
new file mode 100644
index 00000000000..742e605d224
--- /dev/null
+++ b/nixos/modules/services/databases/couchdb.nix
@@ -0,0 +1,225 @@
+{ config, options, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.couchdb;
+  opt = options.services.couchdb;
+  configFile = pkgs.writeText "couchdb.ini" (
+    ''
+      [couchdb]
+      database_dir = ${cfg.databaseDir}
+      uri_file = ${cfg.uriFile}
+      view_index_dir = ${cfg.viewIndexDir}
+    '' + (optionalString (cfg.adminPass != null) ''
+      [admins]
+      ${cfg.adminUser} = ${cfg.adminPass}
+    '' + ''
+      [chttpd]
+    '') +
+    ''
+      port = ${toString cfg.port}
+      bind_address = ${cfg.bindAddress}
+
+      [log]
+      file = ${cfg.logFile}
+    '');
+  executable = "${cfg.package}/bin/couchdb";
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.couchdb = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run CouchDB Server.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.couchdb3;
+        defaultText = literalExpression "pkgs.couchdb3";
+        description = ''
+          CouchDB package to use.
+        '';
+      };
+
+      adminUser = mkOption {
+        type = types.str;
+        default = "admin";
+        description = ''
+          Couchdb (i.e. fauxton) account with permission for all dbs and
+          tasks.
+        '';
+      };
+
+      adminPass = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Couchdb (i.e. fauxton) account with permission for all dbs and
+          tasks.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "couchdb";
+        description = ''
+          User account under which couchdb runs.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "couchdb";
+        description = ''
+          Group account under which couchdb runs.
+        '';
+      };
+
+      # couchdb options: http://docs.couchdb.org/en/latest/config/index.html
+
+      databaseDir = mkOption {
+        type = types.path;
+        default = "/var/lib/couchdb";
+        description = ''
+          Specifies location of CouchDB database files (*.couch named). This
+          location should be writable and readable for the user the CouchDB
+          service runs as (couchdb by default).
+        '';
+      };
+
+      uriFile = mkOption {
+        type = types.path;
+        default = "/run/couchdb/couchdb.uri";
+        description = ''
+          This file contains the full URI that can be used to access this
+          instance of CouchDB. It is used to help discover the port CouchDB is
+          running on (if it was set to 0 (e.g. automatically assigned any free
+          one). This file should be writable and readable for the user that
+          runs the CouchDB service (couchdb by default).
+        '';
+      };
+
+      viewIndexDir = mkOption {
+        type = types.path;
+        default = "/var/lib/couchdb";
+        description = ''
+          Specifies location of CouchDB view index files. This location should
+          be writable and readable for the user that runs the CouchDB service
+          (couchdb by default).
+        '';
+      };
+
+      bindAddress = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = ''
+          Defines the IP address by which CouchDB will be accessible.
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 5984;
+        description = ''
+          Defined the port number to listen.
+        '';
+      };
+
+      logFile = mkOption {
+        type = types.path;
+        default = "/var/log/couchdb.log";
+        description = ''
+          Specifies the location of file for logging output.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration. Overrides any other cofiguration.
+        '';
+      };
+
+      argsFile = mkOption {
+        type = types.path;
+        default = "${cfg.package}/etc/vm.args";
+        defaultText = literalExpression ''"config.${opt.package}/etc/vm.args"'';
+        description = ''
+          vm.args configuration. Overrides Couchdb's Erlang VM parameters file.
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.path;
+        description = ''
+          Configuration file for persisting runtime changes. File
+          needs to be readable and writable from couchdb user/group.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf config.services.couchdb.enable {
+
+    environment.systemPackages = [ cfg.package ];
+
+    services.couchdb.configFile = mkDefault "/var/lib/couchdb/local.ini";
+
+    systemd.tmpfiles.rules = [
+      "d '${dirOf cfg.uriFile}' - ${cfg.user} ${cfg.group} - -"
+      "f '${cfg.logFile}' - ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.databaseDir}' -  ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.viewIndexDir}' -  ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.couchdb = {
+      description = "CouchDB Server";
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        touch ${cfg.configFile}
+      '';
+
+      environment = {
+        # we are actually specifying 5 configuration files:
+        # 1. the preinstalled default.ini
+        # 2. the module configuration
+        # 3. the extraConfig from the module options
+        # 4. the locally writable config file, which couchdb itself writes to
+        ERL_FLAGS= ''-couch_ini ${cfg.package}/etc/default.ini ${configFile} ${pkgs.writeText "couchdb-extra.ini" cfg.extraConfig} ${cfg.configFile}'';
+        # 5. the vm.args file
+        COUCHDB_ARGS_FILE=''${cfg.argsFile}'';
+      };
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = executable;
+      };
+    };
+
+    users.users.couchdb = {
+      description = "CouchDB Server user";
+      group = "couchdb";
+      uid = config.ids.uids.couchdb;
+    };
+
+    users.groups.couchdb.gid = config.ids.gids.couchdb;
+
+  };
+}
diff --git a/nixos/modules/services/databases/firebird.nix b/nixos/modules/services/databases/firebird.nix
new file mode 100644
index 00000000000..4e3130bea22
--- /dev/null
+++ b/nixos/modules/services/databases/firebird.nix
@@ -0,0 +1,168 @@
+{ config, lib, pkgs, ... }:
+
+# TODO: This may file may need additional review, eg which configuartions to
+# expose to the user.
+#
+# I only used it to access some simple databases.
+
+# test:
+# isql, then type the following commands:
+# CREATE DATABASE '/var/db/firebird/data/test.fdb' USER 'SYSDBA' PASSWORD 'masterkey';
+# CONNECT '/var/db/firebird/data/test.fdb' USER 'SYSDBA' PASSWORD 'masterkey';
+# CREATE TABLE test ( text varchar(100) );
+# DROP DATABASE;
+#
+# Be careful, virtuoso-opensource also provides a different isql command !
+
+# There are at least two ways to run firebird. superserver has been choosen
+# however there are no strong reasons to prefer this or the other one AFAIK
+# Eg superserver is said to be most efficiently using resources according to
+# http://www.firebirdsql.org/manual/qsg25-classic-or-super.html
+
+with lib;
+
+let
+
+  cfg = config.services.firebird;
+
+  firebird = cfg.package;
+
+  dataDir = "${cfg.baseDir}/data";
+  systemDir = "${cfg.baseDir}/system";
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.firebird = {
+
+      enable = mkEnableOption "the Firebird super server";
+
+      package = mkOption {
+        default = pkgs.firebird;
+        defaultText = literalExpression "pkgs.firebird";
+        type = types.package;
+        example = literalExpression "pkgs.firebird_3";
+        description = ''
+          Which Firebird package to be installed: <code>pkgs.firebird_3</code>
+          For SuperServer use override: <code>pkgs.firebird_3.override { superServer = true; };</code>
+        '';
+      };
+
+      port = mkOption {
+        default = 3050;
+        type = types.port;
+        description = ''
+          Port Firebird uses.
+        '';
+      };
+
+      user = mkOption {
+        default = "firebird";
+        type = types.str;
+        description = ''
+          User account under which firebird runs.
+        '';
+      };
+
+      baseDir = mkOption {
+        default = "/var/lib/firebird";
+        type = types.str;
+        description = ''
+          Location containing data/ and system/ directories.
+          data/ stores the databases, system/ stores the password database security2.fdb.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.firebird.enable {
+
+    environment.systemPackages = [cfg.package];
+
+    systemd.tmpfiles.rules = [
+      "d '${dataDir}' 0700 ${cfg.user} - - -"
+      "d '${systemDir}' 0700 ${cfg.user} - - -"
+    ];
+
+    systemd.services.firebird =
+      { description = "Firebird Super-Server";
+
+        wantedBy = [ "multi-user.target" ];
+
+        # TODO: moving security2.fdb into the data directory works, maybe there
+        # is a better way
+        preStart =
+          ''
+            if ! test -e "${systemDir}/security2.fdb"; then
+                cp ${firebird}/security2.fdb "${systemDir}"
+            fi
+
+            if ! test -e "${systemDir}/security3.fdb"; then
+                cp ${firebird}/security3.fdb "${systemDir}"
+            fi
+
+            if ! test -e "${systemDir}/security4.fdb"; then
+                cp ${firebird}/security4.fdb "${systemDir}"
+            fi
+
+            chmod -R 700         "${dataDir}" "${systemDir}" /var/log/firebird
+          '';
+
+        serviceConfig.User = cfg.user;
+        serviceConfig.LogsDirectory = "firebird";
+        serviceConfig.LogsDirectoryMode = "0700";
+        serviceConfig.ExecStart = "${firebird}/bin/fbserver -d";
+
+        # TODO think about shutdown
+      };
+
+    environment.etc."firebird/firebird.msg".source = "${firebird}/firebird.msg";
+
+    # think about this again - and eventually make it an option
+    environment.etc."firebird/firebird.conf".text = ''
+      # RootDirectory = Restrict ${dataDir}
+      DatabaseAccess = Restrict ${dataDir}
+      ExternalFileAccess = Restrict ${dataDir}
+      # what is this? is None allowed?
+      UdfAccess = None
+      # "Native" =  traditional interbase/firebird, "mixed" is windows only
+      Authentication = Native
+
+      # defaults to -1 on non Win32
+      #MaxUnflushedWrites = 100
+      #MaxUnflushedWriteTime = 100
+
+      # show trace if trouble occurs (does this require debug build?)
+      # BugcheckAbort = 0
+      # ConnectionTimeout = 180
+
+      #RemoteServiceName = gds_db
+      RemoteServicePort = ${cfg.port}
+
+      # randomly choose port for server Event Notification
+      #RemoteAuxPort = 0
+      # rsetrict connections to a network card:
+      #RemoteBindAddress =
+      # there are some additional settings which should be reviewed
+    '';
+
+    users.users.firebird = {
+      description = "Firebird server user";
+      group = "firebird";
+      uid = config.ids.uids.firebird;
+    };
+
+    users.groups.firebird.gid = config.ids.gids.firebird;
+
+  };
+}
diff --git a/nixos/modules/services/databases/foundationdb.nix b/nixos/modules/services/databases/foundationdb.nix
new file mode 100644
index 00000000000..e22127403e9
--- /dev/null
+++ b/nixos/modules/services/databases/foundationdb.nix
@@ -0,0 +1,429 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.foundationdb;
+  pkg = cfg.package;
+
+  # used for initial cluster configuration
+  initialIpAddr = if (cfg.publicAddress != "auto") then cfg.publicAddress else "127.0.0.1";
+
+  fdbServers = n:
+    concatStringsSep "\n" (map (x: "[fdbserver.${toString (x+cfg.listenPortStart)}]") (range 0 (n - 1)));
+
+  backupAgents = n:
+    concatStringsSep "\n" (map (x: "[backup_agent.${toString x}]") (range 1 n));
+
+  configFile = pkgs.writeText "foundationdb.conf" ''
+    [general]
+    cluster_file  = /etc/foundationdb/fdb.cluster
+
+    [fdbmonitor]
+    restart_delay = ${toString cfg.restartDelay}
+    user          = ${cfg.user}
+    group         = ${cfg.group}
+
+    [fdbserver]
+    command        = ${pkg}/bin/fdbserver
+    public_address = ${cfg.publicAddress}:$ID
+    listen_address = ${cfg.listenAddress}
+    datadir        = ${cfg.dataDir}/$ID
+    logdir         = ${cfg.logDir}
+    logsize        = ${cfg.logSize}
+    maxlogssize    = ${cfg.maxLogSize}
+    ${optionalString (cfg.class != null) "class = ${cfg.class}"}
+    memory         = ${cfg.memory}
+    storage_memory = ${cfg.storageMemory}
+
+    ${optionalString (lib.versionAtLeast cfg.package.version "6.1") ''
+    trace_format   = ${cfg.traceFormat}
+    ''}
+
+    ${optionalString (cfg.tls != null) ''
+      tls_plugin           = ${pkg}/libexec/plugins/FDBLibTLS.so
+      tls_certificate_file = ${cfg.tls.certificate}
+      tls_key_file         = ${cfg.tls.key}
+      tls_verify_peers     = ${cfg.tls.allowedPeers}
+    ''}
+
+    ${optionalString (cfg.locality.machineId    != null) "locality_machineid=${cfg.locality.machineId}"}
+    ${optionalString (cfg.locality.zoneId       != null) "locality_zoneid=${cfg.locality.zoneId}"}
+    ${optionalString (cfg.locality.datacenterId != null) "locality_dcid=${cfg.locality.datacenterId}"}
+    ${optionalString (cfg.locality.dataHall     != null) "locality_data_hall=${cfg.locality.dataHall}"}
+
+    ${fdbServers cfg.serverProcesses}
+
+    [backup_agent]
+    command = ${pkg}/libexec/backup_agent
+    ${backupAgents cfg.backupProcesses}
+  '';
+in
+{
+  options.services.foundationdb = {
+
+    enable = mkEnableOption "FoundationDB Server";
+
+    package = mkOption {
+      type        = types.package;
+      description = ''
+        The FoundationDB package to use for this server. This must be specified by the user
+        in order to ensure migrations and upgrades are controlled appropriately.
+      '';
+    };
+
+    publicAddress = mkOption {
+      type        = types.str;
+      default     = "auto";
+      description = "Publicly visible IP address of the process. Port is determined by process ID";
+    };
+
+    listenAddress = mkOption {
+      type        = types.str;
+      default     = "public";
+      description = "Publicly visible IP address of the process. Port is determined by process ID";
+    };
+
+    listenPortStart = mkOption {
+      type          = types.int;
+      default       = 4500;
+      description   = ''
+        Starting port number for database listening sockets. Every FDB process binds to a
+        subsequent port, to this number reflects the start of the overall range. e.g. having
+        8 server processes will use all ports between 4500 and 4507.
+      '';
+    };
+
+    openFirewall = mkOption {
+      type        = types.bool;
+      default     = false;
+      description = ''
+        Open the firewall ports corresponding to FoundationDB processes and coordinators
+        using <option>config.networking.firewall.*</option>.
+      '';
+    };
+
+    dataDir = mkOption {
+      type        = types.path;
+      default     = "/var/lib/foundationdb";
+      description = "Data directory. All cluster data will be put under here.";
+    };
+
+    logDir = mkOption {
+      type        = types.path;
+      default     = "/var/log/foundationdb";
+      description = "Log directory.";
+    };
+
+    user = mkOption {
+      type        = types.str;
+      default     = "foundationdb";
+      description = "User account under which FoundationDB runs.";
+    };
+
+    group = mkOption {
+      type        = types.str;
+      default     = "foundationdb";
+      description = "Group account under which FoundationDB runs.";
+    };
+
+    class = mkOption {
+      type        = types.nullOr (types.enum [ "storage" "transaction" "stateless" ]);
+      default     = null;
+      description = "Process class";
+    };
+
+    restartDelay = mkOption {
+      type = types.int;
+      default = 10;
+      description = "Number of seconds to wait before restarting servers.";
+    };
+
+    logSize = mkOption {
+      type        = types.str;
+      default     = "10MiB";
+      description = ''
+        Roll over to a new log file after the current log file
+        reaches the specified size.
+      '';
+    };
+
+    maxLogSize = mkOption {
+      type        = types.str;
+      default     = "100MiB";
+      description = ''
+        Delete the oldest log file when the total size of all log
+        files exceeds the specified size. If set to 0, old log files
+        will not be deleted.
+      '';
+    };
+
+    serverProcesses = mkOption {
+      type = types.int;
+      default = 1;
+      description = "Number of fdbserver processes to run.";
+    };
+
+    backupProcesses = mkOption {
+      type = types.int;
+      default = 1;
+      description = "Number of backup_agent processes to run for snapshots.";
+    };
+
+    memory = mkOption {
+      type        = types.str;
+      default     = "8GiB";
+      description = ''
+        Maximum memory used by the process. The default value is
+        <literal>8GiB</literal>. When specified without a unit,
+        <literal>MiB</literal> is assumed. This parameter does not
+        change the memory allocation of the program. Rather, it sets
+        a hard limit beyond which the process will kill itself and
+        be restarted. The default value of <literal>8GiB</literal>
+        is double the intended memory usage in the default
+        configuration (providing an emergency buffer to deal with
+        memory leaks or similar problems). It is not recommended to
+        decrease the value of this parameter below its default
+        value. It may be increased if you wish to allocate a very
+        large amount of storage engine memory or cache. In
+        particular, when the <literal>storageMemory</literal>
+        parameter is increased, the <literal>memory</literal>
+        parameter should be increased by an equal amount.
+      '';
+    };
+
+    storageMemory = mkOption {
+      type        = types.str;
+      default     = "1GiB";
+      description = ''
+        Maximum memory used for data storage. The default value is
+        <literal>1GiB</literal>. When specified without a unit,
+        <literal>MB</literal> is assumed. Clusters using the memory
+        storage engine will be restricted to using this amount of
+        memory per process for purposes of data storage. Memory
+        overhead associated with storing the data is counted against
+        this total. If you increase the
+        <literal>storageMemory</literal>, you should also increase
+        the <literal>memory</literal> parameter by the same amount.
+      '';
+    };
+
+    tls = mkOption {
+      default = null;
+      description = ''
+        FoundationDB Transport Security Layer (TLS) settings.
+      '';
+
+      type = types.nullOr (types.submodule ({
+        options = {
+          certificate = mkOption {
+            type = types.str;
+            description = ''
+              Path to the TLS certificate file. This certificate will
+              be offered to, and may be verified by, clients.
+            '';
+          };
+
+          key = mkOption {
+            type = types.str;
+            description = "Private key file for the certificate.";
+          };
+
+          allowedPeers = mkOption {
+            type = types.str;
+            default = "Check.Valid=1,Check.Unexpired=1";
+            description = ''
+              "Peer verification string". This may be used to adjust which TLS
+              client certificates a server will accept, as a form of user
+              authorization; for example, it may only accept TLS clients who
+              offer a certificate abiding by some locality or organization name.
+
+              For more information, please see the FoundationDB documentation.
+            '';
+          };
+        };
+      }));
+    };
+
+    locality = mkOption {
+      default = {
+        machineId    = null;
+        zoneId       = null;
+        datacenterId = null;
+        dataHall     = null;
+      };
+
+      description = ''
+        FoundationDB locality settings.
+      '';
+
+      type = types.submodule ({
+        options = {
+          machineId = mkOption {
+            default = null;
+            type = types.nullOr types.str;
+            description = ''
+              Machine identifier key. All processes on a machine should share a
+              unique id. By default, processes on a machine determine a unique id to share.
+              This does not generally need to be set.
+            '';
+          };
+
+          zoneId = mkOption {
+            default = null;
+            type = types.nullOr types.str;
+            description = ''
+              Zone identifier key. Processes that share a zone id are
+              considered non-unique for the purposes of data replication.
+              If unset, defaults to machine id.
+            '';
+          };
+
+          datacenterId = mkOption {
+            default = null;
+            type = types.nullOr types.str;
+            description = ''
+              Data center identifier key. All processes physically located in a
+              data center should share the id. If you are depending on data
+              center based replication this must be set on all processes.
+            '';
+          };
+
+          dataHall = mkOption {
+            default = null;
+            type = types.nullOr types.str;
+            description = ''
+              Data hall identifier key. All processes physically located in a
+              data hall should share the id. If you are depending on data
+              hall based replication this must be set on all processes.
+            '';
+          };
+        };
+      });
+    };
+
+    extraReadWritePaths = mkOption {
+      default = [ ];
+      type = types.listOf types.path;
+      description = ''
+        An extra set of filesystem paths that FoundationDB can read to
+        and write from. By default, FoundationDB runs under a heavily
+        namespaced systemd environment without write access to most of
+        the filesystem outside of its data and log directories. By
+        adding paths to this list, the set of writeable paths will be
+        expanded. This is useful for allowing e.g. backups to local files,
+        which must be performed on behalf of the foundationdb service.
+      '';
+    };
+
+    pidfile = mkOption {
+      type        = types.path;
+      default     = "/run/foundationdb.pid";
+      description = "Path to pidfile for fdbmonitor.";
+    };
+
+    traceFormat = mkOption {
+      type = types.enum [ "xml" "json" ];
+      default = "xml";
+      description = "Trace logging format.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = lib.versionOlder cfg.package.version "6.1" -> cfg.traceFormat == "xml";
+        message = ''
+          Versions of FoundationDB before 6.1 do not support configurable trace formats (only XML is supported).
+          This option has no effect for version '' + cfg.package.version + '', and enabling it is an error.
+        '';
+      }
+    ];
+
+    environment.systemPackages = [ pkg ];
+
+    users.users = optionalAttrs (cfg.user == "foundationdb") {
+      foundationdb = {
+        description = "FoundationDB User";
+        uid         = config.ids.uids.foundationdb;
+        group       = cfg.group;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "foundationdb") {
+      foundationdb.gid = config.ids.gids.foundationdb;
+    };
+
+    networking.firewall.allowedTCPPortRanges = mkIf cfg.openFirewall
+      [ { from = cfg.listenPortStart;
+          to = (cfg.listenPortStart + cfg.serverProcesses) - 1;
+        }
+      ];
+
+    systemd.tmpfiles.rules = [
+      "d /etc/foundationdb 0755 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.logDir}' 0770 ${cfg.user} ${cfg.group} - -"
+      "F '${cfg.pidfile}' - ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.foundationdb = {
+      description             = "FoundationDB Service";
+
+      after                   = [ "network.target" ];
+      wantedBy                = [ "multi-user.target" ];
+      unitConfig =
+        { RequiresMountsFor = "${cfg.dataDir} ${cfg.logDir}";
+        };
+
+      serviceConfig =
+        let rwpaths = [ cfg.dataDir cfg.logDir cfg.pidfile "/etc/foundationdb" ]
+                   ++ cfg.extraReadWritePaths;
+        in
+        { Type       = "simple";
+          Restart    = "always";
+          RestartSec = 5;
+          User       = cfg.user;
+          Group      = cfg.group;
+          PIDFile    = "${cfg.pidfile}";
+
+          PermissionsStartOnly = true;  # setup needs root perms
+          TimeoutSec           = 120;   # give reasonable time to shut down
+
+          # Security options
+          NoNewPrivileges       = true;
+          ProtectHome           = true;
+          ProtectSystem         = "strict";
+          ProtectKernelTunables = true;
+          ProtectControlGroups  = true;
+          PrivateTmp            = true;
+          PrivateDevices        = true;
+          ReadWritePaths        = lib.concatStringsSep " " (map (x: "-" + x) rwpaths);
+        };
+
+      path = [ pkg pkgs.coreutils ];
+
+      preStart = ''
+        if [ ! -f /etc/foundationdb/fdb.cluster ]; then
+            cf=/etc/foundationdb/fdb.cluster
+            desc=$(tr -dc A-Za-z0-9 </dev/urandom 2>/dev/null | head -c8)
+            rand=$(tr -dc A-Za-z0-9 </dev/urandom 2>/dev/null | head -c8)
+            echo ''${desc}:''${rand}@${initialIpAddr}:${builtins.toString cfg.listenPortStart} > $cf
+            chmod 0664 $cf
+            touch "${cfg.dataDir}/.first_startup"
+        fi
+      '';
+
+      script = "exec fdbmonitor --lockfile ${cfg.pidfile} --conffile ${configFile}";
+
+      postStart = ''
+        if [ -e "${cfg.dataDir}/.first_startup" ]; then
+          fdbcli --exec "configure new single ssd"
+          rm -f "${cfg.dataDir}/.first_startup";
+        fi
+      '';
+    };
+  };
+
+  meta.doc         = ./foundationdb.xml;
+  meta.maintainers = with lib.maintainers; [ thoughtpolice ];
+}
diff --git a/nixos/modules/services/databases/foundationdb.xml b/nixos/modules/services/databases/foundationdb.xml
new file mode 100644
index 00000000000..b0b1ebeab45
--- /dev/null
+++ b/nixos/modules/services/databases/foundationdb.xml
@@ -0,0 +1,443 @@
+<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/hbase.nix b/nixos/modules/services/databases/hbase.nix
new file mode 100644
index 00000000000..fe4f05eec64
--- /dev/null
+++ b/nixos/modules/services/databases/hbase.nix
@@ -0,0 +1,149 @@
+{ config, options, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.hbase;
+  opt = options.services.hbase;
+
+  buildProperty = configAttr:
+    (builtins.concatStringsSep "\n"
+      (lib.mapAttrsToList
+        (name: value: ''
+          <property>
+            <name>${name}</name>
+            <value>${builtins.toString value}</value>
+          </property>
+        '')
+        configAttr));
+
+  configFile = pkgs.writeText "hbase-site.xml"
+    ''<configuration>
+        ${buildProperty (opt.settings.default // cfg.settings)}
+      </configuration>
+    '';
+
+  configDir = pkgs.runCommand "hbase-config-dir" { preferLocalBuild = true; } ''
+    mkdir -p $out
+    cp ${cfg.package}/conf/* $out/
+    rm $out/hbase-site.xml
+    ln -s ${configFile} $out/hbase-site.xml
+  '' ;
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.hbase = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run HBase.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.hbase;
+        defaultText = literalExpression "pkgs.hbase";
+        description = ''
+          HBase package to use.
+        '';
+      };
+
+
+      user = mkOption {
+        type = types.str;
+        default = "hbase";
+        description = ''
+          User account under which HBase runs.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "hbase";
+        description = ''
+          Group account under which HBase runs.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/hbase";
+        description = ''
+          Specifies location of HBase database files. This location should be
+          writable and readable for the user the HBase service runs as
+          (hbase by default).
+        '';
+      };
+
+      logDir = mkOption {
+        type = types.path;
+        default = "/var/log/hbase";
+        description = ''
+          Specifies the location of HBase log files.
+        '';
+      };
+
+      settings = mkOption {
+        type = with lib.types; attrsOf (oneOf [ str int bool ]);
+        default = {
+          "hbase.rootdir" = "file://${cfg.dataDir}/hbase";
+          "hbase.zookeeper.property.dataDir" = "${cfg.dataDir}/zookeeper";
+        };
+        defaultText = literalExpression ''
+          {
+            "hbase.rootdir" = "file://''${config.${opt.dataDir}}/hbase";
+            "hbase.zookeeper.property.dataDir" = "''${config.${opt.dataDir}}/zookeeper";
+          }
+        '';
+        description = ''
+          configurations in hbase-site.xml, see <link xlink:href="https://github.com/apache/hbase/blob/master/hbase-server/src/test/resources/hbase-site.xml"/> for details.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf config.services.hbase.enable {
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.logDir}' - ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.hbase = {
+      description = "HBase Server";
+      wantedBy = [ "multi-user.target" ];
+
+      environment = {
+        # JRE 15 removed option `UseConcMarkSweepGC` which is needed.
+        JAVA_HOME = "${pkgs.jre8}";
+        HBASE_LOG_DIR = cfg.logDir;
+      };
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.package}/bin/hbase --config ${configDir} master start";
+      };
+    };
+
+    users.users.hbase = {
+      description = "HBase Server user";
+      group = "hbase";
+      uid = config.ids.uids.hbase;
+    };
+
+    users.groups.hbase.gid = config.ids.gids.hbase;
+
+  };
+}
diff --git a/nixos/modules/services/databases/influxdb.nix b/nixos/modules/services/databases/influxdb.nix
new file mode 100644
index 00000000000..f7383b2023a
--- /dev/null
+++ b/nixos/modules/services/databases/influxdb.nix
@@ -0,0 +1,197 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.influxdb;
+
+  configOptions = recursiveUpdate {
+    meta = {
+      bind-address = ":8088";
+      commit-timeout = "50ms";
+      dir = "${cfg.dataDir}/meta";
+      election-timeout = "1s";
+      heartbeat-timeout = "1s";
+      hostname = "localhost";
+      leader-lease-timeout = "500ms";
+      retention-autocreate = true;
+    };
+
+    data = {
+      dir = "${cfg.dataDir}/data";
+      wal-dir = "${cfg.dataDir}/wal";
+      max-wal-size = 104857600;
+      wal-enable-logging = true;
+      wal-flush-interval = "10m";
+      wal-partition-flush-delay = "2s";
+    };
+
+    cluster = {
+      shard-writer-timeout = "5s";
+      write-timeout = "5s";
+    };
+
+    retention = {
+      enabled = true;
+      check-interval = "30m";
+    };
+
+    http = {
+      enabled = true;
+      auth-enabled = false;
+      bind-address = ":8086";
+      https-enabled = false;
+      log-enabled = true;
+      pprof-enabled = false;
+      write-tracing = false;
+    };
+
+    monitor = {
+      store-enabled = false;
+      store-database = "_internal";
+      store-interval = "10s";
+    };
+
+    admin = {
+      enabled = true;
+      bind-address = ":8083";
+      https-enabled = false;
+    };
+
+    graphite = [{
+      enabled = false;
+    }];
+
+    udp = [{
+      enabled = false;
+    }];
+
+    collectd = [{
+      enabled = false;
+      typesdb = "${pkgs.collectd-data}/share/collectd/types.db";
+      database = "collectd_db";
+      bind-address = ":25826";
+    }];
+
+    opentsdb = [{
+      enabled = false;
+    }];
+
+    continuous_queries = {
+      enabled = true;
+      log-enabled = true;
+      recompute-previous-n = 2;
+      recompute-no-older-than = "10m";
+      compute-runs-per-interval = 10;
+      compute-no-more-than = "2m";
+    };
+
+    hinted-handoff = {
+      enabled = true;
+      dir = "${cfg.dataDir}/hh";
+      max-size = 1073741824;
+      max-age = "168h";
+      retry-rate-limit = 0;
+      retry-interval = "1s";
+    };
+  } cfg.extraConfig;
+
+  configFile = pkgs.runCommandLocal "config.toml" {
+    nativeBuildInputs = [ pkgs.remarshal ];
+  } ''
+    remarshal -if json -of toml \
+      < ${pkgs.writeText "config.json" (builtins.toJSON configOptions)} \
+      > $out
+  '';
+in
+{
+
+  ###### interface
+
+  options = {
+
+    services.influxdb = {
+
+      enable = mkOption {
+        default = false;
+        description = "Whether to enable the influxdb server";
+        type = types.bool;
+      };
+
+      package = mkOption {
+        default = pkgs.influxdb;
+        defaultText = literalExpression "pkgs.influxdb";
+        description = "Which influxdb derivation to use";
+        type = types.package;
+      };
+
+      user = mkOption {
+        default = "influxdb";
+        description = "User account under which influxdb runs";
+        type = types.str;
+      };
+
+      group = mkOption {
+        default = "influxdb";
+        description = "Group under which influxdb runs";
+        type = types.str;
+      };
+
+      dataDir = mkOption {
+        default = "/var/db/influxdb";
+        description = "Data directory for influxd data files.";
+        type = types.path;
+      };
+
+      extraConfig = mkOption {
+        default = {};
+        description = "Extra configuration options for influxdb";
+        type = types.attrs;
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.influxdb.enable {
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.influxdb = {
+      description = "InfluxDB Server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = ''${cfg.package}/bin/influxd -config "${configFile}"'';
+        User = cfg.user;
+        Group = cfg.group;
+      };
+      postStart =
+        let
+          scheme = if configOptions.http.https-enabled then "-k https" else "http";
+          bindAddr = (ba: if hasPrefix ":" ba then "127.0.0.1${ba}" else "${ba}")(toString configOptions.http.bind-address);
+        in
+        mkBefore ''
+          until ${pkgs.curl.bin}/bin/curl -s -o /dev/null ${scheme}://${bindAddr}/ping; do
+            sleep 1;
+          done
+        '';
+    };
+
+    users.users = optionalAttrs (cfg.user == "influxdb") {
+      influxdb = {
+        uid = config.ids.uids.influxdb;
+        group = "influxdb";
+        description = "Influxdb daemon user";
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "influxdb") {
+      influxdb.gid = config.ids.gids.influxdb;
+    };
+  };
+
+}
diff --git a/nixos/modules/services/databases/influxdb2.nix b/nixos/modules/services/databases/influxdb2.nix
new file mode 100644
index 00000000000..340c515bbb4
--- /dev/null
+++ b/nixos/modules/services/databases/influxdb2.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  format = pkgs.formats.json { };
+  cfg = config.services.influxdb2;
+  configFile = format.generate "config.json" cfg.settings;
+in
+{
+  options = {
+    services.influxdb2 = {
+      enable = mkEnableOption "the influxdb2 server";
+
+      package = mkOption {
+        default = pkgs.influxdb2-server;
+        defaultText = literalExpression "pkgs.influxdb2";
+        description = "influxdb2 derivation to use.";
+        type = types.package;
+      };
+
+      settings = mkOption {
+        default = { };
+        description = ''configuration options for influxdb2, see <link xlink:href="https://docs.influxdata.com/influxdb/v2.0/reference/config-options"/> for details.'';
+        type = format.type;
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [{
+      assertion = !(builtins.hasAttr "bolt-path" cfg.settings) && !(builtins.hasAttr "engine-path" cfg.settings);
+      message = "services.influxdb2.config: bolt-path and engine-path should not be set as they are managed by systemd";
+    }];
+
+    systemd.services.influxdb2 = {
+      description = "InfluxDB is an open-source, distributed, time series database";
+      documentation = [ "https://docs.influxdata.com/influxdb/" ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      environment = {
+        INFLUXD_CONFIG_PATH = configFile;
+      };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/influxd --bolt-path \${STATE_DIRECTORY}/influxd.bolt --engine-path \${STATE_DIRECTORY}/engine";
+        StateDirectory = "influxdb2";
+        User = "influxdb2";
+        Group = "influxdb2";
+        CapabilityBoundingSet = "";
+        SystemCallFilter = "@system-service";
+        LimitNOFILE = 65536;
+        KillMode = "control-group";
+        Restart = "on-failure";
+      };
+    };
+
+    users.extraUsers.influxdb2 = {
+      isSystemUser = true;
+      group = "influxdb2";
+    };
+
+    users.extraGroups.influxdb2 = {};
+  };
+
+  meta.maintainers = with lib.maintainers; [ nickcao ];
+}
diff --git a/nixos/modules/services/databases/memcached.nix b/nixos/modules/services/databases/memcached.nix
new file mode 100644
index 00000000000..1c06937e2f3
--- /dev/null
+++ b/nixos/modules/services/databases/memcached.nix
@@ -0,0 +1,118 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.memcached;
+
+  memcached = pkgs.memcached;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.memcached = {
+      enable = mkEnableOption "Memcached";
+
+      user = mkOption {
+        type = types.str;
+        default = "memcached";
+        description = "The user to run Memcached as";
+      };
+
+      listen = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "The IP address to bind to.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 11211;
+        description = "The port to bind to.";
+      };
+
+      enableUnixSocket = mkEnableOption "unix socket at /run/memcached/memcached.sock";
+
+      maxMemory = mkOption {
+        type = types.ints.unsigned;
+        default = 64;
+        description = "The maximum amount of memory to use for storage, in megabytes.";
+      };
+
+      maxConnections = mkOption {
+        type = types.ints.unsigned;
+        default = 1024;
+        description = "The maximum number of simultaneous connections.";
+      };
+
+      extraOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "A list of extra options that will be added as a suffix when running memcached.";
+      };
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf config.services.memcached.enable {
+
+    users.users = optionalAttrs (cfg.user == "memcached") {
+      memcached.description = "Memcached server user";
+      memcached.isSystemUser = true;
+      memcached.group = "memcached";
+    };
+    users.groups = optionalAttrs (cfg.user == "memcached") { memcached = {}; };
+
+    environment.systemPackages = [ memcached ];
+
+    systemd.services.memcached = {
+      description = "Memcached server";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        ExecStart =
+        let
+          networking = if cfg.enableUnixSocket
+          then "-s /run/memcached/memcached.sock"
+          else "-l ${cfg.listen} -p ${toString cfg.port}";
+        in "${memcached}/bin/memcached ${networking} -m ${toString cfg.maxMemory} -c ${toString cfg.maxConnections} ${concatStringsSep " " cfg.extraOptions}";
+
+        User = cfg.user;
+
+        # Filesystem access
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        RuntimeDirectory = "memcached";
+        # Caps
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        # Misc.
+        LockPersonality = true;
+        RestrictRealtime = true;
+        PrivateMounts = true;
+        MemoryDenyWriteExecute = true;
+      };
+    };
+  };
+  imports = [
+    (mkRemovedOptionModule ["services" "memcached" "socket"] ''
+      This option was replaced by a fixed unix socket path at /run/memcached/memcached.sock enabled using services.memcached.enableUnixSocket.
+    '')
+  ];
+
+}
diff --git a/nixos/modules/services/databases/monetdb.nix b/nixos/modules/services/databases/monetdb.nix
new file mode 100644
index 00000000000..52a2ef041f8
--- /dev/null
+++ b/nixos/modules/services/databases/monetdb.nix
@@ -0,0 +1,100 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.monetdb;
+
+in {
+  meta.maintainers = with maintainers; [ StillerHarpo primeos ];
+
+  ###### interface
+  options = {
+    services.monetdb = {
+
+      enable = mkEnableOption "the MonetDB database server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.monetdb;
+        defaultText = literalExpression "pkgs.monetdb";
+        description = "MonetDB package to use.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "monetdb";
+        description = "User account under which MonetDB runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "monetdb";
+        description = "Group under which MonetDB runs.";
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/monetdb";
+        description = "Data directory for the dbfarm.";
+      };
+
+      port = mkOption {
+        type = types.ints.u16;
+        default = 50000;
+        description = "Port to listen on.";
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        example = "0.0.0.0";
+        description = "Address to listen on.";
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+
+    users.users.monetdb = mkIf (cfg.user == "monetdb") {
+      uid = config.ids.uids.monetdb;
+      group = cfg.group;
+      description = "MonetDB user";
+      home = cfg.dataDir;
+      createHome = true;
+    };
+
+    users.groups.monetdb = mkIf (cfg.group == "monetdb") {
+      gid = config.ids.gids.monetdb;
+      members = [ cfg.user ];
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.monetdb = {
+      description = "MonetDB database server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = [ cfg.package ];
+      unitConfig.RequiresMountsFor = "${cfg.dataDir}";
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.package}/bin/monetdbd start -n ${cfg.dataDir}";
+        ExecStop = "${cfg.package}/bin/monetdbd stop ${cfg.dataDir}";
+      };
+      preStart = ''
+        if [ ! -e ${cfg.dataDir}/.merovingian_properties ]; then
+          # Create the dbfarm (as cfg.user)
+          ${cfg.package}/bin/monetdbd create ${cfg.dataDir}
+        fi
+
+        # Update the properties
+        ${cfg.package}/bin/monetdbd set port=${toString cfg.port} ${cfg.dataDir}
+        ${cfg.package}/bin/monetdbd set listenaddr=${cfg.listenAddress} ${cfg.dataDir}
+      '';
+    };
+
+  };
+}
diff --git a/nixos/modules/services/databases/mongodb.nix b/nixos/modules/services/databases/mongodb.nix
new file mode 100644
index 00000000000..fccf85d482e
--- /dev/null
+++ b/nixos/modules/services/databases/mongodb.nix
@@ -0,0 +1,197 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.mongodb;
+
+  mongodb = cfg.package;
+
+  mongoCnf = cfg: pkgs.writeText "mongodb.conf"
+  ''
+    net.bindIp: ${cfg.bind_ip}
+    ${optionalString cfg.quiet "systemLog.quiet: true"}
+    systemLog.destination: syslog
+    storage.dbPath: ${cfg.dbpath}
+    ${optionalString cfg.enableAuth "security.authorization: enabled"}
+    ${optionalString (cfg.replSetName != "") "replication.replSetName: ${cfg.replSetName}"}
+    ${cfg.extraConfig}
+  '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.mongodb = {
+
+      enable = mkEnableOption "the MongoDB server";
+
+      package = mkOption {
+        default = pkgs.mongodb;
+        defaultText = literalExpression "pkgs.mongodb";
+        type = types.package;
+        description = "
+          Which MongoDB derivation to use.
+        ";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "mongodb";
+        description = "User account under which MongoDB runs";
+      };
+
+      bind_ip = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "IP to bind to";
+      };
+
+      quiet = mkOption {
+        type = types.bool;
+        default = false;
+        description = "quieter output";
+      };
+
+      enableAuth = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable client authentication. Creates a default superuser with username root!";
+      };
+
+      initialRootPassword = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Password for the root user if auth is enabled.";
+      };
+
+      dbpath = mkOption {
+        type = types.str;
+        default = "/var/db/mongodb";
+        description = "Location where MongoDB stores its files";
+      };
+
+      pidFile = mkOption {
+        type = types.str;
+        default = "/run/mongodb.pid";
+        description = "Location of MongoDB pid file";
+      };
+
+      replSetName = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          If this instance is part of a replica set, set its name here.
+          Otherwise, leave empty to run as single node.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          storage.journal.enabled: false
+        '';
+        description = "MongoDB extra configuration in YAML format";
+      };
+
+      initialScript = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          A file containing MongoDB statements to execute on first startup.
+        '';
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.mongodb.enable {
+    assertions = [
+      { assertion = !cfg.enableAuth || cfg.initialRootPassword != null;
+        message = "`enableAuth` requires `initialRootPassword` to be set.";
+      }
+    ];
+
+    users.users.mongodb = mkIf (cfg.user == "mongodb")
+      { name = "mongodb";
+        isSystemUser = true;
+        group = "mongodb";
+        description = "MongoDB server user";
+      };
+    users.groups.mongodb = mkIf (cfg.user == "mongodb") {};
+
+    environment.systemPackages = [ mongodb ];
+
+    systemd.services.mongodb =
+      { description = "MongoDB server";
+
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+
+        serviceConfig = {
+          ExecStart = "${mongodb}/bin/mongod --config ${mongoCnf cfg} --fork --pidfilepath ${cfg.pidFile}";
+          User = cfg.user;
+          PIDFile = cfg.pidFile;
+          Type = "forking";
+          TimeoutStartSec=120; # intial creating of journal can take some time
+          PermissionsStartOnly = true;
+        };
+
+        preStart = let
+          cfg_ = cfg // { enableAuth = false; bind_ip = "127.0.0.1"; };
+        in ''
+          rm ${cfg.dbpath}/mongod.lock || true
+          if ! test -e ${cfg.dbpath}; then
+              install -d -m0700 -o ${cfg.user} ${cfg.dbpath}
+              # See postStart!
+              touch ${cfg.dbpath}/.first_startup
+          fi
+          if ! test -e ${cfg.pidFile}; then
+              install -D -o ${cfg.user} /dev/null ${cfg.pidFile}
+          fi '' + lib.optionalString cfg.enableAuth ''
+
+          if ! test -e "${cfg.dbpath}/.auth_setup_complete"; then
+            systemd-run --unit=mongodb-for-setup --uid=${cfg.user} ${mongodb}/bin/mongod --config ${mongoCnf cfg_}
+            # wait for mongodb
+            while ! ${mongodb}/bin/mongo --eval "db.version()" > /dev/null 2>&1; do sleep 0.1; done
+
+          ${mongodb}/bin/mongo <<EOF
+            use admin
+            db.createUser(
+              {
+                user: "root",
+                pwd: "${cfg.initialRootPassword}",
+                roles: [
+                  { role: "userAdminAnyDatabase", db: "admin" },
+                  { role: "dbAdminAnyDatabase", db: "admin" },
+                  { role: "readWriteAnyDatabase", db: "admin" }
+                ]
+              }
+            )
+          EOF
+            touch "${cfg.dbpath}/.auth_setup_complete"
+            systemctl stop mongodb-for-setup
+          fi
+        '';
+        postStart = ''
+            if test -e "${cfg.dbpath}/.first_startup"; then
+              ${optionalString (cfg.initialScript != null) ''
+                ${mongodb}/bin/mongo ${optionalString (cfg.enableAuth) "-u root -p ${cfg.initialRootPassword}"} admin "${cfg.initialScript}"
+              ''}
+              rm -f "${cfg.dbpath}/.first_startup"
+            fi
+        '';
+      };
+
+  };
+
+}
diff --git a/nixos/modules/services/databases/mysql.nix b/nixos/modules/services/databases/mysql.nix
new file mode 100644
index 00000000000..625b31d081c
--- /dev/null
+++ b/nixos/modules/services/databases/mysql.nix
@@ -0,0 +1,521 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.mysql;
+
+  isMariaDB = lib.getName cfg.package == lib.getName pkgs.mariadb;
+
+  mysqldOptions =
+    "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${cfg.package}";
+
+  format = pkgs.formats.ini { listsAsDuplicateKeys = true; };
+  configFile = format.generate "my.cnf" cfg.settings;
+
+in
+
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "mysql" "pidDir" ] "Don't wait for pidfiles, describe dependencies through systemd.")
+    (mkRemovedOptionModule [ "services" "mysql" "rootPassword" ] "Use socket authentication or set the password outside of the nix store.")
+    (mkRemovedOptionModule [ "services" "mysql" "extraOptions" ] "Use services.mysql.settings.mysqld instead.")
+    (mkRemovedOptionModule [ "services" "mysql" "bind" ] "Use services.mysql.settings.mysqld.bind-address instead.")
+    (mkRemovedOptionModule [ "services" "mysql" "port" ] "Use services.mysql.settings.mysqld.port instead.")
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.mysql = {
+
+      enable = mkEnableOption "MySQL server";
+
+      package = mkOption {
+        type = types.package;
+        example = literalExpression "pkgs.mariadb";
+        description = "
+          Which MySQL derivation to use. MariaDB packages are supported too.
+        ";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "mysql";
+        description = ''
+          User account under which MySQL runs.
+
+          <note><para>
+          If left as the default value this user will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the user exists before the MySQL service starts.
+          </para></note>
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "mysql";
+        description = ''
+          Group account under which MySQL runs.
+
+          <note><para>
+          If left as the default value this group will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the user exists before the MySQL service starts.
+          </para></note>
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        example = "/var/lib/mysql";
+        description = ''
+          The data directory for MySQL.
+
+          <note><para>
+          If left as the default value of <literal>/var/lib/mysql</literal> this directory will automatically be created before the MySQL
+          server starts, otherwise you are responsible for ensuring the directory exists with appropriate ownership and permissions.
+          </para></note>
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.path;
+        default = configFile;
+        defaultText = ''
+          A configuration file automatically generated by NixOS.
+        '';
+        description = ''
+          Override the configuration file used by MySQL. By default,
+          NixOS generates one automatically from <option>services.mysql.settings</option>.
+        '';
+        example = literalExpression ''
+          pkgs.writeText "my.cnf" '''
+            [mysqld]
+            datadir = /var/lib/mysql
+            bind-address = 127.0.0.1
+            port = 3336
+
+            !includedir /etc/mysql/conf.d/
+          ''';
+        '';
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = {};
+        description = ''
+          MySQL configuration. Refer to
+          <link xlink:href="https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html"/>,
+          <link xlink:href="https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html"/>,
+          and <link xlink:href="https://mariadb.com/kb/en/server-system-variables/"/>
+          for details on supported values.
+
+          <note>
+            <para>
+              MySQL configuration options such as <literal>--quick</literal> should be treated as
+              boolean options and provided values such as <literal>true</literal>, <literal>false</literal>,
+              <literal>1</literal>, or <literal>0</literal>. See the provided example below.
+            </para>
+          </note>
+        '';
+        example = literalExpression ''
+          {
+            mysqld = {
+              key_buffer_size = "6G";
+              table_cache = 1600;
+              log-error = "/var/log/mysql_err.log";
+              plugin-load-add = [ "server_audit" "ed25519=auth_ed25519" ];
+            };
+            mysqldump = {
+              quick = true;
+              max_allowed_packet = "16M";
+            };
+          }
+        '';
+      };
+
+      initialDatabases = mkOption {
+        type = types.listOf (types.submodule {
+          options = {
+            name = mkOption {
+              type = types.str;
+              description = ''
+                The name of the database to create.
+              '';
+            };
+            schema = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = ''
+                The initial schema of the database; if null (the default),
+                an empty database is created.
+              '';
+            };
+          };
+        });
+        default = [];
+        description = ''
+          List of database names and their initial schemas that should be used to create databases on the first startup
+          of MySQL. The schema attribute is optional: If not specified, an empty database is created.
+        '';
+        example = [
+          { name = "foodatabase"; schema = literalExpression "./foodatabase.sql"; }
+          { name = "bardatabase"; }
+        ];
+      };
+
+      initialScript = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = "A file containing SQL statements to be executed on the first startup. Can be used for granting certain permissions on the database.";
+      };
+
+      ensureDatabases = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Ensures that the specified databases exist.
+          This option will never delete existing databases, especially not when the value of this
+          option is changed. This means that databases created once through this option or
+          otherwise have to be removed manually.
+        '';
+        example = [
+          "nextcloud"
+          "matomo"
+        ];
+      };
+
+      ensureUsers = mkOption {
+        type = types.listOf (types.submodule {
+          options = {
+            name = mkOption {
+              type = types.str;
+              description = ''
+                Name of the user to ensure.
+              '';
+            };
+            ensurePermissions = mkOption {
+              type = types.attrsOf types.str;
+              default = {};
+              description = ''
+                Permissions to ensure for the user, specified as attribute set.
+                The attribute names specify the database and tables to grant the permissions for,
+                separated by a dot. You may use wildcards here.
+                The attribute values specfiy the permissions to grant.
+                You may specify one or multiple comma-separated SQL privileges here.
+
+                For more information on how to specify the target
+                and on which privileges exist, see the
+                <link xlink:href="https://mariadb.com/kb/en/library/grant/">GRANT syntax</link>.
+                The attributes are used as <code>GRANT ''${attrName} ON ''${attrValue}</code>.
+              '';
+              example = literalExpression ''
+                {
+                  "database.*" = "ALL PRIVILEGES";
+                  "*.*" = "SELECT, LOCK TABLES";
+                }
+              '';
+            };
+          };
+        });
+        default = [];
+        description = ''
+          Ensures that the specified users exist and have at least the ensured permissions.
+          The MySQL users will be identified using Unix socket authentication. This authenticates the Unix user with the
+          same name only, and that without the need for a password.
+          This option will never delete existing users or remove permissions, especially not when the value of this
+          option is changed. This means that users created and permissions assigned once through this option or
+          otherwise have to be removed manually.
+        '';
+        example = literalExpression ''
+          [
+            {
+              name = "nextcloud";
+              ensurePermissions = {
+                "nextcloud.*" = "ALL PRIVILEGES";
+              };
+            }
+            {
+              name = "backup";
+              ensurePermissions = {
+                "*.*" = "SELECT, LOCK TABLES";
+              };
+            }
+          ]
+        '';
+      };
+
+      replication = {
+        role = mkOption {
+          type = types.enum [ "master" "slave" "none" ];
+          default = "none";
+          description = "Role of the MySQL server instance.";
+        };
+
+        serverId = mkOption {
+          type = types.int;
+          default = 1;
+          description = "Id of the MySQL server instance. This number must be unique for each instance.";
+        };
+
+        masterHost = mkOption {
+          type = types.str;
+          description = "Hostname of the MySQL master server.";
+        };
+
+        slaveHost = mkOption {
+          type = types.str;
+          description = "Hostname of the MySQL slave server.";
+        };
+
+        masterUser = mkOption {
+          type = types.str;
+          description = "Username of the MySQL replication user.";
+        };
+
+        masterPassword = mkOption {
+          type = types.str;
+          description = "Password of the MySQL replication user.";
+        };
+
+        masterPort = mkOption {
+          type = types.port;
+          default = 3306;
+          description = "Port number on which the MySQL master server runs.";
+        };
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.mysql.dataDir =
+      mkDefault (if versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql"
+                 else "/var/mysql");
+
+    services.mysql.settings.mysqld = mkMerge [
+      {
+        datadir = cfg.dataDir;
+        port = mkDefault 3306;
+      }
+      (mkIf (cfg.replication.role == "master" || cfg.replication.role == "slave") {
+        log-bin = "mysql-bin-${toString cfg.replication.serverId}";
+        log-bin-index = "mysql-bin-${toString cfg.replication.serverId}.index";
+        relay-log = "mysql-relay-bin";
+        server-id = cfg.replication.serverId;
+        binlog-ignore-db = [ "information_schema" "performance_schema" "mysql" ];
+      })
+      (mkIf (!isMariaDB) {
+        plugin-load-add = "auth_socket.so";
+      })
+    ];
+
+    users.users = optionalAttrs (cfg.user == "mysql") {
+      mysql = {
+        description = "MySQL server user";
+        group = cfg.group;
+        uid = config.ids.uids.mysql;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "mysql") {
+      mysql.gid = config.ids.gids.mysql;
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    environment.etc."my.cnf".source = cfg.configFile;
+
+    systemd.services.mysql = {
+      description = "MySQL Server";
+
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ cfg.configFile ];
+
+      unitConfig.RequiresMountsFor = cfg.dataDir;
+
+      path = [
+        # Needed for the mysql_install_db command in the preStart script
+        # which calls the hostname command.
+        pkgs.nettools
+      ];
+
+      preStart = if isMariaDB then ''
+        if ! test -e ${cfg.dataDir}/mysql; then
+          ${cfg.package}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions}
+          touch ${cfg.dataDir}/mysql_init
+        fi
+      '' else ''
+        if ! test -e ${cfg.dataDir}/mysql; then
+          ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure
+          touch ${cfg.dataDir}/mysql_init
+        fi
+      '';
+
+      script = ''
+        # https://mariadb.com/kb/en/getting-started-with-mariadb-galera-cluster/#systemd-and-galera-recovery
+        if test -n "''${_WSREP_START_POSITION}"; then
+          if test -e "${cfg.package}/bin/galera_recovery"; then
+            VAR=$(cd ${cfg.package}/bin/..; ${cfg.package}/bin/galera_recovery); [[ $? -eq 0 ]] && export _WSREP_START_POSITION=$VAR || exit 1
+          fi
+        fi
+
+        # The last two environment variables are used for starting Galera clusters
+        exec ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION
+      '';
+
+      postStart = let
+        # The super user account to use on *first* run of MySQL server
+        superUser = if isMariaDB then cfg.user else "root";
+      in ''
+        ${optionalString (!isMariaDB) ''
+          # Wait until the MySQL server is available for use
+          count=0
+          while [ ! -e /run/mysqld/mysqld.sock ]
+          do
+              if [ $count -eq 30 ]
+              then
+                  echo "Tried 30 times, giving up..."
+                  exit 1
+              fi
+
+              echo "MySQL daemon not yet started. Waiting for 1 second..."
+              count=$((count++))
+              sleep 1
+          done
+        ''}
+
+        if [ -f ${cfg.dataDir}/mysql_init ]
+        then
+            # While MariaDB comes with a 'mysql' super user account since 10.4.x, MySQL does not
+            # Since we don't want to run this service as 'root' we need to ensure the account exists on first run
+            ( echo "CREATE USER IF NOT EXISTS '${cfg.user}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};"
+              echo "GRANT ALL PRIVILEGES ON *.* TO '${cfg.user}'@'localhost' WITH GRANT OPTION;"
+            ) | ${cfg.package}/bin/mysql -u ${superUser} -N
+
+            ${concatMapStrings (database: ''
+              # Create initial databases
+              if ! test -e "${cfg.dataDir}/${database.name}"; then
+                  echo "Creating initial database: ${database.name}"
+                  ( echo 'create database `${database.name}`;'
+
+                    ${optionalString (database.schema != null) ''
+                    echo 'use `${database.name}`;'
+
+                    # TODO: this silently falls through if database.schema does not exist,
+                    # we should catch this somehow and exit, but can't do it here because we're in a subshell.
+                    if [ -f "${database.schema}" ]
+                    then
+                        cat ${database.schema}
+                    elif [ -d "${database.schema}" ]
+                    then
+                        cat ${database.schema}/mysql-databases/*.sql
+                    fi
+                    ''}
+                  ) | ${cfg.package}/bin/mysql -u ${superUser} -N
+              fi
+            '') cfg.initialDatabases}
+
+            ${optionalString (cfg.replication.role == "master")
+              ''
+                # Set up the replication master
+
+                ( echo "use mysql;"
+                  echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;"
+                  echo "SET PASSWORD FOR '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' = PASSWORD('${cfg.replication.masterPassword}');"
+                  echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';"
+                ) | ${cfg.package}/bin/mysql -u ${superUser} -N
+              ''}
+
+            ${optionalString (cfg.replication.role == "slave")
+              ''
+                # Set up the replication slave
+
+                ( echo "stop slave;"
+                  echo "change master to master_host='${cfg.replication.masterHost}', master_user='${cfg.replication.masterUser}', master_password='${cfg.replication.masterPassword}';"
+                  echo "start slave;"
+                ) | ${cfg.package}/bin/mysql -u ${superUser} -N
+              ''}
+
+            ${optionalString (cfg.initialScript != null)
+              ''
+                # Execute initial script
+                # using toString to avoid copying the file to nix store if given as path instead of string,
+                # as it might contain credentials
+                cat ${toString cfg.initialScript} | ${cfg.package}/bin/mysql -u ${superUser} -N
+              ''}
+
+            rm ${cfg.dataDir}/mysql_init
+        fi
+
+        ${optionalString (cfg.ensureDatabases != []) ''
+          (
+          ${concatMapStrings (database: ''
+            echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;"
+          '') cfg.ensureDatabases}
+          ) | ${cfg.package}/bin/mysql -N
+        ''}
+
+        ${concatMapStrings (user:
+          ''
+            ( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};"
+              ${concatStringsSep "\n" (mapAttrsToList (database: permission: ''
+                echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';"
+              '') user.ensurePermissions)}
+            ) | ${cfg.package}/bin/mysql -N
+          '') cfg.ensureUsers}
+      '';
+
+      serviceConfig = mkMerge [
+        {
+          Type = if isMariaDB then "notify" else "simple";
+          Restart = "on-abort";
+          RestartSec = "5s";
+
+          # User and group
+          User = cfg.user;
+          Group = cfg.group;
+          # Runtime directory and mode
+          RuntimeDirectory = "mysqld";
+          RuntimeDirectoryMode = "0755";
+          # Access write directories
+          ReadWritePaths = [ cfg.dataDir ];
+          # Capabilities
+          CapabilityBoundingSet = "";
+          # Security
+          NoNewPrivileges = true;
+          # Sandboxing
+          ProtectSystem = "strict";
+          ProtectHome = true;
+          PrivateTmp = true;
+          PrivateDevices = true;
+          ProtectHostname = true;
+          ProtectKernelTunables = true;
+          ProtectKernelModules = true;
+          ProtectControlGroups = true;
+          RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          PrivateMounts = true;
+          # System Call Filtering
+          SystemCallArchitectures = "native";
+        }
+        (mkIf (cfg.dataDir == "/var/lib/mysql") {
+          StateDirectory = "mysql";
+          StateDirectoryMode = "0700";
+        })
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/databases/neo4j.nix b/nixos/modules/services/databases/neo4j.nix
new file mode 100644
index 00000000000..8816f3b2e4b
--- /dev/null
+++ b/nixos/modules/services/databases/neo4j.nix
@@ -0,0 +1,673 @@
+{ config, options, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.neo4j;
+  opt = options.services.neo4j;
+  certDirOpt = options.services.neo4j.directories.certificates;
+  isDefaultPathOption = opt: isOption opt && opt.type == types.path && opt.highestPrio >= 1500;
+
+  sslPolicies = mapAttrsToList (
+    name: conf: ''
+      dbms.ssl.policy.${name}.allow_key_generation=${boolToString conf.allowKeyGeneration}
+      dbms.ssl.policy.${name}.base_directory=${conf.baseDirectory}
+      ${optionalString (conf.ciphers != null) ''
+        dbms.ssl.policy.${name}.ciphers=${concatStringsSep "," conf.ciphers}
+      ''}
+      dbms.ssl.policy.${name}.client_auth=${conf.clientAuth}
+      ${if length (splitString "/" conf.privateKey) > 1 then
+        "dbms.ssl.policy.${name}.private_key=${conf.privateKey}"
+      else
+        "dbms.ssl.policy.${name}.private_key=${conf.baseDirectory}/${conf.privateKey}"
+      }
+      ${if length (splitString "/" conf.privateKey) > 1 then
+        "dbms.ssl.policy.${name}.public_certificate=${conf.publicCertificate}"
+      else
+        "dbms.ssl.policy.${name}.public_certificate=${conf.baseDirectory}/${conf.publicCertificate}"
+      }
+      dbms.ssl.policy.${name}.revoked_dir=${conf.revokedDir}
+      dbms.ssl.policy.${name}.tls_versions=${concatStringsSep "," conf.tlsVersions}
+      dbms.ssl.policy.${name}.trust_all=${boolToString conf.trustAll}
+      dbms.ssl.policy.${name}.trusted_dir=${conf.trustedDir}
+    ''
+  ) cfg.ssl.policies;
+
+  serverConfig = pkgs.writeText "neo4j.conf" ''
+    # General
+    dbms.allow_upgrade=${boolToString cfg.allowUpgrade}
+    dbms.connectors.default_listen_address=${cfg.defaultListenAddress}
+    dbms.read_only=${boolToString cfg.readOnly}
+    ${optionalString (cfg.workerCount > 0) ''
+      dbms.threads.worker_count=${toString cfg.workerCount}
+    ''}
+
+    # Directories
+    dbms.directories.certificates=${cfg.directories.certificates}
+    dbms.directories.data=${cfg.directories.data}
+    dbms.directories.logs=${cfg.directories.home}/logs
+    dbms.directories.plugins=${cfg.directories.plugins}
+    ${optionalString (cfg.constrainLoadCsv) ''
+      dbms.directories.import=${cfg.directories.imports}
+    ''}
+
+    # HTTP Connector
+    ${optionalString (cfg.http.enable) ''
+      dbms.connector.http.enabled=${boolToString cfg.http.enable}
+      dbms.connector.http.listen_address=${cfg.http.listenAddress}
+    ''}
+    ${optionalString (!cfg.http.enable) ''
+      # It is not possible to disable the HTTP connector. To fully prevent
+      # clients from connecting to HTTP, block the HTTP port (7474 by default)
+      # via firewall. listen_address is set to the loopback interface to
+      # prevent remote clients from connecting.
+      dbms.connector.http.listen_address=127.0.0.1
+    ''}
+
+    # HTTPS Connector
+    dbms.connector.https.enabled=${boolToString cfg.https.enable}
+    dbms.connector.https.listen_address=${cfg.https.listenAddress}
+    https.ssl_policy=${cfg.https.sslPolicy}
+
+    # BOLT Connector
+    dbms.connector.bolt.enabled=${boolToString cfg.bolt.enable}
+    dbms.connector.bolt.listen_address=${cfg.bolt.listenAddress}
+    bolt.ssl_policy=${cfg.bolt.sslPolicy}
+    dbms.connector.bolt.tls_level=${cfg.bolt.tlsLevel}
+
+    # neo4j-shell
+    dbms.shell.enabled=${boolToString cfg.shell.enable}
+
+    # SSL Policies
+    ${concatStringsSep "\n" sslPolicies}
+
+    # Default retention policy from neo4j.conf
+    dbms.tx_log.rotation.retention_policy=1 days
+
+    # Default JVM parameters from neo4j.conf
+    dbms.jvm.additional=-XX:+UseG1GC
+    dbms.jvm.additional=-XX:-OmitStackTraceInFastThrow
+    dbms.jvm.additional=-XX:+AlwaysPreTouch
+    dbms.jvm.additional=-XX:+UnlockExperimentalVMOptions
+    dbms.jvm.additional=-XX:+TrustFinalNonStaticFields
+    dbms.jvm.additional=-XX:+DisableExplicitGC
+    dbms.jvm.additional=-Djdk.tls.ephemeralDHKeySize=2048
+    dbms.jvm.additional=-Djdk.tls.rejectClientInitiatedRenegotiation=true
+    dbms.jvm.additional=-Dunsupported.dbms.udc.source=tarball
+
+    # Usage Data Collector
+    dbms.udc.enabled=${boolToString cfg.udc.enable}
+
+    # Extra Configuration
+    ${cfg.extraServerConfig}
+  '';
+
+in {
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "neo4j" "host" ] [ "services" "neo4j" "defaultListenAddress" ])
+    (mkRenamedOptionModule [ "services" "neo4j" "listenAddress" ] [ "services" "neo4j" "defaultListenAddress" ])
+    (mkRenamedOptionModule [ "services" "neo4j" "enableBolt" ] [ "services" "neo4j" "bolt" "enable" ])
+    (mkRenamedOptionModule [ "services" "neo4j" "enableHttps" ] [ "services" "neo4j" "https" "enable" ])
+    (mkRenamedOptionModule [ "services" "neo4j" "certDir" ] [ "services" "neo4j" "directories" "certificates" ])
+    (mkRenamedOptionModule [ "services" "neo4j" "dataDir" ] [ "services" "neo4j" "directories" "home" ])
+    (mkRemovedOptionModule [ "services" "neo4j" "port" ] "Use services.neo4j.http.listenAddress instead.")
+    (mkRemovedOptionModule [ "services" "neo4j" "boltPort" ] "Use services.neo4j.bolt.listenAddress instead.")
+    (mkRemovedOptionModule [ "services" "neo4j" "httpsPort" ] "Use services.neo4j.https.listenAddress instead.")
+  ];
+
+  ###### interface
+
+  options.services.neo4j = {
+
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable Neo4j Community Edition.
+      '';
+    };
+
+    allowUpgrade = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Allow upgrade of Neo4j database files from an older version.
+      '';
+    };
+
+    constrainLoadCsv = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Sets the root directory for file URLs used with the Cypher
+        <literal>LOAD CSV</literal> clause to be that defined by
+        <option>directories.imports</option>. It restricts
+        access to only those files within that directory and its
+        subdirectories.
+        </para>
+        <para>
+        Setting this option to <literal>false</literal> introduces
+        possible security problems.
+      '';
+    };
+
+    defaultListenAddress = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = ''
+        Default network interface to listen for incoming connections. To
+        listen for connections on all interfaces, use "0.0.0.0".
+        </para>
+        <para>
+        Specifies the default IP address and address part of connector
+        specific <option>listenAddress</option> options. To bind specific
+        connectors to a specific network interfaces, specify the entire
+        <option>listenAddress</option> option for that connector.
+      '';
+    };
+
+    extraServerConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Extra configuration for Neo4j Community server. Refer to the
+        <link xlink:href="https://neo4j.com/docs/operations-manual/current/reference/configuration-settings/">complete reference</link>
+        of Neo4j configuration settings.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.neo4j;
+      defaultText = literalExpression "pkgs.neo4j";
+      description = ''
+        Neo4j package to use.
+      '';
+    };
+
+    readOnly = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Only allow read operations from this Neo4j instance.
+      '';
+    };
+
+    workerCount = mkOption {
+      type = types.ints.between 0 44738;
+      default = 0;
+      description = ''
+        Number of Neo4j worker threads, where the default of
+        <literal>0</literal> indicates a worker count equal to the number of
+        available processors.
+      '';
+    };
+
+    bolt = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable the BOLT connector for Neo4j. Setting this option to
+          <literal>false</literal> will stop Neo4j from listening for incoming
+          connections on the BOLT port (7687 by default).
+        '';
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = ":7687";
+        description = ''
+          Neo4j listen address for BOLT traffic. The listen address is
+          expressed in the format <literal>&lt;ip-address&gt;:&lt;port-number&gt;</literal>.
+        '';
+      };
+
+      sslPolicy = mkOption {
+        type = types.str;
+        default = "legacy";
+        description = ''
+          Neo4j SSL policy for BOLT traffic.
+          </para>
+          <para>
+          The legacy policy is a special policy which is not defined in
+          the policy configuration section, but rather derives from
+          <option>directories.certificates</option> and
+          associated files (by default: <filename>neo4j.key</filename> and
+          <filename>neo4j.cert</filename>). Its use will be deprecated.
+          </para>
+          <para>
+          Note: This connector must be configured to support/require
+          SSL/TLS for the legacy policy to actually be utilized. See
+          <option>bolt.tlsLevel</option>.
+        '';
+      };
+
+      tlsLevel = mkOption {
+        type = types.enum [ "REQUIRED" "OPTIONAL" "DISABLED" ];
+        default = "OPTIONAL";
+        description = ''
+          SSL/TSL requirement level for BOLT traffic.
+        '';
+      };
+    };
+
+    directories = {
+      certificates = mkOption {
+        type = types.path;
+        default = "${cfg.directories.home}/certificates";
+        defaultText = literalExpression ''"''${config.${opt.directories.home}}/certificates"'';
+        description = ''
+          Directory for storing certificates to be used by Neo4j for
+          TLS connections.
+          </para>
+          <para>
+          When setting this directory to something other than its default,
+          ensure the directory's existence, and that read/write permissions are
+          given to the Neo4j daemon user <literal>neo4j</literal>.
+          </para>
+          <para>
+          Note that changing this directory from its default will prevent
+          the directory structure required for each SSL policy from being
+          automatically generated. A policy's directory structure as defined by
+          its <option>baseDirectory</option>,<option>revokedDir</option> and
+          <option>trustedDir</option> must then be setup manually. The
+          existence of these directories is mandatory, as well as the presence
+          of the certificate file and the private key. Ensure the correct
+          permissions are set on these directories and files.
+        '';
+      };
+
+      data = mkOption {
+        type = types.path;
+        default = "${cfg.directories.home}/data";
+        defaultText = literalExpression ''"''${config.${opt.directories.home}}/data"'';
+        description = ''
+          Path of the data directory. You must not configure more than one
+          Neo4j installation to use the same data directory.
+          </para>
+          <para>
+          When setting this directory to something other than its default,
+          ensure the directory's existence, and that read/write permissions are
+          given to the Neo4j daemon user <literal>neo4j</literal>.
+        '';
+      };
+
+      home = mkOption {
+        type = types.path;
+        default = "/var/lib/neo4j";
+        description = ''
+          Path of the Neo4j home directory. Other default directories are
+          subdirectories of this path. This directory will be created if
+          non-existent, and its ownership will be <command>chown</command> to
+          the Neo4j daemon user <literal>neo4j</literal>.
+        '';
+      };
+
+      imports = mkOption {
+        type = types.path;
+        default = "${cfg.directories.home}/import";
+        defaultText = literalExpression ''"''${config.${opt.directories.home}}/import"'';
+        description = ''
+          The root directory for file URLs used with the Cypher
+          <literal>LOAD CSV</literal> clause. Only meaningful when
+          <option>constrainLoadCvs</option> is set to
+          <literal>true</literal>.
+          </para>
+          <para>
+          When setting this directory to something other than its default,
+          ensure the directory's existence, and that read permission is
+          given to the Neo4j daemon user <literal>neo4j</literal>.
+        '';
+      };
+
+      plugins = mkOption {
+        type = types.path;
+        default = "${cfg.directories.home}/plugins";
+        defaultText = literalExpression ''"''${config.${opt.directories.home}}/plugins"'';
+        description = ''
+          Path of the database plugin directory. Compiled Java JAR files that
+          contain database procedures will be loaded if they are placed in
+          this directory.
+          </para>
+          <para>
+          When setting this directory to something other than its default,
+          ensure the directory's existence, and that read permission is
+          given to the Neo4j daemon user <literal>neo4j</literal>.
+        '';
+      };
+    };
+
+    http = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          The HTTP connector is required for Neo4j, and cannot be disabled.
+          Setting this option to <literal>false</literal> will force the HTTP
+          connector's <option>listenAddress</option> to the loopback
+          interface to prevent connection of remote clients. To prevent all
+          clients from connecting, block the HTTP port (7474 by default) by
+          firewall.
+        '';
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = ":7474";
+        description = ''
+          Neo4j listen address for HTTP traffic. The listen address is
+          expressed in the format <literal>&lt;ip-address&gt;:&lt;port-number&gt;</literal>.
+        '';
+      };
+    };
+
+    https = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable the HTTPS connector for Neo4j. Setting this option to
+          <literal>false</literal> will stop Neo4j from listening for incoming
+          connections on the HTTPS port (7473 by default).
+        '';
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = ":7473";
+        description = ''
+          Neo4j listen address for HTTPS traffic. The listen address is
+          expressed in the format <literal>&lt;ip-address&gt;:&lt;port-number&gt;</literal>.
+        '';
+      };
+
+      sslPolicy = mkOption {
+        type = types.str;
+        default = "legacy";
+        description = ''
+          Neo4j SSL policy for HTTPS traffic.
+          </para>
+          <para>
+          The legacy policy is a special policy which is not defined in the
+          policy configuration section, but rather derives from
+          <option>directories.certificates</option> and
+          associated files (by default: <filename>neo4j.key</filename> and
+          <filename>neo4j.cert</filename>). Its use will be deprecated.
+        '';
+      };
+    };
+
+    shell = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable a remote shell server which Neo4j Shell clients can log in to.
+          Only applicable to <command>neo4j-shell</command>.
+        '';
+      };
+    };
+
+    ssl.policies = mkOption {
+      type = with types; attrsOf (submodule ({ name, config, options, ... }: {
+        options = {
+
+          allowKeyGeneration = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Allows the generation of a private key and associated self-signed
+              certificate. Only performed when both objects cannot be found for
+              this policy. It is recommended to turn this off again after keys
+              have been generated.
+              </para>
+              <para>
+              The public certificate is required to be duplicated to the
+              directory holding trusted certificates as defined by the
+              <option>trustedDir</option> option.
+              </para>
+              <para>
+              Keys should in general be generated and distributed offline by a
+              trusted certificate authority and not by utilizing this mode.
+            '';
+          };
+
+          baseDirectory = mkOption {
+            type = types.path;
+            default = "${cfg.directories.certificates}/${name}";
+            defaultText = literalExpression ''"''${config.${opt.directories.certificates}}/''${name}"'';
+            description = ''
+              The mandatory base directory for cryptographic objects of this
+              policy. This path is only automatically generated when this
+              option as well as <option>directories.certificates</option> are
+              left at their default. Ensure read/write permissions are given
+              to the Neo4j daemon user <literal>neo4j</literal>.
+              </para>
+              <para>
+              It is also possible to override each individual
+              configuration with absolute paths. See the
+              <option>privateKey</option> and <option>publicCertificate</option>
+              policy options.
+            '';
+          };
+
+          ciphers = mkOption {
+            type = types.nullOr (types.listOf types.str);
+            default = null;
+            description = ''
+              Restrict the allowed ciphers of this policy to those defined
+              here. The default ciphers are those of the JVM platform.
+            '';
+          };
+
+          clientAuth = mkOption {
+            type = types.enum [ "NONE" "OPTIONAL" "REQUIRE" ];
+            default = "REQUIRE";
+            description = ''
+              The client authentication stance for this policy.
+            '';
+          };
+
+          privateKey = mkOption {
+            type = types.str;
+            default = "private.key";
+            description = ''
+              The name of private PKCS #8 key file for this policy to be found
+              in the <option>baseDirectory</option>, or the absolute path to
+              the key file. It is mandatory that a key can be found or generated.
+            '';
+          };
+
+          publicCertificate = mkOption {
+            type = types.str;
+            default = "public.crt";
+            description = ''
+              The name of public X.509 certificate (chain) file in PEM format
+              for this policy to be found in the <option>baseDirectory</option>,
+              or the absolute path to the certificate file. It is mandatory
+              that a certificate can be found or generated.
+              </para>
+              <para>
+              The public certificate is required to be duplicated to the
+              directory holding trusted certificates as defined by the
+              <option>trustedDir</option> option.
+            '';
+          };
+
+          revokedDir = mkOption {
+            type = types.path;
+            default = "${config.baseDirectory}/revoked";
+            defaultText = literalExpression ''"''${config.${options.baseDirectory}}/revoked"'';
+            description = ''
+              Path to directory of CRLs (Certificate Revocation Lists) in
+              PEM format. Must be an absolute path. The existence of this
+              directory is mandatory and will need to be created manually when:
+              setting this option to something other than its default; setting
+              either this policy's <option>baseDirectory</option> or
+              <option>directories.certificates</option> to something other than
+              their default. Ensure read/write permissions are given to the
+              Neo4j daemon user <literal>neo4j</literal>.
+            '';
+          };
+
+          tlsVersions = mkOption {
+            type = types.listOf types.str;
+            default = [ "TLSv1.2" ];
+            description = ''
+              Restrict the TLS protocol versions of this policy to those
+              defined here.
+            '';
+          };
+
+          trustAll = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Makes this policy trust all remote parties. Enabling this is not
+              recommended and the policy's trusted directory will be ignored.
+              Use of this mode is discouraged. It would offer encryption but
+              no security.
+            '';
+          };
+
+          trustedDir = mkOption {
+            type = types.path;
+            default = "${config.baseDirectory}/trusted";
+            defaultText = literalExpression ''"''${config.${options.baseDirectory}}/trusted"'';
+            description = ''
+              Path to directory of X.509 certificates in PEM format for
+              trusted parties. Must be an absolute path. The existence of this
+              directory is mandatory and will need to be created manually when:
+              setting this option to something other than its default; setting
+              either this policy's <option>baseDirectory</option> or
+              <option>directories.certificates</option> to something other than
+              their default. Ensure read/write permissions are given to the
+              Neo4j daemon user <literal>neo4j</literal>.
+              </para>
+              <para>
+              The public certificate as defined by
+              <option>publicCertificate</option> is required to be duplicated
+              to this directory.
+            '';
+          };
+
+          directoriesToCreate = mkOption {
+            type = types.listOf types.path;
+            internal = true;
+            readOnly = true;
+            description = ''
+              Directories of this policy that will be created automatically
+              when the certificates directory is left at its default value.
+              This includes all options of type path that are left at their
+              default value.
+            '';
+          };
+
+        };
+
+        config.directoriesToCreate = optionals
+          (certDirOpt.highestPrio >= 1500 && options.baseDirectory.highestPrio >= 1500)
+          (map (opt: opt.value) (filter isDefaultPathOption (attrValues options)));
+
+      }));
+      default = {};
+      description = ''
+        Defines the SSL policies for use with Neo4j connectors. Each attribute
+        of this set defines a policy, with the attribute name defining the name
+        of the policy and its namespace. Refer to the operations manual section
+        on Neo4j's
+        <link xlink:href="https://neo4j.com/docs/operations-manual/current/security/ssl-framework/">SSL Framework</link>
+        for further details.
+      '';
+    };
+
+    udc = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the Usage Data Collector which Neo4j uses to collect usage
+          data. Refer to the operations manual section on the
+          <link xlink:href="https://neo4j.com/docs/operations-manual/current/configuration/usage-data-collector/">Usage Data Collector</link>
+          for more information.
+        '';
+      };
+    };
+
+  };
+
+  ###### implementation
+
+  config =
+    let
+      # Assertion helpers
+      policyNameList = attrNames cfg.ssl.policies;
+      validPolicyNameList = [ "legacy" ] ++ policyNameList;
+      validPolicyNameString = concatStringsSep ", " validPolicyNameList;
+
+      # Capture various directories left at their default so they can be created.
+      defaultDirectoriesToCreate = map (opt: opt.value) (filter isDefaultPathOption (attrValues options.services.neo4j.directories));
+      policyDirectoriesToCreate = concatMap (pol: pol.directoriesToCreate) (attrValues cfg.ssl.policies);
+    in
+
+    mkIf cfg.enable {
+      assertions = [
+        { assertion = !elem "legacy" policyNameList;
+          message = "The policy 'legacy' is special to Neo4j, and its name is reserved."; }
+        { assertion = elem cfg.bolt.sslPolicy validPolicyNameList;
+          message = "Invalid policy assigned: `services.neo4j.bolt.sslPolicy = \"${cfg.bolt.sslPolicy}\"`, defined policies are: ${validPolicyNameString}"; }
+        { assertion = elem cfg.https.sslPolicy validPolicyNameList;
+          message = "Invalid policy assigned: `services.neo4j.https.sslPolicy = \"${cfg.https.sslPolicy}\"`, defined policies are: ${validPolicyNameString}"; }
+      ];
+
+      systemd.services.neo4j = {
+        description = "Neo4j Daemon";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        environment = {
+          NEO4J_HOME = "${cfg.package}/share/neo4j";
+          NEO4J_CONF = "${cfg.directories.home}/conf";
+        };
+        serviceConfig = {
+          ExecStart = "${cfg.package}/bin/neo4j console";
+          User = "neo4j";
+          PermissionsStartOnly = true;
+          LimitNOFILE = 40000;
+        };
+
+        preStart = ''
+          # Directories Setup
+          #   Always ensure home exists with nested conf, logs directories.
+          mkdir -m 0700 -p ${cfg.directories.home}/{conf,logs}
+
+          #   Create other sub-directories and policy directories that have been left at their default.
+          ${concatMapStringsSep "\n" (
+            dir: ''
+              mkdir -m 0700 -p ${dir}
+          '') (defaultDirectoriesToCreate ++ policyDirectoriesToCreate)}
+
+          # Place the configuration where Neo4j can find it.
+          ln -fs ${serverConfig} ${cfg.directories.home}/conf/neo4j.conf
+
+          # Ensure neo4j user ownership
+          chown -R neo4j ${cfg.directories.home}
+        '';
+      };
+
+      environment.systemPackages = [ cfg.package ];
+
+      users.users.neo4j = {
+        isSystemUser = true;
+        group = "neo4j";
+        description = "Neo4j daemon user";
+        home = cfg.directories.home;
+      };
+      users.groups.neo4j = {};
+    };
+
+  meta = {
+    maintainers = with lib.maintainers; [ patternspandemic ];
+  };
+}
diff --git a/nixos/modules/services/databases/openldap.nix b/nixos/modules/services/databases/openldap.nix
new file mode 100644
index 00000000000..2c1e25d4308
--- /dev/null
+++ b/nixos/modules/services/databases/openldap.nix
@@ -0,0 +1,325 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.openldap;
+  legacyOptions = [ "rootpwFile" "suffix" "dataDir" "rootdn" "rootpw" ];
+  openldap = cfg.package;
+  configDir = if cfg.configDir != null then cfg.configDir else "/etc/openldap/slapd.d";
+
+  ldapValueType = let
+    # Can't do types.either with multiple non-overlapping submodules, so define our own
+    singleLdapValueType = lib.mkOptionType rec {
+      name = "LDAP";
+      description = "LDAP value";
+      check = x: lib.isString x || (lib.isAttrs x && (x ? path || x ? base64));
+      merge = lib.mergeEqualOption;
+    };
+    # We don't coerce to lists of single values, as some values must be unique
+  in types.either singleLdapValueType (types.listOf singleLdapValueType);
+
+  ldapAttrsType =
+    let
+      options = {
+        attrs = mkOption {
+          type = types.attrsOf ldapValueType;
+          default = {};
+          description = "Attributes of the parent entry.";
+        };
+        children = mkOption {
+          # Hide the child attributes, to avoid infinite recursion in e.g. documentation
+          # Actual Nix evaluation is lazy, so this is not an issue there
+          type = let
+            hiddenOptions = lib.mapAttrs (name: attr: attr // { visible = false; }) options;
+          in types.attrsOf (types.submodule { options = hiddenOptions; });
+          default = {};
+          description = "Child entries of the current entry, with recursively the same structure.";
+          example = lib.literalExpression ''
+            {
+                "cn=schema" = {
+                # The attribute used in the DN must be defined
+                attrs = { cn = "schema"; };
+                children = {
+                    # This entry's DN is expanded to "cn=foo,cn=schema"
+                    "cn=foo" = { ... };
+                };
+                # These includes are inserted after "cn=schema", but before "cn=foo,cn=schema"
+                includes = [ ... ];
+                };
+            }
+          '';
+        };
+        includes = mkOption {
+          type = types.listOf types.path;
+          default = [];
+          description = ''
+            LDIF files to include after the parent's attributes but before its children.
+          '';
+        };
+      };
+    in types.submodule { inherit options; };
+
+  valueToLdif = attr: values: let
+    listValues = if lib.isList values then values else lib.singleton values;
+  in map (value:
+    if lib.isAttrs value then
+      if lib.hasAttr "path" value
+      then "${attr}:< file://${value.path}"
+      else "${attr}:: ${value.base64}"
+    else "${attr}: ${lib.replaceStrings [ "\n" ] [ "\n " ] value}"
+  ) listValues;
+
+  attrsToLdif = dn: { attrs, children, includes, ... }: [''
+    dn: ${dn}
+    ${lib.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList valueToLdif attrs))}
+  ''] ++ (map (path: "include: file://${path}\n") includes) ++ (
+    lib.flatten (lib.mapAttrsToList (name: value: attrsToLdif "${name},${dn}" value) children)
+  );
+in {
+  imports = let
+    deprecationNote = "This option is removed due to the deprecation of `slapd.conf` upstream. Please migrate to `services.openldap.settings`, see the release notes for advice with this process.";
+    mkDatabaseOption = old: new:
+      lib.mkChangedOptionModule [ "services" "openldap" old ] [ "services" "openldap" "settings" "children" ]
+        (config: let
+          database = lib.getAttrFromPath [ "services" "openldap" "database" ] config;
+          value = lib.getAttrFromPath [ "services" "openldap" old ] config;
+        in lib.setAttrByPath ([ "olcDatabase={1}${database}" "attrs" ] ++ new) value);
+  in [
+    (lib.mkRemovedOptionModule [ "services" "openldap" "extraConfig" ] deprecationNote)
+    (lib.mkRemovedOptionModule [ "services" "openldap" "extraDatabaseConfig" ] deprecationNote)
+
+    (lib.mkChangedOptionModule [ "services" "openldap" "logLevel" ] [ "services" "openldap" "settings" "attrs" "olcLogLevel" ]
+      (config: lib.splitString " " (lib.getAttrFromPath [ "services" "openldap" "logLevel" ] config)))
+    (lib.mkChangedOptionModule [ "services" "openldap" "defaultSchemas" ] [ "services" "openldap" "settings" "children" "cn=schema" "includes"]
+      (config: lib.optionals (lib.getAttrFromPath [ "services" "openldap" "defaultSchemas" ] config) (
+        map (schema: "${openldap}/etc/schema/${schema}.ldif") [ "core" "cosine" "inetorgperson" "nis" ])))
+
+    (lib.mkChangedOptionModule [ "services" "openldap" "database" ] [ "services" "openldap" "settings" "children" ]
+      (config: let
+        database = lib.getAttrFromPath [ "services" "openldap" "database" ] config;
+      in {
+        "olcDatabase={1}${database}".attrs = {
+          # objectClass is case-insensitive, so don't need to capitalize ${database}
+          objectClass = [ "olcdatabaseconfig" "olc${database}config" ];
+          olcDatabase = "{1}${database}";
+          olcDbDirectory = lib.mkDefault "/var/db/openldap";
+        };
+        "cn=schema".includes = lib.mkDefault (
+          map (schema: "${openldap}/etc/schema/${schema}.ldif") [ "core" "cosine" "inetorgperson" "nis" ]
+        );
+      }))
+    (mkDatabaseOption "rootpwFile" [ "olcRootPW" "path" ])
+    (mkDatabaseOption "suffix" [ "olcSuffix" ])
+    (mkDatabaseOption "dataDir" [ "olcDbDirectory" ])
+    (mkDatabaseOption "rootdn" [ "olcRootDN" ])
+    (mkDatabaseOption "rootpw" [ "olcRootPW" ])
+  ];
+  options = {
+    services.openldap = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "
+          Whether to enable the ldap server.
+        ";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.openldap;
+        defaultText = literalExpression "pkgs.openldap";
+        description = ''
+          OpenLDAP package to use.
+
+          This can be used to, for example, set an OpenLDAP package
+          with custom overrides to enable modules or other
+          functionality.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "openldap";
+        description = "User account under which slapd runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "openldap";
+        description = "Group account under which slapd runs.";
+      };
+
+      urlList = mkOption {
+        type = types.listOf types.str;
+        default = [ "ldap:///" ];
+        description = "URL list slapd should listen on.";
+        example = [ "ldaps:///" ];
+      };
+
+      settings = mkOption {
+        type = ldapAttrsType;
+        description = "Configuration for OpenLDAP, in OLC format";
+        example = lib.literalExpression ''
+          {
+            attrs.olcLogLevel = [ "stats" ];
+            children = {
+              "cn=schema".includes = [
+                 "''${pkgs.openldap}/etc/schema/core.ldif"
+                 "''${pkgs.openldap}/etc/schema/cosine.ldif"
+                 "''${pkgs.openldap}/etc/schema/inetorgperson.ldif"
+              ];
+              "olcDatabase={-1}frontend" = {
+                attrs = {
+                  objectClass = "olcDatabaseConfig";
+                  olcDatabase = "{-1}frontend";
+                  olcAccess = [ "{0}to * by dn.exact=uidNumber=0+gidNumber=0,cn=peercred,cn=external,cn=auth manage stop by * none stop" ];
+                };
+              };
+              "olcDatabase={0}config" = {
+                attrs = {
+                  objectClass = "olcDatabaseConfig";
+                  olcDatabase = "{0}config";
+                  olcAccess = [ "{0}to * by * none break" ];
+                };
+              };
+              "olcDatabase={1}mdb" = {
+                attrs = {
+                  objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
+                  olcDatabase = "{1}mdb";
+                  olcDbDirectory = "/var/db/ldap";
+                  olcDbIndex = [
+                    "objectClass eq"
+                    "cn pres,eq"
+                    "uid pres,eq"
+                    "sn pres,eq,subany"
+                  ];
+                  olcSuffix = "dc=example,dc=com";
+                  olcAccess = [ "{0}to * by * read break" ];
+                };
+              };
+            };
+          };
+        '';
+      };
+
+      # This option overrides settings
+      configDir = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          Use this config directory instead of generating one from the
+          <literal>settings</literal> option. Overrides all NixOS settings. If
+          you use this option,ensure `olcPidFile` is set to `/run/slapd/slapd.conf`.
+        '';
+        example = "/var/db/slapd.d";
+      };
+
+      declarativeContents = mkOption {
+        type = with types; attrsOf lines;
+        default = {};
+        description = ''
+          Declarative contents for the LDAP database, in LDIF format by suffix.
+
+          All data will be erased when starting the LDAP server. Modifications
+          to the database are not prevented, they are just dropped on the next
+          reboot of the server. Performance-wise the database and indexes are
+          rebuilt on each server startup, so this will slow down server startup,
+          especially with large databases.
+        '';
+        example = lib.literalExpression ''
+          {
+            "dc=example,dc=org" = '''
+              dn= dn: dc=example,dc=org
+              objectClass: domain
+              dc: example
+
+              dn: ou=users,dc=example,dc=org
+              objectClass = organizationalUnit
+              ou: users
+
+              # ...
+            ''';
+          }
+        '';
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ mic92 kwohlfahrt ];
+
+  config = mkIf cfg.enable {
+    assertions = map (opt: {
+      assertion = ((getAttr opt cfg) != "_mkMergedOptionModule") -> (cfg.database != "_mkMergedOptionModule");
+      message = "Legacy OpenLDAP option `services.openldap.${opt}` requires `services.openldap.database` (use value \"mdb\" if unsure)";
+    }) legacyOptions;
+    environment.systemPackages = [ openldap ];
+
+    # Literal attributes must always be set
+    services.openldap.settings = {
+      attrs = {
+        objectClass = "olcGlobal";
+        cn = "config";
+        olcPidFile = "/run/slapd/slapd.pid";
+      };
+      children."cn=schema".attrs = {
+        cn = "schema";
+        objectClass = "olcSchemaConfig";
+      };
+    };
+
+    systemd.services.openldap = {
+      description = "LDAP server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      preStart = let
+        settingsFile = pkgs.writeText "config.ldif" (lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings));
+
+        dbSettings = lib.filterAttrs (name: value: lib.hasPrefix "olcDatabase=" name) cfg.settings.children;
+        dataDirs = lib.mapAttrs' (name: value: lib.nameValuePair value.attrs.olcSuffix value.attrs.olcDbDirectory)
+          (lib.filterAttrs (_: value: value.attrs ? olcDbDirectory) dbSettings);
+        dataFiles = lib.mapAttrs (dn: contents: pkgs.writeText "${dn}.ldif" contents) cfg.declarativeContents;
+        mkLoadScript = dn: let
+          dataDir = lib.escapeShellArg (getAttr dn dataDirs);
+        in  ''
+          rm -rf ${dataDir}/*
+          ${openldap}/bin/slapadd -F ${lib.escapeShellArg configDir} -b ${dn} -l ${getAttr dn dataFiles}
+          chown -R "${cfg.user}:${cfg.group}" ${dataDir}
+        '';
+      in ''
+        mkdir -p /run/slapd
+        chown -R "${cfg.user}:${cfg.group}" /run/slapd
+
+        mkdir -p ${lib.escapeShellArg configDir} ${lib.escapeShellArgs (lib.attrValues dataDirs)}
+        chown "${cfg.user}:${cfg.group}" ${lib.escapeShellArg configDir} ${lib.escapeShellArgs (lib.attrValues dataDirs)}
+
+        ${lib.optionalString (cfg.configDir == null) (''
+          rm -Rf ${configDir}/*
+          ${openldap}/bin/slapadd -F ${configDir} -bcn=config -l ${settingsFile}
+        '')}
+        chown -R "${cfg.user}:${cfg.group}" ${lib.escapeShellArg configDir}
+
+        ${lib.concatStrings (map mkLoadScript (lib.attrNames cfg.declarativeContents))}
+        ${openldap}/bin/slaptest -u -F ${lib.escapeShellArg configDir}
+      '';
+      serviceConfig = {
+        ExecStart = lib.escapeShellArgs ([
+          "${openldap}/libexec/slapd" "-u" cfg.user "-g" cfg.group "-F" configDir
+          "-h" (lib.concatStringsSep " " cfg.urlList)
+        ]);
+        Type = "forking";
+        PIDFile = cfg.settings.attrs.olcPidFile;
+      };
+    };
+
+    users.users = lib.optionalAttrs (cfg.user == "openldap") {
+      openldap = {
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = lib.optionalAttrs (cfg.group == "openldap") {
+      openldap = {};
+    };
+  };
+}
diff --git a/nixos/modules/services/databases/opentsdb.nix b/nixos/modules/services/databases/opentsdb.nix
new file mode 100644
index 00000000000..e873b2f7011
--- /dev/null
+++ b/nixos/modules/services/databases/opentsdb.nix
@@ -0,0 +1,108 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.opentsdb;
+
+  configFile = pkgs.writeText "opentsdb.conf" cfg.config;
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.opentsdb = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run OpenTSDB.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.opentsdb;
+        defaultText = literalExpression "pkgs.opentsdb";
+        description = ''
+          OpenTSDB package to use.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "opentsdb";
+        description = ''
+          User account under which OpenTSDB runs.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "opentsdb";
+        description = ''
+          Group account under which OpenTSDB runs.
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 4242;
+        description = ''
+          Which port OpenTSDB listens on.
+        '';
+      };
+
+      config = mkOption {
+        type = types.lines;
+        default = ''
+          tsd.core.auto_create_metrics = true
+          tsd.http.request.enable_chunked  = true
+        '';
+        description = ''
+          The contents of OpenTSDB's configuration file
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf config.services.opentsdb.enable {
+
+    systemd.services.opentsdb = {
+      description = "OpenTSDB Server";
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "hbase.service" ];
+
+      environment.JAVA_HOME = "${pkgs.jre}";
+      path = [ pkgs.gnuplot ];
+
+      preStart =
+        ''
+        COMPRESSION=NONE HBASE_HOME=${config.services.hbase.package} ${cfg.package}/share/opentsdb/tools/create_table.sh
+        '';
+
+      serviceConfig = {
+        PermissionsStartOnly = true;
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.package}/bin/tsdb tsd --staticroot=${cfg.package}/share/opentsdb/static --cachedir=/tmp/opentsdb --port=${toString cfg.port} --config=${configFile}";
+      };
+    };
+
+    users.users.opentsdb = {
+      description = "OpenTSDB Server user";
+      group = "opentsdb";
+      uid = config.ids.uids.opentsdb;
+    };
+
+    users.groups.opentsdb.gid = config.ids.gids.opentsdb;
+
+  };
+}
diff --git a/nixos/modules/services/databases/pgmanage.nix b/nixos/modules/services/databases/pgmanage.nix
new file mode 100644
index 00000000000..f30f71866af
--- /dev/null
+++ b/nixos/modules/services/databases/pgmanage.nix
@@ -0,0 +1,207 @@
+{ lib, pkgs, config, ... } :
+
+with lib;
+
+let
+  cfg = config.services.pgmanage;
+
+  confFile = pkgs.writeTextFile {
+    name = "pgmanage.conf";
+    text =  ''
+      connection_file = ${pgmanageConnectionsFile}
+
+      allow_custom_connections = ${builtins.toJSON cfg.allowCustomConnections}
+
+      pgmanage_port = ${toString cfg.port}
+
+      super_only = ${builtins.toJSON cfg.superOnly}
+
+      ${optionalString (cfg.loginGroup != null) "login_group = ${cfg.loginGroup}"}
+
+      login_timeout = ${toString cfg.loginTimeout}
+
+      web_root = ${cfg.package}/etc/pgmanage/web_root
+
+      sql_root = ${cfg.sqlRoot}
+
+      ${optionalString (cfg.tls != null) ''
+      tls_cert = ${cfg.tls.cert}
+      tls_key = ${cfg.tls.key}
+      ''}
+
+      log_level = ${cfg.logLevel}
+    '';
+  };
+
+  pgmanageConnectionsFile = pkgs.writeTextFile {
+    name = "pgmanage-connections.conf";
+    text = concatStringsSep "\n"
+      (mapAttrsToList (name : conn : "${name}: ${conn}") cfg.connections);
+  };
+
+  pgmanage = "pgmanage";
+
+in {
+
+  options.services.pgmanage = {
+    enable = mkEnableOption "PostgreSQL Administration for the web";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.pgmanage;
+      defaultText = literalExpression "pkgs.pgmanage";
+      description = ''
+        The pgmanage package to use.
+      '';
+    };
+
+    connections = mkOption {
+      type = types.attrsOf types.str;
+      default = {};
+      example = {
+        nuc-server  = "hostaddr=192.168.0.100 port=5432 dbname=postgres";
+        mini-server = "hostaddr=127.0.0.1 port=5432 dbname=postgres sslmode=require";
+      };
+      description = ''
+        pgmanage requires at least one PostgreSQL server be defined.
+        </para><para>
+        Detailed information about PostgreSQL connection strings is available at:
+        <link xlink:href="http://www.postgresql.org/docs/current/static/libpq-connect.html"/>
+        </para><para>
+        Note that you should not specify your user name or password. That
+        information will be entered on the login screen. If you specify a
+        username or password, it will be removed by pgmanage before attempting to
+        connect to a database.
+      '';
+    };
+
+    allowCustomConnections = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        This tells pgmanage whether or not to allow anyone to use a custom
+        connection from the login screen.
+      '';
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = 8080;
+      description = ''
+        This tells pgmanage what port to listen on for browser requests.
+      '';
+    };
+
+    localOnly = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        This tells pgmanage whether or not to set the listening socket to local
+        addresses only.
+      '';
+    };
+
+    superOnly = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        This tells pgmanage whether or not to only allow super users to
+        login. The recommended value is true and will restrict users who are not
+        super users from logging in to any PostgreSQL instance through
+        pgmanage. Note that a connection will be made to PostgreSQL in order to
+        test if the user is a superuser.
+      '';
+    };
+
+    loginGroup = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        This tells pgmanage to only allow users in a certain PostgreSQL group to
+        login to pgmanage. Note that a connection will be made to PostgreSQL in
+        order to test if the user is a member of the login group.
+      '';
+    };
+
+    loginTimeout = mkOption {
+      type = types.int;
+      default = 3600;
+      description = ''
+        Number of seconds of inactivity before user is automatically logged
+        out.
+      '';
+    };
+
+    sqlRoot = mkOption {
+      type = types.str;
+      default = "/var/lib/pgmanage";
+      description = ''
+        This tells pgmanage where to put the SQL file history. All tabs are saved
+        to this location so that if you get disconnected from pgmanage you
+        don't lose your work.
+      '';
+    };
+
+    tls = mkOption {
+      type = types.nullOr (types.submodule {
+        options = {
+          cert = mkOption {
+            type = types.str;
+            description = "TLS certificate";
+          };
+          key = mkOption {
+            type = types.str;
+            description = "TLS key";
+          };
+        };
+      });
+      default = null;
+      description = ''
+        These options tell pgmanage where the TLS Certificate and Key files
+        reside. If you use these options then you'll only be able to access
+        pgmanage through a secure TLS connection. These options are only
+        necessary if you wish to connect directly to pgmanage using a secure TLS
+        connection. As an alternative, you can set up pgmanage in a reverse proxy
+        configuration. This allows your web server to terminate the secure
+        connection and pass on the request to pgmanage. You can find help to set
+        up this configuration in:
+        <link xlink:href="https://github.com/pgManage/pgManage/blob/master/INSTALL_NGINX.md"/>
+      '';
+    };
+
+    logLevel = mkOption {
+      type = types.enum ["error" "warn" "notice" "info"];
+      default = "error";
+      description = ''
+        Verbosity of logs
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.pgmanage = {
+      description = "pgmanage - PostgreSQL Administration for the web";
+      wants    = [ "postgresql.service" ];
+      after    = [ "postgresql.service" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        User         = pgmanage;
+        Group        = pgmanage;
+        ExecStart    = "${pkgs.pgmanage}/sbin/pgmanage -c ${confFile}" +
+                       optionalString cfg.localOnly " --local-only=true";
+      };
+    };
+    users = {
+      users.${pgmanage} = {
+        name  = pgmanage;
+        group = pgmanage;
+        home  = cfg.sqlRoot;
+        createHome = true;
+        isSystemUser = true;
+      };
+      groups.${pgmanage} = {
+        name = pgmanage;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/databases/postgresql.nix b/nixos/modules/services/databases/postgresql.nix
new file mode 100644
index 00000000000..2919022496a
--- /dev/null
+++ b/nixos/modules/services/databases/postgresql.nix
@@ -0,0 +1,424 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.postgresql;
+
+  postgresql =
+    if cfg.extraPlugins == []
+      then cfg.package
+      else cfg.package.withPackages (_: cfg.extraPlugins);
+
+  toStr = value:
+    if true == value then "yes"
+    else if false == value then "no"
+    else if isString value then "'${lib.replaceStrings ["'"] ["''"] value}'"
+    else toString value;
+
+  # The main PostgreSQL configuration file.
+  configFile = pkgs.writeTextDir "postgresql.conf" (concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${toStr v}") cfg.settings));
+
+  configFileCheck = pkgs.runCommand "postgresql-configfile-check" {} ''
+    ${cfg.package}/bin/postgres -D${configFile} -C config_file >/dev/null
+    touch $out
+  '';
+
+  groupAccessAvailable = versionAtLeast postgresql.version "11.0";
+
+in
+
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "postgresql" "extraConfig" ] "Use services.postgresql.settings instead.")
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.postgresql = {
+
+      enable = mkEnableOption "PostgreSQL Server";
+
+      package = mkOption {
+        type = types.package;
+        example = literalExpression "pkgs.postgresql_11";
+        description = ''
+          PostgreSQL package to use.
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 5432;
+        description = ''
+          The port on which PostgreSQL listens.
+        '';
+      };
+
+      checkConfig = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Check the syntax of the configuration file at compile time";
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        defaultText = literalExpression ''"/var/lib/postgresql/''${config.services.postgresql.package.psqlSchema}"'';
+        example = "/var/lib/postgresql/11";
+        description = ''
+          The data directory for PostgreSQL. If left as the default value
+          this directory will automatically be created before the PostgreSQL server starts, otherwise
+          the sysadmin is responsible for ensuring the directory exists with appropriate ownership
+          and permissions.
+        '';
+      };
+
+      authentication = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Defines how users authenticate themselves to the server. See the
+          <link xlink:href="https://www.postgresql.org/docs/current/auth-pg-hba-conf.html">
+          PostgreSQL documentation for pg_hba.conf</link>
+          for details on the expected format of this option. By default,
+          peer based authentication will be used for users connecting
+          via the Unix socket, and md5 password authentication will be
+          used for users connecting via TCP. Any added rules will be
+          inserted above the default rules. If you'd like to replace the
+          default rules entirely, you can use <function>lib.mkForce</function> in your
+          module.
+        '';
+      };
+
+      identMap = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Defines the mapping from system users to database users.
+
+          The general form is:
+
+          map-name system-username database-username
+        '';
+      };
+
+      initdbArgs = mkOption {
+        type = with types; listOf str;
+        default = [];
+        example = [ "--data-checksums" "--allow-group-access" ];
+        description = ''
+          Additional arguments passed to <literal>initdb</literal> during data dir
+          initialisation.
+        '';
+      };
+
+      initialScript = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          A file containing SQL statements to execute on first startup.
+        '';
+      };
+
+      ensureDatabases = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Ensures that the specified databases exist.
+          This option will never delete existing databases, especially not when the value of this
+          option is changed. This means that databases created once through this option or
+          otherwise have to be removed manually.
+        '';
+        example = [
+          "gitea"
+          "nextcloud"
+        ];
+      };
+
+      ensureUsers = mkOption {
+        type = types.listOf (types.submodule {
+          options = {
+            name = mkOption {
+              type = types.str;
+              description = ''
+                Name of the user to ensure.
+              '';
+            };
+            ensurePermissions = mkOption {
+              type = types.attrsOf types.str;
+              default = {};
+              description = ''
+                Permissions to ensure for the user, specified as an attribute set.
+                The attribute names specify the database and tables to grant the permissions for.
+                The attribute values specify the permissions to grant. You may specify one or
+                multiple comma-separated SQL privileges here.
+
+                For more information on how to specify the target
+                and on which privileges exist, see the
+                <link xlink:href="https://www.postgresql.org/docs/current/sql-grant.html">GRANT syntax</link>.
+                The attributes are used as <code>GRANT ''${attrValue} ON ''${attrName}</code>.
+              '';
+              example = literalExpression ''
+                {
+                  "DATABASE \"nextcloud\"" = "ALL PRIVILEGES";
+                  "ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES";
+                }
+              '';
+            };
+          };
+        });
+        default = [];
+        description = ''
+          Ensures that the specified users exist and have at least the ensured permissions.
+          The PostgreSQL users will be identified using peer authentication. This authenticates the Unix user with the
+          same name only, and that without the need for a password.
+          This option will never delete existing users or remove permissions, especially not when the value of this
+          option is changed. This means that users created and permissions assigned once through this option or
+          otherwise have to be removed manually.
+        '';
+        example = literalExpression ''
+          [
+            {
+              name = "nextcloud";
+              ensurePermissions = {
+                "DATABASE nextcloud" = "ALL PRIVILEGES";
+              };
+            }
+            {
+              name = "superuser";
+              ensurePermissions = {
+                "ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES";
+              };
+            }
+          ]
+        '';
+      };
+
+      enableTCPIP = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether PostgreSQL should listen on all network interfaces.
+          If disabled, the database can only be accessed via its Unix
+          domain socket or via TCP connections to localhost.
+        '';
+      };
+
+      logLinePrefix = mkOption {
+        type = types.str;
+        default = "[%p] ";
+        example = "%m [%p] ";
+        description = ''
+          A printf-style string that is output at the beginning of each log line.
+          Upstream default is <literal>'%m [%p] '</literal>, i.e. it includes the timestamp. We do
+          not include the timestamp, because journal has it anyway.
+        '';
+      };
+
+      extraPlugins = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        example = literalExpression "with pkgs.postgresql_11.pkgs; [ postgis pg_repack ]";
+        description = ''
+          List of PostgreSQL plugins. PostgreSQL version for each plugin should
+          match version for <literal>services.postgresql.package</literal> value.
+        '';
+      };
+
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ bool float int str ]);
+        default = {};
+        description = ''
+          PostgreSQL configuration. Refer to
+          <link xlink:href="https://www.postgresql.org/docs/11/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE"/>
+          for an overview of <literal>postgresql.conf</literal>.
+
+          <note><para>
+            String values will automatically be enclosed in single quotes. Single quotes will be
+            escaped with two single quotes as described by the upstream documentation linked above.
+          </para></note>
+        '';
+        example = literalExpression ''
+          {
+            log_connections = true;
+            log_statement = "all";
+            logging_collector = true
+            log_disconnections = true
+            log_destination = lib.mkForce "syslog";
+          }
+        '';
+      };
+
+      recoveryConfig = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = ''
+          Contents of the <filename>recovery.conf</filename> file.
+        '';
+      };
+
+      superUser = mkOption {
+        type = types.str;
+        default = "postgres";
+        internal = true;
+        readOnly = true;
+        description = ''
+          PostgreSQL superuser account to use for various operations. Internal since changing
+          this value would lead to breakage while setting up databases.
+        '';
+        };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.postgresql.settings =
+      {
+        hba_file = "${pkgs.writeText "pg_hba.conf" cfg.authentication}";
+        ident_file = "${pkgs.writeText "pg_ident.conf" cfg.identMap}";
+        log_destination = "stderr";
+        log_line_prefix = cfg.logLinePrefix;
+        listen_addresses = if cfg.enableTCPIP then "*" else "localhost";
+        port = cfg.port;
+      };
+
+    services.postgresql.package = let
+        mkThrow = ver: throw "postgresql_${ver} was removed, please upgrade your postgresql version.";
+    in
+      # Note: when changing the default, make it conditional on
+      # ‘system.stateVersion’ to maintain compatibility with existing
+      # systems!
+      mkDefault (if versionAtLeast config.system.stateVersion "21.11" then pkgs.postgresql_13
+            else if versionAtLeast config.system.stateVersion "20.03" then pkgs.postgresql_11
+            else if versionAtLeast config.system.stateVersion "17.09" then mkThrow "9_6"
+            else mkThrow "9_5");
+
+    services.postgresql.dataDir = mkDefault "/var/lib/postgresql/${cfg.package.psqlSchema}";
+
+    services.postgresql.authentication = mkAfter
+      ''
+        # Generated file; do not edit!
+        local all all              peer
+        host  all all 127.0.0.1/32 md5
+        host  all all ::1/128      md5
+      '';
+
+    users.users.postgres =
+      { name = "postgres";
+        uid = config.ids.uids.postgres;
+        group = "postgres";
+        description = "PostgreSQL server user";
+        home = "${cfg.dataDir}";
+        useDefaultShell = true;
+      };
+
+    users.groups.postgres.gid = config.ids.gids.postgres;
+
+    environment.systemPackages = [ postgresql ];
+
+    environment.pathsToLink = [
+     "/share/postgresql"
+    ];
+
+    system.extraDependencies = lib.optional (cfg.checkConfig && pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) configFileCheck;
+
+    systemd.services.postgresql =
+      { description = "PostgreSQL Server";
+
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+
+        environment.PGDATA = cfg.dataDir;
+
+        path = [ postgresql ];
+
+        preStart =
+          ''
+            if ! test -e ${cfg.dataDir}/PG_VERSION; then
+              # Cleanup the data directory.
+              rm -f ${cfg.dataDir}/*.conf
+
+              # Initialise the database.
+              initdb -U ${cfg.superUser} ${concatStringsSep " " cfg.initdbArgs}
+
+              # See postStart!
+              touch "${cfg.dataDir}/.first_startup"
+            fi
+
+            ln -sfn "${configFile}/postgresql.conf" "${cfg.dataDir}/postgresql.conf"
+            ${optionalString (cfg.recoveryConfig != null) ''
+              ln -sfn "${pkgs.writeText "recovery.conf" cfg.recoveryConfig}" \
+                "${cfg.dataDir}/recovery.conf"
+            ''}
+          '';
+
+        # Wait for PostgreSQL to be ready to accept connections.
+        postStart =
+          ''
+            PSQL="psql --port=${toString cfg.port}"
+
+            while ! $PSQL -d postgres -c "" 2> /dev/null; do
+                if ! kill -0 "$MAINPID"; then exit 1; fi
+                sleep 0.1
+            done
+
+            if test -e "${cfg.dataDir}/.first_startup"; then
+              ${optionalString (cfg.initialScript != null) ''
+                $PSQL -f "${cfg.initialScript}" -d postgres
+              ''}
+              rm -f "${cfg.dataDir}/.first_startup"
+            fi
+          '' + optionalString (cfg.ensureDatabases != []) ''
+            ${concatMapStrings (database: ''
+              $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${database}'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "${database}"'
+            '') cfg.ensureDatabases}
+          '' + ''
+            ${concatMapStrings (user: ''
+              $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user.name}'" | grep -q 1 || $PSQL -tAc 'CREATE USER "${user.name}"'
+              ${concatStringsSep "\n" (mapAttrsToList (database: permission: ''
+                $PSQL -tAc 'GRANT ${permission} ON ${database} TO "${user.name}"'
+              '') user.ensurePermissions)}
+            '') cfg.ensureUsers}
+          '';
+
+        serviceConfig = mkMerge [
+          { ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+            User = "postgres";
+            Group = "postgres";
+            RuntimeDirectory = "postgresql";
+            Type = if versionAtLeast cfg.package.version "9.6"
+                   then "notify"
+                   else "simple";
+
+            # Shut down Postgres using SIGINT ("Fast Shutdown mode").  See
+            # http://www.postgresql.org/docs/current/static/server-shutdown.html
+            KillSignal = "SIGINT";
+            KillMode = "mixed";
+
+            # Give Postgres a decent amount of time to clean up after
+            # receiving systemd's SIGINT.
+            TimeoutSec = 120;
+
+            ExecStart = "${postgresql}/bin/postgres";
+          }
+          (mkIf (cfg.dataDir == "/var/lib/postgresql/${cfg.package.psqlSchema}") {
+            StateDirectory = "postgresql postgresql/${cfg.package.psqlSchema}";
+            StateDirectoryMode = if groupAccessAvailable then "0750" else "0700";
+          })
+        ];
+
+        unitConfig.RequiresMountsFor = "${cfg.dataDir}";
+      };
+
+  };
+
+  meta.doc = ./postgresql.xml;
+  meta.maintainers = with lib.maintainers; [ thoughtpolice danbst ];
+}
diff --git a/nixos/modules/services/databases/postgresql.xml b/nixos/modules/services/databases/postgresql.xml
new file mode 100644
index 00000000000..0ca9f3faed2
--- /dev/null
+++ b/nixos/modules/services/databases/postgresql.xml
@@ -0,0 +1,214 @@
+<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" /> = [
+    (pkgs.writeScriptBin "upgrade-pg-cluster" ''
+      set -eux
+      # XXX it's perhaps advisable to stop all services that depend on postgresql
+      systemctl stop postgresql
+
+      # XXX replace `&lt;new version&gt;` with the psqlSchema here
+      export NEWDATA="/var/lib/postgresql/&lt;new version&gt;"
+
+      # XXX specify the postgresql package you'd like to upgrade to
+      export NEWBIN="${pkgs.postgresql_13}/bin"
+
+      export OLDDATA="${config.<xref linkend="opt-services.postgresql.dataDir"/>}"
+      export OLDBIN="${config.<xref linkend="opt-services.postgresql.package"/>}/bin"
+
+      install -d -m 0700 -o postgres -g postgres "$NEWDATA"
+      cd "$NEWDATA"
+      sudo -u postgres $NEWBIN/initdb -D "$NEWDATA"
+
+      sudo -u postgres $NEWBIN/pg_upgrade \
+        --old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \
+        --old-bindir $OLDBIN --new-bindir $NEWBIN \
+        "$@"
+    '')
+  ];
+}
+</programlisting>
+  </para>
+
+  <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 (as <literal>su -l postgres</literal> in the
+     <xref linkend="opt-services.postgresql.dataDir" />, in this example <filename>/var/lib/postgresql/13</filename>):
+<programlisting>
+<prompt>$ </prompt>./analyze_new_cluster.sh
+</programlisting>
+     <warning><para>The next step removes the old state-directory!</para></warning>
+<programlisting>
+<prompt>$ </prompt>./delete_old_cluster.sh
+</programlisting>
+    </para>
+   </listitem>
+  </orderedlist>
+ </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/databases/redis.nix b/nixos/modules/services/databases/redis.nix
new file mode 100644
index 00000000000..a1bd73c9e37
--- /dev/null
+++ b/nixos/modules/services/databases/redis.nix
@@ -0,0 +1,391 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.redis;
+
+  mkValueString = value:
+    if value == true then "yes"
+    else if value == false then "no"
+    else generators.mkValueStringDefault { } value;
+
+  redisConfig = settings: pkgs.writeText "redis.conf" (generators.toKeyValue {
+    listsAsDuplicateKeys = true;
+    mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } " ";
+  } settings);
+
+  redisName = name: "redis" + optionalString (name != "") ("-"+name);
+  enabledServers = filterAttrs (name: conf: conf.enable) config.services.redis.servers;
+
+in {
+  imports = [
+    (mkRemovedOptionModule [ "services" "redis" "user" ] "The redis module now is hardcoded to the redis user.")
+    (mkRemovedOptionModule [ "services" "redis" "dbpath" ] "The redis module now uses /var/lib/redis as data directory.")
+    (mkRemovedOptionModule [ "services" "redis" "dbFilename" ] "The redis module now uses /var/lib/redis/dump.rdb as database dump location.")
+    (mkRemovedOptionModule [ "services" "redis" "appendOnlyFilename" ] "This option was never used.")
+    (mkRemovedOptionModule [ "services" "redis" "pidFile" ] "This option was removed.")
+    (mkRemovedOptionModule [ "services" "redis" "extraConfig" ] "Use services.redis.servers.*.settings instead.")
+    (mkRenamedOptionModule [ "services" "redis" "enable"] [ "services" "redis" "servers" "" "enable" ])
+    (mkRenamedOptionModule [ "services" "redis" "port"] [ "services" "redis" "servers" "" "port" ])
+    (mkRenamedOptionModule [ "services" "redis" "openFirewall"] [ "services" "redis" "servers" "" "openFirewall" ])
+    (mkRenamedOptionModule [ "services" "redis" "bind"] [ "services" "redis" "servers" "" "bind" ])
+    (mkRenamedOptionModule [ "services" "redis" "unixSocket"] [ "services" "redis" "servers" "" "unixSocket" ])
+    (mkRenamedOptionModule [ "services" "redis" "unixSocketPerm"] [ "services" "redis" "servers" "" "unixSocketPerm" ])
+    (mkRenamedOptionModule [ "services" "redis" "logLevel"] [ "services" "redis" "servers" "" "logLevel" ])
+    (mkRenamedOptionModule [ "services" "redis" "logfile"] [ "services" "redis" "servers" "" "logfile" ])
+    (mkRenamedOptionModule [ "services" "redis" "syslog"] [ "services" "redis" "servers" "" "syslog" ])
+    (mkRenamedOptionModule [ "services" "redis" "databases"] [ "services" "redis" "servers" "" "databases" ])
+    (mkRenamedOptionModule [ "services" "redis" "maxclients"] [ "services" "redis" "servers" "" "maxclients" ])
+    (mkRenamedOptionModule [ "services" "redis" "save"] [ "services" "redis" "servers" "" "save" ])
+    (mkRenamedOptionModule [ "services" "redis" "slaveOf"] [ "services" "redis" "servers" "" "slaveOf" ])
+    (mkRenamedOptionModule [ "services" "redis" "masterAuth"] [ "services" "redis" "servers" "" "masterAuth" ])
+    (mkRenamedOptionModule [ "services" "redis" "requirePass"] [ "services" "redis" "servers" "" "requirePass" ])
+    (mkRenamedOptionModule [ "services" "redis" "requirePassFile"] [ "services" "redis" "servers" "" "requirePassFile" ])
+    (mkRenamedOptionModule [ "services" "redis" "appendOnly"] [ "services" "redis" "servers" "" "appendOnly" ])
+    (mkRenamedOptionModule [ "services" "redis" "appendFsync"] [ "services" "redis" "servers" "" "appendFsync" ])
+    (mkRenamedOptionModule [ "services" "redis" "slowLogLogSlowerThan"] [ "services" "redis" "servers" "" "slowLogLogSlowerThan" ])
+    (mkRenamedOptionModule [ "services" "redis" "slowLogMaxLen"] [ "services" "redis" "servers" "" "slowLogMaxLen" ])
+    (mkRenamedOptionModule [ "services" "redis" "settings"] [ "services" "redis" "servers" "" "settings" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.redis = {
+      package = mkOption {
+        type = types.package;
+        default = pkgs.redis;
+        defaultText = literalExpression "pkgs.redis";
+        description = "Which Redis derivation to use.";
+      };
+
+      vmOverCommit = mkEnableOption ''
+        setting of vm.overcommit_memory to 1
+        (Suggested for Background Saving: http://redis.io/topics/faq)
+      '';
+
+      servers = mkOption {
+        type = with types; attrsOf (submodule ({config, name, ...}@args: {
+          options = {
+            enable = mkEnableOption ''
+              Redis server.
+
+              Note that the NixOS module for Redis disables kernel support
+              for Transparent Huge Pages (THP),
+              because this features causes major performance problems for Redis,
+              e.g. (https://redis.io/topics/latency).
+            '';
+
+            user = mkOption {
+              type = types.str;
+              default = redisName name;
+              defaultText = literalExpression ''
+                if name == "" then "redis" else "redis-''${name}"
+              '';
+              description = "The username and groupname for redis-server.";
+            };
+
+            port = mkOption {
+              type = types.port;
+              default = if name == "" then 6379 else 0;
+              defaultText = literalExpression ''if name == "" then 6379 else 0'';
+              description = ''
+                The TCP port to accept connections.
+                If port 0 is specified Redis will not listen on a TCP socket.
+              '';
+            };
+
+            openFirewall = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Whether to open ports in the firewall for the server.
+              '';
+            };
+
+            bind = mkOption {
+              type = with types; nullOr str;
+              default = "127.0.0.1";
+              description = ''
+                The IP interface to bind to.
+                <literal>null</literal> means "all interfaces".
+              '';
+              example = "192.0.2.1";
+            };
+
+            unixSocket = mkOption {
+              type = with types; nullOr path;
+              default = "/run/${redisName name}/redis.sock";
+              defaultText = literalExpression ''
+                if name == "" then "/run/redis/redis.sock" else "/run/redis-''${name}/redis.sock"
+              '';
+              description = "The path to the socket to bind to.";
+            };
+
+            unixSocketPerm = mkOption {
+              type = types.int;
+              default = 660;
+              description = "Change permissions for the socket";
+              example = 600;
+            };
+
+            logLevel = mkOption {
+              type = types.str;
+              default = "notice"; # debug, verbose, notice, warning
+              example = "debug";
+              description = "Specify the server verbosity level, options: debug, verbose, notice, warning.";
+            };
+
+            logfile = mkOption {
+              type = types.str;
+              default = "/dev/null";
+              description = "Specify the log file name. Also 'stdout' can be used to force Redis to log on the standard output.";
+              example = "/var/log/redis.log";
+            };
+
+            syslog = mkOption {
+              type = types.bool;
+              default = true;
+              description = "Enable logging to the system logger.";
+            };
+
+            databases = mkOption {
+              type = types.int;
+              default = 16;
+              description = "Set the number of databases.";
+            };
+
+            maxclients = mkOption {
+              type = types.int;
+              default = 10000;
+              description = "Set the max number of connected clients at the same time.";
+            };
+
+            save = mkOption {
+              type = with types; listOf (listOf int);
+              default = [ [900 1] [300 10] [60 10000] ];
+              description = "The schedule in which data is persisted to disk, represented as a list of lists where the first element represent the amount of seconds and the second the number of changes.";
+            };
+
+            slaveOf = mkOption {
+              type = with types; nullOr (submodule ({ ... }: {
+                options = {
+                  ip = mkOption {
+                    type = str;
+                    description = "IP of the Redis master";
+                    example = "192.168.1.100";
+                  };
+
+                  port = mkOption {
+                    type = port;
+                    description = "port of the Redis master";
+                    default = 6379;
+                  };
+                };
+              }));
+
+              default = null;
+              description = "IP and port to which this redis instance acts as a slave.";
+              example = { ip = "192.168.1.100"; port = 6379; };
+            };
+
+            masterAuth = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''If the master is password protected (using the requirePass configuration)
+              it is possible to tell the slave to authenticate before starting the replication synchronization
+              process, otherwise the master will refuse the slave request.
+              (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE)'';
+            };
+
+            requirePass = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Password for database (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE).
+                Use requirePassFile to store it outside of the nix store in a dedicated file.
+              '';
+              example = "letmein!";
+            };
+
+            requirePassFile = mkOption {
+              type = with types; nullOr path;
+              default = null;
+              description = "File with password for the database.";
+              example = "/run/keys/redis-password";
+            };
+
+            appendOnly = mkOption {
+              type = types.bool;
+              default = false;
+              description = "By default data is only periodically persisted to disk, enable this option to use an append-only file for improved persistence.";
+            };
+
+            appendFsync = mkOption {
+              type = types.str;
+              default = "everysec"; # no, always, everysec
+              description = "How often to fsync the append-only log, options: no, always, everysec.";
+            };
+
+            slowLogLogSlowerThan = mkOption {
+              type = types.int;
+              default = 10000;
+              description = "Log queries whose execution take longer than X in milliseconds.";
+              example = 1000;
+            };
+
+            slowLogMaxLen = mkOption {
+              type = types.int;
+              default = 128;
+              description = "Maximum number of items to keep in slow log.";
+            };
+
+            settings = mkOption {
+              # TODO: this should be converted to freeformType
+              type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
+              default = {};
+              description = ''
+                Redis configuration. Refer to
+                <link xlink:href="https://redis.io/topics/config"/>
+                for details on supported values.
+              '';
+              example = literalExpression ''
+                {
+                  loadmodule = [ "/path/to/my_module.so" "/path/to/other_module.so" ];
+                }
+              '';
+            };
+          };
+          config.settings = mkMerge [
+            {
+              port = config.port;
+              daemonize = false;
+              supervised = "systemd";
+              loglevel = config.logLevel;
+              logfile = config.logfile;
+              syslog-enabled = config.syslog;
+              databases = config.databases;
+              maxclients = config.maxclients;
+              save = map (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}") config.save;
+              dbfilename = "dump.rdb";
+              dir = "/var/lib/${redisName name}";
+              appendOnly = config.appendOnly;
+              appendfsync = config.appendFsync;
+              slowlog-log-slower-than = config.slowLogLogSlowerThan;
+              slowlog-max-len = config.slowLogMaxLen;
+            }
+            (mkIf (config.bind != null) { bind = config.bind; })
+            (mkIf (config.unixSocket != null) {
+              unixsocket = config.unixSocket;
+              unixsocketperm = toString config.unixSocketPerm;
+            })
+            (mkIf (config.slaveOf != null) { slaveof = "${config.slaveOf.ip} ${toString config.slaveOf.port}"; })
+            (mkIf (config.masterAuth != null) { masterauth = config.masterAuth; })
+            (mkIf (config.requirePass != null) { requirepass = config.requirePass; })
+          ];
+        }));
+        description = "Configuration of multiple <literal>redis-server</literal> instances.";
+        default = {};
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf (enabledServers != {}) {
+
+    assertions = attrValues (mapAttrs (name: conf: {
+      assertion = conf.requirePass != null -> conf.requirePassFile == null;
+      message = ''
+        You can only set one services.redis.servers.${name}.requirePass
+        or services.redis.servers.${name}.requirePassFile
+      '';
+    }) enabledServers);
+
+    boot.kernel.sysctl = mkMerge [
+      { "vm.nr_hugepages" = "0"; }
+      ( mkIf cfg.vmOverCommit { "vm.overcommit_memory" = "1"; } )
+    ];
+
+    networking.firewall.allowedTCPPorts = concatMap (conf:
+      optional conf.openFirewall conf.port
+    ) (attrValues enabledServers);
+
+    environment.systemPackages = [ cfg.package ];
+
+    users.users = mapAttrs' (name: conf: nameValuePair (redisName name) {
+      description = "System user for the redis-server instance ${name}";
+      isSystemUser = true;
+      group = redisName name;
+    }) enabledServers;
+    users.groups = mapAttrs' (name: conf: nameValuePair (redisName name) {
+    }) enabledServers;
+
+    systemd.services = mapAttrs' (name: conf: nameValuePair (redisName name) {
+      description = "Redis Server - ${redisName name}";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/redis-server /run/${redisName name}/redis.conf";
+        ExecStartPre = [("+"+pkgs.writeShellScript "${redisName name}-credentials" (''
+            install -o '${conf.user}' -m 600 ${redisConfig conf.settings} /run/${redisName name}/redis.conf
+          '' + optionalString (conf.requirePassFile != null) ''
+            {
+              printf requirePass' '
+              cat ${escapeShellArg conf.requirePassFile}
+            } >>/run/${redisName name}/redis.conf
+          '')
+        )];
+        Type = "notify";
+        # User and group
+        User = conf.user;
+        Group = conf.user;
+        # Runtime directory and mode
+        RuntimeDirectory = redisName name;
+        RuntimeDirectoryMode = "0750";
+        # State directory and mode
+        StateDirectory = redisName name;
+        StateDirectoryMode = "0700";
+        # Access write directories
+        UMask = "0077";
+        # Capabilities
+        CapabilityBoundingSet = "";
+        # Security
+        NoNewPrivileges = true;
+        # Process Properties
+        LimitNOFILE = mkDefault "${toString (conf.maxclients + 32)}";
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies =
+          optionals (conf.port != 0) ["AF_INET" "AF_INET6"] ++
+          optional (conf.unixSocket != null) "AF_UNIX";
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        # System Call Filtering
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "~@cpu-emulation @debug @keyring @memlock @mount @obsolete @privileged @resources @setuid";
+      };
+    }) enabledServers;
+
+  };
+}
diff --git a/nixos/modules/services/databases/rethinkdb.nix b/nixos/modules/services/databases/rethinkdb.nix
new file mode 100644
index 00000000000..c764d6c21c6
--- /dev/null
+++ b/nixos/modules/services/databases/rethinkdb.nix
@@ -0,0 +1,108 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.rethinkdb;
+  rethinkdb = cfg.package;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.rethinkdb = {
+
+      enable = mkEnableOption "RethinkDB server";
+
+      #package = mkOption {
+      #  default = pkgs.rethinkdb;
+      #  description = "Which RethinkDB derivation to use.";
+      #};
+
+      user = mkOption {
+        default = "rethinkdb";
+        description = "User account under which RethinkDB runs.";
+      };
+
+      group = mkOption {
+        default = "rethinkdb";
+        description = "Group which rethinkdb user belongs to.";
+      };
+
+      dbpath = mkOption {
+        default = "/var/db/rethinkdb";
+        description = "Location where RethinkDB stores its data, 1 data directory per instance.";
+      };
+
+      pidpath = mkOption {
+        default = "/run/rethinkdb";
+        description = "Location where each instance's pid file is located.";
+      };
+
+      #cfgpath = mkOption {
+      #  default = "/etc/rethinkdb/instances.d";
+      #  description = "Location where RethinkDB stores it config files, 1 config file per instance.";
+      #};
+
+      # TODO: currently not used by our implementation.
+      #instances = mkOption {
+      #  type = types.attrsOf types.str;
+      #  default = {};
+      #  description = "List of named RethinkDB instances in our cluster.";
+      #};
+
+    };
+
+  };
+
+  ###### implementation
+  config = mkIf config.services.rethinkdb.enable {
+
+    environment.systemPackages = [ rethinkdb ];
+
+    systemd.services.rethinkdb = {
+      description = "RethinkDB server";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        # TODO: abstract away 'default', which is a per-instance directory name
+        #       allowing end user of this nix module to provide multiple instances,
+        #       and associated directory per instance
+        ExecStart = "${rethinkdb}/bin/rethinkdb -d ${cfg.dbpath}/default";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        User = cfg.user;
+        Group = cfg.group;
+        PIDFile = "${cfg.pidpath}/default.pid";
+        PermissionsStartOnly = true;
+      };
+
+      preStart = ''
+        if ! test -e ${cfg.dbpath}; then
+            install -d -m0755 -o ${cfg.user} -g ${cfg.group} ${cfg.dbpath}
+            install -d -m0755 -o ${cfg.user} -g ${cfg.group} ${cfg.dbpath}/default
+            chown -R ${cfg.user}:${cfg.group} ${cfg.dbpath}
+        fi
+        if ! test -e "${cfg.pidpath}/default.pid"; then
+            install -D -o ${cfg.user} -g ${cfg.group} /dev/null "${cfg.pidpath}/default.pid"
+        fi
+      '';
+    };
+
+    users.users.rethinkdb = mkIf (cfg.user == "rethinkdb")
+      { name = "rethinkdb";
+        description = "RethinkDB server user";
+        isSystemUser = true;
+      };
+
+    users.groups = optionalAttrs (cfg.group == "rethinkdb") (singleton
+      { name = "rethinkdb";
+      });
+
+  };
+
+}
diff --git a/nixos/modules/services/databases/riak.nix b/nixos/modules/services/databases/riak.nix
new file mode 100644
index 00000000000..cc4237d038c
--- /dev/null
+++ b/nixos/modules/services/databases/riak.nix
@@ -0,0 +1,162 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.riak;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.riak = {
+
+      enable = mkEnableOption "riak";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.riak;
+        defaultText = literalExpression "pkgs.riak";
+        description = ''
+          Riak package to use.
+        '';
+      };
+
+      nodeName = mkOption {
+        type = types.str;
+        default = "riak@127.0.0.1";
+        description = ''
+          Name of the Erlang node.
+        '';
+      };
+
+      distributedCookie = mkOption {
+        type = types.str;
+        default = "riak";
+        description = ''
+          Cookie for distributed node communication.  All nodes in the
+          same cluster should use the same cookie or they will not be able to
+          communicate.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/db/riak";
+        description = ''
+          Data directory for Riak.
+        '';
+      };
+
+      logDir = mkOption {
+        type = types.path;
+        default = "/var/log/riak";
+        description = ''
+          Log directory for Riak.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Additional text to be appended to <filename>riak.conf</filename>.
+        '';
+      };
+
+      extraAdvancedConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Additional text to be appended to <filename>advanced.config</filename>.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ cfg.package ];
+    environment.etc."riak/riak.conf".text = ''
+      nodename = ${cfg.nodeName}
+      distributed_cookie = ${cfg.distributedCookie}
+
+      platform_log_dir = ${cfg.logDir}
+      platform_etc_dir = /etc/riak
+      platform_data_dir = ${cfg.dataDir}
+
+      ${cfg.extraConfig}
+    '';
+
+    environment.etc."riak/advanced.config".text = ''
+      ${cfg.extraAdvancedConfig}
+    '';
+
+    users.users.riak = {
+      name = "riak";
+      uid = config.ids.uids.riak;
+      group = "riak";
+      description = "Riak server user";
+    };
+
+    users.groups.riak.gid = config.ids.gids.riak;
+
+    systemd.services.riak = {
+      description = "Riak Server";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      path = [
+        pkgs.util-linux # for `logger`
+        pkgs.bash
+      ];
+
+      environment.HOME = "${cfg.dataDir}";
+      environment.RIAK_DATA_DIR = "${cfg.dataDir}";
+      environment.RIAK_LOG_DIR = "${cfg.logDir}";
+      environment.RIAK_ETC_DIR = "/etc/riak";
+
+      preStart = ''
+        if ! test -e ${cfg.logDir}; then
+          mkdir -m 0755 -p ${cfg.logDir}
+          chown -R riak ${cfg.logDir}
+        fi
+
+        if ! test -e ${cfg.dataDir}; then
+          mkdir -m 0700 -p ${cfg.dataDir}
+          chown -R riak ${cfg.dataDir}
+        fi
+      '';
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/riak console";
+        ExecStop = "${cfg.package}/bin/riak stop";
+        StandardInput = "tty";
+        User = "riak";
+        Group = "riak";
+        PermissionsStartOnly = true;
+        # Give Riak a decent amount of time to clean up.
+        TimeoutStopSec = 120;
+        LimitNOFILE = 65536;
+      };
+
+      unitConfig.RequiresMountsFor = [
+        "${cfg.dataDir}"
+        "${cfg.logDir}"
+        "/etc/riak"
+      ];
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/databases/victoriametrics.nix b/nixos/modules/services/databases/victoriametrics.nix
new file mode 100644
index 00000000000..0513dcff172
--- /dev/null
+++ b/nixos/modules/services/databases/victoriametrics.nix
@@ -0,0 +1,78 @@
+{ config, pkgs, lib, ... }:
+let cfg = config.services.victoriametrics; in
+{
+  options.services.victoriametrics = with lib; {
+    enable = mkEnableOption "victoriametrics";
+    package = mkOption {
+      type = types.package;
+      default = pkgs.victoriametrics;
+      defaultText = literalExpression "pkgs.victoriametrics";
+      description = ''
+        The VictoriaMetrics distribution to use.
+      '';
+    };
+    listenAddress = mkOption {
+      default = ":8428";
+      type = types.str;
+      description = ''
+        The listen address for the http interface.
+      '';
+    };
+    retentionPeriod = mkOption {
+      type = types.int;
+      default = 1;
+      description = ''
+        Retention period in months.
+      '';
+    };
+    extraOptions = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Extra options to pass to VictoriaMetrics. See the README: <link
+        xlink:href="https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/README.md" />
+        or <command>victoriametrics -help</command> for more
+        information.
+      '';
+    };
+  };
+  config = lib.mkIf cfg.enable {
+    systemd.services.victoriametrics = {
+      description = "VictoriaMetrics time series database";
+      after = [ "network.target" ];
+      startLimitBurst = 5;
+      serviceConfig = {
+        Restart = "on-failure";
+        RestartSec = 1;
+        StateDirectory = "victoriametrics";
+        DynamicUser = true;
+        ExecStart = ''
+          ${cfg.package}/bin/victoria-metrics \
+              -storageDataPath=/var/lib/victoriametrics \
+              -httpListenAddr ${cfg.listenAddress} \
+              -retentionPeriod ${toString cfg.retentionPeriod} \
+              ${lib.escapeShellArgs cfg.extraOptions}
+        '';
+        # victoriametrics 1.59 with ~7GB of data seems to eventually panic when merging files and then
+        # begins restart-looping forever. Set LimitNOFILE= to a large number to work around this issue.
+        #
+        # panic: FATAL: unrecoverable error when merging small parts in the partition "/var/lib/victoriametrics/data/small/2021_08":
+        # cannot open source part for merging: cannot open values file in stream mode:
+        # cannot open file "/var/lib/victoriametrics/data/small/2021_08/[...]/values.bin":
+        # open /var/lib/victoriametrics/data/small/2021_08/[...]/values.bin: too many open files
+        LimitNOFILE = 1048576;
+      };
+      wantedBy = [ "multi-user.target" ];
+
+      postStart =
+        let
+          bindAddr = (lib.optionalString (lib.hasPrefix ":" cfg.listenAddress) "127.0.0.1") + cfg.listenAddress;
+        in
+        lib.mkBefore ''
+          until ${lib.getBin pkgs.curl}/bin/curl -s -o /dev/null http://${bindAddr}/ping; do
+            sleep 1;
+          done
+        '';
+    };
+  };
+}
diff --git a/nixos/modules/services/desktops/accountsservice.nix b/nixos/modules/services/desktops/accountsservice.nix
new file mode 100644
index 00000000000..ae2ecb5ffeb
--- /dev/null
+++ b/nixos/modules/services/desktops/accountsservice.nix
@@ -0,0 +1,58 @@
+# AccountsService daemon.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.freedesktop.members;
+  };
+
+  ###### interface
+
+  options = {
+
+    services.accounts-daemon = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable AccountsService, a DBus service for accessing
+          the list of user accounts and information attached to those accounts.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.accounts-daemon.enable {
+
+    environment.systemPackages = [ pkgs.accountsservice ];
+
+    # Accounts daemon looks for dbus interfaces in $XDG_DATA_DIRS/accountsservice
+    environment.pathsToLink = [ "/share/accountsservice" ];
+
+    services.dbus.packages = [ pkgs.accountsservice ];
+
+    systemd.packages = [ pkgs.accountsservice ];
+
+    systemd.services.accounts-daemon = recursiveUpdate {
+
+      wantedBy = [ "graphical.target" ];
+
+      # Accounts daemon looks for dbus interfaces in $XDG_DATA_DIRS/accountsservice
+      environment.XDG_DATA_DIRS = "${config.system.path}/share";
+
+    } (optionalAttrs (!config.users.mutableUsers) {
+      environment.NIXOS_USERS_PURE = "true";
+    });
+  };
+
+}
diff --git a/nixos/modules/services/desktops/bamf.nix b/nixos/modules/services/desktops/bamf.nix
new file mode 100644
index 00000000000..13de3a44328
--- /dev/null
+++ b/nixos/modules/services/desktops/bamf.nix
@@ -0,0 +1,27 @@
+# Bamf
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  meta = with lib; {
+    maintainers = with maintainers; [ ] ++ teams.pantheon.members;
+  };
+
+  ###### interface
+
+  options = {
+    services.bamf = {
+      enable = mkEnableOption "bamf";
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf config.services.bamf.enable {
+    services.dbus.packages = [ pkgs.bamf ];
+
+    systemd.packages = [ pkgs.bamf ];
+  };
+}
diff --git a/nixos/modules/services/desktops/blueman.nix b/nixos/modules/services/desktops/blueman.nix
new file mode 100644
index 00000000000..18ad610247e
--- /dev/null
+++ b/nixos/modules/services/desktops/blueman.nix
@@ -0,0 +1,25 @@
+# blueman service
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.blueman;
+in {
+  ###### interface
+  options = {
+    services.blueman = {
+      enable = mkEnableOption "blueman";
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.blueman ];
+
+    services.dbus.packages = [ pkgs.blueman ];
+
+    systemd.packages = [ pkgs.blueman ];
+  };
+}
diff --git a/nixos/modules/services/desktops/cpupower-gui.nix b/nixos/modules/services/desktops/cpupower-gui.nix
new file mode 100644
index 00000000000..f66afc0a3dc
--- /dev/null
+++ b/nixos/modules/services/desktops/cpupower-gui.nix
@@ -0,0 +1,56 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cpupower-gui;
+in {
+  options = {
+    services.cpupower-gui = {
+      enable = mkOption {
+        type = lib.types.bool;
+        default = false;
+        example = true;
+        description = ''
+          Enables dbus/systemd service needed by cpupower-gui.
+          These services are responsible for retrieving and modifying cpu power
+          saving settings.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.cpupower-gui ];
+    services.dbus.packages = [ pkgs.cpupower-gui ];
+    systemd.user = {
+      services.cpupower-gui-user = {
+        description = "Apply cpupower-gui config at user login";
+        wantedBy = [ "graphical-session.target" ];
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = "${pkgs.cpupower-gui}/bin/cpupower-gui config";
+        };
+      };
+    };
+    systemd.services = {
+      cpupower-gui = {
+        description = "Apply cpupower-gui config at boot";
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = "${pkgs.cpupower-gui}/bin/cpupower-gui config";
+        };
+      };
+      cpupower-gui-helper = {
+        description = "cpupower-gui system helper";
+        aliases = [ "dbus-org.rnd2.cpupower_gui.helper.service" ];
+        serviceConfig = {
+          Type = "dbus";
+          BusName = "org.rnd2.cpupower_gui.helper";
+          ExecStart = "${pkgs.cpupower-gui}/lib/cpupower-gui/cpupower-gui-helper";
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/desktops/dleyna-renderer.nix b/nixos/modules/services/desktops/dleyna-renderer.nix
new file mode 100644
index 00000000000..7f88605f627
--- /dev/null
+++ b/nixos/modules/services/desktops/dleyna-renderer.nix
@@ -0,0 +1,28 @@
+# dleyna-renderer service.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  ###### interface
+  options = {
+    services.dleyna-renderer = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable dleyna-renderer service, a DBus service
+          for handling DLNA renderers.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+  config = mkIf config.services.dleyna-renderer.enable {
+    environment.systemPackages = [ pkgs.dleyna-renderer ];
+
+    services.dbus.packages = [ pkgs.dleyna-renderer ];
+  };
+}
diff --git a/nixos/modules/services/desktops/dleyna-server.nix b/nixos/modules/services/desktops/dleyna-server.nix
new file mode 100644
index 00000000000..9a131a5e700
--- /dev/null
+++ b/nixos/modules/services/desktops/dleyna-server.nix
@@ -0,0 +1,28 @@
+# dleyna-server service.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  ###### interface
+  options = {
+    services.dleyna-server = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable dleyna-server service, a DBus service
+          for handling DLNA servers.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+  config = mkIf config.services.dleyna-server.enable {
+    environment.systemPackages = [ pkgs.dleyna-server ];
+
+    services.dbus.packages = [ pkgs.dleyna-server ];
+  };
+}
diff --git a/nixos/modules/services/desktops/espanso.nix b/nixos/modules/services/desktops/espanso.nix
new file mode 100644
index 00000000000..4ef6724dda0
--- /dev/null
+++ b/nixos/modules/services/desktops/espanso.nix
@@ -0,0 +1,24 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let cfg = config.services.espanso;
+in {
+  meta = { maintainers = with lib.maintainers; [ numkem ]; };
+
+  options = {
+    services.espanso = { enable = options.mkEnableOption "Espanso"; };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.espanso = {
+      description = "Espanso daemon";
+      serviceConfig = {
+        ExecStart = "${pkgs.espanso}/bin/espanso daemon";
+        Restart = "on-failure";
+      };
+      wantedBy = [ "default.target" ];
+    };
+
+    environment.systemPackages = [ pkgs.espanso ];
+  };
+}
diff --git a/nixos/modules/services/desktops/flatpak.nix b/nixos/modules/services/desktops/flatpak.nix
new file mode 100644
index 00000000000..5fecc64b4f7
--- /dev/null
+++ b/nixos/modules/services/desktops/flatpak.nix
@@ -0,0 +1,56 @@
+# flatpak service.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.flatpak;
+in {
+  meta = {
+    doc = ./flatpak.xml;
+    maintainers = pkgs.flatpak.meta.maintainers;
+  };
+
+  ###### interface
+  options = {
+    services.flatpak = {
+      enable = mkEnableOption "flatpak";
+    };
+  };
+
+
+  ###### implementation
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = (config.xdg.portal.enable == true);
+        message = "To use Flatpak you must enable XDG Desktop Portals with xdg.portal.enable.";
+      }
+    ];
+
+    environment.systemPackages = [ pkgs.flatpak ];
+
+    security.polkit.enable = true;
+
+    services.dbus.packages = [ pkgs.flatpak ];
+
+    systemd.packages = [ pkgs.flatpak ];
+
+    environment.profiles = [
+      "$HOME/.local/share/flatpak/exports"
+      "/var/lib/flatpak/exports"
+    ];
+
+    # It has been possible since https://github.com/flatpak/flatpak/releases/tag/1.3.2
+    # to build a SELinux policy module.
+
+    # TODO: use sysusers.d
+    users.users.flatpak = {
+      description = "Flatpak system helper";
+      group = "flatpak";
+      isSystemUser = true;
+    };
+
+    users.groups.flatpak = { };
+  };
+}
diff --git a/nixos/modules/services/desktops/flatpak.xml b/nixos/modules/services/desktops/flatpak.xml
new file mode 100644
index 00000000000..8f080b25022
--- /dev/null
+++ b/nixos/modules/services/desktops/flatpak.xml
@@ -0,0 +1,56 @@
+<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/geoclue2.nix b/nixos/modules/services/desktops/geoclue2.nix
new file mode 100644
index 00000000000..60a34dd6563
--- /dev/null
+++ b/nixos/modules/services/desktops/geoclue2.nix
@@ -0,0 +1,270 @@
+# GeoClue 2 daemon.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  # the demo agent isn't built by default, but we need it here
+  package = pkgs.geoclue2.override { withDemoAgent = config.services.geoclue2.enableDemoAgent; };
+
+  cfg = config.services.geoclue2;
+
+  defaultWhitelist = [ "gnome-shell" "io.elementary.desktop.agent-geoclue2" ];
+
+  appConfigModule = types.submodule ({ name, ... }: {
+    options = {
+      desktopID = mkOption {
+        type = types.str;
+        description = "Desktop ID of the application.";
+      };
+
+      isAllowed = mkOption {
+        type = types.bool;
+        description = ''
+          Whether the application will be allowed access to location information.
+        '';
+      };
+
+      isSystem = mkOption {
+        type = types.bool;
+        description = ''
+          Whether the application is a system component or not.
+        '';
+      };
+
+      users = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          List of UIDs of all users for which this application is allowed location
+          info access, Defaults to an empty string to allow it for all users.
+        '';
+      };
+    };
+
+    config.desktopID = mkDefault name;
+  });
+
+  appConfigToINICompatible = _: { desktopID, isAllowed, isSystem, users, ... }: {
+    name = desktopID;
+    value = {
+      allowed = isAllowed;
+      system = isSystem;
+      users = concatStringsSep ";" users;
+    };
+  };
+
+in
+{
+
+  ###### interface
+
+  options = {
+
+    services.geoclue2 = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable GeoClue 2 daemon, a DBus service
+          that provides location information for accessing.
+        '';
+      };
+
+      enableDemoAgent = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to use the GeoClue demo agent. This should be
+          overridden by desktop environments that provide their own
+          agent.
+        '';
+      };
+
+      enableNmea = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to fetch location from NMEA sources on local network.
+        '';
+      };
+
+      enable3G = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable 3G source.
+        '';
+      };
+
+      enableCDMA = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable CDMA source.
+        '';
+      };
+
+      enableModemGPS = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable Modem-GPS source.
+        '';
+      };
+
+      enableWifi = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable WiFi source.
+        '';
+      };
+
+      geoProviderUrl = mkOption {
+        type = types.str;
+        default = "https://location.services.mozilla.com/v1/geolocate?key=geoclue";
+        example = "https://www.googleapis.com/geolocation/v1/geolocate?key=YOUR_KEY";
+        description = ''
+          The url to the wifi GeoLocation Service.
+        '';
+      };
+
+      submitData = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to submit data to a GeoLocation Service.
+        '';
+      };
+
+      submissionUrl = mkOption {
+        type = types.str;
+        default = "https://location.services.mozilla.com/v1/submit?key=geoclue";
+        description = ''
+          The url to submit data to a GeoLocation Service.
+        '';
+      };
+
+      submissionNick = mkOption {
+        type = types.str;
+        default = "geoclue";
+        description = ''
+          A nickname to submit network data with.
+          Must be 2-32 characters long.
+        '';
+      };
+
+      appConfig = mkOption {
+        type = types.attrsOf appConfigModule;
+        default = {};
+        example = literalExpression ''
+          "com.github.app" = {
+            isAllowed = true;
+            isSystem = true;
+            users = [ "300" ];
+          };
+        '';
+        description = ''
+          Specify extra settings per application.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ package ];
+
+    services.dbus.packages = [ package ];
+
+    systemd.packages = [ package ];
+
+    # we cannot use DynamicUser as we need the the geoclue user to exist for the
+    # dbus policy to work
+    users = {
+      users.geoclue = {
+        isSystemUser = true;
+        home = "/var/lib/geoclue";
+        group = "geoclue";
+        description = "Geoinformation service";
+      };
+
+      groups.geoclue = {};
+    };
+
+    systemd.services.geoclue = {
+      # restart geoclue service when the configuration changes
+      restartTriggers = [
+        config.environment.etc."geoclue/geoclue.conf".source
+      ];
+      serviceConfig.StateDirectory = "geoclue";
+    };
+
+    # this needs to run as a user service, since it's associated with the
+    # user who is making the requests
+    systemd.user.services = mkIf cfg.enableDemoAgent {
+      geoclue-agent = {
+        description = "Geoclue agent";
+        # this should really be `partOf = [ "geoclue.service" ]`, but
+        # we can't be part of a system service, and the agent should
+        # be okay with the main service coming and going
+        wantedBy = [ "default.target" ];
+        unitConfig.ConditionUser = "!@system";
+        serviceConfig = {
+          Type = "exec";
+          ExecStart = "${package}/libexec/geoclue-2.0/demos/agent";
+          Restart = "on-failure";
+          PrivateTmp = true;
+        };
+      };
+    };
+
+    services.geoclue2.appConfig.epiphany = {
+      isAllowed = true;
+      isSystem = false;
+    };
+
+    services.geoclue2.appConfig.firefox = {
+      isAllowed = true;
+      isSystem = false;
+    };
+
+    environment.etc."geoclue/geoclue.conf".text =
+      generators.toINI {} ({
+        agent = {
+          whitelist = concatStringsSep ";"
+            (optional cfg.enableDemoAgent "geoclue-demo-agent" ++ defaultWhitelist);
+        };
+        network-nmea = {
+          enable = cfg.enableNmea;
+        };
+        "3g" = {
+          enable = cfg.enable3G;
+        };
+        cdma = {
+          enable = cfg.enableCDMA;
+        };
+        modem-gps = {
+          enable = cfg.enableModemGPS;
+        };
+        wifi = {
+          enable = cfg.enableWifi;
+          url = cfg.geoProviderUrl;
+          submit-data = boolToString cfg.submitData;
+          submission-url = cfg.submissionUrl;
+          submission-nick = cfg.submissionNick;
+        };
+      } // mapAttrs' appConfigToINICompatible cfg.appConfig);
+  };
+
+  meta = with lib; {
+    maintainers = with maintainers; [ ] ++ teams.pantheon.members;
+  };
+}
diff --git a/nixos/modules/services/desktops/gnome/at-spi2-core.nix b/nixos/modules/services/desktops/gnome/at-spi2-core.nix
new file mode 100644
index 00000000000..1268a9d49b8
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/at-spi2-core.nix
@@ -0,0 +1,57 @@
+# at-spi2-core daemon.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  ###### interface
+
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "at-spi2-core" "enable" ]
+      [ "services" "gnome" "at-spi2-core" "enable" ]
+    )
+  ];
+
+  options = {
+
+    services.gnome.at-spi2-core = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable at-spi2-core, a service for the Assistive Technologies
+          available on the GNOME platform.
+
+          Enable this if you get the error or warning
+          <literal>The name org.a11y.Bus was not provided by any .service files</literal>.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkMerge [
+    (mkIf config.services.gnome.at-spi2-core.enable {
+      environment.systemPackages = [ pkgs.at-spi2-core ];
+      services.dbus.packages = [ pkgs.at-spi2-core ];
+      systemd.packages = [ pkgs.at-spi2-core ];
+    })
+
+    (mkIf (!config.services.gnome.at-spi2-core.enable) {
+      environment.variables.NO_AT_BRIDGE = "1";
+    })
+  ];
+}
diff --git a/nixos/modules/services/desktops/gnome/chrome-gnome-shell.nix b/nixos/modules/services/desktops/gnome/chrome-gnome-shell.nix
new file mode 100644
index 00000000000..15c5bfbd821
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/chrome-gnome-shell.nix
@@ -0,0 +1,41 @@
+# Chrome GNOME Shell native host connector.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "chrome-gnome-shell" "enable" ]
+      [ "services" "gnome" "chrome-gnome-shell" "enable" ]
+    )
+  ];
+
+  ###### interface
+  options = {
+    services.gnome.chrome-gnome-shell.enable = mkEnableOption ''
+      Chrome GNOME Shell native host connector, a DBus service
+      allowing to install GNOME Shell extensions from a web browser.
+    '';
+  };
+
+
+  ###### implementation
+  config = mkIf config.services.gnome.chrome-gnome-shell.enable {
+    environment.etc = {
+      "chromium/native-messaging-hosts/org.gnome.chrome_gnome_shell.json".source = "${pkgs.chrome-gnome-shell}/etc/chromium/native-messaging-hosts/org.gnome.chrome_gnome_shell.json";
+      "opt/chrome/native-messaging-hosts/org.gnome.chrome_gnome_shell.json".source = "${pkgs.chrome-gnome-shell}/etc/opt/chrome/native-messaging-hosts/org.gnome.chrome_gnome_shell.json";
+    };
+
+    environment.systemPackages = [ pkgs.chrome-gnome-shell ];
+
+    services.dbus.packages = [ pkgs.chrome-gnome-shell ];
+
+    nixpkgs.config.firefox.enableGnomeExtensions = true;
+  };
+}
diff --git a/nixos/modules/services/desktops/gnome/evolution-data-server.nix b/nixos/modules/services/desktops/gnome/evolution-data-server.nix
new file mode 100644
index 00000000000..bd2242d9818
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/evolution-data-server.nix
@@ -0,0 +1,71 @@
+# Evolution Data Server daemon.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "evolution-data-server" "enable" ]
+      [ "services" "gnome" "evolution-data-server" "enable" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "evolution-data-server" "plugins" ]
+      [ "services" "gnome" "evolution-data-server" "plugins" ]
+    )
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.gnome.evolution-data-server = {
+      enable = mkEnableOption "Evolution Data Server, a collection of services for storing addressbooks and calendars.";
+      plugins = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        description = "Plugins for Evolution Data Server.";
+      };
+    };
+    programs.evolution = {
+      enable = mkEnableOption "Evolution, a Personal information management application that provides integrated mail, calendaring and address book functionality.";
+      plugins = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        example = literalExpression "[ pkgs.evolution-ews ]";
+        description = "Plugins for Evolution.";
+      };
+
+    };
+  };
+
+  ###### implementation
+
+  config =
+    let
+      bundle = pkgs.evolutionWithPlugins.override { inherit (config.services.gnome.evolution-data-server) plugins; };
+    in
+    mkMerge [
+      (mkIf config.services.gnome.evolution-data-server.enable {
+        environment.systemPackages = [ bundle ];
+
+        services.dbus.packages = [ bundle ];
+
+        systemd.packages = [ bundle ];
+      })
+      (mkIf config.programs.evolution.enable {
+        services.gnome.evolution-data-server = {
+          enable = true;
+          plugins = [ pkgs.evolution ] ++ config.programs.evolution.plugins;
+        };
+        services.gnome.gnome-keyring.enable = true;
+      })
+    ];
+}
diff --git a/nixos/modules/services/desktops/gnome/glib-networking.nix b/nixos/modules/services/desktops/gnome/glib-networking.nix
new file mode 100644
index 00000000000..1039605391a
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/glib-networking.nix
@@ -0,0 +1,45 @@
+# GLib Networking
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "glib-networking" "enable" ]
+      [ "services" "gnome" "glib-networking" "enable" ]
+    )
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.gnome.glib-networking = {
+
+      enable = mkEnableOption "network extensions for GLib";
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf config.services.gnome.glib-networking.enable {
+
+    services.dbus.packages = [ pkgs.glib-networking ];
+
+    systemd.packages = [ pkgs.glib-networking ];
+
+    environment.sessionVariables.GIO_EXTRA_MODULES = [ "${pkgs.glib-networking.out}/lib/gio/modules" ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/gnome/gnome-initial-setup.nix b/nixos/modules/services/desktops/gnome/gnome-initial-setup.nix
new file mode 100644
index 00000000000..9e9771cf541
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/gnome-initial-setup.nix
@@ -0,0 +1,98 @@
+# GNOME Initial Setup.
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  # GNOME initial setup's run is conditioned on whether
+  # the gnome-initial-setup-done file exists in XDG_CONFIG_HOME
+  # Because of this, every existing user will have initial setup
+  # running because they never ran it before.
+  #
+  # To prevent this we create the file if the users stateVersion
+  # is older than 20.03 (the release we added this module).
+
+  script = pkgs.writeScript "create-gis-stamp-files" ''
+    #!${pkgs.runtimeShell}
+    setup_done=$HOME/.config/gnome-initial-setup-done
+
+    echo "Creating g-i-s stamp file $setup_done ..."
+    cat - > $setup_done <<- EOF
+    yes
+    EOF
+  '';
+
+  createGisStampFilesAutostart = pkgs.writeTextFile rec {
+    name = "create-g-i-s-stamp-files";
+    destination = "/etc/xdg/autostart/${name}.desktop";
+    text = ''
+      [Desktop Entry]
+      Type=Application
+      Name=Create GNOME Initial Setup stamp files
+      Exec=${script}
+      StartupNotify=false
+      NoDisplay=true
+      OnlyShowIn=GNOME;
+      AutostartCondition=unless-exists gnome-initial-setup-done
+      X-GNOME-Autostart-Phase=EarlyInitialization
+    '';
+  };
+
+in
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-initial-setup" "enable" ]
+      [ "services" "gnome" "gnome-initial-setup" "enable" ]
+    )
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.gnome.gnome-initial-setup = {
+
+      enable = mkEnableOption "GNOME Initial Setup, a Simple, easy, and safe way to prepare a new system";
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.gnome.gnome-initial-setup.enable {
+
+    environment.systemPackages = [
+      pkgs.gnome.gnome-initial-setup
+    ]
+    ++ optional (versionOlder config.system.stateVersion "20.03") createGisStampFilesAutostart
+    ;
+
+    systemd.packages = [
+      pkgs.gnome.gnome-initial-setup
+    ];
+
+    systemd.user.targets."gnome-session".wants = [
+      "gnome-initial-setup-copy-worker.service"
+      "gnome-initial-setup-first-login.service"
+      "gnome-welcome-tour.service"
+    ];
+
+    systemd.user.targets."gnome-session@gnome-initial-setup".wants = [
+      "gnome-initial-setup.service"
+    ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/gnome/gnome-keyring.nix b/nixos/modules/services/desktops/gnome/gnome-keyring.nix
new file mode 100644
index 00000000000..d821da164be
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/gnome-keyring.nix
@@ -0,0 +1,63 @@
+# GNOME Keyring daemon.
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-keyring" "enable" ]
+      [ "services" "gnome" "gnome-keyring" "enable" ]
+    )
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.gnome.gnome-keyring = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable GNOME Keyring daemon, a service designed to
+          take care of the user's security credentials,
+          such as user names and passwords.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.gnome.gnome-keyring.enable {
+
+    environment.systemPackages = [ pkgs.gnome.gnome-keyring ];
+
+    services.dbus.packages = [ pkgs.gnome.gnome-keyring pkgs.gcr ];
+
+    xdg.portal.extraPortals = [ pkgs.gnome.gnome-keyring ];
+
+    security.pam.services.login.enableGnomeKeyring = true;
+
+    security.wrappers.gnome-keyring-daemon = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_ipc_lock=ep";
+      source = "${pkgs.gnome.gnome-keyring}/bin/gnome-keyring-daemon";
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/gnome/gnome-online-accounts.nix b/nixos/modules/services/desktops/gnome/gnome-online-accounts.nix
new file mode 100644
index 00000000000..01f7e3695cf
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/gnome-online-accounts.nix
@@ -0,0 +1,51 @@
+# GNOME Online Accounts daemon.
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-online-accounts" "enable" ]
+      [ "services" "gnome" "gnome-online-accounts" "enable" ]
+    )
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.gnome.gnome-online-accounts = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable GNOME Online Accounts daemon, a service that provides
+          a single sign-on framework for the GNOME desktop.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.gnome.gnome-online-accounts.enable {
+
+    environment.systemPackages = [ pkgs.gnome-online-accounts ];
+
+    services.dbus.packages = [ pkgs.gnome-online-accounts ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/gnome/gnome-online-miners.nix b/nixos/modules/services/desktops/gnome/gnome-online-miners.nix
new file mode 100644
index 00000000000..5f9039f68c4
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/gnome-online-miners.nix
@@ -0,0 +1,51 @@
+# GNOME Online Miners daemon.
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-online-miners" "enable" ]
+      [ "services" "gnome" "gnome-online-miners" "enable" ]
+    )
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.gnome.gnome-online-miners = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable GNOME Online Miners, a service that
+          crawls through your online content.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.gnome.gnome-online-miners.enable {
+
+    environment.systemPackages = [ pkgs.gnome.gnome-online-miners ];
+
+    services.dbus.packages = [ pkgs.gnome.gnome-online-miners ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/gnome/gnome-remote-desktop.nix b/nixos/modules/services/desktops/gnome/gnome-remote-desktop.nix
new file mode 100644
index 00000000000..b5573d2fc21
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/gnome-remote-desktop.nix
@@ -0,0 +1,32 @@
+# Remote desktop daemon using Pipewire.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-remote-desktop" "enable" ]
+      [ "services" "gnome" "gnome-remote-desktop" "enable" ]
+    )
+  ];
+
+  ###### interface
+  options = {
+    services.gnome.gnome-remote-desktop = {
+      enable = mkEnableOption "Remote Desktop support using Pipewire";
+    };
+  };
+
+  ###### implementation
+  config = mkIf config.services.gnome.gnome-remote-desktop.enable {
+    services.pipewire.enable = true;
+
+    systemd.packages = [ pkgs.gnome.gnome-remote-desktop ];
+  };
+}
diff --git a/nixos/modules/services/desktops/gnome/gnome-settings-daemon.nix b/nixos/modules/services/desktops/gnome/gnome-settings-daemon.nix
new file mode 100644
index 00000000000..9c68c9b76e9
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/gnome-settings-daemon.nix
@@ -0,0 +1,70 @@
+# GNOME Settings Daemon
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.gnome.gnome-settings-daemon;
+
+in
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  imports = [
+    (mkRemovedOptionModule
+      ["services" "gnome3" "gnome-settings-daemon" "package"]
+      "")
+
+    # Added 2021-05-07
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-settings-daemon" "enable" ]
+      [ "services" "gnome" "gnome-settings-daemon" "enable" ]
+    )
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.gnome.gnome-settings-daemon = {
+
+      enable = mkEnableOption "GNOME Settings Daemon";
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [
+      pkgs.gnome.gnome-settings-daemon
+    ];
+
+    services.udev.packages = [
+      pkgs.gnome.gnome-settings-daemon
+    ];
+
+    systemd.packages = [
+      pkgs.gnome.gnome-settings-daemon
+    ];
+
+    systemd.user.targets."gnome-session-x11-services".wants = [
+      "org.gnome.SettingsDaemon.XSettings.service"
+    ];
+
+    systemd.user.targets."gnome-session-x11-services-ready".wants = [
+      "org.gnome.SettingsDaemon.XSettings.service"
+    ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/gnome/gnome-user-share.nix b/nixos/modules/services/desktops/gnome/gnome-user-share.nix
new file mode 100644
index 00000000000..38256af309c
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/gnome-user-share.nix
@@ -0,0 +1,48 @@
+# GNOME User Share daemon.
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  imports = [
+    # Added 2021-05-07
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-user-share" "enable" ]
+      [ "services" "gnome" "gnome-user-share" "enable" ]
+    )
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.gnome.gnome-user-share = {
+
+      enable = mkEnableOption "GNOME User Share, a user-level file sharing service for GNOME";
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.gnome.gnome-user-share.enable {
+
+    environment.systemPackages = [
+      pkgs.gnome.gnome-user-share
+    ];
+
+    systemd.packages = [
+      pkgs.gnome.gnome-user-share
+    ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/gnome/rygel.nix b/nixos/modules/services/desktops/gnome/rygel.nix
new file mode 100644
index 00000000000..7ea9778fc40
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/rygel.nix
@@ -0,0 +1,44 @@
+# rygel service.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  imports = [
+    # Added 2021-05-07
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "rygel" "enable" ]
+      [ "services" "gnome" "rygel" "enable" ]
+    )
+  ];
+
+  ###### interface
+  options = {
+    services.gnome.rygel = {
+      enable = mkOption {
+        default = false;
+        description = ''
+          Whether to enable Rygel UPnP Mediaserver.
+
+          You will need to also allow UPnP connections in firewall, see the following <link xlink:href="https://github.com/NixOS/nixpkgs/pull/45045#issuecomment-416030795">comment</link>.
+        '';
+        type = types.bool;
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf config.services.gnome.rygel.enable {
+    environment.systemPackages = [ pkgs.gnome.rygel ];
+
+    services.dbus.packages = [ pkgs.gnome.rygel ];
+
+    systemd.packages = [ pkgs.gnome.rygel ];
+
+    environment.etc."rygel.conf".source = "${pkgs.gnome.rygel}/etc/rygel.conf";
+  };
+}
diff --git a/nixos/modules/services/desktops/gnome/sushi.nix b/nixos/modules/services/desktops/gnome/sushi.nix
new file mode 100644
index 00000000000..3133a3a0d98
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/sushi.nix
@@ -0,0 +1,50 @@
+# GNOME Sushi daemon.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  imports = [
+    # Added 2021-05-07
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "sushi" "enable" ]
+      [ "services" "gnome" "sushi" "enable" ]
+    )
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.gnome.sushi = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Sushi, a quick previewer for nautilus.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.gnome.sushi.enable {
+
+    environment.systemPackages = [ pkgs.gnome.sushi ];
+
+    services.dbus.packages = [ pkgs.gnome.sushi ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/gnome/tracker-miners.nix b/nixos/modules/services/desktops/gnome/tracker-miners.nix
new file mode 100644
index 00000000000..9351007d30b
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/tracker-miners.nix
@@ -0,0 +1,54 @@
+# Tracker Miners daemons.
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  imports = [
+    # Added 2021-05-07
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "tracker-miners" "enable" ]
+      [ "services" "gnome" "tracker-miners" "enable" ]
+    )
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.gnome.tracker-miners = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Tracker miners, indexing services for Tracker
+          search engine and metadata storage system.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf config.services.gnome.tracker-miners.enable {
+
+    environment.systemPackages = [ pkgs.tracker-miners ];
+
+    services.dbus.packages = [ pkgs.tracker-miners ];
+
+    systemd.packages = [ pkgs.tracker-miners ];
+
+    services.gnome.tracker.subcommandPackages = [ pkgs.tracker-miners ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/gnome/tracker.nix b/nixos/modules/services/desktops/gnome/tracker.nix
new file mode 100644
index 00000000000..fef399d0112
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/tracker.nix
@@ -0,0 +1,76 @@
+# Tracker daemon.
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gnome.tracker;
+in
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  imports = [
+    # Added 2021-05-07
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "tracker" "enable" ]
+      [ "services" "gnome" "tracker" "enable" ]
+    )
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.gnome.tracker = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Tracker services, a search engine,
+          search tool and metadata storage system.
+        '';
+      };
+
+      subcommandPackages = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        internal = true;
+        description = ''
+          List of packages containing tracker3 subcommands.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.tracker ];
+
+    services.dbus.packages = [ pkgs.tracker ];
+
+    systemd.packages = [ pkgs.tracker ];
+
+    environment.variables = {
+      TRACKER_CLI_SUBCOMMANDS_DIR =
+        let
+          subcommandPackagesTree = pkgs.symlinkJoin {
+            name = "tracker-with-subcommands-${pkgs.tracker.version}";
+            paths = [ pkgs.tracker ] ++ cfg.subcommandPackages;
+          };
+        in
+        "${subcommandPackagesTree}/libexec/tracker3";
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/gsignond.nix b/nixos/modules/services/desktops/gsignond.nix
new file mode 100644
index 00000000000..465acd73fa6
--- /dev/null
+++ b/nixos/modules/services/desktops/gsignond.nix
@@ -0,0 +1,45 @@
+# Accounts-SSO gSignOn daemon
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  package = pkgs.gsignond.override { plugins = config.services.gsignond.plugins; };
+in
+{
+
+  meta.maintainers = teams.pantheon.members;
+
+  ###### interface
+
+  options = {
+
+    services.gsignond = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable gSignOn daemon, a DBus service
+          which performs user authentication on behalf of its clients.
+        '';
+      };
+
+      plugins = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        description = ''
+          What plugins to use with the gSignOn daemon.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf config.services.gsignond.enable {
+    environment.etc."gsignond.conf".source = "${package}/etc/gsignond.conf";
+    services.dbus.packages = [ package ];
+  };
+
+}
diff --git a/nixos/modules/services/desktops/gvfs.nix b/nixos/modules/services/desktops/gvfs.nix
new file mode 100644
index 00000000000..1aa64ea37db
--- /dev/null
+++ b/nixos/modules/services/desktops/gvfs.nix
@@ -0,0 +1,64 @@
+# GVfs
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.gvfs;
+
+in
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  # Added 2019-08-19
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gvfs" "enable" ]
+      [ "services" "gvfs" "enable" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.gvfs = {
+
+      enable = mkEnableOption "GVfs, a userspace virtual filesystem";
+
+      # gvfs can be built with multiple configurations
+      package = mkOption {
+        type = types.package;
+        default = pkgs.gnome.gvfs;
+        defaultText = literalExpression "pkgs.gnome.gvfs";
+        description = "Which GVfs package to use.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ cfg.package ];
+
+    services.dbus.packages = [ cfg.package ];
+
+    systemd.packages = [ cfg.package ];
+
+    services.udev.packages = [ pkgs.libmtp.out ];
+
+    # Needed for unwrapped applications
+    environment.sessionVariables.GIO_EXTRA_MODULES = [ "${cfg.package}/lib/gio/modules" ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/malcontent.nix b/nixos/modules/services/desktops/malcontent.nix
new file mode 100644
index 00000000000..1fbeb17e6ae
--- /dev/null
+++ b/nixos/modules/services/desktops/malcontent.nix
@@ -0,0 +1,40 @@
+# Malcontent daemon.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.malcontent = {
+
+      enable = mkEnableOption "Malcontent, parental control support for applications";
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.malcontent.enable {
+
+    environment.systemPackages = with pkgs; [
+      malcontent
+      malcontent-ui
+    ];
+
+    services.dbus.packages = [
+      # D-Bus services are in `out`, not the default `bin` output that would be picked up by `makeDbusConf`.
+      pkgs.malcontent.out
+    ];
+
+    services.accounts-daemon.enable = true;
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/neard.nix b/nixos/modules/services/desktops/neard.nix
new file mode 100644
index 00000000000..9b0f8d1b3a7
--- /dev/null
+++ b/nixos/modules/services/desktops/neard.nix
@@ -0,0 +1,23 @@
+# neard service.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  ###### interface
+  options = {
+    services.neard = {
+      enable = mkEnableOption "neard, NFC daemon";
+    };
+  };
+
+
+  ###### implementation
+  config = mkIf config.services.neard.enable {
+    environment.systemPackages = [ pkgs.neard ];
+
+    services.dbus.packages = [ pkgs.neard ];
+
+    systemd.packages = [ pkgs.neard ];
+  };
+}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json b/nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json
new file mode 100644
index 00000000000..9aa51b61431
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json
@@ -0,0 +1,39 @@
+{
+  "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
new file mode 100644
index 00000000000..71294a0e78a
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/daemon/client.conf.json
@@ -0,0 +1,31 @@
+{
+  "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/jack.conf.json b/nixos/modules/services/desktops/pipewire/daemon/jack.conf.json
new file mode 100644
index 00000000000..128178bfa02
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/daemon/jack.conf.json
@@ -0,0 +1,38 @@
+{
+  "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
new file mode 100644
index 00000000000..c7f58fd5799
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/daemon/minimal.conf.json
@@ -0,0 +1,118 @@
+{
+  "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,
+          "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,
+          "position": "unknown"
+        }
+      }
+    }
+  ],
+  "context.exec": []
+}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json b/nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json
new file mode 100644
index 00000000000..df0f62556df
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json
@@ -0,0 +1,99 @@
+{
+  "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": {
+        "server.address": [
+          "unix:native"
+        ],
+        "vm.overrides": {
+          "pulse.min.quantum": "1024/48000"
+        }
+      }
+    }
+  ],
+  "context.exec": [
+    {
+      "path": "pactl",
+      "args": "load-module module-always-sink"
+    }
+  ],
+  "stream.properties": {},
+  "pulse.rules": [
+    {
+      "matches": [
+        {}
+      ],
+      "actions": {
+        "update-props": {}
+      }
+    },
+    {
+      "matches": [
+        {
+          "application.process.binary": "teams"
+        },
+        {
+          "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
new file mode 100644
index 00000000000..7c79f0168c0
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/daemon/pipewire.conf.json
@@ -0,0 +1,96 @@
+{
+  "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",
+    "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
new file mode 100644
index 00000000000..53fc9cc9634
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/media-session/alsa-monitor.conf.json
@@ -0,0 +1,34 @@
+{
+  "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
new file mode 100644
index 00000000000..6d1c23e8256
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/media-session/bluez-monitor.conf.json
@@ -0,0 +1,36 @@
+{
+  "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
new file mode 100644
index 00000000000..4b4e302af38
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/media-session/media-session.conf.json
@@ -0,0 +1,68 @@
+{
+  "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
new file mode 100644
index 00000000000..b08cba1b604
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/media-session/v4l2-monitor.conf.json
@@ -0,0 +1,30 @@
+{
+  "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
new file mode 100644
index 00000000000..109c91134b9
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix
@@ -0,0 +1,136 @@
+# 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 = "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 = ''
+          The pipewire-media-session derivation to use.
+        '';
+      };
+
+      config = {
+        media-session = mkOption {
+          type = json.type;
+          description = ''
+            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 = {};
+        };
+
+        alsa-monitor = mkOption {
+          type = json.type;
+          description = ''
+            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 = {};
+        };
+
+        bluez-monitor = mkOption {
+          type = json.type;
+          description = ''
+            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 = {};
+        };
+
+        v4l2-monitor = mkOption {
+          type = json.type;
+          description = ''
+            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 = {};
+        };
+      };
+    };
+  };
+
+  ###### 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-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
new file mode 100644
index 00000000000..59e9342a6ea
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/pipewire.nix
@@ -0,0 +1,247 @@
+# pipewire service.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  json = pkgs.formats.json {};
+  cfg = config.services.pipewire;
+  enable32BitAlsaPlugins = cfg.alsa.support32Bit
+                           && pkgs.stdenv.isx86_64
+                           && pkgs.pkgsi686Linux.pipewire != null;
+
+  # The package doesn't output to $out/lib/pipewire directly so that the
+  # overlays can use the outputs to replace the originals in FHS environments.
+  #
+  # This doesn't work in general because of missing development information.
+  jack-libs = pkgs.runCommand "jack-libs" {} ''
+    mkdir -p "$out/lib"
+    ln -s "${cfg.package.jack}/lib" "$out/lib/pipewire"
+  '';
+
+  # 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;
+  };
+
+  ###### interface
+  options = {
+    services.pipewire = {
+      enable = mkEnableOption "pipewire service";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.pipewire;
+        defaultText = literalExpression "pkgs.pipewire";
+        description = ''
+          The pipewire derivation to use.
+        '';
+      };
+
+      socketActivation = mkOption {
+        default = true;
+        type = types.bool;
+        description = ''
+          Automatically run pipewire when connections are made to the pipewire socket.
+        '';
+      };
+
+      config = {
+        client = mkOption {
+          type = json.type;
+          default = {};
+          description = ''
+            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 = ''
+            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 = ''
+            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 = ''
+            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 = ''
+            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
+          '';
+        };
+      };
+
+      alsa = {
+        enable = mkEnableOption "ALSA support";
+        support32Bit = mkEnableOption "32-bit ALSA support on 64-bit systems";
+      };
+
+      jack = {
+        enable = mkEnableOption "JACK audio emulation";
+      };
+
+      pulse = {
+        enable = mkEnableOption "PulseAudio server emulation";
+      };
+
+      systemWide = lib.mkOption {
+        type = lib.types.bool;
+        default = false;
+        description = ''
+          If true, a system-wide PipeWire service and socket is enabled
+          allowing all users in the "pipewire" group to use it simultaneously.
+          If false, then user units are used instead, restricting access to
+          only one user.
+
+          Enabling system-wide PipeWire is however not recommended and disabled
+          by default according to
+          https://github.com/PipeWire/pipewire/blob/master/NEWS
+        '';
+      };
+
+    };
+  };
+
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.pulse.enable -> !config.hardware.pulseaudio.enable;
+        message = "PipeWire based PulseAudio server emulation replaces PulseAudio. This option requires `hardware.pulseaudio.enable` to be set to false";
+      }
+      {
+        assertion = cfg.jack.enable -> !config.services.jack.jackd.enable;
+        message = "PipeWire based JACK emulation doesn't use the JACK service. This option requires `services.jack.jackd.enable` to be set to false";
+      }
+    ];
+
+    environment.systemPackages = [ cfg.package ]
+                                 ++ lib.optional cfg.jack.enable jack-libs;
+
+    systemd.packages = [ cfg.package ]
+                       ++ lib.optional cfg.pulse.enable cfg.package.pulse;
+
+    # PipeWire depends on DBUS but doesn't list it. Without this booting
+    # into a terminal results in the service crashing with an error.
+    systemd.services.pipewire.bindsTo = [ "dbus.service" ];
+    systemd.user.services.pipewire.bindsTo = [ "dbus.service" ];
+
+    # Enable either system or user units.  Note that for pipewire-pulse there
+    # are only user units, which work in both cases.
+    systemd.sockets.pipewire.enable = cfg.systemWide;
+    systemd.services.pipewire.enable = cfg.systemWide;
+    systemd.user.sockets.pipewire.enable = !cfg.systemWide;
+    systemd.user.services.pipewire.enable = !cfg.systemWide;
+
+    systemd.sockets.pipewire.wantedBy = lib.mkIf cfg.socketActivation [ "sockets.target" ];
+    systemd.user.sockets.pipewire.wantedBy = lib.mkIf cfg.socketActivation [ "sockets.target" ];
+    systemd.user.sockets.pipewire-pulse.wantedBy = lib.mkIf (cfg.socketActivation && cfg.pulse.enable) ["sockets.target"];
+
+    services.udev.packages = [ cfg.package ];
+
+    # If any paths are updated here they must also be updated in the package test.
+    environment.etc."alsa/conf.d/49-pipewire-modules.conf" = mkIf cfg.alsa.enable {
+      text = ''
+        pcm_type.pipewire {
+          libs.native = ${cfg.package.lib}/lib/alsa-lib/libasound_module_pcm_pipewire.so ;
+          ${optionalString enable32BitAlsaPlugins
+            "libs.32Bit = ${pkgs.pkgsi686Linux.pipewire.lib}/lib/alsa-lib/libasound_module_pcm_pipewire.so ;"}
+        }
+        ctl_type.pipewire {
+          libs.native = ${cfg.package.lib}/lib/alsa-lib/libasound_module_ctl_pipewire.so ;
+          ${optionalString enable32BitAlsaPlugins
+            "libs.32Bit = ${pkgs.pkgsi686Linux.pipewire.lib}/lib/alsa-lib/libasound_module_ctl_pipewire.so ;"}
+        }
+      '';
+    };
+    environment.etc."alsa/conf.d/50-pipewire.conf" = mkIf cfg.alsa.enable {
+      source = "${cfg.package}/share/alsa/alsa.conf.d/50-pipewire.conf";
+    };
+    environment.etc."alsa/conf.d/99-pipewire-default.conf" = mkIf cfg.alsa.enable {
+      source = "${cfg.package}/share/alsa/alsa.conf.d/99-pipewire-default.conf";
+    };
+
+    environment.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" = {
+      source = json.generate "pipewire-pulse.conf" configs.pipewire-pulse;
+    };
+
+    environment.sessionVariables.LD_LIBRARY_PATH =
+      lib.optional cfg.jack.enable "${cfg.package.jack}/lib";
+
+    users = lib.mkIf cfg.systemWide {
+      users.pipewire = {
+        uid = config.ids.uids.pipewire;
+        group = "pipewire";
+        extraGroups = [
+          "audio"
+          "video"
+        ] ++ lib.optional config.security.rtkit.enable "rtkit";
+        description = "Pipewire system service user";
+        isSystemUser = true;
+      };
+      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";
+  };
+}
diff --git a/nixos/modules/services/desktops/pipewire/wireplumber.nix b/nixos/modules/services/desktops/pipewire/wireplumber.nix
new file mode 100644
index 00000000000..52ec17b95db
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/wireplumber.nix
@@ -0,0 +1,44 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.pipewire.wireplumber;
+in
+{
+  meta.maintainers = [ lib.maintainers.k900 ];
+
+  options = {
+    services.pipewire.wireplumber = {
+      enable = lib.mkOption {
+        type = lib.types.bool;
+        default = config.services.pipewire.enable;
+        defaultText = lib.literalExpression "config.services.pipewire.enable";
+        description = "Whether to enable Wireplumber, a modular session / policy manager for PipeWire";
+      };
+
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.wireplumber;
+        defaultText = lib.literalExpression "pkgs.wireplumber";
+        description = "The wireplumber derivation to use.";
+      };
+    };
+  };
+
+  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.";
+      }
+    ];
+
+    environment.systemPackages = [ cfg.package ];
+    systemd.packages = [ cfg.package ];
+
+    systemd.services.wireplumber.enable = config.services.pipewire.systemWide;
+    systemd.user.services.wireplumber.enable = !config.services.pipewire.systemWide;
+
+    systemd.services.wireplumber.wantedBy = [ "pipewire.service" ];
+    systemd.user.services.wireplumber.wantedBy = [ "pipewire.service" ];
+  };
+}
diff --git a/nixos/modules/services/desktops/profile-sync-daemon.nix b/nixos/modules/services/desktops/profile-sync-daemon.nix
new file mode 100644
index 00000000000..6206295272f
--- /dev/null
+++ b/nixos/modules/services/desktops/profile-sync-daemon.nix
@@ -0,0 +1,77 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.psd;
+in {
+  options.services.psd = with types; {
+    enable = mkOption {
+      type = bool;
+      default = false;
+      description = ''
+        Whether to enable the Profile Sync daemon.
+      '';
+    };
+    resyncTimer = mkOption {
+      type = str;
+      default = "1h";
+      example = "1h 30min";
+      description = ''
+        The amount of time to wait before syncing browser profiles back to the
+        disk.
+
+        Takes a systemd.unit time span. The time unit defaults to seconds if
+        omitted.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd = {
+      user = {
+        services = {
+          psd = {
+            enable = true;
+            description = "Profile Sync daemon";
+            wants = [ "psd-resync.service" ];
+            wantedBy = [ "default.target" ];
+            path = with pkgs; [ rsync kmod gawk nettools util-linux profile-sync-daemon ];
+            unitConfig = {
+              RequiresMountsFor = [ "/home/" ];
+            };
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = "yes";
+              ExecStart = "${pkgs.profile-sync-daemon}/bin/profile-sync-daemon sync";
+              ExecStop = "${pkgs.profile-sync-daemon}/bin/profile-sync-daemon unsync";
+            };
+          };
+
+          psd-resync = {
+            enable = true;
+            description = "Timed profile resync";
+            after = [ "psd.service" ];
+            wants = [ "psd-resync.timer" ];
+            partOf = [ "psd.service" ];
+            wantedBy = [ "default.target" ];
+            path = with pkgs; [ rsync kmod gawk nettools util-linux profile-sync-daemon ];
+            serviceConfig = {
+              Type = "oneshot";
+              ExecStart = "${pkgs.profile-sync-daemon}/bin/profile-sync-daemon resync";
+            };
+          };
+        };
+
+        timers.psd-resync = {
+          description = "Timer for profile sync daemon - ${cfg.resyncTimer}";
+          partOf = [ "psd-resync.service" "psd.service" ];
+
+          timerConfig = {
+            OnUnitActiveSec = "${cfg.resyncTimer}";
+          };
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/desktops/system-config-printer.nix b/nixos/modules/services/desktops/system-config-printer.nix
new file mode 100644
index 00000000000..09c68c587b4
--- /dev/null
+++ b/nixos/modules/services/desktops/system-config-printer.nix
@@ -0,0 +1,41 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.system-config-printer = {
+
+      enable = mkEnableOption "system-config-printer, a service for CUPS administration used by printing interfaces";
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.system-config-printer.enable {
+
+    services.dbus.packages = [
+      pkgs.system-config-printer
+    ];
+
+    systemd.packages = [
+      pkgs.system-config-printer
+    ];
+
+    services.udev.packages = [
+      pkgs.system-config-printer
+    ];
+
+    # for $out/bin/install-printer-driver
+    services.packagekit.enable = true;
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/telepathy.nix b/nixos/modules/services/desktops/telepathy.nix
new file mode 100644
index 00000000000..b5f6a5fcbcf
--- /dev/null
+++ b/nixos/modules/services/desktops/telepathy.nix
@@ -0,0 +1,48 @@
+# Telepathy daemon.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  ###### interface
+
+  options = {
+
+    services.telepathy = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Telepathy service, a communications framework
+          that enables real-time communication via pluggable protocol backends.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.telepathy.enable {
+
+    environment.systemPackages = [ pkgs.telepathy-mission-control ];
+
+    services.dbus.packages = [ pkgs.telepathy-mission-control ];
+
+    # Enable runtime optional telepathy in gnome-shell
+    services.xserver.desktopManager.gnome.sessionPath = with pkgs; [
+      telepathy-glib
+      telepathy-logger
+    ];
+  };
+
+}
diff --git a/nixos/modules/services/desktops/tumbler.nix b/nixos/modules/services/desktops/tumbler.nix
new file mode 100644
index 00000000000..f5341df2f7a
--- /dev/null
+++ b/nixos/modules/services/desktops/tumbler.nix
@@ -0,0 +1,52 @@
+# Tumbler
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.tumbler;
+
+in
+
+{
+
+  imports = [
+    (mkRemovedOptionModule
+      [ "services" "tumbler" "package" ]
+      "")
+  ];
+
+  meta = with lib; {
+    maintainers = with maintainers; [ ] ++ teams.pantheon.members;
+  };
+
+  ###### interface
+
+  options = {
+
+    services.tumbler = {
+
+      enable = mkEnableOption "Tumbler, A D-Bus thumbnailer service";
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = with pkgs.xfce; [
+      tumbler
+    ];
+
+    services.dbus.packages = with pkgs.xfce; [
+      tumbler
+    ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/zeitgeist.nix b/nixos/modules/services/desktops/zeitgeist.nix
new file mode 100644
index 00000000000..297fd1d3ff2
--- /dev/null
+++ b/nixos/modules/services/desktops/zeitgeist.nix
@@ -0,0 +1,31 @@
+# Zeitgeist
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  meta = with lib; {
+    maintainers = with maintainers; [ ] ++ teams.pantheon.members;
+  };
+
+  ###### interface
+
+  options = {
+    services.zeitgeist = {
+      enable = mkEnableOption "zeitgeist";
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf config.services.zeitgeist.enable {
+
+    environment.systemPackages = [ pkgs.zeitgeist ];
+
+    services.dbus.packages = [ pkgs.zeitgeist ];
+
+    systemd.packages = [ pkgs.zeitgeist ];
+  };
+}
diff --git a/nixos/modules/services/development/blackfire.nix b/nixos/modules/services/development/blackfire.nix
new file mode 100644
index 00000000000..8564aabc6a3
--- /dev/null
+++ b/nixos/modules/services/development/blackfire.nix
@@ -0,0 +1,60 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.blackfire-agent;
+
+  agentConfigFile = lib.generators.toINI {} {
+    blackfire =  cfg.settings;
+  };
+
+  agentSock = "blackfire/agent.sock";
+in {
+  meta = {
+    maintainers = pkgs.blackfire.meta.maintainers;
+    doc = ./blackfire.xml;
+  };
+
+  options = {
+    services.blackfire-agent = {
+      enable = lib.mkEnableOption "Blackfire profiler agent";
+      settings = lib.mkOption {
+        description = ''
+          See https://blackfire.io/docs/up-and-running/configuration/agent
+        '';
+        type = lib.types.submodule {
+          freeformType = with lib.types; attrsOf str;
+
+          options = {
+            server-id = lib.mkOption {
+              type = lib.types.str;
+              description = ''
+                Sets the server id used to authenticate with Blackfire
+
+                You can find your personal server-id at https://blackfire.io/my/settings/credentials
+              '';
+            };
+
+            server-token = lib.mkOption {
+              type = lib.types.str;
+              description = ''
+                Sets the server token used to authenticate with Blackfire
+
+                You can find your personal server-token at https://blackfire.io/my/settings/credentials
+              '';
+            };
+          };
+        };
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment.etc."blackfire/agent".text = agentConfigFile;
+
+    services.blackfire-agent.settings.socket = "unix:///run/${agentSock}";
+
+    systemd.packages = [
+      pkgs.blackfire
+    ];
+  };
+}
diff --git a/nixos/modules/services/development/blackfire.xml b/nixos/modules/services/development/blackfire.xml
new file mode 100644
index 00000000000..cecd249dda4
--- /dev/null
+++ b/nixos/modules/services/development/blackfire.xml
@@ -0,0 +1,46 @@
+<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/bloop.nix b/nixos/modules/services/development/bloop.nix
new file mode 100644
index 00000000000..c1180a8bbdd
--- /dev/null
+++ b/nixos/modules/services/development/bloop.nix
@@ -0,0 +1,54 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.bloop;
+
+in {
+
+  options.services.bloop = {
+    extraOptions = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = [
+        "-J-Xmx2G"
+        "-J-XX:MaxInlineLevel=20"
+        "-J-XX:+UseParallelGC"
+      ];
+      description = ''
+        Specifies additional command line argument to pass to bloop
+        java process.
+      '';
+    };
+
+    install = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to install a user service for the Bloop server.
+
+        The service must be manually started for each user with
+        "systemctl --user start bloop".
+      '';
+    };
+  };
+
+  config = mkIf (cfg.install) {
+    systemd.user.services.bloop = {
+      description = "Bloop Scala build server";
+
+      environment = {
+        PATH = mkForce "${makeBinPath [ config.programs.java.package ]}";
+      };
+      serviceConfig = {
+        Type        = "simple";
+        ExecStart   = "${pkgs.bloop}/bin/bloop server";
+        Restart     = "always";
+      };
+    };
+
+    environment.systemPackages = [ pkgs.bloop ];
+  };
+}
diff --git a/nixos/modules/services/development/distccd.nix b/nixos/modules/services/development/distccd.nix
new file mode 100644
index 00000000000..9f6d5c813c4
--- /dev/null
+++ b/nixos/modules/services/development/distccd.nix
@@ -0,0 +1,155 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.distccd;
+in
+{
+  options = {
+    services.distccd = {
+      enable = mkEnableOption "distccd";
+
+      allowedClients = mkOption {
+        type = types.listOf types.str;
+        default = [ "127.0.0.1" ];
+        example = [ "127.0.0.1" "192.168.0.0/24" "10.0.0.0/24" ];
+        description = ''
+          Client IPs which are allowed to connect to distccd in CIDR notation.
+
+          Anyone who can connect to the distccd server can run arbitrary
+          commands on that system as the distcc user, therefore you should use
+          this judiciously.
+        '';
+      };
+
+      jobTimeout = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          Maximum duration, in seconds, of a single compilation request.
+        '';
+      };
+
+      logLevel = mkOption {
+        type = types.nullOr (types.enum [ "critical" "error" "warning" "notice" "info" "debug" ]);
+        default = "warning";
+        description = ''
+          Set the minimum severity of error that will be included in the log
+          file. Useful if you only want to see error messages rather than an
+          entry for each connection.
+        '';
+      };
+
+      maxJobs = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          Maximum number of tasks distccd should execute at any time.
+        '';
+      };
+
+
+      nice = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          Niceness of the compilation tasks.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Opens the specified TCP port for distcc.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.distcc;
+        defaultText = literalExpression "pkgs.distcc";
+        description = ''
+          The distcc package to use.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 3632;
+        description = ''
+          The TCP port which distccd will listen on.
+        '';
+      };
+
+      stats = {
+        enable = mkEnableOption "statistics reporting via HTTP server";
+        port = mkOption {
+          type = types.port;
+          default = 3633;
+          description = ''
+            The TCP port which the distccd statistics HTTP server will listen
+            on.
+          '';
+        };
+      };
+
+      zeroconf = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to register via mDNS/DNS-SD
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ]
+        ++ optionals cfg.stats.enable [ cfg.stats.port ];
+    };
+
+    systemd.services.distccd = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      description = "Distributed C, C++ and Objective-C compiler";
+      documentation = [ "man:distccd(1)" ];
+
+      serviceConfig = {
+        User = "distcc";
+        Group = "distcc";
+        # FIXME: I'd love to get rid of `--enable-tcp-insecure` here, but I'm
+        # not sure how I'm supposed to get distccd to "accept" running a binary
+        # (the compiler) that's outside of /usr/lib.
+        ExecStart = pkgs.writeShellScript "start-distccd" ''
+          export PATH="${pkgs.distccMasquerade}/bin"
+          ${cfg.package}/bin/distccd \
+            --no-detach \
+            --daemon \
+            --enable-tcp-insecure \
+            --port ${toString cfg.port} \
+            ${optionalString (cfg.jobTimeout != null) "--job-lifetime ${toString cfg.jobTimeout}"} \
+            ${optionalString (cfg.logLevel != null) "--log-level ${cfg.logLevel}"} \
+            ${optionalString (cfg.maxJobs != null) "--jobs ${toString cfg.maxJobs}"} \
+            ${optionalString (cfg.nice != null) "--nice ${toString cfg.nice}"} \
+            ${optionalString cfg.stats.enable "--stats"} \
+            ${optionalString cfg.stats.enable "--stats-port ${toString cfg.stats.port}"} \
+            ${optionalString cfg.zeroconf "--zeroconf"} \
+            ${concatMapStrings (c: "--allow ${c} ") cfg.allowedClients}
+        '';
+      };
+    };
+
+    users = {
+      groups.distcc.gid = config.ids.gids.distcc;
+      users.distcc = {
+        description = "distccd user";
+        group = "distcc";
+        uid = config.ids.uids.distcc;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/development/hoogle.nix b/nixos/modules/services/development/hoogle.nix
new file mode 100644
index 00000000000..7c2a1c8e162
--- /dev/null
+++ b/nixos/modules/services/development/hoogle.nix
@@ -0,0 +1,81 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.hoogle;
+
+  hoogleEnv = pkgs.buildEnv {
+    name = "hoogle";
+    paths = [ (cfg.haskellPackages.ghcWithHoogle cfg.packages) ];
+  };
+
+in {
+
+  options.services.hoogle = {
+    enable = mkEnableOption "Haskell documentation server";
+
+    port = mkOption {
+      type = types.port;
+      default = 8080;
+      description = ''
+        Port number Hoogle will be listening to.
+      '';
+    };
+
+    packages = mkOption {
+      type = types.functionTo (types.listOf types.package);
+      default = hp: [];
+      defaultText = literalExpression "hp: []";
+      example = literalExpression "hp: with hp; [ text lens ]";
+      description = ''
+        The Haskell packages to generate documentation for.
+
+        The option value is a function that takes the package set specified in
+        the <varname>haskellPackages</varname> option as its sole parameter and
+        returns a list of packages.
+      '';
+    };
+
+    haskellPackages = mkOption {
+      description = "Which haskell package set to use.";
+      type = types.attrs;
+      default = pkgs.haskellPackages;
+      defaultText = literalExpression "pkgs.haskellPackages";
+    };
+
+    home = mkOption {
+      type = types.str;
+      description = "Url for hoogle logo";
+      default = "https://hoogle.haskell.org";
+    };
+
+    host = mkOption {
+      type = types.str;
+      description = "Set the host to bind on.";
+      default = "127.0.0.1";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.hoogle = {
+      description = "Haskell documentation server";
+
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Restart = "always";
+        ExecStart = ''${hoogleEnv}/bin/hoogle server --local --port ${toString cfg.port} --home ${cfg.home} --host ${cfg.host}'';
+
+        DynamicUser = true;
+
+        ProtectHome = true;
+
+        RuntimeDirectory = "hoogle";
+        WorkingDirectory = "%t/hoogle";
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/services/development/jupyter/default.nix b/nixos/modules/services/development/jupyter/default.nix
new file mode 100644
index 00000000000..bebb3c3f13f
--- /dev/null
+++ b/nixos/modules/services/development/jupyter/default.nix
@@ -0,0 +1,201 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.jupyter;
+
+  package = cfg.package;
+
+  kernels = (pkgs.jupyter-kernel.create  {
+    definitions = if cfg.kernels != null
+      then cfg.kernels
+      else  pkgs.jupyter-kernel.default;
+  });
+
+  notebookConfig = pkgs.writeText "jupyter_config.py" ''
+    ${cfg.notebookConfig}
+
+    c.NotebookApp.password = ${cfg.password}
+  '';
+
+in {
+  meta.maintainers = with maintainers; [ aborsu ];
+
+  options.services.jupyter = {
+    enable = mkEnableOption "Jupyter development server";
+
+    ip = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = ''
+        IP address Jupyter will be listening on.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      # NOTE: We don't use top-level jupyter because we don't
+      # want to pass in JUPYTER_PATH but use .environment instead,
+      # saving a rebuild.
+      default = pkgs.python3.pkgs.notebook;
+      defaultText = literalExpression "pkgs.python3.pkgs.notebook";
+      description = ''
+        Jupyter package to use.
+      '';
+    };
+
+    command = mkOption {
+      type = types.str;
+      default = "jupyter-notebook";
+      example = "jupyter-lab";
+      description = ''
+        Which command the service runs. Note that not all jupyter packages
+        have all commands, e.g. jupyter-lab isn't present in the default package.
+       '';
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = 8888;
+      description = ''
+        Port number Jupyter will be listening on.
+      '';
+    };
+
+    notebookDir = mkOption {
+      type = types.str;
+      default = "~/";
+      description = ''
+        Root directory for notebooks.
+      '';
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = "jupyter";
+      description = ''
+        Name of the user used to run the jupyter service.
+        For security reason, jupyter should really not be run as root.
+        If not set (jupyter), the service will create a jupyter user with appropriate settings.
+      '';
+      example = "aborsu";
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "jupyter";
+      description = ''
+        Name of the group used to run the jupyter service.
+        Use this if you want to create a group of users that are able to view the notebook directory's content.
+      '';
+      example = "users";
+    };
+
+    password = mkOption {
+      type = types.str;
+      description = ''
+        Password to use with notebook.
+        Can be generated using:
+          In [1]: from notebook.auth import passwd
+          In [2]: passwd('test')
+          Out[2]: 'sha1:1b961dc713fb:88483270a63e57d18d43cf337e629539de1436ba'
+          NOTE: you need to keep the single quote inside the nix string.
+        Or you can use a python oneliner:
+          "open('/path/secret_file', 'r', encoding='utf8').read().strip()"
+        It will be interpreted at the end of the notebookConfig.
+      '';
+      example = "'sha1:1b961dc713fb:88483270a63e57d18d43cf337e629539de1436ba'";
+    };
+
+    notebookConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Raw jupyter config.
+      '';
+    };
+
+    kernels = mkOption {
+      type = types.nullOr (types.attrsOf(types.submodule (import ./kernel-options.nix {
+        inherit lib;
+      })));
+
+      default = null;
+      example = literalExpression ''
+        {
+          python3 = let
+            env = (pkgs.python3.withPackages (pythonPackages: with pythonPackages; [
+                    ipykernel
+                    pandas
+                    scikit-learn
+                  ]));
+          in {
+            displayName = "Python 3 for machine learning";
+            argv = [
+              "''${env.interpreter}"
+              "-m"
+              "ipykernel_launcher"
+              "-f"
+              "{connection_file}"
+            ];
+            language = "python";
+            logo32 = "''${env.sitePackages}/ipykernel/resources/logo-32x32.png";
+            logo64 = "''${env.sitePackages}/ipykernel/resources/logo-64x64.png";
+          };
+        }
+      '';
+      description = "Declarative kernel config
+
+      Kernels can be declared in any language that supports and has the required
+      dependencies to communicate with a jupyter server.
+      In python's case, it means that ipykernel package must always be included in
+      the list of packages of the targeted environment.
+      ";
+    };
+  };
+
+  config = mkMerge [
+    (mkIf cfg.enable  {
+      systemd.services.jupyter = {
+        description = "Jupyter development server";
+
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        # TODO: Patch notebook so we can explicitly pass in a shell
+        path = [ pkgs.bash ]; # needed for sh in cell magic to work
+
+        environment = {
+          JUPYTER_PATH = toString kernels;
+        };
+
+        serviceConfig = {
+          Restart = "always";
+          ExecStart = ''${package}/bin/${cfg.command} \
+            --no-browser \
+            --ip=${cfg.ip} \
+            --port=${toString cfg.port} --port-retries 0 \
+            --notebook-dir=${cfg.notebookDir} \
+            --NotebookApp.config_file=${notebookConfig}
+          '';
+          User = cfg.user;
+          Group = cfg.group;
+          WorkingDirectory = "~";
+        };
+      };
+    })
+    (mkIf (cfg.enable && (cfg.group == "jupyter")) {
+      users.groups.jupyter = {};
+    })
+    (mkIf (cfg.enable && (cfg.user == "jupyter")) {
+      users.extraUsers.jupyter = {
+        extraGroups = [ cfg.group ];
+        home = "/var/lib/jupyter";
+        createHome = true;
+        useDefaultShell = true; # needed so that the user can start a terminal.
+      };
+    })
+  ];
+}
diff --git a/nixos/modules/services/development/jupyter/kernel-options.nix b/nixos/modules/services/development/jupyter/kernel-options.nix
new file mode 100644
index 00000000000..348a8b44b38
--- /dev/null
+++ b/nixos/modules/services/development/jupyter/kernel-options.nix
@@ -0,0 +1,60 @@
+# Options that can be used for creating a jupyter kernel.
+{lib }:
+
+with lib;
+
+{
+  options = {
+
+    displayName = mkOption {
+      type = types.str;
+      default = "";
+      example = literalExpression ''
+        "Python 3"
+        "Python 3 for Data Science"
+      '';
+      description = ''
+        Name that will be shown to the user.
+      '';
+    };
+
+    argv = mkOption {
+      type = types.listOf types.str;
+      example = [
+        "{customEnv.interpreter}"
+        "-m"
+        "ipykernel_launcher"
+        "-f"
+        "{connection_file}"
+      ];
+      description = ''
+        Command and arguments to start the kernel.
+      '';
+    };
+
+    language = mkOption {
+      type = types.str;
+      example = "python";
+      description = ''
+        Language of the environment. Typically the name of the binary.
+      '';
+    };
+
+    logo32 = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = literalExpression ''"''${env.sitePackages}/ipykernel/resources/logo-32x32.png"'';
+      description = ''
+        Path to 32x32 logo png.
+      '';
+    };
+    logo64 = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = literalExpression ''"''${env.sitePackages}/ipykernel/resources/logo-64x64.png"'';
+      description = ''
+        Path to 64x64 logo png.
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/development/jupyterhub/default.nix b/nixos/modules/services/development/jupyterhub/default.nix
new file mode 100644
index 00000000000..fa6b3be960a
--- /dev/null
+++ b/nixos/modules/services/development/jupyterhub/default.nix
@@ -0,0 +1,202 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.jupyterhub;
+
+  kernels = (pkgs.jupyter-kernel.create  {
+    definitions = if cfg.kernels != null
+      then cfg.kernels
+      else  pkgs.jupyter-kernel.default;
+  });
+
+  jupyterhubConfig = pkgs.writeText "jupyterhub_config.py" ''
+    c.JupyterHub.bind_url = "http://${cfg.host}:${toString cfg.port}"
+
+    c.JupyterHub.authenticator_class = "${cfg.authentication}"
+    c.JupyterHub.spawner_class = "${cfg.spawner}"
+
+    c.SystemdSpawner.default_url = '/lab'
+    c.SystemdSpawner.cmd = "${cfg.jupyterlabEnv}/bin/jupyterhub-singleuser"
+    c.SystemdSpawner.environment = {
+      'JUPYTER_PATH': '${kernels}'
+    }
+
+    ${cfg.extraConfig}
+  '';
+in {
+  meta.maintainers = with maintainers; [ costrouc ];
+
+  options.services.jupyterhub = {
+    enable = mkEnableOption "Jupyterhub development server";
+
+    authentication = mkOption {
+      type = types.str;
+      default = "jupyterhub.auth.PAMAuthenticator";
+      description = ''
+        Jupyterhub authentication to use
+
+        There are many authenticators available including: oauth, pam,
+        ldap, kerberos, etc.
+      '';
+    };
+
+    spawner = mkOption {
+      type = types.str;
+      default = "systemdspawner.SystemdSpawner";
+      description = ''
+        Jupyterhub spawner to use
+
+        There are many spawners available including: local process,
+        systemd, docker, kubernetes, yarn, batch, etc.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Extra contents appended to the jupyterhub configuration
+
+        Jupyterhub configuration is a normal python file using
+        Traitlets. https://jupyterhub.readthedocs.io/en/stable/getting-started/config-basics.html. The
+        base configuration of this module was designed to have sane
+        defaults for configuration but you can override anything since
+        this is a python file.
+      '';
+      example = ''
+        c.SystemdSpawner.mem_limit = '8G'
+        c.SystemdSpawner.cpu_limit = 2.0
+      '';
+    };
+
+    jupyterhubEnv = mkOption {
+      type = types.package;
+      default = pkgs.python3.withPackages (p: with p; [
+        jupyterhub
+        jupyterhub-systemdspawner
+      ]);
+      defaultText = literalExpression ''
+        pkgs.python3.withPackages (p: with p; [
+          jupyterhub
+          jupyterhub-systemdspawner
+        ])
+      '';
+      description = ''
+        Python environment to run jupyterhub
+
+        Customizing will affect the packages available in the hub and
+        proxy. This will allow packages to be available for the
+        extraConfig that you may need. This will not normally need to
+        be changed.
+      '';
+    };
+
+    jupyterlabEnv = mkOption {
+      type = types.package;
+      default = pkgs.python3.withPackages (p: with p; [
+        jupyterhub
+        jupyterlab
+      ]);
+      defaultText = literalExpression ''
+        pkgs.python3.withPackages (p: with p; [
+          jupyterhub
+          jupyterlab
+        ])
+      '';
+      description = ''
+        Python environment to run jupyterlab
+
+        Customizing will affect the packages available in the
+        jupyterlab server and the default kernel provided. This is the
+        way to customize the jupyterlab extensions and jupyter
+        notebook extensions. This will not normally need to
+        be changed.
+      '';
+    };
+
+    kernels = mkOption {
+      type = types.nullOr (types.attrsOf(types.submodule (import ../jupyter/kernel-options.nix {
+        inherit lib;
+      })));
+
+      default = null;
+      example = literalExpression ''
+        {
+          python3 = let
+            env = (pkgs.python3.withPackages (pythonPackages: with pythonPackages; [
+                    ipykernel
+                    pandas
+                    scikit-learn
+                  ]));
+          in {
+            displayName = "Python 3 for machine learning";
+            argv = [
+              "''${env.interpreter}"
+              "-m"
+              "ipykernel_launcher"
+              "-f"
+              "{connection_file}"
+            ];
+            language = "python";
+            logo32 = "''${env}/''${env.sitePackages}/ipykernel/resources/logo-32x32.png";
+            logo64 = "''${env}/''${env.sitePackages}/ipykernel/resources/logo-64x64.png";
+          };
+        }
+      '';
+      description = ''
+        Declarative kernel config
+
+        Kernels can be declared in any language that supports and has
+        the required dependencies to communicate with a jupyter server.
+        In python's case, it means that ipykernel package must always be
+        included in the list of packages of the targeted environment.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8000;
+      description = ''
+        Port number Jupyterhub will be listening on
+      '';
+    };
+
+    host = mkOption {
+      type = types.str;
+      default = "0.0.0.0";
+      description = ''
+        Bind IP JupyterHub will be listening on
+      '';
+    };
+
+    stateDirectory = mkOption {
+      type = types.str;
+      default = "jupyterhub";
+      description = ''
+        Directory for jupyterhub state (token + database)
+      '';
+    };
+  };
+
+  config = mkMerge [
+    (mkIf cfg.enable  {
+      systemd.services.jupyterhub = {
+        description = "Jupyterhub development server";
+
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = {
+          Restart = "always";
+          ExecStart = "${cfg.jupyterhubEnv}/bin/jupyterhub --config ${jupyterhubConfig}";
+          User = "root";
+          StateDirectory = cfg.stateDirectory;
+          WorkingDirectory = "/var/lib/${cfg.stateDirectory}";
+        };
+      };
+    })
+  ];
+}
diff --git a/nixos/modules/services/development/lorri.nix b/nixos/modules/services/development/lorri.nix
new file mode 100644
index 00000000000..bda63518bfd
--- /dev/null
+++ b/nixos/modules/services/development/lorri.nix
@@ -0,0 +1,55 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.lorri;
+  socketPath = "lorri/daemon.socket";
+in {
+  options = {
+    services.lorri = {
+      enable = lib.mkOption {
+        default = false;
+        type = lib.types.bool;
+        description = ''
+          Enables the daemon for `lorri`, a nix-shell replacement for project
+          development. The socket-activated daemon starts on the first request
+          issued by the `lorri` command.
+        '';
+      };
+      package = lib.mkOption {
+        default = pkgs.lorri;
+        type = lib.types.package;
+        description = ''
+          The lorri package to use.
+        '';
+        defaultText = lib.literalExpression "pkgs.lorri";
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.user.sockets.lorri = {
+      description = "Socket for Lorri Daemon";
+      wantedBy = [ "sockets.target" ];
+      socketConfig = {
+        ListenStream = "%t/${socketPath}";
+        RuntimeDirectory = "lorri";
+      };
+    };
+
+    systemd.user.services.lorri = {
+      description = "Lorri Daemon";
+      requires = [ "lorri.socket" ];
+      after = [ "lorri.socket" ];
+      path = with pkgs; [ config.nix.package git gnutar gzip ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/lorri daemon";
+        PrivateTmp = true;
+        ProtectSystem = "strict";
+        ProtectHome = "read-only";
+        Restart = "on-failure";
+      };
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/development/rstudio-server/default.nix b/nixos/modules/services/development/rstudio-server/default.nix
new file mode 100644
index 00000000000..cd903c7e55b
--- /dev/null
+++ b/nixos/modules/services/development/rstudio-server/default.nix
@@ -0,0 +1,107 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.rstudio-server;
+
+  rserver-conf = builtins.toFile "rserver.conf" ''
+    server-working-dir=${cfg.serverWorkingDir}
+    www-address=${cfg.listenAddr}
+    ${cfg.rserverExtraConfig}
+  '';
+
+  rsession-conf = builtins.toFile "rsession.conf" ''
+    ${cfg.rsessionExtraConfig}
+  '';
+
+in
+{
+  meta.maintainers = with maintainers; [ jbedo cfhammill ];
+
+  options.services.rstudio-server = {
+    enable = mkEnableOption "RStudio server";
+
+    serverWorkingDir = mkOption {
+      type = types.str;
+      default = "/var/lib/rstudio-server";
+      description = ''
+        Default working directory for server (server-working-dir in rserver.conf).
+      '';
+    };
+
+    listenAddr = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = ''
+        Address to listen on (www-address in rserver.conf).
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.rstudio-server;
+      defaultText = literalExpression "pkgs.rstudio-server";
+      example = literalExpression "pkgs.rstudioServerWrapper.override { packages = [ pkgs.rPackages.ggplot2 ]; }";
+      description = ''
+        Rstudio server package to use. Can be set to rstudioServerWrapper to provide packages.
+      '';
+    };
+
+    rserverExtraConfig = mkOption {
+      type = types.str;
+      default = "";
+      description = ''
+        Extra contents for rserver.conf.
+      '';
+    };
+
+    rsessionExtraConfig = mkOption {
+      type = types.str;
+      default = "";
+      description = ''
+        Extra contents for resssion.conf.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable
+    {
+      systemd.services.rstudio-server = {
+        description = "Rstudio server";
+
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        restartTriggers = [ rserver-conf rsession-conf ];
+
+        serviceConfig = {
+          Restart = "on-failure";
+          Type = "forking";
+          ExecStart = "${cfg.package}/bin/rserver";
+          StateDirectory = "rstudio-server";
+          RuntimeDirectory = "rstudio-server";
+        };
+      };
+
+      environment.etc = {
+        "rstudio/rserver.conf".source = rserver-conf;
+        "rstudio/rsession.conf".source = rsession-conf;
+        "pam.d/rstudio".source = "/etc/pam.d/login";
+      };
+      environment.systemPackages = [ cfg.package ];
+
+      users = {
+        users.rstudio-server = {
+          uid = config.ids.uids.rstudio-server;
+          description = "rstudio-server";
+          group = "rstudio-server";
+        };
+        groups.rstudio-server = {
+          gid = config.ids.gids.rstudio-server;
+        };
+      };
+
+    };
+}
diff --git a/nixos/modules/services/development/zammad.nix b/nixos/modules/services/development/zammad.nix
new file mode 100644
index 00000000000..d457a607187
--- /dev/null
+++ b/nixos/modules/services/development/zammad.nix
@@ -0,0 +1,323 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.zammad;
+  settingsFormat = pkgs.formats.yaml { };
+  filterNull = filterAttrs (_: v: v != null);
+  serviceConfig = {
+    Type = "simple";
+    Restart = "always";
+
+    User = "zammad";
+    Group = "zammad";
+    PrivateTmp = true;
+    StateDirectory = "zammad";
+    WorkingDirectory = cfg.dataDir;
+  };
+  environment = {
+    RAILS_ENV = "production";
+    NODE_ENV = "production";
+    RAILS_SERVE_STATIC_FILES = "true";
+    RAILS_LOG_TO_STDOUT = "true";
+  };
+  databaseConfig = settingsFormat.generate "database.yml" cfg.database.settings;
+in
+{
+
+  options = {
+    services.zammad = {
+      enable = mkEnableOption "Zammad, a web-based, open source user support/ticketing solution.";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.zammad;
+        defaultText = literalExpression "pkgs.zammad";
+        description = "Zammad package to use.";
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/zammad";
+        description = ''
+          Path to a folder that will contain Zammad working directory.
+        '';
+      };
+
+      host = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        example = "192.168.23.42";
+        description = "Host address.";
+      };
+
+      openPorts = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to open firewall ports for Zammad";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 3000;
+        description = "Web service port.";
+      };
+
+      websocketPort = mkOption {
+        type = types.port;
+        default = 6042;
+        description = "Websocket service port.";
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum [ "PostgreSQL" "MySQL" ];
+          default = "PostgreSQL";
+          example = "MySQL";
+          description = "Database engine to use.";
+        };
+
+        host = mkOption {
+          type = types.nullOr types.str;
+          default = {
+            PostgreSQL = "/run/postgresql";
+            MySQL = "localhost";
+          }.${cfg.database.type};
+          defaultText = literalExpression ''
+            {
+              PostgreSQL = "/run/postgresql";
+              MySQL = "localhost";
+            }.''${config.services.zammad.database.type};
+          '';
+          description = ''
+            Database host address.
+          '';
+        };
+
+        port = mkOption {
+          type = types.nullOr types.port;
+          default = null;
+          description = "Database port. Use <literal>null</literal> for default port.";
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "zammad";
+          description = ''
+            Database name.
+          '';
+        };
+
+        user = mkOption {
+          type = types.nullOr types.str;
+          default = "zammad";
+          description = "Database user.";
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          example = "/run/keys/zammad-dbpassword";
+          description = ''
+            A file containing the password for <option>services.zammad.database.user</option>.
+          '';
+        };
+
+        createLocally = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Whether to create a local database automatically.";
+        };
+
+        settings = mkOption {
+          type = settingsFormat.type;
+          default = { };
+          example = literalExpression ''
+            {
+            }
+          '';
+          description = ''
+            The <filename>database.yml</filename> configuration file as key value set.
+            See <link xlink:href='TODO' />
+            for list of configuration parameters.
+          '';
+        };
+      };
+
+      secretKeyBaseFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/run/keys/secret_key_base";
+        description = ''
+          The path to a file containing the
+          <literal>secret_key_base</literal> secret.
+
+          Zammad uses <literal>secret_key_base</literal> to encrypt
+          the cookie store, which contains session data, and to digest
+          user auth tokens.
+
+          Needs to be a 64 byte long string of hexadecimal
+          characters. You can generate one by running
+
+          <screen>
+          <prompt>$ </prompt>openssl rand -hex 64 >/path/to/secret_key_base_file
+          </screen>
+
+          This should be a string, not a nix path, since nix paths are
+          copied into the world-readable nix store.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    services.zammad.database.settings = {
+      production = mapAttrs (_: v: mkDefault v) (filterNull {
+        adapter = {
+          PostgreSQL = "postgresql";
+          MySQL = "mysql2";
+        }.${cfg.database.type};
+        database = cfg.database.name;
+        pool = 50;
+        timeout = 5000;
+        encoding = "utf8";
+        username = cfg.database.user;
+        host = cfg.database.host;
+        port = cfg.database.port;
+      });
+    };
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openPorts [
+      config.services.zammad.port
+      config.services.zammad.websocketPort
+    ];
+
+    users.users.zammad = {
+      isSystemUser = true;
+      home = cfg.dataDir;
+      group = "zammad";
+    };
+
+    users.groups.zammad = { };
+
+    assertions = [
+      {
+        assertion = cfg.database.createLocally -> cfg.database.user == "zammad";
+        message = "services.zammad.database.user must be set to \"zammad\" if services.zammad.database.createLocally is set to true";
+      }
+      {
+        assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
+        message = "a password cannot be specified if services.zammad.database.createLocally is set to true";
+      }
+    ];
+
+    services.mysql = optionalAttrs (cfg.database.createLocally && cfg.database.type == "MySQL") {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        {
+          name = cfg.database.user;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    services.postgresql = optionalAttrs (cfg.database.createLocally && cfg.database.type == "PostgreSQL") {
+      enable = true;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        {
+          name = cfg.database.user;
+          ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd.services.zammad-web = {
+      inherit environment;
+      serviceConfig = serviceConfig // {
+        # loading all the gems takes time
+        TimeoutStartSec = 1200;
+      };
+      after = [
+        "network.target"
+        "postgresql.service"
+      ];
+      requires = [
+        "postgresql.service"
+      ];
+      description = "Zammad web";
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        # Blindly copy the whole project here.
+        chmod -R +w .
+        rm -rf ./public/assets/*
+        rm -rf ./tmp/*
+        rm -rf ./log/*
+        cp -r --no-preserve=owner ${cfg.package}/* .
+        chmod -R +w .
+        # config file
+        cp ${databaseConfig} ./config/database.yml
+        chmod -R +w .
+        ${optionalString (cfg.database.passwordFile != null) ''
+        {
+          echo -n "  password: "
+          cat ${cfg.database.passwordFile}
+        } >> ./config/database.yml
+        ''}
+        ${optionalString (cfg.secretKeyBaseFile != null) ''
+        {
+          echo "production: "
+          echo -n "  secret_key_base: "
+          cat ${cfg.secretKeyBaseFile}
+        } > ./config/secrets.yml
+        ''}
+
+        if [ `${config.services.postgresql.package}/bin/psql \
+                  --host ${cfg.database.host} \
+                  ${optionalString
+                    (cfg.database.port != null)
+                    "--port ${toString cfg.database.port}"} \
+                  --username ${cfg.database.user} \
+                  --dbname ${cfg.database.name} \
+                  --command "SELECT COUNT(*) FROM pg_class c \
+                            JOIN pg_namespace s ON s.oid = c.relnamespace \
+                            WHERE s.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') \
+                              AND s.nspname NOT LIKE 'pg_temp%';" | sed -n 3p` -eq 0 ]; then
+          echo "Initialize database"
+          ./bin/rake --no-system db:migrate
+          ./bin/rake --no-system db:seed
+        else
+          echo "Migrate database"
+          ./bin/rake --no-system db:migrate
+        fi
+        echo "Done"
+      '';
+      script = "./script/rails server -b ${cfg.host} -p ${toString cfg.port}";
+    };
+
+    systemd.services.zammad-websocket = {
+      inherit serviceConfig environment;
+      after = [ "zammad-web.service" ];
+      requires = [ "zammad-web.service" ];
+      description = "Zammad websocket";
+      wantedBy = [ "multi-user.target" ];
+      script = "./script/websocket-server.rb -b ${cfg.host} -p ${toString cfg.websocketPort} start";
+    };
+
+    systemd.services.zammad-scheduler = {
+      inherit environment;
+      serviceConfig = serviceConfig // { Type = "forking"; };
+      after = [ "zammad-web.service" ];
+      requires = [ "zammad-web.service" ];
+      description = "Zammad scheduler";
+      wantedBy = [ "multi-user.target" ];
+      script = "./script/scheduler.rb start";
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ garbas taeer ];
+}
diff --git a/nixos/modules/services/display-managers/greetd.nix b/nixos/modules/services/display-managers/greetd.nix
new file mode 100644
index 00000000000..895961707d3
--- /dev/null
+++ b/nixos/modules/services/display-managers/greetd.nix
@@ -0,0 +1,111 @@
+{ config, lib, pkgs, ... }:
+with lib;
+
+let
+  cfg = config.services.greetd;
+  tty = "tty${toString cfg.vt}";
+  settingsFormat = pkgs.formats.toml {};
+in
+{
+  options.services.greetd = {
+    enable = mkEnableOption "greetd";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.greetd.greetd;
+      defaultText = literalExpression "pkgs.greetd.greetd";
+      description = "The greetd package that should be used.";
+    };
+
+    settings = mkOption {
+      type = settingsFormat.type;
+      example = literalExpression ''
+        {
+          default_session = {
+            command = "''${pkgs.greetd.greetd}/bin/agreety --cmd sway";
+          };
+        }
+      '';
+      description = ''
+        greetd configuration (<link xlink:href="https://man.sr.ht/~kennylevinsen/greetd/">documentation</link>)
+        as a Nix attribute set.
+      '';
+    };
+
+    vt = mkOption  {
+      type = types.int;
+      default = 1;
+      description = ''
+        The virtual console (tty) that greetd should use. This option also disables getty on that tty.
+      '';
+    };
+
+    restart = mkOption {
+      type = types.bool;
+      default = !(cfg.settings ? initial_session);
+      defaultText = literalExpression "!(config.services.greetd.settings ? initial_session)";
+      description = ''
+        Wether to restart greetd when it terminates (e.g. on failure).
+        This is usually desirable so a user can always log in, but should be disabled when using 'settings.initial_session' (autologin),
+        because every greetd restart will trigger the autologin again.
+      '';
+    };
+  };
+  config = mkIf cfg.enable {
+
+    services.greetd.settings.terminal.vt = mkDefault cfg.vt;
+    services.greetd.settings.default_session = mkDefault "greeter";
+
+    security.pam.services.greetd = {
+      allowNullPassword = true;
+      startSession = true;
+    };
+
+    # This prevents nixos-rebuild from killing greetd by activating getty again
+    systemd.services."autovt@${tty}".enable = false;
+
+    systemd.services.greetd = {
+      unitConfig = {
+        Wants = [
+          "systemd-user-sessions.service"
+        ];
+        After = [
+          "systemd-user-sessions.service"
+          "plymouth-quit-wait.service"
+          "getty@${tty}.service"
+        ];
+        Conflicts = [
+          "getty@${tty}.service"
+        ];
+      };
+
+      serviceConfig = {
+        ExecStart = "${pkgs.greetd.greetd}/bin/greetd --config ${settingsFormat.generate "greetd.toml" cfg.settings}";
+
+        Restart = mkIf cfg.restart "always";
+
+        # Defaults from greetd upstream configuration
+        IgnoreSIGPIPE = false;
+        SendSIGHUP = true;
+        TimeoutStopSec = "30s";
+        KeyringMode = "shared";
+      };
+
+      # Don't kill a user session when using nixos-rebuild
+      restartIfChanged = false;
+
+      wantedBy = [ "graphical.target" ];
+    };
+
+    systemd.defaultUnit = "graphical.target";
+
+    users.users.greeter = {
+      isSystemUser = true;
+      group = "greeter";
+    };
+
+    users.groups.greeter = {};
+  };
+
+  meta.maintainers = with maintainers; [ queezle ];
+}
diff --git a/nixos/modules/services/editors/emacs.nix b/nixos/modules/services/editors/emacs.nix
new file mode 100644
index 00000000000..e2bbd27f6e5
--- /dev/null
+++ b/nixos/modules/services/editors/emacs.nix
@@ -0,0 +1,103 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.emacs;
+
+  editorScript = pkgs.writeScriptBin "emacseditor" ''
+    #!${pkgs.runtimeShell}
+    if [ -z "$1" ]; then
+      exec ${cfg.package}/bin/emacsclient --create-frame --alternate-editor ${cfg.package}/bin/emacs
+    else
+      exec ${cfg.package}/bin/emacsclient --alternate-editor ${cfg.package}/bin/emacs "$@"
+    fi
+  '';
+
+  desktopApplicationFile = pkgs.writeTextFile {
+    name = "emacsclient.desktop";
+    destination = "/share/applications/emacsclient.desktop";
+    text = ''
+      [Desktop Entry]
+      Name=Emacsclient
+      GenericName=Text Editor
+      Comment=Edit text
+      MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;
+      Exec=emacseditor %F
+      Icon=emacs
+      Type=Application
+      Terminal=false
+      Categories=Development;TextEditor;
+      StartupWMClass=Emacs
+      Keywords=Text;Editor;
+    '';
+  };
+
+in
+{
+
+  options.services.emacs = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable a user service for the Emacs daemon. Use <literal>emacsclient</literal> to connect to the
+        daemon. If <literal>true</literal>, <varname>services.emacs.install</varname> is
+        considered <literal>true</literal>, whatever its value.
+      '';
+    };
+
+    install = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to install a user service for the Emacs daemon. Once
+        the service is started, use emacsclient to connect to the
+        daemon.
+
+        The service must be manually started for each user with
+        "systemctl --user start emacs" or globally through
+        <varname>services.emacs.enable</varname>.
+      '';
+    };
+
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.emacs;
+      defaultText = literalExpression "pkgs.emacs";
+      description = ''
+        emacs derivation to use.
+      '';
+    };
+
+    defaultEditor = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        When enabled, configures emacsclient to be the default editor
+        using the EDITOR environment variable.
+      '';
+    };
+  };
+
+  config = mkIf (cfg.enable || cfg.install) {
+    systemd.user.services.emacs = {
+      description = "Emacs: the extensible, self-documenting text editor";
+
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${pkgs.bash}/bin/bash -c 'source ${config.system.build.setEnvironment}; exec ${cfg.package}/bin/emacs --daemon'";
+        ExecStop = "${cfg.package}/bin/emacsclient --eval (kill-emacs)";
+        Restart = "always";
+      };
+    } // optionalAttrs cfg.enable { wantedBy = [ "default.target" ]; };
+
+    environment.systemPackages = [ cfg.package editorScript desktopApplicationFile ];
+
+    environment.variables.EDITOR = mkIf cfg.defaultEditor (mkOverride 900 "${editorScript}/bin/emacseditor");
+  };
+
+  meta.doc = ./emacs.xml;
+}
diff --git a/nixos/modules/services/editors/emacs.xml b/nixos/modules/services/editors/emacs.xml
new file mode 100644
index 00000000000..fd99ee9442c
--- /dev/null
+++ b/nixos/modules/services/editors/emacs.xml
@@ -0,0 +1,580 @@
+<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/editors/infinoted.nix b/nixos/modules/services/editors/infinoted.nix
new file mode 100644
index 00000000000..16fe52a232b
--- /dev/null
+++ b/nixos/modules/services/editors/infinoted.nix
@@ -0,0 +1,160 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.infinoted;
+in {
+  options.services.infinoted = {
+    enable = mkEnableOption "infinoted";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.libinfinity;
+      defaultText = literalExpression "pkgs.libinfinity";
+      description = ''
+        Package providing infinoted
+      '';
+    };
+
+    keyFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Private key to use for TLS
+      '';
+    };
+
+    certificateFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Server certificate to use for TLS
+      '';
+    };
+
+    certificateChain = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Chain of CA-certificates to which our `certificateFile` is relative.
+        Optional for TLS.
+      '';
+    };
+
+    securityPolicy = mkOption {
+      type = types.enum ["no-tls" "allow-tls" "require-tls"];
+      default = "require-tls";
+      description = ''
+        How strictly to enforce clients connection with TLS.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 6523;
+      description = ''
+        Port to listen on
+      '';
+    };
+
+    rootDirectory = mkOption {
+      type = types.path;
+      default = "/var/lib/infinoted/documents/";
+      description = ''
+        Root of the directory structure to serve
+      '';
+    };
+
+    plugins = mkOption {
+      type = types.listOf types.str;
+      default = [ "note-text" "note-chat" "logging" "autosave" ];
+      description = ''
+        Plugins to enable
+      '';
+    };
+
+    passwordFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        File to read server-wide password from
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = ''
+        [autosave]
+        interval=10
+      '';
+      description = ''
+        Additional configuration to append to infinoted.conf
+      '';
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = "infinoted";
+      description = ''
+        What to call the dedicated user under which infinoted is run
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "infinoted";
+      description = ''
+        What to call the primary group of the dedicated user under which infinoted is run
+      '';
+    };
+  };
+
+  config = mkIf (cfg.enable) {
+    users.users = optionalAttrs (cfg.user == "infinoted")
+      { infinoted = {
+          description = "Infinoted user";
+          group = cfg.group;
+          isSystemUser = true;
+        };
+      };
+    users.groups = optionalAttrs (cfg.group == "infinoted")
+      { infinoted = { };
+      };
+
+    systemd.services.infinoted =
+      { description = "Gobby Dedicated Server";
+
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+
+        serviceConfig = {
+          Type = "simple";
+          Restart = "always";
+          ExecStart = "${cfg.package.infinoted} --config-file=/var/lib/infinoted/infinoted.conf";
+          User = cfg.user;
+          Group = cfg.group;
+          PermissionsStartOnly = true;
+        };
+        preStart = ''
+          mkdir -p /var/lib/infinoted
+          install -o ${cfg.user} -g ${cfg.group} -m 0600 /dev/null /var/lib/infinoted/infinoted.conf
+          cat >>/var/lib/infinoted/infinoted.conf <<EOF
+          [infinoted]
+          ${optionalString (cfg.keyFile != null) "key-file=${cfg.keyFile}"}
+          ${optionalString (cfg.certificateFile != null) "certificate-file=${cfg.certificateFile}"}
+          ${optionalString (cfg.certificateChain != null) "certificate-chain=${cfg.certificateChain}"}
+          port=${toString cfg.port}
+          security-policy=${cfg.securityPolicy}
+          root-directory=${cfg.rootDirectory}
+          plugins=${concatStringsSep ";" cfg.plugins}
+          ${optionalString (cfg.passwordFile != null) "password=$(head -n 1 ${cfg.passwordFile})"}
+
+          ${cfg.extraConfig}
+          EOF
+
+          install -o ${cfg.user} -g ${cfg.group} -m 0750 -d ${cfg.rootDirectory}
+        '';
+      };
+  };
+}
diff --git a/nixos/modules/services/finance/odoo.nix b/nixos/modules/services/finance/odoo.nix
new file mode 100644
index 00000000000..422ee951007
--- /dev/null
+++ b/nixos/modules/services/finance/odoo.nix
@@ -0,0 +1,122 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.odoo;
+  format = pkgs.formats.ini {};
+in
+{
+  options = {
+    services.odoo = {
+      enable = mkEnableOption "odoo";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.odoo;
+        defaultText = literalExpression "pkgs.odoo";
+        description = "Odoo package to use.";
+      };
+
+      addons = mkOption {
+        type = with types; listOf package;
+        default = [];
+        example = literalExpression "[ pkgs.odoo_enterprise ]";
+        description = "Odoo addons.";
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = {};
+        description = ''
+          Odoo configuration settings. For more details see <link xlink:href="https://www.odoo.com/documentation/15.0/administration/install/deploy.html"/>
+        '';
+      };
+
+      domain = mkOption {
+        type = with types; nullOr str;
+        description = "Domain to host Odoo with nginx";
+        default = null;
+      };
+    };
+  };
+
+  config = mkIf (cfg.enable) (let
+    cfgFile = format.generate "odoo.cfg" cfg.settings;
+  in {
+    services.nginx = mkIf (cfg.domain != null) {
+      upstreams = {
+        odoo.servers = {
+          "127.0.0.1:8069" = {};
+        };
+
+        odoochat.servers = {
+          "127.0.0.1:8072" = {};
+        };
+      };
+
+      virtualHosts."${cfg.domain}" = {
+        extraConfig = ''
+          proxy_read_timeout 720s;
+          proxy_connect_timeout 720s;
+          proxy_send_timeout 720s;
+
+          proxy_set_header X-Forwarded-Host $host;
+          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+          proxy_set_header X-Forwarded-Proto $scheme;
+          proxy_set_header X-Real-IP $remote_addr;
+        '';
+
+        locations = {
+          "/longpolling" = {
+            proxyPass = "http://odoochat";
+          };
+
+          "/" = {
+            proxyPass = "http://odoo";
+            extraConfig = ''
+              proxy_redirect off;
+            '';
+          };
+        };
+      };
+    };
+
+    services.odoo.settings.options = {
+      proxy_mode = cfg.domain != null;
+    };
+
+    users.users.odoo = {
+      isSystemUser = true;
+      group = "odoo";
+    };
+    users.groups.odoo = {};
+
+    systemd.services.odoo = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "postgresql.service" ];
+
+      # pg_dump
+      path = [ config.services.postgresql.package ];
+
+      requires = [ "postgresql.service" ];
+      script = "HOME=$STATE_DIRECTORY ${cfg.package}/bin/odoo ${optionalString (cfg.addons != []) "--addons-path=${concatMapStringsSep "," escapeShellArg cfg.addons}"} -c ${cfgFile}";
+
+      serviceConfig = {
+        DynamicUser = true;
+        User = "odoo";
+        StateDirectory = "odoo";
+      };
+    };
+
+    services.postgresql = {
+      enable = true;
+
+      ensureUsers = [{
+        name = "odoo";
+        ensurePermissions = { "DATABASE odoo" = "ALL PRIVILEGES"; };
+      }];
+      ensureDatabases = [ "odoo" ];
+    };
+  });
+}
diff --git a/nixos/modules/services/games/asf.nix b/nixos/modules/services/games/asf.nix
new file mode 100644
index 00000000000..ea2bfd40fff
--- /dev/null
+++ b/nixos/modules/services/games/asf.nix
@@ -0,0 +1,236 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.archisteamfarm;
+
+  format = pkgs.formats.json { };
+
+  asf-config = format.generate "ASF.json" (cfg.settings // {
+    # we disable it because ASF cannot update itself anyways
+    # and nixos takes care of restarting the service
+    # is in theory not needed as this is already the default for default builds
+    UpdateChannel = 0;
+    Headless = true;
+  });
+
+  ipc-config = format.generate "IPC.config" cfg.ipcSettings;
+
+  mkBot = n: c:
+    format.generate "${n}.json" (c.settings // {
+      SteamLogin = if c.username == "" then n else c.username;
+      SteamPassword = c.passwordFile;
+      # sets the password format to file (https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Security#file)
+      PasswordFormat = 4;
+      Enabled = c.enabled;
+    });
+in
+{
+  options.services.archisteamfarm = {
+    enable = mkOption {
+      type = types.bool;
+      description = ''
+        If enabled, starts the ArchisSteamFarm service.
+        For configuring the SteamGuard token you will need to use the web-ui, which is enabled by default over on 127.0.0.1:1242.
+        You cannot configure ASF in any way outside of nix, since all the config files get wiped on restart and replaced with the programatically set ones by nix.
+      '';
+      default = false;
+    };
+
+    web-ui = mkOption {
+      type = types.submodule {
+        options = {
+          enable = mkEnableOption
+            "Wheter to start the web-ui. This is the preferred way of configuring things such as the steam guard token";
+
+          package = mkOption {
+            type = types.package;
+            default = pkgs.ArchiSteamFarm.ui;
+            description =
+              "Web-UI package to use. Contents must be in lib/dist.";
+          };
+        };
+      };
+      default = {
+        enable = true;
+        package = pkgs.ArchiSteamFarm.ui;
+      };
+      example = {
+        enable = false;
+      };
+      description = "The Web-UI hosted on 127.0.0.1:1242.";
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.ArchiSteamFarm;
+      description =
+        "Package to use. Should always be the latest version, for security reasons, since this module uses very new features and to not get out of sync with the Steam API.";
+    };
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/asf";
+      description = ''
+        The ASF home directory used to store all data.
+        If left as the default value this directory will automatically be created before the ASF server starts, otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership and permissions.'';
+    };
+
+    settings = mkOption {
+      type = format.type;
+      description = ''
+        The ASF.json file, all the options are documented <link xlink:href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Configuration#global-config">here</link>.
+        Do note that `AutoRestart`  and `UpdateChannel` is always to `false`
+respectively `0` because NixOS takes care of updating everything.
+        `Headless` is also always set to `true` because there is no way to provide inputs via a systemd service.
+        You should try to keep ASF up to date since upstream does not provide support for anything but the latest version and you're exposing yourself to all kinds of issues - as is outlined <link xlink:href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Configuration#updateperiod">here</link>.
+      '';
+      example = {
+        Statistics = false;
+      };
+      default = { };
+    };
+
+    ipcSettings = mkOption {
+      type = format.type;
+      description = ''
+        Settings to write to IPC.config.
+        All options can be found <link xlink:href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/IPC#custom-configuration">here</link>.
+      '';
+      example = {
+        Kestrel = {
+          Endpoints = {
+            HTTP = {
+              Url = "http://*:1242";
+            };
+          };
+        };
+      };
+      default = { };
+    };
+
+    bots = mkOption {
+      type = types.attrsOf (types.submodule {
+        options = {
+          username = mkOption {
+            type = types.str;
+            description =
+              "Name of the user to log in. Default is attribute name.";
+            default = "";
+          };
+          passwordFile = mkOption {
+            type = types.path;
+            description =
+              "Path to a file containig the password. The file must be readable by the <literal>asf</literal> user/group.";
+          };
+          enabled = mkOption {
+            type = types.bool;
+            default = true;
+            description = "Whether to enable the bot on startup.";
+          };
+          settings = mkOption {
+            type = types.attrs;
+            description =
+              "Additional settings that are documented <link xlink:href=\"https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Configuration#bot-config\">here</link>.";
+            default = { };
+          };
+        };
+      });
+      description = ''
+        Bots name and configuration.
+      '';
+      example = {
+        exampleBot = {
+          username = "alice";
+          passwordFile = "/var/lib/asf/secrets/password";
+          settings = { SteamParentalCode = "1234"; };
+        };
+      };
+      default = { };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users = {
+      users.asf = {
+        home = cfg.dataDir;
+        isSystemUser = true;
+        group = "asf";
+        description = "Archis-Steam-Farm service user";
+      };
+      groups.asf = { };
+    };
+
+    systemd.services = {
+      asf = {
+        description = "Archis-Steam-Farm Service";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = mkMerge [
+          (mkIf (cfg.dataDir == "/var/lib/asf") { StateDirectory = "asf"; })
+          {
+            User = "asf";
+            Group = "asf";
+            WorkingDirectory = cfg.dataDir;
+            Type = "simple";
+            ExecStart =
+              "${cfg.package}/bin/ArchiSteamFarm --path ${cfg.dataDir} --process-required --no-restart --service --no-config-migrate";
+
+            # mostly copied from the default systemd service
+            PrivateTmp = true;
+            LockPersonality = true;
+            PrivateDevices = true;
+            PrivateIPC = true;
+            PrivateMounts = true;
+            PrivateUsers = true;
+            ProtectClock = true;
+            ProtectControlGroups = true;
+            ProtectHostname = true;
+            ProtectKernelLogs = true;
+            ProtectKernelModules = true;
+            ProtectKernelTunables = true;
+            ProtectProc = "invisible";
+            ProtectSystem = "full";
+            RemoveIPC = true;
+            RestrictAddressFamilies = "AF_INET AF_INET6";
+            RestrictNamespaces = true;
+            RestrictRealtime = true;
+            RestrictSUIDSGID = true;
+          }
+        ];
+
+        preStart = ''
+          mkdir -p config
+          rm -f www
+          rm -f config/{*.json,*.config}
+
+          ln -s ${asf-config} config/ASF.json
+
+          ${strings.optionalString (cfg.ipcSettings != {}) ''
+            ln -s ${ipc-config} config/IPC.config
+          ''}
+
+          ln -s ${pkgs.runCommandLocal "ASF-bots" {} ''
+            mkdir -p $out/lib/asf/bots
+            for i in ${strings.concatStringsSep " " (lists.map (x: "${getName x},${x}") (attrsets.mapAttrsToList mkBot cfg.bots))}; do IFS=",";
+              set -- $i
+              ln -s $2 $out/lib/asf/bots/$1
+            done
+          ''}/lib/asf/bots/* config/
+
+          ${strings.optionalString cfg.web-ui.enable ''
+            ln -s ${cfg.web-ui.package}/lib/dist www
+          ''}
+        '';
+      };
+    };
+  };
+
+  meta = {
+    buildDocsInSandbox = false;
+    maintainers = with maintainers; [ lom ];
+  };
+}
diff --git a/nixos/modules/services/games/crossfire-server.nix b/nixos/modules/services/games/crossfire-server.nix
new file mode 100644
index 00000000000..a33025e0c3e
--- /dev/null
+++ b/nixos/modules/services/games/crossfire-server.nix
@@ -0,0 +1,179 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.crossfire-server;
+  serverPort = 13327;
+in {
+  options.services.crossfire-server = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        If enabled, the Crossfire game server will be started at boot.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.crossfire-server;
+      defaultText = literalExpression "pkgs.crossfire-server";
+      description = ''
+        The package to use for the Crossfire server (and map/arch data, if you
+        don't change dataDir).
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "${cfg.package}/share/crossfire";
+      defaultText = literalExpression ''"''${config.services.crossfire.package}/share/crossfire"'';
+      description = ''
+        Where to load readonly data from -- maps, archetypes, treasure tables,
+        and the like. If you plan to edit the data on the live server (rather
+        than overlaying the crossfire-maps and crossfire-arch packages and
+        nixos-rebuilding), point this somewhere read-write and copy the data
+        there before starting the server.
+      '';
+    };
+
+    stateDir = mkOption {
+      type = types.str;
+      default = "/var/lib/crossfire";
+      description = ''
+        Where to store runtime data (save files, persistent items, etc).
+
+        If left at the default, this will be automatically created on server
+        startup if it does not already exist. If changed, it is the admin's
+        responsibility to make sure that the directory exists and is writeable
+        by the `crossfire` user.
+      '';
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to open ports in the firewall for the server.
+      '';
+    };
+
+    configFiles = mkOption {
+      type = types.attrsOf types.str;
+      description = ''
+        Text to append to the corresponding configuration files. Note that the
+        files given in the example are *not* the complete set of files available
+        to customize; look in /etc/crossfire after enabling the server to see
+        the available files, and read the comments in each file for detailed
+        documentation on the format and what settings are available.
+
+        Note that the motd, rules, and news files, if configured here, will
+        overwrite the example files that come with the server, rather than being
+        appended to them as the other configuration files are.
+      '';
+      example = literalExpression ''
+        {
+          dm_file = '''
+            admin:secret_password:localhost
+            jane:xyzzy:*
+          ''';
+          ban_file = '''
+            # Bob is a jerk
+            bob@*
+            # So is everyone on 192.168.86.255/24
+            *@192.168.86.
+          ''';
+          metaserver2 = '''
+            metaserver2_notification on
+            localhostname crossfire.example.net
+          ''';
+          motd = "Welcome to CrossFire!";
+          news = "No news yet.";
+          rules = "Don't be a jerk.";
+          settings = '''
+            # be nicer to newbies and harsher to experienced players
+            balanced_stat_loss true
+            # don't let players pick up and use admin-created items
+            real_wiz false
+          ''';
+        }
+      '';
+      default = {};
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.crossfire = {
+      description     = "Crossfire server daemon user";
+      home            = cfg.stateDir;
+      createHome      = false;
+      isSystemUser    = true;
+      group           = "crossfire";
+    };
+    users.groups.crossfire = {};
+
+    # Merge the cfg.configFiles setting with the default files shipped with
+    # Crossfire.
+    # For most files this consists of reading ${crossfire}/etc/crossfire/${name}
+    # and appending the user setting to it; the motd, news, and rules are handled
+    # specially, with user-provided values completely replacing the original.
+    environment.etc = lib.attrsets.mapAttrs'
+      (name: value: lib.attrsets.nameValuePair "crossfire/${name}" {
+        mode = "0644";
+        text =
+          (optionalString (!elem name ["motd" "news" "rules"])
+            (fileContents "${cfg.package}/etc/crossfire/${name}"))
+          + "\n${value}";
+      }) ({
+        ban_file = "";
+        dm_file = "";
+        exp_table = "";
+        forbid = "";
+        metaserver2 = "";
+        motd = (fileContents "${cfg.package}/etc/crossfire/motd");
+        news = (fileContents "${cfg.package}/etc/crossfire/news");
+        rules = (fileContents "${cfg.package}/etc/crossfire/rules");
+        settings = "";
+        stat_bonus = "";
+      } // cfg.configFiles);
+
+    systemd.services.crossfire-server = {
+      description   = "Crossfire Server Daemon";
+      wantedBy      = [ "multi-user.target" ];
+      after         = [ "network.target" ];
+
+      serviceConfig = mkMerge [
+        {
+          ExecStart = "${cfg.package}/bin/crossfire-server -conf /etc/crossfire -local '${cfg.stateDir}' -data '${cfg.dataDir}'";
+          Restart = "always";
+          User = "crossfire";
+          Group = "crossfire";
+          WorkingDirectory = cfg.stateDir;
+        }
+        (mkIf (cfg.stateDir == "/var/lib/crossfire") {
+          StateDirectory = "crossfire";
+        })
+      ];
+
+      # The crossfire server needs access to a bunch of files at runtime that
+      # are not created automatically at server startup; they're meant to be
+      # installed in $PREFIX/var/crossfire by `make install`. And those files
+      # need to be writeable, so we can't just point at the ones in the nix
+      # store. Instead we take the approach of copying them out of the store
+      # on first run. If `bookarch` already exists, we assume the rest of the
+      # files do as well, and copy nothing -- otherwise we risk ovewriting
+      # server state information every time the server is upgraded.
+      preStart = ''
+        if [ ! -e "${cfg.stateDir}"/bookarch ]; then
+          ${pkgs.rsync}/bin/rsync -a --chmod=u=rwX,go=rX \
+            "${cfg.package}/var/crossfire/" "${cfg.stateDir}/"
+        fi
+      '';
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ serverPort ];
+    };
+  };
+}
diff --git a/nixos/modules/services/games/deliantra-server.nix b/nixos/modules/services/games/deliantra-server.nix
new file mode 100644
index 00000000000..b7011f4c354
--- /dev/null
+++ b/nixos/modules/services/games/deliantra-server.nix
@@ -0,0 +1,172 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.deliantra-server;
+  serverPort = 13327;
+in {
+  options.services.deliantra-server = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        If enabled, the Deliantra game server will be started at boot.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.deliantra-server;
+      defaultText = literalExpression "pkgs.deliantra-server";
+      description = ''
+        The package to use for the Deliantra server (and map/arch data, if you
+        don't change dataDir).
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "${pkgs.deliantra-data}";
+      defaultText = literalExpression ''"''${pkgs.deliantra-data}"'';
+      description = ''
+        Where to store readonly data (maps, archetypes, sprites, etc).
+        Note that if you plan to use the live map editor (rather than editing
+        the maps offline and then nixos-rebuilding), THIS MUST BE WRITEABLE --
+        copy the deliantra-data someplace writeable (say,
+        /var/lib/deliantra/data) and update this option accordingly.
+      '';
+    };
+
+    stateDir = mkOption {
+      type = types.str;
+      default = "/var/lib/deliantra";
+      description = ''
+        Where to store runtime data (save files, persistent items, etc).
+
+        If left at the default, this will be automatically created on server
+        startup if it does not already exist. If changed, it is the admin's
+        responsibility to make sure that the directory exists and is writeable
+        by the `crossfire` user.
+      '';
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to open ports in the firewall for the server.
+      '';
+    };
+
+    configFiles = mkOption {
+      type = types.attrsOf types.str;
+      description = ''
+        Contents of the server configuration files. These will be appended to
+        the example configurations the server comes with and overwrite any
+        default settings defined therein.
+
+        The example here is not comprehensive. See the files in
+        /etc/deliantra-server after enabling this module for full documentation.
+      '';
+      example = literalExpression ''
+        {
+          dm_file = '''
+            admin:secret_password:localhost
+            jane:xyzzy:*
+          ''';
+          motd = "Welcome to Deliantra!";
+          settings = '''
+            # Settings for game mechanics.
+            stat_loss_on_death true
+            armor_max_enchant 7
+          ''';
+          config = '''
+            # Settings for the server daemon.
+            hiscore_url https://deliantra.example.net/scores/
+            max_map_reset 86400
+          ''';
+        }
+      '';
+      default = {
+        motd = "";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.deliantra = {
+      description     = "Deliantra server daemon user";
+      home            = cfg.stateDir;
+      createHome      = false;
+      isSystemUser    = true;
+      group           = "deliantra";
+    };
+    users.groups.deliantra = {};
+
+    # Merge the cfg.configFiles setting with the default files shipped with
+    # Deliantra.
+    # For most files this consists of reading
+    # ${deliantra}/etc/deliantra-server/${name} and appending the user setting
+    # to it.
+    environment.etc = lib.attrsets.mapAttrs'
+      (name: value: lib.attrsets.nameValuePair "deliantra-server/${name}" {
+        mode = "0644";
+        text =
+          # Deliantra doesn't come with a motd file, but respects it if present
+          # in /etc.
+          (optionalString (name != "motd")
+            (fileContents "${cfg.package}/etc/deliantra-server/${name}"))
+          + "\n${value}";
+      }) ({
+        motd = "";
+        settings = "";
+        config = "";
+        dm_file = "";
+      } // cfg.configFiles);
+
+    systemd.services.deliantra-server = {
+      description   = "Deliantra Server Daemon";
+      wantedBy      = [ "multi-user.target" ];
+      after         = [ "network.target" ];
+
+      environment = {
+        DELIANTRA_DATADIR="${cfg.dataDir}";
+        DELIANTRA_LOCALDIR="${cfg.stateDir}";
+        DELIANTRA_CONFDIR="/etc/deliantra-server";
+      };
+
+      serviceConfig = mkMerge [
+        {
+          ExecStart = "${cfg.package}/bin/deliantra-server";
+          Restart = "always";
+          User = "deliantra";
+          Group = "deliantra";
+          WorkingDirectory = cfg.stateDir;
+        }
+        (mkIf (cfg.stateDir == "/var/lib/deliantra") {
+          StateDirectory = "deliantra";
+        })
+      ];
+
+      # The deliantra server needs access to a bunch of files at runtime that
+      # are not created automatically at server startup; they're meant to be
+      # installed in $PREFIX/var/deliantra-server by `make install`. And those
+      # files need to be writeable, so we can't just point at the ones in the
+      # nix store. Instead we take the approach of copying them out of the store
+      # on first run. If `bookarch` already exists, we assume the rest of the
+      # files do as well, and copy nothing -- otherwise we risk ovewriting
+      # server state information every time the server is upgraded.
+      preStart = ''
+        if [ ! -e "${cfg.stateDir}"/bookarch ]; then
+          ${pkgs.rsync}/bin/rsync -a --chmod=u=rwX,go=rX \
+            "${cfg.package}/var/deliantra-server/" "${cfg.stateDir}/"
+        fi
+      '';
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ serverPort ];
+    };
+  };
+}
diff --git a/nixos/modules/services/games/factorio.nix b/nixos/modules/services/games/factorio.nix
new file mode 100644
index 00000000000..96fcd6d2c8b
--- /dev/null
+++ b/nixos/modules/services/games/factorio.nix
@@ -0,0 +1,268 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.factorio;
+  name = "Factorio";
+  stateDir = "/var/lib/${cfg.stateDirName}";
+  mkSavePath = name: "${stateDir}/saves/${name}.zip";
+  configFile = pkgs.writeText "factorio.conf" ''
+    use-system-read-write-data-directories=true
+    [path]
+    read-data=${cfg.package}/share/factorio/data
+    write-data=${stateDir}
+  '';
+  serverSettings = {
+    name = cfg.game-name;
+    description = cfg.description;
+    visibility = {
+      public = cfg.public;
+      lan = cfg.lan;
+    };
+    username = cfg.username;
+    password = cfg.password;
+    token = cfg.token;
+    game_password = cfg.game-password;
+    require_user_verification = cfg.requireUserVerification;
+    max_upload_in_kilobytes_per_second = 0;
+    minimum_latency_in_ticks = 0;
+    ignore_player_limit_for_returning_players = false;
+    allow_commands = "admins-only";
+    autosave_interval = cfg.autosave-interval;
+    autosave_slots = 5;
+    afk_autokick_interval = 0;
+    auto_pause = true;
+    only_admins_can_pause_the_game = true;
+    autosave_only_on_server = true;
+    non_blocking_saving = cfg.nonBlockingSaving;
+  } // cfg.extraSettings;
+  serverSettingsFile = pkgs.writeText "server-settings.json" (builtins.toJSON (filterAttrsRecursive (n: v: v != null) serverSettings));
+  serverAdminsFile = pkgs.writeText "server-adminlist.json" (builtins.toJSON cfg.admins);
+  modDir = pkgs.factorio-utils.mkModDirDrv cfg.mods;
+in
+{
+  options = {
+    services.factorio = {
+      enable = mkEnableOption name;
+      port = mkOption {
+        type = types.int;
+        default = 34197;
+        description = ''
+          The port to which the service should bind.
+        '';
+      };
+
+      admins = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "username" ];
+        description = ''
+          List of player names which will be admin.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to automatically open the specified UDP port in the firewall.
+        '';
+      };
+      saveName = mkOption {
+        type = types.str;
+        default = "default";
+        description = ''
+          The name of the savegame that will be used by the server.
+
+          When not present in /var/lib/''${config.services.factorio.stateDirName}/saves,
+          a new map with default settings will be generated before starting the service.
+        '';
+      };
+      # TODO Add more individual settings as nixos-options?
+      # TODO XXX The server tries to copy a newly created config file over the old one
+      #   on shutdown, but fails, because it's in the nix store. When is this needed?
+      #   Can an admin set options in-game and expect to have them persisted?
+      configFile = mkOption {
+        type = types.path;
+        default = configFile;
+        defaultText = literalExpression "configFile";
+        description = ''
+          The server's configuration file.
+
+          The default file generated by this module contains lines essential to
+          the server's operation. Use its contents as a basis for any
+          customizations.
+        '';
+      };
+      stateDirName = mkOption {
+        type = types.str;
+        default = "factorio";
+        description = ''
+          Name of the directory under /var/lib holding the server's data.
+
+          The configuration and map will be stored here.
+        '';
+      };
+      mods = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        description = ''
+          Mods the server should install and activate.
+
+          The derivations in this list must "build" the mod by simply copying
+          the .zip, named correctly, into the output directory. Eventually,
+          there will be a way to pull in the most up-to-date list of
+          derivations via nixos-channel. Until then, this is for experts only.
+        '';
+      };
+      game-name = mkOption {
+        type = types.nullOr types.str;
+        default = "Factorio Game";
+        description = ''
+          Name of the game as it will appear in the game listing.
+        '';
+      };
+      description = mkOption {
+        type = types.nullOr types.str;
+        default = "";
+        description = ''
+          Description of the game that will appear in the listing.
+        '';
+      };
+      extraSettings = mkOption {
+        type = types.attrs;
+        default = {};
+        example = { admins = [ "username" ];};
+        description = ''
+          Extra game configuration that will go into server-settings.json
+        '';
+      };
+      public = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Game will be published on the official Factorio matching server.
+        '';
+      };
+      lan = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Game will be broadcast on LAN.
+        '';
+      };
+      username = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Your factorio.com login credentials. Required for games with visibility public.
+        '';
+      };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.factorio-headless;
+        defaultText = literalExpression "pkgs.factorio-headless";
+        example = literalExpression "pkgs.factorio-headless-experimental";
+        description = ''
+          Factorio version to use. This defaults to the stable channel.
+        '';
+      };
+      password = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Your factorio.com login credentials. Required for games with visibility public.
+        '';
+      };
+      token = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Authentication token. May be used instead of 'password' above.
+        '';
+      };
+      game-password = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Game password.
+        '';
+      };
+      requireUserVerification = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          When set to true, the server will only allow clients that have a valid factorio.com account.
+        '';
+      };
+      autosave-interval = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        example = 10;
+        description = ''
+          Autosave interval in minutes.
+        '';
+      };
+      nonBlockingSaving = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Highly experimental feature, enable only at your own risk of losing your saves.
+          On UNIX systems, server will fork itself to create an autosave.
+          Autosaving on connected Windows clients will be disabled regardless of autosave_only_on_server option.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.factorio = {
+      description   = "Factorio headless server";
+      wantedBy      = [ "multi-user.target" ];
+      after         = [ "network.target" ];
+
+      preStart = toString [
+        "test -e ${stateDir}/saves/${cfg.saveName}.zip"
+        "||"
+        "${cfg.package}/bin/factorio"
+          "--config=${cfg.configFile}"
+          "--create=${mkSavePath cfg.saveName}"
+          (optionalString (cfg.mods != []) "--mod-directory=${modDir}")
+      ];
+
+      serviceConfig = {
+        Restart = "always";
+        KillSignal = "SIGINT";
+        DynamicUser = true;
+        StateDirectory = cfg.stateDirName;
+        UMask = "0007";
+        ExecStart = toString [
+          "${cfg.package}/bin/factorio"
+          "--config=${cfg.configFile}"
+          "--port=${toString cfg.port}"
+          "--start-server=${mkSavePath cfg.saveName}"
+          "--server-settings=${serverSettingsFile}"
+          (optionalString (cfg.mods != []) "--mod-directory=${modDir}")
+          (optionalString (cfg.admins != []) "--server-adminlist=${serverAdminsFile}")
+        ];
+
+        # Sandboxing
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
+        RestrictRealtime = true;
+        RestrictNamespaces = true;
+        MemoryDenyWriteExecute = true;
+      };
+    };
+
+    networking.firewall.allowedUDPPorts = if cfg.openFirewall then [ cfg.port ] else [];
+  };
+}
diff --git a/nixos/modules/services/games/freeciv.nix b/nixos/modules/services/games/freeciv.nix
new file mode 100644
index 00000000000..4923891a617
--- /dev/null
+++ b/nixos/modules/services/games/freeciv.nix
@@ -0,0 +1,187 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.freeciv;
+  inherit (config.users) groups;
+  rootDir = "/run/freeciv";
+  argsFormat = {
+    type = with lib.types; let
+      valueType = nullOr (oneOf [
+        bool int float str
+        (listOf valueType)
+      ]) // {
+        description = "freeciv-server params";
+      };
+    in valueType;
+    generate = name: value:
+      let mkParam = k: v:
+            if v == null then []
+            else if isBool v then if v then [("--"+k)] else []
+            else [("--"+k) v];
+          mkParams = k: v: map (mkParam k) (if isList v then v else [v]);
+      in escapeShellArgs (concatLists (concatLists (mapAttrsToList mkParams value)));
+  };
+in
+{
+  options = {
+    services.freeciv = {
+      enable = mkEnableOption ''freeciv'';
+      settings = mkOption {
+        description = ''
+          Parameters of freeciv-server.
+        '';
+        default = {};
+        type = types.submodule {
+          freeformType = argsFormat.type;
+          options.Announce = mkOption {
+            type = types.enum ["IPv4" "IPv6" "none"];
+            default = "none";
+            description = "Announce game in LAN using given protocol.";
+          };
+          options.auth = mkEnableOption "server authentication";
+          options.Database = mkOption {
+            type = types.nullOr types.str;
+            apply = pkgs.writeText "auth.conf";
+            default = ''
+              [fcdb]
+                backend="sqlite"
+                database="/var/lib/freeciv/auth.sqlite"
+            '';
+            description = "Enable database connection with given configuration.";
+          };
+          options.debug = mkOption {
+            type = types.ints.between 0 3;
+            default = 0;
+            description = "Set debug log level.";
+          };
+          options.exit-on-end = mkEnableOption "exit instead of restarting when a game ends.";
+          options.Guests = mkEnableOption "guests to login if auth is enabled";
+          options.Newusers = mkEnableOption "new users to login if auth is enabled";
+          options.port = mkOption {
+            type = types.port;
+            default = 5556;
+            description = "Listen for clients on given port";
+          };
+          options.quitidle = mkOption {
+            type = types.nullOr types.int;
+            default = null;
+            description = "Quit if no players for given time in seconds.";
+          };
+          options.read = mkOption {
+            type = types.lines;
+            apply = v: pkgs.writeTextDir "read.serv" v + "/read";
+            default = ''
+              /fcdb lua sqlite_createdb()
+            '';
+            description = "Startup script.";
+          };
+          options.saves = mkOption {
+            type = types.nullOr types.str;
+            default = "/var/lib/freeciv/saves/";
+            description = ''
+              Save games to given directory,
+              a sub-directory named after the starting date of the service
+              will me inserted to preserve older saves.
+            '';
+          };
+        };
+      };
+      openFirewall = mkEnableOption "opening the firewall for the port listening for clients";
+    };
+  };
+  config = mkIf cfg.enable {
+    users.groups.freeciv = {};
+    # Use with:
+    #   journalctl -u freeciv.service -f -o cat &
+    #   cat >/run/freeciv.stdin
+    #   load saves/2020-11-14_05-22-27/freeciv-T0005-Y-3750-interrupted.sav.bz2
+    systemd.sockets.freeciv = {
+      wantedBy = [ "sockets.target" ];
+      socketConfig = {
+        ListenFIFO = "/run/freeciv.stdin";
+        SocketGroup = groups.freeciv.name;
+        SocketMode = "660";
+        RemoveOnStop = true;
+      };
+    };
+    systemd.services.freeciv = {
+      description = "Freeciv Service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment.HOME = "/var/lib/freeciv";
+      serviceConfig = {
+        Restart = "on-failure";
+        RestartSec = "5s";
+        StandardInput = "fd:freeciv.socket";
+        StandardOutput = "journal";
+        StandardError = "journal";
+        ExecStart = pkgs.writeShellScript "freeciv-server" (''
+          set -eux
+          savedir=$(date +%Y-%m-%d_%H-%M-%S)
+          '' + "${pkgs.freeciv}/bin/freeciv-server"
+          + " " + optionalString (cfg.settings.saves != null)
+            (concatStringsSep " " [ "--saves" "${escapeShellArg cfg.settings.saves}/$savedir" ])
+          + " " + argsFormat.generate "freeciv-server" (cfg.settings // { saves = null; }));
+        DynamicUser = true;
+        # Create rootDir in the host's mount namespace.
+        RuntimeDirectory = [(baseNameOf rootDir)];
+        RuntimeDirectoryMode = "755";
+        StateDirectory = [ "freeciv" ];
+        WorkingDirectory = "/var/lib/freeciv";
+        # Avoid mounting rootDir in the own rootDir of ExecStart='s mount namespace.
+        InaccessiblePaths = ["-+${rootDir}"];
+        # This is for BindPaths= and BindReadOnlyPaths=
+        # to allow traversal of directories they create in RootDirectory=.
+        UMask = "0066";
+        RootDirectory = rootDir;
+        RootDirectoryStartOnly = true;
+        MountAPIVFS = true;
+        BindReadOnlyPaths = [
+          builtins.storeDir
+          "/etc"
+          "/run"
+        ];
+        # The following options are only for optimizing:
+        # systemd-analyze security freeciv
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateNetwork = mkDefault false;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallFilter = [
+          "@system-service"
+          # Groups in @system-service which do not contain a syscall listed by:
+          # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' freeciv-server
+          # in tests, and seem likely not necessary for freeciv-server.
+          "~@aio" "~@chown" "~@ipc" "~@keyring" "~@memlock"
+          "~@resources" "~@setuid" "~@sync" "~@timer"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+      };
+    };
+    networking.firewall = mkIf cfg.openFirewall
+      { allowedTCPPorts = [ cfg.settings.port ]; };
+  };
+  meta.maintainers = with lib.maintainers; [ julm ];
+}
diff --git a/nixos/modules/services/games/minecraft-server.nix b/nixos/modules/services/games/minecraft-server.nix
new file mode 100644
index 00000000000..5bb8eff5762
--- /dev/null
+++ b/nixos/modules/services/games/minecraft-server.nix
@@ -0,0 +1,257 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.minecraft-server;
+
+  # We don't allow eula=false anyways
+  eulaFile = builtins.toFile "eula.txt" ''
+    # eula.txt managed by NixOS Configuration
+    eula=true
+  '';
+
+  whitelistFile = pkgs.writeText "whitelist.json"
+    (builtins.toJSON
+      (mapAttrsToList (n: v: { name = n; uuid = v; }) cfg.whitelist));
+
+  cfgToString = v: if builtins.isBool v then boolToString v else toString v;
+
+  serverPropertiesFile = pkgs.writeText "server.properties" (''
+    # server.properties managed by NixOS configuration
+  '' + concatStringsSep "\n" (mapAttrsToList
+    (n: v: "${n}=${cfgToString v}") cfg.serverProperties));
+
+
+  # To be able to open the firewall, we need to read out port values in the
+  # server properties, but fall back to the defaults when those don't exist.
+  # These defaults are from https://minecraft.gamepedia.com/Server.properties#Java_Edition_3
+  defaultServerPort = 25565;
+
+  serverPort = cfg.serverProperties.server-port or defaultServerPort;
+
+  rconPort = if cfg.serverProperties.enable-rcon or false
+    then cfg.serverProperties."rcon.port" or 25575
+    else null;
+
+  queryPort = if cfg.serverProperties.enable-query or false
+    then cfg.serverProperties."query.port" or 25565
+    else null;
+
+in {
+  options = {
+    services.minecraft-server = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If enabled, start a Minecraft Server. The server
+          data will be loaded from and saved to
+          <option>services.minecraft-server.dataDir</option>.
+        '';
+      };
+
+      declarative = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to use a declarative Minecraft server configuration.
+          Only if set to <literal>true</literal>, the options
+          <option>services.minecraft-server.whitelist</option> and
+          <option>services.minecraft-server.serverProperties</option> will be
+          applied.
+        '';
+      };
+
+      eula = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether you agree to
+          <link xlink:href="https://account.mojang.com/documents/minecraft_eula">
+          Mojangs EULA</link>. This option must be set to
+          <literal>true</literal> to run Minecraft server.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/minecraft";
+        description = ''
+          Directory to store Minecraft database and other state/data files.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to open ports in the firewall for the server.
+        '';
+      };
+
+      whitelist = mkOption {
+        type = let
+          minecraftUUID = types.strMatching
+            "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" // {
+              description = "Minecraft UUID";
+            };
+          in types.attrsOf minecraftUUID;
+        default = {};
+        description = ''
+          Whitelisted players, only has an effect when
+          <option>services.minecraft-server.declarative</option> is
+          <literal>true</literal> and the whitelist is enabled
+          via <option>services.minecraft-server.serverProperties</option> by
+          setting <literal>white-list</literal> to <literal>true</literal>.
+          This is a mapping from Minecraft usernames to UUIDs.
+          You can use <link xlink:href="https://mcuuid.net/"/> to get a
+          Minecraft UUID for a username.
+        '';
+        example = literalExpression ''
+          {
+            username1 = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+            username2 = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
+          };
+        '';
+      };
+
+      serverProperties = mkOption {
+        type = with types; attrsOf (oneOf [ bool int str ]);
+        default = {};
+        example = literalExpression ''
+          {
+            server-port = 43000;
+            difficulty = 3;
+            gamemode = 1;
+            max-players = 5;
+            motd = "NixOS Minecraft server!";
+            white-list = true;
+            enable-rcon = true;
+            "rcon.password" = "hunter2";
+          }
+        '';
+        description = ''
+          Minecraft server properties for the server.properties file. Only has
+          an effect when <option>services.minecraft-server.declarative</option>
+          is set to <literal>true</literal>. See
+          <link xlink:href="https://minecraft.gamepedia.com/Server.properties#Java_Edition_3"/>
+          for documentation on these values.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.minecraft-server;
+        defaultText = literalExpression "pkgs.minecraft-server";
+        example = literalExpression "pkgs.minecraft-server_1_12_2";
+        description = "Version of minecraft-server to run.";
+      };
+
+      jvmOpts = mkOption {
+        type = types.separatedString " ";
+        default = "-Xmx2048M -Xms2048M";
+        # Example options from https://minecraft.gamepedia.com/Tutorials/Server_startup_script
+        example = "-Xmx2048M -Xms4092M -XX:+UseG1GC -XX:+CMSIncrementalPacing "
+          + "-XX:+CMSClassUnloadingEnabled -XX:ParallelGCThreads=2 "
+          + "-XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10";
+        description = "JVM options for the Minecraft server.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users.minecraft = {
+      description     = "Minecraft server service user";
+      home            = cfg.dataDir;
+      createHome      = true;
+      isSystemUser    = true;
+      group           = "minecraft";
+    };
+    users.groups.minecraft = {};
+
+    systemd.services.minecraft-server = {
+      description   = "Minecraft Server Service";
+      wantedBy      = [ "multi-user.target" ];
+      after         = [ "network.target" ];
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/minecraft-server ${cfg.jvmOpts}";
+        Restart = "always";
+        User = "minecraft";
+        WorkingDirectory = cfg.dataDir;
+        # Hardening
+        CapabilityBoundingSet = [ "" ];
+        DeviceAllow = [ "" ];
+        LockPersonality = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        UMask = "0077";
+      };
+
+      preStart = ''
+        ln -sf ${eulaFile} eula.txt
+      '' + (if cfg.declarative then ''
+
+        if [ -e .declarative ]; then
+
+          # Was declarative before, no need to back up anything
+          ln -sf ${whitelistFile} whitelist.json
+          cp -f ${serverPropertiesFile} server.properties
+
+        else
+
+          # Declarative for the first time, backup stateful files
+          ln -sb --suffix=.stateful ${whitelistFile} whitelist.json
+          cp -b --suffix=.stateful ${serverPropertiesFile} server.properties
+
+          # server.properties must have write permissions, because every time
+          # the server starts it first parses the file and then regenerates it..
+          chmod +w server.properties
+          echo "Autogenerated file that signifies that this server configuration is managed declaratively by NixOS" \
+            > .declarative
+
+        fi
+      '' else ''
+        if [ -e .declarative ]; then
+          rm .declarative
+        fi
+      '');
+    };
+
+    networking.firewall = mkIf cfg.openFirewall (if cfg.declarative then {
+      allowedUDPPorts = [ serverPort ];
+      allowedTCPPorts = [ serverPort ]
+        ++ optional (queryPort != null) queryPort
+        ++ optional (rconPort != null) rconPort;
+    } else {
+      allowedUDPPorts = [ defaultServerPort ];
+      allowedTCPPorts = [ defaultServerPort ];
+    });
+
+    assertions = [
+      { assertion = cfg.eula;
+        message = "You must agree to Mojangs EULA to run minecraft-server."
+          + " Read https://account.mojang.com/documents/minecraft_eula and"
+          + " set `services.minecraft-server.eula` to `true` if you agree.";
+      }
+    ];
+
+  };
+}
diff --git a/nixos/modules/services/games/minetest-server.nix b/nixos/modules/services/games/minetest-server.nix
new file mode 100644
index 00000000000..2111c970d4f
--- /dev/null
+++ b/nixos/modules/services/games/minetest-server.nix
@@ -0,0 +1,107 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg   = config.services.minetest-server;
+  flag  = val: name: if val != null then "--${name} ${toString val} " else "";
+  flags = [
+    (flag cfg.gameId "gameid")
+    (flag cfg.world "world")
+    (flag cfg.configPath "config")
+    (flag cfg.logPath "logfile")
+    (flag cfg.port "port")
+  ];
+in
+{
+  options = {
+    services.minetest-server = {
+      enable = mkOption {
+        type        = types.bool;
+        default     = false;
+        description = "If enabled, starts a Minetest Server.";
+      };
+
+      gameId = mkOption {
+        type        = types.nullOr types.str;
+        default     = null;
+        description = ''
+          Id of the game to use. To list available games run
+          `minetestserver --gameid list`.
+
+          If only one game exists, this option can be null.
+        '';
+      };
+
+      world = mkOption {
+        type        = types.nullOr types.path;
+        default     = null;
+        description = ''
+          Name of the world to use. To list available worlds run
+          `minetestserver --world list`.
+
+          If only one world exists, this option can be null.
+        '';
+      };
+
+      configPath = mkOption {
+        type        = types.nullOr types.path;
+        default     = null;
+        description = ''
+          Path to the config to use.
+
+          If set to null, the config of the running user will be used:
+          `~/.minetest/minetest.conf`.
+        '';
+      };
+
+      logPath = mkOption {
+        type        = types.nullOr types.path;
+        default     = null;
+        description = ''
+          Path to logfile for logging.
+
+          If set to null, logging will be output to stdout which means
+          all output will be catched by systemd.
+        '';
+      };
+
+      port = mkOption {
+        type        = types.nullOr types.int;
+        default     = null;
+        description = ''
+          Port number to bind to.
+
+          If set to null, the default 30000 will be used.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.minetest = {
+      description     = "Minetest Server Service user";
+      home            = "/var/lib/minetest";
+      createHome      = true;
+      uid             = config.ids.uids.minetest;
+      group           = "minetest";
+    };
+    users.groups.minetest.gid = config.ids.gids.minetest;
+
+    systemd.services.minetest-server = {
+      description   = "Minetest Server Service";
+      wantedBy      = [ "multi-user.target" ];
+      after         = [ "network.target" ];
+
+      serviceConfig.Restart = "always";
+      serviceConfig.User    = "minetest";
+      serviceConfig.Group   = "minetest";
+
+      script = ''
+        cd /var/lib/minetest
+
+        exec ${pkgs.minetest}/bin/minetest --server ${concatStrings flags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/games/openarena.nix b/nixos/modules/services/games/openarena.nix
new file mode 100644
index 00000000000..9c441e98b20
--- /dev/null
+++ b/nixos/modules/services/games/openarena.nix
@@ -0,0 +1,56 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.openarena;
+in
+{
+  options = {
+    services.openarena = {
+      enable = mkEnableOption "OpenArena";
+
+      openPorts = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to open firewall ports for OpenArena";
+      };
+
+      extraFlags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Extra flags to pass to <command>oa_ded</command>";
+        example = [
+          "+set dedicated 2"
+          "+set sv_hostname 'My NixOS OpenArena Server'"
+          # Load a map. Mandatory for clients to be able to connect.
+          "+map oa_dm1"
+        ];
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.firewall = mkIf cfg.openPorts {
+      allowedUDPPorts = [ 27960 ];
+    };
+
+    systemd.services.openarena = {
+      description = "OpenArena";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "openarena";
+        ExecStart = "${pkgs.openarena}/bin/oa_ded +set fs_basepath ${pkgs.openarena}/openarena-0.8.8 +set fs_homepath /var/lib/openarena ${concatStringsSep " " cfg.extraFlags}";
+        Restart = "on-failure";
+
+        # Hardening
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/games/quake3-server.nix b/nixos/modules/services/games/quake3-server.nix
new file mode 100644
index 00000000000..175af4a8382
--- /dev/null
+++ b/nixos/modules/services/games/quake3-server.nix
@@ -0,0 +1,112 @@
+{ config, pkgs, lib, ... }:
+with lib;
+
+let
+  cfg = config.services.quake3-server;
+  configFile = pkgs.writeText "q3ds-extra.cfg" ''
+    set net_port ${builtins.toString cfg.port}
+
+    ${cfg.extraConfig}
+  '';
+  defaultBaseq3 = pkgs.requireFile rec {
+    name = "baseq3";
+    hashMode = "recursive";
+    sha256 = "5dd8ee09eabd45e80450f31d7a8b69b846f59738726929298d8a813ce5725ed3";
+    message = ''
+      Unfortunately, we cannot download ${name} automatically.
+      Please purchase a legitimate copy of Quake 3 and change into the installation directory.
+
+      You can either add all relevant files to the nix-store like this:
+      mkdir /tmp/baseq3
+      cp baseq3/pak*.pk3 /tmp/baseq3
+      nix-store --add-fixed sha256 --recursive /tmp/baseq3
+
+      Alternatively you can set services.quake3-server.baseq3 to a path and copy the baseq3 directory into
+      $services.quake3-server.baseq3/.q3a/
+    '';
+  };
+  home = pkgs.runCommand "quake3-home" {} ''
+      mkdir -p $out/.q3a/baseq3
+
+      for file in ${cfg.baseq3}/*; do
+        ln -s $file $out/.q3a/baseq3/$(basename $file)
+      done
+
+      ln -s ${configFile} $out/.q3a/baseq3/nix.cfg
+  '';
+in {
+  options = {
+    services.quake3-server = {
+      enable = mkEnableOption "Quake 3 dedicated server";
+
+      port = mkOption {
+        type = types.port;
+        default = 27960;
+        description = ''
+          UDP Port the server should listen on.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open the firewall.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          seta rconPassword "superSecret"      // sets RCON password for remote console
+          seta sv_hostname "My Quake 3 server"      // name that appears in server list
+        '';
+        description = ''
+          Extra configuration options. Note that options changed via RCON will not be persisted. To list all possible
+          options, use "cvarlist 1" via RCON.
+        '';
+      };
+
+      baseq3 = mkOption {
+        type = types.either types.package types.path;
+        default = defaultBaseq3;
+        defaultText = literalDocBook "Manually downloaded Quake 3 installation directory.";
+        example = "/var/lib/q3ds";
+        description = ''
+          Path to the baseq3 files (pak*.pk3). If this is on the nix store (type = package) all .pk3 files should be saved
+          in the top-level directory. If this is on another filesystem (e.g /var/lib/baseq3) the .pk3 files are searched in
+          $baseq3/.q3a/baseq3/
+        '';
+      };
+    };
+  };
+
+  config = let
+    baseq3InStore = builtins.typeOf cfg.baseq3 == "set";
+  in mkIf cfg.enable {
+    networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
+    systemd.services.q3ds = {
+      description = "Quake 3 dedicated server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+
+      environment.HOME = if baseq3InStore then home else cfg.baseq3;
+
+      serviceConfig = with lib; {
+        Restart = "always";
+        DynamicUser = true;
+        WorkingDirectory = home;
+
+        # It is possible to alter configuration files via RCON. To ensure reproducibility we have to prevent this
+        ReadOnlyPaths = if baseq3InStore then home else cfg.baseq3;
+        ExecStartPre = optionalString (!baseq3InStore) "+${pkgs.coreutils}/bin/cp ${configFile} ${cfg.baseq3}/.q3a/baseq3/nix.cfg";
+
+        ExecStart = "${pkgs.ioquake3}/ioq3ded.x86_64 +exec nix.cfg";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ f4814n ];
+}
diff --git a/nixos/modules/services/games/teeworlds.nix b/nixos/modules/services/games/teeworlds.nix
new file mode 100644
index 00000000000..babf989c98c
--- /dev/null
+++ b/nixos/modules/services/games/teeworlds.nix
@@ -0,0 +1,119 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.teeworlds;
+  register = cfg.register;
+
+  teeworldsConf = pkgs.writeText "teeworlds.cfg" ''
+    sv_port ${toString cfg.port}
+    sv_register ${if cfg.register then "1" else "0"}
+    ${optionalString (cfg.name != null) "sv_name ${cfg.name}"}
+    ${optionalString (cfg.motd != null) "sv_motd ${cfg.motd}"}
+    ${optionalString (cfg.password != null) "password ${cfg.password}"}
+    ${optionalString (cfg.rconPassword != null) "sv_rcon_password ${cfg.rconPassword}"}
+    ${concatStringsSep "\n" cfg.extraOptions}
+  '';
+
+in
+{
+  options = {
+    services.teeworlds = {
+      enable = mkEnableOption "Teeworlds Server";
+
+      openPorts = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to open firewall ports for Teeworlds";
+      };
+
+      name = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Name of the server. Defaults to 'unnamed server'.
+        '';
+      };
+
+      register = mkOption {
+        type = types.bool;
+        example = true;
+        default = false;
+        description = ''
+          Whether the server registers as public server in the global server list. This is disabled by default because of privacy.
+        '';
+      };
+
+      motd = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Set the server message of the day text.
+        '';
+      };
+
+      password = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Password to connect to the server.
+        '';
+      };
+
+      rconPassword = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Password to access the remote console. If not set, a randomly generated one is displayed in the server log.
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8303;
+        description = ''
+          Port the server will listen on.
+        '';
+      };
+
+      extraOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Extra configuration lines for the <filename>teeworlds.cfg</filename>. See <link xlink:href="https://www.teeworlds.com/?page=docs&amp;wiki=server_settings">Teeworlds Documentation</link>.
+        '';
+        example = [ "sv_map dm1" "sv_gametype dm" ];
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.firewall = mkIf cfg.openPorts {
+      allowedUDPPorts = [ cfg.port ];
+    };
+
+    systemd.services.teeworlds = {
+      description = "Teeworlds Server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${pkgs.teeworlds}/bin/teeworlds_srv -f ${teeworldsConf}";
+
+        # Hardening
+        CapabilityBoundingSet = false;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectHome = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        SystemCallArchitectures = "native";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/games/terraria.nix b/nixos/modules/services/games/terraria.nix
new file mode 100644
index 00000000000..29f976b3c2a
--- /dev/null
+++ b/nixos/modules/services/games/terraria.nix
@@ -0,0 +1,169 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg   = config.services.terraria;
+  opt   = options.services.terraria;
+  worldSizeMap = { small = 1; medium = 2; large = 3; };
+  valFlag = name: val: optionalString (val != null) "-${name} \"${escape ["\\" "\""] (toString val)}\"";
+  boolFlag = name: val: optionalString val "-${name}";
+  flags = [
+    (valFlag "port" cfg.port)
+    (valFlag "maxPlayers" cfg.maxPlayers)
+    (valFlag "password" cfg.password)
+    (valFlag "motd" cfg.messageOfTheDay)
+    (valFlag "world" cfg.worldPath)
+    (valFlag "autocreate" (builtins.getAttr cfg.autoCreatedWorldSize worldSizeMap))
+    (valFlag "banlist" cfg.banListPath)
+    (boolFlag "secure" cfg.secure)
+    (boolFlag "noupnp" cfg.noUPnP)
+  ];
+  stopScript = pkgs.writeScript "terraria-stop" ''
+    #!${pkgs.runtimeShell}
+
+    if ! [ -d "/proc/$1" ]; then
+      exit 0
+    fi
+
+    ${getBin pkgs.tmux}/bin/tmux -S ${cfg.dataDir}/terraria.sock send-keys Enter exit Enter
+    ${getBin pkgs.coreutils}/bin/tail --pid="$1" -f /dev/null
+  '';
+in
+{
+  options = {
+    services.terraria = {
+      enable = mkOption {
+        type        = types.bool;
+        default     = false;
+        description = ''
+          If enabled, starts a Terraria server. The server can be connected to via <literal>tmux -S ''${config.${opt.dataDir}}/terraria.sock attach</literal>
+          for administration by users who are a part of the <literal>terraria</literal> group (use <literal>C-b d</literal> shortcut to detach again).
+        '';
+      };
+
+      port = mkOption {
+        type        = types.port;
+        default     = 7777;
+        description = ''
+          Specifies the port to listen on.
+        '';
+      };
+
+      maxPlayers = mkOption {
+        type        = types.ints.u8;
+        default     = 255;
+        description = ''
+          Sets the max number of players (between 1 and 255).
+        '';
+      };
+
+      password = mkOption {
+        type        = types.nullOr types.str;
+        default     = null;
+        description = ''
+          Sets the server password. Leave <literal>null</literal> for no password.
+        '';
+      };
+
+      messageOfTheDay = mkOption {
+        type        = types.nullOr types.str;
+        default     = null;
+        description = ''
+          Set the server message of the day text.
+        '';
+      };
+
+      worldPath = mkOption {
+        type        = types.nullOr types.path;
+        default     = null;
+        description = ''
+          The path to the world file (<literal>.wld</literal>) which should be loaded.
+          If no world exists at this path, one will be created with the size
+          specified by <literal>autoCreatedWorldSize</literal>.
+        '';
+      };
+
+      autoCreatedWorldSize = mkOption {
+        type        = types.enum [ "small" "medium" "large" ];
+        default     = "medium";
+        description = ''
+          Specifies the size of the auto-created world if <literal>worldPath</literal> does not
+          point to an existing world.
+        '';
+      };
+
+      banListPath = mkOption {
+        type        = types.nullOr types.path;
+        default     = null;
+        description = ''
+          The path to the ban list.
+        '';
+      };
+
+      secure = mkOption {
+        type        = types.bool;
+        default     = false;
+        description = "Adds additional cheat protection to the server.";
+      };
+
+      noUPnP = mkOption {
+        type        = types.bool;
+        default     = false;
+        description = "Disables automatic Universal Plug and Play.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Wheter to open ports in the firewall";
+      };
+
+      dataDir = mkOption {
+        type        = types.str;
+        default     = "/var/lib/terraria";
+        example     = "/srv/terraria";
+        description = "Path to variable state data directory for terraria.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.terraria = {
+      description = "Terraria server service user";
+      home        = cfg.dataDir;
+      createHome  = true;
+      uid         = config.ids.uids.terraria;
+    };
+
+    users.groups.terraria = {
+      gid = config.ids.gids.terraria;
+      members = [ "terraria" ];
+    };
+
+    systemd.services.terraria = {
+      description   = "Terraria Server Service";
+      wantedBy      = [ "multi-user.target" ];
+      after         = [ "network.target" ];
+
+      serviceConfig = {
+        User    = "terraria";
+        Type = "forking";
+        GuessMainPID = true;
+        ExecStart = "${getBin pkgs.tmux}/bin/tmux -S ${cfg.dataDir}/terraria.sock new -d ${pkgs.terraria-server}/bin/TerrariaServer ${concatStringsSep " " flags}";
+        ExecStop = "${stopScript} $MAINPID";
+      };
+
+      postStart = ''
+        ${pkgs.coreutils}/bin/chmod 660 ${cfg.dataDir}/terraria.sock
+        ${pkgs.coreutils}/bin/chgrp terraria ${cfg.dataDir}/terraria.sock
+      '';
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+      allowedUDPPorts = [ cfg.port ];
+    };
+
+  };
+}
diff --git a/nixos/modules/services/hardware/acpid.nix b/nixos/modules/services/hardware/acpid.nix
new file mode 100644
index 00000000000..883ef083003
--- /dev/null
+++ b/nixos/modules/services/hardware/acpid.nix
@@ -0,0 +1,155 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.acpid;
+
+  canonicalHandlers = {
+    powerEvent = {
+      event = "button/power.*";
+      action = cfg.powerEventCommands;
+    };
+
+    lidEvent = {
+      event = "button/lid.*";
+      action = cfg.lidEventCommands;
+    };
+
+    acEvent = {
+      event = "ac_adapter.*";
+      action = cfg.acEventCommands;
+    };
+  };
+
+  acpiConfDir = pkgs.runCommand "acpi-events" { preferLocalBuild = true; }
+    ''
+      mkdir -p $out
+      ${
+        # Generate a configuration file for each event. (You can't have
+        # multiple events in one config file...)
+        let f = name: handler:
+          ''
+            fn=$out/${name}
+            echo "event=${handler.event}" > $fn
+            echo "action=${pkgs.writeShellScriptBin "${name}.sh" handler.action }/bin/${name}.sh '%e'" >> $fn
+          '';
+        in concatStringsSep "\n" (mapAttrsToList f (canonicalHandlers // cfg.handlers))
+      }
+    '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.acpid = {
+
+      enable = mkEnableOption "the ACPI daemon";
+
+      logEvents = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Log all event activity.";
+      };
+
+      handlers = mkOption {
+        type = types.attrsOf (types.submodule {
+          options = {
+            event = mkOption {
+              type = types.str;
+              example = literalExpression ''"button/power.*" "button/lid.*" "ac_adapter.*" "button/mute.*" "button/volumedown.*" "cd/play.*" "cd/next.*"'';
+              description = "Event type.";
+            };
+
+            action = mkOption {
+              type = types.lines;
+              description = "Shell commands to execute when the event is triggered.";
+            };
+          };
+        });
+
+        description = ''
+          Event handlers.
+
+          <note><para>
+            Handler can be a single command.
+          </para></note>
+        '';
+        default = {};
+        example = {
+          ac-power = {
+            event = "ac_adapter/*";
+            action = ''
+              vals=($1)  # space separated string to array of multiple values
+              case ''${vals[3]} in
+                  00000000)
+                      echo unplugged >> /tmp/acpi.log
+                      ;;
+                  00000001)
+                      echo plugged in >> /tmp/acpi.log
+                      ;;
+                  *)
+                      echo unknown >> /tmp/acpi.log
+                      ;;
+              esac
+            '';
+          };
+        };
+      };
+
+      powerEventCommands = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Shell commands to execute on a button/power.* event.";
+      };
+
+      lidEventCommands = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Shell commands to execute on a button/lid.* event.";
+      };
+
+      acEventCommands = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Shell commands to execute on an ac_adapter.* event.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.acpid = {
+      description = "ACPI Daemon";
+      documentation = [ "man:acpid(8)" ];
+
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = escapeShellArgs
+          ([ "${pkgs.acpid}/bin/acpid"
+             "--foreground"
+             "--netlink"
+             "--confdir" "${acpiConfDir}"
+           ] ++ optional cfg.logEvents "--logevents"
+          );
+      };
+      unitConfig = {
+        ConditionVirtualization = "!systemd-nspawn";
+        ConditionPathExists = [ "/proc/acpi" ];
+      };
+
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/hardware/actkbd.nix b/nixos/modules/services/hardware/actkbd.nix
new file mode 100644
index 00000000000..b499de97b2c
--- /dev/null
+++ b/nixos/modules/services/hardware/actkbd.nix
@@ -0,0 +1,133 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.actkbd;
+
+  configFile = pkgs.writeText "actkbd.conf" ''
+    ${concatMapStringsSep "\n"
+      ({ keys, events, attributes, command, ... }:
+        ''${concatMapStringsSep "+" toString keys}:${concatStringsSep "," events}:${concatStringsSep "," attributes}:${command}''
+      )
+      cfg.bindings}
+    ${cfg.extraConfig}
+  '';
+
+  bindingCfg = { ... }: {
+    options = {
+
+      keys = mkOption {
+        type = types.listOf types.int;
+        description = "List of keycodes to match.";
+      };
+
+      events = mkOption {
+        type = types.listOf (types.enum ["key" "rep" "rel"]);
+        default = [ "key" ];
+        description = "List of events to match.";
+      };
+
+      attributes = mkOption {
+        type = types.listOf types.str;
+        default = [ "exec" ];
+        description = "List of attributes.";
+      };
+
+      command = mkOption {
+        type = types.str;
+        default = "";
+        description = "What to run.";
+      };
+
+    };
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.actkbd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the <command>actkbd</command> key mapping daemon.
+
+          Turning this on will start an <command>actkbd</command>
+          instance for every evdev input that has at least one key
+          (which is okay even for systems with tiny memory footprint,
+          since actkbd normally uses &lt;100 bytes of memory per
+          instance).
+
+          This allows binding keys globally without the need for e.g.
+          X11.
+        '';
+      };
+
+      bindings = mkOption {
+        type = types.listOf (types.submodule bindingCfg);
+        default = [];
+        example = lib.literalExpression ''
+          [ { keys = [ 113 ]; events = [ "key" ]; command = "''${pkgs.alsa-utils}/bin/amixer -q set Master toggle"; }
+          ]
+        '';
+        description = ''
+          Key bindings for <command>actkbd</command>.
+
+          See <command>actkbd</command> <filename>README</filename> for documentation.
+
+          The example shows a piece of what <option>sound.mediaKeys.enable</option> does when enabled.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Literal contents to append to the end of actkbd configuration file.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.udev.packages = lib.singleton (pkgs.writeTextFile {
+      name = "actkbd-udev-rules";
+      destination = "/etc/udev/rules.d/61-actkbd.rules";
+      text = ''
+        ACTION=="add", SUBSYSTEM=="input", KERNEL=="event[0-9]*", ENV{ID_INPUT_KEY}=="1", TAG+="systemd", ENV{SYSTEMD_WANTS}+="actkbd@$env{DEVNAME}.service"
+      '';
+    });
+
+    systemd.services."actkbd@" = {
+      enable = true;
+      restartIfChanged = true;
+      unitConfig = {
+        Description = "actkbd on %I";
+        ConditionPathExists = "%I";
+      };
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${pkgs.actkbd}/bin/actkbd -D -c ${configFile} -d %I";
+      };
+    };
+
+    # For testing
+    environment.systemPackages = [ pkgs.actkbd ];
+
+  };
+
+}
diff --git a/nixos/modules/services/hardware/auto-cpufreq.nix b/nixos/modules/services/hardware/auto-cpufreq.nix
new file mode 100644
index 00000000000..f846476b30b
--- /dev/null
+++ b/nixos/modules/services/hardware/auto-cpufreq.nix
@@ -0,0 +1,24 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.auto-cpufreq;
+in {
+  options = {
+    services.auto-cpufreq = {
+      enable = mkEnableOption "auto-cpufreq daemon";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.auto-cpufreq ];
+
+    systemd = {
+      packages = [ pkgs.auto-cpufreq ];
+      services.auto-cpufreq = {
+        # Workaround for https://github.com/NixOS/nixpkgs/issues/81138
+        wantedBy = [ "multi-user.target" ];
+        path = with pkgs; [ bash coreutils ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/hardware/bluetooth.nix b/nixos/modules/services/hardware/bluetooth.nix
new file mode 100644
index 00000000000..69a66723e76
--- /dev/null
+++ b/nixos/modules/services/hardware/bluetooth.nix
@@ -0,0 +1,141 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.hardware.bluetooth;
+  package = cfg.package;
+
+  inherit (lib)
+    mkDefault mkEnableOption mkIf mkOption
+    mkRenamedOptionModule mkRemovedOptionModule
+    concatStringsSep escapeShellArgs literalExpression
+    optional optionals optionalAttrs recursiveUpdate types;
+
+  cfgFmt = pkgs.formats.ini { };
+
+  defaults = {
+    General.ControllerMode = "dual";
+    Policy.AutoEnable = cfg.powerOnBoot;
+  };
+
+  hasDisabledPlugins = builtins.length cfg.disabledPlugins > 0;
+
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "hardware" "bluetooth" "config" ] [ "hardware" "bluetooth" "settings" ])
+    (mkRemovedOptionModule [ "hardware" "bluetooth" "extraConfig" ] ''
+      Use hardware.bluetooth.settings instead.
+
+      This is part of the general move to use structured settings instead of raw
+      text for config as introduced by RFC0042:
+      https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md
+    '')
+  ];
+
+  ###### interface
+
+  options = {
+
+    hardware.bluetooth = {
+      enable = mkEnableOption "support for Bluetooth";
+
+      hsphfpd.enable = mkEnableOption "support for hsphfpd[-prototype] implementation";
+
+      powerOnBoot = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to power up the default Bluetooth controller on boot.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.bluez;
+        defaultText = literalExpression "pkgs.bluez";
+        example = literalExpression "pkgs.bluezFull";
+        description = ''
+          Which BlueZ package to use.
+
+          <note><para>
+            Use the <literal>pkgs.bluezFull</literal> package to enable all
+            bluez plugins.
+          </para></note>
+        '';
+      };
+
+      disabledPlugins = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = "Built-in plugins to disable";
+      };
+
+      settings = mkOption {
+        type = cfgFmt.type;
+        default = { };
+        example = {
+          General = {
+            ControllerMode = "bredr";
+          };
+        };
+        description = "Set configuration for system-wide bluetooth (/etc/bluetooth/main.conf).";
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ package ]
+      ++ optional cfg.hsphfpd.enable pkgs.hsphfpd;
+
+    environment.etc."bluetooth/main.conf".source =
+      cfgFmt.generate "main.conf" (recursiveUpdate defaults cfg.settings);
+    services.udev.packages = [ package ];
+    services.dbus.packages = [ package ]
+      ++ optional cfg.hsphfpd.enable pkgs.hsphfpd;
+    systemd.packages = [ package ];
+
+    systemd.services = {
+      bluetooth =
+        let
+          # `man bluetoothd` will refer to main.conf in the nix store but bluez
+          # will in fact load the configuration file at /etc/bluetooth/main.conf
+          # so force it here to avoid any ambiguity and things suddenly breaking
+          # if/when the bluez derivation is changed.
+          args = [ "-f" "/etc/bluetooth/main.conf" ]
+            ++ optional hasDisabledPlugins
+            "--noplugin=${concatStringsSep "," cfg.disabledPlugins}";
+        in
+        {
+          wantedBy = [ "bluetooth.target" ];
+          aliases = [ "dbus-org.bluez.service" ];
+          serviceConfig.ExecStart = [
+            ""
+            "${package}/libexec/bluetooth/bluetoothd ${escapeShellArgs args}"
+          ];
+          # restarting can leave people without a mouse/keyboard
+          unitConfig.X-RestartIfChanged = false;
+        };
+    }
+    // (optionalAttrs cfg.hsphfpd.enable {
+      hsphfpd = {
+        after = [ "bluetooth.service" ];
+        requires = [ "bluetooth.service" ];
+        wantedBy = [ "bluetooth.target" ];
+
+        description = "A prototype implementation used for connecting HSP/HFP Bluetooth devices";
+        serviceConfig.ExecStart = "${pkgs.hsphfpd}/bin/hsphfpd.pl";
+      };
+    });
+
+    systemd.user.services = {
+      obex.aliases = [ "dbus-org.bluez.obex.service" ];
+    }
+    // optionalAttrs cfg.hsphfpd.enable {
+      telephony_client = {
+        wantedBy = [ "default.target" ];
+
+        description = "telephony_client for hsphfpd";
+        serviceConfig.ExecStart = "${pkgs.hsphfpd}/bin/telephony_client.pl";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/hardware/bolt.nix b/nixos/modules/services/hardware/bolt.nix
new file mode 100644
index 00000000000..32b60af0603
--- /dev/null
+++ b/nixos/modules/services/hardware/bolt.nix
@@ -0,0 +1,34 @@
+# Thunderbolt 3 device manager
+
+{ config, lib, pkgs, ...}:
+
+with lib;
+
+{
+  options = {
+
+    services.hardware.bolt = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Bolt, a userspace daemon to enable
+          security levels for Thunderbolt 3 on GNU/Linux.
+
+          Bolt is used by GNOME 3 to handle Thunderbolt settings.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf config.services.hardware.bolt.enable {
+
+    environment.systemPackages = [ pkgs.bolt ];
+    services.udev.packages = [ pkgs.bolt ];
+    systemd.packages = [ pkgs.bolt ];
+
+  };
+}
diff --git a/nixos/modules/services/hardware/brltty.nix b/nixos/modules/services/hardware/brltty.nix
new file mode 100644
index 00000000000..73056017532
--- /dev/null
+++ b/nixos/modules/services/hardware/brltty.nix
@@ -0,0 +1,57 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.brltty;
+
+  targets = [
+    "default.target" "multi-user.target"
+    "rescue.target" "emergency.target"
+  ];
+
+  genApiKey = pkgs.writers.writeDash "generate-brlapi-key" ''
+    if ! test -f /etc/brlapi.key; then
+      echo -n generating brlapi key...
+      ${pkgs.brltty}/bin/brltty-genkey -f /etc/brlapi.key
+      echo done
+    fi
+  '';
+
+in {
+
+  options = {
+
+    services.brltty.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Whether to enable the BRLTTY daemon.";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    users.users.brltty = {
+      description = "BRLTTY daemon user";
+      group = "brltty";
+    };
+    users.groups = {
+      brltty = { };
+      brlapi = { };
+    };
+
+    systemd.services."brltty@".serviceConfig =
+      { ExecStartPre = "!${genApiKey}"; };
+
+    # Install all upstream-provided files
+    systemd.packages = [ pkgs.brltty ];
+    systemd.tmpfiles.packages = [ pkgs.brltty ];
+    services.udev.packages = [ pkgs.brltty ];
+    environment.systemPackages = [ pkgs.brltty ];
+
+    # Add missing WantedBys (see issue #81138)
+    systemd.paths.brltty.wantedBy = targets;
+    systemd.paths."brltty@".wantedBy = targets;
+  };
+
+}
diff --git a/nixos/modules/services/hardware/ddccontrol.nix b/nixos/modules/services/hardware/ddccontrol.nix
new file mode 100644
index 00000000000..f0b5a9c8196
--- /dev/null
+++ b/nixos/modules/services/hardware/ddccontrol.nix
@@ -0,0 +1,39 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+let
+  cfg = config.services.ddccontrol;
+in
+
+{
+  ###### interface
+
+  options = {
+    services.ddccontrol = {
+      enable = lib.mkEnableOption "ddccontrol for controlling displays";
+    };
+  };
+
+  ###### implementation
+
+  config = lib.mkIf cfg.enable {
+    # Load the i2c-dev module
+    boot.kernelModules = [ "i2c_dev" ];
+
+    # Give users access to the "gddccontrol" tool
+    environment.systemPackages = [
+      pkgs.ddccontrol
+    ];
+
+    services.dbus.packages = [
+      pkgs.ddccontrol
+    ];
+
+    systemd.packages = [
+      pkgs.ddccontrol
+    ];
+  };
+}
diff --git a/nixos/modules/services/hardware/fancontrol.nix b/nixos/modules/services/hardware/fancontrol.nix
new file mode 100644
index 00000000000..861b70970b8
--- /dev/null
+++ b/nixos/modules/services/hardware/fancontrol.nix
@@ -0,0 +1,48 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.fancontrol;
+  configFile = pkgs.writeText "fancontrol.conf" cfg.config;
+
+in
+{
+  options.hardware.fancontrol = {
+    enable = mkEnableOption "software fan control (requires fancontrol.config)";
+
+    config = mkOption {
+      type = types.lines;
+      description = "Required fancontrol configuration file content. See <citerefentry><refentrytitle>pwmconfig</refentrytitle><manvolnum>8</manvolnum></citerefentry> from the lm_sensors package.";
+      example = ''
+        # Configuration file generated by pwmconfig
+        INTERVAL=10
+        DEVPATH=hwmon3=devices/virtual/thermal/thermal_zone2 hwmon4=devices/platform/f71882fg.656
+        DEVNAME=hwmon3=soc_dts1 hwmon4=f71869a
+        FCTEMPS=hwmon4/device/pwm1=hwmon3/temp1_input
+        FCFANS=hwmon4/device/pwm1=hwmon4/device/fan1_input
+        MINTEMP=hwmon4/device/pwm1=35
+        MAXTEMP=hwmon4/device/pwm1=65
+        MINSTART=hwmon4/device/pwm1=150
+        MINSTOP=hwmon4/device/pwm1=0
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.fancontrol = {
+      documentation = [ "man:fancontrol(8)" ];
+      description = "software fan control";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "lm_sensors.service" ];
+
+      serviceConfig = {
+        Restart = "on-failure";
+        ExecStart = "${pkgs.lm_sensors}/sbin/fancontrol ${configFile}";
+      };
+    };
+  };
+
+  meta.maintainers = [ maintainers.evils ];
+}
diff --git a/nixos/modules/services/hardware/freefall.nix b/nixos/modules/services/hardware/freefall.nix
new file mode 100644
index 00000000000..3f7b1592449
--- /dev/null
+++ b/nixos/modules/services/hardware/freefall.nix
@@ -0,0 +1,64 @@
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.freefall;
+
+in {
+
+  options.services.freefall = {
+
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to protect HP/Dell laptop hard drives (not SSDs) in free fall.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.freefall;
+      defaultText = literalExpression "pkgs.freefall";
+      description = ''
+        freefall derivation to use.
+      '';
+    };
+
+    devices = mkOption {
+      type = types.listOf types.str;
+      default = [ "/dev/sda" ];
+      description = ''
+        Device paths to all internal spinning hard drives.
+      '';
+    };
+
+  };
+
+  config = let
+
+    mkService = dev:
+      assert dev != "";
+      let dev' = utils.escapeSystemdPath dev; in
+      nameValuePair "freefall-${dev'}" {
+        description = "Free-fall protection for ${dev}";
+        after = [ "${dev'}.device" ];
+        wantedBy = [ "${dev'}.device" ];
+        serviceConfig = {
+          ExecStart = "${cfg.package}/bin/freefall ${dev}";
+          Restart = "on-failure";
+          Type = "forking";
+        };
+      };
+
+  in mkIf cfg.enable {
+
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services = builtins.listToAttrs (map mkService cfg.devices);
+
+  };
+
+}
diff --git a/nixos/modules/services/hardware/fwupd.nix b/nixos/modules/services/hardware/fwupd.nix
new file mode 100644
index 00000000000..e0506416ffa
--- /dev/null
+++ b/nixos/modules/services/hardware/fwupd.nix
@@ -0,0 +1,134 @@
+# fwupd daemon.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.fwupd;
+
+  customEtc = {
+    "fwupd/daemon.conf" = {
+      source = pkgs.writeText "daemon.conf" ''
+        [fwupd]
+        DisabledDevices=${lib.concatStringsSep ";" cfg.disabledDevices}
+        DisabledPlugins=${lib.concatStringsSep ";" cfg.disabledPlugins}
+      '';
+    };
+    "fwupd/uefi.conf" = {
+      source = pkgs.writeText "uefi.conf" ''
+        [uefi]
+        OverrideESPMountPoint=${config.boot.loader.efi.efiSysMountPoint}
+      '';
+    };
+  };
+
+  originalEtc =
+    let
+      mkEtcFile = n: nameValuePair n { source = "${cfg.package}/etc/${n}"; };
+    in listToAttrs (map mkEtcFile cfg.package.filesInstalledToEtc);
+  extraTrustedKeys =
+    let
+      mkName = p: "pki/fwupd/${baseNameOf (toString p)}";
+      mkEtcFile = p: nameValuePair (mkName p) { source = p; };
+    in listToAttrs (map mkEtcFile cfg.extraTrustedKeys);
+
+  # We cannot include the file in $out and rely on filesInstalledToEtc
+  # to install it because it would create a cyclic dependency between
+  # the outputs. We also need to enable the remote,
+  # which should not be done by default.
+  testRemote = if cfg.enableTestRemote then {
+    "fwupd/remotes.d/fwupd-tests.conf" = {
+      source = pkgs.runCommand "fwupd-tests-enabled.conf" {} ''
+        sed "s,^Enabled=false,Enabled=true," \
+        "${cfg.package.installedTests}/etc/fwupd/remotes.d/fwupd-tests.conf" > "$out"
+      '';
+    };
+  } else {};
+in {
+
+  ###### interface
+  options = {
+    services.fwupd = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable fwupd, a DBus service that allows
+          applications to update firmware.
+        '';
+      };
+
+      disabledDevices = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "2082b5e0-7a64-478a-b1b2-e3404fab6dad" ];
+        description = ''
+          Allow disabling specific devices by their GUID
+        '';
+      };
+
+      disabledPlugins = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "udev" ];
+        description = ''
+          Allow disabling specific plugins
+        '';
+      };
+
+      extraTrustedKeys = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        example = literalExpression "[ /etc/nixos/fwupd/myfirmware.pem ]";
+        description = ''
+          Installing a public key allows firmware signed with a matching private key to be recognized as trusted, which may require less authentication to install than for untrusted files. By default trusted firmware can be upgraded (but not downgraded) without the user or administrator password. Only very few keys are installed by default.
+        '';
+      };
+
+      enableTestRemote = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable test remote. This is used by
+          <link xlink:href="https://github.com/fwupd/fwupd/blob/master/data/installed-tests/README.md">installed tests</link>.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.fwupd;
+        defaultText = literalExpression "pkgs.fwupd";
+        description = ''
+          Which fwupd package to use.
+        '';
+      };
+    };
+  };
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "fwupd" "blacklistDevices"] [ "services" "fwupd" "disabledDevices" ])
+    (mkRenamedOptionModule [ "services" "fwupd" "blacklistPlugins"] [ "services" "fwupd" "disabledPlugins" ])
+  ];
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    # Disable test related plug-ins implicitly so that users do not have to care about them.
+    services.fwupd.disabledPlugins = cfg.package.defaultDisabledPlugins;
+
+    environment.systemPackages = [ cfg.package ];
+
+    # customEtc overrides some files from the package
+    environment.etc = originalEtc // customEtc // extraTrustedKeys // testRemote;
+
+    services.dbus.packages = [ cfg.package ];
+
+    services.udev.packages = [ cfg.package ];
+
+    systemd.packages = [ cfg.package ];
+  };
+
+  meta = {
+    maintainers = pkgs.fwupd.meta.maintainers;
+  };
+}
diff --git a/nixos/modules/services/hardware/illum.nix b/nixos/modules/services/hardware/illum.nix
new file mode 100644
index 00000000000..ff73c99a653
--- /dev/null
+++ b/nixos/modules/services/hardware/illum.nix
@@ -0,0 +1,35 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.illum;
+in {
+
+  options = {
+
+    services.illum = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable illum, a daemon for controlling screen brightness with brightness buttons.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.illum = {
+      description = "Backlight Adjustment Service";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig.ExecStart = "${pkgs.illum}/bin/illum-d";
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/hardware/interception-tools.nix b/nixos/modules/services/hardware/interception-tools.nix
new file mode 100644
index 00000000000..e69c05841ee
--- /dev/null
+++ b/nixos/modules/services/hardware/interception-tools.nix
@@ -0,0 +1,62 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.interception-tools;
+in {
+  options.services.interception-tools = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Whether to enable the interception tools service.";
+    };
+
+    plugins = mkOption {
+      type = types.listOf types.package;
+      default = [ pkgs.interception-tools-plugins.caps2esc ];
+      defaultText = literalExpression "[ pkgs.interception-tools-plugins.caps2esc ]";
+      description = ''
+        A list of interception tools plugins that will be made available to use
+        inside the udevmon configuration.
+      '';
+    };
+
+    udevmonConfig = mkOption {
+      type = types.either types.str types.path;
+      default = ''
+        - JOB: "intercept -g $DEVNODE | caps2esc | uinput -d $DEVNODE"
+          DEVICE:
+            EVENTS:
+              EV_KEY: [KEY_CAPSLOCK, KEY_ESC]
+      '';
+      example = ''
+        - JOB: "intercept -g $DEVNODE | y2z | x2y | uinput -d $DEVNODE"
+          DEVICE:
+            EVENTS:
+              EV_KEY: [KEY_X, KEY_Y]
+      '';
+      description = ''
+        String of udevmon YAML configuration, or path to a udevmon YAML
+        configuration file.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.interception-tools = {
+      description = "Interception tools";
+      path = [ pkgs.bash pkgs.interception-tools ] ++ cfg.plugins;
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.interception-tools}/bin/udevmon -c \
+          ${if builtins.typeOf cfg.udevmonConfig == "path"
+          then cfg.udevmonConfig
+          else pkgs.writeText "udevmon.yaml" cfg.udevmonConfig}
+        '';
+        Nice = -20;
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/hardware/irqbalance.nix b/nixos/modules/services/hardware/irqbalance.nix
new file mode 100644
index 00000000000..c79e0eb83ec
--- /dev/null
+++ b/nixos/modules/services/hardware/irqbalance.nix
@@ -0,0 +1,24 @@
+#
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.irqbalance;
+
+in
+{
+  options.services.irqbalance.enable = mkEnableOption "irqbalance daemon";
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.irqbalance ];
+
+    systemd.services.irqbalance.wantedBy = ["multi-user.target"];
+
+    systemd.packages = [ pkgs.irqbalance ];
+
+  };
+
+}
diff --git a/nixos/modules/services/hardware/joycond.nix b/nixos/modules/services/hardware/joycond.nix
new file mode 100644
index 00000000000..ffef4f8a4e1
--- /dev/null
+++ b/nixos/modules/services/hardware/joycond.nix
@@ -0,0 +1,40 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.joycond;
+  kernelPackages = config.boot.kernelPackages;
+in
+
+with lib;
+
+{
+  options.services.joycond = {
+    enable = mkEnableOption "support for Nintendo Pro Controllers and Joycons";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.joycond;
+      defaultText = "pkgs.joycond";
+      description = ''
+        The joycond package to use.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [
+      kernelPackages.hid-nintendo
+      cfg.package
+    ];
+
+    boot.extraModulePackages = [ kernelPackages.hid-nintendo ];
+    boot.kernelModules = [ "hid_nintendo" ];
+
+    services.udev.packages = [ cfg.package ];
+
+    systemd.packages = [ cfg.package ];
+
+    # Workaround for https://github.com/NixOS/nixpkgs/issues/81138
+    systemd.services.joycond.wantedBy = [ "multi-user.target" ];
+  };
+}
diff --git a/nixos/modules/services/hardware/lcd.nix b/nixos/modules/services/hardware/lcd.nix
new file mode 100644
index 00000000000..dc8595ea60c
--- /dev/null
+++ b/nixos/modules/services/hardware/lcd.nix
@@ -0,0 +1,171 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.hardware.lcd;
+  pkg = lib.getBin pkgs.lcdproc;
+
+  serverCfg = pkgs.writeText "lcdd.conf" ''
+    [server]
+    DriverPath=${pkg}/lib/lcdproc/
+    ReportToSyslog=false
+    Bind=${cfg.serverHost}
+    Port=${toString cfg.serverPort}
+    ${cfg.server.extraConfig}
+  '';
+
+  clientCfg = pkgs.writeText "lcdproc.conf" ''
+    [lcdproc]
+    Server=${cfg.serverHost}
+    Port=${toString cfg.serverPort}
+    ReportToSyslog=false
+    ${cfg.client.extraConfig}
+  '';
+
+  serviceCfg = {
+    DynamicUser = true;
+    Restart = "on-failure";
+    Slice = "lcd.slice";
+  };
+
+in with lib; {
+
+  meta.maintainers = with maintainers; [ peterhoeg ];
+
+  options = with types; {
+    services.hardware.lcd = {
+      serverHost = mkOption {
+        type = str;
+        default = "localhost";
+        description = "Host on which LCDd is listening.";
+      };
+
+      serverPort = mkOption {
+        type = int;
+        default = 13666;
+        description = "Port on which LCDd is listening.";
+      };
+
+      server = {
+        enable = mkOption {
+          type = bool;
+          default = false;
+          description = "Enable the LCD panel server (LCDd)";
+        };
+
+        openPorts = mkOption {
+          type = bool;
+          default = false;
+          description = "Open the ports in the firewall";
+        };
+
+        usbPermissions = mkOption {
+          type = bool;
+          default = false;
+          description = ''
+            Set group-write permissions on a USB device.
+            </para>
+            <para>
+            A USB connected LCD panel will most likely require having its
+            permissions modified for lcdd to write to it. Enabling this option
+            sets group-write permissions on the device identified by
+            <option>services.hardware.lcd.usbVid</option> and
+            <option>services.hardware.lcd.usbPid</option>. In order to find the
+            values, you can run the <command>lsusb</command> command. Example
+            output:
+            </para>
+            <para>
+            <literal>
+            Bus 005 Device 002: ID 0403:c630 Future Technology Devices International, Ltd lcd2usb interface
+            </literal>
+            </para>
+            <para>
+            In this case the vendor id is 0403 and the product id is c630.
+          '';
+        };
+
+        usbVid = mkOption {
+          type = str;
+          default = "";
+          description = "The vendor ID of the USB device to claim.";
+        };
+
+        usbPid = mkOption {
+          type = str;
+          default = "";
+          description = "The product ID of the USB device to claim.";
+        };
+
+        usbGroup = mkOption {
+          type = str;
+          default = "dialout";
+          description = "The group to use for settings permissions. This group must exist or you will have to create it.";
+        };
+
+        extraConfig = mkOption {
+          type = lines;
+          default = "";
+          description = "Additional configuration added verbatim to the server config.";
+        };
+      };
+
+      client = {
+        enable = mkOption {
+          type = bool;
+          default = false;
+          description = "Enable the LCD panel client (LCDproc)";
+        };
+
+        extraConfig = mkOption {
+          type = lines;
+          default = "";
+          description = "Additional configuration added verbatim to the client config.";
+        };
+
+        restartForever = mkOption {
+          type = bool;
+          default = true;
+          description = "Try restarting the client forever.";
+        };
+      };
+    };
+  };
+
+  config = mkIf (cfg.server.enable || cfg.client.enable) {
+    networking.firewall.allowedTCPPorts = mkIf (cfg.server.enable && cfg.server.openPorts) [ cfg.serverPort ];
+
+    services.udev.extraRules = mkIf (cfg.server.enable && cfg.server.usbPermissions) ''
+      ACTION=="add", SUBSYSTEMS=="usb", ATTRS{idVendor}=="${cfg.server.usbVid}", ATTRS{idProduct}=="${cfg.server.usbPid}", MODE="660", GROUP="${cfg.server.usbGroup}"
+    '';
+
+    systemd.services = {
+      lcdd = mkIf cfg.server.enable {
+        description = "LCDproc - server";
+        wantedBy = [ "lcd.target" ];
+        serviceConfig = serviceCfg // {
+          ExecStart = "${pkg}/bin/LCDd -f -c ${serverCfg}";
+          SupplementaryGroups = cfg.server.usbGroup;
+        };
+      };
+
+      lcdproc = mkIf cfg.client.enable {
+        description = "LCDproc - client";
+        after = [ "lcdd.service" ];
+        wantedBy = [ "lcd.target" ];
+        # Allow restarting for eternity
+        startLimitIntervalSec = lib.mkIf cfg.client.restartForever 0;
+        serviceConfig = serviceCfg // {
+          ExecStart = "${pkg}/bin/lcdproc -f -c ${clientCfg}";
+          # If the server is being restarted at the same time, the client will
+          # fail as it cannot connect, so space it out a bit.
+          RestartSec = "5";
+        };
+      };
+    };
+
+    systemd.targets.lcd = {
+      description = "LCD client/server";
+      after = [ "lcdd.service" "lcdproc.service" ];
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/hardware/lirc.nix b/nixos/modules/services/hardware/lirc.nix
new file mode 100644
index 00000000000..f970b0a095c
--- /dev/null
+++ b/nixos/modules/services/hardware/lirc.nix
@@ -0,0 +1,100 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.lirc;
+in {
+
+  ###### interface
+
+  options = {
+    services.lirc = {
+
+      enable = mkEnableOption "LIRC daemon";
+
+      options = mkOption {
+        type = types.lines;
+        example = ''
+          [lircd]
+          nodaemon = False
+        '';
+        description = "LIRC default options descriped in man:lircd(8) (<filename>lirc_options.conf</filename>)";
+      };
+
+      configs = mkOption {
+        type = types.listOf types.lines;
+        description = "Configurations for lircd to load, see man:lircd.conf(5) for details (<filename>lircd.conf</filename>)";
+      };
+
+      extraArguments = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Extra arguments to lircd.";
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    # Note: LIRC executables raises a warning, if lirc_options.conf do not exists
+    environment.etc."lirc/lirc_options.conf".text = cfg.options;
+
+    passthru.lirc.socket = "/run/lirc/lircd";
+
+    environment.systemPackages = [ pkgs.lirc ];
+
+    systemd.sockets.lircd = {
+      description = "LIRC daemon socket";
+      wantedBy = [ "sockets.target" ];
+      socketConfig = {
+        ListenStream = config.passthru.lirc.socket;
+        SocketUser = "lirc";
+        SocketMode = "0660";
+      };
+    };
+
+    systemd.services.lircd = let
+      configFile = pkgs.writeText "lircd.conf" (builtins.concatStringsSep "\n" cfg.configs);
+    in {
+      description = "LIRC daemon service";
+      after = [ "network.target" ];
+
+      unitConfig.Documentation = [ "man:lircd(8)" ];
+
+      serviceConfig = {
+        RuntimeDirectory = ["lirc" "lirc/lock"];
+
+        # Service runtime directory and socket share same folder.
+        # Following hacks are necessary to get everything right:
+
+        # 1. prevent socket deletion during stop and restart
+        RuntimeDirectoryPreserve = true;
+
+        # 2. fix runtime folder owner-ship, happens when socket activation
+        #    creates the folder
+        PermissionsStartOnly = true;
+        ExecStartPre = [
+          "${pkgs.coreutils}/bin/chown lirc /run/lirc/"
+        ];
+
+        ExecStart = ''
+          ${pkgs.lirc}/bin/lircd --nodaemon \
+            ${escapeShellArgs cfg.extraArguments} \
+            ${configFile}
+        '';
+        User = "lirc";
+      };
+    };
+
+    users.users.lirc = {
+      uid = config.ids.uids.lirc;
+      group = "lirc";
+      description = "LIRC user for lircd";
+    };
+
+    users.groups.lirc.gid = config.ids.gids.lirc;
+  };
+}
diff --git a/nixos/modules/services/hardware/nvidia-optimus.nix b/nixos/modules/services/hardware/nvidia-optimus.nix
new file mode 100644
index 00000000000..d53175052c7
--- /dev/null
+++ b/nixos/modules/services/hardware/nvidia-optimus.nix
@@ -0,0 +1,43 @@
+{ config, lib, ... }:
+
+let kernel = config.boot.kernelPackages; in
+
+{
+
+  ###### interface
+
+  options = {
+
+    hardware.nvidiaOptimus.disable = lib.mkOption {
+      default = false;
+      type = lib.types.bool;
+      description = ''
+        Completely disable the NVIDIA graphics card and use the
+        integrated graphics processor instead.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = lib.mkIf config.hardware.nvidiaOptimus.disable {
+    boot.blacklistedKernelModules = ["nouveau" "nvidia" "nvidiafb" "nvidia-drm"];
+    boot.kernelModules = [ "bbswitch" ];
+    boot.extraModulePackages = [ kernel.bbswitch ];
+
+    systemd.services.bbswitch = {
+      description = "Disable NVIDIA Card";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStart = "${kernel.bbswitch}/bin/discrete_vga_poweroff";
+        ExecStop = "${kernel.bbswitch}/bin/discrete_vga_poweron";
+      };
+      path = [ kernel.bbswitch ];
+    };
+  };
+
+}
diff --git a/nixos/modules/services/hardware/pcscd.nix b/nixos/modules/services/hardware/pcscd.nix
new file mode 100644
index 00000000000..b1a5c680a02
--- /dev/null
+++ b/nixos/modules/services/hardware/pcscd.nix
@@ -0,0 +1,73 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfgFile = pkgs.writeText "reader.conf" config.services.pcscd.readerConfig;
+
+  pluginEnv = pkgs.buildEnv {
+    name = "pcscd-plugins";
+    paths = map (p: "${p}/pcsc/drivers") config.services.pcscd.plugins;
+  };
+
+in
+{
+
+  ###### interface
+
+  options.services.pcscd = {
+    enable = mkEnableOption "PCSC-Lite daemon";
+
+    plugins = mkOption {
+      type = types.listOf types.package;
+      default = [ pkgs.ccid ];
+      defaultText = literalExpression "[ pkgs.ccid ]";
+      example = literalExpression "[ pkgs.pcsc-cyberjack ]";
+      description = "Plugin packages to be used for PCSC-Lite.";
+    };
+
+    readerConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        FRIENDLYNAME      "Some serial reader"
+        DEVICENAME        /dev/ttyS0
+        LIBPATH           /path/to/serial_reader.so
+        CHANNELID         1
+      '';
+      description = ''
+        Configuration for devices that aren't hotpluggable.
+
+        See <citerefentry><refentrytitle>reader.conf</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for valid options.
+      '';
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf config.services.pcscd.enable {
+
+    environment.etc."reader.conf".source = cfgFile;
+
+    environment.systemPackages = [ pkgs.pcsclite ];
+    systemd.packages = [ (getBin pkgs.pcsclite) ];
+
+    systemd.sockets.pcscd.wantedBy = [ "sockets.target" ];
+
+    systemd.services.pcscd = {
+      environment.PCSCLITE_HP_DROPDIR = pluginEnv;
+      restartTriggers = [ "/etc/reader.conf" ];
+
+      # If the cfgFile is empty and not specified (in which case the default
+      # /etc/reader.conf is assumed), pcscd will happily start going through the
+      # entire confdir (/etc in our case) looking for a config file and try to
+      # parse everything it finds. Doesn't take a lot of imagination to see how
+      # well that works. It really shouldn't do that to begin with, but to work
+      # around it, we force the path to the cfgFile.
+      #
+      # https://github.com/NixOS/nixpkgs/issues/121088
+      serviceConfig.ExecStart = [ "" "${getBin pkgs.pcsclite}/bin/pcscd -f -x -c ${cfgFile}" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/hardware/pommed.nix b/nixos/modules/services/hardware/pommed.nix
new file mode 100644
index 00000000000..bf7d6a46a29
--- /dev/null
+++ b/nixos/modules/services/hardware/pommed.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.hardware.pommed;
+    defaultConf = "${pkgs.pommed_light}/etc/pommed.conf.mactel";
+in {
+
+  options = {
+
+    services.hardware.pommed = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to use the pommed tool to handle Apple laptop
+          keyboard hotkeys.
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          The path to the <filename>pommed.conf</filename> file. Leave
+          to null to use the default config file
+          (<filename>/etc/pommed.conf.mactel</filename>). See the
+          files <filename>/etc/pommed.conf.mactel</filename> and
+          <filename>/etc/pommed.conf.pmac</filename> for examples to
+          build on.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.polkit pkgs.pommed_light ];
+
+    environment.etc."pommed.conf".source =
+      if cfg.configFile == null then defaultConf else cfg.configFile;
+
+    systemd.services.pommed = {
+      description = "Pommed Apple Hotkeys Daemon";
+      wantedBy = [ "multi-user.target" ];
+      script = "${pkgs.pommed_light}/bin/pommed -f";
+    };
+  };
+}
diff --git a/nixos/modules/services/hardware/power-profiles-daemon.nix b/nixos/modules/services/hardware/power-profiles-daemon.nix
new file mode 100644
index 00000000000..4144bc66708
--- /dev/null
+++ b/nixos/modules/services/hardware/power-profiles-daemon.nix
@@ -0,0 +1,55 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.power-profiles-daemon;
+  package = pkgs.power-profiles-daemon;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.power-profiles-daemon = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable power-profiles-daemon, a DBus daemon that allows
+          changing system behavior based upon user-selected power profiles.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = !config.services.tlp.enable;
+        message = ''
+          You have set services.power-profiles-daemon.enable = true;
+          which conflicts with services.tlp.enable = true;
+        '';
+      }
+    ];
+
+    environment.systemPackages = [ package ];
+
+    services.dbus.packages = [ package ];
+
+    services.udev.packages = [ package ];
+
+    systemd.packages = [ package ];
+
+  };
+
+}
diff --git a/nixos/modules/services/hardware/rasdaemon.nix b/nixos/modules/services/hardware/rasdaemon.nix
new file mode 100644
index 00000000000..2d4c6d2ce95
--- /dev/null
+++ b/nixos/modules/services/hardware/rasdaemon.nix
@@ -0,0 +1,170 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.hardware.rasdaemon;
+
+in
+{
+  options.hardware.rasdaemon = {
+
+    enable = mkEnableOption "RAS logging daemon";
+
+    record = mkOption {
+      type = types.bool;
+      default = true;
+      description = "record events via sqlite3, required for ras-mc-ctl";
+    };
+
+    mainboard = mkOption {
+      type = types.lines;
+      default = "";
+      description = "Custom mainboard description, see <citerefentry><refentrytitle>ras-mc-ctl</refentrytitle><manvolnum>8</manvolnum></citerefentry> for more details.";
+      example = ''
+        vendor = ASRock
+        model = B450M Pro4
+
+        # it should default to such values from
+        # /sys/class/dmi/id/board_[vendor|name]
+        # alternatively one can supply a script
+        # that returns the same format as above
+
+        script = <path to script>
+      '';
+    };
+
+    # TODO, accept `rasdaemon.labels = " ";` or `rasdaemon.labels = { dell = " "; asrock = " "; };'
+
+    labels = mkOption {
+      type = types.lines;
+      default = "";
+      description = "Additional memory module label descriptions to be placed in /etc/ras/dimm_labels.d/labels";
+      example = ''
+        # vendor and model may be shown by 'ras-mc-ctl --mainboard'
+        vendor: ASRock
+          product: To Be Filled By O.E.M.
+          model: B450M Pro4
+            # these labels are names for the motherboard slots
+            # the numbers may be shown by `ras-mc-ctl --error-count`
+            # they are mc:csrow:channel
+            DDR4_A1: 0.2.0;  DDR4_B1: 0.2.1;
+            DDR4_A2: 0.3.0;  DDR4_B2: 0.3.1;
+      '';
+    };
+
+    config = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        rasdaemon configuration, currently only used for CE PFA
+        for details, read rasdaemon.outPath/etc/sysconfig/rasdaemon's comments
+      '';
+      example = ''
+        # defaults from included config
+        PAGE_CE_REFRESH_CYCLE="24h"
+        PAGE_CE_THRESHOLD="50"
+        PAGE_CE_ACTION="soft"
+      '';
+    };
+
+    extraModules = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = "extra kernel modules to load";
+      example = [ "i7core_edac" ];
+    };
+
+    testing = mkEnableOption "error injection infrastructure";
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.etc = {
+      "ras/mainboard" = {
+        enable = cfg.mainboard != "";
+        text = cfg.mainboard;
+      };
+    # TODO, handle multiple cfg.labels.brand = " ";
+      "ras/dimm_labels.d/labels" = {
+        enable = cfg.labels != "";
+        text = cfg.labels;
+      };
+      "sysconfig/rasdaemon" = {
+        enable = cfg.config != "";
+        text = cfg.config;
+      };
+    };
+    environment.systemPackages = [ pkgs.rasdaemon ]
+      ++ optionals (cfg.testing) (with pkgs.error-inject; [
+        edac-inject
+        mce-inject
+        aer-inject
+      ]);
+
+    boot.initrd.kernelModules = cfg.extraModules
+      ++ optionals (cfg.testing) [
+        # edac_core and amd64_edac should get loaded automatically
+        # i7core_edac may not be, and may not be required, but should load successfully
+        "edac_core"
+        "amd64_edac"
+        "i7core_edac"
+        "mce-inject"
+        "aer-inject"
+      ];
+
+    boot.kernelPatches = optionals (cfg.testing) [{
+      name = "rasdaemon-tests";
+      patch = null;
+      extraConfig = ''
+        EDAC_DEBUG y
+        X86_MCE_INJECT y
+
+        PCIEPORTBUS y
+        PCIEAER y
+        PCIEAER_INJECT y
+      '';
+    }];
+
+    # i tried to set up a group for this
+    # but rasdaemon needs higher permissions?
+    # `rasdaemon: Can't locate a mounted debugfs`
+
+    # most of this taken from src/misc/
+    systemd.services = {
+      rasdaemon = {
+        description = "the RAS logging daemon";
+        documentation = [ "man:rasdaemon(1)" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = {
+          StateDirectory = optionalString (cfg.record) "rasdaemon";
+
+          ExecStart = "${pkgs.rasdaemon}/bin/rasdaemon --foreground"
+            + optionalString (cfg.record) " --record";
+          ExecStop = "${pkgs.rasdaemon}/bin/rasdaemon --disable";
+          Restart = "on-abort";
+
+          # src/misc/rasdaemon.service.in shows this:
+          # ExecStartPost = ${pkgs.rasdaemon}/bin/rasdaemon --enable
+          # but that results in unpredictable existence of the database
+          # and everything seems to be enabled without this...
+        };
+      };
+      ras-mc-ctl = mkIf (cfg.labels != "") {
+        description = "register DIMM labels on startup";
+        documentation = [ "man:ras-mc-ctl(8)" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = "${pkgs.rasdaemon}/bin/ras-mc-ctl --register-labels";
+          RemainAfterExit = true;
+        };
+      };
+    };
+  };
+
+  meta.maintainers = [ maintainers.evils ];
+
+}
diff --git a/nixos/modules/services/hardware/ratbagd.nix b/nixos/modules/services/hardware/ratbagd.nix
new file mode 100644
index 00000000000..01a8276750f
--- /dev/null
+++ b/nixos/modules/services/hardware/ratbagd.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ratbagd;
+in
+{
+  ###### interface
+
+  options = {
+    services.ratbagd = {
+      enable = mkEnableOption "ratbagd for configuring gaming mice";
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    # Give users access to the "ratbagctl" tool
+    environment.systemPackages = [ pkgs.libratbag ];
+
+    services.dbus.packages = [ pkgs.libratbag ];
+
+    systemd.packages = [ pkgs.libratbag ];
+  };
+}
diff --git a/nixos/modules/services/hardware/sane.nix b/nixos/modules/services/hardware/sane.nix
new file mode 100644
index 00000000000..caf232e234e
--- /dev/null
+++ b/nixos/modules/services/hardware/sane.nix
@@ -0,0 +1,197 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  pkg = pkgs.sane-backends.override {
+    scanSnapDriversUnfree = config.hardware.sane.drivers.scanSnap.enable;
+    scanSnapDriversPackage = config.hardware.sane.drivers.scanSnap.package;
+  };
+
+  sanedConf = pkgs.writeTextFile {
+    name = "saned.conf";
+    destination = "/etc/sane.d/saned.conf";
+    text = ''
+      localhost
+      ${config.services.saned.extraConfig}
+    '';
+  };
+
+  netConf = pkgs.writeTextFile {
+    name = "net.conf";
+    destination = "/etc/sane.d/net.conf";
+    text = ''
+      ${lib.optionalString config.services.saned.enable "localhost"}
+      ${config.hardware.sane.netConf}
+    '';
+  };
+
+  env = {
+    SANE_CONFIG_DIR = config.hardware.sane.configDir;
+    LD_LIBRARY_PATH = [ "${saneConfig}/lib/sane" ];
+  };
+
+  backends = [ pkg netConf ] ++ optional config.services.saned.enable sanedConf ++ config.hardware.sane.extraBackends;
+  saneConfig = pkgs.mkSaneConfig { paths = backends; inherit (config.hardware.sane) disabledDefaultBackends; };
+
+  enabled = config.hardware.sane.enable || config.services.saned.enable;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    hardware.sane.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable support for SANE scanners.
+
+        <note><para>
+          Users in the "scanner" group will gain access to the scanner, or the "lp" group if it's also a printer.
+        </para></note>
+      '';
+    };
+
+    hardware.sane.snapshot = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Use a development snapshot of SANE scanner drivers.";
+    };
+
+    hardware.sane.extraBackends = mkOption {
+      type = types.listOf types.path;
+      default = [];
+      description = ''
+        Packages providing extra SANE backends to enable.
+
+        <note><para>
+          The example contains the package for HP scanners.
+        </para></note>
+      '';
+      example = literalExpression "[ pkgs.hplipWithPlugin ]";
+    };
+
+    hardware.sane.disabledDefaultBackends = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "v4l" ];
+      description = ''
+        Names of backends which are enabled by default but should be disabled.
+        See <literal>$SANE_CONFIG_DIR/dll.conf</literal> for the list of possible names.
+      '';
+    };
+
+    hardware.sane.configDir = mkOption {
+      type = types.str;
+      internal = true;
+      description = "The value of SANE_CONFIG_DIR.";
+    };
+
+    hardware.sane.netConf = mkOption {
+      type = types.lines;
+      default = "";
+      example = "192.168.0.16";
+      description = ''
+        Network hosts that should be probed for remote scanners.
+      '';
+    };
+
+    hardware.sane.drivers.scanSnap.enable = mkOption {
+      type = types.bool;
+      default = false;
+      example = true;
+      description = ''
+        Whether to enable drivers for the Fujitsu ScanSnap scanners.
+
+        The driver files are unfree and extracted from the Windows driver image.
+      '';
+    };
+
+    hardware.sane.drivers.scanSnap.package = mkOption {
+      type = types.package;
+      default = pkgs.sane-drivers.epjitsu;
+      defaultText = literalExpression "pkgs.sane-drivers.epjitsu";
+      description = ''
+        Epjitsu driver package to use. Useful if you want to extract the driver files yourself.
+
+        The process is described in the <literal>/etc/sane.d/epjitsu.conf</literal> file in
+        the <literal>sane-backends</literal> package.
+      '';
+    };
+
+    services.saned.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable saned network daemon for remote connection to scanners.
+
+        saned would be runned from <literal>scanner</literal> user; to allow
+        access to hardware that doesn't have <literal>scanner</literal> group
+        you should add needed groups to this user.
+      '';
+    };
+
+    services.saned.extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = "192.168.0.0/24";
+      description = ''
+        Extra saned configuration lines.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkMerge [
+    (mkIf enabled {
+      hardware.sane.configDir = mkDefault "${saneConfig}/etc/sane.d";
+
+      environment.systemPackages = backends;
+      environment.sessionVariables = env;
+      services.udev.packages = backends;
+
+      users.groups.scanner.gid = config.ids.gids.scanner;
+    })
+
+    (mkIf config.services.saned.enable {
+      networking.firewall.connectionTrackingModules = [ "sane" ];
+
+      systemd.services."saned@" = {
+        description = "Scanner Service";
+        environment = mapAttrs (name: val: toString val) env;
+        serviceConfig = {
+          User = "scanner";
+          Group = "scanner";
+          ExecStart = "${pkg}/bin/saned";
+        };
+      };
+
+      systemd.sockets.saned = {
+        description = "saned incoming socket";
+        wantedBy = [ "sockets.target" ];
+        listenStreams = [ "0.0.0.0:6566" "[::]:6566" ];
+        socketConfig = {
+          # saned needs to distinguish between IPv4 and IPv6 to open matching data sockets.
+          BindIPv6Only = "ipv6-only";
+          Accept = true;
+          MaxConnections = 64;
+        };
+      };
+
+      users.users.scanner = {
+        uid = config.ids.uids.scanner;
+        group = "scanner";
+        extraGroups = [ "lp" ] ++ optionals config.services.avahi.enable [ "avahi" ];
+      };
+    })
+  ];
+
+}
diff --git a/nixos/modules/services/hardware/sane_extra_backends/brscan4.nix b/nixos/modules/services/hardware/sane_extra_backends/brscan4.nix
new file mode 100644
index 00000000000..8f999810840
--- /dev/null
+++ b/nixos/modules/services/hardware/sane_extra_backends/brscan4.nix
@@ -0,0 +1,112 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.sane.brscan4;
+
+  netDeviceList = attrValues cfg.netDevices;
+
+  etcFiles = pkgs.callPackage ./brscan4_etc_files.nix { netDevices = netDeviceList; };
+
+  netDeviceOpts = { name, ... }: {
+
+    options = {
+
+      name = mkOption {
+        type = types.str;
+        description = ''
+          The friendly name you give to the network device. If undefined,
+          the name of attribute will be used.
+        '';
+
+        example = "office1";
+      };
+
+      model = mkOption {
+        type = types.str;
+        description = ''
+          The model of the network device.
+        '';
+
+        example = "MFC-7860DW";
+      };
+
+      ip = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          The ip address of the device. If undefined, you will have to
+          provide a nodename.
+        '';
+
+        example = "192.168.1.2";
+      };
+
+      nodename = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          The node name of the device. If undefined, you will have to
+          provide an ip.
+        '';
+
+        example = "BRW0080927AFBCE";
+      };
+
+    };
+
+
+    config =
+      { name = mkDefault name;
+      };
+  };
+
+in
+
+{
+  options = {
+
+    hardware.sane.brscan4.enable =
+      mkEnableOption "Brother's brscan4 scan backend" // {
+      description = ''
+        When enabled, will automatically register the "brscan4" sane
+        backend and bring configuration files to their expected location.
+      '';
+    };
+
+    hardware.sane.brscan4.netDevices = mkOption {
+      default = {};
+      example =
+        { office1 = { model = "MFC-7860DW"; ip = "192.168.1.2"; };
+          office2 = { model = "MFC-7860DW"; nodename = "BRW0080927AFBCE"; };
+        };
+      type = with types; attrsOf (submodule netDeviceOpts);
+      description = ''
+        The list of network devices that will be registered against the brscan4
+        sane backend.
+      '';
+    };
+  };
+
+  config = mkIf (config.hardware.sane.enable && cfg.enable) {
+
+    hardware.sane.extraBackends = [
+      pkgs.brscan4
+    ];
+
+    environment.etc."opt/brother/scanner/brscan4" =
+      { source = "${etcFiles}/etc/opt/brother/scanner/brscan4"; };
+
+    assertions = [
+      { assertion = all (x: !(null != x.ip && null != x.nodename)) netDeviceList;
+        message = ''
+          When describing a network device as part of the attribute list
+          `hardware.sane.brscan4.netDevices`, only one of its `ip` or `nodename`
+          attribute should be specified, not both!
+        '';
+      }
+    ];
+
+  };
+}
diff --git a/nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix b/nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix
new file mode 100644
index 00000000000..9d083a615a2
--- /dev/null
+++ b/nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix
@@ -0,0 +1,68 @@
+{ stdenv, lib, brscan4, netDevices ? [] }:
+
+/*
+
+Testing
+-------
+
+No net devices:
+
+~~~
+nix-shell -E 'with import <nixpkgs> { }; brscan4-etc-files'
+~~~
+
+Two net devices:
+
+~~~
+nix-shell -E 'with import <nixpkgs> { }; brscan4-etc-files.override{netDevices=[{name="a"; model="MFC-7860DW"; nodename="BRW0080927AFBCE";} {name="b"; model="MFC-7860DW"; ip="192.168.1.2";}];}'
+~~~
+
+*/
+
+let
+
+  addNetDev = nd: ''
+    brsaneconfig4 -a \
+    name="${nd.name}" \
+    model="${nd.model}" \
+    ${if (lib.hasAttr "nodename" nd && nd.nodename != null) then
+      ''nodename="${nd.nodename}"'' else
+      ''ip="${nd.ip}"''}'';
+  addAllNetDev = xs: lib.concatStringsSep "\n" (map addNetDev xs);
+in
+
+stdenv.mkDerivation {
+
+  name = "brscan4-etc-files-0.4.3-3";
+  src = "${brscan4}/opt/brother/scanner/brscan4";
+
+  nativeBuildInputs = [ brscan4 ];
+
+  dontConfigure = true;
+
+  buildPhase = ''
+    TARGET_DIR="$out/etc/opt/brother/scanner/brscan4"
+    mkdir -p "$TARGET_DIR"
+    cp -rp "./models4" "$TARGET_DIR"
+    cp -rp "./Brsane4.ini" "$TARGET_DIR"
+    cp -rp "./brsanenetdevice4.cfg" "$TARGET_DIR"
+
+    export BRSANENETDEVICE4_CFG_FILENAME="$TARGET_DIR/brsanenetdevice4.cfg"
+
+    printf '${addAllNetDev netDevices}\n'
+
+    ${addAllNetDev netDevices}
+  '';
+
+  dontInstall = true;
+  dontStrip = true;
+  dontPatchELF = true;
+
+  meta = with lib; {
+    description = "Brother brscan4 sane backend driver etc files";
+    homepage = "http://www.brother.com";
+    platforms = platforms.linux;
+    license = licenses.unfree;
+    maintainers = with maintainers; [ jraygauthier ];
+  };
+}
diff --git a/nixos/modules/services/hardware/sane_extra_backends/brscan5.nix b/nixos/modules/services/hardware/sane_extra_backends/brscan5.nix
new file mode 100644
index 00000000000..2e4ad8cc3ba
--- /dev/null
+++ b/nixos/modules/services/hardware/sane_extra_backends/brscan5.nix
@@ -0,0 +1,110 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.sane.brscan5;
+
+  netDeviceList = attrValues cfg.netDevices;
+
+  etcFiles = pkgs.callPackage ./brscan5_etc_files.nix { netDevices = netDeviceList; };
+
+  netDeviceOpts = { name, ... }: {
+
+    options = {
+
+      name = mkOption {
+        type = types.str;
+        description = ''
+          The friendly name you give to the network device. If undefined,
+          the name of attribute will be used.
+        '';
+
+        example = "office1";
+      };
+
+      model = mkOption {
+        type = types.str;
+        description = ''
+          The model of the network device.
+        '';
+
+        example = "ADS-1200";
+      };
+
+      ip = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          The ip address of the device. If undefined, you will have to
+          provide a nodename.
+        '';
+
+        example = "192.168.1.2";
+      };
+
+      nodename = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          The node name of the device. If undefined, you will have to
+          provide an ip.
+        '';
+
+        example = "BRW0080927AFBCE";
+      };
+
+    };
+
+
+    config =
+      { name = mkDefault name;
+      };
+  };
+
+in
+
+{
+  options = {
+
+    hardware.sane.brscan5.enable =
+      mkEnableOption "the Brother brscan5 sane backend";
+
+    hardware.sane.brscan5.netDevices = mkOption {
+      default = {};
+      example =
+        { office1 = { model = "MFC-7860DW"; ip = "192.168.1.2"; };
+          office2 = { model = "MFC-7860DW"; nodename = "BRW0080927AFBCE"; };
+        };
+      type = with types; attrsOf (submodule netDeviceOpts);
+      description = ''
+        The list of network devices that will be registered against the brscan5
+        sane backend.
+      '';
+    };
+  };
+
+  config = mkIf (config.hardware.sane.enable && cfg.enable) {
+
+    hardware.sane.extraBackends = [
+      pkgs.brscan5
+    ];
+
+    environment.etc."opt/brother/scanner/brscan5" =
+      { source = "${etcFiles}/etc/opt/brother/scanner/brscan5"; };
+    environment.etc."opt/brother/scanner/models" =
+      { source = "${etcFiles}/etc/opt/brother/scanner/brscan5/models"; };
+    environment.etc."sane.d/dll.d/brother5.conf".source = "${pkgs.brscan5}/etc/sane.d/dll.d/brother.conf";
+
+    assertions = [
+      { assertion = all (x: !(null != x.ip && null != x.nodename)) netDeviceList;
+        message = ''
+          When describing a network device as part of the attribute list
+          `hardware.sane.brscan5.netDevices`, only one of its `ip` or `nodename`
+          attribute should be specified, not both!
+        '';
+      }
+    ];
+
+  };
+}
diff --git a/nixos/modules/services/hardware/sane_extra_backends/brscan5_etc_files.nix b/nixos/modules/services/hardware/sane_extra_backends/brscan5_etc_files.nix
new file mode 100644
index 00000000000..432f0316a4f
--- /dev/null
+++ b/nixos/modules/services/hardware/sane_extra_backends/brscan5_etc_files.nix
@@ -0,0 +1,77 @@
+{ stdenv, lib, brscan5, netDevices ? [] }:
+
+/*
+
+Testing
+-------
+From nixpkgs repo
+
+No net devices:
+
+~~~
+nix-build -E 'let pkgs = import ./. {};
+                  brscan5-etc-files = pkgs.callPackage (import ./nixos/modules/services/hardware/sane_extra_backends/brscan5_etc_files.nix) {};
+              in brscan5-etc-files'
+~~~
+
+Two net devices:
+
+~~~
+nix-build -E 'let pkgs = import ./. {};
+                  brscan5-etc-files = pkgs.callPackage (import ./nixos/modules/services/hardware/sane_extra_backends/brscan5_etc_files.nix) {};
+              in brscan5-etc-files.override {
+                   netDevices = [
+                     {name="a"; model="ADS-1200"; nodename="BRW0080927AFBCE";}
+                     {name="b"; model="ADS-1200"; ip="192.168.1.2";}
+                   ];
+              }'
+~~~
+
+*/
+
+let
+
+  addNetDev = nd: ''
+    brsaneconfig5 -a \
+    name="${nd.name}" \
+    model="${nd.model}" \
+    ${if (lib.hasAttr "nodename" nd && nd.nodename != null) then
+      ''nodename="${nd.nodename}"'' else
+      ''ip="${nd.ip}"''}'';
+  addAllNetDev = xs: lib.concatStringsSep "\n" (map addNetDev xs);
+in
+
+stdenv.mkDerivation {
+
+  name = "brscan5-etc-files";
+  version = "1.2.6-0";
+  src = "${brscan5}/opt/brother/scanner/brscan5";
+
+  nativeBuildInputs = [ brscan5 ];
+
+  dontConfigure = true;
+
+  buildPhase = ''
+    TARGET_DIR="$out/etc/opt/brother/scanner/brscan5"
+    mkdir -p "$TARGET_DIR"
+    cp -rp "./models" "$TARGET_DIR"
+    cp -rp "./brscan5.ini" "$TARGET_DIR"
+    cp -rp "./brsanenetdevice.cfg" "$TARGET_DIR"
+
+    export NIX_REDIRECTS="/etc/opt/brother/scanner/brscan5/=$TARGET_DIR/"
+
+    printf '${addAllNetDev netDevices}\n'
+
+    ${addAllNetDev netDevices}
+  '';
+
+  dontInstall = true;
+
+  meta = with lib; {
+    description = "Brother brscan5 sane backend driver etc files";
+    homepage = "https://www.brother.com";
+    platforms = platforms.linux;
+    license = licenses.unfree;
+    maintainers = with maintainers; [ mattchrist ];
+  };
+}
diff --git a/nixos/modules/services/hardware/sane_extra_backends/dsseries.nix b/nixos/modules/services/hardware/sane_extra_backends/dsseries.nix
new file mode 100644
index 00000000000..d71a17f5ea6
--- /dev/null
+++ b/nixos/modules/services/hardware/sane_extra_backends/dsseries.nix
@@ -0,0 +1,26 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  options = {
+
+    hardware.sane.dsseries.enable =
+      mkEnableOption "Brother DSSeries scan backend" // {
+      description = ''
+        When enabled, will automatically register the "dsseries" SANE backend.
+
+        This supports the Brother DSmobile scanner series, including the
+        DS-620, DS-720D, DS-820W, and DS-920DW scanners.
+      '';
+    };
+  };
+
+  config = mkIf (config.hardware.sane.enable && config.hardware.sane.dsseries.enable) {
+
+    hardware.sane.extraBackends = [ pkgs.dsseries ];
+    services.udev.packages = [ pkgs.dsseries ];
+    boot.kernelModules = [ "sg" ];
+
+  };
+}
diff --git a/nixos/modules/services/hardware/spacenavd.nix b/nixos/modules/services/hardware/spacenavd.nix
new file mode 100644
index 00000000000..69ca6f102ef
--- /dev/null
+++ b/nixos/modules/services/hardware/spacenavd.nix
@@ -0,0 +1,24 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.hardware.spacenavd;
+
+in {
+
+  options = {
+    hardware.spacenavd = {
+      enable = mkEnableOption "spacenavd to support 3DConnexion devices";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.spacenavd = {
+      description = "Daemon for the Spacenavigator 6DOF mice by 3Dconnexion";
+      wantedBy = [ "graphical.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.spacenavd}/bin/spacenavd -d -l syslog";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/hardware/tcsd.nix b/nixos/modules/services/hardware/tcsd.nix
new file mode 100644
index 00000000000..e414b9647c9
--- /dev/null
+++ b/nixos/modules/services/hardware/tcsd.nix
@@ -0,0 +1,162 @@
+# tcsd daemon.
+
+{ config, options, pkgs, lib, ... }:
+
+with lib;
+let
+
+  cfg = config.services.tcsd;
+  opt = options.services.tcsd;
+
+  tcsdConf = pkgs.writeText "tcsd.conf" ''
+    port = 30003
+    num_threads = 10
+    system_ps_file = ${cfg.stateDir}/system.data
+    # This is the log of each individual measurement done by the system.
+    # By re-calculating the PCR registers based on this information, even
+    # finer details about the measured environment can be inferred than
+    # what is available directly from the PCR registers.
+    firmware_log_file = /sys/kernel/security/tpm0/binary_bios_measurements
+    kernel_log_file = /sys/kernel/security/ima/binary_runtime_measurements
+    firmware_pcrs = ${cfg.firmwarePCRs}
+    kernel_pcrs = ${cfg.kernelPCRs}
+    platform_cred = ${cfg.platformCred}
+    conformance_cred = ${cfg.conformanceCred}
+    endorsement_cred = ${cfg.endorsementCred}
+    #remote_ops = create_key,random
+    #host_platform_class = server_12
+    #all_platform_classes = pc_11,pc_12,mobile_12
+  '';
+
+in
+{
+
+  ###### interface
+
+  options = {
+
+    services.tcsd = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to enable tcsd, a Trusted Computing management service
+          that provides TCG Software Stack (TSS).  The tcsd daemon is
+          the only portal to the Trusted Platform Module (TPM), a hardware
+          chip on the motherboard.
+        '';
+      };
+
+      user = mkOption {
+        default = "tss";
+        type = types.str;
+        description = "User account under which tcsd runs.";
+      };
+
+      group = mkOption {
+        default = "tss";
+        type = types.str;
+        description = "Group account under which tcsd runs.";
+      };
+
+      stateDir = mkOption {
+        default = "/var/lib/tpm";
+        type = types.path;
+        description = ''
+          The location of the system persistent storage file.
+          The system persistent storage file holds keys and data across
+          restarts of the TCSD and system reboots.
+        '';
+      };
+
+      firmwarePCRs = mkOption {
+        default = "0,1,2,3,4,5,6,7";
+        type = types.str;
+        description = "PCR indices used in the TPM for firmware measurements.";
+      };
+
+      kernelPCRs = mkOption {
+        default = "8,9,10,11,12";
+        type = types.str;
+        description = "PCR indices used in the TPM for kernel measurements.";
+      };
+
+      platformCred = mkOption {
+        default = "${cfg.stateDir}/platform.cert";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/platform.cert"'';
+        type = types.path;
+        description = ''
+          Path to the platform credential for your TPM. Your TPM
+          manufacturer may have provided you with a set of credentials
+          (certificates) that should be used when creating identities
+          using your TPM. When a user of your TPM makes an identity,
+          this credential will be encrypted as part of that process.
+          See the 1.1b TPM Main specification section 9.3 for information
+          on this process. '';
+      };
+
+      conformanceCred = mkOption {
+        default = "${cfg.stateDir}/conformance.cert";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/conformance.cert"'';
+        type = types.path;
+        description = ''
+          Path to the conformance credential for your TPM.
+          See also the platformCred option'';
+      };
+
+      endorsementCred = mkOption {
+        default = "${cfg.stateDir}/endorsement.cert";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/endorsement.cert"'';
+        type = types.path;
+        description = ''
+          Path to the endorsement credential for your TPM.
+          See also the platformCred option'';
+      };
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.trousers ];
+
+    services.udev.extraRules = ''
+      # Give tcsd ownership of all TPM devices
+      KERNEL=="tpm[0-9]*", MODE="0660", OWNER="${cfg.user}", GROUP="${cfg.group}"
+      # Tag TPM devices to create a .device unit for tcsd to depend on
+      ACTION=="add", KERNEL=="tpm[0-9]*", TAG+="systemd"
+    '';
+
+    systemd.tmpfiles.rules = [
+      # Initialise the state directory
+      "d ${cfg.stateDir} 0770 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.tcsd = {
+      description = "Manager for Trusted Computing resources";
+      documentation = [ "man:tcsd(8)" ];
+
+      requires = [ "dev-tpm0.device" ];
+      after = [ "dev-tpm0.device" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.trousers}/sbin/tcsd -f -c ${tcsdConf}";
+      };
+    };
+
+    users.users = optionalAttrs (cfg.user == "tss") {
+      tss = {
+        group = "tss";
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "tss") { tss = {}; };
+  };
+}
diff --git a/nixos/modules/services/hardware/thermald.nix b/nixos/modules/services/hardware/thermald.nix
new file mode 100644
index 00000000000..fcd02ea90c6
--- /dev/null
+++ b/nixos/modules/services/hardware/thermald.nix
@@ -0,0 +1,57 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.thermald;
+in
+{
+  ###### interface
+  options = {
+    services.thermald = {
+      enable = mkEnableOption "thermald, the temperature management daemon";
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable debug logging.
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = "the thermald manual configuration file.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.thermald;
+        defaultText = literalExpression "pkgs.thermald";
+        description = "Which thermald package to use.";
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.dbus.packages = [ cfg.package ];
+
+    systemd.services.thermald = {
+      description = "Thermal Daemon Service";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        PrivateNetwork = true;
+        ExecStart = ''
+          ${cfg.package}/sbin/thermald \
+            --no-daemon \
+            ${optionalString cfg.debug "--loglevel=debug"} \
+            ${optionalString (cfg.configFile != null) "--config-file ${cfg.configFile}"} \
+            --dbus-enable \
+            --adaptive
+        '';
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/hardware/thinkfan.nix b/nixos/modules/services/hardware/thinkfan.nix
new file mode 100644
index 00000000000..4ea829e496e
--- /dev/null
+++ b/nixos/modules/services/hardware/thinkfan.nix
@@ -0,0 +1,224 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.thinkfan;
+  settingsFormat = pkgs.formats.yaml { };
+  configFile = settingsFormat.generate "thinkfan.yaml" cfg.settings;
+  thinkfan = pkgs.thinkfan.override { inherit (cfg) smartSupport; };
+
+  # fan-speed and temperature levels
+  levelType = with types;
+    let
+      tuple = ts: mkOptionType {
+        name = "tuple";
+        merge = mergeOneOption;
+        check = xs: all id (zipListsWith (t: x: t.check x) ts xs);
+        description = "tuple of" + concatMapStrings (t: " (${t.description})") ts;
+      };
+      level = ints.unsigned;
+      special = enum [ "level auto" "level full-speed" "level disengaged" ];
+    in
+      tuple [ (either level special) level level ];
+
+  # sensor or fan config
+  sensorType = name: types.submodule {
+    freeformType = types.attrsOf settingsFormat.type;
+    options = {
+      type = mkOption {
+        type = types.enum [ "hwmon" "atasmart" "tpacpi" "nvml" ];
+        description = ''
+          The ${name} type, can be
+          <literal>hwmon</literal> for standard ${name}s,
+
+          <literal>atasmart</literal> to read the temperature via
+          S.M.A.R.T (requires smartSupport to be enabled),
+
+          <literal>tpacpi</literal> for the legacy thinkpac_acpi driver, or
+
+          <literal>nvml</literal> for the (proprietary) nVidia driver.
+        '';
+      };
+      query = mkOption {
+        type = types.str;
+        description = ''
+          The query string used to match one or more ${name}s: can be
+          a fullpath to the temperature file (single ${name}) or a fullpath
+          to a driver directory (multiple ${name}s).
+
+          <note><para>
+            When multiple ${name}s match, the query can be restricted using the
+            <option>name</option> or <option>indices</option> options.
+          </para></note>
+        '';
+      };
+      indices = mkOption {
+        type = with types; nullOr (listOf ints.unsigned);
+        default = null;
+        description = ''
+          A list of ${name}s to pick in case multiple ${name}s match the query.
+
+          <note><para>Indices start from 0.</para></note>
+        '';
+      };
+    } // optionalAttrs (name == "sensor") {
+      correction = mkOption {
+        type = with types; nullOr (listOf int);
+        default = null;
+        description = ''
+          A list of values to be added to the temperature of each sensor,
+          can be used to equalize small discrepancies in temperature ratings.
+        '';
+      };
+    };
+  };
+
+  # removes NixOS special and unused attributes
+  sensorToConf = { type, query, ... }@args:
+    (filterAttrs (k: v: v != null && !(elem k ["type" "query"])) args)
+    // { "${type}" = query; };
+
+  syntaxNote = name: ''
+    <note><para>
+      This section slightly departs from the thinkfan.conf syntax.
+      The type and path must be specified like this:
+      <literal>
+        type = "tpacpi";
+        query = "/proc/acpi/ibm/${name}";
+      </literal>
+      instead of a single declaration like:
+      <literal>
+        - tpacpi: /proc/acpi/ibm/${name}
+      </literal>
+    </para></note>
+  '';
+
+in {
+
+  options = {
+
+    services.thinkfan = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable thinkfan, a fan control program.
+
+          <note><para>
+            This module targets IBM/Lenovo thinkpads by default, for
+            other hardware you will have configure it more carefully.
+          </para></note>
+        '';
+        relatedPackages = [ "thinkfan" ];
+      };
+
+      smartSupport = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to build thinkfan with S.M.A.R.T. support to read temperatures
+          directly from hard disks.
+        '';
+      };
+
+      sensors = mkOption {
+        type = types.listOf (sensorType "sensor");
+        default = [
+          { type = "tpacpi";
+            query = "/proc/acpi/ibm/thermal";
+          }
+        ];
+        description = ''
+          List of temperature sensors thinkfan will monitor.
+        '' + syntaxNote "thermal";
+      };
+
+      fans = mkOption {
+        type = types.listOf (sensorType "fan");
+        default = [
+          { type = "tpacpi";
+            query = "/proc/acpi/ibm/fan";
+          }
+        ];
+        description = ''
+          List of fans thinkfan will control.
+        '' + syntaxNote "fan";
+      };
+
+      levels = mkOption {
+        type = types.listOf levelType;
+        default = [
+          [0  0   55]
+          [1  48  60]
+          [2  50  61]
+          [3  52  63]
+          [6  56  65]
+          [7  60  85]
+          ["level auto" 80 32767]
+        ];
+        description = ''
+          [LEVEL LOW HIGH]
+
+          LEVEL is the fan level to use: it can be an integer (0-7 with thinkpad_acpi),
+          "level auto" (to keep the default firmware behavior), "level full-speed" or
+          "level disengaged" (to run the fan as fast as possible).
+          LOW is the temperature at which to step down to the previous level.
+          HIGH is the temperature at which to step up to the next level.
+          All numbers are integers.
+        '';
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "-b" "0" ];
+        description = ''
+          A list of extra command line arguments to pass to thinkfan.
+          Check the thinkfan(1) manpage for available arguments.
+        '';
+      };
+
+      settings = mkOption {
+        type = types.attrsOf settingsFormat.type;
+        default = { };
+        description = ''
+          Thinkfan settings. Use this option to configure thinkfan
+          settings not exposed in a NixOS option or to bypass one.
+          Before changing this, read the <literal>thinkfan.conf(5)</literal>
+          manpage and take a look at the example config file at
+          <link xlink:href="https://github.com/vmatare/thinkfan/blob/master/examples/thinkfan.yaml"/>
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ thinkfan ];
+
+    services.thinkfan.settings = mapAttrs (k: v: mkDefault v) {
+      sensors = map sensorToConf cfg.sensors;
+      fans    = map sensorToConf cfg.fans;
+      levels  = cfg.levels;
+    };
+
+    systemd.packages = [ thinkfan ];
+
+    systemd.services = {
+      thinkfan.environment.THINKFAN_ARGS = escapeShellArgs ([ "-c" configFile ] ++ cfg.extraArgs);
+
+      # must be added manually, see issue #81138
+      thinkfan.wantedBy = [ "multi-user.target" ];
+      thinkfan-wakeup.wantedBy = [ "sleep.target" ];
+      thinkfan-sleep.wantedBy = [ "sleep.target" ];
+    };
+
+    boot.extraModprobeConfig = "options thinkpad_acpi experimental=1 fan_control=1";
+
+  };
+}
diff --git a/nixos/modules/services/hardware/throttled.nix b/nixos/modules/services/hardware/throttled.nix
new file mode 100644
index 00000000000..1905eb565c6
--- /dev/null
+++ b/nixos/modules/services/hardware/throttled.nix
@@ -0,0 +1,36 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.throttled;
+in {
+  options = {
+    services.throttled = {
+      enable = mkEnableOption "fix for Intel CPU throttling";
+
+      extraConfig = mkOption {
+        type = types.str;
+        default = "";
+        description = "Alternative configuration";
+      };
+    };
+  };
+
+  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" ];
+
+    environment.etc."lenovo_fix.conf".source =
+      if cfg.extraConfig != ""
+      then pkgs.writeText "lenovo_fix.conf" cfg.extraConfig
+      else "${pkgs.throttled}/etc/lenovo_fix.conf";
+
+    # Kernel 5.9 spams warnings whenever userspace writes to CPU MSRs.
+    # See https://github.com/erpalma/throttled/issues/215
+    boot.kernelParams =
+      optional (versionAtLeast config.boot.kernelPackages.kernel.version "5.9")
+      "msr.allow_writes=on";
+  };
+}
diff --git a/nixos/modules/services/hardware/tlp.nix b/nixos/modules/services/hardware/tlp.nix
new file mode 100644
index 00000000000..eb53f565a67
--- /dev/null
+++ b/nixos/modules/services/hardware/tlp.nix
@@ -0,0 +1,124 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.tlp;
+  enableRDW = config.networking.networkmanager.enable;
+  tlp = pkgs.tlp.override { inherit enableRDW; };
+  # TODO: Use this for having proper parameters in the future
+  mkTlpConfig = tlpConfig: generators.toKeyValue {
+    mkKeyValue = generators.mkKeyValueDefault {
+      mkValueString = val:
+        if isList val then "\"" + (toString val) + "\""
+        else toString val;
+    } "=";
+  } tlpConfig;
+in
+{
+  ###### interface
+  options = {
+    services.tlp = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the TLP power management daemon.";
+      };
+
+      settings = mkOption {type = with types; attrsOf (oneOf [bool int float str (listOf str)]);
+        default = {};
+        example = {
+          SATA_LINKPWR_ON_BAT = "med_power_with_dipm";
+          USB_BLACKLIST_PHONE = 1;
+        };
+        description = ''
+          Options passed to TLP. See https://linrunner.de/tlp for all supported options..
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Verbatim additional configuration variables for TLP.
+          DEPRECATED: use services.tlp.settings instead.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    boot.kernelModules = [ "msr" ];
+
+    warnings = optional (cfg.extraConfig != "") ''
+      Using config.services.tlp.extraConfig is deprecated and will become unsupported in a future release. Use config.services.tlp.settings instead.
+    '';
+
+    assertions = [{
+      assertion = cfg.enable -> config.powerManagement.scsiLinkPolicy == null;
+      message = ''
+        `services.tlp.enable` and `config.powerManagement.scsiLinkPolicy` cannot be set both.
+        Set `services.tlp.settings.SATA_LINKPWR_ON_AC` and `services.tlp.settings.SATA_LINKPWR_ON_BAT` instead.
+      '';
+    }];
+
+    environment.etc = {
+      "tlp.conf".text = (mkTlpConfig cfg.settings) + cfg.extraConfig;
+    } // optionalAttrs enableRDW {
+      "NetworkManager/dispatcher.d/99tlp-rdw-nm".source =
+        "${tlp}/etc/NetworkManager/dispatcher.d/99tlp-rdw-nm";
+    };
+
+    environment.systemPackages = [ tlp ];
+
+
+    services.tlp.settings = let
+      cfg = config.powerManagement;
+      maybeDefault = val: lib.mkIf (val != null) (lib.mkDefault val);
+    in {
+      CPU_SCALING_GOVERNOR_ON_AC = maybeDefault cfg.cpuFreqGovernor;
+      CPU_SCALING_GOVERNOR_ON_BAT = maybeDefault cfg.cpuFreqGovernor;
+      CPU_SCALING_MIN_FREQ_ON_AC = maybeDefault cfg.cpufreq.min;
+      CPU_SCALING_MAX_FREQ_ON_AC = maybeDefault cfg.cpufreq.max;
+      CPU_SCALING_MIN_FREQ_ON_BAT = maybeDefault cfg.cpufreq.min;
+      CPU_SCALING_MAX_FREQ_ON_BAT = maybeDefault cfg.cpufreq.max;
+    };
+
+    services.udev.packages = [ tlp ];
+
+    systemd = {
+      # use native tlp instead because it can also differentiate between AC/BAT
+      services.cpufreq.enable = false;
+
+      packages = [ tlp ];
+      # XXX: These must always be disabled/masked according to [1].
+      #
+      # [1]: https://github.com/linrunner/TLP/blob/a9ada09e0821f275ce5f93dc80a4d81a7ff62ae4/tlp-stat.in#L319
+      sockets.systemd-rfkill.enable = false;
+      services.systemd-rfkill.enable = false;
+
+      services.tlp = {
+        # XXX: The service should reload whenever the configuration changes,
+        # otherwise newly set power options remain inactive until reboot (or
+        # manual unit restart.)
+        restartTriggers = [ config.environment.etc."tlp.conf".source ];
+        # XXX: When using systemd.packages (which we do above) the [Install]
+        # section of systemd units does not work (citation needed) so we manually
+        # enforce it here.
+        wantedBy = [ "multi-user.target" ];
+      };
+
+      services.tlp-sleep = {
+        # XXX: When using systemd.packages (which we do above) the [Install]
+        # section of systemd units does not work (citation needed) so we manually
+        # enforce it here.
+        before = [ "sleep.target" ];
+        wantedBy = [ "sleep.target" ];
+        # XXX: `tlp suspend` requires /var/lib/tlp to exist in order to save
+        # some stuff in there. There is no way, that I know of, to do this in
+        # the package itself, so we do it here instead making sure the unit
+        # won't fail due to the save dir not existing.
+        serviceConfig.StateDirectory = "tlp";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/hardware/trezord.nix b/nixos/modules/services/hardware/trezord.nix
new file mode 100644
index 00000000000..a65d4250c2e
--- /dev/null
+++ b/nixos/modules/services/hardware/trezord.nix
@@ -0,0 +1,70 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.trezord;
+in {
+
+  ### docs
+
+  meta = {
+    doc = ./trezord.xml;
+  };
+
+  ### interface
+
+  options = {
+    services.trezord = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable Trezor bridge daemon, for use with Trezor hardware bitcoin wallets.
+        '';
+      };
+
+      emulator.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable Trezor emulator support.
+          '';
+       };
+
+      emulator.port = mkOption {
+        type = types.port;
+        default = 21324;
+        description = ''
+          Listening port for the Trezor emulator.
+          '';
+      };
+    };
+  };
+
+  ### implementation
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [ pkgs.trezor-udev-rules ];
+
+    systemd.services.trezord = {
+      description = "Trezor Bridge";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [];
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${pkgs.trezord}/bin/trezord-go ${optionalString cfg.emulator.enable "-e ${builtins.toString cfg.emulator.port}"}";
+        User = "trezord";
+      };
+    };
+
+    users.users.trezord = {
+      group = "trezord";
+      description = "Trezor bridge daemon user";
+      isSystemUser = true;
+    };
+
+    users.groups.trezord = {};
+  };
+}
+
diff --git a/nixos/modules/services/hardware/trezord.xml b/nixos/modules/services/hardware/trezord.xml
new file mode 100644
index 00000000000..972d409d9d0
--- /dev/null
+++ b/nixos/modules/services/hardware/trezord.xml
@@ -0,0 +1,26 @@
+<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/triggerhappy.nix b/nixos/modules/services/hardware/triggerhappy.nix
new file mode 100644
index 00000000000..c2fa87875e1
--- /dev/null
+++ b/nixos/modules/services/hardware/triggerhappy.nix
@@ -0,0 +1,122 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.triggerhappy;
+
+  socket = "/run/thd.socket";
+
+  configFile = pkgs.writeText "triggerhappy.conf" ''
+    ${concatMapStringsSep "\n"
+      ({ keys, event, cmd, ... }:
+        ''${concatMapStringsSep "+" (x: "KEY_" + x) keys} ${toString { press = 1; hold = 2; release = 0; }.${event}} ${cmd}''
+      )
+      cfg.bindings}
+    ${cfg.extraConfig}
+  '';
+
+  bindingCfg = { ... }: {
+    options = {
+
+      keys = mkOption {
+        type = types.listOf types.str;
+        description = "List of keys to match.  Key names as defined in linux/input-event-codes.h";
+      };
+
+      event = mkOption {
+        type = types.enum ["press" "hold" "release"];
+        default = "press";
+        description = "Event to match.";
+      };
+
+      cmd = mkOption {
+        type = types.str;
+        description = "What to run.";
+      };
+
+    };
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.triggerhappy = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the <command>triggerhappy</command> hotkey daemon.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "nobody";
+        example = "root";
+        description = ''
+          User account under which <command>triggerhappy</command> runs.
+        '';
+      };
+
+      bindings = mkOption {
+        type = types.listOf (types.submodule bindingCfg);
+        default = [];
+        example = lib.literalExpression ''
+          [ { keys = ["PLAYPAUSE"];  cmd = "''${pkgs.mpc-cli}/bin/mpc -q toggle"; } ]
+        '';
+        description = ''
+          Key bindings for <command>triggerhappy</command>.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Literal contents to append to the end of <command>triggerhappy</command> configuration file.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.sockets.triggerhappy = {
+      description = "Triggerhappy Socket";
+      wantedBy = [ "sockets.target" ];
+      socketConfig.ListenDatagram = socket;
+    };
+
+    systemd.services.triggerhappy = {
+      wantedBy = [ "multi-user.target" ];
+      description = "Global hotkey daemon";
+      serviceConfig = {
+        ExecStart = "${pkgs.triggerhappy}/bin/thd ${optionalString (cfg.user != "root") "--user ${cfg.user}"} --socket ${socket} --triggers ${configFile} --deviceglob /dev/input/event*";
+      };
+    };
+
+    services.udev.packages = lib.singleton (pkgs.writeTextFile {
+      name = "triggerhappy-udev-rules";
+      destination = "/etc/udev/rules.d/61-triggerhappy.rules";
+      text = ''
+        ACTION=="add", SUBSYSTEM=="input", KERNEL=="event[0-9]*", ATTRS{name}!="triggerhappy", \
+          RUN+="${pkgs.triggerhappy}/bin/th-cmd --socket ${socket} --passfd --udev"
+      '';
+    });
+
+  };
+
+}
diff --git a/nixos/modules/services/hardware/udev.nix b/nixos/modules/services/hardware/udev.nix
new file mode 100644
index 00000000000..61448af2d33
--- /dev/null
+++ b/nixos/modules/services/hardware/udev.nix
@@ -0,0 +1,341 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  udev = config.systemd.package;
+
+  cfg = config.services.udev;
+
+  extraUdevRules = pkgs.writeTextFile {
+    name = "extra-udev-rules";
+    text = cfg.extraRules;
+    destination = "/etc/udev/rules.d/99-local.rules";
+  };
+
+  extraHwdbFile = pkgs.writeTextFile {
+    name = "extra-hwdb-file";
+    text = cfg.extraHwdb;
+    destination = "/etc/udev/hwdb.d/99-local.hwdb";
+  };
+
+  nixosRules = ''
+    # Miscellaneous devices.
+    KERNEL=="kvm",                  MODE="0666"
+    KERNEL=="kqemu",                MODE="0666"
+
+    # Needed for gpm.
+    SUBSYSTEM=="input", KERNEL=="mice", TAG+="systemd"
+  '';
+
+  # Perform substitutions in all udev rules files.
+  udevRules = pkgs.runCommand "udev-rules"
+    { preferLocalBuild = true;
+      allowSubstitutes = false;
+      packages = unique (map toString cfg.packages);
+    }
+    ''
+      mkdir -p $out
+      shopt -s nullglob
+      set +o pipefail
+
+      # Set a reasonable $PATH for programs called by udev rules.
+      echo 'ENV{PATH}="${udevPath}/bin:${udevPath}/sbin"' > $out/00-path.rules
+
+      # Add the udev rules from other packages.
+      for i in $packages; do
+        echo "Adding rules for package $i"
+        for j in $i/{etc,lib}/udev/rules.d/*; do
+          echo "Copying $j to $out/$(basename $j)"
+          cat $j > $out/$(basename $j)
+        done
+      done
+
+      # Fix some paths in the standard udev rules.  Hacky.
+      for i in $out/*.rules; do
+        substituteInPlace $i \
+          --replace \"/sbin/modprobe \"${pkgs.kmod}/bin/modprobe \
+          --replace \"/sbin/mdadm \"${pkgs.mdadm}/sbin/mdadm \
+          --replace \"/sbin/blkid \"${pkgs.util-linux}/sbin/blkid \
+          --replace \"/bin/mount \"${pkgs.util-linux}/bin/mount \
+          --replace /usr/bin/readlink ${pkgs.coreutils}/bin/readlink \
+          --replace /usr/bin/basename ${pkgs.coreutils}/bin/basename
+      done
+
+      echo -n "Checking that all programs called by relative paths in udev rules exist in ${udev}/lib/udev... "
+      import_progs=$(grep 'IMPORT{program}="[^/$]' $out/* |
+        sed -e 's/.*IMPORT{program}="\([^ "]*\)[ "].*/\1/' | uniq)
+      run_progs=$(grep -v '^[[:space:]]*#' $out/* | grep 'RUN+="[^/$]' |
+        sed -e 's/.*RUN+="\([^ "]*\)[ "].*/\1/' | uniq)
+      for i in $import_progs $run_progs; do
+        if [[ ! -x ${udev}/lib/udev/$i && ! $i =~ socket:.* ]]; then
+          echo "FAIL"
+          echo "$i is called in udev rules but not installed by udev"
+          exit 1
+        fi
+      done
+      echo "OK"
+
+      echo -n "Checking that all programs called by absolute paths in udev rules exist... "
+      import_progs=$(grep 'IMPORT{program}="\/' $out/* |
+        sed -e 's/.*IMPORT{program}="\([^ "]*\)[ "].*/\1/' | uniq)
+      run_progs=$(grep -v '^[[:space:]]*#' $out/* | grep 'RUN+="/' |
+        sed -e 's/.*RUN+="\([^ "]*\)[ "].*/\1/' | uniq)
+      for i in $import_progs $run_progs; do
+        # if the path refers to /run/current-system/systemd, replace with config.systemd.package
+        if [[ $i == /run/current-system/systemd* ]]; then
+          i="${config.systemd.package}/''${i#/run/current-system/systemd/}"
+        fi
+        if [[ ! -x $i ]]; then
+          echo "FAIL"
+          echo "$i is called in udev rules but is not executable or does not exist"
+          exit 1
+        fi
+      done
+      echo "OK"
+
+      filesToFixup="$(for i in "$out"/*; do
+        grep -l '\B\(/usr\)\?/s\?bin' "$i" || :
+      done)"
+
+      if [ -n "$filesToFixup" ]; then
+        echo "Consider fixing the following udev rules:"
+        echo "$filesToFixup" | while read localFile; do
+          remoteFile="origin unknown"
+          for i in ${toString cfg.packages}; do
+            for j in "$i"/*/udev/rules.d/*; do
+              [ -e "$out/$(basename "$j")" ] || continue
+              [ "$(basename "$j")" = "$(basename "$localFile")" ] || continue
+              remoteFile="originally from $j"
+              break 2
+            done
+          done
+          refs="$(
+            grep -o '\B\(/usr\)\?/s\?bin/[^ "]\+' "$localFile" \
+              | sed -e ':r;N;''${s/\n/ and /;br};s/\n/, /g;br'
+          )"
+          echo "$localFile ($remoteFile) contains references to $refs."
+        done
+        exit 1
+      fi
+
+      # If auto-configuration is disabled, then remove
+      # udev's 80-drivers.rules file, which contains rules for
+      # automatically calling modprobe.
+      ${optionalString (!config.boot.hardwareScan) ''
+        ln -s /dev/null $out/80-drivers.rules
+      ''}
+    ''; # */
+
+  hwdbBin = pkgs.runCommand "hwdb.bin"
+    { preferLocalBuild = true;
+      allowSubstitutes = false;
+      packages = unique (map toString ([udev] ++ cfg.packages));
+    }
+    ''
+      mkdir -p etc/udev/hwdb.d
+      for i in $packages; do
+        echo "Adding hwdb files for package $i"
+        for j in $i/{etc,lib}/udev/hwdb.d/*; do
+          ln -s $j etc/udev/hwdb.d/$(basename $j)
+        done
+      done
+
+      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)"
+      echo "$res"
+      [ -z "$(echo "$res" | egrep '^Error')" ]
+      mv etc/udev/hwdb.bin $out
+    '';
+
+  # Udev has a 512-character limit for ENV{PATH}, so create a symlink
+  # tree to work around this.
+  udevPath = pkgs.buildEnv {
+    name = "udev-path";
+    paths = cfg.path;
+    pathsToLink = [ "/bin" "/sbin" ];
+    ignoreCollisions = true;
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    boot.hardwareScan = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to try to load kernel modules for all detected hardware.
+        Usually this does a good job of providing you with the modules
+        you need, but sometimes it can crash the system or cause other
+        nasty effects.
+      '';
+    };
+
+    services.udev = {
+
+      packages = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          List of packages containing <command>udev</command> rules.
+          All files found in
+          <filename><replaceable>pkg</replaceable>/etc/udev/rules.d</filename> and
+          <filename><replaceable>pkg</replaceable>/lib/udev/rules.d</filename>
+          will be included.
+        '';
+        apply = map getBin;
+      };
+
+      path = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          Packages added to the <envar>PATH</envar> environment variable when
+          executing programs from Udev rules.
+        '';
+      };
+
+      initrdRules = mkOption {
+        default = "";
+        example = ''
+          SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="00:1D:60:B9:6D:4F", KERNEL=="eth*", NAME="my_fast_network_card"
+        '';
+        type = types.lines;
+        description = ''
+          <command>udev</command> rules to include in the initrd
+          <emphasis>only</emphasis>. They'll be written into file
+          <filename>99-local.rules</filename>. Thus they are read and applied
+          after the essential initrd rules.
+        '';
+      };
+
+      extraRules = mkOption {
+        default = "";
+        example = ''
+          ENV{ID_VENDOR_ID}=="046d", ENV{ID_MODEL_ID}=="0825", ENV{PULSE_IGNORE}="1"
+        '';
+        type = types.lines;
+        description = ''
+          Additional <command>udev</command> rules. They'll be written
+          into file <filename>99-local.rules</filename>. Thus they are
+          read and applied after all other rules.
+        '';
+      };
+
+      extraHwdb = mkOption {
+        default = "";
+        example = ''
+          evdev:input:b0003v05AFp8277*
+            KEYBOARD_KEY_70039=leftalt
+            KEYBOARD_KEY_700e2=leftctrl
+        '';
+        type = types.lines;
+        description = ''
+          Additional <command>hwdb</command> files. They'll be written
+          into file <filename>99-local.hwdb</filename>. Thus they are
+          read after all other files.
+        '';
+      };
+
+    };
+
+    hardware.firmware = mkOption {
+      type = types.listOf types.package;
+      default = [];
+      description = ''
+        List of packages containing firmware files.  Such files
+        will be loaded automatically if the kernel asks for them
+        (i.e., when it has detected specific hardware that requires
+        firmware to function).  If multiple packages contain firmware
+        files with the same name, the first package in the list takes
+        precedence.  Note that you must rebuild your system if you add
+        files to any of these directories.
+      '';
+      apply = list: pkgs.buildEnv {
+        name = "firmware";
+        paths = list;
+        pathsToLink = [ "/lib/firmware" ];
+        ignoreCollisions = true;
+      };
+    };
+
+    networking.usePredictableInterfaceNames = mkOption {
+      default = true;
+      type = types.bool;
+      description = ''
+        Whether to assign <link
+        xlink:href='http://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames'>predictable
+        names to network interfaces</link>.  If enabled, interfaces
+        are assigned names that contain topology information
+        (e.g. <literal>wlp3s0</literal>) and thus should be stable
+        across reboots.  If disabled, names depend on the order in
+        which interfaces are discovered by the kernel, which may
+        change randomly across reboots; for instance, you may find
+        <literal>eth0</literal> and <literal>eth1</literal> flipping
+        unpredictably.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf (!config.boot.isContainer) {
+
+    services.udev.extraRules = nixosRules;
+
+    services.udev.packages = [ extraUdevRules extraHwdbFile ];
+
+    services.udev.path = [ pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.util-linux udev ];
+
+    boot.kernelParams = mkIf (!config.networking.usePredictableInterfaceNames) [ "net.ifnames=0" ];
+
+    boot.initrd.extraUdevRulesCommands = optionalString (cfg.initrdRules != "")
+      ''
+        cat <<'EOF' > $out/99-local.rules
+        ${cfg.initrdRules}
+        EOF
+      '';
+
+    environment.etc =
+      {
+        "udev/rules.d".source = udevRules;
+        "udev/hwdb.bin".source = hwdbBin;
+      };
+
+    system.requiredKernelConfig = with config.lib.kernelConfig; [
+      (isEnabled "UNIX")
+      (isYes "INOTIFY_USER")
+      (isYes "NET")
+    ];
+
+    # We don't place this into `extraModprobeConfig` so that stage-1 ramdisk doesn't bloat.
+    environment.etc."modprobe.d/firmware.conf".text = "options firmware_class path=${config.hardware.firmware}/lib/firmware";
+
+    system.activationScripts.udevd =
+      ''
+        # The deprecated hotplug uevent helper is not used anymore
+        if [ -e /proc/sys/kernel/hotplug ]; then
+          echo "" > /proc/sys/kernel/hotplug
+        fi
+
+        # Allow the kernel to find our firmware.
+        if [ -e /sys/module/firmware_class/parameters/path ]; then
+          echo -n "${config.hardware.firmware}/lib/firmware" > /sys/module/firmware_class/parameters/path
+        fi
+      '';
+
+    systemd.services.systemd-udevd =
+      { restartTriggers = cfg.packages;
+      };
+
+  };
+}
diff --git a/nixos/modules/services/hardware/udisks2.nix b/nixos/modules/services/hardware/udisks2.nix
new file mode 100644
index 00000000000..6be23f39754
--- /dev/null
+++ b/nixos/modules/services/hardware/udisks2.nix
@@ -0,0 +1,46 @@
+# Udisks daemon.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.udisks2 = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable Udisks, a DBus service that allows
+          applications to query and manipulate storage devices.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.udisks2.enable {
+
+    environment.systemPackages = [ pkgs.udisks2 ];
+
+    security.polkit.enable = true;
+
+    services.dbus.packages = [ pkgs.udisks2 ];
+
+    systemd.tmpfiles.rules = [ "d /var/lib/udisks2 0755 root root -" ];
+
+    services.udev.packages = [ pkgs.udisks2 ];
+
+    systemd.packages = [ pkgs.udisks2 ];
+  };
+
+}
diff --git a/nixos/modules/services/hardware/undervolt.nix b/nixos/modules/services/hardware/undervolt.nix
new file mode 100644
index 00000000000..a743bbf21c8
--- /dev/null
+++ b/nixos/modules/services/hardware/undervolt.nix
@@ -0,0 +1,190 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+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";
+      "${toString limit} ${toString window}";
+  cliArgs = lib.cli.toGNUCommandLine {} {
+    inherit (cfg)
+      verbose
+      temp
+      ;
+    # `core` and `cache` are both intentionally set to `cfg.coreOffset` as according to the undervolt docs:
+    #
+    #     Core or Cache offsets have no effect. It is not possible to set different offsets for
+    #     CPU Core and Cache. The CPU will take the smaller of the two offsets, and apply that to
+    #     both CPU and Cache. A warning message will be displayed if you attempt to set different offsets.
+    core = cfg.coreOffset;
+    cache = cfg.coreOffset;
+    gpu = cfg.gpuOffset;
+    uncore = cfg.uncoreOffset;
+    analogio = cfg.analogioOffset;
+
+    temp-bat = cfg.tempBat;
+    temp-ac = cfg.tempAc;
+
+    power-limit-long = mkPLimit cfg.p1.limit cfg.p1.window;
+    power-limit-short = mkPLimit cfg.p2.limit cfg.p2.window;
+  };
+in
+{
+  options.services.undervolt = {
+    enable = mkEnableOption ''
+       Undervolting service for Intel CPUs.
+
+       Warning: This service is not endorsed by Intel and may permanently damage your hardware. Use at your own risk!
+    '';
+
+    verbose = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable verbose logging.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.undervolt;
+      defaultText = literalExpression "pkgs.undervolt";
+      description = ''
+        undervolt derivation to use.
+      '';
+    };
+
+    coreOffset = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      description = ''
+        The amount of voltage in mV to offset the CPU cores by.
+      '';
+    };
+
+    gpuOffset = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      description = ''
+        The amount of voltage in mV to offset the GPU by.
+      '';
+    };
+
+    uncoreOffset = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      description = ''
+        The amount of voltage in mV to offset uncore by.
+      '';
+    };
+
+    analogioOffset = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      description = ''
+        The amount of voltage in mV to offset analogio by.
+      '';
+    };
+
+    temp = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      description = ''
+        The temperature target in Celsius degrees.
+      '';
+    };
+
+    tempAc = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      description = ''
+        The temperature target on AC power in Celsius degrees.
+      '';
+    };
+
+    tempBat = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      description = ''
+        The temperature target on battery power in Celsius degrees.
+      '';
+    };
+
+    p1.limit = mkOption {
+      type = with types; nullOr int;
+      default = null;
+      description = ''
+        The P1 Power Limit in Watts.
+        Both limit and window must be set.
+      '';
+    };
+    p1.window = mkOption {
+      type = with types; nullOr (oneOf [ float int ]);
+      default = null;
+      description = ''
+        The P1 Time Window in seconds.
+        Both limit and window must be set.
+      '';
+    };
+
+    p2.limit = mkOption {
+      type = with types; nullOr int;
+      default = null;
+      description = ''
+        The P2 Power Limit in Watts.
+        Both limit and window must be set.
+      '';
+    };
+    p2.window = mkOption {
+      type = with types; nullOr (oneOf [ float int ]);
+      default = null;
+      description = ''
+        The P2 Time Window in seconds.
+        Both limit and window must be set.
+      '';
+    };
+
+    useTimer = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to set a timer that applies the undervolt settings every 30s.
+        This will cause spam in the journal but might be required for some
+        hardware under specific conditions.
+        Enable this if your undervolt settings don't hold.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    boot.kernelModules = [ "msr" ];
+
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.undervolt = {
+      description = "Intel Undervolting Service";
+
+      # Apply undervolt on boot, nixos generation switch and resume
+      wantedBy = [ "multi-user.target" "post-resume.target" ];
+      after = [ "post-resume.target" ]; # Not sure why but it won't work without this
+
+      serviceConfig = {
+        Type = "oneshot";
+        Restart = "no";
+        ExecStart = "${cfg.package}/bin/undervolt ${toString cliArgs}";
+      };
+    };
+
+    systemd.timers.undervolt = mkIf cfg.useTimer {
+      description = "Undervolt timer to ensure voltage settings are always applied";
+      partOf = [ "undervolt.service" ];
+      wantedBy = [ "multi-user.target" ];
+      timerConfig = {
+        OnBootSec = "2min";
+        OnUnitActiveSec = "30";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/hardware/upower.nix b/nixos/modules/services/hardware/upower.nix
new file mode 100644
index 00000000000..81bf497c993
--- /dev/null
+++ b/nixos/modules/services/hardware/upower.nix
@@ -0,0 +1,239 @@
+# Upower daemon.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.upower;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.upower = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Upower, a DBus service that provides power
+          management support to applications.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.upower;
+        defaultText = literalExpression "pkgs.upower";
+        description = ''
+          Which upower package to use.
+        '';
+      };
+
+      enableWattsUpPro = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the Watts Up Pro device.
+
+          The Watts Up Pro contains a generic FTDI USB device without a specific
+          vendor and product ID. When we probe for WUP devices, we can cause
+          the user to get a perplexing "Device or resource busy" error when
+          attempting to use their non-WUP device.
+
+          The generic FTDI device is known to also be used on:
+
+          <itemizedlist>
+            <listitem><para>Sparkfun FT232 breakout board</para></listitem>
+            <listitem><para>Parallax Propeller</para></listitem>
+          </itemizedlist>
+        '';
+      };
+
+      noPollBatteries = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Don't poll the kernel for battery level changes.
+
+          Some hardware will send us battery level changes through
+          events, rather than us having to poll for it. This option
+          allows disabling polling for hardware that sends out events.
+        '';
+      };
+
+      ignoreLid = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Do we ignore the lid state
+
+          Some laptops are broken. The lid state is either inverted, or stuck
+          on or off. We can't do much to fix these problems, but this is a way
+          for users to make the laptop panel vanish, a state that might be used
+          by a couple of user-space daemons. On Linux systems, see also
+          logind.conf(5).
+        '';
+      };
+
+      usePercentageForPolicy = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Policy for warnings and action based on battery levels
+
+          Whether battery percentage based policy should be used. The default
+          is to use the percentage, which
+          should work around broken firmwares. It is also more reliable than
+          the time left (frantically saving all your files is going to use more
+          battery than letting it rest for example).
+        '';
+      };
+
+      percentageLow = mkOption {
+        type = types.ints.unsigned;
+        default = 10;
+        description = ''
+          When <literal>usePercentageForPolicy</literal> is
+          <literal>true</literal>, the levels at which UPower will consider the
+          battery low.
+
+          This will also be used for batteries which don't have time information
+          such as that of peripherals.
+
+          If any value (of <literal>percentageLow</literal>,
+          <literal>percentageCritical</literal> and
+          <literal>percentageAction</literal>) is invalid, or not in descending
+          order, the defaults will be used.
+        '';
+      };
+
+      percentageCritical = mkOption {
+        type = types.ints.unsigned;
+        default = 3;
+        description = ''
+          When <literal>usePercentageForPolicy</literal> is
+          <literal>true</literal>, the levels at which UPower will consider the
+          battery critical.
+
+          This will also be used for batteries which don't have time information
+          such as that of peripherals.
+
+          If any value (of <literal>percentageLow</literal>,
+          <literal>percentageCritical</literal> and
+          <literal>percentageAction</literal>) is invalid, or not in descending
+          order, the defaults will be used.
+        '';
+      };
+
+      percentageAction = mkOption {
+        type = types.ints.unsigned;
+        default = 2;
+        description = ''
+          When <literal>usePercentageForPolicy</literal> is
+          <literal>true</literal>, the levels at which UPower will take action
+          for the critical battery level.
+
+          This will also be used for batteries which don't have time information
+          such as that of peripherals.
+
+          If any value (of <literal>percentageLow</literal>,
+          <literal>percentageCritical</literal> and
+          <literal>percentageAction</literal>) is invalid, or not in descending
+          order, the defaults will be used.
+        '';
+      };
+
+      timeLow = mkOption {
+        type = types.ints.unsigned;
+        default = 1200;
+        description = ''
+          When <literal>usePercentageForPolicy</literal> is
+          <literal>false</literal>, the time remaining in seconds at which
+          UPower will consider the battery low.
+
+          If any value (of <literal>timeLow</literal>,
+          <literal>timeCritical</literal> and <literal>timeAction</literal>) is
+          invalid, or not in descending order, the defaults will be used.
+        '';
+      };
+
+      timeCritical = mkOption {
+        type = types.ints.unsigned;
+        default = 300;
+        description = ''
+          When <literal>usePercentageForPolicy</literal> is
+          <literal>false</literal>, the time remaining in seconds at which
+          UPower will consider the battery critical.
+
+          If any value (of <literal>timeLow</literal>,
+          <literal>timeCritical</literal> and <literal>timeAction</literal>) is
+          invalid, or not in descending order, the defaults will be used.
+        '';
+      };
+
+      timeAction = mkOption {
+        type = types.ints.unsigned;
+        default = 120;
+        description = ''
+          When <literal>usePercentageForPolicy</literal> is
+          <literal>false</literal>, the time remaining in seconds at which
+          UPower will take action for the critical battery level.
+
+          If any value (of <literal>timeLow</literal>,
+          <literal>timeCritical</literal> and <literal>timeAction</literal>) is
+          invalid, or not in descending order, the defaults will be used.
+        '';
+      };
+
+      criticalPowerAction = mkOption {
+        type = types.enum [ "PowerOff" "Hibernate" "HybridSleep" ];
+        default = "HybridSleep";
+        description = ''
+          The action to take when <literal>timeAction</literal> or
+          <literal>percentageAction</literal> has been reached for the batteries
+          (UPS or laptop batteries) supplying the computer
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ cfg.package ];
+
+    services.dbus.packages = [ cfg.package ];
+
+    services.udev.packages = [ cfg.package ];
+
+    systemd.packages = [ cfg.package ];
+
+    environment.etc."UPower/UPower.conf".text = generators.toINI {} {
+      UPower = {
+        EnableWattsUpPro = cfg.enableWattsUpPro;
+        NoPollBatteries = cfg.noPollBatteries;
+        IgnoreLid = cfg.ignoreLid;
+        UsePercentageForPolicy = cfg.usePercentageForPolicy;
+        PercentageLow = cfg.percentageLow;
+        PercentageCritical = cfg.percentageCritical;
+        PercentageAction = cfg.percentageAction;
+        TimeLow = cfg.timeLow;
+        TimeCritical = cfg.timeCritical;
+        TimeAction = cfg.timeAction;
+        CriticalPowerAction = cfg.criticalPowerAction;
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/services/hardware/usbmuxd.nix b/nixos/modules/services/hardware/usbmuxd.nix
new file mode 100644
index 00000000000..11a4b0a858f
--- /dev/null
+++ b/nixos/modules/services/hardware/usbmuxd.nix
@@ -0,0 +1,76 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  defaultUserGroup = "usbmux";
+  apple = "05ac";
+
+  cfg = config.services.usbmuxd;
+
+in
+
+{
+  options.services.usbmuxd = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable the usbmuxd ("USB multiplexing daemon") service. This daemon is
+        in charge of multiplexing connections over USB to an iOS device. This is
+        needed for transferring data from and to iOS devices (see ifuse). Also
+        this may enable plug-n-play tethering for iPhones.
+      '';
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = defaultUserGroup;
+      description = ''
+        The user usbmuxd should use to run after startup.
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = defaultUserGroup;
+      description = ''
+        The group usbmuxd should use to run after startup.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users = optionalAttrs (cfg.user == defaultUserGroup) {
+      ${cfg.user} = {
+        description = "usbmuxd user";
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == defaultUserGroup) {
+      ${cfg.group} = { };
+    };
+
+    # Give usbmuxd permission for Apple devices
+    services.udev.extraRules = ''
+      SUBSYSTEM=="usb", ATTR{idVendor}=="${apple}", GROUP="${cfg.group}"
+    '';
+
+    systemd.services.usbmuxd = {
+      description = "usbmuxd";
+      wantedBy = [ "multi-user.target" ];
+      unitConfig.Documentation = "man:usbmuxd(8)";
+      serviceConfig = {
+        # Trigger the udev rule manually. This doesn't require replugging the
+        # device when first enabling the option to get it to work
+        ExecStartPre = "${pkgs.udev}/bin/udevadm trigger -s usb -a idVendor=${apple}";
+        ExecStart = "${pkgs.usbmuxd}/bin/usbmuxd -U ${cfg.user} -f";
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/hardware/vdr.nix b/nixos/modules/services/hardware/vdr.nix
new file mode 100644
index 00000000000..5ec222b805c
--- /dev/null
+++ b/nixos/modules/services/hardware/vdr.nix
@@ -0,0 +1,82 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.vdr;
+  libDir = "/var/lib/vdr";
+in {
+
+  ###### interface
+
+  options = {
+
+    services.vdr = {
+      enable = mkEnableOption "VDR. Please put config into ${libDir}";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.vdr;
+        defaultText = literalExpression "pkgs.vdr";
+        example = literalExpression "pkgs.wrapVdr.override { plugins = with pkgs.vdrPlugins; [ hello ]; }";
+        description = "Package to use.";
+      };
+
+      videoDir = mkOption {
+        type = types.path;
+        default = "/srv/vdr/video";
+        description = "Recording directory";
+      };
+
+      extraArguments = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Additional command line arguments to pass to VDR.";
+      };
+
+      enableLirc = mkEnableOption "LIRC";
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable (mkMerge [{
+    systemd.tmpfiles.rules = [
+      "d ${cfg.videoDir} 0755 vdr vdr -"
+      "Z ${cfg.videoDir} - vdr vdr -"
+    ];
+
+    systemd.services.vdr = {
+      description = "VDR";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.package}/bin/vdr \
+            --video="${cfg.videoDir}" \
+            --config="${libDir}" \
+            ${escapeShellArgs cfg.extraArguments}
+        '';
+        User = "vdr";
+        CacheDirectory = "vdr";
+        StateDirectory = "vdr";
+        Restart = "on-failure";
+      };
+    };
+
+    users.users.vdr = {
+      group = "vdr";
+      home = libDir;
+      isSystemUser = true;
+    };
+
+    users.groups.vdr = {};
+  }
+
+  (mkIf cfg.enableLirc {
+    services.lirc.enable = true;
+    users.users.vdr.extraGroups = [ "lirc" ];
+    services.vdr.extraArguments = [
+      "--lirc=${config.passthru.lirc.socket}"
+    ];
+  })]);
+}
diff --git a/nixos/modules/services/hardware/xow.nix b/nixos/modules/services/hardware/xow.nix
new file mode 100644
index 00000000000..311181176bd
--- /dev/null
+++ b/nixos/modules/services/hardware/xow.nix
@@ -0,0 +1,20 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.services.hardware.xow;
+in {
+  options.services.hardware.xow = {
+    enable = lib.mkEnableOption "xow as a systemd service";
+  };
+
+  config = lib.mkIf cfg.enable {
+    hardware.uinput.enable = true;
+
+    boot.extraModprobeConfig = lib.readFile "${pkgs.xow}/lib/modprobe.d/xow-blacklist.conf";
+
+    systemd.packages = [ pkgs.xow ];
+    systemd.services.xow.wantedBy = [ "multi-user.target" ];
+
+    services.udev.packages = [ pkgs.xow ];
+  };
+}
diff --git a/nixos/modules/services/home-automation/home-assistant.nix b/nixos/modules/services/home-automation/home-assistant.nix
new file mode 100644
index 00000000000..6022227f6ea
--- /dev/null
+++ b/nixos/modules/services/home-automation/home-assistant.nix
@@ -0,0 +1,552 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.home-assistant;
+  format = pkgs.formats.yaml {};
+
+  # Render config attribute sets to YAML
+  # Values that are null will be filtered from the output, so this is one way to have optional
+  # options shown in settings.
+  # We post-process the result to add support for YAML functions, like secrets or includes, see e.g.
+  # https://www.home-assistant.io/docs/configuration/secrets/
+  filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ null ])) cfg.config or {};
+  configFile = pkgs.runCommand "configuration.yaml" { preferLocalBuild = true; } ''
+    cp ${format.generate "configuration.yaml" filteredConfig} $out
+    sed -i -e "s/'\!\([a-z_]\+\) \(.*\)'/\!\1 \2/;s/^\!\!/\!/;" $out
+  '';
+  lovelaceConfig = cfg.lovelaceConfig or {};
+  lovelaceConfigFile = format.generate "ui-lovelace.yaml" lovelaceConfig;
+
+  # Components advertised by the home-assistant package
+  availableComponents = cfg.package.availableComponents;
+
+  # Components that were added by overriding the package
+  explicitComponents = cfg.package.extraComponents;
+  useExplicitComponent = component: elem component explicitComponents;
+
+  # Given a component "platform", looks up whether it is used in the config
+  # as `platform = "platform";`.
+  #
+  # For example, the component mqtt.sensor is used as follows:
+  # config.sensor = [ {
+  #   platform = "mqtt";
+  #   ...
+  # } ];
+  usedPlatforms = config:
+    if isAttrs config then
+      optional (config ? platform) config.platform
+      ++ concatMap usedPlatforms (attrValues config)
+    else if isList config then
+      concatMap usedPlatforms config
+    else [ ];
+
+  useComponentPlatform = component: elem component (usedPlatforms cfg.config);
+
+  # Returns whether component is used in config, explicitly passed into package or
+  # configured in the module.
+  useComponent = component:
+    hasAttrByPath (splitString "." component) cfg.config
+    || useComponentPlatform component
+    || useExplicitComponent component
+    || builtins.elem component cfg.extraComponents;
+
+  # Final list of components passed into the package to include required dependencies
+  extraComponents = filter useComponent availableComponents;
+
+  package = (cfg.package.override (oldArgs: {
+    # Respect overrides that already exist in the passed package and
+    # concat it with values passed via the module.
+    extraComponents = oldArgs.extraComponents or [] ++ extraComponents;
+    extraPackages = ps: (oldArgs.extraPackages or (_: []) ps) ++ (cfg.extraPackages ps);
+  }));
+in {
+  imports = [
+    # Migrations in NixOS 22.05
+    (mkRemovedOptionModule [ "services" "home-assistant" "applyDefaultConfig" ] "The default config was migrated into services.home-assistant.config")
+    (mkRemovedOptionModule [ "services" "home-assistant" "autoExtraComponents" ] "Components are now parsed from services.home-assistant.config unconditionally")
+    (mkRenamedOptionModule [ "services" "home-assistant" "port" ] [ "services" "home-assistant" "config" "http" "server_port" ])
+  ];
+
+  meta = {
+    buildDocsInSandbox = false;
+    maintainers = teams.home-assistant.members;
+  };
+
+  options.services.home-assistant = {
+    # Running home-assistant on NixOS is considered an installation method that is unsupported by the upstream project.
+    # https://github.com/home-assistant/architecture/blob/master/adr/0012-define-supported-installation-method.md#decision
+    enable = mkEnableOption "Home Assistant. Please note that this installation method is unsupported upstream";
+
+    configDir = mkOption {
+      default = "/var/lib/hass";
+      type = types.path;
+      description = "The config directory, where your <filename>configuration.yaml</filename> is located.";
+    };
+
+    extraComponents = mkOption {
+      type = types.listOf (types.enum availableComponents);
+      default = [
+        # List of components required to complete the onboarding
+        "default_config"
+        "met"
+        "esphome"
+      ] ++ optionals (pkgs.stdenv.hostPlatform.isAarch32 || pkgs.stdenv.hostPlatform.isAarch64) [
+        # Use the platform as an indicator that we might be running on a RaspberryPi and include
+        # relevant components
+        "rpi_power"
+      ];
+      example = literalExpression ''
+        [
+          "analytics"
+          "default_config"
+          "esphome"
+          "my"
+          "shopping_list"
+          "wled"
+        ]
+      '';
+      description = ''
+        List of <link xlink:href="https://www.home-assistant.io/integrations/">components</link> that have their dependencies included in the package.
+
+        The component name can be found in the URL, for example <literal>https://www.home-assistant.io/integrations/ffmpeg/</literal> would map to <literal>ffmpeg</literal>.
+      '';
+    };
+
+    extraPackages = mkOption {
+      type = types.functionTo (types.listOf types.package);
+      default = _: [];
+      defaultText = literalExpression ''
+        python3Packages: with python3Packages; [];
+      '';
+      example = literalExpression ''
+        python3Packages: with python3Packages; [
+          # postgresql support
+          psycopg2
+        ];
+      '';
+      description = ''
+        List of packages to add to propagatedBuildInputs.
+
+        A popular example is <package>python3Packages.psycopg2</package>
+        for PostgreSQL support in the recorder component.
+      '';
+    };
+
+    config = mkOption {
+      type = types.nullOr (types.submodule {
+        freeformType = format.type;
+        options = {
+          # This is a partial selection of the most common options, so new users can quickly
+          # pick up how to match home-assistants config structure to ours. It also lets us preset
+          # config values intelligently.
+
+          homeassistant = {
+            # https://www.home-assistant.io/docs/configuration/basic/
+            name = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "Home";
+              description = ''
+                Name of the location where Home Assistant is running.
+              '';
+            };
+
+            latitude = mkOption {
+              type = types.nullOr (types.either types.float types.str);
+              default = null;
+              example = 52.3;
+              description = ''
+                Latitude of your location required to calculate the time the sun rises and sets.
+              '';
+            };
+
+            longitude = mkOption {
+              type = types.nullOr (types.either types.float types.str);
+              default = null;
+              example = 4.9;
+              description = ''
+                Longitude of your location required to calculate the time the sun rises and sets.
+              '';
+            };
+
+            unit_system = mkOption {
+              type = types.nullOr (types.enum [ "metric" "imperial" ]);
+              default = null;
+              example = "metric";
+              description = ''
+                The unit system to use. This also sets temperature_unit, Celsius for Metric and Fahrenheit for Imperial.
+              '';
+            };
+
+            temperature_unit = mkOption {
+              type = types.nullOr (types.enum [ "C" "F" ]);
+              default = null;
+              example = "C";
+              description = ''
+                Override temperature unit set by unit_system. <literal>C</literal> for Celsius, <literal>F</literal> for Fahrenheit.
+              '';
+            };
+
+            time_zone = mkOption {
+              type = types.nullOr types.str;
+              default = config.time.timeZone or null;
+              defaultText = literalExpression ''
+                config.time.timeZone or null
+              '';
+              example = "Europe/Amsterdam";
+              description = ''
+                Pick your time zone from the column TZ of Wikipedia’s <link xlink:href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">list of tz database time zones</link>.
+              '';
+            };
+          };
+
+          http = {
+            # https://www.home-assistant.io/integrations/http/
+            server_host = mkOption {
+              type = types.either types.str (types.listOf types.str);
+              default = [
+                "0.0.0.0"
+                "::"
+              ];
+              example = "::1";
+              description = ''
+                Only listen to incoming requests on specific IP/host. The default listed assumes support for IPv4 and IPv6.
+              '';
+            };
+
+            server_port = mkOption {
+              default = 8123;
+              type = types.port;
+              description = ''
+                The port on which to listen.
+              '';
+            };
+          };
+
+          lovelace = {
+            # https://www.home-assistant.io/lovelace/dashboards/
+            mode = mkOption {
+              type = types.enum [ "yaml" "storage" ];
+              default = if cfg.lovelaceConfig != null
+                then "yaml"
+                else "storage";
+              defaultText = literalExpression ''
+                if cfg.lovelaceConfig != null
+                  then "yaml"
+                else "storage";
+              '';
+              example = "yaml";
+              description = ''
+                In what mode should the main Lovelace panel be, <literal>yaml</literal> or <literal>storage</literal> (UI managed).
+              '';
+            };
+          };
+        };
+      });
+      example = literalExpression ''
+        {
+          homeassistant = {
+            name = "Home";
+            latitude = "!secret latitude";
+            longitude = "!secret longitude";
+            elevation = "!secret elevation";
+            unit_system = "metric";
+            time_zone = "UTC";
+          };
+          frontend = {
+            themes = "!include_dir_merge_named themes";
+          };
+          http = {};
+          feedreader.urls = [ "https://nixos.org/blogs.xml" ];
+        }
+      '';
+      description = ''
+        Your <filename>configuration.yaml</filename> as a Nix attribute set.
+
+        YAML functions like <link xlink:href="https://www.home-assistant.io/docs/configuration/secrets/">secrets</link>
+        can be passed as a string and will be unquoted automatically.
+
+        Unless this option is explicitly set to <literal>null</literal>
+        we assume your <filename>configuration.yaml</filename> is
+        managed through this module and thereby overwritten on startup.
+      '';
+    };
+
+    configWritable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to make <filename>configuration.yaml</filename> writable.
+
+        This will allow you to edit it from Home Assistant's web interface.
+
+        This only has an effect if <option>config</option> is set.
+        However, bear in mind that it will be overwritten at every start of the service.
+      '';
+    };
+
+    lovelaceConfig = mkOption {
+      default = null;
+      type = types.nullOr format.type;
+      # from https://www.home-assistant.io/lovelace/dashboards/
+      example = literalExpression ''
+        {
+          title = "My Awesome Home";
+          views = [ {
+            title = "Example";
+            cards = [ {
+              type = "markdown";
+              title = "Lovelace";
+              content = "Welcome to your **Lovelace UI**.";
+            } ];
+          } ];
+        }
+      '';
+      description = ''
+        Your <filename>ui-lovelace.yaml</filename> as a Nix attribute set.
+        Setting this option will automatically set <literal>lovelace.mode</literal> to <literal>yaml</literal>.
+
+        Beware that setting this option will delete your previous <filename>ui-lovelace.yaml</filename>
+      '';
+    };
+
+    lovelaceConfigWritable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to make <filename>ui-lovelace.yaml</filename> writable.
+
+        This will allow you to edit it from Home Assistant's web interface.
+
+        This only has an effect if <option>lovelaceConfig</option> is set.
+        However, bear in mind that it will be overwritten at every start of the service.
+      '';
+    };
+
+    package = mkOption {
+      default = pkgs.home-assistant.overrideAttrs (oldAttrs: {
+        doInstallCheck = false;
+      });
+      defaultText = literalExpression ''
+        pkgs.home-assistant.overrideAttrs (oldAttrs: {
+          doInstallCheck = false;
+        })
+      '';
+      type = types.package;
+      example = literalExpression ''
+        pkgs.home-assistant.override {
+          extraPackages = python3Packages: with python3Packages; [
+            psycopg2
+          ];
+          extraComponents = [
+            "default_config"
+            "esphome"
+            "met"
+          ];
+        }
+      '';
+      description = ''
+        The Home Assistant package to use.
+      '';
+    };
+
+    openFirewall = mkOption {
+      default = false;
+      type = types.bool;
+      description = "Whether to open the firewall for the specified port.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
+    systemd.services.home-assistant = {
+      description = "Home Assistant";
+      after = [
+        "network-online.target"
+
+        # prevent races with database creation
+        "mysql.service"
+        "postgresql.service"
+      ];
+      preStart = let
+        copyConfig = if cfg.configWritable then ''
+          cp --no-preserve=mode ${configFile} "${cfg.configDir}/configuration.yaml"
+        '' else ''
+          rm -f "${cfg.configDir}/configuration.yaml"
+          ln -s ${configFile} "${cfg.configDir}/configuration.yaml"
+        '';
+        copyLovelaceConfig = if cfg.lovelaceConfigWritable then ''
+          cp --no-preserve=mode ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
+        '' else ''
+          rm -f "${cfg.configDir}/ui-lovelace.yaml"
+          ln -s ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
+        '';
+      in
+        (optionalString (cfg.config != null) copyConfig) +
+        (optionalString (cfg.lovelaceConfig != null) copyLovelaceConfig)
+      ;
+      serviceConfig = let
+        # List of capabilities to equip home-assistant with, depending on configured components
+        capabilities = [
+          # Empty string first, so we will never accidentally have an empty capability bounding set
+          # https://github.com/NixOS/nixpkgs/issues/120617#issuecomment-830685115
+          ""
+        ] ++ (unique (optionals (useComponent "bluetooth_tracker" || useComponent "bluetooth_le_tracker") [
+          # Required for interaction with hci devices and bluetooth sockets
+          # https://www.home-assistant.io/integrations/bluetooth_le_tracker/#rootless-setup-on-core-installs
+          "CAP_NET_ADMIN"
+          "CAP_NET_RAW"
+        ] ++ lib.optionals (useComponent "emulated_hue") [
+          # Alexa looks for the service on port 80
+          # https://www.home-assistant.io/integrations/emulated_hue
+          "CAP_NET_BIND_SERVICE"
+        ] ++ lib.optionals (useComponent "nmap_tracker") [
+          # https://www.home-assistant.io/integrations/nmap_tracker#linux-capabilities
+          "CAP_NET_ADMIN"
+          "CAP_NET_BIND_SERVICE"
+          "CAP_NET_RAW"
+        ]));
+        componentsUsingBluetooth = [
+          # Components that require the AF_BLUETOOTH address family
+          "bluetooth_tracker"
+          "bluetooth_le_tracker"
+        ];
+        componentsUsingPing = [
+          # Components that require the capset syscall for the ping wrapper
+          "ping"
+          "wake_on_lan"
+        ];
+        componentsUsingSerialDevices = [
+          # Components that require access to serial devices (/dev/tty*)
+          # List generated from home-assistant documentation:
+          #   git clone https://github.com/home-assistant/home-assistant.io/
+          #   cd source/_integrations
+          #   rg "/dev/tty" -l | cut -d'/' -f3 | cut -d'.' -f1 | sort
+          # And then extended by references found in the source code, these
+          # mostly the ones using config flows already.
+          "acer_projector"
+          "alarmdecoder"
+          "arduino"
+          "blackbird"
+          "deconz"
+          "dsmr"
+          "edl21"
+          "elkm1"
+          "elv"
+          "enocean"
+          "firmata"
+          "flexit"
+          "gpsd"
+          "insteon"
+          "kwb"
+          "lacrosse"
+          "mhz19"
+          "modbus"
+          "modem_callerid"
+          "mysensors"
+          "nad"
+          "numato"
+          "rflink"
+          "rfxtrx"
+          "scsgate"
+          "serial"
+          "serial_pm"
+          "sms"
+          "upb"
+          "usb"
+          "velbus"
+          "w800rf32"
+          "xbee"
+          "zha"
+          "zwave"
+          "zwave_js"
+        ];
+      in {
+        ExecStart = "${package}/bin/hass --config '${cfg.configDir}'";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        User = "hass";
+        Group = "hass";
+        Restart = "on-failure";
+        RestartForceExitStatus = "100";
+        SuccessExitStatus = "100";
+        KillSignal = "SIGINT";
+
+        # Hardening
+        AmbientCapabilities = capabilities;
+        CapabilityBoundingSet = capabilities;
+        DeviceAllow = (optionals (any useComponent componentsUsingSerialDevices) [
+          "char-ttyACM rw"
+          "char-ttyAMA rw"
+          "char-ttyUSB rw"
+        ]);
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        PrivateUsers = false; # prevents gaining capabilities in the host namespace
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "all";
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        ReadWritePaths = let
+          # Allow rw access to explicitly configured paths
+          cfgPath = [ "config" "homeassistant" "allowlist_external_dirs" ];
+          value = attrByPath cfgPath [] cfg;
+          allowPaths = if isList value then value else singleton value;
+        in [ "${cfg.configDir}" ] ++ allowPaths;
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+          "AF_NETLINK"
+          "AF_UNIX"
+        ] ++ optionals (any useComponent componentsUsingBluetooth) [
+          "AF_BLUETOOTH"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SupplementaryGroups = optionals (any useComponent componentsUsingSerialDevices) [
+          "dialout"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+        ] ++ optionals (any useComponent componentsUsingPing) [
+          "capset"
+        ];
+        UMask = "0077";
+      };
+      path = [
+        "/run/wrappers" # needed for ping
+      ];
+    };
+
+    systemd.targets.home-assistant = rec {
+      description = "Home Assistant";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "home-assistant.service" ];
+      after = wants;
+    };
+
+    users.users.hass = {
+      home = cfg.configDir;
+      createHome = true;
+      group = "hass";
+      uid = config.ids.uids.hass;
+    };
+
+    users.groups.hass.gid = config.ids.gids.hass;
+  };
+}
diff --git a/nixos/modules/services/home-automation/zigbee2mqtt.nix b/nixos/modules/services/home-automation/zigbee2mqtt.nix
new file mode 100644
index 00000000000..ff6d595e5a6
--- /dev/null
+++ b/nixos/modules/services/home-automation/zigbee2mqtt.nix
@@ -0,0 +1,142 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.zigbee2mqtt;
+
+  format = pkgs.formats.yaml { };
+  configFile = format.generate "zigbee2mqtt.yaml" cfg.settings;
+
+in
+{
+  meta.maintainers = with maintainers; [ sweber hexa ];
+
+  imports = [
+    # Remove warning before the 21.11 release
+    (mkRenamedOptionModule [ "services" "zigbee2mqtt" "config" ] [ "services" "zigbee2mqtt" "settings" ])
+  ];
+
+  options.services.zigbee2mqtt = {
+    enable = mkEnableOption "enable zigbee2mqtt service";
+
+    package = mkOption {
+      description = "Zigbee2mqtt package to use";
+      default = pkgs.zigbee2mqtt;
+      defaultText = literalExpression ''
+        pkgs.zigbee2mqtt
+      '';
+      type = types.package;
+    };
+
+    dataDir = mkOption {
+      description = "Zigbee2mqtt data directory";
+      default = "/var/lib/zigbee2mqtt";
+      type = types.path;
+    };
+
+    settings = mkOption {
+      type = format.type;
+      default = { };
+      example = literalExpression ''
+        {
+          homeassistant = config.services.home-assistant.enable;
+          permit_join = true;
+          serial = {
+            port = "/dev/ttyACM1";
+          };
+        }
+      '';
+      description = ''
+        Your <filename>configuration.yaml</filename> as a Nix attribute set.
+        Check the <link xlink:href="https://www.zigbee2mqtt.io/information/configuration.html">documentation</link>
+        for possible options.
+      '';
+    };
+  };
+
+  config = mkIf (cfg.enable) {
+
+    # preset config values
+    services.zigbee2mqtt.settings = {
+      homeassistant = mkDefault config.services.home-assistant.enable;
+      permit_join = mkDefault false;
+      mqtt = {
+        base_topic = mkDefault "zigbee2mqtt";
+        server = mkDefault "mqtt://localhost:1883";
+      };
+      serial.port = mkDefault "/dev/ttyACM0";
+      # reference device configuration, that is kept in a separate file
+      # to prevent it being overwritten in the units ExecStartPre script
+      devices = mkDefault "devices.yaml";
+    };
+
+    systemd.services.zigbee2mqtt = {
+      description = "Zigbee2mqtt Service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      environment.ZIGBEE2MQTT_DATA = cfg.dataDir;
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/zigbee2mqtt";
+        User = "zigbee2mqtt";
+        Group = "zigbee2mqtt";
+        WorkingDirectory = cfg.dataDir;
+        Restart = "on-failure";
+
+        # Hardening
+        CapabilityBoundingSet = "";
+        DeviceAllow = [
+          config.services.zigbee2mqtt.settings.serial.port
+        ];
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = false;
+        NoNewPrivileges = true;
+        PrivateDevices = false; # prevents access to /dev/serial, because it is set 0700 root:root
+        PrivateUsers = true;
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        ProtectSystem = "strict";
+        ReadWritePaths = cfg.dataDir;
+        RemoveIPC = true;
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SupplementaryGroups = [
+          "dialout"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+          "~@resources"
+        ];
+        UMask = "0077";
+      };
+      preStart = ''
+        cp --no-preserve=mode ${configFile} "${cfg.dataDir}/configuration.yaml"
+      '';
+    };
+
+    users.users.zigbee2mqtt = {
+      home = cfg.dataDir;
+      createHome = true;
+      group = "zigbee2mqtt";
+      uid = config.ids.uids.zigbee2mqtt;
+    };
+
+    users.groups.zigbee2mqtt.gid = config.ids.gids.zigbee2mqtt;
+  };
+}
diff --git a/nixos/modules/services/logging/SystemdJournal2Gelf.nix b/nixos/modules/services/logging/SystemdJournal2Gelf.nix
new file mode 100644
index 00000000000..f28ecab8ac2
--- /dev/null
+++ b/nixos/modules/services/logging/SystemdJournal2Gelf.nix
@@ -0,0 +1,60 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.SystemdJournal2Gelf;
+in
+
+{ options = {
+    services.SystemdJournal2Gelf = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable SystemdJournal2Gelf.
+        '';
+      };
+
+      graylogServer = mkOption {
+        type = types.str;
+        example = "graylog2.example.com:11201";
+        description = ''
+          Host and port of your graylog2 input. This should be a GELF
+          UDP input.
+        '';
+      };
+
+      extraOptions = mkOption {
+        type = types.separatedString " ";
+        default = "";
+        description = ''
+          Any extra flags to pass to SystemdJournal2Gelf. Note that
+          these are basically <literal>journalctl</literal> flags.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.systemd-journal2gelf;
+        defaultText = literalExpression "pkgs.systemd-journal2gelf";
+        description = ''
+          SystemdJournal2Gelf package to use.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.SystemdJournal2Gelf = {
+      description = "SystemdJournal2Gelf";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/SystemdJournal2Gelf ${cfg.graylogServer} --follow ${cfg.extraOptions}";
+        Restart = "on-failure";
+        RestartSec = "30";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/logging/awstats.nix b/nixos/modules/services/logging/awstats.nix
new file mode 100644
index 00000000000..df0124380ff
--- /dev/null
+++ b/nixos/modules/services/logging/awstats.nix
@@ -0,0 +1,257 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.awstats;
+  package = pkgs.awstats;
+  configOpts = {name, config, ...}: {
+    options = {
+      type = mkOption{
+        type = types.enum [ "mail" "web" ];
+        default = "web";
+        example = "mail";
+        description = ''
+          The type of log being collected.
+        '';
+      };
+      domain = mkOption {
+        type = types.str;
+        default = name;
+        description = "The domain name to collect stats for.";
+        example = "example.com";
+      };
+
+      logFile = mkOption {
+        type = types.str;
+        example = "/var/log/nginx/access.log";
+        description = ''
+          The log file to be scanned.
+
+          For mail, set this to
+          <literal>
+          journalctl $OLD_CURSOR -u postfix.service | ''${pkgs.perl}/bin/perl ''${pkgs.awstats.out}/share/awstats/tools/maillogconvert.pl standard |
+          </literal>
+        '';
+      };
+
+      logFormat = mkOption {
+        type = types.str;
+        default = "1";
+        description = ''
+          The log format being used.
+
+          For mail, set this to
+          <literal>
+          %time2 %email %email_r %host %host_r %method %url %code %bytesd
+          </literal>
+        '';
+      };
+
+      hostAliases = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "www.example.org" ];
+        description = ''
+          List of aliases the site has.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        example = literalExpression ''
+          {
+            "ValidHTTPCodes" = "404";
+          }
+        '';
+        description = "Extra configuration to be appended to awstats.\${name}.conf.";
+      };
+
+      webService = {
+        enable = mkEnableOption "awstats web service";
+
+        hostname = mkOption {
+          type = types.str;
+          default = config.domain;
+          description = "The hostname the web service appears under.";
+        };
+
+        urlPrefix = mkOption {
+          type = types.str;
+          default = "/awstats";
+          description = "The URL prefix under which the awstats pages appear.";
+        };
+      };
+    };
+  };
+  webServices = filterAttrs (name: value: value.webService.enable) cfg.configs;
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "awstats" "service" "enable" ] "Please enable per domain with `services.awstats.configs.<name>.webService.enable`")
+    (mkRemovedOptionModule [ "services" "awstats" "service" "urlPrefix" ] "Please set per domain with `services.awstats.configs.<name>.webService.urlPrefix`")
+    (mkRenamedOptionModule [ "services" "awstats" "vardir" ] [ "services" "awstats" "dataDir" ])
+  ];
+
+  options.services.awstats = {
+    enable = mkEnableOption "awstats";
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/awstats";
+      description = "The directory where awstats data will be stored.";
+    };
+
+    configs = mkOption {
+      type = types.attrsOf (types.submodule configOpts);
+      default = {};
+      example = literalExpression ''
+        {
+          "mysite" = {
+            domain = "example.com";
+            logFile = "/var/log/nginx/access.log";
+          };
+        }
+      '';
+      description = "Attribute set of domains to collect stats for.";
+    };
+
+    updateAt = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "hourly";
+      description = ''
+        Specification of the time at which awstats will get updated.
+        (in the format described by <citerefentry>
+          <refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>)
+      '';
+    };
+  };
+
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ package.bin ];
+
+    environment.etc = mapAttrs' (name: opts:
+    nameValuePair "awstats/awstats.${name}.conf" {
+      source = pkgs.runCommand "awstats.${name}.conf"
+      { preferLocalBuild = true; }
+      (''
+        sed \
+      ''
+      # set up mail stats
+      + optionalString (opts.type == "mail")
+      ''
+        -e 's|^\(LogType\)=.*$|\1=M|' \
+        -e 's|^\(LevelForBrowsersDetection\)=.*$|\1=0|' \
+        -e 's|^\(LevelForOSDetection\)=.*$|\1=0|' \
+        -e 's|^\(LevelForRefererAnalyze\)=.*$|\1=0|' \
+        -e 's|^\(LevelForRobotsDetection\)=.*$|\1=0|' \
+        -e 's|^\(LevelForSearchEnginesDetection\)=.*$|\1=0|' \
+        -e 's|^\(LevelForFileTypesDetection\)=.*$|\1=0|' \
+        -e 's|^\(LevelForWormsDetection\)=.*$|\1=0|' \
+        -e 's|^\(ShowMenu\)=.*$|\1=1|' \
+        -e 's|^\(ShowSummary\)=.*$|\1=HB|' \
+        -e 's|^\(ShowMonthStats\)=.*$|\1=HB|' \
+        -e 's|^\(ShowDaysOfMonthStats\)=.*$|\1=HB|' \
+        -e 's|^\(ShowDaysOfWeekStats\)=.*$|\1=HB|' \
+        -e 's|^\(ShowHoursStats\)=.*$|\1=HB|' \
+        -e 's|^\(ShowDomainsStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowHostsStats\)=.*$|\1=HB|' \
+        -e 's|^\(ShowAuthenticatedUsers\)=.*$|\1=0|' \
+        -e 's|^\(ShowRobotsStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowEMailSenders\)=.*$|\1=HBML|' \
+        -e 's|^\(ShowEMailReceivers\)=.*$|\1=HBML|' \
+        -e 's|^\(ShowSessionsStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowPagesStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowFileTypesStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowFileSizesStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowBrowsersStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowOSStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowOriginStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowKeyphrasesStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowKeywordsStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowMiscStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowHTTPErrorsStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowSMTPErrorsStats\)=.*$|\1=1|' \
+      ''
+      +
+      # common options
+      ''
+        -e 's|^\(DirData\)=.*$|\1="${cfg.dataDir}/${name}"|' \
+        -e 's|^\(DirIcons\)=.*$|\1="icons"|' \
+        -e 's|^\(CreateDirDataIfNotExists\)=.*$|\1=1|' \
+        -e 's|^\(SiteDomain\)=.*$|\1="${name}"|' \
+        -e 's|^\(LogFile\)=.*$|\1="${opts.logFile}"|' \
+        -e 's|^\(LogFormat\)=.*$|\1="${opts.logFormat}"|' \
+      ''
+      +
+      # extra config
+      concatStringsSep "\n" (mapAttrsToList (n: v: ''
+        -e 's|^\(${n}\)=.*$|\1="${v}"|' \
+      '') opts.extraConfig)
+      +
+      ''
+        < '${package.out}/wwwroot/cgi-bin/awstats.model.conf' > "$out"
+      '');
+    }) cfg.configs;
+
+    # create data directory with the correct permissions
+    systemd.tmpfiles.rules =
+      [ "d '${cfg.dataDir}' 755 root root - -" ] ++
+      mapAttrsToList (name: opts: "d '${cfg.dataDir}/${name}' 755 root root - -") cfg.configs ++
+      [ "Z '${cfg.dataDir}' 755 root root - -" ];
+
+    # nginx options
+    services.nginx.virtualHosts = mapAttrs'(name: opts: {
+      name = opts.webService.hostname;
+      value = {
+        locations = {
+          "${opts.webService.urlPrefix}/css/" = {
+            alias = "${package.out}/wwwroot/css/";
+          };
+          "${opts.webService.urlPrefix}/icons/" = {
+            alias = "${package.out}/wwwroot/icon/";
+          };
+          "${opts.webService.urlPrefix}/" = {
+            alias = "${cfg.dataDir}/${name}/";
+            extraConfig = ''
+              autoindex on;
+            '';
+          };
+        };
+      };
+    }) webServices;
+
+    # update awstats
+    systemd.services = mkIf (cfg.updateAt != null) (mapAttrs' (name: opts:
+      nameValuePair "awstats-${name}-update" {
+        description = "update awstats for ${name}";
+        script = optionalString (opts.type == "mail")
+        ''
+          if [[ -f "${cfg.dataDir}/${name}-cursor" ]]; then
+            CURSOR="$(cat "${cfg.dataDir}/${name}-cursor" | tr -d '\n')"
+            if [[ -n "$CURSOR" ]]; then
+              echo "Using cursor: $CURSOR"
+              export OLD_CURSOR="--cursor $CURSOR"
+            fi
+          fi
+          NEW_CURSOR="$(journalctl $OLD_CURSOR -u postfix.service --show-cursor | tail -n 1 | tr -d '\n' | sed -e 's#^-- cursor: \(.*\)#\1#')"
+          echo "New cursor: $NEW_CURSOR"
+          ${package.bin}/bin/awstats -update -config=${name}
+          if [ -n "$NEW_CURSOR" ]; then
+            echo -n "$NEW_CURSOR" > ${cfg.dataDir}/${name}-cursor
+          fi
+        '' + ''
+          ${package.out}/share/awstats/tools/awstats_buildstaticpages.pl \
+            -config=${name} -update -dir=${cfg.dataDir}/${name} \
+            -awstatsprog=${package.bin}/bin/awstats
+        '';
+        startAt = cfg.updateAt;
+    }) cfg.configs);
+  };
+
+}
+
diff --git a/nixos/modules/services/logging/filebeat.nix b/nixos/modules/services/logging/filebeat.nix
new file mode 100644
index 00000000000..223a993c505
--- /dev/null
+++ b/nixos/modules/services/logging/filebeat.nix
@@ -0,0 +1,253 @@
+{ config, lib, utils, pkgs, ... }:
+
+let
+  inherit (lib)
+    attrValues
+    literalExpression
+    mkEnableOption
+    mkIf
+    mkOption
+    types;
+
+  cfg = config.services.filebeat;
+
+  json = pkgs.formats.json {};
+in
+{
+  options = {
+
+    services.filebeat = {
+
+      enable = mkEnableOption "filebeat";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.filebeat;
+        defaultText = literalExpression "pkgs.filebeat";
+        example = literalExpression "pkgs.filebeat7";
+        description = ''
+          The filebeat package to use.
+        '';
+      };
+
+      inputs = mkOption {
+        description = ''
+          Inputs specify how Filebeat locates and processes input data.
+
+          This is like <literal>services.filebeat.settings.filebeat.inputs</literal>,
+          but structured as an attribute set. This has the benefit
+          that multiple NixOS modules can contribute settings to a
+          single filebeat input.
+
+          An input type can be specified multiple times by choosing a
+          different <literal>&lt;name></literal> for each, but setting
+          <xref linkend="opt-services.filebeat.inputs._name_.type"/>
+          to the same value.
+
+          See <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/configuration-filebeat-options.html"/>.
+        '';
+        default = {};
+        type = types.attrsOf (types.submodule ({ name, ... }: {
+          freeformType = json.type;
+          options = {
+            type = mkOption {
+              type = types.str;
+              default = name;
+              description = ''
+                The input type.
+
+                Look for the value after <literal>type:</literal> on
+                the individual input pages linked from
+                <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/configuration-filebeat-options.html"/>.
+              '';
+            };
+          };
+        }));
+        example = literalExpression ''
+          {
+            journald.id = "everything";  # Only for filebeat7
+            log = {
+              enabled = true;
+              paths = [
+                "/var/log/*.log"
+              ];
+            };
+          };
+        '';
+      };
+
+      modules = mkOption {
+        description = ''
+          Filebeat modules provide a quick way to get started
+          processing common log formats. They contain default
+          configurations, Elasticsearch ingest pipeline definitions,
+          and Kibana dashboards to help you implement and deploy a log
+          monitoring solution.
+
+          This is like <literal>services.filebeat.settings.filebeat.modules</literal>,
+          but structured as an attribute set. This has the benefit
+          that multiple NixOS modules can contribute settings to a
+          single filebeat module.
+
+          A module can be specified multiple times by choosing a
+          different <literal>&lt;name></literal> for each, but setting
+          <xref linkend="opt-services.filebeat.modules._name_.module"/>
+          to the same value.
+
+          See <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-modules.html"/>.
+        '';
+        default = {};
+        type = types.attrsOf (types.submodule ({ name, ... }: {
+          freeformType = json.type;
+          options = {
+            module = mkOption {
+              type = types.str;
+              default = name;
+              description = ''
+                The name of the module.
+
+                Look for the value after <literal>module:</literal> on
+                the individual input pages linked from
+                <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-modules.html"/>.
+              '';
+            };
+          };
+        }));
+        example = literalExpression ''
+          {
+            nginx = {
+              access = {
+                enabled = true;
+                var.paths = [ "/path/to/log/nginx/access.log*" ];
+              };
+              error = {
+                enabled = true;
+                var.paths = [ "/path/to/log/nginx/error.log*" ];
+              };
+            };
+          };
+        '';
+      };
+
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = json.type;
+
+          options = {
+
+            output.elasticsearch.hosts = mkOption {
+              type = with types; listOf str;
+              default = [ "127.0.0.1:9200" ];
+              example = [ "myEShost:9200" ];
+              description = ''
+                The list of Elasticsearch nodes to connect to.
+
+                The events are distributed to these nodes in round
+                robin order. If one node becomes unreachable, the
+                event is automatically sent to another node. Each
+                Elasticsearch node can be defined as a URL or
+                IP:PORT. For example:
+                <literal>http://192.15.3.2</literal>,
+                <literal>https://es.found.io:9230</literal> or
+                <literal>192.24.3.2:9300</literal>. If no port is
+                specified, <literal>9200</literal> is used.
+              '';
+            };
+
+            filebeat = {
+              inputs = mkOption {
+                type = types.listOf json.type;
+                default = [];
+                internal = true;
+                description = ''
+                  Inputs specify how Filebeat locates and processes
+                  input data. Use <xref
+                  linkend="opt-services.filebeat.inputs"/> instead.
+
+                  See <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/configuration-filebeat-options.html"/>.
+                '';
+              };
+              modules = mkOption {
+                type = types.listOf json.type;
+                default = [];
+                internal = true;
+                description = ''
+                  Filebeat modules provide a quick way to get started
+                  processing common log formats. They contain default
+                  configurations, Elasticsearch ingest pipeline
+                  definitions, and Kibana dashboards to help you
+                  implement and deploy a log monitoring solution.
+
+                  Use <xref linkend="opt-services.filebeat.modules"/> instead.
+
+                  See <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-modules.html"/>.
+                '';
+              };
+            };
+          };
+        };
+        default = {};
+        example = literalExpression ''
+          {
+            settings = {
+              output.elasticsearch = {
+                hosts = [ "myEShost:9200" ];
+                username = "filebeat_internal";
+                password = { _secret = "/var/keys/elasticsearch_password"; };
+              };
+              logging.level = "info";
+            };
+          };
+        '';
+
+        description = ''
+          Configuration for filebeat. See
+          <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-reference-yml.html"/>
+          for supported values.
+
+          Options containing secret data should be set to an attribute
+          set containing the attribute <literal>_secret</literal> - a
+          string pointing to a file containing the value the option
+          should be set to. See the example to get a better picture of
+          this: in the resulting
+          <filename>filebeat.yml</filename> file, the
+          <literal>output.elasticsearch.password</literal>
+          key will be set to the contents of the
+          <filename>/var/keys/elasticsearch_password</filename> file.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    services.filebeat.settings.filebeat.inputs = attrValues cfg.inputs;
+    services.filebeat.settings.filebeat.modules = attrValues cfg.modules;
+
+    systemd.services.filebeat = {
+      description = "Filebeat log shipper";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "elasticsearch.service" ];
+      after = [ "elasticsearch.service" ];
+      serviceConfig = {
+        ExecStartPre = pkgs.writeShellScript "filebeat-exec-pre" ''
+          set -euo pipefail
+
+          umask u=rwx,g=,o=
+
+          ${utils.genJqSecretsReplacementSnippet
+              cfg.settings
+              "/var/lib/filebeat/filebeat.yml"
+           }
+        '';
+        ExecStart = ''
+          ${cfg.package}/bin/filebeat -e \
+            -c "/var/lib/filebeat/filebeat.yml" \
+            --path.data "/var/lib/filebeat"
+        '';
+        Restart = "always";
+        StateDirectory = "filebeat";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/logging/fluentd.nix b/nixos/modules/services/logging/fluentd.nix
new file mode 100644
index 00000000000..dd19617a13f
--- /dev/null
+++ b/nixos/modules/services/logging/fluentd.nix
@@ -0,0 +1,58 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.fluentd;
+
+  pluginArgs = concatStringsSep " " (map (x: "-p ${x}") cfg.plugins);
+in {
+  ###### interface
+
+  options = {
+
+    services.fluentd = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable fluentd.";
+      };
+
+      config = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Fluentd config.";
+      };
+
+      package = mkOption {
+        type = types.path;
+        default = pkgs.fluentd;
+        defaultText = literalExpression "pkgs.fluentd";
+        description = "The fluentd package to use.";
+      };
+
+      plugins = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          A list of plugin paths to pass into fluentd. It will make plugins defined in ruby files
+          there available in your config.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.fluentd = with pkgs; {
+      description = "Fluentd Daemon";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/fluentd -c ${pkgs.writeText "fluentd.conf" cfg.config} ${pluginArgs}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/logging/graylog.nix b/nixos/modules/services/logging/graylog.nix
new file mode 100644
index 00000000000..e6a23233ba2
--- /dev/null
+++ b/nixos/modules/services/logging/graylog.nix
@@ -0,0 +1,169 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.graylog;
+
+  confFile = pkgs.writeText "graylog.conf" ''
+    is_master = ${boolToString cfg.isMaster}
+    node_id_file = ${cfg.nodeIdFile}
+    password_secret = ${cfg.passwordSecret}
+    root_username = ${cfg.rootUsername}
+    root_password_sha2 = ${cfg.rootPasswordSha2}
+    elasticsearch_hosts = ${concatStringsSep "," cfg.elasticsearchHosts}
+    message_journal_dir = ${cfg.messageJournalDir}
+    mongodb_uri = ${cfg.mongodbUri}
+    plugin_dir = /var/lib/graylog/plugins
+
+    ${cfg.extraConfig}
+  '';
+
+  glPlugins = pkgs.buildEnv {
+    name = "graylog-plugins";
+    paths = cfg.plugins;
+  };
+
+in
+
+{
+  ###### interface
+
+  options = {
+
+    services.graylog = {
+
+      enable = mkEnableOption "Graylog";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.graylog;
+        defaultText = literalExpression "pkgs.graylog";
+        description = "Graylog package to use.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "graylog";
+        description = "User account under which graylog runs";
+      };
+
+      isMaster = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether this is the master instance of your Graylog cluster";
+      };
+
+      nodeIdFile = mkOption {
+        type = types.str;
+        default = "/var/lib/graylog/server/node-id";
+        description = "Path of the file containing the graylog node-id";
+      };
+
+      passwordSecret = mkOption {
+        type = types.str;
+        description = ''
+          You MUST set a secret to secure/pepper the stored user passwords here. Use at least 64 characters.
+          Generate one by using for example: pwgen -N 1 -s 96
+        '';
+      };
+
+      rootUsername = mkOption {
+        type = types.str;
+        default = "admin";
+        description = "Name of the default administrator user";
+      };
+
+      rootPasswordSha2 = mkOption {
+        type = types.str;
+        example = "e3c652f0ba0b4801205814f8b6bc49672c4c74e25b497770bb89b22cdeb4e952";
+        description = ''
+          You MUST specify a hash password for the root user (which you only need to initially set up the
+          system and in case you lose connectivity to your authentication backend)
+          This password cannot be changed using the API or via the web interface. If you need to change it,
+          modify it here.
+          Create one by using for example: echo -n yourpassword | shasum -a 256
+          and use the resulting hash value as string for the option
+        '';
+      };
+
+      elasticsearchHosts = mkOption {
+        type = types.listOf types.str;
+        example = literalExpression ''[ "http://node1:9200" "http://user:password@node2:19200" ]'';
+        description = "List of valid URIs of the http ports of your elastic nodes. If one or more of your elasticsearch hosts require authentication, include the credentials in each node URI that requires authentication";
+      };
+
+      messageJournalDir = mkOption {
+        type = types.str;
+        default = "/var/lib/graylog/data/journal";
+        description = "The directory which will be used to store the message journal. The directory must be exclusively used by Graylog and must not contain any other files than the ones created by Graylog itself";
+      };
+
+      mongodbUri = mkOption {
+        type = types.str;
+        default = "mongodb://localhost/graylog";
+        description = "MongoDB connection string. See http://docs.mongodb.org/manual/reference/connection-string/ for details";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Any other configuration options you might want to add";
+      };
+
+      plugins = mkOption {
+        description = "Extra graylog plugins";
+        default = [ ];
+        type = types.listOf types.package;
+      };
+
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users = mkIf (cfg.user == "graylog") {
+      graylog = {
+        isSystemUser = true;
+        group = "graylog";
+        description = "Graylog server daemon user";
+      };
+    };
+    users.groups = mkIf (cfg.user == "graylog") {};
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.messageJournalDir}' - ${cfg.user} - - -"
+    ];
+
+    systemd.services.graylog = {
+      description = "Graylog Server";
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        GRAYLOG_CONF = "${confFile}";
+      };
+      path = [ pkgs.which pkgs.procps ];
+      preStart = ''
+        rm -rf /var/lib/graylog/plugins || true
+        mkdir -p /var/lib/graylog/plugins -m 755
+
+        mkdir -p "$(dirname ${cfg.nodeIdFile})"
+        chown -R ${cfg.user} "$(dirname ${cfg.nodeIdFile})"
+
+        for declarativeplugin in `ls ${glPlugins}/bin/`; do
+          ln -sf ${glPlugins}/bin/$declarativeplugin /var/lib/graylog/plugins/$declarativeplugin
+        done
+        for includedplugin in `ls ${cfg.package}/plugin/`; do
+          ln -s ${cfg.package}/plugin/$includedplugin /var/lib/graylog/plugins/$includedplugin || true
+        done
+      '';
+      serviceConfig = {
+        User="${cfg.user}";
+        StateDirectory = "graylog";
+        ExecStart = "${cfg.package}/bin/graylogctl run";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/logging/heartbeat.nix b/nixos/modules/services/logging/heartbeat.nix
new file mode 100644
index 00000000000..56fb4deabda
--- /dev/null
+++ b/nixos/modules/services/logging/heartbeat.nix
@@ -0,0 +1,74 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.heartbeat;
+
+  heartbeatYml = pkgs.writeText "heartbeat.yml" ''
+    name: ${cfg.name}
+    tags: ${builtins.toJSON cfg.tags}
+
+    ${cfg.extraConfig}
+  '';
+
+in
+{
+  options = {
+
+    services.heartbeat = {
+
+      enable = mkEnableOption "heartbeat";
+
+      name = mkOption {
+        type = types.str;
+        default = "heartbeat";
+        description = "Name of the beat";
+      };
+
+      tags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Tags to place on the shipped log messages";
+      };
+
+      stateDir = mkOption {
+        type = types.str;
+        default = "/var/lib/heartbeat";
+        description = "The state directory. heartbeat's own logs and other data are stored here.";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = ''
+          heartbeat.monitors:
+          - type: http
+            urls: ["http://localhost:9200"]
+            schedule: '@every 10s'
+        '';
+        description = "Any other configuration options you want to add";
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}' - nobody nogroup - -"
+    ];
+
+    systemd.services.heartbeat = with pkgs; {
+      description = "heartbeat log shipper";
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        mkdir -p "${cfg.stateDir}"/{data,logs}
+      '';
+      serviceConfig = {
+        User = "nobody";
+        AmbientCapabilities = "cap_net_raw";
+        ExecStart = "${pkgs.heartbeat}/bin/heartbeat -c \"${heartbeatYml}\" -path.data \"${cfg.stateDir}/data\" -path.logs \"${cfg.stateDir}/logs\"";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/logging/journalbeat.nix b/nixos/modules/services/logging/journalbeat.nix
new file mode 100644
index 00000000000..4035ab48b4b
--- /dev/null
+++ b/nixos/modules/services/logging/journalbeat.nix
@@ -0,0 +1,94 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.journalbeat;
+
+  journalbeatYml = pkgs.writeText "journalbeat.yml" ''
+    name: ${cfg.name}
+    tags: ${builtins.toJSON cfg.tags}
+
+    ${cfg.extraConfig}
+  '';
+
+in
+{
+  options = {
+
+    services.journalbeat = {
+
+      enable = mkEnableOption "journalbeat";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.journalbeat;
+        defaultText = literalExpression "pkgs.journalbeat";
+        description = ''
+          The journalbeat package to use
+        '';
+      };
+
+      name = mkOption {
+        type = types.str;
+        default = "journalbeat";
+        description = "Name of the beat";
+      };
+
+      tags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Tags to place on the shipped log messages";
+      };
+
+      stateDir = mkOption {
+        type = types.str;
+        default = "journalbeat";
+        description = ''
+          Directory below <literal>/var/lib/</literal> to store journalbeat's
+          own logs and other data. This directory will be created automatically
+          using systemd's StateDirectory mechanism.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Any other configuration options you want to add";
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = !hasPrefix "/" cfg.stateDir;
+        message =
+          "The option services.journalbeat.stateDir shouldn't be an absolute directory." +
+          " It should be a directory relative to /var/lib/.";
+      }
+    ];
+
+    systemd.services.journalbeat = {
+      description = "Journalbeat log shipper";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "elasticsearch.service" ];
+      after = [ "elasticsearch.service" ];
+      preStart = ''
+        mkdir -p ${cfg.stateDir}/data
+        mkdir -p ${cfg.stateDir}/logs
+      '';
+      serviceConfig = {
+        StateDirectory = cfg.stateDir;
+        ExecStart = ''
+          ${cfg.package}/bin/journalbeat \
+            -c ${journalbeatYml} \
+            -path.data /var/lib/${cfg.stateDir}/data \
+            -path.logs /var/lib/${cfg.stateDir}/logs'';
+        Restart = "always";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/logging/journaldriver.nix b/nixos/modules/services/logging/journaldriver.nix
new file mode 100644
index 00000000000..9bd581e9ec0
--- /dev/null
+++ b/nixos/modules/services/logging/journaldriver.nix
@@ -0,0 +1,112 @@
+# This module implements a systemd service for running journaldriver,
+# a log forwarding agent that sends logs from journald to Stackdriver
+# Logging.
+#
+# It can be enabled without extra configuration when running on GCP.
+# On machines hosted elsewhere, the other configuration options need
+# to be set.
+#
+# For further information please consult the documentation in the
+# upstream repository at: https://github.com/tazjin/journaldriver/
+
+{ config, lib, pkgs, ...}:
+
+with lib; let cfg = config.services.journaldriver;
+in {
+  options.services.journaldriver = {
+    enable = mkOption {
+      type        = types.bool;
+      default     = false;
+      description = ''
+        Whether to enable journaldriver to forward journald logs to
+        Stackdriver Logging.
+      '';
+    };
+
+    logLevel = mkOption {
+      type        = types.str;
+      default     = "info";
+      description = ''
+        Log level at which journaldriver logs its own output.
+      '';
+    };
+
+    logName = mkOption {
+      type        = with types; nullOr str;
+      default     = null;
+      description = ''
+        Configures the name of the target log in Stackdriver Logging.
+        This option can be set to, for example, the hostname of a
+        machine to improve the user experience in the logging
+        overview.
+      '';
+    };
+
+    googleCloudProject = mkOption {
+      type        = with types; nullOr str;
+      default     = null;
+      description = ''
+        Configures the name of the Google Cloud project to which to
+        forward journald logs.
+
+        This option is required on non-GCP machines, but should not be
+        set on GCP instances.
+      '';
+    };
+
+    logStream = mkOption {
+      type        = with types; nullOr str;
+      default     = null;
+      description = ''
+        Configures the name of the Stackdriver Logging log stream into
+        which to write journald entries.
+
+        This option is required on non-GCP machines, but should not be
+        set on GCP instances.
+      '';
+    };
+
+    applicationCredentials = mkOption {
+      type        = with types; nullOr path;
+      default     = null;
+      description = ''
+        Path to the service account private key (in JSON-format) used
+        to forward log entries to Stackdriver Logging on non-GCP
+        instances.
+
+        This option is required on non-GCP machines, but should not be
+        set on GCP instances.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.journaldriver = {
+      description = "Stackdriver Logging journal forwarder";
+      script      = "${pkgs.journaldriver}/bin/journaldriver";
+      after       = [ "network-online.target" ];
+      wantedBy    = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Restart        = "always";
+        DynamicUser    = true;
+
+        # This directive lets systemd automatically configure
+        # permissions on /var/lib/journaldriver, the directory in
+        # which journaldriver persists its cursor state.
+        StateDirectory = "journaldriver";
+
+        # This group is required for accessing journald.
+        SupplementaryGroups = "systemd-journal";
+      };
+
+      environment = {
+        RUST_LOG                       = cfg.logLevel;
+        LOG_NAME                       = cfg.logName;
+        LOG_STREAM                     = cfg.logStream;
+        GOOGLE_CLOUD_PROJECT           = cfg.googleCloudProject;
+        GOOGLE_APPLICATION_CREDENTIALS = cfg.applicationCredentials;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/logging/journalwatch.nix b/nixos/modules/services/logging/journalwatch.nix
new file mode 100644
index 00000000000..fb86904d1ea
--- /dev/null
+++ b/nixos/modules/services/logging/journalwatch.nix
@@ -0,0 +1,265 @@
+{ config, lib, pkgs, ... }:
+with lib;
+
+let
+  cfg = config.services.journalwatch;
+  user = "journalwatch";
+  # for journal access
+  group = "systemd-journal";
+  dataDir = "/var/lib/${user}";
+
+  journalwatchConfig = pkgs.writeText "config" (''
+    # (File Generated by NixOS journalwatch module.)
+    [DEFAULT]
+    mail_binary = ${cfg.mailBinary}
+    priority = ${toString cfg.priority}
+    mail_from = ${cfg.mailFrom}
+  ''
+  + optionalString (cfg.mailTo != null) ''
+    mail_to = ${cfg.mailTo}
+  ''
+  + cfg.extraConfig);
+
+  journalwatchPatterns = pkgs.writeText "patterns" ''
+    # (File Generated by NixOS journalwatch module.)
+
+    ${mkPatterns cfg.filterBlocks}
+  '';
+
+  # empty line at the end needed to to separate the blocks
+  mkPatterns = filterBlocks: concatStringsSep "\n" (map (block: ''
+    ${block.match}
+    ${block.filters}
+
+  '') filterBlocks);
+
+  # can't use joinSymlinks directly, because when we point $XDG_CONFIG_HOME
+  # to the /nix/store path, we still need the subdirectory "journalwatch" inside that
+  # to match journalwatch's expectations
+  journalwatchConfigDir = pkgs.runCommand "journalwatch-config"
+    { preferLocalBuild = true; allowSubstitutes = false; }
+    ''
+      mkdir -p $out/journalwatch
+      ln -sf ${journalwatchConfig} $out/journalwatch/config
+      ln -sf ${journalwatchPatterns} $out/journalwatch/patterns
+    '';
+
+
+in {
+  options = {
+    services.journalwatch = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If enabled, periodically check the journal with journalwatch and report the results by mail.
+        '';
+      };
+
+      priority = mkOption {
+        type = types.int;
+        default = 6;
+        description = ''
+          Lowest priority of message to be considered.
+          A value between 7 ("debug"), and 0 ("emerg"). Defaults to 6 ("info").
+          If you don't care about anything with "info" priority, you can reduce
+          this to e.g. 5 ("notice") to considerably reduce the amount of
+          messages without needing many <option>filterBlocks</option>.
+        '';
+      };
+
+      # HACK: this is a workaround for journalwatch's usage of socket.getfqdn() which always returns localhost if
+      # there's an alias for the localhost on a separate line in /etc/hosts, or take for ages if it's not present and
+      # then return something right-ish in the direction of /etc/hostname. Just bypass it completely.
+      mailFrom = mkOption {
+        type = types.str;
+        default = "journalwatch@${config.networking.hostName}";
+        defaultText = literalExpression ''"journalwatch@''${config.networking.hostName}"'';
+        description = ''
+          Mail address to send journalwatch reports from.
+        '';
+      };
+
+      mailTo = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Mail address to send journalwatch reports to.
+        '';
+      };
+
+      mailBinary = mkOption {
+        type = types.path;
+        default = "/run/wrappers/bin/sendmail";
+        description = ''
+          Sendmail-compatible binary to be used to send the messages.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Extra lines to be added verbatim to the journalwatch/config configuration file.
+          You can add any commandline argument to the config, without the '--'.
+          See <literal>journalwatch --help</literal> for all arguments and their description.
+          '';
+      };
+
+      filterBlocks = mkOption {
+        type = types.listOf (types.submodule {
+          options = {
+           match = mkOption {
+              type = types.str;
+              example = "SYSLOG_IDENTIFIER = systemd";
+              description = ''
+                Syntax: <literal>field = value</literal>
+                Specifies the log entry <literal>field</literal> this block should apply to.
+                If the <literal>field</literal> of a message matches this <literal>value</literal>,
+                this patternBlock's <option>filters</option> are applied.
+                If <literal>value</literal> starts and ends with a slash, it is interpreted as
+                an extended python regular expression, if not, it's an exact match.
+                The journal fields are explained in systemd.journal-fields(7).
+              '';
+            };
+
+            filters = mkOption {
+              type = types.str;
+              example = ''
+                (Stopped|Stopping|Starting|Started) .*
+                (Reached target|Stopped target) .*
+              '';
+              description = ''
+                The filters to apply on all messages which satisfy <option>match</option>.
+                Any of those messages that match any specified filter will be removed from journalwatch's output.
+                Each filter is an extended Python regular expression.
+                You can specify multiple filters and separate them by newlines.
+                Lines starting with '#' are comments. Inline-comments are not permitted.
+              '';
+            };
+          };
+        });
+
+        example = [
+          # examples taken from upstream
+          {
+            match = "_SYSTEMD_UNIT = systemd-logind.service";
+            filters = ''
+              New session [a-z]?\d+ of user \w+\.
+              Removed session [a-z]?\d+\.
+            '';
+          }
+
+          {
+            match = "SYSLOG_IDENTIFIER = /(CROND|crond)/";
+            filters = ''
+              pam_unix\(crond:session\): session (opened|closed) for user \w+
+              \(\w+\) CMD .*
+            '';
+          }
+        ];
+
+        # another example from upstream.
+        # very useful on priority = 6, and required as journalwatch throws an error when no pattern is defined at all.
+        default = [
+          {
+            match = "SYSLOG_IDENTIFIER = systemd";
+            filters = ''
+              (Stopped|Stopping|Starting|Started) .*
+              (Created slice|Removed slice) user-\d*\.slice\.
+              Received SIGRTMIN\+24 from PID .*
+              (Reached target|Stopped target) .*
+              Startup finished in \d*ms\.
+            '';
+          }
+        ];
+
+
+        description = ''
+          filterBlocks can be defined to blacklist journal messages which are not errors.
+          Each block matches on a log entry field, and the filters in that block then are matched
+          against all messages with a matching log entry field.
+
+          All messages whose PRIORITY is at least 6 (INFO) are processed by journalwatch.
+          If you don't specify any filterBlocks, PRIORITY is reduced to 5 (NOTICE) by default.
+
+          All regular expressions are extended Python regular expressions, for details
+          see: http://doc.pyschools.com/html/regex.html
+        '';
+      };
+
+      interval = mkOption {
+        type = types.str;
+        default = "hourly";
+        description = ''
+          How often to run journalwatch.
+
+          The format is described in systemd.time(7).
+        '';
+      };
+      accuracy = mkOption {
+        type = types.str;
+        default = "10min";
+        description = ''
+          The time window around the interval in which the journalwatch run will be scheduled.
+
+          The format is described in systemd.time(7).
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users.${user} = {
+      isSystemUser = true;
+      home = dataDir;
+      group = group;
+    };
+
+    systemd.tmpfiles.rules = [
+      # present since NixOS 19.09: remove old stateful symlink join directory,
+      # which has been replaced with the journalwatchConfigDir store path
+      "R ${dataDir}/config"
+    ];
+
+    systemd.services.journalwatch = {
+
+      environment = {
+        # journalwatch stores the last processed timpestamp here
+        # the share subdirectory is historic now that config home lives in /nix/store,
+        # but moving this in a backwards-compatible way is much more work than what's justified
+        # for cleaning that up.
+        XDG_DATA_HOME = "${dataDir}/share";
+        XDG_CONFIG_HOME = journalwatchConfigDir;
+      };
+      serviceConfig = {
+        User = user;
+        Group = group;
+        Type = "oneshot";
+        # requires a relative directory name to create beneath /var/lib
+        StateDirectory = user;
+        StateDirectoryMode = 0750;
+        ExecStart = "${pkgs.python3Packages.journalwatch}/bin/journalwatch mail";
+        # lowest CPU and IO priority, but both still in best-effort class to prevent starvation
+        Nice=19;
+        IOSchedulingPriority=7;
+      };
+    };
+
+    systemd.timers.journalwatch = {
+      description = "Periodic journalwatch run";
+      wantedBy = [ "timers.target" ];
+      timerConfig = {
+        OnCalendar = cfg.interval;
+        AccuracySec = cfg.accuracy;
+        Persistent = true;
+      };
+    };
+
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ florianjacob ];
+  };
+}
diff --git a/nixos/modules/services/logging/klogd.nix b/nixos/modules/services/logging/klogd.nix
new file mode 100644
index 00000000000..8d371c161eb
--- /dev/null
+++ b/nixos/modules/services/logging/klogd.nix
@@ -0,0 +1,38 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  ###### interface
+
+  options = {
+
+    services.klogd.enable = mkOption {
+      type = types.bool;
+      default = versionOlder (getVersion config.boot.kernelPackages.kernel) "3.5";
+      defaultText = literalExpression ''versionOlder (getVersion config.boot.kernelPackages.kernel) "3.5"'';
+      description = ''
+        Whether to enable klogd, the kernel log message processing
+        daemon.  Since systemd handles logging of kernel messages on
+        Linux 3.5 and later, this is only useful if you're running an
+        older kernel.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.klogd.enable {
+    systemd.services.klogd = {
+      description = "Kernel Log Daemon";
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.sysklogd ];
+      unitConfig.ConditionVirtualization = "!systemd-nspawn";
+      script =
+        "klogd -c 1 -2 -n " +
+        "-k $(dirname $(readlink -f /run/booted-system/kernel))/System.map";
+    };
+  };
+}
diff --git a/nixos/modules/services/logging/logcheck.nix b/nixos/modules/services/logging/logcheck.nix
new file mode 100644
index 00000000000..c8738b734f9
--- /dev/null
+++ b/nixos/modules/services/logging/logcheck.nix
@@ -0,0 +1,242 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.logcheck;
+
+  defaultRules = pkgs.runCommand "logcheck-default-rules" { preferLocalBuild = true; } ''
+                   cp -prd ${pkgs.logcheck}/etc/logcheck $out
+                   chmod u+w $out
+                   rm -r $out/logcheck.*
+                 '';
+
+  rulesDir = pkgs.symlinkJoin
+    { name = "logcheck-rules-dir";
+      paths = ([ defaultRules ] ++ cfg.extraRulesDirs);
+    };
+
+  configFile = pkgs.writeText "logcheck.conf" cfg.config;
+
+  logFiles = pkgs.writeText "logcheck.logfiles" cfg.files;
+
+  flags = "-r ${rulesDir} -c ${configFile} -L ${logFiles} -${levelFlag} -m ${cfg.mailTo}";
+
+  levelFlag = getAttrFromPath [cfg.level]
+    { paranoid    = "p";
+      server      = "s";
+      workstation = "w";
+    };
+
+  cronJob = ''
+    @reboot   logcheck env PATH=/run/wrappers/bin:$PATH nice -n10 ${pkgs.logcheck}/sbin/logcheck -R ${flags}
+    2 ${cfg.timeOfDay} * * * logcheck env PATH=/run/wrappers/bin:$PATH nice -n10 ${pkgs.logcheck}/sbin/logcheck ${flags}
+  '';
+
+  writeIgnoreRule = name: {level, regex, ...}:
+    pkgs.writeTextFile
+      { inherit name;
+        destination = "/ignore.d.${level}/${name}";
+        text = ''
+          ^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ ${regex}
+        '';
+      };
+
+  writeIgnoreCronRule = name: {level, user, regex, cmdline, ...}:
+    let escapeRegex = escape (stringToCharacters "\\[]{}()^$?*+|.");
+        cmdline_ = builtins.unsafeDiscardStringContext cmdline;
+        re = if regex != "" then regex else if cmdline_ == "" then ".*" else escapeRegex cmdline_;
+    in writeIgnoreRule "cron-${name}" {
+      inherit level;
+      regex = ''
+        (/usr/bin/)?cron\[[0-9]+\]: \(${user}\) CMD \(${re}\)$
+      '';
+    };
+
+  levelOption = mkOption {
+    default = "server";
+    type = types.enum [ "workstation" "server" "paranoid" ];
+    description = ''
+      Set the logcheck level.
+    '';
+  };
+
+  ignoreOptions = {
+    options = {
+      level = levelOption;
+
+      regex = mkOption {
+        default = "";
+        type = types.str;
+        description = ''
+          Regex specifying which log lines to ignore.
+        '';
+      };
+    };
+  };
+
+  ignoreCronOptions = {
+    options = {
+      user = mkOption {
+        default = "root";
+        type = types.str;
+        description = ''
+          User that runs the cronjob.
+        '';
+      };
+
+      cmdline = mkOption {
+        default = "";
+        type = types.str;
+        description = ''
+          Command line for the cron job. Will be turned into a regex for the logcheck ignore rule.
+        '';
+      };
+
+      timeArgs = mkOption {
+        default = null;
+        type = types.nullOr (types.str);
+        example = "02 06 * * *";
+        description = ''
+          "min hr dom mon dow" crontab time args, to auto-create a cronjob too.
+          Leave at null to not do this and just add a logcheck ignore rule.
+        '';
+      };
+    };
+  };
+
+in
+{
+  options = {
+    services.logcheck = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable the logcheck cron job.
+        '';
+      };
+
+      user = mkOption {
+        default = "logcheck";
+        type = types.str;
+        description = ''
+          Username for the logcheck user.
+        '';
+      };
+
+      timeOfDay = mkOption {
+        default = "*";
+        example = "6";
+        type = types.str;
+        description = ''
+          Time of day to run logcheck. A logcheck will be scheduled at xx:02 each day.
+          Leave default (*) to run every hour. Of course when nothing special was logged,
+          logcheck will be silent.
+        '';
+      };
+
+      mailTo = mkOption {
+        default = "root";
+        example = "you@domain.com";
+        type = types.str;
+        description = ''
+          Email address to send reports to.
+        '';
+      };
+
+      level = mkOption {
+        default = "server";
+        type = types.str;
+        description = ''
+          Set the logcheck level. Either "workstation", "server", or "paranoid".
+        '';
+      };
+
+      config = mkOption {
+        default = "FQDN=1";
+        type = types.lines;
+        description = ''
+          Config options that you would like in logcheck.conf.
+        '';
+      };
+
+      files = mkOption {
+        default = [ "/var/log/messages" ];
+        type = types.listOf types.path;
+        example = [ "/var/log/messages" "/var/log/mail" ];
+        description = ''
+          Which log files to check.
+        '';
+      };
+
+      extraRulesDirs = mkOption {
+        default = [];
+        example = [ "/etc/logcheck" ];
+        type = types.listOf types.path;
+        description = ''
+          Directories with extra rules.
+        '';
+      };
+
+      ignore = mkOption {
+        default = {};
+        description = ''
+          This option defines extra ignore rules.
+        '';
+        type = with types; attrsOf (submodule ignoreOptions);
+      };
+
+      ignoreCron = mkOption {
+        default = {};
+        description = ''
+          This option defines extra ignore rules for cronjobs.
+        '';
+        type = with types; attrsOf (submodule ignoreCronOptions);
+      };
+
+      extraGroups = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        example = [ "postdrop" "mongodb" ];
+        description = ''
+          Extra groups for the logcheck user, for example to be able to use sendmail,
+          or to access certain log files.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.logcheck.extraRulesDirs =
+        mapAttrsToList writeIgnoreRule cfg.ignore
+        ++ mapAttrsToList writeIgnoreCronRule cfg.ignoreCron;
+
+    users.users = optionalAttrs (cfg.user == "logcheck") {
+      logcheck = {
+        group = "logcheck";
+        isSystemUser = true;
+        shell = "/bin/sh";
+        description = "Logcheck user account";
+        extraGroups = cfg.extraGroups;
+      };
+    };
+    users.groups = optionalAttrs (cfg.user == "logcheck") {
+      logcheck = {};
+    };
+
+    system.activationScripts.logcheck = ''
+      mkdir -m 700 -p /var/{lib,lock}/logcheck
+      chown ${cfg.user} /var/{lib,lock}/logcheck
+    '';
+
+    services.cron.systemCronJobs =
+        let withTime = name: {timeArgs, ...}: timeArgs != null;
+            mkCron = name: {user, cmdline, timeArgs, ...}: ''
+              ${timeArgs} ${user} ${cmdline}
+            '';
+        in mapAttrsToList mkCron (filterAttrs withTime cfg.ignoreCron)
+           ++ [ cronJob ];
+  };
+}
diff --git a/nixos/modules/services/logging/logrotate.nix b/nixos/modules/services/logging/logrotate.nix
new file mode 100644
index 00000000000..082cf92ff4e
--- /dev/null
+++ b/nixos/modules/services/logging/logrotate.nix
@@ -0,0 +1,179 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.logrotate;
+
+  pathOpts = { name, ... }:  {
+    options = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable log rotation for this path. This can be used to explicitly disable
+          logging that has been configured by NixOS.
+        '';
+      };
+
+      name = mkOption {
+        type = types.str;
+        internal = true;
+      };
+
+      path = mkOption {
+        type = with types; either str (listOf str);
+        default = name;
+        defaultText = "attribute name";
+        description = ''
+          The path to log files to be rotated.
+          Spaces are allowed and normal shell quoting rules apply,
+          with ', ", and \ characters supported.
+        '';
+      };
+
+      user = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          The user account to use for rotation.
+        '';
+      };
+
+      group = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          The group to use for rotation.
+        '';
+      };
+
+      frequency = mkOption {
+        type = types.enum [ "hourly" "daily" "weekly" "monthly" "yearly" ];
+        default = "daily";
+        description = ''
+          How often to rotate the logs.
+        '';
+      };
+
+      keep = mkOption {
+        type = types.int;
+        default = 20;
+        description = ''
+          How many rotations to keep.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra logrotate config options for this path. Refer to
+          <link xlink:href="https://linux.die.net/man/8/logrotate"/> for details.
+        '';
+      };
+
+      priority = mkOption {
+        type = types.int;
+        default = 1000;
+        description = ''
+          Order of this logrotate block in relation to the others. The semantics are
+          the same as with `lib.mkOrder`. Smaller values have a greater priority.
+        '';
+      };
+    };
+
+    config.name = name;
+  };
+
+  mkConf = pathOpts: ''
+    # generated by NixOS using the `services.logrotate.paths.${pathOpts.name}` attribute set
+    ${concatMapStringsSep " " (path: ''"${path}"'') (toList pathOpts.path)} {
+      ${optionalString (pathOpts.user != null || pathOpts.group != null) "su ${pathOpts.user} ${pathOpts.group}"}
+      ${pathOpts.frequency}
+      rotate ${toString pathOpts.keep}
+      ${pathOpts.extraConfig}
+    }
+  '';
+
+  paths = sortProperties (attrValues (filterAttrs (_: pathOpts: pathOpts.enable) cfg.paths));
+  configFile = pkgs.writeText "logrotate.conf" (
+    concatStringsSep "\n" (
+      [ "missingok" "notifempty" cfg.extraConfig ] ++ (map mkConf paths)
+    )
+  );
+
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "logrotate" "config" ] [ "services" "logrotate" "extraConfig" ])
+  ];
+
+  options = {
+    services.logrotate = {
+      enable = mkEnableOption "the logrotate systemd service" // {
+        default = foldr (n: a: a || n.enable) false (attrValues cfg.paths);
+        defaultText = literalExpression "cfg.paths != {}";
+      };
+
+      paths = mkOption {
+        type = with types; attrsOf (submodule pathOpts);
+        default = {};
+        description = ''
+          Attribute set of paths to rotate. The order each block appears in the generated configuration file
+          can be controlled by the <link linkend="opt-services.logrotate.paths._name_.priority">priority</link> option
+          using the same semantics as `lib.mkOrder`. Smaller values have a greater priority.
+        '';
+        example = literalExpression ''
+          {
+            httpd = {
+              path = "/var/log/httpd/*.log";
+              user = config.services.httpd.user;
+              group = config.services.httpd.group;
+              keep = 7;
+            };
+
+            myapp = {
+              path = "/var/log/myapp/*.log";
+              user = "myuser";
+              group = "mygroup";
+              frequency = "weekly";
+              keep = 5;
+              priority = 1;
+            };
+          }
+        '';
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra contents to append to the logrotate configuration file. Refer to
+          <link xlink:href="https://linux.die.net/man/8/logrotate"/> for details.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = mapAttrsToList (name: pathOpts:
+      { assertion = (pathOpts.user != null) == (pathOpts.group != null);
+        message = ''
+          If either of `services.logrotate.paths.${name}.user` or `services.logrotate.paths.${name}.group` are specified then *both* must be specified.
+        '';
+      }
+    ) cfg.paths;
+
+    systemd.services.logrotate = {
+      description = "Logrotate Service";
+      startAt = "hourly";
+
+      serviceConfig = {
+        Restart = "no";
+        User = "root";
+        ExecStart = "${pkgs.logrotate}/sbin/logrotate ${configFile}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/logging/logstash.nix b/nixos/modules/services/logging/logstash.nix
new file mode 100644
index 00000000000..a08203dffe7
--- /dev/null
+++ b/nixos/modules/services/logging/logstash.nix
@@ -0,0 +1,194 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.logstash;
+  ops = lib.optionalString;
+  verbosityFlag = "--log.level " + cfg.logLevel;
+
+  logstashConf = pkgs.writeText "logstash.conf" ''
+    input {
+      ${cfg.inputConfig}
+    }
+
+    filter {
+      ${cfg.filterConfig}
+    }
+
+    output {
+      ${cfg.outputConfig}
+    }
+  '';
+
+  logstashSettingsYml = pkgs.writeText "logstash.yml" cfg.extraSettings;
+
+  logstashJvmOptionsFile = pkgs.writeText "jvm.options" cfg.extraJvmOptions;
+
+  logstashSettingsDir = pkgs.runCommand "logstash-settings" {
+      inherit logstashJvmOptionsFile;
+      inherit logstashSettingsYml;
+      preferLocalBuild = true;
+    } ''
+    mkdir -p $out
+    ln -s $logstashSettingsYml $out/logstash.yml
+    ln -s $logstashJvmOptionsFile $out/jvm.options
+  '';
+in
+
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "logstash" "address" ] [ "services" "logstash" "listenAddress" ])
+    (mkRemovedOptionModule [ "services" "logstash" "enableWeb" ] "The web interface was removed from logstash")
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.logstash = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable logstash.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.logstash;
+        defaultText = literalExpression "pkgs.logstash";
+        description = "Logstash package to use.";
+      };
+
+      plugins = mkOption {
+        type = types.listOf types.path;
+        default = [ ];
+        example = literalExpression "[ pkgs.logstash-contrib ]";
+        description = "The paths to find other logstash plugins in.";
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/logstash";
+        description = ''
+          A path to directory writable by logstash that it uses to store data.
+          Plugins will also have access to this path.
+        '';
+      };
+
+      logLevel = mkOption {
+        type = types.enum [ "debug" "info" "warn" "error" "fatal" ];
+        default = "warn";
+        description = "Logging verbosity level.";
+      };
+
+      filterWorkers = mkOption {
+        type = types.int;
+        default = 1;
+        description = "The quantity of filter workers to run.";
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "Address on which to start webserver.";
+      };
+
+      port = mkOption {
+        type = types.str;
+        default = "9292";
+        description = "Port on which to start webserver.";
+      };
+
+      inputConfig = mkOption {
+        type = types.lines;
+        default = "generator { }";
+        description = "Logstash input configuration.";
+        example = literalExpression ''
+          '''
+            # Read from journal
+            pipe {
+              command => "''${pkgs.systemd}/bin/journalctl -f -o json"
+              type => "syslog" codec => json {}
+            }
+          '''
+        '';
+      };
+
+      filterConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "logstash filter configuration.";
+        example = ''
+          if [type] == "syslog" {
+            # Keep only relevant systemd fields
+            # http://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html
+            prune {
+              whitelist_names => [
+                "type", "@timestamp", "@version",
+                "MESSAGE", "PRIORITY", "SYSLOG_FACILITY"
+              ]
+            }
+          }
+        '';
+      };
+
+      outputConfig = mkOption {
+        type = types.lines;
+        default = "stdout { codec => rubydebug }";
+        description = "Logstash output configuration.";
+        example = ''
+          redis { host => ["localhost"] data_type => "list" key => "logstash" codec => json }
+          elasticsearch { }
+        '';
+      };
+
+      extraSettings = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Extra Logstash settings in YAML format.";
+        example = ''
+          pipeline:
+            batch:
+              size: 125
+              delay: 5
+        '';
+      };
+
+      extraJvmOptions = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Extra JVM options, one per line (jvm.options format).";
+        example = ''
+          -Xms2g
+          -Xmx2g
+        '';
+      };
+
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.logstash = {
+      description = "Logstash Daemon";
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.bash ];
+      serviceConfig = {
+        ExecStartPre = ''${pkgs.coreutils}/bin/mkdir -p "${cfg.dataDir}" ; ${pkgs.coreutils}/bin/chmod 700 "${cfg.dataDir}"'';
+        ExecStart = concatStringsSep " " (filter (s: stringLength s != 0) [
+          "${cfg.package}/bin/logstash"
+          "-w ${toString cfg.filterWorkers}"
+          (concatMapStringsSep " " (x: "--path.plugins ${x}") cfg.plugins)
+          "${verbosityFlag}"
+          "-f ${logstashConf}"
+          "--path.settings ${logstashSettingsDir}"
+          "--path.data ${cfg.dataDir}"
+        ]);
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/logging/promtail.nix b/nixos/modules/services/logging/promtail.nix
new file mode 100644
index 00000000000..a34bc07b6ab
--- /dev/null
+++ b/nixos/modules/services/logging/promtail.nix
@@ -0,0 +1,91 @@
+{ config, lib, pkgs, ... }: with lib;
+let
+  cfg = config.services.promtail;
+
+  prettyJSON = conf: pkgs.runCommandLocal "promtail-config.json" {} ''
+    echo '${builtins.toJSON conf}' | ${pkgs.buildPackages.jq}/bin/jq 'del(._module)' > $out
+  '';
+
+  allowSystemdJournal = cfg.configuration ? scrape_configs && lib.any (v: v ? journal) cfg.configuration.scrape_configs;
+
+  allowPositionsFile = !lib.hasPrefix "/var/cache/promtail" positionsFile;
+  positionsFile = cfg.configuration.positions.filename;
+in {
+  options.services.promtail = with types; {
+    enable = mkEnableOption "the Promtail ingresser";
+
+
+    configuration = mkOption {
+      type = (pkgs.formats.json {}).type;
+      description = ''
+        Specify the configuration for Promtail in Nix.
+      '';
+    };
+
+    extraFlags = mkOption {
+      type = listOf str;
+      default = [];
+      example = [ "--server.http-listen-port=3101" ];
+      description = ''
+        Specify a list of additional command line flags,
+        which get escaped and are then passed to Loki.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.promtail.configuration.positions.filename = mkDefault "/var/cache/promtail/positions.yaml";
+
+    systemd.services.promtail = {
+      description = "Promtail log ingress";
+      wantedBy = [ "multi-user.target" ];
+      stopIfChanged = false;
+
+      serviceConfig = {
+        Restart = "on-failure";
+        TimeoutStopSec = 10;
+
+        ExecStart = "${pkgs.promtail}/bin/promtail -config.file=${prettyJSON cfg.configuration} ${escapeShellArgs cfg.extraFlags}";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        CacheDirectory = "promtail";
+        ReadWritePaths = lib.optional allowPositionsFile (builtins.dirOf positionsFile);
+
+        User = "promtail";
+        Group = "promtail";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+
+        ProtectKernelModules = true;
+        SystemCallArchitectures = "native";
+        ProtectKernelLogs = true;
+        ProtectClock = true;
+
+        LockPersonality = true;
+        ProtectHostname = true;
+        RestrictRealtime = true;
+        MemoryDenyWriteExecute = true;
+        PrivateUsers = true;
+
+        SupplementaryGroups = lib.optional (allowSystemdJournal) "systemd-journal";
+      } // (optionalAttrs (!pkgs.stdenv.isAarch64) { # FIXME: figure out why this breaks on aarch64
+        SystemCallFilter = "@system-service";
+      });
+    };
+
+    users.groups.promtail = {};
+    users.users.promtail = {
+      description = "Promtail service user";
+      isSystemUser = true;
+      group = "promtail";
+    };
+  };
+}
diff --git a/nixos/modules/services/logging/rsyslogd.nix b/nixos/modules/services/logging/rsyslogd.nix
new file mode 100644
index 00000000000..b924d94e0b0
--- /dev/null
+++ b/nixos/modules/services/logging/rsyslogd.nix
@@ -0,0 +1,105 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.rsyslogd;
+
+  syslogConf = pkgs.writeText "syslog.conf" ''
+    $ModLoad imuxsock
+    $SystemLogSocketName /run/systemd/journal/syslog
+    $WorkDirectory /var/spool/rsyslog
+
+    ${cfg.defaultConfig}
+    ${cfg.extraConfig}
+  '';
+
+  defaultConf = ''
+    # "local1" is used for dhcpd messages.
+    local1.*                     -/var/log/dhcpd
+
+    mail.*                       -/var/log/mail
+
+    *.=warning;*.=err            -/var/log/warn
+    *.crit                        /var/log/warn
+
+    *.*;mail.none;local1.none    -/var/log/messages
+  '';
+
+in
+
+{
+  ###### interface
+
+  options = {
+
+    services.rsyslogd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable syslogd.  Note that systemd also logs
+          syslog messages, so you normally don't need to run syslogd.
+        '';
+      };
+
+      defaultConfig = mkOption {
+        type = types.lines;
+        default = defaultConf;
+        description = ''
+          The default <filename>syslog.conf</filename> file configures a
+          fairly standard setup of log files, which can be extended by
+          means of <varname>extraConfig</varname>.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = "news.* -/var/log/news";
+        description = ''
+          Additional text appended to <filename>syslog.conf</filename>,
+          i.e. the contents of <varname>defaultConfig</varname>.
+        '';
+      };
+
+      extraParams = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "-m 0" ];
+        description = ''
+          Additional parameters passed to <command>rsyslogd</command>.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.rsyslog ];
+
+    systemd.services.syslog =
+      { description = "Syslog Daemon";
+
+        requires = [ "syslog.socket" ];
+
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig =
+          { ExecStart = "${pkgs.rsyslog}/sbin/rsyslogd ${toString cfg.extraParams} -f ${syslogConf} -n";
+            ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p /var/spool/rsyslog";
+            # Prevent syslogd output looping back through journald.
+            StandardOutput = "null";
+          };
+      };
+
+  };
+
+}
diff --git a/nixos/modules/services/logging/syslog-ng.nix b/nixos/modules/services/logging/syslog-ng.nix
new file mode 100644
index 00000000000..0a57bf20bd0
--- /dev/null
+++ b/nixos/modules/services/logging/syslog-ng.nix
@@ -0,0 +1,101 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.syslog-ng;
+
+  syslogngConfig = pkgs.writeText "syslog-ng.conf" ''
+    ${cfg.configHeader}
+    ${cfg.extraConfig}
+  '';
+
+  ctrlSocket = "/run/syslog-ng/syslog-ng.ctl";
+  pidFile = "/run/syslog-ng/syslog-ng.pid";
+  persistFile = "/var/syslog-ng/syslog-ng.persist";
+
+  syslogngOptions = [
+    "--foreground"
+    "--module-path=${concatStringsSep ":" (["${cfg.package}/lib/syslog-ng"] ++ cfg.extraModulePaths)}"
+    "--cfgfile=${syslogngConfig}"
+    "--control=${ctrlSocket}"
+    "--persist-file=${persistFile}"
+    "--pidfile=${pidFile}"
+  ];
+
+in {
+  imports = [
+    (mkRemovedOptionModule [ "services" "syslog-ng" "serviceName" ] "")
+    (mkRemovedOptionModule [ "services" "syslog-ng" "listenToJournal" ] "")
+  ];
+
+  options = {
+
+    services.syslog-ng = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the syslog-ng daemon.
+        '';
+      };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.syslogng;
+        defaultText = literalExpression "pkgs.syslogng";
+        description = ''
+          The package providing syslog-ng binaries.
+        '';
+      };
+      extraModulePaths = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = literalExpression ''
+          [ "''${pkgs.syslogng_incubator}/lib/syslog-ng" ]
+        '';
+        description = ''
+          A list of paths that should be included in syslog-ng's
+          <literal>--module-path</literal> option. They should usually
+          end in <literal>/lib/syslog-ng</literal>
+        '';
+      };
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Configuration added to the end of <literal>syslog-ng.conf</literal>.
+        '';
+      };
+      configHeader = mkOption {
+        type = types.lines;
+        default = ''
+          @version: 3.6
+          @include "scl.conf"
+        '';
+        description = ''
+          The very first lines of the configuration file. Should usually contain
+          the syslog-ng version header.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.syslog-ng = {
+      description = "syslog-ng daemon";
+      preStart = "mkdir -p /{var,run}/syslog-ng";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "multi-user.target" ]; # makes sure hostname etc is set
+      serviceConfig = {
+        Type = "notify";
+        PIDFile = pidFile;
+        StandardOutput = "null";
+        Restart = "on-failure";
+        ExecStart = "${cfg.package}/sbin/syslog-ng ${concatStringsSep " " syslogngOptions}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/services/logging/syslogd.nix b/nixos/modules/services/logging/syslogd.nix
new file mode 100644
index 00000000000..fe0b0490811
--- /dev/null
+++ b/nixos/modules/services/logging/syslogd.nix
@@ -0,0 +1,130 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.syslogd;
+
+  syslogConf = pkgs.writeText "syslog.conf" ''
+    ${if (cfg.tty != "") then "kern.warning;*.err;authpriv.none /dev/${cfg.tty}" else ""}
+    ${cfg.defaultConfig}
+    ${cfg.extraConfig}
+  '';
+
+  defaultConf = ''
+    # Send emergency messages to all users.
+    *.emerg                       *
+
+    # "local1" is used for dhcpd messages.
+    local1.*                     -/var/log/dhcpd
+
+    mail.*                       -/var/log/mail
+
+    *.=warning;*.=err            -/var/log/warn
+    *.crit                        /var/log/warn
+
+    *.*;mail.none;local1.none    -/var/log/messages
+  '';
+
+in
+
+{
+  ###### interface
+
+  options = {
+
+    services.syslogd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable syslogd.  Note that systemd also logs
+          syslog messages, so you normally don't need to run syslogd.
+        '';
+      };
+
+      tty = mkOption {
+        type = types.str;
+        default = "tty10";
+        description = ''
+          The tty device on which syslogd will print important log
+          messages. Leave this option blank to disable tty logging.
+        '';
+      };
+
+      defaultConfig = mkOption {
+        type = types.lines;
+        default = defaultConf;
+        description = ''
+          The default <filename>syslog.conf</filename> file configures a
+          fairly standard setup of log files, which can be extended by
+          means of <varname>extraConfig</varname>.
+        '';
+      };
+
+      enableNetworkInput = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Accept logging through UDP. Option -r of syslogd(8).
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = "news.* -/var/log/news";
+        description = ''
+          Additional text appended to <filename>syslog.conf</filename>,
+          i.e. the contents of <varname>defaultConfig</varname>.
+        '';
+      };
+
+      extraParams = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "-m 0" ];
+        description = ''
+          Additional parameters passed to <command>syslogd</command>.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions =
+      [ { assertion = !config.services.rsyslogd.enable;
+          message = "rsyslogd conflicts with syslogd";
+        }
+      ];
+
+    environment.systemPackages = [ pkgs.sysklogd ];
+
+    services.syslogd.extraParams = optional cfg.enableNetworkInput "-r";
+
+    # FIXME: restarting syslog seems to break journal logging.
+    systemd.services.syslog =
+      { description = "Syslog Daemon";
+
+        requires = [ "syslog.socket" ];
+
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig =
+          { ExecStart = "${pkgs.sysklogd}/sbin/syslogd ${toString cfg.extraParams} -f ${syslogConf} -n";
+            # Prevent syslogd output looping back through journald.
+            StandardOutput = "null";
+          };
+      };
+
+  };
+
+}
diff --git a/nixos/modules/services/logging/vector.nix b/nixos/modules/services/logging/vector.nix
new file mode 100644
index 00000000000..be36b2a41bb
--- /dev/null
+++ b/nixos/modules/services/logging/vector.nix
@@ -0,0 +1,64 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let cfg = config.services.vector;
+
+in
+{
+  options.services.vector = {
+    enable = mkEnableOption "Vector";
+
+    journaldAccess = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable Vector to access journald.
+      '';
+    };
+
+    settings = mkOption {
+      type = (pkgs.formats.json { }).type;
+      default = { };
+      description = ''
+        Specify the configuration for Vector in Nix.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users.groups.vector = { };
+    users.users.vector = {
+      description = "Vector service user";
+      group = "vector";
+      isSystemUser = true;
+    };
+    systemd.services.vector = {
+      description = "Vector event and log aggregator";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      requires = [ "network-online.target" ];
+      serviceConfig =
+        let
+          format = pkgs.formats.toml { };
+          conf = format.generate "vector.toml" cfg.settings;
+          validateConfig = file:
+            pkgs.runCommand "validate-vector-conf" { } ''
+              ${pkgs.vector}/bin/vector validate --no-environment "${file}"
+              ln -s "${file}" "$out"
+            '';
+        in
+        {
+          ExecStart = "${pkgs.vector}/bin/vector --config ${validateConfig conf}";
+          User = "vector";
+          Group = "vector";
+          Restart = "no";
+          StateDirectory = "vector";
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+          # This group is required for accessing journald.
+          SupplementaryGroups = mkIf cfg.journaldAccess "systemd-journal";
+        };
+    };
+  };
+}
diff --git a/nixos/modules/services/mail/clamsmtp.nix b/nixos/modules/services/mail/clamsmtp.nix
new file mode 100644
index 00000000000..fc1267c5d28
--- /dev/null
+++ b/nixos/modules/services/mail/clamsmtp.nix
@@ -0,0 +1,181 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.clamsmtp;
+  clamdSocket = "/run/clamav/clamd.ctl"; # See services/security/clamav.nix
+in
+{
+  ##### interface
+  options = {
+    services.clamsmtp = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable clamsmtp.";
+      };
+
+      instances = mkOption {
+        description = "Instances of clamsmtp to run.";
+        type = types.listOf (types.submodule { options = {
+          action = mkOption {
+            type = types.enum [ "bounce" "drop" "pass" ];
+            default = "drop";
+            description =
+              ''
+                Action to take when a virus is detected.
+
+                Note that viruses often spoof sender addresses, so bouncing is
+                in most cases not a good idea.
+              '';
+          };
+
+          header = mkOption {
+            type = types.str;
+            default = "";
+            example = "X-Virus-Scanned: ClamAV using ClamSMTP";
+            description =
+              ''
+                A header to add to scanned messages. See clamsmtpd.conf(5) for
+                more details. Empty means no header.
+              '';
+          };
+
+          keepAlives = mkOption {
+            type = types.int;
+            default = 0;
+            description =
+              ''
+                Number of seconds to wait between each NOOP sent to the sending
+                server. 0 to disable.
+
+                This is meant for slow servers where the sending MTA times out
+                waiting for clamd to scan the file.
+              '';
+          };
+
+          listen = mkOption {
+            type = types.str;
+            example = "127.0.0.1:10025";
+            description =
+              ''
+                Address to wait for incoming SMTP connections on. See
+                clamsmtpd.conf(5) for more details.
+              '';
+          };
+
+          quarantine = mkOption {
+            type = types.bool;
+            default = false;
+            description =
+              ''
+                Whether to quarantine files that contain viruses by leaving them
+                in the temporary directory.
+              '';
+          };
+
+          maxConnections = mkOption {
+            type = types.int;
+            default = 64;
+            description = "Maximum number of connections to accept at once.";
+          };
+
+          outAddress = mkOption {
+            type = types.str;
+            description =
+              ''
+                Address of the SMTP server to send email to once it has been
+                scanned.
+              '';
+          };
+
+          tempDirectory = mkOption {
+            type = types.str;
+            default = "/tmp";
+            description =
+              ''
+                Temporary directory that needs to be accessible to both clamd
+                and clamsmtpd.
+              '';
+          };
+
+          timeout = mkOption {
+            type = types.int;
+            default = 180;
+            description = "Time-out for network connections.";
+          };
+
+          transparentProxy = mkOption {
+            type = types.bool;
+            default = false;
+            description = "Enable clamsmtp's transparent proxy support.";
+          };
+
+          virusAction = mkOption {
+            type = with types; nullOr path;
+            default = null;
+            description =
+              ''
+                Command to run when a virus is found. Please see VIRUS ACTION in
+                clamsmtpd(8) for a discussion of this option and its safe use.
+              '';
+          };
+
+          xClient = mkOption {
+            type = types.bool;
+            default = false;
+            description =
+              ''
+                Send the XCLIENT command to the receiving server, for forwarding
+                client addresses and connection information if the receiving
+                server supports this feature.
+              '';
+          };
+        };});
+      };
+    };
+  };
+
+  ##### implementation
+  config = let
+    configfile = conf: pkgs.writeText "clamsmtpd.conf"
+      ''
+        Action: ${conf.action}
+        ClamAddress: ${clamdSocket}
+        Header: ${conf.header}
+        KeepAlives: ${toString conf.keepAlives}
+        Listen: ${conf.listen}
+        Quarantine: ${if conf.quarantine then "on" else "off"}
+        MaxConnections: ${toString conf.maxConnections}
+        OutAddress: ${conf.outAddress}
+        TempDirectory: ${conf.tempDirectory}
+        TimeOut: ${toString conf.timeout}
+        TransparentProxy: ${if conf.transparentProxy then "on" else "off"}
+        User: clamav
+        ${optionalString (conf.virusAction != null) "VirusAction: ${conf.virusAction}"}
+        XClient: ${if conf.xClient then "on" else "off"}
+      '';
+  in
+    mkIf cfg.enable {
+      assertions = [
+        { assertion = config.services.clamav.daemon.enable;
+          message = "clamsmtp requires clamav to be enabled";
+        }
+      ];
+
+      systemd.services = listToAttrs (imap1 (i: conf:
+        nameValuePair "clamsmtp-${toString i}" {
+          description = "ClamSMTP instance ${toString i}";
+          wantedBy = [ "multi-user.target" ];
+          script = "exec ${pkgs.clamsmtp}/bin/clamsmtpd -f ${configfile conf}";
+          after = [ "clamav-daemon.service" ];
+          requires = [ "clamav-daemon.service" ];
+          serviceConfig.Type = "forking";
+          serviceConfig.PrivateTmp = "yes";
+          unitConfig.JoinsNamespaceOf = "clamav-daemon.service";
+        }
+      ) cfg.instances);
+    };
+
+  meta.maintainers = with lib.maintainers; [ ekleog ];
+}
diff --git a/nixos/modules/services/mail/davmail.nix b/nixos/modules/services/mail/davmail.nix
new file mode 100644
index 00000000000..e9f31e6fb39
--- /dev/null
+++ b/nixos/modules/services/mail/davmail.nix
@@ -0,0 +1,99 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.davmail;
+
+  configType = with types;
+    oneOf [ (attrsOf configType) str int bool ] // {
+      description = "davmail config type (str, int, bool or attribute set thereof)";
+    };
+
+  toStr = val: if isBool val then boolToString val else toString val;
+
+  linesForAttrs = attrs: concatMap (name: let value = attrs.${name}; in
+    if isAttrs value
+      then map (line: name + "." + line) (linesForAttrs value)
+      else [ "${name}=${toStr value}" ]
+  ) (attrNames attrs);
+
+  configFile = pkgs.writeText "davmail.properties" (concatStringsSep "\n" (linesForAttrs cfg.config));
+
+in
+
+  {
+    options.services.davmail = {
+      enable = mkEnableOption "davmail, an MS Exchange gateway";
+
+      url = mkOption {
+        type = types.str;
+        description = "Outlook Web Access URL to access the exchange server, i.e. the base webmail URL.";
+        example = "https://outlook.office365.com/EWS/Exchange.asmx";
+      };
+
+      config = mkOption {
+        type = configType;
+        default = {};
+        description = ''
+          Davmail configuration. Refer to
+          <link xlink:href="http://davmail.sourceforge.net/serversetup.html"/>
+          and <link xlink:href="http://davmail.sourceforge.net/advanced.html"/>
+          for details on supported values.
+        '';
+        example = literalExpression ''
+          {
+            davmail.allowRemote = true;
+            davmail.imapPort = 55555;
+            davmail.bindAddress = "10.0.1.2";
+            davmail.smtpSaveInSent = true;
+            davmail.folderSizeLimit = 10;
+            davmail.caldavAutoSchedule = false;
+            log4j.logger.rootLogger = "DEBUG";
+          }
+        '';
+      };
+    };
+
+    config = mkIf cfg.enable {
+
+      services.davmail.config = {
+        davmail = mapAttrs (name: mkDefault) {
+          server = true;
+          disableUpdateCheck = true;
+          logFilePath = "/var/log/davmail/davmail.log";
+          logFileSize = "1MB";
+          mode = "auto";
+          url = cfg.url;
+          caldavPort = 1080;
+          imapPort = 1143;
+          ldapPort = 1389;
+          popPort = 1110;
+          smtpPort = 1025;
+        };
+        log4j = {
+          logger.davmail = mkDefault "WARN";
+          logger.httpclient.wire = mkDefault "WARN";
+          logger.org.apache.commons.httpclient = mkDefault "WARN";
+          rootLogger = mkDefault "WARN";
+        };
+      };
+
+      systemd.services.davmail = {
+        description = "DavMail POP/IMAP/SMTP Exchange Gateway";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = {
+          Type = "simple";
+          ExecStart = "${pkgs.davmail}/bin/davmail ${configFile}";
+          Restart = "on-failure";
+          DynamicUser = "yes";
+          LogsDirectory = "davmail";
+        };
+      };
+
+      environment.systemPackages = [ pkgs.davmail ];
+    };
+  }
diff --git a/nixos/modules/services/mail/dkimproxy-out.nix b/nixos/modules/services/mail/dkimproxy-out.nix
new file mode 100644
index 00000000000..f4ac9e47007
--- /dev/null
+++ b/nixos/modules/services/mail/dkimproxy-out.nix
@@ -0,0 +1,120 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.dkimproxy-out;
+  keydir = "/var/lib/dkimproxy-out";
+  privkey = "${keydir}/private.key";
+  pubkey = "${keydir}/public.key";
+in
+{
+  ##### interface
+  options = {
+    services.dkimproxy-out = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          ''
+            Whether to enable dkimproxy_out.
+
+            Note that a key will be auto-generated, and can be found in
+            ${keydir}.
+          '';
+      };
+
+      listen = mkOption {
+        type = types.str;
+        example = "127.0.0.1:10027";
+        description = "Address:port DKIMproxy should listen on.";
+      };
+
+      relay = mkOption {
+        type = types.str;
+        example = "127.0.0.1:10028";
+        description = "Address:port DKIMproxy should forward mail to.";
+      };
+
+      domains = mkOption {
+        type = with types; listOf str;
+        example = [ "example.org" "example.com" ];
+        description = "List of domains DKIMproxy can sign for.";
+      };
+
+      selector = mkOption {
+        type = types.str;
+        example = "selector1";
+        description =
+          ''
+            The selector to use for DKIM key identification.
+
+            For example, if 'selector1' is used here, then for each domain
+            'example.org' given in `domain`, 'selector1._domainkey.example.org'
+            should contain the TXT record indicating the public key is the one
+            in ${pubkey}: "v=DKIM1; t=s; p=[THE PUBLIC KEY]".
+          '';
+      };
+
+      keySize = mkOption {
+        type = types.int;
+        default = 2048;
+        description =
+          ''
+            Size of the RSA key to use to sign outgoing emails. Note that the
+            maximum mandatorily verified as per RFC6376 is 2048.
+          '';
+      };
+
+      # TODO: allow signature for other schemes than dkim(c=relaxed/relaxed)?
+      # This being the scheme used by gmail, maybe nothing more is needed for
+      # reasonable use.
+    };
+  };
+
+  ##### implementation
+  config = let
+    configfile = pkgs.writeText "dkimproxy_out.conf"
+      ''
+        listen ${cfg.listen}
+        relay ${cfg.relay}
+
+        domain ${concatStringsSep "," cfg.domains}
+        selector ${cfg.selector}
+
+        signature dkim(c=relaxed/relaxed)
+
+        keyfile ${privkey}
+      '';
+  in
+    mkIf cfg.enable {
+      users.groups.dkimproxy-out = {};
+      users.users.dkimproxy-out = {
+        description = "DKIMproxy_out daemon";
+        group = "dkimproxy-out";
+        isSystemUser = true;
+      };
+
+      systemd.services.dkimproxy-out = {
+        description = "DKIMproxy_out";
+        wantedBy = [ "multi-user.target" ];
+        preStart = ''
+          if [ ! -d "${keydir}" ]; then
+            mkdir -p "${keydir}"
+            chmod 0700 "${keydir}"
+            ${pkgs.openssl}/bin/openssl genrsa -out "${privkey}" ${toString cfg.keySize}
+            ${pkgs.openssl}/bin/openssl rsa -in "${privkey}" -pubout -out "${pubkey}"
+            chown -R dkimproxy-out:dkimproxy-out "${keydir}"
+          fi
+        '';
+        script = ''
+          exec ${pkgs.dkimproxy}/bin/dkimproxy.out --conf_file=${configfile}
+        '';
+        serviceConfig = {
+          User = "dkimproxy-out";
+          PermissionsStartOnly = true;
+        };
+      };
+    };
+
+  meta.maintainers = with lib.maintainers; [ ekleog ];
+}
diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix
new file mode 100644
index 00000000000..a8c1f176782
--- /dev/null
+++ b/nixos/modules/services/mail/dovecot.nix
@@ -0,0 +1,462 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dovecot2;
+  dovecotPkg = pkgs.dovecot;
+
+  baseDir = "/run/dovecot2";
+  stateDir = "/var/lib/dovecot";
+
+  dovecotConf = concatStrings [
+    ''
+      base_dir = ${baseDir}
+      protocols = ${concatStringsSep " " cfg.protocols}
+      sendmail_path = /run/wrappers/bin/sendmail
+      # defining mail_plugins must be done before the first protocol {} filter because of https://doc.dovecot.org/configuration_manual/config_file/config_file_syntax/#variable-expansion
+      mail_plugins = $mail_plugins ${concatStringsSep " " cfg.mailPlugins.globally.enable}
+    ''
+
+    (
+      concatStringsSep "\n" (
+        mapAttrsToList (
+          protocol: plugins: ''
+            protocol ${protocol} {
+              mail_plugins = $mail_plugins ${concatStringsSep " " plugins.enable}
+            }
+          ''
+        ) cfg.mailPlugins.perProtocol
+      )
+    )
+
+    (
+      if cfg.sslServerCert == null then ''
+        ssl = no
+        disable_plaintext_auth = no
+      '' else ''
+        ssl_cert = <${cfg.sslServerCert}
+        ssl_key = <${cfg.sslServerKey}
+        ${optionalString (cfg.sslCACert != null) ("ssl_ca = <" + cfg.sslCACert)}
+        ${optionalString cfg.enableDHE ''ssl_dh = <${config.security.dhparams.params.dovecot2.path}''}
+        disable_plaintext_auth = yes
+      ''
+    )
+
+    ''
+      default_internal_user = ${cfg.user}
+      default_internal_group = ${cfg.group}
+      ${optionalString (cfg.mailUser != null) "mail_uid = ${cfg.mailUser}"}
+      ${optionalString (cfg.mailGroup != null) "mail_gid = ${cfg.mailGroup}"}
+
+      mail_location = ${cfg.mailLocation}
+
+      maildir_copy_with_hardlinks = yes
+      pop3_uidl_format = %08Xv%08Xu
+
+      auth_mechanisms = plain login
+
+      service auth {
+        user = root
+      }
+    ''
+
+    (
+      optionalString cfg.enablePAM ''
+        userdb {
+          driver = passwd
+        }
+
+        passdb {
+          driver = pam
+          args = ${optionalString cfg.showPAMFailure "failure_show_msg=yes"} dovecot2
+        }
+      ''
+    )
+
+    (
+      optionalString (cfg.sieveScripts != {}) ''
+        plugin {
+          ${concatStringsSep "\n" (mapAttrsToList (to: from: "sieve_${to} = ${stateDir}/sieve/${to}") cfg.sieveScripts)}
+        }
+      ''
+    )
+
+    (
+      optionalString (cfg.mailboxes != {}) ''
+        namespace inbox {
+          inbox=yes
+          ${concatStringsSep "\n" (map mailboxConfig (attrValues cfg.mailboxes))}
+        }
+      ''
+    )
+
+    (
+      optionalString cfg.enableQuota ''
+        service quota-status {
+          executable = ${dovecotPkg}/libexec/dovecot/quota-status -p postfix
+          inet_listener {
+            port = ${cfg.quotaPort}
+          }
+          client_limit = 1
+        }
+
+        plugin {
+          quota_rule = *:storage=${cfg.quotaGlobalPerUser}
+          quota = count:User quota # per virtual mail user quota
+          quota_status_success = DUNNO
+          quota_status_nouser = DUNNO
+          quota_status_overquota = "552 5.2.2 Mailbox is full"
+          quota_grace = 10%%
+          quota_vsizes = yes
+        }
+      ''
+    )
+
+    cfg.extraConfig
+  ];
+
+  modulesDir = pkgs.symlinkJoin {
+    name = "dovecot-modules";
+    paths = map (pkg: "${pkg}/lib/dovecot") ([ dovecotPkg ] ++ map (module: module.override { dovecot = dovecotPkg; }) cfg.modules);
+  };
+
+  mailboxConfig = mailbox: ''
+    mailbox "${mailbox.name}" {
+      auto = ${toString mailbox.auto}
+  '' + optionalString (mailbox.autoexpunge != null) ''
+    autoexpunge = ${mailbox.autoexpunge}
+  '' + optionalString (mailbox.specialUse != null) ''
+    special_use = \${toString mailbox.specialUse}
+  '' + "}";
+
+  mailboxes = { name, ... }: {
+    options = {
+      name = mkOption {
+        type = types.strMatching ''[^"]+'';
+        example = "Spam";
+        default = name;
+        readOnly = true;
+        description = "The name of the mailbox.";
+      };
+      auto = mkOption {
+        type = types.enum [ "no" "create" "subscribe" ];
+        default = "no";
+        example = "subscribe";
+        description = "Whether to automatically create or create and subscribe to the mailbox or not.";
+      };
+      specialUse = mkOption {
+        type = types.nullOr (types.enum [ "All" "Archive" "Drafts" "Flagged" "Junk" "Sent" "Trash" ]);
+        default = null;
+        example = "Junk";
+        description = "Null if no special use flag is set. Other than that every use flag mentioned in the RFC is valid.";
+      };
+      autoexpunge = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "60d";
+        description = ''
+          To automatically remove all email from the mailbox which is older than the
+          specified time.
+        '';
+      };
+    };
+  };
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "dovecot2" "package" ] "")
+  ];
+
+  options.services.dovecot2 = {
+    enable = mkEnableOption "the dovecot 2.x POP3/IMAP server";
+
+    enablePop3 = mkEnableOption "starting the POP3 listener (when Dovecot is enabled).";
+
+    enableImap = mkEnableOption "starting the IMAP listener (when Dovecot is enabled)." // { default = true; };
+
+    enableLmtp = mkEnableOption "starting the LMTP listener (when Dovecot is enabled).";
+
+    protocols = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = "Additional listeners to start when Dovecot is enabled.";
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = "dovecot2";
+      description = "Dovecot user name.";
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "dovecot2";
+      description = "Dovecot group name.";
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = "mail_debug = yes";
+      description = "Additional entries to put verbatim into Dovecot's config file.";
+    };
+
+    mailPlugins =
+      let
+        plugins = hint: types.submodule {
+          options = {
+            enable = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = "mail plugins to enable as a list of strings to append to the ${hint} <literal>$mail_plugins</literal> configuration variable";
+            };
+          };
+        };
+      in
+        mkOption {
+          type = with types; submodule {
+            options = {
+              globally = mkOption {
+                description = "Additional entries to add to the mail_plugins variable for all protocols";
+                type = plugins "top-level";
+                example = { enable = [ "virtual" ]; };
+                default = { enable = []; };
+              };
+              perProtocol = mkOption {
+                description = "Additional entries to add to the mail_plugins variable, per protocol";
+                type = attrsOf (plugins "corresponding per-protocol");
+                default = {};
+                example = { imap = [ "imap_acl" ]; };
+              };
+            };
+          };
+          description = "Additional entries to add to the mail_plugins variable, globally and per protocol";
+          example = {
+            globally.enable = [ "acl" ];
+            perProtocol.imap.enable = [ "imap_acl" ];
+          };
+          default = { globally.enable = []; perProtocol = {}; };
+        };
+
+    configFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = "Config file used for the whole dovecot configuration.";
+      apply = v: if v != null then v else pkgs.writeText "dovecot.conf" dovecotConf;
+    };
+
+    mailLocation = mkOption {
+      type = types.str;
+      default = "maildir:/var/spool/mail/%u"; /* Same as inbox, as postfix */
+      example = "maildir:~/mail:INBOX=/var/spool/mail/%u";
+      description = ''
+        Location that dovecot will use for mail folders. Dovecot mail_location option.
+      '';
+    };
+
+    mailUser = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Default user to store mail for virtual users.";
+    };
+
+    mailGroup = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Default group to store mail for virtual users.";
+    };
+
+    createMailUser = mkEnableOption ''automatically creating the user
+      given in <option>services.dovecot.user</option> and the group
+      given in <option>services.dovecot.group</option>.'' // { default = true; };
+
+    modules = mkOption {
+      type = types.listOf types.package;
+      default = [];
+      example = literalExpression "[ pkgs.dovecot_pigeonhole ]";
+      description = ''
+        Symlinks the contents of lib/dovecot of every given package into
+        /etc/dovecot/modules. This will make the given modules available
+        if a dovecot package with the module_dir patch applied is being used.
+      '';
+    };
+
+    sslCACert = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Path to the server's CA certificate key.";
+    };
+
+    sslServerCert = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Path to the server's public key.";
+    };
+
+    sslServerKey = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Path to the server's private key.";
+    };
+
+    enablePAM = mkEnableOption "creating a own Dovecot PAM service and configure PAM user logins." // { default = true; };
+
+    enableDHE = mkEnableOption "enable ssl_dh and generation of primes for the key exchange." // { default = true; };
+
+    sieveScripts = mkOption {
+      type = types.attrsOf types.path;
+      default = {};
+      description = "Sieve scripts to be executed. Key is a sequence, e.g. 'before2', 'after' etc.";
+    };
+
+    showPAMFailure = mkEnableOption "showing the PAM failure message on authentication error (useful for OTPW).";
+
+    mailboxes = mkOption {
+      type = with types; coercedTo
+        (listOf unspecified)
+        (list: listToAttrs (map (entry: { name = entry.name; value = removeAttrs entry ["name"]; }) list))
+        (attrsOf (submodule mailboxes));
+      default = {};
+      example = literalExpression ''
+        {
+          Spam = { specialUse = "Junk"; auto = "create"; };
+        }
+      '';
+      description = "Configure mailboxes and auto create or subscribe them.";
+    };
+
+    enableQuota = mkEnableOption "the dovecot quota service.";
+
+    quotaPort = mkOption {
+      type = types.str;
+      default = "12340";
+      description = ''
+        The Port the dovecot quota service binds to.
+        If using postfix, add check_policy_service inet:localhost:12340 to your smtpd_recipient_restrictions in your postfix config.
+      '';
+    };
+    quotaGlobalPerUser = mkOption {
+      type = types.str;
+      default = "100G";
+      example = "10G";
+      description = "Quota limit for the user in bytes. Supports suffixes b, k, M, G, T and %.";
+    };
+
+  };
+
+
+  config = mkIf cfg.enable {
+    security.pam.services.dovecot2 = mkIf cfg.enablePAM {};
+
+    security.dhparams = mkIf (cfg.sslServerCert != null && cfg.enableDHE) {
+      enable = true;
+      params.dovecot2 = {};
+    };
+    services.dovecot2.protocols =
+      optional cfg.enableImap "imap"
+      ++ optional cfg.enablePop3 "pop3"
+      ++ optional cfg.enableLmtp "lmtp";
+
+    services.dovecot2.mailPlugins = mkIf cfg.enableQuota {
+      globally.enable = [ "quota" ];
+      perProtocol.imap.enable = [ "imap_quota" ];
+    };
+
+    users.users = {
+      dovenull =
+        {
+          uid = config.ids.uids.dovenull2;
+          description = "Dovecot user for untrusted logins";
+          group = "dovenull";
+        };
+    } // optionalAttrs (cfg.user == "dovecot2") {
+      dovecot2 =
+        {
+          uid = config.ids.uids.dovecot2;
+          description = "Dovecot user";
+          group = cfg.group;
+        };
+    } // optionalAttrs (cfg.createMailUser && cfg.mailUser != null) {
+      ${cfg.mailUser} =
+        { description = "Virtual Mail User"; isSystemUser = true; } // optionalAttrs (cfg.mailGroup != null)
+          { group = cfg.mailGroup; };
+    };
+
+    users.groups = {
+      dovenull.gid = config.ids.gids.dovenull2;
+    } // optionalAttrs (cfg.group == "dovecot2") {
+      dovecot2.gid = config.ids.gids.dovecot2;
+    } // optionalAttrs (cfg.createMailUser && cfg.mailGroup != null) {
+      ${cfg.mailGroup} = {};
+    };
+
+    environment.etc."dovecot/modules".source = modulesDir;
+    environment.etc."dovecot/dovecot.conf".source = cfg.configFile;
+
+    systemd.services.dovecot2 = {
+      description = "Dovecot IMAP/POP3 server";
+
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ cfg.configFile modulesDir ];
+
+      startLimitIntervalSec = 60;  # 1 min
+      serviceConfig = {
+        Type = "notify";
+        ExecStart = "${dovecotPkg}/sbin/dovecot -F";
+        ExecReload = "${dovecotPkg}/sbin/doveadm reload";
+        Restart = "on-failure";
+        RestartSec = "1s";
+        RuntimeDirectory = [ "dovecot2" ];
+      };
+
+      # When copying sieve scripts preserve the original time stamp
+      # (should be 0) so that the compiled sieve script is newer than
+      # the source file and Dovecot won't try to compile it.
+      preStart = ''
+        rm -rf ${stateDir}/sieve
+      '' + optionalString (cfg.sieveScripts != {}) ''
+        mkdir -p ${stateDir}/sieve
+        ${concatStringsSep "\n" (
+        mapAttrsToList (
+          to: from: ''
+            if [ -d '${from}' ]; then
+              mkdir '${stateDir}/sieve/${to}'
+              cp -p "${from}/"*.sieve '${stateDir}/sieve/${to}'
+            else
+              cp -p '${from}' '${stateDir}/sieve/${to}'
+            fi
+            ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}'
+          ''
+        ) cfg.sieveScripts
+      )}
+        chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/sieve'
+      '';
+    };
+
+    environment.systemPackages = [ dovecotPkg ];
+
+    warnings = mkIf (any isList options.services.dovecot2.mailboxes.definitions) [
+      "Declaring `services.dovecot2.mailboxes' as a list is deprecated and will break eval in 21.05! See the release notes for more info for migration."
+    ];
+
+    assertions = [
+      {
+        assertion = (cfg.sslServerCert == null) == (cfg.sslServerKey == null)
+        && (cfg.sslCACert != null -> !(cfg.sslServerCert == null || cfg.sslServerKey == null));
+        message = "dovecot needs both sslServerCert and sslServerKey defined for working crypto";
+      }
+      {
+        assertion = cfg.showPAMFailure -> cfg.enablePAM;
+        message = "dovecot is configured with showPAMFailure while enablePAM is disabled";
+      }
+      {
+        assertion = cfg.sieveScripts != {} -> (cfg.mailUser != null && cfg.mailGroup != null);
+        message = "dovecot requires mailUser and mailGroup to be set when sieveScripts is set";
+      }
+    ];
+
+  };
+
+}
diff --git a/nixos/modules/services/mail/dspam.nix b/nixos/modules/services/mail/dspam.nix
new file mode 100644
index 00000000000..766ebc8095a
--- /dev/null
+++ b/nixos/modules/services/mail/dspam.nix
@@ -0,0 +1,150 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.dspam;
+
+  dspam = pkgs.dspam;
+
+  defaultSock = "/run/dspam/dspam.sock";
+
+  cfgfile = pkgs.writeText "dspam.conf" ''
+    Home /var/lib/dspam
+    StorageDriver ${dspam}/lib/dspam/lib${cfg.storageDriver}_drv.so
+
+    Trust root
+    Trust ${cfg.user}
+    SystemLog on
+    UserLog on
+
+    ${optionalString (cfg.domainSocket != null) ''
+      ServerDomainSocketPath "${cfg.domainSocket}"
+      ClientHost "${cfg.domainSocket}"
+    ''}
+
+    ${cfg.extraConfig}
+  '';
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.dspam = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the dspam spam filter.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "dspam";
+        description = "User for the dspam daemon.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "dspam";
+        description = "Group for the dspam daemon.";
+      };
+
+      storageDriver = mkOption {
+        type = types.str;
+        default = "hash";
+        description =  "Storage driver backend to use for dspam.";
+      };
+
+      domainSocket = mkOption {
+        type = types.nullOr types.path;
+        default = defaultSock;
+        description = "Path to local domain socket which is used for communication with the daemon. Set to null to disable UNIX socket.";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Additional dspam configuration.";
+      };
+
+      maintenanceInterval = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "If set, maintenance script will be run at specified (in systemd.timer format) interval";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable (mkMerge [
+    {
+      users.users = optionalAttrs (cfg.user == "dspam") {
+        dspam = {
+          group = cfg.group;
+          uid = config.ids.uids.dspam;
+        };
+      };
+
+      users.groups = optionalAttrs (cfg.group == "dspam") {
+        dspam.gid = config.ids.gids.dspam;
+      };
+
+      environment.systemPackages = [ dspam ];
+
+      environment.etc."dspam/dspam.conf".source = cfgfile;
+
+      systemd.services.dspam = {
+        description = "dspam spam filtering daemon";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "postgresql.service" ];
+        restartTriggers = [ cfgfile ];
+
+        serviceConfig = {
+          ExecStart = "${dspam}/bin/dspam --daemon --nofork";
+          User = cfg.user;
+          Group = cfg.group;
+          RuntimeDirectory = optional (cfg.domainSocket == defaultSock) "dspam";
+          RuntimeDirectoryMode = optional (cfg.domainSocket == defaultSock) "0750";
+          StateDirectory = "dspam";
+          StateDirectoryMode = "0750";
+          LogsDirectory = "dspam";
+          LogsDirectoryMode = "0750";
+          # DSPAM segfaults on just about every error
+          Restart = "on-abort";
+          RestartSec = "1s";
+        };
+      };
+    }
+
+    (mkIf (cfg.maintenanceInterval != null) {
+      systemd.timers.dspam-maintenance = {
+        description = "Timer for dspam maintenance script";
+        wantedBy = [ "timers.target" ];
+        timerConfig = {
+          OnCalendar = cfg.maintenanceInterval;
+          Unit = "dspam-maintenance.service";
+        };
+      };
+
+      systemd.services.dspam-maintenance = {
+        description = "dspam maintenance script";
+        restartTriggers = [ cfgfile ];
+
+        serviceConfig = {
+          ExecStart = "${dspam}/bin/dspam_maintenance --verbose";
+          Type = "oneshot";
+          User = cfg.user;
+          Group = cfg.group;
+        };
+      };
+    })
+  ]);
+}
diff --git a/nixos/modules/services/mail/exim.nix b/nixos/modules/services/mail/exim.nix
new file mode 100644
index 00000000000..7356db2b6a6
--- /dev/null
+++ b/nixos/modules/services/mail/exim.nix
@@ -0,0 +1,132 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) literalExpression mkIf mkOption singleton types;
+  inherit (pkgs) coreutils;
+  cfg = config.services.exim;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.exim = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the Exim mail transfer agent.";
+      };
+
+      config = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Verbatim Exim configuration.  This should not contain exim_user,
+          exim_group, exim_path, or spool_directory.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "exim";
+        description = ''
+          User to use when no root privileges are required.
+          In particular, this applies when receiving messages and when doing
+          remote deliveries.  (Local deliveries run as various non-root users,
+          typically as the owner of a local mailbox.) Specifying this value
+          as root is not supported.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "exim";
+        description = ''
+          Group to use when no root privileges are required.
+        '';
+      };
+
+      spoolDir = mkOption {
+        type = types.path;
+        default = "/var/spool/exim";
+        description = ''
+          Location of the spool directory of exim.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.exim;
+        defaultText = literalExpression "pkgs.exim";
+        description = ''
+          The Exim derivation to use.
+          This can be used to enable features such as LDAP or PAM support.
+        '';
+      };
+
+      queueRunnerInterval = mkOption {
+        type = types.str;
+        default = "5m";
+        description = ''
+          How often to spawn a new queue runner.
+        '';
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment = {
+      etc."exim.conf".text = ''
+        exim_user = ${cfg.user}
+        exim_group = ${cfg.group}
+        exim_path = /run/wrappers/bin/exim
+        spool_directory = ${cfg.spoolDir}
+        ${cfg.config}
+      '';
+      systemPackages = [ cfg.package ];
+    };
+
+    users.users.${cfg.user} = {
+      description = "Exim mail transfer agent user";
+      uid = config.ids.uids.exim;
+      group = cfg.group;
+    };
+
+    users.groups.${cfg.group} = {
+      gid = config.ids.gids.exim;
+    };
+
+    security.wrappers.exim =
+      { setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${cfg.package}/bin/exim";
+      };
+
+    systemd.services.exim = {
+      description = "Exim Mail Daemon";
+      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";
+      };
+      preStart = ''
+        if ! test -d ${cfg.spoolDir}; then
+          ${coreutils}/bin/mkdir -p ${cfg.spoolDir}
+          ${coreutils}/bin/chown ${cfg.user}:${cfg.group} ${cfg.spoolDir}
+        fi
+      '';
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/mail/maddy.nix b/nixos/modules/services/mail/maddy.nix
new file mode 100644
index 00000000000..0b06905ac6f
--- /dev/null
+++ b/nixos/modules/services/mail/maddy.nix
@@ -0,0 +1,273 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  name = "maddy";
+
+  cfg = config.services.maddy;
+
+  defaultConfig = ''
+    # Minimal configuration with TLS disabled, adapted from upstream example
+    # 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
+        dsn credentials.db
+        table_name passwords
+      }
+    }
+
+    storage.imapsql local_mailboxes {
+      driver sqlite3
+      dsn imapsql.db
+    }
+
+    table.chain local_rewrites {
+      optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
+      optional_step static {
+        entry postmaster postmaster@$(primary_domain)
+      }
+      optional_step file /etc/maddy/aliases
+    }
+    msgpipeline local_routing {
+      destination postmaster $(local_domains) {
+        modify {
+          replace_rcpt &local_rewrites
+        }
+        deliver_to &local_mailboxes
+      }
+      default_destination {
+        reject 550 5.1.1 "User doesn't exist"
+      }
+    }
+
+    smtp tcp://0.0.0.0:25 {
+      limits {
+        all rate 20 1s
+        all concurrency 10
+      }
+      dmarc yes
+      check {
+        require_mx_record
+        dkim
+        spf
+      }
+      source $(local_domains) {
+        reject 501 5.1.8 "Use Submission for outgoing SMTP"
+      }
+      default_source {
+        destination postmaster $(local_domains) {
+          deliver_to &local_routing
+        }
+        default_destination {
+          reject 550 5.1.1 "User doesn't exist"
+        }
+      }
+    }
+
+    submission tcp://0.0.0.0:587 {
+      limits {
+        all rate 50 1s
+      }
+      auth &local_authdb
+      source $(local_domains) {
+        check {
+            authorize_sender {
+                prepare_email &local_rewrites
+                user_to_email identity
+            }
+        }
+        destination postmaster $(local_domains) {
+            deliver_to &local_routing
+        }
+        default_destination {
+            modify {
+                dkim $(primary_domain) $(local_domains) default
+            }
+            deliver_to &remote_queue
+        }
+      }
+      default_source {
+        reject 501 5.1.8 "Non-local sender domain"
+      }
+    }
+
+    target.remote outbound_delivery {
+      limits {
+        destination rate 20 1s
+        destination concurrency 10
+      }
+      mx_auth {
+        dane
+        mtasts {
+          cache fs
+          fs_dir mtasts_cache/
+        }
+        local_policy {
+            min_tls_level encrypted
+            min_mx_level none
+        }
+      }
+    }
+
+    target.queue remote_queue {
+      target &outbound_delivery
+      autogenerated_msg_domain $(primary_domain)
+      bounce {
+        destination postmaster $(local_domains) {
+          deliver_to &local_routing
+        }
+        default_destination {
+            reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
+        }
+      }
+    }
+
+    imap tcp://0.0.0.0:143 {
+      auth &local_authdb
+      storage &local_mailboxes
+    }
+  '';
+
+in {
+  options = {
+    services.maddy = {
+
+      enable = mkEnableOption "Maddy, a free an open source mail server";
+
+      user = mkOption {
+        default = "maddy";
+        type = with types; uniq string;
+        description = ''
+          User account under which maddy runs.
+
+          <note><para>
+          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 before the maddy service starts.
+          </para></note>
+        '';
+      };
+
+      group = mkOption {
+        default = "maddy";
+        type = with types; uniq string;
+        description = ''
+          Group account under which maddy runs.
+
+          <note><para>
+          If left as the default value this group will automatically be created
+          on system activation, otherwise the sysadmin is responsible for
+          ensuring the group exists before the maddy service starts.
+          </para></note>
+        '';
+      };
+
+      hostname = mkOption {
+        default = "localhost";
+        type = with types; uniq string;
+        example = ''example.com'';
+        description = ''
+          Hostname to use. It should be FQDN.
+        '';
+      };
+
+      primaryDomain = mkOption {
+        default = "localhost";
+        type = with types; uniq string;
+        example = ''mail.example.com'';
+        description = ''
+          Primary MX domain to use. It should be FQDN.
+        '';
+      };
+
+      localDomains = mkOption {
+        type = with types; listOf str;
+        default = ["$(primary_domain)"];
+        example = [
+          "$(primary_domain)"
+          "example.com"
+          "other.example.com"
+        ];
+        description = ''
+          Define list of allowed domains.
+        '';
+      };
+
+      config = mkOption {
+        type = with types; nullOr lines;
+        default = defaultConfig;
+        description = ''
+          Server configuration, see
+          <link xlink:href="https://maddy.email">https://maddy.email</link> for
+          more information. The default configuration of this module will setup
+          minimal maddy instance for mail transfer without TLS encryption.
+          <note><para>
+          This should not be used in a production environment.
+          </para></note>
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open the configured incoming and outgoing mail server ports.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd = {
+      packages = [ pkgs.maddy ];
+      services.maddy = {
+        serviceConfig = {
+          User = cfg.user;
+          Group = cfg.group;
+          StateDirectory = [ "maddy" ];
+        };
+        restartTriggers = [ config.environment.etc."maddy/maddy.conf".source ];
+        wantedBy = [ "multi-user.target" ];
+      };
+    };
+
+    environment.etc."maddy/maddy.conf" = {
+      text = ''
+        $(hostname) = ${cfg.hostname}
+        $(primary_domain) = ${cfg.primaryDomain}
+        $(local_domains) = ${toString cfg.localDomains}
+        hostname ${cfg.hostname}
+        ${cfg.config}
+      '';
+    };
+
+    users.users = optionalAttrs (cfg.user == name) {
+      ${name} = {
+        isSystemUser = true;
+        group = cfg.group;
+        description = "Maddy mail transfer agent user";
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == name) {
+      ${cfg.group} = { };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 25 143 587 ];
+    };
+
+    environment.systemPackages = [
+      pkgs.maddy
+    ];
+  };
+}
diff --git a/nixos/modules/services/mail/mail.nix b/nixos/modules/services/mail/mail.nix
new file mode 100644
index 00000000000..fcc7ff6db91
--- /dev/null
+++ b/nixos/modules/services/mail/mail.nix
@@ -0,0 +1,34 @@
+{ config, options, lib, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.mail = {
+
+      sendmailSetuidWrapper = mkOption {
+        type = types.nullOr options.security.wrappers.type.nestedTypes.elemType;
+        default = null;
+        internal = true;
+        description = ''
+          Configuration for the sendmail setuid wapper.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf (config.services.mail.sendmailSetuidWrapper != null) {
+
+    security.wrappers.sendmail = config.services.mail.sendmailSetuidWrapper;
+
+  };
+
+}
diff --git a/nixos/modules/services/mail/mailcatcher.nix b/nixos/modules/services/mail/mailcatcher.nix
new file mode 100644
index 00000000000..84f06ed199d
--- /dev/null
+++ b/nixos/modules/services/mail/mailcatcher.nix
@@ -0,0 +1,68 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.services.mailcatcher;
+
+  inherit (lib) mkEnableOption mkIf mkOption types optionalString;
+in
+{
+  # interface
+
+  options = {
+
+    services.mailcatcher = {
+      enable = mkEnableOption "MailCatcher";
+
+      http.ip = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "The ip address of the http server.";
+      };
+
+      http.port = mkOption {
+        type = types.port;
+        default = 1080;
+        description = "The port address of the http server.";
+      };
+
+      http.path = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "Prefix to all HTTP paths.";
+        example = "/mailcatcher";
+      };
+
+      smtp.ip = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "The ip address of the smtp server.";
+      };
+
+      smtp.port = mkOption {
+        type = types.port;
+        default = 1025;
+        description = "The port address of the smtp server.";
+      };
+    };
+
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.mailcatcher ];
+
+    systemd.services.mailcatcher = {
+      description = "MailCatcher Service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+        ExecStart = "${pkgs.mailcatcher}/bin/mailcatcher --foreground --no-quit --http-ip ${cfg.http.ip} --http-port ${toString cfg.http.port} --smtp-ip ${cfg.smtp.ip} --smtp-port ${toString cfg.smtp.port}" + optionalString (cfg.http.path != null) " --http-path ${cfg.http.path}";
+        AmbientCapabilities = optionalString (cfg.http.port < 1024 || cfg.smtp.port < 1024) "cap_net_bind_service";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/mail/mailhog.nix b/nixos/modules/services/mail/mailhog.nix
new file mode 100644
index 00000000000..b113f4ff3de
--- /dev/null
+++ b/nixos/modules/services/mail/mailhog.nix
@@ -0,0 +1,82 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mailhog;
+
+  args = lib.concatStringsSep " " (
+    [
+      "-api-bind-addr :${toString cfg.apiPort}"
+      "-smtp-bind-addr :${toString cfg.smtpPort}"
+      "-ui-bind-addr :${toString cfg.uiPort}"
+      "-storage ${cfg.storage}"
+    ] ++ lib.optional (cfg.storage == "maildir")
+      "-maildir-path $STATE_DIRECTORY"
+    ++ cfg.extraArgs
+  );
+
+in
+{
+  ###### interface
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "mailhog" "user" ] "")
+  ];
+
+  options = {
+
+    services.mailhog = {
+      enable = mkEnableOption "MailHog";
+
+      storage = mkOption {
+        type = types.enum [ "maildir" "memory" ];
+        default = "memory";
+        description = "Store mails on disk or in memory.";
+      };
+
+      apiPort = mkOption {
+        type = types.port;
+        default = 8025;
+        description = "Port on which the API endpoint will listen.";
+      };
+
+      smtpPort = mkOption {
+        type = types.port;
+        default = 1025;
+        description = "Port on which the SMTP endpoint will listen.";
+      };
+
+      uiPort = mkOption {
+        type = types.port;
+        default = 8025;
+        description = "Port on which the HTTP UI will listen.";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "List of additional arguments to pass to the MailHog process.";
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.mailhog = {
+      description = "MailHog - Web and API based SMTP testing";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "exec";
+        ExecStart = "${pkgs.mailhog}/bin/MailHog ${args}";
+        DynamicUser = true;
+        Restart = "on-failure";
+        StateDirectory = "mailhog";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/mail/mailman.nix b/nixos/modules/services/mail/mailman.nix
new file mode 100644
index 00000000000..0c9b38b44b2
--- /dev/null
+++ b/nixos/modules/services/mail/mailman.nix
@@ -0,0 +1,462 @@
+{ config, pkgs, lib, ... }:          # mailman.nix
+
+with lib;
+
+let
+
+  cfg = config.services.mailman;
+
+  pythonEnv = pkgs.python3.withPackages (ps:
+    [ps.mailman ps.mailman-web]
+    ++ lib.optional cfg.hyperkitty.enable ps.mailman-hyperkitty
+    ++ cfg.extraPythonPackages);
+
+  # This deliberately doesn't use recursiveUpdate so users can
+  # override the defaults.
+  webSettings = {
+    DEFAULT_FROM_EMAIL = cfg.siteOwner;
+    SERVER_EMAIL = cfg.siteOwner;
+    ALLOWED_HOSTS = [ "localhost" "127.0.0.1" ] ++ cfg.webHosts;
+    COMPRESS_OFFLINE = true;
+    STATIC_ROOT = "/var/lib/mailman-web-static";
+    MEDIA_ROOT = "/var/lib/mailman-web/media";
+    LOGGING = {
+      version = 1;
+      disable_existing_loggers = true;
+      handlers.console.class = "logging.StreamHandler";
+      loggers.django = {
+        handlers = [ "console" ];
+        level = "INFO";
+      };
+    };
+    HAYSTACK_CONNECTIONS.default = {
+      ENGINE = "haystack.backends.whoosh_backend.WhooshEngine";
+      PATH = "/var/lib/mailman-web/fulltext-index";
+    };
+  } // cfg.webSettings;
+
+  webSettingsJSON = pkgs.writeText "settings.json" (builtins.toJSON webSettings);
+
+  # TODO: Should this be RFC42-ised so that users can set additional options without modifying the module?
+  postfixMtaConfig = pkgs.writeText "mailman-postfix.cfg" ''
+    [postfix]
+    postmap_command: ${pkgs.postfix}/bin/postmap
+    transport_file_type: hash
+  '';
+
+  mailmanCfg = lib.generators.toINI {} cfg.settings;
+
+  mailmanHyperkittyCfg = pkgs.writeText "mailman-hyperkitty.cfg" ''
+    [general]
+    # This is your HyperKitty installation, preferably on the localhost. This
+    # address will be used by Mailman to forward incoming emails to HyperKitty
+    # for archiving. It does not need to be publicly available, in fact it's
+    # better if it is not.
+    base_url: ${cfg.hyperkitty.baseUrl}
+
+    # Shared API key, must be the identical to the value in HyperKitty's
+    # settings.
+    api_key: @API_KEY@
+  '';
+
+in {
+
+  ###### interface
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "mailman" "hyperkittyBaseUrl" ]
+      [ "services" "mailman" "hyperkitty" "baseUrl" ])
+
+    (mkRemovedOptionModule [ "services" "mailman" "hyperkittyApiKey" ] ''
+      The Hyperkitty API key is now generated on first run, and not
+      stored in the world-readable Nix store.  To continue using
+      Hyperkitty, you must set services.mailman.hyperkitty.enable = true.
+    '')
+  ];
+
+  options = {
+
+    services.mailman = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable Mailman on this host. Requires an active MTA on the host (e.g. Postfix).";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.mailman;
+        defaultText = literalExpression "pkgs.mailman";
+        example = literalExpression "pkgs.mailman.override { archivers = []; }";
+        description = "Mailman package to use";
+      };
+
+      enablePostfix = mkOption {
+        type = types.bool;
+        default = true;
+        example = false;
+        description = ''
+          Enable Postfix integration. Requires an active Postfix installation.
+
+          If you want to use another MTA, set this option to false and configure
+          settings in services.mailman.settings.mta.
+
+          Refer to the Mailman manual for more info.
+        '';
+      };
+
+      siteOwner = mkOption {
+        type = types.str;
+        example = "postmaster@example.org";
+        description = ''
+          Certain messages that must be delivered to a human, but which can't
+          be delivered to a list owner (e.g. a bounce from a list owner), will
+          be sent to this address. It should point to a human.
+        '';
+      };
+
+      webHosts = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          The list of hostnames and/or IP addresses from which the Mailman Web
+          UI will accept requests. By default, "localhost" and "127.0.0.1" are
+          enabled. All additional names under which your web server accepts
+          requests for the UI must be listed here or incoming requests will be
+          rejected.
+        '';
+      };
+
+      webUser = mkOption {
+        type = types.str;
+        default = "mailman-web";
+        description = ''
+          User to run mailman-web as
+        '';
+      };
+
+      webSettings = mkOption {
+        type = types.attrs;
+        default = {};
+        description = ''
+          Overrides for the default mailman-web Django settings.
+        '';
+      };
+
+      serve = {
+        enable = mkEnableOption "Automatic nginx and uwsgi setup for mailman-web";
+      };
+
+      extraPythonPackages = mkOption {
+        description = "Packages to add to the python environment used by mailman and mailman-web";
+        type = types.listOf types.package;
+        default = [];
+      };
+
+      settings = mkOption {
+        description = "Settings for mailman.cfg";
+        type = types.attrsOf (types.attrsOf types.str);
+        default = {};
+      };
+
+      hyperkitty = {
+        enable = mkEnableOption "the Hyperkitty archiver for Mailman";
+
+        baseUrl = mkOption {
+          type = types.str;
+          default = "http://localhost:18507/archives/";
+          description = ''
+            Where can Mailman connect to Hyperkitty's internal API, preferably on
+            localhost?
+          '';
+        };
+      };
+
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.mailman.settings = {
+      mailman.site_owner = lib.mkDefault cfg.siteOwner;
+      mailman.layout = "fhs";
+
+      "paths.fhs" = {
+        bin_dir = "${pkgs.python3Packages.mailman}/bin";
+        var_dir = "/var/lib/mailman";
+        queue_dir = "$var_dir/queue";
+        template_dir = "$var_dir/templates";
+        log_dir = "/var/log/mailman";
+        lock_dir = "$var_dir/lock";
+        etc_dir = "/etc";
+        ext_dir = "$etc_dir/mailman.d";
+        pid_file = "/run/mailman/master.pid";
+      };
+
+      mta.configuration = lib.mkDefault (if cfg.enablePostfix then "${postfixMtaConfig}" else throw "When Mailman Postfix integration is disabled, set `services.mailman.settings.mta.configuration` to the path of the config file required to integrate with your MTA.");
+
+      "archiver.hyperkitty" = lib.mkIf cfg.hyperkitty.enable {
+        class = "mailman_hyperkitty.Archiver";
+        enable = "yes";
+        configuration = "/var/lib/mailman/mailman-hyperkitty.cfg";
+      };
+    } // (let
+      loggerNames = ["root" "archiver" "bounce" "config" "database" "debug" "error" "fromusenet" "http" "locks" "mischief" "plugins" "runner" "smtp"];
+      loggerSectionNames = map (n: "logging.${n}") loggerNames;
+      in lib.genAttrs loggerSectionNames(name: { handler = "stderr"; })
+    );
+
+    assertions = let
+      inherit (config.services) postfix;
+
+      requirePostfixHash = optionPath: dataFile:
+        with lib;
+        let
+          expected = "hash:/var/lib/mailman/data/${dataFile}";
+          value = attrByPath optionPath [] postfix;
+        in
+          { assertion = postfix.enable -> isList value && elem expected value;
+            message = ''
+              services.postfix.${concatStringsSep "." optionPath} must contain
+              "${expected}".
+              See <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>.
+            '';
+          };
+    in (lib.optionals cfg.enablePostfix [
+      { assertion = postfix.enable;
+        message = ''
+          Mailman's default NixOS configuration requires Postfix to be enabled.
+
+          If you want to use another MTA, set services.mailman.enablePostfix
+          to false and configure settings in services.mailman.settings.mta.
+
+          Refer to <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>
+          for more info.
+        '';
+      }
+      (requirePostfixHash [ "relayDomains" ] "postfix_domains")
+      (requirePostfixHash [ "config" "transport_maps" ] "postfix_lmtp")
+      (requirePostfixHash [ "config" "local_recipient_maps" ] "postfix_lmtp")
+    ]);
+
+    users.users.mailman = {
+      description = "GNU Mailman";
+      isSystemUser = true;
+      group = "mailman";
+    };
+    users.users.mailman-web = lib.mkIf (cfg.webUser == "mailman-web") {
+      description = "GNU Mailman web interface";
+      isSystemUser = true;
+      group = "mailman";
+    };
+    users.groups.mailman = {};
+
+    environment.etc."mailman.cfg".text = mailmanCfg;
+
+    environment.etc."mailman3/settings.py".text = ''
+      import os
+
+      # Required by mailman_web.settings, but will be overridden when
+      # settings_local.json is loaded.
+      os.environ["SECRET_KEY"] = ""
+
+      from mailman_web.settings.base import *
+      from mailman_web.settings.mailman import *
+
+      import json
+
+      with open('${webSettingsJSON}') as f:
+          globals().update(json.load(f))
+
+      with open('/var/lib/mailman-web/settings_local.json') as f:
+          globals().update(json.load(f))
+    '';
+
+    services.nginx = mkIf cfg.serve.enable {
+      enable = mkDefault true;
+      virtualHosts."${lib.head cfg.webHosts}" = {
+        serverAliases = cfg.webHosts;
+        locations = {
+          "/".extraConfig = "uwsgi_pass unix:/run/mailman-web.socket;";
+          "/static/".alias = webSettings.STATIC_ROOT + "/";
+        };
+      };
+    };
+
+    environment.systemPackages = [ (pkgs.buildEnv {
+      name = "mailman-tools";
+      # We don't want to pollute the system PATH with a python
+      # interpreter etc. so let's pick only the stuff we actually
+      # want from pythonEnv
+      pathsToLink = ["/bin"];
+      paths = [pythonEnv];
+      postBuild = ''
+        find $out/bin/ -mindepth 1 -not -name "mailman*" -delete
+      '';
+    }) ];
+
+    services.postfix = lib.mkIf cfg.enablePostfix {
+      recipientDelimiter = "+";         # bake recipient addresses in mail envelopes via VERP
+      config = {
+        owner_request_special = "no";   # Mailman handles -owner addresses on its own
+      };
+    };
+
+    systemd.sockets.mailman-uwsgi = lib.mkIf cfg.serve.enable {
+      wantedBy = ["sockets.target"];
+      before = ["nginx.service"];
+      socketConfig.ListenStream = "/run/mailman-web.socket";
+    };
+    systemd.services = {
+      mailman = {
+        description = "GNU Mailman Master Process";
+        after = [ "network.target" ];
+        restartTriggers = [ config.environment.etc."mailman.cfg".source ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          ExecStart = "${pythonEnv}/bin/mailman start";
+          ExecStop = "${pythonEnv}/bin/mailman stop";
+          User = "mailman";
+          Group = "mailman";
+          Type = "forking";
+          RuntimeDirectory = "mailman";
+          LogsDirectory = "mailman";
+          PIDFile = "/run/mailman/master.pid";
+        };
+      };
+
+      mailman-settings = {
+        description = "Generate settings files (including secrets) for Mailman";
+        before = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
+        requiredBy = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
+        path = with pkgs; [ jq ];
+        serviceConfig.Type = "oneshot";
+        script = ''
+          mailmanDir=/var/lib/mailman
+          mailmanWebDir=/var/lib/mailman-web
+
+          mailmanCfg=$mailmanDir/mailman-hyperkitty.cfg
+          mailmanWebCfg=$mailmanWebDir/settings_local.json
+
+          install -m 0775 -o mailman -g mailman -d /var/lib/mailman-web-static
+          install -m 0770 -o mailman -g mailman -d $mailmanDir
+          install -m 0770 -o ${cfg.webUser} -g mailman -d $mailmanWebDir
+
+          if [ ! -e $mailmanWebCfg ]; then
+              hyperkittyApiKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
+              secretKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
+
+              mailmanWebCfgTmp=$(mktemp)
+              jq -n '.MAILMAN_ARCHIVER_KEY=$archiver_key | .SECRET_KEY=$secret_key' \
+                  --arg archiver_key "$hyperkittyApiKey" \
+                  --arg secret_key "$secretKey" \
+                  >"$mailmanWebCfgTmp"
+              chown root:mailman "$mailmanWebCfgTmp"
+              chmod 440 "$mailmanWebCfgTmp"
+              mv -n "$mailmanWebCfgTmp" "$mailmanWebCfg"
+          fi
+
+          hyperkittyApiKey="$(jq -r .MAILMAN_ARCHIVER_KEY "$mailmanWebCfg")"
+          mailmanCfgTmp=$(mktemp)
+          sed "s/@API_KEY@/$hyperkittyApiKey/g" ${mailmanHyperkittyCfg} >"$mailmanCfgTmp"
+          chown mailman:mailman "$mailmanCfgTmp"
+          mv "$mailmanCfgTmp" "$mailmanCfg"
+        '';
+      };
+
+      mailman-web-setup = {
+        description = "Prepare mailman-web files and database";
+        before = [ "mailman-uwsgi.service" ];
+        requiredBy = [ "mailman-uwsgi.service" ];
+        restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
+        script = ''
+          [[ -e "${webSettings.STATIC_ROOT}" ]] && find "${webSettings.STATIC_ROOT}/" -mindepth 1 -delete
+          ${pythonEnv}/bin/mailman-web migrate
+          ${pythonEnv}/bin/mailman-web collectstatic
+          ${pythonEnv}/bin/mailman-web compress
+        '';
+        serviceConfig = {
+          User = cfg.webUser;
+          Group = "mailman";
+          Type = "oneshot";
+          WorkingDirectory = "/var/lib/mailman-web";
+        };
+      };
+
+      mailman-uwsgi = mkIf cfg.serve.enable (let
+        uwsgiConfig.uwsgi = {
+          type = "normal";
+          plugins = ["python3"];
+          home = pythonEnv;
+          module = "mailman_web.wsgi";
+          http = "127.0.0.1:18507";
+        };
+        uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig);
+      in {
+        wantedBy = ["multi-user.target"];
+        requires = ["mailman-uwsgi.socket" "mailman-web-setup.service"];
+        restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
+        serviceConfig = {
+          # Since the mailman-web settings.py obstinately creates a logs
+          # dir in the cwd, change to the (writable) runtime directory before
+          # starting uwsgi.
+          ExecStart = "${pkgs.coreutils}/bin/env -C $RUNTIME_DIRECTORY ${pkgs.uwsgi.override { plugins = ["python3"]; }}/bin/uwsgi --json ${uwsgiConfigFile}";
+          User = cfg.webUser;
+          Group = "mailman";
+          RuntimeDirectory = "mailman-uwsgi";
+        };
+      });
+
+      mailman-daily = {
+        description = "Trigger daily Mailman events";
+        startAt = "daily";
+        restartTriggers = [ config.environment.etc."mailman.cfg".source ];
+        serviceConfig = {
+          ExecStart = "${pythonEnv}/bin/mailman digests --send";
+          User = "mailman";
+          Group = "mailman";
+        };
+      };
+
+      hyperkitty = lib.mkIf cfg.hyperkitty.enable {
+        description = "GNU Hyperkitty QCluster Process";
+        after = [ "network.target" ];
+        restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
+        wantedBy = [ "mailman.service" "multi-user.target" ];
+        serviceConfig = {
+          ExecStart = "${pythonEnv}/bin/mailman-web qcluster";
+          User = cfg.webUser;
+          Group = "mailman";
+          WorkingDirectory = "/var/lib/mailman-web";
+        };
+      };
+    } // flip lib.mapAttrs' {
+      "minutely" = "minutely";
+      "quarter_hourly" = "*:00/15";
+      "hourly" = "hourly";
+      "daily" = "daily";
+      "weekly" = "weekly";
+      "yearly" = "yearly";
+    } (name: startAt:
+      lib.nameValuePair "hyperkitty-${name}" (lib.mkIf cfg.hyperkitty.enable {
+        description = "Trigger ${name} Hyperkitty events";
+        inherit startAt;
+        restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
+        serviceConfig = {
+          ExecStart = "${pythonEnv}/bin/mailman-web runjobs ${name}";
+          User = cfg.webUser;
+          Group = "mailman";
+          WorkingDirectory = "/var/lib/mailman-web";
+        };
+      }));
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ lheckemann qyliss ];
+    doc = ./mailman.xml;
+  };
+
+}
diff --git a/nixos/modules/services/mail/mailman.xml b/nixos/modules/services/mail/mailman.xml
new file mode 100644
index 00000000000..27247fb064f
--- /dev/null
+++ b/nixos/modules/services/mail/mailman.xml
@@ -0,0 +1,94 @@
+<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/mlmmj.nix b/nixos/modules/services/mail/mlmmj.nix
new file mode 100644
index 00000000000..fd74f2dc5f0
--- /dev/null
+++ b/nixos/modules/services/mail/mlmmj.nix
@@ -0,0 +1,171 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  concatMapLines = f: l: lib.concatStringsSep "\n" (map f l);
+
+  cfg = config.services.mlmmj;
+  stateDir = "/var/lib/mlmmj";
+  spoolDir = "/var/spool/mlmmj";
+  listDir = domain: list: "${spoolDir}/${domain}/${list}";
+  listCtl = domain: list: "${listDir domain list}/control";
+  transport = domain: list: "${domain}--${list}@local.list.mlmmj mlmmj:${domain}/${list}";
+  virtual = domain: list: "${list}@${domain} ${domain}--${list}@local.list.mlmmj";
+  alias = domain: list: "${list}: \"|${pkgs.mlmmj}/bin/mlmmj-receive -L ${listDir domain list}/\"";
+  subjectPrefix = list: "[${list}]";
+  listAddress = domain: list: "${list}@${domain}";
+  customHeaders = domain: list: [
+    "List-Id: ${list}"
+    "Reply-To: ${list}@${domain}"
+    "List-Post: <mailto:${list}@${domain}>"
+    "List-Help: <mailto:${list}+help@${domain}>"
+    "List-Subscribe: <mailto:${list}+subscribe@${domain}>"
+    "List-Unsubscribe: <mailto:${list}+unsubscribe@${domain}>"
+  ];
+  footer = domain: list: "To unsubscribe send a mail to ${list}+unsubscribe@${domain}";
+  createList = d: l:
+    let ctlDir = listCtl d l; in
+    ''
+      for DIR in incoming queue queue/discarded archive text subconf unsubconf \
+                 bounce control moderation subscribers.d digesters.d requeue \
+                 nomailsubs.d
+      do
+             mkdir -p '${listDir d l}'/"$DIR"
+      done
+      ${pkgs.coreutils}/bin/mkdir -p ${ctlDir}
+      echo ${listAddress d l} > '${ctlDir}/listaddress'
+      [ ! -e ${ctlDir}/customheaders ] && \
+          echo "${lib.concatStringsSep "\n" (customHeaders d l)}" > '${ctlDir}/customheaders'
+      [ ! -e ${ctlDir}/footer ] && \
+          echo ${footer d l} > '${ctlDir}/footer'
+      [ ! -e ${ctlDir}/prefix ] && \
+          echo ${subjectPrefix l} > '${ctlDir}/prefix'
+    '';
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.mlmmj = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable mlmmj";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "mlmmj";
+        description = "mailinglist local user";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "mlmmj";
+        description = "mailinglist local group";
+      };
+
+      listDomain = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Set the mailing list domain";
+      };
+
+      mailLists = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "The collection of hosted maillists";
+      };
+
+      maintInterval = mkOption {
+        type = types.str;
+        default = "20min";
+        description = ''
+          Time interval between mlmmj-maintd runs, see
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry> for format information.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.${cfg.user} = {
+      description = "mlmmj user";
+      home = stateDir;
+      createHome = true;
+      uid = config.ids.uids.mlmmj;
+      group = cfg.group;
+      useDefaultShell = true;
+    };
+
+    users.groups.${cfg.group} = {
+      gid = config.ids.gids.mlmmj;
+    };
+
+    services.postfix = {
+      enable = true;
+      recipientDelimiter= "+";
+      masterConfig.mlmmj = {
+        type = "unix";
+        private = true;
+        privileged = true;
+        chroot = false;
+        wakeup = 0;
+        command = "pipe";
+        args = [
+          "flags=ORhu"
+          "user=mlmmj"
+          "argv=${pkgs.mlmmj}/bin/mlmmj-receive"
+          "-F"
+          "-L"
+          "${spoolDir}/$nexthop"
+        ];
+      };
+
+      extraAliases = concatMapLines (alias cfg.listDomain) cfg.mailLists;
+
+      extraConfig = "propagate_unmatched_extensions = virtual";
+
+      virtual = concatMapLines (virtual cfg.listDomain) cfg.mailLists;
+      transport = concatMapLines (transport cfg.listDomain) cfg.mailLists;
+    };
+
+    environment.systemPackages = [ pkgs.mlmmj ];
+
+    system.activationScripts.mlmmj = ''
+          ${pkgs.coreutils}/bin/mkdir -p ${stateDir} ${spoolDir}/${cfg.listDomain}
+          ${pkgs.coreutils}/bin/chown -R ${cfg.user}:${cfg.group} ${spoolDir}
+          ${concatMapLines (createList cfg.listDomain) cfg.mailLists}
+          ${pkgs.postfix}/bin/postmap /etc/postfix/virtual
+          ${pkgs.postfix}/bin/postmap /etc/postfix/transport
+      '';
+
+    systemd.services.mlmmj-maintd = {
+      description = "mlmmj maintenance daemon";
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.mlmmj}/bin/mlmmj-maintd -F -d ${spoolDir}/${cfg.listDomain}";
+      };
+    };
+
+    systemd.timers.mlmmj-maintd = {
+      description = "mlmmj maintenance timer";
+      timerConfig.OnUnitActiveSec = cfg.maintInterval;
+      wantedBy = [ "timers.target" ];
+    };
+  };
+
+}
diff --git a/nixos/modules/services/mail/nullmailer.nix b/nixos/modules/services/mail/nullmailer.nix
new file mode 100644
index 00000000000..f9c34566997
--- /dev/null
+++ b/nixos/modules/services/mail/nullmailer.nix
@@ -0,0 +1,244 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  options = {
+
+    services.nullmailer = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable nullmailer daemon.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "nullmailer";
+        description = ''
+          User to use to run nullmailer-send.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "nullmailer";
+        description = ''
+          Group to use to run nullmailer-send.
+        '';
+      };
+
+      setSendmail = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to set the system sendmail to nullmailer's.";
+      };
+
+      remotesFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Path to the <code>remotes</code> control file. This file contains a
+          list of remote servers to which to send each message.
+
+          See <code>man 8 nullmailer-send</code> for syntax and available
+          options.
+        '';
+      };
+
+      config = {
+        adminaddr = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            If set, all recipients to users at either "localhost" (the literal string)
+            or the canonical host name (from the me control attribute) are remapped to this address.
+            This is provided to allow local daemons to be able to send email to
+            "somebody@localhost" and have it go somewhere sensible instead of being  bounced
+            by your relay host. To send to multiple addresses,
+            put them all on one line separated by a comma.
+          '';
+        };
+
+        allmailfrom = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            If set, content will override the envelope sender on all messages.
+          '';
+        };
+
+        defaultdomain = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+             The content of this attribute is appended to any host name that
+             does not contain a period (except localhost), including defaulthost
+             and idhost. Defaults to the value of the me attribute, if it exists,
+             otherwise the literal name defauldomain.
+          '';
+        };
+
+        defaulthost = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+             The content of this attribute is appended to any address that
+             is missing a host name. Defaults to the value of the me control
+             attribute, if it exists, otherwise the literal name defaulthost.
+          '';
+        };
+
+        doublebounceto = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            If the original sender was empty (the original message was a
+            delivery status or disposition notification), the double bounce
+            is sent to the address in this attribute.
+          '';
+        };
+
+        helohost = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            Sets  the  environment variable $HELOHOST which is used by the
+            SMTP protocol module to set the parameter given to the HELO command.
+            Defaults to the value of the me configuration attribute.
+          '';
+        };
+
+        idhost = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            The content of this attribute is used when building the message-id
+            string for the message. Defaults to the canonicalized value of defaulthost.
+          '';
+        };
+
+        maxpause = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+             The maximum time to pause between successive queue runs, in seconds.
+             Defaults to 24 hours (86400).
+          '';
+        };
+
+        me = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+             The fully-qualifiled host name of the computer running nullmailer.
+             Defaults to the literal name me.
+          '';
+        };
+
+        pausetime = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            The minimum time to pause between successive queue runs when there
+            are messages in the queue, in seconds. Defaults to 1 minute (60).
+            Each time this timeout is reached, the timeout is doubled to a
+            maximum of maxpause. After new messages are injected, the timeout
+            is reset.  If this is set to 0, nullmailer-send will exit
+            immediately after going through the queue once (one-shot mode).
+          '';
+        };
+
+        remotes = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            A list of remote servers to which to send each message. Each line
+            contains a remote host name or address followed by an optional
+            protocol string, separated by white space.
+
+            See <code>man 8 nullmailer-send</code> for syntax and available
+            options.
+
+            WARNING: This is stored world-readable in the nix store. If you need
+            to specify any secret credentials here, consider using the
+            <code>remotesFile</code> option instead.
+          '';
+        };
+
+        sendtimeout = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            The  time to wait for a remote module listed above to complete sending
+            a message before killing it and trying again, in seconds.
+            Defaults to 1 hour (3600).  If this is set to 0, nullmailer-send
+            will wait forever for messages to complete sending.
+          '';
+        };
+      };
+    };
+  };
+
+  config = let
+    cfg = config.services.nullmailer;
+  in mkIf cfg.enable {
+
+    assertions = [
+      { assertion = cfg.config.remotes == null || cfg.remotesFile == null;
+        message = "Only one of `remotesFile` or `config.remotes` may be used at a time.";
+      }
+    ];
+
+    environment = {
+      systemPackages = [ pkgs.nullmailer ];
+      etc = let
+        validAttrs = filterAttrs (name: value: value != null) cfg.config;
+      in
+        (foldl' (as: name: as // { "nullmailer/${name}".text = validAttrs.${name}; }) {} (attrNames validAttrs))
+          // optionalAttrs (cfg.remotesFile != null) { "nullmailer/remotes".source = cfg.remotesFile; };
+    };
+
+    users = {
+      users.${cfg.user} = {
+        description = "Nullmailer relay-only mta user";
+        group = cfg.group;
+        isSystemUser = true;
+      };
+
+      groups.${cfg.group} = { };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d /var/spool/nullmailer - ${cfg.user} - - -"
+    ];
+
+    systemd.services.nullmailer = {
+      description = "nullmailer";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      preStart = ''
+        mkdir -p /var/spool/nullmailer/{queue,tmp,failed}
+        rm -f /var/spool/nullmailer/trigger && mkfifo -m 660 /var/spool/nullmailer/trigger
+      '';
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.nullmailer}/bin/nullmailer-send";
+        Restart = "always";
+      };
+    };
+
+    services.mail.sendmailSetuidWrapper = mkIf cfg.setSendmail {
+      program = "sendmail";
+      source = "${pkgs.nullmailer}/bin/sendmail";
+      owner = cfg.user;
+      group = cfg.group;
+      setuid = true;
+      setgid = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/mail/offlineimap.nix b/nixos/modules/services/mail/offlineimap.nix
new file mode 100644
index 00000000000..45147758119
--- /dev/null
+++ b/nixos/modules/services/mail/offlineimap.nix
@@ -0,0 +1,72 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.offlineimap;
+in {
+
+  options.services.offlineimap = {
+    enable = mkEnableOption "OfflineIMAP, a software to dispose your mailbox(es) as a local Maildir(s)";
+
+    install = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to install a user service for Offlineimap. Once
+        the service is started, emails will be fetched automatically.
+
+        The service must be manually started for each user with
+        "systemctl --user start offlineimap" or globally through
+        <varname>services.offlineimap.enable</varname>.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.offlineimap;
+      defaultText = literalExpression "pkgs.offlineimap";
+      description = "Offlineimap derivation to use.";
+    };
+
+    path = mkOption {
+      type = types.listOf types.path;
+      default = [];
+      example = literalExpression "[ pkgs.pass pkgs.bash pkgs.notmuch ]";
+      description = "List of derivations to put in Offlineimap's path.";
+    };
+
+    onCalendar = mkOption {
+      type = types.str;
+      default = "*:0/3"; # every 3 minutes
+      description = "How often is offlineimap started. Default is '*:0/3' meaning every 3 minutes. See systemd.time(7) for more information about the format.";
+    };
+
+    timeoutStartSec = mkOption {
+      type = types.str;
+      default = "120sec"; # Kill if still alive after 2 minutes
+      description = "How long waiting for offlineimap before killing it. Default is '120sec' meaning every 2 minutes. See systemd.time(7) for more information about the format.";
+    };
+  };
+  config = mkIf (cfg.enable || cfg.install) {
+    systemd.user.services.offlineimap = {
+      description = "Offlineimap: a software to dispose your mailbox(es) as a local Maildir(s)";
+      serviceConfig = {
+        Type      = "oneshot";
+        ExecStart = "${cfg.package}/bin/offlineimap -u syslog -o -1";
+        TimeoutStartSec = cfg.timeoutStartSec;
+      };
+      path = cfg.path;
+    };
+    environment.systemPackages = [ cfg.package ];
+    systemd.user.timers.offlineimap = {
+      description = "offlineimap timer";
+      timerConfig               = {
+        Unit = "offlineimap.service";
+        OnCalendar = cfg.onCalendar;
+        # start immediately after computer is started:
+        Persistent = "true";
+      };
+    } // optionalAttrs cfg.enable { wantedBy = [ "default.target" ]; };
+  };
+}
diff --git a/nixos/modules/services/mail/opendkim.nix b/nixos/modules/services/mail/opendkim.nix
new file mode 100644
index 00000000000..f1ffc5d3aee
--- /dev/null
+++ b/nixos/modules/services/mail/opendkim.nix
@@ -0,0 +1,167 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.opendkim;
+
+  defaultSock = "local:/run/opendkim/opendkim.sock";
+
+  keyFile = "${cfg.keyPath}/${cfg.selector}.private";
+
+  args = [ "-f" "-l"
+           "-p" cfg.socket
+           "-d" cfg.domains
+           "-k" keyFile
+           "-s" cfg.selector
+         ] ++ optionals (cfg.configFile != null) [ "-x" cfg.configFile ];
+
+in {
+  imports = [
+    (mkRenamedOptionModule [ "services" "opendkim" "keyFile" ] [ "services" "opendkim" "keyPath" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.opendkim = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the OpenDKIM sender authentication system.";
+      };
+
+      socket = mkOption {
+        type = types.str;
+        default = defaultSock;
+        description = "Socket which is used for communication with OpenDKIM.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "opendkim";
+        description = "User for the daemon.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "opendkim";
+        description = "Group for the daemon.";
+      };
+
+      domains = mkOption {
+        type = types.str;
+        default = "csl:${config.networking.hostName}";
+        defaultText = literalExpression ''"csl:''${config.networking.hostName}"'';
+        example = "csl:example.com,mydomain.net";
+        description = ''
+          Local domains set (see <literal>opendkim(8)</literal> for more information on datasets).
+          Messages from them are signed, not verified.
+        '';
+      };
+
+      keyPath = mkOption {
+        type = types.path;
+        description = ''
+          The path that opendkim should put its generated private keys into.
+          The DNS settings will be found in this directory with the name selector.txt.
+        '';
+        default = "/var/lib/opendkim/keys";
+      };
+
+      selector = mkOption {
+        type = types.str;
+        description = "Selector to use when signing.";
+      };
+
+      configFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = "Additional opendkim configuration.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users = optionalAttrs (cfg.user == "opendkim") {
+      opendkim = {
+        group = cfg.group;
+        uid = config.ids.uids.opendkim;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "opendkim") {
+      opendkim.gid = config.ids.gids.opendkim;
+    };
+
+    environment.systemPackages = [ pkgs.opendkim ];
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.keyPath}' - ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.opendkim = {
+      description = "OpenDKIM signing and verification daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        cd "${cfg.keyPath}"
+        if ! test -f ${cfg.selector}.private; then
+          ${pkgs.opendkim}/bin/opendkim-genkey -s ${cfg.selector} -d all-domains-generic-key
+          echo "Generated OpenDKIM key! Please update your DNS settings:\n"
+          echo "-------------------------------------------------------------"
+          cat ${cfg.selector}.txt
+          echo "-------------------------------------------------------------"
+        fi
+      '';
+
+      serviceConfig = {
+        ExecStart = "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}";
+        User = cfg.user;
+        Group = cfg.group;
+        RuntimeDirectory = optional (cfg.socket == defaultSock) "opendkim";
+        StateDirectory = "opendkim";
+        StateDirectoryMode = "0700";
+        ReadWritePaths = [ cfg.keyPath ];
+
+        AmbientCapabilities = [];
+        CapabilityBoundingSet = "";
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        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;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6 AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged @resources" ];
+        UMask = "0077";
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/mail/opensmtpd.nix b/nixos/modules/services/mail/opensmtpd.nix
new file mode 100644
index 00000000000..e7632be2804
--- /dev/null
+++ b/nixos/modules/services/mail/opensmtpd.nix
@@ -0,0 +1,135 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.opensmtpd;
+  conf = pkgs.writeText "smtpd.conf" cfg.serverConfiguration;
+  args = concatStringsSep " " cfg.extraServerArgs;
+
+  sendmail = pkgs.runCommand "opensmtpd-sendmail" { preferLocalBuild = true; } ''
+    mkdir -p $out/bin
+    ln -s ${cfg.package}/sbin/smtpctl $out/bin/sendmail
+  '';
+
+in {
+
+  ###### interface
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "opensmtpd" "addSendmailToSystemPath" ] [ "services" "opensmtpd" "setSendmail" ])
+  ];
+
+  options = {
+
+    services.opensmtpd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the OpenSMTPD server.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.opensmtpd;
+        defaultText = literalExpression "pkgs.opensmtpd";
+        description = "The OpenSMTPD package to use.";
+      };
+
+      setSendmail = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to set the system sendmail to OpenSMTPD's.";
+      };
+
+      extraServerArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "-v" "-P mta" ];
+        description = ''
+          Extra command line arguments provided when the smtpd process
+          is started.
+        '';
+      };
+
+      serverConfiguration = mkOption {
+        type = types.lines;
+        example = ''
+          listen on lo
+          accept for any deliver to lmtp localhost:24
+        '';
+        description = ''
+          The contents of the smtpd.conf configuration file. See the
+          OpenSMTPD documentation for syntax information.
+        '';
+      };
+
+      procPackages = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        description = ''
+          Packages to search for filters, tables, queues, and schedulers.
+
+          Add OpenSMTPD-extras here if you want to use the filters, etc. from
+          that package.
+        '';
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable rec {
+    users.groups = {
+      smtpd.gid = config.ids.gids.smtpd;
+      smtpq.gid = config.ids.gids.smtpq;
+    };
+
+    users.users = {
+      smtpd = {
+        description = "OpenSMTPD process user";
+        uid = config.ids.uids.smtpd;
+        group = "smtpd";
+      };
+      smtpq = {
+        description = "OpenSMTPD queue user";
+        uid = config.ids.uids.smtpq;
+        group = "smtpq";
+      };
+    };
+
+    security.wrappers.smtpctl = {
+      owner = "root";
+      group = "smtpq";
+      setuid = false;
+      setgid = true;
+      source = "${cfg.package}/bin/smtpctl";
+    };
+
+    services.mail.sendmailSetuidWrapper = mkIf cfg.setSendmail
+      (security.wrappers.smtpctl // { program = "sendmail"; });
+
+    systemd.tmpfiles.rules = [
+      "d /var/spool/smtpd 711 root - - -"
+      "d /var/spool/smtpd/offline 770 root smtpq - -"
+      "d /var/spool/smtpd/purge 700 smtpq root - -"
+    ];
+
+    systemd.services.opensmtpd = let
+      procEnv = pkgs.buildEnv {
+        name = "opensmtpd-procs";
+        paths = [ cfg.package ] ++ cfg.procPackages;
+        pathsToLink = [ "/libexec/opensmtpd" ];
+      };
+    in {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig.ExecStart = "${cfg.package}/sbin/smtpd -d -f ${conf} ${args}";
+      environment.OPENSMTPD_PROC_PATH = "${procEnv}/libexec/opensmtpd";
+    };
+  };
+}
diff --git a/nixos/modules/services/mail/pfix-srsd.nix b/nixos/modules/services/mail/pfix-srsd.nix
new file mode 100644
index 00000000000..e3dbf2a014f
--- /dev/null
+++ b/nixos/modules/services/mail/pfix-srsd.nix
@@ -0,0 +1,56 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.pfix-srsd = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to run the postfix sender rewriting scheme daemon.";
+      };
+
+      domain = mkOption {
+        description = "The domain for which to enable srs";
+        type = types.str;
+        example = "example.com";
+      };
+
+      secretsFile = mkOption {
+        description = ''
+          The secret data used to encode the SRS address.
+          to generate, use a command like:
+          <literal>for n in $(seq 5); do dd if=/dev/urandom count=1 bs=1024 status=none | sha256sum | sed 's/  -$//' | sed 's/^/          /'; done</literal>
+        '';
+        type = types.path;
+        default = "/var/lib/pfix-srsd/secrets";
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf config.services.pfix-srsd.enable {
+    environment = {
+      systemPackages = [ pkgs.pfixtools ];
+    };
+
+    systemd.services.pfix-srsd = {
+      description = "Postfix sender rewriting scheme daemon";
+      before = [ "postfix.service" ];
+      #note that we use requires rather than wants because postfix
+      #is unable to process (almost) all mail without srsd
+      requiredBy = [ "postfix.service" ];
+      serviceConfig = {
+        Type = "forking";
+        PIDFile = "/run/pfix-srsd.pid";
+        ExecStart = "${pkgs.pfixtools}/bin/pfix-srsd -p /run/pfix-srsd.pid -I ${config.services.pfix-srsd.domain} ${config.services.pfix-srsd.secretsFile}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix
new file mode 100644
index 00000000000..23d3574ae27
--- /dev/null
+++ b/nixos/modules/services/mail/postfix.nix
@@ -0,0 +1,988 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.postfix;
+  user = cfg.user;
+  group = cfg.group;
+  setgidGroup = cfg.setgidGroup;
+
+  haveAliases = cfg.postmasterAlias != "" || cfg.rootAlias != ""
+                      || cfg.extraAliases != "";
+  haveCanonical = cfg.canonical != "";
+  haveTransport = cfg.transport != "";
+  haveVirtual = cfg.virtual != "";
+  haveLocalRecipients = cfg.localRecipients != null;
+
+  clientAccess =
+    optional (cfg.dnsBlacklistOverrides != "")
+      "check_client_access hash:/etc/postfix/client_access";
+
+  dnsBl =
+    optionals (cfg.dnsBlacklists != [])
+      (map (s: "reject_rbl_client " + s) cfg.dnsBlacklists);
+
+  clientRestrictions = concatStringsSep ", " (clientAccess ++ dnsBl);
+
+  mainCf = let
+    escape = replaceStrings ["$"] ["$$"];
+    mkList = items: "\n  " + concatStringsSep ",\n  " items;
+    mkVal = value:
+      if isList value then mkList value
+        else " " + (if value == true then "yes"
+        else if value == false then "no"
+        else toString value);
+    mkEntry = name: value: "${escape name} =${mkVal value}";
+  in
+    concatStringsSep "\n" (mapAttrsToList mkEntry cfg.config)
+      + "\n" + cfg.extraConfig;
+
+  masterCfOptions = { options, config, name, ... }: {
+    options = {
+      name = mkOption {
+        type = types.str;
+        default = name;
+        example = "smtp";
+        description = ''
+          The name of the service to run. Defaults to the attribute set key.
+        '';
+      };
+
+      type = mkOption {
+        type = types.enum [ "inet" "unix" "unix-dgram" "fifo" "pass" ];
+        default = "unix";
+        example = "inet";
+        description = "The type of the service";
+      };
+
+      private = mkOption {
+        type = types.bool;
+        example = false;
+        description = ''
+          Whether the service's sockets and storage directory is restricted to
+          be only available via the mail system. If <literal>null</literal> is
+          given it uses the postfix default <literal>true</literal>.
+        '';
+      };
+
+      privileged = mkOption {
+        type = types.bool;
+        example = true;
+        description = "";
+      };
+
+      chroot = mkOption {
+        type = types.bool;
+        example = true;
+        description = ''
+          Whether the service is chrooted to have only access to the
+          <option>services.postfix.queueDir</option> and the closure of
+          store paths specified by the <option>program</option> option.
+        '';
+      };
+
+      wakeup = mkOption {
+        type = types.int;
+        example = 60;
+        description = ''
+          Automatically wake up the service after the specified number of
+          seconds. If <literal>0</literal> is given, never wake the service
+          up.
+        '';
+      };
+
+      wakeupUnusedComponent = mkOption {
+        type = types.bool;
+        example = false;
+        description = ''
+          If set to <literal>false</literal> the component will only be woken
+          up if it is used. This is equivalent to postfix' notion of adding a
+          question mark behind the wakeup time in
+          <filename>master.cf</filename>
+        '';
+      };
+
+      maxproc = mkOption {
+        type = types.int;
+        example = 1;
+        description = ''
+          The maximum number of processes to spawn for this service. If the
+          value is <literal>0</literal> it doesn't have any limit. If
+          <literal>null</literal> is given it uses the postfix default of
+          <literal>100</literal>.
+        '';
+      };
+
+      command = mkOption {
+        type = types.str;
+        default = name;
+        example = "smtpd";
+        description = ''
+          A program name specifying a Postfix service/daemon process.
+          By default it's the attribute <option>name</option>.
+        '';
+      };
+
+      args = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "-o" "smtp_helo_timeout=5" ];
+        description = ''
+          Arguments to pass to the <option>command</option>. There is no shell
+          processing involved and shell syntax is passed verbatim to the
+          process.
+        '';
+      };
+
+      rawEntry = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        internal = true;
+        description = ''
+          The raw configuration line for the <filename>master.cf</filename>.
+        '';
+      };
+    };
+
+    config.rawEntry = let
+      mkBool = bool: if bool then "y" else "n";
+      mkArg = arg: "${optionalString (hasPrefix "-" arg) "\n  "}${arg}";
+
+      maybeOption = fun: option:
+        if options.${option}.isDefined then fun config.${option} else "-";
+
+      # This is special, because we have two options for this value.
+      wakeup = let
+        wakeupDefined = options.wakeup.isDefined;
+        wakeupUCDefined = options.wakeupUnusedComponent.isDefined;
+        finalValue = toString config.wakeup
+                   + optionalString (wakeupUCDefined && !config.wakeupUnusedComponent) "?";
+      in if wakeupDefined then finalValue else "-";
+
+    in [
+      config.name
+      config.type
+      (maybeOption mkBool "private")
+      (maybeOption (b: mkBool (!b)) "privileged")
+      (maybeOption mkBool "chroot")
+      wakeup
+      (maybeOption toString "maxproc")
+      (config.command + " " + concatMapStringsSep " " mkArg config.args)
+    ];
+  };
+
+  masterCfContent = let
+
+    labels = [
+      "# service" "type" "private" "unpriv" "chroot" "wakeup" "maxproc"
+      "command + args"
+    ];
+
+    labelDefaults = [
+      "# " "" "(yes)" "(yes)" "(no)" "(never)" "(100)" "" ""
+    ];
+
+    masterCf = mapAttrsToList (const (getAttr "rawEntry")) cfg.masterConfig;
+
+    # A list of the maximum width of the columns across all lines and labels
+    maxWidths = let
+      foldLine = line: acc: let
+        columnLengths = map stringLength line;
+      in zipListsWith max acc columnLengths;
+      # We need to handle the last column specially here, because it's
+      # open-ended (command + args).
+      lines = [ labels labelDefaults ] ++ (map (l: init l ++ [""]) masterCf);
+    in foldr foldLine (genList (const 0) (length labels)) lines;
+
+    # Pad a string with spaces from the right (opposite of fixedWidthString).
+    pad = width: str: let
+      padWidth = width - stringLength str;
+      padding = concatStrings (genList (const " ") padWidth);
+    in str + optionalString (padWidth > 0) padding;
+
+    # It's + 2 here, because that's the amount of spacing between columns.
+    fullWidth = foldr (width: acc: acc + width + 2) 0 maxWidths;
+
+    formatLine = line: concatStringsSep "  " (zipListsWith pad maxWidths line);
+
+    formattedLabels = let
+      sep = "# " + concatStrings (genList (const "=") (fullWidth + 5));
+      lines = [ sep (formatLine labels) (formatLine labelDefaults) sep ];
+    in concatStringsSep "\n" lines;
+
+  in formattedLabels + "\n" + concatMapStringsSep "\n" formatLine masterCf + "\n" + cfg.extraMasterConf;
+
+  headerCheckOptions = { ... }:
+  {
+    options = {
+      pattern = mkOption {
+        type = types.str;
+        default = "/^.*/";
+        example = "/^X-Mailer:/";
+        description = "A regexp pattern matching the header";
+      };
+      action = mkOption {
+        type = types.str;
+        default = "DUNNO";
+        example = "BCC mail@example.com";
+        description = "The action to be executed when the pattern is matched";
+      };
+    };
+  };
+
+  headerChecks = concatStringsSep "\n" (map (x: "${x.pattern} ${x.action}") cfg.headerChecks) + cfg.extraHeaderChecks;
+
+  aliases = let seperator = if cfg.aliasMapType == "hash" then ":" else ""; in
+    optionalString (cfg.postmasterAlias != "") ''
+      postmaster${seperator} ${cfg.postmasterAlias}
+    ''
+    + optionalString (cfg.rootAlias != "") ''
+      root${seperator} ${cfg.rootAlias}
+    ''
+    + cfg.extraAliases
+  ;
+
+  aliasesFile = pkgs.writeText "postfix-aliases" aliases;
+  canonicalFile = pkgs.writeText "postfix-canonical" cfg.canonical;
+  virtualFile = pkgs.writeText "postfix-virtual" cfg.virtual;
+  localRecipientMapFile = pkgs.writeText "postfix-local-recipient-map" (concatMapStrings (x: x + " ACCEPT\n") cfg.localRecipients);
+  checkClientAccessFile = pkgs.writeText "postfix-check-client-access" cfg.dnsBlacklistOverrides;
+  mainCfFile = pkgs.writeText "postfix-main.cf" mainCf;
+  masterCfFile = pkgs.writeText "postfix-master.cf" masterCfContent;
+  transportFile = pkgs.writeText "postfix-transport" cfg.transport;
+  headerChecksFile = pkgs.writeText "postfix-header-checks" headerChecks;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.postfix = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to run the Postfix mail server.";
+      };
+
+      enableSmtp = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to enable smtp in master.cf.";
+      };
+
+      enableSubmission = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable smtp submission.";
+      };
+
+      enableSubmissions = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable smtp submission via smtps.
+
+          According to RFC 8314 this should be preferred
+          over STARTTLS for submission of messages by end user clients.
+        '';
+      };
+
+      submissionOptions = mkOption {
+        type = with types; attrsOf str;
+        default = {
+          smtpd_tls_security_level = "encrypt";
+          smtpd_sasl_auth_enable = "yes";
+          smtpd_client_restrictions = "permit_sasl_authenticated,reject";
+          milter_macro_daemon_name = "ORIGINATING";
+        };
+        example = {
+          smtpd_tls_security_level = "encrypt";
+          smtpd_sasl_auth_enable = "yes";
+          smtpd_sasl_type = "dovecot";
+          smtpd_client_restrictions = "permit_sasl_authenticated,reject";
+          milter_macro_daemon_name = "ORIGINATING";
+        };
+        description = "Options for the submission config in master.cf";
+      };
+
+      submissionsOptions = mkOption {
+        type = with types; attrsOf str;
+        default = {
+          smtpd_sasl_auth_enable = "yes";
+          smtpd_client_restrictions = "permit_sasl_authenticated,reject";
+          milter_macro_daemon_name = "ORIGINATING";
+        };
+        example = {
+          smtpd_sasl_auth_enable = "yes";
+          smtpd_sasl_type = "dovecot";
+          smtpd_client_restrictions = "permit_sasl_authenticated,reject";
+          milter_macro_daemon_name = "ORIGINATING";
+        };
+        description = ''
+          Options for the submission config via smtps in master.cf.
+
+          smtpd_tls_security_level will be set to encrypt, if it is missing
+          or has one of the values "may" or "none".
+
+          smtpd_tls_wrappermode with value "yes" will be added automatically.
+        '';
+      };
+
+      setSendmail = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to set the system sendmail to postfix's.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "postfix";
+        description = "What to call the Postfix user (must be used only for postfix).";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "postfix";
+        description = "What to call the Postfix group (must be used only for postfix).";
+      };
+
+      setgidGroup = mkOption {
+        type = types.str;
+        default = "postdrop";
+        description = "
+          How to call postfix setgid group (for postdrop). Should
+          be uniquely used group.
+        ";
+      };
+
+      networks = mkOption {
+        type = types.nullOr (types.listOf types.str);
+        default = null;
+        example = ["192.168.0.1/24"];
+        description = "
+          Net masks for trusted - allowed to relay mail to third parties -
+          hosts. Leave empty to use mynetworks_style configuration or use
+          default (localhost-only).
+        ";
+      };
+
+      networksStyle = mkOption {
+        type = types.str;
+        default = "";
+        description = "
+          Name of standard way of trusted network specification to use,
+          leave blank if you specify it explicitly or if you want to use
+          default (localhost-only).
+        ";
+      };
+
+      hostname = mkOption {
+        type = types.str;
+        default = "";
+        description ="
+          Hostname to use. Leave blank to use just the hostname of machine.
+          It should be FQDN.
+        ";
+      };
+
+      domain = mkOption {
+        type = types.str;
+        default = "";
+        description ="
+          Domain to use. Leave blank to use hostname minus first component.
+        ";
+      };
+
+      origin = mkOption {
+        type = types.str;
+        default = "";
+        description ="
+          Origin to use in outgoing e-mail. Leave blank to use hostname.
+        ";
+      };
+
+      destination = mkOption {
+        type = types.nullOr (types.listOf types.str);
+        default = null;
+        example = ["localhost"];
+        description = "
+          Full (!) list of domains we deliver locally. Leave blank for
+          acceptable Postfix default.
+        ";
+      };
+
+      relayDomains = mkOption {
+        type = types.nullOr (types.listOf types.str);
+        default = null;
+        example = ["localdomain"];
+        description = "
+          List of domains we agree to relay to. Default is empty.
+        ";
+      };
+
+      relayHost = mkOption {
+        type = types.str;
+        default = "";
+        description = "
+          Mail relay for outbound mail.
+        ";
+      };
+
+      relayPort = mkOption {
+        type = types.int;
+        default = 25;
+        description = "
+          SMTP port for relay mail relay.
+        ";
+      };
+
+      lookupMX = mkOption {
+        type = types.bool;
+        default = false;
+        description = "
+          Whether relay specified is just domain whose MX must be used.
+        ";
+      };
+
+      postmasterAlias = mkOption {
+        type = types.str;
+        default = "root";
+        description = "
+          Who should receive postmaster e-mail. Multiple values can be added by
+          separating values with comma.
+        ";
+      };
+
+      rootAlias = mkOption {
+        type = types.str;
+        default = "";
+        description = "
+          Who should receive root e-mail. Blank for no redirection.
+          Multiple values can be added by separating values with comma.
+        ";
+      };
+
+      extraAliases = mkOption {
+        type = types.lines;
+        default = "";
+        description = "
+          Additional entries to put verbatim into aliases file, cf. man-page aliases(8).
+        ";
+      };
+
+      aliasMapType = mkOption {
+        type = with types; enum [ "hash" "regexp" "pcre" ];
+        default = "hash";
+        example = "regexp";
+        description = "The format the alias map should have. Use regexp if you want to use regular expressions.";
+      };
+
+      config = mkOption {
+        type = with types; attrsOf (oneOf [ bool str (listOf str) ]);
+        description = ''
+          The main.cf configuration file as key value set.
+        '';
+        example = {
+          mail_owner = "postfix";
+          smtp_tls_security_level = "may";
+        };
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "
+          Extra lines to be added verbatim to the main.cf configuration file.
+        ";
+      };
+
+      tlsTrustedAuthorities = mkOption {
+        type = types.str;
+        default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
+        defaultText = literalExpression ''"''${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"'';
+        description = ''
+          File containing trusted certification authorities (CA) to verify certificates of mailservers contacted for mail delivery. This basically sets smtp_tls_CAfile and enables opportunistic tls. Defaults to NixOS trusted certification authorities.
+        '';
+      };
+
+      sslCert = mkOption {
+        type = types.str;
+        default = "";
+        description = "SSL certificate to use.";
+      };
+
+      sslKey = mkOption {
+        type = types.str;
+        default = "";
+        description = "SSL key to use.";
+      };
+
+      recipientDelimiter = mkOption {
+        type = types.str;
+        default = "";
+        example = "+";
+        description = "
+          Delimiter for address extension: so mail to user+test can be handled by ~user/.forward+test
+        ";
+      };
+
+      canonical = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Entries for the <citerefentry><refentrytitle>canonical</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> table.
+        '';
+      };
+
+      virtual = mkOption {
+        type = types.lines;
+        default = "";
+        description = "
+          Entries for the virtual alias map, cf. man-page virtual(5).
+        ";
+      };
+
+      virtualMapType = mkOption {
+        type = types.enum ["hash" "regexp" "pcre"];
+        default = "hash";
+        description = ''
+          What type of virtual alias map file to use. Use <literal>"regexp"</literal> for regular expressions.
+        '';
+      };
+
+      localRecipients = mkOption {
+        type = with types; nullOr (listOf str);
+        default = null;
+        description = ''
+          List of accepted local users. Specify a bare username, an
+          <literal>"@domain.tld"</literal> wild-card, or a complete
+          <literal>"user@domain.tld"</literal> address. If set, these names end
+          up in the local recipient map -- see the local(8) man-page -- and
+          effectively replace the system user database lookup that's otherwise
+          used by default.
+        '';
+      };
+
+      transport = mkOption {
+        default = "";
+        type = types.lines;
+        description = "
+          Entries for the transport map, cf. man-page transport(8).
+        ";
+      };
+
+      dnsBlacklists = mkOption {
+        default = [];
+        type = with types; listOf str;
+        description = "dns blacklist servers to use with smtpd_client_restrictions";
+      };
+
+      dnsBlacklistOverrides = mkOption {
+        default = "";
+        type = types.lines;
+        description = "contents of check_client_access for overriding dnsBlacklists";
+      };
+
+      masterConfig = mkOption {
+        type = types.attrsOf (types.submodule masterCfOptions);
+        default = {};
+        example =
+          { submission = {
+              type = "inet";
+              args = [ "-o" "smtpd_tls_security_level=encrypt" ];
+            };
+          };
+        description = ''
+          An attribute set of service options, which correspond to the service
+          definitions usually done within the Postfix
+          <filename>master.cf</filename> file.
+        '';
+      };
+
+      extraMasterConf = mkOption {
+        type = types.lines;
+        default = "";
+        example = "submission inet n - n - - smtpd";
+        description = "Extra lines to append to the generated master.cf file.";
+      };
+
+      enableHeaderChecks = mkOption {
+        type = types.bool;
+        default = false;
+        example = true;
+        description = "Whether to enable postfix header checks";
+      };
+
+      headerChecks = mkOption {
+        type = types.listOf (types.submodule headerCheckOptions);
+        default = [];
+        example = [ { pattern = "/^X-Spam-Flag:/"; action = "REDIRECT spam@example.com"; } ];
+        description = "Postfix header checks.";
+      };
+
+      extraHeaderChecks = mkOption {
+        type = types.lines;
+        default = "";
+        example = "/^X-Spam-Flag:/ REDIRECT spam@example.com";
+        description = "Extra lines to /etc/postfix/header_checks file.";
+      };
+
+      aliasFiles = mkOption {
+        type = types.attrsOf types.path;
+        default = {};
+        description = "Aliases' tables to be compiled and placed into /var/lib/postfix/conf.";
+      };
+
+      mapFiles = mkOption {
+        type = types.attrsOf types.path;
+        default = {};
+        description = "Maps to be compiled and placed into /var/lib/postfix/conf.";
+      };
+
+      useSrs = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable sender rewriting scheme";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.postfix.enable (mkMerge [
+    {
+
+      environment = {
+        etc.postfix.source = "/var/lib/postfix/conf";
+
+        # This makes it comfortable to run 'postqueue/postdrop' for example.
+        systemPackages = [ pkgs.postfix ];
+      };
+
+      services.pfix-srsd.enable = config.services.postfix.useSrs;
+
+      services.mail.sendmailSetuidWrapper = mkIf config.services.postfix.setSendmail {
+        program = "sendmail";
+        source = "${pkgs.postfix}/bin/sendmail";
+        owner = "root";
+        group = setgidGroup;
+        setuid = false;
+        setgid = true;
+      };
+
+      security.wrappers.mailq = {
+        program = "mailq";
+        source = "${pkgs.postfix}/bin/mailq";
+        owner = "root";
+        group = setgidGroup;
+        setuid = false;
+        setgid = true;
+      };
+
+      security.wrappers.postqueue = {
+        program = "postqueue";
+        source = "${pkgs.postfix}/bin/postqueue";
+        owner = "root";
+        group = setgidGroup;
+        setuid = false;
+        setgid = true;
+      };
+
+      security.wrappers.postdrop = {
+        program = "postdrop";
+        source = "${pkgs.postfix}/bin/postdrop";
+        owner = "root";
+        group = setgidGroup;
+        setuid = false;
+        setgid = true;
+      };
+
+      users.users = optionalAttrs (user == "postfix")
+        { postfix = {
+            description = "Postfix mail server user";
+            uid = config.ids.uids.postfix;
+            group = group;
+          };
+        };
+
+      users.groups =
+        optionalAttrs (group == "postfix")
+        { ${group}.gid = config.ids.gids.postfix;
+        }
+        // optionalAttrs (setgidGroup == "postdrop")
+        { ${setgidGroup}.gid = config.ids.gids.postdrop;
+        };
+
+      systemd.services.postfix =
+        { description = "Postfix mail server";
+
+          wantedBy = [ "multi-user.target" ];
+          after = [ "network.target" ];
+          path = [ pkgs.postfix ];
+
+          serviceConfig = {
+            Type = "forking";
+            Restart = "always";
+            PIDFile = "/var/lib/postfix/queue/pid/master.pid";
+            ExecStart = "${pkgs.postfix}/bin/postfix start";
+            ExecStop = "${pkgs.postfix}/bin/postfix stop";
+            ExecReload = "${pkgs.postfix}/bin/postfix reload";
+          };
+
+          preStart = ''
+            # Backwards compatibility
+            if [ ! -d /var/lib/postfix ] && [ -d /var/postfix ]; then
+              mkdir -p /var/lib
+              mv /var/postfix /var/lib/postfix
+            fi
+
+            # All permissions set according ${pkgs.postfix}/etc/postfix/postfix-files script
+            mkdir -p /var/lib/postfix /var/lib/postfix/queue/{pid,public,maildrop}
+            chmod 0755 /var/lib/postfix
+            chown root:root /var/lib/postfix
+
+            rm -rf /var/lib/postfix/conf
+            mkdir -p /var/lib/postfix/conf
+            chmod 0755 /var/lib/postfix/conf
+            ln -sf ${pkgs.postfix}/etc/postfix/postfix-files /var/lib/postfix/conf/postfix-files
+            ln -sf ${mainCfFile} /var/lib/postfix/conf/main.cf
+            ln -sf ${masterCfFile} /var/lib/postfix/conf/master.cf
+
+            ${concatStringsSep "\n" (mapAttrsToList (to: from: ''
+              ln -sf ${from} /var/lib/postfix/conf/${to}
+              ${pkgs.postfix}/bin/postalias /var/lib/postfix/conf/${to}
+            '') cfg.aliasFiles)}
+            ${concatStringsSep "\n" (mapAttrsToList (to: from: ''
+              ln -sf ${from} /var/lib/postfix/conf/${to}
+              ${pkgs.postfix}/bin/postmap /var/lib/postfix/conf/${to}
+            '') cfg.mapFiles)}
+
+            mkdir -p /var/spool/mail
+            chown root:root /var/spool/mail
+            chmod a+rwxt /var/spool/mail
+            ln -sf /var/spool/mail /var/
+
+            #Finally delegate to postfix checking remain directories in /var/lib/postfix and set permissions on them
+            ${pkgs.postfix}/bin/postfix set-permissions config_directory=/var/lib/postfix/conf
+          '';
+        };
+
+      services.postfix.config = (mapAttrs (_: v: mkDefault v) {
+        compatibility_level  = pkgs.postfix.version;
+        mail_owner           = cfg.user;
+        default_privs        = "nobody";
+
+        # NixOS specific locations
+        data_directory       = "/var/lib/postfix/data";
+        queue_directory      = "/var/lib/postfix/queue";
+
+        # Default location of everything in package
+        meta_directory       = "${pkgs.postfix}/etc/postfix";
+        command_directory    = "${pkgs.postfix}/bin";
+        sample_directory     = "/etc/postfix";
+        newaliases_path      = "${pkgs.postfix}/bin/newaliases";
+        mailq_path           = "${pkgs.postfix}/bin/mailq";
+        readme_directory     = false;
+        sendmail_path        = "${pkgs.postfix}/bin/sendmail";
+        daemon_directory     = "${pkgs.postfix}/libexec/postfix";
+        manpage_directory    = "${pkgs.postfix}/share/man";
+        html_directory       = "${pkgs.postfix}/share/postfix/doc/html";
+        shlib_directory      = false;
+        mail_spool_directory = "/var/spool/mail/";
+        setgid_group         = cfg.setgidGroup;
+      })
+      // 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 (cfg.networks != null) { mynetworks = cfg.networks; }
+      // optionalAttrs (cfg.networksStyle != "") { mynetworks_style = cfg.networksStyle; }
+      // optionalAttrs (cfg.hostname != "") { myhostname = cfg.hostname; }
+      // optionalAttrs (cfg.domain != "") { mydomain = cfg.domain; }
+      // optionalAttrs (cfg.origin != "") { myorigin =  cfg.origin; }
+      // optionalAttrs (cfg.destination != null) { mydestination = cfg.destination; }
+      // optionalAttrs (cfg.relayDomains != null) { relay_domains = cfg.relayDomains; }
+      // optionalAttrs (cfg.recipientDelimiter != "") { recipient_delimiter = cfg.recipientDelimiter; }
+      // optionalAttrs haveAliases { alias_maps = [ "${cfg.aliasMapType}:/etc/postfix/aliases" ]; }
+      // optionalAttrs haveTransport { transport_maps = [ "hash:/etc/postfix/transport" ]; }
+      // optionalAttrs haveVirtual { virtual_alias_maps = [ "${cfg.virtualMapType}:/etc/postfix/virtual" ]; }
+      // optionalAttrs haveLocalRecipients { local_recipient_maps = [ "hash:/etc/postfix/local_recipients" ] ++ optional haveAliases "$alias_maps"; }
+      // optionalAttrs (cfg.dnsBlacklists != []) { smtpd_client_restrictions = clientRestrictions; }
+      // optionalAttrs cfg.useSrs {
+        sender_canonical_maps = [ "tcp:127.0.0.1:10001" ];
+        sender_canonical_classes = [ "envelope_sender" ];
+        recipient_canonical_maps = [ "tcp:127.0.0.1:10002" ];
+        recipient_canonical_classes = [ "envelope_recipient" ];
+      }
+      // optionalAttrs cfg.enableHeaderChecks { header_checks = [ "regexp:/etc/postfix/header_checks" ]; }
+      // optionalAttrs (cfg.tlsTrustedAuthorities != "") {
+        smtp_tls_CAfile = cfg.tlsTrustedAuthorities;
+        smtp_tls_security_level = mkDefault "may";
+      }
+      // optionalAttrs (cfg.sslCert != "") {
+        smtp_tls_cert_file = cfg.sslCert;
+        smtp_tls_key_file = cfg.sslKey;
+
+        smtp_tls_security_level = mkDefault "may";
+
+        smtpd_tls_cert_file = cfg.sslCert;
+        smtpd_tls_key_file = cfg.sslKey;
+
+        smtpd_tls_security_level = "may";
+      };
+
+      services.postfix.masterConfig = {
+        pickup = {
+          private = false;
+          wakeup = 60;
+          maxproc = 1;
+        };
+        cleanup = {
+          private = false;
+          maxproc = 0;
+        };
+        qmgr = {
+          private = false;
+          wakeup = 300;
+          maxproc = 1;
+        };
+        tlsmgr = {
+          wakeup = 1000;
+          wakeupUnusedComponent = false;
+          maxproc = 1;
+        };
+        rewrite = {
+          command = "trivial-rewrite";
+        };
+        bounce = {
+          maxproc = 0;
+        };
+        defer = {
+          maxproc = 0;
+          command = "bounce";
+        };
+        trace = {
+          maxproc = 0;
+          command = "bounce";
+        };
+        verify = {
+          maxproc = 1;
+        };
+        flush = {
+          private = false;
+          wakeup = 1000;
+          wakeupUnusedComponent = false;
+          maxproc = 0;
+        };
+        proxymap = {
+          command = "proxymap";
+        };
+        proxywrite = {
+          maxproc = 1;
+          command = "proxymap";
+        };
+        showq = {
+          private = false;
+        };
+        error = {};
+        retry = {
+          command = "error";
+        };
+        discard = {};
+        local = {
+          privileged = true;
+        };
+        virtual = {
+          privileged = true;
+        };
+        lmtp = {
+        };
+        anvil = {
+          maxproc = 1;
+        };
+        scache = {
+          maxproc = 1;
+        };
+      } // optionalAttrs cfg.enableSubmission {
+        submission = {
+          type = "inet";
+          private = false;
+          command = "smtpd";
+          args = let
+            mkKeyVal = opt: val: [ "-o" (opt + "=" + val) ];
+          in concatLists (mapAttrsToList mkKeyVal cfg.submissionOptions);
+        };
+      } // optionalAttrs cfg.enableSmtp {
+        smtp_inet = {
+          name = "smtp";
+          type = "inet";
+          private = false;
+          command = "smtpd";
+        };
+        smtp = {};
+        relay = {
+          command = "smtp";
+          args = [ "-o" "smtp_fallback_relay=" ];
+        };
+      } // optionalAttrs cfg.enableSubmissions {
+        submissions = {
+          type = "inet";
+          private = false;
+          command = "smtpd";
+          args = let
+            mkKeyVal = opt: val: [ "-o" (opt + "=" + val) ];
+            adjustSmtpTlsSecurityLevel = !(cfg.submissionsOptions ? smtpd_tls_security_level) ||
+                                      cfg.submissionsOptions.smtpd_tls_security_level == "none" ||
+                                      cfg.submissionsOptions.smtpd_tls_security_level == "may";
+            submissionsOptions = cfg.submissionsOptions // {
+              smtpd_tls_wrappermode = "yes";
+            } // optionalAttrs adjustSmtpTlsSecurityLevel {
+              smtpd_tls_security_level = "encrypt";
+            };
+          in concatLists (mapAttrsToList mkKeyVal submissionsOptions);
+        };
+      };
+    }
+
+    (mkIf haveAliases {
+      services.postfix.aliasFiles.aliases = aliasesFile;
+    })
+    (mkIf haveCanonical {
+      services.postfix.mapFiles.canonical = canonicalFile;
+    })
+    (mkIf haveTransport {
+      services.postfix.mapFiles.transport = transportFile;
+    })
+    (mkIf haveVirtual {
+      services.postfix.mapFiles.virtual = virtualFile;
+    })
+    (mkIf haveLocalRecipients {
+      services.postfix.mapFiles.local_recipients = localRecipientMapFile;
+    })
+    (mkIf cfg.enableHeaderChecks {
+      services.postfix.mapFiles.header_checks = headerChecksFile;
+    })
+    (mkIf (cfg.dnsBlacklists != []) {
+      services.postfix.mapFiles.client_access = checkClientAccessFile;
+    })
+  ]);
+
+  imports = [
+   (mkRemovedOptionModule [ "services" "postfix" "sslCACert" ]
+     "services.postfix.sslCACert was replaced by services.postfix.tlsTrustedAuthorities. In case you intend that your server should validate requested client certificates use services.postfix.extraConfig.")
+
+   (mkChangedOptionModule [ "services" "postfix" "useDane" ]
+     [ "services" "postfix" "config" "smtp_tls_security_level" ]
+     (config: mkIf config.services.postfix.useDane "dane"))
+  ];
+}
diff --git a/nixos/modules/services/mail/postfixadmin.nix b/nixos/modules/services/mail/postfixadmin.nix
new file mode 100644
index 00000000000..a0846ad5290
--- /dev/null
+++ b/nixos/modules/services/mail/postfixadmin.nix
@@ -0,0 +1,199 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.postfixadmin;
+  fpm = config.services.phpfpm.pools.postfixadmin;
+  localDB = cfg.database.host == "localhost";
+  user = if localDB then cfg.database.username else "nginx";
+in
+{
+  options.services.postfixadmin = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable postfixadmin.
+
+        Also enables nginx virtual host management.
+        Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.&lt;name&gt;</literal>.
+        See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
+      '';
+    };
+
+    hostName = mkOption {
+      type = types.str;
+      example = "postfixadmin.example.com";
+      description = "Hostname to use for the nginx vhost";
+    };
+
+    adminEmail = mkOption {
+      type = types.str;
+      example = "postmaster@example.com";
+      description = ''
+        Defines the Site Admin's email address.
+        This will be used to send emails from to create mailboxes and
+        from Send Email / Broadcast message pages.
+      '';
+    };
+
+    setupPasswordFile = mkOption {
+      type = types.path;
+      description = ''
+        Password file for the admin.
+        Generate with <literal>php -r "echo password_hash('some password here', PASSWORD_DEFAULT);"</literal>
+      '';
+    };
+
+    database = {
+      username = mkOption {
+        type = types.str;
+        default = "postfixadmin";
+        description = ''
+          Username for the postgresql connection.
+          If <literal>database.host</literal> is set to <literal>localhost</literal>, a unix user and group of the same name will be created as well.
+        '';
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = ''
+          Host of the postgresql server. If this is not set to
+          <literal>localhost</literal>, you have to create the
+          postgresql user and database yourself, with appropriate
+          permissions.
+        '';
+      };
+      passwordFile = mkOption {
+        type = types.path;
+        description = "Password file for the postgresql connection. Must be readable by user <literal>nginx</literal>.";
+      };
+      dbname = mkOption {
+        type = types.str;
+        default = "postfixadmin";
+        description = "Name of the postgresql database";
+      };
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = "Extra configuration for the postfixadmin instance, see postfixadmin's config.inc.php for available options.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."postfixadmin/config.local.php".text = ''
+      <?php
+
+      $CONF['setup_password'] = file_get_contents('${cfg.setupPasswordFile}');
+
+      $CONF['database_type'] = 'pgsql';
+      $CONF['database_host'] = ${if localDB then "null" else "'${cfg.database.host}'"};
+      ${optionalString localDB "$CONF['database_user'] = '${cfg.database.username}';"}
+      $CONF['database_password'] = ${if localDB then "'dummy'" else "file_get_contents('${cfg.database.passwordFile}')"};
+      $CONF['database_name'] = '${cfg.database.dbname}';
+      $CONF['configured'] = true;
+
+      ${cfg.extraConfig}
+    '';
+
+    systemd.tmpfiles.rules = [ "d /var/cache/postfixadmin/templates_c 700 ${user} ${user}" ];
+
+    services.nginx = {
+      enable = true;
+      virtualHosts = {
+        ${cfg.hostName} = {
+          forceSSL = mkDefault true;
+          enableACME = mkDefault true;
+          locations."/" = {
+            root = "${pkgs.postfixadmin}/public";
+            index = "index.php";
+            extraConfig = ''
+              location ~* \.php$ {
+                fastcgi_split_path_info ^(.+\.php)(/.+)$;
+                fastcgi_pass unix:${fpm.socket};
+                include ${config.services.nginx.package}/conf/fastcgi_params;
+                include ${pkgs.nginx}/conf/fastcgi.conf;
+              }
+            '';
+          };
+        };
+      };
+    };
+
+    services.postgresql = mkIf localDB {
+      enable = true;
+      ensureUsers = [ {
+        name = cfg.database.username;
+      } ];
+    };
+    # The postgresql module doesn't currently support concepts like
+    # objects owners and extensions; for now we tack on what's needed
+    # here.
+    systemd.services.postfixadmin-postgres = let pgsql = config.services.postgresql; in mkIf localDB {
+      after = [ "postgresql.service" ];
+      bindsTo = [ "postgresql.service" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [
+        pgsql.package
+        pkgs.util-linux
+      ];
+      script = ''
+        set -eu
+
+        PSQL() {
+            psql --port=${toString pgsql.port} "$@"
+        }
+
+        PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.database.dbname}'" | grep -q 1 || PSQL -tAc 'CREATE DATABASE "${cfg.database.dbname}" OWNER "${cfg.database.username}"'
+        current_owner=$(PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.database.dbname}'")
+        if [[ "$current_owner" != "${cfg.database.username}" ]]; then
+            PSQL -tAc 'ALTER DATABASE "${cfg.database.dbname}" OWNER TO "${cfg.database.username}"'
+            if [[ -e "${config.services.postgresql.dataDir}/.reassigning_${cfg.database.dbname}" ]]; then
+                echo "Reassigning ownership of database ${cfg.database.dbname} to user ${cfg.database.username} failed on last boot. Failing..."
+                exit 1
+            fi
+            touch "${config.services.postgresql.dataDir}/.reassigning_${cfg.database.dbname}"
+            PSQL "${cfg.database.dbname}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.database.username}\""
+            rm "${config.services.postgresql.dataDir}/.reassigning_${cfg.database.dbname}"
+        fi
+      '';
+
+      serviceConfig = {
+        User = pgsql.superUser;
+        Type = "oneshot";
+        RemainAfterExit = true;
+      };
+    };
+
+    users.users.${user} = mkIf localDB {
+      group = user;
+      isSystemUser = true;
+      createHome = false;
+    };
+    users.groups.${user} = mkIf localDB {};
+
+    services.phpfpm.pools.postfixadmin = {
+      user = user;
+      phpPackage = pkgs.php74;
+      phpOptions = ''
+        error_log = 'stderr'
+        log_errors = on
+      '';
+      settings = mapAttrs (name: mkDefault) {
+        "listen.owner" = "nginx";
+        "listen.group" = "nginx";
+        "listen.mode" = "0660";
+        "pm" = "dynamic";
+        "pm.max_children" = 75;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 1;
+        "pm.max_spare_servers" = 20;
+        "pm.max_requests" = 500;
+        "catch_workers_output" = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/mail/postgrey.nix b/nixos/modules/services/mail/postgrey.nix
new file mode 100644
index 00000000000..7c206e3725e
--- /dev/null
+++ b/nixos/modules/services/mail/postgrey.nix
@@ -0,0 +1,205 @@
+{ config, lib, pkgs, ... }:
+
+with lib; let
+
+  cfg = config.services.postgrey;
+
+  natural = with types; addCheck int (x: x >= 0);
+  natural' = with types; addCheck int (x: x > 0);
+
+  socket = with types; addCheck (either (submodule unixSocket) (submodule inetSocket)) (x: x ? path || x ? port);
+
+  inetSocket = with types; {
+    options = {
+      addr = mkOption {
+        type = nullOr str;
+        default = null;
+        example = "127.0.0.1";
+        description = "The address to bind to. Localhost if null";
+      };
+      port = mkOption {
+        type = natural';
+        default = 10030;
+        description = "Tcp port to bind to";
+      };
+    };
+  };
+
+  unixSocket = with types; {
+    options = {
+      path = mkOption {
+        type = path;
+        default = "/run/postgrey.sock";
+        description = "Path of the unix socket";
+      };
+
+      mode = mkOption {
+        type = str;
+        default = "0777";
+        description = "Mode of the unix socket";
+      };
+    };
+  };
+
+in {
+  imports = [
+    (mkMergedOptionModule [ [ "services" "postgrey" "inetAddr" ] [ "services" "postgrey" "inetPort" ] ] [ "services" "postgrey" "socket" ] (config: let
+        value = p: getAttrFromPath p config;
+        inetAddr = [ "services" "postgrey" "inetAddr" ];
+        inetPort = [ "services" "postgrey" "inetPort" ];
+      in
+        if value inetAddr == null
+        then { path = "/run/postgrey.sock"; }
+        else { addr = value inetAddr; port = value inetPort; }
+    ))
+  ];
+
+  options = {
+    services.postgrey = with types; {
+      enable = mkOption {
+        type = bool;
+        default = false;
+        description = "Whether to run the Postgrey daemon";
+      };
+      socket = mkOption {
+        type = socket;
+        default = {
+          path = "/run/postgrey.sock";
+          mode = "0777";
+        };
+        example = {
+          addr = "127.0.0.1";
+          port = 10030;
+        };
+        description = "Socket to bind to";
+      };
+      greylistText = mkOption {
+        type = str;
+        default = "Greylisted for %%s seconds";
+        description = "Response status text for greylisted messages; use %%s for seconds left until greylisting is over and %%r for mail domain of recipient";
+      };
+      greylistAction = mkOption {
+        type = str;
+        default = "DEFER_IF_PERMIT";
+        description = "Response status for greylisted messages (see access(5))";
+      };
+      greylistHeader = mkOption {
+        type = str;
+        default = "X-Greylist: delayed %%t seconds by postgrey-%%v at %%h; %%d";
+        description = "Prepend header to greylisted mails; use %%t for seconds delayed due to greylisting, %%v for the version of postgrey, %%d for the date, and %%h for the host";
+      };
+      delay = mkOption {
+        type = natural;
+        default = 300;
+        description = "Greylist for N seconds";
+      };
+      maxAge = mkOption {
+        type = natural;
+        default = 35;
+        description = "Delete entries from whitelist if they haven't been seen for N days";
+      };
+      retryWindow = mkOption {
+        type = either str natural;
+        default = 2;
+        example = "12h";
+        description = "Allow N days for the first retry. Use string with appended 'h' to specify time in hours";
+      };
+      lookupBySubnet = mkOption {
+        type = bool;
+        default = true;
+        description = "Strip the last N bits from IP addresses, determined by IPv4CIDR and IPv6CIDR";
+      };
+      IPv4CIDR = mkOption {
+        type = natural;
+        default = 24;
+        description = "Strip N bits from IPv4 addresses if lookupBySubnet is true";
+      };
+      IPv6CIDR = mkOption {
+        type = natural;
+        default = 64;
+        description = "Strip N bits from IPv6 addresses if lookupBySubnet is true";
+      };
+      privacy = mkOption {
+        type = bool;
+        default = true;
+        description = "Store data using one-way hash functions (SHA1)";
+      };
+      autoWhitelist = mkOption {
+        type = nullOr natural';
+        default = 5;
+        description = "Whitelist clients after successful delivery of N messages";
+      };
+      whitelistClients = mkOption {
+        type = listOf path;
+        default = [];
+        description = "Client address whitelist files (see postgrey(8))";
+      };
+      whitelistRecipients = mkOption {
+        type = listOf path;
+        default = [];
+        description = "Recipient address whitelist files (see postgrey(8))";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.postgrey ];
+
+    users = {
+      users = {
+        postgrey = {
+          description = "Postgrey Daemon";
+          uid = config.ids.uids.postgrey;
+          group = "postgrey";
+        };
+      };
+      groups = {
+        postgrey = {
+          gid = config.ids.gids.postgrey;
+        };
+      };
+    };
+
+    systemd.services.postgrey = let
+      bind-flag = if cfg.socket ? path then
+        "--unix=${cfg.socket.path} --socketmode=${cfg.socket.mode}"
+      else
+        ''--inet=${optionalString (cfg.socket.addr != null) (cfg.socket.addr + ":")}${toString cfg.socket.port}'';
+    in {
+      description = "Postfix Greylisting Service";
+      wantedBy = [ "multi-user.target" ];
+      before = [ "postfix.service" ];
+      preStart = ''
+        mkdir -p /var/postgrey
+        chown postgrey:postgrey /var/postgrey
+        chmod 0770 /var/postgrey
+      '';
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = ''${pkgs.postgrey}/bin/postgrey \
+          ${bind-flag} \
+          --group=postgrey --user=postgrey \
+          --dbdir=/var/postgrey \
+          --delay=${toString cfg.delay} \
+          --max-age=${toString cfg.maxAge} \
+          --retry-window=${toString cfg.retryWindow} \
+          ${if cfg.lookupBySubnet then "--lookup-by-subnet" else "--lookup-by-host"} \
+          --ipv4cidr=${toString cfg.IPv4CIDR} --ipv6cidr=${toString cfg.IPv6CIDR} \
+          ${optionalString cfg.privacy "--privacy"} \
+          --auto-whitelist-clients=${toString (if cfg.autoWhitelist == null then 0 else cfg.autoWhitelist)} \
+          --greylist-action=${cfg.greylistAction} \
+          --greylist-text="${cfg.greylistText}" \
+          --x-greylist-header="${cfg.greylistHeader}" \
+          ${concatMapStringsSep " " (x: "--whitelist-clients=" + x) cfg.whitelistClients} \
+          ${concatMapStringsSep " " (x: "--whitelist-recipients=" + x) cfg.whitelistRecipients}
+        '';
+        Restart = "always";
+        RestartSec = 5;
+        TimeoutSec = 10;
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/mail/postsrsd.nix b/nixos/modules/services/mail/postsrsd.nix
new file mode 100644
index 00000000000..2ebc675ab10
--- /dev/null
+++ b/nixos/modules/services/mail/postsrsd.nix
@@ -0,0 +1,135 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.postsrsd;
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.postsrsd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the postsrsd SRS server for Postfix.";
+      };
+
+      secretsFile = mkOption {
+        type = types.path;
+        default = "/var/lib/postsrsd/postsrsd.secret";
+        description = "Secret keys used for signing and verification";
+      };
+
+      domain = mkOption {
+        type = types.str;
+        description = "Domain name for rewrite";
+      };
+
+      separator = mkOption {
+        type = types.enum ["-" "=" "+"];
+        default = "=";
+        description = "First separator character in generated addresses";
+      };
+
+      # bindAddress = mkOption { # uncomment once 1.5 is released
+      #   type = types.str;
+      #   default = "127.0.0.1";
+      #   description = "Socket listen address";
+      # };
+
+      forwardPort = mkOption {
+        type = types.int;
+        default = 10001;
+        description = "Port for the forward SRS lookup";
+      };
+
+      reversePort = mkOption {
+        type = types.int;
+        default = 10002;
+        description = "Port for the reverse SRS lookup";
+      };
+
+      timeout = mkOption {
+        type = types.int;
+        default = 1800;
+        description = "Timeout for idle client connections in seconds";
+      };
+
+      excludeDomains = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Origin domains to exclude from rewriting in addition to primary domain";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "postsrsd";
+        description = "User for the daemon";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "postsrsd";
+        description = "Group for the daemon";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.postsrsd.domain = mkDefault config.networking.hostName;
+
+    users.users = optionalAttrs (cfg.user == "postsrsd") {
+      postsrsd = {
+        group = cfg.group;
+        uid = config.ids.uids.postsrsd;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "postsrsd") {
+      postsrsd.gid = config.ids.gids.postsrsd;
+    };
+
+    systemd.services.postsrsd = {
+      description = "PostSRSd SRS rewriting server";
+      after = [ "network.target" ];
+      before = [ "postfix.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      path = [ pkgs.coreutils ];
+
+      serviceConfig = {
+        ExecStart = ''${pkgs.postsrsd}/sbin/postsrsd "-s${cfg.secretsFile}" "-d${cfg.domain}" -a${cfg.separator} -f${toString cfg.forwardPort} -r${toString cfg.reversePort} -t${toString cfg.timeout} "-X${concatStringsSep "," cfg.excludeDomains}"'';
+        User = cfg.user;
+        Group = cfg.group;
+        PermissionsStartOnly = true;
+      };
+
+      preStart = ''
+        if [ ! -e "${cfg.secretsFile}" ]; then
+          echo "WARNING: secrets file not found, autogenerating!"
+          DIR="$(dirname "${cfg.secretsFile}")"
+          if [ ! -d "$DIR" ]; then
+            mkdir -p -m750 "$DIR"
+            chown "${cfg.user}:${cfg.group}" "$DIR"
+          fi
+          dd if=/dev/random bs=18 count=1 | base64 > "${cfg.secretsFile}"
+          chmod 600 "${cfg.secretsFile}"
+        fi
+        chown "${cfg.user}:${cfg.group}" "${cfg.secretsFile}"
+      '';
+    };
+
+  };
+}
diff --git a/nixos/modules/services/mail/roundcube.nix b/nixos/modules/services/mail/roundcube.nix
new file mode 100644
index 00000000000..1dd393da882
--- /dev/null
+++ b/nixos/modules/services/mail/roundcube.nix
@@ -0,0 +1,249 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.roundcube;
+  fpm = config.services.phpfpm.pools.roundcube;
+  localDB = cfg.database.host == "localhost";
+  user = cfg.database.username;
+  phpWithPspell = pkgs.php80.withExtensions ({ enabled, all }: [ all.pspell ] ++ enabled);
+in
+{
+  options.services.roundcube = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable roundcube.
+
+        Also enables nginx virtual host management.
+        Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.&lt;name&gt;</literal>.
+        See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
+      '';
+    };
+
+    hostName = mkOption {
+      type = types.str;
+      example = "webmail.example.com";
+      description = "Hostname to use for the nginx vhost";
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.roundcube;
+      defaultText = literalExpression "pkgs.roundcube";
+
+      example = literalExpression ''
+        roundcube.withPlugins (plugins: [ plugins.persistent_login ])
+      '';
+
+      description = ''
+        The package which contains roundcube's sources. Can be overriden to create
+        an environment which contains roundcube and third-party plugins.
+      '';
+    };
+
+    database = {
+      username = mkOption {
+        type = types.str;
+        default = "roundcube";
+        description = ''
+          Username for the postgresql connection.
+          If <literal>database.host</literal> is set to <literal>localhost</literal>, a unix user and group of the same name will be created as well.
+        '';
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = ''
+          Host of the postgresql server. If this is not set to
+          <literal>localhost</literal>, you have to create the
+          postgresql user and database yourself, with appropriate
+          permissions.
+        '';
+      };
+      password = mkOption {
+        type = types.str;
+        description = "Password for the postgresql connection. Do not use: the password will be stored world readable in the store; use <literal>passwordFile</literal> instead.";
+        default = "";
+      };
+      passwordFile = mkOption {
+        type = types.str;
+        description = "Password file for the postgresql connection. Must be readable by user <literal>nginx</literal>. Ignored if <literal>database.host</literal> is set to <literal>localhost</literal>, as peer authentication will be used.";
+      };
+      dbname = mkOption {
+        type = types.str;
+        default = "roundcube";
+        description = "Name of the postgresql database";
+      };
+    };
+
+    plugins = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        List of roundcube plugins to enable. Currently, only those directly shipped with Roundcube are supported.
+      '';
+    };
+
+    dicts = mkOption {
+      type = types.listOf types.package;
+      default = [];
+      example = literalExpression "with pkgs.aspellDicts; [ en fr de ]";
+      description = ''
+        List of aspell dictionnaries for spell checking. If empty, spell checking is disabled.
+      '';
+    };
+
+    maxAttachmentSize = mkOption {
+      type = types.int;
+      default = 18;
+      description = ''
+        The maximum attachment size in MB.
+
+        Note: Since roundcube only uses 70% of max upload values configured in php
+        30% is added automatically to <xref linkend="opt-services.roundcube.maxAttachmentSize"/>.
+      '';
+      apply = configuredMaxAttachmentSize: "${toString (configuredMaxAttachmentSize * 1.3)}M";
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = "Extra configuration for roundcube webmail instance";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # backward compatibility: if password is set but not passwordFile, make one.
+    services.roundcube.database.passwordFile = mkIf (!localDB && cfg.database.password != "") (mkDefault ("${pkgs.writeText "roundcube-password" cfg.database.password}"));
+    warnings = lib.optional (!localDB && cfg.database.password != "") "services.roundcube.database.password is deprecated and insecure; use services.roundcube.database.passwordFile instead";
+
+    environment.etc."roundcube/config.inc.php".text = ''
+      <?php
+
+      ${lib.optionalString (!localDB) "$password = file_get_contents('${cfg.database.passwordFile}');"}
+
+      $config = array();
+      $config['db_dsnw'] = 'pgsql://${cfg.database.username}${lib.optionalString (!localDB) ":' . $password . '"}@${if localDB then "unix(/run/postgresql)" else cfg.database.host}/${cfg.database.dbname}';
+      $config['log_driver'] = 'syslog';
+      $config['max_message_size'] =  '${cfg.maxAttachmentSize}';
+      $config['plugins'] = [${concatMapStringsSep "," (p: "'${p}'") cfg.plugins}];
+      $config['des_key'] = file_get_contents('/var/lib/roundcube/des_key');
+      $config['mime_types'] = '${pkgs.nginx}/conf/mime.types';
+      $config['enable_spellcheck'] = ${if cfg.dicts == [] then "false" else "true"};
+      # by default, spellchecking uses a third-party cloud services
+      $config['spellcheck_engine'] = 'pspell';
+      $config['spellcheck_languages'] = array(${lib.concatMapStringsSep ", " (dict: let p = builtins.parseDrvName dict.shortName; in "'${p.name}' => '${dict.fullName}'") cfg.dicts});
+
+      ${cfg.extraConfig}
+    '';
+
+    services.nginx = {
+      enable = true;
+      virtualHosts = {
+        ${cfg.hostName} = {
+          forceSSL = mkDefault true;
+          enableACME = mkDefault true;
+          locations."/" = {
+            root = cfg.package;
+            index = "index.php";
+            extraConfig = ''
+              location ~* \.php$ {
+                fastcgi_split_path_info ^(.+\.php)(/.+)$;
+                fastcgi_pass unix:${fpm.socket};
+                include ${config.services.nginx.package}/conf/fastcgi_params;
+                include ${pkgs.nginx}/conf/fastcgi.conf;
+              }
+            '';
+          };
+        };
+      };
+    };
+
+    services.postgresql = mkIf localDB {
+      enable = true;
+      ensureDatabases = [ cfg.database.dbname ];
+      ensureUsers = [ {
+        name = cfg.database.username;
+        ensurePermissions = {
+          "DATABASE ${cfg.database.username}" = "ALL PRIVILEGES";
+        };
+      } ];
+    };
+
+    users.users.${user} = mkIf localDB {
+      group = user;
+      isSystemUser = true;
+      createHome = false;
+    };
+    users.groups.${user} = mkIf localDB {};
+
+    services.phpfpm.pools.roundcube = {
+      user = if localDB then user else "nginx";
+      phpOptions = ''
+        error_log = 'stderr'
+        log_errors = on
+        post_max_size = ${cfg.maxAttachmentSize}
+        upload_max_filesize = ${cfg.maxAttachmentSize}
+      '';
+      settings = mapAttrs (name: mkDefault) {
+        "listen.owner" = "nginx";
+        "listen.group" = "nginx";
+        "listen.mode" = "0660";
+        "pm" = "dynamic";
+        "pm.max_children" = 75;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 1;
+        "pm.max_spare_servers" = 20;
+        "pm.max_requests" = 500;
+        "catch_workers_output" = true;
+      };
+      phpPackage = phpWithPspell;
+      phpEnv.ASPELL_CONF = "dict-dir ${pkgs.aspellWithDicts (_: cfg.dicts)}/lib/aspell";
+    };
+    systemd.services.phpfpm-roundcube.after = [ "roundcube-setup.service" ];
+
+    # Restart on config changes.
+    systemd.services.phpfpm-roundcube.restartTriggers = [
+      config.environment.etc."roundcube/config.inc.php".source
+    ];
+
+    systemd.services.roundcube-setup = mkMerge [
+      (mkIf (cfg.database.host == "localhost") {
+        requires = [ "postgresql.service" ];
+        after = [ "postgresql.service" ];
+        path = [ config.services.postgresql.package ];
+      })
+      {
+        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}";
+        in
+        ''
+          version="$(${psql} -t <<< "select value from system where name = 'roundcube-version';" || true)"
+          if ! (grep -E '[a-zA-Z0-9]' <<< "$version"); then
+            ${psql} -f ${cfg.package}/SQL/postgres.initial.sql
+          fi
+
+          if [ ! -f /var/lib/roundcube/des_key ]; then
+            base64 /dev/urandom | head -c 24 > /var/lib/roundcube/des_key;
+            # we need to log out everyone in case change the des_key
+            # from the default when upgrading from nixos 19.09
+            ${psql} <<< 'TRUNCATE TABLE session;'
+          fi
+
+          ${phpWithPspell}/bin/php ${cfg.package}/bin/update.sh
+        '';
+        serviceConfig = {
+          Type = "oneshot";
+          StateDirectory = "roundcube";
+          User = if localDB then user else "nginx";
+          # so that the des_key is not world readable
+          StateDirectoryMode = "0700";
+        };
+      }
+    ];
+  };
+}
diff --git a/nixos/modules/services/mail/rspamd.nix b/nixos/modules/services/mail/rspamd.nix
new file mode 100644
index 00000000000..a570e137a55
--- /dev/null
+++ b/nixos/modules/services/mail/rspamd.nix
@@ -0,0 +1,446 @@
+{ config, options, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.rspamd;
+  opt = options.services.rspamd;
+  postfixCfg = config.services.postfix;
+
+  bindSocketOpts = {options, config, ... }: {
+    options = {
+      socket = mkOption {
+        type = types.str;
+        example = "localhost:11333";
+        description = ''
+          Socket for this worker to listen on in a format acceptable by rspamd.
+        '';
+      };
+      mode = mkOption {
+        type = types.str;
+        default = "0644";
+        description = "Mode to set on unix socket";
+      };
+      owner = mkOption {
+        type = types.str;
+        default = "${cfg.user}";
+        description = "Owner to set on unix socket";
+      };
+      group = mkOption {
+        type = types.str;
+        default = "${cfg.group}";
+        description = "Group to set on unix socket";
+      };
+      rawEntry = mkOption {
+        type = types.str;
+        internal = true;
+      };
+    };
+    config.rawEntry = let
+      maybeOption = option:
+        optionalString options.${option}.isDefined " ${option}=${config.${option}}";
+    in
+      if (!(hasPrefix "/" config.socket)) then "${config.socket}"
+      else "${config.socket}${maybeOption "mode"}${maybeOption "owner"}${maybeOption "group"}";
+  };
+
+  traceWarning = w: x: builtins.trace "warning: ${w}" x;
+
+  workerOpts = { name, options, ... }: {
+    options = {
+      enable = mkOption {
+        type = types.nullOr types.bool;
+        default = null;
+        description = "Whether to run the rspamd worker.";
+      };
+      name = mkOption {
+        type = types.nullOr types.str;
+        default = name;
+        description = "Name of the worker";
+      };
+      type = mkOption {
+        type = types.nullOr (types.enum [
+          "normal" "controller" "fuzzy" "rspamd_proxy" "lua" "proxy"
+        ]);
+        description = ''
+          The type of this worker. The type <literal>proxy</literal> is
+          deprecated and only kept for backwards compatibility and should be
+          replaced with <literal>rspamd_proxy</literal>.
+        '';
+        apply = let
+            from = "services.rspamd.workers.\"${name}\".type";
+            files = options.type.files;
+            warning = "The option `${from}` defined in ${showFiles files} has enum value `proxy` which has been renamed to `rspamd_proxy`";
+          in x: if x == "proxy" then traceWarning warning "rspamd_proxy" else x;
+      };
+      bindSockets = mkOption {
+        type = types.listOf (types.either types.str (types.submodule bindSocketOpts));
+        default = [];
+        description = ''
+          List of sockets to listen, in format acceptable by rspamd
+        '';
+        example = [{
+          socket = "/run/rspamd.sock";
+          mode = "0666";
+          owner = "rspamd";
+        } "*:11333"];
+        apply = value: map (each: if (isString each)
+          then if (isUnixSocket each)
+            then {socket = each; owner = cfg.user; group = cfg.group; mode = "0644"; rawEntry = "${each}";}
+            else {socket = each; rawEntry = "${each}";}
+          else each) value;
+      };
+      count = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          Number of worker instances to run
+        '';
+      };
+      includes = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          List of files to include in configuration
+        '';
+      };
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Additional entries to put verbatim into worker section of rspamd config file.";
+      };
+    };
+    config = mkIf (name == "normal" || name == "controller" || name == "fuzzy" || name == "rspamd_proxy") {
+      type = mkDefault name;
+      includes = mkDefault [ "$CONFDIR/worker-${if name == "rspamd_proxy" then "proxy" else name}.inc" ];
+      bindSockets =
+        let
+          unixSocket = name: {
+            mode = "0660";
+            socket = "/run/rspamd/${name}.sock";
+            owner = cfg.user;
+            group = cfg.group;
+          };
+        in mkDefault (if name == "normal" then [(unixSocket "rspamd")]
+          else if name == "controller" then [ "localhost:11334" ]
+          else if name == "rspamd_proxy" then [ (unixSocket "proxy") ]
+          else [] );
+    };
+  };
+
+  isUnixSocket = socket: hasPrefix "/" (if (isString socket) then socket else socket.socket);
+
+  mkBindSockets = enabled: socks: concatStringsSep "\n  "
+    (flatten (map (each: "bind_socket = \"${each.rawEntry}\";") socks));
+
+  rspamdConfFile = pkgs.writeText "rspamd.conf"
+    ''
+      .include "$CONFDIR/common.conf"
+
+      options {
+        pidfile = "$RUNDIR/rspamd.pid";
+        .include "$CONFDIR/options.inc"
+        .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/options.inc"
+        .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/options.inc"
+      }
+
+      logging {
+        type = "syslog";
+        .include "$CONFDIR/logging.inc"
+        .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/logging.inc"
+        .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/logging.inc"
+      }
+
+      ${concatStringsSep "\n" (mapAttrsToList (name: value: let
+          includeName = if name == "rspamd_proxy" then "proxy" else name;
+          tryOverride = boolToString (value.extraConfig == "");
+        in ''
+        worker "${value.type}" {
+          type = "${value.type}";
+          ${optionalString (value.enable != null)
+            "enabled = ${if value.enable != false then "yes" else "no"};"}
+          ${mkBindSockets value.enable value.bindSockets}
+          ${optionalString (value.count != null) "count = ${toString value.count};"}
+          ${concatStringsSep "\n  " (map (each: ".include \"${each}\"") value.includes)}
+          .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/worker-${includeName}.inc"
+          .include(try=${tryOverride}; priority=10) "$LOCAL_CONFDIR/override.d/worker-${includeName}.inc"
+        }
+      '') cfg.workers)}
+
+      ${optionalString (cfg.extraConfig != "") ''
+        .include(priority=10) "$LOCAL_CONFDIR/override.d/extra-config.inc"
+      ''}
+   '';
+
+  filterFiles = files: filterAttrs (n: v: v.enable) files;
+  rspamdDir = pkgs.linkFarm "etc-rspamd-dir" (
+    (mapAttrsToList (name: file: { name = "local.d/${name}"; path = file.source; }) (filterFiles cfg.locals)) ++
+    (mapAttrsToList (name: file: { name = "override.d/${name}"; path = file.source; }) (filterFiles cfg.overrides)) ++
+    (optional (cfg.localLuaRules != null) { name = "rspamd.local.lua"; path = cfg.localLuaRules; }) ++
+    [ { name = "rspamd.conf"; path = rspamdConfFile; } ]
+  );
+
+  configFileModule = prefix: { name, config, ... }: {
+    options = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether this file ${prefix} should be generated.  This
+          option allows specific ${prefix} files to be disabled.
+        '';
+      };
+
+      text = mkOption {
+        default = null;
+        type = types.nullOr types.lines;
+        description = "Text of the file.";
+      };
+
+      source = mkOption {
+        type = types.path;
+        description = "Path of the source file.";
+      };
+    };
+    config = {
+      source = mkIf (config.text != null) (
+        let name' = "rspamd-${prefix}-" + baseNameOf name;
+        in mkDefault (pkgs.writeText name' config.text));
+    };
+  };
+
+  configOverrides =
+    (mapAttrs' (n: v: nameValuePair "worker-${if n == "rspamd_proxy" then "proxy" else n}.inc" {
+      text = v.extraConfig;
+    })
+    (filterAttrs (n: v: v.extraConfig != "") cfg.workers))
+    // (if cfg.extraConfig == "" then {} else {
+      "extra-config.inc".text = cfg.extraConfig;
+    });
+in
+
+{
+  ###### interface
+
+  options = {
+
+    services.rspamd = {
+
+      enable = mkEnableOption "rspamd, the Rapid spam filtering system";
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to run the rspamd daemon in debug mode.";
+      };
+
+      locals = mkOption {
+        type = with types; attrsOf (submodule (configFileModule "locals"));
+        default = {};
+        description = ''
+          Local configuration files, written into <filename>/etc/rspamd/local.d/{name}</filename>.
+        '';
+        example = literalExpression ''
+          { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
+            "arc.conf".text = "allow_envfrom_empty = true;";
+          }
+        '';
+      };
+
+      overrides = mkOption {
+        type = with types; attrsOf (submodule (configFileModule "overrides"));
+        default = {};
+        description = ''
+          Overridden configuration files, written into <filename>/etc/rspamd/override.d/{name}</filename>.
+        '';
+        example = literalExpression ''
+          { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
+            "arc.conf".text = "allow_envfrom_empty = true;";
+          }
+        '';
+      };
+
+      localLuaRules = mkOption {
+        default = null;
+        type = types.nullOr types.path;
+        description = ''
+          Path of file to link to <filename>/etc/rspamd/rspamd.local.lua</filename> for local
+          rules written in Lua
+        '';
+      };
+
+      workers = mkOption {
+        type = with types; attrsOf (submodule workerOpts);
+        description = ''
+          Attribute set of workers to start.
+        '';
+        default = {
+          normal = {};
+          controller = {};
+        };
+        example = literalExpression ''
+          {
+            normal = {
+              includes = [ "$CONFDIR/worker-normal.inc" ];
+              bindSockets = [{
+                socket = "/run/rspamd/rspamd.sock";
+                mode = "0660";
+                owner = "''${config.${opt.user}}";
+                group = "''${config.${opt.group}}";
+              }];
+            };
+            controller = {
+              includes = [ "$CONFDIR/worker-controller.inc" ];
+              bindSockets = [ "[::1]:11334" ];
+            };
+          }
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration to add at the end of the rspamd configuration
+          file.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "rspamd";
+        description = ''
+          User to use when no root privileges are required.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "rspamd";
+        description = ''
+          Group to use when no root privileges are required.
+        '';
+      };
+
+      postfix = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Add rspamd milter to postfix main.conf";
+        };
+
+        config = mkOption {
+          type = with types; attrsOf (oneOf [ bool str (listOf str) ]);
+          description = ''
+            Addon to postfix configuration
+          '';
+          default = {
+            smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
+            non_smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
+          };
+        };
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    services.rspamd.overrides = configOverrides;
+    services.rspamd.workers = mkIf cfg.postfix.enable {
+      controller = {};
+      rspamd_proxy = {
+        bindSockets = [ {
+          mode = "0660";
+          socket = "/run/rspamd/rspamd-milter.sock";
+          owner = cfg.user;
+          group = postfixCfg.group;
+        } ];
+        extraConfig = ''
+          upstream "local" {
+            default = yes; # Self-scan upstreams are always default
+            self_scan = yes; # Enable self-scan
+          }
+        '';
+      };
+    };
+    services.postfix.config = mkIf cfg.postfix.enable cfg.postfix.config;
+
+    systemd.services.postfix = mkIf cfg.postfix.enable {
+      serviceConfig.SupplementaryGroups = [ postfixCfg.group ];
+    };
+
+    # Allow users to run 'rspamc' and 'rspamadm'.
+    environment.systemPackages = [ pkgs.rspamd ];
+
+    users.users.${cfg.user} = {
+      description = "rspamd daemon";
+      uid = config.ids.uids.rspamd;
+      group = cfg.group;
+    };
+
+    users.groups.${cfg.group} = {
+      gid = config.ids.gids.rspamd;
+    };
+
+    environment.etc.rspamd.source = rspamdDir;
+
+    systemd.services.rspamd = {
+      description = "Rspamd Service";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      restartTriggers = [ rspamdDir ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} -c /etc/rspamd/rspamd.conf -f";
+        Restart = "always";
+
+        User = "${cfg.user}";
+        Group = "${cfg.group}";
+        SupplementaryGroups = mkIf cfg.postfix.enable [ postfixCfg.group ];
+
+        RuntimeDirectory = "rspamd";
+        RuntimeDirectoryMode = "0755";
+        StateDirectory = "rspamd";
+        StateDirectoryMode = "0700";
+
+        AmbientCapabilities = [];
+        CapabilityBoundingSet = "";
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateTmp = true;
+        # we need to chown socket to rspamd-milter
+        PrivateUsers = !cfg.postfix.enable;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "@system-service";
+        UMask = "0077";
+      };
+    };
+  };
+  imports = [
+    (mkRemovedOptionModule [ "services" "rspamd" "socketActivation" ]
+       "Socket activation never worked correctly and could at this time not be fixed and so was removed")
+    (mkRenamedOptionModule [ "services" "rspamd" "bindSocket" ] [ "services" "rspamd" "workers" "normal" "bindSockets" ])
+    (mkRenamedOptionModule [ "services" "rspamd" "bindUISocket" ] [ "services" "rspamd" "workers" "controller" "bindSockets" ])
+    (mkRemovedOptionModule [ "services" "rmilter" ] "Use services.rspamd.* instead to set up milter service")
+  ];
+}
diff --git a/nixos/modules/services/mail/rss2email.nix b/nixos/modules/services/mail/rss2email.nix
new file mode 100644
index 00000000000..7f8d2adac64
--- /dev/null
+++ b/nixos/modules/services/mail/rss2email.nix
@@ -0,0 +1,135 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.rss2email;
+in {
+
+  ###### interface
+
+  options = {
+
+    services.rss2email = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable rss2email.";
+      };
+
+      to = mkOption {
+        type = types.str;
+        description = "Mail address to which to send emails";
+      };
+
+      interval = mkOption {
+        type = types.str;
+        default = "12h";
+        description = "How often to check the feeds, in systemd interval format";
+      };
+
+      config = mkOption {
+        type = with types; attrsOf (oneOf [ str int bool ]);
+        default = {};
+        description = ''
+          The configuration to give rss2email.
+
+          Default will use system-wide <literal>sendmail</literal> to send the
+          email. This is rss2email's default when running
+          <literal>r2e new</literal>.
+
+          This set contains key-value associations that will be set in the
+          <literal>[DEFAULT]</literal> block along with the
+          <literal>to</literal> parameter.
+
+          See <literal>man r2e</literal> for more information on which
+          parameters are accepted.
+        '';
+      };
+
+      feeds = mkOption {
+        description = "The feeds to watch.";
+        type = types.attrsOf (types.submodule {
+          options = {
+            url = mkOption {
+              type = types.str;
+              description = "The URL at which to fetch the feed.";
+            };
+
+            to = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Email address to which to send feed items.
+
+                If <literal>null</literal>, this will not be set in the
+                configuration file, and rss2email will make it default to
+                <literal>rss2email.to</literal>.
+              '';
+            };
+          };
+        });
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    users.groups = {
+      rss2email.gid = config.ids.gids.rss2email;
+    };
+
+    users.users = {
+      rss2email = {
+        description = "rss2email user";
+        uid = config.ids.uids.rss2email;
+        group = "rss2email";
+      };
+    };
+
+    environment.systemPackages = with pkgs; [ rss2email ];
+
+    services.rss2email.config.to = cfg.to;
+
+    systemd.tmpfiles.rules = [
+      "d /var/rss2email 0700 rss2email rss2email - -"
+    ];
+
+    systemd.services.rss2email = let
+      conf = pkgs.writeText "rss2email.cfg" (lib.generators.toINI {} ({
+          DEFAULT = cfg.config;
+        } // lib.mapAttrs' (name: feed: nameValuePair "feed.${name}" (
+          { inherit (feed) url; } //
+          lib.optionalAttrs (feed.to != null) { inherit (feed) to; }
+        )) cfg.feeds
+      ));
+    in
+    {
+      preStart = ''
+        cp ${conf} /var/rss2email/conf.cfg
+        if [ ! -f /var/rss2email/db.json ]; then
+          echo '{"version":2,"feeds":[]}' > /var/rss2email/db.json
+        fi
+      '';
+      path = [ pkgs.system-sendmail ];
+      serviceConfig = {
+        ExecStart =
+          "${pkgs.rss2email}/bin/r2e -c /var/rss2email/conf.cfg -d /var/rss2email/db.json run";
+        User = "rss2email";
+      };
+    };
+
+    systemd.timers.rss2email = {
+      partOf = [ "rss2email.service" ];
+      wantedBy = [ "timers.target" ];
+      timerConfig.OnBootSec = "0";
+      timerConfig.OnUnitActiveSec = cfg.interval;
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ ekleog ];
+}
diff --git a/nixos/modules/services/mail/spamassassin.nix b/nixos/modules/services/mail/spamassassin.nix
new file mode 100644
index 00000000000..ac878222b26
--- /dev/null
+++ b/nixos/modules/services/mail/spamassassin.nix
@@ -0,0 +1,191 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.spamassassin;
+  spamassassin-local-cf = pkgs.writeText "local.cf" cfg.config;
+
+in
+
+{
+  options = {
+
+    services.spamassassin = {
+      enable = mkEnableOption "the SpamAssassin daemon";
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to run the SpamAssassin daemon in debug mode";
+      };
+
+      config = mkOption {
+        type = types.lines;
+        description = ''
+          The SpamAssassin local.cf config
+
+          If you are using this configuration:
+            add_header all Status _YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_
+
+          Then you can Use this sieve filter:
+            require ["fileinto", "reject", "envelope"];
+
+            if header :contains "X-Spam-Flag" "YES" {
+              fileinto "spam";
+            }
+
+          Or this procmail filter:
+            :0:
+            * ^X-Spam-Flag: YES
+            /var/vpopmail/domains/lastlog.de/js/.maildir/.spam/new
+
+          To filter your messages based on the additional mail headers added by spamassassin.
+        '';
+        example = ''
+          #rewrite_header Subject [***** SPAM _SCORE_ *****]
+          required_score          5.0
+          use_bayes               1
+          bayes_auto_learn        1
+          add_header all Status _YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_
+        '';
+        default = "";
+      };
+
+      initPreConf = mkOption {
+        type = with types; either str path;
+        description = "The SpamAssassin init.pre config.";
+        apply = val: if builtins.isPath val then val else pkgs.writeText "init.pre" val;
+        default =
+        ''
+          #
+          # to update this list, run this command in the rules directory:
+          # grep 'loadplugin.*Mail::SpamAssassin::Plugin::.*' -o -h * | sort | uniq
+          #
+
+          #loadplugin Mail::SpamAssassin::Plugin::AccessDB
+          #loadplugin Mail::SpamAssassin::Plugin::AntiVirus
+          loadplugin Mail::SpamAssassin::Plugin::AskDNS
+          # loadplugin Mail::SpamAssassin::Plugin::ASN
+          loadplugin Mail::SpamAssassin::Plugin::AutoLearnThreshold
+          #loadplugin Mail::SpamAssassin::Plugin::AWL
+          loadplugin Mail::SpamAssassin::Plugin::Bayes
+          loadplugin Mail::SpamAssassin::Plugin::BodyEval
+          loadplugin Mail::SpamAssassin::Plugin::Check
+          #loadplugin Mail::SpamAssassin::Plugin::DCC
+          loadplugin Mail::SpamAssassin::Plugin::DKIM
+          loadplugin Mail::SpamAssassin::Plugin::DNSEval
+          loadplugin Mail::SpamAssassin::Plugin::FreeMail
+          loadplugin Mail::SpamAssassin::Plugin::Hashcash
+          loadplugin Mail::SpamAssassin::Plugin::HeaderEval
+          loadplugin Mail::SpamAssassin::Plugin::HTMLEval
+          loadplugin Mail::SpamAssassin::Plugin::HTTPSMismatch
+          loadplugin Mail::SpamAssassin::Plugin::ImageInfo
+          loadplugin Mail::SpamAssassin::Plugin::MIMEEval
+          loadplugin Mail::SpamAssassin::Plugin::MIMEHeader
+          # loadplugin Mail::SpamAssassin::Plugin::PDFInfo
+          #loadplugin Mail::SpamAssassin::Plugin::PhishTag
+          loadplugin Mail::SpamAssassin::Plugin::Pyzor
+          loadplugin Mail::SpamAssassin::Plugin::Razor2
+          # loadplugin Mail::SpamAssassin::Plugin::RelayCountry
+          loadplugin Mail::SpamAssassin::Plugin::RelayEval
+          loadplugin Mail::SpamAssassin::Plugin::ReplaceTags
+          # loadplugin Mail::SpamAssassin::Plugin::Rule2XSBody
+          # loadplugin Mail::SpamAssassin::Plugin::Shortcircuit
+          loadplugin Mail::SpamAssassin::Plugin::SpamCop
+          loadplugin Mail::SpamAssassin::Plugin::SPF
+          #loadplugin Mail::SpamAssassin::Plugin::TextCat
+          # loadplugin Mail::SpamAssassin::Plugin::TxRep
+          loadplugin Mail::SpamAssassin::Plugin::URIDetail
+          loadplugin Mail::SpamAssassin::Plugin::URIDNSBL
+          loadplugin Mail::SpamAssassin::Plugin::URIEval
+          # loadplugin Mail::SpamAssassin::Plugin::URILocalBL
+          loadplugin Mail::SpamAssassin::Plugin::VBounce
+          loadplugin Mail::SpamAssassin::Plugin::WhiteListSubject
+          loadplugin Mail::SpamAssassin::Plugin::WLBLEval
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."mail/spamassassin/init.pre".source = cfg.initPreConf;
+    environment.etc."mail/spamassassin/local.cf".source = spamassassin-local-cf;
+
+    # Allow users to run 'spamc'.
+    environment.systemPackages = [ pkgs.spamassassin ];
+
+    users.users.spamd = {
+      description = "Spam Assassin Daemon";
+      uid = config.ids.uids.spamd;
+      group = "spamd";
+    };
+
+    users.groups.spamd = {
+      gid = config.ids.gids.spamd;
+    };
+
+    systemd.services.sa-update = {
+      # Needs to be able to contact the update server.
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+
+      serviceConfig = {
+        Type = "oneshot";
+        User = "spamd";
+        Group = "spamd";
+        StateDirectory = "spamassassin";
+        ExecStartPost = "+${pkgs.systemd}/bin/systemctl -q --no-block try-reload-or-restart spamd.service";
+      };
+
+      script = ''
+        set +e
+        ${pkgs.spamassassin}/bin/sa-update --verbose --gpghomedir=/var/lib/spamassassin/sa-update-keys/
+        rc=$?
+        set -e
+
+        if [[ $rc -gt 1 ]]; then
+          # sa-update failed.
+          exit $rc
+        fi
+
+        if [[ $rc -eq 1 ]]; then
+          # No update was available, exit successfully.
+          exit 0
+        fi
+
+        # An update was available and installed. Compile the rules.
+        ${pkgs.spamassassin}/bin/sa-compile
+      '';
+    };
+
+    systemd.timers.sa-update = {
+      description = "sa-update-service";
+      partOf      = [ "sa-update.service" ];
+      wantedBy    = [ "timers.target" ];
+      timerConfig = {
+        OnCalendar = "1:*";
+        Persistent = true;
+      };
+    };
+
+    systemd.services.spamd = {
+      description = "SpamAssassin Server";
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "sa-update.service" ];
+      after = [
+        "network.target"
+        "sa-update.service"
+      ];
+
+      serviceConfig = {
+        User = "spamd";
+        Group = "spamd";
+        ExecStart = "+${pkgs.spamassassin}/bin/spamd ${optionalString cfg.debug "-D"} --username=spamd --groupname=spamd --virtual-config-dir=%S/spamassassin/user-%u --allow-tell --pidfile=/run/spamd.pid";
+        ExecReload = "+${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        StateDirectory = "spamassassin";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/mail/sympa.nix b/nixos/modules/services/mail/sympa.nix
new file mode 100644
index 00000000000..f3578bef96e
--- /dev/null
+++ b/nixos/modules/services/mail/sympa.nix
@@ -0,0 +1,590 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.sympa;
+  dataDir = "/var/lib/sympa";
+  user = "sympa";
+  group = "sympa";
+  pkg = pkgs.sympa;
+  fqdns = attrNames cfg.domains;
+  usingNginx = cfg.web.enable && cfg.web.server == "nginx";
+  mysqlLocal = cfg.database.createLocally && cfg.database.type == "MySQL";
+  pgsqlLocal = cfg.database.createLocally && cfg.database.type == "PostgreSQL";
+
+  sympaSubServices = [
+    "sympa-archive.service"
+    "sympa-bounce.service"
+    "sympa-bulk.service"
+    "sympa-task.service"
+  ];
+
+  # common for all services including wwsympa
+  commonServiceConfig = {
+    StateDirectory = "sympa";
+    ProtectHome = true;
+    ProtectSystem = "full";
+    ProtectControlGroups = true;
+  };
+
+  # wwsympa has its own service config
+  sympaServiceConfig = srv: {
+    Type = "simple";
+    Restart = "always";
+    ExecStart = "${pkg}/bin/${srv}.pl --foreground";
+    PIDFile = "/run/sympa/${srv}.pid";
+    User = user;
+    Group = group;
+
+    # avoid duplicating log messageges in journal
+    StandardError = "null";
+  } // commonServiceConfig;
+
+  configVal = value:
+    if isBool value then
+      if value then "on" else "off"
+    else toString value;
+  configGenerator = c: concatStrings (flip mapAttrsToList c (key: val: "${key}\t${configVal val}\n"));
+
+  mainConfig = pkgs.writeText "sympa.conf" (configGenerator cfg.settings);
+  robotConfig = fqdn: domain: pkgs.writeText "${fqdn}-robot.conf" (configGenerator domain.settings);
+
+  transport = pkgs.writeText "transport.sympa" (concatStringsSep "\n" (flip map fqdns (domain: ''
+    ${domain}                        error:User unknown in recipient table
+    sympa@${domain}                  sympa:sympa@${domain}
+    listmaster@${domain}             sympa:listmaster@${domain}
+    bounce@${domain}                 sympabounce:sympa@${domain}
+    abuse-feedback-report@${domain}  sympabounce:sympa@${domain}
+  '')));
+
+  virtual = pkgs.writeText "virtual.sympa" (concatStringsSep "\n" (flip map fqdns (domain: ''
+    sympa-request@${domain}  postmaster@localhost
+    sympa-owner@${domain}    postmaster@localhost
+  '')));
+
+  listAliases = pkgs.writeText "list_aliases.tt2" ''
+    #--- [% list.name %]@[% list.domain %]: list transport map created at [% date %]
+    [% list.name %]@[% list.domain %] sympa:[% list.name %]@[% list.domain %]
+    [% list.name %]-request@[% list.domain %] sympa:[% list.name %]-request@[% list.domain %]
+    [% list.name %]-editor@[% list.domain %] sympa:[% list.name %]-editor@[% list.domain %]
+    #[% list.name %]-subscribe@[% list.domain %] sympa:[% list.name %]-subscribe@[%list.domain %]
+    [% list.name %]-unsubscribe@[% list.domain %] sympa:[% list.name %]-unsubscribe@[% list.domain %]
+    [% list.name %][% return_path_suffix %]@[% list.domain %] sympabounce:[% list.name %]@[% list.domain %]
+  '';
+
+  enabledFiles = filterAttrs (n: v: v.enable) cfg.settingsFile;
+in
+{
+
+  ###### interface
+  options.services.sympa = with types; {
+
+    enable = mkEnableOption "Sympa mailing list manager";
+
+    lang = mkOption {
+      type = str;
+      default = "en_US";
+      example = "cs";
+      description = ''
+        Default Sympa language.
+        See <link xlink:href='https://github.com/sympa-community/sympa/tree/sympa-6.2/po/sympa' />
+        for available options.
+      '';
+    };
+
+    listMasters = mkOption {
+      type = listOf str;
+      example = [ "postmaster@sympa.example.org" ];
+      description = ''
+        The list of the email addresses of the listmasters
+        (users authorized to perform global server commands).
+      '';
+    };
+
+    mainDomain = mkOption {
+      type = nullOr str;
+      default = null;
+      example = "lists.example.org";
+      description = ''
+        Main domain to be used in <filename>sympa.conf</filename>.
+        If <literal>null</literal>, one of the <option>services.sympa.domains</option> is chosen for you.
+      '';
+    };
+
+    domains = mkOption {
+      type = attrsOf (submodule ({ name, config, ... }: {
+        options = {
+          webHost = mkOption {
+            type = nullOr str;
+            default = null;
+            example = "archive.example.org";
+            description = ''
+              Domain part of the web interface URL (no web interface for this domain if <literal>null</literal>).
+              DNS record of type A (or AAAA or CNAME) has to exist with this value.
+            '';
+          };
+          webLocation = mkOption {
+            type = str;
+            default = "/";
+            example = "/sympa";
+            description = "URL path part of the web interface.";
+          };
+          settings = mkOption {
+            type = attrsOf (oneOf [ str int bool ]);
+            default = {};
+            example = {
+              default_max_list_members = 3;
+            };
+            description = ''
+              The <filename>robot.conf</filename> configuration file as key value set.
+              See <link xlink:href='https://sympa-community.github.io/gpldoc/man/sympa.conf.5.html' />
+              for list of configuration parameters.
+            '';
+          };
+        };
+
+        config.settings = mkIf (cfg.web.enable && config.webHost != null) {
+          wwsympa_url = mkDefault "https://${config.webHost}${strings.removeSuffix "/" config.webLocation}";
+        };
+      }));
+
+      description = ''
+        Email domains handled by this instance. There have
+        to be MX records for keys of this attribute set.
+      '';
+      example = literalExpression ''
+        {
+          "lists.example.org" = {
+            webHost = "lists.example.org";
+            webLocation = "/";
+          };
+          "sympa.example.com" = {
+            webHost = "example.com";
+            webLocation = "/sympa";
+          };
+        }
+      '';
+    };
+
+    database = {
+      type = mkOption {
+        type = enum [ "SQLite" "PostgreSQL" "MySQL" ];
+        default = "SQLite";
+        example = "MySQL";
+        description = "Database engine to use.";
+      };
+
+      host = mkOption {
+        type = nullOr str;
+        default = null;
+        description = ''
+          Database host address.
+
+          For MySQL, use <literal>localhost</literal> to connect using Unix domain socket.
+
+          For PostgreSQL, use path to directory (e.g. <filename>/run/postgresql</filename>)
+          to connect using Unix domain socket located in this directory.
+
+          Use <literal>null</literal> to fall back on Sympa default, or when using
+          <option>services.sympa.database.createLocally</option>.
+        '';
+      };
+
+      port = mkOption {
+        type = nullOr port;
+        default = null;
+        description = "Database port. Use <literal>null</literal> for default port.";
+      };
+
+      name = mkOption {
+        type = str;
+        default = if cfg.database.type == "SQLite" then "${dataDir}/sympa.sqlite" else "sympa";
+        defaultText = literalExpression ''if database.type == "SQLite" then "${dataDir}/sympa.sqlite" else "sympa"'';
+        description = ''
+          Database name. When using SQLite this must be an absolute
+          path to the database file.
+        '';
+      };
+
+      user = mkOption {
+        type = nullOr str;
+        default = user;
+        description = "Database user. The system user name is used as a default.";
+      };
+
+      passwordFile = mkOption {
+        type = nullOr path;
+        default = null;
+        example = "/run/keys/sympa-dbpassword";
+        description = ''
+          A file containing the password for <option>services.sympa.database.user</option>.
+        '';
+      };
+
+      createLocally = mkOption {
+        type = bool;
+        default = true;
+        description = "Whether to create a local database automatically.";
+      };
+    };
+
+    web = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = "Whether to enable Sympa web interface.";
+      };
+
+      server = mkOption {
+        type = enum [ "nginx" "none" ];
+        default = "nginx";
+        description = ''
+          The webserver used for the Sympa web interface. Set it to `none` if you want to configure it yourself.
+          Further nginx configuration can be done by adapting
+          <option>services.nginx.virtualHosts.<replaceable>name</replaceable></option>.
+        '';
+      };
+
+      https = mkOption {
+        type = bool;
+        default = true;
+        description = ''
+          Whether to use HTTPS. When nginx integration is enabled, this option forces SSL and enables ACME.
+          Please note that Sympa web interface always uses https links even when this option is disabled.
+        '';
+      };
+
+      fcgiProcs = mkOption {
+        type = ints.positive;
+        default = 2;
+        description = "Number of FastCGI processes to fork.";
+      };
+    };
+
+    mta = {
+      type = mkOption {
+        type = enum [ "postfix" "none" ];
+        default = "postfix";
+        description = ''
+          Mail transfer agent (MTA) integration. Use <literal>none</literal> if you want to configure it yourself.
+
+          The <literal>postfix</literal> integration sets up local Postfix instance that will pass incoming
+          messages from configured domains to Sympa. You still need to configure at least outgoing message
+          handling using e.g. <option>services.postfix.relayHost</option>.
+        '';
+      };
+    };
+
+    settings = mkOption {
+      type = attrsOf (oneOf [ str int bool ]);
+      default = {};
+      example = literalExpression ''
+        {
+          default_home = "lists";
+          viewlogs_page_size = 50;
+        }
+      '';
+      description = ''
+        The <filename>sympa.conf</filename> configuration file as key value set.
+        See <link xlink:href='https://sympa-community.github.io/gpldoc/man/sympa.conf.5.html' />
+        for list of configuration parameters.
+      '';
+    };
+
+    settingsFile = mkOption {
+      type = attrsOf (submodule ({ name, config, ... }: {
+        options = {
+          enable = mkOption {
+            type = bool;
+            default = true;
+            description = "Whether this file should be generated. This option allows specific files to be disabled.";
+          };
+          text = mkOption {
+            default = null;
+            type = nullOr lines;
+            description = "Text of the file.";
+          };
+          source = mkOption {
+            type = path;
+            description = "Path of the source file.";
+          };
+        };
+
+        config.source = mkIf (config.text != null) (mkDefault (pkgs.writeText "sympa-${baseNameOf name}" config.text));
+      }));
+      default = {};
+      example = literalExpression ''
+        {
+          "list_data/lists.example.org/help" = {
+            text = "subject This list provides help to users";
+          };
+        }
+      '';
+      description = "Set of files to be linked in <filename>${dataDir}</filename>.";
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.sympa.settings = (mapAttrs (_: v: mkDefault v) {
+      domain     = if cfg.mainDomain != null then cfg.mainDomain else head fqdns;
+      listmaster = concatStringsSep "," cfg.listMasters;
+      lang       = cfg.lang;
+
+      home        = "${dataDir}/list_data";
+      arc_path    = "${dataDir}/arc";
+      bounce_path = "${dataDir}/bounce";
+
+      sendmail = "${pkgs.system-sendmail}/bin/sendmail";
+
+      db_type = cfg.database.type;
+      db_name = cfg.database.name;
+    }
+    // (optionalAttrs (cfg.database.host != null) {
+      db_host = cfg.database.host;
+    })
+    // (optionalAttrs mysqlLocal {
+      db_host = "localhost"; # use unix domain socket
+    })
+    // (optionalAttrs pgsqlLocal {
+      db_host = "/run/postgresql"; # use unix domain socket
+    })
+    // (optionalAttrs (cfg.database.port != null) {
+      db_port = cfg.database.port;
+    })
+    // (optionalAttrs (cfg.database.user != null) {
+      db_user = cfg.database.user;
+    })
+    // (optionalAttrs (cfg.mta.type == "postfix") {
+      sendmail_aliases = "${dataDir}/sympa_transport";
+      aliases_program  = "${pkgs.postfix}/bin/postmap";
+      aliases_db_type  = "hash";
+    })
+    // (optionalAttrs cfg.web.enable {
+      static_content_path = "${dataDir}/static_content";
+      css_path            = "${dataDir}/static_content/css";
+      pictures_path       = "${dataDir}/static_content/pictures";
+      mhonarc             = "${pkgs.perlPackages.MHonArc}/bin/mhonarc";
+    }));
+
+    services.sympa.settingsFile = {
+      "virtual.sympa"        = mkDefault { source = virtual; };
+      "transport.sympa"      = mkDefault { source = transport; };
+      "etc/list_aliases.tt2" = mkDefault { source = listAliases; };
+    }
+    // (flip mapAttrs' cfg.domains (fqdn: domain:
+          nameValuePair "etc/${fqdn}/robot.conf" (mkDefault { source = robotConfig fqdn domain; })));
+
+    environment = {
+      systemPackages = [ pkg ];
+    };
+
+    users.users.${user} = {
+      description = "Sympa mailing list manager user";
+      group = group;
+      home = dataDir;
+      createHome = false;
+      isSystemUser = true;
+    };
+
+    users.groups.${group} = {};
+
+    assertions = [
+      { assertion = cfg.database.createLocally -> cfg.database.user == user;
+        message = "services.sympa.database.user must be set to ${user} if services.sympa.database.createLocally is set to true";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
+        message = "a password cannot be specified if services.sympa.database.createLocally is set to true";
+      }
+    ];
+
+    systemd.tmpfiles.rules = [
+      "d  ${dataDir}                   0711 ${user} ${group} - -"
+      "d  ${dataDir}/etc               0700 ${user} ${group} - -"
+      "d  ${dataDir}/spool             0700 ${user} ${group} - -"
+      "d  ${dataDir}/list_data         0700 ${user} ${group} - -"
+      "d  ${dataDir}/arc               0700 ${user} ${group} - -"
+      "d  ${dataDir}/bounce            0700 ${user} ${group} - -"
+      "f  ${dataDir}/sympa_transport   0600 ${user} ${group} - -"
+
+      # force-copy static_content so it's up to date with package
+      # set permissions for wwsympa which needs write access (...)
+      "R  ${dataDir}/static_content    -    -       -        - -"
+      "C  ${dataDir}/static_content    0711 ${user} ${group} - ${pkg}/var/lib/sympa/static_content"
+      "e  ${dataDir}/static_content/*  0711 ${user} ${group} - -"
+
+      "d  /run/sympa                   0755 ${user} ${group} - -"
+    ]
+    ++ (flip concatMap fqdns (fqdn: [
+      "d  ${dataDir}/etc/${fqdn}       0700 ${user} ${group} - -"
+      "d  ${dataDir}/list_data/${fqdn} 0700 ${user} ${group} - -"
+    ]))
+    #++ (flip mapAttrsToList enabledFiles (k: v:
+    #  "L+ ${dataDir}/${k}              -    -       -        - ${v.source}"
+    #))
+    ++ (concatLists (flip mapAttrsToList enabledFiles (k: v: [
+      # sympa doesn't handle symlinks well (e.g. fails to create locks)
+      # force-copy instead
+      "R ${dataDir}/${k}              -    -       -        - -"
+      "C ${dataDir}/${k}              0700 ${user}  ${group} - ${v.source}"
+    ])));
+
+    systemd.services.sympa = {
+      description = "Sympa mailing list manager";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      wants = sympaSubServices;
+      before = sympaSubServices;
+      serviceConfig = sympaServiceConfig "sympa_msg";
+
+      preStart = ''
+        umask 0077
+
+        cp -f ${mainConfig} ${dataDir}/etc/sympa.conf
+        ${optionalString (cfg.database.passwordFile != null) ''
+          chmod u+w ${dataDir}/etc/sympa.conf
+          echo -n "db_passwd " >> ${dataDir}/etc/sympa.conf
+          cat ${cfg.database.passwordFile} >> ${dataDir}/etc/sympa.conf
+        ''}
+
+        ${optionalString (cfg.mta.type == "postfix") ''
+          ${pkgs.postfix}/bin/postmap hash:${dataDir}/virtual.sympa
+          ${pkgs.postfix}/bin/postmap hash:${dataDir}/transport.sympa
+        ''}
+        ${pkg}/bin/sympa_newaliases.pl
+        ${pkg}/bin/sympa.pl --health_check
+      '';
+    };
+    systemd.services.sympa-archive = {
+      description = "Sympa mailing list manager (archiving)";
+      bindsTo = [ "sympa.service" ];
+      serviceConfig = sympaServiceConfig "archived";
+    };
+    systemd.services.sympa-bounce = {
+      description = "Sympa mailing list manager (bounce processing)";
+      bindsTo = [ "sympa.service" ];
+      serviceConfig = sympaServiceConfig "bounced";
+    };
+    systemd.services.sympa-bulk = {
+      description = "Sympa mailing list manager (message distribution)";
+      bindsTo = [ "sympa.service" ];
+      serviceConfig = sympaServiceConfig "bulk";
+    };
+    systemd.services.sympa-task = {
+      description = "Sympa mailing list manager (task management)";
+      bindsTo = [ "sympa.service" ];
+      serviceConfig = sympaServiceConfig "task_manager";
+    };
+
+    systemd.services.wwsympa = mkIf usingNginx {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "sympa.service" ];
+      serviceConfig = {
+        Type = "forking";
+        PIDFile = "/run/sympa/wwsympa.pid";
+        Restart = "always";
+        ExecStart = ''${pkgs.spawn_fcgi}/bin/spawn-fcgi \
+          -u ${user} \
+          -g ${group} \
+          -U nginx \
+          -M 0600 \
+          -F ${toString cfg.web.fcgiProcs} \
+          -P /run/sympa/wwsympa.pid \
+          -s /run/sympa/wwsympa.socket \
+          -- ${pkg}/lib/sympa/cgi/wwsympa.fcgi
+        '';
+
+      } // commonServiceConfig;
+    };
+
+    services.nginx.enable = mkIf usingNginx true;
+    services.nginx.virtualHosts = mkIf usingNginx (let
+      vHosts = unique (remove null (mapAttrsToList (_k: v: v.webHost) cfg.domains));
+      hostLocations = host: map (v: v.webLocation) (filter (v: v.webHost == host) (attrValues cfg.domains));
+      httpsOpts = optionalAttrs cfg.web.https { forceSSL = mkDefault true; enableACME = mkDefault true; };
+    in
+    genAttrs vHosts (host: {
+      locations = genAttrs (hostLocations host) (loc: {
+        extraConfig = ''
+          include ${config.services.nginx.package}/conf/fastcgi_params;
+
+          fastcgi_pass unix:/run/sympa/wwsympa.socket;
+        '';
+      }) // {
+        "/static-sympa/".alias = "${dataDir}/static_content/";
+      };
+    } // httpsOpts));
+
+    services.postfix = mkIf (cfg.mta.type == "postfix") {
+      enable = true;
+      recipientDelimiter = "+";
+      config = {
+        virtual_alias_maps = [ "hash:${dataDir}/virtual.sympa" ];
+        virtual_mailbox_maps = [
+          "hash:${dataDir}/transport.sympa"
+          "hash:${dataDir}/sympa_transport"
+          "hash:${dataDir}/virtual.sympa"
+        ];
+        virtual_mailbox_domains = [ "hash:${dataDir}/transport.sympa" ];
+        transport_maps = [
+          "hash:${dataDir}/transport.sympa"
+          "hash:${dataDir}/sympa_transport"
+        ];
+      };
+      masterConfig = {
+        "sympa" = {
+          type = "unix";
+          privileged = true;
+          chroot = false;
+          command = "pipe";
+          args = [
+            "flags=hqRu"
+            "user=${user}"
+            "argv=${pkg}/libexec/queue"
+            "\${nexthop}"
+          ];
+        };
+        "sympabounce" = {
+          type = "unix";
+          privileged = true;
+          chroot = false;
+          command = "pipe";
+          args = [
+            "flags=hqRu"
+            "user=${user}"
+            "argv=${pkg}/libexec/bouncequeue"
+            "\${nexthop}"
+          ];
+        };
+      };
+    };
+
+    services.mysql = optionalAttrs mysqlLocal {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    services.postgresql = optionalAttrs pgsqlLocal {
+      enable = true;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ mmilata sorki ];
+}
diff --git a/nixos/modules/services/matrix/matrix-synapse-log_config.yaml b/nixos/modules/services/matrix/matrix-synapse-log_config.yaml
new file mode 100644
index 00000000000..d85bdd1208f
--- /dev/null
+++ b/nixos/modules/services/matrix/matrix-synapse-log_config.yaml
@@ -0,0 +1,25 @@
+version: 1
+
+# In systemd's journal, loglevel is implicitly stored, so let's omit it
+# from the message text.
+formatters:
+    journal_fmt:
+        format: '%(name)s: [%(request)s] %(message)s'
+
+filters:
+    context:
+        (): synapse.util.logcontext.LoggingContextFilter
+        request: ""
+
+handlers:
+    journal:
+        class: systemd.journal.JournalHandler
+        formatter: journal_fmt
+        filters: [context]
+        SYSLOG_IDENTIFIER: synapse
+
+root:
+    level: INFO
+    handlers: [journal]
+
+disable_existing_loggers: False
diff --git a/nixos/modules/services/matrix/matrix-synapse.nix b/nixos/modules/services/matrix/matrix-synapse.nix
new file mode 100644
index 00000000000..c4d14dbd547
--- /dev/null
+++ b/nixos/modules/services/matrix/matrix-synapse.nix
@@ -0,0 +1,773 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.matrix-synapse;
+  format = pkgs.formats.yaml {};
+
+  # remove null values from the final configuration
+  finalSettings = lib.filterAttrsRecursive (_: v: v != null) cfg.settings;
+  configFile = format.generate "homeserver.yaml" finalSettings;
+  logConfigFile = format.generate "log_config.yaml" cfg.logConfig;
+
+  pluginsEnv = cfg.package.python.buildEnv.override {
+    extraLibs = cfg.plugins;
+  };
+
+  usePostgresql = cfg.settings.database.name == "psycopg2";
+  hasLocalPostgresDB = let args = cfg.settings.database.args; in
+    usePostgresql && (!(args ? host) || (elem args.host [ "localhost" "127.0.0.1" "::1" ]));
+
+  registerNewMatrixUser =
+    let
+      isIpv6 = x: lib.length (lib.splitString ":" x) > 1;
+      listener =
+        lib.findFirst (
+          listener: lib.any (
+            resource: lib.any (
+              name: name == "client"
+            ) resource.names
+          ) listener.resources
+        ) (lib.last cfg.settings.listeners) cfg.settings.listeners;
+        # FIXME: Handle cases with missing client listener properly,
+        # don't rely on lib.last, this will not work.
+
+      # add a tail, so that without any bind_addresses we still have a useable address
+      bindAddress = head (listener.bind_addresses ++ [ "127.0.0.1" ]);
+      listenerProtocol = if listener.tls
+        then "https"
+        else "http";
+    in
+    pkgs.writeShellScriptBin "matrix-synapse-register_new_matrix_user" ''
+      exec ${cfg.package}/bin/register_new_matrix_user \
+        $@ \
+        ${lib.concatMapStringsSep " " (x: "-c ${x}") ([ configFile ] ++ cfg.extraConfigFiles)} \
+        "${listenerProtocol}://${
+          if (isIpv6 bindAddress) then
+            "[${bindAddress}]"
+          else
+            "${bindAddress}"
+        }:${builtins.toString listener.port}/"
+    '';
+in {
+
+  imports = [
+
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "trusted_third_party_id_servers" ] ''
+      The `trusted_third_party_id_servers` option as been removed in `matrix-synapse` v1.4.0
+      as the behavior is now obsolete.
+    '')
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "create_local_database" ] ''
+      Database configuration must be done manually. An exemplary setup is demonstrated in
+      <nixpkgs/nixos/tests/matrix-synapse.nix>
+    '')
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "web_client" ] "")
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "room_invite_state_types" ] ''
+      You may add additional event types via
+      `services.matrix-synapse.room_prejoin_state.additional_event_types` and
+      disable the default events via
+      `services.matrix-synapse.room_prejoin_state.disable_default_event_types`.
+    '')
+
+    # options that don't exist in synapse anymore
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "bind_host" ] "Use listener settings instead." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "bind_port" ] "Use listener settings instead." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "expire_access_tokens" ] "" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "no_tls" ] "It is no longer supported by synapse." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "tls_dh_param_path" ] "It was removed from synapse." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "unsecure_port" ] "Use settings.listeners instead." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "user_creation_max_duration" ] "It is no longer supported by synapse." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "verbose" ] "Use a log config instead." )
+
+    # options that were moved into rfc42 style settigns
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "app_service_config_files" ] "Use settings.app_service_config_Files instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "database_args" ] "Use settings.database.args instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "database_name" ] "Use settings.database.args.database instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "database_type" ] "Use settings.database.name instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "database_user" ] "Use settings.database.args.user instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "dynamic_thumbnails" ] "Use settings.dynamic_thumbnails instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "enable_metrics" ] "Use settings.enable_metrics instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "enable_registration" ] "Use settings.enable_registration instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "extraConfig" ] "Use settings instead." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "listeners" ] "Use settings.listeners instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "logConfig" ] "Use settings.log_config instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "max_image_pixels" ] "Use settings.max_image_pixels instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "max_upload_size" ] "Use settings.max_upload_size instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "presence" "enabled" ] "Use settings.presence.enabled instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "public_baseurl" ] "Use settings.public_baseurl instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "report_stats" ] "Use settings.report_stats instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "server_name" ] "Use settings.server_name instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "servers" ] "Use settings.trusted_key_servers instead." )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "tls_certificate_path" ] "Use settings.tls_certificate_path instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "tls_private_key_path" ] "Use settings.tls_private_key_path instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "turn_shared_secret" ] "Use settings.turn_shared_secret instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "turn_uris" ] "Use settings.turn_uris instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "turn_user_lifetime" ] "Use settings.turn_user_lifetime instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "url_preview_enabled" ] "Use settings.url_preview_enabled instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "url_preview_ip_range_blacklist" ] "Use settings.url_preview_ip_range_blacklist instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "url_preview_ip_range_whitelist" ] "Use settings.url_preview_ip_range_whitelist instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "url_preview_url_blacklist" ] "Use settings.url_preview_url_blacklist instead" )
+
+    # options that are too specific to mention them explicitly in settings
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "account_threepid_delegates" "email" ] "Use settings.account_threepid_delegates.email instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "account_threepid_delegates" "msisdn" ] "Use settings.account_threepid_delegates.msisdn instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "allow_guest_access" ] "Use settings.allow_guest_access instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "bcrypt_rounds" ] "Use settings.bcrypt_rounds instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "enable_registration_captcha" ] "Use settings.enable_registration_captcha instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "event_cache_size" ] "Use settings.event_cache_size instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "federation_rc_concurrent" ] "Use settings.rc_federation.concurrent instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "federation_rc_reject_limit" ] "Use settings.rc_federation.reject_limit instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "federation_rc_sleep_delay" ] "Use settings.rc_federation.sleep_delay instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "federation_rc_sleep_limit" ] "Use settings.rc_federation.sleep_limit instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "federation_rc_window_size" ] "Use settings.rc_federation.window_size instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "key_refresh_interval" ] "Use settings.key_refresh_interval instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "rc_messages_burst_count" ] "Use settings.rc_messages.burst_count instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "rc_messages_per_second" ] "Use settings.rc_messages.per_second instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "recaptcha_private_key" ] "Use settings.recaptcha_private_key instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "recaptcha_public_key" ] "Use settings.recaptcha_public_key instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "redaction_retention_period" ] "Use settings.redaction_retention_period instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "room_prejoin_state" "additional_event_types" ] "Use settings.room_prejoin_state.additional_event_types instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "room_prejoin_state" "disable_default_event_types" ] "Use settings.room_prejoin-state.disable_default_event_types instead" )
+
+    # Options that should be passed via extraConfigFiles, so they are not persisted into the nix store
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "macaroon_secret_key" ] "Pass this value via extraConfigFiles instead" )
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "registration_shared_secret" ] "Pass this value via extraConfigFiles instead" )
+
+  ];
+
+  options = {
+    services.matrix-synapse = {
+      enable = mkEnableOption "matrix.org synapse";
+
+      configFile = mkOption {
+        type = types.str;
+        readOnly = true;
+        description = ''
+          Path to the configuration file on the target system. Useful to configure e.g. workers
+          that also need this.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.matrix-synapse;
+        defaultText = literalExpression "pkgs.matrix-synapse";
+        description = ''
+          Overridable attribute of the matrix synapse server package to use.
+        '';
+      };
+
+      plugins = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        example = literalExpression ''
+          with config.services.matrix-synapse.package.plugins; [
+            matrix-synapse-ldap3
+            matrix-synapse-pam
+          ];
+        '';
+        description = ''
+          List of additional Matrix plugins to make available.
+        '';
+      };
+
+      withJemalloc = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to preload jemalloc to reduce memory fragmentation and overall usage.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/matrix-synapse";
+        description = ''
+          The directory where matrix-synapse stores its stateful data such as
+          certificates, media and uploads.
+        '';
+      };
+
+      settings = mkOption {
+        default = {};
+        description = ''
+          The primary synapse configuration. See the
+          <link xlink:href="https://github.com/matrix-org/synapse/blob/v${cfg.package.version}/docs/sample_config.yaml">sample configuration</link>
+          for possible values.
+
+          Secrets should be passed in by using the <literal>extraConfigFiles</literal> option.
+        '';
+        type = with types; submodule {
+          freeformType = format.type;
+          options = {
+            # This is a reduced set of popular options and defaults
+            # Do not add every available option here, they can be specified
+            # by the user at their own discretion. This is a freeform type!
+
+            server_name = mkOption {
+              type = types.str;
+              example = "example.com";
+              default = config.networking.hostName;
+              defaultText = literalExpression "config.networking.hostName";
+              description = ''
+                The domain name of the server, with optional explicit port.
+                This is used by remote servers to look up the server address.
+                This is also the last part of your UserID.
+
+                The server_name cannot be changed later so it is important to configure this correctly before you start Synapse.
+              '';
+            };
+
+            enable_registration = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Enable registration for new users.
+              '';
+            };
+
+            registration_shared_secret = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = ''
+                If set, allows registration by anyone who also has the shared
+                secret, even if registration is otherwise disabled.
+
+                Secrets should be passed in via <literal>extraConfigFiles</literal>!
+              '';
+            };
+
+            macaroon_secret_key = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = ''
+                Secret key for authentication tokens. If none is specified,
+                the registration_shared_secret is used, if one is given; otherwise,
+                a secret key is derived from the signing key.
+
+                Secrets should be passed in via <literal>extraConfigFiles</literal>!
+              '';
+            };
+
+            enable_metrics = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Enable collection and rendering of performance metrics
+              '';
+            };
+
+            report_stats = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Whether or not to report anonymized homeserver usage statistics.
+              '';
+            };
+
+            signing_key_path = mkOption {
+              type = types.path;
+              default = "${cfg.dataDir}/homeserver.signing.key";
+              description = ''
+                Path to the signing key to sign messages with.
+              '';
+            };
+
+            pid_file = mkOption {
+              type = types.path;
+              default = "/run/matrix-synapse.pid";
+              readOnly = true;
+              description = ''
+                The file to store the PID in.
+              '';
+            };
+
+            log_config = mkOption {
+              type = types.path;
+              default = ./matrix-synapse-log_config.yaml;
+              description = ''
+                The file that holds the logging configuration.
+              '';
+            };
+
+            media_store_path = mkOption {
+              type = types.path;
+              default = if lib.versionAtLeast config.system.stateVersion "22.05"
+                then "${cfg.dataDir}/media_store"
+                else "${cfg.dataDir}/media";
+              description = ''
+                Directory where uploaded images and attachments are stored.
+              '';
+            };
+
+            public_baseurl = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "https://example.com:8448/";
+              description = ''
+                The public-facing base URL for the client API (not including _matrix/...)
+              '';
+            };
+
+            tls_certificate_path = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "/var/lib/acme/example.com/fullchain.pem";
+              description = ''
+                PEM encoded X509 certificate for TLS.
+                You can replace the self-signed certificate that synapse
+                autogenerates on launch with your own SSL certificate + key pair
+                if you like.  Any required intermediary certificates can be
+                appended after the primary certificate in hierarchical order.
+              '';
+            };
+
+            tls_private_key_path = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "/var/lib/acme/example.com/key.pem";
+              description = ''
+                PEM encoded private key for TLS. Specify null if synapse is not
+                speaking TLS directly.
+              '';
+            };
+
+            presence.enabled = mkOption {
+              type = types.bool;
+              default = true;
+              example = false;
+              description = ''
+                Whether to enable presence tracking.
+
+                Presence tracking allows users to see the state (e.g online/offline)
+                of other local and remote users.
+              '';
+            };
+
+            listeners = mkOption {
+              type = types.listOf (types.submodule {
+                options = {
+                  port = mkOption {
+                    type = types.port;
+                    example = 8448;
+                    description = ''
+                      The port to listen for HTTP(S) requests on.
+                    '';
+                  };
+
+                  bind_addresses = mkOption {
+                    type = types.listOf types.str;
+                    default = [
+                      "::1"
+                      "127.0.0.1"
+                    ];
+                    example = literalExpression ''
+                    [
+                      "::"
+                      "0.0.0.0"
+                    ]
+                    '';
+                    description = ''
+                     IP addresses to bind the listener to.
+                    '';
+                  };
+
+                  type = mkOption {
+                    type = types.enum [
+                      "http"
+                      "manhole"
+                      "metrics"
+                      "replication"
+                    ];
+                    default = "http";
+                    example = "metrics";
+                    description = ''
+                      The type of the listener, usually http.
+                    '';
+                  };
+
+                  tls = mkOption {
+                    type = types.bool;
+                    default = true;
+                    example = false;
+                    description = ''
+                      Whether to enable TLS on the listener socket.
+                    '';
+                  };
+
+                  x_forwarded = mkOption {
+                    type = types.bool;
+                    default = false;
+                    example = true;
+                    description = ''
+                      Use the X-Forwarded-For (XFF) header as the client IP and not the
+                      actual client IP.
+                    '';
+                  };
+
+                  resources = mkOption {
+                    type = types.listOf (types.submodule {
+                      options = {
+                        names = mkOption {
+                          type = types.listOf (types.enum [
+                            "client"
+                            "consent"
+                            "federation"
+                            "keys"
+                            "media"
+                            "metrics"
+                            "openid"
+                            "replication"
+                            "static"
+                          ]);
+                          description = ''
+                            List of resources to host on this listener.
+                          '';
+                          example = [
+                            "client"
+                          ];
+                        };
+                        compress = mkOption {
+                          type = types.bool;
+                          description = ''
+                            Should synapse compress HTTP responses to clients that support it?
+                            This should be disabled if running synapse behind a load balancer
+                            that can do automatic compression.
+                          '';
+                        };
+                      };
+                    });
+                    description = ''
+                      List of HTTP resources to serve on this listener.
+                    '';
+                  };
+                };
+              });
+              default = [ {
+                port = 8008;
+                bind_addresses = [ "127.0.0.1" ];
+                type = "http";
+                tls = false;
+                x_forwarded = true;
+                resources = [ {
+                  names = [ "client" ];
+                  compress = true;
+                } {
+                  names = [ "federation" ];
+                  compress = false;
+                } ];
+              } ];
+              description = ''
+                List of ports that Synapse should listen on, their purpose and their configuration.
+              '';
+            };
+
+            database.name = mkOption {
+              type = types.enum [
+                "sqlite3"
+                "psycopg2"
+              ];
+              default = if versionAtLeast config.system.stateVersion "18.03"
+                then "psycopg2"
+                else "sqlite3";
+               defaultText = literalExpression ''
+                if versionAtLeast config.system.stateVersion "18.03"
+                then "psycopg2"
+                else "sqlite3"
+              '';
+              description = ''
+                The database engine name. Can be sqlite3 or psycopg2.
+              '';
+            };
+
+            database.args.database = mkOption {
+              type = types.str;
+              default = {
+                sqlite3 = "${cfg.dataDir}/homeserver.db";
+                psycopg2 = "matrix-synapse";
+              }.${cfg.settings.database.name};
+              defaultText = literalExpression ''
+              {
+                sqlite3 = "''${${options.services.matrix-synapse.dataDir}}/homeserver.db";
+                psycopg2 = "matrix-synapse";
+              }.''${${options.services.matrix-synapse.settings}.database.name};
+              '';
+              description = ''
+                Name of the database when using the psycopg2 backend,
+                path to the database location when using sqlite3.
+              '';
+            };
+
+            database.args.user = mkOption {
+              type = types.nullOr types.str;
+              default = {
+                sqlite3 = null;
+                psycopg2 = "matrix-synapse";
+              }.${cfg.settings.database.name};
+              description = ''
+                Username to connect with psycopg2, set to null
+                when using sqlite3.
+              '';
+            };
+
+            url_preview_enabled = mkOption {
+              type = types.bool;
+              default = true;
+              example = false;
+              description = ''
+                Is the preview URL API enabled?  If enabled, you *must* specify an
+                explicit url_preview_ip_range_blacklist of IPs that the spider is
+                denied from accessing.
+              '';
+            };
+
+            url_preview_ip_range_blacklist = mkOption {
+              type = types.listOf types.str;
+              default = [
+                "10.0.0.0/8"
+                "100.64.0.0/10"
+                "127.0.0.0/8"
+                "169.254.0.0/16"
+                "172.16.0.0/12"
+                "192.0.0.0/24"
+                "192.0.2.0/24"
+                "192.168.0.0/16"
+                "192.88.99.0/24"
+                "198.18.0.0/15"
+                "198.51.100.0/24"
+                "2001:db8::/32"
+                "203.0.113.0/24"
+                "224.0.0.0/4"
+                "::1/128"
+                "fc00::/7"
+                "fe80::/10"
+                "fec0::/10"
+                "ff00::/8"
+              ];
+              description = ''
+                List of IP address CIDR ranges that the URL preview spider is denied
+                from accessing.
+              '';
+            };
+
+            url_preview_ip_range_whitelist = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = ''
+                List of IP address CIDR ranges that the URL preview spider is allowed
+                to access even if they are specified in url_preview_ip_range_blacklist.
+              '';
+            };
+
+            url_preview_url_blacklist = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = ''
+                Optional list of URL matches that the URL preview spider is
+                denied from accessing.
+              '';
+            };
+
+            max_upload_size = mkOption {
+              type = types.str;
+              default = "50M";
+              example = "100M";
+              description = ''
+                The largest allowed upload size in bytes
+              '';
+            };
+
+            max_image_pixels = mkOption {
+              type = types.str;
+              default = "32M";
+              example = "64M";
+              description = ''
+                Maximum number of pixels that will be thumbnailed
+              '';
+            };
+
+            dynamic_thumbnails = mkOption {
+              type = types.bool;
+              default = false;
+              example = true;
+              description = ''
+                Whether to generate new thumbnails on the fly to precisely match
+                the resolution requested by the client. If true then whenever
+                a new resolution is requested by the client the server will
+                generate a new thumbnail. If false the server will pick a thumbnail
+                from a precalculated list.
+              '';
+            };
+
+            turn_uris = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [
+                "turn:turn.example.com:3487?transport=udp"
+                "turn:turn.example.com:3487?transport=tcp"
+                "turns:turn.example.com:5349?transport=udp"
+                "turns:turn.example.com:5349?transport=tcp"
+              ];
+              description = ''
+                The public URIs of the TURN server to give to clients
+              '';
+            };
+            turn_shared_secret = mkOption {
+              type = types.str;
+              default = "";
+              example = literalExpression ''
+                config.services.coturn.static-auth-secret
+              '';
+              description = ''
+                The shared secret used to compute passwords for the TURN server.
+
+                Secrets should be passed in via <literal>extraConfigFiles</literal>!
+              '';
+            };
+
+            trusted_key_servers = mkOption {
+              type = types.listOf (types.submodule {
+                options = {
+                  server_name = mkOption {
+                    type = types.str;
+                    example = "matrix.org";
+                    description = ''
+                      Hostname of the trusted server.
+                    '';
+                  };
+
+                  verify_keys = mkOption {
+                    type = types.nullOr (types.attrsOf types.str);
+                    default = null;
+                    example = literalExpression ''
+                      {
+                        "ed25519:auto" = "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw";
+                      }
+                    '';
+                    description = ''
+                      Attribute set from key id to base64 encoded public key.
+
+                      If specified synapse will check that the response is signed
+                      by at least one of the given keys.
+                    '';
+                  };
+                };
+              });
+              default = [ {
+                server_name = "matrix.org";
+                verify_keys = {
+                  "ed25519:auto" = "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw";
+                };
+              } ];
+              description = ''
+                The trusted servers to download signing keys from.
+              '';
+            };
+
+            app_service_config_files = mkOption {
+              type = types.listOf types.path;
+              default = [ ];
+              description = ''
+                A list of application service config file to use
+              '';
+            };
+
+          };
+        };
+      };
+
+      extraConfigFiles = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          Extra config files to include.
+
+          The configuration files will be included based on the command line
+          argument --config-path. This allows to configure secrets without
+          having to go through the Nix store, e.g. based on deployment keys if
+          NixOps is in use.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = hasLocalPostgresDB -> config.services.postgresql.enable;
+        message = ''
+          Cannot deploy matrix-synapse with a configuration for a local postgresql database
+            and a missing postgresql service. Since 20.03 it's mandatory to manually configure the
+            database (please read the thread in https://github.com/NixOS/nixpkgs/pull/80447 for
+            further reference).
+
+            If you
+            - try to deploy a fresh synapse, you need to configure the database yourself. An example
+              for this can be found in <nixpkgs/nixos/tests/matrix-synapse.nix>
+            - update your existing matrix-synapse instance, you simply need to add `services.postgresql.enable = true`
+              to your configuration.
+
+          For further information about this update, please read the release-notes of 20.03 carefully.
+        '';
+      }
+    ];
+
+    services.matrix-synapse.configFile = configFile;
+
+    users.users.matrix-synapse = {
+      group = "matrix-synapse";
+      home = cfg.dataDir;
+      createHome = true;
+      shell = "${pkgs.bash}/bin/bash";
+      uid = config.ids.uids.matrix-synapse;
+    };
+
+    users.groups.matrix-synapse = {
+      gid = config.ids.gids.matrix-synapse;
+    };
+
+    systemd.services.matrix-synapse = {
+      description = "Synapse Matrix homeserver";
+      after = [ "network.target" ] ++ optional hasLocalPostgresDB "postgresql.service";
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        ${cfg.package}/bin/synapse_homeserver \
+          --config-path ${configFile} \
+          --keys-directory ${cfg.dataDir} \
+          --generate-keys
+      '';
+      environment = {
+        PYTHONPATH = makeSearchPathOutput "lib" cfg.package.python.sitePackages [ pluginsEnv ];
+      } // optionalAttrs (cfg.withJemalloc) {
+        LD_PRELOAD = "${pkgs.jemalloc}/lib/libjemalloc.so";
+      };
+      serviceConfig = {
+        Type = "notify";
+        User = "matrix-synapse";
+        Group = "matrix-synapse";
+        WorkingDirectory = cfg.dataDir;
+        ExecStartPre = [ ("+" + (pkgs.writeShellScript "matrix-synapse-fix-permissions" ''
+          chown matrix-synapse:matrix-synapse ${cfg.dataDir}/homeserver.signing.key
+          chmod 0600 ${cfg.dataDir}/homeserver.signing.key
+        '')) ];
+        ExecStart = ''
+          ${cfg.package}/bin/synapse_homeserver \
+            ${ concatMapStringsSep "\n  " (x: "--config-path ${x} \\") ([ configFile ] ++ cfg.extraConfigFiles) }
+            --keys-directory ${cfg.dataDir}
+        '';
+        ExecReload = "${pkgs.util-linux}/bin/kill -HUP $MAINPID";
+        Restart = "on-failure";
+        UMask = "0077";
+      };
+    };
+
+    environment.systemPackages = [ registerNewMatrixUser ];
+  };
+
+  meta = {
+    buildDocsInSandbox = false;
+    doc = ./matrix-synapse.xml;
+    maintainers = teams.matrix.members;
+  };
+
+}
diff --git a/nixos/modules/services/matrix/matrix-synapse.xml b/nixos/modules/services/matrix/matrix-synapse.xml
new file mode 100644
index 00000000000..cf33957d58e
--- /dev/null
+++ b/nixos/modules/services/matrix/matrix-synapse.xml
@@ -0,0 +1,231 @@
+<chapter xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xmlns:xi="http://www.w3.org/2001/XInclude"
+         version="5.0"
+         xml:id="module-services-matrix">
+ <title>Matrix</title>
+ <para>
+  <link xlink:href="https://matrix.org/">Matrix</link> is an open standard for
+  interoperable, decentralised, real-time communication over IP. It can be used
+  to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things
+  communication - or anywhere you need a standard HTTP API for publishing and
+  subscribing to data whilst tracking the conversation history.
+ </para>
+ <para>
+  This chapter will show you how to set up your own, self-hosted Matrix
+  homeserver using the Synapse reference homeserver, and how to serve your own
+  copy of the Element web client. See the
+  <link xlink:href="https://matrix.org/docs/projects/try-matrix-now.html">Try
+  Matrix Now!</link> overview page for links to Element Apps for Android and iOS,
+  desktop clients, as well as bridges to other networks and other projects
+  around Matrix.
+ </para>
+ <section xml:id="module-services-matrix-synapse">
+  <title>Synapse Homeserver</title>
+
+  <para>
+   <link xlink:href="https://github.com/matrix-org/synapse">Synapse</link> is
+   the reference homeserver implementation of Matrix from the core development
+   team at matrix.org. The following configuration example will set up a
+   synapse server for the <literal>example.org</literal> domain, served from
+   the host <literal>myhostname.example.org</literal>. For more information,
+   please refer to the
+   <link xlink:href="https://github.com/matrix-org/synapse#synapse-installation">
+   installation instructions of Synapse </link>.
+<programlisting>
+{ pkgs, lib, ... }:
+let
+  fqdn =
+    let
+      join = hostName: domain: hostName + lib.optionalString (domain != null) ".${domain}";
+    in join config.networking.hostName config.networking.domain;
+in {
+  networking = {
+    <link linkend="opt-networking.hostName">hostName</link> = "myhostname";
+    <link linkend="opt-networking.domain">domain</link> = "example.org";
+  };
+  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
+
+  <link linkend="opt-services.postgresql.enable">services.postgresql.enable</link> = true;
+  <link linkend="opt-services.postgresql.initialScript">services.postgresql.initialScript</link> = pkgs.writeText "synapse-init.sql" ''
+    CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
+    CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
+      TEMPLATE template0
+      LC_COLLATE = "C"
+      LC_CTYPE = "C";
+  '';
+
+  services.nginx = {
+    <link linkend="opt-services.nginx.enable">enable</link> = true;
+    # only recommendedProxySettings and recommendedGzipSettings are strictly required,
+    # but the rest make sense as well
+    <link linkend="opt-services.nginx.recommendedTlsSettings">recommendedTlsSettings</link> = true;
+    <link linkend="opt-services.nginx.recommendedOptimisation">recommendedOptimisation</link> = true;
+    <link linkend="opt-services.nginx.recommendedGzipSettings">recommendedGzipSettings</link> = true;
+    <link linkend="opt-services.nginx.recommendedProxySettings">recommendedProxySettings</link> = true;
+
+    <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
+      # This host section can be placed on a different host than the rest,
+      # i.e. to delegate from the host being accessible as ${config.networking.domain}
+      # to another host actually running the Matrix homeserver.
+      "${config.networking.domain}" = {
+        <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
+        <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
+
+        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."= /.well-known/matrix/server".extraConfig</link> =
+          let
+            # use 443 instead of the default 8448 port to unite
+            # the client-server and server-server port for simplicity
+            server = { "m.server" = "${fqdn}:443"; };
+          in ''
+            add_header Content-Type application/json;
+            return 200 '${builtins.toJSON server}';
+          '';
+        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."= /.well-known/matrix/client".extraConfig</link> =
+          let
+            client = {
+              "m.homeserver" =  { "base_url" = "https://${fqdn}"; };
+              "m.identity_server" =  { "base_url" = "https://vector.im"; };
+            };
+          # ACAO required to allow element-web on any URL to request this json file
+          in ''
+            add_header Content-Type application/json;
+            add_header Access-Control-Allow-Origin *;
+            return 200 '${builtins.toJSON client}';
+          '';
+      };
+
+      # Reverse proxy for Matrix client-server and server-server communication
+      ${fqdn} = {
+        <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
+        <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
+
+        # Or do a redirect instead of the 404, or whatever is appropriate for you.
+        # But do not put a Matrix Web client here! See the Element web section below.
+        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."/".extraConfig</link> = ''
+          return 404;
+        '';
+
+        # forward all Matrix API calls to the synapse Matrix homeserver
+        locations."/_matrix" = {
+          <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.proxyPass">proxyPass</link> = "http://[::1]:8008"; # without a trailing /
+        };
+      };
+    };
+  };
+  services.matrix-synapse = {
+    <link linkend="opt-services.matrix-synapse.enable">enable</link> = true;
+    <link linkend="opt-services.matrix-synapse.settings.server_name">server_name</link> = config.networking.domain;
+    <link linkend="opt-services.matrix-synapse.settings.listeners">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" ];
+          <link linkend="opt-services.matrix-synapse.settings.listeners._.resources._.compress">compress</link> = true;
+        } {
+          <link linkend="opt-services.matrix-synapse.settings.listeners._.resources._.names">names</link> = [ "federation" ];
+          <link linkend="opt-services.matrix-synapse.settings.listeners._.resources._.compress">compress</link> = false;
+        } ];
+      }
+    ];
+  };
+}
+</programlisting>
+  </para>
+
+  <para>
+   If the <code>A</code> and <code>AAAA</code> DNS records on
+   <literal>example.org</literal> do not point on the same host as the records
+   for <code>myhostname.example.org</code>, you can easily move the
+   <code>/.well-known</code> virtualHost section of the code to the host that
+   is serving <literal>example.org</literal>, while the rest stays on
+   <literal>myhostname.example.org</literal> with no other changes required.
+   This pattern also allows to seamlessly move the homeserver from
+   <literal>myhostname.example.org</literal> to
+   <literal>myotherhost.example.org</literal> by only changing the
+   <code>/.well-known</code> redirection target.
+  </para>
+
+  <para>
+   If you want to run a server with public registration by anybody, you can
+   then enable <literal><link linkend="opt-services.matrix-synapse.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 run nixpkgs.matrix-synapse
+<prompt>$ </prompt>register_new_matrix_user -k <replaceable>your-registration-shared-secret</replaceable> http://localhost:8008
+<prompt>New user localpart: </prompt><replaceable>your-username</replaceable>
+<prompt>Password:</prompt>
+<prompt>Confirm password:</prompt>
+<prompt>Make admin [no]:</prompt>
+Success!
+</screen>
+   In the example, this would create a user with the Matrix Identifier
+   <literal>@your-username:example.org</literal>. Note that the registration
+   secret ends up in the nix store and therefore is world-readable by any user
+   on your machine, so it makes sense to only temporarily activate the
+   <link linkend="opt-services.matrix-synapse.settings.registration_shared_secret">registration_shared_secret</link>
+   option until a better solution for NixOS is in place.
+  </para>
+ </section>
+ <section xml:id="module-services-matrix-element-web">
+  <title>Element (formerly known as Riot) Web Client</title>
+
+  <para>
+   <link xlink:href="https://github.com/vector-im/riot-web/">Element Web</link> is
+   the reference web client for Matrix and developed by the core team at
+   matrix.org. Element was formerly known as Riot.im, see the
+   <link xlink:href="https://element.io/blog/welcome-to-element/">Element introductory blog post</link>
+   for more information. The following snippet can be optionally added to the code before
+   to complete the synapse installation with a web client served at
+   <code>https://element.myhostname.example.org</code> and
+   <code>https://element.example.org</code>. Alternatively, you can use the hosted
+   copy at <link xlink:href="https://app.element.io/">https://app.element.io/</link>,
+   or use other web clients or native client applications. Due to the
+   <literal>/.well-known</literal> urls set up done above, many clients should
+   fill in the required connection details automatically when you enter your
+   Matrix Identifier. See
+   <link xlink:href="https://matrix.org/docs/projects/try-matrix-now.html">Try
+   Matrix Now!</link> for a list of existing clients and their supported
+   featureset.
+<programlisting>
+{
+  services.nginx.virtualHosts."element.${fqdn}" = {
+    <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
+    <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
+    <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [
+      "element.${config.networking.domain}"
+    ];
+
+    <link linkend="opt-services.nginx.virtualHosts._name_.root">root</link> = pkgs.element-web.override {
+      conf = {
+        default_server_config."m.homeserver" = {
+          "base_url" = "https://${fqdn}";
+          "server_name" = "${fqdn}";
+        };
+      };
+    };
+  };
+}
+</programlisting>
+  </para>
+
+  <para>
+   Note that the Element developers do not recommend running Element and your Matrix
+   homeserver on the same fully-qualified domain name for security reasons. In
+   the example, this means that you should not reuse the
+   <literal>myhostname.example.org</literal> virtualHost to also serve Element,
+   but instead serve it on a different subdomain, like
+   <literal>element.example.org</literal> in the example. See the
+   <link xlink:href="https://github.com/vector-im/riot-web#important-security-note">Element
+   Important Security Notes</link> for more information on this subject.
+  </para>
+ </section>
+</chapter>
diff --git a/nixos/modules/services/matrix/mjolnir.nix b/nixos/modules/services/matrix/mjolnir.nix
new file mode 100644
index 00000000000..278924b05cf
--- /dev/null
+++ b/nixos/modules/services/matrix/mjolnir.nix
@@ -0,0 +1,242 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.mjolnir;
+
+  yamlConfig = {
+    inherit (cfg) dataPath managementRoom protectedRooms;
+
+    accessToken = "@ACCESS_TOKEN@"; # will be replaced in "generateConfig"
+    homeserverUrl =
+      if cfg.pantalaimon.enable then
+        "http://${cfg.pantalaimon.options.listenAddress}:${toString cfg.pantalaimon.options.listenPort}"
+      else
+        cfg.homeserverUrl;
+
+    rawHomeserverUrl = cfg.homeserverUrl;
+
+    pantalaimon = {
+      inherit (cfg.pantalaimon) username;
+
+      use = cfg.pantalaimon.enable;
+      password = "@PANTALAIMON_PASSWORD@"; # will be replaced in "generateConfig"
+    };
+  };
+
+  moduleConfigFile = pkgs.writeText "module-config.yaml" (
+    generators.toYAML { } (filterAttrs (_: v: v != null)
+      (fold recursiveUpdate { } [ yamlConfig cfg.settings ])));
+
+  # these config files will be merged one after the other to build the final config
+  configFiles = [
+    "${pkgs.mjolnir}/share/mjolnir/config/default.yaml"
+    moduleConfigFile
+  ];
+
+  # this will generate the default.yaml file with all configFiles as inputs and
+  # replace all secret strings using replace-secret
+  generateConfig = pkgs.writeShellScript "mjolnir-generate-config" (
+    let
+      yqEvalStr = concatImapStringsSep " * " (pos: _: "select(fileIndex == ${toString (pos - 1)})") configFiles;
+      yqEvalArgs = concatStringsSep " " configFiles;
+    in
+    ''
+      set -euo pipefail
+
+      umask 077
+
+      # mjolnir will try to load a config from "./config/default.yaml" in the working directory
+      # -> let's place the generated config there
+      mkdir -p ${cfg.dataPath}/config
+
+      # merge all config files into one, overriding settings of the previous one with the next config
+      # e.g. "eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' filea.yaml fileb.yaml" will merge filea.yaml with fileb.yaml
+      ${pkgs.yq-go}/bin/yq eval-all -P '${yqEvalStr}' ${yqEvalArgs} > ${cfg.dataPath}/config/default.yaml
+
+      ${optionalString (cfg.accessTokenFile != null) ''
+        ${pkgs.replace-secret}/bin/replace-secret '@ACCESS_TOKEN@' '${cfg.accessTokenFile}' ${cfg.dataPath}/config/default.yaml
+      ''}
+      ${optionalString (cfg.pantalaimon.passwordFile != null) ''
+        ${pkgs.replace-secret}/bin/replace-secret '@PANTALAIMON_PASSWORD@' '${cfg.pantalaimon.passwordFile}' ${cfg.dataPath}/config/default.yaml
+      ''}
+    ''
+  );
+in
+{
+  options.services.mjolnir = {
+    enable = mkEnableOption "Mjolnir, a moderation tool for Matrix";
+
+    homeserverUrl = mkOption {
+      type = types.str;
+      default = "https://matrix.org";
+      description = ''
+        Where the homeserver is located (client-server URL).
+
+        If <literal>pantalaimon.enable</literal> is <literal>true</literal>, this option will become the homeserver to which <literal>pantalaimon</literal> connects.
+        The listen address of <literal>pantalaimon</literal> will then become the <literal>homeserverUrl</literal> of <literal>mjolnir</literal>.
+      '';
+    };
+
+    accessTokenFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      description = ''
+        File containing the matrix access token for the <literal>mjolnir</literal> user.
+      '';
+    };
+
+    pantalaimon = mkOption {
+      description = ''
+        <literal>pantalaimon</literal> options (enables E2E Encryption support).
+
+        This will create a <literal>pantalaimon</literal> instance with the name "mjolnir".
+      '';
+      default = { };
+      type = types.submodule {
+        options = {
+          enable = mkEnableOption ''
+            If true, accessToken is ignored and the username/password below will be
+            used instead. The access token of the bot will be stored in the dataPath.
+          '';
+
+          username = mkOption {
+            type = types.str;
+            description = "The username to login with.";
+          };
+
+          passwordFile = mkOption {
+            type = with types; nullOr path;
+            default = null;
+            description = ''
+              File containing the matrix password for the <literal>mjolnir</literal> user.
+            '';
+          };
+
+          options = mkOption {
+            type = types.submodule (import ./pantalaimon-options.nix);
+            default = { };
+            description = ''
+              passthrough additional options to the <literal>pantalaimon</literal> service.
+            '';
+          };
+        };
+      };
+    };
+
+    dataPath = mkOption {
+      type = types.path;
+      default = "/var/lib/mjolnir";
+      description = ''
+        The directory the bot should store various bits of information in.
+      '';
+    };
+
+    managementRoom = mkOption {
+      type = types.str;
+      default = "#moderators:example.org";
+      description = ''
+        The room ID where people can use the bot. The bot has no access controls, so
+        anyone in this room can use the bot - secure your room!
+        This should be a room alias or room ID - not a matrix.to URL.
+        Note: <literal>mjolnir</literal> is fairly verbose - expect a lot of messages from it.
+      '';
+    };
+
+    protectedRooms = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = literalExpression ''
+        [
+          "https://matrix.to/#/#yourroom:example.org"
+          "https://matrix.to/#/#anotherroom:example.org"
+        ]
+      '';
+      description = ''
+        A list of rooms to protect (matrix.to URLs).
+      '';
+    };
+
+    settings = mkOption {
+      default = { };
+      type = (pkgs.formats.yaml { }).type;
+      example = literalExpression ''
+        {
+          autojoinOnlyIfManager = true;
+          automaticallyRedactForReasons = [ "spam" "advertising" ];
+        }
+      '';
+      description = ''
+        Additional settings (see <link xlink:href="https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml">mjolnir default config</link> for available settings). These settings will override settings made by the module config.
+      '';
+    };
+  };
+
+  config = mkIf config.services.mjolnir.enable {
+    assertions = [
+      {
+        assertion = !(cfg.pantalaimon.enable && cfg.pantalaimon.passwordFile == null);
+        message = "Specify pantalaimon.passwordFile";
+      }
+      {
+        assertion = !(cfg.pantalaimon.enable && cfg.accessTokenFile != null);
+        message = "Do not specify accessTokenFile when using pantalaimon";
+      }
+      {
+        assertion = !(!cfg.pantalaimon.enable && cfg.accessTokenFile == null);
+        message = "Specify accessTokenFile when not using pantalaimon";
+      }
+    ];
+
+    services.pantalaimon-headless.instances."mjolnir" = mkIf cfg.pantalaimon.enable
+      {
+        homeserver = cfg.homeserverUrl;
+      } // cfg.pantalaimon.options;
+
+    systemd.services.mjolnir = {
+      description = "mjolnir - a moderation tool for Matrix";
+      wants = [ "network-online.target" ] ++ optionals (cfg.pantalaimon.enable) [ "pantalaimon-mjolnir.service" ];
+      after = [ "network-online.target" ] ++ optionals (cfg.pantalaimon.enable) [ "pantalaimon-mjolnir.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = ''${pkgs.mjolnir}/bin/mjolnir'';
+        ExecStartPre = [ generateConfig ];
+        WorkingDirectory = cfg.dataPath;
+        StateDirectory = "mjolnir";
+        StateDirectoryMode = "0700";
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        User = "mjolnir";
+        Restart = "on-failure";
+
+        /* TODO: wait for #102397 to be resolved. Then load secrets from $CREDENTIALS_DIRECTORY+"/NAME"
+        DynamicUser = true;
+        LoadCredential = [] ++
+          optionals (cfg.accessTokenFile != null) [
+            "access_token:${cfg.accessTokenFile}"
+          ] ++
+          optionals (cfg.pantalaimon.passwordFile != null) [
+            "pantalaimon_password:${cfg.pantalaimon.passwordFile}"
+          ];
+        */
+      };
+    };
+
+    users = {
+      users.mjolnir = {
+        group = "mjolnir";
+        isSystemUser = true;
+      };
+      groups.mjolnir = { };
+    };
+  };
+
+  meta = {
+    doc = ./mjolnir.xml;
+    maintainers = with maintainers; [ jojosch ];
+  };
+}
diff --git a/nixos/modules/services/matrix/mjolnir.xml b/nixos/modules/services/matrix/mjolnir.xml
new file mode 100644
index 00000000000..b07abe33979
--- /dev/null
+++ b/nixos/modules/services/matrix/mjolnir.xml
@@ -0,0 +1,134 @@
+<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/matrix/pantalaimon-options.nix b/nixos/modules/services/matrix/pantalaimon-options.nix
new file mode 100644
index 00000000000..035c57540d0
--- /dev/null
+++ b/nixos/modules/services/matrix/pantalaimon-options.nix
@@ -0,0 +1,70 @@
+{ config, lib, name, ... }:
+
+with lib;
+{
+  options = {
+    dataPath = mkOption {
+      type = types.path;
+      default = "/var/lib/pantalaimon-${name}";
+      description = ''
+        The directory where <literal>pantalaimon</literal> should store its state such as the database file.
+      '';
+    };
+
+    logLevel = mkOption {
+      type = types.enum [ "info" "warning" "error" "debug" ];
+      default = "warning";
+      description = ''
+        Set the log level of the daemon.
+      '';
+    };
+
+    homeserver = mkOption {
+      type = types.str;
+      example = "https://matrix.org";
+      description = ''
+        The URI of the homeserver that the <literal>pantalaimon</literal> proxy should
+        forward requests to, without the matrix API path but including
+        the http(s) schema.
+      '';
+    };
+
+    ssl = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether or not SSL verification should be enabled for outgoing
+        connections to the homeserver.
+      '';
+    };
+
+    listenAddress = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = ''
+        The address where the daemon will listen to client connections
+        for this homeserver.
+      '';
+    };
+
+    listenPort = mkOption {
+      type = types.port;
+      default = 8009;
+      description = ''
+        The port where the daemon will listen to client connections for
+        this homeserver. Note that the listen address/port combination
+        needs to be unique between different homeservers.
+      '';
+    };
+
+    extraSettings = mkOption {
+      type = types.attrs;
+      default = { };
+      description = ''
+        Extra configuration options. See
+        <link xlink:href="https://github.com/matrix-org/pantalaimon/blob/master/docs/man/pantalaimon.5.md">pantalaimon(5)</link>
+        for available options.
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/matrix/pantalaimon.nix b/nixos/modules/services/matrix/pantalaimon.nix
new file mode 100644
index 00000000000..63b40099ca5
--- /dev/null
+++ b/nixos/modules/services/matrix/pantalaimon.nix
@@ -0,0 +1,70 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.pantalaimon-headless;
+
+  iniFmt = pkgs.formats.ini { };
+
+  mkConfigFile = name: instanceConfig: iniFmt.generate "pantalaimon.conf" {
+    Default = {
+      LogLevel = instanceConfig.logLevel;
+      Notifications = false;
+    };
+
+    ${name} = (recursiveUpdate
+      {
+        Homeserver = instanceConfig.homeserver;
+        ListenAddress = instanceConfig.listenAddress;
+        ListenPort = instanceConfig.listenPort;
+        SSL = instanceConfig.ssl;
+
+        # Set some settings to prevent user interaction for headless operation
+        IgnoreVerification = true;
+        UseKeyring = false;
+      }
+      instanceConfig.extraSettings
+    );
+  };
+
+  mkPantalaimonService = name: instanceConfig:
+    nameValuePair "pantalaimon-${name}" {
+      description = "pantalaimon instance ${name} - E2EE aware proxy daemon for matrix clients";
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = ''${pkgs.pantalaimon-headless}/bin/pantalaimon --config ${mkConfigFile name instanceConfig} --data-path ${instanceConfig.dataPath}'';
+        Restart = "on-failure";
+        DynamicUser = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectSystem = "strict";
+        StateDirectory = "pantalaimon-${name}";
+      };
+    };
+in
+{
+  options.services.pantalaimon-headless.instances = mkOption {
+    default = { };
+    type = types.attrsOf (types.submodule (import ./pantalaimon-options.nix));
+    description = ''
+      Declarative instance config.
+
+      Note: to use pantalaimon interactively, e.g. for a Matrix client which does not
+      support End-to-end encryption (like <literal>fractal</literal>), refer to the home-manager module.
+    '';
+  };
+
+  config = mkIf (config.services.pantalaimon-headless.instances != { })
+    {
+      systemd.services = mapAttrs' mkPantalaimonService config.services.pantalaimon-headless.instances;
+    };
+
+  meta = {
+    maintainers = with maintainers; [ jojosch ];
+  };
+}
diff --git a/nixos/modules/services/misc/airsonic.nix b/nixos/modules/services/misc/airsonic.nix
new file mode 100644
index 00000000000..2b9c6d80abb
--- /dev/null
+++ b/nixos/modules/services/misc/airsonic.nix
@@ -0,0 +1,179 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.airsonic;
+  opt = options.services.airsonic;
+in {
+  options = {
+
+    services.airsonic = {
+      enable = mkEnableOption "Airsonic, the Free and Open Source media streaming server (fork of Subsonic and Libresonic)";
+
+      user = mkOption {
+        type = types.str;
+        default = "airsonic";
+        description = "User account under which airsonic runs.";
+      };
+
+      home = mkOption {
+        type = types.path;
+        default = "/var/lib/airsonic";
+        description = ''
+          The directory where Airsonic will create files.
+          Make sure it is writable.
+        '';
+      };
+
+      virtualHost = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
+        '';
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = ''
+          The host name or IP address on which to bind Airsonic.
+          The default value is appropriate for first launch, when the
+          default credentials are easy to guess. It is also appropriate
+          if you intend to use the virtualhost option in the service
+          module. In other cases, you may want to change this to a
+          specific IP or 0.0.0.0 to listen on all interfaces.
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 4040;
+        description = ''
+          The port on which Airsonic will listen for
+          incoming HTTP traffic. Set to 0 to disable.
+        '';
+      };
+
+      contextPath = mkOption {
+        type = types.path;
+        default = "/";
+        description = ''
+          The context path, i.e., the last part of the Airsonic
+          URL. Typically '/' or '/airsonic'. Default '/'
+        '';
+      };
+
+      maxMemory = mkOption {
+        type = types.int;
+        default = 100;
+        description = ''
+          The memory limit (max Java heap size) in megabytes.
+          Default: 100
+        '';
+      };
+
+      transcoders = mkOption {
+        type = types.listOf types.path;
+        default = [ "${pkgs.ffmpeg.bin}/bin/ffmpeg" ];
+        defaultText = literalExpression ''[ "''${pkgs.ffmpeg.bin}/bin/ffmpeg" ]'';
+        description = ''
+          List of paths to transcoder executables that should be accessible
+          from Airsonic. Symlinks will be created to each executable inside
+          ''${config.${opt.home}}/transcoders.
+        '';
+      };
+
+      jre = mkOption {
+        type = types.package;
+        default = pkgs.jre8;
+        defaultText = literalExpression "pkgs.jre8";
+        description = ''
+          JRE package to use.
+
+          Airsonic only supports Java 8, airsonic-advanced requires at least
+          Java 11.
+        '';
+      };
+
+      war = mkOption {
+        type = types.path;
+        default = "${pkgs.airsonic}/webapps/airsonic.war";
+        defaultText = literalExpression ''"''${pkgs.airsonic}/webapps/airsonic.war"'';
+        description = "Airsonic war file to use.";
+      };
+
+      jvmOptions = mkOption {
+        description = ''
+          Extra command line options for the JVM running AirSonic.
+          Useful for sending jukebox output to non-default alsa
+          devices.
+        '';
+        default = [
+        ];
+        type = types.listOf types.str;
+        example = [
+          "-Djavax.sound.sampled.Clip='#CODEC [plughw:1,0]'"
+          "-Djavax.sound.sampled.Port='#Port CODEC [hw:1]'"
+          "-Djavax.sound.sampled.SourceDataLine='#CODEC [plughw:1,0]'"
+          "-Djavax.sound.sampled.TargetDataLine='#CODEC [plughw:1,0]'"
+        ];
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.airsonic = {
+      description = "Airsonic Media Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        # Install transcoders.
+        rm -rf ${cfg.home}/transcode
+        mkdir -p ${cfg.home}/transcode
+        for exe in ${toString cfg.transcoders}; do
+          ln -sf "$exe" ${cfg.home}/transcode
+        done
+      '';
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.jre}/bin/java -Xmx${toString cfg.maxMemory}m \
+          -Dairsonic.home=${cfg.home} \
+          -Dserver.address=${cfg.listenAddress} \
+          -Dserver.port=${toString cfg.port} \
+          -Dairsonic.contextPath=${cfg.contextPath} \
+          -Djava.awt.headless=true \
+          ${optionalString (cfg.virtualHost != null)
+            "-Dserver.use-forward-headers=true"} \
+          ${toString cfg.jvmOptions} \
+          -verbose:gc \
+          -jar ${cfg.war}
+        '';
+        Restart = "always";
+        User = "airsonic";
+        UMask = "0022";
+      };
+    };
+
+    services.nginx = mkIf (cfg.virtualHost != null) {
+      enable = true;
+      recommendedProxySettings = true;
+      virtualHosts.${cfg.virtualHost} = {
+        locations.${cfg.contextPath}.proxyPass = "http://${cfg.listenAddress}:${toString cfg.port}";
+      };
+    };
+
+    users.users.airsonic = {
+      description = "Airsonic service user";
+      group = "airsonic";
+      name = cfg.user;
+      home = cfg.home;
+      createHome = true;
+      isSystemUser = true;
+    };
+    users.groups.airsonic = {};
+  };
+}
diff --git a/nixos/modules/services/misc/ananicy.nix b/nixos/modules/services/misc/ananicy.nix
new file mode 100644
index 00000000000..191666bc362
--- /dev/null
+++ b/nixos/modules/services/misc/ananicy.nix
@@ -0,0 +1,107 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ananicy;
+  configFile = pkgs.writeText "ananicy.conf" (generators.toKeyValue { } cfg.settings);
+  extraRules = pkgs.writeText "extraRules" cfg.extraRules;
+  servicename = if ((lib.getName cfg.package) == (lib.getName pkgs.ananicy-cpp)) then "ananicy-cpp" else "ananicy";
+in
+{
+  options = {
+    services.ananicy = {
+      enable = mkEnableOption "Ananicy, an auto nice daemon";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.ananicy;
+        defaultText = literalExpression "pkgs.ananicy";
+        example = literalExpression "pkgs.ananicy-cpp";
+        description = ''
+          Which ananicy package to use.
+        '';
+      };
+
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ int bool str ]);
+        default = { };
+        example = {
+          apply_nice = false;
+        };
+        description = ''
+          See <link xlink:href="https://github.com/Nefelim4ag/Ananicy/blob/master/ananicy.d/ananicy.conf"/>
+        '';
+      };
+
+      extraRules = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Extra rules in json format on separate lines. See:
+          <link xlink:href="https://github.com/Nefelim4ag/Ananicy#configuration"/>
+          <link xlink:href="https://gitlab.com/ananicy-cpp/ananicy-cpp/#global-configuration"/>
+        '';
+        example = literalExpression ''
+          '''
+            { "name": "eog", "type": "Image-View" }
+            { "name": "fdupes", "type": "BG_CPUIO" }
+          '''
+        '';
+
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment = {
+      systemPackages = [ cfg.package ];
+      etc."ananicy.d".source = pkgs.runCommandLocal "ananicyfiles" { } ''
+        mkdir -p $out
+        # ananicy-cpp does not include rules or settings on purpose
+        cp -r ${pkgs.ananicy}/etc/ananicy.d/* $out
+        rm $out/ananicy.conf
+        cp ${configFile} $out/ananicy.conf
+        ${optionalString (cfg.extraRules != "") "cp ${extraRules} $out/nixRules.rules"}
+      '';
+    };
+
+    # ananicy and ananicy-cpp have different default settings
+    services.ananicy.settings =
+      let
+        mkOD = mkOptionDefault;
+      in
+      {
+        cgroup_load = mkOD true;
+        type_load = mkOD true;
+        rule_load = mkOD true;
+        apply_nice = mkOD true;
+        apply_ioclass = mkOD true;
+        apply_ionice = mkOD true;
+        apply_sched = mkOD true;
+        apply_oom_score_adj = mkOD true;
+        apply_cgroup = mkOD true;
+      } // (if ((lib.getName cfg.package) == (lib.getName pkgs.ananicy-cpp)) then {
+        # https://gitlab.com/ananicy-cpp/ananicy-cpp/-/blob/master/src/config.cpp#L12
+        loglevel = mkOD "warn"; # default is info but its spammy
+        cgroup_realtime_workaround = mkOD config.systemd.enableUnifiedCgroupHierarchy;
+      } else {
+        # https://github.com/Nefelim4ag/Ananicy/blob/master/ananicy.d/ananicy.conf
+        check_disks_schedulers = mkOD true;
+        check_freq = mkOD 5;
+      });
+
+    systemd = {
+      # https://gitlab.com/ananicy-cpp/ananicy-cpp/#cgroups applies to both ananicy and -cpp
+      enableUnifiedCgroupHierarchy = mkDefault false;
+      packages = [ cfg.package ];
+      services."${servicename}" = {
+        wantedBy = [ "default.target" ];
+      };
+    };
+  };
+
+  meta = {
+    maintainers = with maintainers; [ artturin ];
+  };
+}
diff --git a/nixos/modules/services/misc/ankisyncd.nix b/nixos/modules/services/misc/ankisyncd.nix
new file mode 100644
index 00000000000..69e471f4f57
--- /dev/null
+++ b/nixos/modules/services/misc/ankisyncd.nix
@@ -0,0 +1,79 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ankisyncd;
+
+  name = "ankisyncd";
+
+  stateDir = "/var/lib/${name}";
+
+  authDbPath = "${stateDir}/auth.db";
+
+  sessionDbPath = "${stateDir}/session.db";
+
+  configFile = pkgs.writeText "ankisyncd.conf" (lib.generators.toINI {} {
+    sync_app = {
+      host = cfg.host;
+      port = cfg.port;
+      data_root = stateDir;
+      auth_db_path = authDbPath;
+      session_db_path = sessionDbPath;
+
+      base_url = "/sync/";
+      base_media_url = "/msync/";
+    };
+  });
+in
+  {
+    options.services.ankisyncd = {
+      enable = mkEnableOption "ankisyncd";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.ankisyncd;
+        defaultText = literalExpression "pkgs.ankisyncd";
+        description = "The package to use for the ankisyncd command.";
+      };
+
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "ankisyncd host";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 27701;
+        description = "ankisyncd port";
+      };
+
+      openFirewall = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to open the firewall for the specified port.";
+      };
+    };
+
+    config = mkIf cfg.enable {
+      networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
+      environment.etc."ankisyncd/ankisyncd.conf".source = configFile;
+
+      systemd.services.ankisyncd = {
+        description = "ankisyncd - Anki sync server";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        path = [ cfg.package ];
+
+        serviceConfig = {
+          Type = "simple";
+          DynamicUser = true;
+          StateDirectory = name;
+          ExecStart = "${cfg.package}/bin/ankisyncd";
+          Restart = "always";
+        };
+      };
+    };
+  }
diff --git a/nixos/modules/services/misc/apache-kafka.nix b/nixos/modules/services/misc/apache-kafka.nix
new file mode 100644
index 00000000000..d1856fff4aa
--- /dev/null
+++ b/nixos/modules/services/misc/apache-kafka.nix
@@ -0,0 +1,151 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.apache-kafka;
+
+  serverProperties =
+    if cfg.serverProperties != null then
+      cfg.serverProperties
+    else
+      ''
+        # Generated by nixos
+        broker.id=${toString cfg.brokerId}
+        port=${toString cfg.port}
+        host.name=${cfg.hostname}
+        log.dirs=${concatStringsSep "," cfg.logDirs}
+        zookeeper.connect=${cfg.zookeeper}
+        ${toString cfg.extraProperties}
+      '';
+
+  serverConfig = pkgs.writeText "server.properties" serverProperties;
+  logConfig = pkgs.writeText "log4j.properties" cfg.log4jProperties;
+
+in {
+
+  options.services.apache-kafka = {
+    enable = mkOption {
+      description = "Whether to enable Apache Kafka.";
+      default = false;
+      type = types.bool;
+    };
+
+    brokerId = mkOption {
+      description = "Broker ID.";
+      default = -1;
+      type = types.int;
+    };
+
+    port = mkOption {
+      description = "Port number the broker should listen on.";
+      default = 9092;
+      type = types.int;
+    };
+
+    hostname = mkOption {
+      description = "Hostname the broker should bind to.";
+      default = "localhost";
+      type = types.str;
+    };
+
+    logDirs = mkOption {
+      description = "Log file directories";
+      default = [ "/tmp/kafka-logs" ];
+      type = types.listOf types.path;
+    };
+
+    zookeeper = mkOption {
+      description = "Zookeeper connection string";
+      default = "localhost:2181";
+      type = types.str;
+    };
+
+    extraProperties = mkOption {
+      description = "Extra properties for server.properties.";
+      type = types.nullOr types.lines;
+      default = null;
+    };
+
+    serverProperties = mkOption {
+      description = ''
+        Complete server.properties content. Other server.properties config
+        options will be ignored if this option is used.
+      '';
+      type = types.nullOr types.lines;
+      default = null;
+    };
+
+    log4jProperties = mkOption {
+      description = "Kafka log4j property configuration.";
+      default = ''
+        log4j.rootLogger=INFO, stdout
+
+        log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+        log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+        log4j.appender.stdout.layout.ConversionPattern=[%d] %p %m (%c)%n
+      '';
+      type = types.lines;
+    };
+
+    jvmOptions = mkOption {
+      description = "Extra command line options for the JVM running Kafka.";
+      default = [];
+      type = types.listOf types.str;
+      example = [
+        "-Djava.net.preferIPv4Stack=true"
+        "-Dcom.sun.management.jmxremote"
+        "-Dcom.sun.management.jmxremote.local.only=true"
+      ];
+    };
+
+    package = mkOption {
+      description = "The kafka package to use";
+      default = pkgs.apacheKafka;
+      defaultText = literalExpression "pkgs.apacheKafka";
+      type = types.package;
+    };
+
+    jre = mkOption {
+      description = "The JRE with which to run Kafka";
+      default = cfg.package.passthru.jre;
+      defaultText = literalExpression "pkgs.apacheKafka.passthru.jre";
+      type = types.package;
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [cfg.package];
+
+    users.users.apache-kafka = {
+      isSystemUser = true;
+      group = "apache-kafka";
+      description = "Apache Kafka daemon user";
+      home = head cfg.logDirs;
+    };
+    users.groups.apache-kafka = {};
+
+    systemd.tmpfiles.rules = map (logDir: "d '${logDir}' 0700 apache-kafka - - -") cfg.logDirs;
+
+    systemd.services.apache-kafka = {
+      description = "Apache Kafka Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.jre}/bin/java \
+            -cp "${cfg.package}/libs/*" \
+            -Dlog4j.configuration=file:${logConfig} \
+            ${toString cfg.jvmOptions} \
+            kafka.Kafka \
+            ${serverConfig}
+        '';
+        User = "apache-kafka";
+        SuccessExitStatus = "0 143";
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/misc/autofs.nix b/nixos/modules/services/misc/autofs.nix
new file mode 100644
index 00000000000..5fce990afec
--- /dev/null
+++ b/nixos/modules/services/misc/autofs.nix
@@ -0,0 +1,100 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.autofs;
+
+  autoMaster = pkgs.writeText "auto.master" cfg.autoMaster;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.autofs = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Mount filesystems on demand. Unmount them automatically.
+          You may also be interested in afuse.
+        '';
+      };
+
+      autoMaster = mkOption {
+        type = types.str;
+        example = literalExpression ''
+          let
+            mapConf = pkgs.writeText "auto" '''
+             kernel    -ro,soft,intr       ftp.kernel.org:/pub/linux
+             boot      -fstype=ext2        :/dev/hda1
+             windoze   -fstype=smbfs       ://windoze/c
+             removable -fstype=ext2        :/dev/hdd
+             cd        -fstype=iso9660,ro  :/dev/hdc
+             floppy    -fstype=auto        :/dev/fd0
+             server    -rw,hard,intr       / -ro myserver.me.org:/ \
+                                           /usr myserver.me.org:/usr \
+                                           /home myserver.me.org:/home
+            ''';
+          in '''
+            /auto file:''${mapConf}
+          '''
+        '';
+        description = ''
+          Contents of <literal>/etc/auto.master</literal> file. See <command>auto.master(5)</command> and <command>autofs(5)</command>.
+        '';
+      };
+
+      timeout = mkOption {
+        type = types.int;
+        default = 600;
+        description = "Set the global minimum timeout, in seconds, until directories are unmounted";
+      };
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Pass -d and -7 to automount and write log to the system journal.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    boot.kernelModules = [ "autofs4" ];
+
+    systemd.services.autofs =
+      { description = "Automounts filesystems on demand";
+        after = [ "network.target" "ypbind.service" "sssd.service" "network-online.target" ];
+        wants = [ "network-online.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        preStart = ''
+          # There should be only one autofs service managed by systemd, so this should be safe.
+          rm -f /tmp/autofs-running
+        '';
+
+        serviceConfig = {
+          Type = "forking";
+          PIDFile = "/run/autofs.pid";
+          ExecStart = "${pkgs.autofs5}/bin/automount ${optionalString cfg.debug "-d"} -p /run/autofs.pid -t ${builtins.toString cfg.timeout} ${autoMaster}";
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        };
+      };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/autorandr.nix b/nixos/modules/services/misc/autorandr.nix
new file mode 100644
index 00000000000..a65c5c9d11c
--- /dev/null
+++ b/nixos/modules/services/misc/autorandr.nix
@@ -0,0 +1,53 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.autorandr;
+
+in {
+
+  options = {
+
+    services.autorandr = {
+      enable = mkEnableOption "handling of hotplug and sleep events by autorandr";
+
+      defaultTarget = mkOption {
+        default = "default";
+        type = types.str;
+        description = ''
+          Fallback if no monitor layout can be detected. See the docs
+          (https://github.com/phillipberndt/autorandr/blob/v1.0/README.md#how-to-use)
+          for further reference.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    services.udev.packages = [ pkgs.autorandr ];
+
+    environment.systemPackages = [ pkgs.autorandr ];
+
+    systemd.services.autorandr = {
+      wantedBy = [ "sleep.target" ];
+      description = "Autorandr execution hook";
+      after = [ "sleep.target" ];
+
+      startLimitIntervalSec = 5;
+      startLimitBurst = 1;
+      serviceConfig = {
+        ExecStart = "${pkgs.autorandr}/bin/autorandr --batch --change --default ${cfg.defaultTarget}";
+        Type = "oneshot";
+        RemainAfterExit = false;
+        KillMode = "process";
+      };
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ ];
+}
diff --git a/nixos/modules/services/misc/bazarr.nix b/nixos/modules/services/misc/bazarr.nix
new file mode 100644
index 00000000000..99343a146a7
--- /dev/null
+++ b/nixos/modules/services/misc/bazarr.nix
@@ -0,0 +1,77 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.bazarr;
+in
+{
+  options = {
+    services.bazarr = {
+      enable = mkEnableOption "bazarr, a subtitle manager for Sonarr and Radarr";
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the bazarr web interface.";
+      };
+
+      listenPort = mkOption {
+        type = types.port;
+        default = 6767;
+        description = "Port on which the bazarr web interface should listen";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "bazarr";
+        description = "User account under which bazarr runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "bazarr";
+        description = "Group under which bazarr runs.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.bazarr = {
+      description = "bazarr";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = rec {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        StateDirectory = "bazarr";
+        SyslogIdentifier = "bazarr";
+        ExecStart = pkgs.writeShellScript "start-bazarr" ''
+          ${pkgs.bazarr}/bin/bazarr \
+            --config '/var/lib/${StateDirectory}' \
+            --port ${toString cfg.listenPort} \
+            --no-update True
+        '';
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listenPort ];
+    };
+
+    users.users = mkIf (cfg.user == "bazarr") {
+      bazarr = {
+        isSystemUser = true;
+        group = cfg.group;
+        home = "/var/lib/${config.systemd.services.bazarr.serviceConfig.StateDirectory}";
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "bazarr") {
+      bazarr = {};
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/beanstalkd.nix b/nixos/modules/services/misc/beanstalkd.nix
new file mode 100644
index 00000000000..1c674a5b23b
--- /dev/null
+++ b/nixos/modules/services/misc/beanstalkd.nix
@@ -0,0 +1,63 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.beanstalkd;
+  pkg = pkgs.beanstalkd;
+in
+
+{
+  # interface
+
+  options = {
+    services.beanstalkd = {
+      enable = mkEnableOption "the Beanstalk work queue";
+
+      listen = {
+        port = mkOption {
+          type = types.int;
+          description = "TCP port that will be used to accept client connections.";
+          default = 11300;
+        };
+
+        address = mkOption {
+          type = types.str;
+          description = "IP address to listen on.";
+          default = "127.0.0.1";
+          example = "0.0.0.0";
+        };
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to open ports in the firewall for the server.";
+      };
+    };
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listen.port ];
+    };
+
+    environment.systemPackages = [ pkg ];
+
+    systemd.services.beanstalkd = {
+      description = "Beanstalk Work Queue";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+        ExecStart = "${pkg}/bin/beanstalkd -l ${cfg.listen.address} -p ${toString cfg.listen.port} -b $STATE_DIRECTORY";
+        StateDirectory = "beanstalkd";
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/misc/bees.nix b/nixos/modules/services/misc/bees.nix
new file mode 100644
index 00000000000..fa00d7e4f55
--- /dev/null
+++ b/nixos/modules/services/misc/bees.nix
@@ -0,0 +1,132 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.beesd;
+
+  logLevels = { emerg = 0; alert = 1; crit = 2; err = 3; warning = 4; notice = 5; info = 6; debug = 7; };
+
+  fsOptions = with types; {
+    options.spec = mkOption {
+      type = str;
+      description = ''
+        Description of how to identify the filesystem to be duplicated by this
+        instance of bees. Note that deduplication crosses subvolumes; one must
+        not configure multiple instances for subvolumes of the same filesystem
+        (or block devices which are part of the same filesystem), but only for
+        completely independent btrfs filesystems.
+        </para>
+        <para>
+        This must be in a format usable by findmnt; that could be a key=value
+        pair, or a bare path to a mount point.
+        Using bare paths will allow systemd to start the beesd service only
+        after mounting the associated path.
+      '';
+      example = "LABEL=MyBulkDataDrive";
+    };
+    options.hashTableSizeMB = mkOption {
+      type = types.addCheck types.int (n: mod n 16 == 0);
+      default = 1024; # 1GB; default from upstream beesd script
+      description = ''
+        Hash table size in MB; must be a multiple of 16.
+        </para>
+        <para>
+        A larger ratio of index size to storage size means smaller blocks of
+        duplicate content are recognized.
+        </para>
+        <para>
+        If you have 1TB of data, a 4GB hash table (which is to say, a value of
+        4096) will permit 4KB extents (the smallest possible size) to be
+        recognized, whereas a value of 1024 -- creating a 1GB hash table --
+        will recognize only aligned duplicate blocks of 16KB.
+      '';
+    };
+    options.verbosity = mkOption {
+      type = types.enum (attrNames logLevels ++ attrValues logLevels);
+      apply = v: if isString v then logLevels.${v} else v;
+      default = "info";
+      description = "Log verbosity (syslog keyword/level).";
+    };
+    options.workDir = mkOption {
+      type = str;
+      default = ".beeshome";
+      description = ''
+        Name (relative to the root of the filesystem) of the subvolume where
+        the hash table will be stored.
+      '';
+    };
+    options.extraOptions = mkOption {
+      type = listOf str;
+      default = [ ];
+      description = ''
+        Extra command-line options passed to the daemon. See upstream bees documentation.
+      '';
+      example = literalExpression ''
+        [ "--thread-count" "4" ]
+      '';
+    };
+  };
+
+in
+{
+
+  options.services.beesd = {
+    filesystems = mkOption {
+      type = with types; attrsOf (submodule fsOptions);
+      description = "BTRFS filesystems to run block-level deduplication on.";
+      default = { };
+      example = literalExpression ''
+        {
+          root = {
+            spec = "LABEL=root";
+            hashTableSizeMB = 2048;
+            verbosity = "crit";
+            extraOptions = [ "--loadavg-target" "5.0" ];
+          };
+        }
+      '';
+    };
+  };
+  config = {
+    systemd.services = mapAttrs'
+      (name: fs: nameValuePair "beesd@${name}" {
+        description = "Block-level BTRFS deduplication for %i";
+        after = [ "sysinit.target" ];
+
+        serviceConfig =
+          let
+            configOpts = [
+              fs.spec
+              "verbosity=${toString fs.verbosity}"
+              "idxSizeMB=${toString fs.hashTableSizeMB}"
+              "workDir=${fs.workDir}"
+            ];
+            configOptsStr = escapeShellArgs configOpts;
+          in
+          {
+            # Values from https://github.com/Zygo/bees/blob/v0.6.5/scripts/beesd@.service.in
+            ExecStart = "${pkgs.bees}/bin/bees-service-wrapper run ${configOptsStr} -- --no-timestamps ${escapeShellArgs fs.extraOptions}";
+            ExecStopPost = "${pkgs.bees}/bin/bees-service-wrapper cleanup ${configOptsStr}";
+            CPUAccounting = true;
+            CPUSchedulingPolicy = "batch";
+            CPUWeight = 12;
+            IOSchedulingClass = "idle";
+            IOSchedulingPriority = 7;
+            IOWeight = 10;
+            KillMode = "control-group";
+            KillSignal = "SIGTERM";
+            MemoryAccounting = true;
+            Nice = 19;
+            Restart = "on-abnormal";
+            StartupCPUWeight = 25;
+            StartupIOWeight = 25;
+            SyslogIdentifier = "beesd"; # would otherwise be "bees-service-wrapper"
+          };
+        unitConfig.RequiresMountsFor = lib.mkIf (lib.hasPrefix "/" fs.spec) fs.spec;
+        wantedBy = [ "multi-user.target" ];
+      })
+      cfg.filesystems;
+  };
+}
diff --git a/nixos/modules/services/misc/bepasty.nix b/nixos/modules/services/misc/bepasty.nix
new file mode 100644
index 00000000000..f69832e5b2b
--- /dev/null
+++ b/nixos/modules/services/misc/bepasty.nix
@@ -0,0 +1,179 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  gunicorn = pkgs.python3Packages.gunicorn;
+  bepasty = pkgs.bepasty;
+  gevent = pkgs.python3Packages.gevent;
+  python = pkgs.python3Packages.python;
+  cfg = config.services.bepasty;
+  user = "bepasty";
+  group = "bepasty";
+  default_home = "/var/lib/bepasty";
+in
+{
+  options.services.bepasty = {
+    enable = mkEnableOption "Bepasty servers";
+
+    servers = mkOption {
+      default = {};
+      description = ''
+        configure a number of bepasty servers which will be started with
+        gunicorn.
+        '';
+      type = with types ; attrsOf (submodule ({ config, ... } : {
+
+        options = {
+
+          bind = mkOption {
+            type = types.str;
+            description = ''
+              Bind address to be used for this server.
+              '';
+            example = "0.0.0.0:8000";
+            default = "127.0.0.1:8000";
+          };
+
+          dataDir = mkOption {
+            type = types.str;
+            description = ''
+              Path to the directory where the pastes will be saved to
+              '';
+            default = default_home+"/data";
+          };
+
+          defaultPermissions = mkOption {
+            type = types.str;
+            description = ''
+              default permissions for all unauthenticated accesses.
+              '';
+            example = "read,create,delete";
+            default = "read";
+          };
+
+          extraConfig = mkOption {
+            type = types.lines;
+            description = ''
+              Extra configuration for bepasty server to be appended on the
+              configuration.
+              see https://bepasty-server.readthedocs.org/en/latest/quickstart.html#configuring-bepasty
+              for all options.
+              '';
+            default = "";
+            example = ''
+              PERMISSIONS = {
+                'myadminsecret': 'admin,list,create,read,delete',
+              }
+              MAX_ALLOWED_FILE_SIZE = 5 * 1000 * 1000
+              '';
+          };
+
+          secretKey = mkOption {
+            type = types.str;
+            description = ''
+              server secret for safe session cookies, must be set.
+
+              Warning: this secret is stored in the WORLD-READABLE Nix store!
+
+              It's recommended to use <option>secretKeyFile</option>
+              which takes precedence over <option>secretKey</option>.
+              '';
+            default = "";
+          };
+
+          secretKeyFile = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            description = ''
+              A file that contains the server secret for safe session cookies, must be set.
+
+              <option>secretKeyFile</option> takes precedence over <option>secretKey</option>.
+
+              Warning: when <option>secretKey</option> is non-empty <option>secretKeyFile</option>
+              defaults to a file in the WORLD-READABLE Nix store containing that secret.
+              '';
+          };
+
+          workDir = mkOption {
+            type = types.str;
+            description = ''
+              Path to the working directory (used for config and pidfile).
+              Defaults to the users home directory.
+              '';
+            default = default_home;
+          };
+
+        };
+        config = {
+          secretKeyFile = mkDefault (
+            if config.secretKey != ""
+            then toString (pkgs.writeTextFile {
+              name = "bepasty-secret-key";
+              text = config.secretKey;
+            })
+            else null
+          );
+        };
+      }));
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ bepasty ];
+
+    # creates gunicorn systemd service for each configured server
+    systemd.services = mapAttrs' (name: server:
+      nameValuePair ("bepasty-server-${name}-gunicorn")
+        ({
+          description = "Bepasty Server ${name}";
+          wantedBy = [ "multi-user.target" ];
+          after = [ "network.target" ];
+          restartIfChanged = true;
+
+          environment = let
+            penv = python.buildEnv.override {
+              extraLibs = [ bepasty gevent ];
+            };
+          in {
+            BEPASTY_CONFIG = "${server.workDir}/bepasty-${name}.conf";
+            PYTHONPATH= "${penv}/${python.sitePackages}/";
+          };
+
+          serviceConfig = {
+            Type = "simple";
+            PrivateTmp = true;
+            ExecStartPre = assert server.secretKeyFile != null; pkgs.writeScript "bepasty-server.${name}-init" ''
+              #!/bin/sh
+              mkdir -p "${server.workDir}"
+              mkdir -p "${server.dataDir}"
+              chown ${user}:${group} "${server.workDir}" "${server.dataDir}"
+              cat > ${server.workDir}/bepasty-${name}.conf <<EOF
+              SITENAME="${name}"
+              STORAGE_FILESYSTEM_DIRECTORY="${server.dataDir}"
+              SECRET_KEY="$(cat "${server.secretKeyFile}")"
+              DEFAULT_PERMISSIONS="${server.defaultPermissions}"
+              ${server.extraConfig}
+              EOF
+            '';
+            ExecStart = ''${gunicorn}/bin/gunicorn bepasty.wsgi --name ${name} \
+              -u ${user} \
+              -g ${group} \
+              --workers 3 --log-level=info \
+              --bind=${server.bind} \
+              --pid ${server.workDir}/gunicorn-${name}.pid \
+              -k gevent
+            '';
+          };
+        })
+    ) cfg.servers;
+
+    users.users.${user} =
+      { uid = config.ids.uids.bepasty;
+        group = group;
+        home = default_home;
+      };
+
+    users.groups.${group}.gid = config.ids.gids.bepasty;
+  };
+}
diff --git a/nixos/modules/services/misc/calibre-server.nix b/nixos/modules/services/misc/calibre-server.nix
new file mode 100644
index 00000000000..2467d34b524
--- /dev/null
+++ b/nixos/modules/services/misc/calibre-server.nix
@@ -0,0 +1,86 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.calibre-server;
+
+in
+
+{
+  imports = [
+    (mkChangedOptionModule [ "services" "calibre-server" "libraryDir" ] [ "services" "calibre-server" "libraries" ]
+      (config:
+        let libraryDir = getAttrFromPath [ "services" "calibre-server" "libraryDir" ] config;
+        in [ libraryDir ]
+      )
+    )
+  ];
+
+  ###### interface
+
+  options = {
+    services.calibre-server = {
+
+      enable = mkEnableOption "calibre-server";
+
+      libraries = mkOption {
+        description = ''
+          The directories of the libraries to serve. They must be readable for the user under which the server runs.
+        '';
+        type = types.listOf types.path;
+      };
+
+      user = mkOption {
+        description = "The user under which calibre-server runs.";
+        type = types.str;
+        default = "calibre-server";
+      };
+
+      group = mkOption {
+        description = "The group under which calibre-server runs.";
+        type = types.str;
+        default = "calibre-server";
+      };
+
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.calibre-server = {
+        description = "Calibre Server";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          User = cfg.user;
+          Restart = "always";
+          ExecStart = "${pkgs.calibre}/bin/calibre-server ${lib.concatStringsSep " " cfg.libraries}";
+        };
+
+      };
+
+    environment.systemPackages = [ pkgs.calibre ];
+
+    users.users = optionalAttrs (cfg.user == "calibre-server") {
+      calibre-server = {
+        home = "/var/lib/calibre-server";
+        createHome = true;
+        uid = config.ids.uids.calibre-server;
+        group = cfg.group;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "calibre-server") {
+      calibre-server = {
+        gid = config.ids.gids.calibre-server;
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/canto-daemon.nix b/nixos/modules/services/misc/canto-daemon.nix
new file mode 100644
index 00000000000..db51a263aab
--- /dev/null
+++ b/nixos/modules/services/misc/canto-daemon.nix
@@ -0,0 +1,37 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+cfg = config.services.canto-daemon;
+
+in {
+
+##### interface
+
+  options = {
+
+    services.canto-daemon = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the canto RSS daemon.";
+      };
+    };
+
+  };
+
+##### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.user.services.canto-daemon = {
+      description = "Canto RSS Daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "default.target" ];
+      serviceConfig.ExecStart = "${pkgs.canto-daemon}/bin/canto-daemon";
+    };
+  };
+
+}
diff --git a/nixos/modules/services/misc/cfdyndns.nix b/nixos/modules/services/misc/cfdyndns.nix
new file mode 100644
index 00000000000..5885617d742
--- /dev/null
+++ b/nixos/modules/services/misc/cfdyndns.nix
@@ -0,0 +1,82 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cfdyndns;
+in
+{
+  imports = [
+    (mkRemovedOptionModule
+      [ "services" "cfdyndns" "apikey" ]
+      "Use services.cfdyndns.apikeyFile instead.")
+  ];
+
+  options = {
+    services.cfdyndns = {
+      enable = mkEnableOption "Cloudflare Dynamic DNS Client";
+
+      email = mkOption {
+        type = types.str;
+        description = ''
+          The email address to use to authenticate to CloudFlare.
+        '';
+      };
+
+      apikeyFile = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description = ''
+          The path to a file containing the API Key
+          used to authenticate with CloudFlare.
+        '';
+      };
+
+      records = mkOption {
+        default = [];
+        example = [ "host.tld" ];
+        type = types.listOf types.str;
+        description = ''
+          The records to update in CloudFlare.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.cfdyndns = {
+      description = "CloudFlare Dynamic DNS Client";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      startAt = "*:0/5";
+      serviceConfig = {
+        Type = "simple";
+        User = config.ids.uids.cfdyndns;
+        Group = config.ids.gids.cfdyndns;
+      };
+      environment = {
+        CLOUDFLARE_EMAIL="${cfg.email}";
+        CLOUDFLARE_RECORDS="${concatStringsSep "," cfg.records}";
+      };
+      script = ''
+        ${optionalString (cfg.apikeyFile != null) ''
+          export CLOUDFLARE_APIKEY="$(cat ${escapeShellArg cfg.apikeyFile})"
+        ''}
+        ${pkgs.cfdyndns}/bin/cfdyndns
+      '';
+    };
+
+    users.users = {
+      cfdyndns = {
+        group = "cfdyndns";
+        uid = config.ids.uids.cfdyndns;
+      };
+    };
+
+    users.groups = {
+      cfdyndns = {
+        gid = config.ids.gids.cfdyndns;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/cgminer.nix b/nixos/modules/services/misc/cgminer.nix
new file mode 100644
index 00000000000..60f75530723
--- /dev/null
+++ b/nixos/modules/services/misc/cgminer.nix
@@ -0,0 +1,148 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cgminer;
+
+  convType = with builtins;
+    v: if isBool v then boolToString v else toString v;
+  mergedHwConfig =
+    mapAttrsToList (n: v: ''"${n}": "${(concatStringsSep "," (map convType v))}"'')
+      (foldAttrs (n: a: [n] ++ a) [] cfg.hardware);
+  mergedConfig = with builtins;
+    mapAttrsToList (n: v: ''"${n}":  ${if isBool v then "" else ''"''}${convType v}${if isBool v then "" else ''"''}'')
+      cfg.config;
+
+  cgminerConfig = pkgs.writeText "cgminer.conf" ''
+  {
+  ${concatStringsSep ",\n" mergedHwConfig},
+  ${concatStringsSep ",\n" mergedConfig},
+  "pools": [
+  ${concatStringsSep ",\n"
+    (map (v: ''{"url": "${v.url}", "user": "${v.user}", "pass": "${v.pass}"}'')
+          cfg.pools)}]
+  }
+  '';
+in
+{
+  ###### interface
+  options = {
+
+    services.cgminer = {
+
+      enable = mkEnableOption "cgminer, an ASIC/FPGA/GPU miner for bitcoin and litecoin";
+
+      package = mkOption {
+        default = pkgs.cgminer;
+        defaultText = literalExpression "pkgs.cgminer";
+        description = "Which cgminer derivation to use.";
+        type = types.package;
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "cgminer";
+        description = "User account under which cgminer runs";
+      };
+
+      pools = mkOption {
+        default = [];  # Run benchmark
+        type = types.listOf (types.attrsOf types.str);
+        description = "List of pools where to mine";
+        example = [{
+          url = "http://p2pool.org:9332";
+          username = "17EUZxTvs9uRmPsjPZSYUU3zCz9iwstudk";
+          password="X";
+        }];
+      };
+
+      hardware = mkOption {
+        default = []; # Run without options
+        type = types.listOf (types.attrsOf (types.either types.str types.int));
+        description= "List of config options for every GPU";
+        example = [
+        {
+          intensity = 9;
+          gpu-engine = "0-985";
+          gpu-fan = "0-85";
+          gpu-memclock = 860;
+          gpu-powertune = 20;
+          temp-cutoff = 95;
+          temp-overheat = 85;
+          temp-target = 75;
+        }
+        {
+          intensity = 9;
+          gpu-engine = "0-950";
+          gpu-fan = "0-85";
+          gpu-memclock = 825;
+          gpu-powertune = 20;
+          temp-cutoff = 95;
+          temp-overheat = 85;
+          temp-target = 75;
+        }];
+      };
+
+      config = mkOption {
+        default = {};
+        type = types.attrsOf (types.either types.bool types.int);
+        description = "Additional config";
+        example = {
+          auto-fan = true;
+          auto-gpu = true;
+          expiry = 120;
+          failover-only = true;
+          gpu-threads = 2;
+          log = 5;
+          queue = 1;
+          scan-time = 60;
+          temp-histeresys = 3;
+        };
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.cgminer.enable {
+
+    users.users = optionalAttrs (cfg.user == "cgminer") {
+      cgminer = {
+        isSystemUser = true;
+        group = "cgminer";
+        description = "Cgminer user";
+      };
+    };
+    users.groups = optionalAttrs (cfg.user == "cgminer") {
+      cgminer = {};
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.cgminer = {
+      path = [ pkgs.cgminer ];
+
+      after = [ "network.target" "display-manager.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment = {
+        LD_LIBRARY_PATH = "/run/opengl-driver/lib:/run/opengl-driver-32/lib";
+        DISPLAY = ":${toString config.services.xserver.display}";
+        GPU_MAX_ALLOC_PERCENT = "100";
+        GPU_USE_SYNC_OBJECTS = "1";
+      };
+
+      startLimitIntervalSec = 60;  # 1 min
+      serviceConfig = {
+        ExecStart = "${pkgs.cgminer}/bin/cgminer --syslog --text-only --config ${cgminerConfig}";
+        User = cfg.user;
+        RestartSec = "30s";
+        Restart = "always";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/clipcat.nix b/nixos/modules/services/misc/clipcat.nix
new file mode 100644
index 00000000000..8b749aa7289
--- /dev/null
+++ b/nixos/modules/services/misc/clipcat.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.clipcat;
+in {
+
+  options.services.clipcat= {
+    enable = mkEnableOption "Clipcat clipboard daemon";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.clipcat;
+      defaultText = literalExpression "pkgs.clipcat";
+      description = "clipcat derivation to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.clipcat = {
+      enable      = true;
+      description = "clipcat daemon";
+      wantedBy = [ "graphical-session.target" ];
+      after    = [ "graphical-session.target" ];
+      serviceConfig.ExecStart = "${cfg.package}/bin/clipcatd --no-daemon";
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/misc/clipmenu.nix b/nixos/modules/services/misc/clipmenu.nix
new file mode 100644
index 00000000000..ef95985f8d8
--- /dev/null
+++ b/nixos/modules/services/misc/clipmenu.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.clipmenu;
+in {
+
+  options.services.clipmenu = {
+    enable = mkEnableOption "clipmenu, the clipboard management daemon";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.clipmenu;
+      defaultText = literalExpression "pkgs.clipmenu";
+      description = "clipmenu derivation to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.clipmenu = {
+      enable      = true;
+      description = "Clipboard management daemon";
+      wantedBy = [ "graphical-session.target" ];
+      after    = [ "graphical-session.target" ];
+      serviceConfig.ExecStart = "${cfg.package}/bin/clipmenud";
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/misc/confd.nix b/nixos/modules/services/misc/confd.nix
new file mode 100755
index 00000000000..6c66786524b
--- /dev/null
+++ b/nixos/modules/services/misc/confd.nix
@@ -0,0 +1,90 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.confd;
+
+  confdConfig = ''
+    backend = "${cfg.backend}"
+    confdir = "${cfg.confDir}"
+    interval = ${toString cfg.interval}
+    nodes = [ ${concatMapStringsSep "," (s: ''"${s}"'') cfg.nodes}, ]
+    prefix = "${cfg.prefix}"
+    log-level = "${cfg.logLevel}"
+    watch = ${boolToString cfg.watch}
+  '';
+
+in {
+  options.services.confd = {
+    enable = mkEnableOption "confd service";
+
+    backend = mkOption {
+      description = "Confd config storage backend to use.";
+      default = "etcd";
+      type = types.enum ["etcd" "consul" "redis" "zookeeper"];
+    };
+
+    interval = mkOption {
+      description = "Confd check interval.";
+      default = 10;
+      type = types.int;
+    };
+
+    nodes = mkOption {
+      description = "Confd list of nodes to connect to.";
+      default = [ "http://127.0.0.1:2379" ];
+      type = types.listOf types.str;
+    };
+
+    watch = mkOption {
+      description = "Confd, whether to watch etcd config for changes.";
+      default = true;
+      type = types.bool;
+    };
+
+    prefix = mkOption {
+      description = "The string to prefix to keys.";
+      default = "/";
+      type = types.path;
+    };
+
+    logLevel = mkOption {
+      description = "Confd log level.";
+      default = "info";
+      type = types.enum ["info" "debug"];
+    };
+
+    confDir = mkOption {
+      description = "The path to the confd configs.";
+      default = "/etc/confd";
+      type = types.path;
+    };
+
+    package = mkOption {
+      description = "Confd package to use.";
+      default = pkgs.confd;
+      defaultText = literalExpression "pkgs.confd";
+      type = types.package;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.confd = {
+      description = "Confd Service.";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/confd";
+      };
+    };
+
+    environment.etc = {
+      "confd/confd.toml".text = confdConfig;
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    services.etcd.enable = mkIf (cfg.backend == "etcd") (mkDefault true);
+  };
+}
diff --git a/nixos/modules/services/misc/cpuminer-cryptonight.nix b/nixos/modules/services/misc/cpuminer-cryptonight.nix
new file mode 100644
index 00000000000..907b9d90da2
--- /dev/null
+++ b/nixos/modules/services/misc/cpuminer-cryptonight.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cpuminer-cryptonight;
+
+  json = builtins.toJSON (
+    cfg // {
+       enable = null;
+       threads =
+         if cfg.threads == 0 then null else toString cfg.threads;
+    }
+  );
+
+  confFile = builtins.toFile "cpuminer.json" json;
+in
+{
+
+  options = {
+
+    services.cpuminer-cryptonight = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the cpuminer cryptonight miner.
+        '';
+      };
+      url = mkOption {
+        type = types.str;
+        description = "URL of mining server";
+      };
+      user = mkOption {
+        type = types.str;
+        description = "Username for mining server";
+      };
+      pass = mkOption {
+        type = types.str;
+        default = "x";
+        description = "Password for mining server";
+      };
+      threads = mkOption {
+        type = types.int;
+        default = 0;
+        description = "Number of miner threads, defaults to available processors";
+      };
+    };
+
+  };
+
+  config = mkIf config.services.cpuminer-cryptonight.enable {
+
+    systemd.services.cpuminer-cryptonight = {
+      description = "Cryptonight cpuminer";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.cpuminer-multi}/bin/minerd --syslog --config=${confFile}";
+        User = "nobody";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/dendrite.nix b/nixos/modules/services/misc/dendrite.nix
new file mode 100644
index 00000000000..b2885b09415
--- /dev/null
+++ b/nixos/modules/services/misc/dendrite.nix
@@ -0,0 +1,275 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.dendrite;
+  settingsFormat = pkgs.formats.yaml { };
+  configurationYaml = settingsFormat.generate "dendrite.yaml" cfg.settings;
+  workingDir = "/var/lib/dendrite";
+in
+{
+  options.services.dendrite = {
+    enable = lib.mkEnableOption "matrix.org dendrite";
+    httpPort = lib.mkOption {
+      type = lib.types.nullOr lib.types.port;
+      default = 8008;
+      description = ''
+        The port to listen for HTTP requests on.
+      '';
+    };
+    httpsPort = lib.mkOption {
+      type = lib.types.nullOr lib.types.port;
+      default = null;
+      description = ''
+        The port to listen for HTTPS requests on.
+      '';
+    };
+    tlsCert = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      example = "/var/lib/dendrite/server.cert";
+      default = null;
+      description = ''
+        The path to the TLS certificate.
+
+        <programlisting>
+          nix-shell -p dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
+        </programlisting>
+      '';
+    };
+    tlsKey = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      example = "/var/lib/dendrite/server.key";
+      default = null;
+      description = ''
+        The path to the TLS key.
+
+        <programlisting>
+          nix-shell -p dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
+        </programlisting>
+      '';
+    };
+    environmentFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      example = "/var/lib/dendrite/registration_secret";
+      default = null;
+      description = ''
+        Environment file as defined in <citerefentry>
+        <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+        </citerefentry>.
+        Secrets may be passed to the service without adding them to the world-readable
+        Nix store, by specifying placeholder variables as the option value in Nix and
+        setting these variables accordingly in the environment file. Currently only used
+        for the registration secret to allow secure registration when
+        client_api.registration_disabled is true.
+
+        <programlisting>
+          # snippet of dendrite-related config
+          services.dendrite.settings.client_api.registration_shared_secret = "$REGISTRATION_SHARED_SECRET";
+        </programlisting>
+
+        <programlisting>
+          # content of the environment file
+          REGISTRATION_SHARED_SECRET=verysecretpassword
+        </programlisting>
+
+        Note that this file needs to be available on the host on which
+        <literal>dendrite</literal> is running.
+      '';
+    };
+    settings = lib.mkOption {
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+        options.global = {
+          server_name = lib.mkOption {
+            type = lib.types.str;
+            example = "example.com";
+            description = ''
+              The domain name of the server, with optional explicit port.
+              This is used by remote servers to connect to this server.
+              This is also the last part of your UserID.
+            '';
+          };
+          private_key = lib.mkOption {
+            type = lib.types.path;
+            example = "${workingDir}/matrix_key.pem";
+            description = ''
+              The path to the signing private key file, used to sign
+              requests and events.
+
+              <programlisting>
+                nix-shell -p dendrite --command "generate-keys --private-key matrix_key.pem"
+              </programlisting>
+            '';
+          };
+          trusted_third_party_id_servers = lib.mkOption {
+            type = lib.types.listOf lib.types.str;
+            example = [ "matrix.org" ];
+            default = [ "matrix.org" "vector.im" ];
+            description = ''
+              Lists of domains that the server will trust as identity
+              servers to verify third party identifiers such as phone
+              numbers and email addresses
+            '';
+          };
+        };
+        options.app_service_api.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:federationapi.db";
+            description = ''
+              Database for the Appservice API.
+            '';
+          };
+        };
+        options.client_api = {
+          registration_disabled = lib.mkOption {
+            type = lib.types.bool;
+            default = true;
+            description = ''
+              Whether to disable user registration to the server
+              without the shared secret.
+            '';
+          };
+        };
+        options.federation_api.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:federationapi.db";
+            description = ''
+              Database for the Federation API.
+            '';
+          };
+        };
+        options.key_server.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:keyserver.db";
+            description = ''
+              Database for the Key Server (for end-to-end encryption).
+            '';
+          };
+        };
+        options.media_api = {
+          database = {
+            connection_string = lib.mkOption {
+              type = lib.types.str;
+              default = "file:mediaapi.db";
+              description = ''
+                Database for the Media API.
+              '';
+            };
+          };
+          base_path = lib.mkOption {
+            type = lib.types.str;
+            default = "${workingDir}/media_store";
+            description = ''
+              Storage path for uploaded media.
+            '';
+          };
+        };
+        options.room_server.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:roomserver.db";
+            description = ''
+              Database for the Room Server.
+            '';
+          };
+        };
+        options.sync_api.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:syncserver.db";
+            description = ''
+              Database for the Sync API.
+            '';
+          };
+        };
+        options.user_api = {
+          account_database = {
+            connection_string = lib.mkOption {
+              type = lib.types.str;
+              default = "file:userapi_accounts.db";
+              description = ''
+                Database for the User API, accounts.
+              '';
+            };
+          };
+          device_database = {
+            connection_string = lib.mkOption {
+              type = lib.types.str;
+              default = "file:userapi_devices.db";
+              description = ''
+                Database for the User API, devices.
+              '';
+            };
+          };
+        };
+        options.mscs = {
+          database = {
+            connection_string = lib.mkOption {
+              type = lib.types.str;
+              default = "file:mscs.db";
+              description = ''
+                Database for exerimental MSC's.
+              '';
+            };
+          };
+        };
+      };
+      default = { };
+      description = ''
+        Configuration for dendrite, see:
+        <link xlink:href="https://github.com/matrix-org/dendrite/blob/master/dendrite-config.yaml"/>
+        for available options with which to populate settings.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [{
+      assertion = cfg.httpsPort != null -> (cfg.tlsCert != null && cfg.tlsKey != null);
+      message = ''
+        If Dendrite is configured to use https, tlsCert and tlsKey must be provided.
+
+        nix-shell -p dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
+      '';
+    }];
+
+    systemd.services.dendrite = {
+      description = "Dendrite Matrix homeserver";
+      after = [
+        "network.target"
+      ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "simple";
+        DynamicUser = true;
+        StateDirectory = "dendrite";
+        WorkingDirectory = workingDir;
+        RuntimeDirectory = "dendrite";
+        RuntimeDirectoryMode = "0700";
+        EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
+        ExecStartPre =
+          if (cfg.environmentFile != null) then ''
+            ${pkgs.envsubst}/bin/envsubst \
+              -i ${configurationYaml} \
+              -o /run/dendrite/dendrite.yaml
+          '' else ''
+            ${pkgs.coreutils}/bin/cp ${configurationYaml} /run/dendrite/dendrite.yaml
+          '';
+        ExecStart = lib.strings.concatStringsSep " " ([
+          "${pkgs.dendrite}/bin/dendrite-monolith-server"
+          "--config /run/dendrite/dendrite.yaml"
+        ] ++ lib.optionals (cfg.httpPort != null) [
+          "--http-bind-address :${builtins.toString cfg.httpPort}"
+        ] ++ lib.optionals (cfg.httpsPort != null) [
+          "--https-bind-address :${builtins.toString cfg.httpsPort}"
+          "--tls-cert ${cfg.tlsCert}"
+          "--tls-key ${cfg.tlsKey}"
+        ]);
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        Restart = "on-failure";
+      };
+    };
+  };
+  meta.maintainers = lib.teams.matrix.members;
+}
diff --git a/nixos/modules/services/misc/devmon.nix b/nixos/modules/services/misc/devmon.nix
new file mode 100644
index 00000000000..e4a3348646b
--- /dev/null
+++ b/nixos/modules/services/misc/devmon.nix
@@ -0,0 +1,25 @@
+{ pkgs, config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.devmon;
+
+in {
+  options = {
+    services.devmon = {
+      enable = mkEnableOption "devmon, an automatic device mounting daemon";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.devmon = {
+      description = "devmon automatic device mounting daemon";
+      wantedBy = [ "default.target" ];
+      path = [ pkgs.udevil pkgs.procps pkgs.udisks2 pkgs.which ];
+      serviceConfig.ExecStart = "${pkgs.udevil}/bin/devmon";
+    };
+
+    services.udisks2.enable = true;
+  };
+}
diff --git a/nixos/modules/services/misc/dictd.nix b/nixos/modules/services/misc/dictd.nix
new file mode 100644
index 00000000000..96e2a4e7c26
--- /dev/null
+++ b/nixos/modules/services/misc/dictd.nix
@@ -0,0 +1,65 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dictd;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.dictd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the DICT.org dictionary server.
+        '';
+      };
+
+      DBs = mkOption {
+        type = types.listOf types.package;
+        default = with pkgs.dictdDBs; [ wiktionary wordnet ];
+        defaultText = literalExpression "with pkgs.dictdDBs; [ wiktionary wordnet ]";
+        example = literalExpression "[ pkgs.dictdDBs.nld2eng ]";
+        description = "List of databases to make available.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = let dictdb = pkgs.dictDBCollector { dictlist = map (x: {
+               name = x.name;
+               filename = x; } ) cfg.DBs; };
+  in mkIf cfg.enable {
+
+    # get the command line client on system path to make some use of the service
+    environment.systemPackages = [ pkgs.dict ];
+
+    users.users.dictd =
+      { group = "dictd";
+        description = "DICT.org dictd server";
+        home = "${dictdb}/share/dictd";
+        uid = config.ids.uids.dictd;
+      };
+
+    users.groups.dictd.gid = config.ids.gids.dictd;
+
+    systemd.services.dictd = {
+      description = "DICT.org Dictionary Server";
+      wantedBy = [ "multi-user.target" ];
+      environment = { LOCALE_ARCHIVE = "/run/current-system/sw/lib/locale/locale-archive"; };
+      serviceConfig.Type = "forking";
+      script = "${pkgs.dict}/sbin/dictd -s -c ${dictdb}/share/dictd/dictd.conf --locale en_US.UTF-8";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/disnix.nix b/nixos/modules/services/misc/disnix.nix
new file mode 100644
index 00000000000..07c0613336a
--- /dev/null
+++ b/nixos/modules/services/misc/disnix.nix
@@ -0,0 +1,98 @@
+# Disnix server
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.disnix;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.disnix = {
+
+      enable = mkEnableOption "Disnix";
+
+      enableMultiUser = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to support multi-user mode by enabling the Disnix D-Bus service";
+      };
+
+      useWebServiceInterface = mkEnableOption "the DisnixWebService interface running on Apache Tomcat";
+
+      package = mkOption {
+        type = types.path;
+        description = "The Disnix package";
+        default = pkgs.disnix;
+        defaultText = literalExpression "pkgs.disnix";
+      };
+
+      enableProfilePath = mkEnableOption "exposing the Disnix profiles in the system's PATH";
+
+      profiles = mkOption {
+        type = types.listOf types.str;
+        default = [ "default" ];
+        description = "Names of the Disnix profiles to expose in the system's PATH";
+      };
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    dysnomia.enable = true;
+
+    environment.systemPackages = [ pkgs.disnix ] ++ optional cfg.useWebServiceInterface pkgs.DisnixWebService;
+    environment.variables.PATH = lib.optionals cfg.enableProfilePath (map (profileName: "/nix/var/nix/profiles/disnix/${profileName}/bin" ) cfg.profiles);
+    environment.variables.DISNIX_REMOTE_CLIENT = lib.optionalString (cfg.enableMultiUser) "disnix-client";
+
+    services.dbus.enable = true;
+    services.dbus.packages = [ pkgs.disnix ];
+
+    services.tomcat.enable = cfg.useWebServiceInterface;
+    services.tomcat.extraGroups = [ "disnix" ];
+    services.tomcat.javaOpts = "${optionalString cfg.useWebServiceInterface "-Djava.library.path=${pkgs.libmatthew_java}/lib/jni"} ";
+    services.tomcat.sharedLibs = optional cfg.useWebServiceInterface "${pkgs.DisnixWebService}/share/java/DisnixConnection.jar"
+      ++ optional cfg.useWebServiceInterface "${pkgs.dbus_java}/share/java/dbus.jar";
+    services.tomcat.webapps = optional cfg.useWebServiceInterface pkgs.DisnixWebService;
+
+    users.groups.disnix.gid = config.ids.gids.disnix;
+
+    systemd.services = {
+      disnix = mkIf cfg.enableMultiUser {
+        description = "Disnix server";
+        wants = [ "dysnomia.target" ];
+        wantedBy = [ "multi-user.target" ];
+        after = [ "dbus.service" ]
+          ++ optional config.services.httpd.enable "httpd.service"
+          ++ optional config.services.mysql.enable "mysql.service"
+          ++ optional config.services.postgresql.enable "postgresql.service"
+          ++ optional config.services.tomcat.enable "tomcat.service"
+          ++ optional config.services.svnserve.enable "svnserve.service"
+          ++ optional config.services.mongodb.enable "mongodb.service"
+          ++ optional config.services.influxdb.enable "influxdb.service";
+
+        restartIfChanged = false;
+
+        path = [ config.nix.package cfg.package config.dysnomia.package "/run/current-system/sw" ];
+
+        environment = {
+          HOME = "/root";
+        }
+        // (if config.environment.variables ? DYSNOMIA_CONTAINERS_PATH then { inherit (config.environment.variables) DYSNOMIA_CONTAINERS_PATH; } else {})
+        // (if config.environment.variables ? DYSNOMIA_MODULES_PATH then { inherit (config.environment.variables) DYSNOMIA_MODULES_PATH; } else {});
+
+        serviceConfig.ExecStart = "${cfg.package}/bin/disnix-service";
+      };
+
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/docker-registry.nix b/nixos/modules/services/misc/docker-registry.nix
new file mode 100644
index 00000000000..cb68a29c530
--- /dev/null
+++ b/nixos/modules/services/misc/docker-registry.nix
@@ -0,0 +1,159 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dockerRegistry;
+
+  blobCache = if cfg.enableRedisCache
+    then "redis"
+    else "inmemory";
+
+  registryConfig = {
+    version =  "0.1";
+    log.fields.service = "registry";
+    storage = {
+      cache.blobdescriptor = blobCache;
+      delete.enabled = cfg.enableDelete;
+    } // (if cfg.storagePath != null
+          then { filesystem.rootdirectory = cfg.storagePath; }
+          else {});
+    http = {
+      addr = "${cfg.listenAddress}:${builtins.toString cfg.port}";
+      headers.X-Content-Type-Options = ["nosniff"];
+    };
+    health.storagedriver = {
+      enabled = true;
+      interval = "10s";
+      threshold = 3;
+    };
+  };
+
+  registryConfig.redis = mkIf cfg.enableRedisCache {
+    addr = "${cfg.redisUrl}";
+    password = "${cfg.redisPassword}";
+    db = 0;
+    dialtimeout = "10ms";
+    readtimeout = "10ms";
+    writetimeout = "10ms";
+    pool = {
+      maxidle = 16;
+      maxactive = 64;
+      idletimeout = "300s";
+    };
+  };
+
+  configFile = pkgs.writeText "docker-registry-config.yml" (builtins.toJSON (recursiveUpdate registryConfig cfg.extraConfig));
+
+in {
+  options.services.dockerRegistry = {
+    enable = mkEnableOption "Docker Registry";
+
+    listenAddress = mkOption {
+      description = "Docker registry host or ip to bind to.";
+      default = "127.0.0.1";
+      type = types.str;
+    };
+
+    port = mkOption {
+      description = "Docker registry port to bind to.";
+      default = 5000;
+      type = types.port;
+    };
+
+    storagePath = mkOption {
+      type = types.nullOr types.path;
+      default = "/var/lib/docker-registry";
+      description = ''
+        Docker registry storage path for the filesystem storage backend. Set to
+        null to configure another backend via extraConfig.
+      '';
+    };
+
+    enableDelete = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable delete for manifests and blobs.";
+    };
+
+    enableRedisCache = mkEnableOption "redis as blob cache";
+
+    redisUrl = mkOption {
+      type = types.str;
+      default = "localhost:6379";
+      description = "Set redis host and port.";
+    };
+
+    redisPassword = mkOption {
+      type = types.str;
+      default = "";
+      description = "Set redis password.";
+    };
+
+    extraConfig = mkOption {
+      description = ''
+        Docker extra registry configuration via environment variables.
+      '';
+      default = {};
+      type = types.attrs;
+    };
+
+    enableGarbageCollect = mkEnableOption "garbage collect";
+
+    garbageCollectDates = mkOption {
+      default = "daily";
+      type = types.str;
+      description = ''
+        Specification (in the format described by
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>) of the time at
+        which the garbage collect will occur.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.docker-registry = {
+      description = "Docker Container Registry";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      script = ''
+        ${pkgs.docker-distribution}/bin/registry serve ${configFile}
+      '';
+
+      serviceConfig = {
+        User = "docker-registry";
+        WorkingDirectory = cfg.storagePath;
+        AmbientCapabilities = mkIf (cfg.port < 1024) "cap_net_bind_service";
+      };
+    };
+
+    systemd.services.docker-registry-garbage-collect = {
+      description = "Run Garbage Collection for docker registry";
+
+      restartIfChanged = false;
+      unitConfig.X-StopOnRemoval = false;
+
+      serviceConfig.Type = "oneshot";
+
+      script = ''
+        ${pkgs.docker-distribution}/bin/registry garbage-collect ${configFile}
+        /run/current-system/systemd/bin/systemctl restart docker-registry.service
+      '';
+
+      startAt = optional cfg.enableGarbageCollect cfg.garbageCollectDates;
+    };
+
+    users.users.docker-registry =
+      (if cfg.storagePath != null
+      then {
+        createHome = true;
+        home = cfg.storagePath;
+      }
+      else {}) // {
+        group = "docker-registry";
+        isSystemUser = true;
+      };
+    users.groups.docker-registry = {};
+  };
+}
diff --git a/nixos/modules/services/misc/domoticz.nix b/nixos/modules/services/misc/domoticz.nix
new file mode 100644
index 00000000000..b1353d48404
--- /dev/null
+++ b/nixos/modules/services/misc/domoticz.nix
@@ -0,0 +1,51 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.domoticz;
+  pkgDesc = "Domoticz home automation";
+
+in {
+
+  options = {
+
+    services.domoticz = {
+      enable = mkEnableOption pkgDesc;
+
+      bind = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "IP address to bind to.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8080;
+        description = "Port to bind to for HTTP, set to 0 to disable HTTP.";
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services."domoticz" = {
+      description = pkgDesc;
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "domoticz";
+        Restart = "always";
+        ExecStart = ''
+          ${pkgs.domoticz}/bin/domoticz -noupdates -www ${toString cfg.port} -wwwbind ${cfg.bind} -sslwww 0 -userdata /var/lib/domoticz -approot ${pkgs.domoticz}/share/domoticz/ -pidfile /var/run/domoticz.pid
+        '';
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/duckling.nix b/nixos/modules/services/misc/duckling.nix
new file mode 100644
index 00000000000..77d2a92380b
--- /dev/null
+++ b/nixos/modules/services/misc/duckling.nix
@@ -0,0 +1,39 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.duckling;
+in {
+  options = {
+    services.duckling = {
+      enable = mkEnableOption "duckling";
+
+      port = mkOption {
+        type = types.port;
+        default = 8080;
+        description = ''
+          Port on which duckling will run.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.duckling = {
+      description = "Duckling server service";
+      wantedBy    = [ "multi-user.target" ];
+      after       = [ "network.target" ];
+
+      environment = {
+        PORT = builtins.toString cfg.port;
+      };
+
+      serviceConfig = {
+        ExecStart = "${pkgs.haskellPackages.duckling}/bin/duckling-example-exe --no-access-log --no-error-log";
+        Restart = "always";
+        DynamicUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/dwm-status.nix b/nixos/modules/services/misc/dwm-status.nix
new file mode 100644
index 00000000000..5f591b3c5d4
--- /dev/null
+++ b/nixos/modules/services/misc/dwm-status.nix
@@ -0,0 +1,73 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dwm-status;
+
+  order = concatMapStringsSep "," (feature: ''"${feature}"'') cfg.order;
+
+  configFile = pkgs.writeText "dwm-status.toml" ''
+    order = [${order}]
+
+    ${cfg.extraConfig}
+  '';
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.dwm-status = {
+
+      enable = mkEnableOption "dwm-status user service";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.dwm-status;
+        defaultText = literalExpression "pkgs.dwm-status";
+        example = literalExpression "pkgs.dwm-status.override { enableAlsaUtils = false; }";
+        description = ''
+          Which dwm-status package to use.
+        '';
+      };
+
+      order = mkOption {
+        type = types.listOf (types.enum [ "audio" "backlight" "battery" "cpu_load" "network" "time" ]);
+        description = ''
+          List of enabled features in order.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra config in TOML format.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.upower.enable = elem "battery" cfg.order;
+
+    systemd.user.services.dwm-status = {
+      description = "Highly performant and configurable DWM status service";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+
+      serviceConfig.ExecStart = "${cfg.package}/bin/dwm-status ${configFile}";
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/dysnomia.nix b/nixos/modules/services/misc/dysnomia.nix
new file mode 100644
index 00000000000..7d9c39a6973
--- /dev/null
+++ b/nixos/modules/services/misc/dysnomia.nix
@@ -0,0 +1,265 @@
+{pkgs, lib, config, ...}:
+
+with lib;
+
+let
+  cfg = config.dysnomia;
+
+  printProperties = properties:
+    concatMapStrings (propertyName:
+      let
+        property = properties.${propertyName};
+      in
+      if isList property then "${propertyName}=(${lib.concatMapStrings (elem: "\"${toString elem}\" ") (properties.${propertyName})})\n"
+      else "${propertyName}=\"${toString property}\"\n"
+    ) (builtins.attrNames properties);
+
+  properties = pkgs.stdenv.mkDerivation {
+    name = "dysnomia-properties";
+    buildCommand = ''
+      cat > $out << "EOF"
+      ${printProperties cfg.properties}
+      EOF
+    '';
+  };
+
+  containersDir = pkgs.stdenv.mkDerivation {
+    name = "dysnomia-containers";
+    buildCommand = ''
+      mkdir -p $out
+      cd $out
+
+      ${concatMapStrings (containerName:
+        let
+          containerProperties = cfg.containers.${containerName};
+        in
+        ''
+          cat > ${containerName} <<EOF
+          ${printProperties containerProperties}
+          type=${containerName}
+          EOF
+        ''
+      ) (builtins.attrNames cfg.containers)}
+    '';
+  };
+
+  linkMutableComponents = {containerName}:
+    ''
+      mkdir ${containerName}
+
+      ${concatMapStrings (componentName:
+        let
+          component = cfg.components.${containerName}.${componentName};
+        in
+        "ln -s ${component} ${containerName}/${componentName}\n"
+      ) (builtins.attrNames (cfg.components.${containerName} or {}))}
+    '';
+
+  componentsDir = pkgs.stdenv.mkDerivation {
+    name = "dysnomia-components";
+    buildCommand = ''
+      mkdir -p $out
+      cd $out
+
+      ${concatMapStrings (containerName:
+        linkMutableComponents { inherit containerName; }
+      ) (builtins.attrNames cfg.components)}
+    '';
+  };
+
+  dysnomiaFlags = {
+    enableApacheWebApplication = config.services.httpd.enable;
+    enableAxis2WebService = config.services.tomcat.axis2.enable;
+    enableDockerContainer = config.virtualisation.docker.enable;
+    enableEjabberdDump = config.services.ejabberd.enable;
+    enableMySQLDatabase = config.services.mysql.enable;
+    enablePostgreSQLDatabase = config.services.postgresql.enable;
+    enableTomcatWebApplication = config.services.tomcat.enable;
+    enableMongoDatabase = config.services.mongodb.enable;
+    enableSubversionRepository = config.services.svnserve.enable;
+    enableInfluxDatabase = config.services.influxdb.enable;
+  };
+in
+{
+  options = {
+    dysnomia = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable Dysnomia";
+      };
+
+      enableAuthentication = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to publish privacy-sensitive authentication credentials";
+      };
+
+      package = mkOption {
+        type = types.path;
+        description = "The Dysnomia package";
+      };
+
+      properties = mkOption {
+        description = "An attribute set in which each attribute represents a machine property. Optionally, these values can be shell substitutions.";
+        default = {};
+        type = types.attrs;
+      };
+
+      containers = mkOption {
+        description = "An attribute set in which each key represents a container and each value an attribute set providing its configuration properties";
+        default = {};
+        type = types.attrsOf types.attrs;
+      };
+
+      components = mkOption {
+        description = "An atttribute set in which each key represents a container and each value an attribute set in which each key represents a component and each value a derivation constructing its initial state";
+        default = {};
+        type = types.attrsOf types.attrs;
+      };
+
+      extraContainerProperties = mkOption {
+        description = "An attribute set providing additional container settings in addition to the default properties";
+        default = {};
+        type = types.attrs;
+      };
+
+      extraContainerPaths = mkOption {
+        description = "A list of paths containing additional container configurations that are added to the search folders";
+        default = [];
+        type = types.listOf types.path;
+      };
+
+      extraModulePaths = mkOption {
+        description = "A list of paths containing additional modules that are added to the search folders";
+        default = [];
+        type = types.listOf types.path;
+      };
+
+      enableLegacyModules = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to enable Dysnomia legacy process and wrapper modules";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.etc = {
+      "dysnomia/containers" = {
+        source = containersDir;
+      };
+      "dysnomia/components" = {
+        source = componentsDir;
+      };
+      "dysnomia/properties" = {
+        source = properties;
+      };
+    };
+
+    environment.variables = {
+      DYSNOMIA_STATEDIR = "/var/state/dysnomia-nixos";
+      DYSNOMIA_CONTAINERS_PATH = "${lib.concatMapStrings (containerPath: "${containerPath}:") cfg.extraContainerPaths}/etc/dysnomia/containers";
+      DYSNOMIA_MODULES_PATH = "${lib.concatMapStrings (modulePath: "${modulePath}:") cfg.extraModulePaths}/etc/dysnomia/modules";
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    dysnomia.package = pkgs.dysnomia.override (origArgs: dysnomiaFlags // lib.optionalAttrs (cfg.enableLegacyModules) {
+      enableLegacy = builtins.trace ''
+        WARNING: Dysnomia has been configured to use the legacy 'process' and 'wrapper'
+        modules for compatibility reasons! If you rely on these modules, consider
+        migrating to better alternatives.
+
+        More information: https://raw.githubusercontent.com/svanderburg/dysnomia/f65a9a84827bcc4024d6b16527098b33b02e4054/README-legacy.md
+
+        If you have migrated already or don't rely on these Dysnomia modules, you can
+        disable legacy mode with the following NixOS configuration option:
+
+        dysnomia.enableLegacyModules = false;
+
+        In a future version of Dysnomia (and NixOS) the legacy option will go away!
+      '' true;
+    });
+
+    dysnomia.properties = {
+      hostname = config.networking.hostName;
+      inherit (config.nixpkgs.localSystem) system;
+
+      supportedTypes = [
+        "echo"
+        "fileset"
+        "process"
+        "wrapper"
+
+        # These are not base modules, but they are still enabled because they work with technology that are always enabled in NixOS
+        "systemd-unit"
+        "sysvinit-script"
+        "nixos-configuration"
+      ]
+      ++ optional (dysnomiaFlags.enableApacheWebApplication) "apache-webapplication"
+      ++ optional (dysnomiaFlags.enableAxis2WebService) "axis2-webservice"
+      ++ optional (dysnomiaFlags.enableDockerContainer) "docker-container"
+      ++ optional (dysnomiaFlags.enableEjabberdDump) "ejabberd-dump"
+      ++ optional (dysnomiaFlags.enableInfluxDatabase) "influx-database"
+      ++ optional (dysnomiaFlags.enableMySQLDatabase) "mysql-database"
+      ++ optional (dysnomiaFlags.enablePostgreSQLDatabase) "postgresql-database"
+      ++ optional (dysnomiaFlags.enableTomcatWebApplication) "tomcat-webapplication"
+      ++ optional (dysnomiaFlags.enableMongoDatabase) "mongo-database"
+      ++ optional (dysnomiaFlags.enableSubversionRepository) "subversion-repository";
+    };
+
+    dysnomia.containers = lib.recursiveUpdate ({
+      process = {};
+      wrapper = {};
+    }
+    // lib.optionalAttrs (config.services.httpd.enable) { apache-webapplication = {
+      documentRoot = config.services.httpd.virtualHosts.localhost.documentRoot;
+    }; }
+    // lib.optionalAttrs (config.services.tomcat.axis2.enable) { axis2-webservice = {}; }
+    // lib.optionalAttrs (config.services.ejabberd.enable) { ejabberd-dump = {
+      ejabberdUser = config.services.ejabberd.user;
+    }; }
+    // lib.optionalAttrs (config.services.mysql.enable) { mysql-database = {
+        mysqlPort = config.services.mysql.port;
+        mysqlSocket = "/run/mysqld/mysqld.sock";
+      } // lib.optionalAttrs cfg.enableAuthentication {
+        mysqlUsername = "root";
+      };
+    }
+    // lib.optionalAttrs (config.services.postgresql.enable) { postgresql-database = {
+      } // lib.optionalAttrs (cfg.enableAuthentication) {
+        postgresqlUsername = "postgres";
+      };
+    }
+    // lib.optionalAttrs (config.services.tomcat.enable) { tomcat-webapplication = {
+      tomcatPort = 8080;
+    }; }
+    // lib.optionalAttrs (config.services.mongodb.enable) { mongo-database = {}; }
+    // lib.optionalAttrs (config.services.influxdb.enable) {
+      influx-database = {
+        influxdbUsername = config.services.influxdb.user;
+        influxdbDataDir = "${config.services.influxdb.dataDir}/data";
+        influxdbMetaDir = "${config.services.influxdb.dataDir}/meta";
+      };
+    }
+    // lib.optionalAttrs (config.services.svnserve.enable) { subversion-repository = {
+      svnBaseDir = config.services.svnserve.svnBaseDir;
+    }; }) cfg.extraContainerProperties;
+
+    boot.extraSystemdUnitPaths = [ "/etc/systemd-mutable/system" ];
+
+    system.activationScripts.dysnomia = ''
+      mkdir -p /etc/systemd-mutable/system
+      if [ ! -f /etc/systemd-mutable/system/dysnomia.target ]
+      then
+          ( echo "[Unit]"
+            echo "Description=Services that are activated and deactivated by Dysnomia"
+            echo "After=final.target"
+          ) > /etc/systemd-mutable/system/dysnomia.target
+      fi
+    '';
+  };
+}
diff --git a/nixos/modules/services/misc/errbot.nix b/nixos/modules/services/misc/errbot.nix
new file mode 100644
index 00000000000..b447ba5d438
--- /dev/null
+++ b/nixos/modules/services/misc/errbot.nix
@@ -0,0 +1,104 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.errbot;
+  pluginEnv = plugins: pkgs.buildEnv {
+    name = "errbot-plugins";
+    paths = plugins;
+  };
+  mkConfigDir = instanceCfg: dataDir: pkgs.writeTextDir "config.py" ''
+    import logging
+    BACKEND = '${instanceCfg.backend}'
+    BOT_DATA_DIR = '${dataDir}'
+    BOT_EXTRA_PLUGIN_DIR = '${pluginEnv instanceCfg.plugins}'
+
+    BOT_LOG_LEVEL = logging.${instanceCfg.logLevel}
+    BOT_LOG_FILE = False
+
+    BOT_ADMINS = (${concatMapStringsSep "," (name: "'${name}'") instanceCfg.admins})
+
+    BOT_IDENTITY = ${builtins.toJSON instanceCfg.identity}
+
+    ${instanceCfg.extraConfig}
+  '';
+in {
+  options = {
+    services.errbot.instances = mkOption {
+      default = {};
+      description = "Errbot instance configs";
+      type = types.attrsOf (types.submodule {
+        options = {
+          dataDir = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            description = "Data directory for errbot instance.";
+          };
+
+          plugins = mkOption {
+            type = types.listOf types.package;
+            default = [];
+            description = "List of errbot plugin derivations.";
+          };
+
+          logLevel = mkOption {
+            type = types.str;
+            default = "INFO";
+            description = "Errbot log level";
+          };
+
+          admins = mkOption {
+            type = types.listOf types.str;
+            default = [];
+            description = "List of identifiers of errbot admins.";
+          };
+
+          backend = mkOption {
+            type = types.str;
+            default = "XMPP";
+            description = "Errbot backend name.";
+          };
+
+          identity = mkOption {
+            type = types.attrs;
+            description = "Errbot identity configuration";
+          };
+
+          extraConfig = mkOption {
+            type = types.lines;
+            default = "";
+            description = "String to be appended to the config verbatim";
+          };
+        };
+      });
+    };
+  };
+
+  config = mkIf (cfg.instances != {}) {
+    users.users.errbot = {
+      group = "errbot";
+      isSystemUser = true;
+    };
+    users.groups.errbot = {};
+
+    systemd.services = mapAttrs' (name: instanceCfg: nameValuePair "errbot-${name}" (
+    let
+      dataDir = if instanceCfg.dataDir != null then instanceCfg.dataDir else
+        "/var/lib/errbot/${name}";
+    in {
+      after = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        mkdir -p ${dataDir}
+        chown -R errbot:errbot ${dataDir}
+      '';
+      serviceConfig = {
+        User = "errbot";
+        Restart = "on-failure";
+        ExecStart = "${pkgs.errbot}/bin/errbot -c ${mkConfigDir instanceCfg dataDir}/config.py";
+        PermissionsStartOnly = true;
+      };
+    })) cfg.instances;
+  };
+}
diff --git a/nixos/modules/services/misc/etcd.nix b/nixos/modules/services/misc/etcd.nix
new file mode 100644
index 00000000000..3925b7dd163
--- /dev/null
+++ b/nixos/modules/services/misc/etcd.nix
@@ -0,0 +1,205 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.etcd;
+  opt = options.services.etcd;
+
+in {
+
+  options.services.etcd = {
+    enable = mkOption {
+      description = "Whether to enable etcd.";
+      default = false;
+      type = types.bool;
+    };
+
+    name = mkOption {
+      description = "Etcd unique node name.";
+      default = config.networking.hostName;
+      defaultText = literalExpression "config.networking.hostName";
+      type = types.str;
+    };
+
+    advertiseClientUrls = mkOption {
+      description = "Etcd list of this member's client URLs to advertise to the rest of the cluster.";
+      default = cfg.listenClientUrls;
+      defaultText = literalExpression "config.${opt.listenClientUrls}";
+      type = types.listOf types.str;
+    };
+
+    listenClientUrls = mkOption {
+      description = "Etcd list of URLs to listen on for client traffic.";
+      default = ["http://127.0.0.1:2379"];
+      type = types.listOf types.str;
+    };
+
+    listenPeerUrls = mkOption {
+      description = "Etcd list of URLs to listen on for peer traffic.";
+      default = ["http://127.0.0.1:2380"];
+      type = types.listOf types.str;
+    };
+
+    initialAdvertisePeerUrls = mkOption {
+      description = "Etcd list of this member's peer URLs to advertise to rest of the cluster.";
+      default = cfg.listenPeerUrls;
+      defaultText = literalExpression "config.${opt.listenPeerUrls}";
+      type = types.listOf types.str;
+    };
+
+    initialCluster = mkOption {
+      description = "Etcd initial cluster configuration for bootstrapping.";
+      default = ["${cfg.name}=http://127.0.0.1:2380"];
+      defaultText = literalExpression ''["''${config.${opt.name}}=http://127.0.0.1:2380"]'';
+      type = types.listOf types.str;
+    };
+
+    initialClusterState = mkOption {
+      description = "Etcd initial cluster configuration for bootstrapping.";
+      default = "new";
+      type = types.enum ["new" "existing"];
+    };
+
+    initialClusterToken = mkOption {
+      description = "Etcd initial cluster token for etcd cluster during bootstrap.";
+      default = "etcd-cluster";
+      type = types.str;
+    };
+
+    discovery = mkOption {
+      description = "Etcd discovery url";
+      default = "";
+      type = types.str;
+    };
+
+    clientCertAuth = mkOption {
+      description = "Whether to use certs for client authentication";
+      default = false;
+      type = types.bool;
+    };
+
+    trustedCaFile = mkOption {
+      description = "Certificate authority file to use for clients";
+      default = null;
+      type = types.nullOr types.path;
+    };
+
+    certFile = mkOption {
+      description = "Cert file to use for clients";
+      default = null;
+      type = types.nullOr types.path;
+    };
+
+    keyFile = mkOption {
+      description = "Key file to use for clients";
+      default = null;
+      type = types.nullOr types.path;
+    };
+
+    peerCertFile = mkOption {
+      description = "Cert file to use for peer to peer communication";
+      default = cfg.certFile;
+      defaultText = literalExpression "config.${opt.certFile}";
+      type = types.nullOr types.path;
+    };
+
+    peerKeyFile = mkOption {
+      description = "Key file to use for peer to peer communication";
+      default = cfg.keyFile;
+      defaultText = literalExpression "config.${opt.keyFile}";
+      type = types.nullOr types.path;
+    };
+
+    peerTrustedCaFile = mkOption {
+      description = "Certificate authority file to use for peer to peer communication";
+      default = cfg.trustedCaFile;
+      defaultText = literalExpression "config.${opt.trustedCaFile}";
+      type = types.nullOr types.path;
+    };
+
+    peerClientCertAuth = mkOption {
+      description = "Whether to check all incoming peer requests from the cluster for valid client certificates signed by the supplied CA";
+      default = false;
+      type = types.bool;
+    };
+
+    extraConf = mkOption {
+      description = ''
+        Etcd extra configuration. See
+        <link xlink:href='https://github.com/coreos/etcd/blob/master/Documentation/op-guide/configuration.md#configuration-flags' />
+      '';
+      type = types.attrsOf types.str;
+      default = {};
+      example = literalExpression ''
+        {
+          "CORS" = "*";
+          "NAME" = "default-name";
+          "MAX_RESULT_BUFFER" = "1024";
+          "MAX_CLUSTER_SIZE" = "9";
+          "MAX_RETRY_ATTEMPTS" = "3";
+        }
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/etcd";
+      description = "Etcd data directory.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 etcd - - -"
+    ];
+
+    systemd.services.etcd = {
+      description = "etcd key-value store";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      environment = (filterAttrs (n: v: v != null) {
+        ETCD_NAME = cfg.name;
+        ETCD_DISCOVERY = cfg.discovery;
+        ETCD_DATA_DIR = cfg.dataDir;
+        ETCD_ADVERTISE_CLIENT_URLS = concatStringsSep "," cfg.advertiseClientUrls;
+        ETCD_LISTEN_CLIENT_URLS = concatStringsSep "," cfg.listenClientUrls;
+        ETCD_LISTEN_PEER_URLS = concatStringsSep "," cfg.listenPeerUrls;
+        ETCD_INITIAL_ADVERTISE_PEER_URLS = concatStringsSep "," cfg.initialAdvertisePeerUrls;
+        ETCD_PEER_TRUSTED_CA_FILE = cfg.peerTrustedCaFile;
+        ETCD_PEER_CERT_FILE = cfg.peerCertFile;
+        ETCD_PEER_KEY_FILE = cfg.peerKeyFile;
+        ETCD_CLIENT_CERT_AUTH = toString cfg.peerClientCertAuth;
+        ETCD_TRUSTED_CA_FILE = cfg.trustedCaFile;
+        ETCD_CERT_FILE = cfg.certFile;
+        ETCD_KEY_FILE = cfg.keyFile;
+      }) // (optionalAttrs (cfg.discovery == ""){
+        ETCD_INITIAL_CLUSTER = concatStringsSep "," cfg.initialCluster;
+        ETCD_INITIAL_CLUSTER_STATE = cfg.initialClusterState;
+        ETCD_INITIAL_CLUSTER_TOKEN = cfg.initialClusterToken;
+      }) // (mapAttrs' (n: v: nameValuePair "ETCD_${n}" v) cfg.extraConf);
+
+      unitConfig = {
+        Documentation = "https://github.com/coreos/etcd";
+      };
+
+      serviceConfig = {
+        Type = "notify";
+        ExecStart = "${pkgs.etcd}/bin/etcd";
+        User = "etcd";
+        LimitNOFILE = 40000;
+      };
+    };
+
+    environment.systemPackages = [ pkgs.etcd ];
+
+    users.users.etcd = {
+      isSystemUser = true;
+      group = "etcd";
+      description = "Etcd daemon user";
+      home = cfg.dataDir;
+    };
+    users.groups.etcd = {};
+  };
+}
diff --git a/nixos/modules/services/misc/etebase-server.nix b/nixos/modules/services/misc/etebase-server.nix
new file mode 100644
index 00000000000..dd84ac37b0d
--- /dev/null
+++ b/nixos/modules/services/misc/etebase-server.nix
@@ -0,0 +1,226 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.etebase-server;
+
+  pythonEnv = pkgs.python3.withPackages (ps: with ps;
+    [ etebase-server daphne ]);
+
+  iniFmt = pkgs.formats.ini {};
+
+  configIni = iniFmt.generate "etebase-server.ini" cfg.settings;
+
+  defaultUser = "etebase-server";
+in
+{
+  imports = [
+    (mkRemovedOptionModule
+      [ "services" "etebase-server" "customIni" ]
+      "Set the option `services.etebase-server.settings' instead.")
+    (mkRemovedOptionModule
+      [ "services" "etebase-server" "database" ]
+      "Set the option `services.etebase-server.settings.database' instead.")
+    (mkRenamedOptionModule
+      [ "services" "etebase-server" "secretFile" ]
+      [ "services" "etebase-server" "settings" "secret_file" ])
+    (mkRenamedOptionModule
+      [ "services" "etebase-server" "host" ]
+      [ "services" "etebase-server" "settings" "allowed_hosts" "allowed_host1" ])
+  ];
+
+  options = {
+    services.etebase-server = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        example = true;
+        description = ''
+          Whether to enable the Etebase server.
+
+          Once enabled you need to create an admin user by invoking the
+          shell command <literal>etebase-server createsuperuser</literal> with
+          the user specified by the <literal>user</literal> option or a superuser.
+          Then you can login and create accounts on your-etebase-server.com/admin
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/etebase-server";
+        description = "Directory to store the Etebase server data.";
+      };
+
+      port = mkOption {
+        type = with types; nullOr port;
+        default = 8001;
+        description = "Port to listen on.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to open ports in the firewall for the server.
+        '';
+      };
+
+      unixSocket = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "The path to the socket to bind to.";
+        example = "/run/etebase-server/etebase-server.sock";
+      };
+
+      settings = mkOption {
+        type = lib.types.submodule {
+          freeformType = iniFmt.type;
+
+          options = {
+            global = {
+              debug = mkOption {
+                type = types.bool;
+                default = false;
+                description = ''
+                  Whether to set django's DEBUG flag.
+                '';
+              };
+              secret_file = mkOption {
+                type = with types; nullOr str;
+                default = null;
+                description = ''
+                  The path to a file containing the secret
+                  used as django's SECRET_KEY.
+                '';
+              };
+              static_root = mkOption {
+                type = types.str;
+                default = "${cfg.dataDir}/static";
+                defaultText = literalExpression ''"''${config.services.etebase-server.dataDir}/static"'';
+                description = "The directory for static files.";
+              };
+              media_root = mkOption {
+                type = types.str;
+                default = "${cfg.dataDir}/media";
+                defaultText = literalExpression ''"''${config.services.etebase-server.dataDir}/media"'';
+                description = "The media directory.";
+              };
+            };
+            allowed_hosts = {
+              allowed_host1 = mkOption {
+                type = types.str;
+                default = "0.0.0.0";
+                example = "localhost";
+                description = ''
+                  The main host that is allowed access.
+                '';
+              };
+            };
+            database = {
+              engine = mkOption {
+                type = types.enum [ "django.db.backends.sqlite3" "django.db.backends.postgresql" ];
+                default = "django.db.backends.sqlite3";
+                description = "The database engine to use.";
+              };
+              name = mkOption {
+                type = types.str;
+                default = "${cfg.dataDir}/db.sqlite3";
+                defaultText = literalExpression ''"''${config.services.etebase-server.dataDir}/db.sqlite3"'';
+                description = "The database name.";
+              };
+            };
+          };
+        };
+        default = {};
+        description = ''
+          Configuration for <package>etebase-server</package>. Refer to
+          <link xlink:href="https://github.com/etesync/server/blob/master/etebase-server.ini.example" />
+          and <link xlink:href="https://github.com/etesync/server/wiki" />
+          for details on supported values.
+        '';
+        example = {
+          global = {
+            debug = true;
+            media_root = "/path/to/media";
+          };
+          allowed_hosts = {
+            allowed_host2 = "localhost";
+          };
+        };
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = defaultUser;
+        description = "User under which Etebase server runs.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = with pkgs; [
+      (runCommand "etebase-server" {
+        buildInputs = [ makeWrapper ];
+      } ''
+        makeWrapper ${pythonEnv}/bin/etebase-server \
+          $out/bin/etebase-server \
+          --run "cd ${cfg.dataDir}" \
+          --prefix ETEBASE_EASY_CONFIG_PATH : "${configIni}"
+      '')
+    ];
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
+    ];
+
+    systemd.services.etebase-server = {
+      description = "An Etebase (EteSync 2.0) server";
+      after = [ "network.target" "systemd-tmpfiles-setup.service" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        User = cfg.user;
+        Restart = "always";
+        WorkingDirectory = cfg.dataDir;
+      };
+      environment = {
+        PYTHONPATH = "${pythonEnv}/${pkgs.python3.sitePackages}";
+        ETEBASE_EASY_CONFIG_PATH = configIni;
+      };
+      preStart = ''
+        # Auto-migrate on first run or if the package has changed
+        versionFile="${cfg.dataDir}/src-version"
+        if [[ $(cat "$versionFile" 2>/dev/null) != ${pkgs.etebase-server} ]]; then
+          ${pythonEnv}/bin/etebase-server migrate --no-input
+          ${pythonEnv}/bin/etebase-server collectstatic --no-input --clear
+          echo ${pkgs.etebase-server} > "$versionFile"
+        fi
+      '';
+      script =
+        let
+          networking = if cfg.unixSocket != null
+          then "-u ${cfg.unixSocket}"
+          else "-b 0.0.0.0 -p ${toString cfg.port}";
+        in ''
+          cd "${pythonEnv}/lib/etebase-server";
+          ${pythonEnv}/bin/daphne ${networking} \
+            etebase_server.asgi:application
+        '';
+    };
+
+    users = optionalAttrs (cfg.user == defaultUser) {
+      users.${defaultUser} = {
+        isSystemUser = true;
+        group = defaultUser;
+        home = cfg.dataDir;
+      };
+
+      groups.${defaultUser} = {};
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/etesync-dav.nix b/nixos/modules/services/misc/etesync-dav.nix
new file mode 100644
index 00000000000..9d7cfda371b
--- /dev/null
+++ b/nixos/modules/services/misc/etesync-dav.nix
@@ -0,0 +1,92 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.etesync-dav;
+in
+  {
+    options.services.etesync-dav = {
+      enable = mkEnableOption "etesync-dav";
+
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "The server host address.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 37358;
+        description = "The server host port.";
+      };
+
+      apiUrl = mkOption {
+        type = types.str;
+        default = "https://api.etesync.com/";
+        description = "The url to the etesync API.";
+      };
+
+      openFirewall = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to open the firewall for the specified port.";
+      };
+
+      sslCertificate = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/etesync.crt";
+        description = ''
+          Path to server SSL certificate. It will be copied into
+          etesync-dav's data directory.
+        '';
+      };
+
+      sslCertificateKey = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/etesync.key";
+        description = ''
+          Path to server SSL certificate key.  It will be copied into
+          etesync-dav's data directory.
+        '';
+      };
+    };
+
+    config = mkIf cfg.enable {
+      networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
+      systemd.services.etesync-dav = {
+        description = "etesync-dav - A CalDAV and CardDAV adapter for EteSync";
+        after = [ "network-online.target" ];
+        wantedBy = [ "multi-user.target" ];
+        path = [ pkgs.etesync-dav ];
+        environment = {
+          ETESYNC_LISTEN_ADDRESS = cfg.host;
+          ETESYNC_LISTEN_PORT = toString cfg.port;
+          ETESYNC_URL = cfg.apiUrl;
+          ETESYNC_DATA_DIR = "/var/lib/etesync-dav";
+        };
+
+        serviceConfig = {
+          Type = "simple";
+          DynamicUser = true;
+          StateDirectory = "etesync-dav";
+          ExecStart = "${pkgs.etesync-dav}/bin/etesync-dav";
+          ExecStartPre = mkIf (cfg.sslCertificate != null || cfg.sslCertificateKey != null) (
+            pkgs.writers.writeBash "etesync-dav-copy-keys" ''
+              ${optionalString (cfg.sslCertificate != null) ''
+                cp ${toString cfg.sslCertificate} $STATE_DIRECTORY/etesync.crt
+              ''}
+              ${optionalString (cfg.sslCertificateKey != null) ''
+                cp ${toString cfg.sslCertificateKey} $STATE_DIRECTORY/etesync.key
+              ''}
+            ''
+          );
+          Restart = "on-failure";
+          RestartSec = "30min 1s";
+        };
+      };
+    };
+  }
diff --git a/nixos/modules/services/misc/ethminer.nix b/nixos/modules/services/misc/ethminer.nix
new file mode 100644
index 00000000000..95afb0460fb
--- /dev/null
+++ b/nixos/modules/services/misc/ethminer.nix
@@ -0,0 +1,117 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ethminer;
+  poolUrl = escapeShellArg "stratum1+tcp://${cfg.wallet}@${cfg.pool}:${toString cfg.stratumPort}/${cfg.rig}/${cfg.registerMail}";
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.ethminer = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable ethminer ether mining.";
+      };
+
+      recheckInterval = mkOption {
+        type = types.int;
+        default = 2000;
+        description = "Interval in milliseconds between farm rechecks.";
+      };
+
+      toolkit = mkOption {
+        type = types.enum [ "cuda" "opencl" ];
+        default = "cuda";
+        description = "Cuda or opencl toolkit.";
+      };
+
+      apiPort = mkOption {
+        type = types.int;
+        default = -3333;
+        description = "Ethminer api port. minus sign puts api in read-only mode.";
+      };
+
+      wallet = mkOption {
+        type = types.str;
+        example = "0x0123456789abcdef0123456789abcdef01234567";
+        description = "Ethereum wallet address.";
+      };
+
+      pool = mkOption {
+        type = types.str;
+        example = "eth-us-east1.nanopool.org";
+        description = "Mining pool address.";
+      };
+
+      stratumPort = mkOption {
+        type = types.port;
+        default = 9999;
+        description = "Stratum protocol tcp port.";
+      };
+
+      rig = mkOption {
+        type = types.str;
+        default = "mining-rig-name";
+        description = "Mining rig name.";
+      };
+
+      registerMail = mkOption {
+        type = types.str;
+        example = "email%40example.org";
+        description = "Url encoded email address to register with pool.";
+      };
+
+      maxPower = mkOption {
+        type = types.int;
+        default = 113;
+        description = "Miner max watt usage.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.ethminer = {
+      path = [ pkgs.cudatoolkit ];
+      description = "ethminer ethereum mining service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStartPre = "${pkgs.ethminer}/bin/.ethminer-wrapped --list-devices";
+        ExecStartPost = optional (cfg.toolkit == "cuda") "+${getBin config.boot.kernelPackages.nvidia_x11}/bin/nvidia-smi -pl ${toString cfg.maxPower}";
+        Restart = "always";
+      };
+
+      environment = {
+        LD_LIBRARY_PATH = "${config.boot.kernelPackages.nvidia_x11}/lib";
+      };
+
+      script = ''
+        ${pkgs.ethminer}/bin/.ethminer-wrapped \
+          --farm-recheck ${toString cfg.recheckInterval} \
+          --report-hashrate \
+          --${cfg.toolkit} \
+          --api-port ${toString cfg.apiPort} \
+          --pool ${poolUrl}
+      '';
+
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/exhibitor.nix b/nixos/modules/services/misc/exhibitor.nix
new file mode 100644
index 00000000000..4c935efbd84
--- /dev/null
+++ b/nixos/modules/services/misc/exhibitor.nix
@@ -0,0 +1,422 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.exhibitor;
+  opt = options.services.exhibitor;
+  exhibitorConfig = ''
+    zookeeper-install-directory=${cfg.baseDir}/zookeeper
+    zookeeper-data-directory=${cfg.zkDataDir}
+    zookeeper-log-directory=${cfg.zkLogDir}
+    zoo-cfg-extra=${cfg.zkExtraCfg}
+    client-port=${toString cfg.zkClientPort}
+    connect-port=${toString cfg.zkConnectPort}
+    election-port=${toString cfg.zkElectionPort}
+    cleanup-period-ms=${toString cfg.zkCleanupPeriod}
+    servers-spec=${concatStringsSep "," cfg.zkServersSpec}
+    auto-manage-instances=${toString cfg.autoManageInstances}
+    ${cfg.extraConf}
+  '';
+  # NB: toString rather than lib.boolToString on cfg.autoManageInstances is intended.
+  # Exhibitor tests if it's an integer not equal to 0, so the empty string (toString false)
+  # will operate in the same fashion as a 0.
+  configDir = pkgs.writeTextDir "exhibitor.properties" exhibitorConfig;
+  cliOptionsCommon = {
+    configtype = cfg.configType;
+    defaultconfig = "${configDir}/exhibitor.properties";
+    port = toString cfg.port;
+    hostname = cfg.hostname;
+    headingtext = if (cfg.headingText != null) then (lib.escapeShellArg cfg.headingText) else null;
+    nodemodification = lib.boolToString cfg.nodeModification;
+    configcheckms = toString cfg.configCheckMs;
+    jquerystyle = cfg.jqueryStyle;
+    loglines = toString cfg.logLines;
+    servo = lib.boolToString cfg.servo;
+    timeout = toString cfg.timeout;
+  };
+  s3CommonOptions = { s3region = cfg.s3Region; s3credentials = cfg.s3Credentials; };
+  cliOptionsPerConfig = {
+    s3 = {
+      s3config = "${cfg.s3Config.bucketName}:${cfg.s3Config.objectKey}";
+      s3configprefix = cfg.s3Config.configPrefix;
+    };
+    zookeeper = {
+      zkconfigconnect = concatStringsSep "," cfg.zkConfigConnect;
+      zkconfigexhibitorpath = cfg.zkConfigExhibitorPath;
+      zkconfigpollms = toString cfg.zkConfigPollMs;
+      zkconfigretry = "${toString cfg.zkConfigRetry.sleepMs}:${toString cfg.zkConfigRetry.retryQuantity}";
+      zkconfigzpath = cfg.zkConfigZPath;
+      zkconfigexhibitorport = toString cfg.zkConfigExhibitorPort; # NB: This might be null
+    };
+    file = {
+      fsconfigdir = cfg.fsConfigDir;
+      fsconfiglockprefix = cfg.fsConfigLockPrefix;
+      fsConfigName = fsConfigName;
+    };
+    none = {
+      noneconfigdir = configDir;
+    };
+  };
+  cliOptions = concatStringsSep " " (mapAttrsToList (k: v: "--${k} ${v}") (filterAttrs (k: v: v != null && v != "") (cliOptionsCommon //
+               cliOptionsPerConfig.${cfg.configType} //
+               s3CommonOptions //
+               optionalAttrs cfg.s3Backup { s3backup = "true"; } //
+               optionalAttrs cfg.fileSystemBackup { filesystembackup = "true"; }
+               )));
+in
+{
+  options = {
+    services.exhibitor = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "
+          Whether to enable the exhibitor server.
+        ";
+      };
+      # See https://github.com/soabase/exhibitor/wiki/Running-Exhibitor for what these mean
+      # General options for any type of config
+      port = mkOption {
+        type = types.int;
+        default = 8080;
+        description = ''
+          The port for exhibitor to listen on and communicate with other exhibitors.
+        '';
+      };
+      baseDir = mkOption {
+        type = types.str;
+        default = "/var/exhibitor";
+        description = ''
+          Baseline directory for exhibitor runtime config.
+        '';
+      };
+      configType = mkOption {
+        type = types.enum [ "file" "s3" "zookeeper" "none" ];
+        description = ''
+          Which configuration type you want to use. Additional config will be
+          required depending on which type you are using.
+        '';
+      };
+      hostname = mkOption {
+        type = types.nullOr types.str;
+        description = ''
+          Hostname to use and advertise
+        '';
+        default = null;
+      };
+      nodeModification = mkOption {
+        type = types.bool;
+        description = ''
+          Whether the Explorer UI will allow nodes to be modified (use with caution).
+        '';
+        default = true;
+      };
+      configCheckMs = mkOption {
+        type = types.int;
+        description = ''
+          Period (ms) to check for shared config updates.
+        '';
+        default = 30000;
+      };
+      headingText = mkOption {
+        type = types.nullOr types.str;
+        description = ''
+          Extra text to display in UI header
+        '';
+        default = null;
+      };
+      jqueryStyle = mkOption {
+        type = types.enum [ "red" "black" "custom" ];
+        description = ''
+          Styling used for the JQuery-based UI.
+        '';
+        default = "red";
+      };
+      logLines = mkOption {
+        type = types.int;
+        description = ''
+        Max lines of logging to keep in memory for display.
+        '';
+        default = 1000;
+      };
+      servo = mkOption {
+        type = types.bool;
+        description = ''
+          ZooKeeper will be queried once a minute for its state via the 'mntr' four
+          letter word (this requires ZooKeeper 3.4.x+). Servo will be used to publish
+          this data via JMX.
+        '';
+        default = false;
+      };
+      timeout = mkOption {
+        type = types.int;
+        description = ''
+          Connection timeout (ms) for ZK connections.
+        '';
+        default = 30000;
+      };
+      autoManageInstances = mkOption {
+        type = types.bool;
+        description = ''
+          Automatically manage ZooKeeper instances in the ensemble
+        '';
+        default = false;
+      };
+      zkDataDir = mkOption {
+        type = types.str;
+        default = "${cfg.baseDir}/zkData";
+        defaultText = literalExpression ''"''${config.${opt.baseDir}}/zkData"'';
+        description = ''
+          The Zookeeper data directory
+        '';
+      };
+      zkLogDir = mkOption {
+        type = types.path;
+        default = "${cfg.baseDir}/zkLogs";
+        defaultText = literalExpression ''"''${config.${opt.baseDir}}/zkLogs"'';
+        description = ''
+          The Zookeeper logs directory
+        '';
+      };
+      extraConf = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Extra Exhibitor configuration to put in the ZooKeeper config file.
+        '';
+      };
+      zkExtraCfg = mkOption {
+        type = types.str;
+        default = "initLimit=5&syncLimit=2&tickTime=2000";
+        description = ''
+          Extra options to pass into Zookeeper
+        '';
+      };
+      zkClientPort = mkOption {
+        type = types.int;
+        default = 2181;
+        description = ''
+          Zookeeper client port
+        '';
+      };
+      zkConnectPort = mkOption {
+        type = types.int;
+        default = 2888;
+        description = ''
+          The port to use for followers to talk to each other.
+        '';
+      };
+      zkElectionPort = mkOption {
+        type = types.int;
+        default = 3888;
+        description = ''
+          The port for Zookeepers to use for leader election.
+        '';
+      };
+      zkCleanupPeriod = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          How often (in milliseconds) to run the Zookeeper log cleanup task.
+        '';
+      };
+      zkServersSpec = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Zookeeper server spec for all servers in the ensemble.
+        '';
+        example = [ "S:1:zk1.example.com" "S:2:zk2.example.com" "S:3:zk3.example.com" "O:4:zk-observer.example.com" ];
+      };
+
+      # Backup options
+      s3Backup = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable backups to S3
+        '';
+      };
+      fileSystemBackup = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enables file system backup of ZooKeeper log files
+        '';
+      };
+
+      # Options for using zookeeper configType
+      zkConfigConnect = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          The initial connection string for ZooKeeper shared config storage
+        '';
+        example = ["host1:2181" "host2:2181"];
+      };
+      zkConfigExhibitorPath = mkOption {
+        type = types.str;
+        description = ''
+          If the ZooKeeper shared config is also running Exhibitor, the URI path for the REST call
+        '';
+        default = "/";
+      };
+      zkConfigExhibitorPort = mkOption {
+        type = types.nullOr types.int;
+        description = ''
+          If the ZooKeeper shared config is also running Exhibitor, the port that
+          Exhibitor is listening on. IMPORTANT: if this value is not set it implies
+          that Exhibitor is not being used on the ZooKeeper shared config.
+        '';
+      };
+      zkConfigPollMs = mkOption {
+        type = types.int;
+        description = ''
+          The period in ms to check for changes in the config ensemble
+        '';
+        default = 10000;
+      };
+      zkConfigRetry = {
+        sleepMs = mkOption {
+          type = types.int;
+          default = 1000;
+          description = ''
+            Retry sleep time connecting to the ZooKeeper config
+          '';
+        };
+        retryQuantity = mkOption {
+          type = types.int;
+          default = 3;
+          description = ''
+            Retries connecting to the ZooKeeper config
+          '';
+        };
+      };
+      zkConfigZPath = mkOption {
+        type = types.str;
+        description = ''
+          The base ZPath that Exhibitor should use
+        '';
+        example = "/exhibitor/config";
+      };
+
+      # Config options for s3 configType
+      s3Config = {
+        bucketName = mkOption {
+          type = types.str;
+          description = ''
+            Bucket name to store config
+          '';
+        };
+        objectKey = mkOption {
+          type = types.str;
+          description = ''
+            S3 key name to store the config
+          '';
+        };
+        configPrefix = mkOption {
+          type = types.str;
+          description = ''
+            When using AWS S3 shared config files, the prefix to use for values such as locks
+          '';
+          default = "exhibitor-";
+        };
+      };
+
+      # The next two are used for either s3backup or s3 configType
+      s3Credentials = mkOption {
+        type = types.nullOr types.path;
+        description = ''
+          Optional credentials to use for s3backup or s3config. Argument is the path
+          to an AWS credential properties file with two properties:
+          com.netflix.exhibitor.s3.access-key-id and com.netflix.exhibitor.s3.access-secret-key
+        '';
+        default = null;
+      };
+      s3Region = mkOption {
+        type = types.nullOr types.str;
+        description = ''
+          Optional region for S3 calls
+        '';
+        default = null;
+      };
+
+      # Config options for file config type
+      fsConfigDir = mkOption {
+        type = types.path;
+        description = ''
+          Directory to store Exhibitor properties (cannot be used with s3config).
+          Exhibitor uses file system locks so you can specify a shared location
+          so as to enable complete ensemble management.
+        '';
+      };
+      fsConfigLockPrefix = mkOption {
+        type = types.str;
+        description = ''
+          A prefix for a locking mechanism used in conjunction with fsconfigdir
+        '';
+        default = "exhibitor-lock-";
+      };
+      fsConfigName = mkOption {
+        type = types.str;
+        description = ''
+          The name of the file to store config in
+        '';
+        default = "exhibitor.properties";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.exhibitor = {
+      description = "Exhibitor Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      environment = {
+        ZOO_LOG_DIR = cfg.baseDir;
+      };
+      serviceConfig = {
+        /***
+          Exhibitor is a bit un-nixy. It wants to present to you a user interface in order to
+          mutate the configuration of both itself and ZooKeeper, and to coordinate changes
+          among the members of the Zookeeper ensemble. I'm going for a different approach here,
+          which is to manage all the configuration via nix and have it write out the configuration
+          files that exhibitor will use, and to reduce the amount of inter-exhibitor orchestration.
+        ***/
+        ExecStart = ''
+          ${pkgs.exhibitor}/bin/startExhibitor.sh ${cliOptions}
+        '';
+        User = "zookeeper";
+        PermissionsStartOnly = true;
+      };
+      # This is a bit wonky, but the reason for this is that Exhibitor tries to write to
+      # ${cfg.baseDir}/zookeeper/bin/../conf/zoo.cfg
+      # I want everything but the conf directory to be in the immutable nix store, and I want defaults
+      # from the nix store
+      # If I symlink the bin directory in, then bin/../ will resolve to the parent of the symlink in the
+      # immutable nix store. Bind mounting a writable conf over the existing conf might work, but it gets very
+      # messy with trying to copy the existing out into a mutable store.
+      # Another option is to try to patch upstream exhibitor, but the current package just pulls down the
+      # prebuild JARs off of Maven, rather than building them ourselves, as Maven support in Nix isn't
+      # very mature. So, it seems like a reasonable compromise is to just copy out of the immutable store
+      # just before starting the service, so we're running binaries from the immutable store, but we work around
+      # Exhibitor's desire to mutate its current installation.
+      preStart = ''
+        mkdir -m 0700 -p ${cfg.baseDir}/zookeeper
+        # Not doing a chown -R to keep the base ZK files owned by root
+        chown zookeeper ${cfg.baseDir} ${cfg.baseDir}/zookeeper
+        cp -Rf ${pkgs.zookeeper}/* ${cfg.baseDir}/zookeeper
+        chown -R zookeeper ${cfg.baseDir}/zookeeper/conf
+        chmod -R u+w ${cfg.baseDir}/zookeeper/conf
+        replace_what=$(echo ${pkgs.zookeeper} | sed 's/[\/&]/\\&/g')
+        replace_with=$(echo ${cfg.baseDir}/zookeeper | sed 's/[\/&]/\\&/g')
+        sed -i 's/'"$replace_what"'/'"$replace_with"'/g' ${cfg.baseDir}/zookeeper/bin/zk*.sh
+      '';
+    };
+    users.users.zookeeper = {
+      uid = config.ids.uids.zookeeper;
+      description = "Zookeeper daemon user";
+      home = cfg.baseDir;
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/felix.nix b/nixos/modules/services/misc/felix.nix
new file mode 100644
index 00000000000..0283de128af
--- /dev/null
+++ b/nixos/modules/services/misc/felix.nix
@@ -0,0 +1,104 @@
+# Felix server
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.felix;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.felix = {
+
+      enable = mkEnableOption "the Apache Felix OSGi service";
+
+      bundles = mkOption {
+        type = types.listOf types.package;
+        default = [ pkgs.felix_remoteshell ];
+        defaultText = literalExpression "[ pkgs.felix_remoteshell ]";
+        description = "List of bundles that should be activated on startup";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "osgi";
+        description = "User account under which Apache Felix runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "osgi";
+        description = "Group account under which Apache Felix runs.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    users.groups.osgi.gid = config.ids.gids.osgi;
+
+    users.users.osgi =
+      { uid = config.ids.uids.osgi;
+        description = "OSGi user";
+        home = "/homeless-shelter";
+      };
+
+    systemd.services.felix = {
+      description = "Felix server";
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        # Initialise felix instance on first startup
+        if [ ! -d /var/felix ]
+        then
+          # Symlink system files
+
+          mkdir -p /var/felix
+          chown ${cfg.user}:${cfg.group} /var/felix
+
+          for i in ${pkgs.felix}/*
+          do
+              if [ "$i" != "${pkgs.felix}/bundle" ]
+              then
+                  ln -sfn $i /var/felix/$(basename $i)
+              fi
+          done
+
+          # Symlink bundles
+          mkdir -p /var/felix/bundle
+          chown ${cfg.user}:${cfg.group} /var/felix/bundle
+
+          for i in ${pkgs.felix}/bundle/* ${toString cfg.bundles}
+          do
+              if [ -f $i ]
+              then
+                  ln -sfn $i /var/felix/bundle/$(basename $i)
+              elif [ -d $i ]
+              then
+                  for j in $i/bundle/*
+              do
+                  ln -sfn $j /var/felix/bundle/$(basename $j)
+              done
+              fi
+          done
+        fi
+      '';
+
+      script = ''
+        cd /var/felix
+        ${pkgs.su}/bin/su -s ${pkgs.bash}/bin/sh ${cfg.user} -c '${pkgs.jre}/bin/java -jar bin/felix.jar'
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/freeswitch.nix b/nixos/modules/services/misc/freeswitch.nix
new file mode 100644
index 00000000000..472b0b73ff6
--- /dev/null
+++ b/nixos/modules/services/misc/freeswitch.nix
@@ -0,0 +1,104 @@
+{ config, lib, pkgs, ...}:
+with lib;
+let
+  cfg = config.services.freeswitch;
+  pkg = cfg.package;
+  configDirectory = pkgs.runCommand "freeswitch-config-d" { } ''
+    mkdir -p $out
+    cp -rT ${cfg.configTemplate} $out
+    chmod -R +w $out
+    ${concatStringsSep "\n" (mapAttrsToList (fileName: filePath: ''
+      mkdir -p $out/$(dirname ${fileName})
+      cp ${filePath} $out/${fileName}
+    '') cfg.configDir)}
+  '';
+  configPath = if cfg.enableReload
+    then "/etc/freeswitch"
+    else configDirectory;
+in {
+  options = {
+    services.freeswitch = {
+      enable = mkEnableOption "FreeSWITCH";
+      enableReload = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Issue the <literal>reloadxml</literal> command to FreeSWITCH when configuration directory changes (instead of restart).
+          See <link xlink:href="https://freeswitch.org/confluence/display/FREESWITCH/Reloading">FreeSWITCH documentation</link> for more info.
+          The configuration directory is exposed at <filename>/etc/freeswitch</filename>.
+          See also <literal>systemd.services.*.restartIfChanged</literal>.
+        '';
+      };
+      configTemplate = mkOption {
+        type = types.path;
+        default = "${config.services.freeswitch.package}/share/freeswitch/conf/vanilla";
+        defaultText = literalExpression ''"''${config.services.freeswitch.package}/share/freeswitch/conf/vanilla"'';
+        example = literalExpression ''"''${config.services.freeswitch.package}/share/freeswitch/conf/minimal"'';
+        description = ''
+          Configuration template to use.
+          See available templates in <link xlink:href="https://github.com/signalwire/freeswitch/tree/master/conf">FreeSWITCH repository</link>.
+          You can also set your own configuration directory.
+        '';
+      };
+      configDir = mkOption {
+        type = with types; attrsOf path;
+        default = { };
+        example = literalExpression ''
+          {
+            "freeswitch.xml" = ./freeswitch.xml;
+            "dialplan/default.xml" = pkgs.writeText "dialplan-default.xml" '''
+              [xml lines]
+            ''';
+          }
+        '';
+        description = ''
+          Override file in FreeSWITCH config template directory.
+          Each top-level attribute denotes a file path in the configuration directory, its value is the file path.
+          See <link xlink:href="https://freeswitch.org/confluence/display/FREESWITCH/Default+Configuration">FreeSWITCH documentation</link> for more info.
+          Also check available templates in <link xlink:href="https://github.com/signalwire/freeswitch/tree/master/conf">FreeSWITCH repository</link>.
+        '';
+      };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.freeswitch;
+        defaultText = literalExpression "pkgs.freeswitch";
+        description = ''
+          FreeSWITCH package.
+        '';
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    environment.etc.freeswitch = mkIf cfg.enableReload {
+      source = configDirectory;
+    };
+    systemd.services.freeswitch-config-reload = mkIf cfg.enableReload {
+      before = [ "freeswitch.service" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ configDirectory ];
+      serviceConfig = {
+        ExecStart = "/run/current-system/systemd/bin/systemctl try-reload-or-restart freeswitch.service";
+        RemainAfterExit = true;
+        Type = "oneshot";
+      };
+    };
+    systemd.services.freeswitch = {
+      description = "Free and open-source application server for real-time communication";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "freeswitch";
+        ExecStart = "${pkg}/bin/freeswitch -nf \\
+          -mod ${pkg}/lib/freeswitch/mod \\
+          -conf ${configPath} \\
+          -base /var/lib/freeswitch";
+        ExecReload = "${pkg}/bin/fs_cli -x reloadxml";
+        Restart = "on-failure";
+        RestartSec = "5s";
+        CPUSchedulingPolicy = "fifo";
+      };
+    };
+    environment.systemPackages = [ pkg ];
+  };
+}
diff --git a/nixos/modules/services/misc/fstrim.nix b/nixos/modules/services/misc/fstrim.nix
new file mode 100644
index 00000000000..a9fc04b46f0
--- /dev/null
+++ b/nixos/modules/services/misc/fstrim.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.fstrim;
+
+in {
+
+  options = {
+
+    services.fstrim = {
+      enable = mkEnableOption "periodic SSD TRIM of mounted partitions in background";
+
+      interval = mkOption {
+        type = types.str;
+        default = "weekly";
+        description = ''
+          How often we run fstrim. For most desktop and server systems
+          a sufficient trimming frequency is once a week.
+
+          The format is described in
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.packages = [ pkgs.util-linux ];
+
+    systemd.timers.fstrim = {
+      timerConfig = {
+        OnCalendar = cfg.interval;
+      };
+      wantedBy = [ "timers.target" ];
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ ];
+}
diff --git a/nixos/modules/services/misc/gammu-smsd.nix b/nixos/modules/services/misc/gammu-smsd.nix
new file mode 100644
index 00000000000..d4bb58d81dd
--- /dev/null
+++ b/nixos/modules/services/misc/gammu-smsd.nix
@@ -0,0 +1,253 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+let
+  cfg = config.services.gammu-smsd;
+
+  configFile = pkgs.writeText "gammu-smsd.conf" ''
+    [gammu]
+    Device = ${cfg.device.path}
+    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 ""}
+    ${cfg.extraConfig.gammu}
+
+
+    [smsd]
+    LogFile = ${cfg.log.file}
+    Service = ${cfg.backend.service}
+
+    ${optionalString (cfg.backend.service == "files") ''
+      InboxPath = ${cfg.backend.files.inboxPath}
+      OutboxPath = ${cfg.backend.files.outboxPath}
+      SentSMSPath = ${cfg.backend.files.sentSMSPath}
+      ErrorSMSPath = ${cfg.backend.files.errorSMSPath}
+    ''}
+
+    ${optionalString (cfg.backend.service == "sql" && cfg.backend.sql.driver == "sqlite") ''
+      Driver = ${cfg.backend.sql.driver}
+      DBDir = ${cfg.backend.sql.database}
+    ''}
+
+    ${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 ""}
+      '')}
+
+    ${cfg.extraConfig.smsd}
+  '';
+
+  initDBDir = "share/doc/gammu/examples/sql";
+
+  gammuPackage = with cfg.backend; (pkgs.gammu.override {
+    dbiSupport = (service == "sql" && sql.driver == "sqlite");
+    postgresSupport = (service == "sql" && sql.driver == "native_pgsql");
+  });
+
+in {
+  options = {
+    services.gammu-smsd = {
+
+      enable = mkEnableOption "gammu-smsd daemon";
+
+      user = mkOption {
+        type = types.str;
+        default = "smsd";
+        description = "User that has access to the device";
+      };
+
+      device = {
+        path = mkOption {
+          type = types.path;
+          description = "Device node or address of the phone";
+          example = "/dev/ttyUSB2";
+        };
+
+        group = mkOption {
+          type = types.str;
+          default = "root";
+          description = "Owner group of the device";
+          example = "dialout";
+        };
+
+        connection = mkOption {
+          type = types.str;
+          default = "at";
+          description = "Protocol which will be used to talk to the phone";
+        };
+
+        synchronizeTime = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Whether to set time from computer to the phone during starting connection";
+        };
+
+        pin = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = "PIN code for the simcard";
+        };
+      };
+
+
+      log = {
+        file = mkOption {
+          type = types.str;
+          default = "syslog";
+          description = "Path to file where information about communication will be stored";
+        };
+
+        format = mkOption {
+          type = types.enum [ "nothing" "text" "textall" "textalldate" "errors" "errorsdate" "binary" ];
+          default = "errors";
+          description = "Determines what will be logged to the LogFile";
+        };
+      };
+
+
+      extraConfig = {
+        gammu = mkOption {
+          type = types.lines;
+          default = "";
+          description = "Extra config lines to be added into [gammu] section";
+        };
+
+
+        smsd = mkOption {
+          type = types.lines;
+          default = "";
+          description = "Extra config lines to be added into [smsd] section";
+        };
+      };
+
+
+      backend = {
+        service = mkOption {
+          type = types.enum [ "null" "files" "sql" ];
+          default = "null";
+          description = "Service to use to store sms data.";
+        };
+
+        files = {
+          inboxPath = mkOption {
+            type = types.path;
+            default = "/var/spool/sms/inbox/";
+            description = "Where the received SMSes are stored";
+          };
+
+          outboxPath = mkOption {
+            type = types.path;
+            default = "/var/spool/sms/outbox/";
+            description = "Where SMSes to be sent should be placed";
+          };
+
+          sentSMSPath = mkOption {
+            type = types.path;
+            default = "/var/spool/sms/sent/";
+            description = "Where the transmitted SMSes are placed";
+          };
+
+          errorSMSPath = mkOption {
+            type = types.path;
+            default = "/var/spool/sms/error/";
+            description = "Where SMSes with error in transmission is placed";
+          };
+        };
+
+        sql = {
+          driver = mkOption {
+            type = types.enum [ "native_mysql" "native_pgsql" "odbc" "dbi" ];
+            description = "DB driver to use";
+          };
+
+          sqlDialect = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            description = "SQL dialect to use (odbc driver only)";
+          };
+
+          database = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            description = "Database name to store sms data";
+          };
+
+          host = mkOption {
+            type = types.str;
+            default = "localhost";
+            description = "Database server address";
+          };
+
+          user = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            description = "User name used for connection to the database";
+          };
+
+          password = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            description = "User password used for connetion to the database";
+          };
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.${cfg.user} = {
+      description = "gammu-smsd user";
+      isSystemUser = true;
+      group = cfg.device.group;
+    };
+
+    environment.systemPackages = with cfg.backend; [ gammuPackage ]
+    ++ optionals (service == "sql" && sql.driver == "sqlite")  [ pkgs.sqlite ];
+
+    systemd.services.gammu-smsd = {
+      description = "gammu-smsd daemon";
+
+      wantedBy = [ "multi-user.target" ];
+
+      wants = with cfg.backend; [ ]
+      ++ optionals (service == "sql" && sql.driver == "native_pgsql") [ "postgresql.service" ];
+
+      preStart = with cfg.backend;
+
+        optionalString (service == "files") (with files; ''
+          mkdir -m 755 -p ${inboxPath} ${outboxPath} ${sentSMSPath} ${errorSMSPath}
+          chown ${cfg.user} -R ${inboxPath}
+          chown ${cfg.user} -R ${outboxPath}
+          chown ${cfg.user} -R ${sentSMSPath}
+          chown ${cfg.user} -R ${errorSMSPath}
+        '')
+      + optionalString (service == "sql" && sql.driver == "sqlite") ''
+         cat "${gammuPackage}/${initDBDir}/sqlite.sql" \
+         | ${pkgs.sqlite.bin}/bin/sqlite3 ${sql.database}
+        ''
+      + (let execPsql = extraArgs: concatStringsSep " " [
+          (optionalString (sql.password != null) "PGPASSWORD=${sql.password}")
+          "${config.services.postgresql.package}/bin/psql"
+          (optionalString (sql.host != null) "-h ${sql.host}")
+          (optionalString (sql.user != null) "-U ${sql.user}")
+          "$extraArgs"
+          "${sql.database}"
+        ]; in optionalString (service == "sql" && sql.driver == "native_pgsql") ''
+         echo '\i '"${gammuPackage}/${initDBDir}/pgsql.sql" | ${execPsql ""}
+       '');
+
+      serviceConfig = {
+        User = "${cfg.user}";
+        Group = "${cfg.device.group}";
+        PermissionsStartOnly = true;
+        ExecStart = "${gammuPackage}/bin/gammu-smsd -c ${configFile}";
+      };
+
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/geoipupdate.nix b/nixos/modules/services/misc/geoipupdate.nix
new file mode 100644
index 00000000000..3211d4d88e4
--- /dev/null
+++ b/nixos/modules/services/misc/geoipupdate.nix
@@ -0,0 +1,187 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.geoipupdate;
+in
+{
+  imports = [
+    (lib.mkRemovedOptionModule [ "services" "geoip-updater" ] "services.geoip-updater has been removed, use services.geoipupdate instead.")
+  ];
+
+  options = {
+    services.geoipupdate = {
+      enable = lib.mkEnableOption ''
+        periodic downloading of GeoIP databases using
+        <productname>geoipupdate</productname>.
+      '';
+
+      interval = lib.mkOption {
+        type = lib.types.str;
+        default = "weekly";
+        description = ''
+          Update the GeoIP databases at this time / interval.
+          The format is described in
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+
+      settings = lib.mkOption {
+        description = ''
+          <productname>geoipupdate</productname> configuration
+          options. See
+          <link xlink:href="https://github.com/maxmind/geoipupdate/blob/main/doc/GeoIP.conf.md" />
+          for a full list of available options.
+        '';
+        type = lib.types.submodule {
+          freeformType =
+            with lib.types;
+            let
+              type = oneOf [str int bool];
+            in
+              attrsOf (either type (listOf type));
+
+          options = {
+
+            AccountID = lib.mkOption {
+              type = lib.types.int;
+              description = ''
+                Your MaxMind account ID.
+              '';
+            };
+
+            EditionIDs = lib.mkOption {
+              type = with lib.types; listOf (either str int);
+              example = [
+                "GeoLite2-ASN"
+                "GeoLite2-City"
+                "GeoLite2-Country"
+              ];
+              description = ''
+                List of database edition IDs. This includes new string
+                IDs like <literal>GeoIP2-City</literal> and old
+                numeric IDs like <literal>106</literal>.
+              '';
+            };
+
+            LicenseKey = lib.mkOption {
+              type = lib.types.path;
+              description = ''
+                A file containing the <productname>MaxMind</productname>
+                license key.
+              '';
+            };
+
+            DatabaseDirectory = lib.mkOption {
+              type = lib.types.path;
+              default = "/var/lib/GeoIP";
+              example = "/run/GeoIP";
+              description = ''
+                The directory to store the database files in. The
+                directory will be automatically created, the owner
+                changed to <literal>geoip</literal> and permissions
+                set to world readable. This applies if the directory
+                already exists as well, so don't use a directory with
+                sensitive contents.
+              '';
+            };
+
+          };
+        };
+      };
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    services.geoipupdate.settings = {
+      LockFile = "/run/geoipupdate/.lock";
+    };
+
+    systemd.services.geoipupdate-create-db-dir = {
+      serviceConfig.Type = "oneshot";
+      script = ''
+        mkdir -p ${cfg.settings.DatabaseDirectory}
+        chmod 0755 ${cfg.settings.DatabaseDirectory}
+      '';
+    };
+
+    systemd.services.geoipupdate = {
+      description = "GeoIP Updater";
+      requires = [ "geoipupdate-create-db-dir.service" ];
+      after = [
+        "geoipupdate-create-db-dir.service"
+        "network-online.target"
+        "nss-lookup.target"
+      ];
+      wants = [ "network-online.target" ];
+      startAt = cfg.interval;
+      serviceConfig = {
+        ExecStartPre =
+          let
+            geoipupdateKeyValue = lib.generators.toKeyValue {
+              mkKeyValue = lib.flip lib.generators.mkKeyValueDefault " " rec {
+                mkValueString = v: with builtins;
+                  if isInt           v then toString v
+                  else if isString   v then v
+                  else if true  ==   v then "1"
+                  else if false ==   v then "0"
+                  else if isList     v then lib.concatMapStringsSep " " mkValueString v
+                  else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
+              };
+            };
+
+            geoipupdateConf = pkgs.writeText "geoipupdate.conf" (geoipupdateKeyValue cfg.settings);
+
+            script = ''
+              chown geoip "${cfg.settings.DatabaseDirectory}"
+
+              cp ${geoipupdateConf} /run/geoipupdate/GeoIP.conf
+              ${pkgs.replace-secret}/bin/replace-secret '${cfg.settings.LicenseKey}' \
+                                                        '${cfg.settings.LicenseKey}' \
+                                                        /run/geoipupdate/GeoIP.conf
+            '';
+          in
+            "+${pkgs.writeShellScript "start-pre-full-privileges" script}";
+        ExecStart = "${pkgs.geoipupdate}/bin/geoipupdate -f /run/geoipupdate/GeoIP.conf";
+        User = "geoip";
+        DynamicUser = true;
+        ReadWritePaths = cfg.settings.DatabaseDirectory;
+        RuntimeDirectory = "geoipupdate";
+        RuntimeDirectoryMode = 0700;
+        CapabilityBoundingSet = "";
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictNamespaces = true;
+        MemoryDenyWriteExecute = true;
+        LockPersonality = true;
+        SystemCallArchitectures = "native";
+      };
+    };
+
+    systemd.timers.geoipupdate-initial-run = {
+      wantedBy = [ "timers.target" ];
+      unitConfig.ConditionPathExists = "!${cfg.settings.DatabaseDirectory}";
+      timerConfig = {
+        Unit = "geoipupdate.service";
+        OnActiveSec = 0;
+      };
+    };
+  };
+
+  meta.maintainers = [ lib.maintainers.talyz ];
+}
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
new file mode 100644
index 00000000000..bc7bb663ee0
--- /dev/null
+++ b/nixos/modules/services/misc/gitea.nix
@@ -0,0 +1,663 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gitea;
+  opt = options.services.gitea;
+  gitea = cfg.package;
+  pg = config.services.postgresql;
+  useMysql = cfg.database.type == "mysql";
+  usePostgresql = cfg.database.type == "postgres";
+  useSqlite = cfg.database.type == "sqlite3";
+  configFile = pkgs.writeText "app.ini" ''
+    APP_NAME = ${cfg.appName}
+    RUN_USER = ${cfg.user}
+    RUN_MODE = prod
+
+    ${generators.toINI {} cfg.settings}
+
+    ${optionalString (cfg.extraConfig != null) cfg.extraConfig}
+  '';
+in
+
+{
+  options = {
+    services.gitea = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Enable Gitea Service.";
+      };
+
+      package = mkOption {
+        default = pkgs.gitea;
+        type = types.package;
+        defaultText = literalExpression "pkgs.gitea";
+        description = "gitea derivation to use";
+      };
+
+      useWizard = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Do not generate a configuration and use gitea' installation wizard instead. The first registered user will be administrator.";
+      };
+
+      stateDir = mkOption {
+        default = "/var/lib/gitea";
+        type = types.str;
+        description = "gitea data directory.";
+      };
+
+      log = {
+        rootPath = mkOption {
+          default = "${cfg.stateDir}/log";
+          defaultText = literalExpression ''"''${config.${opt.stateDir}}/log"'';
+          type = types.str;
+          description = "Root path for log files.";
+        };
+        level = mkOption {
+          default = "Info";
+          type = types.enum [ "Trace" "Debug" "Info" "Warn" "Error" "Critical" ];
+          description = "General log level.";
+        };
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "gitea";
+        description = "User account under which gitea runs.";
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum [ "sqlite3" "mysql" "postgres" ];
+          example = "mysql";
+          default = "sqlite3";
+          description = "Database engine to use.";
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "Database host address.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = (if !usePostgresql then 3306 else pg.port);
+          defaultText = literalExpression ''
+            if config.${opt.database.type} != "postgresql"
+            then 3306
+            else config.${options.services.postgresql.port}
+          '';
+          description = "Database host port.";
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "gitea";
+          description = "Database name.";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "gitea";
+          description = "Database user.";
+        };
+
+        password = mkOption {
+          type = types.str;
+          default = "";
+          description = ''
+            The password corresponding to <option>database.user</option>.
+            Warning: this is stored in cleartext in the Nix store!
+            Use <option>database.passwordFile</option> instead.
+          '';
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          example = "/run/keys/gitea-dbpassword";
+          description = ''
+            A file containing the password corresponding to
+            <option>database.user</option>.
+          '';
+        };
+
+        socket = mkOption {
+          type = types.nullOr types.path;
+          default = if (cfg.database.createDatabase && usePostgresql) then "/run/postgresql" else if (cfg.database.createDatabase && useMysql) then "/run/mysqld/mysqld.sock" else null;
+          defaultText = literalExpression "null";
+          example = "/run/mysqld/mysqld.sock";
+          description = "Path to the unix socket file to use for authentication.";
+        };
+
+        path = mkOption {
+          type = types.str;
+          default = "${cfg.stateDir}/data/gitea.db";
+          defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/gitea.db"'';
+          description = "Path to the sqlite3 database file.";
+        };
+
+        createDatabase = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Whether to create a local database automatically.";
+        };
+      };
+
+      dump = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Enable a timer that runs gitea dump to generate backup-files of the
+            current gitea database and repositories.
+          '';
+        };
+
+        interval = mkOption {
+          type = types.str;
+          default = "04:31";
+          example = "hourly";
+          description = ''
+            Run a gitea dump at this interval. Runs by default at 04:31 every day.
+
+            The format is described in
+            <citerefentry><refentrytitle>systemd.time</refentrytitle>
+            <manvolnum>7</manvolnum></citerefentry>.
+          '';
+        };
+
+        backupDir = mkOption {
+          type = types.str;
+          default = "${cfg.stateDir}/dump";
+          defaultText = literalExpression ''"''${config.${opt.stateDir}}/dump"'';
+          description = "Path to the dump files.";
+        };
+
+        type = mkOption {
+          type = types.enum [ "zip" "rar" "tar" "sz" "tar.gz" "tar.xz" "tar.bz2" "tar.br" "tar.lz4" ];
+          default = "zip";
+          description = "Archive format used to store the dump file.";
+        };
+
+        file = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = "Filename to be used for the dump. If `null` a default name is choosen by gitea.";
+          example = "gitea-dump";
+        };
+      };
+
+      ssh = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Enable external SSH feature.";
+        };
+
+        clonePort = mkOption {
+          type = types.int;
+          default = 22;
+          example = 2222;
+          description = ''
+            SSH port displayed in clone URL.
+            The option is required to configure a service when the external visible port
+            differs from the local listening port i.e. if port forwarding is used.
+          '';
+        };
+      };
+
+      lfs = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Enables git-lfs support.";
+        };
+
+        contentDir = mkOption {
+          type = types.str;
+          default = "${cfg.stateDir}/data/lfs";
+          defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/lfs"'';
+          description = "Where to store LFS files.";
+        };
+      };
+
+      appName = mkOption {
+        type = types.str;
+        default = "gitea: Gitea Service";
+        description = "Application name.";
+      };
+
+      repositoryRoot = mkOption {
+        type = types.str;
+        default = "${cfg.stateDir}/repositories";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"'';
+        description = "Path to the git repositories.";
+      };
+
+      domain = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Domain name of your server.";
+      };
+
+      rootUrl = mkOption {
+        type = types.str;
+        default = "http://localhost:3000/";
+        description = "Full public URL of gitea server.";
+      };
+
+      httpAddress = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "HTTP listen address.";
+      };
+
+      httpPort = mkOption {
+        type = types.int;
+        default = 3000;
+        description = "HTTP listen port.";
+      };
+
+      enableUnixSocket = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Configure Gitea to listen on a unix socket instead of the default TCP port.";
+      };
+
+      cookieSecure = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Marks session cookies as "secure" as a hint for browsers to only send
+          them via HTTPS. This option is recommend, if gitea is being served over HTTPS.
+        '';
+      };
+
+      staticRootPath = mkOption {
+        type = types.either types.str types.path;
+        default = gitea.data;
+        defaultText = literalExpression "package.data";
+        example = "/var/lib/gitea/data";
+        description = "Upper level of template and static files path.";
+      };
+
+      mailerPasswordFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/var/lib/secrets/gitea/mailpw";
+        description = "Path to a file containing the SMTP password.";
+      };
+
+      disableRegistration = mkEnableOption "the registration lock" // {
+        description = ''
+          By default any user can create an account on this <literal>gitea</literal> instance.
+          This can be disabled by using this option.
+
+          <emphasis>Note:</emphasis> please keep in mind that this should be added after the initial
+          deploy unless <link linkend="opt-services.gitea.useWizard">services.gitea.useWizard</link>
+          is <literal>true</literal> as the first registered user will be the administrator if
+          no install wizard is used.
+        '';
+      };
+
+      settings = mkOption {
+        type = with types; attrsOf (attrsOf (oneOf [ bool int str ]));
+        default = {};
+        description = ''
+          Gitea configuration. Refer to <link xlink:href="https://docs.gitea.io/en-us/config-cheat-sheet/"/>
+          for details on supported values.
+        '';
+        example = literalExpression ''
+          {
+            "cron.sync_external_users" = {
+              RUN_AT_START = true;
+              SCHEDULE = "@every 24h";
+              UPDATE_EXISTING = true;
+            };
+            mailer = {
+              ENABLED = true;
+              MAILER_TYPE = "sendmail";
+              FROM = "do-not-reply@example.org";
+              SENDMAIL_PATH = "''${pkgs.system-sendmail}/bin/sendmail";
+            };
+            other = {
+              SHOW_FOOTER_VERSION = false;
+            };
+          }
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "Configuration lines appended to the generated gitea configuration file.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.database.createDatabase -> 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 = {
+      database = mkMerge [
+        {
+          DB_TYPE = cfg.database.type;
+        }
+        (mkIf (useMysql || usePostgresql) {
+          HOST = if cfg.database.socket != null then cfg.database.socket else cfg.database.host + ":" + toString cfg.database.port;
+          NAME = cfg.database.name;
+          USER = cfg.database.user;
+          PASSWD = "#dbpass#";
+        })
+        (mkIf useSqlite {
+          PATH = cfg.database.path;
+        })
+        (mkIf usePostgresql {
+          SSL_MODE = "disable";
+        })
+      ];
+
+      repository = {
+        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.ssh.enable {
+          DISABLE_SSH = false;
+          SSH_PORT = cfg.ssh.clonePort;
+        })
+        (mkIf (!cfg.ssh.enable) {
+          DISABLE_SSH = true;
+        })
+        (mkIf cfg.lfs.enable {
+          LFS_START_SERVER = true;
+          LFS_CONTENT_PATH = cfg.lfs.contentDir;
+        })
+
+      ];
+
+      session = {
+        COOKIE_NAME = "session";
+        COOKIE_SECURE = cfg.cookieSecure;
+      };
+
+      security = {
+        SECRET_KEY = "#secretkey#";
+        INTERNAL_TOKEN = "#internaltoken#";
+        INSTALL_LOCK = true;
+      };
+
+      log = {
+        ROOT_PATH = cfg.log.rootPath;
+        LEVEL = cfg.log.level;
+      };
+
+      service = {
+        DISABLE_REGISTRATION = cfg.disableRegistration;
+      };
+
+      mailer = mkIf (cfg.mailerPasswordFile != null) {
+        PASSWD = "#mailerpass#";
+      };
+
+      oauth2 = {
+        JWT_SECRET = "#oauth2jwtsecret#";
+      };
+    };
+
+    services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) {
+      enable = mkDefault true;
+
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    services.mysql = optionalAttrs (useMysql && cfg.database.createDatabase) {
+      enable = mkDefault true;
+      package = mkDefault pkgs.mariadb;
+
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    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 - -"
+
+      # 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"
+    ];
+
+    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 ];
+
+      # In older versions the secret naming for JWT was kind of confusing.
+      # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET
+      # wasn't persistant at all.
+      # To fix that, there is now the file oauth2_jwt_secret containing the
+      # values for JWT_SECRET and the file jwt_secret gets renamed to
+      # lfs_jwt_secret.
+      # We have to consider this to stay compatible with older installations.
+      preStart = let
+        runConfig = "${cfg.stateDir}/custom/conf/app.ini";
+        secretKey = "${cfg.stateDir}/custom/conf/secret_key";
+        oauth2JwtSecret = "${cfg.stateDir}/custom/conf/oauth2_jwt_secret";
+        oldLfsJwtSecret = "${cfg.stateDir}/custom/conf/jwt_secret"; # old file for LFS_JWT_SECRET
+        lfsJwtSecret = "${cfg.stateDir}/custom/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET
+        internalToken = "${cfg.stateDir}/custom/conf/internal_token";
+      in ''
+        # copy custom configuration and generate a random secret key if needed
+        ${optionalString (cfg.useWizard == false) ''
+          function gitea_setup {
+            cp -f ${configFile} ${runConfig}
+
+            if [ ! -e ${secretKey} ]; then
+                ${gitea}/bin/gitea generate secret SECRET_KEY > ${secretKey}
+            fi
+
+            # Migrate LFS_JWT_SECRET filename
+            if [[ -e ${oldLfsJwtSecret} && ! -e ${lfsJwtSecret} ]]; then
+                mv ${oldLfsJwtSecret} ${lfsJwtSecret}
+            fi
+
+            if [ ! -e ${oauth2JwtSecret} ]; then
+                ${gitea}/bin/gitea generate secret JWT_SECRET > ${oauth2JwtSecret}
+            fi
+
+            if [ ! -e ${lfsJwtSecret} ]; then
+                ${gitea}/bin/gitea generate secret LFS_JWT_SECRET > ${lfsJwtSecret}
+            fi
+
+            if [ ! -e ${internalToken} ]; then
+                ${gitea}/bin/gitea generate secret INTERNAL_TOKEN > ${internalToken}
+            fi
+
+            SECRETKEY="$(head -n1 ${secretKey})"
+            DBPASS="$(head -n1 ${cfg.database.passwordFile})"
+            OAUTH2JWTSECRET="$(head -n1 ${oauth2JwtSecret})"
+            LFSJWTSECRET="$(head -n1 ${lfsJwtSecret})"
+            INTERNALTOKEN="$(head -n1 ${internalToken})"
+            ${if (cfg.mailerPasswordFile == null) then ''
+              MAILERPASSWORD="#mailerpass#"
+            '' else ''
+              MAILERPASSWORD="$(head -n1 ${cfg.mailerPasswordFile} || :)"
+            ''}
+            sed -e "s,#secretkey#,$SECRETKEY,g" \
+                -e "s,#dbpass#,$DBPASS,g" \
+                -e "s,#oauth2jwtsecret#,$OAUTH2JWTSECRET,g" \
+                -e "s,#lfsjwtsecret#,$LFSJWTSECRET,g" \
+                -e "s,#internaltoken#,$INTERNALTOKEN,g" \
+                -e "s,#mailerpass#,$MAILERPASSWORD,g" \
+                -i ${runConfig}
+          }
+          (umask 027; gitea_setup)
+        ''}
+
+        # run migrations/init the database
+        ${gitea}/bin/gitea migrate
+
+        # update all hooks' binary paths
+        ${gitea}/bin/gitea admin regenerate hooks
+
+        # update command option in authorized_keys
+        if [ -r ${cfg.stateDir}/.ssh/authorized_keys ]
+        then
+          ${gitea}/bin/gitea admin regenerate keys
+        fi
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = "gitea";
+        WorkingDirectory = cfg.stateDir;
+        ExecStart = "${gitea}/bin/gitea 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 ];
+        UMask = "0027";
+        # Capabilities
+        CapabilityBoundingSet = "";
+        # Security
+        NoNewPrivileges = true;
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6" ];
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        # System Call Filtering
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @reboot @resources @setuid @swap";
+      };
+
+      environment = {
+        USER = cfg.user;
+        HOME = cfg.stateDir;
+        GITEA_WORK_DIR = cfg.stateDir;
+      };
+    };
+
+    users.users = mkIf (cfg.user == "gitea") {
+      gitea = {
+        description = "Gitea Service";
+        home = cfg.stateDir;
+        useDefaultShell = true;
+        group = "gitea";
+        isSystemUser = true;
+      };
+    };
+
+    users.groups.gitea = {};
+
+    warnings =
+      optional (cfg.database.password != "") "config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead." ++
+      optional (cfg.extraConfig != null) ''
+        services.gitea.`extraConfig` is deprecated, please use services.gitea.`settings`.
+      '';
+
+    # Create database passwordFile default when password is configured.
+    services.gitea.database.passwordFile =
+      (mkDefault (toString (pkgs.writeTextFile {
+        name = "gitea-database-password";
+        text = cfg.database.password;
+      })));
+
+    systemd.services.gitea-dump = mkIf cfg.dump.enable {
+       description = "gitea dump";
+       after = [ "gitea.service" ];
+       wantedBy = [ "default.target" ];
+       path = [ gitea ];
+
+       environment = {
+         USER = cfg.user;
+         HOME = cfg.stateDir;
+         GITEA_WORK_DIR = cfg.stateDir;
+       };
+
+       serviceConfig = {
+         Type = "oneshot";
+         User = cfg.user;
+         ExecStart = "${gitea}/bin/gitea dump --type ${cfg.dump.type}" + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}";
+         WorkingDirectory = cfg.dump.backupDir;
+       };
+    };
+
+    systemd.timers.gitea-dump = mkIf cfg.dump.enable {
+      description = "Update timer for gitea-dump";
+      partOf = [ "gitea-dump.service" ];
+      wantedBy = [ "timers.target" ];
+      timerConfig.OnCalendar = cfg.dump.interval;
+    };
+  };
+  meta.maintainers = with lib.maintainers; [ srhb ma27 ];
+}
diff --git a/nixos/modules/services/misc/gitit.nix b/nixos/modules/services/misc/gitit.nix
new file mode 100644
index 00000000000..ceb186c0f04
--- /dev/null
+++ b/nixos/modules/services/misc/gitit.nix
@@ -0,0 +1,725 @@
+{ 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/" + pkgs.stdenv.hostPlatform.system + "-" + ghc.name + "/" + 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 = "Enable the gitit service.";
+      };
+
+      haskellPackages = mkOption {
+        default = pkgs.haskellPackages;
+        defaultText = literalExpression "pkgs.haskellPackages";
+        example = literalExpression "pkgs.haskell.packages.ghc784";
+        description = "haskellPackages used to build gitit and plugins.";
+      };
+
+      extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
+        default = self: [];
+        example = literalExpression ''
+          haskellPackages: [
+            haskellPackages.wreq
+          ]
+        '';
+        description = ''
+          Extra packages available to ghc when running gitit. The
+          value must be a function which receives the attrset defined
+          in <varname>haskellPackages</varname> as the sole argument.
+        '';
+      };
+
+      address = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "IP address on which the web server will listen.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 5001;
+        description = "Port on which the web server will run.";
+      };
+
+      wikiTitle = mkOption {
+        type = types.str;
+        default = "Gitit!";
+        description = "The wiki title.";
+      };
+
+      repositoryType = mkOption {
+        type = types.enum ["git" "darcs" "mercurial"];
+        default = "git";
+        description = "Specifies the type of repository used for wiki content.";
+      };
+
+      repositoryPath = mkOption {
+        type = types.path;
+        default = homeDir + "/wiki";
+        description = ''
+          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 = ''
+          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 = ''
+          '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 = ''
+          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 = ''
+          Number of minutes of inactivity before a session expires.
+        '';
+      };
+
+      staticDir = mkOption {
+        type = types.path;
+        default = gititShared + "/data/static";
+        description = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = "Path where rendered pages will be cached.";
+      };
+
+      maxUploadSize = mkOption {
+        type = types.str;
+        default = "1000K";
+        description = ''
+          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 = "Specifies an upper limit on the size (in bytes) of pages.";
+      };
+
+      debugMode = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Causes debug information to be logged while gitit is running.";
+      };
+
+      compressResponses = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Specifies whether HTTP responses should be compressed.";
+      };
+
+      mimeTypesFile = mkOption {
+        type = types.path;
+        default = "/etc/mime/types.info";
+        description = ''
+          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:
+<programlisting>
+video/x-ms-wmx  wmx
+</programlisting>
+          If the file is not found, some simple defaults will be used.
+        '';
+      };
+
+      useReCaptcha = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = "RPX account access key.";
+      };
+
+      mailCommand = mkOption {
+        type = types.str;
+        default = "sendmail %s";
+        description = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = ''
+          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 = "Number of days to be included in feeds.";
+      };
+
+      feedRefreshTime = mkOption {
+        type = types.int;
+        default = 60;
+        description = "Number of minutes to cache feeds before refreshing.";
+      };
+
+      pdfExport = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          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 = ''
+          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 = ''
+          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 = "OAuth client ID";
+      };
+
+      oauthClientSecret = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "OAuth client secret";
+      };
+
+      oauthCallback = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "OAuth callback URL";
+      };
+
+      oauthAuthorizeEndpoint = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "OAuth authorize endpoint";
+      };
+
+      oauthAccessTokenEndpoint = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "OAuth access token endpoint";
+      };
+
+      githubOrg = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "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
+            ${pkgs.darcs}/bin/darcs initialize
+            echo "${gm}" > _darcs/prefs/email
+          ''
+          else if repositoryType == "mercurial" then
+          ''
+          if [ ! -d .hg ]
+          then
+            ${pkgs.mercurial}/bin/hg init
+            cat >> .hg/hgrc <<NAMED
+[ui]
+username = gitit ${gm}
+NAMED
+          ''
+          else
+          ''
+          if [ ! -d  .git ]
+          then
+            ${pkgs.git}/bin/git init
+            ${pkgs.git}/bin/git config user.email "${gm}"
+            ${pkgs.git}/bin/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.nix b/nixos/modules/services/misc/gitlab.nix
new file mode 100644
index 00000000000..e48444f7161
--- /dev/null
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -0,0 +1,1458 @@
+{ config, lib, options, pkgs, utils, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gitlab;
+  opt = options.services.gitlab;
+
+  ruby = cfg.packages.gitlab.ruby;
+
+  postgresqlPackage = if config.services.postgresql.enable then
+                        config.services.postgresql.package
+                      else
+                        pkgs.postgresql_12;
+
+  gitlabSocket = "${cfg.statePath}/tmp/sockets/gitlab.socket";
+  gitalySocket = "${cfg.statePath}/tmp/sockets/gitaly.socket";
+  pathUrlQuote = url: replaceStrings ["/"] ["%2F"] url;
+
+  databaseConfig = {
+    production = {
+      adapter = "postgresql";
+      database = cfg.databaseName;
+      host = cfg.databaseHost;
+      username = cfg.databaseUsername;
+      encoding = "utf8";
+      pool = cfg.databasePool;
+    } // cfg.extraDatabaseConfig;
+  };
+
+  # We only want to create a database if we're actually going to connect to it.
+  databaseActuallyCreateLocally = cfg.databaseCreateLocally && cfg.databaseHost == "";
+
+  gitalyToml = pkgs.writeText "gitaly.toml" ''
+    socket_path = "${lib.escape ["\""] gitalySocket}"
+    bin_dir = "${cfg.packages.gitaly}/bin"
+    prometheus_listen_addr = "localhost:9236"
+
+    [git]
+    bin_path = "${pkgs.git}/bin/git"
+
+    [gitaly-ruby]
+    dir = "${cfg.packages.gitaly.ruby}"
+
+    [gitlab-shell]
+    dir = "${cfg.packages.gitlab-shell}"
+
+    [hooks]
+    custom_hooks_dir = "${cfg.statePath}/custom_hooks"
+
+    [gitlab]
+    secret_file = "${cfg.statePath}/gitlab_shell_secret"
+    url = "http+unix://${pathUrlQuote gitlabSocket}"
+
+    [gitlab.http-settings]
+    self_signed_cert = false
+
+    ${concatStringsSep "\n" (attrValues (mapAttrs (k: v: ''
+    [[storage]]
+    name = "${lib.escape ["\""] k}"
+    path = "${lib.escape ["\""] v.path}"
+    '') gitlabConfig.production.repositories.storages))}
+  '';
+
+  gitlabShellConfig = flip recursiveUpdate cfg.extraShellConfig {
+    user = cfg.user;
+    gitlab_url = "http+unix://${pathUrlQuote gitlabSocket}";
+    http_settings.self_signed_cert = false;
+    repos_path = "${cfg.statePath}/repositories";
+    secret_file = "${cfg.statePath}/gitlab_shell_secret";
+    log_file = "${cfg.statePath}/log/gitlab-shell.log";
+    redis = {
+      bin = "${pkgs.redis}/bin/redis-cli";
+      host = "127.0.0.1";
+      port = config.services.redis.servers.gitlab.port;
+      database = 0;
+      namespace = "resque:gitlab";
+    };
+  };
+
+  redisConfig.production.url = cfg.redisUrl;
+
+  pagesArgs = [
+    "-pages-domain" gitlabConfig.production.pages.host
+    "-pages-root" "${gitlabConfig.production.shared.path}/pages"
+  ] ++ cfg.pagesExtraArgs;
+
+  gitlabConfig = {
+    # These are the default settings from config/gitlab.example.yml
+    production = flip recursiveUpdate cfg.extraConfig {
+      gitlab = {
+        host = cfg.host;
+        port = cfg.port;
+        https = cfg.https;
+        user = cfg.user;
+        email_enabled = true;
+        email_display_name = "GitLab";
+        email_reply_to = "noreply@localhost";
+        default_theme = 2;
+        default_projects_features = {
+          issues = true;
+          merge_requests = true;
+          wiki = true;
+          snippets = true;
+          builds = true;
+          container_registry = true;
+        };
+      };
+      repositories.storages.default.path = "${cfg.statePath}/repositories";
+      repositories.storages.default.gitaly_address = "unix:${gitalySocket}";
+      artifacts.enabled = true;
+      lfs.enabled = true;
+      gravatar.enabled = true;
+      cron_jobs = { };
+      gitlab_ci.builds_path = "${cfg.statePath}/builds";
+      ldap.enabled = false;
+      omniauth.enabled = false;
+      shared.path = "${cfg.statePath}/shared";
+      gitaly.client_path = "${cfg.packages.gitaly}/bin";
+      backup = {
+        gitaly_backup_path = "${cfg.packages.gitaly}/bin/gitaly-backup";
+        path = cfg.backup.path;
+        keep_time = cfg.backup.keepTime;
+      } // (optionalAttrs (cfg.backup.uploadOptions != {}) {
+        upload = cfg.backup.uploadOptions;
+      });
+      gitlab_shell = {
+        path = "${cfg.packages.gitlab-shell}";
+        hooks_path = "${cfg.statePath}/shell/hooks";
+        secret_file = "${cfg.statePath}/gitlab_shell_secret";
+        upload_pack = true;
+        receive_pack = true;
+      };
+      workhorse.secret_file = "${cfg.statePath}/.gitlab_workhorse_secret";
+      gitlab_kas.secret_file = "${cfg.statePath}/.gitlab_kas_secret";
+      git.bin_path = "git";
+      monitoring = {
+        ip_whitelist = [ "127.0.0.0/8" "::1/128" ];
+        sidekiq_exporter = {
+          enable = true;
+          address = "localhost";
+          port = 3807;
+        };
+      };
+      registry = lib.optionalAttrs cfg.registry.enable {
+        enabled = true;
+        host = cfg.registry.externalAddress;
+        port = cfg.registry.externalPort;
+        key = cfg.registry.keyFile;
+        api_url = "http://${config.services.dockerRegistry.listenAddress}:${toString config.services.dockerRegistry.port}/";
+        issuer = "gitlab-issuer";
+      };
+      extra = {};
+      uploads.storage_path = cfg.statePath;
+    };
+  };
+
+  gitlabEnv = cfg.packages.gitlab.gitlabEnv // {
+    HOME = "${cfg.statePath}/home";
+    PUMA_PATH = "${cfg.statePath}/";
+    GITLAB_PATH = "${cfg.packages.gitlab}/share/gitlab/";
+    SCHEMA = "${cfg.statePath}/db/structure.sql";
+    GITLAB_UPLOADS_PATH = "${cfg.statePath}/uploads";
+    GITLAB_LOG_PATH = "${cfg.statePath}/log";
+    GITLAB_REDIS_CONFIG_FILE = pkgs.writeText "redis.yml" (builtins.toJSON redisConfig);
+    prometheus_multiproc_dir = "/run/gitlab";
+    RAILS_ENV = "production";
+    MALLOC_ARENA_MAX = "2";
+  } // cfg.extraEnv;
+
+  gitlab-rake = pkgs.stdenv.mkDerivation {
+    name = "gitlab-rake";
+    buildInputs = [ pkgs.makeWrapper ];
+    dontBuild = true;
+    dontUnpack = true;
+    installPhase = ''
+      mkdir -p $out/bin
+      makeWrapper ${cfg.packages.gitlab.rubyEnv}/bin/rake $out/bin/gitlab-rake \
+          ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") gitlabEnv)} \
+          --set PATH '${lib.makeBinPath [ pkgs.nodejs pkgs.gzip pkgs.git pkgs.gnutar postgresqlPackage pkgs.coreutils pkgs.procps ]}:$PATH' \
+          --set RAKEOPT '-f ${cfg.packages.gitlab}/share/gitlab/Rakefile' \
+          --run 'cd ${cfg.packages.gitlab}/share/gitlab'
+     '';
+  };
+
+  gitlab-rails = pkgs.stdenv.mkDerivation {
+    name = "gitlab-rails";
+    buildInputs = [ pkgs.makeWrapper ];
+    dontBuild = true;
+    dontUnpack = true;
+    installPhase = ''
+      mkdir -p $out/bin
+      makeWrapper ${cfg.packages.gitlab.rubyEnv}/bin/rails $out/bin/gitlab-rails \
+          ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") gitlabEnv)} \
+          --set PATH '${lib.makeBinPath [ pkgs.nodejs pkgs.gzip pkgs.git pkgs.gnutar postgresqlPackage pkgs.coreutils pkgs.procps ]}:$PATH' \
+          --run 'cd ${cfg.packages.gitlab}/share/gitlab'
+     '';
+  };
+
+  extraGitlabRb = pkgs.writeText "extra-gitlab.rb" cfg.extraGitlabRb;
+
+  smtpSettings = pkgs.writeText "gitlab-smtp-settings.rb" ''
+    if Rails.env.production?
+      Rails.application.config.action_mailer.delivery_method = :smtp
+
+      ActionMailer::Base.delivery_method = :smtp
+      ActionMailer::Base.smtp_settings = {
+        address: "${cfg.smtp.address}",
+        port: ${toString cfg.smtp.port},
+        ${optionalString (cfg.smtp.username != null) ''user_name: "${cfg.smtp.username}",''}
+        ${optionalString (cfg.smtp.passwordFile != null) ''password: "@smtpPassword@",''}
+        domain: "${cfg.smtp.domain}",
+        ${optionalString (cfg.smtp.authentication != null) "authentication: :${cfg.smtp.authentication},"}
+        enable_starttls_auto: ${boolToString cfg.smtp.enableStartTLSAuto},
+        tls: ${boolToString cfg.smtp.tls},
+        ca_file: "/etc/ssl/certs/ca-certificates.crt",
+        openssl_verify_mode: '${cfg.smtp.opensslVerifyMode}'
+      }
+    end
+  '';
+
+in {
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "gitlab" "stateDir" ] [ "services" "gitlab" "statePath" ])
+    (mkRenamedOptionModule [ "services" "gitlab" "backupPath" ] [ "services" "gitlab" "backup" "path" ])
+    (mkRemovedOptionModule [ "services" "gitlab" "satelliteDir" ] "")
+  ];
+
+  options = {
+    services.gitlab = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the gitlab service.
+        '';
+      };
+
+      packages.gitlab = mkOption {
+        type = types.package;
+        default = pkgs.gitlab;
+        defaultText = literalExpression "pkgs.gitlab";
+        description = "Reference to the gitlab package";
+        example = literalExpression "pkgs.gitlab-ee";
+      };
+
+      packages.gitlab-shell = mkOption {
+        type = types.package;
+        default = pkgs.gitlab-shell;
+        defaultText = literalExpression "pkgs.gitlab-shell";
+        description = "Reference to the gitlab-shell package";
+      };
+
+      packages.gitlab-workhorse = mkOption {
+        type = types.package;
+        default = pkgs.gitlab-workhorse;
+        defaultText = literalExpression "pkgs.gitlab-workhorse";
+        description = "Reference to the gitlab-workhorse package";
+      };
+
+      packages.gitaly = mkOption {
+        type = types.package;
+        default = pkgs.gitaly;
+        defaultText = literalExpression "pkgs.gitaly";
+        description = "Reference to the gitaly package";
+      };
+
+      packages.pages = mkOption {
+        type = types.package;
+        default = pkgs.gitlab-pages;
+        defaultText = literalExpression "pkgs.gitlab-pages";
+        description = "Reference to the gitlab-pages package";
+      };
+
+      statePath = mkOption {
+        type = types.str;
+        default = "/var/gitlab/state";
+        description = ''
+          GitLab state directory. Configuration, repositories and
+          logs, among other things, are stored here.
+
+          The directory will be created automatically if it doesn't
+          exist already. Its parent directories must be owned by
+          either <literal>root</literal> or the user set in
+          <option>services.gitlab.user</option>.
+        '';
+      };
+
+      extraEnv = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        description = ''
+          Additional environment variables for the GitLab environment.
+        '';
+      };
+
+      backup.startAt = mkOption {
+        type = with types; either str (listOf str);
+        default = [];
+        example = "03:00";
+        description = ''
+          The time(s) to run automatic backup of GitLab
+          state. Specified in systemd's time format; see
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+
+      backup.path = mkOption {
+        type = types.str;
+        default = cfg.statePath + "/backup";
+        defaultText = literalExpression ''config.${opt.statePath} + "/backup"'';
+        description = "GitLab path for backups.";
+      };
+
+      backup.keepTime = mkOption {
+        type = types.int;
+        default = 0;
+        example = 48;
+        apply = x: x * 60 * 60;
+        description = ''
+          How long to keep the backups around, in
+          hours. <literal>0</literal> means <quote>keep
+          forever</quote>.
+        '';
+      };
+
+      backup.skip = mkOption {
+        type = with types;
+          let value = enum [
+                "db"
+                "uploads"
+                "builds"
+                "artifacts"
+                "lfs"
+                "registry"
+                "pages"
+                "repositories"
+                "tar"
+              ];
+          in
+            either value (listOf value);
+        default = [];
+        example = [ "artifacts" "lfs" ];
+        apply = x: if isString x then x else concatStringsSep "," x;
+        description = ''
+          Directories to exclude from the backup. The example excludes
+          CI artifacts and LFS objects from the backups. The
+          <literal>tar</literal> option skips the creation of a tar
+          file.
+
+          Refer to <link xlink:href="https://docs.gitlab.com/ee/raketasks/backup_restore.html#excluding-specific-directories-from-the-backup"/>
+          for more information.
+        '';
+      };
+
+      backup.uploadOptions = mkOption {
+        type = types.attrs;
+        default = {};
+        example = literalExpression ''
+          {
+            # Fog storage connection settings, see http://fog.io/storage/
+            connection = {
+              provider = "AWS";
+              region = "eu-north-1";
+              aws_access_key_id = "AKIAXXXXXXXXXXXXXXXX";
+              aws_secret_access_key = { _secret = config.deployment.keys.aws_access_key.path; };
+            };
+
+            # The remote 'directory' to store your backups in.
+            # For S3, this would be the bucket name.
+            remote_directory = "my-gitlab-backups";
+
+            # Use multipart uploads when file size reaches 100MB, see
+            # http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html
+            multipart_chunk_size = 104857600;
+
+            # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
+            encryption = "AES256";
+
+            # Specifies Amazon S3 storage class to use for backups, this is optional
+            storage_class = "STANDARD";
+          };
+        '';
+        description = ''
+          GitLab automatic upload specification. Tells GitLab to
+          upload the backup to a remote location when done.
+
+          Attributes specified here are added under
+          <literal>production -> backup -> upload</literal> in
+          <filename>config/gitlab.yml</filename>.
+        '';
+      };
+
+      databaseHost = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          GitLab database hostname. An empty string means <quote>use
+          local unix socket connection</quote>.
+        '';
+      };
+
+      databasePasswordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = ''
+          File containing the GitLab database user password.
+
+          This should be a string, not a nix path, since nix paths are
+          copied into the world-readable nix store.
+        '';
+      };
+
+      databaseCreateLocally = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether a database should be automatically created on the
+          local host. Set this to <literal>false</literal> if you plan
+          on provisioning a local database yourself. This has no effect
+          if <option>services.gitlab.databaseHost</option> is customized.
+        '';
+      };
+
+      databaseName = mkOption {
+        type = types.str;
+        default = "gitlab";
+        description = "GitLab database name.";
+      };
+
+      databaseUsername = mkOption {
+        type = types.str;
+        default = "gitlab";
+        description = "GitLab database user.";
+      };
+
+      databasePool = mkOption {
+        type = types.int;
+        default = 5;
+        description = "Database connection pool size.";
+      };
+
+      extraDatabaseConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        description = "Extra configuration in config/database.yml.";
+      };
+
+      redisUrl = mkOption {
+        type = types.str;
+        default = "redis://localhost:${toString config.services.redis.servers.gitlab.port}/";
+        defaultText = literalExpression ''redis://localhost:''${toString config.services.redis.servers.gitlab.port}/'';
+        description = "Redis URL for all GitLab services except gitlab-shell";
+      };
+
+      extraGitlabRb = mkOption {
+        type = types.str;
+        default = "";
+        example = ''
+          if Rails.env.production?
+            Rails.application.config.action_mailer.delivery_method = :sendmail
+            ActionMailer::Base.delivery_method = :sendmail
+            ActionMailer::Base.sendmail_settings = {
+              location: "/run/wrappers/bin/sendmail",
+              arguments: "-i -t"
+            }
+          end
+        '';
+        description = ''
+          Extra configuration to be placed in config/extra-gitlab.rb. This can
+          be used to add configuration not otherwise exposed through this module's
+          options.
+        '';
+      };
+
+      host = mkOption {
+        type = types.str;
+        default = config.networking.hostName;
+        defaultText = literalExpression "config.networking.hostName";
+        description = "GitLab host name. Used e.g. for copy-paste URLs.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 8080;
+        description = ''
+          GitLab server port for copy-paste URLs, e.g. 80 or 443 if you're
+          service over https.
+        '';
+      };
+
+      https = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether gitlab prints URLs with https as scheme.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "gitlab";
+        description = "User to run gitlab and all related services.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "gitlab";
+        description = "Group to run gitlab and all related services.";
+      };
+
+      initialRootEmail = mkOption {
+        type = types.str;
+        default = "admin@local.host";
+        description = ''
+          Initial email address of the root account if this is a new install.
+        '';
+      };
+
+      initialRootPasswordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = ''
+          File containing the initial password of the root account if
+          this is a new install.
+
+          This should be a string, not a nix path, since nix paths are
+          copied into the world-readable nix store.
+        '';
+      };
+
+      registry = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Enable GitLab container registry.";
+        };
+        host = mkOption {
+          type = types.str;
+          default = config.services.gitlab.host;
+          defaultText = literalExpression "config.services.gitlab.host";
+          description = "GitLab container registry host name.";
+        };
+        port = mkOption {
+          type = types.int;
+          default = 4567;
+          description = "GitLab container registry port.";
+        };
+        certFile = mkOption {
+          type = types.path;
+          description = "Path to GitLab container registry certificate.";
+        };
+        keyFile = mkOption {
+          type = types.path;
+          description = "Path to GitLab container registry certificate-key.";
+        };
+        defaultForProjects = mkOption {
+          type = types.bool;
+          default = cfg.registry.enable;
+          defaultText = literalExpression "config.${opt.registry.enable}";
+          description = "If GitLab container registry should be enabled by default for projects.";
+        };
+        issuer = mkOption {
+          type = types.str;
+          default = "gitlab-issuer";
+          description = "GitLab container registry issuer.";
+        };
+        serviceName = mkOption {
+          type = types.str;
+          default = "container_registry";
+          description = "GitLab container registry service name.";
+        };
+        externalAddress = mkOption {
+          type = types.str;
+          default = "";
+          description = "External address used to access registry from the internet";
+        };
+        externalPort = mkOption {
+          type = types.int;
+          description = "External port used to access registry from the internet";
+        };
+      };
+
+      smtp = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Enable gitlab mail delivery over SMTP.";
+        };
+
+        address = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = "Address of the SMTP server for GitLab.";
+        };
+
+        port = mkOption {
+          type = types.int;
+          default = 25;
+          description = "Port of the SMTP server for GitLab.";
+        };
+
+        username = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = "Username of the SMTP server for GitLab.";
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          description = ''
+            File containing the password of the SMTP server for GitLab.
+
+            This should be a string, not a nix path, since nix paths
+            are copied into the world-readable nix store.
+          '';
+        };
+
+        domain = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = "HELO domain to use for outgoing mail.";
+        };
+
+        authentication = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = "Authentication type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
+        };
+
+        enableStartTLSAuto = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Whether to try to use StartTLS.";
+        };
+
+        tls = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Whether to use TLS wrapper-mode.";
+        };
+
+        opensslVerifyMode = mkOption {
+          type = types.str;
+          default = "peer";
+          description = "How OpenSSL checks the certificate, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
+        };
+      };
+
+      pagesExtraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ "-listen-proxy" "127.0.0.1:8090" ];
+        description = "Arguments to pass to the gitlab-pages daemon";
+      };
+
+      secrets.secretFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = ''
+          A file containing the secret used to encrypt variables in
+          the DB. If you change or lose this key you will be unable to
+          access variables stored in database.
+
+          Make sure the secret is at least 32 characters and all random,
+          no regular words or you'll be exposed to dictionary attacks.
+
+          This should be a string, not a nix path, since nix paths are
+          copied into the world-readable nix store.
+        '';
+      };
+
+      secrets.dbFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = ''
+          A file containing the secret used to encrypt variables in
+          the DB. If you change or lose this key you will be unable to
+          access variables stored in database.
+
+          Make sure the secret is at least 32 characters and all random,
+          no regular words or you'll be exposed to dictionary attacks.
+
+          This should be a string, not a nix path, since nix paths are
+          copied into the world-readable nix store.
+        '';
+      };
+
+      secrets.otpFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = ''
+          A file containing the secret used to encrypt secrets for OTP
+          tokens. If you change or lose this key, users which have 2FA
+          enabled for login won't be able to login anymore.
+
+          Make sure the secret is at least 32 characters and all random,
+          no regular words or you'll be exposed to dictionary attacks.
+
+          This should be a string, not a nix path, since nix paths are
+          copied into the world-readable nix store.
+        '';
+      };
+
+      secrets.jwsFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = ''
+          A file containing the secret used to encrypt session
+          keys. If you change or lose this key, users will be
+          disconnected.
+
+          Make sure the secret is an RSA private key in PEM format. You can
+          generate one with
+
+          openssl genrsa 2048
+
+          This should be a string, not a nix path, since nix paths are
+          copied into the world-readable nix store.
+        '';
+      };
+
+      extraShellConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        description = "Extra configuration to merge into shell-config.yml";
+      };
+
+      puma.workers = mkOption {
+        type = types.int;
+        default = 2;
+        apply = x: builtins.toString x;
+        description = ''
+          The number of worker processes Puma should spawn. This
+          controls the amount of parallel Ruby code can be
+          executed. GitLab recommends <quote>Number of CPU cores -
+          1</quote>, but at least two.
+
+          <note>
+            <para>
+              Each worker consumes quite a bit of memory, so
+              be careful when increasing this.
+            </para>
+          </note>
+        '';
+      };
+
+      puma.threadsMin = mkOption {
+        type = types.int;
+        default = 0;
+        apply = x: builtins.toString x;
+        description = ''
+          The minimum number of threads Puma should use per
+          worker.
+
+          <note>
+            <para>
+              Each thread consumes memory and contributes to Global VM
+              Lock contention, so be careful when increasing this.
+            </para>
+          </note>
+        '';
+      };
+
+      puma.threadsMax = mkOption {
+        type = types.int;
+        default = 4;
+        apply = x: builtins.toString x;
+        description = ''
+          The maximum number of threads Puma should use per
+          worker. This limits how many threads Puma will automatically
+          spawn in response to requests. In contrast to workers,
+          threads will never be able to run Ruby code in parallel, but
+          give higher IO parallelism.
+
+          <note>
+            <para>
+              Each thread consumes memory and contributes to Global VM
+              Lock contention, so be careful when increasing this.
+            </para>
+          </note>
+        '';
+      };
+
+      sidekiq.memoryKiller.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether the Sidekiq MemoryKiller should be turned
+          on. MemoryKiller kills Sidekiq when its memory consumption
+          exceeds a certain limit.
+
+          See <link xlink:href="https://docs.gitlab.com/ee/administration/operations/sidekiq_memory_killer.html"/>
+          for details.
+        '';
+      };
+
+      sidekiq.memoryKiller.maxMemory = mkOption {
+        type = types.int;
+        default = 2000;
+        apply = x: builtins.toString (x * 1024);
+        description = ''
+          The maximum amount of memory, in MiB, a Sidekiq worker is
+          allowed to consume before being killed.
+        '';
+      };
+
+      sidekiq.memoryKiller.graceTime = mkOption {
+        type = types.int;
+        default = 900;
+        apply = x: builtins.toString x;
+        description = ''
+          The time MemoryKiller waits after noticing excessive memory
+          consumption before killing Sidekiq.
+        '';
+      };
+
+      sidekiq.memoryKiller.shutdownWait = mkOption {
+        type = types.int;
+        default = 30;
+        apply = x: builtins.toString x;
+        description = ''
+          The time allowed for all jobs to finish before Sidekiq is
+          killed forcefully.
+        '';
+      };
+
+      logrotate = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Enable rotation of log files.
+          '';
+        };
+
+        frequency = mkOption {
+          type = types.str;
+          default = "daily";
+          description = "How often to rotate the logs.";
+        };
+
+        keep = mkOption {
+          type = types.int;
+          default = 30;
+          description = "How many rotations to keep.";
+        };
+
+        extraConfig = mkOption {
+          type = types.lines;
+          default = ''
+            copytruncate
+            compress
+          '';
+          description = ''
+            Extra logrotate config options for this path. Refer to
+            <link xlink:href="https://linux.die.net/man/8/logrotate"/> for details.
+          '';
+        };
+      };
+
+      extraConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        example = literalExpression ''
+          {
+            gitlab = {
+              default_projects_features = {
+                builds = false;
+              };
+            };
+            omniauth = {
+              enabled = true;
+              auto_sign_in_with_provider = "openid_connect";
+              allow_single_sign_on = ["openid_connect"];
+              block_auto_created_users = false;
+              providers = [
+                {
+                  name = "openid_connect";
+                  label = "OpenID Connect";
+                  args = {
+                    name = "openid_connect";
+                    scope = ["openid" "profile"];
+                    response_type = "code";
+                    issuer = "https://keycloak.example.com/auth/realms/My%20Realm";
+                    discovery = true;
+                    client_auth_method = "query";
+                    uid_field = "preferred_username";
+                    client_options = {
+                      identifier = "gitlab";
+                      secret = { _secret = "/var/keys/gitlab_oidc_secret"; };
+                      redirect_uri = "https://git.example.com/users/auth/openid_connect/callback";
+                    };
+                  };
+                }
+              ];
+            };
+          };
+        '';
+        description = ''
+          Extra options to be added under
+          <literal>production</literal> in
+          <filename>config/gitlab.yml</filename>, as a nix attribute
+          set.
+
+          Options containing secret data should be set to an attribute
+          set containing the attribute <literal>_secret</literal> - a
+          string pointing to a file containing the value the option
+          should be set to. See the example to get a better picture of
+          this: in the resulting
+          <filename>config/gitlab.yml</filename> file, the
+          <literal>production.omniauth.providers[0].args.client_options.secret</literal>
+          key will be set to the contents of the
+          <filename>/var/keys/gitlab_oidc_secret</filename> file.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.databaseUsername);
+        message = ''For local automatic database provisioning (services.gitlab.databaseCreateLocally == true) with peer authentication (services.gitlab.databaseHost == "") to work services.gitlab.user and services.gitlab.databaseUsername must be identical.'';
+      }
+      {
+        assertion = (cfg.databaseHost != "") -> (cfg.databasePasswordFile != null);
+        message = "When services.gitlab.databaseHost is customized, services.gitlab.databasePasswordFile must be set!";
+      }
+      {
+        assertion = cfg.initialRootPasswordFile != null;
+        message = "services.gitlab.initialRootPasswordFile must be set!";
+      }
+      {
+        assertion = cfg.secrets.secretFile != null;
+        message = "services.gitlab.secrets.secretFile must be set!";
+      }
+      {
+        assertion = cfg.secrets.dbFile != null;
+        message = "services.gitlab.secrets.dbFile must be set!";
+      }
+      {
+        assertion = cfg.secrets.otpFile != null;
+        message = "services.gitlab.secrets.otpFile must be set!";
+      }
+      {
+        assertion = cfg.secrets.jwsFile != null;
+        message = "services.gitlab.secrets.jwsFile must be set!";
+      }
+      {
+        assertion = versionAtLeast postgresqlPackage.version "12.0.0";
+        message = "PostgreSQL >=12 is required to run GitLab 14. Follow the instructions in the manual section for upgrading PostgreSQL here: https://nixos.org/manual/nixos/stable/index.html#module-services-postgres-upgrading";
+      }
+    ];
+
+    environment.systemPackages = [ pkgs.git gitlab-rake gitlab-rails cfg.packages.gitlab-shell ];
+
+    systemd.targets.gitlab = {
+      description = "Common target for all GitLab services.";
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    # Redis is required for the sidekiq queue runner.
+    services.redis.servers.gitlab = {
+      enable = mkDefault true;
+      port = mkDefault 31636;
+      bind = mkDefault "127.0.0.1";
+    };
+
+    # We use postgres as the main data store.
+    services.postgresql = optionalAttrs databaseActuallyCreateLocally {
+      enable = true;
+      ensureUsers = singleton { name = cfg.databaseUsername; };
+    };
+
+    # Enable rotation of log files
+    services.logrotate = {
+      enable = cfg.logrotate.enable;
+      paths = {
+        gitlab = {
+          path = "${cfg.statePath}/log/*.log";
+          user = cfg.user;
+          group = cfg.group;
+          frequency = cfg.logrotate.frequency;
+          keep = cfg.logrotate.keep;
+          extraConfig = cfg.logrotate.extraConfig;
+        };
+      };
+    };
+
+    # The postgresql module doesn't currently support concepts like
+    # objects owners and extensions; for now we tack on what's needed
+    # here.
+    systemd.services.gitlab-postgresql = let pgsql = config.services.postgresql; in mkIf databaseActuallyCreateLocally {
+      after = [ "postgresql.service" ];
+      bindsTo = [ "postgresql.service" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      path = [
+        pgsql.package
+        pkgs.util-linux
+      ];
+      script = ''
+        set -eu
+
+        PSQL() {
+            psql --port=${toString pgsql.port} "$@"
+        }
+
+        PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.databaseName}'" | grep -q 1 || PSQL -tAc 'CREATE DATABASE "${cfg.databaseName}" OWNER "${cfg.databaseUsername}"'
+        current_owner=$(PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.databaseName}'")
+        if [[ "$current_owner" != "${cfg.databaseUsername}" ]]; then
+            PSQL -tAc 'ALTER DATABASE "${cfg.databaseName}" OWNER TO "${cfg.databaseUsername}"'
+            if [[ -e "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" ]]; then
+                echo "Reassigning ownership of database ${cfg.databaseName} to user ${cfg.databaseUsername} failed on last boot. Failing..."
+                exit 1
+            fi
+            touch "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
+            PSQL "${cfg.databaseName}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.databaseUsername}\""
+            rm "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
+        fi
+        PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
+        PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS btree_gist;"
+      '';
+
+      serviceConfig = {
+        User = pgsql.superUser;
+        Type = "oneshot";
+        RemainAfterExit = true;
+      };
+    };
+
+    systemd.services.gitlab-registry-cert = optionalAttrs cfg.registry.enable {
+      path = with pkgs; [ openssl ];
+
+      script = ''
+        mkdir -p $(dirname ${cfg.registry.keyFile})
+        mkdir -p $(dirname ${cfg.registry.certFile})
+        openssl req -nodes -newkey rsa:4096 -keyout ${cfg.registry.keyFile} -out /tmp/registry-auth.csr -subj "/CN=${cfg.registry.issuer}"
+        openssl x509 -in /tmp/registry-auth.csr -out ${cfg.registry.certFile} -req -signkey ${cfg.registry.keyFile} -days 3650
+        chown ${cfg.user}:${cfg.group} $(dirname ${cfg.registry.keyFile})
+        chown ${cfg.user}:${cfg.group} $(dirname ${cfg.registry.certFile})
+        chown ${cfg.user}:${cfg.group} ${cfg.registry.keyFile}
+        chown ${cfg.user}:${cfg.group} ${cfg.registry.certFile}
+      '';
+
+      serviceConfig = {
+        ConditionPathExists = "!${cfg.registry.certFile}";
+      };
+    };
+
+    # Ensure Docker Registry launches after the certificate generation job
+    systemd.services.docker-registry = optionalAttrs cfg.registry.enable {
+      wants = [ "gitlab-registry-cert.service" ];
+    };
+
+    # Enable Docker Registry, if GitLab-Container Registry is enabled
+    services.dockerRegistry = optionalAttrs cfg.registry.enable {
+      enable = true;
+      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";
+          service = cfg.registry.serviceName;
+          issuer = cfg.registry.issuer;
+          rootcertbundle = cfg.registry.certFile;
+        };
+      };
+    };
+
+    # Use postfix to send out mails.
+    services.postfix.enable = mkDefault (cfg.smtp.enable && cfg.smtp.address == "localhost");
+
+    users.users.${cfg.user} =
+      { group = cfg.group;
+        home = "${cfg.statePath}/home";
+        shell = "${pkgs.bash}/bin/bash";
+        uid = config.ids.uids.gitlab;
+      };
+
+    users.groups.${cfg.group}.gid = config.ids.gids.gitlab;
+
+    systemd.tmpfiles.rules = [
+      "d /run/gitlab 0755 ${cfg.user} ${cfg.group} -"
+      "d ${gitlabEnv.HOME} 0750 ${cfg.user} ${cfg.group} -"
+      "z ${gitlabEnv.HOME}/.ssh/authorized_keys 0600 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.backup.path} 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath} 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/builds 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/config 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/db 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/log 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/repositories 2770 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/shell 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/tmp 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/tmp/pids 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/tmp/sockets 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/uploads 0700 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/custom_hooks 0700 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/custom_hooks/pre-receive.d 0700 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/custom_hooks/post-receive.d 0700 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/custom_hooks/update.d 0700 ${cfg.user} ${cfg.group} -"
+      "d ${gitlabConfig.production.shared.path} 0750 ${cfg.user} ${cfg.group} -"
+      "d ${gitlabConfig.production.shared.path}/artifacts 0750 ${cfg.user} ${cfg.group} -"
+      "d ${gitlabConfig.production.shared.path}/lfs-objects 0750 ${cfg.user} ${cfg.group} -"
+      "d ${gitlabConfig.production.shared.path}/packages 0750 ${cfg.user} ${cfg.group} -"
+      "d ${gitlabConfig.production.shared.path}/pages 0750 ${cfg.user} ${cfg.group} -"
+      "d ${gitlabConfig.production.shared.path}/terraform_state 0750 ${cfg.user} ${cfg.group} -"
+      "L+ /run/gitlab/config - - - - ${cfg.statePath}/config"
+      "L+ /run/gitlab/log - - - - ${cfg.statePath}/log"
+      "L+ /run/gitlab/tmp - - - - ${cfg.statePath}/tmp"
+      "L+ /run/gitlab/uploads - - - - ${cfg.statePath}/uploads"
+
+      "L+ /run/gitlab/shell-config.yml - - - - ${pkgs.writeText "config.yml" (builtins.toJSON gitlabShellConfig)}"
+    ];
+
+
+    systemd.services.gitlab-config = {
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      path = with pkgs; [
+        jq
+        openssl
+        replace-secret
+        git
+      ];
+      serviceConfig = {
+        Type = "oneshot";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+        WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
+        RemainAfterExit = true;
+
+        ExecStartPre = let
+          preStartFullPrivileges = ''
+            set -o errexit -o pipefail -o nounset
+            shopt -s dotglob nullglob inherit_errexit
+
+            chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/*
+            if [[ -n "$(ls -A '${cfg.statePath}'/config/)" ]]; then
+              chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/config/*
+            fi
+          '';
+        in "+${pkgs.writeShellScript "gitlab-pre-start-full-privileges" preStartFullPrivileges}";
+
+        ExecStart = pkgs.writeShellScript "gitlab-config" ''
+          set -o errexit -o pipefail -o nounset
+          shopt -s inherit_errexit
+
+          umask u=rwx,g=rx,o=
+
+          cp -f ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION
+          rm -rf ${cfg.statePath}/db/*
+          rm -f ${cfg.statePath}/lib
+          find '${cfg.statePath}/config/' -maxdepth 1 -mindepth 1 -type d -execdir rm -rf {} \;
+          cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config
+          cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/db/* ${cfg.statePath}/db
+          ln -sf ${extraGitlabRb} ${cfg.statePath}/config/initializers/extra-gitlab.rb
+
+          ${cfg.packages.gitlab-shell}/bin/install
+
+          ${optionalString cfg.smtp.enable ''
+              install -m u=rw ${smtpSettings} ${cfg.statePath}/config/initializers/smtp_settings.rb
+              ${optionalString (cfg.smtp.passwordFile != null) ''
+                  replace-secret '@smtpPassword@' '${cfg.smtp.passwordFile}' '${cfg.statePath}/config/initializers/smtp_settings.rb'
+              ''}
+          ''}
+
+          (
+            umask u=rwx,g=,o=
+
+            openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret
+
+            rm -f '${cfg.statePath}/config/database.yml'
+
+            ${if cfg.databasePasswordFile != null then ''
+                db_password="$(<'${cfg.databasePasswordFile}')"
+                export db_password
+
+                if [[ -z "$db_password" ]]; then
+                  >&2 echo "Database password was an empty string!"
+                  exit 1
+                fi
+
+                jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
+                   '.production.password = $ENV.db_password' \
+                   >'${cfg.statePath}/config/database.yml'
+              ''
+              else ''
+                jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
+                   >'${cfg.statePath}/config/database.yml'
+              ''
+            }
+
+            ${utils.genJqSecretsReplacementSnippet
+                gitlabConfig
+                "${cfg.statePath}/config/gitlab.yml"
+            }
+
+            rm -f '${cfg.statePath}/config/secrets.yml'
+
+            secret="$(<'${cfg.secrets.secretFile}')"
+            db="$(<'${cfg.secrets.dbFile}')"
+            otp="$(<'${cfg.secrets.otpFile}')"
+            jws="$(<'${cfg.secrets.jwsFile}')"
+            export secret db otp jws
+            jq -n '{production: {secret_key_base: $ENV.secret,
+                    otp_key_base: $ENV.otp,
+                    db_key_base: $ENV.db,
+                    openid_connect_signing_key: $ENV.jws}}' \
+               > '${cfg.statePath}/config/secrets.yml'
+          )
+
+          # We remove potentially broken links to old gitlab-shell versions
+          rm -Rf ${cfg.statePath}/repositories/**/*.git/hooks
+
+          git config --global core.autocrlf "input"
+        '';
+      };
+    };
+
+    systemd.services.gitlab-db-config = {
+      after = [ "gitlab-config.service" "gitlab-postgresql.service" "postgresql.service" ];
+      bindsTo = [
+        "gitlab-config.service"
+      ] ++ optional (cfg.databaseHost == "") "postgresql.service"
+        ++ optional databaseActuallyCreateLocally "gitlab-postgresql.service";
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+        WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
+        RemainAfterExit = true;
+
+        ExecStart = pkgs.writeShellScript "gitlab-db-config" ''
+          set -o errexit -o pipefail -o nounset
+          shopt -s inherit_errexit
+          umask u=rwx,g=rx,o=
+
+          initial_root_password="$(<'${cfg.initialRootPasswordFile}')"
+          ${gitlab-rake}/bin/gitlab-rake gitlab:db:configure GITLAB_ROOT_PASSWORD="$initial_root_password" \
+                                                             GITLAB_ROOT_EMAIL='${cfg.initialRootEmail}' > /dev/null
+        '';
+      };
+    };
+
+    systemd.services.gitlab-sidekiq = {
+      after = [
+        "network.target"
+        "redis-gitlab.service"
+        "postgresql.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ];
+      bindsTo = [
+        "redis-gitlab.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ] ++ optional (cfg.databaseHost == "") "postgresql.service";
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      environment = gitlabEnv // (optionalAttrs cfg.sidekiq.memoryKiller.enable {
+        SIDEKIQ_MEMORY_KILLER_MAX_RSS = cfg.sidekiq.memoryKiller.maxMemory;
+        SIDEKIQ_MEMORY_KILLER_GRACE_TIME = cfg.sidekiq.memoryKiller.graceTime;
+        SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT = cfg.sidekiq.memoryKiller.shutdownWait;
+      });
+      path = with pkgs; [
+        postgresqlPackage
+        git
+        ruby
+        openssh
+        nodejs
+        gnupg
+
+        # Needed for GitLab project imports
+        gnutar
+        gzip
+
+        procps # Sidekiq MemoryKiller
+      ];
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "infinity";
+        Restart = "always";
+        WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
+        ExecStart="${cfg.packages.gitlab.rubyEnv}/bin/sidekiq -C \"${cfg.packages.gitlab}/share/gitlab/config/sidekiq_queues.yml\" -e production";
+      };
+    };
+
+    systemd.services.gitaly = {
+      after = [ "network.target" "gitlab-config.service" ];
+      bindsTo = [ "gitlab-config.service" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      path = with pkgs; [
+        openssh
+        procps  # See https://gitlab.com/gitlab-org/gitaly/issues/1562
+        git
+        cfg.packages.gitaly.rubyEnv
+        cfg.packages.gitaly.rubyEnv.wrappedRuby
+        gzip
+        bzip2
+      ];
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+        WorkingDirectory = gitlabEnv.HOME;
+        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;
+      };
+    };
+
+    systemd.services.gitlab-workhorse = {
+      after = [ "network.target" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      path = with pkgs; [
+        exiftool
+        git
+        gnutar
+        gzip
+        openssh
+        gitlab-workhorse
+      ];
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+        WorkingDirectory = gitlabEnv.HOME;
+        ExecStart =
+          "${cfg.packages.gitlab-workhorse}/bin/workhorse "
+          + "-listenUmask 0 "
+          + "-listenNetwork unix "
+          + "-listenAddr /run/gitlab/gitlab-workhorse.socket "
+          + "-authSocket ${gitlabSocket} "
+          + "-documentRoot ${cfg.packages.gitlab}/share/gitlab/public "
+          + "-secretPath ${cfg.statePath}/.gitlab_workhorse_secret";
+      };
+    };
+
+    systemd.services.gitlab-mailroom = mkIf (gitlabConfig.production.incoming_email.enabled or false) {
+      description = "GitLab incoming mail daemon";
+      after = [ "network.target" "redis-gitlab.service" "gitlab-config.service" ];
+      bindsTo = [ "gitlab-config.service" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      environment = gitlabEnv;
+      serviceConfig = {
+        Type = "simple";
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.packages.gitlab.rubyEnv}/bin/bundle exec mail_room -c ${cfg.statePath}/config/mail_room.yml";
+        WorkingDirectory = gitlabEnv.HOME;
+      };
+    };
+
+    systemd.services.gitlab = {
+      after = [
+        "gitlab-workhorse.service"
+        "network.target"
+        "redis-gitlab.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ];
+      bindsTo = [
+        "redis-gitlab.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ] ++ optional (cfg.databaseHost == "") "postgresql.service";
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      environment = gitlabEnv;
+      path = with pkgs; [
+        postgresqlPackage
+        git
+        openssh
+        nodejs
+        procps
+        gnupg
+      ];
+      serviceConfig = {
+        Type = "notify";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+        WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
+        ExecStart = concatStringsSep " " [
+          "${cfg.packages.gitlab.rubyEnv}/bin/puma"
+          "-e production"
+          "-C ${cfg.statePath}/config/puma.rb"
+          "-w ${cfg.puma.workers}"
+          "-t ${cfg.puma.threadsMin}:${cfg.puma.threadsMax}"
+        ];
+      };
+
+    };
+
+    systemd.services.gitlab-backup = {
+      after = [ "gitlab.service" ];
+      bindsTo = [ "gitlab.service" ];
+      startAt = cfg.backup.startAt;
+      environment = {
+        RAILS_ENV = "production";
+        CRON = "1";
+      } // optionalAttrs (stringLength cfg.backup.skip > 0) {
+        SKIP = cfg.backup.skip;
+      };
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${gitlab-rake}/bin/gitlab-rake gitlab:backup:create";
+      };
+    };
+
+  };
+
+  meta.doc = ./gitlab.xml;
+
+}
diff --git a/nixos/modules/services/misc/gitlab.xml b/nixos/modules/services/misc/gitlab.xml
new file mode 100644
index 00000000000..40424c5039a
--- /dev/null
+++ b/nixos/modules/services/misc/gitlab.xml
@@ -0,0 +1,151 @@
+<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 availabe 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/gitolite.nix b/nixos/modules/services/misc/gitolite.nix
new file mode 100644
index 00000000000..810ef1f21b9
--- /dev/null
+++ b/nixos/modules/services/misc/gitolite.nix
@@ -0,0 +1,234 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gitolite;
+  # Use writeTextDir to not leak Nix store hash into file name
+  pubkeyFile = (pkgs.writeTextDir "gitolite-admin.pub" cfg.adminPubkey) + "/gitolite-admin.pub";
+  hooks = lib.concatMapStrings (hook: "${hook} ") cfg.commonHooks;
+in
+{
+  options = {
+    services.gitolite = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable gitolite management under the
+          <literal>gitolite</literal> user. After
+          switching to a configuration with Gitolite enabled, you can
+          then run <literal>git clone
+          gitolite@host:gitolite-admin.git</literal> to manage it further.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/gitolite";
+        description = ''
+          The gitolite home directory used to store all repositories. If left as the default value
+          this directory will automatically be created before the gitolite server starts, otherwise
+          the sysadmin is responsible for ensuring the directory exists with appropriate ownership
+          and permissions.
+        '';
+      };
+
+      adminPubkey = mkOption {
+        type = types.str;
+        description = ''
+          Initial administrative public key for Gitolite. This should
+          be an SSH Public Key. Note that this key will only be used
+          once, upon the first initialization of the Gitolite user.
+          The key string cannot have any line breaks in it.
+        '';
+      };
+
+      enableGitAnnex = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable git-annex support. Uses the <literal>extraGitoliteRc</literal> option
+          to apply the necessary configuration.
+        '';
+      };
+
+      commonHooks = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          A list of custom git hooks that get copied to <literal>~/.gitolite/hooks/common</literal>.
+        '';
+      };
+
+      extraGitoliteRc = mkOption {
+        type = types.lines;
+        default = "";
+        example = literalExpression ''
+          '''
+            $RC{UMASK} = 0027;
+            $RC{SITE_INFO} = 'This is our private repository host';
+            push( @{$RC{ENABLE}}, 'Kindergarten' ); # enable the command/feature
+            @{$RC{ENABLE}} = grep { $_ ne 'desc' } @{$RC{ENABLE}}; # disable the command/feature
+          '''
+        '';
+        description = ''
+          Extra configuration to append to the default <literal>~/.gitolite.rc</literal>.
+
+          This should be Perl code that modifies the <literal>%RC</literal>
+          configuration variable. The default <literal>~/.gitolite.rc</literal>
+          content is generated by invoking <literal>gitolite print-default-rc</literal>,
+          and extra configuration from this option is appended to it. The result
+          is placed to Nix store, and the <literal>~/.gitolite.rc</literal> file
+          becomes a symlink to it.
+
+          If you already have a customized (or otherwise changed)
+          <literal>~/.gitolite.rc</literal> file, NixOS will refuse to replace
+          it with a symlink, and the `gitolite-init` initialization service
+          will fail. In this situation, in order to use this option, you
+          will need to take any customizations you may have in
+          <literal>~/.gitolite.rc</literal>, convert them to appropriate Perl
+          statements, add them to this option, and remove the file.
+
+          See also the <literal>enableGitAnnex</literal> option.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "gitolite";
+        description = ''
+          Gitolite user account. This is the username of the gitolite endpoint.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "gitolite";
+        description = ''
+          Primary group of the Gitolite user account.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable (
+  let
+    manageGitoliteRc = cfg.extraGitoliteRc != "";
+    rcDir = pkgs.runCommand "gitolite-rc" { preferLocalBuild = true; } rcDirScript;
+    rcDirScript =
+      ''
+        mkdir "$out"
+        export HOME=temp-home
+        mkdir -p "$HOME/.gitolite/logs" # gitolite can't run without it
+        '${pkgs.gitolite}'/bin/gitolite print-default-rc >>"$out/gitolite.rc.default"
+        cat <<END >>"$out/gitolite.rc"
+        # This file is managed by NixOS.
+        # Use services.gitolite options to control it.
+
+        END
+        cat "$out/gitolite.rc.default" >>"$out/gitolite.rc"
+      '' +
+      optionalString (cfg.extraGitoliteRc != "") ''
+        echo -n ${escapeShellArg ''
+
+          # Added by NixOS:
+          ${removeSuffix "\n" cfg.extraGitoliteRc}
+
+          # per perl rules, this should be the last line in such a file:
+          1;
+        ''} >>"$out/gitolite.rc"
+      '';
+  in {
+    services.gitolite.extraGitoliteRc = optionalString cfg.enableGitAnnex ''
+      # Enable git-annex support:
+      push( @{$RC{ENABLE}}, 'git-annex-shell ua');
+    '';
+
+    users.users.${cfg.user} = {
+      description     = "Gitolite user";
+      home            = cfg.dataDir;
+      uid             = config.ids.uids.gitolite;
+      group           = cfg.group;
+      useDefaultShell = true;
+    };
+    users.groups.${cfg.group}.gid = config.ids.gids.gitolite;
+
+    systemd.services.gitolite-init = {
+      description = "Gitolite initialization";
+      wantedBy    = [ "multi-user.target" ];
+      unitConfig.RequiresMountsFor = cfg.dataDir;
+
+      environment = {
+        GITOLITE_RC = ".gitolite.rc";
+        GITOLITE_RC_DEFAULT = "${rcDir}/gitolite.rc.default";
+      };
+
+      serviceConfig = mkMerge [
+        (mkIf (cfg.dataDir == "/var/lib/gitolite") {
+          StateDirectory = "gitolite gitolite/.gitolite gitolite/.gitolite/logs";
+          StateDirectoryMode = "0750";
+        })
+        {
+          Type = "oneshot";
+          User = cfg.user;
+          Group = cfg.group;
+          WorkingDirectory = "~";
+          RemainAfterExit = true;
+        }
+      ];
+
+      path = [ pkgs.gitolite pkgs.git pkgs.perl pkgs.bash pkgs.diffutils config.programs.ssh.package ];
+      script =
+      let
+        rcSetupScriptIfCustomFile =
+          if manageGitoliteRc then ''
+            cat <<END
+            <3>ERROR: NixOS can't apply declarative configuration
+            <3>to your .gitolite.rc file, because it seems to be
+            <3>already customized manually.
+            <3>See the services.gitolite.extraGitoliteRc option
+            <3>in "man configuration.nix" for more information.
+            END
+            # Not sure if the line below addresses the issue directly or just
+            # adds a delay, but without it our error message often doesn't
+            # show up in `systemctl status gitolite-init`.
+            journalctl --flush
+            exit 1
+          '' else ''
+            :
+          '';
+        rcSetupScriptIfDefaultFileOrStoreSymlink =
+          if manageGitoliteRc then ''
+            ln -sf "${rcDir}/gitolite.rc" "$GITOLITE_RC"
+          '' else ''
+            [[ -L "$GITOLITE_RC" ]] && rm -f "$GITOLITE_RC"
+          '';
+      in
+        ''
+          if ( [[ ! -e "$GITOLITE_RC" ]] && [[ ! -L "$GITOLITE_RC" ]] ) ||
+             ( [[ -f "$GITOLITE_RC" ]] && diff -q "$GITOLITE_RC" "$GITOLITE_RC_DEFAULT" >/dev/null ) ||
+             ( [[ -L "$GITOLITE_RC" ]] && [[ "$(readlink "$GITOLITE_RC")" =~ ^/nix/store/ ]] )
+          then
+        '' + rcSetupScriptIfDefaultFileOrStoreSymlink +
+        ''
+          else
+        '' + rcSetupScriptIfCustomFile +
+        ''
+          fi
+
+          if [ ! -d repositories ]; then
+            gitolite setup -pk ${pubkeyFile}
+          fi
+          if [ -n "${hooks}" ]; then
+            cp -f ${hooks} .gitolite/hooks/common/
+            chmod +x .gitolite/hooks/common/*
+          fi
+          gitolite setup # Upgrade if needed
+        '';
+    };
+
+    environment.systemPackages = [ pkgs.gitolite pkgs.git ]
+        ++ optional cfg.enableGitAnnex pkgs.git-annex;
+  });
+}
diff --git a/nixos/modules/services/misc/gitweb.nix b/nixos/modules/services/misc/gitweb.nix
new file mode 100644
index 00000000000..a1180716e36
--- /dev/null
+++ b/nixos/modules/services/misc/gitweb.nix
@@ -0,0 +1,60 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gitweb;
+
+in
+{
+
+  options.services.gitweb = {
+
+    projectroot = mkOption {
+      default = "/srv/git";
+      type = types.path;
+      description = ''
+        Path to git projects (bare repositories) that should be served by
+        gitweb. Must not end with a slash.
+      '';
+    };
+
+    extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      description = ''
+        Verbatim configuration text appended to the generated gitweb.conf file.
+      '';
+      example = ''
+        $feature{'highlight'}{'default'} = [1];
+        $feature{'ctags'}{'default'} = [1];
+        $feature{'avatar'}{'default'} = ['gravatar'];
+      '';
+    };
+
+    gitwebTheme = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Use an alternative theme for gitweb, strongly inspired by GitHub.
+      '';
+    };
+
+    gitwebConfigFile = mkOption {
+      default = pkgs.writeText "gitweb.conf" ''
+        # path to git projects (<project>.git)
+        $projectroot = "${cfg.projectroot}";
+        $highlight_bin = "${pkgs.highlight}/bin/highlight";
+        ${cfg.extraConfig}
+      '';
+      defaultText = literalDocBook "generated config file";
+      type = types.path;
+      readOnly = true;
+      internal = true;
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ ];
+
+}
diff --git a/nixos/modules/services/misc/gogs.nix b/nixos/modules/services/misc/gogs.nix
new file mode 100644
index 00000000000..c7ae4f49407
--- /dev/null
+++ b/nixos/modules/services/misc/gogs.nix
@@ -0,0 +1,274 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gogs;
+  opt = options.services.gogs;
+  configFile = pkgs.writeText "app.ini" ''
+    APP_NAME = ${cfg.appName}
+    RUN_USER = ${cfg.user}
+    RUN_MODE = prod
+
+    [database]
+    DB_TYPE = ${cfg.database.type}
+    HOST = ${cfg.database.host}:${toString cfg.database.port}
+    NAME = ${cfg.database.name}
+    USER = ${cfg.database.user}
+    PASSWD = #dbpass#
+    PATH = ${cfg.database.path}
+
+    [repository]
+    ROOT = ${cfg.repositoryRoot}
+
+    [server]
+    DOMAIN = ${cfg.domain}
+    HTTP_ADDR = ${cfg.httpAddress}
+    HTTP_PORT = ${toString cfg.httpPort}
+    ROOT_URL = ${cfg.rootUrl}
+
+    [session]
+    COOKIE_NAME = session
+    COOKIE_SECURE = ${boolToString cfg.cookieSecure}
+
+    [security]
+    SECRET_KEY = #secretkey#
+    INSTALL_LOCK = true
+
+    [log]
+    ROOT_PATH = ${cfg.stateDir}/log
+
+    ${cfg.extraConfig}
+  '';
+in
+
+{
+  options = {
+    services.gogs = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Enable Go Git Service.";
+      };
+
+      useWizard = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Do not generate a configuration and use Gogs' installation wizard instead. The first registered user will be administrator.";
+      };
+
+      stateDir = mkOption {
+        default = "/var/lib/gogs";
+        type = types.str;
+        description = "Gogs data directory.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "gogs";
+        description = "User account under which Gogs runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "gogs";
+        description = "Group account under which Gogs runs.";
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum [ "sqlite3" "mysql" "postgres" ];
+          example = "mysql";
+          default = "sqlite3";
+          description = "Database engine to use.";
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "Database host address.";
+        };
+
+        port = mkOption {
+          type = types.int;
+          default = 3306;
+          description = "Database host port.";
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "gogs";
+          description = "Database name.";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "gogs";
+          description = "Database user.";
+        };
+
+        password = mkOption {
+          type = types.str;
+          default = "";
+          description = ''
+            The password corresponding to <option>database.user</option>.
+            Warning: this is stored in cleartext in the Nix store!
+            Use <option>database.passwordFile</option> instead.
+          '';
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          example = "/run/keys/gogs-dbpassword";
+          description = ''
+            A file containing the password corresponding to
+            <option>database.user</option>.
+          '';
+        };
+
+        path = mkOption {
+          type = types.str;
+          default = "${cfg.stateDir}/data/gogs.db";
+          defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/gogs.db"'';
+          description = "Path to the sqlite3 database file.";
+        };
+      };
+
+      appName = mkOption {
+        type = types.str;
+        default = "Gogs: Go Git Service";
+        description = "Application name.";
+      };
+
+      repositoryRoot = mkOption {
+        type = types.str;
+        default = "${cfg.stateDir}/repositories";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"'';
+        description = "Path to the git repositories.";
+      };
+
+      domain = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Domain name of your server.";
+      };
+
+      rootUrl = mkOption {
+        type = types.str;
+        default = "http://localhost:3000/";
+        description = "Full public URL of Gogs server.";
+      };
+
+      httpAddress = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "HTTP listen address.";
+      };
+
+      httpPort = mkOption {
+        type = types.int;
+        default = 3000;
+        description = "HTTP listen port.";
+      };
+
+      cookieSecure = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Marks session cookies as "secure" as a hint for browsers to only send
+          them via HTTPS. This option is recommend, if Gogs is being served over HTTPS.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.str;
+        default = "";
+        description = "Configuration lines appended to the generated Gogs configuration file.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.gogs = {
+      description = "Gogs (Go Git Service)";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.gogs ];
+
+      preStart = let
+        runConfig = "${cfg.stateDir}/custom/conf/app.ini";
+        secretKey = "${cfg.stateDir}/custom/conf/secret_key";
+      in ''
+        mkdir -p ${cfg.stateDir}
+
+        # copy custom configuration and generate a random secret key if needed
+        ${optionalString (cfg.useWizard == false) ''
+          mkdir -p ${cfg.stateDir}/custom/conf
+          cp -f ${configFile} ${runConfig}
+
+          if [ ! -e ${secretKey} ]; then
+              head -c 16 /dev/urandom | base64 > ${secretKey}
+          fi
+
+          KEY=$(head -n1 ${secretKey})
+          DBPASS=$(head -n1 ${cfg.database.passwordFile})
+          sed -e "s,#secretkey#,$KEY,g" \
+              -e "s,#dbpass#,$DBPASS,g" \
+              -i ${runConfig}
+          chmod 440 ${runConfig} ${secretKey}
+        ''}
+
+        mkdir -p ${cfg.repositoryRoot}
+        # update all hooks' binary paths
+        HOOKS=$(find ${cfg.repositoryRoot} -mindepth 4 -maxdepth 4 -type f -wholename "*git/hooks/*")
+        if [ "$HOOKS" ]
+        then
+          sed -ri 's,/nix/store/[a-z0-9.-]+/bin/gogs,${pkgs.gogs}/bin/gogs,g' $HOOKS
+          sed -ri 's,/nix/store/[a-z0-9.-]+/bin/env,${pkgs.coreutils}/bin/env,g' $HOOKS
+          sed -ri 's,/nix/store/[a-z0-9.-]+/bin/bash,${pkgs.bash}/bin/bash,g' $HOOKS
+          sed -ri 's,/nix/store/[a-z0-9.-]+/bin/perl,${pkgs.perl}/bin/perl,g' $HOOKS
+        fi
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        WorkingDirectory = cfg.stateDir;
+        ExecStart = "${pkgs.gogs}/bin/gogs web";
+        Restart = "always";
+      };
+
+      environment = {
+        USER = cfg.user;
+        HOME = cfg.stateDir;
+        GOGS_WORK_DIR = cfg.stateDir;
+      };
+    };
+
+    users = mkIf (cfg.user == "gogs") {
+      users.gogs = {
+        description = "Go Git Service";
+        uid = config.ids.uids.gogs;
+        group = "gogs";
+        home = cfg.stateDir;
+        createHome = true;
+        shell = pkgs.bash;
+      };
+      groups.gogs.gid = config.ids.gids.gogs;
+    };
+
+    warnings = optional (cfg.database.password != "")
+      ''config.services.gogs.database.password will be stored as plaintext
+        in the Nix store. Use database.passwordFile instead.'';
+
+    # Create database passwordFile default when password is configured.
+    services.gogs.database.passwordFile =
+      (mkDefault (toString (pkgs.writeTextFile {
+        name = "gogs-database-password";
+        text = cfg.database.password;
+      })));
+  };
+}
diff --git a/nixos/modules/services/misc/gollum.nix b/nixos/modules/services/misc/gollum.nix
new file mode 100644
index 00000000000..cad73a871ba
--- /dev/null
+++ b/nixos/modules/services/misc/gollum.nix
@@ -0,0 +1,121 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gollum;
+in
+
+{
+  options.services.gollum = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable the Gollum service.";
+    };
+
+    address = mkOption {
+      type = types.str;
+      default = "0.0.0.0";
+      description = "IP address on which the web server will listen.";
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = 4567;
+      description = "Port on which the web server will run.";
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = "Content of the configuration file";
+    };
+
+    mathjax = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable support for math rendering using MathJax";
+    };
+
+    allowUploads = mkOption {
+      type = types.nullOr (types.enum [ "dir" "page" ]);
+      default = null;
+      description = "Enable uploads of external files";
+    };
+
+    emoji = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Parse and interpret emoji tags";
+    };
+
+    h1-title = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Use the first h1 as page title";
+    };
+
+    branch = mkOption {
+      type = types.str;
+      default = "master";
+      example = "develop";
+      description = "Git branch to serve";
+    };
+
+    stateDir = mkOption {
+      type = types.path;
+      default = "/var/lib/gollum";
+      description = "Specifies the path of the repository directory. If it does not exist, Gollum will create it on startup.";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users.gollum = {
+      group = config.users.users.gollum.name;
+      description = "Gollum user";
+      createHome = false;
+      isSystemUser = true;
+    };
+
+    users.groups.gollum = { };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}' - ${config.users.users.gollum.name} ${config.users.groups.gollum.name} - -"
+    ];
+
+    systemd.services.gollum = {
+      description = "Gollum wiki";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.git ];
+
+      preStart = ''
+        # This is safe to be run on an existing repo
+        git init ${cfg.stateDir}
+      '';
+
+      serviceConfig = {
+        User = config.users.users.gollum.name;
+        Group = config.users.groups.gollum.name;
+        WorkingDirectory = cfg.stateDir;
+        ExecStart = ''
+          ${pkgs.gollum}/bin/gollum \
+            --port ${toString cfg.port} \
+            --host ${cfg.address} \
+            --config ${pkgs.writeText "gollum-config.rb" cfg.extraConfig} \
+            --ref ${cfg.branch} \
+            ${optionalString cfg.mathjax "--mathjax"} \
+            ${optionalString cfg.emoji "--emoji"} \
+            ${optionalString cfg.h1-title "--h1-title"} \
+            ${optionalString (cfg.allowUploads != null) "--allow-uploads ${cfg.allowUploads}"} \
+            ${cfg.stateDir}
+        '';
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ erictapen ];
+}
diff --git a/nixos/modules/services/misc/gpsd.nix b/nixos/modules/services/misc/gpsd.nix
new file mode 100644
index 00000000000..6494578f764
--- /dev/null
+++ b/nixos/modules/services/misc/gpsd.nix
@@ -0,0 +1,116 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  uid = config.ids.uids.gpsd;
+  gid = config.ids.gids.gpsd;
+  cfg = config.services.gpsd;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.gpsd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable `gpsd', a GPS service daemon.
+        '';
+      };
+
+      device = mkOption {
+        type = types.str;
+        default = "/dev/ttyUSB0";
+        description = ''
+          A device may be a local serial device for GPS input, or a URL of the form:
+               <literal>[{dgpsip|ntrip}://][user:passwd@]host[:port][/stream]</literal>
+          in which case it specifies an input source for DGPS or ntrip data.
+        '';
+      };
+
+      readonly = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable the broken-device-safety, otherwise
+          known as read-only mode.  Some popular bluetooth and USB
+          receivers lock up or become totally inaccessible when
+          probed or reconfigured.  This switch prevents gpsd from
+          writing to a receiver.  This means that gpsd cannot
+          configure the receiver for optimal performance, but it
+          also means that gpsd cannot break the receiver.  A better
+          solution would be for Bluetooth to not be so fragile.  A
+          platform independent method to identify
+          serial-over-Bluetooth devices would also be nice.
+        '';
+      };
+
+      nowait = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          don't wait for client connects to poll GPS
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 2947;
+        description = ''
+          The port where to listen for TCP connections.
+        '';
+      };
+
+      debugLevel = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          The debugging level.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.gpsd =
+      { inherit uid;
+        group = "gpsd";
+        description = "gpsd daemon user";
+        home = "/var/empty";
+      };
+
+    users.groups.gpsd = { inherit gid; };
+
+    systemd.services.gpsd = {
+      description = "GPSD daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = ''
+          ${pkgs.gpsd}/sbin/gpsd -D "${toString cfg.debugLevel}"  \
+            -S "${toString cfg.port}"                             \
+            ${optionalString cfg.readonly "-b"}                   \
+            ${optionalString cfg.nowait "-n"}                     \
+            "${cfg.device}"
+        '';
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/greenclip.nix b/nixos/modules/services/misc/greenclip.nix
new file mode 100644
index 00000000000..32e8d746cb5
--- /dev/null
+++ b/nixos/modules/services/misc/greenclip.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.greenclip;
+in {
+
+  options.services.greenclip = {
+    enable = mkEnableOption "Greenclip daemon";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.haskellPackages.greenclip;
+      defaultText = literalExpression "pkgs.haskellPackages.greenclip";
+      description = "greenclip derivation to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.greenclip = {
+      enable      = true;
+      description = "greenclip daemon";
+      wantedBy = [ "graphical-session.target" ];
+      after    = [ "graphical-session.target" ];
+      serviceConfig.ExecStart = "${cfg.package}/bin/greenclip daemon";
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/misc/headphones.nix b/nixos/modules/services/misc/headphones.nix
new file mode 100644
index 00000000000..31bd61cb4c2
--- /dev/null
+++ b/nixos/modules/services/misc/headphones.nix
@@ -0,0 +1,89 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  name = "headphones";
+
+  cfg = config.services.headphones;
+  opt = options.services.headphones;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+    services.headphones = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the headphones server.";
+      };
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/${name}";
+        description = "Path where to store data files.";
+      };
+      configFile = mkOption {
+        type = types.path;
+        default = "${cfg.dataDir}/config.ini";
+        defaultText = literalExpression ''"''${config.${opt.dataDir}}/config.ini"'';
+        description = "Path to config file.";
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Host to listen on.";
+      };
+      port = mkOption {
+        type = types.ints.u16;
+        default = 8181;
+        description = "Port to bind to.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = name;
+        description = "User to run the service as";
+      };
+      group = mkOption {
+        type = types.str;
+        default = name;
+        description = "Group to run the service as";
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users = optionalAttrs (cfg.user == name) {
+      ${name} = {
+        uid = config.ids.uids.headphones;
+        group = cfg.group;
+        description = "headphones user";
+        home = cfg.dataDir;
+        createHome = true;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == name) {
+      ${name}.gid = config.ids.gids.headphones;
+    };
+
+    systemd.services.headphones = {
+        description = "Headphones Server";
+        wantedBy    = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig = {
+          User = cfg.user;
+          Group = cfg.group;
+          ExecStart = "${pkgs.headphones}/bin/headphones --datadir ${cfg.dataDir} --config ${cfg.configFile} --host ${cfg.host} --port ${toString cfg.port}";
+        };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/heisenbridge.nix b/nixos/modules/services/misc/heisenbridge.nix
new file mode 100644
index 00000000000..7ce8a23d9af
--- /dev/null
+++ b/nixos/modules/services/misc/heisenbridge.nix
@@ -0,0 +1,222 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.heisenbridge;
+
+  pkg = config.services.heisenbridge.package;
+  bin = "${pkg}/bin/heisenbridge";
+
+  jsonType = (pkgs.formats.json { }).type;
+
+  registrationFile = "/var/lib/heisenbridge/registration.yml";
+  # JSON is a proper subset of YAML
+  bridgeConfig = builtins.toFile "heisenbridge-registration.yml" (builtins.toJSON {
+    id = "heisenbridge";
+    url = cfg.registrationUrl;
+    # Don't specify as_token and hs_token
+    rate_limited = false;
+    sender_localpart = "heisenbridge";
+    namespaces = cfg.namespaces;
+  });
+in
+{
+  options.services.heisenbridge = {
+    enable = mkEnableOption "the Matrix to IRC bridge";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.heisenbridge;
+      defaultText = "pkgs.heisenbridge";
+      example = "pkgs.heisenbridge.override { … = …; }";
+      description = ''
+        Package of the application to run, exposed for overriding purposes.
+      '';
+    };
+
+    homeserver = mkOption {
+      type = types.str;
+      description = "The URL to the home server for client-server API calls";
+      example = "http://localhost:8008";
+    };
+
+    registrationUrl = mkOption {
+      type = types.str;
+      description = ''
+        The URL where the application service is listening for HS requests, from the Matrix HS perspective.#
+        The default value assumes the bridge runs on the same host as the home server, in the same network.
+      '';
+      example = "https://matrix.example.org";
+      default = "http://${cfg.address}:${toString cfg.port}";
+      defaultText = "http://$${cfg.address}:$${toString cfg.port}";
+    };
+
+    address = mkOption {
+      type = types.str;
+      description = "Address to listen on. IPv6 does not seem to be supported.";
+      default = "127.0.0.1";
+      example = "0.0.0.0";
+    };
+
+    port = mkOption {
+      type = types.port;
+      description = "The port to listen on";
+      default = 9898;
+    };
+
+    debug = mkOption {
+      type = types.bool;
+      description = "More verbose logging. Recommended during initial setup.";
+      default = false;
+    };
+
+    owner = mkOption {
+      type = types.nullOr types.str;
+      description = ''
+        Set owner MXID otherwise first talking local user will claim the bridge
+      '';
+      default = null;
+      example = "@admin:example.org";
+    };
+
+    namespaces = mkOption {
+      description = "Configure the 'namespaces' section of the registration.yml for the bridge and the server";
+      # TODO link to Matrix documentation of the format
+      type = types.submodule {
+        freeformType = jsonType;
+      };
+
+      default = {
+        users = [
+          {
+            regex = "@irc_.*";
+            exclusive = true;
+          }
+        ];
+        aliases = [ ];
+        rooms = [ ];
+      };
+    };
+
+    identd.enable = mkEnableOption "identd service support";
+    identd.port = mkOption {
+      type = types.port;
+      description = "identd listen port";
+      default = 113;
+    };
+
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      description = "Heisenbridge is configured over the command line. Append extra arguments here";
+      default = [ ];
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.heisenbridge = {
+      description = "Matrix<->IRC bridge";
+      before = [ "matrix-synapse.service" ]; # So the registration file can be used by Synapse
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        umask 077
+        set -e -u -o pipefail
+
+        if ! [ -f "${registrationFile}" ]; then
+          # Generate registration file if not present (actually, we only care about the tokens in it)
+          ${bin} --generate --config ${registrationFile}
+        fi
+
+        # Overwrite the registration file with our generated one (the config may have changed since then),
+        # but keep the tokens. Two step procedure to be failure safe
+        ${pkgs.yq}/bin/yq --slurp \
+          '.[0] + (.[1] | {as_token, hs_token})' \
+          ${bridgeConfig} \
+          ${registrationFile} \
+          > ${registrationFile}.new
+        mv -f ${registrationFile}.new ${registrationFile}
+
+        # Grant Synapse access to the registration
+        if ${getBin pkgs.glibc}/bin/getent group matrix-synapse > /dev/null; then
+          chgrp -v matrix-synapse ${registrationFile}
+          chmod -v g+r ${registrationFile}
+        fi
+      '';
+
+      serviceConfig = rec {
+        Type = "simple";
+        ExecStart = lib.concatStringsSep " " (
+          [
+            bin
+            (if cfg.debug then "-vvv" else "-v")
+            "--config"
+            registrationFile
+            "--listen-address"
+            (lib.escapeShellArg cfg.address)
+            "--listen-port"
+            (toString cfg.port)
+          ]
+          ++ (lib.optionals (cfg.owner != null) [
+            "--owner"
+            (lib.escapeShellArg cfg.owner)
+          ])
+          ++ (lib.optionals cfg.identd.enable [
+            "--identd"
+            "--identd-port"
+            (toString cfg.identd.port)
+          ])
+          ++ [
+            (lib.escapeShellArg cfg.homeserver)
+          ]
+          ++ (map (lib.escapeShellArg) cfg.extraArgs)
+        );
+
+        # Hardening options
+
+        User = "heisenbridge";
+        Group = "heisenbridge";
+        RuntimeDirectory = "heisenbridge";
+        RuntimeDirectoryMode = "0700";
+        StateDirectory = "heisenbridge";
+        StateDirectoryMode = "0755";
+
+        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 = [ "CAP_CHOWN" ] ++ optional (cfg.port < 1024 || (cfg.identd.enable && cfg.identd.port < 1024)) "CAP_NET_BIND_SERVICE";
+        AmbientCapabilities = CapabilityBoundingSet;
+        NoNewPrivileges = true;
+        LockPersonality = true;
+        RestrictRealtime = true;
+        SystemCallFilter = ["@system-service" "~@priviledged" "@chown"];
+        SystemCallArchitectures = "native";
+        RestrictAddressFamilies = "AF_INET AF_INET6";
+      };
+    };
+
+    users.groups.heisenbridge = {};
+    users.users.heisenbridge = {
+      description = "Service user for the Heisenbridge";
+      group = "heisenbridge";
+      isSystemUser = true;
+    };
+  };
+
+  meta.maintainers = [ lib.maintainers.piegames ];
+}
diff --git a/nixos/modules/services/misc/ihaskell.nix b/nixos/modules/services/misc/ihaskell.nix
new file mode 100644
index 00000000000..9978e8a4653
--- /dev/null
+++ b/nixos/modules/services/misc/ihaskell.nix
@@ -0,0 +1,65 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.ihaskell;
+  ihaskell = pkgs.ihaskell.override {
+    packages = cfg.extraPackages;
+  };
+
+in
+
+{
+  options = {
+    services.ihaskell = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Autostart an IHaskell notebook service.";
+      };
+
+      extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
+        default = haskellPackages: [];
+        defaultText = literalExpression "haskellPackages: []";
+        example = literalExpression ''
+          haskellPackages: [
+            haskellPackages.wreq
+            haskellPackages.lens
+          ]
+        '';
+        description = ''
+          Extra packages available to ghc when running ihaskell. The
+          value must be a function which receives the attrset defined
+          in <varname>haskellPackages</varname> as the sole argument.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users.ihaskell = {
+      group = config.users.groups.ihaskell.name;
+      description = "IHaskell user";
+      home = "/var/lib/ihaskell";
+      createHome = true;
+      uid = config.ids.uids.ihaskell;
+    };
+
+    users.groups.ihaskell.gid = config.ids.gids.ihaskell;
+
+    systemd.services.ihaskell = {
+      description = "IHaskell notebook instance";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        User = config.users.users.ihaskell.name;
+        Group = config.users.groups.ihaskell.name;
+        ExecStart = "${pkgs.runtimeShell} -c \"cd $HOME;${ihaskell}/bin/ihaskell-notebook\"";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/input-remapper.nix b/nixos/modules/services/misc/input-remapper.nix
new file mode 100644
index 00000000000..f5fb2bf5308
--- /dev/null
+++ b/nixos/modules/services/misc/input-remapper.nix
@@ -0,0 +1,30 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let cfg = config.services.input-remapper; in
+{
+  options = {
+    services.input-remapper = {
+      enable = mkEnableOption "input-remapper, an easy to use tool to change the mapping of your input device buttons.";
+      package = options.mkPackageOption pkgs "input-remapper" { };
+      enableUdevRules = mkEnableOption "udev rules added by input-remapper to handle hotplugged devices. Currently disabled by default due to https://github.com/sezanzeb/input-remapper/issues/140";
+      serviceWantedBy = mkOption {
+        default = [ "graphical.target" ];
+        example = [ "multi-user.target" ];
+        type = types.listOf types.str;
+        description = "Specifies the WantedBy setting for the input-remapper service.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = mkIf cfg.enableUdevRules [ cfg.package ];
+    services.dbus.packages = [ cfg.package ];
+    systemd.packages = [ cfg.package ];
+    environment.systemPackages = [ cfg.package ];
+    systemd.services.input-remapper.wantedBy = cfg.serviceWantedBy;
+  };
+
+  meta.maintainers = with lib.maintainers; [ LunNova ];
+}
diff --git a/nixos/modules/services/misc/irkerd.nix b/nixos/modules/services/misc/irkerd.nix
new file mode 100644
index 00000000000..993d77ba424
--- /dev/null
+++ b/nixos/modules/services/misc/irkerd.nix
@@ -0,0 +1,67 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.irkerd;
+  ports = [ 6659 ];
+in
+{
+  options.services.irkerd = {
+    enable = mkOption {
+      description = "Whether to enable irker, an IRC notification daemon.";
+      default = false;
+      type = types.bool;
+    };
+
+    openPorts = mkOption {
+      description = "Open ports in the firewall for irkerd";
+      default = false;
+      type = types.bool;
+    };
+
+    listenAddress = mkOption {
+      default = "localhost";
+      example = "0.0.0.0";
+      type = types.str;
+      description = ''
+        Specifies the bind address on which the irker daemon listens.
+        The default is localhost.
+
+        Irker authors strongly warn about the risks of running this on
+        a publicly accessible interface, so change this with caution.
+      '';
+    };
+
+    nick = mkOption {
+      default = "irker";
+      type = types.str;
+      description = "Nick to use for irker";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.irkerd = {
+      description = "Internet Relay Chat (IRC) notification daemon";
+      documentation = [ "man:irkerd(8)" "man:irkerhook(1)" "man:irk(1)" ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.irker}/bin/irkerd -H ${cfg.listenAddress} -n ${cfg.nick}";
+        User = "irkerd";
+      };
+    };
+
+    environment.systemPackages = [ pkgs.irker ];
+
+    users.users.irkerd = {
+      description = "Irker daemon user";
+      isSystemUser = true;
+      group = "irkerd";
+    };
+    users.groups.irkerd = {};
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openPorts ports;
+    networking.firewall.allowedUDPPorts = mkIf cfg.openPorts ports;
+  };
+}
diff --git a/nixos/modules/services/misc/jackett.nix b/nixos/modules/services/misc/jackett.nix
new file mode 100644
index 00000000000..c2144d4a9a9
--- /dev/null
+++ b/nixos/modules/services/misc/jackett.nix
@@ -0,0 +1,82 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.jackett;
+
+in
+{
+  options = {
+    services.jackett = {
+      enable = mkEnableOption "Jackett";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/jackett/.config/Jackett";
+        description = "The directory where Jackett stores its data files.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the Jackett web interface.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "jackett";
+        description = "User account under which Jackett runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "jackett";
+        description = "Group under which Jackett runs.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.jackett;
+        defaultText = literalExpression "pkgs.jackett";
+        description = "Jackett package to use.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.jackett = {
+      description = "Jackett";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.package}/bin/Jackett --NoUpdates --DataFolder '${cfg.dataDir}'";
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 9117 ];
+    };
+
+    users.users = mkIf (cfg.user == "jackett") {
+      jackett = {
+        group = cfg.group;
+        home = cfg.dataDir;
+        uid = config.ids.uids.jackett;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "jackett") {
+      jackett.gid = config.ids.gids.jackett;
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/jellyfin.nix b/nixos/modules/services/misc/jellyfin.nix
new file mode 100644
index 00000000000..04cf82f8a46
--- /dev/null
+++ b/nixos/modules/services/misc/jellyfin.nix
@@ -0,0 +1,122 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.jellyfin;
+in
+{
+  options = {
+    services.jellyfin = {
+      enable = mkEnableOption "Jellyfin Media Server";
+
+      user = mkOption {
+        type = types.str;
+        default = "jellyfin";
+        description = "User account under which Jellyfin runs.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.jellyfin;
+        defaultText = literalExpression "pkgs.jellyfin";
+        description = ''
+          Jellyfin package to use.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "jellyfin";
+        description = "Group under which jellyfin runs.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open the default ports in the firewall for the media server. The
+          HTTP/HTTPS ports can be changed in the Web UI, so this option should
+          only be used if they are unchanged.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.jellyfin = {
+      description = "Jellyfin Media Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = rec {
+        User = cfg.user;
+        Group = cfg.group;
+        StateDirectory = "jellyfin";
+        CacheDirectory = "jellyfin";
+        ExecStart = "${cfg.package}/bin/jellyfin --datadir '/var/lib/${StateDirectory}' --cachedir '/var/cache/${CacheDirectory}'";
+        Restart = "on-failure";
+
+        # Security options:
+
+        NoNewPrivileges = true;
+
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+
+        LockPersonality = true;
+
+        PrivateTmp = true;
+        # Disabled to allow Jellyfin to access hw accel devices endpoints
+        # PrivateDevices = true;
+        PrivateUsers = true;
+
+        # Disabled as it does not allow Jellyfin to interface with CUDA devices
+        # ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+
+        RemoveIPC = true;
+
+        RestrictNamespaces = true;
+        # AF_NETLINK needed because Jellyfin monitors the network connection
+        RestrictAddressFamilies = [ "AF_NETLINK" "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [
+          "@system-service"
+          "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@setuid"
+        ];
+      };
+    };
+
+    users.users = mkIf (cfg.user == "jellyfin") {
+      jellyfin = {
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "jellyfin") {
+      jellyfin = {};
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      # from https://jellyfin.org/docs/general/networking/index.html
+      allowedTCPPorts = [ 8096 8920 ];
+      allowedUDPPorts = [ 1900 7359 ];
+    };
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ minijackson ];
+}
diff --git a/nixos/modules/services/misc/klipper.nix b/nixos/modules/services/misc/klipper.nix
new file mode 100644
index 00000000000..7b3780b5cc9
--- /dev/null
+++ b/nixos/modules/services/misc/klipper.nix
@@ -0,0 +1,117 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.klipper;
+  format = pkgs.formats.ini {
+    # https://github.com/NixOS/nixpkgs/pull/121613#issuecomment-885241996
+    listToValue = l:
+      if builtins.length l == 1 then generators.mkValueStringDefault {} (head l)
+      else lib.concatMapStrings (s: "\n  ${generators.mkValueStringDefault {} s}") l;
+    mkKeyValue = generators.mkKeyValueDefault {} ":";
+  };
+in
+{
+  ##### interface
+  options = {
+    services.klipper = {
+      enable = mkEnableOption "Klipper, the 3D printer firmware";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.klipper;
+        defaultText = literalExpression "pkgs.klipper";
+        description = "The Klipper package.";
+      };
+
+      inputTTY = mkOption {
+        type = types.path;
+        default = "/run/klipper/tty";
+        description = "Path of the virtual printer symlink to create.";
+      };
+
+      apiSocket = mkOption {
+        type = types.nullOr types.path;
+        default = "/run/klipper/api";
+        description = "Path of the API socket to create.";
+      };
+
+      octoprintIntegration = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Allows Octoprint to control Klipper.";
+      };
+
+      user = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          User account under which Klipper runs.
+
+          If null is specified (default), a temporary user will be created by systemd.
+        '';
+      };
+
+      group = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Group account under which Klipper runs.
+
+          If null is specified (default), a temporary user will be created by systemd.
+        '';
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = { };
+        description = ''
+          Configuration for Klipper. See the <link xlink:href="https://www.klipper3d.org/Overview.html#configuration-and-tuning-guides">documentation</link>
+          for supported values.
+        '';
+      };
+    };
+  };
+
+  ##### implementation
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.octoprintIntegration -> config.services.octoprint.enable;
+        message = "Option klipper.octoprintIntegration requires Octoprint to be enabled on this system. Please enable services.octoprint to use it.";
+      }
+      {
+        assertion = cfg.user != null -> cfg.group != null;
+        message = "Option klipper.group is not set when a user is specified.";
+      }
+    ];
+
+    environment.etc."klipper.cfg".source = format.generate "klipper.cfg" cfg.settings;
+
+    services.klipper = mkIf cfg.octoprintIntegration {
+      user = config.services.octoprint.user;
+      group = config.services.octoprint.group;
+    };
+
+    systemd.services.klipper = let
+      klippyArgs = "--input-tty=${cfg.inputTTY}"
+        + optionalString (cfg.apiSocket != null) " --api-server=${cfg.apiSocket}";
+    in {
+      description = "Klipper 3D Printer Firmware";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/lib/klipper/klippy.py ${klippyArgs} /etc/klipper.cfg";
+        RuntimeDirectory = "klipper";
+        SupplementaryGroups = [ "dialout" ];
+        WorkingDirectory = "${cfg.package}/lib";
+      } // (if cfg.user != null then {
+        Group = cfg.group;
+        User = cfg.user;
+      } else {
+        DynamicUser = true;
+        User = "klipper";
+      });
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/leaps.nix b/nixos/modules/services/misc/leaps.nix
new file mode 100644
index 00000000000..f797218522c
--- /dev/null
+++ b/nixos/modules/services/misc/leaps.nix
@@ -0,0 +1,62 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.leaps;
+  stateDir = "/var/lib/leaps/";
+in
+{
+  options = {
+    services.leaps = {
+      enable = mkEnableOption "leaps";
+      port = mkOption {
+        type = types.port;
+        default = 8080;
+        description = "A port where leaps listens for incoming http requests";
+      };
+      address = mkOption {
+        default = "";
+        type = types.str;
+        example = "127.0.0.1";
+        description = "Hostname or IP-address to listen to. By default it will listen on all interfaces.";
+      };
+      path = mkOption {
+        default = "/";
+        type = types.path;
+        description = "Subdirectory used for reverse proxy setups";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users = {
+      users.leaps = {
+        uid             = config.ids.uids.leaps;
+        description     = "Leaps server user";
+        group           = "leaps";
+        home            = stateDir;
+        createHome      = true;
+      };
+
+      groups.leaps = {
+        gid = config.ids.gids.leaps;
+      };
+    };
+
+    systemd.services.leaps = {
+      description   = "leaps service";
+      wantedBy      = [ "multi-user.target" ];
+      after         = [ "network.target" ];
+
+      serviceConfig = {
+        User = "leaps";
+        Group = "leaps";
+        Restart = "on-failure";
+        WorkingDirectory = stateDir;
+        PrivateTmp = true;
+        ExecStart = "${pkgs.leaps}/bin/leaps -path ${toString cfg.path} -address ${cfg.address}:${toString cfg.port}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/libreddit.nix b/nixos/modules/services/misc/libreddit.nix
new file mode 100644
index 00000000000..77b34a85620
--- /dev/null
+++ b/nixos/modules/services/misc/libreddit.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+  let
+    cfg = config.services.libreddit;
+
+    args = concatStringsSep " " ([
+      "--port ${toString cfg.port}"
+      "--address ${cfg.address}"
+    ] ++ optional cfg.redirect "--redirect-https");
+
+in
+{
+  options = {
+    services.libreddit = {
+      enable = mkEnableOption "Private front-end for Reddit";
+
+      address = mkOption {
+        default = "0.0.0.0";
+        example = "127.0.0.1";
+        type =  types.str;
+        description = "The address to listen on";
+      };
+
+      port = mkOption {
+        default = 8080;
+        example = 8000;
+        type = types.port;
+        description = "The port to listen on";
+      };
+
+      redirect = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable the redirecting to HTTPS";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the libreddit web interface";
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.libreddit = {
+        description = "Private front-end for Reddit";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig = {
+          DynamicUser = true;
+          ExecStart = "${pkgs.libreddit}/bin/libreddit ${args}";
+          AmbientCapabilities = lib.mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+          Restart = "on-failure";
+          RestartSec = "2s";
+        };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/lidarr.nix b/nixos/modules/services/misc/lidarr.nix
new file mode 100644
index 00000000000..20153c7e61a
--- /dev/null
+++ b/nixos/modules/services/misc/lidarr.nix
@@ -0,0 +1,89 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.lidarr;
+in
+{
+  options = {
+    services.lidarr = {
+      enable = mkEnableOption "Lidarr";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/lidarr/.config/Lidarr";
+        description = "The directory where Lidarr stores its data files.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.lidarr;
+        defaultText = literalExpression "pkgs.lidarr";
+        description = "The Lidarr package to use";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for Lidarr
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "lidarr";
+        description = ''
+          User account under which Lidarr runs.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "lidarr";
+        description = ''
+          Group under which Lidarr runs.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.lidarr = {
+      description = "Lidarr";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.package}/bin/Lidarr -nobrowser -data='${cfg.dataDir}'";
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 8686 ];
+    };
+
+    users.users = mkIf (cfg.user == "lidarr") {
+      lidarr = {
+        group = cfg.group;
+        home = "/var/lib/lidarr";
+        uid = config.ids.uids.lidarr;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "lidarr") {
+      lidarr = {
+        gid = config.ids.gids.lidarr;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/lifecycled.nix b/nixos/modules/services/misc/lifecycled.nix
new file mode 100644
index 00000000000..1c8942998d6
--- /dev/null
+++ b/nixos/modules/services/misc/lifecycled.nix
@@ -0,0 +1,164 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.lifecycled;
+
+  # TODO: Add the ability to extend this with an rfc 42-like interface.
+  # In the meantime, one can modify the environment (as
+  # long as it's not overriding anything from here) with
+  # systemd.services.lifecycled.serviceConfig.Environment
+  configFile = pkgs.writeText "lifecycled" ''
+    LIFECYCLED_HANDLER=${cfg.handler}
+    ${lib.optionalString (cfg.cloudwatchGroup != null) "LIFECYCLED_CLOUDWATCH_GROUP=${cfg.cloudwatchGroup}"}
+    ${lib.optionalString (cfg.cloudwatchStream != null) "LIFECYCLED_CLOUDWATCH_STREAM=${cfg.cloudwatchStream}"}
+    ${lib.optionalString cfg.debug "LIFECYCLED_DEBUG=${lib.boolToString cfg.debug}"}
+    ${lib.optionalString (cfg.instanceId != null) "LIFECYCLED_INSTANCE_ID=${cfg.instanceId}"}
+    ${lib.optionalString cfg.json "LIFECYCLED_JSON=${lib.boolToString cfg.json}"}
+    ${lib.optionalString cfg.noSpot "LIFECYCLED_NO_SPOT=${lib.boolToString cfg.noSpot}"}
+    ${lib.optionalString (cfg.snsTopic != null) "LIFECYCLED_SNS_TOPIC=${cfg.snsTopic}"}
+    ${lib.optionalString (cfg.awsRegion != null) "AWS_REGION=${cfg.awsRegion}"}
+  '';
+in
+{
+  meta.maintainers = with maintainers; [ cole-h grahamc ];
+
+  options = {
+    services.lifecycled = {
+      enable = mkEnableOption "lifecycled";
+
+      queueCleaner = {
+        enable = mkEnableOption "lifecycled-queue-cleaner";
+
+        frequency = mkOption {
+          type = types.str;
+          default = "hourly";
+          description = ''
+            How often to trigger the queue cleaner.
+
+            NOTE: This string should be a valid value for a systemd
+            timer's <literal>OnCalendar</literal> configuration. See
+            <citerefentry><refentrytitle>systemd.timer</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+            for more information.
+          '';
+        };
+
+        parallel = mkOption {
+          type = types.ints.unsigned;
+          default = 20;
+          description = ''
+            The number of parallel deletes to run.
+          '';
+        };
+      };
+
+      instanceId = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The instance ID to listen for events for.
+        '';
+      };
+
+      snsTopic = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The SNS topic that receives events.
+        '';
+      };
+
+      noSpot = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Disable the spot termination listener.
+        '';
+      };
+
+      handler = mkOption {
+        type = types.path;
+        description = ''
+          The script to invoke to handle events.
+        '';
+      };
+
+      json = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable JSON logging.
+        '';
+      };
+
+      cloudwatchGroup = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Write logs to a specific Cloudwatch Logs group.
+        '';
+      };
+
+      cloudwatchStream = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Write logs to a specific Cloudwatch Logs stream. Defaults to the instance ID.
+        '';
+      };
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable debugging information.
+        '';
+      };
+
+      # XXX: Can be removed if / when
+      # https://github.com/buildkite/lifecycled/pull/91 is merged.
+      awsRegion = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The region used for accessing AWS services.
+        '';
+      };
+    };
+  };
+
+  ### Implementation ###
+
+  config = mkMerge [
+    (mkIf cfg.enable {
+      environment.etc."lifecycled".source = configFile;
+
+      systemd.packages = [ pkgs.lifecycled ];
+      systemd.services.lifecycled = {
+        wantedBy = [ "network-online.target" ];
+        restartTriggers = [ configFile ];
+      };
+    })
+
+    (mkIf cfg.queueCleaner.enable {
+      systemd.services.lifecycled-queue-cleaner = {
+        description = "Lifecycle Daemon Queue Cleaner";
+        environment = optionalAttrs (cfg.awsRegion != null) { AWS_REGION = cfg.awsRegion; };
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = "${pkgs.lifecycled}/bin/lifecycled-queue-cleaner -parallel ${toString cfg.queueCleaner.parallel}";
+        };
+      };
+
+      systemd.timers.lifecycled-queue-cleaner = {
+        description = "Lifecycle Daemon Queue Cleaner Timer";
+        wantedBy = [ "timers.target" ];
+        after = [ "network-online.target" ];
+        timerConfig = {
+          Unit = "lifecycled-queue-cleaner.service";
+          OnCalendar = "${cfg.queueCleaner.frequency}";
+        };
+      };
+    })
+  ];
+}
diff --git a/nixos/modules/services/misc/logkeys.nix b/nixos/modules/services/misc/logkeys.nix
new file mode 100644
index 00000000000..0082db63a06
--- /dev/null
+++ b/nixos/modules/services/misc/logkeys.nix
@@ -0,0 +1,30 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.logkeys;
+in {
+  options.services.logkeys = {
+    enable = mkEnableOption "logkeys service";
+
+    device = mkOption {
+      description = "Use the given device as keyboard input event device instead of /dev/input/eventX default.";
+      default = null;
+      type = types.nullOr types.str;
+      example = "/dev/input/event15";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.logkeys = {
+      description = "LogKeys Keylogger Daemon";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.logkeys}/bin/logkeys -s${lib.optionalString (cfg.device != null) " -d ${cfg.device}"}";
+        ExecStop = "${pkgs.logkeys}/bin/logkeys -k";
+        Type = "forking";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/mame.nix b/nixos/modules/services/misc/mame.nix
new file mode 100644
index 00000000000..dd6c5ef9aa0
--- /dev/null
+++ b/nixos/modules/services/misc/mame.nix
@@ -0,0 +1,69 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mame;
+  mame = "mame${lib.optionalString pkgs.stdenv.is64bit "64"}";
+in
+{
+  options = {
+    services.mame = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to setup TUN/TAP Ethernet interface for MAME emulator.
+        '';
+      };
+      user = mkOption {
+        type = types.str;
+        description = ''
+          User from which you run MAME binary.
+        '';
+      };
+      hostAddr = mkOption {
+        type = types.str;
+        description = ''
+          IP address of the host system. Usually an address of the main network
+          adapter or the adapter through which you get an internet connection.
+        '';
+        example = "192.168.31.156";
+      };
+      emuAddr = mkOption {
+        type = types.str;
+        description = ''
+          IP address of the guest system. The same you set inside guest OS under
+          MAME. Should be on the same subnet as <option>services.mame.hostAddr</option>.
+        '';
+        example = "192.168.31.155";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.mame ];
+
+    security.wrappers."${mame}" = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_net_admin,cap_net_raw+eip";
+      source = "${pkgs.mame}/bin/${mame}";
+    };
+
+    systemd.services.mame = {
+      description = "MAME TUN/TAP Ethernet interface";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.iproute2 ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStart = "${pkgs.mame}/bin/taputil.sh -c ${cfg.user} ${cfg.emuAddr} ${cfg.hostAddr} -";
+        ExecStop = "${pkgs.mame}/bin/taputil.sh -d ${cfg.user}";
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ ];
+}
diff --git a/nixos/modules/services/misc/matrix-appservice-discord.nix b/nixos/modules/services/misc/matrix-appservice-discord.nix
new file mode 100644
index 00000000000..8a8c7f41e3c
--- /dev/null
+++ b/nixos/modules/services/misc/matrix-appservice-discord.nix
@@ -0,0 +1,161 @@
+{ config, options, pkgs, lib, ... }:
+
+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
+  settingsFile = pkgs.writeText "matrix-appservice-discord-settings.json" (builtins.toJSON cfg.settings);
+
+in {
+  options = {
+    services.matrix-appservice-discord = {
+      enable = mkEnableOption "a bridge between Matrix and Discord";
+
+      settings = mkOption rec {
+        # TODO: switch to types.config.json as prescribed by RFC42 once it's implemented
+        type = types.attrs;
+        apply = recursiveUpdate default;
+        default = {
+          database = {
+            filename = "${dataDir}/discord.db";
+          };
+
+          # empty values necessary for registration file generation
+          # actual values defined in environmentFile
+          auth = {
+            clientID = "";
+            botToken = "";
+          };
+        };
+        example = literalExpression ''
+          {
+            bridge = {
+              domain = "public-domain.tld";
+              homeserverUrl = "http://public-domain.tld:8008";
+            };
+          }
+        '';
+        description = ''
+          <filename>config.yaml</filename> configuration as a Nix attribute set.
+          </para>
+
+          <para>
+          Configuration options should match those described in
+          <link xlink:href="https://github.com/Half-Shot/matrix-appservice-discord/blob/master/config/config.sample.yaml">
+          config.sample.yaml</link>.
+          </para>
+
+          <para>
+          <option>config.bridge.domain</option> and <option>config.bridge.homeserverUrl</option>
+          should be set to match the public host name of the Matrix homeserver for webhooks and avatars to work.
+          </para>
+
+          <para>
+          Secret tokens should be specified using <option>environmentFile</option>
+          instead of this world-readable attribute set.
+        '';
+      };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          File containing environment variables to be passed to the matrix-appservice-discord service,
+          in which secret tokens can be specified securely by defining values for
+          <literal>APPSERVICE_DISCORD_AUTH_CLIENT_I_D</literal> and
+          <literal>APPSERVICE_DISCORD_AUTH_BOT_TOKEN</literal>.
+        '';
+      };
+
+      url = mkOption {
+        type = types.str;
+        default = "http://localhost:${toString cfg.port}";
+        defaultText = literalExpression ''"http://localhost:''${toString config.${opt.port}}"'';
+        description = ''
+          The URL where the application service is listening for HS requests.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 9005; # from https://github.com/Half-Shot/matrix-appservice-discord/blob/master/package.json#L11
+        description = ''
+          Port number on which the bridge should listen for internal communication with the Matrix homeserver.
+        '';
+      };
+
+      localpart = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          The user_id localpart to assign to the AS.
+        '';
+      };
+
+      serviceDependencies = mkOption {
+        type = with types; listOf str;
+        default = optional config.services.matrix-synapse.enable "matrix-synapse.service";
+        defaultText = literalExpression ''
+          optional config.services.matrix-synapse.enable "matrix-synapse.service"
+        '';
+        description = ''
+          List of Systemd services to require and wait for when starting the application service,
+          such as the Matrix homeserver if it's running on the same host.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.matrix-appservice-discord = {
+      description = "A bridge between Matrix and Discord.";
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
+      after = [ "network-online.target" ] ++ cfg.serviceDependencies;
+
+      preStart = ''
+        if [ ! -f '${registrationFile}' ]; then
+          ${pkgs.matrix-appservice-discord}/bin/matrix-appservice-discord \
+            --generate-registration \
+            --url=${escapeShellArg cfg.url} \
+            ${optionalString (cfg.localpart != null) "--localpart=${escapeShellArg cfg.localpart}"} \
+            --config='${settingsFile}' \
+            --file='${registrationFile}'
+        fi
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+
+        DynamicUser = true;
+        PrivateTmp = true;
+        WorkingDirectory = appDir;
+        StateDirectory = baseNameOf dataDir;
+        UMask = 0027;
+        EnvironmentFile = cfg.environmentFile;
+
+        ExecStart = ''
+          ${pkgs.matrix-appservice-discord}/bin/matrix-appservice-discord \
+            --file='${registrationFile}' \
+            --config='${settingsFile}' \
+            --port='${toString cfg.port}'
+        '';
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ pacien ];
+}
diff --git a/nixos/modules/services/misc/matrix-appservice-irc.nix b/nixos/modules/services/misc/matrix-appservice-irc.nix
new file mode 100644
index 00000000000..b041c9c82c5
--- /dev/null
+++ b/nixos/modules/services/misc/matrix-appservice-irc.nix
@@ -0,0 +1,232 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.matrix-appservice-irc;
+
+  pkg = pkgs.matrix-appservice-irc;
+  bin = "${pkg}/bin/matrix-appservice-irc";
+
+  jsonType = (pkgs.formats.json {}).type;
+
+  configFile = pkgs.runCommand "matrix-appservice-irc.yml" {
+    # Because this program will be run at build time, we need `nativeBuildInputs`
+    nativeBuildInputs = [ (pkgs.python3.withPackages (ps: [ ps.pyyaml ps.jsonschema ])) ];
+    preferLocalBuild = true;
+
+    config = builtins.toJSON cfg.settings;
+    passAsFile = [ "config" ];
+  } ''
+    # The schema is given as yaml, we need to convert it to json
+    python -c 'import json; import yaml; import sys; json.dump(yaml.safe_load(sys.stdin), sys.stdout)' \
+      < ${pkg}/lib/node_modules/matrix-appservice-irc/config.schema.yml \
+      > config.schema.json
+    python -m jsonschema config.schema.json -i $configPath
+    cp "$configPath" "$out"
+  '';
+  registrationFile = "/var/lib/matrix-appservice-irc/registration.yml";
+in {
+  options.services.matrix-appservice-irc = with types; {
+    enable = mkEnableOption "the Matrix/IRC bridge";
+
+    port = mkOption {
+      type = port;
+      description = "The port to listen on";
+      default = 8009;
+    };
+
+    needBindingCap = mkOption {
+      type = bool;
+      description = "Whether the daemon needs to bind to ports below 1024 (e.g. for the ident service)";
+      default = false;
+    };
+
+    passwordEncryptionKeyLength = mkOption {
+      type = ints.unsigned;
+      description = "Length of the key to encrypt IRC passwords with";
+      default = 4096;
+      example = 8192;
+    };
+
+    registrationUrl = mkOption {
+      type = str;
+      description = ''
+        The URL where the application service is listening for homeserver requests,
+        from the Matrix homeserver perspective.
+      '';
+      example = "http://localhost:8009";
+    };
+
+    localpart = mkOption {
+      type = str;
+      description = "The user_id localpart to assign to the appservice";
+      default = "appservice-irc";
+    };
+
+    settings = mkOption {
+      description = ''
+        Configuration for the appservice, see
+        <link xlink:href="https://github.com/matrix-org/matrix-appservice-irc/blob/${pkgs.matrix-appservice-irc.version}/config.sample.yaml"/>
+        for supported values
+      '';
+      default = {};
+      type = submodule {
+        freeformType = jsonType;
+
+        options = {
+          homeserver = mkOption {
+            description = "Homeserver configuration";
+            default = {};
+            type = submodule {
+              freeformType = jsonType;
+
+              options = {
+                url = mkOption {
+                  type = str;
+                  description = "The URL to the home server for client-server API calls";
+                };
+
+                domain = mkOption {
+                  type = str;
+                  description = ''
+                    The 'domain' part for user IDs on this home server. Usually
+                    (but not always) is the "domain name" part of the homeserver URL.
+                  '';
+                };
+              };
+            };
+          };
+
+          database = mkOption {
+            default = {};
+            description = "Configuration for the database";
+            type = submodule {
+              freeformType = jsonType;
+
+              options = {
+                engine = mkOption {
+                  type = str;
+                  description = "Which database engine to use";
+                  default = "nedb";
+                  example = "postgres";
+                };
+
+                connectionString = mkOption {
+                  type = str;
+                  description = "The database connection string";
+                  default = "nedb://var/lib/matrix-appservice-irc/data";
+                  example = "postgres://username:password@host:port/databasename";
+                };
+              };
+            };
+          };
+
+          ircService = mkOption {
+            default = {};
+            description = "IRC bridge configuration";
+            type = submodule {
+              freeformType = jsonType;
+
+              options = {
+                passwordEncryptionKeyPath = mkOption {
+                  type = str;
+                  description = ''
+                    Location of the key with which IRC passwords are encrypted
+                    for storage. Will be generated on first run if not present.
+                  '';
+                  default = "/var/lib/matrix-appservice-irc/passkey.pem";
+                };
+
+                servers = mkOption {
+                  type = submodule { freeformType = jsonType; };
+                  description = "IRC servers to connect to";
+                };
+              };
+            };
+          };
+        };
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    systemd.services.matrix-appservice-irc = {
+      description = "Matrix-IRC bridge";
+      before = [ "matrix-synapse.service" ]; # So the registration can be used by Synapse
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        umask 077
+        # Generate key for crypting passwords
+        if ! [ -f "${cfg.settings.ircService.passwordEncryptionKeyPath}" ]; then
+          ${pkgs.openssl}/bin/openssl genpkey \
+              -out "${cfg.settings.ircService.passwordEncryptionKeyPath}" \
+              -outform PEM \
+              -algorithm RSA \
+              -pkeyopt "rsa_keygen_bits:${toString cfg.passwordEncryptionKeyLength}"
+        fi
+        # Generate registration file
+        if ! [ -f "${registrationFile}" ]; then
+          # The easy case: the file has not been generated yet
+          ${bin} --generate-registration --file ${registrationFile} --config ${configFile} --url ${cfg.registrationUrl} --localpart ${cfg.localpart}
+        else
+          # The tricky case: we already have a generation file. Because the NixOS configuration might have changed, we need to
+          # regenerate it. But this would give the service a new random ID and tokens, so we need to back up and restore them.
+          # 1. Backup
+          id=$(grep "^id:.*$" ${registrationFile})
+          hs_token=$(grep "^hs_token:.*$" ${registrationFile})
+          as_token=$(grep "^as_token:.*$" ${registrationFile})
+          # 2. Regenerate
+          ${bin} --generate-registration --file ${registrationFile} --config ${configFile} --url ${cfg.registrationUrl} --localpart ${cfg.localpart}
+          # 3. Restore
+          sed -i "s/^id:.*$/$id/g" ${registrationFile}
+          sed -i "s/^hs_token:.*$/$hs_token/g" ${registrationFile}
+          sed -i "s/^as_token:.*$/$as_token/g" ${registrationFile}
+        fi
+        # Allow synapse access to the registration
+        if ${getBin pkgs.glibc}/bin/getent group matrix-synapse > /dev/null; then
+          chgrp matrix-synapse ${registrationFile}
+          chmod g+r ${registrationFile}
+        fi
+      '';
+
+      serviceConfig = rec {
+        Type = "simple";
+        ExecStart = "${bin} --config ${configFile} --file ${registrationFile} --port ${toString cfg.port}";
+
+        ProtectHome = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        StateDirectory = "matrix-appservice-irc";
+        StateDirectoryMode = "755";
+
+        User = "matrix-appservice-irc";
+        Group = "matrix-appservice-irc";
+
+        CapabilityBoundingSet = [ "CAP_CHOWN" ] ++ optional (cfg.needBindingCap) "CAP_NET_BIND_SERVICE";
+        AmbientCapabilities = CapabilityBoundingSet;
+        NoNewPrivileges = true;
+
+        LockPersonality = true;
+        RestrictRealtime = true;
+        PrivateMounts = true;
+        SystemCallFilter = "~@aio @clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @setuid @swap";
+        SystemCallArchitectures = "native";
+        # AF_UNIX is required to connect to a postgres socket.
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
+      };
+    };
+
+    users.groups.matrix-appservice-irc = {};
+    users.users.matrix-appservice-irc = {
+      description = "Service user for the Matrix-IRC bridge";
+      group = "matrix-appservice-irc";
+      isSystemUser = true;
+    };
+  };
+
+  # uses attributes of the linked package
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/misc/matrix-conduit.nix b/nixos/modules/services/misc/matrix-conduit.nix
new file mode 100644
index 00000000000..108f64de7aa
--- /dev/null
+++ b/nixos/modules/services/misc/matrix-conduit.nix
@@ -0,0 +1,149 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.matrix-conduit;
+
+  format = pkgs.formats.toml {};
+  configFile = format.generate "conduit.toml" cfg.settings;
+in
+  {
+    meta.maintainers = with maintainers; [ pstn piegames ];
+    options.services.matrix-conduit = {
+      enable = mkEnableOption "matrix-conduit";
+
+      extraEnvironment = mkOption {
+        type = types.attrsOf types.str;
+        description = "Extra Environment variables to pass to the conduit server.";
+        default = {};
+        example = { RUST_BACKTRACE="yes"; };
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.matrix-conduit;
+        defaultText = "pkgs.matrix-conduit";
+        example = "pkgs.matrix-conduit";
+        description = ''
+          Package of the conduit matrix server to use.
+        '';
+      };
+
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = format.type;
+          options = {
+            global.server_name = mkOption {
+              type = types.str;
+              example = "example.com";
+              description = "The server_name is the name of this server. It is used as a suffix for user # and room ids.";
+            };
+            global.port = mkOption {
+              type = types.port;
+              default = 6167;
+              description = "The port Conduit will be running on. You need to set up a reverse proxy in your web server (e.g. apache or nginx), so all requests to /_matrix on port 443 and 8448 will be forwarded to the Conduit instance running on this port";
+            };
+            global.max_request_size = mkOption {
+              type = types.ints.positive;
+              default = 20000000;
+              description = "Max request size in bytes. Don't forget to also change it in the proxy.";
+            };
+            global.allow_registration = mkOption {
+              type = types.bool;
+              default = false;
+              description = "Whether new users can register on this server.";
+            };
+            global.allow_encryption = mkOption {
+              type = types.bool;
+              default = true;
+              description = "Whether new encrypted rooms can be created. Note: existing rooms will continue to work.";
+            };
+            global.allow_federation = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                Whether this server federates with other servers.
+              '';
+            };
+            global.trusted_servers = mkOption {
+              type = types.listOf types.str;
+              default = [ "matrix.org" ];
+              description = "Servers trusted with signing server keys.";
+            };
+            global.address = mkOption {
+              type = types.str;
+              default = "::1";
+              description = "Address to listen on for connections by the reverse proxy/tls terminator.";
+            };
+            global.database_path = mkOption {
+              type = types.str;
+              default = "/var/lib/matrix-conduit/";
+              readOnly = true;
+              description = ''
+                Path to the conduit database, the directory where conduit will save its data.
+                Note that due to using the DynamicUser feature of systemd, this value should not be changed
+                and is set to be read only.
+              '';
+            };
+            global.database_backend = mkOption {
+              type = types.enum [ "sqlite" "rocksdb" ];
+              default = "sqlite";
+              example = "rocksdb";
+              description = ''
+                The database backend for the service. Switching it on an existing
+                instance will require manual migration of data.
+              '';
+            };
+          };
+        };
+        default = {};
+        description = ''
+            Generates the conduit.toml configuration file. Refer to
+            <link xlink:href="https://gitlab.com/famedly/conduit/-/blob/master/conduit-example.toml"/>
+            for details on supported values.
+            Note that database_path can not be edited because the service's reliance on systemd StateDir.
+        '';
+      };
+    };
+
+    config = mkIf cfg.enable {
+      systemd.services.conduit = {
+        description = "Conduit Matrix Server";
+        documentation = [ "https://gitlab.com/famedly/conduit/" ];
+        wantedBy = [ "multi-user.target" ];
+        environment = lib.mkMerge ([
+          { CONDUIT_CONFIG = configFile; }
+          cfg.extraEnvironment
+        ]);
+        serviceConfig = {
+          DynamicUser = true;
+          User = "conduit";
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          ProtectClock = true;
+          ProtectControlGroups = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          PrivateDevices = true;
+          PrivateMounts = true;
+          PrivateUsers = true;
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [
+            "@system-service"
+            "~@privileged"
+          ];
+          StateDirectory = "matrix-conduit";
+          ExecStart = "${cfg.package}/bin/conduit";
+          Restart = "on-failure";
+          RestartSec = 10;
+          StartLimitBurst = 5;
+        };
+      };
+    };
+  }
diff --git a/nixos/modules/services/misc/mautrix-facebook.nix b/nixos/modules/services/misc/mautrix-facebook.nix
new file mode 100644
index 00000000000..e046c791ac0
--- /dev/null
+++ b/nixos/modules/services/misc/mautrix-facebook.nix
@@ -0,0 +1,195 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mautrix-facebook;
+  settingsFormat = pkgs.formats.json {};
+  settingsFile = settingsFormat.generate "mautrix-facebook-config.json" cfg.settings;
+
+  puppetRegex = concatStringsSep
+    ".*"
+    (map
+      escapeRegex
+      (splitString
+        "{userid}"
+        cfg.settings.bridge.username_template));
+in {
+  options = {
+    services.mautrix-facebook = {
+      enable = mkEnableOption "Mautrix-Facebook, a Matrix-Facebook hybrid puppeting/relaybot bridge";
+
+      settings = mkOption rec {
+        apply = recursiveUpdate default;
+        type = settingsFormat.type;
+        default = {
+          homeserver = {
+            address = "http://localhost:8008";
+          };
+
+          appservice = rec {
+            address = "http://${hostname}:${toString port}";
+            hostname = "localhost";
+            port = 29319;
+
+            database = "postgresql://";
+
+            bot_username = "facebookbot";
+          };
+
+          metrics.enabled = false;
+          manhole.enabled = false;
+
+          bridge = {
+            encryption = {
+              allow = true;
+              default = true;
+            };
+            username_template = "facebook_{userid}";
+          };
+
+          logging = {
+            version = 1;
+            formatters.journal_fmt.format = "%(name)s: %(message)s";
+            handlers.journal = {
+              class = "systemd.journal.JournalHandler";
+              formatter = "journal_fmt";
+              SYSLOG_IDENTIFIER = "mautrix-facebook";
+            };
+            root = {
+              level = "INFO";
+              handlers = ["journal"];
+            };
+          };
+        };
+        example = literalExpression ''
+          {
+            homeserver = {
+              address = "http://localhost:8008";
+              domain = "mydomain.example";
+            };
+
+            bridge.permissions = {
+              "@admin:mydomain.example" = "admin";
+              "mydomain.example" = "user";
+            };
+          }
+        '';
+        description = ''
+          <filename>config.yaml</filename> configuration as a Nix attribute set.
+          Configuration options should match those described in
+          <link xlink:href="https://github.com/mautrix/facebook/blob/master/mautrix_facebook/example-config.yaml">
+          example-config.yaml</link>.
+          </para>
+
+          <para>
+          Secret tokens should be specified using <option>environmentFile</option>
+          instead of this world-readable attribute set.
+        '';
+      };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          File containing environment variables to be passed to the mautrix-telegram service.
+
+          Any config variable can be overridden by setting <literal>MAUTRIX_FACEBOOK_SOME_KEY</literal> to override the <literal>some.key</literal> variable.
+        '';
+      };
+
+      configurePostgresql = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable PostgreSQL and create a user and database for mautrix-facebook. The default <literal>settings</literal> reference this database, if you disable this option you must provide a database URL.
+        '';
+      };
+
+      registrationData = mkOption {
+        type = types.attrs;
+        default = {};
+        description = ''
+          Output data for appservice registration. Simply make any desired changes and serialize to JSON. Note that this data contains secrets so think twice before putting it into the nix store.
+
+          Currently <literal>as_token</literal> and <literal>hs_token</literal> need to be added as they are not known to this module.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.mautrix-facebook = {
+      group = "mautrix-facebook";
+      isSystemUser = true;
+    };
+
+    services.postgresql = mkIf cfg.configurePostgresql {
+      ensureDatabases = ["mautrix-facebook"];
+      ensureUsers = [{
+        name = "mautrix-facebook";
+        ensurePermissions = {
+          "DATABASE \"mautrix-facebook\"" = "ALL PRIVILEGES";
+        };
+      }];
+    };
+
+    systemd.services.mautrix-facebook = rec {
+      wantedBy = [ "multi-user.target" ];
+      wants = [
+        "network-online.target"
+      ] ++ optional config.services.matrix-synapse.enable "matrix-synapse.service"
+        ++ optional cfg.configurePostgresql "postgresql.service";
+      after = wants;
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        User = "mautrix-facebook";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        PrivateTmp = true;
+
+        EnvironmentFile = cfg.environmentFile;
+
+        ExecStart = ''
+          ${pkgs.mautrix-facebook}/bin/mautrix-facebook --config=${settingsFile}
+        '';
+      };
+    };
+
+    services.mautrix-facebook = {
+      registrationData = {
+        id = "mautrix-facebook";
+
+        namespaces = {
+          users = [
+            {
+              exclusive = true;
+              regex = escapeRegex "@${cfg.settings.appservice.bot_username}:${cfg.settings.homeserver.domain}";
+            }
+            {
+              exclusive = true;
+              regex = "@${puppetRegex}:${escapeRegex cfg.settings.homeserver.domain}";
+            }
+          ];
+          aliases = [];
+        };
+
+        url = cfg.settings.appservice.address;
+        sender_localpart = "mautrix-facebook-sender";
+
+        rate_limited = false;
+        "de.sorunome.msc2409.push_ephemeral" = true;
+        push_ephemeral = true;
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ kevincox ];
+}
diff --git a/nixos/modules/services/misc/mautrix-telegram.nix b/nixos/modules/services/misc/mautrix-telegram.nix
new file mode 100644
index 00000000000..794c4dd9ddc
--- /dev/null
+++ b/nixos/modules/services/misc/mautrix-telegram.nix
@@ -0,0 +1,181 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  dataDir = "/var/lib/mautrix-telegram";
+  registrationFile = "${dataDir}/telegram-registration.yaml";
+  cfg = config.services.mautrix-telegram;
+  settingsFormat = pkgs.formats.json {};
+  settingsFileUnsubstituted = settingsFormat.generate "mautrix-telegram-config-unsubstituted.json" cfg.settings;
+  settingsFile = "${dataDir}/config.json";
+
+in {
+  options = {
+    services.mautrix-telegram = {
+      enable = mkEnableOption "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge";
+
+      settings = mkOption rec {
+        apply = recursiveUpdate default;
+        inherit (settingsFormat) type;
+        default = {
+          appservice = rec {
+            database = "sqlite:///${dataDir}/mautrix-telegram.db";
+            database_opts = {};
+            hostname = "0.0.0.0";
+            port = 8080;
+            address = "http://localhost:${toString port}";
+          };
+
+          bridge = {
+            permissions."*" = "relaybot";
+            relaybot.whitelist = [ ];
+            double_puppet_server_map = {};
+            login_shared_secret_map = {};
+          };
+
+          logging = {
+            version = 1;
+
+            formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s";
+
+            handlers.console = {
+              class = "logging.StreamHandler";
+              formatter = "precise";
+            };
+
+            loggers = {
+              mau.level = "INFO";
+              telethon.level = "INFO";
+
+              # prevent tokens from leaking in the logs:
+              # https://github.com/tulir/mautrix-telegram/issues/351
+              aiohttp.level = "WARNING";
+            };
+
+            # log to console/systemd instead of file
+            root = {
+              level = "INFO";
+              handlers = [ "console" ];
+            };
+          };
+        };
+        example = literalExpression ''
+          {
+            homeserver = {
+              address = "http://localhost:8008";
+              domain = "public-domain.tld";
+            };
+
+            appservice.public = {
+              prefix = "/public";
+              external = "https://public-appservice-address/public";
+            };
+
+            bridge.permissions = {
+              "example.com" = "full";
+              "@admin:example.com" = "admin";
+            };
+          }
+        '';
+        description = ''
+          <filename>config.yaml</filename> configuration as a Nix attribute set.
+          Configuration options should match those described in
+          <link xlink:href="https://github.com/tulir/mautrix-telegram/blob/master/example-config.yaml">
+          example-config.yaml</link>.
+          </para>
+
+          <para>
+          Secret tokens should be specified using <option>environmentFile</option>
+          instead of this world-readable attribute set.
+        '';
+      };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          File containing environment variables to be passed to the mautrix-telegram service,
+          in which secret tokens can be specified securely by defining values for
+          <literal>MAUTRIX_TELEGRAM_APPSERVICE_AS_TOKEN</literal>,
+          <literal>MAUTRIX_TELEGRAM_APPSERVICE_HS_TOKEN</literal>,
+          <literal>MAUTRIX_TELEGRAM_TELEGRAM_API_ID</literal>,
+          <literal>MAUTRIX_TELEGRAM_TELEGRAM_API_HASH</literal> and optionally
+          <literal>MAUTRIX_TELEGRAM_TELEGRAM_BOT_TOKEN</literal>.
+        '';
+      };
+
+      serviceDependencies = mkOption {
+        type = with types; listOf str;
+        default = optional config.services.matrix-synapse.enable "matrix-synapse.service";
+        defaultText = literalExpression ''
+          optional config.services.matrix-synapse.enable "matrix-synapse.service"
+        '';
+        description = ''
+          List of Systemd services to require and wait for when starting the application service.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.mautrix-telegram = {
+      description = "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge.";
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
+      after = [ "network-online.target" ] ++ cfg.serviceDependencies;
+
+      preStart = ''
+        # Not all secrets can be passed as environment variable (yet)
+        # https://github.com/tulir/mautrix-telegram/issues/584
+        [ -f ${settingsFile} ] && rm -f ${settingsFile}
+        old_umask=$(umask)
+        umask 0177
+        ${pkgs.envsubst}/bin/envsubst \
+          -o ${settingsFile} \
+          -i ${settingsFileUnsubstituted}
+        umask $old_umask
+
+        # generate the appservice's registration file if absent
+        if [ ! -f '${registrationFile}' ]; then
+          ${pkgs.mautrix-telegram}/bin/mautrix-telegram \
+            --generate-registration \
+            --base-config='${pkgs.mautrix-telegram}/${pkgs.mautrix-telegram.pythonModule.sitePackages}/mautrix_telegram/example-config.yaml' \
+            --config='${settingsFile}' \
+            --registration='${registrationFile}'
+        fi
+      '' + lib.optionalString (pkgs.mautrix-telegram ? alembic) ''
+        # run automatic database init and migration scripts
+        ${pkgs.mautrix-telegram.alembic}/bin/alembic -x config='${settingsFile}' upgrade head
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+
+        DynamicUser = true;
+        PrivateTmp = true;
+        WorkingDirectory = pkgs.mautrix-telegram; # necessary for the database migration scripts to be found
+        StateDirectory = baseNameOf dataDir;
+        UMask = 0027;
+        EnvironmentFile = cfg.environmentFile;
+
+        ExecStart = ''
+          ${pkgs.mautrix-telegram}/bin/mautrix-telegram \
+            --config='${settingsFile}'
+        '';
+      };
+
+      restartTriggers = [ settingsFileUnsubstituted ];
+    };
+  };
+
+  meta.maintainers = with maintainers; [ pacien vskilet ];
+}
diff --git a/nixos/modules/services/misc/mbpfan.nix b/nixos/modules/services/misc/mbpfan.nix
new file mode 100644
index 00000000000..e0a4d8a13e7
--- /dev/null
+++ b/nixos/modules/services/misc/mbpfan.nix
@@ -0,0 +1,107 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mbpfan;
+  verbose = if cfg.verbose then "v" else "";
+  settingsFormat = pkgs.formats.ini {};
+  settingsFile = settingsFormat.generate "mbpfan.ini" cfg.settings;
+
+in {
+  options.services.mbpfan = {
+    enable = mkEnableOption "mbpfan, fan controller daemon for Apple Macs and MacBooks";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.mbpfan;
+      defaultText = literalExpression "pkgs.mbpfan";
+      description = ''
+        The package used for the mbpfan daemon.
+      '';
+    };
+
+    verbose = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        If true, sets the log level to verbose.
+      '';
+    };
+
+    settings = mkOption {
+      default = {};
+      description = "The INI configuration for Mbpfan.";
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+
+        options.general.min_fan1_speed = mkOption {
+          type = types.nullOr types.int;
+          default = 2000;
+          description = ''
+            The minimum fan speed. Setting to null enables automatic detection.
+            Check minimum fan limits with "cat /sys/devices/platform/applesmc.768/fan*_min".
+          '';
+        };
+        options.general.max_fan1_speed = mkOption {
+          type = types.nullOr types.int;
+          default = 6199;
+          description = ''
+            The maximum fan speed. Setting to null enables automatic detection.
+            Check maximum fan limits with "cat /sys/devices/platform/applesmc.768/fan*_max".
+          '';
+        };
+        options.general.low_temp = mkOption {
+          type = types.int;
+          default = 55;
+          description = "Temperature below which fan speed will be at minimum. Try ranges 55-63.";
+        };
+        options.general.high_temp = mkOption {
+          type = types.int;
+          default = 58;
+          description = "Fan will increase speed when higher than this temperature. Try ranges 58-66.";
+        };
+        options.general.max_temp = mkOption {
+          type = types.int;
+          default = 86;
+          description = "Fan will run at full speed above this temperature. Do not set it > 90.";
+        };
+        options.general.polling_interval = mkOption {
+          type = types.int;
+          default = 1;
+          description = "The polling interval.";
+        };
+      };
+    };
+  };
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "mbpfan" "pollingInterval" ] [ "services" "mbpfan" "settings" "general" "polling_interval" ])
+    (mkRenamedOptionModule [ "services" "mbpfan" "maxTemp" ] [ "services" "mbpfan" "settings" "general" "max_temp" ])
+    (mkRenamedOptionModule [ "services" "mbpfan" "lowTemp" ] [ "services" "mbpfan" "settings" "general" "low_temp" ])
+    (mkRenamedOptionModule [ "services" "mbpfan" "highTemp" ] [ "services" "mbpfan" "settings" "general" "high_temp" ])
+    (mkRenamedOptionModule [ "services" "mbpfan" "minFanSpeed" ] [ "services" "mbpfan" "settings" "general" "min_fan1_speed" ])
+    (mkRenamedOptionModule [ "services" "mbpfan" "maxFanSpeed" ] [ "services" "mbpfan" "settings" "general" "max_fan1_speed" ])
+  ];
+
+  config = mkIf cfg.enable {
+    boot.kernelModules = [ "coretemp" "applesmc" ];
+
+    environment.etc."mbpfan.conf".source = settingsFile;
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.mbpfan = {
+      description = "A fan manager daemon for MacBook Pro";
+      wantedBy = [ "sysinit.target" ];
+      after = [ "syslog.target" "sysinit.target" ];
+      restartTriggers = [ config.environment.etc."mbpfan.conf".source ];
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${cfg.package}/bin/mbpfan -f${verbose}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        PIDFile = "/run/mbpfan.pid";
+        Restart = "always";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/mediatomb.nix b/nixos/modules/services/misc/mediatomb.nix
new file mode 100644
index 00000000000..ee5c0ef8d27
--- /dev/null
+++ b/nixos/modules/services/misc/mediatomb.nix
@@ -0,0 +1,394 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  gid = config.ids.gids.mediatomb;
+  cfg = config.services.mediatomb;
+  opt = options.services.mediatomb;
+  name = cfg.package.pname;
+  pkg = cfg.package;
+  optionYesNo = option: if option then "yes" else "no";
+  # configuration on media directory
+  mediaDirectory = {
+    options = {
+      path = mkOption {
+        type = types.str;
+        description = ''
+          Absolute directory path to the media directory to index.
+        '';
+      };
+      recursive = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether the indexation must take place recursively or not.";
+      };
+      hidden-files = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to index the hidden files or not.";
+      };
+    };
+  };
+  toMediaDirectory = d: "<directory location=\"${d.path}\" mode=\"inotify\" recursive=\"${optionYesNo d.recursive}\" hidden-files=\"${optionYesNo d.hidden-files}\" />\n";
+
+  transcodingConfig = if cfg.transcoding then with pkgs; ''
+    <transcoding enabled="yes">
+      <mimetype-profile-mappings>
+        <transcode mimetype="video/x-flv" using="vlcmpeg" />
+        <transcode mimetype="application/ogg" using="vlcmpeg" />
+        <transcode mimetype="audio/ogg" using="ogg2mp3" />
+        <transcode mimetype="audio/x-flac" using="oggflac2raw"/>
+      </mimetype-profile-mappings>
+      <profiles>
+        <profile name="ogg2mp3" enabled="no" type="external">
+          <mimetype>audio/mpeg</mimetype>
+          <accept-url>no</accept-url>
+          <first-resource>yes</first-resource>
+          <accept-ogg-theora>no</accept-ogg-theora>
+          <agent command="${ffmpeg}/bin/ffmpeg" arguments="-y -i %in -f mp3 %out" />
+          <buffer size="1048576" chunk-size="131072" fill-size="262144" />
+        </profile>
+        <profile name="vlcmpeg" enabled="no" type="external">
+          <mimetype>video/mpeg</mimetype>
+          <accept-url>yes</accept-url>
+          <first-resource>yes</first-resource>
+          <accept-ogg-theora>yes</accept-ogg-theora>
+          <agent command="${libsForQt5.vlc}/bin/vlc"
+            arguments="-I dummy %in --sout #transcode{venc=ffmpeg,vcodec=mp2v,vb=4096,fps=25,aenc=ffmpeg,acodec=mpga,ab=192,samplerate=44100,channels=2}:standard{access=file,mux=ps,dst=%out} vlc:quit" />
+          <buffer size="14400000" chunk-size="512000" fill-size="120000" />
+        </profile>
+      </profiles>
+    </transcoding>
+'' else ''
+    <transcoding enabled="no">
+    </transcoding>
+'';
+
+  configText = optionalString (! cfg.customCfg) ''
+<?xml version="1.0" encoding="UTF-8"?>
+<config version="2" xmlns="http://mediatomb.cc/config/2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://mediatomb.cc/config/2 http://mediatomb.cc/config/2.xsd">
+    <server>
+      <ui enabled="yes" show-tooltips="yes">
+        <accounts enabled="no" session-timeout="30">
+          <account user="${name}" password="${name}"/>
+        </accounts>
+      </ui>
+      <name>${cfg.serverName}</name>
+      <udn>uuid:${cfg.uuid}</udn>
+      <home>${cfg.dataDir}</home>
+      <interface>${cfg.interface}</interface>
+      <webroot>${pkg}/share/${name}/web</webroot>
+      <pc-directory upnp-hide="${optionYesNo cfg.pcDirectoryHide}"/>
+      <storage>
+        <sqlite3 enabled="yes">
+          <database-file>${name}.db</database-file>
+        </sqlite3>
+      </storage>
+      <protocolInfo extend="${optionYesNo cfg.ps3Support}"/>
+      ${optionalString cfg.dsmSupport ''
+      <custom-http-headers>
+        <add header="X-User-Agent: redsonic"/>
+      </custom-http-headers>
+
+      <manufacturerURL>redsonic.com</manufacturerURL>
+      <modelNumber>105</modelNumber>
+      ''}
+        ${optionalString cfg.tg100Support ''
+      <upnp-string-limit>101</upnp-string-limit>
+      ''}
+      <extended-runtime-options>
+        <mark-played-items enabled="yes" suppress-cds-updates="yes">
+          <string mode="prepend">*</string>
+          <mark>
+            <content>video</content>
+          </mark>
+        </mark-played-items>
+      </extended-runtime-options>
+    </server>
+    <import hidden-files="no">
+      <autoscan use-inotify="auto">
+      ${concatMapStrings toMediaDirectory cfg.mediaDirectories}
+      </autoscan>
+      <scripting script-charset="UTF-8">
+        <common-script>${pkg}/share/${name}/js/common.js</common-script>
+        <playlist-script>${pkg}/share/${name}/js/playlists.js</playlist-script>
+        <virtual-layout type="builtin">
+          <import-script>${pkg}/share/${name}/js/import.js</import-script>
+        </virtual-layout>
+      </scripting>
+      <mappings>
+        <extension-mimetype ignore-unknown="no">
+          <map from="mp3" to="audio/mpeg"/>
+          <map from="ogx" to="application/ogg"/>
+          <map from="ogv" to="video/ogg"/>
+          <map from="oga" to="audio/ogg"/>
+          <map from="ogg" to="audio/ogg"/>
+          <map from="ogm" to="video/ogg"/>
+          <map from="asf" to="video/x-ms-asf"/>
+          <map from="asx" to="video/x-ms-asf"/>
+          <map from="wma" to="audio/x-ms-wma"/>
+          <map from="wax" to="audio/x-ms-wax"/>
+          <map from="wmv" to="video/x-ms-wmv"/>
+          <map from="wvx" to="video/x-ms-wvx"/>
+          <map from="wm" to="video/x-ms-wm"/>
+          <map from="wmx" to="video/x-ms-wmx"/>
+          <map from="m3u" to="audio/x-mpegurl"/>
+          <map from="pls" to="audio/x-scpls"/>
+          <map from="flv" to="video/x-flv"/>
+          <map from="mkv" to="video/x-matroska"/>
+          <map from="mka" to="audio/x-matroska"/>
+          ${optionalString cfg.ps3Support ''
+          <map from="avi" to="video/divx"/>
+          ''}
+          ${optionalString cfg.dsmSupport ''
+          <map from="avi" to="video/avi"/>
+          ''}
+        </extension-mimetype>
+        <mimetype-upnpclass>
+          <map from="audio/*" to="object.item.audioItem.musicTrack"/>
+          <map from="video/*" to="object.item.videoItem"/>
+          <map from="image/*" to="object.item.imageItem"/>
+        </mimetype-upnpclass>
+        <mimetype-contenttype>
+          <treat mimetype="audio/mpeg" as="mp3"/>
+          <treat mimetype="application/ogg" as="ogg"/>
+          <treat mimetype="audio/ogg" as="ogg"/>
+          <treat mimetype="audio/x-flac" as="flac"/>
+          <treat mimetype="audio/x-ms-wma" as="wma"/>
+          <treat mimetype="audio/x-wavpack" as="wv"/>
+          <treat mimetype="image/jpeg" as="jpg"/>
+          <treat mimetype="audio/x-mpegurl" as="playlist"/>
+          <treat mimetype="audio/x-scpls" as="playlist"/>
+          <treat mimetype="audio/x-wav" as="pcm"/>
+          <treat mimetype="audio/L16" as="pcm"/>
+          <treat mimetype="video/x-msvideo" as="avi"/>
+          <treat mimetype="video/mp4" as="mp4"/>
+          <treat mimetype="audio/mp4" as="mp4"/>
+          <treat mimetype="application/x-iso9660" as="dvd"/>
+          <treat mimetype="application/x-iso9660-image" as="dvd"/>
+        </mimetype-contenttype>
+      </mappings>
+      <online-content>
+        <YouTube enabled="no" refresh="28800" update-at-start="no" purge-after="604800" racy-content="exclude" format="mp4" hd="no">
+          <favorites user="${name}"/>
+          <standardfeed feed="most_viewed" time-range="today"/>
+          <playlists user="${name}"/>
+          <uploads user="${name}"/>
+          <standardfeed feed="recently_featured" time-range="today"/>
+        </YouTube>
+      </online-content>
+    </import>
+    ${transcodingConfig}
+  </config>
+'';
+  defaultFirewallRules = {
+    # udp 1900 port needs to be opened for SSDP (not configurable within
+    # mediatomb/gerbera) cf.
+    # http://docs.gerbera.io/en/latest/run.html?highlight=udp%20port#network-setup
+    allowedUDPPorts = [ 1900 cfg.port ];
+    allowedTCPPorts = [ cfg.port ];
+  };
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.mediatomb = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the Gerbera/Mediatomb DLNA server.
+        '';
+      };
+
+      serverName = mkOption {
+        type = types.str;
+        default = "Gerbera (Mediatomb)";
+        description = ''
+          How to identify the server on the network.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.gerbera;
+        defaultText = literalExpression "pkgs.gerbera";
+        description = ''
+          Underlying package to be used with the module.
+        '';
+      };
+
+      ps3Support = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable ps3 specific tweaks.
+          WARNING: incompatible with DSM 320 support.
+        '';
+      };
+
+      dsmSupport = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable D-Link DSM 320 specific tweaks.
+          WARNING: incompatible with ps3 support.
+        '';
+      };
+
+      tg100Support = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Telegent TG100 specific tweaks.
+        '';
+      };
+
+      transcoding = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable transcoding.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/${name}";
+        defaultText = literalExpression ''"/var/lib/''${config.${opt.package}.pname}"'';
+        description = ''
+          The directory where Gerbera/Mediatomb stores its state, data, etc.
+        '';
+      };
+
+      pcDirectoryHide = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to list the top-level directory or not (from upnp client standpoint).
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "mediatomb";
+        description = "User account under which the service runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "mediatomb";
+        description = "Group account under which the service runs.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 49152;
+        description = ''
+          The network port to listen on.
+        '';
+      };
+
+      interface = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          A specific interface to bind to.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If false (the default), this is up to the user to declare the firewall rules.
+          If true, this opens port 1900 (tcp and udp) and the port specified by
+          <option>sercvices.mediatomb.port</option>.
+
+          If the option <option>services.mediatomb.interface</option> is set,
+          the firewall rules opened are dedicated to that interface. Otherwise,
+          those rules are opened globally.
+        '';
+      };
+
+      uuid = mkOption {
+        type = types.str;
+        default = "fdfc8a4e-a3ad-4c1d-b43d-a2eedb03a687";
+        description = ''
+          A unique (on your network) to identify the server by.
+        '';
+      };
+
+      mediaDirectories = mkOption {
+        type = with types; listOf (submodule mediaDirectory);
+        default = [];
+        description = ''
+          Declare media directories to index.
+        '';
+        example = [
+          { path = "/data/pictures"; recursive = false; hidden-files = false; }
+          { path = "/data/audio"; recursive = true; hidden-files = false; }
+        ];
+      };
+
+      customCfg = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Allow the service to create and use its own config file inside the <literal>dataDir</literal> as
+          configured by <option>services.mediatomb.dataDir</option>.
+          Deactivated by default, the service then runs with the configuration generated from this module.
+          Otherwise, when enabled, no service configuration is generated. Gerbera/Mediatomb then starts using
+          config.xml within the configured <literal>dataDir</literal>. It's up to the user to make a correct
+          configuration file.
+        '';
+      };
+
+    };
+  };
+
+
+  ###### implementation
+
+  config = let binaryCommand = "${pkg}/bin/${name}";
+               interfaceFlag = optionalString ( cfg.interface != "") "--interface ${cfg.interface}";
+               configFlag = optionalString (! cfg.customCfg) "--config ${pkgs.writeText "config.xml" configText}";
+    in mkIf cfg.enable {
+    systemd.services.mediatomb = {
+      description = "${cfg.serverName} media Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig.ExecStart = "${binaryCommand} --port ${toString cfg.port} ${interfaceFlag} ${configFlag} --home ${cfg.dataDir}";
+      serviceConfig.User = cfg.user;
+      serviceConfig.Group = cfg.group;
+    };
+
+    users.groups = optionalAttrs (cfg.group == "mediatomb") {
+      mediatomb.gid = gid;
+    };
+
+    users.users = optionalAttrs (cfg.user == "mediatomb") {
+      mediatomb = {
+        isSystemUser = true;
+        group = cfg.group;
+        home = cfg.dataDir;
+        createHome = true;
+        description = "${name} DLNA Server User";
+      };
+    };
+
+    # Open firewall only if users enable it
+    networking.firewall = mkMerge [
+      (mkIf (cfg.openFirewall && cfg.interface != "") {
+        interfaces."${cfg.interface}" = defaultFirewallRules;
+      })
+      (mkIf (cfg.openFirewall && cfg.interface == "") defaultFirewallRules)
+    ];
+  };
+}
diff --git a/nixos/modules/services/misc/metabase.nix b/nixos/modules/services/misc/metabase.nix
new file mode 100644
index 00000000000..e78100a046a
--- /dev/null
+++ b/nixos/modules/services/misc/metabase.nix
@@ -0,0 +1,103 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.metabase;
+
+  inherit (lib) mkEnableOption mkIf mkOption;
+  inherit (lib) optional optionalAttrs types;
+
+  dataDir = "/var/lib/metabase";
+
+in {
+
+  options = {
+
+    services.metabase = {
+      enable = mkEnableOption "Metabase service";
+
+      listen = {
+        ip = mkOption {
+          type = types.str;
+          default = "0.0.0.0";
+          description = ''
+            IP address that Metabase should listen on.
+          '';
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 3000;
+          description = ''
+            Listen port for Metabase.
+          '';
+        };
+      };
+
+      ssl = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether to enable SSL (https) support.
+          '';
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8443;
+          description = ''
+            Listen port over SSL (https) for Metabase.
+          '';
+        };
+
+        keystore = mkOption {
+          type = types.nullOr types.path;
+          default = "${dataDir}/metabase.jks";
+          example = "/etc/secrets/keystore.jks";
+          description = ''
+            <link xlink:href="https://www.digitalocean.com/community/tutorials/java-keytool-essentials-working-with-java-keystores">Java KeyStore</link> file containing the certificates.
+          '';
+        };
+
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for Metabase.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.metabase = {
+      description = "Metabase server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      environment = {
+        MB_PLUGINS_DIR = "${dataDir}/plugins";
+        MB_DB_FILE = "${dataDir}/metabase.db";
+        MB_JETTY_HOST = cfg.listen.ip;
+        MB_JETTY_PORT = toString cfg.listen.port;
+      } // optionalAttrs (cfg.ssl.enable) {
+        MB_JETTY_SSL = true;
+        MB_JETTY_SSL_PORT = toString cfg.ssl.port;
+        MB_JETTY_SSL_KEYSTORE = cfg.ssl.keystore;
+      };
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = baseNameOf dataDir;
+        ExecStart = "${pkgs.metabase}/bin/metabase";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listen.port ] ++ optional cfg.ssl.enable cfg.ssl.port;
+    };
+
+  };
+}
diff --git a/nixos/modules/services/misc/moonraker.nix b/nixos/modules/services/misc/moonraker.nix
new file mode 100644
index 00000000000..ae57aaa6d47
--- /dev/null
+++ b/nixos/modules/services/misc/moonraker.nix
@@ -0,0 +1,138 @@
+{ config, lib, options, pkgs, ... }:
+with lib;
+let
+  pkg = pkgs.moonraker;
+  cfg = config.services.moonraker;
+  opt = options.services.moonraker;
+  format = pkgs.formats.ini {
+    # https://github.com/NixOS/nixpkgs/pull/121613#issuecomment-885241996
+    listToValue = l:
+      if builtins.length l == 1 then generators.mkValueStringDefault {} (head l)
+      else lib.concatMapStrings (s: "\n  ${generators.mkValueStringDefault {} s}") l;
+    mkKeyValue = generators.mkKeyValueDefault {} ":";
+  };
+in {
+  options = {
+    services.moonraker = {
+      enable = mkEnableOption "Moonraker, an API web server for Klipper";
+
+      klipperSocket = mkOption {
+        type = types.path;
+        default = config.services.klipper.apiSocket;
+        defaultText = literalExpression "config.services.klipper.apiSocket";
+        description = "Path to Klipper's API socket.";
+      };
+
+      stateDir = mkOption {
+        type = types.path;
+        default = "/var/lib/moonraker";
+        description = "The directory containing the Moonraker databases.";
+      };
+
+      configDir = mkOption {
+        type = types.path;
+        default = cfg.stateDir + "/config";
+        defaultText = literalExpression ''config.${opt.stateDir} + "/config"'';
+        description = ''
+          The directory containing client-writable configuration files.
+
+          Clients will be able to edit files in this directory via the API. This directory must be writable.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "moonraker";
+        description = "User account under which Moonraker runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "moonraker";
+        description = "Group account under which Moonraker runs.";
+      };
+
+      address = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        example = "0.0.0.0";
+        description = "The IP or host to listen on.";
+      };
+
+      port = mkOption {
+        type = types.ints.unsigned;
+        default = 7125;
+        description = "The port to listen on.";
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = { };
+        example = {
+          authorization = {
+            trusted_clients = [ "10.0.0.0/24" ];
+            cors_domains = [ "https://app.fluidd.xyz" ];
+          };
+        };
+        description = ''
+          Configuration for Moonraker. See the <link xlink:href="https://moonraker.readthedocs.io/en/latest/configuration/">documentation</link>
+          for supported values.
+        '';
+      };
+    };
+  };
+
+  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.'';
+
+    users.users = optionalAttrs (cfg.user == "moonraker") {
+      moonraker = {
+        group = cfg.group;
+        uid = config.ids.uids.moonraker;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "moonraker") {
+      moonraker.gid = config.ids.gids.moonraker;
+    };
+
+    environment.etc."moonraker.cfg".source = let
+      forcedConfig = {
+        server = {
+          host = cfg.address;
+          port = cfg.port;
+          klippy_uds_address = cfg.klipperSocket;
+          config_path = cfg.configDir;
+          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} - -"
+    ];
+
+    systemd.services.moonraker = {
+      description = "Moonraker, an API web server for Klipper";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ]
+        ++ optional config.services.klipper.enable "klipper.service";
+
+      # 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
+      '';
+
+      serviceConfig = {
+        WorkingDirectory = cfg.stateDir;
+        Group = cfg.group;
+        User = cfg.user;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/mx-puppet-discord.nix b/nixos/modules/services/misc/mx-puppet-discord.nix
new file mode 100644
index 00000000000..6214f7f7eb6
--- /dev/null
+++ b/nixos/modules/services/misc/mx-puppet-discord.nix
@@ -0,0 +1,122 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  dataDir = "/var/lib/mx-puppet-discord";
+  registrationFile = "${dataDir}/discord-registration.yaml";
+  cfg = config.services.mx-puppet-discord;
+  settingsFormat = pkgs.formats.json {};
+  settingsFile = settingsFormat.generate "mx-puppet-discord-config.json" cfg.settings;
+
+in {
+  options = {
+    services.mx-puppet-discord = {
+      enable = mkEnableOption ''
+        mx-puppet-discord is a discord puppeting bridge for matrix.
+        It handles bridging private and group DMs, as well as Guilds (servers)
+      '';
+
+      settings = mkOption rec {
+        apply = recursiveUpdate default;
+        inherit (settingsFormat) type;
+        default = {
+          bridge.port = 8434;
+          presence = {
+            enabled = true;
+            interval = 500;
+          };
+          provisioning.whitelist = [ ];
+          relay.whitelist = [ ];
+
+          # variables are preceded by a colon.
+          namePatterns = {
+            user = ":name";
+            userOverride = ":displayname";
+            room = ":name";
+            group = ":name";
+          };
+
+          #defaults to sqlite but can be configured to use postgresql with
+          #connstring
+          database.filename = "${dataDir}/database.db";
+          logging = {
+            console = "info";
+            lineDateFormat = "MMM-D HH:mm:ss.SSS";
+          };
+        };
+        example = literalExpression ''
+          {
+            bridge = {
+              bindAddress = "localhost";
+              domain = "example.com";
+              homeserverUrl = "https://example.com";
+            };
+
+            provisioning.whitelist = [ "@admin:example.com" ];
+            relay.whitelist = [ "@.*:example.com" ];
+          }
+        '';
+        description = ''
+          <filename>config.yaml</filename> configuration as a Nix attribute set.
+          Configuration options should match those described in
+          <link xlink:href="https://github.com/matrix-discord/mx-puppet-discord/blob/master/sample.config.yaml">
+          sample.config.yaml</link>.
+        '';
+      };
+      serviceDependencies = mkOption {
+        type = with types; listOf str;
+        default = optional config.services.matrix-synapse.enable "matrix-synapse.service";
+        defaultText = literalExpression ''
+          optional config.services.matrix-synapse.enable "matrix-synapse.service"
+        '';
+        description = ''
+          List of Systemd services to require and wait for when starting the application service.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.mx-puppet-discord = {
+      description = "Matrix to Discord puppeting bridge";
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
+      after = [ "network-online.target" ] ++ cfg.serviceDependencies;
+
+      preStart = ''
+        # generate the appservice's registration file if absent
+        if [ ! -f '${registrationFile}' ]; then
+          ${pkgs.mx-puppet-discord}/bin/mx-puppet-discord -r -c ${settingsFile} \
+          -f ${registrationFile}
+        fi
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+
+        DynamicUser = true;
+        PrivateTmp = true;
+        WorkingDirectory = pkgs.mx-puppet-discord;
+        StateDirectory = baseNameOf dataDir;
+        UMask = 0027;
+
+        ExecStart = ''
+          ${pkgs.mx-puppet-discord}/bin/mx-puppet-discord \
+            -c ${settingsFile} \
+            -f ${registrationFile}
+        '';
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ govanify ];
+}
diff --git a/nixos/modules/services/misc/n8n.nix b/nixos/modules/services/misc/n8n.nix
new file mode 100644
index 00000000000..77e717eeff9
--- /dev/null
+++ b/nixos/modules/services/misc/n8n.nix
@@ -0,0 +1,79 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.n8n;
+  format = pkgs.formats.json {};
+  configFile = format.generate "n8n.json" cfg.settings;
+in
+{
+  options.services.n8n = {
+
+    enable = mkEnableOption "n8n server";
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Open ports in the firewall for the n8n web interface.";
+    };
+
+    settings = mkOption {
+      type = format.type;
+      default = {};
+      description = ''
+        Configuration for n8n, see <link xlink:href="https://docs.n8n.io/reference/configuration.html"/>
+        for supported values.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    services.n8n.settings = {
+      # We use this to open the firewall, so we need to know about the default at eval time
+      port = lib.mkDefault 5678;
+    };
+
+    systemd.services.n8n = {
+      description = "N8N service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        # This folder must be writeable as the application is storing
+        # its data in it, so the StateDirectory is a good choice
+        N8N_USER_FOLDER = "/var/lib/n8n";
+        HOME = "/var/lib/n8n";
+        N8N_CONFIG_FILES = "${configFile}";
+      };
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${pkgs.n8n}/bin/n8n";
+        Restart = "on-failure";
+        StateDirectory = "n8n";
+
+        # Basic Hardening
+        NoNewPrivileges = "yes";
+        PrivateTmp = "yes";
+        PrivateDevices = "yes";
+        DevicePolicy = "closed";
+        DynamicUser = "true";
+        ProtectSystem = "strict";
+        ProtectHome = "read-only";
+        ProtectControlGroups = "yes";
+        ProtectKernelModules = "yes";
+        ProtectKernelTunables = "yes";
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+        RestrictNamespaces = "yes";
+        RestrictRealtime = "yes";
+        RestrictSUIDSGID = "yes";
+        MemoryDenyWriteExecute = "no"; # v8 JIT requires memory segments to be Writable-Executable.
+        LockPersonality = "yes";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.settings.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/nitter.nix b/nixos/modules/services/misc/nitter.nix
new file mode 100644
index 00000000000..97005c9d914
--- /dev/null
+++ b/nixos/modules/services/misc/nitter.nix
@@ -0,0 +1,358 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nitter;
+  configFile = pkgs.writeText "nitter.conf" ''
+    ${generators.toINI {
+      # String values need to be quoted
+      mkKeyValue = generators.mkKeyValueDefault {
+        mkValueString = v:
+          if isString v then "\"" + (strings.escape ["\""] (toString v)) + "\""
+          else generators.mkValueStringDefault {} v;
+      } " = ";
+    } (lib.recursiveUpdate {
+      Server = cfg.server;
+      Cache = cfg.cache;
+      Config = cfg.config // { hmacKey = "@hmac@"; };
+      Preferences = cfg.preferences;
+    } cfg.settings)}
+  '';
+  # `hmac` is a secret used for cryptographic signing of video URLs.
+  # Generate it on first launch, then copy configuration and replace
+  # `@hmac@` with this value.
+  # We are not using sed as it would leak the value in the command line.
+  preStart = pkgs.writers.writePython3 "nitter-prestart" {} ''
+    import os
+    import secrets
+
+    state_dir = os.environ.get("STATE_DIRECTORY")
+    if not os.path.isfile(f"{state_dir}/hmac"):
+        # Generate hmac on first launch
+        hmac = secrets.token_hex(32)
+        with open(f"{state_dir}/hmac", "w") as f:
+            f.write(hmac)
+    else:
+        # Load previously generated hmac
+        with open(f"{state_dir}/hmac", "r") as f:
+            hmac = f.read()
+
+    configFile = "${configFile}"
+    with open(configFile, "r") as f_in:
+        with open(f"{state_dir}/nitter.conf", "w") as f_out:
+            f_out.write(f_in.read().replace("@hmac@", hmac))
+  '';
+in
+{
+  options = {
+    services.nitter = {
+      enable = mkEnableOption "If enabled, start Nitter.";
+
+      package = mkOption {
+        default = pkgs.nitter;
+        type = types.package;
+        defaultText = literalExpression "pkgs.nitter";
+        description = "The nitter derivation to use.";
+      };
+
+      server = {
+        address = mkOption {
+          type =  types.str;
+          default = "0.0.0.0";
+          example = "127.0.0.1";
+          description = "The address to listen on.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8080;
+          example = 8000;
+          description = "The port to listen on.";
+        };
+
+        https = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Set secure attribute on cookies. Keep it disabled to enable cookies when not using HTTPS.";
+        };
+
+        httpMaxConnections = mkOption {
+          type = types.int;
+          default = 100;
+          description = "Maximum number of HTTP connections.";
+        };
+
+        staticDir = mkOption {
+          type = types.path;
+          default = "${cfg.package}/share/nitter/public";
+          defaultText = literalExpression ''"''${config.services.nitter.package}/share/nitter/public"'';
+          description = "Path to the static files directory.";
+        };
+
+        title = mkOption {
+          type = types.str;
+          default = "nitter";
+          description = "Title of the instance.";
+        };
+
+        hostname = mkOption {
+          type = types.str;
+          default = "localhost";
+          example = "nitter.net";
+          description = "Hostname of the instance.";
+        };
+      };
+
+      cache = {
+        listMinutes = mkOption {
+          type = types.int;
+          default = 240;
+          description = "How long to cache list info (not the tweets, so keep it high).";
+        };
+
+        rssMinutes = mkOption {
+          type = types.int;
+          default = 10;
+          description = "How long to cache RSS queries.";
+        };
+
+        redisHost = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = "Redis host.";
+        };
+
+        redisPort = mkOption {
+          type = types.port;
+          default = 6379;
+          description = "Redis port.";
+        };
+
+        redisConnections = mkOption {
+          type = types.int;
+          default = 20;
+          description = "Redis connection pool size.";
+        };
+
+        redisMaxConnections = mkOption {
+          type = types.int;
+          default = 30;
+          description = ''
+            Maximum number of connections to Redis.
+
+            New connections are opened when none are available, but if the
+            pool size goes above this, they are closed when released, do not
+            worry about this unless you receive tons of requests per second.
+          '';
+        };
+      };
+
+      config = {
+        base64Media = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Use base64 encoding for proxied media URLs.";
+        };
+
+        tokenCount = mkOption {
+          type = types.int;
+          default = 10;
+          description = ''
+            Minimum amount of usable tokens.
+
+            Tokens are used to authorize API requests, but they expire after
+            ~1 hour, and have a limit of 187 requests. The limit gets reset
+            every 15 minutes, and the pool is filled up so there is always at
+            least tokenCount usable tokens. Only increase this if you receive
+            major bursts all the time.
+          '';
+        };
+      };
+
+      preferences = {
+        replaceTwitter = mkOption {
+          type = types.str;
+          default = "";
+          example = "nitter.net";
+          description = "Replace Twitter links with links to this instance (blank to disable).";
+        };
+
+        replaceYouTube = mkOption {
+          type = types.str;
+          default = "";
+          example = "piped.kavin.rocks";
+          description = "Replace YouTube links with links to this instance (blank to disable).";
+        };
+
+        replaceInstagram = mkOption {
+          type = types.str;
+          default = "";
+          description = "Replace Instagram links with links to this instance (blank to disable).";
+        };
+
+        mp4Playback = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Enable MP4 video playback.";
+        };
+
+        hlsPlayback = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Enable HLS video streaming (requires JavaScript).";
+        };
+
+        proxyVideos = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Proxy video streaming through the server (might be slow).";
+        };
+
+        muteVideos = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Mute videos by default.";
+        };
+
+        autoplayGifs = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Autoplay GIFs.";
+        };
+
+        theme = mkOption {
+          type = types.str;
+          default = "Nitter";
+          description = "Instance theme.";
+        };
+
+        infiniteScroll = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Infinite scrolling (requires JavaScript, experimental!).";
+        };
+
+        stickyProfile = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Make profile sidebar stick to top.";
+        };
+
+        bidiSupport = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Support bidirectional text (makes clicking on tweets harder).";
+        };
+
+        hideTweetStats = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Hide tweet stats (replies, retweets, likes).";
+        };
+
+        hideBanner = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Hide profile banner.";
+        };
+
+        hidePins = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Hide pinned tweets.";
+        };
+
+        hideReplies = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Hide tweet replies.";
+        };
+      };
+
+      settings = mkOption {
+        type = types.attrs;
+        default = {};
+        description = ''
+          Add settings here to override NixOS module generated settings.
+
+          Check the official repository for the available settings:
+          https://github.com/zedeus/nitter/blob/master/nitter.conf
+        '';
+      };
+
+      redisCreateLocally = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Configure local Redis server for Nitter.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for Nitter web interface.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !cfg.redisCreateLocally || (cfg.cache.redisHost == "localhost" && cfg.cache.redisPort == 6379);
+        message = "When services.nitter.redisCreateLocally is enabled, you need to use localhost:6379 as a cache server.";
+      }
+    ];
+
+    systemd.services.nitter = {
+        description = "Nitter (An alternative Twitter front-end)";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig = {
+          DynamicUser = true;
+          StateDirectory = "nitter";
+          Environment = [ "NITTER_CONF_FILE=/var/lib/nitter/nitter.conf" ];
+          # Some parts of Nitter expect `public` folder in working directory,
+          # see https://github.com/zedeus/nitter/issues/414
+          WorkingDirectory = "${cfg.package}/share/nitter";
+          ExecStart = "${cfg.package}/bin/nitter";
+          ExecStartPre = "${preStart}";
+          AmbientCapabilities = lib.mkIf (cfg.server.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+          Restart = "on-failure";
+          RestartSec = "5s";
+          # Hardening
+          CapabilityBoundingSet = if (cfg.server.port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ];
+          DeviceAllow = [ "" ];
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          PrivateDevices = true;
+          # A private user cannot have process capabilities on the host's user
+          # namespace and thus CAP_NET_BIND_SERVICE has no effect.
+          PrivateUsers = (cfg.server.port >= 1024);
+          ProcSubset = "pid";
+          ProtectClock = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectProc = "invisible";
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+          UMask = "0077";
+        };
+    };
+
+    services.redis = lib.mkIf (cfg.redisCreateLocally) {
+      enable = true;
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.server.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
new file mode 100644
index 00000000000..2b21df91b82
--- /dev/null
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -0,0 +1,818 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.nix;
+
+  nixPackage = cfg.package.out;
+
+  isNixAtLeast = versionAtLeast (getVersion nixPackage);
+
+  makeNixBuildUser = nr: {
+    name = "nixbld${toString nr}";
+    value = {
+      description = "Nix build user ${toString nr}";
+
+      /*
+        For consistency with the setgid(2), setuid(2), and setgroups(2)
+        calls in `libstore/build.cc', don't add any supplementary group
+        here except "nixbld".
+      */
+      uid = builtins.add config.ids.uids.nixbld nr;
+      isSystemUser = true;
+      group = "nixbld";
+      extraGroups = [ "nixbld" ];
+    };
+  };
+
+  nixbldUsers = listToAttrs (map makeNixBuildUser (range 1 cfg.nrBuildUsers));
+
+  nixConf =
+    assert isNixAtLeast "2.2";
+    let
+
+      mkValueString = v:
+        if v == null then ""
+        else if isInt v then toString v
+        else if isBool v then boolToString v
+        else if isFloat v then floatToString v
+        else if isList v then toString v
+        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 abort "The nix conf value: ${toPretty {} v} can not be encoded";
+
+      mkKeyValue = k: v: "${escape [ "=" ] k} = ${mkValueString v}";
+
+      mkKeyValuePairs = attrs: concatStringsSep "\n" (mapAttrsToList mkKeyValue attrs);
+
+    in
+    pkgs.writeTextFile {
+      name = "nix.conf";
+      text = ''
+        # WARNING: this file is generated from the nix.* options in
+        # your NixOS configuration, typically
+        # /etc/nixos/configuration.nix.  Do not edit it!
+        ${mkKeyValuePairs cfg.settings}
+        ${cfg.extraOptions}
+      '';
+      checkPhase =
+        if pkgs.stdenv.hostPlatform != pkgs.stdenv.buildPlatform then ''
+          echo "Ignoring validation for cross-compilation"
+        ''
+        else ''
+          echo "Validating generated nix.conf"
+          ln -s $out ./nix.conf
+          set -e
+          set +o pipefail
+          NIX_CONF_DIR=$PWD \
+            ${cfg.package}/bin/nix show-config ${optionalString (isNixAtLeast "2.3pre") "--no-net"} \
+              ${optionalString (isNixAtLeast "2.4pre") "--option experimental-features nix-command"} \
+            |& sed -e 's/^warning:/error:/' \
+            | (! grep '${if cfg.checkConfig then "^error:" else "^error: unknown setting"}')
+          set -o pipefail
+        '';
+    };
+
+  legacyConfMappings = {
+    useSandbox = "sandbox";
+    buildCores = "cores";
+    maxJobs = "max-jobs";
+    sandboxPaths = "extra-sandbox-paths";
+    binaryCaches = "substituters";
+    trustedBinaryCaches = "trusted-substituters";
+    binaryCachePublicKeys = "trusted-public-keys";
+    autoOptimiseStore = "auto-optimise-store";
+    requireSignedBinaryCaches = "require-sigs";
+    trustedUsers = "trusted-users";
+    allowedUsers = "allowed-users";
+    systemFeatures = "system-features";
+  };
+
+  semanticConfType = with types;
+    let
+      confAtom = nullOr
+        (oneOf [
+          bool
+          int
+          float
+          str
+          path
+          package
+        ]) // {
+        description = "Nix config atom (null, bool, int, float, str, path or package)";
+      };
+    in
+    attrsOf (either confAtom (listOf confAtom));
+
+in
+
+{
+  imports = [
+    (mkRenamedOptionModule [ "nix" "useChroot" ] [ "nix" "useSandbox" ])
+    (mkRenamedOptionModule [ "nix" "chrootDirs" ] [ "nix" "sandboxPaths" ])
+    (mkRenamedOptionModule [ "nix" "daemonIONiceLevel" ] [ "nix" "daemonIOSchedPriority" ])
+    (mkRemovedOptionModule [ "nix" "daemonNiceLevel" ] "Consider nix.daemonCPUSchedPolicy instead.")
+  ] ++ mapAttrsToList (oldConf: newConf: mkRenamedOptionModule [ "nix" oldConf ] [ "nix" "settings" newConf ]) legacyConfMappings;
+
+  ###### interface
+
+  options = {
+
+    nix = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable Nix.
+          Disabling Nix makes the system hard to modify and the Nix programs and configuration will not be made available by NixOS itself.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.nix;
+        defaultText = literalExpression "pkgs.nix";
+        description = ''
+          This option specifies the Nix package instance to use throughout the system.
+        '';
+      };
+
+      distributedBuilds = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to distribute builds to the machines listed in
+          <option>nix.buildMachines</option>.
+        '';
+      };
+
+      daemonCPUSchedPolicy = mkOption {
+        type = types.enum [ "other" "batch" "idle" ];
+        default = "other";
+        example = "batch";
+        description = ''
+          Nix daemon process CPU scheduling policy. This policy propagates to
+          build processes. <literal>other</literal> is the default scheduling
+          policy for regular tasks. The <literal>batch</literal> policy is
+          similar to <literal>other</literal>, but optimised for
+          non-interactive tasks. <literal>idle</literal> is for extremely
+          low-priority tasks that should only be run when no other task
+          requires CPU time.
+
+          Please note that while using the <literal>idle</literal> policy may
+          greatly improve responsiveness of a system performing expensive
+          builds, it may also slow down and potentially starve crucial
+          configuration updates during load.
+
+          <literal>idle</literal> may therefore be a sensible policy for
+          systems that experience only intermittent phases of high CPU load,
+          such as desktop or portable computers used interactively. Other
+          systems should use the <literal>other</literal> or
+          <literal>batch</literal> policy instead.
+
+          For more fine-grained resource control, please refer to
+          <citerefentry><refentrytitle>systemd.resource-control
+          </refentrytitle><manvolnum>5</manvolnum></citerefentry> and adjust
+          <option>systemd.services.nix-daemon</option> directly.
+      '';
+      };
+
+      daemonIOSchedClass = mkOption {
+        type = types.enum [ "best-effort" "idle" ];
+        default = "best-effort";
+        example = "idle";
+        description = ''
+          Nix daemon process I/O scheduling class. This class propagates to
+          build processes. <literal>best-effort</literal> is the default
+          class for regular tasks. The <literal>idle</literal> class is for
+          extremely low-priority tasks that should only perform I/O when no
+          other task does.
+
+          Please note that while using the <literal>idle</literal> scheduling
+          class can improve responsiveness of a system performing expensive
+          builds, it might also slow down or starve crucial configuration
+          updates during load.
+
+          <literal>idle</literal> may therefore be a sensible class for
+          systems that experience only intermittent phases of high I/O load,
+          such as desktop or portable computers used interactively. Other
+          systems should use the <literal>best-effort</literal> class.
+      '';
+      };
+
+      daemonIOSchedPriority = mkOption {
+        type = types.int;
+        default = 0;
+        example = 1;
+        description = ''
+          Nix daemon process I/O scheduling priority. This priority propagates
+          to build processes. The supported priorities depend on the
+          scheduling policy: With idle, priorities are not used in scheduling
+          decisions. best-effort supports values in the range 0 (high) to 7
+          (low).
+        '';
+      };
+
+      buildMachines = mkOption {
+        type = types.listOf (types.submodule {
+          options = {
+            hostName = mkOption {
+              type = types.str;
+              example = "nixbuilder.example.org";
+              description = ''
+                The hostname of the build machine.
+              '';
+            };
+            system = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "x86_64-linux";
+              description = ''
+                The system type the build machine can execute derivations on.
+                Either this attribute or <varname>systems</varname> must be
+                present, where <varname>system</varname> takes precedence if
+                both are set.
+              '';
+            };
+            systems = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              example = [ "x86_64-linux" "aarch64-linux" ];
+              description = ''
+                The system types the build machine can execute derivations on.
+                Either this attribute or <varname>system</varname> must be
+                present, where <varname>system</varname> takes precedence if
+                both are set.
+              '';
+            };
+            sshUser = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "builder";
+              description = ''
+                The username to log in as on the remote host. This user must be
+                able to log in and run nix commands non-interactively. It must
+                also be privileged to build derivations, so must be included in
+                <option>nix.settings.trusted-users</option>.
+              '';
+            };
+            sshKey = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "/root/.ssh/id_buildhost_builduser";
+              description = ''
+                The path to the SSH private key with which to authenticate on
+                the build machine. The private key must not have a passphrase.
+                If null, the building user (root on NixOS machines) must have an
+                appropriate ssh configuration to log in non-interactively.
+
+                Note that for security reasons, this path must point to a file
+                in the local filesystem, *not* to the nix store.
+              '';
+            };
+            maxJobs = mkOption {
+              type = types.int;
+              default = 1;
+              description = ''
+                The number of concurrent jobs the build machine supports. The
+                build machine will enforce its own limits, but this allows hydra
+                to schedule better since there is no work-stealing between build
+                machines.
+              '';
+            };
+            speedFactor = mkOption {
+              type = types.int;
+              default = 1;
+              description = ''
+                The relative speed of this builder. This is an arbitrary integer
+                that indicates the speed of this builder, relative to other
+                builders. Higher is faster.
+              '';
+            };
+            mandatoryFeatures = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              example = [ "big-parallel" ];
+              description = ''
+                A list of features mandatory for this builder. The builder will
+                be ignored for derivations that don't require all features in
+                this list. All mandatory features are automatically included in
+                <varname>supportedFeatures</varname>.
+              '';
+            };
+            supportedFeatures = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              example = [ "kvm" "big-parallel" ];
+              description = ''
+                A list of features supported by this builder. The builder will
+                be ignored for derivations that require features not in this
+                list.
+              '';
+            };
+            publicHostKey = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = ''
+                The (base64-encoded) public host key of this builder. The field
+                is calculated via <command>base64 -w0 /etc/ssh/ssh_host_type_key.pub</command>.
+                If null, SSH will use its regular known-hosts file when connecting.
+              '';
+            };
+          };
+        });
+        default = [ ];
+        description = ''
+          This option lists the machines to be used if distributed builds are
+          enabled (see <option>nix.distributedBuilds</option>).
+          Nix will perform derivations on those machines via SSH by copying the
+          inputs to the Nix store on the remote machine, starting the build,
+          then copying the output back to the local Nix store.
+        '';
+      };
+
+      # Environment variables for running Nix.
+      envVars = mkOption {
+        type = types.attrs;
+        internal = true;
+        default = { };
+        description = "Environment variables used by Nix.";
+      };
+
+      nrBuildUsers = mkOption {
+        type = types.int;
+        description = ''
+          Number of <literal>nixbld</literal> user accounts created to
+          perform secure concurrent builds.  If you receive an error
+          message saying that “all build users are currently in useâ€,
+          you should increase this value.
+        '';
+      };
+
+      readOnlyStore = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          If set, NixOS will enforce the immutability of the Nix store
+          by making <filename>/nix/store</filename> a read-only bind
+          mount.  Nix will automatically make the store writable when
+          needed.
+        '';
+      };
+
+      nixPath = mkOption {
+        type = types.listOf types.str;
+        default = [
+          "nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos"
+          "nixos-config=/etc/nixos/configuration.nix"
+          "/nix/var/nix/profiles/per-user/root/channels"
+        ];
+        description = ''
+          The default Nix expression search path, used by the Nix
+          evaluator to look up paths enclosed in angle brackets
+          (e.g. <literal>&lt;nixpkgs&gt;</literal>).
+        '';
+      };
+
+      checkConfig = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          If enabled (the default), checks for data type mismatches and that Nix
+          can parse the generated nix.conf.
+        '';
+      };
+
+      registry = mkOption {
+        type = types.attrsOf (types.submodule (
+          let
+            referenceAttrs = with types; attrsOf (oneOf [
+              str
+              int
+              bool
+              package
+            ]);
+          in
+          { config, name, ... }:
+          {
+            options = {
+              from = mkOption {
+                type = referenceAttrs;
+                example = { type = "indirect"; id = "nixpkgs"; };
+                description = "The flake reference to be rewritten.";
+              };
+              to = mkOption {
+                type = referenceAttrs;
+                example = { type = "github"; owner = "my-org"; repo = "my-nixpkgs"; };
+                description = "The flake reference <option>from></option> is rewritten to.";
+              };
+              flake = mkOption {
+                type = types.nullOr types.attrs;
+                default = null;
+                example = literalExpression "nixpkgs";
+                description = ''
+                  The flake input <option>from></option> is rewritten to.
+                '';
+              };
+              exact = mkOption {
+                type = types.bool;
+                default = true;
+                description = ''
+                  Whether the <option>from</option> reference needs to match exactly. If set,
+                  a <option>from</option> reference like <literal>nixpkgs</literal> does not
+                  match with a reference like <literal>nixpkgs/nixos-20.03</literal>.
+                '';
+              };
+            };
+            config = {
+              from = mkDefault { type = "indirect"; id = name; };
+              to = mkIf (config.flake != null) (mkDefault
+                {
+                  type = "path";
+                  path = config.flake.outPath;
+                } // filterAttrs
+                (n: _: n == "lastModified" || n == "rev" || n == "revCount" || n == "narHash")
+                config.flake);
+            };
+          }
+        ));
+        default = { };
+        description = ''
+          A system-wide flake registry.
+        '';
+      };
+
+      extraOptions = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          keep-outputs = true
+          keep-derivations = true
+        '';
+        description = "Additional text appended to <filename>nix.conf</filename>.";
+      };
+
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = semanticConfType;
+
+          options = {
+            max-jobs = mkOption {
+              type = types.either types.int (types.enum [ "auto" ]);
+              default = "auto";
+              example = 64;
+              description = ''
+                This option defines the maximum number of jobs that Nix will try to
+                build in parallel. The default is auto, which means it will use all
+                available logical cores. It is recommend to set it to the total
+                number of logical cores in your system (e.g., 16 for two CPUs with 4
+                cores each and hyper-threading).
+              '';
+            };
+
+            auto-optimise-store = mkOption {
+              type = types.bool;
+              default = false;
+              example = true;
+              description = ''
+                If set to true, Nix automatically detects files in the store that have
+                identical contents, and replaces them with hard links to a single copy.
+                This saves disk space. If set to false (the default), you can still run
+                nix-store --optimise to get rid of duplicate files.
+              '';
+            };
+
+            cores = mkOption {
+              type = types.int;
+              default = 0;
+              example = 64;
+              description = ''
+                This option defines the maximum number of concurrent tasks during
+                one build. It affects, e.g., -j option for make.
+                The special value 0 means that the builder should use all
+                available CPU cores in the system. Some builds may become
+                non-deterministic with this option; use with care! Packages will
+                only be affected if enableParallelBuilding is set for them.
+              '';
+            };
+
+            sandbox = mkOption {
+              type = types.either types.bool (types.enum [ "relaxed" ]);
+              default = true;
+              description = ''
+                If set, Nix will perform builds in a sandboxed environment that it
+                will set up automatically for each build. This prevents impurities
+                in builds by disallowing access to dependencies outside of the Nix
+                store by using network and mount namespaces in a chroot environment.
+                This is enabled by default even though it has a possible performance
+                impact due to the initial setup time of a sandbox for each build. It
+                doesn't affect derivation hashes, so changing this option will not
+                trigger a rebuild of packages.
+              '';
+            };
+
+            extra-sandbox-paths = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              example = [ "/dev" "/proc" ];
+              description = ''
+                Directories from the host filesystem to be included
+                in the sandbox.
+              '';
+            };
+
+            substituters = mkOption {
+              type = types.listOf types.str;
+              description = ''
+                List of binary cache URLs used to obtain pre-built binaries
+                of Nix packages.
+
+                By default https://cache.nixos.org/ is added.
+              '';
+            };
+
+            trusted-substituters = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              example = [ "https://hydra.nixos.org/" ];
+              description = ''
+                List of binary cache URLs that non-root users can use (in
+                addition to those specified using
+                <option>nix.settings.substituters</option>) by passing
+                <literal>--option binary-caches</literal> to Nix commands.
+              '';
+            };
+
+            require-sigs = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                If enabled (the default), Nix will only download binaries from binary caches if
+                they are cryptographically signed with any of the keys listed in
+                <option>nix.settings.trusted-public-keys</option>. If disabled, signatures are neither
+                required nor checked, so it's strongly recommended that you use only
+                trustworthy caches and https to prevent man-in-the-middle attacks.
+              '';
+            };
+
+            trusted-public-keys = mkOption {
+              type = types.listOf types.str;
+              example = [ "hydra.nixos.org-1:CNHJZBh9K4tP3EKF6FkkgeVYsS3ohTl+oS0Qa8bezVs=" ];
+              description = ''
+                List of public keys used to sign binary caches. If
+                <option>nix.settings.trusted-public-keys</option> is enabled,
+                then Nix will use a binary from a binary cache if and only
+                if it is signed by <emphasis>any</emphasis> of the keys
+                listed here. By default, only the key for
+                <uri>cache.nixos.org</uri> is included.
+              '';
+            };
+
+            trusted-users = mkOption {
+              type = types.listOf types.str;
+              default = [ "root" ];
+              example = [ "root" "alice" "@wheel" ];
+              description = ''
+                A list of names of users that have additional rights when
+                connecting to the Nix daemon, such as the ability to specify
+                additional binary caches, or to import unsigned NARs. You
+                can also specify groups by prefixing them with
+                <literal>@</literal>; for instance,
+                <literal>@wheel</literal> means all users in the wheel
+                group.
+              '';
+            };
+
+            system-features = mkOption {
+              type = types.listOf types.str;
+              example = [ "kvm" "big-parallel" "gccarch-skylake" ];
+              description = ''
+                The set of features supported by the machine. Derivations
+                can express dependencies on system features through the
+                <literal>requiredSystemFeatures</literal> attribute.
+
+                By default, pseudo-features <literal>nixos-test</literal>, <literal>benchmark</literal>,
+                and <literal>big-parallel</literal> used in Nixpkgs are set, <literal>kvm</literal>
+                is also included in it is avaliable.
+              '';
+            };
+
+            allowed-users = mkOption {
+              type = types.listOf types.str;
+              default = [ "*" ];
+              example = [ "@wheel" "@builders" "alice" "bob" ];
+              description = ''
+                A list of names of users (separated by whitespace) that are
+                allowed to connect to the Nix daemon. As with
+                <option>nix.settings.trusted-users</option>, you can specify groups by
+                prefixing them with <literal>@</literal>. Also, you can
+                allow all users by specifying <literal>*</literal>. The
+                default is <literal>*</literal>. Note that trusted users are
+                always allowed to connect.
+              '';
+            };
+          };
+        };
+        default = { };
+        example = literalExpression ''
+          {
+            use-sandbox = true;
+            show-trace = true;
+
+            system-features = [ "big-parallel" "kvm" "recursive-nix" ];
+            sandbox-paths = { "/bin/sh" = "''${pkgs.busybox-sandbox-shell.out}/bin/busybox"; };
+          }
+        '';
+        description = ''
+          Configuration for Nix, see
+          <link xlink:href="https://nixos.org/manual/nix/stable/#sec-conf-file"/> or
+          <citerefentry>
+            <refentrytitle>nix.conf</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry> for avalaible options.
+          The value declared here will be translated directly to the key-value pairs Nix expects.
+          </para>
+          <para>
+          You can use <command>nix-instantiate --eval --strict '&lt;nixpkgs/nixos&gt;' -A config.nix.settings</command>
+          to view the current value. By default it is empty.
+          </para>
+          <para>
+          Nix configurations defined under <option>nix.*</option> will be translated and applied to this
+          option. In addition, configuration specified in <option>nix.extraOptions</option> which will be appended
+          verbatim to the resulting config file.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages =
+      [
+        nixPackage
+        pkgs.nix-info
+      ]
+      ++ optional (config.programs.bash.enableCompletion) pkgs.nix-bash-completions;
+
+    environment.etc."nix/nix.conf".source = nixConf;
+
+    environment.etc."nix/registry.json".text = builtins.toJSON {
+      version = 2;
+      flakes = mapAttrsToList (n: v: { inherit (v) from to exact; }) cfg.registry;
+    };
+
+    # List of machines for distributed Nix builds in the format
+    # expected by build-remote.pl.
+    environment.etc."nix/machines" = mkIf (cfg.buildMachines != [ ]) {
+      text =
+        concatMapStrings
+          (machine:
+            (concatStringsSep " " ([
+              "${optionalString (machine.sshUser != null) "${machine.sshUser}@"}${machine.hostName}"
+              (if machine.system != null then machine.system else if machine.systems != [ ] then concatStringsSep "," machine.systems else "-")
+              (if machine.sshKey != null then machine.sshKey else "-")
+              (toString machine.maxJobs)
+              (toString machine.speedFactor)
+              (concatStringsSep "," (machine.supportedFeatures ++ machine.mandatoryFeatures))
+              (concatStringsSep "," machine.mandatoryFeatures)
+            ]
+            ++ optional (isNixAtLeast "2.4pre") (if machine.publicHostKey != null then machine.publicHostKey else "-")))
+            + "\n"
+          )
+          cfg.buildMachines;
+    };
+
+    assertions =
+      let badMachine = m: m.system == null && m.systems == [ ];
+      in
+      [
+        {
+          assertion = !(any badMachine cfg.buildMachines);
+          message = ''
+            At least one system type (via <varname>system</varname> or
+              <varname>systems</varname>) must be set for every build machine.
+              Invalid machine specifications:
+          '' + "      " +
+          (concatStringsSep "\n      "
+            (map (m: m.hostName)
+              (filter (badMachine) cfg.buildMachines)));
+        }
+      ];
+
+    systemd.packages = [ nixPackage ];
+
+    systemd.sockets.nix-daemon.wantedBy = [ "sockets.target" ];
+
+    systemd.services.nix-daemon =
+      {
+        path = [ nixPackage pkgs.util-linux config.programs.ssh.package ]
+          ++ optionals cfg.distributedBuilds [ pkgs.gzip ];
+
+        environment = cfg.envVars
+          // { CURL_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"; }
+          // config.networking.proxy.envVars;
+
+        unitConfig.RequiresMountsFor = "/nix/store";
+
+        serviceConfig =
+          {
+            CPUSchedulingPolicy = cfg.daemonCPUSchedPolicy;
+            IOSchedulingClass = cfg.daemonIOSchedClass;
+            IOSchedulingPriority = cfg.daemonIOSchedPriority;
+            LimitNOFILE = 4096;
+          };
+
+        restartTriggers = [ nixConf ];
+
+        # `stopIfChanged = false` changes to switch behavior
+        # from   stop -> update units -> start
+        #   to   update units -> restart
+        #
+        # The `stopIfChanged` setting therefore controls a trade-off between a
+        # more predictable lifecycle, which runs the correct "version" of
+        # the `ExecStop` line, and on the other hand the availability of
+        # sockets during the switch, as the effectiveness of the stop operation
+        # depends on the socket being stopped as well.
+        #
+        # As `nix-daemon.service` does not make use of `ExecStop`, we prefer
+        # to keep the socket up and available. This is important for machines
+        # that run Nix-based services, such as automated build, test, and deploy
+        # services, that expect the daemon socket to be available at all times.
+        #
+        # Notably, the Nix client does not retry on failure to connect to the
+        # daemon socket, and the in-process RemoteStore instance will disable
+        # itself. This makes retries infeasible even for services that are
+        # aware of the issue. Failure to connect can affect not only new client
+        # processes, but also new RemoteStore instances in existing processes,
+        # as well as existing RemoteStore instances that have not saturated
+        # their connection pool.
+        #
+        # Also note that `stopIfChanged = true` does not kill existing
+        # connection handling daemons, as one might wish to happen before a
+        # breaking Nix upgrade (which is rare). The daemon forks that handle
+        # the individual connections split off into their own sessions, causing
+        # them not to be stopped by systemd.
+        # If a Nix upgrade does require all existing daemon processes to stop,
+        # nix-daemon must do so on its own accord, and only when the new version
+        # starts and detects that Nix's persistent state needs an upgrade.
+        stopIfChanged = false;
+
+      };
+
+    # Set up the environment variables for running Nix.
+    environment.sessionVariables = cfg.envVars // { NIX_PATH = cfg.nixPath; };
+
+    environment.extraInit =
+      ''
+        if [ -e "$HOME/.nix-defexpr/channels" ]; then
+          export NIX_PATH="$HOME/.nix-defexpr/channels''${NIX_PATH:+:$NIX_PATH}"
+        fi
+      '';
+
+    nix.nrBuildUsers = mkDefault (max 32 (if cfg.settings.max-jobs == "auto" then 0 else cfg.settings.max-jobs));
+
+    users.users = nixbldUsers;
+
+    services.xserver.displayManager.hiddenUsers = attrNames nixbldUsers;
+
+    system.activationScripts.nix = stringAfter [ "etc" "users" ]
+      ''
+        install -m 0755 -d /nix/var/nix/{gcroots,profiles}/per-user
+
+        # Subscribe the root user to the NixOS channel by default.
+        if [ ! -e "/root/.nix-channels" ]; then
+            echo "${config.system.defaultChannel} nixos" > "/root/.nix-channels"
+        fi
+      '';
+
+    # Legacy configuration conversion.
+    nix.settings = mkMerge [
+      {
+        trusted-public-keys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ];
+        substituters = mkAfter [ "https://cache.nixos.org/" ];
+
+        system-features = mkDefault (
+          [ "nixos-test" "benchmark" "big-parallel" "kvm" ] ++
+          optionals (pkgs.hostPlatform ? gcc.arch) (
+            # a builder can run code for `gcc.arch` and inferior architectures
+            [ "gccarch-${pkgs.hostPlatform.gcc.arch}" ] ++
+            map (x: "gccarch-${x}") systems.architectures.inferiors.${pkgs.hostPlatform.gcc.arch}
+          )
+        );
+      }
+
+      (mkIf (!cfg.distributedBuilds) { builders = null; })
+
+      (mkIf (isNixAtLeast "2.3pre") { sandbox-fallback = false; })
+    ];
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/nix-gc.nix b/nixos/modules/services/misc/nix-gc.nix
new file mode 100644
index 00000000000..a7a6a3b5964
--- /dev/null
+++ b/nixos/modules/services/misc/nix-gc.nix
@@ -0,0 +1,100 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.nix.gc;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    nix.gc = {
+
+      automatic = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Automatically run the garbage collector at a specific time.";
+      };
+
+      dates = mkOption {
+        type = types.str;
+        default = "03:15";
+        example = "weekly";
+        description = ''
+          How often or when garbage collection is performed. For most desktop and server systems
+          a sufficient garbage collection is once a week.
+
+          The format is described in
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+
+      randomizedDelaySec = mkOption {
+        default = "0";
+        type = types.str;
+        example = "45min";
+        description = ''
+          Add a randomized delay before each automatic upgrade.
+          The delay will be chosen between zero and this value.
+          This value must be a time span in the format specified by
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>
+        '';
+      };
+
+      persistent = mkOption {
+        default = true;
+        type = types.bool;
+        example = false;
+        description = ''
+          Takes a boolean argument. If true, the time when the service
+          unit was last triggered is stored on disk. When the timer is
+          activated, the service unit is triggered immediately if it
+          would have been triggered at least once during the time when
+          the timer was inactive. Such triggering is nonetheless
+          subject to the delay imposed by RandomizedDelaySec=. This is
+          useful to catch up on missed runs of the service when the
+          system was powered down.
+        '';
+      };
+
+      options = mkOption {
+        default = "";
+        example = "--max-freed $((64 * 1024**3))";
+        type = types.str;
+        description = ''
+          Options given to <filename>nix-collect-garbage</filename> when the
+          garbage collector is run automatically.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = {
+
+    systemd.services.nix-gc = {
+      description = "Nix Garbage Collector";
+      script = "exec ${config.nix.package.out}/bin/nix-collect-garbage ${cfg.options}";
+      startAt = optional cfg.automatic cfg.dates;
+    };
+
+    systemd.timers.nix-gc = lib.mkIf cfg.automatic {
+      timerConfig = {
+        RandomizedDelaySec = cfg.randomizedDelaySec;
+        Persistent = cfg.persistent;
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/nix-optimise.nix b/nixos/modules/services/misc/nix-optimise.nix
new file mode 100644
index 00000000000..e02026d5f76
--- /dev/null
+++ b/nixos/modules/services/misc/nix-optimise.nix
@@ -0,0 +1,51 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.nix.optimise;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    nix.optimise = {
+
+      automatic = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Automatically run the nix store optimiser at a specific time.";
+      };
+
+      dates = mkOption {
+        default = ["03:45"];
+        type = types.listOf types.str;
+        description = ''
+          Specification (in the format described by
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>) of the time at
+          which the optimiser will run.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = {
+
+    systemd.services.nix-optimise =
+      { description = "Nix Store Optimiser";
+        # No point this if the nix daemon (and thus the nix store) is outside
+        unitConfig.ConditionPathIsReadWrite = "/nix/var/nix/daemon-socket";
+        serviceConfig.ExecStart = "${config.nix.package}/bin/nix-store --optimise";
+        startAt = optionals cfg.automatic cfg.dates;
+      };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/nix-ssh-serve.nix b/nixos/modules/services/misc/nix-ssh-serve.nix
new file mode 100644
index 00000000000..355fad5db46
--- /dev/null
+++ b/nixos/modules/services/misc/nix-ssh-serve.nix
@@ -0,0 +1,69 @@
+{ config, lib, ... }:
+
+with lib;
+let cfg = config.nix.sshServe;
+    command =
+      if cfg.protocol == "ssh"
+        then "nix-store --serve ${lib.optionalString cfg.write "--write"}"
+      else "nix-daemon --stdio";
+in {
+  options = {
+
+    nix.sshServe = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable serving the Nix store as a remote store via SSH.";
+      };
+
+      write = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable writing to the Nix store as a remote store via SSH. Note: the sshServe user is named nix-ssh and is not a trusted-user. nix-ssh should be added to the <option>nix.settings.trusted-users</option> option in most use cases, such as allowing remote building of derivations.";
+      };
+
+      keys = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "ssh-dss AAAAB3NzaC1k... alice@example.org" ];
+        description = "A list of SSH public keys allowed to access the binary cache via SSH.";
+      };
+
+      protocol = mkOption {
+        type = types.enum [ "ssh" "ssh-ng" ];
+        default = "ssh";
+        description = "The specific Nix-over-SSH protocol to use.";
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users.nix-ssh = {
+      description = "Nix SSH store user";
+      isSystemUser = true;
+      group = "nix-ssh";
+      useDefaultShell = true;
+    };
+    users.groups.nix-ssh = {};
+
+    services.openssh.enable = true;
+
+    services.openssh.extraConfig = ''
+      Match User nix-ssh
+        AllowAgentForwarding no
+        AllowTcpForwarding no
+        PermitTTY no
+        PermitTunnel no
+        X11Forwarding no
+        ForceCommand ${config.nix.package.out}/bin/${command}
+      Match All
+    '';
+
+    users.users.nix-ssh.openssh.authorizedKeys.keys = cfg.keys;
+
+  };
+}
diff --git a/nixos/modules/services/misc/novacomd.nix b/nixos/modules/services/misc/novacomd.nix
new file mode 100644
index 00000000000..7cfc68d2b67
--- /dev/null
+++ b/nixos/modules/services/misc/novacomd.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.novacomd;
+
+in {
+
+  options = {
+    services.novacomd = {
+      enable = mkEnableOption "Novacom service for connecting to WebOS devices";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.webos.novacom ];
+
+    systemd.services.novacomd = {
+      description = "Novacom WebOS daemon";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.webos.novacomd}/sbin/novacomd";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ dtzWill ];
+}
diff --git a/nixos/modules/services/misc/nzbget.nix b/nixos/modules/services/misc/nzbget.nix
new file mode 100644
index 00000000000..27c5f2e395f
--- /dev/null
+++ b/nixos/modules/services/misc/nzbget.nix
@@ -0,0 +1,117 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nzbget;
+  pkg = pkgs.nzbget;
+  stateDir = "/var/lib/nzbget";
+  configFile = "${stateDir}/nzbget.conf";
+  configOpts = concatStringsSep " " (mapAttrsToList (name: value: "-o ${name}=${escapeShellArg (toStr value)}") cfg.settings);
+  toStr = v:
+    if v == true then "yes"
+    else if v == false then "no"
+    else if isInt v then toString v
+    else v;
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "misc" "nzbget" "configFile" ] "The configuration of nzbget is now managed by users through the web interface.")
+    (mkRemovedOptionModule [ "services" "misc" "nzbget" "dataDir" ] "The data directory for nzbget is now /var/lib/nzbget.")
+    (mkRemovedOptionModule [ "services" "misc" "nzbget" "openFirewall" ] "The port used by nzbget is managed through the web interface so you should adjust your firewall rules accordingly.")
+  ];
+
+  # interface
+
+  options = {
+    services.nzbget = {
+      enable = mkEnableOption "NZBGet";
+
+      user = mkOption {
+        type = types.str;
+        default = "nzbget";
+        description = "User account under which NZBGet runs";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "nzbget";
+        description = "Group under which NZBGet runs";
+      };
+
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ bool int str ]);
+        default = {};
+        description = ''
+          NZBGet configuration, passed via command line using switch -o. Refer to
+          <link xlink:href="https://github.com/nzbget/nzbget/blob/master/nzbget.conf"/>
+          for details on supported values.
+        '';
+        example = {
+          MainDir = "/data";
+        };
+      };
+    };
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+    services.nzbget.settings = {
+      # allows nzbget to run as a "simple" service
+      OutputMode = "loggable";
+      # use journald for logging
+      WriteLog = "none";
+      ErrorTarget = "screen";
+      WarningTarget = "screen";
+      InfoTarget = "screen";
+      DetailTarget = "screen";
+      # required paths
+      ConfigTemplate = "${pkg}/share/nzbget/nzbget.conf";
+      WebDir = "${pkg}/share/nzbget/webui";
+      # nixos handles package updates
+      UpdateCheck = "none";
+    };
+
+    systemd.services.nzbget = {
+      description = "NZBGet Daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = with pkgs; [
+        unrar
+        p7zip
+      ];
+
+      preStart = ''
+        if [ ! -f ${configFile} ]; then
+          ${pkgs.coreutils}/bin/install -m 0700 ${pkg}/share/nzbget/nzbget.conf ${configFile}
+        fi
+      '';
+
+      serviceConfig = {
+        StateDirectory = "nzbget";
+        StateDirectoryMode = "0750";
+        User = cfg.user;
+        Group = cfg.group;
+        UMask = "0002";
+        Restart = "on-failure";
+        ExecStart = "${pkg}/bin/nzbget --server --configfile ${stateDir}/nzbget.conf ${configOpts}";
+        ExecStop = "${pkg}/bin/nzbget --quit";
+      };
+    };
+
+    users.users = mkIf (cfg.user == "nzbget") {
+      nzbget = {
+        home = stateDir;
+        group = cfg.group;
+        uid = config.ids.uids.nzbget;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "nzbget") {
+      nzbget = {
+        gid = config.ids.gids.nzbget;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/nzbhydra2.nix b/nixos/modules/services/misc/nzbhydra2.nix
new file mode 100644
index 00000000000..500c40f117d
--- /dev/null
+++ b/nixos/modules/services/misc/nzbhydra2.nix
@@ -0,0 +1,78 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let cfg = config.services.nzbhydra2;
+
+in {
+  options = {
+    services.nzbhydra2 = {
+      enable = mkEnableOption "NZBHydra2";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/nzbhydra2";
+        description = "The directory where NZBHydra2 stores its data files.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          "Open ports in the firewall for the NZBHydra2 web interface.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.nzbhydra2;
+        defaultText = literalExpression "pkgs.nzbhydra2";
+        description = "NZBHydra2 package to use.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules =
+      [ "d '${cfg.dataDir}' 0700 nzbhydra2 nzbhydra2 - -" ];
+
+    systemd.services.nzbhydra2 = {
+      description = "NZBHydra2";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = "nzbhydra2";
+        Group = "nzbhydra2";
+        ExecStart =
+          "${cfg.package}/bin/nzbhydra2 --nobrowser --datafolder '${cfg.dataDir}'";
+        Restart = "on-failure";
+        # Hardening
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        DevicePolicy = "closed";
+        ProtectSystem = "strict";
+        ReadWritePaths = cfg.dataDir;
+        ProtectHome = "read-only";
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies ="AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        LockPersonality = true;
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall { allowedTCPPorts = [ 5076 ]; };
+
+    users.users.nzbhydra2 = {
+      group = "nzbhydra2";
+      isSystemUser = true;
+    };
+
+    users.groups.nzbhydra2 = {};
+  };
+}
diff --git a/nixos/modules/services/misc/octoprint.nix b/nixos/modules/services/misc/octoprint.nix
new file mode 100644
index 00000000000..cd846d3f268
--- /dev/null
+++ b/nixos/modules/services/misc/octoprint.nix
@@ -0,0 +1,133 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.octoprint;
+
+  baseConfig = {
+    plugins.curalegacy.cura_engine = "${pkgs.curaengine_stable}/bin/CuraEngine";
+    server.host = cfg.host;
+    server.port = cfg.port;
+    webcam.ffmpeg = "${pkgs.ffmpeg.bin}/bin/ffmpeg";
+  };
+
+  fullConfig = recursiveUpdate cfg.extraConfig baseConfig;
+
+  cfgUpdate = pkgs.writeText "octoprint-config.yaml" (builtins.toJSON fullConfig);
+
+  pluginsEnv = package.python.withPackages (ps: [ps.octoprint] ++ (cfg.plugins ps));
+
+  package = pkgs.octoprint;
+
+in
+{
+  ##### interface
+
+  options = {
+
+    services.octoprint = {
+
+      enable = mkEnableOption "OctoPrint, web interface for 3D printers";
+
+      host = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = ''
+          Host to bind OctoPrint to.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 5000;
+        description = ''
+          Port to bind OctoPrint to.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "octoprint";
+        description = "User for the daemon.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "octoprint";
+        description = "Group for the daemon.";
+      };
+
+      stateDir = mkOption {
+        type = types.path;
+        default = "/var/lib/octoprint";
+        description = "State directory of the daemon.";
+      };
+
+      plugins = mkOption {
+        type = types.functionTo (types.listOf types.package);
+        default = plugins: [];
+        defaultText = literalExpression "plugins: []";
+        example = literalExpression "plugins: with plugins; [ themeify stlviewer ]";
+        description = "Additional plugins to be used. Available plugins are passed through the plugins input.";
+      };
+
+      extraConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        description = "Extra options which are added to OctoPrint's YAML configuration file.";
+      };
+
+    };
+
+  };
+
+  ##### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users = optionalAttrs (cfg.user == "octoprint") {
+      octoprint = {
+        group = cfg.group;
+        uid = config.ids.uids.octoprint;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "octoprint") {
+      octoprint.gid = config.ids.gids.octoprint;
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.octoprint = {
+      description = "OctoPrint, web interface for 3D printers";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = [ pluginsEnv ];
+
+      preStart = ''
+        if [ -e "${cfg.stateDir}/config.yaml" ]; then
+          ${pkgs.yaml-merge}/bin/yaml-merge "${cfg.stateDir}/config.yaml" "${cfgUpdate}" > "${cfg.stateDir}/config.yaml.tmp"
+          mv "${cfg.stateDir}/config.yaml.tmp" "${cfg.stateDir}/config.yaml"
+        else
+          cp "${cfgUpdate}" "${cfg.stateDir}/config.yaml"
+          chmod 600 "${cfg.stateDir}/config.yaml"
+        fi
+      '';
+
+      serviceConfig = {
+        ExecStart = "${pluginsEnv}/bin/octoprint serve -b ${cfg.stateDir}";
+        User = cfg.user;
+        Group = cfg.group;
+        SupplementaryGroups = [
+          "dialout"
+        ];
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/ombi.nix b/nixos/modules/services/misc/ombi.nix
new file mode 100644
index 00000000000..b5882168e51
--- /dev/null
+++ b/nixos/modules/services/misc/ombi.nix
@@ -0,0 +1,81 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let cfg = config.services.ombi;
+
+in {
+  options = {
+    services.ombi = {
+      enable = mkEnableOption ''
+        Ombi.
+        Optionally see <link xlink:href="https://docs.ombi.app/info/reverse-proxy"/>
+        on how to set up a reverse proxy
+      '';
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/ombi";
+        description = "The directory where Ombi stores its data files.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 5000;
+        description = "The port for the Ombi web interface.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the Ombi web interface.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "ombi";
+        description = "User account under which Ombi runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "ombi";
+        description = "Group under which Ombi runs.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.ombi = {
+      description = "Ombi";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.ombi}/bin/Ombi --storage '${cfg.dataDir}' --host 'http://*:${toString cfg.port}'";
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+
+    users.users = mkIf (cfg.user == "ombi") {
+      ombi = {
+        isSystemUser = true;
+        group = cfg.group;
+        home = cfg.dataDir;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "ombi") { ombi = { }; };
+  };
+}
diff --git a/nixos/modules/services/misc/osrm.nix b/nixos/modules/services/misc/osrm.nix
new file mode 100644
index 00000000000..79c347ab7e0
--- /dev/null
+++ b/nixos/modules/services/misc/osrm.nix
@@ -0,0 +1,86 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.osrm;
+in
+
+{
+  options.services.osrm = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable the OSRM service.";
+    };
+
+    address = mkOption {
+      type = types.str;
+      default = "0.0.0.0";
+      description = "IP address on which the web server will listen.";
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = 5000;
+      description = "Port on which the web server will run.";
+    };
+
+    threads = mkOption {
+      type = types.int;
+      default = 4;
+      description = "Number of threads to use.";
+    };
+
+    algorithm = mkOption {
+      type = types.enum [ "CH" "CoreCH" "MLD" ];
+      default = "MLD";
+      description = "Algorithm to use for the data. Must be one of CH, CoreCH, MLD";
+    };
+
+    extraFlags = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "--max-table-size 1000" "--max-matching-size 1000" ];
+      description = "Extra command line arguments passed to osrm-routed";
+    };
+
+    dataFile = mkOption {
+      type = types.path;
+      example = "/var/lib/osrm/berlin-latest.osrm";
+      description = "Data file location";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users.osrm = {
+      group = config.users.users.osrm.name;
+      description = "OSRM user";
+      createHome = false;
+      isSystemUser = true;
+    };
+
+    users.groups.osrm = { };
+
+    systemd.services.osrm = {
+      description = "OSRM service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        User = config.users.users.osrm.name;
+        ExecStart = ''
+          ${pkgs.osrm-backend}/bin/osrm-routed \
+            --ip ${cfg.address} \
+            --port ${toString cfg.port} \
+            --threads ${toString cfg.threads} \
+            --algorithm ${cfg.algorithm} \
+            ${toString cfg.extraFlags} \
+            ${cfg.dataFile}
+        '';
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/owncast.nix b/nixos/modules/services/misc/owncast.nix
new file mode 100644
index 00000000000..0852335238f
--- /dev/null
+++ b/nixos/modules/services/misc/owncast.nix
@@ -0,0 +1,98 @@
+{ lib, pkgs, config, ... }:
+with lib;
+let cfg = config.services.owncast;
+in {
+
+  options.services.owncast = {
+
+    enable = mkEnableOption "owncast";
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/owncast";
+      description = ''
+        The directory where owncast stores its data files. If left as the default value this directory will automatically be created before the owncast server starts, otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership and permissions.
+      '';
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Open the appropriate ports in the firewall for owncast.
+      '';
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = "owncast";
+      description = "User account under which owncast runs.";
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "owncast";
+      description = "Group under which owncast runs.";
+    };
+
+    listen = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      example = "0.0.0.0";
+      description = "The IP address to bind the owncast web server to.";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8080;
+      description = ''
+        TCP port where owncast web-gui listens.
+      '';
+    };
+
+    rtmp-port = mkOption {
+      type = types.port;
+      default = 1935;
+      description = ''
+        TCP port where owncast rtmp service listens.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.owncast = {
+      description = "A self-hosted live video and web chat server";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = mkMerge [
+        {
+          User = cfg.user;
+          Group = cfg.group;
+          WorkingDirectory = cfg.dataDir;
+          ExecStart = "${pkgs.owncast}/bin/owncast -webserverport ${toString cfg.port} -rtmpport ${toString cfg.rtmp-port} -webserverip ${cfg.listen}";
+          Restart = "on-failure";
+        }
+        (mkIf (cfg.dataDir == "/var/lib/owncast") {
+          StateDirectory = "owncast";
+        })
+      ];
+    };
+
+    users.users = mkIf (cfg.user == "owncast") {
+      owncast = {
+        isSystemUser = true;
+        group = cfg.group;
+        description = "owncast system user";
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "owncast") { owncast = { }; };
+
+    networking.firewall =
+      mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.rtmp-port ] ++ optional (cfg.listen != "127.0.0.1") cfg.port; };
+
+  };
+  meta = { maintainers = with lib.maintainers; [ MayNiklas ]; };
+}
diff --git a/nixos/modules/services/misc/packagekit.nix b/nixos/modules/services/misc/packagekit.nix
new file mode 100644
index 00000000000..9191078ef9c
--- /dev/null
+++ b/nixos/modules/services/misc/packagekit.nix
@@ -0,0 +1,74 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.packagekit;
+
+  inherit (lib)
+    mkEnableOption mkOption mkIf mkRemovedOptionModule types
+    listToAttrs recursiveUpdate;
+
+  iniFmt = pkgs.formats.ini { };
+
+  confFiles = [
+    (iniFmt.generate "PackageKit.conf" (recursiveUpdate
+      {
+        Daemon = {
+          DefaultBackend = "nix";
+          KeepCache = false;
+        };
+      }
+      cfg.settings))
+
+    (iniFmt.generate "Vendor.conf" (recursiveUpdate
+      {
+        PackagesNotFound = rec {
+          DefaultUrl = "https://github.com/NixOS/nixpkgs";
+          CodecUrl = DefaultUrl;
+          HardwareUrl = DefaultUrl;
+          FontUrl = DefaultUrl;
+          MimeUrl = DefaultUrl;
+        };
+      }
+      cfg.vendorSettings))
+  ];
+
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "packagekit" "backend" ] "Always set to Nix.")
+  ];
+
+  options.services.packagekit = {
+    enable = mkEnableOption ''
+      PackageKit provides a cross-platform D-Bus abstraction layer for
+      installing software. Software utilizing PackageKit can install
+      software regardless of the package manager.
+    '';
+
+    settings = mkOption {
+      type = iniFmt.type;
+      default = { };
+      description = "Additional settings passed straight through to PackageKit.conf";
+    };
+
+    vendorSettings = mkOption {
+      type = iniFmt.type;
+      default = { };
+      description = "Additional settings passed straight through to Vendor.conf";
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    services.dbus.packages = with pkgs; [ packagekit ];
+
+    environment.systemPackages = with pkgs; [ packagekit ];
+
+    systemd.packages = with pkgs; [ packagekit ];
+
+    environment.etc = listToAttrs (map
+      (e:
+        lib.nameValuePair "PackageKit/${e.name}" { source = e; })
+      confFiles);
+  };
+}
diff --git a/nixos/modules/services/misc/paperless-ng.nix b/nixos/modules/services/misc/paperless-ng.nix
new file mode 100644
index 00000000000..11e44f5ece5
--- /dev/null
+++ b/nixos/modules/services/misc/paperless-ng.nix
@@ -0,0 +1,322 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.paperless-ng;
+
+  defaultUser = "paperless";
+
+  hasCustomRedis = hasAttr "PAPERLESS_REDIS" cfg.extraConfig;
+
+  env = {
+    PAPERLESS_DATA_DIR = cfg.dataDir;
+    PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
+    PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
+    GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
+  } // (
+    lib.mapAttrs (_: toString) cfg.extraConfig
+  ) // (optionalAttrs (!hasCustomRedis) {
+    PAPERLESS_REDIS = "unix://${config.services.redis.servers.paperless-ng.unixSocket}";
+  });
+
+  manage = let
+    setupEnv = lib.concatStringsSep "\n" (mapAttrsToList (name: val: "export ${name}=\"${val}\"") env);
+  in pkgs.writeShellScript "manage" ''
+    ${setupEnv}
+    exec ${cfg.package}/bin/paperless-ng "$@"
+  '';
+
+  # Secure the services
+  defaultServiceConfig = {
+    TemporaryFileSystem = "/:ro";
+    BindReadOnlyPaths = [
+      "/nix/store"
+      "-/etc/resolv.conf"
+      "-/etc/nsswitch.conf"
+      "-/etc/hosts"
+      "-/etc/localtime"
+      "-/run/postgresql"
+    ] ++ (optional (!hasCustomRedis) config.services.redis.servers.paperless-ng.unixSocket);
+    BindPaths = [
+      cfg.consumptionDir
+      cfg.dataDir
+      cfg.mediaDir
+    ];
+    CapabilityBoundingSet = "";
+    # ProtectClock adds DeviceAllow=char-rtc r
+    DeviceAllow = "";
+    LockPersonality = true;
+    MemoryDenyWriteExecute = true;
+    NoNewPrivileges = true;
+    PrivateDevices = true;
+    PrivateMounts = true;
+    PrivateNetwork = true;
+    PrivateTmp = true;
+    PrivateUsers = true;
+    ProcSubset = "pid";
+    ProtectClock = true;
+    # Breaks if the home dir of the user is in /home
+    # Also does not add much value in combination with the TemporaryFileSystem.
+    # ProtectHome = true;
+    ProtectHostname = true;
+    # Would re-mount paths ignored by temporary root
+    #ProtectSystem = "strict";
+    ProtectControlGroups = true;
+    ProtectKernelLogs = true;
+    ProtectKernelModules = true;
+    ProtectKernelTunables = true;
+    ProtectProc = "invisible";
+    RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+    RestrictNamespaces = true;
+    RestrictRealtime = true;
+    RestrictSUIDSGID = true;
+    SupplementaryGroups = optional (!hasCustomRedis) config.services.redis.servers.paperless-ng.user;
+    SystemCallArchitectures = "native";
+    SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
+    # Does not work well with the temporary root
+    #UMask = "0066";
+  };
+in
+{
+  meta.maintainers = with maintainers; [ earvstedt Flakebi ];
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "paperless"] ''
+      The paperless module has been removed as the upstream project died.
+      Users should migrate to the paperless-ng module (services.paperless-ng).
+      More information can be found in the NixOS 21.11 release notes.
+    '')
+  ];
+
+  options.services.paperless-ng = {
+    enable = mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = ''
+        Enable Paperless-ng.
+
+        When started, the Paperless database is automatically created if it doesn't
+        exist and updated if the Paperless package has changed.
+        Both tasks are achieved by running a Django migration.
+
+        A script to manage the Paperless instance (by wrapping Django's manage.py) is linked to
+        <literal>''${dataDir}/paperless-ng-manage</literal>.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/paperless";
+      description = "Directory to store the Paperless data.";
+    };
+
+    mediaDir = mkOption {
+      type = types.str;
+      default = "${cfg.dataDir}/media";
+      defaultText = literalExpression ''"''${dataDir}/media"'';
+      description = "Directory to store the Paperless documents.";
+    };
+
+    consumptionDir = mkOption {
+      type = types.str;
+      default = "${cfg.dataDir}/consume";
+      defaultText = literalExpression ''"''${dataDir}/consume"'';
+      description = "Directory from which new documents are imported.";
+    };
+
+    consumptionDirIsPublic = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Whether all users can write to the consumption dir.";
+    };
+
+    passwordFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/run/keys/paperless-ng-password";
+      description = ''
+        A file containing the superuser password.
+
+        A superuser is required to access the web interface.
+        If unset, you can create a superuser manually by running
+        <literal>''${dataDir}/paperless-ng-manage createsuperuser</literal>.
+
+        The default superuser name is <literal>admin</literal>. To change it, set
+        option <option>extraConfig.PAPERLESS_ADMIN_USER</option>.
+        WARNING: When changing the superuser name after the initial setup, the old superuser
+        will continue to exist.
+
+        To disable login for the web interface, set the following:
+        <literal>extraConfig.PAPERLESS_AUTO_LOGIN_USERNAME = "admin";</literal>.
+        WARNING: Only use this on a trusted system without internet access to Paperless.
+      '';
+    };
+
+    address = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = "Web interface address.";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 28981;
+      description = "Web interface port.";
+    };
+
+    extraConfig = mkOption {
+      type = types.attrs;
+      default = {};
+      description = ''
+        Extra paperless-ng config options.
+
+        See <link xlink:href="https://paperless-ng.readthedocs.io/en/latest/configuration.html">the documentation</link>
+        for available options.
+      '';
+      example = literalExpression ''
+        {
+          PAPERLESS_OCR_LANGUAGE = "deu+eng";
+        }
+      '';
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = "User under which Paperless runs.";
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.paperless-ng;
+      defaultText = literalExpression "pkgs.paperless-ng";
+      description = "The Paperless package to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # Enable redis if no special url is set
+    services.redis.servers.paperless-ng.enable = mkIf (!hasCustomRedis) true;
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
+      "d '${cfg.mediaDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
+      (if cfg.consumptionDirIsPublic then
+        "d '${cfg.consumptionDir}' 777 - - - -"
+      else
+        "d '${cfg.consumptionDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
+      )
+    ];
+
+    systemd.services.paperless-ng-server = {
+      description = "Paperless document server";
+      serviceConfig = defaultServiceConfig // {
+        User = cfg.user;
+        ExecStart = "${cfg.package}/bin/paperless-ng qcluster";
+        Restart = "on-failure";
+        # The `mbind` syscall is needed for running the classifier.
+        SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ];
+      };
+      environment = env;
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "paperless-ng-consumer.service" "paperless-ng-web.service" ];
+
+      preStart = ''
+        ln -sf ${manage} ${cfg.dataDir}/paperless-ng-manage
+
+        # Auto-migrate on first run or if the package has changed
+        versionFile="${cfg.dataDir}/src-version"
+        if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then
+          ${cfg.package}/bin/paperless-ng migrate
+          echo ${cfg.package} > "$versionFile"
+        fi
+      ''
+      + optionalString (cfg.passwordFile != null) ''
+        export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}"
+        export PAPERLESS_ADMIN_PASSWORD=$(cat "${cfg.dataDir}/superuser-password")
+        superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD"
+        superuserStateFile="${cfg.dataDir}/superuser-state"
+
+        if [[ $(cat "$superuserStateFile" 2>/dev/null) != $superuserState ]]; then
+          ${cfg.package}/bin/paperless-ng manage_superuser
+          echo "$superuserState" > "$superuserStateFile"
+        fi
+      '';
+    } // optionalAttrs (!hasCustomRedis) {
+      after = [ "redis-paperless-ng.service" ];
+    };
+
+    # Password copying can't be implemented as a privileged preStart script
+    # in 'paperless-ng-server' because 'defaultServiceConfig' limits the filesystem
+    # paths accessible by the service.
+    systemd.services.paperless-ng-copy-password = mkIf (cfg.passwordFile != null) {
+      requiredBy = [ "paperless-ng-server.service" ];
+      before = [ "paperless-ng-server.service" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.coreutils}/bin/install --mode 600 --owner '${cfg.user}' --compare \
+            '${cfg.passwordFile}' '${cfg.dataDir}/superuser-password'
+        '';
+        Type = "oneshot";
+        # Needs to talk to mail server for automated import rules
+        PrivateNetwork = false;
+      };
+    };
+
+    systemd.services.paperless-ng-consumer = {
+      description = "Paperless document consumer";
+      serviceConfig = defaultServiceConfig // {
+        User = cfg.user;
+        ExecStart = "${cfg.package}/bin/paperless-ng document_consumer";
+        Restart = "on-failure";
+      };
+      environment = env;
+      # Bind to `paperless-ng-server` so that the consumer never runs
+      # during migrations
+      bindsTo = [ "paperless-ng-server.service" ];
+      after = [ "paperless-ng-server.service" ];
+    };
+
+    systemd.services.paperless-ng-web = {
+      description = "Paperless web server";
+      serviceConfig = defaultServiceConfig // {
+        User = cfg.user;
+        ExecStart = ''
+          ${pkgs.python3Packages.gunicorn}/bin/gunicorn \
+            -c ${cfg.package}/lib/paperless-ng/gunicorn.conf.py paperless.asgi:application
+        '';
+        Restart = "on-failure";
+
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
+        # gunicorn needs setuid
+        SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid" ];
+        # Needs to serve web page
+        PrivateNetwork = false;
+      };
+      environment = env // {
+        PATH = mkForce cfg.package.path;
+        PYTHONPATH = "${cfg.package.pythonPath}:${cfg.package}/lib/paperless-ng/src";
+      };
+      # Allow the web interface to access the private /tmp directory of the server.
+      # This is required to support uploading files via the web interface.
+      unitConfig.JoinsNamespaceOf = "paperless-ng-server.service";
+      # Bind to `paperless-ng-server` so that the web server never runs
+      # during migrations
+      bindsTo = [ "paperless-ng-server.service" ];
+      after = [ "paperless-ng-server.service" ];
+    };
+
+    users = optionalAttrs (cfg.user == defaultUser) {
+      users.${defaultUser} = {
+        group = defaultUser;
+        uid = config.ids.uids.paperless;
+        home = cfg.dataDir;
+      };
+
+      groups.${defaultUser} = {
+        gid = config.ids.gids.paperless;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/parsoid.nix b/nixos/modules/services/misc/parsoid.nix
new file mode 100644
index 00000000000..09b7f977bfb
--- /dev/null
+++ b/nixos/modules/services/misc/parsoid.nix
@@ -0,0 +1,129 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.parsoid;
+
+  parsoid = pkgs.nodePackages.parsoid;
+
+  confTree = {
+    worker_heartbeat_timeout = 300000;
+    logging = { level = "info"; };
+    services = [{
+      module = "lib/index.js";
+      entrypoint = "apiServiceWorker";
+      conf = {
+        mwApis = map (x: if isAttrs x then x else { uri = x; }) cfg.wikis;
+        serverInterface = cfg.interface;
+        serverPort = cfg.port;
+      };
+    }];
+  };
+
+  confFile = pkgs.writeText "config.yml" (builtins.toJSON (recursiveUpdate confTree cfg.extraConfig));
+
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "parsoid" "interwikis" ] "Use services.parsoid.wikis instead")
+  ];
+
+  ##### interface
+
+  options = {
+
+    services.parsoid = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Parsoid -- bidirectional
+          wikitext parser.
+        '';
+      };
+
+      wikis = mkOption {
+        type = types.listOf (types.either types.str types.attrs);
+        example = [ "http://localhost/api.php" ];
+        description = ''
+          Used MediaWiki API endpoints.
+        '';
+      };
+
+      workers = mkOption {
+        type = types.int;
+        default = 2;
+        description = ''
+          Number of Parsoid workers.
+        '';
+      };
+
+      interface = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = ''
+          Interface to listen on.
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8000;
+        description = ''
+          Port to listen on.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        description = ''
+          Extra configuration to add to parsoid configuration.
+        '';
+      };
+
+    };
+
+  };
+
+  ##### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.parsoid = {
+      description = "Bidirectional wikitext parser";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = "${parsoid}/lib/node_modules/parsoid/bin/server.js -c ${confFile} -n ${toString cfg.workers}";
+
+        DynamicUser = true;
+        User = "parsoid";
+        Group = "parsoid";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        #MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/pinnwand.nix b/nixos/modules/services/misc/pinnwand.nix
new file mode 100644
index 00000000000..cbc796c9a7c
--- /dev/null
+++ b/nixos/modules/services/misc/pinnwand.nix
@@ -0,0 +1,103 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.pinnwand;
+
+  format = pkgs.formats.toml {};
+  configFile = format.generate "pinnwand.toml" cfg.settings;
+in
+{
+  options.services.pinnwand = {
+    enable = mkEnableOption "Pinnwand";
+
+    port = mkOption {
+      type = types.port;
+      description = "The port to listen on.";
+      default = 8000;
+    };
+
+    settings = mkOption {
+      type = format.type;
+      description = ''
+        Your <filename>pinnwand.toml</filename> as a Nix attribute set. Look up
+        possible options in the <link xlink:href="https://github.com/supakeen/pinnwand/blob/master/pinnwand.toml-example">pinnwand.toml-example</link>.
+      '';
+      default = {};
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.pinnwand.settings = {
+      database_uri = mkDefault "sqlite:////var/lib/pinnwand/pinnwand.db";
+      paste_size = mkDefault 262144;
+      paste_help = mkDefault ''
+        <p>Welcome to pinnwand, this site is a pastebin. It allows you to share code with others. If you write code in the text area below and press the paste button you will be given a link you can share with others so they can view your code as well.</p><p>People with the link can view your pasted code, only you can remove your paste and it expires automatically. Note that anyone could guess the URI to your paste so don't rely on it being private.</p>
+      '';
+      footer = mkDefault ''
+        View <a href="//github.com/supakeen/pinnwand" target="_BLANK">source code</a>, the <a href="/removal">removal</a> or <a href="/expiry">expiry</a> stories, or read the <a href="/about">about</a> page.
+      '';
+    };
+
+    systemd.services = let
+      hardeningOptions = {
+        User = "pinnwand";
+        DynamicUser = true;
+
+        StateDirectory = "pinnwand";
+        StateDirectoryMode = "0700";
+
+        AmbientCapabilities = [];
+        CapabilityBoundingSet = "";
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        RestrictAddressFamilies = [
+          "AF_UNIX"
+          "AF_INET"
+          "AF_INET6"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "@system-service";
+        UMask = "0077";
+      };
+
+      command = "${pkgs.pinnwand}/bin/pinnwand --configuration-path ${configFile}";
+    in {
+      pinnwand = {
+        description = "Pinnwannd HTTP Server";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        unitConfig.Documentation = "https://pinnwand.readthedocs.io/en/latest/";
+
+        serviceConfig = {
+          ExecStart = "${command} http --port ${toString(cfg.port)}";
+        } // hardeningOptions;
+      };
+
+      pinnwand-reaper = {
+        description = "Pinnwand Reaper";
+        startAt = "daily";
+
+        serviceConfig = {
+          ExecStart = "${command} -vvvv reap";  # verbosity increased to show number of deleted pastes
+        } // hardeningOptions;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/plex.nix b/nixos/modules/services/misc/plex.nix
new file mode 100644
index 00000000000..1cd8da768f4
--- /dev/null
+++ b/nixos/modules/services/misc/plex.nix
@@ -0,0 +1,180 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.plex;
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "plex" "managePlugins" ] "Please omit or define the option: `services.plex.extraPlugins' instead.")
+  ];
+
+  options = {
+    services.plex = {
+      enable = mkEnableOption "Plex Media Server";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/plex";
+        description = ''
+          The directory where Plex stores its data files.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for the media server.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "plex";
+        description = ''
+          User account under which Plex runs.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "plex";
+        description = ''
+          Group under which Plex runs.
+        '';
+      };
+
+      extraPlugins = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          A list of paths to extra plugin bundles to install in Plex's plugin
+          directory. Every time the systemd unit for Plex starts up, all of the
+          symlinks in Plex's plugin directory will be cleared and this module
+          will symlink all of the paths specified here to that directory.
+        '';
+        example = literalExpression ''
+          [
+            (builtins.path {
+              name = "Audnexus.bundle";
+              path = pkgs.fetchFromGitHub {
+                owner = "djdembeck";
+                repo = "Audnexus.bundle";
+                rev = "v0.2.8";
+                sha256 = "sha256-IWOSz3vYL7zhdHan468xNc6C/eQ2C2BukQlaJNLXh7E=";
+              };
+            })
+          ]
+        '';
+      };
+
+      extraScanners = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          A list of paths to extra scanners to install in Plex's scanners
+          directory.
+
+          Every time the systemd unit for Plex starts up, all of the symlinks
+          in Plex's scanners directory will be cleared and this module will
+          symlink all of the paths specified here to that directory.
+        '';
+        example = literalExpression ''
+          [
+            (fetchFromGitHub {
+              owner = "ZeroQI";
+              repo = "Absolute-Series-Scanner";
+              rev = "773a39f502a1204b0b0255903cee4ed02c46fde0";
+              sha256 = "4l+vpiDdC8L/EeJowUgYyB3JPNTZ1sauN8liFAcK+PY=";
+            })
+          ]
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.plex;
+        defaultText = literalExpression "pkgs.plex";
+        description = ''
+          The Plex package to use. Plex subscribers may wish to use their own
+          package here, pointing to subscriber-only server versions.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # Most of this is just copied from the RPM package's systemd service file.
+    systemd.services.plex = {
+      description = "Plex Media Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+
+        # Run the pre-start script with full permissions (the "!" prefix) so it
+        # can create the data directory if necessary.
+        ExecStartPre = let
+          preStartScript = pkgs.writeScript "plex-run-prestart" ''
+            #!${pkgs.bash}/bin/bash
+
+            # Create data directory if it doesn't exist
+            if ! test -d "$PLEX_DATADIR"; then
+              echo "Creating initial Plex data directory in: $PLEX_DATADIR"
+              install -d -m 0755 -o "${cfg.user}" -g "${cfg.group}" "$PLEX_DATADIR"
+            fi
+         '';
+        in
+          "!${preStartScript}";
+
+        ExecStart = "${cfg.package}/bin/plexmediaserver";
+        KillSignal = "SIGQUIT";
+        Restart = "on-failure";
+      };
+
+      environment = {
+        # Configuration for our FHS userenv script
+        PLEX_DATADIR=cfg.dataDir;
+        PLEX_PLUGINS=concatMapStringsSep ":" builtins.toString cfg.extraPlugins;
+        PLEX_SCANNERS=concatMapStringsSep ":" builtins.toString cfg.extraScanners;
+
+        # The following variables should be set by the FHS userenv script:
+        #   PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR
+        #   PLEX_MEDIA_SERVER_HOME
+
+        # Allow access to GPU acceleration; the Plex LD_LIBRARY_PATH is added
+        # by the FHS userenv script.
+        LD_LIBRARY_PATH="/run/opengl-driver/lib";
+
+        PLEX_MEDIA_SERVER_MAX_PLUGIN_PROCS="6";
+        PLEX_MEDIA_SERVER_TMPDIR="/tmp";
+        PLEX_MEDIA_SERVER_USE_SYSLOG="true";
+        LC_ALL="en_US.UTF-8";
+        LANG="en_US.UTF-8";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 32400 3005 8324 32469 ];
+      allowedUDPPorts = [ 1900 5353 32410 32412 32413 32414 ];
+    };
+
+    users.users = mkIf (cfg.user == "plex") {
+      plex = {
+        group = cfg.group;
+        uid = config.ids.uids.plex;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "plex") {
+      plex = {
+        gid = config.ids.gids.plex;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/plikd.nix b/nixos/modules/services/misc/plikd.nix
new file mode 100644
index 00000000000..a62dbef1d2a
--- /dev/null
+++ b/nixos/modules/services/misc/plikd.nix
@@ -0,0 +1,82 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.plikd;
+
+  format = pkgs.formats.toml {};
+  plikdCfg = format.generate "plikd.cfg" cfg.settings;
+in
+{
+  options = {
+    services.plikd = {
+      enable = mkEnableOption "the plikd server";
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the plikd.";
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = {};
+        description = ''
+          Configuration for plikd, see <link xlink:href="https://github.com/root-gg/plik/blob/master/server/plikd.cfg"/>
+          for supported values.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.plikd.settings = mapAttrs (name: mkDefault) {
+      ListenPort = 8080;
+      ListenAddress = "localhost";
+      DataBackend = "file";
+      DataBackendConfig = {
+         Directory = "/var/lib/plikd";
+      };
+      MetadataBackendConfig = {
+        Driver = "sqlite3";
+        ConnectionString = "/var/lib/plikd/plik.db";
+      };
+    };
+
+    systemd.services.plikd = {
+      description = "Plikd file sharing server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${pkgs.plikd}/bin/plikd --config ${plikdCfg}";
+        Restart = "on-failure";
+        StateDirectory = "plikd";
+        LogsDirectory = "plikd";
+        DynamicUser = true;
+
+        # Basic hardening
+        NoNewPrivileges = "yes";
+        PrivateTmp = "yes";
+        PrivateDevices = "yes";
+        DevicePolicy = "closed";
+        ProtectSystem = "strict";
+        ProtectHome = "read-only";
+        ProtectControlGroups = "yes";
+        ProtectKernelModules = "yes";
+        ProtectKernelTunables = "yes";
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+        RestrictNamespaces = "yes";
+        RestrictRealtime = "yes";
+        RestrictSUIDSGID = "yes";
+        MemoryDenyWriteExecute = "yes";
+        LockPersonality = "yes";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.settings.ListenPort ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/podgrab.nix b/nixos/modules/services/misc/podgrab.nix
new file mode 100644
index 00000000000..7077408b794
--- /dev/null
+++ b/nixos/modules/services/misc/podgrab.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.podgrab;
+in
+{
+  options.services.podgrab = with lib; {
+    enable = mkEnableOption "Podgrab, a self-hosted podcast manager";
+
+    passwordFile = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "/run/secrets/password.env";
+      description = ''
+        The path to a file containing the PASSWORD environment variable
+        definition for Podgrab's authentification.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8080;
+      example = 4242;
+      description = "The port on which Podgrab will listen for incoming HTTP traffic.";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.podgrab = {
+      description = "Podgrab podcast manager";
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        CONFIG = "/var/lib/podgrab/config";
+        DATA = "/var/lib/podgrab/data";
+        GIN_MODE = "release";
+        PORT = toString cfg.port;
+      };
+      serviceConfig = {
+        DynamicUser = true;
+        EnvironmentFile = lib.optional (cfg.passwordFile != null) [
+          cfg.passwordFile
+        ];
+        ExecStart = "${pkgs.podgrab}/bin/podgrab";
+        WorkingDirectory = "${pkgs.podgrab}/share";
+        StateDirectory = [ "podgrab/config" "podgrab/data" ];
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ ambroisie ];
+}
diff --git a/nixos/modules/services/misc/prowlarr.nix b/nixos/modules/services/misc/prowlarr.nix
new file mode 100644
index 00000000000..ef820b4022d
--- /dev/null
+++ b/nixos/modules/services/misc/prowlarr.nix
@@ -0,0 +1,41 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.prowlarr;
+
+in
+{
+  options = {
+    services.prowlarr = {
+      enable = mkEnableOption "Prowlarr";
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the Prowlarr web interface.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.prowlarr = {
+      description = "Prowlarr";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        DynamicUser = true;
+        StateDirectory = "prowlarr";
+        ExecStart = "${pkgs.prowlarr}/bin/Prowlarr -nobrowser -data=/var/lib/prowlarr";
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 9696 ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/pykms.nix b/nixos/modules/services/misc/pykms.nix
new file mode 100644
index 00000000000..2f752bcc7ed
--- /dev/null
+++ b/nixos/modules/services/misc/pykms.nix
@@ -0,0 +1,92 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.pykms;
+  libDir = "/var/lib/pykms";
+
+in
+{
+  meta.maintainers = with lib.maintainers; [ peterhoeg ];
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "pykms" "verbose" ] "Use services.pykms.logLevel instead")
+  ];
+
+  options = {
+    services.pykms = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the PyKMS service.";
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "The IP address on which to listen.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 1688;
+        description = "The port on which to listen.";
+      };
+
+      openFirewallPort = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether the listening port should be opened automatically.";
+      };
+
+      memoryLimit = mkOption {
+        type = types.str;
+        default = "64M";
+        description = "How much memory to use at most.";
+      };
+
+      logLevel = mkOption {
+        type = types.enum [ "CRITICAL" "ERROR" "WARNING" "INFO" "DEBUG" "MININFO" ];
+        default = "INFO";
+        description = "How much to log";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = "Additional arguments";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewallPort [ cfg.port ];
+
+    systemd.services.pykms = {
+      description = "Python KMS";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      # python programs with DynamicUser = true require HOME to be set
+      environment.HOME = libDir;
+      serviceConfig = with pkgs; {
+        DynamicUser = true;
+        StateDirectory = baseNameOf libDir;
+        ExecStartPre = "${getBin pykms}/libexec/create_pykms_db.sh ${libDir}/clients.db";
+        ExecStart = lib.concatStringsSep " " ([
+          "${getBin pykms}/bin/server"
+          "--logfile=STDOUT"
+          "--loglevel=${cfg.logLevel}"
+          "--sqlite=${libDir}/clients.db"
+        ] ++ cfg.extraArgs ++ [
+          cfg.listenAddress
+          (toString cfg.port)
+        ]);
+        ProtectHome = "tmpfs";
+        WorkingDirectory = libDir;
+        SyslogIdentifier = "pykms";
+        Restart = "on-failure";
+        MemoryLimit = cfg.memoryLimit;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/radarr.nix b/nixos/modules/services/misc/radarr.nix
new file mode 100644
index 00000000000..74444e24043
--- /dev/null
+++ b/nixos/modules/services/misc/radarr.nix
@@ -0,0 +1,75 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.radarr;
+
+in
+{
+  options = {
+    services.radarr = {
+      enable = mkEnableOption "Radarr";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/radarr/.config/Radarr";
+        description = "The directory where Radarr stores its data files.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the Radarr web interface.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "radarr";
+        description = "User account under which Radarr runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "radarr";
+        description = "Group under which Radarr runs.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.radarr = {
+      description = "Radarr";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.radarr}/bin/Radarr -nobrowser -data='${cfg.dataDir}'";
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 7878 ];
+    };
+
+    users.users = mkIf (cfg.user == "radarr") {
+      radarr = {
+        group = cfg.group;
+        home = cfg.dataDir;
+        uid = config.ids.uids.radarr;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "radarr") {
+      radarr.gid = config.ids.gids.radarr;
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix
new file mode 100644
index 00000000000..696b8d1a25d
--- /dev/null
+++ b/nixos/modules/services/misc/redmine.nix
@@ -0,0 +1,384 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkBefore mkDefault mkEnableOption mkIf mkOption mkRemovedOptionModule types;
+  inherit (lib) concatStringsSep literalExpression mapAttrsToList;
+  inherit (lib) optional optionalAttrs optionalString;
+
+  cfg = config.services.redmine;
+  format = pkgs.formats.yaml {};
+  bundle = "${cfg.package}/share/redmine/bin/bundle";
+
+  databaseYml = pkgs.writeText "database.yml" ''
+    production:
+      adapter: ${cfg.database.type}
+      database: ${cfg.database.name}
+      host: ${if (cfg.database.type == "postgresql" && cfg.database.socket != null) then cfg.database.socket else cfg.database.host}
+      port: ${toString cfg.database.port}
+      username: ${cfg.database.user}
+      password: #dbpass#
+      ${optionalString (cfg.database.type == "mysql2" && cfg.database.socket != null) "socket: ${cfg.database.socket}"}
+  '';
+
+  configurationYml = format.generate "configuration.yml" cfg.settings;
+  additionalEnvironment = pkgs.writeText "additional_environment.rb" cfg.extraEnv;
+
+  unpackTheme = unpack "theme";
+  unpackPlugin = unpack "plugin";
+  unpack = id: (name: source:
+    pkgs.stdenv.mkDerivation {
+      name = "redmine-${id}-${name}";
+      nativeBuildInputs = [ pkgs.unzip ];
+      buildCommand = ''
+        mkdir -p $out
+        cd $out
+        unpackFile ${source}
+      '';
+  });
+
+  mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql2";
+  pgsqlLocal = cfg.database.createLocally && cfg.database.type == "postgresql";
+
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "redmine" "extraConfig" ] "Use services.redmine.settings instead.")
+    (mkRemovedOptionModule [ "services" "redmine" "database" "password" ] "Use services.redmine.database.passwordFile instead.")
+  ];
+
+  # interface
+  options = {
+    services.redmine = {
+      enable = mkEnableOption "Redmine";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.redmine;
+        defaultText = literalExpression "pkgs.redmine";
+        description = "Which Redmine package to use.";
+        example = literalExpression "pkgs.redmine.override { ruby = pkgs.ruby_2_7; }";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "redmine";
+        description = "User under which Redmine is ran.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "redmine";
+        description = "Group under which Redmine is ran.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 3000;
+        description = "Port on which Redmine is ran.";
+      };
+
+      stateDir = mkOption {
+        type = types.str;
+        default = "/var/lib/redmine";
+        description = "The state directory, logs and plugins are stored here.";
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = {};
+        description = ''
+          Redmine configuration (<filename>configuration.yml</filename>). Refer to
+          <link xlink:href="https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration"/>
+          for details.
+        '';
+        example = literalExpression ''
+          {
+            email_delivery = {
+              delivery_method = "smtp";
+              smtp_settings = {
+                address = "mail.example.com";
+                port = 25;
+              };
+            };
+          }
+        '';
+      };
+
+      extraEnv = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration in additional_environment.rb.
+
+          See <link xlink:href="https://svn.redmine.org/redmine/trunk/config/additional_environment.rb.example"/>
+          for details.
+        '';
+        example = ''
+          config.logger.level = Logger::DEBUG
+        '';
+      };
+
+      themes = mkOption {
+        type = types.attrsOf types.path;
+        default = {};
+        description = "Set of themes.";
+        example = literalExpression ''
+          {
+            dkuk-redmine_alex_skin = builtins.fetchurl {
+              url = "https://bitbucket.org/dkuk/redmine_alex_skin/get/1842ef675ef3.zip";
+              sha256 = "0hrin9lzyi50k4w2bd2b30vrf1i4fi1c0gyas5801wn8i7kpm9yl";
+            };
+          }
+        '';
+      };
+
+      plugins = mkOption {
+        type = types.attrsOf types.path;
+        default = {};
+        description = "Set of plugins.";
+        example = literalExpression ''
+          {
+            redmine_env_auth = builtins.fetchurl {
+              url = "https://github.com/Intera/redmine_env_auth/archive/0.6.zip";
+              sha256 = "0yyr1yjd8gvvh832wdc8m3xfnhhxzk2pk3gm2psg5w9jdvd6skak";
+            };
+          }
+        '';
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum [ "mysql2" "postgresql" ];
+          example = "postgresql";
+          default = "mysql2";
+          description = "Database engine to use.";
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = "Database host address.";
+        };
+
+        port = mkOption {
+          type = types.int;
+          default = if cfg.database.type == "postgresql" then 5432 else 3306;
+          defaultText = literalExpression "3306";
+          description = "Database host port.";
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "redmine";
+          description = "Database name.";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "redmine";
+          description = "Database user.";
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          example = "/run/keys/redmine-dbpassword";
+          description = ''
+            A file containing the password corresponding to
+            <option>database.user</option>.
+          '';
+        };
+
+        socket = mkOption {
+          type = types.nullOr types.path;
+          default =
+            if mysqlLocal then "/run/mysqld/mysqld.sock"
+            else if pgsqlLocal then "/run/postgresql"
+            else null;
+          defaultText = literalExpression "/run/mysqld/mysqld.sock";
+          example = "/run/mysqld/mysqld.sock";
+          description = "Path to the unix socket file to use for authentication.";
+        };
+
+        createLocally = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Create the database and database user locally.";
+        };
+      };
+    };
+  };
+
+  # implementation
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = cfg.database.passwordFile != null || cfg.database.socket != null;
+        message = "one of services.redmine.database.socket or services.redmine.database.passwordFile must be set";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.user == cfg.user;
+        message = "services.redmine.database.user must be set to ${cfg.user} if services.redmine.database.createLocally is set true";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.socket != null;
+        message = "services.redmine.database.socket must be set if services.redmine.database.createLocally is set to true";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.host == "localhost";
+        message = "services.redmine.database.host must be set to localhost if services.redmine.database.createLocally is set to true";
+      }
+    ];
+
+    services.redmine.settings = {
+      production = {
+        scm_subversion_command = "${pkgs.subversion}/bin/svn";
+        scm_mercurial_command = "${pkgs.mercurial}/bin/hg";
+        scm_git_command = "${pkgs.git}/bin/git";
+        scm_cvs_command = "${pkgs.cvs}/bin/cvs";
+        scm_bazaar_command = "${pkgs.breezy}/bin/bzr";
+        scm_darcs_command = "${pkgs.darcs}/bin/darcs";
+      };
+    };
+
+    services.redmine.extraEnv = mkBefore ''
+      config.logger = Logger.new("${cfg.stateDir}/log/production.log", 14, 1048576)
+      config.logger.level = Logger::INFO
+    '';
+
+    services.mysql = mkIf mysqlLocal {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    services.postgresql = mkIf pgsqlLocal {
+      enable = true;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    # create symlinks for the basic directory layout the redmine package expects
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/cache' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/config' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/files' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/plugins' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/public' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/public/plugin_assets' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/public/themes' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/tmp' 0750 ${cfg.user} ${cfg.group} - -"
+
+      "d /run/redmine - - - - -"
+      "d /run/redmine/public - - - - -"
+      "L+ /run/redmine/config - - - - ${cfg.stateDir}/config"
+      "L+ /run/redmine/files - - - - ${cfg.stateDir}/files"
+      "L+ /run/redmine/log - - - - ${cfg.stateDir}/log"
+      "L+ /run/redmine/plugins - - - - ${cfg.stateDir}/plugins"
+      "L+ /run/redmine/public/plugin_assets - - - - ${cfg.stateDir}/public/plugin_assets"
+      "L+ /run/redmine/public/themes - - - - ${cfg.stateDir}/public/themes"
+      "L+ /run/redmine/tmp - - - - ${cfg.stateDir}/tmp"
+    ];
+
+    systemd.services.redmine = {
+      after = [ "network.target" ] ++ optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
+      wantedBy = [ "multi-user.target" ];
+      environment.RAILS_ENV = "production";
+      environment.RAILS_CACHE = "${cfg.stateDir}/cache";
+      environment.REDMINE_LANG = "en";
+      environment.SCHEMA = "${cfg.stateDir}/cache/schema.db";
+      path = with pkgs; [
+        imagemagick
+        breezy
+        cvs
+        darcs
+        git
+        mercurial
+        subversion
+      ];
+      preStart = ''
+        rm -rf "${cfg.stateDir}/plugins/"*
+        rm -rf "${cfg.stateDir}/public/themes/"*
+
+        # start with a fresh config directory
+        # the config directory is copied instead of linked as some mutable data is stored in there
+        find "${cfg.stateDir}/config" ! -name "secret_token.rb" -type f -exec rm -f {} +
+        cp -r ${cfg.package}/share/redmine/config.dist/* "${cfg.stateDir}/config/"
+
+        chmod -R u+w "${cfg.stateDir}/config"
+
+        # link in the application configuration
+        ln -fs ${configurationYml} "${cfg.stateDir}/config/configuration.yml"
+
+        # link in the additional environment configuration
+        ln -fs ${additionalEnvironment} "${cfg.stateDir}/config/additional_environment.rb"
+
+
+        # link in all user specified themes
+        for theme in ${concatStringsSep " " (mapAttrsToList unpackTheme cfg.themes)}; do
+          ln -fs $theme/* "${cfg.stateDir}/public/themes"
+        done
+
+        # link in redmine provided themes
+        ln -sf ${cfg.package}/share/redmine/public/themes.dist/* "${cfg.stateDir}/public/themes/"
+
+
+        # link in all user specified plugins
+        for plugin in ${concatStringsSep " " (mapAttrsToList unpackPlugin cfg.plugins)}; do
+          ln -fs $plugin/* "${cfg.stateDir}/plugins/''${plugin##*-redmine-plugin-}"
+        done
+
+
+        # handle database.passwordFile & permissions
+        DBPASS=${optionalString (cfg.database.passwordFile != null) "$(head -n1 ${cfg.database.passwordFile})"}
+        cp -f ${databaseYml} "${cfg.stateDir}/config/database.yml"
+        sed -e "s,#dbpass#,$DBPASS,g" -i "${cfg.stateDir}/config/database.yml"
+        chmod 440 "${cfg.stateDir}/config/database.yml"
+
+
+        # generate a secret token if required
+        if ! test -e "${cfg.stateDir}/config/initializers/secret_token.rb"; then
+          ${bundle} exec rake generate_secret_token
+          chmod 440 "${cfg.stateDir}/config/initializers/secret_token.rb"
+        fi
+
+        # execute redmine required commands prior to starting the application
+        ${bundle} exec rake db:migrate
+        ${bundle} exec rake redmine:plugins:migrate
+        ${bundle} exec rake redmine:load_default_data
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "300";
+        WorkingDirectory = "${cfg.package}/share/redmine";
+        ExecStart="${bundle} exec rails server webrick -e production -p ${toString cfg.port} -P '${cfg.stateDir}/redmine.pid'";
+      };
+
+    };
+
+    users.users = optionalAttrs (cfg.user == "redmine") {
+      redmine = {
+        group = cfg.group;
+        home = cfg.stateDir;
+        uid = config.ids.uids.redmine;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "redmine") {
+      redmine.gid = config.ids.gids.redmine;
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/ripple-data-api.nix b/nixos/modules/services/misc/ripple-data-api.nix
new file mode 100644
index 00000000000..93eba98b7d3
--- /dev/null
+++ b/nixos/modules/services/misc/ripple-data-api.nix
@@ -0,0 +1,195 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.rippleDataApi;
+
+  deployment_env_config = builtins.toJSON {
+    production = {
+      port = toString cfg.port;
+      maxSockets = 150;
+      batchSize = 100;
+      startIndex = 32570;
+      rippleds = cfg.rippleds;
+      redis = {
+        enable = cfg.redis.enable;
+        host = cfg.redis.host;
+        port = cfg.redis.port;
+        options.auth_pass = null;
+      };
+    };
+  };
+
+  db_config = builtins.toJSON {
+    production = {
+      username = optional (cfg.couchdb.pass != "") cfg.couchdb.user;
+      password = optional (cfg.couchdb.pass != "") cfg.couchdb.pass;
+      host = cfg.couchdb.host;
+      port = cfg.couchdb.port;
+      database = cfg.couchdb.db;
+      protocol = "http";
+    };
+  };
+
+in {
+  options = {
+    services.rippleDataApi = {
+      enable = mkEnableOption "ripple data api";
+
+      port = mkOption {
+        description = "Ripple data api port";
+        default = 5993;
+        type = types.int;
+      };
+
+      importMode = mkOption {
+        description = "Ripple data api import mode.";
+        default = "liveOnly";
+        type = types.enum ["live" "liveOnly"];
+      };
+
+      minLedger = mkOption {
+        description = "Ripple data api minimal ledger to fetch.";
+        default = null;
+        type = types.nullOr types.int;
+      };
+
+      maxLedger = mkOption {
+        description = "Ripple data api maximal ledger to fetch.";
+        default = null;
+        type = types.nullOr types.int;
+      };
+
+      redis = {
+        enable = mkOption {
+          description = "Whether to enable caching of ripple data to redis.";
+          default = true;
+          type = types.bool;
+        };
+
+        host = mkOption {
+          description = "Ripple data api redis host.";
+          default = "localhost";
+          type = types.str;
+        };
+
+        port = mkOption {
+          description = "Ripple data api redis port.";
+          default = 5984;
+          type = types.int;
+        };
+      };
+
+      couchdb = {
+        host = mkOption {
+          description = "Ripple data api couchdb host.";
+          default = "localhost";
+          type = types.str;
+        };
+
+        port = mkOption {
+          description = "Ripple data api couchdb port.";
+          default = 5984;
+          type = types.int;
+        };
+
+        db = mkOption {
+          description = "Ripple data api couchdb database.";
+          default = "rippled";
+          type = types.str;
+        };
+
+        user = mkOption {
+          description = "Ripple data api couchdb username.";
+          default = "rippled";
+          type = types.str;
+        };
+
+        pass = mkOption {
+          description = "Ripple data api couchdb password.";
+          default = "";
+          type = types.str;
+        };
+
+        create = mkOption {
+          description = "Whether to create couchdb database needed by ripple data api.";
+          type = types.bool;
+          default = true;
+        };
+      };
+
+      rippleds = mkOption {
+        description = "List of rippleds to be used by ripple data api.";
+        default = [
+          "http://s_east.ripple.com:51234"
+          "http://s_west.ripple.com:51234"
+        ];
+        type = types.listOf types.str;
+      };
+    };
+  };
+
+  config = mkIf (cfg.enable) {
+    services.couchdb.enable = mkDefault true;
+    services.couchdb.bindAddress = mkDefault "0.0.0.0";
+    services.redis.enable = mkDefault true;
+
+    systemd.services.ripple-data-api = {
+      after = [ "couchdb.service" "redis.service" "ripple-data-api-importer.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment = {
+        NODE_ENV = "production";
+        DEPLOYMENT_ENVS_CONFIG = pkgs.writeText "deployment.environment.json" deployment_env_config;
+        DB_CONFIG = pkgs.writeText "db.config.json" db_config;
+      };
+
+      serviceConfig = {
+        ExecStart = "${pkgs.ripple-data-api}/bin/api";
+        Restart = "always";
+        User = "ripple-data-api";
+      };
+    };
+
+    systemd.services.ripple-data-importer = {
+      after = [ "couchdb.service" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.curl ];
+
+      environment = {
+        NODE_ENV = "production";
+        DEPLOYMENT_ENVS_CONFIG = pkgs.writeText "deployment.environment.json" deployment_env_config;
+        DB_CONFIG = pkgs.writeText "db.config.json" db_config;
+        LOG_FILE = "/dev/null";
+      };
+
+      serviceConfig = let
+        importMode =
+          if cfg.minLedger != null && cfg.maxLedger != null then
+            "${toString cfg.minLedger} ${toString cfg.maxLedger}"
+          else
+            cfg.importMode;
+      in {
+        ExecStart = "${pkgs.ripple-data-api}/bin/importer ${importMode} debug";
+        Restart = "always";
+        User = "ripple-data-api";
+      };
+
+      preStart = mkMerge [
+        (mkIf (cfg.couchdb.create) ''
+          HOST="http://${optionalString (cfg.couchdb.pass != "") "${cfg.couchdb.user}:${cfg.couchdb.pass}@"}${cfg.couchdb.host}:${toString cfg.couchdb.port}"
+          curl -X PUT $HOST/${cfg.couchdb.db} || true
+        '')
+        "${pkgs.ripple-data-api}/bin/update-views"
+      ];
+    };
+
+    users.users.ripple-data-api =
+      { description = "Ripple data api user";
+        isSystemUser = true;
+        group = "ripple-data-api";
+      };
+    users.groups.ripple-data-api = {};
+  };
+}
diff --git a/nixos/modules/services/misc/rippled.nix b/nixos/modules/services/misc/rippled.nix
new file mode 100644
index 00000000000..f6ec0677774
--- /dev/null
+++ b/nixos/modules/services/misc/rippled.nix
@@ -0,0 +1,438 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.rippled;
+  opt = options.services.rippled;
+
+  b2i = val: if val then "1" else "0";
+
+  dbCfg = db: ''
+    type=${db.type}
+    path=${db.path}
+    ${optionalString (db.compression != null) ("compression=${b2i db.compression}") }
+    ${optionalString (db.onlineDelete != null) ("online_delete=${toString db.onlineDelete}")}
+    ${optionalString (db.advisoryDelete != null) ("advisory_delete=${b2i db.advisoryDelete}")}
+    ${db.extraOpts}
+  '';
+
+  rippledCfg = ''
+    [server]
+    ${concatMapStringsSep "\n" (n: "port_${n}") (attrNames cfg.ports)}
+
+    ${concatMapStrings (p: ''
+    [port_${p.name}]
+    ip=${p.ip}
+    port=${toString p.port}
+    protocol=${concatStringsSep "," p.protocol}
+    ${optionalString (p.user != "") "user=${p.user}"}
+    ${optionalString (p.password != "") "user=${p.password}"}
+    admin=${concatStringsSep "," p.admin}
+    ${optionalString (p.ssl.key != null) "ssl_key=${p.ssl.key}"}
+    ${optionalString (p.ssl.cert != null) "ssl_cert=${p.ssl.cert}"}
+    ${optionalString (p.ssl.chain != null) "ssl_chain=${p.ssl.chain}"}
+    '') (attrValues cfg.ports)}
+
+    [database_path]
+    ${cfg.databasePath}
+
+    [node_db]
+    ${dbCfg cfg.nodeDb}
+
+    ${optionalString (cfg.tempDb != null) ''
+    [temp_db]
+    ${dbCfg cfg.tempDb}''}
+
+    ${optionalString (cfg.importDb != null) ''
+    [import_db]
+    ${dbCfg cfg.importDb}''}
+
+    [ips]
+    ${concatStringsSep "\n" cfg.ips}
+
+    [ips_fixed]
+    ${concatStringsSep "\n" cfg.ipsFixed}
+
+    [validators]
+    ${concatStringsSep "\n" cfg.validators}
+
+    [node_size]
+    ${cfg.nodeSize}
+
+    [ledger_history]
+    ${toString cfg.ledgerHistory}
+
+    [fetch_depth]
+    ${toString cfg.fetchDepth}
+
+    [validation_quorum]
+    ${toString cfg.validationQuorum}
+
+    [sntp_servers]
+    ${concatStringsSep "\n" cfg.sntpServers}
+
+    ${optionalString cfg.statsd.enable ''
+    [insight]
+    server=statsd
+    address=${cfg.statsd.address}
+    prefix=${cfg.statsd.prefix}
+    ''}
+
+    [rpc_startup]
+    { "command": "log_level", "severity": "${cfg.logLevel}" }
+  '' + cfg.extraConfig;
+
+  portOptions = { name, ...}: {
+    options = {
+      name = mkOption {
+        internal = true;
+        default = name;
+      };
+
+      ip = mkOption {
+        default = "127.0.0.1";
+        description = "Ip where rippled listens.";
+        type = types.str;
+      };
+
+      port = mkOption {
+        description = "Port where rippled listens.";
+        type = types.int;
+      };
+
+      protocol = mkOption {
+        description = "Protocols expose by rippled.";
+        type = types.listOf (types.enum ["http" "https" "ws" "wss" "peer"]);
+      };
+
+      user = mkOption {
+        description = "When set, these credentials will be required on HTTP/S requests.";
+        type = types.str;
+        default = "";
+      };
+
+      password = mkOption {
+        description = "When set, these credentials will be required on HTTP/S requests.";
+        type = types.str;
+        default = "";
+      };
+
+      admin = mkOption {
+        description = "A comma-separated list of admin IP addresses.";
+        type = types.listOf types.str;
+        default = ["127.0.0.1"];
+      };
+
+      ssl = {
+        key = mkOption {
+          description = ''
+            Specifies the filename holding the SSL key in PEM format.
+          '';
+          default = null;
+          type = types.nullOr types.path;
+        };
+
+        cert = mkOption {
+          description = ''
+            Specifies the path to the SSL certificate file in PEM format.
+            This is not needed if the chain includes it.
+          '';
+          default = null;
+          type = types.nullOr types.path;
+        };
+
+        chain = mkOption {
+          description = ''
+            If you need a certificate chain, specify the path to the
+            certificate chain here. The chain may include the end certificate.
+          '';
+          default = null;
+          type = types.nullOr types.path;
+        };
+      };
+    };
+  };
+
+  dbOptions = {
+    options = {
+      type = mkOption {
+        description = "Rippled database type.";
+        type = types.enum ["rocksdb" "nudb"];
+        default = "rocksdb";
+      };
+
+      path = mkOption {
+        description = "Location to store the database.";
+        type = types.path;
+        default = cfg.databasePath;
+        defaultText = literalExpression "config.${opt.databasePath}";
+      };
+
+      compression = mkOption {
+        description = "Whether to enable snappy compression.";
+        type = types.nullOr types.bool;
+        default = null;
+      };
+
+      onlineDelete = mkOption {
+        description = "Enable automatic purging of older ledger information.";
+        type = types.nullOr (types.addCheck types.int (v: v > 256));
+        default = cfg.ledgerHistory;
+        defaultText = literalExpression "config.${opt.ledgerHistory}";
+      };
+
+      advisoryDelete = mkOption {
+        description = ''
+          If set, then require administrative RPC call "can_delete"
+          to enable online deletion of ledger records.
+        '';
+        type = types.nullOr types.bool;
+        default = null;
+      };
+
+      extraOpts = mkOption {
+        description = "Extra database options.";
+        type = types.lines;
+        default = "";
+      };
+    };
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+    services.rippled = {
+      enable = mkEnableOption "rippled";
+
+      package = mkOption {
+        description = "Which rippled package to use.";
+        type = types.package;
+        default = pkgs.rippled;
+        defaultText = literalExpression "pkgs.rippled";
+      };
+
+      ports = mkOption {
+        description = "Ports exposed by rippled";
+        type = with types; attrsOf (submodule portOptions);
+        default = {
+          rpc = {
+            port = 5005;
+            admin = ["127.0.0.1"];
+            protocol = ["http"];
+          };
+
+          peer = {
+            port = 51235;
+            ip = "0.0.0.0";
+            protocol = ["peer"];
+          };
+
+          ws_public = {
+            port = 5006;
+            ip = "0.0.0.0";
+            protocol = ["ws" "wss"];
+          };
+        };
+      };
+
+      nodeDb = mkOption {
+        description = "Rippled main database options.";
+        type = with types; nullOr (submodule dbOptions);
+        default = {
+          type = "rocksdb";
+          extraOpts = ''
+            open_files=2000
+            filter_bits=12
+            cache_mb=256
+            file_size_pb=8
+            file_size_mult=2;
+          '';
+        };
+      };
+
+      tempDb = mkOption {
+        description = "Rippled temporary database options.";
+        type = with types; nullOr (submodule dbOptions);
+        default = null;
+      };
+
+      importDb = mkOption {
+        description = "Settings for performing a one-time import.";
+        type = with types; nullOr (submodule dbOptions);
+        default = null;
+      };
+
+      nodeSize = mkOption {
+        description = ''
+          Rippled size of the node you are running.
+          "tiny", "small", "medium", "large", and "huge"
+        '';
+        type = types.enum ["tiny" "small" "medium" "large" "huge"];
+        default = "small";
+      };
+
+      ips = mkOption {
+        description = ''
+          List of hostnames or ips where the Ripple protocol is served.
+          For a starter list, you can either copy entries from:
+          https://ripple.com/ripple.txt or if you prefer you can let it
+           default to r.ripple.com 51235
+
+          A port may optionally be specified after adding a space to the
+          address. By convention, if known, IPs are listed in from most
+          to least trusted.
+        '';
+        type = types.listOf types.str;
+        default = ["r.ripple.com 51235"];
+      };
+
+      ipsFixed = mkOption {
+        description = ''
+          List of IP addresses or hostnames to which rippled should always
+          attempt to maintain peer connections with. This is useful for
+          manually forming private networks, for example to configure a
+          validation server that connects to the Ripple network through a
+          public-facing server, or for building a set of cluster peers.
+
+          A port may optionally be specified after adding a space to the address
+        '';
+        type = types.listOf types.str;
+        default = [];
+      };
+
+      validators = mkOption {
+        description = ''
+          List of nodes to always accept as validators. Nodes are specified by domain
+          or public key.
+        '';
+        type = types.listOf types.str;
+        default = [
+          "n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7  RL1"
+          "n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj  RL2"
+          "n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C  RL3"
+          "n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS  RL4"
+          "n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA  RL5"
+        ];
+      };
+
+      databasePath = mkOption {
+        description = ''
+          Path to the ripple database.
+        '';
+        type = types.path;
+        default = "/var/lib/rippled";
+      };
+
+      validationQuorum = mkOption {
+        description = ''
+          The minimum number of trusted validations a ledger must have before
+          the server considers it fully validated.
+        '';
+        type = types.int;
+        default = 3;
+      };
+
+      ledgerHistory = mkOption {
+        description = ''
+          The number of past ledgers to acquire on server startup and the minimum
+          to maintain while running.
+        '';
+        type = types.either types.int (types.enum ["full"]);
+        default = 1296000; # 1 month
+      };
+
+      fetchDepth = mkOption {
+        description = ''
+          The number of past ledgers to serve to other peers that request historical
+          ledger data (or "full" for no limit).
+        '';
+        type = types.either types.int (types.enum ["full"]);
+        default = "full";
+      };
+
+      sntpServers = mkOption {
+        description = ''
+          IP address or domain of NTP servers to use for time synchronization.;
+        '';
+        type = types.listOf types.str;
+        default = [
+          "time.windows.com"
+          "time.apple.com"
+          "time.nist.gov"
+          "pool.ntp.org"
+        ];
+      };
+
+      logLevel = mkOption {
+        description = "Logging verbosity.";
+        type = types.enum ["debug" "error" "info"];
+        default = "error";
+      };
+
+      statsd = {
+        enable = mkEnableOption "statsd monitoring for rippled";
+
+        address = mkOption {
+          description = "The UDP address and port of the listening StatsD server.";
+          default = "127.0.0.1:8125";
+          type = types.str;
+        };
+
+        prefix = mkOption {
+          description = "A string prepended to each collected metric.";
+          default = "";
+          type = types.str;
+        };
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra lines to be added verbatim to the rippled.cfg configuration file.
+        '';
+      };
+
+      config = mkOption {
+        internal = true;
+        default = pkgs.writeText "rippled.conf" rippledCfg;
+        defaultText = literalDocBook "generated config file";
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.rippled = {
+        description = "Ripple server user";
+        isSystemUser = true;
+        group = "rippled";
+        home = cfg.databasePath;
+        createHome = true;
+      };
+    users.groups.rippled = {};
+
+    systemd.services.rippled = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/rippled --fg --conf ${cfg.config}";
+        User = "rippled";
+        Restart = "on-failure";
+        LimitNOFILE=10000;
+      };
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+  };
+}
diff --git a/nixos/modules/services/misc/rmfakecloud.nix b/nixos/modules/services/misc/rmfakecloud.nix
new file mode 100644
index 00000000000..fe522653c21
--- /dev/null
+++ b/nixos/modules/services/misc/rmfakecloud.nix
@@ -0,0 +1,147 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.rmfakecloud;
+  serviceDataDir = "/var/lib/rmfakecloud";
+
+in {
+  options = {
+    services.rmfakecloud = {
+      enable = mkEnableOption "rmfakecloud remarkable self-hosted cloud";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.rmfakecloud;
+        defaultText = literalExpression "pkgs.rmfakecloud";
+        description = ''
+          rmfakecloud package to use.
+
+          The default does not include the web user interface.
+        '';
+      };
+
+      storageUrl = mkOption {
+        type = types.str;
+        example = "https://local.appspot.com";
+        description = ''
+          URL used by the tablet to access the rmfakecloud service.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 3000;
+        description = ''
+          Listening port number.
+        '';
+      };
+
+      logLevel = mkOption {
+        type = types.enum [ "info" "debug" "warn" "error" ];
+        default = "info";
+        description = ''
+          Logging level.
+        '';
+      };
+
+      extraSettings = mkOption {
+        type = with types; attrsOf str;
+        default = { };
+        example = { DATADIR = "/custom/path/for/rmfakecloud/data"; };
+        description = ''
+          Extra settings in the form of a set of key-value pairs.
+          For tokens and secrets, use `environmentFile` instead.
+
+          Available settings are listed on
+          https://ddvk.github.io/rmfakecloud/install/configuration/.
+        '';
+      };
+
+      environmentFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "/etc/secrets/rmfakecloud.env";
+        description = ''
+          Path to an environment file loaded for the rmfakecloud service.
+
+          This can be used to securely store tokens and secrets outside of the
+          world-readable Nix store. Since this file is read by systemd, it may
+          have permission 0400 and be owned by root.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.rmfakecloud = {
+      description = "rmfakecloud remarkable self-hosted cloud";
+
+      environment = {
+        STORAGE_URL = cfg.storageUrl;
+        PORT = toString cfg.port;
+        LOGLEVEL = cfg.logLevel;
+      } // cfg.extraSettings;
+
+      preStart = ''
+        # Generate the secret key used to sign client session tokens.
+        # Replacing it invalidates the previously established sessions.
+        if [ -z "$JWT_SECRET_KEY" ] && [ ! -f jwt_secret_key ]; then
+          (umask 077; touch jwt_secret_key)
+          cat /dev/urandom | tr -cd '[:alnum:]' | head -c 48 >> jwt_secret_key
+        fi
+      '';
+
+      script = ''
+        if [ -z "$JWT_SECRET_KEY" ]; then
+          export JWT_SECRET_KEY="$(cat jwt_secret_key)"
+        fi
+
+        ${cfg.package}/bin/rmfakecloud
+      '';
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        EnvironmentFile =
+          mkIf (cfg.environmentFile != null) cfg.environmentFile;
+
+        AmbientCapabilities =
+          mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+
+        DynamicUser = true;
+        PrivateDevices = true;
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        CapabilityBoundingSet = [ "" ];
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        ProtectClock = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        WorkingDirectory = serviceDataDir;
+        StateDirectory = baseNameOf serviceDataDir;
+        UMask = 0027;
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ pacien ];
+}
diff --git a/nixos/modules/services/misc/safeeyes.nix b/nixos/modules/services/misc/safeeyes.nix
new file mode 100644
index 00000000000..638218d8bb0
--- /dev/null
+++ b/nixos/modules/services/misc/safeeyes.nix
@@ -0,0 +1,51 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.safeeyes;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.safeeyes = {
+
+      enable = mkEnableOption "the safeeyes OSGi service";
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.safeeyes ];
+
+    systemd.user.services.safeeyes = {
+      description = "Safeeyes";
+
+      wantedBy = [ "graphical-session.target" ];
+      partOf   = [ "graphical-session.target" ];
+
+      path = [ pkgs.alsa-utils ];
+
+      startLimitIntervalSec = 350;
+      startLimitBurst = 10;
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.safeeyes}/bin/safeeyes
+        '';
+        Restart = "on-failure";
+        RestartSec = 3;
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/misc/sdrplay.nix b/nixos/modules/services/misc/sdrplay.nix
new file mode 100644
index 00000000000..2801108f082
--- /dev/null
+++ b/nixos/modules/services/misc/sdrplay.nix
@@ -0,0 +1,35 @@
+{ config, lib, pkgs, ... }:
+with lib;
+{
+  options.services.sdrplayApi = {
+    enable = mkOption {
+      default = false;
+      example = true;
+      description = ''
+        Whether to enable the SDRplay API service and udev rules.
+
+        <note><para>
+          To enable integration with SoapySDR and GUI applications like gqrx create an overlay containing
+          <literal>soapysdr-with-plugins = super.soapysdr.override { extraPackages = [ super.soapysdrplay ]; };</literal>
+        </para></note>
+      '';
+      type = lib.types.bool;
+    };
+  };
+
+  config = mkIf config.services.sdrplayApi.enable {
+    systemd.services.sdrplayApi = {
+      description = "SDRplay API Service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.sdrplay}/bin/sdrplay_apiService";
+        DynamicUser = true;
+        Restart = "on-failure";
+        RestartSec = "1s";
+      };
+    };
+    services.udev.packages = [ pkgs.sdrplay ];
+
+  };
+}
diff --git a/nixos/modules/services/misc/serviio.nix b/nixos/modules/services/misc/serviio.nix
new file mode 100644
index 00000000000..0ead6a81691
--- /dev/null
+++ b/nixos/modules/services/misc/serviio.nix
@@ -0,0 +1,87 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.serviio;
+
+  serviioStart = pkgs.writeScript "serviio.sh" ''
+    #!${pkgs.bash}/bin/sh
+
+    SERVIIO_HOME=${pkgs.serviio}
+
+    # Setup the classpath
+    SERVIIO_CLASS_PATH="$SERVIIO_HOME/lib/*:$SERVIIO_HOME/config"
+
+    # Setup Serviio specific properties
+    JAVA_OPTS="-Djava.net.preferIPv4Stack=true -Djava.awt.headless=true -Dorg.restlet.engine.loggerFacadeClass=org.restlet.ext.slf4j.Slf4jLoggerFacade
+               -Dderby.system.home=${cfg.dataDir}/library -Dserviio.home=${cfg.dataDir} -Dffmpeg.location=${pkgs.ffmpeg}/bin/ffmpeg -Ddcraw.location=${pkgs.dcraw}/bin/dcraw"
+
+    # Execute the JVM in the foreground
+    exec ${pkgs.jre}/bin/java -Xmx512M -Xms20M -XX:+UseG1GC -XX:GCTimeRatio=1 -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=20 $JAVA_OPTS -classpath "$SERVIIO_CLASS_PATH" org.serviio.MediaServer "$@"
+  '';
+
+in {
+
+  ###### interface
+  options = {
+    services.serviio = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the Serviio Media Server.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/serviio";
+        description = ''
+          The directory where serviio stores its state, data, etc.
+        '';
+      };
+
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.serviio = {
+      description = "Serviio Media Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.serviio ];
+      serviceConfig = {
+        User = "serviio";
+        Group = "serviio";
+        ExecStart = "${serviioStart}";
+        ExecStop = "${serviioStart} -stop";
+      };
+    };
+
+    users.users.serviio =
+      { group = "serviio";
+        home = cfg.dataDir;
+        description = "Serviio Media Server User";
+        createHome = true;
+        isSystemUser = true;
+      };
+
+    users.groups.serviio = { };
+
+    networking.firewall = {
+      allowedTCPPorts = [
+        8895  # serve UPnP responses
+        23423 # console
+        23424 # mediabrowser
+      ];
+      allowedUDPPorts = [
+        1900 # UPnP service discovey
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sickbeard.nix b/nixos/modules/services/misc/sickbeard.nix
new file mode 100644
index 00000000000..a3db9928634
--- /dev/null
+++ b/nixos/modules/services/misc/sickbeard.nix
@@ -0,0 +1,95 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  name = "sickbeard";
+
+  cfg = config.services.sickbeard;
+  opt = options.services.sickbeard;
+  sickbeard = cfg.package;
+
+in
+{
+
+  ###### interface
+
+  options = {
+    services.sickbeard = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the sickbeard server.";
+      };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.sickbeard;
+        defaultText = literalExpression "pkgs.sickbeard";
+        example = literalExpression "pkgs.sickrage";
+        description =''
+          Enable <literal>pkgs.sickrage</literal> or <literal>pkgs.sickgear</literal>
+          as an alternative to SickBeard
+        '';
+      };
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/${name}";
+        description = "Path where to store data files.";
+      };
+      configFile = mkOption {
+        type = types.path;
+        default = "${cfg.dataDir}/config.ini";
+        defaultText = literalExpression ''"''${config.${opt.dataDir}}/config.ini"'';
+        description = "Path to config file.";
+      };
+      port = mkOption {
+        type = types.ints.u16;
+        default = 8081;
+        description = "Port to bind to.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = name;
+        description = "User to run the service as";
+      };
+      group = mkOption {
+        type = types.str;
+        default = name;
+        description = "Group to run the service as";
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users = optionalAttrs (cfg.user == name) {
+      ${name} = {
+        uid = config.ids.uids.sickbeard;
+        group = cfg.group;
+        description = "sickbeard user";
+        home = cfg.dataDir;
+        createHome = true;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == name) {
+      ${name}.gid = config.ids.gids.sickbeard;
+    };
+
+    systemd.services.sickbeard = {
+      description = "Sickbeard Server";
+      wantedBy    = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${sickbeard}/bin/${sickbeard.pname} --datadir ${cfg.dataDir} --config ${cfg.configFile} --port ${toString cfg.port}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/signald.nix b/nixos/modules/services/misc/signald.nix
new file mode 100644
index 00000000000..4cd34e4326d
--- /dev/null
+++ b/nixos/modules/services/misc/signald.nix
@@ -0,0 +1,105 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.signald;
+  dataDir = "/var/lib/signald";
+  defaultUser = "signald";
+in
+{
+  options.services.signald = {
+    enable = mkEnableOption "the signald service";
+
+    user = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = "User under which signald runs.";
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = "Group under which signald runs.";
+    };
+
+    socketPath = mkOption {
+      type = types.str;
+      default = "/run/signald/signald.sock";
+      description = "Path to the signald socket";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} = {
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == defaultUser) {
+      ${defaultUser} = { };
+    };
+
+    systemd.services.signald = {
+      description = "A daemon for interacting with the Signal Private Messenger";
+      wants = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.signald}/bin/signald -d ${dataDir} -s ${cfg.socketPath}";
+        Restart = "on-failure";
+        StateDirectory = "signald";
+        RuntimeDirectory = "signald";
+        StateDirectoryMode = "0750";
+        RuntimeDirectoryMode = "0750";
+
+        BindReadOnlyPaths = [
+          "/nix/store"
+          "-/etc/resolv.conf"
+          "-/etc/nsswitch.conf"
+          "-/etc/hosts"
+          "-/etc/localtime"
+        ];
+        CapabilityBoundingSet = "";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        # Use a static user so other applications can access the files
+        #DynamicUser = true;
+        LockPersonality = true;
+        # Needed for java
+        #MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        # Needs network access
+        #PrivateNetwork = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        # Would re-mount paths ignored by temporary root
+        #ProtectSystem = "strict";
+        ProtectControlGroups = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
+        TemporaryFileSystem = "/:ro";
+        # Does not work well with the temporary root
+        #UMask = "0066";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/siproxd.nix b/nixos/modules/services/misc/siproxd.nix
new file mode 100644
index 00000000000..20fe0793b84
--- /dev/null
+++ b/nixos/modules/services/misc/siproxd.nix
@@ -0,0 +1,179 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.siproxd;
+
+  conf = ''
+    daemonize = 0
+    rtp_proxy_enable = 1
+    user = siproxd
+    if_inbound  = ${cfg.ifInbound}
+    if_outbound = ${cfg.ifOutbound}
+    sip_listen_port = ${toString cfg.sipListenPort}
+    rtp_port_low    = ${toString cfg.rtpPortLow}
+    rtp_port_high   = ${toString cfg.rtpPortHigh}
+    rtp_dscp        = ${toString cfg.rtpDscp}
+    sip_dscp        = ${toString cfg.sipDscp}
+    ${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 ""}
+    ${cfg.extraConfig}
+  '';
+
+  confFile = builtins.toFile "siproxd.conf" conf;
+
+in
+{
+  ##### interface
+
+  options = {
+
+    services.siproxd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the Siproxd SIP
+          proxy/masquerading daemon.
+        '';
+      };
+
+      ifInbound = mkOption {
+        type = types.str;
+        example = "eth0";
+        description = "Local network interface";
+      };
+
+      ifOutbound = mkOption {
+        type = types.str;
+        example = "ppp0";
+        description = "Public network interface";
+      };
+
+      hostsAllowReg = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "192.168.1.0/24" "192.168.2.0/24" ];
+        description = ''
+          Acess control list for incoming SIP registrations.
+        '';
+      };
+
+      hostsAllowSip = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "123.45.0.0/16" "123.46.0.0/16" ];
+        description = ''
+          Acess control list for incoming SIP traffic.
+        '';
+      };
+
+      hostsDenySip = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "10.0.0.0/8" "11.0.0.0/8" ];
+        description = ''
+          Acess control list for denying incoming
+          SIP registrations and traffic.
+        '';
+      };
+
+      sipListenPort = mkOption {
+        type = types.int;
+        default = 5060;
+        description = ''
+          Port to listen for incoming SIP messages.
+        '';
+      };
+
+      rtpPortLow = mkOption {
+        type = types.int;
+        default = 7070;
+        description = ''
+         Bottom of UDP port range for incoming and outgoing RTP traffic
+        '';
+      };
+
+      rtpPortHigh = mkOption {
+        type = types.int;
+        default = 7089;
+        description = ''
+         Top of UDP port range for incoming and outgoing RTP traffic
+        '';
+      };
+
+      rtpTimeout = mkOption {
+        type = types.int;
+        default = 300;
+        description = ''
+          Timeout for an RTP stream. If for the specified
+          number of seconds no data is relayed on an active
+          stream, it is considered dead and will be killed.
+        '';
+      };
+
+      rtpDscp = mkOption {
+        type = types.int;
+        default = 46;
+        description = ''
+          DSCP (differentiated services) value to be assigned
+          to RTP packets. Allows QOS aware routers to handle
+          different types traffic with different priorities.
+        '';
+      };
+
+      sipDscp = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          DSCP (differentiated services) value to be assigned
+          to SIP packets. Allows QOS aware routers to handle
+          different types traffic with different priorities.
+        '';
+      };
+
+      passwordFile = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Path to per-user password file.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration to add to siproxd configuration.
+        '';
+      };
+
+    };
+
+  };
+
+  ##### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.siproxyd = {
+      uid = config.ids.uids.siproxd;
+    };
+
+    systemd.services.siproxd = {
+      description = "SIP proxy/masquerading daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.siproxd}/sbin/siproxd -c ${confFile}";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/snapper.nix b/nixos/modules/services/misc/snapper.nix
new file mode 100644
index 00000000000..3c3f6c4d641
--- /dev/null
+++ b/nixos/modules/services/misc/snapper.nix
@@ -0,0 +1,187 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.snapper;
+in
+
+{
+  options.services.snapper = {
+
+    snapshotRootOnBoot = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to snapshot root on boot
+      '';
+    };
+
+    snapshotInterval = mkOption {
+      type = types.str;
+      default = "hourly";
+      description = ''
+        Snapshot interval.
+
+        The format is described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.
+      '';
+    };
+
+    cleanupInterval = mkOption {
+      type = types.str;
+      default = "1d";
+      description = ''
+        Cleanup interval.
+
+        The format is described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.
+      '';
+    };
+
+    filters = mkOption {
+      type = types.nullOr types.lines;
+      default = null;
+      description = ''
+        Global display difference filter. See man:snapper(8) for more details.
+      '';
+    };
+
+    configs = mkOption {
+      default = { };
+      example = literalExpression ''
+        {
+          home = {
+            subvolume = "/home";
+            extraConfig = '''
+              ALLOW_USERS="alice"
+              TIMELINE_CREATE=yes
+              TIMELINE_CLEANUP=yes
+            ''';
+          };
+        }
+      '';
+
+      description = ''
+        Subvolume configuration
+      '';
+
+      type = types.attrsOf (types.submodule {
+        options = {
+          subvolume = mkOption {
+            type = types.path;
+            description = ''
+              Path of the subvolume or mount point.
+              This path is a subvolume and has to contain a subvolume named
+              .snapshots.
+              See also man:snapper(8) section PERMISSIONS.
+            '';
+          };
+
+          fstype = mkOption {
+            type = types.enum [ "btrfs" ];
+            default = "btrfs";
+            description = ''
+              Filesystem type. Only btrfs is stable and tested.
+            '';
+          };
+
+          extraConfig = mkOption {
+            type = types.lines;
+            default = "";
+            description = ''
+              Additional configuration next to SUBVOLUME and FSTYPE.
+              See man:snapper-configs(5).
+            '';
+          };
+        };
+      });
+    };
+  };
+
+  config = mkIf (cfg.configs != {}) (let
+    documentation = [ "man:snapper(8)" "man:snapper-configs(5)" ];
+  in {
+
+    environment = {
+
+      systemPackages = [ pkgs.snapper ];
+
+      # Note: snapper/config-templates/default is only needed for create-config
+      #       which is not the NixOS way to configure.
+      etc = {
+
+        "sysconfig/snapper".text = ''
+          SNAPPER_CONFIGS="${lib.concatStringsSep " " (builtins.attrNames cfg.configs)}"
+        '';
+
+      }
+      // (mapAttrs' (name: subvolume: nameValuePair "snapper/configs/${name}" ({
+        text = ''
+          ${subvolume.extraConfig}
+          FSTYPE="${subvolume.fstype}"
+          SUBVOLUME="${subvolume.subvolume}"
+        '';
+      })) cfg.configs)
+      // (lib.optionalAttrs (cfg.filters != null) {
+        "snapper/filters/default.txt".text = cfg.filters;
+      });
+
+    };
+
+    services.dbus.packages = [ pkgs.snapper ];
+
+    systemd.services.snapperd = {
+      description = "DBus interface for snapper";
+      inherit documentation;
+      serviceConfig = {
+        Type = "dbus";
+        BusName = "org.opensuse.Snapper";
+        ExecStart = "${pkgs.snapper}/bin/snapperd";
+        CapabilityBoundingSet = "CAP_DAC_OVERRIDE CAP_FOWNER CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_SYS_ADMIN CAP_SYS_MODULE CAP_IPC_LOCK CAP_SYS_NICE";
+        LockPersonality = true;
+        NoNewPrivileges = false;
+        PrivateNetwork = true;
+        ProtectHostname = true;
+        RestrictAddressFamilies = "AF_UNIX";
+        RestrictRealtime = true;
+      };
+    };
+
+    systemd.services.snapper-timeline = {
+      description = "Timeline of Snapper Snapshots";
+      inherit documentation;
+      requires = [ "local-fs.target" ];
+      serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --timeline";
+      startAt = cfg.snapshotInterval;
+    };
+
+    systemd.services.snapper-cleanup = {
+      description = "Cleanup of Snapper Snapshots";
+      inherit documentation;
+      serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --cleanup";
+    };
+
+    systemd.timers.snapper-cleanup = {
+      description = "Cleanup of Snapper Snapshots";
+      inherit documentation;
+      wantedBy = [ "timers.target" ];
+      requires = [ "local-fs.target" ];
+      timerConfig.OnBootSec = "10m";
+      timerConfig.OnUnitActiveSec = cfg.cleanupInterval;
+    };
+
+    systemd.services.snapper-boot = lib.optionalAttrs cfg.snapshotRootOnBoot {
+      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";
+      requires = [ "local-fs.target" ];
+      wantedBy = [ "multi-user.target" ];
+      unitConfig.ConditionPathExists = "/etc/snapper/configs/root";
+    };
+
+  });
+}
diff --git a/nixos/modules/services/misc/sonarr.nix b/nixos/modules/services/misc/sonarr.nix
new file mode 100644
index 00000000000..77c7f0582d0
--- /dev/null
+++ b/nixos/modules/services/misc/sonarr.nix
@@ -0,0 +1,76 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.sonarr;
+in
+{
+  options = {
+    services.sonarr = {
+      enable = mkEnableOption "Sonarr";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/sonarr/.config/NzbDrone";
+        description = "The directory where Sonarr stores its data files.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for the Sonarr web interface
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "sonarr";
+        description = "User account under which Sonaar runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "sonarr";
+        description = "Group under which Sonaar runs.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.sonarr = {
+      description = "Sonarr";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.sonarr}/bin/NzbDrone -nobrowser -data='${cfg.dataDir}'";
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 8989 ];
+    };
+
+    users.users = mkIf (cfg.user == "sonarr") {
+      sonarr = {
+        group = cfg.group;
+        home = cfg.dataDir;
+        uid = config.ids.uids.sonarr;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "sonarr") {
+      sonarr.gid = config.ids.gids.sonarr;
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/builds.nix b/nixos/modules/services/misc/sourcehut/builds.nix
new file mode 100644
index 00000000000..685a132d350
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/builds.nix
@@ -0,0 +1,236 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
+  scfg = cfg.builds;
+  rcfg = config.services.redis;
+  iniKey = "builds.sr.ht";
+
+  drv = pkgs.sourcehut.buildsrht;
+in
+{
+  options.services.sourcehut.builds = {
+    user = mkOption {
+      type = types.str;
+      default = "buildsrht";
+      description = ''
+        User for builds.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5002;
+      description = ''
+        Port on which the "builds" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "builds.sr.ht";
+      description = ''
+        PostgreSQL database name for builds.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/buildsrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/buildsrht"'';
+      description = ''
+        State path for builds.sr.ht.
+      '';
+    };
+
+    enableWorker = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Run workers for builds.sr.ht.
+      '';
+    };
+
+    images = mkOption {
+      type = types.attrsOf (types.attrsOf (types.attrsOf types.package));
+      default = { };
+      example = lib.literalExpression ''(let
+          # Pinning unstable to allow usage with flakes and limit rebuilds.
+          pkgs_unstable = builtins.fetchGit {
+              url = "https://github.com/NixOS/nixpkgs";
+              rev = "ff96a0fa5635770390b184ae74debea75c3fd534";
+              ref = "nixos-unstable";
+          };
+          image_from_nixpkgs = pkgs_unstable: (import ("''${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
+            pkgs = (import pkgs_unstable {});
+          });
+        in
+        {
+          nixos.unstable.x86_64 = image_from_nixpkgs pkgs_unstable;
+        }
+      )'';
+      description = ''
+        Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2.
+      '';
+    };
+
+  };
+
+  config = with scfg; let
+    image_dirs = lib.lists.flatten (
+      lib.attrsets.mapAttrsToList
+        (distro: revs:
+          lib.attrsets.mapAttrsToList
+            (rev: archs:
+              lib.attrsets.mapAttrsToList
+                (arch: image:
+                  pkgs.runCommand "buildsrht-images" { } ''
+                    mkdir -p $out/${distro}/${rev}/${arch}
+                    ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
+                  '')
+                archs)
+            revs)
+        scfg.images);
+    image_dir_pre = pkgs.symlinkJoin {
+      name = "builds.sr.ht-worker-images-pre";
+      paths = image_dirs ++ [
+        "${pkgs.sourcehut.buildsrht}/lib/images"
+      ];
+    };
+    image_dir = pkgs.runCommand "builds.sr.ht-worker-images" { } ''
+      mkdir -p $out/images
+      cp -Lr ${image_dir_pre}/* $out/images
+    '';
+  in
+  lib.mkIf (cfg.enable && elem "builds" cfg.services) {
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          extraGroups = lib.optionals cfg.builds.enableWorker [ "docker" ];
+          description = "builds.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0755 ${user} ${user} -"
+      ] ++ (lib.optionals cfg.builds.enableWorker
+        [ "d ${statePath}/logs 0775 ${user} ${user} - -" ]
+      );
+
+      services = {
+        buildsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey
+          {
+            after = [ "postgresql.service" "network.target" ];
+            requires = [ "postgresql.service" ];
+            wantedBy = [ "multi-user.target" ];
+
+            description = "builds.sr.ht website service";
+
+            serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+
+            # Hack to bypass this hack: https://git.sr.ht/~sircmpwn/core.sr.ht/tree/master/item/srht-update-profiles#L6
+          } // { preStart = " "; };
+
+        buildsrht-worker = {
+          enable = scfg.enableWorker;
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+          partOf = [ "buildsrht.service" ];
+          description = "builds.sr.ht worker service";
+          path = [ pkgs.openssh pkgs.docker ];
+          preStart = let qemuPackage = pkgs.qemu_kvm;
+          in ''
+            if [[ "$(docker images -q qemu:latest 2> /dev/null)" == "" || "$(cat ${statePath}/docker-image-qemu 2> /dev/null || true)" != "${qemuPackage.version}" ]]; then
+              # Create and import qemu:latest image for docker
+              ${
+                pkgs.dockerTools.streamLayeredImage {
+                  name = "qemu";
+                  tag = "latest";
+                  contents = [ qemuPackage ];
+                }
+              } | docker load
+              # Mark down current package version
+              printf "%s" "${qemuPackage.version}" > ${statePath}/docker-image-qemu
+            fi
+          '';
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Group = "nginx";
+            Restart = "always";
+          };
+          serviceConfig.ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL builds.sr.ht is being served at (protocol://domain)
+      "builds.sr.ht".origin = mkDefault "http://builds.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "builds.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "builds.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "builds.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "builds.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # builds.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "builds.sr.ht".oauth-client-id = mkDefault null;
+      "builds.sr.ht".oauth-client-secret = mkDefault null;
+      # The redis connection used for the celery worker
+      "builds.sr.ht".redis = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/3";
+      # The shell used for ssh
+      "builds.sr.ht".shell = mkDefault "runner-shell";
+      # Register the builds.sr.ht dispatcher
+      "git.sr.ht::dispatch".${builtins.unsafeDiscardStringContext "${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys"} = mkDefault "${user}:${user}";
+
+      # Location for build logs, images, and control command
+    } // lib.attrsets.optionalAttrs scfg.enableWorker {
+      # Default worker stores logs that are accessible via this address:port
+      "builds.sr.ht::worker".name = mkDefault "127.0.0.1:5020";
+      "builds.sr.ht::worker".buildlogs = mkDefault "${scfg.statePath}/logs";
+      "builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
+      "builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
+      "builds.sr.ht::worker".timeout = mkDefault "3m";
+    };
+
+    services.nginx.virtualHosts."logs.${cfg.originBase}" =
+      if scfg.enableWorker then {
+        listen = with builtins; let address = split ":" cfg.settings."builds.sr.ht::worker".name;
+        in [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
+        locations."/logs".root = "${scfg.statePath}";
+      } else { };
+
+    services.nginx.virtualHosts."builds.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.buildsrht}/${pkgs.sourcehut.python.sitePackages}/buildsrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/default.nix b/nixos/modules/services/misc/sourcehut/default.nix
new file mode 100644
index 00000000000..21551d7d5f0
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/default.nix
@@ -0,0 +1,1386 @@
+{ config, pkgs, lib, ... }:
+with lib;
+let
+  inherit (config.services) nginx postfix postgresql redis;
+  inherit (config.users) users groups;
+  cfg = config.services.sourcehut;
+  domain = cfg.settings."sr.ht".global-domain;
+  settingsFormat = pkgs.formats.ini {
+    listToValue = concatMapStringsSep "," (generators.mkValueStringDefault {});
+    mkKeyValue = k: v:
+      if v == null then ""
+      else generators.mkKeyValueDefault {
+        mkValueString = v:
+          if v == true then "yes"
+          else if v == false then "no"
+          else generators.mkValueStringDefault {} v;
+      } "=" k v;
+  };
+  configIniOfService = srv: settingsFormat.generate "sourcehut-${srv}-config.ini"
+    # Each service needs access to only a subset of sections (and secrets).
+    (filterAttrs (k: v: v != null)
+    (mapAttrs (section: v:
+      let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht(::.*)?$" section; in
+      if srvMatch == null # Include sections shared by all services
+      || head srvMatch == srv # Include sections for the service being configured
+      then v
+      # Enable Web links and integrations between services.
+      else if tail srvMatch == [ null ] && elem (head srvMatch) cfg.services
+      then {
+        inherit (v) origin;
+        # mansrht crashes without it
+        oauth-client-id = v.oauth-client-id or null;
+      }
+      # Drop sub-sections of other services
+      else null)
+    (recursiveUpdate cfg.settings {
+      # Those paths are mounted using BindPaths= or BindReadOnlyPaths=
+      # for services needing access to them.
+      "builds.sr.ht::worker".buildlogs = "/var/log/sourcehut/buildsrht-worker";
+      "git.sr.ht".post-update-script = "/usr/bin/gitsrht-update-hook";
+      "git.sr.ht".repos = "/var/lib/sourcehut/gitsrht/repos";
+      "hg.sr.ht".changegroup-script = "/usr/bin/hgsrht-hook-changegroup";
+      "hg.sr.ht".repos = "/var/lib/sourcehut/hgsrht/repos";
+      # Making this a per service option despite being in a global section,
+      # so that it uses the redis-server used by the service.
+      "sr.ht".redis-host = cfg.${srv}.redis.host;
+    })));
+  commonServiceSettings = srv: {
+    origin = mkOption {
+      description = "URL ${srv}.sr.ht is being served at (protocol://domain)";
+      type = types.str;
+      default = "https://${srv}.${domain}";
+      defaultText = "https://${srv}.example.com";
+    };
+    debug-host = mkOption {
+      description = "Address to bind the debug server to.";
+      type = with types; nullOr str;
+      default = null;
+    };
+    debug-port = mkOption {
+      description = "Port to bind the debug server to.";
+      type = with types; nullOr str;
+      default = null;
+    };
+    connection-string = mkOption {
+      description = "SQLAlchemy connection string for the database.";
+      type = types.str;
+      default = "postgresql:///localhost?user=${srv}srht&host=/run/postgresql";
+    };
+    migrate-on-upgrade = mkEnableOption "automatic migrations on package upgrade" // { default = true; };
+    oauth-client-id = mkOption {
+      description = "${srv}.sr.ht's OAuth client id for meta.sr.ht.";
+      type = types.str;
+    };
+    oauth-client-secret = mkOption {
+      description = "${srv}.sr.ht's OAuth client secret for meta.sr.ht.";
+      type = types.path;
+      apply = s: "<" + toString s;
+    };
+  };
+
+  # Specialized python containing all the modules
+  python = pkgs.sourcehut.python.withPackages (ps: with ps; [
+    gunicorn
+    eventlet
+    # For monitoring Celery: sudo -u listssrht celery --app listssrht.process -b redis+socket:///run/redis-sourcehut/redis.sock?virtual_host=5 flower
+    flower
+    # Sourcehut services
+    srht
+    buildsrht
+    dispatchsrht
+    gitsrht
+    hgsrht
+    hubsrht
+    listssrht
+    mansrht
+    metasrht
+    # Not a python package
+    #pagessrht
+    pastesrht
+    todosrht
+  ]);
+  mkOptionNullOrStr = description: mkOption {
+    inherit description;
+    type = with types; nullOr str;
+    default = null;
+  };
+in
+{
+  options.services.sourcehut = {
+    enable = mkEnableOption ''
+      sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
+      task dispatching, wiki and account management services
+    '';
+
+    services = mkOption {
+      type = with types; listOf (enum
+        [ "builds" "dispatch" "git" "hg" "hub" "lists" "man" "meta" "pages" "paste" "todo" ]);
+      defaultText = "locally enabled services";
+      description = ''
+        Services that may be displayed as links in the title bar of the Web interface.
+      '';
+    };
+
+    listenAddress = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = "Address to bind to.";
+    };
+
+    python = mkOption {
+      internal = true;
+      type = types.package;
+      default = python;
+      description = ''
+        The python package to use. It should contain references to the *srht modules and also
+        gunicorn.
+      '';
+    };
+
+    minio = {
+      enable = mkEnableOption ''local minio integration'';
+    };
+
+    nginx = {
+      enable = mkEnableOption ''local nginx integration'';
+      virtualHost = mkOption {
+        type = types.attrs;
+        default = {};
+        description = "Virtual-host configuration merged with all Sourcehut's virtual-hosts.";
+      };
+    };
+
+    postfix = {
+      enable = mkEnableOption ''local postfix integration'';
+    };
+
+    postgresql = {
+      enable = mkEnableOption ''local postgresql integration'';
+    };
+
+    redis = {
+      enable = mkEnableOption ''local redis integration in a dedicated redis-server'';
+    };
+
+    settings = mkOption {
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+        options."sr.ht" = {
+          global-domain = mkOption {
+            description = "Global domain name.";
+            type = types.str;
+            example = "example.com";
+          };
+          environment = mkOption {
+            description = "Values other than \"production\" adds a banner to each page.";
+            type = types.enum [ "development" "production" ];
+            default = "development";
+          };
+          network-key = mkOption {
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to a secret key to encrypt internal messages with. Use <code>srht-keygen network</code> to
+              generate this key. It must be consistent between all services and nodes.
+            '';
+            type = types.path;
+            apply = s: "<" + toString s;
+          };
+          owner-email = mkOption {
+            description = "Owner's email.";
+            type = types.str;
+            default = "contact@example.com";
+          };
+          owner-name = mkOption {
+            description = "Owner's name.";
+            type = types.str;
+            default = "John Doe";
+          };
+          site-blurb = mkOption {
+            description = "Blurb for your site.";
+            type = types.str;
+            default = "the hacker's forge";
+          };
+          site-info = mkOption {
+            description = "The top-level info page for your site.";
+            type = types.str;
+            default = "https://sourcehut.org";
+          };
+          service-key = mkOption {
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to a key used for encrypting session cookies. Use <code>srht-keygen service</code> to
+              generate the service key. This must be shared between each node of the same
+              service (e.g. git1.sr.ht and git2.sr.ht), but different services may use
+              different keys. If you configure all of your services with the same
+              config.ini, you may use the same service-key for all of them.
+            '';
+            type = types.path;
+            apply = s: "<" + toString s;
+          };
+          site-name = mkOption {
+            description = "The name of your network of sr.ht-based sites.";
+            type = types.str;
+            default = "sourcehut";
+          };
+          source-url = mkOption {
+            description = "The source code for your fork of sr.ht.";
+            type = types.str;
+            default = "https://git.sr.ht/~sircmpwn/srht";
+          };
+        };
+        options.mail = {
+          smtp-host = mkOptionNullOrStr "Outgoing SMTP host.";
+          smtp-port = mkOption {
+            description = "Outgoing SMTP port.";
+            type = with types; nullOr port;
+            default = null;
+          };
+          smtp-user = mkOptionNullOrStr "Outgoing SMTP user.";
+          smtp-password = mkOptionNullOrStr "Outgoing SMTP password.";
+          smtp-from = mkOptionNullOrStr "Outgoing SMTP FROM.";
+          error-to = mkOptionNullOrStr "Address receiving application exceptions";
+          error-from = mkOptionNullOrStr "Address sending application exceptions";
+          pgp-privkey = mkOptionNullOrStr ''
+            An absolute file path (which should be outside the Nix-store)
+            to an OpenPGP private key.
+
+            Your PGP key information (DO NOT mix up pub and priv here)
+            You must remove the password from your secret key, if present.
+            You can do this with <code>gpg --edit-key [key-id]</code>,
+            then use the <code>passwd</code> command and do not enter a new password.
+          '';
+          pgp-pubkey = mkOptionNullOrStr "OpenPGP public key.";
+          pgp-key-id = mkOptionNullOrStr "OpenPGP key identifier.";
+        };
+        options.objects = {
+          s3-upstream = mkOption {
+            description = "Configure the S3-compatible object storage service.";
+            type = with types; nullOr str;
+            default = null;
+          };
+          s3-access-key = mkOption {
+            description = "Access key to the S3-compatible object storage service";
+            type = with types; nullOr str;
+            default = null;
+          };
+          s3-secret-key = mkOption {
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to the secret key of the S3-compatible object storage service.
+            '';
+            type = with types; nullOr path;
+            default = null;
+            apply = mapNullable (s: "<" + toString s);
+          };
+        };
+        options.webhooks = {
+          private-key = mkOption {
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to a base64-encoded Ed25519 key for signing webhook payloads.
+              This should be consistent for all *.sr.ht sites,
+              as this key will be used to verify signatures
+              from other sites in your network.
+              Use the <code>srht-keygen webhook</code> command to generate a key.
+            '';
+            type = types.path;
+            apply = s: "<" + toString s;
+          };
+        };
+
+        options."dispatch.sr.ht" = commonServiceSettings "dispatch" // {
+        };
+        options."dispatch.sr.ht::github" = {
+          oauth-client-id = mkOptionNullOrStr "OAuth client id.";
+          oauth-client-secret = mkOptionNullOrStr "OAuth client secret.";
+        };
+        options."dispatch.sr.ht::gitlab" = {
+          enabled = mkEnableOption "GitLab integration";
+          canonical-upstream = mkOption {
+            type = types.str;
+            description = "Canonical upstream.";
+            default = "gitlab.com";
+          };
+          repo-cache = mkOption {
+            type = types.str;
+            description = "Repository cache directory.";
+            default = "./repo-cache";
+          };
+          "gitlab.com" = mkOption {
+            type = with types; nullOr str;
+            description = "GitLab id and secret.";
+            default = null;
+            example = "GitLab:application id:secret";
+          };
+        };
+
+        options."builds.sr.ht" = commonServiceSettings "builds" // {
+          allow-free = mkEnableOption "nonpaying users to submit builds";
+          redis = mkOption {
+            description = "The Redis connection used for the Celery worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-buildsrht/redis.sock?virtual_host=2";
+          };
+          shell = mkOption {
+            description = ''
+              Scripts used to launch on SSH connection.
+              <literal>/usr/bin/master-shell</literal> on master,
+              <literal>/usr/bin/runner-shell</literal> on runner.
+              If master and worker are on the same system
+              set to <literal>/usr/bin/runner-shell</literal>.
+            '';
+            type = types.enum ["/usr/bin/master-shell" "/usr/bin/runner-shell"];
+            default = "/usr/bin/master-shell";
+          };
+        };
+        options."builds.sr.ht::worker" = {
+          bind-address = mkOption {
+            description = ''
+              HTTP bind address for serving local build information/monitoring.
+            '';
+            type = types.str;
+            default = "localhost:8080";
+          };
+          buildlogs = mkOption {
+            description = "Path to write build logs.";
+            type = types.str;
+            default = "/var/log/sourcehut/buildsrht-worker";
+          };
+          name = mkOption {
+            description = ''
+              Listening address and listening port
+              of the build runner (with HTTP port if not 80).
+            '';
+            type = types.str;
+            default = "localhost:5020";
+          };
+          timeout = mkOption {
+            description = ''
+              Max build duration.
+              See <link xlink:href="https://golang.org/pkg/time/#ParseDuration"/>.
+            '';
+            type = types.str;
+            default = "3m";
+          };
+        };
+
+        options."git.sr.ht" = commonServiceSettings "git" // {
+          outgoing-domain = mkOption {
+            description = "Outgoing domain.";
+            type = types.str;
+            default = "https://git.localhost.localdomain";
+          };
+          post-update-script = mkOption {
+            description = ''
+              A post-update script which is installed in every git repo.
+              This setting is propagated to newer and existing repositories.
+            '';
+            type = types.path;
+            default = "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
+            defaultText = "\${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
+          };
+          repos = mkOption {
+            description = ''
+              Path to git repositories on disk.
+              If changing the default, you must ensure that
+              the gitsrht's user as read and write access to it.
+            '';
+            type = types.str;
+            default = "/var/lib/sourcehut/gitsrht/repos";
+          };
+          webhooks = mkOption {
+            description = "The Redis connection used for the webhooks worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-gitsrht/redis.sock?virtual_host=1";
+          };
+        };
+        options."git.sr.ht::api" = {
+          internal-ipnet = mkOption {
+            description = ''
+              Set of IP subnets which are permitted to utilize internal API
+              authentication. This should be limited to the subnets
+              from which your *.sr.ht services are running.
+              See <xref linkend="opt-services.sourcehut.listenAddress"/>.
+            '';
+            type = with types; listOf str;
+            default = [ "127.0.0.0/8" "::1/128" ];
+          };
+        };
+
+        options."hg.sr.ht" = commonServiceSettings "hg" // {
+          changegroup-script = mkOption {
+            description = ''
+              A changegroup script which is installed in every mercurial repo.
+              This setting is propagated to newer and existing repositories.
+            '';
+            type = types.str;
+            default = "${cfg.python}/bin/hgsrht-hook-changegroup";
+            defaultText = "\${cfg.python}/bin/hgsrht-hook-changegroup";
+          };
+          repos = mkOption {
+            description = ''
+              Path to mercurial repositories on disk.
+              If changing the default, you must ensure that
+              the hgsrht's user as read and write access to it.
+            '';
+            type = types.str;
+            default = "/var/lib/sourcehut/hgsrht/repos";
+          };
+          srhtext = mkOptionNullOrStr ''
+            Path to the srht mercurial extension
+            (defaults to where the hgsrht code is)
+          '';
+          clone_bundle_threshold = mkOption {
+            description = ".hg/store size (in MB) past which the nightly job generates clone bundles.";
+            type = types.ints.unsigned;
+            default = 50;
+          };
+          hg_ssh = mkOption {
+            description = "Path to hg-ssh (if not in $PATH).";
+            type = types.str;
+            default = "${pkgs.mercurial}/bin/hg-ssh";
+            defaultText = "\${pkgs.mercurial}/bin/hg-ssh";
+          };
+          webhooks = mkOption {
+            description = "The Redis connection used for the webhooks worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-hgsrht/redis.sock?virtual_host=1";
+          };
+        };
+
+        options."hub.sr.ht" = commonServiceSettings "hub" // {
+        };
+
+        options."lists.sr.ht" = commonServiceSettings "lists" // {
+          allow-new-lists = mkEnableOption "Allow creation of new lists.";
+          notify-from = mkOption {
+            description = "Outgoing email for notifications generated by users.";
+            type = types.str;
+            default = "lists-notify@localhost.localdomain";
+          };
+          posting-domain = mkOption {
+            description = "Posting domain.";
+            type = types.str;
+            default = "lists.localhost.localdomain";
+          };
+          redis = mkOption {
+            description = "The Redis connection used for the Celery worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-listssrht/redis.sock?virtual_host=2";
+          };
+          webhooks = mkOption {
+            description = "The Redis connection used for the webhooks worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-listssrht/redis.sock?virtual_host=1";
+          };
+        };
+        options."lists.sr.ht::worker" = {
+          reject-mimetypes = mkOption {
+            description = ''
+              Comma-delimited list of Content-Types to reject. Messages with Content-Types
+              included in this list are rejected. Multipart messages are always supported,
+              and each part is checked against this list.
+
+              Uses fnmatch for wildcard expansion.
+            '';
+            type = with types; listOf str;
+            default = ["text/html"];
+          };
+          reject-url = mkOption {
+            description = "Reject URL.";
+            type = types.str;
+            default = "https://man.sr.ht/lists.sr.ht/etiquette.md";
+          };
+          sock = mkOption {
+            description = ''
+              Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
+              Alternatively, specify IP:PORT and an SMTP server will be run instead.
+            '';
+            type = types.str;
+            default = "/tmp/lists.sr.ht-lmtp.sock";
+          };
+          sock-group = mkOption {
+            description = ''
+              The lmtp daemon will make the unix socket group-read/write
+              for users in this group.
+            '';
+            type = types.str;
+            default = "postfix";
+          };
+        };
+
+        options."man.sr.ht" = commonServiceSettings "man" // {
+        };
+
+        options."meta.sr.ht" =
+          removeAttrs (commonServiceSettings "meta")
+            ["oauth-client-id" "oauth-client-secret"] // {
+          api-origin = mkOption {
+            description = "Origin URL for API, 100 more than web.";
+            type = types.str;
+            default = "http://${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
+            defaultText = ''http://<xref linkend="opt-services.sourcehut.listenAddress"/>:''${toString (<xref linkend="opt-services.sourcehut.meta.port"/> + 100)}'';
+          };
+          webhooks = mkOption {
+            description = "The Redis connection used for the webhooks worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-metasrht/redis.sock?virtual_host=1";
+          };
+          welcome-emails = mkEnableOption "sending stock sourcehut welcome emails after signup";
+        };
+        options."meta.sr.ht::api" = {
+          internal-ipnet = mkOption {
+            description = ''
+              Set of IP subnets which are permitted to utilize internal API
+              authentication. This should be limited to the subnets
+              from which your *.sr.ht services are running.
+              See <xref linkend="opt-services.sourcehut.listenAddress"/>.
+            '';
+            type = with types; listOf str;
+            default = [ "127.0.0.0/8" "::1/128" ];
+          };
+        };
+        options."meta.sr.ht::aliases" = mkOption {
+          description = "Aliases for the client IDs of commonly used OAuth clients.";
+          type = with types; attrsOf int;
+          default = {};
+          example = { "git.sr.ht" = 12345; };
+        };
+        options."meta.sr.ht::billing" = {
+          enabled = mkEnableOption "the billing system";
+          stripe-public-key = mkOptionNullOrStr "Public key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
+          stripe-secret-key = mkOptionNullOrStr ''
+            An absolute file path (which should be outside the Nix-store)
+            to a secret key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys
+          '' // {
+            apply = mapNullable (s: "<" + toString s);
+          };
+        };
+        options."meta.sr.ht::settings" = {
+          registration = mkEnableOption "public registration";
+          onboarding-redirect = mkOption {
+            description = "Where to redirect new users upon registration.";
+            type = types.str;
+            default = "https://meta.localhost.localdomain";
+          };
+          user-invites = mkOption {
+            description = ''
+              How many invites each user is issued upon registration
+              (only applicable if open registration is disabled).
+            '';
+            type = types.ints.unsigned;
+            default = 5;
+          };
+        };
+
+        options."pages.sr.ht" = commonServiceSettings "pages" // {
+          gemini-certs = mkOption {
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to Gemini certificates.
+            '';
+            type = with types; nullOr path;
+            default = null;
+          };
+          max-site-size = mkOption {
+            description = "Maximum size of any given site (post-gunzip), in MiB.";
+            type = types.int;
+            default = 1024;
+          };
+          user-domain = mkOption {
+            description = ''
+              Configures the user domain, if enabled.
+              All users are given &lt;username&gt;.this.domain.
+            '';
+            type = with types; nullOr str;
+            default = null;
+          };
+        };
+        options."pages.sr.ht::api" = {
+          internal-ipnet = mkOption {
+            description = ''
+              Set of IP subnets which are permitted to utilize internal API
+              authentication. This should be limited to the subnets
+              from which your *.sr.ht services are running.
+              See <xref linkend="opt-services.sourcehut.listenAddress"/>.
+            '';
+            type = with types; listOf str;
+            default = [ "127.0.0.0/8" "::1/128" ];
+          };
+        };
+
+        options."paste.sr.ht" = commonServiceSettings "paste" // {
+        };
+
+        options."todo.sr.ht" = commonServiceSettings "todo" // {
+          notify-from = mkOption {
+            description = "Outgoing email for notifications generated by users.";
+            type = types.str;
+            default = "todo-notify@localhost.localdomain";
+          };
+          webhooks = mkOption {
+            description = "The Redis connection used for the webhooks worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-todosrht/redis.sock?virtual_host=1";
+          };
+        };
+        options."todo.sr.ht::mail" = {
+          posting-domain = mkOption {
+            description = "Posting domain.";
+            type = types.str;
+            default = "todo.localhost.localdomain";
+          };
+          sock = mkOption {
+            description = ''
+              Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
+              Alternatively, specify IP:PORT and an SMTP server will be run instead.
+            '';
+            type = types.str;
+            default = "/tmp/todo.sr.ht-lmtp.sock";
+          };
+          sock-group = mkOption {
+            description = ''
+              The lmtp daemon will make the unix socket group-read/write
+              for users in this group.
+            '';
+            type = types.str;
+            default = "postfix";
+          };
+        };
+      };
+      default = { };
+      description = ''
+        The configuration for the sourcehut network.
+      '';
+    };
+
+    builds = {
+      enableWorker = mkEnableOption ''
+        worker for builds.sr.ht
+
+        <warning><para>
+        For smaller deployments, job runners can be installed alongside the master server
+        but even if you only build your own software, integration with other services
+        may cause you to run untrusted builds
+        (e.g. automatic testing of patches via listssrht).
+        See <link xlink:href="https://man.sr.ht/builds.sr.ht/configuration.md#security-model"/>.
+        </para></warning>
+      '';
+
+      images = mkOption {
+        type = with types; attrsOf (attrsOf (attrsOf package));
+        default = { };
+        example = lib.literalExpression ''(let
+            # Pinning unstable to allow usage with flakes and limit rebuilds.
+            pkgs_unstable = builtins.fetchGit {
+                url = "https://github.com/NixOS/nixpkgs";
+                rev = "ff96a0fa5635770390b184ae74debea75c3fd534";
+                ref = "nixos-unstable";
+            };
+            image_from_nixpkgs = (import ("''${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
+              pkgs = (import pkgs_unstable {});
+            });
+          in
+          {
+            nixos.unstable.x86_64 = image_from_nixpkgs;
+          }
+        )'';
+        description = ''
+          Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2.
+        '';
+      };
+    };
+
+    git = {
+      package = mkOption {
+        type = types.package;
+        default = pkgs.git;
+        defaultText = literalExpression "pkgs.git";
+        example = literalExpression "pkgs.gitFull";
+        description = ''
+          Git package for git.sr.ht. This can help silence collisions.
+        '';
+      };
+      fcgiwrap.preforkProcess = mkOption {
+        description = "Number of fcgiwrap processes to prefork.";
+        type = types.int;
+        default = 4;
+      };
+    };
+
+    hg = {
+      package = mkOption {
+        type = types.package;
+        default = pkgs.mercurial;
+        defaultText = literalExpression "pkgs.mercurial";
+        description = ''
+          Mercurial package for hg.sr.ht. This can help silence collisions.
+        '';
+      };
+      cloneBundles = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories).
+        '';
+      };
+    };
+
+    lists = {
+      process = {
+        extraArgs = mkOption {
+          type = with types; listOf str;
+          default = [ "--loglevel DEBUG" "--pool eventlet" "--without-heartbeat" ];
+          description = "Extra arguments passed to the Celery responsible for processing mails.";
+        };
+        celeryConfig = mkOption {
+          type = types.lines;
+          default = "";
+          description = "Content of the <literal>celeryconfig.py</literal> used by the Celery of <literal>listssrht-process</literal>.";
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable (mkMerge [
+    {
+      environment.systemPackages = [ pkgs.sourcehut.coresrht ];
+
+      services.sourcehut.settings = {
+        "git.sr.ht".outgoing-domain = mkDefault "https://git.${domain}";
+        "lists.sr.ht".notify-from = mkDefault "lists-notify@${domain}";
+        "lists.sr.ht".posting-domain = mkDefault "lists.${domain}";
+        "meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${domain}";
+        "todo.sr.ht".notify-from = mkDefault "todo-notify@${domain}";
+        "todo.sr.ht::mail".posting-domain = mkDefault "todo.${domain}";
+      };
+    }
+    (mkIf cfg.postgresql.enable {
+      assertions = [
+        { assertion = postgresql.enable;
+          message = "postgresql must be enabled and configured";
+        }
+      ];
+    })
+    (mkIf cfg.postfix.enable {
+      assertions = [
+        { assertion = postfix.enable;
+          message = "postfix must be enabled and configured";
+        }
+      ];
+      # Needed for sharing the LMTP sockets with JoinsNamespaceOf=
+      systemd.services.postfix.serviceConfig.PrivateTmp = true;
+    })
+    (mkIf cfg.redis.enable {
+      services.redis.vmOverCommit = mkDefault true;
+    })
+    (mkIf cfg.nginx.enable {
+      assertions = [
+        { assertion = nginx.enable;
+          message = "nginx must be enabled and configured";
+        }
+      ];
+      # For proxyPass= in virtual-hosts for Sourcehut services.
+      services.nginx.recommendedProxySettings = mkDefault true;
+    })
+    (mkIf (cfg.builds.enable || cfg.git.enable || cfg.hg.enable) {
+      services.openssh = {
+        # Note that sshd will continue to honor AuthorizedKeysFile.
+        # Note that you may want automatically rotate
+        # or link to /dev/null the following log files:
+        # - /var/log/gitsrht-dispatch
+        # - /var/log/{build,git,hg}srht-keys
+        # - /var/log/{git,hg}srht-shell
+        # - /var/log/gitsrht-update-hook
+        authorizedKeysCommand = ''/etc/ssh/sourcehut/subdir/srht-dispatch "%u" "%h" "%t" "%k"'';
+        # srht-dispatch will setuid/setgid according to [git.sr.ht::dispatch]
+        authorizedKeysCommandUser = "root";
+        extraConfig = ''
+          PermitUserEnvironment SRHT_*
+        '';
+      };
+      environment.etc."ssh/sourcehut/config.ini".source =
+        settingsFormat.generate "sourcehut-dispatch-config.ini"
+          (filterAttrs (k: v: k == "git.sr.ht::dispatch")
+          cfg.settings);
+      environment.etc."ssh/sourcehut/subdir/srht-dispatch" = {
+        # sshd_config(5): The program must be owned by root, not writable by group or others
+        mode = "0755";
+        source = pkgs.writeShellScript "srht-dispatch" ''
+          set -e
+          cd /etc/ssh/sourcehut/subdir
+          ${cfg.python}/bin/gitsrht-dispatch "$@"
+        '';
+      };
+      systemd.services.sshd = {
+        #path = optional cfg.git.enable [ cfg.git.package ];
+        serviceConfig = {
+          BindReadOnlyPaths =
+            # Note that those /usr/bin/* paths are hardcoded in multiple places in *.sr.ht,
+            # for instance to get the user from the [git.sr.ht::dispatch] settings.
+            # *srht-keys needs to:
+            # - access a redis-server in [sr.ht] redis-host,
+            # - access the PostgreSQL server in [*.sr.ht] connection-string,
+            # - query metasrht-api (through the HTTP API).
+            # Using this has the side effect of creating empty files in /usr/bin/
+            optionals cfg.builds.enable [
+              "${pkgs.writeShellScript "buildsrht-keys-wrapper" ''
+                set -e
+                cd /run/sourcehut/buildsrht/subdir
+                set -x
+                exec -a "$0" ${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys "$@"
+              ''}:/usr/bin/buildsrht-keys"
+              "${pkgs.sourcehut.buildsrht}/bin/master-shell:/usr/bin/master-shell"
+              "${pkgs.sourcehut.buildsrht}/bin/runner-shell:/usr/bin/runner-shell"
+            ] ++
+            optionals cfg.git.enable [
+              # /path/to/gitsrht-keys calls /path/to/gitsrht-shell,
+              # or [git.sr.ht] shell= if set.
+              "${pkgs.writeShellScript "gitsrht-keys-wrapper" ''
+                set -e
+                cd /run/sourcehut/gitsrht/subdir
+                set -x
+                exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys "$@"
+              ''}:/usr/bin/gitsrht-keys"
+              "${pkgs.writeShellScript "gitsrht-shell-wrapper" ''
+                set -e
+                cd /run/sourcehut/gitsrht/subdir
+                set -x
+                exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-shell "$@"
+              ''}:/usr/bin/gitsrht-shell"
+              "${pkgs.writeShellScript "gitsrht-update-hook" ''
+                set -e
+                test -e "''${PWD%/*}"/config.ini ||
+                # Git hooks are run relative to their repository's directory,
+                # but gitsrht-update-hook looks up ../config.ini
+                ln -s /run/sourcehut/gitsrht/config.ini "''${PWD%/*}"/config.ini
+                # hooks/post-update calls /usr/bin/gitsrht-update-hook as hooks/stage-3
+                # but this wrapper being a bash script, it overrides $0 with /usr/bin/gitsrht-update-hook
+                # hence this hack to put hooks/stage-3 back into gitsrht-update-hook's $0
+                if test "''${STAGE3:+set}"
+                then
+                  set -x
+                  exec -a hooks/stage-3 ${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook "$@"
+                else
+                  export STAGE3=set
+                  set -x
+                  exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook "$@"
+                fi
+              ''}:/usr/bin/gitsrht-update-hook"
+            ] ++
+            optionals cfg.hg.enable [
+              # /path/to/hgsrht-keys calls /path/to/hgsrht-shell,
+              # or [hg.sr.ht] shell= if set.
+              "${pkgs.writeShellScript "hgsrht-keys-wrapper" ''
+                set -e
+                cd /run/sourcehut/hgsrht/subdir
+                set -x
+                exec -a "$0" ${pkgs.sourcehut.hgsrht}/bin/hgsrht-keys "$@"
+              ''}:/usr/bin/hgsrht-keys"
+              "${pkgs.writeShellScript "hgsrht-shell-wrapper" ''
+                set -e
+                cd /run/sourcehut/hgsrht/subdir
+                set -x
+                exec -a "$0" ${pkgs.sourcehut.hgsrht}/bin/hgsrht-shell "$@"
+              ''}:/usr/bin/hgsrht-shell"
+              # Mercurial's changegroup hooks are run relative to their repository's directory,
+              # but hgsrht-hook-changegroup looks up ./config.ini
+              "${pkgs.writeShellScript "hgsrht-hook-changegroup" ''
+                set -e
+                test -e "''$PWD"/config.ini ||
+                ln -s /run/sourcehut/hgsrht/config.ini "''$PWD"/config.ini
+                set -x
+                exec -a "$0" ${cfg.python}/bin/hgsrht-hook-changegroup "$@"
+              ''}:/usr/bin/hgsrht-hook-changegroup"
+            ];
+        };
+      };
+    })
+  ]);
+
+  imports = [
+
+    (import ./service.nix "builds" {
+      inherit configIniOfService;
+      srvsrht = "buildsrht";
+      port = 5002;
+      # TODO: a celery worker on the master and worker are apparently needed
+      extraServices.buildsrht-worker = let
+        qemuPackage = pkgs.qemu_kvm;
+        serviceName = "buildsrht-worker";
+        statePath = "/var/lib/sourcehut/${serviceName}";
+        in mkIf cfg.builds.enableWorker {
+        path = [ pkgs.openssh pkgs.docker ];
+        preStart = ''
+          set -x
+          if test -z "$(docker images -q qemu:latest 2>/dev/null)" \
+          || test "$(cat ${statePath}/docker-image-qemu)" != "${qemuPackage.version}"
+          then
+            # Create and import qemu:latest image for docker
+            ${pkgs.dockerTools.streamLayeredImage {
+              name = "qemu";
+              tag = "latest";
+              contents = [ qemuPackage ];
+            }} | docker load
+            # Mark down current package version
+            echo '${qemuPackage.version}' >${statePath}/docker-image-qemu
+          fi
+        '';
+        serviceConfig = {
+          ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
+          BindPaths = [ cfg.settings."builds.sr.ht::worker".buildlogs ];
+          LogsDirectory = [ "sourcehut/${serviceName}" ];
+          RuntimeDirectory = [ "sourcehut/${serviceName}/subdir" ];
+          StateDirectory = [ "sourcehut/${serviceName}" ];
+          TimeoutStartSec = "1800s";
+          # builds.sr.ht-worker looks up ../config.ini
+          WorkingDirectory = "-"+"/run/sourcehut/${serviceName}/subdir";
+        };
+      };
+      extraConfig = let
+        image_dirs = flatten (
+          mapAttrsToList (distro: revs:
+            mapAttrsToList (rev: archs:
+              mapAttrsToList (arch: image:
+                pkgs.runCommand "buildsrht-images" { } ''
+                  mkdir -p $out/${distro}/${rev}/${arch}
+                  ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
+                ''
+              ) archs
+            ) revs
+          ) cfg.builds.images
+        );
+        image_dir_pre = pkgs.symlinkJoin {
+          name = "builds.sr.ht-worker-images-pre";
+          paths = image_dirs;
+            # FIXME: not working, apparently because ubuntu/latest is a broken link
+            # ++ [ "${pkgs.sourcehut.buildsrht}/lib/images" ];
+        };
+        image_dir = pkgs.runCommand "builds.sr.ht-worker-images" { } ''
+          mkdir -p $out/images
+          cp -Lr ${image_dir_pre}/* $out/images
+        '';
+        in mkMerge [
+        {
+          users.users.${cfg.builds.user}.shell = pkgs.bash;
+
+          virtualisation.docker.enable = true;
+
+          services.sourcehut.settings = mkMerge [
+            { # Note that git.sr.ht::dispatch is not a typo,
+              # gitsrht-dispatch always use this section
+              "git.sr.ht::dispatch"."/usr/bin/buildsrht-keys" =
+                mkDefault "${cfg.builds.user}:${cfg.builds.group}";
+            }
+            (mkIf cfg.builds.enableWorker {
+              "builds.sr.ht::worker".shell = "/usr/bin/runner-shell";
+              "builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
+              "builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
+            })
+          ];
+        }
+        (mkIf cfg.builds.enableWorker {
+          users.groups = {
+            docker.members = [ cfg.builds.user ];
+          };
+        })
+        (mkIf (cfg.builds.enableWorker && cfg.nginx.enable) {
+          # Allow nginx access to buildlogs
+          users.users.${nginx.user}.extraGroups = [ cfg.builds.group ];
+          systemd.services.nginx = {
+            serviceConfig.BindReadOnlyPaths = [ cfg.settings."builds.sr.ht::worker".buildlogs ];
+          };
+          services.nginx.virtualHosts."logs.${domain}" = mkMerge [ {
+            /* FIXME: is a listen needed?
+            listen = with builtins;
+              # FIXME: not compatible with IPv6
+              let address = split ":" cfg.settings."builds.sr.ht::worker".name; in
+              [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
+            */
+            locations."/logs/".alias = cfg.settings."builds.sr.ht::worker".buildlogs + "/";
+          } cfg.nginx.virtualHost ];
+        })
+      ];
+    })
+
+    (import ./service.nix "dispatch" {
+      inherit configIniOfService;
+      port = 5005;
+    })
+
+    (import ./service.nix "git" (let
+      baseService = {
+        path = [ cfg.git.package ];
+        serviceConfig.BindPaths = [ "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos" ];
+      };
+      in {
+      inherit configIniOfService;
+      mainService = mkMerge [ baseService {
+        serviceConfig.StateDirectory = [ "sourcehut/gitsrht" "sourcehut/gitsrht/repos" ];
+        preStart = mkIf (!versionAtLeast config.system.stateVersion "22.05") (mkBefore ''
+          # Fix Git hooks of repositories pre-dating https://github.com/NixOS/nixpkgs/pull/133984
+          (
+          set +f
+          shopt -s nullglob
+          for h in /var/lib/sourcehut/gitsrht/repos/~*/*/hooks/{pre-receive,update,post-update}
+          do ln -fnsv /usr/bin/gitsrht-update-hook "$h"; done
+          )
+        '');
+      } ];
+      port = 5001;
+      webhooks = true;
+      extraTimers.gitsrht-periodic = {
+        service = baseService;
+        timerConfig.OnCalendar = ["*:0/20"];
+      };
+      extraConfig = mkMerge [
+        {
+          # https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
+          # Probably could use gitsrht-shell if output is restricted to just parameters...
+          users.users.${cfg.git.user}.shell = pkgs.bash;
+          services.sourcehut.settings = {
+            "git.sr.ht::dispatch"."/usr/bin/gitsrht-keys" =
+              mkDefault "${cfg.git.user}:${cfg.git.group}";
+          };
+          systemd.services.sshd = baseService;
+        }
+        (mkIf cfg.nginx.enable {
+          services.nginx.virtualHosts."git.${domain}" = {
+            locations."/authorize" = {
+              proxyPass = "http://${cfg.listenAddress}:${toString cfg.git.port}";
+              extraConfig = ''
+                proxy_pass_request_body off;
+                proxy_set_header Content-Length "";
+                proxy_set_header X-Original-URI $request_uri;
+              '';
+            };
+            locations."~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$" = {
+              root = "/var/lib/sourcehut/gitsrht/repos";
+              fastcgiParams = {
+                GIT_HTTP_EXPORT_ALL = "";
+                GIT_PROJECT_ROOT = "$document_root";
+                PATH_INFO = "$uri";
+                SCRIPT_FILENAME = "${cfg.git.package}/bin/git-http-backend";
+              };
+              extraConfig = ''
+                auth_request /authorize;
+                fastcgi_read_timeout 500s;
+                fastcgi_pass unix:/run/gitsrht-fcgiwrap.sock;
+                gzip off;
+              '';
+            };
+          };
+          systemd.sockets.gitsrht-fcgiwrap = {
+            before = [ "nginx.service" ];
+            wantedBy = [ "sockets.target" "gitsrht.service" ];
+            # This path remains accessible to nginx.service, which has no RootDirectory=
+            socketConfig.ListenStream = "/run/gitsrht-fcgiwrap.sock";
+            socketConfig.SocketUser = nginx.user;
+            socketConfig.SocketMode = "600";
+          };
+        })
+      ];
+      extraServices.gitsrht-fcgiwrap = mkIf cfg.nginx.enable {
+        serviceConfig = {
+          # Socket is passed by gitsrht-fcgiwrap.socket
+          ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${toString cfg.git.fcgiwrap.preforkProcess}";
+          # No need for config.ini
+          ExecStartPre = mkForce [];
+          User = null;
+          DynamicUser = true;
+          BindReadOnlyPaths = [ "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos" ];
+          IPAddressDeny = "any";
+          InaccessiblePaths = [ "-+/run/postgresql" "-+/run/redis-sourcehut" ];
+          PrivateNetwork = true;
+          RestrictAddressFamilies = mkForce [ "none" ];
+          SystemCallFilter = mkForce [
+            "@system-service"
+            "~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@setuid"
+            # @timer is needed for alarm()
+          ];
+        };
+      };
+    }))
+
+    (import ./service.nix "hg" (let
+      baseService = {
+        path = [ cfg.hg.package ];
+        serviceConfig.BindPaths = [ "${cfg.settings."hg.sr.ht".repos}:/var/lib/sourcehut/hgsrht/repos" ];
+      };
+      in {
+      inherit configIniOfService;
+      mainService = mkMerge [ baseService {
+        serviceConfig.StateDirectory = [ "sourcehut/hgsrht" "sourcehut/hgsrht/repos" ];
+      } ];
+      port = 5010;
+      webhooks = true;
+      extraTimers.hgsrht-periodic = {
+        service = baseService;
+        timerConfig.OnCalendar = ["*:0/20"];
+      };
+      extraTimers.hgsrht-clonebundles = mkIf cfg.hg.cloneBundles {
+        service = baseService;
+        timerConfig.OnCalendar = ["daily"];
+        timerConfig.AccuracySec = "1h";
+      };
+      extraConfig = mkMerge [
+        {
+          users.users.${cfg.hg.user}.shell = pkgs.bash;
+          services.sourcehut.settings = {
+            # Note that git.sr.ht::dispatch is not a typo,
+            # gitsrht-dispatch always uses this section.
+            "git.sr.ht::dispatch"."/usr/bin/hgsrht-keys" =
+              mkDefault "${cfg.hg.user}:${cfg.hg.group}";
+          };
+          systemd.services.sshd = baseService;
+        }
+        (mkIf cfg.nginx.enable {
+          # Allow nginx access to repositories
+          users.users.${nginx.user}.extraGroups = [ cfg.hg.group ];
+          services.nginx.virtualHosts."hg.${domain}" = {
+            locations."/authorize" = {
+              proxyPass = "http://${cfg.listenAddress}:${toString cfg.hg.port}";
+              extraConfig = ''
+                proxy_pass_request_body off;
+                proxy_set_header Content-Length "";
+                proxy_set_header X-Original-URI $request_uri;
+              '';
+            };
+            # Let clients reach pull bundles. We don't really need to lock this down even for
+            # private repos because the bundles are named after the revision hashes...
+            # so someone would need to know or guess a SHA value to download anything.
+            # TODO: proxyPass to an hg serve service?
+            locations."~ ^/[~^][a-z0-9_]+/[a-zA-Z0-9_.-]+/\\.hg/bundles/.*$" = {
+              root = "/var/lib/nginx/hgsrht/repos";
+              extraConfig = ''
+                auth_request /authorize;
+                gzip off;
+              '';
+            };
+          };
+          systemd.services.nginx = {
+            serviceConfig.BindReadOnlyPaths = [ "${cfg.settings."hg.sr.ht".repos}:/var/lib/nginx/hgsrht/repos" ];
+          };
+        })
+      ];
+    }))
+
+    (import ./service.nix "hub" {
+      inherit configIniOfService;
+      port = 5014;
+      extraConfig = {
+        services.nginx = mkIf cfg.nginx.enable {
+          virtualHosts."hub.${domain}" = mkMerge [ {
+            serverAliases = [ domain ];
+          } cfg.nginx.virtualHost ];
+        };
+      };
+    })
+
+    (import ./service.nix "lists" (let
+      srvsrht = "listssrht";
+      in {
+      inherit configIniOfService;
+      port = 5006;
+      webhooks = true;
+      # Receive the mail from Postfix and enqueue them into Redis and PostgreSQL
+      extraServices.listssrht-lmtp = {
+        wants = [ "postfix.service" ];
+        unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
+        serviceConfig.ExecStart = "${cfg.python}/bin/listssrht-lmtp";
+        # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
+        serviceConfig.PrivateUsers = mkForce false;
+      };
+      # Dequeue the mails from Redis and dispatch them
+      extraServices.listssrht-process = {
+        serviceConfig = {
+          preStart = ''
+            cp ${pkgs.writeText "${srvsrht}-webhooks-celeryconfig.py" cfg.lists.process.celeryConfig} \
+               /run/sourcehut/${srvsrht}-webhooks/celeryconfig.py
+          '';
+          ExecStart = "${cfg.python}/bin/celery --app listssrht.process worker --hostname listssrht-process@%%h " + concatStringsSep " " cfg.lists.process.extraArgs;
+          # Avoid crashing: os.getloadavg()
+          ProcSubset = mkForce "all";
+        };
+      };
+      extraConfig = mkIf cfg.postfix.enable {
+        users.groups.${postfix.group}.members = [ cfg.lists.user ];
+        services.sourcehut.settings."lists.sr.ht::mail".sock-group = postfix.group;
+        services.postfix = {
+          destination = [ "lists.${domain}" ];
+          # FIXME: an accurate recipient list should be queried
+          # from the lists.sr.ht PostgreSQL database to avoid backscattering.
+          # But usernames are unfortunately not in that database but in meta.sr.ht.
+          # Note that two syntaxes are allowed:
+          # - ~username/list-name@lists.${domain}
+          # - u.username.list-name@lists.${domain}
+          localRecipients = [ "@lists.${domain}" ];
+          transport = ''
+            lists.${domain} lmtp:unix:${cfg.settings."lists.sr.ht::worker".sock}
+          '';
+        };
+      };
+    }))
+
+    (import ./service.nix "man" {
+      inherit configIniOfService;
+      port = 5004;
+    })
+
+    (import ./service.nix "meta" {
+      inherit configIniOfService;
+      port = 5000;
+      webhooks = true;
+      extraServices.metasrht-api = {
+        serviceConfig.Restart = "always";
+        serviceConfig.RestartSec = "2s";
+        preStart = "set -x\n" + concatStringsSep "\n\n" (attrValues (mapAttrs (k: s:
+          let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht$" k;
+              srv = head srvMatch;
+          in
+          # Configure client(s) as "preauthorized"
+          optionalString (srvMatch != null && cfg.${srv}.enable && ((s.oauth-client-id or null) != null)) ''
+            # Configure ${srv}'s OAuth client as "preauthorized"
+            ${postgresql.package}/bin/psql '${cfg.settings."meta.sr.ht".connection-string}' \
+              -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${s.oauth-client-id}'"
+          ''
+          ) cfg.settings));
+        serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b ${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
+      };
+      extraTimers.metasrht-daily.timerConfig = {
+        OnCalendar = ["daily"];
+        AccuracySec = "1h";
+      };
+      extraConfig = mkMerge [
+        {
+          assertions = [
+            { assertion = let s = cfg.settings."meta.sr.ht::billing"; in
+                          s.enabled == "yes" -> (s.stripe-public-key != null && s.stripe-secret-key != null);
+              message = "If meta.sr.ht::billing is enabled, the keys must be defined.";
+            }
+          ];
+          environment.systemPackages = optional cfg.meta.enable
+            (pkgs.writeShellScriptBin "metasrht-manageuser" ''
+              set -eux
+              if test "$(${pkgs.coreutils}/bin/id -n -u)" != '${cfg.meta.user}'
+              then exec sudo -u '${cfg.meta.user}' "$0" "$@"
+              else
+                # In order to load config.ini
+                if cd /run/sourcehut/metasrht
+                then exec ${cfg.python}/bin/metasrht-manageuser "$@"
+                else cat <<EOF
+                  Please run: sudo systemctl start metasrht
+              EOF
+                  exit 1
+                fi
+              fi
+            '');
+        }
+        (mkIf cfg.nginx.enable {
+          services.nginx.virtualHosts."meta.${domain}" = {
+            locations."/query" = {
+              proxyPass = cfg.settings."meta.sr.ht".api-origin;
+              extraConfig = ''
+                if ($request_method = 'OPTIONS') {
+                  add_header 'Access-Control-Allow-Origin' '*';
+                  add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+                  add_header 'Access-Control-Allow-Headers' 'User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
+                  add_header 'Access-Control-Max-Age' 1728000;
+                  add_header 'Content-Type' 'text/plain; charset=utf-8';
+                  add_header 'Content-Length' 0;
+                  return 204;
+                }
+
+                add_header 'Access-Control-Allow-Origin' '*';
+                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+                add_header 'Access-Control-Allow-Headers' 'User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
+                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
+              '';
+            };
+          };
+        })
+      ];
+    })
+
+    (import ./service.nix "pages" {
+      inherit configIniOfService;
+      port = 5112;
+      mainService = let
+        srvsrht = "pagessrht";
+        version = pkgs.sourcehut.${srvsrht}.version;
+        stateDir = "/var/lib/sourcehut/${srvsrht}";
+        iniKey = "pages.sr.ht";
+        in {
+        preStart = mkBefore ''
+          set -x
+          # Use the /run/sourcehut/${srvsrht}/config.ini
+          # installed by a previous ExecStartPre= in baseService
+          cd /run/sourcehut/${srvsrht}
+
+          if test ! -e ${stateDir}/db; then
+            ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f ${pkgs.sourcehut.pagessrht}/share/sql/schema.sql
+            echo ${version} >${stateDir}/db
+          fi
+
+          ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
+            # Just try all the migrations because they're not linked to the version
+            for sql in ${pkgs.sourcehut.pagessrht}/share/sql/migrations/*.sql; do
+              ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f "$sql" || true
+            done
+          ''}
+
+          # Disable webhook
+          touch ${stateDir}/webhook
+        '';
+        serviceConfig = {
+          ExecStart = mkForce "${pkgs.sourcehut.pagessrht}/bin/pages.sr.ht -b ${cfg.listenAddress}:${toString cfg.pages.port}";
+        };
+      };
+    })
+
+    (import ./service.nix "paste" {
+      inherit configIniOfService;
+      port = 5011;
+    })
+
+    (import ./service.nix "todo" {
+      inherit configIniOfService;
+      port = 5003;
+      webhooks = true;
+      extraServices.todosrht-lmtp = {
+        wants = [ "postfix.service" ];
+        unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
+        serviceConfig.ExecStart = "${cfg.python}/bin/todosrht-lmtp";
+        # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
+        serviceConfig.PrivateUsers = mkForce false;
+      };
+      extraConfig = mkIf cfg.postfix.enable {
+        users.groups.${postfix.group}.members = [ cfg.todo.user ];
+        services.sourcehut.settings."todo.sr.ht::mail".sock-group = postfix.group;
+        services.postfix = {
+          destination = [ "todo.${domain}" ];
+          # FIXME: an accurate recipient list should be queried
+          # from the todo.sr.ht PostgreSQL database to avoid backscattering.
+          # But usernames are unfortunately not in that database but in meta.sr.ht.
+          # Note that two syntaxes are allowed:
+          # - ~username/tracker-name@todo.${domain}
+          # - u.username.tracker-name@todo.${domain}
+          localRecipients = [ "@todo.${domain}" ];
+          transport = ''
+            todo.${domain} lmtp:unix:${cfg.settings."todo.sr.ht::mail".sock}
+          '';
+        };
+      };
+    })
+
+    (mkRenamedOptionModule [ "services" "sourcehut" "originBase" ]
+                           [ "services" "sourcehut" "settings" "sr.ht" "global-domain" ])
+    (mkRenamedOptionModule [ "services" "sourcehut" "address" ]
+                           [ "services" "sourcehut" "listenAddress" ])
+
+  ];
+
+  meta.doc = ./sourcehut.xml;
+  meta.maintainers = with maintainers; [ julm tomberek ];
+}
diff --git a/nixos/modules/services/misc/sourcehut/dispatch.nix b/nixos/modules/services/misc/sourcehut/dispatch.nix
new file mode 100644
index 00000000000..292a51d3e1c
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/dispatch.nix
@@ -0,0 +1,127 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.dispatch;
+  iniKey = "dispatch.sr.ht";
+
+  drv = pkgs.sourcehut.dispatchsrht;
+in
+{
+  options.services.sourcehut.dispatch = {
+    user = mkOption {
+      type = types.str;
+      default = "dispatchsrht";
+      description = ''
+        User for dispatch.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5005;
+      description = ''
+        Port on which the "dispatch" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "dispatch.sr.ht";
+      description = ''
+        PostgreSQL database name for dispatch.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/dispatchsrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/dispatchsrht"'';
+      description = ''
+        State path for dispatch.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "dispatch" cfg.services) {
+
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          description = "dispatch.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services.dispatchsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+        after = [ "postgresql.service" "network.target" ];
+        requires = [ "postgresql.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        description = "dispatch.sr.ht website service";
+
+        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL dispatch.sr.ht is being served at (protocol://domain)
+      "dispatch.sr.ht".origin = mkDefault "http://dispatch.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "dispatch.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "dispatch.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "dispatch.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "dispatch.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # dispatch.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "dispatch.sr.ht".oauth-client-id = mkDefault null;
+      "dispatch.sr.ht".oauth-client-secret = mkDefault null;
+
+      # Github Integration
+      "dispatch.sr.ht::github".oauth-client-id = mkDefault null;
+      "dispatch.sr.ht::github".oauth-client-secret = mkDefault null;
+
+      # Gitlab Integration
+      "dispatch.sr.ht::gitlab".enabled = mkDefault null;
+      "dispatch.sr.ht::gitlab".canonical-upstream = mkDefault "gitlab.com";
+      "dispatch.sr.ht::gitlab".repo-cache = mkDefault "./repo-cache";
+      # "dispatch.sr.ht::gitlab"."gitlab.com" = mkDefault "GitLab:application id:secret";
+    };
+
+    services.nginx.virtualHosts."dispatch.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.dispatchsrht}/${pkgs.sourcehut.python.sitePackages}/dispatchsrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/git.nix b/nixos/modules/services/misc/sourcehut/git.nix
new file mode 100644
index 00000000000..ff110905d18
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/git.nix
@@ -0,0 +1,217 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
+  scfg = cfg.git;
+  iniKey = "git.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.gitsrht;
+in
+{
+  options.services.sourcehut.git = {
+    user = mkOption {
+      type = types.str;
+      visible = false;
+      internal = true;
+      readOnly = true;
+      default = "git";
+      description = ''
+        User for git.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5001;
+      description = ''
+        Port on which the "git" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "git.sr.ht";
+      description = ''
+        PostgreSQL database name for git.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/gitsrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/gitsrht"'';
+      description = ''
+        State path for git.sr.ht.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.git;
+      defaultText = literalExpression "pkgs.git";
+      example = literalExpression "pkgs.gitFull";
+      description = ''
+        Git package for git.sr.ht. This can help silence collisions.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "git" cfg.services) {
+    # sshd refuses to run with `Unsafe AuthorizedKeysCommand ... bad ownership or modes for directory /nix/store`
+    environment.etc."ssh/gitsrht-dispatch" = {
+      mode = "0755";
+      text = ''
+        #! ${pkgs.stdenv.shell}
+        ${cfg.python}/bin/gitsrht-dispatch "$@"
+      '';
+    };
+
+    # Needs this in the $PATH when sshing into the server
+    environment.systemPackages = [ cfg.git.package ];
+
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          # https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
+          # Probably could use gitsrht-shell if output is restricted to just parameters...
+          shell = pkgs.bash;
+          description = "git.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services = {
+      cron.systemCronJobs = [ "*/20 * * * * ${cfg.python}/bin/gitsrht-periodic" ];
+      fcgiwrap.enable = true;
+
+      openssh.authorizedKeysCommand = ''/etc/ssh/gitsrht-dispatch "%u" "%h" "%t" "%k"'';
+      openssh.authorizedKeysCommandUser = "root";
+      openssh.extraConfig = ''
+        PermitUserEnvironment SRHT_*
+      '';
+
+      postgresql = {
+        authentication = ''
+          local ${database} ${user} trust
+        '';
+        ensureDatabases = [ database ];
+        ensureUsers = [
+          {
+            name = user;
+            ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+          }
+        ];
+      };
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        # /var/log is owned by root
+        "f /var/log/git-srht-shell 0644 ${user} ${user} -"
+
+        "d ${statePath} 0750 ${user} ${user} -"
+        "d ${cfg.settings."${iniKey}".repos} 2755 ${user} ${user} -"
+      ];
+
+      services = {
+        gitsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "redis.service" "postgresql.service" "network.target" ];
+          requires = [ "redis.service" "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          # Needs internally to create repos at the very least
+          path = [ pkgs.git ];
+          description = "git.sr.ht website service";
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+        };
+
+        gitsrht-webhooks = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "git.sr.ht webhooks service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+          };
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL git.sr.ht is being served at (protocol://domain)
+      "git.sr.ht".origin = mkDefault "http://git.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "git.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "git.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "git.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "git.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # The redis connection used for the webhooks worker
+      "git.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
+
+      # A post-update script which is installed in every git repo.
+      "git.sr.ht".post-update-script = mkDefault "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
+
+      # git.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "git.sr.ht".oauth-client-id = mkDefault null;
+      "git.sr.ht".oauth-client-secret = mkDefault null;
+      # Path to git repositories on disk
+      "git.sr.ht".repos = mkDefault "/var/lib/git";
+
+      "git.sr.ht".outgoing-domain = mkDefault "http://git.${cfg.originBase}";
+
+      # The authorized keys hook uses this to dispatch to various handlers
+      # The format is a program to exec into as the key, and the user to match as the
+      # value. When someone tries to log in as this user, this program is executed
+      # and is expected to omit an AuthorizedKeys file.
+      #
+      # Discard of the string context is in order to allow derivation-derived strings.
+      # This is safe if the relevant package is installed which will be the case if the setting is utilized.
+      "git.sr.ht::dispatch".${builtins.unsafeDiscardStringContext "${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys"} = mkDefault "${user}:${user}";
+    };
+
+    services.nginx.virtualHosts."git.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.gitsrht}/${pkgs.sourcehut.python.sitePackages}/gitsrht";
+      extraConfig = ''
+            location = /authorize {
+            proxy_pass http://${cfg.address}:${toString port};
+            proxy_pass_request_body off;
+            proxy_set_header Content-Length "";
+            proxy_set_header X-Original-URI $request_uri;
+        }
+            location ~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$ {
+                auth_request /authorize;
+                root /var/lib/git;
+                fastcgi_pass unix:/run/fcgiwrap.sock;
+                fastcgi_param SCRIPT_FILENAME ${pkgs.git}/bin/git-http-backend;
+                fastcgi_param PATH_INFO $uri;
+                fastcgi_param GIT_PROJECT_ROOT $document_root;
+                fastcgi_read_timeout 500s;
+                include ${config.services.nginx.package}/conf/fastcgi_params;
+                gzip off;
+            }
+      '';
+
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/hg.nix b/nixos/modules/services/misc/sourcehut/hg.nix
new file mode 100644
index 00000000000..6ba1df8b6dd
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/hg.nix
@@ -0,0 +1,175 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
+  scfg = cfg.hg;
+  iniKey = "hg.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.hgsrht;
+in
+{
+  options.services.sourcehut.hg = {
+    user = mkOption {
+      type = types.str;
+      internal = true;
+      readOnly = true;
+      default = "hg";
+      description = ''
+        User for hg.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5010;
+      description = ''
+        Port on which the "hg" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "hg.sr.ht";
+      description = ''
+        PostgreSQL database name for hg.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/hgsrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/hgsrht"'';
+      description = ''
+        State path for hg.sr.ht.
+      '';
+    };
+
+    cloneBundles = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories).
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "hg" cfg.services) {
+    # In case it ever comes into being
+    environment.etc."ssh/hgsrht-dispatch" = {
+      mode = "0755";
+      text = ''
+        #! ${pkgs.stdenv.shell}
+        ${cfg.python}/bin/gitsrht-dispatch $@
+      '';
+    };
+
+    environment.systemPackages = [ pkgs.mercurial ];
+
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          # Assuming hg.sr.ht needs this too
+          shell = pkgs.bash;
+          description = "hg.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services = {
+      cron.systemCronJobs = [ "*/20 * * * * ${cfg.python}/bin/hgsrht-periodic" ]
+        ++ optional cloneBundles "0 * * * * ${cfg.python}/bin/hgsrht-clonebundles";
+
+      openssh.authorizedKeysCommand = ''/etc/ssh/hgsrht-dispatch "%u" "%h" "%t" "%k"'';
+      openssh.authorizedKeysCommandUser = "root";
+      openssh.extraConfig = ''
+        PermitUserEnvironment SRHT_*
+      '';
+
+      postgresql = {
+        authentication = ''
+          local ${database} ${user} trust
+        '';
+        ensureDatabases = [ database ];
+        ensureUsers = [
+          {
+            name = user;
+            ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+          }
+        ];
+      };
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        # /var/log is owned by root
+        "f /var/log/hg-srht-shell 0644 ${user} ${user} -"
+
+        "d ${statePath} 0750 ${user} ${user} -"
+        "d ${cfg.settings."${iniKey}".repos} 2755 ${user} ${user} -"
+      ];
+
+      services.hgsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+        after = [ "redis.service" "postgresql.service" "network.target" ];
+        requires = [ "redis.service" "postgresql.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        path = [ pkgs.mercurial ];
+        description = "hg.sr.ht website service";
+
+        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL hg.sr.ht is being served at (protocol://domain)
+      "hg.sr.ht".origin = mkDefault "http://hg.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "hg.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "hg.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "hg.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # The redis connection used for the webhooks worker
+      "hg.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
+      # A post-update script which is installed in every mercurial repo.
+      "hg.sr.ht".changegroup-script = mkDefault "${cfg.python}/bin/hgsrht-hook-changegroup";
+      # hg.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "hg.sr.ht".oauth-client-id = mkDefault null;
+      "hg.sr.ht".oauth-client-secret = mkDefault null;
+      # Path to mercurial repositories on disk
+      "hg.sr.ht".repos = mkDefault "/var/lib/hg";
+      # Path to the srht mercurial extension
+      # (defaults to where the hgsrht code is)
+      # "hg.sr.ht".srhtext = mkDefault null;
+      # .hg/store size (in MB) past which the nightly job generates clone bundles.
+      # "hg.sr.ht".clone_bundle_threshold = mkDefault 50;
+      # Path to hg-ssh (if not in $PATH)
+      # "hg.sr.ht".hg_ssh = mkDefault /path/to/hg-ssh;
+
+      # The authorized keys hook uses this to dispatch to various handlers
+      # The format is a program to exec into as the key, and the user to match as the
+      # value. When someone tries to log in as this user, this program is executed
+      # and is expected to omit an AuthorizedKeys file.
+      #
+      # Uncomment the relevant lines to enable the various sr.ht dispatchers.
+      "hg.sr.ht::dispatch"."/run/current-system/sw/bin/hgsrht-keys" = mkDefault "${user}:${user}";
+    };
+
+    # TODO: requires testing and addition of hg-specific requirements
+    services.nginx.virtualHosts."hg.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.hgsrht}/${pkgs.sourcehut.python.sitePackages}/hgsrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/hub.nix b/nixos/modules/services/misc/sourcehut/hub.nix
new file mode 100644
index 00000000000..7d137a76505
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/hub.nix
@@ -0,0 +1,120 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.hub;
+  iniKey = "hub.sr.ht";
+
+  drv = pkgs.sourcehut.hubsrht;
+in
+{
+  options.services.sourcehut.hub = {
+    user = mkOption {
+      type = types.str;
+      default = "hubsrht";
+      description = ''
+        User for hub.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5014;
+      description = ''
+        Port on which the "hub" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "hub.sr.ht";
+      description = ''
+        PostgreSQL database name for hub.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/hubsrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/hubsrht"'';
+      description = ''
+        State path for hub.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "hub" cfg.services) {
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          description = "hub.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services.hubsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+        after = [ "postgresql.service" "network.target" ];
+        requires = [ "postgresql.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        description = "hub.sr.ht website service";
+
+        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL hub.sr.ht is being served at (protocol://domain)
+      "hub.sr.ht".origin = mkDefault "http://hub.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "hub.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "hub.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "hub.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "hub.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # hub.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "hub.sr.ht".oauth-client-id = mkDefault null;
+      "hub.sr.ht".oauth-client-secret = mkDefault null;
+    };
+
+    services.nginx.virtualHosts."${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.hubsrht}/${pkgs.sourcehut.python.sitePackages}/hubsrht";
+    };
+    services.nginx.virtualHosts."hub.${cfg.originBase}" = {
+      globalRedirect = "${cfg.originBase}";
+      forceSSL = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/lists.nix b/nixos/modules/services/misc/sourcehut/lists.nix
new file mode 100644
index 00000000000..76f155caa05
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/lists.nix
@@ -0,0 +1,187 @@
+# Email setup is fairly involved, useful references:
+# https://drewdevault.com/2018/08/05/Local-mail-server.html
+
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.lists;
+  iniKey = "lists.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.listssrht;
+in
+{
+  options.services.sourcehut.lists = {
+    user = mkOption {
+      type = types.str;
+      default = "listssrht";
+      description = ''
+        User for lists.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5006;
+      description = ''
+        Port on which the "lists" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "lists.sr.ht";
+      description = ''
+        PostgreSQL database name for lists.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/listssrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/listssrht"'';
+      description = ''
+        State path for lists.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "lists" cfg.services) {
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          extraGroups = [ "postfix" ];
+          description = "lists.sr.ht user";
+        };
+      };
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services = {
+        listssrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "lists.sr.ht website service";
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+        };
+
+        listssrht-process = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "lists.sr.ht process service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.process worker --loglevel=info";
+          };
+        };
+
+        listssrht-lmtp = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "lists.sr.ht process service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/listssrht-lmtp";
+          };
+        };
+
+
+        listssrht-webhooks = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "lists.sr.ht webhooks service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
+          };
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL lists.sr.ht is being served at (protocol://domain)
+      "lists.sr.ht".origin = mkDefault "http://lists.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "lists.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "lists.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "lists.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "lists.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # lists.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "lists.sr.ht".oauth-client-id = mkDefault null;
+      "lists.sr.ht".oauth-client-secret = mkDefault null;
+      # Outgoing email for notifications generated by users
+      "lists.sr.ht".notify-from = mkDefault "CHANGEME@example.org";
+      # The redis connection used for the webhooks worker
+      "lists.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/2";
+      # The redis connection used for the celery worker
+      "lists.sr.ht".redis = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/4";
+      # Network-key
+      "lists.sr.ht".network-key = mkDefault null;
+      # Allow creation
+      "lists.sr.ht".allow-new-lists = mkDefault "no";
+      # Posting Domain
+      "lists.sr.ht".posting-domain = mkDefault "lists.${cfg.originBase}";
+
+      # Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
+      # Alternatively, specify IP:PORT and an SMTP server will be run instead.
+      "lists.sr.ht::worker".sock = mkDefault "/tmp/lists.sr.ht-lmtp.sock";
+      # The lmtp daemon will make the unix socket group-read/write for users in this
+      # group.
+      "lists.sr.ht::worker".sock-group = mkDefault "postfix";
+      "lists.sr.ht::worker".reject-url = mkDefault "https://man.sr.ht/lists.sr.ht/etiquette.md";
+      "lists.sr.ht::worker".reject-mimetypes = mkDefault "text/html";
+
+    };
+
+    services.nginx.virtualHosts."lists.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.listssrht}/${pkgs.sourcehut.python.sitePackages}/listssrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/man.nix b/nixos/modules/services/misc/sourcehut/man.nix
new file mode 100644
index 00000000000..8ca271c32ee
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/man.nix
@@ -0,0 +1,124 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.man;
+  iniKey = "man.sr.ht";
+
+  drv = pkgs.sourcehut.mansrht;
+in
+{
+  options.services.sourcehut.man = {
+    user = mkOption {
+      type = types.str;
+      default = "mansrht";
+      description = ''
+        User for man.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5004;
+      description = ''
+        Port on which the "man" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "man.sr.ht";
+      description = ''
+        PostgreSQL database name for man.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/mansrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/mansrht"'';
+      description = ''
+        State path for man.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "man" cfg.services) {
+    assertions =
+      [
+        {
+          assertion = hasAttrByPath [ "git.sr.ht" "oauth-client-id" ] cfgIni;
+          message = "man.sr.ht needs access to git.sr.ht.";
+        }
+      ];
+
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          description = "man.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services.mansrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+        after = [ "postgresql.service" "network.target" ];
+        requires = [ "postgresql.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        description = "man.sr.ht website service";
+
+        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL man.sr.ht is being served at (protocol://domain)
+      "man.sr.ht".origin = mkDefault "http://man.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "man.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "man.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "man.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "man.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # man.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "man.sr.ht".oauth-client-id = mkDefault null;
+      "man.sr.ht".oauth-client-secret = mkDefault null;
+    };
+
+    services.nginx.virtualHosts."man.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.mansrht}/${pkgs.sourcehut.python.sitePackages}/mansrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/meta.nix b/nixos/modules/services/misc/sourcehut/meta.nix
new file mode 100644
index 00000000000..33e4f2332b5
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/meta.nix
@@ -0,0 +1,213 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.meta;
+  iniKey = "meta.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.metasrht;
+in
+{
+  options.services.sourcehut.meta = {
+    user = mkOption {
+      type = types.str;
+      default = "metasrht";
+      description = ''
+        User for meta.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5000;
+      description = ''
+        Port on which the "meta" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "meta.sr.ht";
+      description = ''
+        PostgreSQL database name for meta.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/metasrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/metasrht"'';
+      description = ''
+        State path for meta.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "meta" cfg.services) {
+    assertions =
+      [
+        {
+          assertion = with cfgIni."meta.sr.ht::billing"; enabled == "yes" -> (stripe-public-key != null && stripe-secret-key != null);
+          message = "If meta.sr.ht::billing is enabled, the keys should be defined.";
+        }
+      ];
+
+    users = {
+      users = {
+        ${user} = {
+          isSystemUser = true;
+          group = user;
+          description = "meta.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.cron.systemCronJobs = [ "0 0 * * * ${cfg.python}/bin/metasrht-daily" ];
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services = {
+        metasrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "meta.sr.ht website service";
+
+          preStart = ''
+            # Configure client(s) as "preauthorized"
+            ${concatMapStringsSep "\n\n"
+              (attr: ''
+                if ! test -e "${statePath}/${attr}.oauth" || [ "$(cat ${statePath}/${attr}.oauth)" != "${cfgIni."${attr}".oauth-client-id}" ]; then
+                  # Configure ${attr}'s OAuth client as "preauthorized"
+                  psql ${database} \
+                    -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${cfgIni."${attr}".oauth-client-id}'"
+
+                  printf "%s" "${cfgIni."${attr}".oauth-client-id}" > "${statePath}/${attr}.oauth"
+                fi
+              '')
+              (builtins.attrNames (filterAttrs
+                (k: v: !(hasInfix "::" k) && builtins.hasAttr "oauth-client-id" v && v.oauth-client-id != null)
+                cfg.settings))}
+          '';
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+        };
+
+        metasrht-api = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "meta.sr.ht api service";
+
+          preStart = ''
+            # Configure client(s) as "preauthorized"
+            ${concatMapStringsSep "\n\n"
+              (attr: ''
+                if ! test -e "${statePath}/${attr}.oauth" || [ "$(cat ${statePath}/${attr}.oauth)" != "${cfgIni."${attr}".oauth-client-id}" ]; then
+                  # Configure ${attr}'s OAuth client as "preauthorized"
+                  psql ${database} \
+                    -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${cfgIni."${attr}".oauth-client-id}'"
+
+                  printf "%s" "${cfgIni."${attr}".oauth-client-id}" > "${statePath}/${attr}.oauth"
+                fi
+              '')
+              (builtins.attrNames (filterAttrs
+                (k: v: !(hasInfix "::" k) && builtins.hasAttr "oauth-client-id" v && v.oauth-client-id != null)
+                cfg.settings))}
+          '';
+
+          serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b :${toString (port + 100)}";
+        };
+
+        metasrht-webhooks = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "meta.sr.ht webhooks service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
+          };
+
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL meta.sr.ht is being served at (protocol://domain)
+      "meta.sr.ht".origin = mkDefault "https://meta.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "meta.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "meta.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "meta.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "meta.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # If "yes", the user will be sent the stock sourcehut welcome emails after
+      # signup (requires cron to be configured properly). These are specific to the
+      # sr.ht instance so you probably want to patch these before enabling this.
+      "meta.sr.ht".welcome-emails = mkDefault "no";
+
+      # The redis connection used for the webhooks worker
+      "meta.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/6";
+
+      # If "no", public registration will not be permitted.
+      "meta.sr.ht::settings".registration = mkDefault "no";
+      # Where to redirect new users upon registration
+      "meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${cfg.originBase}";
+      # How many invites each user is issued upon registration (only applicable if
+      # open registration is disabled)
+      "meta.sr.ht::settings".user-invites = mkDefault 5;
+
+      # Origin URL for API, 100 more than web
+      "meta.sr.ht".api-origin = mkDefault "http://localhost:5100";
+
+      # You can add aliases for the client IDs of commonly used OAuth clients here.
+      #
+      # Example:
+      "meta.sr.ht::aliases" = mkDefault { };
+      # "meta.sr.ht::aliases"."git.sr.ht" = 12345;
+
+      # "yes" to enable the billing system
+      "meta.sr.ht::billing".enabled = mkDefault "no";
+      # Get your keys at https://dashboard.stripe.com/account/apikeys
+      "meta.sr.ht::billing".stripe-public-key = mkDefault null;
+      "meta.sr.ht::billing".stripe-secret-key = mkDefault null;
+    };
+
+    services.nginx.virtualHosts."meta.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.metasrht}/${pkgs.sourcehut.python.sitePackages}/metasrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/paste.nix b/nixos/modules/services/misc/sourcehut/paste.nix
new file mode 100644
index 00000000000..b481ebaf891
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/paste.nix
@@ -0,0 +1,135 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.paste;
+  iniKey = "paste.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.pastesrht;
+in
+{
+  options.services.sourcehut.paste = {
+    user = mkOption {
+      type = types.str;
+      default = "pastesrht";
+      description = ''
+        User for paste.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5011;
+      description = ''
+        Port on which the "paste" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "paste.sr.ht";
+      description = ''
+        PostgreSQL database name for paste.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/pastesrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/pastesrht"'';
+      description = ''
+        State path for pastesrht.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "paste" cfg.services) {
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          description = "paste.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services = {
+        pastesrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "paste.sr.ht website service";
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+        };
+
+        pastesrht-webhooks = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "paste.sr.ht webhooks service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
+          };
+
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL paste.sr.ht is being served at (protocol://domain)
+      "paste.sr.ht".origin = mkDefault "http://paste.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "paste.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "paste.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "paste.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "paste.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # paste.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "paste.sr.ht".oauth-client-id = mkDefault null;
+      "paste.sr.ht".oauth-client-secret = mkDefault null;
+      "paste.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/5";
+    };
+
+    services.nginx.virtualHosts."paste.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.pastesrht}/${pkgs.sourcehut.python.sitePackages}/pastesrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/service.nix b/nixos/modules/services/misc/sourcehut/service.nix
new file mode 100644
index 00000000000..f1706ad0a6a
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/service.nix
@@ -0,0 +1,375 @@
+srv:
+{ configIniOfService
+, srvsrht ? "${srv}srht" # Because "buildsrht" does not follow that pattern (missing an "s").
+, iniKey ? "${srv}.sr.ht"
+, webhooks ? false
+, extraTimers ? {}
+, mainService ? {}
+, extraServices ? {}
+, extraConfig ? {}
+, port
+}:
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  inherit (config.services) postgresql;
+  redis = config.services.redis.servers."sourcehut-${srvsrht}";
+  inherit (config.users) users;
+  cfg = config.services.sourcehut;
+  configIni = configIniOfService srv;
+  srvCfg = cfg.${srv};
+  baseService = serviceName: { allowStripe ? false }: extraService: let
+    runDir = "/run/sourcehut/${serviceName}";
+    rootDir = "/run/sourcehut/chroots/${serviceName}";
+    in
+    mkMerge [ extraService {
+    after = [ "network.target" ] ++
+      optional cfg.postgresql.enable "postgresql.service" ++
+      optional cfg.redis.enable "redis-sourcehut-${srvsrht}.service";
+    requires =
+      optional cfg.postgresql.enable "postgresql.service" ++
+      optional cfg.redis.enable "redis-sourcehut-${srvsrht}.service";
+    path = [ pkgs.gawk ];
+    environment.HOME = runDir;
+    serviceConfig = {
+      User = mkDefault srvCfg.user;
+      Group = mkDefault srvCfg.group;
+      RuntimeDirectory = [
+        "sourcehut/${serviceName}"
+        # Used by *srht-keys which reads ../config.ini
+        "sourcehut/${serviceName}/subdir"
+        "sourcehut/chroots/${serviceName}"
+      ];
+      RuntimeDirectoryMode = "2750";
+      # No need for the chroot path once inside the chroot
+      InaccessiblePaths = [ "-+${rootDir}" ];
+      # g+rx is for group members (eg. fcgiwrap or nginx)
+      # to read Git/Mercurial repositories, buildlogs, etc.
+      # o+x is for intermediate directories created by BindPaths= and like,
+      # as they're owned by root:root.
+      UMask = "0026";
+      RootDirectory = rootDir;
+      RootDirectoryStartOnly = true;
+      PrivateTmp = true;
+      MountAPIVFS = true;
+      # config.ini is looked up in there, before /etc/srht/config.ini
+      # Note that it fails to be set in ExecStartPre=
+      WorkingDirectory = mkDefault ("-"+runDir);
+      BindReadOnlyPaths = [
+        builtins.storeDir
+        "/etc"
+        "/run/booted-system"
+        "/run/current-system"
+        "/run/systemd"
+        ] ++
+        optional cfg.postgresql.enable "/run/postgresql" ++
+        optional cfg.redis.enable "/run/redis-sourcehut-${srvsrht}";
+      # LoadCredential= are unfortunately not available in ExecStartPre=
+      # Hence this one is run as root (the +) with RootDirectoryStartOnly=
+      # to reach credentials wherever they are.
+      # Note that each systemd service gets its own ${runDir}/config.ini file.
+      ExecStartPre = mkBefore [("+"+pkgs.writeShellScript "${serviceName}-credentials" ''
+        set -x
+        # Replace values begining with a '<' by the content of the file whose name is after.
+        gawk '{ if (match($0,/^([^=]+=)<(.+)/,m)) { getline f < m[2]; print m[1] f } else print $0 }' ${configIni} |
+        ${optionalString (!allowStripe) "gawk '!/^stripe-secret-key=/' |"}
+        install -o ${srvCfg.user} -g root -m 400 /dev/stdin ${runDir}/config.ini
+      '')];
+      # The following options are only for optimizing:
+      # systemd-analyze security
+      AmbientCapabilities = "";
+      CapabilityBoundingSet = "";
+      # ProtectClock= adds DeviceAllow=char-rtc r
+      DeviceAllow = "";
+      LockPersonality = true;
+      MemoryDenyWriteExecute = true;
+      NoNewPrivileges = true;
+      PrivateDevices = true;
+      PrivateMounts = true;
+      PrivateNetwork = mkDefault false;
+      PrivateUsers = true;
+      ProcSubset = "pid";
+      ProtectClock = true;
+      ProtectControlGroups = true;
+      ProtectHome = true;
+      ProtectHostname = true;
+      ProtectKernelLogs = true;
+      ProtectKernelModules = true;
+      ProtectKernelTunables = true;
+      ProtectProc = "invisible";
+      ProtectSystem = "strict";
+      RemoveIPC = true;
+      RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+      RestrictNamespaces = true;
+      RestrictRealtime = true;
+      RestrictSUIDSGID = true;
+      #SocketBindAllow = [ "tcp:${toString srvCfg.port}" "tcp:${toString srvCfg.prometheusPort}" ];
+      #SocketBindDeny = "any";
+      SystemCallFilter = [
+        "@system-service"
+        "~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@timer"
+        "@chown" "@setuid"
+      ];
+      SystemCallArchitectures = "native";
+    };
+  } ];
+in
+{
+  options.services.sourcehut.${srv} = {
+    enable = mkEnableOption "${srv} service";
+
+    user = mkOption {
+      type = types.str;
+      default = srvsrht;
+      description = ''
+        User for ${srv}.sr.ht.
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = srvsrht;
+      description = ''
+        Group for ${srv}.sr.ht.
+        Membership grants access to the Git/Mercurial repositories by default,
+        but not to the config.ini file (where secrets are).
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = port;
+      description = ''
+        Port on which the "${srv}" backend should listen.
+      '';
+    };
+
+    redis = {
+      host = mkOption {
+        type = types.str;
+        default = "unix:/run/redis-sourcehut-${srvsrht}/redis.sock?db=0";
+        example = "redis://shared.wireguard:6379/0";
+        description = ''
+          The redis host URL. This is used for caching and temporary storage, and must
+          be shared between nodes (e.g. git1.sr.ht and git2.sr.ht), but need not be
+          shared between services. It may be shared between services, however, with no
+          ill effect, if this better suits your infrastructure.
+        '';
+      };
+    };
+
+    postgresql = {
+      database = mkOption {
+        type = types.str;
+        default = "${srv}.sr.ht";
+        description = ''
+          PostgreSQL database name for the ${srv}.sr.ht service,
+          used if <xref linkend="opt-services.sourcehut.postgresql.enable"/> is <literal>true</literal>.
+        '';
+      };
+    };
+
+    gunicorn = {
+      extraArgs = mkOption {
+        type = with types; listOf str;
+        default = ["--timeout 120" "--workers 1" "--log-level=info"];
+        description = "Extra arguments passed to Gunicorn.";
+      };
+    };
+  } // optionalAttrs webhooks {
+    webhooks = {
+      extraArgs = mkOption {
+        type = with types; listOf str;
+        default = ["--loglevel DEBUG" "--pool eventlet" "--without-heartbeat"];
+        description = "Extra arguments passed to the Celery responsible for webhooks.";
+      };
+      celeryConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Content of the <literal>celeryconfig.py</literal> used by the Celery responsible for webhooks.";
+      };
+    };
+  };
+
+  config = lib.mkIf (cfg.enable && srvCfg.enable) (mkMerge [ extraConfig {
+    users = {
+      users = {
+        "${srvCfg.user}" = {
+          isSystemUser = true;
+          group = mkDefault srvCfg.group;
+          description = mkDefault "sourcehut user for ${srv}.sr.ht";
+        };
+      };
+      groups = {
+        "${srvCfg.group}" = { };
+      } // optionalAttrs (cfg.postgresql.enable
+        && hasSuffix "0" (postgresql.settings.unix_socket_permissions or "")) {
+        "postgres".members = [ srvCfg.user ];
+      } // optionalAttrs (cfg.redis.enable
+        && hasSuffix "0" (redis.settings.unixsocketperm or "")) {
+        "redis-sourcehut-${srvsrht}".members = [ srvCfg.user ];
+      };
+    };
+
+    services.nginx = mkIf cfg.nginx.enable {
+      virtualHosts."${srv}.${cfg.settings."sr.ht".global-domain}" = mkMerge [ {
+        forceSSL = mkDefault true;
+        locations."/".proxyPass = "http://${cfg.listenAddress}:${toString srvCfg.port}";
+        locations."/static" = {
+          root = "${pkgs.sourcehut.${srvsrht}}/${pkgs.sourcehut.python.sitePackages}/${srvsrht}";
+          extraConfig = mkDefault ''
+            expires 30d;
+          '';
+        };
+      } cfg.nginx.virtualHost ];
+    };
+
+    services.postgresql = mkIf cfg.postgresql.enable {
+      authentication = ''
+        local ${srvCfg.postgresql.database} ${srvCfg.user} trust
+      '';
+      ensureDatabases = [ srvCfg.postgresql.database ];
+      ensureUsers = map (name: {
+          inherit name;
+          ensurePermissions = { "DATABASE \"${srvCfg.postgresql.database}\"" = "ALL PRIVILEGES"; };
+        }) [srvCfg.user];
+    };
+
+    services.sourcehut.services = mkDefault (filter (s: cfg.${s}.enable)
+      [ "builds" "dispatch" "git" "hg" "hub" "lists" "man" "meta" "pages" "paste" "todo" ]);
+
+    services.sourcehut.settings = mkMerge [
+      {
+        "${srv}.sr.ht".origin = mkDefault "https://${srv}.${cfg.settings."sr.ht".global-domain}";
+      }
+
+      (mkIf cfg.postgresql.enable {
+        "${srv}.sr.ht".connection-string = mkDefault "postgresql:///${srvCfg.postgresql.database}?user=${srvCfg.user}&host=/run/postgresql";
+      })
+    ];
+
+    services.redis.servers."sourcehut-${srvsrht}" = mkIf cfg.redis.enable {
+      enable = true;
+      databases = 3;
+      syslog = true;
+      # TODO: set a more informed value
+      save = mkDefault [ [1800 10] [300 100] ];
+      settings = {
+        # TODO: set a more informed value
+        maxmemory = "128MB";
+        maxmemory-policy = "volatile-ttl";
+      };
+    };
+
+    systemd.services = mkMerge [
+      {
+        "${srvsrht}" = baseService srvsrht { allowStripe = srv == "meta"; } (mkMerge [
+        {
+          description = "sourcehut ${srv}.sr.ht website service";
+          before = optional cfg.nginx.enable "nginx.service";
+          wants = optional cfg.nginx.enable "nginx.service";
+          wantedBy = [ "multi-user.target" ];
+          path = optional cfg.postgresql.enable postgresql.package;
+          # Beware: change in credentials' content will not trigger restart.
+          restartTriggers = [ configIni ];
+          serviceConfig = {
+            Type = "simple";
+            Restart = mkDefault "always";
+            #RestartSec = mkDefault "2min";
+            StateDirectory = [ "sourcehut/${srvsrht}" ];
+            StateDirectoryMode = "2750";
+            ExecStart = "${cfg.python}/bin/gunicorn ${srvsrht}.app:app --name ${srvsrht} --bind ${cfg.listenAddress}:${toString srvCfg.port} " + concatStringsSep " " srvCfg.gunicorn.extraArgs;
+          };
+          preStart = let
+            version = pkgs.sourcehut.${srvsrht}.version;
+            stateDir = "/var/lib/sourcehut/${srvsrht}";
+            in mkBefore ''
+            set -x
+            # Use the /run/sourcehut/${srvsrht}/config.ini
+            # installed by a previous ExecStartPre= in baseService
+            cd /run/sourcehut/${srvsrht}
+
+            if test ! -e ${stateDir}/db; then
+              # Setup the initial database.
+              # Note that it stamps the alembic head afterward
+              ${cfg.python}/bin/${srvsrht}-initdb
+              echo ${version} >${stateDir}/db
+            fi
+
+            ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
+              if [ "$(cat ${stateDir}/db)" != "${version}" ]; then
+                # Manage schema migrations using alembic
+                ${cfg.python}/bin/${srvsrht}-migrate -a upgrade head
+                echo ${version} >${stateDir}/db
+              fi
+            ''}
+
+            # Update copy of each users' profile to the latest
+            # See https://lists.sr.ht/~sircmpwn/sr.ht-admins/<20190302181207.GA13778%40cirno.my.domain>
+            if test ! -e ${stateDir}/webhook; then
+              # Update ${iniKey}'s users' profile copy to the latest
+              ${cfg.python}/bin/srht-update-profiles ${iniKey}
+              touch ${stateDir}/webhook
+            fi
+          '';
+        } mainService ]);
+      }
+
+      (mkIf webhooks {
+        "${srvsrht}-webhooks" = baseService "${srvsrht}-webhooks" {}
+          {
+            description = "sourcehut ${srv}.sr.ht webhooks service";
+            after = [ "${srvsrht}.service" ];
+            wantedBy = [ "${srvsrht}.service" ];
+            partOf = [ "${srvsrht}.service" ];
+            preStart = ''
+              cp ${pkgs.writeText "${srvsrht}-webhooks-celeryconfig.py" srvCfg.webhooks.celeryConfig} \
+                 /run/sourcehut/${srvsrht}-webhooks/celeryconfig.py
+            '';
+            serviceConfig = {
+              Type = "simple";
+              Restart = "always";
+              ExecStart = "${cfg.python}/bin/celery --app ${srvsrht}.webhooks worker --hostname ${srvsrht}-webhooks@%%h " + concatStringsSep " " srvCfg.webhooks.extraArgs;
+              # Avoid crashing: os.getloadavg()
+              ProcSubset = mkForce "all";
+            };
+          };
+      })
+
+      (mapAttrs (timerName: timer: (baseService timerName {} (mkMerge [
+        {
+          description = "sourcehut ${timerName} service";
+          after = [ "network.target" "${srvsrht}.service" ];
+          serviceConfig = {
+            Type = "oneshot";
+            ExecStart = "${cfg.python}/bin/${timerName}";
+          };
+        }
+        (timer.service or {})
+      ]))) extraTimers)
+
+      (mapAttrs (serviceName: extraService: baseService serviceName {} (mkMerge [
+        {
+          description = "sourcehut ${serviceName} service";
+          # So that extraServices have the PostgreSQL database initialized.
+          after = [ "${srvsrht}.service" ];
+          wantedBy = [ "${srvsrht}.service" ];
+          partOf = [ "${srvsrht}.service" ];
+          serviceConfig = {
+            Type = "simple";
+            Restart = mkDefault "always";
+          };
+        }
+        extraService
+      ])) extraServices)
+    ];
+
+    systemd.timers = mapAttrs (timerName: timer:
+      {
+        description = "sourcehut timer for ${timerName}";
+        wantedBy = [ "timers.target" ];
+        inherit (timer) timerConfig;
+      }) extraTimers;
+  } ]);
+}
diff --git a/nixos/modules/services/misc/sourcehut/sourcehut.xml b/nixos/modules/services/misc/sourcehut/sourcehut.xml
new file mode 100644
index 00000000000..41094f65a94
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/sourcehut.xml
@@ -0,0 +1,119 @@
+<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/sourcehut/todo.nix b/nixos/modules/services/misc/sourcehut/todo.nix
new file mode 100644
index 00000000000..262fa48f59d
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/todo.nix
@@ -0,0 +1,163 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.todo;
+  iniKey = "todo.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.todosrht;
+in
+{
+  options.services.sourcehut.todo = {
+    user = mkOption {
+      type = types.str;
+      default = "todosrht";
+      description = ''
+        User for todo.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5003;
+      description = ''
+        Port on which the "todo" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "todo.sr.ht";
+      description = ''
+        PostgreSQL database name for todo.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/todosrht";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/todosrht"'';
+      description = ''
+        State path for todo.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "todo" cfg.services) {
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          extraGroups = [ "postfix" ];
+          description = "todo.sr.ht user";
+        };
+      };
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services = {
+        todosrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "todo.sr.ht website service";
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+        };
+
+       todosrht-lmtp = {
+         after = [ "postgresql.service" "network.target" ];
+         bindsTo = [ "postgresql.service" ];
+         wantedBy = [ "multi-user.target" ];
+
+         description = "todo.sr.ht process service";
+         serviceConfig = {
+           Type = "simple";
+           User = user;
+           Restart = "always";
+           ExecStart = "${cfg.python}/bin/todosrht-lmtp";
+         };
+       };
+
+        todosrht-webhooks = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "todo.sr.ht webhooks service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
+          };
+
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL todo.sr.ht is being served at (protocol://domain)
+      "todo.sr.ht".origin = mkDefault "http://todo.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "todo.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "todo.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "todo.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "todo.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # todo.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "todo.sr.ht".oauth-client-id = mkDefault null;
+      "todo.sr.ht".oauth-client-secret = mkDefault null;
+      # Outgoing email for notifications generated by users
+      "todo.sr.ht".notify-from = mkDefault "CHANGEME@example.org";
+      # The redis connection used for the webhooks worker
+      "todo.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
+      # Network-key
+      "todo.sr.ht".network-key = mkDefault null;
+
+      # Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
+      # Alternatively, specify IP:PORT and an SMTP server will be run instead.
+      "todo.sr.ht::mail".sock = mkDefault "/tmp/todo.sr.ht-lmtp.sock";
+      # The lmtp daemon will make the unix socket group-read/write for users in this
+      # group.
+      "todo.sr.ht::mail".sock-group = mkDefault "postfix";
+
+      "todo.sr.ht::mail".posting-domain = mkDefault "todo.${cfg.originBase}";
+    };
+
+    services.nginx.virtualHosts."todo.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.todosrht}/${pkgs.sourcehut.python.sitePackages}/todosrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/spice-vdagentd.nix b/nixos/modules/services/misc/spice-vdagentd.nix
new file mode 100644
index 00000000000..2dd9fcf68ab
--- /dev/null
+++ b/nixos/modules/services/misc/spice-vdagentd.nix
@@ -0,0 +1,30 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.spice-vdagentd;
+in
+{
+  options = {
+    services.spice-vdagentd = {
+      enable = mkEnableOption "Spice guest vdagent daemon";
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.spice-vdagent ];
+
+    systemd.services.spice-vdagentd = {
+      description = "spice-vdagent daemon";
+      wantedBy = [ "graphical.target" ];
+      preStart = ''
+        mkdir -p "/run/spice-vdagentd/"
+      '';
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${pkgs.spice-vdagent}/bin/spice-vdagentd";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/ssm-agent.nix b/nixos/modules/services/misc/ssm-agent.nix
new file mode 100644
index 00000000000..4ae596ade17
--- /dev/null
+++ b/nixos/modules/services/misc/ssm-agent.nix
@@ -0,0 +1,73 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.ssm-agent;
+
+  # The SSM agent doesn't pay attention to our /etc/os-release yet, and the lsb-release tool
+  # in nixpkgs doesn't seem to work properly on NixOS, so let's just fake the two fields SSM
+  # looks for. See https://github.com/aws/amazon-ssm-agent/issues/38 for upstream fix.
+  fake-lsb-release = pkgs.writeScriptBin "lsb_release" ''
+    #!${pkgs.runtimeShell}
+
+    case "$1" in
+      -i) echo "nixos";;
+      -r) echo "${config.system.nixos.version}";;
+    esac
+  '';
+in {
+  options.services.ssm-agent = {
+    enable = mkEnableOption "AWS SSM agent";
+
+    package = mkOption {
+      type = types.path;
+      description = "The SSM agent package to use";
+      default = pkgs.ssm-agent.override { overrideEtc = false; };
+      defaultText = literalExpression "pkgs.ssm-agent.override { overrideEtc = false; }";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.ssm-agent = {
+      inherit (cfg.package.meta) description;
+      after    = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      path = [ fake-lsb-release pkgs.coreutils ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/amazon-ssm-agent";
+        KillMode = "process";
+        # We want this restating pretty frequently. It could be our only means
+        # of accessing the instance.
+        Restart = "always";
+        RestartSec = "1min";
+      };
+    };
+
+    # Add user that Session Manager needs, and give it sudo.
+    # This is consistent with Amazon Linux 2 images.
+    security.sudo.extraRules = [
+      {
+        users = [ "ssm-user" ];
+        commands = [
+          {
+            command = "ALL";
+            options = [ "NOPASSWD" ];
+          }
+        ];
+      }
+    ];
+    # On Amazon Linux 2 images, the ssm-user user is pretty much a
+    # normal user with its own group. We do the same.
+    users.groups.ssm-user = {};
+    users.users.ssm-user = {
+      isNormalUser = true;
+      group = "ssm-user";
+    };
+
+    environment.etc."amazon/ssm/seelog.xml".source = "${cfg.package}/seelog.xml.template";
+
+    environment.etc."amazon/ssm/amazon-ssm-agent.json".source =  "${cfg.package}/etc/amazon/ssm/amazon-ssm-agent.json.template";
+
+  };
+}
diff --git a/nixos/modules/services/misc/sssd.nix b/nixos/modules/services/misc/sssd.nix
new file mode 100644
index 00000000000..386281e2b7c
--- /dev/null
+++ b/nixos/modules/services/misc/sssd.nix
@@ -0,0 +1,97 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.sssd;
+  nscd = config.services.nscd;
+in {
+  options = {
+    services.sssd = {
+      enable = mkEnableOption "the System Security Services Daemon";
+
+      config = mkOption {
+        type = types.lines;
+        description = "Contents of <filename>sssd.conf</filename>.";
+        default = ''
+          [sssd]
+          config_file_version = 2
+          services = nss, pam
+          domains = shadowutils
+
+          [nss]
+
+          [pam]
+
+          [domain/shadowutils]
+          id_provider = proxy
+          proxy_lib_name = files
+          auth_provider = proxy
+          proxy_pam_target = sssd-shadowutils
+          proxy_fast_alias = True
+        '';
+      };
+
+      sshAuthorizedKeysIntegration = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to make sshd look up authorized keys from SSS.
+          For this to work, the <literal>ssh</literal> SSS service must be enabled in the sssd configuration.
+        '';
+      };
+    };
+  };
+  config = mkMerge [
+    (mkIf cfg.enable {
+      systemd.services.sssd = {
+        description = "System Security Services Daemon";
+        wantedBy    = [ "multi-user.target" ];
+        before = [ "systemd-user-sessions.service" "nss-user-lookup.target" ];
+        after = [ "network-online.target" "nscd.service" ];
+        requires = [ "network-online.target" "nscd.service" ];
+        wants = [ "nss-user-lookup.target" ];
+        restartTriggers = [
+          config.environment.etc."nscd.conf".source
+          config.environment.etc."sssd/sssd.conf".source
+        ];
+        script = ''
+          export LDB_MODULES_PATH+="''${LDB_MODULES_PATH+:}${pkgs.ldb}/modules/ldb:${pkgs.sssd}/modules/ldb"
+          mkdir -p /var/lib/sss/{pubconf,db,mc,pipes,gpo_cache,secrets} /var/lib/sss/pipes/private /var/lib/sss/pubconf/krb5.include.d
+          ${pkgs.sssd}/bin/sssd -D
+        '';
+        serviceConfig = {
+          Type = "forking";
+          PIDFile = "/run/sssd.pid";
+        };
+      };
+
+      environment.etc."sssd/sssd.conf" = {
+        text = cfg.config;
+        mode = "0400";
+      };
+
+      system.nssModules = [ pkgs.sssd ];
+      system.nssDatabases = {
+        group = [ "sss" ];
+        passwd = [ "sss" ];
+        services = [ "sss" ];
+        shadow = [ "sss" ];
+      };
+      services.dbus.packages = [ pkgs.sssd ];
+    })
+
+    (mkIf cfg.sshAuthorizedKeysIntegration {
+    # Ugly: sshd refuses to start if a store path is given because /nix/store is group-writable.
+    # So indirect by a symlink.
+    environment.etc."ssh/authorized_keys_command" = {
+      mode = "0755";
+      text = ''
+        #!/bin/sh
+        exec ${pkgs.sssd}/bin/sss_ssh_authorizedkeys "$@"
+      '';
+    };
+    services.openssh.authorizedKeysCommand = "/etc/ssh/authorized_keys_command";
+    services.openssh.authorizedKeysCommandUser = "nobody";
+  })];
+
+  meta.maintainers = with maintainers; [ bbigras ];
+}
diff --git a/nixos/modules/services/misc/subsonic.nix b/nixos/modules/services/misc/subsonic.nix
new file mode 100644
index 00000000000..2dda8970dd3
--- /dev/null
+++ b/nixos/modules/services/misc/subsonic.nix
@@ -0,0 +1,169 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.subsonic;
+  opt = options.services.subsonic;
+in {
+  options = {
+    services.subsonic = {
+      enable = mkEnableOption "Subsonic daemon";
+
+      home = mkOption {
+        type = types.path;
+        default = "/var/lib/subsonic";
+        description = ''
+          The directory where Subsonic will create files.
+          Make sure it is writable.
+        '';
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = ''
+          The host name or IP address on which to bind Subsonic.
+          Only relevant if you have multiple network interfaces and want
+          to make Subsonic available on only one of them. The default value
+          will bind Subsonic to all available network interfaces.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 4040;
+        description = ''
+          The port on which Subsonic will listen for
+          incoming HTTP traffic. Set to 0 to disable.
+        '';
+      };
+
+      httpsPort = mkOption {
+        type = types.port;
+        default = 0;
+        description = ''
+          The port on which Subsonic will listen for
+          incoming HTTPS traffic. Set to 0 to disable.
+        '';
+      };
+
+      contextPath = mkOption {
+        type = types.path;
+        default = "/";
+        description = ''
+          The context path, i.e., the last part of the Subsonic
+          URL. Typically '/' or '/subsonic'. Default '/'
+        '';
+      };
+
+      maxMemory = mkOption {
+        type = types.int;
+        default = 100;
+        description = ''
+          The memory limit (max Java heap size) in megabytes.
+          Default: 100
+        '';
+      };
+
+      defaultMusicFolder = mkOption {
+        type = types.path;
+        default = "/var/music";
+        description = ''
+          Configure Subsonic to use this folder for music.  This option
+          only has effect the first time Subsonic is started.
+        '';
+      };
+
+      defaultPodcastFolder = mkOption {
+        type = types.path;
+        default = "/var/music/Podcast";
+        description = ''
+          Configure Subsonic to use this folder for Podcasts.  This option
+          only has effect the first time Subsonic is started.
+        '';
+      };
+
+      defaultPlaylistFolder = mkOption {
+        type = types.path;
+        default = "/var/playlists";
+        description = ''
+          Configure Subsonic to use this folder for playlists.  This option
+          only has effect the first time Subsonic is started.
+        '';
+      };
+
+      transcoders = mkOption {
+        type = types.listOf types.path;
+        default = [ "${pkgs.ffmpeg.bin}/bin/ffmpeg" ];
+        defaultText = literalExpression ''[ "''${pkgs.ffmpeg.bin}/bin/ffmpeg" ]'';
+        description = ''
+          List of paths to transcoder executables that should be accessible
+          from Subsonic. Symlinks will be created to each executable inside
+          ''${config.${opt.home}}/transcoders.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.subsonic = {
+      description = "Personal media streamer";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      script = ''
+        ${pkgs.jre8}/bin/java -Xmx${toString cfg.maxMemory}m \
+          -Dsubsonic.home=${cfg.home} \
+          -Dsubsonic.host=${cfg.listenAddress} \
+          -Dsubsonic.port=${toString cfg.port} \
+          -Dsubsonic.httpsPort=${toString cfg.httpsPort} \
+          -Dsubsonic.contextPath=${cfg.contextPath} \
+          -Dsubsonic.defaultMusicFolder=${cfg.defaultMusicFolder} \
+          -Dsubsonic.defaultPodcastFolder=${cfg.defaultPodcastFolder} \
+          -Dsubsonic.defaultPlaylistFolder=${cfg.defaultPlaylistFolder} \
+          -Djava.awt.headless=true \
+          -verbose:gc \
+          -jar ${pkgs.subsonic}/subsonic-booter-jar-with-dependencies.jar
+      '';
+
+      preStart = ''
+        # Formerly this module set cfg.home to /var/subsonic. Try to move
+        # /var/subsonic to cfg.home.
+        oldHome="/var/subsonic"
+        if [ "${cfg.home}" != "$oldHome" ] &&
+                ! [ -e "${cfg.home}" ] &&
+                [ -d "$oldHome" ] &&
+                [ $(${pkgs.coreutils}/bin/stat -c %u "$oldHome") -eq \
+                    ${toString config.users.users.subsonic.uid} ]; then
+            logger Moving "$oldHome" to "${cfg.home}"
+            ${pkgs.coreutils}/bin/mv -T "$oldHome" "${cfg.home}"
+        fi
+
+        # Install transcoders.
+        ${pkgs.coreutils}/bin/rm -rf ${cfg.home}/transcode ; \
+        ${pkgs.coreutils}/bin/mkdir -p ${cfg.home}/transcode ; \
+        ${pkgs.bash}/bin/bash -c ' \
+          for exe in "$@"; do \
+            ${pkgs.coreutils}/bin/ln -sf "$exe" ${cfg.home}/transcode; \
+          done' IGNORED_FIRST_ARG ${toString cfg.transcoders}
+      '';
+      serviceConfig = {
+        # Needed for Subsonic to find subsonic.war.
+        WorkingDirectory = "${pkgs.subsonic}";
+        Restart = "always";
+        User = "subsonic";
+        UMask = "0022";
+      };
+    };
+
+    users.users.subsonic = {
+      description = "Subsonic daemon user";
+      home = cfg.home;
+      createHome = true;
+      group = "subsonic";
+      uid = config.ids.uids.subsonic;
+    };
+
+    users.groups.subsonic.gid = config.ids.gids.subsonic;
+  };
+}
diff --git a/nixos/modules/services/misc/sundtek.nix b/nixos/modules/services/misc/sundtek.nix
new file mode 100644
index 00000000000..e3234518c94
--- /dev/null
+++ b/nixos/modules/services/misc/sundtek.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.sundtek;
+
+in
+{
+  options.services.sundtek = {
+    enable = mkEnableOption "Sundtek driver";
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.sundtek ];
+
+    systemd.services.sundtek = {
+      description = "Sundtek driver";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = ''
+          ${pkgs.sundtek}/bin/mediasrv -d -v -p ${pkgs.sundtek}/bin ;\
+          ${pkgs.sundtek}/bin/mediaclient --start --wait-for-devices
+          '';
+        ExecStop = "${pkgs.sundtek}/bin/mediaclient --shutdown";
+        RemainAfterExit = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/svnserve.nix b/nixos/modules/services/misc/svnserve.nix
new file mode 100644
index 00000000000..5fa262ca3b9
--- /dev/null
+++ b/nixos/modules/services/misc/svnserve.nix
@@ -0,0 +1,46 @@
+# SVN server
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.svnserve;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.svnserve = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable svnserve to serve Subversion repositories through the SVN protocol.";
+      };
+
+      svnBaseDir = mkOption {
+        type = types.str;
+        default = "/repos";
+        description = "Base directory from which Subversion repositories are accessed.";
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.svnserve = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      preStart = "mkdir -p ${cfg.svnBaseDir}";
+      script = "${pkgs.subversion.out}/bin/svnserve -r ${cfg.svnBaseDir} -d --foreground --pid-file=/run/svnserve.pid";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/synergy.nix b/nixos/modules/services/misc/synergy.nix
new file mode 100644
index 00000000000..d6cd5d7f0d6
--- /dev/null
+++ b/nixos/modules/services/misc/synergy.nix
@@ -0,0 +1,149 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfgC = config.services.synergy.client;
+  cfgS = config.services.synergy.server;
+
+in
+
+{
+  ###### interface
+
+  options = {
+
+    services.synergy = {
+
+      # !!! All these option descriptions needs to be cleaned up.
+
+      client = {
+        enable = mkEnableOption "the Synergy client (receive keyboard and mouse events from a Synergy server)";
+
+        screenName = mkOption {
+          default = "";
+          type = types.str;
+          description = ''
+            Use the given name instead of the hostname to identify
+            ourselves to the server.
+          '';
+        };
+        serverAddress = mkOption {
+          type = types.str;
+          description = ''
+            The server address is of the form: [hostname][:port].  The
+            hostname must be the address or hostname of the server.  The
+            port overrides the default port, 24800.
+          '';
+        };
+        autoStart = mkOption {
+          default = true;
+          type = types.bool;
+          description = "Whether the Synergy client should be started automatically.";
+        };
+      };
+
+      server = {
+        enable = mkEnableOption "the Synergy server (send keyboard and mouse events)";
+
+        configFile = mkOption {
+          type = types.path;
+          default = "/etc/synergy-server.conf";
+          description = "The Synergy server configuration file.";
+        };
+        screenName = mkOption {
+          type = types.str;
+          default = "";
+          description = ''
+            Use the given name instead of the hostname to identify
+            this screen in the configuration.
+          '';
+        };
+        address = mkOption {
+          type = types.str;
+          default = "";
+          description = "Address on which to listen for clients.";
+        };
+        autoStart = mkOption {
+          default = true;
+          type = types.bool;
+          description = "Whether the Synergy server should be started automatically.";
+        };
+        tls = {
+          enable = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Whether TLS encryption should be used.
+
+              Using this requires a TLS certificate that can be
+              generated by starting the Synergy GUI once and entering
+              a valid product key.
+            '';
+          };
+
+          cert = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "~/.synergy/SSL/Synergy.pem";
+            description = "The TLS certificate to use for encryption.";
+          };
+        };
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkMerge [
+    (mkIf cfgC.enable {
+      systemd.user.services.synergy-client = {
+        after = [ "network.target" "graphical-session.target" ];
+        description = "Synergy client";
+        wantedBy = optional cfgC.autoStart "graphical-session.target";
+        path = [ pkgs.synergy ];
+        serviceConfig.ExecStart = ''${pkgs.synergy}/bin/synergyc -f ${optionalString (cfgC.screenName != "") "-n ${cfgC.screenName}"} ${cfgC.serverAddress}'';
+        serviceConfig.Restart = "on-failure";
+      };
+    })
+    (mkIf cfgS.enable {
+      systemd.user.services.synergy-server = {
+        after = [ "network.target" "graphical-session.target" ];
+        description = "Synergy server";
+        wantedBy = optional cfgS.autoStart "graphical-session.target";
+        path = [ pkgs.synergy ];
+        serviceConfig.ExecStart = ''${pkgs.synergy}/bin/synergys -c ${cfgS.configFile} -f${optionalString (cfgS.address != "") " -a ${cfgS.address}"}${optionalString (cfgS.screenName != "") " -n ${cfgS.screenName}"}${optionalString cfgS.tls.enable " --enable-crypto"}${optionalString (cfgS.tls.cert != null) (" --tls-cert=${cfgS.tls.cert}")}'';
+        serviceConfig.Restart = "on-failure";
+      };
+    })
+  ];
+
+}
+
+/* SYNERGY SERVER example configuration file
+section: screens
+  laptop:
+  dm:
+  win:
+end
+section: aliases
+    laptop:
+      192.168.5.5
+    dm:
+      192.168.5.78
+    win:
+      192.168.5.54
+end
+section: links
+   laptop:
+       left = dm
+   dm:
+       right = laptop
+       left = win
+  win:
+      right = dm
+end
+*/
diff --git a/nixos/modules/services/misc/sysprof.nix b/nixos/modules/services/misc/sysprof.nix
new file mode 100644
index 00000000000..ab91a8b586a
--- /dev/null
+++ b/nixos/modules/services/misc/sysprof.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+{
+  options = {
+    services.sysprof = {
+      enable = lib.mkEnableOption "sysprof profiling daemon";
+    };
+  };
+
+  config = lib.mkIf config.services.sysprof.enable {
+    environment.systemPackages = [ pkgs.sysprof ];
+
+    services.dbus.packages = [ pkgs.sysprof ];
+
+    systemd.packages = [ pkgs.sysprof ];
+  };
+
+  meta.maintainers = pkgs.sysprof.meta.maintainers;
+}
diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix
new file mode 100644
index 00000000000..ff63c41e193
--- /dev/null
+++ b/nixos/modules/services/misc/taskserver/default.nix
@@ -0,0 +1,569 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.taskserver;
+
+  taskd = "${pkgs.taskserver}/bin/taskd";
+
+  mkManualPkiOption = desc: mkOption {
+    type = types.nullOr types.path;
+    default = null;
+    description = desc + ''
+      <note><para>
+      Setting this option will prevent automatic CA creation and handling.
+      </para></note>
+    '';
+  };
+
+  manualPkiOptions = {
+    ca.cert = mkManualPkiOption ''
+      Fully qualified path to the CA certificate.
+    '';
+
+    server.cert = mkManualPkiOption ''
+      Fully qualified path to the server certificate.
+    '';
+
+    server.crl = mkManualPkiOption ''
+      Fully qualified path to the server certificate revocation list.
+    '';
+
+    server.key = mkManualPkiOption ''
+      Fully qualified path to the server key.
+    '';
+  };
+
+  mkAutoDesc = preamble: ''
+    ${preamble}
+
+    <note><para>
+    This option is for the automatically handled CA and will be ignored if any
+    of the <option>services.taskserver.pki.manual.*</option> options are set.
+    </para></note>
+  '';
+
+  mkExpireOption = desc: mkOption {
+    type = types.nullOr types.int;
+    default = null;
+    example = 365;
+    apply = val: if val == null then -1 else val;
+    description = mkAutoDesc ''
+      The expiration time of ${desc} in days or <literal>null</literal> for no
+      expiration time.
+    '';
+  };
+
+  autoPkiOptions = {
+    bits = mkOption {
+      type = types.int;
+      default = 4096;
+      example = 2048;
+      description = mkAutoDesc "The bit size for generated keys.";
+    };
+
+    expiration = {
+      ca = mkExpireOption "the CA certificate";
+      server = mkExpireOption "the server certificate";
+      client = mkExpireOption "client certificates";
+      crl = mkExpireOption "the certificate revocation list (CRL)";
+    };
+  };
+
+  needToCreateCA = let
+    notFound = path: let
+      dotted = concatStringsSep "." path;
+    in throw "Can't find option definitions for path `${dotted}'.";
+    findPkiDefinitions = path: attrs: let
+      mkSublist = key: val: let
+        newPath = path ++ singleton key;
+      in if isOption val
+         then attrByPath newPath (notFound newPath) cfg.pki.manual
+         else findPkiDefinitions newPath val;
+    in flatten (mapAttrsToList mkSublist attrs);
+  in all (x: x == null) (findPkiDefinitions [] manualPkiOptions);
+
+  orgOptions = { ... }: {
+    options.users = mkOption {
+      type = types.uniq (types.listOf types.str);
+      default = [];
+      example = [ "alice" "bob" ];
+      description = ''
+        A list of user names that belong to the organization.
+      '';
+    };
+
+    options.groups = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "workers" "slackers" ];
+      description = ''
+        A list of group names that belong to the organization.
+      '';
+    };
+  };
+
+  certtool = "${pkgs.gnutls.bin}/bin/certtool";
+
+  nixos-taskserver = with pkgs.python2.pkgs; buildPythonApplication {
+    name = "nixos-taskserver";
+
+    src = pkgs.runCommand "nixos-taskserver-src" { preferLocalBuild = true; } ''
+      mkdir -p "$out"
+      cat "${pkgs.substituteAll {
+        src = ./helper-tool.py;
+        inherit taskd certtool;
+        inherit (cfg) dataDir user group fqdn;
+        certBits = cfg.pki.auto.bits;
+        clientExpiration = cfg.pki.auto.expiration.client;
+        crlExpiration = cfg.pki.auto.expiration.crl;
+        isAutoConfig = if needToCreateCA then "True" else "False";
+      }}" > "$out/main.py"
+      cat > "$out/setup.py" <<EOF
+      from setuptools import setup
+      setup(name="nixos-taskserver",
+            py_modules=["main"],
+            install_requires=["Click"],
+            entry_points="[console_scripts]\\nnixos-taskserver=main:cli")
+      EOF
+    '';
+
+    propagatedBuildInputs = [ click ];
+  };
+
+in {
+  options = {
+    services.taskserver = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = let
+          url = "https://nixos.org/manual/nixos/stable/index.html#module-services-taskserver";
+        in ''
+          Whether to enable the Taskwarrior server.
+
+          More instructions about NixOS in conjuction with Taskserver can be
+          found <link xlink:href="${url}">in the NixOS manual</link>.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "taskd";
+        description = "User for Taskserver.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "taskd";
+        description = "Group for Taskserver.";
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/taskserver";
+        description = "Data directory for Taskserver.";
+      };
+
+      ciphers = mkOption {
+        type = types.nullOr (types.separatedString ":");
+        default = null;
+        example = "NORMAL:-VERS-SSL3.0";
+        description = let
+          url = "https://gnutls.org/manual/html_node/Priority-Strings.html";
+        in ''
+          List of GnuTLS ciphers to use. See the GnuTLS documentation about
+          priority strings at <link xlink:href="${url}"/> for full details.
+        '';
+      };
+
+      organisations = mkOption {
+        type = types.attrsOf (types.submodule orgOptions);
+        default = {};
+        example.myShinyOrganisation.users = [ "alice" "bob" ];
+        example.myShinyOrganisation.groups = [ "staff" "outsiders" ];
+        example.yetAnotherOrganisation.users = [ "foo" "bar" ];
+        description = ''
+          An attribute set where the keys name the organisation and the values
+          are a set of lists of <option>users</option> and
+          <option>groups</option>.
+        '';
+      };
+
+      confirmation = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Determines whether certain commands are confirmed.
+        '';
+      };
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Logs debugging information.
+        '';
+      };
+
+      extensions = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          Fully qualified path of the Taskserver extension scripts.
+          Currently there are none.
+        '';
+      };
+
+      ipLog = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Logs the IP addresses of incoming requests.
+        '';
+      };
+
+      queueSize = mkOption {
+        type = types.int;
+        default = 10;
+        description = ''
+          Size of the connection backlog, see <citerefentry>
+            <refentrytitle>listen</refentrytitle>
+            <manvolnum>2</manvolnum>
+          </citerefentry>.
+        '';
+      };
+
+      requestLimit = mkOption {
+        type = types.int;
+        default = 1048576;
+        description = ''
+          Size limit of incoming requests, in bytes.
+        '';
+      };
+
+      allowedClientIDs = mkOption {
+        type = with types; either str (listOf str);
+        default = [];
+        example = [ "[Tt]ask [2-9]+" ];
+        description = ''
+          A list of regular expressions that are matched against the reported
+          client id (such as <literal>task 2.3.0</literal>).
+
+          The values <literal>all</literal> or <literal>none</literal> have
+          special meaning. Overidden by any entry in the option
+          <option>services.taskserver.disallowedClientIDs</option>.
+        '';
+      };
+
+      disallowedClientIDs = mkOption {
+        type = with types; either str (listOf str);
+        default = [];
+        example = [ "[Tt]ask [2-9]+" ];
+        description = ''
+          A list of regular expressions that are matched against the reported
+          client id (such as <literal>task 2.3.0</literal>).
+
+          The values <literal>all</literal> or <literal>none</literal> have
+          special meaning. Any entry here overrides those in
+          <option>services.taskserver.allowedClientIDs</option>.
+        '';
+      };
+
+      listenHost = mkOption {
+        type = types.str;
+        default = "localhost";
+        example = "::";
+        description = ''
+          The address (IPv4, IPv6 or DNS) to listen on.
+
+          If the value is something else than <literal>localhost</literal> the
+          port defined by <option>listenPort</option> is automatically added to
+          <option>networking.firewall.allowedTCPPorts</option>.
+        '';
+      };
+
+      listenPort = mkOption {
+        type = types.int;
+        default = 53589;
+        description = ''
+          Port number of the Taskserver.
+        '';
+      };
+
+      fqdn = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = ''
+          The fully qualified domain name of this server, which is also used
+          as the common name in the certificates.
+        '';
+      };
+
+      trust = mkOption {
+        type = types.enum [ "allow all" "strict" ];
+        default = "strict";
+        description = ''
+          Determines how client certificates are validated.
+
+          The value <literal>allow all</literal> performs no client
+          certificate validation. This is not recommended. The value
+          <literal>strict</literal> causes the client certificate to be
+          validated against a CA.
+        '';
+      };
+
+      pki.manual = manualPkiOptions;
+      pki.auto = autoPkiOptions;
+
+      config = mkOption {
+        type = types.attrs;
+        example.client.cert = "/tmp/debugging.cert";
+        description = ''
+          Configuration options to pass to Taskserver.
+
+          The options here are the same as described in <citerefentry>
+            <refentrytitle>taskdrc</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry>, but with one difference:
+
+          The <literal>server</literal> option is
+          <literal>server.listen</literal> here, because the
+          <literal>server</literal> option would collide with other options
+          like <literal>server.cert</literal> and we would run in a type error
+          (attribute set versus string).
+
+          Nix types like integers or booleans are automatically converted to
+          the right values Taskserver would expect.
+        '';
+        apply = let
+          mkKey = path: if path == ["server" "listen"] then "server"
+                        else concatStringsSep "." path;
+          recurse = path: attrs: let
+            mapper = name: val: let
+              newPath = path ++ [ name ];
+              scalar = if val == true then "true"
+                       else if val == false then "false"
+                       else toString val;
+            in if isAttrs val then recurse newPath val
+               else [ "${mkKey newPath}=${scalar}" ];
+          in concatLists (mapAttrsToList mapper attrs);
+        in recurse [];
+      };
+    };
+  };
+
+  imports = [
+    (mkRemovedOptionModule ["services" "taskserver" "extraConfig"] ''
+      This option was removed in favor of `services.taskserver.config` with
+      different semantics (it's now a list of attributes instead of lines).
+
+      Please look up the documentation of `services.taskserver.config' to get
+      more information about the new way to pass additional configuration
+      options.
+    '')
+  ];
+
+  config = mkMerge [
+    (mkIf cfg.enable {
+      environment.systemPackages = [ nixos-taskserver ];
+
+      users.users = optionalAttrs (cfg.user == "taskd") {
+        taskd = {
+          uid = config.ids.uids.taskd;
+          description = "Taskserver user";
+          group = cfg.group;
+        };
+      };
+
+      users.groups = optionalAttrs (cfg.group == "taskd") {
+        taskd.gid = config.ids.gids.taskd;
+      };
+
+      services.taskserver.config = {
+        # systemd related
+        daemon = false;
+        log = "-";
+
+        # logging
+        debug = cfg.debug;
+        ip.log = cfg.ipLog;
+
+        # general
+        ciphers = cfg.ciphers;
+        confirmation = cfg.confirmation;
+        extensions = cfg.extensions;
+        queue.size = cfg.queueSize;
+        request.limit = cfg.requestLimit;
+
+        # client
+        client.allow = cfg.allowedClientIDs;
+        client.deny = cfg.disallowedClientIDs;
+
+        # server
+        trust = cfg.trust;
+        server = {
+          listen = "${cfg.listenHost}:${toString cfg.listenPort}";
+        } // (if needToCreateCA then {
+          cert = "${cfg.dataDir}/keys/server.cert";
+          key = "${cfg.dataDir}/keys/server.key";
+          crl = "${cfg.dataDir}/keys/server.crl";
+        } else {
+          cert = "${cfg.pki.manual.server.cert}";
+          key = "${cfg.pki.manual.server.key}";
+          ${mapNullable (_: "crl") cfg.pki.manual.server.crl} = "${cfg.pki.manual.server.crl}";
+        });
+
+        ca.cert = if needToCreateCA then "${cfg.dataDir}/keys/ca.cert"
+                  else "${cfg.pki.manual.ca.cert}";
+      };
+
+      systemd.services.taskserver-init = {
+        wantedBy = [ "taskserver.service" ];
+        before = [ "taskserver.service" ];
+        description = "Initialize Taskserver Data Directory";
+
+        preStart = ''
+          mkdir -m 0770 -p "${cfg.dataDir}"
+          chown "${cfg.user}:${cfg.group}" "${cfg.dataDir}"
+        '';
+
+        script = ''
+          ${taskd} init
+          touch "${cfg.dataDir}/.is_initialized"
+        '';
+
+        environment.TASKDDATA = cfg.dataDir;
+
+        unitConfig.ConditionPathExists = "!${cfg.dataDir}/.is_initialized";
+
+        serviceConfig.Type = "oneshot";
+        serviceConfig.User = cfg.user;
+        serviceConfig.Group = cfg.group;
+        serviceConfig.PermissionsStartOnly = true;
+        serviceConfig.PrivateNetwork = true;
+        serviceConfig.PrivateDevices = true;
+        serviceConfig.PrivateTmp = true;
+      };
+
+      systemd.services.taskserver = {
+        description = "Taskwarrior Server";
+
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+
+        environment.TASKDDATA = cfg.dataDir;
+
+        preStart = let
+          jsonOrgs = builtins.toJSON cfg.organisations;
+          jsonFile = pkgs.writeText "orgs.json" jsonOrgs;
+          helperTool = "${nixos-taskserver}/bin/nixos-taskserver";
+        in "${helperTool} process-json '${jsonFile}'";
+
+        serviceConfig = {
+          ExecStart = let
+            mkCfgFlag = flag: escapeShellArg "--${flag}";
+            cfgFlags = concatMapStringsSep " " mkCfgFlag cfg.config;
+          in "@${taskd} taskd server ${cfgFlags}";
+          ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID";
+          Restart = "on-failure";
+          PermissionsStartOnly = true;
+          PrivateTmp = true;
+          PrivateDevices = true;
+          User = cfg.user;
+          Group = cfg.group;
+        };
+      };
+    })
+    (mkIf (cfg.enable && needToCreateCA) {
+      systemd.services.taskserver-ca = {
+        wantedBy = [ "taskserver.service" ];
+        after = [ "taskserver-init.service" ];
+        before = [ "taskserver.service" ];
+        description = "Initialize CA for TaskServer";
+        serviceConfig.Type = "oneshot";
+        serviceConfig.UMask = "0077";
+        serviceConfig.PrivateNetwork = true;
+        serviceConfig.PrivateTmp = true;
+
+        script = ''
+          silent_certtool() {
+            if ! output="$("${certtool}" "$@" 2>&1)"; then
+              echo "GNUTLS certtool invocation failed with output:" >&2
+              echo "$output" >&2
+            fi
+          }
+
+          mkdir -m 0700 -p "${cfg.dataDir}/keys"
+          chown root:root "${cfg.dataDir}/keys"
+
+          if [ ! -e "${cfg.dataDir}/keys/ca.key" ]; then
+            silent_certtool -p \
+              --bits ${toString cfg.pki.auto.bits} \
+              --outfile "${cfg.dataDir}/keys/ca.key"
+            silent_certtool -s \
+              --template "${pkgs.writeText "taskserver-ca.template" ''
+                cn = ${cfg.fqdn}
+                expiration_days = ${toString cfg.pki.auto.expiration.ca}
+                cert_signing_key
+                ca
+              ''}" \
+              --load-privkey "${cfg.dataDir}/keys/ca.key" \
+              --outfile "${cfg.dataDir}/keys/ca.cert"
+
+            chgrp "${cfg.group}" "${cfg.dataDir}/keys/ca.cert"
+            chmod g+r "${cfg.dataDir}/keys/ca.cert"
+          fi
+
+          if [ ! -e "${cfg.dataDir}/keys/server.key" ]; then
+            silent_certtool -p \
+              --bits ${toString cfg.pki.auto.bits} \
+              --outfile "${cfg.dataDir}/keys/server.key"
+
+            silent_certtool -c \
+              --template "${pkgs.writeText "taskserver-cert.template" ''
+                cn = ${cfg.fqdn}
+                expiration_days = ${toString cfg.pki.auto.expiration.server}
+                tls_www_server
+                encryption_key
+                signing_key
+              ''}" \
+              --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \
+              --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \
+              --load-privkey "${cfg.dataDir}/keys/server.key" \
+              --outfile "${cfg.dataDir}/keys/server.cert"
+
+            chgrp "${cfg.group}" \
+              "${cfg.dataDir}/keys/server.key" \
+              "${cfg.dataDir}/keys/server.cert"
+
+            chmod g+r \
+              "${cfg.dataDir}/keys/server.key" \
+              "${cfg.dataDir}/keys/server.cert"
+          fi
+
+          if [ ! -e "${cfg.dataDir}/keys/server.crl" ]; then
+            silent_certtool --generate-crl \
+              --template "${pkgs.writeText "taskserver-crl.template" ''
+                expiration_days = ${toString cfg.pki.auto.expiration.crl}
+              ''}" \
+              --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \
+              --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \
+              --outfile "${cfg.dataDir}/keys/server.crl"
+
+            chgrp "${cfg.group}" "${cfg.dataDir}/keys/server.crl"
+            chmod g+r "${cfg.dataDir}/keys/server.crl"
+          fi
+
+          chmod go+x "${cfg.dataDir}/keys"
+        '';
+      };
+    })
+    (mkIf (cfg.enable && cfg.listenHost != "localhost") {
+      networking.firewall.allowedTCPPorts = [ cfg.listenPort ];
+    })
+  ];
+
+  meta.doc = ./doc.xml;
+}
diff --git a/nixos/modules/services/misc/taskserver/doc.xml b/nixos/modules/services/misc/taskserver/doc.xml
new file mode 100644
index 00000000000..f6ead7c3785
--- /dev/null
+++ b/nixos/modules/services/misc/taskserver/doc.xml
@@ -0,0 +1,135 @@
+<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/taskserver/helper-tool.py b/nixos/modules/services/misc/taskserver/helper-tool.py
new file mode 100644
index 00000000000..22a3d8d5311
--- /dev/null
+++ b/nixos/modules/services/misc/taskserver/helper-tool.py
@@ -0,0 +1,688 @@
+import grp
+import json
+import pwd
+import os
+import re
+import string
+import subprocess
+import sys
+
+from contextlib import contextmanager
+from shutil import rmtree
+from tempfile import NamedTemporaryFile
+
+import click
+
+IS_AUTO_CONFIG = @isAutoConfig@ # NOQA
+CERTTOOL_COMMAND = "@certtool@"
+CERT_BITS = "@certBits@"
+CLIENT_EXPIRATION = "@clientExpiration@"
+CRL_EXPIRATION = "@crlExpiration@"
+
+TASKD_COMMAND = "@taskd@"
+TASKD_DATA_DIR = "@dataDir@"
+TASKD_USER = "@user@"
+TASKD_GROUP = "@group@"
+FQDN = "@fqdn@"
+
+CA_KEY = os.path.join(TASKD_DATA_DIR, "keys", "ca.key")
+CA_CERT = os.path.join(TASKD_DATA_DIR, "keys", "ca.cert")
+CRL_FILE = os.path.join(TASKD_DATA_DIR, "keys", "server.crl")
+
+RE_CONFIGUSER = re.compile(r'^\s*user\s*=(.*)$')
+RE_USERKEY = re.compile(r'New user key: (.+)$', re.MULTILINE)
+
+
+def lazyprop(fun):
+    """
+    Decorator which only evaluates the specified function when accessed.
+    """
+    name = '_lazy_' + fun.__name__
+
+    @property
+    def _lazy(self):
+        val = getattr(self, name, None)
+        if val is None:
+            val = fun(self)
+            setattr(self, name, val)
+        return val
+
+    return _lazy
+
+
+class TaskdError(OSError):
+    pass
+
+
+def run_as_taskd_user():
+    uid = pwd.getpwnam(TASKD_USER).pw_uid
+    gid = grp.getgrnam(TASKD_GROUP).gr_gid
+    os.setgid(gid)
+    os.setuid(uid)
+
+
+def taskd_cmd(cmd, *args, **kwargs):
+    """
+    Invoke taskd with the specified command with the privileges of the 'taskd'
+    user and 'taskd' group.
+
+    If 'capture_stdout' is passed as a keyword argument with the value True,
+    the return value are the contents the command printed to stdout.
+    """
+    capture_stdout = kwargs.pop("capture_stdout", False)
+    fun = subprocess.check_output if capture_stdout else subprocess.check_call
+    return fun(
+        [TASKD_COMMAND, cmd, "--data", TASKD_DATA_DIR] + list(args),
+        preexec_fn=run_as_taskd_user,
+        **kwargs
+    )
+
+
+def certtool_cmd(*args, **kwargs):
+    """
+    Invoke certtool from GNUTLS and return the output of the command.
+
+    The provided arguments are added to the certtool command and keyword
+    arguments are added to subprocess.check_output().
+
+    Note that this will suppress all output of certtool and it will only be
+    printed whenever there is an unsuccessful return code.
+    """
+    return subprocess.check_output(
+        [CERTTOOL_COMMAND] + list(args),
+        preexec_fn=lambda: os.umask(0077),
+        stderr=subprocess.STDOUT,
+        **kwargs
+    )
+
+
+def label(msg):
+    if sys.stdout.isatty() or sys.stderr.isatty():
+        sys.stderr.write(msg + "\n")
+
+
+def mkpath(*args):
+    return os.path.join(TASKD_DATA_DIR, "orgs", *args)
+
+
+def mark_imperative(*path):
+    """
+    Mark the specified path as being imperatively managed by creating an empty
+    file called ".imperative", so that it doesn't interfere with the
+    declarative configuration.
+    """
+    open(os.path.join(mkpath(*path), ".imperative"), 'a').close()
+
+
+def is_imperative(*path):
+    """
+    Check whether the given path is marked as imperative, see mark_imperative()
+    for more information.
+    """
+    full_path = []
+    for component in path:
+        full_path.append(component)
+        if os.path.exists(os.path.join(mkpath(*full_path), ".imperative")):
+            return True
+    return False
+
+
+def fetch_username(org, key):
+    for line in open(mkpath(org, "users", key, "config"), "r"):
+        match = RE_CONFIGUSER.match(line)
+        if match is None:
+            continue
+        return match.group(1).strip()
+    return None
+
+
+@contextmanager
+def create_template(contents):
+    """
+    Generate a temporary file with the specified contents as a list of strings
+    and yield its path as the context.
+    """
+    template = NamedTemporaryFile(mode="w", prefix="certtool-template")
+    template.writelines(map(lambda l: l + "\n", contents))
+    template.flush()
+    yield template.name
+    template.close()
+
+
+def generate_key(org, user):
+    if not IS_AUTO_CONFIG:
+        msg = "Automatic PKI handling is disabled, you need to " \
+              "manually issue a client certificate for user {}.\n"
+        sys.stderr.write(msg.format(user))
+        return
+
+    basedir = os.path.join(TASKD_DATA_DIR, "keys", org, user)
+    if os.path.exists(basedir):
+        raise OSError("Keyfile directory for {} already exists.".format(user))
+
+    privkey = os.path.join(basedir, "private.key")
+    pubcert = os.path.join(basedir, "public.cert")
+
+    try:
+        os.makedirs(basedir, mode=0700)
+
+        certtool_cmd("-p", "--bits", CERT_BITS, "--outfile", privkey)
+
+        template_data = [
+            "organization = {0}".format(org),
+            "cn = {}".format(FQDN),
+            "expiration_days = {}".format(CLIENT_EXPIRATION),
+            "tls_www_client",
+            "encryption_key",
+            "signing_key"
+        ]
+
+        with create_template(template_data) as template:
+            certtool_cmd(
+                "-c",
+                "--load-privkey", privkey,
+                "--load-ca-privkey", CA_KEY,
+                "--load-ca-certificate", CA_CERT,
+                "--template", template,
+                "--outfile", pubcert
+            )
+    except:
+        rmtree(basedir)
+        raise
+
+
+def revoke_key(org, user):
+    basedir = os.path.join(TASKD_DATA_DIR, "keys", org, user)
+    if not os.path.exists(basedir):
+        raise OSError("Keyfile directory for {} doesn't exist.".format(user))
+
+    pubcert = os.path.join(basedir, "public.cert")
+
+    expiration = "expiration_days = {}".format(CRL_EXPIRATION)
+
+    with create_template([expiration]) as template:
+        oldcrl = NamedTemporaryFile(mode="wb", prefix="old-crl")
+        oldcrl.write(open(CRL_FILE, "rb").read())
+        oldcrl.flush()
+        certtool_cmd(
+            "--generate-crl",
+            "--load-crl", oldcrl.name,
+            "--load-ca-privkey", CA_KEY,
+            "--load-ca-certificate", CA_CERT,
+            "--load-certificate", pubcert,
+            "--template", template,
+            "--outfile", CRL_FILE
+        )
+        oldcrl.close()
+    rmtree(basedir)
+
+
+def is_key_line(line, match):
+    return line.startswith("---") and line.lstrip("- ").startswith(match)
+
+
+def getkey(*args):
+    path = os.path.join(TASKD_DATA_DIR, "keys", *args)
+    buf = []
+    for line in open(path, "r"):
+        if len(buf) == 0:
+            if is_key_line(line, "BEGIN"):
+                buf.append(line)
+            continue
+
+        buf.append(line)
+
+        if is_key_line(line, "END"):
+            return ''.join(buf)
+    raise IOError("Unable to get key from {}.".format(path))
+
+
+def mktaskkey(cfg, path, keydata):
+    heredoc = 'cat > "{}" <<EOF\n{}EOF'.format(path, keydata)
+    cmd = 'task config taskd.{} -- "{}"'.format(cfg, path)
+    return heredoc + "\n" + cmd
+
+
+class User(object):
+    def __init__(self, org, name, key):
+        self.__org = org
+        self.name = name
+        self.key = key
+
+    def export(self):
+        credentials = '/'.join([self.__org, self.name, self.key])
+        allow_unquoted = string.ascii_letters + string.digits + "/-_."
+        if not all((c in allow_unquoted) for c in credentials):
+            credentials = "'" + credentials.replace("'", r"'\''") + "'"
+
+        script = []
+
+        if IS_AUTO_CONFIG:
+            pubcert = getkey(self.__org, self.name, "public.cert")
+            privkey = getkey(self.__org, self.name, "private.key")
+            cacert = getkey("ca.cert")
+
+            keydir = "${TASKDATA:-$HOME/.task}/keys"
+
+            script += [
+                "umask 0077",
+                'mkdir -p "{}"'.format(keydir),
+                mktaskkey("certificate", os.path.join(keydir, "public.cert"),
+                          pubcert),
+                mktaskkey("key", os.path.join(keydir, "private.key"), privkey),
+                mktaskkey("ca", os.path.join(keydir, "ca.cert"), cacert)
+            ]
+
+        script.append(
+            "task config taskd.credentials -- {}".format(credentials)
+        )
+
+        return "\n".join(script) + "\n"
+
+
+class Group(object):
+    def __init__(self, org, name):
+        self.__org = org
+        self.name = name
+
+
+class Organisation(object):
+    def __init__(self, name, ignore_imperative):
+        self.name = name
+        self.ignore_imperative = ignore_imperative
+
+    def add_user(self, name):
+        """
+        Create a new user along with a certificate and key.
+
+        Returns a 'User' object or None if the user already exists.
+        """
+        if self.ignore_imperative and is_imperative(self.name):
+            return None
+        if name not in self.users.keys():
+            output = taskd_cmd("add", "user", self.name, name,
+                               capture_stdout=True)
+            key = RE_USERKEY.search(output)
+            if key is None:
+                msg = "Unable to find key while creating user {}."
+                raise TaskdError(msg.format(name))
+
+            generate_key(self.name, name)
+            newuser = User(self.name, name, key.group(1))
+            self._lazy_users[name] = newuser
+            return newuser
+        return None
+
+    def del_user(self, name):
+        """
+        Delete a user and revoke its keys.
+        """
+        if name in self.users.keys():
+            user = self.get_user(name)
+            if self.ignore_imperative and \
+               is_imperative(self.name, "users", user.key):
+                return
+
+            # Work around https://bug.tasktools.org/browse/TD-40:
+            rmtree(mkpath(self.name, "users", user.key))
+
+            revoke_key(self.name, name)
+            del self._lazy_users[name]
+
+    def add_group(self, name):
+        """
+        Create a new group.
+
+        Returns a 'Group' object or None if the group already exists.
+        """
+        if self.ignore_imperative and is_imperative(self.name):
+            return None
+        if name not in self.groups.keys():
+            taskd_cmd("add", "group", self.name, name)
+            newgroup = Group(self.name, name)
+            self._lazy_groups[name] = newgroup
+            return newgroup
+        return None
+
+    def del_group(self, name):
+        """
+        Delete a group.
+        """
+        if name in self.users.keys():
+            if self.ignore_imperative and \
+               is_imperative(self.name, "groups", name):
+                return
+            taskd_cmd("remove", "group", self.name, name)
+            del self._lazy_groups[name]
+
+    def get_user(self, name):
+        return self.users.get(name)
+
+    @lazyprop
+    def users(self):
+        result = {}
+        for key in os.listdir(mkpath(self.name, "users")):
+            user = fetch_username(self.name, key)
+            if user is not None:
+                result[user] = User(self.name, user, key)
+        return result
+
+    def get_group(self, name):
+        return self.groups.get(name)
+
+    @lazyprop
+    def groups(self):
+        result = {}
+        for group in os.listdir(mkpath(self.name, "groups")):
+            result[group] = Group(self.name, group)
+        return result
+
+
+class Manager(object):
+    def __init__(self, ignore_imperative=False):
+        """
+        Instantiates an organisations manager.
+
+        If ignore_imperative is True, all actions that modify data are checked
+        whether they're created imperatively and if so, they will result in no
+        operation.
+        """
+        self.ignore_imperative = ignore_imperative
+
+    def add_org(self, name):
+        """
+        Create a new organisation.
+
+        Returns an 'Organisation' object or None if the organisation already
+        exists.
+        """
+        if name not in self.orgs.keys():
+            taskd_cmd("add", "org", name)
+            neworg = Organisation(name, self.ignore_imperative)
+            self._lazy_orgs[name] = neworg
+            return neworg
+        return None
+
+    def del_org(self, name):
+        """
+        Delete and revoke keys of an organisation with all its users and
+        groups.
+        """
+        org = self.get_org(name)
+        if org is not None:
+            if self.ignore_imperative and is_imperative(name):
+                return
+            for user in org.users.keys():
+                org.del_user(user)
+            for group in org.groups.keys():
+                org.del_group(group)
+            taskd_cmd("remove", "org", name)
+            del self._lazy_orgs[name]
+
+    def get_org(self, name):
+        return self.orgs.get(name)
+
+    @lazyprop
+    def orgs(self):
+        result = {}
+        for org in os.listdir(mkpath()):
+            result[org] = Organisation(org, self.ignore_imperative)
+        return result
+
+
+class OrganisationType(click.ParamType):
+    name = 'organisation'
+
+    def convert(self, value, param, ctx):
+        org = Manager().get_org(value)
+        if org is None:
+            self.fail("Organisation {} does not exist.".format(value))
+        return org
+
+ORGANISATION = OrganisationType()
+
+
+@click.group()
+@click.pass_context
+def cli(ctx):
+    """
+    Manage Taskserver users and certificates
+    """
+    if not IS_AUTO_CONFIG:
+        return
+    for path in (CA_KEY, CA_CERT, CRL_FILE):
+        if not os.path.exists(path):
+            msg = "CA setup not done or incomplete, missing file {}."
+            ctx.fail(msg.format(path))
+
+
+@cli.group("org")
+def org_cli():
+    """
+    Manage organisations
+    """
+    pass
+
+
+@cli.group("user")
+def user_cli():
+    """
+    Manage users
+    """
+    pass
+
+
+@cli.group("group")
+def group_cli():
+    """
+    Manage groups
+    """
+    pass
+
+
+@user_cli.command("list")
+@click.argument("organisation", type=ORGANISATION)
+def list_users(organisation):
+    """
+    List all users belonging to the specified organisation.
+    """
+    label("The following users exists for {}:".format(organisation.name))
+    for user in organisation.users.values():
+        sys.stdout.write(user.name + "\n")
+
+
+@group_cli.command("list")
+@click.argument("organisation", type=ORGANISATION)
+def list_groups(organisation):
+    """
+    List all users belonging to the specified organisation.
+    """
+    label("The following users exists for {}:".format(organisation.name))
+    for group in organisation.groups.values():
+        sys.stdout.write(group.name + "\n")
+
+
+@org_cli.command("list")
+def list_orgs():
+    """
+    List available organisations
+    """
+    label("The following organisations exist:")
+    for org in Manager().orgs:
+        sys.stdout.write(org.name + "\n")
+
+
+@user_cli.command("getkey")
+@click.argument("organisation", type=ORGANISATION)
+@click.argument("user")
+def get_uuid(organisation, user):
+    """
+    Get the UUID of the specified user belonging to the specified organisation.
+    """
+    userobj = organisation.get_user(user)
+    if userobj is None:
+        msg = "User {} doesn't exist in organisation {}."
+        sys.exit(msg.format(userobj.name, organisation.name))
+
+    label("User {} has the following UUID:".format(userobj.name))
+    sys.stdout.write(user.key + "\n")
+
+
+@user_cli.command("export")
+@click.argument("organisation", type=ORGANISATION)
+@click.argument("user")
+def export_user(organisation, user):
+    """
+    Export user of the specified organisation as a series of shell commands
+    that can be used on the client side to easily import the certificates.
+
+    Note that the private key will be exported as well, so use this with care!
+    """
+    userobj = organisation.get_user(user)
+    if userobj is None:
+        msg = "User {} doesn't exist in organisation {}."
+        sys.exit(msg.format(user, organisation.name))
+
+    sys.stdout.write(userobj.export())
+
+
+@org_cli.command("add")
+@click.argument("name")
+def add_org(name):
+    """
+    Create an organisation with the specified name.
+    """
+    if os.path.exists(mkpath(name)):
+        msg = "Organisation with name {} already exists."
+        sys.exit(msg.format(name))
+
+    taskd_cmd("add", "org", name)
+    mark_imperative(name)
+
+
+@org_cli.command("remove")
+@click.argument("name")
+def del_org(name):
+    """
+    Delete the organisation with the specified name.
+
+    All of the users and groups will be deleted as well and client certificates
+    will be revoked.
+    """
+    Manager().del_org(name)
+    msg = ("Organisation {} deleted. Be sure to restart the Taskserver"
+           " using 'systemctl restart taskserver.service' in order for"
+           " the certificate revocation to apply.")
+    click.echo(msg.format(name), err=True)
+
+
+@user_cli.command("add")
+@click.argument("organisation", type=ORGANISATION)
+@click.argument("user")
+def add_user(organisation, user):
+    """
+    Create a user for the given organisation along with a client certificate
+    and print the key of the new user.
+
+    The client certificate along with it's public key can be shown via the
+    'user export' subcommand.
+    """
+    userobj = organisation.add_user(user)
+    if userobj is None:
+        msg = "User {} already exists in organisation {}."
+        sys.exit(msg.format(user, organisation))
+    else:
+        mark_imperative(organisation.name, "users", userobj.key)
+
+
+@user_cli.command("remove")
+@click.argument("organisation", type=ORGANISATION)
+@click.argument("user")
+def del_user(organisation, user):
+    """
+    Delete a user from the given organisation.
+
+    This will also revoke the client certificate of the given user.
+    """
+    organisation.del_user(user)
+    msg = ("User {} deleted. Be sure to restart the Taskserver using"
+           " 'systemctl restart taskserver.service' in order for the"
+           " certificate revocation to apply.")
+    click.echo(msg.format(user), err=True)
+
+
+@group_cli.command("add")
+@click.argument("organisation", type=ORGANISATION)
+@click.argument("group")
+def add_group(organisation, group):
+    """
+    Create a group for the given organisation.
+    """
+    groupobj = organisation.add_group(group)
+    if groupobj is None:
+        msg = "Group {} already exists in organisation {}."
+        sys.exit(msg.format(group, organisation))
+    else:
+        mark_imperative(organisation.name, "groups", groupobj.name)
+
+
+@group_cli.command("remove")
+@click.argument("organisation", type=ORGANISATION)
+@click.argument("group")
+def del_group(organisation, group):
+    """
+    Delete a group from the given organisation.
+    """
+    organisation.del_group(group)
+    click("Group {} deleted.".format(group), err=True)
+
+
+def add_or_delete(old, new, add_fun, del_fun):
+    """
+    Given an 'old' and 'new' list, figure out the intersections and invoke
+    'add_fun' against every element that is not in the 'old' list and 'del_fun'
+    against every element that is not in the 'new' list.
+
+    Returns a tuple where the first element is the list of elements that were
+    added and the second element consisting of elements that were deleted.
+    """
+    old_set = set(old)
+    new_set = set(new)
+    to_delete = old_set - new_set
+    to_add = new_set - old_set
+    for elem in to_delete:
+        del_fun(elem)
+    for elem in to_add:
+        add_fun(elem)
+    return to_add, to_delete
+
+
+@cli.command("process-json")
+@click.argument('json-file', type=click.File('rb'))
+def process_json(json_file):
+    """
+    Create and delete users, groups and organisations based on a JSON file.
+
+    The structure of this file is exactly the same as the
+    'services.taskserver.organisations' option of the NixOS module and is used
+    for declaratively adding and deleting users.
+
+    Hence this subcommand is not recommended outside of the scope of the NixOS
+    module.
+    """
+    data = json.load(json_file)
+
+    mgr = Manager(ignore_imperative=True)
+    add_or_delete(mgr.orgs.keys(), data.keys(), mgr.add_org, mgr.del_org)
+
+    for org in mgr.orgs.values():
+        if is_imperative(org.name):
+            continue
+        add_or_delete(org.users.keys(), data[org.name]['users'],
+                      org.add_user, org.del_user)
+        add_or_delete(org.groups.keys(), data[org.name]['groups'],
+                      org.add_group, org.del_group)
+
+
+if __name__ == '__main__':
+    cli()
diff --git a/nixos/modules/services/misc/tautulli.nix b/nixos/modules/services/misc/tautulli.nix
new file mode 100644
index 00000000000..9a972b29122
--- /dev/null
+++ b/nixos/modules/services/misc/tautulli.nix
@@ -0,0 +1,81 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.tautulli;
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "plexpy" ] [ "services" "tautulli" ])
+  ];
+
+  options = {
+    services.tautulli = {
+      enable = mkEnableOption "Tautulli Plex Monitor";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/plexpy";
+        description = "The directory where Tautulli stores its data files.";
+      };
+
+      configFile = mkOption {
+        type = types.str;
+        default = "/var/lib/plexpy/config.ini";
+        description = "The location of Tautulli's config file.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8181;
+        description = "TCP port where Tautulli listens.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "plexpy";
+        description = "User account under which Tautulli runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "nogroup";
+        description = "Group under which Tautulli runs.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.tautulli;
+        defaultText = literalExpression "pkgs.tautulli";
+        description = ''
+          The Tautulli package to use.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.tautulli = {
+      description = "Tautulli Plex Monitor";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        GuessMainPID = "false";
+        ExecStart = "${cfg.package}/bin/tautulli --datadir ${cfg.dataDir} --config ${cfg.configFile} --port ${toString cfg.port} --pidfile ${cfg.dataDir}/tautulli.pid --nolaunch";
+        Restart = "on-failure";
+      };
+    };
+
+    users.users = mkIf (cfg.user == "plexpy") {
+      plexpy = { group = cfg.group; uid = config.ids.uids.plexpy; };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/tiddlywiki.nix b/nixos/modules/services/misc/tiddlywiki.nix
new file mode 100644
index 00000000000..2adc08f6cfe
--- /dev/null
+++ b/nixos/modules/services/misc/tiddlywiki.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.tiddlywiki;
+  listenParams = concatStrings (mapAttrsToList (n: v: " '${n}=${toString v}' ") cfg.listenOptions);
+  exe = "${pkgs.nodePackages.tiddlywiki}/lib/node_modules/.bin/tiddlywiki";
+  name = "tiddlywiki";
+  dataDir = "/var/lib/" + name;
+
+in {
+
+  options.services.tiddlywiki = {
+
+    enable = mkEnableOption "TiddlyWiki nodejs server";
+
+    listenOptions = mkOption {
+      type = types.attrs;
+      default = {};
+      example = {
+        credentials = "../credentials.csv";
+        readers="(authenticated)";
+        port = 3456;
+      };
+      description = ''
+        Parameters passed to <literal>--listen</literal> command.
+        Refer to <link xlink:href="https://tiddlywiki.com/#WebServer"/>
+        for details on supported values.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd = {
+      services.tiddlywiki = {
+        description = "TiddlyWiki nodejs server";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          Type = "simple";
+          Restart = "on-failure";
+          DynamicUser = true;
+          StateDirectory = name;
+          ExecStartPre = "-${exe} ${dataDir} --init server";
+          ExecStart = "${exe} ${dataDir} --listen ${listenParams}";
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/tp-auto-kbbl.nix b/nixos/modules/services/misc/tp-auto-kbbl.nix
new file mode 100644
index 00000000000..59018f7f81f
--- /dev/null
+++ b/nixos/modules/services/misc/tp-auto-kbbl.nix
@@ -0,0 +1,58 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.tp-auto-kbbl;
+
+in {
+  meta.maintainers = with maintainers; [ sebtm ];
+
+  options = {
+    services.tp-auto-kbbl = {
+      enable = mkEnableOption "Auto toggle keyboard back-lighting on Thinkpads (and maybe other laptops) for Linux";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.tp-auto-kbbl;
+        defaultText = literalExpression "pkgs.tp-auto-kbbl";
+        description = "Package providing <command>tp-auto-kbbl</command>.";
+      };
+
+      arguments = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = ''
+          List of arguments appended to <literal>./tp-auto-kbbl --device [device] [arguments]</literal>
+        '';
+      };
+
+      device = mkOption {
+        type = types.str;
+        default = "/dev/input/event0";
+        description = "Device watched for activities.";
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.tp-auto-kbbl = {
+      serviceConfig = {
+        ExecStart = concatStringsSep " "
+          ([ "${cfg.package}/bin/tp-auto-kbbl" "--device ${cfg.device}" ] ++ cfg.arguments);
+        Restart = "always";
+        Type = "simple";
+      };
+
+      unitConfig = {
+        Description = "Auto toggle keyboard backlight";
+        Documentation = "https://github.com/saibotd/tp-auto-kbbl";
+        After = [ "dbus.service" ];
+      };
+
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/tzupdate.nix b/nixos/modules/services/misc/tzupdate.nix
new file mode 100644
index 00000000000..eac1e1112a5
--- /dev/null
+++ b/nixos/modules/services/misc/tzupdate.nix
@@ -0,0 +1,45 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.tzupdate;
+in {
+  options.services.tzupdate = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable the tzupdate timezone updating service. This provides
+        a one-shot service which can be activated with systemctl to
+        update the timezone.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # We need to have imperative time zone management for this to work.
+    # This will give users an error if they have set an explicit time
+    # zone, which is better than silently overriding it.
+    time.timeZone = null;
+
+    # We provide a one-shot service which can be manually run. We could
+    # provide a service that runs on startup, but it's tricky to get
+    # a service to run after you have *internet* access.
+    systemd.services.tzupdate = {
+      description = "tzupdate timezone update service";
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+
+      serviceConfig = {
+        Type = "oneshot";
+        # We could link directly into pkgs.tzdata, but at least timedatectl seems
+        # to expect the symlink to point directly to a file in etc.
+        # Setting the "debian timezone file" to point at /dev/null stops it doing anything.
+        ExecStart = "${pkgs.tzupdate}/bin/tzupdate -z /etc/zoneinfo -d /dev/null";
+      };
+    };
+  };
+
+  meta.maintainers = [ maintainers.michaelpj ];
+}
diff --git a/nixos/modules/services/misc/uhub.nix b/nixos/modules/services/misc/uhub.nix
new file mode 100644
index 00000000000..0d0a8c2a4cb
--- /dev/null
+++ b/nixos/modules/services/misc/uhub.nix
@@ -0,0 +1,112 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  settingsFormat = {
+    type = with lib.types; attrsOf (oneOf [ bool int str ]);
+    generate = name: attrs:
+      pkgs.writeText name (lib.strings.concatStringsSep "\n"
+        (lib.attrsets.mapAttrsToList
+          (key: value: "${key}=${builtins.toJSON value}") attrs));
+  };
+in {
+  options = {
+
+    services.uhub = mkOption {
+      default = { };
+      description = "Uhub ADC hub instances";
+      type = types.attrsOf (types.submodule {
+        options = {
+
+          enable = mkEnableOption "hub instance" // { default = true; };
+
+          enableTLS = mkOption {
+            type = types.bool;
+            default = false;
+            description = "Whether to enable TLS support.";
+          };
+
+          settings = mkOption {
+            inherit (settingsFormat) type;
+            description = ''
+              Configuration of uhub.
+              See https://www.uhub.org/doc/config.php for a list of options.
+            '';
+            default = { };
+            example = {
+              server_bind_addr = "any";
+              server_port = 1511;
+              hub_name = "My Public Hub";
+              hub_description = "Yet another ADC hub";
+              max_users = 150;
+            };
+          };
+
+          plugins = mkOption {
+            description = "Uhub plugin configuration.";
+            type = with types;
+              listOf (submodule {
+                options = {
+                  plugin = mkOption {
+                    type = path;
+                    example = literalExpression
+                      "$${pkgs.uhub}/plugins/mod_auth_sqlite.so";
+                    description = "Path to plugin file.";
+                  };
+                  settings = mkOption {
+                    description = "Settings specific to this plugin.";
+                    type = with types; attrsOf str;
+                    example = { file = "/etc/uhub/users.db"; };
+                  };
+                };
+              });
+            default = [ ];
+          };
+
+        };
+      });
+    };
+
+  };
+
+  config = let
+    hubs = lib.attrsets.filterAttrs (_: cfg: cfg.enable) config.services.uhub;
+  in {
+
+    environment.etc = lib.attrsets.mapAttrs' (name: cfg:
+      let
+        settings' = cfg.settings // {
+          tls_enable = cfg.enableTLS;
+          file_plugins = pkgs.writeText "uhub-plugins.conf"
+            (lib.strings.concatStringsSep "\n" (map ({ plugin, settings }:
+              "plugin ${plugin} ${
+                toString
+                (lib.attrsets.mapAttrsToList (key: value: ''"${key}=${value}"'')
+                  settings)
+              }") cfg.plugins));
+        };
+      in {
+        name = "uhub/${name}.conf";
+        value.source = settingsFormat.generate "uhub-${name}.conf" settings';
+      }) hubs;
+
+    systemd.services = lib.attrsets.mapAttrs' (name: cfg: {
+      name = "uhub-${name}";
+      value = let pkg = pkgs.uhub.override { tlsSupport = cfg.enableTLS; };
+      in {
+        description = "high performance peer-to-peer hub for the ADC network";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        reloadIfChanged = true;
+        serviceConfig = {
+          Type = "notify";
+          ExecStart = "${pkg}/bin/uhub -c /etc/uhub/${name}.conf -L";
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          DynamicUser = true;
+        };
+      };
+    }) hubs;
+  };
+
+}
diff --git a/nixos/modules/services/misc/weechat.nix b/nixos/modules/services/misc/weechat.nix
new file mode 100644
index 00000000000..7a4c4dca2ac
--- /dev/null
+++ b/nixos/modules/services/misc/weechat.nix
@@ -0,0 +1,63 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.weechat;
+in
+
+{
+  options.services.weechat = {
+    enable = mkEnableOption "weechat";
+    root = mkOption {
+      description = "Weechat state directory.";
+      type = types.str;
+      default = "/var/lib/weechat";
+    };
+    sessionName = mkOption {
+      description = "Name of the `screen' session for weechat.";
+      default = "weechat-screen";
+      type = types.str;
+    };
+    binary = mkOption {
+      type = types.path;
+      description = "Binary to execute.";
+      default = "${pkgs.weechat}/bin/weechat";
+      defaultText = literalExpression ''"''${pkgs.weechat}/bin/weechat"'';
+      example = literalExpression ''"''${pkgs.weechat}/bin/weechat-headless"'';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users = {
+      groups.weechat = {};
+      users.weechat = {
+        createHome = true;
+        group = "weechat";
+        home = cfg.root;
+        isSystemUser = true;
+      };
+    };
+
+    systemd.services.weechat = {
+      environment.WEECHAT_HOME = cfg.root;
+      serviceConfig = {
+        User = "weechat";
+        Group = "weechat";
+        RemainAfterExit = "yes";
+      };
+      script = "exec ${config.security.wrapperDir}/screen -Dm -S ${cfg.sessionName} ${cfg.binary}";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network.target" ];
+    };
+
+    security.wrappers.screen =
+      { setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${pkgs.screen}/bin/screen";
+      };
+  };
+
+  meta.doc = ./weechat.xml;
+}
diff --git a/nixos/modules/services/misc/weechat.xml b/nixos/modules/services/misc/weechat.xml
new file mode 100644
index 00000000000..7255edfb9da
--- /dev/null
+++ b/nixos/modules/services/misc/weechat.xml
@@ -0,0 +1,66 @@
+<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/xmr-stak.nix b/nixos/modules/services/misc/xmr-stak.nix
new file mode 100644
index 00000000000..9256e9ae01c
--- /dev/null
+++ b/nixos/modules/services/misc/xmr-stak.nix
@@ -0,0 +1,93 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xmr-stak;
+
+  pkg = pkgs.xmr-stak.override {
+    inherit (cfg) openclSupport cudaSupport;
+  };
+
+in
+
+{
+  options = {
+    services.xmr-stak = {
+      enable = mkEnableOption "xmr-stak miner";
+      openclSupport = mkEnableOption "support for OpenCL (AMD/ATI graphics cards)";
+      cudaSupport = mkEnableOption "support for CUDA (NVidia graphics cards)";
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "--noCPU" "--currency monero" ];
+        description = "List of parameters to pass to xmr-stak.";
+      };
+
+      configFiles = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        example = literalExpression ''
+          {
+            "config.txt" = '''
+              "verbose_level" : 4,
+              "h_print_time" : 60,
+              "tls_secure_algo" : true,
+            ''';
+            "pools.txt" = '''
+              "currency" : "monero7",
+              "pool_list" :
+              [ { "pool_address" : "pool.supportxmr.com:443",
+                  "wallet_address" : "my-wallet-address",
+                  "rig_id" : "",
+                  "pool_password" : "nixos",
+                  "use_nicehash" : false,
+                  "use_tls" : true,
+                  "tls_fingerprint" : "",
+                  "pool_weight" : 23
+                },
+              ],
+            ''';
+          }
+        '';
+        description = ''
+          Content of config files like config.txt, pools.txt or cpu.txt.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.xmr-stak = {
+      wantedBy = [ "multi-user.target" ];
+      bindsTo = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+      environment = mkIf cfg.cudaSupport {
+        LD_LIBRARY_PATH = "${pkgs.linuxPackages_latest.nvidia_x11}/lib";
+      };
+
+      preStart = concatStrings (flip mapAttrsToList cfg.configFiles (fn: content: ''
+        ln -sf '${pkgs.writeText "xmr-stak-${fn}" content}' '${fn}'
+      ''));
+
+      serviceConfig = let rootRequired = cfg.openclSupport || cfg.cudaSupport; in {
+        ExecStart = "${pkg}/bin/xmr-stak ${concatStringsSep " " cfg.extraArgs}";
+        # xmr-stak generates cpu and/or gpu configuration files
+        WorkingDirectory = "/tmp";
+        PrivateTmp = true;
+        DynamicUser = !rootRequired;
+        LimitMEMLOCK = toString (1024*1024);
+      };
+    };
+  };
+
+  imports = [
+    (mkRemovedOptionModule ["services" "xmr-stak" "configText"] ''
+      This option was removed in favour of `services.xmr-stak.configFiles`
+      because the new config file `pools.txt` was introduced. You are
+      now able to define all other config files like cpu.txt or amd.txt.
+    '')
+  ];
+}
diff --git a/nixos/modules/services/misc/xmrig.nix b/nixos/modules/services/misc/xmrig.nix
new file mode 100644
index 00000000000..c5c3803920c
--- /dev/null
+++ b/nixos/modules/services/misc/xmrig.nix
@@ -0,0 +1,76 @@
+{ config, pkgs, lib, ... }:
+
+
+let
+  cfg = config.services.xmrig;
+
+  json = pkgs.formats.json { };
+  configFile = json.generate "config.json" cfg.settings;
+in
+
+with lib;
+
+{
+  options = {
+    services.xmrig = {
+      enable = mkEnableOption "XMRig Mining Software";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.xmrig;
+        defaultText = literalExpression "pkgs.xmrig";
+        example = literalExpression "pkgs.xmrig-mo";
+        description = "XMRig package to use.";
+      };
+
+      settings = mkOption {
+        default = { };
+        type = json.type;
+        example = literalExpression ''
+          {
+            autosave = true;
+            cpu = true;
+            opencl = false;
+            cuda = false;
+            pools = [
+              {
+                url = "pool.supportxmr.com:443";
+                user = "your-wallet";
+                keepalive = true;
+                tls = true;
+              }
+            ]
+          }
+        '';
+        description = ''
+          XMRig configuration. Refer to
+          <link xlink:href="https://xmrig.com/docs/miner/config"/>
+          for details on supported values.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    boot.kernelModules = [ "msr" ];
+
+    systemd.services.xmrig = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      description = "XMRig Mining Software Service";
+      serviceConfig = {
+        ExecStartPre = "${cfg.package}/bin/xmrig --config=${configFile} --dry-run";
+        ExecStart = "${cfg.package}/bin/xmrig --config=${configFile}";
+        # https://xmrig.com/docs/miner/randomx-optimization-guide/msr
+        # If you use recent XMRig with root privileges (Linux) or admin
+        # privileges (Windows) the miner configure all MSR registers
+        # automatically.
+        DynamicUser = lib.mkDefault false;
+      };
+    };
+  };
+
+  meta = with lib; {
+    maintainers = with maintainers; [ ratsclub ];
+  };
+}
diff --git a/nixos/modules/services/misc/zoneminder.nix b/nixos/modules/services/misc/zoneminder.nix
new file mode 100644
index 00000000000..a557e742b7c
--- /dev/null
+++ b/nixos/modules/services/misc/zoneminder.nix
@@ -0,0 +1,370 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.zoneminder;
+  fpm = config.services.phpfpm.pools.zoneminder;
+  pkg = pkgs.zoneminder;
+
+  dirName = pkg.dirName;
+
+  user = "zoneminder";
+  group = {
+    nginx = config.services.nginx.group;
+    none  = user;
+  }.${cfg.webserver};
+
+  useNginx = cfg.webserver == "nginx";
+
+  defaultDir = "/var/lib/${user}";
+  home = if useCustomDir then cfg.storageDir else defaultDir;
+
+  useCustomDir = cfg.storageDir != null;
+
+  zms = "/cgi-bin/zms";
+
+  dirs = dirList: [ dirName ] ++ map (e: "${dirName}/${e}") dirList;
+
+  cacheDirs = [ "swap" ];
+  libDirs   = [ "events" "exports" "images" "sounds" ];
+
+  dirStanzas = baseDir:
+    lib.concatStringsSep "\n" (map (e:
+      "ZM_DIR_${lib.toUpper e}=${baseDir}/${e}"
+      ) libDirs);
+
+  defaultsFile = pkgs.writeText "60-defaults.conf" ''
+    # 01-system-paths.conf
+    ${dirStanzas home}
+    ZM_PATH_ARP=${lib.getBin pkgs.nettools}/bin/arp
+    ZM_PATH_LOGS=/var/log/${dirName}
+    ZM_PATH_MAP=/dev/shm
+    ZM_PATH_SOCKS=/run/${dirName}
+    ZM_PATH_SWAP=/var/cache/${dirName}/swap
+    ZM_PATH_ZMS=${zms}
+
+    # 02-multiserver.conf
+    ZM_SERVER_HOST=
+
+    # Database
+    ZM_DB_TYPE=mysql
+    ZM_DB_HOST=${cfg.database.host}
+    ZM_DB_NAME=${cfg.database.name}
+    ZM_DB_USER=${cfg.database.username}
+    ZM_DB_PASS=${cfg.database.password}
+
+    # Web
+    ZM_WEB_USER=${user}
+    ZM_WEB_GROUP=${group}
+  '';
+
+  configFile = pkgs.writeText "80-nixos.conf" ''
+    # You can override defaults here
+
+    ${cfg.extraConfig}
+  '';
+
+in {
+  options = {
+    services.zoneminder = with lib; {
+      enable = lib.mkEnableOption ''
+        ZoneMinder
+        </para><para>
+        If you intend to run the database locally, you should set
+        `config.services.zoneminder.database.createLocally` to true. Otherwise,
+        when set to `false` (the default), you will have to create the database
+        and database user as well as populate the database yourself.
+        Additionally, you will need to run `zmupdate.pl` yourself when
+        upgrading to a newer version.
+      '';
+
+      webserver = mkOption {
+        type = types.enum [ "nginx" "none" ];
+        default = "nginx";
+        description = ''
+          The webserver to configure for the PHP frontend.
+          </para>
+          <para>
+
+          Set it to `none` if you want to configure it yourself. PRs are welcome
+          for support for other web servers.
+        '';
+      };
+
+      hostname = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = ''
+          The hostname on which to listen.
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8095;
+        description = ''
+          The port on which to listen.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open the firewall port(s).
+        '';
+      };
+
+      database = {
+        createLocally = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Create the database and database user locally.
+          '';
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = ''
+            Hostname hosting the database.
+          '';
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "zm";
+          description = ''
+            Name of database.
+          '';
+        };
+
+        username = mkOption {
+          type = types.str;
+          default = "zmuser";
+          description = ''
+            Username for accessing the database.
+          '';
+        };
+
+        password = mkOption {
+          type = types.str;
+          default = "zmpass";
+          description = ''
+            Username for accessing the database.
+            Not used if <literal>createLocally</literal> is set.
+          '';
+        };
+      };
+
+      cameras = mkOption {
+        type = types.int;
+        default = 1;
+        description = ''
+          Set this to the number of cameras you expect to support.
+        '';
+      };
+
+      storageDir = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/storage/tank";
+        description = ''
+          ZoneMinder can generate quite a lot of data, so in case you don't want
+          to use the default ${defaultDir}, you can override the path here.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Additional configuration added verbatim to the configuration file.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    assertions = [
+      { assertion = cfg.database.createLocally -> cfg.database.username == user;
+        message = "services.zoneminder.database.username must be set to ${user} if services.zoneminder.database.createLocally is set true";
+      }
+    ];
+
+    environment.etc = {
+      "zoneminder/60-defaults.conf".source = defaultsFile;
+      "zoneminder/80-nixos.conf".source    = configFile;
+    };
+
+    networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [
+      cfg.port
+      6802 # zmtrigger
+    ];
+
+    services = {
+      fcgiwrap = lib.mkIf useNginx {
+        enable = true;
+        preforkProcesses = cfg.cameras;
+        inherit user group;
+      };
+
+      mysql = lib.mkIf cfg.database.createLocally {
+        enable = true;
+        package = lib.mkDefault pkgs.mariadb;
+        ensureDatabases = [ cfg.database.name ];
+        ensureUsers = [{
+          name = cfg.database.username;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }];
+      };
+
+      nginx = lib.mkIf useNginx {
+        enable = true;
+        virtualHosts = {
+          ${cfg.hostname} = {
+            default = true;
+            root = "${pkg}/share/zoneminder/www";
+            listen = [ { addr = "0.0.0.0"; inherit (cfg) port; } ];
+            extraConfig = let
+              fcgi = config.services.fcgiwrap;
+            in ''
+              index index.php;
+
+              location / {
+                try_files $uri $uri/ /index.php?$args =404;
+
+                rewrite ^/skins/.*/css/fonts/(.*)$ /fonts/$1 permanent;
+
+                location ~ /api/(css|img|ico) {
+                  rewrite ^/api(.+)$ /api/app/webroot/$1 break;
+                  try_files $uri $uri/ =404;
+                }
+
+                location ~ \.(gif|ico|jpg|jpeg|png)$ {
+                  access_log off;
+                  expires 30d;
+                }
+
+                location /api {
+                  rewrite ^/api(.+)$ /api/app/webroot/index.php?p=$1 last;
+                }
+
+                location /cgi-bin {
+                  gzip off;
+
+                  include ${config.services.nginx.package}/conf/fastcgi_params;
+                  fastcgi_param SCRIPT_FILENAME ${pkg}/libexec/zoneminder/${zms};
+                  fastcgi_param HTTP_PROXY "";
+                  fastcgi_intercept_errors on;
+
+                  fastcgi_pass ${fcgi.socketType}:${fcgi.socketAddress};
+                }
+
+                location /cache/ {
+                  alias /var/cache/${dirName}/;
+                }
+
+                location ~ \.php$ {
+                  try_files $uri =404;
+                  fastcgi_index index.php;
+
+                  include ${config.services.nginx.package}/conf/fastcgi_params;
+                  fastcgi_param SCRIPT_FILENAME $request_filename;
+                  fastcgi_param HTTP_PROXY "";
+
+                  fastcgi_pass unix:${fpm.socket};
+                }
+              }
+            '';
+          };
+        };
+      };
+
+      phpfpm = lib.mkIf useNginx {
+        pools.zoneminder = {
+          inherit user group;
+          phpPackage = pkgs.php.withExtensions ({ enabled, all }: enabled ++ [ all.apcu ]);
+          phpOptions = ''
+            date.timezone = "${config.time.timeZone}"
+          '';
+          settings = lib.mapAttrs (name: lib.mkDefault) {
+            "listen.owner" = user;
+            "listen.group" = group;
+            "listen.mode" = "0660";
+
+            "pm" = "dynamic";
+            "pm.start_servers" = 1;
+            "pm.min_spare_servers" = 1;
+            "pm.max_spare_servers" = 2;
+            "pm.max_requests" = 500;
+            "pm.max_children" = 5;
+            "pm.status_path" = "/$pool-status";
+            "ping.path" = "/$pool-ping";
+          };
+        };
+      };
+    };
+
+    systemd.services = {
+      zoneminder = with pkgs; {
+        inherit (zoneminder.meta) description;
+        documentation = [ "https://zoneminder.readthedocs.org/en/latest/" ];
+        path = [
+          coreutils
+          procps
+          psmisc
+        ];
+        after = [ "nginx.service" ] ++ lib.optional cfg.database.createLocally "mysql.service";
+        wantedBy = [ "multi-user.target" ];
+        restartTriggers = [ defaultsFile configFile ];
+        preStart = lib.optionalString useCustomDir ''
+          install -dm775 -o ${user} -g ${group} ${cfg.storageDir}/{${lib.concatStringsSep "," libDirs}}
+        '' + lib.optionalString cfg.database.createLocally ''
+          if ! test -e "/var/lib/${dirName}/db-created"; then
+            ${config.services.mysql.package}/bin/mysql < ${pkg}/share/zoneminder/db/zm_create.sql
+            touch "/var/lib/${dirName}/db-created"
+          fi
+
+          ${zoneminder}/bin/zmupdate.pl -nointeractive
+        '';
+        serviceConfig = {
+          User = user;
+          Group = group;
+          SupplementaryGroups = [ "video" ];
+          ExecStart  = "${zoneminder}/bin/zmpkg.pl start";
+          ExecStop   = "${zoneminder}/bin/zmpkg.pl stop";
+          ExecReload = "${zoneminder}/bin/zmpkg.pl restart";
+          PIDFile = "/run/${dirName}/zm.pid";
+          Type = "forking";
+          Restart = "on-failure";
+          RestartSec = "10s";
+          CacheDirectory = dirs cacheDirs;
+          RuntimeDirectory = dirName;
+          ReadWriteDirectories = lib.mkIf useCustomDir [ cfg.storageDir ];
+          StateDirectory = dirs (if useCustomDir then [] else libDirs);
+          LogsDirectory = dirName;
+          PrivateTmp = true;
+          ProtectSystem = "strict";
+          ProtectKernelTunables = true;
+          SystemCallArchitectures = "native";
+          NoNewPrivileges = true;
+        };
+      };
+    };
+
+    users.groups.${user} = {
+      gid = config.ids.gids.zoneminder;
+    };
+
+    users.users.${user} = {
+      uid = config.ids.uids.zoneminder;
+      group = user;
+      inherit home;
+      inherit (pkgs.zoneminder.meta) description;
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ ];
+}
diff --git a/nixos/modules/services/misc/zookeeper.nix b/nixos/modules/services/misc/zookeeper.nix
new file mode 100644
index 00000000000..3809a93a61e
--- /dev/null
+++ b/nixos/modules/services/misc/zookeeper.nix
@@ -0,0 +1,158 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.zookeeper;
+
+  zookeeperConfig = ''
+    dataDir=${cfg.dataDir}
+    clientPort=${toString cfg.port}
+    autopurge.purgeInterval=${toString cfg.purgeInterval}
+    ${cfg.extraConf}
+    ${cfg.servers}
+  '';
+
+  configDir = pkgs.buildEnv {
+    name = "zookeeper-conf";
+    paths = [
+      (pkgs.writeTextDir "zoo.cfg" zookeeperConfig)
+      (pkgs.writeTextDir "log4j.properties" cfg.logging)
+    ];
+  };
+
+in {
+
+  options.services.zookeeper = {
+    enable = mkOption {
+      description = "Whether to enable Zookeeper.";
+      default = false;
+      type = types.bool;
+    };
+
+    port = mkOption {
+      description = "Zookeeper Client port.";
+      default = 2181;
+      type = types.int;
+    };
+
+    id = mkOption {
+      description = "Zookeeper ID.";
+      default = 0;
+      type = types.int;
+    };
+
+    purgeInterval = mkOption {
+      description = ''
+        The time interval in hours for which the purge task has to be triggered. Set to a positive integer (1 and above) to enable the auto purging.
+      '';
+      default = 1;
+      type = types.int;
+    };
+
+    extraConf = mkOption {
+      description = "Extra configuration for Zookeeper.";
+      type = types.lines;
+      default = ''
+        initLimit=5
+        syncLimit=2
+        tickTime=2000
+      '';
+    };
+
+    servers = mkOption {
+      description = "All Zookeeper Servers.";
+      default = "";
+      type = types.lines;
+      example = ''
+        server.0=host0:2888:3888
+        server.1=host1:2888:3888
+        server.2=host2:2888:3888
+      '';
+    };
+
+    logging = mkOption {
+      description = "Zookeeper logging configuration.";
+      default = ''
+        zookeeper.root.logger=INFO, CONSOLE
+        log4j.rootLogger=INFO, CONSOLE
+        log4j.logger.org.apache.zookeeper.audit.Log4jAuditLogger=INFO, CONSOLE
+        log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
+        log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
+        log4j.appender.CONSOLE.layout.ConversionPattern=[myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n
+      '';
+      type = types.lines;
+    };
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/zookeeper";
+      description = ''
+        Data directory for Zookeeper
+      '';
+    };
+
+    extraCmdLineOptions = mkOption {
+      description = "Extra command line options for the Zookeeper launcher.";
+      default = [ "-Dcom.sun.management.jmxremote" "-Dcom.sun.management.jmxremote.local.only=true" ];
+      type = types.listOf types.str;
+      example = [ "-Djava.net.preferIPv4Stack=true" "-Dcom.sun.management.jmxremote" "-Dcom.sun.management.jmxremote.local.only=true" ];
+    };
+
+    preferIPv4 = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Add the -Djava.net.preferIPv4Stack=true flag to the Zookeeper server.
+      '';
+    };
+
+    package = mkOption {
+      description = "The zookeeper package to use";
+      default = pkgs.zookeeper;
+      defaultText = literalExpression "pkgs.zookeeper";
+      type = types.package;
+    };
+
+  };
+
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [cfg.package];
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 zookeeper - - -"
+      "Z '${cfg.dataDir}' 0700 zookeeper - - -"
+    ];
+
+    systemd.services.zookeeper = {
+      description = "Zookeeper Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.jre}/bin/java \
+            -cp "${cfg.package}/lib/*:${configDir}" \
+            ${escapeShellArgs cfg.extraCmdLineOptions} \
+            -Dzookeeper.datadir.autocreate=false \
+            ${optionalString cfg.preferIPv4 "-Djava.net.preferIPv4Stack=true"} \
+            org.apache.zookeeper.server.quorum.QuorumPeerMain \
+            ${configDir}/zoo.cfg
+        '';
+        User = "zookeeper";
+      };
+      preStart = ''
+        echo "${toString cfg.id}" > ${cfg.dataDir}/myid
+        mkdir -p ${cfg.dataDir}/version-2
+      '';
+    };
+
+    users.users.zookeeper = {
+      isSystemUser = true;
+      group = "zookeeper";
+      description = "Zookeeper daemon user";
+      home = cfg.dataDir;
+    };
+    users.groups.zookeeper = {};
+  };
+}
diff --git a/nixos/modules/services/monitoring/alerta.nix b/nixos/modules/services/monitoring/alerta.nix
new file mode 100644
index 00000000000..a73d94001f7
--- /dev/null
+++ b/nixos/modules/services/monitoring/alerta.nix
@@ -0,0 +1,111 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.alerta;
+
+  alertaConf = pkgs.writeTextFile {
+    name = "alertad.conf";
+    text = ''
+      DATABASE_URL = '${cfg.databaseUrl}'
+      DATABASE_NAME = '${cfg.databaseName}'
+      LOG_FILE = '${cfg.logDir}/alertad.log'
+      LOG_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+      CORS_ORIGINS = [ ${concatMapStringsSep ", " (s: "\"" + s + "\"") cfg.corsOrigins} ];
+      AUTH_REQUIRED = ${if cfg.authenticationRequired then "True" else "False"}
+      SIGNUP_ENABLED = ${if cfg.signupEnabled then "True" else "False"}
+      ${cfg.extraConfig}
+    '';
+  };
+in
+{
+  options.services.alerta = {
+    enable = mkEnableOption "alerta";
+
+    port = mkOption {
+      type = types.int;
+      default = 5000;
+      description = "Port of Alerta";
+    };
+
+    bind = mkOption {
+      type = types.str;
+      default = "0.0.0.0";
+      description = "Address to bind to. The default is to bind to all addresses";
+    };
+
+    logDir = mkOption {
+      type = types.path;
+      description = "Location where the logfiles are stored";
+      default = "/var/log/alerta";
+    };
+
+    databaseUrl = mkOption {
+      type = types.str;
+      description = "URL of the MongoDB or PostgreSQL database to connect to";
+      default = "mongodb://localhost";
+    };
+
+    databaseName = mkOption {
+      type = types.str;
+      description = "Name of the database instance to connect to";
+      default = "monitoring";
+    };
+
+    corsOrigins = mkOption {
+      type = types.listOf types.str;
+      description = "List of URLs that can access the API for Cross-Origin Resource Sharing (CORS)";
+      default = [ "http://localhost" "http://localhost:5000" ];
+    };
+
+    authenticationRequired = mkOption {
+      type = types.bool;
+      description = "Whether users must authenticate when using the web UI or command-line tool";
+      default = false;
+    };
+
+    signupEnabled = mkOption {
+      type = types.bool;
+      description = "Whether to prevent sign-up of new users via the web UI";
+      default = true;
+    };
+
+    extraConfig = mkOption {
+      description = "These lines go into alertad.conf verbatim.";
+      default = "";
+      type = types.lines;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.logDir}' - alerta alerta - -"
+    ];
+
+    systemd.services.alerta = {
+      description = "Alerta Monitoring System";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+      environment = {
+        ALERTA_SVR_CONF_FILE = alertaConf;
+      };
+      serviceConfig = {
+        ExecStart = "${pkgs.alerta-server}/bin/alertad run --port ${toString cfg.port} --host ${cfg.bind}";
+        User = "alerta";
+        Group = "alerta";
+      };
+    };
+
+    environment.systemPackages = [ pkgs.alerta ];
+
+    users.users.alerta = {
+      uid = config.ids.uids.alerta;
+      description = "Alerta user";
+    };
+
+    users.groups.alerta = {
+      gid = config.ids.gids.alerta;
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/apcupsd.nix b/nixos/modules/services/monitoring/apcupsd.nix
new file mode 100644
index 00000000000..1dccbc93edf
--- /dev/null
+++ b/nixos/modules/services/monitoring/apcupsd.nix
@@ -0,0 +1,191 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.apcupsd;
+
+  configFile = pkgs.writeText "apcupsd.conf" ''
+    ## apcupsd.conf v1.1 ##
+    # apcupsd complains if the first line is not like above.
+    ${cfg.configText}
+    SCRIPTDIR ${toString scriptDir}
+  '';
+
+  # List of events from "man apccontrol"
+  eventList = [
+    "annoyme"
+    "battattach"
+    "battdetach"
+    "changeme"
+    "commfailure"
+    "commok"
+    "doreboot"
+    "doshutdown"
+    "emergency"
+    "failing"
+    "killpower"
+    "loadlimit"
+    "mainsback"
+    "onbattery"
+    "offbattery"
+    "powerout"
+    "remotedown"
+    "runlimit"
+    "timeout"
+    "startselftest"
+    "endselftest"
+  ];
+
+  shellCmdsForEventScript = eventname: commands: ''
+    echo "#!${pkgs.runtimeShell}" > "$out/${eventname}"
+    echo '${commands}' >> "$out/${eventname}"
+    chmod a+x "$out/${eventname}"
+  '';
+
+  eventToShellCmds = event: if builtins.hasAttr event cfg.hooks then (shellCmdsForEventScript event (builtins.getAttr event cfg.hooks)) else "";
+
+  scriptDir = pkgs.runCommand "apcupsd-scriptdir" { preferLocalBuild = true; } (''
+    mkdir "$out"
+    # Copy SCRIPTDIR from apcupsd package
+    cp -r ${pkgs.apcupsd}/etc/apcupsd/* "$out"/
+    # Make the files writeable (nix will unset the write bits afterwards)
+    chmod u+w "$out"/*
+    # Remove the sample event notification scripts, because they don't work
+    # anyways (they try to send mail to "root" with the "mail" command)
+    (cd "$out" && rm changeme commok commfailure onbattery offbattery)
+    # Remove the sample apcupsd.conf file (we're generating our own)
+    rm "$out/apcupsd.conf"
+    # Set the SCRIPTDIR= line in apccontrol to the dir we're creating now
+    sed -i -e "s|^SCRIPTDIR=.*|SCRIPTDIR=$out|" "$out/apccontrol"
+    '' + concatStringsSep "\n" (map eventToShellCmds eventList)
+
+  );
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.apcupsd = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to enable the APC UPS daemon. apcupsd monitors your UPS and
+          permits orderly shutdown of your computer in the event of a power
+          failure. User manual: http://www.apcupsd.com/manual/manual.html.
+          Note that apcupsd runs as root (to allow shutdown of computer).
+          You can check the status of your UPS with the "apcaccess" command.
+        '';
+      };
+
+      configText = mkOption {
+        default = ''
+          UPSTYPE usb
+          NISIP 127.0.0.1
+          BATTERYLEVEL 50
+          MINUTES 5
+        '';
+        type = types.lines;
+        description = ''
+          Contents of the runtime configuration file, apcupsd.conf. The default
+          settings makes apcupsd autodetect USB UPSes, limit network access to
+          localhost and shutdown the system when the battery level is below 50
+          percent, or when the UPS has calculated that it has 5 minutes or less
+          of remaining power-on time. See man apcupsd.conf for details.
+        '';
+      };
+
+      hooks = mkOption {
+        default = {};
+        example = {
+          doshutdown = "# shell commands to notify that the computer is shutting down";
+        };
+        type = types.attrsOf types.lines;
+        description = ''
+          Each attribute in this option names an apcupsd event and the string
+          value it contains will be executed in a shell, in response to that
+          event (prior to the default action). See "man apccontrol" for the
+          list of events and what they represent.
+
+          A hook script can stop apccontrol from doing its default action by
+          exiting with value 99. Do not do this unless you know what you're
+          doing.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [ {
+      assertion = let hooknames = builtins.attrNames cfg.hooks; in all (x: elem x eventList) hooknames;
+      message = ''
+        One (or more) attribute names in services.apcupsd.hooks are invalid.
+        Current attribute names: ${toString (builtins.attrNames cfg.hooks)}
+        Valid attribute names  : ${toString eventList}
+      '';
+    } ];
+
+    # Give users access to the "apcaccess" tool
+    environment.systemPackages = [ pkgs.apcupsd ];
+
+    # NOTE 1: apcupsd runs as root because it needs permission to run
+    # "shutdown"
+    #
+    # NOTE 2: When apcupsd calls "wall", it prints an error because stdout is
+    # not connected to a tty (it is connected to the journal):
+    #   wall: cannot get tty name: Inappropriate ioctl for device
+    # The message still gets through.
+    systemd.services.apcupsd = {
+      description = "APC UPS Daemon";
+      wantedBy = [ "multi-user.target" ];
+      preStart = "mkdir -p /run/apcupsd/";
+      serviceConfig = {
+        ExecStart = "${pkgs.apcupsd}/bin/apcupsd -b -f ${configFile} -d1";
+        # TODO: When apcupsd has initiated a shutdown, systemd always ends up
+        # waiting for it to stop ("A stop job is running for UPS daemon"). This
+        # is weird, because in the journal one can clearly see that apcupsd has
+        # received the SIGTERM signal and has already quit (or so it seems).
+        # This reduces the wait time from 90 seconds (default) to just 5. Then
+        # systemd kills it with SIGKILL.
+        TimeoutStopSec = 5;
+      };
+      unitConfig.Documentation = "man:apcupsd(8)";
+    };
+
+    # A special service to tell the UPS to power down/hibernate just before the
+    # computer shuts down. (The UPS has a built in delay before it actually
+    # shuts off power.) Copied from here:
+    # http://forums.opensuse.org/english/get-technical-help-here/applications/479499-apcupsd-systemd-killpower-issues.html
+    systemd.services.apcupsd-killpower = {
+      description = "APC UPS Kill Power";
+      after = [ "shutdown.target" ]; # append umount.target?
+      before = [ "final.target" ];
+      wantedBy = [ "shutdown.target" ];
+      unitConfig = {
+        ConditionPathExists = "/run/apcupsd/powerfail";
+        DefaultDependencies = "no";
+      };
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkgs.apcupsd}/bin/apcupsd --killpower -f ${configFile}";
+        TimeoutSec = "infinity";
+        StandardOutput = "tty";
+        RemainAfterExit = "yes";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/monitoring/arbtt.nix b/nixos/modules/services/monitoring/arbtt.nix
new file mode 100644
index 00000000000..94eead220ae
--- /dev/null
+++ b/nixos/modules/services/monitoring/arbtt.nix
@@ -0,0 +1,62 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.arbtt;
+in {
+  options = {
+    services.arbtt = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the arbtt statistics capture service.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.haskellPackages.arbtt;
+        defaultText = literalExpression "pkgs.haskellPackages.arbtt";
+        description = ''
+          The package to use for the arbtt binaries.
+        '';
+      };
+
+      logFile = mkOption {
+        type = types.str;
+        default = "%h/.arbtt/capture.log";
+        example = "/home/username/.arbtt-capture.log";
+        description = ''
+          The log file for captured samples.
+        '';
+      };
+
+      sampleRate = mkOption {
+        type = types.int;
+        default = 60;
+        example = 120;
+        description = ''
+          The sampling interval in seconds.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.arbtt = {
+      description = "arbtt statistics capture service";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${cfg.package}/bin/arbtt-capture --logfile=${cfg.logFile} --sample-rate=${toString cfg.sampleRate}";
+        Restart = "always";
+      };
+    };
+  };
+
+  meta.maintainers = [ maintainers.michaelpj ];
+}
diff --git a/nixos/modules/services/monitoring/bosun.nix b/nixos/modules/services/monitoring/bosun.nix
new file mode 100644
index 00000000000..4b278b9c200
--- /dev/null
+++ b/nixos/modules/services/monitoring/bosun.nix
@@ -0,0 +1,165 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.bosun;
+
+  configFile = pkgs.writeText "bosun.conf" ''
+    ${optionalString (cfg.opentsdbHost !=null) "tsdbHost = ${cfg.opentsdbHost}"}
+    ${optionalString (cfg.influxHost !=null) "influxHost = ${cfg.influxHost}"}
+    httpListen = ${cfg.listenAddress}
+    stateFile = ${cfg.stateFile}
+    ledisDir = ${cfg.ledisDir}
+    checkFrequency = ${cfg.checkFrequency}
+
+    ${cfg.extraConfig}
+  '';
+
+in {
+
+  options = {
+
+    services.bosun = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run bosun.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.bosun;
+        defaultText = literalExpression "pkgs.bosun";
+        description = ''
+          bosun binary to use.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "bosun";
+        description = ''
+          User account under which bosun runs.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "bosun";
+        description = ''
+          Group account under which bosun runs.
+        '';
+      };
+
+      opentsdbHost = mkOption {
+        type = types.nullOr types.str;
+        default = "localhost:4242";
+        description = ''
+          Host and port of the OpenTSDB database that stores bosun data.
+          To disable opentsdb you can pass null as parameter.
+        '';
+      };
+
+      influxHost = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "localhost:8086";
+        description = ''
+           Host and port of the influxdb database.
+        '';
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = ":8070";
+        description = ''
+          The host address and port that bosun's web interface will listen on.
+        '';
+      };
+
+      stateFile = mkOption {
+        type = types.path;
+        default = "/var/lib/bosun/bosun.state";
+        description = ''
+          Path to bosun's state file.
+        '';
+      };
+
+      ledisDir = mkOption {
+        type = types.path;
+        default = "/var/lib/bosun/ledis_data";
+        description = ''
+          Path to bosun's ledis data dir
+        '';
+      };
+
+      checkFrequency = mkOption {
+        type = types.str;
+        default = "5m";
+        description = ''
+          Bosun's check frequency
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration options for Bosun. You should describe your
+          desired templates, alerts, macros, etc through this configuration
+          option.
+
+          A detailed description of the supported syntax can be found at-spi2-atk
+          http://bosun.org/configuration.html
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.bosun = {
+      description = "bosun metrics collector (part of Bosun)";
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        mkdir -p "$(dirname "${cfg.stateFile}")";
+        touch "${cfg.stateFile}"
+        touch "${cfg.stateFile}.tmp"
+
+        mkdir -p "${cfg.ledisDir}";
+
+        if [ "$(id -u)" = 0 ]; then
+          chown ${cfg.user}:${cfg.group} "${cfg.stateFile}"
+          chown ${cfg.user}:${cfg.group} "${cfg.stateFile}.tmp"
+          chown ${cfg.user}:${cfg.group} "${cfg.ledisDir}"
+        fi
+      '';
+
+      serviceConfig = {
+        PermissionsStartOnly = true;
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = ''
+          ${cfg.package}/bin/bosun -c ${configFile}
+        '';
+      };
+    };
+
+    users.users.bosun = {
+      description = "bosun user";
+      group = "bosun";
+      uid = config.ids.uids.bosun;
+    };
+
+    users.groups.bosun.gid = config.ids.gids.bosun;
+
+  };
+
+}
diff --git a/nixos/modules/services/monitoring/cadvisor.nix b/nixos/modules/services/monitoring/cadvisor.nix
new file mode 100644
index 00000000000..dfbf07efcae
--- /dev/null
+++ b/nixos/modules/services/monitoring/cadvisor.nix
@@ -0,0 +1,142 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cadvisor;
+
+in {
+  options = {
+    services.cadvisor = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to enable cadvisor service.";
+      };
+
+      listenAddress = mkOption {
+        default = "127.0.0.1";
+        type = types.str;
+        description = "Cadvisor listening host";
+      };
+
+      port = mkOption {
+        default = 8080;
+        type = types.int;
+        description = "Cadvisor listening port";
+      };
+
+      storageDriver = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        example = "influxdb";
+        description = "Cadvisor storage driver.";
+      };
+
+      storageDriverHost = mkOption {
+        default = "localhost:8086";
+        type = types.str;
+        description = "Cadvisor storage driver host.";
+      };
+
+      storageDriverDb = mkOption {
+        default = "root";
+        type = types.str;
+        description = "Cadvisord storage driver database name.";
+      };
+
+      storageDriverUser = mkOption {
+        default = "root";
+        type = types.str;
+        description = "Cadvisor storage driver username.";
+      };
+
+      storageDriverPassword = mkOption {
+        default = "root";
+        type = types.str;
+        description = ''
+          Cadvisor storage driver password.
+
+          Warning: this password is stored in the world-readable Nix store. It's
+          recommended to use the <option>storageDriverPasswordFile</option> option
+          since that gives you control over the security of the password.
+          <option>storageDriverPasswordFile</option> also takes precedence over <option>storageDriverPassword</option>.
+        '';
+      };
+
+      storageDriverPasswordFile = mkOption {
+        type = types.str;
+        description = ''
+          File that contains the cadvisor storage driver password.
+
+          <option>storageDriverPasswordFile</option> takes precedence over <option>storageDriverPassword</option>
+
+          Warning: when <option>storageDriverPassword</option> is non-empty this defaults to a file in the
+          world-readable Nix store that contains the value of <option>storageDriverPassword</option>.
+
+          It's recommended to override this with a path not in the Nix store.
+          Tip: use <link xlink:href='https://nixos.org/nixops/manual/#idm140737318306400'>nixops key management</link>
+        '';
+      };
+
+      storageDriverSecure = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Cadvisor storage driver, enable secure communication.";
+      };
+
+      extraOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Additional cadvisor options.
+
+          See <link xlink:href='https://github.com/google/cadvisor/blob/master/docs/runtime_options.md'/> for available options.
+        '';
+      };
+    };
+  };
+
+  config = mkMerge [
+    { services.cadvisor.storageDriverPasswordFile = mkIf (cfg.storageDriverPassword != "") (
+        mkDefault (toString (pkgs.writeTextFile {
+          name = "cadvisor-storage-driver-password";
+          text = cfg.storageDriverPassword;
+        }))
+      );
+    }
+
+    (mkIf cfg.enable {
+      systemd.services.cadvisor = {
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" "docker.service" "influxdb.service" ];
+
+        path = optionals config.boot.zfs.enabled [ pkgs.zfs ];
+
+        postStart = mkBefore ''
+          until ${pkgs.curl.bin}/bin/curl -s -o /dev/null 'http://${cfg.listenAddress}:${toString cfg.port}/containers/'; do
+            sleep 1;
+          done
+        '';
+
+        script = ''
+          exec ${pkgs.cadvisor}/bin/cadvisor \
+            -logtostderr=true \
+            -listen_ip="${cfg.listenAddress}" \
+            -port="${toString cfg.port}" \
+            ${escapeShellArgs cfg.extraOptions} \
+            ${optionalString (cfg.storageDriver != null) ''
+              -storage_driver "${cfg.storageDriver}" \
+              -storage_driver_user "${cfg.storageDriverHost}" \
+              -storage_driver_db "${cfg.storageDriverDb}" \
+              -storage_driver_user "${cfg.storageDriverUser}" \
+              -storage_driver_password "$(cat "${cfg.storageDriverPasswordFile}")" \
+              ${optionalString cfg.storageDriverSecure "-storage_driver_secure"}
+            ''}
+        '';
+
+        serviceConfig.TimeoutStartSec=300;
+      };
+    })
+  ];
+}
diff --git a/nixos/modules/services/monitoring/collectd.nix b/nixos/modules/services/monitoring/collectd.nix
new file mode 100644
index 00000000000..8d81737a3ef
--- /dev/null
+++ b/nixos/modules/services/monitoring/collectd.nix
@@ -0,0 +1,162 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.collectd;
+
+  unvalidated_conf = pkgs.writeText "collectd-unvalidated.conf" ''
+    BaseDir "${cfg.dataDir}"
+    AutoLoadPlugin ${boolToString cfg.autoLoadPlugin}
+    Hostname "${config.networking.hostName}"
+
+    LoadPlugin syslog
+    <Plugin "syslog">
+      LogLevel "info"
+      NotifyLevel "OKAY"
+    </Plugin>
+
+    ${concatStrings (mapAttrsToList (plugin: pluginConfig: ''
+      LoadPlugin ${plugin}
+      <Plugin "${plugin}">
+      ${pluginConfig}
+      </Plugin>
+    '') cfg.plugins)}
+
+    ${concatMapStrings (f: ''
+      Include "${f}"
+    '') cfg.include}
+
+    ${cfg.extraConfig}
+  '';
+
+  conf = if cfg.validateConfig then
+    pkgs.runCommand "collectd.conf" {} ''
+      echo testing ${unvalidated_conf}
+      # collectd -t fails if BaseDir does not exist.
+      sed '1s/^BaseDir.*$/BaseDir "."/' ${unvalidated_conf} > collectd.conf
+      ${package}/bin/collectd -t -C collectd.conf
+      cp ${unvalidated_conf} $out
+    '' else unvalidated_conf;
+
+  package =
+    if cfg.buildMinimalPackage
+    then minimalPackage
+    else cfg.package;
+
+  minimalPackage = cfg.package.override {
+    enabledPlugins = [ "syslog" ] ++ builtins.attrNames cfg.plugins;
+  };
+
+in {
+  options.services.collectd = with types; {
+    enable = mkEnableOption "collectd agent";
+
+    validateConfig = mkOption {
+      default = true;
+      description = ''
+        Validate the syntax of collectd configuration file at build time.
+        Disable this if you use the Include directive on files unavailable in
+        the build sandbox, or when cross-compiling.
+      '';
+      type = types.bool;
+    };
+
+    package = mkOption {
+      default = pkgs.collectd;
+      defaultText = literalExpression "pkgs.collectd";
+      description = ''
+        Which collectd package to use.
+      '';
+      type = types.package;
+    };
+
+    buildMinimalPackage = mkOption {
+      default = false;
+      description = ''
+        Build a minimal collectd package with only the configured `services.collectd.plugins`
+      '';
+      type = bool;
+    };
+
+    user = mkOption {
+      default = "collectd";
+      description = ''
+        User under which to run collectd.
+      '';
+      type = nullOr str;
+    };
+
+    dataDir = mkOption {
+      default = "/var/lib/collectd";
+      description = ''
+        Data directory for collectd agent.
+      '';
+      type = path;
+    };
+
+    autoLoadPlugin = mkOption {
+      default = false;
+      description = ''
+        Enable plugin autoloading.
+      '';
+      type = bool;
+    };
+
+    include = mkOption {
+      default = [];
+      description = ''
+        Additional paths to load config from.
+      '';
+      type = listOf str;
+    };
+
+    plugins = mkOption {
+      default = {};
+      example = { cpu = ""; memory = ""; network = "Server 192.168.1.1 25826"; };
+      description = ''
+        Attribute set of plugin names to plugin config segments
+      '';
+      type = attrsOf lines;
+    };
+
+    extraConfig = mkOption {
+      default = "";
+      description = ''
+        Extra configuration for collectd.
+      '';
+      type = lines;
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - ${cfg.user} - - -"
+    ];
+
+    systemd.services.collectd = {
+      description = "Collectd Monitoring Agent";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = "${package}/sbin/collectd -C ${conf} -f";
+        User = cfg.user;
+        Restart = "on-failure";
+        RestartSec = 3;
+      };
+    };
+
+    users.users = optionalAttrs (cfg.user == "collectd") {
+      collectd = {
+        isSystemUser = true;
+        group = "collectd";
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.user == "collectd") {
+      collectd = {};
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/das_watchdog.nix b/nixos/modules/services/monitoring/das_watchdog.nix
new file mode 100644
index 00000000000..88ca3a9227d
--- /dev/null
+++ b/nixos/modules/services/monitoring/das_watchdog.nix
@@ -0,0 +1,34 @@
+# A general watchdog for the linux operating system that should run in the
+# background at all times to ensure a realtime process won't hang the machine
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inherit (pkgs) das_watchdog;
+
+in {
+  ###### interface
+
+  options = {
+    services.das_watchdog.enable = mkEnableOption "realtime watchdog";
+  };
+
+  ###### implementation
+
+  config = mkIf config.services.das_watchdog.enable {
+    environment.systemPackages = [ das_watchdog ];
+    systemd.services.das_watchdog = {
+      description = "Watchdog to ensure a realtime process won't hang the machine";
+      after = [ "multi-user.target" "sound.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        User = "root";
+        Type = "simple";
+        ExecStart = "${das_watchdog}/bin/das_watchdog";
+        RemainAfterExit = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/datadog-agent.nix b/nixos/modules/services/monitoring/datadog-agent.nix
new file mode 100644
index 00000000000..6d9d1ef973a
--- /dev/null
+++ b/nixos/modules/services/monitoring/datadog-agent.nix
@@ -0,0 +1,296 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.datadog-agent;
+
+  ddConf = {
+    skip_ssl_validation = false;
+    confd_path          = "/etc/datadog-agent/conf.d";
+    additional_checksd  = "/etc/datadog-agent/checks.d";
+    use_dogstatsd       = true;
+  }
+  // optionalAttrs (cfg.logLevel != null) { log_level = cfg.logLevel; }
+  // optionalAttrs (cfg.hostname != null) { inherit (cfg) hostname; }
+  // optionalAttrs (cfg.ddUrl != null) { dd_url = cfg.ddUrl; }
+  // optionalAttrs (cfg.site != null) { site = cfg.site; }
+  // optionalAttrs (cfg.tags != null ) { tags = concatStringsSep ", " cfg.tags; }
+  // optionalAttrs (cfg.enableLiveProcessCollection) { process_config = { enabled = "true"; }; }
+  // optionalAttrs (cfg.enableTraceAgent) { apm_config = { enabled = true; }; }
+  // cfg.extraConfig;
+
+  # Generate Datadog configuration files for each configured checks.
+  # This works because check configurations have predictable paths,
+  # and because JSON is a valid subset of YAML.
+  makeCheckConfigs = entries: mapAttrs' (name: conf: {
+    name = "datadog-agent/conf.d/${name}.d/conf.yaml";
+    value.source = pkgs.writeText "${name}-check-conf.yaml" (builtins.toJSON conf);
+  }) entries;
+
+  defaultChecks = {
+    disk = cfg.diskCheck;
+    network = cfg.networkCheck;
+  };
+
+  # Assemble all check configurations and the top-level agent
+  # configuration.
+  etcfiles = with pkgs; with builtins;
+  { "datadog-agent/datadog.yaml" = {
+      source = writeText "datadog.yaml" (toJSON ddConf);
+    };
+  } // makeCheckConfigs (cfg.checks // defaultChecks);
+
+  # Apply the configured extraIntegrations to the provided agent
+  # package. See the documentation of `dd-agent/integrations-core.nix`
+  # for detailed information on this.
+  datadogPkg = cfg.package.override {
+    pythonPackages = pkgs.datadog-integrations-core cfg.extraIntegrations;
+  };
+in {
+  options.services.datadog-agent = {
+    enable = mkOption {
+      description = ''
+        Whether to enable the datadog-agent v7 monitoring service
+      '';
+      default = false;
+      type = types.bool;
+    };
+
+    package = mkOption {
+      default = pkgs.datadog-agent;
+      defaultText = literalExpression "pkgs.datadog-agent";
+      description = ''
+        Which DataDog v7 agent package to use. Note that the provided
+        package is expected to have an overridable `pythonPackages`-attribute
+        which configures the Python environment with the Datadog
+        checks.
+      '';
+      type = types.package;
+    };
+
+    apiKeyFile = mkOption {
+      description = ''
+        Path to a file containing the Datadog API key to associate the
+        agent with your account.
+      '';
+      example = "/run/keys/datadog_api_key";
+      type = types.path;
+    };
+
+    ddUrl = mkOption {
+      description = ''
+        Custom dd_url to configure the agent with. Useful if traffic to datadog
+        needs to go through a proxy.
+        Don't use this to point to another datadog site (EU) - use site instead.
+      '';
+      default = null;
+      example = "http://haproxy.example.com:3834";
+      type = types.nullOr types.str;
+    };
+
+    site = mkOption {
+      description = ''
+        The datadog site to point the agent towards.
+        Set to datadoghq.eu to point it to their EU site.
+      '';
+      default = null;
+      example = "datadoghq.eu";
+      type = types.nullOr types.str;
+    };
+
+    tags = mkOption {
+      description = "The tags to mark this Datadog agent";
+      example = [ "test" "service" ];
+      default = null;
+      type = types.nullOr (types.listOf types.str);
+    };
+
+    hostname = mkOption {
+      description = "The hostname to show in the Datadog dashboard (optional)";
+      default = null;
+      example = "mymachine.mydomain";
+      type = types.nullOr types.str;
+    };
+
+    logLevel = mkOption {
+      description = "Logging verbosity.";
+      default = null;
+      type = types.nullOr (types.enum ["DEBUG" "INFO" "WARN" "ERROR"]);
+    };
+
+    extraIntegrations = mkOption {
+      default = {};
+      type    = types.attrs;
+
+      description = ''
+        Extra integrations from the Datadog core-integrations
+        repository that should be built and included.
+
+        By default the included integrations are disk, mongo, network,
+        nginx and postgres.
+
+        To include additional integrations the name of the derivation
+        and a function to filter its dependencies from the Python
+        package set must be provided.
+      '';
+
+      example = literalExpression ''
+        {
+          ntp = pythonPackages: [ pythonPackages.ntplib ];
+        }
+      '';
+    };
+
+    extraConfig = mkOption {
+      default = {};
+      type = types.attrs;
+      description = ''
+        Extra configuration options that will be merged into the
+        main config file <filename>datadog.yaml</filename>.
+      '';
+     };
+
+    enableLiveProcessCollection = mkOption {
+      description = ''
+        Whether to enable the live process collection agent.
+      '';
+      default = false;
+      type = types.bool;
+    };
+
+    enableTraceAgent = mkOption {
+      description = ''
+        Whether to enable the trace agent.
+      '';
+      default = false;
+      type = types.bool;
+    };
+
+    checks = mkOption {
+      description = ''
+        Configuration for all Datadog checks. Keys of this attribute
+        set will be used as the name of the check to create the
+        appropriate configuration in `conf.d/$check.d/conf.yaml`.
+
+        The configuration is converted into JSON from the plain Nix
+        language configuration, meaning that you should write
+        configuration adhering to Datadog's documentation - but in Nix
+        language.
+
+        Refer to the implementation of this module (specifically the
+        definition of `defaultChecks`) for an example.
+
+        Note: The 'disk' and 'network' check are configured in
+        separate options because they exist by default. Attempting to
+        override their configuration here will have no effect.
+      '';
+
+      example = {
+        http_check = {
+          init_config = null; # sic!
+          instances = [
+            {
+              name = "some-service";
+              url = "http://localhost:1337/healthz";
+              tags = [ "some-service" ];
+            }
+          ];
+        };
+      };
+
+      default = {};
+
+      # sic! The structure of the values is up to the check, so we can
+      # not usefully constrain the type further.
+      type = with types; attrsOf attrs;
+    };
+
+    diskCheck = mkOption {
+      description = "Disk check config";
+      type = types.attrs;
+      default = {
+        init_config = {};
+        instances = [ { use_mount = "false"; } ];
+      };
+    };
+
+    networkCheck = mkOption {
+      description = "Network check config";
+      type = types.attrs;
+      default = {
+        init_config = {};
+        # Network check only supports one configured instance
+        instances = [ { collect_connection_state = false;
+          excluded_interfaces = [ "lo" "lo0" ]; } ];
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ datadogPkg pkgs.sysstat pkgs.procps pkgs.iproute2 ];
+
+    users.users.datadog = {
+      description = "Datadog Agent User";
+      uid = config.ids.uids.datadog;
+      group = "datadog";
+      home = "/var/log/datadog/";
+      createHome = true;
+    };
+
+    users.groups.datadog.gid = config.ids.gids.datadog;
+
+    systemd.services = let
+      makeService = attrs: recursiveUpdate {
+        path = [ datadogPkg pkgs.python pkgs.sysstat pkgs.procps pkgs.iproute2 ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          User = "datadog";
+          Group = "datadog";
+          Restart = "always";
+          RestartSec = 2;
+        };
+        restartTriggers = [ datadogPkg ] ++  map (x: x.source) (attrValues etcfiles);
+      } attrs;
+    in {
+      datadog-agent = makeService {
+        description = "Datadog agent monitor";
+        preStart = ''
+          chown -R datadog: /etc/datadog-agent
+          rm -f /etc/datadog-agent/auth_token
+        '';
+        script = ''
+          export DD_API_KEY=$(head -n 1 ${cfg.apiKeyFile})
+          exec ${datadogPkg}/bin/agent run -c /etc/datadog-agent/datadog.yaml
+        '';
+        serviceConfig.PermissionsStartOnly = true;
+      };
+
+      dd-jmxfetch = lib.mkIf (lib.hasAttr "jmx" cfg.checks) (makeService {
+        description = "Datadog JMX Fetcher";
+        path = [ datadogPkg pkgs.python pkgs.sysstat pkgs.procps pkgs.jdk ];
+        serviceConfig.ExecStart = "${datadogPkg}/bin/dd-jmxfetch";
+      });
+
+      datadog-process-agent = lib.mkIf cfg.enableLiveProcessCollection (makeService {
+        description = "Datadog Live Process Agent";
+        path = [ ];
+        script = ''
+          export DD_API_KEY=$(head -n 1 ${cfg.apiKeyFile})
+          ${pkgs.datadog-process-agent}/bin/process-agent --config /etc/datadog-agent/datadog.yaml
+        '';
+      });
+
+      datadog-trace-agent = lib.mkIf cfg.enableTraceAgent (makeService {
+        description = "Datadog Trace Agent";
+        path = [ ];
+        script = ''
+          export DD_API_KEY=$(head -n 1 ${cfg.apiKeyFile})
+          ${datadogPkg}/bin/trace-agent -config /etc/datadog-agent/datadog.yaml
+        '';
+      });
+
+    };
+
+    environment.etc = etcfiles;
+  };
+}
diff --git a/nixos/modules/services/monitoring/dd-agent/dd-agent-defaults.nix b/nixos/modules/services/monitoring/dd-agent/dd-agent-defaults.nix
new file mode 100644
index 00000000000..04512819742
--- /dev/null
+++ b/nixos/modules/services/monitoring/dd-agent/dd-agent-defaults.nix
@@ -0,0 +1,8 @@
+# Generated using update-dd-agent-default, please re-run after updating dd-agent. DO NOT EDIT MANUALLY.
+[
+  "auto_conf"
+  "agent_metrics.yaml.default"
+  "disk.yaml.default"
+  "network.yaml.default"
+  "ntp.yaml.default"
+]
diff --git a/nixos/modules/services/monitoring/dd-agent/dd-agent.nix b/nixos/modules/services/monitoring/dd-agent/dd-agent.nix
new file mode 100644
index 00000000000..a290dae8d4b
--- /dev/null
+++ b/nixos/modules/services/monitoring/dd-agent/dd-agent.nix
@@ -0,0 +1,236 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dd-agent;
+
+  ddConf = pkgs.writeText "datadog.conf" ''
+    [Main]
+    dd_url: https://app.datadoghq.com
+    skip_ssl_validation: no
+    api_key: ${cfg.api_key}
+    ${optionalString (cfg.hostname != null) "hostname: ${cfg.hostname}"}
+
+    collector_log_file: /var/log/datadog/collector.log
+    forwarder_log_file: /var/log/datadog/forwarder.log
+    dogstatsd_log_file: /var/log/datadog/dogstatsd.log
+    pup_log_file:       /var/log/datadog/pup.log
+
+    # proxy_host: my-proxy.com
+    # proxy_port: 3128
+    # proxy_user: user
+    # proxy_password: password
+
+    # tags: mytag0, mytag1
+    ${optionalString (cfg.tags != null ) "tags: ${concatStringsSep ", " cfg.tags }"}
+
+    # collect_ec2_tags: no
+    # recent_point_threshold: 30
+    # use_mount: no
+    # listen_port: 17123
+    # graphite_listen_port: 17124
+    # non_local_traffic: no
+    # use_curl_http_client: False
+    # bind_host: localhost
+
+    # use_pup: no
+    # pup_port: 17125
+    # pup_interface: localhost
+    # pup_url: http://localhost:17125
+
+    # dogstatsd_port : 8125
+    # dogstatsd_interval : 10
+    # dogstatsd_normalize : yes
+    # statsd_forward_host: address_of_own_statsd_server
+    # statsd_forward_port: 8125
+
+    # device_blacklist_re: .*\/dev\/mapper\/lxc-box.*
+
+    # ganglia_host: localhost
+    # ganglia_port: 8651
+  '';
+
+  diskConfig = pkgs.writeText "disk.yaml" ''
+    init_config:
+
+    instances:
+      - use_mount: no
+  '';
+
+  networkConfig = pkgs.writeText "network.yaml" ''
+    init_config:
+
+    instances:
+      # Network check only supports one configured instance
+      - collect_connection_state: false
+        excluded_interfaces:
+          - lo
+          - lo0
+  '';
+
+  postgresqlConfig = pkgs.writeText "postgres.yaml" cfg.postgresqlConfig;
+  nginxConfig = pkgs.writeText "nginx.yaml" cfg.nginxConfig;
+  mongoConfig = pkgs.writeText "mongo.yaml" cfg.mongoConfig;
+  jmxConfig = pkgs.writeText "jmx.yaml" cfg.jmxConfig;
+  processConfig = pkgs.writeText "process.yaml" cfg.processConfig;
+
+  etcfiles =
+    let
+      defaultConfd = import ./dd-agent-defaults.nix;
+    in
+      listToAttrs (map (f: {
+        name = "dd-agent/conf.d/${f}";
+        value.source = "${pkgs.dd-agent}/agent/conf.d-system/${f}";
+      }) defaultConfd) //
+      {
+        "dd-agent/datadog.conf".source = ddConf;
+        "dd-agent/conf.d/disk.yaml".source = diskConfig;
+        "dd-agent/conf.d/network.yaml".source = networkConfig;
+      } //
+      (optionalAttrs (cfg.postgresqlConfig != null)
+      {
+        "dd-agent/conf.d/postgres.yaml".source = postgresqlConfig;
+      }) //
+      (optionalAttrs (cfg.nginxConfig != null)
+      {
+        "dd-agent/conf.d/nginx.yaml".source = nginxConfig;
+      }) //
+      (optionalAttrs (cfg.mongoConfig != null)
+      {
+        "dd-agent/conf.d/mongo.yaml".source = mongoConfig;
+      }) //
+      (optionalAttrs (cfg.processConfig != null)
+      {
+        "dd-agent/conf.d/process.yaml".source = processConfig;
+      }) //
+      (optionalAttrs (cfg.jmxConfig != null)
+      {
+        "dd-agent/conf.d/jmx.yaml".source = jmxConfig;
+      });
+
+in {
+  options.services.dd-agent = {
+    enable = mkOption {
+      description = ''
+        Whether to enable the dd-agent v5 monitoring service.
+        For datadog-agent v6, see <option>services.datadog-agent.enable</option>.
+      '';
+      default = false;
+      type = types.bool;
+    };
+
+    api_key = mkOption {
+      description = ''
+        The Datadog API key to associate the agent with your account.
+
+        Warning: this key is stored in cleartext within the world-readable
+        Nix store! Consider using the new v6
+        <option>services.datadog-agent</option> module instead.
+      '';
+      example = "ae0aa6a8f08efa988ba0a17578f009ab";
+      type = types.str;
+    };
+
+    tags = mkOption {
+      description = "The tags to mark this Datadog agent";
+      example = [ "test" "service" ];
+      default = null;
+      type = types.nullOr (types.listOf types.str);
+    };
+
+    hostname = mkOption {
+      description = "The hostname to show in the Datadog dashboard (optional)";
+      default = null;
+      example = "mymachine.mydomain";
+      type = types.nullOr types.str;
+    };
+
+    postgresqlConfig = mkOption {
+      description = "Datadog PostgreSQL integration configuration";
+      default = null;
+      type = types.nullOr types.lines;
+    };
+
+    nginxConfig = mkOption {
+      description = "Datadog nginx integration configuration";
+      default = null;
+      type = types.nullOr types.lines;
+    };
+
+    mongoConfig = mkOption {
+      description = "MongoDB integration configuration";
+      default = null;
+      type = types.nullOr types.lines;
+    };
+
+    jmxConfig = mkOption {
+      description = "JMX integration configuration";
+      default = null;
+      type = types.nullOr types.lines;
+    };
+
+    processConfig = mkOption {
+      description = ''
+        Process integration configuration
+        See <link xlink:href="https://docs.datadoghq.com/integrations/process/"/>
+      '';
+      default = null;
+      type = types.nullOr types.lines;
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.dd-agent pkgs.sysstat pkgs.procps ];
+
+    users.users.datadog = {
+      description = "Datadog Agent User";
+      uid = config.ids.uids.datadog;
+      group = "datadog";
+      home = "/var/log/datadog/";
+      createHome = true;
+    };
+
+    users.groups.datadog.gid = config.ids.gids.datadog;
+
+    systemd.services = let
+      makeService = attrs: recursiveUpdate {
+        path = [ pkgs.dd-agent pkgs.python pkgs.sysstat pkgs.procps pkgs.gohai ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          User = "datadog";
+          Group = "datadog";
+          Restart = "always";
+          RestartSec = 2;
+          PrivateTmp = true;
+        };
+        restartTriggers = [ pkgs.dd-agent ddConf diskConfig networkConfig postgresqlConfig nginxConfig mongoConfig jmxConfig processConfig ];
+      } attrs;
+    in {
+      dd-agent = makeService {
+        description = "Datadog agent monitor";
+        serviceConfig.ExecStart = "${pkgs.dd-agent}/bin/dd-agent foreground";
+      };
+
+      dogstatsd = makeService {
+        description = "Datadog statsd";
+        environment.TMPDIR = "/run/dogstatsd";
+        serviceConfig = {
+          ExecStart = "${pkgs.dd-agent}/bin/dogstatsd start";
+          Type = "forking";
+          PIDFile = "/run/dogstatsd/dogstatsd.pid";
+          RuntimeDirectory = "dogstatsd";
+        };
+      };
+
+      dd-jmxfetch = lib.mkIf (cfg.jmxConfig != null) {
+        description = "Datadog JMX Fetcher";
+        path = [ pkgs.dd-agent pkgs.python pkgs.sysstat pkgs.procps pkgs.jdk ];
+        serviceConfig.ExecStart = "${pkgs.dd-agent}/bin/dd-jmxfetch";
+      };
+    };
+
+    environment.etc = etcfiles;
+  };
+}
diff --git a/nixos/modules/services/monitoring/dd-agent/update-dd-agent-defaults b/nixos/modules/services/monitoring/dd-agent/update-dd-agent-defaults
new file mode 100755
index 00000000000..76724173171
--- /dev/null
+++ b/nixos/modules/services/monitoring/dd-agent/update-dd-agent-defaults
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+dd=$(nix-build --no-out-link -A dd-agent ../../../..)
+echo '# Generated using update-dd-agent-default, please re-run after updating dd-agent. DO NOT EDIT MANUALLY.' > dd-agent-defaults.nix
+echo '[' >> dd-agent-defaults.nix
+echo '  "auto_conf"' >> dd-agent-defaults.nix
+for f in $(find $dd/agent/conf.d-system -maxdepth 1 -type f | grep -v '\.example' | sort); do
+  echo "  \"$(basename $f)\"" >> dd-agent-defaults.nix
+done
+echo ']' >> dd-agent-defaults.nix
diff --git a/nixos/modules/services/monitoring/do-agent.nix b/nixos/modules/services/monitoring/do-agent.nix
new file mode 100644
index 00000000000..4dfb6236727
--- /dev/null
+++ b/nixos/modules/services/monitoring/do-agent.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.do-agent;
+
+in
+{
+  options.services.do-agent = {
+    enable = mkEnableOption "do-agent, the DigitalOcean droplet metrics agent";
+  };
+
+  config = mkIf cfg.enable {
+    systemd.packages = [ pkgs.do-agent ];
+
+    systemd.services.do-agent = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = [ "" "${pkgs.do-agent}/bin/do-agent --syslog" ];
+        DynamicUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/fusion-inventory.nix b/nixos/modules/services/monitoring/fusion-inventory.nix
new file mode 100644
index 00000000000..9b65c76ce02
--- /dev/null
+++ b/nixos/modules/services/monitoring/fusion-inventory.nix
@@ -0,0 +1,63 @@
+# Fusion Inventory daemon.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.fusionInventory;
+
+  configFile = pkgs.writeText "fusion_inventory.conf" ''
+    server = ${concatStringsSep ", " cfg.servers}
+
+    logger = stderr
+
+    ${cfg.extraConfig}
+  '';
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.fusionInventory = {
+
+      enable = mkEnableOption "Fusion Inventory Agent";
+
+      servers = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          The urls of the OCS/GLPI servers to connect to.
+        '';
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Configuration that is injected verbatim into the configuration file.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.fusion-inventory = {
+      description = "FusionInventory user";
+      isSystemUser = true;
+    };
+
+    systemd.services.fusion-inventory = {
+      description = "Fusion Inventory Agent";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.fusionInventory}/bin/fusioninventory-agent --conf-file=${configFile} --daemon --no-fork";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixos/modules/services/monitoring/grafana-image-renderer.nix
new file mode 100644
index 00000000000..b8b95d846c6
--- /dev/null
+++ b/nixos/modules/services/monitoring/grafana-image-renderer.nix
@@ -0,0 +1,150 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.grafana-image-renderer;
+
+  format = pkgs.formats.json { };
+
+  configFile = format.generate "grafana-image-renderer-config.json" cfg.settings;
+in {
+  options.services.grafana-image-renderer = {
+    enable = mkEnableOption "grafana-image-renderer";
+
+    chromium = mkOption {
+      type = types.package;
+      description = ''
+        The chromium to use for image rendering.
+      '';
+    };
+
+    verbose = mkEnableOption "verbosity for the service";
+
+    provisionGrafana = mkEnableOption "Grafana configuration for grafana-image-renderer";
+
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = format.type;
+
+        options = {
+          service = {
+            port = mkOption {
+              type = types.port;
+              default = 8081;
+              description = ''
+                The TCP port to use for the rendering server.
+              '';
+            };
+            logging.level = mkOption {
+              type = types.enum [ "error" "warning" "info" "debug" ];
+              default = "info";
+              description = ''
+                The log-level of the <filename>grafana-image-renderer.service</filename>-unit.
+              '';
+            };
+          };
+          rendering = {
+            width = mkOption {
+              default = 1000;
+              type = types.ints.positive;
+              description = ''
+                Width of the PNG used to display the alerting graph.
+              '';
+            };
+            height = mkOption {
+              default = 500;
+              type = types.ints.positive;
+              description = ''
+                Height of the PNG used to display the alerting graph.
+              '';
+            };
+            mode = mkOption {
+              default = "default";
+              type = types.enum [ "default" "reusable" "clustered" ];
+              description = ''
+                Rendering mode of <package>grafana-image-renderer</package>:
+                <itemizedlist>
+                <listitem><para><literal>default:</literal> Creates on browser-instance
+                  per rendering request.</para></listitem>
+                <listitem><para><literal>reusable:</literal> One browser instance
+                  will be started and reused for each rendering request.</para></listitem>
+                <listitem><para><literal>clustered:</literal> allows to precisely
+                  configure how many browser-instances are supposed to be used. The values
+                  for that mode can be declared in <literal>rendering.clustering</literal>.
+                  </para></listitem>
+                </itemizedlist>
+              '';
+            };
+            args = mkOption {
+              type = types.listOf types.str;
+              default = [ "--no-sandbox" ];
+              description = ''
+                List of CLI flags passed to <package>chromium</package>.
+              '';
+            };
+          };
+        };
+      };
+
+      default = {};
+
+      description = ''
+        Configuration attributes for <package>grafana-image-renderer</package>.
+
+        See <link xlink:href="https://github.com/grafana/grafana-image-renderer/blob/ce1f81438e5f69c7fd7c73ce08bab624c4c92e25/default.json" />
+        for supported values.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.provisionGrafana -> config.services.grafana.enable;
+        message = ''
+          To provision a Grafana instance to use grafana-image-renderer,
+          `services.grafana.enable` must be set to `true`!
+        '';
+      }
+    ];
+
+    services.grafana.extraOptions = mkIf cfg.provisionGrafana {
+      RENDERING_SERVER_URL = "http://localhost:${toString cfg.settings.service.port}/render";
+      RENDERING_CALLBACK_URL = "http://localhost:${toString config.services.grafana.port}";
+    };
+
+    services.grafana-image-renderer.chromium = mkDefault pkgs.chromium;
+
+    services.grafana-image-renderer.settings = {
+      rendering = mapAttrs (const mkDefault) {
+        chromeBin = "${cfg.chromium}/bin/chromium";
+        verboseLogging = cfg.verbose;
+        timezone = config.time.timeZone;
+      };
+
+      service = {
+        logging.level = mkIf cfg.verbose (mkDefault "debug");
+        metrics.enabled = mkDefault false;
+      };
+    };
+
+    systemd.services.grafana-image-renderer = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      description = " A Grafana backend plugin that handles rendering of panels & dashboards to PNGs using headless browser (Chromium/Chrome)";
+
+      environment = {
+        PUPPETEER_SKIP_CHROMIUM_DOWNLOAD = "true";
+      };
+
+      serviceConfig = {
+        DynamicUser = true;
+        PrivateTmp = true;
+        ExecStart = "${pkgs.grafana-image-renderer}/bin/grafana-image-renderer server --config=${configFile}";
+        Restart = "always";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ ma27 ];
+}
diff --git a/nixos/modules/services/monitoring/grafana-reporter.nix b/nixos/modules/services/monitoring/grafana-reporter.nix
new file mode 100644
index 00000000000..e40d78f538f
--- /dev/null
+++ b/nixos/modules/services/monitoring/grafana-reporter.nix
@@ -0,0 +1,67 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.grafana_reporter;
+
+in {
+  options.services.grafana_reporter = {
+    enable = mkEnableOption "grafana_reporter";
+
+    grafana = {
+      protocol = mkOption {
+        description = "Grafana protocol.";
+        default = "http";
+        type = types.enum ["http" "https"];
+      };
+      addr = mkOption {
+        description = "Grafana address.";
+        default = "127.0.0.1";
+        type = types.str;
+      };
+      port = mkOption {
+        description = "Grafana port.";
+        default = 3000;
+        type = types.int;
+      };
+
+    };
+    addr = mkOption {
+      description = "Listening address.";
+      default = "127.0.0.1";
+      type = types.str;
+    };
+
+    port = mkOption {
+      description = "Listening port.";
+      default = 8686;
+      type = types.int;
+    };
+
+    templateDir = mkOption {
+      description = "Optional template directory to use custom tex templates";
+      default = pkgs.grafana_reporter;
+      defaultText = literalExpression "pkgs.grafana_reporter";
+      type = types.either types.str types.path;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.grafana_reporter = {
+      description = "Grafana Reporter Service Daemon";
+      wantedBy = ["multi-user.target"];
+      after = ["network.target"];
+      serviceConfig = let
+        args = lib.concatStringsSep " " [
+          "-proto ${cfg.grafana.protocol}://"
+          "-ip ${cfg.grafana.addr}:${toString cfg.grafana.port}"
+          "-port :${toString cfg.port}"
+          "-templates ${cfg.templateDir}"
+        ];
+      in {
+        ExecStart = "${pkgs.grafana_reporter}/bin/grafana-reporter ${args}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/grafana.nix b/nixos/modules/services/monitoring/grafana.nix
new file mode 100644
index 00000000000..81fca33f5fe
--- /dev/null
+++ b/nixos/modules/services/monitoring/grafana.nix
@@ -0,0 +1,723 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.grafana;
+  opt = options.services.grafana;
+  declarativePlugins = pkgs.linkFarm "grafana-plugins" (builtins.map (pkg: { name = pkg.pname; path = pkg; }) cfg.declarativePlugins);
+  useMysql = cfg.database.type == "mysql";
+  usePostgresql = cfg.database.type == "postgres";
+
+  envOptions = {
+    PATHS_DATA = cfg.dataDir;
+    PATHS_PLUGINS = if builtins.isNull cfg.declarativePlugins then "${cfg.dataDir}/plugins" else declarativePlugins;
+    PATHS_LOGS = "${cfg.dataDir}/log";
+
+    SERVER_PROTOCOL = cfg.protocol;
+    SERVER_HTTP_ADDR = cfg.addr;
+    SERVER_HTTP_PORT = cfg.port;
+    SERVER_SOCKET = cfg.socket;
+    SERVER_DOMAIN = cfg.domain;
+    SERVER_ROOT_URL = cfg.rootUrl;
+    SERVER_STATIC_ROOT_PATH = cfg.staticRootPath;
+    SERVER_CERT_FILE = cfg.certFile;
+    SERVER_CERT_KEY = cfg.certKey;
+
+    DATABASE_TYPE = cfg.database.type;
+    DATABASE_HOST = cfg.database.host;
+    DATABASE_NAME = cfg.database.name;
+    DATABASE_USER = cfg.database.user;
+    DATABASE_PASSWORD = cfg.database.password;
+    DATABASE_PATH = cfg.database.path;
+    DATABASE_CONN_MAX_LIFETIME = cfg.database.connMaxLifetime;
+
+    SECURITY_ADMIN_USER = cfg.security.adminUser;
+    SECURITY_ADMIN_PASSWORD = cfg.security.adminPassword;
+    SECURITY_SECRET_KEY = cfg.security.secretKey;
+
+    USERS_ALLOW_SIGN_UP = boolToString cfg.users.allowSignUp;
+    USERS_ALLOW_ORG_CREATE = boolToString cfg.users.allowOrgCreate;
+    USERS_AUTO_ASSIGN_ORG = boolToString cfg.users.autoAssignOrg;
+    USERS_AUTO_ASSIGN_ORG_ROLE = cfg.users.autoAssignOrgRole;
+
+    AUTH_ANONYMOUS_ENABLED = boolToString cfg.auth.anonymous.enable;
+    AUTH_ANONYMOUS_ORG_NAME = cfg.auth.anonymous.org_name;
+    AUTH_ANONYMOUS_ORG_ROLE = cfg.auth.anonymous.org_role;
+    AUTH_GOOGLE_ENABLED = boolToString cfg.auth.google.enable;
+    AUTH_GOOGLE_ALLOW_SIGN_UP = boolToString cfg.auth.google.allowSignUp;
+    AUTH_GOOGLE_CLIENT_ID = cfg.auth.google.clientId;
+
+    ANALYTICS_REPORTING_ENABLED = boolToString cfg.analytics.reporting.enable;
+
+    SMTP_ENABLED = boolToString cfg.smtp.enable;
+    SMTP_HOST = cfg.smtp.host;
+    SMTP_USER = cfg.smtp.user;
+    SMTP_PASSWORD = cfg.smtp.password;
+    SMTP_FROM_ADDRESS = cfg.smtp.fromAddress;
+  } // cfg.extraOptions;
+
+  datasourceConfiguration = {
+    apiVersion = 1;
+    datasources = cfg.provision.datasources;
+  };
+
+  datasourceFile = pkgs.writeText "datasource.yaml" (builtins.toJSON datasourceConfiguration);
+
+  dashboardConfiguration = {
+    apiVersion = 1;
+    providers = cfg.provision.dashboards;
+  };
+
+  dashboardFile = pkgs.writeText "dashboard.yaml" (builtins.toJSON dashboardConfiguration);
+
+  notifierConfiguration = {
+    apiVersion = 1;
+    notifiers = cfg.provision.notifiers;
+  };
+
+  notifierFile = pkgs.writeText "notifier.yaml" (builtins.toJSON notifierConfiguration);
+
+  provisionConfDir =  pkgs.runCommand "grafana-provisioning" { } ''
+    mkdir -p $out/{datasources,dashboards,notifiers}
+    ln -sf ${datasourceFile} $out/datasources/datasource.yaml
+    ln -sf ${dashboardFile} $out/dashboards/dashboard.yaml
+    ln -sf ${notifierFile} $out/notifiers/notifier.yaml
+  '';
+
+  # Get a submodule without any embedded metadata:
+  _filter = x: filterAttrs (k: v: k != "_module") x;
+
+  # http://docs.grafana.org/administration/provisioning/#datasources
+  grafanaTypes.datasourceConfig = types.submodule {
+    options = {
+      name = mkOption {
+        type = types.str;
+        description = "Name of the datasource. Required.";
+      };
+      type = mkOption {
+        type = types.str;
+        description = "Datasource type. Required.";
+      };
+      access = mkOption {
+        type = types.enum ["proxy" "direct"];
+        default = "proxy";
+        description = "Access mode. proxy or direct (Server or Browser in the UI). Required.";
+      };
+      orgId = mkOption {
+        type = types.int;
+        default = 1;
+        description = "Org id. will default to orgId 1 if not specified.";
+      };
+      url = mkOption {
+        type = types.str;
+        description = "Url of the datasource.";
+      };
+      password = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Database password, if used.";
+      };
+      user = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Database user, if used.";
+      };
+      database = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Database name, if used.";
+      };
+      basicAuth = mkOption {
+        type = types.nullOr types.bool;
+        default = null;
+        description = "Enable/disable basic auth.";
+      };
+      basicAuthUser = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Basic auth username.";
+      };
+      basicAuthPassword = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Basic auth password.";
+      };
+      withCredentials = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable/disable with credentials headers.";
+      };
+      isDefault = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Mark as default datasource. Max one per org.";
+      };
+      jsonData = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        description = "Datasource specific configuration.";
+      };
+      secureJsonData = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        description = "Datasource specific secure configuration.";
+      };
+      version = mkOption {
+        type = types.int;
+        default = 1;
+        description = "Version.";
+      };
+      editable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Allow users to edit datasources from the UI.";
+      };
+    };
+  };
+
+  # http://docs.grafana.org/administration/provisioning/#dashboards
+  grafanaTypes.dashboardConfig = types.submodule {
+    options = {
+      name = mkOption {
+        type = types.str;
+        default = "default";
+        description = "Provider name.";
+      };
+      orgId = mkOption {
+        type = types.int;
+        default = 1;
+        description = "Organization ID.";
+      };
+      folder = mkOption {
+        type = types.str;
+        default = "";
+        description = "Add dashboards to the specified folder.";
+      };
+      type = mkOption {
+        type = types.str;
+        default = "file";
+        description = "Dashboard provider type.";
+      };
+      disableDeletion = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Disable deletion when JSON file is removed.";
+      };
+      updateIntervalSeconds = mkOption {
+        type = types.int;
+        default = 10;
+        description = "How often Grafana will scan for changed dashboards.";
+      };
+      options = {
+        path = mkOption {
+          type = types.path;
+          description = "Path grafana will watch for dashboards.";
+        };
+      };
+    };
+  };
+
+  grafanaTypes.notifierConfig = types.submodule {
+    options = {
+      name = mkOption {
+        type = types.str;
+        default = "default";
+        description = "Notifier name.";
+      };
+      type = mkOption {
+        type = types.enum ["dingding" "discord" "email" "googlechat" "hipchat" "kafka" "line" "teams" "opsgenie" "pagerduty" "prometheus-alertmanager" "pushover" "sensu" "sensugo" "slack" "telegram" "threema" "victorops" "webhook"];
+        description = "Notifier type.";
+      };
+      uid = mkOption {
+        type = types.str;
+        description = "Unique notifier identifier.";
+      };
+      org_id = mkOption {
+        type = types.int;
+        default = 1;
+        description = "Organization ID.";
+      };
+      org_name = mkOption {
+        type = types.str;
+        default = "Main Org.";
+        description = "Organization name.";
+      };
+      is_default = mkOption {
+        type = types.bool;
+        description = "Is the default notifier.";
+        default = false;
+      };
+      send_reminder = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Should the notifier be sent reminder notifications while alerts continue to fire.";
+      };
+      frequency = mkOption {
+        type = types.str;
+        default = "5m";
+        description = "How frequently should the notifier be sent reminders.";
+      };
+      disable_resolve_message = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Turn off the message that sends when an alert returns to OK.";
+      };
+      settings = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        description = "Settings for the notifier type.";
+      };
+      secure_settings = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        description = "Secure settings for the notifier type.";
+      };
+    };
+  };
+in {
+  options.services.grafana = {
+    enable = mkEnableOption "grafana";
+
+    protocol = mkOption {
+      description = "Which protocol to listen.";
+      default = "http";
+      type = types.enum ["http" "https" "socket"];
+    };
+
+    addr = mkOption {
+      description = "Listening address.";
+      default = "127.0.0.1";
+      type = types.str;
+    };
+
+    port = mkOption {
+      description = "Listening port.";
+      default = 3000;
+      type = types.port;
+    };
+
+    socket = mkOption {
+      description = "Listening socket.";
+      default = "/run/grafana/grafana.sock";
+      type = types.str;
+    };
+
+    domain = mkOption {
+      description = "The public facing domain name used to access grafana from a browser.";
+      default = "localhost";
+      type = types.str;
+    };
+
+    rootUrl = mkOption {
+      description = "Full public facing url.";
+      default = "%(protocol)s://%(domain)s:%(http_port)s/";
+      type = types.str;
+    };
+
+    certFile = mkOption {
+      description = "Cert file for ssl.";
+      default = "";
+      type = types.str;
+    };
+
+    certKey = mkOption {
+      description = "Cert key for ssl.";
+      default = "";
+      type = types.str;
+    };
+
+    staticRootPath = mkOption {
+      description = "Root path for static assets.";
+      default = "${cfg.package}/share/grafana/public";
+      defaultText = literalExpression ''"''${package}/share/grafana/public"'';
+      type = types.str;
+    };
+
+    package = mkOption {
+      description = "Package to use.";
+      default = pkgs.grafana;
+      defaultText = literalExpression "pkgs.grafana";
+      type = types.package;
+    };
+
+    declarativePlugins = mkOption {
+      type = with types; nullOr (listOf path);
+      default = null;
+      description = "If non-null, then a list of packages containing Grafana plugins to install. If set, plugins cannot be manually installed.";
+      example = literalExpression "with pkgs.grafanaPlugins; [ grafana-piechart-panel ]";
+      # Make sure each plugin is added only once; otherwise building
+      # the link farm fails, since the same path is added multiple
+      # times.
+      apply = x: if isList x then lib.unique x else x;
+    };
+
+    dataDir = mkOption {
+      description = "Data directory.";
+      default = "/var/lib/grafana";
+      type = types.path;
+    };
+
+    database = {
+      type = mkOption {
+        description = "Database type.";
+        default = "sqlite3";
+        type = types.enum ["mysql" "sqlite3" "postgres"];
+      };
+
+      host = mkOption {
+        description = "Database host.";
+        default = "127.0.0.1:3306";
+        type = types.str;
+      };
+
+      name = mkOption {
+        description = "Database name.";
+        default = "grafana";
+        type = types.str;
+      };
+
+      user = mkOption {
+        description = "Database user.";
+        default = "root";
+        type = types.str;
+      };
+
+      password = mkOption {
+        description = ''
+          Database password.
+          This option is mutual exclusive with the passwordFile option.
+        '';
+        default = "";
+        type = types.str;
+      };
+
+      passwordFile = mkOption {
+        description = ''
+          File that containts the database password.
+          This option is mutual exclusive with the password option.
+        '';
+        default = null;
+        type = types.nullOr types.path;
+      };
+
+      path = mkOption {
+        description = "Database path.";
+        default = "${cfg.dataDir}/data/grafana.db";
+        defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"'';
+        type = types.path;
+      };
+
+      connMaxLifetime = mkOption {
+        description = ''
+          Sets the maximum amount of time (in seconds) a connection may be reused.
+          For MySQL this setting should be shorter than the `wait_timeout' variable.
+        '';
+        default = "unlimited";
+        example = 14400;
+        type = types.either types.int (types.enum [ "unlimited" ]);
+      };
+    };
+
+    provision = {
+      enable = mkEnableOption "provision";
+      datasources = mkOption {
+        description = "Grafana datasources configuration.";
+        default = [];
+        type = types.listOf grafanaTypes.datasourceConfig;
+        apply = x: map _filter x;
+      };
+      dashboards = mkOption {
+        description = "Grafana dashboard configuration.";
+        default = [];
+        type = types.listOf grafanaTypes.dashboardConfig;
+        apply = x: map _filter x;
+      };
+      notifiers = mkOption {
+        description = "Grafana notifier configuration.";
+        default = [];
+        type = types.listOf grafanaTypes.notifierConfig;
+        apply = x: map _filter x;
+      };
+    };
+
+    security = {
+      adminUser = mkOption {
+        description = "Default admin username.";
+        default = "admin";
+        type = types.str;
+      };
+
+      adminPassword = mkOption {
+        description = ''
+          Default admin password.
+          This option is mutual exclusive with the adminPasswordFile option.
+        '';
+        default = "admin";
+        type = types.str;
+      };
+
+      adminPasswordFile = mkOption {
+        description = ''
+          Default admin password.
+          This option is mutual exclusive with the <literal>adminPassword</literal> option.
+        '';
+        default = null;
+        type = types.nullOr types.path;
+      };
+
+      secretKey = mkOption {
+        description = "Secret key used for signing.";
+        default = "SW2YcwTIb9zpOOhoPsMm";
+        type = types.str;
+      };
+
+      secretKeyFile = mkOption {
+        description = "Secret key used for signing.";
+        default = null;
+        type = types.nullOr types.path;
+      };
+    };
+
+    smtp = {
+      enable = mkEnableOption "smtp";
+      host = mkOption {
+        description = "Host to connect to.";
+        default = "localhost:25";
+        type = types.str;
+      };
+      user = mkOption {
+        description = "User used for authentication.";
+        default = "";
+        type = types.str;
+      };
+      password = mkOption {
+        description = ''
+          Password used for authentication.
+          This option is mutual exclusive with the passwordFile option.
+        '';
+        default = "";
+        type = types.str;
+      };
+      passwordFile = mkOption {
+        description = ''
+          Password used for authentication.
+          This option is mutual exclusive with the password option.
+        '';
+        default = null;
+        type = types.nullOr types.path;
+      };
+      fromAddress = mkOption {
+        description = "Email address used for sending.";
+        default = "admin@grafana.localhost";
+        type = types.str;
+      };
+    };
+
+    users = {
+      allowSignUp = mkOption {
+        description = "Disable user signup / registration.";
+        default = false;
+        type = types.bool;
+      };
+
+      allowOrgCreate = mkOption {
+        description = "Whether user is allowed to create organizations.";
+        default = false;
+        type = types.bool;
+      };
+
+      autoAssignOrg = mkOption {
+        description = "Whether to automatically assign new users to default org.";
+        default = true;
+        type = types.bool;
+      };
+
+      autoAssignOrgRole = mkOption {
+        description = "Default role new users will be auto assigned.";
+        default = "Viewer";
+        type = types.enum ["Viewer" "Editor"];
+      };
+    };
+
+    auth = {
+      anonymous = {
+        enable = mkOption {
+          description = "Whether to allow anonymous access.";
+          default = false;
+          type = types.bool;
+        };
+        org_name = mkOption {
+          description = "Which organization to allow anonymous access to.";
+          default = "Main Org.";
+          type = types.str;
+        };
+        org_role = mkOption {
+          description = "Which role anonymous users have in the organization.";
+          default = "Viewer";
+          type = types.str;
+        };
+      };
+      google = {
+        enable = mkOption {
+          description = "Whether to allow Google OAuth2.";
+          default = false;
+          type = types.bool;
+        };
+        allowSignUp = mkOption {
+          description = "Whether to allow sign up with Google OAuth2.";
+          default = false;
+          type = types.bool;
+        };
+        clientId = mkOption {
+          description = "Google OAuth2 client ID.";
+          default = "";
+          type = types.str;
+        };
+        clientSecretFile = mkOption {
+          description = "Google OAuth2 client secret.";
+          default = null;
+          type = types.nullOr types.path;
+        };
+      };
+    };
+
+    analytics.reporting = {
+      enable = mkOption {
+        description = "Whether to allow anonymous usage reporting to stats.grafana.net.";
+        default = true;
+        type = types.bool;
+      };
+    };
+
+    extraOptions = mkOption {
+      description = ''
+        Extra configuration options passed as env variables as specified in
+        <link xlink:href="http://docs.grafana.org/installation/configuration/">documentation</link>,
+        but without GF_ prefix
+      '';
+      default = {};
+      type = with types; attrsOf (either str path);
+    };
+  };
+
+  config = mkIf cfg.enable {
+    warnings = flatten [
+      (optional (
+        cfg.database.password != opt.database.password.default ||
+        cfg.security.adminPassword != opt.security.adminPassword.default
+      ) "Grafana passwords will be stored as plaintext in the Nix store!")
+      (optional (
+        any (x: x.password != null || x.basicAuthPassword != null || x.secureJsonData != null) cfg.provision.datasources
+      ) "Datasource passwords will be stored as plaintext in the Nix store!")
+      (optional (
+        any (x: x.secure_settings != null) cfg.provision.notifiers
+      ) "Notifier secure settings will be stored as plaintext in the Nix store!")
+    ];
+
+    environment.systemPackages = [ cfg.package ];
+
+    assertions = [
+      {
+        assertion = cfg.database.password != opt.database.password.default -> cfg.database.passwordFile == null;
+        message = "Cannot set both password and passwordFile";
+      }
+      {
+        assertion = cfg.security.adminPassword != opt.security.adminPassword.default -> cfg.security.adminPasswordFile == null;
+        message = "Cannot set both adminPassword and adminPasswordFile";
+      }
+      {
+        assertion = cfg.security.secretKey != opt.security.secretKey.default -> cfg.security.secretKeyFile == null;
+        message = "Cannot set both secretKey and secretKeyFile";
+      }
+      {
+        assertion = cfg.smtp.password != opt.smtp.password.default -> cfg.smtp.passwordFile == null;
+        message = "Cannot set both password and passwordFile";
+      }
+    ];
+
+    systemd.services.grafana = {
+      description = "Grafana Service Daemon";
+      wantedBy = ["multi-user.target"];
+      after = ["networking.target"] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
+      environment = {
+        QT_QPA_PLATFORM = "offscreen";
+      } // mapAttrs' (n: v: nameValuePair "GF_${n}" (toString v)) envOptions;
+      script = ''
+        set -o errexit -o pipefail -o nounset -o errtrace
+        shopt -s inherit_errexit
+
+        ${optionalString (cfg.auth.google.clientSecretFile != null) ''
+          GF_AUTH_GOOGLE_CLIENT_SECRET="$(<${escapeShellArg cfg.auth.google.clientSecretFile})"
+          export GF_AUTH_GOOGLE_CLIENT_SECRET
+        ''}
+        ${optionalString (cfg.database.passwordFile != null) ''
+          GF_DATABASE_PASSWORD="$(<${escapeShellArg cfg.database.passwordFile})"
+          export GF_DATABASE_PASSWORD
+        ''}
+        ${optionalString (cfg.security.adminPasswordFile != null) ''
+          GF_SECURITY_ADMIN_PASSWORD="$(<${escapeShellArg cfg.security.adminPasswordFile})"
+          export GF_SECURITY_ADMIN_PASSWORD
+        ''}
+        ${optionalString (cfg.security.secretKeyFile != null) ''
+          GF_SECURITY_SECRET_KEY="$(<${escapeShellArg cfg.security.secretKeyFile})"
+          export GF_SECURITY_SECRET_KEY
+        ''}
+        ${optionalString (cfg.smtp.passwordFile != null) ''
+          GF_SMTP_PASSWORD="$(<${escapeShellArg cfg.smtp.passwordFile})"
+          export GF_SMTP_PASSWORD
+        ''}
+        ${optionalString cfg.provision.enable ''
+          export GF_PATHS_PROVISIONING=${provisionConfDir};
+        ''}
+        exec ${cfg.package}/bin/grafana-server -homepath ${cfg.dataDir}
+      '';
+      serviceConfig = {
+        WorkingDirectory = cfg.dataDir;
+        User = "grafana";
+        RuntimeDirectory = "grafana";
+        RuntimeDirectoryMode = "0755";
+        # Hardening
+        AmbientCapabilities = lib.mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+        CapabilityBoundingSet = if (cfg.port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ];
+        DeviceAllow = [ "" ];
+        LockPersonality = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "full";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        # Upstream grafana is not setting SystemCallFilter for compatibility
+        # reasons, see https://github.com/grafana/grafana/pull/40176
+        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+        UMask = "0027";
+      };
+      preStart = ''
+        ln -fs ${cfg.package}/share/grafana/conf ${cfg.dataDir}
+        ln -fs ${cfg.package}/share/grafana/tools ${cfg.dataDir}
+      '';
+    };
+
+    users.users.grafana = {
+      uid = config.ids.uids.grafana;
+      description = "Grafana user";
+      home = cfg.dataDir;
+      createHome = true;
+      group = "grafana";
+    };
+    users.groups.grafana = {};
+  };
+}
diff --git a/nixos/modules/services/monitoring/graphite.nix b/nixos/modules/services/monitoring/graphite.nix
new file mode 100644
index 00000000000..baa943302a0
--- /dev/null
+++ b/nixos/modules/services/monitoring/graphite.nix
@@ -0,0 +1,582 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.graphite;
+  opt = options.services.graphite;
+  writeTextOrNull = f: t: mapNullable (pkgs.writeTextDir f) t;
+
+  dataDir = cfg.dataDir;
+  staticDir = cfg.dataDir + "/static";
+
+  graphiteLocalSettingsDir = pkgs.runCommand "graphite_local_settings" {
+      inherit graphiteLocalSettings;
+      preferLocalBuild = true;
+    } ''
+    mkdir -p $out
+    ln -s $graphiteLocalSettings $out/graphite_local_settings.py
+  '';
+
+  graphiteLocalSettings = pkgs.writeText "graphite_local_settings.py" (
+    "STATIC_ROOT = '${staticDir}'\n" +
+    optionalString (config.time.timeZone != null) "TIME_ZONE = '${config.time.timeZone}'\n"
+    + cfg.web.extraConfig
+  );
+
+  graphiteApiConfig = pkgs.writeText "graphite-api.yaml" ''
+    search_index: ${dataDir}/index
+    ${optionalString (config.time.timeZone != null) "time_zone: ${config.time.timeZone}"}
+    ${optionalString (cfg.api.finders != []) "finders:"}
+    ${concatMapStringsSep "\n" (f: "  - " + f.moduleName) cfg.api.finders}
+    ${optionalString (cfg.api.functions != []) "functions:"}
+    ${concatMapStringsSep "\n" (f: "  - " + f) cfg.api.functions}
+    ${cfg.api.extraConfig}
+  '';
+
+  seyrenConfig = {
+    SEYREN_URL = cfg.seyren.seyrenUrl;
+    MONGO_URL = cfg.seyren.mongoUrl;
+    GRAPHITE_URL = cfg.seyren.graphiteUrl;
+  } // cfg.seyren.extraConfig;
+
+  configDir = pkgs.buildEnv {
+    name = "graphite-config";
+    paths = lists.filter (el: el != null) [
+      (writeTextOrNull "carbon.conf" cfg.carbon.config)
+      (writeTextOrNull "storage-aggregation.conf" cfg.carbon.storageAggregation)
+      (writeTextOrNull "storage-schemas.conf" cfg.carbon.storageSchemas)
+      (writeTextOrNull "blacklist.conf" cfg.carbon.blacklist)
+      (writeTextOrNull "whitelist.conf" cfg.carbon.whitelist)
+      (writeTextOrNull "rewrite-rules.conf" cfg.carbon.rewriteRules)
+      (writeTextOrNull "relay-rules.conf" cfg.carbon.relayRules)
+      (writeTextOrNull "aggregation-rules.conf" cfg.carbon.aggregationRules)
+    ];
+  };
+
+  carbonOpts = name: with config.ids; ''
+    --nodaemon --syslog --prefix=${name} --pidfile /run/${name}/${name}.pid ${name}
+  '';
+
+  carbonEnv = {
+    PYTHONPATH = let
+      cenv = pkgs.python3.buildEnv.override {
+        extraLibs = [ pkgs.python3Packages.carbon ];
+      };
+    in "${cenv}/${pkgs.python3.sitePackages}";
+    GRAPHITE_ROOT = dataDir;
+    GRAPHITE_CONF_DIR = configDir;
+    GRAPHITE_STORAGE_DIR = dataDir;
+  };
+
+in {
+
+  imports = [
+    (mkRemovedOptionModule ["services" "graphite" "pager"] "")
+  ];
+
+  ###### interface
+
+  options.services.graphite = {
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/db/graphite";
+      description = ''
+        Data directory for graphite.
+      '';
+    };
+
+    web = {
+      enable = mkOption {
+        description = "Whether to enable graphite web frontend.";
+        default = false;
+        type = types.bool;
+      };
+
+      listenAddress = mkOption {
+        description = "Graphite web frontend listen address.";
+        default = "127.0.0.1";
+        type = types.str;
+      };
+
+      port = mkOption {
+        description = "Graphite web frontend port.";
+        default = 8080;
+        type = types.int;
+      };
+
+      extraConfig = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Graphite webapp settings. See:
+          <link xlink:href="http://graphite.readthedocs.io/en/latest/config-local-settings.html"/>
+        '';
+      };
+    };
+
+    api = {
+      enable = mkOption {
+        description = ''
+          Whether to enable graphite api. Graphite api is lightweight alternative
+          to graphite web, with api and without dashboard. It's advised to use
+          grafana as alternative dashboard and influxdb as alternative to
+          graphite carbon.
+
+          For more information visit
+          <link xlink:href="https://graphite-api.readthedocs.org/en/latest/"/>
+        '';
+        default = false;
+        type = types.bool;
+      };
+
+      finders = mkOption {
+        description = "List of finder plugins to load.";
+        default = [];
+        example = literalExpression "[ pkgs.python3Packages.influxgraph ]";
+        type = types.listOf types.package;
+      };
+
+      functions = mkOption {
+        description = "List of functions to load.";
+        default = [
+          "graphite_api.functions.SeriesFunctions"
+          "graphite_api.functions.PieFunctions"
+        ];
+        type = types.listOf types.str;
+      };
+
+      listenAddress = mkOption {
+        description = "Graphite web service listen address.";
+        default = "127.0.0.1";
+        type = types.str;
+      };
+
+      port = mkOption {
+        description = "Graphite api service port.";
+        default = 8080;
+        type = types.int;
+      };
+
+      package = mkOption {
+        description = "Package to use for graphite api.";
+        default = pkgs.python3Packages.graphite_api;
+        defaultText = literalExpression "pkgs.python3Packages.graphite_api";
+        type = types.package;
+      };
+
+      extraConfig = mkOption {
+        description = "Extra configuration for graphite api.";
+        default = ''
+          whisper:
+            directories:
+                - ${dataDir}/whisper
+        '';
+        defaultText = literalExpression ''
+          '''
+            whisper:
+              directories:
+                - ''${config.${opt.dataDir}}/whisper
+          '''
+        '';
+        example = ''
+          allowed_origins:
+            - dashboard.example.com
+          cheat_times: true
+          influxdb:
+            host: localhost
+            port: 8086
+            user: influxdb
+            pass: influxdb
+            db: metrics
+          cache:
+            CACHE_TYPE: 'filesystem'
+            CACHE_DIR: '/tmp/graphite-api-cache'
+        '';
+        type = types.lines;
+      };
+    };
+
+    carbon = {
+      config = mkOption {
+        description = "Content of carbon configuration file.";
+        default = ''
+          [cache]
+          # Listen on localhost by default for security reasons
+          UDP_RECEIVER_INTERFACE = 127.0.0.1
+          PICKLE_RECEIVER_INTERFACE = 127.0.0.1
+          LINE_RECEIVER_INTERFACE = 127.0.0.1
+          CACHE_QUERY_INTERFACE = 127.0.0.1
+          # Do not log every update
+          LOG_UPDATES = False
+          LOG_CACHE_HITS = False
+        '';
+        type = types.str;
+      };
+
+      enableCache = mkOption {
+        description = "Whether to enable carbon cache, the graphite storage daemon.";
+        default = false;
+        type = types.bool;
+      };
+
+      storageAggregation = mkOption {
+        description = "Defines how to aggregate data to lower-precision retentions.";
+        default = null;
+        type = types.nullOr types.str;
+        example = ''
+          [all_min]
+          pattern = \.min$
+          xFilesFactor = 0.1
+          aggregationMethod = min
+        '';
+      };
+
+      storageSchemas = mkOption {
+        description = "Defines retention rates for storing metrics.";
+        default = "";
+        type = types.nullOr types.str;
+        example = ''
+          [apache_busyWorkers]
+          pattern = ^servers\.www.*\.workers\.busyWorkers$
+          retentions = 15s:7d,1m:21d,15m:5y
+        '';
+      };
+
+      blacklist = mkOption {
+        description = "Any metrics received which match one of the experssions will be dropped.";
+        default = null;
+        type = types.nullOr types.str;
+        example = "^some\\.noisy\\.metric\\.prefix\\..*";
+      };
+
+      whitelist = mkOption {
+        description = "Only metrics received which match one of the experssions will be persisted.";
+        default = null;
+        type = types.nullOr types.str;
+        example = ".*";
+      };
+
+      rewriteRules = mkOption {
+        description = ''
+          Regular expression patterns that can be used to rewrite metric names
+          in a search and replace fashion.
+        '';
+        default = null;
+        type = types.nullOr types.str;
+        example = ''
+          [post]
+          _sum$ =
+          _avg$ =
+        '';
+      };
+
+      enableRelay = mkOption {
+        description = "Whether to enable carbon relay, the carbon replication and sharding service.";
+        default = false;
+        type = types.bool;
+      };
+
+      relayRules = mkOption {
+        description = "Relay rules are used to send certain metrics to a certain backend.";
+        default = null;
+        type = types.nullOr types.str;
+        example = ''
+          [example]
+          pattern = ^mydata\.foo\..+
+          servers = 10.1.2.3, 10.1.2.4:2004, myserver.mydomain.com
+        '';
+      };
+
+      enableAggregator = mkOption {
+        description = "Whether to enable carbon aggregator, the carbon buffering service.";
+        default = false;
+        type = types.bool;
+      };
+
+      aggregationRules = mkOption {
+        description = "Defines if and how received metrics will be aggregated.";
+        default = null;
+        type = types.nullOr types.str;
+        example = ''
+          <env>.applications.<app>.all.requests (60) = sum <env>.applications.<app>.*.requests
+          <env>.applications.<app>.all.latency (60) = avg <env>.applications.<app>.*.latency
+        '';
+      };
+    };
+
+    seyren = {
+      enable = mkOption {
+        description = "Whether to enable seyren service.";
+        default = false;
+        type = types.bool;
+      };
+
+      port = mkOption {
+        description = "Seyren listening port.";
+        default = 8081;
+        type = types.int;
+      };
+
+      seyrenUrl = mkOption {
+        default = "http://localhost:${toString cfg.seyren.port}/";
+        defaultText = literalExpression ''"http://localhost:''${toString config.${opt.seyren.port}}/"'';
+        description = "Host where seyren is accessible.";
+        type = types.str;
+      };
+
+      graphiteUrl = mkOption {
+        default = "http://${cfg.web.listenAddress}:${toString cfg.web.port}";
+        defaultText = literalExpression ''"http://''${config.${opt.web.listenAddress}}:''${toString config.${opt.web.port}}"'';
+        description = "Host where graphite service runs.";
+        type = types.str;
+      };
+
+      mongoUrl = mkOption {
+        default = "mongodb://${config.services.mongodb.bind_ip}:27017/seyren";
+        defaultText = literalExpression ''"mongodb://''${config.services.mongodb.bind_ip}:27017/seyren"'';
+        description = "Mongodb connection string.";
+        type = types.str;
+      };
+
+      extraConfig = mkOption {
+        default = {};
+        description = ''
+          Extra seyren configuration. See
+          <link xlink:href='https://github.com/scobal/seyren#config' />
+        '';
+        type = types.attrsOf types.str;
+        example = literalExpression ''
+          {
+            GRAPHITE_USERNAME = "user";
+            GRAPHITE_PASSWORD = "pass";
+          }
+        '';
+      };
+    };
+
+    beacon = {
+      enable = mkEnableOption "graphite beacon";
+
+      config = mkOption {
+        description = "Graphite beacon configuration.";
+        default = {};
+        type = types.attrs;
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkMerge [
+    (mkIf cfg.carbon.enableCache {
+      systemd.services.carbonCache = let name = "carbon-cache"; in {
+        description = "Graphite Data Storage Backend";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        environment = carbonEnv;
+        serviceConfig = {
+          RuntimeDirectory = name;
+          ExecStart = "${pkgs.python3Packages.twisted}/bin/twistd ${carbonOpts name}";
+          User = "graphite";
+          Group = "graphite";
+          PermissionsStartOnly = true;
+          PIDFile="/run/${name}/${name}.pid";
+        };
+        preStart = ''
+          install -dm0700 -o graphite -g graphite ${cfg.dataDir}
+          install -dm0700 -o graphite -g graphite ${cfg.dataDir}/whisper
+        '';
+      };
+    })
+
+    (mkIf cfg.carbon.enableAggregator {
+      systemd.services.carbonAggregator = let name = "carbon-aggregator"; in {
+        enable = cfg.carbon.enableAggregator;
+        description = "Carbon Data Aggregator";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        environment = carbonEnv;
+        serviceConfig = {
+          RuntimeDirectory = name;
+          ExecStart = "${pkgs.python3Packages.twisted}/bin/twistd ${carbonOpts name}";
+          User = "graphite";
+          Group = "graphite";
+          PIDFile="/run/${name}/${name}.pid";
+        };
+      };
+    })
+
+    (mkIf cfg.carbon.enableRelay {
+      systemd.services.carbonRelay = let name = "carbon-relay"; in {
+        description = "Carbon Data Relay";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        environment = carbonEnv;
+        serviceConfig = {
+          RuntimeDirectory = name;
+          ExecStart = "${pkgs.python3Packages.twisted}/bin/twistd ${carbonOpts name}";
+          User = "graphite";
+          Group = "graphite";
+          PIDFile="/run/${name}/${name}.pid";
+        };
+      };
+    })
+
+    (mkIf (cfg.carbon.enableCache || cfg.carbon.enableAggregator || cfg.carbon.enableRelay) {
+      environment.systemPackages = [
+        pkgs.python3Packages.carbon
+      ];
+    })
+
+    (mkIf cfg.web.enable ({
+      systemd.services.graphiteWeb = {
+        description = "Graphite Web Interface";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        path = [ pkgs.perl ];
+        environment = {
+          PYTHONPATH = let
+              penv = pkgs.python3.buildEnv.override {
+                extraLibs = [
+                  pkgs.python3Packages.graphite-web
+                ];
+              };
+              penvPack = "${penv}/${pkgs.python3.sitePackages}";
+            in concatStringsSep ":" [
+                 "${graphiteLocalSettingsDir}"
+                 "${penvPack}"
+                 # explicitly adding pycairo in path because it cannot be imported via buildEnv
+                 "${pkgs.python3Packages.pycairo}/${pkgs.python3.sitePackages}"
+               ];
+          DJANGO_SETTINGS_MODULE = "graphite.settings";
+          GRAPHITE_SETTINGS_MODULE = "graphite_local_settings";
+          GRAPHITE_CONF_DIR = configDir;
+          GRAPHITE_STORAGE_DIR = dataDir;
+          LD_LIBRARY_PATH = "${pkgs.cairo.out}/lib";
+        };
+        serviceConfig = {
+          ExecStart = ''
+            ${pkgs.python3Packages.waitress-django}/bin/waitress-serve-django \
+              --host=${cfg.web.listenAddress} --port=${toString cfg.web.port}
+          '';
+          User = "graphite";
+          Group = "graphite";
+          PermissionsStartOnly = true;
+        };
+        preStart = ''
+          if ! test -e ${dataDir}/db-created; then
+            mkdir -p ${dataDir}/{whisper/,log/webapp/}
+            chmod 0700 ${dataDir}/{whisper/,log/webapp/}
+
+            ${pkgs.python3Packages.django}/bin/django-admin.py migrate --noinput
+
+            chown -R graphite:graphite ${dataDir}
+
+            touch ${dataDir}/db-created
+          fi
+
+          # Only collect static files when graphite_web changes.
+          if ! [ "${dataDir}/current_graphite_web" -ef "${pkgs.python3Packages.graphite-web}" ]; then
+            mkdir -p ${staticDir}
+            ${pkgs.python3Packages.django}/bin/django-admin.py collectstatic  --noinput --clear
+            chown -R graphite:graphite ${staticDir}
+            ln -sfT "${pkgs.python3Packages.graphite-web}" "${dataDir}/current_graphite_web"
+          fi
+        '';
+      };
+
+      environment.systemPackages = [ pkgs.python3Packages.graphite-web ];
+    }))
+
+    (mkIf cfg.api.enable {
+      systemd.services.graphiteApi = {
+        description = "Graphite Api Interface";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        environment = {
+          PYTHONPATH = let
+              aenv = pkgs.python3.buildEnv.override {
+                extraLibs = [ cfg.api.package pkgs.cairo pkgs.python3Packages.cffi ] ++ cfg.api.finders;
+              };
+            in "${aenv}/${pkgs.python3.sitePackages}";
+          GRAPHITE_API_CONFIG = graphiteApiConfig;
+          LD_LIBRARY_PATH = "${pkgs.cairo.out}/lib";
+        };
+        serviceConfig = {
+          ExecStart = ''
+            ${pkgs.python3Packages.waitress}/bin/waitress-serve \
+            --host=${cfg.api.listenAddress} --port=${toString cfg.api.port} \
+            graphite_api.app:app
+          '';
+          User = "graphite";
+          Group = "graphite";
+          PermissionsStartOnly = true;
+        };
+        preStart = ''
+          if ! test -e ${dataDir}/db-created; then
+            mkdir -p ${dataDir}/cache/
+            chmod 0700 ${dataDir}/cache/
+
+            chown graphite:graphite ${cfg.dataDir}
+            chown -R graphite:graphite ${cfg.dataDir}/cache
+
+            touch ${dataDir}/db-created
+          fi
+        '';
+      };
+    })
+
+    (mkIf cfg.seyren.enable {
+      systemd.services.seyren = {
+        description = "Graphite Alerting Dashboard";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" "mongodb.service" ];
+        environment = seyrenConfig;
+        serviceConfig = {
+          ExecStart = "${pkgs.seyren}/bin/seyren -httpPort ${toString cfg.seyren.port}";
+          WorkingDirectory = dataDir;
+          User = "graphite";
+          Group = "graphite";
+        };
+        preStart = ''
+          if ! test -e ${dataDir}/db-created; then
+            mkdir -p ${dataDir}
+            chown graphite:graphite ${dataDir}
+          fi
+        '';
+      };
+
+      services.mongodb.enable = mkDefault true;
+    })
+
+    (mkIf cfg.beacon.enable {
+      systemd.services.graphite-beacon = {
+        description = "Grpahite Beacon Alerting Daemon";
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          ExecStart = ''
+            ${pkgs.python3Packages.graphite_beacon}/bin/graphite-beacon \
+              --config=${pkgs.writeText "graphite-beacon.json" (builtins.toJSON cfg.beacon.config)}
+          '';
+          User = "graphite";
+          Group = "graphite";
+        };
+      };
+    })
+
+    (mkIf (
+      cfg.carbon.enableCache || cfg.carbon.enableAggregator || cfg.carbon.enableRelay ||
+      cfg.web.enable || cfg.api.enable ||
+      cfg.seyren.enable || cfg.beacon.enable
+     ) {
+      users.users.graphite = {
+        uid = config.ids.uids.graphite;
+        group = "graphite";
+        description = "Graphite daemon user";
+        home = dataDir;
+      };
+      users.groups.graphite.gid = config.ids.gids.graphite;
+    })
+  ];
+}
diff --git a/nixos/modules/services/monitoring/hdaps.nix b/nixos/modules/services/monitoring/hdaps.nix
new file mode 100644
index 00000000000..2cad3b84d84
--- /dev/null
+++ b/nixos/modules/services/monitoring/hdaps.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.hdapsd;
+  hdapsd = [ pkgs.hdapsd ];
+in
+{
+  options = {
+    services.hdapsd.enable = mkEnableOption
+      ''
+        Hard Drive Active Protection System Daemon,
+        devices are detected and managed automatically by udev and systemd
+      '';
+  };
+
+  config = mkIf cfg.enable {
+    boot.kernelModules = [ "hdapsd" ];
+    services.udev.packages = hdapsd;
+    systemd.packages = hdapsd;
+  };
+}
diff --git a/nixos/modules/services/monitoring/heapster.nix b/nixos/modules/services/monitoring/heapster.nix
new file mode 100644
index 00000000000..44f53e1890a
--- /dev/null
+++ b/nixos/modules/services/monitoring/heapster.nix
@@ -0,0 +1,59 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.heapster;
+in {
+  options.services.heapster = {
+    enable = mkOption {
+      description = "Whether to enable heapster monitoring";
+      default = false;
+      type = types.bool;
+    };
+
+    source = mkOption {
+      description = "Heapster metric source";
+      example = "kubernetes:https://kubernetes.default";
+      type = types.str;
+    };
+
+    sink = mkOption {
+      description = "Heapster metic sink";
+      example = "influxdb:http://localhost:8086";
+      type = types.str;
+    };
+
+    extraOpts = mkOption {
+      description = "Heapster extra options";
+      default = "";
+      type = types.separatedString " ";
+    };
+
+    package = mkOption {
+      description = "Package to use by heapster";
+      default = pkgs.heapster;
+      defaultText = literalExpression "pkgs.heapster";
+      type = types.package;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.heapster = {
+      wantedBy = ["multi-user.target"];
+      after = ["cadvisor.service" "kube-apiserver.service"];
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/heapster --source=${cfg.source} --sink=${cfg.sink} ${cfg.extraOpts}";
+        User = "heapster";
+      };
+    };
+
+    users.users.heapster = {
+      isSystemUser = true;
+      group = "heapster";
+      description = "Heapster user";
+    };
+    users.groups.heapster = {};
+  };
+}
diff --git a/nixos/modules/services/monitoring/incron.nix b/nixos/modules/services/monitoring/incron.nix
new file mode 100644
index 00000000000..2681c35d6a0
--- /dev/null
+++ b/nixos/modules/services/monitoring/incron.nix
@@ -0,0 +1,103 @@
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.incron;
+
+in
+
+{
+  options = {
+
+    services.incron = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the incron daemon.
+
+          Note that commands run under incrontab only support common Nix profiles for the <envar>PATH</envar> provided variable.
+        '';
+      };
+
+      allow = mkOption {
+        type = types.nullOr (types.listOf types.str);
+        default = null;
+        description = ''
+          Users allowed to use incrontab.
+
+          If empty then no user will be allowed to have their own incrontab.
+          If <literal>null</literal> then will defer to <option>deny</option>.
+          If both <option>allow</option> and <option>deny</option> are null
+          then all users will be allowed to have their own incrontab.
+        '';
+      };
+
+      deny = mkOption {
+        type = types.nullOr (types.listOf types.str);
+        default = null;
+        description = "Users forbidden from using incrontab.";
+      };
+
+      systab = mkOption {
+        type = types.lines;
+        default = "";
+        description = "The system incrontab contents.";
+        example = ''
+          /var/mail IN_CLOSE_WRITE abc $@/$#
+          /tmp IN_ALL_EVENTS efg $@/$# $&
+        '';
+      };
+
+      extraPackages = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "[ pkgs.rsync ]";
+        description = "Extra packages available to the system incrontab.";
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    warnings = optional (cfg.allow != null && cfg.deny != null)
+      "If `services.incron.allow` is set then `services.incron.deny` will be ignored.";
+
+    environment.systemPackages = [ pkgs.incron ];
+
+    security.wrappers.incrontab =
+    { setuid = true;
+      owner = "root";
+      group = "root";
+      source = "${pkgs.incron}/bin/incrontab";
+    };
+
+    # incron won't read symlinks
+    environment.etc."incron.d/system" = {
+      mode = "0444";
+      text = cfg.systab;
+    };
+    environment.etc."incron.allow" = mkIf (cfg.allow != null) {
+      text = concatStringsSep "\n" cfg.allow;
+    };
+    environment.etc."incron.deny" = mkIf (cfg.deny != null) {
+      text = concatStringsSep "\n" cfg.deny;
+    };
+
+    systemd.services.incron = {
+      description = "File System Events Scheduler";
+      wantedBy = [ "multi-user.target" ];
+      path = cfg.extraPackages;
+      serviceConfig.PIDFile = "/run/incrond.pid";
+      serviceConfig.ExecStartPre = "${pkgs.coreutils}/bin/mkdir -m 710 -p /var/spool/incron";
+      serviceConfig.ExecStart = "${pkgs.incron}/bin/incrond --foreground";
+    };
+  };
+
+}
diff --git a/nixos/modules/services/monitoring/kapacitor.nix b/nixos/modules/services/monitoring/kapacitor.nix
new file mode 100644
index 00000000000..a79c647becf
--- /dev/null
+++ b/nixos/modules/services/monitoring/kapacitor.nix
@@ -0,0 +1,188 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.kapacitor;
+
+  kapacitorConf = pkgs.writeTextFile {
+    name = "kapacitord.conf";
+    text = ''
+      hostname="${config.networking.hostName}"
+      data_dir="${cfg.dataDir}"
+
+      [http]
+        bind-address = "${cfg.bind}:${toString cfg.port}"
+        log-enabled = false
+        auth-enabled = false
+
+      [task]
+        dir = "${cfg.dataDir}/tasks"
+        snapshot-interval = "${cfg.taskSnapshotInterval}"
+
+      [replay]
+        dir = "${cfg.dataDir}/replay"
+
+      [storage]
+        boltdb = "${cfg.dataDir}/kapacitor.db"
+
+      ${optionalString (cfg.loadDirectory != null) ''
+        [load]
+          enabled = true
+          dir = "${cfg.loadDirectory}"
+      ''}
+
+      ${optionalString (cfg.defaultDatabase.enable) ''
+        [[influxdb]]
+          name = "default"
+          enabled = true
+          default = true
+          urls = [ "${cfg.defaultDatabase.url}" ]
+          username = "${cfg.defaultDatabase.username}"
+          password = "${cfg.defaultDatabase.password}"
+      ''}
+
+      ${optionalString (cfg.alerta.enable) ''
+        [alerta]
+          enabled = true
+          url = "${cfg.alerta.url}"
+          token = "${cfg.alerta.token}"
+          environment = "${cfg.alerta.environment}"
+          origin = "${cfg.alerta.origin}"
+      ''}
+
+      ${cfg.extraConfig}
+    '';
+  };
+in
+{
+  options.services.kapacitor = {
+    enable = mkEnableOption "kapacitor";
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/kapacitor";
+      description = "Location where Kapacitor stores its state";
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = 9092;
+      description = "Port of Kapacitor";
+    };
+
+    bind = mkOption {
+      type = types.str;
+      default = "";
+      example = "0.0.0.0";
+      description = "Address to bind to. The default is to bind to all addresses";
+    };
+
+    extraConfig = mkOption {
+      description = "These lines go into kapacitord.conf verbatim.";
+      default = "";
+      type = types.lines;
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = "kapacitor";
+      description = "User account under which Kapacitor runs";
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "kapacitor";
+      description = "Group under which Kapacitor runs";
+    };
+
+    taskSnapshotInterval = mkOption {
+      type = types.str;
+      description = "Specifies how often to snapshot the task state  (in InfluxDB time units)";
+      default = "1m0s";
+    };
+
+    loadDirectory = mkOption {
+      type = types.nullOr types.path;
+      description = "Directory where to load services from, such as tasks, templates and handlers (or null to disable service loading on startup)";
+      default = null;
+    };
+
+    defaultDatabase = {
+      enable = mkEnableOption "kapacitor.defaultDatabase";
+
+      url = mkOption {
+        description = "The URL to an InfluxDB server that serves as the default database";
+        example = "http://localhost:8086";
+        type = types.str;
+      };
+
+      username = mkOption {
+        description = "The username to connect to the remote InfluxDB server";
+        type = types.str;
+      };
+
+      password = mkOption {
+        description = "The password to connect to the remote InfluxDB server";
+        type = types.str;
+      };
+    };
+
+    alerta = {
+      enable = mkEnableOption "kapacitor alerta integration";
+
+      url = mkOption {
+        description = "The URL to the Alerta REST API";
+        default = "http://localhost:5000";
+        type = types.str;
+      };
+
+      token = mkOption {
+        description = "Default Alerta authentication token";
+        type = types.str;
+        default = "";
+      };
+
+      environment = mkOption {
+        description = "Default Alerta environment";
+        type = types.str;
+        default = "Production";
+      };
+
+      origin = mkOption {
+        description = "Default origin of alert";
+        type = types.str;
+        default = "kapacitor";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.kapacitor ];
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.kapacitor = {
+      description = "Kapacitor Real-Time Stream Processing Engine";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.kapacitor}/bin/kapacitord -config ${kapacitorConf}";
+        User = "kapacitor";
+        Group = "kapacitor";
+      };
+    };
+
+    users.users.kapacitor = {
+      uid = config.ids.uids.kapacitor;
+      description = "Kapacitor user";
+      home = cfg.dataDir;
+    };
+
+    users.groups.kapacitor = {
+      gid = config.ids.gids.kapacitor;
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/loki.nix b/nixos/modules/services/monitoring/loki.nix
new file mode 100644
index 00000000000..ebac70c30c2
--- /dev/null
+++ b/nixos/modules/services/monitoring/loki.nix
@@ -0,0 +1,114 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) escapeShellArgs mkEnableOption mkIf mkOption types;
+
+  cfg = config.services.loki;
+
+  prettyJSON = conf:
+    pkgs.runCommand "loki-config.json" { } ''
+      echo '${builtins.toJSON conf}' | ${pkgs.jq}/bin/jq 'del(._module)' > $out
+    '';
+
+in {
+  options.services.loki = {
+    enable = mkEnableOption "loki";
+
+    user = mkOption {
+      type = types.str;
+      default = "loki";
+      description = ''
+        User under which the Loki service runs.
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "loki";
+      description = ''
+        Group under which the Loki service runs.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/loki";
+      description = ''
+        Specify the directory for Loki.
+      '';
+    };
+
+    configuration = mkOption {
+      type = (pkgs.formats.json {}).type;
+      default = {};
+      description = ''
+        Specify the configuration for Loki in Nix.
+      '';
+    };
+
+    configFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Specify a configuration file that Loki should use.
+      '';
+    };
+
+    extraFlags = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "--server.http-listen-port=3101" ];
+      description = ''
+        Specify a list of additional command line flags,
+        which get escaped and are then passed to Loki.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [{
+      assertion = (
+        (cfg.configuration == {} -> cfg.configFile != null) &&
+        (cfg.configFile != null -> cfg.configuration == {})
+      );
+      message  = ''
+        Please specify either
+        'services.loki.configuration' or
+        'services.loki.configFile'.
+      '';
+    }];
+
+    environment.systemPackages = [ pkgs.grafana-loki ]; # logcli
+
+    users.groups.${cfg.group} = { };
+    users.users.${cfg.user} = {
+      description = "Loki Service User";
+      group = cfg.group;
+      home = cfg.dataDir;
+      createHome = true;
+      isSystemUser = true;
+    };
+
+    systemd.services.loki = {
+      description = "Loki Service Daemon";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = let
+        conf = if cfg.configFile == null
+               then prettyJSON cfg.configuration
+               else cfg.configFile;
+      in
+      {
+        ExecStart = "${pkgs.grafana-loki}/bin/loki --config.file=${conf} ${escapeShellArgs cfg.extraFlags}";
+        User = cfg.user;
+        Restart = "always";
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectSystem = "full";
+        DevicePolicy = "closed";
+        NoNewPrivileges = true;
+        WorkingDirectory = cfg.dataDir;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/longview.nix b/nixos/modules/services/monitoring/longview.nix
new file mode 100644
index 00000000000..9c38956f9ba
--- /dev/null
+++ b/nixos/modules/services/monitoring/longview.nix
@@ -0,0 +1,160 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.longview;
+
+  runDir = "/run/longview";
+  configsDir = "${runDir}/longview.d";
+
+in {
+  options = {
+
+    services.longview = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If enabled, system metrics will be sent to Linode LongView.
+        '';
+      };
+
+      apiKey = mkOption {
+        type = types.str;
+        default = "";
+        example = "01234567-89AB-CDEF-0123456789ABCDEF";
+        description = ''
+          Longview API key. To get this, look in Longview settings which
+          are found at https://manager.linode.com/longview/.
+
+          Warning: this secret is stored in the world-readable Nix store!
+          Use <option>apiKeyFile</option> instead.
+        '';
+      };
+
+      apiKeyFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/run/keys/longview-api-key";
+        description = ''
+          A file containing the Longview API key.
+          To get this, look in Longview settings which
+          are found at https://manager.linode.com/longview/.
+
+          <option>apiKeyFile</option> takes precedence over <option>apiKey</option>.
+        '';
+      };
+
+      apacheStatusUrl = mkOption {
+        type = types.str;
+        default = "";
+        example = "http://127.0.0.1/server-status";
+        description = ''
+          The Apache status page URL. If provided, Longview will
+          gather statistics from this location. This requires Apache
+          mod_status to be loaded and enabled.
+        '';
+      };
+
+      nginxStatusUrl = mkOption {
+        type = types.str;
+        default = "";
+        example = "http://127.0.0.1/nginx_status";
+        description = ''
+          The Nginx status page URL. Longview will gather statistics
+          from this URL. This requires the Nginx stub_status module to
+          be enabled and configured at the given location.
+        '';
+      };
+
+      mysqlUser = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          The user for connecting to the MySQL database. If provided,
+          Longview will connect to MySQL and collect statistics about
+          queries, etc. This user does not need to have been granted
+          any extra privileges.
+        '';
+      };
+
+      mysqlPassword = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          The password corresponding to <option>mysqlUser</option>.
+          Warning: this is stored in cleartext in the Nix store!
+          Use <option>mysqlPasswordFile</option> instead.
+        '';
+      };
+
+      mysqlPasswordFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/run/keys/dbpassword";
+        description = ''
+          A file containing the password corresponding to <option>mysqlUser</option>.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.longview =
+      { description = "Longview Metrics Collection";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig.Type = "forking";
+        serviceConfig.ExecStop = "-${pkgs.coreutils}/bin/kill -TERM $MAINPID";
+        serviceConfig.ExecReload = "-${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        serviceConfig.PIDFile = "${runDir}/longview.pid";
+        serviceConfig.ExecStart = "${pkgs.longview}/bin/longview";
+        preStart = ''
+          umask 077
+          mkdir -p ${configsDir}
+        '' + (optionalString (cfg.apiKeyFile != null) ''
+          cp --no-preserve=all "${cfg.apiKeyFile}" ${runDir}/longview.key
+        '') + (optionalString (cfg.apacheStatusUrl != "") ''
+          cat > ${configsDir}/Apache.conf <<EOF
+          location ${cfg.apacheStatusUrl}?auto
+          EOF
+        '') + (optionalString (cfg.mysqlUser != "" && cfg.mysqlPasswordFile != null) ''
+          cat > ${configsDir}/MySQL.conf <<EOF
+          username ${cfg.mysqlUser}
+          password `head -n1 "${cfg.mysqlPasswordFile}"`
+          EOF
+        '') + (optionalString (cfg.nginxStatusUrl != "") ''
+          cat > ${configsDir}/Nginx.conf <<EOF
+          location ${cfg.nginxStatusUrl}
+          EOF
+        '');
+      };
+
+    warnings = let warn = k: optional (cfg.${k} != "")
+                 "config.services.longview.${k} is insecure. Use ${k}File instead.";
+               in concatMap warn [ "apiKey" "mysqlPassword" ];
+
+    assertions = [
+      { assertion = cfg.apiKeyFile != null;
+        message = "Longview needs an API key configured";
+      }
+    ];
+
+    # Create API key file if not configured.
+    services.longview.apiKeyFile = mkIf (cfg.apiKey != "")
+      (mkDefault (toString (pkgs.writeTextFile {
+        name = "longview.key";
+        text = cfg.apiKey;
+      })));
+
+    # Create MySQL password file if not configured.
+    services.longview.mysqlPasswordFile = mkDefault (toString (pkgs.writeTextFile {
+      name = "mysql-password-file";
+      text = cfg.mysqlPassword;
+    }));
+  };
+}
diff --git a/nixos/modules/services/monitoring/mackerel-agent.nix b/nixos/modules/services/monitoring/mackerel-agent.nix
new file mode 100644
index 00000000000..aeb6247abd8
--- /dev/null
+++ b/nixos/modules/services/monitoring/mackerel-agent.nix
@@ -0,0 +1,110 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mackerel-agent;
+  settingsFmt = pkgs.formats.toml {};
+in {
+  options.services.mackerel-agent = {
+    enable = mkEnableOption "mackerel.io agent";
+
+    # the upstream package runs as root, but doesn't seem to be strictly
+    # necessary for basic functionality
+    runAsRoot = mkEnableOption "Whether to run as root.";
+
+    autoRetirement = mkEnableOption ''
+      Whether to automatically retire the host upon OS shutdown.
+    '';
+
+    apiKeyFile = mkOption {
+      type = types.path;
+      example = "/run/keys/mackerel-api-key";
+      description = ''
+        Path to file containing the Mackerel API key. The file should contain a
+        single line of the following form:
+
+        <literallayout>apikey = "EXAMPLE_API_KEY"</literallayout>
+      '';
+    };
+
+    settings = mkOption {
+      description = ''
+        Options for mackerel-agent.conf.
+
+        Documentation:
+        <link xlink:href="https://mackerel.io/docs/entry/spec/agent"/>
+      '';
+
+      default = {};
+      example = {
+        verbose = false;
+        silent = false;
+      };
+
+      type = types.submodule {
+        freeformType = settingsFmt.type;
+
+        options.host_status = {
+          on_start = mkOption {
+            type = types.enum [ "working" "standby" "maintenance" "poweroff" ];
+            description = "Host status after agent startup.";
+            default = "working";
+          };
+          on_stop = mkOption {
+            type = types.enum [ "working" "standby" "maintenance" "poweroff" ];
+            description = "Host status after agent shutdown.";
+            default = "poweroff";
+          };
+        };
+
+        options.diagnostic =
+          mkEnableOption "Collect memory usage for the agent itself";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ mackerel-agent ];
+
+    environment.etc = {
+      "mackerel-agent/mackerel-agent.conf".source =
+        settingsFmt.generate "mackerel-agent.conf" cfg.settings;
+      "mackerel-agent/conf.d/api-key.conf".source = cfg.apiKeyFile;
+    };
+
+    services.mackerel-agent.settings = {
+      root = mkDefault "/var/lib/mackerel-agent";
+      pidfile = mkDefault "/run/mackerel-agent/mackerel-agent.pid";
+
+      # conf.d stores the symlink to cfg.apiKeyFile
+      include = mkDefault "/etc/mackerel-agent/conf.d/*.conf";
+    };
+
+    # upstream service file in https://git.io/JUt4Q
+    systemd.services.mackerel-agent = {
+      description = "mackerel.io agent";
+      after = [ "network-online.target" "nss-lookup.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        MACKEREL_PLUGIN_WORKDIR = mkDefault "%C/mackerel-agent";
+      };
+      serviceConfig = {
+        DynamicUser = !cfg.runAsRoot;
+        PrivateTmp = mkDefault true;
+        CacheDirectory = "mackerel-agent";
+        ConfigurationDirectory = "mackerel-agent";
+        RuntimeDirectory = "mackerel-agent";
+        StateDirectory = "mackerel-agent";
+        ExecStart = "${pkgs.mackerel-agent}/bin/mackerel-agent supervise";
+        ExecStopPost = mkIf cfg.autoRetirement "${pkg.mackerel-agent}/bin/mackerel-agent retire -force";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        LimitNOFILE = mkDefault 65536;
+        LimitNPROC = mkDefault 65536;
+      };
+      restartTriggers = [
+        config.environment.etc."mackerel-agent/mackerel-agent.conf".source
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/metricbeat.nix b/nixos/modules/services/monitoring/metricbeat.nix
new file mode 100644
index 00000000000..e75039daa10
--- /dev/null
+++ b/nixos/modules/services/monitoring/metricbeat.nix
@@ -0,0 +1,151 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib)
+    attrValues
+    literalExpression
+    mkEnableOption
+    mkIf
+    mkOption
+    types
+    ;
+  cfg = config.services.metricbeat;
+
+  settingsFormat = pkgs.formats.yaml {};
+
+in
+{
+  options = {
+
+    services.metricbeat = {
+
+      enable = mkEnableOption "metricbeat";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.metricbeat;
+        defaultText = literalExpression "pkgs.metricbeat";
+        example = literalExpression "pkgs.metricbeat7";
+        description = ''
+          The metricbeat package to use
+        '';
+      };
+
+      modules = mkOption {
+        description = ''
+          Metricbeat modules are responsible for reading metrics from the various sources.
+
+          This is like <literal>services.metricbeat.settings.metricbeat.modules</literal>,
+          but structured as an attribute set. This has the benefit that multiple
+          NixOS modules can contribute settings to a single metricbeat module.
+
+          A module can be specified multiple times by choosing a different <literal>&lt;name></literal>
+          for each, but setting <xref linkend="opt-services.metricbeat.modules._name_.module"/> to the same value.
+
+          See <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-modules.html"/>.
+        '';
+        default = {};
+        type = types.attrsOf (types.submodule ({ name, ... }: {
+          freeformType = settingsFormat.type;
+          options = {
+            module = mkOption {
+              type = types.str;
+              default = name;
+              description = ''
+                The name of the module.
+
+                Look for the value after <literal>module:</literal> on the individual
+                module pages linked from <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-modules.html"/>.
+              '';
+            };
+          };
+        }));
+        example = {
+          system = {
+            metricsets = ["cpu" "load" "memory" "network" "process" "process_summary" "uptime" "socket_summary"];
+            enabled = true;
+            period = "10s";
+            processes = [".*"];
+            cpu.metrics = ["percentages" "normalized_percentages"];
+            core.metrics = ["percentages"];
+          };
+        };
+      };
+
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = settingsFormat.type;
+          options = {
+
+            name = mkOption {
+              type = types.str;
+              default = "";
+              description = ''
+                Name of the beat. Defaults to the hostname.
+                See <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/configuration-general-options.html#_name"/>.
+              '';
+            };
+
+            tags = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = ''
+                Tags to place on the shipped metrics.
+                See <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/configuration-general-options.html#_tags_2"/>.
+              '';
+            };
+
+            metricbeat.modules = mkOption {
+              type = types.listOf settingsFormat.type;
+              default = [];
+              internal = true;
+              description = ''
+                The metric collecting modules. Use <xref linkend="opt-services.metricbeat.modules"/> instead.
+
+                See <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-modules.html"/>.
+              '';
+            };
+          };
+        };
+        default = {};
+        description = ''
+          Configuration for metricbeat. See <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/configuring-howto-metricbeat.html"/> for supported values.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      {
+        # empty modules would cause a failure at runtime
+        assertion = cfg.settings.metricbeat.modules != [];
+        message = "services.metricbeat: You must configure one or more modules.";
+      }
+    ];
+
+    services.metricbeat.settings.metricbeat.modules = attrValues cfg.modules;
+
+    systemd.services.metricbeat = {
+      description = "metricbeat metrics shipper";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.package}/bin/metricbeat \
+            -c ${settingsFormat.generate "metricbeat.yml" cfg.settings} \
+            --path.data $STATE_DIRECTORY \
+            --path.logs $LOGS_DIRECTORY \
+            ;
+        '';
+        Restart = "always";
+        DynamicUser = true;
+        ProtectSystem = "strict";
+        ProtectHome = "tmpfs";
+        StateDirectory = "metricbeat";
+        LogsDirectory = "metricbeat";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/monit.nix b/nixos/modules/services/monitoring/monit.nix
new file mode 100644
index 00000000000..379ee967620
--- /dev/null
+++ b/nixos/modules/services/monitoring/monit.nix
@@ -0,0 +1,48 @@
+{config, pkgs, lib, ...}:
+
+with lib;
+
+let
+  cfg = config.services.monit;
+in
+
+{
+  options.services.monit = {
+
+    enable = mkEnableOption "Monit";
+
+    config = mkOption {
+      type = types.lines;
+      default = "";
+      description = "monitrc content";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.monit ];
+
+    environment.etc.monitrc = {
+      text = cfg.config;
+      mode = "0400";
+    };
+
+    systemd.services.monit = {
+      description = "Pro-active monitoring utility for unix systems";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.monit}/bin/monit -I -c /etc/monitrc";
+        ExecStop = "${pkgs.monit}/bin/monit -c /etc/monitrc quit";
+        ExecReload = "${pkgs.monit}/bin/monit -c /etc/monitrc reload";
+        KillMode = "process";
+        Restart = "always";
+      };
+      restartTriggers = [ config.environment.etc.monitrc.source ];
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ ryantm ];
+}
diff --git a/nixos/modules/services/monitoring/munin.nix b/nixos/modules/services/monitoring/munin.nix
new file mode 100644
index 00000000000..4fddb1e37e2
--- /dev/null
+++ b/nixos/modules/services/monitoring/munin.nix
@@ -0,0 +1,404 @@
+{ config, lib, pkgs, ... }:
+
+# TODO: support munin-async
+# TODO: LWP/Pg perl libs aren't recognized
+
+# TODO: support fastcgi
+# http://guide.munin-monitoring.org/en/latest/example/webserver/apache-cgi.html
+# spawn-fcgi -s /run/munin/fastcgi-graph.sock -U www-data   -u munin -g munin /usr/lib/munin/cgi/munin-cgi-graph
+# spawn-fcgi -s /run/munin/fastcgi-html.sock  -U www-data   -u munin -g munin /usr/lib/munin/cgi/munin-cgi-html
+# https://paste.sh/vofcctHP#-KbDSXVeWoifYncZmLfZzgum
+# nginx https://munin.readthedocs.org/en/latest/example/webserver/nginx.html
+
+
+with lib;
+
+let
+  nodeCfg = config.services.munin-node;
+  cronCfg = config.services.munin-cron;
+
+  muninConf = pkgs.writeText "munin.conf"
+    ''
+      dbdir     /var/lib/munin
+      htmldir   /var/www/munin
+      logdir    /var/log/munin
+      rundir    /run/munin
+
+      ${lib.optionalString (cronCfg.extraCSS != "") "staticdir ${customStaticDir}"}
+
+      ${cronCfg.extraGlobalConfig}
+
+      ${cronCfg.hosts}
+    '';
+
+  nodeConf = pkgs.writeText "munin-node.conf"
+    ''
+      log_level 3
+      log_file Sys::Syslog
+      port 4949
+      host *
+      background 0
+      user root
+      group root
+      host_name ${config.networking.hostName}
+      setsid 0
+
+      # wrapped plugins by makeWrapper being with dots
+      ignore_file ^\.
+
+      allow ^::1$
+      allow ^127\.0\.0\.1$
+
+      ${nodeCfg.extraConfig}
+    '';
+
+  pluginConf = pkgs.writeText "munin-plugin-conf"
+    ''
+      [hddtemp_smartctl]
+      user root
+      group root
+
+      [meminfo]
+      user root
+      group root
+
+      [ipmi*]
+      user root
+      group root
+
+      [munin*]
+      env.UPDATE_STATSFILE /var/lib/munin/munin-update.stats
+
+      ${nodeCfg.extraPluginConfig}
+    '';
+
+  pluginConfDir = pkgs.stdenv.mkDerivation {
+    name = "munin-plugin-conf.d";
+    buildCommand = ''
+      mkdir $out
+      ln -s ${pluginConf} $out/nixos-config
+    '';
+  };
+
+  # Copy one Munin plugin into the Nix store with a specific name.
+  # This is suitable for use with plugins going directly into /etc/munin/plugins,
+  # i.e. munin.extraPlugins.
+  internOnePlugin = name: path:
+    "cp -a '${path}' '${name}'";
+
+  # Copy an entire tree of Munin plugins into a single directory in the Nix
+  # store, with no renaming.
+  # This is suitable for use with munin-node-configure --suggest, i.e.
+  # munin.extraAutoPlugins.
+  internManyPlugins = name: path:
+    "find '${path}' -type f -perm /a+x -exec cp -a -t . '{}' '+'";
+
+  # Use the appropriate intern-fn to copy the plugins into the store and patch
+  # them afterwards in an attempt to get them to run on NixOS.
+  internAndFixPlugins = name: intern-fn: paths:
+    pkgs.runCommand name {} ''
+      mkdir -p "$out"
+      cd "$out"
+      ${lib.concatStringsSep "\n"
+          (lib.attrsets.mapAttrsToList intern-fn paths)}
+      chmod -R u+w .
+      find . -type f -exec sed -E -i '
+        s,(/usr)?/s?bin/,/run/current-system/sw/bin/,g
+      ' '{}' '+'
+    '';
+
+  # TODO: write a derivation for munin-contrib, so that for contrib plugins
+  # you can just refer to them by name rather than needing to include a copy
+  # of munin-contrib in your nixos configuration.
+  extraPluginDir = internAndFixPlugins "munin-extra-plugins.d"
+    internOnePlugin nodeCfg.extraPlugins;
+
+  extraAutoPluginDir = internAndFixPlugins "munin-extra-auto-plugins.d"
+    internManyPlugins
+    (builtins.listToAttrs
+      (map
+        (path: { name = baseNameOf path; value = path; })
+        nodeCfg.extraAutoPlugins));
+
+  customStaticDir = pkgs.runCommand "munin-custom-static-data" {} ''
+    cp -a "${pkgs.munin}/etc/opt/munin/static" "$out"
+    cd "$out"
+    chmod -R u+w .
+    echo "${cronCfg.extraCSS}" >> style.css
+    echo "${cronCfg.extraCSS}" >> style-new.css
+  '';
+in
+
+{
+
+  options = {
+
+    services.munin-node = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable Munin Node agent. Munin node listens on 0.0.0.0 and
+          by default accepts connections only from 127.0.0.1 for security reasons.
+
+          See <link xlink:href='http://guide.munin-monitoring.org/en/latest/architecture/index.html' />.
+        '';
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          <filename>munin-node.conf</filename> extra configuration. See
+          <link xlink:href='http://guide.munin-monitoring.org/en/latest/reference/munin-node.conf.html' />
+        '';
+      };
+
+      extraPluginConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          <filename>plugin-conf.d</filename> extra plugin configuration. See
+          <link xlink:href='http://guide.munin-monitoring.org/en/latest/plugin/use.html' />
+        '';
+        example = ''
+          [fail2ban_*]
+          user root
+        '';
+      };
+
+      extraPlugins = mkOption {
+        default = {};
+        type = with types; attrsOf path;
+        description = ''
+          Additional Munin plugins to activate. Keys are the name of the plugin
+          symlink, values are the path to the underlying plugin script. You
+          can use the same plugin script multiple times (e.g. for wildcard
+          plugins).
+
+          Note that these plugins do not participate in autoconfiguration. If
+          you want to autoconfigure additional plugins, use
+          <option>services.munin-node.extraAutoPlugins</option>.
+
+          Plugins enabled in this manner take precedence over autoconfigured
+          plugins.
+
+          Plugins will be copied into the Nix store, and it will attempt to
+          modify them to run properly by fixing hardcoded references to
+          <literal>/bin</literal>, <literal>/usr/bin</literal>,
+          <literal>/sbin</literal>, and <literal>/usr/sbin</literal>.
+        '';
+        example = literalExpression ''
+          {
+            zfs_usage_bigpool = /src/munin-contrib/plugins/zfs/zfs_usage_;
+            zfs_usage_smallpool = /src/munin-contrib/plugins/zfs/zfs_usage_;
+            zfs_list = /src/munin-contrib/plugins/zfs/zfs_list;
+          };
+        '';
+      };
+
+      extraAutoPlugins = mkOption {
+        default = [];
+        type = with types; listOf path;
+        description = ''
+          Additional Munin plugins to autoconfigure, using
+          <literal>munin-node-configure --suggest</literal>. These should be
+          the actual paths to the plugin files (or directories containing them),
+          not just their names.
+
+          If you want to manually enable individual plugins instead, use
+          <option>services.munin-node.extraPlugins</option>.
+
+          Note that only plugins that have the 'autoconfig' capability will do
+          anything if listed here, since plugins that cannot autoconfigure
+          won't be automatically enabled by
+          <literal>munin-node-configure</literal>.
+
+          Plugins will be copied into the Nix store, and it will attempt to
+          modify them to run properly by fixing hardcoded references to
+          <literal>/bin</literal>, <literal>/usr/bin</literal>,
+          <literal>/sbin</literal>, and <literal>/usr/sbin</literal>.
+        '';
+        example = literalExpression ''
+          [
+            /src/munin-contrib/plugins/zfs
+            /src/munin-contrib/plugins/ssh
+          ];
+        '';
+      };
+
+      disabledPlugins = mkOption {
+        # TODO: figure out why Munin isn't writing the log file and fix it.
+        # In the meantime this at least suppresses a useless graph full of
+        # NaNs in the output.
+        default = [ "munin_stats" ];
+        type = with types; listOf str;
+        description = ''
+          Munin plugins to disable, even if
+          <literal>munin-node-configure --suggest</literal> tries to enable
+          them. To disable a wildcard plugin, use an actual wildcard, as in
+          the example.
+
+          munin_stats is disabled by default as it tries to read
+          <literal>/var/log/munin/munin-update.log</literal> for timing
+          information, and the NixOS build of Munin does not write this file.
+        '';
+        example = [ "diskstats" "zfs_usage_*" ];
+      };
+    };
+
+    services.munin-cron = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable munin-cron. Takes care of all heavy lifting to collect data from
+          nodes and draws graphs to html. Runs munin-update, munin-limits,
+          munin-graphs and munin-html in that order.
+
+          HTML output is in <filename>/var/www/munin/</filename>, configure your
+          favourite webserver to serve static files.
+        '';
+      };
+
+      extraGlobalConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          <filename>munin.conf</filename> extra global configuration.
+          See <link xlink:href='http://guide.munin-monitoring.org/en/latest/reference/munin.conf.html' />.
+          Useful to setup notifications, see
+          <link xlink:href='http://guide.munin-monitoring.org/en/latest/tutorial/alert.html' />
+        '';
+        example = ''
+          contact.email.command mail -s "Munin notification for ''${var:host}" someone@example.com
+        '';
+      };
+
+      hosts = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Definitions of hosts of nodes to collect data from. Needs at least one
+          host for cron to succeed. See
+          <link xlink:href='http://guide.munin-monitoring.org/en/latest/reference/munin.conf.html' />
+        '';
+        example = literalExpression ''
+          '''
+            [''${config.networking.hostName}]
+            address localhost
+          '''
+        '';
+      };
+
+      extraCSS = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Custom styling for the HTML that munin-cron generates. This will be
+          appended to the CSS files used by munin-cron and will thus take
+          precedence over the builtin styles.
+        '';
+        example = ''
+          /* A simple dark theme. */
+          html, body { background: #222222; }
+          #header, #footer { background: #333333; }
+          img.i, img.iwarn, img.icrit, img.iunkn {
+            filter: invert(100%) hue-rotate(-30deg);
+          }
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkMerge [ (mkIf (nodeCfg.enable || cronCfg.enable)  {
+
+    environment.systemPackages = [ pkgs.munin ];
+
+    users.users.munin = {
+      description = "Munin monitoring user";
+      group = "munin";
+      uid = config.ids.uids.munin;
+      home = "/var/lib/munin";
+    };
+
+    users.groups.munin = {
+      gid = config.ids.gids.munin;
+    };
+
+  }) (mkIf nodeCfg.enable {
+
+    systemd.services.munin-node = {
+      description = "Munin Node";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = with pkgs; [ munin smartmontools "/run/current-system/sw" "/run/wrappers" ];
+      environment.MUNIN_LIBDIR = "${pkgs.munin}/lib";
+      environment.MUNIN_PLUGSTATE = "/run/munin";
+      environment.MUNIN_LOGDIR = "/var/log/munin";
+      preStart = ''
+        echo "Updating munin plugins..."
+
+        mkdir -p /etc/munin/plugins
+        rm -rf /etc/munin/plugins/*
+
+        # Autoconfigure builtin plugins
+        ${pkgs.munin}/bin/munin-node-configure --suggest --shell --families contrib,auto,manual --config ${nodeConf} --libdir=${pkgs.munin}/lib/plugins --servicedir=/etc/munin/plugins --sconfdir=${pluginConfDir} 2>/dev/null | ${pkgs.bash}/bin/bash
+
+        # Autoconfigure extra plugins
+        ${pkgs.munin}/bin/munin-node-configure --suggest --shell --families contrib,auto,manual --config ${nodeConf} --libdir=${extraAutoPluginDir} --servicedir=/etc/munin/plugins --sconfdir=${pluginConfDir} 2>/dev/null | ${pkgs.bash}/bin/bash
+
+        ${lib.optionalString (nodeCfg.extraPlugins != {}) ''
+            # Link in manually enabled plugins
+            ln -f -s -t /etc/munin/plugins ${extraPluginDir}/*
+          ''}
+
+        ${lib.optionalString (nodeCfg.disabledPlugins != []) ''
+            # Disable plugins
+            cd /etc/munin/plugins
+            rm -f ${toString nodeCfg.disabledPlugins}
+          ''}
+      '';
+      serviceConfig = {
+        ExecStart = "${pkgs.munin}/sbin/munin-node --config ${nodeConf} --servicedir /etc/munin/plugins/ --sconfdir=${pluginConfDir}";
+      };
+    };
+
+    # munin_stats plugin breaks as of 2.0.33 when this doesn't exist
+    systemd.tmpfiles.rules = [ "d /run/munin 0755 munin munin -" ];
+
+  }) (mkIf cronCfg.enable {
+
+    # Munin is hardcoded to use DejaVu Mono and the graphs come out wrong if
+    # it's not available.
+    fonts.fonts = [ pkgs.dejavu_fonts ];
+
+    systemd.timers.munin-cron = {
+      description = "batch Munin master programs";
+      wantedBy = [ "timers.target" ];
+      timerConfig.OnCalendar = "*:0/5";
+    };
+
+    systemd.services.munin-cron = {
+      description = "batch Munin master programs";
+      unitConfig.Documentation = "man:munin-cron(8)";
+
+      serviceConfig = {
+        Type = "oneshot";
+        User = "munin";
+        ExecStart = "${pkgs.munin}/bin/munin-cron --config ${muninConf}";
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d /run/munin 0755 munin munin -"
+      "d /var/log/munin 0755 munin munin -"
+      "d /var/www/munin 0755 munin munin -"
+      "d /var/lib/munin 0755 munin munin -"
+    ];
+  })];
+}
diff --git a/nixos/modules/services/monitoring/nagios.nix b/nixos/modules/services/monitoring/nagios.nix
new file mode 100644
index 00000000000..2c7f0ed1966
--- /dev/null
+++ b/nixos/modules/services/monitoring/nagios.nix
@@ -0,0 +1,213 @@
+# Nagios system/network monitoring daemon.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nagios;
+
+  nagiosState = "/var/lib/nagios";
+  nagiosLogDir = "/var/log/nagios";
+  urlPath = "/nagios";
+
+  nagiosObjectDefs = cfg.objectDefs;
+
+  nagiosObjectDefsDir = pkgs.runCommand "nagios-objects" {
+      inherit nagiosObjectDefs;
+      preferLocalBuild = true;
+    } "mkdir -p $out; ln -s $nagiosObjectDefs $out/";
+
+  nagiosCfgFile = let
+    default = {
+      log_file="${nagiosLogDir}/current";
+      log_archive_path="${nagiosLogDir}/archive";
+      status_file="${nagiosState}/status.dat";
+      object_cache_file="${nagiosState}/objects.cache";
+      temp_file="${nagiosState}/nagios.tmp";
+      lock_file="/run/nagios.lock";
+      state_retention_file="${nagiosState}/retention.dat";
+      query_socket="${nagiosState}/nagios.qh";
+      check_result_path="${nagiosState}";
+      command_file="${nagiosState}/nagios.cmd";
+      cfg_dir="${nagiosObjectDefsDir}";
+      nagios_user="nagios";
+      nagios_group="nagios";
+      illegal_macro_output_chars="`~$&|'\"<>";
+      retain_state_information="1";
+    };
+    lines = mapAttrsToList (key: value: "${key}=${value}") (default // cfg.extraConfig);
+    content = concatStringsSep "\n" lines;
+    file = pkgs.writeText "nagios.cfg" content;
+    validated =  pkgs.runCommand "nagios-checked.cfg" {preferLocalBuild=true;} ''
+      cp ${file} nagios.cfg
+      # nagios checks the existence of /var/lib/nagios, but
+      # it does not exist in the build sandbox, so we fake it
+      mkdir lib
+      lib=$(readlink -f lib)
+      sed -i s@=${nagiosState}@=$lib@ nagios.cfg
+      ${pkgs.nagios}/bin/nagios -v nagios.cfg && cp ${file} $out
+    '';
+    defaultCfgFile = if cfg.validateConfig then validated else file;
+  in
+  if cfg.mainConfigFile == null then defaultCfgFile else cfg.mainConfigFile;
+
+  # Plain configuration for the Nagios web-interface with no
+  # authentication.
+  nagiosCGICfgFile = pkgs.writeText "nagios.cgi.conf"
+    ''
+      main_config_file=${cfg.mainConfigFile}
+      use_authentication=0
+      url_html_path=${urlPath}
+    '';
+
+  extraHttpdConfig =
+    ''
+      ScriptAlias ${urlPath}/cgi-bin ${pkgs.nagios}/sbin
+
+      <Directory "${pkgs.nagios}/sbin">
+        Options ExecCGI
+        Require all granted
+        SetEnv NAGIOS_CGI_CONFIG ${cfg.cgiConfigFile}
+      </Directory>
+
+      Alias ${urlPath} ${pkgs.nagios}/share
+
+      <Directory "${pkgs.nagios}/share">
+        Options None
+        Require all granted
+      </Directory>
+    '';
+
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "nagios" "urlPath" ] "The urlPath option has been removed as it is hard coded to /nagios in the nagios package.")
+  ];
+
+  meta.maintainers = with lib.maintainers; [ symphorien ];
+
+  options = {
+    services.nagios = {
+      enable = mkEnableOption "<link xlink:href='http://www.nagios.org/'>Nagios</link> to monitor your system or network.";
+
+      objectDefs = mkOption {
+        description = "
+          A list of Nagios object configuration files that must define
+          the hosts, host groups, services and contacts for the
+          network that you want Nagios to monitor.
+        ";
+        type = types.listOf types.path;
+        example = literalExpression "[ ./objects.cfg ]";
+      };
+
+      plugins = mkOption {
+        type = types.listOf types.package;
+        default = with pkgs; [ monitoring-plugins ssmtp mailutils ];
+        defaultText = literalExpression "[pkgs.monitoring-plugins pkgs.ssmtp pkgs.mailutils]";
+        description = "
+          Packages to be added to the Nagios <envar>PATH</envar>.
+          Typically used to add plugins, but can be anything.
+        ";
+      };
+
+      mainConfigFile = mkOption {
+        type = types.nullOr types.package;
+        default = null;
+        description = "
+          If non-null, overrides the main configuration file of Nagios.
+        ";
+      };
+
+      extraConfig = mkOption {
+        type = types.attrsOf types.str;
+        example = {
+          debug_level = "-1";
+          debug_file = "/var/log/nagios/debug.log";
+        };
+        default = {};
+        description = "Configuration to add to /etc/nagios.cfg";
+      };
+
+      validateConfig = mkOption {
+        type = types.bool;
+        default = pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform;
+        defaultText = literalExpression "pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform";
+        description = "if true, the syntax of the nagios configuration file is checked at build time";
+      };
+
+      cgiConfigFile = mkOption {
+        type = types.package;
+        default = nagiosCGICfgFile;
+        defaultText = literalExpression "nagiosCGICfgFile";
+        description = "
+          Derivation for the configuration file of Nagios CGI scripts
+          that can be used in web servers for running the Nagios web interface.
+        ";
+      };
+
+      enableWebInterface = mkOption {
+        type = types.bool;
+        default = false;
+        description = "
+          Whether to enable the Nagios web interface.  You should also
+          enable Apache (<option>services.httpd.enable</option>).
+        ";
+      };
+
+      virtualHost = mkOption {
+        type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
+        example = literalExpression ''
+          { hostName = "example.org";
+            adminAddr = "webmaster@example.org";
+            enableSSL = true;
+            sslServerCert = "/var/lib/acme/example.org/full.pem";
+            sslServerKey = "/var/lib/acme/example.org/key.pem";
+          }
+        '';
+        description = ''
+          Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>.
+          See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+        '';
+      };
+    };
+  };
+
+
+  config = mkIf cfg.enable {
+    users.users.nagios = {
+      description = "Nagios user ";
+      uid         = config.ids.uids.nagios;
+      home        = nagiosState;
+      group       = "nagios";
+    };
+
+    users.groups.nagios = { };
+
+    # This isn't needed, it's just so that the user can type "nagiostats
+    # -c /etc/nagios.cfg".
+    environment.etc."nagios.cfg".source = nagiosCfgFile;
+
+    environment.systemPackages = [ pkgs.nagios ];
+    systemd.services.nagios = {
+      description = "Nagios monitoring daemon";
+      path     = [ pkgs.nagios ] ++ cfg.plugins;
+      wantedBy = [ "multi-user.target" ];
+      after    = [ "network.target" ];
+      restartTriggers = [ nagiosCfgFile ];
+
+      serviceConfig = {
+        User = "nagios";
+        Group = "nagios";
+        Restart = "always";
+        RestartSec = 2;
+        LogsDirectory = "nagios";
+        StateDirectory = "nagios";
+        ExecStart = "${pkgs.nagios}/bin/nagios /etc/nagios.cfg";
+      };
+    };
+
+    services.httpd.virtualHosts = optionalAttrs cfg.enableWebInterface {
+      ${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost { extraConfig = extraHttpdConfig; } ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/netdata.nix b/nixos/modules/services/monitoring/netdata.nix
new file mode 100644
index 00000000000..f528d183042
--- /dev/null
+++ b/nixos/modules/services/monitoring/netdata.nix
@@ -0,0 +1,310 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.netdata;
+
+  wrappedPlugins = pkgs.runCommand "wrapped-plugins" { preferLocalBuild = true; } ''
+    mkdir -p $out/libexec/netdata/plugins.d
+    ln -s /run/wrappers/bin/apps.plugin $out/libexec/netdata/plugins.d/apps.plugin
+    ln -s /run/wrappers/bin/cgroup-network $out/libexec/netdata/plugins.d/cgroup-network
+    ln -s /run/wrappers/bin/perf.plugin $out/libexec/netdata/plugins.d/perf.plugin
+    ln -s /run/wrappers/bin/slabinfo.plugin $out/libexec/netdata/plugins.d/slabinfo.plugin
+    ln -s /run/wrappers/bin/freeipmi.plugin $out/libexec/netdata/plugins.d/freeipmi.plugin
+  '';
+
+  plugins = [
+    "${cfg.package}/libexec/netdata/plugins.d"
+    "${wrappedPlugins}/libexec/netdata/plugins.d"
+  ] ++ cfg.extraPluginPaths;
+
+  configDirectory = pkgs.runCommand "netdata-config-d" { } ''
+    mkdir $out
+    ${concatStringsSep "\n" (mapAttrsToList (path: file: ''
+        mkdir -p "$out/$(dirname ${path})"
+        ln -s "${file}" "$out/${path}"
+      '') cfg.configDir)}
+  '';
+
+  localConfig = {
+    global = {
+      "config directory" = "/etc/netdata/conf.d";
+      "plugins directory" = concatStringsSep " " plugins;
+    };
+    web = {
+      "web files owner" = "root";
+      "web files group" = "root";
+    };
+    "plugin:cgroups" = {
+      "script to get cgroup network interfaces" = "${wrappedPlugins}/libexec/netdata/plugins.d/cgroup-network";
+      "use unified cgroups" = "yes";
+    };
+  };
+  mkConfig = generators.toINI {} (recursiveUpdate localConfig cfg.config);
+  configFile = pkgs.writeText "netdata.conf" (if cfg.configText != null then cfg.configText else mkConfig);
+
+  defaultUser = "netdata";
+
+in {
+  options = {
+    services.netdata = {
+      enable = mkEnableOption "netdata";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.netdata;
+        defaultText = literalExpression "pkgs.netdata";
+        description = "Netdata package to use.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "netdata";
+        description = "User account under which netdata runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "netdata";
+        description = "Group under which netdata runs.";
+      };
+
+      configText = mkOption {
+        type = types.nullOr types.lines;
+        description = "Verbatim netdata.conf, cannot be combined with config.";
+        default = null;
+        example = ''
+          [global]
+          debug log = syslog
+          access log = syslog
+          error log = syslog
+        '';
+      };
+
+      python = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether to enable python-based plugins
+          '';
+        };
+        extraPackages = mkOption {
+          type = types.functionTo (types.listOf types.package);
+          default = ps: [];
+          defaultText = literalExpression "ps: []";
+          example = literalExpression ''
+            ps: [
+              ps.psycopg2
+              ps.docker
+              ps.dnspython
+            ]
+          '';
+          description = ''
+            Extra python packages available at runtime
+            to enable additional python plugins.
+          '';
+        };
+      };
+
+      extraPluginPaths = mkOption {
+        type = types.listOf types.path;
+        default = [ ];
+        example = literalExpression ''
+          [ "/path/to/plugins.d" ]
+        '';
+        description = ''
+          Extra paths to add to the netdata global "plugins directory"
+          option.  Useful for when you want to include your own
+          collection scripts.
+          </para><para>
+          Details about writing a custom netdata plugin are available at:
+          <link xlink:href="https://docs.netdata.cloud/collectors/plugins.d/"/>
+          </para><para>
+          Cannot be combined with configText.
+        '';
+      };
+
+      config = mkOption {
+        type = types.attrsOf types.attrs;
+        default = {};
+        description = "netdata.conf configuration as nix attributes. cannot be combined with configText.";
+        example = literalExpression ''
+          global = {
+            "debug log" = "syslog";
+            "access log" = "syslog";
+            "error log" = "syslog";
+          };
+        '';
+      };
+
+      configDir = mkOption {
+        type = types.attrsOf types.path;
+        default = {};
+        description = ''
+          Complete netdata config directory except netdata.conf.
+          The default configuration is merged with changes
+          defined in this option.
+          Each top-level attribute denotes a path in the configuration
+          directory as in environment.etc.
+          Its value is the absolute path and must be readable by netdata.
+          Cannot be combined with configText.
+        '';
+        example = literalExpression ''
+          "health_alarm_notify.conf" = pkgs.writeText "health_alarm_notify.conf" '''
+            sendmail="/path/to/sendmail"
+          ''';
+          "health.d" = "/run/secrets/netdata/health.d";
+        '';
+      };
+
+      enableAnalyticsReporting = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable reporting of anonymous usage statistics to Netdata Inc. via either
+          Google Analytics (in versions prior to 1.29.4), or Netdata Inc.'s
+          self-hosted PostHog (in versions 1.29.4 and later).
+          See: <link xlink:href="https://learn.netdata.cloud/docs/agent/anonymous-statistics"/>
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions =
+      [ { assertion = cfg.config != {} -> cfg.configText == null ;
+          message = "Cannot specify both config and configText";
+        }
+      ];
+
+    environment.etc."netdata/netdata.conf".source = configFile;
+    environment.etc."netdata/conf.d".source = configDirectory;
+
+    systemd.services.netdata = {
+      description = "Real time performance monitoring";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = (with pkgs; [ curl gawk iproute2 which procps ])
+        ++ lib.optional cfg.python.enable (pkgs.python3.withPackages cfg.python.extraPackages)
+        ++ lib.optional config.virtualisation.libvirtd.enable (config.virtualisation.libvirtd.package);
+      environment = {
+        PYTHONPATH = "${cfg.package}/libexec/netdata/python.d/python_modules";
+      } // lib.optionalAttrs (!cfg.enableAnalyticsReporting) {
+        DO_NOT_TRACK = "1";
+      };
+      restartTriggers = [
+        config.environment.etc."netdata/netdata.conf".source
+        config.environment.etc."netdata/conf.d".source
+      ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/netdata -P /run/netdata/netdata.pid -D -c /etc/netdata/netdata.conf";
+        ExecReload = "${pkgs.util-linux}/bin/kill -s HUP -s USR1 -s USR2 $MAINPID";
+        TimeoutStopSec = 60;
+        Restart = "on-failure";
+        # User and group
+        User = cfg.user;
+        Group = cfg.group;
+        # Performance
+        LimitNOFILE = "30000";
+        # Runtime directory and mode
+        RuntimeDirectory = "netdata";
+        RuntimeDirectoryMode = "0750";
+        # State directory and mode
+        StateDirectory = "netdata";
+        StateDirectoryMode = "0750";
+        # Cache directory and mode
+        CacheDirectory = "netdata";
+        CacheDirectoryMode = "0750";
+        # Logs directory and mode
+        LogsDirectory = "netdata";
+        LogsDirectoryMode = "0750";
+        # Configuration directory and mode
+        ConfigurationDirectory = "netdata";
+        ConfigurationDirectoryMode = "0755";
+        # Capabilities
+        CapabilityBoundingSet = [
+          "CAP_DAC_OVERRIDE"      # is required for freeipmi and slabinfo plugins
+          "CAP_DAC_READ_SEARCH"   # is required for apps plugin
+          "CAP_FOWNER"            # is required for freeipmi plugin
+          "CAP_SETPCAP"           # is required for apps, perf and slabinfo plugins
+          "CAP_SYS_ADMIN"         # is required for perf plugin
+          "CAP_SYS_PTRACE"        # is required for apps plugin
+          "CAP_SYS_RESOURCE"      # is required for ebpf plugin
+          "CAP_NET_RAW"           # is required for fping app
+          "CAP_SYS_CHROOT"        # is required for cgroups plugin
+          "CAP_SETUID"            # is required for cgroups and cgroups-network plugins
+        ];
+        # Sandboxing
+        ProtectSystem = "full";
+        ProtectHome = "read-only";
+        PrivateTmp = true;
+        ProtectControlGroups = true;
+        PrivateMounts = true;
+      };
+    };
+
+    systemd.enableCgroupAccounting = true;
+
+    security.wrappers = {
+      "apps.plugin" = {
+        source = "${cfg.package}/libexec/netdata/plugins.d/apps.plugin.org";
+        capabilities = "cap_dac_read_search,cap_sys_ptrace+ep";
+        owner = cfg.user;
+        group = cfg.group;
+        permissions = "u+rx,g+x,o-rwx";
+      };
+
+      "cgroup-network" = {
+        source = "${cfg.package}/libexec/netdata/plugins.d/cgroup-network.org";
+        capabilities = "cap_setuid+ep";
+        owner = cfg.user;
+        group = cfg.group;
+        permissions = "u+rx,g+x,o-rwx";
+      };
+
+      "perf.plugin" = {
+        source = "${cfg.package}/libexec/netdata/plugins.d/perf.plugin.org";
+        capabilities = "cap_sys_admin+ep";
+        owner = cfg.user;
+        group = cfg.group;
+        permissions = "u+rx,g+x,o-rwx";
+      };
+
+      "slabinfo.plugin" = {
+        source = "${cfg.package}/libexec/netdata/plugins.d/slabinfo.plugin.org";
+        capabilities = "cap_dac_override+ep";
+        owner = cfg.user;
+        group = cfg.group;
+        permissions = "u+rx,g+x,o-rwx";
+      };
+
+    } // optionalAttrs (cfg.package.withIpmi) {
+      "freeipmi.plugin" = {
+        source = "${cfg.package}/libexec/netdata/plugins.d/freeipmi.plugin.org";
+        capabilities = "cap_dac_override,cap_fowner+ep";
+        owner = cfg.user;
+        group = cfg.group;
+        permissions = "u+rx,g+x,o-rwx";
+      };
+    };
+
+    security.pam.loginLimits = [
+      { domain = "netdata"; type = "soft"; item = "nofile"; value = "10000"; }
+      { domain = "netdata"; type = "hard"; item = "nofile"; value = "30000"; }
+    ];
+
+    users.users = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} = {
+        group = defaultUser;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == defaultUser) {
+      ${defaultUser} = { };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/monitoring/parsedmarc.md b/nixos/modules/services/monitoring/parsedmarc.md
new file mode 100644
index 00000000000..d93134a4cc7
--- /dev/null
+++ b/nixos/modules/services/monitoring/parsedmarc.md
@@ -0,0 +1,113 @@
+# parsedmarc {#module-services-parsedmarc}
+[parsedmarc](https://domainaware.github.io/parsedmarc/) is a service
+which parses incoming [DMARC](https://dmarc.org/) 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.
+
+## Basic usage {#module-services-parsedmarc-basic-usage}
+A very minimal setup which reads incoming reports from an external
+email address and saves them to a local Elasticsearch instance looks
+like this:
+
+```nix
+services.parsedmarc = {
+  enable = true;
+  settings.imap = {
+    host = "imap.example.com";
+    user = "alice@example.com";
+    password = "/path/to/imap_password_file";
+    watch = true;
+  };
+  provision.geoIp = false; # Not recommended!
+};
+```
+
+Note that GeoIP provisioning is disabled in the example for
+simplicity, but should be turned on for fully functional reports.
+
+## 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
+[services.parsedmarc.provision.localMail.recipientName](options.html#opt-services.parsedmarc.provision.localMail.recipientName). 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
+`monitoring.example.com` and the complete email address that should be
+configured in the domain's dmarc policy is
+`dmarc@monitoring.example.com`.
+
+```nix
+services.parsedmarc = {
+  enable = true;
+  provision = {
+    localMail = {
+      enable = true;
+      hostname = monitoring.example.com;
+    };
+    geoIp = false; # Not recommended!
+  };
+};
+```
+
+## Grafana and 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
+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.
+
+```nix
+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 = "/path/to/license_key_file";
+  };
+};
+
+services.grafana = {
+  enable = true;
+  addr = "0.0.0.0";
+  domain = url;
+  rootUrl = "https://" + url;
+  protocol = "socket";
+  security = {
+    adminUser = "admin";
+    adminPasswordFile = "/path/to/admin_password_file";
+    secretKeyFile = "/path/to/secret_key_file";
+  };
+};
+
+services.nginx = {
+  enable = true;
+  recommendedTlsSettings = true;
+  recommendedOptimisation = true;
+  recommendedGzipSettings = true;
+  recommendedProxySettings = true;
+  upstreams.grafana.servers."unix:/${config.services.grafana.socket}" = {};
+  virtualHosts.${url} = {
+    root = config.services.grafana.staticRootPath;
+    enableACME = true;
+    forceSSL = true;
+    locations."/".tryFiles = "$uri @grafana";
+    locations."@grafana".proxyPass = "http://grafana";
+  };
+};
+users.users.nginx.extraGroups = [ "grafana" ];
+```
diff --git a/nixos/modules/services/monitoring/parsedmarc.nix b/nixos/modules/services/monitoring/parsedmarc.nix
new file mode 100644
index 00000000000..ec71365ba3c
--- /dev/null
+++ b/nixos/modules/services/monitoring/parsedmarc.nix
@@ -0,0 +1,542 @@
+{ config, lib, options, pkgs, ... }:
+
+let
+  cfg = config.services.parsedmarc;
+  opt = options.services.parsedmarc;
+  ini = pkgs.formats.ini {};
+in
+{
+  options.services.parsedmarc = {
+
+    enable = lib.mkEnableOption ''
+      parsedmarc, a DMARC report monitoring service
+    '';
+
+    provision = {
+      localMail = {
+        enable = lib.mkOption {
+          type = lib.types.bool;
+          default = false;
+          description = ''
+            Whether Postfix and Dovecot should be set up to receive
+            mail locally. parsedmarc will be configured to watch the
+            local inbox as the automatically created user specified in
+            <xref linkend="opt-services.parsedmarc.provision.localMail.recipientName" />
+          '';
+        };
+
+        recipientName = lib.mkOption {
+          type = lib.types.str;
+          default = "dmarc";
+          description = ''
+            The DMARC mail recipient name, i.e. the name part of the
+            email address which receives DMARC reports.
+
+            A local user with this name will be set up and assigned a
+            randomized password on service start.
+          '';
+        };
+
+        hostname = lib.mkOption {
+          type = lib.types.str;
+          default = config.networking.fqdn;
+          defaultText = lib.literalExpression "config.networking.fqdn";
+          example = "monitoring.example.com";
+          description = ''
+            The hostname to use when configuring Postfix.
+
+            Should correspond to the host's fully qualified domain
+            name and the domain part of the email address which
+            receives DMARC reports. You also have to set up an MX record
+            pointing to this domain name.
+          '';
+        };
+      };
+
+      geoIp = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = ''
+          Whether to enable and configure the <link
+          linkend="opt-services.geoipupdate.enable">geoipupdate</link>
+          service to automatically fetch GeoIP databases. Not crucial,
+          but recommended for full functionality.
+
+          To finish the setup, you need to manually set the <xref
+          linkend="opt-services.geoipupdate.settings.AccountID" /> and
+          <xref linkend="opt-services.geoipupdate.settings.LicenseKey" />
+          options.
+        '';
+      };
+
+      elasticsearch = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = ''
+          Whether to set up and use a local instance of Elasticsearch.
+        '';
+      };
+
+      grafana = {
+        datasource = lib.mkOption {
+          type = lib.types.bool;
+          default = cfg.provision.elasticsearch && config.services.grafana.enable;
+          defaultText = lib.literalExpression ''
+            config.${opt.provision.elasticsearch} && config.${options.services.grafana.enable}
+          '';
+          apply = x: x && cfg.provision.elasticsearch;
+          description = ''
+            Whether the automatically provisioned Elasticsearch
+            instance should be added as a grafana datasource. Has no
+            effect unless
+            <xref linkend="opt-services.parsedmarc.provision.elasticsearch" />
+            is also enabled.
+          '';
+        };
+
+        dashboard = lib.mkOption {
+          type = lib.types.bool;
+          default = config.services.grafana.enable;
+          defaultText = lib.literalExpression "config.services.grafana.enable";
+          description = ''
+            Whether the official parsedmarc grafana dashboard should
+            be provisioned to the local grafana instance.
+          '';
+        };
+      };
+    };
+
+    settings = lib.mkOption {
+      description = ''
+        Configuration parameters to set in
+        <filename>parsedmarc.ini</filename>. For a full list of
+        available parameters, see
+        <link xlink:href="https://domainaware.github.io/parsedmarc/#configuration-file" />.
+      '';
+
+      type = lib.types.submodule {
+        freeformType = ini.type;
+
+        options = {
+          general = {
+            save_aggregate = lib.mkOption {
+              type = lib.types.bool;
+              default = true;
+              description = ''
+                Save aggregate report data to Elasticsearch and/or Splunk.
+              '';
+            };
+
+            save_forensic = lib.mkOption {
+              type = lib.types.bool;
+              default = true;
+              description = ''
+                Save forensic report data to Elasticsearch and/or Splunk.
+              '';
+            };
+          };
+
+          imap = {
+            host = lib.mkOption {
+              type = lib.types.str;
+              default = "localhost";
+              description = ''
+                The IMAP server hostname or IP address.
+              '';
+            };
+
+            port = lib.mkOption {
+              type = lib.types.port;
+              default = 993;
+              description = ''
+                The IMAP server port.
+              '';
+            };
+
+            ssl = lib.mkOption {
+              type = lib.types.bool;
+              default = true;
+              description = ''
+                Use an encrypted SSL/TLS connection.
+              '';
+            };
+
+            user = lib.mkOption {
+              type = with lib.types; nullOr str;
+              default = null;
+              description = ''
+                The IMAP server username.
+              '';
+            };
+
+            password = lib.mkOption {
+              type = with lib.types; nullOr path;
+              default = null;
+              description = ''
+                The path to a file containing the IMAP server password.
+              '';
+            };
+
+            watch = lib.mkOption {
+              type = lib.types.bool;
+              default = true;
+              description = ''
+                Use the IMAP IDLE command to process messages as they arrive.
+              '';
+            };
+
+            delete = lib.mkOption {
+              type = lib.types.bool;
+              default = false;
+              description = ''
+                Delete messages after processing them, instead of archiving them.
+              '';
+            };
+          };
+
+          smtp = {
+            host = lib.mkOption {
+              type = with lib.types; nullOr str;
+              default = null;
+              description = ''
+                The SMTP server hostname or IP address.
+              '';
+            };
+
+            port = lib.mkOption {
+              type = with lib.types; nullOr port;
+              default = null;
+              description = ''
+                The SMTP server port.
+              '';
+            };
+
+            ssl = lib.mkOption {
+              type = with lib.types; nullOr bool;
+              default = null;
+              description = ''
+                Use an encrypted SSL/TLS connection.
+              '';
+            };
+
+            user = lib.mkOption {
+              type = with lib.types; nullOr str;
+              default = null;
+              description = ''
+                The SMTP server username.
+              '';
+            };
+
+            password = lib.mkOption {
+              type = with lib.types; nullOr path;
+              default = null;
+              description = ''
+                The path to a file containing the SMTP server password.
+              '';
+            };
+
+            from = lib.mkOption {
+              type = with lib.types; nullOr str;
+              default = null;
+              description = ''
+                The <literal>From</literal> address to use for the
+                outgoing mail.
+              '';
+            };
+
+            to = lib.mkOption {
+              type = with lib.types; nullOr (listOf str);
+              default = null;
+              description = ''
+                The addresses to send outgoing mail to.
+              '';
+            };
+          };
+
+          elasticsearch = {
+            hosts = lib.mkOption {
+              default = [];
+              type = with lib.types; listOf str;
+              apply = x: if x == [] then null else lib.concatStringsSep "," x;
+              description = ''
+                A list of Elasticsearch hosts to push parsed reports
+                to.
+              '';
+            };
+
+            user = lib.mkOption {
+              type = with lib.types; nullOr str;
+              default = null;
+              description = ''
+                Username to use when connecting to Elasticsearch, if
+                required.
+              '';
+            };
+
+            password = lib.mkOption {
+              type = with lib.types; nullOr path;
+              default = null;
+              description = ''
+                The path to a file containing the password to use when
+                connecting to Elasticsearch, if required.
+              '';
+            };
+
+            ssl = lib.mkOption {
+              type = lib.types.bool;
+              default = false;
+              description = ''
+                Whether to use an encrypted SSL/TLS connection.
+              '';
+            };
+
+            cert_path = lib.mkOption {
+              type = lib.types.path;
+              default = "/etc/ssl/certs/ca-certificates.crt";
+              description = ''
+                The path to a TLS certificate bundle used to verify
+                the server's certificate.
+              '';
+            };
+          };
+
+          kafka = {
+            hosts = lib.mkOption {
+              default = [];
+              type = with lib.types; listOf str;
+              apply = x: if x == [] then null else lib.concatStringsSep "," x;
+              description = ''
+                A list of Apache Kafka hosts to publish parsed reports
+                to.
+              '';
+            };
+
+            user = lib.mkOption {
+              type = with lib.types; nullOr str;
+              default = null;
+              description = ''
+                Username to use when connecting to Kafka, if
+                required.
+              '';
+            };
+
+            password = lib.mkOption {
+              type = with lib.types; nullOr path;
+              default = null;
+              description = ''
+                The path to a file containing the password to use when
+                connecting to Kafka, if required.
+              '';
+            };
+
+            ssl = lib.mkOption {
+              type = with lib.types; nullOr bool;
+              default = null;
+              description = ''
+                Whether to use an encrypted SSL/TLS connection.
+              '';
+            };
+
+            aggregate_topic = lib.mkOption {
+              type = with lib.types; nullOr str;
+              default = null;
+              example = "aggregate";
+              description = ''
+                The Kafka topic to publish aggregate reports on.
+              '';
+            };
+
+            forensic_topic = lib.mkOption {
+              type = with lib.types; nullOr str;
+              default = null;
+              example = "forensic";
+              description = ''
+                The Kafka topic to publish forensic reports on.
+              '';
+            };
+          };
+
+        };
+
+      };
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    services.elasticsearch.enable = lib.mkDefault cfg.provision.elasticsearch;
+
+    services.geoipupdate = lib.mkIf cfg.provision.geoIp {
+      enable = true;
+      settings = {
+        EditionIDs = [
+          "GeoLite2-ASN"
+          "GeoLite2-City"
+          "GeoLite2-Country"
+        ];
+        DatabaseDirectory = "/var/lib/GeoIP";
+      };
+    };
+
+    services.dovecot2 = lib.mkIf cfg.provision.localMail.enable {
+      enable = true;
+      protocols = [ "imap" ];
+    };
+
+    services.postfix = lib.mkIf cfg.provision.localMail.enable {
+      enable = true;
+      origin = cfg.provision.localMail.hostname;
+      config = {
+        myhostname = cfg.provision.localMail.hostname;
+        mydestination = cfg.provision.localMail.hostname;
+      };
+    };
+
+    services.grafana = {
+      declarativePlugins = with pkgs.grafanaPlugins;
+        lib.mkIf cfg.provision.grafana.dashboard [
+          grafana-worldmap-panel
+          grafana-piechart-panel
+        ];
+
+      provision = {
+        enable = cfg.provision.grafana.datasource || cfg.provision.grafana.dashboard;
+        datasources =
+          let
+            pkgVer = lib.getVersion config.services.elasticsearch.package;
+            esVersion =
+              if lib.versionOlder pkgVer "7" then
+                "60"
+              else if lib.versionOlder pkgVer "8" then
+                "70"
+              else
+                throw "When provisioning parsedmarc grafana datasources: unknown Elasticsearch version.";
+          in
+            lib.mkIf cfg.provision.grafana.datasource [
+              {
+                name = "dmarc-ag";
+                type = "elasticsearch";
+                access = "proxy";
+                url = "localhost:9200";
+                jsonData = {
+                  timeField = "date_range";
+                  inherit esVersion;
+                };
+              }
+              {
+                name = "dmarc-fo";
+                type = "elasticsearch";
+                access = "proxy";
+                url = "localhost:9200";
+                jsonData = {
+                  timeField = "date_range";
+                  inherit esVersion;
+                };
+              }
+            ];
+        dashboards = lib.mkIf cfg.provision.grafana.dashboard [{
+          name = "parsedmarc";
+          options.path = "${pkgs.python3Packages.parsedmarc.dashboard}";
+        }];
+      };
+    };
+
+    services.parsedmarc.settings = lib.mkMerge [
+      (lib.mkIf cfg.provision.elasticsearch {
+        elasticsearch = {
+          hosts = [ "localhost:9200" ];
+          ssl = false;
+        };
+      })
+      (lib.mkIf cfg.provision.localMail.enable {
+        imap = {
+          host = "localhost";
+          port = 143;
+          ssl = false;
+          user = cfg.provision.localMail.recipientName;
+          password = "${pkgs.writeText "imap-password" "@imap-password@"}";
+          watch = true;
+        };
+      })
+    ];
+
+    systemd.services.parsedmarc =
+      let
+        # Remove any empty attributes from the config, i.e. empty
+        # lists, empty attrsets and null. This makes it possible to
+        # list interesting options in `settings` without them always
+        # ending up in the resulting config.
+        filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! builtins.elem v [ null [] {} ])) cfg.settings;
+        parsedmarcConfig = ini.generate "parsedmarc.ini" filteredConfig;
+        mkSecretReplacement = file:
+          lib.optionalString (file != null) ''
+            replace-secret '${file}' '${file}' /run/parsedmarc/parsedmarc.ini
+          '';
+      in
+        {
+          wantedBy = [ "multi-user.target" ];
+          after = [ "postfix.service" "dovecot2.service" "elasticsearch.service" ];
+          path = with pkgs; [ replace-secret openssl shadow ];
+          serviceConfig = {
+            ExecStartPre = let
+              startPreFullPrivileges = ''
+                set -o errexit -o pipefail -o nounset -o errtrace
+                shopt -s inherit_errexit
+
+                umask u=rwx,g=,o=
+                cp ${parsedmarcConfig} /run/parsedmarc/parsedmarc.ini
+                chown parsedmarc:parsedmarc /run/parsedmarc/parsedmarc.ini
+                ${mkSecretReplacement cfg.settings.smtp.password}
+                ${mkSecretReplacement cfg.settings.imap.password}
+                ${mkSecretReplacement cfg.settings.elasticsearch.password}
+                ${mkSecretReplacement cfg.settings.kafka.password}
+              '' + lib.optionalString cfg.provision.localMail.enable ''
+                openssl rand -hex 64 >/run/parsedmarc/dmarc_user_passwd
+                replace-secret '@imap-password@' '/run/parsedmarc/dmarc_user_passwd' /run/parsedmarc/parsedmarc.ini
+                echo "Setting new randomized password for user '${cfg.provision.localMail.recipientName}'."
+                cat <(echo -n "${cfg.provision.localMail.recipientName}:") /run/parsedmarc/dmarc_user_passwd | chpasswd
+              '';
+            in
+              "+${pkgs.writeShellScript "parsedmarc-start-pre-full-privileges" startPreFullPrivileges}";
+            Type = "simple";
+            User = "parsedmarc";
+            Group = "parsedmarc";
+            DynamicUser = true;
+            RuntimeDirectory = "parsedmarc";
+            RuntimeDirectoryMode = 0700;
+            CapabilityBoundingSet = "";
+            PrivateDevices = true;
+            PrivateMounts = true;
+            PrivateUsers = true;
+            ProtectClock = true;
+            ProtectControlGroups = true;
+            ProtectHome = true;
+            ProtectHostname = true;
+            ProtectKernelLogs = true;
+            ProtectKernelModules = true;
+            ProtectKernelTunables = true;
+            ProtectProc = "invisible";
+            ProcSubset = "pid";
+            SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+            RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+            RestrictRealtime = true;
+            RestrictNamespaces = true;
+            MemoryDenyWriteExecute = true;
+            LockPersonality = true;
+            SystemCallArchitectures = "native";
+            ExecStart = "${pkgs.python3Packages.parsedmarc}/bin/parsedmarc -c /run/parsedmarc/parsedmarc.ini";
+          };
+        };
+
+    users.users.${cfg.provision.localMail.recipientName} = lib.mkIf cfg.provision.localMail.enable {
+      isNormalUser = true;
+      description = "DMARC mail recipient";
+    };
+  };
+
+  # 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.maintainers = [ lib.maintainers.talyz ];
+}
diff --git a/nixos/modules/services/monitoring/parsedmarc.xml b/nixos/modules/services/monitoring/parsedmarc.xml
new file mode 100644
index 00000000000..7167b52d035
--- /dev/null
+++ b/nixos/modules/services/monitoring/parsedmarc.xml
@@ -0,0 +1,125 @@
+<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.nix b/nixos/modules/services/monitoring/prometheus/alertmanager.nix
new file mode 100644
index 00000000000..1f396634ae0
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/alertmanager.nix
@@ -0,0 +1,187 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.alertmanager;
+  mkConfigFile = pkgs.writeText "alertmanager.yml" (builtins.toJSON cfg.configuration);
+
+  checkedConfig = file: pkgs.runCommand "checked-config" { buildInputs = [ cfg.package ]; } ''
+    ln -s ${file} $out
+    amtool check-config $out
+  '';
+
+  alertmanagerYml = let
+    yml = if cfg.configText != null then
+        pkgs.writeText "alertmanager.yml" cfg.configText
+        else mkConfigFile;
+    in checkedConfig yml;
+
+  cmdlineArgs = cfg.extraFlags ++ [
+    "--config.file /tmp/alert-manager-substituted.yaml"
+    "--web.listen-address ${cfg.listenAddress}:${toString cfg.port}"
+    "--log.level ${cfg.logLevel}"
+    "--storage.path /var/lib/alertmanager"
+    (toString (map (peer: "--cluster.peer ${peer}:9094") cfg.clusterPeers))
+    ] ++ (optional (cfg.webExternalUrl != null)
+      "--web.external-url ${cfg.webExternalUrl}"
+    ) ++ (optional (cfg.logFormat != null)
+      "--log.format ${cfg.logFormat}"
+  );
+in {
+  imports = [
+    (mkRemovedOptionModule [ "services" "prometheus" "alertmanager" "user" ] "The alertmanager service is now using systemd's DynamicUser mechanism which obviates a user setting.")
+    (mkRemovedOptionModule [ "services" "prometheus" "alertmanager" "group" ] "The alertmanager service is now using systemd's DynamicUser mechanism which obviates a group setting.")
+    (mkRemovedOptionModule [ "services" "prometheus" "alertmanagerURL" ] ''
+      Due to incompatibility, the alertmanagerURL option has been removed,
+      please use 'services.prometheus2.alertmanagers' instead.
+    '')
+  ];
+
+  options = {
+    services.prometheus.alertmanager = {
+      enable = mkEnableOption "Prometheus Alertmanager";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.prometheus-alertmanager;
+        defaultText = literalExpression "pkgs.alertmanager";
+        description = ''
+          Package that should be used for alertmanager.
+        '';
+      };
+
+      configuration = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        description = ''
+          Alertmanager configuration as nix attribute set.
+        '';
+      };
+
+      configText = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = ''
+          Alertmanager configuration as YAML text. If non-null, this option
+          defines the text that is written to alertmanager.yml. If null, the
+          contents of alertmanager.yml is generated from the structured config
+          options.
+        '';
+      };
+
+      logFormat = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          If set use a syslog logger or JSON logging.
+        '';
+      };
+
+      logLevel = mkOption {
+        type = types.enum ["debug" "info" "warn" "error" "fatal"];
+        default = "warn";
+        description = ''
+          Only log messages with the given severity or above.
+        '';
+      };
+
+      webExternalUrl = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The URL under which Alertmanager is externally reachable (for example, if Alertmanager is served via a reverse proxy).
+          Used for generating relative and absolute links back to Alertmanager itself.
+          If the URL has a path portion, it will be used to prefix all HTTP endoints served by Alertmanager.
+          If omitted, relevant URL components will be derived automatically.
+        '';
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Address to listen on for the web interface and API. Empty string will listen on all interfaces.
+          "localhost" will listen on 127.0.0.1 (but not ::1).
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 9093;
+        description = ''
+          Port to listen on for the web interface and API.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open port in firewall for incoming connections.
+        '';
+      };
+
+      clusterPeers = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Initial peers for HA cluster.
+        '';
+      };
+
+      extraFlags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Extra commandline options when launching the Alertmanager.
+        '';
+      };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/root/alertmanager.env";
+        description = ''
+          File to load as environment file. Environment variables
+          from this file will be interpolated into the config file
+          using envsubst with this syntax:
+          <literal>$ENVIRONMENT ''${VARIABLE}</literal>
+        '';
+      };
+    };
+  };
+
+  config = mkMerge [
+    (mkIf cfg.enable {
+      assertions = singleton {
+        assertion = cfg.configuration != null || cfg.configText != null;
+        message = "Can not enable alertmanager without a configuration. "
+         + "Set either the `configuration` or `configText` attribute.";
+      };
+    })
+    (mkIf cfg.enable {
+      networking.firewall.allowedTCPPorts = optional cfg.openFirewall cfg.port;
+
+      systemd.services.alertmanager = {
+        wantedBy = [ "multi-user.target" ];
+        after    = [ "network-online.target" ];
+        preStart = ''
+           ${lib.getBin pkgs.envsubst}/bin/envsubst -o "/tmp/alert-manager-substituted.yaml" \
+                                                    -i "${alertmanagerYml}"
+        '';
+        serviceConfig = {
+          Restart  = "always";
+          StateDirectory = "alertmanager";
+          DynamicUser = true; # implies PrivateTmp
+          EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
+          WorkingDirectory = "/tmp";
+          ExecStart = "${cfg.package}/bin/alertmanager" +
+            optionalString (length cmdlineArgs != 0) (" \\\n  " +
+              concatStringsSep " \\\n  " cmdlineArgs);
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        };
+      };
+    })
+  ];
+}
diff --git a/nixos/modules/services/monitoring/prometheus/default.nix b/nixos/modules/services/monitoring/prometheus/default.nix
new file mode 100644
index 00000000000..f563861b61c
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/default.nix
@@ -0,0 +1,1835 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus;
+
+  workingDir = "/var/lib/" + cfg.stateDir;
+
+  prometheusYmlOut = "${workingDir}/prometheus-substituted.yaml";
+
+  triggerReload = pkgs.writeShellScriptBin "trigger-reload-prometheus" ''
+    PATH="${makeBinPath (with pkgs; [ systemd ])}"
+    if systemctl -q is-active prometheus.service; then
+      systemctl reload prometheus.service
+    fi
+  '';
+
+  reload = pkgs.writeShellScriptBin "reload-prometheus" ''
+    PATH="${makeBinPath (with pkgs; [ systemd coreutils gnugrep ])}"
+    cursor=$(journalctl --show-cursor -n0 | grep -oP "cursor: \K.*")
+    kill -HUP $MAINPID
+    journalctl -u prometheus.service --after-cursor="$cursor" -f \
+      | grep -m 1 "Completed loading of configuration file" > /dev/null
+  '';
+
+  # a wrapper that verifies that the configuration is valid
+  promtoolCheck = what: name: file:
+    if cfg.checkConfig then
+      pkgs.runCommandLocal
+        "${name}-${replaceStrings [" "] [""] what}-checked"
+        { buildInputs = [ cfg.package ]; } ''
+        ln -s ${file} $out
+        promtool ${what} $out
+      '' else file;
+
+  # Pretty-print JSON to a file
+  writePrettyJSON = name: x:
+    pkgs.runCommandLocal name { } ''
+      echo '${builtins.toJSON x}' | ${pkgs.jq}/bin/jq . > $out
+    '';
+
+  generatedPrometheusYml = writePrettyJSON "prometheus.yml" promConfig;
+
+  # This becomes the main config file for Prometheus
+  promConfig = {
+    global = filterValidPrometheus cfg.globalConfig;
+    rule_files = map (promtoolCheck "check rules" "rules") (cfg.ruleFiles ++ [
+      (pkgs.writeText "prometheus.rules" (concatStringsSep "\n" cfg.rules))
+    ]);
+    scrape_configs = filterValidPrometheus cfg.scrapeConfigs;
+    remote_write = filterValidPrometheus cfg.remoteWrite;
+    remote_read = filterValidPrometheus cfg.remoteRead;
+    alerting = {
+      inherit (cfg) alertmanagers;
+    };
+  };
+
+  prometheusYml =
+    let
+      yml =
+        if cfg.configText != null then
+          pkgs.writeText "prometheus.yml" cfg.configText
+        else generatedPrometheusYml;
+    in
+    promtoolCheck "check config" "prometheus.yml" yml;
+
+  cmdlineArgs = cfg.extraFlags ++ [
+    "--storage.tsdb.path=${workingDir}/data/"
+    "--config.file=${
+      if cfg.enableReload
+      then "/etc/prometheus/prometheus.yaml"
+      else prometheusYml
+    }"
+    "--web.listen-address=${cfg.listenAddress}:${builtins.toString cfg.port}"
+    "--alertmanager.notification-queue-capacity=${toString cfg.alertmanagerNotificationQueueCapacity}"
+    "--alertmanager.timeout=${toString cfg.alertmanagerTimeout}s"
+  ] ++ optional (cfg.webExternalUrl != null) "--web.external-url=${cfg.webExternalUrl}"
+    ++ optional (cfg.retentionTime != null) "--storage.tsdb.retention.time=${cfg.retentionTime}";
+
+  filterValidPrometheus = filterAttrsListRecursive (n: v: !(n == "_module" || v == null));
+  filterAttrsListRecursive = pred: x:
+    if isAttrs x then
+      listToAttrs
+        (
+          concatMap
+            (name:
+              let v = x.${name}; in
+              if pred name v then [
+                (nameValuePair name (filterAttrsListRecursive pred v))
+              ] else [ ]
+            )
+            (attrNames x)
+        )
+    else if isList x then
+      map (filterAttrsListRecursive pred) x
+    else x;
+
+  #
+  # Config types: helper functions
+  #
+
+  mkDefOpt = type: defaultStr: description: mkOpt type (description + ''
+
+    Defaults to <literal>${defaultStr}</literal> in prometheus
+    when set to <literal>null</literal>.
+  '');
+
+  mkOpt = type: description: mkOption {
+    type = types.nullOr type;
+    default = null;
+    inherit description;
+  };
+
+  mkSdConfigModule = extraOptions: types.submodule {
+    options = {
+      basic_auth = mkOpt promTypes.basic_auth ''
+        Optional HTTP basic authentication information.
+      '';
+
+      authorization = mkOpt
+        (types.submodule {
+          options = {
+            type = mkDefOpt types.str "Bearer" ''
+              Sets the authentication type.
+            '';
+
+            credentials = mkOpt types.str ''
+              Sets the credentials. It is mutually exclusive with `credentials_file`.
+            '';
+
+            credentials_file = mkOpt types.str ''
+              Sets the credentials to the credentials read from the configured file.
+              It is mutually exclusive with `credentials`.
+            '';
+          };
+        }) ''
+        Optional `Authorization` header configuration.
+      '';
+
+      oauth2 = mkOpt promtypes.oauth2 ''
+        Optional OAuth 2.0 configuration.
+        Cannot be used at the same time as basic_auth or authorization.
+      '';
+
+      proxy_url = mkOpt types.str ''
+        Optional proxy URL.
+      '';
+
+      follow_redirects = mkDefOpt types.bool "true" ''
+        Configure whether HTTP requests follow HTTP 3xx redirects.
+      '';
+
+      tls_config = mkOpt promTypes.tls_config ''
+        TLS configuration.
+      '';
+    } // extraOptions;
+  };
+
+  #
+  # Config types: general
+  #
+
+  promTypes.globalConfig = types.submodule {
+    options = {
+      scrape_interval = mkDefOpt types.str "1m" ''
+        How frequently to scrape targets by default.
+      '';
+
+      scrape_timeout = mkDefOpt types.str "10s" ''
+        How long until a scrape request times out.
+      '';
+
+      evaluation_interval = mkDefOpt types.str "1m" ''
+        How frequently to evaluate rules by default.
+      '';
+
+      external_labels = mkOpt (types.attrsOf types.str) ''
+        The labels to add to any time series or alerts when
+        communicating with external systems (federation, remote
+        storage, Alertmanager).
+      '';
+    };
+  };
+
+  promTypes.basic_auth = types.submodule {
+    options = {
+      username = mkOption {
+        type = types.str;
+        description = ''
+          HTTP username
+        '';
+      };
+      password = mkOpt types.str "HTTP password";
+      password_file = mkOpt types.str "HTTP password file";
+    };
+  };
+
+  promTypes.tls_config = types.submodule {
+    options = {
+      ca_file = mkOpt types.str ''
+        CA certificate to validate API server certificate with.
+      '';
+
+      cert_file = mkOpt types.str ''
+        Certificate file for client cert authentication to the server.
+      '';
+
+      key_file = mkOpt types.str ''
+        Key file for client cert authentication to the server.
+      '';
+
+      server_name = mkOpt types.str ''
+        ServerName extension to indicate the name of the server.
+        http://tools.ietf.org/html/rfc4366#section-3.1
+      '';
+
+      insecure_skip_verify = mkOpt types.bool ''
+        Disable validation of the server certificate.
+      '';
+    };
+  };
+
+  promtypes.oauth2 = types.submodule {
+    options = {
+      client_id = mkOpt types.str ''
+        OAuth client ID.
+      '';
+
+      client_secret = mkOpt types.str ''
+        OAuth client secret.
+      '';
+
+      client_secret_file = mkOpt types.str ''
+        Read the client secret from a file. It is mutually exclusive with `client_secret`.
+      '';
+
+      scopes = mkOpt (types.listOf types.str) ''
+        Scopes for the token request.
+      '';
+
+      token_url = mkOpt types.str ''
+        The URL to fetch the token from.
+      '';
+
+      endpoint_params = mkOpt (types.attrsOf types.str) ''
+        Optional parameters to append to the token URL.
+      '';
+    };
+  };
+
+  promTypes.scrape_config = types.submodule {
+    options = {
+      authorization = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        description = ''
+          Sets the `Authorization` header on every scrape request with the configured credentials.
+        '';
+      };
+      job_name = mkOption {
+        type = types.str;
+        description = ''
+          The job name assigned to scraped metrics by default.
+        '';
+      };
+      scrape_interval = mkOpt types.str ''
+        How frequently to scrape targets from this job. Defaults to the
+        globally configured default.
+      '';
+
+      scrape_timeout = mkOpt types.str ''
+        Per-target timeout when scraping this job. Defaults to the
+        globally configured default.
+      '';
+
+      metrics_path = mkDefOpt types.str "/metrics" ''
+        The HTTP resource path on which to fetch metrics from targets.
+      '';
+
+      honor_labels = mkDefOpt types.bool "false" ''
+        Controls how Prometheus handles conflicts between labels
+        that are already present in scraped data and labels that
+        Prometheus would attach server-side ("job" and "instance"
+        labels, manually configured target labels, and labels
+        generated by service discovery implementations).
+
+        If honor_labels is set to "true", label conflicts are
+        resolved by keeping label values from the scraped data and
+        ignoring the conflicting server-side labels.
+
+        If honor_labels is set to "false", label conflicts are
+        resolved by renaming conflicting labels in the scraped data
+        to "exported_&lt;original-label&gt;" (for example
+        "exported_instance", "exported_job") and then attaching
+        server-side labels. This is useful for use cases such as
+        federation, where all labels specified in the target should
+        be preserved.
+      '';
+
+      honor_timestamps = mkDefOpt types.bool "true" ''
+        honor_timestamps controls whether Prometheus respects the timestamps present
+        in scraped data.
+
+        If honor_timestamps is set to <literal>true</literal>, the timestamps of the metrics exposed
+        by the target will be used.
+
+        If honor_timestamps is set to <literal>false</literal>, the timestamps of the metrics exposed
+        by the target will be ignored.
+      '';
+
+      scheme = mkDefOpt (types.enum [ "http" "https" ]) "http" ''
+        The URL scheme with which to fetch metrics from targets.
+      '';
+
+      params = mkOpt (types.attrsOf (types.listOf types.str)) ''
+        Optional HTTP URL parameters.
+      '';
+
+      basic_auth = mkOpt promTypes.basic_auth ''
+        Sets the `Authorization` header on every scrape request with the
+        configured username and password.
+        password and password_file are mutually exclusive.
+      '';
+
+      bearer_token = mkOpt types.str ''
+        Sets the `Authorization` header on every scrape request with
+        the configured bearer token. It is mutually exclusive with
+        <option>bearer_token_file</option>.
+      '';
+
+      bearer_token_file = mkOpt types.str ''
+        Sets the `Authorization` header on every scrape request with
+        the bearer token read from the configured file. It is mutually
+        exclusive with <option>bearer_token</option>.
+      '';
+
+      tls_config = mkOpt promTypes.tls_config ''
+        Configures the scrape request's TLS settings.
+      '';
+
+      proxy_url = mkOpt types.str ''
+        Optional proxy URL.
+      '';
+
+      azure_sd_configs = mkOpt (types.listOf promTypes.azure_sd_config) ''
+        List of Azure service discovery configurations.
+      '';
+
+      consul_sd_configs = mkOpt (types.listOf promTypes.consul_sd_config) ''
+        List of Consul service discovery configurations.
+      '';
+
+      digitalocean_sd_configs = mkOpt (types.listOf promTypes.digitalocean_sd_config) ''
+        List of DigitalOcean service discovery configurations.
+      '';
+
+      docker_sd_configs = mkOpt (types.listOf promTypes.docker_sd_config) ''
+        List of Docker service discovery configurations.
+      '';
+
+      dockerswarm_sd_configs = mkOpt (types.listOf promTypes.dockerswarm_sd_config) ''
+        List of Docker Swarm service discovery configurations.
+      '';
+
+      dns_sd_configs = mkOpt (types.listOf promTypes.dns_sd_config) ''
+        List of DNS service discovery configurations.
+      '';
+
+      ec2_sd_configs = mkOpt (types.listOf promTypes.ec2_sd_config) ''
+        List of EC2 service discovery configurations.
+      '';
+
+      eureka_sd_configs = mkOpt (types.listOf promTypes.eureka_sd_config) ''
+        List of Eureka service discovery configurations.
+      '';
+
+      file_sd_configs = mkOpt (types.listOf promTypes.file_sd_config) ''
+        List of file service discovery configurations.
+      '';
+
+      gce_sd_configs = mkOpt (types.listOf promTypes.gce_sd_config) ''
+        List of Google Compute Engine service discovery configurations.
+
+        See <link
+        xlink:href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#gce_sd_config">the
+        relevant Prometheus configuration docs</link> for more detail.
+      '';
+
+      hetzner_sd_configs = mkOpt (types.listOf promTypes.hetzner_sd_config) ''
+        List of Hetzner service discovery configurations.
+      '';
+
+      http_sd_configs = mkOpt (types.listOf promTypes.http_sd_config) ''
+        List of HTTP service discovery configurations.
+      '';
+
+      kubernetes_sd_configs = mkOpt (types.listOf promTypes.kubernetes_sd_config) ''
+        List of Kubernetes service discovery configurations.
+      '';
+
+      kuma_sd_configs = mkOpt (types.listOf promTypes.kuma_sd_config) ''
+        List of Kuma service discovery configurations.
+      '';
+
+      lightsail_sd_configs = mkOpt (types.listOf promTypes.lightsail_sd_config) ''
+        List of Lightsail service discovery configurations.
+      '';
+
+      linode_sd_configs = mkOpt (types.listOf promTypes.linode_sd_config) ''
+        List of Linode service discovery configurations.
+      '';
+
+      marathon_sd_configs = mkOpt (types.listOf promTypes.marathon_sd_config) ''
+        List of Marathon service discovery configurations.
+      '';
+
+      nerve_sd_configs = mkOpt (types.listOf promTypes.nerve_sd_config) ''
+        List of AirBnB's Nerve service discovery configurations.
+      '';
+
+      openstack_sd_configs = mkOpt (types.listOf promTypes.openstack_sd_config) ''
+        List of OpenStack service discovery configurations.
+      '';
+
+      puppetdb_sd_configs = mkOpt (types.listOf promTypes.puppetdb_sd_config) ''
+        List of PuppetDB service discovery configurations.
+      '';
+
+      scaleway_sd_configs = mkOpt (types.listOf promTypes.scaleway_sd_config) ''
+        List of Scaleway service discovery configurations.
+      '';
+
+      serverset_sd_configs = mkOpt (types.listOf promTypes.serverset_sd_config) ''
+        List of Zookeeper Serverset service discovery configurations.
+      '';
+
+      triton_sd_configs = mkOpt (types.listOf promTypes.triton_sd_config) ''
+        List of Triton Serverset service discovery configurations.
+      '';
+
+      uyuni_sd_configs = mkOpt (types.listOf promTypes.uyuni_sd_config) ''
+        List of Uyuni Serverset service discovery configurations.
+      '';
+
+      static_configs = mkOpt (types.listOf promTypes.static_config) ''
+        List of labeled target groups for this job.
+      '';
+
+      relabel_configs = mkOpt (types.listOf promTypes.relabel_config) ''
+        List of relabel configurations.
+      '';
+
+      metric_relabel_configs = mkOpt (types.listOf promTypes.relabel_config) ''
+        List of metric relabel configurations.
+      '';
+
+      body_size_limit = mkDefOpt types.str "0" ''
+        An uncompressed response body larger than this many bytes will cause the
+        scrape to fail. 0 means no limit. Example: 100MB.
+        This is an experimental feature, this behaviour could
+        change or be removed in the future.
+      '';
+
+      sample_limit = mkDefOpt types.int "0" ''
+        Per-scrape limit on number of scraped samples that will be accepted.
+        If more than this number of samples are present after metric relabelling
+        the entire scrape will be treated as failed. 0 means no limit.
+      '';
+
+      label_limit = mkDefOpt types.int "0" ''
+        Per-scrape limit on number of labels that will be accepted for a sample. If
+        more than this number of labels are present post metric-relabeling, the
+        entire scrape will be treated as failed. 0 means no limit.
+      '';
+
+      label_name_length_limit = mkDefOpt types.int "0" ''
+        Per-scrape limit on length of labels name that will be accepted for a sample.
+        If a label name is longer than this number post metric-relabeling, the entire
+        scrape will be treated as failed. 0 means no limit.
+      '';
+
+      label_value_length_limit = mkDefOpt types.int "0" ''
+        Per-scrape limit on length of labels value that will be accepted for a sample.
+        If a label value is longer than this number post metric-relabeling, the
+        entire scrape will be treated as failed. 0 means no limit.
+      '';
+
+      target_limit = mkDefOpt types.int "0" ''
+        Per-scrape config limit on number of unique targets that will be
+        accepted. If more than this number of targets are present after target
+        relabeling, Prometheus will mark the targets as failed without scraping them.
+        0 means no limit. This is an experimental feature, this behaviour could
+        change in the future.
+      '';
+    };
+  };
+
+  #
+  # Config types: service discovery
+  #
+
+  # For this one, the docs actually define all types needed to use mkSdConfigModule, but a bunch
+  # of them are marked with 'currently not support by Azure' so we don't bother adding them in
+  # here.
+  promTypes.azure_sd_config = types.submodule {
+    options = {
+      environment = mkDefOpt types.str "AzurePublicCloud" ''
+        The Azure environment.
+      '';
+
+      authentication_method = mkDefOpt (types.enum [ "OAuth" "ManagedIdentity" ]) "OAuth" ''
+        The authentication method, either OAuth or ManagedIdentity.
+        See https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview
+      '';
+
+      subscription_id = mkOption {
+        type = types.str;
+        description = ''
+          The subscription ID.
+        '';
+      };
+
+      tenant_id = mkOpt types.str ''
+        Optional tenant ID. Only required with authentication_method OAuth.
+      '';
+
+      client_id = mkOpt types.str ''
+        Optional client ID. Only required with authentication_method OAuth.
+      '';
+
+      client_secret = mkOpt types.str ''
+        Optional client secret. Only required with authentication_method OAuth.
+      '';
+
+      refresh_interval = mkDefOpt types.str "300s" ''
+        Refresh interval to re-read the instance list.
+      '';
+
+      port = mkDefOpt types.int "80" ''
+        The port to scrape metrics from. If using the public IP
+        address, this must instead be specified in the relabeling
+        rule.
+      '';
+
+      proxy_url = mkOpt types.str ''
+        Optional proxy URL.
+      '';
+
+      follow_redirects = mkDefOpt types.bool "true" ''
+        Configure whether HTTP requests follow HTTP 3xx redirects.
+      '';
+
+      tls_config = mkOpt promTypes.tls_config ''
+        TLS configuration.
+      '';
+    };
+  };
+
+  promTypes.consul_sd_config = mkSdConfigModule {
+    server = mkDefOpt types.str "localhost:8500" ''
+      Consul server to query.
+    '';
+
+    token = mkOpt types.str "Consul token";
+
+    datacenter = mkOpt types.str "Consul datacenter";
+
+    scheme = mkDefOpt types.str "http" "Consul scheme";
+
+    username = mkOpt types.str "Consul username";
+
+    password = mkOpt types.str "Consul password";
+
+    tls_config = mkOpt promTypes.tls_config ''
+      Configures the Consul request's TLS settings.
+    '';
+
+    services = mkOpt (types.listOf types.str) ''
+      A list of services for which targets are retrieved.
+    '';
+
+    tags = mkOpt (types.listOf types.str) ''
+      An optional list of tags used to filter nodes for a given
+      service. Services must contain all tags in the list.
+    '';
+
+    node_meta = mkOpt (types.attrsOf types.str) ''
+      Node metadata used to filter nodes for a given service.
+    '';
+
+    tag_separator = mkDefOpt types.str "," ''
+      The string by which Consul tags are joined into the tag label.
+    '';
+
+    allow_stale = mkOpt types.bool ''
+      Allow stale Consul results
+      (see <link xlink:href="https://www.consul.io/api/index.html#consistency-modes"/>).
+
+      Will reduce load on Consul.
+    '';
+
+    refresh_interval = mkDefOpt types.str "30s" ''
+      The time after which the provided names are refreshed.
+
+      On large setup it might be a good idea to increase this value
+      because the catalog will change all the time.
+    '';
+  };
+
+  promTypes.digitalocean_sd_config = mkSdConfigModule {
+    port = mkDefOpt types.int "80" ''
+      The port to scrape metrics from.
+    '';
+
+    refresh_interval = mkDefOpt types.str "60s" ''
+      The time after which the droplets are refreshed.
+    '';
+  };
+
+  mkDockerSdConfigModule = extraOptions: mkSdConfigModule ({
+    host = mkOption {
+      type = types.str;
+      description = ''
+        Address of the Docker daemon.
+      '';
+    };
+
+    port = mkDefOpt types.int "80" ''
+      The port to scrape metrics from, when `role` is nodes, and for discovered
+      tasks and services that don't have published ports.
+    '';
+
+    filters = mkOpt
+      (types.listOf (types.submodule {
+        options = {
+          name = mkOption {
+            type = types.str;
+            description = ''
+              Name of the filter. The available filters are listed in the upstream documentation:
+              Services: <link xlink:href="https://docs.docker.com/engine/api/v1.40/#operation/ServiceList"/>
+              Tasks: <link xlink:href="https://docs.docker.com/engine/api/v1.40/#operation/TaskList"/>
+              Nodes: <link xlink:href="https://docs.docker.com/engine/api/v1.40/#operation/NodeList"/>
+            '';
+          };
+          values = mkOption {
+            type = types.str;
+            description = ''
+              Value for the filter.
+            '';
+          };
+        };
+      })) ''
+      Optional filters to limit the discovery process to a subset of available resources.
+    '';
+
+    refresh_interval = mkDefOpt types.str "60s" ''
+      The time after which the containers are refreshed.
+    '';
+  } // extraOptions);
+
+  promTypes.docker_sd_config = mkDockerSdConfigModule {
+    host_networking_host = mkDefOpt types.str "localhost" ''
+      The host to use if the container is in host networking mode.
+    '';
+  };
+
+  promTypes.dockerswarm_sd_config = mkDockerSdConfigModule {
+    role = mkOption {
+      type = types.enum [ "services" "tasks" "nodes" ];
+      description = ''
+        Role of the targets to retrieve. Must be `services`, `tasks`, or `nodes`.
+      '';
+    };
+  };
+
+  promTypes.dns_sd_config = types.submodule {
+    options = {
+      names = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          A list of DNS SRV record names to be queried.
+        '';
+      };
+
+      type = mkDefOpt (types.enum [ "SRV" "A" "AAAA" ]) "SRV" ''
+        The type of DNS query to perform. One of SRV, A, or AAAA.
+      '';
+
+      port = mkOpt types.int ''
+        The port number used if the query type is not SRV.
+      '';
+
+      refresh_interval = mkDefOpt types.str "30s" ''
+        The time after which the provided names are refreshed.
+      '';
+    };
+  };
+
+  promTypes.ec2_sd_config = types.submodule {
+    options = {
+      region = mkOption {
+        type = types.str;
+        description = ''
+          The AWS Region. If blank, the region from the instance metadata is used.
+        '';
+      };
+      endpoint = mkOpt types.str ''
+        Custom endpoint to be used.
+      '';
+
+      access_key = mkOpt types.str ''
+        The AWS API key id. If blank, the environment variable
+        <literal>AWS_ACCESS_KEY_ID</literal> is used.
+      '';
+
+      secret_key = mkOpt types.str ''
+        The AWS API key secret. If blank, the environment variable
+         <literal>AWS_SECRET_ACCESS_KEY</literal> is used.
+      '';
+
+      profile = mkOpt types.str ''
+        Named AWS profile used to connect to the API.
+      '';
+
+      role_arn = mkOpt types.str ''
+        AWS Role ARN, an alternative to using AWS API keys.
+      '';
+
+      refresh_interval = mkDefOpt types.str "60s" ''
+        Refresh interval to re-read the instance list.
+      '';
+
+      port = mkDefOpt types.int "80" ''
+        The port to scrape metrics from. If using the public IP
+        address, this must instead be specified in the relabeling
+        rule.
+      '';
+
+      filters = mkOpt
+        (types.listOf (types.submodule {
+          options = {
+            name = mkOption {
+              type = types.str;
+              description = ''
+                See <link xlink:href="https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html">this list</link>
+                for the available filters.
+              '';
+            };
+
+            values = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              description = ''
+                Value of the filter.
+              '';
+            };
+          };
+        })) ''
+        Filters can be used optionally to filter the instance list by other criteria.
+      '';
+    };
+  };
+
+  promTypes.eureka_sd_config = mkSdConfigModule {
+    server = mkOption {
+      type = types.str;
+      description = ''
+        The URL to connect to the Eureka server.
+      '';
+    };
+  };
+
+  promTypes.file_sd_config = types.submodule {
+    options = {
+      files = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          Patterns for files from which target groups are extracted. Refer
+          to the Prometheus documentation for permitted filename patterns
+          and formats.
+        '';
+      };
+
+      refresh_interval = mkDefOpt types.str "5m" ''
+        Refresh interval to re-read the files.
+      '';
+    };
+  };
+
+  promTypes.gce_sd_config = types.submodule {
+    options = {
+      # Use `mkOption` instead of `mkOpt` for project and zone because they are
+      # required configuration values for `gce_sd_config`.
+      project = mkOption {
+        type = types.str;
+        description = ''
+          The GCP Project.
+        '';
+      };
+
+      zone = mkOption {
+        type = types.str;
+        description = ''
+          The zone of the scrape targets. If you need multiple zones use multiple
+          gce_sd_configs.
+        '';
+      };
+
+      filter = mkOpt types.str ''
+        Filter can be used optionally to filter the instance list by other
+        criteria Syntax of this filter string is described here in the filter
+        query parameter section: <link
+        xlink:href="https://cloud.google.com/compute/docs/reference/latest/instances/list"
+        />.
+      '';
+
+      refresh_interval = mkDefOpt types.str "60s" ''
+        Refresh interval to re-read the cloud instance list.
+      '';
+
+      port = mkDefOpt types.port "80" ''
+        The port to scrape metrics from. If using the public IP address, this
+        must instead be specified in the relabeling rule.
+      '';
+
+      tag_separator = mkDefOpt types.str "," ''
+        The tag separator used to separate concatenated GCE instance network tags.
+
+        See the GCP documentation on network tags for more information:
+        <link xlink:href="https://cloud.google.com/vpc/docs/add-remove-network-tags" />
+      '';
+    };
+  };
+
+  promTypes.hetzner_sd_config = mkSdConfigModule {
+    role = mkOption {
+      type = types.enum [ "robot" "hcloud" ];
+      description = ''
+        The Hetzner role of entities that should be discovered.
+        One of <literal>robot</literal> or <literal>hcloud</literal>.
+      '';
+    };
+
+    port = mkDefOpt types.int "80" ''
+      The port to scrape metrics from.
+    '';
+
+    refresh_interval = mkDefOpt types.str "60s" ''
+      The time after which the servers are refreshed.
+    '';
+  };
+
+  promTypes.http_sd_config = types.submodule {
+    options = {
+      url = mkOption {
+        type = types.str;
+        description = ''
+          URL from which the targets are fetched.
+        '';
+      };
+
+      refresh_interval = mkDefOpt types.str "60s" ''
+        Refresh interval to re-query the endpoint.
+      '';
+
+      basic_auth = mkOpt promTypes.basic_auth ''
+        Authentication information used to authenticate to the API server.
+        password and password_file are mutually exclusive.
+      '';
+
+      proxy_url = mkOpt types.str ''
+        Optional proxy URL.
+      '';
+
+      follow_redirects = mkDefOpt types.bool "true" ''
+        Configure whether HTTP requests follow HTTP 3xx redirects.
+      '';
+
+      tls_config = mkOpt promTypes.tls_config ''
+        Configures the scrape request's TLS settings.
+      '';
+    };
+  };
+
+  promTypes.kubernetes_sd_config = mkSdConfigModule {
+    api_server = mkOpt types.str ''
+      The API server addresses. If left empty, Prometheus is assumed to run inside
+      of the cluster and will discover API servers automatically and use the pod's
+      CA certificate and bearer token file at /var/run/secrets/kubernetes.io/serviceaccount/.
+    '';
+
+    role = mkOption {
+      type = types.enum [ "endpoints" "service" "pod" "node" "ingress" ];
+      description = ''
+        The Kubernetes role of entities that should be discovered.
+        One of endpoints, service, pod, node, or ingress.
+      '';
+    };
+
+    kubeconfig_file = mkOpt types.str ''
+      Optional path to a kubeconfig file.
+      Note that api_server and kube_config are mutually exclusive.
+    '';
+
+    namespaces = mkOpt
+      (
+        types.submodule {
+          options = {
+            names = mkOpt (types.listOf types.str) ''
+              Namespace name.
+            '';
+          };
+        }
+      ) ''
+      Optional namespace discovery. If omitted, all namespaces are used.
+    '';
+
+    selectors = mkOpt
+      (
+        types.listOf (
+          types.submodule {
+            options = {
+              role = mkOption {
+                type = types.str;
+                description = ''
+                  Selector role
+                '';
+              };
+
+              label = mkOpt types.str ''
+                Selector label
+              '';
+
+              field = mkOpt types.str ''
+                Selector field
+              '';
+            };
+          }
+        )
+      ) ''
+      Optional label and field selectors to limit the discovery process to a subset of available resources.
+      See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/
+      and https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ to learn more about the possible
+      filters that can be used. Endpoints role supports pod, service and endpoints selectors, other roles
+      only support selectors matching the role itself (e.g. node role can only contain node selectors).
+
+      Note: When making decision about using field/label selector make sure that this
+      is the best approach - it will prevent Prometheus from reusing single list/watch
+      for all scrape configs. This might result in a bigger load on the Kubernetes API,
+      because per each selector combination there will be additional LIST/WATCH. On the other hand,
+      if you just want to monitor small subset of pods in large cluster it's recommended to use selectors.
+      Decision, if selectors should be used or not depends on the particular situation.
+    '';
+  };
+
+  promTypes.kuma_sd_config = mkSdConfigModule {
+    server = mkOption {
+      type = types.str;
+      description = ''
+        Address of the Kuma Control Plane's MADS xDS server.
+      '';
+    };
+
+    refresh_interval = mkDefOpt types.str "30s" ''
+      The time to wait between polling update requests.
+    '';
+
+    fetch_timeout = mkDefOpt types.str "2m" ''
+      The time after which the monitoring assignments are refreshed.
+    '';
+  };
+
+  promTypes.lightsail_sd_config = types.submodule {
+    options = {
+      region = mkOpt types.str ''
+        The AWS region. If blank, the region from the instance metadata is used.
+      '';
+
+      endpoint = mkOpt types.str ''
+        Custom endpoint to be used.
+      '';
+
+      access_key = mkOpt types.str ''
+        The AWS API keys. If blank, the environment variable <literal>AWS_ACCESS_KEY_ID</literal> is used.
+      '';
+
+      secret_key = mkOpt types.str ''
+        The AWS API keys. If blank, the environment variable <literal>AWS_SECRET_ACCESS_KEY</literal> is used.
+      '';
+
+      profile = mkOpt types.str ''
+        Named AWS profile used to connect to the API.
+      '';
+
+      role_arn = mkOpt types.str ''
+        AWS Role ARN, an alternative to using AWS API keys.
+      '';
+
+      refresh_interval = mkDefOpt types.str "60s" ''
+        Refresh interval to re-read the instance list.
+      '';
+
+      port = mkDefOpt types.int "80" ''
+        The port to scrape metrics from. If using the public IP address, this must
+        instead be specified in the relabeling rule.
+      '';
+    };
+  };
+
+  promTypes.linode_sd_config = mkSdConfigModule {
+    port = mkDefOpt types.int "80" ''
+      The port to scrape metrics from.
+    '';
+
+    tag_separator = mkDefOpt types.str "," ''
+      The string by which Linode Instance tags are joined into the tag label.
+    '';
+
+    refresh_interval = mkDefOpt types.str "60s" ''
+      The time after which the linode instances are refreshed.
+    '';
+  };
+
+  promTypes.marathon_sd_config = mkSdConfigModule {
+    servers = mkOption {
+      type = types.listOf types.str;
+      description = ''
+        List of URLs to be used to contact Marathon servers. You need to provide at least one server URL.
+      '';
+    };
+
+    refresh_interval = mkDefOpt types.str "30s" ''
+      Polling interval.
+    '';
+
+    auth_token = mkOpt types.str ''
+      Optional authentication information for token-based authentication:
+      <link xlink:href="https://docs.mesosphere.com/1.11/security/ent/iam-api/#passing-an-authentication-token" />
+      It is mutually exclusive with <literal>auth_token_file</literal> and other authentication mechanisms.
+    '';
+
+    auth_token_file = mkOpt types.str ''
+      Optional authentication information for token-based authentication:
+      <link xlink:href="https://docs.mesosphere.com/1.11/security/ent/iam-api/#passing-an-authentication-token" />
+      It is mutually exclusive with <literal>auth_token</literal> and other authentication mechanisms.
+    '';
+  };
+
+  promTypes.nerve_sd_config = types.submodule {
+    options = {
+      servers = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          The Zookeeper servers.
+        '';
+      };
+
+      paths = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          Paths can point to a single service, or the root of a tree of services.
+        '';
+      };
+
+      timeout = mkDefOpt types.str "10s" ''
+        Timeout value.
+      '';
+    };
+  };
+
+  promTypes.openstack_sd_config = types.submodule {
+    options =
+      let
+        userDescription = ''
+          username is required if using Identity V2 API. Consult with your provider's
+          control panel to discover your account's username. In Identity V3, either
+          userid or a combination of username and domain_id or domain_name are needed.
+        '';
+
+        domainDescription = ''
+          At most one of domain_id and domain_name must be provided if using username
+          with Identity V3. Otherwise, either are optional.
+        '';
+
+        projectDescription = ''
+          The project_id and project_name fields are optional for the Identity V2 API.
+          Some providers allow you to specify a project_name instead of the project_id.
+          Some require both. Your provider's authentication policies will determine
+          how these fields influence authentication.
+        '';
+
+        applicationDescription = ''
+          The application_credential_id or application_credential_name fields are
+          required if using an application credential to authenticate. Some providers
+          allow you to create an application credential to authenticate rather than a
+          password.
+        '';
+      in
+      {
+        role = mkOption {
+          type = types.str;
+          description = ''
+            The OpenStack role of entities that should be discovered.
+          '';
+        };
+
+        region = mkOption {
+          type = types.str;
+          description = ''
+            The OpenStack Region.
+          '';
+        };
+
+        identity_endpoint = mkOpt types.str ''
+          identity_endpoint specifies the HTTP endpoint that is required to work with
+          the Identity API of the appropriate version. While it's ultimately needed by
+          all of the identity services, it will often be populated by a provider-level
+          function.
+        '';
+
+        username = mkOpt types.str userDescription;
+        userid = mkOpt types.str userDescription;
+
+        password = mkOpt types.str ''
+          password for the Identity V2 and V3 APIs. Consult with your provider's
+          control panel to discover your account's preferred method of authentication.
+        '';
+
+        domain_name = mkOpt types.str domainDescription;
+        domain_id = mkOpt types.str domainDescription;
+
+        project_name = mkOpt types.str projectDescription;
+        project_id = mkOpt types.str projectDescription;
+
+        application_credential_name = mkOpt types.str applicationDescription;
+        application_credential_id = mkOpt types.str applicationDescription;
+
+        application_credential_secret = mkOpt types.str ''
+          The application_credential_secret field is required if using an application
+          credential to authenticate.
+        '';
+
+        all_tenants = mkDefOpt types.bool "false" ''
+          Whether the service discovery should list all instances for all projects.
+          It is only relevant for the 'instance' role and usually requires admin permissions.
+        '';
+
+        refresh_interval = mkDefOpt types.str "60s" ''
+          Refresh interval to re-read the instance list.
+        '';
+
+        port = mkDefOpt types.int "80" ''
+          The port to scrape metrics from. If using the public IP address, this must
+          instead be specified in the relabeling rule.
+        '';
+
+        availability = mkDefOpt (types.enum [ "public" "admin" "internal" ]) "public" ''
+          The availability of the endpoint to connect to. Must be one of public, admin or internal.
+        '';
+
+        tls_config = mkOpt promTypes.tls_config ''
+          TLS configuration.
+        '';
+      };
+  };
+
+  promTypes.puppetdb_sd_config = mkSdConfigModule {
+    url = mkOption {
+      type = types.str;
+      description = ''
+        The URL of the PuppetDB root query endpoint.
+      '';
+    };
+
+    query = mkOption {
+      type = types.str;
+      description = ''
+        Puppet Query Language (PQL) query. Only resources are supported.
+        https://puppet.com/docs/puppetdb/latest/api/query/v4/pql.html
+      '';
+    };
+
+    include_parameters = mkDefOpt types.bool "false" ''
+      Whether to include the parameters as meta labels.
+      Due to the differences between parameter types and Prometheus labels,
+      some parameters might not be rendered. The format of the parameters might
+      also change in future releases.
+
+      Note: Enabling this exposes parameters in the Prometheus UI and API. Make sure
+      that you don't have secrets exposed as parameters if you enable this.
+    '';
+
+    refresh_interval = mkDefOpt types.str "60s" ''
+      Refresh interval to re-read the resources list.
+    '';
+
+    port = mkDefOpt types.int "80" ''
+      The port to scrape metrics from.
+    '';
+  };
+
+  promTypes.scaleway_sd_config = types.submodule {
+    options = {
+      access_key = mkOption {
+        type = types.str;
+        description = ''
+          Access key to use. https://console.scaleway.com/project/credentials
+        '';
+      };
+
+      secret_key = mkOpt types.str ''
+        Secret key to use when listing targets. https://console.scaleway.com/project/credentials
+        It is mutually exclusive with `secret_key_file`.
+      '';
+
+      secret_key_file = mkOpt types.str ''
+        Sets the secret key with the credentials read from the configured file.
+        It is mutually exclusive with `secret_key`.
+      '';
+
+      project_id = mkOption {
+        type = types.str;
+        description = ''
+          Project ID of the targets.
+        '';
+      };
+
+      role = mkOption {
+        type = types.enum [ "instance" "baremetal" ];
+        description = ''
+          Role of the targets to retrieve. Must be `instance` or `baremetal`.
+        '';
+      };
+
+      port = mkDefOpt types.int "80" ''
+        The port to scrape metrics from.
+      '';
+
+      api_url = mkDefOpt types.str "https://api.scaleway.com" ''
+        API URL to use when doing the server listing requests.
+      '';
+
+      zone = mkDefOpt types.str "fr-par-1" ''
+        Zone is the availability zone of your targets (e.g. fr-par-1).
+      '';
+
+      name_filter = mkOpt types.str ''
+        Specify a name filter (works as a LIKE) to apply on the server listing request.
+      '';
+
+      tags_filter = mkOpt (types.listOf types.str) ''
+        Specify a tag filter (a server needs to have all defined tags to be listed) to apply on the server listing request.
+      '';
+
+      refresh_interval = mkDefOpt types.str "60s" ''
+        Refresh interval to re-read the managed targets list.
+      '';
+
+      proxy_url = mkOpt types.str ''
+        Optional proxy URL.
+      '';
+
+      follow_redirects = mkDefOpt types.bool "true" ''
+        Configure whether HTTP requests follow HTTP 3xx redirects.
+      '';
+
+      tls_config = mkOpt promTypes.tls_config ''
+        TLS configuration.
+      '';
+    };
+  };
+
+  # These are exactly the same.
+  promTypes.serverset_sd_config = promTypes.nerve_sd_config;
+
+  promTypes.triton_sd_config = types.submodule {
+    options = {
+      account = mkOption {
+        type = types.str;
+        description = ''
+          The account to use for discovering new targets.
+        '';
+      };
+
+      role = mkDefOpt (types.enum [ "container" "cn" ]) "container" ''
+        The type of targets to discover, can be set to:
+        - "container" to discover virtual machines (SmartOS zones, lx/KVM/bhyve branded zones) running on Triton
+        - "cn" to discover compute nodes (servers/global zones) making up the Triton infrastructure
+      '';
+
+      dns_suffix = mkOption {
+        type = types.str;
+        description = ''
+          The DNS suffix which should be applied to target.
+        '';
+      };
+
+      endpoint = mkOption {
+        type = types.str;
+        description = ''
+          The Triton discovery endpoint (e.g. <literal>cmon.us-east-3b.triton.zone</literal>). This is
+          often the same value as dns_suffix.
+        '';
+      };
+
+      groups = mkOpt (types.listOf types.str) ''
+        A list of groups for which targets are retrieved, only supported when targeting the <literal>container</literal> role.
+        If omitted all containers owned by the requesting account are scraped.
+      '';
+
+      port = mkDefOpt types.int "9163" ''
+        The port to use for discovery and metric scraping.
+      '';
+
+      refresh_interval = mkDefOpt types.str "60s" ''
+        The interval which should be used for refreshing targets.
+      '';
+
+      version = mkDefOpt types.int "1" ''
+        The Triton discovery API version.
+      '';
+
+      tls_config = mkOpt promTypes.tls_config ''
+        TLS configuration.
+      '';
+    };
+  };
+
+  promTypes.uyuni_sd_config = mkSdConfigModule {
+    server = mkOption {
+      type = types.str;
+      description = ''
+        The URL to connect to the Uyuni server.
+      '';
+    };
+
+    username = mkOption {
+      type = types.str;
+      description = ''
+        Credentials are used to authenticate the requests to Uyuni API.
+      '';
+    };
+
+    password = mkOption {
+      type = types.str;
+      description = ''
+        Credentials are used to authenticate the requests to Uyuni API.
+      '';
+    };
+
+    entitlement = mkDefOpt types.str "monitoring_entitled" ''
+      The entitlement string to filter eligible systems.
+    '';
+
+    separator = mkDefOpt types.str "," ''
+      The string by which Uyuni group names are joined into the groups label
+    '';
+
+    refresh_interval = mkDefOpt types.str "60s" ''
+      Refresh interval to re-read the managed targets list.
+    '';
+  };
+
+  promTypes.static_config = types.submodule {
+    options = {
+      targets = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          The targets specified by the target group.
+        '';
+      };
+      labels = mkOption {
+        type = types.attrsOf types.str;
+        default = { };
+        description = ''
+          Labels assigned to all metrics scraped from the targets.
+        '';
+      };
+    };
+  };
+
+  #
+  # Config types: relabling
+  #
+
+  promTypes.relabel_config = types.submodule {
+    options = {
+      source_labels = mkOpt (types.listOf types.str) ''
+        The source labels select values from existing labels. Their content
+        is concatenated using the configured separator and matched against
+        the configured regular expression.
+      '';
+
+      separator = mkDefOpt types.str ";" ''
+        Separator placed between concatenated source label values.
+      '';
+
+      target_label = mkOpt types.str ''
+        Label to which the resulting value is written in a replace action.
+        It is mandatory for replace actions.
+      '';
+
+      regex = mkDefOpt types.str "(.*)" ''
+        Regular expression against which the extracted value is matched.
+      '';
+
+      modulus = mkOpt types.int ''
+        Modulus to take of the hash of the source label values.
+      '';
+
+      replacement = mkDefOpt types.str "$1" ''
+        Replacement value against which a regex replace is performed if the
+        regular expression matches.
+      '';
+
+      action =
+        mkDefOpt (types.enum [ "replace" "keep" "drop" "hashmod" "labelmap" "labeldrop" "labelkeep" ]) "replace" ''
+          Action to perform based on regex matching.
+        '';
+    };
+  };
+
+  #
+  # Config types : remote read / write
+  #
+
+  promTypes.remote_write = types.submodule {
+    options = {
+      url = mkOption {
+        type = types.str;
+        description = ''
+          ServerName extension to indicate the name of the server.
+          http://tools.ietf.org/html/rfc4366#section-3.1
+        '';
+      };
+      remote_timeout = mkOpt types.str ''
+        Timeout for requests to the remote write endpoint.
+      '';
+      write_relabel_configs = mkOpt (types.listOf promTypes.relabel_config) ''
+        List of remote write relabel configurations.
+      '';
+      name = mkOpt types.str ''
+        Name of the remote write config, which if specified must be unique among remote write configs.
+        The name will be used in metrics and logging in place of a generated value to help users distinguish between
+        remote write configs.
+      '';
+      basic_auth = mkOpt promTypes.basic_auth ''
+        Sets the `Authorization` header on every remote write request with the
+        configured username and password.
+        password and password_file are mutually exclusive.
+      '';
+      bearer_token = mkOpt types.str ''
+        Sets the `Authorization` header on every remote write request with
+        the configured bearer token. It is mutually exclusive with `bearer_token_file`.
+      '';
+      bearer_token_file = mkOpt types.str ''
+        Sets the `Authorization` header on every remote write request with the bearer token
+        read from the configured file. It is mutually exclusive with `bearer_token`.
+      '';
+      tls_config = mkOpt promTypes.tls_config ''
+        Configures the remote write request's TLS settings.
+      '';
+      proxy_url = mkOpt types.str "Optional Proxy URL.";
+      queue_config = mkOpt
+        (types.submodule {
+          options = {
+            capacity = mkOpt types.int ''
+              Number of samples to buffer per shard before we block reading of more
+              samples from the WAL. It is recommended to have enough capacity in each
+              shard to buffer several requests to keep throughput up while processing
+              occasional slow remote requests.
+            '';
+            max_shards = mkOpt types.int ''
+              Maximum number of shards, i.e. amount of concurrency.
+            '';
+            min_shards = mkOpt types.int ''
+              Minimum number of shards, i.e. amount of concurrency.
+            '';
+            max_samples_per_send = mkOpt types.int ''
+              Maximum number of samples per send.
+            '';
+            batch_send_deadline = mkOpt types.str ''
+              Maximum time a sample will wait in buffer.
+            '';
+            min_backoff = mkOpt types.str ''
+              Initial retry delay. Gets doubled for every retry.
+            '';
+            max_backoff = mkOpt types.str ''
+              Maximum retry delay.
+            '';
+          };
+        }) ''
+        Configures the queue used to write to remote storage.
+      '';
+      metadata_config = mkOpt
+        (types.submodule {
+          options = {
+            send = mkOpt types.bool ''
+              Whether metric metadata is sent to remote storage or not.
+            '';
+            send_interval = mkOpt types.str ''
+              How frequently metric metadata is sent to remote storage.
+            '';
+          };
+        }) ''
+        Configures the sending of series metadata to remote storage.
+        Metadata configuration is subject to change at any point
+        or be removed in future releases.
+      '';
+    };
+  };
+
+  promTypes.remote_read = types.submodule {
+    options = {
+      url = mkOption {
+        type = types.str;
+        description = ''
+          ServerName extension to indicate the name of the server.
+          http://tools.ietf.org/html/rfc4366#section-3.1
+        '';
+      };
+      name = mkOpt types.str ''
+        Name of the remote read config, which if specified must be unique among remote read configs.
+        The name will be used in metrics and logging in place of a generated value to help users distinguish between
+        remote read configs.
+      '';
+      required_matchers = mkOpt (types.attrsOf types.str) ''
+        An optional list of equality matchers which have to be
+        present in a selector to query the remote read endpoint.
+      '';
+      remote_timeout = mkOpt types.str ''
+        Timeout for requests to the remote read endpoint.
+      '';
+      read_recent = mkOpt types.bool ''
+        Whether reads should be made for queries for time ranges that
+        the local storage should have complete data for.
+      '';
+      basic_auth = mkOpt promTypes.basic_auth ''
+        Sets the `Authorization` header on every remote read request with the
+        configured username and password.
+        password and password_file are mutually exclusive.
+      '';
+      bearer_token = mkOpt types.str ''
+        Sets the `Authorization` header on every remote read request with
+        the configured bearer token. It is mutually exclusive with `bearer_token_file`.
+      '';
+      bearer_token_file = mkOpt types.str ''
+        Sets the `Authorization` header on every remote read request with the bearer token
+        read from the configured file. It is mutually exclusive with `bearer_token`.
+      '';
+      tls_config = mkOpt promTypes.tls_config ''
+        Configures the remote read request's TLS settings.
+      '';
+      proxy_url = mkOpt types.str "Optional Proxy URL.";
+    };
+  };
+
+in
+{
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "prometheus2" ] [ "services" "prometheus" ])
+    (mkRemovedOptionModule [ "services" "prometheus" "environmentFile" ]
+      "It has been removed since it was causing issues (https://github.com/NixOS/nixpkgs/issues/126083) and Prometheus now has native support for secret files, i.e. `basic_auth.password_file` and `authorization.credentials_file`.")
+  ];
+
+  options.services.prometheus = {
+
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable the Prometheus monitoring daemon.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.prometheus;
+      defaultText = literalExpression "pkgs.prometheus";
+      description = ''
+        The prometheus package that should be used.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 9090;
+      description = ''
+        Port to listen on.
+      '';
+    };
+
+    listenAddress = mkOption {
+      type = types.str;
+      default = "0.0.0.0";
+      description = ''
+        Address to listen on for the web interface, API, and telemetry.
+      '';
+    };
+
+    stateDir = mkOption {
+      type = types.str;
+      default = "prometheus2";
+      description = ''
+        Directory below <literal>/var/lib</literal> to store Prometheus metrics data.
+        This directory will be created automatically using systemd's StateDirectory mechanism.
+      '';
+    };
+
+    extraFlags = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      description = ''
+        Extra commandline options when launching Prometheus.
+      '';
+    };
+
+    enableReload = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Reload prometheus when configuration file changes (instead of restart).
+
+        The following property holds: switching to a configuration
+        (<literal>switch-to-configuration</literal>) that changes the prometheus
+        configuration only finishes successully when prometheus has finished
+        loading the new configuration.
+      '';
+    };
+
+    configText = mkOption {
+      type = types.nullOr types.lines;
+      default = null;
+      description = ''
+        If non-null, this option defines the text that is written to
+        prometheus.yml. If null, the contents of prometheus.yml is generated
+        from the structured config options.
+      '';
+    };
+
+    globalConfig = mkOption {
+      type = promTypes.globalConfig;
+      default = { };
+      description = ''
+        Parameters that are valid in all  configuration contexts. They
+        also serve as defaults for other configuration sections
+      '';
+    };
+
+    remoteRead = mkOption {
+      type = types.listOf promTypes.remote_read;
+      default = [ ];
+      description = ''
+        Parameters of the endpoints to query from.
+        See <link xlink:href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_read">the official documentation</link> for more information.
+      '';
+    };
+
+    remoteWrite = mkOption {
+      type = types.listOf promTypes.remote_write;
+      default = [ ];
+      description = ''
+        Parameters of the endpoints to send samples to.
+        See <link xlink:href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write">the official documentation</link> for more information.
+      '';
+    };
+
+    rules = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      description = ''
+        Alerting and/or Recording rules to evaluate at runtime.
+      '';
+    };
+
+    ruleFiles = mkOption {
+      type = types.listOf types.path;
+      default = [ ];
+      description = ''
+        Any additional rules files to include in this configuration.
+      '';
+    };
+
+    scrapeConfigs = mkOption {
+      type = types.listOf promTypes.scrape_config;
+      default = [ ];
+      description = ''
+        A list of scrape configurations.
+      '';
+    };
+
+    alertmanagers = mkOption {
+      type = types.listOf types.attrs;
+      example = literalExpression ''
+        [ {
+          scheme = "https";
+          path_prefix = "/alertmanager";
+          static_configs = [ {
+            targets = [
+              "prometheus.domain.tld"
+            ];
+          } ];
+        } ]
+      '';
+      default = [ ];
+      description = ''
+        A list of alertmanagers to send alerts to.
+        See <link xlink:href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#alertmanager_config">the official documentation</link> for more information.
+      '';
+    };
+
+    alertmanagerNotificationQueueCapacity = mkOption {
+      type = types.int;
+      default = 10000;
+      description = ''
+        The capacity of the queue for pending alert manager notifications.
+      '';
+    };
+
+    alertmanagerTimeout = mkOption {
+      type = types.int;
+      default = 10;
+      description = ''
+        Alert manager HTTP API timeout (in seconds).
+      '';
+    };
+
+    webExternalUrl = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "https://example.com/";
+      description = ''
+        The URL under which Prometheus is externally reachable (for example,
+        if Prometheus is served via a reverse proxy).
+      '';
+    };
+
+    checkConfig = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Check configuration with <literal>promtool
+        check</literal>. The call to <literal>promtool</literal> is
+        subject to sandboxing by Nix. When credentials are stored in
+        external files (<literal>password_file</literal>,
+        <literal>bearer_token_file</literal>, etc), they will not be
+        visible to <literal>promtool</literal> and it will report
+        errors, despite a correct configuration.
+      '';
+    };
+
+    retentionTime = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "15d";
+      description = ''
+        How long to retain samples in storage.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      (
+        let
+          # Match something with dots (an IPv4 address) or something ending in
+          # a square bracket (an IPv6 addresses) followed by a port number.
+          legacy = builtins.match "(.*\\..*|.*]):([[:digit:]]+)" cfg.listenAddress;
+        in
+        {
+          assertion = legacy == null;
+          message = ''
+            Do not specify the port for Prometheus to listen on in the
+            listenAddress option; use the port option instead:
+              services.prometheus.listenAddress = ${builtins.elemAt legacy 0};
+              services.prometheus.port = ${builtins.elemAt legacy 1};
+          '';
+        }
+      )
+    ];
+
+    users.groups.prometheus.gid = config.ids.gids.prometheus;
+    users.users.prometheus = {
+      description = "Prometheus daemon user";
+      uid = config.ids.uids.prometheus;
+      group = "prometheus";
+    };
+    environment.etc."prometheus/prometheus.yaml" = mkIf cfg.enableReload {
+      source = prometheusYml;
+    };
+    systemd.services.prometheus = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/prometheus" +
+          optionalString (length cmdlineArgs != 0) (" \\\n  " +
+            concatStringsSep " \\\n  " cmdlineArgs);
+        ExecReload = mkIf cfg.enableReload "+${reload}/bin/reload-prometheus";
+        User = "prometheus";
+        Restart = "always";
+        RuntimeDirectory = "prometheus";
+        RuntimeDirectoryMode = "0700";
+        WorkingDirectory = workingDir;
+        StateDirectory = cfg.stateDir;
+        StateDirectoryMode = "0700";
+      };
+    };
+    # prometheus-config-reload will activate after prometheus. However, what we
+    # don't want is that on startup it immediately reloads prometheus because
+    # prometheus itself might have just started.
+    #
+    # Instead we only want to reload prometheus when the config file has
+    # changed. So on startup prometheus-config-reload will just output a
+    # harmless message and then stay active (RemainAfterExit).
+    #
+    # Then, when the config file has changed, switch-to-configuration notices
+    # that this service has changed (restartTriggers) and needs to be reloaded
+    # (reloadIfChanged). The reload command then reloads prometheus.
+    systemd.services.prometheus-config-reload = mkIf cfg.enableReload {
+      wantedBy = [ "prometheus.service" ];
+      after = [ "prometheus.service" ];
+      reloadIfChanged = true;
+      restartTriggers = [ prometheusYml ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        TimeoutSec = 60;
+        ExecStart = "${pkgs.logger}/bin/logger 'prometheus-config-reload will only reload prometheus when reloaded itself.'";
+        ExecReload = [ "${triggerReload}/bin/trigger-reload-prometheus" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix
new file mode 100644
index 00000000000..41302d6d3ce
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters.nix
@@ -0,0 +1,303 @@
+{ config, pkgs, lib, options, ... }:
+
+let
+  inherit (lib) concatStrings foldl foldl' genAttrs literalExpression maintainers
+                mapAttrsToList mkDefault mkEnableOption mkIf mkMerge mkOption
+                optional types mkOptionDefault flip attrNames;
+
+  cfg = config.services.prometheus.exporters;
+
+  # each attribute in `exporterOpts` is expected to have specified:
+  #   - port        (types.int):   port on which the exporter listens
+  #   - serviceOpts (types.attrs): config that is merged with the
+  #                                default definition of the exporter's
+  #                                systemd service
+  #   - extraOpts   (types.attrs): extra configuration options to
+  #                                configure the exporter with, which
+  #                                are appended to the default options
+  #
+  #  Note that `extraOpts` is optional, but a script for the exporter's
+  #  systemd service must be provided by specifying either
+  #  `serviceOpts.script` or `serviceOpts.serviceConfig.ExecStart`
+
+  exporterOpts = genAttrs [
+    "apcupsd"
+    "artifactory"
+    "bind"
+    "bird"
+    "bitcoin"
+    "blackbox"
+    "buildkite-agent"
+    "collectd"
+    "dmarc"
+    "dnsmasq"
+    "domain"
+    "dovecot"
+    "fastly"
+    "fritzbox"
+    "influxdb"
+    "json"
+    "jitsi"
+    "kea"
+    "keylight"
+    "knot"
+    "lnd"
+    "mail"
+    "mikrotik"
+    "minio"
+    "modemmanager"
+    "nextcloud"
+    "nginx"
+    "nginxlog"
+    "node"
+    "openldap"
+    "openvpn"
+    "pihole"
+    "postfix"
+    "postgres"
+    "process"
+    "pve"
+    "py-air-control"
+    "redis"
+    "rspamd"
+    "rtl_433"
+    "script"
+    "snmp"
+    "smartctl"
+    "smokeping"
+    "sql"
+    "surfboard"
+    "systemd"
+    "tor"
+    "unbound"
+    "unifi"
+    "unifi-poller"
+    "varnish"
+    "wireguard"
+    "flow"
+  ] (name:
+    import (./. + "/exporters/${name}.nix") { inherit config lib pkgs options; }
+  );
+
+  mkExporterOpts = ({ name, port }: {
+    enable = mkEnableOption "the prometheus ${name} exporter";
+    port = mkOption {
+      type = types.port;
+      default = port;
+      description = ''
+        Port to listen on.
+      '';
+    };
+    listenAddress = mkOption {
+      type = types.str;
+      default = "0.0.0.0";
+      description = ''
+        Address to listen on.
+      '';
+    };
+    extraFlags = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Extra commandline options to pass to the ${name} exporter.
+      '';
+    };
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Open port in firewall for incoming connections.
+      '';
+    };
+    firewallFilter = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = literalExpression ''
+        "-i eth0 -p tcp -m tcp --dport ${toString port}"
+      '';
+      description = ''
+        Specify a filter for iptables to use when
+        <option>services.prometheus.exporters.${name}.openFirewall</option>
+        is true. It is used as `ip46tables -I nixos-fw <option>firewallFilter</option> -j nixos-fw-accept`.
+      '';
+    };
+    user = mkOption {
+      type = types.str;
+      default = "${name}-exporter";
+      description = ''
+        User name under which the ${name} exporter shall be run.
+      '';
+    };
+    group = mkOption {
+      type = types.str;
+      default = "${name}-exporter";
+      description = ''
+        Group under which the ${name} exporter shall be run.
+      '';
+    };
+  });
+
+  mkSubModule = { name, port, extraOpts, imports }: {
+    ${name} = mkOption {
+      type = types.submodule [{
+        inherit imports;
+        options = (mkExporterOpts {
+          inherit name port;
+        } // extraOpts);
+      } ({ config, ... }: mkIf config.openFirewall {
+        firewallFilter = mkDefault "-p tcp -m tcp --dport ${toString config.port}";
+      })];
+      internal = true;
+      default = {};
+    };
+  };
+
+  mkSubModules = (foldl' (a: b: a//b) {}
+    (mapAttrsToList (name: opts: mkSubModule {
+      inherit name;
+      inherit (opts) port;
+      extraOpts = opts.extraOpts or {};
+      imports = opts.imports or [];
+    }) exporterOpts)
+  );
+
+  mkExporterConf = { name, conf, serviceOpts }:
+    let
+      enableDynamicUser = serviceOpts.serviceConfig.DynamicUser or true;
+    in
+    mkIf conf.enable {
+      warnings = conf.warnings or [];
+      users.users."${name}-exporter" = (mkIf (conf.user == "${name}-exporter" && !enableDynamicUser) {
+        description = "Prometheus ${name} exporter service user";
+        isSystemUser = true;
+        inherit (conf) group;
+      });
+      users.groups = (mkIf (conf.group == "${name}-exporter" && !enableDynamicUser) {
+        "${name}-exporter" = {};
+      });
+      networking.firewall.extraCommands = mkIf conf.openFirewall (concatStrings [
+        "ip46tables -A nixos-fw ${conf.firewallFilter} "
+        "-m comment --comment ${name}-exporter -j nixos-fw-accept"
+      ]);
+      systemd.services."prometheus-${name}-exporter" = mkMerge ([{
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig.Restart = mkDefault "always";
+        serviceConfig.PrivateTmp = mkDefault true;
+        serviceConfig.WorkingDirectory = mkDefault /tmp;
+        serviceConfig.DynamicUser = mkDefault enableDynamicUser;
+        serviceConfig.User = mkDefault conf.user;
+        serviceConfig.Group = conf.group;
+        # Hardening
+        serviceConfig.CapabilityBoundingSet = mkDefault [ "" ];
+        serviceConfig.DeviceAllow = [ "" ];
+        serviceConfig.LockPersonality = true;
+        serviceConfig.MemoryDenyWriteExecute = true;
+        serviceConfig.NoNewPrivileges = true;
+        serviceConfig.PrivateDevices = true;
+        serviceConfig.ProtectClock = mkDefault true;
+        serviceConfig.ProtectControlGroups = true;
+        serviceConfig.ProtectHome = true;
+        serviceConfig.ProtectHostname = true;
+        serviceConfig.ProtectKernelLogs = true;
+        serviceConfig.ProtectKernelModules = true;
+        serviceConfig.ProtectKernelTunables = true;
+        serviceConfig.ProtectSystem = mkDefault "strict";
+        serviceConfig.RemoveIPC = true;
+        serviceConfig.RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        serviceConfig.RestrictNamespaces = true;
+        serviceConfig.RestrictRealtime = true;
+        serviceConfig.RestrictSUIDSGID = true;
+        serviceConfig.SystemCallArchitectures = "native";
+        serviceConfig.UMask = "0077";
+      } serviceOpts ]);
+  };
+in
+{
+
+  imports = (lib.forEach [ "blackboxExporter" "collectdExporter" "fritzboxExporter"
+                   "jsonExporter" "minioExporter" "nginxExporter" "nodeExporter"
+                   "snmpExporter" "unifiExporter" "varnishExporter" ]
+       (opt: lib.mkRemovedOptionModule [ "services" "prometheus" "${opt}" ] ''
+         The prometheus exporters are now configured using `services.prometheus.exporters'.
+         See the 18.03 release notes for more information.
+       '' ));
+
+  options.services.prometheus.exporters = mkOption {
+    type = types.submodule {
+      options = (mkSubModules);
+    };
+    description = "Prometheus exporter configuration";
+    default = {};
+    example = literalExpression ''
+      {
+        node = {
+          enable = true;
+          enabledCollectors = [ "systemd" ];
+        };
+        varnish.enable = true;
+      }
+    '';
+  };
+
+  config = mkMerge ([{
+    assertions = [ {
+      assertion = cfg.snmp.enable -> (
+        (cfg.snmp.configurationPath == null) != (cfg.snmp.configuration == null)
+      );
+      message = ''
+        Please ensure you have either `services.prometheus.exporters.snmp.configuration'
+          or `services.prometheus.exporters.snmp.configurationPath' set!
+      '';
+    } {
+      assertion = cfg.mikrotik.enable -> (
+        (cfg.mikrotik.configFile == null) != (cfg.mikrotik.configuration == null)
+      );
+      message = ''
+        Please specify either `services.prometheus.exporters.mikrotik.configuration'
+          or `services.prometheus.exporters.mikrotik.configFile'.
+      '';
+    } {
+      assertion = cfg.mail.enable -> (
+        (cfg.mail.configFile == null) != (cfg.mail.configuration == null)
+      );
+      message = ''
+        Please specify either 'services.prometheus.exporters.mail.configuration'
+          or 'services.prometheus.exporters.mail.configFile'.
+      '';
+    } {
+      assertion = cfg.sql.enable -> (
+        (cfg.sql.configFile == null) != (cfg.sql.configuration == null)
+      );
+      message = ''
+        Please specify either 'services.prometheus.exporters.sql.configuration' or
+          'services.prometheus.exporters.sql.configFile'
+      '';
+    } ] ++ (flip map (attrNames cfg) (exporter: {
+      assertion = cfg.${exporter}.firewallFilter != null -> cfg.${exporter}.openFirewall;
+      message = ''
+        The `firewallFilter'-option of exporter ${exporter} doesn't have any effect unless
+        `openFirewall' is set to `true'!
+      '';
+    }));
+  }] ++ [(mkIf config.services.minio.enable {
+    services.prometheus.exporters.minio.minioAddress  = mkDefault "http://localhost:9000";
+    services.prometheus.exporters.minio.minioAccessKey = mkDefault config.services.minio.accessKey;
+    services.prometheus.exporters.minio.minioAccessSecret = mkDefault config.services.minio.secretKey;
+  })] ++ [(mkIf config.services.prometheus.exporters.rtl_433.enable {
+    hardware.rtl-sdr.enable = mkDefault true;
+  })] ++ [(mkIf config.services.postfix.enable {
+    services.prometheus.exporters.postfix.group = mkDefault config.services.postfix.setgidGroup;
+  })] ++ (mapAttrsToList (name: conf:
+    mkExporterConf {
+      inherit name;
+      inherit (conf) serviceOpts;
+      conf = cfg.${name};
+    }) exporterOpts)
+  );
+
+  meta = {
+    doc = ./exporters.xml;
+    maintainers = [ maintainers.willibutz ];
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.xml b/nixos/modules/services/monitoring/prometheus/exporters.xml
new file mode 100644
index 00000000000..c2d4b05996a
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters.xml
@@ -0,0 +1,227 @@
+<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;
+    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 seperately 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>
+ </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 confiuration.
+  # 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/apcupsd.nix b/nixos/modules/services/monitoring/prometheus/exporters/apcupsd.nix
new file mode 100644
index 00000000000..57c35a742c5
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/apcupsd.nix
@@ -0,0 +1,38 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.apcupsd;
+in
+{
+  port = 9162;
+  extraOpts = {
+    apcupsdAddress = mkOption {
+      type = types.str;
+      default = ":3551";
+      description = ''
+        Address of the apcupsd Network Information Server (NIS).
+      '';
+    };
+
+    apcupsdNetwork = mkOption {
+      type = types.enum ["tcp" "tcp4" "tcp6"];
+      default = "tcp";
+      description = ''
+        Network of the apcupsd Network Information Server (NIS): one of "tcp", "tcp4", or "tcp6".
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-apcupsd-exporter}/bin/apcupsd_exporter \
+          -telemetry.addr ${cfg.listenAddress}:${toString cfg.port} \
+          -apcupsd.addr ${cfg.apcupsdAddress} \
+          -apcupsd.network ${cfg.apcupsdNetwork} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/artifactory.nix b/nixos/modules/services/monitoring/prometheus/exporters/artifactory.nix
new file mode 100644
index 00000000000..2adcecc728b
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/artifactory.nix
@@ -0,0 +1,59 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.artifactory;
+in
+{
+  port = 9531;
+  extraOpts = {
+    scrapeUri = mkOption {
+      type = types.str;
+      default = "http://localhost:8081/artifactory";
+      description = ''
+        URI on which to scrape JFrog Artifactory.
+      '';
+    };
+
+    artiUsername = mkOption {
+      type = types.str;
+      description = ''
+        Username for authentication against JFrog Artifactory API.
+      '';
+    };
+
+    artiPassword = mkOption {
+      type = types.str;
+      default = "";
+      description = ''
+        Password for authentication against JFrog Artifactory API.
+        One of the password or access token needs to be set.
+      '';
+    };
+
+    artiAccessToken = mkOption {
+      type = types.str;
+      default = "";
+      description = ''
+        Access token for authentication against JFrog Artifactory API.
+        One of the password or access token needs to be set.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-artifactory-exporter}/bin/artifactory_exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --artifactory.scrape-uri ${cfg.scrapeUri} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+      Environment = [
+        "ARTI_USERNAME=${cfg.artiUsername}"
+        "ARTI_PASSWORD=${cfg.artiPassword}"
+        "ARTI_ACCESS_TOKEN=${cfg.artiAccessToken}"
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/bind.nix b/nixos/modules/services/monitoring/prometheus/exporters/bind.nix
new file mode 100644
index 00000000000..16c2920751d
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/bind.nix
@@ -0,0 +1,54 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.bind;
+in
+{
+  port = 9119;
+  extraOpts = {
+    bindURI = mkOption {
+      type = types.str;
+      default = "http://localhost:8053/";
+      description = ''
+        HTTP XML API address of an Bind server.
+      '';
+    };
+    bindTimeout = mkOption {
+      type = types.str;
+      default = "10s";
+      description = ''
+        Timeout for trying to get stats from Bind.
+      '';
+    };
+    bindVersion = mkOption {
+      type = types.enum [ "xml.v2" "xml.v3" "auto" ];
+      default = "auto";
+      description = ''
+        BIND statistics version. Can be detected automatically.
+      '';
+    };
+    bindGroups = mkOption {
+      type = types.listOf (types.enum [ "server" "view" "tasks" ]);
+      default = [ "server" "view" ];
+      description = ''
+        List of statistics to collect. Available: [server, view, tasks]
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-bind-exporter}/bin/bind_exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --bind.pid-file /var/run/named/named.pid \
+          --bind.timeout ${toString cfg.bindTimeout} \
+          --bind.stats-url ${cfg.bindURI} \
+          --bind.stats-version ${cfg.bindVersion} \
+          --bind.stats-groups ${concatStringsSep "," cfg.bindGroups} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/bird.nix b/nixos/modules/services/monitoring/prometheus/exporters/bird.nix
new file mode 100644
index 00000000000..1ef264fc86e
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/bird.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.bird;
+in
+{
+  port = 9324;
+  extraOpts = {
+    birdVersion = mkOption {
+      type = types.enum [ 1 2 ];
+      default = 2;
+      description = ''
+        Specifies whether BIRD1 or BIRD2 is in use.
+      '';
+    };
+    birdSocket = mkOption {
+      type = types.path;
+      default = "/var/run/bird.ctl";
+      description = ''
+        Path to BIRD2 (or BIRD1 v4) socket.
+      '';
+    };
+    newMetricFormat = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Enable the new more-generic metric format.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      SupplementaryGroups = singleton (if cfg.birdVersion == 1 then "bird" else "bird2");
+      ExecStart = ''
+        ${pkgs.prometheus-bird-exporter}/bin/bird_exporter \
+          -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          -bird.socket ${cfg.birdSocket} \
+          -bird.v2=${if cfg.birdVersion == 2 then "true" else "false"} \
+          -format.new=${if cfg.newMetricFormat then "true" else "false"} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+      RestrictAddressFamilies = [
+        # Need AF_UNIX to collect data
+        "AF_UNIX"
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/bitcoin.nix b/nixos/modules/services/monitoring/prometheus/exporters/bitcoin.nix
new file mode 100644
index 00000000000..43721f70b49
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/bitcoin.nix
@@ -0,0 +1,82 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.bitcoin;
+in
+{
+  port = 9332;
+  extraOpts = {
+    rpcUser = mkOption {
+      type = types.str;
+      default = "bitcoinrpc";
+      description = ''
+        RPC user name.
+      '';
+    };
+
+    rpcPasswordFile = mkOption {
+      type = types.path;
+      description = ''
+        File containing RPC password.
+      '';
+    };
+
+    rpcScheme = mkOption {
+      type = types.enum [ "http" "https" ];
+      default = "http";
+      description = ''
+        Whether to connect to bitcoind over http or https.
+      '';
+    };
+
+    rpcHost = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = ''
+        RPC host.
+      '';
+    };
+
+    rpcPort = mkOption {
+      type = types.port;
+      default = 8332;
+      description = ''
+        RPC port number.
+      '';
+    };
+
+    refreshSeconds = mkOption {
+      type = types.ints.unsigned;
+      default = 300;
+      description = ''
+        How often to ask bitcoind for metrics.
+      '';
+    };
+
+    extraEnv = mkOption {
+      type = types.attrsOf types.str;
+      default = {};
+      description = ''
+        Extra environment variables for the exporter.
+      '';
+    };
+  };
+  serviceOpts = {
+    script = ''
+      export BITCOIN_RPC_PASSWORD=$(cat ${cfg.rpcPasswordFile})
+      exec ${pkgs.prometheus-bitcoin-exporter}/bin/bitcoind-monitor.py
+    '';
+
+    environment = {
+      BITCOIN_RPC_USER = cfg.rpcUser;
+      BITCOIN_RPC_SCHEME = cfg.rpcScheme;
+      BITCOIN_RPC_HOST = cfg.rpcHost;
+      BITCOIN_RPC_PORT = toString cfg.rpcPort;
+      METRICS_ADDR = cfg.listenAddress;
+      METRICS_PORT = toString cfg.port;
+      REFRESH_SECONDS = toString cfg.refreshSeconds;
+    } // cfg.extraEnv;
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix b/nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix
new file mode 100644
index 00000000000..fe8d905da3f
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix
@@ -0,0 +1,70 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  logPrefix = "services.prometheus.exporter.blackbox";
+  cfg = config.services.prometheus.exporters.blackbox;
+
+  # This ensures that we can deal with string paths, path types and
+  # store-path strings with context.
+  coerceConfigFile = file:
+    if (builtins.isPath file) || (lib.isStorePath file) then
+      file
+    else
+      (lib.warn ''
+        ${logPrefix}: configuration file "${file}" is being copied to the nix-store.
+        If you would like to avoid that, please set enableConfigCheck to false.
+      '' /. + file);
+  checkConfigLocation = file:
+    if lib.hasPrefix "/tmp/" file then
+      throw
+      "${logPrefix}: configuration file must not reside within /tmp - it won't be visible to the systemd service."
+    else
+      true;
+  checkConfig = file:
+    pkgs.runCommand "checked-blackbox-exporter.conf" {
+      preferLocalBuild = true;
+      buildInputs = [ pkgs.buildPackages.prometheus-blackbox-exporter ];
+    } ''
+      ln -s ${coerceConfigFile file} $out
+      blackbox_exporter --config.check --config.file $out
+    '';
+in {
+  port = 9115;
+  extraOpts = {
+    configFile = mkOption {
+      type = types.path;
+      description = ''
+        Path to configuration file.
+      '';
+    };
+    enableConfigCheck = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to run a correctness check for the configuration file. This depends
+        on the configuration file residing in the nix-store. Paths passed as string will
+        be copied to the store.
+      '';
+    };
+  };
+
+  serviceOpts = let
+    adjustedConfigFile = if cfg.enableConfigCheck then
+      checkConfig cfg.configFile
+    else
+      checkConfigLocation cfg.configFile;
+  in {
+    serviceConfig = {
+      AmbientCapabilities = [ "CAP_NET_RAW" ]; # for ping probes
+      ExecStart = ''
+        ${pkgs.prometheus-blackbox-exporter}/bin/blackbox_exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --config.file ${escapeShellArg adjustedConfigFile} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+      ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/buildkite-agent.nix b/nixos/modules/services/monitoring/prometheus/exporters/buildkite-agent.nix
new file mode 100644
index 00000000000..e9be39608fc
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/buildkite-agent.nix
@@ -0,0 +1,64 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.buildkite-agent;
+in
+{
+  port = 9876;
+  extraOpts = {
+    tokenPath = mkOption {
+      type = types.nullOr types.path;
+      apply = final: if final == null then null else toString final;
+      description = ''
+        The token from your Buildkite "Agents" page.
+
+        A run-time path to the token file, which is supposed to be provisioned
+        outside of Nix store.
+      '';
+    };
+    interval = mkOption {
+      type = types.str;
+      default = "30s";
+      example = "1min";
+      description = ''
+        How often to update metrics.
+      '';
+    };
+    endpoint = mkOption {
+      type = types.str;
+      default = "https://agent.buildkite.com/v3";
+      description = ''
+        The Buildkite Agent API endpoint.
+      '';
+    };
+    queues = mkOption {
+      type = with types; nullOr (listOf str);
+      default = null;
+      example = literalExpression ''[ "my-queue1" "my-queue2" ]'';
+      description = ''
+        Which specific queues to process.
+      '';
+    };
+  };
+  serviceOpts = {
+    script =
+      let
+        queues = concatStringsSep " " (map (q: "-queue ${q}") cfg.queues);
+      in
+      ''
+        export BUILDKITE_AGENT_TOKEN="$(cat ${toString cfg.tokenPath})"
+        exec ${pkgs.buildkite-agent-metrics}/bin/buildkite-agent-metrics \
+          -backend prometheus \
+          -interval ${cfg.interval} \
+          -endpoint ${cfg.endpoint} \
+          ${optionalString (cfg.queues != null) queues} \
+          -prometheus-addr "${cfg.listenAddress}:${toString cfg.port}" ${concatStringsSep " " cfg.extraFlags}
+      '';
+    serviceConfig = {
+      DynamicUser = false;
+      RuntimeDirectory = "buildkite-agent-metrics";
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
new file mode 100644
index 00000000000..a7f4d3e096f
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
@@ -0,0 +1,77 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.collectd;
+in
+{
+  port = 9103;
+  extraOpts = {
+    collectdBinary = {
+      enable = mkEnableOption "collectd binary protocol receiver";
+
+      authFile = mkOption {
+        default = null;
+        type = types.nullOr types.path;
+        description = "File mapping user names to pre-shared keys (passwords).";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 25826;
+        description = "Network address on which to accept collectd binary network packets.";
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = ''
+          Address to listen on for binary network packets.
+          '';
+      };
+
+      securityLevel = mkOption {
+        type = types.enum ["None" "Sign" "Encrypt"];
+        default = "None";
+        description = ''
+          Minimum required security level for accepted packets.
+        '';
+      };
+    };
+
+    logFormat = mkOption {
+      type = types.enum [ "logfmt" "json" ];
+      default = "logfmt";
+      example = "json";
+      description = ''
+        Set the log format.
+      '';
+    };
+
+    logLevel = mkOption {
+      type = types.enum ["debug" "info" "warn" "error" "fatal"];
+      default = "info";
+      description = ''
+        Only log messages with the given severity or above.
+      '';
+    };
+  };
+  serviceOpts = let
+    collectSettingsArgs = if (cfg.collectdBinary.enable) then ''
+      --collectd.listen-address ${cfg.collectdBinary.listenAddress}:${toString cfg.collectdBinary.port} \
+      --collectd.security-level ${cfg.collectdBinary.securityLevel} \
+    '' else "";
+  in {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-collectd-exporter}/bin/collectd_exporter \
+          --log.format ${escapeShellArg cfg.logFormat} \
+          --log.level ${cfg.logLevel} \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          ${collectSettingsArgs} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/dmarc.nix b/nixos/modules/services/monitoring/prometheus/exporters/dmarc.nix
new file mode 100644
index 00000000000..330610a15d9
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/dmarc.nix
@@ -0,0 +1,117 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.dmarc;
+
+  json = builtins.toJSON {
+    inherit (cfg) folders port;
+    listen_addr = cfg.listenAddress;
+    storage_path = "$STATE_DIRECTORY";
+    imap = (builtins.removeAttrs cfg.imap [ "passwordFile" ]) // { password = "$IMAP_PASSWORD"; use_ssl = true; };
+    poll_interval_seconds = cfg.pollIntervalSeconds;
+    deduplication_max_seconds = cfg.deduplicationMaxSeconds;
+    logging = {
+      version = 1;
+      disable_existing_loggers = false;
+    };
+  };
+in {
+  port = 9797;
+  extraOpts = {
+    imap = {
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = ''
+          Hostname of IMAP server to connect to.
+        '';
+      };
+      port = mkOption {
+        type = types.port;
+        default = 993;
+        description = ''
+          Port of the IMAP server to connect to.
+        '';
+      };
+      username = mkOption {
+        type = types.str;
+        example = "postmaster@example.org";
+        description = ''
+          Login username for the IMAP connection.
+        '';
+      };
+      passwordFile = mkOption {
+        type = types.str;
+        example = "/run/secrets/dovecot_pw";
+        description = ''
+          File containing the login password for the IMAP connection.
+        '';
+      };
+    };
+    folders = {
+      inbox = mkOption {
+        type = types.str;
+        default = "INBOX";
+        description = ''
+          IMAP mailbox that is checked for incoming DMARC aggregate reports
+        '';
+      };
+      done = mkOption {
+        type = types.str;
+        default = "Archive";
+        description = ''
+          IMAP mailbox that successfully processed reports are moved to.
+        '';
+      };
+      error = mkOption {
+        type = types.str;
+        default = "Invalid";
+        description = ''
+          IMAP mailbox that emails are moved to that could not be processed.
+        '';
+      };
+    };
+    pollIntervalSeconds = mkOption {
+      type = types.ints.unsigned;
+      default = 60;
+      description = ''
+        How often to poll the IMAP server in seconds.
+      '';
+    };
+    deduplicationMaxSeconds = mkOption {
+      type = types.ints.unsigned;
+      default = 604800;
+      defaultText = "7 days (in seconds)";
+      description = ''
+        How long individual report IDs will be remembered to avoid
+        counting double delivered reports twice.
+      '';
+    };
+    debug = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to declare enable <literal>--debug</literal>.
+      '';
+    };
+  };
+  serviceOpts = {
+    path = with pkgs; [ envsubst coreutils ];
+    serviceConfig = {
+      StateDirectory = "prometheus-dmarc-exporter";
+      WorkingDirectory = "/var/lib/prometheus-dmarc-exporter";
+      ExecStart = "${pkgs.writeShellScript "setup-cfg" ''
+        export IMAP_PASSWORD="$(<${cfg.imap.passwordFile})"
+        envsubst \
+          -i ${pkgs.writeText "dmarc-exporter.json.template" json} \
+          -o ''${STATE_DIRECTORY}/dmarc-exporter.json
+
+        exec ${pkgs.prometheus-dmarc-exporter}/bin/prometheus-dmarc-exporter \
+          --configuration /var/lib/prometheus-dmarc-exporter/dmarc-exporter.json \
+          ${optionalString cfg.debug "--debug"}
+      ''}";
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/dnsmasq.nix b/nixos/modules/services/monitoring/prometheus/exporters/dnsmasq.nix
new file mode 100644
index 00000000000..68afba21d64
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/dnsmasq.nix
@@ -0,0 +1,38 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.dnsmasq;
+in
+{
+  port = 9153;
+  extraOpts = {
+    dnsmasqListenAddress = mkOption {
+      type = types.str;
+      default = "localhost:53";
+      description = ''
+        Address on which dnsmasq listens.
+      '';
+    };
+    leasesPath = mkOption {
+      type = types.path;
+      default = "/var/lib/misc/dnsmasq.leases";
+      example = "/var/lib/dnsmasq/dnsmasq.leases";
+      description = ''
+        Path to the <literal>dnsmasq.leases</literal> file.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-dnsmasq-exporter}/bin/dnsmasq_exporter \
+          --listen ${cfg.listenAddress}:${toString cfg.port} \
+          --dnsmasq ${cfg.dnsmasqListenAddress} \
+          --leases_path ${escapeShellArg cfg.leasesPath} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/domain.nix b/nixos/modules/services/monitoring/prometheus/exporters/domain.nix
new file mode 100644
index 00000000000..61e2fc80afd
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/domain.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.domain;
+in
+{
+  port = 9222;
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-domain-exporter}/bin/domain_exporter \
+          --bind ${cfg.listenAddress}:${toString cfg.port} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix b/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix
new file mode 100644
index 00000000000..092ac6fea7d
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix
@@ -0,0 +1,92 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.dovecot;
+in
+{
+  port = 9166;
+  extraOpts = {
+    telemetryPath = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = ''
+        Path under which to expose metrics.
+      '';
+    };
+    socketPath = mkOption {
+      type = types.path;
+      default = "/var/run/dovecot/stats";
+      example = "/var/run/dovecot2/old-stats";
+      description = ''
+        Path under which the stats socket is placed.
+        The user/group under which the exporter runs,
+        should be able to access the socket in order
+        to scrape the metrics successfully.
+
+        Please keep in mind that the stats module has changed in
+        <link xlink:href="https://wiki2.dovecot.org/Upgrading/2.3">Dovecot 2.3+</link> which
+        is not <link xlink:href="https://github.com/kumina/dovecot_exporter/issues/8">compatible with this exporter</link>.
+
+        The following extra config has to be passed to Dovecot to ensure that recent versions
+        work with this exporter:
+        <programlisting>
+        {
+          <xref linkend="opt-services.prometheus.exporters.dovecot.enable" /> = true;
+          <xref linkend="opt-services.prometheus.exporters.dovecot.socketPath" /> = "/var/run/dovecot2/old-stats";
+          <xref linkend="opt-services.dovecot2.mailPlugins.globally.enable" /> = [ "old_stats" ];
+          <xref linkend="opt-services.dovecot2.extraConfig" /> = '''
+            service old-stats {
+              unix_listener old-stats {
+                user = dovecot-exporter
+                group = dovecot-exporter
+                mode = 0660
+              }
+              fifo_listener old-stats-mail {
+                mode = 0660
+                user = dovecot
+                group = dovecot
+              }
+              fifo_listener old-stats-user {
+                mode = 0660
+                user = dovecot
+                group = dovecot
+              }
+            }
+            plugin {
+              old_stats_refresh = 30 secs
+              old_stats_track_cmds = yes
+            }
+          ''';
+        }
+        </programlisting>
+      '';
+    };
+    scopes = mkOption {
+      type = types.listOf types.str;
+      default = [ "user" ];
+      example = [ "user" "global" ];
+      description = ''
+        Stats scopes to query.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      DynamicUser = false;
+      ExecStart = ''
+        ${pkgs.prometheus-dovecot-exporter}/bin/dovecot_exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --web.telemetry-path ${cfg.telemetryPath} \
+          --dovecot.socket-path ${escapeShellArg cfg.socketPath} \
+          --dovecot.scopes ${concatStringsSep "," cfg.scopes} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+      RestrictAddressFamilies = [
+        # Need AF_UNIX to collect data
+        "AF_UNIX"
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/fastly.nix b/nixos/modules/services/monitoring/prometheus/exporters/fastly.nix
new file mode 100644
index 00000000000..55a61c4949e
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/fastly.nix
@@ -0,0 +1,41 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let cfg = config.services.prometheus.exporters.fastly;
+in
+{
+  port = 9118;
+  extraOpts = {
+    debug = mkEnableOption "Debug logging mode for fastly-exporter";
+
+    configFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Path to a fastly-exporter configuration file.
+        Example one can be generated with <literal>fastly-exporter --config-file-example</literal>.
+      '';
+      example = "./fastly-exporter-config.txt";
+    };
+
+    tokenPath = mkOption {
+      type = types.nullOr types.path;
+      apply = final: if final == null then null else toString final;
+      description = ''
+        A run-time path to the token file, which is supposed to be provisioned
+        outside of Nix store.
+      '';
+    };
+  };
+  serviceOpts = {
+    script = ''
+      ${optionalString (cfg.tokenPath != null)
+      "export FASTLY_API_TOKEN=$(cat ${toString cfg.tokenPath})"}
+      ${pkgs.prometheus-fastly-exporter}/bin/fastly-exporter \
+        -listen http://${cfg.listenAddress}:${toString cfg.port}
+        ${optionalString cfg.debug "-debug true"} \
+        ${optionalString (cfg.configFile != null) "-config-file ${cfg.configFile}"}
+    '';
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/flow.nix b/nixos/modules/services/monitoring/prometheus/exporters/flow.nix
new file mode 100644
index 00000000000..b85e5461f21
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/flow.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.flow;
+in {
+  port = 9590;
+  extraOpts = {
+    brokers = mkOption {
+      type = types.listOf types.str;
+      example = literalExpression ''[ "kafka.example.org:19092" ]'';
+      description = "List of Kafka brokers to connect to.";
+    };
+
+    asn = mkOption {
+      type = types.ints.positive;
+      example = 65542;
+      description = "The ASN being monitored.";
+    };
+
+    partitions = mkOption {
+      type = types.listOf types.int;
+      default = [];
+      description = ''
+        The number of the partitions to consume, none means all.
+      '';
+    };
+
+    topic = mkOption {
+      type = types.str;
+      example = "pmacct.acct";
+      description = "The Kafka topic to consume from.";
+    };
+  };
+
+  serviceOpts = {
+    serviceConfig = {
+      DynamicUser = true;
+      ExecStart = ''
+        ${pkgs.prometheus-flow-exporter}/bin/flow-exporter \
+          -asn ${toString cfg.asn} \
+          -topic ${cfg.topic} \
+          -brokers ${concatStringsSep "," cfg.brokers} \
+          ${optionalString (cfg.partitions != []) "-partitions ${concatStringsSep "," cfg.partitions}"} \
+          -addr ${cfg.listenAddress}:${toString cfg.port} ${concatStringsSep " " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/fritzbox.nix b/nixos/modules/services/monitoring/prometheus/exporters/fritzbox.nix
new file mode 100644
index 00000000000..9526597b8c9
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/fritzbox.nix
@@ -0,0 +1,38 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.fritzbox;
+in
+{
+  port = 9133;
+  extraOpts = {
+    gatewayAddress = mkOption {
+      type = types.str;
+      default = "fritz.box";
+      description = ''
+        The hostname or IP of the FRITZ!Box.
+      '';
+    };
+
+    gatewayPort = mkOption {
+      type = types.int;
+      default = 49000;
+      description = ''
+        The port of the FRITZ!Box UPnP service.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-fritzbox-exporter}/bin/exporter \
+          -listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          -gateway-address ${cfg.gatewayAddress} \
+          -gateway-port ${toString cfg.gatewayPort} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/influxdb.nix b/nixos/modules/services/monitoring/prometheus/exporters/influxdb.nix
new file mode 100644
index 00000000000..ba45173e946
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/influxdb.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.influxdb;
+in
+{
+  port = 9122;
+  extraOpts = {
+    sampleExpiry = mkOption {
+      type = types.str;
+      default = "5m";
+      example = "10m";
+      description = "How long a sample is valid for";
+    };
+    udpBindAddress = mkOption {
+      type = types.str;
+      default = ":9122";
+      example = "192.0.2.1:9122";
+      description = "Address on which to listen for udp packets";
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      RuntimeDirectory = "prometheus-influxdb-exporter";
+      ExecStart = ''
+        ${pkgs.prometheus-influxdb-exporter}/bin/influxdb_exporter \
+        --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+        --influxdb.sample-expiry ${cfg.sampleExpiry} ${concatStringsSep " " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/jitsi.nix b/nixos/modules/services/monitoring/prometheus/exporters/jitsi.nix
new file mode 100644
index 00000000000..c93a8f98e55
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/jitsi.nix
@@ -0,0 +1,40 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.jitsi;
+in
+{
+  port = 9700;
+  extraOpts = {
+    url = mkOption {
+      type = types.str;
+      default = "http://localhost:8080/colibri/stats";
+      description = ''
+        Jitsi Videobridge metrics URL to monitor.
+        This is usually /colibri/stats on port 8080 of the jitsi videobridge host.
+      '';
+    };
+    interval = mkOption {
+      type = types.str;
+      default = "30s";
+      example = "1min";
+      description = ''
+        How often to scrape new data
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-jitsi-exporter}/bin/jitsiexporter \
+          -url ${escapeShellArg cfg.url} \
+          -host ${cfg.listenAddress} \
+          -port ${toString cfg.port} \
+          -interval ${toString cfg.interval} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/json.nix b/nixos/modules/services/monitoring/prometheus/exporters/json.nix
new file mode 100644
index 00000000000..1800da69a25
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/json.nix
@@ -0,0 +1,43 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.json;
+in
+{
+  port = 7979;
+  extraOpts = {
+    configFile = mkOption {
+      type = types.path;
+      description = ''
+        Path to configuration file.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-json-exporter}/bin/json_exporter \
+          --config.file ${escapeShellArg cfg.configFile} \
+          --web.listen-address="${cfg.listenAddress}:${toString cfg.port}" \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+  imports = [
+    (mkRemovedOptionModule [ "url" ] ''
+      This option was removed. The URL of the endpoint serving JSON
+      must now be provided to the exporter by prometheus via the url
+      parameter `target'.
+
+      In prometheus a scrape URL would look like this:
+
+        http://some.json-exporter.host:7979/probe?target=https://example.com/some/json/endpoint
+
+      For more information, take a look at the official documentation
+      (https://github.com/prometheus-community/json_exporter) of the json_exporter.
+    '')
+     ({ options.warnings = options.warnings; options.assertions = options.assertions; })
+  ];
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/kea.nix b/nixos/modules/services/monitoring/prometheus/exporters/kea.nix
new file mode 100644
index 00000000000..27aeb909624
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/kea.nix
@@ -0,0 +1,43 @@
+{ config
+, lib
+, pkgs
+, options
+}:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.kea;
+in {
+  port = 9547;
+  extraOpts = {
+    controlSocketPaths = mkOption {
+      type = types.listOf types.str;
+      example = literalExpression ''
+        [
+          "/run/kea/kea-dhcp4.socket"
+          "/run/kea/kea-dhcp6.socket"
+        ]
+      '';
+      description = ''
+        Paths to kea control sockets
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      User = "kea";
+      ExecStart = ''
+        ${pkgs.prometheus-kea-exporter}/bin/kea-exporter \
+          --address ${cfg.listenAddress} \
+          --port ${toString cfg.port} \
+          ${concatStringsSep " \\n" cfg.controlSocketPaths}
+      '';
+      SupplementaryGroups = [ "kea" ];
+      RestrictAddressFamilies = [
+        # Need AF_UNIX to collect data
+        "AF_UNIX"
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/keylight.nix b/nixos/modules/services/monitoring/prometheus/exporters/keylight.nix
new file mode 100644
index 00000000000..dfa56343b87
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/keylight.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.keylight;
+in
+{
+  port = 9288;
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-keylight-exporter}/bin/keylight_exporter \
+          -metrics.addr ${cfg.listenAddress}:${toString cfg.port} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/knot.nix b/nixos/modules/services/monitoring/prometheus/exporters/knot.nix
new file mode 100644
index 00000000000..29e543f1013
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/knot.nix
@@ -0,0 +1,54 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.knot;
+in {
+  port = 9433;
+  extraOpts = {
+    knotLibraryPath = mkOption {
+      type = types.str;
+      default = "${pkgs.knot-dns.out}/lib/libknot.so";
+      defaultText = literalExpression ''"''${pkgs.knot-dns.out}/lib/libknot.so"'';
+      description = ''
+        Path to the library of <package>knot-dns</package>.
+      '';
+    };
+
+    knotSocketPath = mkOption {
+      type = types.str;
+      default = "/run/knot/knot.sock";
+      description = ''
+        Socket path of <citerefentry><refentrytitle>knotd</refentrytitle>
+        <manvolnum>8</manvolnum></citerefentry>.
+      '';
+    };
+
+    knotSocketTimeout = mkOption {
+      type = types.int;
+      default = 2000;
+      description = ''
+        Timeout in seconds.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-knot-exporter}/bin/knot_exporter \
+          --web-listen-addr ${cfg.listenAddress} \
+          --web-listen-port ${toString cfg.port} \
+          --knot-library-path ${cfg.knotLibraryPath} \
+          --knot-socket-path ${cfg.knotSocketPath} \
+          --knot-socket-timeout ${toString cfg.knotSocketTimeout} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+      SupplementaryGroups = [ "knot" ];
+      RestrictAddressFamilies = [
+        # Need AF_UNIX to collect data
+        "AF_UNIX"
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/lnd.nix b/nixos/modules/services/monitoring/prometheus/exporters/lnd.nix
new file mode 100644
index 00000000000..35f97202057
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/lnd.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.lnd;
+in
+{
+  port = 9092;
+  extraOpts = {
+    lndHost = mkOption {
+      type = types.str;
+      default = "localhost:10009";
+      description = ''
+        lnd instance gRPC address:port.
+      '';
+    };
+
+    lndTlsPath = mkOption {
+      type = types.path;
+      description = ''
+        Path to lnd TLS certificate.
+      '';
+    };
+
+    lndMacaroonDir = mkOption {
+      type = types.path;
+      description = ''
+        Path to lnd macaroons.
+      '';
+    };
+  };
+  serviceOpts.serviceConfig = {
+    ExecStart = ''
+      ${pkgs.prometheus-lnd-exporter}/bin/lndmon \
+        --prometheus.listenaddr=${cfg.listenAddress}:${toString cfg.port} \
+        --prometheus.logdir=/var/log/prometheus-lnd-exporter \
+        --lnd.host=${cfg.lndHost} \
+        --lnd.tlspath=${cfg.lndTlsPath} \
+        --lnd.macaroondir=${cfg.lndMacaroonDir} \
+        ${concatStringsSep " \\\n  " cfg.extraFlags}
+    '';
+    LogsDirectory = "prometheus-lnd-exporter";
+    ReadOnlyPaths = [ cfg.lndTlsPath cfg.lndMacaroonDir ];
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/mail.nix b/nixos/modules/services/monitoring/prometheus/exporters/mail.nix
new file mode 100644
index 00000000000..956bd96aa45
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/mail.nix
@@ -0,0 +1,176 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.mail;
+
+  configurationFile = pkgs.writeText "prometheus-mail-exporter.conf" (builtins.toJSON (
+    # removes the _module attribute, null values and converts attrNames to lowercase
+    mapAttrs' (name: value:
+      if name == "servers"
+      then nameValuePair (toLower name)
+        ((map (srv: (mapAttrs' (n: v: nameValuePair (toLower n) v)
+          (filterAttrs (n: v: !(n == "_module" || v == null)) srv)
+        ))) value)
+      else nameValuePair (toLower name) value
+    ) (filterAttrs (n: _: !(n == "_module")) cfg.configuration)
+  ));
+
+  serverOptions.options = {
+    name = mkOption {
+      type = types.str;
+      description = ''
+        Value for label 'configname' which will be added to all metrics.
+      '';
+    };
+    server = mkOption {
+      type = types.str;
+      description = ''
+        Hostname of the server that should be probed.
+      '';
+    };
+    port = mkOption {
+      type = types.int;
+      example = 587;
+      description = ''
+        Port to use for SMTP.
+      '';
+    };
+    from = mkOption {
+      type = types.str;
+      example = "exporteruser@domain.tld";
+      description = ''
+        Content of 'From' Header for probing mails.
+      '';
+    };
+    to = mkOption {
+      type = types.str;
+      example = "exporteruser@domain.tld";
+      description = ''
+        Content of 'To' Header for probing mails.
+      '';
+    };
+    detectionDir = mkOption {
+      type = types.path;
+      example = "/var/spool/mail/exporteruser/new";
+      description = ''
+        Directory in which new mails for the exporter user are placed.
+        Note that this needs to exist when the exporter starts.
+      '';
+    };
+    login = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "exporteruser@domain.tld";
+      description = ''
+        Username to use for SMTP authentication.
+      '';
+    };
+    passphrase = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Password to use for SMTP authentication.
+      '';
+    };
+  };
+
+  exporterOptions.options = {
+    monitoringInterval = mkOption {
+      type = types.str;
+      example = "10s";
+      description = ''
+        Time interval between two probe attempts.
+      '';
+    };
+    mailCheckTimeout = mkOption {
+      type = types.str;
+      description = ''
+        Timeout until mails are considered "didn't make it".
+      '';
+    };
+    disableFileDeletion = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Disables the exporter's function to delete probing mails.
+      '';
+    };
+    servers = mkOption {
+      type = types.listOf (types.submodule serverOptions);
+      default = [];
+      example = literalExpression ''
+        [ {
+          name = "testserver";
+          server = "smtp.domain.tld";
+          port = 587;
+          from = "exporteruser@domain.tld";
+          to = "exporteruser@domain.tld";
+          detectionDir = "/path/to/Maildir/new";
+        } ]
+      '';
+      description = ''
+        List of servers that should be probed.
+
+        <emphasis>Note:</emphasis> if your mailserver has <citerefentry>
+        <refentrytitle>rspamd</refentrytitle><manvolnum>8</manvolnum></citerefentry> configured,
+        it can happen that emails from this exporter are marked as spam.
+
+        It's possible to work around the issue with a config like this:
+        <programlisting>
+        {
+          <link linkend="opt-services.rspamd.locals._name_.text">services.rspamd.locals."multimap.conf".text</link> = '''
+            ALLOWLIST_PROMETHEUS {
+              filter = "email:domain:tld";
+              type = "from";
+              map = "''${pkgs.writeText "allowmap" "domain.tld"}";
+              score = -100.0;
+            }
+          ''';
+        }
+        </programlisting>
+      '';
+    };
+  };
+in
+{
+  port = 9225;
+  extraOpts = {
+    configFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Specify the mailexporter configuration file to use.
+      '';
+    };
+    configuration = mkOption {
+      type = types.nullOr (types.submodule exporterOptions);
+      default = null;
+      description = ''
+        Specify the mailexporter configuration file to use.
+      '';
+    };
+    telemetryPath = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = ''
+        Path under which to expose metrics.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      DynamicUser = false;
+      ExecStart = ''
+        ${pkgs.prometheus-mail-exporter}/bin/mailexporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --web.telemetry-path ${cfg.telemetryPath} \
+          --config.file ${
+            if cfg.configuration != null then configurationFile else (escapeShellArg cfg.configFile)
+          } \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/mikrotik.nix b/nixos/modules/services/monitoring/prometheus/exporters/mikrotik.nix
new file mode 100644
index 00000000000..8f9536b702a
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/mikrotik.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.mikrotik;
+in
+{
+  port = 9436;
+  extraOpts = {
+    configFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Path to a mikrotik exporter configuration file. Mutually exclusive with
+        <option>configuration</option> option.
+      '';
+      example = literalExpression "./mikrotik.yml";
+    };
+
+    configuration = mkOption {
+      type = types.nullOr types.attrs;
+      default = null;
+      description = ''
+        Mikrotik exporter configuration as nix attribute set. Mutually exclusive with
+        <option>configFile</option> option.
+
+        See <link xlink:href="https://github.com/nshttpd/mikrotik-exporter/blob/master/README.md"/>
+        for the description of the configuration file format.
+      '';
+      example = literalExpression ''
+        {
+          devices = [
+            {
+              name = "my_router";
+              address = "10.10.0.1";
+              user = "prometheus";
+              password = "changeme";
+            }
+          ];
+          features = {
+            bgp = true;
+            dhcp = true;
+            routes = true;
+            optics = true;
+          };
+        }
+      '';
+    };
+  };
+  serviceOpts = let
+    configFile = if cfg.configFile != null
+                 then cfg.configFile
+                 else "${pkgs.writeText "mikrotik-exporter.yml" (builtins.toJSON cfg.configuration)}";
+    in {
+    serviceConfig = {
+      # -port is misleading name, it actually accepts address too
+      ExecStart = ''
+        ${pkgs.prometheus-mikrotik-exporter}/bin/mikrotik-exporter \
+          -config-file=${escapeShellArg configFile} \
+          -port=${cfg.listenAddress}:${toString cfg.port} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/minio.nix b/nixos/modules/services/monitoring/prometheus/exporters/minio.nix
new file mode 100644
index 00000000000..d6dd62f871b
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/minio.nix
@@ -0,0 +1,64 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.minio;
+in
+{
+  port = 9290;
+  extraOpts = {
+    minioAddress = mkOption {
+      type = types.str;
+      example = "https://10.0.0.1:9000";
+      description = ''
+        The URL of the minio server.
+        Use HTTPS if Minio accepts secure connections only.
+        By default this connects to the local minio server if enabled.
+      '';
+    };
+
+    minioAccessKey = mkOption {
+      type = types.str;
+      example = "yourMinioAccessKey";
+      description = ''
+        The value of the Minio access key.
+        It is required in order to connect to the server.
+        By default this uses the one from the local minio server if enabled
+        and <literal>config.services.minio.accessKey</literal>.
+      '';
+    };
+
+    minioAccessSecret = mkOption {
+      type = types.str;
+      description = ''
+        The value of the Minio access secret.
+        It is required in order to connect to the server.
+        By default this uses the one from the local minio server if enabled
+        and <literal>config.services.minio.secretKey</literal>.
+      '';
+    };
+
+    minioBucketStats = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Collect statistics about the buckets and files in buckets.
+        It requires more computation, use it carefully in case of large buckets..
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-minio-exporter}/bin/minio-exporter \
+          -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          -minio.server ${cfg.minioAddress} \
+          -minio.access-key ${escapeShellArg cfg.minioAccessKey} \
+          -minio.access-secret ${escapeShellArg cfg.minioAccessSecret} \
+          ${optionalString cfg.minioBucketStats "-minio.bucket-stats"} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/modemmanager.nix b/nixos/modules/services/monitoring/prometheus/exporters/modemmanager.nix
new file mode 100644
index 00000000000..afd03f6c270
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/modemmanager.nix
@@ -0,0 +1,37 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.modemmanager;
+in
+{
+  port = 9539;
+  extraOpts = {
+    refreshRate = mkOption {
+      type = types.str;
+      default = "5s";
+      description = ''
+        How frequently ModemManager will refresh the extended signal quality
+        information for each modem. The duration should be specified in seconds
+        ("5s"), minutes ("1m"), or hours ("1h").
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      # Required in order to authenticate with ModemManager via D-Bus.
+      SupplementaryGroups = "networkmanager";
+      ExecStart = ''
+        ${pkgs.prometheus-modemmanager-exporter}/bin/modemmanager_exporter \
+          -addr ${cfg.listenAddress}:${toString cfg.port} \
+          -rate ${cfg.refreshRate} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+      RestrictAddressFamilies = [
+        # Need AF_UNIX to collect data
+        "AF_UNIX"
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix b/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix
new file mode 100644
index 00000000000..ce7125bf5a8
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix
@@ -0,0 +1,58 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.nextcloud;
+in
+{
+  port = 9205;
+  extraOpts = {
+    url = mkOption {
+      type = types.str;
+      example = "https://domain.tld";
+      description = ''
+        URL to the Nextcloud serverinfo page.
+        Adding the path to the serverinfo API is optional, it defaults
+        to <literal>/ocs/v2.php/apps/serverinfo/api/v1/info</literal>.
+      '';
+    };
+    username = mkOption {
+      type = types.str;
+      default = "nextcloud-exporter";
+      description = ''
+        Username for connecting to Nextcloud.
+        Note that this account needs to have admin privileges in Nextcloud.
+      '';
+    };
+    passwordFile = mkOption {
+      type = types.path;
+      example = "/path/to/password-file";
+      description = ''
+        File containing the password for connecting to Nextcloud.
+        Make sure that this file is readable by the exporter user.
+      '';
+    };
+    timeout = mkOption {
+      type = types.str;
+      default = "5s";
+      description = ''
+        Timeout for getting server info document.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      DynamicUser = false;
+      ExecStart = ''
+        ${pkgs.prometheus-nextcloud-exporter}/bin/nextcloud-exporter \
+          --addr ${cfg.listenAddress}:${toString cfg.port} \
+          --username ${cfg.username} \
+          --timeout ${cfg.timeout} \
+          --server ${cfg.url} \
+          --password ${escapeShellArg "@${cfg.passwordFile}"} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix b/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix
new file mode 100644
index 00000000000..6f69f5919d1
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix
@@ -0,0 +1,68 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.nginx;
+in
+{
+  port = 9113;
+  extraOpts = {
+    scrapeUri = mkOption {
+      type = types.str;
+      default = "http://localhost/nginx_status";
+      description = ''
+        Address to access the nginx status page.
+        Can be enabled with services.nginx.statusPage = true.
+      '';
+    };
+    telemetryPath = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = ''
+        Path under which to expose metrics.
+      '';
+    };
+    sslVerify = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to perform certificate verification for https.
+      '';
+    };
+    constLabels = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [
+        "label1=value1"
+        "label2=value2"
+      ];
+      description = ''
+        A list of constant labels that will be used in every metric.
+      '';
+    };
+  };
+  serviceOpts = mkMerge ([{
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-nginx-exporter}/bin/nginx-prometheus-exporter \
+          --nginx.scrape-uri='${cfg.scrapeUri}' \
+          --nginx.ssl-verify=${boolToString cfg.sslVerify} \
+          --web.listen-address=${cfg.listenAddress}:${toString cfg.port} \
+          --web.telemetry-path=${cfg.telemetryPath} \
+          --prometheus.const-labels=${concatStringsSep "," cfg.constLabels} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  }] ++ [(mkIf config.services.nginx.enable {
+    after = [ "nginx.service" ];
+    requires = [ "nginx.service" ];
+  })]);
+  imports = [
+    (mkRenamedOptionModule [ "telemetryEndpoint" ] [ "telemetryPath" ])
+    (mkRemovedOptionModule [ "insecure" ] ''
+      This option was replaced by 'prometheus.exporters.nginx.sslVerify'.
+    '')
+    ({ options.warnings = options.warnings; options.assertions = options.assertions; })
+  ];
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix b/nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix
new file mode 100644
index 00000000000..8c1f552d58a
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix
@@ -0,0 +1,51 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.nginxlog;
+in {
+  port = 9117;
+  extraOpts = {
+    settings = mkOption {
+      type = types.attrs;
+      default = {};
+      description = ''
+        All settings of nginxlog expressed as an Nix attrset.
+
+        Check the official documentation for the corresponding YAML
+        settings that can all be used here: https://github.com/martin-helmich/prometheus-nginxlog-exporter
+
+        The `listen` object is already generated by `port`, `listenAddress` and `metricsEndpoint` and
+        will be merged with the value of `settings` before writting it as JSON.
+      '';
+    };
+
+    metricsEndpoint = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = ''
+        Path under which to expose metrics.
+      '';
+    };
+  };
+
+  serviceOpts = let
+    listenConfig = {
+      listen = {
+        port = cfg.port;
+        address = cfg.listenAddress;
+        metrics_endpoint = cfg.metricsEndpoint;
+      };
+    };
+    completeConfig = pkgs.writeText "nginxlog-exporter.yaml" (builtins.toJSON (lib.recursiveUpdate listenConfig cfg.settings));
+  in {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-nginxlog-exporter}/bin/prometheus-nginxlog-exporter -config-file ${completeConfig}
+      '';
+      Restart="always";
+      ProtectSystem="full";
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/node.nix b/nixos/modules/services/monitoring/prometheus/exporters/node.nix
new file mode 100644
index 00000000000..5e5fc7cd552
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/node.nix
@@ -0,0 +1,49 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.node;
+in
+{
+  port = 9100;
+  extraOpts = {
+    enabledCollectors = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "systemd" ];
+      description = ''
+        Collectors to enable. The collectors listed here are enabled in addition to the default ones.
+      '';
+    };
+    disabledCollectors = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "timex" ];
+      description = ''
+        Collectors to disable which are enabled by default.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      DynamicUser = false;
+      RuntimeDirectory = "prometheus-node-exporter";
+      ExecStart = ''
+        ${pkgs.prometheus-node-exporter}/bin/node_exporter \
+          ${concatMapStringsSep " " (x: "--collector." + x) cfg.enabledCollectors} \
+          ${concatMapStringsSep " " (x: "--no-collector." + x) cfg.disabledCollectors} \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} ${concatStringsSep " " cfg.extraFlags}
+      '';
+      RestrictAddressFamilies = optionals (any (collector: (collector == "logind" || collector == "systemd")) cfg.enabledCollectors) [
+        # needs access to dbus via unix sockets (logind/systemd)
+        "AF_UNIX"
+      ] ++ optionals (any (collector: (collector == "network_route" || collector == "wifi")) cfg.enabledCollectors) [
+        # needs netlink sockets for wireless collector
+        "AF_NETLINK"
+      ];
+      # The timex collector needs to access clock APIs
+      ProtectClock = any (collector: collector == "timex") cfg.disabledCollectors;
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/openldap.nix b/nixos/modules/services/monitoring/prometheus/exporters/openldap.nix
new file mode 100644
index 00000000000..888611ee6fa
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/openldap.nix
@@ -0,0 +1,67 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.openldap;
+in {
+  port = 9330;
+  extraOpts = {
+    ldapCredentialFile = mkOption {
+      type = types.path;
+      example = "/run/keys/ldap_pass";
+      description = ''
+        Environment file to contain the credentials to authenticate against
+        <package>openldap</package>.
+
+        The file should look like this:
+        <programlisting>
+        ---
+        ldapUser: "cn=monitoring,cn=Monitor"
+        ldapPass: "secret"
+        </programlisting>
+      '';
+    };
+    protocol = mkOption {
+      default = "tcp";
+      example = "udp";
+      type = types.str;
+      description = ''
+        Which protocol to use to connect against <package>openldap</package>.
+      '';
+    };
+    ldapAddr = mkOption {
+      default = "localhost:389";
+      type = types.str;
+      description = ''
+        Address of the <package>openldap</package>-instance.
+      '';
+    };
+    metricsPath = mkOption {
+      default = "/metrics";
+      type = types.str;
+      description = ''
+        URL path where metrics should be exposed.
+      '';
+    };
+    interval = mkOption {
+      default = "30s";
+      type = types.str;
+      example = "1m";
+      description = ''
+        Scrape interval of the exporter.
+      '';
+    };
+  };
+  serviceOpts.serviceConfig = {
+    ExecStart = ''
+      ${pkgs.prometheus-openldap-exporter}/bin/openldap_exporter \
+        --promAddr ${cfg.listenAddress}:${toString cfg.port} \
+        --metrPath ${cfg.metricsPath} \
+        --ldapNet ${cfg.protocol} \
+        --interval ${cfg.interval} \
+        --config ${cfg.ldapCredentialFile} \
+        ${concatStringsSep " \\\n  " cfg.extraFlags}
+    '';
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/openvpn.nix b/nixos/modules/services/monitoring/prometheus/exporters/openvpn.nix
new file mode 100644
index 00000000000..a97a753ebc3
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/openvpn.nix
@@ -0,0 +1,39 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.openvpn;
+in {
+  port = 9176;
+  extraOpts = {
+    statusPaths = mkOption {
+      type = types.listOf types.str;
+      description = ''
+        Paths to OpenVPN status files. Please configure the OpenVPN option
+        <literal>status</literal> accordingly.
+      '';
+    };
+    telemetryPath = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = ''
+        Path under which to expose metrics.
+      '';
+    };
+  };
+
+  serviceOpts = {
+    serviceConfig = {
+      PrivateDevices = true;
+      ProtectKernelModules = true;
+      NoNewPrivileges = true;
+      ExecStart = ''
+        ${pkgs.prometheus-openvpn-exporter}/bin/openvpn_exporter \
+          -openvpn.status_paths "${concatStringsSep "," cfg.statusPaths}" \
+          -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          -web.telemetry-path ${cfg.telemetryPath}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix b/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix
new file mode 100644
index 00000000000..4bc27ebc32f
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix
@@ -0,0 +1,74 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.pihole;
+in
+{
+  port = 9617;
+  extraOpts = {
+    apiToken = mkOption {
+      type = types.str;
+      default = "";
+      example = "580a770cb40511eb85290242ac130003580a770cb40511eb85290242ac130003";
+      description = ''
+        pi-hole API token which can be used instead of a password
+      '';
+    };
+    interval = mkOption {
+      type = types.str;
+      default = "10s";
+      example = "30s";
+      description = ''
+        How often to scrape new data
+      '';
+    };
+    password = mkOption {
+      type = types.str;
+      default = "";
+      example = "password";
+      description = ''
+        The password to login into pihole. An api token can be used instead.
+      '';
+    };
+    piholeHostname = mkOption {
+      type = types.str;
+      default = "pihole";
+      example = "127.0.0.1";
+      description = ''
+        Hostname or address where to find the pihole webinterface
+      '';
+    };
+    piholePort = mkOption {
+      type = types.port;
+      default = 80;
+      example = 443;
+      description = ''
+        The port pihole webinterface is reachable on
+      '';
+    };
+    protocol = mkOption {
+      type = types.enum [ "http" "https" ];
+      default = "http";
+      example = "https";
+      description = ''
+        The protocol which is used to connect to pihole
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.bash}/bin/bash -c "${pkgs.prometheus-pihole-exporter}/bin/pihole-exporter \
+          -interval ${cfg.interval} \
+          ${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}"
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix b/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix
new file mode 100644
index 00000000000..4d3c1fa267e
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix
@@ -0,0 +1,98 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.postfix;
+in
+{
+  port = 9154;
+  extraOpts = {
+    group = mkOption {
+      type = types.str;
+      description = ''
+        Group under which the postfix exporter shall be run.
+        It should match the group that is allowed to access the
+        <literal>showq</literal> socket in the <literal>queue/public/</literal> directory.
+        Defaults to <literal>services.postfix.setgidGroup</literal> when postfix is enabled.
+      '';
+    };
+    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/lib/postfix/queue/public/showq";
+      example = "/var/spool/postfix/public/showq";
+      description = ''
+        Path where Postfix places its showq socket.
+      '';
+    };
+    systemd = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable reading metrics from the systemd journal instead of from a logfile
+        '';
+      };
+      unit = mkOption {
+        type = types.str;
+        default = "postfix.service";
+        description = ''
+          Name of the postfix systemd unit.
+        '';
+      };
+      slice = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Name of the postfix systemd slice.
+          This overrides the <option>systemd.unit</option>.
+        '';
+      };
+      journalPath = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          Path to the systemd journal.
+        '';
+      };
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      DynamicUser = false;
+      # By default, each prometheus exporter only gets AF_INET & AF_INET6,
+      # but AF_UNIX is needed to read from the `showq`-socket.
+      RestrictAddressFamilies = [ "AF_UNIX" ];
+      ExecStart = ''
+        ${pkgs.prometheus-postfix-exporter}/bin/postfix_exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --web.telemetry-path ${cfg.telemetryPath} \
+          --postfix.showq_path ${escapeShellArg cfg.showqPath} \
+          ${concatStringsSep " \\\n  " (cfg.extraFlags
+          ++ optional cfg.systemd.enable "--systemd.enable"
+          ++ optional cfg.systemd.enable (if cfg.systemd.slice != null
+                                          then "--systemd.slice ${cfg.systemd.slice}"
+                                          else "--systemd.unit ${cfg.systemd.unit}")
+          ++ optional (cfg.systemd.enable && (cfg.systemd.journalPath != null))
+                       "--systemd.journal_path ${escapeShellArg cfg.systemd.journalPath}"
+          ++ optional (!cfg.systemd.enable) "--postfix.logfile_path ${escapeShellArg cfg.logfilePath}")}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix b/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix
new file mode 100644
index 00000000000..3f9a32ef399
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix
@@ -0,0 +1,88 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.postgres;
+in
+{
+  port = 9187;
+  extraOpts = {
+    telemetryPath = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = ''
+        Path under which to expose metrics.
+      '';
+    };
+    dataSourceName = mkOption {
+      type = types.str;
+      default = "user=postgres database=postgres host=/run/postgresql sslmode=disable";
+      example = "postgresql://username:password@localhost:5432/postgres?sslmode=disable";
+      description = ''
+        Accepts PostgreSQL URI form and key=value form arguments.
+      '';
+    };
+    runAsLocalSuperUser = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to run the exporter as the local 'postgres' super user.
+      '';
+    };
+
+    # TODO perhaps LoadCredential would be more appropriate
+    environmentFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/root/prometheus-postgres-exporter.env";
+      description = ''
+        Environment file as defined in <citerefentry>
+        <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+        </citerefentry>.
+
+        Secrets may be passed to the service without adding them to the
+        world-readable Nix store, by specifying placeholder variables as
+        the option value in Nix and setting these variables accordingly in the
+        environment file.
+
+        Environment variables from this file will be interpolated into the
+        config file using envsubst with this syntax:
+        <literal>$ENVIRONMENT ''${VARIABLE}</literal>
+
+        The main use is to set the DATA_SOURCE_NAME that contains the
+        postgres password
+
+        note that contents from this file will override dataSourceName
+        if you have set it from nix.
+
+        <programlisting>
+          # Content of the environment file
+          DATA_SOURCE_NAME=postgresql://username:password@localhost:5432/postgres?sslmode=disable
+        </programlisting>
+
+        Note that this file needs to be available on the host on which
+        this exporter is running.
+      '';
+    };
+
+  };
+  serviceOpts = {
+    environment.DATA_SOURCE_NAME = cfg.dataSourceName;
+    serviceConfig = {
+      DynamicUser = false;
+      User = mkIf cfg.runAsLocalSuperUser (mkForce "postgres");
+      EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
+      ExecStart = ''
+        ${pkgs.prometheus-postgres-exporter}/bin/postgres_exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --web.telemetry-path ${cfg.telemetryPath} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+      RestrictAddressFamilies = [
+        # Need AF_UNIX to collect data
+        "AF_UNIX"
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/process.nix b/nixos/modules/services/monitoring/prometheus/exporters/process.nix
new file mode 100644
index 00000000000..1e9c402fb55
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/process.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.process;
+  configFile = pkgs.writeText "process-exporter.yaml" (builtins.toJSON cfg.settings);
+in
+{
+  port = 9256;
+  extraOpts = {
+    settings.process_names = mkOption {
+      type = types.listOf types.anything;
+      default = [];
+      example = literalExpression ''
+        [
+          # Remove nix store path from process name
+          { name = "{{.Matches.Wrapped}} {{ .Matches.Args }}"; cmdline = [ "^/nix/store[^ ]*/(?P<Wrapped>[^ /]*) (?P<Args>.*)" ]; }
+        ]
+      '';
+      description = ''
+        All settings expressed as an Nix attrset.
+
+        Check the official documentation for the corresponding YAML
+        settings that can all be used here: <link xlink:href="https://github.com/ncabatoff/process-exporter" />
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      DynamicUser = false;
+      ExecStart = ''
+        ${pkgs.prometheus-process-exporter}/bin/process-exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --config.path ${configFile} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+      NoNewPrivileges = true;
+      ProtectHome = true;
+      ProtectSystem = true;
+      ProtectKernelTunables = true;
+      ProtectKernelModules = true;
+      ProtectControlGroups = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/pve.nix b/nixos/modules/services/monitoring/prometheus/exporters/pve.nix
new file mode 100644
index 00000000000..ef708414c95
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/pve.nix
@@ -0,0 +1,118 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+let
+  cfg = config.services.prometheus.exporters.pve;
+
+  # pve exporter requires a config file so create an empty one if configFile is not provided
+  emptyConfigFile = pkgs.writeTextFile {
+    name = "pve.yml";
+    text = "default:";
+  };
+
+  computedConfigFile = "${if cfg.configFile == null then emptyConfigFile else cfg.configFile}";
+in
+{
+  port = 9221;
+  extraOpts = {
+    package = mkOption {
+      type = types.package;
+      default = pkgs.prometheus-pve-exporter;
+      defaultText = literalExpression "pkgs.prometheus-pve-exporter";
+      example = literalExpression "pkgs.prometheus-pve-exporter";
+      description = ''
+        The package to use for prometheus-pve-exporter
+      '';
+    };
+
+    environmentFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = "/etc/prometheus-pve-exporter/pve.env";
+      description = ''
+        Path to the service's environment file. This path can either be a computed path in /nix/store or a path in the local filesystem.
+
+        The environment file should NOT be stored in /nix/store as it contains passwords and/or keys in plain text.
+
+        Environment reference: https://github.com/prometheus-pve/prometheus-pve-exporter#authentication
+      '';
+    };
+
+    configFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = "/etc/prometheus-pve-exporter/pve.yml";
+      description = ''
+        Path to the service's config file. This path can either be a computed path in /nix/store or a path in the local filesystem.
+
+        The config file should NOT be stored in /nix/store as it will contain passwords and/or keys in plain text.
+
+        If both configFile and environmentFile are provided, the configFile option will be ignored.
+
+        Configuration reference: https://github.com/prometheus-pve/prometheus-pve-exporter/#authentication
+      '';
+    };
+
+    collectors = {
+      status = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Collect Node/VM/CT status
+        '';
+      };
+      version = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Collect PVE version info
+        '';
+      };
+      node = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Collect PVE node info
+        '';
+      };
+      cluster = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Collect PVE cluster info
+        '';
+      };
+      resources = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Collect PVE resources info
+        '';
+      };
+      config = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Collect PVE onboot status
+        '';
+      };
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${cfg.package}/bin/pve_exporter \
+          --${if cfg.collectors.status == true then "" else "no-"}collector.status \
+          --${if cfg.collectors.version == true then "" else "no-"}collector.version \
+          --${if cfg.collectors.node == true then "" else "no-"}collector.node \
+          --${if cfg.collectors.cluster == true then "" else "no-"}collector.cluster \
+          --${if cfg.collectors.resources == true then "" else "no-"}collector.resources \
+          --${if cfg.collectors.config == true then "" else "no-"}collector.config \
+          ${computedConfigFile} \
+          ${toString cfg.port} ${cfg.listenAddress}
+      '';
+    } // optionalAttrs (cfg.environmentFile != null) {
+          EnvironmentFile = cfg.environmentFile;
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix b/nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix
new file mode 100644
index 00000000000..d9ab99221d9
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix
@@ -0,0 +1,53 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.py-air-control;
+
+  workingDir = "/var/lib/${cfg.stateDir}";
+
+in
+{
+  port = 9896;
+  extraOpts = {
+    deviceHostname = mkOption {
+      type = types.str;
+      example = "192.168.1.123";
+      description = ''
+        The hostname of the air purification device from which to scrape the metrics.
+      '';
+    };
+    protocol = mkOption {
+      type = types.str;
+      default = "http";
+      description = ''
+        The protocol to use when communicating with the air purification device.
+        Available: [http, coap, plain_coap]
+      '';
+    };
+    stateDir = mkOption {
+      type = types.str;
+      default = "prometheus-py-air-control-exporter";
+      description = ''
+        Directory below <literal>/var/lib</literal> to store runtime data.
+        This directory will be created automatically using systemd's StateDirectory mechanism.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      DynamicUser = false;
+      StateDirectory = cfg.stateDir;
+      WorkingDirectory = workingDir;
+      ExecStart = ''
+        ${pkgs.python3Packages.py-air-control-exporter}/bin/py-air-control-exporter \
+          --host ${cfg.deviceHostname} \
+          --protocol ${cfg.protocol} \
+          --listen-port ${toString cfg.port} \
+          --listen-address ${cfg.listenAddress}
+      '';
+      Environment = [ "HOME=${workingDir}" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/redis.nix b/nixos/modules/services/monitoring/prometheus/exporters/redis.nix
new file mode 100644
index 00000000000..befbcb21f76
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/redis.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.redis;
+in
+{
+  port = 9121;
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-redis-exporter}/bin/redis_exporter \
+          -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix b/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix
new file mode 100644
index 00000000000..ed985751e42
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix
@@ -0,0 +1,97 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.rspamd;
+
+  mkFile = conf:
+    pkgs.writeText "rspamd-exporter-config.yml" (builtins.toJSON conf);
+
+  generateConfig = extraLabels: {
+    metrics = (map (path: {
+      name = "rspamd_${replaceStrings [ "[" "." " " "]" "\\" "'" ] [ "_" "_" "_" "" "" "" ] path}";
+      path = "{ .${path} }";
+      labels = extraLabels;
+    }) [
+      "actions['add\\ header']"
+      "actions['no\\ action']"
+      "actions['rewrite\\ subject']"
+      "actions['soft\\ reject']"
+      "actions.greylist"
+      "actions.reject"
+      "bytes_allocated"
+      "chunks_allocated"
+      "chunks_freed"
+      "chunks_oversized"
+      "connections"
+      "control_connections"
+      "ham_count"
+      "learned"
+      "pools_allocated"
+      "pools_freed"
+      "read_only"
+      "scanned"
+      "shared_chunks_allocated"
+      "spam_count"
+      "total_learns"
+    ]) ++ [{
+      name = "rspamd_statfiles";
+      type = "object";
+      path = "{.statfiles[*]}";
+      labels = recursiveUpdate {
+        symbol = "{.symbol}";
+        type = "{.type}";
+      } extraLabels;
+      values = {
+        revision = "{.revision}";
+        size = "{.size}";
+        total = "{.total}";
+        used = "{.used}";
+        languages = "{.languages}";
+        users = "{.users}";
+      };
+    }];
+  };
+in
+{
+  port = 7980;
+  extraOpts = {
+    extraLabels = mkOption {
+      type = types.attrsOf types.str;
+      default = {
+        host = config.networking.hostName;
+      };
+      defaultText = literalExpression "{ host = config.networking.hostName; }";
+      example = literalExpression ''
+        {
+          host = config.networking.hostName;
+          custom_label = "some_value";
+        }
+      '';
+      description = "Set of labels added to each metric.";
+    };
+  };
+  serviceOpts.serviceConfig.ExecStart = ''
+    ${pkgs.prometheus-json-exporter}/bin/json_exporter \
+      --config.file ${mkFile (generateConfig cfg.extraLabels)} \
+      --web.listen-address "${cfg.listenAddress}:${toString cfg.port}" \
+      ${concatStringsSep " \\\n  " cfg.extraFlags}
+  '';
+
+  imports = [
+    (mkRemovedOptionModule [ "url" ] ''
+      This option was removed. The URL of the rspamd metrics endpoint
+      must now be provided to the exporter by prometheus via the url
+      parameter `target'.
+
+      In prometheus a scrape URL would look like this:
+
+        http://some.rspamd-exporter.host:7980/probe?target=http://some.rspamd.host:11334/stat
+
+      For more information, take a look at the official documentation
+      (https://github.com/prometheus-community/json_exporter) of the json_exporter.
+    '')
+     ({ options.warnings = options.warnings; options.assertions = options.assertions; })
+  ];
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix b/nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix
new file mode 100644
index 00000000000..ef829a1b7d0
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix
@@ -0,0 +1,83 @@
+{ config, lib, pkgs, options }:
+
+let
+  cfg = config.services.prometheus.exporters.rtl_433;
+in
+{
+  port = 9550;
+
+  extraOpts = let
+    mkMatcherOptionType = field: description: with lib.types;
+      listOf (submodule {
+        options = {
+          name = lib.mkOption {
+            type = str;
+            description = "Name to match.";
+          };
+          "${field}" = lib.mkOption {
+            type = int;
+            inherit description;
+          };
+          location = lib.mkOption {
+            type = str;
+            description = "Location to match.";
+          };
+        };
+      });
+  in
+  {
+    rtl433Flags = lib.mkOption {
+      type = lib.types.str;
+      default = "-C si";
+      example = "-C si -R 19";
+      description = ''
+        Flags passed verbatim to rtl_433 binary.
+        Having <literal>-C si</literal> (the default) is recommended since only Celsius temperatures are parsed.
+      '';
+    };
+    channels = lib.mkOption {
+      type = mkMatcherOptionType "channel" "Channel to match.";
+      default = [];
+      example = [
+        { name = "Acurite"; channel = 6543; location = "Kitchen"; }
+      ];
+      description = ''
+        List of channel matchers to export.
+      '';
+    };
+    ids = lib.mkOption {
+      type = mkMatcherOptionType "id" "ID to match.";
+      default = [];
+      example = [
+        { name = "Nexus"; id = 1; location = "Bedroom"; }
+      ];
+      description = ''
+        List of ID matchers to export.
+      '';
+    };
+  };
+
+  serviceOpts = {
+    serviceConfig = {
+      # rtl-sdr udev rules make supported USB devices +rw by plugdev.
+      SupplementaryGroups = "plugdev";
+      # rtl_433 needs rw access to the USB radio.
+      PrivateDevices = lib.mkForce false;
+      DeviceAllow = lib.mkForce "char-usb_device rw";
+      RestrictAddressFamilies = [ "AF_NETLINK" ];
+
+      ExecStart = let
+        matchers = (map (m:
+          "--channel_matcher '${m.name},${toString m.channel},${m.location}'"
+        ) cfg.channels) ++ (map (m:
+          "--id_matcher '${m.name},${toString m.id},${m.location}'"
+        ) cfg.ids); in ''
+        ${pkgs.prometheus-rtl_433-exporter}/bin/rtl_433_prometheus \
+          -listen ${cfg.listenAddress}:${toString cfg.port} \
+          -subprocess "${pkgs.rtl_433}/bin/rtl_433 -F json ${cfg.rtl433Flags}" \
+          ${lib.concatStringsSep " \\\n  " matchers} \
+          ${lib.concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/script.nix b/nixos/modules/services/monitoring/prometheus/exporters/script.nix
new file mode 100644
index 00000000000..a805a0ad335
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/script.nix
@@ -0,0 +1,64 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.script;
+  configFile = pkgs.writeText "script-exporter.yaml" (builtins.toJSON cfg.settings);
+in
+{
+  port = 9172;
+  extraOpts = {
+    settings.scripts = mkOption {
+      type = with types; listOf (submodule {
+        options = {
+          name = mkOption {
+            type = str;
+            example = "sleep";
+            description = "Name of the script.";
+          };
+          script = mkOption {
+            type = str;
+            example = "sleep 5";
+            description = "Shell script to execute when metrics are requested.";
+          };
+          timeout = mkOption {
+            type = nullOr int;
+            default = null;
+            example = 60;
+            description = "Optional timeout for the script in seconds.";
+          };
+        };
+      });
+      example = literalExpression ''
+        {
+          scripts = [
+            { name = "sleep"; script = "sleep 5"; }
+          ];
+        }
+      '';
+      description = ''
+        All settings expressed as an Nix attrset.
+
+        Check the official documentation for the corresponding YAML
+        settings that can all be used here: <link xlink:href="https://github.com/adhocteam/script_exporter#sample-configuration" />
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-script-exporter}/bin/script_exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --config.file ${configFile} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+      NoNewPrivileges = true;
+      ProtectHome = true;
+      ProtectSystem = "strict";
+      ProtectKernelTunables = true;
+      ProtectKernelModules = true;
+      ProtectControlGroups = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix
new file mode 100644
index 00000000000..bac98364538
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix
@@ -0,0 +1,75 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.smartctl;
+  format = pkgs.formats.yaml {};
+  configFile = format.generate "smartctl-exporter.yml" {
+    smartctl_exporter = {
+      bind_to = "${cfg.listenAddress}:${toString cfg.port}";
+      url_path = "/metrics";
+      smartctl_location = "${pkgs.smartmontools}/bin/smartctl";
+      collect_not_more_than_period = cfg.maxInterval;
+      devices = cfg.devices;
+    };
+  };
+in {
+  port = 9633;
+
+  extraOpts = {
+    devices = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = literalExpression ''
+        [ "/dev/sda", "/dev/nvme0n1" ];
+      '';
+      description = ''
+        Paths to the disks that will be monitored. Will autodiscover
+        all disks if none given.
+      '';
+    };
+    maxInterval = mkOption {
+      type = types.str;
+      default = "60s";
+      example = "2m";
+      description = ''
+        Interval that limits how often a disk can be queried.
+      '';
+    };
+  };
+
+  serviceOpts = {
+    serviceConfig = {
+      AmbientCapabilities = [
+        "CAP_SYS_RAWIO"
+        "CAP_SYS_ADMIN"
+      ];
+      CapabilityBoundingSet = [
+        "CAP_SYS_RAWIO"
+        "CAP_SYS_ADMIN"
+      ];
+      DevicePolicy = "closed";
+      DeviceAllow = lib.mkOverride 100 (
+        if cfg.devices != [] then
+          cfg.devices
+        else [
+          "block-blkext rw"
+          "block-sd rw"
+          "char-nvme rw"
+        ]
+      );
+      ExecStart = ''
+        ${pkgs.prometheus-smartctl-exporter}/bin/smartctl_exporter -config ${configFile}
+      '';
+      PrivateDevices = lib.mkForce false;
+      ProtectProc = "invisible";
+      ProcSubset = "pid";
+      SupplementaryGroups = [ "disk" ];
+      SystemCallFilter = [
+        "@system-service"
+        "~@privileged @resources"
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix b/nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix
new file mode 100644
index 00000000000..0181c341a7e
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix
@@ -0,0 +1,61 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.smokeping;
+  goDuration = types.mkOptionType {
+    name = "goDuration";
+    description = "Go duration (https://golang.org/pkg/time/#ParseDuration)";
+    check = x: types.str.check x && builtins.match "(-?[0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+" x != null;
+    inherit (types.str) merge;
+  };
+in
+{
+  port = 9374;
+  extraOpts = {
+    telemetryPath = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = ''
+        Path under which to expose metrics.
+      '';
+    };
+    pingInterval = mkOption {
+      type = goDuration;
+      default = "1s";
+      description = ''
+        Interval between pings.
+      '';
+    };
+    buckets = mkOption {
+      type = types.commas;
+      default = "5e-05,0.0001,0.0002,0.0004,0.0008,0.0016,0.0032,0.0064,0.0128,0.0256,0.0512,0.1024,0.2048,0.4096,0.8192,1.6384,3.2768,6.5536,13.1072,26.2144";
+      description = ''
+        List of buckets to use for the response duration histogram.
+      '';
+    };
+    hosts = mkOption {
+      type = with types; listOf str;
+      description = ''
+        List of endpoints to probe.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      AmbientCapabilities = [ "CAP_NET_RAW" ];
+      CapabilityBoundingSet = [ "CAP_NET_RAW" ];
+      ExecStart = ''
+        ${pkgs.prometheus-smokeping-prober}/bin/smokeping_prober \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --web.telemetry-path ${cfg.telemetryPath} \
+          --buckets ${cfg.buckets} \
+          --ping.interval ${cfg.pingInterval} \
+          --privileged \
+          ${concatStringsSep " \\\n  " cfg.extraFlags} \
+          ${concatStringsSep " " cfg.hosts}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/snmp.nix b/nixos/modules/services/monitoring/prometheus/exporters/snmp.nix
new file mode 100644
index 00000000000..de42663e67f
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/snmp.nix
@@ -0,0 +1,68 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.snmp;
+in
+{
+  port = 9116;
+  extraOpts = {
+    configurationPath = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Path to a snmp exporter configuration file. Mutually exclusive with 'configuration' option.
+      '';
+      example = literalExpression "./snmp.yml";
+    };
+
+    configuration = mkOption {
+      type = types.nullOr types.attrs;
+      default = null;
+      description = ''
+        Snmp exporter configuration as nix attribute set. Mutually exclusive with 'configurationPath' option.
+      '';
+      example = {
+        "default" = {
+          "version" = 2;
+          "auth" = {
+            "community" = "public";
+          };
+        };
+      };
+    };
+
+    logFormat = mkOption {
+      type = types.enum ["logfmt" "json"];
+      default = "logfmt";
+      description = ''
+        Output format of log messages.
+      '';
+    };
+
+    logLevel = mkOption {
+      type = types.enum ["debug" "info" "warn" "error"];
+      default = "info";
+      description = ''
+        Only log messages with the given severity or above.
+      '';
+    };
+  };
+  serviceOpts = let
+    configFile = if cfg.configurationPath != null
+                 then cfg.configurationPath
+                 else "${pkgs.writeText "snmp-exporter-conf.yml" (builtins.toJSON cfg.configuration)}";
+    in {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-snmp-exporter}/bin/snmp_exporter \
+          --config.file=${escapeShellArg configFile} \
+          --log.format=${escapeShellArg cfg.logFormat} \
+          --log.level=${cfg.logLevel} \
+          --web.listen-address=${cfg.listenAddress}:${toString cfg.port} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/sql.nix b/nixos/modules/services/monitoring/prometheus/exporters/sql.nix
new file mode 100644
index 00000000000..3496fd9541f
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/sql.nix
@@ -0,0 +1,108 @@
+{ config, lib, pkgs, options }:
+with lib;
+let
+  cfg = config.services.prometheus.exporters.sql;
+  cfgOptions = {
+    options = with types; {
+      jobs = mkOption {
+        type = attrsOf (submodule jobOptions);
+        default = { };
+        description = "An attrset of metrics scraping jobs to run.";
+      };
+    };
+  };
+  jobOptions = {
+    options = with types; {
+      interval = mkOption {
+        type = str;
+        description = ''
+          How often to run this job, specified in
+          <link xlink:href="https://golang.org/pkg/time/#ParseDuration">Go duration</link> format.
+        '';
+      };
+      connections = mkOption {
+        type = listOf str;
+        description = "A list of connection strings of the SQL servers to scrape metrics from";
+      };
+      startupSql = mkOption {
+        type = listOf str;
+        default = [];
+        description = "A list of SQL statements to execute once after making a connection.";
+      };
+      queries = mkOption {
+        type = attrsOf (submodule queryOptions);
+        description = "SQL queries to run.";
+      };
+    };
+  };
+  queryOptions = {
+    options = with types; {
+      help = mkOption {
+        type = nullOr str;
+        default = null;
+        description = "A human-readable description of this metric.";
+      };
+      labels = mkOption {
+        type = listOf str;
+        default = [ ];
+        description = "A set of columns that will be used as Prometheus labels.";
+      };
+      query = mkOption {
+        type = str;
+        description = "The SQL query to run.";
+      };
+      values = mkOption {
+        type = listOf str;
+        description = "A set of columns that will be used as values of this metric.";
+      };
+    };
+  };
+
+  configFile =
+    if cfg.configFile != null
+    then cfg.configFile
+    else
+      let
+        nameInline = mapAttrsToList (k: v: v // { name = k; });
+        renameStartupSql = j: removeAttrs (j // { startup_sql = j.startupSql; }) [ "startupSql" ];
+        configuration = {
+          jobs = map renameStartupSql
+            (nameInline (mapAttrs (k: v: (v // { queries = nameInline v.queries; })) cfg.configuration.jobs));
+        };
+      in
+      builtins.toFile "config.yaml" (builtins.toJSON configuration);
+in
+{
+  extraOpts = {
+    configFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      description = ''
+        Path to configuration file.
+      '';
+    };
+    configuration = mkOption {
+      type = with types; nullOr (submodule cfgOptions);
+      default = null;
+      description = ''
+        Exporter configuration as nix attribute set. Mutually exclusive with 'configFile' option.
+      '';
+    };
+  };
+
+  port = 9237;
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-sql-exporter}/bin/sql_exporter \
+          -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          -config.file ${configFile} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+      RestrictAddressFamilies = [
+        # Need AF_UNIX to collect data
+        "AF_UNIX"
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/surfboard.nix b/nixos/modules/services/monitoring/prometheus/exporters/surfboard.nix
new file mode 100644
index 00000000000..81c5c70ed93
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/surfboard.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.surfboard;
+in
+{
+  port = 9239;
+  extraOpts = {
+    modemAddress = mkOption {
+      type = types.str;
+      default = "192.168.100.1";
+      description = ''
+        The hostname or IP of the cable modem.
+      '';
+    };
+  };
+  serviceOpts = {
+    description = "Prometheus exporter for surfboard cable modem";
+    unitConfig.Documentation = "https://github.com/ipstatic/surfboard_exporter";
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-surfboard-exporter}/bin/surfboard_exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --modem-address ${cfg.modemAddress} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix b/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix
new file mode 100644
index 00000000000..2edd1de83e1
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix
@@ -0,0 +1,22 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let cfg = config.services.prometheus.exporters.systemd;
+
+in {
+  port = 9558;
+
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-systemd-exporter}/bin/systemd_exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} ${concatStringsSep " " cfg.extraFlags}
+      '';
+      RestrictAddressFamilies = [
+        # Need AF_UNIX to collect data
+        "AF_UNIX"
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/tor.nix b/nixos/modules/services/monitoring/prometheus/exporters/tor.nix
new file mode 100644
index 00000000000..36c473677ef
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/tor.nix
@@ -0,0 +1,44 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.tor;
+in
+{
+  port = 9130;
+  extraOpts = {
+    torControlAddress = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = ''
+        Tor control IP address or hostname.
+      '';
+    };
+
+    torControlPort = mkOption {
+      type = types.int;
+      default = 9051;
+      description = ''
+        Tor control port.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-tor-exporter}/bin/prometheus-tor-exporter \
+          -b ${cfg.listenAddress} \
+          -p ${toString cfg.port} \
+          -a ${cfg.torControlAddress} \
+          -c ${toString cfg.torControlPort} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+
+    # CPython requires a process to either have $HOME defined or run as a UID
+    # defined in /etc/passwd. The latter is false with DynamicUser, so define a
+    # dummy $HOME. https://bugs.python.org/issue10496
+    environment = { HOME = "/var/empty"; };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix b/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix
new file mode 100644
index 00000000000..cf0efddd340
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix
@@ -0,0 +1,63 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.unbound;
+in
+{
+  port = 9167;
+  extraOpts = {
+    fetchType = mkOption {
+      # TODO: add shm when upstream implemented it
+      type = types.enum [ "tcp" "uds" ];
+      default = "uds";
+      description = ''
+        Which methods the exporter uses to get the information from unbound.
+      '';
+    };
+
+    telemetryPath = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = ''
+        Path under which to expose metrics.
+      '';
+    };
+
+    controlInterface = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "/run/unbound/unbound.socket";
+      description = ''
+        Path to the unbound socket for uds mode or the control interface port for tcp mode.
+
+        Example:
+          uds-mode: /run/unbound/unbound.socket
+          tcp-mode: 127.0.0.1:8953
+      '';
+    };
+  };
+
+  serviceOpts = mkMerge ([{
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-unbound-exporter}/bin/unbound-telemetry \
+          ${cfg.fetchType} \
+          --bind ${cfg.listenAddress}:${toString cfg.port} \
+          --path ${cfg.telemetryPath} \
+          ${optionalString (cfg.controlInterface != null) "--control-interface ${cfg.controlInterface}"} \
+          ${toString cfg.extraFlags}
+      '';
+      RestrictAddressFamilies = [
+        # Need AF_UNIX to collect data
+        "AF_UNIX"
+      ];
+    };
+  }] ++ [
+    (mkIf config.services.unbound.enable {
+      after = [ "unbound.service" ];
+      requires = [ "unbound.service" ];
+    })
+  ]);
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix b/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix
new file mode 100644
index 00000000000..394e6e201f0
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.unifi-poller;
+
+  configFile = pkgs.writeText "prometheus-unifi-poller-exporter.json" (generators.toJSON {} {
+    poller = { inherit (cfg.log) debug quiet; };
+    unifi = { inherit (cfg) controllers; };
+    influxdb.disable = true;
+    prometheus = {
+      http_listen = "${cfg.listenAddress}:${toString cfg.port}";
+      report_errors = cfg.log.prometheusErrors;
+    };
+  });
+
+in {
+  port = 9130;
+
+  extraOpts = {
+    inherit (options.services.unifi-poller.unifi) controllers;
+    log = {
+      debug = mkEnableOption "debug logging including line numbers, high resolution timestamps, per-device logs.";
+      quiet = mkEnableOption "startup and error logs only.";
+      prometheusErrors = mkEnableOption "emitting errors to prometheus.";
+    };
+  };
+
+  serviceOpts.serviceConfig = {
+    ExecStart = "${pkgs.unifi-poller}/bin/unifi-poller --config ${configFile}";
+    DynamicUser = false;
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/unifi.nix b/nixos/modules/services/monitoring/prometheus/exporters/unifi.nix
new file mode 100644
index 00000000000..8d0e8764001
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/unifi.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.unifi;
+in
+{
+  port = 9130;
+  extraOpts = {
+    unifiAddress = mkOption {
+      type = types.str;
+      example = "https://10.0.0.1:8443";
+      description = ''
+        URL of the UniFi Controller API.
+      '';
+    };
+
+    unifiInsecure = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        If enabled skip the verification of the TLS certificate of the UniFi Controller API.
+        Use with caution.
+      '';
+    };
+
+    unifiUsername = mkOption {
+      type = types.str;
+      example = "ReadOnlyUser";
+      description = ''
+        username for authentication against UniFi Controller API.
+      '';
+    };
+
+    unifiPassword = mkOption {
+      type = types.str;
+      description = ''
+        Password for authentication against UniFi Controller API.
+      '';
+    };
+
+    unifiTimeout = mkOption {
+      type = types.str;
+      default = "5s";
+      example = "2m";
+      description = ''
+        Timeout including unit for UniFi Controller API requests.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-unifi-exporter}/bin/unifi_exporter \
+          -telemetry.addr ${cfg.listenAddress}:${toString cfg.port} \
+          -unifi.addr ${cfg.unifiAddress} \
+          -unifi.username ${escapeShellArg cfg.unifiUsername} \
+          -unifi.password ${escapeShellArg cfg.unifiPassword} \
+          -unifi.timeout ${cfg.unifiTimeout} \
+          ${optionalString cfg.unifiInsecure "-unifi.insecure" } \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/varnish.nix b/nixos/modules/services/monitoring/prometheus/exporters/varnish.nix
new file mode 100644
index 00000000000..5b5a6e18fcd
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/varnish.nix
@@ -0,0 +1,88 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.varnish;
+in
+{
+  port = 9131;
+  extraOpts = {
+    noExit = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Do not exit server on Varnish scrape errors.
+      '';
+    };
+    withGoMetrics = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Export go runtime and http handler metrics.
+      '';
+    };
+    verbose = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable verbose logging.
+      '';
+    };
+    raw = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable raw stdout logging without timestamps.
+      '';
+    };
+    varnishStatPath = mkOption {
+      type = types.str;
+      default = "varnishstat";
+      description = ''
+        Path to varnishstat.
+      '';
+    };
+    instance = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        varnishstat -n value.
+      '';
+    };
+    healthPath = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Path under which to expose healthcheck. Disabled unless configured.
+      '';
+    };
+    telemetryPath = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = ''
+        Path under which to expose metrics.
+      '';
+    };
+  };
+  serviceOpts = {
+    path = [ pkgs.varnish ];
+    serviceConfig = {
+      RestartSec = mkDefault 1;
+      DynamicUser = false;
+      ExecStart = ''
+        ${pkgs.prometheus-varnish-exporter}/bin/prometheus_varnish_exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --web.telemetry-path ${cfg.telemetryPath} \
+          --varnishstat-path ${escapeShellArg cfg.varnishStatPath} \
+          ${concatStringsSep " \\\n  " (cfg.extraFlags
+            ++ optional (cfg.healthPath != null) "--web.health-path ${cfg.healthPath}"
+            ++ optional (cfg.instance != null) "-n ${escapeShellArg cfg.instance}"
+            ++ optional cfg.noExit "--no-exit"
+            ++ optional cfg.withGoMetrics "--with-go-metrics"
+            ++ optional cfg.verbose "--verbose"
+            ++ optional cfg.raw "--raw")}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix b/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix
new file mode 100644
index 00000000000..d4aa69629ec
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix
@@ -0,0 +1,71 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.wireguard;
+in {
+  port = 9586;
+  imports = [
+    (mkRenamedOptionModule [ "addr" ] [ "listenAddress" ])
+    ({ options.warnings = options.warnings; options.assertions = options.assertions; })
+  ];
+  extraOpts = {
+    verbose = mkEnableOption "Verbose logging mode for prometheus-wireguard-exporter";
+
+    wireguardConfig = mkOption {
+      type = with types; nullOr (either path str);
+      default = null;
+
+      description = ''
+        Path to the Wireguard Config to
+        <link xlink:href="https://github.com/MindFlavor/prometheus_wireguard_exporter/tree/2.0.0#usage">add the peer's name to the stats of a peer</link>.
+
+        Please note that <literal>networking.wg-quick</literal> is required for this feature
+        as <literal>networking.wireguard</literal> uses
+        <citerefentry><refentrytitle>wg</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+        to set the peers up.
+      '';
+    };
+
+    singleSubnetPerField = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        By default, all allowed IPs and subnets are comma-separated in the
+        <literal>allowed_ips</literal> field. With this option enabled,
+        a single IP and subnet will be listed in fields like <literal>allowed_ip_0</literal>,
+        <literal>allowed_ip_1</literal> and so on.
+      '';
+    };
+
+    withRemoteIp = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether or not the remote IP of a WireGuard peer should be exposed via prometheus.
+      '';
+    };
+  };
+  serviceOpts = {
+    path = [ pkgs.wireguard-tools ];
+
+    serviceConfig = {
+      AmbientCapabilities = [ "CAP_NET_ADMIN" ];
+      CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
+      ExecStart = ''
+        ${pkgs.prometheus-wireguard-exporter}/bin/prometheus_wireguard_exporter \
+          -p ${toString cfg.port} \
+          -l ${cfg.listenAddress} \
+          ${optionalString cfg.verbose "-v"} \
+          ${optionalString cfg.singleSubnetPerField "-s"} \
+          ${optionalString cfg.withRemoteIp "-r"} \
+          ${optionalString (cfg.wireguardConfig != null) "-n ${escapeShellArg cfg.wireguardConfig}"}
+      '';
+      RestrictAddressFamilies = [
+        # Need AF_NETLINK to collect data
+        "AF_NETLINK"
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/pushgateway.nix b/nixos/modules/services/monitoring/prometheus/pushgateway.nix
new file mode 100644
index 00000000000..01b99376243
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/pushgateway.nix
@@ -0,0 +1,166 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.pushgateway;
+
+  cmdlineArgs =
+       opt "web.listen-address" cfg.web.listen-address
+    ++ opt "web.telemetry-path" cfg.web.telemetry-path
+    ++ opt "web.external-url" cfg.web.external-url
+    ++ opt "web.route-prefix" cfg.web.route-prefix
+    ++ optional cfg.persistMetrics ''--persistence.file="/var/lib/${cfg.stateDir}/metrics"''
+    ++ opt "persistence.interval" cfg.persistence.interval
+    ++ opt "log.level" cfg.log.level
+    ++ opt "log.format" cfg.log.format
+    ++ cfg.extraFlags;
+
+  opt = k : v : optional (v != null) ''--${k}="${v}"'';
+
+in {
+  options = {
+    services.prometheus.pushgateway = {
+      enable = mkEnableOption "Prometheus Pushgateway";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.prometheus-pushgateway;
+        defaultText = literalExpression "pkgs.prometheus-pushgateway";
+        description = ''
+          Package that should be used for the prometheus pushgateway.
+        '';
+      };
+
+      web.listen-address = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Address to listen on for the web interface, API and telemetry.
+
+          <literal>null</literal> will default to <literal>:9091</literal>.
+        '';
+      };
+
+      web.telemetry-path = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Path under which to expose metrics.
+
+          <literal>null</literal> will default to <literal>/metrics</literal>.
+        '';
+      };
+
+      web.external-url = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The URL under which Pushgateway is externally reachable.
+        '';
+      };
+
+      web.route-prefix = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Prefix for the internal routes of web endpoints.
+
+          Defaults to the path of
+          <option>services.prometheus.pushgateway.web.external-url</option>.
+        '';
+      };
+
+      persistence.interval = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "10m";
+        description = ''
+          The minimum interval at which to write out the persistence file.
+
+          <literal>null</literal> will default to <literal>5m</literal>.
+        '';
+      };
+
+      log.level = mkOption {
+        type = types.nullOr (types.enum ["debug" "info" "warn" "error" "fatal"]);
+        default = null;
+        description = ''
+          Only log messages with the given severity or above.
+
+          <literal>null</literal> will default to <literal>info</literal>.
+        '';
+      };
+
+      log.format = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "logger:syslog?appname=bob&local=7";
+        description = ''
+          Set the log target and format.
+
+          <literal>null</literal> will default to <literal>logger:stderr</literal>.
+        '';
+      };
+
+      extraFlags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Extra commandline options when launching the Pushgateway.
+        '';
+      };
+
+      persistMetrics = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to persist metrics to a file.
+
+          When enabled metrics will be saved to a file called
+          <literal>metrics</literal> in the directory
+          <literal>/var/lib/pushgateway</literal>. The directory below
+          <literal>/var/lib</literal> can be set using
+          <option>services.prometheus.pushgateway.stateDir</option>.
+        '';
+      };
+
+      stateDir = mkOption {
+        type = types.str;
+        default = "pushgateway";
+        description = ''
+          Directory below <literal>/var/lib</literal> to store metrics.
+
+          This directory will be created automatically using systemd's
+          StateDirectory mechanism when
+          <option>services.prometheus.pushgateway.persistMetrics</option>
+          is enabled.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !hasPrefix "/" cfg.stateDir;
+        message =
+          "The option services.prometheus.pushgateway.stateDir" +
+          " shouldn't be an absolute directory." +
+          " It should be a directory relative to /var/lib.";
+      }
+    ];
+    systemd.services.pushgateway = {
+      wantedBy = [ "multi-user.target" ];
+      after    = [ "network.target" ];
+      serviceConfig = {
+        Restart  = "always";
+        DynamicUser = true;
+        ExecStart = "${cfg.package}/bin/pushgateway" +
+          optionalString (length cmdlineArgs != 0) (" \\\n  " +
+            concatStringsSep " \\\n  " cmdlineArgs);
+        StateDirectory = if cfg.persistMetrics then cfg.stateDir else null;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix b/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix
new file mode 100644
index 00000000000..980c93c9c47
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix
@@ -0,0 +1,55 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.xmpp-alerts;
+  settingsFormat = pkgs.formats.yaml {};
+  configFile = settingsFormat.generate "prometheus-xmpp-alerts.yml" cfg.settings;
+in
+{
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "prometheus" "xmpp-alerts" "configuration" ]
+      [ "services" "prometheus" "xmpp-alerts" "settings" ])
+  ];
+
+  options.services.prometheus.xmpp-alerts = {
+    enable = mkEnableOption "XMPP Web hook service for Alertmanager";
+
+    settings = mkOption {
+      type = settingsFormat.type;
+      default = {};
+
+      description = ''
+        Configuration for prometheus xmpp-alerts, see
+        <link xlink:href="https://github.com/jelmer/prometheus-xmpp-alerts/blob/master/xmpp-alerts.yml.example"/>
+        for supported values.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.prometheus-xmpp-alerts = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.prometheus-xmpp-alerts}/bin/prometheus-xmpp-alerts --config ${configFile}";
+        Restart = "on-failure";
+        DynamicUser = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHome = true;
+        ProtectSystem = "strict";
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        NoNewPrivileges = true;
+        SystemCallArchitectures = "native";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        SystemCallFilter = [ "@system-service" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/riemann-dash.nix b/nixos/modules/services/monitoring/riemann-dash.nix
new file mode 100644
index 00000000000..16eb8300850
--- /dev/null
+++ b/nixos/modules/services/monitoring/riemann-dash.nix
@@ -0,0 +1,81 @@
+{ config, pkgs, lib, ... }:
+
+with pkgs;
+with lib;
+
+let
+
+  cfg = config.services.riemann-dash;
+
+  conf = writeText "config.rb" ''
+    riemann_base = "${cfg.dataDir}"
+    config.store[:ws_config] = "#{riemann_base}/config/config.json"
+    ${cfg.config}
+  '';
+
+  launcher = writeScriptBin "riemann-dash" ''
+    #!/bin/sh
+    exec ${pkgs.riemann-dash}/bin/riemann-dash ${conf}
+  '';
+
+in {
+
+  options = {
+
+    services.riemann-dash = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the riemann-dash dashboard daemon.
+        '';
+      };
+      config = mkOption {
+        type = types.lines;
+        description = ''
+          Contents added to the end of the riemann-dash configuration file.
+        '';
+      };
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/riemann-dash";
+        description = ''
+          Location of the riemann-base dir. The dashboard configuration file is
+          is stored to this directory. The directory is created automatically on
+          service start, and owner is set to the riemanndash user.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    users.groups.riemanndash.gid = config.ids.gids.riemanndash;
+
+    users.users.riemanndash = {
+      description = "riemann-dash daemon user";
+      uid = config.ids.uids.riemanndash;
+      group = "riemanndash";
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - riemanndash riemanndash - -"
+    ];
+
+    systemd.services.riemann-dash = {
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "riemann.service" ];
+      after = [ "riemann.service" ];
+      preStart = ''
+        mkdir -p '${cfg.dataDir}/config'
+      '';
+      serviceConfig = {
+        User = "riemanndash";
+        ExecStart = "${launcher}/bin/riemann-dash";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/monitoring/riemann-tools.nix b/nixos/modules/services/monitoring/riemann-tools.nix
new file mode 100644
index 00000000000..86a11694e7b
--- /dev/null
+++ b/nixos/modules/services/monitoring/riemann-tools.nix
@@ -0,0 +1,70 @@
+{ config, pkgs, lib, ... }:
+
+with pkgs;
+with lib;
+
+let
+
+  cfg = config.services.riemann-tools;
+
+  riemannHost = "${cfg.riemannHost}";
+
+  healthLauncher = writeScriptBin "riemann-health" ''
+    #!/bin/sh
+    exec ${pkgs.riemann-tools}/bin/riemann-health ${builtins.concatStringsSep " " cfg.extraArgs} --host ${riemannHost}
+  '';
+
+
+in {
+
+  options = {
+
+    services.riemann-tools = {
+      enableHealth = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the riemann-health daemon.
+        '';
+      };
+      riemannHost = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = ''
+          Address of the host riemann node. Defaults to localhost.
+        '';
+      };
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          A list of commandline-switches forwarded to a riemann-tool.
+          See for example `riemann-health --help` for available options.
+        '';
+        example = ["-p 5555" "--timeout=30" "--attribute=myattribute=42"];
+      };
+    };
+  };
+
+  config = mkIf cfg.enableHealth {
+
+    users.groups.riemanntools.gid = config.ids.gids.riemanntools;
+
+    users.users.riemanntools = {
+      description = "riemann-tools daemon user";
+      uid = config.ids.uids.riemanntools;
+      group = "riemanntools";
+    };
+
+    systemd.services.riemann-health = {
+      wantedBy = [ "multi-user.target" ];
+      path = [ procps ];
+      serviceConfig = {
+        User = "riemanntools";
+        ExecStart = "${healthLauncher}/bin/riemann-health";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/monitoring/riemann.nix b/nixos/modules/services/monitoring/riemann.nix
new file mode 100644
index 00000000000..13d2b1cc060
--- /dev/null
+++ b/nixos/modules/services/monitoring/riemann.nix
@@ -0,0 +1,105 @@
+{ config, pkgs, lib, ... }:
+
+with pkgs;
+with lib;
+
+let
+
+  cfg = config.services.riemann;
+
+  classpath = concatStringsSep ":" (
+    cfg.extraClasspathEntries ++ [ "${riemann}/share/java/riemann.jar" ]
+  );
+
+  riemannConfig = concatStringsSep "\n" (
+    [cfg.config] ++ (map (f: ''(load-file "${f}")'') cfg.configFiles)
+  );
+
+  launcher = writeScriptBin "riemann" ''
+    #!/bin/sh
+    exec ${jdk}/bin/java ${concatStringsSep " " cfg.extraJavaOpts} \
+      -cp ${classpath} \
+      riemann.bin ${cfg.configFile}
+  '';
+
+in {
+
+  options = {
+
+    services.riemann = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the Riemann network monitoring daemon.
+        '';
+      };
+      config = mkOption {
+        type = types.lines;
+        description = ''
+          Contents of the Riemann configuration file. For more complicated
+          config you should use configFile.
+        '';
+      };
+      configFiles = mkOption {
+        type = with types; listOf path;
+        default = [];
+        description = ''
+          Extra files containing Riemann configuration. These files will be
+          loaded at runtime by Riemann (with Clojure's
+          <literal>load-file</literal> function) at the end of the
+          configuration if you use the config option, this is ignored if you
+          use configFile.
+        '';
+      };
+      configFile = mkOption {
+        type = types.str;
+        description = ''
+          A Riemann config file. Any files in the same directory as this file
+          will be added to the classpath by Riemann.
+        '';
+      };
+      extraClasspathEntries = mkOption {
+        type = with types; listOf str;
+        default = [];
+        description = ''
+          Extra entries added to the Java classpath when running Riemann.
+        '';
+      };
+      extraJavaOpts = mkOption {
+        type = with types; listOf str;
+        default = [];
+        description = ''
+          Extra Java options used when launching Riemann.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users.groups.riemann.gid = config.ids.gids.riemann;
+
+    users.users.riemann = {
+      description = "riemann daemon user";
+      uid = config.ids.uids.riemann;
+      group = "riemann";
+    };
+
+    services.riemann.configFile = mkDefault (
+      writeText "riemann-config.clj" riemannConfig
+    );
+
+    systemd.services.riemann = {
+      wantedBy = [ "multi-user.target" ];
+      path = [ inetutils ];
+      serviceConfig = {
+        User = "riemann";
+        ExecStart = "${launcher}/bin/riemann";
+      };
+      serviceConfig.LimitNOFILE = 65536;
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/monitoring/scollector.nix b/nixos/modules/services/monitoring/scollector.nix
new file mode 100644
index 00000000000..6a6fe110f94
--- /dev/null
+++ b/nixos/modules/services/monitoring/scollector.nix
@@ -0,0 +1,134 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.scollector;
+
+  collectors = pkgs.runCommand "collectors" { preferLocalBuild = true; }
+    ''
+    mkdir -p $out
+    ${lib.concatStringsSep
+        "\n"
+        (lib.mapAttrsToList
+          (frequency: binaries:
+            "mkdir -p $out/${frequency}\n" +
+            (lib.concatStringsSep
+              "\n"
+              (map (path: "ln -s ${path} $out/${frequency}/$(basename ${path})")
+                   binaries)))
+          cfg.collectors)}
+    '';
+
+  conf = pkgs.writeText "scollector.toml" ''
+    Host = "${cfg.bosunHost}"
+    ColDir = "${collectors}"
+    ${cfg.extraConfig}
+  '';
+
+in {
+
+  options = {
+
+    services.scollector = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run scollector.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.scollector;
+        defaultText = literalExpression "pkgs.scollector";
+        description = ''
+          scollector binary to use.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "scollector";
+        description = ''
+          User account under which scollector runs.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "scollector";
+        description = ''
+          Group account under which scollector runs.
+        '';
+      };
+
+      bosunHost = mkOption {
+        type = types.str;
+        default = "localhost:8070";
+        description = ''
+          Host and port of the bosun server that will store the collected
+          data.
+        '';
+      };
+
+      collectors = mkOption {
+        type = with types; attrsOf (listOf path);
+        default = {};
+        example = literalExpression ''{ "0" = [ "''${postgresStats}/bin/collect-stats" ]; }'';
+        description = ''
+          An attribute set mapping the frequency of collection to a list of
+          binaries that should be executed at that frequency. You can use "0"
+          to run a binary forever.
+        '';
+      };
+
+      extraOpts = mkOption {
+        type = with types; listOf str;
+        default = [];
+        example = [ "-d" ];
+        description = ''
+          Extra scollector command line options
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra scollector configuration added to the end of scollector.toml
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf config.services.scollector.enable {
+
+    systemd.services.scollector = {
+      description = "scollector metrics collector (part of Bosun)";
+      wantedBy = [ "multi-user.target" ];
+
+      path = [ pkgs.coreutils pkgs.iproute2 ];
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.package}/bin/scollector -conf=${conf} ${lib.concatStringsSep " " cfg.extraOpts}";
+      };
+    };
+
+    users.users.scollector = {
+      description = "scollector user";
+      group = "scollector";
+      uid = config.ids.uids.scollector;
+    };
+
+    users.groups.scollector.gid = config.ids.gids.scollector;
+
+  };
+
+}
diff --git a/nixos/modules/services/monitoring/smartd.nix b/nixos/modules/services/monitoring/smartd.nix
new file mode 100644
index 00000000000..6d39cc3e4e6
--- /dev/null
+++ b/nixos/modules/services/monitoring/smartd.nix
@@ -0,0 +1,253 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  host = config.networking.hostName or "unknown"
+       + optionalString (config.networking.domain != null) ".${config.networking.domain}";
+
+  cfg = config.services.smartd;
+  opt = options.services.smartd;
+
+  nm = cfg.notifications.mail;
+  nw = cfg.notifications.wall;
+  nx = cfg.notifications.x11;
+
+  smartdNotify = pkgs.writeScript "smartd-notify.sh" ''
+    #! ${pkgs.runtimeShell}
+    ${optionalString nm.enable ''
+      {
+      ${pkgs.coreutils}/bin/cat << EOF
+      From: smartd on ${host} <${nm.sender}>
+      To: undisclosed-recipients:;
+      Subject: $SMARTD_SUBJECT
+
+      $SMARTD_FULLMESSAGE
+      EOF
+
+      ${pkgs.smartmontools}/sbin/smartctl -a -d "$SMARTD_DEVICETYPE" "$SMARTD_DEVICE"
+      } | ${nm.mailer} -i "${nm.recipient}"
+    ''}
+    ${optionalString nw.enable ''
+      {
+      ${pkgs.coreutils}/bin/cat << EOF
+      Problem detected with disk: $SMARTD_DEVICESTRING
+      Warning message from smartd is:
+
+      $SMARTD_MESSAGE
+      EOF
+      } | ${pkgs.util-linux}/bin/wall 2>/dev/null
+    ''}
+    ${optionalString nx.enable ''
+      export DISPLAY=${nx.display}
+      {
+      ${pkgs.coreutils}/bin/cat << EOF
+      Problem detected with disk: $SMARTD_DEVICESTRING
+      Warning message from smartd is:
+
+      $SMARTD_FULLMESSAGE
+      EOF
+      } | ${pkgs.xorg.xmessage}/bin/xmessage -file - 2>/dev/null &
+    ''}
+  '';
+
+  notifyOpts = optionalString (nm.enable || nw.enable || nx.enable)
+    ("-m <nomailer> -M exec ${smartdNotify} " + optionalString cfg.notifications.test "-M test ");
+
+  smartdConf = pkgs.writeText "smartd.conf" ''
+    # Autogenerated smartd startup config file
+    DEFAULT ${notifyOpts}${cfg.defaults.monitored}
+
+    ${concatMapStringsSep "\n" (d: "${d.device} ${d.options}") cfg.devices}
+
+    ${optionalString cfg.autodetect
+       "DEVICESCAN ${notifyOpts}${cfg.defaults.autodetected}"}
+  '';
+
+  smartdDeviceOpts = { ... }: {
+
+    options = {
+
+      device = mkOption {
+        example = "/dev/sda";
+        type = types.str;
+        description = "Location of the device.";
+      };
+
+      options = mkOption {
+        default = "";
+        example = "-d sat";
+        type = types.separatedString " ";
+        description = "Options that determine how smartd monitors the device.";
+      };
+
+    };
+
+  };
+
+in
+
+{
+  ###### interface
+
+  options = {
+
+    services.smartd = {
+
+      enable = mkEnableOption "smartd daemon from <literal>smartmontools</literal> package";
+
+      autodetect = mkOption {
+        default = true;
+        type = types.bool;
+        description = ''
+          Whenever smartd should monitor all devices connected to the
+          machine at the time it's being started (the default).
+
+          Set to false to monitor the devices listed in
+          <option>services.smartd.devices</option> only.
+        '';
+      };
+
+      extraOptions = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        example = ["-A /var/log/smartd/" "--interval=3600"];
+        description = ''
+          Extra command-line options passed to the <literal>smartd</literal>
+          daemon on startup.
+
+          (See <literal>man 8 smartd</literal>.)
+        '';
+      };
+
+      notifications = {
+
+        mail = {
+          enable = mkOption {
+            default = config.services.mail.sendmailSetuidWrapper != null;
+            defaultText = literalExpression "config.services.mail.sendmailSetuidWrapper != null";
+            type = types.bool;
+            description = "Whenever to send e-mail notifications.";
+          };
+
+          sender = mkOption {
+            default = "root";
+            example = "example@domain.tld";
+            type = types.str;
+            description = ''
+              Sender of the notification messages.
+              Acts as the value of <literal>email</literal> in the emails' <literal>From: ... </literal> field.
+            '';
+          };
+
+          recipient = mkOption {
+            default = "root";
+            type = types.str;
+            description = "Recipient of the notification messages.";
+          };
+
+          mailer = mkOption {
+            default = "/run/wrappers/bin/sendmail";
+            type = types.path;
+            description = ''
+              Sendmail-compatible binary to be used to send the messages.
+
+              You should probably enable
+              <option>services.postfix</option> or some other MTA for
+              this to work.
+            '';
+          };
+        };
+
+        wall = {
+          enable = mkOption {
+            default = true;
+            type = types.bool;
+            description = "Whenever to send wall notifications to all users.";
+          };
+        };
+
+        x11 = {
+          enable = mkOption {
+            default = config.services.xserver.enable;
+            defaultText = literalExpression "config.services.xserver.enable";
+            type = types.bool;
+            description = "Whenever to send X11 xmessage notifications.";
+          };
+
+          display = mkOption {
+            default = ":${toString config.services.xserver.display}";
+            defaultText = literalExpression ''":''${toString config.services.xserver.display}"'';
+            type = types.str;
+            description = "DISPLAY to send X11 notifications to.";
+          };
+        };
+
+        test = mkOption {
+          default = false;
+          type = types.bool;
+          description = "Whenever to send a test notification on startup.";
+        };
+
+      };
+
+      defaults = {
+        monitored = mkOption {
+          default = "-a";
+          type = types.separatedString " ";
+          example = "-a -o on -s (S/../.././02|L/../../7/04)";
+          description = ''
+            Common default options for explicitly monitored (listed in
+            <option>services.smartd.devices</option>) devices.
+
+            The default value turns on monitoring of all the things (see
+            <literal>man 5 smartd.conf</literal>).
+
+            The example also turns on SMART Automatic Offline Testing on
+            startup, and schedules short self-tests daily, and long
+            self-tests weekly.
+          '';
+        };
+
+        autodetected = mkOption {
+          default = cfg.defaults.monitored;
+          defaultText = literalExpression "config.${opt.defaults.monitored}";
+          type = types.separatedString " ";
+          description = ''
+            Like <option>services.smartd.defaults.monitored</option>, but for the
+            autodetected devices.
+          '';
+        };
+      };
+
+      devices = mkOption {
+        default = [];
+        example = [ { device = "/dev/sda"; } { device = "/dev/sdb"; options = "-d sat"; } ];
+        type = with types; listOf (submodule smartdDeviceOpts);
+        description = "List of devices to monitor.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [ {
+      assertion = cfg.autodetect || cfg.devices != [];
+      message = "smartd can't run with both disabled autodetect and an empty list of devices to monitor.";
+    } ];
+
+    systemd.services.smartd = {
+      description = "S.M.A.R.T. Daemon";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig.ExecStart = "${pkgs.smartmontools}/sbin/smartd ${lib.concatStringsSep " " cfg.extraOptions} --no-fork --configfile=${smartdConf}";
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/monitoring/statsd.nix b/nixos/modules/services/monitoring/statsd.nix
new file mode 100644
index 00000000000..30b2916a992
--- /dev/null
+++ b/nixos/modules/services/monitoring/statsd.nix
@@ -0,0 +1,149 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.statsd;
+
+  isBuiltinBackend = name:
+    builtins.elem name [ "graphite" "console" "repeater" ];
+
+  backendsToPackages = let
+    mkMap = list: name:
+      if isBuiltinBackend name then list
+      else list ++ [ pkgs.nodePackages.${name} ];
+  in foldl mkMap [];
+
+  configFile = pkgs.writeText "statsd.conf" ''
+    {
+      address: "${cfg.listenAddress}",
+      port: "${toString cfg.port}",
+      mgmt_address: "${cfg.mgmt_address}",
+      mgmt_port: "${toString cfg.mgmt_port}",
+      backends: [${
+        concatMapStringsSep "," (name:
+          if (isBuiltinBackend name)
+          then ''"./backends/${name}"''
+          else ''"${name}"''
+        ) cfg.backends}],
+      ${optionalString (cfg.graphiteHost!=null) ''graphiteHost: "${cfg.graphiteHost}",''}
+      ${optionalString (cfg.graphitePort!=null) ''graphitePort: "${toString cfg.graphitePort}",''}
+      console: {
+        prettyprint: false
+      },
+      log: {
+        backend: "stdout"
+      },
+      automaticConfigReload: false${optionalString (cfg.extraConfig != null) ","}
+      ${cfg.extraConfig}
+    }
+  '';
+
+  deps = pkgs.buildEnv {
+    name = "statsd-runtime-deps";
+    pathsToLink = [ "/lib" ];
+    ignoreCollisions = true;
+
+    paths = backendsToPackages cfg.backends;
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options.services.statsd = {
+
+    enable = mkEnableOption "statsd";
+
+    listenAddress = mkOption {
+      description = "Address that statsd listens on over UDP";
+      default = "127.0.0.1";
+      type = types.str;
+    };
+
+    port = mkOption {
+      description = "Port that stats listens for messages on over UDP";
+      default = 8125;
+      type = types.int;
+    };
+
+    mgmt_address = mkOption {
+      description = "Address to run management TCP interface on";
+      default = "127.0.0.1";
+      type = types.str;
+    };
+
+    mgmt_port = mkOption {
+      description = "Port to run the management TCP interface on";
+      default = 8126;
+      type = types.int;
+    };
+
+    backends = mkOption {
+      description = "List of backends statsd will use for data persistence";
+      default = [];
+      example = [
+        "graphite"
+        "console"
+        "repeater"
+        "statsd-librato-backend"
+        "stackdriver-statsd-backend"
+        "statsd-influxdb-backend"
+      ];
+      type = types.listOf types.str;
+    };
+
+    graphiteHost = mkOption {
+      description = "Hostname or IP of Graphite server";
+      default = null;
+      type = types.nullOr types.str;
+    };
+
+    graphitePort = mkOption {
+      description = "Port of Graphite server (i.e. carbon-cache).";
+      default = null;
+      type = types.nullOr types.int;
+    };
+
+    extraConfig = mkOption {
+      description = "Extra configuration options for statsd";
+      default = "";
+      type = types.nullOr types.str;
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = map (backend: {
+      assertion = !isBuiltinBackend backend -> hasAttrByPath [ backend ] pkgs.nodePackages;
+      message = "Only builtin backends (graphite, console, repeater) or backends enumerated in `pkgs.nodePackages` are allowed!";
+    }) cfg.backends;
+
+    users.users.statsd = {
+      uid = config.ids.uids.statsd;
+      description = "Statsd daemon user";
+    };
+
+    systemd.services.statsd = {
+      description = "Statsd Server";
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        NODE_PATH = "${deps}/lib/node_modules";
+      };
+      serviceConfig = {
+        ExecStart = "${pkgs.statsd}/bin/statsd ${configFile}";
+        User = "statsd";
+      };
+    };
+
+    environment.systemPackages = [ pkgs.statsd ];
+
+  };
+
+}
diff --git a/nixos/modules/services/monitoring/sysstat.nix b/nixos/modules/services/monitoring/sysstat.nix
new file mode 100644
index 00000000000..ca2cff82723
--- /dev/null
+++ b/nixos/modules/services/monitoring/sysstat.nix
@@ -0,0 +1,76 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.sysstat;
+in {
+  options = {
+    services.sysstat = {
+      enable = mkEnableOption "sar system activity collection";
+
+      collect-frequency = mkOption {
+        type = types.str;
+        default = "*:00/10";
+        description = ''
+          OnCalendar specification for sysstat-collect
+        '';
+      };
+
+      collect-args = mkOption {
+        type = types.str;
+        default = "1 1";
+        description = ''
+          Arguments to pass sa1 when collecting statistics
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.sysstat = {
+      description = "Resets System Activity Logs";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        User = "root";
+        RemainAfterExit = true;
+        Type = "oneshot";
+        ExecStart = "${pkgs.sysstat}/lib/sa/sa1 --boot";
+        LogsDirectory = "sa";
+      };
+    };
+
+    systemd.services.sysstat-collect = {
+      description = "system activity accounting tool";
+      unitConfig.Documentation = "man:sa1(8)";
+
+      serviceConfig = {
+        Type = "oneshot";
+        User = "root";
+        ExecStart = "${pkgs.sysstat}/lib/sa/sa1 ${cfg.collect-args}";
+      };
+    };
+
+    systemd.timers.sysstat-collect = {
+      description = "Run system activity accounting tool on a regular basis";
+      wantedBy = [ "timers.target" ];
+      timerConfig.OnCalendar = cfg.collect-frequency;
+    };
+
+    systemd.services.sysstat-summary = {
+      description = "Generate a daily summary of process accounting";
+      unitConfig.Documentation = "man:sa2(8)";
+
+      serviceConfig = {
+        Type = "oneshot";
+        User = "root";
+        ExecStart = "${pkgs.sysstat}/lib/sa/sa2 -A";
+      };
+    };
+
+    systemd.timers.sysstat-summary = {
+      description = "Generate summary of yesterday's process accounting";
+      wantedBy = [ "timers.target" ];
+      timerConfig.OnCalendar = "00:07:00";
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/teamviewer.nix b/nixos/modules/services/monitoring/teamviewer.nix
new file mode 100644
index 00000000000..e2271e571c4
--- /dev/null
+++ b/nixos/modules/services/monitoring/teamviewer.nix
@@ -0,0 +1,49 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.teamviewer;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.teamviewer.enable = mkEnableOption "TeamViewer daemon";
+
+  };
+
+  ###### implementation
+
+  config = mkIf (cfg.enable) {
+
+    environment.systemPackages = [ pkgs.teamviewer ];
+
+    services.dbus.packages = [ pkgs.teamviewer ];
+
+    systemd.services.teamviewerd = {
+      description = "TeamViewer remote control daemon";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "NetworkManager-wait-online.service" "network.target" "dbus.service" ];
+      requires = [ "dbus.service" ];
+      preStart = "mkdir -pv /var/lib/teamviewer /var/log/teamviewer";
+
+      startLimitIntervalSec = 60;
+      startLimitBurst = 10;
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${pkgs.teamviewer}/bin/teamviewerd -f";
+        PIDFile = "/run/teamviewerd.pid";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        Restart = "on-abort";
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/services/monitoring/telegraf.nix b/nixos/modules/services/monitoring/telegraf.nix
new file mode 100644
index 00000000000..13aae58d0f3
--- /dev/null
+++ b/nixos/modules/services/monitoring/telegraf.nix
@@ -0,0 +1,90 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.telegraf;
+
+  settingsFormat = pkgs.formats.toml {};
+  configFile = settingsFormat.generate "config.toml" cfg.extraConfig;
+in {
+  ###### interface
+  options = {
+    services.telegraf = {
+      enable = mkEnableOption "telegraf server";
+
+      package = mkOption {
+        default = pkgs.telegraf;
+        defaultText = literalExpression "pkgs.telegraf";
+        description = "Which telegraf derivation to use";
+        type = types.package;
+      };
+
+      environmentFiles = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        example = [ "/run/keys/telegraf.env" ];
+        description = ''
+          File to load as environment file. Environment variables from this file
+          will be interpolated into the config file using envsubst with this
+          syntax: <literal>$ENVIRONMENT</literal> or <literal>''${VARIABLE}</literal>.
+          This is useful to avoid putting secrets into the nix store.
+        '';
+      };
+
+      extraConfig = mkOption {
+        default = {};
+        description = "Extra configuration options for telegraf";
+        type = settingsFormat.type;
+        example = {
+          outputs.influxdb = {
+            urls = ["http://localhost:8086"];
+            database = "telegraf";
+          };
+          inputs.statsd = {
+            service_address = ":8125";
+            delete_timings = true;
+          };
+        };
+      };
+    };
+  };
+
+
+  ###### implementation
+  config = mkIf config.services.telegraf.enable {
+    systemd.services.telegraf = let
+      finalConfigFile = if config.services.telegraf.environmentFiles == []
+                        then configFile
+                        else "/var/run/telegraf/config.toml";
+    in {
+      description = "Telegraf Agent";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      serviceConfig = {
+        EnvironmentFile = config.services.telegraf.environmentFiles;
+        ExecStartPre = lib.optional (config.services.telegraf.environmentFiles != [])
+          (pkgs.writeShellScript "pre-start" ''
+            umask 077
+            ${pkgs.envsubst}/bin/envsubst -i "${configFile}" > /var/run/telegraf/config.toml
+          '');
+        ExecStart="${cfg.package}/bin/telegraf -config ${finalConfigFile}";
+        ExecReload="${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        RuntimeDirectory = "telegraf";
+        User = "telegraf";
+        Group = "telegraf";
+        Restart = "on-failure";
+        # for ping probes
+        AmbientCapabilities = [ "CAP_NET_RAW" ];
+      };
+    };
+
+    users.users.telegraf = {
+      uid = config.ids.uids.telegraf;
+      group = "telegraf";
+      description = "telegraf daemon user";
+    };
+
+    users.groups.telegraf = {};
+  };
+}
diff --git a/nixos/modules/services/monitoring/thanos.nix b/nixos/modules/services/monitoring/thanos.nix
new file mode 100644
index 00000000000..9e93d8dbb0e
--- /dev/null
+++ b/nixos/modules/services/monitoring/thanos.nix
@@ -0,0 +1,838 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.thanos;
+
+  nullOpt = type: description: mkOption {
+    type = types.nullOr type;
+    default = null;
+    inherit description;
+  };
+
+  optionToArgs = opt: v  : optional (v != null)  ''--${opt}="${toString v}"'';
+  flagToArgs   = opt: v  : optional v            "--${opt}";
+  listToArgs   = opt: vs : map               (v: ''--${opt}="${v}"'') vs;
+  attrsToArgs  = opt: kvs: mapAttrsToList (k: v: ''--${opt}=${k}=\"${v}\"'') kvs;
+
+  mkParamDef = type: default: description: mkParam type (description + ''
+
+    Defaults to <literal>${toString default}</literal> in Thanos
+    when set to <literal>null</literal>.
+  '');
+
+  mkParam = type: description: {
+    toArgs = optionToArgs;
+    option = nullOpt type description;
+  };
+
+  mkFlagParam = description: {
+    toArgs = flagToArgs;
+    option = mkOption {
+      type = types.bool;
+      default = false;
+      inherit description;
+    };
+  };
+
+  mkListParam = opt: description: {
+    toArgs = _opt: listToArgs opt;
+    option = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      inherit description;
+    };
+  };
+
+  mkAttrsParam = opt: description: {
+    toArgs = _opt: attrsToArgs opt;
+    option = mkOption {
+      type = types.attrsOf types.str;
+      default = {};
+      inherit description;
+    };
+  };
+
+  mkStateDirParam = opt: default: description: {
+    toArgs = _opt: stateDir: optionToArgs opt "/var/lib/${stateDir}";
+    option = mkOption {
+      type = types.str;
+      inherit default;
+      inherit description;
+    };
+  };
+
+  toYAML = name: attrs: pkgs.runCommand name {
+    preferLocalBuild = true;
+    json = builtins.toFile "${name}.json" (builtins.toJSON attrs);
+    nativeBuildInputs = [ pkgs.remarshal ];
+  } "json2yaml -i $json -o $out";
+
+  thanos = cmd: "${cfg.package}/bin/thanos ${cmd}" +
+    (let args = cfg.${cmd}.arguments;
+     in optionalString (length args != 0) (" \\\n  " +
+         concatStringsSep " \\\n  " args));
+
+  argumentsOf = cmd: concatLists (collect isList
+    (flip mapParamsRecursive params.${cmd} (path: param:
+      let opt = concatStringsSep "." path;
+          v = getAttrFromPath path cfg.${cmd};
+      in param.toArgs opt v)));
+
+  mkArgumentsOption = cmd: mkOption {
+    type = types.listOf types.str;
+    default = argumentsOf cmd;
+    defaultText = literalDocBook ''
+      calculated from <literal>config.services.thanos.${cmd}</literal>
+    '';
+    description = ''
+      Arguments to the <literal>thanos ${cmd}</literal> command.
+
+      Defaults to a list of arguments formed by converting the structured
+      options of <option>services.thanos.${cmd}</option> to a list of arguments.
+
+      Overriding this option will cause none of the structured options to have
+      any effect. So only set this if you know what you're doing!
+    '';
+  };
+
+  mapParamsRecursive =
+    let noParam = attr: !(attr ? toArgs && attr ? option);
+    in mapAttrsRecursiveCond noParam;
+
+  paramsToOptions = mapParamsRecursive (_path: param: param.option);
+
+  params = {
+
+    log = {
+
+      log.level = mkParamDef (types.enum ["debug" "info" "warn" "error" "fatal"]) "info" ''
+        Log filtering level.
+      '';
+
+      log.format = mkParam types.str ''
+        Log format to use.
+      '';
+    };
+
+    tracing = cfg: {
+      tracing.config-file = {
+        toArgs = _opt: path: optionToArgs "tracing.config-file" path;
+        option = mkOption {
+          type = with types; nullOr str;
+          default = if cfg.tracing.config == null then null
+                    else toString (toYAML "tracing.yaml" cfg.tracing.config);
+          defaultText = literalExpression ''
+            if config.services.thanos.<cmd>.tracing.config == null then null
+            else toString (toYAML "tracing.yaml" config.services.thanos.<cmd>.tracing.config);
+          '';
+          description = ''
+            Path to YAML file that contains tracing configuration.
+
+            See format details: <link xlink:href="https://thanos.io/tracing.md/#configuration"/>
+          '';
+        };
+      };
+
+      tracing.config =
+        {
+          toArgs = _opt: _attrs: [];
+          option = nullOpt types.attrs ''
+            Tracing configuration.
+
+            When not <literal>null</literal> the attribute set gets converted to
+            a YAML file and stored in the Nix store. The option
+            <option>tracing.config-file</option> will default to its path.
+
+            If <option>tracing.config-file</option> is set this option has no effect.
+
+            See format details: <link xlink:href="https://thanos.io/tracing.md/#configuration"/>
+          '';
+        };
+    };
+
+    common = cfg: params.log // params.tracing cfg // {
+
+      http-address = mkParamDef types.str "0.0.0.0:10902" ''
+        Listen <literal>host:port</literal> for HTTP endpoints.
+      '';
+
+      grpc-address = mkParamDef types.str "0.0.0.0:10901" ''
+        Listen <literal>ip:port</literal> address for gRPC endpoints (StoreAPI).
+
+        Make sure this address is routable from other components.
+      '';
+
+      grpc-server-tls-cert = mkParam types.str ''
+        TLS Certificate for gRPC server, leave blank to disable TLS
+      '';
+
+      grpc-server-tls-key = mkParam types.str ''
+        TLS Key for the gRPC server, leave blank to disable TLS
+      '';
+
+      grpc-server-tls-client-ca = mkParam types.str ''
+        TLS CA to verify clients against.
+
+        If no client CA is specified, there is no client verification on server side.
+        (tls.NoClientCert)
+      '';
+    };
+
+    objstore = cfg: {
+
+      objstore.config-file = {
+        toArgs = _opt: path: optionToArgs "objstore.config-file" path;
+        option = mkOption {
+          type = with types; nullOr str;
+          default = if cfg.objstore.config == null then null
+                    else toString (toYAML "objstore.yaml" cfg.objstore.config);
+          defaultText = literalExpression ''
+            if config.services.thanos.<cmd>.objstore.config == null then null
+            else toString (toYAML "objstore.yaml" config.services.thanos.<cmd>.objstore.config);
+          '';
+          description = ''
+            Path to YAML file that contains object store configuration.
+
+            See format details: <link xlink:href="https://thanos.io/storage.md/#configuration"/>
+          '';
+        };
+      };
+
+      objstore.config =
+        {
+          toArgs = _opt: _attrs: [];
+          option = nullOpt types.attrs ''
+            Object store configuration.
+
+            When not <literal>null</literal> the attribute set gets converted to
+            a YAML file and stored in the Nix store. The option
+            <option>objstore.config-file</option> will default to its path.
+
+            If <option>objstore.config-file</option> is set this option has no effect.
+
+            See format details: <link xlink:href="https://thanos.io/storage.md/#configuration"/>
+          '';
+        };
+    };
+
+    sidecar = params.common cfg.sidecar // params.objstore cfg.sidecar // {
+
+      prometheus.url = mkParamDef types.str "http://localhost:9090" ''
+        URL at which to reach Prometheus's API.
+
+        For better performance use local network.
+      '';
+
+      tsdb.path = {
+        toArgs = optionToArgs;
+        option = mkOption {
+          type = types.str;
+          default = "/var/lib/${config.services.prometheus.stateDir}/data";
+          defaultText = literalExpression ''"/var/lib/''${config.services.prometheus.stateDir}/data"'';
+          description = ''
+            Data directory of TSDB.
+          '';
+        };
+      };
+
+      reloader.config-file = mkParam types.str ''
+        Config file watched by the reloader.
+      '';
+
+      reloader.config-envsubst-file = mkParam types.str ''
+        Output file for environment variable substituted config file.
+      '';
+
+      reloader.rule-dirs = mkListParam "reloader.rule-dir" ''
+        Rule directories for the reloader to refresh.
+      '';
+
+    };
+
+    store = params.common cfg.store // params.objstore cfg.store // {
+
+      stateDir = mkStateDirParam "data-dir" "thanos-store" ''
+        Data directory relative to <literal>/var/lib</literal>
+        in which to cache remote blocks.
+      '';
+
+      index-cache-size = mkParamDef types.str "250MB" ''
+        Maximum size of items held in the index cache.
+      '';
+
+      chunk-pool-size = mkParamDef types.str "2GB" ''
+        Maximum size of concurrently allocatable bytes for chunks.
+      '';
+
+      store.grpc.series-sample-limit = mkParamDef types.int 0 ''
+        Maximum amount of samples returned via a single Series call.
+
+        <literal>0</literal> means no limit.
+
+        NOTE: for efficiency we take 120 as the number of samples in chunk (it
+        cannot be bigger than that), so the actual number of samples might be
+        lower, even though the maximum could be hit.
+      '';
+
+      store.grpc.series-max-concurrency = mkParamDef types.int 20 ''
+        Maximum number of concurrent Series calls.
+      '';
+
+      sync-block-duration = mkParamDef types.str "3m" ''
+        Repeat interval for syncing the blocks between local and remote view.
+      '';
+
+      block-sync-concurrency = mkParamDef types.int 20 ''
+        Number of goroutines to use when syncing blocks from object storage.
+      '';
+
+      min-time = mkParamDef types.str "0000-01-01T00:00:00Z" ''
+        Start of time range limit to serve.
+
+        Thanos Store serves only metrics, which happened later than this
+        value. Option can be a constant time in RFC3339 format or time duration
+        relative to current time, such as -1d or 2h45m. Valid duration units are
+        ms, s, m, h, d, w, y.
+      '';
+
+      max-time = mkParamDef types.str "9999-12-31T23:59:59Z" ''
+        End of time range limit to serve.
+
+        Thanos Store serves only blocks, which happened eariler than this
+        value. Option can be a constant time in RFC3339 format or time duration
+        relative to current time, such as -1d or 2h45m. Valid duration units are
+        ms, s, m, h, d, w, y.
+      '';
+    };
+
+    query = params.common cfg.query // {
+
+      grpc-client-tls-secure = mkFlagParam ''
+        Use TLS when talking to the gRPC server
+      '';
+
+      grpc-client-tls-cert = mkParam types.str ''
+        TLS Certificates to use to identify this client to the server
+      '';
+
+      grpc-client-tls-key = mkParam types.str ''
+        TLS Key for the client's certificate
+      '';
+
+      grpc-client-tls-ca = mkParam types.str ''
+        TLS CA Certificates to use to verify gRPC servers
+      '';
+
+      grpc-client-server-name = mkParam types.str ''
+        Server name to verify the hostname on the returned gRPC certificates.
+        See <link xlink:href="https://tools.ietf.org/html/rfc4366#section-3.1"/>
+      '';
+
+      web.route-prefix = mkParam types.str ''
+        Prefix for API and UI endpoints.
+
+        This allows thanos UI to be served on a sub-path. This option is
+        analogous to <option>web.route-prefix</option> of Promethus.
+      '';
+
+      web.external-prefix = mkParam types.str ''
+        Static prefix for all HTML links and redirect URLs in the UI query web
+        interface.
+
+        Actual endpoints are still served on / or the
+        <option>web.route-prefix</option>. This allows thanos UI to be served
+        behind a reverse proxy that strips a URL sub-path.
+      '';
+
+      web.prefix-header = mkParam types.str ''
+        Name of HTTP request header used for dynamic prefixing of UI links and
+        redirects.
+
+        This option is ignored if the option
+        <literal>web.external-prefix</literal> is set.
+
+        Security risk: enable this option only if a reverse proxy in front of
+        thanos is resetting the header.
+
+        The setting <literal>web.prefix-header="X-Forwarded-Prefix"</literal>
+        can be useful, for example, if Thanos UI is served via Traefik reverse
+        proxy with <literal>PathPrefixStrip</literal> option enabled, which
+        sends the stripped prefix value in <literal>X-Forwarded-Prefix</literal>
+        header. This allows thanos UI to be served on a sub-path.
+      '';
+
+      query.timeout = mkParamDef types.str "2m" ''
+        Maximum time to process query by query node.
+      '';
+
+      query.max-concurrent = mkParamDef types.int 20 ''
+        Maximum number of queries processed concurrently by query node.
+      '';
+
+      query.replica-label = mkParam types.str ''
+        Label to treat as a replica indicator along which data is
+        deduplicated.
+
+        Still you will be able to query without deduplication using
+        <literal>dedup=false</literal> parameter.
+      '';
+
+      selector-labels = mkAttrsParam "selector-label" ''
+        Query selector labels that will be exposed in info endpoint.
+      '';
+
+      store.addresses = mkListParam "store" ''
+        Addresses of statically configured store API servers.
+
+        The scheme may be prefixed with <literal>dns+</literal> or
+        <literal>dnssrv+</literal> to detect store API servers through
+        respective DNS lookups.
+      '';
+
+      store.sd-files = mkListParam "store.sd-files" ''
+        Path to files that contain addresses of store API servers. The path
+        can be a glob pattern.
+      '';
+
+      store.sd-interval = mkParamDef types.str "5m" ''
+        Refresh interval to re-read file SD files. It is used as a resync fallback.
+      '';
+
+      store.sd-dns-interval = mkParamDef types.str "30s" ''
+        Interval between DNS resolutions.
+      '';
+
+      store.unhealthy-timeout = mkParamDef types.str "5m" ''
+        Timeout before an unhealthy store is cleaned from the store UI page.
+      '';
+
+      query.auto-downsampling = mkFlagParam ''
+        Enable automatic adjustment (step / 5) to what source of data should
+        be used in store gateways if no
+        <literal>max_source_resolution</literal> param is specified.
+      '';
+
+      query.partial-response = mkFlagParam ''
+        Enable partial response for queries if no
+        <literal>partial_response</literal> param is specified.
+      '';
+
+      query.default-evaluation-interval = mkParamDef types.str "1m" ''
+        Set default evaluation interval for sub queries.
+      '';
+
+      store.response-timeout = mkParamDef types.str "0ms" ''
+        If a Store doesn't send any data in this specified duration then a
+        Store will be ignored and partial data will be returned if it's
+        enabled. <literal>0</literal> disables timeout.
+      '';
+    };
+
+    rule = params.common cfg.rule // params.objstore cfg.rule // {
+
+      labels = mkAttrsParam "label" ''
+        Labels to be applied to all generated metrics.
+
+        Similar to external labels for Prometheus,
+        used to identify ruler and its blocks as unique source.
+      '';
+
+      stateDir = mkStateDirParam "data-dir" "thanos-rule" ''
+        Data directory relative to <literal>/var/lib</literal>.
+      '';
+
+      rule-files = mkListParam "rule-file" ''
+        Rule files that should be used by rule manager. Can be in glob format.
+      '';
+
+      eval-interval = mkParamDef types.str "30s" ''
+        The default evaluation interval to use.
+      '';
+
+      tsdb.block-duration = mkParamDef types.str "2h" ''
+        Block duration for TSDB block.
+      '';
+
+      tsdb.retention = mkParamDef types.str "48h" ''
+        Block retention time on local disk.
+      '';
+
+      alertmanagers.urls = mkListParam "alertmanagers.url" ''
+        Alertmanager replica URLs to push firing alerts.
+
+        Ruler claims success if push to at least one alertmanager from
+        discovered succeeds. The scheme may be prefixed with
+        <literal>dns+</literal> or <literal>dnssrv+</literal> to detect
+        Alertmanager IPs through respective DNS lookups. The port defaults to
+        <literal>9093</literal> or the SRV record's value. The URL path is
+        used as a prefix for the regular Alertmanager API path.
+      '';
+
+      alertmanagers.send-timeout = mkParamDef types.str "10s" ''
+        Timeout for sending alerts to alertmanager.
+      '';
+
+      alert.query-url = mkParam types.str ''
+        The external Thanos Query URL that would be set in all alerts 'Source' field.
+      '';
+
+      alert.label-drop = mkListParam "alert.label-drop" ''
+        Labels by name to drop before sending to alertmanager.
+
+        This allows alert to be deduplicated on replica label.
+
+        Similar Prometheus alert relabelling
+      '';
+
+      web.route-prefix = mkParam types.str ''
+        Prefix for API and UI endpoints.
+
+        This allows thanos UI to be served on a sub-path.
+
+        This option is analogous to <literal>--web.route-prefix</literal> of Promethus.
+      '';
+
+      web.external-prefix = mkParam types.str ''
+        Static prefix for all HTML links and redirect URLs in the UI query web
+        interface.
+
+        Actual endpoints are still served on / or the
+        <option>web.route-prefix</option>. This allows thanos UI to be served
+        behind a reverse proxy that strips a URL sub-path.
+      '';
+
+      web.prefix-header = mkParam types.str ''
+        Name of HTTP request header used for dynamic prefixing of UI links and
+        redirects.
+
+        This option is ignored if the option
+        <option>web.external-prefix</option> is set.
+
+        Security risk: enable this option only if a reverse proxy in front of
+        thanos is resetting the header.
+
+        The header <literal>X-Forwarded-Prefix</literal> can be useful, for
+        example, if Thanos UI is served via Traefik reverse proxy with
+        <literal>PathPrefixStrip</literal> option enabled, which sends the
+        stripped prefix value in <literal>X-Forwarded-Prefix</literal>
+        header. This allows thanos UI to be served on a sub-path.
+      '';
+
+      query.addresses = mkListParam "query" ''
+        Addresses of statically configured query API servers.
+
+        The scheme may be prefixed with <literal>dns+</literal> or
+        <literal>dnssrv+</literal> to detect query API servers through
+        respective DNS lookups.
+      '';
+
+      query.sd-files = mkListParam "query.sd-files" ''
+        Path to file that contain addresses of query peers.
+        The path can be a glob pattern.
+      '';
+
+      query.sd-interval = mkParamDef types.str "5m" ''
+        Refresh interval to re-read file SD files. (used as a fallback)
+      '';
+
+      query.sd-dns-interval = mkParamDef types.str "30s" ''
+        Interval between DNS resolutions.
+      '';
+    };
+
+    compact = params.log // params.tracing cfg.compact // params.objstore cfg.compact // {
+
+      http-address = mkParamDef types.str "0.0.0.0:10902" ''
+        Listen <literal>host:port</literal> for HTTP endpoints.
+      '';
+
+      stateDir = mkStateDirParam "data-dir" "thanos-compact" ''
+        Data directory relative to <literal>/var/lib</literal>
+        in which to cache blocks and process compactions.
+      '';
+
+      consistency-delay = mkParamDef types.str "30m" ''
+        Minimum age of fresh (non-compacted) blocks before they are being
+        processed. Malformed blocks older than the maximum of consistency-delay
+        and 30m0s will be removed.
+      '';
+
+      retention.resolution-raw = mkParamDef types.str "0d" ''
+        How long to retain raw samples in bucket.
+
+        <literal>0d</literal> - disables this retention
+      '';
+
+      retention.resolution-5m = mkParamDef types.str "0d" ''
+        How long to retain samples of resolution 1 (5 minutes) in bucket.
+
+        <literal>0d</literal> - disables this retention
+      '';
+
+      retention.resolution-1h = mkParamDef types.str "0d" ''
+        How long to retain samples of resolution 2 (1 hour) in bucket.
+
+        <literal>0d</literal> - disables this retention
+      '';
+
+      startAt = {
+        toArgs = _opt: startAt: flagToArgs "wait" (startAt == null);
+        option = nullOpt types.str ''
+          When this option is set to a <literal>systemd.time</literal>
+          specification the Thanos compactor will run at the specified period.
+
+          When this option is <literal>null</literal> the Thanos compactor service
+          will run continuously. So it will not exit after all compactions have
+          been processed but wait for new work.
+        '';
+      };
+
+      downsampling.disable = mkFlagParam ''
+        Disables downsampling.
+
+        This is not recommended as querying long time ranges without
+        non-downsampled data is not efficient and useful e.g it is not possible
+        to render all samples for a human eye anyway
+      '';
+
+      block-sync-concurrency = mkParamDef types.int 20 ''
+        Number of goroutines to use when syncing block metadata from object storage.
+      '';
+
+      compact.concurrency = mkParamDef types.int 1 ''
+        Number of goroutines to use when compacting groups.
+      '';
+    };
+
+    downsample = params.log // params.tracing cfg.downsample // params.objstore cfg.downsample // {
+
+      stateDir = mkStateDirParam "data-dir" "thanos-downsample" ''
+        Data directory relative to <literal>/var/lib</literal>
+        in which to cache blocks and process downsamplings.
+      '';
+
+    };
+
+    receive = params.common cfg.receive // params.objstore cfg.receive // {
+
+      remote-write.address = mkParamDef types.str "0.0.0.0:19291" ''
+        Address to listen on for remote write requests.
+      '';
+
+      stateDir = mkStateDirParam "tsdb.path" "thanos-receive" ''
+        Data directory relative to <literal>/var/lib</literal> of TSDB.
+      '';
+
+      labels = mkAttrsParam "labels" ''
+        External labels to announce.
+
+        This flag will be removed in the future when handling multiple tsdb
+        instances is added.
+      '';
+
+      tsdb.retention = mkParamDef types.str "15d" ''
+        How long to retain raw samples on local storage.
+
+        <literal>0d</literal> - disables this retention
+      '';
+    };
+
+  };
+
+  assertRelativeStateDir = cmd: {
+    assertions = [
+      {
+        assertion = !hasPrefix "/" cfg.${cmd}.stateDir;
+        message =
+          "The option services.thanos.${cmd}.stateDir should not be an absolute directory." +
+          " It should be a directory relative to /var/lib.";
+      }
+    ];
+  };
+
+in {
+
+  options.services.thanos = {
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.thanos;
+      defaultText = literalExpression "pkgs.thanos";
+      description = ''
+        The thanos package that should be used.
+      '';
+    };
+
+    sidecar = paramsToOptions params.sidecar // {
+      enable = mkEnableOption
+        "the Thanos sidecar for Prometheus server";
+      arguments = mkArgumentsOption "sidecar";
+    };
+
+    store = paramsToOptions params.store // {
+      enable = mkEnableOption
+        "the Thanos store node giving access to blocks in a bucket provider.";
+      arguments = mkArgumentsOption "store";
+    };
+
+    query = paramsToOptions params.query // {
+      enable = mkEnableOption
+        ("the Thanos query node exposing PromQL enabled Query API " +
+         "with data retrieved from multiple store nodes");
+      arguments = mkArgumentsOption "query";
+    };
+
+    rule = paramsToOptions params.rule // {
+      enable = mkEnableOption
+        ("the Thanos ruler service which evaluates Prometheus rules against" +
+        " given Query nodes, exposing Store API and storing old blocks in bucket");
+      arguments = mkArgumentsOption "rule";
+    };
+
+    compact = paramsToOptions params.compact // {
+      enable = mkEnableOption
+        "the Thanos compactor which continuously compacts blocks in an object store bucket";
+      arguments = mkArgumentsOption "compact";
+    };
+
+    downsample = paramsToOptions params.downsample // {
+      enable = mkEnableOption
+        "the Thanos downsampler which continuously downsamples blocks in an object store bucket";
+      arguments = mkArgumentsOption "downsample";
+    };
+
+    receive = paramsToOptions params.receive // {
+      enable = mkEnableOption
+        ("the Thanos receiver which accept Prometheus remote write API requests " +
+         "and write to local tsdb (EXPERIMENTAL, this may change drastically without notice)");
+      arguments = mkArgumentsOption "receive";
+    };
+  };
+
+  config = mkMerge [
+
+    (mkIf cfg.sidecar.enable {
+      assertions = [
+        {
+          assertion = config.services.prometheus.enable;
+          message =
+            "Please enable services.prometheus when enabling services.thanos.sidecar.";
+        }
+        {
+          assertion = !(config.services.prometheus.globalConfig.external_labels == null ||
+                        config.services.prometheus.globalConfig.external_labels == {});
+          message =
+            "services.thanos.sidecar requires uniquely identifying external labels " +
+            "to be configured in the Prometheus server. " +
+            "Please set services.prometheus.globalConfig.external_labels.";
+        }
+      ];
+      systemd.services.thanos-sidecar = {
+        wantedBy = [ "multi-user.target" ];
+        after    = [ "network.target" "prometheus.service" ];
+        serviceConfig = {
+          User = "prometheus";
+          Restart = "always";
+          ExecStart = thanos "sidecar";
+        };
+      };
+    })
+
+    (mkIf cfg.store.enable (mkMerge [
+      (assertRelativeStateDir "store")
+      {
+        systemd.services.thanos-store = {
+          wantedBy = [ "multi-user.target" ];
+          after    = [ "network.target" ];
+          serviceConfig = {
+            DynamicUser = true;
+            StateDirectory = cfg.store.stateDir;
+            Restart = "always";
+            ExecStart = thanos "store";
+          };
+        };
+      }
+    ]))
+
+    (mkIf cfg.query.enable {
+      systemd.services.thanos-query = {
+        wantedBy = [ "multi-user.target" ];
+        after    = [ "network.target" ];
+        serviceConfig = {
+          DynamicUser = true;
+          Restart = "always";
+          ExecStart = thanos "query";
+        };
+      };
+    })
+
+    (mkIf cfg.rule.enable (mkMerge [
+      (assertRelativeStateDir "rule")
+      {
+        systemd.services.thanos-rule = {
+          wantedBy = [ "multi-user.target" ];
+          after    = [ "network.target" ];
+          serviceConfig = {
+            DynamicUser = true;
+            StateDirectory = cfg.rule.stateDir;
+            Restart = "always";
+            ExecStart = thanos "rule";
+          };
+        };
+      }
+    ]))
+
+    (mkIf cfg.compact.enable (mkMerge [
+      (assertRelativeStateDir "compact")
+      {
+        systemd.services.thanos-compact =
+          let wait = cfg.compact.startAt == null; in {
+            wantedBy = [ "multi-user.target" ];
+            after    = [ "network.target" ];
+            serviceConfig = {
+              Type    = if wait then "simple" else "oneshot";
+              Restart = if wait then "always" else "no";
+              DynamicUser = true;
+              StateDirectory = cfg.compact.stateDir;
+              ExecStart = thanos "compact";
+            };
+          } // optionalAttrs (!wait) { inherit (cfg.compact) startAt; };
+      }
+    ]))
+
+    (mkIf cfg.downsample.enable (mkMerge [
+      (assertRelativeStateDir "downsample")
+      {
+        systemd.services.thanos-downsample = {
+          wantedBy = [ "multi-user.target" ];
+          after    = [ "network.target" ];
+          serviceConfig = {
+            DynamicUser = true;
+            StateDirectory = cfg.downsample.stateDir;
+            Restart = "always";
+            ExecStart = thanos "downsample";
+          };
+        };
+      }
+    ]))
+
+    (mkIf cfg.receive.enable (mkMerge [
+      (assertRelativeStateDir "receive")
+      {
+        systemd.services.thanos-receive = {
+          wantedBy = [ "multi-user.target" ];
+          after    = [ "network.target" ];
+          serviceConfig = {
+            DynamicUser = true;
+            StateDirectory = cfg.receive.stateDir;
+            Restart = "always";
+            ExecStart = thanos "receive";
+          };
+        };
+      }
+    ]))
+
+  ];
+}
diff --git a/nixos/modules/services/monitoring/tuptime.nix b/nixos/modules/services/monitoring/tuptime.nix
new file mode 100644
index 00000000000..de80282559a
--- /dev/null
+++ b/nixos/modules/services/monitoring/tuptime.nix
@@ -0,0 +1,91 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.tuptime;
+
+in {
+
+  options.services.tuptime = {
+
+    enable = mkEnableOption "the total uptime service";
+
+    timer = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to regularly log uptime to detect bad shutdowns.";
+      };
+
+      period = mkOption {
+        type = types.str;
+        default = "*:0/5";
+        description = "systemd calendar event";
+      };
+    };
+  };
+
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.tuptime ];
+
+    users = {
+      groups._tuptime.members = [ "_tuptime" ];
+      users._tuptime = {
+        isSystemUser = true;
+        group = "_tuptime";
+        description = "tuptime database owner";
+      };
+    };
+
+    systemd = {
+      services = {
+
+        tuptime = {
+          description = "the total uptime service";
+          documentation = [ "man:tuptime(1)" ];
+          after = [ "time-sync.target" ];
+          wantedBy = [ "multi-user.target" ];
+          serviceConfig = {
+            StateDirectory = "tuptime";
+            Type = "oneshot";
+            User = "_tuptime";
+            RemainAfterExit = true;
+            ExecStart = "${pkgs.tuptime}/bin/tuptime -x";
+            ExecStop = "${pkgs.tuptime}/bin/tuptime -xg";
+          };
+        };
+
+        tuptime-oneshot = mkIf cfg.timer.enable {
+          description = "the tuptime scheduled execution unit";
+          serviceConfig = {
+            StateDirectory = "tuptime";
+            Type = "oneshot";
+            User = "_tuptime";
+            ExecStart = "${pkgs.tuptime}/bin/tuptime -x";
+          };
+        };
+      };
+
+      timers.tuptime = mkIf cfg.timer.enable {
+        description = "the tuptime scheduled execution timer";
+        # this timer should be started if the service is started
+        # even if the timer was previously stopped
+        wantedBy = [ "tuptime.service" "timers.target" ];
+        # this timer should be stopped if the service is stopped
+        partOf = [ "tuptime.service" ];
+        timerConfig = {
+          OnBootSec = "1min";
+          OnCalendar = cfg.timer.period;
+          Unit = "tuptime-oneshot.service";
+        };
+      };
+    };
+  };
+
+  meta.maintainers = [ maintainers.evils ];
+
+}
diff --git a/nixos/modules/services/monitoring/unifi-poller.nix b/nixos/modules/services/monitoring/unifi-poller.nix
new file mode 100644
index 00000000000..cca4a0e7207
--- /dev/null
+++ b/nixos/modules/services/monitoring/unifi-poller.nix
@@ -0,0 +1,318 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.unifi-poller;
+
+  configFile = pkgs.writeText "unifi-poller.json" (generators.toJSON {} {
+    inherit (cfg) poller influxdb loki prometheus unifi;
+  });
+
+in {
+  options.services.unifi-poller = {
+    enable = mkEnableOption "unifi-poller";
+
+    poller = {
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Turns on line numbers, microsecond logging, and a per-device log.
+          This may be noisy if you have a lot of devices. It adds one line per device.
+        '';
+      };
+      quiet = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Turns off per-interval logs. Only startup and error logs will be emitted.
+        '';
+      };
+      plugins = mkOption {
+        type = with types; listOf str;
+        default = [];
+        description = ''
+          Load additional plugins.
+        '';
+      };
+    };
+
+    prometheus = {
+      disable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to disable the prometheus ouput plugin.
+        '';
+      };
+      http_listen = mkOption {
+        type = types.str;
+        default = "[::]:9130";
+        description = ''
+          Bind the prometheus exporter to this IP or hostname.
+        '';
+      };
+      report_errors = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to report errors.
+        '';
+      };
+    };
+
+    influxdb = {
+      disable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to disable the influxdb ouput plugin.
+        '';
+      };
+      url = mkOption {
+        type = types.str;
+        default = "http://127.0.0.1:8086";
+        description = ''
+          URL of the influxdb host.
+        '';
+      };
+      user = mkOption {
+        type = types.str;
+        default = "unifipoller";
+        description = ''
+          Username for the influxdb.
+        '';
+      };
+      pass = mkOption {
+        type = types.path;
+        default = pkgs.writeText "unifi-poller-influxdb-default.password" "unifipoller";
+        defaultText = literalExpression "unifi-poller-influxdb-default.password";
+        description = ''
+          Path of a file containing the password for influxdb.
+          This file needs to be readable by the unifi-poller user.
+        '';
+        apply = v: "file://${v}";
+      };
+      db = mkOption {
+        type = types.str;
+        default = "unifi";
+        description = ''
+          Database name. Database should exist.
+        '';
+      };
+      verify_ssl = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Verify the influxdb's certificate.
+        '';
+      };
+      interval = mkOption {
+        type = types.str;
+        default = "30s";
+        description = ''
+          Setting this lower than the Unifi controller's refresh
+          interval may lead to zeroes in your database.
+        '';
+      };
+    };
+
+    loki = {
+      url = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          URL of the Loki host.
+        '';
+      };
+      user = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Username for Loki.
+        '';
+      };
+      pass = mkOption {
+        type = types.path;
+        default = pkgs.writeText "unifi-poller-loki-default.password" "";
+        defaultText = "unifi-poller-influxdb-default.password";
+        description = ''
+          Path of a file containing the password for Loki.
+          This file needs to be readable by the unifi-poller user.
+        '';
+        apply = v: "file://${v}";
+      };
+      verify_ssl = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Verify Loki's certificate.
+        '';
+      };
+      tenant_id = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Tenant ID to use in Loki.
+        '';
+      };
+      interval = mkOption {
+        type = types.str;
+        default = "2m";
+        description = ''
+          How often the events are polled and pushed to Loki.
+        '';
+      };
+      timeout = mkOption {
+        type = types.str;
+        default = "10s";
+        description = ''
+          Should be increased in case of timeout errors.
+        '';
+      };
+    };
+
+    unifi = let
+      controllerOptions = {
+        user = mkOption {
+          type = types.str;
+          default = "unifi";
+          description = ''
+            Unifi service user name.
+          '';
+        };
+        pass = mkOption {
+          type = types.path;
+          default = pkgs.writeText "unifi-poller-unifi-default.password" "unifi";
+          defaultText = literalExpression "unifi-poller-unifi-default.password";
+          description = ''
+            Path of a file containing the password for the unifi service user.
+            This file needs to be readable by the unifi-poller user.
+          '';
+          apply = v: "file://${v}";
+        };
+        url = mkOption {
+          type = types.str;
+          default = "https://unifi:8443";
+          description = ''
+            URL of the Unifi controller.
+          '';
+        };
+        sites = mkOption {
+          type = with types; either (enum [ "default" "all" ]) (listOf str);
+          default = "all";
+          description = ''
+            List of site names for which statistics should be exported.
+            Or the string "default" for the default site or the string "all" for all sites.
+          '';
+          apply = toList;
+        };
+        save_ids = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Collect and save data from the intrusion detection system to influxdb and Loki.
+          '';
+        };
+        save_events = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Collect and save data from UniFi events to influxdb and Loki.
+          '';
+        };
+        save_alarms = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Collect and save data from UniFi alarms to influxdb and Loki.
+          '';
+        };
+        save_anomalies = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Collect and save data from UniFi anomalies to influxdb and Loki.
+          '';
+        };
+        save_dpi = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Collect and save data from deep packet inspection.
+            Adds around 150 data points and impacts performance.
+          '';
+        };
+        save_sites = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Collect and save site data.
+          '';
+        };
+        hash_pii = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Hash, with md5, client names and MAC addresses. This attempts
+            to protect personally identifiable information.
+          '';
+        };
+        verify_ssl = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Verify the Unifi controller's certificate.
+          '';
+        };
+      };
+
+    in {
+      dynamic = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Let prometheus select which controller to poll when scraping.
+          Use with default credentials. See unifi-poller wiki for more.
+        '';
+      };
+
+      defaults = controllerOptions;
+
+      controllers = mkOption {
+        type = with types; listOf (submodule { options = controllerOptions; });
+        default = [];
+        description = ''
+          List of Unifi controllers to poll. Use defaults if empty.
+        '';
+        apply = map (flip removeAttrs [ "_module" ]);
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups.unifi-poller = { };
+    users.users.unifi-poller = {
+      description = "unifi-poller Service User";
+      group = "unifi-poller";
+      isSystemUser = true;
+    };
+
+    systemd.services.unifi-poller = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.unifi-poller}/bin/unifi-poller --config ${configFile}";
+        Restart = "always";
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectSystem = "full";
+        DevicePolicy = "closed";
+        NoNewPrivileges = true;
+        User = "unifi-poller";
+        WorkingDirectory = "/tmp";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/ups.nix b/nixos/modules/services/monitoring/ups.nix
new file mode 100644
index 00000000000..ae5097c5442
--- /dev/null
+++ b/nixos/modules/services/monitoring/ups.nix
@@ -0,0 +1,263 @@
+{ config, lib, pkgs, ... }:
+
+# TODO: This is not secure, have a look at the file docs/security.txt inside
+# the project sources.
+with lib;
+
+let
+  cfg = config.power.ups;
+in
+
+let
+  upsOptions = {name, config, ...}:
+  {
+    options = {
+      # This can be infered from the UPS model by looking at
+      # /nix/store/nut/share/driver.list
+      driver = mkOption {
+        type = types.str;
+        description = ''
+          Specify the program to run to talk to this UPS.  apcsmart,
+          bestups, and sec are some examples.
+        '';
+      };
+
+      port = mkOption {
+        type = types.str;
+        description = ''
+          The serial port to which your UPS is connected.  /dev/ttyS0 is
+          usually the first port on Linux boxes, for example.
+        '';
+      };
+
+      shutdownOrder = mkOption {
+        default = 0;
+        type = types.int;
+        description = ''
+          When you have multiple UPSes on your system, you usually need to
+          turn them off in a certain order.  upsdrvctl shuts down all the
+          0s, then the 1s, 2s, and so on.  To exclude a UPS from the
+          shutdown sequence, set this to -1.
+        '';
+      };
+
+      maxStartDelay = mkOption {
+        default = null;
+        type = types.uniq (types.nullOr types.int);
+        description = ''
+          This can be set as a global variable above your first UPS
+          definition and it can also be set in a UPS section.  This value
+          controls how long upsdrvctl will wait for the driver to finish
+          starting.  This keeps your system from getting stuck due to a
+          broken driver or UPS.
+        '';
+      };
+
+      description = mkOption {
+        default = "";
+        type = types.str;
+        description = ''
+          Description of the UPS.
+        '';
+      };
+
+      directives = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        description = ''
+          List of configuration directives for this UPS.
+        '';
+      };
+
+      summary = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Lines which would be added inside ups.conf for handling this UPS.
+        '';
+      };
+
+    };
+
+    config = {
+      directives = mkOrder 10 ([
+        "driver = ${config.driver}"
+        "port = ${config.port}"
+        ''desc = "${config.description}"''
+        "sdorder = ${toString config.shutdownOrder}"
+      ] ++ (optional (config.maxStartDelay != null)
+            "maxstartdelay = ${toString config.maxStartDelay}")
+      );
+
+      summary =
+        concatStringsSep "\n      "
+          (["[${name}]"] ++ config.directives);
+    };
+  };
+
+in
+
+
+{
+  options = {
+    # powerManagement.powerDownCommands
+
+    power.ups = {
+      enable = mkOption {
+        default = false;
+        type = with types; bool;
+        description = ''
+          Enables support for Power Devices, such as Uninterruptible Power
+          Supplies, Power Distribution Units and Solar Controllers.
+        '';
+      };
+
+      # This option is not used yet.
+      mode = mkOption {
+        default = "standalone";
+        type = types.str;
+        description = ''
+          The MODE determines which part of the NUT is to be started, and
+          which configuration files must be modified.
+
+          The values of MODE can be:
+
+          - none: NUT is not configured, or use the Integrated Power
+            Management, or use some external system to startup NUT
+            components. So nothing is to be started.
+
+          - standalone: This mode address a local only configuration, with 1
+            UPS protecting the local system. This implies to start the 3 NUT
+            layers (driver, upsd and upsmon) and the matching configuration
+            files. This mode can also address UPS redundancy.
+
+          - netserver: same as for the standalone configuration, but also
+            need some more ACLs and possibly a specific LISTEN directive in
+            upsd.conf.  Since this MODE is opened to the network, a special
+            care should be applied to security concerns.
+
+          - netclient: this mode only requires upsmon.
+        '';
+      };
+
+      schedulerRules = mkOption {
+        example = "/etc/nixos/upssched.conf";
+        type = types.str;
+        description = ''
+          File which contains the rules to handle UPS events.
+        '';
+      };
+
+
+      maxStartDelay = mkOption {
+        default = 45;
+        type = types.int;
+        description = ''
+          This can be set as a global variable above your first UPS
+          definition and it can also be set in a UPS section.  This value
+          controls how long upsdrvctl will wait for the driver to finish
+          starting.  This keeps your system from getting stuck due to a
+          broken driver or UPS.
+        '';
+      };
+
+      ups = mkOption {
+        default = {};
+        # see nut/etc/ups.conf.sample
+        description = ''
+          This is where you configure all the UPSes that this system will be
+          monitoring directly.  These are usually attached to serial ports,
+          but USB devices are also supported.
+        '';
+        type = with types; attrsOf (submodule upsOptions);
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.nut ];
+
+    systemd.services.upsmon = {
+      description = "Uninterruptible Power Supplies (Monitor)";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig.Type = "forking";
+      script = "${pkgs.nut}/sbin/upsmon";
+      environment.NUT_CONFPATH = "/etc/nut/";
+      environment.NUT_STATEPATH = "/var/lib/nut/";
+    };
+
+    systemd.services.upsd = {
+      description = "Uninterruptible Power Supplies (Daemon)";
+      after = [ "network.target" "upsmon.service" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig.Type = "forking";
+      # TODO: replace 'root' by another username.
+      script = "${pkgs.nut}/sbin/upsd -u root";
+      environment.NUT_CONFPATH = "/etc/nut/";
+      environment.NUT_STATEPATH = "/var/lib/nut/";
+    };
+
+    systemd.services.upsdrv = {
+      description = "Uninterruptible Power Supplies (Register all UPS)";
+      after = [ "upsd.service" ];
+      wantedBy = [ "multi-user.target" ];
+      # TODO: replace 'root' by another username.
+      script = "${pkgs.nut}/bin/upsdrvctl -u root start";
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+      };
+      environment.NUT_CONFPATH = "/etc/nut/";
+      environment.NUT_STATEPATH = "/var/lib/nut/";
+    };
+
+    environment.etc = {
+      "nut/nut.conf".source = pkgs.writeText "nut.conf"
+        ''
+          MODE = ${cfg.mode}
+        '';
+      "nut/ups.conf".source = pkgs.writeText "ups.conf"
+        ''
+          maxstartdelay = ${toString cfg.maxStartDelay}
+
+          ${flip concatStringsSep (forEach (attrValues cfg.ups) (ups: ups.summary)) "
+
+          "}
+        '';
+      "nut/upssched.conf".source = cfg.schedulerRules;
+      # These file are containing private informations and thus should not
+      # be stored inside the Nix store.
+      /*
+      "nut/upsd.conf".source = "";
+      "nut/upsd.users".source = "";
+      "nut/upsmon.conf".source = "";
+      */
+    };
+
+    power.ups.schedulerRules = mkDefault "${pkgs.nut}/etc/upssched.conf.sample";
+
+    system.activationScripts.upsSetup = stringAfter [ "users" "groups" ]
+      ''
+        # Used to store pid files of drivers.
+        mkdir -p /var/state/ups
+      '';
+
+
+/*
+    users.users.nut =
+      { uid = 84;
+        home = "/var/lib/nut";
+        createHome = true;
+        group = "nut";
+        description = "UPnP A/V Media Server user";
+      };
+
+    users.groups."nut" =
+      { gid = 84; };
+*/
+
+  };
+}
diff --git a/nixos/modules/services/monitoring/uptime.nix b/nixos/modules/services/monitoring/uptime.nix
new file mode 100644
index 00000000000..79b86be6cc7
--- /dev/null
+++ b/nixos/modules/services/monitoring/uptime.nix
@@ -0,0 +1,100 @@
+{ config, options, pkgs, lib, ... }:
+let
+  inherit (lib) literalExpression mkOption mkEnableOption mkIf mkMerge types optional;
+
+  cfg = config.services.uptime;
+  opt = options.services.uptime;
+
+  configDir = pkgs.runCommand "config" { preferLocalBuild = true; }
+  (if cfg.configFile != null then ''
+    mkdir $out
+    ext=`echo ${cfg.configFile} | grep -o \\..*`
+    ln -sv ${cfg.configFile} $out/default$ext
+    ln -sv /var/lib/uptime/runtime.json $out/runtime.json
+  '' else ''
+    mkdir $out
+    cat ${pkgs.nodePackages.node-uptime}/lib/node_modules/node-uptime/config/default.yaml > $out/default.yaml
+    cat >> $out/default.yaml <<EOF
+
+    autoStartMonitor: false
+
+    mongodb:
+      connectionString: 'mongodb://localhost/uptime'
+    EOF
+    ln -sv /var/lib/uptime/runtime.json $out/runtime.json
+  '');
+in {
+  options.services.uptime = {
+    configFile = mkOption {
+      description = ''
+        The uptime configuration file
+
+        If mongodb: server != localhost, please set usesRemoteMongo = true
+
+        If you only want to run the monitor, please set enableWebService = false
+        and enableSeparateMonitoringService = true
+
+        If autoStartMonitor: false (recommended) and you want to run both
+        services, please set enableSeparateMonitoringService = true
+      '';
+
+      type = types.nullOr types.path;
+
+      default = null;
+    };
+
+    usesRemoteMongo = mkOption {
+      description = "Whether the configuration file specifies a remote mongo instance";
+
+      default = false;
+
+      type = types.bool;
+    };
+
+    enableWebService = mkEnableOption "the uptime monitoring program web service";
+
+    enableSeparateMonitoringService = mkEnableOption "the uptime monitoring service" // {
+      default = cfg.enableWebService;
+      defaultText = literalExpression "config.${opt.enableWebService}";
+    };
+
+    nodeEnv = mkOption {
+      description = "The node environment to run in (development, production, etc.)";
+
+      type = types.str;
+
+      default = "production";
+    };
+  };
+
+  config = mkMerge [ (mkIf cfg.enableWebService {
+    systemd.services.uptime = {
+      description = "uptime web service";
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        NODE_CONFIG_DIR = configDir;
+        NODE_ENV = cfg.nodeEnv;
+        NODE_PATH = "${pkgs.nodePackages.node-uptime}/lib/node_modules/node-uptime/node_modules";
+      };
+      preStart = "mkdir -p /var/lib/uptime";
+      serviceConfig.ExecStart = "${pkgs.nodejs}/bin/node ${pkgs.nodePackages.node-uptime}/lib/node_modules/node-uptime/app.js";
+    };
+
+    services.mongodb.enable = mkIf (!cfg.usesRemoteMongo) true;
+  }) (mkIf cfg.enableSeparateMonitoringService {
+    systemd.services.uptime-monitor = {
+      description = "uptime monitoring service";
+      wantedBy = [ "multi-user.target" ];
+      requires = optional cfg.enableWebService "uptime.service";
+      after = optional cfg.enableWebService "uptime.service";
+      environment = {
+        NODE_CONFIG_DIR = configDir;
+        NODE_ENV = cfg.nodeEnv;
+        NODE_PATH = "${pkgs.nodePackages.node-uptime}/lib/node_modules/node-uptime/node_modules";
+      };
+      # Ugh, need to wait for web service to be up
+      preStart = if cfg.enableWebService then "sleep 1s" else "mkdir -p /var/lib/uptime";
+      serviceConfig.ExecStart = "${pkgs.nodejs}/bin/node ${pkgs.nodePackages.node-uptime}/lib/node_modules/node-uptime/monitor.js";
+    };
+  }) ];
+}
diff --git a/nixos/modules/services/monitoring/vnstat.nix b/nixos/modules/services/monitoring/vnstat.nix
new file mode 100644
index 00000000000..5e19c399568
--- /dev/null
+++ b/nixos/modules/services/monitoring/vnstat.nix
@@ -0,0 +1,60 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.vnstat;
+in {
+  options.services.vnstat = {
+    enable = mkEnableOption "update of network usage statistics via vnstatd";
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.vnstat ];
+
+    users = {
+      groups.vnstatd = {};
+
+      users.vnstatd = {
+        isSystemUser = true;
+        group = "vnstatd";
+        description = "vnstat daemon user";
+      };
+    };
+
+    systemd.services.vnstat = {
+      description = "vnStat network traffic monitor";
+      path = [ pkgs.coreutils ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      documentation = [
+        "man:vnstatd(1)"
+        "man:vnstat(1)"
+        "man:vnstat.conf(5)"
+      ];
+      serviceConfig = {
+        ExecStart = "${pkgs.vnstat}/bin/vnstatd -n";
+        ExecReload = "${pkgs.procps}/bin/kill -HUP $MAINPID";
+
+        # Hardening (from upstream example service)
+        ProtectSystem = "strict";
+        StateDirectory = "vnstat";
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectKernelModules = true;
+        PrivateTmp = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictNamespaces = true;
+
+        User = "vnstatd";
+        Group = "vnstatd";
+      };
+    };
+  };
+
+  meta.maintainers = [ maintainers.evils ];
+}
diff --git a/nixos/modules/services/monitoring/zabbix-agent.nix b/nixos/modules/services/monitoring/zabbix-agent.nix
new file mode 100644
index 00000000000..c48b973f1ef
--- /dev/null
+++ b/nixos/modules/services/monitoring/zabbix-agent.nix
@@ -0,0 +1,178 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.zabbixAgent;
+
+  inherit (lib) mkDefault mkEnableOption mkIf mkMerge mkOption;
+  inherit (lib) attrValues concatMapStringsSep literalExpression optionalString types;
+  inherit (lib.generators) toKeyValue;
+
+  user = "zabbix-agent";
+  group = "zabbix-agent";
+
+  moduleEnv = pkgs.symlinkJoin {
+    name = "zabbix-agent-module-env";
+    paths = attrValues cfg.modules;
+  };
+
+  configFile = pkgs.writeText "zabbix_agent.conf" (toKeyValue { listsAsDuplicateKeys = true; } cfg.settings);
+
+in
+
+{
+  imports = [
+    (lib.mkRemovedOptionModule [ "services" "zabbixAgent" "extraConfig" ] "Use services.zabbixAgent.settings instead.")
+  ];
+
+  # interface
+
+  options = {
+
+    services.zabbixAgent = {
+      enable = mkEnableOption "the Zabbix Agent";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.zabbix.agent;
+        defaultText = literalExpression "pkgs.zabbix.agent";
+        description = "The Zabbix package to use.";
+      };
+
+      extraPackages = mkOption {
+        type = types.listOf types.package;
+        default = with pkgs; [ nettools ];
+        defaultText = literalExpression "with pkgs; [ nettools ]";
+        example = literalExpression "with pkgs; [ nettools mysql ]";
+        description = ''
+          Packages to be added to the Zabbix <envar>PATH</envar>.
+          Typically used to add executables for scripts, but can be anything.
+        '';
+      };
+
+      modules = mkOption {
+        type = types.attrsOf types.package;
+        description = "A set of modules to load.";
+        default = {};
+        example = literalExpression ''
+          {
+            "dummy.so" = pkgs.stdenv.mkDerivation {
+              name = "zabbix-dummy-module-''${cfg.package.version}";
+              src = cfg.package.src;
+              buildInputs = [ cfg.package ];
+              sourceRoot = "zabbix-''${cfg.package.version}/src/modules/dummy";
+              installPhase = '''
+                mkdir -p $out/lib
+                cp dummy.so $out/lib/
+              ''';
+            };
+          }
+        '';
+      };
+
+      server = mkOption {
+        type = types.str;
+        description = ''
+          The IP address or hostname of the Zabbix server to connect to.
+        '';
+      };
+
+      listen = {
+        ip = mkOption {
+          type = types.str;
+          default = "0.0.0.0";
+          description = ''
+            List of comma delimited IP addresses that the agent should listen on.
+          '';
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 10050;
+          description = ''
+            Agent will listen on this port for connections from the server.
+          '';
+        };
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for the Zabbix Agent.
+        '';
+      };
+
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ int str (listOf str) ]);
+        default = {};
+        description = ''
+          Zabbix Agent configuration. Refer to
+          <link xlink:href="https://www.zabbix.com/documentation/current/manual/appendix/config/zabbix_agentd"/>
+          for details on supported values.
+        '';
+        example = {
+          Hostname = "example.org";
+          DebugLevel = 4;
+        };
+      };
+
+    };
+
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+
+    services.zabbixAgent.settings = mkMerge [
+      {
+        LogType = "console";
+        Server = cfg.server;
+        ListenPort = cfg.listen.port;
+      }
+      (mkIf (cfg.modules != {}) {
+        LoadModule = builtins.attrNames cfg.modules;
+        LoadModulePath = "${moduleEnv}/lib";
+      })
+
+      # the default value for "ListenIP" is 0.0.0.0 but zabbix agent 2 cannot accept configuration files which
+      # explicitly set "ListenIP" to the default value...
+      (mkIf (cfg.listen.ip != "0.0.0.0") { ListenIP = cfg.listen.ip; })
+    ];
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listen.port ];
+    };
+
+    users.users.${user} = {
+      description = "Zabbix Agent daemon user";
+      inherit group;
+      isSystemUser = true;
+    };
+
+    users.groups.${group} = { };
+
+    systemd.services.zabbix-agent = {
+      description = "Zabbix Agent";
+
+      wantedBy = [ "multi-user.target" ];
+
+      # https://www.zabbix.com/documentation/current/manual/config/items/userparameters
+      # > User parameters are commands executed by Zabbix agent.
+      # > /bin/sh is used as a command line interpreter under UNIX operating systems.
+      path = with pkgs; [ bash "/run/wrappers" ] ++ cfg.extraPackages;
+
+      serviceConfig = {
+        ExecStart = "@${cfg.package}/sbin/zabbix_agentd zabbix_agentd -f --config ${configFile}";
+        Restart = "always";
+        RestartSec = 2;
+
+        User = user;
+        Group = group;
+        PrivateTmp = true;
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/monitoring/zabbix-proxy.nix b/nixos/modules/services/monitoring/zabbix-proxy.nix
new file mode 100644
index 00000000000..0ebd7bcff83
--- /dev/null
+++ b/nixos/modules/services/monitoring/zabbix-proxy.nix
@@ -0,0 +1,323 @@
+{ config, lib, options, pkgs, ... }:
+
+let
+  cfg = config.services.zabbixProxy;
+  opt = options.services.zabbixProxy;
+  pgsql = config.services.postgresql;
+  mysql = config.services.mysql;
+
+  inherit (lib) mkAfter mkDefault mkEnableOption mkIf mkMerge mkOption;
+  inherit (lib) attrValues concatMapStringsSep getName literalExpression optional optionalAttrs optionalString types;
+  inherit (lib.generators) toKeyValue;
+
+  user = "zabbix";
+  group = "zabbix";
+  runtimeDir = "/run/zabbix";
+  stateDir = "/var/lib/zabbix";
+  passwordFile = "${runtimeDir}/zabbix-dbpassword.conf";
+
+  moduleEnv = pkgs.symlinkJoin {
+    name = "zabbix-proxy-module-env";
+    paths = attrValues cfg.modules;
+  };
+
+  configFile = pkgs.writeText "zabbix_proxy.conf" (toKeyValue { listsAsDuplicateKeys = true; } cfg.settings);
+
+  mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
+  pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
+
+in
+
+{
+  imports = [
+    (lib.mkRemovedOptionModule [ "services" "zabbixProxy" "extraConfig" ] "Use services.zabbixProxy.settings instead.")
+  ];
+
+  # interface
+
+  options = {
+
+    services.zabbixProxy = {
+      enable = mkEnableOption "the Zabbix Proxy";
+
+      server = mkOption {
+        type = types.str;
+        description = ''
+          The IP address or hostname of the Zabbix server to connect to.
+          '';
+        };
+
+      package = mkOption {
+        type = types.package;
+        default =
+          if cfg.database.type == "mysql" then pkgs.zabbix.proxy-mysql
+          else if cfg.database.type == "pgsql" then pkgs.zabbix.proxy-pgsql
+          else pkgs.zabbix.proxy-sqlite;
+        defaultText = literalExpression "pkgs.zabbix.proxy-pgsql";
+        description = "The Zabbix package to use.";
+      };
+
+      extraPackages = mkOption {
+        type = types.listOf types.package;
+        default = with pkgs; [ nettools nmap traceroute ];
+        defaultText = literalExpression "[ nettools nmap traceroute ]";
+        description = ''
+          Packages to be added to the Zabbix <envar>PATH</envar>.
+          Typically used to add executables for scripts, but can be anything.
+        '';
+      };
+
+      modules = mkOption {
+        type = types.attrsOf types.package;
+        description = "A set of modules to load.";
+        default = {};
+        example = literalExpression ''
+          {
+            "dummy.so" = pkgs.stdenv.mkDerivation {
+              name = "zabbix-dummy-module-''${cfg.package.version}";
+              src = cfg.package.src;
+              buildInputs = [ cfg.package ];
+              sourceRoot = "zabbix-''${cfg.package.version}/src/modules/dummy";
+              installPhase = '''
+                mkdir -p $out/lib
+                cp dummy.so $out/lib/
+              ''';
+            };
+          }
+        '';
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum [ "mysql" "pgsql" "sqlite" ];
+          example = "mysql";
+          default = "pgsql";
+          description = "Database engine to use.";
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = "Database host address.";
+        };
+
+        port = mkOption {
+          type = types.int;
+          default = if cfg.database.type == "mysql" then mysql.port else pgsql.port;
+          defaultText = literalExpression ''
+            if config.${opt.database.type} == "mysql"
+            then config.${options.services.mysql.port}
+            else config.${options.services.postgresql.port}
+          '';
+          description = "Database host port.";
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = if cfg.database.type == "sqlite" then "${stateDir}/zabbix.db" else "zabbix";
+          defaultText = literalExpression "zabbix";
+          description = "Database name.";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "zabbix";
+          description = "Database user.";
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          example = "/run/keys/zabbix-dbpassword";
+          description = ''
+            A file containing the password corresponding to
+            <option>database.user</option>.
+          '';
+        };
+
+        socket = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          example = "/run/postgresql";
+          description = "Path to the unix socket file to use for authentication.";
+        };
+
+        createLocally = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Whether to create a local database automatically.";
+        };
+      };
+
+      listen = {
+        ip = mkOption {
+          type = types.str;
+          default = "0.0.0.0";
+          description = ''
+            List of comma delimited IP addresses that the trapper should listen on.
+            Trapper will listen on all network interfaces if this parameter is missing.
+          '';
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 10051;
+          description = ''
+            Listen port for trapper.
+          '';
+        };
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for the Zabbix Proxy.
+        '';
+      };
+
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ int str (listOf str) ]);
+        default = {};
+        description = ''
+          Zabbix Proxy configuration. Refer to
+          <link xlink:href="https://www.zabbix.com/documentation/current/manual/appendix/config/zabbix_proxy"/>
+          for details on supported values.
+        '';
+        example = {
+          CacheSize = "1G";
+          SSHKeyLocation = "/var/lib/zabbix/.ssh";
+          StartPingers = 32;
+        };
+      };
+
+    };
+
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = !config.services.zabbixServer.enable;
+        message = "Please choose one of services.zabbixServer or services.zabbixProxy.";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.user == user;
+        message = "services.zabbixProxy.database.user must be set to ${user} if services.zabbixProxy.database.createLocally is set true";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
+        message = "a password cannot be specified if services.zabbixProxy.database.createLocally is set to true";
+      }
+    ];
+
+    services.zabbixProxy.settings = mkMerge [
+      {
+        LogType = "console";
+        ListenIP = cfg.listen.ip;
+        ListenPort = cfg.listen.port;
+        Server = cfg.server;
+        # TODO: set to cfg.database.socket if database type is pgsql?
+        DBHost = optionalString (cfg.database.createLocally != true) cfg.database.host;
+        DBName = cfg.database.name;
+        DBUser = cfg.database.user;
+        SocketDir = runtimeDir;
+        FpingLocation = "/run/wrappers/bin/fping";
+        LoadModule = builtins.attrNames cfg.modules;
+      }
+      (mkIf (cfg.database.createLocally != true) { DBPort = cfg.database.port; })
+      (mkIf (cfg.database.passwordFile != null) { Include = [ "${passwordFile}" ]; })
+      (mkIf (mysqlLocal && cfg.database.socket != null) { DBSocket = cfg.database.socket; })
+      (mkIf (cfg.modules != {}) { LoadModulePath = "${moduleEnv}/lib"; })
+    ];
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listen.port ];
+    };
+
+    services.mysql = optionalAttrs mysqlLocal {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+    };
+
+    systemd.services.mysql.postStart = mkAfter (optionalString mysqlLocal ''
+      ( echo "CREATE DATABASE IF NOT EXISTS \`${cfg.database.name}\` CHARACTER SET utf8 COLLATE utf8_bin;"
+        echo "CREATE USER IF NOT EXISTS '${cfg.database.user}'@'localhost' IDENTIFIED WITH ${if (getName config.services.mysql.package == getName pkgs.mariadb) then "unix_socket" else "auth_socket"};"
+        echo "GRANT ALL PRIVILEGES ON \`${cfg.database.name}\`.* TO '${cfg.database.user}'@'localhost';"
+      ) | ${config.services.mysql.package}/bin/mysql -N
+    '');
+
+    services.postgresql = optionalAttrs pgsqlLocal {
+      enable = true;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    users.users.${user} = {
+      description = "Zabbix daemon user";
+      uid = config.ids.uids.zabbix;
+      inherit group;
+    };
+
+    users.groups.${group} = {
+      gid = config.ids.gids.zabbix;
+    };
+
+    security.wrappers = {
+      fping =
+        { setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${pkgs.fping}/bin/fping";
+        };
+    };
+
+    systemd.services.zabbix-proxy = {
+      description = "Zabbix Proxy";
+
+      wantedBy = [ "multi-user.target" ];
+      after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
+
+      path = [ "/run/wrappers" ] ++ cfg.extraPackages;
+      preStart = optionalString pgsqlLocal ''
+        if ! test -e "${stateDir}/db-created"; then
+          cat ${cfg.package}/share/zabbix/database/postgresql/schema.sql | ${pgsql.package}/bin/psql ${cfg.database.name}
+          touch "${stateDir}/db-created"
+        fi
+      '' + optionalString mysqlLocal ''
+        if ! test -e "${stateDir}/db-created"; then
+          cat ${cfg.package}/share/zabbix/database/mysql/schema.sql | ${mysql.package}/bin/mysql ${cfg.database.name}
+          touch "${stateDir}/db-created"
+        fi
+      '' + optionalString (cfg.database.type == "sqlite") ''
+        if ! test -e "${cfg.database.name}"; then
+          ${pkgs.sqlite}/bin/sqlite3 "${cfg.database.name}" < ${cfg.package}/share/zabbix/database/sqlite3/schema.sql
+        fi
+      '' + optionalString (cfg.database.passwordFile != null) ''
+        # create a copy of the supplied password file in a format zabbix can consume
+        touch ${passwordFile}
+        chmod 0600 ${passwordFile}
+        echo -n "DBPassword = " > ${passwordFile}
+        cat ${cfg.database.passwordFile} >> ${passwordFile}
+      '';
+
+      serviceConfig = {
+        ExecStart = "@${cfg.package}/sbin/zabbix_proxy zabbix_proxy -f --config ${configFile}";
+        Restart = "always";
+        RestartSec = 2;
+
+        User = user;
+        Group = group;
+        RuntimeDirectory = "zabbix";
+        StateDirectory = "zabbix";
+        PrivateTmp = true;
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/monitoring/zabbix-server.nix b/nixos/modules/services/monitoring/zabbix-server.nix
new file mode 100644
index 00000000000..9f960517a81
--- /dev/null
+++ b/nixos/modules/services/monitoring/zabbix-server.nix
@@ -0,0 +1,320 @@
+{ config, lib, options, pkgs, ... }:
+
+let
+  cfg = config.services.zabbixServer;
+  opt = options.services.zabbixServer;
+  pgsql = config.services.postgresql;
+  mysql = config.services.mysql;
+
+  inherit (lib) mkAfter mkDefault mkEnableOption mkIf mkMerge mkOption;
+  inherit (lib) attrValues concatMapStringsSep getName literalExpression optional optionalAttrs optionalString types;
+  inherit (lib.generators) toKeyValue;
+
+  user = "zabbix";
+  group = "zabbix";
+  runtimeDir = "/run/zabbix";
+  stateDir = "/var/lib/zabbix";
+  passwordFile = "${runtimeDir}/zabbix-dbpassword.conf";
+
+  moduleEnv = pkgs.symlinkJoin {
+    name = "zabbix-server-module-env";
+    paths = attrValues cfg.modules;
+  };
+
+  configFile = pkgs.writeText "zabbix_server.conf" (toKeyValue { listsAsDuplicateKeys = true; } cfg.settings);
+
+  mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
+  pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
+
+in
+
+{
+  imports = [
+    (lib.mkRenamedOptionModule [ "services" "zabbixServer" "dbServer" ] [ "services" "zabbixServer" "database" "host" ])
+    (lib.mkRemovedOptionModule [ "services" "zabbixServer" "dbPassword" ] "Use services.zabbixServer.database.passwordFile instead.")
+    (lib.mkRemovedOptionModule [ "services" "zabbixServer" "extraConfig" ] "Use services.zabbixServer.settings instead.")
+  ];
+
+  # interface
+
+  options = {
+
+    services.zabbixServer = {
+      enable = mkEnableOption "the Zabbix Server";
+
+      package = mkOption {
+        type = types.package;
+        default = if cfg.database.type == "mysql" then pkgs.zabbix.server-mysql else pkgs.zabbix.server-pgsql;
+        defaultText = literalExpression "pkgs.zabbix.server-pgsql";
+        description = "The Zabbix package to use.";
+      };
+
+      extraPackages = mkOption {
+        type = types.listOf types.package;
+        default = with pkgs; [ nettools nmap traceroute ];
+        defaultText = literalExpression "[ nettools nmap traceroute ]";
+        description = ''
+          Packages to be added to the Zabbix <envar>PATH</envar>.
+          Typically used to add executables for scripts, but can be anything.
+        '';
+      };
+
+      modules = mkOption {
+        type = types.attrsOf types.package;
+        description = "A set of modules to load.";
+        default = {};
+        example = literalExpression ''
+          {
+            "dummy.so" = pkgs.stdenv.mkDerivation {
+              name = "zabbix-dummy-module-''${cfg.package.version}";
+              src = cfg.package.src;
+              buildInputs = [ cfg.package ];
+              sourceRoot = "zabbix-''${cfg.package.version}/src/modules/dummy";
+              installPhase = '''
+                mkdir -p $out/lib
+                cp dummy.so $out/lib/
+              ''';
+            };
+          }
+        '';
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum [ "mysql" "pgsql" ];
+          example = "mysql";
+          default = "pgsql";
+          description = "Database engine to use.";
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = "Database host address.";
+        };
+
+        port = mkOption {
+          type = types.int;
+          default = if cfg.database.type == "mysql" then mysql.port else pgsql.port;
+          defaultText = literalExpression ''
+            if config.${opt.database.type} == "mysql"
+            then config.${options.services.mysql.port}
+            else config.${options.services.postgresql.port}
+          '';
+          description = "Database host port.";
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "zabbix";
+          description = "Database name.";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "zabbix";
+          description = "Database user.";
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          example = "/run/keys/zabbix-dbpassword";
+          description = ''
+            A file containing the password corresponding to
+            <option>database.user</option>.
+          '';
+        };
+
+        socket = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          example = "/run/postgresql";
+          description = "Path to the unix socket file to use for authentication.";
+        };
+
+        createLocally = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Whether to create a local database automatically.";
+        };
+      };
+
+      listen = {
+        ip = mkOption {
+          type = types.str;
+          default = "0.0.0.0";
+          description = ''
+            List of comma delimited IP addresses that the trapper should listen on.
+            Trapper will listen on all network interfaces if this parameter is missing.
+          '';
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 10051;
+          description = ''
+            Listen port for trapper.
+          '';
+        };
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for the Zabbix Server.
+        '';
+      };
+
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ int str (listOf str) ]);
+        default = {};
+        description = ''
+          Zabbix Server configuration. Refer to
+          <link xlink:href="https://www.zabbix.com/documentation/current/manual/appendix/config/zabbix_server"/>
+          for details on supported values.
+        '';
+        example = {
+          CacheSize = "1G";
+          SSHKeyLocation = "/var/lib/zabbix/.ssh";
+          StartPingers = 32;
+        };
+      };
+
+    };
+
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = cfg.database.createLocally -> cfg.database.user == user;
+        message = "services.zabbixServer.database.user must be set to ${user} if services.zabbixServer.database.createLocally is set true";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
+        message = "a password cannot be specified if services.zabbixServer.database.createLocally is set to true";
+      }
+    ];
+
+    services.zabbixServer.settings = mkMerge [
+      {
+        LogType = "console";
+        ListenIP = cfg.listen.ip;
+        ListenPort = cfg.listen.port;
+        # TODO: set to cfg.database.socket if database type is pgsql?
+        DBHost = optionalString (cfg.database.createLocally != true) cfg.database.host;
+        DBName = cfg.database.name;
+        DBUser = cfg.database.user;
+        PidFile = "${runtimeDir}/zabbix_server.pid";
+        SocketDir = runtimeDir;
+        FpingLocation = "/run/wrappers/bin/fping";
+        LoadModule = builtins.attrNames cfg.modules;
+      }
+      (mkIf (cfg.database.createLocally != true) { DBPort = cfg.database.port; })
+      (mkIf (cfg.database.passwordFile != null) { Include = [ "${passwordFile}" ]; })
+      (mkIf (mysqlLocal && cfg.database.socket != null) { DBSocket = cfg.database.socket; })
+      (mkIf (cfg.modules != {}) { LoadModulePath = "${moduleEnv}/lib"; })
+    ];
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listen.port ];
+    };
+
+    services.mysql = optionalAttrs mysqlLocal {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+    };
+
+    systemd.services.mysql.postStart = mkAfter (optionalString mysqlLocal ''
+      ( echo "CREATE DATABASE IF NOT EXISTS \`${cfg.database.name}\` CHARACTER SET utf8 COLLATE utf8_bin;"
+        echo "CREATE USER IF NOT EXISTS '${cfg.database.user}'@'localhost' IDENTIFIED WITH ${if (getName config.services.mysql.package == getName pkgs.mariadb) then "unix_socket" else "auth_socket"};"
+        echo "GRANT ALL PRIVILEGES ON \`${cfg.database.name}\`.* TO '${cfg.database.user}'@'localhost';"
+      ) | ${config.services.mysql.package}/bin/mysql -N
+    '');
+
+    services.postgresql = optionalAttrs pgsqlLocal {
+      enable = true;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    users.users.${user} = {
+      description = "Zabbix daemon user";
+      uid = config.ids.uids.zabbix;
+      inherit group;
+    };
+
+    users.groups.${group} = {
+      gid = config.ids.gids.zabbix;
+    };
+
+    security.wrappers = {
+      fping =
+        { setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${pkgs.fping}/bin/fping";
+        };
+    };
+
+    systemd.services.zabbix-server = {
+      description = "Zabbix Server";
+
+      wantedBy = [ "multi-user.target" ];
+      after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
+
+      path = [ "/run/wrappers" ] ++ cfg.extraPackages;
+      preStart = ''
+        # pre 19.09 compatibility
+        if test -e "${runtimeDir}/db-created"; then
+          mv "${runtimeDir}/db-created" "${stateDir}/"
+        fi
+      '' + optionalString pgsqlLocal ''
+        if ! test -e "${stateDir}/db-created"; then
+          cat ${cfg.package}/share/zabbix/database/postgresql/schema.sql | ${pgsql.package}/bin/psql ${cfg.database.name}
+          cat ${cfg.package}/share/zabbix/database/postgresql/images.sql | ${pgsql.package}/bin/psql ${cfg.database.name}
+          cat ${cfg.package}/share/zabbix/database/postgresql/data.sql | ${pgsql.package}/bin/psql ${cfg.database.name}
+          touch "${stateDir}/db-created"
+        fi
+      '' + optionalString mysqlLocal ''
+        if ! test -e "${stateDir}/db-created"; then
+          cat ${cfg.package}/share/zabbix/database/mysql/schema.sql | ${mysql.package}/bin/mysql ${cfg.database.name}
+          cat ${cfg.package}/share/zabbix/database/mysql/images.sql | ${mysql.package}/bin/mysql ${cfg.database.name}
+          cat ${cfg.package}/share/zabbix/database/mysql/data.sql | ${mysql.package}/bin/mysql ${cfg.database.name}
+          touch "${stateDir}/db-created"
+        fi
+      '' + optionalString (cfg.database.passwordFile != null) ''
+        # create a copy of the supplied password file in a format zabbix can consume
+        touch ${passwordFile}
+        chmod 0600 ${passwordFile}
+        echo -n "DBPassword = " > ${passwordFile}
+        cat ${cfg.database.passwordFile} >> ${passwordFile}
+      '';
+
+      serviceConfig = {
+        ExecStart = "@${cfg.package}/sbin/zabbix_server zabbix_server -f --config ${configFile}";
+        Restart = "always";
+        RestartSec = 2;
+
+        User = user;
+        Group = group;
+        RuntimeDirectory = "zabbix";
+        StateDirectory = "zabbix";
+        PrivateTmp = true;
+      };
+    };
+
+    systemd.services.httpd.after =
+      optional (config.services.zabbixWeb.enable && mysqlLocal) "mysql.service" ++
+      optional (config.services.zabbixWeb.enable && pgsqlLocal) "postgresql.service";
+
+  };
+
+}
diff --git a/nixos/modules/services/network-filesystems/cachefilesd.nix b/nixos/modules/services/network-filesystems/cachefilesd.nix
new file mode 100644
index 00000000000..229c9665419
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/cachefilesd.nix
@@ -0,0 +1,63 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.cachefilesd;
+
+  cfgFile = pkgs.writeText "cachefilesd.conf" ''
+    dir ${cfg.cacheDir}
+    ${cfg.extraConfig}
+  '';
+
+in
+
+{
+  options = {
+    services.cachefilesd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable cachefilesd network filesystems caching daemon.";
+      };
+
+      cacheDir = mkOption {
+        type = types.str;
+        default = "/var/cache/fscache";
+        description = "Directory to contain filesystem cache.";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = "brun 10%";
+        description = "Additional configuration file entries. See cachefilesd.conf(5) for more information.";
+      };
+
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    boot.kernelModules = [ "cachefiles" ];
+
+    systemd.services.cachefilesd = {
+      description = "Local network file caching management daemon";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "exec";
+        ExecStart = "${pkgs.cachefilesd}/bin/cachefilesd -n -f ${cfgFile}";
+        Restart = "on-failure";
+        PrivateTmp = true;
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d ${cfg.cacheDir} 0700 root root - -"
+    ];
+  };
+}
diff --git a/nixos/modules/services/network-filesystems/ceph.nix b/nixos/modules/services/network-filesystems/ceph.nix
new file mode 100644
index 00000000000..7a1444decaf
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/ceph.nix
@@ -0,0 +1,406 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg  = config.services.ceph;
+
+  # function that translates "camelCaseOptions" to "camel case options", credits to tilpner in #nixos@freenode
+  expandCamelCase = replaceStrings upperChars (map (s: " ${s}") lowerChars);
+  expandCamelCaseAttrs = mapAttrs' (name: value: nameValuePair (expandCamelCase name) value);
+
+  makeServices = (daemonType: daemonIds:
+    mkMerge (map (daemonId:
+      { "ceph-${daemonType}-${daemonId}" = makeService daemonType daemonId cfg.global.clusterName pkgs.ceph; })
+      daemonIds));
+
+  makeService = (daemonType: daemonId: clusterName: ceph:
+    let
+      stateDirectory = "ceph/${if daemonType == "rgw" then "radosgw" else daemonType}/${clusterName}-${daemonId}"; in {
+    enable = true;
+    description = "Ceph ${builtins.replaceStrings lowerChars upperChars daemonType} daemon ${daemonId}";
+    after = [ "network-online.target" "time-sync.target" ] ++ optional (daemonType == "osd") "ceph-mon.target";
+    wants = [ "network-online.target" "time-sync.target" ];
+    partOf = [ "ceph-${daemonType}.target" ];
+    wantedBy = [ "ceph-${daemonType}.target" ];
+
+    path = [ pkgs.getopt ];
+
+    # Don't start services that are not yet initialized
+    unitConfig.ConditionPathExists = "/var/lib/${stateDirectory}/keyring";
+    startLimitBurst =
+      if daemonType == "osd" then 30 else if lib.elem daemonType ["mgr" "mds"] then 3 else 5;
+    startLimitIntervalSec = 60 * 30;  # 30 mins
+
+    serviceConfig = {
+      LimitNOFILE = 1048576;
+      LimitNPROC = 1048576;
+      Environment = "CLUSTER=${clusterName}";
+      ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+      PrivateDevices = "yes";
+      PrivateTmp = "true";
+      ProtectHome = "true";
+      ProtectSystem = "full";
+      Restart = "on-failure";
+      StateDirectory = stateDirectory;
+      User = "ceph";
+      Group = if daemonType == "osd" then "disk" else "ceph";
+      ExecStart = ''${ceph.out}/bin/${if daemonType == "rgw" then "radosgw" else "ceph-${daemonType}"} \
+                    -f --cluster ${clusterName} --id ${daemonId}'';
+    } // optionalAttrs (daemonType == "osd") {
+      ExecStartPre = "${ceph.lib}/libexec/ceph/ceph-osd-prestart.sh --id ${daemonId} --cluster ${clusterName}";
+      RestartSec = "20s";
+      PrivateDevices = "no"; # osd needs disk access
+    } // optionalAttrs ( daemonType == "mon") {
+      RestartSec = "10";
+    };
+  });
+
+  makeTarget = (daemonType:
+    {
+      "ceph-${daemonType}" = {
+        description = "Ceph target allowing to start/stop all ceph-${daemonType} services at once";
+        partOf = [ "ceph.target" ];
+        wantedBy = [ "ceph.target" ];
+        before = [ "ceph.target" ];
+        unitConfig.StopWhenUnneeded = true;
+      };
+    }
+  );
+in
+{
+  options.services.ceph = {
+    # Ceph has a monolithic configuration file but different sections for
+    # each daemon, a separate client section and a global section
+    enable = mkEnableOption "Ceph global configuration";
+
+    global = {
+      fsid = mkOption {
+        type = types.str;
+        example = ''
+          433a2193-4f8a-47a0-95d2-209d7ca2cca5
+        '';
+        description = ''
+          Filesystem ID, a generated uuid, its must be generated and set before
+          attempting to start a cluster
+        '';
+      };
+
+      clusterName = mkOption {
+        type = types.str;
+        default = "ceph";
+        description = ''
+          Name of cluster
+        '';
+      };
+
+      mgrModulePath = mkOption {
+        type = types.path;
+        default = "${pkgs.ceph.lib}/lib/ceph/mgr";
+        defaultText = literalExpression ''"''${pkgs.ceph.lib}/lib/ceph/mgr"'';
+        description = ''
+          Path at which to find ceph-mgr modules.
+        '';
+      };
+
+      monInitialMembers = mkOption {
+        type = with types; nullOr commas;
+        default = null;
+        example = ''
+          node0, node1, node2
+        '';
+        description = ''
+          List of hosts that will be used as monitors at startup.
+        '';
+      };
+
+      monHost = mkOption {
+        type = with types; nullOr commas;
+        default = null;
+        example = ''
+          10.10.0.1, 10.10.0.2, 10.10.0.3
+        '';
+        description = ''
+          List of hostname shortnames/IP addresses of the initial monitors.
+        '';
+      };
+
+      maxOpenFiles = mkOption {
+        type = types.int;
+        default = 131072;
+        description = ''
+          Max open files for each OSD daemon.
+        '';
+      };
+
+      authClusterRequired = mkOption {
+        type = types.enum [ "cephx" "none" ];
+        default = "cephx";
+        description = ''
+          Enables requiring daemons to authenticate with eachother in the cluster.
+        '';
+      };
+
+      authServiceRequired = mkOption {
+        type = types.enum [ "cephx" "none" ];
+        default = "cephx";
+        description = ''
+          Enables requiring clients to authenticate with the cluster to access services in the cluster (e.g. radosgw, mds or osd).
+        '';
+      };
+
+      authClientRequired = mkOption {
+        type = types.enum [ "cephx" "none" ];
+        default = "cephx";
+        description = ''
+          Enables requiring the cluster to authenticate itself to the client.
+        '';
+      };
+
+      publicNetwork = mkOption {
+        type = with types; nullOr commas;
+        default = null;
+        example = ''
+          10.20.0.0/24, 192.168.1.0/24
+        '';
+        description = ''
+          A comma-separated list of subnets that will be used as public networks in the cluster.
+        '';
+      };
+
+      clusterNetwork = mkOption {
+        type = with types; nullOr commas;
+        default = null;
+        example = ''
+          10.10.0.0/24, 192.168.0.0/24
+        '';
+        description = ''
+          A comma-separated list of subnets that will be used as cluster networks in the cluster.
+        '';
+      };
+
+      rgwMimeTypesFile = mkOption {
+        type = with types; nullOr path;
+        default = "${pkgs.mailcap}/etc/mime.types";
+        defaultText = literalExpression ''"''${pkgs.mailcap}/etc/mime.types"'';
+        description = ''
+          Path to mime types used by radosgw.
+        '';
+      };
+    };
+
+    extraConfig = mkOption {
+      type = with types; attrsOf str;
+      default = {};
+      example = {
+        "ms bind ipv6" = "true";
+      };
+      description = ''
+        Extra configuration to add to the global section. Use for setting values that are common for all daemons in the cluster.
+      '';
+    };
+
+    mgr = {
+      enable = mkEnableOption "Ceph MGR daemon";
+      daemons = mkOption {
+        type = with types; listOf str;
+        default = [];
+        example = [ "name1" "name2" ];
+        description = ''
+          A list of names for manager daemons that should have a service created. The names correspond
+          to the id part in ceph i.e. [ "name1" ] would result in mgr.name1
+        '';
+      };
+      extraConfig = mkOption {
+        type = with types; attrsOf str;
+        default = {};
+        description = ''
+          Extra configuration to add to the global section for manager daemons.
+        '';
+      };
+    };
+
+    mon = {
+      enable = mkEnableOption "Ceph MON daemon";
+      daemons = mkOption {
+        type = with types; listOf str;
+        default = [];
+        example = [ "name1" "name2" ];
+        description = ''
+          A list of monitor daemons that should have a service created. The names correspond
+          to the id part in ceph i.e. [ "name1" ] would result in mon.name1
+        '';
+      };
+      extraConfig = mkOption {
+        type = with types; attrsOf str;
+        default = {};
+        description = ''
+          Extra configuration to add to the monitor section.
+        '';
+      };
+    };
+
+    osd = {
+      enable = mkEnableOption "Ceph OSD daemon";
+      daemons = mkOption {
+        type = with types; listOf str;
+        default = [];
+        example = [ "name1" "name2" ];
+        description = ''
+          A list of OSD daemons that should have a service created. The names correspond
+          to the id part in ceph i.e. [ "name1" ] would result in osd.name1
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = with types; attrsOf str;
+        default = {
+          "osd journal size" = "10000";
+          "osd pool default size" = "3";
+          "osd pool default min size" = "2";
+          "osd pool default pg num" = "200";
+          "osd pool default pgp num" = "200";
+          "osd crush chooseleaf type" = "1";
+        };
+        description = ''
+          Extra configuration to add to the OSD section.
+        '';
+      };
+    };
+
+    mds = {
+      enable = mkEnableOption "Ceph MDS daemon";
+      daemons = mkOption {
+        type = with types; listOf str;
+        default = [];
+        example = [ "name1" "name2" ];
+        description = ''
+          A list of metadata service daemons that should have a service created. The names correspond
+          to the id part in ceph i.e. [ "name1" ] would result in mds.name1
+        '';
+      };
+      extraConfig = mkOption {
+        type = with types; attrsOf str;
+        default = {};
+        description = ''
+          Extra configuration to add to the MDS section.
+        '';
+      };
+    };
+
+    rgw = {
+      enable = mkEnableOption "Ceph RadosGW daemon";
+      daemons = mkOption {
+        type = with types; listOf str;
+        default = [];
+        example = [ "name1" "name2" ];
+        description = ''
+          A list of rados gateway daemons that should have a service created. The names correspond
+          to the id part in ceph i.e. [ "name1" ] would result in client.name1, radosgw daemons
+          aren't daemons to cluster in the sense that OSD, MGR or MON daemons are. They are simply
+          daemons, from ceph, that uses the cluster as a backend.
+        '';
+      };
+    };
+
+    client = {
+      enable = mkEnableOption "Ceph client configuration";
+      extraConfig = mkOption {
+        type = with types; attrsOf (attrsOf str);
+        default = {};
+        example = literalExpression ''
+          {
+            # This would create a section for a radosgw daemon named node0 and related
+            # configuration for it
+            "client.radosgw.node0" = { "some config option" = "true"; };
+          };
+        '';
+        description = ''
+          Extra configuration to add to the client section. Configuration for rados gateways
+          would be added here, with their own sections, see example.
+        '';
+      };
+    };
+  };
+
+  config = mkIf config.services.ceph.enable {
+    assertions = [
+      { assertion = cfg.global.fsid != "";
+        message = "fsid has to be set to a valid uuid for the cluster to function";
+      }
+      { assertion = cfg.mon.enable == true -> cfg.mon.daemons != [];
+        message = "have to set id of atleast one MON if you're going to enable Monitor";
+      }
+      { assertion = cfg.mds.enable == true -> cfg.mds.daemons != [];
+        message = "have to set id of atleast one MDS if you're going to enable Metadata Service";
+      }
+      { assertion = cfg.osd.enable == true -> cfg.osd.daemons != [];
+        message = "have to set id of atleast one OSD if you're going to enable OSD";
+      }
+      { assertion = cfg.mgr.enable == true -> cfg.mgr.daemons != [];
+        message = "have to set id of atleast one MGR if you're going to enable MGR";
+      }
+    ];
+
+    warnings = optional (cfg.global.monInitialMembers == null)
+      "Not setting up a list of members in monInitialMembers requires that you set the host variable for each mon daemon or else the cluster won't function";
+
+    environment.etc."ceph/ceph.conf".text = let
+      # Merge the extraConfig set for mgr daemons, as mgr don't have their own section
+      globalSection = expandCamelCaseAttrs (cfg.global // cfg.extraConfig // optionalAttrs cfg.mgr.enable cfg.mgr.extraConfig);
+      # Remove all name-value pairs with null values from the attribute set to avoid making empty sections in the ceph.conf
+      globalSection' = filterAttrs (name: value: value != null) globalSection;
+      totalConfig = {
+          global = globalSection';
+        } // optionalAttrs (cfg.mon.enable && cfg.mon.extraConfig != {}) { mon = cfg.mon.extraConfig; }
+          // optionalAttrs (cfg.mds.enable && cfg.mds.extraConfig != {}) { mds = cfg.mds.extraConfig; }
+          // optionalAttrs (cfg.osd.enable && cfg.osd.extraConfig != {}) { osd = cfg.osd.extraConfig; }
+          // optionalAttrs (cfg.client.enable && cfg.client.extraConfig != {})  cfg.client.extraConfig;
+      in
+        generators.toINI {} totalConfig;
+
+    users.users.ceph = {
+      uid = config.ids.uids.ceph;
+      description = "Ceph daemon user";
+      group = "ceph";
+      extraGroups = [ "disk" ];
+    };
+
+    users.groups.ceph = {
+      gid = config.ids.gids.ceph;
+    };
+
+    systemd.services = let
+      services = []
+        ++ optional cfg.mon.enable (makeServices "mon" cfg.mon.daemons)
+        ++ optional cfg.mds.enable (makeServices "mds" cfg.mds.daemons)
+        ++ optional cfg.osd.enable (makeServices "osd" cfg.osd.daemons)
+        ++ optional cfg.rgw.enable (makeServices "rgw" cfg.rgw.daemons)
+        ++ optional cfg.mgr.enable (makeServices "mgr" cfg.mgr.daemons);
+      in
+        mkMerge services;
+
+    systemd.targets = let
+      targets = [
+        { ceph = {
+          description = "Ceph target allowing to start/stop all ceph service instances at once";
+          wantedBy = [ "multi-user.target" ];
+          unitConfig.StopWhenUnneeded = true;
+        }; } ]
+        ++ optional cfg.mon.enable (makeTarget "mon")
+        ++ optional cfg.mds.enable (makeTarget "mds")
+        ++ optional cfg.osd.enable (makeTarget "osd")
+        ++ optional cfg.rgw.enable (makeTarget "rgw")
+        ++ optional cfg.mgr.enable (makeTarget "mgr");
+      in
+        mkMerge targets;
+
+    systemd.tmpfiles.rules = [
+      "d /etc/ceph - ceph ceph - -"
+      "d /run/ceph 0770 ceph ceph -"
+      "d /var/lib/ceph - ceph ceph - -"]
+    ++ optionals cfg.mgr.enable [ "d /var/lib/ceph/mgr - ceph ceph - -"]
+    ++ optionals cfg.mon.enable [ "d /var/lib/ceph/mon - ceph ceph - -"]
+    ++ optionals cfg.osd.enable [ "d /var/lib/ceph/osd - ceph ceph - -"];
+  };
+}
diff --git a/nixos/modules/services/network-filesystems/davfs2.nix b/nixos/modules/services/network-filesystems/davfs2.nix
new file mode 100644
index 00000000000..8cf314fe63a
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/davfs2.nix
@@ -0,0 +1,93 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.davfs2;
+  cfgFile = pkgs.writeText "davfs2.conf" ''
+    dav_user ${cfg.davUser}
+    dav_group ${cfg.davGroup}
+    ${cfg.extraConfig}
+  '';
+in
+{
+  options.services.davfs2 = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable davfs2.
+      '';
+    };
+
+    davUser = mkOption {
+      type = types.str;
+      default = "davfs2";
+      description = ''
+        When invoked by root the mount.davfs daemon will run as this user.
+        Value must be given as name, not as numerical id.
+      '';
+    };
+
+    davGroup = mkOption {
+      type = types.str;
+      default = "davfs2";
+      description = ''
+        The group of the running mount.davfs daemon. Ordinary users must be
+        member of this group in order to mount a davfs2 file system. Value must
+        be given as name, not as numerical id.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        kernel_fs coda
+        proxy foo.bar:8080
+        use_locks 0
+      '';
+      description = ''
+        Extra lines appended to the configuration of davfs2.
+      ''  ;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.davfs2 ];
+    environment.etc."davfs2/davfs2.conf".source = cfgFile;
+
+    users.groups = optionalAttrs (cfg.davGroup == "davfs2") {
+      davfs2.gid = config.ids.gids.davfs2;
+    };
+
+    users.users = optionalAttrs (cfg.davUser == "davfs2") {
+      davfs2 = {
+        createHome = false;
+        group = cfg.davGroup;
+        uid = config.ids.uids.davfs2;
+        description = "davfs2 user";
+      };
+    };
+
+    security.wrappers."mount.davfs" = {
+      program = "mount.davfs";
+      source = "${pkgs.davfs2}/bin/mount.davfs";
+      owner = "root";
+      group = cfg.davGroup;
+      setuid = true;
+      permissions = "u+rx,g+x";
+    };
+
+    security.wrappers."umount.davfs" = {
+      program = "umount.davfs";
+      source = "${pkgs.davfs2}/bin/umount.davfs";
+      owner = "root";
+      group = cfg.davGroup;
+      setuid = true;
+      permissions = "u+rx,g+x";
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/network-filesystems/diod.nix b/nixos/modules/services/network-filesystems/diod.nix
new file mode 100644
index 00000000000..063bae6ddb1
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/diod.nix
@@ -0,0 +1,159 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.diod;
+
+  diodBool = b: if b then "1" else "0";
+
+  diodConfig = pkgs.writeText "diod.conf" ''
+    allsquash = ${diodBool cfg.allsquash}
+    auth_required = ${diodBool cfg.authRequired}
+    exportall = ${diodBool cfg.exportall}
+    exportopts = "${concatStringsSep "," cfg.exportopts}"
+    exports = { ${concatStringsSep ", " (map (s: ''"${s}"'' ) cfg.exports)} }
+    listen = { ${concatStringsSep ", " (map (s: ''"${s}"'' ) cfg.listen)} }
+    logdest = "${cfg.logdest}"
+    nwthreads = ${toString cfg.nwthreads}
+    squashuser = "${cfg.squashuser}"
+    statfs_passthru = ${diodBool cfg.statfsPassthru}
+    userdb = ${diodBool cfg.userdb}
+    ${cfg.extraConfig}
+  '';
+in
+{
+  options = {
+    services.diod = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the diod 9P file server.";
+      };
+
+      listen = mkOption {
+        type = types.listOf types.str;
+        default = [ "0.0.0.0:564" ];
+        description = ''
+          [ "IP:PORT" [,"IP:PORT",...] ]
+          List the interfaces and ports that diod should listen on.
+        '';
+      };
+
+      exports = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          List the file systems that clients will be allowed to mount. All paths should
+          be fully qualified. The exports table can include two types of element:
+          a string element (as above),
+          or an alternate table element form { path="/path", opts="ro" }.
+          In the alternate form, the (optional) opts attribute is a comma-separated list
+          of export options. The two table element forms can be mixed in the exports
+          table. Note that although diod will not traverse file system boundaries for a
+          given mount due to inode uniqueness constraints, subdirectories of a file
+          system can be separately exported.
+        '';
+      };
+
+      exportall = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Export all file systems listed in /proc/mounts. If new file systems are mounted
+          after diod has started, they will become immediately mountable. If there is a
+          duplicate entry for a file system in the exports list, any options listed in
+          the exports entry will apply.
+        '';
+      };
+
+      exportopts = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Establish a default set of export options. These are overridden, not appended
+          to, by opts attributes in an "exports" entry.
+        '';
+      };
+
+      nwthreads = mkOption {
+        type = types.int;
+        default = 16;
+        description = ''
+          Sets the (fixed) number of worker threads created to handle 9P
+          requests for a unique aname.
+        '';
+      };
+
+      authRequired = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Allow clients to connect without authentication, i.e. without a valid MUNGE credential.
+        '';
+      };
+
+      userdb = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          This option disables password/group lookups. It allows any uid to attach and
+          assumes gid=uid, and supplementary groups contain only the primary gid.
+        '';
+      };
+
+      allsquash = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Remap all users to "nobody". The attaching user need not be present in the
+          password file.
+        '';
+      };
+
+      squashuser = mkOption {
+        type = types.str;
+        default = "nobody";
+        description = ''
+          Change the squash user. The squash user must be present in the password file.
+        '';
+      };
+
+      logdest = mkOption {
+        type = types.str;
+        default = "syslog:daemon:err";
+        description = ''
+          Set the destination for logging.
+          The value has the form of "syslog:facility:level" or "filename".
+        '';
+      };
+
+
+      statfsPassthru = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          This option configures statfs to return the host file system's type
+          rather than V9FS_MAGIC.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Extra configuration options for diod.conf.";
+      };
+    };
+  };
+
+  config = mkIf config.services.diod.enable {
+    environment.systemPackages = [ pkgs.diod ];
+
+    systemd.services.diod = {
+      description = "diod 9P file server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.diod}/sbin/diod -f -c ${diodConfig}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/network-filesystems/drbd.nix b/nixos/modules/services/network-filesystems/drbd.nix
new file mode 100644
index 00000000000..c730e0b34e9
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/drbd.nix
@@ -0,0 +1,63 @@
+# Support for DRBD, the Distributed Replicated Block Device.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.drbd; in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.drbd.enable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to enable support for DRBD, the Distributed Replicated
+        Block Device.
+      '';
+    };
+
+    services.drbd.config = mkOption {
+      default = "";
+      type = types.lines;
+      description = ''
+        Contents of the <filename>drbd.conf</filename> configuration file.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.drbd ];
+
+    services.udev.packages = [ pkgs.drbd ];
+
+    boot.kernelModules = [ "drbd" ];
+
+    boot.extraModprobeConfig =
+      ''
+        options drbd usermode_helper=/run/current-system/sw/bin/drbdadm
+      '';
+
+    environment.etc."drbd.conf" =
+      { source = pkgs.writeText "drbd.conf" cfg.config; };
+
+    systemd.services.drbd = {
+      after = [ "systemd-udev.settle.service" "network.target" ];
+      wants = [ "systemd-udev.settle.service" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.drbd}/sbin/drbdadm up all";
+        ExecStop = "${pkgs.drbd}/sbin/drbdadm down all";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/network-filesystems/glusterfs.nix b/nixos/modules/services/network-filesystems/glusterfs.nix
new file mode 100644
index 00000000000..38be098de5d
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/glusterfs.nix
@@ -0,0 +1,208 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  inherit (pkgs) glusterfs rsync;
+
+  tlsCmd = if (cfg.tlsSettings != null) then
+  ''
+    mkdir -p /var/lib/glusterd
+    touch /var/lib/glusterd/secure-access
+  ''
+  else
+  ''
+    rm -f /var/lib/glusterd/secure-access
+  '';
+
+  restartTriggers = if (cfg.tlsSettings != null) then [
+    config.environment.etc."ssl/glusterfs.pem".source
+    config.environment.etc."ssl/glusterfs.key".source
+    config.environment.etc."ssl/glusterfs.ca".source
+  ] else [];
+
+  cfg = config.services.glusterfs;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.glusterfs = {
+
+      enable = mkEnableOption "GlusterFS Daemon";
+
+      logLevel = mkOption {
+        type = types.enum ["DEBUG" "INFO" "WARNING" "ERROR" "CRITICAL" "TRACE" "NONE"];
+        description = "Log level used by the GlusterFS daemon";
+        default = "INFO";
+      };
+
+      useRpcbind = mkOption {
+        type = types.bool;
+        description = ''
+          Enable use of rpcbind. This is required for Gluster's NFS functionality.
+
+          You may want to turn it off to reduce the attack surface for DDoS reflection attacks.
+
+          See https://davelozier.com/glusterfs-and-rpcbind-portmap-ddos-reflection-attacks/
+          and https://bugzilla.redhat.com/show_bug.cgi?id=1426842 for details.
+        '';
+        default = true;
+      };
+
+      enableGlustereventsd = mkOption {
+        type = types.bool;
+        description = "Whether to enable the GlusterFS Events Daemon";
+        default = true;
+      };
+
+      killMode = mkOption {
+        type = types.enum ["control-group" "process" "mixed" "none"];
+        description = ''
+          The systemd KillMode to use for glusterd.
+
+          glusterd spawns other daemons like gsyncd.
+          If you want these to stop when glusterd is stopped (e.g. to ensure
+          that NixOS config changes are reflected even for these sub-daemons),
+          set this to 'control-group'.
+          If however you want running volume processes (glusterfsd) and thus
+          gluster mounts not be interrupted when glusterd is restarted
+          (for example, when you want to restart them manually at a later time),
+          set this to 'process'.
+        '';
+        default = "control-group";
+      };
+
+      stopKillTimeout = mkOption {
+        type = types.str;
+        description = ''
+          The systemd TimeoutStopSec to use.
+
+          After this time after having been asked to shut down, glusterd
+          (and depending on the killMode setting also its child processes)
+          are killed by systemd.
+
+          The default is set low because GlusterFS (as of 3.10) is known to
+          not tell its children (like gsyncd) to terminate at all.
+        '';
+        default = "5s";
+      };
+
+      extraFlags = mkOption {
+        type = types.listOf types.str;
+        description = "Extra flags passed to the GlusterFS daemon";
+        default = [];
+      };
+
+      tlsSettings = mkOption {
+        description = ''
+          Make the server communicate via TLS.
+          This means it will only connect to other gluster
+          servers having certificates signed by the same CA.
+
+          Enabling this will create a file <filename>/var/lib/glusterd/secure-access</filename>.
+          Disabling will delete this file again.
+
+          See also: https://gluster.readthedocs.io/en/latest/Administrator%20Guide/SSL/
+        '';
+        default = null;
+        type = types.nullOr (types.submodule {
+          options = {
+            tlsKeyPath = mkOption {
+              type = types.str;
+              description = "Path to the private key used for TLS.";
+            };
+
+            tlsPem = mkOption {
+              type = types.path;
+              description = "Path to the certificate used for TLS.";
+            };
+
+            caCert = mkOption {
+              type = types.path;
+              description = "Path certificate authority used to sign the cluster certificates.";
+            };
+          };
+        });
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.glusterfs ];
+
+    services.rpcbind.enable = cfg.useRpcbind;
+
+    environment.etc = mkIf (cfg.tlsSettings != null) {
+      "ssl/glusterfs.pem".source = cfg.tlsSettings.tlsPem;
+      "ssl/glusterfs.key".source = cfg.tlsSettings.tlsKeyPath;
+      "ssl/glusterfs.ca".source = cfg.tlsSettings.caCert;
+    };
+
+    systemd.services.glusterd = {
+      inherit restartTriggers;
+
+      description = "GlusterFS, a clustered file-system server";
+
+      wantedBy = [ "multi-user.target" ];
+
+      requires = lib.optional cfg.useRpcbind "rpcbind.service";
+      after = [ "network.target" ] ++ lib.optional cfg.useRpcbind "rpcbind.service";
+
+      preStart = ''
+        install -m 0755 -d /var/log/glusterfs
+      ''
+      # The copying of hooks is due to upstream bug https://bugzilla.redhat.com/show_bug.cgi?id=1452761
+      + ''
+        mkdir -p /var/lib/glusterd/hooks/
+        ${rsync}/bin/rsync -a ${glusterfs}/var/lib/glusterd/hooks/ /var/lib/glusterd/hooks/
+
+        ${tlsCmd}
+      ''
+      # `glusterfind` needs dirs that upstream installs at `make install` phase
+      # https://github.com/gluster/glusterfs/blob/v3.10.2/tools/glusterfind/Makefile.am#L16-L17
+      + ''
+        mkdir -p /var/lib/glusterd/glusterfind/.keys
+        mkdir -p /var/lib/glusterd/hooks/1/delete/post/
+      '';
+
+      serviceConfig = {
+        LimitNOFILE=65536;
+        ExecStart="${glusterfs}/sbin/glusterd --no-daemon --log-level=${cfg.logLevel} ${toString cfg.extraFlags}";
+        KillMode=cfg.killMode;
+        TimeoutStopSec=cfg.stopKillTimeout;
+      };
+    };
+
+    systemd.services.glustereventsd = mkIf cfg.enableGlustereventsd {
+      inherit restartTriggers;
+
+      description = "Gluster Events Notifier";
+
+      wantedBy = [ "multi-user.target" ];
+
+      after = [ "network.target" ];
+
+      preStart = ''
+        install -m 0755 -d /var/log/glusterfs
+      '';
+
+      # glustereventsd uses the `gluster` executable
+      path = [ glusterfs ];
+
+      serviceConfig = {
+        Type="simple";
+        PIDFile="/run/glustereventsd.pid";
+        ExecStart="${glusterfs}/sbin/glustereventsd --pid-file /run/glustereventsd.pid";
+        ExecReload="/bin/kill -SIGUSR2 $MAINPID";
+        KillMode="control-group";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/network-filesystems/ipfs.nix b/nixos/modules/services/network-filesystems/ipfs.nix
new file mode 100644
index 00000000000..17da020bf3e
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/ipfs.nix
@@ -0,0 +1,311 @@
+{ config, lib, pkgs, options, ... }:
+with lib;
+let
+  cfg = config.services.ipfs;
+  opt = options.services.ipfs;
+
+  ipfsFlags = toString ([
+    (optionalString cfg.autoMount "--mount")
+    (optionalString cfg.enableGC "--enable-gc")
+    (optionalString (cfg.serviceFdlimit != null) "--manage-fdlimit=false")
+    (optionalString (cfg.defaultMode == "offline") "--offline")
+    (optionalString (cfg.defaultMode == "norouting") "--routing=none")
+  ] ++ cfg.extraFlags);
+
+  profile =
+    if cfg.localDiscovery
+    then "local-discovery"
+    else "server";
+
+  splitMulitaddr = addrRaw: lib.tail (lib.splitString "/" addrRaw);
+
+  multiaddrToListenStream = addrRaw:
+    let
+      addr = splitMulitaddr addrRaw;
+      s = builtins.elemAt addr;
+    in
+    if s 0 == "ip4" && s 2 == "tcp"
+    then "${s 1}:${s 3}"
+    else if s 0 == "ip6" && s 2 == "tcp"
+    then "[${s 1}]:${s 3}"
+    else if s 0 == "unix"
+    then "/${lib.concatStringsSep "/" (lib.tail addr)}"
+    else null; # not valid for listen stream, skip
+
+  multiaddrToListenDatagram = addrRaw:
+    let
+      addr = splitMulitaddr addrRaw;
+      s = builtins.elemAt addr;
+    in
+    if s 0 == "ip4" && s 2 == "udp"
+    then "${s 1}:${s 3}"
+    else if s 0 == "ip6" && s 2 == "udp"
+    then "[${s 1}]:${s 3}"
+    else null; # not valid for listen datagram, skip
+
+in
+{
+
+  ###### interface
+
+  options = {
+
+    services.ipfs = {
+
+      enable = mkEnableOption "Interplanetary File System (WARNING: may cause severe network degredation)";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.ipfs;
+        defaultText = literalExpression "pkgs.ipfs";
+        description = "Which IPFS package to use.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "ipfs";
+        description = "User under which the IPFS daemon runs";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "ipfs";
+        description = "Group under which the IPFS daemon runs";
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default =
+          if versionAtLeast config.system.stateVersion "17.09"
+          then "/var/lib/ipfs"
+          else "/var/lib/ipfs/.ipfs";
+        defaultText = literalExpression ''
+          if versionAtLeast config.system.stateVersion "17.09"
+          then "/var/lib/ipfs"
+          else "/var/lib/ipfs/.ipfs"
+        '';
+        description = "The data dir for IPFS";
+      };
+
+      defaultMode = mkOption {
+        type = types.enum [ "online" "offline" "norouting" ];
+        default = "online";
+        description = "systemd service that is enabled by default";
+      };
+
+      autoMount = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether IPFS should try to mount /ipfs and /ipns at startup.";
+      };
+
+      autoMigrate = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether IPFS should try to run the fs-repo-migration at startup.";
+      };
+
+      ipfsMountDir = mkOption {
+        type = types.str;
+        default = "/ipfs";
+        description = "Where to mount the IPFS namespace to";
+      };
+
+      ipnsMountDir = mkOption {
+        type = types.str;
+        default = "/ipns";
+        description = "Where to mount the IPNS namespace to";
+      };
+
+      gatewayAddress = mkOption {
+        type = types.str;
+        default = "/ip4/127.0.0.1/tcp/8080";
+        description = "Where the IPFS Gateway can be reached";
+      };
+
+      apiAddress = mkOption {
+        type = types.str;
+        default = "/ip4/127.0.0.1/tcp/5001";
+        description = "Where IPFS exposes its API to";
+      };
+
+      swarmAddress = mkOption {
+        type = types.listOf types.str;
+        default = [
+          "/ip4/0.0.0.0/tcp/4001"
+          "/ip6/::/tcp/4001"
+          "/ip4/0.0.0.0/udp/4001/quic"
+          "/ip6/::/udp/4001/quic"
+        ];
+        description = "Where IPFS listens for incoming p2p connections";
+      };
+
+      enableGC = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable automatic garbage collection";
+      };
+
+      emptyRepo = mkOption {
+        type = types.bool;
+        default = false;
+        description = "If set to true, the repo won't be initialized with help files";
+      };
+
+      extraConfig = mkOption {
+        type = types.attrs;
+        description = ''
+          Attrset of daemon configuration to set using <command>ipfs config</command>, every time the daemon starts.
+          These are applied last, so may override configuration set by other options in this module.
+          Keep in mind that this configuration is stateful; i.e., unsetting anything in here does not reset the value to the default!
+        '';
+        default = { };
+        example = {
+          Datastore.StorageMax = "100GB";
+          Discovery.MDNS.Enabled = false;
+          Bootstrap = [
+            "/ip4/128.199.219.111/tcp/4001/ipfs/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu"
+            "/ip4/162.243.248.213/tcp/4001/ipfs/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm"
+          ];
+          Swarm.AddrFilters = null;
+        };
+
+      };
+
+      extraFlags = mkOption {
+        type = types.listOf types.str;
+        description = "Extra flags passed to the IPFS daemon";
+        default = [ ];
+      };
+
+      localDiscovery = mkOption {
+        type = types.bool;
+        description = ''Whether to enable local discovery for the ipfs daemon.
+          This will allow ipfs to scan ports on your local network. Some hosting services will ban you if you do this.
+        '';
+        default = false;
+      };
+
+      serviceFdlimit = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = "The fdlimit for the IPFS systemd unit or <literal>null</literal> to have the daemon attempt to manage it";
+        example = 64 * 1024;
+      };
+
+      startWhenNeeded = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to use socket activation to start IPFS when needed.";
+      };
+
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    environment.variables.IPFS_PATH = cfg.dataDir;
+
+    # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size
+    boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000;
+
+    programs.fuse = mkIf cfg.autoMount {
+      userAllowOther = true;
+    };
+
+    users.users = mkIf (cfg.user == "ipfs") {
+      ipfs = {
+        group = cfg.group;
+        home = cfg.dataDir;
+        createHome = false;
+        uid = config.ids.uids.ipfs;
+        description = "IPFS daemon user";
+        packages = [
+          pkgs.ipfs-migrator
+        ];
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "ipfs") {
+      ipfs.gid = config.ids.gids.ipfs;
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - ${cfg.user} ${cfg.group} - -"
+    ] ++ optionals cfg.autoMount [
+      "d '${cfg.ipfsMountDir}' - ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.ipnsMountDir}' - ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.packages = [ cfg.package ];
+
+    systemd.services.ipfs = {
+      path = [ "/run/wrappers" cfg.package ];
+      environment.IPFS_PATH = cfg.dataDir;
+
+      preStart = ''
+        if [[ ! -f "$IPFS_PATH/config" ]]; then
+          ipfs init ${optionalString cfg.emptyRepo "-e"} --profile=${profile}
+        else
+          # After an unclean shutdown this file may exist which will cause the config command to attempt to talk to the daemon. This will hang forever if systemd is holding our sockets open.
+          rm -vf "$IPFS_PATH/api"
+
+          ipfs --offline config profile apply ${profile}
+        fi
+      '' + optionalString cfg.autoMount ''
+        ipfs --offline config Mounts.FuseAllowOther --json true
+        ipfs --offline config Mounts.IPFS ${cfg.ipfsMountDir}
+        ipfs --offline config Mounts.IPNS ${cfg.ipnsMountDir}
+      '' + optionalString cfg.autoMigrate ''
+        ${pkgs.ipfs-migrator}/bin/fs-repo-migrations -to '${cfg.package.repoVersion}' -y
+      '' + ''
+        ipfs --offline config show \
+          | ${pkgs.jq}/bin/jq '. * $extraConfig' --argjson extraConfig ${
+              escapeShellArg (builtins.toJSON ({
+                Addresses.API = cfg.apiAddress;
+                Addresses.Gateway = cfg.gatewayAddress;
+                Addresses.Swarm = cfg.swarmAddress;
+              } // cfg.extraConfig))
+            } \
+          | ipfs --offline config replace -
+      '';
+      serviceConfig = {
+        ExecStart = [ "" "${cfg.package}/bin/ipfs daemon ${ipfsFlags}" ];
+        User = cfg.user;
+        Group = cfg.group;
+      } // optionalAttrs (cfg.serviceFdlimit != null) { LimitNOFILE = cfg.serviceFdlimit; };
+    } // optionalAttrs (!cfg.startWhenNeeded) {
+      wantedBy = [ "default.target" ];
+    };
+
+    systemd.sockets.ipfs-gateway = {
+      wantedBy = [ "sockets.target" ];
+      socketConfig = {
+        ListenStream =
+          let
+            fromCfg = multiaddrToListenStream cfg.gatewayAddress;
+          in
+          [ "" ] ++ lib.optional (fromCfg != null) fromCfg;
+        ListenDatagram =
+          let
+            fromCfg = multiaddrToListenDatagram cfg.gatewayAddress;
+          in
+          [ "" ] ++ lib.optional (fromCfg != null) fromCfg;
+      };
+    };
+
+    systemd.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.apiAddress;
+        in
+        [ "" "%t/ipfs.sock" ] ++ lib.optional (fromCfg != null) fromCfg;
+    };
+
+  };
+}
diff --git a/nixos/modules/services/network-filesystems/kbfs.nix b/nixos/modules/services/network-filesystems/kbfs.nix
new file mode 100644
index 00000000000..a43ac656f66
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/kbfs.nix
@@ -0,0 +1,118 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  inherit (config.security) wrapperDir;
+  cfg = config.services.kbfs;
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.kbfs = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to mount the Keybase filesystem.";
+      };
+
+      enableRedirector = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the Keybase root redirector service, allowing
+          any user to access KBFS files via <literal>/keybase</literal>,
+          which will show different contents depending on the requester.
+        '';
+      };
+
+      mountPoint = mkOption {
+        type = types.str;
+        default = "%h/keybase";
+        example = "/keybase";
+        description = "Mountpoint for the Keybase filesystem.";
+      };
+
+      extraFlags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [
+          "-label kbfs"
+          "-mount-type normal"
+        ];
+        description = ''
+          Additional flags to pass to the Keybase filesystem on launch.
+        '';
+      };
+
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable (mkMerge [
+    {
+      # Upstream: https://github.com/keybase/client/blob/master/packaging/linux/systemd/kbfs.service
+      systemd.user.services.kbfs = {
+        description = "Keybase File System";
+
+        # Note that the "Requires" directive will cause a unit to be restarted whenever its dependency is restarted.
+        # Do not issue a hard dependency on keybase, because kbfs can reconnect to a restarted service.
+        # Do not issue a hard dependency on keybase-redirector, because it's ok if it fails (e.g., if it is disabled).
+        wants = [ "keybase.service" ] ++ optional cfg.enableRedirector "keybase-redirector.service";
+        path = [ "/run/wrappers" ];
+        unitConfig.ConditionUser = "!@system";
+
+        serviceConfig = {
+          Type = "notify";
+          # Keybase notifies from a forked process
+          EnvironmentFile = [
+            "-%E/keybase/keybase.autogen.env"
+            "-%E/keybase/keybase.env"
+          ];
+          ExecStartPre = [
+            "${pkgs.coreutils}/bin/mkdir -p \"${cfg.mountPoint}\""
+            "-${wrapperDir}/fusermount -uz \"${cfg.mountPoint}\""
+          ];
+          ExecStart = "${pkgs.kbfs}/bin/kbfsfuse ${toString cfg.extraFlags} \"${cfg.mountPoint}\"";
+          ExecStop = "${wrapperDir}/fusermount -uz \"${cfg.mountPoint}\"";
+          Restart = "on-failure";
+          PrivateTmp = true;
+        };
+        wantedBy = [ "default.target" ];
+      };
+
+      services.keybase.enable = true;
+
+      environment.systemPackages = [ pkgs.kbfs ];
+    }
+
+    (mkIf cfg.enableRedirector {
+      security.wrappers."keybase-redirector".source = "${pkgs.kbfs}/bin/redirector";
+
+      systemd.tmpfiles.rules = [ "d /keybase 0755 root root 0" ];
+
+      # Upstream: https://github.com/keybase/client/blob/master/packaging/linux/systemd/keybase-redirector.service
+      systemd.user.services.keybase-redirector = {
+        description = "Keybase Root Redirector for KBFS";
+        wants = [ "keybase.service" ];
+        unitConfig.ConditionUser = "!@system";
+
+        serviceConfig = {
+          EnvironmentFile = [
+            "-%E/keybase/keybase.autogen.env"
+            "-%E/keybase/keybase.env"
+          ];
+          # Note: The /keybase mount point is not currently configurable upstream.
+          ExecStart = "${wrapperDir}/keybase-redirector /keybase";
+          Restart = "on-failure";
+          PrivateTmp = true;
+        };
+
+        wantedBy = [ "default.target" ];
+      };
+    })
+  ]);
+}
diff --git a/nixos/modules/services/network-filesystems/litestream/default.nix b/nixos/modules/services/network-filesystems/litestream/default.nix
new file mode 100644
index 00000000000..51eb920d778
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/litestream/default.nix
@@ -0,0 +1,100 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.litestream;
+  settingsFormat = pkgs.formats.yaml {};
+in
+{
+  options.services.litestream = {
+    enable = mkEnableOption "litestream";
+
+    package = mkOption {
+      description = "Package to use.";
+      default = pkgs.litestream;
+      defaultText = literalExpression "pkgs.litestream";
+      type = types.package;
+    };
+
+    settings = mkOption {
+      description = ''
+        See the <link xlink:href="https://litestream.io/reference/config/">documentation</link>.
+      '';
+      type = settingsFormat.type;
+      example = {
+        dbs = [
+          {
+            path = "/var/lib/db1";
+            replicas = [
+              {
+                url = "s3://mybkt.litestream.io/db1";
+              }
+            ];
+          }
+        ];
+      };
+    };
+
+    environmentFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/run/secrets/litestream";
+      description = ''
+        Environment file as defined in <citerefentry>
+        <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+        </citerefentry>.
+
+        Secrets may be passed to the service without adding them to the
+        world-readable Nix store, by specifying placeholder variables as
+        the option value in Nix and setting these variables accordingly in the
+        environment file.
+
+        By default, Litestream will perform environment variable expansion
+        within the config file before reading it. Any references to ''$VAR or
+        ''${VAR} formatted variables will be replaced with their environment
+        variable values. If no value is set then it will be replaced with an
+        empty string.
+
+        <programlisting>
+          # Content of the environment file
+          LITESTREAM_ACCESS_KEY_ID=AKIAxxxxxxxxxxxxxxxx
+          LITESTREAM_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxxx
+        </programlisting>
+
+        Note that this file needs to be available on the host on which
+        this exporter is running.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    environment.etc = {
+      "litestream.yml" = {
+        source = settingsFormat.generate "litestream-config.yaml" cfg.settings;
+      };
+    };
+
+    systemd.services.litestream = {
+      description = "Litestream";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+      serviceConfig = {
+        EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
+        ExecStart = "${cfg.package}/bin/litestream replicate";
+        Restart = "always";
+        User = "litestream";
+        Group = "litestream";
+      };
+    };
+
+    users.users.litestream = {
+      description = "Litestream user";
+      group = "litestream";
+      isSystemUser = true;
+    };
+    users.groups.litestream = {};
+  };
+  meta.doc = ./litestream.xml;
+}
diff --git a/nixos/modules/services/network-filesystems/litestream/litestream.xml b/nixos/modules/services/network-filesystems/litestream/litestream.xml
new file mode 100644
index 00000000000..598f9be8cf6
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/litestream/litestream.xml
@@ -0,0 +1,65 @@
+<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>
+
+ <section xml:id="module-services-litestream-configuration">
+  <title>Configuration</title>
+
+  <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.database.path">
+   grafana database</link>:
+<programlisting>
+{ pkgs, ... }:
+{
+  users.users.litestream.extraGroups = [ "grafana" ];
+
+  systemd.services.grafana.serviceConfig.ExecStartPost = "+" + pkgs.writeShellScript "grant-grafana-permissions" ''
+    timeout=10
+
+    while [ ! -f /var/lib/grafana/data/grafana.db ];
+    do
+      if [ "$timeout" == 0 ]; then
+        echo "ERROR: Timeout while waiting for /var/lib/grafana/data/grafana.db."
+        exit 1
+      fi
+
+      sleep 1
+
+      ((timeout--))
+    done
+
+    find /var/lib/grafana -type d -exec chmod -v 775 {} \;
+    find /var/lib/grafana -type f -exec chmod -v 660 {} \;
+  '';
+
+  services.litestream = {
+    enable = true;
+
+    environmentFile = "/run/secrets/litestream";
+
+    settings = {
+      dbs = [
+        {
+          path = "/var/lib/grafana/data/grafana.db";
+          replicas = [{
+            url = "s3://mybkt.litestream.io/grafana";
+          }];
+        }
+      ];
+    };
+  };
+}
+</programlisting>
+  </para>
+ </section>
+
+</chapter>
diff --git a/nixos/modules/services/network-filesystems/moosefs.nix b/nixos/modules/services/network-filesystems/moosefs.nix
new file mode 100644
index 00000000000..88b2ada37e7
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/moosefs.nix
@@ -0,0 +1,249 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.moosefs;
+
+  mfsUser = if cfg.runAsUser then "moosefs" else "root";
+
+  settingsFormat = let
+    listSep = " ";
+    allowedTypes = with types; [ bool int float str ];
+    valueToString = val:
+        if isList val then concatStringsSep listSep (map (x: valueToString x) val)
+        else if isBool val then (if val then "1" else "0")
+        else toString val;
+
+    in {
+      type = with types; let
+        valueType = oneOf ([
+          (listOf valueType)
+        ] ++ allowedTypes) // {
+          description = "Flat key-value file";
+        };
+      in attrsOf valueType;
+
+      generate = name: value:
+        pkgs.writeText name ( lib.concatStringsSep "\n" (
+          lib.mapAttrsToList (key: val: "${key} = ${valueToString val}") value ));
+    };
+
+
+  initTool = pkgs.writeShellScriptBin "mfsmaster-init" ''
+    if [ ! -e ${cfg.master.settings.DATA_PATH}/metadata.mfs ]; then
+      cp ${pkgs.moosefs}/var/mfs/metadata.mfs.empty ${cfg.master.settings.DATA_PATH}
+      chmod +w ${cfg.master.settings.DATA_PATH}/metadata.mfs.empty
+      ${pkgs.moosefs}/bin/mfsmaster -a -c ${masterCfg} start
+      ${pkgs.moosefs}/bin/mfsmaster -c ${masterCfg} stop
+      rm ${cfg.master.settings.DATA_PATH}/metadata.mfs.empty
+    fi
+  '';
+
+  # master config file
+  masterCfg = settingsFormat.generate
+    "mfsmaster.cfg" cfg.master.settings;
+
+  # metalogger config file
+  metaloggerCfg = settingsFormat.generate
+    "mfsmetalogger.cfg" cfg.metalogger.settings;
+
+  # chunkserver config file
+  chunkserverCfg = settingsFormat.generate
+    "mfschunkserver.cfg" cfg.chunkserver.settings;
+
+  # generic template for all deamons
+  systemdService = name: extraConfig: configFile: {
+    wantedBy = [ "multi-user.target" ];
+    wants = [ "network-online.target" ];
+    after = [ "network.target" "network-online.target" ];
+
+    serviceConfig = {
+      Type = "forking";
+      ExecStart  = "${pkgs.moosefs}/bin/mfs${name} -c ${configFile} start";
+      ExecStop   = "${pkgs.moosefs}/bin/mfs${name} -c ${configFile} stop";
+      ExecReload = "${pkgs.moosefs}/bin/mfs${name} -c ${configFile} reload";
+      PIDFile = "${cfg."${name}".settings.DATA_PATH}/.mfs${name}.lock";
+    } // extraConfig;
+  };
+
+in {
+  ###### interface
+
+  options = {
+    services.moosefs = {
+      masterHost = mkOption {
+        type = types.str;
+        default = null;
+        description = "IP or DNS name of master host.";
+      };
+
+      runAsUser = mkOption {
+        type = types.bool;
+        default = true;
+        example = true;
+        description = "Run daemons as user moosefs instead of root.";
+      };
+
+      client.enable = mkEnableOption "Moosefs client.";
+
+      master = {
+        enable = mkOption {
+          type = types.bool;
+          description = ''
+            Enable Moosefs master daemon.
+
+            You need to run <literal>mfsmaster-init</literal> on a freshly installed master server to
+            initialize the <literal>DATA_PATH</literal> direcory.
+          '';
+          default = false;
+        };
+
+        exports = mkOption {
+          type = with types; listOf str;
+          default = null;
+          description = "Paths to export (see mfsexports.cfg).";
+          example = [
+            "* / rw,alldirs,admin,maproot=0:0"
+            "* . rw"
+          ];
+        };
+
+        openFirewall = mkOption {
+          type = types.bool;
+          description = "Whether to automatically open the necessary ports in the firewall.";
+          default = false;
+        };
+
+        settings = mkOption {
+          type = types.submodule {
+            freeformType = settingsFormat.type;
+
+            options.DATA_PATH = mkOption {
+              type = types.str;
+              default = "/var/lib/mfs";
+              description = "Data storage directory.";
+            };
+          };
+
+          description = "Contents of config file (mfsmaster.cfg).";
+        };
+      };
+
+      metalogger = {
+        enable = mkEnableOption "Moosefs metalogger daemon.";
+
+        settings = mkOption {
+          type = types.submodule {
+            freeformType = settingsFormat.type;
+
+            options.DATA_PATH = mkOption {
+              type = types.str;
+              default = "/var/lib/mfs";
+              description = "Data storage directory";
+            };
+          };
+
+          description = "Contents of metalogger config file (mfsmetalogger.cfg).";
+        };
+      };
+
+      chunkserver = {
+        enable = mkEnableOption "Moosefs chunkserver daemon.";
+
+        openFirewall = mkOption {
+          type = types.bool;
+          description = "Whether to automatically open the necessary ports in the firewall.";
+          default = false;
+        };
+
+        hdds = mkOption {
+          type = with types; listOf str;
+          default =  null;
+          description = "Mount points to be used by chunkserver for storage (see mfshdd.cfg).";
+          example = [ "/mnt/hdd1" ];
+        };
+
+        settings = mkOption {
+          type = types.submodule {
+            freeformType = settingsFormat.type;
+
+            options.DATA_PATH = mkOption {
+              type = types.str;
+              default = "/var/lib/mfs";
+              description = "Directory for lock file.";
+            };
+          };
+
+          description = "Contents of chunkserver config file (mfschunkserver.cfg).";
+        };
+      };
+    };
+  };
+
+  ###### implementation
+
+  config =  mkIf ( cfg.client.enable || cfg.master.enable || cfg.metalogger.enable || cfg.chunkserver.enable ) {
+
+    warnings = [ ( mkIf (!cfg.runAsUser) "Running moosefs services as root is not recommended.") ];
+
+    # Service settings
+    services.moosefs = {
+      master.settings = mkIf cfg.master.enable {
+        WORKING_USER = mfsUser;
+        EXPORTS_FILENAME = toString ( pkgs.writeText "mfsexports.cfg"
+          (concatStringsSep "\n" cfg.master.exports));
+      };
+
+      metalogger.settings = mkIf cfg.metalogger.enable {
+        WORKING_USER = mfsUser;
+        MASTER_HOST = cfg.masterHost;
+      };
+
+      chunkserver.settings = mkIf cfg.chunkserver.enable {
+        WORKING_USER = mfsUser;
+        MASTER_HOST = cfg.masterHost;
+        HDD_CONF_FILENAME = toString ( pkgs.writeText "mfshdd.cfg"
+          (concatStringsSep "\n" cfg.chunkserver.hdds));
+      };
+    };
+
+    # Create system user account for daemons
+    users = mkIf ( cfg.runAsUser && ( cfg.master.enable || cfg.metalogger.enable || cfg.chunkserver.enable ) ) {
+      users.moosefs = {
+        isSystemUser = true;
+        description = "moosefs daemon user";
+        group = "moosefs";
+      };
+      groups.moosefs = {};
+    };
+
+    environment.systemPackages =
+      (lib.optional cfg.client.enable pkgs.moosefs) ++
+      (lib.optional cfg.master.enable initTool);
+
+    networking.firewall.allowedTCPPorts =
+      (lib.optionals cfg.master.openFirewall [ 9419 9420 9421 ]) ++
+      (lib.optional cfg.chunkserver.openFirewall 9422);
+
+    # Ensure storage directories exist
+    systemd.tmpfiles.rules =
+         optional cfg.master.enable "d ${cfg.master.settings.DATA_PATH} 0700 ${mfsUser} ${mfsUser}"
+      ++ optional cfg.metalogger.enable "d ${cfg.metalogger.settings.DATA_PATH} 0700 ${mfsUser} ${mfsUser}"
+      ++ optional cfg.chunkserver.enable "d ${cfg.chunkserver.settings.DATA_PATH} 0700 ${mfsUser} ${mfsUser}";
+
+    # Service definitions
+    systemd.services.mfs-master = mkIf cfg.master.enable
+    ( systemdService "master" {
+      TimeoutStartSec = 1800;
+      TimeoutStopSec = 1800;
+      Restart = "no";
+    } masterCfg );
+
+    systemd.services.mfs-metalogger = mkIf cfg.metalogger.enable
+      ( systemdService "metalogger" { Restart = "on-abnormal"; } metaloggerCfg );
+
+    systemd.services.mfs-chunkserver = mkIf cfg.chunkserver.enable
+      ( systemdService "chunkserver" { Restart = "on-abnormal"; } chunkserverCfg );
+    };
+}
diff --git a/nixos/modules/services/network-filesystems/netatalk.nix b/nixos/modules/services/network-filesystems/netatalk.nix
new file mode 100644
index 00000000000..06a36eb30c2
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/netatalk.nix
@@ -0,0 +1,97 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.netatalk;
+  settingsFormat = pkgs.formats.ini { };
+  afpConfFile = settingsFormat.generate "afp.conf" cfg.settings;
+in {
+  options = {
+    services.netatalk = {
+
+      enable = mkEnableOption "the Netatalk AFP fileserver";
+
+      port = mkOption {
+        type = types.port;
+        default = 548;
+        description = "TCP port to be used for AFP.";
+      };
+
+      settings = mkOption {
+        inherit (settingsFormat) type;
+        default = { };
+        example = {
+          Global = { "uam list" = "uams_guest.so"; };
+          Homes = {
+            path = "afp-data";
+            "basedir regex" = "/home";
+          };
+          example-volume = {
+            path = "/srv/volume";
+            "read only" = true;
+          };
+        };
+        description = ''
+          Configuration for Netatalk. See
+          <citerefentry><refentrytitle>afp.conf</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry>.
+        '';
+      };
+
+      extmap = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          File name extension mappings.
+          See <citerefentry><refentrytitle>extmap.conf</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry>. for more information.
+        '';
+      };
+
+    };
+  };
+
+  imports = (map (option:
+    mkRemovedOptionModule [ "services" "netatalk" option ]
+    "This option was removed in favor of `services.netatalk.settings`.") [
+      "extraConfig"
+      "homes"
+      "volumes"
+    ]);
+
+  config = mkIf cfg.enable {
+
+    services.netatalk.settings.Global = {
+      "afp port" = toString cfg.port;
+      "extmap file" = "${pkgs.writeText "extmap.conf" cfg.extmap}";
+    };
+
+    systemd.services.netatalk = {
+      description = "Netatalk AFP fileserver for Macintosh clients";
+      unitConfig.Documentation =
+        "man:afp.conf(5) man:netatalk(8) man:afpd(8) man:cnid_metad(8) man:cnid_dbd(8)";
+      after = [ "network.target" "avahi-daemon.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      path = [ pkgs.netatalk ];
+
+      serviceConfig = {
+        Type = "forking";
+        GuessMainPID = "no";
+        PIDFile = "/run/lock/netatalk";
+        ExecStart = "${pkgs.netatalk}/sbin/netatalk -F ${afpConfFile}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP  $MAINPID";
+        ExecStop = "${pkgs.coreutils}/bin/kill -TERM $MAINPID";
+        Restart = "always";
+        RestartSec = 1;
+        StateDirectory = [ "netatalk/CNID" ];
+      };
+
+    };
+
+    security.pam.services.netatalk.unixAuth = true;
+
+  };
+
+}
diff --git a/nixos/modules/services/network-filesystems/nfsd.nix b/nixos/modules/services/network-filesystems/nfsd.nix
new file mode 100644
index 00000000000..1b62bfa8203
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/nfsd.nix
@@ -0,0 +1,175 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.nfs.server;
+
+  exports = pkgs.writeText "exports" cfg.exports;
+
+in
+
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "nfs" "lockdPort" ] [ "services" "nfs" "server" "lockdPort" ])
+    (mkRenamedOptionModule [ "services" "nfs" "statdPort" ] [ "services" "nfs" "server" "statdPort" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.nfs = {
+
+      server = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether to enable the kernel's NFS server.
+          '';
+        };
+
+        extraNfsdConfig = mkOption {
+          type = types.str;
+          default = "";
+          description = ''
+            Extra configuration options for the [nfsd] section of /etc/nfs.conf.
+          '';
+        };
+
+        exports = mkOption {
+          type = types.lines;
+          default = "";
+          description = ''
+            Contents of the /etc/exports file.  See
+            <citerefentry><refentrytitle>exports</refentrytitle>
+            <manvolnum>5</manvolnum></citerefentry> for the format.
+          '';
+        };
+
+        hostName = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            Hostname or address on which NFS requests will be accepted.
+            Default is all.  See the <option>-H</option> option in
+            <citerefentry><refentrytitle>nfsd</refentrytitle>
+            <manvolnum>8</manvolnum></citerefentry>.
+          '';
+        };
+
+        nproc = mkOption {
+          type = types.int;
+          default = 8;
+          description = ''
+            Number of NFS server threads.  Defaults to the recommended value of 8.
+          '';
+        };
+
+        createMountPoints = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Whether to create the mount points in the exports file at startup time.";
+        };
+
+        mountdPort = mkOption {
+          type = types.nullOr types.int;
+          default = null;
+          example = 4002;
+          description = ''
+            Use fixed port for rpc.mountd, useful if server is behind firewall.
+          '';
+        };
+
+        lockdPort = mkOption {
+          type = types.nullOr types.int;
+          default = null;
+          example = 4001;
+          description = ''
+            Use a fixed port for the NFS lock manager kernel module
+            (<literal>lockd/nlockmgr</literal>).  This is useful if the
+            NFS server is behind a firewall.
+          '';
+        };
+
+        statdPort = mkOption {
+          type = types.nullOr types.int;
+          default = null;
+          example = 4000;
+          description = ''
+            Use a fixed port for <command>rpc.statd</command>. This is
+            useful if the NFS server is behind a firewall.
+          '';
+        };
+
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.nfs.extraConfig = ''
+      [nfsd]
+      threads=${toString cfg.nproc}
+      ${optionalString (cfg.hostName != null) "host=${cfg.hostName}"}
+      ${cfg.extraNfsdConfig}
+
+      [mountd]
+      ${optionalString (cfg.mountdPort != null) "port=${toString cfg.mountdPort}"}
+
+      [statd]
+      ${optionalString (cfg.statdPort != null) "port=${toString cfg.statdPort}"}
+
+      [lockd]
+      ${optionalString (cfg.lockdPort != null) ''
+        port=${toString cfg.lockdPort}
+        udp-port=${toString cfg.lockdPort}
+      ''}
+    '';
+
+    services.rpcbind.enable = true;
+
+    boot.supportedFilesystems = [ "nfs" ]; # needed for statd and idmapd
+
+    environment.etc.exports.source = exports;
+
+    systemd.services.nfs-server =
+      { enable = true;
+        wantedBy = [ "multi-user.target" ];
+
+        preStart =
+          ''
+            mkdir -p /var/lib/nfs/v4recovery
+          '';
+      };
+
+    systemd.services.nfs-mountd =
+      { enable = true;
+        restartTriggers = [ exports ];
+
+        preStart =
+          ''
+            mkdir -p /var/lib/nfs
+
+            ${optionalString cfg.createMountPoints
+              ''
+                # create export directories:
+                # skip comments, take first col which may either be a quoted
+                # "foo bar" or just foo (-> man export)
+                sed '/^#.*/d;s/^"\([^"]*\)".*/\1/;t;s/[ ].*//' ${exports} \
+                | xargs -d '\n' mkdir -p
+              ''
+            }
+          '';
+      };
+
+  };
+
+}
diff --git a/nixos/modules/services/network-filesystems/openafs/client.nix b/nixos/modules/services/network-filesystems/openafs/client.nix
new file mode 100644
index 00000000000..c8cc5052c2a
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/openafs/client.nix
@@ -0,0 +1,252 @@
+{ config, lib, pkgs, ... }:
+
+# openafsMod, openafsBin, mkCellServDB
+with import ./lib.nix { inherit config lib pkgs; };
+
+let
+  inherit (lib) getBin literalExpression mkOption mkIf optionalString singleton types;
+
+  cfg = config.services.openafsClient;
+
+  cellServDB = pkgs.fetchurl {
+    url = "http://dl.central.org/dl/cellservdb/CellServDB.2018-05-14";
+    sha256 = "1wmjn6mmyy2r8p10nlbdzs4nrqxy8a9pjyrdciy5nmppg4053rk2";
+  };
+
+  clientServDB = pkgs.writeText "client-cellServDB-${cfg.cellName}" (mkCellServDB cfg.cellName cfg.cellServDB);
+
+  afsConfig = pkgs.runCommand "afsconfig" { preferLocalBuild = true; } ''
+    mkdir -p $out
+    echo ${cfg.cellName} > $out/ThisCell
+    cat ${cellServDB} ${clientServDB} > $out/CellServDB
+    echo "${cfg.mountPoint}:${cfg.cache.directory}:${toString cfg.cache.blocks}" > $out/cacheinfo
+  '';
+
+in
+{
+  ###### interface
+
+  options = {
+
+    services.openafsClient = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to enable the OpenAFS client.";
+      };
+
+      afsdb = mkOption {
+        default = true;
+        type = types.bool;
+        description = "Resolve cells via AFSDB DNS records.";
+      };
+
+      cellName = mkOption {
+        default = "";
+        type = types.str;
+        description = "Cell name.";
+        example = "grand.central.org";
+      };
+
+      cellServDB = mkOption {
+        default = [];
+        type = with types; listOf (submodule { options = cellServDBConfig; });
+        description = ''
+          This cell's database server records, added to the global
+          CellServDB. See CellServDB(5) man page for syntax. Ignored when
+          <literal>afsdb</literal> is set to <literal>true</literal>.
+        '';
+        example = [
+          { ip = "1.2.3.4"; dnsname = "first.afsdb.server.dns.fqdn.org"; }
+          { ip = "2.3.4.5"; dnsname = "second.afsdb.server.dns.fqdn.org"; }
+        ];
+      };
+
+      cache = {
+        blocks = mkOption {
+          default = 100000;
+          type = types.int;
+          description = "Cache size in 1KB blocks.";
+        };
+
+        chunksize = mkOption {
+          default = 0;
+          type = types.ints.between 0 30;
+          description = ''
+            Size of each cache chunk given in powers of
+            2. <literal>0</literal> resets the chunk size to its default
+            values (13 (8 KB) for memcache, 18-20 (256 KB to 1 MB) for
+            diskcache). Maximum value is 30. Important performance
+            parameter. Set to higher values when dealing with large files.
+          '';
+        };
+
+        directory = mkOption {
+          default = "/var/cache/openafs";
+          type = types.str;
+          description = "Cache directory.";
+        };
+
+        diskless = mkOption {
+          default = false;
+          type = types.bool;
+          description = ''
+            Use in-memory cache for diskless machines. Has no real
+            performance benefit anymore.
+          '';
+        };
+      };
+
+      crypt = mkOption {
+        default = true;
+        type = types.bool;
+        description = "Whether to enable (weak) protocol encryption.";
+      };
+
+      daemons = mkOption {
+        default = 2;
+        type = types.int;
+        description = ''
+          Number of daemons to serve user requests. Numbers higher than 6
+          usually do no increase performance. Default is sufficient for up
+          to five concurrent users.
+        '';
+      };
+
+      fakestat = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Return fake data on stat() calls. If <literal>true</literal>,
+          always do so. If <literal>false</literal>, only do so for
+          cross-cell mounts (as these are potentially expensive).
+        '';
+      };
+
+      inumcalc = mkOption {
+        default = "compat";
+        type = types.strMatching "compat|md5";
+        description = ''
+          Inode calculation method. <literal>compat</literal> is
+          computationally less expensive, but <literal>md5</literal> greatly
+          reduces the likelihood of inode collisions in larger scenarios
+          involving multiple cells mounted into one AFS space.
+        '';
+      };
+
+      mountPoint = mkOption {
+        default = "/afs";
+        type = types.str;
+        description = ''
+          Mountpoint of the AFS file tree, conventionally
+          <literal>/afs</literal>. When set to a different value, only
+          cross-cells that use the same value can be accessed.
+        '';
+      };
+
+      packages = {
+        module = mkOption {
+          default = config.boot.kernelPackages.openafs;
+          defaultText = literalExpression "config.boot.kernelPackages.openafs";
+          type = types.package;
+          description = "OpenAFS kernel module package. MUST match the userland package!";
+        };
+        programs = mkOption {
+          default = getBin pkgs.openafs;
+          defaultText = literalExpression "getBin pkgs.openafs";
+          type = types.package;
+          description = "OpenAFS programs package. MUST match the kernel module package!";
+        };
+      };
+
+      sparse = mkOption {
+        default = true;
+        type = types.bool;
+        description = "Minimal cell list in /afs.";
+      };
+
+      startDisconnected = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Start up in disconnected mode.  You need to execute
+          <literal>fs disco online</literal> (as root) to switch to
+          connected mode. Useful for roaming devices.
+        '';
+      };
+
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = cfg.afsdb || cfg.cellServDB != [];
+        message = "You should specify all cell-local database servers in config.services.openafsClient.cellServDB or set config.services.openafsClient.afsdb.";
+      }
+      { assertion = cfg.cellName != "";
+        message = "You must specify the local cell name in config.services.openafsClient.cellName.";
+      }
+    ];
+
+    environment.systemPackages = [ openafsBin ];
+
+    environment.etc = {
+      clientCellServDB = {
+        source = pkgs.runCommand "CellServDB" { preferLocalBuild = true; } ''
+          cat ${cellServDB} ${clientServDB} > $out
+        '';
+        target = "openafs/CellServDB";
+        mode = "0644";
+      };
+      clientCell = {
+        text = ''
+          ${cfg.cellName}
+        '';
+        target = "openafs/ThisCell";
+        mode = "0644";
+      };
+    };
+
+    systemd.services.afsd = {
+      description = "AFS client";
+      wantedBy = [ "multi-user.target" ];
+      after = singleton (if cfg.startDisconnected then  "network.target" else "network-online.target");
+      serviceConfig = { RemainAfterExit = true; };
+      restartIfChanged = false;
+
+      preStart = ''
+        mkdir -p -m 0755 ${cfg.mountPoint}
+        mkdir -m 0700 -p ${cfg.cache.directory}
+        ${pkgs.kmod}/bin/insmod ${openafsMod}/lib/modules/*/extra/openafs/libafs.ko.xz
+        ${openafsBin}/sbin/afsd \
+          -mountdir ${cfg.mountPoint} \
+          -confdir ${afsConfig} \
+          ${optionalString (!cfg.cache.diskless) "-cachedir ${cfg.cache.directory}"} \
+          -blocks ${toString cfg.cache.blocks} \
+          -chunksize ${toString cfg.cache.chunksize} \
+          ${optionalString cfg.cache.diskless "-memcache"} \
+          -inumcalc ${cfg.inumcalc} \
+          ${if cfg.fakestat then "-fakestat-all" else "-fakestat"} \
+          ${if cfg.sparse then "-dynroot-sparse" else "-dynroot"} \
+          ${optionalString cfg.afsdb "-afsdb"}
+        ${openafsBin}/bin/fs setcrypt ${if cfg.crypt then "on" else "off"}
+        ${optionalString cfg.startDisconnected "${openafsBin}/bin/fs discon offline"}
+      '';
+
+      # Doing this in preStop, because after these commands AFS is basically
+      # stopped, so systemd has nothing to do, just noticing it.  If done in
+      # postStop, then we get a hang + kernel oops, because AFS can't be
+      # stopped simply by sending signals to processes.
+      preStop = ''
+        ${pkgs.util-linux}/bin/umount ${cfg.mountPoint}
+        ${openafsBin}/sbin/afsd -shutdown
+        ${pkgs.kmod}/sbin/rmmod libafs
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/network-filesystems/openafs/lib.nix b/nixos/modules/services/network-filesystems/openafs/lib.nix
new file mode 100644
index 00000000000..e068ee761c2
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/openafs/lib.nix
@@ -0,0 +1,33 @@
+{ config, lib, ...}:
+
+let
+  inherit (lib) concatStringsSep mkOption types;
+
+in {
+
+  mkCellServDB = cellName: db: ''
+    >${cellName}
+  '' + (concatStringsSep "\n" (map (dbm: if (dbm.ip != "" && dbm.dnsname != "") then dbm.ip + " #" + dbm.dnsname else "")
+                                   db))
+     + "\n";
+
+  # CellServDB configuration type
+  cellServDBConfig = {
+    ip = mkOption {
+      type = types.str;
+      default = "";
+      example = "1.2.3.4";
+      description = "IP Address of a database server";
+    };
+    dnsname = mkOption {
+      type = types.str;
+      default = "";
+      example = "afs.example.org";
+      description = "DNS full-qualified domain name of a database server";
+    };
+  };
+
+  openafsMod = config.services.openafsClient.packages.module;
+  openafsBin = config.services.openafsClient.packages.programs;
+  openafsSrv = config.services.openafsServer.package;
+}
diff --git a/nixos/modules/services/network-filesystems/openafs/server.nix b/nixos/modules/services/network-filesystems/openafs/server.nix
new file mode 100644
index 00000000000..9c974335def
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/openafs/server.nix
@@ -0,0 +1,269 @@
+{ config, lib, pkgs, ... }:
+
+# openafsBin, openafsSrv, mkCellServDB
+with import ./lib.nix { inherit config lib pkgs; };
+
+let
+  inherit (lib) concatStringsSep literalExpression mkIf mkOption optionalString types;
+
+  bosConfig = pkgs.writeText "BosConfig" (''
+    restrictmode 1
+    restarttime 16 0 0 0 0
+    checkbintime 3 0 5 0 0
+  '' + (optionalString cfg.roles.database.enable ''
+    bnode simple vlserver 1
+    parm ${openafsSrv}/libexec/openafs/vlserver ${optionalString cfg.dottedPrincipals "-allow-dotted-principals"} ${cfg.roles.database.vlserverArgs}
+    end
+    bnode simple ptserver 1
+    parm ${openafsSrv}/libexec/openafs/ptserver ${optionalString cfg.dottedPrincipals "-allow-dotted-principals"} ${cfg.roles.database.ptserverArgs}
+    end
+  '') + (optionalString cfg.roles.fileserver.enable ''
+    bnode dafs dafs 1
+    parm ${openafsSrv}/libexec/openafs/dafileserver ${optionalString cfg.dottedPrincipals "-allow-dotted-principals"} -udpsize ${udpSizeStr} ${cfg.roles.fileserver.fileserverArgs}
+    parm ${openafsSrv}/libexec/openafs/davolserver ${optionalString cfg.dottedPrincipals "-allow-dotted-principals"} -udpsize ${udpSizeStr} ${cfg.roles.fileserver.volserverArgs}
+    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) ''
+    bnode simple buserver 1
+    parm ${openafsSrv}/libexec/openafs/buserver ${cfg.roles.backup.buserverArgs} ${optionalString (cfg.roles.backup.cellServDB != []) "-cellservdb /etc/openafs/backup/"}
+    end
+  ''));
+
+  netInfo = if (cfg.advertisedAddresses != []) then
+    pkgs.writeText "NetInfo" ((concatStringsSep "\nf " cfg.advertisedAddresses) + "\n")
+  else null;
+
+  buCellServDB = pkgs.writeText "backup-cellServDB-${cfg.cellName}" (mkCellServDB cfg.cellName cfg.roles.backup.cellServDB);
+
+  cfg = config.services.openafsServer;
+
+  udpSizeStr = toString cfg.udpPacketSize;
+
+in {
+
+  options = {
+
+    services.openafsServer = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to enable the OpenAFS server. An OpenAFS server needs a
+          complex setup. So, be aware that enabling this service and setting
+          some options does not give you a turn-key-ready solution. You need
+          at least a running Kerberos 5 setup, as OpenAFS relies on it for
+          authentication. See the Guide "QuickStartUnix" coming with
+          <literal>pkgs.openafs.doc</literal> for complete setup
+          instructions.
+        '';
+      };
+
+      advertisedAddresses = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "List of IP addresses this server is advertised under. See NetInfo(5)";
+      };
+
+      cellName = mkOption {
+        default = "";
+        type = types.str;
+        description = "Cell name, this server will serve.";
+        example = "grand.central.org";
+      };
+
+      cellServDB = mkOption {
+        default = [];
+        type = with types; listOf (submodule [ { options = cellServDBConfig;} ]);
+        description = "Definition of all cell-local database server machines.";
+      };
+
+      package = mkOption {
+        default = pkgs.openafs.server or pkgs.openafs;
+        defaultText = literalExpression "pkgs.openafs.server or pkgs.openafs";
+        type = types.package;
+        description = "OpenAFS package for the server binaries";
+      };
+
+      roles = {
+        fileserver = {
+          enable = mkOption {
+            default = true;
+            type = types.bool;
+            description = "Fileserver role, serves files and volumes from its local storage.";
+          };
+
+          fileserverArgs = mkOption {
+            default = "-vattachpar 128 -vhashsize 11 -L -rxpck 400 -cb 1000000";
+            type = types.str;
+            description = "Arguments to the dafileserver process. See its man page.";
+          };
+
+          volserverArgs = mkOption {
+            default = "";
+            type = types.str;
+            description = "Arguments to the davolserver process. See its man page.";
+            example = "-sync never";
+          };
+
+          salvageserverArgs = mkOption {
+            default = "";
+            type = types.str;
+            description = "Arguments to the salvageserver process. See its man page.";
+            example = "-showlog";
+          };
+
+          salvagerArgs = mkOption {
+            default = "";
+            type = types.str;
+            description = "Arguments to the dasalvager process. See its man page.";
+            example = "-showlog -showmounts";
+          };
+        };
+
+        database = {
+          enable = mkOption {
+            default = true;
+            type = types.bool;
+            description = ''
+              Database server role, maintains the Volume Location Database,
+              Protection Database (and Backup Database, see
+              <literal>backup</literal> role). There can be multiple
+              servers in the database role for replication, which then need
+              reliable network connection to each other.
+
+              Servers in this role appear in AFSDB DNS records or the
+              CellServDB.
+            '';
+          };
+
+          vlserverArgs = mkOption {
+            default = "";
+            type = types.str;
+            description = "Arguments to the vlserver process. See its man page.";
+            example = "-rxbind";
+          };
+
+          ptserverArgs = mkOption {
+            default = "";
+            type = types.str;
+            description = "Arguments to the ptserver process. See its man page.";
+            example = "-restricted -default_access S---- S-M---";
+          };
+        };
+
+        backup = {
+          enable = mkOption {
+            default = false;
+            type = types.bool;
+            description = ''
+              Backup server role. Use in conjunction with the
+              <literal>database</literal> role to maintain the Backup
+              Database. Normally only used in conjunction with tape storage
+              or IBM's Tivoli Storage Manager.
+            '';
+          };
+
+          buserverArgs = mkOption {
+            default = "";
+            type = types.str;
+            description = "Arguments to the buserver process. See its man page.";
+            example = "-p 8";
+          };
+
+          cellServDB = mkOption {
+            default = [];
+            type = with types; listOf (submodule [ { options = cellServDBConfig;} ]);
+            description = ''
+              Definition of all cell-local backup database server machines.
+              Use this when your cell uses less backup database servers than
+              other database server machines.
+            '';
+          };
+        };
+      };
+
+      dottedPrincipals= mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If enabled, allow principal names containing (.) dots. Enabling
+          this has security implications!
+        '';
+      };
+
+      udpPacketSize = mkOption {
+        default = 1310720;
+        type = types.int;
+        description = ''
+          UDP packet size to use in Bytes. Higher values can speed up
+          communications. The default of 1 MB is a sufficient in most
+          cases. Make sure to increase the kernel's UDP buffer size
+          accordingly via <literal>net.core(w|r|opt)mem_max</literal>
+          sysctl.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = cfg.cellServDB != [];
+        message = "You must specify all cell-local database servers in config.services.openafsServer.cellServDB.";
+      }
+      { assertion = cfg.cellName != "";
+        message = "You must specify the local cell name in config.services.openafsServer.cellName.";
+      }
+    ];
+
+    environment.systemPackages = [ openafsBin ];
+
+    environment.etc = {
+      bosConfig = {
+        source = bosConfig;
+        target = "openafs/BosConfig";
+        mode = "0644";
+      };
+      cellServDB = {
+        text = mkCellServDB cfg.cellName cfg.cellServDB;
+        target = "openafs/server/CellServDB";
+        mode = "0644";
+      };
+      thisCell = {
+        text = cfg.cellName;
+        target = "openafs/server/ThisCell";
+        mode = "0644";
+      };
+      buCellServDB = {
+        enable = (cfg.roles.backup.cellServDB != []);
+        text = mkCellServDB cfg.cellName cfg.roles.backup.cellServDB;
+        target = "openafs/backup/CellServDB";
+      };
+    };
+
+    systemd.services = {
+      openafs-server = {
+        description = "OpenAFS server";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        restartIfChanged = false;
+        unitConfig.ConditionPathExists = [
+          "|/etc/openafs/server/KeyFileExt"
+        ];
+        preStart = ''
+          mkdir -m 0755 -p /var/openafs
+          ${optionalString (netInfo != null) "cp ${netInfo} /var/openafs/netInfo"}
+          ${optionalString (cfg.roles.backup.cellServDB != []) "cp ${buCellServDB}"}
+        '';
+        serviceConfig = {
+          ExecStart = "${openafsBin}/bin/bosserver -nofork";
+          ExecStop = "${openafsBin}/bin/bos shutdown localhost -wait -localauth";
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/network-filesystems/orangefs/client.nix b/nixos/modules/services/network-filesystems/orangefs/client.nix
new file mode 100644
index 00000000000..36ea5af2168
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/orangefs/client.nix
@@ -0,0 +1,96 @@
+{ config, lib, pkgs, ...} :
+
+with lib;
+
+let
+  cfg = config.services.orangefs.client;
+
+in {
+  ###### interface
+
+  options = {
+    services.orangefs.client = {
+      enable = mkEnableOption "OrangeFS client daemon";
+
+      extraOptions = mkOption {
+        type = with types; listOf str;
+        default = [];
+        description = "Extra command line options for pvfs2-client.";
+      };
+
+      fileSystems = mkOption {
+        description = ''
+          The orangefs file systems to be mounted.
+          This option is prefered over using <option>fileSystems</option> directly since
+          the pvfs client service needs to be running for it to be mounted.
+        '';
+
+        example = [{
+          mountPoint = "/orangefs";
+          target = "tcp://server:3334/orangefs";
+        }];
+
+        type = with types; listOf (submodule ({ ... } : {
+          options = {
+
+            mountPoint = mkOption {
+              type = types.str;
+              default = "/orangefs";
+              description = "Mount point.";
+            };
+
+            options = mkOption {
+              type = with types; listOf str;
+              default = [];
+              description = "Mount options";
+            };
+
+            target = mkOption {
+              type = types.str;
+              example = "tcp://server:3334/orangefs";
+              description = "Target URL";
+            };
+          };
+        }));
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.orangefs ];
+
+    boot.supportedFilesystems = [ "pvfs2" ];
+    boot.kernelModules = [ "orangefs" ];
+
+    systemd.services.orangefs-client = {
+      requires = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+
+         ExecStart = ''
+           ${pkgs.orangefs}/bin/pvfs2-client-core \
+              --logtype=syslog ${concatStringsSep " " cfg.extraOptions}
+        '';
+
+        TimeoutStopSec = "120";
+      };
+    };
+
+    systemd.mounts = map (fs: {
+      requires = [ "orangefs-client.service" ];
+      after = [ "orangefs-client.service" ];
+      bindsTo = [ "orangefs-client.service" ];
+      wantedBy = [ "remote-fs.target" ];
+      type = "pvfs2";
+      options = concatStringsSep "," fs.options;
+      what = fs.target;
+      where = fs.mountPoint;
+    }) cfg.fileSystems;
+  };
+}
+
diff --git a/nixos/modules/services/network-filesystems/orangefs/server.nix b/nixos/modules/services/network-filesystems/orangefs/server.nix
new file mode 100644
index 00000000000..621c2fe8f78
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/orangefs/server.nix
@@ -0,0 +1,225 @@
+{ config, lib, pkgs, ...} :
+
+with lib;
+
+let
+  cfg = config.services.orangefs.server;
+
+  aliases = mapAttrsToList (alias: url: alias) cfg.servers;
+
+  # Maximum handle number is 2^63
+  maxHandle = 9223372036854775806;
+
+  # One range of handles for each meta/data instance
+  handleStep = maxHandle / (length aliases) / 2;
+
+  fileSystems = mapAttrsToList (name: fs: ''
+    <FileSystem>
+      Name ${name}
+      ID ${toString fs.id}
+      RootHandle ${toString fs.rootHandle}
+
+      ${fs.extraConfig}
+
+      <MetaHandleRanges>
+      ${concatStringsSep "\n" (
+          imap0 (i: alias:
+            let
+              begin = i * handleStep + 3;
+              end = begin + handleStep - 1;
+            in "Range ${alias} ${toString begin}-${toString end}") aliases
+       )}
+      </MetaHandleRanges>
+
+      <DataHandleRanges>
+      ${concatStringsSep "\n" (
+          imap0 (i: alias:
+            let
+              begin = i * handleStep + 3 + (length aliases) * handleStep;
+              end = begin + handleStep - 1;
+            in "Range ${alias} ${toString begin}-${toString end}") aliases
+       )}
+      </DataHandleRanges>
+
+      <StorageHints>
+      TroveSyncMeta ${if fs.troveSyncMeta then "yes" else "no"}
+      TroveSyncData ${if fs.troveSyncData then "yes" else "no"}
+      ${fs.extraStorageHints}
+      </StorageHints>
+
+    </FileSystem>
+  '') cfg.fileSystems;
+
+  configFile = ''
+    <Defaults>
+    LogType ${cfg.logType}
+    DataStorageSpace ${cfg.dataStorageSpace}
+    MetaDataStorageSpace ${cfg.metadataStorageSpace}
+
+    BMIModules ${concatStringsSep "," cfg.BMIModules}
+    ${cfg.extraDefaults}
+    </Defaults>
+
+    ${cfg.extraConfig}
+
+    <Aliases>
+    ${concatStringsSep "\n" (mapAttrsToList (alias: url: "Alias ${alias} ${url}") cfg.servers)}
+    </Aliases>
+
+    ${concatStringsSep "\n" fileSystems}
+  '';
+
+in {
+  ###### interface
+
+  options = {
+    services.orangefs.server = {
+      enable = mkEnableOption "OrangeFS server";
+
+      logType = mkOption {
+        type = with types; enum [ "file" "syslog" ];
+        default = "syslog";
+        description = "Destination for log messages.";
+      };
+
+      dataStorageSpace = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/data/storage";
+        description = "Directory for data storage.";
+      };
+
+      metadataStorageSpace = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/data/meta";
+        description = "Directory for meta data storage.";
+      };
+
+      BMIModules = mkOption {
+        type = with types; listOf str;
+        default = [ "bmi_tcp" ];
+        example = [ "bmi_tcp" "bmi_ib"];
+        description = "List of BMI modules to load.";
+      };
+
+      extraDefaults = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Extra config for <literal>&lt;Defaults&gt;</literal> section.";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Extra config for the global section.";
+      };
+
+      servers = mkOption {
+        type = with types; attrsOf types.str;
+        default = {};
+        example = {
+          node1 = "tcp://node1:3334";
+          node2 = "tcp://node2:3334";
+        };
+        description = "URLs for storage server including port. The attribute names define the server alias.";
+      };
+
+      fileSystems = mkOption {
+        description = ''
+          These options will create the <literal>&lt;FileSystem&gt;</literal> sections of config file.
+        '';
+        default = { orangefs = {}; };
+        example = literalExpression ''
+          {
+            fs1 = {
+              id = 101;
+            };
+
+            fs2 = {
+              id = 102;
+            };
+          }
+        '';
+        type = with types; attrsOf (submodule ({ ... } : {
+          options = {
+            id = mkOption {
+              type = types.int;
+              default = 1;
+              description = "File system ID (must be unique within configuration).";
+            };
+
+            rootHandle = mkOption {
+              type = types.int;
+              default = 3;
+              description = "File system root ID.";
+            };
+
+            extraConfig = mkOption {
+              type = types.lines;
+              default = "";
+              description = "Extra config for <literal>&lt;FileSystem&gt;</literal> section.";
+            };
+
+            troveSyncMeta = mkOption {
+              type = types.bool;
+              default = true;
+              description = "Sync meta data.";
+            };
+
+            troveSyncData = mkOption {
+              type = types.bool;
+              default = false;
+              description = "Sync data.";
+            };
+
+            extraStorageHints = mkOption {
+              type = types.lines;
+              default = "";
+              description = "Extra config for <literal>&lt;StorageHints&gt;</literal> section.";
+            };
+          };
+        }));
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.orangefs ];
+
+    # orangefs daemon will run as user
+    users.users.orangefs = {
+      isSystemUser = true;
+      group = "orangfs";
+    };
+    users.groups.orangefs = {};
+
+    # To format the file system the config file is needed.
+    environment.etc."orangefs/server.conf" = {
+      text = configFile;
+      user = "orangefs";
+      group = "orangefs";
+    };
+
+    systemd.services.orangefs-server = {
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+
+      serviceConfig = {
+        # Run as "simple" in forground mode.
+        # This is more reliable
+        ExecStart = ''
+          ${pkgs.orangefs}/bin/pvfs2-server -d \
+            /etc/orangefs/server.conf
+        '';
+        TimeoutStopSec = "120";
+        User = "orangefs";
+        Group = "orangefs";
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/services/network-filesystems/rsyncd.nix b/nixos/modules/services/network-filesystems/rsyncd.nix
new file mode 100644
index 00000000000..e72f9b54cd6
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/rsyncd.nix
@@ -0,0 +1,128 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.rsyncd;
+  settingsFormat = pkgs.formats.ini { };
+  configFile = settingsFormat.generate "rsyncd.conf" cfg.settings;
+in {
+  options = {
+    services.rsyncd = {
+
+      enable = mkEnableOption "the rsync daemon";
+
+      port = mkOption {
+        default = 873;
+        type = types.port;
+        description = "TCP port the daemon will listen on.";
+      };
+
+      settings = mkOption {
+        inherit (settingsFormat) type;
+        default = { };
+        example = {
+          global = {
+            uid = "nobody";
+            gid = "nobody";
+            "use chroot" = true;
+            "max connections" = 4;
+          };
+          ftp = {
+            path = "/var/ftp/./pub";
+            comment = "whole ftp area";
+          };
+          cvs = {
+            path = "/data/cvs";
+            comment = "CVS repository (requires authentication)";
+            "auth users" = [ "tridge" "susan" ];
+            "secrets file" = "/etc/rsyncd.secrets";
+          };
+        };
+        description = ''
+          Configuration for rsyncd. See
+          <citerefentry><refentrytitle>rsyncd.conf</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry>.
+        '';
+      };
+
+      socketActivated = mkOption {
+        default = false;
+        type = types.bool;
+        description =
+          "If enabled Rsync will be socket-activated rather than run persistently.";
+      };
+
+    };
+  };
+
+  imports = (map (option:
+    mkRemovedOptionModule [ "services" "rsyncd" option ]
+    "This option was removed in favor of `services.rsyncd.settings`.") [
+      "address"
+      "extraConfig"
+      "motd"
+      "user"
+      "group"
+    ]);
+
+  config = mkIf cfg.enable {
+
+    services.rsyncd.settings.global.port = toString cfg.port;
+
+    systemd = let
+      serviceConfigSecurity = {
+        ProtectSystem = "full";
+        PrivateDevices = "on";
+        NoNewPrivileges = "on";
+      };
+    in {
+      services.rsync = {
+        enable = !cfg.socketActivated;
+        aliases = [ "rsyncd.service" ];
+
+        description = "fast remote file copy program daemon";
+        after = [ "network.target" ];
+        documentation = [ "man:rsync(1)" "man:rsyncd.conf(5)" ];
+
+        serviceConfig = serviceConfigSecurity // {
+          ExecStart =
+            "${pkgs.rsync}/bin/rsync --daemon --no-detach --config=${configFile}";
+          RestartSec = 1;
+        };
+
+        wantedBy = [ "multi-user.target" ];
+      };
+
+      services."rsync@" = {
+        description = "fast remote file copy program daemon";
+        after = [ "network.target" ];
+
+        serviceConfig = serviceConfigSecurity // {
+          ExecStart = "${pkgs.rsync}/bin/rsync --daemon --config=${configFile}";
+          StandardInput = "socket";
+          StandardOutput = "inherit";
+          StandardError = "journal";
+        };
+      };
+
+      sockets.rsync = {
+        enable = cfg.socketActivated;
+
+        description = "socket for fast remote file copy program daemon";
+        conflicts = [ "rsync.service" ];
+
+        listenStreams = [ (toString cfg.port) ];
+        socketConfig.Accept = true;
+
+        wantedBy = [ "sockets.target" ];
+      };
+    };
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ ehmry ];
+
+  # TODO: socket activated rsyncd
+
+}
diff --git a/nixos/modules/services/network-filesystems/samba-wsdd.nix b/nixos/modules/services/network-filesystems/samba-wsdd.nix
new file mode 100644
index 00000000000..800ef448d37
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/samba-wsdd.nix
@@ -0,0 +1,124 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.samba-wsdd;
+
+in {
+  options = {
+    services.samba-wsdd = {
+      enable = mkEnableOption ''
+        Enable Web Services Dynamic Discovery host daemon. This enables (Samba) hosts, like your local NAS device,
+        to be found by Web Service Discovery Clients like Windows.
+        <note>
+          <para>If you use the firewall consider adding the following:</para>
+          <programlisting>
+            networking.firewall.allowedTCPPorts = [ 5357 ];
+            networking.firewall.allowedUDPPorts = [ 3702 ];
+          </programlisting>
+        </note>
+      '';
+      interface = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "eth0";
+        description = "Interface or address to use.";
+      };
+      hoplimit = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        example = 2;
+        description = "Hop limit for multicast packets (default = 1).";
+      };
+      workgroup = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "HOME";
+        description = "Set workgroup name (default WORKGROUP).";
+      };
+      hostname = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "FILESERVER";
+        description = "Override (NetBIOS) hostname to be used (default hostname).";
+      };
+      domain = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Set domain name (disables workgroup).";
+      };
+      discovery = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable discovery operation mode.";
+      };
+      listen = mkOption {
+        type = types.str;
+        default = "/run/wsdd/wsdd.sock";
+        description = "Listen on path or localhost port in discovery mode.";
+      };
+      extraOptions = mkOption {
+        type = types.listOf types.str;
+        default = [ "--shortlog" ];
+        example = [ "--verbose" "--no-http" "--ipv4only" "--no-host" ];
+        description = "Additional wsdd options.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.wsdd ];
+
+    systemd.services.samba-wsdd = {
+      description = "Web Services Dynamic Discovery host daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        Type = "simple";
+        ExecStart = ''
+          ${pkgs.wsdd}/bin/wsdd ${optionalString (cfg.interface != null) "--interface '${cfg.interface}'"} \
+                                ${optionalString (cfg.hoplimit != null) "--hoplimit '${toString cfg.hoplimit}'"} \
+                                ${optionalString (cfg.workgroup != null) "--workgroup '${cfg.workgroup}'"} \
+                                ${optionalString (cfg.hostname != null) "--hostname '${cfg.hostname}'"} \
+                                ${optionalString (cfg.domain != null) "--domain '${cfg.domain}'"} \
+                                ${optionalString cfg.discovery "--discovery --listen '${cfg.listen}'"} \
+                                ${escapeShellArgs cfg.extraOptions}
+        '';
+        # Runtime directory and mode
+        RuntimeDirectory = "wsdd";
+        RuntimeDirectoryMode = "0750";
+        # Access write directories
+        UMask = "0027";
+        # Capabilities
+        CapabilityBoundingSet = "";
+        # Security
+        NoNewPrivileges = true;
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = false;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        # System Call Filtering
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "~@cpu-emulation @debug @mount @obsolete @privileged @resources";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/network-filesystems/samba.nix b/nixos/modules/services/network-filesystems/samba.nix
new file mode 100644
index 00000000000..9ed755d0465
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/samba.nix
@@ -0,0 +1,252 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  smbToString = x: if builtins.typeOf x == "bool"
+                   then boolToString x
+                   else toString x;
+
+  cfg = config.services.samba;
+
+  samba = cfg.package;
+
+  shareConfig = name:
+    let share = getAttr name cfg.shares; in
+    "[${name}]\n " + (smbToString (
+       map
+         (key: "${key} = ${smbToString (getAttr key share)}\n")
+         (attrNames share)
+    ));
+
+  configFile = pkgs.writeText "smb.conf"
+    (if cfg.configText != null then cfg.configText else
+    ''
+      [global]
+      security = ${cfg.securityType}
+      passwd program = /run/wrappers/bin/passwd %u
+      invalid users = ${smbToString cfg.invalidUsers}
+
+      ${cfg.extraConfig}
+
+      ${smbToString (map shareConfig (attrNames cfg.shares))}
+    '');
+
+  # This may include nss_ldap, needed for samba if it has to use ldap.
+  nssModulesPath = config.system.nssModules.path;
+
+  daemonService = appName: args:
+    { description = "Samba Service Daemon ${appName}";
+
+      after = [ (mkIf (cfg.enableNmbd && "${appName}" == "smbd") "samba-nmbd.service") ];
+      requiredBy = [ "samba.target" ];
+      partOf = [ "samba.target" ];
+
+      environment = {
+        LD_LIBRARY_PATH = nssModulesPath;
+        LOCALE_ARCHIVE = "/run/current-system/sw/lib/locale/locale-archive";
+      };
+
+      serviceConfig = {
+        ExecStart = "${samba}/sbin/${appName} --foreground --no-process-group ${args}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        LimitNOFILE = 16384;
+        PIDFile = "/run/${appName}.pid";
+        Type = "notify";
+        NotifyAccess = "all"; #may not do anything...
+      };
+      unitConfig.RequiresMountsFor = "/var/lib/samba";
+
+      restartTriggers = [ configFile ];
+    };
+
+in
+
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "samba" "defaultShare" ] "")
+    (mkRemovedOptionModule [ "services" "samba" "syncPasswordsByPam" ] "This option has been removed by upstream, see https://bugzilla.samba.org/show_bug.cgi?id=10669#c10")
+  ];
+
+  ###### interface
+
+  options = {
+
+    # !!! clean up the descriptions.
+
+    services.samba = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Samba, which provides file and print
+          services to Windows clients through the SMB/CIFS protocol.
+
+          <note>
+            <para>If you use the firewall consider adding the following:</para>
+          <programlisting>
+            services.samba.openFirewall = true;
+          </programlisting>
+          </note>
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to automatically open the necessary ports in the firewall.
+        '';
+      };
+
+      enableNmbd = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable Samba's nmbd, which replies to NetBIOS over IP name
+          service requests. It also participates in the browsing protocols
+          which make up the Windows "Network Neighborhood" view.
+        '';
+      };
+
+      enableWinbindd = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable Samba's winbindd, which provides a number of services
+          to the Name Service Switch capability found in most modern C libraries,
+          to arbitrary applications via PAM and ntlm_auth and to Samba itself.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.samba;
+        defaultText = literalExpression "pkgs.samba";
+        example = literalExpression "pkgs.samba4Full";
+        description = ''
+          Defines which package should be used for the samba server.
+        '';
+      };
+
+      invalidUsers = mkOption {
+        type = types.listOf types.str;
+        default = [ "root" ];
+        description = ''
+          List of users who are denied to login via Samba.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Additional global section and extra section lines go in here.
+        '';
+        example = ''
+          guest account = nobody
+          map to guest = bad user
+        '';
+      };
+
+      configText = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = ''
+          Verbatim contents of smb.conf. If null (default), use the
+          autogenerated file from NixOS instead.
+        '';
+      };
+
+      securityType = mkOption {
+        type = types.str;
+        default = "user";
+        description = "Samba security type";
+      };
+
+      nsswins = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to enable the WINS NSS (Name Service Switch) plug-in.
+          Enabling it allows applications to resolve WINS/NetBIOS names (a.k.a.
+          Windows machine names) by transparently querying the winbindd daemon.
+        '';
+      };
+
+      shares = mkOption {
+        default = {};
+        description = ''
+          A set describing shared resources.
+          See <command>man smb.conf</command> for options.
+        '';
+        type = types.attrsOf (types.attrsOf types.unspecified);
+        example = literalExpression ''
+          { public =
+            { path = "/srv/public";
+              "read only" = true;
+              browseable = "yes";
+              "guest ok" = "yes";
+              comment = "Public samba share.";
+            };
+          }
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkMerge
+    [ { assertions =
+          [ { assertion = cfg.nsswins -> cfg.enableWinbindd;
+              message   = "If samba.nsswins is enabled, then samba.enableWinbindd must also be enabled";
+            }
+          ];
+        # Always provide a smb.conf to shut up programs like smbclient and smbspool.
+        environment.etc."samba/smb.conf".source = mkOptionDefault (
+          if cfg.enable then configFile
+          else pkgs.writeText "smb-dummy.conf" "# Samba is disabled."
+        );
+      }
+
+      (mkIf cfg.enable {
+
+        system.nssModules = optional cfg.nsswins samba;
+        system.nssDatabases.hosts = optional cfg.nsswins "wins";
+
+        systemd = {
+          targets.samba = {
+            description = "Samba Server";
+            after = [ "network.target" ];
+            wantedBy = [ "multi-user.target" ];
+          };
+          # Refer to https://github.com/samba-team/samba/tree/master/packaging/systemd
+          # for correct use with systemd
+          services = {
+            samba-smbd = daemonService "smbd" "";
+            samba-nmbd = mkIf cfg.enableNmbd (daemonService "nmbd" "");
+            samba-winbindd = mkIf cfg.enableWinbindd (daemonService "winbindd" "");
+          };
+          tmpfiles.rules = [
+            "d /var/lock/samba - - - - -"
+            "d /var/log/samba - - - - -"
+            "d /var/cache/samba - - - - -"
+            "d /var/lib/samba/private - - - - -"
+          ];
+        };
+
+        security.pam.services.samba = {};
+        environment.systemPackages = [ cfg.package ];
+
+        networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ 139 445 ];
+        networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ 137 138 ];
+      })
+    ];
+
+}
diff --git a/nixos/modules/services/network-filesystems/tahoe.nix b/nixos/modules/services/network-filesystems/tahoe.nix
new file mode 100644
index 00000000000..5426463dffa
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/tahoe.nix
@@ -0,0 +1,366 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.tahoe;
+in
+  {
+    options.services.tahoe = {
+      introducers = mkOption {
+        default = {};
+        type = with types; attrsOf (submodule {
+          options = {
+            nickname = mkOption {
+              type = types.str;
+              description = ''
+                The nickname of this Tahoe introducer.
+              '';
+            };
+            tub.port = mkOption {
+              default = 3458;
+              type = types.int;
+              description = ''
+                The port on which the introducer will listen.
+              '';
+            };
+            tub.location = mkOption {
+              default = null;
+              type = types.nullOr types.str;
+              description = ''
+                The external location that the introducer should listen on.
+
+                If specified, the port should be included.
+              '';
+            };
+            package = mkOption {
+              default = pkgs.tahoelafs;
+              defaultText = literalExpression "pkgs.tahoelafs";
+              type = types.package;
+              description = ''
+                The package to use for the Tahoe LAFS daemon.
+              '';
+            };
+          };
+        });
+        description = ''
+          The Tahoe introducers.
+        '';
+      };
+      nodes = mkOption {
+        default = {};
+        type = with types; attrsOf (submodule {
+          options = {
+            nickname = mkOption {
+              type = types.str;
+              description = ''
+                The nickname of this Tahoe node.
+              '';
+            };
+            tub.port = mkOption {
+              default = 3457;
+              type = types.int;
+              description = ''
+                The port on which the tub will listen.
+
+                This is the correct setting to tweak if you want Tahoe's storage
+                system to listen on a different port.
+              '';
+            };
+            tub.location = mkOption {
+              default = null;
+              type = types.nullOr types.str;
+              description = ''
+                The external location that the node should listen on.
+
+                This is the setting to tweak if there are multiple interfaces
+                and you want to alter which interface Tahoe is advertising.
+
+                If specified, the port should be included.
+              '';
+            };
+            web.port = mkOption {
+              default = 3456;
+              type = types.int;
+              description = ''
+                The port on which the Web server will listen.
+
+                This is the correct setting to tweak if you want Tahoe's WUI to
+                listen on a different port.
+              '';
+            };
+            client.introducer = mkOption {
+              default = null;
+              type = types.nullOr types.str;
+              description = ''
+                The furl for a Tahoe introducer node.
+
+                Like all furls, keep this safe and don't share it.
+              '';
+            };
+            client.helper = mkOption {
+              default = null;
+              type = types.nullOr types.str;
+              description = ''
+                The furl for a Tahoe helper node.
+
+                Like all furls, keep this safe and don't share it.
+              '';
+            };
+            client.shares.needed = mkOption {
+              default = 3;
+              type = types.int;
+              description = ''
+                The number of shares required to reconstitute a file.
+              '';
+            };
+            client.shares.happy = mkOption {
+              default = 7;
+              type = types.int;
+              description = ''
+                The number of distinct storage nodes required to store
+                a file.
+              '';
+            };
+            client.shares.total = mkOption {
+              default = 10;
+              type = types.int;
+              description = ''
+                The number of shares required to store a file.
+              '';
+            };
+            storage.enable = mkEnableOption "storage service";
+            storage.reservedSpace = mkOption {
+              default = "1G";
+              type = types.str;
+              description = ''
+                The amount of filesystem space to not use for storage.
+              '';
+            };
+            helper.enable = mkEnableOption "helper service";
+            sftpd.enable = mkEnableOption "SFTP service";
+            sftpd.port = mkOption {
+              default = null;
+              type = types.nullOr types.int;
+              description = ''
+                The port on which the SFTP server will listen.
+
+                This is the correct setting to tweak if you want Tahoe's SFTP
+                daemon to listen on a different port.
+              '';
+            };
+            sftpd.hostPublicKeyFile = mkOption {
+              default = null;
+              type = types.nullOr types.path;
+              description = ''
+                Path to the SSH host public key.
+              '';
+            };
+            sftpd.hostPrivateKeyFile = mkOption {
+              default = null;
+              type = types.nullOr types.path;
+              description = ''
+                Path to the SSH host private key.
+              '';
+            };
+            sftpd.accounts.file = mkOption {
+              default = null;
+              type = types.nullOr types.path;
+              description = ''
+                Path to the accounts file.
+              '';
+            };
+            sftpd.accounts.url = mkOption {
+              default = null;
+              type = types.nullOr types.str;
+              description = ''
+                URL of the accounts server.
+              '';
+            };
+            package = mkOption {
+              default = pkgs.tahoelafs;
+              defaultText = literalExpression "pkgs.tahoelafs";
+              type = types.package;
+              description = ''
+                The package to use for the Tahoe LAFS daemon.
+              '';
+            };
+          };
+        });
+        description = ''
+          The Tahoe nodes.
+        '';
+      };
+    };
+    config = mkMerge [
+      (mkIf (cfg.introducers != {}) {
+        environment = {
+          etc = flip mapAttrs' cfg.introducers (node: settings:
+            nameValuePair "tahoe-lafs/introducer-${node}.cfg" {
+              mode = "0444";
+              text = ''
+                # This configuration is generated by Nix. Edit at your own
+                # peril; here be dragons.
+
+                [node]
+                nickname = ${settings.nickname}
+                tub.port = ${toString settings.tub.port}
+                ${optionalString (settings.tub.location != null)
+                  "tub.location = ${settings.tub.location}"}
+              '';
+            });
+          # Actually require Tahoe, so that we will have it installed.
+          systemPackages = flip mapAttrsToList cfg.introducers (node: settings:
+            settings.package
+          );
+        };
+        # Open up the firewall.
+        # networking.firewall.allowedTCPPorts = flip mapAttrsToList cfg.introducers
+        #   (node: settings: settings.tub.port);
+        systemd.services = flip mapAttrs' cfg.introducers (node: settings:
+          let
+            pidfile = "/run/tahoe.introducer-${node}.pid";
+            # This is a directory, but it has no trailing slash. Tahoe commands
+            # get antsy when there's a trailing slash.
+            nodedir = "/var/db/tahoe-lafs/introducer-${node}";
+          in nameValuePair "tahoe.introducer-${node}" {
+            description = "Tahoe LAFS node ${node}";
+            wantedBy = [ "multi-user.target" ];
+            path = [ settings.package ];
+            restartTriggers = [
+              config.environment.etc."tahoe-lafs/introducer-${node}.cfg".source ];
+            serviceConfig = {
+              Type = "simple";
+              PIDFile = pidfile;
+              # Believe it or not, Tahoe is very brittle about the order of
+              # arguments to $(tahoe run). The node directory must come first,
+              # and arguments which alter Twisted's behavior come afterwards.
+              ExecStart = ''
+                ${settings.package}/bin/tahoe run ${lib.escapeShellArg nodedir} --pidfile=${lib.escapeShellArg pidfile}
+              '';
+            };
+            preStart = ''
+              if [ ! -d ${lib.escapeShellArg nodedir} ]; then
+                mkdir -p /var/db/tahoe-lafs
+                # See https://github.com/NixOS/nixpkgs/issues/25273
+                tahoe create-introducer \
+                  --hostname="${config.networking.hostName}" \
+                  ${lib.escapeShellArg nodedir}
+              fi
+
+              # Tahoe has created a predefined tahoe.cfg which we must now
+              # scribble over.
+              # XXX I thought that a symlink would work here, but it doesn't, so
+              # we must do this on every prestart. Fixes welcome.
+              # rm ${nodedir}/tahoe.cfg
+              # ln -s /etc/tahoe-lafs/introducer-${node}.cfg ${nodedir}/tahoe.cfg
+              cp /etc/tahoe-lafs/introducer-"${node}".cfg ${lib.escapeShellArg nodedir}/tahoe.cfg
+            '';
+          });
+        users.users = flip mapAttrs' cfg.introducers (node: _:
+          nameValuePair "tahoe.introducer-${node}" {
+            description = "Tahoe node user for introducer ${node}";
+            isSystemUser = true;
+          });
+      })
+      (mkIf (cfg.nodes != {}) {
+        environment = {
+          etc = flip mapAttrs' cfg.nodes (node: settings:
+            nameValuePair "tahoe-lafs/${node}.cfg" {
+              mode = "0444";
+              text = ''
+                # This configuration is generated by Nix. Edit at your own
+                # peril; here be dragons.
+
+                [node]
+                nickname = ${settings.nickname}
+                tub.port = ${toString settings.tub.port}
+                ${optionalString (settings.tub.location != null)
+                  "tub.location = ${settings.tub.location}"}
+                # This is a Twisted endpoint. Twisted Web doesn't work on
+                # non-TCP. ~ C.
+                web.port = tcp:${toString settings.web.port}
+
+                [client]
+                ${optionalString (settings.client.introducer != null)
+                  "introducer.furl = ${settings.client.introducer}"}
+                ${optionalString (settings.client.helper != null)
+                  "helper.furl = ${settings.client.helper}"}
+
+                shares.needed = ${toString settings.client.shares.needed}
+                shares.happy = ${toString settings.client.shares.happy}
+                shares.total = ${toString settings.client.shares.total}
+
+                [storage]
+                enabled = ${boolToString settings.storage.enable}
+                reserved_space = ${settings.storage.reservedSpace}
+
+                [helper]
+                enabled = ${boolToString settings.helper.enable}
+
+                [sftpd]
+                enabled = ${boolToString settings.sftpd.enable}
+                ${optionalString (settings.sftpd.port != null)
+                  "port = ${toString settings.sftpd.port}"}
+                ${optionalString (settings.sftpd.hostPublicKeyFile != null)
+                  "host_pubkey_file = ${settings.sftpd.hostPublicKeyFile}"}
+                ${optionalString (settings.sftpd.hostPrivateKeyFile != null)
+                  "host_privkey_file = ${settings.sftpd.hostPrivateKeyFile}"}
+                ${optionalString (settings.sftpd.accounts.file != null)
+                  "accounts.file = ${settings.sftpd.accounts.file}"}
+                ${optionalString (settings.sftpd.accounts.url != null)
+                  "accounts.url = ${settings.sftpd.accounts.url}"}
+              '';
+            });
+          # Actually require Tahoe, so that we will have it installed.
+          systemPackages = flip mapAttrsToList cfg.nodes (node: settings:
+            settings.package
+          );
+        };
+        # Open up the firewall.
+        # networking.firewall.allowedTCPPorts = flip mapAttrsToList cfg.nodes
+        #   (node: settings: settings.tub.port);
+        systemd.services = flip mapAttrs' cfg.nodes (node: settings:
+          let
+            pidfile = "/run/tahoe.${node}.pid";
+            # This is a directory, but it has no trailing slash. Tahoe commands
+            # get antsy when there's a trailing slash.
+            nodedir = "/var/db/tahoe-lafs/${node}";
+          in nameValuePair "tahoe.${node}" {
+            description = "Tahoe LAFS node ${node}";
+            wantedBy = [ "multi-user.target" ];
+            path = [ settings.package ];
+            restartTriggers = [
+              config.environment.etc."tahoe-lafs/${node}.cfg".source ];
+            serviceConfig = {
+              Type = "simple";
+              PIDFile = pidfile;
+              # Believe it or not, Tahoe is very brittle about the order of
+              # arguments to $(tahoe run). The node directory must come first,
+              # and arguments which alter Twisted's behavior come afterwards.
+              ExecStart = ''
+                ${settings.package}/bin/tahoe run ${lib.escapeShellArg nodedir} --pidfile=${lib.escapeShellArg pidfile}
+              '';
+            };
+            preStart = ''
+              if [ ! -d ${lib.escapeShellArg nodedir} ]; then
+                mkdir -p /var/db/tahoe-lafs
+                tahoe create-node --hostname=localhost ${lib.escapeShellArg nodedir}
+              fi
+
+              # Tahoe has created a predefined tahoe.cfg which we must now
+              # scribble over.
+              # XXX I thought that a symlink would work here, but it doesn't, so
+              # we must do this on every prestart. Fixes welcome.
+              # rm ${nodedir}/tahoe.cfg
+              # ln -s /etc/tahoe-lafs/${lib.escapeShellArg node}.cfg ${nodedir}/tahoe.cfg
+              cp /etc/tahoe-lafs/${lib.escapeShellArg node}.cfg ${lib.escapeShellArg nodedir}/tahoe.cfg
+            '';
+          });
+        users.users = flip mapAttrs' cfg.nodes (node: _:
+          nameValuePair "tahoe.${node}" {
+            description = "Tahoe node user for node ${node}";
+            isSystemUser = true;
+          });
+      })
+    ];
+  }
diff --git a/nixos/modules/services/network-filesystems/u9fs.nix b/nixos/modules/services/network-filesystems/u9fs.nix
new file mode 100644
index 00000000000..77961b78cad
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/u9fs.nix
@@ -0,0 +1,78 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.u9fs;
+in
+{
+
+  options = {
+
+    services.u9fs = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to run the u9fs 9P server for Unix.";
+      };
+
+      listenStreams = mkOption {
+        type = types.listOf types.str;
+        default = [ "564" ];
+        example = [ "192.168.16.1:564" ];
+        description = ''
+          Sockets to listen for clients on.
+          See <command>man 5 systemd.socket</command> for socket syntax.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "nobody";
+        description =
+          "User to run u9fs under.";
+      };
+
+      extraArgs = mkOption {
+        type = types.str;
+        default = "";
+        example = "-a none";
+        description =
+          ''
+            Extra arguments to pass on invocation,
+            see <command>man 4 u9fs</command>
+          '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd = {
+      sockets.u9fs = {
+        description = "U9fs Listening Socket";
+        wantedBy = [ "sockets.target" ];
+        after = [ "network.target" ];
+        inherit (cfg) listenStreams;
+        socketConfig.Accept = "yes";
+      };
+      services."u9fs@" = {
+        description = "9P Protocol Server";
+        reloadIfChanged = true;
+        requires = [ "u9fs.socket" ];
+        serviceConfig =
+          { ExecStart = "-${pkgs.u9fs}/bin/u9fs ${cfg.extraArgs}";
+            StandardInput = "socket";
+            StandardError = "journal";
+            User = cfg.user;
+            AmbientCapabilities = "cap_setuid cap_setgid";
+          };
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/network-filesystems/webdav-server-rs.nix b/nixos/modules/services/network-filesystems/webdav-server-rs.nix
new file mode 100644
index 00000000000..1c5c299cb67
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/webdav-server-rs.nix
@@ -0,0 +1,144 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.webdav-server-rs;
+  format = pkgs.formats.toml { };
+  settings = recursiveUpdate
+    {
+      server.uid = config.users.users."${cfg.user}".uid;
+      server.gid = config.users.groups."${cfg.group}".gid;
+    }
+    cfg.settings;
+in
+{
+  options = {
+    services.webdav-server-rs = {
+      enable = mkEnableOption "WebDAV server";
+
+      user = mkOption {
+        type = types.str;
+        default = "webdav";
+        description = "User to run under when setuid is not enabled.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "webdav";
+        description = "Group to run under when setuid is not enabled.";
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = { };
+        description = ''
+          Attrset that is converted and passed as config file. Available
+          options can be found at
+          <link xlink:href="https://github.com/miquels/webdav-server-rs/blob/master/webdav-server.toml">here</link>.
+        '';
+        example = literalExpression ''
+          {
+            server.listen = [ "0.0.0.0:4918" "[::]:4918" ];
+            accounts = {
+              auth-type = "htpasswd.default";
+              acct-type = "unix";
+            };
+            htpasswd.default = {
+              htpasswd = "/etc/htpasswd";
+            };
+            location = [
+              {
+                route = [ "/public/*path" ];
+                directory = "/srv/public";
+                handler = "filesystem";
+                methods = [ "webdav-ro" ];
+                autoindex = true;
+                auth = "false";
+              }
+              {
+                route = [ "/user/:user/*path" ];
+                directory = "~";
+                handler = "filesystem";
+                methods = [ "webdav-rw" ];
+                autoindex = true;
+                auth = "true";
+                setuid = true;
+              }
+            ];
+          }
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.path;
+        default = format.generate "webdav-server.toml" settings;
+        defaultText = "Config file generated from services.webdav-server-rs.settings";
+        description = ''
+          Path to config file. If this option is set, it will override any
+          configuration done in services.webdav-server-rs.settings.
+        '';
+        example = "/etc/webdav-server.toml";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = hasAttr cfg.user config.users.users && config.users.users."${cfg.user}".uid != null;
+        message = "users.users.${cfg.user} and users.users.${cfg.user}.uid must be defined.";
+      }
+      {
+        assertion = hasAttr cfg.group config.users.groups && config.users.groups."${cfg.group}".gid != null;
+        message = "users.groups.${cfg.group} and users.groups.${cfg.group}.gid must be defined.";
+      }
+    ];
+
+    users.users = optionalAttrs (cfg.user == "webdav") {
+      webdav = {
+        description = "WebDAV user";
+        group = cfg.group;
+        uid = config.ids.uids.webdav;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "webdav") {
+      webdav.gid = config.ids.gids.webdav;
+    };
+
+    systemd.services.webdav-server-rs = {
+      description = "WebDAV server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.webdav-server-rs}/bin/webdav-server -c ${cfg.configFile}";
+
+        CapabilityBoundingSet = [
+          "CAP_SETUID"
+          "CAP_SETGID"
+        ];
+
+        NoExecPaths = [ "/" ];
+        ExecPaths = [ "/nix/store" ];
+
+        # This program actively detects if it is running in root user account
+        # when it starts and uses root privilege to switch process uid to
+        # respective unix user when a user logs in.  Maybe we can enable
+        # DynamicUser in the future when it's able to detect CAP_SETUID and
+        # CAP_SETGID capabilities.
+
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = true;
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ pmy ];
+}
diff --git a/nixos/modules/services/network-filesystems/webdav.nix b/nixos/modules/services/network-filesystems/webdav.nix
new file mode 100644
index 00000000000..a810af40fd4
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/webdav.nix
@@ -0,0 +1,107 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.webdav;
+  format = pkgs.formats.yaml { };
+in
+{
+  options = {
+    services.webdav = {
+      enable = mkEnableOption "WebDAV server";
+
+      user = mkOption {
+        type = types.str;
+        default = "webdav";
+        description = "User account under which WebDAV runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "webdav";
+        description = "Group under which WebDAV runs.";
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = { };
+        description = ''
+          Attrset that is converted and passed as config file. Available options
+          can be found at
+          <link xlink:href="https://github.com/hacdias/webdav">here</link>.
+
+          This program supports reading username and password configuration
+          from environment variables, so it's strongly recommended to store
+          username and password in a separate
+          <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.exec.html#EnvironmentFile=">EnvironmentFile</link>.
+          This prevents adding secrets to the world-readable Nix store.
+        '';
+        example = literalExpression ''
+          {
+              address = "0.0.0.0";
+              port = 8080;
+              scope = "/srv/public";
+              modify = true;
+              auth = true;
+              users = [
+                {
+                  username = "{env}ENV_USERNAME";
+                  password = "{env}ENV_PASSWORD";
+                }
+              ];
+          }
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.path;
+        default = format.generate "webdav.yaml" cfg.settings;
+        defaultText = "Config file generated from services.webdav.settings";
+        description = ''
+          Path to config file. If this option is set, it will override any
+          configuration done in options.services.webdav.settings.
+        '';
+        example = "/etc/webdav/config.yaml";
+      };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          Environment file as defined in <citerefentry>
+          <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+          </citerefentry>.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users = mkIf (cfg.user == "webdav") {
+      webdav = {
+        description = "WebDAV daemon user";
+        group = cfg.group;
+        uid = config.ids.uids.webdav;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "webdav") {
+      webdav.gid = config.ids.gids.webdav;
+    };
+
+    systemd.services.webdav = {
+      description = "WebDAV server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.webdav}/bin/webdav -c ${cfg.configFile}";
+        Restart = "on-failure";
+        User = cfg.user;
+        Group = cfg.group;
+        EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ pmy ];
+}
diff --git a/nixos/modules/services/network-filesystems/xtreemfs.nix b/nixos/modules/services/network-filesystems/xtreemfs.nix
new file mode 100644
index 00000000000..fc072311578
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/xtreemfs.nix
@@ -0,0 +1,495 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xtreemfs;
+
+  xtreemfs = pkgs.xtreemfs;
+
+  home = cfg.homeDir;
+
+  startupScript = class: configPath: pkgs.writeScript "xtreemfs-osd.sh" ''
+    #! ${pkgs.runtimeShell}
+    JAVA_HOME="${pkgs.jdk}"
+    JAVADIR="${xtreemfs}/share/java"
+    JAVA_CALL="$JAVA_HOME/bin/java -ea -cp $JAVADIR/XtreemFS.jar:$JAVADIR/BabuDB.jar:$JAVADIR/Flease.jar:$JAVADIR/protobuf-java-2.5.0.jar:$JAVADIR/Foundation.jar:$JAVADIR/jdmkrt.jar:$JAVADIR/jdmktk.jar:$JAVADIR/commons-codec-1.3.jar"
+    $JAVA_CALL ${class} ${configPath}
+  '';
+
+  dirReplicationConfig = pkgs.writeText "xtreemfs-dir-replication-plugin.properties" ''
+    babudb.repl.backupDir = ${home}/server-repl-dir
+    plugin.jar = ${xtreemfs}/share/java/BabuDB_replication_plugin.jar
+    babudb.repl.dependency.0 = ${xtreemfs}/share/java/Flease.jar
+
+    ${cfg.dir.replication.extraConfig}
+  '';
+
+  dirConfig = pkgs.writeText "xtreemfs-dir-config.properties" ''
+    uuid = ${cfg.dir.uuid}
+    listen.port = ${toString cfg.dir.port}
+    ${optionalString (cfg.dir.address != "") "listen.address = ${cfg.dir.address}"}
+    http_port = ${toString cfg.dir.httpPort}
+    babudb.baseDir = ${home}/dir/database
+    babudb.logDir = ${home}/dir/db-log
+    babudb.sync = ${if cfg.dir.replication.enable then "FDATASYNC" else cfg.dir.syncMode}
+
+    ${optionalString cfg.dir.replication.enable "babudb.plugin.0 = ${dirReplicationConfig}"}
+
+    ${cfg.dir.extraConfig}
+  '';
+
+  mrcReplicationConfig = pkgs.writeText "xtreemfs-mrc-replication-plugin.properties" ''
+    babudb.repl.backupDir = ${home}/server-repl-mrc
+    plugin.jar = ${xtreemfs}/share/java/BabuDB_replication_plugin.jar
+    babudb.repl.dependency.0 = ${xtreemfs}/share/java/Flease.jar
+
+    ${cfg.mrc.replication.extraConfig}
+  '';
+
+  mrcConfig = pkgs.writeText "xtreemfs-mrc-config.properties" ''
+    uuid = ${cfg.mrc.uuid}
+    listen.port = ${toString cfg.mrc.port}
+    ${optionalString (cfg.mrc.address != "") "listen.address = ${cfg.mrc.address}"}
+    http_port = ${toString cfg.mrc.httpPort}
+    babudb.baseDir = ${home}/mrc/database
+    babudb.logDir = ${home}/mrc/db-log
+    babudb.sync = ${if cfg.mrc.replication.enable then "FDATASYNC" else cfg.mrc.syncMode}
+
+    ${optionalString cfg.mrc.replication.enable "babudb.plugin.0 = ${mrcReplicationConfig}"}
+
+    ${cfg.mrc.extraConfig}
+  '';
+
+  osdConfig = pkgs.writeText "xtreemfs-osd-config.properties" ''
+    uuid = ${cfg.osd.uuid}
+    listen.port = ${toString cfg.osd.port}
+    ${optionalString (cfg.osd.address != "") "listen.address = ${cfg.osd.address}"}
+    http_port = ${toString cfg.osd.httpPort}
+    object_dir = ${home}/osd/
+
+    ${cfg.osd.extraConfig}
+  '';
+
+  optionalDir = optionals cfg.dir.enable ["xtreemfs-dir.service"];
+
+  systemdOptionalDependencies = {
+    after = [ "network.target" ] ++ optionalDir;
+    wantedBy = [ "multi-user.target" ] ++ optionalDir;
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.xtreemfs = {
+
+      enable = mkEnableOption "XtreemFS";
+
+      homeDir = mkOption {
+        type = types.path;
+        default = "/var/lib/xtreemfs";
+        description = ''
+          XtreemFS home dir for the xtreemfs user.
+        '';
+      };
+
+      dir = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether to enable XtreemFS DIR service.
+          '';
+        };
+
+        uuid = mkOption {
+          example = "eacb6bab-f444-4ebf-a06a-3f72d7465e40";
+          type = types.str;
+          description = ''
+            Must be set to a unique identifier, preferably a UUID according to
+            RFC 4122. UUIDs can be generated with `uuidgen` command, found in
+            the `util-linux` package.
+          '';
+        };
+        port = mkOption {
+          default = 32638;
+          type = types.port;
+          description = ''
+            The port to listen on for incoming connections (TCP).
+          '';
+        };
+        address = mkOption {
+          type = types.str;
+          example = "127.0.0.1";
+          default = "";
+          description = ''
+            If specified, it defines the interface to listen on. If not
+            specified, the service will listen on all interfaces (any).
+          '';
+        };
+        httpPort = mkOption {
+          default = 30638;
+          type = types.port;
+          description = ''
+            Specifies the listen port for the HTTP service that returns the
+            status page.
+          '';
+        };
+        syncMode = mkOption {
+          type = types.enum [ "ASYNC" "SYNC_WRITE_METADATA" "SYNC_WRITE" "FDATASYNC" "FSYNC" ];
+          default = "FSYNC";
+          example = "FDATASYNC";
+          description = ''
+            The sync mode influences how operations are committed to the disk
+            log before the operation is acknowledged to the caller.
+
+            -ASYNC mode the writes to the disk log are buffered in memory by the operating system. This is the fastest mode but will lead to data loss in case of a crash, kernel panic or power failure.
+            -SYNC_WRITE_METADATA opens the file with O_SYNC, the system will not buffer any writes. The operation will be acknowledged when data has been safely written to disk. This mode is slow but offers maximum data safety. However, BabuDB cannot influence the disk drive caches, this depends on the OS and hard disk model.
+            -SYNC_WRITE similar to SYNC_WRITE_METADATA but opens file with O_DSYNC which means that only the data is commit to disk. This can lead to some data loss depending on the implementation of the underlying file system. Linux does not implement this mode.
+            -FDATASYNC is similar to SYNC_WRITE but opens the file in asynchronous mode and calls fdatasync() after writing the data to disk.
+            -FSYNC is similar to SYNC_WRITE_METADATA but opens the file in asynchronous mode and calls fsync() after writing the data to disk.
+
+            For best throughput use ASYNC, for maximum data safety use FSYNC.
+
+            (If xtreemfs.dir.replication.enable is true then FDATASYNC is forced)
+          '';
+        };
+        extraConfig = mkOption {
+          type = types.lines;
+          default = "";
+          example = ''
+            # specify whether SSL is required
+            ssl.enabled = true
+            ssl.service_creds.pw = passphrase
+            ssl.service_creds.container = pkcs12
+            ssl.service_creds = /etc/xos/xtreemfs/truststore/certs/dir.p12
+            ssl.trusted_certs = /etc/xos/xtreemfs/truststore/certs/trusted.jks
+            ssl.trusted_certs.pw = jks_passphrase
+            ssl.trusted_certs.container = jks
+          '';
+          description = ''
+            Configuration of XtreemFS DIR service.
+            WARNING: configuration is saved as plaintext inside nix store.
+            For more options: http://www.xtreemfs.org/xtfs-guide-1.5.1/index.html
+          '';
+        };
+        replication = {
+          enable = mkEnableOption "XtreemFS DIR replication plugin";
+          extraConfig = mkOption {
+            type = types.lines;
+            example = ''
+              # participants of the replication including this replica
+              babudb.repl.participant.0 = 192.168.0.10
+              babudb.repl.participant.0.port = 35676
+              babudb.repl.participant.1 = 192.168.0.11
+              babudb.repl.participant.1.port = 35676
+              babudb.repl.participant.2 = 192.168.0.12
+              babudb.repl.participant.2.port = 35676
+
+              # number of servers that at least have to be up to date
+              # To have a fault-tolerant system, this value has to be set to the
+              # majority of nodes i.e., if you have three replicas, set this to 2
+              # Please note that a setup with two nodes provides no fault-tolerance.
+              babudb.repl.sync.n = 2
+
+              # specify whether SSL is required
+              babudb.ssl.enabled = true
+
+              babudb.ssl.protocol = tlsv12
+
+              # server credentials for SSL handshakes
+              babudb.ssl.service_creds = /etc/xos/xtreemfs/truststore/certs/osd.p12
+              babudb.ssl.service_creds.pw = passphrase
+              babudb.ssl.service_creds.container = pkcs12
+
+              # trusted certificates for SSL handshakes
+              babudb.ssl.trusted_certs = /etc/xos/xtreemfs/truststore/certs/trusted.jks
+              babudb.ssl.trusted_certs.pw = jks_passphrase
+              babudb.ssl.trusted_certs.container = jks
+
+              babudb.ssl.authenticationWithoutEncryption = false
+            '';
+            description = ''
+              Configuration of XtreemFS DIR replication plugin.
+              WARNING: configuration is saved as plaintext inside nix store.
+              For more options: http://www.xtreemfs.org/xtfs-guide-1.5.1/index.html
+            '';
+          };
+        };
+      };
+
+      mrc = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether to enable XtreemFS MRC service.
+          '';
+        };
+
+        uuid = mkOption {
+          example = "eacb6bab-f444-4ebf-a06a-3f72d7465e41";
+          type = types.str;
+          description = ''
+            Must be set to a unique identifier, preferably a UUID according to
+            RFC 4122. UUIDs can be generated with `uuidgen` command, found in
+            the `util-linux` package.
+          '';
+        };
+        port = mkOption {
+          default = 32636;
+          type = types.port;
+          description = ''
+            The port to listen on for incoming connections (TCP).
+          '';
+        };
+        address = mkOption {
+          example = "127.0.0.1";
+          type = types.str;
+          default = "";
+          description = ''
+            If specified, it defines the interface to listen on. If not
+            specified, the service will listen on all interfaces (any).
+          '';
+        };
+        httpPort = mkOption {
+          default = 30636;
+          type = types.port;
+          description = ''
+            Specifies the listen port for the HTTP service that returns the
+            status page.
+          '';
+        };
+        syncMode = mkOption {
+          default = "FSYNC";
+          type = types.enum [ "ASYNC" "SYNC_WRITE_METADATA" "SYNC_WRITE" "FDATASYNC" "FSYNC" ];
+          example = "FDATASYNC";
+          description = ''
+            The sync mode influences how operations are committed to the disk
+            log before the operation is acknowledged to the caller.
+
+            -ASYNC mode the writes to the disk log are buffered in memory by the operating system. This is the fastest mode but will lead to data loss in case of a crash, kernel panic or power failure.
+            -SYNC_WRITE_METADATA opens the file with O_SYNC, the system will not buffer any writes. The operation will be acknowledged when data has been safely written to disk. This mode is slow but offers maximum data safety. However, BabuDB cannot influence the disk drive caches, this depends on the OS and hard disk model.
+            -SYNC_WRITE similar to SYNC_WRITE_METADATA but opens file with O_DSYNC which means that only the data is commit to disk. This can lead to some data loss depending on the implementation of the underlying file system. Linux does not implement this mode.
+            -FDATASYNC is similar to SYNC_WRITE but opens the file in asynchronous mode and calls fdatasync() after writing the data to disk.
+            -FSYNC is similar to SYNC_WRITE_METADATA but opens the file in asynchronous mode and calls fsync() after writing the data to disk.
+
+            For best throughput use ASYNC, for maximum data safety use FSYNC.
+
+            (If xtreemfs.mrc.replication.enable is true then FDATASYNC is forced)
+          '';
+        };
+        extraConfig = mkOption {
+          type = types.lines;
+          example = ''
+            osd_check_interval = 300
+            no_atime = true
+            local_clock_renewal = 0
+            remote_time_sync = 30000
+            authentication_provider = org.xtreemfs.common.auth.NullAuthProvider
+
+            # shared secret between the MRC and all OSDs
+            capability_secret = iNG8UuQJrJ6XVDTe
+
+            dir_service.host = 192.168.0.10
+            dir_service.port = 32638
+
+            # if replication is enabled
+            dir_service.1.host = 192.168.0.11
+            dir_service.1.port = 32638
+            dir_service.2.host = 192.168.0.12
+            dir_service.2.port = 32638
+
+            # specify whether SSL is required
+            ssl.enabled = true
+            ssl.protocol = tlsv12
+            ssl.service_creds.pw = passphrase
+            ssl.service_creds.container = pkcs12
+            ssl.service_creds = /etc/xos/xtreemfs/truststore/certs/mrc.p12
+            ssl.trusted_certs = /etc/xos/xtreemfs/truststore/certs/trusted.jks
+            ssl.trusted_certs.pw = jks_passphrase
+            ssl.trusted_certs.container = jks
+          '';
+          description = ''
+            Configuration of XtreemFS MRC service.
+            WARNING: configuration is saved as plaintext inside nix store.
+            For more options: http://www.xtreemfs.org/xtfs-guide-1.5.1/index.html
+          '';
+        };
+        replication = {
+          enable = mkEnableOption "XtreemFS MRC replication plugin";
+          extraConfig = mkOption {
+            type = types.lines;
+            example = ''
+              # participants of the replication including this replica
+              babudb.repl.participant.0 = 192.168.0.10
+              babudb.repl.participant.0.port = 35678
+              babudb.repl.participant.1 = 192.168.0.11
+              babudb.repl.participant.1.port = 35678
+              babudb.repl.participant.2 = 192.168.0.12
+              babudb.repl.participant.2.port = 35678
+
+              # number of servers that at least have to be up to date
+              # To have a fault-tolerant system, this value has to be set to the
+              # majority of nodes i.e., if you have three replicas, set this to 2
+              # Please note that a setup with two nodes provides no fault-tolerance.
+              babudb.repl.sync.n = 2
+
+              # specify whether SSL is required
+              babudb.ssl.enabled = true
+
+              babudb.ssl.protocol = tlsv12
+
+              # server credentials for SSL handshakes
+              babudb.ssl.service_creds = /etc/xos/xtreemfs/truststore/certs/osd.p12
+              babudb.ssl.service_creds.pw = passphrase
+              babudb.ssl.service_creds.container = pkcs12
+
+              # trusted certificates for SSL handshakes
+              babudb.ssl.trusted_certs = /etc/xos/xtreemfs/truststore/certs/trusted.jks
+              babudb.ssl.trusted_certs.pw = jks_passphrase
+              babudb.ssl.trusted_certs.container = jks
+
+              babudb.ssl.authenticationWithoutEncryption = false
+            '';
+            description = ''
+              Configuration of XtreemFS MRC replication plugin.
+              WARNING: configuration is saved as plaintext inside nix store.
+              For more options: http://www.xtreemfs.org/xtfs-guide-1.5.1/index.html
+            '';
+          };
+        };
+      };
+
+      osd = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether to enable XtreemFS OSD service.
+          '';
+        };
+
+        uuid = mkOption {
+          example = "eacb6bab-f444-4ebf-a06a-3f72d7465e42";
+          type = types.str;
+          description = ''
+            Must be set to a unique identifier, preferably a UUID according to
+            RFC 4122. UUIDs can be generated with `uuidgen` command, found in
+            the `util-linux` package.
+          '';
+        };
+        port = mkOption {
+          default = 32640;
+          type = types.port;
+          description = ''
+            The port to listen on for incoming connections (TCP and UDP).
+          '';
+        };
+        address = mkOption {
+          example = "127.0.0.1";
+          type = types.str;
+          default = "";
+          description = ''
+            If specified, it defines the interface to listen on. If not
+            specified, the service will listen on all interfaces (any).
+          '';
+        };
+        httpPort = mkOption {
+          default = 30640;
+          type = types.port;
+          description = ''
+            Specifies the listen port for the HTTP service that returns the
+            status page.
+          '';
+        };
+        extraConfig = mkOption {
+          type = types.lines;
+          example = ''
+            local_clock_renewal = 0
+            remote_time_sync = 30000
+            report_free_space = true
+            capability_secret = iNG8UuQJrJ6XVDTe
+
+            dir_service.host = 192.168.0.10
+            dir_service.port = 32638
+
+            # if replication is used
+            dir_service.1.host = 192.168.0.11
+            dir_service.1.port = 32638
+            dir_service.2.host = 192.168.0.12
+            dir_service.2.port = 32638
+
+            # specify whether SSL is required
+            ssl.enabled = true
+            ssl.service_creds.pw = passphrase
+            ssl.service_creds.container = pkcs12
+            ssl.service_creds = /etc/xos/xtreemfs/truststore/certs/osd.p12
+            ssl.trusted_certs = /etc/xos/xtreemfs/truststore/certs/trusted.jks
+            ssl.trusted_certs.pw = jks_passphrase
+            ssl.trusted_certs.container = jks
+          '';
+          description = ''
+            Configuration of XtreemFS OSD service.
+            WARNING: configuration is saved as plaintext inside nix store.
+            For more options: http://www.xtreemfs.org/xtfs-guide-1.5.1/index.html
+          '';
+        };
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = lib.mkIf cfg.enable {
+
+    environment.systemPackages = [ xtreemfs ];
+
+    users.users.xtreemfs =
+      { uid = config.ids.uids.xtreemfs;
+        description = "XtreemFS user";
+        createHome = true;
+        home = home;
+      };
+
+    users.groups.xtreemfs =
+      { gid = config.ids.gids.xtreemfs;
+      };
+
+    systemd.services.xtreemfs-dir = mkIf cfg.dir.enable {
+      description = "XtreemFS-DIR Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        User = "xtreemfs";
+        ExecStart = "${startupScript "org.xtreemfs.dir.DIR" dirConfig}";
+      };
+    };
+
+    systemd.services.xtreemfs-mrc = mkIf cfg.mrc.enable ({
+      description = "XtreemFS-MRC Server";
+      serviceConfig = {
+        User = "xtreemfs";
+        ExecStart = "${startupScript "org.xtreemfs.mrc.MRC" mrcConfig}";
+      };
+    } // systemdOptionalDependencies);
+
+    systemd.services.xtreemfs-osd = mkIf cfg.osd.enable ({
+      description = "XtreemFS-OSD Server";
+      serviceConfig = {
+        User = "xtreemfs";
+        ExecStart = "${startupScript "org.xtreemfs.osd.OSD" osdConfig}";
+      };
+    } // systemdOptionalDependencies);
+
+  };
+
+}
diff --git a/nixos/modules/services/network-filesystems/yandex-disk.nix b/nixos/modules/services/network-filesystems/yandex-disk.nix
new file mode 100644
index 00000000000..a5b1f9d4ab6
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/yandex-disk.nix
@@ -0,0 +1,116 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.yandex-disk;
+
+  dir = "/var/lib/yandex-disk";
+
+  u = if cfg.user != null then cfg.user else "yandexdisk";
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.yandex-disk = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "
+          Whether to enable Yandex-disk client. See https://disk.yandex.ru/
+        ";
+      };
+
+      username = mkOption {
+        default = "";
+        type = types.str;
+        description = ''
+          Your yandex.com login name.
+        '';
+      };
+
+      password = mkOption {
+        default = "";
+        type = types.str;
+        description = ''
+          Your yandex.com password. Warning: it will be world-readable in /nix/store.
+        '';
+      };
+
+      user = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description = ''
+          The user the yandex-disk daemon should run as.
+        '';
+      };
+
+      directory = mkOption {
+        type = types.path;
+        default = "/home/Yandex.Disk";
+        description = "The directory to use for Yandex.Disk storage";
+      };
+
+      excludes = mkOption {
+        default = "";
+        type = types.commas;
+        example = "data,backup";
+        description = ''
+          Comma-separated list of directories which are excluded from synchronization.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users = mkIf (cfg.user == null) [ {
+      name = u;
+      uid = config.ids.uids.yandexdisk;
+      group = "nogroup";
+      home = dir;
+    } ];
+
+    systemd.services.yandex-disk = {
+      description = "Yandex-disk server";
+
+      after = [ "network.target" ];
+
+      wantedBy = [ "multi-user.target" ];
+
+      # FIXME: have to specify ${directory} here as well
+      unitConfig.RequiresMountsFor = dir;
+
+      script = ''
+        mkdir -p -m 700 ${dir}
+        chown ${u} ${dir}
+
+        if ! test -d "${cfg.directory}" ; then
+          (mkdir -p -m 755 ${cfg.directory} && chown ${u} ${cfg.directory}) ||
+            exit 1
+        fi
+
+        ${pkgs.su}/bin/su -s ${pkgs.runtimeShell} ${u} \
+          -c '${pkgs.yandex-disk}/bin/yandex-disk token -p ${cfg.password} ${cfg.username} ${dir}/token'
+
+        ${pkgs.su}/bin/su -s ${pkgs.runtimeShell} ${u} \
+          -c '${pkgs.yandex-disk}/bin/yandex-disk start --no-daemon -a ${dir}/token -d ${cfg.directory} --exclude-dirs=${cfg.excludes}'
+      '';
+
+    };
+  };
+
+}
+
diff --git a/nixos/modules/services/networking/3proxy.nix b/nixos/modules/services/networking/3proxy.nix
new file mode 100644
index 00000000000..326a8671fcc
--- /dev/null
+++ b/nixos/modules/services/networking/3proxy.nix
@@ -0,0 +1,413 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  pkg = pkgs._3proxy;
+  cfg = config.services._3proxy;
+  optionalList = list: if list == [ ] then "*" else concatMapStringsSep "," toString list;
+in {
+  options.services._3proxy = {
+    enable = mkEnableOption "3proxy";
+    confFile = mkOption {
+      type = types.path;
+      example = "/var/lib/3proxy/3proxy.conf";
+      description = ''
+        Ignore all other 3proxy options and load configuration from this file.
+      '';
+    };
+    usersFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/var/lib/3proxy/3proxy.passwd";
+      description = ''
+        Load users and passwords from this file.
+
+        Example users file with plain-text passwords:
+
+        <literal>
+          test1:CL:password1
+          test2:CL:password2
+        </literal>
+
+        Example users file with md5-crypted passwords:
+
+        <literal>
+          test1:CR:$1$tFkisVd2$1GA8JXkRmTXdLDytM/i3a1
+          test2:CR:$1$rkpibm5J$Aq1.9VtYAn0JrqZ8M.1ME.
+        </literal>
+
+        You can generate md5-crypted passwords via https://unix4lyfe.org/crypt/
+        Note that htpasswd tool generates incompatible md5-crypted passwords.
+        Consult <link xlink:href="https://github.com/z3APA3A/3proxy/wiki/How-To-(incomplete)#USERS">documentation</link> for more information.
+      '';
+    };
+    services = mkOption {
+      type = types.listOf (types.submodule {
+        options = {
+          type = mkOption {
+            type = types.enum [
+              "proxy"
+              "socks"
+              "pop3p"
+              "ftppr"
+              "admin"
+              "dnspr"
+              "tcppm"
+              "udppm"
+            ];
+            example = "proxy";
+            description = ''
+              Service type. The following values are valid:
+
+              <itemizedlist>
+                <listitem><para>
+                  <literal>"proxy"</literal>: HTTP/HTTPS proxy (default port 3128).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"socks"</literal>: SOCKS 4/4.5/5 proxy (default port 1080).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"pop3p"</literal>: POP3 proxy (default port 110).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"ftppr"</literal>: FTP proxy (default port 21).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"admin"</literal>: Web interface (default port 80).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"dnspr"</literal>: Caching DNS proxy (default port 53).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"tcppm"</literal>: TCP portmapper.
+                </para></listitem>
+                <listitem><para>
+                  <literal>"udppm"</literal>: UDP portmapper.
+                </para></listitem>
+              </itemizedlist>
+            '';
+          };
+          bindAddress = mkOption {
+            type = types.str;
+            default = "[::]";
+            example = "127.0.0.1";
+            description = ''
+              Address used for service.
+            '';
+          };
+          bindPort = mkOption {
+            type = types.nullOr types.int;
+            default = null;
+            example = 3128;
+            description = ''
+              Override default port used for service.
+            '';
+          };
+          maxConnections = mkOption {
+            type = types.int;
+            default = 100;
+            example = 1000;
+            description = ''
+              Maximum number of simulationeous connections to this service.
+            '';
+          };
+          auth = mkOption {
+            type = types.listOf (types.enum [ "none" "iponly" "strong" ]);
+            example = [ "iponly" "strong" ];
+            description = ''
+              Authentication type. The following values are valid:
+
+              <itemizedlist>
+                <listitem><para>
+                  <literal>"none"</literal>: disables both authentication and authorization. You can not use ACLs.
+                </para></listitem>
+                <listitem><para>
+                  <literal>"iponly"</literal>: specifies no authentication. ACLs authorization is used.
+                </para></listitem>
+                <listitem><para>
+                  <literal>"strong"</literal>: authentication by username/password. If user is not registered their access is denied regardless of ACLs.
+                </para></listitem>
+              </itemizedlist>
+
+              Double authentication is possible, e.g.
+
+              <literal>
+                {
+                  auth = [ "iponly" "strong" ];
+                  acl = [
+                    {
+                      rule = "allow";
+                      targets = [ "192.168.0.0/16" ];
+                    }
+                    {
+                      rule = "allow"
+                      users = [ "user1" "user2" ];
+                    }
+                  ];
+                }
+              </literal>
+              In this example strong username authentication is not required to access 192.168.0.0/16.
+            '';
+          };
+          acl = mkOption {
+            type = types.listOf (types.submodule {
+              options = {
+                rule = mkOption {
+                  type = types.enum [ "allow" "deny" ];
+                  example = "allow";
+                  description = ''
+                    ACL rule. The following values are valid:
+
+                    <itemizedlist>
+                      <listitem><para>
+                        <literal>"allow"</literal>: connections allowed.
+                      </para></listitem>
+                      <listitem><para>
+                        <literal>"deny"</literal>: connections not allowed.
+                      </para></listitem>
+                    </itemizedlist>
+                  '';
+                };
+                users = mkOption {
+                  type = types.listOf types.str;
+                  default = [ ];
+                  example = [ "user1" "user2" "user3" ];
+                  description = ''
+                    List of users, use empty list for any.
+                  '';
+                };
+                sources = mkOption {
+                  type = types.listOf types.str;
+                  default = [ ];
+                  example = [ "127.0.0.1" "192.168.1.0/24" ];
+                  description = ''
+                    List of source IP range, use empty list for any.
+                  '';
+                };
+                targets = mkOption {
+                  type = types.listOf types.str;
+                  default = [ ];
+                  example = [ "127.0.0.1" "192.168.1.0/24" ];
+                  description = ''
+                    List of target IP ranges, use empty list for any.
+                    May also contain host names instead of addresses.
+                    It's possible to use wildmask in the begginning and in the the end of hostname, e.g. *badsite.com or *badcontent*.
+                    Hostname is only checked if hostname presents in request.
+                  '';
+                };
+                targetPorts = mkOption {
+                  type = types.listOf types.int;
+                  default = [ ];
+                  example = [ 80 443 ];
+                  description = ''
+                    List of target ports, use empty list for any.
+                  '';
+                };
+              };
+            });
+            default = [ ];
+            example = literalExpression ''
+              [
+                {
+                  rule = "allow";
+                  users = [ "user1" ];
+                }
+                {
+                  rule = "allow";
+                  sources = [ "192.168.1.0/24" ];
+                }
+                {
+                  rule = "deny";
+                }
+              ]
+            '';
+            description = ''
+              Use this option to limit user access to resources.
+            '';
+          };
+          extraArguments = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "-46";
+            description = ''
+              Extra arguments for service.
+              Consult "Options" section in <link xlink:href="https://github.com/z3APA3A/3proxy/wiki/3proxy.cfg">documentation</link> for available arguments.
+            '';
+          };
+          extraConfig = mkOption {
+            type = types.nullOr types.lines;
+            default = null;
+            description = ''
+              Extra configuration for service. Use this to configure things like bandwidth limiter or ACL-based redirection.
+              Consult <link xlink:href="https://github.com/z3APA3A/3proxy/wiki/3proxy.cfg">documentation</link> for available options.
+            '';
+          };
+        };
+      });
+      default = [ ];
+      example = literalExpression ''
+        [
+          {
+            type = "proxy";
+            bindAddress = "192.168.1.24";
+            bindPort = 3128;
+            auth = [ "none" ];
+          }
+          {
+            type = "proxy";
+            bindAddress = "10.10.1.20";
+            bindPort = 3128;
+            auth = [ "iponly" ];
+          }
+          {
+            type = "socks";
+            bindAddress = "172.17.0.1";
+            bindPort = 1080;
+            auth = [ "strong" ];
+          }
+        ]
+      '';
+      description = ''
+        Use this option to define 3proxy services.
+      '';
+    };
+    denyPrivate = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to deny access to private IP ranges including loopback.
+      '';
+    };
+    privateRanges = mkOption {
+      type = types.listOf types.str;
+      default = [
+        "0.0.0.0/8"
+        "127.0.0.0/8"
+        "10.0.0.0/8"
+        "100.64.0.0/10"
+        "172.16.0.0/12"
+        "192.168.0.0/16"
+        "::"
+        "::1"
+        "fc00::/7"
+      ];
+      description = ''
+        What IP ranges to deny access when denyPrivate is set tu true.
+      '';
+    };
+    resolution = mkOption {
+      type = types.submodule {
+        options = {
+          nserver = mkOption {
+            type = types.listOf types.str;
+            default = [ ];
+            example = [ "127.0.0.53" "192.168.1.3:5353/tcp" ];
+            description = ''
+              List of nameservers to use.
+
+              Up to 5 nservers may be specified. If no nserver is configured,
+              default system name resolution functions are used.
+            '';
+          };
+          nscache = mkOption {
+            type = types.int;
+            default = 65535;
+            description = "Set name cache size for IPv4.";
+          };
+          nscache6 = mkOption {
+            type = types.int;
+            default = 65535;
+            description = "Set name cache size for IPv6.";
+          };
+          nsrecord = mkOption {
+            type = types.attrsOf types.str;
+            default = { };
+            example = literalExpression ''
+              {
+                "files.local" = "192.168.1.12";
+                "site.local" = "192.168.1.43";
+              }
+            '';
+            description = "Adds static nsrecords.";
+          };
+        };
+      };
+      default = { };
+      description = ''
+        Use this option to configure name resolution and DNS caching.
+      '';
+    };
+    extraConfig = mkOption {
+      type = types.nullOr types.lines;
+      default = null;
+      description = ''
+        Extra configuration, appended to the 3proxy configuration file.
+        Consult <link xlink:href="https://github.com/z3APA3A/3proxy/wiki/3proxy.cfg">documentation</link> for available options.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services._3proxy.confFile = mkDefault (pkgs.writeText "3proxy.conf" ''
+      # log to stdout
+      log
+
+      ${concatMapStringsSep "\n" (x: "nserver " + x) cfg.resolution.nserver}
+
+      nscache ${toString cfg.resolution.nscache}
+      nscache6 ${toString cfg.resolution.nscache6}
+
+      ${concatMapStringsSep "\n" (x: "nsrecord " + x)
+      (mapAttrsToList (name: value: "${name} ${value}")
+        cfg.resolution.nsrecord)}
+
+      ${optionalString (cfg.usersFile != null)
+        ''users $"${cfg.usersFile}"''
+      }
+
+      ${concatMapStringsSep "\n" (service: ''
+        auth ${concatStringsSep " " service.auth}
+
+        ${optionalString (cfg.denyPrivate)
+        "deny * * ${optionalList cfg.privateRanges}"}
+
+        ${concatMapStringsSep "\n" (acl:
+          "${acl.rule} ${
+            concatMapStringsSep " " optionalList [
+              acl.users
+              acl.sources
+              acl.targets
+              acl.targetPorts
+            ]
+          }") service.acl}
+
+        maxconn ${toString service.maxConnections}
+
+        ${optionalString (service.extraConfig != null) service.extraConfig}
+
+        ${service.type} -i${toString service.bindAddress} ${
+          optionalString (service.bindPort != null)
+          "-p${toString service.bindPort}"
+        } ${
+          optionalString (service.extraArguments != null) service.extraArguments
+        }
+
+        flush
+      '') cfg.services}
+      ${optionalString (cfg.extraConfig != null) cfg.extraConfig}
+    '');
+    systemd.services."3proxy" = {
+      description = "Tiny free proxy server";
+      documentation = [ "https://github.com/z3APA3A/3proxy/wiki" ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "3proxy";
+        ExecStart = "${pkg}/bin/3proxy ${cfg.confFile}";
+        Restart = "on-failure";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ misuzu ];
+}
diff --git a/nixos/modules/services/networking/adguardhome.nix b/nixos/modules/services/networking/adguardhome.nix
new file mode 100644
index 00000000000..98ddf071608
--- /dev/null
+++ b/nixos/modules/services/networking/adguardhome.nix
@@ -0,0 +1,140 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.adguardhome;
+
+  args = concatStringsSep " " ([
+    "--no-check-update"
+    "--pidfile /run/AdGuardHome/AdGuardHome.pid"
+    "--work-dir /var/lib/AdGuardHome/"
+    "--config /var/lib/AdGuardHome/AdGuardHome.yaml"
+  ] ++ cfg.extraArgs);
+
+  baseConfig = {
+    bind_host = cfg.host;
+    bind_port = cfg.port;
+  };
+
+  configFile = pkgs.writeTextFile {
+    name = "AdGuardHome.yaml";
+    text = builtins.toJSON (recursiveUpdate cfg.settings baseConfig);
+    checkPhase = "${pkgs.adguardhome}/bin/adguardhome -c $out --check-config";
+  };
+
+in {
+  options.services.adguardhome = with types; {
+    enable = mkEnableOption "AdGuard Home network-wide ad blocker";
+
+    host = mkOption {
+      default = "0.0.0.0";
+      type = str;
+      description = ''
+        Host address to bind HTTP server to.
+      '';
+    };
+
+    port = mkOption {
+      default = 3000;
+      type = port;
+      description = ''
+        Port to serve HTTP pages on.
+      '';
+    };
+
+    openFirewall = mkOption {
+      default = false;
+      type = bool;
+      description = ''
+        Open ports in the firewall for the AdGuard Home web interface. Does not
+        open the port needed to access the DNS resolver.
+      '';
+    };
+
+    mutableSettings = mkOption {
+      default = true;
+      type = bool;
+      description = ''
+        Allow changes made on the AdGuard Home web interface to persist between
+        service restarts.
+      '';
+    };
+
+    settings = mkOption {
+      type = (pkgs.formats.yaml { }).type;
+      default = { };
+      description = ''
+        AdGuard Home configuration. Refer to
+        <link xlink:href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#configuration-file"/>
+        for details on supported values.
+
+        <note><para>
+          On start and if <option>mutableSettings</option> is <literal>true</literal>,
+          these options are merged into the configuration file on start, taking
+          precedence over configuration changes made on the web interface.
+        </para></note>
+      '';
+    };
+
+    extraArgs = mkOption {
+      default = [ ];
+      type = listOf str;
+      description = ''
+        Extra command line parameters to be passed to the adguardhome binary.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.settings != { }
+          -> (hasAttrByPath [ "dns" "bind_host" ] cfg.settings)
+          || (hasAttrByPath [ "dns" "bind_hosts" ] cfg.settings);
+        message =
+          "AdGuard setting dns.bind_host or dns.bind_hosts needs to be configured for a minimal working configuration";
+      }
+      {
+        assertion = cfg.settings != { }
+          -> hasAttrByPath [ "dns" "bootstrap_dns" ] cfg.settings;
+        message =
+          "AdGuard setting dns.bootstrap_dns needs to be configured for a minimal working configuration";
+      }
+    ];
+
+    systemd.services.adguardhome = {
+      description = "AdGuard Home: Network-level blocker";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      unitConfig = {
+        StartLimitIntervalSec = 5;
+        StartLimitBurst = 10;
+      };
+
+      preStart = optionalString (cfg.settings != { }) ''
+        if    [ -e "$STATE_DIRECTORY/AdGuardHome.yaml" ] \
+           && [ "${toString cfg.mutableSettings}" = "1" ]; then
+          # Writing directly to AdGuardHome.yaml results in empty file
+          ${pkgs.yaml-merge}/bin/yaml-merge "$STATE_DIRECTORY/AdGuardHome.yaml" "${configFile}" > "$STATE_DIRECTORY/AdGuardHome.yaml.tmp"
+          mv "$STATE_DIRECTORY/AdGuardHome.yaml.tmp" "$STATE_DIRECTORY/AdGuardHome.yaml"
+        else
+          cp --force "${configFile}" "$STATE_DIRECTORY/AdGuardHome.yaml"
+          chmod 600 "$STATE_DIRECTORY/AdGuardHome.yaml"
+        fi
+      '';
+
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${pkgs.adguardhome}/bin/adguardhome ${args}";
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        Restart = "always";
+        RestartSec = 10;
+        RuntimeDirectory = "AdGuardHome";
+        StateDirectory = "AdGuardHome";
+      };
+    };
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+  };
+}
diff --git a/nixos/modules/services/networking/amuled.nix b/nixos/modules/services/networking/amuled.nix
new file mode 100644
index 00000000000..aa72a047526
--- /dev/null
+++ b/nixos/modules/services/networking/amuled.nix
@@ -0,0 +1,83 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.amule;
+  opt = options.services.amule;
+  user = if cfg.user != null then cfg.user else "amule";
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.amule = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run the AMule daemon. You need to manually run "amuled --ec-config" to configure the service for the first time.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/home/${user}/";
+        defaultText = literalExpression ''
+          "/home/''${config.${opt.user}}/"
+        '';
+        description = ''
+          The directory holding configuration, incoming and temporary files.
+        '';
+      };
+
+      user = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The user the AMule daemon should run as.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users = mkIf (cfg.user == null) [
+      { name = "amule";
+        description = "AMule daemon";
+        group = "amule";
+        uid = config.ids.uids.amule;
+      } ];
+
+    users.groups = mkIf (cfg.user == null) [
+      { name = "amule";
+        gid = config.ids.gids.amule;
+      } ];
+
+    systemd.services.amuled = {
+      description = "AMule daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      preStart = ''
+        mkdir -p ${cfg.dataDir}
+        chown ${user} ${cfg.dataDir}
+      '';
+
+      script = ''
+        ${pkgs.su}/bin/su -s ${pkgs.runtimeShell} ${user} \
+            -c 'HOME="${cfg.dataDir}" ${pkgs.amule-daemon}/bin/amuled'
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/antennas.nix b/nixos/modules/services/networking/antennas.nix
new file mode 100644
index 00000000000..ef98af22f20
--- /dev/null
+++ b/nixos/modules/services/networking/antennas.nix
@@ -0,0 +1,80 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.antennas;
+in
+
+{
+  options = {
+    services.antennas = {
+      enable = mkEnableOption "Antennas";
+
+      tvheadendUrl = mkOption {
+        type        = types.str;
+        default     = "http://localhost:9981";
+        description = "URL of Tvheadend.";
+      };
+
+      antennasUrl = mkOption {
+        type        = types.str;
+        default     = "http://127.0.0.1:5004";
+        description = "URL of Antennas.";
+      };
+
+      tunerCount = mkOption {
+        type        = types.int;
+        default     = 6;
+        description = "Numbers of tuners in tvheadend.";
+      };
+
+      deviceUUID = mkOption {
+        type        = types.str;
+        default     = "2f70c0d7-90a3-4429-8275-cbeeee9cd605";
+        description = "Device tuner UUID. Change this if you are running multiple instances.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.antennas = {
+      description = "Antennas HDHomeRun emulator for Tvheadend. ";
+      wantedBy    = [ "multi-user.target" ];
+
+      # Config
+      environment = {
+        TVHEADEND_URL = cfg.tvheadendUrl;
+        ANTENNAS_URL = cfg.antennasUrl;
+        TUNER_COUNT = toString cfg.tunerCount;
+        DEVICE_UUID = cfg.deviceUUID;
+      };
+
+      serviceConfig = {
+         ExecStart = "${pkgs.antennas}/bin/antennas";
+
+        # Antennas expects all resources like html and config to be relative to it's working directory
+        WorkingDirectory = "${pkgs.antennas}/libexec/antennas/deps/antennas/";
+
+        # Hardening
+        CapabilityBoundingSet = [ "" ];
+        DynamicUser = true;
+        LockPersonality = true;
+        ProcSubset = "pid";
+        PrivateDevices = true;
+        PrivateUsers = true;
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/aria2.nix b/nixos/modules/services/networking/aria2.nix
new file mode 100644
index 00000000000..156fef14479
--- /dev/null
+++ b/nixos/modules/services/networking/aria2.nix
@@ -0,0 +1,131 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.aria2;
+
+  homeDir = "/var/lib/aria2";
+
+  settingsDir = "${homeDir}";
+  sessionFile = "${homeDir}/aria2.session";
+  downloadDir = "${homeDir}/Downloads";
+
+  rangesToStringList = map (x: builtins.toString x.from +"-"+ builtins.toString x.to);
+
+  settingsFile = pkgs.writeText "aria2.conf"
+  ''
+    dir=${cfg.downloadDir}
+    listen-port=${concatStringsSep "," (rangesToStringList cfg.listenPortRange)}
+    rpc-listen-port=${toString cfg.rpcListenPort}
+    rpc-secret=${cfg.rpcSecret}
+  '';
+
+in
+{
+  options = {
+    services.aria2 = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether or not to enable the headless Aria2 daemon service.
+
+          Aria2 daemon can be controlled via the RPC interface using
+          one of many WebUI (http://localhost:6800/ by default).
+
+          Targets are downloaded to ${downloadDir} by default and are
+          accessible to users in the "aria2" group.
+        '';
+      };
+      openPorts = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open listen and RPC ports found in listenPortRange and rpcListenPort
+          options in the firewall.
+        '';
+      };
+      downloadDir = mkOption {
+        type = types.path;
+        default = downloadDir;
+        description = ''
+          Directory to store downloaded files.
+        '';
+      };
+      listenPortRange = mkOption {
+        type = types.listOf types.attrs;
+        default = [ { from = 6881; to = 6999; } ];
+        description = ''
+          Set UDP listening port range used by DHT(IPv4, IPv6) and UDP tracker.
+        '';
+      };
+      rpcListenPort = mkOption {
+        type = types.int;
+        default = 6800;
+        description = "Specify a port number for JSON-RPC/XML-RPC server to listen to. Possible Values: 1024-65535";
+      };
+      rpcSecret = mkOption {
+        type = types.str;
+        default = "aria2rpc";
+        description = ''
+          Set RPC secret authorization token.
+          Read https://aria2.github.io/manual/en/html/aria2c.html#rpc-auth to know how this option value is used.
+        '';
+      };
+      extraArguments = mkOption {
+        type = types.separatedString " ";
+        example = "--rpc-listen-all --remote-time=true";
+        default = "";
+        description = ''
+          Additional arguments to be passed to Aria2.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    # Need to open ports for proper functioning
+    networking.firewall = mkIf cfg.openPorts {
+      allowedUDPPortRanges = config.services.aria2.listenPortRange;
+      allowedTCPPorts = [ config.services.aria2.rpcListenPort ];
+    };
+
+    users.users.aria2 = {
+      group = "aria2";
+      uid = config.ids.uids.aria2;
+      description = "aria2 user";
+      home = homeDir;
+      createHome = false;
+    };
+
+    users.groups.aria2.gid = config.ids.gids.aria2;
+
+    systemd.tmpfiles.rules = [
+      "d '${homeDir}' 0770 aria2 aria2 - -"
+      "d '${config.services.aria2.downloadDir}' 0770 aria2 aria2 - -"
+    ];
+
+    systemd.services.aria2 = {
+      description = "aria2 Service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        if [[ ! -e "${sessionFile}" ]]
+        then
+          touch "${sessionFile}"
+        fi
+        cp -f "${settingsFile}" "${settingsDir}/aria2.conf"
+      '';
+
+      serviceConfig = {
+        Restart = "on-abort";
+        ExecStart = "${pkgs.aria2}/bin/aria2c --enable-rpc --conf-path=${settingsDir}/aria2.conf ${config.services.aria2.extraArguments} --save-session=${sessionFile}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        User = "aria2";
+        Group = "aria2";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/asterisk.nix b/nixos/modules/services/networking/asterisk.nix
new file mode 100644
index 00000000000..af091d55c01
--- /dev/null
+++ b/nixos/modules/services/networking/asterisk.nix
@@ -0,0 +1,264 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.asterisk;
+
+  asteriskUser = "asterisk";
+  asteriskGroup = "asterisk";
+
+  varlibdir = "/var/lib/asterisk";
+  spooldir = "/var/spool/asterisk";
+  logdir = "/var/log/asterisk";
+
+  # Add filecontents from files of useTheseDefaultConfFiles to confFiles, do not override
+  defaultConfFiles = subtractLists (attrNames cfg.confFiles) cfg.useTheseDefaultConfFiles;
+  allConfFiles =
+    cfg.confFiles //
+    builtins.listToAttrs (map (x: { name = x;
+                                    value = builtins.readFile (cfg.package + "/etc/asterisk/" + x); })
+                              defaultConfFiles);
+
+  asteriskEtc = pkgs.stdenv.mkDerivation
+  ((mapAttrs' (name: value: nameValuePair
+        # Fudge the names to make bash happy
+        ((replaceChars ["."] ["_"] name) + "_")
+        (value)
+      ) allConfFiles) //
+  {
+    confFilesString = concatStringsSep " " (
+      attrNames allConfFiles
+    );
+
+    name = "asterisk-etc";
+
+    # Default asterisk.conf file
+    # (Notice that astetcdir will be set to the path of this derivation)
+    asteriskConf = ''
+      [directories]
+      astetcdir => /etc/asterisk
+      astmoddir => ${cfg.package}/lib/asterisk/modules
+      astvarlibdir => /var/lib/asterisk
+      astdbdir => /var/lib/asterisk
+      astkeydir => /var/lib/asterisk
+      astdatadir => /var/lib/asterisk
+      astagidir => /var/lib/asterisk/agi-bin
+      astspooldir => /var/spool/asterisk
+      astrundir => /run/asterisk
+      astlogdir => /var/log/asterisk
+      astsbindir => ${cfg.package}/sbin
+    '';
+    extraConf = cfg.extraConfig;
+
+    # Loading all modules by default is considered sensible by the authors of
+    # "Asterisk: The Definitive Guide". Secure sites will likely want to
+    # specify their own "modules.conf" in the confFiles option.
+    modulesConf = ''
+      [modules]
+      autoload=yes
+    '';
+
+    # Use syslog for logging so logs can be viewed with journalctl
+    loggerConf = ''
+      [general]
+
+      [logfiles]
+      syslog.local0 => notice,warning,error
+    '';
+
+    buildCommand = ''
+      mkdir -p "$out"
+
+      # Create asterisk.conf, pointing astetcdir to the path of this derivation
+      echo "$asteriskConf" | sed "s|@out@|$out|g" > "$out"/asterisk.conf
+      echo "$extraConf" >> "$out"/asterisk.conf
+
+      echo "$modulesConf" > "$out"/modules.conf
+
+      echo "$loggerConf" > "$out"/logger.conf
+
+      # Config files specified in confFiles option override all other files
+      for i in $confFilesString; do
+        conf=$(echo "$i"_ | sed 's/\./_/g')
+        echo "''${!conf}" > "$out"/"$i"
+      done
+    '';
+  });
+in
+
+{
+  options = {
+    services.asterisk = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the Asterisk PBX server.
+        '';
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        type = types.lines;
+        example = ''
+          [options]
+          verbose=3
+          debug=3
+        '';
+        description = ''
+          Extra configuration options appended to the default
+          <literal>asterisk.conf</literal> file.
+        '';
+      };
+
+      confFiles = mkOption {
+        default = {};
+        type = types.attrsOf types.str;
+        example = literalExpression
+          ''
+            {
+              "extensions.conf" = '''
+                [tests]
+                ; Dial 100 for "hello, world"
+                exten => 100,1,Answer()
+                same  =>     n,Wait(1)
+                same  =>     n,Playback(hello-world)
+                same  =>     n,Hangup()
+
+                [softphones]
+                include => tests
+
+                [unauthorized]
+              ''';
+              "sip.conf" = '''
+                [general]
+                allowguest=no              ; Require authentication
+                context=unauthorized       ; Send unauthorized users to /dev/null
+                srvlookup=no               ; Don't do DNS lookup
+                udpbindaddr=0.0.0.0        ; Listen on all interfaces
+                nat=force_rport,comedia    ; Assume device is behind NAT
+
+                [softphone](!)
+                type=friend                ; Match on username first, IP second
+                context=softphones         ; Send to softphones context in
+                                           ; extensions.conf file
+                host=dynamic               ; Device will register with asterisk
+                disallow=all               ; Manually specify codecs to allow
+                allow=g722
+                allow=ulaw
+                allow=alaw
+
+                [myphone](softphone)
+                secret=GhoshevFew          ; Change this password!
+              ''';
+              "logger.conf" = '''
+                [general]
+
+                [logfiles]
+                ; Add debug output to log
+                syslog.local0 => notice,warning,error,debug
+              ''';
+            }
+        '';
+        description = ''
+          Sets the content of config files (typically ending with
+          <literal>.conf</literal>) in the Asterisk configuration directory.
+
+          Note that if you want to change <literal>asterisk.conf</literal>, it
+          is preferable to use the <option>services.asterisk.extraConfig</option>
+          option over this option. If <literal>"asterisk.conf"</literal> is
+          specified with the <option>confFiles</option> option (not recommended),
+          you must be prepared to set your own <literal>astetcdir</literal>
+          path.
+
+          See
+          <link xlink:href="http://www.asterisk.org/community/documentation"/>
+          for more examples of what is possible here.
+        '';
+      };
+
+      useTheseDefaultConfFiles = mkOption {
+        default = [ "ari.conf" "acl.conf" "agents.conf" "amd.conf" "calendar.conf" "cdr.conf" "cdr_syslog.conf" "cdr_custom.conf" "cel.conf" "cel_custom.conf" "cli_aliases.conf" "confbridge.conf" "dundi.conf" "features.conf" "hep.conf" "iax.conf" "pjsip.conf" "pjsip_wizard.conf" "phone.conf" "phoneprov.conf" "queues.conf" "res_config_sqlite3.conf" "res_parking.conf" "statsd.conf" "udptl.conf" "unistim.conf" ];
+        type = types.listOf types.str;
+        example = [ "sip.conf" "dundi.conf" ];
+        description = ''Sets these config files to the default content. The default value for
+          this option contains all necesscary files to avoid errors at startup.
+          This does not override settings via <option>services.asterisk.confFiles</option>.
+        '';
+      };
+
+      extraArguments = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        example =
+          [ "-vvvddd" "-e" "1024" ];
+        description = ''
+          Additional command line arguments to pass to Asterisk.
+        '';
+      };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.asterisk;
+        defaultText = literalExpression "pkgs.asterisk";
+        description = "The Asterisk package to use.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    environment.etc.asterisk.source = asteriskEtc;
+
+    users.users.asterisk =
+      { name = asteriskUser;
+        group = asteriskGroup;
+        uid = config.ids.uids.asterisk;
+        description = "Asterisk daemon user";
+        home = varlibdir;
+      };
+
+    users.groups.asterisk =
+      { name = asteriskGroup;
+        gid = config.ids.gids.asterisk;
+      };
+
+    systemd.services.asterisk = {
+      description = ''
+        Asterisk PBX server
+      '';
+
+      wantedBy = [ "multi-user.target" ];
+
+      # Do not restart, to avoid disruption of running calls. Restart unit by yourself!
+      restartIfChanged = false;
+
+      preStart = ''
+        # Copy skeleton directory tree to /var
+        for d in '${varlibdir}' '${spooldir}' '${logdir}'; do
+          # TODO: Make exceptions for /var directories that likely should be updated
+          if [ ! -e "$d" ]; then
+            mkdir -p "$d"
+            cp --recursive ${cfg.package}/"$d"/* "$d"/
+            chown --recursive ${asteriskUser}:${asteriskGroup} "$d"
+            find "$d" -type d | xargs chmod 0755
+          fi
+        done
+      '';
+
+      serviceConfig = {
+        ExecStart =
+          let
+            # FIXME: This doesn't account for arguments with spaces
+            argString = concatStringsSep " " cfg.extraArguments;
+          in
+          "${cfg.package}/bin/asterisk -U ${asteriskUser} -C /etc/asterisk/asterisk.conf ${argString} -F";
+        ExecReload = ''${cfg.package}/bin/asterisk -x "core reload"
+          '';
+        Type = "forking";
+        PIDFile = "/run/asterisk/asterisk.pid";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/atftpd.nix b/nixos/modules/services/networking/atftpd.nix
new file mode 100644
index 00000000000..da5e305201f
--- /dev/null
+++ b/nixos/modules/services/networking/atftpd.nix
@@ -0,0 +1,65 @@
+# NixOS module for atftpd TFTP server
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.atftpd;
+
+in
+
+{
+
+  options = {
+
+    services.atftpd = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to enable the atftpd TFTP server. By default, the server
+          binds to address 0.0.0.0.
+        '';
+      };
+
+      extraOptions = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        example = literalExpression ''
+          [ "--bind-address 192.168.9.1"
+            "--verbose=7"
+          ]
+        '';
+        description = ''
+          Extra command line arguments to pass to atftp.
+        '';
+      };
+
+      root = mkOption {
+        default = "/srv/tftp";
+        type = types.path;
+        description = ''
+          Document root directory for the atftpd.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.atftpd = {
+      description = "TFTP Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      # runs as nobody
+      serviceConfig.ExecStart = "${pkgs.atftp}/sbin/atftpd --daemon --no-fork ${lib.concatStringsSep " " cfg.extraOptions} ${cfg.root}";
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/autossh.nix b/nixos/modules/services/networking/autossh.nix
new file mode 100644
index 00000000000..245f2bfc2cf
--- /dev/null
+++ b/nixos/modules/services/networking/autossh.nix
@@ -0,0 +1,113 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.autossh;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.autossh = {
+
+      sessions = mkOption {
+        type = types.listOf (types.submodule {
+          options = {
+            name = mkOption {
+              type = types.str;
+              example = "socks-peer";
+              description = "Name of the local AutoSSH session";
+            };
+            user = mkOption {
+              type = types.str;
+              example = "bill";
+              description = "Name of the user the AutoSSH session should run as";
+            };
+            monitoringPort = mkOption {
+              type = types.int;
+              default = 0;
+              example = 20000;
+              description = ''
+                Port to be used by AutoSSH for peer monitoring. Note, that
+                AutoSSH also uses mport+1. Value of 0 disables the keep-alive
+                style monitoring
+              '';
+            };
+            extraArguments = mkOption {
+              type = types.separatedString " ";
+              example = "-N -D4343 bill@socks.example.net";
+              description = ''
+                Arguments to be passed to AutoSSH and retransmitted to SSH
+                process. Some meaningful options include -N (don't run remote
+                command), -D (open SOCKS proxy on local port), -R (forward
+                remote port), -L (forward local port), -v (Enable debug). Check
+                ssh manual for the complete list.
+              '';
+            };
+          };
+        });
+
+        default = [];
+        description = ''
+          List of AutoSSH sessions to start as systemd services. Each service is
+          named 'autossh-{session.name}'.
+        '';
+
+        example = [
+          {
+            name="socks-peer";
+            user="bill";
+            monitoringPort = 20000;
+            extraArguments="-N -D4343 billremote@socks.host.net";
+          }
+        ];
+
+      };
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf (cfg.sessions != []) {
+
+    systemd.services =
+
+      lib.foldr ( s : acc : acc //
+        {
+          "autossh-${s.name}" =
+            let
+              mport = if s ? monitoringPort then s.monitoringPort else 0;
+            in
+            {
+              description = "AutoSSH session (" + s.name + ")";
+
+              after = [ "network.target" ];
+              wantedBy = [ "multi-user.target" ];
+
+              # To be able to start the service with no network connection
+              environment.AUTOSSH_GATETIME="0";
+
+              # How often AutoSSH checks the network, in seconds
+              environment.AUTOSSH_POLL="30";
+
+              serviceConfig = {
+                  User = "${s.user}";
+                  # AutoSSH may exit with 0 code if the SSH session was
+                  # gracefully terminated by either local or remote side.
+                  Restart = "on-success";
+                  ExecStart = "${pkgs.autossh}/bin/autossh -M ${toString mport} ${s.extraArguments}";
+              };
+            };
+        }) {} cfg.sessions;
+
+    environment.systemPackages = [ pkgs.autossh ];
+
+  };
+}
diff --git a/nixos/modules/services/networking/avahi-daemon.nix b/nixos/modules/services/networking/avahi-daemon.nix
new file mode 100644
index 00000000000..50c4ffdedce
--- /dev/null
+++ b/nixos/modules/services/networking/avahi-daemon.nix
@@ -0,0 +1,286 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.avahi;
+
+  yesNo = yes : if yes then "yes" else "no";
+
+  avahiDaemonConf = with cfg; pkgs.writeText "avahi-daemon.conf" ''
+    [server]
+    ${# Users can set `networking.hostName' to the empty string, when getting
+      # a host name from DHCP.  In that case, let Avahi take whatever the
+      # current host name is; setting `host-name' to the empty string in
+      # `avahi-daemon.conf' would be invalid.
+      optionalString (hostName != "") "host-name=${hostName}"}
+    browse-domains=${concatStringsSep ", " browseDomains}
+    use-ipv4=${yesNo ipv4}
+    use-ipv6=${yesNo ipv6}
+    ${optionalString (interfaces!=null) "allow-interfaces=${concatStringsSep "," interfaces}"}
+    ${optionalString (domainName!=null) "domain-name=${domainName}"}
+    allow-point-to-point=${yesNo allowPointToPoint}
+    ${optionalString (cacheEntriesMax!=null) "cache-entries-max=${toString cacheEntriesMax}"}
+
+    [wide-area]
+    enable-wide-area=${yesNo wideArea}
+
+    [publish]
+    disable-publishing=${yesNo (!publish.enable)}
+    disable-user-service-publishing=${yesNo (!publish.userServices)}
+    publish-addresses=${yesNo (publish.userServices || publish.addresses)}
+    publish-hinfo=${yesNo publish.hinfo}
+    publish-workstation=${yesNo publish.workstation}
+    publish-domain=${yesNo publish.domain}
+
+    [reflector]
+    enable-reflector=${yesNo reflector}
+    ${extraConfig}
+  '';
+in
+{
+  options.services.avahi = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        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').
+      '';
+    };
+
+    hostName = mkOption {
+      type = types.str;
+      default = config.networking.hostName;
+      defaultText = literalExpression "config.networking.hostName";
+      description = ''
+        Host name advertised on the LAN. If not set, avahi will use the value
+        of <option>config.networking.hostName</option>.
+      '';
+    };
+
+    domainName = mkOption {
+      type = types.str;
+      default = "local";
+      description = ''
+        Domain name for all advertisements.
+      '';
+    };
+
+    browseDomains = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = [ "0pointer.de" "zeroconf.org" ];
+      description = ''
+        List of non-local DNS domains to be browsed.
+      '';
+    };
+
+    ipv4 = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Whether to use IPv4.";
+    };
+
+    ipv6 = mkOption {
+      type = types.bool;
+      default = config.networking.enableIPv6;
+      defaultText = literalExpression "config.networking.enableIPv6";
+      description = "Whether to use IPv6.";
+    };
+
+    interfaces = mkOption {
+      type = types.nullOr (types.listOf types.str);
+      default = null;
+      description = ''
+        List of network interfaces that should be used by the <command>avahi-daemon</command>.
+        Other interfaces will be ignored. If <literal>null</literal>, all local interfaces
+        except loopback and point-to-point will be used.
+      '';
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to open the firewall for UDP port 5353.
+      '';
+    };
+
+    allowPointToPoint = mkOption {
+      type = types.bool;
+      default = false;
+      description= ''
+        Whether to use POINTTOPOINT interfaces. Might make mDNS unreliable due to usually large
+        latencies with such links and opens a potential security hole by allowing mDNS access from Internet
+        connections.
+      '';
+    };
+
+    wideArea = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Whether to enable wide-area service discovery.";
+    };
+
+    reflector = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Reflect incoming mDNS requests to all allowed network interfaces.";
+    };
+
+    extraServiceFiles = mkOption {
+      type = with types; attrsOf (either str path);
+      default = {};
+      example = literalExpression ''
+        {
+          ssh = "''${pkgs.avahi}/etc/avahi/services/ssh.service";
+          smb = '''
+            <?xml version="1.0" standalone='no'?><!--*-nxml-*-->
+            <!DOCTYPE service-group SYSTEM "avahi-service.dtd">
+            <service-group>
+              <name replace-wildcards="yes">%h</name>
+              <service>
+                <type>_smb._tcp</type>
+                <port>445</port>
+              </service>
+            </service-group>
+          ''';
+        }
+      '';
+      description = ''
+        Specify custom service definitions which are placed in the avahi service directory.
+        See the <citerefentry><refentrytitle>avahi.service</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> manpage for detailed information.
+      '';
+    };
+
+    publish = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to allow publishing in general.";
+      };
+
+      userServices = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to publish user services. Will set <literal>addresses=true</literal>.";
+      };
+
+      addresses = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to register mDNS address records for all local IP addresses.";
+      };
+
+      hinfo = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to register a mDNS HINFO record which contains information about the
+          local operating system and CPU.
+        '';
+      };
+
+      workstation = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to register a service of type "_workstation._tcp" on the local LAN.
+        '';
+      };
+
+      domain = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to announce the locally used domain name for browsing by other hosts.";
+      };
+    };
+
+    nssmdns = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable the mDNS NSS (Name Service Switch) plug-in.
+        Enabling it allows applications to resolve names in the `.local'
+        domain by transparently querying the Avahi daemon.
+      '';
+    };
+
+    cacheEntriesMax = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      description = ''
+        Number of resource records to be cached per interface. Use 0 to
+        disable caching. Avahi daemon defaults to 4096 if not set.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Extra config to append to avahi-daemon.conf.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.avahi = {
+      description = "avahi-daemon privilege separation user";
+      home = "/var/empty";
+      group = "avahi";
+      isSystemUser = true;
+    };
+
+    users.groups.avahi = {};
+
+    system.nssModules = optional cfg.nssmdns pkgs.nssmdns;
+    system.nssDatabases.hosts = optionals cfg.nssmdns (mkMerge [
+      (mkBefore [ "mdns_minimal [NOTFOUND=return]" ]) # before resolve
+      (mkAfter [ "mdns" ]) # after dns
+    ]);
+
+    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);
+
+    systemd.sockets.avahi-daemon = {
+      description = "Avahi mDNS/DNS-SD Stack Activation Socket";
+      listenStreams = [ "/run/avahi-daemon/socket" ];
+      wantedBy = [ "sockets.target" ];
+    };
+
+    systemd.tmpfiles.rules = [ "d /run/avahi-daemon - avahi avahi -" ];
+
+    systemd.services.avahi-daemon = {
+      description = "Avahi mDNS/DNS-SD Stack";
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "avahi-daemon.socket" ];
+
+      # Make NSS modules visible so that `avahi_nss_support ()' can
+      # return a sensible value.
+      environment.LD_LIBRARY_PATH = config.system.nssModules.path;
+
+      path = [ pkgs.coreutils pkgs.avahi ];
+
+      serviceConfig = {
+        NotifyAccess = "main";
+        BusName = "org.freedesktop.Avahi";
+        Type = "dbus";
+        ExecStart = "${pkgs.avahi}/sbin/avahi-daemon --syslog -f ${avahiDaemonConf}";
+      };
+    };
+
+    services.dbus.enable = true;
+    services.dbus.packages = [ pkgs.avahi ];
+
+    networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ 5353 ];
+  };
+}
diff --git a/nixos/modules/services/networking/babeld.nix b/nixos/modules/services/networking/babeld.nix
new file mode 100644
index 00000000000..aae6f1498a4
--- /dev/null
+++ b/nixos/modules/services/networking/babeld.nix
@@ -0,0 +1,144 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.babeld;
+
+  conditionalBoolToString = value: if (isBool value) then (boolToString value) else (toString value);
+
+  paramsString = params:
+    concatMapStringsSep " " (name: "${name} ${conditionalBoolToString (getAttr name params)}")
+                   (attrNames params);
+
+  interfaceConfig = name:
+    let
+      interface = getAttr name cfg.interfaces;
+    in
+    "interface ${name} ${paramsString interface}\n";
+
+  configFile = with cfg; pkgs.writeText "babeld.conf" (
+    ''
+      skip-kernel-setup true
+    ''
+    + (optionalString (cfg.interfaceDefaults != null) ''
+      default ${paramsString cfg.interfaceDefaults}
+    '')
+    + (concatMapStrings interfaceConfig (attrNames cfg.interfaces))
+    + extraConfig);
+
+in
+
+{
+
+  meta.maintainers = with maintainers; [ hexa ];
+
+  ###### interface
+
+  options = {
+
+    services.babeld = {
+
+      enable = mkEnableOption "the babeld network routing daemon";
+
+      interfaceDefaults = mkOption {
+        default = null;
+        description = ''
+          A set describing default parameters for babeld interfaces.
+          See <citerefentry><refentrytitle>babeld</refentrytitle><manvolnum>8</manvolnum></citerefentry> for options.
+        '';
+        type = types.nullOr (types.attrsOf types.unspecified);
+        example =
+          {
+            type = "tunnel";
+            split-horizon = true;
+          };
+      };
+
+      interfaces = mkOption {
+        default = {};
+        description = ''
+          A set describing babeld interfaces.
+          See <citerefentry><refentrytitle>babeld</refentrytitle><manvolnum>8</manvolnum></citerefentry> for options.
+        '';
+        type = types.attrsOf (types.attrsOf types.unspecified);
+        example =
+          { enp0s2 =
+            { type = "wired";
+              hello-interval = 5;
+              split-horizon = "auto";
+            };
+          };
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Options that will be copied to babeld.conf.
+          See <citerefentry><refentrytitle>babeld</refentrytitle><manvolnum>8</manvolnum></citerefentry> for details.
+        '';
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.babeld.enable {
+
+    boot.kernel.sysctl = {
+      "net.ipv6.conf.all.forwarding" = 1;
+      "net.ipv6.conf.all.accept_redirects" = 0;
+      "net.ipv4.conf.all.forwarding" = 1;
+      "net.ipv4.conf.all.rp_filter" = 0;
+    } // lib.mapAttrs' (ifname: _: lib.nameValuePair "net.ipv4.conf.${ifname}.rp_filter" (lib.mkDefault 0)) config.services.babeld.interfaces;
+
+    systemd.services.babeld = {
+      description = "Babel routing daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.babeld}/bin/babeld -c ${configFile} -I /run/babeld/babeld.pid -S /var/lib/babeld/state";
+        AmbientCapabilities = [ "CAP_NET_ADMIN" ];
+        CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
+        DevicePolicy = "closed";
+        DynamicUser = true;
+        IPAddressAllow = [ "fe80::/64" "ff00::/8" "::1/128" "127.0.0.0/8" ];
+        IPAddressDeny = "any";
+        LockPersonality = true;
+        NoNewPrivileges = true;
+        MemoryDenyWriteExecute = true;
+        ProtectSystem = "strict";
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_NETLINK" "AF_INET6" "AF_INET" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectProc = "invisible";
+        PrivateMounts = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = false; # kernel_route(ADD): Operation not permitted
+        ProcSubset = "pid";
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged @resources"
+        ];
+        UMask = "0177";
+        RuntimeDirectory = "babeld";
+        StateDirectory = "babeld";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/bee-clef.nix b/nixos/modules/services/networking/bee-clef.nix
new file mode 100644
index 00000000000..719714b2898
--- /dev/null
+++ b/nixos/modules/services/networking/bee-clef.nix
@@ -0,0 +1,107 @@
+{ config, lib, pkgs, ... }:
+
+# NOTE for now nothing is installed into /etc/bee-clef/. the config files are used as read-only from the nix store.
+
+with lib;
+let
+  cfg = config.services.bee-clef;
+in {
+  meta = {
+    maintainers = with maintainers; [ attila-lendvai ];
+  };
+
+  ### interface
+
+  options = {
+    services.bee-clef = {
+      enable = mkEnableOption "clef external signer instance for Ethereum Swarm Bee";
+
+      dataDir = mkOption {
+        type = types.nullOr types.str;
+        default = "/var/lib/bee-clef";
+        description = ''
+          Data dir for bee-clef. Beware that some helper scripts may not work when changed!
+          The service itself should work fine, though.
+        '';
+      };
+
+      passwordFile = mkOption {
+        type = types.nullOr types.str;
+        default = "/var/lib/bee-clef/password";
+        description = "Password file for bee-clef.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "bee-clef";
+        description = ''
+          User the bee-clef daemon should execute under.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "bee-clef";
+        description = ''
+          Group the bee-clef daemon should execute under.
+        '';
+      };
+    };
+  };
+
+  ### implementation
+
+  config = mkIf cfg.enable {
+    # if we ever want to have rules.js under /etc/bee-clef/
+    # environment.etc."bee-clef/rules.js".source = ${pkgs.bee-clef}/rules.js
+
+    systemd.packages = [ pkgs.bee-clef ]; # include the upstream bee-clef.service file
+
+    systemd.tmpfiles.rules = [
+        "d '${cfg.dataDir}/'         0750 ${cfg.user} ${cfg.group}"
+        "d '${cfg.dataDir}/keystore' 0700 ${cfg.user} ${cfg.group}"
+      ];
+
+    systemd.services.bee-clef = {
+      path = [
+        # these are needed for the ensure-clef-account script
+        pkgs.coreutils
+        pkgs.gnused
+        pkgs.gawk
+      ];
+
+      wantedBy = [ "bee.service" "multi-user.target" ];
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStartPre = ''${pkgs.bee-clef}/share/bee-clef/ensure-clef-account "${cfg.dataDir}" "${pkgs.bee-clef}/share/bee-clef/"'';
+        ExecStart = [
+          "" # this hides/overrides what's in the original entry
+          "${pkgs.bee-clef}/share/bee-clef/bee-clef-service start"
+        ];
+        ExecStop = [
+          "" # this hides/overrides what's in the original entry
+          "${pkgs.bee-clef}/share/bee-clef/bee-clef-service stop"
+        ];
+        Environment = [
+          "CONFIGDIR=${cfg.dataDir}"
+          "PASSWORD_FILE=${cfg.passwordFile}"
+        ];
+      };
+    };
+
+    users.users = optionalAttrs (cfg.user == "bee-clef") {
+      bee-clef = {
+        group = cfg.group;
+        home = cfg.dataDir;
+        isSystemUser = true;
+        description = "Daemon user for the bee-clef service";
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "bee-clef") {
+      bee-clef = {};
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/bee.nix b/nixos/modules/services/networking/bee.nix
new file mode 100644
index 00000000000..d6efade0630
--- /dev/null
+++ b/nixos/modules/services/networking/bee.nix
@@ -0,0 +1,149 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.bee;
+  format = pkgs.formats.yaml {};
+  configFile = format.generate "bee.yaml" cfg.settings;
+in {
+  meta = {
+    # doc = ./bee.xml;
+    maintainers = with maintainers; [ attila-lendvai ];
+  };
+
+  ### interface
+
+  options = {
+    services.bee = {
+      enable = mkEnableOption "Ethereum Swarm Bee";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.bee;
+        defaultText = literalExpression "pkgs.bee";
+        example = literalExpression "pkgs.bee-unstable";
+        description = "The package providing the bee binary for the service.";
+      };
+
+      settings = mkOption {
+        type = format.type;
+        description = ''
+          Ethereum Swarm Bee configuration. Refer to
+          <link xlink:href="https://gateway.ethswarm.org/bzz/docs.swarm.eth/docs/installation/configuration/"/>
+          for details on supported values.
+        '';
+      };
+
+      daemonNiceLevel = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          Daemon process priority for bee.
+          0 is the default Unix process priority, 19 is the lowest.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "bee";
+        description = ''
+          User the bee binary should execute under.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "bee";
+        description = ''
+          Group the bee binary should execute under.
+        '';
+      };
+    };
+  };
+
+  ### implementation
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = (hasAttr "password" cfg.settings) != true;
+        message = ''
+          `services.bee.settings.password` is insecure. Use `services.bee.settings.password-file` or `systemd.services.bee.serviceConfig.EnvironmentFile` instead.
+        '';
+      }
+      { assertion = (hasAttr "swap-endpoint" cfg.settings) || (cfg.settings.swap-enable or true == false);
+        message = ''
+          In a swap-enabled network a working Ethereum blockchain node is required. You must specify one using `services.bee.settings.swap-endpoint`, or disable `services.bee.settings.swap-enable` = false.
+        '';
+      }
+    ];
+
+    warnings = optional (! config.services.bee-clef.enable) "The bee service requires an external signer. Consider setting `config.services.bee-clef.enable` = true";
+
+    services.bee.settings = {
+      data-dir             = lib.mkDefault "/var/lib/bee";
+      password-file        = lib.mkDefault "/var/lib/bee/password";
+      clef-signer-enable   = lib.mkDefault true;
+      clef-signer-endpoint = lib.mkDefault "/var/lib/bee-clef/clef.ipc";
+      swap-endpoint        = lib.mkDefault "https://rpc.slock.it/goerli";
+    };
+
+    systemd.packages = [ cfg.package ]; # include the upstream bee.service file
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.settings.data-dir}' 0750 ${cfg.user} ${cfg.group}"
+    ];
+
+    systemd.services.bee = {
+      requires = optional config.services.bee-clef.enable
+        "bee-clef.service";
+
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Nice = cfg.daemonNiceLevel;
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = [
+          "" # this hides/overrides what's in the original entry
+          "${cfg.package}/bin/bee --config=${configFile} start"
+        ];
+      };
+
+      preStart = with cfg.settings; ''
+        if ! test -f ${password-file}; then
+          < /dev/urandom tr -dc _A-Z-a-z-0-9 2> /dev/null | head -c32 > ${password-file}
+          chmod 0600 ${password-file}
+          echo "Initialized ${password-file} from /dev/urandom"
+        fi
+        if [ ! -f ${data-dir}/keys/libp2p.key ]; then
+          ${cfg.package}/bin/bee init --config=${configFile} >/dev/null
+          echo "
+Logs:   journalctl -f -u bee.service
+
+Bee has SWAP enabled by default and it needs ethereum endpoint to operate.
+It is recommended to use external signer with bee.
+Check documentation for more info:
+- SWAP https://docs.ethswarm.org/docs/installation/manual#swap-bandwidth-incentives
+- External signer https://docs.ethswarm.org/docs/installation/bee-clef
+
+After you finish configuration run 'sudo bee-get-addr'."
+        fi
+      '';
+    };
+
+    users.users = optionalAttrs (cfg.user == "bee") {
+      bee = {
+        group = cfg.group;
+        home = cfg.settings.data-dir;
+        isSystemUser = true;
+        description = "Daemon user for Ethereum Swarm Bee";
+        extraGroups = optional config.services.bee-clef.enable
+          config.services.bee-clef.group;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "bee") {
+      bee = {};
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/biboumi.nix b/nixos/modules/services/networking/biboumi.nix
new file mode 100644
index 00000000000..3f46b95eaf0
--- /dev/null
+++ b/nixos/modules/services/networking/biboumi.nix
@@ -0,0 +1,270 @@
+{ config, lib, pkgs, options, ... }:
+with lib;
+let
+  cfg = config.services.biboumi;
+  inherit (config.environment) etc;
+  rootDir = "/run/biboumi/mnt-root";
+  stateDir = "/var/lib/biboumi";
+  settingsFile = pkgs.writeText "biboumi.cfg" (
+    generators.toKeyValue {
+      mkKeyValue = k: v:
+        if v == null then ""
+        else generators.mkKeyValueDefault {} "=" k v;
+    } cfg.settings);
+  need_CAP_NET_BIND_SERVICE = cfg.settings.identd_port != 0 && cfg.settings.identd_port < 1024;
+in
+{
+  options = {
+    services.biboumi = {
+      enable = mkEnableOption "the Biboumi XMPP gateway to IRC";
+
+      settings = mkOption {
+        description = ''
+          See <link xlink:href="https://lab.louiz.org/louiz/biboumi/blob/8.5/doc/biboumi.1.rst">biboumi 8.5</link>
+          for documentation.
+        '';
+        default = {};
+        type = types.submodule {
+          freeformType = with types;
+            (attrsOf (nullOr (oneOf [str int bool]))) // {
+              description = "settings option";
+            };
+          options.admin = mkOption {
+            type = with types; listOf str;
+            default = [];
+            example = ["admin@example.org"];
+            apply = concatStringsSep ":";
+            description = ''
+              The bare JID of the gateway administrator. This JID will have more
+              privileges than other standard users, for example some administration
+              ad-hoc commands will only be available to that JID.
+            '';
+          };
+          options.ca_file = mkOption {
+            type = types.path;
+            default = "/etc/ssl/certs/ca-certificates.crt";
+            description = ''
+              Specifies which file should be used as the list of trusted CA
+              when negociating a TLS session.
+            '';
+          };
+          options.db_name = mkOption {
+            type = with types; either path str;
+            default = "${stateDir}/biboumi.sqlite";
+            description = ''
+              The name of the database to use.
+            '';
+            example = "postgresql://user:secret@localhost";
+          };
+          options.hostname = mkOption {
+            type = types.str;
+            example = "biboumi.example.org";
+            description = ''
+              The hostname served by the XMPP gateway.
+              This domain must be configured in the XMPP server
+              as an external component.
+            '';
+          };
+          options.identd_port = mkOption {
+            type = types.port;
+            default = 113;
+            example = 0;
+            description = ''
+              The TCP port on which to listen for identd queries.
+            '';
+          };
+          options.log_level = mkOption {
+            type = types.ints.between 0 3;
+            default = 1;
+            description = ''
+              Indicate what type of log messages to write in the logs.
+              0 is debug, 1 is info, 2 is warning, 3 is error.
+            '';
+          };
+          options.password = mkOption {
+            type = with types; nullOr str;
+            description = ''
+              The password used to authenticate the XMPP component to your XMPP server.
+              This password must be configured in the XMPP server,
+              associated with the external component on
+              <link linkend="opt-services.biboumi.settings.hostname">hostname</link>.
+
+              Set it to null and use <link linkend="opt-services.biboumi.credentialsFile">credentialsFile</link>
+              if you do not want this password to go into the Nix store.
+            '';
+          };
+          options.persistent_by_default = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Whether all rooms will be persistent by default:
+              the value of the “persistent†option in the global configuration of each
+              user will be “trueâ€, but the value of each individual room will still
+              default to false. This means that a user just needs to change the global
+              “persistent†configuration option to false in order to override this.
+            '';
+          };
+          options.policy_directory = mkOption {
+            type = types.path;
+            default = "${pkgs.biboumi}/etc/biboumi";
+            defaultText = literalExpression ''"''${pkgs.biboumi}/etc/biboumi"'';
+            description = ''
+              A directory that should contain the policy files,
+              used to customize Botan’s behaviour
+              when negociating the TLS connections with the IRC servers.
+            '';
+          };
+          options.port = mkOption {
+            type = types.port;
+            default = 5347;
+            description = ''
+              The TCP port to use to connect to the local XMPP component.
+            '';
+          };
+          options.realname_customization = mkOption {
+            type = types.bool;
+            default = true;
+            description = ''
+              Whether the users will be able to use
+              the ad-hoc commands that lets them configure
+              their realname and username.
+            '';
+          };
+          options.realname_from_jid = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Whether the realname and username of each biboumi
+              user will be extracted from their JID.
+              Otherwise they will be set to the nick
+              they used to connect to the IRC server.
+            '';
+          };
+          options.xmpp_server_ip = mkOption {
+            type = types.str;
+            default = "127.0.0.1";
+            description = ''
+              The IP address to connect to the XMPP server on.
+              The connection to the XMPP server is unencrypted,
+              so the biboumi instance and the server should
+              normally be on the same host.
+            '';
+          };
+        };
+      };
+
+      credentialsFile = mkOption {
+        type = types.path;
+        description = ''
+          Path to a configuration file to be merged with the settings.
+          Beware not to surround "=" with spaces when setting biboumi's options in this file.
+          Useful to merge a file which is better kept out of the Nix store
+          because it contains sensible data like
+          <link linkend="opt-services.biboumi.settings.password">password</link>.
+        '';
+        default = "/dev/null";
+        example = "/run/keys/biboumi.cfg";
+      };
+
+      openFirewall = mkEnableOption "opening of the identd port in the firewall";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.firewall = mkIf (cfg.openFirewall && cfg.settings.identd_port != 0)
+      { allowedTCPPorts = [ cfg.settings.identd_port ]; };
+
+    systemd.services.biboumi = {
+      description = "Biboumi, XMPP to IRC gateway";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "notify";
+        # Biboumi supports systemd's watchdog.
+        WatchdogSec = 20;
+        Restart = "always";
+        # Use "+" because credentialsFile may not be accessible to User= or Group=.
+        ExecStartPre = [("+" + pkgs.writeShellScript "biboumi-prestart" ''
+          set -eux
+          cat ${settingsFile} '${cfg.credentialsFile}' |
+          install -m 644 /dev/stdin /run/biboumi/biboumi.cfg
+        '')];
+        ExecStart = "${pkgs.biboumi}/bin/biboumi /run/biboumi/biboumi.cfg";
+        ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID";
+        # Firewalls needing opening for output connections can still do that
+        # selectively for biboumi with:
+        # users.users.biboumi.isSystemUser = true;
+        # and, for example:
+        # networking.nftables.ruleset = ''
+        #   add rule inet filter output meta skuid biboumi tcp accept
+        # '';
+        DynamicUser = true;
+        RootDirectory = rootDir;
+        RootDirectoryStartOnly = true;
+        InaccessiblePaths = [ "-+${rootDir}" ];
+        RuntimeDirectory = [ "biboumi" (removePrefix "/run/" rootDir) ];
+        RuntimeDirectoryMode = "700";
+        StateDirectory = "biboumi";
+        StateDirectoryMode = "700";
+        MountAPIVFS = true;
+        UMask = "0066";
+        BindPaths = [
+          stateDir
+          # This is for Type="notify"
+          # See https://github.com/systemd/systemd/issues/3544
+          "/run/systemd/notify"
+          "/run/systemd/journal/socket"
+        ];
+        BindReadOnlyPaths = [
+          builtins.storeDir
+          "/etc"
+        ];
+        # The following options are only for optimizing:
+        # systemd-analyze security biboumi
+        AmbientCapabilities = [ (optionalString need_CAP_NET_BIND_SERVICE "CAP_NET_BIND_SERVICE") ];
+        CapabilityBoundingSet = [ (optionalString need_CAP_NET_BIND_SERVICE "CAP_NET_BIND_SERVICE") ];
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateNetwork = mkDefault false;
+        PrivateTmp = true;
+        # PrivateUsers=true breaks AmbientCapabilities=CAP_NET_BIND_SERVICE
+        # See https://bugs.archlinux.org/task/65921
+        PrivateUsers = !need_CAP_NET_BIND_SERVICE;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        # AF_UNIX is for /run/systemd/notify
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallFilter = [
+          "@system-service"
+          # Groups in @system-service which do not contain a syscall
+          # listed by perf stat -e 'syscalls:sys_enter_*' biboumi biboumi.cfg
+          # in tests, and seem likely not necessary for biboumi.
+          # To run such a perf in ExecStart=, you have to:
+          # - AmbientCapabilities="CAP_SYS_ADMIN"
+          # - mount -o remount,mode=755 /sys/kernel/debug/{,tracing}
+          "~@aio" "~@chown" "~@ipc" "~@keyring" "~@resources" "~@setuid" "~@timer"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ julm ];
+}
diff --git a/nixos/modules/services/networking/bind.nix b/nixos/modules/services/networking/bind.nix
new file mode 100644
index 00000000000..2045612ec05
--- /dev/null
+++ b/nixos/modules/services/networking/bind.nix
@@ -0,0 +1,274 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.bind;
+
+  bindPkg = config.services.bind.package;
+
+  bindUser = "named";
+
+  bindZoneCoerce = list: builtins.listToAttrs (lib.forEach list (zone: { name = zone.name; value = zone; }));
+
+  bindZoneOptions = { name, config, ... }: {
+    options = {
+      name = mkOption {
+        type = types.str;
+        default = name;
+        description = "Name of the zone.";
+      };
+      master = mkOption {
+        description = "Master=false means slave server";
+        type = types.bool;
+      };
+      file = mkOption {
+        type = types.either types.str types.path;
+        description = "Zone file resource records contain columns of data, separated by whitespace, that define the record.";
+      };
+      masters = mkOption {
+        type = types.listOf types.str;
+        description = "List of servers for inclusion in stub and secondary zones.";
+      };
+      slaves = mkOption {
+        type = types.listOf types.str;
+        description = "Addresses who may request zone transfers.";
+        default = [ ];
+      };
+      extraConfig = mkOption {
+        type = types.str;
+        description = "Extra zone config to be appended at the end of the zone section.";
+        default = "";
+      };
+    };
+  };
+
+  confFile = pkgs.writeText "named.conf"
+    ''
+      include "/etc/bind/rndc.key";
+      controls {
+        inet 127.0.0.1 allow {localhost;} keys {"rndc-key";};
+      };
+
+      acl cachenetworks { ${concatMapStrings (entry: " ${entry}; ") cfg.cacheNetworks} };
+      acl badnetworks { ${concatMapStrings (entry: " ${entry}; ") cfg.blockedNetworks} };
+
+      options {
+        listen-on { ${concatMapStrings (entry: " ${entry}; ") cfg.listenOn} };
+        listen-on-v6 { ${concatMapStrings (entry: " ${entry}; ") cfg.listenOnIpv6} };
+        allow-query { cachenetworks; };
+        blackhole { badnetworks; };
+        forward ${cfg.forward};
+        forwarders { ${concatMapStrings (entry: " ${entry}; ") cfg.forwarders} };
+        directory "${cfg.directory}";
+        pid-file "/run/named/named.pid";
+        ${cfg.extraOptions}
+      };
+
+      ${cfg.extraConfig}
+
+      ${ concatMapStrings
+          ({ name, file, master ? true, slaves ? [], masters ? [], extraConfig ? "" }:
+            ''
+              zone "${name}" {
+                type ${if master then "master" else "slave"};
+                file "${file}";
+                ${ if master then
+                   ''
+                     allow-transfer {
+                       ${concatMapStrings (ip: "${ip};\n") slaves}
+                     };
+                   ''
+                   else
+                   ''
+                     masters {
+                       ${concatMapStrings (ip: "${ip};\n") masters}
+                     };
+                   ''
+                }
+                allow-query { any; };
+                ${extraConfig}
+              };
+            '')
+          (attrValues cfg.zones) }
+    '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.bind = {
+
+      enable = mkEnableOption "BIND domain name server";
+
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.bind;
+        defaultText = literalExpression "pkgs.bind";
+        description = "The BIND package to use.";
+      };
+
+      cacheNetworks = mkOption {
+        default = [ "127.0.0.0/24" ];
+        type = types.listOf types.str;
+        description = "
+          What networks are allowed to use us as a resolver.  Note
+          that this is for recursive queries -- all networks are
+          allowed to query zones configured with the `zones` option.
+          It is recommended that you limit cacheNetworks to avoid your
+          server being used for DNS amplification attacks.
+        ";
+      };
+
+      blockedNetworks = mkOption {
+        default = [ ];
+        type = types.listOf types.str;
+        description = "
+          What networks are just blocked.
+        ";
+      };
+
+      ipv4Only = mkOption {
+        default = false;
+        type = types.bool;
+        description = "
+          Only use ipv4, even if the host supports ipv6.
+        ";
+      };
+
+      forwarders = mkOption {
+        default = config.networking.nameservers;
+        defaultText = literalExpression "config.networking.nameservers";
+        type = types.listOf types.str;
+        description = "
+          List of servers we should forward requests to.
+        ";
+      };
+
+      forward = mkOption {
+        default = "first";
+        type = types.enum ["first" "only"];
+        description = "
+          Whether to forward 'first' (try forwarding but lookup directly if forwarding fails) or 'only'.
+        ";
+      };
+
+      listenOn = mkOption {
+        default = [ "any" ];
+        type = types.listOf types.str;
+        description = "
+          Interfaces to listen on.
+        ";
+      };
+
+      listenOnIpv6 = mkOption {
+        default = [ "any" ];
+        type = types.listOf types.str;
+        description = "
+          Ipv6 interfaces to listen on.
+        ";
+      };
+
+      directory = mkOption {
+        type = types.str;
+        default = "/run/named";
+        description = "Working directory of BIND.";
+      };
+
+      zones = mkOption {
+        default = [ ];
+        type = with types; coercedTo (listOf attrs) bindZoneCoerce (attrsOf (types.submodule bindZoneOptions));
+        description = "
+          List of zones we claim authority over.
+        ";
+        example = {
+          "example.com" = {
+            master = false;
+            file = "/var/dns/example.com";
+            masters = [ "192.168.0.1" ];
+            slaves = [ ];
+            extraConfig = "";
+          };
+        };
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "
+          Extra lines to be added verbatim to the generated named configuration file.
+        ";
+      };
+
+      extraOptions = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra lines to be added verbatim to the options section of the
+          generated named configuration file.
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.path;
+        default = confFile;
+        defaultText = literalExpression "confFile";
+        description = "
+          Overridable config file to use for named. By default, that
+          generated by nixos.
+        ";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    networking.resolvconf.useLocalResolver = mkDefault true;
+
+    users.users.${bindUser} =
+      {
+        group = bindUser;
+        description = "BIND daemon user";
+        isSystemUser = true;
+      };
+    users.groups.${bindUser} = {};
+
+    systemd.services.bind = {
+      description = "BIND Domain Name Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        mkdir -m 0755 -p /etc/bind
+        if ! [ -f "/etc/bind/rndc.key" ]; then
+          ${bindPkg.out}/sbin/rndc-confgen -c /etc/bind/rndc.key -u ${bindUser} -a -A hmac-sha256 2>/dev/null
+        fi
+
+        ${pkgs.coreutils}/bin/mkdir -p /run/named
+        chown ${bindUser} /run/named
+
+        ${pkgs.coreutils}/bin/mkdir -p ${cfg.directory}
+        chown ${bindUser} ${cfg.directory}
+      '';
+
+      serviceConfig = {
+        ExecStart = "${bindPkg.out}/sbin/named -u ${bindUser} ${optionalString cfg.ipv4Only "-4"} -c ${cfg.configFile} -f";
+        ExecReload = "${bindPkg.out}/sbin/rndc -k '/etc/bind/rndc.key' reload";
+        ExecStop = "${bindPkg.out}/sbin/rndc -k '/etc/bind/rndc.key' stop";
+      };
+
+      unitConfig.Documentation = "man:named(8)";
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/bird.nix b/nixos/modules/services/networking/bird.nix
new file mode 100644
index 00000000000..3049c4f2bce
--- /dev/null
+++ b/nixos/modules/services/networking/bird.nix
@@ -0,0 +1,103 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkEnableOption mkIf mkOption optionalString types;
+
+  cfg = config.services.bird2;
+  caps = [ "CAP_NET_ADMIN" "CAP_NET_BIND_SERVICE" "CAP_NET_RAW" ];
+in
+{
+  ###### interface
+  options = {
+    services.bird2 = {
+      enable = mkEnableOption "BIRD Internet Routing Daemon";
+      config = mkOption {
+        type = types.lines;
+        description = ''
+          BIRD Internet Routing Daemon configuration file.
+          <link xlink:href='http://bird.network.cz/'/>
+        '';
+      };
+      checkConfig = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether the config should be checked at build time.
+          When the config can't be checked during build time, for example when it includes
+          other files, either disable this option or use <code>preCheckConfig</code> to create
+          the included files before checking.
+        '';
+      };
+      preCheckConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          echo "cost 100;" > include.conf
+        '';
+        description = ''
+          Commands to execute before the config file check. The file to be checked will be
+          available as <code>bird2.conf</code> in the current directory.
+
+          Files created with this option will not be available at service runtime, only during
+          build time checking.
+        '';
+      };
+    };
+  };
+
+
+  imports = [
+    (lib.mkRemovedOptionModule [ "services" "bird" ] "Use services.bird2 instead")
+    (lib.mkRemovedOptionModule [ "services" "bird6" ] "Use services.bird2 instead")
+  ];
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.bird ];
+
+    environment.etc."bird/bird2.conf".source = pkgs.writeTextFile {
+      name = "bird2";
+      text = cfg.config;
+      checkPhase = optionalString cfg.checkConfig ''
+        ln -s $out bird2.conf
+        ${cfg.preCheckConfig}
+        ${pkgs.bird}/bin/bird -d -p -c bird2.conf
+      '';
+    };
+
+    systemd.services.bird2 = {
+      description = "BIRD Internet Routing Daemon";
+      wantedBy = [ "multi-user.target" ];
+      reloadIfChanged = true;
+      restartTriggers = [ config.environment.etc."bird/bird2.conf".source ];
+      serviceConfig = {
+        Type = "forking";
+        Restart = "on-failure";
+        User = "bird2";
+        Group = "bird2";
+        ExecStart = "${pkgs.bird}/bin/bird -c /etc/bird/bird2.conf";
+        ExecReload = "${pkgs.bird}/bin/birdc configure";
+        ExecStop = "${pkgs.bird}/bin/birdc down";
+        RuntimeDirectory = "bird";
+        CapabilityBoundingSet = caps;
+        AmbientCapabilities = caps;
+        ProtectSystem = "full";
+        ProtectHome = "yes";
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        SystemCallFilter = "~@cpu-emulation @debug @keyring @module @mount @obsolete @raw-io";
+        MemoryDenyWriteExecute = "yes";
+      };
+    };
+    users = {
+      users.bird2 = {
+        description = "BIRD Internet Routing Daemon user";
+        group = "bird2";
+        isSystemUser = true;
+      };
+      groups.bird2 = { };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/bitcoind.nix b/nixos/modules/services/networking/bitcoind.nix
new file mode 100644
index 00000000000..80033d95860
--- /dev/null
+++ b/nixos/modules/services/networking/bitcoind.nix
@@ -0,0 +1,261 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  eachBitcoind = config.services.bitcoind;
+
+  rpcUserOpts = { name, ... }: {
+    options = {
+      name = mkOption {
+        type = types.str;
+        example = "alice";
+        description = ''
+          Username for JSON-RPC connections.
+        '';
+      };
+      passwordHMAC = mkOption {
+        type = types.uniq (types.strMatching "[0-9a-f]+\\$[0-9a-f]{64}");
+        example = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
+        description = ''
+          Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the
+          format &lt;SALT-HEX&gt;$&lt;HMAC-HEX&gt;.
+
+          Tool (Python script) for HMAC generation is available here:
+          <link xlink:href="https://github.com/bitcoin/bitcoin/blob/master/share/rpcauth/rpcauth.py"/>
+        '';
+      };
+    };
+    config = {
+      name = mkDefault name;
+    };
+  };
+
+  bitcoindOpts = { config, lib, name, ...}: {
+    options = {
+
+      enable = mkEnableOption "Bitcoin daemon";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.bitcoind;
+        defaultText = literalExpression "pkgs.bitcoind";
+        description = "The package providing bitcoin binaries.";
+      };
+
+      configFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/lib/${name}/bitcoin.conf";
+        description = "The configuration file path to supply bitcoind.";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          par=16
+          rpcthreads=16
+          logips=1
+        '';
+        description = "Additional configurations to be appended to <filename>bitcoin.conf</filename>.";
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/bitcoind-${name}";
+        description = "The data directory for bitcoind.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "bitcoind-${name}";
+        description = "The user as which to run bitcoind.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = config.user;
+        description = "The group as which to run bitcoind.";
+      };
+
+      rpc = {
+        port = mkOption {
+          type = types.nullOr types.port;
+          default = null;
+          description = "Override the default port on which to listen for JSON-RPC connections.";
+        };
+        users = mkOption {
+          default = {};
+          example = literalExpression ''
+            {
+              alice.passwordHMAC = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
+              bob.passwordHMAC = "b2dd077cb54591a2f3139e69a897ac$4e71f08d48b4347cf8eff3815c0e25ae2e9a4340474079f55705f40574f4ec99";
+            }
+          '';
+          type = types.attrsOf (types.submodule rpcUserOpts);
+          description = "RPC user information for JSON-RPC connnections.";
+        };
+      };
+
+      pidFile = mkOption {
+        type = types.path;
+        default = "${config.dataDir}/bitcoind.pid";
+        description = "Location of bitcoind pid file.";
+      };
+
+      testnet = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to use the testnet instead of mainnet.";
+      };
+
+      port = mkOption {
+        type = types.nullOr types.port;
+        default = null;
+        description = "Override the default port on which to listen for connections.";
+      };
+
+      dbCache = mkOption {
+        type = types.nullOr (types.ints.between 4 16384);
+        default = null;
+        example = 4000;
+        description = "Override the default database cache size in MiB.";
+      };
+
+      prune = mkOption {
+        type = types.nullOr (types.coercedTo
+          (types.enum [ "disable" "manual" ])
+          (x: if x == "disable" then 0 else 1)
+          types.ints.unsigned
+        );
+        default = null;
+        example = 10000;
+        description = ''
+          Reduce storage requirements by enabling pruning (deleting) of old
+          blocks. This allows the pruneblockchain RPC to be called to delete
+          specific blocks, and enables automatic pruning of old blocks if a
+          target size in MiB is provided. This mode is incompatible with -txindex
+          and -rescan. Warning: Reverting this setting requires re-downloading
+          the entire blockchain. ("disable" = disable pruning blocks, "manual"
+          = allow manual pruning via RPC, >=550 = automatically prune block files
+          to stay under the specified target size in MiB).
+        '';
+      };
+
+      extraCmdlineOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Extra command line options to pass to bitcoind.
+          Run bitcoind --help to list all available options.
+        '';
+      };
+    };
+  };
+in
+{
+
+  options = {
+    services.bitcoind = mkOption {
+      type = types.attrsOf (types.submodule bitcoindOpts);
+      default = {};
+      description = "Specification of one or more bitcoind instances.";
+    };
+  };
+
+  config = mkIf (eachBitcoind != {}) {
+
+    assertions = flatten (mapAttrsToList (bitcoindName: cfg: [
+    {
+      assertion = (cfg.prune != null) -> (builtins.elem cfg.prune [ "disable" "manual" 0 1 ] || (builtins.isInt cfg.prune && cfg.prune >= 550));
+      message = ''
+        If set, services.bitcoind.${bitcoindName}.prune has to be "disable", "manual", 0 , 1 or >= 550.
+      '';
+    }
+    {
+      assertion = (cfg.rpc.users != {}) -> (cfg.configFile == null);
+      message = ''
+        You cannot set both services.bitcoind.${bitcoindName}.rpc.users and services.bitcoind.${bitcoindName}.configFile
+        as they are exclusive. RPC user setting would have no effect if custom configFile would be used.
+      '';
+    }
+    ]) eachBitcoind);
+
+    environment.systemPackages = flatten (mapAttrsToList (bitcoindName: cfg: [
+      cfg.package
+    ]) eachBitcoind);
+
+    systemd.services = mapAttrs' (bitcoindName: cfg: (
+      nameValuePair "bitcoind-${bitcoindName}" (
+      let
+        configFile = pkgs.writeText "bitcoin.conf" ''
+          # If Testnet is enabled, we need to add [test] section
+          # otherwise, some options (e.g.: custom RPC port) will not work
+          ${optionalString cfg.testnet "[test]"}
+          # RPC users
+          ${concatMapStringsSep  "\n"
+            (rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}")
+            (attrValues cfg.rpc.users)
+          }
+          # Extra config options (from bitcoind nixos service)
+          ${cfg.extraConfig}
+        '';
+      in {
+        description = "Bitcoin daemon";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          User = cfg.user;
+          Group = cfg.group;
+          ExecStart = ''
+            ${cfg.package}/bin/bitcoind \
+            ${if (cfg.configFile != null) then
+              "-conf=${cfg.configFile}"
+            else
+              "-conf=${configFile}"
+            } \
+            -datadir=${cfg.dataDir} \
+            -pid=${cfg.pidFile} \
+            ${optionalString cfg.testnet "-testnet"}\
+            ${optionalString (cfg.port != null) "-port=${toString cfg.port}"}\
+            ${optionalString (cfg.prune != null) "-prune=${toString cfg.prune}"}\
+            ${optionalString (cfg.dbCache != null) "-dbcache=${toString cfg.dbCache}"}\
+            ${optionalString (cfg.rpc.port != null) "-rpcport=${toString cfg.rpc.port}"}\
+            ${toString cfg.extraCmdlineOptions}
+          '';
+          Restart = "on-failure";
+
+          # Hardening measures
+          PrivateTmp = "true";
+          ProtectSystem = "full";
+          NoNewPrivileges = "true";
+          PrivateDevices = "true";
+          MemoryDenyWriteExecute = "true";
+        };
+      }
+    ))) eachBitcoind;
+
+    systemd.tmpfiles.rules = flatten (mapAttrsToList (bitcoindName: cfg: [
+      "d '${cfg.dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -"
+    ]) eachBitcoind);
+
+    users.users = mapAttrs' (bitcoindName: cfg: (
+      nameValuePair "bitcoind-${bitcoindName}" {
+      name = cfg.user;
+      group = cfg.group;
+      description = "Bitcoin daemon user";
+      home = cfg.dataDir;
+      isSystemUser = true;
+    })) eachBitcoind;
+
+    users.groups = mapAttrs' (bitcoindName: cfg: (
+      nameValuePair "${cfg.group}" { }
+    )) eachBitcoind;
+
+  };
+
+  meta.maintainers = with maintainers; [ _1000101 ];
+
+}
diff --git a/nixos/modules/services/networking/bitlbee.nix b/nixos/modules/services/networking/bitlbee.nix
new file mode 100644
index 00000000000..8bf04e3a1a2
--- /dev/null
+++ b/nixos/modules/services/networking/bitlbee.nix
@@ -0,0 +1,189 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.bitlbee;
+  bitlbeeUid = config.ids.uids.bitlbee;
+
+  bitlbeePkg = pkgs.bitlbee.override {
+    enableLibPurple = cfg.libpurple_plugins != [];
+    enablePam = cfg.authBackend == "pam";
+  };
+
+  bitlbeeConfig = pkgs.writeText "bitlbee.conf"
+    ''
+    [settings]
+    RunMode = Daemon
+    ConfigDir = ${cfg.configDir}
+    DaemonInterface = ${cfg.interface}
+    DaemonPort = ${toString cfg.portNumber}
+    AuthMode = ${cfg.authMode}
+    AuthBackend = ${cfg.authBackend}
+    Plugindir = ${pkgs.bitlbee-plugins cfg.plugins}/lib/bitlbee
+    ${lib.optionalString (cfg.hostName != "") "HostName = ${cfg.hostName}"}
+    ${lib.optionalString (cfg.protocols != "") "Protocols = ${cfg.protocols}"}
+    ${cfg.extraSettings}
+
+    [defaults]
+    ${cfg.extraDefaults}
+    '';
+
+  purple_plugin_path =
+    lib.concatMapStringsSep ":"
+      (plugin: "${plugin}/lib/pidgin/:${plugin}/lib/purple-2/")
+      cfg.libpurple_plugins
+    ;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.bitlbee = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run the BitlBee IRC to other chat network gateway.
+          Running it allows you to access the MSN, Jabber, Yahoo! and ICQ chat
+          networks via an IRC client.
+        '';
+      };
+
+      interface = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = ''
+          The interface the BitlBee deamon will be listening to.  If `127.0.0.1',
+          only clients on the local host can connect to it; if `0.0.0.0', clients
+          can access it from any network interface.
+        '';
+      };
+
+      portNumber = mkOption {
+        default = 6667;
+        type = types.int;
+        description = ''
+          Number of the port BitlBee will be listening to.
+        '';
+      };
+
+      authBackend = mkOption {
+        default = "storage";
+        type = types.enum [ "storage" "pam" ];
+        description = ''
+          How users are authenticated
+            storage -- save passwords internally
+            pam -- Linux PAM authentication
+        '';
+      };
+
+      authMode = mkOption {
+        default = "Open";
+        type = types.enum [ "Open" "Closed" "Registered" ];
+        description = ''
+          The following authentication modes are available:
+            Open -- Accept connections from anyone, use NickServ for user authentication.
+            Closed -- Require authorization (using the PASS command during login) before allowing the user to connect at all.
+            Registered -- Only allow registered users to use this server; this disables the register- and the account command until the user identifies himself.
+        '';
+      };
+
+      hostName = mkOption {
+        default = "";
+        type = types.str;
+        description = ''
+          Normally, BitlBee gets a hostname using getsockname(). If you have a nicer
+          alias for your BitlBee daemon, you can set it here and BitlBee will identify
+          itself with that name instead.
+        '';
+      };
+
+      plugins = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "[ pkgs.bitlbee-facebook ]";
+        description = ''
+          The list of bitlbee plugins to install.
+        '';
+      };
+
+      libpurple_plugins = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "[ pkgs.purple-matrix ]";
+        description = ''
+          The list of libpurple plugins to install.
+        '';
+      };
+
+      configDir = mkOption {
+        default = "/var/lib/bitlbee";
+        type = types.path;
+        description = ''
+          Specify an alternative directory to store all the per-user configuration
+          files.
+        '';
+      };
+
+      protocols = mkOption {
+        default = "";
+        type = types.str;
+        description = ''
+          This option allows to remove the support of protocol, even if compiled
+          in. If nothing is given, there are no restrictions.
+        '';
+      };
+
+      extraSettings = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Will be inserted in the Settings section of the config file.
+        '';
+      };
+
+      extraDefaults = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Will be inserted in the Default section of the config file.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config =  mkMerge [
+    (mkIf config.services.bitlbee.enable {
+      systemd.services.bitlbee = {
+        environment.PURPLE_PLUGIN_PATH = purple_plugin_path;
+        description = "BitlBee IRC to other chat networks gateway";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = {
+          DynamicUser = true;
+          StateDirectory = "bitlbee";
+          ExecStart = "${bitlbeePkg}/sbin/bitlbee -F -n -c ${bitlbeeConfig}";
+        };
+      };
+
+      environment.systemPackages = [ bitlbeePkg ];
+
+    })
+    (mkIf (config.services.bitlbee.authBackend == "pam") {
+      security.pam.services.bitlbee = {};
+    })
+  ];
+
+}
diff --git a/nixos/modules/services/networking/blockbook-frontend.nix b/nixos/modules/services/networking/blockbook-frontend.nix
new file mode 100644
index 00000000000..eeea521c8d5
--- /dev/null
+++ b/nixos/modules/services/networking/blockbook-frontend.nix
@@ -0,0 +1,278 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  eachBlockbook = config.services.blockbook-frontend;
+
+  blockbookOpts = { config, lib, name, ...}: {
+
+    options = {
+
+      enable = mkEnableOption "blockbook-frontend application.";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.blockbook;
+        defaultText = literalExpression "pkgs.blockbook";
+        description = "Which blockbook package to use.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "blockbook-frontend-${name}";
+        description = "The user as which to run blockbook-frontend-${name}.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "${config.user}";
+        description = "The group as which to run blockbook-frontend-${name}.";
+      };
+
+      certFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/etc/secrets/blockbook-frontend-${name}/certFile";
+        description = ''
+          To enable SSL, specify path to the name of certificate files without extension.
+          Expecting <filename>certFile.crt</filename> and <filename>certFile.key</filename>.
+        '';
+      };
+
+      configFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "${config.dataDir}/config.json";
+        description = "Location of the blockbook configuration file.";
+      };
+
+      coinName = mkOption {
+        type = types.str;
+        default = "Bitcoin";
+        description = ''
+          See <link xlink:href="https://github.com/trezor/blockbook/blob/master/bchain/coins/blockchain.go#L61"/>
+          for current of coins supported in master (Note: may differ from release).
+        '';
+      };
+
+      cssDir = mkOption {
+        type = types.path;
+        default = "${config.package}/share/css/";
+        defaultText = literalExpression ''"''${package}/share/css/"'';
+        example = literalExpression ''"''${dataDir}/static/css/"'';
+        description = ''
+          Location of the dir with <filename>main.css</filename> CSS file.
+          By default, the one shipped with the package is used.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/blockbook-frontend-${name}";
+        description = "Location of blockbook-frontend-${name} data directory.";
+      };
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Debug mode, return more verbose errors, reload templates on each request.";
+      };
+
+      internal = mkOption {
+        type = types.nullOr types.str;
+        default = ":9030";
+        description = "Internal http server binding <literal>[address]:port</literal>.";
+      };
+
+      messageQueueBinding = mkOption {
+        type = types.str;
+        default = "tcp://127.0.0.1:38330";
+        description = "Message Queue Binding <literal>address:port</literal>.";
+      };
+
+      public = mkOption {
+        type = types.nullOr types.str;
+        default = ":9130";
+        description = "Public http server binding <literal>[address]:port</literal>.";
+      };
+
+      rpc = {
+        url = mkOption {
+          type = types.str;
+          default = "http://127.0.0.1";
+          description = "URL for JSON-RPC connections.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8030;
+          description = "Port for JSON-RPC connections.";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "rpc";
+          description = "Username for JSON-RPC connections.";
+        };
+
+        password = mkOption {
+          type = types.str;
+          default = "rpc";
+          description = ''
+            RPC password for JSON-RPC connections.
+            Warning: this is stored in cleartext in the Nix store!!!
+            Use <literal>configFile</literal> or <literal>passwordFile</literal> if needed.
+          '';
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          description = ''
+            File containing password of the RPC user.
+            Note: This options is ignored when <literal>configFile</literal> is used.
+          '';
+        };
+      };
+
+      sync = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Synchronizes until tip, if together with zeromq, keeps index synchronized.";
+      };
+
+      templateDir = mkOption {
+        type = types.path;
+        default = "${config.package}/share/templates/";
+        defaultText = literalExpression ''"''${package}/share/templates/"'';
+        example = literalExpression ''"''${dataDir}/templates/static/"'';
+        description = "Location of the HTML templates. By default, ones shipped with the package are used.";
+      };
+
+      extraConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        example = literalExpression '' {
+          "alternative_estimate_fee" = "whatthefee-disabled";
+          "alternative_estimate_fee_params" = "{\"url\": \"https://whatthefee.io/data.json\", \"periodSeconds\": 60}";
+          "fiat_rates" = "coingecko";
+          "fiat_rates_params" = "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin\", \"periodSeconds\": 60}";
+          "coin_shortcut" = "BTC";
+          "coin_label" = "Bitcoin";
+          "parse" = true;
+          "subversion" = "";
+          "address_format" = "";
+          "xpub_magic" = 76067358;
+          "xpub_magic_segwit_p2sh" = 77429938;
+          "xpub_magic_segwit_native" = 78792518;
+          "mempool_workers" = 8;
+          "mempool_sub_workers" = 2;
+          "block_addresses_to_keep" = 300;
+        }'';
+        description = ''
+          Additional configurations to be appended to <filename>coin.conf</filename>.
+          Overrides any already defined configuration options.
+          See <link xlink:href="https://github.com/trezor/blockbook/tree/master/configs/coins"/>
+          for current configuration options supported in master (Note: may differ from release).
+        '';
+      };
+
+      extraCmdLineOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "-workers=1" "-dbcache=0" "-logtosderr" ];
+        description = ''
+          Extra command line options to pass to Blockbook.
+          Run blockbook --help to list all available options.
+        '';
+      };
+    };
+  };
+in
+{
+  # interface
+
+  options = {
+    services.blockbook-frontend = mkOption {
+      type = types.attrsOf (types.submodule blockbookOpts);
+      default = {};
+      description = "Specification of one or more blockbook-frontend instances.";
+    };
+  };
+
+  # implementation
+
+  config = mkIf (eachBlockbook != {}) {
+
+    systemd.services = mapAttrs' (blockbookName: cfg: (
+      nameValuePair "blockbook-frontend-${blockbookName}" (
+        let
+          configFile = if cfg.configFile != null then cfg.configFile else
+            pkgs.writeText "config.conf" (builtins.toJSON ( {
+                coin_name = "${cfg.coinName}";
+                rpc_user = "${cfg.rpc.user}";
+                rpc_pass = "${cfg.rpc.password}";
+                rpc_url = "${cfg.rpc.url}:${toString cfg.rpc.port}";
+                message_queue_binding = "${cfg.messageQueueBinding}";
+              } // cfg.extraConfig)
+            );
+        in {
+          description = "blockbook-frontend-${blockbookName} daemon";
+          after = [ "network.target" ];
+          wantedBy = [ "multi-user.target" ];
+          preStart = ''
+            ln -sf ${cfg.templateDir} ${cfg.dataDir}/static/
+            ln -sf ${cfg.cssDir} ${cfg.dataDir}/static/
+            ${optionalString (cfg.rpc.passwordFile != null && cfg.configFile == null) ''
+              CONFIGTMP=$(mktemp)
+              ${pkgs.jq}/bin/jq ".rpc_pass = \"$(cat ${cfg.rpc.passwordFile})\"" ${configFile} > $CONFIGTMP
+              mv $CONFIGTMP ${cfg.dataDir}/${blockbookName}-config.json
+            ''}
+          '';
+          serviceConfig = {
+            User = cfg.user;
+            Group = cfg.group;
+            ExecStart = ''
+               ${cfg.package}/bin/blockbook \
+               ${if (cfg.rpc.passwordFile != null && cfg.configFile == null) then
+               "-blockchaincfg=${cfg.dataDir}/${blockbookName}-config.json"
+               else
+               "-blockchaincfg=${configFile}"
+               } \
+               -datadir=${cfg.dataDir} \
+               ${optionalString (cfg.sync != false) "-sync"} \
+               ${optionalString (cfg.certFile != null) "-certfile=${toString cfg.certFile}"} \
+               ${optionalString (cfg.debug != false) "-debug"} \
+               ${optionalString (cfg.internal != null) "-internal=${toString cfg.internal}"} \
+               ${optionalString (cfg.public != null) "-public=${toString cfg.public}"} \
+               ${toString cfg.extraCmdLineOptions}
+            '';
+            Restart = "on-failure";
+            WorkingDirectory = cfg.dataDir;
+            LimitNOFILE = 65536;
+          };
+        }
+    ) )) eachBlockbook;
+
+    systemd.tmpfiles.rules = flatten (mapAttrsToList (blockbookName: cfg: [
+      "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -"
+      "d ${cfg.dataDir}/static 0750 ${cfg.user} ${cfg.group} - -"
+    ]) eachBlockbook);
+
+    users.users = mapAttrs' (blockbookName: cfg: (
+      nameValuePair "blockbook-frontend-${blockbookName}" {
+      name = cfg.user;
+      group = cfg.group;
+      home = cfg.dataDir;
+      isSystemUser = true;
+    })) eachBlockbook;
+
+    users.groups = mapAttrs' (instanceName: cfg: (
+      nameValuePair "${cfg.group}" { })) eachBlockbook;
+  };
+
+  meta.maintainers = with maintainers; [ _1000101 ];
+
+}
diff --git a/nixos/modules/services/networking/blocky.nix b/nixos/modules/services/networking/blocky.nix
new file mode 100644
index 00000000000..7488e05fc03
--- /dev/null
+++ b/nixos/modules/services/networking/blocky.nix
@@ -0,0 +1,40 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.blocky;
+
+  format = pkgs.formats.yaml { };
+  configFile = format.generate "config.yaml" cfg.settings;
+in
+{
+  options.services.blocky = {
+    enable = mkEnableOption "Fast and lightweight DNS proxy as ad-blocker for local network with many features";
+
+    settings = mkOption {
+      type = format.type;
+      default = { };
+      description = ''
+        Blocky configuration. Refer to
+        <link xlink:href="https://0xerr0r.github.io/blocky/configuration/"/>
+        for details on supported values.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.blocky = {
+      description = "A DNS proxy and ad-blocker for the local network";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${pkgs.blocky}/bin/blocky --config ${configFile}";
+
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/charybdis.nix b/nixos/modules/services/networking/charybdis.nix
new file mode 100644
index 00000000000..ff09c0160cb
--- /dev/null
+++ b/nixos/modules/services/networking/charybdis.nix
@@ -0,0 +1,114 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkEnableOption mkIf mkOption singleton types;
+  inherit (pkgs) coreutils charybdis;
+  cfg = config.services.charybdis;
+
+  configFile = pkgs.writeText "charybdis.conf" ''
+    ${cfg.config}
+  '';
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.charybdis = {
+
+      enable = mkEnableOption "Charybdis IRC daemon";
+
+      config = mkOption {
+        type = types.str;
+        description = ''
+          Charybdis IRC daemon configuration file.
+        '';
+      };
+
+      statedir = mkOption {
+        type = types.path;
+        default = "/var/lib/charybdis";
+        description = ''
+          Location of the state directory of charybdis.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "ircd";
+        description = ''
+          Charybdis IRC daemon user.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "ircd";
+        description = ''
+          Charybdis IRC daemon group.
+        '';
+      };
+
+      motd = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = ''
+          Charybdis MOTD text.
+
+          Charybdis will read its MOTD from /etc/charybdis/ircd.motd .
+          If set, the value of this option will be written to this path.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable (lib.mkMerge [
+    {
+      users.users.${cfg.user} = {
+        description = "Charybdis IRC daemon user";
+        uid = config.ids.uids.ircd;
+        group = cfg.group;
+      };
+
+      users.groups.${cfg.group} = {
+        gid = config.ids.gids.ircd;
+      };
+
+      systemd.tmpfiles.rules = [
+        "d ${cfg.statedir} - ${cfg.user} ${cfg.group} - -"
+      ];
+
+      environment.etc."charybdis/ircd.conf".source = configFile;
+
+      systemd.services.charybdis = {
+        description = "Charybdis IRC daemon";
+        wantedBy = [ "multi-user.target" ];
+        reloadIfChanged = true;
+        restartTriggers = [
+          configFile
+        ];
+        environment = {
+          BANDB_DBPATH = "${cfg.statedir}/ban.db";
+        };
+        serviceConfig = {
+          ExecStart   = "${charybdis}/bin/charybdis -foreground -logfile /dev/stdout -configfile /etc/charybdis/ircd.conf";
+          ExecReload = "${coreutils}/bin/kill -HUP $MAINPID";
+          Group = cfg.group;
+          User = cfg.user;
+        };
+      };
+
+    }
+
+    (mkIf (cfg.motd != null) {
+      environment.etc."charybdis/ircd.motd".text = cfg.motd;
+    })
+  ]);
+}
diff --git a/nixos/modules/services/networking/cjdns.nix b/nixos/modules/services/networking/cjdns.nix
new file mode 100644
index 00000000000..0d97d379e90
--- /dev/null
+++ b/nixos/modules/services/networking/cjdns.nix
@@ -0,0 +1,304 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  pkg = pkgs.cjdns;
+
+  cfg = config.services.cjdns;
+
+  connectToSubmodule =
+  { ... }:
+  { options =
+    { password = mkOption {
+        type = types.str;
+        description = "Authorized password to the opposite end of the tunnel.";
+      };
+      login = mkOption {
+        default = "";
+        type = types.str;
+        description = "(optional) name your peer has for you";
+      };
+      peerName = mkOption {
+        default = "";
+        type = types.str;
+        description = "(optional) human-readable name for peer";
+      };
+      publicKey = mkOption {
+        type = types.str;
+        description = "Public key at the opposite end of the tunnel.";
+      };
+      hostname = mkOption {
+        default = "";
+        example = "foobar.hype";
+        type = types.str;
+        description = "Optional hostname to add to /etc/hosts; prevents reverse lookup failures.";
+      };
+    };
+  };
+
+  # Additional /etc/hosts entries for peers with an associated hostname
+  cjdnsExtraHosts = pkgs.runCommand "cjdns-hosts" {} ''
+    exec >$out
+    ${concatStringsSep "\n" (mapAttrsToList (k: v:
+        optionalString (v.hostname != "")
+          "echo $(${pkgs.cjdns}/bin/publictoip6 ${v.publicKey}) ${v.hostname}")
+        (cfg.ETHInterface.connectTo // cfg.UDPInterface.connectTo))}
+  '';
+
+  parseModules = x:
+    x // { connectTo = mapAttrs (name: value: { inherit (value) password publicKey; }) x.connectTo; };
+
+  cjdrouteConf = builtins.toJSON ( recursiveUpdate {
+    admin = {
+      bind = cfg.admin.bind;
+      password = "@CJDNS_ADMIN_PASSWORD@";
+    };
+    authorizedPasswords = map (p: { password = p; }) cfg.authorizedPasswords;
+    interfaces = {
+      ETHInterface = if (cfg.ETHInterface.bind != "") then [ (parseModules cfg.ETHInterface) ] else [ ];
+      UDPInterface = if (cfg.UDPInterface.bind != "") then [ (parseModules cfg.UDPInterface) ] else [ ];
+    };
+
+    privateKey = "@CJDNS_PRIVATE_KEY@";
+
+    resetAfterInactivitySeconds = 100;
+
+    router = {
+      interface = { type = "TUNInterface"; };
+      ipTunnel = {
+        allowedConnections = [];
+        outgoingConnections = [];
+      };
+    };
+
+    security = [ { exemptAngel = 1; setuser = "nobody"; } ];
+
+  } cfg.extraConfig);
+
+in
+
+{
+  options = {
+
+    services.cjdns = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the cjdns network encryption
+          and routing engine. A file at /etc/cjdns.keys will
+          be created if it does not exist to contain a random
+          secret key that your IPv6 address will be derived from.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        example = { router.interface.tunDevice = "tun10"; };
+        description = ''
+          Extra configuration, given as attrs, that will be merged recursively
+          with the rest of the JSON generated by this module, at the root node.
+        '';
+      };
+
+      confFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/etc/cjdroute.conf";
+        description = ''
+          Ignore all other cjdns options and load configuration from this file.
+        '';
+      };
+
+      authorizedPasswords = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [
+          "snyrfgkqsc98qh1y4s5hbu0j57xw5s0"
+          "z9md3t4p45mfrjzdjurxn4wuj0d8swv"
+          "49275fut6tmzu354pq70sr5b95qq0vj"
+        ];
+        description = ''
+          Any remote cjdns nodes that offer these passwords on
+          connection will be allowed to route through this node.
+        '';
+      };
+
+      admin = {
+        bind = mkOption {
+          type = types.str;
+          default = "127.0.0.1:11234";
+          description = ''
+            Bind the administration port to this address and port.
+          '';
+        };
+      };
+
+      UDPInterface = {
+        bind = mkOption {
+          type = types.str;
+          default = "";
+          example = "192.168.1.32:43211";
+          description = ''
+            Address and port to bind UDP tunnels to.
+          '';
+         };
+        connectTo = mkOption {
+          type = types.attrsOf ( types.submodule ( connectToSubmodule ) );
+          default = { };
+          example = literalExpression ''
+            {
+              "192.168.1.1:27313" = {
+                hostname = "homer.hype";
+                password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
+                publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k";
+              };
+            }
+          '';
+          description = ''
+            Credentials for making UDP tunnels.
+          '';
+        };
+      };
+
+      ETHInterface = {
+        bind = mkOption {
+          type = types.str;
+          default = "";
+          example = "eth0";
+          description =
+            ''
+              Bind to this device for native ethernet operation.
+              <literal>all</literal> is a pseudo-name which will try to connect to all devices.
+            '';
+        };
+
+        beacon = mkOption {
+          type = types.int;
+          default = 2;
+          description = ''
+            Auto-connect to other cjdns nodes on the same network.
+            Options:
+              0: Disabled.
+              1: Accept beacons, this will cause cjdns to accept incoming
+                 beacon messages and try connecting to the sender.
+              2: Accept and send beacons, this will cause cjdns to broadcast
+                 messages on the local network which contain a randomly
+                 generated per-session password, other nodes which have this
+                 set to 1 or 2 will hear the beacon messages and connect
+                 automatically.
+          '';
+        };
+
+        connectTo = mkOption {
+          type = types.attrsOf ( types.submodule ( connectToSubmodule ) );
+          default = { };
+          example = literalExpression ''
+            {
+              "01:02:03:04:05:06" = {
+                hostname = "homer.hype";
+                password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
+                publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k";
+              };
+            }
+          '';
+          description = ''
+            Credentials for connecting look similar to UDP credientials
+            except they begin with the mac address.
+          '';
+        };
+      };
+
+      addExtraHosts = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to add cjdns peers with an associated hostname to
+          <filename>/etc/hosts</filename>.  Beware that enabling this
+          incurs heavy eval-time costs.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    boot.kernelModules = [ "tun" ];
+
+    # networking.firewall.allowedUDPPorts = ...
+
+    systemd.services.cjdns = {
+      description = "cjdns: routing engine designed for security, scalability, speed and ease of use";
+      wantedBy = [ "multi-user.target" "sleep.target"];
+      after = [ "network-online.target" ];
+      bindsTo = [ "network-online.target" ];
+
+      preStart = if cfg.confFile != null then "" else ''
+        [ -e /etc/cjdns.keys ] && source /etc/cjdns.keys
+
+        if [ -z "$CJDNS_PRIVATE_KEY" ]; then
+            shopt -s lastpipe
+            ${pkg}/bin/makekeys | { read private ipv6 public; }
+
+            umask 0077
+            echo "CJDNS_PRIVATE_KEY=$private" >> /etc/cjdns.keys
+            echo -e "CJDNS_IPV6=$ipv6\nCJDNS_PUBLIC_KEY=$public" > /etc/cjdns.public
+
+            chmod 600 /etc/cjdns.keys
+            chmod 444 /etc/cjdns.public
+        fi
+
+        if [ -z "$CJDNS_ADMIN_PASSWORD" ]; then
+            echo "CJDNS_ADMIN_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)" \
+                >> /etc/cjdns.keys
+        fi
+      '';
+
+      script = (
+        if cfg.confFile != null then "${pkg}/bin/cjdroute < ${cfg.confFile}" else
+          ''
+            source /etc/cjdns.keys
+            (cat <<'EOF'
+            ${cjdrouteConf}
+            EOF
+            ) | sed \
+                -e "s/@CJDNS_ADMIN_PASSWORD@/$CJDNS_ADMIN_PASSWORD/g" \
+                -e "s/@CJDNS_PRIVATE_KEY@/$CJDNS_PRIVATE_KEY/g" \
+                | ${pkg}/bin/cjdroute
+         ''
+      );
+
+      startLimitIntervalSec = 0;
+      serviceConfig = {
+        Type = "forking";
+        Restart = "always";
+        RestartSec = 1;
+        CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW CAP_SETUID";
+        ProtectSystem = true;
+        # Doesn't work on i686, causing service to fail
+        MemoryDenyWriteExecute = !pkgs.stdenv.isi686;
+        ProtectHome = true;
+        PrivateTmp = true;
+      };
+    };
+
+    networking.hostFiles = mkIf cfg.addExtraHosts [ cjdnsExtraHosts ];
+
+    assertions = [
+      { assertion = ( cfg.ETHInterface.bind != "" || cfg.UDPInterface.bind != "" || cfg.confFile != null );
+        message = "Neither cjdns.ETHInterface.bind nor cjdns.UDPInterface.bind defined.";
+      }
+      { assertion = config.networking.enableIPv6;
+        message = "networking.enableIPv6 must be enabled for CJDNS to work";
+      }
+    ];
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/cntlm.nix b/nixos/modules/services/networking/cntlm.nix
new file mode 100644
index 00000000000..eea28e12ce0
--- /dev/null
+++ b/nixos/modules/services/networking/cntlm.nix
@@ -0,0 +1,126 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.cntlm;
+
+  configFile = if cfg.configText != "" then
+    pkgs.writeText "cntlm.conf" ''
+      ${cfg.configText}
+    ''
+    else
+    pkgs.writeText "lighttpd.conf" ''
+      # Cntlm Authentication Proxy Configuration
+      Username ${cfg.username}
+      Domain ${cfg.domain}
+      Password ${cfg.password}
+      ${optionalString (cfg.netbios_hostname != "") "Workstation ${cfg.netbios_hostname}"}
+      ${concatMapStrings (entry: "Proxy ${entry}\n") cfg.proxy}
+      ${optionalString (cfg.noproxy != []) "NoProxy ${concatStringsSep ", " cfg.noproxy}"}
+
+      ${concatMapStrings (port: ''
+        Listen ${toString port}
+      '') cfg.port}
+
+      ${cfg.extraConfig}
+    '';
+
+in
+
+{
+
+  options.services.cntlm = {
+
+    enable = mkEnableOption "cntlm, which starts a local proxy";
+
+    username = mkOption {
+      type = types.str;
+      description = ''
+        Proxy account name, without the possibility to include domain name ('at' sign is interpreted literally).
+      '';
+    };
+
+    domain = mkOption {
+      type = types.str;
+      description = "Proxy account domain/workgroup name.";
+    };
+
+    password = mkOption {
+      default = "/etc/cntlm.password";
+      type = types.str;
+      description = "Proxy account password. Note: use chmod 0600 on /etc/cntlm.password for security.";
+    };
+
+    netbios_hostname = mkOption {
+      type = types.str;
+      default = "";
+      description = ''
+        The hostname of your machine.
+      '';
+    };
+
+    proxy = mkOption {
+      type = types.listOf types.str;
+      description = ''
+        A list of NTLM/NTLMv2 authenticating HTTP proxies.
+
+        Parent proxy, which requires authentication. The same as proxy on the command-line, can be used more than  once  to  specify  unlimited
+        number  of  proxies.  Should  one proxy fail, cntlm automatically moves on to the next one. The connect request fails only if the whole
+        list of proxies is scanned and (for each request) and found to be invalid. Command-line takes precedence over the configuration file.
+      '';
+      example = [ "proxy.example.com:81" ];
+    };
+
+    noproxy = mkOption {
+      description = ''
+        A list of domains where the proxy is skipped.
+      '';
+      default = [];
+      type = types.listOf types.str;
+      example = [ "*.example.com" "example.com" ];
+    };
+
+    port = mkOption {
+      default = [3128];
+      type = types.listOf types.port;
+      description = "Specifies on which ports the cntlm daemon listens.";
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = "Additional config appended to the end of the generated <filename>cntlm.conf</filename>.";
+    };
+
+    configText = mkOption {
+       type = types.lines;
+       default = "";
+       description = "Verbatim contents of <filename>cntlm.conf</filename>.";
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.cntlm = {
+      description = "CNTLM is an NTLM / NTLM Session Response / NTLMv2 authenticating HTTP proxy";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        User = "cntlm";
+        ExecStart = ''
+          ${pkgs.cntlm}/bin/cntlm -U cntlm -c ${configFile} -v -f
+        '';
+      };
+    };
+
+    users.users.cntlm = {
+      name = "cntlm";
+      description = "cntlm system-wide daemon";
+      isSystemUser = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/connman.nix b/nixos/modules/services/networking/connman.nix
new file mode 100644
index 00000000000..9945dc83a27
--- /dev/null
+++ b/nixos/modules/services/networking/connman.nix
@@ -0,0 +1,162 @@
+{ config, lib, pkgs, ... }:
+
+with pkgs;
+with lib;
+
+let
+  cfg = config.services.connman;
+  configFile = pkgs.writeText "connman.conf" ''
+    [General]
+    NetworkInterfaceBlacklist=${concatStringsSep "," cfg.networkInterfaceBlacklist}
+
+    ${cfg.extraConfig}
+  '';
+  enableIwd = cfg.wifi.backend == "iwd";
+in {
+
+  imports = [
+    (mkRenamedOptionModule [ "networking" "connman" ] [ "services" "connman" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.connman = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to use ConnMan for managing your network connections.
+        '';
+      };
+
+      enableVPN = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable ConnMan VPN service.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Configuration lines appended to the generated connman configuration file.
+        '';
+      };
+
+      networkInterfaceBlacklist = mkOption {
+        type = with types; listOf str;
+        default = [ "vmnet" "vboxnet" "virbr" "ifb" "ve" ];
+        description = ''
+          Default blacklisted interfaces, this includes NixOS containers interfaces (ve).
+        '';
+      };
+
+      wifi = {
+        backend = mkOption {
+          type = types.enum [ "wpa_supplicant" "iwd" ];
+          default = "wpa_supplicant";
+          description = ''
+            Specify the Wi-Fi backend used.
+            Currently supported are <option>wpa_supplicant</option> or <option>iwd</option>.
+          '';
+        };
+      };
+
+      extraFlags = mkOption {
+        type = with types; listOf str;
+        default = [ ];
+        example = [ "--nodnsproxy" ];
+        description = ''
+          Extra flags to pass to connmand
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        description = "The connman package / build flavor";
+        default = connman;
+        defaultText = literalExpression "pkgs.connman";
+        example = literalExpression "pkgs.connmanFull";
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [{
+      assertion = !config.networking.useDHCP;
+      message = "You can not use services.connman with networking.useDHCP";
+    }{
+      # TODO: connman seemingly can be used along network manager and
+      # connmanFull supports this - so this should be worked out somehow
+      assertion = !config.networking.networkmanager.enable;
+      message = "You can not use services.connman with networking.networkmanager";
+    }];
+
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.connman = {
+      description = "Connection service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "syslog.target" ] ++ optional enableIwd "iwd.service";
+      requires = optional enableIwd "iwd.service";
+      serviceConfig = {
+        Type = "dbus";
+        BusName = "net.connman";
+        Restart = "on-failure";
+        ExecStart = toString ([
+          "${cfg.package}/sbin/connmand"
+          "--config=${configFile}"
+          "--nodaemon"
+        ] ++ optional enableIwd "--wifi=iwd_agent"
+          ++ cfg.extraFlags);
+        StandardOutput = "null";
+      };
+    };
+
+    systemd.services.connman-vpn = mkIf cfg.enableVPN {
+      description = "ConnMan VPN service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "syslog.target" ];
+      before = [ "connman.service" ];
+      serviceConfig = {
+        Type = "dbus";
+        BusName = "net.connman.vpn";
+        ExecStart = "${cfg.package}/sbin/connman-vpnd -n";
+        StandardOutput = "null";
+      };
+    };
+
+    systemd.services.net-connman-vpn = mkIf cfg.enableVPN {
+      description = "D-BUS Service";
+      serviceConfig = {
+        Name = "net.connman.vpn";
+        before = [ "connman.service" ];
+        ExecStart = "${cfg.package}/sbin/connman-vpnd -n";
+        User = "root";
+        SystemdService = "connman-vpn.service";
+      };
+    };
+
+    networking = {
+      useDHCP = false;
+      wireless = {
+        enable = mkIf (!enableIwd) true;
+        dbusControlled = true;
+        iwd = mkIf enableIwd {
+          enable = true;
+        };
+      };
+      networkmanager.enable = false;
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/consul.nix b/nixos/modules/services/networking/consul.nix
new file mode 100644
index 00000000000..ca9c422e6d7
--- /dev/null
+++ b/nixos/modules/services/networking/consul.nix
@@ -0,0 +1,259 @@
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+let
+
+  dataDir = "/var/lib/consul";
+  cfg = config.services.consul;
+
+  configOptions = {
+    data_dir = dataDir;
+    ui_config = {
+      enabled = cfg.webUi;
+    };
+  } // cfg.extraConfig;
+
+  configFiles = [ "/etc/consul.json" "/etc/consul-addrs.json" ]
+    ++ cfg.extraConfigFiles;
+
+  devices = attrValues (filterAttrs (_: i: i != null) cfg.interface);
+  systemdDevices = forEach devices
+    (i: "sys-subsystem-net-devices-${utils.escapeSystemdPath i}.device");
+in
+{
+  options = {
+
+    services.consul = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enables the consul daemon.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.consul;
+        defaultText = literalExpression "pkgs.consul";
+        description = ''
+          The package used for the Consul agent and CLI.
+        '';
+      };
+
+
+      webUi = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enables the web interface on the consul http port.
+        '';
+      };
+
+      leaveOnStop = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If enabled, causes a leave action to be sent when closing consul.
+          This allows a clean termination of the node, but permanently removes
+          it from the cluster. You probably don't want this option unless you
+          are running a node which going offline in a permanent / semi-permanent
+          fashion.
+        '';
+      };
+
+      interface = {
+
+        advertise = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            The name of the interface to pull the advertise_addr from.
+          '';
+        };
+
+        bind = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            The name of the interface to pull the bind_addr from.
+          '';
+        };
+
+      };
+
+      forceIpv4 = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether we should force the interfaces to only pull ipv4 addresses.
+        '';
+      };
+
+      dropPrivileges = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether the consul agent should be run as a non-root consul user.
+        '';
+      };
+
+      extraConfig = mkOption {
+        default = { };
+        type = types.attrsOf types.anything;
+        description = ''
+          Extra configuration options which are serialized to json and added
+          to the config.json file.
+        '';
+      };
+
+      extraConfigFiles = mkOption {
+        default = [ ];
+        type = types.listOf types.str;
+        description = ''
+          Additional configuration files to pass to consul
+          NOTE: These will not trigger the service to be restarted when altered.
+        '';
+      };
+
+      alerts = {
+        enable = mkEnableOption "consul-alerts";
+
+        package = mkOption {
+          description = "Package to use for consul-alerts.";
+          default = pkgs.consul-alerts;
+          defaultText = literalExpression "pkgs.consul-alerts";
+          type = types.package;
+        };
+
+        listenAddr = mkOption {
+          description = "Api listening address.";
+          default = "localhost:9000";
+          type = types.str;
+        };
+
+        consulAddr = mkOption {
+          description = "Consul api listening adddress";
+          default = "localhost:8500";
+          type = types.str;
+        };
+
+        watchChecks = mkOption {
+          description = "Whether to enable check watcher.";
+          default = true;
+          type = types.bool;
+        };
+
+        watchEvents = mkOption {
+          description = "Whether to enable event watcher.";
+          default = true;
+          type = types.bool;
+        };
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable (
+    mkMerge [{
+
+      users.users.consul = {
+        description = "Consul agent daemon user";
+        isSystemUser = true;
+        group = "consul";
+        # The shell is needed for health checks
+        shell = "/run/current-system/sw/bin/bash";
+      };
+      users.groups.consul = {};
+
+      environment = {
+        etc."consul.json".text = builtins.toJSON configOptions;
+        # We need consul.d to exist for consul to start
+        etc."consul.d/dummy.json".text = "{ }";
+        systemPackages = [ cfg.package ];
+      };
+
+      systemd.services.consul = {
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ] ++ systemdDevices;
+        bindsTo = systemdDevices;
+        restartTriggers = [ config.environment.etc."consul.json".source ]
+          ++ mapAttrsToList (_: d: d.source)
+            (filterAttrs (n: _: hasPrefix "consul.d/" n) config.environment.etc);
+
+        serviceConfig = {
+          ExecStart = "@${cfg.package}/bin/consul consul agent -config-dir /etc/consul.d"
+            + concatMapStrings (n: " -config-file ${n}") configFiles;
+          ExecReload = "${cfg.package}/bin/consul reload";
+          PermissionsStartOnly = true;
+          User = if cfg.dropPrivileges then "consul" else null;
+          Restart = "on-failure";
+          TimeoutStartSec = "infinity";
+        } // (optionalAttrs (cfg.leaveOnStop) {
+          ExecStop = "${cfg.package}/bin/consul leave";
+        });
+
+        path = with pkgs; [ iproute2 gnugrep gawk consul ];
+        preStart = ''
+          mkdir -m 0700 -p ${dataDir}
+          chown -R consul ${dataDir}
+
+          # Determine interface addresses
+          getAddrOnce () {
+            ip addr show dev "$1" \
+              | grep 'inet${optionalString (cfg.forceIpv4) " "}.*scope global' \
+              | awk -F '[ /\t]*' '{print $3}' | head -n 1
+          }
+          getAddr () {
+            ADDR="$(getAddrOnce $1)"
+            LEFT=60 # Die after 1 minute
+            while [ -z "$ADDR" ]; do
+              sleep 1
+              LEFT=$(expr $LEFT - 1)
+              if [ "$LEFT" -eq "0" ]; then
+                echo "Address lookup timed out"
+                exit 1
+              fi
+              ADDR="$(getAddrOnce $1)"
+            done
+            echo "$ADDR"
+          }
+          echo "{" > /etc/consul-addrs.json
+          delim=" "
+        ''
+        + concatStrings (flip mapAttrsToList cfg.interface (name: i:
+          optionalString (i != null) ''
+            echo "$delim \"${name}_addr\": \"$(getAddr "${i}")\"" >> /etc/consul-addrs.json
+            delim=","
+          ''))
+        + ''
+          echo "}" >> /etc/consul-addrs.json
+        '';
+      };
+    }
+
+    (mkIf (cfg.alerts.enable) {
+      systemd.services.consul-alerts = {
+        wantedBy = [ "multi-user.target" ];
+        after = [ "consul.service" ];
+
+        path = [ cfg.package ];
+
+        serviceConfig = {
+          ExecStart = ''
+            ${cfg.alerts.package}/bin/consul-alerts start \
+              --alert-addr=${cfg.alerts.listenAddr} \
+              --consul-addr=${cfg.alerts.consulAddr} \
+              ${optionalString cfg.alerts.watchChecks "--watch-checks"} \
+              ${optionalString cfg.alerts.watchEvents "--watch-events"}
+          '';
+          User = if cfg.dropPrivileges then "consul" else null;
+          Restart = "on-failure";
+        };
+      };
+    })
+
+  ]);
+}
diff --git a/nixos/modules/services/networking/coredns.nix b/nixos/modules/services/networking/coredns.nix
new file mode 100644
index 00000000000..88615d8e610
--- /dev/null
+++ b/nixos/modules/services/networking/coredns.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.coredns;
+  configFile = pkgs.writeText "Corefile" cfg.config;
+in {
+  options.services.coredns = {
+    enable = mkEnableOption "Coredns dns server";
+
+    config = mkOption {
+      default = "";
+      example = ''
+        . {
+          whoami
+        }
+      '';
+      type = types.lines;
+      description = "Verbatim Corefile to use. See <link xlink:href=\"https://coredns.io/manual/toc/#configuration\"/> for details.";
+    };
+
+    package = mkOption {
+      default = pkgs.coredns;
+      defaultText = literalExpression "pkgs.coredns";
+      type = types.package;
+      description = "Coredns package to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.coredns = {
+      description = "Coredns dns server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        PermissionsStartOnly = true;
+        LimitNPROC = 512;
+        LimitNOFILE = 1048576;
+        CapabilityBoundingSet = "cap_net_bind_service";
+        AmbientCapabilities = "cap_net_bind_service";
+        NoNewPrivileges = true;
+        DynamicUser = true;
+        ExecStart = "${getBin cfg.package}/bin/coredns -conf=${configFile}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -SIGUSR1 $MAINPID";
+        Restart = "on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/corerad.nix b/nixos/modules/services/networking/corerad.nix
new file mode 100644
index 00000000000..9d79d5d7686
--- /dev/null
+++ b/nixos/modules/services/networking/corerad.nix
@@ -0,0 +1,82 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.corerad;
+  settingsFormat = pkgs.formats.toml {};
+
+in {
+  meta.maintainers = with maintainers; [ mdlayher ];
+
+  options.services.corerad = {
+    enable = mkEnableOption "CoreRAD IPv6 NDP RA daemon";
+
+    settings = mkOption {
+      type = settingsFormat.type;
+      example = literalExpression ''
+        {
+          interfaces = [
+            # eth0 is an upstream interface monitoring for IPv6 router advertisements.
+            {
+              name = "eth0";
+              monitor = true;
+            }
+            # eth1 is a downstream interface advertising IPv6 prefixes for SLAAC.
+            {
+              name = "eth1";
+              advertise = true;
+              prefix = [{ prefix = "::/64"; }];
+            }
+          ];
+          # Optionally enable Prometheus metrics.
+          debug = {
+            address = "localhost:9430";
+            prometheus = true;
+          };
+        }
+      '';
+      description = ''
+        Configuration for CoreRAD, see <link xlink:href="https://github.com/mdlayher/corerad/blob/main/internal/config/reference.toml"/>
+        for supported values. Ignored if configFile is set.
+      '';
+    };
+
+    configFile = mkOption {
+      type = types.path;
+      example = literalExpression ''"''${pkgs.corerad}/etc/corerad/corerad.toml"'';
+      description = "Path to CoreRAD TOML configuration file.";
+    };
+
+    package = mkOption {
+      default = pkgs.corerad;
+      defaultText = literalExpression "pkgs.corerad";
+      type = types.package;
+      description = "CoreRAD package to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # Prefer the config file over settings if both are set.
+    services.corerad.configFile = mkDefault (settingsFormat.generate "corerad.toml" cfg.settings);
+
+    systemd.services.corerad = {
+      description = "CoreRAD IPv6 NDP RA daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        LimitNPROC = 512;
+        LimitNOFILE = 1048576;
+        CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW";
+        AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_RAW";
+        NoNewPrivileges = true;
+        DynamicUser = true;
+        Type = "notify";
+        NotifyAccess = "main";
+        ExecStart = "${getBin cfg.package}/bin/corerad -c=${cfg.configFile}";
+        Restart = "on-failure";
+        RestartKillSignal = "SIGHUP";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/coturn.nix b/nixos/modules/services/networking/coturn.nix
new file mode 100644
index 00000000000..ce563c31136
--- /dev/null
+++ b/nixos/modules/services/networking/coturn.nix
@@ -0,0 +1,365 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.coturn;
+  pidfile = "/run/turnserver/turnserver.pid";
+  configFile = pkgs.writeText "turnserver.conf" ''
+listening-port=${toString cfg.listening-port}
+tls-listening-port=${toString cfg.tls-listening-port}
+alt-listening-port=${toString cfg.alt-listening-port}
+alt-tls-listening-port=${toString cfg.alt-tls-listening-port}
+${concatStringsSep "\n" (map (x: "listening-ip=${x}") cfg.listening-ips)}
+${concatStringsSep "\n" (map (x: "relay-ip=${x}") cfg.relay-ips)}
+min-port=${toString cfg.min-port}
+max-port=${toString cfg.max-port}
+${lib.optionalString cfg.lt-cred-mech "lt-cred-mech"}
+${lib.optionalString cfg.no-auth "no-auth"}
+${lib.optionalString cfg.use-auth-secret "use-auth-secret"}
+${lib.optionalString (cfg.static-auth-secret != null) ("static-auth-secret=${cfg.static-auth-secret}")}
+${lib.optionalString (cfg.static-auth-secret-file != null) ("static-auth-secret=#static-auth-secret#")}
+realm=${cfg.realm}
+${lib.optionalString cfg.no-udp "no-udp"}
+${lib.optionalString cfg.no-tcp "no-tcp"}
+${lib.optionalString cfg.no-tls "no-tls"}
+${lib.optionalString cfg.no-dtls "no-dtls"}
+${lib.optionalString cfg.no-udp-relay "no-udp-relay"}
+${lib.optionalString cfg.no-tcp-relay "no-tcp-relay"}
+${lib.optionalString (cfg.cert != null) "cert=${cfg.cert}"}
+${lib.optionalString (cfg.pkey != null) "pkey=${cfg.pkey}"}
+${lib.optionalString (cfg.dh-file != null) ("dh-file=${cfg.dh-file}")}
+no-stdout-log
+syslog
+pidfile=${pidfile}
+${lib.optionalString cfg.secure-stun "secure-stun"}
+${lib.optionalString cfg.no-cli "no-cli"}
+cli-ip=${cfg.cli-ip}
+cli-port=${toString cfg.cli-port}
+${lib.optionalString (cfg.cli-password != null) ("cli-password=${cfg.cli-password}")}
+${cfg.extraConfig}
+'';
+in {
+  options = {
+    services.coturn = {
+      enable = mkEnableOption "coturn TURN server";
+      listening-port = mkOption {
+        type = types.int;
+        default = 3478;
+        description = ''
+          TURN listener port for UDP and TCP.
+          Note: actually, TLS and DTLS sessions can connect to the
+          "plain" TCP and UDP port(s), too - if allowed by configuration.
+        '';
+      };
+      tls-listening-port = mkOption {
+        type = types.int;
+        default = 5349;
+        description = ''
+          TURN listener port for TLS.
+          Note: actually, "plain" TCP and UDP sessions can connect to the TLS and
+          DTLS port(s), too - if allowed by configuration. The TURN server
+          "automatically" recognizes the type of traffic. Actually, two listening
+          endpoints (the "plain" one and the "tls" one) are equivalent in terms of
+          functionality; but we keep both endpoints to satisfy the RFC 5766 specs.
+          For secure TCP connections, we currently support SSL version 3 and
+          TLS version 1.0, 1.1 and 1.2.
+          For secure UDP connections, we support DTLS version 1.
+        '';
+      };
+      alt-listening-port = mkOption {
+        type = types.int;
+        default = cfg.listening-port + 1;
+        defaultText = literalExpression "listening-port + 1";
+        description = ''
+          Alternative listening port for UDP and TCP listeners;
+          default (or zero) value means "listening port plus one".
+          This is needed for RFC 5780 support
+          (STUN extension specs, NAT behavior discovery). The TURN Server
+          supports RFC 5780 only if it is started with more than one
+          listening IP address of the same family (IPv4 or IPv6).
+          RFC 5780 is supported only by UDP protocol, other protocols
+          are listening to that endpoint only for "symmetry".
+        '';
+      };
+      alt-tls-listening-port = mkOption {
+        type = types.int;
+        default = cfg.tls-listening-port + 1;
+        defaultText = literalExpression "tls-listening-port + 1";
+        description = ''
+          Alternative listening port for TLS and DTLS protocols.
+        '';
+      };
+      listening-ips = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "203.0.113.42" "2001:DB8::42" ];
+        description = ''
+          Listener IP addresses of relay server.
+          If no IP(s) specified in the config file or in the command line options,
+          then all IPv4 and IPv6 system IPs will be used for listening.
+        '';
+      };
+      relay-ips = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "203.0.113.42" "2001:DB8::42" ];
+        description = ''
+          Relay address (the local IP address that will be used to relay the
+          packets to the peer).
+          Multiple relay addresses may be used.
+          The same IP(s) can be used as both listening IP(s) and relay IP(s).
+
+          If no relay IP(s) specified, then the turnserver will apply the default
+          policy: it will decide itself which relay addresses to be used, and it
+          will always be using the client socket IP address as the relay IP address
+          of the TURN session (if the requested relay address family is the same
+          as the family of the client socket).
+        '';
+      };
+      min-port = mkOption {
+        type = types.int;
+        default = 49152;
+        description = ''
+          Lower bound of UDP relay endpoints
+        '';
+      };
+      max-port = mkOption {
+        type = types.int;
+        default = 65535;
+        description = ''
+          Upper bound of UDP relay endpoints
+        '';
+      };
+      lt-cred-mech = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Use long-term credential mechanism.
+        '';
+      };
+      no-auth = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          This option is opposite to lt-cred-mech.
+          (TURN Server with no-auth option allows anonymous access).
+          If neither option is defined, and no users are defined,
+          then no-auth is default. If at least one user is defined,
+          in this file or in command line or in usersdb file, then
+          lt-cred-mech is default.
+        '';
+      };
+      use-auth-secret = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          TURN REST API flag.
+          Flag that sets a special authorization option that is based upon authentication secret.
+          This feature can be used with the long-term authentication mechanism, only.
+          This feature purpose is to support "TURN Server REST API", see
+          "TURN REST API" link in the project's page
+          https://github.com/coturn/coturn/
+
+          This option is used with timestamp:
+
+          usercombo -> "timestamp:userid"
+          turn user -> usercombo
+          turn password -> base64(hmac(secret key, usercombo))
+
+          This allows TURN credentials to be accounted for a specific user id.
+          If you don't have a suitable id, the timestamp alone can be used.
+          This option is just turning on secret-based authentication.
+          The actual value of the secret is defined either by option static-auth-secret,
+          or can be found in the turn_secret table in the database.
+        '';
+      };
+      static-auth-secret = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          'Static' authentication secret value (a string) for TURN REST API only.
+          If not set, then the turn server
+          will try to use the 'dynamic' value in turn_secret table
+          in user database (if present). The database-stored  value can be changed on-the-fly
+          by a separate program, so this is why that other mode is 'dynamic'.
+        '';
+      };
+      static-auth-secret-file = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Path to the file containing the static authentication secret.
+        '';
+      };
+      realm = mkOption {
+        type = types.str;
+        default = config.networking.hostName;
+        defaultText = literalExpression "config.networking.hostName";
+        example = "example.com";
+        description = ''
+          The default realm to be used for the users when no explicit
+          origin/realm relationship was found in the database, or if the TURN
+          server is not using any database (just the commands-line settings
+          and the userdb file). Must be used with long-term credentials
+          mechanism or with TURN REST API.
+        '';
+      };
+      cert = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/var/lib/acme/example.com/fullchain.pem";
+        description = ''
+          Certificate file in PEM format.
+        '';
+      };
+      pkey = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/var/lib/acme/example.com/key.pem";
+        description = ''
+          Private key file in PEM format.
+        '';
+      };
+      dh-file = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Use custom DH TLS key, stored in PEM format in the file.
+        '';
+      };
+      secure-stun = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Require authentication of the STUN Binding request.
+          By default, the clients are allowed anonymous access to the STUN Binding functionality.
+        '';
+      };
+      no-cli = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Turn OFF the CLI support.
+        '';
+      };
+      cli-ip = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = ''
+          Local system IP address to be used for CLI server endpoint.
+        '';
+      };
+      cli-port = mkOption {
+        type = types.int;
+        default = 5766;
+        description = ''
+          CLI server port.
+        '';
+      };
+      cli-password = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          CLI access password.
+          For the security reasons, it is recommended to use the encrypted
+          for of the password (see the -P command in the turnadmin utility).
+        '';
+      };
+      no-udp = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Disable UDP client listener";
+      };
+      no-tcp = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Disable TCP client listener";
+      };
+      no-tls = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Disable TLS client listener";
+      };
+      no-dtls = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Disable DTLS client listener";
+      };
+      no-udp-relay = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Disable UDP relay endpoints";
+      };
+      no-tcp-relay = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Disable TCP relay endpoints";
+      };
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Additional configuration options";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable (mkMerge ([
+    { assertions = [
+      { assertion = cfg.static-auth-secret != null -> cfg.static-auth-secret-file == null ;
+        message = "static-auth-secret and static-auth-secret-file cannot be set at the same time";
+      }
+    ];}
+
+    {
+      users.users.turnserver =
+        { uid = config.ids.uids.turnserver;
+          group = "turnserver";
+          description = "coturn TURN server user";
+        };
+      users.groups.turnserver =
+        { gid = config.ids.gids.turnserver;
+          members = [ "turnserver" ];
+        };
+
+      systemd.services.coturn = let
+        runConfig = "/run/coturn/turnserver.cfg";
+      in {
+        description = "coturn TURN server";
+        after = [ "network-online.target" ];
+        wants = [ "network-online.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        unitConfig = {
+          Documentation = "man:coturn(1) man:turnadmin(1) man:turnserver(1)";
+        };
+
+        preStart = ''
+          cat ${configFile} > ${runConfig}
+          ${optionalString (cfg.static-auth-secret-file != null) ''
+            STATIC_AUTH_SECRET="$(head -n1 ${cfg.static-auth-secret-file} || :)"
+            sed -e "s,#static-auth-secret#,$STATIC_AUTH_SECRET,g" \
+              -i ${runConfig}
+          '' }
+          chmod 640 ${runConfig}
+        '';
+        serviceConfig = {
+          Type = "simple";
+          ExecStart = "${pkgs.coturn}/bin/turnserver -c ${runConfig}";
+          RuntimeDirectory = "turnserver";
+          User = "turnserver";
+          Group = "turnserver";
+          AmbientCapabilities =
+            mkIf (
+              cfg.listening-port < 1024 ||
+              cfg.alt-listening-port < 1024 ||
+              cfg.tls-listening-port < 1024 ||
+              cfg.alt-tls-listening-port < 1024 ||
+              cfg.min-port < 1024
+            ) "cap_net_bind_service";
+          Restart = "on-abort";
+        };
+      };
+    systemd.tmpfiles.rules = [
+      "d  /run/coturn 0700 turnserver turnserver - -"
+    ];
+  }]));
+}
diff --git a/nixos/modules/services/networking/croc.nix b/nixos/modules/services/networking/croc.nix
new file mode 100644
index 00000000000..d044979e10d
--- /dev/null
+++ b/nixos/modules/services/networking/croc.nix
@@ -0,0 +1,86 @@
+{ config, lib, pkgs, ... }:
+let
+  inherit (lib) types;
+  cfg = config.services.croc;
+  rootDir = "/run/croc";
+in
+{
+  options.services.croc = {
+    enable = lib.mkEnableOption "croc relay";
+    ports = lib.mkOption {
+      type = with types; listOf port;
+      default = [9009 9010 9011 9012 9013];
+      description = "Ports of the relay.";
+    };
+    pass = lib.mkOption {
+      type = with types; either path str;
+      default = "pass123";
+      description = "Password or passwordfile for the relay.";
+    };
+    openFirewall = lib.mkEnableOption "opening of the peer port(s) in the firewall";
+    debug = lib.mkEnableOption "debug logs";
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.croc = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.croc}/bin/croc --pass '${cfg.pass}' ${lib.optionalString cfg.debug "--debug"} relay --ports ${lib.concatMapStringsSep "," toString cfg.ports}";
+        # The following options are only for optimizing:
+        # systemd-analyze security croc
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+        DynamicUser = true;
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        MountAPIVFS = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateNetwork = lib.mkDefault false;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RootDirectory = rootDir;
+        # Avoid mounting rootDir in the own rootDir of ExecStart='s mount namespace.
+        InaccessiblePaths = [ "-+${rootDir}" ];
+        BindReadOnlyPaths = [
+          builtins.storeDir
+        ] ++ lib.optional (types.path.check cfg.pass) cfg.pass;
+        # This is for BindReadOnlyPaths=
+        # to allow traversal of directories they create in RootDirectory=.
+        UMask = "0066";
+        # Create rootDir in the host's mount namespace.
+        RuntimeDirectory = [(baseNameOf rootDir)];
+        RuntimeDirectoryMode = "700";
+        SystemCallFilter = [
+          "@system-service"
+          "~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@setuid" "~@sync" "~@timer"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+      };
+    };
+
+    networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall cfg.ports;
+  };
+
+  meta.maintainers = with lib.maintainers; [ hax404 julm ];
+}
diff --git a/nixos/modules/services/networking/dante.nix b/nixos/modules/services/networking/dante.nix
new file mode 100644
index 00000000000..20d4faa1cdb
--- /dev/null
+++ b/nixos/modules/services/networking/dante.nix
@@ -0,0 +1,62 @@
+{ config, lib, pkgs, ... }:
+with lib;
+
+let
+  cfg = config.services.dante;
+  confFile = pkgs.writeText "dante-sockd.conf" ''
+    user.privileged: root
+    user.unprivileged: dante
+    logoutput: syslog
+
+    ${cfg.config}
+  '';
+in
+
+{
+  meta = {
+    maintainers = with maintainers; [ arobyn ];
+  };
+
+  options = {
+    services.dante = {
+      enable = mkEnableOption "Dante SOCKS proxy";
+
+      config = mkOption {
+        type        = types.lines;
+        description = ''
+          Contents of Dante's configuration file.
+          NOTE: user.privileged, user.unprivileged and logoutput are set by the service.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion   = cfg.config != "";
+        message     = "please provide Dante configuration file contents";
+      }
+    ];
+
+    users.users.dante = {
+      description   = "Dante SOCKS proxy daemon user";
+      isSystemUser  = true;
+      group         = "dante";
+    };
+    users.groups.dante = {};
+
+    systemd.services.dante = {
+      description   = "Dante SOCKS v4 and v5 compatible proxy server";
+      after         = [ "network-online.target" ];
+      wantedBy      = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type        = "simple";
+        ExecStart   = "${pkgs.dante}/bin/sockd -f ${confFile}";
+        ExecReload  = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        # Can crash sometimes; see https://github.com/NixOS/nixpkgs/pull/39005#issuecomment-381828708
+        Restart     = "on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/ddclient.nix b/nixos/modules/services/networking/ddclient.nix
new file mode 100644
index 00000000000..d025c8f8177
--- /dev/null
+++ b/nixos/modules/services/networking/ddclient.nix
@@ -0,0 +1,239 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.services.ddclient;
+  boolToStr = bool: if bool then "yes" else "no";
+  dataDir = "/var/lib/ddclient";
+  StateDirectory = builtins.baseNameOf dataDir;
+  RuntimeDirectory = StateDirectory;
+
+  configFile' = pkgs.writeText "ddclient.conf" ''
+    # This file can be used as a template for configFile or is automatically generated by Nix options.
+    cache=${dataDir}/ddclient.cache
+    foreground=YES
+    use=${cfg.use}
+    login=${cfg.username}
+    password=${lib.optionalString (cfg.protocol == "nsupdate") "/run/${RuntimeDirectory}/ddclient.key"}
+    protocol=${cfg.protocol}
+    ${lib.optionalString (cfg.script != "") "script=${cfg.script}"}
+    ${lib.optionalString (cfg.server != "") "server=${cfg.server}"}
+    ${lib.optionalString (cfg.zone != "")   "zone=${cfg.zone}"}
+    ssl=${boolToStr cfg.ssl}
+    wildcard=YES
+    ipv6=${boolToStr cfg.ipv6}
+    quiet=${boolToStr cfg.quiet}
+    verbose=${boolToStr cfg.verbose}
+    ${cfg.extraConfig}
+    ${lib.concatStringsSep "," cfg.domains}
+  '';
+  configFile = if (cfg.configFile != null) then cfg.configFile else configFile';
+
+  preStart = ''
+    install ${configFile} /run/${RuntimeDirectory}/ddclient.conf
+    ${lib.optionalString (cfg.configFile == null) (if (cfg.protocol == "nsupdate") then ''
+      install ${cfg.passwordFile} /run/${RuntimeDirectory}/ddclient.key
+    '' else if (cfg.passwordFile != null) then ''
+      password=$(printf "%q" "$(head -n 1 "${cfg.passwordFile}")")
+      sed -i "s|^password=$|password=$password|" /run/${RuntimeDirectory}/ddclient.conf
+    '' else ''
+      sed -i '/^password=$/d' /run/${RuntimeDirectory}/ddclient.conf
+    '')}
+  '';
+
+in
+
+with lib;
+
+{
+
+  imports = [
+    (mkChangedOptionModule [ "services" "ddclient" "domain" ] [ "services" "ddclient" "domains" ]
+      (config:
+        let value = getAttrFromPath [ "services" "ddclient" "domain" ] config;
+        in if value != "" then [ value ] else []))
+    (mkRemovedOptionModule [ "services" "ddclient" "homeDir" ] "")
+    (mkRemovedOptionModule [ "services" "ddclient" "password" ] "Use services.ddclient.passwordFile instead.")
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.ddclient = with lib.types; {
+
+      enable = mkOption {
+        default = false;
+        type = bool;
+        description = ''
+          Whether to synchronise your machine's IP address with a dynamic DNS provider (e.g. dyndns.org).
+        '';
+      };
+
+      package = mkOption {
+        type = package;
+        default = pkgs.ddclient;
+        defaultText = "pkgs.ddclient";
+        description = ''
+          The ddclient executable package run by the service.
+        '';
+      };
+
+      domains = mkOption {
+        default = [ "" ];
+        type = listOf str;
+        description = ''
+          Domain name(s) to synchronize.
+        '';
+      };
+
+      username = mkOption {
+        # For `nsupdate` username contains the path to the nsupdate executable
+        default = lib.optionalString (config.services.ddclient.protocol == "nsupdate") "${pkgs.bind.dnsutils}/bin/nsupdate";
+        defaultText = "";
+        type = str;
+        description = ''
+          User name.
+        '';
+      };
+
+      passwordFile = mkOption {
+        default = null;
+        type = nullOr str;
+        description = ''
+          A file containing the password or a TSIG key in named format when using the nsupdate protocol.
+        '';
+      };
+
+      interval = mkOption {
+        default = "10min";
+        type = str;
+        description = ''
+          The interval at which to run the check and update.
+          See <command>man 7 systemd.time</command> for the format.
+        '';
+      };
+
+      configFile = mkOption {
+        default = null;
+        type = nullOr path;
+        description = ''
+          Path to configuration file.
+          When set this overrides the generated configuration from module options.
+        '';
+        example = "/root/nixos/secrets/ddclient.conf";
+      };
+
+      protocol = mkOption {
+        default = "dyndns2";
+        type = str;
+        description = ''
+          Protocol to use with dynamic DNS provider (see https://sourceforge.net/p/ddclient/wiki/protocols).
+        '';
+      };
+
+      server = mkOption {
+        default = "";
+        type = str;
+        description = ''
+          Server address.
+        '';
+      };
+
+      ssl = mkOption {
+        default = true;
+        type = bool;
+        description = ''
+          Whether to use SSL/TLS to connect to dynamic DNS provider.
+        '';
+      };
+
+      ipv6 = mkOption {
+        default = false;
+        type = bool;
+        description = ''
+          Whether to use IPv6.
+        '';
+      };
+
+
+      quiet = mkOption {
+        default = false;
+        type = bool;
+        description = ''
+          Print no messages for unnecessary updates.
+        '';
+      };
+
+      script = mkOption {
+        default = "";
+        type = str;
+        description = ''
+          script as required by some providers.
+        '';
+      };
+
+      use = mkOption {
+        default = "web, web=checkip.dyndns.com/, web-skip='Current IP Address: '";
+        type = str;
+        description = ''
+          Method to determine the IP address to send to the dynamic DNS provider.
+        '';
+      };
+
+      verbose = mkOption {
+        default = true;
+        type = bool;
+        description = ''
+          Print verbose information.
+        '';
+      };
+
+      zone = mkOption {
+        default = "";
+        type = str;
+        description = ''
+          zone as required by some providers.
+        '';
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        type = lines;
+        description = ''
+          Extra configuration. Contents will be added verbatim to the configuration file.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.ddclient.enable {
+    systemd.services.ddclient = {
+      description = "Dynamic DNS Client";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      restartTriggers = optional (cfg.configFile != null) cfg.configFile;
+
+      serviceConfig = {
+        DynamicUser = true;
+        RuntimeDirectoryMode = "0700";
+        inherit RuntimeDirectory;
+        inherit StateDirectory;
+        Type = "oneshot";
+        ExecStartPre = "!${pkgs.writeShellScript "ddclient-prestart" preStart}";
+        ExecStart = "${lib.getBin cfg.package}/bin/ddclient -file /run/${RuntimeDirectory}/ddclient.conf";
+      };
+    };
+
+    systemd.timers.ddclient = {
+      description = "Run ddclient";
+      wantedBy = [ "timers.target" ];
+      timerConfig = {
+        OnBootSec = cfg.interval;
+        OnUnitInactiveSec = cfg.interval;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/dhcpcd.nix b/nixos/modules/services/networking/dhcpcd.nix
new file mode 100644
index 00000000000..3eb7ca99eaf
--- /dev/null
+++ b/nixos/modules/services/networking/dhcpcd.nix
@@ -0,0 +1,250 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  dhcpcd = if !config.boot.isContainer then pkgs.dhcpcd else pkgs.dhcpcd.override { udev = null; };
+
+  cfg = config.networking.dhcpcd;
+
+  interfaces = attrValues config.networking.interfaces;
+
+  enableDHCP = config.networking.dhcpcd.enable &&
+        (config.networking.useDHCP || any (i: i.useDHCP == true) interfaces);
+
+  # Don't start dhcpcd on explicitly configured interfaces or on
+  # interfaces that are part of a bridge, bond or sit device.
+  ignoredInterfaces =
+    map (i: i.name) (filter (i: if i.useDHCP != null then !i.useDHCP else i.ipv4.addresses != [ ]) interfaces)
+    ++ mapAttrsToList (i: _: i) config.networking.sits
+    ++ concatLists (attrValues (mapAttrs (n: v: v.interfaces) config.networking.bridges))
+    ++ flatten (concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues config.networking.vswitches))
+    ++ concatLists (attrValues (mapAttrs (n: v: v.interfaces) config.networking.bonds))
+    ++ config.networking.dhcpcd.denyInterfaces;
+
+  arrayAppendOrNull = a1: a2: if a1 == null && a2 == null then null
+    else if a1 == null then a2 else if a2 == null then a1
+      else a1 ++ a2;
+
+  # If dhcp is disabled but explicit interfaces are enabled,
+  # we need to provide dhcp just for those interfaces.
+  allowInterfaces = arrayAppendOrNull cfg.allowInterfaces
+    (if !config.networking.useDHCP && enableDHCP then
+      map (i: i.name) (filter (i: i.useDHCP == true) interfaces) else null);
+
+  # Config file adapted from the one that ships with dhcpcd.
+  dhcpcdConf = pkgs.writeText "dhcpcd.conf"
+    ''
+      # Inform the DHCP server of our hostname for DDNS.
+      hostname
+
+      # A list of options to request from the DHCP server.
+      option domain_name_servers, domain_name, domain_search, host_name
+      option classless_static_routes, ntp_servers, interface_mtu
+
+      # A ServerID is required by RFC2131.
+      # Commented out because of many non-compliant DHCP servers in the wild :(
+      #require dhcp_server_identifier
+
+      # A hook script is provided to lookup the hostname if not set by
+      # the DHCP server, but it should not be run by default.
+      nohook lookup-hostname
+
+      # Ignore peth* devices; on Xen, they're renamed physical
+      # Ethernet cards used for bridging.  Likewise for vif* and tap*
+      # (Xen) and virbr* and vnet* (libvirt).
+      denyinterfaces ${toString ignoredInterfaces} lo peth* vif* tap* tun* virbr* vnet* vboxnet* sit*
+
+      # Use the list of allowed interfaces if specified
+      ${optionalString (allowInterfaces != null) "allowinterfaces ${toString allowInterfaces}"}
+
+      # Immediately fork to background if specified, otherwise wait for IP address to be assigned
+      ${{
+        background = "background";
+        any = "waitip";
+        ipv4 = "waitip 4";
+        ipv6 = "waitip 6";
+        both = "waitip 4\nwaitip 6";
+        if-carrier-up = "";
+      }.${cfg.wait}}
+
+      ${optionalString (config.networking.enableIPv6 == false) ''
+        # Don't solicit or accept IPv6 Router Advertisements and DHCPv6 if disabled IPv6
+        noipv6
+      ''}
+
+      ${cfg.extraConfig}
+    '';
+
+  exitHook = pkgs.writeText "dhcpcd.exit-hook"
+    ''
+      if [ "$reason" = BOUND -o "$reason" = REBOOT ]; then
+          # Restart ntpd.  We need to restart it to make sure that it
+          # will actually do something: if ntpd cannot resolve the
+          # server hostnames in its config file, then it will never do
+          # anything ever again ("couldn't resolve ..., giving up on
+          # it"), so we silently lose time synchronisation. This also
+          # applies to openntpd.
+          /run/current-system/systemd/bin/systemctl try-reload-or-restart ntpd.service openntpd.service chronyd.service || true
+      fi
+
+      ${cfg.runHook}
+    '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    networking.dhcpcd.enable = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to enable dhcpcd for device configuration. This is mainly to
+        explicitly disable dhcpcd (for example when using networkd).
+      '';
+    };
+
+    networking.dhcpcd.persistent = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+          Whenever to leave interfaces configured on dhcpcd daemon
+          shutdown. Set to true if you have your root or store mounted
+          over the network or this machine accepts SSH connections
+          through DHCP interfaces and clients should be notified when
+          it shuts down.
+      '';
+    };
+
+    networking.dhcpcd.denyInterfaces = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+         Disable the DHCP client for any interface whose name matches
+         any of the shell glob patterns in this list. The purpose of
+         this option is to blacklist virtual interfaces such as those
+         created by Xen, libvirt, LXC, etc.
+      '';
+    };
+
+    networking.dhcpcd.allowInterfaces = mkOption {
+      type = types.nullOr (types.listOf types.str);
+      default = null;
+      description = ''
+         Enable the DHCP client for any interface whose name matches
+         any of the shell glob patterns in this list. Any interface not
+         explicitly matched by this pattern will be denied. This pattern only
+         applies when non-null.
+      '';
+    };
+
+    networking.dhcpcd.extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+         Literal string to append to the config file generated for dhcpcd.
+      '';
+    };
+
+    networking.dhcpcd.runHook = mkOption {
+      type = types.lines;
+      default = "";
+      example = "if [[ $reason =~ BOUND ]]; then echo $interface: Routers are $new_routers - were $old_routers; fi";
+      description = ''
+         Shell code that will be run after all other hooks. See
+         `man dhcpcd-run-hooks` for details on what is possible.
+      '';
+    };
+
+    networking.dhcpcd.wait = mkOption {
+      type = types.enum [ "background" "any" "ipv4" "ipv6" "both" "if-carrier-up" ];
+      default = "any";
+      description = ''
+        This option specifies when the dhcpcd service will fork to background.
+        If set to "background", dhcpcd will fork to background immediately.
+        If set to "ipv4" or "ipv6", dhcpcd will wait for the corresponding IP
+        address to be assigned. If set to "any", dhcpcd will wait for any type
+        (IPv4 or IPv6) to be assigned. If set to "both", dhcpcd will wait for
+        both an IPv4 and an IPv6 address before forking.
+        The option "if-carrier-up" is equivalent to "any" if either ethernet
+        is plugged nor WiFi is powered, and to "background" otherwise.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf enableDHCP {
+
+    assertions = [ {
+      # dhcpcd doesn't start properly with malloc ∉ [ libc scudo ]
+      # see https://github.com/NixOS/nixpkgs/issues/151696
+      assertion =
+        dhcpcd.enablePrivSep
+          -> elem config.environment.memoryAllocator.provider [ "libc" "scudo" ];
+      message = ''
+        dhcpcd with privilege separation is incompatible with chosen system malloc.
+          Currently only the `libc` and `scudo` allocators are known to work.
+          To disable dhcpcd's privilege separation, overlay Nixpkgs and override dhcpcd
+          to set `enablePrivSep = false`.
+      '';
+    } ];
+
+    systemd.services.dhcpcd = let
+      cfgN = config.networking;
+      hasDefaultGatewaySet = (cfgN.defaultGateway != null && cfgN.defaultGateway.address != "")
+                          && (!cfgN.enableIPv6 || (cfgN.defaultGateway6 != null && cfgN.defaultGateway6.address != ""));
+    in
+      { description = "DHCP Client";
+
+        wantedBy = [ "multi-user.target" ] ++ optional (!hasDefaultGatewaySet) "network-online.target";
+        wants = [ "network.target" ];
+        before = [ "network-online.target" ];
+
+        restartTriggers = [ exitHook ];
+
+        # Stopping dhcpcd during a reconfiguration is undesirable
+        # because it brings down the network interfaces configured by
+        # dhcpcd.  So do a "systemctl restart" instead.
+        stopIfChanged = false;
+
+        path = [ dhcpcd pkgs.nettools pkgs.openresolv ];
+
+        unitConfig.ConditionCapability = "CAP_NET_ADMIN";
+
+        serviceConfig =
+          { Type = "forking";
+            PIDFile = "/run/dhcpcd/pid";
+            RuntimeDirectory = "dhcpcd";
+            ExecStart = "@${dhcpcd}/sbin/dhcpcd dhcpcd --quiet ${optionalString cfg.persistent "--persistent"} --config ${dhcpcdConf}";
+            ExecReload = "${dhcpcd}/sbin/dhcpcd --rebind";
+            Restart = "always";
+          };
+      };
+
+    users.users.dhcpcd = {
+      isSystemUser = true;
+      group = "dhcpcd";
+    };
+    users.groups.dhcpcd = {};
+
+    environment.systemPackages = [ dhcpcd ];
+
+    environment.etc."dhcpcd.exit-hook".source = exitHook;
+
+    powerManagement.resumeCommands = mkIf config.systemd.services.dhcpcd.enable
+      ''
+        # Tell dhcpcd to rebind its interfaces if it's running.
+        /run/current-system/systemd/bin/systemctl reload dhcpcd.service
+      '';
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/dhcpd.nix b/nixos/modules/services/networking/dhcpd.nix
new file mode 100644
index 00000000000..3c4c0069dfd
--- /dev/null
+++ b/nixos/modules/services/networking/dhcpd.nix
@@ -0,0 +1,221 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg4 = config.services.dhcpd4;
+  cfg6 = config.services.dhcpd6;
+
+  writeConfig = cfg: pkgs.writeText "dhcpd.conf"
+    ''
+      default-lease-time 600;
+      max-lease-time 7200;
+      ${optionalString (!cfg.authoritative) "not "}authoritative;
+      ddns-update-style interim;
+      log-facility local1; # see dhcpd.nix
+
+      ${cfg.extraConfig}
+
+      ${lib.concatMapStrings
+          (machine: ''
+            host ${machine.hostName} {
+              hardware ethernet ${machine.ethernetAddress};
+              fixed-address ${machine.ipAddress};
+            }
+          '')
+          cfg.machines
+      }
+    '';
+
+  dhcpdService = postfix: cfg:
+    let
+      configFile =
+        if cfg.configFile != null
+          then cfg.configFile
+          else writeConfig cfg;
+      leaseFile = "/var/lib/dhcpd${postfix}/dhcpd.leases";
+      args = [
+        "@${pkgs.dhcp}/sbin/dhcpd" "dhcpd${postfix}" "-${postfix}"
+        "-pf" "/run/dhcpd${postfix}/dhcpd.pid"
+        "-cf" configFile
+        "-lf" leaseFile
+      ] ++ cfg.extraFlags
+        ++ cfg.interfaces;
+    in
+      optionalAttrs cfg.enable {
+        "dhcpd${postfix}" = {
+          description = "DHCPv${postfix} server";
+          wantedBy = [ "multi-user.target" ];
+          after = [ "network.target" ];
+
+          preStart = "touch ${leaseFile}";
+          serviceConfig = {
+            ExecStart = concatMapStringsSep " " escapeShellArg args;
+            Type = "forking";
+            Restart = "always";
+            DynamicUser = true;
+            User = "dhcpd";
+            Group = "dhcpd";
+            AmbientCapabilities = [
+              "CAP_NET_RAW"          # to send ICMP messages
+              "CAP_NET_BIND_SERVICE" # to bind on DHCP port (67)
+            ];
+            StateDirectory   = "dhcpd${postfix}";
+            RuntimeDirectory = "dhcpd${postfix}";
+            PIDFile = "/run/dhcpd${postfix}/dhcpd.pid";
+          };
+        };
+      };
+
+  machineOpts = { ... }: {
+
+    options = {
+
+      hostName = mkOption {
+        type = types.str;
+        example = "foo";
+        description = ''
+          Hostname which is assigned statically to the machine.
+        '';
+      };
+
+      ethernetAddress = mkOption {
+        type = types.str;
+        example = "00:16:76:9a:32:1d";
+        description = ''
+          MAC address of the machine.
+        '';
+      };
+
+      ipAddress = mkOption {
+        type = types.str;
+        example = "192.168.1.10";
+        description = ''
+          IP address of the machine.
+        '';
+      };
+
+    };
+  };
+
+  dhcpConfig = postfix: {
+
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable the DHCPv${postfix} server.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        option subnet-mask 255.255.255.0;
+        option broadcast-address 192.168.1.255;
+        option routers 192.168.1.5;
+        option domain-name-servers 130.161.158.4, 130.161.33.17, 130.161.180.1;
+        option domain-name "example.org";
+        subnet 192.168.1.0 netmask 255.255.255.0 {
+          range 192.168.1.100 192.168.1.200;
+        }
+      '';
+      description = ''
+        Extra text to be appended to the DHCP server configuration
+        file. Currently, you almost certainly need to specify something
+        there, such as the options specifying the subnet mask, DNS servers,
+        etc.
+      '';
+    };
+
+    extraFlags = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Additional command line flags to be passed to the dhcpd daemon.
+      '';
+    };
+
+    configFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        The path of the DHCP server configuration file.  If no file
+        is specified, a file is generated using the other options.
+      '';
+    };
+
+    interfaces = mkOption {
+      type = types.listOf types.str;
+      default = ["eth0"];
+      description = ''
+        The interfaces on which the DHCP server should listen.
+      '';
+    };
+
+    machines = mkOption {
+      type = with types; listOf (submodule machineOpts);
+      default = [];
+      example = [
+        { hostName = "foo";
+          ethernetAddress = "00:16:76:9a:32:1d";
+          ipAddress = "192.168.1.10";
+        }
+        { hostName = "bar";
+          ethernetAddress = "00:19:d1:1d:c4:9a";
+          ipAddress = "192.168.1.11";
+        }
+      ];
+      description = ''
+        A list mapping Ethernet addresses to IPv${postfix} addresses for the
+        DHCP server.
+      '';
+    };
+
+    authoritative = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether the DHCP server shall send DHCPNAK messages to misconfigured
+        clients. If this is not done, clients may be unable to get a correct
+        IP address after changing subnets until their old lease has expired.
+      '';
+    };
+
+  };
+
+in
+
+{
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "dhcpd" ] [ "services" "dhcpd4" ])
+  ] ++ flip map [ "4" "6" ] (postfix:
+    mkRemovedOptionModule [ "services" "dhcpd${postfix}" "stateDir" ] ''
+      The DHCP server state directory is now managed with the systemd's DynamicUser mechanism.
+      This means the directory is named after the service (dhcpd${postfix}), created under
+      /var/lib/private/ and symlinked to /var/lib/.
+    ''
+  );
+
+  ###### interface
+
+  options = {
+
+    services.dhcpd4 = dhcpConfig "4";
+    services.dhcpd6 = dhcpConfig "6";
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf (cfg4.enable || cfg6.enable) {
+
+    systemd.services = dhcpdService "4" cfg4 // dhcpdService "6" cfg6;
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/dnscache.nix b/nixos/modules/services/networking/dnscache.nix
new file mode 100644
index 00000000000..7452210de47
--- /dev/null
+++ b/nixos/modules/services/networking/dnscache.nix
@@ -0,0 +1,108 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dnscache;
+
+  dnscache-root = pkgs.runCommand "dnscache-root" { preferLocalBuild = true; } ''
+    mkdir -p $out/{servers,ip}
+
+    ${concatMapStrings (ip: ''
+      touch "$out/ip/"${lib.escapeShellArg ip}
+    '') cfg.clientIps}
+
+    ${concatStrings (mapAttrsToList (host: ips: ''
+      ${concatMapStrings (ip: ''
+        echo ${lib.escapeShellArg ip} >> "$out/servers/"${lib.escapeShellArg host}
+      '') ips}
+    '') cfg.domainServers)}
+
+    # if a list of root servers was not provided in config, copy it
+    # over. (this is also done by dnscache-conf, but we 'rm -rf
+    # /var/lib/dnscache/root' below & replace it wholesale with this,
+    # so we have to ensure servers/@ exists ourselves.)
+    if [ ! -e $out/servers/@ ]; then
+      # symlink does not work here, due chroot
+      cp ${pkgs.djbdns}/etc/dnsroots.global $out/servers/@;
+    fi
+  '';
+
+in {
+
+  ###### interface
+
+  options = {
+    services.dnscache = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to run the dnscache caching dns server.";
+      };
+
+      ip = mkOption {
+        default = "0.0.0.0";
+        type = types.str;
+        description = "IP address on which to listen for connections.";
+      };
+
+      clientIps = mkOption {
+        default = [ "127.0.0.1" ];
+        type = types.listOf types.str;
+        description = "Client IP addresses (or prefixes) from which to accept connections.";
+        example = ["192.168" "172.23.75.82"];
+      };
+
+      domainServers = mkOption {
+        default = { };
+        type = types.attrsOf (types.listOf types.str);
+        description = ''
+          Table of {hostname: server} pairs to use as authoritative servers for hosts (and subhosts).
+          If entry for @ is not specified predefined list of root servers is used.
+        '';
+        example = literalExpression ''
+          {
+            "@" = ["8.8.8.8" "8.8.4.4"];
+            "example.com" = ["192.168.100.100"];
+          }
+        '';
+      };
+
+      forwardOnly = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to treat root servers (for @) as caching
+          servers, requesting addresses the same way a client does. This is
+          needed if you want to use e.g. Google DNS as your upstream DNS.
+        '';
+      };
+
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf config.services.dnscache.enable {
+    environment.systemPackages = [ pkgs.djbdns ];
+    users.users.dnscache.isSystemUser = true;
+
+    systemd.services.dnscache = {
+      description = "djbdns dnscache server";
+      wantedBy = [ "multi-user.target" ];
+      path = with pkgs; [ bash daemontools djbdns ];
+      preStart = ''
+        rm -rf /var/lib/dnscache
+        dnscache-conf dnscache dnscache /var/lib/dnscache ${config.services.dnscache.ip}
+        rm -rf /var/lib/dnscache/root
+        ln -sf ${dnscache-root} /var/lib/dnscache/root
+      '';
+      script = ''
+        cd /var/lib/dnscache/
+        ${optionalString cfg.forwardOnly "export FORWARDONLY=1"}
+        exec ./run
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/dnscrypt-proxy2.nix b/nixos/modules/services/networking/dnscrypt-proxy2.nix
new file mode 100644
index 00000000000..316e6e37f9d
--- /dev/null
+++ b/nixos/modules/services/networking/dnscrypt-proxy2.nix
@@ -0,0 +1,124 @@
+{ config, lib, pkgs, ... }: with lib;
+
+let
+  cfg = config.services.dnscrypt-proxy2;
+in
+
+{
+  options.services.dnscrypt-proxy2 = {
+    enable = mkEnableOption "dnscrypt-proxy2";
+
+    settings = mkOption {
+      description = ''
+        Attrset that is converted and passed as TOML config file.
+        For available params, see: <link xlink:href="https://github.com/DNSCrypt/dnscrypt-proxy/blob/${pkgs.dnscrypt-proxy2.version}/dnscrypt-proxy/example-dnscrypt-proxy.toml"/>
+      '';
+      example = literalExpression ''
+        {
+          sources.public-resolvers = {
+            urls = [ "https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md" ];
+            cache_file = "public-resolvers.md";
+            minisign_key = "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3";
+            refresh_delay = 72;
+          };
+        }
+      '';
+      type = types.attrs;
+      default = {};
+    };
+
+    upstreamDefaults = mkOption {
+      description = ''
+        Whether to base the config declared in <option>services.dnscrypt-proxy2.settings</option> on the upstream example config (<link xlink:href="https://github.com/DNSCrypt/dnscrypt-proxy/blob/master/dnscrypt-proxy/example-dnscrypt-proxy.toml"/>)
+
+        Disable this if you want to declare your dnscrypt config from scratch.
+      '';
+      type = types.bool;
+      default = true;
+    };
+
+    configFile = mkOption {
+      description = ''
+        Path to TOML config file. See: <link xlink:href="https://github.com/DNSCrypt/dnscrypt-proxy/blob/master/dnscrypt-proxy/example-dnscrypt-proxy.toml"/>
+        If this option is set, it will override any configuration done in options.services.dnscrypt-proxy2.settings.
+      '';
+      example = "/etc/dnscrypt-proxy/dnscrypt-proxy.toml";
+      type = types.path;
+      default = pkgs.runCommand "dnscrypt-proxy.toml" {
+        json = builtins.toJSON cfg.settings;
+        passAsFile = [ "json" ];
+      } ''
+        ${if cfg.upstreamDefaults then ''
+          ${pkgs.remarshal}/bin/toml2json ${pkgs.dnscrypt-proxy2.src}/dnscrypt-proxy/example-dnscrypt-proxy.toml > example.json
+          ${pkgs.jq}/bin/jq --slurp add example.json $jsonPath > config.json # merges the two
+        '' else ''
+          cp $jsonPath config.json
+        ''}
+        ${pkgs.remarshal}/bin/json2toml < config.json > $out
+      '';
+      defaultText = literalDocBook "TOML file generated from <option>services.dnscrypt-proxy2.settings</option>";
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    networking.nameservers = lib.mkDefault [ "127.0.0.1" ];
+
+    systemd.services.dnscrypt-proxy2 = {
+      description = "DNSCrypt-proxy client";
+      wants = [
+        "network-online.target"
+        "nss-lookup.target"
+      ];
+      before = [
+        "nss-lookup.target"
+      ];
+      wantedBy = [
+        "multi-user.target"
+      ];
+      serviceConfig = {
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        CacheDirectory = "dnscrypt-proxy";
+        DynamicUser = true;
+        ExecStart = "${pkgs.dnscrypt-proxy2}/bin/dnscrypt-proxy -config ${cfg.configFile}";
+        LockPersonality = true;
+        LogsDirectory = "dnscrypt-proxy";
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        NonBlocking = true;
+        PrivateDevices = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        Restart = "always";
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RuntimeDirectory = "dnscrypt-proxy";
+        StateDirectory = "dnscrypt-proxy";
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "@chown"
+          "~@aio"
+          "~@keyring"
+          "~@memlock"
+          "~@resources"
+          "~@setuid"
+          "~@timer"
+        ];
+      };
+    };
+  };
+
+  # uses attributes of the linked package
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/networking/dnscrypt-wrapper.nix b/nixos/modules/services/networking/dnscrypt-wrapper.nix
new file mode 100644
index 00000000000..c2add170e9c
--- /dev/null
+++ b/nixos/modules/services/networking/dnscrypt-wrapper.nix
@@ -0,0 +1,286 @@
+{ config, lib, pkgs, ... }:
+with lib;
+
+let
+  cfg     = config.services.dnscrypt-wrapper;
+  dataDir = "/var/lib/dnscrypt-wrapper";
+
+  mkPath = path: default:
+    if path != null
+      then toString path
+      else default;
+
+  publicKey = mkPath cfg.providerKey.public "${dataDir}/public.key";
+  secretKey = mkPath cfg.providerKey.secret "${dataDir}/secret.key";
+
+  daemonArgs = with cfg; [
+    "--listen-address=${address}:${toString port}"
+    "--resolver-address=${upstream.address}:${toString upstream.port}"
+    "--provider-name=${providerName}"
+    "--provider-publickey-file=${publicKey}"
+    "--provider-secretkey-file=${secretKey}"
+    "--provider-cert-file=${providerName}.crt"
+    "--crypt-secretkey-file=${providerName}.key"
+  ];
+
+  genKeys = ''
+    # generates time-limited keypairs
+    keyGen() {
+      dnscrypt-wrapper --gen-crypt-keypair \
+        --crypt-secretkey-file=${cfg.providerName}.key
+
+      dnscrypt-wrapper --gen-cert-file \
+        --crypt-secretkey-file=${cfg.providerName}.key \
+        --provider-cert-file=${cfg.providerName}.crt \
+        --provider-publickey-file=${publicKey} \
+        --provider-secretkey-file=${secretKey} \
+        --cert-file-expire-days=${toString cfg.keys.expiration}
+    }
+
+    cd ${dataDir}
+
+    # generate provider keypair (first run only)
+    ${optionalString (cfg.providerKey.public == null || cfg.providerKey.secret == null) ''
+      if [ ! -f ${publicKey} ] || [ ! -f ${secretKey} ]; then
+        dnscrypt-wrapper --gen-provider-keypair
+      fi
+    ''}
+
+    # generate new keys for rotation
+    if [ ! -f ${cfg.providerName}.key ] || [ ! -f ${cfg.providerName}.crt ]; then
+      keyGen
+    fi
+  '';
+
+  rotateKeys = ''
+    # check if keys are not expired
+    keyValid() {
+      fingerprint=$(dnscrypt-wrapper \
+        --show-provider-publickey \
+        --provider-publickey-file=${publicKey} \
+        | awk '{print $(NF)}')
+      dnscrypt-proxy --test=${toString (cfg.keys.checkInterval + 1)} \
+        --resolver-address=127.0.0.1:${toString cfg.port} \
+        --provider-name=${cfg.providerName} \
+        --provider-key=$fingerprint
+    }
+
+    cd ${dataDir}
+
+    # archive old keys and restart the service
+    if ! keyValid; then
+      echo "certificate soon to become invalid; backing up old cert"
+      mkdir -p oldkeys
+      mv -v ${cfg.providerName}.key oldkeys/${cfg.providerName}-$(date +%F-%T).key
+      mv -v ${cfg.providerName}.crt oldkeys/${cfg.providerName}-$(date +%F-%T).crt
+      systemctl restart dnscrypt-wrapper
+    fi
+  '';
+
+
+  # This is the fork of the original dnscrypt-proxy maintained by Dyne.org.
+  # dnscrypt-proxy2 doesn't provide the `--test` feature that is needed to
+  # correctly implement key rotation of dnscrypt-wrapper ephemeral keys.
+  dnscrypt-proxy1 = pkgs.callPackage
+    ({ stdenv, fetchFromGitHub, autoreconfHook
+    , pkg-config, libsodium, ldns, openssl, systemd }:
+
+    stdenv.mkDerivation rec {
+      pname = "dnscrypt-proxy";
+      version = "2019-08-20";
+
+      src = fetchFromGitHub {
+        owner = "dyne";
+        repo = "dnscrypt-proxy";
+        rev = "07ac3825b5069adc28e2547c16b1d983a8ed8d80";
+        sha256 = "0c4mq741q4rpmdn09agwmxap32kf0vgfz7pkhcdc5h54chc3g3xy";
+      };
+
+      configureFlags = optional stdenv.isLinux "--with-systemd";
+
+      nativeBuildInputs = [ autoreconfHook pkg-config ];
+
+      # <ldns/ldns.h> depends on <openssl/ssl.h>
+      buildInputs = [ libsodium openssl.dev ldns ] ++ optional stdenv.isLinux systemd;
+
+      postInstall = ''
+        # Previous versions required libtool files to load plugins; they are
+        # now strictly optional.
+        rm $out/lib/dnscrypt-proxy/*.la
+      '';
+
+      meta = {
+        description = "A tool for securing communications between a client and a DNS resolver";
+        homepage = "https://github.com/dyne/dnscrypt-proxy";
+        license = licenses.isc;
+        maintainers = with maintainers; [ rnhmjoj ];
+        platforms = platforms.linux;
+      };
+    }) { };
+
+in {
+
+
+  ###### interface
+
+  options.services.dnscrypt-wrapper = {
+    enable = mkEnableOption "DNSCrypt wrapper";
+
+    address = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = ''
+        The DNSCrypt wrapper will bind to this IP address.
+      '';
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = 5353;
+      description = ''
+        The DNSCrypt wrapper will listen for DNS queries on this port.
+      '';
+    };
+
+    providerName = mkOption {
+      type = types.str;
+      default = "2.dnscrypt-cert.${config.networking.hostName}";
+      defaultText = literalExpression ''"2.dnscrypt-cert.''${config.networking.hostName}"'';
+      example = "2.dnscrypt-cert.myresolver";
+      description = ''
+        The name that will be given to this DNSCrypt resolver.
+        Note: the resolver name must start with <literal>2.dnscrypt-cert.</literal>.
+      '';
+    };
+
+    providerKey.public = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/etc/secrets/public.key";
+      description = ''
+        The filepath to the provider public key. If not given a new
+        provider key pair will be generated on the first run.
+      '';
+    };
+
+    providerKey.secret = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/etc/secrets/secret.key";
+      description = ''
+        The filepath to the provider secret key. If not given a new
+        provider key pair will be generated on the first run.
+      '';
+    };
+
+    upstream.address = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = ''
+        The IP address of the upstream DNS server DNSCrypt will "wrap".
+      '';
+    };
+
+    upstream.port = mkOption {
+      type = types.int;
+      default = 53;
+      description = ''
+        The port of the upstream DNS server DNSCrypt will "wrap".
+      '';
+    };
+
+    keys.expiration = mkOption {
+      type = types.int;
+      default = 30;
+      description = ''
+        The duration (in days) of the time-limited secret key.
+        This will be automatically rotated before expiration.
+      '';
+    };
+
+    keys.checkInterval = mkOption {
+      type = types.int;
+      default = 1440;
+      description = ''
+        The time interval (in minutes) between key expiration checks.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.dnscrypt-wrapper = {
+      description = "dnscrypt-wrapper daemon user";
+      home = "${dataDir}";
+      createHome = true;
+      isSystemUser = true;
+      group = "dnscrypt-wrapper";
+    };
+    users.groups.dnscrypt-wrapper = { };
+
+    security.polkit.extraConfig = ''
+      // Allow dnscrypt-wrapper user to restart dnscrypt-wrapper.service
+      polkit.addRule(function(action, subject) {
+          if (action.id == "org.freedesktop.systemd1.manage-units" &&
+              action.lookup("unit") == "dnscrypt-wrapper.service" &&
+              subject.user == "dnscrypt-wrapper") {
+              return polkit.Result.YES;
+          }
+        });
+    '';
+
+    systemd.services.dnscrypt-wrapper = {
+      description = "dnscrypt-wrapper daemon";
+      after    = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path     = [ pkgs.dnscrypt-wrapper ];
+
+      serviceConfig = {
+        User = "dnscrypt-wrapper";
+        WorkingDirectory = dataDir;
+        Restart   = "on-failure";
+        ExecStart = "${pkgs.dnscrypt-wrapper}/bin/dnscrypt-wrapper ${toString daemonArgs}";
+      };
+
+      preStart = genKeys;
+    };
+
+
+    systemd.services.dnscrypt-wrapper-rotate = {
+      after    = [ "network.target" ];
+      requires = [ "dnscrypt-wrapper.service" ];
+      description = "Rotates DNSCrypt wrapper keys if soon to expire";
+
+      path   = with pkgs; [ dnscrypt-wrapper dnscrypt-proxy1 gawk ];
+      script = rotateKeys;
+      serviceConfig.User = "dnscrypt-wrapper";
+    };
+
+
+    systemd.timers.dnscrypt-wrapper-rotate = {
+      description = "Periodically check DNSCrypt wrapper keys for expiration";
+      wantedBy = [ "multi-user.target" ];
+
+      timerConfig = {
+        Unit = "dnscrypt-wrapper-rotate.service";
+        OnBootSec = "1min";
+        OnUnitActiveSec = cfg.keys.checkInterval * 60;
+      };
+    };
+
+    assertions = with cfg; [
+      { assertion = (providerKey.public == null && providerKey.secret == null) ||
+                    (providerKey.secret != null && providerKey.public != null);
+        message = "The secret and public provider key must be set together.";
+      }
+    ];
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+}
diff --git a/nixos/modules/services/networking/dnsdist.nix b/nixos/modules/services/networking/dnsdist.nix
new file mode 100644
index 00000000000..c7c6a79864c
--- /dev/null
+++ b/nixos/modules/services/networking/dnsdist.nix
@@ -0,0 +1,53 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dnsdist;
+  configFile = pkgs.writeText "dnsdist.conf" ''
+    setLocal('${cfg.listenAddress}:${toString cfg.listenPort}')
+    ${cfg.extraConfig}
+  '';
+in {
+  options = {
+    services.dnsdist = {
+      enable = mkEnableOption "dnsdist domain name server";
+
+      listenAddress = mkOption {
+        type = types.str;
+        description = "Listen IP Address";
+        default = "0.0.0.0";
+      };
+      listenPort = mkOption {
+        type = types.int;
+        description = "Listen port";
+        default = 53;
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra lines to be added verbatim to dnsdist.conf.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.packages = [ pkgs.dnsdist ];
+
+    systemd.services.dnsdist = {
+      wantedBy = [ "multi-user.target" ];
+
+      startLimitIntervalSec = 0;
+      serviceConfig = {
+        DynamicUser = true;
+
+        # upstream overrides for better nixos compatibility
+        ExecStartPre = [ "" "${pkgs.dnsdist}/bin/dnsdist --check-config --config ${configFile}" ];
+        ExecStart = [ "" "${pkgs.dnsdist}/bin/dnsdist --supervised --disable-syslog --config ${configFile}" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/dnsmasq.nix b/nixos/modules/services/networking/dnsmasq.nix
new file mode 100644
index 00000000000..59a3ca2f28e
--- /dev/null
+++ b/nixos/modules/services/networking/dnsmasq.nix
@@ -0,0 +1,130 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dnsmasq;
+  dnsmasq = pkgs.dnsmasq;
+  stateDir = "/var/lib/dnsmasq";
+
+  dnsmasqConf = pkgs.writeText "dnsmasq.conf" ''
+    dhcp-leasefile=${stateDir}/dnsmasq.leases
+    ${optionalString cfg.resolveLocalQueries ''
+      conf-file=/etc/dnsmasq-conf.conf
+      resolv-file=/etc/dnsmasq-resolv.conf
+    ''}
+    ${flip concatMapStrings cfg.servers (server: ''
+      server=${server}
+    '')}
+    ${cfg.extraConfig}
+  '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.dnsmasq = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run dnsmasq.
+        '';
+      };
+
+      resolveLocalQueries = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether dnsmasq should resolve local queries (i.e. add 127.0.0.1 to
+          /etc/resolv.conf).
+        '';
+      };
+
+      servers = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "8.8.8.8" "8.8.4.4" ];
+        description = ''
+          The DNS servers which dnsmasq should query.
+        '';
+      };
+
+      alwaysKeepRunning = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If enabled, systemd will always respawn dnsmasq even if shut down manually. The default, disabled, will only restart it on error.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration directives that should be added to
+          <literal>dnsmasq.conf</literal>.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    networking.nameservers =
+      optional cfg.resolveLocalQueries "127.0.0.1";
+
+    services.dbus.packages = [ dnsmasq ];
+
+    users.users.dnsmasq = {
+      isSystemUser = true;
+      group = "dnsmasq";
+      description = "Dnsmasq daemon user";
+    };
+    users.groups.dnsmasq = {};
+
+    networking.resolvconf = mkIf cfg.resolveLocalQueries {
+      useLocalResolver = mkDefault true;
+
+      extraConfig = ''
+        dnsmasq_conf=/etc/dnsmasq-conf.conf
+        dnsmasq_resolv=/etc/dnsmasq-resolv.conf
+      '';
+    };
+
+    systemd.services.dnsmasq = {
+        description = "Dnsmasq Daemon";
+        after = [ "network.target" "systemd-resolved.service" ];
+        wantedBy = [ "multi-user.target" ];
+        path = [ dnsmasq ];
+        preStart = ''
+          mkdir -m 755 -p ${stateDir}
+          touch ${stateDir}/dnsmasq.leases
+          chown -R dnsmasq ${stateDir}
+          touch /etc/dnsmasq-{conf,resolv}.conf
+          dnsmasq --test
+        '';
+        serviceConfig = {
+          Type = "dbus";
+          BusName = "uk.org.thekelleys.dnsmasq";
+          ExecStart = "${dnsmasq}/bin/dnsmasq -k --enable-dbus --user=dnsmasq -C ${dnsmasqConf}";
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          PrivateTmp = true;
+          ProtectSystem = true;
+          ProtectHome = true;
+          Restart = if cfg.alwaysKeepRunning then "always" else "on-failure";
+        };
+        restartTriggers = [ config.environment.etc.hosts.source ];
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/doh-proxy-rust.nix b/nixos/modules/services/networking/doh-proxy-rust.nix
new file mode 100644
index 00000000000..efd492e23f8
--- /dev/null
+++ b/nixos/modules/services/networking/doh-proxy-rust.nix
@@ -0,0 +1,60 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.doh-proxy-rust;
+
+in {
+
+  options.services.doh-proxy-rust = {
+
+    enable = mkEnableOption "doh-proxy-rust";
+
+    flags = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "--server-address=9.9.9.9:53" ];
+      description = ''
+        A list of command-line flags to pass to doh-proxy. For details on the
+        available options, see <link xlink:href="https://github.com/jedisct1/doh-server#usage"/>.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.doh-proxy-rust = {
+      description = "doh-proxy-rust";
+      after = [ "network.target" "nss-lookup.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.doh-proxy-rust}/bin/doh-proxy ${escapeShellArgs cfg.flags}";
+        Restart = "always";
+        RestartSec = 10;
+        DynamicUser = true;
+
+        CapabilityBoundingSet = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        ProtectClock = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        RemoveIPC = true;
+        RestrictAddressFamilies = "AF_INET AF_INET6";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [ "@system-service" "~@privileged @resources" ];
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ stephank ];
+
+}
diff --git a/nixos/modules/services/networking/ejabberd.nix b/nixos/modules/services/networking/ejabberd.nix
new file mode 100644
index 00000000000..daf8d5c4247
--- /dev/null
+++ b/nixos/modules/services/networking/ejabberd.nix
@@ -0,0 +1,157 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.ejabberd;
+
+  ctlcfg = pkgs.writeText "ejabberdctl.cfg" ''
+    ERL_EPMD_ADDRESS=127.0.0.1
+    ${cfg.ctlConfig}
+  '';
+
+  ectl = ''${cfg.package}/bin/ejabberdctl ${optionalString (cfg.configFile != null) "--config ${cfg.configFile}"} --ctl-config "${ctlcfg}" --spool "${cfg.spoolDir}" --logs "${cfg.logsDir}"'';
+
+  dumps = lib.escapeShellArgs cfg.loadDumps;
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.ejabberd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable ejabberd server";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.ejabberd;
+        defaultText = literalExpression "pkgs.ejabberd";
+        description = "ejabberd server package to use";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "ejabberd";
+        description = "User under which ejabberd is ran";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "ejabberd";
+        description = "Group under which ejabberd is ran";
+      };
+
+      spoolDir = mkOption {
+        type = types.path;
+        default = "/var/lib/ejabberd";
+        description = "Location of the spooldir of ejabberd";
+      };
+
+      logsDir = mkOption {
+        type = types.path;
+        default = "/var/log/ejabberd";
+        description = "Location of the logfile directory of ejabberd";
+      };
+
+      configFile = mkOption {
+        type = types.nullOr types.path;
+        description = "Configuration file for ejabberd in YAML format";
+        default = null;
+      };
+
+      ctlConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Configuration of ejabberdctl";
+      };
+
+      loadDumps = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = "Configuration dumps that should be loaded on the first startup";
+        example = literalExpression "[ ./myejabberd.dump ]";
+      };
+
+      imagemagick = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Add ImageMagick to server's path; allows for image thumbnailing";
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    users.users = optionalAttrs (cfg.user == "ejabberd") {
+      ejabberd = {
+        group = cfg.group;
+        home = cfg.spoolDir;
+        createHome = true;
+        uid = config.ids.uids.ejabberd;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "ejabberd") {
+      ejabberd.gid = config.ids.gids.ejabberd;
+    };
+
+    systemd.services.ejabberd = {
+      description = "ejabberd server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = [ pkgs.findutils pkgs.coreutils ] ++ lib.optional cfg.imagemagick pkgs.imagemagick;
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${ectl} foreground";
+        ExecStop = "${ectl} stop";
+        ExecReload = "${ectl} reload_config";
+      };
+
+      preStart = ''
+        if [ -z "$(ls -A '${cfg.spoolDir}')" ]; then
+          touch "${cfg.spoolDir}/.firstRun"
+        fi
+      '';
+
+      postStart = ''
+        while ! ${ectl} status >/dev/null 2>&1; do
+          if ! kill -0 "$MAINPID"; then exit 1; fi
+          sleep 0.1
+        done
+
+        if [ -e "${cfg.spoolDir}/.firstRun" ]; then
+          rm "${cfg.spoolDir}/.firstRun"
+          for src in ${dumps}; do
+            find "$src" -type f | while read dump; do
+              echo "Loading configuration dump at $dump"
+              ${ectl} load "$dump"
+            done
+          done
+        fi
+      '';
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.logsDir}' 0750 ${cfg.user} ${cfg.group} -"
+      "d '${cfg.spoolDir}' 0700 ${cfg.user} ${cfg.group} -"
+    ];
+
+    security.pam.services.ejabberd = {};
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/epmd.nix b/nixos/modules/services/networking/epmd.nix
new file mode 100644
index 00000000000..75d78476e57
--- /dev/null
+++ b/nixos/modules/services/networking/epmd.nix
@@ -0,0 +1,72 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.epmd;
+in
+{
+  ###### interface
+  options.services.epmd = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable socket activation for Erlang Port Mapper Daemon (epmd),
+        which acts as a name server on all hosts involved in distributed
+        Erlang computations.
+      '';
+    };
+    package = mkOption {
+      type = types.package;
+      default = pkgs.erlang;
+      defaultText = literalExpression "pkgs.erlang";
+      description = ''
+        The Erlang package to use to get epmd binary. That way you can re-use
+        an Erlang runtime that is already installed for other purposes.
+      '';
+    };
+    listenStream = mkOption
+      {
+        type = types.str;
+        default = "[::]:4369";
+        description = ''
+          the listenStream used by the systemd socket.
+          see https://www.freedesktop.org/software/systemd/man/systemd.socket.html#ListenStream= for more informations.
+          use this to change the port epmd will run on.
+          if not defined, epmd will use "[::]:4369"
+        '';
+      };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    assertions = [{
+      assertion = cfg.listenStream == "[::]:4369" -> config.networking.enableIPv6;
+      message = "epmd listens by default on ipv6, enable ipv6 or change config.services.epmd.listenStream";
+    }];
+    systemd.sockets.epmd = rec {
+      description = "Erlang Port Mapper Daemon Activation Socket";
+      wantedBy = [ "sockets.target" ];
+      before = wantedBy;
+      socketConfig = {
+        ListenStream = cfg.listenStream;
+        Accept = "false";
+      };
+    };
+
+    systemd.services.epmd = {
+      description = "Erlang Port Mapper Daemon";
+      after = [ "network.target" ];
+      requires = [ "epmd.socket" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${cfg.package}/bin/epmd -systemd";
+        Type = "notify";
+      };
+    };
+  };
+
+  meta.maintainers = teams.beam.members;
+}
diff --git a/nixos/modules/services/networking/ergo.nix b/nixos/modules/services/networking/ergo.nix
new file mode 100644
index 00000000000..6e55a7cfff6
--- /dev/null
+++ b/nixos/modules/services/networking/ergo.nix
@@ -0,0 +1,143 @@
+{ config, lib, options, pkgs, ... }:
+
+let
+  cfg = config.services.ergo;
+  opt = options.services.ergo;
+
+  inherit (lib) literalExpression mkEnableOption mkIf mkOption optionalString types;
+
+  configFile = pkgs.writeText "ergo.conf" (''
+ergo {
+  directory = "${cfg.dataDir}"
+  node {
+    mining = false
+  }
+  wallet.secretStorage.secretDir = "${cfg.dataDir}/wallet/keystore"
+}
+
+scorex {
+  network {
+    bindAddress = "${cfg.listen.ip}:${toString cfg.listen.port}"
+  }
+'' + optionalString (cfg.api.keyHash != null) ''
+ restApi {
+    apiKeyHash = "${cfg.api.keyHash}"
+    bindAddress = "${cfg.api.listen.ip}:${toString cfg.api.listen.port}"
+ }
+'' + ''
+}
+'');
+
+in {
+
+  options = {
+
+    services.ergo = {
+      enable = mkEnableOption "Ergo service";
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/ergo";
+        description = "The data directory for the Ergo node.";
+      };
+
+      listen = {
+        ip = mkOption {
+          type = types.str;
+          default = "0.0.0.0";
+          description = "IP address on which the Ergo node should listen.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 9006;
+          description = "Listen port for the Ergo node.";
+        };
+      };
+
+      api = {
+       keyHash = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "324dcf027dd4a30a932c441f365a25e86b173defa4b8e58948253471b81b72cf";
+        description = "Hex-encoded Blake2b256 hash of an API key as a 64-chars long Base16 string.";
+       };
+
+       listen = {
+        ip = mkOption {
+          type = types.str;
+          default = "0.0.0.0";
+          description = "IP address that the Ergo node API should listen on if <option>api.keyHash</option> is defined.";
+          };
+
+        port = mkOption {
+          type = types.port;
+          default = 9052;
+          description = "Listen port for the API endpoint if <option>api.keyHash</option> is defined.";
+        };
+       };
+      };
+
+      testnet = mkOption {
+         type = types.bool;
+         default = false;
+         description = "Connect to testnet network instead of the default mainnet.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "ergo";
+        description = "The user as which to run the Ergo node.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = cfg.user;
+        defaultText = literalExpression "config.${opt.user}";
+        description = "The group as which to run the Ergo node.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the Ergo node as well as the API.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -"
+    ];
+
+    systemd.services.ergo = {
+      description = "ergo server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = ''${pkgs.ergo}/bin/ergo \
+                      ${optionalString (!cfg.testnet)
+                      "--mainnet"} \
+                      -c ${configFile}'';
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listen.port ] ++ [ cfg.api.listen.port ];
+    };
+
+    users.users.${cfg.user} = {
+      name = cfg.user;
+      group = cfg.group;
+      description = "Ergo daemon user";
+      home = cfg.dataDir;
+      isSystemUser = true;
+    };
+
+    users.groups.${cfg.group} = {};
+
+  };
+}
diff --git a/nixos/modules/services/networking/ergochat.nix b/nixos/modules/services/networking/ergochat.nix
new file mode 100644
index 00000000000..cfaf69fc613
--- /dev/null
+++ b/nixos/modules/services/networking/ergochat.nix
@@ -0,0 +1,155 @@
+{ config, lib, options, pkgs, ... }: let
+  cfg = config.services.ergochat;
+in {
+  options = {
+    services.ergochat = {
+
+      enable = lib.mkEnableOption "Ergo IRC daemon";
+
+      openFilesLimit = lib.mkOption {
+        type = lib.types.int;
+        default = 1024;
+        description = ''
+          Maximum number of open files. Limits the clients and server connections.
+        '';
+      };
+
+      configFile = lib.mkOption {
+        type = lib.types.path;
+        default = (pkgs.formats.yaml {}).generate "ergo.conf" cfg.settings;
+        defaultText = "generated config file from <literal>.settings</literal>";
+        description = ''
+          Path to configuration file.
+          Setting this will skip any configuration done via <literal>.settings</literal>
+        '';
+      };
+
+      settings = lib.mkOption {
+        type = (pkgs.formats.yaml {}).type;
+        description = ''
+          Ergo IRC daemon configuration file.
+          https://raw.githubusercontent.com/ergochat/ergo/master/default.yaml
+        '';
+        default = {
+          network = {
+            name = "testnetwork";
+          };
+          server = {
+            name = "example.com";
+            listeners = {
+              ":6667" = {};
+            };
+            casemapping = "permissive";
+            enforce-utf = true;
+            lookup-hostnames = false;
+            ip-cloaking = {
+              enabled = false;
+            };
+            forward-confirm-hostnames = false;
+            check-ident = false;
+            relaymsg = {
+              enabled = false;
+            };
+            max-sendq = "1M";
+            ip-limits = {
+              count = false;
+              throttle = false;
+            };
+          };
+          datastore = {
+            autoupgrade = true;
+            # this points to the StateDirectory of the systemd service
+            path = "/var/lib/ergo/ircd.db";
+          };
+          accounts = {
+            authentication-enabled = true;
+            registration = {
+              enabled = true;
+              allow-before-connect = true;
+              throttling = {
+                enabled = true;
+                duration = "10m";
+                max-attempts = 30;
+              };
+              bcrypt-cost = 4;
+              email-verification.enabled = false;
+            };
+            multiclient = {
+              enabled = true;
+              allowed-by-default = true;
+              always-on = "opt-out";
+              auto-away = "opt-out";
+            };
+          };
+          channels = {
+            default-modes = "+ntC";
+            registration = {
+              enabled = true;
+            };
+          };
+          limits = {
+            nicklen = 32;
+            identlen = 20;
+            channellen = 64;
+            awaylen = 390;
+            kicklen = 390;
+            topiclen = 390;
+          };
+          history = {
+            enabled = true;
+            channel-length = 2048;
+            client-length = 256;
+            autoresize-window = "3d";
+            autoreplay-on-join = 0;
+            chathistory-maxmessages = 100;
+            znc-maxmessages = 2048;
+            restrictions = {
+              expire-time = "1w";
+              query-cutoff = "none";
+              grace-period = "1h";
+            };
+            retention = {
+              allow-individual-delete = false;
+              enable-account-indexing = false;
+            };
+            tagmsg-storage = {
+              default = false;
+              whitelist = [
+                "+draft/react"
+                "+react"
+              ];
+            };
+          };
+        };
+      };
+
+    };
+  };
+  config = lib.mkIf cfg.enable {
+
+    environment.etc."ergo.yaml".source = cfg.configFile;
+
+    # merge configured values with default values
+    services.ergochat.settings =
+      lib.mapAttrsRecursive (_: lib.mkDefault) options.services.ergochat.settings.default;
+
+    systemd.services.ergochat = {
+      description = "Ergo IRC daemon";
+      wantedBy = [ "multi-user.target" ];
+      # reload is not applying the changed config. further investigation is needed
+      # at some point this should be enabled, since we don't want to restart for
+      # every config change
+      # reloadIfChanged = true;
+      restartTriggers = [ cfg.configFile ];
+      serviceConfig = {
+        ExecStart = "${pkgs.ergochat}/bin/ergo run --conf /etc/ergo.yaml";
+        ExecReload = "${pkgs.util-linux}/bin/kill -HUP $MAINPID";
+        DynamicUser = true;
+        StateDirectory = "ergo";
+        LimitNOFILE = toString cfg.openFilesLimit;
+      };
+    };
+
+  };
+  meta.maintainers = with lib.maintainers; [ lassulus tv ];
+}
diff --git a/nixos/modules/services/networking/eternal-terminal.nix b/nixos/modules/services/networking/eternal-terminal.nix
new file mode 100644
index 00000000000..0dcf3d28f4e
--- /dev/null
+++ b/nixos/modules/services/networking/eternal-terminal.nix
@@ -0,0 +1,95 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.eternal-terminal;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.eternal-terminal = {
+
+      enable = mkEnableOption "Eternal Terminal server";
+
+      port = mkOption {
+        default = 2022;
+        type = types.int;
+        description = ''
+          The port the server should listen on. Will use the server's default (2022) if not specified.
+
+          Make sure to open this port in the firewall if necessary.
+        '';
+      };
+
+      verbosity = mkOption {
+        default = 0;
+        type = types.enum (lib.range 0 9);
+        description = ''
+          The verbosity level (0-9).
+        '';
+      };
+
+      silent = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If enabled, disables all logging.
+        '';
+      };
+
+      logSize = mkOption {
+        default = 20971520;
+        type = types.int;
+        description = ''
+          The maximum log size.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    # We need to ensure the et package is fully installed because
+    # the (remote) et client runs the `etterminal` binary when it
+    # connects.
+    environment.systemPackages = [ pkgs.eternal-terminal ];
+
+    systemd.services = {
+      eternal-terminal = {
+        description = "Eternal Terminal server.";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig = {
+          Type = "forking";
+          ExecStart = "${pkgs.eternal-terminal}/bin/etserver --daemon --cfgfile=${pkgs.writeText "et.cfg" ''
+            ; et.cfg : Config file for Eternal Terminal
+            ;
+
+            [Networking]
+            port = ${toString cfg.port}
+
+            [Debug]
+            verbose = ${toString cfg.verbosity}
+            silent = ${if cfg.silent then "1" else "0"}
+            logsize = ${toString cfg.logSize}
+          ''}";
+          Restart = "on-failure";
+          KillMode = "process";
+        };
+      };
+    };
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ ];
+  };
+}
diff --git a/nixos/modules/services/networking/fakeroute.nix b/nixos/modules/services/networking/fakeroute.nix
new file mode 100644
index 00000000000..7916ad4098a
--- /dev/null
+++ b/nixos/modules/services/networking/fakeroute.nix
@@ -0,0 +1,65 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.fakeroute;
+  routeConf = pkgs.writeText "route.conf" (concatStringsSep "\n" cfg.route);
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.fakeroute = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the fakeroute service.
+        '';
+      };
+
+      route = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [
+          "216.102.187.130"
+          "4.0.1.122"
+          "198.116.142.34"
+          "63.199.8.242"
+        ];
+        description = ''
+         Fake route that will appear after the real
+         one to any host running a traceroute.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.fakeroute = {
+      description = "Fakeroute Daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "forking";
+        User = "root";
+        ExecStart = "${pkgs.fakeroute}/bin/fakeroute -f ${routeConf}";
+      };
+    };
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+}
diff --git a/nixos/modules/services/networking/ferm.nix b/nixos/modules/services/networking/ferm.nix
new file mode 100644
index 00000000000..8e03f30efc0
--- /dev/null
+++ b/nixos/modules/services/networking/ferm.nix
@@ -0,0 +1,63 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ferm;
+
+  configFile = pkgs.stdenv.mkDerivation {
+    name = "ferm.conf";
+    text = cfg.config;
+    preferLocalBuild = true;
+    buildCommand = ''
+      echo -n "$text" > $out
+      ${cfg.package}/bin/ferm --noexec $out
+    '';
+  };
+in {
+  options = {
+    services.ferm = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to enable Ferm Firewall.
+          *Warning*: Enabling this service WILL disable the existing NixOS
+          firewall! Default firewall rules provided by packages are not
+          considered at the moment.
+        '';
+      };
+      config = mkOption {
+        description = "Verbatim ferm.conf configuration.";
+        default = "";
+        defaultText = literalDocBook "empty firewall, allows any traffic";
+        type = types.lines;
+      };
+      package = mkOption {
+        description = "The ferm package.";
+        type = types.package;
+        default = pkgs.ferm;
+        defaultText = literalExpression "pkgs.ferm";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.firewall.enable = false;
+    systemd.services.ferm = {
+      description = "Ferm Firewall";
+      after = [ "ipset.target" ];
+      before = [ "network-pre.target" ];
+      wants = [ "network-pre.target" ];
+      wantedBy = [ "multi-user.target" ];
+      reloadIfChanged = true;
+      serviceConfig = {
+        Type="oneshot";
+        RemainAfterExit = "yes";
+        ExecStart = "${cfg.package}/bin/ferm ${configFile}";
+        ExecReload = "${cfg.package}/bin/ferm ${configFile}";
+        ExecStop = "${cfg.package}/bin/ferm -F ${configFile}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/fireqos.nix b/nixos/modules/services/networking/fireqos.nix
new file mode 100644
index 00000000000..0b34f0b6b8b
--- /dev/null
+++ b/nixos/modules/services/networking/fireqos.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.fireqos;
+  fireqosConfig = pkgs.writeText "fireqos.conf" "${cfg.config}";
+in {
+  options.services.fireqos = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        If enabled, FireQOS will be launched with the specified
+        configuration given in `config`.
+      '';
+    };
+
+    config = mkOption {
+      type = types.str;
+      default = "";
+      example = ''
+        interface wlp3s0 world-in input rate 10mbit ethernet
+          class web commit 50kbit
+            match tcp ports 80,443
+
+        interface wlp3s0 world-out input rate 10mbit ethernet
+          class web commit 50kbit
+            match tcp ports 80,443
+      '';
+      description = ''
+        The FireQOS configuration goes here.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.fireqos = {
+      description = "FireQOS";
+      after = [ "network.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStart = "${pkgs.firehol}/bin/fireqos start ${fireqosConfig}";
+        ExecStop = [
+          "${pkgs.firehol}/bin/fireqos stop"
+          "${pkgs.firehol}/bin/fireqos clear_all_qos"
+        ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/firewall.nix b/nixos/modules/services/networking/firewall.nix
new file mode 100644
index 00000000000..c213a5516a4
--- /dev/null
+++ b/nixos/modules/services/networking/firewall.nix
@@ -0,0 +1,584 @@
+/* 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 raw 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}";
+
+  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 raw -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true
+    ip46tables -t raw -F nixos-fw-rpfilter 2> /dev/null || true
+    ip46tables -t raw -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 raw table doesn't have a log-refuse yet
+      ip46tables -t raw -N nixos-fw-rpfilter 2> /dev/null || true
+      ip46tables -t raw -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 raw -A nixos-fw-rpfilter -p udp --sport 67 --dport 68 -j RETURN
+
+      # Allows this host to act as a DHCPv4 server
+      iptables -t raw -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 raw -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: "
+      ''}
+      ip46tables -t raw -A nixos-fw-rpfilter -j DROP
+
+      ip46tables -t raw -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 raw -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);
+
+  commonOptions = {
+    allowedTCPPorts = mkOption {
+      type = types.listOf types.port;
+      default = [ ];
+      apply = canonicalizePortList;
+      example = [ 22 80 ];
+      description =
+        ''
+          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 =
+        ''
+          A range of TCP ports on which incoming connections are
+          accepted.
+        '';
+    };
+
+    allowedUDPPorts = mkOption {
+      type = types.listOf types.port;
+      default = [ ];
+      apply = canonicalizePortList;
+      example = [ 53 ];
+      description =
+        ''
+          List of open UDP ports.
+        '';
+    };
+
+    allowedUDPPortRanges = mkOption {
+      type = types.listOf (types.attrsOf types.port);
+      default = [ ];
+      example = [ { from = 60000; to = 61000; } ];
+      description =
+        ''
+          Range of open UDP ports.
+        '';
+    };
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    networking.firewall = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description =
+          ''
+            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.
+          '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.iptables;
+        defaultText = literalExpression "pkgs.iptables";
+        example = literalExpression "pkgs.iptables-legacy";
+        description =
+          ''
+            The iptables package to use for running the firewall service."
+          '';
+      };
+
+      logRefusedConnections = mkOption {
+        type = types.bool;
+        default = true;
+        description =
+          ''
+            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 =
+          ''
+            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 =
+          ''
+            If <option>networking.firewall.logRefusedPackets</option>
+            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 =
+          ''
+            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 =
+          ''
+            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 =
+          ''
+            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 =
+          ''
+            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"
+          '';
+      };
+
+      checkReversePath = mkOption {
+        type = types.either types.bool (types.enum ["strict" "loose"]);
+        default = kernelHasRPFilter;
+        defaultText = literalDocBook "<literal>true</literal> if supported by the chosen kernel";
+        example = "loose";
+        description =
+          ''
+            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.
+          '';
+      };
+
+      logReversePathDrops = mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          ''
+            Logs dropped packets failing the reverse path filter test if
+            the option networking.firewall.checkReversePath is enabled.
+          '';
+      };
+
+      connectionTrackingModules = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "ftp" "irc" "sane" "sip" "tftp" "amanda" "h323" "netbios_sn" "pptp" "snmp" ];
+        description =
+          ''
+            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 =
+          ''
+            Whether to auto-load connection-tracking helpers.
+            See the description at networking.firewall.connectionTrackingModules
+
+            (needs kernel 3.5+)
+          '';
+      };
+
+      extraCommands = mkOption {
+        type = types.lines;
+        default = "";
+        example = "iptables -A INPUT -p icmp -j ACCEPT";
+        description =
+          ''
+            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.
+          '';
+      };
+
+      extraPackages = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        example = literalExpression "[ pkgs.ipset ]";
+        description =
+          ''
+            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 =
+          ''
+            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.
+          '';
+      };
+
+      interfaces = mkOption {
+        default = { };
+        type = with types; attrsOf (submodule [ { options = commonOptions; } ]);
+        description =
+          ''
+            Interface-specific 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 {
+
+    networking.firewall.trustedInterfaces = [ "lo" ];
+
+    environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages;
+
+    boot.kernelModules = (optional cfg.autoLoadConntrackHelpers "nf_conntrack")
+      ++ map (x: "nf_conntrack_${x}") cfg.connectionTrackingModules;
+    boot.extraModprobeConfig = optionalString cfg.autoLoadConntrackHelpers ''
+      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/flannel.nix b/nixos/modules/services/networking/flannel.nix
new file mode 100644
index 00000000000..ac84b3d35a3
--- /dev/null
+++ b/nixos/modules/services/networking/flannel.nix
@@ -0,0 +1,192 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.flannel;
+
+  networkConfig = filterAttrs (n: v: v != null) {
+    Network = cfg.network;
+    SubnetLen = cfg.subnetLen;
+    SubnetMin = cfg.subnetMin;
+    SubnetMax = cfg.subnetMax;
+    Backend = cfg.backend;
+  };
+in {
+  options.services.flannel = {
+    enable = mkEnableOption "flannel";
+
+    package = mkOption {
+      description = "Package to use for flannel";
+      type = types.package;
+      default = pkgs.flannel;
+      defaultText = literalExpression "pkgs.flannel";
+    };
+
+    publicIp = mkOption {
+      description = ''
+        IP accessible by other nodes for inter-host communication.
+        Defaults to the IP of the interface being used for communication.
+      '';
+      type = types.nullOr types.str;
+      default = null;
+    };
+
+    iface = mkOption {
+      description = ''
+        Interface to use (IP or name) for inter-host communication.
+        Defaults to the interface for the default route on the machine.
+      '';
+      type = types.nullOr types.str;
+      default = null;
+    };
+
+    etcd = {
+      endpoints = mkOption {
+        description = "Etcd endpoints";
+        type = types.listOf types.str;
+        default = ["http://127.0.0.1:2379"];
+      };
+
+      prefix = mkOption {
+        description = "Etcd key prefix";
+        type = types.str;
+        default = "/coreos.com/network";
+      };
+
+      caFile = mkOption {
+        description = "Etcd certificate authority file";
+        type = types.nullOr types.path;
+        default = null;
+      };
+
+      certFile = mkOption {
+        description = "Etcd cert file";
+        type = types.nullOr types.path;
+        default = null;
+      };
+
+      keyFile = mkOption {
+        description = "Etcd key file";
+        type = types.nullOr types.path;
+        default = null;
+      };
+    };
+
+    kubeconfig = mkOption {
+      description = ''
+        Path to kubeconfig to use for storing flannel config using the
+        Kubernetes API
+      '';
+      type = types.nullOr types.path;
+      default = null;
+    };
+
+    network = mkOption {
+      description = " IPv4 network in CIDR format to use for the entire flannel network.";
+      type = types.str;
+    };
+
+    nodeName = mkOption {
+      description = ''
+        Needed when running with Kubernetes as backend as this cannot be auto-detected";
+      '';
+      type = types.nullOr types.str;
+      default = with config.networking; (hostName + optionalString (domain != null) ".${domain}");
+      defaultText = literalExpression ''
+        with config.networking; (hostName + optionalString (domain != null) ".''${domain}")
+      '';
+      example = "node1.example.com";
+    };
+
+    storageBackend = mkOption {
+      description = "Determines where flannel stores its configuration at runtime";
+      type = types.enum ["etcd" "kubernetes"];
+      default = "etcd";
+    };
+
+    subnetLen = mkOption {
+      description = ''
+        The size of the subnet allocated to each host. Defaults to 24 (i.e. /24)
+        unless the Network was configured to be smaller than a /24 in which case
+        it is one less than the network.
+      '';
+      type = types.int;
+      default = 24;
+    };
+
+    subnetMin = mkOption {
+      description = ''
+        The beginning of IP range which the subnet allocation should start with.
+        Defaults to the first subnet of Network.
+      '';
+      type = types.nullOr types.str;
+      default = null;
+    };
+
+    subnetMax = mkOption {
+      description = ''
+        The end of IP range which the subnet allocation should start with.
+        Defaults to the last subnet of Network.
+      '';
+      type = types.nullOr types.str;
+      default = null;
+    };
+
+    backend = mkOption {
+      description = "Type of backend to use and specific configurations for that backend.";
+      type = types.attrs;
+      default = {
+        Type = "vxlan";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.flannel = {
+      description = "Flannel Service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      environment = {
+        FLANNELD_PUBLIC_IP = cfg.publicIp;
+        FLANNELD_IFACE = cfg.iface;
+      } // optionalAttrs (cfg.storageBackend == "etcd") {
+        FLANNELD_ETCD_ENDPOINTS = concatStringsSep "," cfg.etcd.endpoints;
+        FLANNELD_ETCD_KEYFILE = cfg.etcd.keyFile;
+        FLANNELD_ETCD_CERTFILE = cfg.etcd.certFile;
+        FLANNELD_ETCD_CAFILE = cfg.etcd.caFile;
+        ETCDCTL_CERT_FILE = cfg.etcd.certFile;
+        ETCDCTL_KEY_FILE = cfg.etcd.keyFile;
+        ETCDCTL_CA_FILE = cfg.etcd.caFile;
+        ETCDCTL_PEERS = concatStringsSep "," cfg.etcd.endpoints;
+      } // optionalAttrs (cfg.storageBackend == "kubernetes") {
+        FLANNELD_KUBE_SUBNET_MGR = "true";
+        FLANNELD_KUBECONFIG_FILE = cfg.kubeconfig;
+        NODE_NAME = cfg.nodeName;
+      };
+      path = [ pkgs.iptables ];
+      preStart = optionalString (cfg.storageBackend == "etcd") ''
+        echo "setting network configuration"
+        until ${pkgs.etcd}/bin/etcdctl set /coreos.com/network/config '${builtins.toJSON networkConfig}'
+        do
+          echo "setting network configuration, retry"
+          sleep 1
+        done
+      '';
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/flannel";
+        Restart = "always";
+        RestartSec = "10s";
+        RuntimeDirectory = "flannel";
+      };
+    };
+
+    services.etcd.enable = mkDefault (cfg.storageBackend == "etcd" && cfg.etcd.endpoints == ["http://127.0.0.1:2379"]);
+
+    # for some reason, flannel doesn't let you configure this path
+    # see: https://github.com/coreos/flannel/blob/master/Documentation/configuration.md#configuration
+    environment.etc."kube-flannel/net-conf.json" = mkIf (cfg.storageBackend == "kubernetes") {
+      source = pkgs.writeText "net-conf.json" (builtins.toJSON networkConfig);
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/freenet.nix b/nixos/modules/services/networking/freenet.nix
new file mode 100644
index 00000000000..3da3ab0c7df
--- /dev/null
+++ b/nixos/modules/services/networking/freenet.nix
@@ -0,0 +1,64 @@
+# NixOS module for Freenet daemon
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.freenet;
+  varDir = "/var/lib/freenet";
+
+in
+
+{
+
+  ### configuration
+
+  options = {
+
+    services.freenet = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable the Freenet daemon";
+      };
+
+      nice = mkOption {
+        type = types.int;
+        default = 10;
+        description = "Set the nice level for the Freenet daemon";
+      };
+
+    };
+
+  };
+
+  ### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.freenet = {
+      description = "Freenet daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig.ExecStart = "${pkgs.freenet}/bin/freenet";
+      serviceConfig.User = "freenet";
+      serviceConfig.UMask = "0007";
+      serviceConfig.WorkingDirectory = varDir;
+      serviceConfig.Nice = cfg.nice;
+    };
+
+    users.users.freenet = {
+      group = "freenet";
+      description = "Freenet daemon user";
+      home = varDir;
+      createHome = true;
+      uid = config.ids.uids.freenet;
+    };
+
+    users.groups.freenet.gid = config.ids.gids.freenet;
+  };
+
+}
diff --git a/nixos/modules/services/networking/freeradius.nix b/nixos/modules/services/networking/freeradius.nix
new file mode 100644
index 00000000000..7fa3a8fa17f
--- /dev/null
+++ b/nixos/modules/services/networking/freeradius.nix
@@ -0,0 +1,86 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.freeradius;
+
+  freeradiusService = cfg:
+  {
+    description = "FreeRadius server";
+    wantedBy = ["multi-user.target"];
+    after = ["network.target"];
+    wants = ["network.target"];
+    preStart = ''
+      ${pkgs.freeradius}/bin/radiusd -C -d ${cfg.configDir} -l stdout
+    '';
+
+    serviceConfig = {
+        ExecStart = "${pkgs.freeradius}/bin/radiusd -f -d ${cfg.configDir} -l stdout" +
+                    optionalString cfg.debug " -xx";
+        ExecReload = [
+          "${pkgs.freeradius}/bin/radiusd -C -d ${cfg.configDir} -l stdout"
+          "${pkgs.coreutils}/bin/kill -HUP $MAINPID"
+        ];
+        User = "radius";
+        ProtectSystem = "full";
+        ProtectHome = "on";
+        Restart = "on-failure";
+        RestartSec = 2;
+        LogsDirectory = "radius";
+    };
+  };
+
+  freeradiusConfig = {
+    enable = mkEnableOption "the freeradius server";
+
+    configDir = mkOption {
+      type = types.path;
+      default = "/etc/raddb";
+      description = ''
+        The path of the freeradius server configuration directory.
+      '';
+    };
+
+    debug = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable debug logging for freeradius (-xx
+        option). This should not be left on, since it includes
+        sensitive data such as passwords in the logs.
+      '';
+    };
+
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+    services.freeradius = freeradiusConfig;
+  };
+
+
+  ###### implementation
+
+  config = mkIf (cfg.enable) {
+
+    users = {
+      users.radius = {
+        /*uid = config.ids.uids.radius;*/
+        description = "Radius daemon user";
+        isSystemUser = true;
+      };
+    };
+
+    systemd.services.freeradius = freeradiusService cfg;
+    warnings = optional cfg.debug "Freeradius debug logging is enabled. This will log passwords in plaintext to the journal!";
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/frr.nix b/nixos/modules/services/networking/frr.nix
new file mode 100644
index 00000000000..45a82b9450a
--- /dev/null
+++ b/nixos/modules/services/networking/frr.nix
@@ -0,0 +1,211 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.frr;
+
+  services = [
+    "static"
+    "bgp"
+    "ospf"
+    "ospf6"
+    "rip"
+    "ripng"
+    "isis"
+    "pim"
+    "ldp"
+    "nhrp"
+    "eigrp"
+    "babel"
+    "sharp"
+    "pbr"
+    "bfd"
+    "fabric"
+  ];
+
+  allServices = services ++ [ "zebra" ];
+
+  isEnabled = service: cfg.${service}.enable;
+
+  daemonName = service: if service == "zebra" then service else "${service}d";
+
+  configFile = service:
+    let
+      scfg = cfg.${service};
+    in
+      if scfg.configFile != null then scfg.configFile
+      else pkgs.writeText "${daemonName service}.conf"
+        ''
+          ! FRR ${daemonName service} configuration
+          !
+          hostname ${config.networking.hostName}
+          log syslog
+          service password-encryption
+          !
+          ${scfg.config}
+          !
+          end
+        '';
+
+  serviceOptions = service:
+    {
+      enable = mkEnableOption "the FRR ${toUpper service} routing protocol";
+
+      configFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/etc/frr/${daemonName service}.conf";
+        description = ''
+          Configuration file to use for FRR ${daemonName service}.
+          By default the NixOS generated files are used.
+        '';
+      };
+
+      config = mkOption {
+        type = types.lines;
+        default = "";
+        example =
+          let
+            examples = {
+              rip = ''
+                router rip
+                  network 10.0.0.0/8
+              '';
+
+              ospf = ''
+                router ospf
+                  network 10.0.0.0/8 area 0
+              '';
+
+              bgp = ''
+                router bgp 65001
+                  neighbor 10.0.0.1 remote-as 65001
+              '';
+            };
+          in
+            examples.${service} or "";
+        description = ''
+          ${daemonName service} configuration statements.
+        '';
+      };
+
+      vtyListenAddress = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = ''
+          Address to bind to for the VTY interface.
+        '';
+      };
+
+      vtyListenPort = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          TCP Port to bind to for the VTY interface.
+        '';
+      };
+    };
+
+in
+
+{
+
+  ###### interface
+  imports = [
+    {
+      options.services.frr = {
+        zebra = (serviceOptions "zebra") // {
+          enable = mkOption {
+            type = types.bool;
+            default = any isEnabled services;
+            description = ''
+              Whether to enable the Zebra routing manager.
+
+              The Zebra routing manager is automatically enabled
+              if any routing protocols are configured.
+            '';
+          };
+        };
+      };
+    }
+    { options.services.frr = (genAttrs services serviceOptions); }
+  ];
+
+  ###### implementation
+
+  config = mkIf (any isEnabled allServices) {
+
+    environment.systemPackages = [
+      pkgs.frr # for the vtysh tool
+    ];
+
+    users.users.frr = {
+      description = "FRR daemon user";
+      isSystemUser = true;
+      group = "frr";
+    };
+
+    users.groups = {
+      frr = {};
+      # Members of the frrvty group can use vtysh to inspect the FRR daemons
+      frrvty = { members = [ "frr" ]; };
+    };
+
+    environment.etc = let
+      mkEtcLink = service: {
+        name = "frr/${service}.conf";
+        value.source = configFile service;
+      };
+    in
+      (builtins.listToAttrs
+      (map mkEtcLink (filter isEnabled allServices))) // {
+        "frr/vtysh.conf".text = "";
+      };
+
+    systemd.tmpfiles.rules = [
+      "d /run/frr 0750 frr frr -"
+    ];
+
+    systemd.services =
+      let
+        frrService = service:
+          let
+            scfg = cfg.${service};
+            daemon = daemonName service;
+          in
+            nameValuePair daemon ({
+              wantedBy = [ "multi-user.target" ];
+              after = [ "network-pre.target" "systemd-sysctl.service" ] ++ lib.optionals (service != "zebra") [ "zebra.service" ];
+              bindsTo = lib.optionals (service != "zebra") [ "zebra.service" ];
+              wants = [ "network.target" ];
+
+              description = if service == "zebra" then "FRR Zebra routing manager"
+                else "FRR ${toUpper service} routing daemon";
+
+              unitConfig.Documentation = if service == "zebra" then "man:zebra(8)"
+                else "man:${daemon}(8) man:zebra(8)";
+
+              restartTriggers = [
+                (configFile service)
+              ];
+              reloadIfChanged = true;
+
+              serviceConfig = {
+                PIDFile = "frr/${daemon}.pid";
+                ExecStart = "${pkgs.frr}/libexec/frr/${daemon} -f /etc/frr/${service}.conf"
+                  + optionalString (scfg.vtyListenAddress != "") " -A ${scfg.vtyListenAddress}"
+                  + optionalString (scfg.vtyListenPort != null) " -P ${toString scfg.vtyListenPort}";
+                ExecReload = "${pkgs.python3.interpreter} ${pkgs.frr}/libexec/frr/frr-reload.py --reload --daemon ${daemonName service} --bindir ${pkgs.frr}/bin --rundir /run/frr /etc/frr/${service}.conf";
+                Restart = "on-abnormal";
+              };
+            });
+       in
+         listToAttrs (map frrService (filter isEnabled allServices));
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ woffs ];
+
+}
diff --git a/nixos/modules/services/networking/gateone.nix b/nixos/modules/services/networking/gateone.nix
new file mode 100644
index 00000000000..3e3a3c1aa94
--- /dev/null
+++ b/nixos/modules/services/networking/gateone.nix
@@ -0,0 +1,59 @@
+{ config, lib, pkgs, ...}:
+with lib;
+let
+  cfg = config.services.gateone;
+in
+{
+options = {
+    services.gateone = {
+      enable = mkEnableOption "GateOne server";
+      pidDir = mkOption {
+        default = "/run/gateone";
+        type = types.path;
+        description = "Path of pid files for GateOne.";
+      };
+      settingsDir = mkOption {
+        default = "/var/lib/gateone";
+        type = types.path;
+        description = "Path of configuration files for GateOne.";
+      };
+    };
+};
+config = mkIf cfg.enable {
+  environment.systemPackages = with pkgs.pythonPackages; [
+    gateone pkgs.openssh pkgs.procps pkgs.coreutils pkgs.cacert];
+
+  users.users.gateone = {
+    description = "GateOne privilege separation user";
+    uid = config.ids.uids.gateone;
+    home = cfg.settingsDir;
+  };
+  users.groups.gateone.gid = config.ids.gids.gateone;
+
+  systemd.services.gateone = with pkgs; {
+    description = "GateOne web-based terminal";
+    path = [ pythonPackages.gateone nix openssh procps coreutils ];
+    preStart = ''
+      if [ ! -d ${cfg.settingsDir} ] ; then
+        mkdir -m 0750 -p ${cfg.settingsDir}
+        chown -R gateone.gateone ${cfg.settingsDir}
+      fi
+      if [ ! -d ${cfg.pidDir} ] ; then
+        mkdir -m 0750 -p ${cfg.pidDir}
+        chown -R gateone.gateone ${cfg.pidDir}
+      fi
+      '';
+    #unitConfig.RequiresMountsFor = "${cfg.settingsDir}";
+    serviceConfig = {
+      ExecStart = ''${pythonPackages.gateone}/bin/gateone --settings_dir=${cfg.settingsDir} --pid_file=${cfg.pidDir}/gateone.pid --gid=${toString config.ids.gids.gateone} --uid=${toString config.ids.uids.gateone}'';
+      User = "gateone";
+      Group = "gateone";
+      WorkingDirectory = cfg.settingsDir;
+    };
+
+    wantedBy = [ "multi-user.target" ];
+    requires = [ "network.target" ];
+  };
+};
+}
+
diff --git a/nixos/modules/services/networking/gdomap.nix b/nixos/modules/services/networking/gdomap.nix
new file mode 100644
index 00000000000..3d829cb6913
--- /dev/null
+++ b/nixos/modules/services/networking/gdomap.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  #
+  # interface
+  #
+  options = {
+    services.gdomap = {
+      enable = mkEnableOption "GNUstep Distributed Objects name server";
+   };
+  };
+
+  #
+  # implementation
+  #
+  config = mkIf config.services.gdomap.enable {
+    # NOTE: gdomap runs as root
+    # TODO: extra user for gdomap?
+    systemd.services.gdomap = {
+      description = "gdomap server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path  = [ pkgs.gnustep.base ];
+      serviceConfig.ExecStart = "${pkgs.gnustep.base}/bin/gdomap -f";
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/ghostunnel.nix b/nixos/modules/services/networking/ghostunnel.nix
new file mode 100644
index 00000000000..7a62d378e2c
--- /dev/null
+++ b/nixos/modules/services/networking/ghostunnel.nix
@@ -0,0 +1,242 @@
+{ config, lib, pkgs, ... }:
+let
+  inherit (lib)
+    attrValues
+    concatMap
+    concatStringsSep
+    escapeShellArg
+    literalExpression
+    mapAttrs'
+    mkDefault
+    mkEnableOption
+    mkIf
+    mkOption
+    nameValuePair
+    optional
+    types
+    ;
+
+  mainCfg = config.services.ghostunnel;
+
+  module = { config, name, ... }:
+    {
+      options = {
+
+        listen = mkOption {
+          description = ''
+            Address and port to listen on (can be HOST:PORT, unix:PATH).
+          '';
+          type = types.str;
+        };
+
+        target = mkOption {
+          description = ''
+            Address to forward connections to (can be HOST:PORT or unix:PATH).
+          '';
+          type = types.str;
+        };
+
+        keystore = mkOption {
+          description = ''
+            Path to keystore (combined PEM with cert/key, or PKCS12 keystore).
+
+            NB: storepass is not supported because it would expose credentials via <code>/proc/*/cmdline</code>.
+
+            Specify this or <code>cert</code> and <code>key</code>.
+          '';
+          type = types.nullOr types.str;
+          default = null;
+        };
+
+        cert = mkOption {
+          description = ''
+            Path to certificate (PEM with certificate chain).
+
+            Not required if <code>keystore</code> is set.
+          '';
+          type = types.nullOr types.str;
+          default = null;
+        };
+
+        key = mkOption {
+          description = ''
+            Path to certificate private key (PEM with private key).
+
+            Not required if <code>keystore</code> is set.
+          '';
+          type = types.nullOr types.str;
+          default = null;
+        };
+
+        cacert = mkOption {
+          description = ''
+            Path to CA bundle file (PEM/X509). Uses system trust store if <code>null</code>.
+          '';
+          type = types.nullOr types.str;
+        };
+
+        disableAuthentication = mkOption {
+          description = ''
+            Disable client authentication, no client certificate will be required.
+          '';
+          type = types.bool;
+          default = false;
+        };
+
+        allowAll = mkOption {
+          description = ''
+            If true, allow all clients, do not check client cert subject.
+          '';
+          type = types.bool;
+          default = false;
+        };
+
+        allowCN = mkOption {
+          description = ''
+            Allow client if common name appears in the list.
+          '';
+          type = types.listOf types.str;
+          default = [];
+        };
+
+        allowOU = mkOption {
+          description = ''
+            Allow client if organizational unit name appears in the list.
+          '';
+          type = types.listOf types.str;
+          default = [];
+        };
+
+        allowDNS = mkOption {
+          description = ''
+            Allow client if DNS subject alternative name appears in the list.
+          '';
+          type = types.listOf types.str;
+          default = [];
+        };
+
+        allowURI = mkOption {
+          description = ''
+            Allow client if URI subject alternative name appears in the list.
+          '';
+          type = types.listOf types.str;
+          default = [];
+        };
+
+        extraArguments = mkOption {
+          description = "Extra arguments to pass to <code>ghostunnel server</code>";
+          type = types.separatedString " ";
+          default = "";
+        };
+
+        unsafeTarget = mkOption {
+          description = ''
+            If set, does not limit target to localhost, 127.0.0.1, [::1], or UNIX sockets.
+
+            This is meant to protect against accidental unencrypted traffic on
+            untrusted networks.
+          '';
+          type = types.bool;
+          default = false;
+        };
+
+        # Definitions to apply at the root of the NixOS configuration.
+        atRoot = mkOption {
+          internal = true;
+        };
+      };
+
+      # Clients should not be authenticated with the public root certificates
+      # (afaict, it doesn't make sense), so we only provide that default when
+      # client cert auth is disabled.
+      config.cacert = mkIf config.disableAuthentication (mkDefault null);
+
+      config.atRoot = {
+        assertions = [
+          { message = ''
+              services.ghostunnel.servers.${name}: At least one access control flag is required.
+              Set at least one of:
+                - services.ghostunnel.servers.${name}.disableAuthentication
+                - services.ghostunnel.servers.${name}.allowAll
+                - services.ghostunnel.servers.${name}.allowCN
+                - services.ghostunnel.servers.${name}.allowOU
+                - services.ghostunnel.servers.${name}.allowDNS
+                - services.ghostunnel.servers.${name}.allowURI
+            '';
+            assertion = config.disableAuthentication
+              || config.allowAll
+              || config.allowCN != []
+              || config.allowOU != []
+              || config.allowDNS != []
+              || config.allowURI != []
+              ;
+          }
+        ];
+
+        systemd.services."ghostunnel-server-${name}" = {
+          after = [ "network.target" ];
+          wants = [ "network.target" ];
+          wantedBy = [ "multi-user.target" ];
+          serviceConfig = {
+            Restart = "always";
+            AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
+            DynamicUser = true;
+            LoadCredential = optional (config.keystore != null) "keystore:${config.keystore}"
+              ++ optional (config.cert != null) "cert:${config.cert}"
+              ++ optional (config.key != null) "key:${config.key}"
+              ++ optional (config.cacert != null) "cacert:${config.cacert}";
+           };
+          script = concatStringsSep " " (
+            [ "${mainCfg.package}/bin/ghostunnel" ]
+            ++ optional (config.keystore != null) "--keystore=$CREDENTIALS_DIRECTORY/keystore"
+            ++ optional (config.cert != null) "--cert=$CREDENTIALS_DIRECTORY/cert"
+            ++ optional (config.key != null) "--key=$CREDENTIALS_DIRECTORY/key"
+            ++ optional (config.cacert != null) "--cacert=$CREDENTIALS_DIRECTORY/cacert"
+            ++ [
+              "server"
+              "--listen ${config.listen}"
+              "--target ${config.target}"
+            ] ++ optional config.allowAll "--allow-all"
+              ++ map (v: "--allow-cn=${escapeShellArg v}") config.allowCN
+              ++ map (v: "--allow-ou=${escapeShellArg v}") config.allowOU
+              ++ map (v: "--allow-dns=${escapeShellArg v}") config.allowDNS
+              ++ map (v: "--allow-uri=${escapeShellArg v}") config.allowURI
+              ++ optional config.disableAuthentication "--disable-authentication"
+              ++ optional config.unsafeTarget "--unsafe-target"
+              ++ [ config.extraArguments ]
+          );
+        };
+      };
+    };
+
+in
+{
+
+  options = {
+    services.ghostunnel.enable = mkEnableOption "ghostunnel";
+
+    services.ghostunnel.package = mkOption {
+      description = "The ghostunnel package to use.";
+      type = types.package;
+      default = pkgs.ghostunnel;
+      defaultText = literalExpression "pkgs.ghostunnel";
+    };
+
+    services.ghostunnel.servers = mkOption {
+      description = ''
+        Server mode ghostunnels (TLS listener -> plain TCP/UNIX target)
+      '';
+      type = types.attrsOf (types.submodule module);
+      default = {};
+    };
+  };
+
+  config = mkIf mainCfg.enable {
+    assertions = lib.mkMerge (map (v: v.atRoot.assertions) (attrValues mainCfg.servers));
+    systemd = lib.mkMerge (map (v: v.atRoot.systemd) (attrValues mainCfg.servers));
+  };
+
+  meta.maintainers = with lib.maintainers; [
+    roberth
+  ];
+}
diff --git a/nixos/modules/services/networking/git-daemon.nix b/nixos/modules/services/networking/git-daemon.nix
new file mode 100644
index 00000000000..6be72505c21
--- /dev/null
+++ b/nixos/modules/services/networking/git-daemon.nix
@@ -0,0 +1,131 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+
+  cfg = config.services.gitDaemon;
+
+in
+{
+
+  ###### interface
+
+  options = {
+    services.gitDaemon = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable Git daemon, which allows public hosting of git repositories
+          without any access controls. This is mostly intended for read-only access.
+
+          You can allow write access by setting daemon.receivepack configuration
+          item of the repository to true. This is solely meant for a closed LAN setting
+          where everybody is friendly.
+
+          If you need any access controls, use something else.
+        '';
+      };
+
+      basePath = mkOption {
+        type = types.str;
+        default = "";
+        example = "/srv/git/";
+        description = ''
+          Remap all the path requests as relative to the given path. For example,
+          if you set base-path to /srv/git, then if you later try to pull
+          git://example.com/hello.git, Git daemon will interpret the path as /srv/git/hello.git.
+        '';
+      };
+
+      exportAll = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Publish all directories that look like Git repositories (have the objects
+          and refs subdirectories), even if they do not have the git-daemon-export-ok file.
+
+          If disabled, you need to touch .git/git-daemon-export-ok in each repository
+          you want the daemon to publish.
+
+          Warning: enabling this without a repository whitelist or basePath
+          publishes every git repository you have.
+        '';
+      };
+
+      repositories = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "/srv/git" "/home/user/git/repo2" ];
+        description = ''
+          A whitelist of paths of git repositories, or directories containing repositories
+          all of which would be published. Paths must not end in "/".
+
+          Warning: leaving this empty and enabling exportAll publishes all
+          repositories in your filesystem or basePath if specified.
+        '';
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "";
+        example = "example.com";
+        description = "Listen on a specific IP address or hostname.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 9418;
+        description = "Port to listen on.";
+      };
+
+      options = mkOption {
+        type = types.str;
+        default = "";
+        description = "Extra configuration options to be passed to Git daemon.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "git";
+        description = "User under which Git daemon would be running.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "git";
+        description = "Group under which Git daemon would be running.";
+      };
+
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users = optionalAttrs (cfg.user == "git") {
+      git = {
+        uid = config.ids.uids.git;
+        group = "git";
+        description = "Git daemon user";
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "git") {
+      git.gid = config.ids.gids.git;
+    };
+
+    systemd.services.git-daemon = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      script = "${pkgs.git}/bin/git daemon --reuseaddr "
+        + (optionalString (cfg.basePath != "") "--base-path=${cfg.basePath} ")
+        + (optionalString (cfg.listenAddress != "") "--listen=${cfg.listenAddress} ")
+        + "--port=${toString cfg.port} --user=${cfg.user} --group=${cfg.group} ${cfg.options} "
+        + "--verbose " + (optionalString cfg.exportAll "--export-all ")  + concatStringsSep " " cfg.repositories;
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/globalprotect-vpn.nix b/nixos/modules/services/networking/globalprotect-vpn.nix
new file mode 100644
index 00000000000..976fdf2b962
--- /dev/null
+++ b/nixos/modules/services/networking/globalprotect-vpn.nix
@@ -0,0 +1,43 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.globalprotect;
+
+  execStart = if cfg.csdWrapper == null then
+      "${pkgs.globalprotect-openconnect}/bin/gpservice"
+    else
+      "${pkgs.globalprotect-openconnect}/bin/gpservice --csd-wrapper=${cfg.csdWrapper}";
+in
+
+{
+  options.services.globalprotect = {
+    enable = mkEnableOption "globalprotect";
+
+    csdWrapper = mkOption {
+      description = ''
+        A script that will produce a Host Integrity Protection (HIP) report,
+        as described at <link xlink:href="https://www.infradead.org/openconnect/hip.html" />
+      '';
+      default = null;
+      example = literalExpression ''"''${pkgs.openconnect}/libexec/openconnect/hipreport.sh"'';
+      type = types.nullOr types.path;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.dbus.packages = [ pkgs.globalprotect-openconnect ];
+
+    systemd.services.gpservice = {
+      description = "GlobalProtect openconnect DBus service";
+      serviceConfig = {
+        Type="dbus";
+        BusName="com.yuezk.qt.GPService";
+        ExecStart=execStart;
+      };
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/gnunet.nix b/nixos/modules/services/networking/gnunet.nix
new file mode 100644
index 00000000000..5c41967d279
--- /dev/null
+++ b/nixos/modules/services/networking/gnunet.nix
@@ -0,0 +1,170 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.gnunet;
+
+  stateDir = "/var/lib/gnunet";
+
+  configFile = with cfg;
+    ''
+      [PATHS]
+      GNUNET_HOME = ${stateDir}
+      GNUNET_RUNTIME_DIR = /run/gnunet
+      GNUNET_USER_RUNTIME_DIR = /run/gnunet
+      GNUNET_DATA_HOME = ${stateDir}/data
+
+      [ats]
+      WAN_QUOTA_IN = ${toString load.maxNetDownBandwidth} b
+      WAN_QUOTA_OUT = ${toString load.maxNetUpBandwidth} b
+
+      [datastore]
+      QUOTA = ${toString fileSharing.quota} MB
+
+      [transport-udp]
+      PORT = ${toString udp.port}
+      ADVERTISED_PORT = ${toString udp.port}
+
+      [transport-tcp]
+      PORT = ${toString tcp.port}
+      ADVERTISED_PORT = ${toString tcp.port}
+
+      ${extraOptions}
+    '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.gnunet = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run the GNUnet daemon.  GNUnet is GNU's anonymous
+          peer-to-peer communication and file sharing framework.
+        '';
+      };
+
+      fileSharing = {
+        quota = mkOption {
+          type = types.int;
+          default = 1024;
+          description = ''
+            Maximum file system usage (in MiB) for file sharing.
+          '';
+        };
+      };
+
+      udp = {
+        port = mkOption {
+          type = types.port;
+          default = 2086;  # assigned by IANA
+          description = ''
+            The UDP port for use by GNUnet.
+          '';
+        };
+      };
+
+      tcp = {
+        port = mkOption {
+          type = types.port;
+          default = 2086;  # assigned by IANA
+          description = ''
+            The TCP port for use by GNUnet.
+          '';
+        };
+      };
+
+      load = {
+        maxNetDownBandwidth = mkOption {
+          type = types.int;
+          default = 50000;
+          description = ''
+            Maximum bandwidth usage (in bits per second) for GNUnet
+            when downloading data.
+          '';
+        };
+
+        maxNetUpBandwidth = mkOption {
+          type = types.int;
+          default = 50000;
+          description = ''
+            Maximum bandwidth usage (in bits per second) for GNUnet
+            when downloading data.
+          '';
+        };
+
+        hardNetUpBandwidth = mkOption {
+          type = types.int;
+          default = 0;
+          description = ''
+            Hard bandwidth limit (in bits per second) when uploading
+            data.
+          '';
+        };
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.gnunet;
+        defaultText = literalExpression "pkgs.gnunet";
+        description = "Overridable attribute of the gnunet package to use.";
+        example = literalExpression "pkgs.gnunet_git";
+      };
+
+      extraOptions = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Additional options that will be copied verbatim in `gnunet.conf'.
+          See `gnunet.conf(5)' for details.
+        '';
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.gnunet.enable {
+
+    users.users.gnunet = {
+      group = "gnunet";
+      description = "GNUnet User";
+      uid = config.ids.uids.gnunet;
+    };
+
+    users.groups.gnunet.gid = config.ids.gids.gnunet;
+
+    # The user tools that talk to `gnunetd' should come from the same source,
+    # so install them globally.
+    environment.systemPackages = [ cfg.package ];
+
+    environment.etc."gnunet.conf".text = configFile;
+
+    systemd.services.gnunet = {
+      description = "GNUnet";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ configFile ];
+      path = [ cfg.package pkgs.miniupnpc ];
+      serviceConfig.ExecStart = "${cfg.package}/lib/gnunet/libexec/gnunet-service-arm -c /etc/gnunet.conf";
+      serviceConfig.User = "gnunet";
+      serviceConfig.UMask = "0007";
+      serviceConfig.WorkingDirectory = stateDir;
+      serviceConfig.RuntimeDirectory = "gnunet";
+      serviceConfig.StateDirectory = "gnunet";
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/go-neb.nix b/nixos/modules/services/networking/go-neb.nix
new file mode 100644
index 00000000000..765834fad83
--- /dev/null
+++ b/nixos/modules/services/networking/go-neb.nix
@@ -0,0 +1,79 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.go-neb;
+
+  settingsFormat = pkgs.formats.yaml {};
+  configFile = settingsFormat.generate "config.yaml" cfg.config;
+in {
+  options.services.go-neb = {
+    enable = mkEnableOption "Extensible matrix bot written in Go";
+
+    bindAddress = mkOption {
+      type = types.str;
+      description = "Port (and optionally address) to listen on.";
+      default = ":4050";
+    };
+
+    secretFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/run/keys/go-neb.env";
+      description = ''
+        Environment variables from this file will be interpolated into the
+        final config file using envsubst with this syntax: <literal>$ENVIRONMENT</literal>
+        or <literal>''${VARIABLE}</literal>.
+        The file should contain lines formatted as <literal>SECRET_VAR=SECRET_VALUE</literal>.
+        This is useful to avoid putting secrets into the nix store.
+      '';
+    };
+
+    baseUrl = mkOption {
+      type = types.str;
+      description = "Public-facing endpoint that can receive webhooks.";
+    };
+
+    config = mkOption {
+      inherit (settingsFormat) type;
+      description = ''
+        Your <filename>config.yaml</filename> as a Nix attribute set.
+        See <link xlink:href="https://github.com/matrix-org/go-neb/blob/master/config.sample.yaml">config.sample.yaml</link>
+        for possible options.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.go-neb = let
+      finalConfigFile = if cfg.secretFile == null then configFile else "/var/run/go-neb/config.yaml";
+    in {
+      description = "Extensible matrix bot written in Go";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        BASE_URL = cfg.baseUrl;
+        BIND_ADDRESS = cfg.bindAddress;
+        CONFIG_FILE = finalConfigFile;
+      };
+
+      serviceConfig = {
+        ExecStartPre = lib.optional (cfg.secretFile != null)
+          (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";
+        DynamicUser = true;
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ hexa maralorn ];
+}
diff --git a/nixos/modules/services/networking/go-shadowsocks2.nix b/nixos/modules/services/networking/go-shadowsocks2.nix
new file mode 100644
index 00000000000..afbd7ea27c6
--- /dev/null
+++ b/nixos/modules/services/networking/go-shadowsocks2.nix
@@ -0,0 +1,30 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.go-shadowsocks2.server;
+in {
+  options.services.go-shadowsocks2.server = {
+    enable = mkEnableOption "go-shadowsocks2 server";
+
+    listenAddress = mkOption {
+      type = types.str;
+      description = "Server listen address or URL";
+      example = "ss://AEAD_CHACHA20_POLY1305:your-password@:8488";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.go-shadowsocks2-server = {
+      description = "go-shadowsocks2 server";
+
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.go-shadowsocks2}/bin/go-shadowsocks2 -s '${cfg.listenAddress}'";
+        DynamicUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/gobgpd.nix b/nixos/modules/services/networking/gobgpd.nix
new file mode 100644
index 00000000000..29ef9a5cf1e
--- /dev/null
+++ b/nixos/modules/services/networking/gobgpd.nix
@@ -0,0 +1,64 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gobgpd;
+  format = pkgs.formats.toml { };
+  confFile = format.generate "gobgpd.conf" cfg.settings;
+in {
+  options.services.gobgpd = {
+    enable = mkEnableOption "GoBGP Routing Daemon";
+
+    settings = mkOption {
+      type = format.type;
+      default = { };
+      description = ''
+        GoBGP configuration. Refer to
+        <link xlink:href="https://github.com/osrg/gobgp#documentation"/>
+        for details on supported values.
+      '';
+      example = literalExpression ''
+        {
+          global = {
+            config = {
+              as = 64512;
+              router-id = "192.168.255.1";
+            };
+          };
+          neighbors = [
+            {
+              config = {
+                neighbor-address = "10.0.255.1";
+                peer-as = 65001;
+              };
+            }
+            {
+              config = {
+                neighbor-address = "10.0.255.2";
+                peer-as = 65002;
+              };
+            }
+          ];
+        }
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.gobgpd ];
+    systemd.services.gobgpd = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      description = "GoBGP Routing Daemon";
+      serviceConfig = {
+        Type = "notify";
+        ExecStartPre = "${pkgs.gobgpd}/bin/gobgpd -f ${confFile} -d";
+        ExecStart = "${pkgs.gobgpd}/bin/gobgpd -f ${confFile} --sdnotify";
+        ExecReload = "${pkgs.gobgpd}/bin/gobgpd -r";
+        DynamicUser = true;
+        AmbientCapabilities = "cap_net_bind_service";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/gvpe.nix b/nixos/modules/services/networking/gvpe.nix
new file mode 100644
index 00000000000..4fad37ba15e
--- /dev/null
+++ b/nixos/modules/services/networking/gvpe.nix
@@ -0,0 +1,130 @@
+# GNU Virtual Private Ethernet
+
+{config, pkgs, lib, ...}:
+
+let
+  inherit (lib) mkOption mkIf types;
+
+  cfg = config.services.gvpe;
+
+  finalConfig = if cfg.configFile != null then
+    cfg.configFile
+  else if cfg.configText != null then
+    pkgs.writeTextFile {
+      name = "gvpe.conf";
+      text = cfg.configText;
+    }
+  else
+    throw "You must either specify contents of the config file or the config file itself for GVPE";
+
+  ifupScript = if cfg.ipAddress == null || cfg.subnet == null then
+     throw "Specify IP address and subnet (with mask) for GVPE"
+   else if cfg.nodename == null then
+     throw "You must set node name for GVPE"
+   else
+   (pkgs.writeTextFile {
+    name = "gvpe-if-up";
+    text = ''
+      #! /bin/sh
+
+      export PATH=$PATH:${pkgs.iproute2}/sbin
+
+      ip link set $IFNAME up
+      ip address add ${cfg.ipAddress} dev $IFNAME
+      ip route add ${cfg.subnet} dev $IFNAME
+
+      ${cfg.customIFSetup}
+    '';
+    executable = true;
+  });
+in
+
+{
+  options = {
+    services.gvpe = {
+      enable = lib.mkEnableOption "gvpe";
+
+      nodename = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description =''
+          GVPE node name
+        '';
+      };
+      configText = mkOption {
+        default = null;
+        type = types.nullOr types.lines;
+        example = ''
+          tcp-port = 655
+          udp-port = 655
+          mtu = 1480
+          ifname = vpn0
+
+          node = alpha
+          hostname = alpha.example.org
+          connect = always
+          enable-udp = true
+          enable-tcp = true
+          on alpha if-up = if-up-0
+          on alpha pid-file = /var/gvpe/gvpe.pid
+        '';
+        description = ''
+          GVPE config contents
+        '';
+      };
+      configFile = mkOption {
+        default = null;
+        type = types.nullOr types.path;
+        example = "/root/my-gvpe-conf";
+        description = ''
+          GVPE config file, if already present
+        '';
+      };
+      ipAddress = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description = ''
+          IP address to assign to GVPE interface
+        '';
+      };
+      subnet = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        example = "10.0.0.0/8";
+        description = ''
+          IP subnet assigned to GVPE network
+        '';
+      };
+      customIFSetup = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Additional commands to apply in ifup script
+        '';
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    systemd.services.gvpe = {
+      description = "GNU Virtual Private Ethernet node";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        mkdir -p /var/gvpe
+        mkdir -p /var/gvpe/pubkey
+        chown root /var/gvpe
+        chmod 700 /var/gvpe
+        cp ${finalConfig} /var/gvpe/gvpe.conf
+        cp ${ifupScript} /var/gvpe/if-up
+      '';
+
+      script = "${pkgs.gvpe}/sbin/gvpe -c /var/gvpe -D ${cfg.nodename} "
+        + " ${cfg.nodename}.pid-file=/var/gvpe/gvpe.pid"
+        + " ${cfg.nodename}.if-up=if-up"
+        + " &> /var/log/gvpe";
+
+      serviceConfig.Restart = "always";
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/hans.nix b/nixos/modules/services/networking/hans.nix
new file mode 100644
index 00000000000..2639b4b6800
--- /dev/null
+++ b/nixos/modules/services/networking/hans.nix
@@ -0,0 +1,145 @@
+# NixOS module for hans, ip over icmp daemon
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.hans;
+
+  hansUser = "hans";
+
+in
+{
+
+  ### configuration
+
+  options = {
+
+    services.hans = {
+      clients = mkOption {
+        default = {};
+        description = ''
+          Each attribute of this option defines a systemd service that
+          runs hans. Many or none may be defined.
+          The name of each service is
+          <literal>hans-<replaceable>name</replaceable></literal>
+          where <replaceable>name</replaceable> is the name of the
+          corresponding attribute name.
+        '';
+        example = literalExpression ''
+        {
+          foo = {
+            server = "192.0.2.1";
+            extraConfig = "-v";
+          }
+        }
+        '';
+        type = types.attrsOf (types.submodule (
+        {
+          options = {
+            server = mkOption {
+              type = types.str;
+              default = "";
+              description = "IP address of server running hans";
+              example = "192.0.2.1";
+            };
+
+            extraConfig = mkOption {
+              type = types.str;
+              default = "";
+              description = "Additional command line parameters";
+              example = "-v";
+            };
+
+            passwordFile = mkOption {
+              type = types.str;
+              default = "";
+              description = "File that containts password";
+            };
+
+          };
+        }));
+      };
+
+      server = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = "enable hans server";
+        };
+
+        ip = mkOption {
+          type = types.str;
+          default = "";
+          description = "The assigned ip range";
+          example = "198.51.100.0";
+        };
+
+        respondToSystemPings = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Force hans respond to ordinary pings";
+        };
+
+        extraConfig = mkOption {
+          type = types.str;
+          default = "";
+          description = "Additional command line parameters";
+          example = "-v";
+        };
+
+        passwordFile = mkOption {
+          type = types.str;
+          default = "";
+          description = "File that containts password";
+        };
+      };
+
+    };
+  };
+
+  ### implementation
+
+  config = mkIf (cfg.server.enable || cfg.clients != {}) {
+    boot.kernel.sysctl = optionalAttrs cfg.server.respondToSystemPings {
+      "net.ipv4.icmp_echo_ignore_all" = 1;
+    };
+
+    boot.kernelModules = [ "tun" ];
+
+    systemd.services =
+    let
+      createHansClientService = name: cfg:
+      {
+        description = "hans client - ${name}";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        script = "${pkgs.hans}/bin/hans -f -u ${hansUser} ${cfg.extraConfig} -c ${cfg.server} ${optionalString (cfg.passwordFile != "") "-p $(cat \"${cfg.passwordFile}\")"}";
+        serviceConfig = {
+          RestartSec = "30s";
+          Restart = "always";
+        };
+      };
+    in
+    listToAttrs (
+      mapAttrsToList
+        (name: value: nameValuePair "hans-${name}" (createHansClientService name value))
+        cfg.clients
+    ) // {
+      hans = mkIf (cfg.server.enable) {
+        description = "hans, ip over icmp server daemon";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        script = "${pkgs.hans}/bin/hans -f -u ${hansUser} ${cfg.server.extraConfig} -s ${cfg.server.ip} ${optionalString cfg.server.respondToSystemPings "-r"} ${optionalString (cfg.server.passwordFile != "") "-p $(cat \"${cfg.server.passwordFile}\")"}";
+      };
+    };
+
+    users.users.${hansUser} = {
+      description = "Hans daemon user";
+      isSystemUser = true;
+    };
+  };
+
+  meta.maintainers = with maintainers; [ ];
+}
diff --git a/nixos/modules/services/networking/haproxy.nix b/nixos/modules/services/networking/haproxy.nix
new file mode 100644
index 00000000000..e9d72b35499
--- /dev/null
+++ b/nixos/modules/services/networking/haproxy.nix
@@ -0,0 +1,112 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.haproxy;
+
+  haproxyCfg = pkgs.writeText "haproxy.conf" ''
+    global
+      # needed for hot-reload to work without dropping packets in multi-worker mode
+      stats socket /run/haproxy/haproxy.sock mode 600 expose-fd listeners level user
+
+    ${cfg.config}
+  '';
+
+in
+with lib;
+{
+  options = {
+    services.haproxy = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable HAProxy, the reliable, high performance TCP/HTTP
+          load balancer.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "haproxy";
+        description = "User account under which haproxy runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "haproxy";
+        description = "Group account under which haproxy runs.";
+      };
+
+      config = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = ''
+          Contents of the HAProxy configuration file,
+          <filename>haproxy.conf</filename>.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [{
+      assertion = cfg.config != null;
+      message = "You must provide services.haproxy.config.";
+    }];
+
+    # configuration file indirection is needed to support reloading
+    environment.etc."haproxy.cfg".source = haproxyCfg;
+
+    systemd.services.haproxy = {
+      description = "HAProxy";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        Type = "notify";
+        ExecStartPre = [
+          # when the master process receives USR2, it reloads itself using exec(argv[0]),
+          # so we create a symlink there and update it before reloading
+          "${pkgs.coreutils}/bin/ln -sf ${pkgs.haproxy}/sbin/haproxy /run/haproxy/haproxy"
+          # when running the config test, don't be quiet so we can see what goes wrong
+          "/run/haproxy/haproxy -c -f ${haproxyCfg}"
+        ];
+        ExecStart = "/run/haproxy/haproxy -Ws -f /etc/haproxy.cfg -p /run/haproxy/haproxy.pid";
+        # support reloading
+        ExecReload = [
+          "${pkgs.haproxy}/sbin/haproxy -c -f ${haproxyCfg}"
+          "${pkgs.coreutils}/bin/ln -sf ${pkgs.haproxy}/sbin/haproxy /run/haproxy/haproxy"
+          "${pkgs.coreutils}/bin/kill -USR2 $MAINPID"
+        ];
+        KillMode = "mixed";
+        SuccessExitStatus = "143";
+        Restart = "always";
+        RuntimeDirectory = "haproxy";
+        # upstream hardening options
+        NoNewPrivileges = true;
+        ProtectHome = true;
+        ProtectSystem = "strict";
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        SystemCallFilter= "~@cpu-emulation @keyring @module @obsolete @raw-io @reboot @swap @sync";
+        # needed in case we bind to port < 1024
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+      };
+    };
+
+    users.users = optionalAttrs (cfg.user == "haproxy") {
+      haproxy = {
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "haproxy") {
+      haproxy = {};
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/headscale.nix b/nixos/modules/services/networking/headscale.nix
new file mode 100644
index 00000000000..091d2a938cd
--- /dev/null
+++ b/nixos/modules/services/networking/headscale.nix
@@ -0,0 +1,490 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.headscale;
+
+  dataDir = "/var/lib/headscale";
+  runDir = "/run/headscale";
+
+  settingsFormat = pkgs.formats.yaml { };
+  configFile = settingsFormat.generate "headscale.yaml" cfg.settings;
+in
+{
+  options = {
+    services.headscale = {
+      enable = mkEnableOption "headscale, Open Source coordination server for Tailscale";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.headscale;
+        defaultText = literalExpression "pkgs.headscale";
+        description = ''
+          Which headscale package to use for the running server.
+        '';
+      };
+
+      user = mkOption {
+        default = "headscale";
+        type = types.str;
+        description = ''
+          User account under which headscale runs.
+          <note><para>
+          If left as the default value this user will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the user exists before the headscale service starts.
+          </para></note>
+        '';
+      };
+
+      group = mkOption {
+        default = "headscale";
+        type = types.str;
+        description = ''
+          Group under which headscale runs.
+          <note><para>
+          If left as the default value this group will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the user exists before the headscale service starts.
+          </para></note>
+        '';
+      };
+
+      serverUrl = mkOption {
+        type = types.str;
+        default = "http://127.0.0.1:8080";
+        description = ''
+          The url clients will connect to.
+        '';
+        example = "https://myheadscale.example.com:443";
+      };
+
+      address = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = ''
+          Listening address of headscale.
+        '';
+        example = "0.0.0.0";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 8080;
+        description = ''
+          Listening port of headscale.
+        '';
+        example = 443;
+      };
+
+      privateKeyFile = mkOption {
+        type = types.path;
+        default = "${dataDir}/private.key";
+        description = ''
+          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 = ''
+            List of urls containing DERP maps.
+            See <link xlink:href="https://tailscale.com/blog/how-tailscale-works/">How Tailscale works</link> for more information on DERP maps.
+          '';
+        };
+
+        paths = mkOption {
+          type = types.listOf types.path;
+          default = [ ];
+          description = ''
+            List of file paths containing DERP maps.
+            See <link xlink:href="https://tailscale.com/blog/how-tailscale-works/">How Tailscale works</link> for more information on DERP maps.
+          '';
+        };
+
+
+        autoUpdate = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether to automatically update DERP maps on a set frequency.
+          '';
+          example = false;
+        };
+
+        updateFrequency = mkOption {
+          type = types.str;
+          default = "24h";
+          description = ''
+            Frequency to update DERP maps.
+          '';
+          example = "5m";
+        };
+
+      };
+
+      ephemeralNodeInactivityTimeout = mkOption {
+        type = types.str;
+        default = "30m";
+        description = ''
+          Time before an inactive ephemeral node is deleted.
+        '';
+        example = "5m";
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum [ "sqlite3" "postgres" ];
+          example = "postgres";
+          default = "sqlite3";
+          description = "Database engine to use.";
+        };
+
+        host = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          example = "127.0.0.1";
+          description = "Database host address.";
+        };
+
+        port = mkOption {
+          type = types.nullOr types.port;
+          default = null;
+          example = 3306;
+          description = "Database host port.";
+        };
+
+        name = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          example = "headscale";
+          description = "Database name.";
+        };
+
+        user = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          example = "headscale";
+          description = "Database user.";
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          example = "/run/keys/headscale-dbpassword";
+          description = ''
+            A file containing the password corresponding to
+            <option>database.user</option>.
+          '';
+        };
+
+        path = mkOption {
+          type = types.nullOr types.str;
+          default = "${dataDir}/db.sqlite";
+          description = "Path to the sqlite3 database file.";
+        };
+      };
+
+      logLevel = mkOption {
+        type = types.str;
+        default = "info";
+        description = ''
+          headscale log level.
+        '';
+        example = "debug";
+      };
+
+      dns = {
+        nameservers = mkOption {
+          type = types.listOf types.str;
+          default = [ "1.1.1.1" ];
+          description = ''
+            List of nameservers to pass to Tailscale clients.
+          '';
+        };
+
+        domains = mkOption {
+          type = types.listOf types.str;
+          default = [ ];
+          description = ''
+            Search domains to inject to Tailscale clients.
+          '';
+          example = [ "mydomain.internal" ];
+        };
+
+        magicDns = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            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 = ''
+            Defines the base domain to create the hostnames for MagicDNS.
+            <option>baseDomain</option> must be a FQDNs, without the trailing dot.
+            The FQDN of the hosts will be
+            <literal>hostname.namespace.base_domain</literal> (e.g.
+            <literal>myhost.mynamespace.example.com</literal>).
+          '';
+        };
+      };
+
+      openIdConnect = {
+        issuer = mkOption {
+          type = types.str;
+          default = "";
+          description = ''
+            URL to OpenID issuer.
+          '';
+          example = "https://openid.example.com";
+        };
+
+        clientId = mkOption {
+          type = types.str;
+          default = "";
+          description = ''
+            OpenID Connect client ID.
+          '';
+        };
+
+        clientSecretFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          description = ''
+            Path to OpenID Connect client secret file.
+          '';
+        };
+
+        domainMap = mkOption {
+          type = types.attrsOf types.str;
+          default = { };
+          description = ''
+            Domain map is used to map incomming 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 = ''
+              Domain name to request a TLS certificate for.
+            '';
+          };
+          challengeType = mkOption {
+            type = types.enum [ "TLS_ALPN-01" "HTTP-01" ];
+            default = "HTTP-01";
+            description = ''
+              Type of ACME challenge to use, currently supported types:
+              <literal>HTTP-01</literal> or <literal>TLS_ALPN-01</literal>.
+            '';
+          };
+          httpListen = mkOption {
+            type = types.nullOr types.str;
+            default = ":http";
+            description = ''
+              When HTTP-01 challenge is chosen, letsencrypt must set up a
+              verification endpoint, and it will be listening on:
+              <literal>:http = port 80</literal>.
+            '';
+          };
+        };
+
+        certFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          description = ''
+            Path to already created certificate.
+          '';
+        };
+        keyFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          description = ''
+            Path to key for already created certificate.
+          '';
+        };
+      };
+
+      aclPolicyFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          Path to a file containg ACL policies.
+        '';
+      };
+
+      settings = mkOption {
+        type = settingsFormat.type;
+        default = { };
+        description = ''
+          Overrides to <filename>config.yaml</filename> as a Nix attribute set.
+          This option is ideal for overriding settings not exposed as Nix options.
+          Check the <link xlink:href="https://github.com/juanfont/headscale/blob/main/config-example.yaml">example config</link>
+          for possible options.
+        '';
+      };
+
+
+    };
+
+  };
+  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
+    # allow both the Server and the Client use it to find the socket
+    # for communication.
+    environment.etc."headscale/config.yaml".source = configFile;
+
+    users.groups.headscale = mkIf (cfg.group == "headscale") { };
+
+    users.users.headscale = mkIf (cfg.user == "headscale") {
+      description = "headscale user";
+      home = dataDir;
+      group = cfg.group;
+      isSystemUser = true;
+    };
+
+    systemd.services.headscale = {
+      description = "headscale coordination server for Tailscale";
+      after = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ configFile ];
+
+      script = ''
+        ${optionalString (cfg.database.passwordFile != null) ''
+          export HEADSCALE_DB_PASS="$(head -n1 ${escapeShellArg cfg.database.passwordFile})"
+        ''}
+
+        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" "~@priviledged" "@chown" ];
+          SystemCallArchitectures = "native";
+          RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
+        };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ kradalby ];
+}
diff --git a/nixos/modules/services/networking/helpers.nix b/nixos/modules/services/networking/helpers.nix
new file mode 100644
index 00000000000..d7d42de0e3a
--- /dev/null
+++ b/nixos/modules/services/networking/helpers.nix
@@ -0,0 +1,11 @@
+{ config, lib, ... }: ''
+  # Helper command to manipulate both the IPv4 and IPv6 tables.
+  ip46tables() {
+    iptables -w "$@"
+    ${
+      lib.optionalString config.networking.enableIPv6 ''
+        ip6tables -w "$@"
+      ''
+    }
+  }
+''
diff --git a/nixos/modules/services/networking/hostapd.nix b/nixos/modules/services/networking/hostapd.nix
new file mode 100644
index 00000000000..f719ff59cc7
--- /dev/null
+++ b/nixos/modules/services/networking/hostapd.nix
@@ -0,0 +1,219 @@
+{ config, lib, pkgs, utils, ... }:
+
+# TODO:
+#
+# asserts
+#   ensure that the nl80211 module is loaded/compiled in the kernel
+#   wpa_supplicant and hostapd on the same wireless interface doesn't make any sense
+
+with lib;
+
+let
+
+  cfg = config.services.hostapd;
+
+  escapedInterface = utils.escapeSystemdPath cfg.interface;
+
+  configFile = pkgs.writeText "hostapd.conf" ''
+    interface=${cfg.interface}
+    driver=${cfg.driver}
+    ssid=${cfg.ssid}
+    hw_mode=${cfg.hwMode}
+    channel=${toString cfg.channel}
+    ${optionalString (cfg.countryCode != null) "country_code=${cfg.countryCode}"}
+    ${optionalString (cfg.countryCode != null) "ieee80211d=1"}
+
+    # logging (debug level)
+    logger_syslog=-1
+    logger_syslog_level=${toString cfg.logLevel}
+    logger_stdout=-1
+    logger_stdout_level=${toString cfg.logLevel}
+
+    ctrl_interface=/run/hostapd
+    ctrl_interface_group=${cfg.group}
+
+    ${optionalString cfg.wpa ''
+      wpa=2
+      wpa_passphrase=${cfg.wpaPassphrase}
+    ''}
+    ${optionalString cfg.noScan "noscan=1"}
+
+    ${cfg.extraConfig}
+  '' ;
+
+in
+
+{
+  ###### interface
+
+  options = {
+
+    services.hostapd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable putting a wireless interface into infrastructure mode,
+          allowing other wireless devices to associate with the wireless
+          interface and do wireless networking. A simple access point will
+          <option>enable hostapd.wpa</option>,
+          <option>hostapd.wpaPassphrase</option>, and
+          <option>hostapd.ssid</option>, as well as DHCP on the wireless
+          interface to provide IP addresses to the associated stations, and
+          NAT (from the wireless interface to an upstream interface).
+        '';
+      };
+
+      interface = mkOption {
+        default = "";
+        example = "wlp2s0";
+        type = types.str;
+        description = ''
+          The interfaces <command>hostapd</command> will use.
+        '';
+      };
+
+      noScan = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Do not scan for overlapping BSSs in HT40+/- mode.
+          Caution: turning this on will violate regulatory requirements!
+        '';
+      };
+
+      driver = mkOption {
+        default = "nl80211";
+        example = "hostapd";
+        type = types.str;
+        description = ''
+          Which driver <command>hostapd</command> will use.
+          Most applications will probably use the default.
+        '';
+      };
+
+      ssid = mkOption {
+        default = "nixos";
+        example = "mySpecialSSID";
+        type = types.str;
+        description = "SSID to be used in IEEE 802.11 management frames.";
+      };
+
+      hwMode = mkOption {
+        default = "g";
+        type = types.enum [ "a" "b" "g" ];
+        description = ''
+          Operation mode.
+          (a = IEEE 802.11a, b = IEEE 802.11b, g = IEEE 802.11g).
+        '';
+      };
+
+      channel = mkOption {
+        default = 7;
+        example = 11;
+        type = types.int;
+        description = ''
+          Channel number (IEEE 802.11)
+          Please note that some drivers do not use this value from
+          <command>hostapd</command> and the channel will need to be configured
+          separately with <command>iwconfig</command>.
+        '';
+      };
+
+      group = mkOption {
+        default = "wheel";
+        example = "network";
+        type = types.str;
+        description = ''
+          Members of this group can control <command>hostapd</command>.
+        '';
+      };
+
+      wpa = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable WPA (IEEE 802.11i/D3.0) to authenticate with the access point.
+        '';
+      };
+
+      wpaPassphrase = mkOption {
+        default = "my_sekret";
+        example = "any_64_char_string";
+        type = types.str;
+        description = ''
+          WPA-PSK (pre-shared-key) passphrase. Clients will need this
+          passphrase to associate with this access point.
+          Warning: This passphrase will get put into a world-readable file in
+          the Nix store!
+        '';
+      };
+
+      logLevel = mkOption {
+        default = 2;
+        type = types.int;
+        description = ''
+          Levels (minimum value for logged events):
+          0 = verbose debugging
+          1 = debugging
+          2 = informational messages
+          3 = notification
+          4 = warning
+        '';
+      };
+
+      countryCode = mkOption {
+        default = null;
+        example = "US";
+        type = with types; nullOr str;
+        description = ''
+          Country code (ISO/IEC 3166-1). Used to set regulatory domain.
+          Set as needed to indicate country in which device is operating.
+          This can limit available channels and transmit power.
+          These two octets are used as the first two octets of the Country String
+          (dot11CountryString).
+          If set this enables IEEE 802.11d. This advertises the countryCode and
+          the set of allowed channels and transmit power levels based on the
+          regulatory limits.
+        '';
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        example = ''
+          auth_algo=0
+          ieee80211n=1
+          ht_capab=[HT40-][SHORT-GI-40][DSSS_CCK-40]
+          '';
+        type = types.lines;
+        description = "Extra configuration options to put in hostapd.conf.";
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages =  [ pkgs.hostapd ];
+
+    services.udev.packages = optional (cfg.countryCode != null) [ pkgs.crda ];
+
+    systemd.services.hostapd =
+      { description = "hostapd wireless AP";
+
+        path = [ pkgs.hostapd ];
+        after = [ "sys-subsystem-net-devices-${escapedInterface}.device" ];
+        bindsTo = [ "sys-subsystem-net-devices-${escapedInterface}.device" ];
+        requiredBy = [ "network-link-${cfg.interface}.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig =
+          { ExecStart = "${pkgs.hostapd}/bin/hostapd ${configFile}";
+            Restart = "always";
+          };
+      };
+  };
+}
diff --git a/nixos/modules/services/networking/htpdate.nix b/nixos/modules/services/networking/htpdate.nix
new file mode 100644
index 00000000000..6954e5b060c
--- /dev/null
+++ b/nixos/modules/services/networking/htpdate.nix
@@ -0,0 +1,80 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  inherit (pkgs) htpdate;
+
+  cfg = config.services.htpdate;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.htpdate = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable htpdate daemon.
+        '';
+      };
+
+      extraOptions = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Additional command line arguments to pass to htpdate.
+        '';
+      };
+
+      servers = mkOption {
+        type = types.listOf types.str;
+        default = [ "www.google.com" ];
+        description = ''
+          HTTP servers to use for time synchronization.
+        '';
+      };
+
+      proxy = mkOption {
+        type = types.str;
+        default = "";
+        example = "127.0.0.1:8118";
+        description = ''
+          HTTP proxy used for requests.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.htpdate = {
+      description = "htpdate daemon";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "forking";
+        PIDFile = "/run/htpdate.pid";
+        ExecStart = concatStringsSep " " [
+          "${htpdate}/bin/htpdate"
+          "-D -u nobody"
+          "-a -s"
+          "-l"
+          "${optionalString (cfg.proxy != "") "-P ${cfg.proxy}"}"
+          "${cfg.extraOptions}"
+          "${concatStringsSep " " cfg.servers}"
+        ];
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/hylafax/default.nix b/nixos/modules/services/networking/hylafax/default.nix
new file mode 100644
index 00000000000..d8ffa3fc04d
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/default.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+{
+
+  imports = [
+    ./options.nix
+    ./systemd.nix
+  ];
+
+  config = lib.modules.mkIf config.services.hylafax.enable {
+    environment.systemPackages = [ pkgs.hylafaxplus ];
+    users.users.uucp = {
+      uid = config.ids.uids.uucp;
+      group = "uucp";
+      description = "Unix-to-Unix CoPy system";
+      isSystemUser = true;
+      inherit (config.users.users.nobody) home;
+    };
+    assertions = [{
+      assertion = config.services.hylafax.modems != {};
+      message = ''
+        HylaFAX cannot be used without modems.
+        Please define at least one modem with
+        <option>config.services.hylafax.modems</option>.
+      '';
+    }];
+  };
+
+  meta.maintainers = [ lib.maintainers.yarny ];
+
+}
diff --git a/nixos/modules/services/networking/hylafax/faxq-default.nix b/nixos/modules/services/networking/hylafax/faxq-default.nix
new file mode 100644
index 00000000000..9b634650cf7
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/faxq-default.nix
@@ -0,0 +1,12 @@
+{ ... }:
+
+# see man:hylafax-config(5)
+
+{
+
+  ModemGroup = [ ''"any:0:.*"'' ];
+  ServerTracing = "0x78701";
+  SessionTracing = "0x78701";
+  UUCPLockDir = "/var/lock";
+
+}
diff --git a/nixos/modules/services/networking/hylafax/faxq-wait.sh b/nixos/modules/services/networking/hylafax/faxq-wait.sh
new file mode 100755
index 00000000000..1826aa30e62
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/faxq-wait.sh
@@ -0,0 +1,29 @@
+#! @runtimeShell@ -e
+
+# skip this if there are no modems at all
+if ! stat -t "@spoolAreaPath@"/etc/config.* >/dev/null 2>&1
+then
+  exit 0
+fi
+
+echo "faxq started, waiting for modem(s) to initialize..."
+
+for i in `seq @timeoutSec@0 -1 0`  # gracefully timeout
+do
+  sleep 0.1
+  # done if status files exist, but don't mention initialization
+  if \
+    stat -t "@spoolAreaPath@"/status/* >/dev/null 2>&1 \
+    && \
+    ! grep --silent --ignore-case 'initializing server' \
+    "@spoolAreaPath@"/status/*
+  then
+    echo "modem(s) apparently ready"
+    exit 0
+  fi
+  # if i reached 0, modems probably failed to initialize
+  if test $i -eq 0
+  then
+    echo "warning: modem initialization timed out"
+  fi
+done
diff --git a/nixos/modules/services/networking/hylafax/hfaxd-default.nix b/nixos/modules/services/networking/hylafax/hfaxd-default.nix
new file mode 100644
index 00000000000..8999dae57f4
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/hfaxd-default.nix
@@ -0,0 +1,10 @@
+{ ... }:
+
+# see man:hfaxd(8)
+
+{
+
+  ServerTracing = "0x91";
+  XferLogFile = "/clientlog";
+
+}
diff --git a/nixos/modules/services/networking/hylafax/modem-default.nix b/nixos/modules/services/networking/hylafax/modem-default.nix
new file mode 100644
index 00000000000..707b8209282
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/modem-default.nix
@@ -0,0 +1,22 @@
+{ pkgs, ... }:
+
+# see man:hylafax-config(5)
+
+{
+
+  TagLineFont = "etc/LiberationSans-25.pcf";
+  TagLineLocale = "en_US.UTF-8";
+
+  AdminGroup = "root";  # groups that can change server config
+  AnswerRotary = "fax";  # don't accept anything else but faxes
+  LogFileMode = "0640";
+  PriorityScheduling = true;
+  RecvFileMode = "0640";
+  ServerTracing = "0x78701";
+  SessionTracing = "0x78701";
+  UUCPLockDir = "/var/lock";
+
+  SendPageCmd = "${pkgs.coreutils}/bin/false";  # prevent pager transmit
+  SendUUCPCmd = "${pkgs.coreutils}/bin/false";  # prevent UUCP transmit
+
+}
diff --git a/nixos/modules/services/networking/hylafax/options.nix b/nixos/modules/services/networking/hylafax/options.nix
new file mode 100644
index 00000000000..8f621b61002
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/options.nix
@@ -0,0 +1,372 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+  inherit (lib.options) literalExpression mkEnableOption mkOption;
+  inherit (lib.types) bool enum ints lines attrsOf nonEmptyStr nullOr path str submodule;
+  inherit (lib.modules) mkDefault mkIf mkMerge;
+
+  commonDescr = ''
+    Values can be either strings or integers
+    (which will be added to the config file verbatimly)
+    or lists thereof
+    (which will be translated to multiple
+    lines with the same configuration key).
+    Boolean values are translated to "Yes" or "No".
+    The default contains some reasonable
+    configuration to yield an operational system.
+  '';
+
+  configAttrType =
+    # Options in HylaFAX configuration files can be
+    # booleans, strings, integers, or list thereof
+    # representing multiple config directives with the same key.
+    # This type definition resolves all
+    # those types into a list of strings.
+    let
+      inherit (lib.types) attrsOf coercedTo int listOf;
+      innerType = coercedTo bool (x: if x then "Yes" else "No")
+        (coercedTo int (toString) str);
+    in
+      attrsOf (coercedTo innerType lib.singleton (listOf innerType));
+
+  cfg = config.services.hylafax;
+
+  modemConfigOptions = { name, config, ... }: {
+    options = {
+      name = mkOption {
+        type = nonEmptyStr;
+        example = "ttyS1";
+        description = ''
+          Name of modem device,
+          will be searched for in <filename>/dev</filename>.
+        '';
+      };
+      type = mkOption {
+        type = nonEmptyStr;
+        example = "cirrus";
+        description = ''
+          Name of modem configuration file,
+          will be searched for in <filename>config</filename>
+          in the spooling area directory.
+        '';
+      };
+      config = mkOption {
+        type = configAttrType;
+        example = {
+          AreaCode = "49";
+          LocalCode = "30";
+          FAXNumber = "123456";
+          LocalIdentifier = "LostInBerlin";
+        };
+        description = ''
+          Attribute set of values for the given modem.
+          ${commonDescr}
+          Options defined here override options in
+          <option>commonModemConfig</option> for this modem.
+        '';
+      };
+    };
+    config.name = mkDefault name;
+    config.config.Include = [ "config/${config.type}" ];
+  };
+
+  defaultConfig =
+    let
+      inherit (config.security) wrapperDir;
+      inherit (config.services.mail.sendmailSetuidWrapper) program;
+      mkIfDefault = cond: value: mkIf cond (mkDefault value);
+      noWrapper = config.services.mail.sendmailSetuidWrapper==null;
+      # If a sendmail setuid wrapper exists,
+      # we add the path to the default configuration file.
+      # Otherwise, we use `false` to provoke
+      # an error if hylafax tries to use it.
+      c.sendmailPath = mkMerge [
+        (mkIfDefault noWrapper "${pkgs.coreutils}/bin/false")
+        (mkIfDefault (!noWrapper) "${wrapperDir}/${program}")
+      ];
+      importDefaultConfig = file:
+        lib.attrsets.mapAttrs
+        (lib.trivial.const mkDefault)
+        (import file { inherit pkgs; });
+      c.commonModemConfig = importDefaultConfig ./modem-default.nix;
+      c.faxqConfig = importDefaultConfig ./faxq-default.nix;
+      c.hfaxdConfig = importDefaultConfig ./hfaxd-default.nix;
+    in
+      c;
+
+  localConfig =
+    let
+      c.hfaxdConfig.UserAccessFile = cfg.userAccessFile;
+      c.faxqConfig = lib.attrsets.mapAttrs
+        (lib.trivial.const (v: mkIf (v!=null) v))
+        {
+          AreaCode = cfg.areaCode;
+          CountryCode = cfg.countryCode;
+          LongDistancePrefix = cfg.longDistancePrefix;
+          InternationalPrefix = cfg.internationalPrefix;
+        };
+      c.commonModemConfig = c.faxqConfig;
+    in
+      c;
+
+in
+
+
+{
+
+
+  options.services.hylafax = {
+
+    enable = mkEnableOption "HylaFAX server";
+
+    autostart = mkOption {
+      type = bool;
+      default = true;
+      example = false;
+      description = ''
+        Autostart the HylaFAX queue manager at system start.
+        If this is <literal>false</literal>, the queue manager
+        will still be started if there are pending
+        jobs or if a user tries to connect to it.
+      '';
+    };
+
+    countryCode = mkOption {
+      type = nullOr nonEmptyStr;
+      default = null;
+      example = "49";
+      description = "Country code for server and all modems.";
+    };
+
+    areaCode = mkOption {
+      type = nullOr nonEmptyStr;
+      default = null;
+      example = "30";
+      description = "Area code for server and all modems.";
+    };
+
+    longDistancePrefix = mkOption {
+      type = nullOr str;
+      default = null;
+      example = "0";
+      description = "Long distance prefix for server and all modems.";
+    };
+
+    internationalPrefix = mkOption {
+      type = nullOr str;
+      default = null;
+      example = "00";
+      description = "International prefix for server and all modems.";
+    };
+
+    spoolAreaPath = mkOption {
+      type = path;
+      default = "/var/spool/fax";
+      description = ''
+        The spooling area will be created/maintained
+        at the location given here.
+      '';
+    };
+
+    userAccessFile = mkOption {
+      type = path;
+      default = "/etc/hosts.hfaxd";
+      description = ''
+        The <filename>hosts.hfaxd</filename>
+        file entry in the spooling area
+        will be symlinked to the location given here.
+        This file must exist and be
+        readable only by the <literal>uucp</literal> user.
+        See hosts.hfaxd(5) for details.
+        This configuration permits access for all users:
+        <literal>
+          environment.etc."hosts.hfaxd" = {
+            mode = "0600";
+            user = "uucp";
+            text = ".*";
+          };
+        </literal>
+        Note that host-based access can be controlled with
+        <option>config.systemd.sockets.hylafax-hfaxd.listenStreams</option>;
+        by default, only 127.0.0.1 is permitted to connect.
+      '';
+    };
+
+    sendmailPath = mkOption {
+      type = path;
+      example = literalExpression ''"''${pkgs.postfix}/bin/sendmail"'';
+      # '' ;  # fix vim
+      description = ''
+        Path to <filename>sendmail</filename> program.
+        The default uses the local sendmail wrapper
+        (see <option>config.services.mail.sendmailSetuidWrapper</option>),
+        otherwise the <filename>false</filename>
+        binary to cause an error if used.
+      '';
+    };
+
+    hfaxdConfig = mkOption {
+      type = configAttrType;
+      example.RecvqProtection = "0400";
+      description = ''
+        Attribute set of lines for the global
+        hfaxd config file <filename>etc/hfaxd.conf</filename>.
+        ${commonDescr}
+      '';
+    };
+
+    faxqConfig = mkOption {
+      type = configAttrType;
+      example = {
+        InternationalPrefix = "00";
+        LongDistancePrefix = "0";
+      };
+      description = ''
+        Attribute set of lines for the global
+        faxq config file <filename>etc/config</filename>.
+        ${commonDescr}
+      '';
+    };
+
+    commonModemConfig = mkOption {
+      type = configAttrType;
+      example = {
+        InternationalPrefix = "00";
+        LongDistancePrefix = "0";
+      };
+      description = ''
+        Attribute set of default values for
+        modem config files <filename>etc/config.*</filename>.
+        ${commonDescr}
+        Think twice before changing
+        paths of fax-processing scripts.
+      '';
+    };
+
+    modems = mkOption {
+      type = attrsOf (submodule [ modemConfigOptions ]);
+      default = {};
+      example.ttyS1 = {
+        type = "cirrus";
+        config = {
+          FAXNumber = "123456";
+          LocalIdentifier = "Smith";
+        };
+      };
+      description = ''
+        Description of installed modems.
+        At least on modem must be defined
+        to enable the HylaFAX server.
+      '';
+    };
+
+    spoolExtraInit = mkOption {
+      type = lines;
+      default = "";
+      example = "chmod 0755 .  # everyone may read my faxes";
+      description = ''
+        Additional shell code that is executed within the
+        spooling area directory right after its setup.
+      '';
+    };
+
+    faxcron.enable.spoolInit = mkEnableOption ''
+      Purge old files from the spooling area with
+      <filename>faxcron</filename>
+      each time the spooling area is initialized.
+    '';
+    faxcron.enable.frequency = mkOption {
+      type = nullOr nonEmptyStr;
+      default = null;
+      example = "daily";
+      description = ''
+        Purge old files from the spooling area with
+        <filename>faxcron</filename> with the given frequency
+        (see systemd.time(7)).
+      '';
+    };
+    faxcron.infoDays = mkOption {
+      type = ints.positive;
+      default = 30;
+      description = ''
+        Set the expiration time for data in the
+        remote machine information directory in days.
+      '';
+    };
+    faxcron.logDays = mkOption {
+      type = ints.positive;
+      default = 30;
+      description = ''
+        Set the expiration time for
+        session trace log files in days.
+      '';
+    };
+    faxcron.rcvDays = mkOption {
+      type = ints.positive;
+      default = 7;
+      description = ''
+        Set the expiration time for files in
+        the received facsimile queue in days.
+      '';
+    };
+
+    faxqclean.enable.spoolInit = mkEnableOption ''
+      Purge old files from the spooling area with
+      <filename>faxqclean</filename>
+      each time the spooling area is initialized.
+    '';
+    faxqclean.enable.frequency = mkOption {
+      type = nullOr nonEmptyStr;
+      default = null;
+      example = "daily";
+      description = ''
+        Purge old files from the spooling area with
+        <filename>faxcron</filename> with the given frequency
+        (see systemd.time(7)).
+      '';
+    };
+    faxqclean.archiving = mkOption {
+      type = enum [ "never" "as-flagged" "always" ];
+      default = "as-flagged";
+      example = "always";
+      description = ''
+        Enable or suppress job archiving:
+        <literal>never</literal> disables job archiving,
+        <literal>as-flagged</literal> archives jobs that
+        have been flagged for archiving by sendfax,
+        <literal>always</literal> forces archiving of all jobs.
+        See also sendfax(1) and faxqclean(8).
+      '';
+    };
+    faxqclean.doneqMinutes = mkOption {
+      type = ints.positive;
+      default = 15;
+      example = literalExpression "24*60";
+      description = ''
+        Set the job
+        age threshold (in minutes) that controls how long
+        jobs may reside in the doneq directory.
+      '';
+    };
+    faxqclean.docqMinutes = mkOption {
+      type = ints.positive;
+      default = 60;
+      example = literalExpression "24*60";
+      description = ''
+        Set the document
+        age threshold (in minutes) that controls how long
+        unreferenced files may reside in the docq directory.
+      '';
+    };
+
+  };
+
+
+  config.services.hylafax =
+    mkIf
+    (config.services.hylafax.enable)
+    (mkMerge [ defaultConfig localConfig ])
+  ;
+
+}
diff --git a/nixos/modules/services/networking/hylafax/spool.sh b/nixos/modules/services/networking/hylafax/spool.sh
new file mode 100755
index 00000000000..8b723df77df
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/spool.sh
@@ -0,0 +1,111 @@
+#! @runtimeShell@ -e
+
+# The following lines create/update the HylaFAX spool directory:
+# Subdirectories/files with persistent data are kept,
+# other directories/files are removed/recreated,
+# mostly from the template spool
+# directory in the HylaFAX package.
+
+# This block explains how the spool area is
+# derived from the spool template in the HylaFAX package:
+#
+#                  + capital letter: directory; file otherwise
+#                  + P/p: persistent directory
+#                  + F/f: directory with symlinks per entry
+#                  + T/t: temporary data
+#                  + S/s: single symlink into package
+#                  |
+#                  | + u: change ownership to uucp:uucp
+#                  | + U: ..also change access mode to user-only
+#                  | |
+# archive          P U
+# bin              S
+# client           T u  (client connection info)
+# config           S
+# COPYRIGHT        s
+# dev              T u  (maybe some FIFOs)
+# docq             P U
+# doneq            P U
+# etc              F    contains customized config files!
+# etc/hosts.hfaxd  f
+# etc/xferfaxlog   f
+# info             P u  (database of called devices)
+# log              P u  (communication logs)
+# pollq            P U
+# recvq            P u
+# sendq            P U
+# status           T u  (modem status info files)
+# tmp              T U
+
+
+shopt -s dotglob  # if bash sees "*", it also includes dot files
+lnsym () { ln --symbol "$@" ; }
+lnsymfrc () { ln --symbolic --force "$@" ; }
+cprd () { cp --remove-destination "$@" ; }
+update () { install --owner=@faxuser@ --group=@faxgroup@ "$@" ; }
+
+
+## create/update spooling area
+
+update --mode=0750 -d "@spoolAreaPath@"
+cd "@spoolAreaPath@"
+
+persist=(archive docq doneq info log pollq recvq sendq)
+
+# remove entries that don't belong here
+touch dummy  # ensure "*" resolves to something
+for k in *
+do
+  keep=0
+  for j in "${persist[@]}" xferfaxlog clientlog faxcron.lastrun
+  do
+    if test "$k" == "$j"
+    then
+      keep=1
+      break
+    fi
+  done
+  if test "$keep" == "0"
+  then
+    rm --recursive "$k"
+  fi
+done
+
+# create persistent data directories (unless they exist already)
+update --mode=0700 -d "${persist[@]}"
+chmod 0755 info log recvq
+
+# create ``xferfaxlog``, ``faxcron.lastrun``, ``clientlog``
+touch clientlog faxcron.lastrun xferfaxlog
+chown @faxuser@:@faxgroup@ clientlog faxcron.lastrun xferfaxlog
+
+# create symlinks for frozen directories/files
+lnsym --target-directory=. "@hylafaxplus@"/spool/{COPYRIGHT,bin,config}
+
+# create empty temporary directories
+update --mode=0700 -d client dev status
+update -d tmp
+
+
+## create and fill etc
+
+install -d "@spoolAreaPath@/etc"
+cd "@spoolAreaPath@/etc"
+
+# create symlinks to all files in template's etc
+lnsym --target-directory=. "@hylafaxplus@/spool/etc"/*
+
+# set LOCKDIR in setup.cache
+sed --regexp-extended 's|^(UUCP_LOCKDIR=).*$|\1'"'@lockPath@'|g" --in-place setup.cache
+
+# etc/{xferfaxlog,lastrun} are stored in the spool root
+lnsymfrc --target-directory=. ../xferfaxlog
+lnsymfrc --no-target-directory ../faxcron.lastrun lastrun
+
+# etc/hosts.hfaxd is provided by the NixOS configuration
+lnsymfrc --no-target-directory "@userAccessFile@" hosts.hfaxd
+
+# etc/config and etc/config.${DEVID} must be copied:
+# hfaxd reads these file after locking itself up in a chroot
+cprd --no-target-directory "@globalConfigPath@" config
+cprd --target-directory=. "@modemConfigPath@"/*
diff --git a/nixos/modules/services/networking/hylafax/systemd.nix b/nixos/modules/services/networking/hylafax/systemd.nix
new file mode 100644
index 00000000000..4506bbbc5eb
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/systemd.nix
@@ -0,0 +1,249 @@
+{ config, lib, pkgs, ... }:
+
+
+let
+
+  inherit (lib) mkIf mkMerge;
+  inherit (lib) concatStringsSep optionalString;
+
+  cfg = config.services.hylafax;
+  mapModems = lib.forEach (lib.attrValues cfg.modems);
+
+  mkConfigFile = name: conf:
+    # creates hylafax config file,
+    # makes sure "Include" is listed *first*
+    let
+      mkLines = lib.flip lib.pipe [
+        (lib.mapAttrsToList (key: map (val: "${key}: ${val}")))
+        lib.concatLists
+      ];
+      include = mkLines { Include = conf.Include or []; };
+      other = mkLines ( conf // { Include = []; } );
+    in
+      pkgs.writeText "hylafax-config${name}"
+      (concatStringsSep "\n" (include ++ other));
+
+  globalConfigPath = mkConfigFile "" cfg.faxqConfig;
+
+  modemConfigPath =
+    let
+      mkModemConfigFile = { config, name, ... }:
+        mkConfigFile ".${name}"
+        (cfg.commonModemConfig // config);
+      mkLine = { name, type, ... }@modem: ''
+        # check if modem config file exists:
+        test -f "${pkgs.hylafaxplus}/spool/config/${type}"
+        ln \
+          --symbolic \
+          --no-target-directory \
+          "${mkModemConfigFile modem}" \
+          "$out/config.${name}"
+      '';
+    in
+      pkgs.runCommand "hylafax-config-modems" { preferLocalBuild = true; }
+      ''mkdir --parents "$out/" ${concatStringsSep "\n" (mapModems mkLine)}'';
+
+  setupSpoolScript = pkgs.substituteAll {
+    name = "hylafax-setup-spool.sh";
+    src = ./spool.sh;
+    isExecutable = true;
+    faxuser = "uucp";
+    faxgroup = "uucp";
+    lockPath = "/var/lock";
+    inherit globalConfigPath modemConfigPath;
+    inherit (cfg) sendmailPath spoolAreaPath userAccessFile;
+    inherit (pkgs) hylafaxplus runtimeShell;
+  };
+
+  waitFaxqScript = pkgs.substituteAll {
+    # This script checks the modems status files
+    # and waits until all modems report readiness.
+    name = "hylafax-faxq-wait-start.sh";
+    src = ./faxq-wait.sh;
+    isExecutable = true;
+    timeoutSec = toString 10;
+    inherit (cfg) spoolAreaPath;
+    inherit (pkgs) runtimeShell;
+  };
+
+  sockets.hylafax-hfaxd = {
+    description = "HylaFAX server socket";
+    documentation = [ "man:hfaxd(8)" ];
+    wantedBy = [ "multi-user.target" ];
+    listenStreams = [ "127.0.0.1:4559" ];
+    socketConfig.FreeBind = true;
+    socketConfig.Accept = true;
+  };
+
+  paths.hylafax-faxq = {
+    description = "HylaFAX queue manager sendq watch";
+    documentation = [ "man:faxq(8)" "man:sendq(5)" ];
+    wantedBy = [ "multi-user.target" ];
+    pathConfig.PathExistsGlob = [ "${cfg.spoolAreaPath}/sendq/q*" ];
+  };
+
+  timers = mkMerge [
+    (
+      mkIf (cfg.faxcron.enable.frequency!=null)
+      { hylafax-faxcron.timerConfig.Persistent = true; }
+    )
+    (
+      mkIf (cfg.faxqclean.enable.frequency!=null)
+      { hylafax-faxqclean.timerConfig.Persistent = true; }
+    )
+  ];
+
+  hardenService =
+    # Add some common systemd service hardening settings,
+    # but allow each service (here) to override
+    # settings by explicitely setting those to `null`.
+    # More hardening would be nice but makes
+    # customizing hylafax setups very difficult.
+    # If at all, it should only be added along
+    # with some options to customize it.
+    let
+      hardening = {
+        PrivateDevices = true;  # breaks /dev/tty...
+        PrivateNetwork = true;
+        PrivateTmp = true;
+        #ProtectClock = true;  # breaks /dev/tty... (why?)
+        ProtectControlGroups = true;
+        #ProtectHome = true;  # breaks custom spool dirs
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        #ProtectSystem = "strict";  # breaks custom spool dirs
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+      };
+      filter = key: value: (value != null) || ! (lib.hasAttr key hardening);
+      apply = service: lib.filterAttrs filter (hardening // (service.serviceConfig or {}));
+    in
+      service: service // { serviceConfig = apply service; };
+
+  services.hylafax-spool = {
+    description = "HylaFAX spool area preparation";
+    documentation = [ "man:hylafax-server(4)" ];
+    script = ''
+      ${setupSpoolScript}
+      cd "${cfg.spoolAreaPath}"
+      ${cfg.spoolExtraInit}
+      if ! test -f "${cfg.spoolAreaPath}/etc/hosts.hfaxd"
+      then
+        echo hosts.hfaxd is missing
+        exit 1
+      fi
+    '';
+    serviceConfig.ExecStop = "${setupSpoolScript}";
+    serviceConfig.RemainAfterExit = true;
+    serviceConfig.Type = "oneshot";
+    unitConfig.RequiresMountsFor = [ cfg.spoolAreaPath ];
+  };
+
+  services.hylafax-faxq = {
+    description = "HylaFAX queue manager";
+    documentation = [ "man:faxq(8)" ];
+    requires = [ "hylafax-spool.service" ];
+    after = [ "hylafax-spool.service" ];
+    wants = mapModems ( { name, ... }: "hylafax-faxgetty@${name}.service" );
+    wantedBy = mkIf cfg.autostart [ "multi-user.target" ];
+    serviceConfig.Type = "forking";
+    serviceConfig.ExecStart = ''${pkgs.hylafaxplus}/spool/bin/faxq -q "${cfg.spoolAreaPath}"'';
+    # This delays the "readiness" of this service until
+    # all modems are initialized (or a timeout is reached).
+    # Otherwise, sending a fax with the fax service
+    # stopped will always yield a failed send attempt:
+    # The fax service is started when the job is created with
+    # `sendfax`, but modems need some time to initialize.
+    serviceConfig.ExecStartPost = [ "${waitFaxqScript}" ];
+    # faxquit fails if the pipe is already gone
+    # (e.g. the service is already stopping)
+    serviceConfig.ExecStop = ''-${pkgs.hylafaxplus}/spool/bin/faxquit -q "${cfg.spoolAreaPath}"'';
+    # disable some systemd hardening settings
+    serviceConfig.PrivateDevices = null;
+    serviceConfig.RestrictRealtime = null;
+  };
+
+  services."hylafax-hfaxd@" = {
+    description = "HylaFAX server";
+    documentation = [ "man:hfaxd(8)" ];
+    after = [ "hylafax-faxq.service" ];
+    requires = [ "hylafax-faxq.service" ];
+    serviceConfig.StandardInput = "socket";
+    serviceConfig.StandardOutput = "socket";
+    serviceConfig.ExecStart = ''${pkgs.hylafaxplus}/spool/bin/hfaxd -q "${cfg.spoolAreaPath}" -d -I'';
+    unitConfig.RequiresMountsFor = [ cfg.userAccessFile ];
+    # disable some systemd hardening settings
+    serviceConfig.PrivateDevices = null;
+    serviceConfig.PrivateNetwork = null;
+  };
+
+  services.hylafax-faxcron = rec {
+    description = "HylaFAX spool area maintenance";
+    documentation = [ "man:faxcron(8)" ];
+    after = [ "hylafax-spool.service" ];
+    requires = [ "hylafax-spool.service" ];
+    wantedBy = mkIf cfg.faxcron.enable.spoolInit requires;
+    startAt = mkIf (cfg.faxcron.enable.frequency!=null) cfg.faxcron.enable.frequency;
+    serviceConfig.ExecStart = concatStringsSep " " [
+      "${pkgs.hylafaxplus}/spool/bin/faxcron"
+      ''-q "${cfg.spoolAreaPath}"''
+      ''-info ${toString cfg.faxcron.infoDays}''
+      ''-log  ${toString cfg.faxcron.logDays}''
+      ''-rcv  ${toString cfg.faxcron.rcvDays}''
+    ];
+  };
+
+  services.hylafax-faxqclean = rec {
+    description = "HylaFAX spool area queue cleaner";
+    documentation = [ "man:faxqclean(8)" ];
+    after = [ "hylafax-spool.service" ];
+    requires = [ "hylafax-spool.service" ];
+    wantedBy = mkIf cfg.faxqclean.enable.spoolInit requires;
+    startAt = mkIf (cfg.faxqclean.enable.frequency!=null) cfg.faxqclean.enable.frequency;
+    serviceConfig.ExecStart = concatStringsSep " " [
+      "${pkgs.hylafaxplus}/spool/bin/faxqclean"
+      ''-q "${cfg.spoolAreaPath}"''
+      "-v"
+      (optionalString (cfg.faxqclean.archiving!="never") "-a")
+      (optionalString (cfg.faxqclean.archiving=="always")  "-A")
+      ''-j ${toString (cfg.faxqclean.doneqMinutes*60)}''
+      ''-d ${toString (cfg.faxqclean.docqMinutes*60)}''
+    ];
+  };
+
+  mkFaxgettyService = { name, ... }:
+    lib.nameValuePair "hylafax-faxgetty@${name}" rec {
+      description = "HylaFAX faxgetty for %I";
+      documentation = [ "man:faxgetty(8)" ];
+      bindsTo = [ "dev-%i.device" ];
+      requires = [ "hylafax-spool.service" ];
+      after = bindsTo ++ requires;
+      before = [ "hylafax-faxq.service" "getty.target" ];
+      unitConfig.StopWhenUnneeded = true;
+      unitConfig.AssertFileNotEmpty = "${cfg.spoolAreaPath}/etc/config.%I";
+      serviceConfig.UtmpIdentifier = "%I";
+      serviceConfig.TTYPath = "/dev/%I";
+      serviceConfig.Restart = "always";
+      serviceConfig.KillMode = "process";
+      serviceConfig.IgnoreSIGPIPE = false;
+      serviceConfig.ExecStart = ''-${pkgs.hylafaxplus}/spool/bin/faxgetty -q "${cfg.spoolAreaPath}" /dev/%I'';
+      # faxquit fails if the pipe is already gone
+      # (e.g. the service is already stopping)
+      serviceConfig.ExecStop = ''-${pkgs.hylafaxplus}/spool/bin/faxquit -q "${cfg.spoolAreaPath}" %I'';
+      # disable some systemd hardening settings
+      serviceConfig.PrivateDevices = null;
+      serviceConfig.RestrictRealtime = null;
+    };
+
+  modemServices =
+    lib.listToAttrs (mapModems mkFaxgettyService);
+
+in
+
+{
+  config.systemd = mkIf cfg.enable {
+    inherit sockets timers paths;
+    services = lib.mapAttrs (lib.const hardenService) (services // modemServices);
+  };
+}
diff --git a/nixos/modules/services/networking/i2p.nix b/nixos/modules/services/networking/i2p.nix
new file mode 100644
index 00000000000..3b6010531f1
--- /dev/null
+++ b/nixos/modules/services/networking/i2p.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.i2p;
+  homeDir = "/var/lib/i2p";
+in {
+  ###### interface
+  options.services.i2p.enable = mkEnableOption "I2P router";
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    users.users.i2p = {
+      group = "i2p";
+      description = "i2p User";
+      home = homeDir;
+      createHome = true;
+      uid = config.ids.uids.i2p;
+    };
+    users.groups.i2p.gid = config.ids.gids.i2p;
+    systemd.services.i2p = {
+      description = "I2P router with administration interface for hidden services";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        User = "i2p";
+        WorkingDirectory = homeDir;
+        Restart = "on-abort";
+        ExecStart = "${pkgs.i2p}/bin/i2prouter-plain";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/i2pd.nix b/nixos/modules/services/networking/i2pd.nix
new file mode 100644
index 00000000000..34fda57b23d
--- /dev/null
+++ b/nixos/modules/services/networking/i2pd.nix
@@ -0,0 +1,691 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.i2pd;
+
+  homeDir = "/var/lib/i2pd";
+
+  strOpt = k: v: k + " = " + v;
+  boolOpt = k: v: k + " = " + boolToString v;
+  intOpt = k: v: k + " = " + toString v;
+  lstOpt = k: xs: k + " = " + concatStringsSep "," xs;
+  optionalNullString = o: s: optional (s != null) (strOpt o s);
+  optionalNullBool = o: b: optional (b != null) (boolOpt o b);
+  optionalNullInt = o: i: optional (i != null) (intOpt o i);
+  optionalEmptyList = o: l: optional ([] != l) (lstOpt o l);
+
+  mkEnableTrueOption = name: mkEnableOption name // { default = true; };
+
+  mkEndpointOpt = name: addr: port: {
+    enable = mkEnableOption name;
+    name = mkOption {
+      type = types.str;
+      default = name;
+      description = "The endpoint name.";
+    };
+    address = mkOption {
+      type = types.str;
+      default = addr;
+      description = "Bind address for ${name} endpoint.";
+    };
+    port = mkOption {
+      type = types.port;
+      default = port;
+      description = "Bind port for ${name} endpoint.";
+    };
+  };
+
+  i2cpOpts = name: {
+    length = mkOption {
+      type = types.int;
+      description = "Guaranteed minimum hops for ${name} tunnels.";
+      default = 3;
+    };
+    quantity = mkOption {
+      type = types.int;
+      description = "Number of simultaneous ${name} tunnels.";
+      default = 5;
+    };
+  };
+
+  mkKeyedEndpointOpt = name: addr: port: keyloc:
+    (mkEndpointOpt name addr port) // {
+      keys = mkOption {
+        type = with types; nullOr str;
+        default = keyloc;
+        description = ''
+          File to persist ${lib.toUpper name} keys.
+        '';
+      };
+      inbound = i2cpOpts name;
+      outbound = i2cpOpts name;
+      latency.min = mkOption {
+        type = with types; nullOr int;
+        description = "Min latency for tunnels.";
+        default = null;
+      };
+      latency.max = mkOption {
+        type = with types; nullOr int;
+        description = "Max latency for tunnels.";
+        default = null;
+      };
+    };
+
+  commonTunOpts = name: {
+    outbound = i2cpOpts name;
+    inbound = i2cpOpts name;
+    crypto.tagsToSend = mkOption {
+      type = types.int;
+      description = "Number of ElGamal/AES tags to send.";
+      default = 40;
+    };
+    destination = mkOption {
+      type = types.str;
+      description = "Remote endpoint, I2P hostname or b32.i2p address.";
+    };
+    keys = mkOption {
+      type = types.str;
+      default = name + "-keys.dat";
+      description = "Keyset used for tunnel identity.";
+    };
+  } // mkEndpointOpt name "127.0.0.1" 0;
+
+  sec = name: "\n[" + name + "]";
+  notice = "# DO NOT EDIT -- this file has been generated automatically.";
+  i2pdConf = let
+    opts = [
+      notice
+      (strOpt "loglevel" cfg.logLevel)
+      (boolOpt "logclftime" cfg.logCLFTime)
+      (boolOpt "ipv4" cfg.enableIPv4)
+      (boolOpt "ipv6" cfg.enableIPv6)
+      (boolOpt "notransit" cfg.notransit)
+      (boolOpt "floodfill" cfg.floodfill)
+      (intOpt "netid" cfg.netid)
+    ] ++ (optionalNullInt "bandwidth" cfg.bandwidth)
+      ++ (optionalNullInt "port" cfg.port)
+      ++ (optionalNullString "family" cfg.family)
+      ++ (optionalNullString "datadir" cfg.dataDir)
+      ++ (optionalNullInt "share" cfg.share)
+      ++ (optionalNullBool "ssu" cfg.ssu)
+      ++ (optionalNullBool "ntcp" cfg.ntcp)
+      ++ (optionalNullString "ntcpproxy" cfg.ntcpProxy)
+      ++ (optionalNullString "ifname" cfg.ifname)
+      ++ (optionalNullString "ifname4" cfg.ifname4)
+      ++ (optionalNullString "ifname6" cfg.ifname6)
+      ++ [
+      (sec "limits")
+      (intOpt "transittunnels" cfg.limits.transittunnels)
+      (intOpt "coresize" cfg.limits.coreSize)
+      (intOpt "openfiles" cfg.limits.openFiles)
+      (intOpt "ntcphard" cfg.limits.ntcpHard)
+      (intOpt "ntcpsoft" cfg.limits.ntcpSoft)
+      (intOpt "ntcpthreads" cfg.limits.ntcpThreads)
+      (sec "upnp")
+      (boolOpt "enabled" cfg.upnp.enable)
+      (sec "precomputation")
+      (boolOpt "elgamal" cfg.precomputation.elgamal)
+      (sec "reseed")
+      (boolOpt "verify" cfg.reseed.verify)
+    ] ++ (optionalNullString "file" cfg.reseed.file)
+      ++ (optionalEmptyList "urls" cfg.reseed.urls)
+      ++ (optionalNullString "floodfill" cfg.reseed.floodfill)
+      ++ (optionalNullString "zipfile" cfg.reseed.zipfile)
+      ++ (optionalNullString "proxy" cfg.reseed.proxy)
+      ++ [
+      (sec "trust")
+      (boolOpt "enabled" cfg.trust.enable)
+      (boolOpt "hidden" cfg.trust.hidden)
+    ] ++ (optionalEmptyList "routers" cfg.trust.routers)
+      ++ (optionalNullString "family" cfg.trust.family)
+      ++ [
+      (sec "websockets")
+      (boolOpt "enabled" cfg.websocket.enable)
+      (strOpt "address" cfg.websocket.address)
+      (intOpt "port" cfg.websocket.port)
+      (sec "exploratory")
+      (intOpt "inbound.length" cfg.exploratory.inbound.length)
+      (intOpt "inbound.quantity" cfg.exploratory.inbound.quantity)
+      (intOpt "outbound.length" cfg.exploratory.outbound.length)
+      (intOpt "outbound.quantity" cfg.exploratory.outbound.quantity)
+      (sec "ntcp2")
+      (boolOpt "enabled" cfg.ntcp2.enable)
+      (boolOpt "published" cfg.ntcp2.published)
+      (intOpt "port" cfg.ntcp2.port)
+      (sec "addressbook")
+      (strOpt "defaulturl" cfg.addressbook.defaulturl)
+    ] ++ (optionalEmptyList "subscriptions" cfg.addressbook.subscriptions)
+      ++ (flip map
+      (collect (proto: proto ? port && proto ? address) cfg.proto)
+      (proto: let protoOpts = [
+        (sec proto.name)
+        (boolOpt "enabled" proto.enable)
+        (strOpt "address" proto.address)
+        (intOpt "port" proto.port)
+        ] ++ (if proto ? keys then optionalNullString "keys" proto.keys else [])
+        ++ (if proto ? auth then optionalNullBool "auth" proto.auth else [])
+        ++ (if proto ? user then optionalNullString "user" proto.user else [])
+        ++ (if proto ? pass then optionalNullString "pass" proto.pass else [])
+        ++ (if proto ? strictHeaders then optionalNullBool "strictheaders" proto.strictHeaders else [])
+        ++ (if proto ? hostname then optionalNullString "hostname" proto.hostname else [])
+        ++ (if proto ? outproxy then optionalNullString "outproxy" proto.outproxy else [])
+        ++ (if proto ? outproxyPort then optionalNullInt "outproxyport" proto.outproxyPort else [])
+        ++ (if proto ? outproxyEnable then optionalNullBool "outproxy.enabled" proto.outproxyEnable else []);
+        in (concatStringsSep "\n" protoOpts)
+      ));
+  in
+    pkgs.writeText "i2pd.conf" (concatStringsSep "\n" opts);
+
+  tunnelConf = let opts = [
+    notice
+    (flip map
+      (collect (tun: tun ? port && tun ? destination) cfg.outTunnels)
+      (tun: let outTunOpts = [
+        (sec tun.name)
+        "type = client"
+        (intOpt "port" tun.port)
+        (strOpt "destination" tun.destination)
+        ] ++ (if tun ? destinationPort then optionalNullInt "destinationport" tun.destinationPort else [])
+        ++ (if tun ? keys then
+            optionalNullString "keys" tun.keys else [])
+        ++ (if tun ? address then
+            optionalNullString "address" tun.address else [])
+        ++ (if tun ? inbound.length then
+            optionalNullInt "inbound.length" tun.inbound.length else [])
+        ++ (if tun ? inbound.quantity then
+            optionalNullInt "inbound.quantity" tun.inbound.quantity else [])
+        ++ (if tun ? outbound.length then
+            optionalNullInt "outbound.length" tun.outbound.length else [])
+        ++ (if tun ? outbound.quantity then
+            optionalNullInt "outbound.quantity" tun.outbound.quantity else [])
+        ++ (if tun ? crypto.tagsToSend then
+            optionalNullInt "crypto.tagstosend" tun.crypto.tagsToSend else []);
+        in concatStringsSep "\n" outTunOpts))
+    (flip map
+      (collect (tun: tun ? port && tun ? address) cfg.inTunnels)
+      (tun: let inTunOpts = [
+        (sec tun.name)
+        "type = server"
+        (intOpt "port" tun.port)
+        (strOpt "host" tun.address)
+      ] ++ (if tun ? destination then
+            optionalNullString "destination" tun.destination else [])
+        ++ (if tun ? keys then
+            optionalNullString "keys" tun.keys else [])
+        ++ (if tun ? inPort then
+            optionalNullInt "inport" tun.inPort else [])
+        ++ (if tun ? accessList then
+            optionalEmptyList "accesslist" tun.accessList else []);
+        in concatStringsSep "\n" inTunOpts))];
+    in pkgs.writeText "i2pd-tunnels.conf" opts;
+
+  i2pdFlags = concatStringsSep " " (
+    optional (cfg.address != null) ("--host=" + cfg.address) ++ [
+    "--service"
+    ("--conf=" + i2pdConf)
+    ("--tunconf=" + tunnelConf)
+  ]);
+
+in
+
+{
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "i2pd" "extIp" ] [ "services" "i2pd" "address" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.i2pd = {
+
+      enable = mkEnableOption "I2Pd daemon" // {
+        description = ''
+          Enables I2Pd as a running service upon activation.
+          Please read http://i2pd.readthedocs.io/en/latest/ for further
+          configuration help.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.i2pd;
+        defaultText = literalExpression "pkgs.i2pd";
+        description = ''
+          i2pd package to use.
+        '';
+      };
+
+      logLevel = mkOption {
+        type = types.enum ["debug" "info" "warn" "error"];
+        default = "error";
+        description = ''
+          The log level. <command>i2pd</command> defaults to "info"
+          but that generates copious amounts of log messages.
+
+          We default to "error" which is similar to the default log
+          level of <command>tor</command>.
+        '';
+      };
+
+      logCLFTime = mkEnableOption "Full CLF-formatted date and time to log";
+
+      address = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Your external IP or hostname.
+        '';
+      };
+
+      family = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Specify a family the router belongs to.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Alternative path to storage of i2pd data (RI, keys, peer profiles, ...)
+        '';
+      };
+
+      share = mkOption {
+        type = types.int;
+        default = 100;
+        description = ''
+          Limit of transit traffic from max bandwidth in percents.
+        '';
+      };
+
+      ifname = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Network interface to bind to.
+        '';
+      };
+
+      ifname4 = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          IPv4 interface to bind to.
+        '';
+      };
+
+      ifname6 = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          IPv6 interface to bind to.
+        '';
+      };
+
+      ntcpProxy = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Proxy URL for NTCP transport.
+        '';
+      };
+
+      ntcp = mkEnableTrueOption "ntcp";
+      ssu = mkEnableTrueOption "ssu";
+
+      notransit = mkEnableOption "notransit" // {
+        description = ''
+          Tells the router to not accept transit tunnels during startup.
+        '';
+      };
+
+      floodfill = mkEnableOption "floodfill" // {
+        description = ''
+          If the router is declared to be unreachable and needs introduction nodes.
+        '';
+      };
+
+      netid = mkOption {
+        type = types.int;
+        default = 2;
+        description = ''
+          I2P overlay netid.
+        '';
+      };
+
+      bandwidth = mkOption {
+        type = with types; nullOr int;
+        default = null;
+        description = ''
+           Set a router bandwidth limit integer in KBps.
+           If not set, <command>i2pd</command> defaults to 32KBps.
+        '';
+      };
+
+      port = mkOption {
+        type = with types; nullOr int;
+        default = null;
+        description = ''
+          I2P listen port. If no one is given the router will pick between 9111 and 30777.
+        '';
+      };
+
+      enableIPv4 = mkEnableTrueOption "IPv4 connectivity";
+      enableIPv6 = mkEnableOption "IPv6 connectivity";
+      nat = mkEnableTrueOption "NAT bypass";
+
+      upnp.enable = mkEnableOption "UPnP service discovery";
+      upnp.name = mkOption {
+        type = types.str;
+        default = "I2Pd";
+        description = ''
+          Name i2pd appears in UPnP forwardings list.
+        '';
+      };
+
+      precomputation.elgamal = mkEnableTrueOption "Precomputed ElGamal tables" // {
+        description = ''
+          Whenever to use precomputated tables for ElGamal.
+          <command>i2pd</command> defaults to <literal>false</literal>
+          to save 64M of memory (and looses some performance).
+
+          We default to <literal>true</literal> as that is what most
+          users want anyway.
+        '';
+      };
+
+      reseed.verify = mkEnableOption "SU3 signature verification";
+
+      reseed.file = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Full path to SU3 file to reseed from.
+        '';
+      };
+
+      reseed.urls = mkOption {
+        type = with types; listOf str;
+        default = [];
+        description = ''
+          Reseed URLs.
+        '';
+      };
+
+      reseed.floodfill = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Path to router info of floodfill to reseed from.
+        '';
+      };
+
+      reseed.zipfile = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Path to local .zip file to reseed from.
+        '';
+      };
+
+      reseed.proxy = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          URL for reseed proxy, supports http/socks.
+        '';
+      };
+
+     addressbook.defaulturl = mkOption {
+        type = types.str;
+        default = "http://joajgazyztfssty4w2on5oaqksz6tqoxbduy553y34mf4byv6gpq.b32.i2p/export/alive-hosts.txt";
+        description = ''
+          AddressBook subscription URL for initial setup
+        '';
+      };
+     addressbook.subscriptions = mkOption {
+        type = with types; listOf str;
+        default = [
+          "http://inr.i2p/export/alive-hosts.txt"
+          "http://i2p-projekt.i2p/hosts.txt"
+          "http://stats.i2p/cgi-bin/newhosts.txt"
+        ];
+        description = ''
+          AddressBook subscription URLs
+        '';
+      };
+
+      trust.enable = mkEnableOption "Explicit trust options";
+
+      trust.family = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Router Familiy to trust for first hops.
+        '';
+      };
+
+      trust.routers = mkOption {
+        type = with types; listOf str;
+        default = [];
+        description = ''
+          Only connect to the listed routers.
+        '';
+      };
+
+      trust.hidden = mkEnableOption "Router concealment";
+
+      websocket = mkEndpointOpt "websockets" "127.0.0.1" 7666;
+
+      exploratory.inbound = i2cpOpts "exploratory";
+      exploratory.outbound = i2cpOpts "exploratory";
+
+      ntcp2.enable = mkEnableTrueOption "NTCP2";
+      ntcp2.published = mkEnableOption "NTCP2 publication";
+      ntcp2.port = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          Port to listen for incoming NTCP2 connections (0=auto).
+        '';
+      };
+
+      limits.transittunnels = mkOption {
+        type = types.int;
+        default = 2500;
+        description = ''
+          Maximum number of active transit sessions.
+        '';
+      };
+
+      limits.coreSize = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          Maximum size of corefile in Kb (0 - use system limit).
+        '';
+      };
+
+      limits.openFiles = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          Maximum number of open files (0 - use system default).
+        '';
+      };
+
+      limits.ntcpHard = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          Maximum number of active transit sessions.
+        '';
+      };
+
+      limits.ntcpSoft = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          Threshold to start probabalistic backoff with ntcp sessions (default: use system limit).
+        '';
+      };
+
+      limits.ntcpThreads = mkOption {
+        type = types.int;
+        default = 1;
+        description = ''
+          Maximum number of threads used by NTCP DH worker.
+        '';
+      };
+
+      proto.http = (mkEndpointOpt "http" "127.0.0.1" 7070) // {
+
+        auth = mkEnableOption "Webconsole authentication";
+
+        user = mkOption {
+          type = types.str;
+          default = "i2pd";
+          description = ''
+            Username for webconsole access
+          '';
+        };
+
+        pass = mkOption {
+          type = types.str;
+          default = "i2pd";
+          description = ''
+            Password for webconsole access.
+          '';
+        };
+
+        strictHeaders = mkOption {
+          type = with types; nullOr bool;
+          default = null;
+          description = ''
+            Enable strict host checking on WebUI.
+          '';
+        };
+
+        hostname = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = ''
+            Expected hostname for WebUI.
+          '';
+        };
+      };
+
+      proto.httpProxy = (mkKeyedEndpointOpt "httpproxy" "127.0.0.1" 4444 "httpproxy-keys.dat")
+      // {
+        outproxy = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = "Upstream outproxy bind address.";
+        };
+      };
+      proto.socksProxy = (mkKeyedEndpointOpt "socksproxy" "127.0.0.1" 4447 "socksproxy-keys.dat")
+      // {
+        outproxyEnable = mkEnableOption "SOCKS outproxy";
+        outproxy = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "Upstream outproxy bind address.";
+        };
+        outproxyPort = mkOption {
+          type = types.int;
+          default = 4444;
+          description = "Upstream outproxy bind port.";
+        };
+      };
+
+      proto.sam = mkEndpointOpt "sam" "127.0.0.1" 7656;
+      proto.bob = mkEndpointOpt "bob" "127.0.0.1" 2827;
+      proto.i2cp = mkEndpointOpt "i2cp" "127.0.0.1" 7654;
+      proto.i2pControl = mkEndpointOpt "i2pcontrol" "127.0.0.1" 7650;
+
+      outTunnels = mkOption {
+        default = {};
+        type = with types; attrsOf (submodule (
+          { name, ... }: {
+            options = {
+              destinationPort = mkOption {
+                type = with types; nullOr int;
+                default = null;
+                description = "Connect to particular port at destination.";
+              };
+            } // commonTunOpts name;
+            config = {
+              name = mkDefault name;
+            };
+          }
+        ));
+        description = ''
+          Connect to someone as a client and establish a local accept endpoint
+        '';
+      };
+
+      inTunnels = mkOption {
+        default = {};
+        type = with types; attrsOf (submodule (
+          { name, ... }: {
+            options = {
+              inPort = mkOption {
+                type = types.int;
+                default = 0;
+                description = "Service port. Default to the tunnel's listen port.";
+              };
+              accessList = mkOption {
+                type = with types; listOf str;
+                default = [];
+                description = "I2P nodes that are allowed to connect to this service.";
+              };
+            } // commonTunOpts name;
+            config = {
+              name = mkDefault name;
+            };
+          }
+        ));
+        description = ''
+          Serve something on I2P network at port and delegate requests to address inPort.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.i2pd = {
+      group = "i2pd";
+      description = "I2Pd User";
+      home = homeDir;
+      createHome = true;
+      uid = config.ids.uids.i2pd;
+    };
+
+    users.groups.i2pd.gid = config.ids.gids.i2pd;
+
+    systemd.services.i2pd = {
+      description = "Minimal I2P router";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig =
+      {
+        User = "i2pd";
+        WorkingDirectory = homeDir;
+        Restart = "on-abort";
+        ExecStart = "${cfg.package}/bin/i2pd ${i2pdFlags}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/icecream/daemon.nix b/nixos/modules/services/networking/icecream/daemon.nix
new file mode 100644
index 00000000000..8593c94e34d
--- /dev/null
+++ b/nixos/modules/services/networking/icecream/daemon.nix
@@ -0,0 +1,155 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.icecream.daemon;
+in {
+
+  ###### interface
+
+  options = {
+
+    services.icecream.daemon = {
+
+     enable = mkEnableOption "Icecream Daemon";
+
+      openFirewall = mkOption {
+        type = types.bool;
+        description = ''
+          Whether to automatically open receive port in the firewall.
+        '';
+      };
+
+      openBroadcast = mkOption {
+        type = types.bool;
+        description = ''
+          Whether to automatically open the firewall for scheduler discovery.
+        '';
+      };
+
+      cacheLimit = mkOption {
+        type = types.ints.u16;
+        default = 256;
+        description = ''
+          Maximum size in Megabytes of cache used to store compile environments of compile clients.
+        '';
+      };
+
+      netName = mkOption {
+        type = types.str;
+        default = "ICECREAM";
+        description = ''
+          Network name to connect to. A scheduler with the same name needs to be running.
+        '';
+      };
+
+      noRemote = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Prevent jobs from other nodes being scheduled on this daemon.
+        '';
+      };
+
+      schedulerHost = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Explicit scheduler hostname, useful in firewalled environments.
+
+          Uses scheduler autodiscovery via broadcast if set to null.
+        '';
+      };
+
+      maxProcesses = mkOption {
+        type = types.nullOr types.ints.u16;
+        default = null;
+        description = ''
+          Maximum number of compile jobs started in parallel for this daemon.
+
+          Uses the number of CPUs if set to null.
+        '';
+      };
+
+      nice = mkOption {
+        type = types.int;
+        default = 5;
+        description = ''
+          The level of niceness to use.
+        '';
+      };
+
+      hostname = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Hostname of the daemon in the icecream infrastructure.
+
+          Uses the hostname retrieved via uname if set to null.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "icecc";
+        description = ''
+          User to run the icecream daemon as. Set to root to enable receive of
+          remote compile environments.
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.icecream;
+        defaultText = literalExpression "pkgs.icecream";
+        type = types.package;
+        description = "Icecream package to use.";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Additional command line parameters.";
+        example = [ "-v" ];
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ 10245 ];
+    networking.firewall.allowedUDPPorts = mkIf cfg.openBroadcast [ 8765 ];
+
+    systemd.services.icecc-daemon = {
+      description = "Icecream compile daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = escapeShellArgs ([
+          "${getBin cfg.package}/bin/iceccd"
+          "-b" "$STATE_DIRECTORY"
+          "-u" "icecc"
+          (toString cfg.nice)
+        ]
+        ++ optionals (cfg.schedulerHost != null) ["-s" cfg.schedulerHost]
+        ++ optionals (cfg.netName != null) [ "-n" cfg.netName ]
+        ++ optionals (cfg.cacheLimit != null) [ "--cache-limit" (toString cfg.cacheLimit) ]
+        ++ optionals (cfg.maxProcesses != null) [ "-m" (toString cfg.maxProcesses) ]
+        ++ optionals (cfg.hostname != null) [ "-N" (cfg.hostname) ]
+        ++ optional  cfg.noRemote "--no-remote"
+        ++ cfg.extraArgs);
+        DynamicUser = true;
+        User = "icecc";
+        Group = "icecc";
+        StateDirectory = "icecc";
+        RuntimeDirectory = "icecc";
+        AmbientCapabilities = "CAP_SYS_CHROOT";
+        CapabilityBoundingSet = "CAP_SYS_CHROOT";
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ emantor ];
+}
diff --git a/nixos/modules/services/networking/icecream/scheduler.nix b/nixos/modules/services/networking/icecream/scheduler.nix
new file mode 100644
index 00000000000..14fbc966b98
--- /dev/null
+++ b/nixos/modules/services/networking/icecream/scheduler.nix
@@ -0,0 +1,101 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.icecream.scheduler;
+in {
+
+  ###### interface
+
+  options = {
+
+    services.icecream.scheduler = {
+      enable = mkEnableOption "Icecream Scheduler";
+
+      netName = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Network name for the icecream scheduler.
+
+          Uses the default ICECREAM if null.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 8765;
+        description = ''
+          Server port to listen for icecream daemon requests.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        description = ''
+          Whether to automatically open the daemon port in the firewall.
+        '';
+      };
+
+      openTelnet = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to open the telnet TCP port on 8766.
+        '';
+      };
+
+      persistentClientConnection = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to prevent clients from connecting to a better scheduler.
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.icecream;
+        defaultText = literalExpression "pkgs.icecream";
+        type = types.package;
+        description = "Icecream package to use.";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Additional command line parameters";
+        example = [ "-v" ];
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    networking.firewall.allowedTCPPorts = mkMerge [
+      (mkIf cfg.openFirewall [ cfg.port ])
+      (mkIf cfg.openTelnet [ 8766 ])
+    ];
+
+    systemd.services.icecc-scheduler = {
+      description = "Icecream scheduling server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = escapeShellArgs ([
+          "${getBin cfg.package}/bin/icecc-scheduler"
+          "-p" (toString cfg.port)
+        ]
+        ++ optionals (cfg.netName != null) [ "-n" (toString cfg.netName) ]
+        ++ optional cfg.persistentClientConnection "-r"
+        ++ cfg.extraArgs);
+
+        DynamicUser = true;
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ emantor ];
+}
diff --git a/nixos/modules/services/networking/inspircd.nix b/nixos/modules/services/networking/inspircd.nix
new file mode 100644
index 00000000000..81c367ec8f7
--- /dev/null
+++ b/nixos/modules/services/networking/inspircd.nix
@@ -0,0 +1,62 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.inspircd;
+
+  configFile = pkgs.writeText "inspircd.conf" cfg.config;
+
+in {
+  meta = {
+    maintainers = [ lib.maintainers.sternenseemann ];
+  };
+
+  options = {
+    services.inspircd = {
+      enable = lib.mkEnableOption "InspIRCd";
+
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.inspircd;
+        defaultText = lib.literalExpression "pkgs.inspircd";
+        example = lib.literalExpression "pkgs.inspircdMinimal";
+        description = ''
+          The InspIRCd package to use. This is mainly useful
+          to specify an overridden version of the
+          <literal>pkgs.inspircd</literal> dervivation, for
+          example if you want to use a more minimal InspIRCd
+          distribution with less modules enabled or with
+          modules enabled which can't be distributed in binary
+          form due to licensing issues.
+        '';
+      };
+
+      config = lib.mkOption {
+        type = lib.types.lines;
+        description = ''
+          Verbatim <literal>inspircd.conf</literal> file.
+          For a list of options, consult the
+          <link xlink:href="https://docs.inspircd.org/3/configuration/">InspIRCd documentation</link>, the
+          <link xlink:href="https://docs.inspircd.org/3/modules/">Module documentation</link>
+          and the example configuration files distributed
+          with <literal>pkgs.inspircd.doc</literal>
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.inspircd = {
+      description = "InspIRCd - the stable, high-performance and modular Internet Relay Chat Daemon";
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "network.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = ''
+          ${lib.getBin cfg.package}/bin/inspircd start --config ${configFile} --nofork --nopid
+        '';
+        DynamicUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/iodine.nix b/nixos/modules/services/networking/iodine.nix
new file mode 100644
index 00000000000..e241afe3269
--- /dev/null
+++ b/nixos/modules/services/networking/iodine.nix
@@ -0,0 +1,198 @@
+# NixOS module for iodine, ip over dns daemon
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.iodine;
+
+  iodinedUser = "iodined";
+
+  /* is this path made unreadable by ProtectHome = true ? */
+  isProtected = x: hasPrefix "/root" x || hasPrefix "/home" x;
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "iodined" "enable" ] [ "services" "iodine" "server" "enable" ])
+    (mkRenamedOptionModule [ "services" "iodined" "domain" ] [ "services" "iodine" "server" "domain" ])
+    (mkRenamedOptionModule [ "services" "iodined" "ip" ] [ "services" "iodine" "server" "ip" ])
+    (mkRenamedOptionModule [ "services" "iodined" "extraConfig" ] [ "services" "iodine" "server" "extraConfig" ])
+    (mkRemovedOptionModule [ "services" "iodined" "client" ] "")
+  ];
+
+  ### configuration
+
+  options = {
+
+    services.iodine = {
+      clients = mkOption {
+        default = {};
+        description = ''
+          Each attribute of this option defines a systemd service that
+          runs iodine. Many or none may be defined.
+          The name of each service is
+          <literal>iodine-<replaceable>name</replaceable></literal>
+          where <replaceable>name</replaceable> is the name of the
+          corresponding attribute name.
+        '';
+        example = literalExpression ''
+          {
+            foo = {
+              server = "tunnel.mdomain.com";
+              relay = "8.8.8.8";
+              extraConfig = "-v";
+            }
+          }
+        '';
+        type = types.attrsOf (
+          types.submodule (
+            {
+              options = {
+                server = mkOption {
+                  type = types.str;
+                  default = "";
+                  description = "Hostname of server running iodined";
+                  example = "tunnel.mydomain.com";
+                };
+
+                relay = mkOption {
+                  type = types.str;
+                  default = "";
+                  description = "DNS server to use as an intermediate relay to the iodined server";
+                  example = "8.8.8.8";
+                };
+
+                extraConfig = mkOption {
+                  type = types.str;
+                  default = "";
+                  description = "Additional command line parameters";
+                  example = "-l 192.168.1.10 -p 23";
+                };
+
+                passwordFile = mkOption {
+                  type = types.str;
+                  default = "";
+                  description = "Path to a file containing the password.";
+                };
+              };
+            }
+          )
+        );
+      };
+
+      server = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = "enable iodined server";
+        };
+
+        ip = mkOption {
+          type = types.str;
+          default = "";
+          description = "The assigned ip address or ip range";
+          example = "172.16.10.1/24";
+        };
+
+        domain = mkOption {
+          type = types.str;
+          default = "";
+          description = "Domain or subdomain of which nameservers point to us";
+          example = "tunnel.mydomain.com";
+        };
+
+        extraConfig = mkOption {
+          type = types.str;
+          default = "";
+          description = "Additional command line parameters";
+          example = "-l 192.168.1.10 -p 23";
+        };
+
+        passwordFile = mkOption {
+          type = types.str;
+          default = "";
+          description = "File that contains password";
+        };
+      };
+
+    };
+  };
+
+  ### implementation
+
+  config = mkIf (cfg.server.enable || cfg.clients != {}) {
+    environment.systemPackages = [ pkgs.iodine ];
+    boot.kernelModules = [ "tun" ];
+
+    systemd.services =
+      let
+        createIodineClientService = name: cfg:
+          {
+            description = "iodine client - ${name}";
+            after = [ "network.target" ];
+            wantedBy = [ "multi-user.target" ];
+            script = "exec ${pkgs.iodine}/bin/iodine -f -u ${iodinedUser} ${cfg.extraConfig} ${optionalString (cfg.passwordFile != "") "< \"${builtins.toString cfg.passwordFile}\""} ${cfg.relay} ${cfg.server}";
+            serviceConfig = {
+              RestartSec = "30s";
+              Restart = "always";
+
+              # hardening :
+              # Filesystem access
+              ProtectSystem = "strict";
+              ProtectHome = if isProtected cfg.passwordFile then "read-only" else "true" ;
+              PrivateTmp = true;
+              ReadWritePaths = "/dev/net/tun";
+              PrivateDevices = false;
+              ProtectKernelTunables = true;
+              ProtectKernelModules = true;
+              ProtectControlGroups = true;
+              # Caps
+              NoNewPrivileges = true;
+              # Misc.
+              LockPersonality = true;
+              RestrictRealtime = true;
+              PrivateMounts = true;
+              MemoryDenyWriteExecute = true;
+            };
+          };
+      in
+        listToAttrs (
+          mapAttrsToList
+            (name: value: nameValuePair "iodine-${name}" (createIodineClientService name value))
+            cfg.clients
+        ) // {
+          iodined = mkIf (cfg.server.enable) {
+            description = "iodine, ip over dns server daemon";
+            after = [ "network.target" ];
+            wantedBy = [ "multi-user.target" ];
+            script = "exec ${pkgs.iodine}/bin/iodined -f -u ${iodinedUser} ${cfg.server.extraConfig} ${optionalString (cfg.server.passwordFile != "") "< \"${builtins.toString cfg.server.passwordFile}\""} ${cfg.server.ip} ${cfg.server.domain}";
+            serviceConfig = {
+              # Filesystem access
+              ProtectSystem = "strict";
+              ProtectHome = if isProtected cfg.server.passwordFile then "read-only" else "true" ;
+              PrivateTmp = true;
+              ReadWritePaths = "/dev/net/tun";
+              PrivateDevices = false;
+              ProtectKernelTunables = true;
+              ProtectKernelModules = true;
+              ProtectControlGroups = true;
+              # Caps
+              NoNewPrivileges = true;
+              # Misc.
+              LockPersonality = true;
+              RestrictRealtime = true;
+              PrivateMounts = true;
+              MemoryDenyWriteExecute = true;
+            };
+          };
+        };
+
+    users.users.${iodinedUser} = {
+      uid = config.ids.uids.iodined;
+      group = "iodined";
+      description = "Iodine daemon user";
+    };
+    users.groups.iodined.gid = config.ids.gids.iodined;
+  };
+}
diff --git a/nixos/modules/services/networking/iperf3.nix b/nixos/modules/services/networking/iperf3.nix
new file mode 100644
index 00000000000..0fe378b225d
--- /dev/null
+++ b/nixos/modules/services/networking/iperf3.nix
@@ -0,0 +1,97 @@
+{ config, lib, pkgs, ... }: with lib;
+let
+  cfg = config.services.iperf3;
+
+  api = {
+    enable = mkEnableOption "iperf3 network throughput testing server";
+    port = mkOption {
+      type        = types.ints.u16;
+      default     = 5201;
+      description = "Server port to listen on for iperf3 client requsts.";
+    };
+    affinity = mkOption {
+      type        = types.nullOr types.ints.unsigned;
+      default     = null;
+      description = "CPU affinity for the process.";
+    };
+    bind = mkOption {
+      type        = types.nullOr types.str;
+      default     = null;
+      description = "Bind to the specific interface associated with the given address.";
+    };
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Open ports in the firewall for iperf3.";
+    };
+    verbose = mkOption {
+      type        = types.bool;
+      default     = false;
+      description = "Give more detailed output.";
+    };
+    forceFlush = mkOption {
+      type        = types.bool;
+      default     = false;
+      description = "Force flushing output at every interval.";
+    };
+    debug = mkOption {
+      type        = types.bool;
+      default     = false;
+      description = "Emit debugging output.";
+    };
+    rsaPrivateKey = mkOption {
+      type        = types.nullOr types.path;
+      default     = null;
+      description = "Path to the RSA private key (not password-protected) used to decrypt authentication credentials from the client.";
+    };
+    authorizedUsersFile = mkOption {
+      type        = types.nullOr types.path;
+      default     = null;
+      description = "Path to the configuration file containing authorized users credentials to run iperf tests.";
+    };
+    extraFlags = mkOption {
+      type        = types.listOf types.str;
+      default     = [ ];
+      description = "Extra flags to pass to iperf3(1).";
+    };
+  };
+
+  imp = {
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+
+    systemd.services.iperf3 = {
+      description = "iperf3 daemon";
+      unitConfig.Documentation = "man:iperf3(1) https://iperf.fr/iperf-doc.php";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        Restart = "on-failure";
+        RestartSec = 2;
+        DynamicUser = true;
+        PrivateDevices = true;
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        ExecStart = ''
+          ${pkgs.iperf3}/bin/iperf \
+            --server \
+            --port ${toString cfg.port} \
+            ${optionalString (cfg.affinity != null) "--affinity ${toString cfg.affinity}"} \
+            ${optionalString (cfg.bind != null) "--bind ${cfg.bind}"} \
+            ${optionalString (cfg.rsaPrivateKey != null) "--rsa-private-key-path ${cfg.rsaPrivateKey}"} \
+            ${optionalString (cfg.authorizedUsersFile != null) "--authorized-users-path ${cfg.authorizedUsersFile}"} \
+            ${optionalString cfg.verbose "--verbose"} \
+            ${optionalString cfg.debug "--debug"} \
+            ${optionalString cfg.forceFlush "--forceflush"} \
+            ${escapeShellArgs cfg.extraFlags}
+        '';
+      };
+    };
+  };
+in {
+  options.services.iperf3 = api;
+  config = mkIf cfg.enable imp;
+}
diff --git a/nixos/modules/services/networking/ircd-hybrid/builder.sh b/nixos/modules/services/networking/ircd-hybrid/builder.sh
new file mode 100644
index 00000000000..38312210df2
--- /dev/null
+++ b/nixos/modules/services/networking/ircd-hybrid/builder.sh
@@ -0,0 +1,31 @@
+source $stdenv/setup
+
+doSub() {
+    local src=$1
+    local dst=$2
+    mkdir -p $(dirname $dst)
+    substituteAll $src $dst
+}
+
+subDir=/
+for i in $scripts; do
+    if test "$(echo $i | cut -c1-2)" = "=>"; then
+        subDir=$(echo $i | cut -c3-)
+    else
+        dst=$out/$subDir/$(stripHash $i | sed 's/\.in//')
+        doSub $i $dst
+        chmod +x $dst # !!!
+    fi
+done
+
+subDir=/
+for i in $substFiles; do
+    if test "$(echo $i | cut -c1-2)" = "=>"; then
+        subDir=$(echo $i | cut -c3-)
+    else
+        dst=$out/$subDir/$(stripHash $i | sed 's/\.in//')
+        doSub $i $dst
+    fi
+done
+
+mkdir -p $out/bin
diff --git a/nixos/modules/services/networking/ircd-hybrid/control.in b/nixos/modules/services/networking/ircd-hybrid/control.in
new file mode 100644
index 00000000000..312dfaada32
--- /dev/null
+++ b/nixos/modules/services/networking/ircd-hybrid/control.in
@@ -0,0 +1,26 @@
+#! @shell@ -e
+
+# Make sure that the environment is deterministic.
+export PATH=@coreutils@/bin
+
+if test "$1" = "start"; then
+	if ! @procps@/bin/pgrep ircd; then
+	if @ipv6Enabled@; then 
+		while ! @iproute@/sbin/ip addr | 
+			@gnugrep@/bin/grep inet6 | 
+			@gnugrep@/bin/grep global; do
+			sleep 1;
+		done;
+	fi;
+	rm -rf /home/ircd
+	mkdir -p /home/ircd
+	chown ircd: /home/ircd
+	cd /home/ircd
+    env - HOME=/homeless-shelter $extraEnv \
+        @su@/bin/su ircd --shell=/bin/sh -c ' @ircdHybrid@/bin/ircd -configfile @out@/conf/ircd.conf </dev/null -logfile /home/ircd/ircd.log' 2>&1 >/var/log/ircd-hybrid.out
+	fi;
+fi
+
+if test "$1" = "stop" ; then 
+	@procps@/bin/pkill ircd;
+fi;
diff --git a/nixos/modules/services/networking/ircd-hybrid/default.nix b/nixos/modules/services/networking/ircd-hybrid/default.nix
new file mode 100644
index 00000000000..f659f3f3e8c
--- /dev/null
+++ b/nixos/modules/services/networking/ircd-hybrid/default.nix
@@ -0,0 +1,133 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.ircdHybrid;
+
+  ircdService = pkgs.stdenv.mkDerivation rec {
+    name = "ircd-hybrid-service";
+    scripts = [ "=>/bin" ./control.in ];
+    substFiles = [ "=>/conf" ./ircd.conf ];
+    inherit (pkgs) ircdHybrid coreutils su iproute2 gnugrep procps;
+
+    ipv6Enabled = boolToString config.networking.enableIPv6;
+
+    inherit (cfg) serverName sid description adminEmail
+            extraPort;
+
+    cryptoSettings =
+      (optionalString (cfg.rsaKey != null) "rsa_private_key_file = \"${cfg.rsaKey}\";\n") +
+      (optionalString (cfg.certificate != null) "ssl_certificate_file = \"${cfg.certificate}\";\n");
+
+    extraListen = map (ip: "host = \""+ip+"\";\nport = 6665 .. 6669, "+extraPort+"; ") cfg.extraIPs;
+
+    builder = ./builder.sh;
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.ircdHybrid = {
+
+      enable = mkEnableOption "IRCD";
+
+      serverName = mkOption {
+        default = "hades.arpa";
+        type = types.str;
+        description = "
+          IRCD server name.
+        ";
+      };
+
+      sid = mkOption {
+        default = "0NL";
+        type = types.str;
+        description = "
+          IRCD server unique ID in a net of servers.
+        ";
+      };
+
+      description = mkOption {
+        default = "Hybrid-7 IRC server.";
+        type = types.str;
+        description = "
+          IRCD server description.
+        ";
+      };
+
+      rsaKey = mkOption {
+        default = null;
+        example = literalExpression "/root/certificates/irc.key";
+        type = types.nullOr types.path;
+        description = "
+          IRCD server RSA key.
+        ";
+      };
+
+      certificate = mkOption {
+        default = null;
+        example = literalExpression "/root/certificates/irc.pem";
+        type = types.nullOr types.path;
+        description = "
+          IRCD server SSL certificate. There are some limitations - read manual.
+        ";
+      };
+
+      adminEmail = mkOption {
+        default = "<bit-bucket@example.com>";
+        type = types.str;
+        example = "<name@domain.tld>";
+        description = "
+          IRCD server administrator e-mail.
+        ";
+      };
+
+      extraIPs = mkOption {
+        default = [];
+        example = ["127.0.0.1"];
+        type = types.listOf types.str;
+        description = "
+          Extra IP's to bind.
+        ";
+      };
+
+      extraPort = mkOption {
+        default = "7117";
+        type = types.str;
+        description = "
+          Extra port to avoid filtering.
+        ";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.ircdHybrid.enable {
+
+    users.users.ircd =
+      { description = "IRCD owner";
+        group = "ircd";
+        uid = config.ids.uids.ircd;
+      };
+
+    users.groups.ircd.gid = config.ids.gids.ircd;
+
+    systemd.services.ircd-hybrid = {
+      description = "IRCD Hybrid server";
+      after = [ "started networking" ];
+      wantedBy = [ "multi-user.target" ];
+      script = "${ircdService}/bin/control start";
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/ircd-hybrid/ircd.conf b/nixos/modules/services/networking/ircd-hybrid/ircd.conf
new file mode 100644
index 00000000000..17ef203840a
--- /dev/null
+++ b/nixos/modules/services/networking/ircd-hybrid/ircd.conf
@@ -0,0 +1,1051 @@
+/* doc/example.conf - ircd-hybrid-7 Example configuration file
+ * Copyright (C) 2000-2006 Hybrid Development Team
+ *
+ * Written by ejb, wcampbel, db, leeh and others
+ * Other example configurations can be found in the source dir under
+ * etc/.
+ *
+ * $Id: example.conf 639 2006-06-01 14:12:21Z michael $
+ */
+
+/* IMPORTANT NOTES:
+ *
+ * auth {} blocks MUST be specified in order of precedence.  The first one
+ * that matches a user will be used.  So place spoofs first, then specials,
+ * then general access.
+ *
+ * Shell style (#), C++ style (//) and C style comments are supported.
+ *
+ * Files may be included by either:
+ *        .include "filename"
+ *        .include <filename>
+ *
+ * Times/durations are written as:
+ *        12 hours 30 minutes 1 second
+ *        
+ * Valid units of time:
+ *        month, week, day, hour, minute, second
+ *
+ * Valid units of size:
+ *        megabyte/mbyte/mb, kilobyte/kbyte/kb, byte
+ *
+ * Sizes and times may be singular or plural.  
+ */ 
+
+/* EFNET NOTE:
+ *
+ * This config file is NOT suitable for EFNet.  EFNet admins should use
+ * example.efnet.conf
+ */
+ 
+/*
+ * serverinfo {}:  contains information about the server. (OLD M:)
+ */
+serverinfo {
+	/*
+	 * name: the name of our server.  This cannot be changed at runtime.
+	 */
+	name = "@serverName@";
+
+	/*
+	 * sid: a server's unique ID.  This is three characters long and must
+	 * be in the form [0-9][A-Z0-9][A-Z0-9].  The first character must be
+	 * a digit, followed by 2 alpha-numerical letters.
+	 * NOTE: The letters must be capitalized.  This cannot be changed at runtime.
+	 */
+	sid = "@sid@";
+
+	/*
+	 * description: the description of the server.  '[' and ']' may not
+	 * be used here for compatibility with older servers.
+	 */
+	description = "@description@";
+
+	/*
+	 * network info: the name and description of the network this server
+	 * is on.  Shown in the 005 reply and used with serverhiding.
+	 */
+	network_name = "JustIRCNetwork";
+	network_desc = "This is My Network";
+
+	/*
+	 * hub: allow this server to act as a hub and have multiple servers
+	 * connected to it.  This may not be changed if there are active
+	 * LazyLink servers.
+	 */
+	hub = no;
+
+	/*
+	 * vhost: the IP to bind to when we connect outward to ipv4 servers.
+	 * This should be an ipv4 IP only, or "* for INADDR_ANY.
+	 */
+	#vhost = "192.169.0.1";
+
+	/*
+	 * vhost6: the IP to bind to when we connect outward to ipv6 servers.
+	 * This should be an ipv6 IP only, or "* for INADDR_ANY.
+	 */
+	#vhost6 = "3ffe:80e8:546::2";
+
+	/* max_clients: the maximum number of clients allowed to connect */
+	max_clients = 512;
+
+	/*
+	 * rsa key: the path to the file containing our rsa key for cryptlink.
+	 *
+	 * Example command to store a 2048 bit RSA keypair in
+	 * rsa.key, and the public key in rsa.pub:
+	 * 
+	 * 	openssl genrsa -out rsa.key 2048
+	 *	openssl rsa -in rsa.key -pubout -out rsa.pub
+	 *	chown <ircd-user>.<ircd.group> rsa.key rsa.pub
+	 *	chmod 0600 rsa.key
+	 *	chmod 0644 rsa.pub
+	 */
+	#rsa_private_key_file = "/usr/local/ircd/etc/rsa.key";
+
+	/*
+	 * ssl certificate: the path to the file containing our ssl certificate
+	 * for encrypted client connection.
+	 *
+	 * This assumes your private RSA key is stored in rsa.key. You
+	 * MUST have an RSA key in order to generate the certificate
+	 *
+	 *	openssl req -new -days 365 -x509 -key rsa.key -out cert.pem
+	 *
+	 * See http://www.openssl.org/docs/HOWTO/certificates.txt
+	 *
+	 * Please use the following values when generating the cert
+	 *
+	 *	Organization Name: Network Name
+	 *	Organization Unit Name: changme.someirc.net
+	 *	Common Name: irc.someirc.net
+	 *	E-mail: you@domain.com
+	 */
+	#ssl_certificate_file = "/usr/local/ircd/etc/cert.pem";
+
+	@cryptoSettings@
+};
+
+/*
+ * admin {}:  contains admin information about the server. (OLD A:)
+ */
+admin {
+	name = "Anonymous Hero";
+	description = "Main Server Administrator";
+	email = "@adminEmail@";
+};
+
+/*
+ * log {}:  contains information about logfiles.
+ */
+log {
+	/* Do you want to enable logging to ircd.log? */
+	use_logging = yes;
+
+	/*
+	 * logfiles: the logfiles to use for user connects, /oper uses,
+	 * and failed /oper.  These files must exist for logging to be used.
+	 */
+	fname_userlog = "/home/ircd/logs/userlog";
+	fname_operlog = "/home/ircd/logs/operlog";
+	fname_killlog = "/home/ircd/logs/kill";
+	fname_klinelog = "/home/ircd/logs/kline";
+	fname_glinelog = "/home/ircd/logs/gline";
+
+	/*
+	 * log_level: the amount of detail to log in ircd.log.  The
+	 * higher, the more information is logged.  May be changed
+	 * once the server is running via /quote SET LOG.  Either:
+	 * L_CRIT, L_ERROR, L_WARN, L_NOTICE, L_TRACE, L_INFO or L_DEBUG
+	 */
+	log_level = L_INFO;
+};
+
+/*
+ * class {}:  contains information about classes for users (OLD Y:)
+ */
+class {
+	/* name: the name of the class.  classes are text now */
+	name = "users";
+
+	/*
+	 * ping_time: how often a client must reply to a PING from the
+	 * server before they are dropped.
+	 */
+	ping_time = 90 seconds;
+
+	/*
+	 * number_per_ip: how many local users are allowed to connect
+	 * from one IP  (optional)
+	 */
+	number_per_ip = 10;
+
+	/*
+	 * max_local: how many local users are allowed to connect
+	 * from one ident@host  (optional)
+	 */
+	max_local = 50;
+
+	/*
+	 * max_global: network-wide limit of users per ident@host  (optional)
+	 */
+	max_global = 50;
+
+	/*
+	 * max_number: the maximum number of users allowed in this class (optional)
+	 */
+	max_number = 10000;
+
+	/*
+	 * the following lines are optional and allow you to define
+	 * how many users can connect from one /NN subnet
+	 */
+	/*cidr_bitlen_ipv4 = 24;
+	 *cidr_bitlen_ipv6 = 120;
+	 *number_per_cidr = 16;*/
+
+	/*
+	 * sendq: the amount of data allowed in a clients queue before
+	 * they are dropped.
+	 */
+	sendq = 100 kbytes;
+};
+
+class {
+	name = "opers";
+	ping_time = 90 seconds;
+	number_per_ip = 10;
+	max_number = 100;
+	sendq = 100kbytes;
+};
+
+class {
+	name = "server";
+	ping_time = 90 seconds;
+
+	/*
+	 * ping_warning: how fast a server must reply to a PING before
+	 * a warning to opers is generated.
+	 */
+	ping_warning = 15 seconds;
+
+	/*
+	 * connectfreq: only used in server classes.  Specifies the delay
+	 * between autoconnecting to servers.
+	 */
+	connectfreq = 5 minutes;
+
+	/* max number: the amount of servers to autoconnect to */
+	max_number = 1;
+
+	/* sendq: servers need a higher sendq as they send more data */
+	sendq = 2 megabytes;
+};
+
+/*
+ * listen {}:  contains information about the ports ircd listens on (OLD P:)
+ */
+listen {
+	/*
+	 * port: the specific port to listen on.  If no host is specified
+	 * before, it will listen on all available IPs.
+	 *
+	 * Ports are separated via a comma, a range may be specified using ".."
+	 */
+	
+	/* port: listen on all available IPs, ports 6665 to 6669 */
+	port = 6665 .. 6669;
+
+	/*
+	 * Listen on 192.168.0.1/6697 with ssl enabled and hidden from STATS P
+	 * unless you are an administrator.
+	 *
+	 * NOTE: The "flags" directive has to come before "port".  Always!
+	 */
+	#flags = hidden, ssl;
+	#host = "192.168.0.1";
+	#port = 6697;
+
+	/*
+	 * host: set a specific IP/host the ports after the line will listen 
+	 * on.  This may be ipv4 or ipv6.
+	 */
+	#host = "1.2.3.4";
+	#port = 7000, 7001;
+
+	#host = "3ffe:1234:a:b:c::d";
+	#port = 7002;
+	
+	@extraListen@
+};
+
+auth {
+	user = "*@*";
+	class = "users";
+	#flags = need_ident;
+};
+
+/*
+ * operator {}:  defines ircd operators. (OLD O:)
+ *
+ * ircd-hybrid no longer supports local operators, privileges are
+ * controlled via flags.
+ */
+operator {
+	/* name: the name of the oper */
+	/* NOTE: operator "opername"{} is also supported */
+	name = "god";
+
+	/*
+	 * user: the user@host required for this operator.  CIDR is not
+	 * supported.  Multiple user="" lines are supported.
+	 */
+	user = "*god@*";
+	user = "*@127.0.0.1";
+
+	/*
+	 * password: the password required to oper.  By default this will
+	 * need to be encrypted using 'mkpasswd'.  MD5 is supported.
+	 */
+	password = "iamoperator";
+
+	/*
+	 * encrypted: controls whether the oper password above has been
+	 * encrypted.  (OLD CRYPT_OPER_PASSWORD now optional per operator)
+	 */
+	encrypted = no;
+
+	/*
+	 * rsa_public_key_file: the public key for this oper when using Challenge.
+	 * A password should not be defined when this is used, see 
+	 * doc/challenge.txt for more information.
+	 */
+#	rsa_public_key_file = "/usr/local/ircd/etc/oper.pub";
+
+	/* class: the class the oper joins when they successfully /oper */
+	class = "opers";
+
+	/*
+	 * umodes: default usermodes opers get when they /oper.  If defined,
+	 * it will override oper_umodes settings in general {}.
+	 * Available usermodes:
+	 *
+	 * +b - bots         - See bot and drone flooding notices
+	 * +c - cconn        - Client connection/quit notices
+	 * +D - deaf         - Don't receive channel messages
+	 * +d - debug        - See debugging notices
+	 * +f - full         - See I: line full notices
+	 * +G - softcallerid - Server Side Ignore for users not on your channels
+	 * +g - callerid     - Server Side Ignore (for privmsgs etc)
+	 * +i - invisible    - Not shown in NAMES or WHO unless you share a
+	 *                     a channel
+	 * +k - skill        - See server generated KILL messages
+	 * +l - locops       - See LOCOPS messages
+	 * +n - nchange      - See client nick changes
+	 * +r - rej          - See rejected client notices
+	 * +s - servnotice   - See general server notices
+	 * +u - unauth       - See unauthorized client notices
+	 * +w - wallop       - See server generated WALLOPS
+	 * +x - external     - See remote server connection and split notices
+	 * +y - spy          - See LINKS, STATS, TRACE notices etc.
+	 * +z - operwall     - See oper generated WALLOPS
+	 */
+#	umodes = locops, servnotice, operwall, wallop;
+
+	/*
+	 * privileges: controls the activities and commands an oper is 
+	 * allowed to do on the server.  All options default to no.
+	 * Available options:
+	 *
+	 * global_kill:  allows remote users to be /KILL'd (OLD 'O' flag)
+	 * remote:       allows remote SQUIT and CONNECT   (OLD 'R' flag)
+	 * remoteban:    allows remote KLINE/UNKLINE
+	 * kline:        allows KILL, KLINE and DLINE      (OLD 'K' flag)
+	 * unkline:      allows UNKLINE and UNDLINE        (OLD 'U' flag)
+	 * gline:        allows GLINE                      (OLD 'G' flag)
+	 * xline:         allows XLINE                     (OLD 'X' flag)
+	 * operwall:     allows OPERWALL
+	 * nick_changes: allows oper to see nickchanges    (OLD 'N' flag)
+	 *               via usermode +n
+	 * rehash:       allows oper to REHASH config      (OLD 'H' flag)
+	 * die:          allows DIE and RESTART            (OLD 'D' flag)
+	 * admin:        gives admin privileges.  admins
+	 *               may (un)load modules and see the
+	 *               real IPs of servers.
+	 * hidden_admin: same as 'admin', but noone can recognize you as
+	 *               being an admin
+	 * hidden_oper:  not shown in /stats p (except for other operators)
+	 */
+	/* You can either use
+	 * die = yes;
+	 * rehash = yes;
+	 *
+	 * or in a flags statement i.e.
+	 * flags = die, rehash;
+	 *
+	 * You can also negate a flag with ~ i.e.
+	 * flags = ~remote;
+	 *
+	 */
+	flags = global_kill, remote, kline, unkline, xline,
+		die, rehash, nick_changes, admin, operwall;
+};
+
+/*
+ * shared {}: users that are allowed to remote kline (OLD U:)
+ *
+ * NOTE: This can be effectively used for remote klines.
+ *       Please note that there is no password authentication
+ *       for users setting remote klines.  You must also be
+ *       /oper'd in order to issue a remote kline.
+ */
+shared {
+	/*
+	 * name: the server the user must be on to set klines.  If this is not
+	 * specified, the user will be allowed to kline from all servers.
+	 */
+	name = "irc2.some.server";
+
+	/*
+	 * user: the user@host mask that is allowed to set klines.  If this is
+	 * not specified, all users on the server above will be allowed to set
+	 * a remote kline.
+	 */
+	user = "oper@my.host.is.spoofed";
+
+	/*
+	 * type: list of what to share, options are as follows:
+	 *	kline	- allow oper/server to kline
+	 *	tkline	- allow temporary klines
+	 *	unkline	- allow oper/server to unkline
+	 *	xline	- allow oper/server to xline
+	 * 	txline	- allow temporary xlines
+	 *	unxline	- allow oper/server to unxline
+	 *	resv	- allow oper/server to resv
+	 * 	tresv	- allow temporary resvs
+	 *	unresv	- allow oper/server to unresv
+	 *      locops  - allow oper/server to locops - only used for servers that cluster
+	 *	all	- allow oper/server to do all of the above (default)
+	 */
+	type = kline, unkline, resv;
+};
+
+/*
+ * kill {}:  users that are not allowed to connect (OLD K:)
+ * Oper issued klines will be added to the specified kline config
+ */
+kill {
+	user = "bad@*.hacked.edu";
+	reason = "Obviously hacked account";
+};
+
+kill {
+	user = "^O[[:alpha:]]?[[:digit:]]+(x\.o|\.xo)$@^[[:alnum:]]{4}\.evilnet.org$";
+	type = regex;
+};
+
+/*
+ * deny {}:  IPs that are not allowed to connect (before DNS/ident lookup)
+ * Oper issued dlines will be added to the specified dline config
+ */
+deny {
+	ip = "10.0.1.0/24";
+	reason = "Reconnecting vhosted bots";
+};
+
+/*
+ * exempt {}: IPs that are exempt from deny {} and Dlines. (OLD d:)
+ */
+exempt {
+	ip = "192.168.0.0/16";
+};
+
+/*
+ * resv {}:  nicks and channels users may not use/join (OLD Q:)
+ */
+resv {
+	/* reason: the reason for the proceeding resv's */
+	reason = "There are no services on this network";
+
+	/* resv: the nicks and channels users may not join/use */
+	nick = "nickserv";
+	nick = "chanserv";
+	channel = "#services";
+
+	/* resv: wildcard masks are also supported in nicks only */
+	reason = "Clone bots";
+	nick = "clone*";
+};
+
+/*
+ * gecos {}:  The X: replacement, used for banning users based on
+ * their "realname".
+ */
+gecos {
+	name = "*sex*";
+	reason = "Possible spambot";
+};
+
+gecos {
+	name = "sub7server";
+	reason = "Trojan drone";
+};
+
+gecos {
+	name = "*http*";
+	reason = "Spambot";
+};
+
+gecos {
+	name = "^\[J[0o]hn Do[3e]\]-[0-9]{2,5}$";
+	type = regex;
+};
+
+/*
+ * channel {}:  The channel block contains options pertaining to channels
+ */
+channel {
+	/*
+	 * disable_fake_channels: this option, if set to 'yes', will
+	 * disallow clients to create or join channels that have one
+	 * of the following ASCII characters in their name:
+	 *
+	 *   2 | bold
+	 *   3 | mirc color
+         *  15 | plain text
+	 *  22 | reverse
+	 *  31 | underline
+	 * 160 | non-breaking space
+	 */
+	disable_fake_channels = yes;
+
+	/*
+	 * restrict_channels: reverse channel RESVs logic, only reserved
+	 * channels are allowed
+	 */
+	restrict_channels = no;
+
+	/*
+	 * disable_local_channels: prevent users from joining &channels.
+	 */
+	disable_local_channels = no;
+
+	/*
+	 * use_invex: Enable/disable channel mode +I, a n!u@h list of masks
+	 * that can join a +i channel without an invite.
+	 */
+	use_invex = yes;
+
+	/*
+	 * use_except: Enable/disable channel mode +e, a n!u@h list of masks
+	 * that can join a channel through a ban (+b).
+	 */
+	use_except = yes;
+
+	/*
+	 * use_knock: Allows users to request an invite to a channel that
+	 * is locked somehow (+ikl).  If the channel is +p or you are banned
+	 * the knock will not be sent.
+	 */
+	use_knock = yes;
+
+	/*
+	 * knock_delay: The amount of time a user must wait between issuing
+	 * the knock command.
+	 */
+	knock_delay = 1 minutes;
+
+	/*
+	 * knock_delay_channel: How often a knock to any specific channel
+	 * is permitted, regardless of the user sending the knock.
+	 */
+	knock_delay_channel = 1 minute;
+
+	/*
+	 * burst_topicwho: enable sending of who set topic on topicburst
+	 * default is yes
+	 */
+	burst_topicwho = yes;
+
+	/*
+	 * max_chans_per_user: The maximum number of channels a user can
+	 * join/be on.
+	 */
+	max_chans_per_user = 25;
+
+	/* quiet_on_ban: stop banned people talking in channels. */
+	quiet_on_ban = yes;
+
+	/* max_bans: maximum number of +b/e/I modes in a channel */
+	max_bans = 1000;
+
+	/*
+	 * how many joins in how many seconds constitute a flood, use 0 to
+	 * disable. +b opers will be notified (changeable via /set)
+	 */
+	join_flood_count = 100;
+	join_flood_time = 10 seconds;
+
+	/*
+	 * splitcode: The ircd will now check splitmode every few seconds.
+	 *
+	 * Either split users or split servers can activate splitmode, but
+	 * both conditions must be met for the ircd to deactivate splitmode.
+	 * 
+	 * You may force splitmode to be permanent by /quote set splitmode on
+	 */
+
+	/*
+	 * default_split_user_count: when the usercount is lower than this level,
+	 * consider ourselves split.  This must be set for automatic splitmode.
+	 */
+	default_split_user_count = 0;
+
+	/*
+	 * default_split_server_count: when the servercount is lower than this,
+	 * consider ourselves split.  This must be set for automatic splitmode.
+	 */
+	default_split_server_count = 0;
+
+	/* split no create: disallow users creating channels on split. */
+	no_create_on_split = yes;
+
+	/* split: no join: disallow users joining channels at all on a split */
+	no_join_on_split = no;
+};
+
+/*
+ * serverhide {}:  The serverhide block contains the options regarding
+ * serverhiding
+ */
+serverhide {
+	/*
+	 * flatten_links: this option will show all servers in /links appear
+	 * that they are linked to this current server
+	 */
+	flatten_links = no;
+
+	/*
+	 * links_delay: how often to update the links file when it is
+	 * flattened.
+	 */
+	links_delay = 5 minutes;
+
+	/*
+	 * hidden: hide this server from a /links output on servers that
+	 * support it.  This allows hub servers to be hidden etc.
+	 */
+	hidden = no;
+
+	/*
+	 * disable_hidden: prevent servers hiding themselves from a
+	 * /links output.
+	 */
+	disable_hidden = no;
+
+	/*
+	 * hide_servers: hide remote servernames everywhere and instead use
+	 * hidden_name and network_desc.
+	 */
+	hide_servers = no;
+
+	/*
+	 * Use this as the servername users see if hide_servers = yes.
+	 */
+	hidden_name = "*.hidden.com";
+
+	/*
+	 * hide_server_ips: If this is disabled, opers will be unable to see servers
+	 * ips and will be shown a masked ip, admins will be shown the real ip.
+	 *
+	 * If this is enabled, nobody can see a servers ip.  *This is a kludge*, it
+	 * has the side effect of hiding the ips everywhere, including logfiles.
+	 *
+	 * We recommend you leave this disabled, and just take care with who you
+	 * give admin=yes; to.
+	 */
+	hide_server_ips = no;
+};
+
+/*
+ * general {}:  The general block contains many of the options that were once
+ * compiled in options in config.h.  The general block is read at start time.
+ */
+general {
+	/*
+	 * gline_min_cidr: the minimum required length of a CIDR bitmask
+	 * for IPv4 based glines
+	 */
+	gline_min_cidr = 16;
+
+	/*
+	 * gline_min_cidr6: the minimum required length of a CIDR bitmask
+	 * for IPv6 based glines
+	 */
+	gline_min_cidr6 = 48;
+
+	/*
+	 * Whether to automatically set mode +i on connecting users.
+	 */
+	invisible_on_connect = yes;
+
+	/*
+	 * If you don't explicitly specify burst_away in your connect blocks, then
+	 * they will default to the burst_away value below.
+	 */
+	burst_away = no;
+
+	/*
+	 * Show "actually using host <ip>" on /whois when possible.
+	 */
+	use_whois_actually = yes;
+
+	/*
+	 * Max time from the nickname change that still causes KILL
+	 * automatically to switch for the current nick of that user. (seconds)
+	 */
+	kill_chase_time_limit = 90;
+
+	/*
+	 * If hide_spoof_ips is disabled, opers will be allowed to see the real IP of spoofed
+	 * users in /trace etc.  If this is defined they will be shown a masked IP.
+	 */
+	hide_spoof_ips = yes;
+
+	/*
+	 * Ignore bogus timestamps from other servers.  Yes, this will desync
+	 * the network, but it will allow chanops to resync with a valid non TS 0
+	 *
+	 * This should be enabled network wide, or not at all.
+	 */
+	ignore_bogus_ts = no;
+
+	/*
+	 * disable_auth: completely disable ident lookups; if you enable this,
+	 * be careful of what you set need_ident to in your auth {} blocks
+	 */
+	disable_auth = no;
+
+	/* disable_remote_commands: disable users doing commands on remote servers */
+	disable_remote_commands = no;
+
+	/*
+	 * tkline_expire_notices: enables or disables temporary kline/xline
+	 * expire notices.
+	 */
+	tkline_expire_notices = no;
+
+	/*
+	 * default_floodcount: the default value of floodcount that is configurable
+	 * via /quote set floodcount.  This is the amount of lines a user
+	 * may send to any other user/channel in one second.
+	 */
+	default_floodcount = 10;
+
+	/*
+	 * failed_oper_notice: send a notice to all opers on the server when 
+	 * someone tries to OPER and uses the wrong password, host or ident.
+	 */
+	failed_oper_notice = yes;
+
+	/*
+	 * dots_in_ident: the amount of '.' characters permitted in an ident
+	 * reply before the user is rejected.
+	 */
+	dots_in_ident = 2;
+
+	/*
+	 * dot_in_ip6_addr: ircd-hybrid-6.0 and earlier will disallow hosts 
+	 * without a '.' in them.  This will add one to the end.  Only needed
+	 * for older servers.
+	 */
+	dot_in_ip6_addr = no;
+
+	/*
+	 * min_nonwildcard: the minimum non wildcard characters in k/d/g lines
+	 * placed via the server.  klines hand placed are exempt from limits.
+	 * wildcard chars: '.' ':' '*' '?' '@' '!' '#'
+	 */
+	min_nonwildcard = 4;
+
+	/*
+	 * min_nonwildcard_simple: the minimum non wildcard characters in 
+	 * gecos bans.  wildcard chars: '*' '?' '#'
+	 */
+	min_nonwildcard_simple = 3;
+
+	/* max_accept: maximum allowed /accept's for +g usermode */
+	max_accept = 20;
+
+	/* anti_nick_flood: enable the nickflood control code */
+	anti_nick_flood = yes;
+
+	/* nick flood: the nick changes allowed in the specified period */
+	max_nick_time = 20 seconds;
+	max_nick_changes = 5;
+
+	/*
+	 * anti_spam_exit_message_time: the minimum time a user must be connected
+	 * before custom quit messages are allowed.
+	 */
+	anti_spam_exit_message_time = 5 minutes;
+
+	/*
+	 * ts delta: the time delta allowed between server clocks before
+	 * a warning is given, or before the link is dropped.  all servers
+	 * should run ntpdate/rdate to keep clocks in sync
+	 */
+	ts_warn_delta = 30 seconds;
+	ts_max_delta = 5 minutes;
+
+	/*
+	 * kline_with_reason: show the user the reason why they are k/d/glined 
+	 * on exit.  May give away who set k/dline when set via tcm.
+	 */
+	kline_with_reason = yes;
+
+	/*
+	 * kline_reason: show this message to users on channel
+	 * instead of the oper reason.
+	 */
+	kline_reason = "Connection closed";
+
+	/*
+	 * reject_hold_time: wait this amount of time before disconnecting
+	 * a rejected client. Use 0 to disable.
+	 */
+	reject_hold_time = 0;
+
+	/*
+	 * warn_no_nline: warn opers about servers that try to connect but
+	 * we don't have a connect {} block for.  Twits with misconfigured 
+	 * servers can get really annoying with this enabled.
+	 */
+	warn_no_nline = yes;
+
+	/*
+	 * stats_e_disabled: set this to 'yes' to disable "STATS e" for both
+	 * operators and administrators.  Doing so is a good idea in case
+	 * there are any exempted (exempt{}) server IPs you don't want to
+	 * see leaked.
+	 */
+	stats_e_disabled = no;
+
+	/* stats_o_oper only: make stats o (opers) oper only */
+	stats_o_oper_only = yes;
+
+	/* stats_P_oper_only: make stats P (ports) oper only */
+	stats_P_oper_only = yes;
+
+	/*
+	 * stats i oper only: make stats i (auth {}) oper only. set to:
+	 *     yes:    show users no auth blocks, made oper only.
+	 *     masked: show users first matching auth block
+	 *     no:     show users all auth blocks.
+	 */
+	stats_i_oper_only = yes;
+
+	/*
+	 * stats_k_oper_only: make stats k/K (klines) oper only.  set to:
+	 *     yes:    show users no auth blocks, made oper only
+	 *     masked: show users first matching auth block
+	 *     no:     show users all auth blocks.
+	 */
+	stats_k_oper_only = yes;
+
+	/*
+	 * caller_id_wait: time between notifying a +g user that somebody
+	 * is messaging them.
+	 */
+	caller_id_wait = 1 minute;
+
+	/*
+	 * opers_bypass_callerid: allows operators to bypass +g and message
+	 * anyone who has it set (useful if you use services).
+	 */
+	opers_bypass_callerid = no;
+
+	/*
+	 * pace_wait_simple: time between use of less intensive commands
+	 * (ADMIN, HELP, (L)USERS, VERSION, remote WHOIS)
+	 */
+	pace_wait_simple = 1 second;
+
+	/*
+	 * pace_wait: time between more intensive commands
+	 * (INFO, LINKS, LIST, MAP, MOTD, STATS, WHO, wildcard WHOIS, WHOWAS)
+	 */
+	pace_wait = 10 seconds;
+
+	/*
+	 * short_motd: send clients a notice telling them to read the motd
+	 * instead of forcing a motd to clients who may simply ignore it.
+	 */
+	short_motd = no;
+
+	/*
+	 * ping_cookie: require clients to respond exactly to a ping command,
+	 * can help block certain types of drones and FTP PASV mode spoofing.
+	 */
+	ping_cookie = no;
+
+	/* no_oper_flood: increase flood limits for opers. */
+	no_oper_flood = yes;
+
+	/*
+	 * true_no_oper_flood: completely eliminate flood limits for opers
+	 * and for clients with can_flood = yes in their auth {} blocks
+	 */
+	true_no_oper_flood = yes;
+
+	/* oper_pass_resv: allow opers to over-ride RESVs on nicks/channels */
+	oper_pass_resv = yes;
+
+	/*
+	 * idletime: the maximum amount of time a user may idle before
+	 * they are disconnected
+	 */
+	idletime = 0;
+
+	/* REMOVE ME.  The following line checks you've been reading. */
+	#havent_read_conf = 1;
+
+	/*
+	 * max_targets: the maximum amount of targets in a single 
+	 * PRIVMSG/NOTICE.  Set to 999 NOT 0 for unlimited.
+	 */
+	max_targets = 4;
+
+	/*
+	 * client_flood: maximum amount of data in a clients queue before
+	 * they are dropped for flooding.
+	 */
+	client_flood = 2560 bytes;
+
+	/*
+	 * message_locale: the default message locale
+	 * Use "standard" for the compiled in defaults.
+	 * To install the translated messages, go into messages/ in the
+	 * source directory and run `make install'.
+	 */
+	message_locale = "standard";
+
+	/*
+	 * usermodes configurable: a list of usermodes for the options below
+	 *
+	 * +b - bots         - See bot and drone flooding notices
+	 * +c - cconn        - Client connection/quit notices
+	 * +D - deaf         - Don't receive channel messages
+	 * +d - debug        - See debugging notices
+	 * +f - full         - See I: line full notices
+	 * +G - softcallerid - Server Side Ignore for users not on your channels
+	 * +g - callerid     - Server Side Ignore (for privmsgs etc)
+	 * +i - invisible    - Not shown in NAMES or WHO unless you share a 
+	 *                     a channel
+	 * +k - skill        - See server generated KILL messages
+	 * +l - locops       - See LOCOPS messages
+	 * +n - nchange      - See client nick changes
+	 * +r - rej          - See rejected client notices
+	 * +s - servnotice   - See general server notices
+	 * +u - unauth       - See unauthorized client notices
+	 * +w - wallop       - See server generated WALLOPS
+	 * +x - external     - See remote server connection and split notices
+	 * +y - spy          - See LINKS, STATS, TRACE notices etc.
+	 * +z - operwall     - See oper generated WALLOPS
+	 */
+
+	/* oper_only_umodes: usermodes only opers may set */
+	oper_only_umodes = bots, cconn, debug, full, skill, nchange, 
+			   rej, spy, external, operwall, locops, unauth;
+
+	/* oper_umodes: default usermodes opers get when they /oper */
+	oper_umodes = bots, locops, servnotice, operwall, wallop;
+
+	/*
+	 * servlink_path: path to 'servlink' program used by ircd to handle
+	 * encrypted/compressed server <-> server links.
+	 *
+	 * only define if servlink is not in same directory as ircd itself.
+	 */
+	#servlink_path = "/usr/local/ircd/bin/servlink";
+
+	/*
+	 * default_cipher_preference: default cipher to use for cryptlink when none is
+	 * specified in connect block.
+	 */
+	#default_cipher_preference = "BF/168";
+
+	/*
+	 * use_egd: if your system does not have *random devices yet you
+	 * want to use OpenSSL and encrypted links, enable this.  Beware -
+	 * EGD is *very* CPU intensive when gathering data for its pool
+	 */
+#	use_egd = yes;
+
+	/*
+	 * egdpool_path: path to EGD pool. Not necessary for OpenSSL >= 0.9.7
+	 * which automatically finds the path.
+	 */
+#	egdpool_path = "/run/egd-pool";
+
+
+	/*
+	 * compression_level: level of compression for compressed links between
+	 * servers.  
+	 *
+	 * values are between: 1 (least compression, fastest)
+	 *                and: 9 (most compression, slowest).
+	 */
+#	compression_level = 6;
+
+	/*
+	 * throttle_time: the minimum amount of time between connections from
+	 * the same ip.  exempt {} blocks are excluded from this throttling.
+	 * Offers protection against flooders who reconnect quickly.  
+	 * Set to 0 to disable.
+	 */
+	throttle_time = 10;
+};
+
+glines {
+	/* enable: enable glines, network wide temp klines */
+	enable = yes;
+
+	/*
+	 * duration: the amount of time a gline will remain on your
+	 * server before expiring
+	 */
+	duration = 1 day;
+
+	/*
+	 * logging: which types of rules you want to log when triggered
+	 * (choose reject or block)
+	 */
+	logging = reject, block;
+
+	/*
+	 * NOTE: gline ACLs can cause a desync of glines throughout the
+	 * network, meaning some servers may have a gline triggered, and
+	 * others may not. Also, you only need insert rules for glines
+	 * that you want to block and/or reject. If you want to accept and
+	 * propagate the gline, do NOT put a rule for it.
+	 */
+
+	/* user@host for rule to apply to */
+	user = "god@I.still.hate.packets";
+	/* server for rule to apply to */
+	name = "hades.arpa";
+
+	/*
+	 * action: action to take when a matching gline is found. options are:
+	 *  reject	- do not apply the gline locally
+	 *  block	- do not propagate the gline
+	 */
+	action = reject, block;
+
+	user = "god@*";
+	name = "*";
+	action = block;
+};
+
diff --git a/nixos/modules/services/networking/iscsi/initiator.nix b/nixos/modules/services/networking/iscsi/initiator.nix
new file mode 100644
index 00000000000..051c9c7bff3
--- /dev/null
+++ b/nixos/modules/services/networking/iscsi/initiator.nix
@@ -0,0 +1,84 @@
+{ config, lib, pkgs, ... }: with lib;
+let
+  cfg = config.services.openiscsi;
+in
+{
+  options.services.openiscsi = with types; {
+    enable = mkEnableOption "the openiscsi iscsi daemon";
+    enableAutoLoginOut = mkEnableOption ''
+      automatic login and logout of all automatic targets.
+      You probably do not want this.
+    '';
+    discoverPortal = mkOption {
+      type = nullOr str;
+      default = null;
+      description = "Portal to discover targets on";
+    };
+    name = mkOption {
+      type = str;
+      description = "Name of this iscsi initiator";
+      example = "iqn.2020-08.org.linux-iscsi.initiatorhost:example";
+    };
+    package = mkOption {
+      type = package;
+      description = "openiscsi package to use";
+      default = pkgs.openiscsi;
+      defaultText = literalExpression "pkgs.openiscsi";
+    };
+
+    extraConfig = mkOption {
+      type = str;
+      default = "";
+      description = "Lines to append to default iscsid.conf";
+    };
+
+    extraConfigFile = mkOption {
+      description = ''
+        Append an additional file's contents to /etc/iscsid.conf. Use a non-store path
+        and store passwords in this file.
+      '';
+      default = null;
+      type = nullOr str;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."iscsi/iscsid.conf.fragment".source = pkgs.runCommand "iscsid.conf" {} ''
+      cat "${cfg.package}/etc/iscsi/iscsid.conf" > $out
+      cat << 'EOF' >> $out
+      ${cfg.extraConfig}
+      ${optionalString cfg.enableAutoLoginOut "node.startup = automatic"}
+      EOF
+    '';
+    environment.etc."iscsi/initiatorname.iscsi".text = "InitiatorName=${cfg.name}";
+
+    system.activationScripts.iscsid = let
+      extraCfgDumper = optionalString (cfg.extraConfigFile != null) ''
+        if [ -f "${cfg.extraConfigFile}" ]; then
+          printf "\n# The following is from ${cfg.extraConfigFile}:\n"
+          cat "${cfg.extraConfigFile}"
+        else
+          echo "Warning: services.openiscsi.extraConfigFile ${cfg.extraConfigFile} does not exist!" >&2
+        fi
+      '';
+    in ''
+      (
+        cat ${config.environment.etc."iscsi/iscsid.conf.fragment".source}
+        ${extraCfgDumper}
+      ) > /etc/iscsi/iscsid.conf
+    '';
+
+    systemd.packages = [ cfg.package ];
+
+    systemd.services."iscsid".wantedBy = [ "multi-user.target" ];
+    systemd.sockets."iscsid".wantedBy = [ "sockets.target" ];
+
+    systemd.services."iscsi" = mkIf cfg.enableAutoLoginOut {
+      wantedBy = [ "remote-fs.target" ];
+      serviceConfig.ExecStartPre = mkIf (cfg.discoverPortal != null) "${cfg.package}/bin/iscsiadm --mode discoverydb --type sendtargets --portal ${escapeShellArg cfg.discoverPortal} --discover";
+    };
+
+    environment.systemPackages = [ cfg.package ];
+    boot.kernelModules = [ "iscsi_tcp" ];
+  };
+}
diff --git a/nixos/modules/services/networking/iscsi/root-initiator.nix b/nixos/modules/services/networking/iscsi/root-initiator.nix
new file mode 100644
index 00000000000..c12aca1bc24
--- /dev/null
+++ b/nixos/modules/services/networking/iscsi/root-initiator.nix
@@ -0,0 +1,190 @@
+{ config, lib, pkgs, ... }: with lib;
+let
+  cfg = config.boot.iscsi-initiator;
+in
+{
+  # If you're booting entirely off another machine you may want to add
+  # this snippet to always boot the latest "system" version. It is not
+  # enabled by default in case you have an initrd on a local disk:
+  #
+  #     boot.initrd.postMountCommands = ''
+  #       ln -sfn /nix/var/nix/profiles/system/init /mnt-root/init
+  #       stage2Init=/init
+  #     '';
+  #
+  # Note: Theoretically you might want to connect to multiple portals and
+  # log in to multiple targets, however the authors of this module so far
+  # don't have the need or expertise to reasonably implement it. Also,
+  # consider carefully before making your boot chain depend on multiple
+  # machines to be up.
+  options.boot.iscsi-initiator = with types; {
+    name = mkOption {
+      description = ''
+        Name of the iSCSI initiator to boot from. Note, booting from iscsi
+        requires networkd based networking.
+      '';
+      default = null;
+      example = "iqn.2020-08.org.linux-iscsi.initiatorhost:example";
+      type = nullOr str;
+    };
+
+    discoverPortal = mkOption {
+      description = ''
+        iSCSI portal to boot from.
+      '';
+      default = null;
+      example = "192.168.1.1:3260";
+      type = nullOr str;
+    };
+
+    target = mkOption {
+      description = ''
+        Name of the iSCSI target to boot from.
+      '';
+      default = null;
+      example = "iqn.2020-08.org.linux-iscsi.targethost:example";
+      type = nullOr str;
+    };
+
+    logLevel = mkOption {
+      description = ''
+        Higher numbers elicits more logs.
+      '';
+      default = 1;
+      example = 8;
+      type = int;
+    };
+
+    loginAll = mkOption {
+      description = ''
+        Do not log into a specific target on the portal, but to all that we discover.
+        This overrides setting target.
+      '';
+      type = bool;
+      default = false;
+    };
+
+    extraIscsiCommands = mkOption {
+      description = "Extra iscsi commands to run in the initrd.";
+      default = "";
+      type = lines;
+    };
+
+    extraConfig = mkOption {
+      description = "Extra lines to append to /etc/iscsid.conf";
+      default = null;
+      type = nullOr lines;
+    };
+
+    extraConfigFile = mkOption {
+      description = ''
+        Append an additional file's contents to `/etc/iscsid.conf`. Use a non-store path
+        and store passwords in this file. Note: the file specified here must be available
+        in the initrd, see: `boot.initrd.secrets`.
+      '';
+      default = null;
+      type = nullOr str;
+    };
+  };
+
+  config = mkIf (cfg.name != null) {
+    # The "scripted" networking configuration (ie: non-networkd)
+    # doesn't properly order the start and stop of the interfaces, and the
+    # network interfaces are torn down before unmounting disks. Since this
+    # module is specifically for very-early-boot network mounts, we need
+    # the network to stay on.
+    #
+    # We could probably fix the scripted options to properly order, but I'm
+    # not inclined to invest that time today. Hopefully this gets users far
+    # enough along and they can just use networkd.
+    networking.useNetworkd = true;
+    networking.useDHCP = false; # Required to set useNetworkd = true
+
+    boot.initrd = {
+      network.enable = true;
+
+      # By default, the stage-1 disables the network and resets the interfaces
+      # on startup. Since our startup disks are on the network, we can't let
+      # the network not work.
+      network.flushBeforeStage2 = false;
+
+      kernelModules = [ "iscsi_tcp" ];
+
+      extraUtilsCommands = ''
+        copy_bin_and_libs ${pkgs.openiscsi}/bin/iscsid
+        copy_bin_and_libs ${pkgs.openiscsi}/bin/iscsiadm
+        ${optionalString (!config.boot.initrd.network.ssh.enable) "cp -pv ${pkgs.glibc.out}/lib/libnss_files.so.* $out/lib"}
+
+        mkdir -p $out/etc/iscsi
+        cp ${config.environment.etc.hosts.source} $out/etc/hosts
+        cp ${pkgs.openiscsi}/etc/iscsi/iscsid.conf $out/etc/iscsi/iscsid.fragment.conf
+        chmod +w $out/etc/iscsi/iscsid.fragment.conf
+        cat << 'EOF' >> $out/etc/iscsi/iscsid.fragment.conf
+        ${optionalString (cfg.extraConfig != null) cfg.extraConfig}
+        EOF
+      '';
+
+      extraUtilsCommandsTest = ''
+        $out/bin/iscsiadm --version
+      '';
+
+      preLVMCommands = let
+        extraCfgDumper = optionalString (cfg.extraConfigFile != null) ''
+          if [ -f "${cfg.extraConfigFile}" ]; then
+            printf "\n# The following is from ${cfg.extraConfigFile}:\n"
+            cat "${cfg.extraConfigFile}"
+          else
+            echo "Warning: boot.iscsi-initiator.extraConfigFile ${cfg.extraConfigFile} does not exist!" >&2
+          fi
+        '';
+      in ''
+        ${optionalString (!config.boot.initrd.network.ssh.enable) ''
+        # stolen from initrd-ssh.nix
+        echo 'root:x:0:0:root:/root:/bin/ash' > /etc/passwd
+        echo 'passwd: files' > /etc/nsswitch.conf
+      ''}
+
+        cp -f $extraUtils/etc/hosts /etc/hosts
+
+        mkdir -p /etc/iscsi /run/lock/iscsi
+        echo "InitiatorName=${cfg.name}" > /etc/iscsi/initiatorname.iscsi
+
+        (
+          cat "$extraUtils/etc/iscsi/iscsid.fragment.conf"
+          printf "\n"
+          ${optionalString cfg.loginAll ''echo "node.startup = automatic"''}
+          ${extraCfgDumper}
+        ) > /etc/iscsi/iscsid.conf
+
+        iscsid --foreground --no-pid-file --debug ${toString cfg.logLevel} &
+        iscsiadm --mode discoverydb \
+          --type sendtargets \
+          --discover \
+          --portal ${escapeShellArg cfg.discoverPortal} \
+          --debug ${toString cfg.logLevel}
+
+        ${if cfg.loginAll then ''
+        iscsiadm --mode node --loginall all
+      '' else ''
+        iscsiadm --mode node --targetname ${escapeShellArg cfg.target} --login
+      ''}
+
+        ${cfg.extraIscsiCommands}
+
+        pkill -9 iscsid
+      '';
+    };
+
+    services.openiscsi = {
+      enable = true;
+      inherit (cfg) name;
+    };
+
+    assertions = [
+      {
+        assertion = cfg.loginAll -> cfg.target == null;
+        message = "iSCSI target name is set while login on all portals is enabled.";
+      }
+    ];
+  };
+}
diff --git a/nixos/modules/services/networking/iscsi/target.nix b/nixos/modules/services/networking/iscsi/target.nix
new file mode 100644
index 00000000000..8a10e7d346a
--- /dev/null
+++ b/nixos/modules/services/networking/iscsi/target.nix
@@ -0,0 +1,53 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.target;
+in
+{
+  ###### interface
+  options = {
+    services.target = with types; {
+      enable = mkEnableOption "the kernel's LIO iscsi target";
+
+      config = mkOption {
+        type = attrs;
+        default = {};
+        description = ''
+          Content of /etc/target/saveconfig.json
+          This file is normally read and written by targetcli
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    environment.etc."target/saveconfig.json" = {
+      text = builtins.toJSON cfg.config;
+      mode = "0600";
+    };
+
+    environment.systemPackages = with pkgs; [ targetcli ];
+
+    boot.kernelModules = [ "configfs" "target_core_mod" "iscsi_target_mod" ];
+
+    systemd.services.iscsi-target = {
+      enable = true;
+      after = [ "network.target" "local-fs.target" ];
+      requires = [ "sys-kernel-config.mount" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkgs.python3.pkgs.rtslib}/bin/targetctl restore";
+        ExecStop = "${pkgs.python3.pkgs.rtslib}/bin/targetctl clear";
+        RemainAfterExit = "yes";
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d /etc/target 0700 root root - -"
+    ];
+  };
+}
diff --git a/nixos/modules/services/networking/iwd.nix b/nixos/modules/services/networking/iwd.nix
new file mode 100644
index 00000000000..8835f7f9372
--- /dev/null
+++ b/nixos/modules/services/networking/iwd.nix
@@ -0,0 +1,62 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.networking.wireless.iwd;
+  ini = pkgs.formats.ini { };
+  configFile = ini.generate "main.conf" cfg.settings;
+in {
+  options.networking.wireless.iwd = {
+    enable = mkEnableOption "iwd";
+
+    settings = mkOption {
+      type = ini.type;
+      default = { };
+
+      example = {
+        Settings.AutoConnect = true;
+
+        Network = {
+          EnableIPv6 = true;
+          RoutePriorityOffset = 300;
+        };
+      };
+
+      description = ''
+        Options passed to iwd.
+        See <link xlink:href="https://iwd.wiki.kernel.org/networkconfigurationsettings">here</link> for supported options.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [{
+      assertion = !config.networking.wireless.enable;
+      message = ''
+        Only one wireless daemon is allowed at the time: networking.wireless.enable and networking.wireless.iwd.enable are mutually exclusive.
+      '';
+    }];
+
+    environment.etc."iwd/main.conf".source = configFile;
+
+    # for iwctl
+    environment.systemPackages =  [ pkgs.iwd ];
+
+    services.dbus.packages = [ pkgs.iwd ];
+
+    systemd.packages = [ pkgs.iwd ];
+
+    systemd.network.links."80-iwd" = {
+      matchConfig.Type = "wlan";
+      linkConfig.NamePolicy = "keep kernel";
+    };
+
+    systemd.services.iwd = {
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ configFile ];
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ mic92 dtzWill ];
+}
diff --git a/nixos/modules/services/networking/jibri/default.nix b/nixos/modules/services/networking/jibri/default.nix
new file mode 100644
index 00000000000..113a7aa4384
--- /dev/null
+++ b/nixos/modules/services/networking/jibri/default.nix
@@ -0,0 +1,417 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.jibri;
+
+  # Copied from the jitsi-videobridge.nix file.
+  toHOCON = x:
+    if isAttrs x && x ? __hocon_envvar then ("\${" + x.__hocon_envvar + "}")
+    else if isAttrs x then "{${ concatStringsSep "," (mapAttrsToList (k: v: ''"${k}":${toHOCON v}'') x) }}"
+    else if isList x then "[${ concatMapStringsSep "," toHOCON x }]"
+    else builtins.toJSON x;
+
+  # We're passing passwords in environment variables that have names generated
+  # from an attribute name, which may not be a valid bash identifier.
+  toVarName = s: "XMPP_PASSWORD_" + stringAsChars (c: if builtins.match "[A-Za-z0-9]" c != null then c else "_") s;
+
+  defaultJibriConfig = {
+    id = "";
+    single-use-mode = false;
+
+    api = {
+      http.external-api-port = 2222;
+      http.internal-api-port = 3333;
+
+      xmpp.environments = flip mapAttrsToList cfg.xmppEnvironments (name: env: {
+        inherit name;
+
+        xmpp-server-hosts = env.xmppServerHosts;
+        xmpp-domain = env.xmppDomain;
+        control-muc = {
+          domain = env.control.muc.domain;
+          room-name = env.control.muc.roomName;
+          nickname = env.control.muc.nickname;
+        };
+
+        control-login = {
+          domain = env.control.login.domain;
+          username = env.control.login.username;
+          password.__hocon_envvar = toVarName "${name}_control";
+        };
+
+        call-login = {
+          domain = env.call.login.domain;
+          username = env.call.login.username;
+          password.__hocon_envvar = toVarName "${name}_call";
+        };
+
+        strip-from-room-domain = env.stripFromRoomDomain;
+        usage-timeout = env.usageTimeout;
+        trust-all-xmpp-certs = env.disableCertificateVerification;
+      });
+    };
+
+    recording = {
+      recordings-directory = "/tmp/recordings";
+      finalize-script = "${cfg.finalizeScript}";
+    };
+
+    streaming.rtmp-allow-list = [ ".*" ];
+
+    chrome.flags = [
+      "--use-fake-ui-for-media-stream"
+      "--start-maximized"
+      "--kiosk"
+      "--enabled"
+      "--disable-infobars"
+      "--autoplay-policy=no-user-gesture-required"
+    ]
+    ++ lists.optional cfg.ignoreCert
+      "--ignore-certificate-errors";
+
+
+    stats.enable-stats-d = true;
+    webhook.subscribers = [ ];
+
+    jwt-info = { };
+
+    call-status-checks = {
+      no-media-timout = "30 seconds";
+      all-muted-timeout = "10 minutes";
+      default-call-empty-timout = "30 seconds";
+    };
+  };
+  # Allow overriding leaves of the default config despite types.attrs not doing any merging.
+  jibriConfig = recursiveUpdate defaultJibriConfig cfg.config;
+  configFile = pkgs.writeText "jibri.conf" (toHOCON { jibri = jibriConfig; });
+in
+{
+  options.services.jibri = with types; {
+    enable = mkEnableOption "Jitsi BRoadcasting Infrastructure. Currently Jibri must be run on a host that is also running <option>services.jitsi-meet.enable</option>, so for most use cases it will be simpler to run <option>services.jitsi-meet.jibri.enable</option>";
+    config = mkOption {
+      type = attrs;
+      default = { };
+      description = ''
+        Jibri configuration.
+        See <link xlink:href="https://github.com/jitsi/jibri/blob/master/src/main/resources/reference.conf" />
+        for default configuration with comments.
+      '';
+    };
+
+    finalizeScript = mkOption {
+      type = types.path;
+      default = pkgs.writeScript "finalize_recording.sh" ''
+        #!/bin/sh
+
+        RECORDINGS_DIR=$1
+
+        echo "This is a dummy finalize script" > /tmp/finalize.out
+        echo "The script was invoked with recordings directory $RECORDINGS_DIR." >> /tmp/finalize.out
+        echo "You should put any finalize logic (renaming, uploading to a service" >> /tmp/finalize.out
+        echo "or storage provider, etc.) in this script" >> /tmp/finalize.out
+
+        exit 0
+      '';
+      defaultText = literalExpression ''
+        pkgs.writeScript "finalize_recording.sh" ''''''
+        #!/bin/sh
+
+        RECORDINGS_DIR=$1
+
+        echo "This is a dummy finalize script" > /tmp/finalize.out
+        echo "The script was invoked with recordings directory $RECORDINGS_DIR." >> /tmp/finalize.out
+        echo "You should put any finalize logic (renaming, uploading to a service" >> /tmp/finalize.out
+        echo "or storage provider, etc.) in this script" >> /tmp/finalize.out
+
+        exit 0
+        '''''';
+      '';
+      example = literalExpression ''
+        pkgs.writeScript "finalize_recording.sh" ''''''
+        #!/bin/sh
+        RECORDINGS_DIR=$1
+        ''${pkgs.rclone}/bin/rclone copy $RECORDINGS_DIR RCLONE_REMOTE:jibri-recordings/ -v --log-file=/var/log/jitsi/jibri/recording-upload.txt
+        exit 0
+        '''''';
+      '';
+      description = ''
+        This script runs when jibri finishes recording a video of a conference.
+      '';
+    };
+
+    ignoreCert = mkOption {
+      type = bool;
+      default = false;
+      example = true;
+      description = ''
+        Whether to enable the flag "--ignore-certificate-errors" for the Chromium browser opened by Jibri.
+        Intended for use in automated tests or anywhere else where using a verified cert for Jitsi-Meet is not possible.
+      '';
+    };
+
+    xmppEnvironments = mkOption {
+      description = ''
+        XMPP servers to connect to.
+      '';
+      example = literalExpression ''
+        "jitsi-meet" = {
+          xmppServerHosts = [ "localhost" ];
+          xmppDomain = config.services.jitsi-meet.hostName;
+
+          control.muc = {
+            domain = "internal.''${config.services.jitsi-meet.hostName}";
+            roomName = "JibriBrewery";
+            nickname = "jibri";
+          };
+
+          control.login = {
+            domain = "auth.''${config.services.jitsi-meet.hostName}";
+            username = "jibri";
+            passwordFile = "/var/lib/jitsi-meet/jibri-auth-secret";
+          };
+
+          call.login = {
+            domain = "recorder.''${config.services.jitsi-meet.hostName}";
+            username = "recorder";
+            passwordFile = "/var/lib/jitsi-meet/jibri-recorder-secret";
+          };
+
+          usageTimeout = "0";
+          disableCertificateVerification = true;
+          stripFromRoomDomain = "conference.";
+        };
+      '';
+      default = { };
+      type = attrsOf (submodule ({ name, ... }: {
+        options = {
+          xmppServerHosts = mkOption {
+            type = listOf str;
+            example = [ "xmpp.example.org" ];
+            description = ''
+              Hostnames of the XMPP servers to connect to.
+            '';
+          };
+          xmppDomain = mkOption {
+            type = str;
+            example = "xmpp.example.org";
+            description = ''
+              The base XMPP domain.
+            '';
+          };
+          control.muc.domain = mkOption {
+            type = str;
+            description = ''
+              The domain part of the MUC to connect to for control.
+            '';
+          };
+          control.muc.roomName = mkOption {
+            type = str;
+            default = "JibriBrewery";
+            description = ''
+              The room name of the MUC to connect to for control.
+            '';
+          };
+          control.muc.nickname = mkOption {
+            type = str;
+            default = "jibri";
+            description = ''
+              The nickname for this Jibri instance in the MUC.
+            '';
+          };
+          control.login.domain = mkOption {
+            type = str;
+            description = ''
+              The domain part of the JID for this Jibri instance.
+            '';
+          };
+          control.login.username = mkOption {
+            type = str;
+            default = "jvb";
+            description = ''
+              User part of the JID.
+            '';
+          };
+          control.login.passwordFile = mkOption {
+            type = str;
+            example = "/run/keys/jibri-xmpp1";
+            description = ''
+              File containing the password for the user.
+            '';
+          };
+
+          call.login.domain = mkOption {
+            type = str;
+            example = "recorder.xmpp.example.org";
+            description = ''
+              The domain part of the JID for the recorder.
+            '';
+          };
+          call.login.username = mkOption {
+            type = str;
+            default = "recorder";
+            description = ''
+              User part of the JID for the recorder.
+            '';
+          };
+          call.login.passwordFile = mkOption {
+            type = str;
+            example = "/run/keys/jibri-recorder-xmpp1";
+            description = ''
+              File containing the password for the user.
+            '';
+          };
+          disableCertificateVerification = mkOption {
+            type = bool;
+            default = false;
+            description = ''
+              Whether to skip validation of the server's certificate.
+            '';
+          };
+
+          stripFromRoomDomain = mkOption {
+            type = str;
+            default = "0";
+            example = "conference.";
+            description = ''
+              The prefix to strip from the room's JID domain to derive the call URL.
+            '';
+          };
+          usageTimeout = mkOption {
+            type = str;
+            default = "0";
+            example = "1 hour";
+            description = ''
+              The duration that the Jibri session can be.
+              A value of zero means indefinitely.
+            '';
+          };
+        };
+
+        config =
+          let
+            nick = mkDefault (builtins.replaceStrings [ "." ] [ "-" ] (
+              config.networking.hostName + optionalString (config.networking.domain != null) ".${config.networking.domain}"
+            ));
+          in
+          {
+            call.login.username = nick;
+            control.muc.nickname = nick;
+          };
+      }));
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups.jibri = { };
+    users.groups.plugdev = { };
+    users.users.jibri = {
+      isSystemUser = true;
+      group = "jibri";
+      home = "/var/lib/jibri";
+      extraGroups = [ "jitsi-meet" "adm" "audio" "video" "plugdev" ];
+    };
+
+    systemd.services.jibri-xorg = {
+      description = "Jitsi Xorg Process";
+
+      after = [ "network.target" ];
+      wantedBy = [ "jibri.service" "jibri-icewm.service" ];
+
+      preStart = ''
+        cp --no-preserve=mode,ownership ${pkgs.jibri}/etc/jitsi/jibri/* /var/lib/jibri
+        mv /var/lib/jibri/{,.}asoundrc
+      '';
+
+      environment.DISPLAY = ":0";
+      serviceConfig = {
+        Type = "simple";
+
+        User = "jibri";
+        Group = "jibri";
+        KillMode = "process";
+        Restart = "on-failure";
+        RestartPreventExitStatus = 255;
+
+        StateDirectory = "jibri";
+
+        ExecStart = "${pkgs.xorg.xorgserver}/bin/Xorg -nocursor -noreset +extension RANDR +extension RENDER -config ${pkgs.jibri}/etc/jitsi/jibri/xorg-video-dummy.conf -logfile /dev/null :0";
+      };
+    };
+
+    systemd.services.jibri-icewm = {
+      description = "Jitsi Window Manager";
+
+      requires = [ "jibri-xorg.service" ];
+      after = [ "jibri-xorg.service" ];
+      wantedBy = [ "jibri.service" ];
+
+      environment.DISPLAY = ":0";
+      serviceConfig = {
+        Type = "simple";
+
+        User = "jibri";
+        Group = "jibri";
+        Restart = "on-failure";
+        RestartPreventExitStatus = 255;
+
+        StateDirectory = "jibri";
+
+        ExecStart = "${pkgs.icewm}/bin/icewm-session";
+      };
+    };
+
+    systemd.services.jibri = {
+      description = "Jibri Process";
+
+      requires = [ "jibri-icewm.service" "jibri-xorg.service" ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      path = with pkgs; [ chromedriver chromium ffmpeg-full ];
+
+      script = (concatStrings (mapAttrsToList
+        (name: env: ''
+          export ${toVarName "${name}_control"}=$(cat ${env.control.login.passwordFile})
+          export ${toVarName "${name}_call"}=$(cat ${env.call.login.passwordFile})
+        '')
+        cfg.xmppEnvironments))
+      + ''
+        ${pkgs.jre8_headless}/bin/java -Djava.util.logging.config.file=${./logging.properties-journal} -Dconfig.file=${configFile} -jar ${pkgs.jibri}/opt/jitsi/jibri/jibri.jar --config /var/lib/jibri/jibri.json
+      '';
+
+      environment.HOME = "/var/lib/jibri";
+
+      serviceConfig = {
+        Type = "simple";
+
+        User = "jibri";
+        Group = "jibri";
+        Restart = "always";
+        RestartPreventExitStatus = 255;
+
+        StateDirectory = "jibri";
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d /var/log/jitsi/jibri 755 jibri jibri"
+    ];
+
+
+
+    # Configure Chromium to not show the "Chrome is being controlled by automatic test software" message.
+    environment.etc."chromium/policies/managed/managed_policies.json".text = builtins.toJSON { CommandLineFlagSecurityWarningsEnabled = false; };
+    warnings = [ "All security warnings for Chromium have been disabled. This is necessary for Jibri, but it also impacts all other uses of Chromium on this system." ];
+
+    boot = {
+      extraModprobeConfig = ''
+        options snd-aloop enable=1,1,1,1,1,1,1,1
+      '';
+      kernelModules = [ "snd-aloop" ];
+    };
+  };
+
+  meta.maintainers = lib.teams.jitsi.members;
+}
diff --git a/nixos/modules/services/networking/jibri/logging.properties-journal b/nixos/modules/services/networking/jibri/logging.properties-journal
new file mode 100644
index 00000000000..61eadbfddcb
--- /dev/null
+++ b/nixos/modules/services/networking/jibri/logging.properties-journal
@@ -0,0 +1,32 @@
+handlers = java.util.logging.FileHandler
+
+java.util.logging.FileHandler.level = FINE
+java.util.logging.FileHandler.pattern   = /var/log/jitsi/jibri/log.%g.txt
+java.util.logging.FileHandler.formatter = net.java.sip.communicator.util.ScLogFormatter
+java.util.logging.FileHandler.count = 10
+java.util.logging.FileHandler.limit = 10000000
+
+org.jitsi.jibri.capture.ffmpeg.util.FfmpegFileHandler.level = FINE
+org.jitsi.jibri.capture.ffmpeg.util.FfmpegFileHandler.pattern   = /var/log/jitsi/jibri/ffmpeg.%g.txt
+org.jitsi.jibri.capture.ffmpeg.util.FfmpegFileHandler.formatter = net.java.sip.communicator.util.ScLogFormatter
+org.jitsi.jibri.capture.ffmpeg.util.FfmpegFileHandler.count = 10
+org.jitsi.jibri.capture.ffmpeg.util.FfmpegFileHandler.limit = 10000000
+
+org.jitsi.jibri.sipgateway.pjsua.util.PjsuaFileHandler.level = FINE
+org.jitsi.jibri.sipgateway.pjsua.util.PjsuaFileHandler.pattern   = /var/log/jitsi/jibri/pjsua.%g.txt
+org.jitsi.jibri.sipgateway.pjsua.util.PjsuaFileHandler.formatter = net.java.sip.communicator.util.ScLogFormatter
+org.jitsi.jibri.sipgateway.pjsua.util.PjsuaFileHandler.count = 10
+org.jitsi.jibri.sipgateway.pjsua.util.PjsuaFileHandler.limit = 10000000
+
+org.jitsi.jibri.selenium.util.BrowserFileHandler.level = FINE
+org.jitsi.jibri.selenium.util.BrowserFileHandler.pattern   = /var/log/jitsi/jibri/browser.%g.txt
+org.jitsi.jibri.selenium.util.BrowserFileHandler.formatter = net.java.sip.communicator.util.ScLogFormatter
+org.jitsi.jibri.selenium.util.BrowserFileHandler.count = 10
+org.jitsi.jibri.selenium.util.BrowserFileHandler.limit = 10000000
+
+org.jitsi.level = FINE
+org.jitsi.jibri.config.level = INFO
+
+org.glassfish.level = INFO
+org.osgi.level = INFO
+org.jitsi.xmpp.level = INFO
diff --git a/nixos/modules/services/networking/jicofo.nix b/nixos/modules/services/networking/jicofo.nix
new file mode 100644
index 00000000000..647119b9039
--- /dev/null
+++ b/nixos/modules/services/networking/jicofo.nix
@@ -0,0 +1,152 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.jicofo;
+in
+{
+  options.services.jicofo = with types; {
+    enable = mkEnableOption "Jitsi Conference Focus - component of Jitsi Meet";
+
+    xmppHost = mkOption {
+      type = str;
+      example = "localhost";
+      description = ''
+        Hostname of the XMPP server to connect to.
+      '';
+    };
+
+    xmppDomain = mkOption {
+      type = nullOr str;
+      example = "meet.example.org";
+      description = ''
+        Domain name of the XMMP server to which to connect as a component.
+
+        If null, <option>xmppHost</option> is used.
+      '';
+    };
+
+    componentPasswordFile = mkOption {
+      type = str;
+      example = "/run/keys/jicofo-component";
+      description = ''
+        Path to file containing component secret.
+      '';
+    };
+
+    userName = mkOption {
+      type = str;
+      default = "focus";
+      description = ''
+        User part of the JID for XMPP user connection.
+      '';
+    };
+
+    userDomain = mkOption {
+      type = str;
+      example = "auth.meet.example.org";
+      description = ''
+        Domain part of the JID for XMPP user connection.
+      '';
+    };
+
+    userPasswordFile = mkOption {
+      type = str;
+      example = "/run/keys/jicofo-user";
+      description = ''
+        Path to file containing password for XMPP user connection.
+      '';
+    };
+
+    bridgeMuc = mkOption {
+      type = str;
+      example = "jvbbrewery@internal.meet.example.org";
+      description = ''
+        JID of the internal MUC used to communicate with Videobridges.
+      '';
+    };
+
+    config = mkOption {
+      type = attrsOf str;
+      default = { };
+      example = literalExpression ''
+        {
+          "org.jitsi.jicofo.auth.URL" = "XMPP:jitsi-meet.example.com";
+        }
+      '';
+      description = ''
+        Contents of the <filename>sip-communicator.properties</filename> configuration file for jicofo.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.jicofo.config = mapAttrs (_: v: mkDefault v) {
+      "org.jitsi.jicofo.BRIDGE_MUC" = cfg.bridgeMuc;
+    };
+
+    users.groups.jitsi-meet = {};
+
+    systemd.services.jicofo = let
+      jicofoProps = {
+        "-Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION" = "/etc/jitsi";
+        "-Dnet.java.sip.communicator.SC_HOME_DIR_NAME" = "jicofo";
+        "-Djava.util.logging.config.file" = "/etc/jitsi/jicofo/logging.properties";
+      };
+    in
+    {
+      description = "JItsi COnference FOcus";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      restartTriggers = [
+        config.environment.etc."jitsi/jicofo/sip-communicator.properties".source
+      ];
+      environment.JAVA_SYS_PROPS = concatStringsSep " " (mapAttrsToList (k: v: "${k}=${toString v}") jicofoProps);
+
+      script = ''
+        ${pkgs.jicofo}/bin/jicofo \
+          --host=${cfg.xmppHost} \
+          --domain=${if cfg.xmppDomain == null then cfg.xmppHost else cfg.xmppDomain} \
+          --secret=$(cat ${cfg.componentPasswordFile}) \
+          --user_name=${cfg.userName} \
+          --user_domain=${cfg.userDomain} \
+          --user_password=$(cat ${cfg.userPasswordFile})
+      '';
+
+      serviceConfig = {
+        Type = "exec";
+
+        DynamicUser = true;
+        User = "jicofo";
+        Group = "jitsi-meet";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+      };
+    };
+
+    environment.etc."jitsi/jicofo/sip-communicator.properties".source =
+      pkgs.writeText "sip-communicator.properties" (
+        generators.toKeyValue {} cfg.config
+      );
+    environment.etc."jitsi/jicofo/logging.properties".source =
+      mkDefault "${pkgs.jicofo}/etc/jitsi/jicofo/logging.properties-journal";
+  };
+
+  meta.maintainers = lib.teams.jitsi.members;
+}
diff --git a/nixos/modules/services/networking/jitsi-videobridge.nix b/nixos/modules/services/networking/jitsi-videobridge.nix
new file mode 100644
index 00000000000..abb0bd0a25e
--- /dev/null
+++ b/nixos/modules/services/networking/jitsi-videobridge.nix
@@ -0,0 +1,288 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.jitsi-videobridge;
+  attrsToArgs = a: concatStringsSep " " (mapAttrsToList (k: v: "${k}=${toString v}") a);
+
+  # HOCON is a JSON superset that videobridge2 uses for configuration.
+  # It can substitute environment variables which we use for passwords here.
+  # https://github.com/lightbend/config/blob/master/README.md
+  #
+  # Substitution for environment variable FOO is represented as attribute set
+  # { __hocon_envvar = "FOO"; }
+  toHOCON = x: if isAttrs x && x ? __hocon_envvar then ("\${" + x.__hocon_envvar + "}")
+    else if isAttrs x then "{${ concatStringsSep "," (mapAttrsToList (k: v: ''"${k}":${toHOCON v}'') x) }}"
+    else if isList x then "[${ concatMapStringsSep "," toHOCON x }]"
+    else builtins.toJSON x;
+
+  # We're passing passwords in environment variables that have names generated
+  # from an attribute name, which may not be a valid bash identifier.
+  toVarName = s: "XMPP_PASSWORD_" + stringAsChars (c: if builtins.match "[A-Za-z0-9]" c != null then c else "_") s;
+
+  defaultJvbConfig = {
+    videobridge = {
+      ice = {
+        tcp = {
+          enabled = true;
+          port = 4443;
+        };
+        udp.port = 10000;
+      };
+      stats = {
+        enabled = true;
+        transports = [ { type = "muc"; } ];
+      };
+      apis.xmpp-client.configs = flip mapAttrs cfg.xmppConfigs (name: xmppConfig: {
+        hostname = xmppConfig.hostName;
+        domain = xmppConfig.domain;
+        username = xmppConfig.userName;
+        password = { __hocon_envvar = toVarName name; };
+        muc_jids = xmppConfig.mucJids;
+        muc_nickname = xmppConfig.mucNickname;
+        disable_certificate_verification = xmppConfig.disableCertificateVerification;
+      });
+    };
+  };
+
+  # Allow overriding leaves of the default config despite types.attrs not doing any merging.
+  jvbConfig = recursiveUpdate defaultJvbConfig cfg.config;
+in
+{
+  options.services.jitsi-videobridge = with types; {
+    enable = mkEnableOption "Jitsi Videobridge, a WebRTC compatible video router";
+
+    config = mkOption {
+      type = attrs;
+      default = { };
+      example = literalExpression ''
+        {
+          videobridge = {
+            ice.udp.port = 5000;
+            websockets = {
+              enabled = true;
+              server-id = "jvb1";
+            };
+          };
+        }
+      '';
+      description = ''
+        Videobridge configuration.
+
+        See <link xlink:href="https://github.com/jitsi/jitsi-videobridge/blob/master/src/main/resources/reference.conf" />
+        for default configuration with comments.
+      '';
+    };
+
+    xmppConfigs = mkOption {
+      description = ''
+        XMPP servers to connect to.
+
+        See <link xlink:href="https://github.com/jitsi/jitsi-videobridge/blob/master/doc/muc.md" /> for more information.
+      '';
+      default = { };
+      example = literalExpression ''
+        {
+          "localhost" = {
+            hostName = "localhost";
+            userName = "jvb";
+            domain = "auth.xmpp.example.org";
+            passwordFile = "/var/lib/jitsi-meet/videobridge-secret";
+            mucJids = "jvbbrewery@internal.xmpp.example.org";
+          };
+        }
+      '';
+      type = attrsOf (submodule ({ name, ... }: {
+        options = {
+          hostName = mkOption {
+            type = str;
+            example = "xmpp.example.org";
+            description = ''
+              Hostname of the XMPP server to connect to. Name of the attribute set is used by default.
+            '';
+          };
+          domain = mkOption {
+            type = nullOr str;
+            default = null;
+            example = "auth.xmpp.example.org";
+            description = ''
+              Domain part of JID of the XMPP user, if it is different from hostName.
+            '';
+          };
+          userName = mkOption {
+            type = str;
+            default = "jvb";
+            description = ''
+              User part of the JID.
+            '';
+          };
+          passwordFile = mkOption {
+            type = str;
+            example = "/run/keys/jitsi-videobridge-xmpp1";
+            description = ''
+              File containing the password for the user.
+            '';
+          };
+          mucJids = mkOption {
+            type = str;
+            example = "jvbbrewery@internal.xmpp.example.org";
+            description = ''
+              JID of the MUC to join. JiCoFo needs to be configured to join the same MUC.
+            '';
+          };
+          mucNickname = mkOption {
+            # Upstream DEBs use UUID, let's use hostname instead.
+            type = str;
+            description = ''
+              Videobridges use the same XMPP account and need to be distinguished by the
+              nickname (aka resource part of the JID). By default, system hostname is used.
+            '';
+          };
+          disableCertificateVerification = mkOption {
+            type = bool;
+            default = false;
+            description = ''
+              Whether to skip validation of the server's certificate.
+            '';
+          };
+        };
+        config = {
+          hostName = mkDefault name;
+          mucNickname = mkDefault (builtins.replaceStrings [ "." ] [ "-" ] (
+            config.networking.hostName + optionalString (config.networking.domain != null) ".${config.networking.domain}"
+          ));
+        };
+      }));
+    };
+
+    nat = {
+      localAddress = mkOption {
+        type = nullOr str;
+        default = null;
+        example = "192.168.1.42";
+        description = ''
+          Local address when running behind NAT.
+        '';
+      };
+
+      publicAddress = mkOption {
+        type = nullOr str;
+        default = null;
+        example = "1.2.3.4";
+        description = ''
+          Public address when running behind NAT.
+        '';
+      };
+    };
+
+    extraProperties = mkOption {
+      type = attrsOf str;
+      default = { };
+      description = ''
+        Additional Java properties passed to jitsi-videobridge.
+      '';
+    };
+
+    openFirewall = mkOption {
+      type = bool;
+      default = false;
+      description = ''
+        Whether to open ports in the firewall for the videobridge.
+      '';
+    };
+
+    apis = mkOption {
+      type = with types; listOf str;
+      description = ''
+        What is passed as --apis= parameter. If this is empty, "none" is passed.
+        Needed for monitoring jitsi.
+      '';
+      default = [];
+      example = literalExpression "[ \"colibri\" \"rest\" ]";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups.jitsi-meet = {};
+
+    services.jitsi-videobridge.extraProperties = optionalAttrs (cfg.nat.localAddress != null) {
+      "org.ice4j.ice.harvest.NAT_HARVESTER_LOCAL_ADDRESS" = cfg.nat.localAddress;
+      "org.ice4j.ice.harvest.NAT_HARVESTER_PUBLIC_ADDRESS" = cfg.nat.publicAddress;
+    };
+
+    systemd.services.jitsi-videobridge2 = let
+      jvbProps = {
+        "-Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION" = "/etc/jitsi";
+        "-Dnet.java.sip.communicator.SC_HOME_DIR_NAME" = "videobridge";
+        "-Djava.util.logging.config.file" = "/etc/jitsi/videobridge/logging.properties";
+        "-Dconfig.file" = pkgs.writeText "jvb.conf" (toHOCON jvbConfig);
+        # Mitigate CVE-2021-44228
+        "-Dlog4j2.formatMsgNoLookups" = true;
+      } // (mapAttrs' (k: v: nameValuePair "-D${k}" v) cfg.extraProperties);
+    in
+    {
+      aliases = [ "jitsi-videobridge.service" ];
+      description = "Jitsi Videobridge";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment.JAVA_SYS_PROPS = attrsToArgs jvbProps;
+
+      script = (concatStrings (mapAttrsToList (name: xmppConfig:
+        "export ${toVarName name}=$(cat ${xmppConfig.passwordFile})\n"
+      ) cfg.xmppConfigs))
+      + ''
+        ${pkgs.jitsi-videobridge}/bin/jitsi-videobridge --apis=${if (cfg.apis == []) then "none" else concatStringsSep "," cfg.apis}
+      '';
+
+      serviceConfig = {
+        Type = "exec";
+
+        DynamicUser = true;
+        User = "jitsi-videobridge";
+        Group = "jitsi-meet";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+
+        TasksMax = 65000;
+        LimitNPROC = 65000;
+        LimitNOFILE = 65000;
+      };
+    };
+
+    environment.etc."jitsi/videobridge/logging.properties".source =
+      mkDefault "${pkgs.jitsi-videobridge}/etc/jitsi/videobridge/logging.properties-journal";
+
+    # (from videobridge2 .deb)
+    # this sets the max, so that we can bump the JVB UDP single port buffer size.
+    boot.kernel.sysctl."net.core.rmem_max" = mkDefault 10485760;
+    boot.kernel.sysctl."net.core.netdev_max_backlog" = mkDefault 100000;
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall
+      [ jvbConfig.videobridge.ice.tcp.port ];
+    networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall
+      [ jvbConfig.videobridge.ice.udp.port ];
+
+    assertions = [{
+      message = "publicAddress must be set if and only if localAddress is set";
+      assertion = (cfg.nat.publicAddress == null) == (cfg.nat.localAddress == null);
+    }];
+  };
+
+  meta.maintainers = lib.teams.jitsi.members;
+}
diff --git a/nixos/modules/services/networking/kea.nix b/nixos/modules/services/networking/kea.nix
new file mode 100644
index 00000000000..17b4eb2e283
--- /dev/null
+++ b/nixos/modules/services/networking/kea.nix
@@ -0,0 +1,383 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+with lib;
+
+let
+  cfg = config.services.kea;
+
+  format = pkgs.formats.json {};
+
+  ctrlAgentConfig = format.generate "kea-ctrl-agent.conf" {
+    Control-agent = cfg.ctrl-agent.settings;
+  };
+  dhcp4Config = format.generate "kea-dhcp4.conf" {
+    Dhcp4 = cfg.dhcp4.settings;
+  };
+  dhcp6Config = format.generate "kea-dhcp6.conf" {
+    Dhcp6 = cfg.dhcp6.settings;
+  };
+  dhcpDdnsConfig = format.generate "kea-dhcp-ddns.conf" {
+    DhcpDdns = cfg.dhcp-ddns.settings;
+  };
+
+  package = pkgs.kea;
+in
+{
+  options.services.kea = with types; {
+    ctrl-agent = mkOption {
+      description = ''
+        Kea Control Agent configuration
+      '';
+      default = {};
+      type = submodule {
+        options = {
+          enable = mkEnableOption "Kea Control Agent";
+
+          extraArgs = mkOption {
+            type = listOf str;
+            default = [];
+            description = ''
+              List of additonal arguments to pass to the daemon.
+            '';
+          };
+
+          settings = mkOption {
+            type = format.type;
+            default = null;
+            description = ''
+              Kea Control Agent configuration as an attribute set, see <link xlink:href="https://kea.readthedocs.io/en/kea-${package.version}/arm/agent.html"/>.
+            '';
+          };
+        };
+      };
+    };
+
+    dhcp4 = mkOption {
+      description = ''
+        DHCP4 Server configuration
+      '';
+      default = {};
+      type = submodule {
+        options = {
+          enable = mkEnableOption "Kea DHCP4 server";
+
+          extraArgs = mkOption {
+            type = listOf str;
+            default = [];
+            description = ''
+              List of additonal arguments to pass to the daemon.
+            '';
+          };
+
+          settings = mkOption {
+            type = format.type;
+            default = null;
+            example = {
+              valid-lifetime = 4000;
+              renew-timer = 1000;
+              rebind-timer = 2000;
+              interfaces-config = {
+                interfaces = [
+                  "eth0"
+                ];
+              };
+              lease-database = {
+                type = "memfile";
+                persist = true;
+                name = "/var/lib/kea/dhcp4.leases";
+              };
+              subnet4 = [ {
+                subnet = "192.0.2.0/24";
+                pools = [ {
+                  pool = "192.0.2.100 - 192.0.2.240";
+                } ];
+              } ];
+            };
+            description = ''
+              Kea DHCP4 configuration as an attribute set, see <link xlink:href="https://kea.readthedocs.io/en/kea-${package.version}/arm/dhcp4-srv.html"/>.
+            '';
+          };
+        };
+      };
+    };
+
+    dhcp6 = mkOption {
+      description = ''
+        DHCP6 Server configuration
+      '';
+      default = {};
+      type = submodule {
+        options = {
+          enable = mkEnableOption "Kea DHCP6 server";
+
+          extraArgs = mkOption {
+            type = listOf str;
+            default = [];
+            description = ''
+              List of additonal arguments to pass to the daemon.
+            '';
+          };
+
+          settings = mkOption {
+            type = format.type;
+            default = null;
+            example = {
+              valid-lifetime = 4000;
+              renew-timer = 1000;
+              rebind-timer = 2000;
+              preferred-lifetime = 3000;
+              interfaces-config = {
+                interfaces = [
+                  "eth0"
+                ];
+              };
+              lease-database = {
+                type = "memfile";
+                persist = true;
+                name = "/var/lib/kea/dhcp6.leases";
+              };
+              subnet6 = [ {
+                subnet = "2001:db8:1::/64";
+                pools = [ {
+                  pool = "2001:db8:1::1-2001:db8:1::ffff";
+                } ];
+              } ];
+            };
+            description = ''
+              Kea DHCP6 configuration as an attribute set, see <link xlink:href="https://kea.readthedocs.io/en/kea-${package.version}/arm/dhcp6-srv.html"/>.
+            '';
+          };
+        };
+      };
+    };
+
+    dhcp-ddns = mkOption {
+      description = ''
+        Kea DHCP-DDNS configuration
+      '';
+      default = {};
+      type = submodule {
+        options = {
+          enable = mkEnableOption "Kea DDNS server";
+
+          extraArgs = mkOption {
+            type = listOf str;
+            default = [];
+            description = ''
+              List of additonal arguments to pass to the daemon.
+            '';
+          };
+
+          settings = mkOption {
+            type = format.type;
+            default = null;
+            example = {
+              ip-address = "127.0.0.1";
+              port = 53001;
+              dns-server-timeout = 100;
+              ncr-protocol = "UDP";
+              ncr-format = "JSON";
+              tsig-keys = [ ];
+              forward-ddns = {
+                ddns-domains = [ ];
+              };
+              reverse-ddns = {
+                ddns-domains = [ ];
+              };
+            };
+            description = ''
+              Kea DHCP-DDNS configuration as an attribute set, see <link xlink:href="https://kea.readthedocs.io/en/kea-${package.version}/arm/ddns.html"/>.
+            '';
+          };
+        };
+      };
+    };
+  };
+
+  config = let
+    commonServiceConfig = {
+      ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+      DynamicUser = true;
+      User = "kea";
+      ConfigurationDirectory = "kea";
+      RuntimeDirectory = "kea";
+      StateDirectory = "kea";
+      UMask = "0077";
+    };
+  in mkIf (cfg.ctrl-agent.enable || cfg.dhcp4.enable || cfg.dhcp6.enable || cfg.dhcp-ddns.enable) (mkMerge [
+  {
+    environment.systemPackages = [ package ];
+  }
+
+  (mkIf cfg.ctrl-agent.enable {
+
+    environment.etc."kea/ctrl-agent.conf".source = ctrlAgentConfig;
+
+    systemd.services.kea-ctrl-agent = {
+      description = "Kea Control Agent";
+      documentation = [
+        "man:kea-ctrl-agent(8)"
+        "https://kea.readthedocs.io/en/kea-${package.version}/arm/agent.html"
+      ];
+
+      after = [
+        "network-online.target"
+        "time-sync.target"
+      ];
+      wantedBy = [
+        "kea-dhcp4-server.service"
+        "kea-dhcp6-server.service"
+        "kea-dhcp-ddns-server.service"
+      ];
+
+      environment = {
+        KEA_PIDFILE_DIR = "/run/kea";
+        KEA_LOCKFILE_DIR = "/run/kea";
+      };
+
+      restartTriggers = [
+        ctrlAgentConfig
+      ];
+
+      serviceConfig = {
+        ExecStart = "${package}/bin/kea-ctrl-agent -c /etc/kea/ctrl-agent.conf ${lib.escapeShellArgs cfg.dhcp4.extraArgs}";
+        KillMode = "process";
+        Restart = "on-failure";
+      } // commonServiceConfig;
+    };
+  })
+
+  (mkIf cfg.dhcp4.enable {
+
+    environment.etc."kea/dhcp4-server.conf".source = dhcp4Config;
+
+    systemd.services.kea-dhcp4-server = {
+      description = "Kea DHCP4 Server";
+      documentation = [
+        "man:kea-dhcp4(8)"
+        "https://kea.readthedocs.io/en/kea-${package.version}/arm/dhcp4-srv.html"
+      ];
+
+      after = [
+        "network-online.target"
+        "time-sync.target"
+      ];
+      wantedBy = [
+        "multi-user.target"
+      ];
+
+      environment = {
+        KEA_PIDFILE_DIR = "/run/kea";
+        KEA_LOCKFILE_DIR = "/run/kea";
+      };
+
+      restartTriggers = [
+        dhcp4Config
+      ];
+
+      serviceConfig = {
+        ExecStart = "${package}/bin/kea-dhcp4 -c /etc/kea/dhcp4-server.conf ${lib.escapeShellArgs cfg.dhcp4.extraArgs}";
+        # Kea does not request capabilities by itself
+        AmbientCapabilities = [
+          "CAP_NET_BIND_SERVICE"
+          "CAP_NET_RAW"
+        ];
+        CapabilityBoundingSet = [
+          "CAP_NET_BIND_SERVICE"
+          "CAP_NET_RAW"
+        ];
+      } // commonServiceConfig;
+    };
+  })
+
+  (mkIf cfg.dhcp6.enable {
+
+    environment.etc."kea/dhcp6-server.conf".source = dhcp6Config;
+
+    systemd.services.kea-dhcp6-server = {
+      description = "Kea DHCP6 Server";
+      documentation = [
+        "man:kea-dhcp6(8)"
+        "https://kea.readthedocs.io/en/kea-${package.version}/arm/dhcp6-srv.html"
+      ];
+
+      after = [
+        "network-online.target"
+        "time-sync.target"
+      ];
+      wantedBy = [
+        "multi-user.target"
+      ];
+
+      environment = {
+        KEA_PIDFILE_DIR = "/run/kea";
+        KEA_LOCKFILE_DIR = "/run/kea";
+      };
+
+      restartTriggers = [
+        dhcp6Config
+      ];
+
+      serviceConfig = {
+        ExecStart = "${package}/bin/kea-dhcp6 -c /etc/kea/dhcp6-server.conf ${lib.escapeShellArgs cfg.dhcp6.extraArgs}";
+        # Kea does not request capabilities by itself
+        AmbientCapabilities = [
+          "CAP_NET_BIND_SERVICE"
+        ];
+        CapabilityBoundingSet = [
+          "CAP_NET_BIND_SERVICE"
+        ];
+      } // commonServiceConfig;
+    };
+  })
+
+  (mkIf cfg.dhcp-ddns.enable {
+
+    environment.etc."kea/dhcp-ddns.conf".source = dhcpDdnsConfig;
+
+    systemd.services.kea-dhcp-ddns-server = {
+      description = "Kea DHCP-DDNS Server";
+      documentation = [
+        "man:kea-dhcp-ddns(8)"
+        "https://kea.readthedocs.io/en/kea-${package.version}/arm/ddns.html"
+      ];
+
+      after = [
+        "network-online.target"
+        "time-sync.target"
+      ];
+      wantedBy = [
+        "multi-user.target"
+      ];
+
+      environment = {
+        KEA_PIDFILE_DIR = "/run/kea";
+        KEA_LOCKFILE_DIR = "/run/kea";
+      };
+
+      restartTriggers = [
+        dhcpDdnsConfig
+      ];
+
+      serviceConfig = {
+        ExecStart = "${package}/bin/kea-dhcp-ddns -c /etc/kea/dhcp-ddns.conf ${lib.escapeShellArgs cfg.dhcp-ddns.extraArgs}";
+        AmbientCapabilities = [
+          "CAP_NET_BIND_SERVICE"
+        ];
+        CapabilityBoundingSet = [
+          "CAP_NET_BIND_SERVICE"
+        ];
+      } // commonServiceConfig;
+    };
+  })
+
+  ]);
+
+  meta.maintainers = with maintainers; [ hexa ];
+  # uses attributes of the linked package
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/networking/keepalived/default.nix b/nixos/modules/services/networking/keepalived/default.nix
new file mode 100644
index 00000000000..c9ac2ee2599
--- /dev/null
+++ b/nixos/modules/services/networking/keepalived/default.nix
@@ -0,0 +1,303 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.keepalived;
+
+  keepalivedConf = pkgs.writeText "keepalived.conf" ''
+    global_defs {
+      ${optionalString cfg.enableScriptSecurity "enable_script_security"}
+      ${snmpGlobalDefs}
+      ${cfg.extraGlobalDefs}
+    }
+
+    ${vrrpScriptStr}
+    ${vrrpInstancesStr}
+    ${cfg.extraConfig}
+  '';
+
+  snmpGlobalDefs = with cfg.snmp; optionalString enable (
+    optionalString (socket != null) "snmp_socket ${socket}\n"
+    + optionalString enableKeepalived "enable_snmp_keepalived\n"
+    + optionalString enableChecker "enable_snmp_checker\n"
+    + optionalString enableRfc "enable_snmp_rfc\n"
+    + optionalString enableRfcV2 "enable_snmp_rfcv2\n"
+    + optionalString enableRfcV3 "enable_snmp_rfcv3\n"
+    + optionalString enableTraps "enable_traps"
+  );
+
+  vrrpScriptStr = concatStringsSep "\n" (map (s:
+    ''
+      vrrp_script ${s.name} {
+        script "${s.script}"
+        interval ${toString s.interval}
+        fall ${toString s.fall}
+        rise ${toString s.rise}
+        timeout ${toString s.timeout}
+        weight ${toString s.weight}
+        user ${s.user} ${optionalString (s.group != null) s.group}
+
+        ${s.extraConfig}
+      }
+    ''
+  ) vrrpScripts);
+
+  vrrpInstancesStr = concatStringsSep "\n" (map (i:
+    ''
+      vrrp_instance ${i.name} {
+        interface ${i.interface}
+        state ${i.state}
+        virtual_router_id ${toString i.virtualRouterId}
+        priority ${toString i.priority}
+        ${optionalString i.noPreempt "nopreempt"}
+
+        ${optionalString i.useVmac (
+          "use_vmac" + optionalString (i.vmacInterface != null) " ${i.vmacInterface}"
+        )}
+        ${optionalString i.vmacXmitBase "vmac_xmit_base"}
+
+        ${optionalString (i.unicastSrcIp != null) "unicast_src_ip ${i.unicastSrcIp}"}
+        unicast_peer {
+          ${concatStringsSep "\n" i.unicastPeers}
+        }
+
+        virtual_ipaddress {
+          ${concatMapStringsSep "\n" virtualIpLine i.virtualIps}
+        }
+
+        ${optionalString (builtins.length i.trackScripts > 0) ''
+          track_script {
+            ${concatStringsSep "\n" i.trackScripts}
+          }
+        ''}
+
+        ${optionalString (builtins.length i.trackInterfaces > 0) ''
+          track_interface {
+            ${concatStringsSep "\n" i.trackInterfaces}
+          }
+        ''}
+
+        ${i.extraConfig}
+      }
+    ''
+  ) vrrpInstances);
+
+  virtualIpLine = (ip:
+    ip.addr
+    + optionalString (notNullOrEmpty ip.brd) " brd ${ip.brd}"
+    + optionalString (notNullOrEmpty ip.dev) " dev ${ip.dev}"
+    + optionalString (notNullOrEmpty ip.scope) " scope ${ip.scope}"
+    + optionalString (notNullOrEmpty ip.label) " label ${ip.label}"
+  );
+
+  notNullOrEmpty = s: !(s == null || s == "");
+
+  vrrpScripts = mapAttrsToList (name: config:
+    {
+      inherit name;
+    } // config
+  ) cfg.vrrpScripts;
+
+  vrrpInstances = mapAttrsToList (iName: iConfig:
+    {
+      name = iName;
+    } // iConfig
+  ) cfg.vrrpInstances;
+
+  vrrpInstanceAssertions = i: [
+    { assertion = i.interface != "";
+      message = "services.keepalived.vrrpInstances.${i.name}.interface option cannot be empty.";
+    }
+    { assertion = i.virtualRouterId >= 0 && i.virtualRouterId <= 255;
+      message = "services.keepalived.vrrpInstances.${i.name}.virtualRouterId must be an integer between 0..255.";
+    }
+    { assertion = i.priority >= 0 && i.priority <= 255;
+      message = "services.keepalived.vrrpInstances.${i.name}.priority must be an integer between 0..255.";
+    }
+    { assertion = i.vmacInterface == null || i.useVmac;
+      message = "services.keepalived.vrrpInstances.${i.name}.vmacInterface has no effect when services.keepalived.vrrpInstances.${i.name}.useVmac is not set.";
+    }
+    { assertion = !i.vmacXmitBase || i.useVmac;
+      message = "services.keepalived.vrrpInstances.${i.name}.vmacXmitBase has no effect when services.keepalived.vrrpInstances.${i.name}.useVmac is not set.";
+    }
+  ] ++ flatten (map (virtualIpAssertions i.name) i.virtualIps)
+    ++ flatten (map (vrrpScriptAssertion i.name) i.trackScripts);
+
+  virtualIpAssertions = vrrpName: ip: [
+    { assertion = ip.addr != "";
+      message = "The 'addr' option for an services.keepalived.vrrpInstances.${vrrpName}.virtualIps entry cannot be empty.";
+    }
+  ];
+
+  vrrpScriptAssertion = vrrpName: scriptName: {
+    assertion = builtins.hasAttr scriptName cfg.vrrpScripts;
+    message = "services.keepalived.vrrpInstances.${vrrpName} trackscript ${scriptName} is not defined in services.keepalived.vrrpScripts.";
+  };
+
+  pidFile = "/run/keepalived.pid";
+
+in
+{
+
+  options = {
+    services.keepalived = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Keepalived.
+        '';
+      };
+
+      enableScriptSecurity = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Don't run scripts configured to be run as root if any part of the path is writable by a non-root user.
+        '';
+      };
+
+      snmp = {
+
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether to enable the builtin AgentX subagent.
+          '';
+        };
+
+        socket = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            Socket to use for connecting to SNMP master agent. If this value is
+            set to null, keepalived's default will be used, which is
+            unix:/var/agentx/master, unless using a network namespace, when the
+            default is udp:localhost:705.
+          '';
+        };
+
+        enableKeepalived = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Enable SNMP handling of vrrp element of KEEPALIVED MIB.
+          '';
+        };
+
+        enableChecker = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Enable SNMP handling of checker element of KEEPALIVED MIB.
+          '';
+        };
+
+        enableRfc = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Enable SNMP handling of RFC2787 and RFC6527 VRRP MIBs.
+          '';
+        };
+
+        enableRfcV2 = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Enable SNMP handling of RFC2787 VRRP MIB.
+          '';
+        };
+
+        enableRfcV3 = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Enable SNMP handling of RFC6527 VRRP MIB.
+          '';
+        };
+
+        enableTraps = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Enable SNMP traps.
+          '';
+        };
+
+      };
+
+      vrrpScripts = mkOption {
+        type = types.attrsOf (types.submodule (import ./vrrp-script-options.nix {
+          inherit lib;
+        }));
+        default = {};
+        description = "Declarative vrrp script config";
+      };
+
+      vrrpInstances = mkOption {
+        type = types.attrsOf (types.submodule (import ./vrrp-instance-options.nix {
+          inherit lib;
+        }));
+        default = {};
+        description = "Declarative vhost config";
+      };
+
+      extraGlobalDefs = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra lines to be added verbatim to the 'global_defs' block of the
+          configuration file
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra lines to be added verbatim to the configuration file.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = flatten (map vrrpInstanceAssertions vrrpInstances);
+
+    systemd.timers.keepalived-boot-delay = {
+      description = "Keepalive Daemon delay to avoid instant transition to MASTER state";
+      after = [ "network.target" "network-online.target" "syslog.target" ];
+      requires = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      timerConfig = {
+        OnActiveSec = "5s";
+        Unit = "keepalived.service";
+      };
+    };
+
+    systemd.services.keepalived = {
+      description = "Keepalive Daemon (LVS and VRRP)";
+      after = [ "network.target" "network-online.target" "syslog.target" ];
+      wants = [ "network-online.target" ];
+      serviceConfig = {
+        Type = "forking";
+        PIDFile = pidFile;
+        KillMode = "process";
+        ExecStart = "${pkgs.keepalived}/sbin/keepalived"
+          + " -f ${keepalivedConf}"
+          + " -p ${pidFile}"
+          + optionalString cfg.snmp.enable " --snmp";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        Restart = "always";
+        RestartSec = "1s";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/keepalived/virtual-ip-options.nix b/nixos/modules/services/networking/keepalived/virtual-ip-options.nix
new file mode 100644
index 00000000000..1b8889b1b47
--- /dev/null
+++ b/nixos/modules/services/networking/keepalived/virtual-ip-options.nix
@@ -0,0 +1,50 @@
+{ lib } :
+
+with lib;
+{
+  options = {
+
+    addr = mkOption {
+      type = types.str;
+      description = ''
+        IP address, optionally with a netmask: IPADDR[/MASK]
+      '';
+    };
+
+    brd = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The broadcast address on the interface.
+      '';
+    };
+
+    dev = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The name of the device to add the address to.
+      '';
+    };
+
+    scope = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The scope of the area where this address is valid.
+      '';
+    };
+
+    label = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Each address may be tagged with a label string. In order to preserve
+        compatibility with Linux-2.0 net aliases, this string must coincide with
+        the name of the device or must be prefixed with the device name followed
+        by colon.
+      '';
+    };
+
+  };
+}
diff --git a/nixos/modules/services/networking/keepalived/vrrp-instance-options.nix b/nixos/modules/services/networking/keepalived/vrrp-instance-options.nix
new file mode 100644
index 00000000000..e96dde5fa89
--- /dev/null
+++ b/nixos/modules/services/networking/keepalived/vrrp-instance-options.nix
@@ -0,0 +1,133 @@
+{ lib } :
+
+with lib;
+{
+  options = {
+
+    interface = mkOption {
+      type = types.str;
+      description = ''
+        Interface for inside_network, bound by vrrp.
+      '';
+    };
+
+    state = mkOption {
+      type = types.enum [ "MASTER" "BACKUP" ];
+      default = "BACKUP";
+      description = ''
+        Initial state. As soon as the other machine(s) come up, an election will
+        be held and the machine with the highest "priority" will become MASTER.
+        So the entry here doesn't matter a whole lot.
+      '';
+    };
+
+    virtualRouterId = mkOption {
+      type = types.int;
+      description = ''
+        Arbitrary unique number 0..255. Used to differentiate multiple instances
+        of vrrpd running on the same NIC (and hence same socket).
+      '';
+    };
+
+    priority = mkOption {
+      type = types.int;
+      default = 100;
+      description = ''
+        For electing MASTER, highest priority wins. To be MASTER, make 50 more
+        than other machines.
+      '';
+    };
+
+    noPreempt = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        VRRP will normally preempt a lower priority machine when a higher
+        priority machine comes online. "nopreempt" allows the lower priority
+        machine to maintain the master role, even when a higher priority machine
+        comes back online. NOTE: For this to work, the initial state of this
+        entry must be BACKUP.
+      '';
+    };
+
+    useVmac = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Use VRRP Virtual MAC.
+      '';
+    };
+
+    vmacInterface = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+         Name of the vmac interface to use. keepalived will come up with a name
+         if you don't specify one.
+      '';
+    };
+
+    vmacXmitBase = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Send/Recv VRRP messages from base interface instead of VMAC interface.
+      '';
+    };
+
+    unicastSrcIp = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+         Default IP for binding vrrpd is the primary IP on interface. If you
+         want to hide location of vrrpd, use this IP as src_addr for unicast
+         vrrp packets.
+      '';
+    };
+
+    unicastPeers = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Do not send VRRP adverts over VRRP multicast group. Instead it sends
+        adverts to the following list of ip addresses using unicast design
+        fashion. It can be cool to use VRRP FSM and features in a networking
+        environment where multicast is not supported! IP Addresses specified can
+        IPv4 as well as IPv6.
+      '';
+    };
+
+    virtualIps = mkOption {
+      type = types.listOf (types.submodule (import ./virtual-ip-options.nix {
+        inherit lib;
+      }));
+      default = [];
+      # TODO: example
+      description = "Declarative vhost config";
+    };
+
+    trackScripts = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "chk_cmd1" "chk_cmd2" ];
+      description = "List of script names to invoke for health tracking.";
+    };
+
+    trackInterfaces = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "eth0" "eth1" ];
+      description = "List of network interfaces to monitor for health tracking.";
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Extra lines to be added verbatim to the vrrp_instance section.
+      '';
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/keepalived/vrrp-script-options.nix b/nixos/modules/services/networking/keepalived/vrrp-script-options.nix
new file mode 100644
index 00000000000..df7a89cff8c
--- /dev/null
+++ b/nixos/modules/services/networking/keepalived/vrrp-script-options.nix
@@ -0,0 +1,64 @@
+{ lib } :
+
+with lib;
+with lib.types;
+{
+  options = {
+
+    script = mkOption {
+      type = str;
+      example = literalExpression ''"''${pkgs.curl} -f http://localhost:80"'';
+      description = "(Path of) Script command to execute followed by args, i.e. cmd [args]...";
+    };
+
+    interval = mkOption {
+      type = int;
+      default = 1;
+      description = "Seconds between script invocations.";
+    };
+
+    timeout = mkOption {
+      type = int;
+      default = 5;
+      description = "Seconds after which script is considered to have failed.";
+    };
+
+    weight = mkOption {
+      type = int;
+      default = 0;
+      description = "Following a failure, adjust the priority by this weight.";
+    };
+
+    rise = mkOption {
+      type = int;
+      default = 5;
+      description = "Required number of successes for OK transition.";
+    };
+
+    fall = mkOption {
+      type = int;
+      default = 3;
+      description = "Required number of failures for KO transition.";
+    };
+
+    user = mkOption {
+      type = str;
+      default = "keepalived_script";
+      description = "Name of user to run the script under.";
+    };
+
+    group = mkOption {
+      type = nullOr str;
+      default = null;
+      description = "Name of group to run the script under. Defaults to user group.";
+    };
+
+    extraConfig = mkOption {
+      type = lines;
+      default = "";
+      description = "Extra lines to be added verbatim to the vrrp_script section.";
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/keybase.nix b/nixos/modules/services/networking/keybase.nix
new file mode 100644
index 00000000000..495102cb7ee
--- /dev/null
+++ b/nixos/modules/services/networking/keybase.nix
@@ -0,0 +1,47 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.keybase;
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.keybase = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to start the Keybase service.";
+      };
+
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    # Upstream: https://github.com/keybase/client/blob/master/packaging/linux/systemd/keybase.service
+    systemd.user.services.keybase = {
+      description = "Keybase service";
+      unitConfig.ConditionUser = "!@system";
+      environment.KEYBASE_SERVICE_TYPE = "systemd";
+      serviceConfig = {
+        Type = "notify";
+        EnvironmentFile = [
+          "-%E/keybase/keybase.autogen.env"
+          "-%E/keybase/keybase.env"
+        ];
+        ExecStart = "${pkgs.keybase}/bin/keybase service";
+        Restart = "on-failure";
+        PrivateTmp = true;
+      };
+      wantedBy = [ "default.target" ];
+    };
+
+    environment.systemPackages = [ pkgs.keybase ];
+  };
+}
diff --git a/nixos/modules/services/networking/knot.nix b/nixos/modules/services/networking/knot.nix
new file mode 100644
index 00000000000..a58a03997b3
--- /dev/null
+++ b/nixos/modules/services/networking/knot.nix
@@ -0,0 +1,152 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.knot;
+
+  configFile = pkgs.writeTextFile {
+    name = "knot.conf";
+    text = (concatMapStringsSep "\n" (file: "include: ${file}") cfg.keyFiles) + "\n" +
+           cfg.extraConfig;
+    checkPhase = lib.optionalString (cfg.keyFiles == []) ''
+      ${cfg.package}/bin/knotc --config=$out conf-check
+    '';
+  };
+
+  socketFile = "/run/knot/knot.sock";
+
+  knot-cli-wrappers = pkgs.stdenv.mkDerivation {
+    name = "knot-cli-wrappers";
+    buildInputs = [ pkgs.makeWrapper ];
+    buildCommand = ''
+      mkdir -p $out/bin
+      makeWrapper ${cfg.package}/bin/knotc "$out/bin/knotc" \
+        --add-flags "--config=${configFile}" \
+        --add-flags "--socket=${socketFile}"
+      makeWrapper ${cfg.package}/bin/keymgr "$out/bin/keymgr" \
+        --add-flags "--config=${configFile}"
+      for executable in kdig khost kjournalprint knsec3hash knsupdate kzonecheck
+      do
+        ln -s "${cfg.package}/bin/$executable" "$out/bin/$executable"
+      done
+      mkdir -p "$out/share"
+      ln -s '${cfg.package}/share/man' "$out/share/"
+    '';
+  };
+in {
+  options = {
+    services.knot = {
+      enable = mkEnableOption "Knot authoritative-only DNS server";
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          List of additional command line paramters for knotd
+        '';
+      };
+
+      keyFiles = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          A list of files containing additional configuration
+          to be included using the include directive. This option
+          allows to include configuration like TSIG keys without
+          exposing them to the nix store readable to any process.
+          Note that using this option will also disable configuration
+          checks at build time.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra lines to be added verbatim to knot.conf
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.knot-dns;
+        defaultText = literalExpression "pkgs.knot-dns";
+        description = ''
+          Which Knot DNS package to use
+        '';
+      };
+    };
+  };
+
+  config = mkIf config.services.knot.enable {
+    users.groups.knot = {};
+    users.users.knot = {
+      isSystemUser = true;
+      group = "knot";
+      description = "Knot daemon user";
+    };
+
+    systemd.services.knot = {
+      unitConfig.Documentation = "man:knotd(8) man:knot.conf(5) man:knotc(8) https://www.knot-dns.cz/docs/${cfg.package.version}/html/";
+      description = cfg.package.meta.description;
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network.target" ];
+      after = ["network.target" ];
+
+      serviceConfig = {
+        Type = "notify";
+        ExecStart = "${cfg.package}/bin/knotd --config=${configFile} --socket=${socketFile} ${concatStringsSep " " cfg.extraArgs}";
+        ExecReload = "${knot-cli-wrappers}/bin/knotc reload";
+        User = "knot";
+        Group = "knot";
+
+        AmbientCapabilities = [
+          "CAP_NET_BIND_SERVICE"
+        ];
+        CapabilityBoundingSet = [
+          "CAP_NET_BIND_SERVICE"
+        ];
+        DeviceAllow = "";
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = false; # breaks capability passing
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        Restart = "on-abort";
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+          "AF_UNIX"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime =true;
+        RestrictSUIDSGID = true;
+        RuntimeDirectory = "knot";
+        StateDirectory = "knot";
+        StateDirectoryMode = "0700";
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+        ];
+        UMask = "0077";
+      };
+    };
+
+    environment.systemPackages = [ knot-cli-wrappers ];
+  };
+}
diff --git a/nixos/modules/services/networking/kresd.nix b/nixos/modules/services/networking/kresd.nix
new file mode 100644
index 00000000000..28b8be7a9a0
--- /dev/null
+++ b/nixos/modules/services/networking/kresd.nix
@@ -0,0 +1,151 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.kresd;
+
+  # Convert systemd-style address specification to kresd config line(s).
+  # On Nix level we don't attempt to precisely validate the address specifications.
+  # The optional IPv6 scope spec comes *after* port, perhaps surprisingly.
+  mkListen = kind: addr: let
+    al_v4 = builtins.match "([0-9.]+):([0-9]+)($)" addr;
+    al_v6 = builtins.match "\\[(.+)]:([0-9]+)(%.*|$)" addr;
+    al_portOnly = builtins.match "([0-9]+)" addr;
+    al = findFirst (a: a != null)
+      (throw "services.kresd.*: incorrect address specification '${addr}'")
+      [ al_v4 al_v6 al_portOnly ];
+    port = elemAt al 1;
+    addrSpec = if al_portOnly == null then "'${head al}${elemAt al 2}'" else "{'::', '0.0.0.0'}";
+    in # freebind is set for compatibility with earlier kresd services;
+       # it could be configurable, for example.
+      ''
+        net.listen(${addrSpec}, ${port}, { kind = '${kind}', freebind = true })
+      '';
+
+  configFile = pkgs.writeText "kresd.conf" (
+    ""
+    + concatMapStrings (mkListen "dns") cfg.listenPlain
+    + concatMapStrings (mkListen "tls") cfg.listenTLS
+    + concatMapStrings (mkListen "doh2") cfg.listenDoH
+    + cfg.extraConfig
+  );
+in {
+  meta.maintainers = [ maintainers.vcunat /* upstream developer */ ];
+
+  imports = [
+    (mkChangedOptionModule [ "services" "kresd" "interfaces" ] [ "services" "kresd" "listenPlain" ]
+      (config:
+        let value = getAttrFromPath [ "services" "kresd" "interfaces" ] config;
+        in map
+          (iface: if elem ":" (stringToCharacters iface) then "[${iface}]:53" else "${iface}:53") # Syntax depends on being IPv6 or IPv4.
+          value
+      )
+    )
+    (mkRemovedOptionModule [ "services" "kresd" "cacheDir" ] "Please use (bind-)mounting instead.")
+  ];
+
+  ###### interface
+  options.services.kresd = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable knot-resolver domain name server.
+        DNSSEC validation is turned on by default.
+        You can run <literal>sudo nc -U /run/knot-resolver/control/1</literal>
+        and give commands interactively to kresd@1.service.
+      '';
+    };
+    package = mkOption {
+      type = types.package;
+      description = "
+        knot-resolver package to use.
+      ";
+      default = pkgs.knot-resolver;
+      defaultText = literalExpression "pkgs.knot-resolver";
+      example = literalExpression "pkgs.knot-resolver.override { extraFeatures = true; }";
+    };
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Extra lines to be added verbatim to the generated configuration file.
+      '';
+    };
+    listenPlain = mkOption {
+      type = with types; listOf str;
+      default = [ "[::1]:53" "127.0.0.1:53" ];
+      example = [ "53" ];
+      description = ''
+        What addresses and ports the server should listen on.
+        For detailed syntax see ListenStream in man systemd.socket.
+      '';
+    };
+    listenTLS = mkOption {
+      type = with types; listOf str;
+      default = [];
+      example = [ "198.51.100.1:853" "[2001:db8::1]:853" "853" ];
+      description = ''
+        Addresses and ports on which kresd should provide DNS over TLS (see RFC 7858).
+        For detailed syntax see ListenStream in man systemd.socket.
+      '';
+    };
+    listenDoH = mkOption {
+      type = with types; listOf str;
+      default = [];
+      example = [ "198.51.100.1:443" "[2001:db8::1]:443" "443" ];
+      description = ''
+        Addresses and ports on which kresd should provide DNS over HTTPS/2 (see RFC 8484).
+        For detailed syntax see ListenStream in man systemd.socket.
+      '';
+    };
+    instances = mkOption {
+      type = types.ints.unsigned;
+      default = 1;
+      description = ''
+        The number of instances to start.  They will be called kresd@{1,2,...}.service.
+        Knot Resolver uses no threads, so this is the way to scale.
+        You can dynamically start/stop them at will, so this is just system default.
+      '';
+    };
+    # TODO: perhaps options for more common stuff like cache size or forwarding
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    environment.etc."knot-resolver/kresd.conf".source = configFile; # not required
+
+    networking.resolvconf.useLocalResolver = mkDefault true;
+
+    users.users.knot-resolver =
+      { isSystemUser = true;
+        group = "knot-resolver";
+        description = "Knot-resolver daemon user";
+      };
+    users.groups.knot-resolver.gid = null;
+
+    systemd.packages = [ cfg.package ]; # the units are patched inside the package a bit
+
+    systemd.targets.kresd = { # configure units started by default
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "kres-cache-gc.service" ]
+        ++ map (i: "kresd@${toString i}.service") (range 1 cfg.instances);
+    };
+    systemd.services."kresd@".serviceConfig = {
+      ExecStart = "${cfg.package}/bin/kresd --noninteractive "
+        + "-c ${cfg.package}/lib/knot-resolver/distro-preconfig.lua -c ${configFile}";
+      # Ensure /run/knot-resolver exists
+      RuntimeDirectory = "knot-resolver";
+      RuntimeDirectoryMode = "0770";
+      # Ensure /var/lib/knot-resolver exists
+      StateDirectory = "knot-resolver";
+      StateDirectoryMode = "0770";
+      # Ensure /var/cache/knot-resolver exists
+      CacheDirectory = "knot-resolver";
+      CacheDirectoryMode = "0770";
+    };
+    # We don't mind running stop phase from wrong version.  It seems less racy.
+    systemd.services."kresd@".stopIfChanged = false;
+  };
+}
diff --git a/nixos/modules/services/networking/lambdabot.nix b/nixos/modules/services/networking/lambdabot.nix
new file mode 100644
index 00000000000..3005e582455
--- /dev/null
+++ b/nixos/modules/services/networking/lambdabot.nix
@@ -0,0 +1,82 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.lambdabot;
+
+  rc = builtins.toFile "script.rc" cfg.script;
+
+in
+
+{
+
+  ### configuration
+
+  options = {
+
+    services.lambdabot = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable the Lambdabot IRC bot";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.lambdabot;
+        defaultText = literalExpression "pkgs.lambdabot";
+        description = "Used lambdabot package";
+      };
+
+      script = mkOption {
+        type = types.str;
+        default = "";
+        description = "Lambdabot script";
+      };
+
+    };
+
+  };
+
+  ### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.lambdabot = {
+      description = "Lambdabot daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      # Workaround for https://github.com/lambdabot/lambdabot/issues/117
+      script = ''
+        mkdir -p ~/.lambdabot
+        cd ~/.lambdabot
+        mkfifo /run/lambdabot/offline
+        (
+          echo 'rc ${rc}'
+          while true; do
+            cat /run/lambdabot/offline
+          done
+        ) | ${cfg.package}/bin/lambdabot
+      '';
+      serviceConfig = {
+        User = "lambdabot";
+        RuntimeDirectory = [ "lambdabot" ];
+      };
+    };
+
+    users.users.lambdabot = {
+      group = "lambdabot";
+      description = "Lambdabot daemon user";
+      home = "/var/lib/lambdabot";
+      createHome = true;
+      uid = config.ids.uids.lambdabot;
+    };
+
+    users.groups.lambdabot.gid = config.ids.gids.lambdabot;
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/libreswan.nix b/nixos/modules/services/networking/libreswan.nix
new file mode 100644
index 00000000000..429167aed9d
--- /dev/null
+++ b/nixos/modules/services/networking/libreswan.nix
@@ -0,0 +1,160 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.libreswan;
+
+  libexec = "${pkgs.libreswan}/libexec/ipsec";
+  ipsec = "${pkgs.libreswan}/sbin/ipsec";
+
+  trim = chars: str:
+  let
+    nonchars = filter (x : !(elem x.value chars))
+               (imap0 (i: v: {ind = i; value = v;}) (stringToCharacters str));
+  in
+    if length nonchars == 0 then ""
+    else substring (head nonchars).ind (add 1 (sub (last nonchars).ind (head nonchars).ind)) str;
+  indent = str: concatStrings (concatMap (s: ["  " (trim [" " "\t"] s) "\n"]) (splitString "\n" str));
+  configText = indent (toString cfg.configSetup);
+  connectionText = concatStrings (mapAttrsToList (n: v:
+    ''
+      conn ${n}
+      ${indent v}
+    '') cfg.connections);
+
+  configFile = pkgs.writeText "ipsec-nixos.conf"
+    ''
+      config setup
+      ${configText}
+
+      ${connectionText}
+    '';
+
+  policyFiles = mapAttrs' (name: text:
+    { name = "ipsec.d/policies/${name}";
+      value.source = pkgs.writeText "ipsec-policy-${name}" text;
+    }) cfg.policies;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.libreswan = {
+
+      enable = mkEnableOption "Libreswan IPsec service";
+
+      configSetup = mkOption {
+        type = types.lines;
+        default = ''
+            protostack=netkey
+            virtual_private=%v4:10.0.0.0/8,%v4:192.168.0.0/16,%v4:172.16.0.0/12,%v4:25.0.0.0/8,%v4:100.64.0.0/10,%v6:fd00::/8,%v6:fe80::/10
+        '';
+        example = ''
+            secretsfile=/root/ipsec.secrets
+            protostack=netkey
+            virtual_private=%v4:10.0.0.0/8,%v4:192.168.0.0/16,%v4:172.16.0.0/12,%v4:25.0.0.0/8,%v4:100.64.0.0/10,%v6:fd00::/8,%v6:fe80::/10
+        '';
+        description = "Options to go in the 'config setup' section of the Libreswan IPsec configuration";
+      };
+
+      connections = mkOption {
+        type = types.attrsOf types.lines;
+        default = {};
+        example = literalExpression ''
+          { myconnection = '''
+              auto=add
+              left=%defaultroute
+              leftid=@user
+
+              right=my.vpn.com
+
+              ikev2=no
+              ikelifetime=8h
+            ''';
+          }
+        '';
+        description = "A set of connections to define for the Libreswan IPsec service";
+      };
+
+      policies = mkOption {
+        type = types.attrsOf types.lines;
+        default = {};
+        example = literalExpression ''
+          { private-or-clear = '''
+              # Attempt opportunistic IPsec for the entire Internet
+              0.0.0.0/0
+              ::/0
+            ''';
+          }
+        '';
+        description = ''
+          A set of policies to apply to the IPsec connections.
+
+          <note><para>
+            The policy name must match the one of connection it needs to apply to.
+          </para></note>
+        '';
+      };
+
+      disableRedirects = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to disable send and accept redirects for all nework interfaces.
+          See the Libreswan <link xlink:href="https://libreswan.org/wiki/FAQ#Why_is_it_recommended_to_disable_send_redirects_in_.2Fproc.2Fsys.2Fnet_.3F">
+          FAQ</link> page for why this is recommended.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    # Install package, systemd units, etc.
+    environment.systemPackages = [ pkgs.libreswan pkgs.iproute2 ];
+    systemd.packages = [ pkgs.libreswan ];
+    systemd.tmpfiles.packages = [ pkgs.libreswan ];
+
+    # Install configuration files
+    environment.etc = {
+      "ipsec.secrets".source = "${pkgs.libreswan}/etc/ipsec.secrets";
+      "ipsec.conf".source = "${pkgs.libreswan}/etc/ipsec.conf";
+      "ipsec.d/01-nixos.conf".source = configFile;
+    } // policyFiles;
+
+    # Create NSS database directory
+    systemd.tmpfiles.rules = [ "d /var/lib/ipsec/nss 755 root root -" ];
+
+    systemd.services.ipsec = {
+      description = "Internet Key Exchange (IKE) Protocol Daemon for IPsec";
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ configFile ] ++ mapAttrsToList (n: v: v.source) policyFiles;
+      path = with pkgs; [
+        libreswan
+        iproute2
+        procps
+        nssTools
+        iptables
+        nettools
+      ];
+      preStart = optionalString cfg.disableRedirects ''
+        # Disable send/receive redirects
+        echo 0 | tee /proc/sys/net/ipv4/conf/*/send_redirects
+        echo 0 | tee /proc/sys/net/ipv{4,6}/conf/*/accept_redirects
+      '';
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/lldpd.nix b/nixos/modules/services/networking/lldpd.nix
new file mode 100644
index 00000000000..d5de9c45d84
--- /dev/null
+++ b/nixos/modules/services/networking/lldpd.nix
@@ -0,0 +1,39 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.lldpd;
+
+in
+
+{
+  options.services.lldpd = {
+    enable = mkEnableOption "Link Layer Discovery Protocol Daemon";
+
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "-c" "-k" "-I eth0" ];
+      description = "List of command line parameters for lldpd";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users._lldpd = {
+      description = "lldpd user";
+      group = "_lldpd";
+      home = "/run/lldpd";
+      isSystemUser = true;
+    };
+    users.groups._lldpd = {};
+
+    environment.systemPackages = [ pkgs.lldpd ];
+    systemd.packages = [ pkgs.lldpd ];
+
+    systemd.services.lldpd = {
+      wantedBy = [ "multi-user.target" ];
+      environment.LLDPD_OPTIONS = concatStringsSep " " cfg.extraArgs;
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/logmein-hamachi.nix b/nixos/modules/services/networking/logmein-hamachi.nix
new file mode 100644
index 00000000000..11cbdda2f84
--- /dev/null
+++ b/nixos/modules/services/networking/logmein-hamachi.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.logmein-hamachi;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.logmein-hamachi.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description =
+        ''
+          Whether to enable LogMeIn Hamachi, a proprietary
+          (closed source) commercial VPN software.
+        '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.logmein-hamachi = {
+      description = "LogMeIn Hamachi Daemon";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${pkgs.logmein-hamachi}/bin/hamachid";
+      };
+    };
+
+    environment.systemPackages = [ pkgs.logmein-hamachi ];
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/lxd-image-server.nix b/nixos/modules/services/networking/lxd-image-server.nix
new file mode 100644
index 00000000000..b119ba8acf6
--- /dev/null
+++ b/nixos/modules/services/networking/lxd-image-server.nix
@@ -0,0 +1,137 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.lxd-image-server;
+  format = pkgs.formats.toml {};
+
+  location = "/var/www/simplestreams";
+in
+{
+  options = {
+    services.lxd-image-server = {
+      enable = mkEnableOption "lxd-image-server";
+
+      group = mkOption {
+        type = types.str;
+        description = "Group assigned to the user and the webroot directory.";
+        default = "nginx";
+        example = "www-data";
+      };
+
+      settings = mkOption {
+        type = format.type;
+        description = ''
+          Configuration for lxd-image-server.
+
+          Example see <link xlink:href="https://github.com/Avature/lxd-image-server/blob/master/config.toml"/>.
+        '';
+        default = {};
+      };
+
+      nginx = {
+        enable = mkEnableOption "nginx";
+        domain = mkOption {
+          type = types.str;
+          description = "Domain to use for nginx virtual host.";
+          example = "images.example.org";
+        };
+      };
+    };
+  };
+
+  config = mkMerge [
+    (mkIf (cfg.enable) {
+      users.users.lxd-image-server = {
+        isSystemUser = true;
+        group = cfg.group;
+      };
+      users.groups.${cfg.group} = {};
+
+      environment.etc."lxd-image-server/config.toml".source = format.generate "config.toml" cfg.settings;
+
+      services.logrotate.paths.lxd-image-server = {
+        path = "/var/log/lxd-image-server/lxd-image-server.log";
+        frequency = "daily";
+        keep = 21;
+        extraConfig = ''
+          create 755 lxd-image-server ${cfg.group}
+          missingok
+          compress
+          delaycompress
+          copytruncate
+          notifempty
+        '';
+      };
+
+      systemd.tmpfiles.rules = [
+        "d /var/www/simplestreams 0755 lxd-image-server ${cfg.group}"
+      ];
+
+      systemd.services.lxd-image-server = {
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+
+        description = "LXD Image Server";
+
+        script = ''
+          ${pkgs.lxd-image-server}/bin/lxd-image-server init
+          ${pkgs.lxd-image-server}/bin/lxd-image-server watch
+        '';
+
+        serviceConfig = {
+          User = "lxd-image-server";
+          Group = cfg.group;
+          DynamicUser = true;
+          LogsDirectory = "lxd-image-server";
+          RuntimeDirectory = "lxd-image-server";
+          ExecReload = "${pkgs.lxd-image-server}/bin/lxd-image-server reload";
+          ReadWritePaths = [ location ];
+        };
+      };
+    })
+    # this is seperate so it can be enabled on mirrored hosts
+    (mkIf (cfg.nginx.enable) {
+      # https://github.com/Avature/lxd-image-server/blob/master/resources/nginx/includes/lxd-image-server.pkg.conf
+      services.nginx.virtualHosts = {
+        "${cfg.nginx.domain}" = {
+          forceSSL = true;
+          enableACME = mkDefault true;
+
+          root = location;
+
+          locations = {
+            "/streams/v1/" = {
+              index = "index.json";
+            };
+
+            # Serve json files with content type header application/json
+            "~ \.json$" = {
+              extraConfig = ''
+                add_header Content-Type application/json;
+              '';
+            };
+
+            "~ \.tar.xz$" = {
+              extraConfig = ''
+                add_header Content-Type application/octet-stream;
+              '';
+            };
+
+            "~ \.tar.gz$" = {
+              extraConfig = ''
+                add_header Content-Type application/octet-stream;
+              '';
+            };
+
+            # Deny access to document root and the images folder
+            "~ ^/(images/)?$" = {
+              return = "403";
+            };
+          };
+        };
+      };
+    })
+  ];
+}
diff --git a/nixos/modules/services/networking/magic-wormhole-mailbox-server.nix b/nixos/modules/services/networking/magic-wormhole-mailbox-server.nix
new file mode 100644
index 00000000000..09d357cd2b6
--- /dev/null
+++ b/nixos/modules/services/networking/magic-wormhole-mailbox-server.nix
@@ -0,0 +1,28 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.magic-wormhole-mailbox-server;
+  dataDir = "/var/lib/magic-wormhole-mailbox-server;";
+  python = pkgs.python3.withPackages (py: [ py.magic-wormhole-mailbox-server py.twisted ]);
+in
+{
+  options.services.magic-wormhole-mailbox-server = {
+    enable = mkEnableOption "Enable Magic Wormhole Mailbox Server";
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.magic-wormhole-mailbox-server = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${python}/bin/twistd --nodaemon wormhole-mailbox";
+        WorkingDirectory = dataDir;
+        StateDirectory = baseNameOf dataDir;
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/networking/matterbridge.nix b/nixos/modules/services/networking/matterbridge.nix
new file mode 100644
index 00000000000..9186eee26ab
--- /dev/null
+++ b/nixos/modules/services/networking/matterbridge.nix
@@ -0,0 +1,120 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.matterbridge;
+
+  matterbridgeConfToml =
+    if cfg.configPath == null then
+      pkgs.writeText "matterbridge.toml" (cfg.configFile)
+    else
+      cfg.configPath;
+
+in
+
+{
+  options = {
+    services.matterbridge = {
+      enable = mkEnableOption "Matterbridge chat platform bridge";
+
+      configPath = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        example = "/etc/nixos/matterbridge.toml";
+        description = ''
+          The path to the matterbridge configuration file.
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.str;
+        example = ''
+          # WARNING: as this file contains credentials, do not use this option!
+          # It is kept only for backwards compatibility, and would cause your
+          # credentials to be in the nix-store, thus with the world-readable
+          # permission bits.
+          # Use services.matterbridge.configPath instead.
+
+          [irc]
+              [irc.libera]
+              Server="irc.libera.chat:6667"
+              Nick="matterbot"
+
+          [mattermost]
+              [mattermost.work]
+               # Do not prefix it with http:// or https://
+               Server="yourmattermostserver.domain"
+               Team="yourteam"
+               Login="yourlogin"
+               Password="yourpass"
+               PrefixMessagesWithNick=true
+
+          [[gateway]]
+          name="gateway1"
+          enable=true
+              [[gateway.inout]]
+              account="irc.libera"
+              channel="#testing"
+
+              [[gateway.inout]]
+              account="mattermost.work"
+              channel="off-topic"
+        '';
+        description = ''
+          WARNING: THIS IS INSECURE, as your password will end up in
+          <filename>/nix/store</filename>, thus publicly readable. Use
+          <literal>services.matterbridge.configPath</literal> instead.
+
+          The matterbridge configuration file in the TOML file format.
+        '';
+      };
+      user = mkOption {
+        type = types.str;
+        default = "matterbridge";
+        description = ''
+          User which runs the matterbridge service.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "matterbridge";
+        description = ''
+          Group which runs the matterbridge service.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    warnings = optional options.services.matterbridge.configFile.isDefined
+      "The option services.matterbridge.configFile is insecure and should be replaced with services.matterbridge.configPath";
+
+    users.users = optionalAttrs (cfg.user == "matterbridge")
+      { matterbridge = {
+          group = "matterbridge";
+          isSystemUser = true;
+        };
+      };
+
+    users.groups = optionalAttrs (cfg.group == "matterbridge")
+      { matterbridge = { };
+      };
+
+    systemd.services.matterbridge = {
+      description = "Matterbridge chat platform bridge";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.matterbridge}/bin/matterbridge -conf ${matterbridgeConfToml}";
+        Restart = "always";
+        RestartSec = "10";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/minidlna.nix b/nixos/modules/services/networking/minidlna.nix
new file mode 100644
index 00000000000..c860f63efa6
--- /dev/null
+++ b/nixos/modules/services/networking/minidlna.nix
@@ -0,0 +1,193 @@
+# Module for MiniDLNA, a simple DLNA server.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.minidlna;
+  port = 8200;
+in
+
+{
+  ###### interface
+  options = {
+    services.minidlna.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description =
+        ''
+          Whether to enable MiniDLNA, a simple DLNA server.  It serves
+          media files such as video and music to DLNA client devices
+          such as televisions and media players.
+        '';
+    };
+
+    services.minidlna.mediaDirs = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "/data/media" "V,/home/alice/video" ];
+      description =
+        ''
+          Directories to be scanned for media files.  The prefixes
+          <literal>A,</literal>, <literal>V,</literal> and
+          <literal>P,</literal> restrict a directory to audio, video
+          or image files.  The directories must be accessible to the
+          <literal>minidlna</literal> user account.
+        '';
+    };
+
+    services.minidlna.friendlyName = mkOption {
+      type = types.str;
+      default = "${config.networking.hostName} MiniDLNA";
+      defaultText = literalExpression ''"''${config.networking.hostName} MiniDLNA"'';
+      example = "rpi3";
+      description =
+        ''
+          Name that the DLNA server presents to clients.
+        '';
+    };
+
+    services.minidlna.rootContainer = mkOption {
+      type = types.str;
+      default = ".";
+      example = "B";
+      description =
+        ''
+          Use a different container as the root of the directory tree presented
+          to clients. The possible values are:
+          - "." - standard container
+          - "B" - "Browse Directory"
+          - "M" - "Music"
+          - "P" - "Pictures"
+          - "V" - "Video"
+          - Or, you can specify the ObjectID of your desired root container
+            (eg. 1$F for Music/Playlists)
+          If you specify "B" and the client device is audio-only then
+          "Music/Folders" will be used as root.
+         '';
+    };
+
+    services.minidlna.loglevel = mkOption {
+      type = types.str;
+      default = "warn";
+      example = "general,artwork,database,inotify,scanner,metadata,http,ssdp,tivo=warn";
+      description =
+        ''
+          Defines the type of messages that should be logged, and down to
+          which level of importance they should be considered.
+
+          The possible types are “artworkâ€, “databaseâ€, “generalâ€, “httpâ€,
+          “inotifyâ€, “metadataâ€, “scannerâ€, “ssdp†and “tivoâ€.
+
+          The levels are “offâ€, “fatalâ€, “errorâ€, “warnâ€, “info†and
+          “debugâ€, listed here in order of decreasing importance.  “offâ€
+          turns off logging messages entirely, “fatal†logs the most
+          critical messages only, and so on down to “debug†that logs every
+          single messages.
+
+          The types are comma-separated, followed by an equal sign (‘=’),
+          followed by a level that applies to the preceding types. This can
+          be repeated, separating each of these constructs with a comma.
+
+          Defaults to “general,artwork,database,inotify,scanner,metadata,
+          http,ssdp,tivo=warn†which logs every type of message at the
+          “warn†level.
+        '';
+    };
+
+    services.minidlna.announceInterval = mkOption {
+      type = types.int;
+      default = 895;
+      description =
+        ''
+          The interval between announces (in seconds).
+
+          By default miniDLNA will announce its presence on the network
+          approximately every 15 minutes.
+
+          Many people prefer shorter announce intervals (e.g. 60 seconds)
+          on their home networks, especially when DLNA clients are
+          started on demand.
+        '';
+    };
+
+    services.minidlna.config = mkOption {
+      type = types.lines;
+      description =
+      ''
+        The contents of MiniDLNA's configuration file.
+        When the service is activated, a basic template is generated
+        from the current options opened here.
+      '';
+    };
+
+    services.minidlna.extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        # Not exhaustive example
+        # Support for streaming .jpg and .mp3 files to a TiVo supporting HMO.
+        enable_tivo=no
+        # SSDP notify interval, in seconds.
+        notify_interval=10
+        # maximum number of simultaneous connections
+        # note: many clients open several simultaneous connections while
+        # streaming
+        max_connections=50
+        # set this to yes to allow symlinks that point outside user-defined
+        # media_dirs.
+        wide_links=yes
+      '';
+      description =
+      ''
+        Extra minidlna options not yet opened for configuration here
+        (strict_dlna, model_number, model_name, etc...).  This is appended
+        to the current service already provided.
+      '';
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.minidlna.config =
+      ''
+        port=${toString port}
+        friendly_name=${cfg.friendlyName}
+        db_dir=/var/cache/minidlna
+        log_level=${cfg.loglevel}
+        inotify=yes
+        root_container=${cfg.rootContainer}
+        ${concatMapStrings (dir: ''
+          media_dir=${dir}
+        '') cfg.mediaDirs}
+        notify_interval=${toString cfg.announceInterval}
+        ${cfg.extraConfig}
+      '';
+
+    users.users.minidlna = {
+      description = "MiniDLNA daemon user";
+      group = "minidlna";
+      uid = config.ids.uids.minidlna;
+    };
+
+    users.groups.minidlna.gid = config.ids.gids.minidlna;
+
+    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 ${pkgs.writeText "minidlna.conf" cfg.config}";
+          };
+      };
+  };
+}
diff --git a/nixos/modules/services/networking/miniupnpd.nix b/nixos/modules/services/networking/miniupnpd.nix
new file mode 100644
index 00000000000..c095d994854
--- /dev/null
+++ b/nixos/modules/services/networking/miniupnpd.nix
@@ -0,0 +1,79 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.miniupnpd;
+  configFile = pkgs.writeText "miniupnpd.conf" ''
+    ext_ifname=${cfg.externalInterface}
+    enable_natpmp=${if cfg.natpmp then "yes" else "no"}
+    enable_upnp=${if cfg.upnp then "yes" else "no"}
+
+    ${concatMapStrings (range: ''
+      listening_ip=${range}
+    '') cfg.internalIPs}
+
+    ${cfg.appendConfig}
+  '';
+in
+{
+  options = {
+    services.miniupnpd = {
+      enable = mkEnableOption "MiniUPnP daemon";
+
+      externalInterface = mkOption {
+        type = types.str;
+        description = ''
+          Name of the external interface.
+        '';
+      };
+
+      internalIPs = mkOption {
+        type = types.listOf types.str;
+        example = [ "192.168.1.1/24" "enp1s0" ];
+        description = ''
+          The IP address ranges to listen on.
+        '';
+      };
+
+      natpmp = mkEnableOption "NAT-PMP support";
+
+      upnp = mkOption {
+        default = true;
+        type = types.bool;
+        description = ''
+          Whether to enable UPNP support.
+        '';
+      };
+
+      appendConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Configuration lines appended to the MiniUPnP config.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.firewall.extraCommands = ''
+      ${pkgs.bash}/bin/bash -x ${pkgs.miniupnpd}/etc/miniupnpd/iptables_init.sh -i ${cfg.externalInterface}
+    '';
+
+    networking.firewall.extraStopCommands = ''
+      ${pkgs.bash}/bin/bash -x ${pkgs.miniupnpd}/etc/miniupnpd/iptables_removeall.sh -i ${cfg.externalInterface}
+    '';
+
+    systemd.services.miniupnpd = {
+      description = "MiniUPnP daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.miniupnpd}/bin/miniupnpd -f ${configFile}";
+        PIDFile = "/run/miniupnpd.pid";
+        Type = "forking";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/miredo.nix b/nixos/modules/services/networking/miredo.nix
new file mode 100644
index 00000000000..b7f657efb71
--- /dev/null
+++ b/nixos/modules/services/networking/miredo.nix
@@ -0,0 +1,92 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.miredo;
+  pidFile = "/run/miredo.pid";
+  miredoConf = pkgs.writeText "miredo.conf" ''
+    InterfaceName ${cfg.interfaceName}
+    ServerAddress ${cfg.serverAddress}
+    ${optionalString (cfg.bindAddress != null) "BindAddress ${cfg.bindAddress}"}
+    ${optionalString (cfg.bindPort != null) "BindPort ${cfg.bindPort}"}
+  '';
+in
+{
+
+  ###### interface
+
+  options = {
+
+    services.miredo = {
+
+      enable = mkEnableOption "the Miredo IPv6 tunneling service";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.miredo;
+        defaultText = literalExpression "pkgs.miredo";
+        description = ''
+          The package to use for the miredo daemon's binary.
+        '';
+      };
+
+      serverAddress = mkOption {
+        default = "teredo.remlab.net";
+        type = types.str;
+        description = ''
+          The hostname or primary IPv4 address of the Teredo server.
+          This setting is required if Miredo runs as a Teredo client.
+          "teredo.remlab.net" is an experimental service for testing only.
+          Please use another server for production and/or large scale deployments.
+        '';
+      };
+
+      interfaceName = mkOption {
+        default = "teredo";
+        type = types.str;
+        description = ''
+          Name of the network tunneling interface.
+        '';
+      };
+
+      bindAddress = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description = ''
+          Depending on the local firewall/NAT rules, you might need to force
+          Miredo to use a fixed UDP port and or IPv4 address.
+        '';
+      };
+
+      bindPort = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description = ''
+          Depending on the local firewall/NAT rules, you might need to force
+          Miredo to use a fixed UDP port and or IPv4 address.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.miredo = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      description = "Teredo IPv6 Tunneling Daemon";
+      serviceConfig = {
+        Restart = "always";
+        RestartSec = "5s";
+        ExecStart = "${cfg.package}/bin/miredo -c ${miredoConf} -p ${pidFile} -f";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/mjpg-streamer.nix b/nixos/modules/services/networking/mjpg-streamer.nix
new file mode 100644
index 00000000000..dbc35e2e71c
--- /dev/null
+++ b/nixos/modules/services/networking/mjpg-streamer.nix
@@ -0,0 +1,80 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.mjpg-streamer;
+
+in {
+
+  options = {
+
+    services.mjpg-streamer = {
+
+      enable = mkEnableOption "mjpg-streamer webcam streamer";
+
+      inputPlugin = mkOption {
+        type = types.str;
+        default = "input_uvc.so";
+        description = ''
+          Input plugin. See plugins documentation for more information.
+        '';
+      };
+
+      outputPlugin = mkOption {
+        type = types.str;
+        default = "output_http.so -w @www@ -n -p 5050";
+        description = ''
+          Output plugin. <literal>@www@</literal> is substituted for default mjpg-streamer www directory.
+          See plugins documentation for more information.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "mjpg-streamer";
+        description = "mjpg-streamer user name.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "video";
+        description = "mjpg-streamer group name.";
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users = optionalAttrs (cfg.user == "mjpg-streamer") {
+      mjpg-streamer = {
+        uid = config.ids.uids.mjpg-streamer;
+        group = cfg.group;
+      };
+    };
+
+    systemd.services.mjpg-streamer = {
+      description = "mjpg-streamer webcam streamer";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        Restart = "on-failure";
+        RestartSec = 1;
+      };
+
+      script = ''
+        IPLUGIN="${cfg.inputPlugin}"
+        OPLUGIN="${cfg.outputPlugin}"
+        OPLUGIN="''${OPLUGIN//@www@/${pkgs.mjpg-streamer}/share/mjpg-streamer/www}"
+        exec ${pkgs.mjpg-streamer}/bin/mjpg_streamer -i "$IPLUGIN" -o "$OPLUGIN"
+      '';
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/monero.nix b/nixos/modules/services/networking/monero.nix
new file mode 100644
index 00000000000..8bed89917c8
--- /dev/null
+++ b/nixos/modules/services/networking/monero.nix
@@ -0,0 +1,244 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg     = config.services.monero;
+
+  listToConf = option: list:
+    concatMapStrings (value: "${option}=${value}\n") list;
+
+  login = (cfg.rpc.user != null && cfg.rpc.password != null);
+
+  configFile = with cfg; pkgs.writeText "monero.conf" ''
+    log-file=/dev/stdout
+    data-dir=${dataDir}
+
+    ${optionalString mining.enable ''
+      start-mining=${mining.address}
+      mining-threads=${toString mining.threads}
+    ''}
+
+    rpc-bind-ip=${rpc.address}
+    rpc-bind-port=${toString rpc.port}
+    ${optionalString login ''
+      rpc-login=${rpc.user}:${rpc.password}
+    ''}
+    ${optionalString rpc.restricted ''
+      restricted-rpc=1
+    ''}
+
+    limit-rate-up=${toString limits.upload}
+    limit-rate-down=${toString limits.download}
+    max-concurrency=${toString limits.threads}
+    block-sync-size=${toString limits.syncSize}
+
+    ${listToConf "add-peer" extraNodes}
+    ${listToConf "add-priority-node" priorityNodes}
+    ${listToConf "add-exclusive-node" exclusiveNodes}
+
+    ${extraConfig}
+  '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.monero = {
+
+      enable = mkEnableOption "Monero node daemon";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/monero";
+        description = ''
+          The directory where Monero stores its data files.
+        '';
+      };
+
+      mining.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to mine monero.
+        '';
+      };
+
+      mining.address = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Monero address where to send mining rewards.
+        '';
+      };
+
+      mining.threads = mkOption {
+        type = types.addCheck types.int (x: x>=0);
+        default = 0;
+        description = ''
+          Number of threads used for mining.
+          Set to <literal>0</literal> to use all available.
+        '';
+      };
+
+      rpc.user = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          User name for RPC connections.
+        '';
+      };
+
+      rpc.password = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Password for RPC connections.
+        '';
+      };
+
+      rpc.address = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = ''
+          IP address the RPC server will bind to.
+        '';
+      };
+
+      rpc.port = mkOption {
+        type = types.port;
+        default = 18081;
+        description = ''
+          Port the RPC server will bind to.
+        '';
+      };
+
+      rpc.restricted = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to restrict RPC to view only commands.
+        '';
+      };
+
+      limits.upload = mkOption {
+        type = types.addCheck types.int (x: x>=-1);
+        default = -1;
+        description = ''
+          Limit of the upload rate in kB/s.
+          Set to <literal>-1</literal> to leave unlimited.
+        '';
+      };
+
+      limits.download = mkOption {
+        type = types.addCheck types.int (x: x>=-1);
+        default = -1;
+        description = ''
+          Limit of the download rate in kB/s.
+          Set to <literal>-1</literal> to leave unlimited.
+        '';
+      };
+
+      limits.threads = mkOption {
+        type = types.addCheck types.int (x: x>=0);
+        default = 0;
+        description = ''
+          Maximum number of threads used for a parallel job.
+          Set to <literal>0</literal> to leave unlimited.
+        '';
+      };
+
+      limits.syncSize = mkOption {
+        type = types.addCheck types.int (x: x>=0);
+        default = 0;
+        description = ''
+          Maximum number of blocks to sync at once.
+          Set to <literal>0</literal> for adaptive.
+        '';
+      };
+
+      extraNodes = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = ''
+          List of additional peer IP addresses to add to the local list.
+        '';
+      };
+
+      priorityNodes = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = ''
+          List of peer IP addresses to connect to and
+          attempt to keep the connection open.
+        '';
+      };
+
+      exclusiveNodes = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = ''
+          List of peer IP addresses to connect to *only*.
+          If given the other peer options will be ignored.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra lines to be added verbatim to monerod configuration.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.monero = {
+      isSystemUser = true;
+      group = "monero";
+      description = "Monero daemon user";
+      home = cfg.dataDir;
+      createHome = true;
+    };
+
+    users.groups.monero = { };
+
+    systemd.services.monero = {
+      description = "monero daemon";
+      after    = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        User  = "monero";
+        Group = "monero";
+        ExecStart = "${pkgs.monero-cli}/bin/monerod --config-file=${configFile} --non-interactive";
+        Restart = "always";
+        SuccessExitStatus = [ 0 1 ];
+      };
+    };
+
+    assertions = singleton {
+      assertion = cfg.mining.enable -> cfg.mining.address != "";
+      message   = ''
+       You need a Monero address to receive mining rewards:
+       specify one using option monero.mining.address.
+      '';
+    };
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+}
+
diff --git a/nixos/modules/services/networking/morty.nix b/nixos/modules/services/networking/morty.nix
new file mode 100644
index 00000000000..dff2f482ca6
--- /dev/null
+++ b/nixos/modules/services/networking/morty.nix
@@ -0,0 +1,98 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.morty;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.morty = {
+
+      enable = mkEnableOption
+        "Morty proxy server. See https://github.com/asciimoo/morty";
+
+      ipv6 = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Allow IPv6 HTTP requests?";
+      };
+
+      key = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          HMAC url validation key (hexadecimal encoded).
+          Leave blank to disable. Without validation key, anyone can
+          submit proxy requests. Leave blank to disable.
+          Generate with <literal>printf %s somevalue | openssl dgst -sha1 -hmac somekey</literal>
+        '';
+      };
+
+      timeout = mkOption {
+        type = types.int;
+        default = 2;
+        description = "Request timeout in seconds.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.morty;
+        defaultText = literalExpression "pkgs.morty";
+        description = "morty package to use.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 3000;
+        description = "Listing port";
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "The address on which the service listens";
+      };
+
+    };
+
+  };
+
+  ###### Service definition
+
+  config = mkIf config.services.morty.enable {
+
+    users.users.morty =
+      { description = "Morty user";
+        createHome = true;
+        home = "/var/lib/morty";
+        isSystemUser = true;
+        group = "morty";
+      };
+    users.groups.morty = {};
+
+    systemd.services.morty =
+      {
+        description = "Morty sanitizing proxy server.";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          User = "morty";
+          ExecStart = ''${cfg.package}/bin/morty              \
+            -listen ${cfg.listenAddress}:${toString cfg.port} \
+            ${optionalString cfg.ipv6 "-ipv6"}                \
+            ${optionalString (cfg.key != "") "-key " + cfg.key} \
+          '';
+        };
+      };
+    environment.systemPackages = [ cfg.package ];
+
+  };
+}
diff --git a/nixos/modules/services/networking/mosquitto.md b/nixos/modules/services/networking/mosquitto.md
new file mode 100644
index 00000000000..5cdb598151e
--- /dev/null
+++ b/nixos/modules/services/networking/mosquitto.md
@@ -0,0 +1,102 @@
+# Mosquitto {#module-services-mosquitto}
+
+Mosquitto is a MQTT broker often used for IoT or home automation data transport.
+
+## Quickstart {#module-services-mosquitto-quickstart}
+
+A minimal configuration for Mosquitto is
+
+```nix
+services.mosquitto = {
+  enable = true;
+  listeners = [ {
+    acl = [ "pattern readwrite #" ];
+    omitPasswordAuth = true;
+    settings.allow_anonymous = true;
+  } ];
+};
+```
+
+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.
+
+User authentication can be configured with the `users` key of listeners. A config that gives
+full read access to a user `monitor` and restricted write access to a user `service` could look
+like
+
+```nix
+services.mosquitto = {
+  enable = true;
+  listeners = [ {
+    users = {
+      monitor = {
+        acl = [ "read #" ];
+        password = "monitor";
+      };
+      service = {
+        acl = [ "write service/#" ];
+        password = "service";
+      };
+    };
+  } ];
+};
+```
+
+TLS authentication is configured by setting TLS-related options of the listener:
+
+```nix
+services.mosquitto = {
+  enable = true;
+  listeners = [ {
+    port = 8883; # port change is not required, but helpful to avoid mistakes
+    # ...
+    settings = {
+      cafile = "/path/to/mqtt.ca.pem";
+      certfile = "/path/to/mqtt.pem";
+      keyfile = "/path/to/mqtt.key";
+    };
+  } ];
+```
+
+## Configuration {#module-services-mosquitto-config}
+
+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).
+
+Almost all options of Mosquitto are available for configuration at their appropriate levels, some
+as NixOS options written in camel case, the remainders under `settings` with their exact names in
+the Mosquitto config file. The exceptions are `acl_file` (which is always set according to the
+`acl` attributes of a listener and its users) and `per_listener_settings` (which is always set to
+`true`).
+
+### Password authentication {#module-services-mosquitto-config-passwords}
+
+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 `omitPasswordAuth = true` for a listener; in this case it is necessary
+to either set `settings.allow_anonymous = true` to allow all logins, or to configure other
+authentication methods like TLS client certificates with `settings.use_identity_as_username = true`.
+
+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.
+
+### ACL format {#module-services-mosquitto-config-acl}
+
+Every listener has a Mosquitto `acl_file` attached to it. This ACL is configured via two
+attributes of the config:
+
+  * the `acl` attribute of the listener configures pattern ACL entries and topic ACL entries
+    for anonymous users. Each entry must be prefixed with `pattern` or `topic` to distinguish
+    between these two cases.
+  * the `acl` 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.
+
+The default ACL for a listener is empty, disallowing all accesses from all clients. To configure
+a completely open ACL, set `acl = [ "pattern readwrite #" ]` in the listener.
diff --git a/nixos/modules/services/networking/mosquitto.nix b/nixos/modules/services/networking/mosquitto.nix
new file mode 100644
index 00000000000..b41a2fd27be
--- /dev/null
+++ b/nixos/modules/services/networking/mosquitto.nix
@@ -0,0 +1,673 @@
+{ config, lib, pkgs, ...}:
+
+with lib;
+
+let
+  cfg = config.services.mosquitto;
+
+  # note that mosquitto config parsing is very simplistic as of may 2021.
+  # often times they'll e.g. strtok() a line, check the first two tokens, and ignore the rest.
+  # there's no escaping available either, so we have to prevent any being necessary.
+  str = types.strMatching "[^\r\n]*" // {
+    description = "single-line string";
+  };
+  path = types.addCheck types.path (p: str.check "${p}");
+  configKey = types.strMatching "[^\r\n\t ]+";
+  optionType = with types; oneOf [ str path bool int ] // {
+    description = "string, path, bool, or integer";
+  };
+  optionToString = v:
+    if isBool v then boolToString v
+    else if path.check v then "${v}"
+    else toString v;
+
+  assertKeysValid = prefix: valid: config:
+    mapAttrsToList
+      (n: _: {
+        assertion = valid ? ${n};
+        message = "Invalid config key ${prefix}.${n}.";
+      })
+      config;
+
+  formatFreeform = { prefix ? "" }: mapAttrsToList (n: v: "${prefix}${n} ${optionToString v}");
+
+  userOptions = with types; submodule {
+    options = {
+      password = mkOption {
+        type = uniq (nullOr str);
+        default = null;
+        description = ''
+          Specifies the (clear text) password for the MQTT User.
+        '';
+      };
+
+      passwordFile = mkOption {
+        type = uniq (nullOr types.path);
+        example = "/path/to/file";
+        default = null;
+        description = ''
+          Specifies the path to a file containing the
+          clear text password for the MQTT user.
+        '';
+      };
+
+      hashedPassword = mkOption {
+        type = uniq (nullOr str);
+        default = null;
+        description = ''
+          Specifies the hashed password for the MQTT User.
+          To generate hashed password install <literal>mosquitto</literal>
+          package and use <literal>mosquitto_passwd</literal>.
+        '';
+      };
+
+      hashedPasswordFile = mkOption {
+        type = uniq (nullOr types.path);
+        example = "/path/to/file";
+        default = null;
+        description = ''
+          Specifies the path to a file containing the
+          hashed password for the MQTT user.
+          To generate hashed password install <literal>mosquitto</literal>
+          package and use <literal>mosquitto_passwd</literal>.
+        '';
+      };
+
+      acl = mkOption {
+        type = listOf str;
+        example = [ "read A/B" "readwrite A/#" ];
+        default = [];
+        description = ''
+          Control client access to topics on the broker.
+        '';
+      };
+    };
+  };
+
+  userAsserts = prefix: users:
+    mapAttrsToList
+      (n: _: {
+        assertion = builtins.match "[^:\r\n]+" n != null;
+        message = "Invalid user name ${n} in ${prefix}";
+      })
+      users
+    ++ mapAttrsToList
+      (n: u: {
+        assertion = count (s: s != null) [
+          u.password u.passwordFile u.hashedPassword u.hashedPasswordFile
+        ] <= 1;
+        message = "Cannot set more than one password option for user ${n} in ${prefix}";
+      }) users;
+
+  makePasswordFile = users: path:
+    let
+      makeLines = store: file:
+        mapAttrsToList
+          (n: u: "addLine ${escapeShellArg n} ${escapeShellArg u.${store}}")
+          (filterAttrs (_: u: u.${store} != null) users)
+        ++ mapAttrsToList
+          (n: u: "addFile ${escapeShellArg n} ${escapeShellArg "${u.${file}}"}")
+          (filterAttrs (_: u: u.${file} != null) users);
+      plainLines = makeLines "password" "passwordFile";
+      hashedLines = makeLines "hashedPassword" "hashedPasswordFile";
+    in
+      pkgs.writeScript "make-mosquitto-passwd"
+        (''
+          #! ${pkgs.runtimeShell}
+
+          set -eu
+
+          file=${escapeShellArg path}
+
+          rm -f "$file"
+          touch "$file"
+
+          addLine() {
+            echo "$1:$2" >> "$file"
+          }
+          addFile() {
+            if [ $(wc -l <"$2") -gt 1 ]; then
+              echo "invalid mosquitto password file $2" >&2
+              return 1
+            fi
+            echo "$1:$(cat "$2")" >> "$file"
+          }
+        ''
+        + concatStringsSep "\n"
+          (plainLines
+           ++ optional (plainLines != []) ''
+             ${cfg.package}/bin/mosquitto_passwd -U "$file"
+           ''
+           ++ hashedLines));
+
+  makeACLFile = idx: users: supplement:
+    pkgs.writeText "mosquitto-acl-${toString idx}.conf"
+      (concatStringsSep
+        "\n"
+        (flatten [
+          supplement
+          (mapAttrsToList
+            (n: u: [ "user ${n}" ] ++ map (t: "topic ${t}") u.acl)
+            users)
+        ]));
+
+  authPluginOptions = with types; submodule {
+    options = {
+      plugin = mkOption {
+        type = path;
+        description = ''
+          Plugin path to load, should be a <literal>.so</literal> file.
+        '';
+      };
+
+      denySpecialChars = mkOption {
+        type = bool;
+        description = ''
+          Automatically disallow all clients using <literal>#</literal>
+          or <literal>+</literal> in their name/id.
+        '';
+        default = true;
+      };
+
+      options = mkOption {
+        type = attrsOf optionType;
+        description = ''
+          Options for the auth plugin. Each key turns into a <literal>auth_opt_*</literal>
+           line in the config.
+        '';
+        default = {};
+      };
+    };
+  };
+
+  authAsserts = prefix: auth:
+    mapAttrsToList
+      (n: _: {
+        assertion = configKey.check n;
+        message = "Invalid auth plugin key ${prefix}.${n}";
+      })
+      auth;
+
+  formatAuthPlugin = plugin:
+    [
+      "auth_plugin ${plugin.plugin}"
+      "auth_plugin_deny_special_chars ${optionToString plugin.denySpecialChars}"
+    ]
+    ++ formatFreeform { prefix = "auth_opt_"; } plugin.options;
+
+  freeformListenerKeys = {
+    allow_anonymous = 1;
+    allow_zero_length_clientid = 1;
+    auto_id_prefix = 1;
+    cafile = 1;
+    capath = 1;
+    certfile = 1;
+    ciphers = 1;
+    "ciphers_tls1.3" = 1;
+    crlfile = 1;
+    dhparamfile = 1;
+    http_dir = 1;
+    keyfile = 1;
+    max_connections = 1;
+    max_qos = 1;
+    max_topic_alias = 1;
+    mount_point = 1;
+    protocol = 1;
+    psk_file = 1;
+    psk_hint = 1;
+    require_certificate = 1;
+    socket_domain = 1;
+    tls_engine = 1;
+    tls_engine_kpass_sha1 = 1;
+    tls_keyform = 1;
+    tls_version = 1;
+    use_identity_as_username = 1;
+    use_subject_as_username = 1;
+    use_username_as_clientid = 1;
+  };
+
+  listenerOptions = with types; submodule {
+    options = {
+      port = mkOption {
+        type = port;
+        description = ''
+          Port to listen on. Must be set to 0 to listen on a unix domain socket.
+        '';
+        default = 1883;
+      };
+
+      address = mkOption {
+        type = nullOr str;
+        description = ''
+          Address to listen on. Listen on <literal>0.0.0.0</literal>/<literal>::</literal>
+          when unset.
+        '';
+        default = null;
+      };
+
+      authPlugins = mkOption {
+        type = listOf authPluginOptions;
+        description = ''
+          Authentication plugin to attach to this listener.
+          Refer to the <link xlink:href="https://mosquitto.org/man/mosquitto-conf-5.html">
+          mosquitto.conf documentation</link> for details on authentication plugins.
+        '';
+        default = [];
+      };
+
+      users = mkOption {
+        type = attrsOf userOptions;
+        example = { john = { password = "123456"; acl = [ "readwrite john/#" ]; }; };
+        description = ''
+          A set of users and their passwords and ACLs.
+        '';
+        default = {};
+      };
+
+      omitPasswordAuth = mkOption {
+        type = bool;
+        description = ''
+          Omits password checking, allowing anyone to log in with any user name unless
+          other mandatory authentication methods (eg TLS client certificates) are configured.
+        '';
+        default = false;
+      };
+
+      acl = mkOption {
+        type = listOf str;
+        description = ''
+          Additional ACL items to prepend to the generated ACL file.
+        '';
+        example = [ "pattern read #" "topic readwrite anon/report/#" ];
+        default = [];
+      };
+
+      settings = mkOption {
+        type = submodule {
+          freeformType = attrsOf optionType;
+        };
+        description = ''
+          Additional settings for this listener.
+        '';
+        default = {};
+      };
+    };
+  };
+
+  listenerAsserts = prefix: listener:
+    assertKeysValid prefix freeformListenerKeys listener.settings
+    ++ userAsserts prefix listener.users
+    ++ imap0
+      (i: v: authAsserts "${prefix}.authPlugins.${toString i}" v)
+      listener.authPlugins;
+
+  formatListener = idx: listener:
+    [
+      "listener ${toString listener.port} ${toString listener.address}"
+      "acl_file ${makeACLFile idx listener.users listener.acl}"
+    ]
+    ++ optional (! listener.omitPasswordAuth) "password_file ${cfg.dataDir}/passwd-${toString idx}"
+    ++ formatFreeform {} listener.settings
+    ++ concatMap formatAuthPlugin listener.authPlugins;
+
+  freeformBridgeKeys = {
+    bridge_alpn = 1;
+    bridge_attempt_unsubscribe = 1;
+    bridge_bind_address = 1;
+    bridge_cafile = 1;
+    bridge_capath = 1;
+    bridge_certfile = 1;
+    bridge_identity = 1;
+    bridge_insecure = 1;
+    bridge_keyfile = 1;
+    bridge_max_packet_size = 1;
+    bridge_outgoing_retain = 1;
+    bridge_protocol_version = 1;
+    bridge_psk = 1;
+    bridge_require_ocsp = 1;
+    bridge_tls_version = 1;
+    cleansession = 1;
+    idle_timeout = 1;
+    keepalive_interval = 1;
+    local_cleansession = 1;
+    local_clientid = 1;
+    local_password = 1;
+    local_username = 1;
+    notification_topic = 1;
+    notifications = 1;
+    notifications_local_only = 1;
+    remote_clientid = 1;
+    remote_password = 1;
+    remote_username = 1;
+    restart_timeout = 1;
+    round_robin = 1;
+    start_type = 1;
+    threshold = 1;
+    try_private = 1;
+  };
+
+  bridgeOptions = with types; submodule {
+    options = {
+      addresses = mkOption {
+        type = listOf (submodule {
+          options = {
+            address = mkOption {
+              type = str;
+              description = ''
+                Address of the remote MQTT broker.
+              '';
+            };
+
+            port = mkOption {
+              type = port;
+              description = ''
+                Port of the remote MQTT broker.
+              '';
+              default = 1883;
+            };
+          };
+        });
+        default = [];
+        description = ''
+          Remote endpoints for the bridge.
+        '';
+      };
+
+      topics = mkOption {
+        type = listOf str;
+        description = ''
+          Topic patterns to be shared between the two brokers.
+          Refer to the <link xlink:href="https://mosquitto.org/man/mosquitto-conf-5.html">
+          mosquitto.conf documentation</link> for details on the format.
+        '';
+        default = [];
+        example = [ "# both 2 local/topic/ remote/topic/" ];
+      };
+
+      settings = mkOption {
+        type = submodule {
+          freeformType = attrsOf optionType;
+        };
+        description = ''
+          Additional settings for this bridge.
+        '';
+        default = {};
+      };
+    };
+  };
+
+  bridgeAsserts = prefix: bridge:
+    assertKeysValid prefix freeformBridgeKeys bridge.settings
+    ++ [ {
+      assertion = length bridge.addresses > 0;
+      message = "Bridge ${prefix} needs remote broker addresses";
+    } ];
+
+  formatBridge = name: bridge:
+    [
+      "connection ${name}"
+      "addresses ${concatMapStringsSep " " (a: "${a.address}:${toString a.port}") bridge.addresses}"
+    ]
+    ++ map (t: "topic ${t}") bridge.topics
+    ++ formatFreeform {} bridge.settings;
+
+  freeformGlobalKeys = {
+    allow_duplicate_messages = 1;
+    autosave_interval = 1;
+    autosave_on_changes = 1;
+    check_retain_source = 1;
+    connection_messages = 1;
+    log_facility = 1;
+    log_timestamp = 1;
+    log_timestamp_format = 1;
+    max_inflight_bytes = 1;
+    max_inflight_messages = 1;
+    max_keepalive = 1;
+    max_packet_size = 1;
+    max_queued_bytes = 1;
+    max_queued_messages = 1;
+    memory_limit = 1;
+    message_size_limit = 1;
+    persistence_file = 1;
+    persistence_location = 1;
+    persistent_client_expiration = 1;
+    pid_file = 1;
+    queue_qos0_messages = 1;
+    retain_available = 1;
+    set_tcp_nodelay = 1;
+    sys_interval = 1;
+    upgrade_outgoing_qos = 1;
+    websockets_headers_size = 1;
+    websockets_log_level = 1;
+  };
+
+  globalOptions = with types; {
+    enable = mkEnableOption "the MQTT Mosquitto broker";
+
+    package = mkOption {
+      type = package;
+      default = pkgs.mosquitto;
+      defaultText = literalExpression "pkgs.mosquitto";
+      description = ''
+        Mosquitto package to use.
+      '';
+    };
+
+    bridges = mkOption {
+      type = attrsOf bridgeOptions;
+      default = {};
+      description = ''
+        Bridges to build to other MQTT brokers.
+      '';
+    };
+
+    listeners = mkOption {
+      type = listOf listenerOptions;
+      default = {};
+      description = ''
+        Listeners to configure on this broker.
+      '';
+    };
+
+    includeDirs = mkOption {
+      type = listOf path;
+      description = ''
+        Directories to be scanned for further config files to include.
+        Directories will processed in the order given,
+        <literal>*.conf</literal> files in the directory will be
+        read in case-sensistive alphabetical order.
+      '';
+      default = [];
+    };
+
+    logDest = mkOption {
+      type = listOf (either path (enum [ "stdout" "stderr" "syslog" "topic" "dlt" ]));
+      description = ''
+        Destinations to send log messages to.
+      '';
+      default = [ "stderr" ];
+    };
+
+    logType = mkOption {
+      type = listOf (enum [ "debug" "error" "warning" "notice" "information"
+                            "subscribe" "unsubscribe" "websockets" "none" "all" ]);
+      description = ''
+        Types of messages to log.
+      '';
+      default = [];
+    };
+
+    persistence = mkOption {
+      type = bool;
+      description = ''
+        Enable persistent storage of subscriptions and messages.
+      '';
+      default = true;
+    };
+
+    dataDir = mkOption {
+      default = "/var/lib/mosquitto";
+      type = types.path;
+      description = ''
+        The data directory.
+      '';
+    };
+
+    settings = mkOption {
+      type = submodule {
+        freeformType = attrsOf optionType;
+      };
+      description = ''
+        Global configuration options for the mosquitto broker.
+      '';
+      default = {};
+    };
+  };
+
+  globalAsserts = prefix: cfg:
+    flatten [
+      (assertKeysValid prefix freeformGlobalKeys cfg.settings)
+      (imap0 (n: l: listenerAsserts "${prefix}.listener.${toString n}" l) cfg.listeners)
+      (mapAttrsToList (n: b: bridgeAsserts "${prefix}.bridge.${n}" b) cfg.bridges)
+    ];
+
+  formatGlobal = cfg:
+    [
+      "per_listener_settings true"
+      "persistence ${optionToString cfg.persistence}"
+    ]
+    ++ map
+      (d: if path.check d then "log_dest file ${d}" else "log_dest ${d}")
+      cfg.logDest
+    ++ map (t: "log_type ${t}") cfg.logType
+    ++ formatFreeform {} cfg.settings
+    ++ concatLists (imap0 formatListener cfg.listeners)
+    ++ concatLists (mapAttrsToList formatBridge cfg.bridges)
+    ++ map (d: "include_dir ${d}") cfg.includeDirs;
+
+  configFile = pkgs.writeText "mosquitto.conf"
+    (concatStringsSep "\n" (formatGlobal cfg));
+
+in
+
+{
+
+  ###### Interface
+
+  options.services.mosquitto = globalOptions;
+
+  ###### Implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = globalAsserts "services.mosquitto" cfg;
+
+    systemd.services.mosquitto = {
+      description = "Mosquitto MQTT Broker Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      serviceConfig = {
+        Type = "notify";
+        NotifyAccess = "main";
+        User = "mosquitto";
+        Group = "mosquitto";
+        RuntimeDirectory = "mosquitto";
+        WorkingDirectory = cfg.dataDir;
+        Restart = "on-failure";
+        ExecStart = "${cfg.package}/bin/mosquitto -c ${configFile}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+
+        # Hardening
+        CapabilityBoundingSet = "";
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        ProtectSystem = "strict";
+        ReadWritePaths = [
+          cfg.dataDir
+          "/tmp"  # mosquitto_passwd creates files in /tmp before moving them
+        ] ++ filter path.check cfg.logDest;
+        ReadOnlyPaths =
+          map (p: "${p}")
+            (cfg.includeDirs
+             ++ filter
+               (v: v != null)
+               (flatten [
+                 (map
+                   (l: [
+                     (l.settings.psk_file or null)
+                     (l.settings.http_dir or null)
+                     (l.settings.cafile or null)
+                     (l.settings.capath or null)
+                     (l.settings.certfile or null)
+                     (l.settings.crlfile or null)
+                     (l.settings.dhparamfile or null)
+                     (l.settings.keyfile or null)
+                   ])
+                   cfg.listeners)
+                 (mapAttrsToList
+                   (_: b: [
+                     (b.settings.bridge_cafile or null)
+                     (b.settings.bridge_capath or null)
+                     (b.settings.bridge_certfile or null)
+                     (b.settings.bridge_keyfile or null)
+                   ])
+                   cfg.bridges)
+               ]));
+        RemoveIPC = true;
+        RestrictAddressFamilies = [
+          "AF_UNIX"  # for sd_notify() call
+          "AF_INET"
+          "AF_INET6"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+          "~@resources"
+        ];
+        UMask = "0077";
+      };
+      preStart =
+        concatStringsSep
+          "\n"
+          (imap0
+            (idx: listener: makePasswordFile listener.users "${cfg.dataDir}/passwd-${toString idx}")
+            cfg.listeners);
+    };
+
+    users.users.mosquitto = {
+      description = "Mosquitto MQTT Broker Daemon owner";
+      group = "mosquitto";
+      uid = config.ids.uids.mosquitto;
+      home = cfg.dataDir;
+      createHome = true;
+    };
+
+    users.groups.mosquitto.gid = config.ids.gids.mosquitto;
+
+  };
+
+  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;
+  };
+}
diff --git a/nixos/modules/services/networking/mosquitto.xml b/nixos/modules/services/networking/mosquitto.xml
new file mode 100644
index 00000000000..d16ab28c026
--- /dev/null
+++ b/nixos/modules/services/networking/mosquitto.xml
@@ -0,0 +1,147 @@
+<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/mstpd.nix b/nixos/modules/services/networking/mstpd.nix
new file mode 100644
index 00000000000..bd71010ce54
--- /dev/null
+++ b/nixos/modules/services/networking/mstpd.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.mstpd;
+in
+with lib;
+{
+  options.services.mstpd = {
+
+    enable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to enable the multiple spanning tree protocol daemon.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.mstpd ];
+
+    systemd.services.mstpd = {
+      description = "Multiple Spanning Tree Protocol Daemon";
+      wantedBy = [ "network.target" ];
+      unitConfig.ConditionCapability = "CAP_NET_ADMIN";
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "@${pkgs.mstpd}/bin/mstpd mstpd";
+        PIDFile = "/run/mstpd.pid";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/mtprotoproxy.nix b/nixos/modules/services/networking/mtprotoproxy.nix
new file mode 100644
index 00000000000..d896f227b82
--- /dev/null
+++ b/nixos/modules/services/networking/mtprotoproxy.nix
@@ -0,0 +1,110 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.mtprotoproxy;
+
+  configOpts = {
+    PORT = cfg.port;
+    USERS = cfg.users;
+    SECURE_ONLY = cfg.secureOnly;
+  } // lib.optionalAttrs (cfg.adTag != null) { AD_TAG = cfg.adTag; }
+    // cfg.extraConfig;
+
+  convertOption = opt:
+    if isString opt || isInt opt then
+      builtins.toJSON opt
+    else if isBool opt then
+      if opt then "True" else "False"
+    else if isList opt then
+      "[" + concatMapStringsSep "," convertOption opt + "]"
+    else if isAttrs opt then
+      "{" + concatStringsSep "," (mapAttrsToList (name: opt: "${builtins.toJSON name}: ${convertOption opt}") opt) + "}"
+    else
+      throw "Invalid option type";
+
+  configFile = pkgs.writeText "config.py" (concatStringsSep "\n" (mapAttrsToList (name: opt: "${name} = ${convertOption opt}") configOpts));
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.mtprotoproxy = {
+
+      enable = mkEnableOption "mtprotoproxy";
+
+      port = mkOption {
+        type = types.int;
+        default = 3256;
+        description = ''
+          TCP port to accept mtproto connections on.
+        '';
+      };
+
+      users = mkOption {
+        type = types.attrsOf types.str;
+        example = {
+          tg = "00000000000000000000000000000000";
+          tg2 = "0123456789abcdef0123456789abcdef";
+        };
+        description = ''
+          Allowed users and their secrets. A secret is a 32 characters long hex string.
+        '';
+      };
+
+      secureOnly = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Don't allow users to connect in non-secure mode (without random padding).
+        '';
+      };
+
+      adTag = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        # Taken from mtproxyproto's repo.
+        example = "3c09c680b76ee91a4c25ad51f742267d";
+        description = ''
+          Tag for advertising that can be obtained from @MTProxybot.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        example = {
+          STATS_PRINT_PERIOD = 600;
+        };
+        description = ''
+          Extra configuration options for mtprotoproxy.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.mtprotoproxy = {
+      description = "MTProto Proxy Daemon";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.mtprotoproxy}/bin/mtprotoproxy ${configFile}";
+        DynamicUser = true;
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/mtr-exporter.nix b/nixos/modules/services/networking/mtr-exporter.nix
new file mode 100644
index 00000000000..ca261074ebd
--- /dev/null
+++ b/nixos/modules/services/networking/mtr-exporter.nix
@@ -0,0 +1,87 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib)
+    maintainers types mkEnableOption mkOption mkIf
+    literalExpression escapeShellArg escapeShellArgs;
+  cfg = config.services.mtr-exporter;
+in {
+  options = {
+    services = {
+      mtr-exporter = {
+        enable = mkEnableOption "a Prometheus exporter for MTR";
+
+        target = mkOption {
+          type = types.str;
+          example = "example.org";
+          description = "Target to check using MTR.";
+        };
+
+        interval = mkOption {
+          type = types.int;
+          default = 60;
+          description = "Interval between MTR checks in seconds.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8080;
+          description = "Listen port for MTR exporter.";
+        };
+
+        address = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "Listen address for MTR exporter.";
+        };
+
+        mtrFlags = mkOption {
+          type = with types; listOf str;
+          default = [];
+          example = ["-G1"];
+          description = "Additional flags to pass to MTR.";
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.mtr-exporter = {
+      script = ''
+        exec ${pkgs.mtr-exporter}/bin/mtr-exporter \
+          -mtr ${pkgs.mtr}/bin/mtr \
+          -schedule '@every ${toString cfg.interval}s' \
+          -bind ${escapeShellArg cfg.address}:${toString cfg.port} \
+          -- \
+          ${escapeShellArgs (cfg.mtrFlags ++ [ cfg.target ])}
+      '';
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "network.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        Restart = "on-failure";
+        # Hardening
+        CapabilityBoundingSet = [ "" ];
+        DynamicUser = true;
+        LockPersonality = true;
+        ProcSubset = "pid";
+        PrivateDevices = true;
+        PrivateUsers = true;
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ jakubgs ];
+}
diff --git a/nixos/modules/services/networking/mullvad-vpn.nix b/nixos/modules/services/networking/mullvad-vpn.nix
new file mode 100644
index 00000000000..9ec1ddc929e
--- /dev/null
+++ b/nixos/modules/services/networking/mullvad-vpn.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.mullvad-vpn;
+in
+with lib;
+{
+  options.services.mullvad-vpn.enable = mkOption {
+    type = types.bool;
+    default = false;
+    description = ''
+      This option enables Mullvad VPN daemon.
+      This sets <option>networking.firewall.checkReversePath</option> to "loose", which might be undesirable for security.
+    '';
+  };
+
+  config = mkIf cfg.enable {
+    boot.kernelModules = [ "tun" ];
+
+    # mullvad-daemon writes to /etc/iproute2/rt_tables
+    networking.iproute2.enable = true;
+
+    # See https://github.com/NixOS/nixpkgs/issues/113589
+    networking.firewall.checkReversePath = "loose";
+
+    systemd.services.mullvad-daemon = {
+      description = "Mullvad VPN daemon";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network.target" ];
+      after = [
+        "network-online.target"
+        "NetworkManager.service"
+        "systemd-resolved.service"
+      ];
+      path = [
+        pkgs.iproute2
+        # Needed for ping
+        "/run/wrappers"
+      ];
+      startLimitBurst = 5;
+      startLimitIntervalSec = 20;
+      serviceConfig = {
+        ExecStart = "${pkgs.mullvad-vpn}/bin/mullvad-daemon -v --disable-stdout-timestamps";
+        Restart = "always";
+        RestartSec = 1;
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ ymarkus ];
+}
diff --git a/nixos/modules/services/networking/multipath.nix b/nixos/modules/services/networking/multipath.nix
new file mode 100644
index 00000000000..1a44184ff6d
--- /dev/null
+++ b/nixos/modules/services/networking/multipath.nix
@@ -0,0 +1,557 @@
+{ config, lib, pkgs, ... }: with lib;
+
+# See http://christophe.varoqui.free.fr/usage.html and
+# https://github.com/opensvc/multipath-tools/blob/master/multipath/multipath.conf.5
+
+let
+  cfg = config.services.multipath;
+
+  indentLines = n: str: concatStringsSep "\n" (
+    map (line: "${fixedWidthString n " " " "}${line}") (
+      filter ( x: x != "" ) ( splitString "\n" str )
+    )
+  );
+
+  addCheckDesc = desc: elemType: check: types.addCheck elemType check
+    // { description = "${elemType.description} (with check: ${desc})"; };
+  hexChars = stringToCharacters "0123456789abcdef";
+  isHexString = s: all (c: elem c hexChars) (stringToCharacters (toLower s));
+  hexStr = addCheckDesc "hexadecimal string" types.str isHexString;
+
+in {
+
+  options.services.multipath = with types; {
+
+    enable = mkEnableOption "the device mapper multipath (DM-MP) daemon";
+
+    package = mkOption {
+      type = package;
+      description = "multipath-tools package to use";
+      default = pkgs.multipath-tools;
+      defaultText = "pkgs.multipath-tools";
+    };
+
+    devices = mkOption {
+      default = [ ];
+      example = literalExpression ''
+        [
+          {
+            vendor = "\"COMPELNT\"";
+            product = "\"Compellent Vol\"";
+            path_checker = "tur";
+            no_path_retry = "queue";
+            max_sectors_kb = 256;
+          }, ...
+        ]
+      '';
+      description = ''
+        This option allows you to define arrays for use in multipath
+        groups.
+      '';
+      type = listOf (submodule {
+        options = {
+
+          vendor = mkOption {
+            type = str;
+            example = "COMPELNT";
+            description = "Regular expression to match the vendor name";
+          };
+
+          product = mkOption {
+            type = str;
+            example = "Compellent Vol";
+            description = "Regular expression to match the product name";
+          };
+
+          revision = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "Regular expression to match the product revision";
+          };
+
+          product_blacklist = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "Products with the given vendor matching this string are blacklisted";
+          };
+
+          alias_prefix = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "The user_friendly_names prefix to use for this device type, instead of the default mpath";
+          };
+
+          vpd_vendor = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "The vendor specific vpd page information, using the vpd page abbreviation";
+          };
+
+          hardware_handler = mkOption {
+            type = nullOr (enum [ "emc" "rdac" "hp_sw" "alua" "ana" ]);
+            default = null;
+            description = "The hardware handler to use for this device type";
+          };
+
+          # Optional arguments
+          path_grouping_policy = mkOption {
+            type = nullOr (enum [ "failover" "multibus" "group_by_serial" "group_by_prio" "group_by_node_name" ]);
+            default = null; # real default: "failover"
+            description = "The default path grouping policy to apply to unspecified multipaths";
+          };
+
+          uid_attribute = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "The udev attribute providing a unique path identifier (WWID)";
+          };
+
+          getuid_callout = mkOption {
+            type = nullOr str;
+            default = null;
+            description = ''
+              (Superseded by uid_attribute) The default program and args to callout
+              to obtain a unique path identifier. Should be specified with an absolute path.
+            '';
+          };
+
+          path_selector = mkOption {
+            type = nullOr (enum [
+              ''"round-robin 0"''
+              ''"queue-length 0"''
+              ''"service-time 0"''
+              ''"historical-service-time 0"''
+            ]);
+            default = null; # real default: "service-time 0"
+            description = "The default path selector algorithm to use; they are offered by the kernel multipath target";
+          };
+
+          path_checker = mkOption {
+            type = enum [ "readsector0" "tur" "emc_clariion" "hp_sw" "rdac" "directio" "cciss_tur" "none" ];
+            default = "tur";
+            description = "The default method used to determine the paths state";
+          };
+
+          prio = mkOption {
+            type = nullOr (enum [
+              "none" "const" "sysfs" "emc" "alua" "ontap" "rdac" "hp_sw" "hds"
+              "random" "weightedpath" "path_latency" "ana" "datacore" "iet"
+            ]);
+            default = null; # real default: "const"
+            description = "The name of the path priority routine";
+          };
+
+          prio_args = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "Arguments to pass to to the prio function";
+          };
+
+          features = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "Specify any device-mapper features to be used";
+          };
+
+          failback = mkOption {
+            type = nullOr str;
+            default = null; # real default: "manual"
+            description = "Tell multipathd how to manage path group failback. Quote integers as strings";
+          };
+
+          rr_weight = mkOption {
+            type = nullOr (enum [ "priorities" "uniform" ]);
+            default = null; # real default: "uniform"
+            description = ''
+              If set to priorities the multipath configurator will assign path weights
+              as "path prio * rr_min_io".
+            '';
+          };
+
+          no_path_retry = mkOption {
+            type = nullOr str;
+            default = null; # real default: "fail"
+            description = "Specify what to do when all paths are down. Quote integers as strings";
+          };
+
+          rr_min_io = mkOption {
+            type = nullOr int;
+            default = null; # real default: 1000
+            description = ''
+              Number of I/O requests to route to a path before switching to the next in the
+              same path group. This is only for Block I/O (BIO) based multipath and
+              only apply to round-robin path_selector.
+            '';
+          };
+
+          rr_min_io_rq = mkOption {
+            type = nullOr int;
+            default = null; # real default: 1
+            description = ''
+              Number of I/O requests to route to a path before switching to the next in the
+              same path group. This is only for Request based multipath and
+              only apply to round-robin path_selector.
+            '';
+          };
+
+          fast_io_fail_tmo = mkOption {
+            type = nullOr str;
+            default = null; # real default: 5
+            description = ''
+              Specify the number of seconds the SCSI layer will wait after a problem has been
+              detected on a FC remote port before failing I/O to devices on that remote port.
+              This should be smaller than dev_loss_tmo. Setting this to "off" will disable
+              the timeout. Quote integers as strings.
+            '';
+          };
+
+          dev_loss_tmo = mkOption {
+            type = nullOr str;
+            default = null; # real default: 600
+            description = ''
+              Specify the number of seconds the SCSI layer will wait after a problem has
+              been detected on a FC remote port before removing it from the system. This
+              can be set to "infinity" which sets it to the max value of 2147483647
+              seconds, or 68 years. It will be automatically adjusted to the overall
+              retry interval no_path_retry * polling_interval
+              if a number of retries is given with no_path_retry and the
+              overall retry interval is longer than the specified dev_loss_tmo value.
+              The Linux kernel will cap this value to 600 if fast_io_fail_tmo
+              is not set.
+            '';
+          };
+
+          flush_on_last_del = mkOption {
+            type = nullOr (enum [ "yes" "no" ]);
+            default = null; # real default: "no"
+            description = ''
+              If set to "yes" multipathd will disable queueing when the last path to a
+              device has been deleted.
+            '';
+          };
+
+          user_friendly_names = mkOption {
+            type = nullOr (enum [ "yes" "no" ]);
+            default = null; # real default: "no"
+            description = ''
+              If set to "yes", using the bindings file /etc/multipath/bindings
+              to assign a persistent and unique alias to the multipath, in the
+              form of mpath. If set to "no" use the WWID as the alias. In either
+              case this be will be overridden by any specific aliases in the
+              multipaths section.
+            '';
+          };
+
+          detect_prio = mkOption {
+            type = nullOr (enum [ "yes" "no" ]);
+            default = null; # real default: "yes"
+            description = ''
+              If set to "yes", multipath will try to detect if the device supports
+              SCSI-3 ALUA. If so, the device will automatically use the sysfs
+              prioritizer if the required sysf attributes access_state and
+              preferred_path are supported, or the alua prioritizer if not. If set
+              to "no", the prioritizer will be selected as usual.
+            '';
+          };
+
+          detect_checker = mkOption {
+            type = nullOr (enum [ "yes" "no" ]);
+            default = null; # real default: "yes"
+            description = ''
+              If set to "yes", multipath will try to detect if the device supports
+              SCSI-3 ALUA. If so, the device will automatically use the tur checker.
+              If set to "no", the checker will be selected as usual.
+            '';
+          };
+
+          deferred_remove = mkOption {
+            type = nullOr (enum [ "yes" "no" ]);
+            default = null; # real default: "no"
+            description = ''
+              If set to "yes", multipathd will do a deferred remove instead of a
+              regular remove when the last path device has been deleted. This means
+              that if the multipath device is still in use, it will be freed when
+              the last user closes it. If path is added to the multipath device
+              before the last user closes it, the deferred remove will be canceled.
+            '';
+          };
+
+          san_path_err_threshold = mkOption {
+            type = nullOr str;
+            default = null;
+            description = ''
+              If set to a value greater than 0, multipathd will watch paths and check
+              how many times a path has been failed due to errors.If the number of
+              failures on a particular path is greater then the san_path_err_threshold,
+              then the path will not reinstate till san_path_err_recovery_time. These
+              path failures should occur within a san_path_err_forget_rate checks, if
+              not we will consider the path is good enough to reinstantate.
+            '';
+          };
+
+          san_path_err_forget_rate = mkOption {
+            type = nullOr str;
+            default = null;
+            description = ''
+              If set to a value greater than 0, multipathd will check whether the path
+              failures has exceeded the san_path_err_threshold within this many checks
+              i.e san_path_err_forget_rate. If so we will not reinstante the path till
+              san_path_err_recovery_time.
+            '';
+          };
+
+          san_path_err_recovery_time = mkOption {
+            type = nullOr str;
+            default = null;
+            description = ''
+              If set to a value greater than 0, multipathd will make sure that when
+              path failures has exceeded the san_path_err_threshold within
+              san_path_err_forget_rate then the path will be placed in failed state
+              for san_path_err_recovery_time duration. Once san_path_err_recovery_time
+              has timeout we will reinstante the failed path. san_path_err_recovery_time
+              value should be in secs.
+            '';
+          };
+
+          marginal_path_err_sample_time = mkOption {
+            type = nullOr int;
+            default = null;
+            description = "One of the four parameters of supporting path check based on accounting IO error such as intermittent error";
+          };
+
+          marginal_path_err_rate_threshold = mkOption {
+            type = nullOr int;
+            default = null;
+            description = "The error rate threshold as a permillage (1/1000)";
+          };
+
+          marginal_path_err_recheck_gap_time = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "One of the four parameters of supporting path check based on accounting IO error such as intermittent error";
+          };
+
+          marginal_path_double_failed_time = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "One of the four parameters of supporting path check based on accounting IO error such as intermittent error";
+          };
+
+          delay_watch_checks = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "This option is deprecated, and mapped to san_path_err_forget_rate";
+          };
+
+          delay_wait_checks = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "This option is deprecated, and mapped to san_path_err_recovery_time";
+          };
+
+          skip_kpartx = mkOption {
+            type = nullOr (enum [ "yes" "no" ]);
+            default = null; # real default: "no"
+            description = "If set to yes, kpartx will not automatically create partitions on the device";
+          };
+
+          max_sectors_kb = mkOption {
+            type = nullOr int;
+            default = null;
+            description = "Sets the max_sectors_kb device parameter on all path devices and the multipath device to the specified value";
+          };
+
+          ghost_delay = mkOption {
+            type = nullOr int;
+            default = null;
+            description = "Sets the number of seconds that multipath will wait after creating a device with only ghost paths before marking it ready for use in systemd";
+          };
+
+          all_tg_pt = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "Set the 'all targets ports' flag when registering keys with mpathpersist";
+          };
+
+        };
+      });
+    };
+
+    defaults = mkOption {
+      type = nullOr str;
+      default = null;
+      description = ''
+        This section defines default values for attributes which are used
+        whenever no values are given in the appropriate device or multipath
+        sections.
+      '';
+    };
+
+    blacklist = mkOption {
+      type = nullOr str;
+      default = null;
+      description = ''
+        This section defines which devices should be excluded from the
+        multipath topology discovery.
+      '';
+    };
+
+    blacklist_exceptions = mkOption {
+      type = nullOr str;
+      default = null;
+      description = ''
+        This section defines which devices should be included in the
+        multipath topology discovery, despite being listed in the
+        blacklist section.
+      '';
+    };
+
+    overrides = mkOption {
+      type = nullOr str;
+      default = null;
+      description = ''
+        This section defines values for attributes that should override the
+        device-specific settings for all devices.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = nullOr str;
+      default = null;
+      description = "Lines to append to default multipath.conf";
+    };
+
+    extraConfigFile = mkOption {
+      type = nullOr str;
+      default = null;
+      description = "Append an additional file's contents to /etc/multipath.conf";
+    };
+
+    pathGroups = mkOption {
+      example = literalExpression ''
+        [
+          {
+            wwid = "360080e500043b35c0123456789abcdef";
+            alias = 10001234;
+            array = "bigarray.example.com";
+            fsType = "zfs"; # optional
+            options = "ro"; # optional
+          }, ...
+        ]
+      '';
+      description = ''
+        This option allows you to define multipath groups as described
+        in http://christophe.varoqui.free.fr/usage.html.
+      '';
+      type = listOf (submodule {
+        options = {
+
+          alias = mkOption {
+            type = int;
+            example = 1001234;
+            description = "The name of the multipath device";
+          };
+
+          wwid = mkOption {
+            type = hexStr;
+            example = "360080e500043b35c0123456789abcdef";
+            description = "The identifier for the multipath device";
+          };
+
+          array = mkOption {
+            type = str;
+            default = null;
+            example = "bigarray.example.com";
+            description = "The DNS name of the storage array";
+          };
+
+          fsType = mkOption {
+            type = nullOr str;
+            default = null;
+            example = "zfs";
+            description = "Type of the filesystem";
+          };
+
+          options = mkOption {
+            type = nullOr str;
+            default = null;
+            example = "ro";
+            description = "Options used to mount the file system";
+          };
+
+        };
+      });
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."multipath.conf".text =
+      let
+        inherit (cfg) defaults blacklist blacklist_exceptions overrides;
+
+        mkDeviceBlock = cfg: let
+          nonNullCfg = lib.filterAttrs (k: v: v != null) cfg;
+          attrs = lib.mapAttrsToList (name: value: "  ${name} ${toString value}") nonNullCfg;
+        in ''
+          device {
+          ${lib.concatStringsSep "\n" attrs}
+          }
+        '';
+        devices = lib.concatMapStringsSep "\n" mkDeviceBlock cfg.devices;
+
+        mkMultipathBlock = m: ''
+          multipath {
+            wwid ${m.wwid}
+            alias ${toString m.alias}
+          }
+        '';
+        multipaths = lib.concatMapStringsSep "\n" mkMultipathBlock cfg.pathGroups;
+
+      in ''
+        devices {
+        ${indentLines 2 devices}
+        }
+
+        ${optionalString (!isNull defaults) ''
+          defaults {
+          ${indentLines 2 defaults}
+            multipath_dir ${cfg.package}/lib/multipath
+          }
+        ''}
+        ${optionalString (!isNull blacklist) ''
+          blacklist {
+          ${indentLines 2 blacklist}
+          }
+        ''}
+        ${optionalString (!isNull blacklist_exceptions) ''
+          blacklist_exceptions {
+          ${indentLines 2 blacklist_exceptions}
+          }
+        ''}
+        ${optionalString (!isNull overrides) ''
+          overrides {
+          ${indentLines 2 overrides}
+          }
+        ''}
+        multipaths {
+        ${indentLines 2 multipaths}
+        }
+      '';
+
+    systemd.packages = [ cfg.package ];
+
+    environment.systemPackages = [ cfg.package ];
+    boot.kernelModules = [ "dm-multipath" "dm-service-time" ];
+
+    # We do not have systemd in stage-1 boot so must invoke `multipathd`
+    # with the `-1` argument which disables systemd calls. Invoke `multipath`
+    # to display the multipath mappings in the output of `journalctl -b`.
+    boot.initrd.kernelModules = [ "dm-multipath" "dm-service-time" ];
+    boot.initrd.postDeviceCommands = ''
+      modprobe -a dm-multipath dm-service-time
+      multipathd -s
+      (set -x && sleep 1 && multipath -ll)
+    '';
+  };
+}
diff --git a/nixos/modules/services/networking/murmur.nix b/nixos/modules/services/networking/murmur.nix
new file mode 100644
index 00000000000..06ec04dbbf1
--- /dev/null
+++ b/nixos/modules/services/networking/murmur.nix
@@ -0,0 +1,318 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.murmur;
+  forking = cfg.logFile != null;
+  configFile = pkgs.writeText "murmurd.ini" ''
+    database=/var/lib/murmur/murmur.sqlite
+    dbDriver=QSQLITE
+
+    autobanAttempts=${toString cfg.autobanAttempts}
+    autobanTimeframe=${toString cfg.autobanTimeframe}
+    autobanTime=${toString cfg.autobanTime}
+
+    logfile=${optionalString (cfg.logFile != null) cfg.logFile}
+    ${optionalString forking "pidfile=/run/murmur/murmurd.pid"}
+
+    welcometext="${cfg.welcometext}"
+    port=${toString cfg.port}
+
+    ${if cfg.hostName == "" then "" else "host="+cfg.hostName}
+    ${if cfg.password == "" then "" else "serverpassword="+cfg.password}
+
+    bandwidth=${toString cfg.bandwidth}
+    users=${toString cfg.users}
+
+    textmessagelength=${toString cfg.textMsgLength}
+    imagemessagelength=${toString cfg.imgMsgLength}
+    allowhtml=${boolToString cfg.allowHtml}
+    logdays=${toString cfg.logDays}
+    bonjour=${boolToString cfg.bonjour}
+    sendversion=${boolToString cfg.sendVersion}
+
+    ${if cfg.registerName     == "" then "" else "registerName="+cfg.registerName}
+    ${if cfg.registerPassword == "" then "" else "registerPassword="+cfg.registerPassword}
+    ${if cfg.registerUrl      == "" then "" else "registerUrl="+cfg.registerUrl}
+    ${if cfg.registerHostname == "" then "" else "registerHostname="+cfg.registerHostname}
+
+    certrequired=${boolToString cfg.clientCertRequired}
+    ${if cfg.sslCert == "" then "" else "sslCert="+cfg.sslCert}
+    ${if cfg.sslKey  == "" then "" else "sslKey="+cfg.sslKey}
+    ${if cfg.sslCa   == "" then "" else "sslCA="+cfg.sslCa}
+
+    ${cfg.extraConfig}
+  '';
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "murmur" "welcome" ] [ "services" "murmur" "welcometext" ])
+    (mkRemovedOptionModule [ "services" "murmur" "pidfile" ] "Hardcoded to /run/murmur/murmurd.pid now")
+  ];
+
+  options = {
+    services.murmur = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "If enabled, start the Murmur Mumble server.";
+      };
+
+      autobanAttempts = mkOption {
+        type = types.int;
+        default = 10;
+        description = ''
+          Number of attempts a client is allowed to make in
+          <literal>autobanTimeframe</literal> seconds, before being
+          banned for <literal>autobanTime</literal>.
+        '';
+      };
+
+      autobanTimeframe = mkOption {
+        type = types.int;
+        default = 120;
+        description = ''
+          Timeframe in which a client can connect without being banned
+          for repeated attempts (in seconds).
+        '';
+      };
+
+      autobanTime = mkOption {
+        type = types.int;
+        default = 300;
+        description = "The amount of time an IP ban lasts (in seconds).";
+      };
+
+      logFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/log/murmur/murmurd.log";
+        description = "Path to the log file for Murmur daemon. Empty means log to journald.";
+      };
+
+      welcometext = mkOption {
+        type = types.str;
+        default = "";
+        description = "Welcome message for connected clients.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 64738;
+        description = "Ports to bind to (UDP and TCP).";
+      };
+
+      hostName = mkOption {
+        type = types.str;
+        default = "";
+        description = "Host to bind to. Defaults binding on all addresses.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.murmur;
+        defaultText = literalExpression "pkgs.murmur";
+        description = "Overridable attribute of the murmur package to use.";
+      };
+
+      password = mkOption {
+        type = types.str;
+        default = "";
+        description = "Required password to join server, if specified.";
+      };
+
+      bandwidth = mkOption {
+        type = types.int;
+        default = 72000;
+        description = ''
+          Maximum bandwidth (in bits per second) that clients may send
+          speech at.
+        '';
+      };
+
+      users = mkOption {
+        type = types.int;
+        default = 100;
+        description = "Maximum number of concurrent clients allowed.";
+      };
+
+      textMsgLength = mkOption {
+        type = types.int;
+        default = 5000;
+        description = "Max length of text messages. Set 0 for no limit.";
+      };
+
+      imgMsgLength = mkOption {
+        type = types.int;
+        default = 131072;
+        description = "Max length of image messages. Set 0 for no limit.";
+      };
+
+      allowHtml = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Allow HTML in client messages, comments, and channel
+          descriptions.
+        '';
+      };
+
+      logDays = mkOption {
+        type = types.int;
+        default = 31;
+        description = ''
+          How long to store RPC logs for in the database. Set 0 to
+          keep logs forever, or -1 to disable DB logging.
+        '';
+      };
+
+      bonjour = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable Bonjour auto-discovery, which allows clients over
+          your LAN to automatically discover Murmur servers.
+        '';
+      };
+
+      sendVersion = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Send Murmur version in UDP response.";
+      };
+
+      registerName = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Public server registration name, and also the name of the
+          Root channel. Even if you don't publicly register your
+          server, you probably still want to set this.
+        '';
+      };
+
+      registerPassword = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Public server registry password, used authenticate your
+          server to the registry to prevent impersonation; required for
+          subsequent registry updates.
+        '';
+      };
+
+      registerUrl = mkOption {
+        type = types.str;
+        default = "";
+        description = "URL website for your server.";
+      };
+
+      registerHostname = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          DNS hostname where your server can be reached. This is only
+          needed if you want your server to be accessed by its
+          hostname and not IP - but the name *must* resolve on the
+          internet properly.
+        '';
+      };
+
+      clientCertRequired = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Require clients to authenticate via certificates.";
+      };
+
+      sslCert = mkOption {
+        type = types.str;
+        default = "";
+        description = "Path to your SSL certificate.";
+      };
+
+      sslKey = mkOption {
+        type = types.str;
+        default = "";
+        description = "Path to your SSL key.";
+      };
+
+      sslCa = mkOption {
+        type = types.str;
+        default = "";
+        description = "Path to your SSL CA certificate.";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Extra configuration to put into murmur.ini.";
+      };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/lib/murmur/murmurd.env";
+        description = ''
+          Environment file as defined in <citerefentry>
+          <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+          </citerefentry>.
+
+          Secrets may be passed to the service without adding them to the world-readable
+          Nix store, by specifying placeholder variables as the option value in Nix and
+          setting these variables accordingly in the environment file.
+
+          <programlisting>
+            # snippet of murmur-related config
+            services.murmur.password = "$MURMURD_PASSWORD";
+          </programlisting>
+
+          <programlisting>
+            # content of the environment file
+            MURMURD_PASSWORD=verysecretpassword
+          </programlisting>
+
+          Note that this file needs to be available on the host on which
+          <literal>murmur</literal> is running.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.murmur = {
+      description     = "Murmur Service user";
+      home            = "/var/lib/murmur";
+      createHome      = true;
+      uid             = config.ids.uids.murmur;
+      group           = "murmur";
+    };
+    users.groups.murmur = {
+      gid             = config.ids.gids.murmur;
+    };
+
+    systemd.services.murmur = {
+      description = "Murmur Chat Service";
+      wantedBy    = [ "multi-user.target" ];
+      after       = [ "network-online.target" ];
+      preStart    = ''
+        ${pkgs.envsubst}/bin/envsubst \
+          -o /run/murmur/murmurd.ini \
+          -i ${configFile}
+      '';
+
+      serviceConfig = {
+        # murmurd doesn't fork when logging to the console.
+        Type = if forking then "forking" else "simple";
+        PIDFile = mkIf forking "/run/murmur/murmurd.pid";
+        EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
+        ExecStart = "${cfg.package}/bin/mumble-server -ini /run/murmur/murmurd.ini";
+        Restart = "always";
+        RuntimeDirectory = "murmur";
+        RuntimeDirectoryMode = "0700";
+        User = "murmur";
+        Group = "murmur";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/mxisd.nix b/nixos/modules/services/networking/mxisd.nix
new file mode 100644
index 00000000000..803f0689d1f
--- /dev/null
+++ b/nixos/modules/services/networking/mxisd.nix
@@ -0,0 +1,127 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  isMa1sd =
+    package:
+    lib.hasPrefix "ma1sd" package.name;
+
+  isMxisd =
+    package:
+    lib.hasPrefix "mxisd" package.name;
+
+  cfg = config.services.mxisd;
+
+  server = optionalAttrs (cfg.server.name != null) { inherit (cfg.server) name; }
+        // optionalAttrs (cfg.server.port != null) { inherit (cfg.server) port; };
+
+  baseConfig = {
+    matrix.domain = cfg.matrix.domain;
+    key.path = "${cfg.dataDir}/signing.key";
+    storage = {
+      provider.sqlite.database = if isMa1sd cfg.package
+                                 then "${cfg.dataDir}/ma1sd.db"
+                                 else "${cfg.dataDir}/mxisd.db";
+    };
+  } // optionalAttrs (server != {}) { inherit server; };
+
+  # merges baseConfig and extraConfig into a single file
+  fullConfig = recursiveUpdate baseConfig cfg.extraConfig;
+
+  configFile = if isMa1sd cfg.package
+               then pkgs.writeText "ma1sd-config.yaml" (builtins.toJSON fullConfig)
+               else pkgs.writeText "mxisd-config.yaml" (builtins.toJSON fullConfig);
+
+in {
+  options = {
+    services.mxisd = {
+      enable = mkEnableOption "matrix federated identity server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.ma1sd;
+        defaultText = literalExpression "pkgs.ma1sd";
+        description = "The mxisd/ma1sd package to use";
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/mxisd";
+        description = "Where data mxisd/ma1sd uses resides";
+      };
+
+      extraConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        description = "Extra options merged into the mxisd/ma1sd configuration";
+      };
+
+      matrix = {
+
+        domain = mkOption {
+          type = types.str;
+          description = ''
+            the domain of the matrix homeserver
+          '';
+        };
+
+      };
+
+      server = {
+
+        name = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            Public hostname of mxisd/ma1sd, if different from the Matrix domain.
+          '';
+        };
+
+        port = mkOption {
+          type = types.nullOr types.int;
+          default = null;
+          description = ''
+            HTTP port to listen on (unencrypted)
+          '';
+        };
+
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.mxisd =
+      {
+        group = "mxisd";
+        home = cfg.dataDir;
+        createHome = true;
+        shell = "${pkgs.bash}/bin/bash";
+        uid = config.ids.uids.mxisd;
+      };
+
+    users.groups.mxisd =
+      {
+        gid = config.ids.gids.mxisd;
+      };
+
+    systemd.services.mxisd = {
+      description = "a federated identity server for the matrix ecosystem";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = let
+        executable = if isMa1sd cfg.package then "ma1sd" else "mxisd";
+      in {
+        Type = "simple";
+        User = "mxisd";
+        Group = "mxisd";
+        ExecStart = "${cfg.package}/bin/${executable} -c ${configFile}";
+        WorkingDirectory = cfg.dataDir;
+        Restart = "on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/namecoind.nix b/nixos/modules/services/networking/namecoind.nix
new file mode 100644
index 00000000000..8f7a5123f7e
--- /dev/null
+++ b/nixos/modules/services/networking/namecoind.nix
@@ -0,0 +1,199 @@
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg     = config.services.namecoind;
+  dataDir = "/var/lib/namecoind";
+  useSSL  = (cfg.rpc.certificate != null) && (cfg.rpc.key != null);
+  useRPC  = (cfg.rpc.user != null) && (cfg.rpc.password != null);
+
+  listToConf = option: list:
+    concatMapStrings (value :"${option}=${value}\n") list;
+
+  configFile = pkgs.writeText "namecoin.conf" (''
+    server=1
+    daemon=0
+    txindex=1
+    txprevcache=1
+    walletpath=${cfg.wallet}
+    gen=${if cfg.generate then "1" else "0"}
+    ${listToConf "addnode" cfg.extraNodes}
+    ${listToConf "connect" cfg.trustedNodes}
+  '' + optionalString useRPC ''
+    rpcbind=${cfg.rpc.address}
+    rpcport=${toString cfg.rpc.port}
+    rpcuser=${cfg.rpc.user}
+    rpcpassword=${cfg.rpc.password}
+    ${listToConf "rpcallowip" cfg.rpc.allowFrom}
+  '' + optionalString useSSL ''
+    rpcssl=1
+    rpcsslcertificatechainfile=${cfg.rpc.certificate}
+    rpcsslprivatekeyfile=${cfg.rpc.key}
+    rpcsslciphers=TLSv1.2+HIGH:TLSv1+HIGH:!SSLv2:!aNULL:!eNULL:!3DES:@STRENGTH
+  '');
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.namecoind = {
+
+      enable = mkEnableOption "namecoind, Namecoin client";
+
+      wallet = mkOption {
+        type = types.path;
+        default = "${dataDir}/wallet.dat";
+        description = ''
+          Wallet file. The ownership of the file has to be
+          namecoin:namecoin, and the permissions must be 0640.
+        '';
+      };
+
+      generate = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to generate (mine) Namecoins.
+        '';
+      };
+
+      extraNodes = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = ''
+          List of additional peer IP addresses to connect to.
+        '';
+      };
+
+      trustedNodes = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = ''
+          List of the only peer IP addresses to connect to. If specified
+          no other connection will be made.
+        '';
+      };
+
+      rpc.user = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          User name for RPC connections.
+        '';
+      };
+
+      rpc.password = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Password for RPC connections.
+        '';
+      };
+
+      rpc.address = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = ''
+          IP address the RPC server will bind to.
+        '';
+      };
+
+      rpc.port = mkOption {
+        type = types.port;
+        default = 8332;
+        description = ''
+          Port the RPC server will bind to.
+        '';
+      };
+
+      rpc.certificate = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/lib/namecoind/server.cert";
+        description = ''
+          Certificate file for securing RPC connections.
+        '';
+      };
+
+      rpc.key = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/lib/namecoind/server.pem";
+        description = ''
+          Key file for securing RPC connections.
+        '';
+      };
+
+
+      rpc.allowFrom = mkOption {
+        type = types.listOf types.str;
+        default = [ "127.0.0.1" ];
+        description = ''
+          List of IP address ranges allowed to use the RPC API.
+          Wiledcards (*) can be user to specify a range.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.namecoin = {
+      uid  = config.ids.uids.namecoin;
+      description = "Namecoin daemon user";
+      home = dataDir;
+      createHome = true;
+    };
+
+    users.groups.namecoin = {
+      gid  = config.ids.gids.namecoin;
+    };
+
+    systemd.services.namecoind = {
+      description = "Namecoind daemon";
+      after    = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      startLimitIntervalSec = 120;
+      startLimitBurst = 5;
+      serviceConfig = {
+        User  = "namecoin";
+        Group = "namecoin";
+        ExecStart  = "${pkgs.namecoind}/bin/namecoind -conf=${configFile} -datadir=${dataDir} -printtoconsole";
+        ExecStop   = "${pkgs.coreutils}/bin/kill -KILL $MAINPID";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        Nice = "10";
+        PrivateTmp = true;
+        TimeoutStopSec     = "60s";
+        TimeoutStartSec    = "2s";
+        Restart            = "always";
+      };
+
+      preStart = optionalString (cfg.wallet != "${dataDir}/wallet.dat")  ''
+        # check wallet file permissions
+        if [ "$(stat --printf '%u' ${cfg.wallet})" != "${toString config.ids.uids.namecoin}" \
+           -o "$(stat --printf '%g' ${cfg.wallet})" != "${toString config.ids.gids.namecoin}" \
+           -o "$(stat --printf '%a' ${cfg.wallet})" != "640" ]; then
+           echo "ERROR: bad ownership or rights on ${cfg.wallet}" >&2
+           exit 1
+        fi
+      '';
+
+    };
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+}
diff --git a/nixos/modules/services/networking/nar-serve.nix b/nixos/modules/services/networking/nar-serve.nix
new file mode 100644
index 00000000000..745138186a2
--- /dev/null
+++ b/nixos/modules/services/networking/nar-serve.nix
@@ -0,0 +1,55 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.nar-serve;
+in
+{
+  meta = {
+    maintainers = [ maintainers.rizary ];
+  };
+  options = {
+    services.nar-serve = {
+      enable = mkEnableOption "Serve NAR file contents via HTTP";
+
+      port = mkOption {
+        type = types.port;
+        default = 8383;
+        description = ''
+          Port number where nar-serve will listen on.
+        '';
+      };
+
+      cacheURL = mkOption {
+        type = types.str;
+        default = "https://cache.nixos.org/";
+        description = ''
+          Binary cache URL to connect to.
+
+          The URL format is compatible with the nix remote url style, such as:
+          - http://, https:// for binary caches via HTTP or HTTPS
+          - s3:// for binary caches stored in Amazon S3
+          - gs:// for binary caches stored in Google Cloud Storage
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.nar-serve = {
+      description = "NAR server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment.PORT = toString cfg.port;
+      environment.NAR_CACHE_URL = cfg.cacheURL;
+
+      serviceConfig = {
+        Restart = "always";
+        RestartSec = "5s";
+        ExecStart = "${pkgs.nar-serve}/bin/nar-serve";
+        DynamicUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nat.nix b/nixos/modules/services/networking/nat.nix
new file mode 100644
index 00000000000..2e58cd699b2
--- /dev/null
+++ b/nixos/modules/services/networking/nat.nix
@@ -0,0 +1,364 @@
+# 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
+
+{
+
+  ###### interface
+
+  options = {
+
+    networking.nat.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description =
+        ''
+          Whether to enable Network Address Translation (NAT).
+        '';
+    };
+
+    networking.nat.enableIPv6 = mkOption {
+      type = types.bool;
+      default = false;
+      description =
+        ''
+          Whether to enable IPv6 NAT.
+        '';
+    };
+
+    networking.nat.internalInterfaces = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "eth0" ];
+      description =
+        ''
+          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 = [];
+      example = [ "192.168.1.0/24" ];
+      description =
+        ''
+          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 = [];
+      example = [ "fc00::/64" ];
+      description =
+        ''
+          The IPv6 address ranges for which to perform NAT.  Packets
+          coming from these addresses (on any interface) and destined
+          for the external interface will be rewritten.
+        '';
+    };
+
+    networking.nat.externalInterface = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "eth1";
+      description =
+        ''
+          The name of the external network interface.
+        '';
+    };
+
+    networking.nat.externalIP = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "203.0.113.123";
+      description =
+        ''
+          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 =
+        ''
+          The public IPv6 address to which packets from the local
+          network are to be rewritten.  If this is left empty, the
+          IP address associated with the external interface will be
+          used.
+        '';
+    };
+
+    networking.nat.forwardPorts = mkOption {
+      type = with types; listOf (submodule {
+        options = {
+          sourcePort = mkOption {
+            type = types.either types.int (types.strMatching "[[:digit:]]+:[[:digit:]]+");
+            example = 8080;
+            description = "Source port of the external interface; to specify a port range, use a string with a colon (e.g. \"60000:61000\")";
+          };
+
+          destination = mkOption {
+            type = types.str;
+            example = "10.0.0.1:80";
+            description = "Forward connection to destination ip:port (or [ipv6]:port); to specify a port range, use ip:start-end";
+          };
+
+          proto = mkOption {
+            type = types.str;
+            default = "tcp";
+            example = "udp";
+            description = "Protocol of forwarded connection";
+          };
+
+          loopbackIPs = mkOption {
+            type = types.listOf types.str;
+            default = [];
+            example = literalExpression ''[ "55.1.2.3" ]'';
+            description = "Public IPs for NAT reflection; for connections to `loopbackip:sourcePort' from the host itself and from other hosts behind NAT";
+          };
+        };
+      });
+      default = [];
+      example = [
+        { sourcePort = 8080; destination = "10.0.0.1:80"; proto = "tcp"; }
+        { sourcePort = 8080; destination = "[fc00::2]:80"; proto = "tcp"; }
+      ];
+      description =
+        ''
+          List of forwarded ports from the external interface to
+          internal destinations by using DNAT. 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 =
+        ''
+          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 =
+        ''
+          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 =
+        ''
+          Additional shell commands executed as part of the nat
+          teardown script.
+        '';
+    };
+
+  };
+
+
+  ###### 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";
+        }
+      ];
+
+      environment.systemPackages = [ pkgs.iptables ];
+
+      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;
+      };
+
+      systemd.services = mkIf (!config.networking.firewall.enable) { nat = {
+        description = "Network Address Translation";
+        wantedBy = [ "network.target" ];
+        after = [ "network-pre.target" "systemd-modules-load.service" ];
+        path = [ pkgs.iptables ];
+        unitConfig.ConditionCapability = "CAP_NET_ADMIN";
+
+        serviceConfig = {
+          Type = "oneshot";
+          RemainAfterExit = true;
+        };
+
+        script = flushNat + setupNat;
+
+        postStop = flushNat;
+      }; };
+    })
+  ];
+}
diff --git a/nixos/modules/services/networking/nats.nix b/nixos/modules/services/networking/nats.nix
new file mode 100644
index 00000000000..3e86a4f07bc
--- /dev/null
+++ b/nixos/modules/services/networking/nats.nix
@@ -0,0 +1,158 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.nats;
+
+  format = pkgs.formats.json { };
+
+  configFile = format.generate "nats.conf" cfg.settings;
+
+in {
+
+  ### Interface
+
+  options = {
+    services.nats = {
+      enable = mkEnableOption "NATS messaging system";
+
+      user = mkOption {
+        type = types.str;
+        default = "nats";
+        description = "User account under which NATS runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "nats";
+        description = "Group under which NATS runs.";
+      };
+
+      serverName = mkOption {
+        default = "nats";
+        example = "n1-c3";
+        type = types.str;
+        description = ''
+          Name of the NATS server, must be unique if clustered.
+        '';
+      };
+
+      jetstream = mkEnableOption "JetStream";
+
+      port = mkOption {
+        default = 4222;
+        type = types.port;
+        description = ''
+          Port on which to listen.
+        '';
+      };
+
+      dataDir = mkOption {
+        default = "/var/lib/nats";
+        type = types.path;
+        description = ''
+          The NATS data directory. Only used if JetStream is enabled, for
+          storing stream metadata and messages.
+
+          If left as the default value this directory will automatically be
+          created before the NATS server starts, otherwise the sysadmin is
+          responsible for ensuring the directory exists with appropriate
+          ownership and permissions.
+        '';
+      };
+
+      settings = mkOption {
+        default = { };
+        type = format.type;
+        example = literalExpression ''
+          {
+            jetstream = {
+              max_mem = "1G";
+              max_file = "10G";
+            };
+          };
+        '';
+        description = ''
+          Declarative NATS configuration. See the
+          <link xlink:href="https://docs.nats.io/nats-server/configuration">
+          NATS documentation</link> for a list of options.
+        '';
+      };
+    };
+  };
+
+  ### Implementation
+
+  config = mkIf cfg.enable {
+    services.nats.settings = {
+      server_name = cfg.serverName;
+      port = cfg.port;
+      jetstream = optionalAttrs cfg.jetstream { store_dir = cfg.dataDir; };
+    };
+
+    systemd.services.nats = {
+      description = "NATS messaging system";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = mkMerge [
+        (mkIf (cfg.dataDir == "/var/lib/nats") {
+          StateDirectory = "nats";
+          StateDirectoryMode = "0750";
+        })
+        {
+          Type = "simple";
+          ExecStart = "${pkgs.nats-server}/bin/nats-server -c ${configFile}";
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          ExecStop = "${pkgs.coreutils}/bin/kill -SIGINT $MAINPID";
+          Restart = "on-failure";
+
+          User = cfg.user;
+          Group = cfg.group;
+
+          # Hardening
+          CapabilityBoundingSet = "";
+          LimitNOFILE = 800000; # JetStream requires 2 FDs open per stream.
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          NoNewPrivileges = true;
+          PrivateDevices = true;
+          PrivateTmp = true;
+          PrivateUsers = true;
+          ProcSubset = "pid";
+          ProtectClock = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectProc = "invisible";
+          ProtectSystem = "strict";
+          ReadOnlyPaths = [ ];
+          ReadWritePaths = [ cfg.dataDir ];
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+          UMask = "0077";
+        }
+      ];
+    };
+
+    users.users = mkIf (cfg.user == "nats") {
+      nats = {
+        description = "NATS daemon user";
+        isSystemUser = true;
+        group = cfg.group;
+        home = cfg.dataDir;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "nats") { nats = { }; };
+  };
+
+}
diff --git a/nixos/modules/services/networking/nbd.nix b/nixos/modules/services/networking/nbd.nix
new file mode 100644
index 00000000000..87f8c41a8e5
--- /dev/null
+++ b/nixos/modules/services/networking/nbd.nix
@@ -0,0 +1,146 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nbd;
+  configFormat = pkgs.formats.ini { };
+  iniFields = with types; attrsOf (oneOf [ bool int float str ]);
+  serverConfig = configFormat.generate "nbd-server-config"
+    ({
+      generic =
+        (cfg.server.extraOptions // {
+          user = "root";
+          group = "root";
+          port = cfg.server.listenPort;
+        } // (optionalAttrs (cfg.server.listenAddress != null) {
+          listenaddr = cfg.server.listenAddress;
+        }));
+    }
+    // (mapAttrs
+      (_: { path, allowAddresses, extraOptions }:
+        extraOptions // {
+          exportname = path;
+        } // (optionalAttrs (allowAddresses != null) {
+          authfile = pkgs.writeText "authfile" (concatStringsSep "\n" allowAddresses);
+        }))
+      cfg.server.exports)
+    );
+  splitLists =
+    partition
+      (path: hasPrefix "/dev/" path)
+      (mapAttrsToList (_: { path, ... }: path) cfg.server.exports);
+  allowedDevices = splitLists.right;
+  boundPaths = splitLists.wrong;
+in
+{
+  options = {
+    services.nbd = {
+      server = {
+        enable = mkEnableOption "the Network Block Device (nbd) server";
+
+        listenPort = mkOption {
+          type = types.port;
+          default = 10809;
+          description = "Port to listen on. The port is NOT automatically opened in the firewall.";
+        };
+
+        extraOptions = mkOption {
+          type = iniFields;
+          default = {
+            allowlist = false;
+          };
+          description = ''
+            Extra options for the server. See
+            <citerefentry><refentrytitle>nbd-server</refentrytitle>
+            <manvolnum>5</manvolnum></citerefentry>.
+          '';
+        };
+
+        exports = mkOption {
+          description = "Files or block devices to make available over the network.";
+          default = { };
+          type = with types; attrsOf
+            (submodule {
+              options = {
+                path = mkOption {
+                  type = str;
+                  description = "File or block device to export.";
+                  example = "/dev/sdb1";
+                };
+
+                allowAddresses = mkOption {
+                  type = nullOr (listOf str);
+                  default = null;
+                  example = [ "10.10.0.0/24" "127.0.0.1" ];
+                  description = "IPs and subnets that are authorized to connect for this device. If not specified, the server will allow all connections.";
+                };
+
+                extraOptions = mkOption {
+                  type = iniFields;
+                  default = {
+                    flush = true;
+                    fua = true;
+                  };
+                  description = ''
+                    Extra options for this export. See
+                    <citerefentry><refentrytitle>nbd-server</refentrytitle>
+                    <manvolnum>5</manvolnum></citerefentry>.
+                  '';
+                };
+              };
+            });
+        };
+
+        listenAddress = mkOption {
+          type = with types; nullOr str;
+          description = "Address to listen on. If not specified, the server will listen on all interfaces.";
+          default = null;
+          example = "10.10.0.1";
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.server.enable {
+    boot.kernelModules = [ "nbd" ];
+
+    systemd.services.nbd-server = {
+      after = [ "network-online.target" ];
+      before = [ "multi-user.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.nbd}/bin/nbd-server -C ${serverConfig}";
+        Type = "forking";
+
+        DeviceAllow = map (path: "${path} rw") allowedDevices;
+        BindPaths = boundPaths;
+
+        CapabilityBoundingSet = "";
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = false;
+        PrivateMounts = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "noaccess";
+        ProtectSystem = "strict";
+        RestrictAddressFamilies = "AF_INET AF_INET6";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        UMask = "0077";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/ncdns.nix b/nixos/modules/services/networking/ncdns.nix
new file mode 100644
index 00000000000..82c285d0516
--- /dev/null
+++ b/nixos/modules/services/networking/ncdns.nix
@@ -0,0 +1,283 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfgs = config.services;
+  cfg  = cfgs.ncdns;
+
+  dataDir  = "/var/lib/ncdns";
+  username = "ncdns";
+
+  valueType = with types; oneOf [ int str bool path ]
+    // { description = "setting type (integer, string, bool or path)"; };
+
+  configType = with types; attrsOf (nullOr (either valueType configType))
+    // { description = ''
+          ncdns.conf configuration type. The format consists of an
+          attribute set of settings. Each setting can be either `null`,
+          a value or an attribute set. The allowed values are integers,
+          strings, booleans or paths.
+         '';
+       };
+
+  configFile = pkgs.runCommand "ncdns.conf"
+    { json = builtins.toJSON cfg.settings;
+      passAsFile = [ "json" ];
+    }
+    "${pkgs.remarshal}/bin/json2toml < $jsonPath > $out";
+
+  defaultFiles = {
+    public  = "${dataDir}/bit.key";
+    private = "${dataDir}/bit.private";
+    zonePublic  = "${dataDir}/bit-zone.key";
+    zonePrivate = "${dataDir}/bit-zone.private";
+  };
+
+  # if all keys are the default value
+  needsKeygen = all id (flip mapAttrsToList cfg.dnssec.keys
+    (n: v: v == getAttr n defaultFiles));
+
+  mkDefaultAttrs = mapAttrs (n: v: mkDefault v);
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.ncdns = {
+
+      enable = mkEnableOption ''
+        ncdns, a Go daemon to bridge Namecoin to DNS.
+        To resolve .bit domains set <literal>services.namecoind.enable = true;</literal>
+        and an RPC username/password
+      '';
+
+      address = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = ''
+          The IP address the ncdns resolver will bind to.  Leave this unchanged
+          if you do not wish to directly expose the resolver.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 5333;
+        description = ''
+          The port the ncdns resolver will bind to.
+        '';
+      };
+
+      identity.hostname = mkOption {
+        type = types.str;
+        default = config.networking.hostName;
+        defaultText = literalExpression "config.networking.hostName";
+        example = "example.com";
+        description = ''
+          The hostname of this ncdns instance, which defaults to the machine
+          hostname. If specified, ncdns lists the hostname as an NS record at
+          the zone apex:
+          <programlisting>
+          bit. IN NS ns1.example.com.
+          </programlisting>
+          If unset ncdns will generate an internal psuedo-hostname under the
+          zone, which will resolve to the value of
+          <option>services.ncdns.identity.address</option>.
+          If you are only using ncdns locally you can ignore this.
+        '';
+      };
+
+      identity.hostmaster = mkOption {
+        type = types.str;
+        default = "";
+        example = "root@example.com";
+        description = ''
+          An email address for the SOA record at the bit zone.
+          If you are only using ncdns locally you can ignore this.
+        '';
+      };
+
+      identity.address = mkOption {
+        type = types.str;
+        default = "127.127.127.127";
+        description = ''
+          The IP address the hostname specified in
+          <option>services.ncdns.identity.hostname</option> should resolve to.
+          If you are only using ncdns locally you can ignore this.
+        '';
+      };
+
+      dnssec.enable = mkEnableOption ''
+        DNSSEC support in ncdns. This will generate KSK and ZSK keypairs
+        (unless provided via the options
+        <option>services.ncdns.dnssec.publicKey</option>,
+        <option>services.ncdns.dnssec.privateKey</option> etc.) and add a trust
+        anchor to recursive resolvers
+      '';
+
+      dnssec.keys.public = mkOption {
+        type = types.path;
+        default = defaultFiles.public;
+        description = ''
+          Path to the file containing the KSK public key.
+          The key can be generated using the <literal>dnssec-keygen</literal>
+          command, provided by the package <package>bind</package> as follows:
+          <programlisting>
+          $ dnssec-keygen -a RSASHA256 -3 -b 2048 -f KSK bit
+          </programlisting>
+        '';
+      };
+
+      dnssec.keys.private = mkOption {
+        type = types.path;
+        default = defaultFiles.private;
+        description = ''
+          Path to the file containing the KSK private key.
+        '';
+      };
+
+      dnssec.keys.zonePublic = mkOption {
+        type = types.path;
+        default = defaultFiles.zonePublic;
+        description = ''
+          Path to the file containing the ZSK public key.
+          The key can be generated using the <literal>dnssec-keygen</literal>
+          command, provided by the package <package>bind</package> as follows:
+          <programlisting>
+          $ dnssec-keygen -a RSASHA256 -3 -b 2048 bit
+          </programlisting>
+        '';
+      };
+
+      dnssec.keys.zonePrivate = mkOption {
+        type = types.path;
+        default = defaultFiles.zonePrivate;
+        description = ''
+          Path to the file containing the ZSK private key.
+        '';
+      };
+
+      settings = mkOption {
+        type = configType;
+        default = { };
+        example = literalExpression ''
+          { # enable webserver
+            ncdns.httplistenaddr = ":8202";
+
+            # synchronize TLS certs
+            certstore.nss = true;
+            # note: all paths are relative to the config file
+            certstore.nsscertdir =  "../../var/lib/ncdns";
+            certstore.nssdbdir = "../../home/alice/.pki/nssdb";
+          }
+        '';
+        description = ''
+          ncdns settings. Use this option to configure ncds
+          settings not exposed in a NixOS option or to bypass one.
+          See the example ncdns.conf file at <link xlink:href="
+          https://git.io/JfX7g"/> for the available options.
+        '';
+      };
+
+    };
+
+    services.pdns-recursor.resolveNamecoin = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Resolve <literal>.bit</literal> top-level domains using ncdns and namecoin.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.pdns-recursor = mkIf cfgs.pdns-recursor.resolveNamecoin {
+      forwardZonesRecurse.bit = "127.0.0.1:${toString cfg.port}";
+      luaConfig =
+        if cfg.dnssec.enable
+          then ''readTrustAnchorsFromFile("${cfg.dnssec.keys.public}")''
+          else ''addNTA("bit", "namecoin DNSSEC disabled")'';
+    };
+
+    # Avoid pdns-recursor not finding the DNSSEC keys
+    systemd.services.pdns-recursor = mkIf cfgs.pdns-recursor.resolveNamecoin {
+      after = [ "ncdns.service" ];
+      wants = [ "ncdns.service" ];
+    };
+
+    services.ncdns.settings = mkDefaultAttrs {
+      ncdns =
+        { # Namecoin RPC
+          namecoinrpcaddress =
+            "${cfgs.namecoind.rpc.address}:${toString cfgs.namecoind.rpc.port}";
+          namecoinrpcusername = cfgs.namecoind.rpc.user;
+          namecoinrpcpassword = cfgs.namecoind.rpc.password;
+
+          # Identity
+          selfname = cfg.identity.hostname;
+          hostmaster = cfg.identity.hostmaster;
+          selfip = cfg.identity.address;
+
+          # Other
+          bind = "${cfg.address}:${toString cfg.port}";
+        }
+        // optionalAttrs cfg.dnssec.enable
+        { # DNSSEC
+          publickey  = "../.." + cfg.dnssec.keys.public;
+          privatekey = "../.." + cfg.dnssec.keys.private;
+          zonepublickey  = "../.." + cfg.dnssec.keys.zonePublic;
+          zoneprivatekey = "../.." + cfg.dnssec.keys.zonePrivate;
+        };
+
+        # Daemon
+        service.daemon = true;
+        xlog.journal = true;
+    };
+
+    users.users.ncdns = {
+      isSystemUser = true;
+      group = "ncdns";
+      description = "ncdns daemon user";
+    };
+    users.groups.ncdns = {};
+
+    systemd.services.ncdns = {
+      description = "ncdns daemon";
+      after    = [ "namecoind.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        User = "ncdns";
+        StateDirectory = "ncdns";
+        Restart = "on-failure";
+        ExecStart = "${pkgs.ncdns}/bin/ncdns -conf=${configFile}";
+      };
+
+      preStart = optionalString (cfg.dnssec.enable && needsKeygen) ''
+        cd ${dataDir}
+        if [ ! -e bit.key ]; then
+          ${pkgs.bind}/bin/dnssec-keygen -a RSASHA256 -3 -b 2048 bit
+          mv Kbit.*.key bit-zone.key
+          mv Kbit.*.private bit-zone.private
+          ${pkgs.bind}/bin/dnssec-keygen -a RSASHA256 -3 -b 2048 -f KSK bit
+          mv Kbit.*.key bit.key
+          mv Kbit.*.private bit.private
+        fi
+      '';
+    };
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+}
diff --git a/nixos/modules/services/networking/ndppd.nix b/nixos/modules/services/networking/ndppd.nix
new file mode 100644
index 00000000000..6046ac860cf
--- /dev/null
+++ b/nixos/modules/services/networking/ndppd.nix
@@ -0,0 +1,189 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ndppd;
+
+  render = s: f: concatStringsSep "\n" (mapAttrsToList f s);
+  prefer = a: b: if a != null then a else b;
+
+  ndppdConf = prefer cfg.configFile (pkgs.writeText "ndppd.conf" ''
+    route-ttl ${toString cfg.routeTTL}
+    ${render cfg.proxies (proxyInterfaceName: proxy: ''
+    proxy ${prefer proxy.interface proxyInterfaceName} {
+      router ${boolToString proxy.router}
+      timeout ${toString proxy.timeout}
+      ttl ${toString proxy.ttl}
+      ${render proxy.rules (ruleNetworkName: rule: ''
+      rule ${prefer rule.network ruleNetworkName} {
+        ${rule.method}${if rule.method == "iface" then " ${rule.interface}" else ""}
+      }'')}
+    }'')}
+  '');
+
+  proxy = types.submodule {
+    options = {
+      interface = mkOption {
+        type = types.nullOr types.str;
+        description = ''
+          Listen for any Neighbor Solicitation messages on this interface,
+          and respond to them according to a set of rules.
+          Defaults to the name of the attrset.
+        '';
+        default = null;
+      };
+      router = mkOption {
+        type = types.bool;
+        description = ''
+          Turns on or off the router flag for Neighbor Advertisement Messages.
+        '';
+        default = true;
+      };
+      timeout = mkOption {
+        type = types.int;
+        description = ''
+          Controls how long to wait for a Neighbor Advertisment Message before
+          invalidating the entry, in milliseconds.
+        '';
+        default = 500;
+      };
+      ttl = mkOption {
+        type = types.int;
+        description = ''
+          Controls how long a valid or invalid entry remains in the cache, in
+          milliseconds.
+        '';
+        default = 30000;
+      };
+      rules = mkOption {
+        type = types.attrsOf rule;
+        description = ''
+          This is a rule that the target address is to match against. If no netmask
+          is provided, /128 is assumed. You may have several rule sections, and the
+          addresses may or may not overlap.
+        '';
+        default = {};
+      };
+    };
+  };
+
+  rule = types.submodule {
+    options = {
+      network = mkOption {
+        type = types.nullOr types.str;
+        description = ''
+          This is the target address is to match against. If no netmask
+          is provided, /128 is assumed. The addresses of serveral rules
+          may or may not overlap.
+          Defaults to the name of the attrset.
+        '';
+        default = null;
+      };
+      method = mkOption {
+        type = types.enum [ "static" "iface" "auto" ];
+        description = ''
+          static: Immediately answer any Neighbor Solicitation Messages
+            (if they match the IP rule).
+          iface: Forward the Neighbor Solicitation Message through the specified
+            interface and only respond if a matching Neighbor Advertisement
+            Message is received.
+          auto: Same as iface, but instead of manually specifying the outgoing
+            interface, check for a matching route in /proc/net/ipv6_route.
+        '';
+        default = "auto";
+      };
+      interface = mkOption {
+        type = types.nullOr types.str;
+        description = "Interface to use when method is iface.";
+        default = null;
+      };
+    };
+  };
+
+in {
+  options.services.ndppd = {
+    enable = mkEnableOption "daemon that proxies NDP (Neighbor Discovery Protocol) messages between interfaces";
+    interface = mkOption {
+      type = types.nullOr types.str;
+      description = ''
+        Interface which is on link-level with router.
+        (Legacy option, use services.ndppd.proxies.&lt;interface&gt;.rules.&lt;network&gt; instead)
+      '';
+      default = null;
+      example = "eth0";
+    };
+    network = mkOption {
+      type = types.nullOr types.str;
+      description = ''
+        Network that we proxy.
+        (Legacy option, use services.ndppd.proxies.&lt;interface&gt;.rules.&lt;network&gt; instead)
+      '';
+      default = null;
+      example = "1111::/64";
+    };
+    configFile = mkOption {
+      type = types.nullOr types.path;
+      description = "Path to configuration file.";
+      default = null;
+    };
+    routeTTL = mkOption {
+      type = types.int;
+      description = ''
+        This tells 'ndppd' how often to reload the route file /proc/net/ipv6_route,
+        in milliseconds.
+      '';
+      default = 30000;
+    };
+    proxies = mkOption {
+      type = types.attrsOf proxy;
+      description = ''
+        This sets up a listener, that will listen for any Neighbor Solicitation
+        messages, and respond to them according to a set of rules.
+      '';
+      default = {};
+      example = literalExpression ''
+        {
+          eth0.rules."1111::/64" = {};
+        }
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    warnings = mkIf (cfg.interface != null && cfg.network != null) [ ''
+      The options services.ndppd.interface and services.ndppd.network will probably be removed soon,
+      please use services.ndppd.proxies.<interface>.rules.<network> instead.
+    '' ];
+
+    services.ndppd.proxies = mkIf (cfg.interface != null && cfg.network != null) {
+      ${cfg.interface}.rules.${cfg.network} = {};
+    };
+
+    systemd.services.ndppd = {
+      description = "NDP Proxy Daemon";
+      documentation = [ "man:ndppd(1)" "man:ndppd.conf(5)" ];
+      after = [ "network-pre.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.ndppd}/bin/ndppd -c ${ndppdConf}";
+
+        # Sandboxing
+        CapabilityBoundingSet = "CAP_NET_RAW CAP_NET_ADMIN";
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = "AF_INET6 AF_PACKET AF_NETLINK";
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nebula.nix b/nixos/modules/services/networking/nebula.nix
new file mode 100644
index 00000000000..de4439415cf
--- /dev/null
+++ b/nixos/modules/services/networking/nebula.nix
@@ -0,0 +1,217 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.nebula;
+  enabledNetworks = filterAttrs (n: v: v.enable) cfg.networks;
+
+  format = pkgs.formats.yaml {};
+
+  nameToId = netName: "nebula-${netName}";
+in
+{
+  # Interface
+
+  options = {
+    services.nebula = {
+      networks = mkOption {
+        description = "Nebula network definitions.";
+        default = {};
+        type = types.attrsOf (types.submodule {
+          options = {
+            enable = mkOption {
+              type = types.bool;
+              default = true;
+              description = "Enable or disable this network.";
+            };
+
+            package = mkOption {
+              type = types.package;
+              default = pkgs.nebula;
+              defaultText = literalExpression "pkgs.nebula";
+              description = "Nebula derivation to use.";
+            };
+
+            ca = mkOption {
+              type = types.path;
+              description = "Path to the certificate authority certificate.";
+              example = "/etc/nebula/ca.crt";
+            };
+
+            cert = mkOption {
+              type = types.path;
+              description = "Path to the host certificate.";
+              example = "/etc/nebula/host.crt";
+            };
+
+            key = mkOption {
+              type = types.path;
+              description = "Path to the host key.";
+              example = "/etc/nebula/host.key";
+            };
+
+            staticHostMap = mkOption {
+              type = types.attrsOf (types.listOf (types.str));
+              default = {};
+              description = ''
+                The static host map defines a set of hosts with fixed IP addresses on the internet (or any network).
+                A host can have multiple fixed IP addresses defined here, and nebula will try each when establishing a tunnel.
+              '';
+              example = { "192.168.100.1" = [ "100.64.22.11:4242" ]; };
+            };
+
+            isLighthouse = mkOption {
+              type = types.bool;
+              default = false;
+              description = "Whether this node is a lighthouse.";
+            };
+
+            lighthouses = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = ''
+                List of IPs of lighthouse hosts this node should report to and query from. This should be empty on lighthouse
+                nodes. The IPs should be the lighthouse's Nebula IPs, not their external IPs.
+              '';
+              example = [ "192.168.100.1" ];
+            };
+
+            listen.host = mkOption {
+              type = types.str;
+              default = "0.0.0.0";
+              description = "IP address to listen on.";
+            };
+
+            listen.port = mkOption {
+              type = types.port;
+              default = 4242;
+              description = "Port number to listen on.";
+            };
+
+            tun.disable = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                When tun is disabled, a lighthouse can be started without a local tun interface (and therefore without root).
+              '';
+            };
+
+            tun.device = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = "Name of the tun device. Defaults to nebula.\${networkName}.";
+            };
+
+            firewall.outbound = mkOption {
+              type = types.listOf types.attrs;
+              default = [];
+              description = "Firewall rules for outbound traffic.";
+              example = [ { port = "any"; proto = "any"; host = "any"; } ];
+            };
+
+            firewall.inbound = mkOption {
+              type = types.listOf types.attrs;
+              default = [];
+              description = "Firewall rules for inbound traffic.";
+              example = [ { port = "any"; proto = "any"; host = "any"; } ];
+            };
+
+            settings = mkOption {
+              type = format.type;
+              default = {};
+              description = ''
+                Nebula configuration. Refer to
+                <link xlink:href="https://github.com/slackhq/nebula/blob/master/examples/config.yml"/>
+                for details on supported values.
+              '';
+              example = literalExpression ''
+                {
+                  lighthouse.dns = {
+                    host = "0.0.0.0";
+                    port = 53;
+                  };
+                }
+              '';
+            };
+          };
+        });
+      };
+    };
+  };
+
+  # Implementation
+  config = mkIf (enabledNetworks != {}) {
+    systemd.services = mkMerge (mapAttrsToList (netName: netCfg:
+      let
+        networkId = nameToId netName;
+        settings = recursiveUpdate {
+          pki = {
+            ca = netCfg.ca;
+            cert = netCfg.cert;
+            key = netCfg.key;
+          };
+          static_host_map = netCfg.staticHostMap;
+          lighthouse = {
+            am_lighthouse = netCfg.isLighthouse;
+            hosts = netCfg.lighthouses;
+          };
+          listen = {
+            host = netCfg.listen.host;
+            port = netCfg.listen.port;
+          };
+          tun = {
+            disabled = netCfg.tun.disable;
+            dev = if (netCfg.tun.device != null) then netCfg.tun.device else "nebula.${netName}";
+          };
+          firewall = {
+            inbound = netCfg.firewall.inbound;
+            outbound = netCfg.firewall.outbound;
+          };
+        } netCfg.settings;
+        configFile = format.generate "nebula-config-${netName}.yml" settings;
+        in
+        {
+          # Create 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;
+              })
+            ];
+          };
+        }) enabledNetworks);
+
+    # Open the chosen ports for UDP.
+    networking.firewall.allowedUDPPorts =
+      unique (mapAttrsToList (netName: netCfg: netCfg.listen.port) enabledNetworks);
+
+    # 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}";
+          isSystemUser = true;
+        };
+      }) enabledNetworks);
+
+    users.groups = mkMerge (mapAttrsToList (netName: netCfg:
+      mkIf netCfg.tun.disable {
+        ${nameToId netName} = {};
+      }) enabledNetworks);
+  };
+}
diff --git a/nixos/modules/services/networking/networkmanager.nix b/nixos/modules/services/networking/networkmanager.nix
new file mode 100644
index 00000000000..7a9d9e5428a
--- /dev/null
+++ b/nixos/modules/services/networking/networkmanager.nix
@@ -0,0 +1,568 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.networking.networkmanager;
+
+  basePackages = with pkgs; [
+    modemmanager
+    networkmanager
+    networkmanager-fortisslvpn
+    networkmanager-iodine
+    networkmanager-l2tp
+    networkmanager-openconnect
+    networkmanager-openvpn
+    networkmanager-vpnc
+    networkmanager-sstp
+   ] ++ optional (!delegateWireless && !enableIwd) wpa_supplicant;
+
+  delegateWireless = config.networking.wireless.enable == true && cfg.unmanaged != [];
+
+  enableIwd = cfg.wifi.backend == "iwd";
+
+  mkValue = v:
+    if v == true then "yes"
+    else if v == false then "no"
+    else if lib.isInt v then toString v
+    else v;
+
+  mkSection = name: attrs: ''
+    [${name}]
+    ${
+      lib.concatStringsSep "\n"
+        (lib.mapAttrsToList
+          (k: v: "${k}=${mkValue v}")
+          (lib.filterAttrs
+            (k: v: v != null)
+            attrs))
+    }
+  '';
+
+  configFile = pkgs.writeText "NetworkManager.conf" (lib.concatStringsSep "\n" [
+    (mkSection "main" {
+      plugins = "keyfile";
+      dhcp = cfg.dhcp;
+      dns = cfg.dns;
+      # If resolvconf is disabled that means that resolv.conf is managed by some other module.
+      rc-manager =
+        if config.networking.resolvconf.enable then "resolvconf"
+        else "unmanaged";
+      firewall-backend = cfg.firewallBackend;
+    })
+    (mkSection "keyfile" {
+      unmanaged-devices =
+        if cfg.unmanaged == [] then null
+        else lib.concatStringsSep ";" cfg.unmanaged;
+    })
+    (mkSection "logging" {
+      audit = config.security.audit.enable;
+      level = cfg.logLevel;
+    })
+    (mkSection "connection" cfg.connectionConfig)
+    (mkSection "device" {
+      "wifi.scan-rand-mac-address" = cfg.wifi.scanRandMacAddress;
+      "wifi.backend" = cfg.wifi.backend;
+    })
+    cfg.extraConfig
+  ]);
+
+  /*
+    [network-manager]
+    Identity=unix-group:networkmanager
+    Action=org.freedesktop.NetworkManager.*
+    ResultAny=yes
+    ResultInactive=no
+    ResultActive=yes
+
+    [modem-manager]
+    Identity=unix-group:networkmanager
+    Action=org.freedesktop.ModemManager*
+    ResultAny=yes
+    ResultInactive=no
+    ResultActive=yes
+  */
+  polkitConf = ''
+    polkit.addRule(function(action, subject) {
+      if (
+        subject.isInGroup("networkmanager")
+        && (action.id.indexOf("org.freedesktop.NetworkManager.") == 0
+            || action.id.indexOf("org.freedesktop.ModemManager")  == 0
+        ))
+          { return polkit.Result.YES; }
+    });
+  '';
+
+  ns = xs: pkgs.writeText "nameservers" (
+    concatStrings (map (s: "nameserver ${s}\n") xs)
+  );
+
+  overrideNameserversScript = pkgs.writeScript "02overridedns" ''
+    #!/bin/sh
+    PATH=${with pkgs; makeBinPath [ gnused gnugrep coreutils ]}
+    tmp=$(mktemp)
+    sed '/nameserver /d' /etc/resolv.conf > $tmp
+    grep 'nameserver ' /etc/resolv.conf | \
+      grep -vf ${ns (cfg.appendNameservers ++ cfg.insertNameservers)} > $tmp.ns
+    cat $tmp ${ns cfg.insertNameservers} $tmp.ns ${ns cfg.appendNameservers} > /etc/resolv.conf
+    rm -f $tmp $tmp.ns
+  '';
+
+  dispatcherTypesSubdirMap = {
+    basic = "";
+    pre-up = "pre-up.d/";
+    pre-down = "pre-down.d/";
+  };
+
+  macAddressOpt = mkOption {
+    type = types.either types.str (types.enum ["permanent" "preserve" "random" "stable"]);
+    default = "preserve";
+    example = "00:11:22:33:44:55";
+    description = ''
+      Set the MAC address of the interface.
+      <variablelist>
+        <varlistentry>
+          <term>"XX:XX:XX:XX:XX:XX"</term>
+          <listitem><para>MAC address of the interface</para></listitem>
+        </varlistentry>
+        <varlistentry>
+          <term><literal>"permanent"</literal></term>
+          <listitem><para>Use the permanent MAC address of the device</para></listitem>
+        </varlistentry>
+        <varlistentry>
+          <term><literal>"preserve"</literal></term>
+          <listitem><para>Don’t change the MAC address of the device upon activation</para></listitem>
+        </varlistentry>
+        <varlistentry>
+          <term><literal>"random"</literal></term>
+          <listitem><para>Generate a randomized value upon each connect</para></listitem>
+        </varlistentry>
+        <varlistentry>
+          <term><literal>"stable"</literal></term>
+          <listitem><para>Generate a stable, hashed MAC address</para></listitem>
+        </varlistentry>
+      </variablelist>
+    '';
+  };
+
+in {
+
+  meta = {
+    maintainers = teams.freedesktop.members;
+  };
+
+  ###### interface
+
+  options = {
+
+    networking.networkmanager = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to use NetworkManager to obtain an IP address and other
+          configuration for all network interfaces that are not manually
+          configured. If enabled, a group <literal>networkmanager</literal>
+          will be created. Add all users that should have permission
+          to change network settings to this group.
+        '';
+      };
+
+      connectionConfig = mkOption {
+        type = with types; attrsOf (nullOr (oneOf [
+          bool
+          int
+          str
+        ]));
+        default = {};
+        description = ''
+          Configuration for the [connection] section of NetworkManager.conf.
+          Refer to
+          <link xlink:href="https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html">
+            https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html#id-1.2.3.11
+          </link>
+          or
+          <citerefentry>
+            <refentrytitle>NetworkManager.conf</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry>
+          for more information.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Configuration appended to the generated NetworkManager.conf.
+          Refer to
+          <link xlink:href="https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html">
+            https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html
+          </link>
+          or
+          <citerefentry>
+            <refentrytitle>NetworkManager.conf</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry>
+          for more information.
+        '';
+      };
+
+      unmanaged = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          List of interfaces that will not be managed by NetworkManager.
+          Interface name can be specified here, but if you need more fidelity,
+          refer to
+          <link xlink:href="https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html#device-spec">
+            https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html#device-spec
+          </link>
+          or the "Device List Format" Appendix of
+          <citerefentry>
+            <refentrytitle>NetworkManager.conf</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry>.
+        '';
+      };
+
+      packages = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        description = ''
+          Extra packages that provide NetworkManager plugins.
+        '';
+        apply = list: basePackages ++ list;
+      };
+
+      dhcp = mkOption {
+        type = types.enum [ "dhclient" "dhcpcd" "internal" ];
+        default = "internal";
+        description = ''
+          Which program (or internal library) should be used for DHCP.
+        '';
+      };
+
+      firewallBackend = mkOption {
+        type = types.enum [ "iptables" "nftables" "none" ];
+        default = "iptables";
+        description = ''
+          Which firewall backend should be used for configuring masquerading with shared mode.
+          If set to none, NetworkManager doesn't manage the configuration at all.
+        '';
+      };
+
+      logLevel = mkOption {
+        type = types.enum [ "OFF" "ERR" "WARN" "INFO" "DEBUG" "TRACE" ];
+        default = "WARN";
+        description = ''
+          Set the default logging verbosity level.
+        '';
+      };
+
+      appendNameservers = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          A list of name servers that should be appended
+          to the ones configured in NetworkManager or received by DHCP.
+        '';
+      };
+
+      insertNameservers = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          A list of name servers that should be inserted before
+          the ones configured in NetworkManager or received by DHCP.
+        '';
+      };
+
+      ethernet.macAddress = macAddressOpt;
+
+      wifi = {
+        macAddress = macAddressOpt;
+
+        backend = mkOption {
+          type = types.enum [ "wpa_supplicant" "iwd" ];
+          default = "wpa_supplicant";
+          description = ''
+            Specify the Wi-Fi backend used for the device.
+            Currently supported are <option>wpa_supplicant</option> or <option>iwd</option> (experimental).
+          '';
+        };
+
+        powersave = mkOption {
+          type = types.nullOr types.bool;
+          default = null;
+          description = ''
+            Whether to enable Wi-Fi power saving.
+          '';
+        };
+
+        scanRandMacAddress = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether to enable MAC address randomization of a Wi-Fi device
+            during scanning.
+          '';
+        };
+      };
+
+      dns = mkOption {
+        type = types.enum [ "default" "dnsmasq" "unbound" "systemd-resolved" "none" ];
+        default = "default";
+        description = ''
+          Set the DNS (<literal>resolv.conf</literal>) processing mode.
+          </para>
+          <para>
+          A description of these modes can be found in the main section of
+          <link xlink:href="https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html">
+            https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html
+          </link>
+          or in
+          <citerefentry>
+            <refentrytitle>NetworkManager.conf</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry>.
+        '';
+      };
+
+      dispatcherScripts = mkOption {
+        type = types.listOf (types.submodule {
+          options = {
+            source = mkOption {
+              type = types.path;
+              description = ''
+                Path to the hook script.
+              '';
+            };
+
+            type = mkOption {
+              type = types.enum (attrNames dispatcherTypesSubdirMap);
+              default = "basic";
+              description = ''
+                Dispatcher hook type. Look up the hooks described at
+                <link xlink:href="https://developer.gnome.org/NetworkManager/stable/NetworkManager.html">https://developer.gnome.org/NetworkManager/stable/NetworkManager.html</link>
+                and choose the type depending on the output folder.
+                You should then filter the event type (e.g., "up"/"down") from within your script.
+              '';
+            };
+          };
+        });
+        default = [];
+        example = literalExpression ''
+        [ {
+              source = pkgs.writeText "upHook" '''
+
+                if [ "$2" != "up" ]; then
+                    logger "exit: event $2 != up"
+                    exit
+                fi
+
+                # coreutils and iproute are in PATH too
+                logger "Device $DEVICE_IFACE coming up"
+            ''';
+            type = "basic";
+        } ]'';
+        description = ''
+          A list of scripts which will be executed in response to  network  events.
+        '';
+      };
+
+      enableStrongSwan = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the StrongSwan plugin.
+          </para><para>
+          If you enable this option the
+          <literal>networkmanager_strongswan</literal> plugin will be added to
+          the <option>networking.networkmanager.packages</option> option
+          so you don't need to to that yourself.
+        '';
+      };
+
+      enableFccUnlock = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable 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.
+        '';
+      };
+    };
+  };
+
+  imports = [
+    (mkRenamedOptionModule [ "networking" "networkmanager" "useDnsmasq" ] [ "networking" "networkmanager" "dns" ])
+    (mkRemovedOptionModule ["networking" "networkmanager" "dynamicHosts"] ''
+      This option was removed because 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 networking.hosts, provide
+      them via the DNS server in your network, or use environment.etc
+      to add a file into /etc/NetworkManager/dnsmasq.d reconfiguring hostsdir.
+    '')
+  ];
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = config.networking.wireless.enable == true -> cfg.unmanaged != [];
+        message = ''
+          You can not use networking.networkmanager with networking.wireless.
+          Except if you mark some interfaces as <literal>unmanaged</literal> by NetworkManager.
+        '';
+      }
+    ];
+
+    hardware.wirelessRegulatoryDatabase = true;
+
+    environment.etc = with pkgs; {
+      "NetworkManager/NetworkManager.conf".source = configFile;
+
+      "NetworkManager/VPN/nm-openvpn-service.name".source =
+        "${networkmanager-openvpn}/lib/NetworkManager/VPN/nm-openvpn-service.name";
+
+      "NetworkManager/VPN/nm-vpnc-service.name".source =
+        "${networkmanager-vpnc}/lib/NetworkManager/VPN/nm-vpnc-service.name";
+
+      "NetworkManager/VPN/nm-openconnect-service.name".source =
+        "${networkmanager-openconnect}/lib/NetworkManager/VPN/nm-openconnect-service.name";
+
+      "NetworkManager/VPN/nm-fortisslvpn-service.name".source =
+        "${networkmanager-fortisslvpn}/lib/NetworkManager/VPN/nm-fortisslvpn-service.name";
+
+      "NetworkManager/VPN/nm-l2tp-service.name".source =
+        "${networkmanager-l2tp}/lib/NetworkManager/VPN/nm-l2tp-service.name";
+
+      "NetworkManager/VPN/nm-iodine-service.name".source =
+        "${networkmanager-iodine}/lib/NetworkManager/VPN/nm-iodine-service.name";
+
+      "NetworkManager/VPN/nm-sstp-service.name".source =
+        "${networkmanager-sstp}/lib/NetworkManager/VPN/nm-sstp-service.name";
+
+      }
+      // optionalAttrs cfg.enableFccUnlock
+         {
+           "ModemManager/fcc-unlock.d".source =
+             "${pkgs.modemmanager}/share/ModemManager/fcc-unlock.available.d/*";
+         }
+      // optionalAttrs (cfg.appendNameservers != [] || cfg.insertNameservers != [])
+         {
+           "NetworkManager/dispatcher.d/02overridedns".source = overrideNameserversScript;
+         }
+      // optionalAttrs cfg.enableStrongSwan
+         {
+           "NetworkManager/VPN/nm-strongswan-service.name".source =
+             "${pkgs.networkmanager_strongswan}/lib/NetworkManager/VPN/nm-strongswan-service.name";
+         }
+      // listToAttrs (lib.imap1 (i: s:
+         {
+            name = "NetworkManager/dispatcher.d/${dispatcherTypesSubdirMap.${s.type}}03userscript${lib.fixedWidthNumber 4 i}";
+            value = { mode = "0544"; inherit (s) source; };
+         }) cfg.dispatcherScripts);
+
+    environment.systemPackages = cfg.packages;
+
+    users.groups = {
+      networkmanager.gid = config.ids.gids.networkmanager;
+      nm-openvpn.gid = config.ids.gids.nm-openvpn;
+    };
+
+    users.users = {
+      nm-openvpn = {
+        uid = config.ids.uids.nm-openvpn;
+        group = "nm-openvpn";
+        extraGroups = [ "networkmanager" ];
+      };
+      nm-iodine = {
+        isSystemUser = true;
+        group = "networkmanager";
+      };
+    };
+
+    systemd.packages = cfg.packages;
+
+    systemd.tmpfiles.rules = [
+      "d /etc/NetworkManager/system-connections 0700 root root -"
+      "d /etc/ipsec.d 0700 root root -"
+      "d /var/lib/NetworkManager-fortisslvpn 0700 root root -"
+
+      "d /var/lib/dhclient 0755 root root -"
+      "d /var/lib/misc 0755 root root -" # for dnsmasq.leases
+    ];
+
+    systemd.services.NetworkManager = {
+      wantedBy = [ "network.target" ];
+      restartTriggers = [ configFile ];
+
+      aliases = [ "dbus-org.freedesktop.NetworkManager.service" ];
+
+      serviceConfig = {
+        StateDirectory = "NetworkManager";
+        StateDirectoryMode = 755; # not sure if this really needs to be 755
+      };
+    };
+
+    systemd.services.NetworkManager-wait-online = {
+      wantedBy = [ "network-online.target" ];
+    };
+
+    systemd.services.ModemManager.aliases = [ "dbus-org.freedesktop.ModemManager1.service" ];
+
+    systemd.services.NetworkManager-dispatcher = {
+      wantedBy = [ "network.target" ];
+      restartTriggers = [ configFile overrideNameserversScript ];
+
+      # useful binaries for user-specified hooks
+      path = [ pkgs.iproute2 pkgs.util-linux pkgs.coreutils ];
+      aliases = [ "dbus-org.freedesktop.nm-dispatcher.service" ];
+    };
+
+    # Turn off NixOS' network management when networking is managed entirely by NetworkManager
+    networking = mkMerge [
+      (mkIf (!delegateWireless) {
+        useDHCP = false;
+      })
+
+      (mkIf cfg.enableStrongSwan {
+        networkmanager.packages = [ pkgs.networkmanager_strongswan ];
+      })
+
+      (mkIf enableIwd {
+        wireless.iwd.enable = true;
+      })
+
+      {
+        networkmanager.connectionConfig = {
+          "ethernet.cloned-mac-address" = cfg.ethernet.macAddress;
+          "wifi.cloned-mac-address" = cfg.wifi.macAddress;
+          "wifi.powersave" =
+            if cfg.wifi.powersave == null then null
+            else if cfg.wifi.powersave then 3
+            else 2;
+        };
+      }
+    ];
+
+    boot.kernelModules = [ "ctr" ];
+
+    security.polkit.enable = true;
+    security.polkit.extraConfig = polkitConf;
+
+    services.dbus.packages = cfg.packages
+      ++ optional cfg.enableStrongSwan pkgs.strongswanNM
+      ++ optional (cfg.dns == "dnsmasq") pkgs.dnsmasq;
+
+    services.udev.packages = cfg.packages;
+  };
+}
diff --git a/nixos/modules/services/networking/nextdns.nix b/nixos/modules/services/networking/nextdns.nix
new file mode 100644
index 00000000000..b070eeec894
--- /dev/null
+++ b/nixos/modules/services/networking/nextdns.nix
@@ -0,0 +1,44 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nextdns;
+in {
+  options = {
+    services.nextdns = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the NextDNS DNS/53 to DoH Proxy service.";
+      };
+      arguments = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "-config" "10.0.3.0/24=abcdef" ];
+        description = "Additional arguments to be passed to nextdns run.";
+      };
+    };
+  };
+
+  # https://github.com/nextdns/nextdns/blob/628ea509eaaccd27adb66337db03e5b56f6f38a8/host/service/systemd/service.go
+  config = mkIf cfg.enable {
+    systemd.services.nextdns = {
+      description = "NextDNS DNS/53 to DoH Proxy";
+      environment = {
+        SERVICE_RUN_MODE = "1";
+      };
+      startLimitIntervalSec = 5;
+      startLimitBurst = 10;
+      serviceConfig = {
+        ExecStart = "${pkgs.nextdns}/bin/nextdns run ${escapeShellArgs config.services.nextdns.arguments}";
+        RestartSec = 120;
+        LimitMEMLOCK = "infinity";
+      };
+      after = [ "network.target" ];
+      before = [ "nss-lookup.target" ];
+      wants = [ "nss-lookup.target" ];
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nftables.nix b/nixos/modules/services/networking/nftables.nix
new file mode 100644
index 00000000000..b911f97491e
--- /dev/null
+++ b/nixos/modules/services/networking/nftables.nix
@@ -0,0 +1,131 @@
+{ config, pkgs, lib, ... }:
+with lib;
+let
+  cfg = config.networking.nftables;
+in
+{
+  ###### interface
+
+  options = {
+    networking.nftables.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description =
+        ''
+          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.
+
+          Note that if you have Docker enabled you will not be able to use
+          nftables without intervention. Docker uses iptables internally to
+          setup NAT for containers. This module disables the ip_tables kernel
+          module, however Docker automatically loads the module. Please see [1]
+          for more information.
+
+          There are other programs that use iptables internally too, such as
+          libvirt. For information on how the two firewalls interact, see [2].
+
+          [1]: https://github.com/NixOS/nixpkgs/issues/24318#issuecomment-289216273
+          [2]: 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.ruleset = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        # Check out https://wiki.nftables.org/ for better documentation.
+        # Table for both IPv4 and IPv6.
+        table inet filter {
+          # Block all incomming connections traffic except SSH and "ping".
+          chain input {
+            type filter hook input priority 0;
+
+            # accept any localhost traffic
+            iifname lo accept
+
+            # accept traffic originated from us
+            ct state {established, related} accept
+
+            # ICMP
+            # routers may also want: mld-listener-query, nd-router-solicit
+            ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
+            ip protocol icmp icmp type { destination-unreachable, router-advertisement, time-exceeded, parameter-problem } accept
+
+            # allow "ping"
+            ip6 nexthdr icmpv6 icmpv6 type echo-request accept
+            ip protocol icmp icmp type echo-request accept
+
+            # accept SSH connections (required for a server)
+            tcp dport 22 accept
+
+            # count and drop any other traffic
+            counter drop
+          }
+
+          # Allow all outgoing connections.
+          chain output {
+            type filter hook output priority 0;
+            accept
+          }
+
+          chain forward {
+            type filter hook forward priority 0;
+            accept
+          }
+        }
+      '';
+      description =
+        ''
+          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.
+        '';
+    };
+    networking.nftables.rulesetFile = mkOption {
+      type = types.path;
+      default = pkgs.writeTextFile {
+        name = "nftables-rules";
+        text = cfg.ruleset;
+      };
+      defaultText = literalDocBook ''a file with the contents of <option>networking.nftables.ruleset</option>'';
+      description =
+        ''
+          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.
+        '';
+    };
+  };
+
+  ###### 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";
+    systemd.services.nftables = {
+      description = "nftables firewall";
+      before = [ "network-pre.target" ];
+      wants = [ "network-pre.target" ];
+      wantedBy = [ "multi-user.target" ];
+      reloadIfChanged = true;
+      serviceConfig = let
+        rulesScript = pkgs.writeScript "nftables-rules" ''
+          #! ${pkgs.nftables}/bin/nft -f
+          flush ruleset
+          include "${cfg.rulesetFile}"
+        '';
+      in {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStart = rulesScript;
+        ExecReload = rulesScript;
+        ExecStop = "${pkgs.nftables}/bin/nft flush ruleset";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nghttpx/backend-params-submodule.nix b/nixos/modules/services/networking/nghttpx/backend-params-submodule.nix
new file mode 100644
index 00000000000..6523f4b8b9e
--- /dev/null
+++ b/nixos/modules/services/networking/nghttpx/backend-params-submodule.nix
@@ -0,0 +1,131 @@
+{ lib, ...}:
+{ options = {
+    proto = lib.mkOption {
+      type        = lib.types.enum [ "h2" "http/1.1" ];
+      default     = "http/1.1";
+      description = ''
+        This option configures the protocol the backend server expects
+        to use.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-b
+        for more detail.
+      '';
+    };
+
+    tls = lib.mkOption {
+      type        = lib.types.bool;
+      default     = false;
+      description = ''
+        This option determines whether nghttpx will negotiate its
+        connection with a backend server using TLS or not. The burden
+        is on the backend server to provide the TLS certificate!
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-b
+        for more detail.
+      '';
+    };
+
+    sni = lib.mkOption {
+      type        = lib.types.nullOr lib.types.str;
+      default     = null;
+      description = ''
+        Override the TLS SNI field value. This value (in nghttpx)
+        defaults to the host value of the backend configuration.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-b
+        for more detail.
+      '';
+    };
+
+    fall = lib.mkOption {
+      type        = lib.types.int;
+      default     = 0;
+      description = ''
+        If nghttpx cannot connect to the backend N times in a row, the
+        backend is assumed to be offline and is excluded from load
+        balancing. If N is 0 the backend is never excluded from load
+        balancing.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-b
+        for more detail.
+      '';
+    };
+
+    rise = lib.mkOption {
+      type        = lib.types.int;
+      default     = 0;
+      description = ''
+        If the backend is excluded from load balancing, nghttpx will
+        periodically attempt to make a connection to the backend. If
+        the connection is successful N times in a row the backend is
+        re-included in load balancing. If N is 0 a backend is never
+        reconsidered for load balancing once it falls.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-b
+        for more detail.
+      '';
+    };
+
+    affinity = lib.mkOption {
+      type        = lib.types.enum [ "ip" "none" ];
+      default     = "none";
+      description = ''
+        If "ip" is given, client IP based session affinity is
+        enabled. If "none" is given, session affinity is disabled.
+
+        Session affinity is enabled (by nghttpx) per-backend
+        pattern. If at least one backend has a non-"none" affinity,
+        then session affinity is enabled for all backend servers
+        sharing the same pattern.
+
+        It is advised to set affinity on all backends explicitly if
+        session affinity is desired. The session affinity may break if
+        one of the backend gets unreachable, or backend settings are
+        reloaded or replaced by API.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-b
+        for more detail.
+      '';
+    };
+
+    dns = lib.mkOption {
+      type        = lib.types.bool;
+      default     = false;
+      description = ''
+        Name resolution of a backends host name is done at start up,
+        or configuration reload. If "dns" is true, name resolution
+        takes place dynamically.
+
+        This is useful if a backends address changes frequently. If
+        "dns" is true, name resolution of a backend's host name at
+        start up, or configuration reload is skipped.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-b
+        for more detail.
+      '';
+    };
+
+    redirect-if-not-tls = lib.mkOption {
+      type        = lib.types.bool;
+      default     = false;
+      description = ''
+        If true, a backend match requires the frontend connection be
+        TLS encrypted. If it is not, nghttpx responds to the request
+        with a 308 status code and https URI the client should use
+        instead in the Location header.
+
+        The port number in the redirect URI is 443 by default and can
+        be changed using 'services.nghttpx.redirect-https-port'
+        option.
+
+        If at least one backend has "redirect-if-not-tls" set to true,
+        this feature is enabled for all backend servers with the same
+        pattern. It is advised to set "redirect-if-no-tls" parameter
+        to all backends explicitly if this feature is desired.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-b
+        for more detail.
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nghttpx/backend-submodule.nix b/nixos/modules/services/networking/nghttpx/backend-submodule.nix
new file mode 100644
index 00000000000..eb559e926e7
--- /dev/null
+++ b/nixos/modules/services/networking/nghttpx/backend-submodule.nix
@@ -0,0 +1,50 @@
+{ lib, ... }:
+{ options = {
+    server = lib.mkOption {
+      type =
+        lib.types.either
+          (lib.types.submodule (import ./server-options.nix))
+          (lib.types.path);
+      example = {
+        host = "127.0.0.1";
+        port = 8888;
+      };
+      default = {
+        host = "127.0.0.1";
+        port = 80;
+      };
+      description = ''
+        Backend server location specified as either a host:port pair
+        or a unix domain docket.
+      '';
+    };
+
+    patterns = lib.mkOption {
+      type    = lib.types.listOf lib.types.str;
+      example = [
+        "*.host.net/v1/"
+        "host.org/v2/mypath"
+        "/somepath"
+      ];
+      default     = [];
+      description = ''
+        List of nghttpx backend patterns.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-b
+        for more information on the pattern syntax and nghttpxs behavior.
+      '';
+    };
+
+    params = lib.mkOption {
+      type    = lib.types.nullOr (lib.types.submodule (import ./backend-params-submodule.nix));
+      example = {
+        proto = "h2";
+        tls   = true;
+      };
+      default     = null;
+      description = ''
+        Parameters to configure a backend.
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nghttpx/default.nix b/nixos/modules/services/networking/nghttpx/default.nix
new file mode 100644
index 00000000000..b8a0a24e3aa
--- /dev/null
+++ b/nixos/modules/services/networking/nghttpx/default.nix
@@ -0,0 +1,118 @@
+{config, pkgs, lib, ...}:
+let
+  cfg = config.services.nghttpx;
+
+  # renderHost :: Either ServerOptions Path -> String
+  renderHost = server:
+    if builtins.isString server
+    then "unix://${server}"
+    else "${server.host},${builtins.toString server.port}";
+
+  # Filter out submodule parameters whose value is null or false or is
+  # the key _module.
+  #
+  # filterParams :: ParamsSubmodule -> ParamsSubmodule
+  filterParams = p:
+    lib.filterAttrs
+      (n: v: ("_module" != n) && (null != v) && (false != v))
+      (lib.optionalAttrs (null != p) p);
+
+  # renderBackend :: BackendSubmodule -> String
+  renderBackend = backend:
+    let
+      host = renderHost backend.server;
+      patterns = lib.concatStringsSep ":" backend.patterns;
+
+      # Render a set of backend parameters, this is somewhat
+      # complicated because nghttpx backend patterns can be entirely
+      # omitted and the params may be given as a mixed collection of
+      # 'key=val' pairs or atoms (e.g: 'proto=h2;tls')
+      params =
+        lib.mapAttrsToList
+          (n: v:
+            if builtins.isBool v
+            then n
+            else if builtins.isString v
+            then "${n}=${v}"
+            else "${n}=${builtins.toString v}")
+          (filterParams backend.params);
+
+      # NB: params are delimited by a ";" which is the same delimiter
+      # to separate the host;[pattern];[params] sections of a backend
+      sections =
+        builtins.filter (e: "" != e) ([
+          host
+          patterns
+        ]++params);
+      formattedSections = lib.concatStringsSep ";" sections;
+    in
+      "backend=${formattedSections}";
+
+  # renderFrontend :: FrontendSubmodule -> String
+  renderFrontend = frontend:
+    let
+      host   = renderHost frontend.server;
+      params0 =
+        lib.mapAttrsToList
+          (n: v: if builtins.isBool v then n else v)
+          (filterParams frontend.params);
+
+      # NB: nghttpx doesn't accept "tls", you must omit "no-tls" for
+      # the default behavior of turning on TLS.
+      params1 = lib.remove "tls" params0;
+
+      sections          = [ host] ++ params1;
+      formattedSections = lib.concatStringsSep ";" sections;
+    in
+      "frontend=${formattedSections}";
+
+  configurationFile = pkgs.writeText "nghttpx.conf" ''
+    ${lib.optionalString (null != cfg.tls) ("private-key-file="+cfg.tls.key)}
+    ${lib.optionalString (null != cfg.tls) ("certificate-file="+cfg.tls.crt)}
+
+    user=nghttpx
+
+    ${lib.concatMapStringsSep "\n" renderFrontend cfg.frontends}
+    ${lib.concatMapStringsSep "\n" renderBackend  cfg.backends}
+
+    backlog=${builtins.toString cfg.backlog}
+    backend-address-family=${cfg.backend-address-family}
+
+    workers=${builtins.toString cfg.workers}
+    rlimit-nofile=${builtins.toString cfg.rlimit-nofile}
+
+    ${lib.optionalString cfg.single-thread "single-thread=yes"}
+    ${lib.optionalString cfg.single-process "single-process=yes"}
+
+    ${cfg.extraConfig}
+  '';
+in
+{ imports = [
+    ./nghttpx-options.nix
+  ];
+
+  config = lib.mkIf cfg.enable {
+
+    users.groups.nghttpx = { };
+    users.users.nghttpx = {
+      group = config.users.groups.nghttpx.name;
+      isSystemUser = true;
+    };
+
+
+    systemd.services = {
+      nghttpx = {
+        wantedBy = [ "multi-user.target" ];
+        after    = [ "network.target" ];
+        script   = ''
+          ${pkgs.nghttp2}/bin/nghttpx --conf=${configurationFile}
+        '';
+
+        serviceConfig = {
+          Restart    = "on-failure";
+          RestartSec = 60;
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nghttpx/frontend-params-submodule.nix b/nixos/modules/services/networking/nghttpx/frontend-params-submodule.nix
new file mode 100644
index 00000000000..33c8572bd14
--- /dev/null
+++ b/nixos/modules/services/networking/nghttpx/frontend-params-submodule.nix
@@ -0,0 +1,64 @@
+{ lib, ...}:
+{ options = {
+    tls = lib.mkOption {
+      type        = lib.types.enum [ "tls" "no-tls" ];
+      default     = "tls";
+      description = ''
+        Enable or disable TLS. If true (enabled) the key and
+        certificate must be configured for nghttpx.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-f
+        for more detail.
+      '';
+    };
+
+    sni-fwd = lib.mkOption {
+      type    = lib.types.bool;
+      default = false;
+      description = ''
+        When performing a match to select a backend server, SNI host
+        name received from the client is used instead of the request
+        host. See --backend option about the pattern match.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-f
+        for more detail.
+      '';
+    };
+
+    api = lib.mkOption {
+      type        = lib.types.bool;
+      default     = false;
+      description = ''
+        Enable API access for this frontend. This enables you to
+        dynamically modify nghttpx at run-time therefore this feature
+        is disabled by default and should be turned on with care.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-f
+        for more detail.
+      '';
+    };
+
+    healthmon = lib.mkOption {
+      type        = lib.types.bool;
+      default     = false;
+      description = ''
+        Make this frontend a health monitor endpoint. Any request
+        received on this frontend is responded to with a 200 OK.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-f
+        for more detail.
+      '';
+    };
+
+    proxyproto = lib.mkOption {
+      type        = lib.types.bool;
+      default     = false;
+      description = ''
+        Accept PROXY protocol version 1 on frontend connection.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-f
+        for more detail.
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nghttpx/frontend-submodule.nix b/nixos/modules/services/networking/nghttpx/frontend-submodule.nix
new file mode 100644
index 00000000000..887ef450213
--- /dev/null
+++ b/nixos/modules/services/networking/nghttpx/frontend-submodule.nix
@@ -0,0 +1,36 @@
+{ lib, ... }:
+{ options = {
+    server = lib.mkOption {
+      type =
+        lib.types.either
+          (lib.types.submodule (import ./server-options.nix))
+          (lib.types.path);
+      example = {
+        host = "127.0.0.1";
+        port = 8888;
+      };
+      default = {
+        host = "127.0.0.1";
+        port = 80;
+      };
+      description = ''
+        Frontend server interface binding specification as either a
+        host:port pair or a unix domain docket.
+
+        NB: a host of "*" listens on all interfaces and includes IPv6
+        addresses.
+      '';
+    };
+
+    params = lib.mkOption {
+      type    = lib.types.nullOr (lib.types.submodule (import ./frontend-params-submodule.nix));
+      example = {
+        tls   = "tls";
+      };
+      default     = null;
+      description = ''
+        Parameters to configure a backend.
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nghttpx/nghttpx-options.nix b/nixos/modules/services/networking/nghttpx/nghttpx-options.nix
new file mode 100644
index 00000000000..51f1d081b97
--- /dev/null
+++ b/nixos/modules/services/networking/nghttpx/nghttpx-options.nix
@@ -0,0 +1,142 @@
+{ lib, ... }:
+{ options.services.nghttpx = {
+    enable = lib.mkEnableOption "nghttpx";
+
+    frontends = lib.mkOption {
+      type        = lib.types.listOf (lib.types.submodule (import ./frontend-submodule.nix));
+      description = ''
+        A list of frontend listener specifications.
+      '';
+      example = [
+        { server = {
+            host = "*";
+            port = 80;
+          };
+
+          params = {
+            tls = "no-tls";
+          };
+        }
+      ];
+    };
+
+    backends  = lib.mkOption {
+      type = lib.types.listOf (lib.types.submodule (import ./backend-submodule.nix));
+      description = ''
+        A list of backend specifications.
+      '';
+      example = [
+        { server = {
+            host = "172.16.0.22";
+            port = 8443;
+          };
+          patterns = [ "/" ];
+          params   = {
+            proto               = "http/1.1";
+            redirect-if-not-tls = true;
+          };
+        }
+      ];
+    };
+
+    tls = lib.mkOption {
+      type        = lib.types.nullOr (lib.types.submodule (import ./tls-submodule.nix));
+      default     = null;
+      description = ''
+        TLS certificate and key paths. Note that this does not enable
+        TLS for a frontend listener, to do so, a frontend
+        specification must set <literal>params.tls</literal> to true.
+      '';
+      example = {
+        key = "/etc/ssl/keys/server.key";
+        crt = "/etc/ssl/certs/server.crt";
+      };
+    };
+
+    extraConfig = lib.mkOption {
+      type        = lib.types.lines;
+      default     = "";
+      description = ''
+        Extra configuration options to be appended to the generated
+        configuration file.
+      '';
+    };
+
+    single-process = lib.mkOption {
+      type        = lib.types.bool;
+      default     = false;
+      description = ''
+        Run this program in a single process mode for debugging
+        purpose. Without this option, nghttpx creates at least 2
+        processes: master and worker processes. If this option is
+        used, master and worker are unified into a single
+        process. nghttpx still spawns additional process if neverbleed
+        is used. In the single process mode, the signal handling
+        feature is disabled.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx--single-process
+      '';
+    };
+
+    backlog = lib.mkOption {
+      type        = lib.types.int;
+      default     = 65536;
+      description = ''
+        Listen backlog size.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx--backlog
+      '';
+    };
+
+    backend-address-family = lib.mkOption {
+      type = lib.types.enum [
+        "auto"
+        "IPv4"
+        "IPv6"
+      ];
+      default = "auto";
+      description = ''
+        Specify address family of backend connections. If "auto" is
+        given, both IPv4 and IPv6 are considered. If "IPv4" is given,
+        only IPv4 address is considered. If "IPv6" is given, only IPv6
+        address is considered.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx--backend-address-family
+      '';
+    };
+
+    workers = lib.mkOption {
+      type        = lib.types.int;
+      default     = 1;
+      description = ''
+        Set the number of worker threads.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-n
+      '';
+    };
+
+    single-thread = lib.mkOption {
+      type        = lib.types.bool;
+      default     = false;
+      description = ''
+        Run everything in one thread inside the worker process. This
+        feature is provided for better debugging experience, or for
+        the platforms which lack thread support. If threading is
+        disabled, this option is always enabled.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx--single-thread
+      '';
+    };
+
+    rlimit-nofile = lib.mkOption {
+      type        = lib.types.int;
+      default     = 0;
+      description = ''
+        Set maximum number of open files (RLIMIT_NOFILE) to &lt;N&gt;. If 0
+        is given, nghttpx does not set the limit.
+
+        Please see https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx--rlimit-nofile
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nghttpx/server-options.nix b/nixos/modules/services/networking/nghttpx/server-options.nix
new file mode 100644
index 00000000000..ef23bfd793c
--- /dev/null
+++ b/nixos/modules/services/networking/nghttpx/server-options.nix
@@ -0,0 +1,18 @@
+{ lib, ... }:
+{ options = {
+    host = lib.mkOption {
+      type        = lib.types.str;
+      example     = "127.0.0.1";
+      description = ''
+        Server host address.
+      '';
+    };
+    port = lib.mkOption {
+      type        = lib.types.int;
+      example     = 5088;
+      description = ''
+        Server host port.
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nghttpx/tls-submodule.nix b/nixos/modules/services/networking/nghttpx/tls-submodule.nix
new file mode 100644
index 00000000000..8f3cdaae2c8
--- /dev/null
+++ b/nixos/modules/services/networking/nghttpx/tls-submodule.nix
@@ -0,0 +1,21 @@
+{lib, ...}:
+{ options = {
+    key = lib.mkOption {
+      type        = lib.types.str;
+      example     = "/etc/ssl/keys/mykeyfile.key";
+      default     = "/etc/ssl/keys/server.key";
+      description = ''
+        Path to the TLS key file.
+      '';
+    };
+
+    crt = lib.mkOption {
+      type        = lib.types.str;
+      example     = "/etc/ssl/certs/mycert.crt";
+      default     = "/etc/ssl/certs/server.crt";
+      description = ''
+        Path to the TLS certificate file.
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/ngircd.nix b/nixos/modules/services/networking/ngircd.nix
new file mode 100644
index 00000000000..c0b9c98fb4b
--- /dev/null
+++ b/nixos/modules/services/networking/ngircd.nix
@@ -0,0 +1,62 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ngircd;
+
+  configFile = pkgs.stdenv.mkDerivation {
+    name = "ngircd.conf";
+
+    text = cfg.config;
+
+    preferLocalBuild = true;
+
+    buildCommand = ''
+      echo -n "$text" > $out
+      ${cfg.package}/sbin/ngircd --config $out --configtest
+    '';
+  };
+in {
+  options = {
+    services.ngircd = {
+      enable = mkEnableOption "the ngircd IRC server";
+
+      config = mkOption {
+        description = "The ngircd configuration (see ngircd.conf(5)).";
+
+        type = types.lines;
+      };
+
+      package = mkOption {
+        description = "The ngircd package.";
+
+        type = types.package;
+
+        default = pkgs.ngircd;
+        defaultText = literalExpression "pkgs.ngircd";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    #!!! TODO: Use ExecReload (see https://github.com/NixOS/nixpkgs/issues/1988)
+    systemd.services.ngircd = {
+      description = "The ngircd IRC server";
+
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig.ExecStart = "${cfg.package}/sbin/ngircd --config ${configFile} --nodaemon";
+
+      serviceConfig.User = "ngircd";
+    };
+
+    users.users.ngircd = {
+      isSystemUser = true;
+      group = "ngircd";
+      description = "ngircd user.";
+    };
+    users.groups.ngircd = {};
+
+  };
+}
diff --git a/nixos/modules/services/networking/nix-serve.nix b/nixos/modules/services/networking/nix-serve.nix
new file mode 100644
index 00000000000..432938d59d9
--- /dev/null
+++ b/nixos/modules/services/networking/nix-serve.nix
@@ -0,0 +1,91 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nix-serve;
+in
+{
+  options = {
+    services.nix-serve = {
+      enable = mkEnableOption "nix-serve, the standalone Nix binary cache server";
+
+      port = mkOption {
+        type = types.port;
+        default = 5000;
+        description = ''
+          Port number where nix-serve will listen on.
+        '';
+      };
+
+      bindAddress = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = ''
+          IP address where nix-serve will bind its listening socket.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for nix-serve.";
+      };
+
+      secretKeyFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The path to the file used for signing derivation data.
+          Generate with:
+
+          ```
+          nix-store --generate-binary-cache-key key-name secret-key-file public-key-file
+          ```
+
+          For more details see <citerefentry><refentrytitle>nix-store</refentrytitle><manvolnum>1</manvolnum></citerefentry>.
+        '';
+      };
+
+      extraParams = mkOption {
+        type = types.separatedString " ";
+        default = "";
+        description = ''
+          Extra command line parameters for nix-serve.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.nix-serve = {
+      description = "nix-serve binary cache server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      path = [ config.nix.package.out pkgs.bzip2.bin ];
+      environment.NIX_REMOTE = "daemon";
+
+      script = ''
+        ${lib.optionalString (cfg.secretKeyFile != null) ''
+          export NIX_SECRET_KEY_FILE="$CREDENTIALS_DIRECTORY/NIX_SECRET_KEY_FILE"
+        ''}
+        exec ${pkgs.nix-serve}/bin/nix-serve --listen ${cfg.bindAddress}:${toString cfg.port} ${cfg.extraParams}
+      '';
+
+      serviceConfig = {
+        Restart = "always";
+        RestartSec = "5s";
+        User = "nix-serve";
+        Group = "nix-serve";
+        DynamicUser = true;
+        LoadCredential = lib.optionalString (cfg.secretKeyFile != null)
+          "NIX_SECRET_KEY_FILE:${cfg.secretKeyFile}";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nix-store-gcs-proxy.nix b/nixos/modules/services/networking/nix-store-gcs-proxy.nix
new file mode 100644
index 00000000000..0012302db2e
--- /dev/null
+++ b/nixos/modules/services/networking/nix-store-gcs-proxy.nix
@@ -0,0 +1,75 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  opts = { name, config, ... }: {
+    options = {
+      enable = mkOption {
+        default = true;
+        type = types.bool;
+        example = true;
+        description = "Whether to enable proxy for this bucket";
+      };
+      bucketName = mkOption {
+        type = types.str;
+        default = name;
+        example = "my-bucket-name";
+        description = "Name of Google storage bucket";
+      };
+      address = mkOption {
+        type = types.str;
+        example = "localhost:3000";
+        description = "The address of the proxy.";
+      };
+    };
+  };
+  enabledProxies = lib.filterAttrs (n: v: v.enable) config.services.nix-store-gcs-proxy;
+  mapProxies = function: lib.mkMerge (lib.mapAttrsToList function enabledProxies);
+in
+{
+  options.services.nix-store-gcs-proxy = mkOption {
+    type = types.attrsOf (types.submodule opts);
+    default = {};
+    description = ''
+      An attribute set describing an HTTP to GCS proxy that allows us to use GCS
+      bucket via HTTP protocol.
+    '';
+  };
+
+  config.systemd.services = mapProxies (name: cfg: {
+    "nix-store-gcs-proxy-${name}" = {
+      description = "A HTTP nix store that proxies requests to Google Storage";
+      wantedBy = ["multi-user.target"];
+
+      startLimitIntervalSec = 10;
+      serviceConfig = {
+        RestartSec = 5;
+        ExecStart = ''
+          ${pkgs.nix-store-gcs-proxy}/bin/nix-store-gcs-proxy \
+            --bucket-name ${cfg.bucketName} \
+            --addr ${cfg.address}
+        '';
+
+        DynamicUser = true;
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+
+        NoNewPrivileges = true;
+        LockPersonality = true;
+        RestrictRealtime = true;
+      };
+    };
+  });
+
+  meta.maintainers = [ maintainers.mrkkrp ];
+}
diff --git a/nixos/modules/services/networking/nixops-dns.nix b/nixos/modules/services/networking/nixops-dns.nix
new file mode 100644
index 00000000000..5e33d872ea4
--- /dev/null
+++ b/nixos/modules/services/networking/nixops-dns.nix
@@ -0,0 +1,78 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  pkg = pkgs.nixops-dns;
+  cfg = config.services.nixops-dns;
+in
+
+{
+  options = {
+    services.nixops-dns = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the nixops-dns resolution
+          of NixOps virtual machines via dnsmasq and fake domain name.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        description = ''
+          The user the nixops-dns daemon should run as.
+          This should be the user, which is also used for nixops and
+          have the .nixops directory in its home.
+        '';
+      };
+
+      domain = mkOption {
+        type = types.str;
+        description = ''
+          Fake domain name to resolve to NixOps virtual machines.
+
+          For example "ops" will resolve "vm.ops".
+        '';
+        default = "ops";
+      };
+
+      dnsmasq = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable dnsmasq forwarding to nixops-dns. This allows to use
+          nixops-dns for `services.nixops-dns.domain` resolution
+          while forwarding the rest of the queries to original resolvers.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.nixops-dns = {
+      description = "nixops-dns: DNS server for resolving NixOps machines";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        ExecStart="${pkg}/bin/nixops-dns --domain=.${cfg.domain}";
+      };
+    };
+
+    services.dnsmasq = mkIf cfg.dnsmasq {
+      enable = true;
+      resolveLocalQueries = true;
+      servers = [
+        "/${cfg.domain}/127.0.0.1#5300"
+      ];
+      extraConfig = ''
+        bind-interfaces
+        listen-address=127.0.0.1
+      '';
+    };
+
+  };
+}
diff --git a/nixos/modules/services/networking/nntp-proxy.nix b/nixos/modules/services/networking/nntp-proxy.nix
new file mode 100644
index 00000000000..a5973cd5933
--- /dev/null
+++ b/nixos/modules/services/networking/nntp-proxy.nix
@@ -0,0 +1,234 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inherit (pkgs) nntp-proxy;
+
+  cfg = config.services.nntp-proxy;
+
+  configBool = b: if b then "TRUE" else "FALSE";
+
+  confFile = pkgs.writeText "nntp-proxy.conf" ''
+    nntp_server:
+    {
+      # NNTP Server host and port address
+      server = "${cfg.upstreamServer}";
+      port = ${toString cfg.upstreamPort};
+      # NNTP username
+      username = "${cfg.upstreamUser}";
+      # NNTP password in clear text
+      password = "${cfg.upstreamPassword}";
+      # Maximum number of connections allowed by the NNTP
+      max_connections = ${toString cfg.upstreamMaxConnections};
+    };
+
+    proxy:
+    {
+      # Local address and port to bind to
+      bind_ip = "${cfg.listenAddress}";
+      bind_port = ${toString cfg.port};
+
+      # SSL key and cert file
+      ssl_key = "${cfg.sslKey}";
+      ssl_cert = "${cfg.sslCert}";
+
+      # prohibit users from posting
+      prohibit_posting = ${configBool cfg.prohibitPosting};
+      # Verbose levels: ERROR, WARNING, NOTICE, INFO, DEBUG
+      verbose = "${toUpper cfg.verbosity}";
+      # Password is made with: 'mkpasswd -m sha-512 <password>'
+      users = (${concatStringsSep ",\n" (mapAttrsToList (username: userConfig:
+        ''
+          {
+              username = "${username}";
+              password = "${userConfig.passwordHash}";
+              max_connections = ${toString userConfig.maxConnections};
+          }
+        '') cfg.users)});
+    };
+  '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.nntp-proxy = {
+      enable = mkEnableOption "NNTP-Proxy";
+
+      upstreamServer = mkOption {
+        type = types.str;
+        default = "";
+        example = "ssl-eu.astraweb.com";
+        description = ''
+          Upstream server address
+        '';
+      };
+
+      upstreamPort = mkOption {
+        type = types.int;
+        default = 563;
+        description = ''
+          Upstream server port
+        '';
+      };
+
+      upstreamMaxConnections = mkOption {
+        type = types.int;
+        default = 20;
+        description = ''
+          Upstream server maximum allowed concurrent connections
+        '';
+      };
+
+      upstreamUser = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Upstream server username
+        '';
+      };
+
+      upstreamPassword = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Upstream server password
+        '';
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        example = "[::]";
+        description = ''
+          Proxy listen address (IPv6 literal addresses need to be enclosed in "[" and "]" characters)
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 5555;
+        description = ''
+          Proxy listen port
+        '';
+      };
+
+      sslKey = mkOption {
+        type = types.str;
+        default = "key.pem";
+        example = "/path/to/your/key.file";
+        description = ''
+          Proxy ssl key path
+        '';
+      };
+
+      sslCert = mkOption {
+        type = types.str;
+        default = "cert.pem";
+        example = "/path/to/your/cert.file";
+        description = ''
+          Proxy ssl certificate path
+        '';
+      };
+
+      prohibitPosting = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to prohibit posting to the upstream server
+        '';
+      };
+
+      verbosity = mkOption {
+        type = types.enum [ "error" "warning" "notice" "info" "debug" ];
+        default = "info";
+        example = "error";
+        description = ''
+          Verbosity level
+        '';
+      };
+
+      users = mkOption {
+        type = types.attrsOf (types.submodule {
+          options = {
+            username = mkOption {
+              type = types.str;
+              description = ''
+                Username
+              '';
+            };
+
+            passwordHash = mkOption {
+              type = types.str;
+              example = "$6$GtzE7FrpE$wwuVgFYU.TZH4Rz.Snjxk9XGua89IeVwPQ/fEUD8eujr40q5Y021yhn0aNcsQ2Ifw.BLclyzvzgegopgKcneL0";
+              description = ''
+                SHA-512 password hash (can be generated by
+                <code>mkpasswd -m sha-512 &lt;password&gt;</code>)
+              '';
+            };
+
+            maxConnections = mkOption {
+              type = types.int;
+              default = 1;
+              description = ''
+                Maximum number of concurrent connections to the proxy for this user
+              '';
+            };
+          };
+        });
+        description = ''
+          NNTP-Proxy user configuration
+        '';
+
+        default = {};
+        example = literalExpression ''
+          {
+            "user1" = {
+              passwordHash = "$6$1l0t5Kn2Dk$appzivc./9l/kjq57eg5UCsBKlcfyCr0zNWYNerKoPsI1d7eAwiT0SVsOVx/CTgaBNT/u4fi2vN.iGlPfv1ek0";
+              maxConnections = 5;
+            };
+            "anotheruser" = {
+              passwordHash = "$6$6lwEsWB.TmsS$W7m1riUx4QrA8pKJz8hvff0dnF1NwtZXgdjmGqA1Dx2MDPj07tI9GNcb0SWlMglE.2/hBgynDdAd/XqqtRqVQ0";
+              maxConnections = 7;
+            };
+          }
+        '';
+      };
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.nntp-proxy = {
+      isSystemUser = true;
+      group = "nntp-proxy";
+      description = "NNTP-Proxy daemon user";
+    };
+    users.groups.nntp-proxy = {};
+
+    systemd.services.nntp-proxy = {
+      description = "NNTP proxy";
+      after = [ "network.target" "nss-lookup.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = { User="nntp-proxy"; };
+      serviceConfig.ExecStart = "${nntp-proxy}/bin/nntp-proxy ${confFile}";
+      preStart = ''
+        if [ ! \( -f ${cfg.sslCert} -a -f ${cfg.sslKey} \) ]; then
+          ${pkgs.openssl.bin}/bin/openssl req -subj '/CN=AutoGeneratedCert/O=NixOS Service/C=US' \
+          -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout ${cfg.sslKey} -out ${cfg.sslCert};
+        fi
+      '';
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/nomad.nix b/nixos/modules/services/networking/nomad.nix
new file mode 100644
index 00000000000..43333af5e2f
--- /dev/null
+++ b/nixos/modules/services/networking/nomad.nix
@@ -0,0 +1,178 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.nomad;
+  format = pkgs.formats.json { };
+in
+{
+  ##### interface
+  options = {
+    services.nomad = {
+      enable = mkEnableOption "Nomad, a distributed, highly available, datacenter-aware scheduler";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.nomad;
+        defaultText = literalExpression "pkgs.nomad";
+        description = ''
+          The package used for the Nomad agent and CLI.
+        '';
+      };
+
+      extraPackages = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        description = ''
+          Extra packages to add to <envar>PATH</envar> for the Nomad agent process.
+        '';
+        example = literalExpression ''
+          with pkgs; [ cni-plugins ]
+        '';
+      };
+
+      dropPrivileges = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether the nomad agent should be run as a non-root nomad user.
+        '';
+      };
+
+      enableDocker = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable Docker support. Needed for Nomad's docker driver.
+
+          Note that the docker group membership is effectively equivalent
+          to being root, see https://github.com/moby/moby/issues/9976.
+        '';
+      };
+
+      extraSettingsPaths = mkOption {
+        type = types.listOf types.path;
+        default = [ ];
+        description = ''
+          Additional settings paths used to configure nomad. These can be files or directories.
+        '';
+        example = literalExpression ''
+          [ "/etc/nomad-mutable.json" "/run/keys/nomad-with-secrets.json" "/etc/nomad/config.d" ]
+        '';
+      };
+
+      extraSettingsPlugins = mkOption {
+        type = types.listOf (types.either types.package types.path);
+        default = [ ];
+        description = ''
+          Additional plugins dir used to configure nomad.
+        '';
+        example = literalExpression ''
+          [ "<pluginDir>" "pkgs.<plugins-name>"]
+        '';
+      };
+
+
+      settings = mkOption {
+        type = format.type;
+        default = { };
+        description = ''
+          Configuration for Nomad. See the <link xlink:href="https://www.nomadproject.io/docs/configuration">documentation</link>
+          for supported values.
+
+          Notes about <literal>data_dir</literal>:
+
+          If <literal>data_dir</literal> is set to a value other than the
+          default value of <literal>"/var/lib/nomad"</literal> it is the Nomad
+          cluster manager's responsibility to make sure that this directory
+          exists and has the appropriate permissions.
+
+          Additionally, if <literal>dropPrivileges</literal> is
+          <literal>true</literal> then <literal>data_dir</literal>
+          <emphasis>cannot</emphasis> be customized. Setting
+          <literal>dropPrivileges</literal> to <literal>true</literal> enables
+          the <literal>DynamicUser</literal> feature of systemd which directly
+          manages and operates on <literal>StateDirectory</literal>.
+        '';
+        example = literalExpression ''
+          {
+            # A minimal config example:
+            server = {
+              enabled = true;
+              bootstrap_expect = 1; # for demo; no fault tolerance
+            };
+            client = {
+              enabled = true;
+            };
+          }
+        '';
+      };
+    };
+  };
+
+  ##### implementation
+  config = mkIf cfg.enable {
+    services.nomad.settings = {
+      # Agrees with `StateDirectory = "nomad"` set below.
+      data_dir = mkDefault "/var/lib/nomad";
+    };
+
+    environment = {
+      etc."nomad.json".source = format.generate "nomad.json" cfg.settings;
+      systemPackages = [ cfg.package ];
+    };
+
+    systemd.services.nomad = {
+      description = "Nomad";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+      restartTriggers = [ config.environment.etc."nomad.json".source ];
+
+      path = cfg.extraPackages ++ (with pkgs; [
+        # Client mode requires at least the following:
+        coreutils
+        iproute2
+        iptables
+      ]);
+
+      serviceConfig = mkMerge [
+        {
+          DynamicUser = cfg.dropPrivileges;
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          ExecStart = "${cfg.package}/bin/nomad agent -config=/etc/nomad.json" +
+            concatMapStrings (path: " -config=${path}") cfg.extraSettingsPaths +
+            concatMapStrings (path: " -plugin-dir=${path}/bin") cfg.extraSettingsPlugins;
+          KillMode = "process";
+          KillSignal = "SIGINT";
+          LimitNOFILE = 65536;
+          LimitNPROC = "infinity";
+          OOMScoreAdjust = -1000;
+          Restart = "on-failure";
+          RestartSec = 2;
+          TasksMax = "infinity";
+        }
+        (mkIf cfg.enableDocker {
+          SupplementaryGroups = "docker"; # space-separated string
+        })
+        (mkIf (cfg.settings.data_dir == "/var/lib/nomad") {
+          StateDirectory = "nomad";
+        })
+      ];
+
+      unitConfig = {
+        StartLimitIntervalSec = 10;
+        StartLimitBurst = 3;
+      };
+    };
+
+    assertions = [
+      {
+        assertion = cfg.dropPrivileges -> cfg.settings.data_dir == "/var/lib/nomad";
+        message = "settings.data_dir must be equal to \"/var/lib/nomad\" if dropPrivileges is true";
+      }
+    ];
+
+    # Docker support requires the Docker daemon to be running.
+    virtualisation.docker.enable = mkIf cfg.enableDocker true;
+  };
+}
diff --git a/nixos/modules/services/networking/nsd.nix b/nixos/modules/services/networking/nsd.nix
new file mode 100644
index 00000000000..a51fc534534
--- /dev/null
+++ b/nixos/modules/services/networking/nsd.nix
@@ -0,0 +1,992 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nsd;
+
+  username = "nsd";
+  stateDir = "/var/lib/nsd";
+  pidFile = stateDir + "/var/nsd.pid";
+
+  # build nsd with the options needed for the given config
+  nsdPkg = pkgs.nsd.override {
+    bind8Stats = cfg.bind8Stats;
+    ipv6 = cfg.ipv6;
+    ratelimit = cfg.ratelimit.enable;
+    rootServer = cfg.rootServer;
+    zoneStats = length (collect (x: (x.zoneStats or null) != null) cfg.zones) > 0;
+  };
+
+  mkZoneFileName = name: if name == "." then "root" else name;
+
+  # replaces include: directives for keys with fake keys for nsd-checkconf
+  injectFakeKeys = keys: concatStrings
+    (mapAttrsToList
+      (keyName: keyOptions: ''
+        fakeKey="$(${pkgs.bind}/bin/tsig-keygen -a ${escapeShellArgs [ keyOptions.algorithm keyName ]} | grep -oP "\s*secret \"\K.*(?=\";)")"
+        sed "s@^\s*include:\s*\"${stateDir}/private/${keyName}\"\$@secret: $fakeKey@" -i $out/nsd.conf
+      '')
+      keys);
+
+  nsdEnv = pkgs.buildEnv {
+    name = "nsd-env";
+
+    paths = [ configFile ]
+      ++ mapAttrsToList (name: zone: writeZoneData name zone.data) zoneConfigs;
+
+    postBuild = ''
+      echo "checking zone files"
+      cd $out/zones
+
+      for zoneFile in *; do
+        echo "|- checking zone '$out/zones/$zoneFile'"
+        ${nsdPkg}/sbin/nsd-checkzone "$zoneFile" "$zoneFile" || {
+          if grep -q \\\\\\$ "$zoneFile"; then
+            echo zone "$zoneFile" contains escaped dollar signs \\\$
+            echo Escaping them is not needed any more. Please make sure \
+                 to unescape them where they prefix a variable name.
+          fi
+
+          exit 1
+        }
+      done
+
+      echo "checking configuration file"
+      # Save original config file including key references...
+      cp $out/nsd.conf{,.orig}
+      # ...inject mock keys into config
+      ${injectFakeKeys cfg.keys}
+      # ...do the checkconf
+      ${nsdPkg}/sbin/nsd-checkconf $out/nsd.conf
+      # ... and restore original config file.
+      mv $out/nsd.conf{.orig,}
+    '';
+  };
+
+  writeZoneData = name: text: pkgs.writeTextFile {
+    name = "nsd-zone-${mkZoneFileName name}";
+    inherit text;
+    destination = "/zones/${mkZoneFileName name}";
+  };
+
+
+  # options are ordered alphanumerically by the nixos option name
+  configFile = pkgs.writeTextDir "nsd.conf" ''
+    server:
+      chroot:   "${stateDir}"
+      username: ${username}
+
+      # The directory for zonefile: files. The daemon chdirs here.
+      zonesdir: "${stateDir}"
+
+      # the list of dynamically added zones.
+      database:     "${stateDir}/var/nsd.db"
+      pidfile:      "${pidFile}"
+      xfrdfile:     "${stateDir}/var/xfrd.state"
+      xfrdir:       "${stateDir}/tmp"
+      zonelistfile: "${stateDir}/var/zone.list"
+
+      # interfaces
+    ${forEach "  ip-address: " cfg.interfaces}
+
+      ip-freebind:         ${yesOrNo  cfg.ipFreebind}
+      hide-version:        ${yesOrNo  cfg.hideVersion}
+      identity:            "${cfg.identity}"
+      ip-transparent:      ${yesOrNo  cfg.ipTransparent}
+      do-ip4:              ${yesOrNo  cfg.ipv4}
+      ipv4-edns-size:      ${toString cfg.ipv4EDNSSize}
+      do-ip6:              ${yesOrNo  cfg.ipv6}
+      ipv6-edns-size:      ${toString cfg.ipv6EDNSSize}
+      log-time-ascii:      ${yesOrNo  cfg.logTimeAscii}
+      ${maybeString "nsid: " cfg.nsid}
+      port:                ${toString cfg.port}
+      reuseport:           ${yesOrNo  cfg.reuseport}
+      round-robin:         ${yesOrNo  cfg.roundRobin}
+      server-count:        ${toString cfg.serverCount}
+      ${maybeToString "statistics: " cfg.statistics}
+      tcp-count:           ${toString cfg.tcpCount}
+      tcp-query-count:     ${toString cfg.tcpQueryCount}
+      tcp-timeout:         ${toString cfg.tcpTimeout}
+      verbosity:           ${toString cfg.verbosity}
+      ${maybeString "version: " cfg.version}
+      xfrd-reload-timeout: ${toString cfg.xfrdReloadTimeout}
+      zonefiles-check:     ${yesOrNo  cfg.zonefilesCheck}
+
+      ${maybeString "rrl-ipv4-prefix-length: " cfg.ratelimit.ipv4PrefixLength}
+      ${maybeString "rrl-ipv6-prefix-length: " cfg.ratelimit.ipv6PrefixLength}
+      rrl-ratelimit:           ${toString cfg.ratelimit.ratelimit}
+      ${maybeString "rrl-slip: "               cfg.ratelimit.slip}
+      rrl-size:                ${toString cfg.ratelimit.size}
+      rrl-whitelist-ratelimit: ${toString cfg.ratelimit.whitelistRatelimit}
+
+    ${keyConfigFile}
+
+    remote-control:
+      control-enable:    ${yesOrNo  cfg.remoteControl.enable}
+      control-key-file:  "${cfg.remoteControl.controlKeyFile}"
+      control-cert-file: "${cfg.remoteControl.controlCertFile}"
+    ${forEach "  control-interface: " cfg.remoteControl.interfaces}
+      control-port:      ${toString cfg.remoteControl.port}
+      server-key-file:   "${cfg.remoteControl.serverKeyFile}"
+      server-cert-file:  "${cfg.remoteControl.serverCertFile}"
+
+    ${concatStrings (mapAttrsToList zoneConfigFile zoneConfigs)}
+
+    ${cfg.extraConfig}
+  '';
+
+  yesOrNo = b: if b then "yes" else "no";
+  maybeString = prefix: x: if x == null then "" else ''${prefix} "${x}"'';
+  maybeToString = prefix: x: if x == null then "" else ''${prefix} ${toString x}'';
+  forEach = pre: l: concatMapStrings (x: pre + x + "\n") l;
+
+
+  keyConfigFile = concatStrings (mapAttrsToList (keyName: keyOptions: ''
+    key:
+      name:      "${keyName}"
+      algorithm: "${keyOptions.algorithm}"
+      include:   "${stateDir}/private/${keyName}"
+  '') cfg.keys);
+
+  copyKeys = concatStrings (mapAttrsToList (keyName: keyOptions: ''
+    secret=$(cat "${keyOptions.keyFile}")
+    dest="${stateDir}/private/${keyName}"
+    echo "  secret: \"$secret\"" > "$dest"
+    chown ${username}:${username} "$dest"
+    chmod 0400 "$dest"
+  '') cfg.keys);
+
+
+  # options are ordered alphanumerically by the nixos option name
+  zoneConfigFile = name: zone: ''
+    zone:
+      name:         "${name}"
+      zonefile:     "${stateDir}/zones/${mkZoneFileName name}"
+      ${maybeString "outgoing-interface: " zone.outgoingInterface}
+    ${forEach     "  rrl-whitelist: "      zone.rrlWhitelist}
+      ${maybeString "zonestats: "          zone.zoneStats}
+
+      ${maybeToString "max-refresh-time: " zone.maxRefreshSecs}
+      ${maybeToString "min-refresh-time: " zone.minRefreshSecs}
+      ${maybeToString "max-retry-time:   " zone.maxRetrySecs}
+      ${maybeToString "min-retry-time:   " zone.minRetrySecs}
+
+      allow-axfr-fallback: ${yesOrNo       zone.allowAXFRFallback}
+    ${forEach     "  allow-notify: "       zone.allowNotify}
+    ${forEach     "  request-xfr: "        zone.requestXFR}
+
+    ${forEach     "  notify: "             zone.notify}
+      notify-retry:                        ${toString zone.notifyRetry}
+    ${forEach     "  provide-xfr: "        zone.provideXFR}
+  '';
+
+  zoneConfigs = zoneConfigs' {} "" { children = cfg.zones; };
+
+  zoneConfigs' = parent: name: zone:
+    if !(zone ? children) || zone.children == null || zone.children == { }
+      # leaf -> actual zone
+      then listToAttrs [ (nameValuePair name (parent // zone)) ]
+
+      # fork -> pattern
+      else zipAttrsWith (name: head) (
+        mapAttrsToList (name: child: zoneConfigs' (parent // zone // { children = {}; }) name child)
+                       zone.children
+      );
+
+  # options are ordered alphanumerically
+  zoneOptions = types.submodule {
+    options = {
+
+      allowAXFRFallback = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          If NSD as secondary server should be allowed to AXFR if the primary
+          server does not allow IXFR.
+        '';
+      };
+
+      allowNotify = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "192.0.2.0/24 NOKEY" "10.0.0.1-10.0.0.5 my_tsig_key_name"
+                    "10.0.3.4&255.255.0.0 BLOCKED"
+                  ];
+        description = ''
+          Listed primary servers are allowed to notify this secondary server.
+          <screen><![CDATA[
+          Format: <ip> <key-name | NOKEY | BLOCKED>
+
+          <ip> either a plain IPv4/IPv6 address or range. Valid patters for ranges:
+          * 10.0.0.0/24            # via subnet size
+          * 10.0.0.0&255.255.255.0 # via subnet mask
+          * 10.0.0.1-10.0.0.254    # via range
+
+          A optional port number could be added with a '@':
+          * 2001:1234::1@1234
+
+          <key-name | NOKEY | BLOCKED>
+          * <key-name> will use the specified TSIG key
+          * NOKEY      no TSIG signature is required
+          * BLOCKED    notifies from non-listed or blocked IPs will be ignored
+          * ]]></screen>
+        '';
+      };
+
+      children = mkOption {
+        # TODO: This relies on the fact that `types.anything` doesn't set any
+        # values of its own to any defaults, because in the above zoneConfigs',
+        # values from children override ones from parents, but only if the
+        # attributes are defined. Because of this, we can't replace the element
+        # type here with `zoneConfigs`, since that would set all the attributes
+        # to default values, breaking the parent inheriting function.
+        type = types.attrsOf types.anything;
+        default = {};
+        description = ''
+          Children zones inherit all options of their parents. Attributes
+          defined in a child will overwrite the ones of its parent. Only
+          leaf zones will be actually served. This way it's possible to
+          define maybe zones which share most attributes without
+          duplicating everything. This mechanism replaces nsd's patterns
+          in a save and functional way.
+        '';
+      };
+
+      data = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          The actual zone data. This is the content of your zone file.
+          Use imports or pkgs.lib.readFile if you don't want this data in your config file.
+        '';
+      };
+
+      dnssec = mkEnableOption "DNSSEC";
+
+      dnssecPolicy = {
+        algorithm = mkOption {
+          type = types.str;
+          default = "RSASHA256";
+          description = "Which algorithm to use for DNSSEC";
+        };
+        keyttl = mkOption {
+          type = types.str;
+          default = "1h";
+          description = "TTL for dnssec records";
+        };
+        coverage = mkOption {
+          type = types.str;
+          default = "1y";
+          description = ''
+            The length of time to ensure that keys will be correct; no action will be taken to create new keys to be activated after this time.
+          '';
+        };
+        zsk = mkOption {
+          type = keyPolicy;
+          default = { keySize = 2048;
+                      prePublish = "1w";
+                      postPublish = "1w";
+                      rollPeriod = "1mo";
+                    };
+          description = "Key policy for zone signing keys";
+        };
+        ksk = mkOption {
+          type = keyPolicy;
+          default = { keySize = 4096;
+                      prePublish = "1mo";
+                      postPublish = "1mo";
+                      rollPeriod = "0";
+                    };
+          description = "Key policy for key signing keys";
+        };
+      };
+
+      maxRefreshSecs = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          Limit refresh time for secondary zones. This is the timer which
+          checks to see if the zone has to be refetched when it expires.
+          Normally the value from the SOA record is used, but this  option
+          restricts that value.
+        '';
+      };
+
+      minRefreshSecs = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          Limit refresh time for secondary zones.
+        '';
+      };
+
+      maxRetrySecs = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          Limit retry time for secondary zones. This is the timeout after
+          a failed fetch attempt for the zone. Normally the value from
+          the SOA record is used, but this option restricts that value.
+        '';
+      };
+
+      minRetrySecs = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          Limit retry time for secondary zones.
+        '';
+      };
+
+
+      notify = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "10.0.0.1@3721 my_key" "::5 NOKEY" ];
+        description = ''
+          This primary server will notify all given secondary servers about
+          zone changes.
+          <screen><![CDATA[
+          Format: <ip> <key-name | NOKEY>
+
+          <ip> a plain IPv4/IPv6 address with on optional port number (ip@port)
+
+          <key-name | NOKEY>
+          * <key-name> sign notifies with the specified key
+          * NOKEY      don't sign notifies
+          ]]></screen>
+        '';
+      };
+
+      notifyRetry = mkOption {
+        type = types.int;
+        default = 5;
+        description = ''
+          Specifies the number of retries for failed notifies. Set this along with notify.
+        '';
+      };
+
+      outgoingInterface = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "2000::1@1234";
+        description = ''
+          This address will be used for zone-transfere requests if configured
+          as a secondary server or notifications in case of a primary server.
+          Supply either a plain IPv4 or IPv6 address with an optional port
+          number (ip@port).
+        '';
+      };
+
+      provideXFR = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "192.0.2.0/24 NOKEY" "192.0.2.0/24 my_tsig_key_name" ];
+        description = ''
+          Allow these IPs and TSIG to transfer zones, addr TSIG|NOKEY|BLOCKED
+          address range 192.0.2.0/24, 1.2.3.4&amp;255.255.0.0, 3.0.2.20-3.0.2.40
+        '';
+      };
+
+      requestXFR = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Format: <code>[AXFR|UDP] &lt;ip-address&gt; &lt;key-name | NOKEY&gt;</code>
+        '';
+      };
+
+      rrlWhitelist = mkOption {
+        type = with types; listOf (enum [ "nxdomain" "error" "referral" "any" "rrsig" "wildcard" "nodata" "dnskey" "positive" "all" ]);
+        default = [];
+        description = ''
+          Whitelists the given rrl-types.
+        '';
+      };
+
+      zoneStats = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "%s";
+        description = ''
+          When set to something distinct to null NSD is able to collect
+          statistics per zone. All statistics of this zone(s) will be added
+          to the group specified by this given name. Use "%s" to use the zones
+          name as the group. The groups are output from nsd-control stats
+          and stats_noreset.
+        '';
+      };
+    };
+  };
+
+  keyPolicy = types.submodule {
+    options = {
+      keySize = mkOption {
+        type = types.int;
+        description = "Key size in bits";
+      };
+      prePublish = mkOption {
+        type = types.str;
+        description = "How long in advance to publish new keys";
+      };
+      postPublish = mkOption {
+        type = types.str;
+        description = "How long after deactivation to keep a key in the zone";
+      };
+      rollPeriod = mkOption {
+        type = types.str;
+        description = "How frequently to change keys";
+      };
+    };
+  };
+
+  dnssecZones = (filterAttrs (n: v: if v ? dnssec then v.dnssec else false) zoneConfigs);
+
+  dnssec = dnssecZones != {};
+
+  dnssecTools = pkgs.bind.override { enablePython = true; };
+
+  signZones = optionalString dnssec ''
+    mkdir -p ${stateDir}/dnssec
+    chown ${username}:${username} ${stateDir}/dnssec
+    chmod 0600 ${stateDir}/dnssec
+
+    ${concatStrings (mapAttrsToList signZone dnssecZones)}
+  '';
+  signZone = name: zone: ''
+    ${dnssecTools}/bin/dnssec-keymgr -g ${dnssecTools}/bin/dnssec-keygen -s ${dnssecTools}/bin/dnssec-settime -K ${stateDir}/dnssec -c ${policyFile name zone.dnssecPolicy} ${name}
+    ${dnssecTools}/bin/dnssec-signzone -S -K ${stateDir}/dnssec -o ${name} -O full -N date ${stateDir}/zones/${name}
+    ${nsdPkg}/sbin/nsd-checkzone ${name} ${stateDir}/zones/${name}.signed && mv -v ${stateDir}/zones/${name}.signed ${stateDir}/zones/${name}
+  '';
+  policyFile = name: policy: pkgs.writeText "${name}.policy" ''
+    zone ${name} {
+      algorithm ${policy.algorithm};
+      key-size zsk ${toString policy.zsk.keySize};
+      key-size ksk ${toString policy.ksk.keySize};
+      keyttl ${policy.keyttl};
+      pre-publish zsk ${policy.zsk.prePublish};
+      pre-publish ksk ${policy.ksk.prePublish};
+      post-publish zsk ${policy.zsk.postPublish};
+      post-publish ksk ${policy.ksk.postPublish};
+      roll-period zsk ${policy.zsk.rollPeriod};
+      roll-period ksk ${policy.ksk.rollPeriod};
+      coverage ${policy.coverage};
+    };
+  '';
+in
+{
+  # options are ordered alphanumerically
+  options.services.nsd = {
+
+    enable = mkEnableOption "NSD authoritative DNS server";
+
+    bind8Stats = mkEnableOption "BIND8 like statistics";
+
+    dnssecInterval = mkOption {
+      type = types.str;
+      default = "1h";
+      description = ''
+        How often to check whether dnssec key rollover is required
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Extra nsd config.
+      '';
+    };
+
+    hideVersion = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether NSD should answer VERSION.BIND and VERSION.SERVER CHAOS class queries.
+      '';
+    };
+
+    identity = mkOption {
+      type = types.str;
+      default = "unidentified server";
+      description = ''
+        Identify the server (CH TXT ID.SERVER entry).
+      '';
+    };
+
+    interfaces = mkOption {
+      type = types.listOf types.str;
+      default = [ "127.0.0.0" "::1" ];
+      description = ''
+        What addresses the server should listen to.
+      '';
+    };
+
+    ipFreebind = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to bind to nonlocal addresses and interfaces that are down.
+        Similar to ip-transparent.
+      '';
+    };
+
+    ipTransparent = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Allow binding to non local addresses.
+      '';
+    };
+
+    ipv4 = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to listen on IPv4 connections.
+      '';
+    };
+
+    ipv4EDNSSize = mkOption {
+      type = types.int;
+      default = 4096;
+      description = ''
+        Preferred EDNS buffer size for IPv4.
+      '';
+    };
+
+    ipv6 = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to listen on IPv6 connections.
+      '';
+    };
+
+    ipv6EDNSSize = mkOption {
+      type = types.int;
+      default = 4096;
+      description = ''
+        Preferred EDNS buffer size for IPv6.
+      '';
+    };
+
+    logTimeAscii = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Log time in ascii, if false then in unix epoch seconds.
+      '';
+    };
+
+    nsid = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        NSID identity (hex string, or "ascii_somestring").
+      '';
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = 53;
+      description = ''
+        Port the service should bind do.
+      '';
+    };
+
+    reuseport = mkOption {
+      type = types.bool;
+      default = pkgs.stdenv.isLinux;
+      defaultText = literalExpression "pkgs.stdenv.isLinux";
+      description = ''
+        Whether to enable SO_REUSEPORT on all used sockets. This lets multiple
+        processes bind to the same port. This speeds up operation especially
+        if the server count is greater than one and makes fast restarts less
+        prone to fail
+      '';
+    };
+
+    rootServer = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether this server will be a root server (a DNS root server, you
+        usually don't want that).
+      '';
+    };
+
+    roundRobin = mkEnableOption "round robin rotation of records";
+
+    serverCount = mkOption {
+      type = types.int;
+      default = 1;
+      description = ''
+        Number of NSD servers to fork. Put the number of CPUs to use here.
+      '';
+    };
+
+    statistics = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      description = ''
+        Statistics are produced every number of seconds. Prints to log.
+        If null no statistics are logged.
+      '';
+    };
+
+    tcpCount = mkOption {
+      type = types.int;
+      default = 100;
+      description = ''
+        Maximum number of concurrent TCP connections per server.
+      '';
+    };
+
+    tcpQueryCount = mkOption {
+      type = types.int;
+      default = 0;
+      description = ''
+        Maximum number of queries served on a single TCP connection.
+        0 means no maximum.
+      '';
+    };
+
+    tcpTimeout = mkOption {
+      type = types.int;
+      default = 120;
+      description = ''
+        TCP timeout in seconds.
+      '';
+    };
+
+    verbosity = mkOption {
+      type = types.int;
+      default = 0;
+      description = ''
+        Verbosity level.
+      '';
+    };
+
+    version = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The version string replied for CH TXT version.server and version.bind
+        queries. Will use the compiled package version on null.
+        See hideVersion for enabling/disabling this responses.
+      '';
+    };
+
+    xfrdReloadTimeout = mkOption {
+      type = types.int;
+      default = 1;
+      description = ''
+        Number of seconds between reloads triggered by xfrd.
+      '';
+    };
+
+    zonefilesCheck = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to check mtime of all zone files on start and sighup.
+      '';
+    };
+
+
+    keys = mkOption {
+      type = types.attrsOf (types.submodule {
+        options = {
+
+          algorithm = mkOption {
+            type = types.str;
+            default = "hmac-sha256";
+            description = ''
+              Authentication algorithm for this key.
+            '';
+          };
+
+          keyFile = mkOption {
+            type = types.path;
+            description = ''
+              Path to the file which contains the actual base64 encoded
+              key. The key will be copied into "${stateDir}/private" before
+              NSD starts. The copied file is only accessibly by the NSD
+              user.
+            '';
+          };
+
+        };
+      });
+      default = {};
+      example = literalExpression ''
+        { "tsig.example.org" = {
+            algorithm = "hmac-md5";
+            keyFile = "/path/to/my/key";
+          };
+        }
+      '';
+      description = ''
+        Define your TSIG keys here.
+      '';
+    };
+
+
+    ratelimit = {
+
+      enable = mkEnableOption "ratelimit capabilities";
+
+      ipv4PrefixLength = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          IPv4 prefix length. Addresses are grouped by netblock.
+        '';
+      };
+
+      ipv6PrefixLength = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          IPv6 prefix length. Addresses are grouped by netblock.
+        '';
+      };
+
+      ratelimit = mkOption {
+        type = types.int;
+        default = 200;
+        description = ''
+          Max qps allowed from any query source.
+          0 means unlimited. With an verbosity of 2 blocked and
+          unblocked subnets will be logged.
+        '';
+      };
+
+      slip = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          Number of packets that get discarded before replying a SLIP response.
+          0 disables SLIP responses. 1 will make every response a SLIP response.
+        '';
+      };
+
+      size = mkOption {
+        type = types.int;
+        default = 1000000;
+        description = ''
+          Size of the hashtable. More buckets use more memory but lower
+          the chance of hash hash collisions.
+        '';
+      };
+
+      whitelistRatelimit = mkOption {
+        type = types.int;
+        default = 2000;
+        description = ''
+          Max qps allowed from whitelisted sources.
+          0 means unlimited. Set the rrl-whitelist option for specific
+          queries to apply this limit instead of the default to them.
+        '';
+      };
+
+    };
+
+
+    remoteControl = {
+
+      enable = mkEnableOption "remote control via nsd-control";
+
+      controlCertFile = mkOption {
+        type = types.path;
+        default = "/etc/nsd/nsd_control.pem";
+        description = ''
+          Path to the client certificate signed with the server certificate.
+          This file is used by nsd-control and generated by nsd-control-setup.
+        '';
+      };
+
+      controlKeyFile = mkOption {
+        type = types.path;
+        default = "/etc/nsd/nsd_control.key";
+        description = ''
+          Path to the client private key, which is used by nsd-control
+          but not by the server. This file is generated by nsd-control-setup.
+        '';
+      };
+
+      interfaces = mkOption {
+        type = types.listOf types.str;
+        default = [ "127.0.0.1" "::1" ];
+        description = ''
+          Which interfaces NSD should bind to for remote control.
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8952;
+        description = ''
+          Port number for remote control operations (uses TLS over TCP).
+        '';
+      };
+
+      serverCertFile = mkOption {
+        type = types.path;
+        default = "/etc/nsd/nsd_server.pem";
+        description = ''
+          Path to the server self signed certificate, which is used by the server
+          but and by nsd-control. This file is generated by nsd-control-setup.
+        '';
+      };
+
+      serverKeyFile = mkOption {
+        type = types.path;
+        default = "/etc/nsd/nsd_server.key";
+        description = ''
+          Path to the server private key, which is used by the server
+          but not by nsd-control. This file is generated by nsd-control-setup.
+        '';
+      };
+
+    };
+
+    zones = mkOption {
+      type = types.attrsOf zoneOptions;
+      default = {};
+      example = literalExpression ''
+        { "serverGroup1" = {
+            provideXFR = [ "10.1.2.3 NOKEY" ];
+            children = {
+              "example.com." = {
+                data = '''
+                  $ORIGIN example.com.
+                  $TTL    86400
+                  @ IN SOA a.ns.example.com. admin.example.com. (
+                  ...
+                ''';
+              };
+              "example.org." = {
+                data = '''
+                  $ORIGIN example.org.
+                  $TTL    86400
+                  @ IN SOA a.ns.example.com. admin.example.com. (
+                  ...
+                ''';
+              };
+            };
+          };
+
+          "example.net." = {
+            provideXFR = [ "10.3.2.1 NOKEY" ];
+            data = '''
+              ...
+            ''';
+          };
+        }
+      '';
+      description = ''
+        Define your zones here. Zones can cascade other zones and therefore
+        inherit settings from parent zones. Look at the definition of
+        children to learn about inheritance and child zones.
+        The given example will define 3 zones (example.(com|org|net).). Both
+        example.com. and example.org. inherit their configuration from
+        serverGroup1.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = singleton {
+      assertion = zoneConfigs ? "." -> cfg.rootServer;
+      message = "You have a root zone configured. If this is really what you "
+              + "want, please enable 'services.nsd.rootServer'.";
+    };
+
+    environment = {
+      systemPackages = [ nsdPkg ];
+      etc."nsd/nsd.conf".source = "${configFile}/nsd.conf";
+    };
+
+    users.groups.${username}.gid = config.ids.gids.nsd;
+
+    users.users.${username} = {
+      description = "NSD service user";
+      home = stateDir;
+      createHome  = true;
+      uid = config.ids.uids.nsd;
+      group = username;
+    };
+
+    systemd.services.nsd = {
+      description = "NSD authoritative only domain name service";
+
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      startLimitBurst = 4;
+      startLimitIntervalSec = 5 * 60;  # 5 mins
+      serviceConfig = {
+        ExecStart = "${nsdPkg}/sbin/nsd -d -c ${nsdEnv}/nsd.conf";
+        StandardError = "null";
+        PIDFile = pidFile;
+        Restart = "always";
+        RestartSec = "4s";
+      };
+
+      preStart = ''
+        rm -Rf "${stateDir}/private/"
+        rm -Rf "${stateDir}/tmp/"
+
+        mkdir -m 0700 -p "${stateDir}/private"
+        mkdir -m 0700 -p "${stateDir}/tmp"
+        mkdir -m 0700 -p "${stateDir}/var"
+
+        cat > "${stateDir}/don't touch anything in here" << EOF
+        Everything in this directory except NSD's state in var and dnssec
+        is automatically generated and will be purged and redeployed by
+        the nsd.service pre-start script.
+        EOF
+
+        chown ${username}:${username} -R "${stateDir}/private"
+        chown ${username}:${username} -R "${stateDir}/tmp"
+        chown ${username}:${username} -R "${stateDir}/var"
+
+        rm -rf "${stateDir}/zones"
+        cp -rL "${nsdEnv}/zones" "${stateDir}/zones"
+
+        ${copyKeys}
+      '';
+    };
+
+    systemd.timers.nsd-dnssec = mkIf dnssec {
+      description = "Automatic DNSSEC key rollover";
+
+      wantedBy = [ "nsd.service" ];
+
+      timerConfig = {
+        OnActiveSec = cfg.dnssecInterval;
+        OnUnitActiveSec = cfg.dnssecInterval;
+      };
+    };
+
+    systemd.services.nsd-dnssec = mkIf dnssec {
+      description = "DNSSEC key rollover";
+
+      wantedBy = [ "nsd.service" ];
+      before = [ "nsd.service" ];
+
+      script = signZones;
+
+      postStop = ''
+        /run/current-system/systemd/bin/systemctl kill -s SIGHUP nsd.service
+      '';
+    };
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ hrdinka ];
+}
diff --git a/nixos/modules/services/networking/ntopng.nix b/nixos/modules/services/networking/ntopng.nix
new file mode 100644
index 00000000000..022fc923eda
--- /dev/null
+++ b/nixos/modules/services/networking/ntopng.nix
@@ -0,0 +1,160 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.ntopng;
+  opt = options.services.ntopng;
+
+  createRedis = cfg.redis.createInstance != null;
+  redisService =
+    if cfg.redis.createInstance == "" then
+      "redis.service"
+    else
+      "redis-${cfg.redis.createInstance}.service";
+
+  configFile = if cfg.configText != "" then
+    pkgs.writeText "ntopng.conf" ''
+      ${cfg.configText}
+    ''
+    else
+    pkgs.writeText "ntopng.conf" ''
+      ${concatStringsSep " " (map (e: "--interface=" + e) cfg.interfaces)}
+      --http-port=${toString cfg.httpPort}
+      --redis=${cfg.redis.address}
+      --data-dir=/var/lib/ntopng
+      --user=ntopng
+      ${cfg.extraConfig}
+    '';
+
+in
+
+{
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "ntopng" "http-port" ] [ "services" "ntopng" "httpPort" ])
+  ];
+
+  options = {
+
+    services.ntopng = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable ntopng, a high-speed web-based traffic analysis and flow
+          collection tool.
+
+          With the default configuration, ntopng monitors all network
+          interfaces and displays its findings at http://localhost:''${toString
+          config.${opt.http-port}}. Default username and password is admin/admin.
+
+          See the ntopng(8) manual page and http://www.ntop.org/products/ntop/
+          for more info.
+
+          Note that enabling ntopng will also enable redis (key-value
+          database server) for persistent data storage.
+        '';
+      };
+
+      interfaces = mkOption {
+        default = [ "any" ];
+        example = [ "eth0" "wlan0" ];
+        type = types.listOf types.str;
+        description = ''
+          List of interfaces to monitor. Use "any" to monitor all interfaces.
+        '';
+      };
+
+      httpPort = mkOption {
+        default = 3000;
+        type = types.int;
+        description = ''
+          Sets the HTTP port of the embedded web server.
+        '';
+      };
+
+      redis.address = mkOption {
+        type = types.str;
+        example = literalExpression "config.services.redis.ntopng.unixSocket";
+        description = ''
+          Redis address - may be a Unix socket or a network host and port.
+        '';
+      };
+
+      redis.createInstance = mkOption {
+        type = types.nullOr types.str;
+        default = if versionAtLeast config.system.stateVersion "22.05" then "ntopng" else "";
+        description = ''
+          Local Redis instance name. Set to <literal>null</literal> to disable
+          local Redis instance. Defaults to <literal>""</literal> for
+          <literal>system.stateVersion</literal> older than 22.05.
+        '';
+      };
+
+      configText = mkOption {
+        default = "";
+        example = ''
+          --interface=any
+          --http-port=3000
+          --disable-login
+        '';
+        type = types.lines;
+        description = ''
+          Overridable configuration file contents to use for ntopng. By
+          default, use the contents automatically generated by NixOS.
+        '';
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Configuration lines that will be appended to the generated ntopng
+          configuration file. Note that this mechanism does not work when the
+          manual <option>configText</option> option is used.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    # ntopng uses redis for data storage
+    services.ntopng.redis.address =
+      mkIf createRedis config.services.redis.servers.${cfg.redis.createInstance}.unixSocket;
+
+    services.redis.servers = mkIf createRedis {
+      ${cfg.redis.createInstance} = {
+        enable = true;
+        user = mkIf (cfg.redis.createInstance == "ntopng") "ntopng";
+      };
+    };
+
+    # nice to have manual page and ntopng command in PATH
+    environment.systemPackages = [ pkgs.ntopng ];
+
+    systemd.tmpfiles.rules = [ "d /var/lib/ntopng 0700 ntopng ntopng -" ];
+
+    systemd.services.ntopng = {
+      description = "Ntopng Network Monitor";
+      requires = optional createRedis redisService;
+      after = [ "network.target" ] ++ optional createRedis redisService;
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig.ExecStart = "${pkgs.ntopng}/bin/ntopng ${configFile}";
+      unitConfig.Documentation = "man:ntopng(8)";
+    };
+
+    users.extraUsers.ntopng = {
+      group = "ntopng";
+      isSystemUser = true;
+    };
+
+    users.extraGroups.ntopng = { };
+  };
+
+}
diff --git a/nixos/modules/services/networking/ntp/chrony.nix b/nixos/modules/services/networking/ntp/chrony.nix
new file mode 100644
index 00000000000..34728455a21
--- /dev/null
+++ b/nixos/modules/services/networking/ntp/chrony.nix
@@ -0,0 +1,178 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.chrony;
+  chronyPkg = cfg.package;
+
+  stateDir = cfg.directory;
+  driftFile = "${stateDir}/chrony.drift";
+  keyFile = "${stateDir}/chrony.keys";
+
+  configFile = pkgs.writeText "chrony.conf" ''
+    ${concatMapStringsSep "\n" (server: "server " + server + " " + cfg.serverOption + optionalString (cfg.enableNTS) " nts") cfg.servers}
+
+    ${optionalString
+      (cfg.initstepslew.enabled && (cfg.servers != []))
+      "initstepslew ${toString cfg.initstepslew.threshold} ${concatStringsSep " " cfg.servers}"
+    }
+
+    driftfile ${driftFile}
+    keyfile ${keyFile}
+    ${optionalString (cfg.enableNTS) "ntsdumpdir ${stateDir}"}
+
+    ${optionalString (!config.time.hardwareClockInLocalTime) "rtconutc"}
+
+    ${cfg.extraConfig}
+  '';
+
+  chronyFlags = "-n -m -u chrony -f ${configFile} ${toString cfg.extraFlags}";
+in
+{
+  options = {
+    services.chrony = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to synchronise your machine's time using chrony.
+          Make sure you disable NTP if you enable this service.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.chrony;
+        defaultText = literalExpression "pkgs.chrony";
+        description = ''
+          Which chrony package to use.
+        '';
+      };
+
+      servers = mkOption {
+        default = config.networking.timeServers;
+        defaultText = literalExpression "config.networking.timeServers";
+        type = types.listOf types.str;
+        description = ''
+          The set of NTP servers from which to synchronise.
+        '';
+      };
+
+      serverOption = mkOption {
+        default = "iburst";
+        type = types.enum [ "iburst" "offline" ];
+        description = ''
+          Set option for server directives.
+
+          Use "iburst" to rapidly poll on startup. Recommended if your machine
+          is consistently online.
+
+          Use "offline" to prevent polling on startup. Recommended if your
+          machine boots offline or is otherwise frequently offline.
+        '';
+      };
+
+      enableNTS = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Network Time Security authentication.
+          Make sure it is supported by your selected NTP server(s).
+        '';
+      };
+
+      initstepslew = {
+        enabled = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Allow chronyd to make a rapid measurement of the system clock error
+            at boot time, and to correct the system clock by stepping before
+            normal operation begins.
+          '';
+        };
+
+        threshold = mkOption {
+          type = types.either types.float types.int;
+          default = 1000; # by default, same threshold as 'ntpd -g' (1000s)
+          description = ''
+            The threshold of system clock error (in seconds) above which the
+            clock will be stepped. If the correction required is less than the
+            threshold, a slew is used instead.
+          '';
+        };
+      };
+
+      directory = mkOption {
+        type = types.str;
+        default = "/var/lib/chrony";
+        description = "Directory where chrony state is stored.";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration directives that should be added to
+          <literal>chrony.conf</literal>
+        '';
+      };
+
+      extraFlags = mkOption {
+        default = [];
+        example = [ "-s" ];
+        type = types.listOf types.str;
+        description = "Extra flags passed to the chronyd command.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    meta.maintainers = with lib.maintainers; [ thoughtpolice ];
+
+    environment.systemPackages = [ chronyPkg ];
+
+    users.groups.chrony.gid = config.ids.gids.chrony;
+
+    users.users.chrony =
+      { uid = config.ids.uids.chrony;
+        group = "chrony";
+        description = "chrony daemon user";
+        home = stateDir;
+      };
+
+    services.timesyncd.enable = mkForce false;
+
+    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 -"
+    ];
+
+    systemd.services.chronyd =
+      { description = "chrony NTP daemon";
+
+        wantedBy = [ "multi-user.target" ];
+        wants    = [ "time-sync.target" ];
+        before   = [ "time-sync.target" ];
+        after    = [ "network.target" "nss-lookup.target" ];
+        conflicts = [ "ntpd.service" "systemd-timesyncd.service" ];
+
+        path = [ chronyPkg ];
+
+        unitConfig.ConditionCapability = "CAP_SYS_TIME";
+        serviceConfig =
+          { Type = "simple";
+            ExecStart = "${chronyPkg}/bin/chronyd ${chronyFlags}";
+
+            ProtectHome = "yes";
+            ProtectSystem = "full";
+            PrivateTmp = "yes";
+          };
+
+      };
+  };
+}
diff --git a/nixos/modules/services/networking/ntp/ntpd.nix b/nixos/modules/services/networking/ntp/ntpd.nix
new file mode 100644
index 00000000000..12be0d045a8
--- /dev/null
+++ b/nixos/modules/services/networking/ntp/ntpd.nix
@@ -0,0 +1,150 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inherit (pkgs) ntp;
+
+  cfg = config.services.ntp;
+
+  stateDir = "/var/lib/ntp";
+
+  configFile = pkgs.writeText "ntp.conf" ''
+    driftfile ${stateDir}/ntp.drift
+
+    restrict default ${toString cfg.restrictDefault}
+    restrict -6 default ${toString cfg.restrictDefault}
+    restrict source ${toString cfg.restrictSource}
+
+    restrict 127.0.0.1
+    restrict -6 ::1
+
+    ${toString (map (server: "server " + server + " iburst\n") cfg.servers)}
+
+    ${cfg.extraConfig}
+  '';
+
+  ntpFlags = "-c ${configFile} -u ntp:ntp ${toString cfg.extraFlags}";
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.ntp = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to synchronise your machine's time using ntpd, as a peer in
+          the NTP network.
+          </para>
+          <para>
+          Disables <literal>systemd.timesyncd</literal> if enabled.
+        '';
+      };
+
+      restrictDefault = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          The restriction flags to be set by default.
+          </para>
+          <para>
+          The default flags prevent external hosts from using ntpd as a DDoS
+          reflector, setting system time, and querying OS/ntpd version. As
+          recommended in section 6.5.1.1.3, answer "No" of
+          http://support.ntp.org/bin/view/Support/AccessRestrictions
+        '';
+        default = [ "limited" "kod" "nomodify" "notrap" "noquery" "nopeer" ];
+      };
+
+      restrictSource = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          The restriction flags to be set on source.
+          </para>
+          <para>
+          The default flags allow peers to be added by ntpd from configured
+          pool(s), but not by other means.
+        '';
+        default = [ "limited" "kod" "nomodify" "notrap" "noquery" ];
+      };
+
+      servers = mkOption {
+        default = config.networking.timeServers;
+        defaultText = literalExpression "config.networking.timeServers";
+        type = types.listOf types.str;
+        description = ''
+          The set of NTP servers from which to synchronise.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          fudge 127.127.1.0 stratum 10
+        '';
+        description = ''
+          Additional text appended to <filename>ntp.conf</filename>.
+        '';
+      };
+
+      extraFlags = mkOption {
+        type = types.listOf types.str;
+        description = "Extra flags passed to the ntpd command.";
+        example = literalExpression ''[ "--interface=eth0" ]'';
+        default = [];
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.ntp.enable {
+    meta.maintainers = with lib.maintainers; [ thoughtpolice ];
+
+    # Make tools such as ntpq available in the system path.
+    environment.systemPackages = [ pkgs.ntp ];
+    services.timesyncd.enable = mkForce false;
+
+    systemd.services.systemd-timedated.environment = { SYSTEMD_TIMEDATED_NTP_SERVICES = "ntpd.service"; };
+
+    users.users.ntp =
+      { isSystemUser = true;
+        group = "ntp";
+        description = "NTP daemon user";
+        home = stateDir;
+      };
+    users.groups.ntp = {};
+
+    systemd.services.ntpd =
+      { description = "NTP Daemon";
+
+        wantedBy = [ "multi-user.target" ];
+        wants = [ "time-sync.target" ];
+        before = [ "time-sync.target" ];
+
+        preStart =
+          ''
+            mkdir -m 0755 -p ${stateDir}
+            chown ntp ${stateDir}
+          '';
+
+        serviceConfig = {
+          ExecStart = "@${ntp}/bin/ntpd ntpd -g ${ntpFlags}";
+          Type = "forking";
+        };
+      };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/ntp/openntpd.nix b/nixos/modules/services/networking/ntp/openntpd.nix
new file mode 100644
index 00000000000..e86b71291f9
--- /dev/null
+++ b/nixos/modules/services/networking/ntp/openntpd.nix
@@ -0,0 +1,85 @@
+{ pkgs, lib, config, options, ... }:
+
+with lib;
+
+let
+  cfg = config.services.openntpd;
+
+  package = pkgs.openntpd_nixos;
+
+  configFile = ''
+    ${concatStringsSep "\n" (map (s: "server ${s}") cfg.servers)}
+    ${cfg.extraConfig}
+  '';
+
+  pidFile = "/run/openntpd.pid";
+
+in
+{
+  ###### interface
+
+  options.services.openntpd = {
+    enable = mkEnableOption "OpenNTP time synchronization server";
+
+    servers = mkOption {
+      default = config.services.ntp.servers;
+      defaultText = literalExpression "config.services.ntp.servers";
+      type = types.listOf types.str;
+      inherit (options.services.ntp.servers) description;
+    };
+
+    extraConfig = mkOption {
+      type = with types; lines;
+      default = "";
+      example = ''
+        listen on 127.0.0.1
+        listen on ::1
+      '';
+      description = ''
+        Additional text appended to <filename>openntpd.conf</filename>.
+      '';
+    };
+
+    extraOptions = mkOption {
+      type = with types; separatedString " ";
+      default = "";
+      example = "-s";
+      description = ''
+        Extra options used when launching openntpd.
+      '';
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    meta.maintainers = with lib.maintainers; [ thoughtpolice ];
+    services.timesyncd.enable = mkForce false;
+
+    # Add ntpctl to the environment for status checking
+    environment.systemPackages = [ package ];
+
+    environment.etc."ntpd.conf".text = configFile;
+
+    users.users.ntp = {
+      isSystemUser = true;
+      group = "ntp";
+      description = "OpenNTP daemon user";
+      home = "/var/empty";
+    };
+    users.groups.ntp = {};
+
+    systemd.services.openntpd = {
+      description = "OpenNTP Server";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" "time-sync.target" ];
+      before = [ "time-sync.target" ];
+      after = [ "dnsmasq.service" "bind.service" "network-online.target" ];
+      serviceConfig = {
+        ExecStart = "${package}/sbin/ntpd -p ${pidFile} ${cfg.extraOptions}";
+        Type = "forking";
+        PIDFile = pidFile;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nullidentdmod.nix b/nixos/modules/services/networking/nullidentdmod.nix
new file mode 100644
index 00000000000..b0d338a2794
--- /dev/null
+++ b/nixos/modules/services/networking/nullidentdmod.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, ... }: with lib; let
+  cfg = config.services.nullidentdmod;
+
+in {
+  options.services.nullidentdmod = with types; {
+    enable = mkEnableOption "the nullidentdmod identd daemon";
+
+    userid = mkOption {
+      type = nullOr str;
+      description = "User ID to return. Set to null to return a random string each time.";
+      default = null;
+      example = "alice";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.sockets.nullidentdmod = {
+      description = "Socket for identd (NullidentdMod)";
+      listenStreams = [ "113" ];
+      socketConfig.Accept = true;
+      wantedBy = [ "sockets.target" ];
+    };
+
+    systemd.services."nullidentdmod@" = {
+      description = "NullidentdMod service";
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${pkgs.nullidentdmod}/bin/nullidentdmod${optionalString (cfg.userid != null) " ${cfg.userid}"}";
+        StandardInput = "socket";
+        StandardOutput = "socket";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nylon.nix b/nixos/modules/services/networking/nylon.nix
new file mode 100644
index 00000000000..a20fa615af8
--- /dev/null
+++ b/nixos/modules/services/networking/nylon.nix
@@ -0,0 +1,166 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.nylon;
+
+  homeDir = "/var/lib/nylon";
+
+  configFile = cfg: pkgs.writeText "nylon-${cfg.name}.conf" ''
+    [General]
+    No-Simultaneous-Conn=${toString cfg.nrConnections}
+    Log=${if cfg.logging then "1" else "0"}
+    Verbose=${if cfg.verbosity then "1" else "0"}
+
+    [Server]
+    Binding-Interface=${cfg.acceptInterface}
+    Connecting-Interface=${cfg.bindInterface}
+    Port=${toString cfg.port}
+    Allow-IP=${concatStringsSep " " cfg.allowedIPRanges}
+    Deny-IP=${concatStringsSep " " cfg.deniedIPRanges}
+  '';
+
+  nylonOpts = { name, ... }: {
+
+    options = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enables nylon as a running service upon activation.
+        '';
+      };
+
+      name = mkOption {
+        type = types.str;
+        default = "";
+        description = "The name of this nylon instance.";
+      };
+
+      nrConnections = mkOption {
+        type = types.int;
+        default = 10;
+        description = ''
+          The number of allowed simultaneous connections to the daemon, default 10.
+        '';
+      };
+
+      logging = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable logging, default is no logging.
+        '';
+      };
+
+      verbosity = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable verbose output, default is to not be verbose.
+        '';
+      };
+
+      acceptInterface = mkOption {
+        type = types.str;
+        default = "lo";
+        description = ''
+          Tell nylon which interface to listen for client requests on, default is "lo".
+        '';
+      };
+
+      bindInterface = mkOption {
+        type = types.str;
+        default = "enp3s0f0";
+        description = ''
+          Tell nylon which interface to use as an uplink, default is "enp3s0f0".
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 1080;
+        description = ''
+          What port to listen for client requests, default is 1080.
+        '';
+      };
+
+      allowedIPRanges = mkOption {
+        type = with types; listOf str;
+        default = [ "192.168.0.0/16" "127.0.0.1/8" "172.16.0.1/12" "10.0.0.0/8" ];
+        description = ''
+           Allowed client IP ranges are evaluated first, defaults to ARIN IPv4 private ranges:
+             [ "192.168.0.0/16" "127.0.0.0/8" "172.16.0.0/12" "10.0.0.0/8" ]
+        '';
+      };
+
+      deniedIPRanges = mkOption {
+        type = with types; listOf str;
+        default = [ "0.0.0.0/0" ];
+        description = ''
+          Denied client IP ranges, these gets evaluated after the allowed IP ranges, defaults to all IPv4 addresses:
+            [ "0.0.0.0/0" ]
+          To block all other access than the allowed.
+        '';
+      };
+    };
+    config = { name = mkDefault name; };
+  };
+
+  mkNamedNylon = cfg: {
+    "nylon-${cfg.name}" = {
+      description = "Nylon, a lightweight SOCKS proxy server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig =
+      {
+        User = "nylon";
+        Group = "nylon";
+        WorkingDirectory = homeDir;
+        ExecStart = "${pkgs.nylon}/bin/nylon -f -c ${configFile cfg}";
+      };
+    };
+  };
+
+  anyNylons = collect (p: p ? enable) cfg;
+  enabledNylons = filter (p: p.enable == true) anyNylons;
+  nylonUnits = map (nylon: mkNamedNylon nylon) enabledNylons;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.nylon = mkOption {
+      default = {};
+      description = "Collection of named nylon instances";
+      type = with types; attrsOf (submodule nylonOpts);
+      internal = true;
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf (length(enabledNylons) > 0) {
+
+    users.users.nylon = {
+      group = "nylon";
+      description = "Nylon SOCKS Proxy";
+      home = homeDir;
+      createHome = true;
+      uid = config.ids.uids.nylon;
+    };
+
+    users.groups.nylon.gid = config.ids.gids.nylon;
+
+    systemd.services = foldr (a: b: a // b) {} nylonUnits;
+
+  };
+}
diff --git a/nixos/modules/services/networking/ocserv.nix b/nixos/modules/services/networking/ocserv.nix
new file mode 100644
index 00000000000..dc26ffeafee
--- /dev/null
+++ b/nixos/modules/services/networking/ocserv.nix
@@ -0,0 +1,99 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.ocserv;
+
+in
+
+{
+  options.services.ocserv = {
+    enable = mkEnableOption "ocserv";
+
+    config = mkOption {
+      type = types.lines;
+
+      description = ''
+        Configuration content to start an OCServ server.
+
+        For a full configuration reference,please refer to the online documentation
+        (https://ocserv.gitlab.io/www/manual.html), the openconnect
+        recipes (https://github.com/openconnect/recipes) or `man ocserv`.
+      '';
+
+      example = ''
+        # configuration examples from $out/doc without explanatory comments.
+        # for a full reference please look at the installed man pages.
+        auth = "plain[passwd=./sample.passwd]"
+        tcp-port = 443
+        udp-port = 443
+        run-as-user = nobody
+        run-as-group = nogroup
+        socket-file = /run/ocserv-socket
+        server-cert = certs/server-cert.pem
+        server-key = certs/server-key.pem
+        keepalive = 32400
+        dpd = 90
+        mobile-dpd = 1800
+        switch-to-tcp-timeout = 25
+        try-mtu-discovery = false
+        cert-user-oid = 0.9.2342.19200300.100.1.1
+        tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-VERS-SSL3.0"
+        auth-timeout = 240
+        min-reauth-time = 300
+        max-ban-score = 80
+        ban-reset-time = 1200
+        cookie-timeout = 300
+        deny-roaming = false
+        rekey-time = 172800
+        rekey-method = ssl
+        use-occtl = true
+        pid-file = /run/ocserv.pid
+        device = vpns
+        predictable-ips = true
+        default-domain = example.com
+        ipv4-network = 192.168.1.0
+        ipv4-netmask = 255.255.255.0
+        dns = 192.168.1.2
+        ping-leases = false
+        route = 10.10.10.0/255.255.255.0
+        route = 192.168.0.0/255.255.0.0
+        no-route = 192.168.5.0/255.255.255.0
+        cisco-client-compat = true
+        dtls-legacy = true
+
+        [vhost:www.example.com]
+        auth = "certificate"
+        ca-cert = certs/ca.pem
+        server-cert = certs/server-cert-secp521r1.pem
+        server-key = cersts/certs/server-key-secp521r1.pem
+        ipv4-network = 192.168.2.0
+        ipv4-netmask = 255.255.255.0
+        cert-user-oid = 0.9.2342.19200300.100.1.1
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.ocserv ];
+    environment.etc."ocserv/ocserv.conf".text = cfg.config;
+
+    security.pam.services.ocserv = {};
+
+    systemd.services.ocserv = {
+      description = "OpenConnect SSL VPN server";
+      documentation = [ "man:ocserv(8)" ];
+      after = [ "dbus.service" "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        PrivateTmp = true;
+        PIDFile = "/run/ocserv.pid";
+        ExecStart = "${pkgs.ocserv}/bin/ocserv --foreground --pid-file /run/ocesrv.pid --config /etc/ocserv/ocserv.conf";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/ofono.nix b/nixos/modules/services/networking/ofono.nix
new file mode 100644
index 00000000000..460b06443c4
--- /dev/null
+++ b/nixos/modules/services/networking/ofono.nix
@@ -0,0 +1,44 @@
+# Ofono daemon.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.ofono;
+
+  plugin_path =
+    lib.concatMapStringsSep ":"
+      (plugin: "${plugin}/lib/ofono/plugins")
+      cfg.plugins
+    ;
+
+in
+
+{
+  ###### interface
+  options = {
+    services.ofono = {
+      enable = mkEnableOption "Ofono";
+
+      plugins = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "[ pkgs.modem-manager-gui ]";
+        description = ''
+          The list of plugins to install.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.dbus.packages = [ pkgs.ofono ];
+
+    systemd.packages = [ pkgs.ofono ];
+
+    systemd.services.ofono.environment.OFONO_PLUGIN_PATH = mkIf (cfg.plugins != []) plugin_path;
+
+  };
+}
diff --git a/nixos/modules/services/networking/oidentd.nix b/nixos/modules/services/networking/oidentd.nix
new file mode 100644
index 00000000000..feb84806ba9
--- /dev/null
+++ b/nixos/modules/services/networking/oidentd.nix
@@ -0,0 +1,44 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.oidentd.enable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to enable ‘oidentd’, an implementation of the Ident
+        protocol (RFC 1413).  It allows remote systems to identify the
+        name of the user associated with a TCP connection.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.oidentd.enable {
+    systemd.services.oidentd = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig.Type = "forking";
+      script = "${pkgs.oidentd}/sbin/oidentd -u oidentd -g nogroup";
+    };
+
+    users.users.oidentd = {
+      description = "Ident Protocol daemon user";
+      group = "oidentd";
+      uid = config.ids.uids.oidentd;
+    };
+
+    users.groups.oidentd.gid = config.ids.gids.oidentd;
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/onedrive.nix b/nixos/modules/services/networking/onedrive.nix
new file mode 100644
index 00000000000..0256a6a4111
--- /dev/null
+++ b/nixos/modules/services/networking/onedrive.nix
@@ -0,0 +1,71 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.onedrive;
+
+  onedriveLauncher =  pkgs.writeShellScriptBin
+    "onedrive-launcher"
+    ''
+      # XDG_CONFIG_HOME is not recognized in the environment here.
+      if [ -f $HOME/.config/onedrive-launcher ]
+      then
+        # Hopefully using underscore boundary helps locate variables
+        for _onedrive_config_dirname_ in $(cat $HOME/.config/onedrive-launcher | grep -v '[ \t]*#' )
+        do
+          systemctl --user start onedrive@$_onedrive_config_dirname_
+        done
+      else
+        systemctl --user start onedrive@onedrive
+      fi
+    ''
+  ;
+
+in {
+  ### Documentation
+  # meta.doc = ./onedrive.xml;
+
+  ### Interface
+
+  options.services.onedrive = {
+    enable = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = "Enable OneDrive service";
+    };
+
+     package = lib.mkOption {
+       type = lib.types.package;
+       default = pkgs.onedrive;
+       defaultText = lib.literalExpression "pkgs.onedrive";
+       description = ''
+         OneDrive package to use.
+       '';
+     };
+  };
+### Implementation
+
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.user.services."onedrive@" = {
+      description = "Onedrive sync service";
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = ''
+          ${cfg.package}/bin/onedrive --monitor --confdir=%h/.config/%i
+        '';
+        Restart="on-failure";
+        RestartSec=3;
+        RestartPreventExitStatus=3;
+      };
+    };
+
+    systemd.user.services.onedrive-launcher = {
+      wantedBy = [ "default.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${onedriveLauncher}/bin/onedrive-launcher";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/onedrive.xml b/nixos/modules/services/networking/onedrive.xml
new file mode 100644
index 00000000000..5a9dcf01aee
--- /dev/null
+++ b/nixos/modules/services/networking/onedrive.xml
@@ -0,0 +1,34 @@
+<chapter xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xmlns:xi="http://www.w3.org/2001/XInclude"
+         version="5.0"
+         xml:id="onedrive">
+ <title>Microsoft OneDrive</title>
+ <para>
+  Microsoft Onedrive is a popular cloud file-hosting service, used by 85% of Fortune 500 companies. NixOS uses a popular OneDrive client for Linux maintained by github user abraunegg. The Linux client is excellent and allows customization of which files or paths to download, not much unlike the default Windows OneDrive client by Microsoft itself. The client allows syncing with multiple onedrive accounts at the same time, of any type- OneDrive personal, OneDrive business, Office365 and Sharepoint libraries, without any additional charge.
+ </para>
+ <para>
+  For more information, guides and documentation, see <link xlink:href="https://abraunegg.github.io/"/>.
+ </para>
+ <para>
+  To enable OneDrive support, add the following to your <filename>configuration.nix</filename>:
+<programlisting>
+<xref linkend="opt-services.onedrive.enable"/> = true;
+</programlisting>
+  This installs the <literal>onedrive</literal> package and a service <literal>onedriveLauncher</literal> which will instantiate a <literal>onedrive</literal> service for all your OneDrive accounts. Follow the steps in documentation of the onedrive client to setup your accounts. To use the service with multiple accounts, create a file named <filename>onedrive-launcher</filename> in <filename>~/.config</filename> and add the filename of the config directory, relative to <filename>~/.config</filename>. For example, if you have two OneDrive accounts with configs in <filename>~/.config/onedrive_bob_work</filename> and <filename>~/.config/onedrive_bob_personal</filename>, add the following lines:
+<programlisting>
+onedrive_bob_work
+# Not in use:
+# onedrive_bob_office365
+onedrive_bob_personal
+</programlisting>
+  No such file needs to be created if you are using only a single OneDrive account with config in the default location <filename>~/.config/onedrive</filename>, in the absence of <filename>~/.config/onedrive-launcher</filename>, only a single service is instantiated, with default config path.
+</para>
+
+  <para>
+  If you wish to use a custom OneDrive package, say from another channel, add the following line:
+<programlisting>
+<xref linkend="opt-services.onedrive.package"/> = pkgs.unstable.onedrive;
+</programlisting>
+ </para>
+</chapter>
diff --git a/nixos/modules/services/networking/openfire.nix b/nixos/modules/services/networking/openfire.nix
new file mode 100644
index 00000000000..fe0499d5232
--- /dev/null
+++ b/nixos/modules/services/networking/openfire.nix
@@ -0,0 +1,56 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  ###### interface
+
+  options = {
+
+    services.openfire = {
+
+      enable = mkEnableOption "OpenFire XMPP server";
+
+      usePostgreSQL = mkOption {
+        type = types.bool;
+        default = true;
+        description = "
+          Whether you use PostgreSQL service for your storage back-end.
+        ";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.openfire.enable {
+
+    assertions = singleton
+      { assertion = !(config.services.openfire.usePostgreSQL -> config.services.postgresql.enable);
+        message = "OpenFire configured to use PostgreSQL but services.postgresql.enable is not enabled.";
+      };
+
+    systemd.services.openfire = {
+      description = "OpenFire XMPP server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ] ++
+        optional config.services.openfire.usePostgreSQL "postgresql.service";
+      path = with pkgs; [ jre openfire coreutils which gnugrep gawk gnused ];
+      script = ''
+        export HOME=/tmp
+        mkdir /var/log/openfire || true
+        mkdir /etc/openfire || true
+        for i in ${pkgs.openfire}/conf.inst/*; do
+            if ! test -f /etc/openfire/$(basename $i); then
+                cp $i /etc/openfire/
+            fi
+        done
+        openfire start
+      ''; # */
+    };
+  };
+
+}
diff --git a/nixos/modules/services/networking/openvpn.nix b/nixos/modules/services/networking/openvpn.nix
new file mode 100644
index 00000000000..cf3f79fc578
--- /dev/null
+++ b/nixos/modules/services/networking/openvpn.nix
@@ -0,0 +1,219 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.openvpn;
+
+  inherit (pkgs) openvpn;
+
+  makeOpenVPNJob = cfg: name:
+    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
+        # name and name server.
+        for var in ''${!foreign_option_*}; do
+          x=(''${!var})
+          if [ "''${x[0]}" = dhcp-option ]; then
+            if [ "''${x[1]}" = DOMAIN ]; then domain="''${x[2]}"
+            elif [ "''${x[1]}" = DNS ]; then nameserver="''${x[2]}"
+            fi
+          fi
+        done
+
+        ${cfg.up}
+        ${optionalString cfg.updateResolvConf
+           "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
+      '';
+
+      downScript = ''
+        #! /bin/sh
+        export PATH=${path}
+        ${optionalString cfg.updateResolvConf
+           "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
+        ${cfg.down}
+      '';
+
+      configFile = pkgs.writeText "openvpn-config-${name}"
+        ''
+          errors-to-stderr
+          ${optionalString (cfg.up != "" || cfg.down != "" || cfg.updateResolvConf) "script-security 2"}
+          ${cfg.config}
+          ${optionalString (cfg.up != "" || cfg.updateResolvConf)
+              "up ${pkgs.writeScript "openvpn-${name}-up" upScript}"}
+          ${optionalString (cfg.down != "" || cfg.updateResolvConf)
+              "down ${pkgs.writeScript "openvpn-${name}-down" downScript}"}
+          ${optionalString (cfg.authUserPass != null)
+              "auth-user-pass ${pkgs.writeText "openvpn-credentials-${name}" ''
+                ${cfg.authUserPass.username}
+                ${cfg.authUserPass.password}
+              ''}"}
+        '';
+
+    in {
+      description = "OpenVPN instance ‘${name}’";
+
+      wantedBy = optional cfg.autoStart "multi-user.target";
+      after = [ "network.target" ];
+
+      path = [ pkgs.iptables pkgs.iproute2 pkgs.nettools ];
+
+      serviceConfig.ExecStart = "@${openvpn}/sbin/openvpn openvpn --suppress-timestamps --config ${configFile}";
+      serviceConfig.Restart = "always";
+      serviceConfig.Type = "notify";
+    };
+
+in
+
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "openvpn" "enable" ] "")
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.openvpn.servers = mkOption {
+      default = {};
+
+      example = literalExpression ''
+        {
+          server = {
+            config = '''
+              # Simplest server configuration: https://community.openvpn.net/openvpn/wiki/StaticKeyMiniHowto
+              # server :
+              dev tun
+              ifconfig 10.8.0.1 10.8.0.2
+              secret /root/static.key
+            ''';
+            up = "ip route add ...";
+            down = "ip route del ...";
+          };
+
+          client = {
+            config = '''
+              client
+              remote vpn.example.org
+              dev tun
+              proto tcp-client
+              port 8080
+              ca /root/.vpn/ca.crt
+              cert /root/.vpn/alice.crt
+              key /root/.vpn/alice.key
+            ''';
+            up = "echo nameserver $nameserver | ''${pkgs.openresolv}/sbin/resolvconf -m 0 -a $dev";
+            down = "''${pkgs.openresolv}/sbin/resolvconf -d $dev";
+          };
+        }
+      '';
+
+      description = ''
+        Each attribute of this option defines a systemd service that
+        runs an OpenVPN instance.  These can be OpenVPN servers or
+        clients.  The name of each systemd service is
+        <literal>openvpn-<replaceable>name</replaceable>.service</literal>,
+        where <replaceable>name</replaceable> is the corresponding
+        attribute name.
+      '';
+
+      type = with types; attrsOf (submodule {
+
+        options = {
+
+          config = mkOption {
+            type = types.lines;
+            description = ''
+              Configuration of this OpenVPN instance.  See
+              <citerefentry><refentrytitle>openvpn</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+              for details.
+
+              To import an external config file, use the following definition:
+              <literal>config = "config /path/to/config.ovpn"</literal>
+            '';
+          };
+
+          up = mkOption {
+            default = "";
+            type = types.lines;
+            description = ''
+              Shell commands executed when the instance is starting.
+            '';
+          };
+
+          down = mkOption {
+            default = "";
+            type = types.lines;
+            description = ''
+              Shell commands executed when the instance is shutting down.
+            '';
+          };
+
+          autoStart = mkOption {
+            default = true;
+            type = types.bool;
+            description = "Whether this OpenVPN instance should be started automatically.";
+          };
+
+          updateResolvConf = mkOption {
+            default = false;
+            type = types.bool;
+            description = ''
+              Use the script from the update-resolv-conf package to automatically
+              update resolv.conf with the DNS information provided by openvpn. The
+              script will be run after the "up" commands and before the "down" commands.
+            '';
+          };
+
+          authUserPass = mkOption {
+            default = null;
+            description = ''
+              This option can be used to store the username / password credentials
+              with the "auth-user-pass" authentication method.
+
+              WARNING: Using this option will put the credentials WORLD-READABLE in the Nix store!
+            '';
+            type = types.nullOr (types.submodule {
+
+              options = {
+                username = mkOption {
+                  description = "The username to store inside the credentials file.";
+                  type = types.str;
+                };
+
+                password = mkOption {
+                  description = "The password to store inside the credentials file.";
+                  type = types.str;
+                };
+              };
+            });
+          };
+        };
+
+      });
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf (cfg.servers != {}) {
+
+    systemd.services = listToAttrs (mapAttrsFlatten (name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)) cfg.servers);
+
+    environment.systemPackages = [ openvpn ];
+
+    boot.kernelModules = [ "tun" ];
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/ostinato.nix b/nixos/modules/services/networking/ostinato.nix
new file mode 100644
index 00000000000..4da11984b9f
--- /dev/null
+++ b/nixos/modules/services/networking/ostinato.nix
@@ -0,0 +1,104 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  pkg = pkgs.ostinato;
+  cfg = config.services.ostinato;
+  configFile = pkgs.writeText "drone.ini" ''
+    [General]
+    RateAccuracy=${cfg.rateAccuracy}
+
+    [RpcServer]
+    Address=${cfg.rpcServer.address}
+
+    [PortList]
+    Include=${concatStringsSep "," cfg.portList.include}
+    Exclude=${concatStringsSep "," cfg.portList.exclude}
+  '';
+
+in
+{
+
+  ###### interface
+
+  options = {
+
+    services.ostinato = {
+
+      enable = mkEnableOption "Ostinato agent-controller (Drone)";
+
+      port = mkOption {
+        type = types.int;
+        default = 7878;
+        description = ''
+          Port to listen on.
+        '';
+      };
+
+      rateAccuracy = mkOption {
+        type = types.enum [ "High" "Low" ];
+        default = "High";
+        description = ''
+          To ensure that the actual transmit rate is as close as possible to
+          the configured transmit rate, Drone runs a busy-wait loop.
+          While this provides the maximum accuracy possible, the CPU
+          utilization is 100% while the transmit is on. You can however,
+          sacrifice the accuracy to reduce the CPU load.
+        '';
+      };
+
+      rpcServer = {
+        address = mkOption {
+          type = types.str;
+          default = "0.0.0.0";
+          description = ''
+            By default, the Drone RPC server will listen on all interfaces and
+            local IPv4 adresses for incoming connections from clients.  Specify
+            a single IPv4 or IPv6 address if you want to restrict that.
+            To listen on any IPv6 address, use ::
+          '';
+        };
+      };
+
+      portList = {
+        include = mkOption {
+          type = types.listOf types.str;
+          default = [];
+          example = [ "eth*" "lo*" ];
+          description = ''
+            For a port to pass the filter and appear on the port list managed
+            by drone, it be allowed by this include list.
+          '';
+        };
+        exclude = mkOption {
+          type = types.listOf types.str;
+          default = [];
+          example = [ "usbmon*" "eth0" ];
+          description = ''
+            A list of ports does not appear on the port list managed by drone.
+          '';
+        };
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkg ];
+
+    systemd.services.drone = {
+      description = "Ostinato agent-controller";
+      wantedBy = [ "multi-user.target" ];
+      script = ''
+        ${pkg}/bin/drone ${toString cfg.port} ${configFile}
+      '';
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/owamp.nix b/nixos/modules/services/networking/owamp.nix
new file mode 100644
index 00000000000..baf64347b09
--- /dev/null
+++ b/nixos/modules/services/networking/owamp.nix
@@ -0,0 +1,45 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.owamp;
+in
+{
+
+  ###### interface
+
+  options = {
+    services.owamp.enable = mkEnableOption "Enable OWAMP server";
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    users.users.owamp = {
+      group = "owamp";
+      description = "Owamp daemon";
+      isSystemUser = true;
+    };
+
+    users.groups.owamp = { };
+
+    systemd.services.owamp = {
+      description = "Owamp server";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart="${pkgs.owamp}/bin/owampd -R /run/owamp -d /run/owamp -v -Z ";
+        PrivateTmp = true;
+        Restart = "always";
+        Type="simple";
+        User = "owamp";
+        Group = "owamp";
+        RuntimeDirectory = "owamp";
+        StateDirectory = "owamp";
+        AmbientCapabilities = "cap_net_bind_service";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/pdns-recursor.nix b/nixos/modules/services/networking/pdns-recursor.nix
new file mode 100644
index 00000000000..0579d314a9b
--- /dev/null
+++ b/nixos/modules/services/networking/pdns-recursor.nix
@@ -0,0 +1,206 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.pdns-recursor;
+
+  oneOrMore  = type: with types; either type (listOf type);
+  valueType  = with types; oneOf [ int str bool path ];
+  configType = with types; attrsOf (nullOr (oneOrMore valueType));
+
+  toBool    = val: if val then "yes" else "no";
+  serialize = val: with types;
+         if str.check       val then val
+    else if int.check       val then toString val
+    else if path.check      val then toString val
+    else if bool.check      val then toBool val
+    else if builtins.isList val then (concatMapStringsSep "," serialize val)
+    else "";
+
+  configDir = pkgs.writeTextDir "recursor.conf"
+    (concatStringsSep "\n"
+      (flip mapAttrsToList cfg.settings
+        (name: val: "${name}=${serialize val}")));
+
+  mkDefaultAttrs = mapAttrs (n: v: mkDefault v);
+
+in {
+  options.services.pdns-recursor = {
+    enable = mkEnableOption "PowerDNS Recursor, a recursive DNS server";
+
+    dns.address = mkOption {
+      type = types.str;
+      default = "0.0.0.0";
+      description = ''
+        IP address Recursor DNS server will bind to.
+      '';
+    };
+
+    dns.port = mkOption {
+      type = types.int;
+      default = 53;
+      description = ''
+        Port number Recursor DNS server will bind to.
+      '';
+    };
+
+    dns.allowFrom = mkOption {
+      type = types.listOf types.str;
+      default = [ "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/16" ];
+      example = [ "0.0.0.0/0" ];
+      description = ''
+        IP address ranges of clients allowed to make DNS queries.
+      '';
+    };
+
+    api.address = mkOption {
+      type = types.str;
+      default = "0.0.0.0";
+      description = ''
+        IP address Recursor REST API server will bind to.
+      '';
+    };
+
+    api.port = mkOption {
+      type = types.int;
+      default = 8082;
+      description = ''
+        Port number Recursor REST API server will bind to.
+      '';
+    };
+
+    api.allowFrom = mkOption {
+      type = types.listOf types.str;
+      default = [ "0.0.0.0/0" ];
+      description = ''
+        IP address ranges of clients allowed to make API requests.
+      '';
+    };
+
+    exportHosts = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+       Whether to export names and IP addresses defined in /etc/hosts.
+      '';
+    };
+
+    forwardZones = mkOption {
+      type = types.attrs;
+      default = {};
+      description = ''
+        DNS zones to be forwarded to other authoritative servers.
+      '';
+    };
+
+    forwardZonesRecurse = mkOption {
+      type = types.attrs;
+      example = { eth = "127.0.0.1:5353"; };
+      default = {};
+      description = ''
+        DNS zones to be forwarded to other recursive servers.
+      '';
+    };
+
+    dnssecValidation = mkOption {
+      type = types.enum ["off" "process-no-validate" "process" "log-fail" "validate"];
+      default = "validate";
+      description = ''
+        Controls the level of DNSSEC processing done by the PowerDNS Recursor.
+        See https://doc.powerdns.com/md/recursor/dnssec/ for a detailed explanation.
+      '';
+    };
+
+    serveRFC1918 = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to directly resolve the RFC1918 reverse-mapping domains:
+        <literal>10.in-addr.arpa</literal>,
+        <literal>168.192.in-addr.arpa</literal>,
+        <literal>16-31.172.in-addr.arpa</literal>
+        This saves load on the AS112 servers.
+      '';
+    };
+
+    settings = mkOption {
+      type = configType;
+      default = { };
+      example = literalExpression ''
+        {
+          loglevel = 8;
+          log-common-errors = true;
+        }
+      '';
+      description = ''
+        PowerDNS Recursor settings. Use this option to configure Recursor
+        settings not exposed in a NixOS option or to bypass one.
+        See the full documentation at
+        <link xlink:href="https://doc.powerdns.com/recursor/settings.html"/>
+        for the available options.
+      '';
+    };
+
+    luaConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        The content Lua configuration file for PowerDNS Recursor. See
+        <link xlink:href="https://doc.powerdns.com/recursor/lua-config/index.html"/>.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    services.pdns-recursor.settings = mkDefaultAttrs {
+      local-address = cfg.dns.address;
+      local-port    = cfg.dns.port;
+      allow-from    = cfg.dns.allowFrom;
+
+      webserver-address    = cfg.api.address;
+      webserver-port       = cfg.api.port;
+      webserver-allow-from = cfg.api.allowFrom;
+
+      forward-zones         = mapAttrsToList (zone: uri: "${zone}.=${uri}") cfg.forwardZones;
+      forward-zones-recurse = mapAttrsToList (zone: uri: "${zone}.=${uri}") cfg.forwardZonesRecurse;
+      export-etc-hosts = cfg.exportHosts;
+      dnssec           = cfg.dnssecValidation;
+      serve-rfc1918    = cfg.serveRFC1918;
+      lua-config-file  = pkgs.writeText "recursor.lua" cfg.luaConfig;
+
+      daemon         = false;
+      write-pid      = false;
+      log-timestamp  = false;
+      disable-syslog = true;
+    };
+
+    systemd.packages = [ pkgs.pdns-recursor ];
+
+    systemd.services.pdns-recursor = {
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = [ "" "${pkgs.pdns-recursor}/bin/pdns_recursor --config-dir=${configDir}" ];
+      };
+    };
+
+    users.users.pdns-recursor = {
+      isSystemUser = true;
+      group = "pdns-recursor";
+      description = "PowerDNS Recursor daemon user";
+    };
+
+    users.groups.pdns-recursor = {};
+
+  };
+
+  imports = [
+   (mkRemovedOptionModule [ "services" "pdns-recursor" "extraConfig" ]
+     "To change extra Recursor settings use services.pdns-recursor.settings instead.")
+  ];
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+}
diff --git a/nixos/modules/services/networking/pdnsd.nix b/nixos/modules/services/networking/pdnsd.nix
new file mode 100644
index 00000000000..24b5bbc5104
--- /dev/null
+++ b/nixos/modules/services/networking/pdnsd.nix
@@ -0,0 +1,91 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.pdnsd;
+  pdnsd = pkgs.pdnsd;
+  pdnsdUser = "pdnsd";
+  pdnsdGroup = "pdnsd";
+  pdnsdConf = pkgs.writeText "pdnsd.conf"
+    ''
+      global {
+        run_as=${pdnsdUser};
+        cache_dir="${cfg.cacheDir}";
+        ${cfg.globalConfig}
+      }
+
+      server {
+        ${cfg.serverConfig}
+      }
+      ${cfg.extraConfig}
+    '';
+in
+
+{ options =
+    { services.pdnsd =
+        { enable = mkEnableOption "pdnsd";
+
+          cacheDir = mkOption {
+            type = types.str;
+            default = "/var/cache/pdnsd";
+            description = "Directory holding the pdnsd cache";
+          };
+
+          globalConfig = mkOption {
+            type = types.lines;
+            default = "";
+            description = ''
+              Global configuration that should be added to the global directory
+              of <literal>pdnsd.conf</literal>.
+            '';
+          };
+
+          serverConfig = mkOption {
+            type = types.lines;
+            default = "";
+            description = ''
+              Server configuration that should be added to the server directory
+              of <literal>pdnsd.conf</literal>.
+            '';
+          };
+
+          extraConfig = mkOption {
+            type = types.lines;
+            default = "";
+            description = ''
+              Extra configuration directives that should be added to
+              <literal>pdnsd.conf</literal>.
+            '';
+          };
+        };
+    };
+
+  config = mkIf cfg.enable {
+    users.users.${pdnsdUser} = {
+      uid = config.ids.uids.pdnsd;
+      group = pdnsdGroup;
+      description = "pdnsd user";
+    };
+
+    users.groups.${pdnsdGroup} = {
+      gid = config.ids.gids.pdnsd;
+    };
+
+    systemd.services.pdnsd =
+      { wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        preStart =
+          ''
+            mkdir -p "${cfg.cacheDir}"
+            touch "${cfg.cacheDir}/pdnsd.cache"
+            chown -R ${pdnsdUser}:${pdnsdGroup} "${cfg.cacheDir}"
+          '';
+        description = "pdnsd";
+        serviceConfig =
+          {
+            ExecStart = "${pdnsd}/bin/pdnsd -c ${pdnsdConf}";
+          };
+      };
+  };
+}
diff --git a/nixos/modules/services/networking/pixiecore.nix b/nixos/modules/services/networking/pixiecore.nix
new file mode 100644
index 00000000000..d2642c82c2d
--- /dev/null
+++ b/nixos/modules/services/networking/pixiecore.nix
@@ -0,0 +1,135 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.pixiecore;
+in
+{
+  meta.maintainers = with maintainers; [ bbigras danderson ];
+
+  options = {
+    services.pixiecore = {
+      enable = mkEnableOption "Pixiecore";
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports (67, 69 UDP and 4011, 'port', 'statusPort' TCP) in the firewall for Pixiecore.
+        '';
+      };
+
+      mode = mkOption {
+        description = "Which mode to use";
+        default = "boot";
+        type = types.enum [ "api" "boot" ];
+      };
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Log more things that aren't directly related to booting a recognized client";
+      };
+
+      dhcpNoBind = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Handle DHCP traffic without binding to the DHCP server port";
+      };
+
+      kernel = mkOption {
+        type = types.str or types.path;
+        default = "";
+        description = "Kernel path. Ignored unless mode is set to 'boot'";
+      };
+
+      initrd = mkOption {
+        type = types.str or types.path;
+        default = "";
+        description = "Initrd path. Ignored unless mode is set to 'boot'";
+      };
+
+      cmdLine = mkOption {
+        type = types.str;
+        default = "";
+        description = "Kernel commandline arguments. Ignored unless mode is set to 'boot'";
+      };
+
+      listen = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "IPv4 address to listen on";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 80;
+        description = "Port to listen on for HTTP";
+      };
+
+      statusPort = mkOption {
+        type = types.port;
+        default = 80;
+        description = "HTTP port for status information (can be the same as --port)";
+      };
+
+      apiServer = mkOption {
+        type = types.str;
+        example = "localhost:8080";
+        description = "host:port to connect to the API. Ignored unless mode is set to 'api'";
+      };
+
+      extraArguments = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Additional command line arguments to pass to Pixiecore";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups.pixiecore = {};
+    users.users.pixiecore = {
+      description = "Pixiecore daemon user";
+      group = "pixiecore";
+      isSystemUser = true;
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 4011 cfg.port cfg.statusPort ];
+      allowedUDPPorts = [ 67 69 ];
+    };
+
+    systemd.services.pixiecore = {
+      description = "Pixiecore server";
+      after = [ "network.target"];
+      wants = [ "network.target"];
+      wantedBy = [ "multi-user.target"];
+      serviceConfig = {
+        User = "pixiecore";
+        Restart = "always";
+        AmbientCapabilities = [ "cap_net_bind_service" ] ++ optional cfg.dhcpNoBind "cap_net_raw";
+        ExecStart =
+          let
+            argString =
+              if cfg.mode == "boot"
+              then [ "boot" cfg.kernel ]
+                   ++ optional (cfg.initrd != "") cfg.initrd
+                   ++ optionals (cfg.cmdLine != "") [ "--cmdline" cfg.cmdLine ]
+              else [ "api" cfg.apiServer ];
+          in
+            ''
+              ${pkgs.pixiecore}/bin/pixiecore \
+                ${lib.escapeShellArgs argString} \
+                ${optionalString cfg.debug "--debug"} \
+                ${optionalString cfg.dhcpNoBind "--dhcp-no-bind"} \
+                --listen-addr ${lib.escapeShellArg cfg.listen} \
+                --port ${toString cfg.port} \
+                --status-port ${toString cfg.statusPort} \
+                ${escapeShellArgs cfg.extraArguments}
+              '';
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/pleroma.nix b/nixos/modules/services/networking/pleroma.nix
new file mode 100644
index 00000000000..c6d4c14dcb7
--- /dev/null
+++ b/nixos/modules/services/networking/pleroma.nix
@@ -0,0 +1,149 @@
+{ config, options, lib, pkgs, stdenv, ... }:
+let
+  cfg = config.services.pleroma;
+  cookieFile = "/var/lib/pleroma/.cookie";
+in {
+  options = {
+    services.pleroma = with lib; {
+      enable = mkEnableOption "pleroma";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.pleroma.override { inherit cookieFile; };
+        defaultText = literalExpression "pkgs.pleroma";
+        description = "Pleroma package to use.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "pleroma";
+        description = "User account under which pleroma runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "pleroma";
+        description = "Group account under which pleroma runs.";
+      };
+
+      stateDir = mkOption {
+        type = types.str;
+        default = "/var/lib/pleroma";
+        readOnly = true;
+        description = "Directory where the pleroma service will save the uploads and static files.";
+      };
+
+      configs = mkOption {
+        type = with types; listOf str;
+        description = ''
+          Pleroma public configuration.
+
+          This list gets appended from left to
+          right into /etc/pleroma/config.exs. Elixir evaluates its
+          configuration imperatively, meaning you can override a
+          setting by appending a new str to this NixOS option list.
+
+          <emphasis>DO NOT STORE ANY PLEROMA SECRET
+          HERE</emphasis>, use
+          <link linkend="opt-services.pleroma.secretConfigFile">services.pleroma.secretConfigFile</link>
+          instead.
+
+          This setting is going to be stored in a file part of
+          the Nix store. The Nix store being world-readable, it's not
+          the right place to store any secret
+
+          Have a look to Pleroma section in the NixOS manual for more
+          informations.
+          '';
+      };
+
+      secretConfigFile = mkOption {
+        type = types.str;
+        default = "/var/lib/pleroma/secrets.exs";
+        description = ''
+          Path to the file containing your secret pleroma configuration.
+
+          <emphasis>DO NOT POINT THIS OPTION TO THE NIX
+          STORE</emphasis>, the store being world-readable, it'll
+          compromise all your secrets.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    users = {
+      users."${cfg.user}" = {
+        description = "Pleroma user";
+        home = cfg.stateDir;
+        group = cfg.group;
+        isSystemUser = true;
+      };
+      groups."${cfg.group}" = {};
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    environment.etc."/pleroma/config.exs".text = ''
+      ${lib.concatMapStrings (x: "${x}") cfg.configs}
+
+      # The lau/tzdata library is trying to download the latest
+      # timezone database in the OTP priv directory by default.
+      # This directory being in the store, it's read-only.
+      # Setting that up to a more appropriate location.
+      config :tzdata, :data_dir, "/var/lib/pleroma/elixir_tzdata_data"
+
+      import_config "${cfg.secretConfigFile}"
+    '';
+
+    systemd.services.pleroma = {
+      description = "Pleroma social network";
+      after = [ "network-online.target" "postgresql.service" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ config.environment.etc."/pleroma/config.exs".source ];
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        Type = "exec";
+        WorkingDirectory = "~";
+        StateDirectory = "pleroma pleroma/static pleroma/uploads";
+        StateDirectoryMode = "700";
+
+        # Checking the conf file is there then running the database
+        # migration before each service start, just in case there are
+        # some pending ones.
+        #
+        # It's sub-optimal as we'll always run this, even if pleroma
+        # has not been updated. But the no-op process is pretty fast.
+        # Better be safe than sorry migration-wise.
+        ExecStartPre =
+          let preScript = pkgs.writers.writeBashBin "pleromaStartPre" ''
+            if [ ! -f "${cookieFile}" ] || [ ! -s "${cookieFile}" ]
+            then
+              echo "Creating cookie file"
+              dd if=/dev/urandom bs=1 count=16 | ${pkgs.hexdump}/bin/hexdump -e '16/1 "%02x"' > "${cookieFile}"
+            fi
+            ${cfg.package}/bin/pleroma_ctl migrate
+          '';
+          in "${preScript}/bin/pleromaStartPre";
+
+        ExecStart = "${cfg.package}/bin/pleroma start";
+        ExecStop = "${cfg.package}/bin/pleroma stop";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+
+        # Systemd sandboxing directives.
+        # Taken from the upstream contrib systemd service at
+        # pleroma/installation/pleroma.service
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectSystem = "full";
+        PrivateDevices = false;
+        NoNewPrivileges = true;
+        CapabilityBoundingSet = "~CAP_SYS_ADMIN";
+      };
+    };
+
+  };
+  meta.maintainers = with lib.maintainers; [ ninjatrappeur ];
+  meta.doc = ./pleroma.xml;
+}
diff --git a/nixos/modules/services/networking/pleroma.xml b/nixos/modules/services/networking/pleroma.xml
new file mode 100644
index 00000000000..ad0a481af28
--- /dev/null
+++ b/nixos/modules/services/networking/pleroma.xml
@@ -0,0 +1,188 @@
+<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/polipo.nix b/nixos/modules/services/networking/polipo.nix
new file mode 100644
index 00000000000..1ff9388346b
--- /dev/null
+++ b/nixos/modules/services/networking/polipo.nix
@@ -0,0 +1,112 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.polipo;
+
+  polipoConfig = pkgs.writeText "polipo.conf" ''
+    proxyAddress = ${cfg.proxyAddress}
+    proxyPort = ${toString cfg.proxyPort}
+    allowedClients = ${concatStringsSep ", " cfg.allowedClients}
+    ${optionalString (cfg.parentProxy != "") "parentProxy = ${cfg.parentProxy}" }
+    ${optionalString (cfg.socksParentProxy != "") "socksParentProxy = ${cfg.socksParentProxy}" }
+    ${config.services.polipo.extraConfig}
+  '';
+
+in
+
+{
+
+  options = {
+
+    services.polipo = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to run the polipo caching web proxy.";
+      };
+
+      proxyAddress = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "IP address on which Polipo will listen.";
+      };
+
+      proxyPort = mkOption {
+        type = types.int;
+        default = 8123;
+        description = "TCP port on which Polipo will listen.";
+      };
+
+      allowedClients = mkOption {
+        type = types.listOf types.str;
+        default = [ "127.0.0.1" "::1" ];
+        example = [ "127.0.0.1" "::1" "134.157.168.0/24" "2001:660:116::/48" ];
+        description = ''
+          List of IP addresses or network addresses that may connect to Polipo.
+        '';
+      };
+
+      parentProxy = mkOption {
+        type = types.str;
+        default = "";
+        example = "localhost:8124";
+        description = ''
+          Hostname and port number of an HTTP parent proxy;
+          it should have the form ‘host:port’.
+        '';
+      };
+
+      socksParentProxy = mkOption {
+        type = types.str;
+        default = "";
+        example = "localhost:9050";
+        description = ''
+          Hostname and port number of an SOCKS parent proxy;
+          it should have the form ‘host:port’.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Polio configuration. Contents will be added
+          verbatim to the configuration file.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users.polipo =
+      { uid = config.ids.uids.polipo;
+        description = "Polipo caching proxy user";
+        home = "/var/cache/polipo";
+        createHome = true;
+      };
+
+    users.groups.polipo =
+      { gid = config.ids.gids.polipo;
+        members = [ "polipo" ];
+      };
+
+    systemd.services.polipo = {
+      description = "caching web proxy";
+      after = [ "network.target" "nss-lookup.target" ];
+      wantedBy = [ "multi-user.target"];
+      serviceConfig = {
+        ExecStart  = "${pkgs.polipo}/bin/polipo -c ${polipoConfig}";
+        User = "polipo";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/powerdns.nix b/nixos/modules/services/networking/powerdns.nix
new file mode 100644
index 00000000000..8cae61b8354
--- /dev/null
+++ b/nixos/modules/services/networking/powerdns.nix
@@ -0,0 +1,47 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.powerdns;
+  configDir = pkgs.writeTextDir "pdns.conf" "${cfg.extraConfig}";
+in {
+  options = {
+    services.powerdns = {
+      enable = mkEnableOption "PowerDNS domain name server";
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "launch=bind";
+        description = ''
+          PowerDNS configuration. Refer to
+          <link xlink:href="https://doc.powerdns.com/authoritative/settings.html"/>
+          for details on supported values.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.packages = [ pkgs.powerdns ];
+
+    systemd.services.pdns = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "mysql.service" "postgresql.service" "openldap.service" ];
+
+      serviceConfig = {
+        ExecStart = [ "" "${pkgs.powerdns}/bin/pdns_server --config-dir=${configDir} --guardian=no --daemon=no --disable-syslog --log-timestamp=no --write-pid=no" ];
+      };
+    };
+
+    users.users.pdns = {
+      isSystemUser = true;
+      group = "pdns";
+      description = "PowerDNS";
+    };
+
+    users.groups.pdns = {};
+
+  };
+}
diff --git a/nixos/modules/services/networking/pppd.nix b/nixos/modules/services/networking/pppd.nix
new file mode 100644
index 00000000000..d1ed25b0238
--- /dev/null
+++ b/nixos/modules/services/networking/pppd.nix
@@ -0,0 +1,154 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.pppd;
+in
+{
+  meta = {
+    maintainers = with maintainers; [ danderson ];
+  };
+
+  options = {
+    services.pppd = {
+      enable = mkEnableOption "pppd";
+
+      package = mkOption {
+        default = pkgs.ppp;
+        defaultText = literalExpression "pkgs.ppp";
+        type = types.package;
+        description = "pppd package to use.";
+      };
+
+      peers = mkOption {
+        default = {};
+        description = "pppd peers.";
+        type = types.attrsOf (types.submodule (
+          { name, ... }:
+          {
+            options = {
+              name = mkOption {
+                type = types.str;
+                default = name;
+                example = "dialup";
+                description = "Name of the PPP peer.";
+              };
+
+              enable = mkOption {
+                type = types.bool;
+                default = true;
+                example = false;
+                description = "Whether to enable this PPP peer.";
+              };
+
+              autostart = mkOption {
+                type = types.bool;
+                default = true;
+                example = false;
+                description = "Whether the PPP session is automatically started at boot time.";
+              };
+
+              config = mkOption {
+                type = types.lines;
+                default = "";
+                description = "pppd configuration for this peer, see the pppd(8) man page.";
+              };
+            };
+          }));
+      };
+    };
+  };
+
+  config = let
+    enabledConfigs = filter (f: f.enable) (attrValues cfg.peers);
+
+    mkEtc = peerCfg: {
+      name = "ppp/peers/${peerCfg.name}";
+      value.text = peerCfg.config;
+    };
+
+    mkSystemd = peerCfg: {
+      name = "pppd-${peerCfg.name}";
+      value = {
+        restartTriggers = [ config.environment.etc."ppp/peers/${peerCfg.name}".source ];
+        before = [ "network.target" ];
+        wants = [ "network.target" ];
+        after = [ "network-pre.target" ];
+        environment = {
+          # pppd likes to write directly into /var/run. This is rude
+          # on a modern system, so we use libredirect to transparently
+          # move those files into /run/pppd.
+          LD_PRELOAD = "${pkgs.libredirect}/lib/libredirect.so";
+          NIX_REDIRECTS = "/var/run=/run/pppd";
+        };
+        serviceConfig = let
+          capabilities = [
+            "CAP_BPF"
+            "CAP_SYS_TTY_CONFIG"
+            "CAP_NET_ADMIN"
+            "CAP_NET_RAW"
+          ];
+        in
+        {
+          ExecStart = "${getBin cfg.package}/sbin/pppd call ${peerCfg.name} nodetach nolog";
+          Restart = "always";
+          RestartSec = 5;
+
+          AmbientCapabilities = capabilities;
+          CapabilityBoundingSet = capabilities;
+          KeyringMode = "private";
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          NoNewPrivileges = true;
+          PrivateMounts = true;
+          PrivateTmp = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelModules = true;
+          # pppd can be configured to tweak kernel settings.
+          ProtectKernelTunables = false;
+          ProtectSystem = "strict";
+          RemoveIPC = true;
+          RestrictAddressFamilies = [
+            "AF_ATMPVC"
+            "AF_ATMSVC"
+            "AF_INET"
+            "AF_INET6"
+            "AF_IPX"
+            "AF_NETLINK"
+            "AF_PACKET"
+            "AF_PPPOX"
+            "AF_UNIX"
+          ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          SecureBits = "no-setuid-fixup-locked noroot-locked";
+          SystemCallFilter = "@system-service";
+          SystemCallArchitectures = "native";
+
+          # All pppd instances on a system must share a runtime
+          # directory in order for PPP multilink to work correctly. So
+          # we give all instances the same /run/pppd directory to store
+          # things in.
+          #
+          # For the same reason, we can't set PrivateUsers=true, because
+          # all instances need to run as the same user to access the
+          # multilink database.
+          RuntimeDirectory = "pppd";
+          RuntimeDirectoryPreserve = true;
+        };
+        wantedBy = mkIf peerCfg.autostart [ "multi-user.target" ];
+      };
+    };
+
+    etcFiles = listToAttrs (map mkEtc enabledConfigs);
+    systemdConfigs = listToAttrs (map mkSystemd enabledConfigs);
+
+  in mkIf cfg.enable {
+    environment.etc = etcFiles;
+    systemd.services = systemdConfigs;
+  };
+}
diff --git a/nixos/modules/services/networking/pptpd.nix b/nixos/modules/services/networking/pptpd.nix
new file mode 100644
index 00000000000..3e7753b9dd3
--- /dev/null
+++ b/nixos/modules/services/networking/pptpd.nix
@@ -0,0 +1,124 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+  options = {
+    services.pptpd = {
+      enable = mkEnableOption "pptpd, the Point-to-Point Tunneling Protocol daemon";
+
+      serverIp = mkOption {
+        type        = types.str;
+        description = "The server-side IP address.";
+        default     = "10.124.124.1";
+      };
+
+      clientIpRange = mkOption {
+        type        = types.str;
+        description = "The range from which client IPs are drawn.";
+        default     = "10.124.124.2-11";
+      };
+
+      maxClients = mkOption {
+        type        = types.int;
+        description = "The maximum number of simultaneous connections.";
+        default     = 10;
+      };
+
+      extraPptpdOptions = mkOption {
+        type        = types.lines;
+        description = "Adds extra lines to the pptpd configuration file.";
+        default     = "";
+      };
+
+      extraPppdOptions = mkOption {
+        type        = types.lines;
+        description = "Adds extra lines to the pppd options file.";
+        default     = "";
+        example     = ''
+          ms-dns 8.8.8.8
+          ms-dns 8.8.4.4
+        '';
+      };
+    };
+  };
+
+  config = mkIf config.services.pptpd.enable {
+    systemd.services.pptpd = let
+      cfg = config.services.pptpd;
+
+      pptpd-conf = pkgs.writeText "pptpd.conf" ''
+        # Inspired from pptpd-1.4.0/samples/pptpd.conf
+        ppp ${ppp-pptpd-wrapped}/bin/pppd
+        option ${pppd-options}
+        pidfile /run/pptpd.pid
+        localip ${cfg.serverIp}
+        remoteip ${cfg.clientIpRange}
+        connections ${toString cfg.maxClients} # (Will get harmless warning if inconsistent with IP range)
+
+        # Extra
+        ${cfg.extraPptpdOptions}
+      '';
+
+      pppd-options = pkgs.writeText "ppp-options-pptpd.conf" ''
+        # From: cat pptpd-1.4.0/samples/options.pptpd | grep -v ^# | grep -v ^$
+        name pptpd
+        refuse-pap
+        refuse-chap
+        refuse-mschap
+        require-mschap-v2
+        require-mppe-128
+        proxyarp
+        lock
+        nobsdcomp
+        novj
+        novjccomp
+        nologfd
+
+        # Extra:
+        ${cfg.extraPppdOptions}
+      '';
+
+      ppp-pptpd-wrapped = pkgs.stdenv.mkDerivation {
+        name         = "ppp-pptpd-wrapped";
+        phases       = [ "installPhase" ];
+        buildInputs  = with pkgs; [ makeWrapper ];
+        installPhase = ''
+          mkdir -p $out/bin
+          makeWrapper ${pkgs.ppp}/bin/pppd $out/bin/pppd \
+            --set LD_PRELOAD    "${pkgs.libredirect}/lib/libredirect.so" \
+            --set NIX_REDIRECTS "/etc/ppp=/etc/ppp-pptpd"
+        '';
+      };
+    in {
+      description = "pptpd server";
+
+      requires = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        mkdir -p -m 700 /etc/ppp-pptpd
+
+        secrets="/etc/ppp-pptpd/chap-secrets"
+
+        [ -f "$secrets" ] || cat > "$secrets" << EOF
+        # From: pptpd-1.4.0/samples/chap-secrets
+        # Secrets for authentication using CHAP
+        # client	server	secret		IP addresses
+        #username	pptpd	password	*
+        EOF
+
+        chown root.root "$secrets"
+        chmod 600 "$secrets"
+      '';
+
+      serviceConfig = {
+        ExecStart = "${pkgs.pptpd}/bin/pptpd --conf ${pptpd-conf}";
+        KillMode  = "process";
+        Restart   = "on-success";
+        Type      = "forking";
+        PIDFile   = "/run/pptpd.pid";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/prayer.nix b/nixos/modules/services/networking/prayer.nix
new file mode 100644
index 00000000000..ae9258b2712
--- /dev/null
+++ b/nixos/modules/services/networking/prayer.nix
@@ -0,0 +1,90 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inherit (pkgs) prayer;
+
+  cfg = config.services.prayer;
+
+  stateDir = "/var/lib/prayer";
+
+  prayerUser = "prayer";
+  prayerGroup = "prayer";
+
+  prayerExtraCfg = pkgs.writeText "extraprayer.cf" ''
+    prefix = "${prayer}"
+    var_prefix = "${stateDir}"
+    prayer_user = "${prayerUser}"
+    prayer_group = "${prayerGroup}"
+    sendmail_path = "/run/wrappers/bin/sendmail"
+
+    use_http_port ${cfg.port}
+
+    ${cfg.extraConfig}
+  '';
+
+  prayerCfg = pkgs.runCommand "prayer.cf" { preferLocalBuild = true; } ''
+    # We have to remove the http_port 80, or it will start a server there
+    cat ${prayer}/etc/prayer.cf | grep -v http_port > $out
+    cat ${prayerExtraCfg} >> $out
+  '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.prayer = {
+
+      enable = mkEnableOption "the prayer webmail http server";
+
+      port = mkOption {
+        default = 2080;
+        type = types.port;
+        description = ''
+          Port the prayer http server is listening to.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "" ;
+        description = ''
+          Extra configuration. Contents will be added verbatim to the configuration file.
+        '';
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.prayer.enable {
+    environment.systemPackages = [ prayer ];
+
+    users.users.${prayerUser} =
+      { uid = config.ids.uids.prayer;
+        description = "Prayer daemon user";
+        home = stateDir;
+      };
+
+    users.groups.${prayerGroup} =
+      { gid = config.ids.gids.prayer; };
+
+    systemd.services.prayer = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig.Type = "forking";
+      preStart = ''
+        mkdir -m 0755 -p ${stateDir}
+        chown ${prayerUser}.${prayerGroup} ${stateDir}
+      '';
+      script = "${prayer}/sbin/prayer --config-file=${prayerCfg}";
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/privoxy.nix b/nixos/modules/services/networking/privoxy.nix
new file mode 100644
index 00000000000..7bc964d5f34
--- /dev/null
+++ b/nixos/modules/services/networking/privoxy.nix
@@ -0,0 +1,279 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.privoxy;
+
+  serialise = name: val:
+         if isList val then concatMapStrings (serialise name) val
+    else if isBool val then serialise name (if val then "1" else "0")
+    else "${name} ${toString val}\n";
+
+  configType = with types;
+    let atom = oneOf [ int bool string path ];
+    in attrsOf (either atom (listOf atom))
+    // { description = ''
+          privoxy configuration type. The format consists of an attribute
+          set of settings. Each setting can be either a value (integer, string,
+          boolean or path) or a list of such values.
+        '';
+       };
+
+  ageType = types.str // {
+    check = x:
+      isString x &&
+      (builtins.match "([0-9]+([smhdw]|min|ms|us)*)+" x != null);
+    description = "tmpfiles.d(5) age format";
+  };
+
+  configFile = pkgs.writeText "privoxy.conf"
+    (concatStrings (
+      # Relative paths in some options are relative to confdir. Privoxy seems
+      # to parse the options in order of appearance, so this must come first.
+      # Nix however doesn't preserve the order in attrsets, so we have to
+      # hardcode confdir here.
+      [ "confdir ${pkgs.privoxy}/etc\n" ]
+      ++ mapAttrsToList serialise cfg.settings
+    ));
+
+  inspectAction = pkgs.writeText "inspect-all-https.action"
+    ''
+      # Enable HTTPS inspection for all requests
+      {+https-inspection}
+      /
+    '';
+
+in
+
+{
+
+  ###### interface
+
+  options.services.privoxy = {
+
+    enable = mkEnableOption "Privoxy, non-caching filtering proxy";
+
+    enableTor = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to configure Privoxy to use Tor's faster SOCKS port,
+        suitable for HTTP.
+      '';
+    };
+
+    inspectHttps = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to configure Privoxy to inspect HTTPS requests, meaning all
+        encrypted traffic will be filtered as well. This works by decrypting
+        and re-encrypting the requests using a per-domain generated certificate.
+
+        To issue per-domain certificates, Privoxy must be provided with a CA
+        certificate, using the <literal>ca-cert-file</literal>,
+        <literal>ca-key-file</literal> settings.
+
+        <warning><para>
+          The CA certificate must also be added to the system trust roots,
+          otherwise browsers will reject all Privoxy certificates as invalid.
+          You can do so by using the option
+          <option>security.pki.certificateFiles</option>.
+        </para></warning>
+      '';
+    };
+
+    certsLifetime = mkOption {
+      type = ageType;
+      default = "10d";
+      example = "12h";
+      description = ''
+        If <literal>inspectHttps</literal> is enabled, the time generated HTTPS
+        certificates will be stored in a temporary directory for reuse. Once
+        the lifetime has expired the directory will cleared and the certificate
+        will have to be generated again, on-demand.
+
+        Depending on the traffic, you may want to reduce the lifetime to limit
+        the disk usage, since Privoxy itself never deletes the certificates.
+
+        <note><para>The format is that of the <literal>tmpfiles.d(5)</literal>
+        Age parameter.</para></note>
+      '';
+    };
+
+    userActions = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Actions to be included in a <literal>user.action</literal> file. This
+        will have a higher priority and can be used to override all other
+        actions.
+      '';
+    };
+
+    userFilters = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Filters to be included in a <literal>user.filter</literal> file. This
+        will have a higher priority and can be used to override all other
+        filters definitions.
+      '';
+    };
+
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = configType;
+
+        options.listen-address = mkOption {
+          type = types.str;
+          default = "127.0.0.1:8118";
+          description = "Pair of address:port the proxy server is listening to.";
+        };
+
+        options.enable-edit-actions = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Whether the web-based actions file editor may be used.";
+        };
+
+        options.actionsfile = mkOption {
+          type = types.listOf types.str;
+          # This must come after all other entries, in order to override the
+          # other actions/filters installed by Privoxy or the user.
+          apply = x: x ++ optional (cfg.userActions != "")
+            (toString (pkgs.writeText "user.actions" cfg.userActions));
+          default = [ "match-all.action" "default.action" ];
+          description = ''
+            List of paths to Privoxy action files. These paths may either be
+            absolute or relative to the privoxy configuration directory.
+          '';
+        };
+
+        options.filterfile = mkOption {
+          type = types.listOf types.str;
+          default = [ "default.filter" ];
+          apply = x: x ++ optional (cfg.userFilters != "")
+            (toString (pkgs.writeText "user.filter" cfg.userFilters));
+          description = ''
+            List of paths to Privoxy filter files. These paths may either be
+            absolute or relative to the privoxy configuration directory.
+          '';
+        };
+      };
+      default = {};
+      example = literalExpression ''
+        { # Listen on IPv6 only
+          listen-address = "[::]:8118";
+
+          # Forward .onion requests to Tor
+          forward-socks5 = ".onion localhost:9050 .";
+
+          # Log redirects and filters
+          debug = [ 128 64 ];
+          # This is equivalent to writing these lines
+          # in the Privoxy configuration file:
+          # debug 128
+          # debug 64
+        }
+      '';
+      description = ''
+        This option is mapped to the main Privoxy configuration file.
+        Check out the Privoxy user manual at
+        <link xlink:href="https://www.privoxy.org/user-manual/config.html"/>
+        for available settings and documentation.
+
+        <note><para>
+          Repeated settings can be represented by using a list.
+        </para></note>
+      '';
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.privoxy = {
+      description = "Privoxy daemon user";
+      isSystemUser = true;
+      group = "privoxy";
+    };
+
+    users.groups.privoxy = {};
+
+    systemd.tmpfiles.rules = optional cfg.inspectHttps
+      "d ${cfg.settings.certificate-directory} 0770 privoxy privoxy ${cfg.certsLifetime}";
+
+    systemd.services.privoxy = {
+      description = "Filtering web proxy";
+      after = [ "network.target" "nss-lookup.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        User = "privoxy";
+        Group = "privoxy";
+        ExecStart = "${pkgs.privoxy}/bin/privoxy --no-daemon ${configFile}";
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectSystem = "full";
+      };
+      unitConfig =  mkIf cfg.inspectHttps {
+        ConditionPathExists = with cfg.settings;
+          [ ca-cert-file ca-key-file ];
+      };
+    };
+
+    services.tor.settings.SOCKSPort = mkIf cfg.enableTor [
+      # Route HTTP traffic over a faster port (without IsolateDestAddr).
+      { addr = "127.0.0.1"; port = 9063; IsolateDestAddr = false; }
+    ];
+
+    services.privoxy.settings = {
+      user-manual = "${pkgs.privoxy}/share/doc/privoxy/user-manual";
+      # This is needed for external filters
+      temporary-directory = "/tmp";
+      filterfile = [ "default.filter" ];
+      actionsfile =
+        [ "match-all.action"
+          "default.action"
+        ] ++ optional cfg.inspectHttps (toString inspectAction);
+    } // (optionalAttrs cfg.enableTor {
+      forward-socks5 = "/ 127.0.0.1:9063 .";
+      toggle = true;
+      enable-remote-toggle = false;
+      enable-edit-actions = false;
+      enable-remote-http-toggle = false;
+    }) // (optionalAttrs cfg.inspectHttps {
+      # This allows setting absolute key/crt paths
+      ca-directory = "/var/empty";
+      certificate-directory = "/run/privoxy/certs";
+      trusted-cas-file = "/etc/ssl/certs/ca-certificates.crt";
+    });
+
+  };
+
+  imports =
+    let
+      top = x: [ "services" "privoxy" x ];
+      setting = x: [ "services" "privoxy" "settings" x ];
+    in
+    [ (mkRenamedOptionModule (top "enableEditActions") (setting "enable-edit-actions"))
+      (mkRenamedOptionModule (top "listenAddress") (setting "listen-address"))
+      (mkRenamedOptionModule (top "actionsFiles") (setting "actionsfile"))
+      (mkRenamedOptionModule (top "filterFiles") (setting "filterfile"))
+      (mkRemovedOptionModule (top "extraConfig")
+      ''
+        Use services.privoxy.settings instead.
+        This is part of the general move to use structured settings instead of raw
+        text for config as introduced by RFC0042:
+        https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md
+      '')
+    ];
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+}
diff --git a/nixos/modules/services/networking/prosody.nix b/nixos/modules/services/networking/prosody.nix
new file mode 100644
index 00000000000..42596ccfefd
--- /dev/null
+++ b/nixos/modules/services/networking/prosody.nix
@@ -0,0 +1,882 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.prosody;
+
+  sslOpts = { ... }: {
+
+    options = {
+
+      key = mkOption {
+        type = types.path;
+        description = "Path to the key file.";
+      };
+
+      # TODO: rename to certificate to match the prosody config
+      cert = mkOption {
+        type = types.path;
+        description = "Path to the certificate file.";
+      };
+
+      extraOptions = mkOption {
+        type = types.attrs;
+        default = {};
+        description = "Extra SSL configuration options.";
+      };
+
+    };
+  };
+
+  discoOpts = {
+    options = {
+      url = mkOption {
+        type = types.str;
+        description = "URL of the endpoint you want to make discoverable";
+      };
+      description = mkOption {
+        type = types.str;
+        description = "A short description of the endpoint you want to advertise";
+      };
+    };
+  };
+
+  moduleOpts = {
+    # Required for compliance with https://compliance.conversations.im/about/
+    roster = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Allow users to have a roster";
+    };
+
+    saslauth = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Authentication for clients and servers. Recommended if you want to log in.";
+    };
+
+    tls = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Add support for secure TLS on c2s/s2s connections";
+    };
+
+    dialback = mkOption {
+      type = types.bool;
+      default = true;
+      description = "s2s dialback support";
+    };
+
+    disco = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Service discovery";
+    };
+
+    # Not essential, but recommended
+    carbons = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Keep multiple clients in sync";
+    };
+
+    csi = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Implements the CSI protocol that allows clients to report their active/inactive state to the server";
+    };
+
+    cloud_notify = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Push notifications to inform users of new messages or other pertinent information even when they have no XMPP clients online";
+    };
+
+    pep = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Enables users to publish their mood, activity, playing music and more";
+    };
+
+    private = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Private XML storage (for room bookmarks, etc.)";
+    };
+
+    blocklist = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Allow users to block communications with other users";
+    };
+
+    vcard = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Allow users to set vCards";
+    };
+
+    vcard_legacy = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Converts users profiles and Avatars between old and new formats";
+    };
+
+    bookmarks = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Allows interop between older clients that use XEP-0048: Bookmarks in its 1.0 version and recent clients which use it in PEP";
+    };
+
+    # Nice to have
+    version = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Replies to server version requests";
+    };
+
+    uptime = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Report how long server has been running";
+    };
+
+    time = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Let others know the time here on this server";
+    };
+
+    ping = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Replies to XMPP pings with pongs";
+    };
+
+    register = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Allow users to register on this server using a client and change passwords";
+    };
+
+    mam = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Store messages in an archive and allow users to access it";
+    };
+
+    smacks = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Allow a client to resume a disconnected session, and prevent message loss";
+    };
+
+    # Admin interfaces
+    admin_adhoc = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Allows administration via an XMPP client that supports ad-hoc commands";
+    };
+
+    http_files = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Serve static files from a directory over HTTP";
+    };
+
+    proxy65 = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Enables a file transfer proxy service which clients behind NAT can use";
+    };
+
+    admin_telnet = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Opens telnet console interface on localhost port 5582";
+    };
+
+    # HTTP modules
+    bosh = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable BOSH clients, aka 'Jabber over HTTP'";
+    };
+
+    websocket = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable WebSocket support";
+    };
+
+    # Other specific functionality
+    limits = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable bandwidth limiting for XMPP connections";
+    };
+
+    groups = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Shared roster support";
+    };
+
+    server_contact_info = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Publish contact information for this service";
+    };
+
+    announce = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Send announcement to all online users";
+    };
+
+    welcome = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Welcome users who register accounts";
+    };
+
+    watchregistrations = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Alert admins of registrations";
+    };
+
+    motd = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Send a message to users when they log in";
+    };
+
+    legacyauth = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Legacy authentication. Only used by some old clients and bots";
+    };
+  };
+
+  toLua = x:
+    if builtins.isString x then ''"${x}"''
+    else if builtins.isBool x then boolToString x
+    else if builtins.isInt x then toString x
+    else if builtins.isList x then ''{ ${lib.concatStringsSep ", " (map (n: toLua n) x) } }''
+    else throw "Invalid Lua value";
+
+  createSSLOptsStr = o: ''
+    ssl = {
+      cafile = "/etc/ssl/certs/ca-bundle.crt";
+      key = "${o.key}";
+      certificate = "${o.cert}";
+      ${concatStringsSep "\n" (mapAttrsToList (name: value: "${name} = ${toLua value};") o.extraOptions)}
+    };
+  '';
+
+  mucOpts = { ... }: {
+    options = {
+      domain = mkOption {
+        type = types.str;
+        description = "Domain name of the MUC";
+      };
+      name = mkOption {
+        type = types.str;
+        description = "The name to return in service discovery responses for the MUC service itself";
+        default = "Prosody Chatrooms";
+      };
+      restrictRoomCreation = mkOption {
+        type = types.enum [ true false "admin" "local" ];
+        default = false;
+        description = "Restrict room creation to server admins";
+      };
+      maxHistoryMessages = mkOption {
+        type = types.int;
+        default = 20;
+        description = "Specifies a limit on what each room can be configured to keep";
+      };
+      roomLocking = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enables room locking, which means that a room must be
+          configured before it can be used. Locked rooms are invisible
+          and cannot be entered by anyone but the creator
+        '';
+      };
+      roomLockTimeout = mkOption {
+        type = types.int;
+        default = 300;
+        description = ''
+          Timout after which the room is destroyed or unlocked if not
+          configured, in seconds
+       '';
+      };
+      tombstones = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          When a room is destroyed, it leaves behind a tombstone which
+          prevents the room being entered or recreated. It also allows
+          anyone who was not in the room at the time it was destroyed
+          to learn about it, and to update their bookmarks. Tombstones
+          prevents the case where someone could recreate a previously
+          semi-anonymous room in order to learn the real JIDs of those
+          who often join there.
+        '';
+      };
+      tombstoneExpiry = mkOption {
+        type = types.int;
+        default = 2678400;
+        description = ''
+          This settings controls how long a tombstone is considered
+          valid. It defaults to 31 days. After this time, the room in
+          question can be created again.
+        '';
+      };
+
+      vcard_muc = mkOption {
+        type = types.bool;
+        default = true;
+      description = "Adds the ability to set vCard for Multi User Chat rooms";
+      };
+
+      # Extra parameters. Defaulting to prosody default values.
+      # Adding them explicitly to make them visible from the options
+      # documentation.
+      #
+      # See https://prosody.im/doc/modules/mod_muc for more details.
+      roomDefaultPublic = mkOption {
+        type = types.bool;
+        default = true;
+        description = "If set, the MUC rooms will be public by default.";
+      };
+      roomDefaultMembersOnly = mkOption {
+        type = types.bool;
+        default = false;
+        description = "If set, the MUC rooms will only be accessible to the members by default.";
+      };
+      roomDefaultModerated = mkOption {
+        type = types.bool;
+        default = false;
+        description = "If set, the MUC rooms will be moderated by default.";
+      };
+      roomDefaultPublicJids = mkOption {
+        type = types.bool;
+        default = false;
+        description = "If set, the MUC rooms will display the public JIDs by default.";
+      };
+      roomDefaultChangeSubject = mkOption {
+        type = types.bool;
+        default = false;
+        description = "If set, the rooms will display the public JIDs by default.";
+      };
+      roomDefaultHistoryLength = mkOption {
+        type = types.int;
+        default = 20;
+        description = "Number of history message sent to participants by default.";
+      };
+      roomDefaultLanguage = mkOption {
+        type = types.str;
+        default = "en";
+        description = "Default room language.";
+      };
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Additional MUC specific configuration";
+      };
+    };
+  };
+
+  uploadHttpOpts = { ... }: {
+    options = {
+      domain = mkOption {
+        type = types.nullOr types.str;
+        description = "Domain name for the http-upload service";
+      };
+      uploadFileSizeLimit = mkOption {
+        type = types.str;
+        default = "50 * 1024 * 1024";
+        description = "Maximum file size, in bytes. Defaults to 50MB.";
+      };
+      uploadExpireAfter = mkOption {
+        type = types.str;
+        default = "60 * 60 * 24 * 7";
+        description = "Max age of a file before it gets deleted, in seconds.";
+      };
+      userQuota = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        example = 1234;
+        description = ''
+          Maximum size of all uploaded files per user, in bytes. There
+          will be no quota if this option is set to null.
+        '';
+      };
+      httpUploadPath = mkOption {
+        type = types.str;
+        description = ''
+          Directory where the uploaded files will be stored. By
+          default, uploaded files are put in a sub-directory of the
+          default Prosody storage path (usually /var/lib/prosody).
+        '';
+        default = "/var/lib/prosody";
+      };
+    };
+  };
+
+  vHostOpts = { ... }: {
+
+    options = {
+
+      # TODO: require attribute
+      domain = mkOption {
+        type = types.str;
+        description = "Domain name";
+      };
+
+      enabled = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the virtual host";
+      };
+
+      ssl = mkOption {
+        type = types.nullOr (types.submodule sslOpts);
+        default = null;
+        description = "Paths to SSL files";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Additional virtual host specific configuration";
+      };
+
+    };
+
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.prosody = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the prosody server";
+      };
+
+      xmppComplianceSuite = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          The XEP-0423 defines a set of recommended XEPs to implement
+          for a server. It's generally a good idea to implement this
+          set of extensions if you want to provide your users with a
+          good XMPP experience.
+
+          This NixOS module aims to provide a "advanced server"
+          experience as per defined in the XEP-0423[1] specification.
+
+          Setting this option to true will prevent you from building a
+          NixOS configuration which won't comply with this standard.
+          You can explicitely decide to ignore this standard if you
+          know what you are doing by setting this option to false.
+
+          [1] https://xmpp.org/extensions/xep-0423.html
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        description = "Prosody package to use";
+        default = pkgs.prosody;
+        defaultText = literalExpression "pkgs.prosody";
+        example = literalExpression ''
+          pkgs.prosody.override {
+            withExtraLibs = [ pkgs.luaPackages.lpty ];
+            withCommunityModules = [ "auth_external" ];
+          };
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        description = "Directory where Prosody stores its data";
+        default = "/var/lib/prosody";
+      };
+
+      disco_items = mkOption {
+        type = types.listOf (types.submodule discoOpts);
+        default = [];
+        description = "List of discoverable items you want to advertise.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "prosody";
+        description = "User account under which prosody runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "prosody";
+        description = "Group account under which prosody runs.";
+      };
+
+      allowRegistration = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Allow account creation";
+      };
+
+      # HTTP server-related options
+      httpPorts = mkOption {
+        type = types.listOf types.int;
+        description = "Listening HTTP ports list for this service.";
+        default = [ 5280 ];
+      };
+
+      httpInterfaces = mkOption {
+        type = types.listOf types.str;
+        default = [ "*" "::" ];
+        description = "Interfaces on which the HTTP server will listen on.";
+      };
+
+      httpsPorts = mkOption {
+        type = types.listOf types.int;
+        description = "Listening HTTPS ports list for this service.";
+        default = [ 5281 ];
+      };
+
+      httpsInterfaces = mkOption {
+        type = types.listOf types.str;
+        default = [ "*" "::" ];
+        description = "Interfaces on which the HTTPS server will listen on.";
+      };
+
+      c2sRequireEncryption = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Force clients to use encrypted connections? This option will
+          prevent clients from authenticating unless they are using encryption.
+        '';
+      };
+
+      s2sRequireEncryption = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Force servers to use encrypted connections? This option will
+          prevent servers from authenticating unless they are using encryption.
+          Note that this is different from authentication.
+        '';
+      };
+
+      s2sSecureAuth = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Force certificate authentication for server-to-server connections?
+          This provides ideal security, but requires servers you communicate
+          with to support encryption AND present valid, trusted certificates.
+          For more information see https://prosody.im/doc/s2s#security
+        '';
+      };
+
+      s2sInsecureDomains = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "insecure.example.com" ];
+        description = ''
+          Some servers have invalid or self-signed certificates. You can list
+          remote domains here that will not be required to authenticate using
+          certificates. They will be authenticated using DNS instead, even
+          when s2s_secure_auth is enabled.
+        '';
+      };
+
+      s2sSecureDomains = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "jabber.org" ];
+        description = ''
+          Even if you leave s2s_secure_auth disabled, you can still require valid
+          certificates for some domains by specifying a list here.
+        '';
+      };
+
+
+      modules = moduleOpts;
+
+      extraModules = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Enable custom modules";
+      };
+
+      extraPluginPaths = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = "Addtional path in which to look find plugins/modules";
+      };
+
+      uploadHttp = mkOption {
+        description = ''
+          Configures the Prosody builtin HTTP server to handle user uploads.
+        '';
+        type = types.nullOr (types.submodule uploadHttpOpts);
+        default = null;
+        example = {
+          domain = "uploads.my-xmpp-example-host.org";
+        };
+      };
+
+      muc = mkOption {
+        type = types.listOf (types.submodule mucOpts);
+        default = [ ];
+        example = [ {
+          domain = "conference.my-xmpp-example-host.org";
+        } ];
+        description = "Multi User Chat (MUC) configuration";
+      };
+
+      virtualHosts = mkOption {
+
+        description = "Define the virtual hosts";
+
+        type = with types; attrsOf (submodule vHostOpts);
+
+        example = {
+          myhost = {
+            domain = "my-xmpp-example-host.org";
+            enabled = true;
+          };
+        };
+
+        default = {
+          localhost = {
+            domain = "localhost";
+            enabled = true;
+          };
+        };
+
+      };
+
+      ssl = mkOption {
+        type = types.nullOr (types.submodule sslOpts);
+        default = null;
+        description = "Paths to SSL files";
+      };
+
+      admins = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "admin1@example.com" "admin2@example.com" ];
+        description = "List of administrators of the current host";
+      };
+
+      authentication = mkOption {
+        type = types.enum [ "internal_plain" "internal_hashed" "cyrus" "anonymous" ];
+        default = "internal_hashed";
+        example = "internal_plain";
+        description = "Authentication mechanism used for logins.";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Additional prosody configuration";
+      };
+
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = let
+      genericErrMsg = ''
+
+          Having a server not XEP-0423-compliant might make your XMPP
+          experience terrible. See the NixOS manual for further
+          informations.
+
+          If you know what you're doing, you can disable this warning by
+          setting config.services.prosody.xmppComplianceSuite to false.
+      '';
+      errors = [
+        { assertion = (builtins.length cfg.muc > 0) || !cfg.xmppComplianceSuite;
+          message = ''
+            You need to setup at least a MUC domain to comply with
+            XEP-0423.
+          '' + genericErrMsg;}
+        { assertion = cfg.uploadHttp != null || !cfg.xmppComplianceSuite;
+          message = ''
+            You need to setup the uploadHttp module through
+            config.services.prosody.uploadHttp to comply with
+            XEP-0423.
+          '' + genericErrMsg;}
+      ];
+    in errors;
+
+    environment.systemPackages = [ cfg.package ];
+
+    environment.etc."prosody/prosody.cfg.lua".text =
+      let
+        httpDiscoItems = if (cfg.uploadHttp != null)
+            then [{ url = cfg.uploadHttp.domain; description = "HTTP upload endpoint";}]
+            else [];
+        mucDiscoItems = builtins.foldl'
+            (acc: muc: [{ url = muc.domain; description = "${muc.domain} MUC endpoint";}] ++ acc)
+            []
+            cfg.muc;
+        discoItems = cfg.disco_items ++ httpDiscoItems ++ mucDiscoItems;
+      in ''
+
+      pidfile = "/run/prosody/prosody.pid"
+
+      log = "*syslog"
+
+      data_path = "${cfg.dataDir}"
+      plugin_paths = {
+        ${lib.concatStringsSep ", " (map (n: "\"${n}\"") cfg.extraPluginPaths) }
+      }
+
+      ${ optionalString  (cfg.ssl != null) (createSSLOptsStr cfg.ssl) }
+
+      admins = ${toLua cfg.admins}
+
+      -- we already build with libevent, so we can just enable it for a more performant server
+      use_libevent = true
+
+      modules_enabled = {
+
+        ${ lib.concatStringsSep "\n  " (lib.mapAttrsToList
+          (name: val: optionalString val "${toLua name};")
+        cfg.modules) }
+        ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.package.communityModules)}
+        ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.extraModules)}
+      };
+
+      disco_items = {
+      ${ lib.concatStringsSep "\n" (builtins.map (x: ''{ "${x.url}", "${x.description}"};'') discoItems)}
+      };
+
+      allow_registration = ${toLua cfg.allowRegistration}
+
+      c2s_require_encryption = ${toLua cfg.c2sRequireEncryption}
+
+      s2s_require_encryption = ${toLua cfg.s2sRequireEncryption}
+
+      s2s_secure_auth = ${toLua cfg.s2sSecureAuth}
+
+      s2s_insecure_domains = ${toLua cfg.s2sInsecureDomains}
+
+      s2s_secure_domains = ${toLua cfg.s2sSecureDomains}
+
+      authentication = ${toLua cfg.authentication}
+
+      http_interfaces = ${toLua cfg.httpInterfaces}
+
+      https_interfaces = ${toLua cfg.httpsInterfaces}
+
+      http_ports = ${toLua cfg.httpPorts}
+
+      https_ports = ${toLua cfg.httpsPorts}
+
+      ${ cfg.extraConfig }
+
+      ${lib.concatMapStrings (muc: ''
+        Component ${toLua muc.domain} "muc"
+            modules_enabled = { "muc_mam"; ${optionalString muc.vcard_muc ''"vcard_muc";'' } }
+            name = ${toLua muc.name}
+            restrict_room_creation = ${toLua muc.restrictRoomCreation}
+            max_history_messages = ${toLua muc.maxHistoryMessages}
+            muc_room_locking = ${toLua muc.roomLocking}
+            muc_room_lock_timeout = ${toLua muc.roomLockTimeout}
+            muc_tombstones = ${toLua muc.tombstones}
+            muc_tombstone_expiry = ${toLua muc.tombstoneExpiry}
+            muc_room_default_public = ${toLua muc.roomDefaultPublic}
+            muc_room_default_members_only = ${toLua muc.roomDefaultMembersOnly}
+            muc_room_default_moderated = ${toLua muc.roomDefaultModerated}
+            muc_room_default_public_jids = ${toLua muc.roomDefaultPublicJids}
+            muc_room_default_change_subject = ${toLua muc.roomDefaultChangeSubject}
+            muc_room_default_history_length = ${toLua muc.roomDefaultHistoryLength}
+            muc_room_default_language = ${toLua muc.roomDefaultLanguage}
+            ${ muc.extraConfig }
+        '') cfg.muc}
+
+      ${ lib.optionalString (cfg.uploadHttp != null) ''
+        Component ${toLua cfg.uploadHttp.domain} "http_upload"
+            http_upload_file_size_limit = ${cfg.uploadHttp.uploadFileSizeLimit}
+            http_upload_expire_after = ${cfg.uploadHttp.uploadExpireAfter}
+            ${lib.optionalString (cfg.uploadHttp.userQuota != null) "http_upload_quota = ${toLua cfg.uploadHttp.userQuota}"}
+            http_upload_path = ${toLua cfg.uploadHttp.httpUploadPath}
+      ''}
+
+      ${ lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: ''
+        VirtualHost "${v.domain}"
+          enabled = ${boolToString v.enabled};
+          ${ optionalString (v.ssl != null) (createSSLOptsStr v.ssl) }
+          ${ v.extraConfig }
+        '') cfg.virtualHosts) }
+    '';
+
+    users.users.prosody = mkIf (cfg.user == "prosody") {
+      uid = config.ids.uids.prosody;
+      description = "Prosody user";
+      createHome = true;
+      inherit (cfg) group;
+      home = "${cfg.dataDir}";
+    };
+
+    users.groups.prosody = mkIf (cfg.group == "prosody") {
+      gid = config.ids.gids.prosody;
+    };
+
+    systemd.services.prosody = {
+      description = "Prosody XMPP server";
+      after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ config.environment.etc."prosody/prosody.cfg.lua".source ];
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        Type = "forking";
+        RuntimeDirectory = [ "prosody" ];
+        PIDFile = "/run/prosody/prosody.pid";
+        ExecStart = "${cfg.package}/bin/prosodyctl start";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+
+        MemoryDenyWriteExecute = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateTmp = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+      };
+    };
+
+  };
+  meta.doc = ./prosody.xml;
+}
diff --git a/nixos/modules/services/networking/prosody.xml b/nixos/modules/services/networking/prosody.xml
new file mode 100644
index 00000000000..6358d744ff7
--- /dev/null
+++ b/nixos/modules/services/networking/prosody.xml
@@ -0,0 +1,87 @@
+<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/quassel.nix b/nixos/modules/services/networking/quassel.nix
new file mode 100644
index 00000000000..844c9a6b8b3
--- /dev/null
+++ b/nixos/modules/services/networking/quassel.nix
@@ -0,0 +1,139 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.quassel;
+  opt = options.services.quassel;
+  quassel = cfg.package;
+  user = if cfg.user != null then cfg.user else "quassel";
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.quassel = {
+
+      enable = mkEnableOption "the Quassel IRC client daemon";
+
+      certificateFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Path to the certificate used for SSL connections with clients.
+        '';
+      };
+
+      requireSSL = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Require SSL for connections from clients.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.quasselDaemon;
+        defaultText = literalExpression "pkgs.quasselDaemon";
+        description = ''
+          The package of the quassel daemon.
+        '';
+      };
+
+      interfaces = mkOption {
+        type = types.listOf types.str;
+        default = [ "127.0.0.1" ];
+        description = ''
+          The interfaces the Quassel daemon will be listening to.  If `[ 127.0.0.1 ]',
+          only clients on the local host can connect to it; if `[ 0.0.0.0 ]', clients
+          can access it from any network interface.
+        '';
+      };
+
+      portNumber = mkOption {
+        type = types.port;
+        default = 4242;
+        description = ''
+          The port number the Quassel daemon will be listening to.
+        '';
+      };
+
+      dataDir = mkOption {
+        default = "/home/${user}/.config/quassel-irc.org";
+        defaultText = literalExpression ''
+          "/home/''${config.${opt.user}}/.config/quassel-irc.org"
+        '';
+        type = types.str;
+        description = ''
+          The directory holding configuration files, the SQlite database and the SSL Cert.
+        '';
+      };
+
+      user = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description = ''
+          The existing user the Quassel daemon should run as. If left empty, a default "quassel" user will be created.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.requireSSL -> cfg.certificateFile != null;
+        message = "Quassel needs a certificate file in order to require SSL";
+      }];
+
+    users.users = optionalAttrs (cfg.user == null) {
+      quassel = {
+        name = "quassel";
+        description = "Quassel IRC client daemon";
+        group = "quassel";
+        uid = config.ids.uids.quassel;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.user == null) {
+      quassel = {
+        name = "quassel";
+        gid = config.ids.gids.quassel;
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - ${user} - - -"
+    ];
+
+    systemd.services.quassel =
+      { description = "Quassel IRC client daemon";
+
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ] ++ optional config.services.postgresql.enable "postgresql.service"
+                                     ++ optional config.services.mysql.enable "mysql.service";
+
+        serviceConfig =
+        {
+          ExecStart = concatStringsSep " " ([
+            "${quassel}/bin/quasselcore"
+            "--listen=${concatStringsSep "," cfg.interfaces}"
+            "--port=${toString cfg.portNumber}"
+            "--configdir=${cfg.dataDir}"
+          ] ++ optional cfg.requireSSL "--require-ssl"
+            ++ optional (cfg.certificateFile != null) "--ssl-cert=${cfg.certificateFile}");
+          User = user;
+        };
+      };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/quicktun.nix b/nixos/modules/services/networking/quicktun.nix
new file mode 100644
index 00000000000..438e67d5ebb
--- /dev/null
+++ b/nixos/modules/services/networking/quicktun.nix
@@ -0,0 +1,118 @@
+{ config, pkgs, lib, ... }:
+
+let
+
+  cfg = config.services.quicktun;
+
+in
+
+with lib;
+
+{
+  options = {
+
+    services.quicktun = mkOption {
+      default = { };
+      description = "QuickTun tunnels";
+      type = types.attrsOf (types.submodule {
+        options = {
+          tunMode = mkOption {
+            type = types.int;
+            default = 0;
+            example = 1;
+            description = "";
+          };
+
+          remoteAddress = mkOption {
+            type = types.str;
+            example = "tunnel.example.com";
+            description = "";
+          };
+
+          localAddress = mkOption {
+            type = types.str;
+            example = "0.0.0.0";
+            description = "";
+          };
+
+          localPort = mkOption {
+            type = types.int;
+            default = 2998;
+            description = "";
+          };
+
+          remotePort = mkOption {
+            type = types.int;
+            default = 2998;
+            description = "";
+          };
+
+          remoteFloat = mkOption {
+            type = types.int;
+            default = 0;
+            description = "";
+          };
+
+          protocol = mkOption {
+            type = types.str;
+            default = "nacltai";
+            description = "";
+          };
+
+          privateKey = mkOption {
+            type = types.str;
+            description = "";
+          };
+
+          publicKey = mkOption {
+            type = types.str;
+            description = "";
+          };
+
+          timeWindow = mkOption {
+            type = types.int;
+            default = 5;
+            description = "";
+          };
+
+          upScript = mkOption {
+            type = types.lines;
+            default = "";
+            description = "";
+          };
+        };
+      });
+    };
+
+  };
+
+  config = mkIf (cfg != []) {
+    systemd.services = foldr (a: b: a // b) {} (
+      mapAttrsToList (name: qtcfg: {
+        "quicktun-${name}" = {
+          wantedBy = [ "multi-user.target" ];
+          after = [ "network.target" ];
+          environment = {
+            INTERFACE = name;
+            TUN_MODE = toString qtcfg.tunMode;
+            REMOTE_ADDRESS = qtcfg.remoteAddress;
+            LOCAL_ADDRESS = qtcfg.localAddress;
+            LOCAL_PORT = toString qtcfg.localPort;
+            REMOTE_PORT = toString qtcfg.remotePort;
+            REMOTE_FLOAT = toString qtcfg.remoteFloat;
+            PRIVATE_KEY = qtcfg.privateKey;
+            PUBLIC_KEY = qtcfg.publicKey;
+            TIME_WINDOW = toString qtcfg.timeWindow;
+            TUN_UP_SCRIPT = pkgs.writeScript "quicktun-${name}-up.sh" qtcfg.upScript;
+            SUID = "nobody";
+          };
+          serviceConfig = {
+            Type = "simple";
+            ExecStart = "${pkgs.quicktun}/bin/quicktun.${qtcfg.protocol}";
+          };
+        };
+      }) cfg
+    );
+  };
+
+}
diff --git a/nixos/modules/services/networking/quorum.nix b/nixos/modules/services/networking/quorum.nix
new file mode 100644
index 00000000000..bddcd18c7fb
--- /dev/null
+++ b/nixos/modules/services/networking/quorum.nix
@@ -0,0 +1,231 @@
+{ config, options, pkgs, lib, ... }:
+let
+
+  inherit (lib) mkEnableOption mkIf mkOption literalExpression types optionalString;
+
+  cfg = config.services.quorum;
+  opt = options.services.quorum;
+  dataDir = "/var/lib/quorum";
+  genesisFile = pkgs.writeText "genesis.json" (builtins.toJSON cfg.genesis);
+  staticNodesFile = pkgs.writeText "static-nodes.json" (builtins.toJSON cfg.staticNodes);
+
+in {
+  options = {
+
+    services.quorum = {
+      enable = mkEnableOption "Quorum blockchain daemon";
+
+      user = mkOption {
+        type = types.str;
+        default = "quorum";
+        description = "The user as which to run quorum.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = cfg.user;
+        defaultText = literalExpression "config.${opt.user}";
+        description = "The group as which to run quorum.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 21000;
+        description = "Override the default port on which to listen for connections.";
+      };
+
+      nodekeyFile = mkOption {
+        type = types.path;
+        default = "${dataDir}/nodekey";
+        description = "Path to the nodekey.";
+      };
+
+      staticNodes = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "enode://dd333ec28f0a8910c92eb4d336461eea1c20803eed9cf2c056557f986e720f8e693605bba2f4e8f289b1162e5ac7c80c914c7178130711e393ca76abc1d92f57@0.0.0.0:30303?discport=0" ];
+        description = "List of validator nodes.";
+      };
+
+      privateconfig = mkOption {
+        type = types.str;
+        default = "ignore";
+        description = "Configuration of privacy transaction manager.";
+      };
+
+      syncmode = mkOption {
+        type = types.enum [ "fast" "full" "light" ];
+        default = "full";
+        description = "Blockchain sync mode.";
+      };
+
+      blockperiod = mkOption {
+        type = types.int;
+        default = 5;
+        description = "Default minimum difference between two consecutive block's timestamps in seconds.";
+      };
+
+      permissioned = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Allow only a defined list of nodes to connect.";
+      };
+
+      rpc = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Enable RPC interface.";
+        };
+
+        address = mkOption {
+          type = types.str;
+          default = "0.0.0.0";
+          description = "Listening address for RPC connections.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 22004;
+          description = "Override the default port on which to listen for RPC connections.";
+        };
+
+        api = mkOption {
+          type = types.str;
+          default = "admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul";
+          description = "API's offered over the HTTP-RPC interface.";
+        };
+      };
+
+     ws = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Enable WS-RPC interface.";
+        };
+
+        address = mkOption {
+          type = types.str;
+          default = "0.0.0.0";
+          description = "Listening address for WS-RPC connections.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8546;
+          description = "Override the default port on which to listen for WS-RPC connections.";
+        };
+
+        api = mkOption {
+          type = types.str;
+          default = "admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul";
+          description = "API's offered over the WS-RPC interface.";
+        };
+
+       origins = mkOption {
+          type = types.str;
+          default = "*";
+          description = "Origins from which to accept websockets requests";
+       };
+     };
+
+      genesis = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        example = literalExpression '' {
+          alloc = {
+            a47385db68718bdcbddc2d2bb7c54018066ec111 = {
+              balance = "1000000000000000000000000000";
+            };
+          };
+          coinbase = "0x0000000000000000000000000000000000000000";
+          config = {
+            byzantiumBlock = 4;
+            chainId = 494702925;
+            eip150Block = 2;
+            eip155Block = 3;
+            eip158Block = 3;
+            homesteadBlock = 1;
+            isQuorum = true;
+            istanbul = {
+              epoch = 30000;
+              policy = 0;
+            };
+          };
+          difficulty = "0x1";
+          extraData = "0x0000000000000000000000000000000000000000000000000000000000000000f85ad59438f0508111273d8e482f49410ca4078afc86a961b8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0";
+          gasLimit = "0x2FEFD800";
+          mixHash = "0x63746963616c2062797a616e74696e65201111756c7420746f6c6572616e6365";
+          nonce = "0x0";
+          parentHash = "0x0000000000000000000000000000000000000000000000000000000000000000";
+          timestamp = "0x00";
+          }'';
+        description = "Blockchain genesis settings.";
+      };
+     };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.quorum ];
+    systemd.tmpfiles.rules = [
+      "d '${dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -"
+    ];
+    systemd.services.quorum = {
+      description = "Quorum daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        PRIVATE_CONFIG = "${cfg.privateconfig}";
+      };
+      preStart = ''
+        if [ ! -d ${dataDir}/geth ]; then
+          if [ ! -d ${dataDir}/keystore ]; then
+            echo ERROR: You need to create a wallet before initializing your genesis file, run:
+            echo   # su -s /bin/sh - quorum
+            echo   $ geth --datadir ${dataDir} account new
+            echo and configure your genesis file accordingly.
+            exit 1;
+          fi
+          ln -s ${staticNodesFile} ${dataDir}/static-nodes.json
+          ${pkgs.quorum}/bin/geth --datadir ${dataDir} init ${genesisFile}
+        fi
+      '';
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = ''${pkgs.quorum}/bin/geth \
+            --nodiscover \
+            --verbosity 5 \
+            --nodekey ${cfg.nodekeyFile} \
+            --istanbul.blockperiod ${toString cfg.blockperiod} \
+            --syncmode ${cfg.syncmode} \
+            ${optionalString (cfg.permissioned)
+            "--permissioned"} \
+            --mine --minerthreads 1 \
+            ${optionalString (cfg.rpc.enable)
+            "--rpc --rpcaddr ${cfg.rpc.address} --rpcport ${toString cfg.rpc.port} --rpcapi ${cfg.rpc.api}"} \
+            ${optionalString (cfg.ws.enable)
+            "--ws --wsaddr ${cfg.ws.address} --wsport ${toString cfg.ws.port} --wsapi ${cfg.ws.api} --wsorigins ${cfg.ws.origins}"} \
+            --emitcheckpoints \
+            --datadir ${dataDir} \
+            --port ${toString cfg.port}'';
+        Restart = "on-failure";
+
+        # Hardening measures
+        PrivateTmp = "true";
+        ProtectSystem = "full";
+        NoNewPrivileges = "true";
+        PrivateDevices = "true";
+        MemoryDenyWriteExecute = "true";
+      };
+    };
+    users.users.${cfg.user} = {
+      name = cfg.user;
+      group = cfg.group;
+      description = "Quorum daemon user";
+      home = dataDir;
+      isSystemUser = true;
+    };
+    users.groups.${cfg.group} = {};
+  };
+}
diff --git a/nixos/modules/services/networking/radicale.nix b/nixos/modules/services/networking/radicale.nix
new file mode 100644
index 00000000000..c6c40777ed7
--- /dev/null
+++ b/nixos/modules/services/networking/radicale.nix
@@ -0,0 +1,204 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.radicale;
+
+  format = pkgs.formats.ini {
+    listToValue = concatMapStringsSep ", " (generators.mkValueStringDefault { });
+  };
+
+  pkg = if isNull cfg.package then
+    pkgs.radicale
+  else
+    cfg.package;
+
+  confFile = if cfg.settings == { } then
+    pkgs.writeText "radicale.conf" cfg.config
+  else
+    format.generate "radicale.conf" cfg.settings;
+
+  rightsFile = format.generate "radicale.rights" cfg.rights;
+
+  bindLocalhost = cfg.settings != { } && !hasAttrByPath [ "server" "hosts" ] cfg.settings;
+
+in {
+  options.services.radicale = {
+    enable = mkEnableOption "Radicale CalDAV and CardDAV server";
+
+    package = mkOption {
+      description = "Radicale package to use.";
+      # Default cannot be pkgs.radicale because non-null values suppress
+      # warnings about incompatible configuration and storage formats.
+      type = with types; nullOr package // { inherit (package) description; };
+      default = null;
+      defaultText = literalExpression "pkgs.radicale";
+    };
+
+    config = mkOption {
+      type = types.str;
+      default = "";
+      description = ''
+        Radicale configuration, this will set the service
+        configuration file.
+        This option is mutually exclusive with <option>settings</option>.
+        This option is deprecated.  Use <option>settings</option> instead.
+      '';
+    };
+
+    settings = mkOption {
+      type = format.type;
+      default = { };
+      description = ''
+        Configuration for Radicale. See
+        <link xlink:href="https://radicale.org/3.0.html#documentation/configuration" />.
+        This option is mutually exclusive with <option>config</option>.
+      '';
+      example = literalExpression ''
+        server = {
+          hosts = [ "0.0.0.0:5232" "[::]:5232" ];
+        };
+        auth = {
+          type = "htpasswd";
+          htpasswd_filename = "/etc/radicale/users";
+          htpasswd_encryption = "bcrypt";
+        };
+        storage = {
+          filesystem_folder = "/var/lib/radicale/collections";
+        };
+      '';
+    };
+
+    rights = mkOption {
+      type = format.type;
+      description = ''
+        Configuration for Radicale's rights file. See
+        <link xlink:href="https://radicale.org/3.0.html#documentation/authentication-and-rights" />.
+        This option only works in conjunction with <option>settings</option>.
+        Setting this will also set <option>settings.rights.type</option> and
+        <option>settings.rights.file</option> to approriate values.
+      '';
+      default = { };
+      example = literalExpression ''
+        root = {
+          user = ".+";
+          collection = "";
+          permissions = "R";
+        };
+        principal = {
+          user = ".+";
+          collection = "{user}";
+          permissions = "RW";
+        };
+        calendars = {
+          user = ".+";
+          collection = "{user}/[^/]+";
+          permissions = "rw";
+        };
+      '';
+    };
+
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = "Extra arguments passed to the Radicale daemon.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.settings == { } || cfg.config == "";
+        message = ''
+          The options services.radicale.config and services.radicale.settings
+          are mutually exclusive.
+        '';
+      }
+    ];
+
+    warnings = optional (isNull cfg.package && 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") ''
+      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.
+      Set services.radicale.package to suppress this warning.
+    '' ++ optional (cfg.config != "") ''
+      The option services.radicale.config is deprecated.
+      Use services.radicale.settings instead.
+    '';
+
+    services.radicale.settings.rights = mkIf (cfg.rights != { }) {
+      type = "from_file";
+      file = toString rightsFile;
+    };
+
+    environment.systemPackages = [ pkg ];
+
+    users.users.radicale = {
+      isSystemUser = true;
+      group = "radicale";
+    };
+
+    users.groups.radicale = {};
+
+    systemd.services.radicale = {
+      description = "A Simple Calendar and Contact Server";
+      after = [ "network.target" ];
+      requires = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = concatStringsSep " " ([
+          "${pkg}/bin/radicale" "-C" confFile
+        ] ++ (
+          map escapeShellArg cfg.extraArgs
+        ));
+        User = "radicale";
+        Group = "radicale";
+        StateDirectory = "radicale/collections";
+        StateDirectoryMode = "0750";
+        # Hardening
+        CapabilityBoundingSet = [ "" ];
+        DeviceAllow = [ "/dev/stdin" ];
+        DevicePolicy = "strict";
+        IPAddressAllow = mkIf bindLocalhost "localhost";
+        IPAddressDeny = mkIf bindLocalhost "any";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        ReadWritePaths = lib.optional
+          (hasAttrByPath [ "storage" "filesystem_folder" ] cfg.settings)
+          cfg.settings.storage.filesystem_folder;
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+        UMask = "0027";
+        WorkingDirectory = "/var/lib/radicale";
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ aneeshusa infinisil dotlambda ];
+}
diff --git a/nixos/modules/services/networking/radvd.nix b/nixos/modules/services/networking/radvd.nix
new file mode 100644
index 00000000000..6e8db55bbf0
--- /dev/null
+++ b/nixos/modules/services/networking/radvd.nix
@@ -0,0 +1,77 @@
+# Module for the IPv6 Router Advertisement Daemon.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.radvd;
+
+  confFile = pkgs.writeText "radvd.conf" cfg.config;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.radvd.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description =
+        ''
+          Whether to enable the Router Advertisement Daemon
+          (<command>radvd</command>), which provides link-local
+          advertisements of IPv6 router addresses and prefixes using
+          the Neighbor Discovery Protocol (NDP).  This enables
+          stateless address autoconfiguration in IPv6 clients on the
+          network.
+        '';
+    };
+
+    services.radvd.config = mkOption {
+      type = types.lines;
+      example =
+        ''
+          interface eth0 {
+            AdvSendAdvert on;
+            prefix 2001:db8:1234:5678::/64 { };
+          };
+        '';
+      description =
+        ''
+          The contents of the radvd configuration file.
+        '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.radvd =
+      {
+        isSystemUser = true;
+        group = "radvd";
+        description = "Router Advertisement Daemon User";
+      };
+    users.groups.radvd = {};
+
+    systemd.services.radvd =
+      { description = "IPv6 Router Advertisement Daemon";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig =
+          { ExecStart = "@${pkgs.radvd}/bin/radvd radvd -n -u radvd -C ${confFile}";
+            Restart = "always";
+          };
+      };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/rdnssd.nix b/nixos/modules/services/networking/rdnssd.nix
new file mode 100644
index 00000000000..fd04bb8108f
--- /dev/null
+++ b/nixos/modules/services/networking/rdnssd.nix
@@ -0,0 +1,82 @@
+# Module for rdnssd, a daemon that configures DNS servers in
+# /etc/resolv/conf from IPv6 RDNSS advertisements.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  mergeHook = pkgs.writeScript "rdnssd-merge-hook" ''
+    #! ${pkgs.runtimeShell} -e
+    ${pkgs.openresolv}/bin/resolvconf -u
+  '';
+in
+{
+
+  ###### interface
+
+  options = {
+
+    services.rdnssd.enable = mkOption {
+      type = types.bool;
+      default = false;
+      #default = config.networking.enableIPv6;
+      description =
+        ''
+          Whether to enable the RDNSS daemon
+          (<command>rdnssd</command>), which configures DNS servers in
+          <filename>/etc/resolv.conf</filename> from RDNSS
+          advertisements sent by IPv6 routers.
+        '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.rdnssd.enable {
+
+    assertions = [{
+      assertion = config.networking.resolvconf.enable;
+      message = "rdnssd needs resolvconf to work (probably something sets up a static resolv.conf)";
+    }];
+
+    systemd.services.rdnssd = {
+      description = "RDNSS daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        # Create the proper run directory
+        mkdir -p /run/rdnssd
+        touch /run/rdnssd/resolv.conf
+        chown -R rdnssd /run/rdnssd
+
+        # Link the resolvconf interfaces to rdnssd
+        rm -f /run/resolvconf/interfaces/rdnssd
+        ln -s /run/rdnssd/resolv.conf /run/resolvconf/interfaces/rdnssd
+        ${mergeHook}
+      '';
+
+      postStop = ''
+        rm -f /run/resolvconf/interfaces/rdnssd
+        ${mergeHook}
+      '';
+
+      serviceConfig = {
+        ExecStart = "@${pkgs.ndisc6}/bin/rdnssd rdnssd -p /run/rdnssd/rdnssd.pid -r /run/rdnssd/resolv.conf -u rdnssd -H ${mergeHook}";
+        Type = "forking";
+        PIDFile = "/run/rdnssd/rdnssd.pid";
+      };
+    };
+
+    users.users.rdnssd = {
+      description = "RDNSSD Daemon User";
+      isSystemUser = true;
+      group = "rdnssd";
+    };
+    users.groups.rdnssd = {};
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/redsocks.nix b/nixos/modules/services/networking/redsocks.nix
new file mode 100644
index 00000000000..8481f9debf3
--- /dev/null
+++ b/nixos/modules/services/networking/redsocks.nix
@@ -0,0 +1,272 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.redsocks;
+in
+{
+  ##### interface
+  options = {
+    services.redsocks = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable redsocks.";
+      };
+
+      log_debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Log connection progress.";
+      };
+
+      log_info = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Log start and end of client sessions.";
+      };
+
+      log = mkOption {
+        type = types.str;
+        default = "stderr";
+        description =
+          ''
+            Where to send logs.
+
+            Possible values are:
+              - stderr
+              - file:/path/to/file
+              - syslog:FACILITY where FACILITY is any of "daemon", "local0",
+              etc.
+          '';
+      };
+
+      chroot = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description =
+          ''
+            Chroot under which to run redsocks. Log file is opened before
+            chroot, but if logging to syslog /etc/localtime may be required.
+          '';
+      };
+
+      redsocks = mkOption {
+        description =
+          ''
+            Local port to proxy associations to be performed.
+
+            The example shows how to configure a proxy to handle port 80 as HTTP
+            relay, and all other ports as HTTP connect.
+          '';
+        example = [
+          { port = 23456; proxy = "1.2.3.4:8080"; type = "http-relay";
+            redirectCondition = "--dport 80";
+            doNotRedirect = [ "-d 1.2.0.0/16" ];
+          }
+          { port = 23457; proxy = "1.2.3.4:8080"; type = "http-connect";
+            redirectCondition = true;
+            doNotRedirect = [ "-d 1.2.0.0/16" ];
+          }
+        ];
+        type = types.listOf (types.submodule { options = {
+          ip = mkOption {
+            type = types.str;
+            default = "127.0.0.1";
+            description =
+              ''
+                IP on which redsocks should listen. Defaults to 127.0.0.1 for
+                security reasons.
+              '';
+          };
+
+          port = mkOption {
+            type = types.int;
+            default = 12345;
+            description = "Port on which redsocks should listen.";
+          };
+
+          proxy = mkOption {
+            type = types.str;
+            description =
+              ''
+                Proxy through which redsocks should forward incoming traffic.
+                Example: "example.org:8080"
+              '';
+          };
+
+          type = mkOption {
+            type = types.enum [ "socks4" "socks5" "http-connect" "http-relay" ];
+            description = "Type of proxy.";
+          };
+
+          login = mkOption {
+            type = with types; nullOr str;
+            default = null;
+            description = "Login to send to proxy.";
+          };
+
+          password = mkOption {
+            type = with types; nullOr str;
+            default = null;
+            description =
+              ''
+                Password to send to proxy. WARNING, this will end up
+                world-readable in the store! Awaiting
+                https://github.com/NixOS/nix/issues/8 to be able to fix.
+              '';
+          };
+
+          disclose_src = mkOption {
+            type = types.enum [ "false" "X-Forwarded-For" "Forwarded_ip"
+                                "Forwarded_ipport" ];
+            default = "false";
+            description =
+              ''
+                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)
+                  - "Forwarded_ipport": add header 'Forwarded: for="IP:port"'
+              '';
+          };
+
+          redirectInternetOnly = mkOption {
+            type = types.bool;
+            default = true;
+            description = "Exclude all non-globally-routable IPs from redsocks";
+          };
+
+          doNotRedirect = mkOption {
+            type = with types; listOf str;
+            default = [];
+            description =
+              ''
+                Iptables filters that if matched will get the packet off of
+                redsocks.
+              '';
+            example = [ "-d 1.2.3.4" ];
+          };
+
+          redirectCondition = mkOption {
+            type = with types; either bool str;
+            default = false;
+            description =
+              ''
+                Conditions to make outbound packets go through this redsocks
+                instance.
+
+                If set to false, no packet will be forwarded. If set to true,
+                all packets will be forwarded (except packets excluded by
+                redirectInternetOnly).
+
+                If set to a string, this is an iptables filter that will be
+                matched against packets before getting them into redsocks. For
+                example, setting it to "--dport 80" will only send
+                packets to port 80 to redsocks. Note "-p tcp" is always
+                implicitly added, as udp can only be proxied through redudp or
+                the like.
+              '';
+          };
+        };});
+      };
+
+      # TODO: Add support for redudp and dnstc
+    };
+  };
+
+  ##### implementation
+  config = let
+    redsocks_blocks = concatMapStrings (block:
+      let proxy = splitString ":" block.proxy; in
+      ''
+        redsocks {
+          local_ip = ${block.ip};
+          local_port = ${toString block.port};
+
+          ip = ${elemAt proxy 0};
+          port = ${elemAt proxy 1};
+          type = ${block.type};
+
+          ${optionalString (block.login != null) "login = \"${block.login}\";"}
+          ${optionalString (block.password != null) "password = \"${block.password}\";"}
+
+          disclose_src = ${block.disclose_src};
+        }
+      '') cfg.redsocks;
+    configfile = pkgs.writeText "redsocks.conf"
+      ''
+        base {
+          log_debug = ${if cfg.log_debug then "on" else "off" };
+          log_info = ${if cfg.log_info then "on" else "off" };
+          log = ${cfg.log};
+
+          daemon = off;
+          redirector = iptables;
+
+          user = redsocks;
+          group = redsocks;
+          ${optionalString (cfg.chroot != null) "chroot = ${cfg.chroot};"}
+        }
+
+        ${redsocks_blocks}
+      '';
+    internetOnly = [ # TODO: add ipv6-equivalent
+      "-d 0.0.0.0/8"
+      "-d 10.0.0.0/8"
+      "-d 127.0.0.0/8"
+      "-d 169.254.0.0/16"
+      "-d 172.16.0.0/12"
+      "-d 192.168.0.0/16"
+      "-d 224.168.0.0/4"
+      "-d 240.168.0.0/4"
+    ];
+    redCond = block:
+      optionalString (isString block.redirectCondition) block.redirectCondition;
+    iptables = concatImapStrings (idx: block:
+      let chain = "REDSOCKS${toString idx}"; doNotRedirect =
+        concatMapStringsSep "\n"
+          (f: "ip46tables -t nat -A ${chain} ${f} -j RETURN 2>/dev/null || true")
+          (block.doNotRedirect ++ (optionals block.redirectInternetOnly internetOnly));
+      in
+      optionalString (block.redirectCondition != false)
+        ''
+          ip46tables -t nat -F ${chain} 2>/dev/null || true
+          ip46tables -t nat -N ${chain} 2>/dev/null || true
+          ${doNotRedirect}
+          ip46tables -t nat -A ${chain} -p tcp -j REDIRECT --to-ports ${toString block.port}
+
+          # TODO: show errors, when it will be easily possible by a switch to
+          # iptables-restore
+          ip46tables -t nat -A OUTPUT -p tcp ${redCond block} -j ${chain} 2>/dev/null || true
+        ''
+    ) cfg.redsocks;
+  in
+    mkIf cfg.enable {
+      users.groups.redsocks = {};
+      users.users.redsocks = {
+        description = "Redsocks daemon";
+        group = "redsocks";
+        isSystemUser = true;
+      };
+
+      systemd.services.redsocks = {
+        description = "Redsocks";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        script = "${pkgs.redsocks}/bin/redsocks -c ${configfile}";
+      };
+
+      networking.firewall.extraCommands = iptables;
+
+      networking.firewall.extraStopCommands =
+        concatImapStringsSep "\n" (idx: block:
+          let chain = "REDSOCKS${toString idx}"; in
+          optionalString (block.redirectCondition != false)
+            "ip46tables -t nat -D OUTPUT -p tcp ${redCond block} -j ${chain} 2>/dev/null || true"
+        ) cfg.redsocks;
+    };
+
+  meta.maintainers = with lib.maintainers; [ ekleog ];
+}
diff --git a/nixos/modules/services/networking/resilio.nix b/nixos/modules/services/networking/resilio.nix
new file mode 100644
index 00000000000..89127850641
--- /dev/null
+++ b/nixos/modules/services/networking/resilio.nix
@@ -0,0 +1,265 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.resilio;
+
+  resilioSync = pkgs.resilio-sync;
+
+  sharedFoldersRecord = map (entry: {
+    secret = entry.secret;
+    dir = entry.directory;
+
+    use_relay_server = entry.useRelayServer;
+    use_tracker = entry.useTracker;
+    use_dht = entry.useDHT;
+
+    search_lan = entry.searchLAN;
+    use_sync_trash = entry.useSyncTrash;
+    known_hosts = entry.knownHosts;
+  }) cfg.sharedFolders;
+
+  configFile = pkgs.writeText "config.json" (builtins.toJSON ({
+    device_name = cfg.deviceName;
+    storage_path = cfg.storagePath;
+    listening_port = cfg.listeningPort;
+    use_gui = false;
+    check_for_updates = cfg.checkForUpdates;
+    use_upnp = cfg.useUpnp;
+    download_limit = cfg.downloadLimit;
+    upload_limit = cfg.uploadLimit;
+    lan_encrypt_data = cfg.encryptLAN;
+  } // optionalAttrs (cfg.directoryRoot != "") { directory_root = cfg.directoryRoot; }
+    // optionalAttrs cfg.enableWebUI {
+    webui = { listen = "${cfg.httpListenAddr}:${toString cfg.httpListenPort}"; } //
+      (optionalAttrs (cfg.httpLogin != "") { login = cfg.httpLogin; }) //
+      (optionalAttrs (cfg.httpPass != "") { password = cfg.httpPass; }) //
+      (optionalAttrs (cfg.apiKey != "") { api_key = cfg.apiKey; });
+  } // optionalAttrs (sharedFoldersRecord != []) {
+    shared_folders = sharedFoldersRecord;
+  }));
+
+in
+{
+  options = {
+    services.resilio = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If enabled, start the Resilio Sync daemon. Once enabled, you can
+          interact with the service through the Web UI, or configure it in your
+          NixOS configuration.
+        '';
+      };
+
+      deviceName = mkOption {
+        type = types.str;
+        example = "Voltron";
+        default = config.networking.hostName;
+        defaultText = literalExpression "config.networking.hostName";
+        description = ''
+          Name of the Resilio Sync device.
+        '';
+      };
+
+      listeningPort = mkOption {
+        type = types.int;
+        default = 0;
+        example = 44444;
+        description = ''
+          Listening port. Defaults to 0 which randomizes the port.
+        '';
+      };
+
+      checkForUpdates = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Determines whether to check for updates and alert the user
+          about them in the UI.
+        '';
+      };
+
+      useUpnp = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Use Universal Plug-n-Play (UPnP)
+        '';
+      };
+
+      downloadLimit = mkOption {
+        type = types.int;
+        default = 0;
+        example = 1024;
+        description = ''
+          Download speed limit. 0 is unlimited (default).
+        '';
+      };
+
+      uploadLimit = mkOption {
+        type = types.int;
+        default = 0;
+        example = 1024;
+        description = ''
+          Upload speed limit. 0 is unlimited (default).
+        '';
+      };
+
+      httpListenAddr = mkOption {
+        type = types.str;
+        default = "[::1]";
+        example = "0.0.0.0";
+        description = ''
+          HTTP address to bind to.
+        '';
+      };
+
+      httpListenPort = mkOption {
+        type = types.int;
+        default = 9000;
+        description = ''
+          HTTP port to bind on.
+        '';
+      };
+
+      httpLogin = mkOption {
+        type = types.str;
+        example = "allyourbase";
+        default = "";
+        description = ''
+          HTTP web login username.
+        '';
+      };
+
+      httpPass = mkOption {
+        type = types.str;
+        example = "arebelongtous";
+        default = "";
+        description = ''
+          HTTP web login password.
+        '';
+      };
+
+      encryptLAN = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Encrypt LAN data.";
+      };
+
+      enableWebUI = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable Web UI for administration. Bound to the specified
+          <literal>httpListenAddress</literal> and
+          <literal>httpListenPort</literal>.
+          '';
+      };
+
+      storagePath = mkOption {
+        type = types.path;
+        default = "/var/lib/resilio-sync/";
+        description = ''
+          Where BitTorrent Sync will store it's database files (containing
+          things like username info and licenses). Generally, you should not
+          need to ever change this.
+        '';
+      };
+
+      apiKey = mkOption {
+        type = types.str;
+        default = "";
+        description = "API key, which enables the developer API.";
+      };
+
+      directoryRoot = mkOption {
+        type = types.str;
+        default = "";
+        example = "/media";
+        description = "Default directory to add folders in the web UI.";
+      };
+
+      sharedFolders = mkOption {
+        default = [];
+        type = types.listOf (types.attrsOf types.anything);
+        example =
+          [ { secret         = "AHMYFPCQAHBM7LQPFXQ7WV6Y42IGUXJ5Y";
+              directory      = "/home/user/sync_test";
+              useRelayServer = true;
+              useTracker     = true;
+              useDHT         = false;
+              searchLAN      = true;
+              useSyncTrash   = true;
+              knownHosts     = [
+                "192.168.1.2:4444"
+                "192.168.1.3:4444"
+              ];
+            }
+          ];
+        description = ''
+          Shared folder list. If enabled, web UI must be
+          disabled. Secrets can be generated using <literal>rslsync
+          --generate-secret</literal>. Note that this secret will be
+          put inside the Nix store, so it is realistically not very
+          secret.
+
+          If you would like to be able to modify the contents of this
+          directories, it is recommended that you make your user a
+          member of the <literal>rslsync</literal> group.
+
+          Directories in this list should be in the
+          <literal>rslsync</literal> group, and that group must have
+          write access to the directory. It is also recommended that
+          <literal>chmod g+s</literal> is applied to the directory
+          so that any sub directories created will also belong to
+          the <literal>rslsync</literal> group. Also,
+          <literal>setfacl -d -m group:rslsync:rwx</literal> and
+          <literal>setfacl -m group:rslsync:rwx</literal> should also
+          be applied so that the sub directories are writable by
+          the group.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions =
+      [ { assertion = cfg.deviceName != "";
+          message   = "Device name cannot be empty.";
+        }
+        { assertion = cfg.enableWebUI -> cfg.sharedFolders == [];
+          message   = "If using shared folders, the web UI cannot be enabled.";
+        }
+        { assertion = cfg.apiKey != "" -> cfg.enableWebUI;
+          message   = "If you're using an API key, you must enable the web server.";
+        }
+      ];
+
+    users.users.rslsync = {
+      description     = "Resilio Sync Service user";
+      home            = cfg.storagePath;
+      createHome      = true;
+      uid             = config.ids.uids.rslsync;
+      group           = "rslsync";
+    };
+
+    users.groups.rslsync = {};
+
+    systemd.services.resilio = with pkgs; {
+      description = "Resilio Sync Service";
+      wantedBy    = [ "multi-user.target" ];
+      after       = [ "network.target" ];
+      serviceConfig = {
+        Restart   = "on-abort";
+        UMask     = "0002";
+        User      = "rslsync";
+        ExecStart = ''
+          ${resilioSync}/bin/rslsync --nodaemon --config ${configFile}
+        '';
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/robustirc-bridge.nix b/nixos/modules/services/networking/robustirc-bridge.nix
new file mode 100644
index 00000000000..255af79ec04
--- /dev/null
+++ b/nixos/modules/services/networking/robustirc-bridge.nix
@@ -0,0 +1,47 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.robustirc-bridge;
+in
+{
+  options = {
+    services.robustirc-bridge = {
+      enable = mkEnableOption "RobustIRC bridge";
+
+      extraFlags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''Extra flags passed to the <command>robustirc-bridge</command> command. See <link xlink:href="https://robustirc.net/docs/adminguide.html#_bridge">RobustIRC Documentation</link> or robustirc-bridge(1) for details.'';
+        example = [
+          "-network robustirc.net"
+        ];
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.robustirc-bridge = {
+      description = "RobustIRC bridge";
+      documentation = [
+        "man:robustirc-bridge(1)"
+        "https://robustirc.net/"
+      ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${pkgs.robustirc-bridge}/bin/robustirc-bridge ${concatStringsSep " " cfg.extraFlags}";
+        Restart = "on-failure";
+
+        # Hardening
+        PrivateDevices = true;
+        ProtectSystem = true;
+        ProtectHome = true;
+        PrivateTmp = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/rpcbind.nix b/nixos/modules/services/networking/rpcbind.nix
new file mode 100644
index 00000000000..0a5df698709
--- /dev/null
+++ b/nixos/modules/services/networking/rpcbind.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.rpcbind = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          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`.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.rpcbind.enable {
+    environment.systemPackages = [ pkgs.rpcbind ];
+
+    systemd.packages = [ pkgs.rpcbind ];
+
+    systemd.services.rpcbind = {
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    users.users.rpc = {
+      group = "nogroup";
+      uid = config.ids.uids.rpc;
+    };
+  };
+
+}
diff --git a/nixos/modules/services/networking/rxe.nix b/nixos/modules/services/networking/rxe.nix
new file mode 100644
index 00000000000..868e2c81ccb
--- /dev/null
+++ b/nixos/modules/services/networking/rxe.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.networking.rxe;
+
+in {
+  ###### interface
+
+  options = {
+    networking.rxe = {
+      enable = mkEnableOption "RDMA over converged ethernet";
+      interfaces = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "eth0" ];
+        description = ''
+          Enable RDMA on the listed interfaces. The corresponding virtual
+          RDMA interfaces will be named rxe_&lt;interface&gt;.
+          UDP port 4791 must be open on the respective ethernet interfaces.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.rxe = {
+      description = "RoCE interfaces";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "systemd-modules-load.service" "network-online.target" ];
+      wants = [ "network-pre.target" ];
+
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStart = map ( x:
+          "${pkgs.iproute2}/bin/rdma link add rxe_${x} type rxe netdev ${x}"
+          ) cfg.interfaces;
+
+        ExecStop = map ( x:
+          "${pkgs.iproute2}/bin/rdma link delete rxe_${x}"
+          ) cfg.interfaces;
+      };
+    };
+  };
+}
+
diff --git a/nixos/modules/services/networking/sabnzbd.nix b/nixos/modules/services/networking/sabnzbd.nix
new file mode 100644
index 00000000000..54eeba1a9ec
--- /dev/null
+++ b/nixos/modules/services/networking/sabnzbd.nix
@@ -0,0 +1,77 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.sabnzbd;
+  inherit (pkgs) sabnzbd;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+    services.sabnzbd = {
+      enable = mkEnableOption "the sabnzbd server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.sabnzbd;
+        defaultText = "pkgs.sabnzbd";
+        description = "The sabnzbd executable package run by the service.";
+      };
+
+      configFile = mkOption {
+        type = types.path;
+        default = "/var/lib/sabnzbd/sabnzbd.ini";
+        description = "Path to config file.";
+      };
+
+      user = mkOption {
+        default = "sabnzbd";
+        type = types.str;
+        description = "User to run the service as";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "sabnzbd";
+        description = "Group to run the service as";
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.sabnzbd = {
+          uid = config.ids.uids.sabnzbd;
+          group = "sabnzbd";
+          description = "sabnzbd user";
+          home = "/var/lib/sabnzbd/";
+          createHome = true;
+    };
+
+    users.groups.sabnzbd = {
+      gid = config.ids.gids.sabnzbd;
+    };
+
+    systemd.services.sabnzbd = {
+        description = "sabnzbd server";
+        wantedBy    = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig = {
+          Type = "forking";
+          GuessMainPID = "no";
+          User = "${cfg.user}";
+          Group = "${cfg.group}";
+          ExecStart = "${lib.getBin cfg.package}/bin/sabnzbd -d -f ${cfg.configFile}";
+        };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/seafile.nix b/nixos/modules/services/networking/seafile.nix
new file mode 100644
index 00000000000..2839ffb60a1
--- /dev/null
+++ b/nixos/modules/services/networking/seafile.nix
@@ -0,0 +1,287 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.seafile;
+  settingsFormat = pkgs.formats.ini { };
+
+  ccnetConf = settingsFormat.generate "ccnet.conf" cfg.ccnetSettings;
+
+  seafileConf = settingsFormat.generate "seafile.conf" cfg.seafileSettings;
+
+  seahubSettings = pkgs.writeText "seahub_settings.py" ''
+    FILE_SERVER_ROOT = '${cfg.ccnetSettings.General.SERVICE_URL}/seafhttp'
+    DATABASES = {
+        'default': {
+            'ENGINE': 'django.db.backends.sqlite3',
+            'NAME': '${seahubDir}/seahub.db',
+        }
+    }
+    MEDIA_ROOT = '${seahubDir}/media/'
+    THUMBNAIL_ROOT = '${seahubDir}/thumbnail/'
+
+    with open('${seafRoot}/.seahubSecret') as f:
+        SECRET_KEY = f.readline().rstrip()
+
+    ${cfg.seahubExtraConf}
+  '';
+
+  seafRoot = "/var/lib/seafile"; # hardcode it due to dynamicuser
+  ccnetDir = "${seafRoot}/ccnet";
+  dataDir = "${seafRoot}/data";
+  seahubDir = "${seafRoot}/seahub";
+
+in {
+
+  ###### Interface
+
+  options.services.seafile = {
+    enable = mkEnableOption "Seafile server";
+
+    ccnetSettings = mkOption {
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+
+        options = {
+          General = {
+            SERVICE_URL = mkOption {
+              type = types.str;
+              example = "https://www.example.com";
+              description = ''
+                Seahub public URL.
+              '';
+            };
+          };
+        };
+      };
+      default = { };
+      description = ''
+        Configuration for ccnet, see
+        <link xlink:href="https://manual.seafile.com/config/ccnet-conf/"/>
+        for supported values.
+      '';
+    };
+
+    seafileSettings = mkOption {
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+
+        options = {
+          fileserver = {
+            port = mkOption {
+              type = types.port;
+              default = 8082;
+              description = ''
+                The tcp port used by seafile fileserver.
+              '';
+            };
+            host = mkOption {
+              type = types.str;
+              default = "127.0.0.1";
+              example = "0.0.0.0";
+              description = ''
+                The binding address used by seafile fileserver.
+              '';
+            };
+          };
+        };
+      };
+      default = { };
+      description = ''
+        Configuration for seafile-server, see
+        <link xlink:href="https://manual.seafile.com/config/seafile-conf/"/>
+        for supported values.
+      '';
+    };
+
+    workers = mkOption {
+      type = types.int;
+      default = 4;
+      example = 10;
+      description = ''
+        The number of gunicorn worker processes for handling requests.
+      '';
+    };
+
+    adminEmail = mkOption {
+      example = "john@example.com";
+      type = types.str;
+      description = ''
+        Seafile Seahub Admin Account Email.
+      '';
+    };
+
+    initialAdminPassword = mkOption {
+      example = "someStrongPass";
+      type = types.str;
+      description = ''
+        Seafile Seahub Admin Account initial password.
+        Should be change via Seahub web front-end.
+      '';
+    };
+
+    seafilePackage = mkOption {
+      type = types.package;
+      description = "Which package to use for the seafile server.";
+      default = pkgs.seafile-server;
+      defaultText = literalExpression "pkgs.seafile-server";
+    };
+
+    seahubExtraConf = mkOption {
+      default = "";
+      type = types.lines;
+      description = ''
+        Extra config to append to `seahub_settings.py` file.
+        Refer to <link xlink:href="https://manual.seafile.com/config/seahub_settings_py/" />
+        for all available options.
+      '';
+    };
+  };
+
+  ###### Implementation
+
+  config = mkIf cfg.enable {
+
+    environment.etc."seafile/ccnet.conf".source = ccnetConf;
+    environment.etc."seafile/seafile.conf".source = seafileConf;
+    environment.etc."seafile/seahub_settings.py".source = seahubSettings;
+
+    systemd.targets.seafile = {
+      wantedBy = [ "multi-user.target" ];
+      description = "Seafile components";
+    };
+
+    systemd.services = let
+      securityOptions = {
+        ProtectHome = true;
+        PrivateUsers = true;
+        PrivateDevices = true;
+        ProtectClock = true;
+        ProtectHostname = true;
+        ProtectProc = "invisible";
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        MemoryDenyWriteExecute = true;
+        SystemCallArchitectures = "native";
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" ];
+      };
+    in {
+      seaf-server = {
+        description = "Seafile server";
+        partOf = [ "seafile.target" ];
+        after = [ "network.target" ];
+        wantedBy = [ "seafile.target" ];
+        restartTriggers = [ ccnetConf seafileConf ];
+        serviceConfig = securityOptions // {
+          User = "seafile";
+          Group = "seafile";
+          DynamicUser = true;
+          StateDirectory = "seafile";
+          RuntimeDirectory = "seafile";
+          LogsDirectory = "seafile";
+          ConfigurationDirectory = "seafile";
+          ExecStart = ''
+            ${cfg.seafilePackage}/bin/seaf-server \
+            --foreground \
+            -F /etc/seafile \
+            -c ${ccnetDir} \
+            -d ${dataDir} \
+            -l /var/log/seafile/server.log \
+            -P /run/seafile/server.pid \
+            -p /run/seafile
+          '';
+        };
+        preStart = ''
+          if [ ! -f "${seafRoot}/server-setup" ]; then
+              mkdir -p ${dataDir}/library-template
+              mkdir -p ${ccnetDir}/{GroupMgr,misc,OrgMgr,PeerMgr}
+              ${pkgs.sqlite}/bin/sqlite3 ${ccnetDir}/GroupMgr/groupmgr.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/groupmgr.sql"
+              ${pkgs.sqlite}/bin/sqlite3 ${ccnetDir}/misc/config.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/config.sql"
+              ${pkgs.sqlite}/bin/sqlite3 ${ccnetDir}/OrgMgr/orgmgr.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/org.sql"
+              ${pkgs.sqlite}/bin/sqlite3 ${ccnetDir}/PeerMgr/usermgr.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/user.sql"
+              ${pkgs.sqlite}/bin/sqlite3 ${dataDir}/seafile.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/seafile.sql"
+              echo "${cfg.seafilePackage.version}-sqlite" > "${seafRoot}"/server-setup
+          fi
+          # checking for upgrades and handling them
+          # WARNING: needs to be extended to actually handle major version migrations
+          installedMajor=$(cat "${seafRoot}/server-setup" | cut -d"-" -f1 | cut -d"." -f1)
+          installedMinor=$(cat "${seafRoot}/server-setup" | cut -d"-" -f1 | cut -d"." -f2)
+          pkgMajor=$(echo "${cfg.seafilePackage.version}" | cut -d"." -f1)
+          pkgMinor=$(echo "${cfg.seafilePackage.version}" | cut -d"." -f2)
+          if [ $installedMajor != $pkgMajor ] || [ $installedMinor != $pkgMinor ]; then
+              echo "Unsupported upgrade" >&2
+              exit 1
+          fi
+        '';
+      };
+
+      seahub = {
+        description = "Seafile Server Web Frontend";
+        wantedBy = [ "seafile.target" ];
+        partOf = [ "seafile.target" ];
+        after = [ "network.target" "seaf-server.service" ];
+        requires = [ "seaf-server.service" ];
+        restartTriggers = [ seahubSettings ];
+        environment = {
+          PYTHONPATH = "${pkgs.seahub.pythonPath}:${pkgs.seahub}/thirdpart:${pkgs.seahub}";
+          DJANGO_SETTINGS_MODULE = "seahub.settings";
+          CCNET_CONF_DIR = ccnetDir;
+          SEAFILE_CONF_DIR = dataDir;
+          SEAFILE_CENTRAL_CONF_DIR = "/etc/seafile";
+          SEAFILE_RPC_PIPE_PATH = "/run/seafile";
+          SEAHUB_LOG_DIR = "/var/log/seafile";
+        };
+        serviceConfig = securityOptions // {
+          User = "seafile";
+          Group = "seafile";
+          DynamicUser = true;
+          RuntimeDirectory = "seahub";
+          StateDirectory = "seafile";
+          LogsDirectory = "seafile";
+          ConfigurationDirectory = "seafile";
+          ExecStart = ''
+            ${pkgs.seahub.python.pkgs.gunicorn}/bin/gunicorn seahub.wsgi:application \
+            --name seahub \
+            --workers ${toString cfg.workers} \
+            --log-level=info \
+            --preload \
+            --timeout=1200 \
+            --limit-request-line=8190 \
+            --bind unix:/run/seahub/gunicorn.sock
+          '';
+        };
+        preStart = ''
+          mkdir -p ${seahubDir}/media
+          # Link all media except avatars
+          for m in `find ${pkgs.seahub}/media/ -maxdepth 1 -not -name "avatars"`; do
+            ln -sf $m ${seahubDir}/media/
+          done
+          if [ ! -e "${seafRoot}/.seahubSecret" ]; then
+              ${pkgs.seahub.python}/bin/python ${pkgs.seahub}/tools/secret_key_generator.py > ${seafRoot}/.seahubSecret
+              chmod 400 ${seafRoot}/.seahubSecret
+          fi
+          if [ ! -f "${seafRoot}/seahub-setup" ]; then
+              # avatars directory should be writable
+              install -D -t ${seahubDir}/media/avatars/ ${pkgs.seahub}/media/avatars/default.png
+              install -D -t ${seahubDir}/media/avatars/groups ${pkgs.seahub}/media/avatars/groups/default.png
+              # init database
+              ${pkgs.seahub}/manage.py migrate
+              # create admin account
+              ${pkgs.expect}/bin/expect -c 'spawn ${pkgs.seahub}/manage.py createsuperuser --email=${cfg.adminEmail}; expect "Password: "; send "${cfg.initialAdminPassword}\r"; expect "Password (again): "; send "${cfg.initialAdminPassword}\r"; expect "Superuser created successfully."'
+              echo "${pkgs.seahub.version}-sqlite" > "${seafRoot}/seahub-setup"
+          fi
+          if [ $(cat "${seafRoot}/seahub-setup" | cut -d"-" -f1) != "${pkgs.seahub.version}" ]; then
+              # update database
+              ${pkgs.seahub}/manage.py migrate
+              echo "${pkgs.seahub.version}-sqlite" > "${seafRoot}/seahub-setup"
+          fi
+        '';
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/searx.nix b/nixos/modules/services/networking/searx.nix
new file mode 100644
index 00000000000..b73f255eb9d
--- /dev/null
+++ b/nixos/modules/services/networking/searx.nix
@@ -0,0 +1,231 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  runDir = "/run/searx";
+
+  cfg = config.services.searx;
+
+  settingsFile = pkgs.writeText "settings.yml"
+    (builtins.toJSON cfg.settings);
+
+  generateConfig = ''
+    cd ${runDir}
+
+    # write NixOS settings as JSON
+    (
+      umask 077
+      cp --no-preserve=mode ${settingsFile} settings.yml
+    )
+
+    # substitute environment variables
+    env -0 | while IFS='=' read -r -d ''' n v; do
+      sed "s#@$n@#$v#g" -i settings.yml
+    done
+  '';
+
+  settingType = with types; (oneOf
+    [ bool int float str
+      (listOf settingType)
+      (attrsOf settingType)
+    ]) // { description = "JSON value"; };
+
+in
+
+{
+
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "searx" "configFile" ]
+      [ "services" "searx" "settingsFile" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.searx = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        relatedPackages = [ "searx" ];
+        description = "Whether to enable Searx, the meta search engine.";
+      };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          Environment file (see <literal>systemd.exec(5)</literal>
+          "EnvironmentFile=" section for the syntax) to define variables for
+          Searx. This option can be used to safely include secret keys into the
+          Searx configuration.
+        '';
+      };
+
+      settings = mkOption {
+        type = types.attrsOf settingType;
+        default = { };
+        example = literalExpression ''
+          { server.port = 8080;
+            server.bind_address = "0.0.0.0";
+            server.secret_key = "@SEARX_SECRET_KEY@";
+
+            engines = lib.singleton
+              { name = "wolframalpha";
+                shortcut = "wa";
+                api_key = "@WOLFRAM_API_KEY@";
+                engine = "wolframalpha_api";
+              };
+          }
+        '';
+        description = ''
+          Searx settings. These will be merged with (taking precedence over)
+          the default configuration. It's also possible to refer to
+          environment variables
+          (defined in <xref linkend="opt-services.searx.environmentFile"/>)
+          using the syntax <literal>@VARIABLE_NAME@</literal>.
+          <note>
+            <para>
+              For available settings, see the Searx
+              <link xlink:href="https://searx.github.io/searx/admin/settings.html">docs</link>.
+            </para>
+          </note>
+        '';
+      };
+
+      settingsFile = mkOption {
+        type = types.path;
+        default = "${runDir}/settings.yml";
+        description = ''
+          The path of the Searx server settings.yml file. If no file is
+          specified, a default file is used (default config file has debug mode
+          enabled). Note: setting this options overrides
+          <xref linkend="opt-services.searx.settings"/>.
+          <warning>
+            <para>
+              This file, along with any secret key it contains, will be copied
+              into the world-readable Nix store.
+            </para>
+          </warning>
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.searx;
+        defaultText = literalExpression "pkgs.searx";
+        description = "searx package to use.";
+      };
+
+      runInUwsgi = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run searx in uWSGI as a "vassal", instead of using its
+          built-in HTTP server. This is the recommended mode for public or
+          large instances, but is unecessary for LAN or local-only use.
+          <warning>
+            <para>
+              The built-in HTTP server logs all queries by default.
+            </para>
+          </warning>
+        '';
+      };
+
+      uwsgiConfig = mkOption {
+        type = options.services.uwsgi.instance.type;
+        default = { http = ":8080"; };
+        example = literalExpression ''
+          {
+            disable-logging = true;
+            http = ":8080";                   # serve via HTTP...
+            socket = "/run/searx/searx.sock"; # ...or UNIX socket
+          }
+        '';
+        description = ''
+          Additional configuration of the uWSGI vassal running searx. It
+          should notably specify on which interfaces and ports the vassal
+          should listen.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    users.users.searx =
+      { description = "Searx daemon user";
+        group = "searx";
+        isSystemUser = true;
+      };
+
+    users.groups.searx = { };
+
+    systemd.services.searx-init = {
+      description = "Initialise Searx settings";
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        User = "searx";
+        RuntimeDirectory = "searx";
+        RuntimeDirectoryMode = "750";
+      } // optionalAttrs (cfg.environmentFile != null)
+        { EnvironmentFile = builtins.toPath cfg.environmentFile; };
+      script = generateConfig;
+    };
+
+    systemd.services.searx = mkIf (!cfg.runInUwsgi) {
+      description = "Searx server, the meta search engine.";
+      wantedBy = [ "network.target" "multi-user.target" ];
+      requires = [ "searx-init.service" ];
+      after = [ "searx-init.service" ];
+      serviceConfig = {
+        User  = "searx";
+        Group = "searx";
+        ExecStart = "${cfg.package}/bin/searx-run";
+      } // optionalAttrs (cfg.environmentFile != null)
+        { EnvironmentFile = builtins.toPath cfg.environmentFile; };
+      environment.SEARX_SETTINGS_PATH = cfg.settingsFile;
+    };
+
+    systemd.services.uwsgi = mkIf (cfg.runInUwsgi)
+      { requires = [ "searx-init.service" ];
+        after = [ "searx-init.service" ];
+      };
+
+    services.searx.settings = {
+      # merge NixOS settings with defaults settings.yml
+      use_default_settings = mkDefault true;
+    };
+
+    services.uwsgi = mkIf (cfg.runInUwsgi) {
+      enable = true;
+      plugins = [ "python3" ];
+
+      instance.type = "emperor";
+      instance.vassals.searx = {
+        type = "normal";
+        strict = true;
+        immediate-uid = "searx";
+        immediate-gid = "searx";
+        lazy-apps = true;
+        enable-threads = true;
+        module = "searx.webapp";
+        env = [ "SEARX_SETTINGS_PATH=${cfg.settingsFile}" ];
+        pythonPackages = self: [ cfg.package ];
+      } // cfg.uwsgiConfig;
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ rnhmjoj ];
+}
diff --git a/nixos/modules/services/networking/shadowsocks.nix b/nixos/modules/services/networking/shadowsocks.nix
new file mode 100644
index 00000000000..7bea269a9ed
--- /dev/null
+++ b/nixos/modules/services/networking/shadowsocks.nix
@@ -0,0 +1,158 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.shadowsocks;
+
+  opts = {
+    server = cfg.localAddress;
+    server_port = cfg.port;
+    method = cfg.encryptionMethod;
+    mode = cfg.mode;
+    user = "nobody";
+    fast_open = cfg.fastOpen;
+  } // optionalAttrs (cfg.plugin != null) {
+    plugin = cfg.plugin;
+    plugin_opts = cfg.pluginOpts;
+  } // optionalAttrs (cfg.password != null) {
+    password = cfg.password;
+  } // cfg.extraConfig;
+
+  configFile = pkgs.writeText "shadowsocks.json" (builtins.toJSON opts);
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.shadowsocks = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run shadowsocks-libev shadowsocks server.
+        '';
+      };
+
+      localAddress = mkOption {
+        type = types.coercedTo types.str singleton (types.listOf types.str);
+        default = [ "[::0]" "0.0.0.0" ];
+        description = ''
+          Local addresses to which the server binds.
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8388;
+        description = ''
+          Port which the server uses.
+        '';
+      };
+
+      password = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Password for connecting clients.
+        '';
+      };
+
+      passwordFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          Password file with a password for connecting clients.
+        '';
+      };
+
+      mode = mkOption {
+        type = types.enum [ "tcp_only" "tcp_and_udp" "udp_only" ];
+        default = "tcp_and_udp";
+        description = ''
+          Relay protocols.
+        '';
+      };
+
+      fastOpen = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          use TCP fast-open
+        '';
+      };
+
+      encryptionMethod = mkOption {
+        type = types.str;
+        default = "chacha20-ietf-poly1305";
+        description = ''
+          Encryption method. See <link xlink:href="https://github.com/shadowsocks/shadowsocks-org/wiki/AEAD-Ciphers"/>.
+        '';
+      };
+
+      plugin = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = literalExpression ''"''${pkgs.shadowsocks-v2ray-plugin}/bin/v2ray-plugin"'';
+        description = ''
+          SIP003 plugin for shadowsocks
+        '';
+      };
+
+      pluginOpts = mkOption {
+        type = types.str;
+        default = "";
+        example = "server;host=example.com";
+        description = ''
+          Options to pass to the plugin if one was specified
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        example = {
+          nameserver = "8.8.8.8";
+        };
+        description = ''
+          Additional configuration for shadowsocks that is not covered by the
+          provided options. The provided attrset will be serialized to JSON and
+          has to contain valid shadowsocks options. Unfortunately most
+          additional options are undocumented but it's easy to find out what is
+          available by looking into the source code of
+          <link xlink:href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/src/jconf.c"/>
+        '';
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    assertions = singleton
+      { assertion = cfg.password == null || cfg.passwordFile == null;
+        message = "Cannot use both password and passwordFile for shadowsocks-libev";
+      };
+
+    systemd.services.shadowsocks-libev = {
+      description = "shadowsocks-libev Daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.shadowsocks-libev ] ++ optional (cfg.plugin != null) cfg.plugin ++ optional (cfg.passwordFile != null) pkgs.jq;
+      serviceConfig.PrivateTmp = true;
+      script = ''
+        ${optionalString (cfg.passwordFile != null) ''
+          cat ${configFile} | jq --arg password "$(cat "${cfg.passwordFile}")" '. + { password: $password }' > /tmp/shadowsocks.json
+        ''}
+        exec ss-server -c ${if cfg.passwordFile != null then "/tmp/shadowsocks.json" else configFile}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/shairport-sync.nix b/nixos/modules/services/networking/shairport-sync.nix
new file mode 100644
index 00000000000..eb61663e4d9
--- /dev/null
+++ b/nixos/modules/services/networking/shairport-sync.nix
@@ -0,0 +1,112 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.shairport-sync;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.shairport-sync = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the shairport-sync daemon.
+
+          Running with a local system-wide or remote pulseaudio server
+          is recommended.
+        '';
+      };
+
+      arguments = mkOption {
+        type = types.str;
+        default = "-v -o pa";
+        description = ''
+          Arguments to pass to the daemon. Defaults to a local pulseaudio
+          server.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to automatically open ports in the firewall.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "shairport";
+        description = ''
+          User account name under which to run shairport-sync. The account
+          will be created.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "shairport";
+        description = ''
+          Group account name under which to run shairport-sync. The account
+          will be created.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.shairport-sync.enable {
+
+    services.avahi.enable = true;
+    services.avahi.publish.enable = true;
+    services.avahi.publish.userServices = true;
+
+    users = {
+      users.${cfg.user} = {
+        description = "Shairport user";
+        isSystemUser = true;
+        createHome = true;
+        home = "/var/lib/shairport-sync";
+        group = cfg.group;
+        extraGroups = [ "audio" ] ++ optional config.hardware.pulseaudio.enable "pulse";
+      };
+      groups.${cfg.group} = {};
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 5000 ];
+      allowedUDPPortRanges = [ { from = 6001; to = 6011; } ];
+    };
+
+    systemd.services.shairport-sync =
+      {
+        description = "shairport-sync";
+        after = [ "network.target" "avahi-daemon.service" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          User = cfg.user;
+          Group = cfg.group;
+          ExecStart = "${pkgs.shairport-sync}/bin/shairport-sync ${cfg.arguments}";
+          RuntimeDirectory = "shairport-sync";
+        };
+      };
+
+    environment.systemPackages = [ pkgs.shairport-sync ];
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/shellhub-agent.nix b/nixos/modules/services/networking/shellhub-agent.nix
new file mode 100644
index 00000000000..a45ef148544
--- /dev/null
+++ b/nixos/modules/services/networking/shellhub-agent.nix
@@ -0,0 +1,91 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.shellhub-agent;
+in {
+
+  ###### interface
+
+  options = {
+
+    services.shellhub-agent = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the ShellHub Agent daemon, which allows
+          secure remote logins.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.shellhub-agent;
+        defaultText = literalExpression "pkgs.shellhub-agent";
+        description = ''
+          Which ShellHub Agent package to use.
+        '';
+      };
+
+      tenantId = mkOption {
+        type = types.str;
+        example = "ba0a880c-2ada-11eb-a35e-17266ef329d6";
+        description = ''
+          The tenant ID to use when connecting to the ShellHub
+          Gateway.
+        '';
+      };
+
+      server = mkOption {
+        type = types.str;
+        default = "https://cloud.shellhub.io";
+        description = ''
+          Server address of ShellHub Gateway to connect.
+        '';
+      };
+
+      privateKey = mkOption {
+        type = types.path;
+        default = "/var/lib/shellhub-agent/private.key";
+        description = ''
+          Location where to store the ShellHub Agent private
+          key.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.shellhub-agent = {
+      description = "ShellHub Agent";
+
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "local-fs.target" ];
+      wants = [ "network-online.target" ];
+      after = [
+        "local-fs.target"
+        "network.target"
+        "network-online.target"
+        "time-sync.target"
+      ];
+
+      environment.SERVER_ADDRESS = cfg.server;
+      environment.PRIVATE_KEY = cfg.privateKey;
+      environment.TENANT_ID = cfg.tenantId;
+
+      serviceConfig = {
+        # The service starts sessions for different users.
+        User = "root";
+        Restart = "on-failure";
+        ExecStart = "${cfg.package}/bin/agent";
+      };
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/networking/shorewall.nix b/nixos/modules/services/networking/shorewall.nix
new file mode 100644
index 00000000000..ac732d4b12e
--- /dev/null
+++ b/nixos/modules/services/networking/shorewall.nix
@@ -0,0 +1,70 @@
+{ config, lib, pkgs, ... }:
+let
+  types = lib.types;
+  cfg = config.services.shorewall;
+in {
+  options = {
+    services.shorewall = {
+      enable = lib.mkOption {
+        type        = types.bool;
+        default     = false;
+        description = ''
+          Whether to enable Shorewall IPv4 Firewall.
+          <warning>
+            <para>
+            Enabling this service WILL disable the existing NixOS
+            firewall! Default firewall rules provided by packages are not
+            considered at the moment.
+            </para>
+          </warning>
+        '';
+      };
+      package = lib.mkOption {
+        type        = types.package;
+        default     = pkgs.shorewall;
+        defaultText = lib.literalExpression "pkgs.shorewall";
+        description = "The shorewall package to use.";
+      };
+      configs = lib.mkOption {
+        type        = types.attrsOf types.lines;
+        default     = {};
+        description = ''
+          This option defines the Shorewall configs.
+          The attribute name defines the name of the config,
+          and the attribute value defines the content of the config.
+        '';
+        apply = lib.mapAttrs (name: text: pkgs.writeText "${name}" text);
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.firewall.enable = false;
+    systemd.services.shorewall = {
+      description     = "Shorewall IPv4 Firewall";
+      after           = [ "ipset.target" ];
+      before          = [ "network-pre.target" ];
+      wants           = [ "network-pre.target" ];
+      wantedBy        = [ "multi-user.target" ];
+      reloadIfChanged = true;
+      restartTriggers = lib.attrValues cfg.configs;
+      serviceConfig = {
+        Type            = "oneshot";
+        RemainAfterExit = "yes";
+        ExecStart       = "${cfg.package}/bin/shorewall start";
+        ExecReload      = "${cfg.package}/bin/shorewall reload";
+        ExecStop        = "${cfg.package}/bin/shorewall stop";
+      };
+      preStart = ''
+        install -D -d -m 750 /var/lib/shorewall
+        install -D -d -m 755 /var/lock/subsys
+        touch                /var/log/shorewall.log
+        chown 750            /var/log/shorewall.log
+      '';
+    };
+    environment = {
+      etc = lib.mapAttrs' (name: conf: lib.nameValuePair "shorewall/${name}" {source=conf;}) cfg.configs;
+      systemPackages = [ cfg.package ];
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/shorewall6.nix b/nixos/modules/services/networking/shorewall6.nix
new file mode 100644
index 00000000000..4235c74a3f8
--- /dev/null
+++ b/nixos/modules/services/networking/shorewall6.nix
@@ -0,0 +1,70 @@
+{ config, lib, pkgs, ... }:
+let
+  types = lib.types;
+  cfg = config.services.shorewall6;
+in {
+  options = {
+    services.shorewall6 = {
+      enable = lib.mkOption {
+        type        = types.bool;
+        default     = false;
+        description = ''
+          Whether to enable Shorewall IPv6 Firewall.
+          <warning>
+            <para>
+            Enabling this service WILL disable the existing NixOS
+            firewall! Default firewall rules provided by packages are not
+            considered at the moment.
+            </para>
+          </warning>
+        '';
+      };
+      package = lib.mkOption {
+        type        = types.package;
+        default     = pkgs.shorewall;
+        defaultText = lib.literalExpression "pkgs.shorewall";
+        description = "The shorewall package to use.";
+      };
+      configs = lib.mkOption {
+        type        = types.attrsOf types.lines;
+        default     = {};
+        description = ''
+          This option defines the Shorewall configs.
+          The attribute name defines the name of the config,
+          and the attribute value defines the content of the config.
+        '';
+        apply = lib.mapAttrs (name: text: pkgs.writeText "${name}" text);
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.firewall.enable = false;
+    systemd.services.shorewall6 = {
+      description     = "Shorewall IPv6 Firewall";
+      after           = [ "ipset.target" ];
+      before          = [ "network-pre.target" ];
+      wants           = [ "network-pre.target" ];
+      wantedBy        = [ "multi-user.target" ];
+      reloadIfChanged = true;
+      restartTriggers = lib.attrValues cfg.configs;
+      serviceConfig = {
+        Type            = "oneshot";
+        RemainAfterExit = "yes";
+        ExecStart       = "${cfg.package}/bin/shorewall6 start";
+        ExecReload      = "${cfg.package}/bin/shorewall6 reload";
+        ExecStop        = "${cfg.package}/bin/shorewall6 stop";
+      };
+      preStart = ''
+        install -D -d -m 750 /var/lib/shorewall6
+        install -D -d -m 755 /var/lock/subsys
+        touch                /var/log/shorewall6.log
+        chown 750            /var/log/shorewall6.log
+      '';
+    };
+    environment = {
+      etc = lib.mapAttrs' (name: conf: lib.nameValuePair "shorewall6/${name}" {source=conf;}) cfg.configs;
+      systemPackages = [ cfg.package ];
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/shout.nix b/nixos/modules/services/networking/shout.nix
new file mode 100644
index 00000000000..cca03a8f88a
--- /dev/null
+++ b/nixos/modules/services/networking/shout.nix
@@ -0,0 +1,115 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.shout;
+  shoutHome = "/var/lib/shout";
+
+  defaultConfig = pkgs.runCommand "config.js" { preferLocalBuild = true; } ''
+    EDITOR=true ${pkgs.shout}/bin/shout config --home $PWD
+    mv config.js $out
+  '';
+
+  finalConfigFile = if (cfg.configFile != null) then cfg.configFile else ''
+    var _ = require('${pkgs.shout}/lib/node_modules/shout/node_modules/lodash')
+
+    module.exports = _.merge(
+      {},
+      require('${defaultConfig}'),
+      ${builtins.toJSON cfg.config}
+    )
+  '';
+
+in {
+  options.services.shout = {
+    enable = mkEnableOption "Shout web IRC client";
+
+    private = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Make your shout instance private. You will need to configure user
+        accounts by adding entries in <filename>${shoutHome}/users</filename>.
+      '';
+    };
+
+    listenAddress = mkOption {
+      type = types.str;
+      default = "0.0.0.0";
+      description = "IP interface to listen on for http connections.";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 9000;
+      description = "TCP port to listen on for http connections.";
+    };
+
+    configFile = mkOption {
+      type = types.nullOr types.lines;
+      default = null;
+      description = ''
+        Contents of Shout's <filename>config.js</filename> file.
+
+        Used for backward compatibility, recommended way is now to use
+        the <literal>config</literal> option.
+
+        Documentation: http://shout-irc.com/docs/server/configuration.html
+      '';
+    };
+
+    config = mkOption {
+      default = {};
+      type = types.attrs;
+      example = {
+        displayNetwork = false;
+        defaults = {
+          name = "Your Network";
+          host = "localhost";
+          port = 6697;
+        };
+      };
+      description = ''
+        Shout <filename>config.js</filename> contents as attribute set (will be
+        converted to JSON to generate the configuration file).
+
+        The options defined here will be merged to the default configuration file.
+
+        Documentation: http://shout-irc.com/docs/server/configuration.html
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.shout = {
+      isSystemUser = true;
+      group = "shout";
+      description = "Shout daemon user";
+      home = shoutHome;
+      createHome = true;
+    };
+    users.groups.shout = {};
+
+    systemd.services.shout = {
+      description = "Shout web IRC client";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+      preStart = "ln -sf ${pkgs.writeText "config.js" finalConfigFile} ${shoutHome}/config.js";
+      script = concatStringsSep " " [
+        "${pkgs.shout}/bin/shout"
+        (if cfg.private then "--private" else "--public")
+        "--port" (toString cfg.port)
+        "--host" (toString cfg.listenAddress)
+        "--home" shoutHome
+      ];
+      serviceConfig = {
+        User = "shout";
+        ProtectHome = "true";
+        ProtectSystem = "full";
+        PrivateTmp = "true";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/skydns.nix b/nixos/modules/services/networking/skydns.nix
new file mode 100644
index 00000000000..dea60a3862a
--- /dev/null
+++ b/nixos/modules/services/networking/skydns.nix
@@ -0,0 +1,93 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.skydns;
+
+in {
+  options.services.skydns = {
+    enable = mkEnableOption "skydns service";
+
+    etcd = {
+      machines = mkOption {
+        default = [ "http://127.0.0.1:2379" ];
+        type = types.listOf types.str;
+        description = "Skydns list of etcd endpoints to connect to.";
+      };
+
+      tlsKey = mkOption {
+        default = null;
+        type = types.nullOr types.path;
+        description = "Skydns path of TLS client certificate - private key.";
+      };
+
+      tlsPem = mkOption {
+        default = null;
+        type = types.nullOr types.path;
+        description = "Skydns path of TLS client certificate - public key.";
+      };
+
+      caCert = mkOption {
+        default = null;
+        type = types.nullOr types.path;
+        description = "Skydns path of TLS certificate authority public key.";
+      };
+    };
+
+    address = mkOption {
+      default = "0.0.0.0:53";
+      type = types.str;
+      description = "Skydns address to bind to.";
+    };
+
+    domain = mkOption {
+      default = "skydns.local.";
+      type = types.str;
+      description = "Skydns default domain if not specified by etcd config.";
+    };
+
+    nameservers = mkOption {
+      default = map (n: n + ":53") config.networking.nameservers;
+      defaultText = literalExpression ''map (n: n + ":53") config.networking.nameservers'';
+      type = types.listOf types.str;
+      description = "Skydns list of nameservers to forward DNS requests to when not authoritative for a domain.";
+      example = ["8.8.8.8:53" "8.8.4.4:53"];
+    };
+
+    package = mkOption {
+      default = pkgs.skydns;
+      defaultText = literalExpression "pkgs.skydns";
+      type = types.package;
+      description = "Skydns package to use.";
+    };
+
+    extraConfig = mkOption {
+      default = {};
+      type = types.attrsOf types.str;
+      description = "Skydns attribute set of extra config options passed as environment variables.";
+    };
+  };
+
+  config = mkIf (cfg.enable) {
+    systemd.services.skydns = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "etcd.service" ];
+      description = "Skydns Service";
+      environment = {
+        ETCD_MACHINES = concatStringsSep "," cfg.etcd.machines;
+        ETCD_TLSKEY = cfg.etcd.tlsKey;
+        ETCD_TLSPEM = cfg.etcd.tlsPem;
+        ETCD_CACERT = cfg.etcd.caCert;
+        SKYDNS_ADDR = cfg.address;
+        SKYDNS_DOMAIN = cfg.domain;
+        SKYDNS_NAMESERVERS = concatStringsSep "," cfg.nameservers;
+      };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/skydns";
+      };
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/networking/smartdns.nix b/nixos/modules/services/networking/smartdns.nix
new file mode 100644
index 00000000000..7f9df42ce9c
--- /dev/null
+++ b/nixos/modules/services/networking/smartdns.nix
@@ -0,0 +1,62 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+  inherit (lib.types) attrsOf coercedTo listOf oneOf str int bool;
+  cfg = config.services.smartdns;
+
+  confFile = pkgs.writeText "smartdns.conf" (with generators;
+    toKeyValue {
+      mkKeyValue = mkKeyValueDefault {
+        mkValueString = v:
+          if isBool v then
+            if v then "yes" else "no"
+          else
+            mkValueStringDefault { } v;
+      } " ";
+      listsAsDuplicateKeys =
+        true; # Allowing duplications because we need to deal with multiple entries with the same key.
+    } cfg.settings);
+in {
+  options.services.smartdns = {
+    enable = mkEnableOption "SmartDNS DNS server";
+
+    bindPort = mkOption {
+      type = types.port;
+      default = 53;
+      description = "DNS listening port number.";
+    };
+
+    settings = mkOption {
+      type =
+      let atom = oneOf [ str int bool ];
+      in attrsOf (coercedTo atom toList (listOf atom));
+      example = literalExpression ''
+        {
+          bind = ":5353 -no-rule -group example";
+          cache-size = 4096;
+          server-tls = [ "8.8.8.8:853" "1.1.1.1:853" ];
+          server-https = "https://cloudflare-dns.com/dns-query -exclude-default-group";
+          prefetch-domain = true;
+          speed-check-mode = "ping,tcp:80";
+        };
+      '';
+      description = ''
+        A set that will be generated into configuration file, see the <link xlink:href="https://github.com/pymumu/smartdns/blob/master/ReadMe_en.md#configuration-parameter">SmartDNS README</link> for details of configuration parameters.
+        You could override the options here like <option>services.smartdns.bindPort</option> by writing <literal>settings.bind = ":5353 -no-rule -group example";</literal>.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.smartdns.settings.bind = mkDefault ":${toString cfg.bindPort}";
+
+    systemd.packages = [ pkgs.smartdns ];
+    systemd.services.smartdns.wantedBy = [ "multi-user.target" ];
+    systemd.services.smartdns.restartTriggers = [ confFile ];
+    environment.etc."smartdns/smartdns.conf".source = confFile;
+    environment.etc."default/smartdns".source =
+      "${pkgs.smartdns}/etc/default/smartdns";
+  };
+}
diff --git a/nixos/modules/services/networking/smokeping.nix b/nixos/modules/services/networking/smokeping.nix
new file mode 100644
index 00000000000..bd71b158dbe
--- /dev/null
+++ b/nixos/modules/services/networking/smokeping.nix
@@ -0,0 +1,362 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+
+  cfg = config.services.smokeping;
+  smokepingHome = "/var/lib/smokeping";
+  smokepingPidDir = "/run";
+  configFile =
+    if cfg.config == null
+      then
+        ''
+          *** General ***
+          cgiurl   = ${cfg.cgiUrl}
+          contact = ${cfg.ownerEmail}
+          datadir  = ${smokepingHome}/data
+          imgcache = ${smokepingHome}/cache
+          imgurl   = ${cfg.imgUrl}
+          linkstyle = ${cfg.linkStyle}
+          ${lib.optionalString (cfg.mailHost != "") "mailhost = ${cfg.mailHost}"}
+          owner = ${cfg.owner}
+          pagedir = ${smokepingHome}/cache
+          piddir  = ${smokepingPidDir}
+          ${lib.optionalString (cfg.sendmail != null) "sendmail = ${cfg.sendmail}"}
+          smokemail = ${cfg.smokeMailTemplate}
+          *** Presentation ***
+          template = ${cfg.presentationTemplate}
+          ${cfg.presentationConfig}
+          *** Alerts ***
+          ${cfg.alertConfig}
+          *** Database ***
+          ${cfg.databaseConfig}
+          *** Probes ***
+          ${cfg.probeConfig}
+          *** Targets ***
+          ${cfg.targetConfig}
+          ${cfg.extraConfig}
+        ''
+      else
+        cfg.config;
+
+  configPath = pkgs.writeText "smokeping.conf" configFile;
+  cgiHome = pkgs.writeScript "smokeping.fcgi" ''
+    #!${pkgs.bash}/bin/bash
+    ${cfg.package}/bin/smokeping_cgi ${configPath}
+  '';
+in
+
+{
+  options = {
+    services.smokeping = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable the smokeping service";
+      };
+      alertConfig = mkOption {
+        type = types.lines;
+        default = ''
+          to = root@localhost
+          from = smokeping@localhost
+        '';
+        example = ''
+          to = alertee@address.somewhere
+          from = smokealert@company.xy
+
+          +someloss
+          type = loss
+          # in percent
+          pattern = >0%,*12*,>0%,*12*,>0%
+          comment = loss 3 times  in a row;
+        '';
+        description = "Configuration for alerts.";
+      };
+      cgiUrl = mkOption {
+        type = types.str;
+        default = "http://${cfg.hostName}:${toString cfg.port}/smokeping.cgi";
+        defaultText = literalExpression ''"http://''${hostName}:''${toString port}/smokeping.cgi"'';
+        example = "https://somewhere.example.com/smokeping.cgi";
+        description = "URL to the smokeping cgi.";
+      };
+      config = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = "Full smokeping config supplied by the user. Overrides " +
+          "and replaces any other configuration supplied.";
+      };
+      databaseConfig = mkOption {
+        type = types.lines;
+        default = ''
+          step     = 300
+          pings    = 20
+          # consfn mrhb steps total
+          AVERAGE  0.5   1  1008
+          AVERAGE  0.5  12  4320
+              MIN  0.5  12  4320
+              MAX  0.5  12  4320
+          AVERAGE  0.5 144   720
+              MAX  0.5 144   720
+              MIN  0.5 144   720
+
+        '';
+        example = ''
+          # near constant pings.
+          step     = 30
+          pings    = 20
+          # consfn mrhb steps total
+          AVERAGE  0.5   1  10080
+          AVERAGE  0.5  12  43200
+              MIN  0.5  12  43200
+              MAX  0.5  12  43200
+          AVERAGE  0.5 144   7200
+              MAX  0.5 144   7200
+              MIN  0.5 144   7200
+        '';
+        description = ''Configure the ping frequency and retention of the rrd files.
+          Once set, changing the interval will require deletion or migration of all
+          the collected data.'';
+      };
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Any additional customization not already included.";
+      };
+      hostName = mkOption {
+        type = types.str;
+        default = config.networking.fqdn;
+        defaultText = literalExpression "config.networking.fqdn";
+        example = "somewhere.example.com";
+        description = "DNS name for the urls generated in the cgi.";
+      };
+      imgUrl = mkOption {
+        type = types.str;
+        default = "cache";
+        defaultText = literalExpression ''"cache"'';
+        example = "https://somewhere.example.com/cache";
+        description = ''
+          Base url for images generated in the cgi.
+
+          The default is a relative URL to ensure it works also when e.g. forwarding
+          the GUI port via SSH.
+        '';
+      };
+      linkStyle = mkOption {
+        type = types.enum ["original" "absolute" "relative"];
+        default = "relative";
+        example = "absolute";
+        description = "DNS name for the urls generated in the cgi.";
+      };
+      mailHost = mkOption {
+        type = types.str;
+        default = "";
+        example = "localhost";
+        description = "Use this SMTP server to send alerts";
+      };
+      owner = mkOption {
+        type = types.str;
+        default = "nobody";
+        example = "Joe Admin";
+        description = "Real name of the owner of the instance";
+      };
+      ownerEmail = mkOption {
+        type = types.str;
+        default = "no-reply@${cfg.hostName}";
+        defaultText = literalExpression ''"no-reply@''${hostName}"'';
+        example = "no-reply@yourdomain.com";
+        description = "Email contact for owner";
+      };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.smokeping;
+        defaultText = literalExpression "pkgs.smokeping";
+        description = "Specify a custom smokeping package";
+      };
+      host = mkOption {
+        type = types.nullOr types.str;
+        default = "localhost";
+        example = "192.0.2.1"; # rfc5737 example IP for documentation
+        description = ''
+          Host/IP to bind to for the web server.
+
+          Setting it to <literal>null</literal> skips passing the -h option to thttpd,
+          which makes it bind to all interfaces.
+        '';
+      };
+      port = mkOption {
+        type = types.int;
+        default = 8081;
+        description = "TCP port to use for the web server.";
+      };
+      presentationConfig = mkOption {
+        type = types.lines;
+        default = ''
+          + charts
+          menu = Charts
+          title = The most interesting destinations
+          ++ stddev
+          sorter = StdDev(entries=>4)
+          title = Top Standard Deviation
+          menu = Std Deviation
+          format = Standard Deviation %f
+          ++ max
+          sorter = Max(entries=>5)
+          title = Top Max Roundtrip Time
+          menu = by Max
+          format = Max Roundtrip Time %f seconds
+          ++ loss
+          sorter = Loss(entries=>5)
+          title = Top Packet Loss
+          menu = Loss
+          format = Packets Lost %f
+          ++ median
+          sorter = Median(entries=>5)
+          title = Top Median Roundtrip Time
+          menu = by Median
+          format = Median RTT %f seconds
+          + overview
+          width = 600
+          height = 50
+          range = 10h
+          + detail
+          width = 600
+          height = 200
+          unison_tolerance = 2
+          "Last 3 Hours"    3h
+          "Last 30 Hours"   30h
+          "Last 10 Days"    10d
+          "Last 360 Days"   360d
+        '';
+        description = "presentation graph style";
+      };
+      presentationTemplate = mkOption {
+        type = types.str;
+        default = "${pkgs.smokeping}/etc/basepage.html.dist";
+        defaultText = literalExpression ''"''${pkgs.smokeping}/etc/basepage.html.dist"'';
+        description = "Default page layout for the web UI.";
+      };
+      probeConfig = mkOption {
+        type = types.lines;
+        default = ''
+          + FPing
+          binary = ${config.security.wrapperDir}/fping
+        '';
+        defaultText = literalExpression ''
+          '''
+            + FPing
+            binary = ''${config.security.wrapperDir}/fping
+          '''
+        '';
+        description = "Probe configuration";
+      };
+      sendmail = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/run/wrappers/bin/sendmail";
+        description = "Use this sendmail compatible script to deliver alerts";
+      };
+      smokeMailTemplate = mkOption {
+        type = types.str;
+        default = "${cfg.package}/etc/smokemail.dist";
+        defaultText = literalExpression ''"''${package}/etc/smokemail.dist"'';
+        description = "Specify the smokemail template for alerts.";
+      };
+      targetConfig = mkOption {
+        type = types.lines;
+        default = ''
+          probe = FPing
+          menu = Top
+          title = Network Latency Grapher
+          remark = Welcome to the SmokePing website of xxx Company. \
+                   Here you will learn all about the latency of our network.
+          + Local
+          menu = Local
+          title = Local Network
+          ++ LocalMachine
+          menu = Local Machine
+          title = This host
+          host = localhost
+        '';
+        description = "Target configuration";
+      };
+      user = mkOption {
+        type = types.str;
+        default = "smokeping";
+        description = "User that runs smokeping and (optionally) thttpd. A group of the same name will be created as well.";
+      };
+      webService = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Enable a smokeping web interface";
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !(cfg.sendmail != null && cfg.mailHost != "");
+        message = "services.smokeping: sendmail and Mailhost cannot both be enabled.";
+      }
+    ];
+    security.wrappers = {
+      fping =
+        { setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${pkgs.fping}/bin/fping";
+        };
+    };
+    environment.systemPackages = [ pkgs.fping ];
+    users.users.${cfg.user} = {
+      isNormalUser = false;
+      isSystemUser = true;
+      group = cfg.user;
+      description = "smokeping daemon user";
+      home = smokepingHome;
+      createHome = true;
+    };
+    users.groups.${cfg.user} = {};
+    systemd.services.smokeping = {
+      requiredBy = [ "multi-user.target"];
+      serviceConfig = {
+        User = cfg.user;
+        Restart = "on-failure";
+        ExecStart = "${cfg.package}/bin/smokeping --config=${configPath} --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
+        ${cfg.package}/bin/smokeping --check --config=${configPath}
+        ${cfg.package}/bin/smokeping --static --config=${configPath}
+      '';
+    };
+    systemd.services.thttpd = mkIf cfg.webService {
+      requiredBy = [ "multi-user.target"];
+      requires = [ "smokeping.service"];
+      path = with pkgs; [ bash rrdtool smokeping thttpd ];
+      serviceConfig = {
+        Restart = "always";
+        ExecStart = lib.concatStringsSep " " (lib.concatLists [
+          [ "${pkgs.thttpd}/bin/thttpd" ]
+          [ "-u ${cfg.user}" ]
+          [ ''-c "**.fcgi"'' ]
+          [ "-d ${smokepingHome}" ]
+          (lib.optional (cfg.host != null) "-h ${cfg.host}")
+          [ "-p ${builtins.toString cfg.port}" ]
+          [ "-D -nos" ]
+        ]);
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [
+    erictapen
+    nh2
+  ];
+}
+
diff --git a/nixos/modules/services/networking/sniproxy.nix b/nixos/modules/services/networking/sniproxy.nix
new file mode 100644
index 00000000000..adca5398e4a
--- /dev/null
+++ b/nixos/modules/services/networking/sniproxy.nix
@@ -0,0 +1,88 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.sniproxy;
+
+  configFile = pkgs.writeText "sniproxy.conf" ''
+    user ${cfg.user}
+    pidfile /run/sniproxy.pid
+    ${cfg.config}
+  '';
+
+in
+{
+  imports = [ (mkRemovedOptionModule [ "services" "sniproxy" "logDir" ] "Now done by LogsDirectory=. Set to a custom path if you log to a different folder in your config.") ];
+
+  options = {
+    services.sniproxy = {
+      enable = mkEnableOption "sniproxy server";
+
+      user = mkOption {
+        type = types.str;
+        default = "sniproxy";
+        description = "User account under which sniproxy runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "sniproxy";
+        description = "Group under which sniproxy runs.";
+      };
+
+      config = mkOption {
+        type = types.lines;
+        default = "";
+        description = "sniproxy.conf configuration excluding the daemon username and pid file.";
+        example = ''
+          error_log {
+            filename /var/log/sniproxy/error.log
+          }
+          access_log {
+            filename /var/log/sniproxy/access.log
+          }
+          listen 443 {
+            proto tls
+          }
+          table {
+            example.com 192.0.2.10
+            example.net 192.0.2.20
+          }
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.sniproxy = {
+      description = "sniproxy server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${pkgs.sniproxy}/bin/sniproxy -c ${configFile}";
+        LogsDirectory = "sniproxy";
+        LogsDirectoryMode = "0640";
+        Restart = "always";
+      };
+    };
+
+    users.users = mkIf (cfg.user == "sniproxy") {
+      sniproxy = {
+        group = cfg.group;
+        uid = config.ids.uids.sniproxy;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "sniproxy") {
+      sniproxy = {
+        gid = config.ids.gids.sniproxy;
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/networking/snowflake-proxy.nix b/nixos/modules/services/networking/snowflake-proxy.nix
new file mode 100644
index 00000000000..2124644ed9b
--- /dev/null
+++ b/nixos/modules/services/networking/snowflake-proxy.nix
@@ -0,0 +1,81 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.snowflake-proxy;
+in
+{
+  options = {
+    services.snowflake-proxy = {
+      enable = mkEnableOption "System to defeat internet censorship";
+
+      broker = mkOption {
+        description = "Broker URL (default \"https://snowflake-broker.torproject.net/\")";
+        type = with types; nullOr str;
+        default = null;
+      };
+
+      capacity = mkOption {
+        description = "Limits the amount of maximum concurrent clients allowed.";
+        type = with types; nullOr int;
+        default = null;
+      };
+
+      relay = mkOption {
+        description = "websocket relay URL (default \"wss://snowflake.bamsoftware.com/\")";
+        type = with types; nullOr str;
+        default = null;
+      };
+
+      stun = mkOption {
+        description = "STUN broker URL (default \"stun:stun.stunprotocol.org:3478\")";
+        type = with types; nullOr str;
+        default = null;
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.snowflake-proxy = {
+      wantedBy = [ "network-online.target" ];
+      serviceConfig = {
+        ExecStart =
+          "${pkgs.snowflake}/bin/proxy " + concatStringsSep " " (
+            optional (cfg.broker != null) "-broker ${cfg.broker}"
+            ++ optional (cfg.capacity != null) "-capacity ${builtins.toString cfg.capacity}"
+            ++ optional (cfg.relay != null) "-relay ${cfg.relay}"
+            ++ optional (cfg.stun != null) "-stun ${cfg.stun}"
+          );
+
+        # Security Hardening
+        # Refer to systemd.exec(5) for option descriptions.
+        CapabilityBoundingSet = "";
+
+        # implies RemoveIPC=, PrivateTmp=, NoNewPrivileges=, RestrictSUIDSGID=,
+        # ProtectSystem=strict, ProtectHome=read-only
+        DynamicUser = true;
+        LockPersonality = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectProc = "invisible";
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "~@clock @cpu-emulation @debug @mount @obsolete @reboot @swap @privileged @resources";
+        UMask = "0077";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ yayayayaka ];
+}
diff --git a/nixos/modules/services/networking/softether.nix b/nixos/modules/services/networking/softether.nix
new file mode 100644
index 00000000000..5405f56871e
--- /dev/null
+++ b/nixos/modules/services/networking/softether.nix
@@ -0,0 +1,163 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.softether;
+
+  package = cfg.package.override { dataDir = cfg.dataDir; };
+
+in
+{
+
+  ###### interface
+
+  options = {
+
+    services.softether = {
+
+      enable = mkEnableOption "SoftEther VPN services";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.softether;
+        defaultText = literalExpression "pkgs.softether";
+        description = ''
+          softether derivation to use.
+        '';
+      };
+
+      vpnserver.enable = mkEnableOption "SoftEther VPN Server";
+
+      vpnbridge.enable = mkEnableOption "SoftEther VPN Bridge";
+
+      vpnclient = {
+        enable = mkEnableOption "SoftEther VPN Client";
+        up = mkOption {
+          type = types.lines;
+          default = "";
+          description = ''
+            Shell commands executed when the Virtual Network Adapter(s) is/are starting.
+          '';
+        };
+        down = mkOption {
+          type = types.lines;
+          default = "";
+          description = ''
+            Shell commands executed when the Virtual Network Adapter(s) is/are shutting down.
+          '';
+        };
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/softether";
+        description = ''
+          Data directory for SoftEther VPN.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable (
+
+    mkMerge [{
+      environment.systemPackages = [ package ];
+
+      systemd.services.softether-init = {
+        description = "SoftEther VPN services initial task";
+        wantedBy = [ "network.target" ];
+        serviceConfig = {
+          Type = "oneshot";
+          RemainAfterExit = false;
+        };
+        script = ''
+            for d in vpnserver vpnbridge vpnclient vpncmd; do
+                if ! test -e ${cfg.dataDir}/$d; then
+                    ${pkgs.coreutils}/bin/mkdir -m0700 -p ${cfg.dataDir}/$d
+                    install -m0600 ${package}${cfg.dataDir}/$d/hamcore.se2 ${cfg.dataDir}/$d/hamcore.se2
+                fi
+            done
+            rm -rf ${cfg.dataDir}/vpncmd/vpncmd
+            ln -s ${package}${cfg.dataDir}/vpncmd/vpncmd ${cfg.dataDir}/vpncmd/vpncmd
+        '';
+      };
+    }
+
+    (mkIf (cfg.vpnserver.enable) {
+      systemd.services.vpnserver = {
+        description = "SoftEther VPN Server";
+        after = [ "softether-init.service" ];
+        requires = [ "softether-init.service" ];
+        wantedBy = [ "network.target" ];
+        serviceConfig = {
+          Type = "forking";
+          ExecStart = "${package}/bin/vpnserver start";
+          ExecStop = "${package}/bin/vpnserver stop";
+        };
+        preStart = ''
+            rm -rf ${cfg.dataDir}/vpnserver/vpnserver
+            ln -s ${package}${cfg.dataDir}/vpnserver/vpnserver ${cfg.dataDir}/vpnserver/vpnserver
+        '';
+        postStop = ''
+            rm -rf ${cfg.dataDir}/vpnserver/vpnserver
+        '';
+      };
+    })
+
+    (mkIf (cfg.vpnbridge.enable) {
+      systemd.services.vpnbridge = {
+        description = "SoftEther VPN Bridge";
+        after = [ "softether-init.service" ];
+        requires = [ "softether-init.service" ];
+        wantedBy = [ "network.target" ];
+        serviceConfig = {
+          Type = "forking";
+          ExecStart = "${package}/bin/vpnbridge start";
+          ExecStop = "${package}/bin/vpnbridge stop";
+        };
+        preStart = ''
+            rm -rf ${cfg.dataDir}/vpnbridge/vpnbridge
+            ln -s ${package}${cfg.dataDir}/vpnbridge/vpnbridge ${cfg.dataDir}/vpnbridge/vpnbridge
+        '';
+        postStop = ''
+            rm -rf ${cfg.dataDir}/vpnbridge/vpnbridge
+        '';
+      };
+    })
+
+    (mkIf (cfg.vpnclient.enable) {
+      systemd.services.vpnclient = {
+        description = "SoftEther VPN Client";
+        after = [ "softether-init.service" ];
+        requires = [ "softether-init.service" ];
+        wantedBy = [ "network.target" ];
+        serviceConfig = {
+          Type = "forking";
+          ExecStart = "${package}/bin/vpnclient start";
+          ExecStop = "${package}/bin/vpnclient stop";
+        };
+        preStart = ''
+            rm -rf ${cfg.dataDir}/vpnclient/vpnclient
+            ln -s ${package}${cfg.dataDir}/vpnclient/vpnclient ${cfg.dataDir}/vpnclient/vpnclient
+        '';
+        postStart = ''
+            sleep 1
+            ${cfg.vpnclient.up}
+        '';
+        postStop = ''
+            rm -rf ${cfg.dataDir}/vpnclient/vpnclient
+            sleep 1
+            ${cfg.vpnclient.down}
+        '';
+      };
+      boot.kernelModules = [ "tun" ];
+    })
+
+  ]);
+
+}
diff --git a/nixos/modules/services/networking/soju.nix b/nixos/modules/services/networking/soju.nix
new file mode 100644
index 00000000000..cb0acf4765f
--- /dev/null
+++ b/nixos/modules/services/networking/soju.nix
@@ -0,0 +1,114 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.soju;
+  stateDir = "/var/lib/soju";
+  listenCfg = concatMapStringsSep "\n" (l: "listen ${l}") cfg.listen;
+  tlsCfg = optionalString (cfg.tlsCertificate != null)
+    "tls ${cfg.tlsCertificate} ${cfg.tlsCertificateKey}";
+  logCfg = optionalString cfg.enableMessageLogging
+    "log fs ${stateDir}/logs";
+
+  configFile = pkgs.writeText "soju.conf" ''
+    ${listenCfg}
+    hostname ${cfg.hostName}
+    ${tlsCfg}
+    db sqlite3 ${stateDir}/soju.db
+    ${logCfg}
+    http-origin ${concatStringsSep " " cfg.httpOrigins}
+    accept-proxy-ip ${concatStringsSep " " cfg.acceptProxyIP}
+
+    ${cfg.extraConfig}
+  '';
+in
+{
+  ###### interface
+
+  options.services.soju = {
+    enable = mkEnableOption "soju";
+
+    listen = mkOption {
+      type = types.listOf types.str;
+      default = [ ":6697" ];
+      description = ''
+        Where soju should listen for incoming connections. See the
+        <literal>listen</literal> directive in
+        <citerefentry><refentrytitle>soju</refentrytitle>
+        <manvolnum>1</manvolnum></citerefentry>.
+      '';
+    };
+
+    hostName = mkOption {
+      type = types.str;
+      default = config.networking.hostName;
+      defaultText = literalExpression "config.networking.hostName";
+      description = "Server hostname.";
+    };
+
+    tlsCertificate = mkOption {
+      type = types.nullOr types.path;
+      example = "/var/host.cert";
+      description = "Path to server TLS certificate.";
+    };
+
+    tlsCertificateKey = mkOption {
+      type = types.nullOr types.path;
+      example = "/var/host.key";
+      description = "Path to server TLS certificate key.";
+    };
+
+    enableMessageLogging = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Whether to enable message logging.";
+    };
+
+    httpOrigins = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        List of allowed HTTP origins for WebSocket listeners. The parameters are
+        interpreted as shell patterns, see
+        <citerefentry><refentrytitle>glob</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.
+      '';
+    };
+
+    acceptProxyIP = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Allow the specified IPs to act as a proxy. Proxys have the ability to
+        overwrite the remote and local connection addresses (via the X-Forwarded-\*
+        HTTP header fields). The special name "localhost" accepts the loopback
+        addresses 127.0.0.0/8 and ::1/128. By default, all IPs are rejected.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = "Lines added verbatim to the configuration file.";
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.soju = {
+      description = "soju IRC bouncer";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+        ExecStart = "${pkgs.soju}/bin/soju -config ${configFile}";
+        StateDirectory = "soju";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ malvo ];
+}
diff --git a/nixos/modules/services/networking/solanum.nix b/nixos/modules/services/networking/solanum.nix
new file mode 100644
index 00000000000..dc066a24549
--- /dev/null
+++ b/nixos/modules/services/networking/solanum.nix
@@ -0,0 +1,109 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkEnableOption mkIf mkOption types;
+  inherit (pkgs) solanum util-linux;
+  cfg = config.services.solanum;
+
+  configFile = pkgs.writeText "solanum.conf" cfg.config;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.solanum = {
+
+      enable = mkEnableOption "Solanum IRC daemon";
+
+      config = mkOption {
+        type = types.str;
+        default = ''
+          serverinfo {
+            name = "irc.example.com";
+            sid = "1ix";
+            description = "irc!";
+
+            vhost = "0.0.0.0";
+            vhost6 = "::";
+          };
+
+          listen {
+            host = "0.0.0.0";
+            port = 6667;
+          };
+
+          auth {
+            user = "*@*";
+            class = "users";
+            flags = exceed_limit;
+          };
+          channel {
+            default_split_user_count = 0;
+          };
+        '';
+        description = ''
+          Solanum IRC daemon configuration file.
+          check <link xlink:href="https://github.com/solanum-ircd/solanum/blob/main/doc/reference.conf"/> for all options.
+        '';
+      };
+
+      openFilesLimit = mkOption {
+        type = types.int;
+        default = 1024;
+        description = ''
+          Maximum number of open files. Limits the clients and server connections.
+        '';
+      };
+
+      motd = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = ''
+          Solanum MOTD text.
+
+          Solanum will read its MOTD from <literal>/etc/solanum/ircd.motd</literal>.
+          If set, the value of this option will be written to this path.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable (lib.mkMerge [
+    {
+
+      environment.etc."solanum/ircd.conf".source = configFile;
+
+      systemd.services.solanum = {
+        description = "Solanum IRC daemon";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        reloadIfChanged = true;
+        restartTriggers = [
+          configFile
+        ];
+        serviceConfig = {
+          ExecStart = "${solanum}/bin/solanum -foreground -logfile /dev/stdout -configfile /etc/solanum/ircd.conf -pidfile /run/solanum/ircd.pid";
+          ExecReload = "${util-linux}/bin/kill -HUP $MAINPID";
+          DynamicUser = true;
+          User = "solanum";
+          StateDirectory = "solanum";
+          RuntimeDirectory = "solanum";
+          LimitNOFILE = "${toString cfg.openFilesLimit}";
+        };
+      };
+
+    }
+
+    (mkIf (cfg.motd != null) {
+      environment.etc."solanum/ircd.motd".text = cfg.motd;
+    })
+  ]);
+}
diff --git a/nixos/modules/services/networking/spacecookie.nix b/nixos/modules/services/networking/spacecookie.nix
new file mode 100644
index 00000000000..400f3e26cc9
--- /dev/null
+++ b/nixos/modules/services/networking/spacecookie.nix
@@ -0,0 +1,216 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.spacecookie;
+
+  spacecookieConfig = {
+    listen = {
+      inherit (cfg) port;
+    };
+  } // cfg.settings;
+
+  format = pkgs.formats.json {};
+
+  configFile = format.generate "spacecookie.json" spacecookieConfig;
+
+in {
+  imports = [
+    (mkRenamedOptionModule [ "services" "spacecookie" "root" ] [ "services" "spacecookie" "settings" "root" ])
+    (mkRenamedOptionModule [ "services" "spacecookie" "hostname" ] [ "services" "spacecookie" "settings" "hostname" ])
+  ];
+
+  options = {
+
+    services.spacecookie = {
+
+      enable = mkEnableOption "spacecookie";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.spacecookie;
+        defaultText = literalExpression "pkgs.spacecookie";
+        example = literalExpression "pkgs.haskellPackages.spacecookie";
+        description = ''
+          The spacecookie derivation to use. This can be used to
+          override the used package or to use another version.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to open the necessary port in the firewall for spacecookie.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 70;
+        description = ''
+          Port the gopher service should be exposed on.
+        '';
+      };
+
+      address = mkOption {
+        type = types.str;
+        default = "[::]";
+        description = ''
+          Address to listen on. Must be in the
+          <literal>ListenStream=</literal> syntax of
+          <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.socket.html">systemd.socket(5)</link>.
+        '';
+      };
+
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = format.type;
+
+          options.hostname = mkOption {
+            type = types.str;
+            default = "localhost";
+            description = ''
+              The hostname the service is reachable via. Clients
+              will use this hostname for further requests after
+              loading the initial gopher menu.
+            '';
+          };
+
+          options.root = mkOption {
+            type = types.path;
+            default = "/srv/gopher";
+            description = ''
+              The directory spacecookie should serve via gopher.
+              Files in there need to be world-readable since
+              the spacecookie service file sets
+              <literal>DynamicUser=true</literal>.
+            '';
+          };
+
+          options.log = {
+            enable = mkEnableOption "logging for spacecookie"
+              // { default = true; example = false; };
+
+            hide-ips = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                If enabled, spacecookie will hide personal
+                information of users like IP addresses from
+                log output.
+              '';
+            };
+
+            hide-time = mkOption {
+              type = types.bool;
+              # since we are starting with systemd anyways
+              # we deviate from the default behavior here:
+              # journald will add timestamps, so no need
+              # to double up.
+              default = true;
+              description = ''
+                If enabled, spacecookie will not print timestamps
+                at the beginning of every log line.
+              '';
+            };
+
+            level = mkOption {
+              type = types.enum [
+                "info"
+                "warn"
+                "error"
+              ];
+              default = "info";
+              description = ''
+                Log level for the spacecookie service.
+              '';
+            };
+          };
+        };
+
+        description = ''
+          Settings for spacecookie. The settings set here are
+          directly translated to the spacecookie JSON config
+          file. See
+          <link xlink:href="https://sternenseemann.github.io/spacecookie/spacecookie.json.5.html">spacecookie.json(5)</link>
+          for explanations of all options.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !(cfg.settings ? user);
+        message = ''
+          spacecookie is started as a normal user, so the setuid
+          feature doesn't work. If you want to run spacecookie as
+          a specific user, set:
+          systemd.services.spacecookie.serviceConfig = {
+            DynamicUser = false;
+            User = "youruser";
+            Group = "yourgroup";
+          }
+        '';
+      }
+      {
+        assertion = !(cfg.settings ? listen || cfg.settings ? port);
+        message = ''
+          The NixOS spacecookie module uses socket activation,
+          so the listen options have no effect. Use the port
+          and address options in services.spacecookie instead.
+        '';
+      }
+    ];
+
+    systemd.sockets.spacecookie = {
+      description = "Socket for the Spacecookie Gopher Server";
+      wantedBy = [ "sockets.target" ];
+      listenStreams = [ "${cfg.address}:${toString cfg.port}" ];
+      socketConfig = {
+        BindIPv6Only = "both";
+      };
+    };
+
+    systemd.services.spacecookie = {
+      description = "Spacecookie Gopher Server";
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "spacecookie.socket" ];
+
+      serviceConfig = {
+        Type = "notify";
+        ExecStart = "${lib.getBin cfg.package}/bin/spacecookie ${configFile}";
+        FileDescriptorStoreMax = 1;
+
+        DynamicUser = true;
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        LockPersonality = true;
+        RestrictRealtime = true;
+
+        # AF_UNIX for communication with systemd
+        # AF_INET replaced by BindIPv6Only=both
+        RestrictAddressFamilies = "AF_UNIX AF_INET6";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/spiped.nix b/nixos/modules/services/networking/spiped.nix
new file mode 100644
index 00000000000..3c229ecfc72
--- /dev/null
+++ b/nixos/modules/services/networking/spiped.nix
@@ -0,0 +1,220 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.spiped;
+in
+{
+  options = {
+    services.spiped = {
+      enable = mkOption {
+        type        = types.bool;
+        default     = false;
+        description = "Enable the spiped service module.";
+      };
+
+      config = mkOption {
+        type = types.attrsOf (types.submodule (
+          {
+            options = {
+              encrypt = mkOption {
+                type    = types.bool;
+                default = false;
+                description = ''
+                  Take unencrypted connections from the
+                  <literal>source</literal> socket and send encrypted
+                  connections to the <literal>target</literal> socket.
+                '';
+              };
+
+              decrypt = mkOption {
+                type    = types.bool;
+                default = false;
+                description = ''
+                  Take encrypted connections from the
+                  <literal>source</literal> socket and send unencrypted
+                  connections to the <literal>target</literal> socket.
+                '';
+              };
+
+              source = mkOption {
+                type    = types.str;
+                description = ''
+                  Address on which spiped should listen for incoming
+                  connections.  Must be in one of the following formats:
+                  <literal>/absolute/path/to/unix/socket</literal>,
+                  <literal>host.name:port</literal>,
+                  <literal>[ip.v4.ad.dr]:port</literal> or
+                  <literal>[ipv6::addr]:port</literal> - note that
+                  hostnames are resolved when spiped is launched and are
+                  not re-resolved later; thus if DNS entries change
+                  spiped will continue to connect to the expired
+                  address.
+                '';
+              };
+
+              target = mkOption {
+                type    = types.str;
+                description = "Address to which spiped should connect.";
+              };
+
+              keyfile = mkOption {
+                type    = types.path;
+                description = ''
+                  Name of a file containing the spiped key. As the
+                  daemon runs as the <literal>spiped</literal> user, the
+                  key file must be somewhere owned by that user. By
+                  default, we recommend putting the keys for any spipe
+                  services in <literal>/var/lib/spiped</literal>.
+                '';
+              };
+
+              timeout = mkOption {
+                type = types.int;
+                default = 5;
+                description = ''
+                  Timeout, in seconds, after which an attempt to connect to
+                  the target or a protocol handshake will be aborted (and the
+                  connection dropped) if not completed
+                '';
+              };
+
+              maxConns = mkOption {
+                type = types.int;
+                default = 100;
+                description = ''
+                  Limit on the number of simultaneous connections allowed.
+                '';
+              };
+
+              waitForDNS = mkOption {
+                type = types.bool;
+                default = false;
+                description = ''
+                  Wait for DNS. Normally when <literal>spiped</literal> is
+                  launched it resolves addresses and binds to its source
+                  socket before the parent process returns; with this option
+                  it will daemonize first and retry failed DNS lookups until
+                  they succeed. This allows <literal>spiped</literal> to
+                  launch even if DNS isn't set up yet, but at the expense of
+                  losing the guarantee that once <literal>spiped</literal> has
+                  finished launching it will be ready to create pipes.
+                '';
+              };
+
+              disableKeepalives = mkOption {
+                type = types.bool;
+                default = false;
+                description = "Disable transport layer keep-alives.";
+              };
+
+              weakHandshake = mkOption {
+                type = types.bool;
+                default = false;
+                description = ''
+                  Use fast/weak handshaking: This reduces the CPU time spent
+                  in the initial connection setup, at the expense of losing
+                  perfect forward secrecy.
+                '';
+              };
+
+              resolveRefresh = mkOption {
+                type = types.int;
+                default = 60;
+                description = ''
+                  Resolution refresh time for the target socket, in seconds.
+                '';
+              };
+
+              disableReresolution = mkOption {
+                type = types.bool;
+                default = false;
+                description = "Disable target address re-resolution.";
+              };
+            };
+          }
+        ));
+
+        default = {};
+
+        example = literalExpression ''
+          {
+            pipe1 =
+              { keyfile = "/var/lib/spiped/pipe1.key";
+                encrypt = true;
+                source  = "localhost:6000";
+                target  = "endpoint.example.com:7000";
+              };
+            pipe2 =
+              { keyfile = "/var/lib/spiped/pipe2.key";
+                decrypt = true;
+                source  = "0.0.0.0:7000";
+                target  = "localhost:3000";
+              };
+          }
+        '';
+
+        description = ''
+          Configuration for a secure pipe daemon. The daemon can be
+          started, stopped, or examined using
+          <literal>systemctl</literal>, under the name
+          <literal>spiped@foo</literal>.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = mapAttrsToList (name: c: {
+      assertion = (c.encrypt -> !c.decrypt) || (c.decrypt -> c.encrypt);
+      message   = "A pipe must either encrypt or decrypt";
+    }) cfg.config;
+
+    users.groups.spiped.gid = config.ids.gids.spiped;
+    users.users.spiped = {
+      description = "Secure Pipe Service user";
+      group       = "spiped";
+      uid         = config.ids.uids.spiped;
+    };
+
+    systemd.services."spiped@" = {
+      description = "Secure pipe '%i'";
+      after       = [ "network.target" ];
+
+      serviceConfig = {
+        Restart   = "always";
+        User      = "spiped";
+        PermissionsStartOnly = true;
+      };
+
+      preStart  = ''
+        cd /var/lib/spiped
+        chmod -R 0660 *
+        chown -R spiped:spiped *
+      '';
+      scriptArgs = "%i";
+      script = "exec ${pkgs.spiped}/bin/spiped -F `cat /etc/spiped/$1.spec`";
+    };
+
+    system.activationScripts.spiped = optionalString (cfg.config != {})
+      "mkdir -p /var/lib/spiped";
+
+    # Setup spiped config files
+    environment.etc = mapAttrs' (name: cfg: nameValuePair "spiped/${name}.spec"
+      { text = concatStringsSep " "
+          [ (if cfg.encrypt then "-e" else "-d")        # Mode
+            "-s ${cfg.source}"                          # Source
+            "-t ${cfg.target}"                          # Target
+            "-k ${cfg.keyfile}"                         # Keyfile
+            "-n ${toString cfg.maxConns}"               # Max number of conns
+            "-o ${toString cfg.timeout}"                # Timeout
+            (optionalString cfg.waitForDNS "-D")        # Wait for DNS
+            (optionalString cfg.weakHandshake "-f")     # No PFS
+            (optionalString cfg.disableKeepalives "-j") # Keepalives
+            (if cfg.disableReresolution then "-R"
+              else "-r ${toString cfg.resolveRefresh}")
+          ];
+      }) cfg.config;
+  };
+}
diff --git a/nixos/modules/services/networking/squid.nix b/nixos/modules/services/networking/squid.nix
new file mode 100644
index 00000000000..4f3881af8bb
--- /dev/null
+++ b/nixos/modules/services/networking/squid.nix
@@ -0,0 +1,176 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.squid;
+
+
+  squidConfig = pkgs.writeText "squid.conf"
+    (if cfg.configText != null then cfg.configText else
+    ''
+    #
+    # Recommended minimum configuration (3.5):
+    #
+
+    # Example rule allowing access from your local networks.
+    # Adapt to list your (internal) IP networks from where browsing
+    # should be allowed
+    acl localnet src 10.0.0.0/8     # RFC 1918 possible internal network
+    acl localnet src 172.16.0.0/12  # RFC 1918 possible internal network
+    acl localnet src 192.168.0.0/16 # RFC 1918 possible internal network
+    acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines
+    acl localnet src fc00::/7       # RFC 4193 local private network range
+    acl localnet src fe80::/10      # RFC 4291 link-local (directly plugged) machines
+
+    acl SSL_ports port 443          # https
+    acl Safe_ports port 80          # http
+    acl Safe_ports port 21          # ftp
+    acl Safe_ports port 443         # https
+    acl Safe_ports port 70          # gopher
+    acl Safe_ports port 210         # wais
+    acl Safe_ports port 1025-65535  # unregistered ports
+    acl Safe_ports port 280         # http-mgmt
+    acl Safe_ports port 488         # gss-http
+    acl Safe_ports port 591         # filemaker
+    acl Safe_ports port 777         # multiling http
+    acl CONNECT method CONNECT
+
+    #
+    # Recommended minimum Access Permission configuration:
+    #
+    # Deny requests to certain unsafe ports
+    http_access deny !Safe_ports
+
+    # Deny CONNECT to other than secure SSL ports
+    http_access deny CONNECT !SSL_ports
+
+    # Only allow cachemgr access from localhost
+    http_access allow localhost manager
+    http_access deny manager
+
+    # We strongly recommend the following be uncommented to protect innocent
+    # web applications running on the proxy server who think the only
+    # one who can access services on "localhost" is a local user
+    http_access deny to_localhost
+
+    # Application logs to syslog, access and store logs have specific files
+    cache_log       syslog
+    access_log      stdio:/var/log/squid/access.log
+    cache_store_log stdio:/var/log/squid/store.log
+
+    # Required by systemd service
+    pid_filename    /run/squid.pid
+
+    # Run as user and group squid
+    cache_effective_user squid squid
+
+    #
+    # INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS
+    #
+    ${cfg.extraConfig}
+
+    # Example rule allowing access from your local networks.
+    # Adapt localnet in the ACL section to list your (internal) IP networks
+    # from where browsing should be allowed
+    http_access allow localnet
+    http_access allow localhost
+
+    # And finally deny all other access to this proxy
+    http_access deny all
+
+    # Squid normally listens to port 3128
+    http_port ${
+      optionalString (cfg.proxyAddress != null) "${cfg.proxyAddress}:"
+    }${toString cfg.proxyPort}
+
+    # Leave coredumps in the first cache dir
+    coredump_dir /var/cache/squid
+
+    #
+    # Add any of your own refresh_pattern entries above these.
+    #
+    refresh_pattern ^ftp:           1440    20%     10080
+    refresh_pattern ^gopher:        1440    0%      1440
+    refresh_pattern -i (/cgi-bin/|\?) 0     0%      0
+    refresh_pattern .               0       20%     4320
+  '');
+
+in
+
+{
+
+  options = {
+
+    services.squid = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to run squid web proxy.";
+      };
+
+      proxyAddress = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "IP address on which squid will listen.";
+      };
+
+      proxyPort = mkOption {
+        type = types.int;
+        default = 3128;
+        description = "TCP port on which squid will listen.";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Squid configuration. Contents will be added
+          verbatim to the configuration file.
+        '';
+      };
+
+      configText = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = ''
+          Verbatim contents of squid.conf. If null (default), use the
+          autogenerated file from NixOS instead.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users.squid = {
+      isSystemUser = true;
+      group = "squid";
+      home = "/var/cache/squid";
+      createHome = true;
+    };
+
+    users.groups.squid = {};
+
+    systemd.services.squid = {
+      description = "Squid caching web proxy";
+      after = [ "network.target" "nss-lookup.target" ];
+      wantedBy = [ "multi-user.target"];
+      preStart = ''
+        mkdir -p "/var/log/squid"
+        chown squid:squid "/var/log/squid"
+      '';
+      serviceConfig = {
+        Type="forking";
+        PIDFile="/run/squid.pid";
+        ExecStart  = "${pkgs.squid}/bin/squid -YCs -f ${squidConfig}";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/ssh/lshd.nix b/nixos/modules/services/networking/ssh/lshd.nix
new file mode 100644
index 00000000000..862ff7df054
--- /dev/null
+++ b/nixos/modules/services/networking/ssh/lshd.nix
@@ -0,0 +1,189 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inherit (pkgs) lsh;
+
+  cfg = config.services.lshd;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.lshd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the GNU lshd SSH2 daemon, which allows
+          secure remote login.
+        '';
+      };
+
+      portNumber = mkOption {
+        default = 22;
+        type = types.port;
+        description = ''
+          The port on which to listen for connections.
+        '';
+      };
+
+      interfaces = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        description = ''
+          List of network interfaces where listening for connections.
+          When providing the empty list, `[]', lshd listens on all
+          network interfaces.
+        '';
+        example = [ "localhost" "1.2.3.4:443" ];
+      };
+
+      hostKey = mkOption {
+        default = "/etc/lsh/host-key";
+        type = types.str;
+        description = ''
+          Path to the server's private key.  Note that this key must
+          have been created, e.g., using "lsh-keygen --server |
+          lsh-writekey --server", so that you can run lshd.
+        '';
+      };
+
+      syslog = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to enable syslog output.";
+      };
+
+      passwordAuthentication = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to enable password authentication.";
+      };
+
+      publicKeyAuthentication = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to enable public key authentication.";
+      };
+
+      rootLogin = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable remote root login.";
+      };
+
+      loginShell = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description = ''
+          If non-null, override the default login shell with the
+          specified value.
+        '';
+        example = "/nix/store/xyz-bash-10.0/bin/bash10";
+      };
+
+      srpKeyExchange = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to enable SRP key exchange and user authentication.
+        '';
+      };
+
+      tcpForwarding = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to enable TCP/IP forwarding.";
+      };
+
+      x11Forwarding = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to enable X11 forwarding.";
+      };
+
+      subsystems = mkOption {
+        type = types.listOf types.path;
+        description = ''
+          List of subsystem-path pairs, where the head of the pair
+          denotes the subsystem name, and the tail denotes the path to
+          an executable implementing it.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.lshd.subsystems = [ ["sftp" "${pkgs.lsh}/sbin/sftp-server"] ];
+
+    systemd.services.lshd = {
+      description = "GNU lshd SSH2 daemon";
+
+      after = [ "network.target" ];
+
+      wantedBy = [ "multi-user.target" ];
+
+      environment = {
+        LD_LIBRARY_PATH = config.system.nssModules.path;
+      };
+
+      preStart = ''
+        test -d /etc/lsh || mkdir -m 0755 -p /etc/lsh
+        test -d /var/spool/lsh || mkdir -m 0755 -p /var/spool/lsh
+
+        if ! test -f /var/spool/lsh/yarrow-seed-file
+        then
+            # XXX: It would be nice to provide feedback to the
+            # user when this fails, so that they can retry it
+            # manually.
+            ${lsh}/bin/lsh-make-seed --sloppy \
+               -o /var/spool/lsh/yarrow-seed-file
+        fi
+
+        if ! test -f "${cfg.hostKey}"
+        then
+            ${lsh}/bin/lsh-keygen --server | \
+            ${lsh}/bin/lsh-writekey --server -o "${cfg.hostKey}"
+        fi
+      '';
+
+      script = with cfg; ''
+        ${lsh}/sbin/lshd --daemonic \
+          --password-helper="${lsh}/sbin/lsh-pam-checkpw" \
+          -p ${toString portNumber} \
+          ${if interfaces == [] then ""
+            else (concatStrings (map (i: "--interface=\"${i}\"")
+                                     interfaces))} \
+          -h "${hostKey}" \
+          ${if !syslog then "--no-syslog" else ""} \
+          ${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 "" } \
+          ${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" } \
+          --subsystems=${concatStringsSep ","
+                                          (map (pair: (head pair) + "=" +
+                                                      (head (tail pair)))
+                                               subsystems)}
+      '';
+    };
+
+    security.pam.services.lshd = {};
+  };
+}
diff --git a/nixos/modules/services/networking/ssh/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix
new file mode 100644
index 00000000000..230ab673a97
--- /dev/null
+++ b/nixos/modules/services/networking/ssh/sshd.nix
@@ -0,0 +1,571 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  # The splicing information needed for nativeBuildInputs isn't available
+  # on the derivations likely to be used as `cfgc.package`.
+  # This middle-ground solution ensures *an* sshd can do their basic validation
+  # on the configuration.
+  validationPackage = if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform
+    then cfgc.package
+    else pkgs.buildPackages.openssh;
+
+  sshconf = pkgs.runCommand "sshd.conf-validated" { nativeBuildInputs = [ validationPackage ]; } ''
+    cat >$out <<EOL
+    ${cfg.extraConfig}
+    EOL
+
+    ssh-keygen -q -f mock-hostkey -N ""
+    sshd -t -f $out -h mock-hostkey
+  '';
+
+  cfg  = config.services.openssh;
+  cfgc = config.programs.ssh;
+
+  nssModulesPath = config.system.nssModules.path;
+
+  userOptions = {
+
+    options.openssh.authorizedKeys = {
+      keys = mkOption {
+        type = types.listOf types.singleLineStr;
+        default = [];
+        description = ''
+          A list of verbatim OpenSSH public keys that should be added to the
+          user's authorized keys. The keys are added to a file that the SSH
+          daemon reads in addition to the the user's authorized_keys file.
+          You can combine the <literal>keys</literal> and
+          <literal>keyFiles</literal> options.
+          Warning: If you are using <literal>NixOps</literal> then don't use this
+          option since it will replace the key required for deployment via ssh.
+        '';
+        example = [
+          "ssh-rsa AAAAB3NzaC1yc2etc/etc/etcjwrsh8e596z6J0l7 example@host"
+          "ssh-ed25519 AAAAC3NzaCetcetera/etceteraJZMfk3QPfQ foo@bar"
+        ];
+      };
+
+      keyFiles = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          A list of files each containing one OpenSSH public key that should be
+          added to the user's authorized keys. The contents of the files are
+          read at build time and added to a file that the SSH daemon reads in
+          addition to the the user's authorized_keys file. You can combine the
+          <literal>keyFiles</literal> and <literal>keys</literal> options.
+        '';
+      };
+    };
+
+  };
+
+  authKeysFiles = let
+    mkAuthKeyFile = u: nameValuePair "ssh/authorized_keys.d/${u.name}" {
+      mode = "0444";
+      source = pkgs.writeText "${u.name}-authorized_keys" ''
+        ${concatStringsSep "\n" u.openssh.authorizedKeys.keys}
+        ${concatMapStrings (f: readFile f + "\n") u.openssh.authorizedKeys.keyFiles}
+      '';
+    };
+    usersWithKeys = attrValues (flip filterAttrs config.users.users (n: u:
+      length u.openssh.authorizedKeys.keys != 0 || length u.openssh.authorizedKeys.keyFiles != 0
+    ));
+  in listToAttrs (map mkAuthKeyFile usersWithKeys);
+
+in
+
+{
+  imports = [
+    (mkAliasOptionModule [ "services" "sshd" "enable" ] [ "services" "openssh" "enable" ])
+    (mkAliasOptionModule [ "services" "openssh" "knownHosts" ] [ "programs" "ssh" "knownHosts" ])
+    (mkRenamedOptionModule [ "services" "openssh" "challengeResponseAuthentication" ] [ "services" "openssh" "kbdInteractiveAuthentication" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.openssh = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the OpenSSH secure shell daemon, which
+          allows secure remote logins.
+        '';
+      };
+
+      startWhenNeeded = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If set, <command>sshd</command> is socket-activated; that
+          is, instead of having it permanently running as a daemon,
+          systemd will start an instance for each incoming connection.
+        '';
+      };
+
+      forwardX11 = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to allow X11 connections to be forwarded.
+        '';
+      };
+
+      allowSFTP = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable the SFTP subsystem in the SSH daemon.  This
+          enables the use of commands such as <command>sftp</command> and
+          <command>sshfs</command>.
+        '';
+      };
+
+      sftpServerExecutable = mkOption {
+        type = types.str;
+        example = "internal-sftp";
+        description = ''
+          The sftp server executable.  Can be a path or "internal-sftp" to use
+          the sftp server built into the sshd binary.
+        '';
+      };
+
+      sftpFlags = mkOption {
+        type = with types; listOf str;
+        default = [];
+        example = [ "-f AUTHPRIV" "-l INFO" ];
+        description = ''
+          Commandline flags to add to sftp-server.
+        '';
+      };
+
+      permitRootLogin = mkOption {
+        default = "prohibit-password";
+        type = types.enum ["yes" "without-password" "prohibit-password" "forced-commands-only" "no"];
+        description = ''
+          Whether the root user can login using ssh.
+        '';
+      };
+
+      gatewayPorts = mkOption {
+        type = types.str;
+        default = "no";
+        description = ''
+          Specifies whether remote hosts are allowed to connect to
+          ports forwarded for the client.  See
+          <citerefentry><refentrytitle>sshd_config</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry>.
+        '';
+      };
+
+      ports = mkOption {
+        type = types.listOf types.port;
+        default = [22];
+        description = ''
+          Specifies on which ports the SSH daemon listens.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to automatically open the specified ports in the firewall.
+        '';
+      };
+
+      listenAddresses = mkOption {
+        type = with types; listOf (submodule {
+          options = {
+            addr = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = ''
+                Host, IPv4 or IPv6 address to listen to.
+              '';
+            };
+            port = mkOption {
+              type = types.nullOr types.int;
+              default = null;
+              description = ''
+                Port to listen to.
+              '';
+            };
+          };
+        });
+        default = [];
+        example = [ { addr = "192.168.3.1"; port = 22; } { addr = "0.0.0.0"; port = 64022; } ];
+        description = ''
+          List of addresses and ports to listen on (ListenAddress directive
+          in config). If port is not specified for address sshd will listen
+          on all ports specified by <literal>ports</literal> option.
+          NOTE: this will override default listening on all local addresses and port 22.
+          NOTE: setting this option won't automatically enable given ports
+          in firewall configuration.
+        '';
+      };
+
+      passwordAuthentication = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Specifies whether password authentication is allowed.
+        '';
+      };
+
+      kbdInteractiveAuthentication = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Specifies whether keyboard-interactive authentication is allowed.
+        '';
+      };
+
+      hostKeys = mkOption {
+        type = types.listOf types.attrs;
+        default =
+          [ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; }
+            { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; }
+          ];
+        example =
+          [ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; rounds = 100; openSSHFormat = true; }
+            { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; rounds = 100; comment = "key comment"; }
+          ];
+        description = ''
+          NixOS can automatically generate SSH host keys.  This option
+          specifies the path, type and size of each key.  See
+          <citerefentry><refentrytitle>ssh-keygen</refentrytitle>
+          <manvolnum>1</manvolnum></citerefentry> for supported types
+          and sizes.
+        '';
+      };
+
+      banner = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = ''
+          Message to display to the remote user before authentication is allowed.
+        '';
+      };
+
+      authorizedKeysFiles = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Specify the rules for which files to read on the host.
+
+          This is an advanced option. If you're looking to configure user
+          keys, you can generally use <xref linkend="opt-users.users._name_.openssh.authorizedKeys.keys"/>
+          or <xref linkend="opt-users.users._name_.openssh.authorizedKeys.keyFiles"/>.
+
+          These are paths relative to the host root file system or home
+          directories and they are subject to certain token expansion rules.
+          See AuthorizedKeysFile in man sshd_config for details.
+        '';
+      };
+
+      authorizedKeysCommand = mkOption {
+        type = types.str;
+        default = "none";
+        description = ''
+          Specifies a program to be used to look up the user's public
+          keys. The program must be owned by root, not writable by group
+          or others and specified by an absolute path.
+        '';
+      };
+
+      authorizedKeysCommandUser = mkOption {
+        type = types.str;
+        default = "nobody";
+        description = ''
+          Specifies the user under whose account the AuthorizedKeysCommand
+          is run. It is recommended to use a dedicated user that has no
+          other role on the host than running authorized keys commands.
+        '';
+      };
+
+      kexAlgorithms = mkOption {
+        type = types.listOf types.str;
+        default = [
+          "curve25519-sha256"
+          "curve25519-sha256@libssh.org"
+          "diffie-hellman-group-exchange-sha256"
+        ];
+        description = ''
+          Allowed key exchange algorithms
+          </para>
+          <para>
+          Defaults to recommended settings from both
+          <link xlink:href="https://stribika.github.io/2015/01/04/secure-secure-shell.html" />
+          and
+          <link xlink:href="https://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 = ''
+          Allowed ciphers
+          </para>
+          <para>
+          Defaults to recommended settings from both
+          <link xlink:href="https://stribika.github.io/2015/01/04/secure-secure-shell.html" />
+          and
+          <link xlink:href="https://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 = ''
+          Allowed MACs
+          </para>
+          <para>
+          Defaults to recommended settings from both
+          <link xlink:href="https://stribika.github.io/2015/01/04/secure-secure-shell.html" />
+          and
+          <link xlink:href="https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67" />
+        '';
+      };
+
+      logLevel = mkOption {
+        type = types.enum [ "QUIET" "FATAL" "ERROR" "INFO" "VERBOSE" "DEBUG" "DEBUG1" "DEBUG2" "DEBUG3" ];
+        default = "INFO"; # upstream default
+        description = ''
+          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.
+        '';
+      };
+
+      useDns = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          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.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Verbatim contents of <filename>sshd_config</filename>.";
+      };
+
+      moduliFile = mkOption {
+        example = "/etc/my-local-ssh-moduli;";
+        type = types.path;
+        description = ''
+          Path to <literal>moduli</literal> file to install in
+          <literal>/etc/ssh/moduli</literal>. If this option is unset, then
+          the <literal>moduli</literal> file shipped with OpenSSH will be used.
+        '';
+      };
+
+    };
+
+    users.users = mkOption {
+      type = with types; attrsOf (submodule userOptions);
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.sshd =
+      {
+        isSystemUser = true;
+        group = "sshd";
+        description = "SSH privilege separation user";
+      };
+    users.groups.sshd = {};
+
+    services.openssh.moduliFile = mkDefault "${cfgc.package}/etc/ssh/moduli";
+    services.openssh.sftpServerExecutable = mkDefault "${cfgc.package}/libexec/sftp-server";
+
+    environment.etc = authKeysFiles //
+      { "ssh/moduli".source = cfg.moduliFile;
+        "ssh/sshd_config".source = sshconf;
+      };
+
+    systemd =
+      let
+        service =
+          { description = "SSH Daemon";
+            wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
+            after = [ "network.target" ];
+            stopIfChanged = false;
+            path = [ cfgc.package pkgs.gawk ];
+            environment.LD_LIBRARY_PATH = nssModulesPath;
+
+            restartTriggers = optionals (!cfg.startWhenNeeded) [
+              config.environment.etc."ssh/sshd_config".source
+            ];
+
+            preStart =
+              ''
+                # Make sure we don't write to stdout, since in case of
+                # socket activation, it goes to the remote side (#19589).
+                exec >&2
+
+                mkdir -m 0755 -p /etc/ssh
+
+                ${flip concatMapStrings cfg.hostKeys (k: ''
+                  if ! [ -s "${k.path}" ]; then
+                      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 ""} \
+                        -f "${k.path}" \
+                        -N ""
+                  fi
+                '')}
+              '';
+
+            serviceConfig =
+              { ExecStart =
+                  (optionalString cfg.startWhenNeeded "-") +
+                  "${cfgc.package}/bin/sshd " + (optionalString cfg.startWhenNeeded "-i ") +
+                  "-D " +  # don't detach into a daemon process
+                  "-f /etc/ssh/sshd_config";
+                KillMode = "process";
+              } // (if cfg.startWhenNeeded then {
+                StandardInput = "socket";
+                StandardError = "journal";
+              } else {
+                Restart = "always";
+                Type = "simple";
+              });
+
+          };
+      in
+
+      if cfg.startWhenNeeded then {
+
+        sockets.sshd =
+          { description = "SSH Socket";
+            wantedBy = [ "sockets.target" ];
+            socketConfig.ListenStream = if cfg.listenAddresses != [] then
+              map (l: "${l.addr}:${toString (if l.port != null then l.port else 22)}") cfg.listenAddresses
+            else
+              cfg.ports;
+            socketConfig.Accept = true;
+            # Prevent brute-force attacks from shutting down socket
+            socketConfig.TriggerLimitIntervalSec = 0;
+          };
+
+        services."sshd@" = service;
+
+      } else {
+
+        services.sshd = service;
+
+      };
+
+    networking.firewall.allowedTCPPorts = if cfg.openFirewall then cfg.ports else [];
+
+    security.pam.services.sshd =
+      { startSession = true;
+        showMotd = true;
+        unixAuth = cfg.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" ];
+
+    services.openssh.extraConfig = mkOrder 0
+      ''
+        UsePAM yes
+
+        Banner ${if cfg.banner == null then "none" else pkgs.writeText "ssh_banner" cfg.banner}
+
+        AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"}
+        ${concatMapStrings (port: ''
+          Port ${toString port}
+        '') cfg.ports}
+
+        ${concatMapStrings ({ port, addr, ... }: ''
+          ListenAddress ${addr}${if port != null then ":" + toString port else ""}
+        '') 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}
+          AuthorizedKeysCommandUser ${cfg.authorizedKeysCommandUser}
+        ''}
+
+        ${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;
+                    message = "cannot enable X11 forwarding without setting xauth location";}]
+      ++ forEach cfg.listenAddresses ({ addr, ... }: {
+        assertion = addr != null;
+        message = "addr must be specified in each listenAddresses entry";
+      });
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/sslh.nix b/nixos/modules/services/networking/sslh.nix
new file mode 100644
index 00000000000..abe96f60f81
--- /dev/null
+++ b/nixos/modules/services/networking/sslh.nix
@@ -0,0 +1,168 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.sslh;
+  user = "sslh";
+  configFile = pkgs.writeText "sslh.conf" ''
+    verbose: ${boolToString cfg.verbose};
+    foreground: true;
+    inetd: false;
+    numeric: false;
+    transparent: ${boolToString cfg.transparent};
+    timeout: "${toString cfg.timeout}";
+
+    listen:
+    (
+      ${
+        concatMapStringsSep ",\n"
+        (addr: ''{ host: "${addr}"; port: "${toString cfg.port}"; }'')
+        cfg.listenAddresses
+      }
+    );
+
+    ${cfg.appendConfig}
+  '';
+  defaultAppendConfig = ''
+    protocols:
+    (
+      { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; probe: "builtin"; },
+      { name: "openvpn"; host: "localhost"; port: "1194"; probe: "builtin"; },
+      { name: "xmpp"; host: "localhost"; port: "5222"; probe: "builtin"; },
+      { name: "http"; host: "localhost"; port: "80"; probe: "builtin"; },
+      { name: "tls"; host: "localhost"; port: "443"; probe: "builtin"; },
+      { name: "anyprot"; host: "localhost"; port: "443"; probe: "builtin"; }
+    );
+  '';
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "sslh" "listenAddress" ] [ "services" "sslh" "listenAddresses" ])
+  ];
+
+  options = {
+    services.sslh = {
+      enable = mkEnableOption "sslh";
+
+      verbose = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Verbose logs.";
+      };
+
+      timeout = mkOption {
+        type = types.int;
+        default = 2;
+        description = "Timeout in seconds.";
+      };
+
+      transparent = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Will the services behind sslh (Apache, sshd and so on) see the external IP and ports as if the external world connected directly to them";
+      };
+
+      listenAddresses = mkOption {
+        type = types.coercedTo types.str singleton (types.listOf types.str);
+        default = [ "0.0.0.0" "[::]" ];
+        description = "Listening addresses or hostnames.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 443;
+        description = "Listening port.";
+      };
+
+      appendConfig = mkOption {
+        type = types.str;
+        default = defaultAppendConfig;
+        description = "Verbatim configuration file.";
+      };
+    };
+  };
+
+  config = mkMerge [
+    (mkIf cfg.enable {
+      systemd.services.sslh = {
+        description = "Applicative Protocol Multiplexer (e.g. share SSH and HTTPS on the same port)";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = {
+          DynamicUser          = true;
+          User                 = "sslh";
+          PermissionsStartOnly = true;
+          Restart              = "always";
+          RestartSec           = "1s";
+          ExecStart            = "${pkgs.sslh}/bin/sslh -F${configFile}";
+          KillMode             = "process";
+          AmbientCapabilities  = "CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_SETGID CAP_SETUID";
+          PrivateTmp           = true;
+          PrivateDevices       = true;
+          ProtectSystem        = "full";
+          ProtectHome          = true;
+        };
+      };
+    })
+
+    # code from https://github.com/yrutschle/sslh#transparent-proxy-support
+    # the only difference is using iptables mark 0x2 instead of 0x1 to avoid conflicts with nixos/nat module
+    (mkIf (cfg.enable && cfg.transparent) {
+      # Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination
+      boot.kernel.sysctl."net.ipv4.conf.default.route_localnet" = 1;
+      boot.kernel.sysctl."net.ipv4.conf.all.route_localnet"     = 1;
+
+      systemd.services.sslh = let
+        iptablesCommands = [
+          # DROP martian packets as they would have been if route_localnet was zero
+          # Note: packets not leaving the server aren't affected by this, thus sslh will still work
+          { table = "raw";    command = "PREROUTING  ! -i lo -d 127.0.0.0/8 -j DROP"; }
+          { table = "mangle"; command = "POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP"; }
+          # Mark all connections made by ssl for special treatment (here sslh is run as user ${user})
+          { table = "nat";    command = "OUTPUT -m owner --uid-owner ${user} -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x02/0x0f"; }
+          # Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark)
+          { table = "mangle"; command = "OUTPUT ! -o lo -p tcp -m connmark --mark 0x02/0x0f -j CONNMARK --restore-mark --mask 0x0f"; }
+        ];
+        ip6tablesCommands = [
+          { table = "raw";    command = "PREROUTING  ! -i lo -d ::1/128     -j DROP"; }
+          { table = "mangle"; command = "POSTROUTING ! -o lo -s ::1/128     -j DROP"; }
+          { table = "nat";    command = "OUTPUT -m owner --uid-owner ${user} -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x02/0x0f"; }
+          { table = "mangle"; command = "OUTPUT ! -o lo -p tcp -m connmark --mark 0x02/0x0f -j CONNMARK --restore-mark --mask 0x0f"; }
+        ];
+      in {
+        path = [ pkgs.iptables pkgs.iproute2 pkgs.procps ];
+
+        preStart = ''
+          # Cleanup old iptables entries which might be still there
+          ${concatMapStringsSep "\n" ({table, command}: "while iptables -w -t ${table} -D ${command} 2>/dev/null; do echo; done") iptablesCommands}
+          ${concatMapStringsSep "\n" ({table, command}:       "iptables -w -t ${table} -A ${command}"                           ) iptablesCommands}
+
+          # Configure routing for those marked packets
+          ip rule  add fwmark 0x2 lookup 100
+          ip route add local 0.0.0.0/0 dev lo table 100
+
+        '' + optionalString config.networking.enableIPv6 ''
+          ${concatMapStringsSep "\n" ({table, command}: "while ip6tables -w -t ${table} -D ${command} 2>/dev/null; do echo; done") ip6tablesCommands}
+          ${concatMapStringsSep "\n" ({table, command}:       "ip6tables -w -t ${table} -A ${command}"                           ) ip6tablesCommands}
+
+          ip -6 rule  add fwmark 0x2 lookup 100
+          ip -6 route add local ::/0 dev lo table 100
+        '';
+
+        postStop = ''
+          ${concatMapStringsSep "\n" ({table, command}: "iptables -w -t ${table} -D ${command}") iptablesCommands}
+
+          ip rule  del fwmark 0x2 lookup 100
+          ip route del local 0.0.0.0/0 dev lo table 100
+        '' + optionalString config.networking.enableIPv6 ''
+          ${concatMapStringsSep "\n" ({table, command}: "ip6tables -w -t ${table} -D ${command}") ip6tablesCommands}
+
+          ip -6 rule  del fwmark 0x2 lookup 100
+          ip -6 route del local ::/0 dev lo table 100
+        '';
+      };
+    })
+  ];
+}
diff --git a/nixos/modules/services/networking/strongswan-swanctl/module.nix b/nixos/modules/services/networking/strongswan-swanctl/module.nix
new file mode 100644
index 00000000000..9287943fcde
--- /dev/null
+++ b/nixos/modules/services/networking/strongswan-swanctl/module.nix
@@ -0,0 +1,84 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+with (import ./param-lib.nix lib);
+
+let
+  cfg = config.services.strongswan-swanctl;
+  swanctlParams = import ./swanctl-params.nix lib;
+in  {
+  options.services.strongswan-swanctl = {
+    enable = mkEnableOption "strongswan-swanctl service";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.strongswan;
+      defaultText = literalExpression "pkgs.strongswan";
+      description = ''
+        The strongswan derivation to use.
+      '';
+    };
+
+    strongswan.extraConfig = mkOption {
+      type = types.str;
+      default = "";
+      description = ''
+        Contents of the <literal>strongswan.conf</literal> file.
+      '';
+    };
+
+    swanctl = paramsToOptions swanctlParams;
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = !config.services.strongswan.enable;
+        message = "cannot enable both services.strongswan and services.strongswan-swanctl. Choose either one.";
+      }
+    ];
+
+    environment.etc."swanctl/swanctl.conf".text =
+      paramsToConf cfg.swanctl swanctlParams;
+
+    # The swanctl command complains when the following directories don't exist:
+    # See: https://wiki.strongswan.org/projects/strongswan/wiki/Swanctldirectory
+    system.activationScripts.strongswan-swanctl-etc = stringAfter ["etc"] ''
+      mkdir -p '/etc/swanctl/x509'     # Trusted X.509 end entity certificates
+      mkdir -p '/etc/swanctl/x509ca'   # Trusted X.509 Certificate Authority certificates
+      mkdir -p '/etc/swanctl/x509ocsp'
+      mkdir -p '/etc/swanctl/x509aa'   # Trusted X.509 Attribute Authority certificates
+      mkdir -p '/etc/swanctl/x509ac'   # Attribute Certificates
+      mkdir -p '/etc/swanctl/x509crl'  # Certificate Revocation Lists
+      mkdir -p '/etc/swanctl/pubkey'   # Raw public keys
+      mkdir -p '/etc/swanctl/private'  # Private keys in any format
+      mkdir -p '/etc/swanctl/rsa'      # PKCS#1 encoded RSA private keys
+      mkdir -p '/etc/swanctl/ecdsa'    # Plain ECDSA private keys
+      mkdir -p '/etc/swanctl/bliss'
+      mkdir -p '/etc/swanctl/pkcs8'    # PKCS#8 encoded private keys of any type
+      mkdir -p '/etc/swanctl/pkcs12'   # PKCS#12 containers
+    '';
+
+    systemd.services.strongswan-swanctl = {
+      description = "strongSwan IPsec IKEv1/IKEv2 daemon using swanctl";
+      wantedBy = [ "multi-user.target" ];
+      after    = [ "network-online.target" ];
+      path     = with pkgs; [ kmod iproute2 iptables util-linux ];
+      environment = {
+        STRONGSWAN_CONF = pkgs.writeTextFile {
+          name = "strongswan.conf";
+          text = cfg.strongswan.extraConfig;
+        };
+        SWANCTL_DIR = "/etc/swanctl";
+      };
+      restartTriggers = [ config.environment.etc."swanctl/swanctl.conf".source ];
+      serviceConfig = {
+        ExecStart     = "${cfg.package}/sbin/charon-systemd";
+        Type          = "notify";
+        ExecStartPost = "${cfg.package}/sbin/swanctl --load-all --noprompt";
+        ExecReload    = "${cfg.package}/sbin/swanctl --reload";
+        Restart       = "on-abnormal";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/strongswan-swanctl/param-constructors.nix b/nixos/modules/services/networking/strongswan-swanctl/param-constructors.nix
new file mode 100644
index 00000000000..dfdfc50d8ae
--- /dev/null
+++ b/nixos/modules/services/networking/strongswan-swanctl/param-constructors.nix
@@ -0,0 +1,162 @@
+# In the following context a parameter is an attribute set that
+# contains a NixOS option and a render function. It also contains the
+# attribute: '_type = "param"' so we can distinguish it from other
+# sets.
+#
+# The render function is used to convert the value of the option to a
+# snippet of strongswan.conf. Most parameters simply render their
+# value to a string. For example, take the following parameter:
+#
+#   threads = mkIntParam 10 "Threads to use for request handling.";
+#
+# When a users defines the corresponding option as for example:
+#
+#   services.strongswan-swanctl.strongswan.threads = 32;
+#
+# It will get rendered to the following snippet in strongswan.conf:
+#
+#   threads = 32
+#
+# Some parameters however need to be able to change the attribute
+# name. For example, take the following parameter:
+#
+#   id = mkPrefixedAttrsOfParam (mkOptionalStrParam "") "...";
+#
+# A user can define the corresponding option as for example:
+#
+#   id = {
+#     "foo" = "bar";
+#     "baz" = "qux";
+#   };
+#
+# This will get rendered to the following snippet:
+#
+#   foo-id = bar
+#   baz-id = qux
+#
+# For this reason the render function is not simply a function from
+# value -> string but a function from a value to an attribute set:
+# { "${name}" = string }. This allows parameters to change the attribute
+# name like in the previous example.
+
+lib :
+
+with lib;
+with (import ./param-lib.nix lib);
+
+rec {
+  mkParamOfType = type : strongswanDefault : description : {
+    _type = "param";
+    option = mkOption {
+      type = types.nullOr type;
+      default = null;
+      description = documentDefault description strongswanDefault;
+    };
+    render = single toString;
+  };
+
+  documentDefault = description : strongswanDefault :
+    if strongswanDefault == null
+    then description
+    else description + ''
+      </para><para>
+      StrongSwan default: <literal><![CDATA[${builtins.toJSON strongswanDefault}]]></literal>
+    '';
+
+  single = f: name: value: { ${name} = f value; };
+
+  mkStrParam         = mkParamOfType types.str;
+  mkOptionalStrParam = mkStrParam null;
+
+  mkEnumParam = values : mkParamOfType (types.enum values);
+
+  mkIntParam         = mkParamOfType types.int;
+  mkOptionalIntParam = mkIntParam null;
+
+  # We should have floats in Nix...
+  mkFloatParam = mkStrParam;
+
+  # TODO: Check for hex format:
+  mkHexParam         = mkStrParam;
+  mkOptionalHexParam = mkOptionalStrParam;
+
+  # TODO: Check for duration format:
+  mkDurationParam         = mkStrParam;
+  mkOptionalDurationParam = mkOptionalStrParam;
+
+  mkYesNoParam = strongswanDefault : description : {
+    _type = "param";
+    option = mkOption {
+      type = types.nullOr types.bool;
+      default = null;
+      description = documentDefault description strongswanDefault;
+    };
+    render = single (b: if b then "yes" else "no");
+  };
+  yes = true;
+  no  = false;
+
+  mkSpaceSepListParam = mkSepListParam " ";
+  mkCommaSepListParam = mkSepListParam ",";
+
+  mkSepListParam = sep : strongswanDefault : description : {
+    _type = "param";
+    option = mkOption {
+      type = types.nullOr (types.listOf types.str);
+      default = null;
+      description = documentDefault description strongswanDefault;
+    };
+    render = single (value: concatStringsSep sep value);
+  };
+
+  mkAttrsOfParams = params :
+    mkAttrsOf params (types.submodule {options = paramsToOptions params;});
+
+  mkAttrsOfParam = param :
+    mkAttrsOf param param.option.type;
+
+  mkAttrsOf = param : option : description : {
+    _type = "param";
+    option = mkOption {
+      type = types.attrsOf option;
+      default = {};
+      inherit description;
+    };
+    render = single (attrs:
+      (paramsToRenderedStrings attrs
+        (mapAttrs (_n: _v: param) attrs)));
+  };
+
+  mkPrefixedAttrsOfParams = params :
+    mkPrefixedAttrsOf params (types.submodule {options = paramsToOptions params;});
+
+  mkPrefixedAttrsOfParam = param :
+    mkPrefixedAttrsOf param param.option.type;
+
+  mkPrefixedAttrsOf = p : option : description : {
+    _type = "param";
+    option = mkOption {
+      type = types.attrsOf option;
+      default = {};
+      inherit description;
+    };
+    render = prefix: attrs:
+      let prefixedAttrs = mapAttrs' (name: nameValuePair "${prefix}-${name}") attrs;
+      in paramsToRenderedStrings prefixedAttrs
+           (mapAttrs (_n: _v: p) prefixedAttrs);
+  };
+
+  mkPostfixedAttrsOfParams = params : description : {
+    _type = "param";
+    option = mkOption {
+      type = types.attrsOf (types.submodule {options = paramsToOptions params;});
+      default = {};
+      inherit description;
+    };
+    render = postfix: attrs:
+      let postfixedAttrs = mapAttrs' (name: nameValuePair "${name}-${postfix}") attrs;
+      in paramsToRenderedStrings postfixedAttrs
+           (mapAttrs (_n: _v: params) postfixedAttrs);
+  };
+
+}
diff --git a/nixos/modules/services/networking/strongswan-swanctl/param-lib.nix b/nixos/modules/services/networking/strongswan-swanctl/param-lib.nix
new file mode 100644
index 00000000000..2bbb39a7604
--- /dev/null
+++ b/nixos/modules/services/networking/strongswan-swanctl/param-lib.nix
@@ -0,0 +1,82 @@
+lib :
+
+with lib;
+
+rec {
+  paramsToConf = cfg : ps : mkConf 0 (paramsToRenderedStrings cfg ps);
+
+  # mkConf takes an indentation level (which usually starts at 0) and a nested
+  # attribute set of strings and will render that set to a strongswan.conf style
+  # configuration format. For example:
+  #
+  #   mkConf 0 {a = "1"; b = { c = { "foo" = "2"; "bar" = "3"; }; d = "4";};}   =>   ''
+  #   a = 1
+  #   b {
+  #     c {
+  #       foo = 2
+  #       bar = 3
+  #     }
+  #     d = 4
+  #   }''
+  mkConf = indent : ps :
+    concatMapStringsSep "\n"
+      (name:
+        let value = ps.${name};
+            indentation = replicate indent " ";
+        in
+        indentation + (
+          if isAttrs value
+          then "${name} {\n" +
+                 mkConf (indent + 2) value + "\n" +
+               indentation + "}"
+          else "${name} = ${value}"
+        )
+      )
+      (attrNames ps);
+
+  replicate = n : c : concatStrings (builtins.genList (_x : c) n);
+
+  # `paramsToRenderedStrings cfg ps` converts the NixOS configuration `cfg`
+  # (typically the "config" argument of a NixOS module) and the set of
+  # parameters `ps` (an attribute set where the values are constructed using the
+  # parameter constructors in ./param-constructors.nix) to a nested attribute
+  # set of strings (rendered parameters).
+  paramsToRenderedStrings = cfg : ps :
+    filterEmptySets (
+      (mapParamsRecursive (path: name: param:
+        let value = attrByPath path null cfg;
+        in optionalAttrs (value != null) (param.render name value)
+      ) ps));
+
+  filterEmptySets = set : filterAttrs (n: v: (v != null)) (mapAttrs (name: value:
+    if isAttrs value
+    then let value' = filterEmptySets value;
+         in if value' == {}
+            then null
+            else value'
+    else value
+  ) set);
+
+  # Recursively map over every parameter in the given attribute set.
+  mapParamsRecursive = mapAttrsRecursiveCond' (as: (!(as ? _type && as._type == "param")));
+
+  mapAttrsRecursiveCond' = cond: f: set:
+    let
+      recurse = path: set:
+        let
+          g =
+            name: value:
+            if isAttrs value && cond value
+              then { ${name} = recurse (path ++ [name]) value; }
+              else f (path ++ [name]) name value;
+        in mapAttrs'' g set;
+    in recurse [] set;
+
+  mapAttrs'' = f: set:
+    foldl' (a: b: a // b) {} (map (attr: f attr set.${attr}) (attrNames set));
+
+  # Extract the options from the given set of parameters.
+  paramsToOptions = ps :
+    mapParamsRecursive (_path: name: param: { ${name} = param.option; }) ps;
+
+}
diff --git a/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix b/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix
new file mode 100644
index 00000000000..cca61b9ce93
--- /dev/null
+++ b/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix
@@ -0,0 +1,1310 @@
+# See: https://wiki.strongswan.org/projects/strongswan/wiki/Swanctlconf
+#
+# When strongSwan is upgraded please update the parameters in this file. You can
+# see which parameters should be deleted, changed or added by diffing
+# swanctl.opt:
+#
+#   git clone https://github.com/strongswan/strongswan.git
+#   cd strongswan
+#   git diff 5.7.2..5.8.0 src/swanctl/swanctl.opt
+
+lib: with (import ./param-constructors.nix lib);
+
+let
+  certParams = {
+    file = mkOptionalStrParam ''
+      Absolute path to the certificate to load. Passed as-is to the daemon, so
+      it must be readable by it.
+      </para><para>
+      Configure either this or <option>handle</option>, but not both, in one section.
+    '';
+
+    handle = mkOptionalHexParam ''
+      Hex-encoded CKA_ID or handle of the certificate on a token or TPM,
+      respectively.
+      </para><para>
+      Configure either this or <option>file</option>, but not both, in one section.
+    '';
+
+    slot = mkOptionalIntParam ''
+      Optional slot number of the token that stores the certificate.
+    '';
+
+    module = mkOptionalStrParam ''
+      Optional PKCS#11 module name.
+    '';
+  };
+in {
+  authorities = mkAttrsOfParams ({
+
+    cacert = mkOptionalStrParam ''
+      The certificates may use a relative path from the swanctl
+      <literal>x509ca</literal> directory or an absolute path.
+      </para><para>
+      Configure one of <option>cacert</option>,
+      <option>file</option>, or
+      <option>handle</option> per section.
+    '';
+
+    cert_uri_base = mkOptionalStrParam ''
+      Defines the base URI for the Hash and URL feature supported by
+      IKEv2. Instead of exchanging complete certificates, IKEv2 allows one to
+      send an URI that resolves to the DER encoded certificate. The certificate
+      URIs are built by appending the SHA1 hash of the DER encoded certificates
+      to this base URI.
+    '';
+
+    crl_uris = mkCommaSepListParam [] ''
+      List of CRL distribution points (ldap, http, or file URI).
+    '';
+
+    ocsp_uris = mkCommaSepListParam [] ''
+      List of OCSP URIs.
+    '';
+
+  } // certParams) ''
+    Section defining complementary attributes of certification authorities, each
+    in its own subsection with an arbitrary yet unique name
+  '';
+
+  connections = mkAttrsOfParams {
+
+    version = mkIntParam 0 ''
+      IKE major version to use for connection.
+      <itemizedlist>
+      <listitem><para>1 uses IKEv1 aka ISAKMP,</para></listitem>
+      <listitem><para>2 uses IKEv2.</para></listitem>
+      <listitem><para>A connection using the default of 0 accepts both IKEv1 and IKEv2 as
+      responder, and initiates the connection actively with IKEv2.</para></listitem>
+      </itemizedlist>
+    '';
+
+    local_addrs	= mkCommaSepListParam [] ''
+      Local address(es) to use for IKE communication. Takes
+      single IPv4/IPv6 addresses, DNS names, CIDR subnets or IP address ranges.
+      </para><para>
+      As initiator, the first non-range/non-subnet is used to initiate the
+      connection from. As responder, the local destination address must match at
+      least to one of the specified addresses, subnets or ranges.
+      </para><para>
+      If FQDNs are assigned they are resolved every time a configuration lookup
+      is done. If DNS resolution times out, the lookup is delayed for that time.
+    '';
+
+    remote_addrs = mkCommaSepListParam [] ''
+      Remote address(es) to use for IKE communication. Takes
+      single IPv4/IPv6 addresses, DNS names, CIDR subnets or IP address ranges.
+      </para><para>
+      As initiator, the first non-range/non-subnet is used to initiate the
+      connection to. As responder, the initiator source address must match at
+      least to one of the specified addresses, subnets or ranges.
+      </para><para>
+      If FQDNs are assigned they are resolved every time a configuration lookup
+      is done. If DNS resolution times out, the lookup is delayed for that time.
+      To initiate a connection, at least one specific address or DNS name must
+      be specified.
+    '';
+
+    local_port = mkIntParam 500 ''
+      Local UDP port for IKE communication. By default the port of the socket
+      backend is used, which is usually <literal>500</literal>. If port
+      <literal>500</literal> is used, automatic IKE port floating to port
+      <literal>4500</literal> is used to work around NAT issues.
+      </para><para>
+      Using a non-default local IKE port requires support from the socket
+      backend in use (socket-dynamic).
+    '';
+
+    remote_port = mkIntParam 500 ''
+      Remote UDP port for IKE communication. If the default of port
+      <literal>500</literal> is used, automatic IKE port floating to port
+      <literal>4500</literal> is used to work around NAT issues.
+    '';
+
+    proposals = mkCommaSepListParam ["default"] ''
+      A proposal is a set of algorithms. For non-AEAD algorithms, this includes
+      for IKE an encryption algorithm, an integrity algorithm, a pseudo random
+      function and a Diffie-Hellman group. For AEAD algorithms, instead of
+      encryption and integrity algorithms, a combined algorithm is used.
+      </para><para>
+      In IKEv2, multiple algorithms of the same kind can be specified in a
+      single proposal, from which one gets selected. In IKEv1, only one
+      algorithm per kind is allowed per proposal, more algorithms get implicitly
+      stripped. Use multiple proposals to offer different algorithms
+      combinations in IKEv1.
+      </para><para>
+      Algorithm keywords get separated using dashes. Multiple proposals may be
+      specified in a list. The special value <literal>default</literal> forms a
+      default proposal of supported algorithms considered safe, and is usually a
+      good choice for interoperability.
+    '';
+
+    vips = mkCommaSepListParam [] ''
+      List of virtual IPs to request in IKEv2 configuration payloads or IKEv1
+      Mode Config. The wildcard addresses <literal>0.0.0.0</literal> and
+      <literal>::</literal> request an arbitrary address, specific addresses may
+      be defined. The responder may return a different address, though, or none
+      at all.
+    '';
+
+    aggressive = mkYesNoParam no ''
+      Enables Aggressive Mode instead of Main Mode with Identity
+      Protection. Aggressive Mode is considered less secure, because the ID and
+      HASH payloads are exchanged unprotected. This allows a passive attacker to
+      snoop peer identities, and even worse, start dictionary attacks on the
+      Preshared Key.
+    '';
+
+    pull = mkYesNoParam yes ''
+      If the default of yes is used, Mode Config works in pull mode, where the
+      initiator actively requests a virtual IP. With no, push mode is used,
+      where the responder pushes down a virtual IP to the initiating peer.
+      </para><para>
+      Push mode is currently supported for IKEv1, but not in IKEv2. It is used
+      by a few implementations only, pull mode is recommended.
+    '';
+
+    dscp = mkStrParam "000000" ''
+      Differentiated Services Field Codepoint to set on outgoing IKE packets for
+      this connection. The value is a six digit binary encoded string specifying
+      the Codepoint to set, as defined in RFC 2474.
+    '';
+
+    encap = mkYesNoParam no ''
+      To enforce UDP encapsulation of ESP packets, the IKE daemon can fake the
+      NAT detection payloads. This makes the peer believe that NAT takes place
+      on the path, forcing it to encapsulate ESP packets in UDP.
+      </para><para>
+      Usually this is not required, but it can help to work around connectivity
+      issues with too restrictive intermediary firewalls.
+    '';
+
+    mobike = mkYesNoParam yes ''
+      Enables MOBIKE on IKEv2 connections. MOBIKE is enabled by default on IKEv2
+      connections, and allows mobility of clients and multi-homing on servers by
+      migrating active IPsec tunnels.
+      </para><para>
+      Usually keeping MOBIKE enabled is unproblematic, as it is not used if the
+      peer does not indicate support for it. However, due to the design of
+      MOBIKE, IKEv2 always floats to port 4500 starting from the second
+      exchange. Some implementations don't like this behavior, hence it can be
+      disabled.
+    '';
+
+    dpd_delay = mkDurationParam "0s" ''
+      Interval to check the liveness of a peer actively using IKEv2
+      INFORMATIONAL exchanges or IKEv1 R_U_THERE messages. Active DPD checking
+      is only enforced if no IKE or ESP/AH packet has been received for the
+      configured DPD delay.
+    '';
+
+    dpd_timeout = mkDurationParam "0s" ''
+      Charon by default uses the normal retransmission mechanism and timeouts to
+      check the liveness of a peer, as all messages are used for liveness
+      checking. For compatibility reasons, with IKEv1 a custom interval may be
+      specified; this option has no effect on connections using IKEv2.
+    '';
+
+    fragmentation = mkEnumParam ["yes" "accept" "force" "no"] "yes" ''
+      Use IKE fragmentation (proprietary IKEv1 extension or RFC 7383 IKEv2
+      fragmentation). Acceptable values are <literal>yes</literal> (the default
+      since 5.5.1), <literal>accept</literal> (since versions:5.5.3),
+      <literal>force</literal> and <literal>no</literal>.
+      <itemizedlist>
+      <listitem><para>If set to <literal>yes</literal>, and the peer
+      supports it, oversized IKE messages will be sent in fragments.</para></listitem>
+      <listitem><para>If set to
+      <literal>accept</literal>, support for fragmentation is announced to the peer but the daemon
+      does not send its own messages in fragments.</para></listitem>
+      <listitem><para>If set to <literal>force</literal> (only
+      supported for IKEv1) the initial IKE message will already be fragmented if
+      required.</para></listitem>
+      <listitem><para>Finally, setting the option to <literal>no</literal> will disable announcing
+      support for this feature.</para></listitem>
+      </itemizedlist>
+      </para><para>
+      Note that fragmented IKE messages sent by a peer are always processed
+      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 <literal>allow</literal> (the default), <literal>force</literal> and
+      <literal>never</literal>. If set to <literal>allow</literal>, 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
+      <literal>force</literal>, 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 <literal>never</literal> disables support for
+      childless IKE_SAs as responder.
+    '';
+
+    send_certreq = mkYesNoParam yes ''
+      Send certificate request payloads to offer trusted root CA certificates to
+      the peer. Certificate requests help the peer to choose an appropriate
+      certificate/private key for authentication and are enabled by default.
+      Disabling certificate requests can be useful if too many trusted root CA
+      certificates are installed, as each certificate request increases the size
+      of the initial IKE packets.
+   '';
+
+    send_cert = mkEnumParam ["always" "never" "ifasked" ] "ifasked" ''
+      Send certificate payloads when using certificate authentication.
+      <itemizedlist>
+      <listitem><para>With the default of <literal>ifasked</literal> the daemon sends
+      certificate payloads only if certificate requests have been received.</para></listitem>
+      <listitem><para><literal>never</literal> disables sending of certificate payloads
+      altogether,</para></listitem>
+      <listitem><para><literal>always</literal> causes certificate payloads to be sent
+      unconditionally whenever certificate authentication is used.</para></listitem>
+      </itemizedlist>
+    '';
+
+    ppk_id = mkOptionalStrParam ''
+       String identifying the Postquantum Preshared Key (PPK) to be used.
+    '';
+
+    ppk_required = mkYesNoParam no ''
+       Whether a Postquantum Preshared Key (PPK) is required for this connection.
+    '';
+
+    keyingtries = mkIntParam 1 ''
+      Number of retransmission sequences to perform during initial
+      connect. Instead of giving up initiation after the first retransmission
+      sequence with the default value of <literal>1</literal>, additional
+      sequences may be started according to the configured value. A value of
+      <literal>0</literal> initiates a new sequence until the connection
+      establishes or fails with a permanent error.
+    '';
+
+    unique = mkEnumParam ["no" "never" "keep" "replace"] "no" ''
+      Connection uniqueness policy to enforce. To avoid multiple connections
+      from the same user, a uniqueness policy can be enforced.
+      </para><para>
+      <itemizedlist>
+      <listitem><para>
+      The value <literal>never</literal> does never enforce such a policy, even
+      if a peer included INITIAL_CONTACT notification messages,
+      </para></listitem>
+      <listitem><para>
+      whereas <literal>no</literal> replaces existing connections for the same
+      identity if a new one has the INITIAL_CONTACT notify.
+      </para></listitem>
+      <listitem><para>
+      <literal>keep</literal> rejects new connection attempts if the same user
+      already has an active connection,
+      </para></listitem>
+      <listitem><para>
+      <literal>replace</literal> deletes any existing connection if a new one
+      for the same user gets established.
+      </para></listitem>
+      </itemizedlist>
+      To compare connections for uniqueness, the remote IKE identity is used. If
+      EAP or XAuth authentication is involved, the EAP-Identity or XAuth
+      username is used to enforce the uniqueness policy instead.
+      </para><para>
+      On initiators this setting specifies whether an INITIAL_CONTACT notify is
+      sent during IKE_AUTH if no existing connection is found with the remote
+      peer (determined by the identities of the first authentication
+      round). Unless set to <literal>never</literal> the client will send a notify.
+    '';
+
+    reauth_time	= mkDurationParam "0s" ''
+      Time to schedule IKE reauthentication. IKE reauthentication recreates the
+      IKE/ISAKMP SA from scratch and re-evaluates the credentials. In asymmetric
+      configurations (with EAP or configuration payloads) it might not be
+      possible to actively reauthenticate as responder. The IKEv2
+      reauthentication lifetime negotiation can instruct the client to perform
+      reauthentication.
+      </para><para>
+      Reauthentication is disabled by default. Enabling it usually may lead to
+      small connection interruptions, as strongSwan uses a break-before-make
+      policy with IKEv2 to avoid any conflicts with associated tunnel resources.
+    '';
+
+    rekey_time = mkDurationParam "4h" ''
+      IKE rekeying refreshes key material using a Diffie-Hellman exchange, but
+      does not re-check associated credentials. It is supported in IKEv2 only,
+      IKEv1 performs a reauthentication procedure instead.
+      </para><para>
+      With the default value IKE rekeying is scheduled every 4 hours, minus the
+      configured rand_time. If a reauth_time is configured, rekey_time defaults
+      to zero, disabling rekeying; explicitly set both to enforce rekeying and
+      reauthentication.
+    '';
+
+    over_time = mkOptionalDurationParam ''
+      Hard IKE_SA lifetime if rekey/reauth does not complete, as time. To avoid
+      having an IKE/ISAKMP kept alive if IKE reauthentication or rekeying fails
+      perpetually, a maximum hard lifetime may be specified. If the IKE_SA fails
+      to rekey or reauthenticate within the specified time, the IKE_SA gets
+      closed.
+      </para><para>
+      In contrast to CHILD_SA rekeying, over_time is relative in time to the
+      rekey_time and reauth_time values, as it applies to both.
+      </para><para>
+      The default is 10% of the longer of <option>rekey_time</option> and
+      <option>reauth_time</option>.
+    '';
+
+    rand_time = mkOptionalDurationParam ''
+      Time range from which to choose a random value to subtract from
+      rekey/reauth times. To avoid having both peers initiating the rekey/reauth
+      procedure simultaneously, a random time gets subtracted from the
+      rekey/reauth times.
+      </para><para>
+      The default is equal to the configured <option>over_time</option>.
+    '';
+
+    pools = mkCommaSepListParam [] ''
+      List of named IP pools to allocate virtual IP addresses
+      and other configuration attributes from. Each name references a pool by
+      name from either the pools section or an external pool.
+    '';
+
+    if_id_in = mkStrParam "0" ''
+      XFRM interface ID set on inbound policies/SA, can be overridden by child
+      config, see there for details.
+    '';
+
+    if_id_out = mkStrParam "0" ''
+      XFRM interface ID set on outbound policies/SA, can be overridden by child
+      config, see there for details.
+    '';
+
+    mediation = mkYesNoParam no ''
+      Whether this connection is a mediation connection, that is, whether this
+      connection is used to mediate other connections using the IKEv2 Mediation
+      Extension. Mediation connections create no CHILD_SA.
+    '';
+
+    mediated_by = mkOptionalStrParam ''
+      The name of the connection to mediate this connection through. If given,
+      the connection will be mediated through the named mediation
+      connection. The mediation connection must have mediation enabled.
+    '';
+
+    mediation_peer = mkOptionalStrParam ''
+      Identity under which the peer is registered at the mediation server, that
+      is, the IKE identity the other end of this connection uses as its local
+      identity on its connection to the mediation server. This is the identity
+      we request the mediation server to mediate us with. Only relevant on
+      connections that set mediated_by. If it is not given, the remote IKE
+      identity of the first authentication round of this connection will be
+      used.
+    '';
+
+    local = mkPrefixedAttrsOfParams {
+
+      round = mkIntParam 0 ''
+        Optional numeric identifier by which authentication rounds are
+        sorted. If not specified rounds are ordered by their position in the
+        config file/vici message.
+      '';
+
+      certs = mkCommaSepListParam [] ''
+        List of certificate candidates to use for
+        authentication. The certificates may use a relative path from the
+        swanctl <literal>x509</literal> directory or an absolute path.
+        </para><para>
+        The certificate used for authentication is selected based on the
+        received certificate request payloads. If no appropriate CA can be
+        located, the first certificate is used.
+      '';
+
+      cert = mkPostfixedAttrsOfParams certParams ''
+        Section for a certificate candidate to use for
+        authentication. Certificates in certs are transmitted as binary blobs,
+        these sections offer more flexibility.
+      '';
+
+      pubkeys = mkCommaSepListParam [] ''
+        List of raw public key candidates to use for
+        authentication. The public keys may use a relative path from the swanctl
+        <literal>pubkey</literal> directory or an absolute path.
+        </para><para>
+        Even though multiple local public keys could be defined in principle,
+        only the first public key in the list is used for authentication.
+      '';
+
+      auth = mkStrParam "pubkey" ''
+        Authentication to perform locally.
+        <itemizedlist>
+        <listitem><para>
+        The default <literal>pubkey</literal> uses public key authentication
+        using a private key associated to a usable certificate.
+        </para></listitem>
+        <listitem><para>
+        <literal>psk</literal> uses pre-shared key authentication.
+        </para></listitem>
+        <listitem><para>
+        The IKEv1 specific <literal>xauth</literal> is used for XAuth or Hybrid
+        authentication,
+        </para></listitem>
+        <listitem><para>
+        while the IKEv2 specific <literal>eap</literal> keyword defines EAP
+        authentication.
+        </para></listitem>
+        <listitem><para>
+        For <literal>xauth</literal>, a specific backend name may be appended,
+        separated by a dash. The appropriate <literal>xauth</literal> backend is
+        selected to perform the XAuth exchange. For traditional XAuth, the
+        <literal>xauth</literal> method is usually defined in the second
+        authentication round following an initial <literal>pubkey</literal> (or
+        <literal>psk</literal>) round. Using <literal>xauth</literal> in the
+        first round performs Hybrid Mode client authentication.
+        </para></listitem>
+        <listitem><para>
+        For <literal>eap</literal>, a specific EAP method name may be appended, separated by a
+        dash. An EAP module implementing the appropriate method is selected to
+        perform the EAP conversation.
+        </para></listitem>
+        <listitem><para>
+        Since 5.4.0, if both peers support RFC 7427 ("Signature Authentication
+        in IKEv2") specific hash algorithms to be used during IKEv2
+        authentication may be configured. To do so use <literal>ike:</literal>
+        followed by a trust chain signature scheme constraint (see description
+        of the <option>remote</option> section's <option>auth</option>
+        keyword). For example, with <literal>ike:pubkey-sha384-sha256</literal>
+        a public key signature scheme with either SHA-384 or SHA-256 would get
+        used for authentication, in that order and depending on the hash
+        algorithms supported by the peer. If no specific hash algorithms are
+        configured, the default is to prefer an algorithm that matches or
+        exceeds the strength of the signature key. If no constraints with
+        <literal>ike:</literal> prefix are configured any signature scheme
+        constraint (without <literal>ike:</literal> prefix) will also apply to
+        IKEv2 authentication, unless this is disabled in
+        <literal>strongswan.conf</literal>. To use RSASSA-PSS signatures use
+        <literal>rsa/pss</literal> instead of <literal>pubkey</literal> or
+        <literal>rsa</literal> as in e.g.
+        <literal>ike:rsa/pss-sha256</literal>. If <literal>pubkey</literal> or
+        <literal>rsa</literal> constraints are configured RSASSA-PSS signatures
+        will only be used if enabled in <literal>strongswan.conf</literal>(5).
+        </para></listitem>
+        </itemizedlist>
+      '';
+
+      id = mkOptionalStrParam ''
+        IKE identity to use for authentication round. When using certificate
+        authentication, the IKE identity must be contained in the certificate,
+        either as subject or as subjectAltName.
+      '';
+
+      eap_id = mkOptionalStrParam ''
+        Client EAP-Identity to use in EAP-Identity exchange and the EAP method.
+      '';
+
+      aaa_id = mkOptionalStrParam ''
+        Server side EAP-Identity to expect in the EAP method. Some EAP methods,
+        such as EAP-TLS, use an identity for the server to perform mutual
+        authentication. This identity may differ from the IKE identity,
+        especially when EAP authentication is delegated from the IKE responder
+        to an AAA backend.
+        </para><para>
+        For EAP-(T)TLS, this defines the identity for which the server must
+        provide a certificate in the TLS exchange.
+      '';
+
+      xauth_id = mkOptionalStrParam ''
+        Client XAuth username used in the XAuth exchange.
+      '';
+
+    } ''
+      Section for a local authentication round. A local authentication round
+      defines the rules how authentication is performed for the local
+      peer. Multiple rounds may be defined to use IKEv2 RFC 4739 Multiple
+      Authentication or IKEv1 XAuth.
+      </para><para>
+      Each round is defined in a section having <literal>local</literal> as
+      prefix, and an optional unique suffix. To define a single authentication
+      round, the suffix may be omitted.
+    '';
+
+    remote = mkPrefixedAttrsOfParams {
+
+      round = mkIntParam 0 ''
+        Optional numeric identifier by which authentication rounds are
+        sorted. If not specified rounds are ordered by their position in the
+        config file/vici message.
+      '';
+
+      id = mkStrParam "%any" ''
+        IKE identity to expect for authentication round. When using certificate
+        authentication, the IKE identity must be contained in the certificate,
+        either as subject or as subjectAltName.
+      '';
+
+      eap_id = mkOptionalStrParam ''
+        Identity to use as peer identity during EAP authentication. If set to
+        <literal>%any</literal> the EAP-Identity method will be used to ask the
+        client for an EAP identity.
+      '';
+
+      groups = mkCommaSepListParam [] ''
+        Authorization group memberships to require. The peer
+        must prove membership to at least one of the specified groups. Group
+        membership can be certified by different means, for example by
+        appropriate Attribute Certificates or by an AAA backend involved in the
+        authentication.
+      '';
+
+      cert_policy = mkCommaSepListParam [] ''
+        List of certificate policy OIDs the peer's certificate
+        must have. OIDs are specified using the numerical dotted representation.
+      '';
+
+      certs = mkCommaSepListParam [] ''
+        List of certificates to accept for authentication. The certificates may
+        use a relative path from the swanctl <literal>x509</literal> directory
+        or an absolute path.
+      '';
+
+      cert = mkPostfixedAttrsOfParams certParams ''
+        Section for a certificate candidate to use for
+        authentication. Certificates in certs are transmitted as binary blobs,
+        these sections offer more flexibility.
+      '';
+
+      ca_id = mkOptionalStrParam ''
+        Identity in CA certificate to accept for authentication. The specified
+        identity must be contained in one (intermediate) CA of the remote peer
+        trustchain, either as subject or as subjectAltName. This has the same
+        effect as specifying <literal>cacerts</literal> to force clients under
+        a CA to specific connections; it does not require the CA certificate
+        to be available locally, and can be received from the peer during the
+        IKE exchange.
+      '';
+
+      cacerts = mkCommaSepListParam [] ''
+        List of CA certificates to accept for
+        authentication. The certificates may use a relative path from the
+        swanctl <literal>x509ca</literal> directory or an absolute path.
+      '';
+
+      cacert = mkPostfixedAttrsOfParams certParams ''
+        Section for a CA certificate to accept for authentication. Certificates
+        in cacerts are transmitted as binary blobs, these sections offer more
+        flexibility.
+      '';
+
+      pubkeys = mkCommaSepListParam [] ''
+        List of raw public keys to accept for
+        authentication. The public keys may use a relative path from the swanctl
+        <literal>pubkey</literal> directory or an absolute path.
+      '';
+
+      revocation = mkEnumParam ["strict" "ifuri" "relaxed"] "relaxed" ''
+        Certificate revocation policy for CRL or OCSP revocation.
+        <itemizedlist>
+        <listitem><para>
+        A <literal>strict</literal> revocation policy fails if no revocation information is
+        available, i.e. the certificate is not known to be unrevoked.
+        </para></listitem>
+        <listitem><para>
+        <literal>ifuri</literal> fails only if a CRL/OCSP URI is available, but certificate
+        revocation checking fails, i.e. there should be revocation information
+        available, but it could not be obtained.
+        </para></listitem>
+        <listitem><para>
+        The default revocation policy <literal>relaxed</literal> fails only if a certificate is
+        revoked, i.e. it is explicitly known that it is bad.
+        </para></listitem>
+        </itemizedlist>
+      '';
+
+      auth = mkStrParam "pubkey" ''
+        Authentication to expect from remote. See the <option>local</option>
+        section's <option>auth</option> keyword description about the details of
+        supported mechanisms.
+        </para><para>
+        Since 5.4.0, to require a trustchain public key strength for the remote
+        side, specify the key type followed by the minimum strength in bits (for
+        example <literal>ecdsa-384</literal> or
+        <literal>rsa-2048-ecdsa-256</literal>). To limit the acceptable set of
+        hashing algorithms for trustchain validation, append hash algorithms to
+        pubkey or a key strength definition (for example
+        <literal>pubkey-sha256-sha512</literal>,
+        <literal>rsa-2048-sha256-sha384-sha512</literal> or
+        <literal>rsa-2048-sha256-ecdsa-256-sha256-sha384</literal>).
+        Unless disabled in <literal>strongswan.conf</literal>, or explicit IKEv2
+        signature constraints are configured (refer to the description of the
+        <option>local</option> section's <option>auth</option> keyword for
+        details), such key types and hash algorithms are also applied as
+        constraints against IKEv2 signature authentication schemes used by the
+        remote side. To require RSASSA-PSS signatures use
+        <literal>rsa/pss</literal> instead of <literal>pubkey</literal> or
+        <literal>rsa</literal> as in e.g. <literal>rsa/pss-sha256</literal>. If
+        <literal>pubkey</literal> or <literal>rsa</literal> constraints are
+        configured RSASSA-PSS signatures will only be accepted if enabled in
+        <literal>strongswan.conf</literal>(5).
+        </para><para>
+        To specify trust chain constraints for EAP-(T)TLS, append a colon to the
+        EAP method, followed by the key type/size and hash algorithm as
+        discussed above (e.g. <literal>eap-tls:ecdsa-384-sha384</literal>).
+      '';
+
+    } ''
+      Section for a remote authentication round. A remote authentication round
+      defines the constraints how the peers must authenticate to use this
+      connection. Multiple rounds may be defined to use IKEv2 RFC 4739 Multiple
+      Authentication or IKEv1 XAuth.
+      </para><para>
+      Each round is defined in a section having <literal>remote</literal> as
+      prefix, and an optional unique suffix. To define a single authentication
+      round, the suffix may be omitted.
+    '';
+
+    children = mkAttrsOfParams {
+      ah_proposals = mkCommaSepListParam [] ''
+        AH proposals to offer for the CHILD_SA. A proposal is a set of
+        algorithms. For AH, this includes an integrity algorithm and an optional
+        Diffie-Hellman group. If a DH group is specified, CHILD_SA/Quick Mode
+        rekeying and initial negotiation uses a separate Diffie-Hellman exchange
+        using the specified group (refer to esp_proposals for details).
+        </para><para>
+        In IKEv2, multiple algorithms of the same kind can be specified in a
+        single proposal, from which one gets selected. In IKEv1, only one
+        algorithm per kind is allowed per proposal, more algorithms get
+        implicitly stripped. Use multiple proposals to offer different algorithms
+        combinations in IKEv1.
+        </para><para>
+        Algorithm keywords get separated using dashes. Multiple proposals may be
+        specified in a list. The special value <literal>default</literal> forms
+        a default proposal of supported algorithms considered safe, and is
+        usually a good choice for interoperability. By default no AH proposals
+        are included, instead ESP is proposed.
+     '';
+
+      esp_proposals = mkCommaSepListParam ["default"] ''
+        ESP proposals to offer for the CHILD_SA. A proposal is a set of
+        algorithms. For ESP non-AEAD proposals, this includes an integrity
+        algorithm, an encryption algorithm, an optional Diffie-Hellman group and
+        an optional Extended Sequence Number Mode indicator. For AEAD proposals,
+        a combined mode algorithm is used instead of the separate
+        encryption/integrity algorithms.
+        </para><para>
+        If a DH group is specified, CHILD_SA/Quick Mode rekeying and initial
+        negotiation use a separate Diffie-Hellman exchange using the specified
+        group. However, for IKEv2, the keys of the CHILD_SA created implicitly
+        with the IKE_SA will always be derived from the IKE_SA's key material. So
+        any DH group specified here will only apply when the CHILD_SA is later
+        rekeyed or is created with a separate CREATE_CHILD_SA exchange. A
+        proposal mismatch might, therefore, not immediately be noticed when the
+        SA is established, but may later cause rekeying to fail.
+        </para><para>
+        Extended Sequence Number support may be indicated with the
+        <literal>esn</literal> and <literal>noesn</literal> values, both may be
+        included to indicate support for both modes. If omitted,
+        <literal>noesn</literal> is assumed.
+        </para><para>
+        In IKEv2, multiple algorithms of the same kind can be specified in a
+        single proposal, from which one gets selected. In IKEv1, only one
+        algorithm per kind is allowed per proposal, more algorithms get
+        implicitly stripped. Use multiple proposals to offer different algorithms
+        combinations in IKEv1.
+        </para><para>
+        Algorithm keywords get separated using dashes. Multiple proposals may be
+        specified as a list. The special value <literal>default</literal> forms
+        a default proposal of supported algorithms considered safe, and is
+        usually a good choice for interoperability. If no algorithms are
+        specified for AH nor ESP, the default set of algorithms for ESP is
+        included.
+      '';
+
+      sha256_96 = mkYesNoParam no ''
+        HMAC-SHA-256 is used with 128-bit truncation with IPsec. For
+        compatibility with implementations that incorrectly use 96-bit truncation
+        this option may be enabled to configure the shorter truncation length in
+        the kernel. This is not negotiated, so this only works with peers that
+        use the incorrect truncation length (or have this option enabled).
+      '';
+
+      local_ts = mkCommaSepListParam ["dynamic"] ''
+        List of local traffic selectors to include in CHILD_SA. Each selector is
+        a CIDR subnet definition, followed by an optional proto/port
+        selector. The special value <literal>dynamic</literal> may be used
+        instead of a subnet definition, which gets replaced by the tunnel outer
+        address or the virtual IP, if negotiated. This is the default.
+        </para><para>
+        A protocol/port selector is surrounded by opening and closing square
+        brackets. Between these brackets, a numeric or getservent(3) protocol
+        name may be specified. After the optional protocol restriction, an
+        optional port restriction may be specified, separated by a slash. The
+        port restriction may be numeric, a getservent(3) service name, or the
+        special value <literal>opaque</literal> for RFC 4301 OPAQUE
+        selectors. Port ranges may be specified as well, none of the kernel
+        backends currently support port ranges, though.
+        </para><para>
+        When IKEv1 is used only the first selector is interpreted, except if the
+        Cisco Unity extension plugin is used. This is due to a limitation of the
+        IKEv1 protocol, which only allows a single pair of selectors per
+        CHILD_SA. So to tunnel traffic matched by several pairs of selectors when
+        using IKEv1 several children (CHILD_SAs) have to be defined that cover
+        the selectors.  The IKE daemon uses traffic selector narrowing for IKEv1,
+        the same way it is standardized and implemented for IKEv2. However, this
+        may lead to problems with other implementations. To avoid that, configure
+        identical selectors in such scenarios.
+      '';
+
+      remote_ts = mkCommaSepListParam ["dynamic"] ''
+        List of remote selectors to include in CHILD_SA. See
+        <option>local_ts</option> for a description of the selector syntax.
+      '';
+
+      rekey_time = mkDurationParam "1h" ''
+        Time to schedule CHILD_SA rekeying. CHILD_SA rekeying refreshes key
+        material, optionally using a Diffie-Hellman exchange if a group is
+        specified in the proposal.  To avoid rekey collisions initiated by both
+        ends simultaneously, a value in the range of <option>rand_time</option>
+        gets subtracted to form the effective soft lifetime.
+        </para><para>
+        By default CHILD_SA rekeying is scheduled every hour, minus
+        <option>rand_time</option>.
+      '';
+
+      life_time = mkOptionalDurationParam ''
+        Maximum lifetime before CHILD_SA gets closed. Usually this hard lifetime
+        is never reached, because the CHILD_SA gets rekeyed before. If that fails
+        for whatever reason, this limit closes the CHILD_SA.  The default is 10%
+        more than the <option>rekey_time</option>.
+      '';
+
+      rand_time = mkOptionalDurationParam ''
+        Time range from which to choose a random value to subtract from
+        <option>rekey_time</option>. The default is the difference between
+        <option>life_time</option> and <option>rekey_time</option>.
+      '';
+
+      rekey_bytes = mkIntParam 0 ''
+        Number of bytes processed before initiating CHILD_SA rekeying. CHILD_SA
+        rekeying refreshes key material, optionally using a Diffie-Hellman
+        exchange if a group is specified in the proposal.
+        </para><para>
+        To avoid rekey collisions initiated by both ends simultaneously, a value
+        in the range of <option>rand_bytes</option> gets subtracted to form the
+        effective soft volume limit.
+        </para><para>
+        Volume based CHILD_SA rekeying is disabled by default.
+      '';
+
+      life_bytes = mkOptionalIntParam ''
+        Maximum bytes processed before CHILD_SA gets closed. Usually this hard
+        volume limit is never reached, because the CHILD_SA gets rekeyed
+        before. If that fails for whatever reason, this limit closes the
+        CHILD_SA.  The default is 10% more than <option>rekey_bytes</option>.
+      '';
+
+      rand_bytes = mkOptionalIntParam ''
+        Byte range from which to choose a random value to subtract from
+        <option>rekey_bytes</option>. The default is the difference between
+        <option>life_bytes</option> and <option>rekey_bytes</option>.
+      '';
+
+      rekey_packets = mkIntParam 0 ''
+        Number of packets processed before initiating CHILD_SA rekeying. CHILD_SA
+        rekeying refreshes key material, optionally using a Diffie-Hellman
+        exchange if a group is specified in the proposal.
+        </para><para>
+        To avoid rekey collisions initiated by both ends simultaneously, a value
+        in the range of <option>rand_packets</option> gets subtracted to form
+        the effective soft packet count limit.
+        </para><para>
+        Packet count based CHILD_SA rekeying is disabled by default.
+      '';
+
+      life_packets = mkOptionalIntParam ''
+        Maximum number of packets processed before CHILD_SA gets closed. Usually
+        this hard packets limit is never reached, because the CHILD_SA gets
+        rekeyed before. If that fails for whatever reason, this limit closes the
+        CHILD_SA.
+        </para><para>
+        The default is 10% more than <option>rekey_bytes</option>.
+      '';
+
+      rand_packets = mkOptionalIntParam ''
+        Packet range from which to choose a random value to subtract from
+        <option>rekey_packets</option>. The default is the difference between
+        <option>life_packets</option> and <option>rekey_packets</option>.
+      '';
+
+      updown = mkOptionalStrParam ''
+        Updown script to invoke on CHILD_SA up and down events.
+      '';
+
+      hostaccess = mkYesNoParam no ''
+        Hostaccess variable to pass to <literal>updown</literal> script.
+      '';
+
+      mode = mkEnumParam [ "tunnel"
+                           "transport"
+                           "transport_proxy"
+                           "beet"
+                           "pass"
+                           "drop"
+                         ] "tunnel" ''
+        IPsec Mode to establish CHILD_SA with.
+        <itemizedlist>
+        <listitem><para>
+        <literal>tunnel</literal> negotiates the CHILD_SA in IPsec Tunnel Mode,
+        </para></listitem>
+        <listitem><para>
+        whereas <literal>transport</literal> uses IPsec Transport Mode.
+        </para></listitem>
+        <listitem><para>
+        <literal>transport_proxy</literal> signifying the special Mobile IPv6
+        Transport Proxy Mode.
+        </para></listitem>
+        <listitem><para>
+        <literal>beet</literal> is the Bound End to End Tunnel mixture mode,
+        working with fixed inner addresses without the need to include them in
+        each packet.
+        </para></listitem>
+        <listitem><para>
+        Both <literal>transport</literal> and <literal>beet</literal> modes are
+        subject to mode negotiation; <literal>tunnel</literal> mode is
+        negotiated if the preferred mode is not available.
+        </para></listitem>
+        <listitem><para>
+        <literal>pass</literal> and <literal>drop</literal> are used to install
+        shunt policies which explicitly bypass the defined traffic from IPsec
+        processing or drop it, respectively.
+        </para></listitem>
+        </itemizedlist>
+      '';
+
+      policies = mkYesNoParam yes ''
+        Whether to install IPsec policies or not. Disabling this can be useful in
+        some scenarios e.g. MIPv6, where policies are not managed by the IKE
+        daemon. Since 5.3.3.
+      '';
+
+      policies_fwd_out = mkYesNoParam no ''
+        Whether to install outbound FWD IPsec policies or not. Enabling this is
+        required in case there is a drop policy that would match and block
+        forwarded traffic for this CHILD_SA. Since 5.5.1.
+      '';
+
+      dpd_action = mkEnumParam ["clear" "trap" "restart"] "clear" ''
+        Action to perform for this CHILD_SA on DPD timeout. The default clear
+        closes the CHILD_SA and does not take further action. trap installs a
+        trap policy, which will catch matching traffic and tries to re-negotiate
+        the tunnel on-demand. restart immediately tries to re-negotiate the
+        CHILD_SA under a fresh IKE_SA.
+      '';
+
+      ipcomp = mkYesNoParam no ''
+        Enable IPComp compression before encryption. If enabled, IKE tries to
+        negotiate IPComp compression to compress ESP payload data prior to
+        encryption.
+      '';
+
+      inactivity = mkDurationParam "0s" ''
+        Timeout before closing CHILD_SA after inactivity. If no traffic has been
+        processed in either direction for the configured timeout, the CHILD_SA
+        gets closed due to inactivity. The default value of 0 disables inactivity
+        checks.
+      '';
+
+      reqid = mkIntParam 0 ''
+        Fixed reqid to use for this CHILD_SA. This might be helpful in some
+        scenarios, but works only if each CHILD_SA configuration is instantiated
+        not more than once. The default of 0 uses dynamic reqids, allocated
+        incrementally.
+      '';
+
+      priority = mkIntParam 0 ''
+        Optional fixed priority for IPsec policies. This could be useful to
+        install high-priority drop policies. The default of 0 uses dynamically
+        calculated priorities based on the size of the traffic selectors.
+      '';
+
+      interface = mkOptionalStrParam ''
+        Optional interface name to restrict outbound IPsec policies.
+      '';
+
+      mark_in = mkStrParam "0/0x00000000" ''
+        Netfilter mark and mask for input traffic. On Linux, Netfilter may
+        require marks on each packet to match an SA/policy having that option
+        set. This allows installing duplicate policies and enables Netfilter
+        rules to select specific SAs/policies for incoming traffic. Note that
+        inbound marks are only set on policies, by default, unless
+        <option>mark_in_sa</option> is enabled. The special value
+        <literal>%unique</literal> sets a unique mark on each CHILD_SA instance,
+        beyond that the value <literal>%unique-dir</literal> assigns a different
+        unique mark for each
+        </para><para>
+        An additional mask may be appended to the mark, separated by
+        <literal>/</literal>. The default mask if omitted is
+        <literal>0xffffffff</literal>.
+      '';
+
+      mark_in_sa = mkYesNoParam no ''
+        Whether to set <option>mark_in</option> on the inbound SA. By default,
+        the inbound mark is only set on the inbound policy. The tuple destination
+        address, protocol and SPI is unique and the mark is not required to find
+        the correct SA, allowing to mark traffic after decryption instead (where
+        more specific selectors may be used) to match different policies. Marking
+        packets before decryption is still possible, even if no mark is set on
+        the SA.
+      '';
+
+      mark_out = mkStrParam "0/0x00000000" ''
+        Netfilter mark and mask for output traffic. On Linux, Netfilter may
+        require marks on each packet to match a policy/SA having that option
+        set. This allows installing duplicate policies and enables Netfilter
+        rules to select specific policies/SAs for outgoing traffic. The special
+        value <literal>%unique</literal> sets a unique mark on each CHILD_SA
+        instance, beyond that the value <literal>%unique-dir</literal> assigns a
+        different unique mark for each CHILD_SA direction (in/out).
+        </para><para>
+        An additional mask may be appended to the mark, separated by
+        <literal>/</literal>. The default mask if omitted is
+        <literal>0xffffffff</literal>.
+      '';
+
+      set_mark_in = mkStrParam "0/0x00000000" ''
+        Netfilter mark applied to packets after the inbound IPsec SA processed
+        them. This way it's not necessary to mark packets via Netfilter before
+        decryption or right afterwards to match policies or process them
+        differently (e.g. via policy routing).
+
+        An additional mask may be appended to the mark, separated by
+        <literal>/</literal>. The default mask if omitted is 0xffffffff. The
+        special value <literal>%same</literal> uses the value (but not the mask)
+        from <option>mark_in</option> as mark value, which can be fixed,
+        <literal>%unique</literal> or <literal>%unique-dir</literal>.
+
+        Setting marks in XFRM input requires Linux 4.19 or higher.
+      '';
+
+      set_mark_out = mkStrParam "0/0x00000000" ''
+        Netfilter mark applied to packets after the outbound IPsec SA processed
+        them. This allows processing ESP packets differently than the original
+        traffic (e.g. via policy routing).
+
+        An additional mask may be appended to the mark, separated by
+        <literal>/</literal>. The default mask if omitted is 0xffffffff. The
+        special value <literal>%same</literal> uses the value (but not the mask)
+        from <option>mark_out</option> as mark value, which can be fixed,
+        <literal>%unique_</literal> or <literal>%unique-dir</literal>.
+
+        Setting marks in XFRM output is supported since Linux 4.14. Setting a
+        mask requires at least Linux 4.19.
+      '';
+
+      if_id_in = mkStrParam "0" ''
+        XFRM interface ID set on inbound policies/SA. This allows installing
+        duplicate policies/SAs and associates them with an interface with the
+        same ID. The special value <literal>%unique</literal> sets a unique
+        interface ID on each CHILD_SA instance, beyond that the value
+        <literal>%unique-dir</literal> assigns a different unique interface ID
+        for each CHILD_SA direction (in/out).
+      '';
+
+      if_id_out = mkStrParam "0" ''
+        XFRM interface ID set on outbound policies/SA. This allows installing
+        duplicate policies/SAs and associates them with an interface with the
+        same ID. The special value <literal>%unique</literal> sets a unique
+        interface ID on each CHILD_SA instance, beyond that the value
+        <literal>%unique-dir</literal> assigns a different unique interface ID
+        for each CHILD_SA direction (in/out).
+
+        The daemon will not install routes for CHILD_SAs that have this option set.
+     '';
+
+      tfc_padding = mkParamOfType (with lib.types; either int (enum ["mtu"])) 0 ''
+        Pads ESP packets with additional data to have a consistent ESP packet
+        size for improved Traffic Flow Confidentiality. The padding defines the
+        minimum size of all ESP packets sent.  The default value of
+        <literal>0</literal> disables TFC padding, the special value
+        <literal>mtu</literal> adds TFC padding to create a packet size equal to
+        the Path Maximum Transfer Unit.
+      '';
+
+      replay_window = mkIntParam 32 ''
+        IPsec replay window to configure for this CHILD_SA. Larger values than
+        the default of <literal>32</literal> are supported using the Netlink
+        backend only, a value of <literal>0</literal> disables IPsec replay
+        protection.
+      '';
+
+      hw_offload = mkEnumParam ["yes" "no" "auto"] "no" ''
+        Enable hardware offload for this CHILD_SA, if supported by the IPsec
+        implementation. The value <literal>yes</literal> enforces offloading
+        and the installation will fail if it's not supported by either kernel or
+        device. The value <literal>auto</literal> enables offloading, if it's
+        supported, but the installation does not fail otherwise.
+      '';
+
+      copy_df = mkYesNoParam yes ''
+        Whether to copy the DF bit to the outer IPv4 header in tunnel mode. This
+        effectively disables Path MTU discovery (PMTUD). Controlling this
+        behavior is not supported by all kernel interfaces.
+      '';
+
+      copy_ecn = mkYesNoParam yes ''
+        Whether to copy the ECN (Explicit Congestion Notification) header field
+        to/from the outer IP header in tunnel mode. Controlling this behavior is
+        not supported by all kernel interfaces.
+      '';
+
+      copy_dscp = mkEnumParam [ "out" "in" "yes" "no" ] "out" ''
+        Whether to copy the DSCP (Differentiated Services Field Codepoint)
+        header field to/from the outer IP header in tunnel mode. The value
+        <literal>out</literal> only copies the field from the inner to the outer
+        header, the value <literal>in</literal> does the opposite and only
+        copies the field from the outer to the inner header when decapsulating,
+        the value <literal>yes</literal> copies the field in both directions,
+        and the value <literal>no</literal> disables copying the field
+        altogether. Setting this to <literal>yes</literal> or
+        <literal>in</literal> could allow an attacker to adversely affect other
+        traffic at the receiver, which is why the default is
+        <literal>out</literal>. Controlling this behavior is not supported by
+        all kernel interfaces.
+      '';
+
+      start_action = mkEnumParam ["none" "trap" "start"] "none" ''
+        Action to perform after loading the configuration.
+        <itemizedlist>
+        <listitem><para>
+        The default of <literal>none</literal> loads the connection only, which
+        then can be manually initiated or used as a responder configuration.
+        </para></listitem>
+        <listitem><para>
+        The value <literal>trap</literal> installs a trap policy, which triggers
+        the tunnel as soon as matching traffic has been detected.
+        </para></listitem>
+        <listitem><para>
+        The value <literal>start</literal> initiates the connection actively.
+        </para></listitem>
+        </itemizedlist>
+        When unloading or replacing a CHILD_SA configuration having a
+        <option>start_action</option> different from <literal>none</literal>,
+        the inverse action is performed. Configurations with
+        <literal>start</literal> get closed, while such with
+        <literal>trap</literal> get uninstalled.
+      '';
+
+      close_action = mkEnumParam ["none" "trap" "start"] "none" ''
+        Action to perform after a CHILD_SA gets closed by the peer.
+        <itemizedlist>
+        <listitem><para>
+        The default of <literal>none</literal> does not take any action,
+        </para></listitem>
+        <listitem><para>
+        <literal>trap</literal> installs a trap policy for the CHILD_SA.
+        </para></listitem>
+        <listitem><para>
+        <literal>start</literal> tries to re-create the CHILD_SA.
+        </para></listitem>
+        </itemizedlist>
+        </para><para>
+        <option>close_action</option> does not provide any guarantee that the
+        CHILD_SA is kept alive. It acts on explicit close messages only, but not
+        on negotiation failures. Use trap policies to reliably re-create failed
+        CHILD_SAs.
+      '';
+
+    } ''
+      CHILD_SA configuration sub-section. Each connection definition may have
+      one or more sections in its <option>children</option> subsection. The
+      section name defines the name of the CHILD_SA configuration, which must be
+      unique within the connection (denoted &#60;child&#62; below).
+    '';
+  } ''
+    Section defining IKE connection configurations, each in its own subsection
+    with an arbitrary yet unique name
+  '';
+
+  secrets = let
+    mkEapXauthParams = mkPrefixedAttrsOfParams {
+      secret = mkOptionalStrParam ''
+        Value of the EAP/XAuth secret. It may either be an ASCII string, a hex
+        encoded string if it has a 0x prefix or a Base64 encoded string if it
+        has a 0s prefix in its value.
+      '';
+
+      id = mkPrefixedAttrsOfParam (mkOptionalStrParam "") ''
+        Identity the EAP/XAuth secret belongs to. Multiple unique identities may
+        be specified, each having an <literal>id</literal> prefix, if a secret
+        is shared between multiple users.
+      '';
+
+    } ''
+      EAP secret section for a specific secret. Each EAP secret is defined in a
+      unique section having the <literal>eap</literal> prefix. EAP secrets are
+      used for XAuth authentication as well.
+    '';
+
+  in {
+
+    eap   = mkEapXauthParams;
+    xauth = mkEapXauthParams;
+
+    ntlm = mkPrefixedAttrsOfParams {
+      secret = mkOptionalStrParam ''
+        Value of the NTLM secret, which is the NT Hash of the actual secret,
+        that is, MD4(UTF-16LE(secret)). The resulting 16-byte value may either
+        be given as a hex encoded string with a 0x prefix or as a Base64 encoded
+        string with a 0s prefix.
+      '';
+
+      id = mkPrefixedAttrsOfParam (mkOptionalStrParam "") ''
+        Identity the NTLM secret belongs to. Multiple unique identities may be
+        specified, each having an id prefix, if a secret is shared between
+        multiple users.
+      '';
+    } ''
+      NTLM secret section for a specific secret. Each NTLM secret is defined in
+      a unique section having the <literal>ntlm</literal> prefix. NTLM secrets
+      may only be used for EAP-MSCHAPv2 authentication.
+    '';
+
+    ike = mkPrefixedAttrsOfParams {
+      secret = mkOptionalStrParam ''
+        Value of the IKE preshared secret. It may either be an ASCII string, a
+        hex encoded string if it has a 0x prefix or a Base64 encoded string if
+        it has a 0s prefix in its value.
+      '';
+
+      id = mkPrefixedAttrsOfParam (mkOptionalStrParam "") ''
+        IKE identity the IKE preshared secret belongs to. Multiple unique
+        identities may be specified, each having an <literal>id</literal>
+        prefix, if a secret is shared between multiple peers.
+      '';
+    } ''
+      IKE preshared secret section for a specific secret. Each IKE PSK is
+      defined in a unique section having the <literal>ike</literal> prefix.
+    '';
+
+    ppk = mkPrefixedAttrsOfParams {
+      secret = mkOptionalStrParam ''
+        Value of the PPK. It may either be an ASCII string, a hex encoded string
+        if it has a <literal>0x</literal> prefix or a Base64 encoded string if
+        it has a <literal>0s</literal> prefix in its value. Should have at least
+        256 bits of entropy for 128-bit security.
+      '';
+
+      id = mkPrefixedAttrsOfParam (mkOptionalStrParam "") ''
+        PPK identity the PPK belongs to. Multiple unique identities may be
+        specified, each having an <literal>id</literal> prefix, if a secret is
+        shared between multiple peers.
+      '';
+    } ''
+      Postquantum Preshared Key (PPK) section for a specific secret. Each PPK is
+      defined in a unique section having the <literal>ppk</literal> prefix.
+    '';
+
+    private = mkPrefixedAttrsOfParams {
+      file = mkOptionalStrParam ''
+        File name in the private folder for which this passphrase should be used.
+      '';
+
+      secret = mkOptionalStrParam ''
+        Value of decryption passphrase for private key.
+      '';
+    } ''
+      Private key decryption passphrase for a key in the
+      <literal>private</literal> folder.
+    '';
+
+    rsa = mkPrefixedAttrsOfParams {
+      file = mkOptionalStrParam ''
+        File name in the <literal>rsa</literal> folder for which this passphrase
+        should be used.
+      '';
+      secret = mkOptionalStrParam ''
+        Value of decryption passphrase for RSA key.
+      '';
+    } ''
+      Private key decryption passphrase for a key in the <literal>rsa</literal>
+      folder.
+    '';
+
+    ecdsa = mkPrefixedAttrsOfParams {
+      file = mkOptionalStrParam ''
+        File name in the <literal>ecdsa</literal> folder for which this
+        passphrase should be used.
+      '';
+      secret = mkOptionalStrParam ''
+        Value of decryption passphrase for ECDSA key.
+      '';
+    } ''
+      Private key decryption passphrase for a key in the
+      <literal>ecdsa</literal> folder.
+    '';
+
+    pkcs8 = mkPrefixedAttrsOfParams {
+      file = mkOptionalStrParam ''
+        File name in the <literal>pkcs8</literal> folder for which this
+        passphrase should be used.
+      '';
+      secret = mkOptionalStrParam ''
+        Value of decryption passphrase for PKCS#8 key.
+      '';
+    } ''
+      Private key decryption passphrase for a key in the
+      <literal>pkcs8</literal> folder.
+    '';
+
+    pkcs12 = mkPrefixedAttrsOfParams {
+      file = mkOptionalStrParam ''
+        File name in the <literal>pkcs12</literal> folder for which this
+        passphrase should be used.
+      '';
+      secret = mkOptionalStrParam ''
+        Value of decryption passphrase for PKCS#12 container.
+      '';
+    } ''
+      PKCS#12 decryption passphrase for a container in the
+      <literal>pkcs12</literal> folder.
+    '';
+
+    token = mkPrefixedAttrsOfParams {
+      handle = mkOptionalHexParam ''
+        Hex-encoded CKA_ID or handle of the private key on the token or TPM,
+        respectively.
+      '';
+
+      slot = mkOptionalIntParam ''
+        Optional slot number to access the token.
+      '';
+
+      module = mkOptionalStrParam ''
+        Optional PKCS#11 module name to access the token.
+      '';
+
+      pin = mkOptionalStrParam ''
+        Optional PIN required to access the key on the token. If none is
+        provided the user is prompted during an interactive
+        <literal>--load-creds</literal> call.
+      '';
+    } "Definition for a private key that's stored on a token/smartcard/TPM.";
+
+  };
+
+  pools = mkAttrsOfParams {
+    addrs = mkOptionalStrParam ''
+      Subnet or range defining addresses allocated in pool. Accepts a single
+      CIDR subnet defining the pool to allocate addresses from or an address
+      range (&#60;from&#62;-&#60;to&#62;). Pools must be unique and non-overlapping.
+    '';
+
+    dns           = mkCommaSepListParam [] "Address or CIDR subnets";
+    nbns          = mkCommaSepListParam [] "Address or CIDR subnets";
+    dhcp          = mkCommaSepListParam [] "Address or CIDR subnets";
+    netmask       = mkCommaSepListParam [] "Address or CIDR subnets";
+    server        = mkCommaSepListParam [] "Address or CIDR subnets";
+    subnet        = mkCommaSepListParam [] "Address or CIDR subnets";
+    split_include = mkCommaSepListParam [] "Address or CIDR subnets";
+    split_exclude = mkCommaSepListParam [] "Address or CIDR subnets";
+  } ''
+    Section defining named pools. Named pools may be referenced by connections
+    with the pools option to assign virtual IPs and other configuration
+    attributes. Each pool must have a unique name (denoted &#60;name&#62; below).
+  '';
+}
diff --git a/nixos/modules/services/networking/strongswan.nix b/nixos/modules/services/networking/strongswan.nix
new file mode 100644
index 00000000000..e3a97207be7
--- /dev/null
+++ b/nixos/modules/services/networking/strongswan.nix
@@ -0,0 +1,170 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+  inherit (builtins) toFile;
+  inherit (lib) concatMapStringsSep concatStringsSep mapAttrsToList
+                mkIf mkEnableOption mkOption types literalExpression;
+
+  cfg = config.services.strongswan;
+
+  ipsecSecrets = secrets: toFile "ipsec.secrets" (
+    concatMapStringsSep "\n" (f: "include ${f}") secrets
+  );
+
+  ipsecConf = {setup, connections, ca}:
+    let
+      # https://wiki.strongswan.org/projects/strongswan/wiki/IpsecConf
+      makeSections = type: sections: concatStringsSep "\n\n" (
+        mapAttrsToList (sec: attrs:
+          "${type} ${sec}\n" +
+            (concatStringsSep "\n" ( mapAttrsToList (k: v: "  ${k}=${v}") attrs ))
+        ) sections
+      );
+      setupConf       = makeSections "config" { inherit setup; };
+      connectionsConf = makeSections "conn" connections;
+      caConf          = makeSections "ca" ca;
+
+    in
+    builtins.toFile "ipsec.conf" ''
+      ${setupConf}
+      ${connectionsConf}
+      ${caConf}
+    '';
+
+  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 ""}
+      plugins {
+        stroke {
+          secrets_file = ${secretsFile}
+        }
+      }
+    }
+
+    starter {
+      config_file = ${ipsecConf { inherit setup connections ca; }}
+    }
+  '';
+
+in
+{
+  options.services.strongswan = {
+    enable = mkEnableOption "strongSwan";
+
+    secrets = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "/run/keys/ipsec-foo.secret" ];
+      description = ''
+        A list of paths to IPSec secret files. These
+        files will be included into the main ipsec.secrets file with
+        the <literal>include</literal> directive. It is safer if these
+        paths are absolute.
+      '';
+    };
+
+    setup = mkOption {
+      type = types.attrsOf types.str;
+      default = {};
+      example = { cachecrls = "yes"; strictcrlpolicy = "yes"; };
+      description = ''
+        A set of options for the ‘config setup’ section of the
+        <filename>ipsec.conf</filename> file. Defines general
+        configuration parameters.
+      '';
+    };
+
+    connections = mkOption {
+      type = types.attrsOf (types.attrsOf types.str);
+      default = {};
+      example = literalExpression ''
+        {
+          "%default" = {
+            keyexchange = "ikev2";
+            keyingtries = "1";
+          };
+          roadwarrior = {
+            auto       = "add";
+            leftcert   = "/run/keys/moonCert.pem";
+            leftid     = "@moon.strongswan.org";
+            leftsubnet = "10.1.0.0/16";
+            right      = "%any";
+          };
+        }
+      '';
+      description = ''
+        A set of connections and their options for the ‘conn xxx’
+        sections of the <filename>ipsec.conf</filename> file.
+      '';
+    };
+
+    ca = mkOption {
+      type = types.attrsOf (types.attrsOf types.str);
+      default = {};
+      example = {
+        strongswan = {
+          auto   = "add";
+          cacert = "/run/keys/strongswanCert.pem";
+          crluri = "http://crl2.strongswan.org/strongswan.crl";
+        };
+      };
+      description = ''
+        A set of CAs (certification authorities) and their options for
+        the ‘ca xxx’ sections of the <filename>ipsec.conf</filename>
+        file.
+      '';
+    };
+
+    managePlugins = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        If set to true, this option will disable automatic plugin loading and
+        then tell strongSwan to enable the plugins specified in the
+        <option>enabledPlugins</option> option.
+      '';
+    };
+
+    enabledPlugins = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        A list of additional plugins to enable if
+        <option>managePlugins</option> is true.
+      '';
+    };
+  };
+
+
+  config = with cfg;
+  let
+    secretsFile = ipsecSecrets cfg.secrets;
+  in
+  mkIf enable
+    {
+
+    # here we should use the default strongswan ipsec.secrets and
+    # append to it (default one is empty so not a pb for now)
+    environment.etc."ipsec.secrets".source = secretsFile;
+
+    systemd.services.strongswan = {
+      description = "strongSwan IPSec Service";
+      wantedBy = [ "multi-user.target" ];
+      path = with pkgs; [ kmod iproute2 iptables util-linux ]; # XXX Linux
+      after = [ "network-online.target" ];
+      environment = {
+        STRONGSWAN_CONF = strongswanConf { inherit setup connections ca secretsFile managePlugins enabledPlugins; };
+      };
+      serviceConfig = {
+        ExecStart  = "${pkgs.strongswan}/sbin/ipsec start --nofork";
+      };
+      preStart = ''
+        # with 'nopeerdns' setting, ppp writes into this folder
+        mkdir -m 700 -p /etc/ppp
+      '';
+    };
+  };
+}
+
diff --git a/nixos/modules/services/networking/stubby.nix b/nixos/modules/services/networking/stubby.nix
new file mode 100644
index 00000000000..78c13798dde
--- /dev/null
+++ b/nixos/modules/services/networking/stubby.nix
@@ -0,0 +1,89 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.stubby;
+  settingsFormat = pkgs.formats.yaml { };
+  confFile = settingsFormat.generate "stubby.yml" cfg.settings;
+in {
+  imports = map (x:
+    (mkRemovedOptionModule [ "services" "stubby" x ]
+      "Stubby configuration moved to services.stubby.settings.")) [
+        "authenticationMode"
+        "fallbackProtocols"
+        "idleTimeout"
+        "listenAddresses"
+        "queryPaddingBlocksize"
+        "roundRobinUpstreams"
+        "subnetPrivate"
+        "upstreamServers"
+      ];
+
+  options = {
+    services.stubby = {
+
+      enable = mkEnableOption "Stubby DNS resolver";
+
+      settings = mkOption {
+        type = types.attrsOf settingsFormat.type;
+        example = lib.literalExpression ''
+          pkgs.stubby.passthru.settingsExample // {
+            upstream_recursive_servers = [{
+              address_data = "158.64.1.29";
+              tls_auth_name = "kaitain.restena.lu";
+              tls_pubkey_pinset = [{
+                digest = "sha256";
+                value = "7ftvIkA+UeN/ktVkovd/7rPZ6mbkhVI7/8HnFJIiLa4=";
+              }];
+            }];
+          };
+        '';
+        description = ''
+          Content of the Stubby configuration file. All Stubby settings may be set or queried
+          here. The default settings are available at
+          <literal>pkgs.stubby.passthru.settingsExample</literal>. See
+          <link xlink:href="https://dnsprivacy.org/wiki/display/DP/Configuring+Stubby"/>.
+          A list of the public recursive servers can be found here:
+          <link xlink:href="https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Test+Servers"/>.
+        '';
+      };
+
+      debugLogging = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Enable or disable debug level logging.";
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [{
+      assertion =
+        (cfg.settings.resolution_type or "") == "GETDNS_RESOLUTION_STUB";
+      message = ''
+        services.stubby.settings.resolution_type must be set to "GETDNS_RESOLUTION_STUB".
+        Is services.stubby.settings unset?
+      '';
+    }];
+
+    services.stubby.settings.appdata_dir = "/var/cache/stubby";
+
+    systemd.services.stubby = {
+      description = "Stubby local DNS resolver";
+      after = [ "network.target" ];
+      before = [ "nss-lookup.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "notify";
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
+        ExecStart = "${pkgs.stubby}/bin/stubby -C ${confFile} ${optionalString cfg.debugLogging "-l"}";
+        DynamicUser = true;
+        CacheDirectory = "stubby";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/stunnel.nix b/nixos/modules/services/networking/stunnel.nix
new file mode 100644
index 00000000000..df4908a0fff
--- /dev/null
+++ b/nixos/modules/services/networking/stunnel.nix
@@ -0,0 +1,238 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.stunnel;
+  yesNo = val: if val then "yes" else "no";
+
+  verifyChainPathAssert = n: c: {
+    assertion = c.verifyHostname == null || (c.verifyChain || c.verifyPeer);
+    message =  "stunnel: \"${n}\" client configuration - hostname verification " +
+      "is not possible without either verifyChain or verifyPeer enabled";
+  };
+
+  serverConfig = {
+    options = {
+      accept = mkOption {
+        type = types.either types.str types.int;
+        description = ''
+          On which [host:]port stunnel should listen for incoming TLS connections.
+          Note that unlike other softwares stunnel ipv6 address need no brackets,
+          so to listen on all IPv6 addresses on port 1234 one would use ':::1234'.
+        '';
+      };
+
+      connect = mkOption {
+        type = types.either types.str types.int;
+        description = "Port or IP:Port to which the decrypted connection should be forwarded.";
+      };
+
+      cert = mkOption {
+        type = types.path;
+        description = "File containing both the private and public keys.";
+      };
+    };
+  };
+
+  clientConfig = {
+    options = {
+      accept = mkOption {
+        type = types.str;
+        description = "IP:Port on which connections should be accepted.";
+      };
+
+      connect = mkOption {
+        type = types.str;
+        description = "IP:Port destination to connect to.";
+      };
+
+      verifyChain = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Check if the provided certificate has a valid certificate chain (against CAPath).";
+      };
+
+      verifyPeer = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Check if the provided certificate is contained in CAPath.";
+      };
+
+      CAPath = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = "Path to a directory containing certificates to validate against.";
+      };
+
+      CAFile = mkOption {
+        type = types.nullOr types.path;
+        default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
+        defaultText = literalExpression ''"''${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"'';
+        description = "Path to a file containing certificates to validate against.";
+      };
+
+      verifyHostname = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "If set, stunnel checks if the provided certificate is valid for the given hostname.";
+      };
+    };
+  };
+
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.stunnel = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the stunnel TLS tunneling service.";
+      };
+
+      user = mkOption {
+        type = with types; nullOr str;
+        default = "nobody";
+        description = "The user under which stunnel runs.";
+      };
+
+      group = mkOption {
+        type = with types; nullOr str;
+        default = "nogroup";
+        description = "The group under which stunnel runs.";
+      };
+
+      logLevel = mkOption {
+        type = types.enum [ "emerg" "alert" "crit" "err" "warning" "notice" "info" "debug" ];
+        default = "info";
+        description = "Verbosity of stunnel output.";
+      };
+
+      fipsMode = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable FIPS 140-2 mode required for compliance.";
+      };
+
+      enableInsecureSSLv3 = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable support for the insecure SSLv3 protocol.";
+      };
+
+
+      servers = mkOption {
+        description = "Define the server configuations.";
+        type = with types; attrsOf (submodule serverConfig);
+        example = {
+          fancyWebserver = {
+            accept = 443;
+            connect = 8080;
+            cert = "/path/to/pem/file";
+          };
+        };
+        default = { };
+      };
+
+      clients = mkOption {
+        description = "Define the client configurations.";
+        type = with types; attrsOf (submodule clientConfig);
+        example = {
+          foobar = {
+            accept = "0.0.0.0:8080";
+            connect = "nixos.org:443";
+            verifyChain = false;
+          };
+        };
+        default = { };
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = concatLists [
+      (singleton {
+        assertion = (length (attrValues cfg.servers) != 0) || ((length (attrValues cfg.clients)) != 0);
+        message = "stunnel: At least one server- or client-configuration has to be present.";
+      })
+
+      (mapAttrsToList verifyChainPathAssert cfg.clients)
+    ];
+
+    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 "" }
+
+      debug = ${cfg.logLevel}
+
+      ${ optionalString cfg.fipsMode "fips = yes" }
+      ${ optionalString cfg.enableInsecureSSLv3 "options = -NO_SSLv3" }
+
+      ; ----- SERVER CONFIGURATIONS -----
+      ${ lib.concatStringsSep "\n"
+           (lib.mapAttrsToList
+             (n: v: ''
+               [${n}]
+               accept = ${toString v.accept}
+               connect = ${toString v.connect}
+               cert = ${v.cert}
+
+             '')
+           cfg.servers)
+      }
+
+      ; ----- CLIENT CONFIGURATIONS -----
+      ${ lib.concatStringsSep "\n"
+           (lib.mapAttrsToList
+             (n: v: ''
+               [${n}]
+               client = yes
+               accept = ${v.accept}
+               connect = ${v.connect}
+               verifyChain = ${yesNo v.verifyChain}
+               verifyPeer = ${yesNo v.verifyPeer}
+               ${optionalString (v.CAPath != null) "CApath = ${v.CAPath}"}
+               ${optionalString (v.CAFile != null) "CAFile = ${v.CAFile}"}
+               ${optionalString (v.verifyHostname != null) "checkHost = ${v.verifyHostname}"}
+               OCSPaia = yes
+
+             '')
+           cfg.clients)
+      }
+    '';
+
+    systemd.services.stunnel = {
+      description = "stunnel TLS tunneling service";
+      after = [ "network.target" ];
+      wants = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ config.environment.etc."stunnel.cfg".source ];
+      serviceConfig = {
+        ExecStart = "${pkgs.stunnel}/bin/stunnel ${config.environment.etc."stunnel.cfg".source}";
+        Type = "forking";
+      };
+    };
+
+    meta.maintainers = with maintainers; [
+      # Server side
+      lschuermann
+      # Client side
+      das_j
+    ];
+  };
+
+}
diff --git a/nixos/modules/services/networking/supplicant.nix b/nixos/modules/services/networking/supplicant.nix
new file mode 100644
index 00000000000..eb24130e519
--- /dev/null
+++ b/nixos/modules/services/networking/supplicant.nix
@@ -0,0 +1,240 @@
+{ config, lib, utils, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.networking.supplicant;
+
+  # We must escape interfaces due to the systemd interpretation
+  subsystemDevice = interface:
+    "sys-subsystem-net-devices-${utils.escapeSystemdPath interface}.device";
+
+  serviceName = iface: "supplicant-${if (iface=="WLAN") then "wlan@" else (
+                                     if (iface=="LAN") then "lan@" else (
+                                     if (iface=="DBUS") then "dbus"
+                                     else (replaceChars [" "] ["-"] iface)))}";
+
+  # TODO: Use proper privilege separation for wpa_supplicant
+  supplicantService = iface: suppl:
+    let
+      deps = (if (iface=="WLAN"||iface=="LAN") then ["sys-subsystem-net-devices-%i.device"] else (
+             if (iface=="DBUS") then ["dbus.service"]
+             else (map subsystemDevice (splitString " " iface))))
+             ++ optional (suppl.bridge!="") (subsystemDevice suppl.bridge);
+
+      ifaceArg = concatStringsSep " -N " (map (i: "-i${i}") (splitString " " iface));
+      driverArg = optionalString (suppl.driver != null) "-D${suppl.driver}";
+      bridgeArg = optionalString (suppl.bridge!="") "-b${suppl.bridge}";
+      confFileArg = optionalString (suppl.configFile.path!=null) "-c${suppl.configFile.path}";
+      extraConfFile = pkgs.writeText "supplicant-extra-conf-${replaceChars [" "] ["-"] iface}" ''
+        ${optionalString suppl.userControlled.enable "ctrl_interface=DIR=${suppl.userControlled.socketDir} GROUP=${suppl.userControlled.group}"}
+        ${optionalString suppl.configFile.writable "update_config=1"}
+        ${suppl.extraConf}
+      '';
+    in
+      { description = "Supplicant ${iface}${optionalString (iface=="WLAN"||iface=="LAN") " %I"}";
+        wantedBy = [ "multi-user.target" ] ++ deps;
+        wants = [ "network.target" ];
+        bindsTo = deps;
+        after = deps;
+        before = [ "network.target" ];
+
+        path = [ pkgs.coreutils ];
+
+        preStart = ''
+          ${optionalString (suppl.configFile.path!=null) ''
+            (umask 077 && touch -a "${suppl.configFile.path}")
+          ''}
+          ${optionalString suppl.userControlled.enable ''
+            install -dm770 -g "${suppl.userControlled.group}" "${suppl.userControlled.socketDir}"
+          ''}
+        '';
+
+        serviceConfig.ExecStart = "${pkgs.wpa_supplicant}/bin/wpa_supplicant -s ${driverArg} ${confFileArg} -I${extraConfFile} ${bridgeArg} ${suppl.extraCmdArgs} ${if (iface=="WLAN"||iface=="LAN") then "-i%I" else (if (iface=="DBUS") then "-u" else ifaceArg)}";
+
+      };
+
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    networking.supplicant = mkOption {
+      type = with types; attrsOf (submodule {
+        options = {
+
+          configFile = {
+
+            path = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              example = literalExpression "/etc/wpa_supplicant.conf";
+              description = ''
+                External <literal>wpa_supplicant.conf</literal> configuration file.
+                The configuration options defined declaratively within <literal>networking.supplicant</literal> have
+                precedence over options defined in <literal>configFile</literal>.
+              '';
+            };
+
+            writable = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Whether the configuration file at <literal>configFile.path</literal> should be written to by
+                <literal>wpa_supplicant</literal>.
+              '';
+            };
+
+          };
+
+          extraConf = mkOption {
+            type = types.lines;
+            default = "";
+            example = ''
+              ap_scan=1
+              device_name=My-NixOS-Device
+              device_type=1-0050F204-1
+              driver_param=use_p2p_group_interface=1
+              disable_scan_offload=1
+              p2p_listen_reg_class=81
+              p2p_listen_channel=1
+              p2p_oper_reg_class=81
+              p2p_oper_channel=1
+              manufacturer=NixOS
+              model_name=NixOS_Unstable
+              model_number=2015
+            '';
+            description = ''
+              Configuration options for <literal>wpa_supplicant.conf</literal>.
+              Options defined here have precedence over options in <literal>configFile</literal>.
+              NOTE: Do not write sensitive data into <literal>extraConf</literal> as it will
+              be world-readable in the <literal>nix-store</literal>. For sensitive information
+              use the <literal>configFile</literal> instead.
+            '';
+          };
+
+          extraCmdArgs = mkOption {
+            type = types.str;
+            default = "";
+            example = "-e/run/wpa_supplicant/entropy.bin";
+            description =
+              "Command line arguments to add when executing <literal>wpa_supplicant</literal>.";
+          };
+
+          driver = mkOption {
+            type = types.nullOr types.str;
+            default = "nl80211,wext";
+            description = "Force a specific wpa_supplicant driver.";
+          };
+
+          bridge = mkOption {
+            type = types.str;
+            default = "";
+            description = "Name of the bridge interface that wpa_supplicant should listen at.";
+          };
+
+          userControlled = {
+
+            enable = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Allow normal users to control wpa_supplicant through wpa_gui or wpa_cli.
+                This is useful for laptop users that switch networks a lot and don't want
+                to depend on a large package such as NetworkManager just to pick nearby
+                access points.
+              '';
+            };
+
+            socketDir = mkOption {
+              type = types.str;
+              default = "/run/wpa_supplicant";
+              description = "Directory of sockets for controlling wpa_supplicant.";
+            };
+
+            group = mkOption {
+              type = types.str;
+              default = "wheel";
+              example = "network";
+              description = "Members of this group can control wpa_supplicant.";
+            };
+
+          };
+        };
+      });
+
+      default = { };
+
+      example = literalExpression ''
+        { "wlan0 wlan1" = {
+            configFile.path = "/etc/wpa_supplicant.conf";
+            userControlled.group = "network";
+            extraConf = '''
+              ap_scan=1
+              p2p_disabled=1
+            ''';
+            extraCmdArgs = "-u -W";
+            bridge = "br0";
+          };
+        }
+      '';
+
+      description = ''
+        Interfaces for which to start <command>wpa_supplicant</command>.
+        The supplicant is used to scan for and associate with wireless networks,
+        or to authenticate with 802.1x capable network switches.
+
+        The value of this option is an attribute set. Each attribute configures a
+        <command>wpa_supplicant</command> service, where the attribute name specifies
+        the name of the interface that <command>wpa_supplicant</command> operates on.
+        The attribute name can be a space separated list of interfaces.
+        The attribute names <literal>WLAN</literal>, <literal>LAN</literal> and <literal>DBUS</literal>
+        have a special meaning. <literal>WLAN</literal> and <literal>LAN</literal> are
+        configurations for universal <command>wpa_supplicant</command> service that is
+        started for each WLAN interface or for each LAN interface, respectively.
+        <literal>DBUS</literal> defines a device-unrelated <command>wpa_supplicant</command>
+        service that can be accessed through <literal>D-Bus</literal>.
+      '';
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf (cfg != {}) {
+
+    environment.systemPackages =  [ pkgs.wpa_supplicant ];
+
+    services.dbus.packages = [ pkgs.wpa_supplicant ];
+
+    systemd.services = mapAttrs' (n: v: nameValuePair (serviceName n) (supplicantService n v)) cfg;
+
+    services.udev.packages = [
+      (pkgs.writeTextFile {
+        name = "99-zzz-60-supplicant.rules";
+        destination = "/etc/udev/rules.d/99-zzz-60-supplicant.rules";
+        text = ''
+          ${flip (concatMapStringsSep "\n") (filter (n: n!="WLAN" && n!="LAN" && n!="DBUS") (attrNames cfg)) (iface:
+            flip (concatMapStringsSep "\n") (splitString " " iface) (i: ''
+              ACTION=="add", SUBSYSTEM=="net", ENV{INTERFACE}=="${i}", TAG+="systemd", ENV{SYSTEMD_WANTS}+="supplicant-${replaceChars [" "] ["-"] iface}.service", TAG+="SUPPLICANT_ASSIGNED"''))}
+
+          ${optionalString (hasAttr "WLAN" cfg) ''
+            ACTION=="add", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", TAG!="SUPPLICANT_ASSIGNED", TAG+="systemd", PROGRAM="${pkgs.systemd}/bin/systemd-escape -p %E{INTERFACE}", ENV{SYSTEMD_WANTS}+="supplicant-wlan@$result.service"
+          ''}
+          ${optionalString (hasAttr "LAN" cfg) ''
+            ACTION=="add", SUBSYSTEM=="net", ENV{DEVTYPE}=="lan", TAG!="SUPPLICANT_ASSIGNED", TAG+="systemd", PROGRAM="${pkgs.systemd}/bin/systemd-escape -p %E{INTERFACE}", ENV{SYSTEMD_WANTS}+="supplicant-lan@$result.service"
+          ''}
+        '';
+      })];
+
+  };
+
+}
+
diff --git a/nixos/modules/services/networking/supybot.nix b/nixos/modules/services/networking/supybot.nix
new file mode 100644
index 00000000000..94b79c7e247
--- /dev/null
+++ b/nixos/modules/services/networking/supybot.nix
@@ -0,0 +1,163 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg  = config.services.supybot;
+  isStateDirHome = hasPrefix "/home/" cfg.stateDir;
+  isStateDirVar = cfg.stateDir == "/var/lib/supybot";
+  pyEnv = pkgs.python3.withPackages (p: [ p.limnoria ] ++ (cfg.extraPackages p));
+in
+{
+  options = {
+
+    services.supybot = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable Supybot, an IRC bot (also known as Limnoria).";
+      };
+
+      stateDir = mkOption {
+        type = types.path;
+        default = if versionAtLeast config.system.stateVersion "20.09"
+          then "/var/lib/supybot"
+          else "/home/supybot";
+        defaultText = literalExpression "/var/lib/supybot";
+        description = "The root directory, logs and plugins are stored here";
+      };
+
+      configFile = mkOption {
+        type = types.path;
+        description = ''
+          Path to initial supybot config file. This can be generated by
+          running supybot-wizard.
+
+          Note: all paths should include the full path to the stateDir
+          directory (backup conf data logs logs/plugins plugins tmp web).
+        '';
+      };
+
+      plugins = mkOption {
+        type = types.attrsOf types.path;
+        default = {};
+        description = ''
+          Attribute set of additional plugins that will be symlinked to the
+          <filename>plugin</filename> subdirectory.
+
+          Please note that you still need to add the plugins to the config
+          file (or with <literal>!load</literal>) using their attribute name.
+        '';
+        example = literalExpression ''
+          let
+            plugins = pkgs.fetchzip {
+              url = "https://github.com/ProgVal/Supybot-plugins/archive/57c2450c.zip";
+              sha256 = "077snf84ibnva3sbpzdfpfma6hcdw7dflwnhg6pw7mgnf0nd84qd";
+            };
+          in
+          {
+            Wikipedia = "''${plugins}/Wikipedia";
+            Decide = ./supy-decide;
+          }
+        '';
+      };
+
+      extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
+        default = p: [];
+        defaultText = literalExpression "p: []";
+        description = ''
+          Extra Python packages available to supybot plugins. The
+          value must be a function which receives the attrset defined
+          in <varname>python3Packages</varname> as the sole argument.
+        '';
+        example = literalExpression "p: [ p.lxml p.requests ]";
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.python3Packages.limnoria ];
+
+    users.users.supybot = {
+      uid = config.ids.uids.supybot;
+      group = "supybot";
+      description = "Supybot IRC bot user";
+      home = cfg.stateDir;
+      isSystemUser = true;
+    };
+
+    users.groups.supybot = {
+      gid = config.ids.gids.supybot;
+    };
+
+    systemd.services.supybot = {
+      description = "Supybot, an IRC bot";
+      documentation = [ "https://limnoria.readthedocs.io/" ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        # This needs to be created afresh every time
+        rm -f '${cfg.stateDir}/supybot.cfg.bak'
+      '';
+
+      startLimitIntervalSec = 5 * 60;  # 5 min
+      startLimitBurst = 1;
+      serviceConfig = {
+        ExecStart = "${pyEnv}/bin/supybot ${cfg.stateDir}/supybot.cfg";
+        PIDFile = "/run/supybot.pid";
+        User = "supybot";
+        Group = "supybot";
+        UMask = "0007";
+        Restart = "on-abort";
+
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateTmp = true;
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RemoveIPC = true;
+        ProtectHostname = true;
+        CapabilityBoundingSet = "";
+        ProtectSystem = "full";
+      }
+      // optionalAttrs isStateDirVar {
+        StateDirectory = "supybot";
+        ProtectSystem = "strict";
+      }
+      // optionalAttrs (!isStateDirHome) {
+        ProtectHome = true;
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}'              0700 supybot supybot - -"
+      "d '${cfg.stateDir}/backup'       0750 supybot supybot - -"
+      "d '${cfg.stateDir}/conf'         0750 supybot supybot - -"
+      "d '${cfg.stateDir}/data'         0750 supybot supybot - -"
+      "d '${cfg.stateDir}/plugins'      0750 supybot supybot - -"
+      "d '${cfg.stateDir}/logs'         0750 supybot supybot - -"
+      "d '${cfg.stateDir}/logs/plugins' 0750 supybot supybot - -"
+      "d '${cfg.stateDir}/tmp'          0750 supybot supybot - -"
+      "d '${cfg.stateDir}/web'          0750 supybot supybot - -"
+      "L '${cfg.stateDir}/supybot.cfg'  -    -       -       - ${cfg.configFile}"
+    ]
+    ++ (flip mapAttrsToList cfg.plugins (name: dest:
+      "L+ '${cfg.stateDir}/plugins/${name}' - - - - ${dest}"
+    ));
+
+  };
+}
diff --git a/nixos/modules/services/networking/syncplay.nix b/nixos/modules/services/networking/syncplay.nix
new file mode 100644
index 00000000000..b6faf2d3f77
--- /dev/null
+++ b/nixos/modules/services/networking/syncplay.nix
@@ -0,0 +1,80 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.syncplay;
+
+  cmdArgs =
+    [ "--port" cfg.port ]
+    ++ optionals (cfg.salt != null) [ "--salt" cfg.salt ]
+    ++ optionals (cfg.certDir != null) [ "--tls" cfg.certDir ];
+
+in
+{
+  options = {
+    services.syncplay = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "If enabled, start the Syncplay server.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 8999;
+        description = ''
+          TCP port to bind to.
+        '';
+      };
+
+      salt = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Salt to allow room operator passwords generated by this server
+          instance to still work when the server is restarted.
+        '';
+      };
+
+      certDir = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          TLS certificates directory to use for encryption. See
+          <link xlink:href="https://github.com/Syncplay/syncplay/wiki/TLS-support"/>.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "nobody";
+        description = ''
+          User to use when running Syncplay.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "nogroup";
+        description = ''
+          Group to use when running Syncplay.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.syncplay = {
+      description = "Syncplay Service";
+      wantedBy    = [ "multi-user.target" ];
+      after       = [ "network-online.target" ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.syncplay}/bin/syncplay-server ${escapeShellArgs cmdArgs}";
+        User = cfg.user;
+        Group = cfg.group;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/syncthing-relay.nix b/nixos/modules/services/networking/syncthing-relay.nix
new file mode 100644
index 00000000000..f5ca63e7893
--- /dev/null
+++ b/nixos/modules/services/networking/syncthing-relay.nix
@@ -0,0 +1,121 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.syncthing.relay;
+
+  dataDirectory = "/var/lib/syncthing-relay";
+
+  relayOptions =
+    [
+      "--keys=${dataDirectory}"
+      "--listen=${cfg.listenAddress}:${toString cfg.port}"
+      "--status-srv=${cfg.statusListenAddress}:${toString cfg.statusPort}"
+      "--provided-by=${escapeShellArg cfg.providedBy}"
+    ]
+    ++ optional (cfg.pools != null) "--pools=${escapeShellArg (concatStringsSep "," cfg.pools)}"
+    ++ optional (cfg.globalRateBps != null) "--global-rate=${toString cfg.globalRateBps}"
+    ++ optional (cfg.perSessionRateBps != null) "--per-session-rate=${toString cfg.perSessionRateBps}"
+    ++ cfg.extraOptions;
+in {
+  ###### interface
+
+  options.services.syncthing.relay = {
+    enable = mkEnableOption "Syncthing relay service";
+
+    listenAddress = mkOption {
+      type = types.str;
+      default = "";
+      example = "1.2.3.4";
+      description = ''
+        Address to listen on for relay traffic.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 22067;
+      description = ''
+        Port to listen on for relay traffic. This port should be added to
+        <literal>networking.firewall.allowedTCPPorts</literal>.
+      '';
+    };
+
+    statusListenAddress = mkOption {
+      type = types.str;
+      default = "";
+      example = "1.2.3.4";
+      description = ''
+        Address to listen on for serving the relay status API.
+      '';
+    };
+
+    statusPort = mkOption {
+      type = types.port;
+      default = 22070;
+      description = ''
+        Port to listen on for serving the relay status API. This port should be
+        added to <literal>networking.firewall.allowedTCPPorts</literal>.
+      '';
+    };
+
+    pools = mkOption {
+      type = types.nullOr (types.listOf types.str);
+      default = null;
+      description = ''
+        Relay pools to join. If null, uses the default global pool.
+      '';
+    };
+
+    providedBy = mkOption {
+      type = types.str;
+      default = "";
+      description = ''
+        Human-readable description of the provider of the relay (you).
+      '';
+    };
+
+    globalRateBps = mkOption {
+      type = types.nullOr types.ints.positive;
+      default = null;
+      description = ''
+        Global bandwidth rate limit in bytes per second.
+      '';
+    };
+
+    perSessionRateBps = mkOption {
+      type = types.nullOr types.ints.positive;
+      default = null;
+      description = ''
+        Per session bandwidth rate limit in bytes per second.
+      '';
+    };
+
+    extraOptions = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Extra command line arguments to pass to strelaysrv.
+      '';
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.syncthing-relay = {
+      description = "Syncthing relay service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = baseNameOf dataDirectory;
+
+        Restart = "on-failure";
+        ExecStart = "${pkgs.syncthing-relay}/bin/strelaysrv ${concatStringsSep " " relayOptions}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix
new file mode 100644
index 00000000000..3a3d4c80ecf
--- /dev/null
+++ b/nixos/modules/services/networking/syncthing.nix
@@ -0,0 +1,601 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.syncthing;
+  opt = options.services.syncthing;
+  defaultUser = "syncthing";
+  defaultGroup = defaultUser;
+
+  devices = mapAttrsToList (name: device: {
+    deviceID = device.id;
+    inherit (device) name addresses introducer autoAcceptFolders;
+  }) cfg.devices;
+
+  folders = mapAttrsToList ( _: folder: {
+    inherit (folder) path id label type;
+    devices = map (device: { deviceId = cfg.devices.${device}.id; }) folder.devices;
+    rescanIntervalS = folder.rescanInterval;
+    fsWatcherEnabled = folder.watch;
+    fsWatcherDelayS = folder.watchDelay;
+    ignorePerms = folder.ignorePerms;
+    ignoreDelete = folder.ignoreDelete;
+    versioning = folder.versioning;
+  }) (filterAttrs (
+    _: folder:
+    folder.enable
+  ) cfg.folders);
+
+  updateConfig = pkgs.writers.writeDash "merge-syncthing-config" ''
+    set -efu
+
+    # get the api key by parsing the config.xml
+    while
+        ! api_key=$(${pkgs.libxml2}/bin/xmllint \
+            --xpath 'string(configuration/gui/apikey)' \
+            ${cfg.configDir}/config.xml)
+    do sleep 1; done
+
+    curl() {
+        ${pkgs.curl}/bin/curl -sSLk -H "X-API-Key: $api_key" \
+            --retry 1000 --retry-delay 1 --retry-all-errors \
+            "$@"
+    }
+
+    # query the old config
+    old_cfg=$(curl ${cfg.guiAddress}/rest/config)
+
+    # generate the new config by merging with the NixOS config options
+    new_cfg=$(printf '%s\n' "$old_cfg" | ${pkgs.jq}/bin/jq -c '. * {
+        "devices": (${builtins.toJSON devices}${optionalString (! cfg.overrideDevices) " + .devices"}),
+        "folders": (${builtins.toJSON folders}${optionalString (! cfg.overrideFolders) " + .folders"})
+    } * ${builtins.toJSON cfg.extraOptions}')
+
+    # send the new config
+    curl -X PUT -d "$new_cfg" ${cfg.guiAddress}/rest/config
+
+    # restart Syncthing if required
+    if curl ${cfg.guiAddress}/rest/config/restart-required |
+       ${pkgs.jq}/bin/jq -e .requiresRestart > /dev/null; then
+        curl -X POST ${cfg.guiAddress}/rest/system/restart
+    fi
+  '';
+in {
+  ###### interface
+  options = {
+    services.syncthing = {
+
+      enable = mkEnableOption
+        "Syncthing, a self-hosted open-source alternative to Dropbox and Bittorrent Sync";
+
+      cert = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Path to the <literal>cert.pem</literal> file, which will be copied into Syncthing's
+          <link linkend="opt-services.syncthing.configDir">configDir</link>.
+        '';
+      };
+
+      key = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Path to the <literal>key.pem</literal> file, which will be copied into Syncthing's
+          <link linkend="opt-services.syncthing.configDir">configDir</link>.
+        '';
+      };
+
+      overrideDevices = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to delete the devices which are not configured via the
+          <link linkend="opt-services.syncthing.devices">devices</link> option.
+          If set to <literal>false</literal>, devices added via the web
+          interface will persist and will have to be deleted manually.
+        '';
+      };
+
+      devices = mkOption {
+        default = {};
+        description = ''
+          Peers/devices which Syncthing should communicate with.
+
+          Note that you can still add devices manually, but those changes
+          will be reverted on restart if <link linkend="opt-services.syncthing.overrideDevices">overrideDevices</link>
+          is enabled.
+        '';
+        example = {
+          bigbox = {
+            id = "7CFNTQM-IMTJBHJ-3UWRDIU-ZGQJFR6-VCXZ3NB-XUH3KZO-N52ITXR-LAIYUAU";
+            addresses = [ "tcp://192.168.0.10:51820" ];
+          };
+        };
+        type = types.attrsOf (types.submodule ({ name, ... }: {
+          options = {
+
+            name = mkOption {
+              type = types.str;
+              default = name;
+              description = ''
+                The name of the device.
+              '';
+            };
+
+            addresses = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = ''
+                The addresses used to connect to the device.
+                If this is left empty, dynamic configuration is attempted.
+              '';
+            };
+
+            id = mkOption {
+              type = types.str;
+              description = ''
+                The device ID. See <link xlink:href="https://docs.syncthing.net/dev/device-ids.html"/>.
+              '';
+            };
+
+            introducer = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Whether the device should act as an introducer and be allowed
+                to add folders on this computer.
+                See <link xlink:href="https://docs.syncthing.net/users/introducer.html"/>.
+              '';
+            };
+
+            autoAcceptFolders = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Automatically create or share folders that this device advertises at the default path.
+                See <link xlink:href="https://docs.syncthing.net/users/config.html?highlight=autoaccept#config-file-format"/>.
+              '';
+            };
+
+          };
+        }));
+      };
+
+      overrideFolders = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to delete the folders which are not configured via the
+          <link linkend="opt-services.syncthing.folders">folders</link> option.
+          If set to <literal>false</literal>, folders added via the web
+          interface will persist and will have to be deleted manually.
+        '';
+      };
+
+      folders = mkOption {
+        default = {};
+        description = ''
+          Folders which should be shared by Syncthing.
+
+          Note that you can still add devices manually, but those changes
+          will be reverted on restart if <link linkend="opt-services.syncthing.overrideDevices">overrideDevices</link>
+          is enabled.
+        '';
+        example = literalExpression ''
+          {
+            "/home/user/sync" = {
+              id = "syncme";
+              devices = [ "bigbox" ];
+            };
+          }
+        '';
+        type = types.attrsOf (types.submodule ({ name, ... }: {
+          options = {
+
+            enable = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                Whether to share this folder.
+                This option is useful when you want to define all folders
+                in one place, but not every machine should share all folders.
+              '';
+            };
+
+            path = mkOption {
+              type = types.str;
+              default = name;
+              description = ''
+                The path to the folder which should be shared.
+              '';
+            };
+
+            id = mkOption {
+              type = types.str;
+              default = name;
+              description = ''
+                The ID of the folder. Must be the same on all devices.
+              '';
+            };
+
+            label = mkOption {
+              type = types.str;
+              default = name;
+              description = ''
+                The label of the folder.
+              '';
+            };
+
+            devices = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = ''
+                The devices this folder should be shared with. Each device must
+                be defined in the <link linkend="opt-services.syncthing.devices">devices</link> option.
+              '';
+            };
+
+            versioning = mkOption {
+              default = null;
+              description = ''
+                How to keep changed/deleted files with Syncthing.
+                There are 4 different types of versioning with different parameters.
+                See <link xlink:href="https://docs.syncthing.net/users/versioning.html"/>.
+              '';
+              example = literalExpression ''
+                [
+                  {
+                    versioning = {
+                      type = "simple";
+                      params.keep = "10";
+                    };
+                  }
+                  {
+                    versioning = {
+                      type = "trashcan";
+                      params.cleanoutDays = "1000";
+                    };
+                  }
+                  {
+                    versioning = {
+                      type = "staggered";
+                      params = {
+                        cleanInterval = "3600";
+                        maxAge = "31536000";
+                        versionsPath = "/syncthing/backup";
+                      };
+                    };
+                  }
+                  {
+                    versioning = {
+                      type = "external";
+                      params.versionsPath = pkgs.writers.writeBash "backup" '''
+                        folderpath="$1"
+                        filepath="$2"
+                        rm -rf "$folderpath/$filepath"
+                      ''';
+                    };
+                  }
+                ]
+              '';
+              type = with types; nullOr (submodule {
+                options = {
+                  type = mkOption {
+                    type = enum [ "external" "simple" "staggered" "trashcan" ];
+                    description = ''
+                      The type of versioning.
+                      See <link xlink:href="https://docs.syncthing.net/users/versioning.html"/>.
+                    '';
+                  };
+                  params = mkOption {
+                    type = attrsOf (either str path);
+                    description = ''
+                      The parameters for versioning. Structure depends on
+                      <link linkend="opt-services.syncthing.folders._name_.versioning.type">versioning.type</link>.
+                      See <link xlink:href="https://docs.syncthing.net/users/versioning.html"/>.
+                    '';
+                  };
+                };
+              });
+            };
+
+            rescanInterval = mkOption {
+              type = types.int;
+              default = 3600;
+              description = ''
+                How often the folder should be rescanned for changes.
+              '';
+            };
+
+            type = mkOption {
+              type = types.enum [ "sendreceive" "sendonly" "receiveonly" ];
+              default = "sendreceive";
+              description = ''
+                Whether to only send changes for this folder, only receive them
+                or both.
+              '';
+            };
+
+            watch = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                Whether the folder should be watched for changes by inotify.
+              '';
+            };
+
+            watchDelay = mkOption {
+              type = types.int;
+              default = 10;
+              description = ''
+                The delay after an inotify event is triggered.
+              '';
+            };
+
+            ignorePerms = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                Whether to ignore permission changes.
+              '';
+            };
+
+            ignoreDelete = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Whether to skip deleting files that are deleted by peers.
+                See <link xlink:href="https://docs.syncthing.net/advanced/folder-ignoredelete.html"/>.
+              '';
+            };
+          };
+        }));
+      };
+
+      extraOptions = mkOption {
+        type = types.addCheck (pkgs.formats.json {}).type isAttrs;
+        default = {};
+        description = ''
+          Extra configuration options for Syncthing.
+          See <link xlink:href="https://docs.syncthing.net/users/config.html"/>.
+        '';
+        example = {
+          options.localAnnounceEnabled = false;
+          gui.theme = "black";
+        };
+      };
+
+      guiAddress = mkOption {
+        type = types.str;
+        default = "127.0.0.1:8384";
+        description = ''
+          The address to serve the web interface at.
+        '';
+      };
+
+      systemService = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to auto-launch Syncthing as a system service.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = defaultUser;
+        example = "yourUser";
+        description = ''
+          The user to run Syncthing as.
+          By default, a user named <literal>${defaultUser}</literal> will be created.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = defaultGroup;
+        example = "yourGroup";
+        description = ''
+          The group to run Syncthing under.
+          By default, a group named <literal>${defaultGroup}</literal> will be created.
+        '';
+      };
+
+      all_proxy = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        example = "socks5://address.com:1234";
+        description = ''
+          Overwrites the all_proxy environment variable for the Syncthing process to
+          the given value. This is normally used to let Syncthing connect
+          through a SOCKS5 proxy server.
+          See <link xlink:href="https://docs.syncthing.net/users/proxying.html"/>.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/syncthing";
+        example = "/home/yourUser";
+        description = ''
+          The path where synchronised directories will exist.
+        '';
+      };
+
+      configDir = let
+        cond = versionAtLeast config.system.stateVersion "19.03";
+      in mkOption {
+        type = types.path;
+        description = ''
+          The path where the settings and keys will exist.
+        '';
+        default = cfg.dataDir + optionalString cond "/.config/syncthing";
+        defaultText = literalDocBook ''
+          <variablelist>
+            <varlistentry>
+              <term><literal>stateVersion >= 19.03</literal></term>
+              <listitem>
+                <programlisting>
+                  config.${opt.dataDir} + "/.config/syncthing"
+                </programlisting>
+              </listitem>
+            </varlistentry>
+            <varlistentry>
+              <term>otherwise</term>
+              <listitem>
+                <programlisting>
+                  config.${opt.dataDir}
+                </programlisting>
+              </listitem>
+            </varlistentry>
+          </variablelist>
+        '';
+      };
+
+      extraFlags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "--reset-deltas" ];
+        description = ''
+          Extra flags passed to the syncthing command in the service definition.
+        '';
+      };
+
+      openDefaultPorts = mkOption {
+        type = types.bool;
+        default = false;
+        example = true;
+        description = ''
+          Whether to open the default ports in the firewall: TCP/UDP 22000 for transfers
+          and UDP 21027 for discovery.
+
+          If multiple users are running Syncthing on this machine, you will need
+          to manually open a set of ports for each instance and leave this disabled.
+          Alternatively, if you are running only a single instance on this machine
+          using the default ports, enable this.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.syncthing;
+        defaultText = literalExpression "pkgs.syncthing";
+        description = ''
+          The Syncthing package to use.
+        '';
+      };
+    };
+  };
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "syncthing" "useInotify" ] ''
+      This option was removed because Syncthing now has the inotify functionality included under the name "fswatcher".
+      It can be enabled on a per-folder basis through the web interface.
+    '')
+  ] ++ map (o:
+    mkRenamedOptionModule [ "services" "syncthing" "declarative" o ] [ "services" "syncthing" o ]
+  ) [ "cert" "key" "devices" "folders" "overrideDevices" "overrideFolders" "extraOptions"];
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    networking.firewall = mkIf cfg.openDefaultPorts {
+      allowedTCPPorts = [ 22000 ];
+      allowedUDPPorts = [ 21027 22000 ];
+    };
+
+    systemd.packages = [ pkgs.syncthing ];
+
+    users.users = mkIf (cfg.systemService && cfg.user == defaultUser) {
+      ${defaultUser} =
+        { group = cfg.group;
+          home  = cfg.dataDir;
+          createHome = true;
+          uid = config.ids.uids.syncthing;
+          description = "Syncthing daemon user";
+        };
+    };
+
+    users.groups = mkIf (cfg.systemService && cfg.group == defaultGroup) {
+      ${defaultGroup}.gid =
+        config.ids.gids.syncthing;
+    };
+
+    systemd.services = {
+      syncthing = mkIf cfg.systemService {
+        description = "Syncthing service";
+        after = [ "network.target" ];
+        environment = {
+          STNORESTART = "yes";
+          STNOUPGRADE = "yes";
+          inherit (cfg) all_proxy;
+        } // config.networking.proxy.envVars;
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          Restart = "on-failure";
+          SuccessExitStatus = "2 3 4";
+          RestartForceExitStatus="3 4";
+          User = cfg.user;
+          Group = cfg.group;
+          ExecStartPre = mkIf (cfg.cert != null || cfg.key != null)
+            "+${pkgs.writers.writeBash "syncthing-copy-keys" ''
+              install -dm700 -o ${cfg.user} -g ${cfg.group} ${cfg.configDir}
+              ${optionalString (cfg.cert != null) ''
+                install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.cert} ${cfg.configDir}/cert.pem
+              ''}
+              ${optionalString (cfg.key != null) ''
+                install -Dm400 -o ${cfg.user} -g ${cfg.group} ${toString cfg.key} ${cfg.configDir}/key.pem
+              ''}
+            ''}"
+          ;
+          ExecStart = ''
+            ${cfg.package}/bin/syncthing \
+              -no-browser \
+              -gui-address=${cfg.guiAddress} \
+              -home=${cfg.configDir} ${escapeShellArgs cfg.extraFlags}
+          '';
+          MemoryDenyWriteExecute = true;
+          NoNewPrivileges = true;
+          PrivateDevices = true;
+          PrivateMounts = true;
+          PrivateTmp = true;
+          PrivateUsers = true;
+          ProtectControlGroups = true;
+          ProtectHostname = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          CapabilityBoundingSet = [
+            "~CAP_SYS_PTRACE" "~CAP_SYS_ADMIN"
+            "~CAP_SETGID" "~CAP_SETUID" "~CAP_SETPCAP"
+            "~CAP_SYS_TIME" "~CAP_KILL"
+          ];
+        };
+      };
+      syncthing-init = mkIf (
+        cfg.devices != {} || cfg.folders != {} || cfg.extraOptions != {}
+      ) {
+        description = "Syncthing configuration updater";
+        requisite = [ "syncthing.service" ];
+        after = [ "syncthing.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = {
+          User = cfg.user;
+          RemainAfterExit = true;
+          Type = "oneshot";
+          ExecStart = updateConfig;
+        };
+      };
+
+      syncthing-resume = {
+        wantedBy = [ "suspend.target" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/tailscale.nix b/nixos/modules/services/networking/tailscale.nix
new file mode 100644
index 00000000000..3f41646bf01
--- /dev/null
+++ b/nixos/modules/services/networking/tailscale.nix
@@ -0,0 +1,44 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.tailscale;
+in {
+  meta.maintainers = with maintainers; [ danderson mbaillie ];
+
+  options.services.tailscale = {
+    enable = mkEnableOption "Tailscale client daemon";
+
+    port = mkOption {
+      type = types.port;
+      default = 41641;
+      description = "The port to listen on for tunnel traffic (0=autoselect).";
+    };
+
+    interfaceName = mkOption {
+      type = types.str;
+      default = "tailscale0";
+      description = ''The interface name for tunnel traffic. Use "userspace-networking" (beta) to not use TUN.'';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.tailscale;
+      defaultText = literalExpression "pkgs.tailscale";
+      description = "The package to use for tailscale";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ]; # for the CLI
+    systemd.packages = [ cfg.package ];
+    systemd.services.tailscaled = {
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.openresolv pkgs.procps ];
+      serviceConfig.Environment = [
+        "PORT=${toString cfg.port}"
+        ''"FLAGS=--tun ${lib.escapeShellArg cfg.interfaceName}"''
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/tcpcrypt.nix b/nixos/modules/services/networking/tcpcrypt.nix
new file mode 100644
index 00000000000..5a91054e166
--- /dev/null
+++ b/nixos/modules/services/networking/tcpcrypt.nix
@@ -0,0 +1,80 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.networking.tcpcrypt;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    networking.tcpcrypt.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable opportunistic TCP encryption. If the other end
+        speaks Tcpcrypt, then your traffic will be encrypted; otherwise
+        it will be sent in clear text. Thus, Tcpcrypt alone provides no
+        guarantees -- it is best effort. If, however, a Tcpcrypt
+        connection is successful and any attackers that exist are
+        passive, then Tcpcrypt guarantees privacy.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users.tcpcryptd = {
+      uid = config.ids.uids.tcpcryptd;
+      description = "tcpcrypt daemon user";
+    };
+
+    systemd.services.tcpcrypt = {
+      description = "tcpcrypt";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      path = [ pkgs.iptables pkgs.tcpcrypt pkgs.procps ];
+
+      preStart = ''
+        mkdir -p /run/tcpcryptd
+        chown tcpcryptd /run/tcpcryptd
+        sysctl -n net.ipv4.tcp_ecn > /run/tcpcryptd/pre-tcpcrypt-ecn-state
+        sysctl -w net.ipv4.tcp_ecn=0
+
+        iptables -t raw -N nixos-tcpcrypt
+        iptables -t raw -A nixos-tcpcrypt -p tcp -m mark --mark 0x0/0x10 -j NFQUEUE --queue-num 666
+        iptables -t raw -I PREROUTING -j nixos-tcpcrypt
+
+        iptables -t mangle -N nixos-tcpcrypt
+        iptables -t mangle -A nixos-tcpcrypt -p tcp -m mark --mark 0x0/0x10 -j NFQUEUE --queue-num 666
+        iptables -t mangle -I POSTROUTING -j nixos-tcpcrypt
+      '';
+
+      script = "tcpcryptd -x 0x10";
+
+      postStop = ''
+        if [ -f /run/tcpcryptd/pre-tcpcrypt-ecn-state ]; then
+          sysctl -w net.ipv4.tcp_ecn=$(cat /run/tcpcryptd/pre-tcpcrypt-ecn-state)
+        fi
+
+        iptables -t mangle -D POSTROUTING -j nixos-tcpcrypt || true
+        iptables -t raw -D PREROUTING -j nixos-tcpcrypt || true
+
+        iptables -t raw -F nixos-tcpcrypt || true
+        iptables -t raw -X nixos-tcpcrypt || true
+
+        iptables -t mangle -F nixos-tcpcrypt || true
+        iptables -t mangle -X nixos-tcpcrypt || true
+      '';
+    };
+  };
+
+}
diff --git a/nixos/modules/services/networking/teamspeak3.nix b/nixos/modules/services/networking/teamspeak3.nix
new file mode 100644
index 00000000000..c0ed08282aa
--- /dev/null
+++ b/nixos/modules/services/networking/teamspeak3.nix
@@ -0,0 +1,160 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  ts3 = pkgs.teamspeak_server;
+  cfg = config.services.teamspeak3;
+  user = "teamspeak";
+  group = "teamspeak";
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.teamspeak3 = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run the Teamspeak3 voice communication server daemon.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/teamspeak3-server";
+        description = ''
+          Directory to store TS3 database and other state/data files.
+        '';
+      };
+
+      logPath = mkOption {
+        type = types.path;
+        default = "/var/log/teamspeak3-server/";
+        description = ''
+          Directory to store log files in.
+        '';
+      };
+
+      voiceIP = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "[::]";
+        description = ''
+          IP on which the server instance will listen for incoming voice connections. Defaults to any IP.
+        '';
+      };
+
+      defaultVoicePort = mkOption {
+        type = types.int;
+        default = 9987;
+        description = ''
+          Default UDP port for clients to connect to virtual servers - used for first virtual server, subsequent ones will open on incrementing port numbers by default.
+        '';
+      };
+
+      fileTransferIP = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "[::]";
+        description = ''
+          IP on which the server instance will listen for incoming file transfer connections. Defaults to any IP.
+        '';
+      };
+
+      fileTransferPort = mkOption {
+        type = types.int;
+        default = 30033;
+        description = ''
+          TCP port opened for file transfers.
+        '';
+      };
+
+      queryIP = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "0.0.0.0";
+        description = ''
+          IP on which the server instance will listen for incoming ServerQuery connections. Defaults to any IP.
+        '';
+      };
+
+      queryPort = mkOption {
+        type = types.int;
+        default = 10011;
+        description = ''
+          TCP port opened for ServerQuery connections.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the TeamSpeak3 server.";
+      };
+
+      openFirewallServerQuery = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the TeamSpeak3 serverquery (administration) system. Requires openFirewall.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    users.users.teamspeak = {
+      description = "Teamspeak3 voice communication server daemon";
+      group = group;
+      uid = config.ids.uids.teamspeak;
+      home = cfg.dataDir;
+      createHome = true;
+    };
+
+    users.groups.teamspeak = {
+      gid = config.ids.gids.teamspeak;
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.logPath}' - ${user} ${group} - -"
+    ];
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.fileTransferPort ] ++ optionals (cfg.openFirewallServerQuery) [ cfg.queryPort (cfg.queryPort + 11) ];
+      # subsequent vServers will use the incremented voice port, let's just open the next 10
+      allowedUDPPortRanges = [ { from = cfg.defaultVoicePort; to = cfg.defaultVoicePort + 10; } ];
+    };
+
+    systemd.services.teamspeak3-server = {
+      description = "Teamspeak3 voice communication server daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = ''
+          ${ts3}/bin/ts3server \
+            dbsqlpath=${ts3}/lib/teamspeak/sql/ logpath=${cfg.logPath} \
+            ${optionalString (cfg.voiceIP != null) "voice_ip=${cfg.voiceIP}"} \
+            default_voice_port=${toString cfg.defaultVoicePort} \
+            ${optionalString (cfg.fileTransferIP != null) "filetransfer_ip=${cfg.fileTransferIP}"} \
+            filetransfer_port=${toString cfg.fileTransferPort} \
+            ${optionalString (cfg.queryIP != null) "query_ip=${cfg.queryIP}"} \
+            query_port=${toString cfg.queryPort} license_accepted=1
+        '';
+        WorkingDirectory = cfg.dataDir;
+        User = user;
+        Group = group;
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ arobyn ];
+}
diff --git a/nixos/modules/services/networking/tedicross.nix b/nixos/modules/services/networking/tedicross.nix
new file mode 100644
index 00000000000..c7830289dca
--- /dev/null
+++ b/nixos/modules/services/networking/tedicross.nix
@@ -0,0 +1,100 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  dataDir = "/var/lib/tedicross";
+  cfg = config.services.tedicross;
+  configJSON = pkgs.writeText "tedicross-settings.json" (builtins.toJSON cfg.config);
+  configYAML = pkgs.runCommand "tedicross-settings.yaml" { preferLocalBuild = true; } ''
+    ${pkgs.remarshal}/bin/json2yaml -i ${configJSON} -o $out
+  '';
+
+in {
+  options = {
+    services.tedicross = {
+      enable = mkEnableOption "the TediCross Telegram-Discord bridge service";
+
+      config = mkOption {
+        type = types.attrs;
+        # from https://github.com/TediCross/TediCross/blob/master/example.settings.yaml
+        example = literalExpression ''
+          {
+            telegram = {
+              useFirstNameInsteadOfUsername = false;
+              colonAfterSenderName = false;
+              skipOldMessages = true;
+              sendEmojiWithStickers = true;
+            };
+            discord = {
+              useNickname = false;
+              skipOldMessages = true;
+              displayTelegramReplies = "embed";
+              replyLength = 100;
+            };
+            bridges = [
+              {
+                name = "Default bridge";
+                direction = "both";
+                telegram = {
+                  chatId = -123456789;
+                  relayJoinMessages = true;
+                  relayLeaveMessages = true;
+                  sendUsernames = true;
+                  ignoreCommands = true;
+                };
+                discord = {
+                  serverId = "DISCORD_SERVER_ID";
+                  channelId = "DISCORD_CHANNEL_ID";
+                  relayJoinMessages = true;
+                  relayLeaveMessages = true;
+                  sendUsernames = true;
+                  crossDeleteOnTelegram = true;
+                };
+              }
+            ];
+
+            debug = false;
+          }
+        '';
+        description = ''
+          <filename>settings.yaml</filename> configuration as a Nix attribute set.
+          Secret tokens should be specified using <option>environmentFile</option>
+          instead of this world-readable file.
+        '';
+      };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          File containing environment variables to be passed to the TediCross service,
+          in which secret tokens can be specified securely using the
+          <literal>TELEGRAM_BOT_TOKEN</literal> and <literal>DISCORD_BOT_TOKEN</literal>
+          keys.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # from https://github.com/TediCross/TediCross/blob/master/guides/autostart/Linux.md
+    systemd.services.tedicross = {
+      description = "TediCross Telegram-Discord bridge service";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${pkgs.nodePackages.tedicross}/bin/tedicross --config='${configYAML}' --data-dir='${dataDir}'";
+        Restart = "always";
+        DynamicUser = true;
+        StateDirectory = baseNameOf dataDir;
+        EnvironmentFile = cfg.environmentFile;
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ pacien ];
+}
+
diff --git a/nixos/modules/services/networking/teleport.nix b/nixos/modules/services/networking/teleport.nix
new file mode 100644
index 00000000000..45479162180
--- /dev/null
+++ b/nixos/modules/services/networking/teleport.nix
@@ -0,0 +1,99 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.teleport;
+  settingsYaml = pkgs.formats.yaml { };
+in
+{
+  options = {
+    services.teleport = with lib.types; {
+      enable = mkEnableOption "the Teleport service";
+
+      settings = mkOption {
+        type = settingsYaml.type;
+        default = { };
+        example = literalExpression ''
+          {
+            teleport = {
+              nodename = "client";
+              advertise_ip = "192.168.1.2";
+              auth_token = "60bdc117-8ff4-478d-95e4-9914597847eb";
+              auth_servers = [ "192.168.1.1:3025" ];
+              log.severity = "DEBUG";
+            };
+            ssh_service = {
+              enabled = true;
+              labels = {
+                role = "client";
+              };
+            };
+            proxy_service.enabled = false;
+            auth_service.enabled = false;
+          }
+        '';
+        description = ''
+          Contents of the <literal>teleport.yaml</literal> config file.
+          The <literal>--config</literal> arguments will only be passed if this set is not empty.
+
+          See <link xlink:href="https://goteleport.com/docs/setup/reference/config/"/>.
+        '';
+      };
+
+      insecure.enable = mkEnableOption ''
+        starting teleport in insecure mode.
+
+        This is dangerous!
+        Sensitive information will be logged to console and certificates will not be verified.
+        Proceed with caution!
+
+        Teleport starts with disabled certificate validation on Proxy Service, validation still occurs on Auth Service
+      '';
+
+      diag = {
+        enable = mkEnableOption ''
+          endpoints for monitoring purposes.
+
+          See <link xlink:href="https://goteleport.com/docs/setup/admin/troubleshooting/#troubleshooting/"/>
+        '';
+
+        addr = mkOption {
+          type = str;
+          default = "127.0.0.1";
+          description = "Metrics and diagnostics address.";
+        };
+
+        port = mkOption {
+          type = int;
+          default = 3000;
+          description = "Metrics and diagnostics port.";
+        };
+      };
+    };
+  };
+
+  config = mkIf config.services.teleport.enable {
+    environment.systemPackages = [ pkgs.teleport ];
+
+    systemd.services.teleport = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.teleport}/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}"}
+        '';
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        LimitNOFILE = 65536;
+        Restart = "always";
+        RestartSec = "5s";
+        RuntimeDirectory = "teleport";
+        Type = "simple";
+      };
+    };
+  };
+}
+
diff --git a/nixos/modules/services/networking/tetrd.nix b/nixos/modules/services/networking/tetrd.nix
new file mode 100644
index 00000000000..0801ce12924
--- /dev/null
+++ b/nixos/modules/services/networking/tetrd.nix
@@ -0,0 +1,96 @@
+{ config, lib, pkgs, ... }:
+
+{
+  options.services.tetrd.enable = lib.mkEnableOption "tetrd";
+
+  config = lib.mkIf config.services.tetrd.enable {
+    environment = {
+      systemPackages = [ pkgs.tetrd ];
+      etc."resolv.conf".source = "/etc/tetrd/resolv.conf";
+    };
+
+    systemd = {
+      tmpfiles.rules = [ "f /etc/tetrd/resolv.conf - - -" ];
+
+      services.tetrd = {
+        description = pkgs.tetrd.meta.description;
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = {
+          ExecStart = "${pkgs.tetrd}/opt/Tetrd/bin/tetrd";
+          Restart = "always";
+          RuntimeDirectory = "tetrd";
+          RootDirectory = "/run/tetrd";
+          DynamicUser = true;
+          UMask = "006";
+          DeviceAllow = "usb_device";
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          NoNewPrivileges = true;
+          PrivateMounts = true;
+          PrivateNetwork = lib.mkDefault false;
+          PrivateTmp = true;
+          PrivateUsers = lib.mkDefault false;
+          ProtectClock = lib.mkDefault false;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectProc = "invisible";
+          ProtectSystem = "strict";
+          RemoveIPC = true;
+          RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          SystemCallArchitectures = "native";
+
+          SystemCallFilter = [
+            "@system-service"
+            "~@aio"
+            "~@chown"
+            "~@clock"
+            "~@cpu-emulation"
+            "~@debug"
+            "~@keyring"
+            "~@memlock"
+            "~@module"
+            "~@mount"
+            "~@obsolete"
+            "~@pkey"
+            "~@raw-io"
+            "~@reboot"
+            "~@swap"
+            "~@sync"
+          ];
+
+          BindReadOnlyPaths = [
+            builtins.storeDir
+            "/etc/ssl"
+            "/etc/static/ssl"
+            "${pkgs.nettools}/bin/route:/usr/bin/route"
+            "${pkgs.nettools}/bin/ifconfig:/usr/bin/ifconfig"
+          ];
+
+          BindPaths = [
+            "/etc/tetrd/resolv.conf:/etc/resolv.conf"
+            "/run"
+            "/var/log"
+          ];
+
+          CapabilityBoundingSet = [
+            "CAP_DAC_OVERRIDE"
+            "CAP_NET_ADMIN"
+          ];
+
+          AmbientCapabilities = [
+            "CAP_DAC_OVERRIDE"
+            "CAP_NET_ADMIN"
+          ];
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/tftpd.nix b/nixos/modules/services/networking/tftpd.nix
new file mode 100644
index 00000000000..c9c0a2b321d
--- /dev/null
+++ b/nixos/modules/services/networking/tftpd.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.tftpd.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable tftpd, a Trivial File Transfer Protocol server.
+        The server will be run as an xinetd service.
+      '';
+    };
+
+    services.tftpd.path = mkOption {
+      type = types.path;
+      default = "/srv/tftp";
+      description = ''
+        Where the tftp server files are stored.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.tftpd.enable {
+
+    services.xinetd.enable = true;
+
+    services.xinetd.services = singleton
+      { name = "tftp";
+        protocol = "udp";
+        server = "${pkgs.netkittftp}/sbin/in.tftpd";
+        serverArgs = "${config.services.tftpd.path}";
+      };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/thelounge.nix b/nixos/modules/services/networking/thelounge.nix
new file mode 100644
index 00000000000..a5118fd8b33
--- /dev/null
+++ b/nixos/modules/services/networking/thelounge.nix
@@ -0,0 +1,106 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.thelounge;
+  dataDir = "/var/lib/thelounge";
+  configJsData = "module.exports = " + builtins.toJSON (
+    { inherit (cfg) public port; } // cfg.extraConfig
+  );
+  pluginManifest = {
+    dependencies = builtins.listToAttrs (builtins.map (pkg: { name = getName pkg; value = getVersion pkg; }) cfg.plugins);
+  };
+  plugins = pkgs.runCommandLocal "thelounge-plugins" { } ''
+    mkdir -p $out/node_modules
+    echo ${escapeShellArg (builtins.toJSON pluginManifest)} >> $out/package.json
+    ${concatMapStringsSep "\n" (pkg: ''
+    ln -s ${pkg}/lib/node_modules/${getName pkg} $out/node_modules/${getName pkg}
+    '') cfg.plugins}
+  '';
+in
+{
+  imports = [ (mkRemovedOptionModule [ "services" "thelounge" "private" ] "The option was renamed to `services.thelounge.public` to follow upstream changes.") ];
+
+  options.services.thelounge = {
+    enable = mkEnableOption "The Lounge web IRC client";
+
+    public = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Make your The Lounge instance public.
+        Setting this to <literal>false</literal> will require you to configure user
+        accounts by using the (<command>thelounge</command>) command or by adding
+        entries in <filename>${dataDir}/users</filename>. You might need to restart
+        The Lounge after making changes to the state directory.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 9000;
+      description = "TCP port to listen on for http connections.";
+    };
+
+    extraConfig = mkOption {
+      default = { };
+      type = types.attrs;
+      example = literalExpression ''{
+        reverseProxy = true;
+        defaults = {
+          name = "Your Network";
+          host = "localhost";
+          port = 6697;
+        };
+      }'';
+      description = ''
+        The Lounge's <filename>config.js</filename> contents as attribute set (will be
+        converted to JSON to generate the configuration file).
+
+        The options defined here will be merged to the default configuration file.
+        Note: In case of duplicate configuration, options from <option>extraConfig</option> have priority.
+
+        Documentation: <link xlink:href="https://thelounge.chat/docs/server/configuration" />
+      '';
+    };
+
+    plugins = mkOption {
+      default = [ ];
+      type = types.listOf types.package;
+      example = literalExpression "[ pkgs.theLoungePlugins.themes.solarized ]";
+      description = ''
+        The Lounge plugins to install. Plugins can be found in
+        <literal>pkgs.theLoungePlugins.plugins</literal> and <literal>pkgs.theLoungePlugins.themes</literal>.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.thelounge = {
+      description = "The Lounge service user";
+      group = "thelounge";
+      isSystemUser = true;
+    };
+
+    users.groups.thelounge = { };
+
+    systemd.services.thelounge = {
+      description = "The Lounge web IRC client";
+      wantedBy = [ "multi-user.target" ];
+      preStart = "ln -sf ${pkgs.writeText "config.js" configJsData} ${dataDir}/config.js";
+      environment.THELOUNGE_PACKAGES = mkIf (cfg.plugins != [ ]) "${plugins}";
+      serviceConfig = {
+        User = "thelounge";
+        StateDirectory = baseNameOf dataDir;
+        ExecStart = "${pkgs.thelounge}/bin/thelounge start";
+      };
+    };
+
+    environment.systemPackages = [ pkgs.thelounge ];
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ winter ];
+  };
+}
diff --git a/nixos/modules/services/networking/tinc.nix b/nixos/modules/services/networking/tinc.nix
new file mode 100644
index 00000000000..31731b60d48
--- /dev/null
+++ b/nixos/modules/services/networking/tinc.nix
@@ -0,0 +1,439 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.tinc;
+
+  mkValueString = value:
+    if value == true then "yes"
+    else if value == false then "no"
+    else generators.mkValueStringDefault { } value;
+
+  toTincConf = generators.toKeyValue {
+    listsAsDuplicateKeys = true;
+    mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } "=";
+  };
+
+  tincConfType = with types;
+    let
+      valueType = oneOf [ bool str int ];
+    in
+    attrsOf (either valueType (listOf valueType));
+
+  addressSubmodule = {
+    options = {
+      address = mkOption {
+        type = types.str;
+        description = "The external IP address or hostname where the host can be reached.";
+      };
+
+      port = mkOption {
+        type = types.nullOr types.port;
+        default = null;
+        description = ''
+          The port where the host can be reached.
+
+          If no port is specified, the default Port is used.
+        '';
+      };
+    };
+  };
+
+  subnetSubmodule = {
+    options = {
+      address = mkOption {
+        type = types.str;
+        description = ''
+          The subnet of this host.
+
+          Subnets can either be single MAC, IPv4 or IPv6 addresses, in which case
+          a subnet consisting of only that single address is assumed, or they can
+          be a IPv4 or IPv6 network address with a prefix length.
+
+          IPv4 subnets are notated like 192.168.1.0/24, IPv6 subnets are notated
+          like fec0:0:0:1::/64. MAC addresses are notated like 0:1a:2b:3c:4d:5e.
+
+          Note that subnets like 192.168.1.1/24 are invalid.
+        '';
+      };
+
+      prefixLength = mkOption {
+        type = with types; nullOr (addCheck int (n: n >= 0 && n <= 128));
+        default = null;
+        description = ''
+          The prefix length of the subnet.
+
+          If null, a subnet consisting of only that single address is assumed.
+
+          This conforms to standard CIDR notation as described in RFC1519.
+        '';
+      };
+
+      weight = mkOption {
+        type = types.ints.unsigned;
+        default = 10;
+        description = ''
+          Indicates the priority over identical Subnets owned by different nodes.
+
+          Lower values indicate higher priority. Packets will be sent to the
+          node with the highest priority, unless that node is not reachable, in
+          which case the node with the next highest priority will be tried, and
+          so on.
+        '';
+      };
+    };
+  };
+
+  hostSubmodule = { config, ... }: {
+    options = {
+      addresses = mkOption {
+        type = types.listOf (types.submodule addressSubmodule);
+        default = [ ];
+        description = ''
+          The external address where the host can be reached. This will set this
+          host's <option>settings.Address</option> option.
+
+          This variable is only required if you want to connect to this host.
+        '';
+      };
+
+      subnets = mkOption {
+        type = types.listOf (types.submodule subnetSubmodule);
+        default = [ ];
+        description = ''
+          The subnets which this tinc daemon will serve. This will set this
+          host's <option>settings.Subnet</option> option.
+
+          Tinc tries to look up which other daemon it should send a packet to by
+          searching the appropriate subnet. If the packet matches a subnet, it
+          will be sent to the daemon who has this subnet in his host
+          configuration file.
+        '';
+      };
+
+      rsaPublicKey = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Legacy RSA public key of the host in PEM format, including start and
+          end markers.
+
+          This will be appended as-is in the host's configuration file.
+
+          The ed25519 public key can be specified using the
+          <option>settings.Ed25519PublicKey</option> option instead.
+        '';
+      };
+
+      settings = mkOption {
+        default = { };
+        type = types.submodule { freeformType = tincConfType; };
+        description = ''
+          Configuration for this host.
+
+          See <link xlink:href="https://tinc-vpn.org/documentation-1.1/Host-configuration-variables.html"/>
+          for supported values.
+        '';
+      };
+    };
+
+    config.settings = {
+      Address = mkDefault (map
+        (address: "${address.address} ${toString address.port}")
+        config.addresses);
+
+      Subnet = mkDefault (map
+        (subnet:
+          if subnet.prefixLength == null then "${subnet.address}#${toString subnet.weight}"
+          else "${subnet.address}/${toString subnet.prefixLength}#${toString subnet.weight}")
+        config.subnets);
+    };
+  };
+
+in
+{
+
+  ###### interface
+
+  options = {
+
+    services.tinc = {
+
+      networks = mkOption {
+        default = { };
+        type = with types; attrsOf (submodule ({ config, ... }: {
+          options = {
+
+            extraConfig = mkOption {
+              default = "";
+              type = types.lines;
+              description = ''
+                Extra lines to add to the tinc service configuration file.
+
+                Note that using the declarative <option>service.tinc.networks.&lt;name&gt;.settings</option>
+                option is preferred.
+              '';
+            };
+
+            name = mkOption {
+              default = null;
+              type = types.nullOr types.str;
+              description = ''
+                The name of the node which is used as an identifier when communicating
+                with the remote nodes in the mesh. If null then the hostname of the system
+                is used to derive a name (note that tinc may replace non-alphanumeric characters in
+                hostnames by underscores).
+              '';
+            };
+
+            ed25519PrivateKeyFile = mkOption {
+              default = null;
+              type = types.nullOr types.path;
+              description = ''
+                Path of the private ed25519 keyfile.
+              '';
+            };
+
+            rsaPrivateKeyFile = mkOption {
+              default = null;
+              type = types.nullOr types.path;
+              description = ''
+                Path of the private RSA keyfile.
+              '';
+            };
+
+            debugLevel = mkOption {
+              default = 0;
+              type = types.addCheck types.int (l: l >= 0 && l <= 5);
+              description = ''
+                The amount of debugging information to add to the log. 0 means little
+                logging while 5 is the most logging. <command>man tincd</command> for
+                more details.
+              '';
+            };
+
+            hosts = mkOption {
+              default = { };
+              type = types.attrsOf types.lines;
+              description = ''
+                The name of the host in the network as well as the configuration for that host.
+                This name should only contain alphanumerics and underscores.
+
+                Note that using the declarative <option>service.tinc.networks.&lt;name&gt;.hostSettings</option>
+                option is preferred.
+              '';
+            };
+
+            hostSettings = mkOption {
+              default = { };
+              example = literalExpression ''
+                {
+                  host1 = {
+                    addresses = [
+                      { address = "192.168.1.42"; }
+                      { address = "192.168.1.42"; port = 1655; }
+                    ];
+                    subnets = [ { address = "10.0.0.42"; } ];
+                    rsaPublicKey = "...";
+                    settings = {
+                      Ed25519PublicKey = "...";
+                    };
+                  };
+                  host2 = {
+                    subnets = [ { address = "10.0.1.0"; prefixLength = 24; weight = 2; } ];
+                    rsaPublicKey = "...";
+                    settings = {
+                      Compression = 10;
+                    };
+                  };
+                }
+              '';
+              type = types.attrsOf (types.submodule hostSubmodule);
+              description = ''
+                The name of the host in the network as well as the configuration for that host.
+                This name should only contain alphanumerics and underscores.
+              '';
+            };
+
+            interfaceType = mkOption {
+              default = "tun";
+              type = types.enum [ "tun" "tap" ];
+              description = ''
+                The type of virtual interface used for the network connection.
+              '';
+            };
+
+            listenAddress = mkOption {
+              default = null;
+              type = types.nullOr types.str;
+              description = ''
+                The ip address to listen on for incoming connections.
+              '';
+            };
+
+            bindToAddress = mkOption {
+              default = null;
+              type = types.nullOr types.str;
+              description = ''
+                The ip address to bind to (both listen on and send packets from).
+              '';
+            };
+
+            package = mkOption {
+              type = types.package;
+              default = pkgs.tinc_pre;
+              defaultText = literalExpression "pkgs.tinc_pre";
+              description = ''
+                The package to use for the tinc daemon's binary.
+              '';
+            };
+
+            chroot = mkOption {
+              default = false;
+              type = types.bool;
+              description = ''
+                Change process root directory to the directory where the config file is located (/etc/tinc/netname/), for added security.
+                The chroot is performed after all the initialization is done, after writing pid files and opening network sockets.
+
+                Note that this currently breaks dns resolution and tinc can't run scripts anymore (such as tinc-down or host-up), unless it is setup to be runnable inside chroot environment.
+              '';
+            };
+
+            settings = mkOption {
+              default = { };
+              type = types.submodule { freeformType = tincConfType; };
+              example = literalExpression ''
+                {
+                  Interface = "custom.interface";
+                  DirectOnly = true;
+                  Mode = "switch";
+                }
+              '';
+              description = ''
+                Configuration of the Tinc daemon for this network.
+
+                See <link xlink:href="https://tinc-vpn.org/documentation-1.1/Main-configuration-variables.html"/>
+                for supported values.
+              '';
+            };
+          };
+
+          config = {
+            hosts = mapAttrs
+              (hostname: host: ''
+                ${toTincConf host.settings}
+                ${host.rsaPublicKey}
+              '')
+              config.hostSettings;
+
+            settings = {
+              DeviceType = mkDefault config.interfaceType;
+              Name = mkDefault (if config.name == null then "$HOST" else config.name);
+              Ed25519PrivateKeyFile = mkIf (config.ed25519PrivateKeyFile != null) (mkDefault config.ed25519PrivateKeyFile);
+              PrivateKeyFile = mkIf (config.rsaPrivateKeyFile != null) (mkDefault config.rsaPrivateKeyFile);
+              ListenAddress = mkIf (config.listenAddress != null) (mkDefault config.listenAddress);
+              BindToAddress = mkIf (config.bindToAddress != null) (mkDefault config.bindToAddress);
+            };
+          };
+        }));
+
+        description = ''
+          Defines the tinc networks which will be started.
+          Each network invokes a different daemon.
+        '';
+      };
+    };
+
+  };
+
+
+  ###### 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}
+            '';
+          };
+        }
+      ));
+
+    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";
+        buildInputs = [ 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}" {}
+    );
+  };
+
+  meta.maintainers = with maintainers; [ minijackson mic92 ];
+}
diff --git a/nixos/modules/services/networking/tinydns.nix b/nixos/modules/services/networking/tinydns.nix
new file mode 100644
index 00000000000..2c44ad49296
--- /dev/null
+++ b/nixos/modules/services/networking/tinydns.nix
@@ -0,0 +1,59 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  ###### interface
+
+  options = {
+    services.tinydns = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to run the tinydns dns server";
+      };
+
+      data = mkOption {
+        type = types.lines;
+        default = "";
+        description = "The DNS data to serve, in the format described by tinydns-data(8)";
+      };
+
+      ip = mkOption {
+        default = "0.0.0.0";
+        type = types.str;
+        description = "IP address on which to listen for connections";
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf config.services.tinydns.enable {
+    environment.systemPackages = [ pkgs.djbdns ];
+
+    users.users.tinydns = {
+      isSystemUser = true;
+      group = "tinydns";
+    };
+    users.groups.tinydns = {};
+
+    systemd.services.tinydns = {
+      description = "djbdns tinydns server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = with pkgs; [ daemontools djbdns ];
+      preStart = ''
+        rm -rf /var/lib/tinydns
+        tinydns-conf tinydns tinydns /var/lib/tinydns ${config.services.tinydns.ip}
+        cd /var/lib/tinydns/root/
+        ln -sf ${pkgs.writeText "tinydns-data" config.services.tinydns.data} data
+        tinydns-data
+      '';
+      script = ''
+        cd /var/lib/tinydns
+        exec ./run
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/tox-bootstrapd.nix b/nixos/modules/services/networking/tox-bootstrapd.nix
new file mode 100644
index 00000000000..7c13724e084
--- /dev/null
+++ b/nixos/modules/services/networking/tox-bootstrapd.nix
@@ -0,0 +1,74 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  WorkingDirectory = "/var/lib/tox-bootstrapd";
+  PIDFile = "${WorkingDirectory}/pid";
+
+  pkg = pkgs.libtoxcore;
+  cfg = config.services.toxBootstrapd;
+  cfgFile = builtins.toFile "tox-bootstrapd.conf"
+    ''
+      port = ${toString cfg.port}
+      keys_file_path = "${WorkingDirectory}/keys"
+      pid_file_path = "${PIDFile}"
+      ${cfg.extraConfig}
+    '';
+in
+{
+  options =
+    { services.toxBootstrapd =
+        { enable = mkOption {
+            type = types.bool;
+            default = false;
+            description =
+              ''
+                Whether to enable the Tox DHT bootstrap daemon.
+              '';
+          };
+
+          port = mkOption {
+            type = types.int;
+            default = 33445;
+            description = "Listening port (UDP).";
+          };
+
+          keysFile = mkOption {
+            type = types.str;
+            default = "${WorkingDirectory}/keys";
+            description = "Node key file.";
+          };
+
+          extraConfig = mkOption {
+            type = types.lines;
+            default = "";
+            description =
+              ''
+                Configuration for bootstrap daemon.
+                See <link xlink:href="https://github.com/irungentoo/toxcore/blob/master/other/bootstrap_daemon/tox-bootstrapd.conf"/>
+                and <link xlink:href="http://wiki.tox.im/Nodes"/>.
+             '';
+          };
+      };
+
+    };
+
+  config = mkIf config.services.toxBootstrapd.enable {
+
+    systemd.services.tox-bootstrapd = {
+      description = "Tox DHT bootstrap daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig =
+        { ExecStart = "${pkg}/bin/tox-bootstrapd --config=${cfgFile}";
+          Type = "forking";
+          inherit PIDFile WorkingDirectory;
+          AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
+          DynamicUser = true;
+          StateDirectory = "tox-bootstrapd";
+        };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/networking/tox-node.nix b/nixos/modules/services/networking/tox-node.nix
new file mode 100644
index 00000000000..c6e5c2d6e81
--- /dev/null
+++ b/nixos/modules/services/networking/tox-node.nix
@@ -0,0 +1,90 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+  pkg = pkgs.tox-node;
+  cfg = config.services.tox-node;
+  homeDir = "/var/lib/tox-node";
+
+  configFile = let
+    src = "${pkg.src}/dpkg/config.yml";
+    confJSON = pkgs.writeText "config.json" (
+      builtins.toJSON {
+        log-type = cfg.logType;
+        keys-file = cfg.keysFile;
+        udp-address = cfg.udpAddress;
+        tcp-addresses = cfg.tcpAddresses;
+        tcp-connections-limit = cfg.tcpConnectionLimit;
+        lan-discovery = cfg.lanDiscovery;
+        threads = cfg.threads;
+        motd = cfg.motd;
+      }
+    );
+  in with pkgs; runCommand "config.yml" {} ''
+    ${remarshal}/bin/remarshal -if yaml -of json ${src} -o src.json
+    ${jq}/bin/jq -s '(.[0] | with_entries( select(.key == "bootstrap-nodes"))) * .[1]' src.json ${confJSON} > $out
+  '';
+
+in {
+  options.services.tox-node = {
+    enable = mkEnableOption "Tox Node service";
+
+    logType = mkOption {
+      type = types.enum [ "Stderr" "Stdout" "Syslog" "None" ];
+      default = "Stderr";
+      description = "Logging implementation.";
+    };
+    keysFile = mkOption {
+      type = types.str;
+      default = "${homeDir}/keys";
+      description = "Path to the file where DHT keys are stored.";
+    };
+    udpAddress = mkOption {
+      type = types.str;
+      default = "0.0.0.0:33445";
+      description = "UDP address to run DHT node.";
+    };
+    tcpAddresses = mkOption {
+      type = types.listOf types.str;
+      default = [ "0.0.0.0:33445" ];
+      description = "TCP addresses to run TCP relay.";
+    };
+    tcpConnectionLimit = mkOption {
+      type = types.int;
+      default = 8192;
+      description = "Maximum number of active TCP connections relay can hold";
+    };
+    lanDiscovery = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Enable local network discovery.";
+    };
+    threads = mkOption {
+      type = types.int;
+      default = 1;
+      description = "Number of threads for execution";
+    };
+    motd = mkOption {
+      type = types.str;
+      default = "Hi from tox-rs! I'm up {{uptime}}. TCP: incoming {{tcp_packets_in}}, outgoing {{tcp_packets_out}}, UDP: incoming {{udp_packets_in}}, outgoing {{udp_packets_out}}";
+      description = "Message of the day";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.tox-node = {
+      description = "Tox Node";
+
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = "${pkg}/bin/tox-node config ${configFile}";
+        StateDirectory = "tox-node";
+        DynamicUser = true;
+        Restart = "always";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/toxvpn.nix b/nixos/modules/services/networking/toxvpn.nix
new file mode 100644
index 00000000000..18cf7672d5f
--- /dev/null
+++ b/nixos/modules/services/networking/toxvpn.nix
@@ -0,0 +1,70 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+  options = {
+    services.toxvpn = {
+      enable = mkEnableOption "toxvpn running on startup";
+
+      localip = mkOption {
+        type        = types.str;
+        default     = "10.123.123.1";
+        description = "your ip on the vpn";
+      };
+
+      port = mkOption {
+        type        = types.int;
+        default     = 33445;
+        description = "udp port for toxcore, port-forward to help with connectivity if you run many nodes behind one NAT";
+      };
+
+      auto_add_peers = mkOption {
+        type        = types.listOf types.str;
+        default     = [];
+        example     = [ "toxid1" "toxid2" ];
+        description = "peers to automatically connect to on startup";
+      };
+    };
+  };
+
+  config = mkIf config.services.toxvpn.enable {
+    systemd.services.toxvpn = {
+      description = "toxvpn daemon";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      preStart = ''
+        mkdir -p /run/toxvpn || true
+        chown toxvpn /run/toxvpn
+      '';
+
+      path = [ pkgs.toxvpn ];
+
+      script = ''
+        exec toxvpn -i ${config.services.toxvpn.localip} -l /run/toxvpn/control -u toxvpn -p ${toString config.services.toxvpn.port} ${lib.concatMapStringsSep " " (x: "-a ${x}") config.services.toxvpn.auto_add_peers}
+      '';
+
+      serviceConfig = {
+        KillMode  = "process";
+        Restart   = "on-success";
+        Type      = "notify";
+      };
+
+      restartIfChanged = false; # Likely to be used for remote admin
+    };
+
+    environment.systemPackages = [ pkgs.toxvpn ];
+
+    users.users = {
+      toxvpn = {
+        isSystemUser = true;
+        group = "toxvpn";
+        home       = "/var/lib/toxvpn";
+        createHome = true;
+      };
+    };
+    users.groups.toxvpn = {};
+  };
+}
diff --git a/nixos/modules/services/networking/trickster.nix b/nixos/modules/services/networking/trickster.nix
new file mode 100644
index 00000000000..e48bba8fa58
--- /dev/null
+++ b/nixos/modules/services/networking/trickster.nix
@@ -0,0 +1,113 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.trickster;
+in
+{
+
+  options = {
+    services.trickster = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable Trickster.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.trickster;
+        defaultText = literalExpression "pkgs.trickster";
+        description = ''
+          Package that should be used for trickster.
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          Path to configuration file.
+        '';
+      };
+
+      instance-id = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          Instance ID for when running multiple processes (default null).
+        '';
+      };
+
+      log-level = mkOption {
+        type = types.str;
+        default = "info";
+        description = ''
+          Level of Logging to use (debug, info, warn, error) (default "info").
+        '';
+      };
+
+      metrics-port = mkOption {
+        type = types.port;
+        default = 8082;
+        description = ''
+          Port that the /metrics endpoint will listen on.
+        '';
+      };
+
+      origin = mkOption {
+        type = types.str;
+        default = "http://prometheus:9090";
+        description = ''
+          URL to the Prometheus Origin. Enter it like you would in grafana, e.g., http://prometheus:9090 (default http://prometheus:9090).
+        '';
+      };
+
+      profiler-port = mkOption {
+        type = types.nullOr types.port;
+        default = null;
+        description = ''
+          Port that the /debug/pprof endpoint will listen on.
+        '';
+      };
+
+      proxy-port = mkOption {
+        type = types.port;
+        default = 9090;
+        description = ''
+          Port that the Proxy server will listen on.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.trickster = {
+      description = "Dashboard Accelerator for Prometheus";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = ''
+          ${cfg.package}/bin/trickster \
+          -log-level ${cfg.log-level} \
+          -metrics-port ${toString cfg.metrics-port} \
+          -origin ${cfg.origin} \
+          -proxy-port ${toString cfg.proxy-port} \
+          ${optionalString (cfg.configFile != null) "-config ${cfg.configFile}"} \
+          ${optionalString (cfg.profiler-port != null) "-profiler-port ${cfg.profiler-port}"} \
+          ${optionalString (cfg.instance-id != null) "-instance-id ${cfg.instance-id}"}
+        '';
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        Restart = "always";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ _1000101 ];
+
+}
diff --git a/nixos/modules/services/networking/tvheadend.nix b/nixos/modules/services/networking/tvheadend.nix
new file mode 100644
index 00000000000..19a10a03bd9
--- /dev/null
+++ b/nixos/modules/services/networking/tvheadend.nix
@@ -0,0 +1,63 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg     = config.services.tvheadend;
+    pidFile = "${config.users.users.tvheadend.home}/tvheadend.pid";
+in
+
+{
+  options = {
+    services.tvheadend = {
+      enable = mkEnableOption "Tvheadend";
+      httpPort = mkOption {
+        type        = types.int;
+        default     = 9981;
+        description = "Port to bind HTTP to.";
+      };
+
+      htspPort = mkOption {
+        type        = types.int;
+        default     = 9982;
+        description = "Port to bind HTSP to.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.tvheadend = {
+      description = "Tvheadend Service user";
+      home        = "/var/lib/tvheadend";
+      createHome  = true;
+      isSystemUser = true;
+      group = "tvheadend";
+    };
+    users.groups.tvheadend = {};
+
+    systemd.services.tvheadend = {
+      description = "Tvheadend TV streaming server";
+      wantedBy    = [ "multi-user.target" ];
+      after       = [ "network.target" ];
+
+      serviceConfig = {
+        Type         = "forking";
+        PIDFile      = pidFile;
+        Restart      = "always";
+        RestartSec   = 5;
+        User         = "tvheadend";
+        Group        = "video";
+        ExecStart    = ''
+                       ${pkgs.tvheadend}/bin/tvheadend \
+                       --http_port ${toString cfg.httpPort} \
+                       --htsp_port ${toString cfg.htspPort} \
+                       -f \
+                       -C \
+                       -p ${pidFile} \
+                       -u tvheadend \
+                       -g video
+                       '';
+        ExecStop     = "${pkgs.coreutils}/bin/rm ${pidFile}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/ucarp.nix b/nixos/modules/services/networking/ucarp.nix
new file mode 100644
index 00000000000..189e4f99cef
--- /dev/null
+++ b/nixos/modules/services/networking/ucarp.nix
@@ -0,0 +1,183 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.networking.ucarp;
+
+  ucarpExec = concatStringsSep " " (
+    [
+      "${cfg.package}/bin/ucarp"
+      "--interface=${cfg.interface}"
+      "--srcip=${cfg.srcIp}"
+      "--vhid=${toString cfg.vhId}"
+      "--passfile=${cfg.passwordFile}"
+      "--addr=${cfg.addr}"
+      "--advbase=${toString cfg.advBase}"
+      "--advskew=${toString cfg.advSkew}"
+      "--upscript=${cfg.upscript}"
+      "--downscript=${cfg.downscript}"
+      "--deadratio=${toString cfg.deadratio}"
+    ]
+    ++ (optional cfg.preempt "--preempt")
+    ++ (optional cfg.neutral "--neutral")
+    ++ (optional cfg.shutdown "--shutdown")
+    ++ (optional cfg.ignoreIfState "--ignoreifstate")
+    ++ (optional cfg.noMcast "--nomcast")
+    ++ (optional (cfg.extraParam != null) "--xparam=${cfg.extraParam}")
+  );
+in {
+  options.networking.ucarp = {
+    enable = mkEnableOption "ucarp, userspace implementation of CARP";
+
+    interface = mkOption {
+      type = types.str;
+      description = "Network interface to bind to.";
+      example = "eth0";
+    };
+
+    srcIp = mkOption {
+      type = types.str;
+      description = "Source (real) IP address of this host.";
+    };
+
+    vhId = mkOption {
+      type = types.ints.between 1 255;
+      description = "Virtual IP identifier shared between CARP hosts.";
+      example = 1;
+    };
+
+    passwordFile = mkOption {
+      type = types.str;
+      description = "File containing shared password between CARP hosts.";
+      example = "/run/keys/ucarp-password";
+    };
+
+    preempt = mkOption {
+      type = types.bool;
+      description = ''
+        Enable preemptive failover.
+        Thus, this host becomes the CARP master as soon as possible.
+      '';
+      default = false;
+    };
+
+    neutral = mkOption {
+      type = types.bool;
+      description = "Do not run downscript at start if the host is the backup.";
+      default = false;
+    };
+
+    addr = mkOption {
+      type = types.str;
+      description = "Virtual shared IP address.";
+    };
+
+    advBase = mkOption {
+      type = types.ints.unsigned;
+      description = "Advertisement frequency in seconds.";
+      default = 1;
+    };
+
+    advSkew = mkOption {
+      type = types.ints.unsigned;
+      description = "Advertisement skew in seconds.";
+      default = 0;
+    };
+
+    upscript = mkOption {
+      type = types.path;
+      description = ''
+        Command to run after become master, the interface name, virtual address
+        and optional extra parameters are passed as arguments.
+      '';
+      example = literalExpression ''
+        pkgs.writeScript "upscript" '''
+          #!/bin/sh
+          ''${pkgs.iproute2}/bin/ip addr add "$2"/24 dev "$1"
+        ''';
+      '';
+    };
+
+    downscript = mkOption {
+      type = types.path;
+      description = ''
+        Command to run after become backup, the interface name, virtual address
+        and optional extra parameters are passed as arguments.
+      '';
+      example = literalExpression ''
+        pkgs.writeScript "downscript" '''
+          #!/bin/sh
+          ''${pkgs.iproute2}/bin/ip addr del "$2"/24 dev "$1"
+        ''';
+      '';
+    };
+
+    deadratio = mkOption {
+      type = types.ints.unsigned;
+      description = "Ratio to consider a host as dead.";
+      default = 3;
+    };
+
+    shutdown = mkOption {
+      type = types.bool;
+      description = "Call downscript at exit.";
+      default = false;
+    };
+
+    ignoreIfState = mkOption {
+      type = types.bool;
+      description = "Ignore interface state, e.g., down or no carrier.";
+      default = false;
+    };
+
+    noMcast = mkOption {
+      type = types.bool;
+      description = "Use broadcast instead of multicast advertisements.";
+      default = false;
+    };
+
+    extraParam = mkOption {
+      type = types.nullOr types.str;
+      description = "Extra parameter to pass to the up/down scripts.";
+      default = null;
+    };
+
+    package = mkOption {
+      type = types.package;
+      description = ''
+        Package that should be used for ucarp.
+
+        Please note that the default package, pkgs.ucarp, has not received any
+        upstream updates for a long time and can be considered as unmaintained.
+      '';
+      default = pkgs.ucarp;
+      defaultText = literalExpression "pkgs.ucarp";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.ucarp = {
+      description = "ucarp, userspace implementation of CARP";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        Type = "exec";
+        ExecStart = ucarpExec;
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ oxzi ];
+}
diff --git a/nixos/modules/services/networking/unbound.nix b/nixos/modules/services/networking/unbound.nix
new file mode 100644
index 00000000000..87873c8c1e8
--- /dev/null
+++ b/nixos/modules/services/networking/unbound.nix
@@ -0,0 +1,314 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.unbound;
+
+  yesOrNo = v: if v then "yes" else "no";
+
+  toOption = indent: n: v: "${indent}${toString n}: ${v}";
+
+  toConf = indent: n: v:
+    if builtins.isFloat v then (toOption indent n (builtins.toJSON v))
+    else if isInt v       then (toOption indent n (toString v))
+    else if isBool v      then (toOption indent n (yesOrNo v))
+    else if isString v    then (toOption indent n v)
+    else if isList v      then (concatMapStringsSep "\n" (toConf indent n) v)
+    else if isAttrs v     then (concatStringsSep "\n" (
+                                  ["${indent}${n}:"] ++ (
+                                    mapAttrsToList (toConf "${indent}  ") v
+                                  )
+                                ))
+    else throw (traceSeq v "services.unbound.settings: unexpected type");
+
+  confNoServer = concatStringsSep "\n" ((mapAttrsToList (toConf "") (builtins.removeAttrs cfg.settings [ "server" ])) ++ [""]);
+  confServer = concatStringsSep "\n" (mapAttrsToList (toConf "  ") (builtins.removeAttrs cfg.settings.server [ "define-tag" ]));
+
+  confFile = pkgs.writeText "unbound.conf" ''
+    server:
+    ${optionalString (cfg.settings.server.define-tag != "") (toOption "  " "define-tag" cfg.settings.server.define-tag)}
+    ${confServer}
+    ${confNoServer}
+  '';
+
+  rootTrustAnchorFile = "${cfg.stateDir}/root.key";
+
+in {
+
+  ###### interface
+
+  options = {
+    services.unbound = {
+
+      enable = mkEnableOption "Unbound domain name server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.unbound-with-systemd;
+        defaultText = literalExpression "pkgs.unbound-with-systemd";
+        description = "The unbound package to use";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "unbound";
+        description = "User account under which unbound runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "unbound";
+        description = "Group under which unbound runs.";
+      };
+
+      stateDir = mkOption {
+        type = types.path;
+        default = "/var/lib/unbound";
+        description = "Directory holding all state for unbound to run.";
+      };
+
+      resolveLocalQueries = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether unbound should resolve local queries (i.e. add 127.0.0.1 to
+          /etc/resolv.conf).
+        '';
+      };
+
+      enableRootTrustAnchor = mkOption {
+        default = true;
+        type = types.bool;
+        description = "Use and update root trust anchor for DNSSEC validation.";
+      };
+
+      localControlSocketPath = mkOption {
+        default = null;
+        # FIXME: What is the proper type here so users can specify strings,
+        # paths and null?
+        # My guess would be `types.nullOr (types.either types.str types.path)`
+        # but I haven't verified yet.
+        type = types.nullOr types.str;
+        example = "/run/unbound/unbound.ctl";
+        description = ''
+          When not set to <literal>null</literal> this option defines the path
+          at which the unbound remote control socket should be created at. The
+          socket will be owned by the unbound user (<literal>unbound</literal>)
+          and group will be <literal>nogroup</literal>.
+
+          Users that should be permitted to access the socket must be in the
+          <literal>config.services.unbound.group</literal> group.
+
+          If this option is <literal>null</literal> remote control will not be
+          enabled. Unbounds default values apply.
+        '';
+      };
+
+      settings = mkOption {
+        default = {};
+        type = with types; submodule {
+
+          freeformType = let
+            validSettingsPrimitiveTypes = oneOf [ int str bool float ];
+            validSettingsTypes = oneOf [ validSettingsPrimitiveTypes (listOf validSettingsPrimitiveTypes) ];
+            settingsType = oneOf [ str (attrsOf validSettingsTypes) ];
+          in attrsOf (oneOf [ settingsType (listOf settingsType) ])
+              // { description = ''
+                unbound.conf configuration type. The format consist of an attribute
+                set of settings. Each settings can be either one value, a list of
+                values or an attribute set. The allowed values are integers,
+                strings, booleans or floats.
+              '';
+            };
+
+          options = {
+            remote-control.control-enable = mkOption {
+              type = bool;
+              default = false;
+              internal = true;
+            };
+          };
+        };
+        example = literalExpression ''
+          {
+            server = {
+              interface = [ "127.0.0.1" ];
+            };
+            forward-zone = [
+              {
+                name = ".";
+                forward-addr = "1.1.1.1@853#cloudflare-dns.com";
+              }
+              {
+                name = "example.org.";
+                forward-addr = [
+                  "1.1.1.1@853#cloudflare-dns.com"
+                  "1.0.0.1@853#cloudflare-dns.com"
+                ];
+              }
+            ];
+            remote-control.control-enable = true;
+          };
+        '';
+        description = ''
+          Declarative Unbound configuration
+          See the <citerefentry><refentrytitle>unbound.conf</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> manpage for a list of
+          available options.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.unbound.settings = {
+      server = {
+        directory = mkDefault cfg.stateDir;
+        username = cfg.user;
+        chroot = ''""'';
+        pidfile = ''""'';
+        # when running under systemd there is no need to daemonize
+        do-daemonize = false;
+        interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"));
+        access-control = mkDefault ([ "127.0.0.0/8 allow" ] ++ (optional config.networking.enableIPv6 "::1/128 allow"));
+        auto-trust-anchor-file = mkIf cfg.enableRootTrustAnchor rootTrustAnchorFile;
+        tls-cert-bundle = mkDefault "/etc/ssl/certs/ca-certificates.crt";
+        # prevent race conditions on system startup when interfaces are not yet
+        # configured
+        ip-freebind = mkDefault true;
+        define-tag = mkDefault "";
+      };
+      remote-control = {
+        control-enable = mkDefault false;
+        control-interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"));
+        server-key-file = mkDefault "${cfg.stateDir}/unbound_server.key";
+        server-cert-file = mkDefault "${cfg.stateDir}/unbound_server.pem";
+        control-key-file = mkDefault "${cfg.stateDir}/unbound_control.key";
+        control-cert-file = mkDefault "${cfg.stateDir}/unbound_control.pem";
+      } // optionalAttrs (cfg.localControlSocketPath != null) {
+        control-enable = true;
+        control-interface = cfg.localControlSocketPath;
+      };
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    users.users = mkIf (cfg.user == "unbound") {
+      unbound = {
+        description = "unbound daemon user";
+        isSystemUser = true;
+        group = cfg.group;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "unbound") {
+      unbound = {};
+    };
+
+    networking = mkIf cfg.resolveLocalQueries {
+      resolvconf = {
+        useLocalResolver = mkDefault true;
+      };
+
+      networkmanager.dns = "unbound";
+    };
+
+    environment.etc."unbound/unbound.conf".source = confFile;
+
+    systemd.services.unbound = {
+      description = "Unbound recursive Domain Name Server";
+      after = [ "network.target" ];
+      before = [ "nss-lookup.target" ];
+      wantedBy = [ "multi-user.target" "nss-lookup.target" ];
+
+      path = mkIf cfg.settings.remote-control.control-enable [ pkgs.openssl ];
+
+      preStart = ''
+        ${optionalString cfg.enableRootTrustAnchor ''
+          ${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
+        ''}
+        ${optionalString cfg.settings.remote-control.control-enable ''
+          ${cfg.package}/bin/unbound-control-setup -d ${cfg.stateDir}
+        ''}
+      '';
+
+      restartTriggers = [
+        confFile
+      ];
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/unbound -p -d -c /etc/unbound/unbound.conf";
+        ExecReload = "+/run/current-system/sw/bin/kill -HUP $MAINPID";
+
+        NotifyAccess = "main";
+        Type = "notify";
+
+        # FIXME: Which of these do we actualy need, can we drop the chroot flag?
+        AmbientCapabilities = [
+          "CAP_NET_BIND_SERVICE"
+          "CAP_NET_RAW"
+          "CAP_SETGID"
+          "CAP_SETUID"
+          "CAP_SYS_CHROOT"
+          "CAP_SYS_RESOURCE"
+        ];
+
+        User = cfg.user;
+        Group = cfg.group;
+
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        ProtectSystem = "strict";
+        RuntimeDirectory = "unbound";
+        ConfigurationDirectory = "unbound";
+        StateDirectory = "unbound";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" "AF_UNIX" ];
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "~@clock"
+          "@cpu-emulation"
+          "@debug"
+          "@keyring"
+          "@module"
+          "mount"
+          "@obsolete"
+          "@resources"
+        ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        RestrictSUIDSGID = true;
+
+        Restart = "on-failure";
+        RestartSec = "5s";
+      };
+    };
+  };
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "unbound" "interfaces" ] [ "services" "unbound" "settings" "server" "interface" ])
+    (mkChangedOptionModule [ "services" "unbound" "allowedAccess" ] [ "services" "unbound" "settings" "server" "access-control" ] (
+      config: map (value: "${value} allow") (getAttrFromPath [ "services" "unbound" "allowedAccess" ] config)
+    ))
+    (mkRemovedOptionModule [ "services" "unbound" "forwardAddresses" ] ''
+      Add a new setting:
+      services.unbound.settings.forward-zone = [{
+        name = ".";
+        forward-addr = [ # Your current services.unbound.forwardAddresses ];
+      }];
+      If any of those addresses are local addresses (127.0.0.1 or ::1), you must
+      also set services.unbound.settings.server.do-not-query-localhost to false.
+    '')
+    (mkRemovedOptionModule [ "services" "unbound" "extraConfig" ] ''
+      You can use services.unbound.settings to add any configuration you want.
+    '')
+  ];
+}
diff --git a/nixos/modules/services/networking/unifi.nix b/nixos/modules/services/networking/unifi.nix
new file mode 100644
index 00000000000..a683c537f05
--- /dev/null
+++ b/nixos/modules/services/networking/unifi.nix
@@ -0,0 +1,202 @@
+{ config, options, lib, pkgs, utils, ... }:
+with lib;
+let
+  cfg = config.services.unifi;
+  stateDir = "/var/lib/unifi";
+  cmd = ''
+    @${cfg.jrePackage}/bin/java java \
+        ${optionalString (cfg.initialJavaHeapSize != null) "-Xms${(toString cfg.initialJavaHeapSize)}m"} \
+        ${optionalString (cfg.maximumJavaHeapSize != null) "-Xmx${(toString cfg.maximumJavaHeapSize)}m"} \
+        -jar ${stateDir}/lib/ace.jar
+  '';
+in
+{
+
+  options = {
+
+    services.unifi.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether or not to enable the unifi controller service.
+      '';
+    };
+
+    services.unifi.jrePackage = mkOption {
+      type = types.package;
+      default = pkgs.jre8;
+      defaultText = literalExpression "pkgs.jre8";
+      description = ''
+        The JRE package to use. Check the release notes to ensure it is supported.
+      '';
+    };
+
+    services.unifi.unifiPackage = mkOption {
+      type = types.package;
+      default = pkgs.unifiLTS;
+      defaultText = literalExpression "pkgs.unifiLTS";
+      description = ''
+        The unifi package to use.
+      '';
+    };
+
+    services.unifi.mongodbPackage = mkOption {
+      type = types.package;
+      default = pkgs.mongodb;
+      defaultText = literalExpression "pkgs.mongodb";
+      description = ''
+        The mongodb package to use.
+      '';
+    };
+
+    services.unifi.openFirewall = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether or not to open the minimum required ports on the firewall.
+
+        This is necessary to allow firmware upgrades and device discovery to
+        work. For remote login, you should additionally open (or forward) port
+        8443.
+      '';
+    };
+
+    services.unifi.initialJavaHeapSize = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      example = 1024;
+      description = ''
+        Set the initial heap size for the JVM in MB. If this option isn't set, the
+        JVM will decide this value at runtime.
+      '';
+    };
+
+    services.unifi.maximumJavaHeapSize = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      example = 4096;
+      description = ''
+        Set the maximimum heap size for the JVM in MB. If this option isn't set, the
+        JVM will decide this value at runtime.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    warnings = optional
+      (options.services.unifi.openFirewall.highestPrio >= (mkOptionDefault null).priority)
+      "The current services.unifi.openFirewall = true default is deprecated and will change to false in 22.11. Set it explicitly to silence this warning.";
+
+    users.users.unifi = {
+      isSystemUser = true;
+      group = "unifi";
+      description = "UniFi controller daemon user";
+      home = "${stateDir}";
+    };
+    users.groups.unifi = {};
+
+    networking.firewall = mkIf cfg.openFirewall {
+      # https://help.ubnt.com/hc/en-us/articles/218506997
+      allowedTCPPorts = [
+        8080  # Port for UAP to inform controller.
+        8880  # Port for HTTP portal redirect, if guest portal is enabled.
+        8843  # Port for HTTPS portal redirect, ditto.
+        6789  # Port for UniFi mobile speed test.
+      ];
+      allowedUDPPorts = [
+        3478  # UDP port used for STUN.
+        10001 # UDP port used for device discovery.
+      ];
+    };
+
+    systemd.services.unifi = {
+      description = "UniFi controller daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      # This a HACK to fix missing dependencies of dynamic libs extracted from jars
+      environment.LD_LIBRARY_PATH = with pkgs.stdenv; "${cc.cc.lib}/lib";
+      # Make sure package upgrades trigger a service restart
+      restartTriggers = [ cfg.unifiPackage cfg.mongodbPackage ];
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${(removeSuffix "\n" cmd)} start";
+        ExecStop = "${(removeSuffix "\n" cmd)} stop";
+        Restart = "on-failure";
+        TimeoutSec = "5min";
+        User = "unifi";
+        UMask = "0077";
+        WorkingDirectory = "${stateDir}";
+        # the stop command exits while the main process is still running, and unifi
+        # wants to manage its own child processes. this means we have to set KillSignal
+        # to something the main process ignores, otherwise every stop will have unifi.service
+        # fail with SIGTERM status.
+        KillSignal = "SIGCONT";
+
+        # Hardening
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        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;
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [ "@system-service" ];
+
+        StateDirectory = "unifi";
+        RuntimeDirectory = "unifi";
+        LogsDirectory = "unifi";
+        CacheDirectory= "unifi";
+
+        TemporaryFileSystem = [
+          # required as we want to create bind mounts below
+          "${stateDir}/webapps:rw"
+        ];
+
+        # We must create the binary directories as bind mounts instead of symlinks
+        # This is because the controller resolves all symlinks to absolute paths
+        # to be used as the working directory.
+        BindPaths =  [
+          "/var/log/unifi:${stateDir}/logs"
+          "/run/unifi:${stateDir}/run"
+          "${cfg.unifiPackage}/dl:${stateDir}/dl"
+          "${cfg.unifiPackage}/lib:${stateDir}/lib"
+          "${cfg.mongodbPackage}/bin:${stateDir}/bin"
+          "${cfg.unifiPackage}/webapps/ROOT:${stateDir}/webapps/ROOT"
+        ];
+
+        # Needs network access
+        PrivateNetwork = false;
+        # Cannot be true due to OpenJDK
+        MemoryDenyWriteExecute = false;
+      };
+    };
+
+  };
+  imports = [
+    (mkRemovedOptionModule [ "services" "unifi" "dataDir" ] "You should move contents of dataDir to /var/lib/unifi/data" )
+    (mkRenamedOptionModule [ "services" "unifi" "openPorts" ] [ "services" "unifi" "openFirewall" ])
+  ];
+
+  meta.maintainers = with lib.maintainers; [ erictapen pennae ];
+}
diff --git a/nixos/modules/services/networking/v2ray.nix b/nixos/modules/services/networking/v2ray.nix
new file mode 100644
index 00000000000..95e8761ba5c
--- /dev/null
+++ b/nixos/modules/services/networking/v2ray.nix
@@ -0,0 +1,95 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  options = {
+
+    services.v2ray = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run v2ray server.
+
+          Either <literal>configFile</literal> or <literal>config</literal> must be specified.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.v2ray;
+        defaultText = literalExpression "pkgs.v2ray";
+        description = ''
+          Which v2ray package to use.
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/etc/v2ray/config.json";
+        description = ''
+          The absolute path to the configuration file.
+
+          Either <literal>configFile</literal> or <literal>config</literal> must be specified.
+
+          See <link xlink:href="https://www.v2fly.org/en_US/config/overview.html"/>.
+        '';
+      };
+
+      config = mkOption {
+        type = types.nullOr (types.attrsOf types.unspecified);
+        default = null;
+        example = {
+          inbounds = [{
+            port = 1080;
+            listen = "127.0.0.1";
+            protocol = "http";
+          }];
+          outbounds = [{
+            protocol = "freedom";
+          }];
+        };
+        description = ''
+          The configuration object.
+
+          Either `configFile` or `config` must be specified.
+
+          See <link xlink:href="https://www.v2fly.org/en_US/config/overview.html"/>.
+        '';
+      };
+    };
+
+  };
+
+  config = let
+    cfg = config.services.v2ray;
+    configFile = if cfg.configFile != null
+      then cfg.configFile
+      else pkgs.writeTextFile {
+        name = "v2ray.json";
+        text = builtins.toJSON cfg.config;
+        checkPhase = ''
+          ${cfg.package}/bin/v2ray -test -config $out
+        '';
+      };
+
+  in mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = (cfg.configFile == null) != (cfg.config == null);
+        message = "Either but not both `configFile` and `config` should be specified for v2ray.";
+      }
+    ];
+
+    systemd.services.v2ray = {
+      description = "v2ray Daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/v2ray -config ${configFile}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/vsftpd.nix b/nixos/modules/services/networking/vsftpd.nix
new file mode 100644
index 00000000000..d205302051e
--- /dev/null
+++ b/nixos/modules/services/networking/vsftpd.nix
@@ -0,0 +1,329 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  /* minimal secure setup:
+
+   enable = true;
+   forceLocalLoginsSSL = true;
+   forceLocalDataSSL = true;
+   userlistDeny = false;
+   localUsers = true;
+   userlist = ["non-root-user" "other-non-root-user"];
+   rsaCertFile = "/var/vsftpd/vsftpd.pem";
+
+  */
+
+  cfg = config.services.vsftpd;
+
+  inherit (pkgs) vsftpd;
+
+  yesNoOption = nixosName: vsftpdName: default: description: {
+    cfgText = "${vsftpdName}=${if getAttr nixosName cfg then "YES" else "NO"}";
+
+    nixosOption = {
+      type = types.bool;
+      name = nixosName;
+      value = mkOption {
+        inherit description default;
+        type = types.bool;
+      };
+    };
+  };
+
+  optionDescription = [
+    (yesNoOption "allowWriteableChroot" "allow_writeable_chroot" false ''
+      Allow the use of writeable root inside chroot().
+    '')
+    (yesNoOption "virtualUseLocalPrivs" "virtual_use_local_privs" false ''
+      If enabled, virtual users will use the same privileges as local
+      users. By default, virtual users will use the same privileges as
+      anonymous users, which tends to be more restrictive (especially
+      in terms of write access).
+    '')
+    (yesNoOption "anonymousUser" "anonymous_enable" false ''
+      Whether to enable the anonymous FTP user.
+    '')
+    (yesNoOption "anonymousUserNoPassword" "no_anon_password" false ''
+      Whether to disable the password for the anonymous FTP user.
+    '')
+    (yesNoOption "localUsers" "local_enable" false ''
+      Whether to enable FTP for local users.
+    '')
+    (yesNoOption "writeEnable" "write_enable" false ''
+      Whether any write activity is permitted to users.
+    '')
+    (yesNoOption "anonymousUploadEnable" "anon_upload_enable" false ''
+      Whether any uploads are permitted to anonymous users.
+    '')
+    (yesNoOption "anonymousMkdirEnable" "anon_mkdir_write_enable" false ''
+      Whether any uploads are permitted to anonymous users.
+    '')
+    (yesNoOption "chrootlocalUser" "chroot_local_user" false ''
+      Whether local users are confined to their home directory.
+    '')
+    (yesNoOption "userlistEnable" "userlist_enable" false ''
+      Whether users are included.
+    '')
+    (yesNoOption "userlistDeny" "userlist_deny" false ''
+      Specifies whether <option>userlistFile</option> is a list of user
+      names to allow or deny access.
+      The default <literal>false</literal> means whitelist/allow.
+    '')
+    (yesNoOption "forceLocalLoginsSSL" "force_local_logins_ssl" false ''
+      Only applies if <option>sslEnable</option> is true. Non anonymous (local) users
+      must use a secure SSL connection to send a password.
+    '')
+    (yesNoOption "forceLocalDataSSL" "force_local_data_ssl" false ''
+      Only applies if <option>sslEnable</option> is true. Non anonymous (local) users
+      must use a secure SSL connection for sending/receiving data on data connection.
+    '')
+    (yesNoOption "portPromiscuous" "port_promiscuous" false ''
+      Set to YES if you want to disable the PORT security check that ensures that
+      outgoing data connections can only connect to the client. Only enable if you
+      know what you are doing!
+    '')
+    (yesNoOption "ssl_tlsv1" "ssl_tlsv1" true  ''
+      Only applies if <option>ssl_enable</option> is activated. If
+      enabled, this option will permit TLS v1 protocol connections.
+      TLS v1 connections are preferred.
+    '')
+    (yesNoOption "ssl_sslv2" "ssl_sslv2" false ''
+      Only applies if <option>ssl_enable</option> is activated. If
+      enabled, this option will permit SSL v2 protocol connections.
+      TLS v1 connections are preferred.
+    '')
+    (yesNoOption "ssl_sslv3" "ssl_sslv3" false ''
+      Only applies if <option>ssl_enable</option> is activated. If
+      enabled, this option will permit SSL v3 protocol connections.
+      TLS v1 connections are preferred.
+    '')
+  ];
+
+  configFile = pkgs.writeText "vsftpd.conf"
+    ''
+      ${concatMapStrings (x: "${x.cfgText}\n") optionDescription}
+      ${optionalString (cfg.rsaCertFile != null) ''
+        ssl_enable=YES
+        rsa_cert_file=${cfg.rsaCertFile}
+      ''}
+      ${optionalString (cfg.rsaKeyFile != null) ''
+        rsa_private_key_file=${cfg.rsaKeyFile}
+      ''}
+      ${optionalString (cfg.userlistFile != null) ''
+        userlist_file=${cfg.userlistFile}
+      ''}
+      background=YES
+      listen=NO
+      listen_ipv6=YES
+      nopriv_user=vsftpd
+      secure_chroot_dir=/var/empty
+      ${optionalString (cfg.localRoot != null) ''
+        local_root=${cfg.localRoot}
+      ''}
+      syslog_enable=YES
+      ${optionalString (pkgs.stdenv.hostPlatform.system == "x86_64-linux") ''
+        seccomp_sandbox=NO
+      ''}
+      anon_umask=${cfg.anonymousUmask}
+      ${optionalString cfg.anonymousUser ''
+        anon_root=${cfg.anonymousUserHome}
+      ''}
+      ${optionalString cfg.enableVirtualUsers ''
+        guest_enable=YES
+        guest_username=vsftpd
+      ''}
+      pam_service_name=vsftpd
+      ${cfg.extraConfig}
+    '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.vsftpd = {
+
+      enable = mkEnableOption "vsftpd";
+
+      userlist = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        description = "See <option>userlistFile</option>.";
+      };
+
+      userlistFile = mkOption {
+        type = types.path;
+        default = pkgs.writeText "userlist" (concatMapStrings (x: "${x}\n") cfg.userlist);
+        defaultText = literalExpression ''pkgs.writeText "userlist" (concatMapStrings (x: "''${x}\n") cfg.userlist)'';
+        description = ''
+          Newline separated list of names to be allowed/denied if <option>userlistEnable</option>
+          is <literal>true</literal>. Meaning see <option>userlistDeny</option>.
+
+          The default is a file containing the users from <option>userlist</option>.
+
+          If explicitely set to null userlist_file will not be set in vsftpd's config file.
+        '';
+      };
+
+      enableVirtualUsers = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the <literal>pam_userdb</literal>-based
+          virtual user system
+        '';
+      };
+
+      userDbPath = mkOption {
+        type = types.nullOr types.str;
+        example = "/etc/vsftpd/userDb";
+        default = null;
+        description = ''
+          Only applies if <option>enableVirtualUsers</option> is true.
+          Path pointing to the <literal>pam_userdb</literal> user
+          database used by vsftpd to authenticate the virtual users.
+
+          This user list should be stored in the Berkeley DB database
+          format.
+
+          To generate a new user database, create a text file, add
+          your users using the following format:
+          <programlisting>
+          user1
+          password1
+          user2
+          password2
+          </programlisting>
+
+          You can then install <literal>pkgs.db</literal> to generate
+          the Berkeley DB using
+          <programlisting>
+          db_load -T -t hash -f logins.txt userDb.db
+          </programlisting>
+
+          Caution: <literal>pam_userdb</literal> will automatically
+          append a <literal>.db</literal> suffix to the filename you
+          provide though this option. This option shouldn't include
+          this filetype suffix.
+        '';
+      };
+
+      localRoot = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/var/www/$USER";
+        description = ''
+          This option represents a directory which vsftpd will try to
+          change into after a local (i.e. non- anonymous) login.
+
+          Failure is silently ignored.
+        '';
+      };
+
+      anonymousUserHome = mkOption {
+        type = types.path;
+        default = "/home/ftp/";
+        description = ''
+          Directory to consider the HOME of the anonymous user.
+        '';
+      };
+
+      rsaCertFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = "RSA certificate file.";
+      };
+
+      rsaKeyFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = "RSA private key file.";
+      };
+
+      anonymousUmask = mkOption {
+        type = types.str;
+        default = "077";
+        example = "002";
+        description = "Anonymous write umask.";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = "ftpd_banner=Hello";
+        description = "Extra configuration to add at the bottom of the generated configuration file.";
+      };
+
+    } // (listToAttrs (catAttrs "nixosOption" optionDescription));
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion =
+              (cfg.forceLocalLoginsSSL -> cfg.rsaCertFile != null)
+          &&  (cfg.forceLocalDataSSL -> cfg.rsaCertFile != null);
+        message = "vsftpd: If forceLocalLoginsSSL or forceLocalDataSSL is true then a rsaCertFile must be provided!";
+      }
+      {
+        assertion = (cfg.enableVirtualUsers -> cfg.userDbPath != null)
+                 && (cfg.enableVirtualUsers -> cfg.localUsers != null);
+        message = "vsftpd: If enableVirtualUsers is true, you need to setup both the userDbPath and localUsers options.";
+      }];
+
+    users.users = {
+      "vsftpd" = {
+        group = "vsftpd";
+        isSystemUser = true;
+        description = "VSFTPD user";
+        home = if cfg.localRoot != null
+               then cfg.localRoot # <= Necessary for virtual users.
+               else "/homeless-shelter";
+      };
+    } // optionalAttrs cfg.anonymousUser {
+      "ftp" = { name = "ftp";
+          uid = config.ids.uids.ftp;
+          group = "ftp";
+          description = "Anonymous FTP user";
+          home = cfg.anonymousUserHome;
+        };
+    };
+
+    users.groups.vsftpd = {};
+    users.groups.ftp.gid = config.ids.gids.ftp;
+
+    # If you really have to access root via FTP use mkOverride or userlistDeny
+    # = false and whitelist root
+    services.vsftpd.userlist = if cfg.userlistDeny then ["root"] else [];
+
+    systemd = {
+      tmpfiles.rules = optional cfg.anonymousUser
+        #Type Path                       Mode User   Gr    Age Arg
+        "d    '${builtins.toString cfg.anonymousUserHome}' 0555 'ftp'  'ftp' -   -";
+      services.vsftpd = {
+        description = "Vsftpd Server";
+
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig.ExecStart = "@${vsftpd}/sbin/vsftpd vsftpd ${configFile}";
+        serviceConfig.Restart = "always";
+        serviceConfig.Type = "forking";
+      };
+    };
+
+    security.pam.services.vsftpd.text = mkIf (cfg.enableVirtualUsers && cfg.userDbPath != null)''
+      auth required pam_userdb.so db=${cfg.userDbPath}
+      account required pam_userdb.so db=${cfg.userDbPath}
+    '';
+  };
+}
diff --git a/nixos/modules/services/networking/wasabibackend.nix b/nixos/modules/services/networking/wasabibackend.nix
new file mode 100644
index 00000000000..b6dcd940915
--- /dev/null
+++ b/nixos/modules/services/networking/wasabibackend.nix
@@ -0,0 +1,160 @@
+{ config, lib, options, pkgs, ... }:
+
+let
+  cfg = config.services.wasabibackend;
+  opt = options.services.wasabibackend;
+
+  inherit (lib) literalExpression mkEnableOption mkIf mkOption optionalAttrs optionalString types;
+
+  confOptions = {
+      BitcoinRpcConnectionString = "${cfg.rpc.user}:${cfg.rpc.password}";
+  } // optionalAttrs (cfg.network == "mainnet") {
+      Network = "Main";
+      MainNetBitcoinP2pEndPoint = "${cfg.endpoint.ip}:${toString cfg.endpoint.port}";
+      MainNetBitcoinCoreRpcEndPoint = "${cfg.rpc.ip}:${toString cfg.rpc.port}";
+  } // optionalAttrs (cfg.network == "testnet") {
+      Network = "TestNet";
+      TestNetBitcoinP2pEndPoint = "${cfg.endpoint.ip}:${toString cfg.endpoint.port}";
+      TestNetBitcoinCoreRpcEndPoint = "${cfg.rpc.ip}:${toString cfg.rpc.port}";
+  } // optionalAttrs (cfg.network == "regtest") {
+      Network = "RegTest";
+      RegTestBitcoinP2pEndPoint = "${cfg.endpoint.ip}:${toString cfg.endpoint.port}";
+      RegTestBitcoinCoreRpcEndPoint = "${cfg.rpc.ip}:${toString cfg.rpc.port}";
+  };
+
+  configFile = pkgs.writeText "wasabibackend.conf" (builtins.toJSON confOptions);
+
+in {
+
+  options = {
+
+    services.wasabibackend = {
+      enable = mkEnableOption "Wasabi backend service";
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/wasabibackend";
+        description = "The data directory for the Wasabi backend node.";
+      };
+
+      customConfigFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = "Defines the path to a custom configuration file that is copied to the user's directory. Overrides any config options.";
+      };
+
+      network = mkOption {
+        type = types.enum [ "mainnet" "testnet" "regtest" ];
+        default = "mainnet";
+        description = "The network to use for the Wasabi backend service.";
+      };
+
+      endpoint = {
+        ip = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "IP address for P2P connection to bitcoind.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8333;
+          description = "Port for P2P connection to bitcoind.";
+        };
+      };
+
+      rpc = {
+        ip = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "IP address for RPC connection to bitcoind.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8332;
+          description = "Port for RPC connection to bitcoind.";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "bitcoin";
+          description = "RPC user for the bitcoin endpoint.";
+        };
+
+        password = mkOption {
+          type = types.str;
+          default = "password";
+          description = "RPC password for the bitcoin endpoint. Warning: this is stored in cleartext in the Nix store! Use <literal>configFile</literal> or <literal>passwordFile</literal> if needed.";
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          description = "File that contains the password of the RPC user.";
+        };
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "wasabibackend";
+        description = "The user as which to run the wasabibackend node.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = cfg.user;
+        defaultText = literalExpression "config.${opt.user}";
+        description = "The group as which to run the wasabibackend node.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -"
+    ];
+
+    systemd.services.wasabibackend = {
+      description = "wasabibackend server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      environment = {
+        DOTNET_PRINT_TELEMETRY_MESSAGE = "false";
+        DOTNET_CLI_TELEMETRY_OPTOUT = "true";
+      };
+      preStart = ''
+        mkdir -p ${cfg.dataDir}/.walletwasabi/backend
+        ${if cfg.customConfigFile != null then ''
+          cp -v ${cfg.customConfigFile} ${cfg.dataDir}/.walletwasabi/backend/Config.json
+        '' else ''
+          cp -v ${configFile} ${cfg.dataDir}/.walletwasabi/backend/Config.json
+          ${optionalString (cfg.rpc.passwordFile != null) ''
+            CONFIGTMP=$(mktemp)
+            cat ${cfg.dataDir}/.walletwasabi/backend/Config.json | ${pkgs.jq}/bin/jq --arg rpconnection "${cfg.rpc.user}:$(cat "${cfg.rpc.passwordFile}")" '. + { BitcoinRpcConnectionString: $rpconnection }' > $CONFIGTMP
+            mv $CONFIGTMP ${cfg.dataDir}/.walletwasabi/backend/Config.json
+          ''}
+        ''}
+        chmod ug+w ${cfg.dataDir}/.walletwasabi/backend/Config.json
+      '';
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.wasabibackend}/bin/WasabiBackend";
+        ProtectSystem = "full";
+      };
+    };
+
+    users.users.${cfg.user} = {
+      name = cfg.user;
+      group = cfg.group;
+      description = "wasabibackend daemon user";
+      home = cfg.dataDir;
+      isSystemUser = true;
+    };
+
+    users.groups.${cfg.group} = {};
+
+  };
+}
diff --git a/nixos/modules/services/networking/websockify.nix b/nixos/modules/services/networking/websockify.nix
new file mode 100644
index 00000000000..f7e014e03ef
--- /dev/null
+++ b/nixos/modules/services/networking/websockify.nix
@@ -0,0 +1,54 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.networking.websockify; in {
+  options = {
+    services.networking.websockify = {
+      enable = mkOption {
+        description = "Whether to enable websockify to forward websocket connections to TCP connections.";
+
+        default = false;
+
+        type = types.bool;
+      };
+
+      sslCert = mkOption {
+        description = "Path to the SSL certificate.";
+        type = types.path;
+      };
+
+      sslKey = mkOption {
+        description = "Path to the SSL key.";
+        default = cfg.sslCert;
+        defaultText = literalExpression "config.services.networking.websockify.sslCert";
+        type = types.path;
+      };
+
+      portMap = mkOption {
+        description = "Ports to map by default.";
+        default = {};
+        type = types.attrsOf types.int;
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services."websockify@" = {
+      description = "Service to forward websocket connections to TCP connections (from port:to port %I)";
+      script = ''
+        IFS=':' read -a array <<< "$1"
+        ${pkgs.pythonPackages.websockify}/bin/websockify --ssl-only \
+          --cert=${cfg.sslCert} --key=${cfg.sslKey} 0.0.0.0:''${array[0]} 0.0.0.0:''${array[1]}
+      '';
+      scriptArgs = "%i";
+    };
+
+    systemd.targets.default-websockify = {
+      description = "Target to start all default websockify@ services";
+      unitConfig.X-StopOnReconfiguration = true;
+      wants = mapAttrsToList (name: value: "websockify@${name}:${toString value}.service") cfg.portMap;
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/wg-netmanager.nix b/nixos/modules/services/networking/wg-netmanager.nix
new file mode 100644
index 00000000000..493ff7ceba9
--- /dev/null
+++ b/nixos/modules/services/networking/wg-netmanager.nix
@@ -0,0 +1,42 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.wg-netmanager;
+in
+{
+
+  options = {
+    services.wg-netmanager = {
+      enable = mkEnableOption "Wireguard network manager";
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    # NOTE: wg-netmanager runs as root
+    systemd.services.wg-netmanager = {
+      description = "Wireguard network manager";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = with pkgs; [ wireguard-tools iproute2 wireguard-go ];
+      serviceConfig = {
+        Type = "simple";
+        Restart = "on-failure";
+        ExecStart = "${pkgs.wg-netmanager}/bin/wg_netmanager";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        ExecStop = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+
+        ReadWritePaths = [
+          "/tmp"  # wg-netmanager creates files in /tmp before deleting them after use
+        ];
+      };
+      unitConfig =  {
+        ConditionPathExists = ["/etc/wg_netmanager/network.yaml" "/etc/wg_netmanager/peer.yaml"];
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ gin66 ];
+}
diff --git a/nixos/modules/services/networking/wg-quick.nix b/nixos/modules/services/networking/wg-quick.nix
new file mode 100644
index 00000000000..414775fc357
--- /dev/null
+++ b/nixos/modules/services/networking/wg-quick.nix
@@ -0,0 +1,304 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.networking.wg-quick;
+
+  kernel = config.boot.kernelPackages;
+
+  # interface options
+
+  interfaceOpts = { ... }: {
+    options = {
+      address = mkOption {
+        example = [ "192.168.2.1/24" ];
+        default = [];
+        type = with types; listOf str;
+        description = "The IP addresses of the interface.";
+      };
+
+      dns = mkOption {
+        example = [ "192.168.2.2" ];
+        default = [];
+        type = with types; listOf str;
+        description = "The IP addresses of DNS servers to configure.";
+      };
+
+      privateKey = mkOption {
+        example = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Base64 private key generated by <command>wg genkey</command>.
+
+          Warning: Consider using privateKeyFile instead if you do not
+          want to store the key in the world-readable Nix store.
+        '';
+      };
+
+      privateKeyFile = mkOption {
+        example = "/private/wireguard_key";
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Private key file as generated by <command>wg genkey</command>.
+        '';
+      };
+
+      listenPort = mkOption {
+        default = null;
+        type = with types; nullOr int;
+        example = 51820;
+        description = ''
+          16-bit port for listening. Optional; if not specified,
+          automatically generated based on interface name.
+        '';
+      };
+
+      preUp = mkOption {
+        example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns add foo"'';
+        default = "";
+        type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
+        description = ''
+          Commands called at the start of the interface setup.
+        '';
+      };
+
+      preDown = mkOption {
+        example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns del foo"'';
+        default = "";
+        type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
+        description = ''
+          Command called before the interface is taken down.
+        '';
+      };
+
+      postUp = mkOption {
+        example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns add foo"'';
+        default = "";
+        type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
+        description = ''
+          Commands called after the interface setup.
+        '';
+      };
+
+      postDown = mkOption {
+        example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns del foo"'';
+        default = "";
+        type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
+        description = ''
+          Command called after the interface is taken down.
+        '';
+      };
+
+      table = mkOption {
+        example = "main";
+        default = null;
+        type = with types; nullOr str;
+        description = ''
+          The kernel routing table to add this interface's
+          associated routes to. Setting this is useful for e.g. policy routing
+          ("ip rule") or virtual routing and forwarding ("ip vrf"). Both
+          numeric table IDs and table names (/etc/rt_tables) can be used.
+          Defaults to "main".
+        '';
+      };
+
+      mtu = mkOption {
+        example = 1248;
+        default = null;
+        type = with types; nullOr int;
+        description = ''
+          If not specified, the MTU is automatically determined
+          from the endpoint addresses or the system default route, which is usually
+          a sane choice. However, to manually specify an MTU to override this
+          automatic discovery, this value may be specified explicitly.
+        '';
+      };
+
+      peers = mkOption {
+        default = [];
+        description = "Peers linked to the interface.";
+        type = with types; listOf (submodule peerOpts);
+      };
+    };
+  };
+
+  # peer options
+
+  peerOpts = {
+    options = {
+      publicKey = mkOption {
+        example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
+        type = types.str;
+        description = "The base64 public key to the peer.";
+      };
+
+      presharedKey = mkOption {
+        default = null;
+        example = "rVXs/Ni9tu3oDBLS4hOyAUAa1qTWVA3loR8eL20os3I=";
+        type = with types; nullOr str;
+        description = ''
+          Base64 preshared key generated by <command>wg genpsk</command>.
+          Optional, and may be omitted. This option adds an additional layer of
+          symmetric-key cryptography to be mixed into the already existing
+          public-key cryptography, for post-quantum resistance.
+
+          Warning: Consider using presharedKeyFile instead if you do not
+          want to store the key in the world-readable Nix store.
+        '';
+      };
+
+      presharedKeyFile = mkOption {
+        default = null;
+        example = "/private/wireguard_psk";
+        type = with types; nullOr str;
+        description = ''
+          File pointing to preshared key as generated by <command>wg genpsk</command>.
+          Optional, and may be omitted. This option adds an additional layer of
+          symmetric-key cryptography to be mixed into the already existing
+          public-key cryptography, for post-quantum resistance.
+        '';
+      };
+
+      allowedIPs = mkOption {
+        example = [ "10.192.122.3/32" "10.192.124.1/24" ];
+        type = with types; listOf str;
+        description = ''List of IP (v4 or v6) addresses with CIDR masks from
+        which this peer is allowed to send incoming traffic and to which
+        outgoing traffic for this peer is directed. The catch-all 0.0.0.0/0 may
+        be specified for matching all IPv4 addresses, and ::/0 may be specified
+        for matching all IPv6 addresses.'';
+      };
+
+      endpoint = mkOption {
+        default = null;
+        example = "demo.wireguard.io:12913";
+        type = with types; nullOr str;
+        description = ''Endpoint IP or hostname of the peer, followed by a colon,
+        and then a port number of the peer.'';
+      };
+
+      persistentKeepalive = mkOption {
+        default = null;
+        type = with types; nullOr int;
+        example = 25;
+        description = ''This is optional and is by default off, because most
+        users will not need it. It represents, in seconds, between 1 and 65535
+        inclusive, how often to send an authenticated empty packet to the peer,
+        for the purpose of keeping a stateful firewall or NAT mapping valid
+        persistently. For example, if the interface very rarely sends traffic,
+        but it might at anytime receive traffic from a peer, and it is behind
+        NAT, the interface might benefit from having a persistent keepalive
+        interval of 25 seconds; however, most users will not need this.'';
+      };
+    };
+  };
+
+  writeScriptFile = name: text: ((pkgs.writeShellScriptBin name text) + "/bin/${name}");
+
+  generateUnit = name: values:
+    assert assertMsg ((values.privateKey != null) != (values.privateKeyFile != null)) "Only one of privateKey or privateKeyFile may be set";
+    let
+      preUpFile = if values.preUp != "" then writeScriptFile "preUp.sh" values.preUp else null;
+      postUp =
+            optional (values.privateKeyFile != null) "wg set ${name} private-key <(cat ${values.privateKeyFile})" ++
+            (concatMap (peer: optional (peer.presharedKeyFile != null) "wg set ${name} peer ${peer.publicKey} preshared-key <(cat ${peer.presharedKeyFile})") values.peers) ++
+            optional (values.postUp != null) values.postUp;
+      postUpFile = if postUp != [] then writeScriptFile "postUp.sh" (concatMapStringsSep "\n" (line: line) postUp) else null;
+      preDownFile = if values.preDown != "" then writeScriptFile "preDown.sh" values.preDown else null;
+      postDownFile = if values.postDown != "" then writeScriptFile "postDown.sh" values.postDown else null;
+      configDir = pkgs.writeTextFile {
+        name = "config-${name}";
+        executable = false;
+        destination = "/${name}.conf";
+        text =
+        ''
+        [interface]
+        ${concatMapStringsSep "\n" (address:
+          "Address = ${address}"
+        ) values.address}
+        ${concatMapStringsSep "\n" (dns:
+          "DNS = ${dns}"
+        ) values.dns}
+        '' +
+        optionalString (values.table != null) "Table = ${values.table}\n" +
+        optionalString (values.mtu != null) "MTU = ${toString values.mtu}\n" +
+        optionalString (values.privateKey != null) "PrivateKey = ${values.privateKey}\n" +
+        optionalString (values.listenPort != null) "ListenPort = ${toString values.listenPort}\n" +
+        optionalString (preUpFile != null) "PreUp = ${preUpFile}\n" +
+        optionalString (postUpFile != null) "PostUp = ${postUpFile}\n" +
+        optionalString (preDownFile != null) "PreDown = ${preDownFile}\n" +
+        optionalString (postDownFile != null) "PostDown = ${postDownFile}\n" +
+        concatMapStringsSep "\n" (peer:
+          assert assertMsg (!((peer.presharedKeyFile != null) && (peer.presharedKey != null))) "Only one of presharedKey or presharedKeyFile may be set";
+          "[Peer]\n" +
+          "PublicKey = ${peer.publicKey}\n" +
+          optionalString (peer.presharedKey != null) "PresharedKey = ${peer.presharedKey}\n" +
+          optionalString (peer.endpoint != null) "Endpoint = ${peer.endpoint}\n" +
+          optionalString (peer.persistentKeepalive != null) "PersistentKeepalive = ${toString peer.persistentKeepalive}\n" +
+          optionalString (peer.allowedIPs != []) "AllowedIPs = ${concatStringsSep "," peer.allowedIPs}\n"
+        ) values.peers;
+      };
+      configPath = "${configDir}/${name}.conf";
+    in
+    nameValuePair "wg-quick-${name}"
+      {
+        description = "wg-quick WireGuard Tunnel - ${name}";
+        requires = [ "network-online.target" ];
+        after = [ "network.target" "network-online.target" ];
+        wantedBy = [ "multi-user.target" ];
+        environment.DEVICE = name;
+        path = [ pkgs.kmod pkgs.wireguard-tools ];
+
+        serviceConfig = {
+          Type = "oneshot";
+          RemainAfterExit = true;
+        };
+
+        script = ''
+          ${optionalString (!config.boot.isContainer) "modprobe wireguard"}
+          wg-quick up ${configPath}
+        '';
+
+        preStop = ''
+          wg-quick down ${configPath}
+        '';
+      };
+in {
+
+  ###### interface
+
+  options = {
+    networking.wg-quick = {
+      interfaces = mkOption {
+        description = "Wireguard interfaces.";
+        default = {};
+        example = {
+          wg0 = {
+            address = [ "192.168.20.4/24" ];
+            privateKey = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
+            peers = [
+              { allowedIPs = [ "192.168.20.1/32" ];
+                publicKey  = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
+                endpoint   = "demo.wireguard.io:12913"; }
+            ];
+          };
+        };
+        type = with types; attrsOf (submodule interfaceOpts);
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf (cfg.interfaces != {}) {
+    boot.extraModulePackages = optional (versionOlder kernel.kernel.version "5.6") kernel.wireguard;
+    environment.systemPackages = [ pkgs.wireguard-tools ];
+    # This is forced to false for now because the default "--validmark" rpfilter we apply on reverse path filtering
+    # breaks the wg-quick routing because wireguard packets leave with a fwmark from wireguard.
+    networking.firewall.checkReversePath = false;
+    systemd.services = mapAttrs' generateUnit cfg.interfaces;
+  };
+}
diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix
new file mode 100644
index 00000000000..7cd44b2f8a0
--- /dev/null
+++ b/nixos/modules/services/networking/wireguard.nix
@@ -0,0 +1,503 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.networking.wireguard;
+  opt = options.networking.wireguard;
+
+  kernel = config.boot.kernelPackages;
+
+  # interface options
+
+  interfaceOpts = { ... }: {
+
+    options = {
+
+      ips = mkOption {
+        example = [ "192.168.2.1/24" ];
+        default = [];
+        type = with types; listOf str;
+        description = "The IP addresses of the interface.";
+      };
+
+      privateKey = mkOption {
+        example = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Base64 private key generated by <command>wg genkey</command>.
+
+          Warning: Consider using privateKeyFile instead if you do not
+          want to store the key in the world-readable Nix store.
+        '';
+      };
+
+      generatePrivateKeyFile = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Automatically generate a private key with
+          <command>wg genkey</command>, at the privateKeyFile location.
+        '';
+      };
+
+      privateKeyFile = mkOption {
+        example = "/private/wireguard_key";
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Private key file as generated by <command>wg genkey</command>.
+        '';
+      };
+
+      listenPort = mkOption {
+        default = null;
+        type = with types; nullOr int;
+        example = 51820;
+        description = ''
+          16-bit port for listening. Optional; if not specified,
+          automatically generated based on interface name.
+        '';
+      };
+
+      preSetup = mkOption {
+        example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns add foo"'';
+        default = "";
+        type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
+        description = ''
+          Commands called at the start of the interface setup.
+        '';
+      };
+
+      postSetup = mkOption {
+        example = literalExpression ''
+          '''printf "nameserver 10.200.100.1" | ''${pkgs.openresolv}/bin/resolvconf -a wg0 -m 0'''
+        '';
+        default = "";
+        type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
+        description = "Commands called at the end of the interface setup.";
+      };
+
+      postShutdown = mkOption {
+        example = literalExpression ''"''${pkgs.openresolv}/bin/resolvconf -d wg0"'';
+        default = "";
+        type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
+        description = "Commands called after shutting down the interface.";
+      };
+
+      table = mkOption {
+        default = "main";
+        type = types.str;
+        description = ''
+          The kernel routing table to add this interface's
+          associated routes to. Setting this is useful for e.g. policy routing
+          ("ip rule") or virtual routing and forwarding ("ip vrf"). Both
+          numeric table IDs and table names (/etc/rt_tables) can be used.
+          Defaults to "main".
+        '';
+      };
+
+      peers = mkOption {
+        default = [];
+        description = "Peers linked to the interface.";
+        type = with types; listOf (submodule peerOpts);
+      };
+
+      allowedIPsAsRoutes = mkOption {
+        example = false;
+        default = true;
+        type = types.bool;
+        description = ''
+          Determines whether to add allowed IPs as routes or not.
+        '';
+      };
+
+      socketNamespace = mkOption {
+        default = null;
+        type = with types; nullOr str;
+        example = "container";
+        description = ''The pre-existing network namespace in which the
+        WireGuard interface is created, and which retains the socket even if the
+        interface is moved via <option>interfaceNamespace</option>. When
+        <literal>null</literal>, the interface is created in the init namespace.
+        See <link
+        xlink:href="https://www.wireguard.com/netns/">documentation</link>.
+        '';
+      };
+
+      interfaceNamespace = mkOption {
+        default = null;
+        type = with types; nullOr str;
+        example = "init";
+        description = ''The pre-existing network namespace the WireGuard
+        interface is moved to. The special value <literal>init</literal> means
+        the init namespace. When <literal>null</literal>, the interface is not
+        moved.
+        See <link
+        xlink:href="https://www.wireguard.com/netns/">documentation</link>.
+        '';
+      };
+    };
+
+  };
+
+  # peer options
+
+  peerOpts = {
+
+    options = {
+
+      publicKey = mkOption {
+        example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
+        type = types.str;
+        description = "The base64 public key of the peer.";
+      };
+
+      presharedKey = mkOption {
+        default = null;
+        example = "rVXs/Ni9tu3oDBLS4hOyAUAa1qTWVA3loR8eL20os3I=";
+        type = with types; nullOr str;
+        description = ''
+          Base64 preshared key generated by <command>wg genpsk</command>.
+          Optional, and may be omitted. This option adds an additional layer of
+          symmetric-key cryptography to be mixed into the already existing
+          public-key cryptography, for post-quantum resistance.
+
+          Warning: Consider using presharedKeyFile instead if you do not
+          want to store the key in the world-readable Nix store.
+        '';
+      };
+
+      presharedKeyFile = mkOption {
+        default = null;
+        example = "/private/wireguard_psk";
+        type = with types; nullOr str;
+        description = ''
+          File pointing to preshared key as generated by <command>wg genpsk</command>.
+          Optional, and may be omitted. This option adds an additional layer of
+          symmetric-key cryptography to be mixed into the already existing
+          public-key cryptography, for post-quantum resistance.
+        '';
+      };
+
+      allowedIPs = mkOption {
+        example = [ "10.192.122.3/32" "10.192.124.1/24" ];
+        type = with types; listOf str;
+        description = ''List of IP (v4 or v6) addresses with CIDR masks from
+        which this peer is allowed to send incoming traffic and to which
+        outgoing traffic for this peer is directed. The catch-all 0.0.0.0/0 may
+        be specified for matching all IPv4 addresses, and ::/0 may be specified
+        for matching all IPv6 addresses.'';
+      };
+
+      endpoint = mkOption {
+        default = null;
+        example = "demo.wireguard.io:12913";
+        type = with types; nullOr str;
+        description = ''Endpoint IP or hostname of the peer, followed by a colon,
+        and then a port number of the peer.
+
+        Warning for endpoints with changing IPs:
+        The WireGuard kernel side cannot perform DNS resolution.
+        Thus DNS resolution is done once by the <literal>wg</literal> userspace
+        utility, when setting up WireGuard. Consequently, if the IP address
+        behind the name changes, WireGuard will not notice.
+        This is especially common for dynamic-DNS setups, but also applies to
+        any other DNS-based setup.
+        If you do not use IP endpoints, you likely want to set
+        <option>networking.wireguard.dynamicEndpointRefreshSeconds</option>
+        to refresh the IPs periodically.
+        '';
+      };
+
+      dynamicEndpointRefreshSeconds = mkOption {
+        default = 0;
+        example = 5;
+        type = with types; int;
+        description = ''
+          Periodically re-execute the <literal>wg</literal> utility every
+          this many seconds in order to let WireGuard notice DNS / hostname
+          changes.
+
+          Setting this to <literal>0</literal> disables periodic reexecution.
+        '';
+      };
+
+      persistentKeepalive = mkOption {
+        default = null;
+        type = with types; nullOr int;
+        example = 25;
+        description = ''This is optional and is by default off, because most
+        users will not need it. It represents, in seconds, between 1 and 65535
+        inclusive, how often to send an authenticated empty packet to the peer,
+        for the purpose of keeping a stateful firewall or NAT mapping valid
+        persistently. For example, if the interface very rarely sends traffic,
+        but it might at anytime receive traffic from a peer, and it is behind
+        NAT, the interface might benefit from having a persistent keepalive
+        interval of 25 seconds; however, most users will not need this.'';
+      };
+
+    };
+
+  };
+
+  generateKeyServiceUnit = name: values:
+    assert values.generatePrivateKeyFile;
+    nameValuePair "wireguard-${name}-key"
+      {
+        description = "WireGuard Tunnel - ${name} - Key Generator";
+        wantedBy = [ "wireguard-${name}.service" ];
+        requiredBy = [ "wireguard-${name}.service" ];
+        before = [ "wireguard-${name}.service" ];
+        path = with pkgs; [ wireguard-tools ];
+
+        serviceConfig = {
+          Type = "oneshot";
+          RemainAfterExit = true;
+        };
+
+        script = ''
+          set -e
+
+          # If the parent dir does not already exist, create it.
+          # Otherwise, does nothing, keeping existing permisions intact.
+          mkdir -p --mode 0755 "${dirOf values.privateKeyFile}"
+
+          if [ ! -f "${values.privateKeyFile}" ]; then
+            # Write private key file with atomically-correct permissions.
+            (set -e; umask 077; wg genkey > "${values.privateKeyFile}")
+          fi
+        '';
+      };
+
+  peerUnitServiceName = interfaceName: publicKey: dynamicRefreshEnabled:
+    let
+      keyToUnitName = replaceChars
+        [ "/" "-"    " "     "+"     "="      ]
+        [ "-" "\\x2d" "\\x20" "\\x2b" "\\x3d" ];
+      unitName = keyToUnitName publicKey;
+      refreshSuffix = optionalString dynamicRefreshEnabled "-refresh";
+    in
+      "wireguard-${interfaceName}-peer-${unitName}${refreshSuffix}";
+
+  generatePeerUnit = { interfaceName, interfaceCfg, peer }:
+    let
+      psk =
+        if peer.presharedKey != null
+          then pkgs.writeText "wg-psk" peer.presharedKey
+          else peer.presharedKeyFile;
+      src = interfaceCfg.socketNamespace;
+      dst = interfaceCfg.interfaceNamespace;
+      ip = nsWrap "ip" src dst;
+      wg = nsWrap "wg" src dst;
+      dynamicRefreshEnabled = peer.dynamicEndpointRefreshSeconds != 0;
+      # We generate a different name (a `-refresh` suffix) when `dynamicEndpointRefreshSeconds`
+      # to avoid that the same service switches `Type` (`oneshot` vs `simple`),
+      # with the intent to make scripting more obvious.
+      serviceName = peerUnitServiceName interfaceName peer.publicKey dynamicRefreshEnabled;
+    in nameValuePair serviceName
+      {
+        description = "WireGuard Peer - ${interfaceName} - ${peer.publicKey}";
+        requires = [ "wireguard-${interfaceName}.service" ];
+        after = [ "wireguard-${interfaceName}.service" ];
+        wantedBy = [ "multi-user.target" "wireguard-${interfaceName}.service" ];
+        environment.DEVICE = interfaceName;
+        environment.WG_ENDPOINT_RESOLUTION_RETRIES = "infinity";
+        path = with pkgs; [ iproute2 wireguard-tools ];
+
+        serviceConfig =
+          if !dynamicRefreshEnabled
+            then
+              {
+                Type = "oneshot";
+                RemainAfterExit = true;
+              }
+            else
+              {
+                Type = "simple"; # re-executes 'wg' indefinitely
+                # Note that `Type = "oneshot"` services with `RemainAfterExit = true`
+                # cannot be used with systemd timers (see `man systemd.timer`),
+                # which is why `simple` with a loop is the best choice here.
+                # It also makes starting and stopping easiest.
+              };
+
+        script = let
+          wg_setup = concatStringsSep " " (
+            [ ''${wg} set ${interfaceName} peer "${peer.publicKey}"'' ]
+            ++ optional (psk != null) ''preshared-key "${psk}"''
+            ++ optional (peer.endpoint != null) ''endpoint "${peer.endpoint}"''
+            ++ optional (peer.persistentKeepalive != null) ''persistent-keepalive "${toString peer.persistentKeepalive}"''
+            ++ optional (peer.allowedIPs != []) ''allowed-ips "${concatStringsSep "," peer.allowedIPs}"''
+          );
+          route_setup =
+            optionalString interfaceCfg.allowedIPsAsRoutes
+              (concatMapStringsSep "\n"
+                (allowedIP:
+                  ''${ip} route replace "${allowedIP}" dev "${interfaceName}" table "${interfaceCfg.table}"''
+                ) peer.allowedIPs);
+        in ''
+          ${wg_setup}
+          ${route_setup}
+
+          ${optionalString (peer.dynamicEndpointRefreshSeconds != 0) ''
+            # Re-execute 'wg' periodically to notice DNS / hostname changes.
+            # Note this will not time out on transient DNS failures such as DNS names
+            # because we have set 'WG_ENDPOINT_RESOLUTION_RETRIES=infinity'.
+            # Also note that 'wg' limits its maximum retry delay to 20 seconds as of writing.
+            while ${wg_setup}; do
+              sleep "${toString peer.dynamicEndpointRefreshSeconds}";
+            done
+          ''}
+        '';
+
+        postStop = let
+          route_destroy = optionalString interfaceCfg.allowedIPsAsRoutes
+            (concatMapStringsSep "\n"
+              (allowedIP:
+                ''${ip} route delete "${allowedIP}" dev "${interfaceName}" table "${interfaceCfg.table}"''
+              ) peer.allowedIPs);
+        in ''
+          ${wg} set "${interfaceName}" peer "${peer.publicKey}" remove
+          ${route_destroy}
+        '';
+      };
+
+  generateInterfaceUnit = name: values:
+    # exactly one way to specify the private key must be set
+    #assert (values.privateKey != null) != (values.privateKeyFile != null);
+    let privKey = if values.privateKeyFile != null then values.privateKeyFile else pkgs.writeText "wg-key" values.privateKey;
+        src = values.socketNamespace;
+        dst = values.interfaceNamespace;
+        ipPreMove  = nsWrap "ip" src null;
+        ipPostMove = nsWrap "ip" src dst;
+        wg = nsWrap "wg" src dst;
+        ns = if dst == "init" then "1" else dst;
+
+    in
+    nameValuePair "wireguard-${name}"
+      {
+        description = "WireGuard Tunnel - ${name}";
+        requires = [ "network-online.target" ];
+        after = [ "network.target" "network-online.target" ];
+        wantedBy = [ "multi-user.target" ];
+        environment.DEVICE = name;
+        path = with pkgs; [ kmod iproute2 wireguard-tools ];
+
+        serviceConfig = {
+          Type = "oneshot";
+          RemainAfterExit = true;
+        };
+
+        script = ''
+          ${optionalString (!config.boot.isContainer) "modprobe wireguard || true"}
+
+          ${values.preSetup}
+
+          ${ipPreMove} link add dev "${name}" type wireguard
+          ${optionalString (values.interfaceNamespace != null && values.interfaceNamespace != values.socketNamespace) ''${ipPreMove} link set "${name}" netns "${ns}"''}
+
+          ${concatMapStringsSep "\n" (ip:
+            ''${ipPostMove} address add "${ip}" dev "${name}"''
+          ) values.ips}
+
+          ${concatStringsSep " " (
+            [ ''${wg} set "${name}" private-key "${privKey}"'' ]
+            ++ optional (values.listenPort != null) ''listen-port "${toString values.listenPort}"''
+          )}
+
+          ${ipPostMove} link set up dev "${name}"
+
+          ${values.postSetup}
+        '';
+
+        postStop = ''
+          ${ipPostMove} link del dev "${name}"
+          ${values.postShutdown}
+        '';
+      };
+
+  nsWrap = cmd: src: dst:
+    let
+      nsList = filter (ns: ns != null) [ src dst ];
+      ns = last nsList;
+    in
+      if (length nsList > 0 && ns != "init") then ''ip netns exec "${ns}" "${cmd}"'' else cmd;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    networking.wireguard = {
+
+      enable = mkOption {
+        description = "Whether to enable WireGuard.";
+        type = types.bool;
+        # 2019-05-25: Backwards compatibility.
+        default = cfg.interfaces != {};
+        defaultText = literalExpression "config.${opt.interfaces} != { }";
+        example = true;
+      };
+
+      interfaces = mkOption {
+        description = "WireGuard interfaces.";
+        default = {};
+        example = {
+          wg0 = {
+            ips = [ "192.168.20.4/24" ];
+            privateKey = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
+            peers = [
+              { allowedIPs = [ "192.168.20.1/32" ];
+                publicKey  = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
+                endpoint   = "demo.wireguard.io:12913"; }
+            ];
+          };
+        };
+        type = with types; attrsOf (submodule interfaceOpts);
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable (let
+    all_peers = flatten
+      (mapAttrsToList (interfaceName: interfaceCfg:
+        map (peer: { inherit interfaceName interfaceCfg peer;}) interfaceCfg.peers
+      ) cfg.interfaces);
+  in {
+
+    assertions = (attrValues (
+        mapAttrs (name: value: {
+          assertion = (value.privateKey != null) != (value.privateKeyFile != null);
+          message = "Either networking.wireguard.interfaces.${name}.privateKey or networking.wireguard.interfaces.${name}.privateKeyFile must be set.";
+        }) cfg.interfaces))
+      ++ (attrValues (
+        mapAttrs (name: value: {
+          assertion = value.generatePrivateKeyFile -> (value.privateKey == null);
+          message = "networking.wireguard.interfaces.${name}.generatePrivateKeyFile must not be set if networking.wireguard.interfaces.${name}.privateKey is set.";
+        }) cfg.interfaces))
+        ++ map ({ interfaceName, peer, ... }: {
+          assertion = (peer.presharedKey == null) || (peer.presharedKeyFile == null);
+          message = "networking.wireguard.interfaces.${interfaceName} peer «${peer.publicKey}» has both presharedKey and presharedKeyFile set, but only one can be used.";
+        }) all_peers;
+
+    boot.extraModulePackages = optional (versionOlder kernel.kernel.version "5.6") kernel.wireguard;
+    environment.systemPackages = [ pkgs.wireguard-tools ];
+
+    systemd.services =
+      (mapAttrs' generateInterfaceUnit cfg.interfaces)
+      // (listToAttrs (map generatePeerUnit all_peers))
+      // (mapAttrs' generateKeyServiceUnit
+      (filterAttrs (name: value: value.generatePrivateKeyFile) cfg.interfaces));
+
+  });
+
+}
diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix
new file mode 100644
index 00000000000..c2e1d37e28b
--- /dev/null
+++ b/nixos/modules/services/networking/wpa_supplicant.nix
@@ -0,0 +1,541 @@
+{ config, lib, options, pkgs, utils, ... }:
+
+with lib;
+
+let
+  package = if cfg.allowAuxiliaryImperativeNetworks
+    then pkgs.wpa_supplicant_ro_ssids
+    else pkgs.wpa_supplicant;
+
+  cfg = config.networking.wireless;
+  opt = options.networking.wireless;
+
+  wpa3Protocols = [ "SAE" "FT-SAE" ];
+  hasMixedWPA = opts:
+    let
+      hasWPA3 = !mutuallyExclusive opts.authProtocols wpa3Protocols;
+      others = subtractLists wpa3Protocols opts.authProtocols;
+    in hasWPA3 && others != [];
+
+  # Gives a WPA3 network higher priority
+  increaseWPA3Priority = opts:
+    opts // optionalAttrs (hasMixedWPA opts)
+      { priority = if opts.priority == null
+                     then 1
+                     else opts.priority + 1;
+      };
+
+  # Creates a WPA2 fallback network
+  mkWPA2Fallback = opts:
+    opts // { authProtocols = subtractLists wpa3Protocols opts.authProtocols; };
+
+  # Networks attrset as a list
+  networkList = mapAttrsToList (ssid: opts: opts // { inherit ssid; })
+                cfg.networks;
+
+  # List of all networks (normal + generated fallbacks)
+  allNetworks =
+    if cfg.fallbackToWPA2
+      then map increaseWPA3Priority networkList
+           ++ map mkWPA2Fallback (filter hasMixedWPA networkList)
+      else networkList;
+
+  # Content of wpa_supplicant.conf
+  generatedConfig = concatStringsSep "\n" (
+    (map mkNetwork allNetworks)
+    ++ optional cfg.userControlled.enable (concatStringsSep "\n"
+      [ "ctrl_interface=/run/wpa_supplicant"
+        "ctrl_interface_group=${cfg.userControlled.group}"
+        "update_config=1"
+      ])
+    ++ [ "pmf=1" ]
+    ++ optional cfg.scanOnLowSignal ''bgscan="simple:30:-70:3600"''
+    ++ optional (cfg.extraConfig != "") cfg.extraConfig);
+
+  configIsGenerated = with cfg;
+    networks != {} || extraConfig != "" || userControlled.enable;
+
+  # the original configuration file
+  configFile =
+    if configIsGenerated
+      then pkgs.writeText "wpa_supplicant.conf" generatedConfig
+      else "/etc/wpa_supplicant.conf";
+  # the config file with environment variables replaced
+  finalConfig = ''"$RUNTIME_DIRECTORY"/wpa_supplicant.conf'';
+
+  # Creates a network block for wpa_supplicant.conf
+  mkNetwork = opts:
+  let
+    quote = x: ''"${x}"'';
+    indent = x: "  " + x;
+
+    pskString = if opts.psk != null
+      then quote opts.psk
+      else opts.pskRaw;
+
+    options = [
+      "ssid=${quote opts.ssid}"
+      (if pskString != null || opts.auth != null
+        then "key_mgmt=${concatStringsSep " " opts.authProtocols}"
+        else "key_mgmt=NONE")
+    ] ++ optional opts.hidden "scan_ssid=1"
+      ++ optional (pskString != null) "psk=${pskString}"
+      ++ optionals (opts.auth != null) (filter (x: x != "") (splitString "\n" opts.auth))
+      ++ optional (opts.priority != null) "priority=${toString opts.priority}"
+      ++ optional (opts.extraConfig != "") opts.extraConfig;
+  in ''
+    network={
+    ${concatMapStringsSep "\n" indent options}
+    }
+  '';
+
+  # Creates a systemd unit for wpa_supplicant bound to a given (or any) interface
+  mkUnit = iface:
+    let
+      deviceUnit = optional (iface != null) "sys-subsystem-net-devices-${utils.escapeSystemdPath iface}.device";
+      configStr = if cfg.allowAuxiliaryImperativeNetworks
+        then "-c /etc/wpa_supplicant.conf -I ${finalConfig}"
+        else "-c ${finalConfig}";
+    in {
+      description = "WPA Supplicant instance" + optionalString (iface != null) " for interface ${iface}";
+
+      after = deviceUnit;
+      before = [ "network.target" ];
+      wants = [ "network.target" ];
+      requires = deviceUnit;
+      wantedBy = [ "multi-user.target" ];
+      stopIfChanged = false;
+
+      path = [ package ];
+      serviceConfig.RuntimeDirectory = "wpa_supplicant";
+      serviceConfig.RuntimeDirectoryMode = "700";
+      serviceConfig.EnvironmentFile = mkIf (cfg.environmentFile != null)
+        (builtins.toString cfg.environmentFile);
+
+      script =
+      ''
+        ${optionalString configIsGenerated ''
+          if [ -f /etc/wpa_supplicant.conf ]; then
+            echo >&2 "<3>/etc/wpa_supplicant.conf present but ignored. Generated ${configFile} is used instead."
+          fi
+        ''}
+
+        # substitute environment variables
+        ${pkgs.gawk}/bin/awk '{
+          for(varname in ENVIRON)
+            gsub("@"varname"@", ENVIRON[varname])
+          print
+        }' "${configFile}" > "${finalConfig}"
+
+        iface_args="-s ${optionalString cfg.dbusControlled "-u"} -D${cfg.driver} ${configStr}"
+
+        ${if iface == null then ''
+          # detect interfaces automatically
+
+          # check if there are no wireless interfaces
+          if ! find -H /sys/class/net/* -name wireless | grep -q .; then
+            # if so, wait until one appears
+            echo "Waiting for wireless interfaces"
+            grep -q '^ACTION=add' < <(stdbuf -oL -- udevadm monitor -s net/wlan -pu)
+            # Note: the above line has been carefully written:
+            # 1. The process substitution avoids udevadm hanging (after grep has quit)
+            #    until it tries to write to the pipe again. Not even pipefail works here.
+            # 2. stdbuf is needed because udevadm output is buffered by default and grep
+            #    may hang until more udev events enter the pipe.
+          fi
+
+          # add any interface found to the daemon arguments
+          for name in $(find -H /sys/class/net/* -name wireless | cut -d/ -f 5); do
+            echo "Adding interface $name"
+            args+="''${args:+ -N} -i$name $iface_args"
+          done
+        '' else ''
+          # add known interface to the daemon arguments
+          args="-i${iface} $iface_args"
+        ''}
+
+        # finally start daemon
+        exec wpa_supplicant $args
+      '';
+    };
+
+  systemctl = "/run/current-system/systemd/bin/systemctl";
+
+in {
+  options = {
+    networking.wireless = {
+      enable = mkEnableOption "wpa_supplicant";
+
+      interfaces = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "wlan0" "wlan1" ];
+        description = ''
+          The interfaces <command>wpa_supplicant</command> will use. If empty, it will
+          automatically use all wireless interfaces.
+
+          <note><para>
+            A separate wpa_supplicant instance will be started for each interface.
+          </para></note>
+        '';
+      };
+
+      driver = mkOption {
+        type = types.str;
+        default = "nl80211,wext";
+        description = "Force a specific wpa_supplicant driver.";
+      };
+
+      allowAuxiliaryImperativeNetworks = mkEnableOption "support for imperative & declarative networks" // {
+        description = ''
+          Whether to allow configuring networks "imperatively" (e.g. via
+          <package>wpa_supplicant_gui</package>) and declaratively via
+          <xref linkend="opt-networking.wireless.networks" />.
+
+          Please note that this adds a custom patch to <package>wpa_supplicant</package>.
+        '';
+      };
+
+      scanOnLowSignal = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to periodically scan for (better) networks when the signal of
+          the current one is low. This will make roaming between access points
+          faster, but will consume more power.
+        '';
+      };
+
+      fallbackToWPA2 = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to fall back to WPA2 authentication protocols if WPA3 failed.
+          This allows old wireless cards (that lack recent features required by
+          WPA3) to connect to mixed WPA2/WPA3 access points.
+
+          To avoid possible downgrade attacks, disable this options.
+        '';
+      };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/run/secrets/wireless.env";
+        description = ''
+          File consisting of lines of the form <literal>varname=value</literal>
+          to define variables for the wireless configuration.
+
+          See section "EnvironmentFile=" in <citerefentry>
+          <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+          </citerefentry> for a syntax reference.
+
+          Secrets (PSKs, passwords, etc.) can be provided without adding them to
+          the world-readable Nix store by defining them in the environment file and
+          referring to them in option <option>networking.wireless.networks</option>
+          with the syntax <literal>@varname@</literal>. Example:
+
+          <programlisting>
+          # content of /run/secrets/wireless.env
+          PSK_HOME=mypassword
+          PASS_WORK=myworkpassword
+          </programlisting>
+
+          <programlisting>
+          # wireless-related configuration
+          networking.wireless.environmentFile = "/run/secrets/wireless.env";
+          networking.wireless.networks = {
+            home.psk = "@PSK_HOME@";
+            work.auth = '''
+              eap=PEAP
+              identity="my-user@example.com"
+              password="@PASS_WORK@"
+            ''';
+          };
+          </programlisting>
+        '';
+      };
+
+      networks = mkOption {
+        type = types.attrsOf (types.submodule {
+          options = {
+            psk = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = ''
+                The network's pre-shared key in plaintext defaulting
+                to being a network without any authentication.
+
+                <warning><para>
+                  Be aware that this will be written to the nix store
+                  in plaintext! Use an environment variable instead.
+                </para></warning>
+
+                <note><para>
+                  Mutually exclusive with <varname>pskRaw</varname>.
+                </para></note>
+              '';
+            };
+
+            pskRaw = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = ''
+                The network's pre-shared key in hex defaulting
+                to being a network without any authentication.
+
+                <warning><para>
+                  Be aware that this will be written to the nix store
+                  in plaintext! Use an environment variable instead.
+                </para></warning>
+
+                <note><para>
+                  Mutually exclusive with <varname>psk</varname>.
+                </para></note>
+              '';
+            };
+
+            authProtocols = mkOption {
+              default = [
+                # WPA2 and WPA3
+                "WPA-PSK" "WPA-EAP" "SAE"
+                # 802.11r variants of the above
+                "FT-PSK" "FT-EAP" "FT-SAE"
+              ];
+              # The list can be obtained by running this command
+              # awk '
+              #   /^# key_mgmt: /{ run=1 }
+              #   /^#$/{ run=0 }
+              #   /^# [A-Z0-9-]{2,}/{ if(run){printf("\"%s\"\n", $2)} }
+              # ' /run/current-system/sw/share/doc/wpa_supplicant/wpa_supplicant.conf.example
+              type = types.listOf (types.enum [
+                "WPA-PSK"
+                "WPA-EAP"
+                "IEEE8021X"
+                "NONE"
+                "WPA-NONE"
+                "FT-PSK"
+                "FT-EAP"
+                "FT-EAP-SHA384"
+                "WPA-PSK-SHA256"
+                "WPA-EAP-SHA256"
+                "SAE"
+                "FT-SAE"
+                "WPA-EAP-SUITE-B"
+                "WPA-EAP-SUITE-B-192"
+                "OSEN"
+                "FILS-SHA256"
+                "FILS-SHA384"
+                "FT-FILS-SHA256"
+                "FT-FILS-SHA384"
+                "OWE"
+                "DPP"
+              ]);
+              description = ''
+                The list of authentication protocols accepted by this network.
+                This corresponds to the <literal>key_mgmt</literal> option in wpa_supplicant.
+              '';
+            };
+
+            auth = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = ''
+                eap=PEAP
+                identity="user@example.com"
+                password="@EXAMPLE_PASSWORD@"
+              '';
+              description = ''
+                Use this option to configure advanced authentication methods like EAP.
+                See
+                <citerefentry>
+                  <refentrytitle>wpa_supplicant.conf</refentrytitle>
+                  <manvolnum>5</manvolnum>
+                </citerefentry>
+                for example configurations.
+
+                <warning><para>
+                  Be aware that this will be written to the nix store
+                  in plaintext! Use an environment variable for secrets.
+                </para></warning>
+
+                <note><para>
+                  Mutually exclusive with <varname>psk</varname> and
+                  <varname>pskRaw</varname>.
+                </para></note>
+              '';
+            };
+
+            hidden = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Set this to <literal>true</literal> if the SSID of the network is hidden.
+              '';
+              example = literalExpression ''
+                { echelon = {
+                    hidden = true;
+                    psk = "abcdefgh";
+                  };
+                }
+              '';
+            };
+
+            priority = mkOption {
+              type = types.nullOr types.int;
+              default = null;
+              description = ''
+                By default, all networks will get same priority group (0). If some of the
+                networks are more desirable, this field can be used to change the order in
+                which wpa_supplicant goes through the networks when selecting a BSS. The
+                priority groups will be iterated in decreasing priority (i.e., the larger the
+                priority value, the sooner the network is matched against the scan results).
+                Within each priority group, networks will be selected based on security
+                policy, signal strength, etc.
+              '';
+            };
+
+            extraConfig = mkOption {
+              type = types.str;
+              default = "";
+              example = ''
+                bssid_blacklist=02:11:22:33:44:55 02:22:aa:44:55:66
+              '';
+              description = ''
+                Extra configuration lines appended to the network block.
+                See
+                <citerefentry>
+                  <refentrytitle>wpa_supplicant.conf</refentrytitle>
+                  <manvolnum>5</manvolnum>
+                </citerefentry>
+                for available options.
+              '';
+            };
+
+          };
+        });
+        description = ''
+          The network definitions to automatically connect to when
+           <command>wpa_supplicant</command> is running. If this
+           parameter is left empty wpa_supplicant will use
+          /etc/wpa_supplicant.conf as the configuration file.
+        '';
+        default = {};
+        example = literalExpression ''
+          { echelon = {                   # SSID with no spaces or special characters
+              psk = "abcdefgh";           # (password will be written to /nix/store!)
+            };
+
+            echelon = {                   # safe version of the above: read PSK from the
+              psk = "@PSK_ECHELON@";      # variable PSK_ECHELON, defined in environmentFile,
+            };                            # this won't leak into /nix/store
+
+            "echelon's AP" = {            # SSID with spaces and/or special characters
+               psk = "ijklmnop";          # (password will be written to /nix/store!)
+            };
+
+            "free.wifi" = {};             # Public wireless network
+          }
+        '';
+      };
+
+      userControlled = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Allow normal users to control wpa_supplicant through wpa_gui or wpa_cli.
+            This is useful for laptop users that switch networks a lot and don't want
+            to depend on a large package such as NetworkManager just to pick nearby
+            access points.
+
+            When using a declarative network specification you cannot persist any
+            settings via wpa_gui or wpa_cli.
+          '';
+        };
+
+        group = mkOption {
+          type = types.str;
+          default = "wheel";
+          example = "network";
+          description = "Members of this group can control wpa_supplicant.";
+        };
+      };
+
+      dbusControlled = mkOption {
+        type = types.bool;
+        default = lib.length cfg.interfaces < 2;
+        defaultText = literalExpression "length config.${opt.interfaces} < 2";
+        description = ''
+          Whether to enable the DBus control interface.
+          This is only needed when using NetworkManager or connman.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.str;
+        default = "";
+        example = ''
+          p2p_disabled=1
+        '';
+        description = ''
+          Extra lines appended to the configuration file.
+          See
+          <citerefentry>
+            <refentrytitle>wpa_supplicant.conf</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry>
+          for available options.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = flip mapAttrsToList cfg.networks (name: cfg: {
+      assertion = with cfg; count (x: x != null) [ psk pskRaw auth ] <= 1;
+      message = ''options networking.wireless."${name}".{psk,pskRaw,auth} are mutually exclusive'';
+    }) ++ [
+      {
+        assertion = length cfg.interfaces > 1 -> !cfg.dbusControlled;
+        message =
+          let daemon = if config.networking.networkmanager.enable then "NetworkManager" else
+                       if config.services.connman.enable then "connman" else null;
+              n = toString (length cfg.interfaces);
+          in ''
+            It's not possible to run multiple wpa_supplicant instances with DBus support.
+            Note: you're seeing this error because `networking.wireless.interfaces` has
+            ${n} entries, implying an equal number of wpa_supplicant instances.
+          '' + optionalString (daemon != null) ''
+            You don't need to change `networking.wireless.interfaces` when using ${daemon}:
+            in this case the interfaces will be configured automatically for you.
+          '';
+      }
+    ];
+
+    hardware.wirelessRegulatoryDatabase = true;
+
+    environment.systemPackages = [ package ];
+    services.dbus.packages = optional cfg.dbusControlled package;
+
+    systemd.services =
+      if cfg.interfaces == []
+        then { wpa_supplicant = mkUnit null; }
+        else listToAttrs (map (i: nameValuePair "wpa_supplicant-${i}" (mkUnit i)) cfg.interfaces);
+
+    # Restart wpa_supplicant after resuming from sleep
+    powerManagement.resumeCommands = concatStringsSep "\n" (
+      optional (cfg.interfaces == []) "${systemctl} try-restart wpa_supplicant"
+      ++ map (i: "${systemctl} try-restart wpa_supplicant-${i}") cfg.interfaces
+    );
+
+    # Restart wpa_supplicant when a wlan device appears or disappears. This is
+    # only needed when an interface hasn't been specified by the user.
+    services.udev.extraRules = optionalString (cfg.interfaces == []) ''
+      ACTION=="add|remove", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", \
+      RUN+="${systemctl} try-restart wpa_supplicant.service"
+    '';
+  };
+
+  meta.maintainers = with lib.maintainers; [ globin rnhmjoj ];
+}
diff --git a/nixos/modules/services/networking/x2goserver.nix b/nixos/modules/services/networking/x2goserver.nix
new file mode 100644
index 00000000000..d4adf6c5650
--- /dev/null
+++ b/nixos/modules/services/networking/x2goserver.nix
@@ -0,0 +1,164 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.x2goserver;
+
+  defaults = {
+    superenicer = { enable = cfg.superenicer.enable; };
+  };
+  confText = generators.toINI {} (recursiveUpdate defaults cfg.settings);
+  x2goServerConf = pkgs.writeText "x2goserver.conf" confText;
+
+  x2goAgentOptions = pkgs.writeText "x2goagent.options" ''
+    X2GO_NXOPTIONS=""
+    X2GO_NXAGENT_DEFAULT_OPTIONS="${concatStringsSep " " cfg.nxagentDefaultOptions}"
+  '';
+
+in {
+  imports = [
+    (mkRenamedOptionModule [ "programs" "x2goserver" ] [ "services" "x2goserver" ])
+  ];
+
+  options.services.x2goserver = {
+    enable = mkEnableOption "x2goserver" // {
+      description = ''
+        Enables the x2goserver module.
+        NOTE: This will create a good amount of symlinks in `/usr/local/bin`
+      '';
+    };
+
+    superenicer = {
+      enable = mkEnableOption "superenicer" // {
+        description = ''
+          Enables the SupeReNicer code in x2gocleansessions, this will renice
+          suspended sessions to nice level 19 and renice them to level 0 if the
+          session becomes marked as running again
+        '';
+      };
+    };
+
+    nxagentDefaultOptions = mkOption {
+      type = types.listOf types.str;
+      default = [ "-extension GLX" "-nolisten tcp" ];
+      description = ''
+        List of default nx agent options.
+      '';
+    };
+
+    settings = mkOption {
+      type = types.attrsOf types.attrs;
+      default = {};
+      description = ''
+        x2goserver.conf ini configuration as nix attributes. See
+        `x2goserver.conf(5)` for details
+      '';
+      example = literalExpression ''
+        {
+          superenicer = {
+            "enable" = "yes";
+            "idle-nice-level" = 19;
+          };
+          telekinesis = { "enable" = "no"; };
+        }
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    # x2goserver can run X11 program even if "services.xserver.enable = false"
+    xdg = {
+      autostart.enable = true;
+      menus.enable = true;
+      mime.enable = true;
+      icons.enable = true;
+    };
+
+    environment.systemPackages = [ pkgs.x2goserver ];
+
+    users.groups.x2go = {};
+    users.users.x2go = {
+      home = "/var/lib/x2go/db";
+      group = "x2go";
+      isSystemUser = true;
+    };
+
+    security.wrappers.x2gosqliteWrapper = {
+      source = "${pkgs.x2goserver}/lib/x2go/libx2go-server-db-sqlite3-wrapper.pl";
+      owner = "x2go";
+      group = "x2go";
+      setuid = false;
+      setgid = true;
+    };
+    security.wrappers.x2goprintWrapper = {
+      source = "${pkgs.x2goserver}/bin/x2goprint";
+      owner = "x2go";
+      group = "x2go";
+      setuid = false;
+      setgid = true;
+    };
+
+    systemd.tmpfiles.rules = with pkgs; [
+      "d /var/lib/x2go/ - x2go x2go - -"
+      "d /var/lib/x2go/db - x2go x2go - -"
+      "d /var/lib/x2go/conf - x2go x2go - -"
+      "d /run/x2go 0755 x2go x2go - -"
+    ] ++
+    # x2goclient sends SSH commands with preset PATH set to
+    # "/usr/local/bin;/usr/bin;/bin". Since we cannot filter arbitrary ssh
+    # commands, we have to make the following executables available.
+    map (f: "L+ /usr/local/bin/${f} - - - - ${x2goserver}/bin/${f}") [
+      "x2goagent" "x2gobasepath" "x2gocleansessions" "x2gocmdexitmessage"
+      "x2godbadmin" "x2gofeature" "x2gofeaturelist" "x2gofm" "x2gogetapps"
+      "x2gogetservers" "x2golistdesktops" "x2golistmounts" "x2golistsessions"
+      "x2golistsessions_root" "x2golistshadowsessions" "x2gomountdirs"
+      "x2gopath" "x2goprint" "x2goresume-desktopsharing" "x2goresume-session"
+      "x2goruncommand" "x2goserver-run-extensions" "x2gosessionlimit"
+      "x2gosetkeyboard" "x2goshowblocks" "x2gostartagent"
+      "x2gosuspend-desktopsharing" "x2gosuspend-session"
+      "x2goterminate-desktopsharing" "x2goterminate-session"
+      "x2goumount-session" "x2goversion"
+    ] ++ [
+      "L+ /usr/local/bin/awk - - - - ${gawk}/bin/awk"
+      "L+ /usr/local/bin/chmod - - - - ${coreutils}/bin/chmod"
+      "L+ /usr/local/bin/cp - - - - ${coreutils}/bin/cp"
+      "L+ /usr/local/bin/sed - - - - ${gnused}/bin/sed"
+      "L+ /usr/local/bin/setsid - - - - ${util-linux}/bin/setsid"
+      "L+ /usr/local/bin/xrandr - - - - ${xorg.xrandr}/bin/xrandr"
+      "L+ /usr/local/bin/xmodmap - - - - ${xorg.xmodmap}/bin/xmodmap"
+    ];
+
+    systemd.services.x2goserver = {
+      description = "X2Go Server Daemon";
+      wantedBy = [ "multi-user.target" ];
+      unitConfig.Documentation = "man:x2goserver.conf(5)";
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${pkgs.x2goserver}/bin/x2gocleansessions";
+        PIDFile = "/run/x2go/x2goserver.pid";
+        User = "x2go";
+        Group = "x2go";
+        RuntimeDirectory = "x2go";
+        StateDirectory = "x2go";
+      };
+      preStart = ''
+        if [ ! -e /var/lib/x2go/setup_ran ]
+        then
+          mkdir -p /var/lib/x2go/conf
+          cp -r ${pkgs.x2goserver}/etc/x2go/* /var/lib/x2go/conf/
+          ln -sf ${x2goServerConf} /var/lib/x2go/conf/x2goserver.conf
+          ln -sf ${x2goAgentOptions} /var/lib/x2go/conf/x2goagent.options
+          ${pkgs.x2goserver}/bin/x2godbadmin --createdb
+          touch /var/lib/x2go/setup_ran
+        fi
+      '';
+    };
+
+    # https://bugs.x2go.org/cgi-bin/bugreport.cgi?bug=276
+    security.sudo.extraConfig = ''
+      Defaults  env_keep+=QT_GRAPHICSSYSTEM
+    '';
+  };
+}
diff --git a/nixos/modules/services/networking/xandikos.nix b/nixos/modules/services/networking/xandikos.nix
new file mode 100644
index 00000000000..4bd45a76e67
--- /dev/null
+++ b/nixos/modules/services/networking/xandikos.nix
@@ -0,0 +1,148 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xandikos;
+in
+{
+
+  options = {
+    services.xandikos = {
+      enable = mkEnableOption "Xandikos CalDAV and CardDAV server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.xandikos;
+        defaultText = literalExpression "pkgs.xandikos";
+        description = "The Xandikos package to use.";
+      };
+
+      address = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = ''
+          The IP address on which Xandikos will listen.
+          By default listens on localhost.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 8080;
+        description = "The port of the Xandikos web application";
+      };
+
+      routePrefix = mkOption {
+        type = types.str;
+        default = "/";
+        description = ''
+          Path to Xandikos.
+          Useful when Xandikos is behind a reverse proxy.
+        '';
+      };
+
+      extraOptions = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        example = literalExpression ''
+          [ "--autocreate"
+            "--defaults"
+            "--current-user-principal user"
+            "--dump-dav-xml"
+          ]
+        '';
+        description = ''
+          Extra command line arguments to pass to xandikos.
+        '';
+      };
+
+      nginx = mkOption {
+        default = {};
+        description = ''
+          Configuration for nginx reverse proxy.
+        '';
+
+        type = types.submodule {
+          options = {
+            enable = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Configure the nginx reverse proxy settings.
+              '';
+            };
+
+            hostName = mkOption {
+              type = types.str;
+              description = ''
+                The hostname use to setup the virtualhost configuration
+              '';
+            };
+          };
+        };
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable (
+    mkMerge [
+      {
+        meta.maintainers = with lib.maintainers; [ _0x4A6F ];
+
+        systemd.services.xandikos = {
+          description = "A Simple Calendar and Contact Server";
+          after = [ "network.target" ];
+          wantedBy = [ "multi-user.target" ];
+
+          serviceConfig = {
+            User = "xandikos";
+            Group = "xandikos";
+            DynamicUser = "yes";
+            RuntimeDirectory = "xandikos";
+            StateDirectory = "xandikos";
+            StateDirectoryMode = "0700";
+            PrivateDevices = true;
+            # Sandboxing
+            CapabilityBoundingSet = "CAP_NET_RAW CAP_NET_ADMIN";
+            ProtectSystem = "strict";
+            ProtectHome = true;
+            PrivateTmp = true;
+            ProtectKernelTunables = true;
+            ProtectKernelModules = true;
+            ProtectControlGroups = true;
+            RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_PACKET AF_NETLINK";
+            RestrictNamespaces = true;
+            LockPersonality = true;
+            MemoryDenyWriteExecute = true;
+            RestrictRealtime = true;
+            RestrictSUIDSGID = true;
+            ExecStart = ''
+              ${cfg.package}/bin/xandikos \
+                --directory /var/lib/xandikos \
+                --listen-address ${cfg.address} \
+                --port ${toString cfg.port} \
+                --route-prefix ${cfg.routePrefix} \
+                ${lib.concatStringsSep " " cfg.extraOptions}
+            '';
+          };
+        };
+      }
+
+      (
+        mkIf cfg.nginx.enable {
+          services.nginx = {
+            enable = true;
+            virtualHosts."${cfg.nginx.hostName}" = {
+              locations."/" = {
+                proxyPass = "http://${cfg.address}:${toString cfg.port}/";
+              };
+            };
+          };
+        }
+      )
+    ]
+  );
+}
diff --git a/nixos/modules/services/networking/xinetd.nix b/nixos/modules/services/networking/xinetd.nix
new file mode 100644
index 00000000000..2f527ab156a
--- /dev/null
+++ b/nixos/modules/services/networking/xinetd.nix
@@ -0,0 +1,147 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xinetd;
+
+  configFile = pkgs.writeText "xinetd.conf"
+    ''
+      defaults
+      {
+        log_type       = SYSLOG daemon info
+        log_on_failure = HOST
+        log_on_success = PID HOST DURATION EXIT
+        ${cfg.extraDefaults}
+      }
+
+      ${concatMapStrings makeService cfg.services}
+    '';
+
+  makeService = srv:
+    ''
+      service ${srv.name}
+      {
+        protocol    = ${srv.protocol}
+        ${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 ""}
+        wait        = ${if srv.protocol == "udp" then "yes" else "no"}
+        user        = ${srv.user}
+        server      = ${srv.server}
+        ${optionalString (srv.serverArgs != "") "server_args = ${srv.serverArgs}"}
+        ${srv.extraConfig}
+      }
+    '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.xinetd.enable = mkEnableOption "the xinetd super-server daemon";
+
+    services.xinetd.extraDefaults = mkOption {
+      default = "";
+      type = types.lines;
+      description = ''
+        Additional configuration lines added to the default section of xinetd's configuration.
+      '';
+    };
+
+    services.xinetd.services = mkOption {
+      default = [];
+      description = ''
+        A list of services provided by xinetd.
+      '';
+
+      type = with types; listOf (submodule ({
+
+        options = {
+
+          name = mkOption {
+            type = types.str;
+            example = "login";
+            description = "Name of the service.";
+          };
+
+          protocol = mkOption {
+            type = types.str;
+            default = "tcp";
+            description =
+              "Protocol of the service.  Usually <literal>tcp</literal> or <literal>udp</literal>.";
+          };
+
+          port = mkOption {
+            type = types.int;
+            default = 0;
+            example = 123;
+            description = "Port number of the service.";
+          };
+
+          user = mkOption {
+            type = types.str;
+            default = "nobody";
+            description = "User account for the service";
+          };
+
+          server = mkOption {
+            type = types.str;
+            example = "/foo/bin/ftpd";
+            description = "Path of the program that implements the service.";
+          };
+
+          serverArgs = mkOption {
+            type = types.separatedString " ";
+            default = "";
+            description = "Command-line arguments for the server program.";
+          };
+
+          flags = mkOption {
+            type = types.str;
+            default = "";
+            description = "";
+          };
+
+          unlisted = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Whether this server is listed in
+              <filename>/etc/services</filename>.  If so, the port
+              number can be omitted.
+            '';
+          };
+
+          extraConfig = mkOption {
+            type = types.lines;
+            default = "";
+            description = "Extra configuration-lines added to the section of the service.";
+          };
+
+        };
+
+      }));
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.xinetd = {
+      description = "xinetd server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.xinetd ];
+      script = "exec xinetd -syslog daemon -dontfork -stayalive -f ${configFile}";
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/xl2tpd.nix b/nixos/modules/services/networking/xl2tpd.nix
new file mode 100644
index 00000000000..7dbe51422d9
--- /dev/null
+++ b/nixos/modules/services/networking/xl2tpd.nix
@@ -0,0 +1,143 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+  options = {
+    services.xl2tpd = {
+      enable = mkEnableOption "xl2tpd, the Layer 2 Tunnelling Protocol Daemon";
+
+      serverIp = mkOption {
+        type        = types.str;
+        description = "The server-side IP address.";
+        default     = "10.125.125.1";
+      };
+
+      clientIpRange = mkOption {
+        type        = types.str;
+        description = "The range from which client IPs are drawn.";
+        default     = "10.125.125.2-11";
+      };
+
+      extraXl2tpOptions = mkOption {
+        type        = types.lines;
+        description = "Adds extra lines to the xl2tpd configuration file.";
+        default     = "";
+      };
+
+      extraPppdOptions = mkOption {
+        type        = types.lines;
+        description = "Adds extra lines to the pppd options file.";
+        default     = "";
+        example     = ''
+          ms-dns 8.8.8.8
+          ms-dns 8.8.4.4
+        '';
+      };
+    };
+  };
+
+  config = mkIf config.services.xl2tpd.enable {
+    systemd.services.xl2tpd = let
+      cfg = config.services.xl2tpd;
+
+      # Config files from https://help.ubuntu.com/community/L2TPServer
+      xl2tpd-conf = pkgs.writeText "xl2tpd.conf" ''
+        [global]
+        ipsec saref = no
+
+        [lns default]
+        local ip = ${cfg.serverIp}
+        ip range = ${cfg.clientIpRange}
+        pppoptfile = ${pppd-options}
+        length bit = yes
+
+        ; Extra
+        ${cfg.extraXl2tpOptions}
+      '';
+
+      pppd-options = pkgs.writeText "ppp-options-xl2tpd.conf" ''
+        refuse-pap
+        refuse-chap
+        refuse-mschap
+        require-mschap-v2
+        # require-mppe-128
+        asyncmap 0
+        auth
+        crtscts
+        idle 1800
+        mtu 1200
+        mru 1200
+        lock
+        hide-password
+        local
+        # debug
+        name xl2tpd
+        # proxyarp
+        lcp-echo-interval 30
+        lcp-echo-failure 4
+
+        # Extra:
+        ${cfg.extraPppdOptions}
+      '';
+
+      xl2tpd-ppp-wrapped = pkgs.stdenv.mkDerivation {
+        name         = "xl2tpd-ppp-wrapped";
+        phases       = [ "installPhase" ];
+        buildInputs  = with pkgs; [ makeWrapper ];
+        installPhase = ''
+          mkdir -p $out/bin
+
+          makeWrapper ${pkgs.ppp}/sbin/pppd $out/bin/pppd \
+            --set LD_PRELOAD    "${pkgs.libredirect}/lib/libredirect.so" \
+            --set NIX_REDIRECTS "/etc/ppp=/etc/xl2tpd/ppp"
+
+          makeWrapper ${pkgs.xl2tpd}/bin/xl2tpd $out/bin/xl2tpd \
+            --set LD_PRELOAD    "${pkgs.libredirect}/lib/libredirect.so" \
+            --set NIX_REDIRECTS "${pkgs.ppp}/sbin/pppd=$out/bin/pppd"
+        '';
+      };
+    in {
+      description = "xl2tpd server";
+
+      requires = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        mkdir -p -m 700 /etc/xl2tpd
+
+        pushd /etc/xl2tpd > /dev/null
+
+        mkdir -p -m 700 ppp
+
+        [ -f ppp/chap-secrets ] || cat > ppp/chap-secrets << EOF
+        # Secrets for authentication using CHAP
+        # client	server	secret		IP addresses
+        #username	xl2tpd	password	*
+        EOF
+
+        chown root.root ppp/chap-secrets
+        chmod 600 ppp/chap-secrets
+
+        # The documentation says this file should be present but doesn't explain why and things work even if not there:
+        [ -f l2tp-secrets ] || (echo -n "* * "; ${pkgs.apg}/bin/apg -n 1 -m 32 -x 32 -a 1 -M LCN) > l2tp-secrets
+        chown root.root l2tp-secrets
+        chmod 600 l2tp-secrets
+
+        popd > /dev/null
+
+        mkdir -p /run/xl2tpd
+        chown root.root /run/xl2tpd
+        chmod 700       /run/xl2tpd
+      '';
+
+      serviceConfig = {
+        ExecStart = "${xl2tpd-ppp-wrapped}/bin/xl2tpd -D -c ${xl2tpd-conf} -s /etc/xl2tpd/l2tp-secrets -p /run/xl2tpd/pid -C /run/xl2tpd/control";
+        KillMode  = "process";
+        Restart   = "on-success";
+        Type      = "simple";
+        PIDFile   = "/run/xl2tpd/pid";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/xrdp.nix b/nixos/modules/services/networking/xrdp.nix
new file mode 100644
index 00000000000..747fb7a1f9c
--- /dev/null
+++ b/nixos/modules/services/networking/xrdp.nix
@@ -0,0 +1,185 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xrdp;
+  confDir = pkgs.runCommand "xrdp.conf" { preferLocalBuild = true; } ''
+    mkdir $out
+
+    cp ${cfg.package}/etc/xrdp/{km-*,xrdp,sesman,xrdp_keyboard}.ini $out
+
+    cat > $out/startwm.sh <<EOF
+    #!/bin/sh
+    . /etc/profile
+    ${cfg.defaultWindowManager}
+    EOF
+    chmod +x $out/startwm.sh
+
+    substituteInPlace $out/xrdp.ini \
+      --replace "#rsakeys_ini=" "rsakeys_ini=/run/xrdp/rsakeys.ini" \
+      --replace "certificate=" "certificate=${cfg.sslCert}" \
+      --replace "key_file=" "key_file=${cfg.sslKey}" \
+      --replace LogFile=xrdp.log LogFile=/dev/null \
+      --replace EnableSyslog=true EnableSyslog=false
+
+    substituteInPlace $out/sesman.ini \
+      --replace LogFile=xrdp-sesman.log LogFile=/dev/null \
+      --replace EnableSyslog=1 EnableSyslog=0
+
+    # Ensure that clipboard works for non-ASCII characters
+    sed -i -e '/.*SessionVariables.*/ a\
+    LANG=${config.i18n.defaultLocale}\
+    LOCALE_ARCHIVE=${config.i18n.glibcLocales}/lib/locale/locale-archive
+    ' $out/sesman.ini
+  '';
+in
+{
+
+  ###### interface
+
+  options = {
+
+    services.xrdp = {
+
+      enable = mkEnableOption "xrdp, the Remote Desktop Protocol server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.xrdp;
+        defaultText = literalExpression "pkgs.xrdp";
+        description = ''
+          The package to use for the xrdp daemon's binary.
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 3389;
+        description = ''
+          Specifies on which port the xrdp daemon listens.
+        '';
+      };
+
+      openFirewall = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to open the firewall for the specified RDP port.";
+      };
+
+      sslKey = mkOption {
+        type = types.str;
+        default = "/etc/xrdp/key.pem";
+        example = "/path/to/your/key.pem";
+        description = ''
+          ssl private key path
+          A self-signed certificate will be generated if file not exists.
+        '';
+      };
+
+      sslCert = mkOption {
+        type = types.str;
+        default = "/etc/xrdp/cert.pem";
+        example = "/path/to/your/cert.pem";
+        description = ''
+          ssl certificate path
+          A self-signed certificate will be generated if file not exists.
+        '';
+      };
+
+      defaultWindowManager = mkOption {
+        type = types.str;
+        default = "xterm";
+        example = "xfce4-session";
+        description = ''
+          The script to run when user log in, usually a window manager, e.g. "icewm", "xfce4-session"
+          This is per-user overridable, if file ~/startwm.sh exists it will be used instead.
+        '';
+      };
+
+      confDir = mkOption {
+        type = types.path;
+        default = confDir;
+        defaultText = literalDocBook "generated from configuration";
+        description = "The location of the config files for xrdp.";
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
+    # xrdp can run X11 program even if "services.xserver.enable = false"
+    xdg = {
+      autostart.enable = true;
+      menus.enable = true;
+      mime.enable = true;
+      icons.enable = true;
+    };
+
+    fonts.enableDefaultFonts = mkDefault true;
+
+    systemd = {
+      services.xrdp = {
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        description = "xrdp daemon";
+        requires = [ "xrdp-sesman.service" ];
+        preStart = ''
+          # prepare directory for unix sockets (the sockets will be owned by loggedinuser:xrdp)
+          mkdir -p /tmp/.xrdp || true
+          chown xrdp:xrdp /tmp/.xrdp
+          chmod 3777 /tmp/.xrdp
+
+          # generate a self-signed certificate
+          if [ ! -s ${cfg.sslCert} -o ! -s ${cfg.sslKey} ]; then
+            mkdir -p $(dirname ${cfg.sslCert}) || true
+            mkdir -p $(dirname ${cfg.sslKey}) || true
+            ${pkgs.openssl.bin}/bin/openssl req -x509 -newkey rsa:2048 -sha256 -nodes -days 365 \
+              -subj /C=US/ST=CA/L=Sunnyvale/O=xrdp/CN=www.xrdp.org \
+              -config ${cfg.package}/share/xrdp/openssl.conf \
+              -keyout ${cfg.sslKey} -out ${cfg.sslCert}
+            chown root:xrdp ${cfg.sslKey} ${cfg.sslCert}
+            chmod 440 ${cfg.sslKey} ${cfg.sslCert}
+          fi
+          if [ ! -s /run/xrdp/rsakeys.ini ]; then
+            mkdir -p /run/xrdp
+            ${cfg.package}/bin/xrdp-keygen xrdp /run/xrdp/rsakeys.ini
+          fi
+        '';
+        serviceConfig = {
+          User = "xrdp";
+          Group = "xrdp";
+          PermissionsStartOnly = true;
+          ExecStart = "${cfg.package}/bin/xrdp --nodaemon --port ${toString cfg.port} --config ${cfg.confDir}/xrdp.ini";
+        };
+      };
+
+      services.xrdp-sesman = {
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        description = "xrdp session manager";
+        restartIfChanged = false; # do not restart on "nixos-rebuild switch". like "display-manager", it can have many interactive programs as children
+        serviceConfig = {
+          ExecStart = "${cfg.package}/bin/xrdp-sesman --nodaemon --config ${cfg.confDir}/sesman.ini";
+          ExecStop  = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
+        };
+      };
+
+    };
+
+    users.users.xrdp = {
+      description   = "xrdp daemon user";
+      isSystemUser  = true;
+      group         = "xrdp";
+    };
+    users.groups.xrdp = {};
+
+    security.pam.services.xrdp-sesman = { allowNullPassword = true; startSession = true; };
+  };
+
+}
diff --git a/nixos/modules/services/networking/yggdrasil.nix b/nixos/modules/services/networking/yggdrasil.nix
new file mode 100644
index 00000000000..99c18ae6919
--- /dev/null
+++ b/nixos/modules/services/networking/yggdrasil.nix
@@ -0,0 +1,201 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  keysPath = "/var/lib/yggdrasil/keys.json";
+
+  cfg = config.services.yggdrasil;
+  configProvided = cfg.config != { };
+  configFileProvided = cfg.configFile != null;
+
+in {
+  options = with types; {
+    services.yggdrasil = {
+      enable = mkEnableOption "the yggdrasil system service";
+
+      config = mkOption {
+        type = attrs;
+        default = {};
+        example = {
+          Peers = [
+            "tcp://aa.bb.cc.dd:eeeee"
+            "tcp://[aaaa:bbbb:cccc:dddd::eeee]:fffff"
+          ];
+          Listen = [
+            "tcp://0.0.0.0:xxxxx"
+          ];
+        };
+        description = ''
+          Configuration for yggdrasil, as a Nix attribute set.
+
+          Warning: this is stored in the WORLD-READABLE Nix store!
+          Therefore, it is not appropriate for private keys. If you
+          wish to specify the keys, use <option>configFile</option>.
+
+          If the <option>persistentKeys</option> is enabled then the
+          keys that are generated during activation will override
+          those in <option>config</option> or
+          <option>configFile</option>.
+
+          If no keys are specified then ephemeral keys are generated
+          and the Yggdrasil interface will have a random IPv6 address
+          each time the service is started, this is the default.
+
+          If both <option>configFile</option> and <option>config</option>
+          are supplied, they will be combined, with values from
+          <option>configFile</option> taking precedence.
+
+          You can use the command <code>nix-shell -p yggdrasil --run
+          "yggdrasil -genconf"</code> to generate default
+          configuration values with documentation.
+        '';
+      };
+
+      configFile = mkOption {
+        type = nullOr path;
+        default = null;
+        example = "/run/keys/yggdrasil.conf";
+        description = ''
+          A file which contains JSON configuration for yggdrasil.
+          See the <option>config</option> option for more information.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "root";
+        example = "wheel";
+        description = "Group to grant access to the Yggdrasil control socket.";
+      };
+
+      openMulticastPort = mkOption {
+        type = bool;
+        default = false;
+        description = ''
+          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 <code>LinkLocalTCPPort</code> in your
+          yggdrasil configuration (<option>config</option> or
+          <option>configFile</option>) to a port number other than 0,
+          and then add that port to
+          <option>networking.firewall.allowedTCPPorts</option>.
+        '';
+      };
+
+      denyDhcpcdInterfaces = mkOption {
+        type = listOf str;
+        default = [];
+        example = [ "tap*" ];
+        description = ''
+          Disable the DHCP client for any interface whose name matches
+          any of the shell glob patterns in this list.  Use this
+          option to prevent the DHCP client from broadcasting requests
+          on the yggdrasil network.  It is only necessary to do so
+          when yggdrasil is running in TAP mode, because TUN
+          interfaces do not support broadcasting.
+        '';
+      };
+
+      package = mkOption {
+        type = package;
+        default = pkgs.yggdrasil;
+        defaultText = literalExpression "pkgs.yggdrasil";
+        description = "Yggdrasil package to use.";
+      };
+
+      persistentKeys = mkEnableOption ''
+        If enabled then keys will be generated once and Yggdrasil
+        will retain the same IPv6 address when the service is
+        restarted. Keys are stored at ${keysPath}.
+      '';
+
+    };
+  };
+
+  config = mkIf cfg.enable (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";
+      bindsTo = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      preStart =
+        (if configProvided || configFileProvided || cfg.persistentKeys then
+          "echo "
+
+          + (lib.optionalString configProvided
+            "'${builtins.toJSON cfg.config}'")
+          + (lib.optionalString configFileProvided "$(cat ${cfg.configFile})")
+          + (lib.optionalString cfg.persistentKeys "$(cat ${keysPath})")
+          + " | ${pkgs.jq}/bin/jq -s add | ${binYggdrasil} -normaliseconf -useconf"
+        else
+          "${binYggdrasil} -genconf") + " > /run/yggdrasil/yggdrasil.conf";
+
+      serviceConfig = {
+        ExecStart =
+          "${binYggdrasil} -useconffile /run/yggdrasil/yggdrasil.conf";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        Restart = "always";
+
+        Group = cfg.group;
+        RuntimeDirectory = "yggdrasil";
+        RuntimeDirectoryMode = "0750";
+        BindReadOnlyPaths = lib.optional configFileProvided cfg.configFile
+          ++ lib.optional cfg.persistentKeys keysPath;
+
+        # TODO: as of yggdrasil 0.3.8 and systemd 243, yggdrasil fails
+        # to set up the network adapter when DynamicUser is set.  See
+        # github.com/yggdrasil-network/yggdrasil-go/issues/557.  The
+        # following options are implied by DynamicUser according to
+        # the systemd.exec documentation, and can be removed if the
+        # upstream issue is fixed and DynamicUser is set to true:
+        PrivateTmp = true;
+        RemoveIPC = true;
+        NoNewPrivileges = true;
+        ProtectSystem = "strict";
+        RestrictSUIDSGID = true;
+        # End of list of options implied by DynamicUser.
+
+        AmbientCapabilities = "CAP_NET_ADMIN";
+        CapabilityBoundingSet = "CAP_NET_ADMIN";
+        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 = "~@clock @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @resources";
+      };
+    };
+
+    networking.dhcpcd.denyInterfaces = cfg.denyDhcpcdInterfaces;
+    networking.firewall.allowedUDPPorts = mkIf cfg.openMulticastPort [ 9001 ];
+
+    # Make yggdrasilctl available on the command line.
+    environment.systemPackages = [ cfg.package ];
+  });
+  meta = {
+    doc = ./yggdrasil.xml;
+    maintainers = with lib.maintainers; [ gazally ehmry ];
+  };
+}
diff --git a/nixos/modules/services/networking/yggdrasil.xml b/nixos/modules/services/networking/yggdrasil.xml
new file mode 100644
index 00000000000..a341d5d8153
--- /dev/null
+++ b/nixos/modules/services/networking/yggdrasil.xml
@@ -0,0 +1,156 @@
+<?xml version="1.0"?>
+<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xi="http://www.w3.org/2001/XInclude" version="5.0" xml:id="module-services-networking-yggdrasil">
+  <title>Yggdrasil</title>
+  <para>
+    <emphasis>Source:</emphasis>
+    <filename>modules/services/networking/yggdrasil/default.nix</filename>
+  </para>
+  <para>
+    <emphasis>Upstream documentation:</emphasis>
+    <link xlink:href="https://yggdrasil-network.github.io/"/>
+  </para>
+  <para>
+Yggdrasil is an early-stage implementation of a fully end-to-end encrypted,
+self-arranging IPv6 network.
+</para>
+  <section xml:id="module-services-networking-yggdrasil-configuration">
+    <title>Configuration</title>
+    <section xml:id="module-services-networking-yggdrasil-configuration-simple">
+      <title>Simple ephemeral node</title>
+      <para>
+An annotated example of a simple configuration:
+<programlisting>
+{
+  services.yggdrasil = {
+    enable = true;
+    persistentKeys = false;
+      # The NixOS module will generate new keys and a new IPv6 address each time
+      # it is started if persistentKeys is not enabled.
+
+    config = {
+      Peers = [
+        # Yggdrasil will automatically connect and "peer" with other nodes it
+        # discovers via link-local multicast annoucements. Unless this is the
+        # case (it probably isn't) a node needs peers within the existing
+        # network that it can tunnel to.
+        "tcp://1.2.3.4:1024"
+        "tcp://1.2.3.5:1024"
+        # Public peers can be found at
+        # https://github.com/yggdrasil-network/public-peers
+      ];
+    };
+  };
+}
+</programlisting>
+   </para>
+    </section>
+    <section xml:id="module-services-networking-yggdrasil-configuration-prefix">
+      <title>Persistent node with prefix</title>
+      <para>
+A node with a fixed address that announces a prefix:
+<programlisting>
+let
+  address = "210:5217:69c0:9afc:1b95:b9f:8718:c3d2";
+  prefix = "310:5217:69c0:9afc";
+  # taken from the output of "yggdrasilctl getself".
+in {
+
+  services.yggdrasil = {
+    enable = true;
+    persistentKeys = true; # Maintain a fixed public key and IPv6 address.
+    config = {
+      Peers = [ "tcp://1.2.3.4:1024" "tcp://1.2.3.5:1024" ];
+      NodeInfo = {
+        # This information is visible to the network.
+        name = config.networking.hostName;
+        location = "The North Pole";
+      };
+    };
+  };
+
+  boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1;
+    # Forward traffic under the prefix.
+
+  networking.interfaces.${eth0}.ipv6.addresses = [{
+    # Set a 300::/8 address on the local physical device.
+    address = prefix + "::1";
+    prefixLength = 64;
+  }];
+
+  services.radvd = {
+    # Annouce the 300::/8 prefix to eth0.
+    enable = true;
+    config = ''
+      interface eth0
+      {
+        AdvSendAdvert on;
+        prefix ${prefix}::/64 {
+          AdvOnLink on;
+          AdvAutonomous on;
+        };
+        route 200::/8 {};
+      };
+    '';
+  };
+}
+</programlisting>
+  </para>
+    </section>
+    <section xml:id="module-services-networking-yggdrasil-configuration-container">
+      <title>Yggdrasil attached Container</title>
+      <para>
+A NixOS container attached to the Yggdrasil network via a node running on the
+host:
+        <programlisting>
+let
+  yggPrefix64 = "310:5217:69c0:9afc";
+    # Again, taken from the output of "yggdrasilctl getself".
+in
+{
+  boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1;
+  # Enable IPv6 forwarding.
+
+  networking = {
+    bridges.br0.interfaces = [ ];
+    # A bridge only to containers&#x2026;
+
+    interfaces.br0 = {
+      # &#x2026; configured with a prefix address.
+      ipv6.addresses = [{
+        address = "${yggPrefix64}::1";
+        prefixLength = 64;
+      }];
+    };
+  };
+
+  containers.foo = {
+    autoStart = true;
+    privateNetwork = true;
+    hostBridge = "br0";
+    # Attach the container to the bridge only.
+    config = { config, pkgs, ... }: {
+      networking.interfaces.eth0.ipv6 = {
+        addresses = [{
+          # Configure a prefix address.
+          address = "${yggPrefix64}::2";
+          prefixLength = 64;
+        }];
+        routes = [{
+          # Configure the prefix route.
+          address = "200::";
+          prefixLength = 7;
+          via = "${yggPrefix64}::1";
+        }];
+      };
+
+      services.httpd.enable = true;
+      networking.firewall.allowedTCPPorts = [ 80 ];
+    };
+  };
+
+}
+</programlisting>
+      </para>
+    </section>
+  </section>
+</chapter>
diff --git a/nixos/modules/services/networking/zerobin.nix b/nixos/modules/services/networking/zerobin.nix
new file mode 100644
index 00000000000..16db25d6230
--- /dev/null
+++ b/nixos/modules/services/networking/zerobin.nix
@@ -0,0 +1,102 @@
+{ config, pkgs, lib, ... }:
+with lib;
+let
+  cfg = config.services.zerobin;
+
+  zerobin_config = pkgs.writeText "zerobin-config.py" ''
+  PASTE_FILES_ROOT = "${cfg.dataDir}"
+  ${cfg.extraConfig}
+  '';
+
+in
+  {
+    options = {
+      services.zerobin = {
+        enable = mkEnableOption "0bin";
+
+        dataDir = mkOption {
+          type = types.str;
+          default = "/var/lib/zerobin";
+          description = ''
+          Path to the 0bin data directory
+          '';
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "zerobin";
+          description = ''
+          The user 0bin should run as
+          '';
+        };
+
+        group = mkOption {
+          type = types.str;
+          default = "zerobin";
+          description = ''
+          The group 0bin should run as
+          '';
+        };
+
+        listenPort = mkOption {
+          type = types.int;
+          default = 8000;
+          example = 1357;
+          description = ''
+          The port zerobin should listen on
+          '';
+        };
+
+        listenAddress = mkOption {
+          type = types.str;
+          default = "localhost";
+          example = "127.0.0.1";
+          description = ''
+          The address zerobin should listen to
+          '';
+        };
+
+        extraConfig = mkOption {
+          type = types.lines;
+          default = "";
+          example = ''
+          MENU = (
+          ('Home', '/'),
+          )
+          COMPRESSED_STATIC_FILE = True
+          '';
+          description = ''
+          Extra configuration to be appended to the 0bin config file
+          (see https://0bin.readthedocs.org/en/latest/en/options.html)
+          '';
+        };
+      };
+    };
+
+    config = mkIf (cfg.enable) {
+      users.users.${cfg.user} =
+      if cfg.user == "zerobin" then {
+        isSystemUser = true;
+        group = cfg.group;
+        home = cfg.dataDir;
+        createHome = true;
+      }
+      else {};
+      users.groups.${cfg.group} = {};
+
+      systemd.services.zerobin = {
+        enable = true;
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig.ExecStart = "${pkgs.zerobin}/bin/zerobin ${cfg.listenAddress} ${toString cfg.listenPort} false ${cfg.user} ${cfg.group} ${zerobin_config}";
+        serviceConfig.PrivateTmp="yes";
+        serviceConfig.User = cfg.user;
+        serviceConfig.Group = cfg.group;
+        preStart = ''
+          mkdir -p ${cfg.dataDir}
+          chown ${cfg.user} ${cfg.dataDir}
+        '';
+      };
+    };
+  }
+
diff --git a/nixos/modules/services/networking/zeronet.nix b/nixos/modules/services/networking/zeronet.nix
new file mode 100644
index 00000000000..3370390a4c6
--- /dev/null
+++ b/nixos/modules/services/networking/zeronet.nix
@@ -0,0 +1,94 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) generators literalExpression mkEnableOption mkIf mkOption recursiveUpdate types;
+  cfg = config.services.zeronet;
+  dataDir = "/var/lib/zeronet";
+  configFile = pkgs.writeText "zeronet.conf" (generators.toINI {} (recursiveUpdate defaultSettings cfg.settings));
+
+  defaultSettings = {
+    global = {
+      data_dir = dataDir;
+      log_dir = dataDir;
+      ui_port = cfg.port;
+      fileserver_port = cfg.fileserverPort;
+      tor = if !cfg.tor then "disable" else if cfg.torAlways then "always" else "enable";
+    };
+  };
+in with lib; {
+  options.services.zeronet = {
+    enable = mkEnableOption "zeronet";
+
+    settings = mkOption {
+      type = with types; attrsOf (oneOf [ str int bool (listOf str) ]);
+      default = {};
+      example = literalExpression "{ global.tor = enable; }";
+
+      description = ''
+        <filename>zeronet.conf</filename> configuration. Refer to
+        <link xlink:href="https://zeronet.readthedocs.io/en/latest/faq/#is-it-possible-to-use-a-configuration-file"/>
+        for details on supported values;
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 43110;
+      description = "Optional zeronet web UI port.";
+    };
+
+    fileserverPort = mkOption {
+      # Not optional: when absent zeronet tries to write one to the
+      # read-only config file and crashes
+      type = types.port;
+      default = 12261;
+      description = "Zeronet fileserver port.";
+    };
+
+    tor = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Use TOR for zeronet traffic where possible.";
+    };
+
+    torAlways = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Use TOR for all zeronet traffic.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.tor = mkIf cfg.tor {
+      enable = true;
+      controlPort = 9051;
+
+      extraConfig = ''
+        CacheDirectoryGroupReadable 1
+        CookieAuthentication 1
+        CookieAuthFileGroupReadable 1
+      '';
+    };
+
+    systemd.services.zeronet = {
+      description = "zeronet";
+      after = [ "network.target" (optionalString cfg.tor "tor.service") ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        User = "zeronet";
+        DynamicUser = true;
+        StateDirectory = "zeronet";
+        SupplementaryGroups = mkIf cfg.tor [ "tor" ];
+        ExecStart = "${pkgs.zeronet}/bin/zeronet --config_file ${configFile}";
+      };
+    };
+  };
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "zeronet" "dataDir" ] "Zeronet will store data by default in /var/lib/zeronet")
+    (mkRemovedOptionModule [ "services" "zeronet" "logDir" ] "Zeronet will log by default in /var/lib/zeronet")
+  ];
+
+  meta.maintainers = with maintainers; [ chiiruno ];
+}
diff --git a/nixos/modules/services/networking/zerotierone.nix b/nixos/modules/services/networking/zerotierone.nix
new file mode 100644
index 00000000000..3bc7d3ac0db
--- /dev/null
+++ b/nixos/modules/services/networking/zerotierone.nix
@@ -0,0 +1,81 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.zerotierone;
+in
+{
+  options.services.zerotierone.enable = mkEnableOption "ZeroTierOne";
+
+  options.services.zerotierone.joinNetworks = mkOption {
+    default = [];
+    example = [ "a8a2c3c10c1a68de" ];
+    type = types.listOf types.str;
+    description = ''
+      List of ZeroTier Network IDs to join on startup
+    '';
+  };
+
+  options.services.zerotierone.port = mkOption {
+    default = 9993;
+    type = types.int;
+    description = ''
+      Network port used by ZeroTier.
+    '';
+  };
+
+  options.services.zerotierone.package = mkOption {
+    default = pkgs.zerotierone;
+    defaultText = literalExpression "pkgs.zerotierone";
+    type = types.package;
+    description = ''
+      ZeroTier One package to use.
+    '';
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.zerotierone = {
+      description = "ZeroTierOne";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      wants = [ "network-online.target" ];
+
+      path = [ cfg.package ];
+
+      preStart = ''
+        mkdir -p /var/lib/zerotier-one/networks.d
+        chmod 700 /var/lib/zerotier-one
+        chown -R root:root /var/lib/zerotier-one
+      '' + (concatMapStrings (netId: ''
+        touch "/var/lib/zerotier-one/networks.d/${netId}.conf"
+      '') cfg.joinNetworks);
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/zerotier-one -p${toString cfg.port}";
+        Restart = "always";
+        KillMode = "process";
+        TimeoutStopSec = 5;
+      };
+    };
+
+    # ZeroTier does not issue DHCP leases, but some strangers might...
+    networking.dhcpcd.denyInterfaces = [ "zt*" ];
+
+    # ZeroTier receives UDP transmissions
+    networking.firewall.allowedUDPPorts = [ cfg.port ];
+
+    environment.systemPackages = [ cfg.package ];
+
+    # Prevent systemd from potentially changing the MAC address
+    systemd.network.links."50-zerotier" = {
+      matchConfig = {
+        OriginalName = "zt*";
+      };
+      linkConfig = {
+        AutoNegotiation = false;
+        MACAddressPolicy = "none";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/znc/default.nix b/nixos/modules/services/networking/znc/default.nix
new file mode 100644
index 00000000000..a98f92d2d71
--- /dev/null
+++ b/nixos/modules/services/networking/znc/default.nix
@@ -0,0 +1,335 @@
+{ config, lib, pkgs, ...}:
+
+with lib;
+
+let
+
+  cfg = config.services.znc;
+
+  defaultUser = "znc";
+
+  modules = pkgs.buildEnv {
+    name = "znc-modules";
+    paths = cfg.modulePackages;
+  };
+
+  listenerPorts = concatMap (l: optional (l ? Port) l.Port)
+    (attrValues (cfg.config.Listener or {}));
+
+  # Converts the config option to a string
+  semanticString = let
+
+      sortedAttrs = set: sort (l: r:
+        if l == "extraConfig" then false # Always put extraConfig last
+        else if isAttrs set.${l} == isAttrs set.${r} then l < r
+        else isAttrs set.${r} # Attrsets should be last, makes for a nice config
+        # This last case occurs when any side (but not both) is an attrset
+        # The order of these is correct when the attrset is on the right
+        # which we're just returning
+      ) (attrNames set);
+
+      # Specifies an attrset that encodes the value according to its type
+      encode = name: value: {
+          null = [];
+          bool = [ "${name} = ${boolToString value}" ];
+          int = [ "${name} = ${toString value}" ];
+
+          # extraConfig should be inserted verbatim
+          string = [ (if name == "extraConfig" then value else "${name} = ${value}") ];
+
+          # Values like `Foo = [ "bar" "baz" ];` should be transformed into
+          #   Foo=bar
+          #   Foo=baz
+          list = concatMap (encode name) value;
+
+          # Values like `Foo = { bar = { Baz = "baz"; Qux = "qux"; Florps = null; }; };` should be transmed into
+          #   <Foo bar>
+          #     Baz=baz
+          #     Qux=qux
+          #   </Foo>
+          set = concatMap (subname: optionals (value.${subname} != null) ([
+              "<${name} ${subname}>"
+            ] ++ map (line: "\t${line}") (toLines value.${subname}) ++ [
+              "</${name}>"
+            ])) (filter (v: v != null) (attrNames value));
+
+        }.${builtins.typeOf value};
+
+      # One level "above" encode, acts upon a set and uses encode on each name,value pair
+      toLines = set: concatMap (name: encode name set.${name}) (sortedAttrs set);
+
+    in
+      concatStringsSep "\n" (toLines cfg.config);
+
+  semanticTypes = with types; rec {
+    zncAtom = nullOr (oneOf [ int bool str ]);
+    zncAttr = attrsOf (nullOr zncConf);
+    zncAll = oneOf [ zncAtom (listOf zncAtom) zncAttr ];
+    zncConf = attrsOf (zncAll // {
+      # Since this is a recursive type and the description by default contains
+      # the description of its subtypes, infinite recursion would occur without
+      # explicitly breaking this cycle
+      description = "znc values (null, atoms (str, int, bool), list of atoms, or attrsets of znc values)";
+    });
+  };
+
+in
+
+{
+
+  imports = [ ./options.nix ];
+
+  options = {
+    services.znc = {
+      enable = mkEnableOption "ZNC";
+
+      user = mkOption {
+        default = "znc";
+        example = "john";
+        type = types.str;
+        description = ''
+          The name of an existing user account to use to own the ZNC server
+          process. If not specified, a default user will be created.
+        '';
+      };
+
+      group = mkOption {
+        default = defaultUser;
+        example = "users";
+        type = types.str;
+        description = ''
+          Group to own the ZNC process.
+        '';
+      };
+
+      dataDir = mkOption {
+        default = "/var/lib/znc";
+        example = "/home/john/.znc";
+        type = types.path;
+        description = ''
+          The state directory for ZNC. The config and the modules will be linked
+          to from this directory as well.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to open ports in the firewall for ZNC. Does work with
+          ports for listeners specified in
+          <option>services.znc.config.Listener</option>.
+        '';
+      };
+
+      config = mkOption {
+        type = semanticTypes.zncConf;
+        default = {};
+        example = literalExpression ''
+          {
+            LoadModule = [ "webadmin" "adminlog" ];
+            User.paul = {
+              Admin = true;
+              Nick = "paul";
+              AltNick = "paul1";
+              LoadModule = [ "chansaver" "controlpanel" ];
+              Network.libera = {
+                Server = "irc.libera.chat +6697";
+                LoadModule = [ "simple_away" ];
+                Chan = {
+                  "#nixos" = { Detached = false; };
+                  "##linux" = { Disabled = true; };
+                };
+              };
+              Pass.password = {
+                Method = "sha256";
+                Hash = "e2ce303c7ea75c571d80d8540a8699b46535be6a085be3414947d638e48d9e93";
+                Salt = "l5Xryew4g*!oa(ECfX2o";
+              };
+            };
+          }
+        '';
+        description = ''
+          Configuration for ZNC, see
+          <link xlink:href="https://wiki.znc.in/Configuration"/> for details. The
+          Nix value declared here will be translated directly to the xml-like
+          format ZNC expects. This is much more flexible than the legacy options
+          under <option>services.znc.confOptions.*</option>, but also can't do
+          any type checking.
+          </para>
+          <para>
+          You can use <command>nix-instantiate --eval --strict '&lt;nixpkgs/nixos&gt;' -A config.services.znc.config</command>
+          to view the current value. By default it contains a listener for port
+          5000 with SSL enabled.
+          </para>
+          <para>
+          Nix attributes called <literal>extraConfig</literal> will be inserted
+          verbatim into the resulting config file.
+          </para>
+          <para>
+          If <option>services.znc.useLegacyConfig</option> is turned on, the
+          option values in <option>services.znc.confOptions.*</option> will be
+          gracefully be applied to this option.
+          </para>
+          <para>
+          If you intend to update the configuration through this option, be sure
+          to enable <option>services.znc.mutable</option>, otherwise none of the
+          changes here will be applied after the initial deploy.
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.path;
+        example = literalExpression "~/.znc/configs/znc.conf";
+        description = ''
+          Configuration file for ZNC. It is recommended to use the
+          <option>config</option> option instead.
+          </para>
+          <para>
+          Setting this option will override any auto-generated config file
+          through the <option>confOptions</option> or <option>config</option>
+          options.
+        '';
+      };
+
+      modulePackages = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        example = literalExpression "[ pkgs.zncModules.fish pkgs.zncModules.push ]";
+        description = ''
+          A list of global znc module packages to add to znc.
+        '';
+      };
+
+      mutable = mkOption {
+        default = true; # TODO: Default to true when config is set, make sure to not delete the old config if present
+        type = types.bool;
+        description = ''
+          Indicates whether to allow the contents of the
+          <literal>dataDir</literal> directory to be changed by the user at
+          run-time.
+          </para>
+          <para>
+          If enabled, modifications to the ZNC configuration after its initial
+          creation are not overwritten by a NixOS rebuild. If disabled, the
+          ZNC configuration is rebuilt on every NixOS rebuild.
+          </para>
+          <para>
+          If the user wants to manage the ZNC service using the web admin
+          interface, this option should be enabled.
+        '';
+      };
+
+      extraFlags = mkOption {
+        default = [ ];
+        example = [ "--debug" ];
+        type = types.listOf types.str;
+        description = ''
+          Extra arguments to use for executing znc.
+        '';
+      };
+    };
+  };
+
+
+  ###### Implementation
+
+  config = mkIf cfg.enable {
+
+    services.znc = {
+      configFile = mkDefault (pkgs.writeText "znc-generated.conf" semanticString);
+      config = {
+        Version = lib.getVersion pkgs.znc;
+        Listener.l.Port = mkDefault 5000;
+        Listener.l.SSL = mkDefault true;
+      };
+    };
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall listenerPorts;
+
+    systemd.services.znc = {
+      description = "ZNC Server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        Restart = "always";
+        ExecStart = "${pkgs.znc}/bin/znc --foreground --datadir ${cfg.dataDir} ${escapeShellArgs cfg.extraFlags}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
+        # Hardening
+        CapabilityBoundingSet = [ "" ];
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        ReadWritePaths = [ cfg.dataDir ];
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+        UMask = "0027";
+      };
+      preStart = ''
+        mkdir -p ${cfg.dataDir}/configs
+
+        # If mutable, regenerate conf file every time.
+        ${optionalString (!cfg.mutable) ''
+          echo "znc is set to be system-managed. Now deleting old znc.conf file to be regenerated."
+          rm -f ${cfg.dataDir}/configs/znc.conf
+        ''}
+
+        # Ensure essential files exist.
+        if [[ ! -f ${cfg.dataDir}/configs/znc.conf ]]; then
+            echo "No znc.conf file found in ${cfg.dataDir}. Creating one now."
+            cp --no-preserve=ownership --no-clobber ${cfg.configFile} ${cfg.dataDir}/configs/znc.conf
+            chmod u+rw ${cfg.dataDir}/configs/znc.conf
+        fi
+
+        if [[ ! -f ${cfg.dataDir}/znc.pem ]]; then
+          echo "No znc.pem file found in ${cfg.dataDir}. Creating one now."
+          ${pkgs.znc}/bin/znc --makepem --datadir ${cfg.dataDir}
+        fi
+
+        # Symlink modules
+        rm ${cfg.dataDir}/modules || true
+        ln -fs ${modules}/lib/znc ${cfg.dataDir}/modules
+      '';
+    };
+
+    users.users = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} =
+        { description = "ZNC server daemon owner";
+          group = defaultUser;
+          uid = config.ids.uids.znc;
+          home = cfg.dataDir;
+          createHome = true;
+        };
+      };
+
+    users.groups = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} =
+        { gid = config.ids.gids.znc;
+          members = [ defaultUser ];
+        };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/networking/znc/options.nix b/nixos/modules/services/networking/znc/options.nix
new file mode 100644
index 00000000000..0db051126e8
--- /dev/null
+++ b/nixos/modules/services/networking/znc/options.nix
@@ -0,0 +1,270 @@
+{ lib, config, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.znc;
+
+  networkOpts = {
+    options = {
+
+      server = mkOption {
+        type = types.str;
+        example = "irc.libera.chat";
+        description = ''
+          IRC server address.
+        '';
+      };
+
+      port = mkOption {
+        type = types.ints.u16;
+        default = 6697;
+        description = ''
+          IRC server port.
+        '';
+      };
+
+      password = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          IRC server password, such as for a Slack gateway.
+        '';
+      };
+
+      useSSL = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to use SSL to connect to the IRC server.
+        '';
+      };
+
+      modules = mkOption {
+        type = types.listOf types.str;
+        default = [ "simple_away" ];
+        example = literalExpression ''[ "simple_away" "sasl" ]'';
+        description = ''
+          ZNC network modules to load.
+        '';
+      };
+
+      channels = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "nixos" ];
+        description = ''
+          IRC channels to join.
+        '';
+      };
+
+      hasBitlbeeControlChannel = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to add the special Bitlbee operations channel.
+        '';
+      };
+
+      extraConf = mkOption {
+        default = "";
+        type = types.lines;
+        example = ''
+          Encoding = ^UTF-8
+          FloodBurst = 4
+          FloodRate = 1.00
+          IRCConnectEnabled = true
+          Ident = johntron
+          JoinDelay = 0
+          Nick = johntron
+        '';
+        description = ''
+          Extra config for the network. Consider using
+          <option>services.znc.config</option> instead.
+        '';
+      };
+    };
+  };
+
+in
+
+{
+
+  options = {
+    services.znc = {
+
+      useLegacyConfig = mkOption {
+        default = true;
+        type = types.bool;
+        description = ''
+          Whether to propagate the legacy options under
+          <option>services.znc.confOptions.*</option> to the znc config. If this
+          is turned on, the znc config will contain a user with the default name
+          "znc", global modules "webadmin" and "adminlog" will be enabled by
+          default, and more, all controlled through the
+          <option>services.znc.confOptions.*</option> options.
+          You can use <command>nix-instantiate --eval --strict '&lt;nixpkgs/nixos&gt;' -A config.services.znc.config</command>
+          to view the current value of the config.
+          </para>
+          <para>
+          In any case, if you need more flexibility,
+          <option>services.znc.config</option> can be used to override/add to
+          all of the legacy options.
+        '';
+      };
+
+      confOptions = {
+        modules = mkOption {
+          type = types.listOf types.str;
+          default = [ "webadmin" "adminlog" ];
+          example = [ "partyline" "webadmin" "adminlog" "log" ];
+          description = ''
+            A list of modules to include in the `znc.conf` file.
+          '';
+        };
+
+        userModules = mkOption {
+          type = types.listOf types.str;
+          default = [ "chansaver" "controlpanel" ];
+          example = [ "chansaver" "controlpanel" "fish" "push" ];
+          description = ''
+            A list of user modules to include in the `znc.conf` file.
+          '';
+        };
+
+        userName = mkOption {
+          default = "znc";
+          example = "johntron";
+          type = types.str;
+          description = ''
+            The user name used to log in to the ZNC web admin interface.
+          '';
+        };
+
+        networks = mkOption {
+          default = { };
+          type = with types; attrsOf (submodule networkOpts);
+          description = ''
+            IRC networks to connect the user to.
+          '';
+          example = literalExpression ''
+            {
+              "libera" = {
+                server = "irc.libera.chat";
+                port = 6697;
+                useSSL = true;
+                modules = [ "simple_away" ];
+              };
+            };
+          '';
+        };
+
+        nick = mkOption {
+          default = "znc-user";
+          example = "john";
+          type = types.str;
+          description = ''
+            The IRC nick.
+          '';
+        };
+
+        passBlock = mkOption {
+          example = ''
+            &lt;Pass password&gt;
+               Method = sha256
+               Hash = e2ce303c7ea75c571d80d8540a8699b46535be6a085be3414947d638e48d9e93
+               Salt = l5Xryew4g*!oa(ECfX2o
+            &lt;/Pass&gt;
+          '';
+          type = types.str;
+          description = ''
+            Generate with `nix-shell -p znc --command "znc --makepass"`.
+            This is the password used to log in to the ZNC web admin interface.
+            You can also set this through
+            <option>services.znc.config.User.&lt;username&gt;.Pass.Method</option>
+            and co.
+          '';
+        };
+
+        port = mkOption {
+          default = 5000;
+          type = types.int;
+          description = ''
+            Specifies the port on which to listen.
+          '';
+        };
+
+        useSSL = mkOption {
+          default = true;
+          type = types.bool;
+          description = ''
+            Indicates whether the ZNC server should use SSL when listening on
+            the specified port. A self-signed certificate will be generated.
+          '';
+        };
+
+        uriPrefix = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          example = "/znc/";
+          description = ''
+            An optional URI prefix for the ZNC web interface. Can be
+            used to make ZNC available behind a reverse proxy.
+          '';
+        };
+
+        extraZncConf = mkOption {
+          default = "";
+          type = types.lines;
+          description = ''
+            Extra config to `znc.conf` file.
+          '';
+        };
+      };
+
+    };
+  };
+
+  config = mkIf cfg.useLegacyConfig {
+
+    services.znc.config = let
+      c = cfg.confOptions;
+      # defaults here should override defaults set in the non-legacy part
+      mkDefault = mkOverride 900;
+    in {
+      LoadModule = mkDefault c.modules;
+      Listener.l = {
+        Port = mkDefault c.port;
+        IPv4 = mkDefault true;
+        IPv6 = mkDefault true;
+        SSL = mkDefault c.useSSL;
+        URIPrefix = c.uriPrefix;
+      };
+      User.${c.userName} = {
+        Admin = mkDefault true;
+        Nick = mkDefault c.nick;
+        AltNick = mkDefault "${c.nick}_";
+        Ident = mkDefault c.nick;
+        RealName = mkDefault c.nick;
+        LoadModule = mkDefault c.userModules;
+        Network = mapAttrs (name: net: {
+          LoadModule = mkDefault net.modules;
+          Server = mkDefault "${net.server} ${optionalString net.useSSL "+"}${toString net.port} ${net.password}";
+          Chan = optionalAttrs net.hasBitlbeeControlChannel { "&bitlbee" = mkDefault {}; } //
+            listToAttrs (map (n: nameValuePair "#${n}" (mkDefault {})) net.channels);
+          extraConfig = if net.extraConf == "" then mkDefault null else net.extraConf;
+        }) c.networks;
+        extraConfig = [ c.passBlock ];
+      };
+      extraConfig = optional (c.extraZncConf != "") c.extraZncConf;
+    };
+  };
+
+  imports = [
+    (mkRemovedOptionModule ["services" "znc" "zncConf"] ''
+      Instead of `services.znc.zncConf = "... foo ...";`, use
+      `services.znc.configFile = pkgs.writeText "znc.conf" "... foo ...";`.
+    '')
+  ];
+}
diff --git a/nixos/modules/services/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix
new file mode 100644
index 00000000000..53091d8e2a0
--- /dev/null
+++ b/nixos/modules/services/printing/cupsd.nix
@@ -0,0 +1,460 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inherit (pkgs) cups cups-pk-helper cups-filters;
+
+  cfg = config.services.printing;
+
+  avahiEnabled = config.services.avahi.enable;
+  polkitEnabled = config.security.polkit.enable;
+
+  additionalBackends = pkgs.runCommand "additional-cups-backends" {
+      preferLocalBuild = true;
+    } ''
+      mkdir -p $out
+      if [ ! -e ${cups.out}/lib/cups/backend/smb ]; then
+        mkdir -p $out/lib/cups/backend
+        ln -sv ${pkgs.samba}/bin/smbspool $out/lib/cups/backend/smb
+      fi
+
+      # Provide support for printing via HTTPS.
+      if [ ! -e ${cups.out}/lib/cups/backend/https ]; then
+        mkdir -p $out/lib/cups/backend
+        ln -sv ${cups.out}/lib/cups/backend/ipp $out/lib/cups/backend/https
+      fi
+    '';
+
+  # Here we can enable additional backends, filters, etc. that are not
+  # part of CUPS itself, e.g. the SMB backend is part of Samba.  Since
+  # we can't update ${cups.out}/lib/cups itself, we create a symlink tree
+  # here and add the additional programs.  The ServerBin directive in
+  # cups-files.conf tells cupsd to use this tree.
+  bindir = pkgs.buildEnv {
+    name = "cups-progs";
+    paths =
+      [ cups.out additionalBackends cups-filters pkgs.ghostscript ]
+      ++ cfg.drivers;
+    pathsToLink = [ "/lib" "/share/cups" "/bin" ];
+    postBuild = cfg.bindirCmds;
+    ignoreCollisions = true;
+  };
+
+  writeConf = name: text: pkgs.writeTextFile {
+    inherit name text;
+    destination = "/etc/cups/${name}";
+  };
+
+  cupsFilesFile = writeConf "cups-files.conf" ''
+    SystemGroup root wheel
+
+    ServerBin ${bindir}/lib/cups
+    DataDir ${bindir}/share/cups
+    DocumentRoot ${cups.out}/share/doc/cups
+
+    AccessLog syslog
+    ErrorLog syslog
+    PageLog syslog
+
+    TempDir ${cfg.tempDir}
+
+    SetEnv PATH /var/lib/cups/path/lib/cups/filter:/var/lib/cups/path/bin
+
+    # User and group used to run external programs, including
+    # those that actually send the job to the printer.  Note that
+    # Udev sets the group of printer devices to `lp', so we want
+    # these programs to run as `lp' as well.
+    User cups
+    Group lp
+
+    ${cfg.extraFilesConf}
+  '';
+
+  cupsdFile = writeConf "cupsd.conf" ''
+    ${concatMapStrings (addr: ''
+      Listen ${addr}
+    '') cfg.listenAddresses}
+    Listen /run/cups/cups.sock
+
+    DefaultShared ${if cfg.defaultShared then "Yes" else "No"}
+
+    Browsing ${if cfg.browsing then "Yes" else "No"}
+
+    WebInterface ${if cfg.webInterface then "Yes" else "No"}
+
+    LogLevel ${cfg.logLevel}
+
+    ${cfg.extraConf}
+  '';
+
+  browsedFile = writeConf "cups-browsed.conf" cfg.browsedConf;
+
+  rootdir = pkgs.buildEnv {
+    name = "cups-progs";
+    paths = [
+      cupsFilesFile
+      cupsdFile
+      (writeConf "client.conf" cfg.clientConf)
+      (writeConf "snmp.conf" cfg.snmpConf)
+    ] ++ optional avahiEnabled browsedFile
+      ++ cfg.drivers;
+    pathsToLink = [ "/etc/cups" ];
+    ignoreCollisions = true;
+  };
+
+  filterGutenprint = filter (pkg: pkg.meta.isGutenprint or false == true);
+  containsGutenprint = pkgs: length (filterGutenprint pkgs) > 0;
+  getGutenprint = pkgs: head (filterGutenprint pkgs);
+
+in
+
+{
+
+  imports = [
+    (mkChangedOptionModule [ "services" "printing" "gutenprint" ] [ "services" "printing" "drivers" ]
+      (config:
+        let enabled = getAttrFromPath [ "services" "printing" "gutenprint" ] config;
+        in if enabled then [ pkgs.gutenprint ] else [ ]))
+    (mkRemovedOptionModule [ "services" "printing" "cupsFilesConf" ] "")
+    (mkRemovedOptionModule [ "services" "printing" "cupsdConf" ] "")
+  ];
+
+  ###### interface
+
+  options = {
+    services.printing = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable printing support through the CUPS daemon.
+        '';
+      };
+
+      startWhenNeeded = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          If set, CUPS is socket-activated; that is,
+          instead of having it permanently running as a daemon,
+          systemd will start it on the first incoming connection.
+        '';
+      };
+
+      listenAddresses = mkOption {
+        type = types.listOf types.str;
+        default = [ "localhost:631" ];
+        example = [ "*:631" ];
+        description = ''
+          A list of addresses and ports on which to listen.
+        '';
+      };
+
+      allowFrom = mkOption {
+        type = types.listOf types.str;
+        default = [ "localhost" ];
+        example = [ "all" ];
+        apply = concatMapStringsSep "\n" (x: "Allow ${x}");
+        description = ''
+          From which hosts to allow unconditional access.
+        '';
+      };
+
+      bindirCmds = mkOption {
+        type = types.lines;
+        internal = true;
+        default = "";
+        description = ''
+          Additional commands executed while creating the directory
+          containing the CUPS server binaries.
+        '';
+      };
+
+      defaultShared = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Specifies whether local printers are shared by default.
+        '';
+      };
+
+      browsing = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Specifies whether shared printers are advertised.
+        '';
+      };
+
+      webInterface = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Specifies whether the web interface is enabled.
+        '';
+      };
+
+      logLevel = mkOption {
+        type = types.str;
+        default = "info";
+        example = "debug";
+        description = ''
+          Specifies the cupsd logging verbosity.
+        '';
+      };
+
+      extraFilesConf = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra contents of the configuration file of the CUPS daemon
+          (<filename>cups-files.conf</filename>).
+        '';
+      };
+
+      extraConf = mkOption {
+        type = types.lines;
+        default = "";
+        example =
+          ''
+            BrowsePoll cups.example.com
+            MaxCopies 42
+          '';
+        description = ''
+          Extra contents of the configuration file of the CUPS daemon
+          (<filename>cupsd.conf</filename>).
+        '';
+      };
+
+      clientConf = mkOption {
+        type = types.lines;
+        default = "";
+        example =
+          ''
+            ServerName server.example.com
+            Encryption Never
+          '';
+        description = ''
+          The contents of the client configuration.
+          (<filename>client.conf</filename>)
+        '';
+      };
+
+      browsedConf = mkOption {
+        type = types.lines;
+        default = "";
+        example =
+          ''
+            BrowsePoll cups.example.com
+          '';
+        description = ''
+          The contents of the configuration. file of the CUPS Browsed daemon
+          (<filename>cups-browsed.conf</filename>)
+        '';
+      };
+
+      snmpConf = mkOption {
+        type = types.lines;
+        default = ''
+          Address @LOCAL
+        '';
+        description = ''
+          The contents of <filename>/etc/cups/snmp.conf</filename>. See "man
+          cups-snmp.conf" for a complete description.
+        '';
+      };
+
+      drivers = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        example = literalExpression "with pkgs; [ gutenprint hplip splix ]";
+        description = ''
+          CUPS drivers to use. Drivers provided by CUPS, cups-filters,
+          Ghostscript and Samba are added unconditionally. If this list contains
+          Gutenprint (i.e. a derivation with
+          <literal>meta.isGutenprint = true</literal>) the PPD files in
+          <filename>/var/lib/cups/ppd</filename> will be updated automatically
+          to avoid errors due to incompatible versions.
+        '';
+      };
+
+      tempDir = mkOption {
+        type = types.path;
+        default = "/tmp";
+        example = "/tmp/cups";
+        description = ''
+          CUPSd temporary directory.
+        '';
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.printing.enable {
+
+    users.users.cups =
+      { uid = config.ids.uids.cups;
+        group = "lp";
+        description = "CUPS printing services";
+      };
+
+    environment.systemPackages = [ cups.out ] ++ optional polkitEnabled cups-pk-helper;
+    environment.etc.cups.source = "/var/lib/cups";
+
+    services.dbus.packages = [ cups.out ] ++ optional polkitEnabled cups-pk-helper;
+
+    # Allow asswordless printer admin for members of wheel group
+    security.polkit.extraConfig = mkIf polkitEnabled ''
+      polkit.addRule(function(action, subject) {
+          if (action.id == "org.opensuse.cupspkhelper.mechanism.all-edit" &&
+              subject.isInGroup("wheel")){
+              return polkit.Result.YES;
+          }
+      });
+    '';
+
+    # Cups uses libusb to talk to printers, and does not use the
+    # linux kernel driver. If the driver is not in a black list, it
+    # gets loaded, and then cups cannot access the printers.
+    boot.blacklistedKernelModules = [ "usblp" ];
+
+    # Some programs like print-manager rely on this value to get
+    # printer test pages.
+    environment.sessionVariables.CUPS_DATADIR = "${bindir}/share/cups";
+
+    systemd.packages = [ cups.out ];
+
+    systemd.sockets.cups = mkIf cfg.startWhenNeeded {
+      wantedBy = [ "sockets.target" ];
+      listenStreams = [ "/run/cups/cups.sock" ]
+        ++ map (x: replaceStrings ["localhost"] ["127.0.0.1"] (removePrefix "*:" x)) cfg.listenAddresses;
+    };
+
+    systemd.services.cups =
+      { wantedBy = optionals (!cfg.startWhenNeeded) [ "multi-user.target" ];
+        wants = [ "network.target" ];
+        after = [ "network.target" ];
+
+        path = [ cups.out ];
+
+        preStart =
+          ''
+            mkdir -m 0700 -p /var/cache/cups
+            mkdir -m 0700 -p /var/spool/cups
+            mkdir -m 0755 -p ${cfg.tempDir}
+
+            mkdir -m 0755 -p /var/lib/cups
+            # While cups will automatically create self-signed certificates if accessed via TLS,
+            # this directory to store the certificates needs to be created manually.
+            mkdir -m 0700 -p /var/lib/cups/ssl
+
+            # Backwards compatibility
+            if [ ! -L /etc/cups ]; then
+              mv /etc/cups/* /var/lib/cups
+              rmdir /etc/cups
+              ln -s /var/lib/cups /etc/cups
+            fi
+            # First, clean existing symlinks
+            if [ -n "$(ls /var/lib/cups)" ]; then
+              for i in /var/lib/cups/*; do
+                [ -L "$i" ] && rm "$i"
+              done
+            fi
+            # Then, populate it with static files
+            cd ${rootdir}/etc/cups
+            for i in *; do
+              [ ! -e "/var/lib/cups/$i" ] && ln -s "${rootdir}/etc/cups/$i" "/var/lib/cups/$i"
+            done
+
+            #update path reference
+            [ -L /var/lib/cups/path ] && \
+              rm /var/lib/cups/path
+            [ ! -e /var/lib/cups/path ] && \
+              ln -s ${bindir} /var/lib/cups/path
+
+            ${optionalString (containsGutenprint cfg.drivers) ''
+              if [ -d /var/lib/cups/ppd ]; then
+                ${getGutenprint cfg.drivers}/bin/cups-genppdupdate -p /var/lib/cups/ppd
+              fi
+            ''}
+          '';
+
+          serviceConfig = {
+            PrivateTmp = true;
+            RuntimeDirectory = [ "cups" ];
+          };
+      };
+
+    systemd.services.cups-browsed = mkIf avahiEnabled
+      { description = "CUPS Remote Printer Discovery";
+
+        wantedBy = [ "multi-user.target" ];
+        wants = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service";
+        bindsTo = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service";
+        partOf = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service";
+        after = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service";
+
+        path = [ cups ];
+
+        serviceConfig.ExecStart = "${cups-filters}/bin/cups-browsed";
+
+        restartTriggers = [ browsedFile ];
+      };
+
+    services.printing.extraConf =
+      ''
+        DefaultAuthType Basic
+
+        <Location />
+          Order allow,deny
+          ${cfg.allowFrom}
+        </Location>
+
+        <Location /admin>
+          Order allow,deny
+          ${cfg.allowFrom}
+        </Location>
+
+        <Location /admin/conf>
+          AuthType Basic
+          Require user @SYSTEM
+          Order allow,deny
+          ${cfg.allowFrom}
+        </Location>
+
+        <Policy default>
+          <Limit Send-Document Send-URI Hold-Job Release-Job Restart-Job Purge-Jobs Set-Job-Attributes Create-Job-Subscription Renew-Subscription Cancel-Subscription Get-Notifications Reprocess-Job Cancel-Current-Job Suspend-Current-Job Resume-Job CUPS-Move-Job>
+            Require user @OWNER @SYSTEM
+            Order deny,allow
+          </Limit>
+
+          <Limit Pause-Printer Resume-Printer Set-Printer-Attributes Enable-Printer Disable-Printer Pause-Printer-After-Current-Job Hold-New-Jobs Release-Held-New-Jobs Deactivate-Printer Activate-Printer Restart-Printer Shutdown-Printer Startup-Printer Promote-Job Schedule-Job-After CUPS-Add-Printer CUPS-Delete-Printer CUPS-Add-Class CUPS-Delete-Class CUPS-Accept-Jobs CUPS-Reject-Jobs CUPS-Set-Default>
+            AuthType Basic
+            Require user @SYSTEM
+            Order deny,allow
+          </Limit>
+
+          <Limit Cancel-Job CUPS-Authenticate-Job>
+            Require user @OWNER @SYSTEM
+            Order deny,allow
+          </Limit>
+
+          <Limit All>
+            Order deny,allow
+          </Limit>
+        </Policy>
+      '';
+
+    security.pam.services.cups = {};
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ matthewbauer ];
+
+}
diff --git a/nixos/modules/services/scheduling/atd.nix b/nixos/modules/services/scheduling/atd.nix
new file mode 100644
index 00000000000..9bb0191ee46
--- /dev/null
+++ b/nixos/modules/services/scheduling/atd.nix
@@ -0,0 +1,106 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.atd;
+
+  inherit (pkgs) at;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.atd.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable the <command>at</command> daemon, a command scheduler.
+      '';
+    };
+
+    services.atd.allowEveryone = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to make <filename>/var/spool/at{jobs,spool}</filename>
+        writeable by everyone (and sticky).  This is normally not
+        needed since the <command>at</command> commands are
+        setuid/setgid <literal>atd</literal>.
+     '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    # Not wrapping "batch" because it's a shell script (kernel drops perms
+    # anyway) and it's patched to invoke the "at" setuid wrapper.
+    security.wrappers = builtins.listToAttrs (
+      map (program: { name = "${program}"; value = {
+      source = "${at}/bin/${program}";
+      owner = "atd";
+      group = "atd";
+      setuid = true;
+      setgid = true;
+    };}) [ "at" "atq" "atrm" ]);
+
+    environment.systemPackages = [ at ];
+
+    security.pam.services.atd = {};
+
+    users.users.atd =
+      {
+        uid = config.ids.uids.atd;
+        group = "atd";
+        description = "atd user";
+        home = "/var/empty";
+      };
+
+    users.groups.atd.gid = config.ids.gids.atd;
+
+    systemd.services.atd = {
+      description = "Job Execution Daemon (atd)";
+      wantedBy = [ "multi-user.target" ];
+
+      path = [ at ];
+
+      preStart = ''
+        # Snippets taken and adapted from the original `install' rule of
+        # the makefile.
+
+        # We assume these values are those actually used in Nixpkgs for
+        # `at'.
+        spooldir=/var/spool/atspool
+        jobdir=/var/spool/atjobs
+        etcdir=/etc/at
+
+        install -dm755 -o atd -g atd "$etcdir"
+        spool_and_job_dir_perms=${if cfg.allowEveryone then "1777" else "1770"}
+        install -dm"$spool_and_job_dir_perms" -o atd -g atd "$spooldir" "$jobdir"
+        if [ ! -f "$etcdir"/at.deny ]; then
+            touch "$etcdir"/at.deny
+            chown root:atd "$etcdir"/at.deny
+            chmod 640 "$etcdir"/at.deny
+        fi
+        if [ ! -f "$jobdir"/.SEQ ]; then
+            touch "$jobdir"/.SEQ
+            chown atd:atd "$jobdir"/.SEQ
+            chmod 600 "$jobdir"/.SEQ
+        fi
+      '';
+
+      script = "atd";
+
+      serviceConfig.Type = "forking";
+    };
+  };
+}
diff --git a/nixos/modules/services/scheduling/cron.nix b/nixos/modules/services/scheduling/cron.nix
new file mode 100644
index 00000000000..1fac54003cb
--- /dev/null
+++ b/nixos/modules/services/scheduling/cron.nix
@@ -0,0 +1,138 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  # Put all the system cronjobs together.
+  systemCronJobsFile = pkgs.writeText "system-crontab"
+    ''
+      SHELL=${pkgs.bash}/bin/bash
+      PATH=${config.system.path}/bin:${config.system.path}/sbin
+      ${optionalString (config.services.cron.mailto != null) ''
+        MAILTO="${config.services.cron.mailto}"
+      ''}
+      NIX_CONF_DIR=/etc/nix
+      ${lib.concatStrings (map (job: job + "\n") config.services.cron.systemCronJobs)}
+    '';
+
+  # Vixie cron requires build-time configuration for the sendmail path.
+  cronNixosPkg = pkgs.cron.override {
+    # The mail.nix nixos module, if there is any local mail system enabled,
+    # should have sendmail in this path.
+    sendmailPath = "/run/wrappers/bin/sendmail";
+  };
+
+  allFiles =
+    optional (config.services.cron.systemCronJobs != []) systemCronJobsFile
+    ++ config.services.cron.cronFiles;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.cron = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the Vixie cron daemon.";
+      };
+
+      mailto = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Email address to which job output will be mailed.";
+      };
+
+      systemCronJobs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = literalExpression ''
+          [ "* * * * *  test   ls -l / > /tmp/cronout 2>&1"
+            "* * * * *  eelco  echo Hello World > /home/eelco/cronout"
+          ]
+        '';
+        description = ''
+          A list of Cron jobs to be appended to the system-wide
+          crontab.  See the manual page for crontab for the expected
+          format. If you want to get the results mailed you must setuid
+          sendmail. See <option>security.wrappers</option>
+
+          If neither /var/cron/cron.deny nor /var/cron/cron.allow exist only root
+          is allowed to have its own crontab file. The /var/cron/cron.deny file
+          is created automatically for you, so every user can use a crontab.
+
+          Many nixos modules set systemCronJobs, so if you decide to disable vixie cron
+          and enable another cron daemon, you may want it to get its system crontab
+          based on systemCronJobs.
+        '';
+      };
+
+      cronFiles = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          A list of extra crontab files that will be read and appended to the main
+          crontab file when the cron service starts.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkMerge [
+
+    { services.cron.enable = mkDefault (allFiles != []); }
+    (mkIf (config.services.cron.enable) {
+      security.wrappers.crontab =
+        { setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${cronNixosPkg}/bin/crontab";
+        };
+      environment.systemPackages = [ cronNixosPkg ];
+      environment.etc.crontab =
+        { source = pkgs.runCommand "crontabs" { inherit allFiles; preferLocalBuild = true; }
+            ''
+              touch $out
+              for i in $allFiles; do
+                cat "$i" >> $out
+              done
+            '';
+          mode = "0600"; # Cron requires this.
+        };
+
+      systemd.services.cron =
+        { description = "Cron Daemon";
+
+          wantedBy = [ "multi-user.target" ];
+
+          preStart =
+            ''
+              mkdir -m 710 -p /var/cron
+
+              # By default, allow all users to create a crontab.  This
+              # is denoted by the existence of an empty cron.deny file.
+              if ! test -e /var/cron/cron.allow -o -e /var/cron/cron.deny; then
+                  touch /var/cron/cron.deny
+              fi
+            '';
+
+          restartTriggers = [ config.time.timeZone ];
+          serviceConfig.ExecStart = "${cronNixosPkg}/bin/cron -n";
+        };
+
+    })
+
+  ];
+
+}
diff --git a/nixos/modules/services/scheduling/fcron.nix b/nixos/modules/services/scheduling/fcron.nix
new file mode 100644
index 00000000000..acaa995f739
--- /dev/null
+++ b/nixos/modules/services/scheduling/fcron.nix
@@ -0,0 +1,170 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.fcron;
+
+  queuelen = if cfg.queuelen == null then "" else "-q ${toString cfg.queuelen}";
+
+  # Duplicate code, also found in cron.nix. Needs deduplication.
+  systemCronJobs =
+    ''
+      SHELL=${pkgs.bash}/bin/bash
+      PATH=${config.system.path}/bin:${config.system.path}/sbin
+      ${optionalString (config.services.cron.mailto != null) ''
+        MAILTO="${config.services.cron.mailto}"
+      ''}
+      NIX_CONF_DIR=/etc/nix
+      ${lib.concatStrings (map (job: job + "\n") config.services.cron.systemCronJobs)}
+    '';
+
+  allowdeny = target: users:
+    { source = pkgs.writeText "fcron.${target}" (concatStringsSep "\n" users);
+      target = "fcron.${target}";
+      mode = "644";
+      gid = config.ids.gids.fcron;
+    };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.fcron = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the <command>fcron</command> daemon.";
+      };
+
+      allow = mkOption {
+        type = types.listOf types.str;
+        default = [ "all" ];
+        description = ''
+          Users allowed to use fcrontab and fcrondyn (one name per
+          line, <literal>all</literal> for everyone).
+        '';
+      };
+
+      deny = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Users forbidden from using fcron.";
+      };
+
+      maxSerialJobs = mkOption {
+        type = types.int;
+        default = 1;
+        description = "Maximum number of serial jobs which can run simultaneously.";
+      };
+
+      queuelen = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = "Number of jobs the serial queue and the lavg queue can contain.";
+      };
+
+      systab = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''The "system" crontab contents.'';
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.fcron.systab = systemCronJobs;
+
+    environment.etc = listToAttrs
+      (map (x: { name = x.target; value = x; })
+      [ (allowdeny "allow" (cfg.allow))
+        (allowdeny "deny" cfg.deny)
+        # see man 5 fcron.conf
+        { source =
+            let
+              isSendmailWrapped =
+                lib.hasAttr "sendmail" config.security.wrappers;
+              sendmailPath =
+                if isSendmailWrapped then "/run/wrappers/bin/sendmail"
+                else "${config.system.path}/bin/sendmail";
+            in
+            pkgs.writeText "fcron.conf" ''
+              fcrontabs   =       /var/spool/fcron
+              pidfile     =       /run/fcron.pid
+              fifofile    =       /run/fcron.fifo
+              fcronallow  =       /etc/fcron.allow
+              fcrondeny   =       /etc/fcron.deny
+              shell       =       /bin/sh
+              sendmail    =       ${sendmailPath}
+              editor      =       ${pkgs.vim}/bin/vim
+            '';
+          target = "fcron.conf";
+          gid = config.ids.gids.fcron;
+          mode = "0644";
+        }
+      ]);
+
+    environment.systemPackages = [ pkgs.fcron ];
+    users.users.fcron = {
+      uid = config.ids.uids.fcron;
+      home = "/var/spool/fcron";
+      group = "fcron";
+    };
+    users.groups.fcron.gid = config.ids.gids.fcron;
+
+    security.wrappers = {
+      fcrontab = {
+        source = "${pkgs.fcron}/bin/fcrontab";
+        owner = "fcron";
+        group = "fcron";
+        setgid = true;
+        setuid = true;
+      };
+      fcrondyn = {
+        source = "${pkgs.fcron}/bin/fcrondyn";
+        owner = "fcron";
+        group = "fcron";
+        setgid = true;
+        setuid = false;
+      };
+      fcronsighup = {
+        source = "${pkgs.fcron}/bin/fcronsighup";
+        owner = "root";
+        group = "fcron";
+        setuid = true;
+      };
+    };
+    systemd.services.fcron = {
+      description = "fcron daemon";
+      wantedBy = [ "multi-user.target" ];
+
+      path = [ pkgs.fcron ];
+
+      preStart = ''
+        install \
+          --mode 0770 \
+          --owner fcron \
+          --group fcron \
+          --directory /var/spool/fcron
+        # load system crontab file
+        /run/wrappers/bin/fcrontab -u systab - < ${pkgs.writeText "systab" cfg.systab}
+      '';
+
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${pkgs.fcron}/sbin/fcron -m ${toString cfg.maxSerialJobs} ${queuelen}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/search/elasticsearch-curator.nix b/nixos/modules/services/search/elasticsearch-curator.nix
new file mode 100644
index 00000000000..bb2612322bb
--- /dev/null
+++ b/nixos/modules/services/search/elasticsearch-curator.nix
@@ -0,0 +1,95 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+    cfg = config.services.elasticsearch-curator;
+    curatorConfig = pkgs.writeTextFile {
+      name = "config.yaml";
+      text = ''
+        ---
+        # Remember, leave a key empty if there is no value.  None will be a string,
+        # not a Python "NoneType"
+        client:
+          hosts: ${builtins.toJSON cfg.hosts}
+          port: ${toString cfg.port}
+          url_prefix:
+          use_ssl: False
+          certificate:
+          client_cert:
+          client_key:
+          ssl_no_validate: False
+          http_auth:
+          timeout: 30
+          master_only: False
+        logging:
+          loglevel: INFO
+          logfile:
+          logformat: default
+          blacklist: ['elasticsearch', 'urllib3']
+        '';
+    };
+    curatorAction = pkgs.writeTextFile {
+      name = "action.yaml";
+      text = cfg.actionYAML;
+    };
+in {
+
+  options.services.elasticsearch-curator = {
+
+    enable = mkEnableOption "elasticsearch curator";
+    interval = mkOption {
+      description = "The frequency to run curator, a systemd.time such as 'hourly'";
+      default = "hourly";
+      type = types.str;
+    };
+    hosts = mkOption {
+      description = "a list of elasticsearch hosts to connect to";
+      type = types.listOf types.str;
+      default = ["localhost"];
+    };
+    port = mkOption {
+      description = "the port that elasticsearch is listening on";
+      type = types.int;
+      default = 9200;
+    };
+    actionYAML = mkOption {
+      description = "curator action.yaml file contents, alternatively use curator-cli which takes a simple action command";
+      type = types.lines;
+      example = ''
+        ---
+        actions:
+          1:
+            action: delete_indices
+            description: >-
+              Delete indices older than 45 days (based on index name), for logstash-
+              prefixed indices. Ignore the error if the filter does not result in an
+              actionable list of indices (ignore_empty_list) and exit cleanly.
+            options:
+              ignore_empty_list: True
+              disable_action: False
+            filters:
+            - filtertype: pattern
+              kind: prefix
+              value: logstash-
+            - filtertype: age
+              source: name
+              direction: older
+              timestring: '%Y.%m.%d'
+              unit: days
+              unit_count: 45
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.elasticsearch-curator = {
+      startAt = cfg.interval;
+      serviceConfig = {
+        ExecStart =
+          "${pkgs.elasticsearch-curator}/bin/curator" +
+          " --config ${curatorConfig} ${curatorAction}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/search/elasticsearch.nix b/nixos/modules/services/search/elasticsearch.nix
new file mode 100644
index 00000000000..041d0b3c43f
--- /dev/null
+++ b/nixos/modules/services/search/elasticsearch.nix
@@ -0,0 +1,239 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.elasticsearch;
+
+  es7 = builtins.compareVersions cfg.package.version "7" >= 0;
+
+  esConfig = ''
+    network.host: ${cfg.listenAddress}
+    cluster.name: ${cfg.cluster_name}
+    ${lib.optionalString cfg.single_node "discovery.type: single-node"}
+    ${lib.optionalString (cfg.single_node && es7) "gateway.auto_import_dangling_indices: true"}
+
+    http.port: ${toString cfg.port}
+    transport.port: ${toString cfg.tcp_port}
+
+    ${cfg.extraConf}
+  '';
+
+  configDir = cfg.dataDir + "/config";
+
+  elasticsearchYml = pkgs.writeTextFile {
+    name = "elasticsearch.yml";
+    text = esConfig;
+  };
+
+  loggingConfigFilename = "log4j2.properties";
+  loggingConfigFile = pkgs.writeTextFile {
+    name = loggingConfigFilename;
+    text = cfg.logging;
+  };
+
+  esPlugins = pkgs.buildEnv {
+    name = "elasticsearch-plugins";
+    paths = cfg.plugins;
+    postBuild = "${pkgs.coreutils}/bin/mkdir -p $out/plugins";
+  };
+
+in
+{
+
+  ###### interface
+
+  options.services.elasticsearch = {
+    enable = mkOption {
+      description = "Whether to enable elasticsearch.";
+      default = false;
+      type = types.bool;
+    };
+
+    package = mkOption {
+      description = "Elasticsearch package to use.";
+      default = pkgs.elasticsearch;
+      defaultText = literalExpression "pkgs.elasticsearch";
+      type = types.package;
+    };
+
+    listenAddress = mkOption {
+      description = "Elasticsearch listen address.";
+      default = "127.0.0.1";
+      type = types.str;
+    };
+
+    port = mkOption {
+      description = "Elasticsearch port to listen for HTTP traffic.";
+      default = 9200;
+      type = types.int;
+    };
+
+    tcp_port = mkOption {
+      description = "Elasticsearch port for the node to node communication.";
+      default = 9300;
+      type = types.int;
+    };
+
+    cluster_name = mkOption {
+      description = "Elasticsearch name that identifies your cluster for auto-discovery.";
+      default = "elasticsearch";
+      type = types.str;
+    };
+
+    single_node = mkOption {
+      description = "Start a single-node cluster";
+      default = true;
+      type = types.bool;
+    };
+
+    extraConf = mkOption {
+      description = "Extra configuration for elasticsearch.";
+      default = "";
+      type = types.str;
+      example = ''
+        node.name: "elasticsearch"
+        node.master: true
+        node.data: false
+      '';
+    };
+
+    logging = mkOption {
+      description = "Elasticsearch logging configuration.";
+      default = ''
+        logger.action.name = org.elasticsearch.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 = mkOption {
+      type = types.path;
+      default = "/var/lib/elasticsearch";
+      description = ''
+        Data directory for elasticsearch.
+      '';
+    };
+
+    extraCmdLineOptions = mkOption {
+      description = "Extra command line options for the elasticsearch launcher.";
+      default = [ ];
+      type = types.listOf types.str;
+    };
+
+    extraJavaOptions = mkOption {
+      description = "Extra command line options for Java.";
+      default = [ ];
+      type = types.listOf types.str;
+      example = [ "-Djava.net.preferIPv4Stack=true" ];
+    };
+
+    plugins = mkOption {
+      description = "Extra elasticsearch plugins";
+      default = [ ];
+      type = types.listOf types.package;
+      example = lib.literalExpression "[ pkgs.elasticsearchPlugins.discovery-ec2 ]";
+    };
+
+    restartIfChanged  = mkOption {
+      type = types.bool;
+      description = ''
+        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;
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.elasticsearch = {
+      description = "Elasticsearch Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = [ pkgs.inetutils ];
+      inherit (cfg) restartIfChanged;
+      environment = {
+        ES_HOME = cfg.dataDir;
+        ES_JAVA_OPTS = toString cfg.extraJavaOptions;
+        ES_PATH_CONF = configDir;
+      };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/elasticsearch ${toString cfg.extraCmdLineOptions}";
+        User = "elasticsearch";
+        PermissionsStartOnly = true;
+        LimitNOFILE = "1024000";
+        Restart = "always";
+        TimeoutStartSec = "infinity";
+      };
+      preStart = ''
+        ${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
+        ''}
+
+        mkdir -m 0700 -p ${cfg.dataDir}
+
+        # Install plugins
+        ln -sfT ${esPlugins}/plugins ${cfg.dataDir}/plugins
+        ln -sfT ${cfg.package}/lib ${cfg.dataDir}/lib
+        ln -sfT ${cfg.package}/modules ${cfg.dataDir}/modules
+
+        # elasticsearch needs to create the elasticsearch.keystore in the config directory
+        # so this directory needs to be writable.
+        mkdir -m 0700 -p ${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/elasticsearch/config/elasticsearch.yml" "read")
+
+        cp ${elasticsearchYml} ${configDir}/elasticsearch.yml
+        # Make sure the logging configuration for old elasticsearch versions is removed:
+        rm -f "${configDir}/logging.yml"
+        cp ${loggingConfigFile} ${configDir}/${loggingConfigFilename}
+        mkdir -p ${configDir}/scripts
+        cp ${cfg.package}/config/jvm.options ${configDir}/jvm.options
+        # redirect jvm logs to the data directory
+        mkdir -m 0700 -p ${cfg.dataDir}/logs
+        ${pkgs.sd}/bin/sd 'logs/gc.log' '${cfg.dataDir}/logs/gc.log' ${configDir}/jvm.options \
+
+        if [ "$(id -u)" = 0 ]; then chown -R elasticsearch:elasticsearch ${cfg.dataDir}; fi
+      '';
+      postStart = ''
+        # Make sure elasticsearch is up and running before dependents
+        # are started
+        while ! ${pkgs.curl}/bin/curl -sS -f http://${cfg.listenAddress}:${toString cfg.port} 2>/dev/null; do
+          sleep 1
+        done
+      '';
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    users = {
+      groups.elasticsearch.gid = config.ids.gids.elasticsearch;
+      users.elasticsearch = {
+        uid = config.ids.uids.elasticsearch;
+        description = "Elasticsearch daemon user";
+        home = cfg.dataDir;
+        group = "elasticsearch";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/search/hound.nix b/nixos/modules/services/search/hound.nix
new file mode 100644
index 00000000000..ef62175b0a3
--- /dev/null
+++ b/nixos/modules/services/search/hound.nix
@@ -0,0 +1,127 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.hound;
+in {
+  options = {
+    services.hound = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the hound code search daemon.
+        '';
+      };
+
+      user = mkOption {
+        default = "hound";
+        type = types.str;
+        description = ''
+          User the hound daemon should execute under.
+        '';
+      };
+
+      group = mkOption {
+        default = "hound";
+        type = types.str;
+        description = ''
+          Group the hound daemon should execute under.
+        '';
+      };
+
+      extraGroups = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "dialout" ];
+        description = ''
+          List of extra groups that the "hound" user should be a part of.
+        '';
+      };
+
+      home = mkOption {
+        default = "/var/lib/hound";
+        type = types.path;
+        description = ''
+          The path to use as hound's $HOME. If the default user
+          "hound" is configured then this is the home of the "hound"
+          user.
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.hound;
+        defaultText = literalExpression "pkgs.hound";
+        type = types.package;
+        description = ''
+          Package for running hound.
+        '';
+      };
+
+      config = mkOption {
+        type = types.str;
+        description = ''
+          The full configuration of the Hound daemon. Note the dbpath
+          should be an absolute path to a writable location on disk.
+        '';
+        example = literalExpression ''
+          '''
+            {
+              "max-concurrent-indexers" : 2,
+              "dbpath" : "''${services.hound.home}/data",
+              "repos" : {
+                  "nixpkgs": {
+                    "url" : "https://www.github.com/NixOS/nixpkgs.git"
+                  }
+              }
+            }
+          '''
+        '';
+      };
+
+      listen = mkOption {
+        type = types.str;
+        default = "0.0.0.0:6080";
+        example = "127.0.0.1:6080 or just :6080";
+        description = ''
+          Listen on this IP:port / :port
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups = optionalAttrs (cfg.group == "hound") {
+      hound.gid = config.ids.gids.hound;
+    };
+
+    users.users = optionalAttrs (cfg.user == "hound") {
+      hound = {
+        description = "hound code search";
+        createHome = true;
+        home = cfg.home;
+        group = cfg.group;
+        extraGroups = cfg.extraGroups;
+        uid = config.ids.uids.hound;
+      };
+    };
+
+    systemd.services.hound = {
+      description = "Hound Code Search";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        WorkingDirectory = cfg.home;
+        ExecStartPre = "${pkgs.git}/bin/git config --global --replace-all http.sslCAinfo /etc/ssl/certs/ca-certificates.crt";
+        ExecStart = "${cfg.package}/bin/houndd" +
+                    " -addr ${cfg.listen}" +
+                    " -conf ${pkgs.writeText "hound.json" cfg.config}";
+
+      };
+      path = [ pkgs.git pkgs.mercurial pkgs.openssh ];
+    };
+  };
+
+}
diff --git a/nixos/modules/services/search/kibana.nix b/nixos/modules/services/search/kibana.nix
new file mode 100644
index 00000000000..e4ab85be9ef
--- /dev/null
+++ b/nixos/modules/services/search/kibana.nix
@@ -0,0 +1,213 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.kibana;
+  opt = options.services.kibana;
+
+  ge7 = builtins.compareVersions cfg.package.version "7" >= 0;
+  lt6_6 = builtins.compareVersions cfg.package.version "6.6" < 0;
+
+  cfgFile = pkgs.writeText "kibana.json" (builtins.toJSON (
+    (filterAttrsRecursive (n: v: v != null && v != []) ({
+      server.host = cfg.listenAddress;
+      server.port = cfg.port;
+      server.ssl.certificate = cfg.cert;
+      server.ssl.key = cfg.key;
+
+      kibana.index = cfg.index;
+      kibana.defaultAppId = cfg.defaultAppId;
+
+      elasticsearch.url = cfg.elasticsearch.url;
+      elasticsearch.hosts = cfg.elasticsearch.hosts;
+      elasticsearch.username = cfg.elasticsearch.username;
+      elasticsearch.password = cfg.elasticsearch.password;
+
+      elasticsearch.ssl.certificate = cfg.elasticsearch.cert;
+      elasticsearch.ssl.key = cfg.elasticsearch.key;
+      elasticsearch.ssl.certificateAuthorities = cfg.elasticsearch.certificateAuthorities;
+    } // cfg.extraConf)
+  )));
+
+in {
+  options.services.kibana = {
+    enable = mkEnableOption "kibana service";
+
+    listenAddress = mkOption {
+      description = "Kibana listening host";
+      default = "127.0.0.1";
+      type = types.str;
+    };
+
+    port = mkOption {
+      description = "Kibana listening port";
+      default = 5601;
+      type = types.int;
+    };
+
+    cert = mkOption {
+      description = "Kibana ssl certificate.";
+      default = null;
+      type = types.nullOr types.path;
+    };
+
+    key = mkOption {
+      description = "Kibana ssl key.";
+      default = null;
+      type = types.nullOr types.path;
+    };
+
+    index = mkOption {
+      description = "Elasticsearch index to use for saving kibana config.";
+      default = ".kibana";
+      type = types.str;
+    };
+
+    defaultAppId = mkOption {
+      description = "Elasticsearch default application id.";
+      default = "discover";
+      type = types.str;
+    };
+
+    elasticsearch = {
+      url = mkOption {
+        description = ''
+          Elasticsearch url.
+
+          Defaults to <literal>"http://localhost:9200"</literal>.
+
+          Don't set this when using Kibana >= 7.0.0 because it will result in a
+          configuration error. Use <option>services.kibana.elasticsearch.hosts</option>
+          instead.
+        '';
+        default = null;
+        type = types.nullOr types.str;
+      };
+
+      hosts = mkOption {
+        description = ''
+          The URLs of the Elasticsearch instances to use for all your queries.
+          All nodes listed here must be on the same cluster.
+
+          Defaults to <literal>[ "http://localhost:9200" ]</literal>.
+
+          This option is only valid when using kibana >= 6.6.
+        '';
+        default = null;
+        type = types.nullOr (types.listOf types.str);
+      };
+
+      username = mkOption {
+        description = "Username for elasticsearch basic auth.";
+        default = null;
+        type = types.nullOr types.str;
+      };
+
+      password = mkOption {
+        description = "Password for elasticsearch basic auth.";
+        default = null;
+        type = types.nullOr types.str;
+      };
+
+      ca = mkOption {
+        description = ''
+          CA file to auth against elasticsearch.
+
+          It's recommended to use the <option>certificateAuthorities</option> option
+          when using kibana-5.4 or newer.
+        '';
+        default = null;
+        type = types.nullOr types.path;
+      };
+
+      certificateAuthorities = mkOption {
+        description = ''
+          CA files to auth against elasticsearch.
+
+          Please use the <option>ca</option> option when using kibana &lt; 5.4
+          because those old versions don't support setting multiple CA's.
+
+          This defaults to the singleton list [ca] when the <option>ca</option> option is defined.
+        '';
+        default = if cfg.elasticsearch.ca == null then [] else [ca];
+        defaultText = literalExpression ''
+          if config.${opt.elasticsearch.ca} == null then [ ] else [ ca ]
+        '';
+        type = types.listOf types.path;
+      };
+
+      cert = mkOption {
+        description = "Certificate file to auth against elasticsearch.";
+        default = null;
+        type = types.nullOr types.path;
+      };
+
+      key = mkOption {
+        description = "Key file to auth against elasticsearch.";
+        default = null;
+        type = types.nullOr types.path;
+      };
+    };
+
+    package = mkOption {
+      description = "Kibana package to use";
+      default = pkgs.kibana;
+      defaultText = literalExpression "pkgs.kibana";
+      type = types.package;
+    };
+
+    dataDir = mkOption {
+      description = "Kibana data directory";
+      default = "/var/lib/kibana";
+      type = types.path;
+    };
+
+    extraConf = mkOption {
+      description = "Kibana extra configuration";
+      default = {};
+      type = types.attrs;
+    };
+  };
+
+  config = mkIf (cfg.enable) {
+    assertions = [
+      {
+        assertion = ge7 -> cfg.elasticsearch.url == null;
+        message =
+          "The option services.kibana.elasticsearch.url has been removed when using kibana >= 7.0.0. " +
+          "Please use option services.kibana.elasticsearch.hosts instead.";
+      }
+      {
+        assertion = lt6_6 -> cfg.elasticsearch.hosts == null;
+        message =
+          "The option services.kibana.elasticsearch.hosts is only valid for kibana >= 6.6.";
+      }
+    ];
+    systemd.services.kibana = {
+      description = "Kibana Service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "elasticsearch.service" ];
+      environment = { BABEL_CACHE_PATH = "${cfg.dataDir}/.babelcache.json"; };
+      serviceConfig = {
+        ExecStart =
+          "${cfg.package}/bin/kibana" +
+          " --config ${cfgFile}" +
+          " --path.data ${cfg.dataDir}";
+        User = "kibana";
+        WorkingDirectory = cfg.dataDir;
+      };
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    users.users.kibana = {
+      isSystemUser = true;
+      description = "Kibana service user";
+      home = cfg.dataDir;
+      createHome = true;
+      group = "kibana";
+    };
+    users.groups.kibana = {};
+  };
+}
diff --git a/nixos/modules/services/search/meilisearch.md b/nixos/modules/services/search/meilisearch.md
new file mode 100644
index 00000000000..98e7c542cb9
--- /dev/null
+++ b/nixos/modules/services/search/meilisearch.md
@@ -0,0 +1,39 @@
+# Meilisearch {#module-services-meilisearch}
+
+Meilisearch is a lightweight, fast and powerful search engine. Think elastic search with a much smaller footprint.
+
+## Quickstart
+
+the minimum to start meilisearch is
+
+```nix
+services.meilisearch.enable = true;
+```
+
+this will start the http server included with meilisearch on port 7700.
+
+test with `curl -X GET 'http://localhost:7700/health'`
+
+## Usage
+
+you first need to add documents to an index before you can search for documents.
+
+### Add a documents to the `movies` index
+
+`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
+
+`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
+
+- 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.
+
+- Anonimized Analytics sent to meilisearch are disabled by default.
+
+- Default deployment is development mode. It doesn't require a secret master key. All routes are not protected and accessible.
+
+## 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
new file mode 100644
index 00000000000..f6210f6f16e
--- /dev/null
+++ b/nixos/modules/services/search/meilisearch.nix
@@ -0,0 +1,132 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.meilisearch;
+
+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;
+
+  ###### interface
+
+  options.services.meilisearch = {
+    enable = mkEnableOption "MeiliSearch - a RESTful search API";
+
+    package = mkOption {
+      description = "The package to use for meilisearch. Use this if you require specific features to be enabled. The default package has no features.";
+      default = pkgs.meilisearch;
+      defaultText = "pkgs.meilisearch";
+      type = types.package;
+    };
+
+    listenAddress = mkOption {
+      description = "MeiliSearch listen address.";
+      default = "127.0.0.1";
+      type = types.str;
+    };
+
+    listenPort = mkOption {
+      description = "MeiliSearch port to listen on.";
+      default = 7700;
+      type = types.port;
+    };
+
+    environment = mkOption {
+      description = "Defines the running environment of MeiliSearch.";
+      default = "development";
+      type = types.enum [ "development" "production" ];
+    };
+
+    # TODO change this to LoadCredentials once possible
+    masterKeyEnvironmentFile = mkOption {
+      description = ''
+        Path to file which contains the master key.
+        By doing so, all routes will be protected and will require a key to be accessed.
+        If no master key is provided, all routes can be accessed without requiring any key.
+        The format is the following:
+        MEILI_MASTER_KEY=my_secret_key
+      '';
+      default = null;
+      type = with types; nullOr path;
+    };
+
+    noAnalytics = mkOption {
+      description = ''
+        Deactivates analytics.
+        Analytics allow MeiliSearch to know how many users are using MeiliSearch,
+        which versions and which platforms are used.
+        This process is entirely anonymous.
+      '';
+      default = true;
+      type = types.bool;
+    };
+
+    logLevel = mkOption {
+      description = ''
+        Defines how much detail should be present in MeiliSearch's logs.
+        MeiliSearch currently supports four log levels, listed in order of increasing verbosity:
+        - 'ERROR': only log unexpected events indicating MeiliSearch is not functioning as expected
+        - 'WARN:' log all unexpected events, regardless of their severity
+        - 'INFO:' log all events. This is the default value
+        - 'DEBUG': log all events and including detailed information on MeiliSearch's internal processes.
+          Useful when diagnosing issues and debugging
+      '';
+      default = "INFO";
+      type = types.str;
+    };
+
+    maxIndexSize = mkOption {
+      description = ''
+        Sets the maximum size of the index.
+        Value must be given in bytes or explicitly stating a base unit.
+        For example, the default value can be written as 107374182400, '107.7Gb', or '107374 Mb'.
+        Default is 100 GiB
+      '';
+      default = "107374182400";
+      type = types.str;
+    };
+
+    payloadSizeLimit = mkOption {
+      description = ''
+        Sets the maximum size of accepted JSON payloads.
+        Value must be given in bytes or explicitly stating a base unit.
+        For example, the default value can be written as 107374182400, '107.7Gb', or '107374 Mb'.
+        Default is ~ 100 MB
+      '';
+      default = "104857600";
+      type = types.str;
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.meilisearch = {
+      description = "MeiliSearch daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      environment = {
+        MEILI_DB_PATH = "/var/lib/meilisearch";
+        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_LOG_LEVEL = cfg.logLevel;
+        MEILI_MAX_INDEX_SIZE = cfg.maxIndexSize;
+      };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/meilisearch";
+        DynamicUser = true;
+        StateDirectory = "meilisearch";
+        EnvironmentFile = mkIf (cfg.masterKeyEnvironmentFile != null) cfg.masterKeyEnvironmentFile;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/search/meilisearch.xml b/nixos/modules/services/search/meilisearch.xml
new file mode 100644
index 00000000000..c1a73f358c2
--- /dev/null
+++ b/nixos/modules/services/search/meilisearch.xml
@@ -0,0 +1,85 @@
+<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/solr.nix b/nixos/modules/services/search/solr.nix
new file mode 100644
index 00000000000..ea76bfc9298
--- /dev/null
+++ b/nixos/modules/services/search/solr.nix
@@ -0,0 +1,110 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.solr;
+
+in
+
+{
+  options = {
+    services.solr = {
+      enable = mkEnableOption "Solr";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.solr;
+        defaultText = literalExpression "pkgs.solr";
+        description = "Which Solr package to use.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8983;
+        description = "Port on which Solr is ran.";
+      };
+
+      stateDir = mkOption {
+        type = types.path;
+        default = "/var/lib/solr";
+        description = "The solr home directory containing config, data, and logging files.";
+      };
+
+      extraJavaOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Extra command line options given to the java process running Solr.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "solr";
+        description = "User under which Solr is ran.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "solr";
+        description = "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
new file mode 100644
index 00000000000..8268b034a15
--- /dev/null
+++ b/nixos/modules/services/security/aesmd.nix
@@ -0,0 +1,236 @@
+{ config, options, pkgs, lib, ... }:
+with lib;
+let
+  cfg = config.services.aesmd;
+  opt = options.services.aesmd;
+
+  sgx-psw = pkgs.sgx-psw.override { inherit (cfg) debug; };
+
+  configFile = with cfg.settings; pkgs.writeText "aesmd.conf" (
+    concatStringsSep "\n" (
+      optional (whitelistUrl != null) "whitelist url = ${whitelistUrl}" ++
+      optional (proxy != null) "aesm proxy = ${proxy}" ++
+      optional (proxyType != null) "proxy type = ${proxyType}" ++
+      optional (defaultQuotingType != null) "default quoting type = ${defaultQuotingType}" ++
+      # Newline at end of file
+      [ "" ]
+    )
+  );
+in
+{
+  options.services.aesmd = {
+    enable = mkEnableOption "Intel's Architectural Enclave Service Manager (AESM) for Intel SGX";
+    debug = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Whether to build the PSW package in debug mode.";
+    };
+    settings = mkOption {
+      description = "AESM configuration";
+      default = { };
+      type = types.submodule {
+        options.whitelistUrl = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          example = "http://whitelist.trustedservices.intel.com/SGX/LCWL/Linux/sgx_white_list_cert.bin";
+          description = "URL to retrieve authorized Intel SGX enclave signers.";
+        };
+        options.proxy = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          example = "http://proxy_url:1234";
+          description = "HTTP network proxy.";
+        };
+        options.proxyType = mkOption {
+          type = with types; nullOr (enum [ "default" "direct" "manual" ]);
+          default = if (cfg.settings.proxy != null) then "manual" else null;
+          defaultText = literalExpression ''
+            if (config.${opt.settings}.proxy != null) then "manual" else null
+          '';
+          example = "default";
+          description = ''
+            Type of proxy to use. The <literal>default</literal> uses the system's default proxy.
+            If <literal>direct</literal> is given, uses no proxy.
+            A value of <literal>manual</literal> uses the proxy from
+            <option>services.aesmd.settings.proxy</option>.
+          '';
+        };
+        options.defaultQuotingType = mkOption {
+          type = with types; nullOr (enum [ "ecdsa_256" "epid_linkable" "epid_unlinkable" ]);
+          default = null;
+          example = "ecdsa_256";
+          description = "Attestation quote type.";
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [{
+      assertion = !(config.boot.specialFileSystems."/dev".options ? "noexec");
+      message = "SGX requires exec permission for /dev";
+    }];
+
+    hardware.cpu.intel.sgx.provision.enable = true;
+
+    # Make sure the AESM service can find the SGX devices until
+    # https://github.com/intel/linux-sgx/issues/772 is resolved
+    # and updated in nixpkgs.
+    hardware.cpu.intel.sgx.enableDcapCompat = mkForce true;
+
+    systemd.services.aesmd =
+      let
+        storeAesmFolder = "${sgx-psw}/aesm";
+        # Hardcoded path AESM_DATA_FOLDER in psw/ae/aesm_service/source/oal/linux/aesm_util.cpp
+        aesmDataFolder = "/var/opt/aesmd/data";
+        aesmStateDirSystemd = "%S/aesmd";
+      in
+      {
+        description = "Intel Architectural Enclave Service Manager";
+        wantedBy = [ "multi-user.target" ];
+
+        after = [
+          "auditd.service"
+          "network.target"
+          "syslog.target"
+        ];
+
+        environment = {
+          NAME = "aesm_service";
+          AESM_PATH = storeAesmFolder;
+          LD_LIBRARY_PATH = storeAesmFolder;
+        };
+
+        # Make sure any of the SGX application enclave devices is available
+        unitConfig.AssertPathExists = [
+          # legacy out-of-tree driver
+          "|/dev/isgx"
+          # DCAP driver
+          "|/dev/sgx/enclave"
+          # in-tree driver
+          "|/dev/sgx_enclave"
+        ];
+
+        serviceConfig = rec {
+          ExecStartPre = pkgs.writeShellScript "copy-aesmd-data-files.sh" ''
+            set -euo pipefail
+            whiteListFile="${aesmDataFolder}/white_list_cert_to_be_verify.bin"
+            if [[ ! -f "$whiteListFile" ]]; then
+              ${pkgs.coreutils}/bin/install -m 644 -D \
+                "${storeAesmFolder}/data/white_list_cert_to_be_verify.bin" \
+                "$whiteListFile"
+            fi
+          '';
+          ExecStart = "${sgx-psw}/bin/aesm_service --no-daemon";
+          ExecReload = ''${pkgs.coreutils}/bin/kill -SIGHUP "$MAINPID"'';
+
+          Restart = "on-failure";
+          RestartSec = "15s";
+
+          DynamicUser = true;
+          Group = "sgx";
+          SupplementaryGroups = [
+            config.hardware.cpu.intel.sgx.provision.group
+          ];
+
+          Type = "simple";
+
+          WorkingDirectory = storeAesmFolder;
+          StateDirectory = "aesmd";
+          StateDirectoryMode = "0700";
+          RuntimeDirectory = "aesmd";
+          RuntimeDirectoryMode = "0750";
+
+          # Hardening
+
+          # chroot into the runtime directory
+          RootDirectory = "%t/aesmd";
+          BindReadOnlyPaths = [
+            builtins.storeDir
+            # Hardcoded path AESM_CONFIG_FILE in psw/ae/aesm_service/source/utils/aesm_config.cpp
+            "${configFile}:/etc/aesmd.conf"
+          ];
+          BindPaths = [
+            # Hardcoded path CONFIG_SOCKET_PATH in psw/ae/aesm_service/source/core/ipc/SocketConfig.h
+            "%t/aesmd:/var/run/aesmd"
+            "%S/aesmd:/var/opt/aesmd"
+          ];
+
+          # PrivateDevices=true will mount /dev noexec which breaks AESM
+          PrivateDevices = false;
+          DevicePolicy = "closed";
+          DeviceAllow = [
+            # legacy out-of-tree driver
+            "/dev/isgx rw"
+            # DCAP driver
+            "/dev/sgx rw"
+            # in-tree driver
+            "/dev/sgx_enclave rw"
+            "/dev/sgx_provision rw"
+          ];
+
+          # Requires Internet access for attestation
+          PrivateNetwork = false;
+
+          RestrictAddressFamilies = [
+            # Allocates the socket /var/run/aesmd/aesm.socket
+            "AF_UNIX"
+            # Uses the HTTP protocol to initialize some services
+            "AF_INET"
+            "AF_INET6"
+          ];
+
+          # True breaks stuff
+          MemoryDenyWriteExecute = false;
+
+          # needs the ipc syscall in order to run
+          SystemCallFilter = [
+            "@system-service"
+            "~@aio"
+            "~@chown"
+            "~@clock"
+            "~@cpu-emulation"
+            "~@debug"
+            "~@keyring"
+            "~@memlock"
+            "~@module"
+            "~@mount"
+            "~@privileged"
+            "~@raw-io"
+            "~@reboot"
+            "~@resources"
+            "~@setuid"
+            "~@swap"
+            "~@sync"
+            "~@timer"
+          ];
+          SystemCallArchitectures = "native";
+          SystemCallErrorNumber = "EPERM";
+
+          CapabilityBoundingSet = "";
+          KeyringMode = "private";
+          LockPersonality = true;
+          NoNewPrivileges = true;
+          NotifyAccess = "none";
+          PrivateMounts = true;
+          PrivateTmp = true;
+          PrivateUsers = true;
+          ProcSubset = "pid";
+          ProtectClock = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectProc = "invisible";
+          ProtectSystem = "strict";
+          RemoveIPC = true;
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          UMask = "0066";
+        };
+      };
+  };
+}
diff --git a/nixos/modules/services/security/certmgr.nix b/nixos/modules/services/security/certmgr.nix
new file mode 100644
index 00000000000..d302a4e0002
--- /dev/null
+++ b/nixos/modules/services/security/certmgr.nix
@@ -0,0 +1,201 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.certmgr;
+
+  specs = mapAttrsToList (n: v: rec {
+    name = n + ".json";
+    path = if isAttrs v then pkgs.writeText name (builtins.toJSON v) else v;
+  }) cfg.specs;
+
+  allSpecs = pkgs.linkFarm "certmgr.d" specs;
+
+  certmgrYaml = pkgs.writeText "certmgr.yaml" (builtins.toJSON {
+    dir = allSpecs;
+    default_remote = cfg.defaultRemote;
+    svcmgr = cfg.svcManager;
+    before = cfg.validMin;
+    interval = cfg.renewInterval;
+    inherit (cfg) metricsPort metricsAddress;
+  });
+
+  specPaths = map dirOf (concatMap (spec:
+    if isAttrs spec then
+      collect isString (filterAttrsRecursive (n: v: isAttrs v || n == "path") spec)
+    else
+      [ spec ]
+  ) (attrValues cfg.specs));
+
+  preStart = ''
+    ${concatStringsSep " \\\n" (["mkdir -p"] ++ map escapeShellArg specPaths)}
+    ${cfg.package}/bin/certmgr -f ${certmgrYaml} check
+  '';
+in
+{
+  options.services.certmgr = {
+    enable = mkEnableOption "certmgr";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.certmgr;
+      defaultText = literalExpression "pkgs.certmgr";
+      description = "Which certmgr package to use in the service.";
+    };
+
+    defaultRemote = mkOption {
+      type = types.str;
+      default = "127.0.0.1:8888";
+      description = "The default CA host:port to use.";
+    };
+
+    validMin = mkOption {
+      default = "72h";
+      type = types.str;
+      description = "The interval before a certificate expires to start attempting to renew it.";
+    };
+
+    renewInterval = mkOption {
+      default = "30m";
+      type = types.str;
+      description = "How often to check certificate expirations and how often to update the cert_next_expires metric.";
+    };
+
+    metricsAddress = mkOption {
+      default = "127.0.0.1";
+      type = types.str;
+      description = "The address for the Prometheus HTTP endpoint.";
+    };
+
+    metricsPort = mkOption {
+      default = 9488;
+      type = types.ints.u16;
+      description = "The port for the Prometheus HTTP endpoint.";
+    };
+
+    specs = mkOption {
+      default = {};
+      example = literalExpression ''
+      {
+        exampleCert =
+        let
+          domain = "example.com";
+          secret = name: "/var/lib/secrets/''${name}.pem";
+        in {
+          service = "nginx";
+          action = "reload";
+          authority = {
+            file.path = secret "ca";
+          };
+          certificate = {
+            path = secret domain;
+          };
+          private_key = {
+            owner = "root";
+            group = "root";
+            mode = "0600";
+            path = secret "''${domain}-key";
+          };
+          request = {
+            CN = domain;
+            hosts = [ "mail.''${domain}" "www.''${domain}" ];
+            key = {
+              algo = "rsa";
+              size = 2048;
+            };
+            names = {
+              O = "Example Organization";
+              C = "USA";
+            };
+          };
+        };
+        otherCert = "/var/certmgr/specs/other-cert.json";
+      }
+      '';
+      type = with types; attrsOf (either path (submodule {
+        options = {
+          service = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "The service on which to perform &lt;action&gt; after fetching.";
+          };
+
+          action = mkOption {
+            type = addCheck str (x: cfg.svcManager == "command" || elem x ["restart" "reload" "nop"]);
+            default = "nop";
+            description = "The action to take after fetching.";
+          };
+
+          # These ought all to be specified according to certmgr spec def.
+          authority = mkOption {
+            type = attrs;
+            description = "certmgr spec authority object.";
+          };
+
+          certificate = mkOption {
+            type = nullOr attrs;
+            description = "certmgr spec certificate object.";
+          };
+
+          private_key = mkOption {
+            type = nullOr attrs;
+            description = "certmgr spec private_key object.";
+          };
+
+          request = mkOption {
+            type = nullOr attrs;
+            description = "certmgr spec request object.";
+          };
+        };
+    }));
+      description = ''
+        Certificate specs as described by:
+        <link xlink:href="https://github.com/cloudflare/certmgr#certificate-specs" />
+        These will be added to the Nix store, so they will be world readable.
+      '';
+    };
+
+    svcManager = mkOption {
+      default = "systemd";
+      type = types.enum [ "circus" "command" "dummy" "openrc" "systemd" "sysv" ];
+      description = ''
+        This specifies the service manager to use for restarting or reloading services.
+        See: <link xlink:href="https://github.com/cloudflare/certmgr#certmgryaml" />.
+        For how to use the "command" service manager in particular,
+        see: <link xlink:href="https://github.com/cloudflare/certmgr#command-svcmgr-and-how-to-use-it" />.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.specs != {};
+        message = "Certmgr specs cannot be empty.";
+      }
+      {
+        assertion = !any (hasAttrByPath [ "authority" "auth_key" ]) (attrValues cfg.specs);
+        message = ''
+          Inline services.certmgr.specs are added to the Nix store rendering them world readable.
+          Specify paths as specs, if you want to use include auth_key - or use the auth_key_file option."
+        '';
+      }
+    ];
+
+    systemd.services.certmgr = {
+      description = "certmgr";
+      path = mkIf (cfg.svcManager == "command") [ pkgs.bash ];
+      after = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      inherit preStart;
+
+      serviceConfig = {
+        Restart = "always";
+        RestartSec = "10s";
+        ExecStart = "${cfg.package}/bin/certmgr -f ${certmgrYaml}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/cfssl.nix b/nixos/modules/services/security/cfssl.nix
new file mode 100644
index 00000000000..6df2343b84d
--- /dev/null
+++ b/nixos/modules/services/security/cfssl.nix
@@ -0,0 +1,222 @@
+{ config, options, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cfssl;
+in {
+  options.services.cfssl = {
+    enable = mkEnableOption "the CFSSL CA api-server";
+
+    dataDir = mkOption {
+      default = "/var/lib/cfssl";
+      type = types.path;
+      description = ''
+        The work directory for CFSSL.
+
+        <note><para>
+          If left as the default value this directory will automatically be
+          created before the CFSSL server starts, otherwise you are
+          responsible for ensuring the directory exists with appropriate
+          ownership and permissions.
+        </para></note>
+      '';
+    };
+
+    address = mkOption {
+      default = "127.0.0.1";
+      type = types.str;
+      description = "Address to bind.";
+    };
+
+    port = mkOption {
+      default = 8888;
+      type = types.port;
+      description = "Port to bind.";
+    };
+
+    ca = mkOption {
+      defaultText = literalExpression ''"''${cfg.dataDir}/ca.pem"'';
+      type = types.str;
+      description = "CA used to sign the new certificate -- accepts '[file:]fname' or 'env:varname'.";
+    };
+
+    caKey = mkOption {
+      defaultText = literalExpression ''"file:''${cfg.dataDir}/ca-key.pem"'';
+      type = types.str;
+      description = "CA private key -- accepts '[file:]fname' or 'env:varname'.";
+    };
+
+    caBundle = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Path to root certificate store.";
+    };
+
+    intBundle = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Path to intermediate certificate store.";
+    };
+
+    intDir = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Intermediates directory.";
+    };
+
+    metadata = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = ''
+        Metadata file for root certificate presence.
+        The content of the file is a json dictionary (k,v): each key k is
+        a SHA-1 digest of a root certificate while value v is a list of key
+        store filenames.
+      '';
+    };
+
+    remote = mkOption {
+      default = null;
+      type = types.nullOr types.str;
+      description = "Remote CFSSL server.";
+    };
+
+    configFile = mkOption {
+      default = null;
+      type = types.nullOr types.str;
+      description = "Path to configuration file. Do not put this in nix-store as it might contain secrets.";
+    };
+
+    responder = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Certificate for OCSP responder.";
+    };
+
+    responderKey = mkOption {
+      default = null;
+      type = types.nullOr types.str;
+      description = "Private key for OCSP responder certificate. Do not put this in nix-store.";
+    };
+
+    tlsKey = mkOption {
+      default = null;
+      type = types.nullOr types.str;
+      description = "Other endpoint's CA private key. Do not put this in nix-store.";
+    };
+
+    tlsCert = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Other endpoint's CA to set up TLS protocol.";
+    };
+
+    mutualTlsCa = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Mutual TLS - require clients be signed by this CA.";
+    };
+
+    mutualTlsCn = mkOption {
+      default = null;
+      type = types.nullOr types.str;
+      description = "Mutual TLS - regex for whitelist of allowed client CNs.";
+    };
+
+    tlsRemoteCa = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "CAs to trust for remote TLS requests.";
+    };
+
+    mutualTlsClientCert = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Mutual TLS - client certificate to call remote instance requiring client certs.";
+    };
+
+    mutualTlsClientKey = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Mutual TLS - client key to call remote instance requiring client certs. Do not put this in nix-store.";
+    };
+
+    dbConfig = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Certificate db configuration file. Path must be writeable.";
+    };
+
+    logLevel = mkOption {
+      default = 1;
+      type = types.enum [ 0 1 2 3 4 5 ];
+      description = "Log level (0 = DEBUG, 5 = FATAL).";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups.cfssl = {
+      gid = config.ids.gids.cfssl;
+    };
+
+    users.users.cfssl = {
+      description = "cfssl user";
+      home = cfg.dataDir;
+      group = "cfssl";
+      uid = config.ids.uids.cfssl;
+    };
+
+    systemd.services.cfssl = {
+      description = "CFSSL CA API server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = lib.mkMerge [
+        {
+          WorkingDirectory = cfg.dataDir;
+          Restart = "always";
+          User = "cfssl";
+          Group = "cfssl";
+
+          ExecStart = with cfg; let
+            opt = n: v: optionalString (v != null) ''-${n}="${v}"'';
+          in
+            lib.concatStringsSep " \\\n" [
+              "${pkgs.cfssl}/bin/cfssl serve"
+              (opt "address" address)
+              (opt "port" (toString port))
+              (opt "ca" ca)
+              (opt "ca-key" caKey)
+              (opt "ca-bundle" caBundle)
+              (opt "int-bundle" intBundle)
+              (opt "int-dir" intDir)
+              (opt "metadata" metadata)
+              (opt "remote" remote)
+              (opt "config" configFile)
+              (opt "responder" responder)
+              (opt "responder-key" responderKey)
+              (opt "tls-key" tlsKey)
+              (opt "tls-cert" tlsCert)
+              (opt "mutual-tls-ca" mutualTlsCa)
+              (opt "mutual-tls-cn" mutualTlsCn)
+              (opt "mutual-tls-client-key" mutualTlsClientKey)
+              (opt "mutual-tls-client-cert" mutualTlsClientCert)
+              (opt "tls-remote-ca" tlsRemoteCa)
+              (opt "db-config" dbConfig)
+              (opt "loglevel" (toString logLevel))
+            ];
+        }
+        (mkIf (cfg.dataDir == options.services.cfssl.dataDir.default) {
+          StateDirectory = baseNameOf cfg.dataDir;
+          StateDirectoryMode = 700;
+        })
+      ];
+    };
+
+    services.cfssl = {
+      ca = mkDefault "${cfg.dataDir}/ca.pem";
+      caKey = mkDefault "${cfg.dataDir}/ca-key.pem";
+    };
+  };
+}
diff --git a/nixos/modules/services/security/clamav.nix b/nixos/modules/services/security/clamav.nix
new file mode 100644
index 00000000000..95a0ad8770e
--- /dev/null
+++ b/nixos/modules/services/security/clamav.nix
@@ -0,0 +1,151 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  clamavUser = "clamav";
+  stateDir = "/var/lib/clamav";
+  runDir = "/run/clamav";
+  clamavGroup = clamavUser;
+  cfg = config.services.clamav;
+  pkg = pkgs.clamav;
+
+  toKeyValue = generators.toKeyValue {
+    mkKeyValue = generators.mkKeyValueDefault { } " ";
+    listsAsDuplicateKeys = true;
+  };
+
+  clamdConfigFile = pkgs.writeText "clamd.conf" (toKeyValue cfg.daemon.settings);
+  freshclamConfigFile = pkgs.writeText "freshclam.conf" (toKeyValue cfg.updater.settings);
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "clamav" "updater" "config" ] "Use services.clamav.updater.settings instead.")
+    (mkRemovedOptionModule [ "services" "clamav" "updater" "extraConfig" ] "Use services.clamav.updater.settings instead.")
+    (mkRemovedOptionModule [ "services" "clamav" "daemon" "extraConfig" ] "Use services.clamav.daemon.settings instead.")
+  ];
+
+  options = {
+    services.clamav = {
+      daemon = {
+        enable = mkEnableOption "ClamAV clamd daemon";
+
+        settings = mkOption {
+          type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
+          default = { };
+          description = ''
+            ClamAV configuration. Refer to <link xlink:href="https://linux.die.net/man/5/clamd.conf"/>,
+            for details on supported values.
+          '';
+        };
+      };
+      updater = {
+        enable = mkEnableOption "ClamAV freshclam updater";
+
+        frequency = mkOption {
+          type = types.int;
+          default = 12;
+          description = ''
+            Number of database checks per day.
+          '';
+        };
+
+        interval = mkOption {
+          type = types.str;
+          default = "hourly";
+          description = ''
+            How often freshclam is invoked. See systemd.time(7) for more
+            information about the format.
+          '';
+        };
+
+        settings = mkOption {
+          type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
+          default = { };
+          description = ''
+            freshclam configuration. Refer to <link xlink:href="https://linux.die.net/man/5/freshclam.conf"/>,
+            for details on supported values.
+          '';
+        };
+      };
+    };
+  };
+
+  config = mkIf (cfg.updater.enable || cfg.daemon.enable) {
+    environment.systemPackages = [ pkg ];
+
+    users.users.${clamavUser} = {
+      uid = config.ids.uids.clamav;
+      group = clamavGroup;
+      description = "ClamAV daemon user";
+      home = stateDir;
+    };
+
+    users.groups.${clamavGroup} =
+      { gid = config.ids.gids.clamav; };
+
+    services.clamav.daemon.settings = {
+      DatabaseDirectory = stateDir;
+      LocalSocket = "${runDir}/clamd.ctl";
+      PidFile = "${runDir}/clamd.pid";
+      TemporaryDirectory = "/tmp";
+      User = "clamav";
+      Foreground = true;
+    };
+
+    services.clamav.updater.settings = {
+      DatabaseDirectory = stateDir;
+      Foreground = true;
+      Checks = cfg.updater.frequency;
+      DatabaseMirror = [ "database.clamav.net" ];
+    };
+
+    environment.etc."clamav/freshclam.conf".source = freshclamConfigFile;
+    environment.etc."clamav/clamd.conf".source = clamdConfigFile;
+
+    systemd.services.clamav-daemon = mkIf cfg.daemon.enable {
+      description = "ClamAV daemon (clamd)";
+      after = optional cfg.updater.enable "clamav-freshclam.service";
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ clamdConfigFile ];
+
+      preStart = ''
+        mkdir -m 0755 -p ${runDir}
+        chown ${clamavUser}:${clamavGroup} ${runDir}
+      '';
+
+      serviceConfig = {
+        ExecStart = "${pkg}/bin/clamd";
+        ExecReload = "${pkgs.coreutils}/bin/kill -USR2 $MAINPID";
+        PrivateTmp = "yes";
+        PrivateDevices = "yes";
+        PrivateNetwork = "yes";
+      };
+    };
+
+    systemd.timers.clamav-freshclam = mkIf cfg.updater.enable {
+      description = "Timer for ClamAV virus database updater (freshclam)";
+      wantedBy = [ "timers.target" ];
+      timerConfig = {
+        OnCalendar = cfg.updater.interval;
+        Unit = "clamav-freshclam.service";
+      };
+    };
+
+    systemd.services.clamav-freshclam = mkIf cfg.updater.enable {
+      description = "ClamAV virus database updater (freshclam)";
+      restartTriggers = [ freshclamConfigFile ];
+      after = [ "network-online.target" ];
+      preStart = ''
+        mkdir -m 0755 -p ${stateDir}
+        chown ${clamavUser}:${clamavGroup} ${stateDir}
+      '';
+
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkg}/bin/freshclam";
+        SuccessExitStatus = "1"; # if databases are up to date
+        PrivateTmp = "yes";
+        PrivateDevices = "yes";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/fail2ban.nix b/nixos/modules/services/security/fail2ban.nix
new file mode 100644
index 00000000000..67e1026dcef
--- /dev/null
+++ b/nixos/modules/services/security/fail2ban.nix
@@ -0,0 +1,340 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.fail2ban;
+
+  fail2banConf = pkgs.writeText "fail2ban.local" cfg.daemonConfig;
+
+  jailConf = pkgs.writeText "jail.local" ''
+    [INCLUDES]
+
+    before = paths-nixos.conf
+
+    ${concatStringsSep "\n" (attrValues (flip mapAttrs cfg.jails (name: def:
+      optionalString (def != "")
+        ''
+          [${name}]
+          ${def}
+        '')))}
+  '';
+
+  pathsConf = pkgs.writeText "paths-nixos.conf" ''
+    # NixOS
+
+    [INCLUDES]
+
+    before = paths-common.conf
+
+    after  = paths-overrides.local
+
+    [DEFAULT]
+  '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.fail2ban = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to enable the fail2ban service.
+
+          See the documentation of <option>services.fail2ban.jails</option>
+          for what jails are enabled by default.
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.fail2ban;
+        defaultText = literalExpression "pkgs.fail2ban";
+        type = types.package;
+        example = literalExpression "pkgs.fail2ban_0_11";
+        description = "The fail2ban package to use for running the fail2ban service.";
+      };
+
+      packageFirewall = mkOption {
+        default = pkgs.iptables;
+        defaultText = literalExpression "pkgs.iptables";
+        type = types.package;
+        example = literalExpression "pkgs.nftables";
+        description = "The firewall package used by fail2ban service.";
+      };
+
+      extraPackages = mkOption {
+        default = [];
+        type = types.listOf types.package;
+        example = lib.literalExpression "[ pkgs.ipset ]";
+        description = ''
+          Extra packages to be made available to the fail2ban service. The example contains
+          the packages needed by the `iptables-ipset-proto6` action.
+        '';
+      };
+
+      maxretry = mkOption {
+        default = 3;
+        type = types.ints.unsigned;
+        description = "Number of failures before a host gets banned.";
+      };
+
+      banaction = mkOption {
+        default = "iptables-multiport";
+        type = types.str;
+        example = "nftables-multiport";
+        description = ''
+          Default banning action (e.g. iptables, iptables-new, iptables-multiport,
+          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";
+        type = types.str;
+        example = "nftables-allport";
+        description = ''
+          Default banning action (e.g. iptables, iptables-new, iptables-multiport,
+          shorewall, etc) It is used to define action_* variables. Can be overridden
+          globally or per section within jail.local file
+        '';
+      };
+
+      bantime-increment.enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          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;
+        example = "8m";
+        description = ''
+          "bantime-increment.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;
+        example = "48h";
+        description = ''
+          "bantime-increment.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;
+        example = "4";
+        description = ''
+          "bantime-increment.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;
+        example = "ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)";
+        description = ''
+          "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-increment.multipliers = mkOption {
+        default = "1 2 4 8 16 32 64";
+        type = types.str;
+        example = "2 4 16 128";
+        description = ''
+          "bantime-increment.multipliers" used to calculate next value of ban time instead of formula, coresponding
+          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
+        '';
+      };
+
+      bantime-increment.overalljails = mkOption {
+        default = false;
+        type = types.bool;
+        example = true;
+        description = ''
+          "bantime-increment.overalljails"  (if true) specifies the search of IP in the database will be executed
+          cross over all jails, if false (dafault), only current jail of the ban IP will be searched
+        '';
+      };
+
+      ignoreIP = mkOption {
+        default = [ ];
+        type = types.listOf types.str;
+        example = [ "192.168.0.0/16" "2001:DB8::42" ];
+        description = ''
+          "ignoreIP" can be a list of IP addresses, CIDR masks or DNS hosts. Fail2ban will not ban a host which
+          matches an address in this list. Several addresses can be defined using space (and/or comma) separator.
+        '';
+      };
+
+      daemonConfig = mkOption {
+        default = ''
+          [Definition]
+          logtarget = SYSLOG
+          socket    = /run/fail2ban/fail2ban.sock
+          pidfile   = /run/fail2ban/fail2ban.pid
+          dbfile    = /var/lib/fail2ban/fail2ban.sqlite3
+        '';
+        type = types.lines;
+        description = ''
+          The contents of Fail2ban's main configuration file.  It's
+          generally not necessary to change it.
+       '';
+      };
+
+      jails = mkOption {
+        default = { };
+        example = literalExpression ''
+          { apache-nohome-iptables = '''
+              # Block an IP address if it accesses a non-existent
+              # home directory more than 5 times in 10 minutes,
+              # since that indicates that it's scanning.
+              filter   = apache-nohome
+              action   = iptables-multiport[name=HTTP, port="http,https"]
+              logpath  = /var/log/httpd/error_log*
+              findtime = 600
+              bantime  = 600
+              maxretry = 5
+            ''';
+          }
+        '';
+        type = types.attrsOf types.lines;
+        description = ''
+          The configuration of each Fail2ban “jailâ€.  A jail
+          consists of an action (such as blocking a port using
+          <command>iptables</command>) that is triggered when a
+          filter applied to a log file triggers more than a certain
+          number of times in a certain time period.  Actions are
+          defined in <filename>/etc/fail2ban/action.d</filename>,
+          while filters are defined in
+          <filename>/etc/fail2ban/filter.d</filename>.
+
+          NixOS comes with a default <literal>sshd</literal> jail;
+          for it to work well,
+          <option>services.openssh.logLevel</option> should be set to
+          <literal>"VERBOSE"</literal> or higher so that fail2ban
+          can observe failed login attempts.
+          This module sets it to <literal>"VERBOSE"</literal> if
+          not set otherwise, so enabling fail2ban can make SSH logs
+          more verbose.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    warnings = mkIf (config.networking.firewall.enable == false && config.networking.nftables.enable == false) [
+      "fail2ban can not be used without a firewall"
+    ];
+
+    environment.systemPackages = [ cfg.package ];
+
+    environment.etc = {
+      "fail2ban/fail2ban.local".source = fail2banConf;
+      "fail2ban/jail.local".source = jailConf;
+      "fail2ban/fail2ban.conf".source = "${cfg.package}/etc/fail2ban/fail2ban.conf";
+      "fail2ban/jail.conf".source = "${cfg.package}/etc/fail2ban/jail.conf";
+      "fail2ban/paths-common.conf".source = "${cfg.package}/etc/fail2ban/paths-common.conf";
+      "fail2ban/paths-nixos.conf".source = pathsConf;
+      "fail2ban/action.d".source = "${cfg.package}/etc/fail2ban/action.d/*.conf";
+      "fail2ban/filter.d".source = "${cfg.package}/etc/fail2ban/filter.d/*.conf";
+    };
+
+    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
+        NoNewPrivileges = true;
+        # Directory
+        RuntimeDirectory = "fail2ban";
+        RuntimeDirectoryMode = "0750";
+        StateDirectory = "fail2ban";
+        StateDirectoryMode = "0750";
+        LogsDirectory = "fail2ban";
+        LogsDirectoryMode = "0750";
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+      };
+    };
+
+    # 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}
+      ''}
+      # Miscellaneous options
+      ignoreip    = 127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP}
+      maxretry    = ${toString cfg.maxretry}
+      backend     = systemd
+      # Actions
+      banaction   = ${cfg.banaction}
+      banaction_allports = ${cfg.banaction-allports}
+    '';
+    # 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.fail2ban.jails.sshd = mkDefault ''
+      enabled = true
+      port    = ${concatMapStringsSep "," (p: toString p) config.services.openssh.ports}
+    '';
+  };
+}
diff --git a/nixos/modules/services/security/fprintd.nix b/nixos/modules/services/security/fprintd.nix
new file mode 100644
index 00000000000..87c3f1f6f9e
--- /dev/null
+++ b/nixos/modules/services/security/fprintd.nix
@@ -0,0 +1,64 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.fprintd;
+  fprintdPkg = if cfg.tod.enable then pkgs.fprintd-tod else pkgs.fprintd;
+
+in
+
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.fprintd = {
+
+      enable = mkEnableOption "fprintd daemon and PAM module for fingerprint readers handling";
+
+      package = mkOption {
+        type = types.package;
+        default = fprintdPkg;
+        defaultText = literalExpression "if config.services.fprintd.tod.enable then pkgs.fprintd-tod else pkgs.fprintd";
+        description = ''
+          fprintd package to use.
+        '';
+      };
+
+      tod = {
+
+        enable = mkEnableOption "Touch OEM Drivers library support";
+
+        driver = mkOption {
+          type = types.package;
+          example = literalExpression "pkgs.libfprint-2-tod1-goodix";
+          description = ''
+            Touch OEM Drivers (TOD) package to use.
+          '';
+        };
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.dbus.packages = [ cfg.package ];
+
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.packages = [ cfg.package ];
+
+    systemd.services.fprintd.environment = mkIf cfg.tod.enable {
+      FP_TOD_DRIVERS_DIR = "${cfg.tod.driver}${cfg.tod.driver.driverPath}";
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/security/haka.nix b/nixos/modules/services/security/haka.nix
new file mode 100644
index 00000000000..2cfc05f3033
--- /dev/null
+++ b/nixos/modules/services/security/haka.nix
@@ -0,0 +1,156 @@
+# This module defines global configuration for Haka.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.haka;
+
+  haka = cfg.package;
+
+  hakaConf = pkgs.writeText "haka.conf"
+  ''
+    [general]
+    configuration = ${if lib.strings.hasPrefix "/" cfg.configFile
+      then "${cfg.configFile}"
+      else "${haka}/share/haka/sample/${cfg.configFile}"}
+    ${optionalString (builtins.lessThan 0 cfg.threads) "thread = ${cfg.threads}"}
+
+    [packet]
+    ${optionalString cfg.pcap ''module = "packet/pcap"''}
+    ${optionalString cfg.nfqueue ''module = "packet/nqueue"''}
+    ${optionalString cfg.dump.enable ''dump = "yes"''}
+    ${optionalString cfg.dump.enable ''dump_input = "${cfg.dump.input}"''}
+    ${optionalString cfg.dump.enable ''dump_output = "${cfg.dump.output}"''}
+
+    interfaces = "${lib.strings.concatStringsSep "," cfg.interfaces}"
+
+    [log]
+    # Select the log module
+    module = "log/syslog"
+
+    # Set the default logging level
+    #level = "info,packet=debug"
+
+    [alert]
+    # Select the alert module
+    module = "alert/syslog"
+
+    # Disable alert on standard output
+    #alert_on_stdout = no
+
+    # alert/file module option
+    #file = "/dev/null"
+  '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.haka = {
+
+      enable = mkEnableOption "Haka";
+
+      package = mkOption {
+        default = pkgs.haka;
+        defaultText = literalExpression "pkgs.haka";
+        type = types.package;
+        description = "
+          Which Haka derivation to use.
+        ";
+      };
+
+      configFile = mkOption {
+        default = "empty.lua";
+        example = "/srv/haka/myfilter.lua";
+        type = types.str;
+        description = ''
+          Specify which configuration file Haka uses.
+          It can be absolute path or a path relative to the sample directory of
+          the haka git repo.
+        '';
+      };
+
+      interfaces = mkOption {
+        default = [ "eth0" ];
+        example = [ "any" ];
+        type = with types; listOf str;
+        description = ''
+          Specify which interface(s) Haka listens to.
+          Use 'any' to listen to all interfaces.
+        '';
+      };
+
+      threads = mkOption {
+        default = 0;
+        example = 4;
+        type = types.int;
+        description = ''
+          The number of threads that will be used.
+          All system threads are used by default.
+        '';
+      };
+
+      pcap = mkOption {
+        default = true;
+        type = types.bool;
+        description = "Whether to enable pcap";
+      };
+
+      nfqueue = mkEnableOption "nfqueue";
+
+      dump.enable = mkEnableOption "dump";
+      dump.input  = mkOption {
+        default = "/tmp/input.pcap";
+        example = "/path/to/file.pcap";
+        type = types.path;
+        description = "Path to file where incoming packets are dumped";
+      };
+
+      dump.output  = mkOption {
+        default = "/tmp/output.pcap";
+        example = "/path/to/file.pcap";
+        type = types.path;
+        description = "Path to file where outgoing packets are dumped";
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = cfg.pcap != cfg.nfqueue;
+        message = "either pcap or nfqueue can be enabled, not both.";
+      }
+      { assertion = cfg.nfqueue -> !dump.enable;
+        message = "dump can only be used with nfqueue.";
+      }
+      { assertion = cfg.interfaces != [];
+        message = "at least one interface must be specified.";
+      }];
+
+
+    environment.systemPackages = [ haka ];
+
+    systemd.services.haka = {
+      description = "Haka";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = "${haka}/bin/haka -c ${hakaConf}";
+        ExecStop = "${haka}/bin/hakactl stop";
+        User = "root";
+        Type = "forking";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/haveged.nix b/nixos/modules/services/security/haveged.nix
new file mode 100644
index 00000000000..57cef7e44d5
--- /dev/null
+++ b/nixos/modules/services/security/haveged.nix
@@ -0,0 +1,77 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.haveged;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.haveged = {
+
+      enable = mkEnableOption ''
+        haveged entropy daemon, which refills /dev/random when low.
+        NOTE: does nothing on kernels newer than 5.6.
+      '';
+      # source for the note https://github.com/jirka-h/haveged/issues/57
+
+      refill_threshold = mkOption {
+        type = types.int;
+        default = 1024;
+        description = ''
+          The number of bits of available entropy beneath which
+          haveged should refill the entropy pool.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    # https://github.com/jirka-h/haveged/blob/a4b69d65a8dfc5a9f52ff8505c7f58dcf8b9234f/contrib/Fedora/haveged.service
+    systemd.services.haveged = {
+      description = "Entropy Daemon based on the HAVEGE algorithm";
+      unitConfig = {
+        Documentation = "man:haveged(8)";
+        DefaultDependencies = false;
+        ConditionKernelVersion = "<5.6";
+      };
+      wantedBy = [ "sysinit.target" ];
+      after = [ "systemd-tmpfiles-setup-dev.service" ];
+      before = [ "sysinit.target" "shutdown.target" "systemd-journald.service" ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.haveged}/bin/haveged -w ${toString cfg.refill_threshold} --Foreground -v 1";
+        Restart = "always";
+        SuccessExitStatus = "137 143";
+        SecureBits = "noroot-locked";
+        CapabilityBoundingSet = [ "CAP_SYS_ADMIN" "CAP_SYS_CHROOT" ];
+        # We can *not* set PrivateTmp=true as it can cause an ordering cycle.
+        PrivateTmp = false;
+        PrivateDevices = true;
+        ProtectSystem = "full";
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "newuname" "~@mount" ];
+        SystemCallErrorNumber = "EPERM";
+      };
+
+    };
+  };
+
+}
diff --git a/nixos/modules/services/security/hockeypuck.nix b/nixos/modules/services/security/hockeypuck.nix
new file mode 100644
index 00000000000..d0e152934f5
--- /dev/null
+++ b/nixos/modules/services/security/hockeypuck.nix
@@ -0,0 +1,106 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.hockeypuck;
+  settingsFormat = pkgs.formats.toml { };
+in {
+  meta.maintainers = with lib.maintainers; [ etu ];
+
+  options.services.hockeypuck = {
+    enable = lib.mkEnableOption "Hockeypuck OpenPGP Key Server";
+
+    port = lib.mkOption {
+      default = 11371;
+      type = lib.types.port;
+      description = "HKP port to listen on.";
+    };
+
+    settings = lib.mkOption {
+      type = settingsFormat.type;
+      default = { };
+      example = lib.literalExpression ''
+        {
+          hockeypuck = {
+            loglevel = "INFO";
+            logfile = "/var/log/hockeypuck/hockeypuck.log";
+            indexTemplate = "''${pkgs.hockeypuck-web}/share/templates/index.html.tmpl";
+            vindexTemplate = "''${pkgs.hockeypuck-web}/share/templates/index.html.tmpl";
+            statsTemplate = "''${pkgs.hockeypuck-web}/share/templates/stats.html.tmpl";
+            webroot = "''${pkgs.hockeypuck-web}/share/webroot";
+
+            hkp.bind = ":''${toString cfg.port}";
+
+            openpgp.db = {
+              driver = "postgres-jsonb";
+              dsn = "database=hockeypuck host=/var/run/postgresql sslmode=disable";
+            };
+          };
+        }
+      '';
+      description = ''
+        Configuration file for hockeypuck, here you can override
+        certain settings (<literal>loglevel</literal> and
+        <literal>openpgp.db.dsn</literal>) by just setting those values.
+
+        For other settings you need to use lib.mkForce to override them.
+
+        This service doesn't provision or enable postgres on your
+        system, it rather assumes that you enable postgres and create
+        the database yourself.
+
+        Example:
+        <literal>
+          services.postgresql = {
+            enable = true;
+            ensureDatabases = [ "hockeypuck" ];
+            ensureUsers = [{
+              name = "hockeypuck";
+              ensurePermissions."DATABASE hockeypuck" = "ALL PRIVILEGES";
+            }];
+          };
+        </literal>
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.hockeypuck.settings.hockeypuck = {
+      loglevel = lib.mkDefault "INFO";
+      logfile = "/var/log/hockeypuck/hockeypuck.log";
+      indexTemplate = "${pkgs.hockeypuck-web}/share/templates/index.html.tmpl";
+      vindexTemplate = "${pkgs.hockeypuck-web}/share/templates/index.html.tmpl";
+      statsTemplate = "${pkgs.hockeypuck-web}/share/templates/stats.html.tmpl";
+      webroot = "${pkgs.hockeypuck-web}/share/webroot";
+
+      hkp.bind = ":${toString cfg.port}";
+
+      openpgp.db = {
+        driver = "postgres-jsonb";
+        dsn = lib.mkDefault "database=hockeypuck host=/var/run/postgresql sslmode=disable";
+      };
+    };
+
+    users.users.hockeypuck = {
+      isSystemUser = true;
+      group = "hockeypuck";
+      description = "Hockeypuck user";
+    };
+    users.groups.hockeypuck = {};
+
+    systemd.services.hockeypuck = {
+      description = "Hockeypuck OpenPGP Key Server";
+      after = [ "network.target" "postgresql.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        WorkingDirectory = "/var/lib/hockeypuck";
+        User = "hockeypuck";
+        ExecStart = "${pkgs.hockeypuck}/bin/hockeypuck -config ${settingsFormat.generate "config.toml" cfg.settings}";
+        Restart = "always";
+        RestartSec = "5s";
+        LogsDirectory = "hockeypuck";
+        LogsDirectoryMode = "0755";
+        StateDirectory = "hockeypuck";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/hologram-agent.nix b/nixos/modules/services/security/hologram-agent.nix
new file mode 100644
index 00000000000..e29267e5000
--- /dev/null
+++ b/nixos/modules/services/security/hologram-agent.nix
@@ -0,0 +1,58 @@
+{pkgs, config, lib, ...}:
+
+with lib;
+
+let
+  cfg = config.services.hologram-agent;
+
+  cfgFile = pkgs.writeText "hologram-agent.json" (builtins.toJSON {
+    host = cfg.dialAddress;
+  });
+in {
+  options = {
+    services.hologram-agent = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the Hologram agent for AWS instance credentials";
+      };
+
+      dialAddress = mkOption {
+        type        = types.str;
+        default     = "localhost:3100";
+        description = "Hologram server and port.";
+      };
+
+      httpPort = mkOption {
+        type        = types.str;
+        default     = "80";
+        description = "Port for metadata service to listen on.";
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    boot.kernelModules = [ "dummy" ];
+
+    networking.interfaces.dummy0.ipv4.addresses = [
+      { address = "169.254.169.254"; prefixLength = 32; }
+    ];
+
+    systemd.services.hologram-agent = {
+      description = "Provide EC2 instance credentials to machines outside of EC2";
+      after       = [ "network.target" ];
+      wantedBy    = [ "multi-user.target" ];
+      requires    = [ "network-link-dummy0.service" "network-addresses-dummy0.service" ];
+      preStart = ''
+        /run/current-system/sw/bin/rm -fv /run/hologram.sock
+      '';
+      serviceConfig = {
+        ExecStart = "${pkgs.hologram}/bin/hologram-agent -debug -conf ${cfgFile} -port ${cfg.httpPort}";
+      };
+    };
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ ];
+}
diff --git a/nixos/modules/services/security/hologram-server.nix b/nixos/modules/services/security/hologram-server.nix
new file mode 100644
index 00000000000..4acf6ae0e21
--- /dev/null
+++ b/nixos/modules/services/security/hologram-server.nix
@@ -0,0 +1,130 @@
+{pkgs, config, lib, ...}:
+
+with lib;
+
+let
+  cfg = config.services.hologram-server;
+
+  cfgFile = pkgs.writeText "hologram-server.json" (builtins.toJSON {
+    ldap = {
+      host = cfg.ldapHost;
+      bind = {
+        dn       = cfg.ldapBindDN;
+        password = cfg.ldapBindPassword;
+      };
+      insecureldap    = cfg.ldapInsecure;
+      userattr        = cfg.ldapUserAttr;
+      baseDN          = cfg.ldapBaseDN;
+      enableldapRoles = cfg.enableLdapRoles;
+      roleAttr        = cfg.roleAttr;
+      groupClassAttr  = cfg.groupClassAttr;
+    };
+    aws = {
+      account     = cfg.awsAccount;
+      defaultrole = cfg.awsDefaultRole;
+    };
+    stats        = cfg.statsAddress;
+    listen       = cfg.listenAddress;
+    cachetimeout = cfg.cacheTimeoutSeconds;
+  });
+in {
+  options = {
+    services.hologram-server = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the Hologram server for AWS instance credentials";
+      };
+
+      listenAddress = mkOption {
+        type        = types.str;
+        default     = "0.0.0.0:3100";
+        description = "Address and port to listen on";
+      };
+
+      ldapHost = mkOption {
+        type        = types.str;
+        description = "Address of the LDAP server to use";
+      };
+
+      ldapInsecure = mkOption {
+        type        = types.bool;
+        default     = false;
+        description = "Whether to connect to LDAP over SSL or not";
+      };
+
+      ldapUserAttr = mkOption {
+        type        = types.str;
+        default     = "cn";
+        description = "The LDAP attribute for usernames";
+      };
+
+      ldapBaseDN = mkOption {
+        type        = types.str;
+        description = "The base DN for your Hologram users";
+      };
+
+      ldapBindDN = mkOption {
+        type        = types.str;
+        description = "DN of account to use to query the LDAP server";
+      };
+
+      ldapBindPassword = mkOption {
+        type        = types.str;
+        description = "Password of account to use to query the LDAP server";
+      };
+
+      enableLdapRoles = mkOption {
+        type        = types.bool;
+        default     = false;
+        description = "Whether to assign user roles based on the user's LDAP group memberships";
+      };
+
+      groupClassAttr = mkOption {
+        type = types.str;
+        default = "groupOfNames";
+        description = "The objectclass attribute to search for groups when enableLdapRoles is true";
+      };
+
+      roleAttr = mkOption {
+        type        = types.str;
+        default     = "businessCategory";
+        description = "Which LDAP group attribute to search for authorized role ARNs";
+      };
+
+      awsAccount = mkOption {
+        type        = types.str;
+        description = "AWS account number";
+      };
+
+      awsDefaultRole = mkOption {
+        type        = types.str;
+        description = "AWS default role";
+      };
+
+      statsAddress = mkOption {
+        type        = types.str;
+        default     = "";
+        description = "Address of statsd server";
+      };
+
+      cacheTimeoutSeconds = mkOption {
+        type        = types.int;
+        default     = 3600;
+        description = "How often (in seconds) to refresh the LDAP cache";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.hologram-server = {
+      description = "Provide EC2 instance credentials to machines outside of EC2";
+      after       = [ "network.target" ];
+      wantedBy    = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.hologram}/bin/hologram-server --debug --conf ${cfgFile}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/munge.nix b/nixos/modules/services/security/munge.nix
new file mode 100644
index 00000000000..89178886471
--- /dev/null
+++ b/nixos/modules/services/security/munge.nix
@@ -0,0 +1,68 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.munge;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.munge = {
+      enable = mkEnableOption "munge service";
+
+      password = mkOption {
+        default = "/etc/munge/munge.key";
+        type = types.path;
+        description = ''
+          The path to a daemon's secret key.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.munge ];
+
+    users.users.munge = {
+      description   = "Munge daemon user";
+      isSystemUser  = true;
+      group         = "munge";
+    };
+
+    users.groups.munge = {};
+
+    systemd.services.munged = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      path = [ pkgs.munge pkgs.coreutils ];
+
+      serviceConfig = {
+        ExecStartPre = "+${pkgs.coreutils}/bin/chmod 0400 ${cfg.password}";
+        ExecStart = "${pkgs.munge}/bin/munged --syslog --key-file ${cfg.password}";
+        PIDFile = "/run/munge/munged.pid";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        User = "munge";
+        Group = "munge";
+        StateDirectory = "munge";
+        StateDirectoryMode = "0711";
+        RuntimeDirectory = "munge";
+      };
+
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/security/nginx-sso.nix b/nixos/modules/services/security/nginx-sso.nix
new file mode 100644
index 00000000000..b4de1d36edd
--- /dev/null
+++ b/nixos/modules/services/security/nginx-sso.nix
@@ -0,0 +1,67 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nginx.sso;
+  pkg = getBin cfg.package;
+  configYml = pkgs.writeText "nginx-sso.yml" (builtins.toJSON cfg.configuration);
+in {
+  options.services.nginx.sso = {
+    enable = mkEnableOption "nginx-sso service";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.nginx-sso;
+      defaultText = literalExpression "pkgs.nginx-sso";
+      description = ''
+        The nginx-sso package that should be used.
+      '';
+    };
+
+    configuration = mkOption {
+      type = types.attrsOf types.unspecified;
+      default = {};
+      example = literalExpression ''
+        {
+          listen = { addr = "127.0.0.1"; port = 8080; };
+
+          providers.token.tokens = {
+            myuser = "MyToken";
+          };
+
+          acl = {
+            rule_sets = [
+              {
+                rules = [ { field = "x-application"; equals = "MyApp"; } ];
+                allow = [ "myuser" ];
+              }
+            ];
+          };
+        }
+      '';
+      description = ''
+        nginx-sso configuration
+        (<link xlink:href="https://github.com/Luzifer/nginx-sso/wiki/Main-Configuration">documentation</link>)
+        as a Nix attribute set.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.nginx-sso = {
+      description = "Nginx SSO Backend";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${pkg}/bin/nginx-sso \
+            --config ${configYml} \
+            --frontend-dir ${pkg}/share/frontend
+        '';
+        Restart = "always";
+        DynamicUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/oauth2_proxy.nix b/nixos/modules/services/security/oauth2_proxy.nix
new file mode 100644
index 00000000000..ce295bd4ba3
--- /dev/null
+++ b/nixos/modules/services/security/oauth2_proxy.nix
@@ -0,0 +1,591 @@
+# NixOS module for oauth2_proxy.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.oauth2_proxy;
+
+  # oauth2_proxy provides many options that are only relevant if you are using
+  # a certain provider. This set maps from provider name to a function that
+  # takes the configuration and returns a string that can be inserted into the
+  # command-line to launch oauth2_proxy.
+  providerSpecificOptions = {
+    azure = cfg: {
+      azure-tenant = cfg.azure.tenant;
+      resource = cfg.azure.resource;
+    };
+
+    github = cfg: { github = {
+      inherit (cfg.github) org team;
+    }; };
+
+    google = cfg: { google = with cfg.google; optionalAttrs (groups != []) {
+      admin-email = adminEmail;
+      service-account = serviceAccountJSON;
+      group = groups;
+    }; };
+  };
+
+  authenticatedEmailsFile = pkgs.writeText "authenticated-emails" cfg.email.addresses;
+
+  getProviderOptions = cfg: provider: providerSpecificOptions.${provider} or (_: {}) cfg;
+
+  allConfig = with cfg; {
+    inherit (cfg) provider scope upstream;
+    approval-prompt = approvalPrompt;
+    basic-auth-password = basicAuthPassword;
+    client-id = clientID;
+    client-secret = clientSecret;
+    custom-templates-dir = customTemplatesDir;
+    email-domain = email.domains;
+    http-address = httpAddress;
+    login-url = loginURL;
+    pass-access-token = passAccessToken;
+    pass-basic-auth = passBasicAuth;
+    pass-host-header = passHostHeader;
+    reverse-proxy = reverseProxy;
+    proxy-prefix = proxyPrefix;
+    profile-url = profileURL;
+    redeem-url = redeemURL;
+    redirect-url = redirectURL;
+    request-logging = requestLogging;
+    skip-auth-regex = skipAuthRegexes;
+    signature-key = signatureKey;
+    validate-url = validateURL;
+    htpasswd-file = htpasswd.file;
+    cookie = {
+      inherit (cookie) domain secure expire name secret refresh;
+      httponly = cookie.httpOnly;
+    };
+    set-xauthrequest = setXauthrequest;
+  } // lib.optionalAttrs (cfg.email.addresses != null) {
+    authenticated-emails-file = authenticatedEmailsFile;
+  } // lib.optionalAttrs (cfg.passBasicAuth) {
+    basic-auth-password = cfg.basicAuthPassword;
+  } // lib.optionalAttrs (cfg.htpasswd.file != null) {
+    display-htpasswd-file = cfg.htpasswd.displayForm;
+  } // lib.optionalAttrs tls.enable {
+    tls-cert-file = tls.certificate;
+    tls-key-file = tls.key;
+    https-address = tls.httpsAddress;
+  } // (getProviderOptions cfg cfg.provider) // cfg.extraConfig;
+
+  mapConfig = key: attr:
+  if attr != null && attr != [] then (
+    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 "";
+
+  configString = concatStringsSep " " (mapAttrsToList mapConfig allConfig);
+in
+{
+  options.services.oauth2_proxy = {
+    enable = mkEnableOption "oauth2_proxy";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.oauth2-proxy;
+      defaultText = literalExpression "pkgs.oauth2-proxy";
+      description = ''
+        The package that provides oauth2-proxy.
+      '';
+    };
+
+    ##############################################
+    # PROVIDER configuration
+    # Taken from: https://github.com/oauth2-proxy/oauth2-proxy/blob/master/providers/providers.go
+    provider = mkOption {
+      type = types.enum [
+        "adfs"
+        "azure"
+        "bitbucket"
+        "digitalocean"
+        "facebook"
+        "github"
+        "gitlab"
+        "google"
+        "keycloak"
+        "keycloak-oidc"
+        "linkedin"
+        "login.gov"
+        "nextcloud"
+        "oidc"
+      ];
+      default = "google";
+      description = ''
+        OAuth provider.
+      '';
+    };
+
+    approvalPrompt = mkOption {
+      type = types.enum ["force" "auto"];
+      default = "force";
+      description = ''
+        OAuth approval_prompt.
+      '';
+    };
+
+    clientID = mkOption {
+      type = types.nullOr types.str;
+      description = ''
+        The OAuth Client ID.
+      '';
+      example = "123456.apps.googleusercontent.com";
+    };
+
+    clientSecret = mkOption {
+      type = types.nullOr types.str;
+      description = ''
+        The OAuth Client Secret.
+      '';
+    };
+
+    skipAuthRegexes = mkOption {
+     type = types.listOf types.str;
+     default = [];
+     description = ''
+       Skip authentication for requests matching any of these regular
+       expressions.
+     '';
+    };
+
+    # XXX: Not clear whether these two options are mutually exclusive or not.
+    email = {
+      domains = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Authenticate emails with the specified domains. Use
+          <literal>*</literal> to authenticate any email.
+        '';
+      };
+
+      addresses = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = ''
+          Line-separated email addresses that are allowed to authenticate.
+        '';
+      };
+    };
+
+    loginURL = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Authentication endpoint.
+
+        You only need to set this if you are using a self-hosted provider (e.g.
+        Github Enterprise). If you're using a publicly hosted provider
+        (e.g github.com), then the default works.
+      '';
+      example = "https://provider.example.com/oauth/authorize";
+    };
+
+    redeemURL = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Token redemption endpoint.
+
+        You only need to set this if you are using a self-hosted provider (e.g.
+        Github Enterprise). If you're using a publicly hosted provider
+        (e.g github.com), then the default works.
+      '';
+      example = "https://provider.example.com/oauth/token";
+    };
+
+    validateURL = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Access token validation endpoint.
+
+        You only need to set this if you are using a self-hosted provider (e.g.
+        Github Enterprise). If you're using a publicly hosted provider
+        (e.g github.com), then the default works.
+      '';
+      example = "https://provider.example.com/user/emails";
+    };
+
+    redirectURL = mkOption {
+      # XXX: jml suspects this is always necessary, but the command-line
+      # doesn't require it so making it optional.
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The OAuth2 redirect URL.
+      '';
+      example = "https://internalapp.yourcompany.com/oauth2/callback";
+    };
+
+    azure = {
+      tenant = mkOption {
+        type = types.str;
+        default = "common";
+        description = ''
+          Go to a tenant-specific or common (tenant-independent) endpoint.
+        '';
+      };
+
+      resource = mkOption {
+        type = types.str;
+        description = ''
+          The resource that is protected.
+        '';
+      };
+    };
+
+    google = {
+      adminEmail = mkOption {
+        type = types.str;
+        description = ''
+          The Google Admin to impersonate for API calls.
+
+          Only users with access to the Admin APIs can access the Admin SDK
+          Directory API, thus the service account needs to impersonate one of
+          those users to access the Admin SDK Directory API.
+
+          See <link xlink:href="https://developers.google.com/admin-sdk/directory/v1/guides/delegation#delegate_domain-wide_authority_to_your_service_account" />.
+        '';
+      };
+
+      groups = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Restrict logins to members of these Google groups.
+        '';
+      };
+
+      serviceAccountJSON = mkOption {
+        type = types.path;
+        description = ''
+          The path to the service account JSON credentials.
+        '';
+      };
+    };
+
+    github = {
+      org = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Restrict logins to members of this organisation.
+        '';
+      };
+
+      team = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Restrict logins to members of this team.
+        '';
+      };
+    };
+
+
+    ####################################################
+    # UPSTREAM Configuration
+    upstream = mkOption {
+      type = with types; coercedTo str (x: [x]) (listOf str);
+      default = [];
+      description = ''
+        The http url(s) of the upstream endpoint or <literal>file://</literal>
+        paths for static files. Routing is based on the path.
+      '';
+    };
+
+    passAccessToken = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Pass OAuth access_token to upstream via X-Forwarded-Access-Token header.
+      '';
+    };
+
+    passBasicAuth = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream.
+      '';
+    };
+
+    basicAuthPassword = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The password to set when passing the HTTP Basic Auth header.
+      '';
+    };
+
+    passHostHeader = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Pass the request Host Header to upstream.
+      '';
+    };
+
+    signatureKey = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        GAP-Signature request signature key.
+      '';
+      example = "sha1:secret0";
+    };
+
+    cookie = {
+      domain = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Optional cookie domains to force cookies to (ie: `.yourcompany.com`).
+          The longest domain matching the request's host will be used (or the shortest
+          cookie domain if there is no match).
+        '';
+        example = ".yourcompany.com";
+      };
+
+      expire = mkOption {
+        type = types.str;
+        default = "168h0m0s";
+        description = ''
+          Expire timeframe for cookie.
+        '';
+      };
+
+      httpOnly = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Set HttpOnly cookie flag.
+        '';
+      };
+
+      name = mkOption {
+        type = types.str;
+        default = "_oauth2_proxy";
+        description = ''
+          The name of the cookie that the oauth_proxy creates.
+        '';
+      };
+
+      refresh = mkOption {
+        # XXX: Unclear what the behavior is when this is not specified.
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Refresh the cookie after this duration; 0 to disable.
+        '';
+        example = "168h0m0s";
+      };
+
+      secret = mkOption {
+        type = types.nullOr types.str;
+        description = ''
+          The seed string for secure cookies.
+        '';
+      };
+
+      secure = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Set secure (HTTPS) cookie flag.
+        '';
+      };
+    };
+
+    ####################################################
+    # OAUTH2 PROXY configuration
+
+    httpAddress = mkOption {
+      type = types.str;
+      default = "http://127.0.0.1:4180";
+      description = ''
+        HTTPS listening address.  This module does not expose the port by
+        default. If you want this URL to be accessible to other machines, please
+        add the port to <literal>networking.firewall.allowedTCPPorts</literal>.
+      '';
+    };
+
+    htpasswd = {
+      file = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          Additionally authenticate against a htpasswd file. Entries must be
+          created with <literal>htpasswd -s</literal> for SHA encryption.
+        '';
+      };
+
+      displayForm = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Display username / password login form if an htpasswd file is provided.
+        '';
+      };
+    };
+
+    customTemplatesDir = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Path to custom HTML templates.
+      '';
+    };
+
+    reverseProxy = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        In case when running behind a reverse proxy, controls whether headers
+        like <literal>X-Real-Ip</literal> are accepted. Usage behind a reverse
+        proxy will require this flag to be set to avoid logging the reverse
+        proxy IP address.
+      '';
+    };
+
+    proxyPrefix = mkOption {
+      type = types.str;
+      default = "/oauth2";
+      description = ''
+        The url root path that this proxy should be nested under.
+      '';
+    };
+
+    tls = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to serve over TLS.
+        '';
+      };
+
+      certificate = mkOption {
+        type = types.path;
+        description = ''
+          Path to certificate file.
+        '';
+      };
+
+      key = mkOption {
+        type = types.path;
+        description = ''
+          Path to private key file.
+        '';
+      };
+
+      httpsAddress = mkOption {
+        type = types.str;
+        default = ":443";
+        description = ''
+          <literal>addr:port</literal> to listen on for HTTPS clients.
+
+          Remember to add <literal>port</literal> to
+          <literal>allowedTCPPorts</literal> if you want other machines to be
+          able to connect to it.
+        '';
+      };
+    };
+
+    requestLogging = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Log requests to stdout.
+      '';
+    };
+
+    ####################################################
+    # UNKNOWN
+
+    # XXX: Is this mandatory? Is it part of another group? Is it part of the provider specification?
+    scope = mkOption {
+      # XXX: jml suspects this is always necessary, but the command-line
+      # doesn't require it so making it optional.
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        OAuth scope specification.
+      '';
+    };
+
+    profileURL = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Profile access endpoint.
+      '';
+    };
+
+    setXauthrequest = mkOption {
+      type = types.nullOr types.bool;
+      default = false;
+      description = ''
+        Set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode). Setting this to 'null' means using the upstream default (false).
+      '';
+    };
+
+    extraConfig = mkOption {
+      default = {};
+      type = types.attrsOf types.anything;
+      description = ''
+        Extra config to pass to oauth2-proxy.
+      '';
+    };
+
+    keyFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        oauth2-proxy allows passing sensitive configuration via environment variables.
+        Make a file that contains lines like
+        OAUTH2_PROXY_CLIENT_SECRET=asdfasdfasdf.apps.googleuserscontent.com
+        and specify the path here.
+      '';
+      example = "/run/keys/oauth2_proxy";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    services.oauth2_proxy = mkIf (cfg.keyFile != null) {
+      clientID = mkDefault null;
+      clientSecret = mkDefault null;
+      cookie.secret = mkDefault null;
+    };
+
+    users.users.oauth2_proxy = {
+      description = "OAuth2 Proxy";
+      isSystemUser = true;
+    };
+
+    systemd.services.oauth2_proxy = {
+      description = "OAuth2 Proxy";
+      path = [ cfg.package ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        User = "oauth2_proxy";
+        Restart = "always";
+        ExecStart = "${cfg.package}/bin/oauth2-proxy ${configString}";
+        EnvironmentFile = mkIf (cfg.keyFile != null) cfg.keyFile;
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/security/oauth2_proxy_nginx.nix b/nixos/modules/services/security/oauth2_proxy_nginx.nix
new file mode 100644
index 00000000000..5853c5a123c
--- /dev/null
+++ b/nixos/modules/services/security/oauth2_proxy_nginx.nix
@@ -0,0 +1,66 @@
+{ config, lib, ... }:
+with lib;
+let
+  cfg = config.services.oauth2_proxy.nginx;
+in
+{
+  options.services.oauth2_proxy.nginx = {
+    proxy = mkOption {
+      type = types.str;
+      default = config.services.oauth2_proxy.httpAddress;
+      defaultText = literalExpression "config.services.oauth2_proxy.httpAddress";
+      description = ''
+        The address of the reverse proxy endpoint for oauth2_proxy
+      '';
+    };
+    virtualHosts = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        A list of nginx virtual hosts to put behind the oauth2 proxy
+      '';
+    };
+  };
+  config.services.oauth2_proxy = mkIf (cfg.virtualHosts != [] && (hasPrefix "127.0.0.1:" cfg.proxy)) {
+    enable = true;
+  };
+  config.services.nginx = mkIf config.services.oauth2_proxy.enable (mkMerge
+  ((optional (cfg.virtualHosts != []) {
+    recommendedProxySettings = true; # needed because duplicate headers
+  }) ++ (map (vhost: {
+    virtualHosts.${vhost} = {
+      locations."/oauth2/" = {
+        proxyPass = cfg.proxy;
+        extraConfig = ''
+          proxy_set_header X-Scheme                $scheme;
+          proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
+        '';
+      };
+      locations."/oauth2/auth" = {
+        proxyPass = cfg.proxy;
+        extraConfig = ''
+          proxy_set_header X-Scheme         $scheme;
+          # nginx auth_request includes headers but not body
+          proxy_set_header Content-Length   "";
+          proxy_pass_request_body           off;
+        '';
+      };
+      locations."/".extraConfig = ''
+        auth_request /oauth2/auth;
+        error_page 401 = /oauth2/sign_in;
+
+        # pass information via X-User and X-Email headers to backend,
+        # requires running with --set-xauthrequest flag
+        auth_request_set $user   $upstream_http_x_auth_request_user;
+        auth_request_set $email  $upstream_http_x_auth_request_email;
+        proxy_set_header X-User  $user;
+        proxy_set_header X-Email $email;
+
+        # if you enabled --cookie-refresh, this is needed for it to work with auth_request
+        auth_request_set $auth_cookie $upstream_http_set_cookie;
+        add_header Set-Cookie $auth_cookie;
+      '';
+
+    };
+  }) cfg.virtualHosts)));
+}
diff --git a/nixos/modules/services/security/opensnitch.nix b/nixos/modules/services/security/opensnitch.nix
new file mode 100644
index 00000000000..f9b4985e199
--- /dev/null
+++ b/nixos/modules/services/security/opensnitch.nix
@@ -0,0 +1,125 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.opensnitch;
+  format = pkgs.formats.json {};
+in {
+  options = {
+    services.opensnitch = {
+      enable = mkEnableOption "Opensnitch application firewall";
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = format.type;
+
+          options = {
+            Server = {
+
+              Address = mkOption {
+                type = types.str;
+                description = ''
+                  Unix socket path (unix:///tmp/osui.sock, the "unix:///" part is
+                  mandatory) or TCP socket (192.168.1.100:50051).
+                '';
+              };
+
+              LogFile = mkOption {
+                type = types.path;
+                description = ''
+                  File to write logs to (use /dev/stdout to write logs to standard
+                  output).
+                '';
+              };
+
+            };
+
+            DefaultAction = mkOption {
+              type = types.enum [ "allow" "deny" ];
+              description = ''
+                Default action whether to block or allow application internet
+                access.
+              '';
+            };
+
+            DefaultDuration = mkOption {
+              type = types.enum [
+                "once" "always" "until restart" "30s" "5m" "15m" "30m" "1h"
+              ];
+              description = ''
+                Default duration of firewall rule.
+              '';
+            };
+
+            InterceptUnknown = mkOption {
+              type = types.bool;
+              description = ''
+                Wheter to intercept spare connections.
+              '';
+            };
+
+            ProcMonitorMethod = mkOption {
+              type = types.enum [ "ebpf" "proc" "ftrace" "audit" ];
+              description = ''
+                Which process monitoring method to use.
+              '';
+            };
+
+            LogLevel = mkOption {
+              type = types.enum [ 0 1 2 3 4 ];
+              description = ''
+                Default log level from 0 to 4 (debug, info, important, warning,
+                error).
+              '';
+            };
+
+            Firewall = mkOption {
+              type = types.enum [ "iptables" "nftables" ];
+              description = ''
+                Which firewall backend to use.
+              '';
+            };
+
+            Stats = {
+
+              MaxEvents = mkOption {
+                type = types.int;
+                description = ''
+                  Max events to send to the GUI.
+                '';
+              };
+
+              MaxStats = mkOption {
+                type = types.int;
+                description = ''
+                  Max stats per item to keep in backlog.
+                '';
+              };
+
+            };
+          };
+        };
+        description = ''
+          opensnitchd configuration. Refer to
+          <link xlink:href="https://github.com/evilsocket/opensnitch/wiki/Configurations"/>
+          for details on supported values.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    # pkg.opensnitch is referred to elsewhere in the module so we don't need to worry about it being garbage collected
+    services.opensnitch.settings = mapAttrs (_: v: mkDefault v) (builtins.fromJSON (builtins.unsafeDiscardStringContext (builtins.readFile "${pkgs.opensnitch}/etc/default-config.json")));
+
+    systemd = {
+      packages = [ pkgs.opensnitch ];
+      services.opensnitchd.wantedBy = [ "multi-user.target" ];
+    };
+
+    environment.etc."opensnitchd/default-config.json".source = format.generate "default-config.json" cfg.settings;
+
+  };
+}
+
diff --git a/nixos/modules/services/security/physlock.nix b/nixos/modules/services/security/physlock.nix
new file mode 100644
index 00000000000..760e80f147f
--- /dev/null
+++ b/nixos/modules/services/security/physlock.nix
@@ -0,0 +1,139 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.physlock;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.physlock = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the <command>physlock</command> screen locking mechanism.
+
+          Enable this and then run <command>systemctl start physlock</command>
+          to securely lock the screen.
+
+          This will switch to a new virtual terminal, turn off console
+          switching and disable SysRq mechanism (when
+          <option>services.physlock.disableSysRq</option> is set)
+          until the root or user password is given.
+        '';
+      };
+
+      allowAnyUser = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to allow any user to lock the screen. This will install a
+          setuid wrapper to allow any user to start physlock as root, which
+          is a minor security risk. Call the physlock binary to use this instead
+          of using the systemd service.
+        '';
+      };
+
+      disableSysRq = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to disable SysRq when locked with physlock.
+        '';
+      };
+
+      lockMessage = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Message to show on physlock login terminal.
+        '';
+      };
+
+      lockOn = {
+
+        suspend = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether to lock screen with physlock just before suspend.
+          '';
+        };
+
+        hibernate = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether to lock screen with physlock just before hibernate.
+          '';
+        };
+
+        extraTargets = mkOption {
+          type = types.listOf types.str;
+          default = [];
+          example = [ "display-manager.service" ];
+          description = ''
+            Other targets to lock the screen just before.
+
+            Useful if you want to e.g. both autologin to X11 so that
+            your <filename>~/.xsession</filename> gets executed and
+            still to have the screen locked so that the system can be
+            booted relatively unattended.
+          '';
+        };
+
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable (mkMerge [
+    {
+
+      # for physlock -l and physlock -L
+      environment.systemPackages = [ pkgs.physlock ];
+
+      systemd.services.physlock = {
+        enable = true;
+        description = "Physlock";
+        wantedBy = optional cfg.lockOn.suspend   "suspend.target"
+                ++ optional cfg.lockOn.hibernate "hibernate.target"
+                ++ cfg.lockOn.extraTargets;
+        before   = optional cfg.lockOn.suspend   "systemd-suspend.service"
+                ++ optional cfg.lockOn.hibernate "systemd-hibernate.service"
+                ++ optional (cfg.lockOn.hibernate || cfg.lockOn.suspend) "systemd-suspend-then-hibernate.service"
+                ++ cfg.lockOn.extraTargets;
+        serviceConfig = {
+          Type = "forking";
+          ExecStart = "${pkgs.physlock}/bin/physlock -d${optionalString cfg.disableSysRq "s"}${optionalString (cfg.lockMessage != "") " -p \"${cfg.lockMessage}\""}";
+        };
+      };
+
+      security.pam.services.physlock = {};
+
+    }
+
+    (mkIf cfg.allowAnyUser {
+
+      security.wrappers.physlock =
+        { setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${pkgs.physlock}/bin/physlock";
+        };
+
+    })
+  ]);
+
+}
diff --git a/nixos/modules/services/security/privacyidea.nix b/nixos/modules/services/security/privacyidea.nix
new file mode 100644
index 00000000000..b8e2d9a8b0d
--- /dev/null
+++ b/nixos/modules/services/security/privacyidea.nix
@@ -0,0 +1,309 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.privacyidea;
+  opt = options.services.privacyidea;
+
+  uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; };
+  python = uwsgi.python3;
+  penv = python.withPackages (const [ pkgs.privacyidea ]);
+  logCfg = pkgs.writeText "privacyidea-log.cfg" ''
+    [formatters]
+    keys=detail
+
+    [handlers]
+    keys=stream
+
+    [formatter_detail]
+    class=privacyidea.lib.log.SecureFormatter
+    format=[%(asctime)s][%(process)d][%(thread)d][%(levelname)s][%(name)s:%(lineno)d] %(message)s
+
+    [handler_stream]
+    class=StreamHandler
+    level=NOTSET
+    formatter=detail
+    args=(sys.stdout,)
+
+    [loggers]
+    keys=root,privacyidea
+
+    [logger_privacyidea]
+    handlers=stream
+    qualname=privacyidea
+    level=INFO
+
+    [logger_root]
+    handlers=stream
+    level=ERROR
+  '';
+
+  piCfgFile = pkgs.writeText "privacyidea.cfg" ''
+    SUPERUSER_REALM = [ '${concatStringsSep "', '" cfg.superuserRealm}' ]
+    SQLALCHEMY_DATABASE_URI = 'postgresql:///privacyidea'
+    SECRET_KEY = '${cfg.secretKey}'
+    PI_PEPPER = '${cfg.pepper}'
+    PI_ENCFILE = '${cfg.encFile}'
+    PI_AUDIT_KEY_PRIVATE = '${cfg.auditKeyPrivate}'
+    PI_AUDIT_KEY_PUBLIC = '${cfg.auditKeyPublic}'
+    PI_LOGCONFIG = '${logCfg}'
+    ${cfg.extraConfig}
+  '';
+
+in
+
+{
+  options = {
+    services.privacyidea = {
+      enable = mkEnableOption "PrivacyIDEA";
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/root/privacyidea.env";
+        description = ''
+          File to load as environment file. Environment variables
+          from this file will be interpolated into the config file
+          using <package>envsubst</package> which is helpful for specifying
+          secrets:
+          <programlisting>
+          { <xref linkend="opt-services.privacyidea.secretKey" /> = "$SECRET"; }
+          </programlisting>
+
+          The environment-file can now specify the actual secret key:
+          <programlisting>
+          SECRET=veryverytopsecret
+          </programlisting>
+        '';
+      };
+
+      stateDir = mkOption {
+        type = types.str;
+        default = "/var/lib/privacyidea";
+        description = ''
+          Directory where all PrivacyIDEA files will be placed by default.
+        '';
+      };
+
+      superuserRealm = mkOption {
+        type = types.listOf types.str;
+        default = [ "super" "administrators" ];
+        description = ''
+          The realm where users are allowed to login as administrators.
+        '';
+      };
+
+      secretKey = mkOption {
+        type = types.str;
+        example = "t0p s3cr3t";
+        description = ''
+          This is used to encrypt the auth_token.
+        '';
+      };
+
+      pepper = mkOption {
+        type = types.str;
+        example = "Never know...";
+        description = ''
+          This is used to encrypt the admin passwords.
+        '';
+      };
+
+      encFile = mkOption {
+        type = types.str;
+        default = "${cfg.stateDir}/enckey";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/enckey"'';
+        description = ''
+          This is used to encrypt the token data and token passwords
+        '';
+      };
+
+      auditKeyPrivate = mkOption {
+        type = types.str;
+        default = "${cfg.stateDir}/private.pem";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/private.pem"'';
+        description = ''
+          Private Key for signing the audit log.
+        '';
+      };
+
+      auditKeyPublic = mkOption {
+        type = types.str;
+        default = "${cfg.stateDir}/public.pem";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/public.pem"'';
+        description = ''
+          Public key for checking signatures of the audit log.
+        '';
+      };
+
+      adminPasswordFile = mkOption {
+        type = types.path;
+        description = "File containing password for the admin user";
+      };
+
+      adminEmail = mkOption {
+        type = types.str;
+        example = "admin@example.com";
+        description = "Mail address for the admin user";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration options for pi.cfg.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "privacyidea";
+        description = "User account under which PrivacyIDEA runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "privacyidea";
+        description = "Group account under which PrivacyIDEA runs.";
+      };
+
+      ldap-proxy = {
+        enable = mkEnableOption "PrivacyIDEA LDAP Proxy";
+
+        configFile = mkOption {
+          type = types.path;
+          description = ''
+            Path to PrivacyIDEA LDAP Proxy configuration (proxy.ini).
+          '';
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "pi-ldap-proxy";
+          description = "User account under which PrivacyIDEA LDAP proxy runs.";
+        };
+
+        group = mkOption {
+          type = types.str;
+          default = "pi-ldap-proxy";
+          description = "Group account under which PrivacyIDEA LDAP proxy runs.";
+        };
+      };
+    };
+  };
+
+  config = mkMerge [
+
+    (mkIf cfg.enable {
+
+      environment.systemPackages = [ pkgs.privacyidea ];
+
+      services.postgresql.enable = mkDefault true;
+
+      systemd.services.privacyidea = let
+        piuwsgi = pkgs.writeText "uwsgi.json" (builtins.toJSON {
+          uwsgi = {
+            buffer-size = 8192;
+            plugins = [ "python3" ];
+            pythonpath = "${penv}/${uwsgi.python3.sitePackages}";
+            socket = "/run/privacyidea/socket";
+            uid = cfg.user;
+            gid = cfg.group;
+            chmod-socket = 770;
+            chown-socket = "${cfg.user}:nginx";
+            chdir = cfg.stateDir;
+            wsgi-file = "${penv}/etc/privacyidea/privacyideaapp.wsgi";
+            processes = 4;
+            harakiri = 60;
+            reload-mercy = 8;
+            stats = "/run/privacyidea/stats.socket";
+            max-requests = 2000;
+            limit-as = 1024;
+            reload-on-as = 512;
+            reload-on-rss = 256;
+            no-orphans = true;
+            vacuum = true;
+          };
+        });
+      in {
+        wantedBy = [ "multi-user.target" ];
+        after = [ "postgresql.service" ];
+        path = with pkgs; [ openssl ];
+        environment.PRIVACYIDEA_CONFIGFILE = "${cfg.stateDir}/privacyidea.cfg";
+        preStart = let
+          pi-manage = "${config.security.sudo.package}/bin/sudo -u privacyidea -HE ${penv}/bin/pi-manage";
+          pgsu = config.services.postgresql.superUser;
+          psql = config.services.postgresql.package;
+        in ''
+          mkdir -p ${cfg.stateDir} /run/privacyidea
+          chown ${cfg.user}:${cfg.group} -R ${cfg.stateDir} /run/privacyidea
+          umask 077
+          ${lib.getBin pkgs.envsubst}/bin/envsubst -o ${cfg.stateDir}/privacyidea.cfg \
+                                                   -i "${piCfgFile}"
+          chown ${cfg.user}:${cfg.group} ${cfg.stateDir}/privacyidea.cfg
+          if ! test -e "${cfg.stateDir}/db-created"; then
+            ${config.security.sudo.package}/bin/sudo -u ${pgsu} ${psql}/bin/createuser --no-superuser --no-createdb --no-createrole ${cfg.user}
+            ${config.security.sudo.package}/bin/sudo -u ${pgsu} ${psql}/bin/createdb --owner ${cfg.user} privacyidea
+            ${pi-manage} create_enckey
+            ${pi-manage} create_audit_keys
+            ${pi-manage} createdb
+            ${pi-manage} admin add admin -e ${cfg.adminEmail} -p "$(cat ${cfg.adminPasswordFile})"
+            ${pi-manage} db stamp head -d ${penv}/lib/privacyidea/migrations
+            touch "${cfg.stateDir}/db-created"
+            chmod g+r "${cfg.stateDir}/enckey" "${cfg.stateDir}/private.pem"
+          fi
+          ${pi-manage} db upgrade -d ${penv}/lib/privacyidea/migrations
+        '';
+        serviceConfig = {
+          Type = "notify";
+          ExecStart = "${uwsgi}/bin/uwsgi --json ${piuwsgi}";
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
+          ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
+          NotifyAccess = "main";
+          KillSignal = "SIGQUIT";
+        };
+      };
+
+      users.users.privacyidea = mkIf (cfg.user == "privacyidea") {
+        group = cfg.group;
+        isSystemUser = true;
+      };
+
+      users.groups.privacyidea = mkIf (cfg.group == "privacyidea") {};
+    })
+
+    (mkIf cfg.ldap-proxy.enable {
+
+      systemd.services.privacyidea-ldap-proxy = let
+        ldap-proxy-env = pkgs.python3.withPackages (ps: [ ps.privacyidea-ldap-proxy ]);
+      in {
+        description = "privacyIDEA LDAP proxy";
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          User = cfg.ldap-proxy.user;
+          Group = cfg.ldap-proxy.group;
+          ExecStart = ''
+            ${ldap-proxy-env}/bin/twistd \
+              --nodaemon \
+              --pidfile= \
+              -u ${cfg.ldap-proxy.user} \
+              -g ${cfg.ldap-proxy.group} \
+              ldap-proxy \
+              -c ${cfg.ldap-proxy.configFile}
+          '';
+          Restart = "always";
+        };
+      };
+
+      users.users.pi-ldap-proxy = mkIf (cfg.ldap-proxy.user == "pi-ldap-proxy") {
+        group = cfg.ldap-proxy.group;
+        isSystemUser = true;
+      };
+
+      users.groups.pi-ldap-proxy = mkIf (cfg.ldap-proxy.group == "pi-ldap-proxy") {};
+    })
+  ];
+
+}
diff --git a/nixos/modules/services/security/shibboleth-sp.nix b/nixos/modules/services/security/shibboleth-sp.nix
new file mode 100644
index 00000000000..fea2a855e20
--- /dev/null
+++ b/nixos/modules/services/security/shibboleth-sp.nix
@@ -0,0 +1,75 @@
+{pkgs, config, lib, ...}:
+
+with lib;
+let
+  cfg = config.services.shibboleth-sp;
+in {
+  options = {
+    services.shibboleth-sp = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the shibboleth service";
+      };
+
+      configFile = mkOption {
+        type = types.path;
+        example = literalExpression ''"''${pkgs.shibboleth-sp}/etc/shibboleth/shibboleth2.xml"'';
+        description = "Path to shibboleth config file";
+      };
+
+      fastcgi.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to include the shibauthorizer and shibresponder FastCGI processes";
+      };
+
+      fastcgi.shibAuthorizerPort = mkOption {
+        type = types.int;
+        default = 9100;
+        description = "Port for shibauthorizer FastCGI proccess to bind to";
+      };
+
+      fastcgi.shibResponderPort = mkOption {
+        type = types.int;
+        default = 9101;
+        description = "Port for shibauthorizer FastCGI proccess to bind to";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.shibboleth-sp = {
+      description = "Provides SSO and federation for web applications";
+      after       = lib.optionals cfg.fastcgi.enable [ "shibresponder.service" "shibauthorizer.service" ];
+      wantedBy    = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.shibboleth-sp}/bin/shibd -F -d ${pkgs.shibboleth-sp} -c ${cfg.configFile}";
+      };
+    };
+
+    systemd.services.shibresponder = mkIf cfg.fastcgi.enable {
+      description = "Provides SSO through Shibboleth via FastCGI";
+      after       = [ "network.target" ];
+      wantedBy    = [ "multi-user.target" ];
+      path    	  = [ "${pkgs.spawn_fcgi}" ];
+      environment.SHIBSP_CONFIG = "${cfg.configFile}";
+      serviceConfig = {
+        ExecStart = "${pkgs.spawn_fcgi}/bin/spawn-fcgi -n -p ${toString cfg.fastcgi.shibResponderPort} ${pkgs.shibboleth-sp}/lib/shibboleth/shibresponder";
+      };
+    };
+
+    systemd.services.shibauthorizer = mkIf cfg.fastcgi.enable {
+      description = "Provides SSO through Shibboleth via FastCGI";
+      after       = [ "network.target" ];
+      wantedBy    = [ "multi-user.target" ];
+      path    	  = [ "${pkgs.spawn_fcgi}" ];
+      environment.SHIBSP_CONFIG = "${cfg.configFile}";
+      serviceConfig = {
+        ExecStart = "${pkgs.spawn_fcgi}/bin/spawn-fcgi -n -p ${toString cfg.fastcgi.shibAuthorizerPort} ${pkgs.shibboleth-sp}/lib/shibboleth/shibauthorizer";
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ jammerful ];
+}
diff --git a/nixos/modules/services/security/sks.nix b/nixos/modules/services/security/sks.nix
new file mode 100644
index 00000000000..f4911597564
--- /dev/null
+++ b/nixos/modules/services/security/sks.nix
@@ -0,0 +1,146 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.sks;
+  sksPkg = cfg.package;
+  dbConfig = pkgs.writeText "DB_CONFIG" ''
+    ${cfg.extraDbConfig}
+  '';
+
+in {
+  meta.maintainers = with maintainers; [ primeos calbrecht jcumming ];
+
+  options = {
+
+    services.sks = {
+
+      enable = mkEnableOption ''
+        SKS (synchronizing key server for OpenPGP) and start the database
+        server. You need to create "''${dataDir}/dump/*.gpg" for the initial
+        import'';
+
+      package = mkOption {
+        default = pkgs.sks;
+        defaultText = literalExpression "pkgs.sks";
+        type = types.package;
+        description = "Which SKS derivation to use.";
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/db/sks";
+        example = "/var/lib/sks";
+        # TODO: The default might change to "/var/lib/sks" as this is more
+        # common. There's also https://github.com/NixOS/nixpkgs/issues/26256
+        # and "/var/db" is not FHS compliant (seems to come from BSD).
+        description = ''
+          Data directory (-basedir) for SKS, where the database and all
+          configuration files are located (e.g. KDB, PTree, membership and
+          sksconf).
+        '';
+      };
+
+      extraDbConfig = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Set contents of the files "KDB/DB_CONFIG" and "PTree/DB_CONFIG" within
+          the ''${dataDir} directory. This is used to configure options for the
+          database for the sks key server.
+
+          Documentation of available options are available in the file named
+          "sampleConfig/DB_CONFIG" in the following repository:
+          https://bitbucket.org/skskeyserver/sks-keyserver/src
+        '';
+      };
+
+      hkpAddress = mkOption {
+        default = [ "127.0.0.1" "::1" ];
+        type = types.listOf types.str;
+        description = ''
+          Domain names, IPv4 and/or IPv6 addresses to listen on for HKP
+          requests.
+        '';
+      };
+
+      hkpPort = mkOption {
+        default = 11371;
+        type = types.ints.u16;
+        description = "HKP port to listen on.";
+      };
+
+      webroot = mkOption {
+        type = types.nullOr types.path;
+        default = "${sksPkg.webSamples}/OpenPKG";
+        defaultText = literalExpression ''"''${package.webSamples}/OpenPKG"'';
+        description = ''
+          Source directory (will be symlinked, if not null) for the files the
+          built-in webserver should serve. SKS (''${pkgs.sks.webSamples})
+          provides the following examples: "HTML5", "OpenPKG", and "XHTML+ES".
+          The index file can be named index.html, index.htm, index.xhtm, or
+          index.xhtml. Files with the extensions .css, .es, .js, .jpg, .jpeg,
+          .png, or .gif are supported. Subdirectories and filenames with
+          anything other than alphanumeric characters and the '.' character
+          will be ignored.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users = {
+      users.sks = {
+        isSystemUser = true;
+        description = "SKS user";
+        home = cfg.dataDir;
+        createHome = true;
+        group = "sks";
+        useDefaultShell = true;
+        packages = [ sksPkg pkgs.db ];
+      };
+      groups.sks = { };
+    };
+
+    systemd.services = let
+      hkpAddress = "'" + (builtins.concatStringsSep " " cfg.hkpAddress) + "'" ;
+      hkpPort = builtins.toString cfg.hkpPort;
+    in {
+      sks-db = {
+        description = "SKS database server";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        preStart = ''
+          ${lib.optionalString (cfg.webroot != null)
+            "ln -sfT \"${cfg.webroot}\" web"}
+          mkdir -p dump
+          ${sksPkg}/bin/sks build dump/*.gpg -n 10 -cache 100 || true #*/
+          ${sksPkg}/bin/sks cleandb || true
+          ${sksPkg}/bin/sks pbuild -cache 20 -ptree_cache 70 || true
+          # Check that both database configs are symlinks before overwriting them
+          # TODO: The initial build will be without DB_CONFIG, but this will
+          # hopefully not cause any significant problems. It might be better to
+          # create both directories manually but we have to check that this does
+          # not affect the initial build of the DB.
+          for CONFIG_FILE in KDB/DB_CONFIG PTree/DB_CONFIG; do
+            if [ -e $CONFIG_FILE ] && [ ! -L $CONFIG_FILE ]; then
+              echo "$CONFIG_FILE exists but is not a symlink." >&2
+              echo "Please remove $PWD/$CONFIG_FILE manually to continue." >&2
+              exit 1
+            fi
+            ln -sf ${dbConfig} $CONFIG_FILE
+          done
+        '';
+        serviceConfig = {
+          WorkingDirectory = "~";
+          User = "sks";
+          Group = "sks";
+          Restart = "always";
+          ExecStart = "${sksPkg}/bin/sks db -hkp_address ${hkpAddress} -hkp_port ${hkpPort}";
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/sshguard.nix b/nixos/modules/services/security/sshguard.nix
new file mode 100644
index 00000000000..53bd9efa5ac
--- /dev/null
+++ b/nixos/modules/services/security/sshguard.nix
@@ -0,0 +1,161 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.sshguard;
+
+  configFile = let
+    args = lib.concatStringsSep " " ([
+      "-afb"
+      "-p info"
+      "-o cat"
+      "-n1"
+    ] ++ (map (name: "-t ${escapeShellArg name}") cfg.services));
+    backend = if config.networking.nftables.enable
+      then "sshg-fw-nft-sets"
+      else "sshg-fw-ipset";
+  in pkgs.writeText "sshguard.conf" ''
+    BACKEND="${pkgs.sshguard}/libexec/${backend}"
+    LOGREADER="LANG=C ${pkgs.systemd}/bin/journalctl ${args}"
+  '';
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.sshguard = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to enable the sshguard service.";
+      };
+
+      attack_threshold = mkOption {
+        default = 30;
+        type = types.int;
+        description = ''
+            Block attackers when their cumulative attack score exceeds threshold. Most attacks have a score of 10.
+          '';
+      };
+
+      blacklist_threshold = mkOption {
+        default = null;
+        example = 120;
+        type = types.nullOr types.int;
+        description = ''
+            Blacklist an attacker when its score exceeds threshold. Blacklisted addresses are loaded from and added to blacklist-file.
+          '';
+      };
+
+      blacklist_file = mkOption {
+        default = "/var/lib/sshguard/blacklist.db";
+        type = types.path;
+        description = ''
+            Blacklist an attacker when its score exceeds threshold. Blacklisted addresses are loaded from and added to blacklist-file.
+          '';
+      };
+
+      blocktime = mkOption {
+        default = 120;
+        type = types.int;
+        description = ''
+            Block attackers for initially blocktime seconds after exceeding threshold. Subsequent blocks increase by a factor of 1.5.
+
+            sshguard unblocks attacks at random intervals, so actual block times will be longer.
+          '';
+      };
+
+      detection_time = mkOption {
+        default = 1800;
+        type = types.int;
+        description = ''
+            Remember potential attackers for up to detection_time seconds before resetting their score.
+          '';
+      };
+
+      whitelist = mkOption {
+        default = [ ];
+        example = [ "198.51.100.56" "198.51.100.2" ];
+        type = types.listOf types.str;
+        description = ''
+            Whitelist a list of addresses, hostnames, or address blocks.
+          '';
+      };
+
+      services = mkOption {
+        default = [ "sshd" ];
+        example = [ "sshd" "exim" ];
+        type = types.listOf types.str;
+        description = ''
+            Systemd services sshguard should receive logs of.
+          '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.etc."sshguard.conf".source = configFile;
+
+    systemd.services.sshguard = {
+      description = "SSHGuard brute-force attacks protection system";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      partOf = optional config.networking.firewall.enable "firewall.service";
+
+      restartTriggers = [ configFile ];
+
+      path = with pkgs; if config.networking.nftables.enable
+        then [ nftables iproute2 systemd ]
+        else [ iptables ipset iproute2 systemd ];
+
+      # The sshguard ipsets must exist before we invoke
+      # iptables. sshguard creates the ipsets after startup if
+      # necessary, but if we let sshguard do it, we can't reliably add
+      # the iptables rules because postStart races with the creation
+      # of the ipsets. So instead, we create both the ipsets and
+      # firewall rules before sshguard starts.
+      preStart = optionalString config.networking.firewall.enable ''
+        ${pkgs.ipset}/bin/ipset -quiet create -exist sshguard4 hash:net family inet
+        ${pkgs.iptables}/bin/iptables  -I INPUT -m set --match-set sshguard4 src -j DROP
+      '' + optionalString (config.networking.firewall.enable && config.networking.enableIPv6) ''
+        ${pkgs.ipset}/bin/ipset -quiet create -exist sshguard6 hash:net family inet6
+        ${pkgs.iptables}/bin/ip6tables -I INPUT -m set --match-set sshguard6 src -j DROP
+      '';
+
+      postStop = optionalString config.networking.firewall.enable ''
+        ${pkgs.iptables}/bin/iptables  -D INPUT -m set --match-set sshguard4 src -j DROP
+        ${pkgs.ipset}/bin/ipset -quiet destroy sshguard4
+      '' + optionalString (config.networking.firewall.enable && config.networking.enableIPv6) ''
+        ${pkgs.iptables}/bin/ip6tables -D INPUT -m set --match-set sshguard6 src -j DROP
+        ${pkgs.ipset}/bin/ipset -quiet destroy sshguard6
+      '';
+
+      unitConfig.Documentation = "man:sshguard(8)";
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = let
+          args = lib.concatStringsSep " " ([
+            "-a ${toString cfg.attack_threshold}"
+            "-p ${toString cfg.blocktime}"
+            "-s ${toString cfg.detection_time}"
+            (optionalString (cfg.blacklist_threshold != null) "-b ${toString cfg.blacklist_threshold}:${cfg.blacklist_file}")
+          ] ++ (map (name: "-w ${escapeShellArg name}") cfg.whitelist));
+        in "${pkgs.sshguard}/bin/sshguard ${args}";
+        Restart = "always";
+        ProtectSystem = "strict";
+        ProtectHome = "tmpfs";
+        RuntimeDirectory = "sshguard";
+        StateDirectory = "sshguard";
+        CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/step-ca.nix b/nixos/modules/services/security/step-ca.nix
new file mode 100644
index 00000000000..95183078d7b
--- /dev/null
+++ b/nixos/modules/services/security/step-ca.nix
@@ -0,0 +1,146 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.step-ca;
+  settingsFormat = (pkgs.formats.json { });
+in
+{
+  meta.maintainers = with lib.maintainers; [ mohe2015 ];
+
+  options = {
+    services.step-ca = {
+      enable = lib.mkEnableOption "the smallstep certificate authority server";
+      openFirewall = lib.mkEnableOption "opening the certificate authority server port";
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.step-ca;
+        defaultText = lib.literalExpression "pkgs.step-ca";
+        description = "Which step-ca package to use.";
+      };
+      address = lib.mkOption {
+        type = lib.types.str;
+        example = "127.0.0.1";
+        description = ''
+          The address (without port) the certificate authority should listen at.
+          This combined with <option>services.step-ca.port</option> overrides <option>services.step-ca.settings.address</option>.
+        '';
+      };
+      port = lib.mkOption {
+        type = lib.types.port;
+        example = 8443;
+        description = ''
+          The port the certificate authority should listen on.
+          This combined with <option>services.step-ca.address</option> overrides <option>services.step-ca.settings.address</option>.
+        '';
+      };
+      settings = lib.mkOption {
+        type = with lib.types; attrsOf anything;
+        description = ''
+          Settings that go into <filename>ca.json</filename>. See
+          <link xlink:href="https://smallstep.com/docs/step-ca/configuration">
+          the step-ca manual</link> for more information. The easiest way to
+          configure this module would be to run <literal>step ca init</literal>
+          to generate <filename>ca.json</filename> and then import it using
+          <literal>builtins.fromJSON</literal>.
+          <link xlink:href="https://smallstep.com/docs/step-cli/basic-crypto-operations#run-an-offline-x509-certificate-authority">This article</link>
+          may also be useful if you want to customize certain aspects of
+          certificate generation for your CA.
+          You need to change the database storage path to <filename>/var/lib/step-ca/db</filename>.
+
+          <warning>
+            <para>
+              The <option>services.step-ca.settings.address</option> option
+              will be ignored and overwritten by
+              <option>services.step-ca.address</option> and
+              <option>services.step-ca.port</option>.
+            </para>
+          </warning>
+        '';
+      };
+      intermediatePasswordFile = lib.mkOption {
+        type = lib.types.path;
+        example = "/run/keys/smallstep-password";
+        description = ''
+          Path to the file containing the password for the intermediate
+          certificate private key.
+
+          <warning>
+            <para>
+              Make sure to use a quoted absolute path instead of a path literal
+              to prevent it from being copied to the globally readable Nix
+              store.
+            </para>
+          </warning>
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf config.services.step-ca.enable (
+    let
+      configFile = settingsFormat.generate "ca.json" (cfg.settings // {
+        address = cfg.address + ":" + toString cfg.port;
+      });
+    in
+    {
+      assertions =
+        [
+          {
+            assertion = !lib.isStorePath cfg.intermediatePasswordFile;
+            message = ''
+              <option>services.step-ca.intermediatePasswordFile</option> points to
+              a file in the Nix store. You should use a quoted absolute path to
+              prevent this.
+            '';
+          }
+        ];
+
+      systemd.packages = [ cfg.package ];
+
+      # configuration file indirection is needed to support reloading
+      environment.etc."smallstep/ca.json".source = configFile;
+
+      systemd.services."step-ca" = {
+        wantedBy = [ "multi-user.target" ];
+        restartTriggers = [ configFile ];
+        unitConfig = {
+          ConditionFileNotEmpty = ""; # override upstream
+        };
+        serviceConfig = {
+          User = "step-ca";
+          Group = "step-ca";
+          UMask = "0077";
+          Environment = "HOME=%S/step-ca";
+          WorkingDirectory = ""; # override upstream
+          ReadWriteDirectories = ""; # override upstream
+
+          # LocalCredential handles file permission problems arising from the use of DynamicUser.
+          LoadCredential = "intermediate_password:${cfg.intermediatePasswordFile}";
+
+          ExecStart = [
+            "" # override upstream
+            "${cfg.package}/bin/step-ca /etc/smallstep/ca.json --password-file \${CREDENTIALS_DIRECTORY}/intermediate_password"
+          ];
+
+          # ProtectProc = "invisible"; # not supported by upstream yet
+          # ProcSubset = "pid"; # not supported by upstream yet
+          # PrivateUsers = true; # doesn't work with privileged ports therefore not supported by upstream
+
+          DynamicUser = true;
+          StateDirectory = "step-ca";
+        };
+      };
+
+      users.users.step-ca = {
+        home = "/var/lib/step-ca";
+        group = "step-ca";
+        isSystemUser = true;
+      };
+
+      users.groups.step-ca = {};
+
+      networking.firewall = lib.mkIf cfg.openFirewall {
+        allowedTCPPorts = [ cfg.port ];
+      };
+    }
+  );
+}
diff --git a/nixos/modules/services/security/tor.nix b/nixos/modules/services/security/tor.nix
new file mode 100644
index 00000000000..a5822c02794
--- /dev/null
+++ b/nixos/modules/services/security/tor.nix
@@ -0,0 +1,1067 @@
+{ config, lib, options, pkgs, ... }:
+
+with builtins;
+with lib;
+
+let
+  cfg = config.services.tor;
+  opt = options.services.tor;
+  stateDir = "/var/lib/tor";
+  runDir = "/run/tor";
+  descriptionGeneric = option: ''
+    See <link xlink:href="https://2019.www.torproject.org/docs/tor-manual.html.en#${option}">torrc manual</link>.
+  '';
+  bindsPrivilegedPort =
+    any (p0:
+      let p1 = if p0 ? "port" then p0.port else p0; in
+      if p1 == "auto" then false
+      else let p2 = if isInt p1 then p1 else toInt p1; in
+        p1 != null && 0 < p2 && p2 < 1024)
+    (flatten [
+      cfg.settings.ORPort
+      cfg.settings.DirPort
+      cfg.settings.DNSPort
+      cfg.settings.ExtORPort
+      cfg.settings.HTTPTunnelPort
+      cfg.settings.NATDPort
+      cfg.settings.SOCKSPort
+      cfg.settings.TransPort
+    ]);
+  optionBool = optionName: mkOption {
+    type = with types; nullOr bool;
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionInt = optionName: mkOption {
+    type = with types; nullOr int;
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionString = optionName: mkOption {
+    type = with types; nullOr str;
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionStrings = optionName: mkOption {
+    type = with types; listOf str;
+    default = [];
+    description = descriptionGeneric optionName;
+  };
+  optionAddress = mkOption {
+    type = with types; nullOr str;
+    default = null;
+    example = "0.0.0.0";
+    description = ''
+      IPv4 or IPv6 (if between brackets) address.
+    '';
+  };
+  optionUnix = mkOption {
+    type = with types; nullOr path;
+    default = null;
+    description = ''
+      Unix domain socket path to use.
+    '';
+  };
+  optionPort = mkOption {
+    type = with types; nullOr (oneOf [port (enum ["auto"])]);
+    default = null;
+  };
+  optionPorts = optionName: mkOption {
+    type = with types; listOf port;
+    default = [];
+    description = descriptionGeneric optionName;
+  };
+  optionIsolablePort = with types; oneOf [
+    port (enum ["auto"])
+    (submodule ({config, ...}: {
+      options = {
+        addr = optionAddress;
+        port = optionPort;
+        flags = optionFlags;
+        SessionGroup = mkOption { type = nullOr int; default = null; };
+      } // genAttrs isolateFlags (name: mkOption { type = types.bool; default = false; });
+      config = {
+        flags = filter (name: config.${name} == true) isolateFlags ++
+                optional (config.SessionGroup != null) "SessionGroup=${toString config.SessionGroup}";
+      };
+    }))
+  ];
+  optionIsolablePorts = optionName: mkOption {
+    default = [];
+    type = with types; either optionIsolablePort (listOf optionIsolablePort);
+    description = descriptionGeneric optionName;
+  };
+  isolateFlags = [
+    "IsolateClientAddr"
+    "IsolateClientProtocol"
+    "IsolateDestAddr"
+    "IsolateDestPort"
+    "IsolateSOCKSAuth"
+    "KeepAliveIsolateSOCKSAuth"
+  ];
+  optionSOCKSPort = doConfig: let
+    flags = [
+      "CacheDNS" "CacheIPv4DNS" "CacheIPv6DNS" "GroupWritable" "IPv6Traffic"
+      "NoDNSRequest" "NoIPv4Traffic" "NoOnionTraffic" "OnionTrafficOnly"
+      "PreferIPv6" "PreferIPv6Automap" "PreferSOCKSNoAuth" "UseDNSCache"
+      "UseIPv4Cache" "UseIPv6Cache" "WorldWritable"
+    ] ++ isolateFlags;
+    in with types; oneOf [
+      port (submodule ({config, ...}: {
+        options = {
+          unix = optionUnix;
+          addr = optionAddress;
+          port = optionPort;
+          flags = optionFlags;
+          SessionGroup = mkOption { type = nullOr int; default = null; };
+        } // genAttrs flags (name: mkOption { type = types.bool; default = false; });
+        config = mkIf doConfig { # Only add flags in SOCKSPort to avoid duplicates
+          flags = filter (name: config.${name} == true) flags ++
+                  optional (config.SessionGroup != null) "SessionGroup=${toString config.SessionGroup}";
+        };
+      }))
+    ];
+  optionFlags = mkOption {
+    type = with types; listOf str;
+    default = [];
+  };
+  optionORPort = optionName: mkOption {
+    default = [];
+    example = 443;
+    type = with types; oneOf [port (enum ["auto"]) (listOf (oneOf [
+      port
+      (enum ["auto"])
+      (submodule ({config, ...}:
+        let flags = [ "IPv4Only" "IPv6Only" "NoAdvertise" "NoListen" ];
+        in {
+        options = {
+          addr = optionAddress;
+          port = optionPort;
+          flags = optionFlags;
+        } // genAttrs flags (name: mkOption { type = types.bool; default = false; });
+        config = {
+          flags = filter (name: config.${name} == true) flags;
+        };
+      }))
+    ]))];
+    description = descriptionGeneric optionName;
+  };
+  optionBandwith = optionName: mkOption {
+    type = with types; nullOr (either int str);
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionPath = optionName: mkOption {
+    type = with types; nullOr path;
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+
+  mkValueString = k: v:
+    if v == null then ""
+    else if isBool v then
+      (if v then "1" else "0")
+    else if v ? "unix" && v.unix != null then
+      "unix:"+v.unix +
+      optionalString (v ? "flags") (" " + concatStringsSep " " v.flags)
+    else if v ? "port" && v.port != null then
+      optionalString (v ? "addr" && v.addr != null) "${v.addr}:" +
+      toString v.port +
+      optionalString (v ? "flags") (" " + concatStringsSep " " v.flags)
+    else if k == "ServerTransportPlugin" then
+      optionalString (v.transports != []) "${concatStringsSep "," v.transports} exec ${v.exec}"
+    else if k == "HidServAuth" then
+      v.onion + " " + v.auth
+    else generators.mkValueStringDefault {} v;
+  genTorrc = settings:
+    generators.toKeyValue {
+      listsAsDuplicateKeys = true;
+      mkKeyValue = k: generators.mkKeyValueDefault { mkValueString = mkValueString k; } " " k;
+    }
+    (lib.mapAttrs (k: v:
+      # Not necesssary, but prettier rendering
+      if elem k [ "AutomapHostsSuffixes" "DirPolicy" "ExitPolicy" "SocksPolicy" ]
+      && v != []
+      then concatStringsSep "," v
+      else v)
+    (lib.filterAttrs (k: v: !(v == null || v == ""))
+    settings));
+  torrc = pkgs.writeText "torrc" (
+    genTorrc cfg.settings +
+    concatStrings (mapAttrsToList (name: onion:
+      "HiddenServiceDir ${onion.path}\n" +
+      genTorrc onion.settings) cfg.relay.onionServices)
+  );
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "tor" "client" "dns" "automapHostsSuffixes" ] [ "services" "tor" "settings" "AutomapHostsSuffixes" ])
+    (mkRemovedOptionModule [ "services" "tor" "client" "dns" "isolationOptions" ] "Use services.tor.settings.DNSPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "dns" "listenAddress" ] "Use services.tor.settings.DNSPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "privoxy" "enable" ] "Use services.privoxy.enable and services.privoxy.enableTor instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "socksIsolationOptions" ] "Use services.tor.settings.SOCKSPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "socksListenAddressFaster" ] "Use services.tor.settings.SOCKSPort instead.")
+    (mkRenamedOptionModule [ "services" "tor" "client" "socksPolicy" ] [ "services" "tor" "settings" "SocksPolicy" ])
+    (mkRemovedOptionModule [ "services" "tor" "client" "transparentProxy" "isolationOptions" ] "Use services.tor.settings.TransPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "transparentProxy" "listenAddress" ] "Use services.tor.settings.TransPort instead.")
+    (mkRenamedOptionModule [ "services" "tor" "controlPort" ] [ "services" "tor" "settings" "ControlPort" ])
+    (mkRemovedOptionModule [ "services" "tor" "extraConfig" ] "Plese use services.tor.settings instead.")
+    (mkRenamedOptionModule [ "services" "tor" "hiddenServices" ] [ "services" "tor" "relay" "onionServices" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "accountingMax" ] [ "services" "tor" "settings" "AccountingMax" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "accountingStart" ] [ "services" "tor" "settings" "AccountingStart" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "address" ] [ "services" "tor" "settings" "Address" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "bandwidthBurst" ] [ "services" "tor" "settings" "BandwidthBurst" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "bandwidthRate" ] [ "services" "tor" "settings" "BandwidthRate" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "bridgeTransports" ] [ "services" "tor" "settings" "ServerTransportPlugin" "transports" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "contactInfo" ] [ "services" "tor" "settings" "ContactInfo" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "exitPolicy" ] [ "services" "tor" "settings" "ExitPolicy" ])
+    (mkRemovedOptionModule [ "services" "tor" "relay" "isBridge" ] "Use services.tor.relay.role instead.")
+    (mkRemovedOptionModule [ "services" "tor" "relay" "isExit" ] "Use services.tor.relay.role instead.")
+    (mkRenamedOptionModule [ "services" "tor" "relay" "nickname" ] [ "services" "tor" "settings" "Nickname" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "port" ] [ "services" "tor" "settings" "ORPort" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "portSpec" ] [ "services" "tor" "settings" "ORPort" ])
+  ];
+
+  options = {
+    services.tor = {
+      enable = mkEnableOption ''Tor daemon.
+        By default, the daemon is run without
+        relay, exit, bridge or client connectivity'';
+
+      openFirewall = mkEnableOption "opening of the relay port(s) in the firewall";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.tor;
+        defaultText = literalExpression "pkgs.tor";
+        description = "Tor package to use.";
+      };
+
+      enableGeoIP = mkEnableOption ''use of GeoIP databases.
+        Disabling this will disable by-country statistics for bridges and relays
+        and some client and third-party software functionality'' // { default = true; };
+
+      controlSocket.enable = mkEnableOption ''control socket,
+        created in <literal>${runDir}/control</literal>'';
+
+      client = {
+        enable = mkEnableOption ''the routing of application connections.
+          You might want to disable this if you plan running a dedicated Tor relay'';
+
+        transparentProxy.enable = mkEnableOption "transparent proxy";
+        dns.enable = mkEnableOption "DNS resolver";
+
+        socksListenAddress = mkOption {
+          type = optionSOCKSPort false;
+          default = {addr = "127.0.0.1"; port = 9050; IsolateDestAddr = true;};
+          example = {addr = "192.168.0.1"; port = 9090; IsolateDestAddr = true;};
+          description = ''
+            Bind to this address to listen for connections from
+            Socks-speaking applications.
+          '';
+        };
+
+        onionServices = mkOption {
+          description = descriptionGeneric "HiddenServiceDir";
+          default = {};
+          example = {
+            "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" = {
+              clientAuthorizations = ["/run/keys/tor/alice.prv.x25519"];
+            };
+          };
+          type = types.attrsOf (types.submodule ({name, config, ...}: {
+            options.clientAuthorizations = mkOption {
+              description = ''
+                Clients' authorizations for a v3 onion service,
+                as a list of files containing each one private key, in the format:
+                <screen>descriptor:x25519:&lt;base32-private-key&gt;</screen>
+              '' + descriptionGeneric "_client_authorization";
+              type = with types; listOf path;
+              default = [];
+              example = ["/run/keys/tor/alice.prv.x25519"];
+            };
+          }));
+        };
+      };
+
+      relay = {
+        enable = mkEnableOption ''relaying of Tor traffic for others.
+
+          See <link xlink:href="https://www.torproject.org/docs/tor-doc-relay" />
+          for details.
+
+          Setting this to true requires setting
+          <option>services.tor.relay.role</option>
+          and
+          <option>services.tor.settings.ORPort</option>
+          options'';
+
+        role = mkOption {
+          type = types.enum [ "exit" "relay" "bridge" "private-bridge" ];
+          description = ''
+            Your role in Tor network. There're several options:
+
+            <variablelist>
+            <varlistentry>
+              <term><literal>exit</literal></term>
+              <listitem>
+                <para>
+                  An exit relay. This allows Tor users to access regular
+                  Internet services through your public IP.
+                </para>
+
+                <important><para>
+                  Running an exit relay may expose you to abuse
+                  complaints. See
+                  <link xlink:href="https://www.torproject.org/faq.html.en#ExitPolicies"/>
+                  for more info.
+                </para></important>
+
+                <para>
+                  You can specify which services Tor users may access via
+                  your exit relay using <option>settings.ExitPolicy</option> option.
+                </para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><literal>relay</literal></term>
+              <listitem>
+                <para>
+                  Regular relay. This allows Tor users to relay onion
+                  traffic to other Tor nodes, but not to public
+                  Internet.
+                </para>
+
+                <important><para>
+                  Note that some misconfigured and/or disrespectful
+                  towards privacy sites will block you even if your
+                  relay is not an exit relay. That is, just being listed
+                  in a public relay directory can have unwanted
+                  consequences.
+
+                  Which means you might not want to use
+                  this role if you browse public Internet from the same
+                  network as your relay, unless you want to write
+                  e-mails to those sites (you should!).
+                </para></important>
+
+                <para>
+                  See
+                  <link xlink:href="https://www.torproject.org/docs/tor-doc-relay.html.en" />
+                  for more info.
+                </para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><literal>bridge</literal></term>
+              <listitem>
+                <para>
+                  Regular bridge. Works like a regular relay, but
+                  doesn't list you in the public relay directory and
+                  hides your Tor node behind obfs4proxy.
+                </para>
+
+                <para>
+                  Using this option will make Tor advertise your bridge
+                  to users through various mechanisms like
+                  <link xlink:href="https://bridges.torproject.org/" />, though.
+                </para>
+
+                <important>
+                  <para>
+                    WARNING: THE FOLLOWING PARAGRAPH IS NOT LEGAL ADVICE.
+                    Consult with your lawyer when in doubt.
+                  </para>
+
+                  <para>
+                    This role should be safe to use in most situations
+                    (unless the act of forwarding traffic for others is
+                    a punishable offence under your local laws, which
+                    would be pretty insane as it would make ISP illegal).
+                  </para>
+                </important>
+
+                <para>
+                  See <link xlink:href="https://www.torproject.org/docs/bridges.html.en" />
+                  for more info.
+                </para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><literal>private-bridge</literal></term>
+              <listitem>
+                <para>
+                  Private bridge. Works like regular bridge, but does
+                  not advertise your node in any way.
+                </para>
+
+                <para>
+                  Using this role means that you won't contribute to Tor
+                  network in any way unless you advertise your node
+                  yourself in some way.
+                </para>
+
+                <para>
+                  Use this if you want to run a private bridge, for
+                  example because you'll give out your bridge addr
+                  manually to your friends.
+                </para>
+
+                <para>
+                  Switching to this role after measurable time in
+                  "bridge" role is pretty useless as some Tor users
+                  would have learned about your node already. In the
+                  latter case you can still change
+                  <option>port</option> option.
+                </para>
+
+                <para>
+                  See <link xlink:href="https://www.torproject.org/docs/bridges.html.en" />
+                  for more info.
+                </para>
+              </listitem>
+            </varlistentry>
+            </variablelist>
+          '';
+        };
+
+        onionServices = mkOption {
+          description = descriptionGeneric "HiddenServiceDir";
+          default = {};
+          example = {
+            "example.org/www" = {
+              map = [ 80 ];
+              authorizedClients = [
+                "descriptor:x25519:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
+              ];
+            };
+          };
+          type = types.attrsOf (types.submodule ({name, config, ...}: {
+            options.path = mkOption {
+              type = types.path;
+              description = ''
+                Path where to store the data files of the hidden service.
+                If the <option>secretKey</option> is null
+                this defaults to <literal>${stateDir}/onion/$onion</literal>,
+                otherwise to <literal>${runDir}/onion/$onion</literal>.
+              '';
+            };
+            options.secretKey = mkOption {
+              type = with types; nullOr path;
+              default = null;
+              example = "/run/keys/tor/onion/expyuzz4wqqyqhjn/hs_ed25519_secret_key";
+              description = ''
+                Secret key of the onion service.
+                If null, Tor reuses any preexisting secret key (in <option>path</option>)
+                or generates a new one.
+                The associated public key and hostname are deterministically regenerated
+                from this file if they do not exist.
+              '';
+            };
+            options.authorizeClient = mkOption {
+              description = descriptionGeneric "HiddenServiceAuthorizeClient";
+              default = null;
+              type = types.nullOr (types.submodule ({...}: {
+                options = {
+                  authType = mkOption {
+                    type = types.enum [ "basic" "stealth" ];
+                    description = ''
+                      Either <literal>"basic"</literal> for a general-purpose authorization protocol
+                      or <literal>"stealth"</literal> for a less scalable protocol
+                      that also hides service activity from unauthorized clients.
+                    '';
+                  };
+                  clientNames = mkOption {
+                    type = with types; nonEmptyListOf (strMatching "[A-Za-z0-9+-_]+");
+                    description = ''
+                      Only clients that are listed here are authorized to access the hidden service.
+                      Generated authorization data can be found in <filename>${stateDir}/onion/$name/hostname</filename>.
+                      Clients need to put this authorization data in their configuration file using
+                      <xref linkend="opt-services.tor.settings.HidServAuth"/>.
+                    '';
+                  };
+                };
+              }));
+            };
+            options.authorizedClients = mkOption {
+              description = ''
+                Authorized clients for a v3 onion service,
+                as a list of public key, in the format:
+                <screen>descriptor:x25519:&lt;base32-public-key&gt;</screen>
+              '' + descriptionGeneric "_client_authorization";
+              type = with types; listOf str;
+              default = [];
+              example = ["descriptor:x25519:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"];
+            };
+            options.map = mkOption {
+              description = descriptionGeneric "HiddenServicePort";
+              type = with types; listOf (oneOf [
+                port (submodule ({...}: {
+                  options = {
+                    port = optionPort;
+                    target = mkOption {
+                      default = null;
+                      type = nullOr (submodule ({...}: {
+                        options = {
+                          unix = optionUnix;
+                          addr = optionAddress;
+                          port = optionPort;
+                        };
+                      }));
+                    };
+                  };
+                }))
+              ]);
+              apply = map (v: if isInt v then {port=v; target=null;} else v);
+            };
+            options.version = mkOption {
+              description = descriptionGeneric "HiddenServiceVersion";
+              type = with types; nullOr (enum [2 3]);
+              default = null;
+            };
+            options.settings = mkOption {
+              description = ''
+                Settings of the onion service.
+              '' + descriptionGeneric "_hidden_service_options";
+              default = {};
+              type = types.submodule {
+                freeformType = with types;
+                  (attrsOf (nullOr (oneOf [str int bool (listOf str)]))) // {
+                    description = "settings option";
+                  };
+                options.HiddenServiceAllowUnknownPorts = optionBool "HiddenServiceAllowUnknownPorts";
+                options.HiddenServiceDirGroupReadable = optionBool "HiddenServiceDirGroupReadable";
+                options.HiddenServiceExportCircuitID = mkOption {
+                  description = descriptionGeneric "HiddenServiceExportCircuitID";
+                  type = with types; nullOr (enum ["haproxy"]);
+                  default = null;
+                };
+                options.HiddenServiceMaxStreams = mkOption {
+                  description = descriptionGeneric "HiddenServiceMaxStreams";
+                  type = with types; nullOr (ints.between 0 65535);
+                  default = null;
+                };
+                options.HiddenServiceMaxStreamsCloseCircuit = optionBool "HiddenServiceMaxStreamsCloseCircuit";
+                options.HiddenServiceNumIntroductionPoints = mkOption {
+                  description = descriptionGeneric "HiddenServiceNumIntroductionPoints";
+                  type = with types; nullOr (ints.between 0 20);
+                  default = null;
+                };
+                options.HiddenServiceSingleHopMode = optionBool "HiddenServiceSingleHopMode";
+                options.RendPostPeriod = optionString "RendPostPeriod";
+              };
+            };
+            config = {
+              path = mkDefault ((if config.secretKey == null then stateDir else runDir) + "/onion/${name}");
+              settings.HiddenServiceVersion = config.version;
+              settings.HiddenServiceAuthorizeClient =
+                if config.authorizeClient != null then
+                  config.authorizeClient.authType + " " +
+                  concatStringsSep "," config.authorizeClient.clientNames
+                else null;
+              settings.HiddenServicePort = map (p: mkValueString "" p.port + " " + mkValueString "" p.target) config.map;
+            };
+          }));
+        };
+      };
+
+      settings = mkOption {
+        description = ''
+          See <link xlink:href="https://2019.www.torproject.org/docs/tor-manual.html.en">torrc manual</link>
+          for documentation.
+        '';
+        default = {};
+        type = types.submodule {
+          freeformType = with types;
+            (attrsOf (nullOr (oneOf [str int bool (listOf str)]))) // {
+              description = "settings option";
+            };
+          options.Address = optionString "Address";
+          options.AssumeReachable = optionBool "AssumeReachable";
+          options.AccountingMax = optionBandwith "AccountingMax";
+          options.AccountingStart = optionString "AccountingStart";
+          options.AuthDirHasIPv6Connectivity = optionBool "AuthDirHasIPv6Connectivity";
+          options.AuthDirListBadExits = optionBool "AuthDirListBadExits";
+          options.AuthDirPinKeys = optionBool "AuthDirPinKeys";
+          options.AuthDirSharedRandomness = optionBool "AuthDirSharedRandomness";
+          options.AuthDirTestEd25519LinkKeys = optionBool "AuthDirTestEd25519LinkKeys";
+          options.AuthoritativeDirectory = optionBool "AuthoritativeDirectory";
+          options.AutomapHostsOnResolve = optionBool "AutomapHostsOnResolve";
+          options.AutomapHostsSuffixes = optionStrings "AutomapHostsSuffixes" // {
+            default = [".onion" ".exit"];
+            example = [".onion"];
+          };
+          options.BandwidthBurst = optionBandwith "BandwidthBurst";
+          options.BandwidthRate = optionBandwith "BandwidthRate";
+          options.BridgeAuthoritativeDir = optionBool "BridgeAuthoritativeDir";
+          options.BridgeRecordUsageByCountry = optionBool "BridgeRecordUsageByCountry";
+          options.BridgeRelay = optionBool "BridgeRelay" // { default = false; };
+          options.CacheDirectory = optionPath "CacheDirectory";
+          options.CacheDirectoryGroupReadable = optionBool "CacheDirectoryGroupReadable"; # default is null and like "auto"
+          options.CellStatistics = optionBool "CellStatistics";
+          options.ClientAutoIPv6ORPort = optionBool "ClientAutoIPv6ORPort";
+          options.ClientDNSRejectInternalAddresses = optionBool "ClientDNSRejectInternalAddresses";
+          options.ClientOnionAuthDir = mkOption {
+            description = descriptionGeneric "ClientOnionAuthDir";
+            default = null;
+            type = with types; nullOr path;
+          };
+          options.ClientPreferIPv6DirPort = optionBool "ClientPreferIPv6DirPort"; # default is null and like "auto"
+          options.ClientPreferIPv6ORPort = optionBool "ClientPreferIPv6ORPort"; # default is null and like "auto"
+          options.ClientRejectInternalAddresses = optionBool "ClientRejectInternalAddresses";
+          options.ClientUseIPv4 = optionBool "ClientUseIPv4";
+          options.ClientUseIPv6 = optionBool "ClientUseIPv6";
+          options.ConnDirectionStatistics = optionBool "ConnDirectionStatistics";
+          options.ConstrainedSockets = optionBool "ConstrainedSockets";
+          options.ContactInfo = optionString "ContactInfo";
+          options.ControlPort = mkOption rec {
+            description = descriptionGeneric "ControlPort";
+            default = [];
+            example = [{port = 9051;}];
+            type = with types; oneOf [port (enum ["auto"]) (listOf (oneOf [
+              port (enum ["auto"]) (submodule ({config, ...}: let
+                flags = ["GroupWritable" "RelaxDirModeCheck" "WorldWritable"];
+                in {
+                options = {
+                  unix = optionUnix;
+                  flags = optionFlags;
+                  addr = optionAddress;
+                  port = optionPort;
+                } // genAttrs flags (name: mkOption { type = types.bool; default = false; });
+                config = {
+                  flags = filter (name: config.${name} == true) flags;
+                };
+              }))
+            ]))];
+          };
+          options.ControlPortFileGroupReadable= optionBool "ControlPortFileGroupReadable";
+          options.ControlPortWriteToFile = optionPath "ControlPortWriteToFile";
+          options.ControlSocket = optionPath "ControlSocket";
+          options.ControlSocketsGroupWritable = optionBool "ControlSocketsGroupWritable";
+          options.CookieAuthFile = optionPath "CookieAuthFile";
+          options.CookieAuthFileGroupReadable = optionBool "CookieAuthFileGroupReadable";
+          options.CookieAuthentication = optionBool "CookieAuthentication";
+          options.DataDirectory = optionPath "DataDirectory" // { default = stateDir; };
+          options.DataDirectoryGroupReadable = optionBool "DataDirectoryGroupReadable";
+          options.DirPortFrontPage = optionPath "DirPortFrontPage";
+          options.DirAllowPrivateAddresses = optionBool "DirAllowPrivateAddresses";
+          options.DormantCanceledByStartup = optionBool "DormantCanceledByStartup";
+          options.DormantOnFirstStartup = optionBool "DormantOnFirstStartup";
+          options.DormantTimeoutDisabledByIdleStreams = optionBool "DormantTimeoutDisabledByIdleStreams";
+          options.DirCache = optionBool "DirCache";
+          options.DirPolicy = mkOption {
+            description = descriptionGeneric "DirPolicy";
+            type = with types; listOf str;
+            default = [];
+            example = ["accept *:*"];
+          };
+          options.DirPort = optionORPort "DirPort";
+          options.DirReqStatistics = optionBool "DirReqStatistics";
+          options.DisableAllSwap = optionBool "DisableAllSwap";
+          options.DisableDebuggerAttachment = optionBool "DisableDebuggerAttachment";
+          options.DisableNetwork = optionBool "DisableNetwork";
+          options.DisableOOSCheck = optionBool "DisableOOSCheck";
+          options.DNSPort = optionIsolablePorts "DNSPort";
+          options.DoSCircuitCreationEnabled = optionBool "DoSCircuitCreationEnabled";
+          options.DoSConnectionEnabled = optionBool "DoSConnectionEnabled"; # default is null and like "auto"
+          options.DoSRefuseSingleHopClientRendezvous = optionBool "DoSRefuseSingleHopClientRendezvous";
+          options.DownloadExtraInfo = optionBool "DownloadExtraInfo";
+          options.EnforceDistinctSubnets = optionBool "EnforceDistinctSubnets";
+          options.EntryStatistics = optionBool "EntryStatistics";
+          options.ExitPolicy = optionStrings "ExitPolicy" // {
+            default = ["reject *:*"];
+            example = ["accept *:*"];
+          };
+          options.ExitPolicyRejectLocalInterfaces = optionBool "ExitPolicyRejectLocalInterfaces";
+          options.ExitPolicyRejectPrivate = optionBool "ExitPolicyRejectPrivate";
+          options.ExitPortStatistics = optionBool "ExitPortStatistics";
+          options.ExitRelay = optionBool "ExitRelay"; # default is null and like "auto"
+          options.ExtORPort = mkOption {
+            description = descriptionGeneric "ExtORPort";
+            default = null;
+            type = with types; nullOr (oneOf [
+              port (enum ["auto"]) (submodule ({...}: {
+                options = {
+                  addr = optionAddress;
+                  port = optionPort;
+                };
+              }))
+            ]);
+            apply = p: if isInt p || isString p then { port = p; } else p;
+          };
+          options.ExtORPortCookieAuthFile = optionPath "ExtORPortCookieAuthFile";
+          options.ExtORPortCookieAuthFileGroupReadable = optionBool "ExtORPortCookieAuthFileGroupReadable";
+          options.ExtendAllowPrivateAddresses = optionBool "ExtendAllowPrivateAddresses";
+          options.ExtraInfoStatistics = optionBool "ExtraInfoStatistics";
+          options.FascistFirewall = optionBool "FascistFirewall";
+          options.FetchDirInfoEarly = optionBool "FetchDirInfoEarly";
+          options.FetchDirInfoExtraEarly = optionBool "FetchDirInfoExtraEarly";
+          options.FetchHidServDescriptors = optionBool "FetchHidServDescriptors";
+          options.FetchServerDescriptors = optionBool "FetchServerDescriptors";
+          options.FetchUselessDescriptors = optionBool "FetchUselessDescriptors";
+          options.ReachableAddresses = optionStrings "ReachableAddresses";
+          options.ReachableDirAddresses = optionStrings "ReachableDirAddresses";
+          options.ReachableORAddresses = optionStrings "ReachableORAddresses";
+          options.GeoIPFile = optionPath "GeoIPFile";
+          options.GeoIPv6File = optionPath "GeoIPv6File";
+          options.GuardfractionFile = optionPath "GuardfractionFile";
+          options.HidServAuth = mkOption {
+            description = descriptionGeneric "HidServAuth";
+            default = [];
+            type = with types; listOf (oneOf [
+              (submodule {
+                options = {
+                  onion = mkOption {
+                    type = strMatching "[a-z2-7]{16}\\.onion";
+                    description = "Onion address.";
+                    example = "xxxxxxxxxxxxxxxx.onion";
+                  };
+                  auth = mkOption {
+                    type = strMatching "[A-Za-z0-9+/]{22}";
+                    description = "Authentication cookie.";
+                  };
+                };
+              })
+            ]);
+            example = [
+              {
+                onion = "xxxxxxxxxxxxxxxx.onion";
+                auth = "xxxxxxxxxxxxxxxxxxxxxx";
+              }
+            ];
+          };
+          options.HiddenServiceNonAnonymousMode = optionBool "HiddenServiceNonAnonymousMode";
+          options.HiddenServiceStatistics = optionBool "HiddenServiceStatistics";
+          options.HSLayer2Nodes = optionStrings "HSLayer2Nodes";
+          options.HSLayer3Nodes = optionStrings "HSLayer3Nodes";
+          options.HTTPTunnelPort = optionIsolablePorts "HTTPTunnelPort";
+          options.IPv6Exit = optionBool "IPv6Exit";
+          options.KeyDirectory = optionPath "KeyDirectory";
+          options.KeyDirectoryGroupReadable = optionBool "KeyDirectoryGroupReadable";
+          options.LogMessageDomains = optionBool "LogMessageDomains";
+          options.LongLivedPorts = optionPorts "LongLivedPorts";
+          options.MainloopStats = optionBool "MainloopStats";
+          options.MaxAdvertisedBandwidth = optionBandwith "MaxAdvertisedBandwidth";
+          options.MaxCircuitDirtiness = optionInt "MaxCircuitDirtiness";
+          options.MaxClientCircuitsPending = optionInt "MaxClientCircuitsPending";
+          options.NATDPort = optionIsolablePorts "NATDPort";
+          options.NewCircuitPeriod = optionInt "NewCircuitPeriod";
+          options.Nickname = optionString "Nickname";
+          options.ORPort = optionORPort "ORPort";
+          options.OfflineMasterKey = optionBool "OfflineMasterKey";
+          options.OptimisticData = optionBool "OptimisticData"; # default is null and like "auto"
+          options.PaddingStatistics = optionBool "PaddingStatistics";
+          options.PerConnBWBurst = optionBandwith "PerConnBWBurst";
+          options.PerConnBWRate = optionBandwith "PerConnBWRate";
+          options.PidFile = optionPath "PidFile";
+          options.ProtocolWarnings = optionBool "ProtocolWarnings";
+          options.PublishHidServDescriptors = optionBool "PublishHidServDescriptors";
+          options.PublishServerDescriptor = mkOption {
+            description = descriptionGeneric "PublishServerDescriptor";
+            type = with types; nullOr (enum [false true 0 1 "0" "1" "v3" "bridge"]);
+            default = null;
+          };
+          options.ReducedExitPolicy = optionBool "ReducedExitPolicy";
+          options.RefuseUnknownExits = optionBool "RefuseUnknownExits"; # default is null and like "auto"
+          options.RejectPlaintextPorts = optionPorts "RejectPlaintextPorts";
+          options.RelayBandwidthBurst = optionBandwith "RelayBandwidthBurst";
+          options.RelayBandwidthRate = optionBandwith "RelayBandwidthRate";
+          #options.RunAsDaemon
+          options.Sandbox = optionBool "Sandbox";
+          options.ServerDNSAllowBrokenConfig = optionBool "ServerDNSAllowBrokenConfig";
+          options.ServerDNSAllowNonRFC953Hostnames = optionBool "ServerDNSAllowNonRFC953Hostnames";
+          options.ServerDNSDetectHijacking = optionBool "ServerDNSDetectHijacking";
+          options.ServerDNSRandomizeCase = optionBool "ServerDNSRandomizeCase";
+          options.ServerDNSResolvConfFile = optionPath "ServerDNSResolvConfFile";
+          options.ServerDNSSearchDomains = optionBool "ServerDNSSearchDomains";
+          options.ServerTransportPlugin = mkOption {
+            description = descriptionGeneric "ServerTransportPlugin";
+            default = null;
+            type = with types; nullOr (submodule ({...}: {
+              options = {
+                transports = mkOption {
+                  description = "List of pluggable transports.";
+                  type = listOf str;
+                  example = ["obfs2" "obfs3" "obfs4" "scramblesuit"];
+                };
+                exec = mkOption {
+                  type = types.str;
+                  description = "Command of pluggable transport.";
+                };
+              };
+            }));
+          };
+          options.ShutdownWaitLength = mkOption {
+            type = types.int;
+            default = 30;
+            description = descriptionGeneric "ShutdownWaitLength";
+          };
+          options.SocksPolicy = optionStrings "SocksPolicy" // {
+            example = ["accept *:*"];
+          };
+          options.SOCKSPort = mkOption {
+            description = descriptionGeneric "SOCKSPort";
+            default = if cfg.settings.HiddenServiceNonAnonymousMode == true then [{port = 0;}] else [];
+            defaultText = literalExpression ''
+              if config.${opt.settings}.HiddenServiceNonAnonymousMode == true
+              then [ { port = 0; } ]
+              else [ ]
+            '';
+            example = [{port = 9090;}];
+            type = types.listOf (optionSOCKSPort true);
+          };
+          options.TestingTorNetwork = optionBool "TestingTorNetwork";
+          options.TransPort = optionIsolablePorts "TransPort";
+          options.TransProxyType = mkOption {
+            description = descriptionGeneric "TransProxyType";
+            type = with types; nullOr (enum ["default" "TPROXY" "ipfw" "pf-divert"]);
+            default = null;
+          };
+          #options.TruncateLogFile
+          options.UnixSocksGroupWritable = optionBool "UnixSocksGroupWritable";
+          options.UseDefaultFallbackDirs = optionBool "UseDefaultFallbackDirs";
+          options.UseMicrodescriptors = optionBool "UseMicrodescriptors";
+          options.V3AuthUseLegacyKey = optionBool "V3AuthUseLegacyKey";
+          options.V3AuthoritativeDirectory = optionBool "V3AuthoritativeDirectory";
+          options.VersioningAuthoritativeDirectory = optionBool "VersioningAuthoritativeDirectory";
+          options.VirtualAddrNetworkIPv4 = optionString "VirtualAddrNetworkIPv4";
+          options.VirtualAddrNetworkIPv6 = optionString "VirtualAddrNetworkIPv6";
+          options.WarnPlaintextPorts = optionPorts "WarnPlaintextPorts";
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # Not sure if `cfg.relay.role == "private-bridge"` helps as tor
+    # sends a lot of stats
+    warnings = optional (cfg.settings.BridgeRelay &&
+      flatten (mapAttrsToList (n: o: o.map) cfg.relay.onionServices) != [])
+      ''
+        Running Tor hidden services on a public relay makes the
+        presence of hidden services visible through simple statistical
+        analysis of publicly available data.
+        See https://trac.torproject.org/projects/tor/ticket/8742
+
+        You can safely ignore this warning if you don't intend to
+        actually hide your hidden services. In either case, you can
+        always create a container/VM with a separate Tor daemon instance.
+      '' ++
+      flatten (mapAttrsToList (n: o:
+        optional (o.settings.HiddenServiceVersion == 2) [
+          (optional (o.settings.HiddenServiceExportCircuitID != null) ''
+            HiddenServiceExportCircuitID is used in the HiddenService: ${n}
+            but this option is only for v3 hidden services.
+          '')
+        ] ++
+        optional (o.settings.HiddenServiceVersion != 2) [
+          (optional (o.settings.HiddenServiceAuthorizeClient != null) ''
+            HiddenServiceAuthorizeClient is used in the HiddenService: ${n}
+            but this option is only for v2 hidden services.
+          '')
+          (optional (o.settings.RendPostPeriod != null) ''
+            RendPostPeriod is used in the HiddenService: ${n}
+            but this option is only for v2 hidden services.
+          '')
+        ]
+      ) cfg.relay.onionServices);
+
+    users.groups.tor.gid = config.ids.gids.tor;
+    users.users.tor =
+      { description = "Tor Daemon User";
+        createHome  = true;
+        home        = stateDir;
+        group       = "tor";
+        uid         = config.ids.uids.tor;
+      };
+
+    services.tor.settings = mkMerge [
+      (mkIf cfg.enableGeoIP {
+        GeoIPFile = "${cfg.package.geoip}/share/tor/geoip";
+        GeoIPv6File = "${cfg.package.geoip}/share/tor/geoip6";
+      })
+      (mkIf cfg.controlSocket.enable {
+        ControlPort = [ { unix = runDir + "/control"; GroupWritable=true; RelaxDirModeCheck=true; } ];
+      })
+      (mkIf cfg.relay.enable (
+        optionalAttrs (cfg.relay.role != "exit") {
+          ExitPolicy = mkForce ["reject *:*"];
+        } //
+        optionalAttrs (elem cfg.relay.role ["bridge" "private-bridge"]) {
+          BridgeRelay = true;
+          ExtORPort.port = mkDefault "auto";
+          ServerTransportPlugin.transports = mkDefault ["obfs4"];
+          ServerTransportPlugin.exec = mkDefault "${pkgs.obfs4}/bin/obfs4proxy managed";
+        } // optionalAttrs (cfg.relay.role == "private-bridge") {
+          ExtraInfoStatistics = false;
+          PublishServerDescriptor = false;
+        }
+      ))
+      (mkIf (!cfg.relay.enable) {
+        # Avoid surprises when leaving ORPort/DirPort configurations in cfg.settings,
+        # because it would still enable Tor as a relay,
+        # which can trigger all sort of problems when not carefully done,
+        # like the blocklisting of the machine's IP addresses
+        # by some hosting providers...
+        DirPort = mkForce [];
+        ORPort = mkForce [];
+        PublishServerDescriptor = mkForce false;
+      })
+      (mkIf (!cfg.client.enable) {
+        # Make sure application connections via SOCKS are disabled
+        # when services.tor.client.enable is false
+        SOCKSPort = mkForce [ 0 ];
+      })
+      (mkIf cfg.client.enable (
+        { SOCKSPort = [ cfg.client.socksListenAddress ];
+        } // optionalAttrs cfg.client.transparentProxy.enable {
+          TransPort = [{ addr = "127.0.0.1"; port = 9040; }];
+        } // optionalAttrs cfg.client.dns.enable {
+          DNSPort = [{ addr = "127.0.0.1"; port = 9053; }];
+          AutomapHostsOnResolve = true;
+        } // optionalAttrs (flatten (mapAttrsToList (n: o: o.clientAuthorizations) cfg.client.onionServices) != []) {
+          ClientOnionAuthDir = runDir + "/ClientOnionAuthDir";
+        }
+      ))
+    ];
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts =
+        concatMap (o:
+          if isInt o && o > 0 then [o]
+          else if o ? "port" && isInt o.port && o.port > 0 then [o.port]
+          else []
+        ) (flatten [
+          cfg.settings.ORPort
+          cfg.settings.DirPort
+        ]);
+    };
+
+    systemd.services.tor = {
+      description = "Tor Daemon";
+      path = [ pkgs.tor ];
+
+      wantedBy = [ "multi-user.target" ];
+      after    = [ "network.target" ];
+      restartTriggers = [ torrc ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = "tor";
+        Group = "tor";
+        ExecStartPre = [
+          "${cfg.package}/bin/tor -f ${torrc} --verify-config"
+          # DOC: Appendix G of https://spec.torproject.org/rend-spec-v3
+          ("+" + pkgs.writeShellScript "ExecStartPre" (concatStringsSep "\n" (flatten (["set -eu"] ++
+            mapAttrsToList (name: onion:
+              optional (onion.authorizedClients != []) ''
+                rm -rf ${escapeShellArg onion.path}/authorized_clients
+                install -d -o tor -g tor -m 0700 ${escapeShellArg onion.path} ${escapeShellArg onion.path}/authorized_clients
+              '' ++
+              imap0 (i: pubKey: ''
+                echo ${pubKey} |
+                install -o tor -g tor -m 0400 /dev/stdin ${escapeShellArg onion.path}/authorized_clients/${toString i}.auth
+              '') onion.authorizedClients ++
+              optional (onion.secretKey != null) ''
+                install -d -o tor -g tor -m 0700 ${escapeShellArg onion.path}
+                key="$(cut -f1 -d: ${escapeShellArg onion.secretKey} | head -1)"
+                case "$key" in
+                 ("== ed25519v"*"-secret")
+                  install -o tor -g tor -m 0400 ${escapeShellArg onion.secretKey} ${escapeShellArg onion.path}/hs_ed25519_secret_key;;
+                 (*) echo >&2 "NixOS does not (yet) support secret key type for onion: ${name}"; exit 1;;
+                esac
+              ''
+            ) cfg.relay.onionServices ++
+            mapAttrsToList (name: onion: imap0 (i: prvKeyPath:
+              let hostname = removeSuffix ".onion" name; in ''
+              printf "%s:" ${escapeShellArg hostname} | cat - ${escapeShellArg prvKeyPath} |
+              install -o tor -g tor -m 0700 /dev/stdin \
+               ${runDir}/ClientOnionAuthDir/${escapeShellArg hostname}.${toString i}.auth_private
+            '') onion.clientAuthorizations)
+            cfg.client.onionServices
+          ))))
+        ];
+        ExecStart = "${cfg.package}/bin/tor -f ${torrc}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        KillSignal = "SIGINT";
+        TimeoutSec = cfg.settings.ShutdownWaitLength + 30; # Wait a bit longer than ShutdownWaitLength before actually timing out
+        Restart = "on-failure";
+        LimitNOFILE = 32768;
+        RuntimeDirectory = [
+          # g+x allows access to the control socket
+          "tor"
+          "tor/root"
+          # g+x can't be removed in ExecStart=, but will be removed by Tor
+          "tor/ClientOnionAuthDir"
+        ];
+        RuntimeDirectoryMode = "0710";
+        StateDirectoryMode = "0700";
+        StateDirectory = [
+            "tor"
+            "tor/onion"
+          ] ++
+          flatten (mapAttrsToList (name: onion:
+            optional (onion.secretKey == null) "tor/onion/${name}"
+          ) cfg.relay.onionServices);
+        # The following options are only to optimize:
+        # systemd-analyze security tor
+        RootDirectory = runDir + "/root";
+        RootDirectoryStartOnly = true;
+        #InaccessiblePaths = [ "-+${runDir}/root" ];
+        UMask = "0066";
+        BindPaths = [ stateDir ];
+        BindReadOnlyPaths = [ storeDir "/etc" ] ++
+          optionals config.services.resolved.enable [
+            "/run/systemd/resolve/stub-resolv.conf"
+            "/run/systemd/resolve/resolv.conf"
+          ];
+        AmbientCapabilities   = [""] ++ lib.optional bindsPrivilegedPort "CAP_NET_BIND_SERVICE";
+        CapabilityBoundingSet = [""] ++ lib.optional bindsPrivilegedPort "CAP_NET_BIND_SERVICE";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateNetwork = mkDefault false;
+        PrivateTmp = true;
+        # Tor cannot currently bind privileged port when PrivateUsers=true,
+        # see https://gitlab.torproject.org/legacy/trac/-/issues/20930
+        PrivateUsers = !bindsPrivilegedPort;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        # See also the finer but experimental option settings.Sandbox
+        SystemCallFilter = [
+          "@system-service"
+          # Groups in @system-service which do not contain a syscall listed by:
+          # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' tor
+          # in tests, and seem likely not necessary for tor.
+          "~@aio" "~@chown" "~@keyring" "~@memlock" "~@resources" "~@setuid" "~@timer"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+      };
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+
+  meta.maintainers = with lib.maintainers; [ julm ];
+}
diff --git a/nixos/modules/services/security/torify.nix b/nixos/modules/services/security/torify.nix
new file mode 100644
index 00000000000..39551190dd3
--- /dev/null
+++ b/nixos/modules/services/security/torify.nix
@@ -0,0 +1,80 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+
+  cfg = config.services.tor;
+
+  torify = pkgs.writeTextFile {
+    name = "tsocks";
+    text = ''
+        #!${pkgs.runtimeShell}
+        TSOCKS_CONF_FILE=${pkgs.writeText "tsocks.conf" cfg.tsocks.config} LD_PRELOAD="${pkgs.tsocks}/lib/libtsocks.so $LD_PRELOAD" "$@"
+    '';
+    executable = true;
+    destination = "/bin/tsocks";
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.tor.tsocks = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to build tsocks wrapper script to relay application traffic via Tor.
+
+          <important>
+            <para>You shouldn't use this unless you know what you're
+            doing because your installation of Tor already comes with
+            its own superior (doesn't leak DNS queries)
+            <literal>torsocks</literal> wrapper which does pretty much
+            exactly the same thing as this.</para>
+          </important>
+        '';
+      };
+
+      server = mkOption {
+        type = types.str;
+        default = "localhost:9050";
+        example = "192.168.0.20";
+        description = ''
+          IP address of TOR client to use.
+        '';
+      };
+
+      config = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration. Contents will be added verbatim to TSocks
+          configuration file.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.tsocks.enable {
+
+    environment.systemPackages = [ torify ];  # expose it to the users
+
+    services.tor.tsocks.config = ''
+      server = ${toString(head (splitString ":" cfg.tsocks.server))}
+      server_port = ${toString(tail (splitString ":" cfg.tsocks.server))}
+
+      local = 127.0.0.0/255.128.0.0
+      local = 127.128.0.0/255.192.0.0
+    '';
+  };
+
+}
diff --git a/nixos/modules/services/security/torsocks.nix b/nixos/modules/services/security/torsocks.nix
new file mode 100644
index 00000000000..fdd6ac32cc6
--- /dev/null
+++ b/nixos/modules/services/security/torsocks.nix
@@ -0,0 +1,121 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.tor.torsocks;
+  optionalNullStr = b: v: optionalString (b != null) v;
+
+  configFile = server: ''
+    TorAddress ${toString (head (splitString ":" server))}
+    TorPort    ${toString (tail (splitString ":" server))}
+
+    OnionAddrRange ${cfg.onionAddrRange}
+
+    ${optionalNullStr cfg.socks5Username
+        "SOCKS5Username ${cfg.socks5Username}"}
+    ${optionalNullStr cfg.socks5Password
+        "SOCKS5Password ${cfg.socks5Password}"}
+
+    AllowInbound ${if cfg.allowInbound then "1" else "0"}
+  '';
+
+  wrapTorsocks = name: server: pkgs.writeTextFile {
+    name = name;
+    text = ''
+        #!${pkgs.runtimeShell}
+        TORSOCKS_CONF_FILE=${pkgs.writeText "torsocks.conf" (configFile server)} ${pkgs.torsocks}/bin/torsocks "$@"
+    '';
+    executable = true;
+    destination = "/bin/${name}";
+  };
+
+in
+{
+  options = {
+    services.tor.torsocks = {
+      enable = mkOption {
+        type        = types.bool;
+        default     = config.services.tor.enable && config.services.tor.client.enable;
+        defaultText = literalExpression "config.services.tor.enable && config.services.tor.client.enable";
+        description = ''
+          Whether to build <literal>/etc/tor/torsocks.conf</literal>
+          containing the specified global torsocks configuration.
+        '';
+      };
+
+      server = mkOption {
+        type    = types.str;
+        default = "127.0.0.1:9050";
+        example = "192.168.0.20:1234";
+        description = ''
+          IP/Port of the Tor SOCKS server. Currently, hostnames are
+          NOT supported by torsocks.
+        '';
+      };
+
+      fasterServer = mkOption {
+        type    = types.str;
+        default = "127.0.0.1:9063";
+        example = "192.168.0.20:1234";
+        description = ''
+          IP/Port of the Tor SOCKS server for torsocks-faster wrapper suitable for HTTP.
+          Currently, hostnames are NOT supported by torsocks.
+        '';
+      };
+
+      onionAddrRange = mkOption {
+        type    = types.str;
+        default = "127.42.42.0/24";
+        description = ''
+          Tor hidden sites do not have real IP addresses. This
+          specifies what range of IP addresses will be handed to the
+          application as "cookies" for .onion names.  Of course, you
+          should pick a block of addresses which you aren't going to
+          ever need to actually connect to. This is similar to the
+          MapAddress feature of the main tor daemon.
+        '';
+      };
+
+      socks5Username = mkOption {
+        type    = types.nullOr types.str;
+        default = null;
+        example = "bob";
+        description = ''
+          SOCKS5 username. The <literal>TORSOCKS_USERNAME</literal>
+          environment variable overrides this option if it is set.
+        '';
+      };
+
+      socks5Password = mkOption {
+        type    = types.nullOr types.str;
+        default = null;
+        example = "sekret";
+        description = ''
+          SOCKS5 password. The <literal>TORSOCKS_PASSWORD</literal>
+          environment variable overrides this option if it is set.
+        '';
+      };
+
+      allowInbound = mkOption {
+        type    = types.bool;
+        default = false;
+        description = ''
+          Set Torsocks to accept inbound connections. If set to
+          <literal>true</literal>, listen() and accept() will be
+          allowed to be used with non localhost address.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.torsocks (wrapTorsocks "torsocks-faster" cfg.fasterServer) ];
+
+    environment.etc."tor/torsocks.conf" =
+      {
+        source = pkgs.writeText "torsocks.conf" (configFile cfg.server);
+      };
+  };
+}
diff --git a/nixos/modules/services/security/usbguard.nix b/nixos/modules/services/security/usbguard.nix
new file mode 100644
index 00000000000..201b37f17ba
--- /dev/null
+++ b/nixos/modules/services/security/usbguard.nix
@@ -0,0 +1,214 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.usbguard;
+
+  # valid policy options
+  policy = (types.enum [ "allow" "block" "reject" "keep" "apply-policy" ]);
+
+  defaultRuleFile = "/var/lib/usbguard/rules.conf";
+
+  # decide what file to use for rules
+  ruleFile = if cfg.rules != null then pkgs.writeText "usbguard-rules" cfg.rules else defaultRuleFile;
+
+  daemonConf = ''
+    # generated by nixos/modules/services/security/usbguard.nix
+    RuleFile=${ruleFile}
+    ImplicitPolicyTarget=${cfg.implictPolicyTarget}
+    PresentDevicePolicy=${cfg.presentDevicePolicy}
+    PresentControllerPolicy=${cfg.presentControllerPolicy}
+    InsertedDevicePolicy=${cfg.insertedDevicePolicy}
+    RestoreControllerDeviceState=${boolToString cfg.restoreControllerDeviceState}
+    # this does not seem useful for endusers to change
+    DeviceManagerBackend=uevent
+    IPCAllowedUsers=${concatStringsSep " " cfg.IPCAllowedUsers}
+    IPCAllowedGroups=${concatStringsSep " " cfg.IPCAllowedGroups}
+    IPCAccessControlFiles=/var/lib/usbguard/IPCAccessControl.d/
+    DeviceRulesWithPort=${boolToString cfg.deviceRulesWithPort}
+    # HACK: that way audit logs still land in the journal
+    AuditFilePath=/dev/null
+  '';
+
+  daemonConfFile = pkgs.writeText "usbguard-daemon-conf" daemonConf;
+
+in
+{
+
+  ###### interface
+
+  options = {
+    services.usbguard = {
+      enable = mkEnableOption "USBGuard daemon";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.usbguard;
+        defaultText = literalExpression "pkgs.usbguard";
+        description = ''
+          The usbguard package to use. If you do not need the Qt GUI, use
+          <literal>pkgs.usbguard-nox</literal> to save disk space.
+        '';
+      };
+
+      rules = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        example = ''
+          allow with-interface equals { 08:*:* }
+        '';
+        description = ''
+          The USBGuard daemon will load this as the policy rule set.
+          As these rules are NixOS managed they are immutable and can't
+          be changed by the IPC interface.
+
+          If you do not set this option, the USBGuard daemon will load
+          it's policy rule set from <literal>${defaultRuleFile}</literal>.
+          This file can be changed manually or via the IPC interface.
+
+          Running <literal>usbguard generate-policy</literal> as root will
+          generate a config for your currently plugged in devices.
+
+          For more details see <citerefentry>
+          <refentrytitle>usbguard-rules.conf</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry>.
+        '';
+      };
+
+      implictPolicyTarget = mkOption {
+        type = policy;
+        default = "block";
+        description = ''
+          How to treat USB devices that don't match any rule in the policy.
+          Target should be one of allow, block or reject (logically remove the
+          device node from the system).
+        '';
+      };
+
+      presentDevicePolicy = mkOption {
+        type = policy;
+        default = "apply-policy";
+        description = ''
+          How to treat USB devices that are already connected when the daemon
+          starts. Policy should be one of allow, block, reject, keep (keep
+          whatever state the device is currently in) or apply-policy (evaluate
+          the rule set for every present device).
+        '';
+      };
+
+      presentControllerPolicy = mkOption {
+        type = policy;
+        default = "keep";
+        description = ''
+          How to treat USB controller devices that are already connected when
+          the daemon starts. One of allow, block, reject, keep or apply-policy.
+        '';
+      };
+
+      insertedDevicePolicy = mkOption {
+        type = policy;
+        default = "apply-policy";
+        description = ''
+          How to treat USB devices that are already connected after the daemon
+          starts. One of block, reject, apply-policy.
+        '';
+      };
+
+      restoreControllerDeviceState = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          The  USBGuard  daemon  modifies  some attributes of controller
+          devices like the default authorization state of new child device
+          instances. Using this setting, you can controll whether the daemon
+          will try to restore the attribute values to the state before
+          modificaton on shutdown.
+        '';
+      };
+
+      IPCAllowedUsers = mkOption {
+        type = types.listOf types.str;
+        default = [ "root" ];
+        example = [ "root" "yourusername" ];
+        description = ''
+          A list of usernames that the daemon will accept IPC connections from.
+        '';
+      };
+
+      IPCAllowedGroups = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "wheel" ];
+        description = ''
+          A list of groupnames that the daemon will accept IPC connections
+          from.
+        '';
+      };
+
+      deviceRulesWithPort = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Generate device specific rules including the "via-port" attribute.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.usbguard = {
+      description = "USBGuard daemon";
+
+      wantedBy = [ "basic.target" ];
+      wants = [ "systemd-udevd.service" ];
+
+      # make sure an empty rule file exists
+      preStart = ''[ -f "${ruleFile}" ] || touch ${ruleFile}'';
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${cfg.package}/bin/usbguard-daemon -P -k -c ${daemonConfFile}";
+        Restart = "on-failure";
+
+        StateDirectory = [
+          "usbguard"
+          "usbguard/IPCAccessControl.d"
+        ];
+
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "CAP_CHOWN CAP_FOWNER";
+        DeviceAllow = "/dev/null rw";
+        DevicePolicy = "strict";
+        IPAddressDeny = "any";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectKernelModules = true;
+        ProtectSystem = true;
+        ReadOnlyPaths = "-/";
+        ReadWritePaths = "-/dev/shm -/tmp";
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_NETLINK" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "@system-service";
+        UMask = "0077";
+      };
+    };
+  };
+  imports = [
+    (mkRemovedOptionModule [ "services" "usbguard" "ruleFile" ] "The usbguard module now uses ${defaultRuleFile} as ruleFile. Alternatively, use services.usbguard.rules to configure rules.")
+    (mkRemovedOptionModule [ "services" "usbguard" "IPCAccessControlFiles" ] "The usbguard module now hardcodes IPCAccessControlFiles to /var/lib/usbguard/IPCAccessControl.d.")
+    (mkRemovedOptionModule [ "services" "usbguard" "auditFilePath" ] "Removed usbguard module audit log files. Audit logs can be found in the systemd journal.")
+  ];
+}
diff --git a/nixos/modules/services/security/vault.nix b/nixos/modules/services/security/vault.nix
new file mode 100644
index 00000000000..d48bc472cb8
--- /dev/null
+++ b/nixos/modules/services/security/vault.nix
@@ -0,0 +1,204 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.vault;
+  opt = options.services.vault;
+
+  configFile = pkgs.writeText "vault.hcl" ''
+    listener "tcp" {
+      address = "${cfg.address}"
+      ${if (cfg.tlsCertFile == null || cfg.tlsKeyFile == null) then ''
+          tls_disable = "true"
+        '' else ''
+          tls_cert_file = "${cfg.tlsCertFile}"
+          tls_key_file = "${cfg.tlsKeyFile}"
+        ''}
+      ${cfg.listenerExtraConfig}
+    }
+    storage "${cfg.storageBackend}" {
+      ${optionalString (cfg.storagePath   != null) ''path = "${cfg.storagePath}"''}
+      ${optionalString (cfg.storageConfig != null) cfg.storageConfig}
+    }
+    ${optionalString (cfg.telemetryConfig != "") ''
+        telemetry {
+          ${cfg.telemetryConfig}
+        }
+      ''}
+    ${cfg.extraConfig}
+  '';
+
+  allConfigPaths = [configFile] ++ cfg.extraSettingsPaths;
+
+  configOptions = escapeShellArgs (concatMap (p: ["-config" p]) allConfigPaths);
+
+in
+
+{
+  options = {
+    services.vault = {
+      enable = mkEnableOption "Vault daemon";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.vault;
+        defaultText = literalExpression "pkgs.vault";
+        description = "This option specifies the vault package to use.";
+      };
+
+      address = mkOption {
+        type = types.str;
+        default = "127.0.0.1:8200";
+        description = "The name of the ip interface to listen to";
+      };
+
+      tlsCertFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/path/to/your/cert.pem";
+        description = "TLS certificate file. TLS will be disabled unless this option is set";
+      };
+
+      tlsKeyFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/path/to/your/key.pem";
+        description = "TLS private key file. TLS will be disabled unless this option is set";
+      };
+
+      listenerExtraConfig = mkOption {
+        type = types.lines;
+        default = ''
+          tls_min_version = "tls12"
+        '';
+        description = "Extra text appended to the listener section.";
+      };
+
+      storageBackend = mkOption {
+        type = types.enum [ "inmem" "file" "consul" "zookeeper" "s3" "azure" "dynamodb" "etcd" "mssql" "mysql" "postgresql" "swift" "gcs" "raft" ];
+        default = "inmem";
+        description = "The name of the type of storage backend";
+      };
+
+      storagePath = mkOption {
+        type = types.nullOr types.path;
+        default = if cfg.storageBackend == "file" then "/var/lib/vault" else null;
+        defaultText = literalExpression ''
+          if config.${opt.storageBackend} == "file"
+          then "/var/lib/vault"
+          else null
+        '';
+        description = "Data directory for file backend";
+      };
+
+      storageConfig = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = ''
+          HCL configuration to insert in the storageBackend section.
+
+          Confidential values should not be specified here because this option's
+          value is written to the Nix store, which is publicly readable.
+          Provide credentials and such in a separate file using
+          <xref linkend="opt-services.vault.extraSettingsPaths"/>.
+        '';
+      };
+
+      telemetryConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Telemetry configuration";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Extra text appended to <filename>vault.hcl</filename>.";
+      };
+
+      extraSettingsPaths = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          Configuration files to load besides the immutable one defined by the NixOS module.
+          This can be used to avoid putting credentials in the Nix store, which can be read by any user.
+
+          Each path can point to a JSON- or HCL-formatted file, or a directory
+          to be scanned for files with <literal>.hcl</literal> or
+          <literal>.json</literal> extensions.
+
+          To upload the confidential file with NixOps, use for example:
+
+          <programlisting><![CDATA[
+          # https://releases.nixos.org/nixops/latest/manual/manual.html#opt-deployment.keys
+          deployment.keys."vault.hcl" = let db = import ./db-credentials.nix; in {
+            text = ${"''"}
+              storage "postgresql" {
+                connection_url = "postgres://''${db.username}:''${db.password}@host.example.com/exampledb?sslmode=verify-ca"
+              }
+            ${"''"};
+            user = "vault";
+          };
+          services.vault.extraSettingsPaths = ["/run/keys/vault.hcl"];
+          services.vault.storageBackend = "postgresql";
+          users.users.vault.extraGroups = ["keys"];
+          ]]></programlisting>
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.storageBackend == "inmem" -> (cfg.storagePath == null && cfg.storageConfig == null);
+        message = ''The "inmem" storage expects no services.vault.storagePath nor services.vault.storageConfig'';
+      }
+      { assertion = (cfg.storageBackend == "file" -> (cfg.storagePath != null && cfg.storageConfig == null)) && (cfg.storagePath != null -> cfg.storageBackend == "file");
+        message = ''You must set services.vault.storagePath only when using the "file" backend'';
+      }
+    ];
+
+    users.users.vault = {
+      name = "vault";
+      group = "vault";
+      uid = config.ids.uids.vault;
+      description = "Vault daemon user";
+    };
+    users.groups.vault.gid = config.ids.gids.vault;
+
+    systemd.tmpfiles.rules = optional (cfg.storagePath != null)
+      "d '${cfg.storagePath}' 0700 vault vault - -";
+
+    systemd.services.vault = {
+      description = "Vault server daemon";
+
+      wantedBy = ["multi-user.target"];
+      after = [ "network.target" ]
+           ++ optional (config.services.consul.enable && cfg.storageBackend == "consul") "consul.service";
+
+      restartIfChanged = false; # do not restart on "nixos-rebuild switch". It would seal the storage and disrupt the clients.
+
+      startLimitIntervalSec = 60;
+      startLimitBurst = 3;
+      serviceConfig = {
+        User = "vault";
+        Group = "vault";
+        ExecStart = "${cfg.package}/bin/vault server ${configOptions}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ProtectSystem = "full";
+        ProtectHome = "read-only";
+        AmbientCapabilities = "cap_ipc_lock";
+        NoNewPrivileges = true;
+        KillSignal = "SIGINT";
+        TimeoutStopSec = "30s";
+        Restart = "on-failure";
+      };
+
+      unitConfig.RequiresMountsFor = optional (cfg.storagePath != null) cfg.storagePath;
+    };
+  };
+
+}
diff --git a/nixos/modules/services/security/vaultwarden/backup.sh b/nixos/modules/services/security/vaultwarden/backup.sh
new file mode 100644
index 00000000000..2a3de0ab1de
--- /dev/null
+++ b/nixos/modules/services/security/vaultwarden/backup.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+
+# Based on: https://github.com/dani-garcia/vaultwarden/wiki/Backing-up-your-vault
+if ! mkdir -p "$BACKUP_FOLDER"; then
+  echo "Could not create backup folder '$BACKUP_FOLDER'" >&2
+  exit 1
+fi
+
+if [[ ! -f "$DATA_FOLDER"/db.sqlite3 ]]; then
+  echo "Could not find SQLite database file '$DATA_FOLDER/db.sqlite3'" >&2
+  exit 1
+fi
+
+sqlite3 "$DATA_FOLDER"/db.sqlite3 ".backup '$BACKUP_FOLDER/db.sqlite3'"
+cp "$DATA_FOLDER"/rsa_key.{der,pem,pub.der} "$BACKUP_FOLDER"
+cp -r "$DATA_FOLDER"/attachments "$BACKUP_FOLDER"
+cp -r "$DATA_FOLDER"/icon_cache "$BACKUP_FOLDER"
diff --git a/nixos/modules/services/security/vaultwarden/default.nix b/nixos/modules/services/security/vaultwarden/default.nix
new file mode 100644
index 00000000000..8277f493639
--- /dev/null
+++ b/nixos/modules/services/security/vaultwarden/default.nix
@@ -0,0 +1,185 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.vaultwarden;
+  user = config.users.users.vaultwarden.name;
+  group = config.users.groups.vaultwarden.name;
+
+  # Convert name from camel case (e.g. disable2FARemember) to upper case snake case (e.g. DISABLE_2FA_REMEMBER).
+  nameToEnvVar = name:
+    let
+      parts = builtins.split "([A-Z0-9]+)" name;
+      partsToEnvVar = parts: foldl' (key: x: let last = stringLength key - 1; in
+        if isList x then key + optionalString (key != "" && substring last 1 key != "_") "_" + head x
+        else if key != "" && elem (substring 0 1 x) lowerChars then # to handle e.g. [ "disable" [ "2FAR" ] "emember" ]
+          substring 0 last key + optionalString (substring (last - 1) 1 key != "_") "_" + substring last 1 key + toUpper x
+        else key + toUpper x) "" parts;
+    in if builtins.match "[A-Z0-9_]+" name != null then name else partsToEnvVar parts;
+
+  # Due to the different naming schemes allowed for config keys,
+  # we can only check for values consistently after converting them to their corresponding environment variable name.
+  configEnv =
+    let
+      configEnv = listToAttrs (concatLists (mapAttrsToList (name: value:
+        if value != null then [ (nameValuePair (nameToEnvVar name) (if isBool value then boolToString value else toString value)) ] else []
+      ) cfg.config));
+    in { DATA_FOLDER = "/var/lib/bitwarden_rs"; } // optionalAttrs (!(configEnv ? WEB_VAULT_ENABLED) || configEnv.WEB_VAULT_ENABLED == "true") {
+      WEB_VAULT_FOLDER = "${cfg.webVaultPackage}/share/vaultwarden/vault";
+    } // configEnv;
+
+  configFile = pkgs.writeText "vaultwarden.env" (concatStrings (mapAttrsToList (name: value: "${name}=${value}\n") configEnv));
+
+  vaultwarden = cfg.package.override { inherit (cfg) dbBackend; };
+
+in {
+  imports = [
+    (mkRenamedOptionModule [ "services" "bitwarden_rs" ] [ "services" "vaultwarden" ])
+  ];
+
+  options.services.vaultwarden = with types; {
+    enable = mkEnableOption "vaultwarden";
+
+    dbBackend = mkOption {
+      type = enum [ "sqlite" "mysql" "postgresql" ];
+      default = "sqlite";
+      description = ''
+        Which database backend vaultwarden will be using.
+      '';
+    };
+
+    backupDir = mkOption {
+      type = nullOr str;
+      default = null;
+      description = ''
+        The directory under which vaultwarden will backup its persistent data.
+      '';
+    };
+
+    config = mkOption {
+      type = attrsOf (nullOr (oneOf [ bool int str ]));
+      default = {};
+      example = literalExpression ''
+        {
+          domain = "https://bw.domain.tld:8443";
+          signupsAllowed = true;
+          rocketPort = 8222;
+          rocketLog = "critical";
+        }
+      '';
+      description = ''
+        The configuration of vaultwarden is done through environment variables,
+        therefore the names are converted from camel case (e.g. disable2FARemember)
+        to upper case snake case (e.g. DISABLE_2FA_REMEMBER).
+        In this conversion digits (0-9) are handled just like upper case characters,
+        so foo2 would be converted to FOO_2.
+        Names already in this format remain unchanged, so FOO2 remains FOO2 if passed as such,
+        even though foo2 would have been converted to FOO_2.
+        This allows working around any potential future conflicting naming conventions.
+
+        Based on the attributes passed to this config option an environment file will be generated
+        that is passed to vaultwarden's systemd service.
+
+        The available configuration options can be found in
+        <link xlink:href="https://github.com/dani-garcia/vaultwarden/blob/${vaultwarden.version}/.env.template">the environment template file</link>.
+      '';
+    };
+
+    environmentFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = "/root/vaultwarden.env";
+      description = ''
+        Additional environment file as defined in <citerefentry>
+        <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+        </citerefentry>.
+
+        Secrets like <envar>ADMIN_TOKEN</envar> and <envar>SMTP_PASSWORD</envar>
+        may be passed to the service without adding them to the world-readable Nix store.
+
+        Note that this file needs to be available on the host on which
+        <literal>vaultwarden</literal> is running.
+      '';
+    };
+
+    package = mkOption {
+      type = package;
+      default = pkgs.vaultwarden;
+      defaultText = literalExpression "pkgs.vaultwarden";
+      description = "Vaultwarden package to use.";
+    };
+
+    webVaultPackage = mkOption {
+      type = package;
+      default = pkgs.vaultwarden-vault;
+      defaultText = literalExpression "pkgs.vaultwarden-vault";
+      description = "Web vault package to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [ {
+      assertion = cfg.backupDir != null -> cfg.dbBackend == "sqlite";
+      message = "Backups for database backends other than sqlite will need customization";
+    } ];
+
+    users.users.vaultwarden = {
+      inherit group;
+      isSystemUser = true;
+    };
+    users.groups.vaultwarden = { };
+
+    systemd.services.vaultwarden = {
+      aliases = [ "bitwarden_rs.service" ];
+      after = [ "network.target" ];
+      path = with pkgs; [ openssl ];
+      serviceConfig = {
+        User = user;
+        Group = group;
+        EnvironmentFile = [ configFile ] ++ optional (cfg.environmentFile != null) cfg.environmentFile;
+        ExecStart = "${vaultwarden}/bin/vaultwarden";
+        LimitNOFILE = "1048576";
+        PrivateTmp = "true";
+        PrivateDevices = "true";
+        ProtectHome = "true";
+        ProtectSystem = "strict";
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        StateDirectory = "bitwarden_rs";
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    systemd.services.backup-vaultwarden = mkIf (cfg.backupDir != null) {
+      aliases = [ "backup-bitwarden_rs.service" ];
+      description = "Backup vaultwarden";
+      environment = {
+        DATA_FOLDER = "/var/lib/bitwarden_rs";
+        BACKUP_FOLDER = cfg.backupDir;
+      };
+      path = with pkgs; [ sqlite ];
+      serviceConfig = {
+        SyslogIdentifier = "backup-vaultwarden";
+        Type = "oneshot";
+        User = mkDefault user;
+        Group = mkDefault group;
+        ExecStart = "${pkgs.bash}/bin/bash ${./backup.sh}";
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    systemd.timers.backup-vaultwarden = mkIf (cfg.backupDir != null) {
+      aliases = [ "backup-bitwarden_rs.service" ];
+      description = "Backup vaultwarden on time";
+      timerConfig = {
+        OnCalendar = mkDefault "23:00";
+        Persistent = "true";
+        Unit = "backup-vaultwarden.service";
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+
+  # uses attributes of the linked package
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/security/yubikey-agent.nix b/nixos/modules/services/security/yubikey-agent.nix
new file mode 100644
index 00000000000..8be2457e1e2
--- /dev/null
+++ b/nixos/modules/services/security/yubikey-agent.nix
@@ -0,0 +1,66 @@
+# Global configuration for yubikey-agent.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.yubikey-agent;
+
+  # reuse the pinentryFlavor option from the gnupg module
+  pinentryFlavor = config.programs.gnupg.agent.pinentryFlavor;
+in
+{
+  ###### interface
+
+  meta.maintainers = with maintainers; [ philandstuff rawkode jwoudenberg ];
+
+  options = {
+
+    services.yubikey-agent = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to start yubikey-agent when you log in.  Also sets
+          SSH_AUTH_SOCK to point at yubikey-agent.
+
+          Note that yubikey-agent will use whatever pinentry is
+          specified in programs.gnupg.agent.pinentryFlavor.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.yubikey-agent;
+        defaultText = literalExpression "pkgs.yubikey-agent";
+        description = ''
+          The package used for the yubikey-agent daemon.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    systemd.packages = [ cfg.package ];
+
+    # This overrides the systemd user unit shipped with the
+    # yubikey-agent package
+    systemd.user.services.yubikey-agent = mkIf (pinentryFlavor != null) {
+      path = [ pkgs.pinentry.${pinentryFlavor} ];
+      wantedBy = [
+        (if pinentryFlavor == "tty" || pinentryFlavor == "curses" then
+          "default.target"
+        else
+          "graphical-session.target")
+      ];
+    };
+
+    environment.extraInit = ''
+      if [ -z "$SSH_AUTH_SOCK" -a -n "$XDG_RUNTIME_DIR" ]; then
+        export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/yubikey-agent/yubikey-agent.sock"
+      fi
+    '';
+  };
+}
diff --git a/nixos/modules/services/system/cachix-agent/default.nix b/nixos/modules/services/system/cachix-agent/default.nix
new file mode 100644
index 00000000000..496e0b90355
--- /dev/null
+++ b/nixos/modules/services/system/cachix-agent/default.nix
@@ -0,0 +1,57 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cachix-agent;
+in {
+  meta.maintainers = [ lib.maintainers.domenkozar ];
+
+  options.services.cachix-agent = {
+    enable = mkEnableOption "Cachix Deploy Agent: https://docs.cachix.org/deploy/";
+
+    name = mkOption {
+      type = types.str;
+      description = "Agent name, usually same as the hostname";
+      default = config.networking.hostName;
+      defaultText = "config.networking.hostName";
+    };
+
+    profile = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Profile name, defaults to 'system' (NixOS).";
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.cachix;
+      defaultText = literalExpression "pkgs.cachix";
+      description = "Cachix Client package to use.";
+    };
+
+    credentialsFile = mkOption {
+      type = types.path;
+      default = "/etc/cachix-agent.token";
+      description = ''
+        Required file that needs to contain CACHIX_AGENT_TOKEN=...
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.cachix-agent = {
+      description = "Cachix Deploy Agent";
+      after = ["network-online.target"];
+      path = [ config.nix.package ];
+      wantedBy = [ "multi-user.target" ];
+      # don't restart while changing
+      reloadIfChanged = true;
+      serviceConfig = {
+        Restart = "on-failure";
+        EnvironmentFile = cfg.credentialsFile;
+        ExecStart = "${cfg.package}/bin/cachix deploy agent ${cfg.name} ${if cfg.profile != null then profile else ""}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/system/cloud-init.nix b/nixos/modules/services/system/cloud-init.nix
new file mode 100644
index 00000000000..8c6a6e294eb
--- /dev/null
+++ b/nixos/modules/services/system/cloud-init.nix
@@ -0,0 +1,194 @@
+{ config, lib, pkgs, ... }:
+
+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
+    ;
+in
+{
+  options = {
+    services.cloud-init = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the cloud-init service. This services reads
+          configuration metadata in a cloud environment and configures
+          the machine according to this metadata.
+
+          This configuration is not completely compatible with the
+          NixOS way of doing configuration, as configuration done by
+          cloud-init might be overriden by a subsequent nixos-rebuild
+          call. However, some parts of cloud-init fall outside of
+          NixOS's responsibility, like filesystem resizing and ssh
+          public key provisioning, and cloud-init is useful for that
+          parts. Thus, be wary that using cloud-init in NixOS might
+          come as some cost.
+        '';
+      };
+
+      btrfs.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Allow the cloud-init service to operate `btrfs` filesystem.
+        '';
+      };
+
+      ext4.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Allow the cloud-init service to operate `ext4` filesystem.
+        '';
+      };
+
+      network.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Allow the cloud-init service to configure network interfaces
+          through systemd-networkd.
+        '';
+      };
+
+      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_etc_hosts
+           - 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 = "cloud-init configuration.";
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.etc."cloud/cloud.cfg".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 =
+      { 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-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" ];
+      };
+  };
+}
diff --git a/nixos/modules/services/system/dbus.nix b/nixos/modules/services/system/dbus.nix
new file mode 100644
index 00000000000..d4cacb85694
--- /dev/null
+++ b/nixos/modules/services/system/dbus.nix
@@ -0,0 +1,139 @@
+# D-Bus configuration and system bus daemon.
+
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.dbus;
+
+  homeDir = "/run/dbus";
+
+  configDir = pkgs.makeDBusConf {
+    inherit (cfg) apparmor;
+    suidHelper = "${config.security.wrapperDir}/dbus-daemon-launch-helper";
+    serviceDirectories = cfg.packages;
+  };
+
+in
+
+{
+  ###### interface
+
+  options = {
+
+    services.dbus = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        internal = true;
+        description = ''
+          Whether to start the D-Bus message bus daemon, which is
+          required by many other system services and applications.
+        '';
+      };
+
+      packages = mkOption {
+        type = types.listOf types.path;
+        default = [ ];
+        description = ''
+          Packages whose D-Bus configuration files should be included in
+          the configuration of the D-Bus system-wide or session-wide
+          message bus.  Specifically, files in the following directories
+          will be included into their respective DBus configuration paths:
+          <filename><replaceable>pkg</replaceable>/etc/dbus-1/system.d</filename>
+          <filename><replaceable>pkg</replaceable>/share/dbus-1/system.d</filename>
+          <filename><replaceable>pkg</replaceable>/share/dbus-1/system-services</filename>
+          <filename><replaceable>pkg</replaceable>/etc/dbus-1/session.d</filename>
+          <filename><replaceable>pkg</replaceable>/share/dbus-1/session.d</filename>
+          <filename><replaceable>pkg</replaceable>/share/dbus-1/services</filename>
+        '';
+      };
+
+      apparmor = mkOption {
+        type = types.enum [ "enabled" "disabled" "required" ];
+        description = ''
+          AppArmor mode for dbus.
+
+          <literal>enabled</literal> enables mediation when it's
+          supported in the kernel, <literal>disabled</literal>
+          always disables AppArmor even with kernel support, and
+          <literal>required</literal> fails when AppArmor was not found
+          in the kernel.
+        '';
+        default = "disabled";
+      };
+
+      socketActivated = mkOption {
+        type = types.nullOr types.bool;
+        default = null;
+        visible = false;
+        description = ''
+          Removed option, do not use.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    warnings = optional (cfg.socketActivated != null) (
+      let
+        files = showFiles options.services.dbus.socketActivated.files;
+      in
+        "The option 'services.dbus.socketActivated' in ${files} no longer has"
+        + " any effect and can be safely removed: the user D-Bus session is"
+        + " now always socket activated."
+    );
+
+    environment.systemPackages = [ pkgs.dbus.daemon pkgs.dbus ];
+
+    environment.etc."dbus-1".source = configDir;
+
+    users.users.messagebus = {
+      uid = config.ids.uids.messagebus;
+      description = "D-Bus system message bus daemon user";
+      home = homeDir;
+      group = "messagebus";
+    };
+
+    users.groups.messagebus.gid = config.ids.gids.messagebus;
+
+    systemd.packages = [ pkgs.dbus.daemon ];
+
+    security.wrappers.dbus-daemon-launch-helper = {
+      source = "${pkgs.dbus.daemon}/libexec/dbus-daemon-launch-helper";
+      owner = "root";
+      group = "messagebus";
+      setuid = true;
+      setgid = false;
+      permissions = "u+rx,g+rx,o-rx";
+    };
+
+    services.dbus.packages = [
+      pkgs.dbus.out
+      config.system.path
+    ];
+
+    systemd.services.dbus = {
+      # Don't restart dbus-daemon. Bad things tend to happen if we do.
+      reloadIfChanged = true;
+      restartTriggers = [ configDir ];
+      environment = { LD_LIBRARY_PATH = config.system.nssModules.path; };
+    };
+
+    systemd.user = {
+      services.dbus = {
+        # Don't restart dbus-daemon. Bad things tend to happen if we do.
+        reloadIfChanged = true;
+        restartTriggers = [ configDir ];
+      };
+      sockets.dbus.wantedBy = [ "sockets.target" ];
+    };
+
+    environment.pathsToLink = [ "/etc/dbus-1" "/share/dbus-1" ];
+  };
+}
diff --git a/nixos/modules/services/system/earlyoom.nix b/nixos/modules/services/system/earlyoom.nix
new file mode 100644
index 00000000000..ddd5bcebcdd
--- /dev/null
+++ b/nixos/modules/services/system/earlyoom.nix
@@ -0,0 +1,104 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.earlyoom;
+
+  inherit (lib)
+    mkDefault mkEnableOption mkIf mkOption types
+    mkRemovedOptionModule
+    concatStringsSep optional;
+
+in
+{
+  options.services.earlyoom = {
+    enable = mkEnableOption "Early out of memory killing";
+
+    freeMemThreshold = mkOption {
+      type = types.ints.between 1 100;
+      default = 10;
+      description = ''
+        Minimum of availabe memory (in percent).
+        If the free memory falls below this threshold and the analog is true for
+        <option>services.earlyoom.freeSwapThreshold</option>
+        the killing begins.
+      '';
+    };
+
+    freeSwapThreshold = mkOption {
+      type = types.ints.between 1 100;
+      default = 10;
+      description = ''
+        Minimum of availabe swap space (in percent).
+        If the available swap space falls below this threshold and the analog
+        is true for <option>services.earlyoom.freeMemThreshold</option>
+        the killing begins.
+      '';
+    };
+
+    # TODO: remove or warn after 1.7 (https://github.com/rfjakob/earlyoom/commit/7ebc4554)
+    ignoreOOMScoreAdjust = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Ignore oom_score_adjust values of processes.
+      '';
+    };
+
+    enableDebugInfo = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable debugging messages.
+      '';
+    };
+
+    enableNotifications = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Send notifications about killed processes via the system d-bus.
+
+        WARNING: enabling this option (while convenient) should *not* be done on a
+        machine where you do not trust the other users as it allows any other
+        local user to DoS your session by spamming notifications.
+
+        To actually see the notifications in your GUI session, you need to have
+        <literal>systembus-notify</literal> running as your user which this
+        option handles.
+
+        See <link xlink:href="https://github.com/rfjakob/earlyoom#notifications">README</link> for details.
+      '';
+    };
+  };
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "earlyoom" "useKernelOOMKiller" ] ''
+      This option is deprecated and ignored by earlyoom since 1.2.
+    '')
+    (mkRemovedOptionModule [ "services" "earlyoom" "notificationsCommand" ] ''
+      This option is deprecated and ignored by earlyoom since 1.6.
+    '')
+  ];
+
+  config = mkIf cfg.enable {
+    services.systembus-notify.enable = mkDefault cfg.enableNotifications;
+
+    systemd.services.earlyoom = {
+      description = "Early OOM Daemon for Linux";
+      wantedBy = [ "multi-user.target" ];
+      path = optional cfg.enableNotifications pkgs.dbus;
+      serviceConfig = {
+        StandardError = "journal";
+        ExecStart = concatStringsSep " " ([
+          "${pkgs.earlyoom}/bin/earlyoom"
+          "-m ${toString cfg.freeMemThreshold}"
+          "-s ${toString cfg.freeSwapThreshold}"
+        ]
+        ++ optional cfg.ignoreOOMScoreAdjust "-i"
+        ++ optional cfg.enableDebugInfo "-d"
+        ++ optional cfg.enableNotifications "-n"
+        );
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/system/kerberos/default.nix b/nixos/modules/services/system/kerberos/default.nix
new file mode 100644
index 00000000000..9a1e6739901
--- /dev/null
+++ b/nixos/modules/services/system/kerberos/default.nix
@@ -0,0 +1,75 @@
+{config, lib, ...}:
+
+let
+  inherit (lib) mkOption mkIf types length attrNames;
+  cfg = config.services.kerberos_server;
+  kerberos = config.krb5.kerberos;
+
+  aclEntry = {
+    options = {
+      principal = mkOption {
+        type = types.str;
+        description = "Which principal the rule applies to";
+      };
+      access = mkOption {
+        type = types.either
+          (types.listOf (types.enum ["add" "cpw" "delete" "get" "list" "modify"]))
+          (types.enum ["all"]);
+        default = "all";
+        description = "The changes the principal is allowed to make.";
+      };
+      target = mkOption {
+        type = types.str;
+        default = "*";
+        description = "The principals that 'access' applies to.";
+      };
+    };
+  };
+
+  realm = {
+    options = {
+      acl = mkOption {
+        type = types.listOf (types.submodule aclEntry);
+        default = [
+          { principal = "*/admin"; access = "all"; }
+          { principal = "admin"; access = "all"; }
+        ];
+        description = ''
+          The privileges granted to a user.
+        '';
+      };
+    };
+  };
+in
+
+{
+  imports = [
+    ./mit.nix
+    ./heimdal.nix
+  ];
+
+  ###### interface
+  options = {
+    services.kerberos_server = {
+      enable = lib.mkEnableOption "the kerberos authentification server";
+
+      realms = mkOption {
+        type = types.attrsOf (types.submodule realm);
+        description = ''
+          The realm(s) to serve keys for.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ kerberos ];
+    assertions = [{
+      assertion = length (attrNames cfg.realms) <= 1;
+      message = "Only one realm per server is currently supported.";
+    }];
+  };
+}
diff --git a/nixos/modules/services/system/kerberos/heimdal.nix b/nixos/modules/services/system/kerberos/heimdal.nix
new file mode 100644
index 00000000000..837c59caa56
--- /dev/null
+++ b/nixos/modules/services/system/kerberos/heimdal.nix
@@ -0,0 +1,68 @@
+{ pkgs, config, lib, ... } :
+
+let
+  inherit (lib) mkIf concatStringsSep concatMapStrings toList mapAttrs
+    mapAttrsToList;
+  cfg = config.services.kerberos_server;
+  kerberos = config.krb5.kerberos;
+  stateDir = "/var/heimdal";
+  aclFiles = mapAttrs
+    (name: {acl, ...}: pkgs.writeText "${name}.acl" (concatMapStrings ((
+      {principal, access, target, ...} :
+      "${principal}\t${concatStringsSep "," (toList access)}\t${target}\n"
+    )) acl)) cfg.realms;
+
+  kdcConfigs = mapAttrsToList (name: value: ''
+    database = {
+      dbname = ${stateDir}/heimdal
+      acl_file = ${value}
+    }
+  '') aclFiles;
+  kdcConfFile = pkgs.writeText "kdc.conf" ''
+    [kdc]
+    ${concatStringsSep "\n" kdcConfigs}
+  '';
+in
+
+{
+  # No documentation about correct triggers, so guessing at them.
+
+  config = mkIf (cfg.enable && kerberos == pkgs.heimdal) {
+    systemd.services.kadmind = {
+      description = "Kerberos Administration Daemon";
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        mkdir -m 0755 -p ${stateDir}
+      '';
+      serviceConfig.ExecStart =
+        "${kerberos}/libexec/heimdal/kadmind --config-file=/etc/heimdal-kdc/kdc.conf";
+      restartTriggers = [ kdcConfFile ];
+    };
+
+    systemd.services.kdc = {
+      description = "Key Distribution Center daemon";
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        mkdir -m 0755 -p ${stateDir}
+      '';
+      serviceConfig.ExecStart =
+        "${kerberos}/libexec/heimdal/kdc --config-file=/etc/heimdal-kdc/kdc.conf";
+      restartTriggers = [ kdcConfFile ];
+    };
+
+    systemd.services.kpasswdd = {
+      description = "Kerberos Password Changing daemon";
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        mkdir -m 0755 -p ${stateDir}
+      '';
+      serviceConfig.ExecStart = "${kerberos}/libexec/heimdal/kpasswdd";
+      restartTriggers = [ kdcConfFile ];
+    };
+
+    environment.etc = {
+      # Can be set via the --config-file option to KDC
+      "heimdal-kdc/kdc.conf".source = kdcConfFile;
+    };
+  };
+}
diff --git a/nixos/modules/services/system/kerberos/mit.nix b/nixos/modules/services/system/kerberos/mit.nix
new file mode 100644
index 00000000000..25d7d51e808
--- /dev/null
+++ b/nixos/modules/services/system/kerberos/mit.nix
@@ -0,0 +1,68 @@
+{ pkgs, config, lib, ... } :
+
+let
+  inherit (lib) mkIf concatStrings concatStringsSep concatMapStrings toList
+    mapAttrs mapAttrsToList;
+  cfg = config.services.kerberos_server;
+  kerberos = config.krb5.kerberos;
+  stateDir = "/var/lib/krb5kdc";
+  PIDFile = "/run/kdc.pid";
+  aclMap = {
+    add = "a"; cpw = "c"; delete = "d"; get = "i"; list = "l"; modify = "m";
+    all = "*";
+  };
+  aclFiles = mapAttrs
+    (name: {acl, ...}: (pkgs.writeText "${name}.acl" (concatMapStrings (
+      {principal, access, target, ...} :
+      let access_code = map (a: aclMap.${a}) (toList access); in
+      "${principal} ${concatStrings access_code} ${target}\n"
+    ) acl))) cfg.realms;
+  kdcConfigs = mapAttrsToList (name: value: ''
+    ${name} = {
+      acl_file = ${value}
+    }
+  '') aclFiles;
+  kdcConfFile = pkgs.writeText "kdc.conf" ''
+    [realms]
+    ${concatStringsSep "\n" kdcConfigs}
+  '';
+  env = {
+    # What Debian uses, could possibly link directly to Nix store?
+    KRB5_KDC_PROFILE = "/etc/krb5kdc/kdc.conf";
+  };
+in
+
+{
+  config = mkIf (cfg.enable && kerberos == pkgs.krb5Full) {
+    systemd.services.kadmind = {
+      description = "Kerberos Administration Daemon";
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        mkdir -m 0755 -p ${stateDir}
+      '';
+      serviceConfig.ExecStart = "${kerberos}/bin/kadmind -nofork";
+      restartTriggers = [ kdcConfFile ];
+      environment = env;
+    };
+
+    systemd.services.kdc = {
+      description = "Key Distribution Center daemon";
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        mkdir -m 0755 -p ${stateDir}
+      '';
+      serviceConfig = {
+        Type = "forking";
+        PIDFile = PIDFile;
+        ExecStart = "${kerberos}/bin/krb5kdc -P ${PIDFile}";
+      };
+      restartTriggers = [ kdcConfFile ];
+      environment = env;
+    };
+
+    environment.etc = {
+      "krb5kdc/kdc.conf".source = kdcConfFile;
+    };
+    environment.variables = env;
+  };
+}
diff --git a/nixos/modules/services/system/localtime.nix b/nixos/modules/services/system/localtime.nix
new file mode 100644
index 00000000000..8f23454af9d
--- /dev/null
+++ b/nixos/modules/services/system/localtime.nix
@@ -0,0 +1,49 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.localtime;
+in {
+  options = {
+    services.localtime = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable <literal>localtime</literal>, simple daemon for keeping the system
+          timezone up-to-date based on the current location. It uses geoclue2 to
+          determine the current location and systemd-timedated to actually set
+          the timezone.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.geoclue2 = {
+      enable = true;
+      appConfig.localtime = {
+        isAllowed = true;
+        isSystem = true;
+      };
+    };
+
+    # Install the polkit rules.
+    environment.systemPackages = [ pkgs.localtime ];
+    # Install the systemd unit.
+    systemd.packages = [ pkgs.localtime ];
+
+    users.users.localtimed = {
+      description = "localtime daemon";
+      isSystemUser = true;
+      group = "localtimed";
+    };
+    users.groups.localtimed = {};
+
+    systemd.services.localtime = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig.Restart = "on-failure";
+    };
+  };
+}
diff --git a/nixos/modules/services/system/nscd.conf b/nixos/modules/services/system/nscd.conf
new file mode 100644
index 00000000000..722b883ba42
--- /dev/null
+++ b/nixos/modules/services/system/nscd.conf
@@ -0,0 +1,34 @@
+# We basically use nscd as a proxy for forwarding nss requests to appropriate
+# nss modules, as we run nscd with LD_LIBRARY_PATH set to the directory
+# containing all such modules
+# Note that we can not use `enable-cache no` As this will actually cause nscd
+# to just reject the nss requests it receives, which then causes glibc to
+# fallback to trying to handle the request by itself. Which won't work as glibc
+# is not aware of the path in which the nss modules live.  As a workaround, we
+# have `enable-cache yes` with an explicit ttl of 0
+server-user             nscd
+
+enable-cache            passwd          yes
+positive-time-to-live   passwd          0
+negative-time-to-live   passwd          0
+shared                  passwd          yes
+
+enable-cache            group           yes
+positive-time-to-live   group           0
+negative-time-to-live   group           0
+shared                  group           yes
+
+enable-cache            netgroup        yes
+positive-time-to-live   netgroup        0
+negative-time-to-live   netgroup        0
+shared                  netgroup        yes
+
+enable-cache            hosts           yes
+positive-time-to-live   hosts           0
+negative-time-to-live   hosts           0
+shared                  hosts           yes
+
+enable-cache            services        yes
+positive-time-to-live   services        0
+negative-time-to-live   services        0
+shared                  services        yes
diff --git a/nixos/modules/services/system/nscd.nix b/nixos/modules/services/system/nscd.nix
new file mode 100644
index 00000000000..00a87e788dc
--- /dev/null
+++ b/nixos/modules/services/system/nscd.nix
@@ -0,0 +1,87 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  nssModulesPath = config.system.nssModules.path;
+  cfg = config.services.nscd;
+
+  nscd = if pkgs.stdenv.hostPlatform.libc == "glibc"
+         then pkgs.stdenv.cc.libc.bin
+         else pkgs.glibc.bin;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.nscd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable the Name Service Cache Daemon.
+          Disabling this is strongly discouraged, as this effectively disables NSS Lookups
+          from all non-glibc NSS modules, including the ones provided by systemd.
+        '';
+      };
+
+      config = mkOption {
+        type = types.lines;
+        default = builtins.readFile ./nscd.conf;
+        description = "Configuration to use for Name Service Cache Daemon.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.etc."nscd.conf".text = cfg.config;
+
+    systemd.services.nscd =
+      { description = "Name Service Cache Daemon";
+
+        before = [ "nss-lookup.target" "nss-user-lookup.target" ];
+        wants = [ "nss-lookup.target" "nss-user-lookup.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        environment = { LD_LIBRARY_PATH = nssModulesPath; };
+
+        restartTriggers = [
+          config.environment.etc.hosts.source
+          config.environment.etc."nsswitch.conf".source
+          config.environment.etc."nscd.conf".source
+        ];
+
+        # We use DynamicUser because in default configurations nscd doesn't
+        # create any files that need to survive restarts. However, in some
+        # configurations, nscd needs to be started as root; it will drop
+        # privileges after all the NSS modules have read their configuration
+        # files. So prefix the ExecStart command with "!" to prevent systemd
+        # from dropping privileges early. See ExecStart in systemd.service(5).
+        serviceConfig =
+          { ExecStart = "!@${nscd}/sbin/nscd nscd";
+            Type = "forking";
+            DynamicUser = true;
+            RuntimeDirectory = "nscd";
+            PIDFile = "/run/nscd/nscd.pid";
+            Restart = "always";
+            ExecReload =
+              [ "${nscd}/sbin/nscd --invalidate passwd"
+                "${nscd}/sbin/nscd --invalidate group"
+                "${nscd}/sbin/nscd --invalidate hosts"
+              ];
+          };
+      };
+
+  };
+}
diff --git a/nixos/modules/services/system/saslauthd.nix b/nixos/modules/services/system/saslauthd.nix
new file mode 100644
index 00000000000..466b0ca60a7
--- /dev/null
+++ b/nixos/modules/services/system/saslauthd.nix
@@ -0,0 +1,62 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.saslauthd;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.saslauthd = {
+
+      enable = mkEnableOption "saslauthd, the Cyrus SASL authentication daemon";
+
+      package = mkOption {
+        default = pkgs.cyrus_sasl.bin;
+        defaultText = literalExpression "pkgs.cyrus_sasl.bin";
+        type = types.package;
+        description = "Cyrus SASL package to use.";
+      };
+
+      mechanism = mkOption {
+        type = types.str;
+        default = "pam";
+        description = "Auth mechanism to use";
+      };
+
+      config = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Configuration to use for Cyrus SASL authentication daemon.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.saslauthd = {
+      description = "Cyrus SASL authentication daemon";
+
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = "@${cfg.package}/sbin/saslauthd saslauthd -a ${cfg.mechanism} -O ${pkgs.writeText "saslauthd.conf" cfg.config}";
+        Type = "forking";
+        PIDFile = "/run/saslauthd/saslauthd.pid";
+        Restart = "always";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/system/self-deploy.nix b/nixos/modules/services/system/self-deploy.nix
new file mode 100644
index 00000000000..d7130a13c73
--- /dev/null
+++ b/nixos/modules/services/system/self-deploy.nix
@@ -0,0 +1,173 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.self-deploy;
+
+  workingDirectory = "/var/lib/nixos-self-deploy";
+  repositoryDirectory = "${workingDirectory}/repo";
+  outPath = "${workingDirectory}/system";
+
+  gitWithRepo = "git -C ${repositoryDirectory}";
+
+  renderNixArgs = args:
+    let
+      toArg = key: value:
+        if builtins.isString value
+        then " --argstr ${lib.escapeShellArg key} ${lib.escapeShellArg value}"
+        else " --arg ${lib.escapeShellArg key} ${lib.escapeShellArg (toString value)}";
+    in
+    lib.concatStrings (lib.mapAttrsToList toArg args);
+
+  isPathType = x: lib.strings.isCoercibleToString x && builtins.substring 0 1 (toString x) == "/";
+
+in
+{
+  options.services.self-deploy = {
+    enable = lib.mkEnableOption "self-deploy";
+
+    nixFile = lib.mkOption {
+      type = lib.types.path;
+
+      default = "/default.nix";
+
+      description = ''
+        Path to nix file in repository. Leading '/' refers to root of
+        git repository.
+      '';
+    };
+
+    nixAttribute = lib.mkOption {
+      type = with lib.types; nullOr str;
+
+      default = null;
+
+      description = ''
+        Attribute of `nixFile` that builds the current system.
+      '';
+    };
+
+    nixArgs = lib.mkOption {
+      type = lib.types.attrs;
+
+      default = { };
+
+      description = ''
+        Arguments to `nix-build` passed as `--argstr` or `--arg` depending on
+        the type.
+      '';
+    };
+
+    switchCommand = lib.mkOption {
+      type = lib.types.enum [ "boot" "switch" "dry-activate" "test" ];
+
+      default = "switch";
+
+      description = ''
+        The `switch-to-configuration` subcommand used.
+      '';
+    };
+
+    repository = lib.mkOption {
+      type = with lib.types; oneOf [ path str ];
+
+      description = ''
+        The repository to fetch from. Must be properly formatted for git.
+
+        If this value is set to a path (must begin with `/`) then it's
+        assumed that the repository is local and the resulting service
+        won't wait for the network to be up.
+
+        If the repository will be fetched over SSH, you must add an
+        entry to `programs.ssh.knownHosts` for the SSH host for the fetch
+        to be successful.
+      '';
+    };
+
+    sshKeyFile = lib.mkOption {
+      type = with lib.types; nullOr path;
+
+      default = null;
+
+      description = ''
+        Path to SSH private key used to fetch private repositories over
+        SSH.
+      '';
+    };
+
+    branch = lib.mkOption {
+      type = lib.types.str;
+
+      default = "master";
+
+      description = ''
+        Branch to track
+
+        Technically speaking any ref can be specified here, as this is
+        passed directly to a `git fetch`, but for the use-case of
+        continuous deployment you're likely to want to specify a branch.
+      '';
+    };
+
+    startAt = lib.mkOption {
+      type = with lib.types; either str (listOf str);
+
+      default = "hourly";
+
+      description = ''
+        The schedule on which to run the `self-deploy` service. Format
+        specified by `systemd.time 7`.
+
+        This value can also be a list of `systemd.time 7` formatted
+        strings, in which case the service will be started on multiple
+        schedules.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.self-deploy = {
+      inherit (cfg) startAt;
+
+      wantedBy = [ "multi-user.target" ];
+
+      requires = lib.mkIf (!(isPathType cfg.repository)) [ "network-online.target" ];
+
+      environment.GIT_SSH_COMMAND = lib.mkIf (!(isNull cfg.sshKeyFile))
+        "${pkgs.openssh}/bin/ssh -i ${lib.escapeShellArg cfg.sshKeyFile}";
+
+      restartIfChanged = false;
+
+      path = with pkgs; [
+        git
+        nix
+      ] ++ lib.optionals (cfg.switchCommand == "boot") [ systemd ];
+
+      script = ''
+        if [ ! -e ${repositoryDirectory} ]; then
+          mkdir --parents ${repositoryDirectory}
+          git init ${repositoryDirectory}
+        fi
+
+        ${gitWithRepo} fetch ${lib.escapeShellArg cfg.repository} ${lib.escapeShellArg cfg.branch}
+
+        ${gitWithRepo} checkout FETCH_HEAD
+
+        nix-build${renderNixArgs cfg.nixArgs} ${lib.cli.toGNUCommandLineShell { } {
+          attr = cfg.nixAttribute;
+          out-link = outPath;
+        }} ${lib.escapeShellArg "${repositoryDirectory}${cfg.nixFile}"}
+
+        ${lib.optionalString (cfg.switchCommand != "test")
+          "nix-env --profile /nix/var/nix/profiles/system --set ${outPath}"}
+
+        ${outPath}/bin/switch-to-configuration ${cfg.switchCommand}
+
+        rm ${outPath}
+
+        ${gitWithRepo} gc --prune=all
+
+        ${lib.optionalString (cfg.switchCommand == "boot") "systemctl reboot"}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/system/systembus-notify.nix b/nixos/modules/services/system/systembus-notify.nix
new file mode 100644
index 00000000000..e918bc552ec
--- /dev/null
+++ b/nixos/modules/services/system/systembus-notify.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.systembus-notify;
+
+  inherit (lib) mkEnableOption mkIf;
+
+in
+{
+  options.services.systembus-notify = {
+    enable = mkEnableOption ''
+      System bus notification support
+
+      WARNING: enabling this option (while convenient) should *not* be done on a
+      machine where you do not trust the other users as it allows any other
+      local user to DoS your session by spamming notifications.
+    '';
+  };
+
+  config = mkIf cfg.enable {
+    systemd = {
+      packages = with pkgs; [ systembus-notify ];
+
+      user.services.systembus-notify.wantedBy = [ "graphical-session.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/system/uptimed.nix b/nixos/modules/services/system/uptimed.nix
new file mode 100644
index 00000000000..67a03876e19
--- /dev/null
+++ b/nixos/modules/services/system/uptimed.nix
@@ -0,0 +1,60 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.uptimed;
+  stateDir = "/var/lib/uptimed";
+in
+{
+  options = {
+    services.uptimed = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable <literal>uptimed</literal>, allowing you to track
+          your highest uptimes.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.uptimed ];
+
+    users.users.uptimed = {
+      description = "Uptimed daemon user";
+      home        = stateDir;
+      uid         = config.ids.uids.uptimed;
+      group       = "uptimed";
+    };
+    users.groups.uptimed = {};
+
+    systemd.services.uptimed = {
+      unitConfig.Documentation = "man:uptimed(8) man:uprecords(1)";
+      description = "uptimed service";
+      wantedBy    = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Restart                 = "on-failure";
+        User                    = "uptimed";
+        Nice                    = 19;
+        IOSchedulingClass       = "idle";
+        PrivateTmp              = "yes";
+        PrivateNetwork          = "yes";
+        NoNewPrivileges         = "yes";
+        StateDirectory          = [ "uptimed" ];
+        InaccessibleDirectories = "/home";
+        ExecStart               = "${pkgs.uptimed}/sbin/uptimed -f -p ${stateDir}/pid";
+      };
+
+      preStart = ''
+        if ! test -f ${stateDir}/bootid ; then
+          ${pkgs.uptimed}/sbin/uptimed -b
+        fi
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/torrent/deluge.nix b/nixos/modules/services/torrent/deluge.nix
new file mode 100644
index 00000000000..cb0da9e83b4
--- /dev/null
+++ b/nixos/modules/services/torrent/deluge.nix
@@ -0,0 +1,279 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.deluge;
+  cfg_web = config.services.deluge.web;
+  isDeluge1 = versionOlder cfg.package.version "2.0.0";
+
+  openFilesLimit = 4096;
+  listenPortsDefault = [ 6881 6889 ];
+
+  listToRange = x: { from = elemAt x 0; to = elemAt x 1; };
+
+  configDir = "${cfg.dataDir}/.config/deluge";
+  configFile = pkgs.writeText "core.conf" (builtins.toJSON cfg.config);
+  declarativeLockFile = "${configDir}/.declarative";
+
+  preStart = if cfg.declarative then ''
+    if [ -e ${declarativeLockFile} ]; then
+      # Was declarative before, no need to back up anything
+      ${if isDeluge1 then "ln -sf" else "cp"} ${configFile} ${configDir}/core.conf
+      ln -sf ${cfg.authFile} ${configDir}/auth
+    else
+      # Declarative for the first time, backup stateful files
+      ${if isDeluge1 then "ln -s" else "cp"} -b --suffix=.stateful ${configFile} ${configDir}/core.conf
+      ln -sb --suffix=.stateful ${cfg.authFile} ${configDir}/auth
+      echo "Autogenerated file that signifies that this server configuration is managed declaratively by NixOS" \
+        > ${declarativeLockFile}
+    fi
+  '' else ''
+    if [ -e ${declarativeLockFile} ]; then
+      rm ${declarativeLockFile}
+    fi
+  '';
+in {
+  options = {
+    services = {
+      deluge = {
+        enable = mkEnableOption "Deluge daemon";
+
+        openFilesLimit = mkOption {
+          default = openFilesLimit;
+          type = types.either types.int types.str;
+          description = ''
+            Number of files to allow deluged to open.
+          '';
+        };
+
+        config = mkOption {
+          type = types.attrs;
+          default = {};
+          example = literalExpression ''
+            {
+              download_location = "/srv/torrents/";
+              max_upload_speed = "1000.0";
+              share_ratio_limit = "2.0";
+              allow_remote = true;
+              daemon_port = 58846;
+              listen_ports = [ ${toString listenPortsDefault} ];
+            }
+          '';
+          description = ''
+            Deluge core configuration for the core.conf file. Only has an effect
+            when <option>services.deluge.declarative</option> is set to
+            <literal>true</literal>. String values must be quoted, integer and
+            boolean values must not. See
+            <link xlink:href="https://git.deluge-torrent.org/deluge/tree/deluge/core/preferencesmanager.py#n41"/>
+            for the availaible options.
+          '';
+        };
+
+        declarative = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether to use a declarative deluge configuration.
+            Only if set to <literal>true</literal>, the options
+            <option>services.deluge.config</option>,
+            <option>services.deluge.openFirewall</option> and
+            <option>services.deluge.authFile</option> will be
+            applied.
+          '';
+        };
+
+        openFirewall = mkOption {
+          default = false;
+          type = types.bool;
+          description = ''
+            Whether to open the firewall for the ports in
+            <option>services.deluge.config.listen_ports</option>. It only takes effet if
+            <option>services.deluge.declarative</option> is set to
+            <literal>true</literal>.
+
+            It does NOT apply to the daemon port nor the web UI port. To access those
+            ports secuerly check the documentation
+            <link xlink:href="https://dev.deluge-torrent.org/wiki/UserGuide/ThinClient#CreateSSHTunnel"/>
+            or use a VPN or configure certificates for deluge.
+          '';
+        };
+
+        dataDir = mkOption {
+          type = types.path;
+          default = "/var/lib/deluge";
+          description = ''
+            The directory where deluge will create files.
+          '';
+        };
+
+        authFile = mkOption {
+          type = types.path;
+          example = "/run/keys/deluge-auth";
+          description = ''
+            The file managing the authentication for deluge, the format of this
+            file is straightforward, each line contains a
+            username:password:level tuple in plaintext. It only has an effect
+            when <option>services.deluge.declarative</option> is set to
+            <literal>true</literal>.
+            See <link xlink:href="https://dev.deluge-torrent.org/wiki/UserGuide/Authentication"/> for
+            more informations.
+          '';
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "deluge";
+          description = ''
+            User account under which deluge runs.
+          '';
+        };
+
+        group = mkOption {
+          type = types.str;
+          default = "deluge";
+          description = ''
+            Group under which deluge runs.
+          '';
+        };
+
+        extraPackages = mkOption {
+          type = types.listOf types.package;
+          default = [];
+          description = ''
+            Extra packages available at runtime to enable Deluge's plugins. For example,
+            extraction utilities are required for the built-in "Extractor" plugin.
+            This always contains unzip, gnutar, xz and bzip2.
+          '';
+        };
+
+        package = mkOption {
+          type = types.package;
+          example = literalExpression "pkgs.deluge-2_x";
+          description = ''
+            Deluge package to use.
+          '';
+        };
+      };
+
+      deluge.web = {
+        enable = mkEnableOption "Deluge Web daemon";
+
+        port = mkOption {
+          type = types.port;
+          default = 8112;
+          description = ''
+            Deluge web UI port.
+          '';
+        };
+
+        openFirewall = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Open ports in the firewall for deluge web daemon
+          '';
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    services.deluge.package = mkDefault (
+      if versionAtLeast config.system.stateVersion "20.09" then
+        pkgs.deluge-2_x
+      else
+        # deluge-1_x is no longer packaged and this will resolve to an error
+        # thanks to the alias for this name.  This is left here so that anyone
+        # using NixOS older than 20.09 receives that error when they upgrade
+        # and is forced to make an intentional choice to switch to deluge-2_x.
+        # That might be slightly inconvenient but there is no path to
+        # downgrade from 2.x to 1.x so NixOS should not automatically perform
+        # this state migration.
+        pkgs.deluge-1_x
+    );
+
+    # Provide a default set of `extraPackages`.
+    services.deluge.extraPackages = with pkgs; [ unzip gnutar xz bzip2 ];
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group}"
+      "d '${cfg.dataDir}/.config' 0770 ${cfg.user} ${cfg.group}"
+      "d '${cfg.dataDir}/.config/deluge' 0770 ${cfg.user} ${cfg.group}"
+    ]
+    ++ optional (cfg.config ? download_location)
+      "d '${cfg.config.download_location}' 0770 ${cfg.user} ${cfg.group}"
+    ++ optional (cfg.config ? torrentfiles_location)
+      "d '${cfg.config.torrentfiles_location}' 0770 ${cfg.user} ${cfg.group}"
+    ++ optional (cfg.config ? move_completed_path)
+      "d '${cfg.config.move_completed_path}' 0770 ${cfg.user} ${cfg.group}";
+
+    systemd.services.deluged = {
+      after = [ "network.target" ];
+      description = "Deluge BitTorrent Daemon";
+      wantedBy = [ "multi-user.target" ];
+      path = [ cfg.package ] ++ cfg.extraPackages;
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.package}/bin/deluged \
+            --do-not-daemonize \
+            --config ${configDir}
+        '';
+        # To prevent "Quit & shutdown daemon" from working; we want systemd to
+        # manage it!
+        Restart = "on-success";
+        User = cfg.user;
+        Group = cfg.group;
+        UMask = "0002";
+        LimitNOFILE = cfg.openFilesLimit;
+      };
+      preStart = preStart;
+    };
+
+    systemd.services.delugeweb = mkIf cfg_web.enable {
+      after = [ "network.target" "deluged.service"];
+      requires = [ "deluged.service" ];
+      description = "Deluge BitTorrent WebUI";
+      wantedBy = [ "multi-user.target" ];
+      path = [ cfg.package ];
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.package}/bin/deluge-web \
+            ${optionalString (!isDeluge1) "--do-not-daemonize"} \
+            --config ${configDir} \
+            --port ${toString cfg.web.port}
+        '';
+        User = cfg.user;
+        Group = cfg.group;
+      };
+    };
+
+    networking.firewall = mkMerge [
+      (mkIf (cfg.declarative && cfg.openFirewall && !(cfg.config.random_port or true)) {
+        allowedTCPPortRanges = singleton (listToRange (cfg.config.listen_ports or listenPortsDefault));
+        allowedUDPPortRanges = singleton (listToRange (cfg.config.listen_ports or listenPortsDefault));
+      })
+      (mkIf (cfg.web.openFirewall) {
+        allowedTCPPorts = [ cfg.web.port ];
+      })
+    ];
+
+    environment.systemPackages = [ cfg.package ];
+
+    users.users = mkIf (cfg.user == "deluge") {
+      deluge = {
+        group = cfg.group;
+        uid = config.ids.uids.deluge;
+        home = cfg.dataDir;
+        description = "Deluge Daemon user";
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "deluge") {
+      deluge = {
+        gid = config.ids.gids.deluge;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/torrent/flexget.nix b/nixos/modules/services/torrent/flexget.nix
new file mode 100644
index 00000000000..e500e02d861
--- /dev/null
+++ b/nixos/modules/services/torrent/flexget.nix
@@ -0,0 +1,100 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.flexget;
+  pkg = pkgs.flexget;
+  ymlFile = pkgs.writeText "flexget.yml" ''
+    ${cfg.config}
+
+    ${optionalString cfg.systemScheduler "schedules: no"}
+'';
+  configFile = "${toString cfg.homeDir}/flexget.yml";
+in {
+  options = {
+    services.flexget = {
+      enable = mkEnableOption "Run FlexGet Daemon";
+
+      user = mkOption {
+        default = "deluge";
+        example = "some_user";
+        type = types.str;
+        description = "The user under which to run flexget.";
+      };
+
+      homeDir = mkOption {
+        default = "/var/lib/deluge";
+        example = "/home/flexget";
+        type = types.path;
+        description = "Where files live.";
+      };
+
+      interval = mkOption {
+        default = "10m";
+        example = "1h";
+        type = types.str;
+        description = "When to perform a <command>flexget</command> run. See <command>man 7 systemd.time</command> for the format.";
+      };
+
+      systemScheduler = mkOption {
+        default = true;
+        example = false;
+        type = types.bool;
+        description = "When true, execute the runs via the flexget-runner.timer. If false, you have to specify the settings yourself in the YML file.";
+      };
+
+      config = mkOption {
+        default = "";
+        type = types.lines;
+        description = "The YAML configuration for FlexGet.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkg ];
+
+    systemd.services = {
+      flexget = {
+        description = "FlexGet Daemon";
+        path = [ pkg ];
+        serviceConfig = {
+          User = cfg.user;
+          Environment = "TZ=${config.time.timeZone}";
+          ExecStartPre = "${pkgs.coreutils}/bin/install -m644 ${ymlFile} ${configFile}";
+          ExecStart = "${pkg}/bin/flexget -c ${configFile} daemon start";
+          ExecStop = "${pkg}/bin/flexget -c ${configFile} daemon stop";
+          ExecReload = "${pkg}/bin/flexget -c ${configFile} daemon reload";
+          Restart = "on-failure";
+          PrivateTmp = true;
+          WorkingDirectory = toString cfg.homeDir;
+        };
+        wantedBy = [ "multi-user.target" ];
+      };
+
+      flexget-runner = mkIf cfg.systemScheduler {
+        description = "FlexGet Runner";
+        after = [ "flexget.service" ];
+        wants = [ "flexget.service" ];
+        serviceConfig = {
+          User = cfg.user;
+          ExecStart = "${pkg}/bin/flexget -c ${configFile} execute";
+          PrivateTmp = true;
+          WorkingDirectory = toString cfg.homeDir;
+        };
+      };
+    };
+
+    systemd.timers.flexget-runner = mkIf cfg.systemScheduler {
+      description = "Run FlexGet every ${cfg.interval}";
+      wantedBy = [ "timers.target" ];
+      timerConfig = {
+        OnBootSec = "5m";
+        OnUnitInactiveSec = cfg.interval;
+        Unit = "flexget-runner.service";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/torrent/magnetico.nix b/nixos/modules/services/torrent/magnetico.nix
new file mode 100644
index 00000000000..3dd7b1ece76
--- /dev/null
+++ b/nixos/modules/services/torrent/magnetico.nix
@@ -0,0 +1,220 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.magnetico;
+
+  dataDir = "/var/lib/magnetico";
+
+  credFile = with cfg.web;
+    if credentialsFile != null
+      then credentialsFile
+      else pkgs.writeText "magnetico-credentials"
+        (concatStrings (mapAttrsToList
+          (user: hash: "${user}:${hash}\n")
+          cfg.web.credentials));
+
+  # default options in magneticod/main.go
+  dbURI = concatStrings
+    [ "sqlite3://${dataDir}/database.sqlite3"
+      "?_journal_mode=WAL"
+      "&_busy_timeout=3000"
+      "&_foreign_keys=true"
+    ];
+
+  crawlerArgs = with cfg.crawler; escapeShellArgs
+    ([ "--database=${dbURI}"
+       "--indexer-addr=${address}:${toString port}"
+       "--indexer-max-neighbors=${toString maxNeighbors}"
+       "--leech-max-n=${toString maxLeeches}"
+     ] ++ extraOptions);
+
+  webArgs = with cfg.web; escapeShellArgs
+    ([ "--database=${dbURI}"
+       (if (cfg.web.credentialsFile != null || cfg.web.credentials != { })
+         then "--credentials=${toString credFile}"
+         else "--no-auth")
+       "--addr=${address}:${toString port}"
+     ] ++ extraOptions);
+
+in {
+
+  ###### interface
+
+  options.services.magnetico = {
+    enable = mkEnableOption "Magnetico, Bittorrent DHT crawler";
+
+    crawler.address = mkOption {
+      type = types.str;
+      default = "0.0.0.0";
+      example = "1.2.3.4";
+      description = ''
+        Address to be used for indexing DHT nodes.
+      '';
+    };
+
+    crawler.port = mkOption {
+      type = types.port;
+      default = 0;
+      description = ''
+        Port to be used for indexing DHT nodes.
+        This port should be added to
+        <option>networking.firewall.allowedTCPPorts</option>.
+      '';
+    };
+
+    crawler.maxNeighbors = mkOption {
+      type = types.ints.positive;
+      default = 1000;
+      description = ''
+        Maximum number of simultaneous neighbors of an indexer.
+        Be careful changing this number: high values can very
+        easily cause your network to be congested or even crash
+        your router.
+      '';
+    };
+
+    crawler.maxLeeches = mkOption {
+      type = types.ints.positive;
+      default = 200;
+      description = ''
+        Maximum number of simultaneous leeches.
+      '';
+    };
+
+    crawler.extraOptions = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Extra command line arguments to pass to magneticod.
+      '';
+    };
+
+    web.address = mkOption {
+      type = types.str;
+      default = "localhost";
+      example = "1.2.3.4";
+      description = ''
+        Address the web interface will listen to.
+      '';
+    };
+
+    web.port = mkOption {
+      type = types.port;
+      default = 8080;
+      description = ''
+        Port the web interface will listen to.
+      '';
+    };
+
+    web.credentials = mkOption {
+      type = types.attrsOf types.str;
+      default = {};
+      example = lib.literalExpression ''
+        {
+          myuser = "$2y$12$YE01LZ8jrbQbx6c0s2hdZO71dSjn2p/O9XsYJpz.5968yCysUgiaG";
+        }
+      '';
+      description = ''
+        The credentials to access the web interface, in case authentication is
+        enabled, in the format <literal>username:hash</literal>. If unset no
+        authentication will be required.
+
+        Usernames must start with a lowercase ([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.  The
+        <command>htpasswd</command> tool from the <package>apacheHttpd
+        </package> package may be used to generate the hash: <command>htpasswd
+        -bnBC 12 username password</command>
+
+        <warning>
+        <para>
+          The hashes will be stored world-readable in the nix store.
+          Consider using the <literal>credentialsFile</literal> option if you
+          don't want this.
+        </para>
+        </warning>
+      '';
+    };
+
+    web.credentialsFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        The path to the file holding the credentials to access the web
+        interface. If unset no authentication will be required.
+
+        The file must constain user names and password hashes in the format
+        <literal>username:hash </literal>, 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.
+        The <command>htpasswd</command> tool from the <package>apacheHttpd
+        </package> package may be used to generate the hash:
+        <command>htpasswd -bnBC 12 username password</command>
+      '';
+    };
+
+    web.extraOptions = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Extra command line arguments to pass to magneticow.
+      '';
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.magnetico = {
+      description = "Magnetico daemons user";
+      group = "magnetico";
+      isSystemUser = true;
+    };
+    users.groups.magnetico = {};
+
+    systemd.services.magneticod = {
+      description = "Magnetico DHT crawler";
+      wantedBy = [ "multi-user.target" ];
+      after    = [ "network.target" ];
+
+      serviceConfig = {
+        User      = "magnetico";
+        Restart   = "on-failure";
+        ExecStart = "${pkgs.magnetico}/bin/magneticod ${crawlerArgs}";
+      };
+    };
+
+    systemd.services.magneticow = {
+      description = "Magnetico web interface";
+      wantedBy = [ "multi-user.target" ];
+      after    = [ "network.target" "magneticod.service"];
+
+      serviceConfig = {
+        User           = "magnetico";
+        StateDirectory = "magnetico";
+        Restart        = "on-failure";
+        ExecStart      = "${pkgs.magnetico}/bin/magneticow ${webArgs}";
+      };
+    };
+
+    assertions =
+    [
+      {
+        assertion = cfg.web.credentialsFile == null || cfg.web.credentials == { };
+        message = ''
+          The options services.magnetico.web.credentialsFile and
+          services.magnetico.web.credentials are mutually exclusives.
+        '';
+      }
+    ];
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+}
diff --git a/nixos/modules/services/torrent/opentracker.nix b/nixos/modules/services/torrent/opentracker.nix
new file mode 100644
index 00000000000..d76d61dfe85
--- /dev/null
+++ b/nixos/modules/services/torrent/opentracker.nix
@@ -0,0 +1,45 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.opentracker;
+in {
+  options.services.opentracker = {
+    enable = mkEnableOption "opentracker";
+
+    package = mkOption {
+      type = types.package;
+      description = ''
+        opentracker package to use
+      '';
+      default = pkgs.opentracker;
+      defaultText = literalExpression "pkgs.opentracker";
+    };
+
+    extraOptions = mkOption {
+      type = types.separatedString " ";
+      description = ''
+        Configuration Arguments for opentracker
+        See https://erdgeist.org/arts/software/opentracker/ for all params
+      '';
+      default = "";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    systemd.services.opentracker = {
+      description = "opentracker server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      restartIfChanged = true;
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/opentracker ${cfg.extraOptions}";
+        PrivateTmp = true;
+        WorkingDirectory = "/var/empty";
+        # By default opentracker drops all privileges and runs in chroot after starting up as root.
+      };
+    };
+  };
+}
+
diff --git a/nixos/modules/services/torrent/peerflix.nix b/nixos/modules/services/torrent/peerflix.nix
new file mode 100644
index 00000000000..821c829f6b4
--- /dev/null
+++ b/nixos/modules/services/torrent/peerflix.nix
@@ -0,0 +1,71 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.peerflix;
+  opt = options.services.peerflix;
+
+  configFile = pkgs.writeText "peerflix-config.json" ''
+    {
+      "connections": 50,
+      "tmp": "${cfg.downloadDir}"
+    }
+  '';
+
+in {
+
+  ###### interface
+
+  options.services.peerflix = {
+    enable = mkOption {
+      description = "Whether to enable peerflix service.";
+      default = false;
+      type = types.bool;
+    };
+
+    stateDir = mkOption {
+      description = "Peerflix state directory.";
+      default = "/var/lib/peerflix";
+      type = types.path;
+    };
+
+    downloadDir = mkOption {
+      description = "Peerflix temporary download directory.";
+      default = "${cfg.stateDir}/torrents";
+      defaultText = literalExpression ''"''${config.${opt.stateDir}}/torrents"'';
+      type = types.path;
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}' - peerflix - - -"
+    ];
+
+    systemd.services.peerflix = {
+      description = "Peerflix Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      environment.HOME = cfg.stateDir;
+
+      preStart = ''
+        mkdir -p "${cfg.stateDir}"/{torrents,.config/peerflix-server}
+        ln -fs "${configFile}" "${cfg.stateDir}/.config/peerflix-server/config.json"
+      '';
+
+      serviceConfig = {
+        ExecStart = "${pkgs.nodePackages.peerflix-server}/bin/peerflix-server";
+        User = "peerflix";
+      };
+    };
+
+    users.users.peerflix = {
+      isSystemUser = true;
+      group = "peerflix";
+    };
+    users.groups.peerflix = {};
+  };
+}
diff --git a/nixos/modules/services/torrent/rtorrent.nix b/nixos/modules/services/torrent/rtorrent.nix
new file mode 100644
index 00000000000..759dcfe2e6c
--- /dev/null
+++ b/nixos/modules/services/torrent/rtorrent.nix
@@ -0,0 +1,211 @@
+{ config, options, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.rtorrent;
+  opt = options.services.rtorrent;
+
+in {
+  options.services.rtorrent = {
+    enable = mkEnableOption "rtorrent";
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/rtorrent";
+      description = ''
+        The directory where rtorrent stores its data files.
+      '';
+    };
+
+    downloadDir = mkOption {
+      type = types.str;
+      default = "${cfg.dataDir}/download";
+      defaultText = literalExpression ''"''${config.${opt.dataDir}}/download"'';
+      description = ''
+        Where to put downloaded files.
+      '';
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = "rtorrent";
+      description = ''
+        User account under which rtorrent runs.
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "rtorrent";
+      description = ''
+        Group under which rtorrent runs.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.rtorrent;
+      defaultText = literalExpression "pkgs.rtorrent";
+      description = ''
+        The rtorrent package to use.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 50000;
+      description = ''
+        The rtorrent port.
+      '';
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to open the firewall for the port in <option>services.rtorrent.port</option>.
+      '';
+    };
+
+    rpcSocket = mkOption {
+      type = types.str;
+      readOnly = true;
+      default = "/run/rtorrent/rpc.sock";
+      description = ''
+        RPC socket path.
+      '';
+    };
+
+    configText = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        The content of <filename>rtorrent.rc</filename>. The <link xlink:href="https://rtorrent-docs.readthedocs.io/en/latest/cookbook.html#modernized-configuration-template">modernized configuration template</link> with the values specified in this module will be prepended using mkBefore. You can use mkForce to overwrite the config completly.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users.groups = mkIf (cfg.group == "rtorrent") {
+      rtorrent = {};
+    };
+
+    users.users = mkIf (cfg.user == "rtorrent") {
+      rtorrent = {
+        group = cfg.group;
+        shell = pkgs.bashInteractive;
+        home = cfg.dataDir;
+        description = "rtorrent Daemon user";
+        isSystemUser = true;
+      };
+    };
+
+    networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall) [ cfg.port ];
+
+    services.rtorrent.configText = mkBefore ''
+      # Instance layout (base paths)
+      method.insert = cfg.basedir, private|const|string, (cat,"${cfg.dataDir}/")
+      method.insert = cfg.watch,   private|const|string, (cat,(cfg.basedir),"watch/")
+      method.insert = cfg.logs,    private|const|string, (cat,(cfg.basedir),"log/")
+      method.insert = cfg.logfile, private|const|string, (cat,(cfg.logs),(system.time),".log")
+      method.insert = cfg.rpcsock, private|const|string, (cat,"${cfg.rpcSocket}")
+
+      # Create instance directories
+      execute.throw = sh, -c, (cat, "mkdir -p ", (cfg.basedir), "/session ", (cfg.watch), " ", (cfg.logs))
+
+      # Listening port for incoming peer traffic (fixed; you can also randomize it)
+      network.port_range.set = ${toString cfg.port}-${toString cfg.port}
+      network.port_random.set = no
+
+      # Tracker-less torrent and UDP tracker support
+      # (conservative settings for 'private' trackers, change for 'public')
+      dht.mode.set = disable
+      protocol.pex.set = no
+      trackers.use_udp.set = no
+
+      # Peer settings
+      throttle.max_uploads.set = 100
+      throttle.max_uploads.global.set = 250
+
+      throttle.min_peers.normal.set = 20
+      throttle.max_peers.normal.set = 60
+      throttle.min_peers.seed.set = 30
+      throttle.max_peers.seed.set = 80
+      trackers.numwant.set = 80
+
+      protocol.encryption.set = allow_incoming,try_outgoing,enable_retry
+
+      # Limits for file handle resources, this is optimized for
+      # an `ulimit` of 1024 (a common default). You MUST leave
+      # a ceiling of handles reserved for rTorrent's internal needs!
+      network.http.max_open.set = 50
+      network.max_open_files.set = 600
+      network.max_open_sockets.set = 3000
+
+      # Memory resource usage (increase if you have a large number of items loaded,
+      # and/or the available resources to spend)
+      pieces.memory.max.set = 1800M
+      network.xmlrpc.size_limit.set = 4M
+
+      # Basic operational settings (no need to change these)
+      session.path.set = (cat, (cfg.basedir), "session/")
+      directory.default.set = "${cfg.downloadDir}"
+      log.execute = (cat, (cfg.logs), "execute.log")
+      ##log.xmlrpc = (cat, (cfg.logs), "xmlrpc.log")
+      execute.nothrow = sh, -c, (cat, "echo >", (session.path), "rtorrent.pid", " ", (system.pid))
+
+      # Other operational settings (check & adapt)
+      encoding.add = utf8
+      system.umask.set = 0027
+      system.cwd.set = (cfg.basedir)
+      network.http.dns_cache_timeout.set = 25
+      schedule2 = monitor_diskspace, 15, 60, ((close_low_diskspace, 1000M))
+
+      # Watch directories (add more as you like, but use unique schedule names)
+      #schedule2 = watch_start, 10, 10, ((load.start, (cat, (cfg.watch), "start/*.torrent")))
+      #schedule2 = watch_load, 11, 10, ((load.normal, (cat, (cfg.watch), "load/*.torrent")))
+
+      # Logging:
+      #   Levels = critical error warn notice info debug
+      #   Groups = connection_* dht_* peer_* rpc_* storage_* thread_* tracker_* torrent_*
+      print = (cat, "Logging to ", (cfg.logfile))
+      log.open_file = "log", (cfg.logfile)
+      log.add_output = "info", "log"
+      ##log.add_output = "tracker_debug", "log"
+
+      # XMLRPC
+      scgi_local = (cfg.rpcsock)
+      schedule = scgi_group,0,0,"execute.nothrow=chown,\":rtorrent\",(cfg.rpcsock)"
+      schedule = scgi_permission,0,0,"execute.nothrow=chmod,\"g+w,o=\",(cfg.rpcsock)"
+    '';
+
+    systemd = {
+      services = {
+        rtorrent = let
+          rtorrentConfigFile = pkgs.writeText "rtorrent.rc" cfg.configText;
+        in {
+          description = "rTorrent system service";
+          after = [ "network.target" ];
+          path = [ cfg.package pkgs.bash ];
+          wantedBy = [ "multi-user.target" ];
+          serviceConfig = {
+            User = cfg.user;
+            Group = cfg.group;
+            Type = "simple";
+            Restart = "on-failure";
+            WorkingDirectory = cfg.dataDir;
+            ExecStartPre=''${pkgs.bash}/bin/bash -c "if test -e ${cfg.dataDir}/session/rtorrent.lock && test -z $(${pkgs.procps}/bin/pidof rtorrent); then rm -f ${cfg.dataDir}/session/rtorrent.lock; fi"'';
+            ExecStart="${cfg.package}/bin/rtorrent -n -o system.daemon.set=true -o import=${rtorrentConfigFile}";
+            RuntimeDirectory = "rtorrent";
+            RuntimeDirectoryMode = 755;
+          };
+        };
+      };
+
+      tmpfiles.rules = [ "d '${cfg.dataDir}' 0750 ${cfg.user} ${cfg.group} -" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
new file mode 100644
index 00000000000..d12d8aa2398
--- /dev/null
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -0,0 +1,487 @@
+{ config, lib, pkgs, options, ... }:
+
+with lib;
+
+let
+  cfg = config.services.transmission;
+  opt = options.services.transmission;
+  inherit (config.environment) etc;
+  apparmor = config.security.apparmor;
+  rootDir = "/run/transmission";
+  settingsDir = ".config/transmission-daemon";
+  downloadsDir = "Downloads";
+  incompleteDir = ".incomplete";
+  watchDir = "watchdir";
+  settingsFormat = pkgs.formats.json {};
+  settingsFile = settingsFormat.generate "settings.json" cfg.settings;
+in
+{
+  imports = [
+    (mkRenamedOptionModule ["services" "transmission" "port"]
+                           ["services" "transmission" "settings" "rpc-port"])
+    (mkAliasOptionModule ["services" "transmission" "openFirewall"]
+                         ["services" "transmission" "openPeerPorts"])
+  ];
+  options = {
+    services.transmission = {
+      enable = mkEnableOption ''the headless Transmission BitTorrent daemon.
+
+        Transmission daemon can be controlled via the RPC interface using
+        transmission-remote, the WebUI (http://127.0.0.1:9091/ by default),
+        or other clients like stig or tremc.
+
+        Torrents are downloaded to <xref linkend="opt-services.transmission.home"/>/${downloadsDir} by default and are
+        accessible to users in the "transmission" group'';
+
+      settings = mkOption {
+        description = ''
+          Settings whose options overwrite fields in
+          <literal>.config/transmission-daemon/settings.json</literal>
+          (each time the service starts).
+
+          See <link xlink:href="https://github.com/transmission/transmission/wiki/Editing-Configuration-Files">Transmission's Wiki</link>
+          for documentation of settings not explicitely covered by this module.
+        '';
+        default = {};
+        type = types.submodule {
+          freeformType = settingsFormat.type;
+          options.download-dir = mkOption {
+            type = types.path;
+            default = "${cfg.home}/${downloadsDir}";
+            defaultText = literalExpression ''"''${config.${opt.home}}/${downloadsDir}"'';
+            description = "Directory where to download torrents.";
+          };
+          options.incomplete-dir = mkOption {
+            type = types.path;
+            default = "${cfg.home}/${incompleteDir}";
+            defaultText = literalExpression ''"''${config.${opt.home}}/${incompleteDir}"'';
+            description = ''
+              When enabled with
+              services.transmission.home
+              <xref linkend="opt-services.transmission.settings.incomplete-dir-enabled"/>,
+              new torrents will download the files to this directory.
+              When complete, the files will be moved to download-dir
+              <xref linkend="opt-services.transmission.settings.download-dir"/>.
+            '';
+          };
+          options.incomplete-dir-enabled = mkOption {
+            type = types.bool;
+            default = true;
+            description = "";
+          };
+          options.message-level = mkOption {
+            type = types.ints.between 0 3;
+            default = 2;
+            description = "Set verbosity of transmission messages.";
+          };
+          options.peer-port = mkOption {
+            type = types.port;
+            default = 51413;
+            description = "The peer port to listen for incoming connections.";
+          };
+          options.peer-port-random-high = mkOption {
+            type = types.port;
+            default = 65535;
+            description = ''
+              The maximum peer port to listen to for incoming connections
+              when <xref linkend="opt-services.transmission.settings.peer-port-random-on-start"/> is enabled.
+            '';
+          };
+          options.peer-port-random-low = mkOption {
+            type = types.port;
+            default = 65535;
+            description = ''
+              The minimal peer port to listen to for incoming connections
+              when <xref linkend="opt-services.transmission.settings.peer-port-random-on-start"/> is enabled.
+            '';
+          };
+          options.peer-port-random-on-start = mkOption {
+            type = types.bool;
+            default = false;
+            description = "Randomize the peer port.";
+          };
+          options.rpc-bind-address = mkOption {
+            type = types.str;
+            default = "127.0.0.1";
+            example = "0.0.0.0";
+            description = ''
+              Where to listen for RPC connections.
+              Use \"0.0.0.0\" to listen on all interfaces.
+            '';
+          };
+          options.rpc-port = mkOption {
+            type = types.port;
+            default = 9091;
+            description = "The RPC port to listen to.";
+          };
+          options.script-torrent-done-enabled = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Whether to run
+              <xref linkend="opt-services.transmission.settings.script-torrent-done-filename"/>
+              at torrent completion.
+            '';
+          };
+          options.script-torrent-done-filename = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            description = "Executable to be run at torrent completion.";
+          };
+          options.umask = mkOption {
+            type = types.int;
+            default = 2;
+            description = ''
+              Sets transmission's file mode creation mask.
+              See the umask(2) manpage for more information.
+              Users who want their saved torrents to be world-writable
+              may want to set this value to 0.
+              Bear in mind that the json markup language only accepts numbers in base 10,
+              so the standard umask(2) octal notation "022" is written in settings.json as 18.
+            '';
+          };
+          options.utp-enabled = mkOption {
+            type = types.bool;
+            default = true;
+            description = ''
+              Whether to enable <link xlink:href="http://en.wikipedia.org/wiki/Micro_Transport_Protocol">Micro Transport Protocol (µTP)</link>.
+            '';
+          };
+          options.watch-dir = mkOption {
+            type = types.path;
+            default = "${cfg.home}/${watchDir}";
+            defaultText = literalExpression ''"''${config.${opt.home}}/${watchDir}"'';
+            description = "Watch a directory for torrent files and add them to transmission.";
+          };
+          options.watch-dir-enabled = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''Whether to enable the
+              <xref linkend="opt-services.transmission.settings.watch-dir"/>.
+            '';
+          };
+          options.trash-original-torrent-files = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''Whether to delete torrents added from the
+              <xref linkend="opt-services.transmission.settings.watch-dir"/>.
+            '';
+          };
+        };
+      };
+
+      downloadDirPermissions = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        example = "770";
+        description = ''
+          If not <code>null</code>, is used as the permissions
+          set by <literal>systemd.activationScripts.transmission-daemon</literal>
+          on the directories <xref linkend="opt-services.transmission.settings.download-dir"/>,
+          <xref linkend="opt-services.transmission.settings.incomplete-dir"/>.
+          and <xref linkend="opt-services.transmission.settings.watch-dir"/>.
+          Note that you may also want to change
+          <xref linkend="opt-services.transmission.settings.umask"/>.
+        '';
+      };
+
+      home = mkOption {
+        type = types.path;
+        default = "/var/lib/transmission";
+        description = ''
+          The directory where Transmission will create <literal>${settingsDir}</literal>.
+          as well as <literal>${downloadsDir}/</literal> unless
+          <xref linkend="opt-services.transmission.settings.download-dir"/> is changed,
+          and <literal>${incompleteDir}/</literal> unless
+          <xref linkend="opt-services.transmission.settings.incomplete-dir"/> is changed.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "transmission";
+        description = "User account under which Transmission runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "transmission";
+        description = "Group account under which Transmission runs.";
+      };
+
+      credentialsFile = mkOption {
+        type = types.path;
+        description = ''
+          Path to a JSON file to be merged with the settings.
+          Useful to merge a file which is better kept out of the Nix store
+          to set secret config parameters like <code>rpc-password</code>.
+        '';
+        default = "/dev/null";
+        example = "/var/lib/secrets/transmission/settings.json";
+      };
+
+      extraFlags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "--log-debug" ];
+        description = ''
+          Extra flags passed to the transmission command in the service definition.
+        '';
+      };
+
+      openPeerPorts = mkEnableOption "opening of the peer port(s) in the firewall";
+
+      openRPCPort = mkEnableOption "opening of the RPC port in the firewall";
+
+      performanceNetParameters = mkEnableOption ''tweaking of kernel parameters
+        to open many more connections at the same time.
+
+        Note that you may also want to increase
+        <code>peer-limit-global"</code>.
+        And be aware that these settings are quite aggressive
+        and might not suite your regular desktop use.
+        For instance, SSH sessions may time out more easily'';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # Note that using systemd.tmpfiles would not work here
+    # because it would fail when creating a directory
+    # with a different owner than its parent directory, by saying:
+    # Detected unsafe path transition /home/foo → /home/foo/Downloads during canonicalization of /home/foo/Downloads
+    # when /home/foo is not owned by cfg.user.
+    # Note also that using an ExecStartPre= wouldn't work either
+    # because BindPaths= needs these directories before.
+    system.activationScripts = mkIf (cfg.downloadDirPermissions != null)
+      { transmission-daemon = ''
+        install -d -m 700 '${cfg.home}/${settingsDir}'
+        chown -R '${cfg.user}:${cfg.group}' ${cfg.home}/${settingsDir}
+        install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.download-dir}'
+        '' + optionalString cfg.settings.incomplete-dir-enabled ''
+        install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.incomplete-dir}'
+        '' + optionalString cfg.settings.watch-dir-enabled ''
+        install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.watch-dir}'
+        '';
+      };
+
+    systemd.services.transmission = {
+      description = "Transmission BitTorrent Service";
+      after = [ "network.target" ] ++ optional apparmor.enable "apparmor.service";
+      requires = optional apparmor.enable "apparmor.service";
+      wantedBy = [ "multi-user.target" ];
+      environment.CURL_CA_BUNDLE = etc."ssl/certs/ca-certificates.crt".source;
+
+      serviceConfig = {
+        # Use "+" because credentialsFile may not be accessible to User= or Group=.
+        ExecStartPre = [("+" + pkgs.writeShellScript "transmission-prestart" ''
+          set -eu${lib.optionalString (cfg.settings.message-level >= 3) "x"}
+          ${pkgs.jq}/bin/jq --slurp add ${settingsFile} '${cfg.credentialsFile}' |
+          install -D -m 600 -o '${cfg.user}' -g '${cfg.group}' /dev/stdin \
+           '${cfg.home}/${settingsDir}/settings.json'
+        '')];
+        ExecStart="${pkgs.transmission}/bin/transmission-daemon -f -g ${cfg.home}/${settingsDir} ${escapeShellArgs cfg.extraFlags}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        User = cfg.user;
+        Group = cfg.group;
+        # Create rootDir in the host's mount namespace.
+        RuntimeDirectory = [(baseNameOf rootDir)];
+        RuntimeDirectoryMode = "755";
+        # This is for BindPaths= and BindReadOnlyPaths=
+        # to allow traversal of directories they create in RootDirectory=.
+        UMask = "0066";
+        # Using RootDirectory= makes it possible
+        # to use the same paths download-dir/incomplete-dir
+        # (which appear in user's interfaces) without requiring cfg.user
+        # to have access to their parent directories,
+        # by using BindPaths=/BindReadOnlyPaths=.
+        # Note that TemporaryFileSystem= could have been used instead
+        # but not without adding some BindPaths=/BindReadOnlyPaths=
+        # that would only be needed for ExecStartPre=,
+        # because RootDirectoryStartOnly=true would not help.
+        RootDirectory = rootDir;
+        RootDirectoryStartOnly = true;
+        MountAPIVFS = true;
+        BindPaths =
+          [ "${cfg.home}/${settingsDir}"
+            cfg.settings.download-dir
+          ] ++
+          optional cfg.settings.incomplete-dir-enabled
+            cfg.settings.incomplete-dir ++
+          optional (cfg.settings.watch-dir-enabled && cfg.settings.trash-original-torrent-files)
+            cfg.settings.watch-dir;
+        BindReadOnlyPaths = [
+          # No confinement done of /nix/store here like in systemd-confinement.nix,
+          # an AppArmor profile is provided to get a confinement based upon paths and rights.
+          builtins.storeDir
+          "/etc"
+          "/run"
+          ] ++
+          optional (cfg.settings.script-torrent-done-enabled &&
+                    cfg.settings.script-torrent-done-filename != null)
+            cfg.settings.script-torrent-done-filename ++
+          optional (cfg.settings.watch-dir-enabled && !cfg.settings.trash-original-torrent-files)
+            cfg.settings.watch-dir;
+        StateDirectory = [
+          "transmission"
+          "transmission/.config/transmission-daemon"
+          "transmission/.incomplete"
+          "transmission/Downloads"
+          "transmission/watch-dir"
+        ];
+        StateDirectoryMode = mkDefault 750;
+        # The following options are only for optimizing:
+        # systemd-analyze security transmission
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateNetwork = mkDefault false;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        # ProtectHome=true would not allow BindPaths= to work accross /home,
+        # and ProtectHome=tmpfs would break statfs(),
+        # preventing transmission-daemon to report the available free space.
+        # However, RootDirectory= is used, so this is not a security concern
+        # since there would be nothing in /home but any BindPaths= wanted by the user.
+        ProtectHome = "read-only";
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        # AF_UNIX may become usable one day:
+        # https://github.com/transmission/transmission/issues/441
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallFilter = [
+          "@system-service"
+          # Groups in @system-service which do not contain a syscall
+          # listed by perf stat -e 'syscalls:sys_enter_*' transmission-daemon -f
+          # in tests, and seem likely not necessary for transmission-daemon.
+          "~@aio" "~@chown" "~@keyring" "~@memlock" "~@resources" "~@setuid" "~@timer"
+          # In the @privileged group, but reached when querying infos through RPC (eg. with stig).
+          "quotactl"
+        ];
+        SystemCallArchitectures = "native";
+      };
+    };
+
+    # It's useful to have transmission in path, e.g. for remote control
+    environment.systemPackages = [ pkgs.transmission ];
+
+    users.users = optionalAttrs (cfg.user == "transmission") ({
+      transmission = {
+        group = cfg.group;
+        uid = config.ids.uids.transmission;
+        description = "Transmission BitTorrent user";
+        home = cfg.home;
+      };
+    });
+
+    users.groups = optionalAttrs (cfg.group == "transmission") ({
+      transmission = {
+        gid = config.ids.gids.transmission;
+      };
+    });
+
+    networking.firewall = mkMerge [
+      (mkIf cfg.openPeerPorts (
+        if cfg.settings.peer-port-random-on-start
+        then
+          { allowedTCPPortRanges =
+              [ { from = cfg.settings.peer-port-random-low;
+                  to   = cfg.settings.peer-port-random-high;
+                }
+              ];
+            allowedUDPPortRanges =
+              [ { from = cfg.settings.peer-port-random-low;
+                  to   = cfg.settings.peer-port-random-high;
+                }
+              ];
+          }
+        else
+          { allowedTCPPorts = [ cfg.settings.peer-port ];
+            allowedUDPPorts = [ cfg.settings.peer-port ];
+          }
+      ))
+      (mkIf cfg.openRPCPort { allowedTCPPorts = [ cfg.settings.rpc-port ]; })
+    ];
+
+    boot.kernel.sysctl = mkMerge [
+      # Transmission uses a single UDP socket in order to implement multiple uTP sockets,
+      # and thus expects large kernel buffers for the UDP socket,
+      # https://trac.transmissionbt.com/browser/trunk/libtransmission/tr-udp.c?rev=11956.
+      # at least up to the values hardcoded here:
+      (mkIf cfg.settings.utp-enabled {
+        "net.core.rmem_max" = mkDefault "4194304"; # 4MB
+        "net.core.wmem_max" = mkDefault "1048576"; # 1MB
+      })
+      (mkIf cfg.performanceNetParameters {
+        # Increase the number of available source (local) TCP and UDP ports to 49151.
+        # Usual default is 32768 60999, ie. 28231 ports.
+        # Find out your current usage with: ss -s
+        "net.ipv4.ip_local_port_range" = mkDefault "16384 65535";
+        # Timeout faster generic TCP states.
+        # Usual default is 600.
+        # Find out your current usage with: watch -n 1 netstat -nptuo
+        "net.netfilter.nf_conntrack_generic_timeout" = mkDefault 60;
+        # Timeout faster established but inactive connections.
+        # Usual default is 432000.
+        "net.netfilter.nf_conntrack_tcp_timeout_established" = mkDefault 600;
+        # Clear immediately TCP states after timeout.
+        # Usual default is 120.
+        "net.netfilter.nf_conntrack_tcp_timeout_time_wait" = mkDefault 1;
+        # Increase the number of trackable connections.
+        # Usual default is 262144.
+        # Find out your current usage with: conntrack -C
+        "net.netfilter.nf_conntrack_max" = mkDefault 1048576;
+      })
+    ];
+
+    security.apparmor.policies."bin.transmission-daemon".profile = ''
+      include "${pkgs.transmission.apparmor}/bin.transmission-daemon"
+    '';
+    security.apparmor.includes."local/bin.transmission-daemon" = ''
+      r ${config.systemd.services.transmission.environment.CURL_CA_BUNDLE},
+
+      owner rw ${cfg.home}/${settingsDir}/**,
+      rw ${cfg.settings.download-dir}/**,
+      ${optionalString cfg.settings.incomplete-dir-enabled ''
+        rw ${cfg.settings.incomplete-dir}/**,
+      ''}
+      ${optionalString cfg.settings.watch-dir-enabled ''
+        r${optionalString cfg.settings.trash-original-torrent-files "w"} ${cfg.settings.watch-dir}/**,
+      ''}
+      profile dirs {
+        rw ${cfg.settings.download-dir}/**,
+        ${optionalString cfg.settings.incomplete-dir-enabled ''
+          rw ${cfg.settings.incomplete-dir}/**,
+        ''}
+        ${optionalString cfg.settings.watch-dir-enabled ''
+          r${optionalString cfg.settings.trash-original-torrent-files "w"} ${cfg.settings.watch-dir}/**,
+        ''}
+      }
+
+      ${optionalString (cfg.settings.script-torrent-done-enabled &&
+                        cfg.settings.script-torrent-done-filename != null) ''
+        # Stack transmission_directories profile on top of
+        # any existing profile for script-torrent-done-filename
+        # FIXME: to be tested as I'm not sure it works well with NoNewPrivileges=
+        # https://gitlab.com/apparmor/apparmor/-/wikis/AppArmorStacking#seccomp-and-no_new_privs
+        px ${cfg.settings.script-torrent-done-filename} -> &@{dirs},
+      ''}
+    '';
+  };
+
+  meta.maintainers = with lib.maintainers; [ julm ];
+}
diff --git a/nixos/modules/services/ttys/getty.nix b/nixos/modules/services/ttys/getty.nix
new file mode 100644
index 00000000000..7021a2c80f8
--- /dev/null
+++ b/nixos/modules/services/ttys/getty.nix
@@ -0,0 +1,162 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.getty;
+
+  baseArgs = [
+    "--login-program" "${cfg.loginProgram}"
+  ] ++ optionals (cfg.autologinUser != null) [
+    "--autologin" cfg.autologinUser
+  ] ++ optionals (cfg.loginOptions != null) [
+    "--login-options" cfg.loginOptions
+  ] ++ cfg.extraArgs;
+
+  gettyCmd = args:
+    "@${pkgs.util-linux}/sbin/agetty agetty ${escapeShellArgs baseArgs} ${args}";
+
+in
+
+{
+
+  ###### interface
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "mingetty" ] [ "services" "getty" ])
+    (mkRemovedOptionModule [ "services" "getty" "serialSpeed" ] ''set non-standard baudrates with `boot.kernelParams` i.e. boot.kernelParams = ["console=ttyS2,1500000"];'')
+  ];
+
+  options = {
+
+    services.getty = {
+
+      autologinUser = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Username of the account that will be automatically logged in at the console.
+          If unspecified, a login prompt is shown as usual.
+        '';
+      };
+
+      loginProgram = mkOption {
+        type = types.path;
+        default = "${pkgs.shadow}/bin/login";
+        defaultText = literalExpression ''"''${pkgs.shadow}/bin/login"'';
+        description = ''
+          Path to the login binary executed by agetty.
+        '';
+      };
+
+      loginOptions = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Template for arguments to be passed to
+          <citerefentry><refentrytitle>login</refentrytitle>
+          <manvolnum>1</manvolnum></citerefentry>.
+
+          See <citerefentry><refentrytitle>agetty</refentrytitle>
+          <manvolnum>1</manvolnum></citerefentry> for details,
+          including security considerations.  If unspecified, agetty
+          will not be invoked with a <option>--login-options</option>
+          option.
+        '';
+        example = "-h darkstar -- \\u";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = ''
+          Additional arguments passed to agetty.
+        '';
+        example = [ "--nohostname" ];
+      };
+
+      greetingLine = mkOption {
+        type = types.str;
+        description = ''
+          Welcome line printed by agetty.
+          The default shows current NixOS version label, machine type and tty.
+        '';
+      };
+
+      helpLine = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Help line printed by agetty below the welcome line.
+          Used by the installation CD to give some hints on
+          how to proceed.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = {
+    # Note: this is set here rather than up there so that changing
+    # nixos.label would not rebuild manual pages
+    services.getty.greetingLine = mkDefault ''<<< Welcome to NixOS ${config.system.nixos.label} (\m) - \l >>>'';
+
+    systemd.services."getty@" =
+      { serviceConfig.ExecStart = [
+          "" # override upstream default with an empty ExecStart
+          (gettyCmd "--noclear --keep-baud %I 115200,38400,9600 $TERM")
+        ];
+        restartIfChanged = false;
+      };
+
+    systemd.services."serial-getty@" =
+      { serviceConfig.ExecStart = [
+          "" # override upstream default with an empty ExecStart
+          (gettyCmd "%I --keep-baud $TERM")
+        ];
+        restartIfChanged = false;
+      };
+
+    systemd.services."autovt@" =
+      { serviceConfig.ExecStart = [
+          "" # override upstream default with an empty ExecStart
+          (gettyCmd "--noclear %I $TERM")
+        ];
+        restartIfChanged = false;
+      };
+
+    systemd.services."container-getty@" =
+      { serviceConfig.ExecStart = [
+          "" # override upstream default with an empty ExecStart
+          (gettyCmd "--noclear --keep-baud pts/%I 115200,38400,9600 $TERM")
+        ];
+        restartIfChanged = false;
+      };
+
+    systemd.services.console-getty =
+      { serviceConfig.ExecStart = [
+          "" # override upstream default with an empty ExecStart
+          (gettyCmd "--noclear --keep-baud console 115200,38400,9600 $TERM")
+        ];
+        serviceConfig.Restart = "always";
+        restartIfChanged = false;
+        enable = mkDefault config.boot.isContainer;
+      };
+
+    environment.etc.issue =
+      { # Friendly greeting on the virtual consoles.
+        source = pkgs.writeText "issue" ''
+
+          ${config.services.getty.greetingLine}
+          ${config.services.getty.helpLine}
+
+        '';
+      };
+
+  };
+
+}
diff --git a/nixos/modules/services/ttys/gpm.nix b/nixos/modules/services/ttys/gpm.nix
new file mode 100644
index 00000000000..308a6d3643a
--- /dev/null
+++ b/nixos/modules/services/ttys/gpm.nix
@@ -0,0 +1,57 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.gpm;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.gpm = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable GPM, the General Purpose Mouse daemon,
+          which enables mouse support in virtual consoles.
+        '';
+      };
+
+      protocol = mkOption {
+        type = types.str;
+        default = "ps/2";
+        description = "Mouse protocol to use.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.gpm =
+      { description = "Console Mouse Daemon";
+
+        wantedBy = [ "multi-user.target" ];
+        requires = [ "dev-input-mice.device" ];
+        after = [ "dev-input-mice.device" ];
+
+        serviceConfig.ExecStart = "@${pkgs.gpm}/sbin/gpm gpm -m /dev/input/mice -t ${cfg.protocol}";
+        serviceConfig.Type = "forking";
+        serviceConfig.PIDFile = "/run/gpm.pid";
+      };
+
+  };
+
+}
diff --git a/nixos/modules/services/ttys/kmscon.nix b/nixos/modules/services/ttys/kmscon.nix
new file mode 100644
index 00000000000..4fe720bf044
--- /dev/null
+++ b/nixos/modules/services/ttys/kmscon.nix
@@ -0,0 +1,97 @@
+{ config, pkgs, lib, ... }:
+let
+  inherit (lib) mkOption types mkIf;
+
+  cfg = config.services.kmscon;
+
+  autologinArg = lib.optionalString (cfg.autologinUser != null) "-f ${cfg.autologinUser}";
+
+  configDir = pkgs.writeTextFile { name = "kmscon-config"; destination = "/kmscon.conf"; text = cfg.extraConfig; };
+in {
+  options = {
+    services.kmscon = {
+      enable = mkOption {
+        description = ''
+          Use kmscon as the virtual console instead of gettys.
+          kmscon is a kms/dri-based userspace virtual terminal implementation.
+          It supports a richer feature set than the standard linux console VT,
+          including full unicode support, and when the video card supports drm
+          should be much faster.
+        '';
+        type = types.bool;
+        default = false;
+      };
+
+      hwRender = mkOption {
+        description = "Whether to use 3D hardware acceleration to render the console.";
+        type = types.bool;
+        default = false;
+      };
+
+      extraConfig = mkOption {
+        description = "Extra contents of the kmscon.conf file.";
+        type = types.lines;
+        default = "";
+        example = "font-size=14";
+      };
+
+      extraOptions = mkOption {
+        description = "Extra flags to pass to kmscon.";
+        type = types.separatedString " ";
+        default = "";
+        example = "--term xterm-256color";
+      };
+
+      autologinUser = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Username of the account that will be automatically logged in at the console.
+          If unspecified, a login prompt is shown as usual.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # Largely copied from unit provided with kmscon source
+    systemd.units."kmsconvt@.service".text = ''
+      [Unit]
+      Description=KMS System Console on %I
+      Documentation=man:kmscon(1)
+      After=systemd-user-sessions.service
+      After=plymouth-quit-wait.service
+      After=systemd-logind.service
+      After=systemd-vconsole-setup.service
+      Requires=systemd-logind.service
+      Before=getty.target
+      Conflicts=getty@%i.service
+      OnFailure=getty@%i.service
+      IgnoreOnIsolate=yes
+      ConditionPathExists=/dev/tty0
+
+      [Service]
+      ExecStart=
+      ExecStart=${pkgs.kmscon}/bin/kmscon "--vt=%I" ${cfg.extraOptions} --seats=seat0 --no-switchvt --configdir ${configDir} --login -- ${pkgs.shadow}/bin/login -p ${autologinArg}
+      UtmpIdentifier=%I
+      TTYPath=/dev/%I
+      TTYReset=yes
+      TTYVHangup=yes
+      TTYVTDisallocate=yes
+
+      X-RestartIfChanged=false
+    '';
+
+    systemd.suppressedSystemUnits = [ "autovt@.service" ];
+    systemd.units."kmsconvt@.service".aliases = [ "autovt@.service" ];
+
+    systemd.services.systemd-vconsole-setup.enable = false;
+
+    services.kmscon.extraConfig = mkIf cfg.hwRender ''
+      drm
+      hwaccel
+    '';
+
+    hardware.opengl.enable = mkIf cfg.hwRender true;
+  };
+}
diff --git a/nixos/modules/services/video/epgstation/default.nix b/nixos/modules/services/video/epgstation/default.nix
new file mode 100644
index 00000000000..191f6eb52e5
--- /dev/null
+++ b/nixos/modules/services/video/epgstation/default.nix
@@ -0,0 +1,334 @@
+{ config, lib, options, pkgs, ... }:
+
+let
+  cfg = config.services.epgstation;
+  opt = options.services.epgstation;
+
+  description = "EPGStation: DVR system for Mirakurun-managed TV tuners";
+
+  username = config.users.users.epgstation.name;
+  groupname = config.users.users.epgstation.group;
+  mirakurun = {
+    sock = config.services.mirakurun.unixSocket;
+    option = options.services.mirakurun.unixSocket;
+  };
+
+  yaml = pkgs.formats.yaml { };
+  settingsTemplate = yaml.generate "config.yml" cfg.settings;
+  preStartScript = pkgs.writeScript "epgstation-prestart" ''
+    #!${pkgs.runtimeShell}
+
+    DB_PASSWORD_FILE=${lib.escapeShellArg cfg.database.passwordFile}
+
+    if [[ ! -f "$DB_PASSWORD_FILE" ]]; then
+      printf "[FATAL] File containing the DB password was not found in '%s'. Double check the NixOS option '%s'." \
+        "$DB_PASSWORD_FILE" ${lib.escapeShellArg opt.database.passwordFile} >&2
+      exit 1
+    fi
+
+    DB_PASSWORD="$(head -n1 ${lib.escapeShellArg cfg.database.passwordFile})"
+
+    # setup configuration
+    touch /etc/epgstation/config.yml
+    chmod 640 /etc/epgstation/config.yml
+    sed \
+      -e "s,@dbPassword@,$DB_PASSWORD,g" \
+      ${settingsTemplate} > /etc/epgstation/config.yml
+    chown "${username}:${groupname}" /etc/epgstation/config.yml
+
+    # NOTE: Use password authentication, since mysqljs does not yet support auth_socket
+    if [ ! -e /var/lib/epgstation/db-created ]; then
+      ${pkgs.mariadb}/bin/mysql -e \
+        "GRANT ALL ON \`${cfg.database.name}\`.* TO '${username}'@'localhost' IDENTIFIED by '$DB_PASSWORD';"
+      touch /var/lib/epgstation/db-created
+    fi
+  '';
+
+  streamingConfig = lib.importJSON ./streaming.json;
+  logConfig = yaml.generate "logConfig.yml" {
+    appenders.stdout.type = "stdout";
+    categories = {
+      default = { appenders = [ "stdout" ]; level = "info"; };
+      system = { appenders = [ "stdout" ]; level = "info"; };
+      access = { appenders = [ "stdout" ]; level = "info"; };
+      stream = { appenders = [ "stdout" ]; level = "info"; };
+    };
+  };
+
+  # Deprecate top level options that are redundant.
+  deprecateTopLevelOption = config:
+    lib.mkRenamedOptionModule
+      ([ "services" "epgstation" ] ++ config)
+      ([ "services" "epgstation" "settings" ] ++ config);
+
+  removeOption = config: instruction:
+    lib.mkRemovedOptionModule
+      ([ "services" "epgstation" ] ++ config)
+      instruction;
+in
+{
+  meta.maintainers = with lib.maintainers; [ midchildan ];
+
+  imports = [
+    (deprecateTopLevelOption [ "port" ])
+    (deprecateTopLevelOption [ "socketioPort" ])
+    (deprecateTopLevelOption [ "clientSocketioPort" ])
+    (removeOption [ "basicAuth" ]
+      "Use a TLS-terminated reverse proxy with authentication instead.")
+  ];
+
+  options.services.epgstation = {
+    enable = lib.mkEnableOption description;
+
+    package = lib.mkOption {
+      default = pkgs.epgstation;
+      type = lib.types.package;
+      defaultText = lib.literalExpression "pkgs.epgstation";
+      description = "epgstation package to use";
+    };
+
+    usePreconfiguredStreaming = lib.mkOption {
+      type = lib.types.bool;
+      default = true;
+      description = ''
+        Use preconfigured default streaming options.
+
+        Upstream defaults:
+        <link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/config/config.yml.template"/>
+      '';
+    };
+
+    openFirewall = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = ''
+        Open ports in the firewall for the EPGStation web interface.
+
+        <warning>
+          <para>
+            Exposing EPGStation to the open internet is generally advised
+            against. Only use it inside a trusted local network, or consider
+            putting it behind a VPN if you want remote access.
+          </para>
+        </warning>
+      '';
+    };
+
+    database = {
+      name = lib.mkOption {
+        type = lib.types.str;
+        default = "epgstation";
+        description = ''
+          Name of the MySQL database that holds EPGStation's data.
+        '';
+      };
+
+      passwordFile = lib.mkOption {
+        type = lib.types.path;
+        example = "/run/keys/epgstation-db-password";
+        description = ''
+          A file containing the password for the database named
+          <option>database.name</option>.
+        '';
+      };
+    };
+
+    # The defaults for some options come from the upstream template
+    # configuration, which is the one that users would get if they follow the
+    # upstream instructions. This is, in some cases, different from the
+    # application defaults. Some options like encodeProcessNum and
+    # concurrentEncodeNum doesn't have an optimal default value that works for
+    # all hardware setups and/or performance requirements. For those kind of
+    # options, the application default wouldn't always result in the expected
+    # out-of-the-box behavior because it's the responsibility of the user to
+    # configure them according to their needs. In these cases, the value in the
+    # upstream template configuration should serve as a "good enough" default.
+    settings = lib.mkOption {
+      description = ''
+        Options to add to config.yml.
+
+        Documentation:
+        <link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/doc/conf-manual.md"/>
+      '';
+
+      default = { };
+      example = {
+        recPriority = 20;
+        conflictPriority = 10;
+      };
+
+      type = lib.types.submodule {
+        freeformType = yaml.type;
+
+        options.port = lib.mkOption {
+          type = lib.types.port;
+          default = 20772;
+          description = ''
+            HTTP port for EPGStation to listen on.
+          '';
+        };
+
+        options.socketioPort = lib.mkOption {
+          type = lib.types.port;
+          default = cfg.settings.port + 1;
+          defaultText = lib.literalExpression "config.${opt.settings}.port + 1";
+          description = ''
+            Socket.io port for EPGStation to listen on. It is valid to share
+            ports with <option>${opt.settings}.port</option>.
+          '';
+        };
+
+        options.clientSocketioPort = lib.mkOption {
+          type = lib.types.port;
+          default = cfg.settings.socketioPort;
+          defaultText = lib.literalExpression "config.${opt.settings}.socketioPort";
+          description = ''
+            Socket.io port that the web client is going to connect to. This may
+            be different from <option>${opt.settings}.socketioPort</option> if
+            EPGStation is hidden behind a reverse proxy.
+          '';
+        };
+
+        options.mirakurunPath = with mirakurun; lib.mkOption {
+          type = lib.types.str;
+          default = "http+unix://${lib.replaceStrings ["/"] ["%2F"] sock}";
+          defaultText = lib.literalExpression ''
+            "http+unix://''${lib.replaceStrings ["/"] ["%2F"] config.${option}}"
+          '';
+          example = "http://localhost:40772";
+          description = "URL to connect to Mirakurun.";
+        };
+
+        options.encodeProcessNum = lib.mkOption {
+          type = lib.types.ints.positive;
+          default = 4;
+          description = ''
+            The maximum number of processes that EPGStation would allow to run
+            at the same time for encoding or streaming videos.
+          '';
+        };
+
+        options.concurrentEncodeNum = lib.mkOption {
+          type = lib.types.ints.positive;
+          default = 1;
+          description = ''
+            The maximum number of encoding jobs that EPGStation would run at the
+            same time.
+          '';
+        };
+
+        options.encode = lib.mkOption {
+          type = with lib.types; listOf attrs;
+          description = "Encoding presets for recorded videos.";
+          default = [
+            {
+              name = "H.264";
+              cmd = "%NODE% ${cfg.package}/libexec/enc.js";
+              suffix = ".mp4";
+            }
+          ];
+          defaultText = lib.literalExpression ''
+            [
+              {
+                name = "H.264";
+                cmd = "%NODE% config.${opt.package}/libexec/enc.js";
+                suffix = ".mp4";
+              }
+            ]
+          '';
+        };
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !(lib.hasAttr "readOnlyOnce" cfg.settings);
+        message = ''
+          The option config.${opt.settings}.readOnlyOnce can no longer be used
+          since it's been removed. No replacements are available.
+        '';
+      }
+    ];
+
+    environment.etc = {
+      "epgstation/epgUpdaterLogConfig.yml".source = logConfig;
+      "epgstation/operatorLogConfig.yml".source = logConfig;
+      "epgstation/serviceLogConfig.yml".source = logConfig;
+    };
+
+    networking.firewall = lib.mkIf cfg.openFirewall {
+      allowedTCPPorts = with cfg.settings; [ port socketioPort ];
+    };
+
+    users.users.epgstation = {
+      description = "EPGStation user";
+      group = config.users.groups.epgstation.name;
+      isSystemUser = true;
+    };
+
+    users.groups.epgstation = { };
+
+    services.mirakurun.enable = lib.mkDefault true;
+
+    services.mysql = {
+      enable = lib.mkDefault true;
+      package = lib.mkDefault pkgs.mariadb;
+      ensureDatabases = [ cfg.database.name ];
+      # FIXME: enable once mysqljs supports auth_socket
+      # ensureUsers = [ {
+      #   name = username;
+      #   ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+      # } ];
+    };
+
+    services.epgstation.settings =
+      let
+        defaultSettings = {
+          dbtype = lib.mkDefault "mysql";
+          mysql = {
+            socketPath = lib.mkDefault "/run/mysqld/mysqld.sock";
+            user = username;
+            password = lib.mkDefault "@dbPassword@";
+            database = cfg.database.name;
+          };
+
+          ffmpeg = lib.mkDefault "${pkgs.ffmpeg-full}/bin/ffmpeg";
+          ffprobe = lib.mkDefault "${pkgs.ffmpeg-full}/bin/ffprobe";
+
+          # for disambiguation with TypeScript files
+          recordedFileExtension = lib.mkDefault ".m2ts";
+        };
+      in
+      lib.mkMerge [
+        defaultSettings
+        (lib.mkIf cfg.usePreconfiguredStreaming streamingConfig)
+      ];
+
+    systemd.tmpfiles.rules = [
+      "d '/var/lib/epgstation/streamfiles' - ${username} ${groupname} - -"
+      "d '/var/lib/epgstation/recorded' - ${username} ${groupname} - -"
+      "d '/var/lib/epgstation/thumbnail' - ${username} ${groupname} - -"
+    ];
+
+    systemd.services.epgstation = {
+      inherit description;
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ]
+        ++ lib.optional config.services.mirakurun.enable "mirakurun.service"
+        ++ lib.optional config.services.mysql.enable "mysql.service";
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/epgstation start";
+        ExecStartPre = "+${preStartScript}";
+        User = username;
+        Group = groupname;
+        StateDirectory = "epgstation";
+        LogsDirectory = "epgstation";
+        ConfigurationDirectory = "epgstation";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/video/epgstation/streaming.json b/nixos/modules/services/video/epgstation/streaming.json
new file mode 100644
index 00000000000..7f8df0817fc
--- /dev/null
+++ b/nixos/modules/services/video/epgstation/streaming.json
@@ -0,0 +1,140 @@
+{
+  "urlscheme": {
+    "m2ts": {
+      "ios": "vlc-x-callback://x-callback-url/stream?url=PROTOCOL://ADDRESS",
+      "android": "intent://ADDRESS#Intent;package=org.videolan.vlc;type=video;scheme=PROTOCOL;end"
+    },
+    "video": {
+      "ios": "infuse://x-callback-url/play?url=PROTOCOL://ADDRESS",
+      "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=PROTOCOL;end"
+    },
+    "download": {
+      "ios": "vlc-x-callback://x-callback-url/download?url=PROTOCOL://ADDRESS&filename=FILENAME"
+    }
+  },
+  "stream": {
+    "live": {
+      "ts": {
+        "m2ts": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -y -f mpegts pipe:1"
+          },
+          {
+            "name": "480p",
+            "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -y -f mpegts pipe:1"
+          },
+          {
+            "name": "無変æ›"
+          }
+        ],
+        "m2tsll": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -f mpegts -analyzeduration 500000 -i pipe:0 -map 0 -c:s copy -c:d copy -ignore_unknown -fflags nobuffer -flags low_delay -max_delay 250000 -max_interleave_delta 1 -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -flags +cgop -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -y -f mpegts pipe:1"
+          },
+          {
+            "name": "480p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -f mpegts -analyzeduration 500000 -i pipe:0 -map 0 -c:s copy -c:d copy -ignore_unknown -fflags nobuffer -flags low_delay -max_delay 250000 -max_interleave_delta 1 -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -flags +cgop -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -y -f mpegts pipe:1"
+          }
+        ],
+        "webm": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
+          },
+          {
+            "name": "480p",
+            "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 2 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
+          }
+        ],
+        "mp4": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
+          },
+          {
+            "name": "480p",
+            "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
+          }
+        ],
+        "hls": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
+          },
+          {
+            "name": "480p",
+            "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%"
+          }
+        ]
+      }
+    },
+    "recorded": {
+      "ts": {
+        "webm": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
+          },
+          {
+            "name": "480p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
+          }
+        ],
+        "mp4": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
+          },
+          {
+            "name": "480p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
+          }
+        ],
+        "hls": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
+          },
+          {
+            "name": "480p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%"
+          }
+        ]
+      },
+      "encoded": {
+        "webm": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
+          },
+          {
+            "name": "480p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
+          }
+        ],
+        "mp4": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf scale=-2:720 -b:v 3000k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
+          },
+          {
+            "name": "480p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf scale=-2:480 -b:v 1500k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
+          }
+        ],
+        "hls": [
+          {
+            "name": "720p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
+          },
+          {
+            "name": "480p",
+            "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf scale=-2:480 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
+          }
+        ]
+      }
+    }
+  }
+}
diff --git a/nixos/modules/services/video/mirakurun.nix b/nixos/modules/services/video/mirakurun.nix
new file mode 100644
index 00000000000..35303b2332c
--- /dev/null
+++ b/nixos/modules/services/video/mirakurun.nix
@@ -0,0 +1,204 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mirakurun;
+  mirakurun = pkgs.mirakurun;
+  username = config.users.users.mirakurun.name;
+  groupname = config.users.users.mirakurun.group;
+  settingsFmt = pkgs.formats.yaml {};
+
+  polkitRule = pkgs.writeTextDir "share/polkit-1/rules.d/10-mirakurun.rules" ''
+    polkit.addRule(function (action, subject) {
+      if (
+        (action.id == "org.debian.pcsc-lite.access_pcsc" ||
+          action.id == "org.debian.pcsc-lite.access_card") &&
+        subject.user == "${username}"
+      ) {
+        return polkit.Result.YES;
+      }
+    });
+  '';
+in
+  {
+    options = {
+      services.mirakurun = {
+        enable = mkEnableOption "the Mirakurun DVR Tuner Server";
+
+        port = mkOption {
+          type = with types; nullOr port;
+          default = 40772;
+          description = ''
+            Port to listen on. If <literal>null</literal>, it won't listen on
+            any port.
+          '';
+        };
+
+        openFirewall = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Open ports in the firewall for Mirakurun.
+
+            <warning>
+              <para>
+                Exposing Mirakurun to the open internet is generally advised
+                against. Only use it inside a trusted local network, or
+                consider putting it behind a VPN if you want remote access.
+              </para>
+            </warning>
+          '';
+        };
+
+        unixSocket = mkOption {
+          type = with types; nullOr path;
+          default = "/var/run/mirakurun/mirakurun.sock";
+          description = ''
+            Path to unix socket to listen on. If <literal>null</literal>, it
+            won't listen on any unix sockets.
+          '';
+        };
+
+        allowSmartCardAccess = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Install polkit rules to allow Mirakurun to access smart card readers
+            which is commonly used along with tuner devices.
+          '';
+        };
+
+        serverSettings = mkOption {
+          type = settingsFmt.type;
+          default = {};
+          example = literalExpression ''
+            {
+              highWaterMark = 25165824;
+              overflowTimeLimit = 30000;
+            };
+          '';
+          description = ''
+            Options for server.yml.
+
+            Documentation:
+            <link xlink:href="https://github.com/Chinachu/Mirakurun/blob/master/doc/Configuration.md"/>
+          '';
+        };
+
+        tunerSettings = mkOption {
+          type = with types; nullOr settingsFmt.type;
+          default = null;
+          example = literalExpression ''
+            [
+              {
+                name = "tuner-name";
+                types = [ "GR" "BS" "CS" "SKY" ];
+                dvbDevicePath = "/dev/dvb/adapterX/dvrX";
+              }
+            ];
+          '';
+          description = ''
+            Options which are added to tuners.yml. If none is specified, it will
+            automatically be generated at runtime.
+
+            Documentation:
+            <link xlink:href="https://github.com/Chinachu/Mirakurun/blob/master/doc/Configuration.md"/>
+          '';
+        };
+
+        channelSettings = mkOption {
+          type = with types; nullOr settingsFmt.type;
+          default = null;
+          example = literalExpression ''
+            [
+              {
+                name = "channel";
+                types = "GR";
+                channel = "0";
+              }
+            ];
+          '';
+          description = ''
+            Options which are added to channels.yml. If none is specified, it
+            will automatically be generated at runtime.
+
+            Documentation:
+            <link xlink:href="https://github.com/Chinachu/Mirakurun/blob/master/doc/Configuration.md"/>
+          '';
+        };
+      };
+    };
+
+    config = mkIf cfg.enable {
+      environment.systemPackages = [ mirakurun ] ++ optional cfg.allowSmartCardAccess polkitRule;
+      environment.etc = {
+        "mirakurun/server.yml".source = settingsFmt.generate "server.yml" cfg.serverSettings;
+        "mirakurun/tuners.yml" = mkIf (cfg.tunerSettings != null) {
+          source = settingsFmt.generate "tuners.yml" cfg.tunerSettings;
+          mode = "0644";
+          user = username;
+          group = groupname;
+        };
+        "mirakurun/channels.yml" = mkIf (cfg.channelSettings != null) {
+          source = settingsFmt.generate "channels.yml" cfg.channelSettings;
+          mode = "0644";
+          user = username;
+          group = groupname;
+        };
+      };
+
+      networking.firewall = mkIf cfg.openFirewall {
+        allowedTCPPorts = mkIf (cfg.port != null) [ cfg.port ];
+      };
+
+      users.users.mirakurun = {
+        description = "Mirakurun user";
+        group = "video";
+        isSystemUser = true;
+      };
+
+      services.mirakurun.serverSettings = {
+        logLevel = mkDefault 2;
+        path = mkIf (cfg.unixSocket != null) cfg.unixSocket;
+        port = mkIf (cfg.port != null) cfg.port;
+      };
+
+      systemd.tmpfiles.rules = [
+        "d '/etc/mirakurun' - ${username} ${groupname} - -"
+      ];
+
+      systemd.services.mirakurun = {
+        description = mirakurun.meta.description;
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig = {
+          ExecStart = "${mirakurun}/bin/mirakurun-start";
+          User = username;
+          Group = groupname;
+          RuntimeDirectory="mirakurun";
+          StateDirectory="mirakurun";
+          Nice = -10;
+          IOSchedulingClass = "realtime";
+          IOSchedulingPriority = 7;
+        };
+
+        environment = {
+          SERVER_CONFIG_PATH = "/etc/mirakurun/server.yml";
+          TUNERS_CONFIG_PATH = "/etc/mirakurun/tuners.yml";
+          CHANNELS_CONFIG_PATH = "/etc/mirakurun/channels.yml";
+          SERVICES_DB_PATH = "/var/lib/mirakurun/services.json";
+          PROGRAMS_DB_PATH = "/var/lib/mirakurun/programs.json";
+          NODE_ENV = "production";
+        };
+
+        restartTriggers = let
+          getconf = target: config.environment.etc."mirakurun/${target}.yml".source;
+          targets = [
+            "server"
+          ] ++ optional (cfg.tunerSettings != null) "tuners"
+            ++ optional (cfg.channelSettings != null) "channels";
+        in (map getconf targets);
+      };
+    };
+  }
diff --git a/nixos/modules/services/video/replay-sorcery.nix b/nixos/modules/services/video/replay-sorcery.nix
new file mode 100644
index 00000000000..abe7202a4a8
--- /dev/null
+++ b/nixos/modules/services/video/replay-sorcery.nix
@@ -0,0 +1,72 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.replay-sorcery;
+  configFile = generators.toKeyValue {} cfg.settings;
+in
+{
+  options = with types; {
+    services.replay-sorcery = {
+      enable = mkEnableOption "the ReplaySorcery service for instant-replays";
+
+      enableSysAdminCapability = mkEnableOption ''
+        the system admin capability to support hardware accelerated
+        video capture. This is equivalent to running ReplaySorcery as
+        root, so use with caution'';
+
+      autoStart = mkOption {
+        type = bool;
+        default = false;
+        description = "Automatically start ReplaySorcery when graphical-session.target starts.";
+      };
+
+      settings = mkOption {
+        type = attrsOf (oneOf [ str int ]);
+        default = {};
+        description = "System-wide configuration for ReplaySorcery (/etc/replay-sorcery.conf).";
+        example = literalExpression ''
+          {
+            videoInput = "hwaccel"; # requires `services.replay-sorcery.enableSysAdminCapability = true`
+            videoFramerate = 60;
+          }
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment = {
+      systemPackages = [ pkgs.replay-sorcery ];
+      etc."replay-sorcery.conf".text = configFile;
+    };
+
+    security.wrappers = mkIf cfg.enableSysAdminCapability {
+      replay-sorcery = {
+        owner = "root";
+        group = "root";
+        capabilities = "cap_sys_admin+ep";
+        source = "${pkgs.replay-sorcery}/bin/replay-sorcery";
+      };
+    };
+
+    systemd = {
+      packages = [ pkgs.replay-sorcery ];
+      user.services.replay-sorcery = {
+        wantedBy = mkIf cfg.autoStart [ "graphical-session.target" ];
+        partOf = mkIf cfg.autoStart [ "graphical-session.target" ];
+        serviceConfig = {
+          ExecStart = mkIf cfg.enableSysAdminCapability [
+            "" # Tell systemd to clear the existing ExecStart list, to prevent appending to it.
+            "${config.security.wrapperDir}/replay-sorcery"
+          ];
+        };
+      };
+    };
+  };
+
+  meta = {
+    maintainers = with maintainers; [ kira-bruneau ];
+  };
+}
diff --git a/nixos/modules/services/video/rtsp-simple-server.nix b/nixos/modules/services/video/rtsp-simple-server.nix
new file mode 100644
index 00000000000..644b1945a1e
--- /dev/null
+++ b/nixos/modules/services/video/rtsp-simple-server.nix
@@ -0,0 +1,80 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.rtsp-simple-server;
+  package = pkgs.rtsp-simple-server;
+  format = pkgs.formats.yaml {};
+in
+{
+  options = {
+    services.rtsp-simple-server = {
+      enable = mkEnableOption "RTSP Simple Server";
+
+      settings = mkOption {
+        description = ''
+          Settings for rtsp-simple-server.
+          Read more at <link xlink:href="https://github.com/aler9/rtsp-simple-server/blob/main/rtsp-simple-server.yml"/>
+        '';
+        type = format.type;
+
+        default = {
+          logLevel = "info";
+          logDestinations = [
+            "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";
+        };
+
+        example = {
+          paths = {
+            cam = {
+              runOnInit = "ffmpeg -f v4l2 -i /dev/video0 -f rtsp rtsp://localhost:$RTSP_PORT/$RTSP_PATH";
+              runOnInitRestart = true;
+            };
+          };
+        };
+      };
+
+      env = mkOption {
+        type = with types; attrsOf anything;
+        description = "Extra environment variables for RTSP Simple Server";
+        default = {};
+        example = {
+          RTSP_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;
+
+    systemd.services.rtsp-simple-server = {
+      environment = cfg.env;
+
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      path = with pkgs; [
+        ffmpeg
+      ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        User = "rtsp-simple-server";
+        Group = "rtsp-simple-server";
+
+        LogsDirectory = "rtsp-simple-server";
+
+        # 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";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/video/unifi-video.nix b/nixos/modules/services/video/unifi-video.nix
new file mode 100644
index 00000000000..43208a9fe4c
--- /dev/null
+++ b/nixos/modules/services/video/unifi-video.nix
@@ -0,0 +1,267 @@
+{ config, lib, options, pkgs, utils, ... }:
+with lib;
+let
+  cfg = config.services.unifi-video;
+  opt = options.services.unifi-video;
+  mainClass = "com.ubnt.airvision.Main";
+  cmd = ''
+    ${pkgs.jsvc}/bin/jsvc \
+    -cwd ${stateDir} \
+    -debug \
+    -verbose:class \
+    -nodetach \
+    -user unifi-video \
+    -home ${cfg.jrePackage}/lib/openjdk \
+    -cp ${pkgs.commonsDaemon}/share/java/commons-daemon-1.2.4.jar:${stateDir}/lib/airvision.jar \
+    -pidfile ${cfg.pidFile} \
+    -procname unifi-video \
+    -Djava.security.egd=file:/dev/./urandom \
+    -Xmx${cfg.maximumJavaHeapSize}M \
+    -Xss512K \
+    -XX:+UseG1GC \
+    -XX:+UseStringDeduplication \
+    -XX:MaxMetaspaceSize=768M \
+    -Djava.library.path=${stateDir}/lib \
+    -Djava.awt.headless=true \
+    -Djavax.net.ssl.trustStore=${stateDir}/etc/ufv-truststore \
+    -Dfile.encoding=UTF-8 \
+    -Dav.tempdir=/var/cache/unifi-video
+  '';
+
+  mongoConf = pkgs.writeTextFile {
+    name = "mongo.conf";
+    executable = false;
+    text = ''
+      # for documentation of all options, see http://docs.mongodb.org/manual/reference/configuration-options/
+
+      storage:
+         dbPath: ${cfg.dataDir}/db
+         journal:
+            enabled: true
+         syncPeriodSecs: 60
+
+      systemLog:
+         destination: file
+         logAppend: true
+         path: ${stateDir}/logs/mongod.log
+
+      net:
+         port: 7441
+         bindIp: 127.0.0.1
+         http:
+            enabled: false
+
+      operationProfiling:
+         slowOpThresholdMs: 500
+         mode: off
+    '';
+  };
+
+
+  mongoWtConf = pkgs.writeTextFile {
+    name = "mongowt.conf";
+    executable = false;
+    text = ''
+      # for documentation of all options, see:
+      #   http://docs.mongodb.org/manual/reference/configuration-options/
+
+      storage:
+         dbPath: ${cfg.dataDir}/db-wt
+         journal:
+            enabled: true
+         wiredTiger:
+            engineConfig:
+               cacheSizeGB: 1
+
+      systemLog:
+         destination: file
+         logAppend: true
+         path: logs/mongod.log
+
+      net:
+         port: 7441
+         bindIp: 127.0.0.1
+
+      operationProfiling:
+         slowOpThresholdMs: 500
+         mode: off
+    '';
+  };
+
+  stateDir = "/var/lib/unifi-video";
+
+in
+  {
+
+    options.services.unifi-video = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether or not to enable the unifi-video service.
+        '';
+      };
+
+      jrePackage = mkOption {
+        type = types.package;
+        default = pkgs.jre8;
+        defaultText = literalExpression "pkgs.jre8";
+        description = ''
+          The JRE package to use. Check the release notes to ensure it is supported.
+        '';
+      };
+
+      unifiVideoPackage = mkOption {
+        type = types.package;
+        default = pkgs.unifi-video;
+        defaultText = literalExpression "pkgs.unifi-video";
+        description = ''
+          The unifi-video package to use.
+        '';
+      };
+
+      mongodbPackage = mkOption {
+        type = types.package;
+        default = pkgs.mongodb-4_0;
+        defaultText = literalExpression "pkgs.mongodb";
+        description = ''
+          The mongodb package to use.
+        '';
+      };
+
+      logDir = mkOption {
+        type = types.str;
+        default = "${stateDir}/logs";
+        description = ''
+          Where to store the logs.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "${stateDir}/data";
+        description = ''
+          Where to store the database and other data.
+        '';
+      };
+
+      openPorts = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether or not to open the required ports on the firewall.
+        '';
+      };
+
+      maximumJavaHeapSize = mkOption {
+        type = types.nullOr types.int;
+        default = 1024;
+        example = 4096;
+        description = ''
+          Set the maximimum heap size for the JVM in MB.
+        '';
+      };
+
+      pidFile = mkOption {
+        type = types.path;
+        default = "${cfg.dataDir}/unifi-video.pid";
+        defaultText = literalExpression ''"''${config.${opt.dataDir}}/unifi-video.pid"'';
+        description = "Location of unifi-video pid file.";
+      };
+
+};
+
+config = mkIf cfg.enable {
+  users = {
+    users.unifi-video = {
+      description = "UniFi Video controller daemon user";
+      home = stateDir;
+      group = "unifi-video";
+      isSystemUser = true;
+    };
+    groups.unifi-video = {};
+  };
+
+  networking.firewall = mkIf cfg.openPorts {
+      # https://help.ui.com/hc/en-us/articles/217875218-UniFi-Video-Ports-Used
+      allowedTCPPorts = [
+        7080 # HTTP portal
+        7443 # HTTPS portal
+        7445 # Video over HTTP (mobile app)
+        7446 # Video over HTTPS (mobile app)
+        7447 # RTSP via the controller
+        7442 # Camera management from cameras to NVR over WAN
+      ];
+      allowedUDPPorts = [
+        6666 # Inbound camera streams sent over WAN
+      ];
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${stateDir}' 0700 unifi-video unifi-video - -"
+      "d '/var/cache/unifi-video' 0700 unifi-video unifi-video - -"
+
+      "d '${stateDir}/logs' 0700 unifi-video unifi-video - -"
+      "C '${stateDir}/etc' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/etc"
+      "C '${stateDir}/webapps' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/webapps"
+      "C '${stateDir}/email' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/email"
+      "C '${stateDir}/fw' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/fw"
+      "C '${stateDir}/lib' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/lib"
+
+      "d '${stateDir}/data' 0700 unifi-video unifi-video - -"
+      "d '${stateDir}/data/db' 0700 unifi-video unifi-video - -"
+      "C '${stateDir}/data/system.properties' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/etc/system.properties"
+
+      "d '${stateDir}/bin' 0700 unifi-video unifi-video - -"
+      "f '${stateDir}/bin/evostreamms' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/evostreamms"
+      "f '${stateDir}/bin/libavcodec.so.54' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/libavcodec.so.54"
+      "f '${stateDir}/bin/libavformat.so.54' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/libavformat.so.54"
+      "f '${stateDir}/bin/libavutil.so.52' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/libavutil.so.52"
+      "f '${stateDir}/bin/ubnt.avtool' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/ubnt.avtool"
+      "f '${stateDir}/bin/ubnt.updater' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/ubnt.updater"
+      "C '${stateDir}/bin/mongo' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongo"
+      "C '${stateDir}/bin/mongod' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongod"
+      "C '${stateDir}/bin/mongoperf' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongoperf"
+      "C '${stateDir}/bin/mongos' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongos"
+
+      "d '${stateDir}/conf' 0700 unifi-video unifi-video - -"
+      "C '${stateDir}/conf/evostream' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/evostream"
+      "Z '${stateDir}/conf/evostream' 0700 unifi-video unifi-video - -"
+      "L+ '${stateDir}/conf/mongodv3.0+.conf' 0700 unifi-video unifi-video - ${mongoConf}"
+      "L+ '${stateDir}/conf/mongodv3.6+.conf' 0700 unifi-video unifi-video - ${mongoConf}"
+      "L+ '${stateDir}/conf/mongod-wt.conf' 0700 unifi-video unifi-video - ${mongoWtConf}"
+      "L+ '${stateDir}/conf/catalina.policy' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/catalina.policy"
+      "L+ '${stateDir}/conf/catalina.properties' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/catalina.properties"
+      "L+ '${stateDir}/conf/context.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/context.xml"
+      "L+ '${stateDir}/conf/logging.properties' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/logging.properties"
+      "L+ '${stateDir}/conf/server.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/server.xml"
+      "L+ '${stateDir}/conf/tomcat-users.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/tomcat-users.xml"
+      "L+ '${stateDir}/conf/web.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/web.xml"
+
+    ];
+
+    systemd.services.unifi-video = {
+      description = "UniFi Video NVR daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ] ;
+      unitConfig.RequiresMountsFor = stateDir;
+      # Make sure package upgrades trigger a service restart
+      restartTriggers = [ cfg.unifiVideoPackage cfg.mongodbPackage ];
+      path = with pkgs; [ gawk coreutils busybox which jre8 lsb-release libcap util-linux ];
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${(removeSuffix "\n" cmd)} ${mainClass} start";
+        ExecStop = "${(removeSuffix "\n" cmd)} stop ${mainClass} stop";
+        Restart = "on-failure";
+        UMask = "0077";
+        User = "unifi-video";
+        WorkingDirectory = "${stateDir}";
+      };
+    };
+
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ rsynnest ];
+  };
+}
diff --git a/nixos/modules/services/wayland/cage.nix b/nixos/modules/services/wayland/cage.nix
new file mode 100644
index 00000000000..15b4e7163f9
--- /dev/null
+++ b/nixos/modules/services/wayland/cage.nix
@@ -0,0 +1,104 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cage;
+in {
+  options.services.cage.enable = mkEnableOption "cage kiosk service";
+
+  options.services.cage.user = mkOption {
+    type = types.str;
+    default = "demo";
+    description = ''
+      User to log-in as.
+    '';
+  };
+
+  options.services.cage.extraArguments = mkOption {
+    type = types.listOf types.str;
+    default = [];
+    defaultText = literalExpression "[]";
+    description = "Additional command line arguments to pass to Cage.";
+    example = ["-d"];
+  };
+
+  options.services.cage.program = mkOption {
+    type = types.str;
+    default = "${pkgs.xterm}/bin/xterm";
+    defaultText = literalExpression ''"''${pkgs.xterm}/bin/xterm"'';
+    description = ''
+      Program to run in cage.
+    '';
+  };
+
+  config = mkIf cfg.enable {
+
+    # The service is partially based off of the one provided in the
+    # cage wiki at
+    # https://github.com/Hjdskes/cage/wiki/Starting-Cage-on-boot-with-systemd.
+    systemd.services."cage-tty1" = {
+      enable = true;
+      after = [
+        "systemd-user-sessions.service"
+        "plymouth-start.service"
+        "plymouth-quit.service"
+        "systemd-logind.service"
+        "getty@tty1.service"
+      ];
+      before = [ "graphical.target" ];
+      wants = [ "dbus.socket" "systemd-logind.service" "plymouth-quit.service"];
+      wantedBy = [ "graphical.target" ];
+      conflicts = [ "getty@tty1.service" ];
+      environment = { "LIBSEAT_BACKEND" = "logind"; };
+
+      restartIfChanged = false;
+      unitConfig.ConditionPathExists = "/dev/tty1";
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.cage}/bin/cage \
+            ${escapeShellArgs cfg.extraArguments} \
+            -- ${cfg.program}
+        '';
+        User = cfg.user;
+
+        IgnoreSIGPIPE = "no";
+
+        # Log this user with utmp, letting it show up with commands 'w' and
+        # 'who'. This is needed since we replace (a)getty.
+        UtmpIdentifier = "%n";
+        UtmpMode = "user";
+        # A virtual terminal is needed.
+        TTYPath = "/dev/tty1";
+        TTYReset = "yes";
+        TTYVHangup = "yes";
+        TTYVTDisallocate = "yes";
+        # Fail to start if not controlling the virtual terminal.
+        StandardInput = "tty-fail";
+        StandardOutput = "journal";
+        StandardError = "journal";
+        # Set up a full (custom) user session for the user, required by Cage.
+        PAMName = "cage";
+      };
+    };
+
+    security.polkit.enable = true;
+
+    security.pam.services.cage.text = ''
+      auth    required pam_unix.so nullok
+      account required pam_unix.so
+      session required pam_unix.so
+      session required pam_env.so conffile=/etc/pam/environment readenv=0
+      session required ${pkgs.systemd}/lib/security/pam_systemd.so
+    '';
+
+    hardware.opengl.enable = mkDefault true;
+
+    systemd.targets.graphical.wants = [ "cage-tty1.service" ];
+
+    systemd.defaultUnit = "graphical.target";
+  };
+
+  meta.maintainers = with lib.maintainers; [ matthewbauer ];
+
+}
diff --git a/nixos/modules/services/web-apps/atlassian/confluence.nix b/nixos/modules/services/web-apps/atlassian/confluence.nix
new file mode 100644
index 00000000000..2d809c17ff0
--- /dev/null
+++ b/nixos/modules/services/web-apps/atlassian/confluence.nix
@@ -0,0 +1,197 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.confluence;
+
+  pkg = cfg.package.override (optionalAttrs cfg.sso.enable {
+    enableSSO = cfg.sso.enable;
+    crowdProperties = ''
+      application.name                        ${cfg.sso.applicationName}
+      application.password                    ${cfg.sso.applicationPassword}
+      application.login.url                   ${cfg.sso.crowd}/console/
+
+      crowd.server.url                        ${cfg.sso.crowd}/services/
+      crowd.base.url                          ${cfg.sso.crowd}/
+
+      session.isauthenticated                 session.isauthenticated
+      session.tokenkey                        session.tokenkey
+      session.validationinterval              ${toString cfg.sso.validationInterval}
+      session.lastvalidation                  session.lastvalidation
+    '';
+  });
+
+in
+
+{
+  options = {
+    services.confluence = {
+      enable = mkEnableOption "Atlassian Confluence service";
+
+      user = mkOption {
+        type = types.str;
+        default = "confluence";
+        description = "User which runs confluence.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "confluence";
+        description = "Group which runs confluence.";
+      };
+
+      home = mkOption {
+        type = types.str;
+        default = "/var/lib/confluence";
+        description = "Home directory of the confluence instance.";
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "Address to listen on.";
+      };
+
+      listenPort = mkOption {
+        type = types.int;
+        default = 8090;
+        description = "Port to listen on.";
+      };
+
+      catalinaOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "-Xms1024m" "-Xmx2048m" "-Dconfluence.disable.peopledirectory.all=true" ];
+        description = "Java options to pass to catalina/tomcat.";
+      };
+
+      proxy = {
+        enable = mkEnableOption "proxy support";
+
+        name = mkOption {
+          type = types.str;
+          example = "confluence.example.com";
+          description = "Virtual hostname at the proxy";
+        };
+
+        port = mkOption {
+          type = types.int;
+          default = 443;
+          example = 80;
+          description = "Port used at the proxy";
+        };
+
+        scheme = mkOption {
+          type = types.str;
+          default = "https";
+          example = "http";
+          description = "Protocol used at the proxy.";
+        };
+      };
+
+      sso = {
+        enable = mkEnableOption "SSO with Atlassian Crowd";
+
+        crowd = mkOption {
+          type = types.str;
+          example = "http://localhost:8095/crowd";
+          description = "Crowd Base URL without trailing slash";
+        };
+
+        applicationName = mkOption {
+          type = types.str;
+          example = "jira";
+          description = "Exact name of this Confluence instance in Crowd";
+        };
+
+        applicationPassword = mkOption {
+          type = types.str;
+          description = "Application password of this Confluence instance in Crowd";
+        };
+
+        validationInterval = mkOption {
+          type = types.int;
+          default = 2;
+          example = 0;
+          description = ''
+            Set to 0, if you want authentication checks to occur on each
+            request. Otherwise set to the number of minutes between request
+            to validate if the user is logged in or out of the Crowd SSO
+            server. Setting this value to 1 or higher will increase the
+            performance of Crowd's integration.
+          '';
+        };
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.atlassian-confluence;
+        defaultText = literalExpression "pkgs.atlassian-confluence";
+        description = "Atlassian Confluence package to use.";
+      };
+
+      jrePackage = mkOption {
+        type = types.package;
+        default = pkgs.oraclejre8;
+        defaultText = literalExpression "pkgs.oraclejre8";
+        description = "Note that Atlassian only support the Oracle JRE (JRASERVER-46152).";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.${cfg.user} = {
+      isSystemUser = true;
+      group = cfg.group;
+    };
+
+    users.groups.${cfg.group} = {};
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.home}' - ${cfg.user} - - -"
+      "d /run/confluence - - - - -"
+
+      "L+ /run/confluence/home - - - - ${cfg.home}"
+      "L+ /run/confluence/logs - - - - ${cfg.home}/logs"
+      "L+ /run/confluence/temp - - - - ${cfg.home}/temp"
+      "L+ /run/confluence/work - - - - ${cfg.home}/work"
+      "L+ /run/confluence/server.xml - - - - ${cfg.home}/server.xml"
+    ];
+
+    systemd.services.confluence = {
+      description = "Atlassian Confluence";
+
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "postgresql.service" ];
+      after = [ "postgresql.service" ];
+
+      path = [ cfg.jrePackage pkgs.bash ];
+
+      environment = {
+        CONF_USER = cfg.user;
+        JAVA_HOME = "${cfg.jrePackage}";
+        CATALINA_OPTS = concatStringsSep " " cfg.catalinaOptions;
+      };
+
+      preStart = ''
+        mkdir -p ${cfg.home}/{logs,work,temp,deploy}
+
+        sed -e 's,port="8090",port="${toString cfg.listenPort}" address="${cfg.listenAddress}",' \
+        '' + (lib.optionalString cfg.proxy.enable ''
+          -e 's,protocol="org.apache.coyote.http11.Http11NioProtocol",protocol="org.apache.coyote.http11.Http11NioProtocol" proxyName="${cfg.proxy.name}" proxyPort="${toString cfg.proxy.port}" scheme="${cfg.proxy.scheme}",' \
+        '') + ''
+          ${pkg}/conf/server.xml.dist > ${cfg.home}/server.xml
+      '';
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        PrivateTmp = true;
+        ExecStart = "${pkg}/bin/start-confluence.sh -fg";
+        ExecStop = "${pkg}/bin/stop-confluence.sh";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/atlassian/crowd.nix b/nixos/modules/services/web-apps/atlassian/crowd.nix
new file mode 100644
index 00000000000..a8b2482d5a9
--- /dev/null
+++ b/nixos/modules/services/web-apps/atlassian/crowd.nix
@@ -0,0 +1,164 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.crowd;
+
+  pkg = cfg.package.override {
+    home = cfg.home;
+    port = cfg.listenPort;
+    openidPassword = cfg.openidPassword;
+  } // (optionalAttrs cfg.proxy.enable {
+    proxyUrl = "${cfg.proxy.scheme}://${cfg.proxy.name}:${toString cfg.proxy.port}";
+  });
+
+in
+
+{
+  options = {
+    services.crowd = {
+      enable = mkEnableOption "Atlassian Crowd service";
+
+      user = mkOption {
+        type = types.str;
+        default = "crowd";
+        description = "User which runs Crowd.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "crowd";
+        description = "Group which runs Crowd.";
+      };
+
+      home = mkOption {
+        type = types.str;
+        default = "/var/lib/crowd";
+        description = "Home directory of the Crowd instance.";
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "Address to listen on.";
+      };
+
+      listenPort = mkOption {
+        type = types.int;
+        default = 8092;
+        description = "Port to listen on.";
+      };
+
+      openidPassword = mkOption {
+        type = types.str;
+        description = "Application password for OpenID server.";
+      };
+
+      catalinaOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "-Xms1024m" "-Xmx2048m" ];
+        description = "Java options to pass to catalina/tomcat.";
+      };
+
+      proxy = {
+        enable = mkEnableOption "reverse proxy support";
+
+        name = mkOption {
+          type = types.str;
+          example = "crowd.example.com";
+          description = "Virtual hostname at the proxy";
+        };
+
+        port = mkOption {
+          type = types.int;
+          default = 443;
+          example = 80;
+          description = "Port used at the proxy";
+        };
+
+        scheme = mkOption {
+          type = types.str;
+          default = "https";
+          example = "http";
+          description = "Protocol used at the proxy.";
+        };
+
+        secure = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Whether the connections to the proxy should be considered secure.";
+        };
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.atlassian-crowd;
+        defaultText = literalExpression "pkgs.atlassian-crowd";
+        description = "Atlassian Crowd package to use.";
+      };
+
+      jrePackage = mkOption {
+        type = types.package;
+        default = pkgs.oraclejre8;
+        defaultText = literalExpression "pkgs.oraclejre8";
+        description = "Note that Atlassian only support the Oracle JRE (JRASERVER-46152).";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.${cfg.user} = {
+      isSystemUser = true;
+      group = cfg.group;
+    };
+
+    users.groups.${cfg.group} = {};
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.home}' - ${cfg.user} ${cfg.group} - -"
+      "d /run/atlassian-crowd - - - - -"
+
+      "L+ /run/atlassian-crowd/database - - - - ${cfg.home}/database"
+      "L+ /run/atlassian-crowd/logs - - - - ${cfg.home}/logs"
+      "L+ /run/atlassian-crowd/work - - - - ${cfg.home}/work"
+      "L+ /run/atlassian-crowd/server.xml - - - - ${cfg.home}/server.xml"
+    ];
+
+    systemd.services.atlassian-crowd = {
+      description = "Atlassian Crowd";
+
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "postgresql.service" ];
+      after = [ "postgresql.service" ];
+
+      path = [ cfg.jrePackage ];
+
+      environment = {
+        JAVA_HOME = "${cfg.jrePackage}";
+        CATALINA_OPTS = concatStringsSep " " cfg.catalinaOptions;
+        CATALINA_TMPDIR = "/tmp";
+      };
+
+      preStart = ''
+        rm -rf ${cfg.home}/work
+        mkdir -p ${cfg.home}/{logs,database,work}
+
+        sed -e 's,port="8095",port="${toString cfg.listenPort}" address="${cfg.listenAddress}",' \
+        '' + (lib.optionalString cfg.proxy.enable ''
+          -e 's,compression="on",compression="off" protocol="HTTP/1.1" proxyName="${cfg.proxy.name}" proxyPort="${toString cfg.proxy.port}" scheme="${cfg.proxy.scheme}" secure="${boolToString cfg.proxy.secure}",' \
+        '') + ''
+          ${pkg}/apache-tomcat/conf/server.xml.dist > ${cfg.home}/server.xml
+      '';
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        PrivateTmp = true;
+        ExecStart = "${pkg}/start_crowd.sh -fg";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/atlassian/jira.nix b/nixos/modules/services/web-apps/atlassian/jira.nix
new file mode 100644
index 00000000000..d7a26838d6f
--- /dev/null
+++ b/nixos/modules/services/web-apps/atlassian/jira.nix
@@ -0,0 +1,204 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.jira;
+
+  pkg = cfg.package.override (optionalAttrs cfg.sso.enable {
+    enableSSO = cfg.sso.enable;
+    crowdProperties = ''
+      application.name                        ${cfg.sso.applicationName}
+      application.password                    ${cfg.sso.applicationPassword}
+      application.login.url                   ${cfg.sso.crowd}/console/
+
+      crowd.server.url                        ${cfg.sso.crowd}/services/
+      crowd.base.url                          ${cfg.sso.crowd}/
+
+      session.isauthenticated                 session.isauthenticated
+      session.tokenkey                        session.tokenkey
+      session.validationinterval              ${toString cfg.sso.validationInterval}
+      session.lastvalidation                  session.lastvalidation
+    '';
+  });
+
+in
+
+{
+  options = {
+    services.jira = {
+      enable = mkEnableOption "Atlassian JIRA service";
+
+      user = mkOption {
+        type = types.str;
+        default = "jira";
+        description = "User which runs JIRA.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "jira";
+        description = "Group which runs JIRA.";
+      };
+
+      home = mkOption {
+        type = types.str;
+        default = "/var/lib/jira";
+        description = "Home directory of the JIRA instance.";
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "Address to listen on.";
+      };
+
+      listenPort = mkOption {
+        type = types.int;
+        default = 8091;
+        description = "Port to listen on.";
+      };
+
+      catalinaOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "-Xms1024m" "-Xmx2048m" ];
+        description = "Java options to pass to catalina/tomcat.";
+      };
+
+      proxy = {
+        enable = mkEnableOption "reverse proxy support";
+
+        name = mkOption {
+          type = types.str;
+          example = "jira.example.com";
+          description = "Virtual hostname at the proxy";
+        };
+
+        port = mkOption {
+          type = types.int;
+          default = 443;
+          example = 80;
+          description = "Port used at the proxy";
+        };
+
+        scheme = mkOption {
+          type = types.str;
+          default = "https";
+          example = "http";
+          description = "Protocol used at the proxy.";
+        };
+
+        secure = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Whether the connections to the proxy should be considered secure.";
+        };
+      };
+
+      sso = {
+        enable = mkEnableOption "SSO with Atlassian Crowd";
+
+        crowd = mkOption {
+          type = types.str;
+          example = "http://localhost:8095/crowd";
+          description = "Crowd Base URL without trailing slash";
+        };
+
+        applicationName = mkOption {
+          type = types.str;
+          example = "jira";
+          description = "Exact name of this JIRA instance in Crowd";
+        };
+
+        applicationPassword = mkOption {
+          type = types.str;
+          description = "Application password of this JIRA instance in Crowd";
+        };
+
+        validationInterval = mkOption {
+          type = types.int;
+          default = 2;
+          example = 0;
+          description = ''
+            Set to 0, if you want authentication checks to occur on each
+            request. Otherwise set to the number of minutes between request
+            to validate if the user is logged in or out of the Crowd SSO
+            server. Setting this value to 1 or higher will increase the
+            performance of Crowd's integration.
+          '';
+        };
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.atlassian-jira;
+        defaultText = literalExpression "pkgs.atlassian-jira";
+        description = "Atlassian JIRA package to use.";
+      };
+
+      jrePackage = mkOption {
+        type = types.package;
+        default = pkgs.oraclejre8;
+        defaultText = literalExpression "pkgs.oraclejre8";
+        description = "Note that Atlassian only support the Oracle JRE (JRASERVER-46152).";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.${cfg.user} = {
+      isSystemUser = true;
+      group = cfg.group;
+    };
+
+    users.groups.${cfg.group} = {};
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.home}' - ${cfg.user} - - -"
+      "d /run/atlassian-jira - - - - -"
+
+      "L+ /run/atlassian-jira/home - - - - ${cfg.home}"
+      "L+ /run/atlassian-jira/logs - - - - ${cfg.home}/logs"
+      "L+ /run/atlassian-jira/work - - - - ${cfg.home}/work"
+      "L+ /run/atlassian-jira/temp - - - - ${cfg.home}/temp"
+      "L+ /run/atlassian-jira/server.xml - - - - ${cfg.home}/server.xml"
+    ];
+
+    systemd.services.atlassian-jira = {
+      description = "Atlassian JIRA";
+
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "postgresql.service" ];
+      after = [ "postgresql.service" ];
+
+      path = [ cfg.jrePackage pkgs.bash ];
+
+      environment = {
+        JIRA_USER = cfg.user;
+        JIRA_HOME = cfg.home;
+        JAVA_HOME = "${cfg.jrePackage}";
+        CATALINA_OPTS = concatStringsSep " " cfg.catalinaOptions;
+      };
+
+      preStart = ''
+        mkdir -p ${cfg.home}/{logs,work,temp,deploy}
+
+        sed -e 's,port="8080",port="${toString cfg.listenPort}" address="${cfg.listenAddress}",' \
+        '' + (lib.optionalString cfg.proxy.enable ''
+          -e 's,protocol="HTTP/1.1",protocol="HTTP/1.1" proxyName="${cfg.proxy.name}" proxyPort="${toString cfg.proxy.port}" scheme="${cfg.proxy.scheme}" secure="${toString cfg.proxy.secure}",' \
+        '') + ''
+          ${pkg}/conf/server.xml.dist > ${cfg.home}/server.xml
+      '';
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        PrivateTmp = true;
+        ExecStart = "${pkg}/bin/start-jira.sh -fg";
+        ExecStop = "${pkg}/bin/stop-jira.sh";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/baget.nix b/nixos/modules/services/web-apps/baget.nix
new file mode 100644
index 00000000000..3007dd4fbb2
--- /dev/null
+++ b/nixos/modules/services/web-apps/baget.nix
@@ -0,0 +1,170 @@
+{ 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 "BaGet NuGet-compatible server";
+
+    apiKeyFile = mkOption {
+      type = types.path;
+      example = "/root/baget.key";
+      description = ''
+        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 = ''
+        Extra configuration options for BaGet. Refer to <link xlink:href="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/bookstack.nix b/nixos/modules/services/web-apps/bookstack.nix
new file mode 100644
index 00000000000..64a2767fab6
--- /dev/null
+++ b/nixos/modules/services/web-apps/bookstack.nix
@@ -0,0 +1,449 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.bookstack;
+  bookstack = pkgs.bookstack.override {
+    dataDir = cfg.dataDir;
+  };
+  db = cfg.database;
+  mail = cfg.mail;
+
+  user = cfg.user;
+  group = cfg.group;
+
+  # shell script for local administration
+  artisan = pkgs.writeScriptBin "bookstack" ''
+    #! ${pkgs.runtimeShell}
+    cd ${bookstack}
+    sudo=exec
+    if [[ "$USER" != ${user} ]]; then
+      sudo='exec /run/wrappers/bin/sudo -u ${user}'
+    fi
+    $sudo ${pkgs.php}/bin/php artisan $*
+  '';
+
+  tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME;
+
+in {
+  imports = [
+    (mkRemovedOptionModule [ "services" "bookstack" "extraConfig" ] "Use services.bookstack.config instead.")
+    (mkRemovedOptionModule [ "services" "bookstack" "cacheDir" ] "The cache directory is now handled automatically.")
+  ];
+
+  options.services.bookstack = {
+
+    enable = mkEnableOption "BookStack";
+
+    user = mkOption {
+      default = "bookstack";
+      description = "User bookstack runs as.";
+      type = types.str;
+    };
+
+    group = mkOption {
+      default = "bookstack";
+      description = "Group bookstack runs as.";
+      type = types.str;
+    };
+
+    appKeyFile = mkOption {
+      description = ''
+        A file containing the Laravel APP_KEY - a 32 character long,
+        base64 encoded key used for encryption where needed. Can be
+        generated with <code>head -c 32 /dev/urandom | base64</code>.
+      '';
+      example = "/run/keys/bookstack-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 = "bookstack.example.com";
+      description = ''
+        The hostname to serve BookStack on.
+      '';
+    };
+
+    appURL = mkOption {
+      description = ''
+        The root URL that you want to host BookStack on. All URLs in BookStack 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 bookstack: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 = "BookStack data directory";
+      default = "/var/lib/bookstack";
+      type = types.path;
+    };
+
+    database = {
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Database host address.";
+      };
+      port = mkOption {
+        type = types.port;
+        default = 3306;
+        description = "Database host port.";
+      };
+      name = mkOption {
+        type = types.str;
+        default = "bookstack";
+        description = "Database name.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = user;
+        defaultText = literalExpression "user";
+        description = "Database username.";
+      };
+      passwordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "/run/keys/bookstack-dbpassword";
+        description = ''
+          A file containing the password corresponding to
+          <option>database.user</option>.
+        '';
+      };
+      createLocally = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Create the database and database user locally.";
+      };
+    };
+
+    mail = {
+      driver = mkOption {
+        type = types.enum [ "smtp" "sendmail" ];
+        default = "smtp";
+        description = "Mail driver to use.";
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Mail host address.";
+      };
+      port = mkOption {
+        type = types.port;
+        default = 1025;
+        description = "Mail host port.";
+      };
+      fromName = mkOption {
+        type = types.str;
+        default = "BookStack";
+        description = "Mail \"from\" name.";
+      };
+      from = mkOption {
+        type = types.str;
+        default = "mail@bookstackapp.com";
+        description = "Mail \"from\" email.";
+      };
+      user = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        example = "bookstack";
+        description = "Mail username.";
+      };
+      passwordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "/run/keys/bookstack-mailpassword";
+        description = ''
+          A file containing the password corresponding to
+          <option>mail.user</option>.
+        '';
+      };
+      encryption = mkOption {
+        type = with types; nullOr (enum [ "tls" ]);
+        default = null;
+        description = "SMTP encryption mechanism to use.";
+      };
+    };
+
+    maxUploadSize = mkOption {
+      type = types.str;
+      default = "18M";
+      example = "1G";
+      description = "The maximum size for uploads (e.g. images).";
+    };
+
+    poolConfig = mkOption {
+      type = with types; attrsOf (oneOf [ str int bool ]);
+      default = {
+        "pm" = "dynamic";
+        "pm.max_children" = 32;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 2;
+        "pm.max_spare_servers" = 4;
+        "pm.max_requests" = 500;
+      };
+      description = ''
+        Options for the bookstack PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+        for details on configuration directives.
+      '';
+    };
+
+    nginx = mkOption {
+      type = types.submodule (
+        recursiveUpdate
+          (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {}
+      );
+      default = {};
+      example = literalExpression ''
+        {
+          serverAliases = [
+            "bookstack.''${config.networking.domain}"
+          ];
+          # To enable encryption and let let's encrypt take care of certificate
+          forceSSL = true;
+          enableACME = true;
+        }
+      '';
+      description = ''
+        With this option, you can customize the nginx virtualHost settings.
+      '';
+    };
+
+    config = mkOption {
+      type = with types;
+        attrsOf
+          (nullOr
+            (either
+              (oneOf [
+                bool
+                int
+                port
+                path
+                str
+              ])
+              (submodule {
+                options = {
+                  _secret = mkOption {
+                    type = nullOr str;
+                    description = ''
+                      The path to a file containing the value the
+                      option should be set to in the final
+                      configuration file.
+                    '';
+                  };
+                };
+              })));
+      default = {};
+      example = literalExpression ''
+        {
+          ALLOWED_IFRAME_HOSTS = "https://example.com";
+          WKHTMLTOPDF = "/home/user/bins/wkhtmltopdf";
+          AUTH_METHOD = "oidc";
+          OIDC_NAME = "MyLogin";
+          OIDC_DISPLAY_NAME_CLAIMS = "name";
+          OIDC_CLIENT_ID = "bookstack";
+          OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"};
+          OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm";
+          OIDC_ISSUER_DISCOVER = true;
+        }
+      '';
+      description = ''
+        BookStack configuration options to set in the
+        <filename>.env</filename> file.
+
+        Refer to <link xlink:href="https://www.bookstackapp.com/docs/"/>
+        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.bookstack.database.user must be set to ${user} if services.bookstack.database.createLocally is set true.";
+      }
+      { assertion = db.createLocally -> db.passwordFile == null;
+        message = "services.bookstack.database.passwordFile cannot be specified if services.bookstack.database.createLocally is set to true.";
+      }
+    ];
+
+    services.bookstack.config = {
+      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/bookstack/cache/services.php";
+      APP_PACKAGES_CACHE = "/run/bookstack/cache/packages.php";
+      APP_CONFIG_CACHE = "/run/bookstack/cache/config.php";
+      APP_ROUTES_CACHE = "/run/bookstack/cache/routes-v7.php";
+      APP_EVENTS_CACHE = "/run/bookstack/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.bookstack = {
+      inherit user;
+      inherit 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;
+      virtualHosts.${cfg.hostname} = mkMerge [ cfg.nginx {
+        root = mkForce "${bookstack}/public";
+        locations = {
+          "/" = {
+            index = "index.php";
+            tryFiles = "$uri $uri/ /index.php?$query_string";
+          };
+          "~ \.php$".extraConfig = ''
+            fastcgi_pass unix:${config.services.phpfpm.pools."bookstack".socket};
+          '';
+          "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
+            extraConfig = "expires 365d;";
+          };
+        };
+      }];
+    };
+
+    systemd.services.bookstack-setup = {
+      description = "Preperation tasks for BookStack";
+      before = [ "phpfpm-bookstack.service" ];
+      after = optional db.createLocally "mysql.service";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        User = user;
+        WorkingDirectory = "${bookstack}";
+        RuntimeDirectory = "bookstack/cache";
+        RuntimeDirectoryMode = 0700;
+      };
+      path = [ pkgs.replace-secret ];
+      script =
+        let
+          isSecret = v: isAttrs v && v ? _secret && isString v._secret;
+          bookstackEnvVars = 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;
+          bookstackEnv = pkgs.writeText "bookstack.env" (bookstackEnvVars filteredConfig);
+        in ''
+        # error handling
+        set -euo pipefail
+
+        # set permissions
+        umask 077
+
+        # create .env file
+        install -T -m 0600 -o ${user} ${bookstackEnv} "${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 db
+        ${pkgs.php}/bin/php artisan migrate --force
+      '';
+    };
+
+    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 == "bookstack") {
+        bookstack = {
+          inherit group;
+          isSystemUser = true;
+        };
+        "${config.services.nginx.user}".extraGroups = [ group ];
+      };
+      groups = mkIf (group == "bookstack") {
+        bookstack = {};
+      };
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ ymarkus ];
+}
diff --git a/nixos/modules/services/web-apps/calibre-web.nix b/nixos/modules/services/web-apps/calibre-web.nix
new file mode 100644
index 00000000000..704cd2cfa8a
--- /dev/null
+++ b/nixos/modules/services/web-apps/calibre-web.nix
@@ -0,0 +1,165 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.calibre-web;
+
+  inherit (lib) concatStringsSep mkEnableOption mkIf mkOption optional optionalString types;
+in
+{
+  options = {
+    services.calibre-web = {
+      enable = mkEnableOption "Calibre-Web";
+
+      listen = {
+        ip = mkOption {
+          type = types.str;
+          default = "::1";
+          description = ''
+            IP address that Calibre-Web should listen on.
+          '';
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8083;
+          description = ''
+            Listen port for Calibre-Web.
+          '';
+        };
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "calibre-web";
+        description = ''
+          The directory below <filename>/var/lib</filename> where Calibre-Web stores its data.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "calibre-web";
+        description = "User account under which Calibre-Web runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "calibre-web";
+        description = "Group account under which Calibre-Web runs.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for the server.
+        '';
+      };
+
+      options = {
+        calibreLibrary = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          description = ''
+            Path to Calibre library.
+          '';
+        };
+
+        enableBookConversion = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Configure path to the Calibre's ebook-convert in the DB.
+          '';
+        };
+
+        enableBookUploading = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Allow books to be uploaded via Calibre-Web UI.
+          '';
+        };
+
+        reverseProxyAuth = {
+          enable = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Enable authorization using auth proxy.
+            '';
+          };
+
+          header = mkOption {
+            type = types.str;
+            default = "";
+            description = ''
+              Auth proxy header name.
+            '';
+          };
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.calibre-web = let
+      appDb = "/var/lib/${cfg.dataDir}/app.db";
+      gdriveDb = "/var/lib/${cfg.dataDir}/gdrive.db";
+      calibreWebCmd = "${pkgs.calibre-web}/bin/calibre-web -p ${appDb} -g ${gdriveDb}";
+
+      settings = concatStringsSep ", " (
+        [
+          "config_port = ${toString cfg.listen.port}"
+          "config_uploading = ${if cfg.options.enableBookUploading then "1" else "0"}"
+          "config_allow_reverse_proxy_header_login = ${if cfg.options.reverseProxyAuth.enable then "1" else "0"}"
+          "config_reverse_proxy_login_header_name = '${cfg.options.reverseProxyAuth.header}'"
+        ]
+        ++ optional (cfg.options.calibreLibrary != null) "config_calibre_dir = '${cfg.options.calibreLibrary}'"
+        ++ optional cfg.options.enableBookConversion "config_converterpath = '${pkgs.calibre}/bin/ebook-convert'"
+      );
+    in
+      {
+        description = "Web app for browsing, reading and downloading eBooks stored in a Calibre database";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = {
+          Type = "simple";
+          User = cfg.user;
+          Group = cfg.group;
+
+          StateDirectory = cfg.dataDir;
+          ExecStartPre = pkgs.writeShellScript "calibre-web-pre-start" (
+            ''
+              __RUN_MIGRATIONS_AND_EXIT=1 ${calibreWebCmd}
+
+              ${pkgs.sqlite}/bin/sqlite3 ${appDb} "update settings set ${settings}"
+            '' + optionalString (cfg.options.calibreLibrary != null) ''
+              test -f ${cfg.options.calibreLibrary}/metadata.db || { echo "Invalid Calibre library"; exit 1; }
+            ''
+          );
+
+          ExecStart = "${calibreWebCmd} -i ${cfg.listen.ip}";
+          Restart = "on-failure";
+        };
+      };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listen.port ];
+    };
+
+    users.users = mkIf (cfg.user == "calibre-web") {
+      calibre-web = {
+        isSystemUser = true;
+        group = cfg.group;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "calibre-web") {
+      calibre-web = {};
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ pborzenkov ];
+}
diff --git a/nixos/modules/services/web-apps/code-server.nix b/nixos/modules/services/web-apps/code-server.nix
new file mode 100644
index 00000000000..474e9140ae8
--- /dev/null
+++ b/nixos/modules/services/web-apps/code-server.nix
@@ -0,0 +1,139 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+
+  cfg = config.services.code-server;
+  defaultUser = "code-server";
+  defaultGroup = defaultUser;
+
+in {
+  ###### interface
+  options = {
+    services.code-server = {
+      enable = mkEnableOption "code-server";
+
+      package = mkOption {
+        default = pkgs.code-server;
+        defaultText = "pkgs.code-server";
+        description = "Which code-server derivation to use.";
+        type = types.package;
+      };
+
+      extraPackages = mkOption {
+        default = [ ];
+        description = "Packages that are available in the PATH of code-server.";
+        example = "[ pkgs.go ]";
+        type = types.listOf types.package;
+      };
+
+      extraEnvironment = mkOption {
+        type = types.attrsOf types.str;
+        description =
+          "Additional environment variables to passed to code-server.";
+        default = { };
+        example = { PKG_CONFIG_PATH = "/run/current-system/sw/lib/pkgconfig"; };
+      };
+
+      extraArguments = mkOption {
+        default = [ "--disable-telemetry" ];
+        description = "Additional arguments that passed to code-server";
+        example = ''[ "--verbose" ]'';
+        type = types.listOf types.str;
+      };
+
+      host = mkOption {
+        default = "127.0.0.1";
+        description = "The host-ip to bind to.";
+        type = types.str;
+      };
+
+      port = mkOption {
+        default = 4444;
+        description = "The port where code-server runs.";
+        type = types.port;
+      };
+
+      auth = mkOption {
+        default = "password";
+        description = "The type of authentication to use.";
+        type = types.enum [ "none" "password" ];
+      };
+
+      hashedPassword = mkOption {
+        default = "";
+        description =
+          "Create the password with: 'echo -n 'thisismypassword' | npx argon2-cli -e'.";
+        type = types.str;
+      };
+
+      user = mkOption {
+        default = defaultUser;
+        example = "yourUser";
+        description = ''
+          The user to run code-server as.
+          By default, a user named <literal>${defaultUser}</literal> will be created.
+        '';
+        type = types.str;
+      };
+
+      group = mkOption {
+        default = defaultGroup;
+        example = "yourGroup";
+        description = ''
+          The group to run code-server under.
+          By default, a group named <literal>${defaultGroup}</literal> will be created.
+        '';
+        type = types.str;
+      };
+
+      extraGroups = mkOption {
+        default = [ ];
+        description =
+          "An array of additional groups for the <literal>${defaultUser}</literal> user.";
+        example = [ "docker" ];
+        type = types.listOf types.str;
+      };
+
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    systemd.services.code-server = {
+      description = "VSCode server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      path = cfg.extraPackages;
+      environment = {
+        HASHED_PASSWORD = cfg.hashedPassword;
+      } // cfg.extraEnvironment;
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/code-server --bind-addr ${cfg.host}:${toString cfg.port} --auth ${cfg.auth} " + builtins.concatStringsSep " " cfg.extraArguments;
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        RuntimeDirectory = cfg.user;
+        User = cfg.user;
+        Group = cfg.group;
+        Restart = "on-failure";
+      };
+
+    };
+
+    users.users."${cfg.user}" = mkMerge [
+      (mkIf (cfg.user == defaultUser) {
+        isNormalUser = true;
+        description = "code-server user";
+        inherit (cfg) group;
+      })
+      {
+        packages = cfg.extraPackages;
+        inherit (cfg) extraGroups;
+      }
+    ];
+
+    users.groups."${defaultGroup}" = mkIf (cfg.group == defaultGroup) { };
+
+  };
+
+  meta.maintainers = with maintainers; [ stackshadow ];
+}
diff --git a/nixos/modules/services/web-apps/convos.nix b/nixos/modules/services/web-apps/convos.nix
new file mode 100644
index 00000000000..8be11eec9f3
--- /dev/null
+++ b/nixos/modules/services/web-apps/convos.nix
@@ -0,0 +1,72 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.convos;
+in
+{
+  options.services.convos = {
+    enable = mkEnableOption "Convos";
+    listenPort = mkOption {
+      type = types.port;
+      default = 3000;
+      example = 8080;
+      description = "Port the web interface should listen on";
+    };
+    listenAddress = mkOption {
+      type = types.str;
+      default = "*";
+      example = "127.0.0.1";
+      description = "Address or host the web interface should listen on";
+    };
+    reverseProxy = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enables reverse proxy support. This will allow Convos to automatically
+        pick up the <literal>X-Forwarded-For</literal> and
+        <literal>X-Request-Base</literal> HTTP headers set in your reverse proxy
+        web server. Note that enabling this option without a reverse proxy in
+        front will be a security issue.
+      '';
+    };
+  };
+  config = mkIf cfg.enable {
+    systemd.services.convos = {
+      description = "Convos Service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+      environment = {
+        CONVOS_HOME = "%S/convos";
+        CONVOS_REVERSE_PROXY = if cfg.reverseProxy then "1" else "0";
+        MOJO_LISTEN = "http://${toString cfg.listenAddress}:${toString cfg.listenPort}";
+      };
+      serviceConfig = {
+        ExecStart = "${pkgs.convos}/bin/convos daemon";
+        Restart = "on-failure";
+        StateDirectory = "convos";
+        WorkingDirectory = "%S/convos";
+        DynamicUser = true;
+        MemoryDenyWriteExecute = true;
+        ProtectHome = true;
+        ProtectClock = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+        LockPersonality = true;
+        RestrictRealtime = true;
+        RestrictNamespaces = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6"];
+        SystemCallFilter = "@system-service";
+        SystemCallArchitectures = "native";
+        CapabilityBoundingSet = "";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/cryptpad.nix b/nixos/modules/services/web-apps/cryptpad.nix
new file mode 100644
index 00000000000..e6772de768e
--- /dev/null
+++ b/nixos/modules/services/web-apps/cryptpad.nix
@@ -0,0 +1,54 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cryptpad;
+in
+{
+  options.services.cryptpad = {
+    enable = mkEnableOption "the Cryptpad service";
+
+    package = mkOption {
+      default = pkgs.cryptpad;
+      defaultText = literalExpression "pkgs.cryptpad";
+      type = types.package;
+      description = "
+        Cryptpad package to use.
+      ";
+    };
+
+    configFile = mkOption {
+      type = types.path;
+      default = "${cfg.package}/lib/node_modules/cryptpad/config/config.example.js";
+      defaultText = literalExpression ''"''${package}/lib/node_modules/cryptpad/config/config.example.js"'';
+      description = ''
+        Path to the JavaScript configuration file.
+
+        See <link
+        xlink:href="https://github.com/xwiki-labs/cryptpad/blob/master/config/config.example.js"/>
+        for a configuration example.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.cryptpad = {
+      description = "Cryptpad Service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        Environment = [
+          "CRYPTPAD_CONFIG=${cfg.configFile}"
+          "HOME=%S/cryptpad"
+        ];
+        ExecStart = "${cfg.package}/bin/cryptpad";
+        PrivateTmp = true;
+        Restart = "always";
+        StateDirectory = "cryptpad";
+        WorkingDirectory = "%S/cryptpad";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/dex.nix b/nixos/modules/services/web-apps/dex.nix
new file mode 100644
index 00000000000..4d4689a4cf2
--- /dev/null
+++ b/nixos/modules/services/web-apps/dex.nix
@@ -0,0 +1,118 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dex;
+  fixClient = client: if client ? secretFile then ((builtins.removeAttrs client [ "secretFile" ]) // { secret = client.secretFile; }) else client;
+  filteredSettings = mapAttrs (n: v: if n == "staticClients" then (builtins.map fixClient v) else v) cfg.settings;
+  secretFiles = flatten (builtins.map (c: if c ? secretFile then [ c.secretFile ] else []) (cfg.settings.staticClients or []));
+
+  settingsFormat = pkgs.formats.yaml {};
+  configFile = settingsFormat.generate "config.yaml" filteredSettings;
+
+  startPreScript = pkgs.writeShellScript "dex-start-pre" (''
+  '' + (concatStringsSep "\n" (builtins.map (file: ''
+    ${pkgs.replace-secret}/bin/replace-secret '${file}' '${file}' /run/dex/config.yaml
+  '') secretFiles)));
+in
+{
+  options.services.dex = {
+    enable = mkEnableOption "the OpenID Connect and OAuth2 identity provider";
+
+    settings = mkOption {
+      type = settingsFormat.type;
+      default = {};
+      example = literalExpression ''
+        {
+          # External url
+          issuer = "http://127.0.0.1:5556/dex";
+          storage = {
+            type = "postgres";
+            config.host = "/var/run/postgres";
+          };
+          web = {
+            http = "127.0.0.1:5556";
+          };
+          enablePasswordDB = true;
+          staticClients = [
+            {
+              id = "oidcclient";
+              name = "Client";
+              redirectURIs = [ "https://example.com/callback" ];
+              secretFile = "/etc/dex/oidcclient"; # The content of `secretFile` will be written into to the config as `secret`.
+            }
+          ];
+        }
+      '';
+      description = ''
+        The available options can be found in
+        <link xlink:href="https://github.com/dexidp/dex/blob/v${pkgs.dex.version}/config.yaml.dist">the example configuration</link>.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.dex = {
+      description = "dex identity provider";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ] ++ (optional (cfg.settings.storage.type == "postgres") "postgresql.service");
+
+      serviceConfig = {
+        ExecStart = "${pkgs.dex-oidc}/bin/dex serve /run/dex/config.yaml";
+        ExecStartPre = [
+          "${pkgs.coreutils}/bin/install -m 600 ${configFile} /run/dex/config.yaml"
+          "+${startPreScript}"
+        ];
+        RuntimeDirectory = "dex";
+
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        BindReadOnlyPaths = [
+          "/nix/store"
+          "-/etc/resolv.conf"
+          "-/etc/nsswitch.conf"
+          "-/etc/hosts"
+          "-/etc/localtime"
+          "-/etc/dex"
+        ];
+        BindPaths = optional (cfg.settings.storage.type == "postgres") "/var/run/postgresql";
+        CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        DynamicUser = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        # Port needs to be exposed to the host network
+        #PrivateNetwork = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        # Would re-mount paths ignored by temporary root
+        #ProtectSystem = "strict";
+        ProtectControlGroups = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
+        TemporaryFileSystem = "/:ro";
+        # Does not work well with the temporary root
+        #UMask = "0066";
+      };
+    };
+  };
+
+  # uses attributes of the linked package
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/web-apps/discourse.nix b/nixos/modules/services/web-apps/discourse.nix
new file mode 100644
index 00000000000..2c2911aada3
--- /dev/null
+++ b/nixos/modules/services/web-apps/discourse.nix
@@ -0,0 +1,1087 @@
+{ config, options, lib, pkgs, utils, ... }:
+
+let
+  json = pkgs.formats.json {};
+
+  cfg = config.services.discourse;
+  opt = options.services.discourse;
+
+  # Keep in sync with https://github.com/discourse/discourse_docker/blob/master/image/base/Dockerfile#L5
+  upstreamPostgresqlVersion = lib.getVersion pkgs.postgresql_13;
+
+  postgresqlPackage = if config.services.postgresql.enable then
+                        config.services.postgresql.package
+                      else
+                        pkgs.postgresql;
+
+  postgresqlVersion = lib.getVersion postgresqlPackage;
+
+  # We only want to create a database if we're actually going to connect to it.
+  databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == null;
+
+  tlsEnabled = (cfg.enableACME
+                || cfg.sslCertificate != null
+                || cfg.sslCertificateKey != null);
+in
+{
+  options = {
+    services.discourse = {
+      enable = lib.mkEnableOption "Discourse, an open source discussion platform";
+
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.discourse;
+        apply = p: p.override {
+          plugins = lib.unique (p.enabledPlugins ++ cfg.plugins);
+        };
+        defaultText = lib.literalExpression "pkgs.discourse";
+        description = ''
+          The discourse package to use.
+        '';
+      };
+
+      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 = "discourse.example.com";
+        description = ''
+          The hostname to serve Discourse on.
+        '';
+      };
+
+      secretKeyBaseFile = lib.mkOption {
+        type = with lib.types; nullOr path;
+        default = null;
+        example = "/run/keys/secret_key_base";
+        description = ''
+          The path to a file containing the
+          <literal>secret_key_base</literal> secret.
+
+          Discourse uses <literal>secret_key_base</literal> to encrypt
+          the cookie store, which contains session data, and to digest
+          user auth tokens.
+
+          Needs to be a 64 byte long string of hexadecimal
+          characters. You can generate one by running
+
+          <screen>
+          <prompt>$ </prompt>openssl rand -hex 64 >/path/to/secret_key_base_file
+          </screen>
+
+          This should be a string, not a nix path, since nix paths are
+          copied into the world-readable nix store.
+        '';
+      };
+
+      sslCertificate = lib.mkOption {
+        type = with lib.types; nullOr path;
+        default = null;
+        example = "/run/keys/ssl.cert";
+        description = ''
+          The path to the server SSL certificate. Set this to enable
+          SSL.
+        '';
+      };
+
+      sslCertificateKey = lib.mkOption {
+        type = with lib.types; nullOr path;
+        default = null;
+        example = "/run/keys/ssl.key";
+        description = ''
+          The path to the server SSL certificate key. Set this to
+          enable SSL.
+        '';
+      };
+
+      enableACME = lib.mkOption {
+        type = lib.types.bool;
+        default = cfg.sslCertificate == null && cfg.sslCertificateKey == null;
+        defaultText = lib.literalDocBook ''
+          <literal>true</literal>, unless <option>services.discourse.sslCertificate</option>
+          and <option>services.discourse.sslCertificateKey</option> are set.
+        '';
+        description = ''
+          Whether an ACME certificate should be used to secure
+          connections to the server.
+        '';
+      };
+
+      backendSettings = lib.mkOption {
+        type = with lib.types; attrsOf (nullOr (oneOf [ str int bool float ]));
+        default = {};
+        example = lib.literalExpression ''
+          {
+            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";
+          };
+        '';
+        description = ''
+          Additional settings to put in the
+          <filename>discourse.conf</filename> file.
+
+          Look in the
+          <link xlink:href="https://github.com/discourse/discourse/blob/master/config/discourse_defaults.conf">discourse_defaults.conf</link>
+          file in the upstream distribution to find available options.
+
+          Setting an option to <literal>null</literal> means
+          <quote>define variable, but leave right-hand side
+          empty</quote>.
+        '';
+      };
+
+      siteSettings = lib.mkOption {
+        type = json.type;
+        default = {};
+        example = lib.literalExpression ''
+          {
+            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;
+            };
+          };
+        '';
+        description = ''
+          Discourse site settings. These are the settings that can be
+          changed from the UI. This only defines their default values:
+          they can still be overridden from the UI.
+
+          Available settings can be found by looking in the
+          <link xlink:href="https://github.com/discourse/discourse/blob/master/config/site_settings.yml">site_settings.yml</link>
+          file of the upstream distribution. To find a setting's path,
+          you only need to care about the first two levels; i.e. its
+          category and name. See the example.
+
+          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>config/nixos_site_settings.json</filename> 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.
+        '';
+      };
+
+      admin = {
+        skipCreate = lib.mkOption {
+          type = lib.types.bool;
+          default = false;
+          description = ''
+            Do not create the admin account, instead rely on other
+            existing admin accounts.
+          '';
+        };
+
+        email = lib.mkOption {
+          type = lib.types.str;
+          example = "admin@example.com";
+          description = ''
+            The admin user email address.
+          '';
+        };
+
+        username = lib.mkOption {
+          type = lib.types.str;
+          example = "admin";
+          description = ''
+            The admin user username.
+          '';
+        };
+
+        fullName = lib.mkOption {
+          type = lib.types.str;
+          description = ''
+            The admin user's full name.
+          '';
+        };
+
+        passwordFile = lib.mkOption {
+          type = lib.types.path;
+          description = ''
+            A path to a file containing the admin user's password.
+
+            This should be a string, not a nix path, since nix paths are
+            copied into the world-readable nix store.
+          '';
+        };
+      };
+
+      nginx.enable = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = ''
+          Whether an <literal>nginx</literal> virtual host should be
+          set up to serve Discourse. Only disable if you're planning
+          to use a different web server, which is not recommended.
+        '';
+      };
+
+      database = {
+        pool = lib.mkOption {
+          type = lib.types.int;
+          default = 8;
+          description = ''
+            Database connection pool size.
+          '';
+        };
+
+        host = lib.mkOption {
+          type = with lib.types; nullOr str;
+          default = null;
+          description = ''
+            Discourse database hostname. <literal>null</literal> means <quote>prefer
+            local unix socket connection</quote>.
+          '';
+        };
+
+        passwordFile = lib.mkOption {
+          type = with lib.types; nullOr path;
+          default = null;
+          description = ''
+            File containing the Discourse database user password.
+
+            This should be a string, not a nix path, since nix paths are
+            copied into the world-readable nix store.
+          '';
+        };
+
+        createLocally = lib.mkOption {
+          type = lib.types.bool;
+          default = true;
+          description = ''
+            Whether a database should be automatically created on the
+            local host. Set this to <literal>false</literal> if you plan
+            on provisioning a local database yourself. This has no effect
+            if <option>services.discourse.database.host</option> is customized.
+          '';
+        };
+
+        name = lib.mkOption {
+          type = lib.types.str;
+          default = "discourse";
+          description = ''
+            Discourse database name.
+          '';
+        };
+
+        username = lib.mkOption {
+          type = lib.types.str;
+          default = "discourse";
+          description = ''
+            Discourse database user.
+          '';
+        };
+
+        ignorePostgresqlVersion = lib.mkOption {
+          type = lib.types.bool;
+          default = false;
+          description = ''
+            Whether to allow other versions of PostgreSQL than the
+            recommended one. Only effective when
+            <option>services.discourse.database.createLocally</option>
+            is enabled.
+          '';
+        };
+      };
+
+      redis = {
+        host = lib.mkOption {
+          type = lib.types.str;
+          default = "localhost";
+          description = ''
+            Redis server hostname.
+          '';
+        };
+
+        passwordFile = lib.mkOption {
+          type = with lib.types; nullOr path;
+          default = null;
+          description = ''
+            File containing the Redis password.
+
+            This should be a string, not a nix path, since nix paths are
+            copied into the world-readable nix store.
+          '';
+        };
+
+        dbNumber = lib.mkOption {
+          type = lib.types.int;
+          default = 0;
+          description = ''
+            Redis database number.
+          '';
+        };
+
+        useSSL = lib.mkOption {
+          type = lib.types.bool;
+          default = cfg.redis.host != "localhost";
+          defaultText = lib.literalExpression ''config.${opt.redis.host} != "localhost"'';
+          description = ''
+            Connect to Redis with SSL.
+          '';
+        };
+      };
+
+      mail = {
+        notificationEmailAddress = lib.mkOption {
+          type = lib.types.str;
+          default = "${if cfg.mail.incoming.enable then "notifications" else "noreply"}@${cfg.hostname}";
+          defaultText = lib.literalExpression ''
+            "''${if config.services.discourse.mail.incoming.enable then "notifications" else "noreply"}@''${config.services.discourse.hostname}"
+          '';
+          description = ''
+            The <literal>from:</literal> email address used when
+            sending all essential system emails. The domain specified
+            here must have SPF, DKIM and reverse PTR records set
+            correctly for email to arrive.
+          '';
+        };
+
+        contactEmailAddress = lib.mkOption {
+          type = lib.types.str;
+          default = "";
+          description = ''
+            Email address of key contact responsible for this
+            site. Used for critical notifications, as well as on the
+            <literal>/about</literal> contact form for urgent matters.
+          '';
+        };
+
+        outgoing = {
+          serverAddress = lib.mkOption {
+            type = lib.types.str;
+            default = "localhost";
+            description = ''
+              The address of the SMTP server Discourse should use to
+              send email.
+            '';
+          };
+
+          port = lib.mkOption {
+            type = lib.types.port;
+            default = 25;
+            description = ''
+              The port of the SMTP server Discourse should use to
+              send email.
+            '';
+          };
+
+          username = lib.mkOption {
+            type = with lib.types; nullOr str;
+            default = null;
+            description = ''
+              The username of the SMTP server.
+            '';
+          };
+
+          passwordFile = lib.mkOption {
+            type = lib.types.nullOr lib.types.path;
+            default = null;
+            description = ''
+              A file containing the password of the SMTP server account.
+
+              This should be a string, not a nix path, since nix paths
+              are copied into the world-readable nix store.
+            '';
+          };
+
+          domain = lib.mkOption {
+            type = lib.types.str;
+            default = cfg.hostname;
+            defaultText = lib.literalExpression "config.${opt.hostname}";
+            description = ''
+              HELO domain to use for outgoing mail.
+            '';
+          };
+
+          authentication = lib.mkOption {
+            type = with lib.types; nullOr (enum ["plain" "login" "cram_md5"]);
+            default = null;
+            description = ''
+              Authentication type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html
+            '';
+          };
+
+          enableStartTLSAuto = lib.mkOption {
+            type = lib.types.bool;
+            default = true;
+            description = ''
+              Whether to try to use StartTLS.
+            '';
+          };
+
+          opensslVerifyMode = lib.mkOption {
+            type = lib.types.str;
+            default = "peer";
+            description = ''
+              How OpenSSL checks the certificate, see http://api.rubyonrails.org/classes/ActionMailer/Base.html
+            '';
+          };
+
+          forceTLS = lib.mkOption {
+            type = lib.types.bool;
+            default = false;
+            description = ''
+              Force implicit TLS as per RFC 8314 3.3.
+            '';
+          };
+        };
+
+        incoming = {
+          enable = lib.mkOption {
+            type = lib.types.bool;
+            default = false;
+            description = ''
+              Whether to set up Postfix to receive incoming mail.
+            '';
+          };
+
+          replyEmailAddress = lib.mkOption {
+            type = lib.types.str;
+            default = "%{reply_key}@${cfg.hostname}";
+            defaultText = lib.literalExpression ''"%{reply_key}@''${config.services.discourse.hostname}"'';
+            description = ''
+              Template for reply by email incoming email address, for
+              example: %{reply_key}@reply.example.com or
+              replies+%{reply_key}@example.com
+            '';
+          };
+
+          mailReceiverPackage = lib.mkOption {
+            type = lib.types.package;
+            default = pkgs.discourse-mail-receiver;
+            defaultText = lib.literalExpression "pkgs.discourse-mail-receiver";
+            description = ''
+              The discourse-mail-receiver package to use.
+            '';
+          };
+
+          apiKeyFile = lib.mkOption {
+            type = lib.types.nullOr lib.types.path;
+            default = null;
+            description = ''
+              A file containing the Discourse API key used to add
+              posts and messages from mail. If left at its default
+              value <literal>null</literal>, one will be automatically
+              generated.
+
+              This should be a string, not a nix path, since nix paths
+              are copied into the world-readable nix store.
+            '';
+          };
+        };
+      };
+
+      plugins = lib.mkOption {
+        type = lib.types.listOf lib.types.package;
+        default = [];
+        example = lib.literalExpression ''
+          with config.services.discourse.package.plugins; [
+            discourse-canned-replies
+            discourse-github
+          ];
+        '';
+        description = ''
+          Plugins to install as part of
+          <productname>Discourse</productname>, expressed as a list of
+          derivations.
+        '';
+      };
+
+      sidekiqProcesses = lib.mkOption {
+        type = lib.types.int;
+        default = 1;
+        description = ''
+          How many Sidekiq processes should be spawned.
+        '';
+      };
+
+      unicornTimeout = lib.mkOption {
+        type = lib.types.int;
+        default = 30;
+        description = ''
+          Time in seconds before a request to Unicorn times out.
+
+          This can be raised if the system Discourse is running on is
+          too slow to handle many requests within 30 seconds.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = (cfg.database.host != null) -> (cfg.database.passwordFile != null);
+        message = "When services.gitlab.database.host is customized, services.discourse.database.passwordFile must be set!";
+      }
+      {
+        assertion = cfg.hostname != "";
+        message = "Could not automatically determine hostname, set service.discourse.hostname manually.";
+      }
+      {
+        assertion = cfg.database.ignorePostgresqlVersion || (databaseActuallyCreateLocally -> upstreamPostgresqlVersion == postgresqlVersion);
+        message = "The PostgreSQL version recommended for use with Discourse is ${upstreamPostgresqlVersion}, you're using ${postgresqlVersion}. "
+                  + "Either update your PostgreSQL package to the correct version or set services.discourse.database.ignorePostgresqlVersion. "
+                  + "See https://nixos.org/manual/nixos/stable/index.html#module-postgresql for details on how to upgrade PostgreSQL.";
+      }
+    ];
+
+
+    # Default config values are from `config/discourse_defaults.conf`
+    # upstream.
+    services.discourse.backendSettings = lib.mapAttrs (_: lib.mkDefault) {
+      db_pool = cfg.database.pool;
+      db_timeout = 5000;
+      db_connect_timeout = 5;
+      db_socket = null;
+      db_host = cfg.database.host;
+      db_backup_host = null;
+      db_port = null;
+      db_backup_port = 5432;
+      db_name = cfg.database.name;
+      db_username = if databaseActuallyCreateLocally then "discourse" else cfg.database.username;
+      db_password = cfg.database.passwordFile;
+      db_prepared_statements = false;
+      db_replica_host = null;
+      db_replica_port = null;
+      db_advisory_locks = true;
+
+      inherit (cfg) hostname;
+      backup_hostname = null;
+
+      smtp_address = cfg.mail.outgoing.serverAddress;
+      smtp_port = cfg.mail.outgoing.port;
+      smtp_domain = cfg.mail.outgoing.domain;
+      smtp_user_name = cfg.mail.outgoing.username;
+      smtp_password = cfg.mail.outgoing.passwordFile;
+      smtp_authentication = cfg.mail.outgoing.authentication;
+      smtp_enable_start_tls = cfg.mail.outgoing.enableStartTLSAuto;
+      smtp_openssl_verify_mode = cfg.mail.outgoing.opensslVerifyMode;
+      smtp_force_tls = cfg.mail.outgoing.forceTLS;
+
+      load_mini_profiler = true;
+      mini_profiler_snapshots_period = 0;
+      mini_profiler_snapshots_transport_url = null;
+      mini_profiler_snapshots_transport_auth_key = null;
+
+      cdn_url = null;
+      cdn_origin_hostname = null;
+      developer_emails = null;
+
+      redis_host = cfg.redis.host;
+      redis_port = 6379;
+      redis_replica_host = null;
+      redis_replica_port = 6379;
+      redis_db = cfg.redis.dbNumber;
+      redis_password = cfg.redis.passwordFile;
+      redis_skip_client_commands = false;
+      redis_use_ssl = cfg.redis.useSSL;
+
+      message_bus_redis_enabled = false;
+      message_bus_redis_host = "localhost";
+      message_bus_redis_port = 6379;
+      message_bus_redis_replica_host = null;
+      message_bus_redis_replica_port = 6379;
+      message_bus_redis_db = 0;
+      message_bus_redis_password = null;
+      message_bus_redis_skip_client_commands = false;
+
+      enable_cors = false;
+      cors_origin = "";
+      serve_static_assets = false;
+      sidekiq_workers = 5;
+      rtl_css = false;
+      connection_reaper_age = 30;
+      connection_reaper_interval = 30;
+      relative_url_root = null;
+      message_bus_max_backlog_size = 100;
+      secret_key_base = cfg.secretKeyBaseFile;
+      fallback_assets_path = null;
+
+      s3_bucket = null;
+      s3_region = null;
+      s3_access_key_id = null;
+      s3_secret_access_key = null;
+      s3_use_iam_profile = null;
+      s3_cdn_url = null;
+      s3_endpoint = null;
+      s3_http_continue_timeout = null;
+      s3_install_cors_rule = null;
+
+      max_user_api_reqs_per_minute = 20;
+      max_user_api_reqs_per_day = 2880;
+      max_admin_api_reqs_per_minute = 60;
+      max_reqs_per_ip_per_minute = 200;
+      max_reqs_per_ip_per_10_seconds = 50;
+      max_asset_reqs_per_ip_per_10_seconds = 200;
+      max_reqs_per_ip_mode = "block";
+      max_reqs_rate_limit_on_private = false;
+      skip_per_ip_rate_limit_trust_level = 1;
+      force_anonymous_min_queue_seconds = 1;
+      force_anonymous_min_per_10_seconds = 3;
+      background_requests_max_queue_length = 0.5;
+      reject_message_bus_queue_seconds = 0.1;
+      disable_search_queue_threshold = 1;
+      max_old_rebakes_per_15_minutes = 300;
+      max_logster_logs = 1000;
+      refresh_maxmind_db_during_precompile_days = 2;
+      maxmind_backup_path = null;
+      maxmind_license_key = null;
+      enable_performance_http_headers = false;
+      enable_js_error_reporting = true;
+      mini_scheduler_workers = 5;
+      compress_anon_cache = false;
+      anon_cache_store_threshold = 2;
+      allowed_theme_repos = null;
+      enable_email_sync_demon = false;
+      max_digests_enqueued_per_30_mins_per_site = 10000;
+      cluster_name = null;
+      multisite_config_path = "config/multisite.yml";
+      enable_long_polling = null;
+      long_polling_interval = null;
+    };
+
+    services.redis.enable = lib.mkDefault (cfg.redis.host == "localhost");
+
+    services.postgresql = lib.mkIf databaseActuallyCreateLocally {
+      enable = true;
+      ensureUsers = [{ name = "discourse"; }];
+    };
+
+    # The postgresql module doesn't currently support concepts like
+    # objects owners and extensions; for now we tack on what's needed
+    # here.
+    systemd.services.discourse-postgresql =
+      let
+        pgsql = config.services.postgresql;
+      in
+        lib.mkIf databaseActuallyCreateLocally {
+          after = [ "postgresql.service" ];
+          bindsTo = [ "postgresql.service" ];
+          wantedBy = [ "discourse.service" ];
+          partOf = [ "discourse.service" ];
+          path = [
+            pgsql.package
+          ];
+          script = ''
+            set -o errexit -o pipefail -o nounset -o errtrace
+            shopt -s inherit_errexit
+
+            psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'discourse'" | grep -q 1 || psql -tAc 'CREATE DATABASE "discourse" OWNER "discourse"'
+            psql '${cfg.database.name}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
+            psql '${cfg.database.name}' -tAc "CREATE EXTENSION IF NOT EXISTS hstore"
+          '';
+
+          serviceConfig = {
+            User = pgsql.superUser;
+            Type = "oneshot";
+            RemainAfterExit = true;
+          };
+        };
+
+    systemd.services.discourse = {
+      wantedBy = [ "multi-user.target" ];
+      after = [
+        "redis.service"
+        "postgresql.service"
+        "discourse-postgresql.service"
+      ];
+      bindsTo = [
+        "redis.service"
+      ] ++ lib.optionals (cfg.database.host == null) [
+        "postgresql.service"
+        "discourse-postgresql.service"
+      ];
+      path = cfg.package.runtimeDeps ++ [
+        postgresqlPackage
+        pkgs.replace-secret
+        cfg.package.rake
+      ];
+      environment = cfg.package.runtimeEnv // {
+        UNICORN_TIMEOUT = builtins.toString cfg.unicornTimeout;
+        UNICORN_SIDEKIQS = builtins.toString cfg.sidekiqProcesses;
+        MALLOC_ARENA_MAX = "2";
+      };
+
+      preStart =
+        let
+          discourseKeyValue = 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 null  ==   v then ""
+                else if isFloat    v then lib.strings.floatToString v
+                else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
+            };
+          };
+
+          discourseConf = pkgs.writeText "discourse.conf" (discourseKeyValue cfg.backendSettings);
+
+          mkSecretReplacement = file:
+            lib.optionalString (file != null) ''
+              replace-secret '${file}' '${file}' /run/discourse/config/discourse.conf
+            '';
+
+          mkAdmin = ''
+            export ADMIN_EMAIL="${cfg.admin.email}"
+            export ADMIN_NAME="${cfg.admin.fullName}"
+            export ADMIN_USERNAME="${cfg.admin.username}"
+            ADMIN_PASSWORD="$(<${cfg.admin.passwordFile})"
+            export ADMIN_PASSWORD
+            discourse-rake admin:create_noninteractively
+          '';
+
+        in ''
+          set -o errexit -o pipefail -o nounset -o errtrace
+          shopt -s inherit_errexit
+
+          umask u=rwx,g=rx,o=
+
+          rm -rf /var/lib/discourse/tmp/*
+
+          cp -r ${cfg.package}/share/discourse/config.dist/* /run/discourse/config/
+          cp -r ${cfg.package}/share/discourse/public.dist/* /run/discourse/public/
+          ln -sf /var/lib/discourse/uploads /run/discourse/public/uploads
+          ln -sf /var/lib/discourse/backups /run/discourse/public/backups
+
+          (
+              umask u=rwx,g=,o=
+
+              ${utils.genJqSecretsReplacementSnippet
+                  cfg.siteSettings
+                  "/run/discourse/config/nixos_site_settings.json"
+              }
+              install -T -m 0600 -o discourse ${discourseConf} /run/discourse/config/discourse.conf
+              ${mkSecretReplacement cfg.database.passwordFile}
+              ${mkSecretReplacement cfg.mail.outgoing.passwordFile}
+              ${mkSecretReplacement cfg.redis.passwordFile}
+              ${mkSecretReplacement cfg.secretKeyBaseFile}
+              chmod 0400 /run/discourse/config/discourse.conf
+          )
+
+          discourse-rake db:migrate >>/var/log/discourse/db_migration.log
+          chmod -R u+w /var/lib/discourse/tmp/
+
+          ${lib.optionalString (!cfg.admin.skipCreate) mkAdmin}
+
+          discourse-rake themes:update
+          discourse-rake uploads:regenerate_missing_optimized
+        '';
+
+      serviceConfig = {
+        Type = "simple";
+        User = "discourse";
+        Group = "discourse";
+        RuntimeDirectory = map (p: "discourse/" + p) [
+          "config"
+          "home"
+          "assets/javascripts/plugins"
+          "public"
+          "sockets"
+        ];
+        RuntimeDirectoryMode = 0750;
+        StateDirectory = map (p: "discourse/" + p) [
+          "uploads"
+          "backups"
+          "tmp"
+        ];
+        StateDirectoryMode = 0750;
+        LogsDirectory = "discourse";
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+        WorkingDirectory = "${cfg.package}/share/discourse";
+
+        RemoveIPC = true;
+        PrivateTmp = true;
+        NoNewPrivileges = true;
+        RestrictSUIDSGID = true;
+        ProtectSystem = "strict";
+        ProtectHome = "read-only";
+
+        ExecStart = "${cfg.package.rubyEnv}/bin/bundle exec config/unicorn_launcher -E production -c config/unicorn.conf.rb";
+      };
+    };
+
+    services.nginx = lib.mkIf cfg.nginx.enable {
+      enable = true;
+      additionalModules = [ pkgs.nginxModules.brotli ];
+
+      recommendedTlsSettings = true;
+      recommendedOptimisation = true;
+      recommendedGzipSettings = true;
+      recommendedProxySettings = true;
+
+      upstreams.discourse.servers."unix:/run/discourse/sockets/unicorn.sock" = {};
+
+      appendHttpConfig = ''
+        # inactive means we keep stuff around for 1440m minutes regardless of last access (1 week)
+        # levels means it is a 2 deep hierarchy cause we can have lots of files
+        # max_size limits the size of the cache
+        proxy_cache_path /var/cache/nginx inactive=1440m levels=1:2 keys_zone=discourse:10m max_size=600m;
+
+        # see: https://meta.discourse.org/t/x/74060
+        proxy_buffer_size 8k;
+      '';
+
+      virtualHosts.${cfg.hostname} = {
+        inherit (cfg) sslCertificate sslCertificateKey enableACME;
+        forceSSL = lib.mkDefault tlsEnabled;
+
+        root = "${cfg.package}/share/discourse/public";
+
+        locations =
+          let
+            proxy = { extraConfig ? "" }: {
+              proxyPass = "http://discourse";
+              extraConfig = extraConfig + ''
+                proxy_set_header X-Request-Start "t=''${msec}";
+              '';
+            };
+            cache = time: ''
+              expires ${time};
+              add_header Cache-Control public,immutable;
+            '';
+            cache_1y = cache "1y";
+            cache_1d = cache "1d";
+          in
+            {
+              "/".tryFiles = "$uri @discourse";
+              "@discourse" = proxy {};
+              "^~ /backups/".extraConfig = ''
+                internal;
+              '';
+              "/favicon.ico" = {
+                return = "204";
+                extraConfig = ''
+                  access_log off;
+                  log_not_found off;
+                '';
+              };
+              "~ ^/uploads/short-url/" = proxy {};
+              "~ ^/secure-media-uploads/" = proxy {};
+              "~* (fonts|assets|plugins|uploads)/.*\.(eot|ttf|woff|woff2|ico|otf)$".extraConfig = cache_1y + ''
+                add_header Access-Control-Allow-Origin *;
+              '';
+              "/srv/status" = proxy {
+                extraConfig = ''
+                  access_log off;
+                  log_not_found off;
+                '';
+              };
+              "~ ^/javascripts/".extraConfig = cache_1d;
+              "~ ^/assets/(?<asset_path>.+)$".extraConfig = cache_1y + ''
+                # asset pipeline enables this
+                brotli_static on;
+                gzip_static on;
+              '';
+              "~ ^/plugins/".extraConfig = cache_1y;
+              "~ /images/emoji/".extraConfig = cache_1y;
+              "~ ^/uploads/" = proxy {
+                extraConfig = cache_1y + ''
+                  proxy_set_header X-Sendfile-Type X-Accel-Redirect;
+                  proxy_set_header X-Accel-Mapping ${cfg.package}/share/discourse/public/=/downloads/;
+
+                  # custom CSS
+                  location ~ /stylesheet-cache/ {
+                      try_files $uri =404;
+                  }
+                  # this allows us to bypass rails
+                  location ~* \.(gif|png|jpg|jpeg|bmp|tif|tiff|ico|webp)$ {
+                      try_files $uri =404;
+                  }
+                  # SVG needs an extra header attached
+                  location ~* \.(svg)$ {
+                  }
+                  # thumbnails & optimized images
+                  location ~ /_?optimized/ {
+                      try_files $uri =404;
+                  }
+                '';
+              };
+              "~ ^/admin/backups/" = proxy {
+                extraConfig = ''
+                  proxy_set_header X-Sendfile-Type X-Accel-Redirect;
+                  proxy_set_header X-Accel-Mapping ${cfg.package}/share/discourse/public/=/downloads/;
+                '';
+              };
+              "~ ^/(svg-sprite/|letter_avatar/|letter_avatar_proxy/|user_avatar|highlight-js|stylesheets|theme-javascripts|favicon/proxied|service-worker)" = proxy {
+                extraConfig = ''
+                  # if Set-Cookie is in the response nothing gets cached
+                  # this is double bad cause we are not passing last modified in
+                  proxy_ignore_headers "Set-Cookie";
+                  proxy_hide_header "Set-Cookie";
+                  proxy_hide_header "X-Discourse-Username";
+                  proxy_hide_header "X-Runtime";
+
+                  # note x-accel-redirect can not be used with proxy_cache
+                  proxy_cache discourse;
+                  proxy_cache_key "$scheme,$host,$request_uri";
+                  proxy_cache_valid 200 301 302 7d;
+                  proxy_cache_valid any 1m;
+                '';
+              };
+              "/message-bus/" = proxy {
+                extraConfig = ''
+                  proxy_http_version 1.1;
+                  proxy_buffering off;
+                '';
+              };
+              "/downloads/".extraConfig = ''
+                internal;
+                alias ${cfg.package}/share/discourse/public/;
+              '';
+            };
+      };
+    };
+
+    systemd.services.discourse-mail-receiver-setup = lib.mkIf cfg.mail.incoming.enable (
+      let
+        mail-receiver-environment = {
+          MAIL_DOMAIN = cfg.hostname;
+          DISCOURSE_BASE_URL = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostname}";
+          DISCOURSE_API_KEY = "@api-key@";
+          DISCOURSE_API_USERNAME = "system";
+        };
+        mail-receiver-json = json.generate "mail-receiver.json" mail-receiver-environment;
+      in
+        {
+          before = [ "postfix.service" ];
+          after = [ "discourse.service" ];
+          wantedBy = [ "discourse.service" ];
+          partOf = [ "discourse.service" ];
+          path = [
+            cfg.package.rake
+            pkgs.jq
+          ];
+          preStart = lib.optionalString (cfg.mail.incoming.apiKeyFile == null) ''
+            set -o errexit -o pipefail -o nounset -o errtrace
+            shopt -s inherit_errexit
+
+            if [[ ! -e /var/lib/discourse-mail-receiver/api_key ]]; then
+                discourse-rake api_key:create_master[email-receiver] >/var/lib/discourse-mail-receiver/api_key
+            fi
+          '';
+          script =
+            let
+              apiKeyPath =
+                if cfg.mail.incoming.apiKeyFile == null then
+                  "/var/lib/discourse-mail-receiver/api_key"
+                else
+                  cfg.mail.incoming.apiKeyFile;
+            in ''
+              set -o errexit -o pipefail -o nounset -o errtrace
+              shopt -s inherit_errexit
+
+              api_key=$(<'${apiKeyPath}')
+              export api_key
+
+              jq <${mail-receiver-json} \
+                 '.DISCOURSE_API_KEY = $ENV.api_key' \
+                 >'/run/discourse-mail-receiver/mail-receiver-environment.json'
+            '';
+
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+            RuntimeDirectory = "discourse-mail-receiver";
+            RuntimeDirectoryMode = "0700";
+            StateDirectory = "discourse-mail-receiver";
+            User = "discourse";
+            Group = "discourse";
+          };
+        });
+
+    services.discourse.siteSettings = {
+      required = {
+        notification_email = cfg.mail.notificationEmailAddress;
+        contact_email = cfg.mail.contactEmailAddress;
+      };
+      email = {
+        manual_polling_enabled = cfg.mail.incoming.enable;
+        reply_by_email_enabled = cfg.mail.incoming.enable;
+        reply_by_email_address = cfg.mail.incoming.replyEmailAddress;
+      };
+    };
+
+    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 "";
+
+      origin = cfg.hostname;
+      relayDomains = [ cfg.hostname ];
+      config = {
+        smtpd_recipient_restrictions = "check_policy_service unix:private/discourse-policy";
+        append_dot_mydomain = lib.mkDefault false;
+        compatibility_level = "2";
+        smtputf8_enable = false;
+        smtpd_banner = lib.mkDefault "ESMTP server";
+        myhostname = lib.mkDefault cfg.hostname;
+        mydestination = lib.mkDefault "localhost";
+      };
+      transport = ''
+        ${cfg.hostname} discourse-mail-receiver:
+      '';
+      masterConfig = {
+        "discourse-mail-receiver" = {
+          type = "unix";
+          privileged = true;
+          chroot = false;
+          command = "pipe";
+          args = [
+            "user=discourse"
+            "argv=${cfg.mail.incoming.mailReceiverPackage}/bin/receive-mail"
+            "\${recipient}"
+          ];
+        };
+        "discourse-policy" = {
+          type = "unix";
+          privileged = true;
+          chroot = false;
+          command = "spawn";
+          args = [
+            "user=discourse"
+            "argv=${cfg.mail.incoming.mailReceiverPackage}/bin/discourse-smtp-fast-rejection"
+          ];
+        };
+      };
+    };
+
+    users.users = {
+      discourse = {
+        group = "discourse";
+        isSystemUser = true;
+      };
+    } // (lib.optionalAttrs cfg.nginx.enable {
+      ${config.services.nginx.user}.extraGroups = [ "discourse" ];
+    });
+
+    users.groups = {
+      discourse = {};
+    };
+
+    environment.systemPackages = [
+      cfg.package.rake
+    ];
+  };
+
+  meta.doc = ./discourse.xml;
+  meta.maintainers = [ lib.maintainers.talyz ];
+}
diff --git a/nixos/modules/services/web-apps/discourse.xml b/nixos/modules/services/web-apps/discourse.xml
new file mode 100644
index 00000000000..ad9b65abf51
--- /dev/null
+++ b/nixos/modules/services/web-apps/discourse.xml
@@ -0,0 +1,355 @@
+<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/documize.nix b/nixos/modules/services/web-apps/documize.nix
new file mode 100644
index 00000000000..7f2ed82ee33
--- /dev/null
+++ b/nixos/modules/services/web-apps/documize.nix
@@ -0,0 +1,150 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.documize;
+
+  mkParams = optional: concatMapStrings (name: let
+    predicate = optional -> cfg.${name} != null;
+    template = " -${name} '${toString cfg.${name}}'";
+  in optionalString predicate template);
+
+in {
+  options.services.documize = {
+    enable = mkEnableOption "Documize Wiki";
+
+    stateDirectoryName = mkOption {
+      type = types.str;
+      default = "documize";
+      description = ''
+        The name of the directory below <filename>/var/lib/private</filename>
+        where documize runs in and stores, for example, backups.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.documize-community;
+      defaultText = literalExpression "pkgs.documize-community";
+      description = ''
+        Which package to use for documize.
+      '';
+    };
+
+    salt = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "3edIYV6c8B28b19fh";
+      description = ''
+        The salt string used to encode JWT tokens, if not set a random value will be generated.
+      '';
+    };
+
+    cert = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The <filename>cert.pem</filename> file used for https.
+      '';
+    };
+
+    key = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The <filename>key.pem</filename> file used for https.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5001;
+      description = ''
+        The http/https port number.
+      '';
+    };
+
+    forcesslport = mkOption {
+      type = types.nullOr types.port;
+      default = null;
+      description = ''
+        Redirect given http port number to TLS.
+      '';
+    };
+
+    offline = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Set <literal>true</literal> for offline mode.
+      '';
+      apply = v: if true == v then 1 else 0;
+    };
+
+    dbtype = mkOption {
+      type = types.enum [ "mysql" "percona" "mariadb" "postgresql" "sqlserver" ];
+      default = "postgresql";
+      description = ''
+        Specify the database provider:
+        <simplelist type='inline'>
+          <member><literal>mysql</literal></member>
+          <member><literal>percona</literal></member>
+          <member><literal>mariadb</literal></member>
+          <member><literal>postgresql</literal></member>
+          <member><literal>sqlserver</literal></member>
+        </simplelist>
+      '';
+    };
+
+    db = mkOption {
+      type = types.str;
+      description = ''
+        Database specific connection string for example:
+        <itemizedlist>
+        <listitem><para>MySQL/Percona/MariaDB:
+          <literal>user:password@tcp(host:3306)/documize</literal>
+        </para></listitem>
+        <listitem><para>MySQLv8+:
+          <literal>user:password@tcp(host:3306)/documize?allowNativePasswords=true</literal>
+        </para></listitem>
+        <listitem><para>PostgreSQL:
+          <literal>host=localhost port=5432 dbname=documize user=admin password=secret sslmode=disable</literal>
+        </para></listitem>
+        <listitem><para>MSSQL:
+          <literal>sqlserver://username:password@localhost:1433?database=Documize</literal> or
+          <literal>sqlserver://sa@localhost/SQLExpress?database=Documize</literal>
+        </para></listitem>
+        </itemizedlist>
+      '';
+    };
+
+    location = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        reserved
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.documize-server = {
+      description = "Documize Wiki";
+      documentation = [ "https://documize.com/" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = concatStringsSep " " [
+          "${cfg.package}/bin/documize"
+          (mkParams false [ "db" "dbtype" "port" ])
+          (mkParams true [ "offline" "location" "forcesslport" "key" "cert" "salt" ])
+        ];
+        Restart = "always";
+        DynamicUser = "yes";
+        StateDirectory = cfg.stateDirectoryName;
+        WorkingDirectory = "/var/lib/${cfg.stateDirectoryName}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/dokuwiki.nix b/nixos/modules/services/web-apps/dokuwiki.nix
new file mode 100644
index 00000000000..1f8ca742db9
--- /dev/null
+++ b/nixos/modules/services/web-apps/dokuwiki.nix
@@ -0,0 +1,439 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dokuwiki;
+  eachSite = cfg.sites;
+  user = "dokuwiki";
+  webserver = config.services.${cfg.webserver};
+  stateDir = hostName: "/var/lib/dokuwiki/${hostName}/data";
+
+  dokuwikiAclAuthConfig = hostName: cfg: pkgs.writeText "acl.auth-${hostName}.php" ''
+    # acl.auth.php
+    # <?php exit()?>
+    #
+    # Access Control Lists
+    #
+    ${toString cfg.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}
+  '';
+
+  dokuwikiPluginsLocalConfig = hostName: cfg: pkgs.writeText "plugins.local-${hostName}.php" ''
+    <?php
+    ${cfg.pluginsConfig}
+  '';
+
+
+  pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
+    pname = "dokuwiki-${hostName}";
+    version = src.version;
+    src = cfg.package;
+
+    installPhase = ''
+      mkdir -p $out
+      cp -r * $out/
+
+      # symlink the dokuwiki config
+      ln -s ${dokuwikiLocalConfig hostName cfg} $out/share/dokuwiki/local.php
+
+      # symlink plugins config
+      ln -s ${dokuwikiPluginsLocalConfig hostName cfg} $out/share/dokuwiki/plugins.local.php
+
+      # symlink acl
+      ln -s ${dokuwikiAclAuthConfig hostName cfg} $out/share/dokuwiki/acl.auth.php
+
+      # 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}
+    '';
+  };
+
+  siteOpts = { config, lib, name, ... }:
+    {
+      options = {
+        enable = mkEnableOption "DokuWiki web application.";
+
+        package = mkOption {
+          type = types.package;
+          default = pkgs.dokuwiki;
+          defaultText = literalExpression "pkgs.dokuwiki";
+          description = "Which DokuWiki package to use.";
+        };
+
+        stateDir = mkOption {
+          type = types.path;
+          default = "/var/lib/dokuwiki/${name}/data";
+          description = "Location of the DokuWiki state directory.";
+        };
+
+        acl = mkOption {
+          type = types.nullOr types.lines;
+          default = null;
+          example = "*               @ALL               8";
+          description = ''
+            Access Control Lists: see <link xlink:href="https://www.dokuwiki.org/acl"/>
+            Mutually exclusive with services.dokuwiki.aclFile
+            Set this to a value other than null to take precedence over aclFile option.
+
+            Warning: Consider using aclFile instead if you do not
+            want to store the ACL in the world-readable Nix store.
+          '';
+        };
+
+        aclFile = mkOption {
+          type = with types; nullOr str;
+          default = if (config.aclUse && config.acl == null) then "/var/lib/dokuwiki/${name}/acl.auth.php" else null;
+          description = ''
+            Location of the dokuwiki acl rules. Mutually exclusive with services.dokuwiki.acl
+            Mutually exclusive with services.dokuwiki.acl which is preferred.
+            Consult documentation <link xlink:href="https://www.dokuwiki.org/acl"/> for further instructions.
+            Example: <link xlink:href="https://github.com/splitbrain/dokuwiki/blob/master/conf/acl.auth.php.dist"/>
+          '';
+          example = "/var/lib/dokuwiki/${name}/acl.auth.php";
+        };
+
+        aclUse = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            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;
+          '';
+          description = ''
+            List of the dokuwiki (un)loaded plugins.
+          '';
+        };
+
+        superUser = mkOption {
+          type = types.nullOr types.str;
+          default = "@admin";
+          description = ''
+            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 <link xlink:href="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;
+          description = ''
+            Location of the dokuwiki users file. List of users. Format:
+            login:passwordhash:Real Name:email:groups,comma,separated
+            Create passwordHash easily by using:$ mkpasswd -5 password `pwgen 8 1`
+            Example: <link xlink:href="https://github.com/splitbrain/dokuwiki/blob/master/conf/users.auth.php.dist"/>
+            '';
+          example = "/var/lib/dokuwiki/${name}/users.auth.php";
+        };
+
+        disableActions = mkOption {
+          type = types.nullOr types.str;
+          default = "";
+          example = "search,register";
+          description = ''
+            Disable individual action modes. Refer to
+            <link xlink:href="https://www.dokuwiki.org/config:action_modes"/>
+            for details on supported values.
+          '';
+        };
+
+        plugins = mkOption {
+          type = types.listOf types.path;
+          default = [];
+          description = ''
+                List of path(s) to respective plugin(s) which are copied from the 'plugin' directory.
+                <note><para>These plugins need to be packaged before use, see example.</para></note>
+          '';
+          example = literalExpression ''
+                let
+                  # Let's package the icalevents plugin
+                  plugin-icalevents = pkgs.stdenv.mkDerivation {
+                    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";
+                    };
+                    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:
+                in [ plugin-icalevents ]
+          '';
+        };
+
+        templates = mkOption {
+          type = types.listOf types.path;
+          default = [];
+          description = ''
+                List of path(s) to respective template(s) which are copied from the 'tpl' directory.
+                <note><para>These templates need to be packaged before use, see example.</para></note>
+          '';
+          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/";
+                  };
+                # And then pass this theme to the template list like this:
+                in [ template-bootstrap3 ]
+          '';
+        };
+
+        poolConfig = mkOption {
+          type = with types; attrsOf (oneOf [ str int bool ]);
+          default = {
+            "pm" = "dynamic";
+            "pm.max_children" = 32;
+            "pm.start_servers" = 2;
+            "pm.min_spare_servers" = 2;
+            "pm.max_spare_servers" = 4;
+            "pm.max_requests" = 500;
+          };
+          description = ''
+            Options for the DokuWiki PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+            for details on configuration directives.
+          '';
+        };
+
+        extraConfig = mkOption {
+          type = types.nullOr types.lines;
+          default = null;
+          example = ''
+            $conf['title'] = 'My Wiki';
+            $conf['userewrite'] = 1;
+          '';
+          description = ''
+            DokuWiki configuration. Refer to
+            <link xlink:href="https://www.dokuwiki.org/config"/>
+            for details on supported values.
+          '';
+        };
+
+      };
+
+    };
+in
+{
+  # interface
+  options = {
+    services.dokuwiki = {
+
+      sites = mkOption {
+        type = types.attrsOf (types.submodule siteOpts);
+        default = {};
+        description = "Specification of one or more DokuWiki sites to serve";
+      };
+
+      webserver = mkOption {
+        type = types.enum [ "nginx" "caddy" ];
+        default = "nginx";
+        description = ''
+          Whether to use nginx or caddy for virtual host management.
+
+          Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.&lt;name&gt;</literal>.
+          See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
+
+          Further apache2 configuration can be done by adapting <literal>services.httpd.virtualHosts.&lt;name&gt;</literal>.
+          See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+        '';
+      };
+
+    };
+  };
+
+  # 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);
+
+    services.phpfpm.pools = mapAttrs' (hostName: cfg: (
+      nameValuePair "dokuwiki-${hostName}" {
+        inherit user;
+        group = webserver.group;
+
+        # Not yet compatible with php 8 https://www.dokuwiki.org/requirements
+        # https://github.com/splitbrain/dokuwiki/issues/3545
+        phpPackage = pkgs.php74;
+        phpEnv = {
+          DOKUWIKI_LOCAL_CONFIG = "${dokuwikiLocalConfig hostName cfg}";
+          DOKUWIKI_PLUGINS_LOCAL_CONFIG = "${dokuwikiPluginsLocalConfig hostName cfg}";
+        } // optionalAttrs (cfg.usersFile != null) {
+          DOKUWIKI_USERS_AUTH_CONFIG = "${cfg.usersFile}";
+        } //optionalAttrs (cfg.aclUse) {
+          DOKUWIKI_ACL_AUTH_CONFIG = if (cfg.acl != null) then "${dokuwikiAclAuthConfig hostName cfg}" else "${toString cfg.aclFile}";
+        };
+
+        settings = {
+          "listen.owner" = webserver.user;
+          "listen.group" = webserver.group;
+        } // cfg.poolConfig;
+      }
+    )) eachSite;
+
+  }
+
+  {
+    systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
+      "d ${stateDir hostName}/attic 0750 ${user} ${webserver.group} - -"
+      "d ${stateDir hostName}/cache 0750 ${user} ${webserver.group} - -"
+      "d ${stateDir hostName}/index 0750 ${user} ${webserver.group} - -"
+      "d ${stateDir hostName}/locks 0750 ${user} ${webserver.group} - -"
+      "d ${stateDir hostName}/media 0750 ${user} ${webserver.group} - -"
+      "d ${stateDir hostName}/media_attic 0750 ${user} ${webserver.group} - -"
+      "d ${stateDir hostName}/media_meta 0750 ${user} ${webserver.group} - -"
+      "d ${stateDir hostName}/meta 0750 ${user} ${webserver.group} - -"
+      "d ${stateDir hostName}/pages 0750 ${user} ${webserver.group} - -"
+      "d ${stateDir hostName}/tmp 0750 ${user} ${webserver.group} - -"
+    ] ++ lib.optional (cfg.aclFile != null) "C ${cfg.aclFile} 0640 ${user} ${webserver.group} - ${pkg hostName cfg}/share/dokuwiki/conf/acl.auth.php.dist"
+    ++ lib.optional (cfg.usersFile != null) "C ${cfg.usersFile} 0640 ${user} ${webserver.group} - ${pkg hostName cfg}/share/dokuwiki/conf/users.auth.php.dist"
+    ) eachSite);
+
+    users.users.${user} = {
+      group = webserver.group;
+      isSystemUser = true;
+    };
+  }
+
+  (mkIf (cfg.webserver == "nginx") {
+    services.nginx = {
+      enable = true;
+      virtualHosts = mapAttrs (hostName: cfg: {
+        serverName = mkDefault hostName;
+        root = "${pkg hostName cfg}/share/dokuwiki";
+
+        locations = {
+          "~ /(conf/|bin/|inc/|install.php)" = {
+            extraConfig = "deny all;";
+          };
+
+          "~ ^/data/" = {
+            root = "${stateDir hostName}";
+            extraConfig = "internal;";
+          };
+
+          "~ ^/lib.*\.(js|css|gif|png|ico|jpg|jpeg)$" = {
+            extraConfig = "expires 365d;";
+          };
+
+          "/" = {
+            priority = 1;
+            index = "doku.php";
+            extraConfig = ''try_files $uri $uri/ @dokuwiki;'';
+          };
+
+          "@dokuwiki" = {
+            extraConfig = ''
+              # rewrites "doku.php/" out of the URLs if you set the userwrite setting to .htaccess in dokuwiki config page
+              rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last;
+              rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;
+              rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;
+              rewrite ^/(.*) /doku.php?id=$1&$args last;
+            '';
+          };
+
+          "~ \\.php$" = {
+            extraConfig = ''
+              try_files $uri $uri/ /doku.php;
+              include ${config.services.nginx.package}/conf/fastcgi_params;
+              fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+              fastcgi_param REDIRECT_STATUS 200;
+              fastcgi_pass unix:${config.services.phpfpm.pools."dokuwiki-${hostName}".socket};
+              '';
+          };
+
+        };
+      }) eachSite;
+    };
+  })
+
+  (mkIf (cfg.webserver == "caddy") {
+    services.caddy = {
+      enable = true;
+      virtualHosts = mapAttrs' (hostName: cfg: (
+        nameValuePair "http://${hostName}" {
+          extraConfig = ''
+            root * ${pkg hostName cfg}/share/dokuwiki
+            file_server
+
+            encode zstd gzip
+            php_fastcgi unix/${config.services.phpfpm.pools."dokuwiki-${hostName}".socket}
+
+            @restrict_files {
+              path /data/* /conf/* /bin/* /inc/* /vendor/* /install.php
+            }
+
+            respond @restrict_files 404
+
+            @allow_media {
+              path_regexp path ^/_media/(.*)$
+            }
+            rewrite @allow_media /lib/exe/fetch.php?media=/{http.regexp.path.1}
+
+            @allow_detail   {
+              path /_detail*
+            }
+            rewrite @allow_detail /lib/exe/detail.php?media={path}
+
+            @allow_export   {
+              path /_export*
+              path_regexp export /([^/]+)/(.*)
+            }
+            rewrite @allow_export /doku.php?do=export_{http.regexp.export.1}&id={http.regexp.export.2}
+
+            try_files {path} {path}/ /doku.php?id={path}&{query}
+          '';
+        }
+      )) eachSite;
+    };
+  })
+
+  ]);
+
+  meta.maintainers = with maintainers; [
+    _1000101
+    onny
+    dandellion
+  ];
+}
diff --git a/nixos/modules/services/web-apps/engelsystem.nix b/nixos/modules/services/web-apps/engelsystem.nix
new file mode 100644
index 00000000000..06c3c6dfc3d
--- /dev/null
+++ b/nixos/modules/services/web-apps/engelsystem.nix
@@ -0,0 +1,186 @@
+{ config, lib, pkgs, utils, ... }:
+
+let
+  inherit (lib) mkDefault mkEnableOption mkIf mkOption types literalExpression;
+  cfg = config.services.engelsystem;
+in {
+  options = {
+    services.engelsystem = {
+      enable = mkOption {
+        default = false;
+        example = true;
+        description = ''
+          Whether to enable engelsystem, an online tool for coordinating volunteers
+          and shifts on large events.
+        '';
+        type = lib.types.bool;
+      };
+
+      domain = mkOption {
+        type = types.str;
+        example = "engelsystem.example.com";
+        description = "Domain to serve on.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        description = "Engelsystem package used for the service.";
+        default = pkgs.engelsystem;
+        defaultText = literalExpression "pkgs.engelsystem";
+      };
+
+      createDatabase = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to create a local database automatically.
+          This will override every database setting in <option>services.engelsystem.config</option>.
+        '';
+      };
+    };
+
+    services.engelsystem.config = mkOption {
+      type = types.attrs;
+      default = {
+        database = {
+          host = "localhost";
+          database = "engelsystem";
+          username = "engelsystem";
+        };
+      };
+      example = {
+        maintenance = false;
+        database = {
+          host = "database.example.com";
+          database = "engelsystem";
+          username = "engelsystem";
+          password._secret = "/var/keys/engelsystem/database";
+        };
+        email = {
+          driver = "smtp";
+          host = "smtp.example.com";
+          port = 587;
+          from.address = "engelsystem@example.com";
+          from.name = "example engelsystem";
+          encryption = "tls";
+          username = "engelsystem@example.com";
+          password._secret = "/var/keys/engelsystem/mail";
+        };
+        autoarrive = true;
+        min_password_length = 6;
+        default_locale = "de_DE";
+      };
+      description = ''
+        Options to be added to config.php, as a nix attribute set. Options containing secret data
+        should be set to an attribute set containing the attribute _secret - a string pointing to a
+        file containing the value the option should be set to. See the example to get a better
+        picture of this: in the resulting config.php file, the email.password key will be set to
+        the contents of the /var/keys/engelsystem/mail file.
+
+        See https://engelsystem.de/doc/admin/configuration/ for available options.
+
+        Note that the admin user login credentials cannot be set here - they always default to
+        admin:asdfasdf. Log in and change them immediately.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # create database
+    services.mysql = mkIf cfg.createDatabase {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureUsers = [{
+        name = "engelsystem";
+        ensurePermissions = { "engelsystem.*" = "ALL PRIVILEGES"; };
+      }];
+      ensureDatabases = [ "engelsystem" ];
+    };
+
+    environment.etc."engelsystem/config.php".source =
+      pkgs.writeText "config.php" ''
+        <?php
+        return json_decode(file_get_contents("/var/lib/engelsystem/config.json"), true);
+      '';
+
+    services.phpfpm.pools.engelsystem = {
+      user = "engelsystem";
+      settings = {
+        "listen.owner" = config.services.nginx.user;
+        "pm" = "dynamic";
+        "pm.max_children" = 32;
+        "pm.max_requests" = 500;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 2;
+        "pm.max_spare_servers" = 5;
+        "php_admin_value[error_log]" = "stderr";
+        "php_admin_flag[log_errors]" = true;
+        "catch_workers_output" = true;
+      };
+    };
+
+    services.nginx = {
+      enable = true;
+      virtualHosts."${cfg.domain}".locations = {
+        "/" = {
+          root = "${cfg.package}/share/engelsystem/public";
+          extraConfig = ''
+            index index.php;
+            try_files $uri $uri/ /index.php?$args;
+            autoindex off;
+          '';
+        };
+        "~ \\.php$" = {
+          root = "${cfg.package}/share/engelsystem/public";
+          extraConfig = ''
+            fastcgi_pass unix:${config.services.phpfpm.pools.engelsystem.socket};
+            fastcgi_index index.php;
+            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+            include ${config.services.nginx.package}/conf/fastcgi_params;
+            include ${config.services.nginx.package}/conf/fastcgi.conf;
+          '';
+        };
+      };
+    };
+
+    systemd.services."engelsystem-init" = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = { Type = "oneshot"; };
+      script =
+        let
+          genConfigScript = pkgs.writeScript "engelsystem-gen-config.sh"
+            (utils.genJqSecretsReplacementSnippet cfg.config "config.json");
+        in ''
+          umask 077
+          mkdir -p /var/lib/engelsystem/storage/app
+          mkdir -p /var/lib/engelsystem/storage/cache/views
+          cd /var/lib/engelsystem
+          ${genConfigScript}
+          chmod 400 config.json
+          chown -R engelsystem .
+      '';
+    };
+    systemd.services."engelsystem-migrate" = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        User = "engelsystem";
+        Group = "engelsystem";
+      };
+      script = ''
+        ${cfg.package}/bin/migrate
+      '';
+      after = [ "engelsystem-init.service" "mysql.service" ];
+    };
+    systemd.services."phpfpm-engelsystem".after =
+      [ "engelsystem-migrate.service" ];
+
+    users.users.engelsystem = {
+      isSystemUser = true;
+      createHome = true;
+      home = "/var/lib/engelsystem/storage";
+      group = "engelsystem";
+    };
+    users.groups.engelsystem = { };
+  };
+}
diff --git a/nixos/modules/services/web-apps/ethercalc.nix b/nixos/modules/services/web-apps/ethercalc.nix
new file mode 100644
index 00000000000..d74def59c6c
--- /dev/null
+++ b/nixos/modules/services/web-apps/ethercalc.nix
@@ -0,0 +1,62 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ethercalc;
+in {
+  options = {
+    services.ethercalc = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          ethercalc, an online collaborative spreadsheet server.
+
+          Persistent state will be maintained under
+          <filename>/var/lib/ethercalc</filename>. Upstream supports using a
+          redis server for storage and recommends the redis backend for
+          intensive use; however, the Nix module doesn't currently support
+          redis.
+
+          Note that while ethercalc is a good and robust project with an active
+          issue tracker, there haven't been new commits since the end of 2020.
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.ethercalc;
+        defaultText = literalExpression "pkgs.ethercalc";
+        type = types.package;
+        description = "Ethercalc package to use.";
+      };
+
+      host = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "Address to listen on (use 0.0.0.0 to allow access from any address).";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 8000;
+        description = "Port to bind to.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.ethercalc = {
+      description = "Ethercalc service";
+      wantedBy    = [ "multi-user.target" ];
+      after       = [ "network.target" ];
+      serviceConfig = {
+        DynamicUser    =   true;
+        ExecStart        = "${cfg.package}/bin/ethercalc --host ${cfg.host} --port ${toString cfg.port}";
+        Restart          = "always";
+        StateDirectory   = "ethercalc";
+        WorkingDirectory = "/var/lib/ethercalc";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/fluidd.nix b/nixos/modules/services/web-apps/fluidd.nix
new file mode 100644
index 00000000000..6ac1acc9d03
--- /dev/null
+++ b/nixos/modules/services/web-apps/fluidd.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.fluidd;
+  moonraker = config.services.moonraker;
+in
+{
+  options.services.fluidd = {
+    enable = mkEnableOption "Fluidd, a Klipper web interface for managing your 3d printer";
+
+    package = mkOption {
+      type = types.package;
+      description = "Fluidd package to be used in the module";
+      default = pkgs.fluidd;
+      defaultText = literalExpression "pkgs.fluidd";
+    };
+
+    hostName = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = "Hostname to serve fluidd on";
+    };
+
+    nginx = mkOption {
+      type = types.submodule
+        (import ../web-servers/nginx/vhost-options.nix { inherit config lib; });
+      default = { };
+      example = literalExpression ''
+        {
+          serverAliases = [ "fluidd.''${config.networking.domain}" ];
+        }
+      '';
+      description = "Extra configuration for the nginx virtual host of fluidd.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.nginx = {
+      enable = true;
+      upstreams.fluidd-apiserver.servers."${moonraker.address}:${toString moonraker.port}" = { };
+      virtualHosts."${cfg.hostName}" = mkMerge [
+        cfg.nginx
+        {
+          root = mkForce "${cfg.package}/share/fluidd/htdocs";
+          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://fluidd-apiserver/websocket";
+            };
+            "~ ^/(printer|api|access|machine|server)/" = {
+              proxyWebsockets = true;
+              proxyPass = "http://fluidd-apiserver$request_uri";
+            };
+          };
+        }
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/galene.nix b/nixos/modules/services/web-apps/galene.nix
new file mode 100644
index 00000000000..1d0a620585b
--- /dev/null
+++ b/nixos/modules/services/web-apps/galene.nix
@@ -0,0 +1,185 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.galene;
+  opt = options.services.galene;
+  defaultstateDir = "/var/lib/galene";
+  defaultrecordingsDir = "${cfg.stateDir}/recordings";
+  defaultgroupsDir = "${cfg.stateDir}/groups";
+  defaultdataDir = "${cfg.stateDir}/data";
+in
+{
+  options = {
+    services.galene = {
+      enable = mkEnableOption "Galene Service.";
+
+      stateDir = mkOption {
+        default = defaultstateDir;
+        type = types.str;
+        description = ''
+          The directory where Galene stores its internal state. If left as the default
+          value this directory will automatically be created before the Galene server
+          starts, otherwise the sysadmin is responsible for ensuring the directory
+          exists with appropriate ownership and permissions.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "galene";
+        description = "User account under which galene runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "galene";
+        description = "Group under which galene runs.";
+      };
+
+      insecure = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether Galene should listen in http or in https. If left as the default
+          value (false), Galene needs to be fed a private key and a certificate.
+        '';
+      };
+
+      certFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/path/to/your/cert.pem";
+        description = ''
+          Path to the server's certificate. The file is copied at runtime to
+          Galene's data directory where it needs to reside.
+        '';
+      };
+
+      keyFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/path/to/your/key.pem";
+        description = ''
+          Path to the server's private key. The file is copied at runtime to
+          Galene's data directory where it needs to reside.
+        '';
+      };
+
+      httpAddress = mkOption {
+        type = types.str;
+        default = "";
+        description = "HTTP listen address for galene.";
+      };
+
+      httpPort = mkOption {
+        type = types.port;
+        default = 8443;
+        description = "HTTP listen port.";
+      };
+
+      staticDir = mkOption {
+        type = types.str;
+        default = "${cfg.package.static}/static";
+        defaultText = literalExpression ''"''${package.static}/static"'';
+        example = "/var/lib/galene/static";
+        description = "Web server directory.";
+      };
+
+      recordingsDir = mkOption {
+        type = types.str;
+        default = defaultrecordingsDir;
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/recordings"'';
+        example = "/var/lib/galene/recordings";
+        description = "Recordings directory.";
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = defaultdataDir;
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/data"'';
+        example = "/var/lib/galene/data";
+        description = "Data directory.";
+      };
+
+      groupsDir = mkOption {
+        type = types.str;
+        default = defaultgroupsDir;
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/groups"'';
+        example = "/var/lib/galene/groups";
+        description = "Web server directory.";
+      };
+
+      package = mkOption {
+        default = pkgs.galene;
+        defaultText = literalExpression "pkgs.galene";
+        type = types.package;
+        description = ''
+          Package for running Galene.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.insecure || (cfg.certFile != null && cfg.keyFile != null);
+        message = ''
+          Galene needs both certFile and keyFile defined for encryption, or
+          the insecure flag.
+        '';
+      }
+    ];
+
+    systemd.services.galene = {
+      description = "galene";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        ${optionalString (cfg.insecure != true) ''
+           install -m 700 -o '${cfg.user}' -g '${cfg.group}' ${cfg.certFile} ${cfg.dataDir}/cert.pem
+           install -m 700 -o '${cfg.user}' -g '${cfg.group}' ${cfg.keyFile} ${cfg.dataDir}/key.pem
+        ''}
+      '';
+
+      serviceConfig = mkMerge [
+        {
+          Type = "simple";
+          User = cfg.user;
+          Group = cfg.group;
+          WorkingDirectory = cfg.stateDir;
+          ExecStart = ''${cfg.package}/bin/galene \
+          ${optionalString (cfg.insecure) "-insecure"} \
+          -data ${cfg.dataDir} \
+          -groups ${cfg.groupsDir} \
+          -recordings ${cfg.recordingsDir} \
+          -static ${cfg.staticDir}'';
+          Restart = "always";
+          # Upstream Requirements
+          LimitNOFILE = 65536;
+          StateDirectory = [ ] ++
+            optional (cfg.stateDir == defaultstateDir) "galene" ++
+            optional (cfg.dataDir == defaultdataDir) "galene/data" ++
+            optional (cfg.groupsDir == defaultgroupsDir) "galene/groups" ++
+            optional (cfg.recordingsDir == defaultrecordingsDir) "galene/recordings";
+        }
+      ];
+    };
+
+    users.users = mkIf (cfg.user == "galene")
+      {
+        galene = {
+          description = "galene Service";
+          group = cfg.group;
+          isSystemUser = true;
+        };
+      };
+
+    users.groups = mkIf (cfg.group == "galene") {
+      galene = { };
+    };
+  };
+  meta.maintainers = with lib.maintainers; [ rgrunbla ];
+}
diff --git a/nixos/modules/services/web-apps/gerrit.nix b/nixos/modules/services/web-apps/gerrit.nix
new file mode 100644
index 00000000000..6bfc67368dd
--- /dev/null
+++ b/nixos/modules/services/web-apps/gerrit.nix
@@ -0,0 +1,242 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.gerrit;
+
+  # NixOS option type for git-like configs
+  gitIniType = with types;
+    let
+      primitiveType = either str (either bool int);
+      multipleType = either primitiveType (listOf primitiveType);
+      sectionType = lazyAttrsOf multipleType;
+      supersectionType = lazyAttrsOf (either multipleType sectionType);
+    in lazyAttrsOf supersectionType;
+
+  gerritConfig = pkgs.writeText "gerrit.conf" (
+    lib.generators.toGitINI cfg.settings
+  );
+
+  replicationConfig = pkgs.writeText "replication.conf" (
+    lib.generators.toGitINI cfg.replicationSettings
+  );
+
+  # Wrap the gerrit java with all the java options so it can be called
+  # like a normal CLI app
+  gerrit-cli = pkgs.writeShellScriptBin "gerrit" ''
+    set -euo pipefail
+    jvmOpts=(
+      ${lib.escapeShellArgs cfg.jvmOpts}
+      -Xmx${cfg.jvmHeapLimit}
+    )
+    exec ${cfg.jvmPackage}/bin/java \
+      "''${jvmOpts[@]}" \
+      -jar ${cfg.package}/webapps/${cfg.package.name}.war \
+      "$@"
+  '';
+
+  gerrit-plugins = pkgs.runCommand
+    "gerrit-plugins"
+    {
+      buildInputs = [ gerrit-cli ];
+    }
+    ''
+      shopt -s nullglob
+      mkdir $out
+
+      for name in ${toString cfg.builtinPlugins}; do
+        echo "Installing builtin plugin $name.jar"
+        gerrit cat plugins/$name.jar > $out/$name.jar
+      done
+
+      for file in ${toString cfg.plugins}; do
+        name=$(echo "$file" | cut -d - -f 2-)
+        echo "Installing plugin $name"
+        ln -sf "$file" $out/$name
+      done
+    '';
+in
+{
+  options = {
+    services.gerrit = {
+      enable = mkEnableOption "Gerrit service";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.gerrit;
+        defaultText = literalExpression "pkgs.gerrit";
+        description = "Gerrit package to use";
+      };
+
+      jvmPackage = mkOption {
+        type = types.package;
+        default = pkgs.jre_headless;
+        defaultText = literalExpression "pkgs.jre_headless";
+        description = "Java Runtime Environment package to use";
+      };
+
+      jvmOpts = mkOption {
+        type = types.listOf types.str;
+        default = [
+          "-Dflogger.backend_factory=com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance"
+          "-Dflogger.logging_context=com.google.gerrit.server.logging.LoggingContext#getInstance"
+        ];
+        description = "A list of JVM options to start gerrit with.";
+      };
+
+      jvmHeapLimit = mkOption {
+        type = types.str;
+        default = "1024m";
+        description = ''
+          How much memory to allocate to the JVM heap
+        '';
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "[::]:8080";
+        description = ''
+          <literal>hostname:port</literal> to listen for HTTP traffic.
+
+          This is bound using the systemd socket activation.
+        '';
+      };
+
+      settings = mkOption {
+        type = gitIniType;
+        default = {};
+        description = ''
+          Gerrit configuration. This will be generated to the
+          <literal>etc/gerrit.config</literal> file.
+        '';
+      };
+
+      replicationSettings = mkOption {
+        type = gitIniType;
+        default = {};
+        description = ''
+          Replication configuration. This will be generated to the
+          <literal>etc/replication.config</literal> file.
+        '';
+      };
+
+      plugins = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        description = ''
+          List of plugins to add to Gerrit. Each derivation is a jar file
+          itself where the name of the derivation is the name of plugin.
+        '';
+      };
+
+      builtinPlugins = mkOption {
+        type = types.listOf (types.enum cfg.package.passthru.plugins);
+        default = [];
+        description = ''
+          List of builtins plugins to install. Those are shipped in the
+          <literal>gerrit.war</literal> file.
+        '';
+      };
+
+      serverId = mkOption {
+        type = types.str;
+        description = ''
+          Set a UUID that uniquely identifies the server.
+
+          This can be generated with
+          <literal>nix-shell -p util-linux --run uuidgen</literal>.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = cfg.replicationSettings != {} -> elem "replication" cfg.builtinPlugins;
+        message = "Gerrit replicationSettings require enabling the replication plugin";
+      }
+    ];
+
+    services.gerrit.settings = {
+      cache.directory = "/var/cache/gerrit";
+      container.heapLimit = cfg.jvmHeapLimit;
+      gerrit.basePath = lib.mkDefault "git";
+      gerrit.serverId = cfg.serverId;
+      httpd.inheritChannel = "true";
+      httpd.listenUrl = lib.mkDefault "http://${cfg.listenAddress}";
+      index.type = lib.mkDefault "lucene";
+    };
+
+    # Add the gerrit CLI to the system to run `gerrit init` and friends.
+    environment.systemPackages = [ gerrit-cli ];
+
+    systemd.sockets.gerrit = {
+      unitConfig.Description = "Gerrit HTTP socket";
+      wantedBy = [ "sockets.target" ];
+      listenStreams = [ cfg.listenAddress ];
+    };
+
+    systemd.services.gerrit = {
+      description = "Gerrit";
+
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "gerrit.socket" ];
+      after = [ "gerrit.socket" "network.target" ];
+
+      path = [
+        gerrit-cli
+        pkgs.bash
+        pkgs.coreutils
+        pkgs.git
+        pkgs.openssh
+      ];
+
+      environment = {
+        GERRIT_HOME = "%S/gerrit";
+        GERRIT_TMP = "%T";
+        HOME = "%S/gerrit";
+        XDG_CONFIG_HOME = "%S/gerrit/.config";
+      };
+
+      preStart = ''
+        set -euo pipefail
+
+        # bootstrap if nothing exists
+        if [[ ! -d git ]]; then
+          gerrit init --batch --no-auto-start
+        fi
+
+        # install gerrit.war for the plugin manager
+        rm -rf bin
+        mkdir bin
+        ln -sfv ${cfg.package}/webapps/${cfg.package.name}.war bin/gerrit.war
+
+        # copy the config, keep it mutable because Gerrit
+        ln -sfv ${gerritConfig} etc/gerrit.config
+        ln -sfv ${replicationConfig} etc/replication.config
+
+        # install the plugins
+        rm -rf plugins
+        ln -sv ${gerrit-plugins} plugins
+      ''
+      ;
+
+      serviceConfig = {
+        CacheDirectory = "gerrit";
+        DynamicUser = true;
+        ExecStart = "${gerrit-cli}/bin/gerrit daemon --console-log";
+        LimitNOFILE = 4096;
+        StandardInput = "socket";
+        StandardOutput = "journal";
+        StateDirectory = "gerrit";
+        WorkingDirectory = "%S/gerrit";
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ edef zimbatm ];
+  # uses attributes of the linked package
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/web-apps/gotify-server.nix b/nixos/modules/services/web-apps/gotify-server.nix
new file mode 100644
index 00000000000..03e01f46a94
--- /dev/null
+++ b/nixos/modules/services/web-apps/gotify-server.nix
@@ -0,0 +1,49 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gotify;
+in {
+  options = {
+    services.gotify = {
+      enable = mkEnableOption "Gotify webserver";
+
+      port = mkOption {
+        type = types.port;
+        description = ''
+          Port the server listens to.
+        '';
+      };
+
+      stateDirectoryName = mkOption {
+        type = types.str;
+        default = "gotify-server";
+        description = ''
+          The name of the directory below <filename>/var/lib</filename> where
+          gotify stores its runtime data.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.gotify-server = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      description = "Simple server for sending and receiving messages";
+
+      environment = {
+        GOTIFY_SERVER_PORT = toString cfg.port;
+      };
+
+      serviceConfig = {
+        WorkingDirectory = "/var/lib/${cfg.stateDirectoryName}";
+        StateDirectory = cfg.stateDirectoryName;
+        Restart = "always";
+        DynamicUser = "yes";
+        ExecStart = "${pkgs.gotify-server}/bin/server";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/grocy.nix b/nixos/modules/services/web-apps/grocy.nix
new file mode 100644
index 00000000000..be2de638dd9
--- /dev/null
+++ b/nixos/modules/services/web-apps/grocy.nix
@@ -0,0 +1,172 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.grocy;
+in {
+  options.services.grocy = {
+    enable = mkEnableOption "grocy";
+
+    hostName = mkOption {
+      type = types.str;
+      description = ''
+        FQDN for the grocy instance.
+      '';
+    };
+
+    nginx.enableSSL = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether or not to enable SSL (with ACME and let's encrypt)
+        for the grocy vhost.
+      '';
+    };
+
+    phpfpm.settings = mkOption {
+      type = with types; attrsOf (oneOf [ int str bool ]);
+      default = {
+        "pm" = "dynamic";
+        "php_admin_value[error_log]" = "stderr";
+        "php_admin_flag[log_errors]" = true;
+        "listen.owner" = "nginx";
+        "catch_workers_output" = true;
+        "pm.max_children" = "32";
+        "pm.start_servers" = "2";
+        "pm.min_spare_servers" = "2";
+        "pm.max_spare_servers" = "4";
+        "pm.max_requests" = "500";
+      };
+
+      description = ''
+        Options for grocy's PHPFPM pool.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/grocy";
+      description = ''
+        Home directory of the <literal>grocy</literal> user which contains
+        the application's state.
+      '';
+    };
+
+    settings = {
+      currency = mkOption {
+        type = types.str;
+        default = "USD";
+        example = "EUR";
+        description = ''
+          ISO 4217 code for the currency to display.
+        '';
+      };
+
+      culture = mkOption {
+        type = types.enum [ "de" "en" "da" "en_GB" "es" "fr" "hu" "it" "nl" "no" "pl" "pt_BR" "ru" "sk_SK" "sv_SE" "tr" ];
+        default = "en";
+        description = ''
+          Display language of the frontend.
+        '';
+      };
+
+      calendar = {
+        showWeekNumber = mkOption {
+          default = true;
+          type = types.bool;
+          description = ''
+            Show the number of the weeks in the calendar views.
+          '';
+        };
+        firstDayOfWeek = mkOption {
+          default = null;
+          type = types.nullOr (types.enum (range 0 6));
+          description = ''
+            Which day of the week (0=Sunday, 1=Monday etc.) should be the
+            first day.
+          '';
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."grocy/config.php".text = ''
+      <?php
+      Setting('CULTURE', '${cfg.settings.culture}');
+      Setting('CURRENCY', '${cfg.settings.currency}');
+      Setting('CALENDAR_FIRST_DAY_OF_WEEK', '${toString cfg.settings.calendar.firstDayOfWeek}');
+      Setting('CALENDAR_SHOW_WEEK_OF_YEAR', ${boolToString cfg.settings.calendar.showWeekNumber});
+    '';
+
+    users.users.grocy = {
+      isSystemUser = true;
+      createHome = true;
+      home = cfg.dataDir;
+      group = "nginx";
+    };
+
+    systemd.tmpfiles.rules = map (
+      dirName: "d '${cfg.dataDir}/${dirName}' - grocy nginx - -"
+    ) [ "viewcache" "plugins" "settingoverrides" "storage" ];
+
+    services.phpfpm.pools.grocy = {
+      user = "grocy";
+      group = "nginx";
+
+      # PHP 7.4 is the only version which is supported/tested by upstream:
+      # https://github.com/grocy/grocy/blob/v3.0.0/README.md#how-to-install
+      phpPackage = pkgs.php74;
+
+      inherit (cfg.phpfpm) settings;
+
+      phpEnv = {
+        GROCY_CONFIG_FILE = "/etc/grocy/config.php";
+        GROCY_DB_FILE = "${cfg.dataDir}/grocy.db";
+        GROCY_STORAGE_DIR = "${cfg.dataDir}/storage";
+        GROCY_PLUGIN_DIR = "${cfg.dataDir}/plugins";
+        GROCY_CACHE_DIR = "${cfg.dataDir}/viewcache";
+      };
+    };
+
+    services.nginx = {
+      enable = true;
+      virtualHosts."${cfg.hostName}" = mkMerge [
+        { root = "${pkgs.grocy}/public";
+          locations."/".extraConfig = ''
+            rewrite ^ /index.php;
+          '';
+          locations."~ \\.php$".extraConfig = ''
+            fastcgi_split_path_info ^(.+\.php)(/.+)$;
+            fastcgi_pass unix:${config.services.phpfpm.pools.grocy.socket};
+            include ${config.services.nginx.package}/conf/fastcgi.conf;
+            include ${config.services.nginx.package}/conf/fastcgi_params;
+          '';
+          locations."~ \\.(js|css|ttf|woff2?|png|jpe?g|svg)$".extraConfig = ''
+            add_header Cache-Control "public, max-age=15778463";
+            add_header X-Content-Type-Options nosniff;
+            add_header X-XSS-Protection "1; mode=block";
+            add_header X-Robots-Tag none;
+            add_header X-Download-Options noopen;
+            add_header X-Permitted-Cross-Domain-Policies none;
+            add_header Referrer-Policy no-referrer;
+            access_log off;
+          '';
+          extraConfig = ''
+            try_files $uri /index.php;
+          '';
+        }
+        (mkIf cfg.nginx.enableSSL {
+          enableACME = true;
+          forceSSL = true;
+        })
+      ];
+    };
+  };
+
+  meta = {
+    maintainers = with maintainers; [ ma27 ];
+    doc = ./grocy.xml;
+  };
+}
diff --git a/nixos/modules/services/web-apps/grocy.xml b/nixos/modules/services/web-apps/grocy.xml
new file mode 100644
index 00000000000..fdf6d00f4b1
--- /dev/null
+++ b/nixos/modules/services/web-apps/grocy.xml
@@ -0,0 +1,77 @@
+<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
new file mode 100644
index 00000000000..9eeabb9d566
--- /dev/null
+++ b/nixos/modules/services/web-apps/hedgedoc.nix
@@ -0,0 +1,1038 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.hedgedoc;
+
+  # 21.03 will not be an official release - it was instead 21.05.  This
+  # versionAtLeast statement remains set to 21.03 for backwards compatibility.
+  # See https://github.com/NixOS/nixpkgs/pull/108899 and
+  # https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md.
+  name = if versionAtLeast config.system.stateVersion "21.03"
+    then "hedgedoc"
+    else "codimd";
+
+  prettyJSON = conf:
+    pkgs.runCommandLocal "hedgedoc-config.json" {
+      nativeBuildInputs = [ pkgs.jq ];
+    } ''
+      echo '${builtins.toJSON conf}' | jq \
+        '{production:del(.[]|nulls)|del(.[][]?|nulls)}' > $out
+    '';
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "codimd" ] [ "services" "hedgedoc" ])
+  ];
+
+  options.services.hedgedoc = {
+    enable = mkEnableOption "the HedgeDoc Markdown Editor";
+
+    groups = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Groups to which the service user should be added.
+      '';
+    };
+
+    workDir = mkOption {
+      type = types.path;
+      default = "/var/lib/${name}";
+      description = ''
+        Working directory for the HedgeDoc service.
+      '';
+    };
+
+    configuration = {
+      debug = mkEnableOption "debug mode";
+      domain = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "hedgedoc.org";
+        description = ''
+          Domain name for the HedgeDoc instance.
+        '';
+      };
+      urlPath = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/url/path/to/hedgedoc";
+        description = ''
+          Path under which HedgeDoc is accessible.
+        '';
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = ''
+          Address to listen on.
+        '';
+      };
+      port = mkOption {
+        type = types.int;
+        default = 3000;
+        example = 80;
+        description = ''
+          Port to listen on.
+        '';
+      };
+      path = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/run/hedgedoc.sock";
+        description = ''
+          Specify where a UNIX domain socket should be placed.
+        '';
+      };
+      allowOrigin = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "localhost" "hedgedoc.org" ];
+        description = ''
+          List of domains to whitelist.
+        '';
+      };
+      useSSL = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable to use SSL server. This will also enable
+          <option>protocolUseSSL</option>.
+        '';
+      };
+      hsts = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether to enable HSTS if HTTPS is also enabled.
+          '';
+        };
+        maxAgeSeconds = mkOption {
+          type = types.int;
+          default = 31536000;
+          description = ''
+            Max duration for clients to keep the HSTS status.
+          '';
+        };
+        includeSubdomains = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether to include subdomains in HSTS.
+          '';
+        };
+        preload = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether to allow preloading of the site's HSTS status.
+          '';
+        };
+      };
+      csp = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        example = literalExpression ''
+          {
+            enable = true;
+            directives = {
+              scriptSrc = "trustworthy.scripts.example.com";
+            };
+            upgradeInsecureRequest = "auto";
+            addDefaults = true;
+          }
+        '';
+        description = ''
+          Specify the Content Security Policy which is passed to Helmet.
+          For configuration details see <link xlink:href="https://helmetjs.github.io/docs/csp/"
+          >https://helmetjs.github.io/docs/csp/</link>.
+        '';
+      };
+      protocolUseSSL = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable to use TLS for resource paths.
+          This only applies when <option>domain</option> is set.
+        '';
+      };
+      urlAddPort = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable to add the port to callback URLs.
+          This only applies when <option>domain</option> is set
+          and only for ports other than 80 and 443.
+        '';
+      };
+      useCDN = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to use CDN resources or not.
+        '';
+      };
+      allowAnonymous = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to allow anonymous usage.
+        '';
+      };
+      allowAnonymousEdits = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to allow guests to edit existing notes with the `freely' permission,
+          when <option>allowAnonymous</option> is enabled.
+        '';
+      };
+      allowFreeURL = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to allow note creation by accessing a nonexistent note URL.
+        '';
+      };
+      defaultPermission = mkOption {
+        type = types.enum [ "freely" "editable" "limited" "locked" "private" ];
+        default = "editable";
+        description = ''
+          Default permissions for notes.
+          This only applies for signed-in users.
+        '';
+      };
+      dbURL = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = ''
+          postgres://user:pass@host:5432/dbname
+        '';
+        description = ''
+          Specify which database to use.
+          HedgeDoc supports mysql, postgres, sqlite and mssql.
+          See <link xlink:href="https://sequelize.readthedocs.io/en/v3/">
+          https://sequelize.readthedocs.io/en/v3/</link> for more information.
+          Note: This option overrides <option>db</option>.
+        '';
+      };
+      db = mkOption {
+        type = types.attrs;
+        default = {};
+        example = literalExpression ''
+          {
+            dialect = "sqlite";
+            storage = "/var/lib/${name}/db.${name}.sqlite";
+          }
+        '';
+        description = ''
+          Specify the configuration for sequelize.
+          HedgeDoc supports mysql, postgres, sqlite and mssql.
+          See <link xlink:href="https://sequelize.readthedocs.io/en/v3/">
+          https://sequelize.readthedocs.io/en/v3/</link> for more information.
+          Note: This option overrides <option>db</option>.
+        '';
+      };
+      sslKeyPath= mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/var/lib/hedgedoc/hedgedoc.key";
+        description = ''
+          Path to the SSL key. Needed when <option>useSSL</option> is enabled.
+        '';
+      };
+      sslCertPath = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/var/lib/hedgedoc/hedgedoc.crt";
+        description = ''
+          Path to the SSL cert. Needed when <option>useSSL</option> is enabled.
+        '';
+      };
+      sslCAPath = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "/var/lib/hedgedoc/ca.crt" ];
+        description = ''
+          SSL ca chain. Needed when <option>useSSL</option> is enabled.
+        '';
+      };
+      dhParamPath = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/var/lib/hedgedoc/dhparam.pem";
+        description = ''
+          Path to the SSL dh params. Needed when <option>useSSL</option> is enabled.
+        '';
+      };
+      tmpPath = mkOption {
+        type = types.str;
+        default = "/tmp";
+        description = ''
+          Path to the temp directory HedgeDoc should use.
+          Note that <option>serviceConfig.PrivateTmp</option> is enabled for
+          the HedgeDoc systemd service by default.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
+        '';
+      };
+      defaultNotePath = mkOption {
+        type = types.nullOr types.str;
+        default = "./public/default.md";
+        description = ''
+          Path to the default Note file.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
+        '';
+      };
+      docsPath = mkOption {
+        type = types.nullOr types.str;
+        default = "./public/docs";
+        description = ''
+          Path to the docs directory.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
+        '';
+      };
+      indexPath = mkOption {
+        type = types.nullOr types.str;
+        default = "./public/views/index.ejs";
+        description = ''
+          Path to the index template file.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
+        '';
+      };
+      hackmdPath = mkOption {
+        type = types.nullOr types.str;
+        default = "./public/views/hackmd.ejs";
+        description = ''
+          Path to the hackmd template file.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
+        '';
+      };
+      errorPath = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        defaultText = literalExpression "./public/views/error.ejs";
+        description = ''
+          Path to the error template file.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
+        '';
+      };
+      prettyPath = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        defaultText = literalExpression "./public/views/pretty.ejs";
+        description = ''
+          Path to the pretty template file.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
+        '';
+      };
+      slidePath = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        defaultText = literalExpression "./public/views/slide.hbs";
+        description = ''
+          Path to the slide template file.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
+        '';
+      };
+      uploadsPath = mkOption {
+        type = types.str;
+        default = "${cfg.workDir}/uploads";
+        defaultText = literalExpression "/var/lib/${name}/uploads";
+        description = ''
+          Path under which uploaded files are saved.
+        '';
+      };
+      sessionName = mkOption {
+        type = types.str;
+        default = "connect.sid";
+        description = ''
+          Specify the name of the session cookie.
+        '';
+      };
+      sessionSecret = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Specify the secret used to sign the session cookie.
+          If unset, one will be generated on startup.
+        '';
+      };
+      sessionLife = mkOption {
+        type = types.int;
+        default = 1209600000;
+        description = ''
+          Session life time in milliseconds.
+        '';
+      };
+      heartbeatInterval = mkOption {
+        type = types.int;
+        default = 5000;
+        description = ''
+          Specify the socket.io heartbeat interval.
+        '';
+      };
+      heartbeatTimeout = mkOption {
+        type = types.int;
+        default = 10000;
+        description = ''
+          Specify the socket.io heartbeat timeout.
+        '';
+      };
+      documentMaxLength = mkOption {
+        type = types.int;
+        default = 100000;
+        description = ''
+          Specify the maximum document length.
+        '';
+      };
+      email = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable email sign-in.
+        '';
+      };
+      allowEmailRegister = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable email registration.
+        '';
+      };
+      allowGravatar = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to use gravatar as profile picture source.
+        '';
+      };
+      imageUploadType = mkOption {
+        type = types.enum [ "imgur" "s3" "minio" "filesystem" ];
+        default = "filesystem";
+        description = ''
+          Specify where to upload images.
+        '';
+      };
+      minio = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            accessKey = mkOption {
+              type = types.str;
+              description = ''
+                Minio access key.
+              '';
+            };
+            secretKey = mkOption {
+              type = types.str;
+              description = ''
+                Minio secret key.
+              '';
+            };
+            endpoint = mkOption {
+              type = types.str;
+              description = ''
+                Minio endpoint.
+              '';
+            };
+            port = mkOption {
+              type = types.int;
+              default = 9000;
+              description = ''
+                Minio listen port.
+              '';
+            };
+            secure = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                Whether to use HTTPS for Minio.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the minio third-party integration.";
+      };
+      s3 = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            accessKeyId = mkOption {
+              type = types.str;
+              description = ''
+                AWS access key id.
+              '';
+            };
+            secretAccessKey = mkOption {
+              type = types.str;
+              description = ''
+                AWS access key.
+              '';
+            };
+            region = mkOption {
+              type = types.str;
+              description = ''
+                AWS S3 region.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the s3 third-party integration.";
+      };
+      s3bucket = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Specify the bucket name for upload types <literal>s3</literal> and <literal>minio</literal>.
+        '';
+      };
+      allowPDFExport = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable PDF exports.
+        '';
+      };
+      imgur.clientId = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Imgur API client ID.
+        '';
+      };
+      azure = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            connectionString = mkOption {
+              type = types.str;
+              description = ''
+                Azure Blob Storage connection string.
+              '';
+            };
+            container = mkOption {
+              type = types.str;
+              description = ''
+                Azure Blob Storage container name.
+                It will be created if non-existent.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the azure third-party integration.";
+      };
+      oauth2 = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            authorizationURL = mkOption {
+              type = types.str;
+              description = ''
+                Specify the OAuth authorization URL.
+              '';
+            };
+            tokenURL = mkOption {
+              type = types.str;
+              description = ''
+                Specify the OAuth token URL.
+              '';
+            };
+            baseURL = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the OAuth base URL.
+              '';
+            };
+            userProfileURL = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the OAuth userprofile URL.
+              '';
+            };
+            userProfileUsernameAttr = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the name of the attribute for the username from the claim.
+              '';
+            };
+            userProfileDisplayNameAttr = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the name of the attribute for the display name from the claim.
+              '';
+            };
+            userProfileEmailAttr = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the name of the attribute for the email from the claim.
+              '';
+            };
+            scope = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the OAuth scope.
+              '';
+            };
+            providerName = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the name to be displayed for this strategy.
+              '';
+            };
+            rolesClaim = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the role claim name.
+              '';
+            };
+            accessRole = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify role which should be included in the ID token roles claim to grant access
+              '';
+            };
+            clientID = mkOption {
+              type = types.str;
+              description = ''
+                Specify the OAuth client ID.
+              '';
+            };
+            clientSecret = mkOption {
+              type = types.str;
+              description = ''
+                Specify the OAuth client secret.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the OAuth integration.";
+      };
+      facebook = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            clientID = mkOption {
+              type = types.str;
+              description = ''
+                Facebook API client ID.
+              '';
+            };
+            clientSecret = mkOption {
+              type = types.str;
+              description = ''
+                Facebook API client secret.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the facebook third-party integration";
+      };
+      twitter = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            consumerKey = mkOption {
+              type = types.str;
+              description = ''
+                Twitter API consumer key.
+              '';
+            };
+            consumerSecret = mkOption {
+              type = types.str;
+              description = ''
+                Twitter API consumer secret.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the Twitter third-party integration.";
+      };
+      github = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            clientID = mkOption {
+              type = types.str;
+              description = ''
+                GitHub API client ID.
+              '';
+            };
+            clientSecret = mkOption {
+              type = types.str;
+              description = ''
+                Github API client secret.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the GitHub third-party integration.";
+      };
+      gitlab = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            baseURL = mkOption {
+              type = types.str;
+              default = "";
+              description = ''
+                GitLab API authentication endpoint.
+                Only needed for other endpoints than gitlab.com.
+              '';
+            };
+            clientID = mkOption {
+              type = types.str;
+              description = ''
+                GitLab API client ID.
+              '';
+            };
+            clientSecret = mkOption {
+              type = types.str;
+              description = ''
+                GitLab API client secret.
+              '';
+            };
+            scope = mkOption {
+              type = types.enum [ "api" "read_user" ];
+              default = "api";
+              description = ''
+                GitLab API requested scope.
+                GitLab snippet import/export requires api scope.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the GitLab third-party integration.";
+      };
+      mattermost = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            baseURL = mkOption {
+              type = types.str;
+              description = ''
+                Mattermost authentication endpoint.
+              '';
+            };
+            clientID = mkOption {
+              type = types.str;
+              description = ''
+                Mattermost API client ID.
+              '';
+            };
+            clientSecret = mkOption {
+              type = types.str;
+              description = ''
+                Mattermost API client secret.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the Mattermost third-party integration.";
+      };
+      dropbox = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            clientID = mkOption {
+              type = types.str;
+              description = ''
+                Dropbox API client ID.
+              '';
+            };
+            clientSecret = mkOption {
+              type = types.str;
+              description = ''
+                Dropbox API client secret.
+              '';
+            };
+            appKey = mkOption {
+              type = types.str;
+              description = ''
+                Dropbox app key.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the Dropbox third-party integration.";
+      };
+      google = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            clientID = mkOption {
+              type = types.str;
+              description = ''
+                Google API client ID.
+              '';
+            };
+            clientSecret = mkOption {
+              type = types.str;
+              description = ''
+                Google API client secret.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the Google third-party integration.";
+      };
+      ldap = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            providerName = mkOption {
+              type = types.str;
+              default = "";
+              description = ''
+                Optional name to be displayed at login form, indicating the LDAP provider.
+              '';
+            };
+            url = mkOption {
+              type = types.str;
+              example = "ldap://localhost";
+              description = ''
+                URL of LDAP server.
+              '';
+            };
+            bindDn = mkOption {
+              type = types.str;
+              description = ''
+                Bind DN for LDAP access.
+              '';
+            };
+            bindCredentials = mkOption {
+              type = types.str;
+              description = ''
+                Bind credentials for LDAP access.
+              '';
+            };
+            searchBase = mkOption {
+              type = types.str;
+              example = "o=users,dc=example,dc=com";
+              description = ''
+                LDAP directory to begin search from.
+              '';
+            };
+            searchFilter = mkOption {
+              type = types.str;
+              example = "(uid={{username}})";
+              description = ''
+                LDAP filter to search with.
+              '';
+            };
+            searchAttributes = mkOption {
+              type = types.listOf types.str;
+              example = [ "displayName" "mail" ];
+              description = ''
+                LDAP attributes to search with.
+              '';
+            };
+            userNameField = mkOption {
+              type = types.str;
+              default = "";
+              description = ''
+                LDAP field which is used as the username on HedgeDoc.
+                By default <option>useridField</option> is used.
+              '';
+            };
+            useridField = mkOption {
+              type = types.str;
+              example = "uid";
+              description = ''
+                LDAP field which is a unique identifier for users on HedgeDoc.
+              '';
+            };
+            tlsca = mkOption {
+              type = types.str;
+              example = "server-cert.pem,root.pem";
+              description = ''
+                Root CA for LDAP TLS in PEM format.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the LDAP integration.";
+      };
+      saml = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            idpSsoUrl = mkOption {
+              type = types.str;
+              example = "https://idp.example.com/sso";
+              description = ''
+                IdP authentication endpoint.
+              '';
+            };
+            idpCert = mkOption {
+              type = types.path;
+              example = "/path/to/cert.pem";
+              description = ''
+                Path to IdP certificate file in PEM format.
+              '';
+            };
+            issuer = mkOption {
+              type = types.str;
+              default = "";
+              description = ''
+                Optional identity of the service provider.
+                This defaults to the server URL.
+              '';
+            };
+            identifierFormat = mkOption {
+              type = types.str;
+              default = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress";
+              description = ''
+                Optional name identifier format.
+              '';
+            };
+            groupAttribute = mkOption {
+              type = types.str;
+              default = "";
+              example = "memberOf";
+              description = ''
+                Optional attribute name for group list.
+              '';
+            };
+            externalGroups = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [ "Temporary-staff" "External-users" ];
+              description = ''
+                Excluded group names.
+              '';
+            };
+            requiredGroups = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [ "Hedgedoc-Users" ];
+              description = ''
+                Required group names.
+              '';
+            };
+            attribute = {
+              id = mkOption {
+                type = types.str;
+                default = "";
+                description = ''
+                  Attribute map for `id'.
+                  Defaults to `NameID' of SAML response.
+                '';
+              };
+              username = mkOption {
+                type = types.str;
+                default = "";
+                description = ''
+                  Attribute map for `username'.
+                  Defaults to `NameID' of SAML response.
+                '';
+              };
+              email = mkOption {
+                type = types.str;
+                default = "";
+                description = ''
+                  Attribute map for `email'.
+                  Defaults to `NameID' of SAML response if
+                  <option>identifierFormat</option> has
+                  the default value.
+                '';
+              };
+            };
+          };
+        });
+        default = null;
+        description = "Configure the SAML integration.";
+      };
+    };
+
+    environmentFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = "/var/lib/hedgedoc/hedgedoc.env";
+      description = ''
+        Environment file as defined in <citerefentry>
+        <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+        </citerefentry>.
+
+        Secrets may be passed to the service without adding them to the world-readable
+        Nix store, by specifying placeholder variables as the option value in Nix and
+        setting these variables accordingly in the environment file.
+
+        <programlisting>
+          # snippet of HedgeDoc-related config
+          services.hedgedoc.configuration.dbURL = "postgres://hedgedoc:\''${DB_PASSWORD}@db-host:5432/hedgedocdb";
+          services.hedgedoc.configuration.minio.secretKey = "$MINIO_SECRET_KEY";
+        </programlisting>
+
+        <programlisting>
+          # content of the environment file
+          DB_PASSWORD=verysecretdbpassword
+          MINIO_SECRET_KEY=verysecretminiokey
+        </programlisting>
+
+        Note that this file needs to be available on the host on which
+        <literal>HedgeDoc</literal> is running.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.hedgedoc;
+      defaultText = literalExpression "pkgs.hedgedoc";
+      description = ''
+        Package that provides HedgeDoc.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.configuration.db == {} -> (
+          cfg.configuration.dbURL != "" && cfg.configuration.dbURL != null
+        );
+        message = "Database configuration for HedgeDoc missing."; }
+    ];
+    users.groups.${name} = {};
+    users.users.${name} = {
+      description = "HedgeDoc service user";
+      group = name;
+      extraGroups = cfg.groups;
+      home = cfg.workDir;
+      createHome = true;
+      isSystemUser = true;
+    };
+
+    systemd.services.hedgedoc = {
+      description = "HedgeDoc Service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+      preStart = ''
+        ${pkgs.envsubst}/bin/envsubst \
+          -o ${cfg.workDir}/config.json \
+          -i ${prettyJSON cfg.configuration}
+      '';
+      serviceConfig = {
+        WorkingDirectory = cfg.workDir;
+        ExecStart = "${cfg.package}/bin/hedgedoc";
+        EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
+        Environment = [
+          "CMD_CONFIG_FILE=${cfg.workDir}/config.json"
+          "NODE_ENV=production"
+        ];
+        Restart = "always";
+        User = name;
+        PrivateTmp = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/hledger-web.nix b/nixos/modules/services/web-apps/hledger-web.nix
new file mode 100644
index 00000000000..4f6a34e6d2f
--- /dev/null
+++ b/nixos/modules/services/web-apps/hledger-web.nix
@@ -0,0 +1,142 @@
+{ lib, pkgs, config, ... }:
+with lib;
+let
+  cfg = config.services.hledger-web;
+in {
+  options.services.hledger-web = {
+
+    enable = mkEnableOption "hledger-web service";
+
+    serveApi = mkEnableOption "Serve only the JSON web API, without the web UI.";
+
+    host = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = ''
+        Address to listen on.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5000;
+      example = 80;
+      description = ''
+        Port to listen on.
+      '';
+    };
+
+    capabilities = {
+      view = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable the view capability.
+        '';
+      };
+      add = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the add capability.
+        '';
+      };
+      manage = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the manage capability.
+        '';
+      };
+    };
+
+    stateDir = mkOption {
+      type = types.path;
+      default = "/var/lib/hledger-web";
+      description = ''
+        Path the service has access to. If left as the default value this
+        directory will automatically be created before the hledger-web server
+        starts, otherwise the sysadmin is responsible for ensuring the
+        directory exists with appropriate ownership and permissions.
+      '';
+    };
+
+    journalFiles = mkOption {
+      type = types.listOf types.str;
+      default = [ ".hledger.journal" ];
+      description = ''
+        Paths to journal files relative to <option>services.hledger-web.stateDir</option>.
+      '';
+    };
+
+    baseUrl = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "https://example.org";
+      description = ''
+        Base URL, when sharing over a network.
+      '';
+    };
+
+    extraOptions = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "--forecast" ];
+      description = ''
+        Extra command line arguments to pass to hledger-web.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users.hledger = {
+      name = "hledger";
+      group = "hledger";
+      isSystemUser = true;
+      home = cfg.stateDir;
+      useDefaultShell = true;
+    };
+
+    users.groups.hledger = {};
+
+    systemd.services.hledger-web = let
+      capabilityString = with cfg.capabilities; concatStringsSep "," (
+        (optional view "view")
+        ++ (optional add "add")
+        ++ (optional manage "manage")
+      );
+      serverArgs = with cfg; escapeShellArgs ([
+        "--serve"
+        "--host=${host}"
+        "--port=${toString port}"
+        "--capabilities=${capabilityString}"
+        (optionalString (cfg.baseUrl != null) "--base-url=${cfg.baseUrl}")
+        (optionalString (cfg.serveApi) "--serve-api")
+      ] ++ (map (f: "--file=${stateDir}/${f}") cfg.journalFiles)
+        ++ extraOptions);
+    in {
+      description = "hledger-web - web-app for the hledger accounting tool.";
+      documentation = [ "https://hledger.org/hledger-web.html" ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+      serviceConfig = mkMerge [
+        {
+          ExecStart = "${pkgs.hledger-web}/bin/hledger-web ${serverArgs}";
+          Restart = "always";
+          WorkingDirectory = cfg.stateDir;
+          User = "hledger";
+          Group = "hledger";
+          PrivateTmp = true;
+        }
+        (mkIf (cfg.stateDir == "/var/lib/hledger-web") {
+          StateDirectory = "hledger-web";
+        })
+      ];
+    };
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ marijanp erictapen ];
+}
diff --git a/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix b/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
new file mode 100644
index 00000000000..b9761061aaa
--- /dev/null
+++ b/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
@@ -0,0 +1,262 @@
+{ config, lib, pkgs, ... }: with lib; let
+  cfg = config.services.icingaweb2;
+  fpm = config.services.phpfpm.pools.${poolName};
+  poolName = "icingaweb2";
+
+  defaultConfig = {
+    global = {
+      module_path = "${pkgs.icingaweb2}/modules";
+    };
+  };
+in {
+  meta.maintainers = with maintainers; [ das_j ];
+
+  options.services.icingaweb2 = with types; {
+    enable = mkEnableOption "the icingaweb2 web interface";
+
+    pool = mkOption {
+      type = str;
+      default = poolName;
+      description = ''
+         Name of existing PHP-FPM pool that is used to run Icingaweb2.
+         If not specified, a pool will automatically created with default values.
+      '';
+    };
+
+    libraryPaths = mkOption {
+      type = attrsOf package;
+      default = { };
+      description = ''
+        Libraries to add to the Icingaweb2 library path.
+        The name of the attribute is the name of the library, the value
+        is the package to add.
+      '';
+    };
+
+    virtualHost = mkOption {
+      type = nullOr str;
+      default = "icingaweb2";
+      description = ''
+        Name of the nginx virtualhost to use and setup. If null, no virtualhost is set up.
+      '';
+    };
+
+    timezone = mkOption {
+      type = str;
+      default = "UTC";
+      example = "Europe/Berlin";
+      description = "PHP-compliant timezone specification";
+    };
+
+    modules = {
+      doc.enable = mkEnableOption "the icingaweb2 doc module";
+      migrate.enable = mkEnableOption "the icingaweb2 migrate module";
+      setup.enable = mkEnableOption "the icingaweb2 setup module";
+      test.enable = mkEnableOption "the icingaweb2 test module";
+      translation.enable = mkEnableOption "the icingaweb2 translation module";
+    };
+
+    modulePackages = mkOption {
+      type = attrsOf package;
+      default = {};
+      example = literalExpression ''
+        {
+          "snow" = icingaweb2Modules.theme-snow;
+        }
+      '';
+      description = ''
+        Name-package attrset of Icingaweb 2 modules packages to enable.
+
+        If you enable modules manually (e.g. via the web ui), they will not be touched.
+      '';
+    };
+
+    generalConfig = mkOption {
+      type = nullOr attrs;
+      default = null;
+      example = {
+        general = {
+          showStacktraces = 1;
+          config_resource = "icingaweb_db";
+        };
+        logging = {
+          log = "syslog";
+          level = "CRITICAL";
+        };
+      };
+      description = ''
+        config.ini contents.
+        Will automatically be converted to a .ini file.
+        If you don't set global.module_path, the module will take care of it.
+
+        If the value is null, no config.ini is created and you can
+        modify it manually (e.g. via the web interface).
+        Note that you need to update module_path manually.
+      '';
+    };
+
+    resources = mkOption {
+      type = nullOr attrs;
+      default = null;
+      example = {
+        icingaweb_db = {
+          type = "db";
+          db = "mysql";
+          host = "localhost";
+          username = "icingaweb2";
+          password = "icingaweb2";
+          dbname = "icingaweb2";
+        };
+      };
+      description = ''
+        resources.ini contents.
+        Will automatically be converted to a .ini file.
+
+        If the value is null, no resources.ini is created and you can
+        modify it manually (e.g. via the web interface).
+        Note that if you set passwords here, they will go into the nix store.
+      '';
+    };
+
+    authentications = mkOption {
+      type = nullOr attrs;
+      default = null;
+      example = {
+        icingaweb = {
+          backend = "db";
+          resource = "icingaweb_db";
+        };
+      };
+      description = ''
+        authentication.ini contents.
+        Will automatically be converted to a .ini file.
+
+        If the value is null, no authentication.ini is created and you can
+        modify it manually (e.g. via the web interface).
+      '';
+    };
+
+    groupBackends = mkOption {
+      type = nullOr attrs;
+      default = null;
+      example = {
+        icingaweb = {
+          backend = "db";
+          resource = "icingaweb_db";
+        };
+      };
+      description = ''
+        groups.ini contents.
+        Will automatically be converted to a .ini file.
+
+        If the value is null, no groups.ini is created and you can
+        modify it manually (e.g. via the web interface).
+      '';
+    };
+
+    roles = mkOption {
+      type = nullOr attrs;
+      default = null;
+      example = {
+        Administrators = {
+          users = "admin";
+          permissions = "*";
+        };
+      };
+      description = ''
+        roles.ini contents.
+        Will automatically be converted to a .ini file.
+
+        If the value is null, no roles.ini is created and you can
+        modify it manually (e.g. via the web interface).
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
+      ${poolName} = {
+        user = "icingaweb2";
+        phpEnv = {
+          ICINGAWEB_LIBDIR = toString (pkgs.linkFarm "icingaweb2-libdir" (mapAttrsToList (name: path: { inherit name path; }) cfg.libraryPaths));
+        };
+        phpPackage = pkgs.php.withExtensions ({ enabled, all }: [ all.imagick ] ++ enabled);
+        phpOptions = ''
+          date.timezone = "${cfg.timezone}"
+        '';
+        settings = mapAttrs (name: mkDefault) {
+          "listen.owner" = "nginx";
+          "listen.group" = "nginx";
+          "listen.mode" = "0600";
+          "pm" = "dynamic";
+          "pm.max_children" = 75;
+          "pm.start_servers" = 2;
+          "pm.min_spare_servers" = 2;
+          "pm.max_spare_servers" = 10;
+        };
+      };
+    };
+
+    services.icingaweb2.libraryPaths = {
+      ipl = pkgs.icingaweb2-ipl;
+      thirdparty = pkgs.icingaweb2-thirdparty;
+    };
+
+    systemd.services."phpfpm-${poolName}".serviceConfig.ReadWritePaths = [ "/etc/icingaweb2" ];
+
+    services.nginx = {
+      enable = true;
+      virtualHosts = mkIf (cfg.virtualHost != null) {
+        ${cfg.virtualHost} = {
+          root = "${pkgs.icingaweb2}/public";
+
+          extraConfig = ''
+            index index.php;
+            try_files $1 $uri $uri/ /index.php$is_args$args;
+          '';
+
+          locations."~ ..*/.*.php$".extraConfig = ''
+            return 403;
+          '';
+
+          locations."~ ^/index.php(.*)$".extraConfig = ''
+            fastcgi_intercept_errors on;
+            fastcgi_index index.php;
+            include ${config.services.nginx.package}/conf/fastcgi.conf;
+            try_files $uri =404;
+            fastcgi_split_path_info ^(.+\.php)(/.+)$;
+            fastcgi_pass unix:${fpm.socket};
+            fastcgi_param SCRIPT_FILENAME ${pkgs.icingaweb2}/public/index.php;
+          '';
+        };
+      };
+    };
+
+    # /etc/icingaweb2
+    environment.etc = let
+      doModule = name: optionalAttrs (cfg.modules.${name}.enable) { "icingaweb2/enabledModules/${name}".source = "${pkgs.icingaweb2}/modules/${name}"; };
+    in {}
+      # Module packages
+      // (mapAttrs' (k: v: nameValuePair "icingaweb2/enabledModules/${k}" { source = v; }) cfg.modulePackages)
+      # Built-in modules
+      // doModule "doc"
+      // doModule "migrate"
+      // doModule "setup"
+      // doModule "test"
+      // doModule "translation"
+      # Configs
+      // optionalAttrs (cfg.generalConfig != null) { "icingaweb2/config.ini".text = generators.toINI {} (defaultConfig // cfg.generalConfig); }
+      // optionalAttrs (cfg.resources != null) { "icingaweb2/resources.ini".text = generators.toINI {} cfg.resources; }
+      // optionalAttrs (cfg.authentications != null) { "icingaweb2/authentication.ini".text = generators.toINI {} cfg.authentications; }
+      // optionalAttrs (cfg.groupBackends != null) { "icingaweb2/groups.ini".text = generators.toINI {} cfg.groupBackends; }
+      // optionalAttrs (cfg.roles != null) { "icingaweb2/roles.ini".text = generators.toINI {} cfg.roles; };
+
+    # User and group
+    users.groups.icingaweb2 = {};
+    users.users.icingaweb2 = {
+      description = "Icingaweb2 service user";
+      group = "icingaweb2";
+      isSystemUser = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/icingaweb2/module-monitoring.nix b/nixos/modules/services/web-apps/icingaweb2/module-monitoring.nix
new file mode 100644
index 00000000000..e9c1d4ffe5e
--- /dev/null
+++ b/nixos/modules/services/web-apps/icingaweb2/module-monitoring.nix
@@ -0,0 +1,157 @@
+{ config, lib, pkgs, ... }: with lib; let
+  cfg = config.services.icingaweb2.modules.monitoring;
+
+  configIni = ''
+    [security]
+    protected_customvars = "${concatStringsSep "," cfg.generalConfig.protectedVars}"
+  '';
+
+  backendsIni = let
+    formatBool = b: if b then "1" else "0";
+  in concatStringsSep "\n" (mapAttrsToList (name: config: ''
+    [${name}]
+    type = "ido"
+    resource = "${config.resource}"
+    disabled = "${formatBool config.disabled}"
+  '') cfg.backends);
+
+  transportsIni = concatStringsSep "\n" (mapAttrsToList (name: config: ''
+    [${name}]
+    type = "${config.type}"
+    ${optionalString (config.instance != null) ''instance = "${config.instance}"''}
+    ${optionalString (config.type == "local" || config.type == "remote") ''path = "${config.path}"''}
+    ${optionalString (config.type != "local") ''
+      host = "${config.host}"
+      ${optionalString (config.port != null) ''port = "${toString config.port}"''}
+      user${optionalString (config.type == "api") "name"} = "${config.username}"
+    ''}
+    ${optionalString (config.type == "api") ''password = "${config.password}"''}
+    ${optionalString (config.type == "remote") ''resource = "${config.resource}"''}
+  '') cfg.transports);
+
+in {
+  options.services.icingaweb2.modules.monitoring = with types; {
+    enable = mkOption {
+      type = bool;
+      default = true;
+      description = "Whether to enable the icingaweb2 monitoring module.";
+    };
+
+    generalConfig = {
+      mutable = mkOption {
+        type = bool;
+        default = false;
+        description = "Make config.ini of the monitoring module mutable (e.g. via the web interface).";
+      };
+
+      protectedVars = mkOption {
+        type = listOf str;
+        default = [ "*pw*" "*pass*" "community" ];
+        description = "List of string patterns for custom variables which should be excluded from user’s view.";
+      };
+    };
+
+    mutableBackends = mkOption {
+      type = bool;
+      default = false;
+      description = "Make backends.ini of the monitoring module mutable (e.g. via the web interface).";
+    };
+
+    backends = mkOption {
+      default = { icinga = { resource = "icinga_ido"; }; };
+      description = "Monitoring backends to define";
+      type = attrsOf (submodule ({ name, ... }: {
+        options = {
+          name = mkOption {
+            visible = false;
+            default = name;
+            type = str;
+            description = "Name of this backend";
+          };
+
+          resource = mkOption {
+            type = str;
+            description = "Name of the IDO resource";
+          };
+
+          disabled = mkOption {
+            type = bool;
+            default = false;
+            description = "Disable this backend";
+          };
+        };
+      }));
+    };
+
+    mutableTransports = mkOption {
+      type = bool;
+      default = true;
+      description = "Make commandtransports.ini of the monitoring module mutable (e.g. via the web interface).";
+    };
+
+    transports = mkOption {
+      default = {};
+      description = "Command transports to define";
+      type = attrsOf (submodule ({ name, ... }: {
+        options = {
+          name = mkOption {
+            visible = false;
+            default = name;
+            type = str;
+            description = "Name of this transport";
+          };
+
+          type = mkOption {
+            type = enum [ "api" "local" "remote" ];
+            default = "api";
+            description = "Type of  this transport";
+          };
+
+          instance = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "Assign a icinga instance to this transport";
+          };
+
+          path = mkOption {
+            type = str;
+            description = "Path to the socket for local or remote transports";
+          };
+
+          host = mkOption {
+            type = str;
+            description = "Host for the api or remote transport";
+          };
+
+          port = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "Port to connect to for the api or remote transport";
+          };
+
+          username = mkOption {
+            type = str;
+            description = "Username for the api or remote transport";
+          };
+
+          password = mkOption {
+            type = str;
+            description = "Password for the api transport";
+          };
+
+          resource = mkOption {
+            type = str;
+            description = "SSH identity resource for the remote transport";
+          };
+        };
+      }));
+    };
+  };
+
+  config = mkIf (config.services.icingaweb2.enable && cfg.enable) {
+    environment.etc = { "icingaweb2/enabledModules/monitoring" = { source = "${pkgs.icingaweb2}/modules/monitoring"; }; }
+      // optionalAttrs (!cfg.generalConfig.mutable) { "icingaweb2/modules/monitoring/config.ini".text = configIni; }
+      // optionalAttrs (!cfg.mutableBackends) { "icingaweb2/modules/monitoring/backends.ini".text = backendsIni; }
+      // optionalAttrs (!cfg.mutableTransports) { "icingaweb2/modules/monitoring/commandtransports.ini".text = transportsIni; };
+  };
+}
diff --git a/nixos/modules/services/web-apps/ihatemoney/default.nix b/nixos/modules/services/web-apps/ihatemoney/default.nix
new file mode 100644
index 00000000000..ad314c885ba
--- /dev/null
+++ b/nixos/modules/services/web-apps/ihatemoney/default.nix
@@ -0,0 +1,153 @@
+{ 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 "ihatemoney webapp. Note that this will set uwsgi to emperor mode";
+      backend = mkOption {
+        type = types.enum [ "sqlite" "postgresql" ];
+        default = "sqlite";
+        description = ''
+          The database engine to use for ihatemoney.
+          If <literal>postgresql</literal> is selected, then a database called
+          <literal>${db}</literal> will be created. If you disable this option,
+          it will however not be removed.
+        '';
+      };
+      adminHashedPassword = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "The hashed password of the administrator. To obtain it, run <literal>ihatemoney generate_password_hash</literal>";
+      };
+      uwsgiConfig = mkOption {
+        type = types.attrs;
+        example = {
+          http = ":8000";
+        };
+        description = "Additionnal 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 = "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 = "The email of the sender of ihatemoney emails";
+        };
+      };
+      secureCookie = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Use secure cookies. Disable this when ihatemoney is served via http instead of https";
+      };
+      enableDemoProject = mkEnableOption "access to the demo project in ihatemoney";
+      enablePublicProjectCreation = mkEnableOption "permission to create projects in ihatemoney by anyone";
+      enableAdminDashboard = mkEnableOption "ihatemoney admin dashboard";
+      enableCaptcha = mkEnableOption "a simplistic captcha for some forms";
+      legalLink = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "The URL to a page explaining legal statements about your service, eg. GDPR-related information.";
+      };
+      extraConfig = mkOption {
+        type = types.str;
+        default = "";
+        description = "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/invidious.nix b/nixos/modules/services/web-apps/invidious.nix
new file mode 100644
index 00000000000..10b30bf1fd1
--- /dev/null
+++ b/nixos/modules/services/web-apps/invidious.nix
@@ -0,0 +1,264 @@
+{ lib, config, pkgs, options, ... }:
+let
+  cfg = config.services.invidious;
+  # To allow injecting secrets with jq, json (instead of yaml) is used
+  settingsFormat = pkgs.formats.json { };
+  inherit (lib) types;
+
+  settingsFile = settingsFormat.generate "invidious-settings" cfg.settings;
+
+  serviceConfig = {
+    systemd.services.invidious = {
+      description = "Invidious (An alternative YouTube front-end)";
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      script =
+        let
+          jqFilter = "."
+            + lib.optionalString (cfg.database.host != null) "[0].db.password = \"'\"'\"$(cat ${lib.escapeShellArg cfg.database.passwordFile})\"'\"'\""
+            + " | .[0]"
+            + lib.optionalString (cfg.extraSettingsFile != null) " * .[1]";
+          jqFiles = [ settingsFile ] ++ lib.optional (cfg.extraSettingsFile != null) cfg.extraSettingsFile;
+        in
+        ''
+          export INVIDIOUS_CONFIG="$(${pkgs.jq}/bin/jq -s "${jqFilter}" ${lib.escapeShellArgs jqFiles})"
+          exec ${cfg.package}/bin/invidious
+        '';
+
+      serviceConfig = {
+        RestartSec = "2s";
+        DynamicUser = true;
+
+        CapabilityBoundingSet = "";
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectHome = true;
+        ProtectKernelLogs = true;
+        ProtectProc = "invisible";
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+      };
+    };
+
+    services.invidious.settings = {
+      inherit (cfg) port;
+
+      # Automatically initialises and migrates the database if necessary
+      check_tables = true;
+
+      db = {
+        user = lib.mkDefault "kemal";
+        dbname = lib.mkDefault "invidious";
+        port = cfg.database.port;
+        # Blank for unix sockets, see
+        # https://github.com/will/crystal-pg/blob/1548bb255210/src/pq/conninfo.cr#L100-L108
+        host = if cfg.database.host == null then "" else cfg.database.host;
+        # Not needed because peer authentication is enabled
+        password = lib.mkIf (cfg.database.host == null) "";
+      };
+    } // (lib.optionalAttrs (cfg.domain != null) {
+      inherit (cfg) domain;
+    });
+
+    assertions = [{
+      assertion = cfg.database.host != null -> cfg.database.passwordFile != null;
+      message = "If database host isn't null, database password needs to be set";
+    }];
+  };
+
+  # Settings necessary for running with an automatically managed local database
+  localDatabaseConfig = lib.mkIf cfg.database.createLocally {
+    # Default to using the local database if we create it
+    services.invidious.database.host = lib.mkDefault null;
+
+    services.postgresql = {
+      enable = true;
+      ensureDatabases = lib.singleton cfg.settings.db.dbname;
+      ensureUsers = lib.singleton {
+        name = cfg.settings.db.user;
+        ensurePermissions = {
+          "DATABASE ${cfg.settings.db.dbname}" = "ALL PRIVILEGES";
+        };
+      };
+      # This is only needed because the unix user invidious isn't the same as
+      # the database user. This tells postgres to map one to the other.
+      identMap = ''
+        invidious invidious ${cfg.settings.db.user}
+      '';
+      # And this specifically enables peer authentication for only this
+      # database, which allows passwordless authentication over the postgres
+      # unix socket for the user map given above.
+      authentication = ''
+        local ${cfg.settings.db.dbname} ${cfg.settings.db.user} peer map=invidious
+      '';
+    };
+
+    systemd.services.invidious-db-clean = {
+      description = "Invidious database cleanup";
+      documentation = [ "https://docs.invidious.io/Database-Information-and-Maintenance.md" ];
+      startAt = lib.mkDefault "weekly";
+      path = [ config.services.postgresql.package ];
+      script = ''
+        psql ${cfg.settings.db.dbname} ${cfg.settings.db.user} -c "DELETE FROM nonces * WHERE expire < current_timestamp"
+        psql ${cfg.settings.db.dbname} ${cfg.settings.db.user} -c "TRUNCATE TABLE videos"
+      '';
+      serviceConfig = {
+        DynamicUser = true;
+        User = "invidious";
+      };
+    };
+
+    systemd.services.invidious = {
+      requires = [ "postgresql.service" ];
+      after = [ "postgresql.service" ];
+
+      serviceConfig = {
+        User = "invidious";
+      };
+    };
+  };
+
+  nginxConfig = lib.mkIf cfg.nginx.enable {
+    services.invidious.settings = {
+      https_only = config.services.nginx.virtualHosts.${cfg.domain}.forceSSL;
+      external_port = 80;
+    };
+
+    services.nginx = {
+      enable = true;
+      virtualHosts.${cfg.domain} = {
+        locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}";
+
+        enableACME = lib.mkDefault true;
+        forceSSL = lib.mkDefault true;
+      };
+    };
+
+    assertions = [{
+      assertion = cfg.domain != null;
+      message = "To use services.invidious.nginx, you need to set services.invidious.domain";
+    }];
+  };
+in
+{
+  options.services.invidious = {
+    enable = lib.mkEnableOption "Invidious";
+
+    package = lib.mkOption {
+      type = types.package;
+      default = pkgs.invidious;
+      defaultText = "pkgs.invidious";
+      description = "The Invidious package to use.";
+    };
+
+    settings = lib.mkOption {
+      type = settingsFormat.type;
+      default = { };
+      description = ''
+        The settings Invidious should use.
+
+        See <link xlink:href="https://github.com/iv-org/invidious/blob/master/config/config.example.yml">config.example.yml</link> for a list of all possible options.
+      '';
+    };
+
+    extraSettingsFile = lib.mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        A file including Invidious settings.
+
+        It gets merged with the setttings specified in <option>services.invidious.settings</option>
+        and can be used to store secrets like <literal>hmac_key</literal> outside of the nix store.
+      '';
+    };
+
+    # This needs to be outside of settings to avoid infinite recursion
+    # (determining if nginx should be enabled and therefore the settings
+    # modified).
+    domain = lib.mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The FQDN Invidious is reachable on.
+
+        This is used to configure nginx and for building absolute URLs.
+      '';
+    };
+
+    port = lib.mkOption {
+      type = types.port;
+      # Default from https://docs.invidious.io/Configuration.md
+      default = 3000;
+      description = ''
+        The port Invidious should listen on.
+
+        To allow access from outside,
+        you can use either <option>services.invidious.nginx</option>
+        or add <literal>config.services.invidious.port</literal> to <option>networking.firewall.allowedTCPPorts</option>.
+      '';
+    };
+
+    database = {
+      createLocally = lib.mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to create a local database with PostgreSQL.
+        '';
+      };
+
+      host = lib.mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The database host Invidious should use.
+
+          If <literal>null</literal>, the local unix socket is used. Otherwise
+          TCP is used.
+        '';
+      };
+
+      port = lib.mkOption {
+        type = types.port;
+        default = options.services.postgresql.port.default;
+        defaultText = lib.literalExpression "options.services.postgresql.port.default";
+        description = ''
+          The port of the database Invidious should use.
+
+          Defaults to the the default postgresql port.
+        '';
+      };
+
+      passwordFile = lib.mkOption {
+        type = types.nullOr types.str;
+        apply = lib.mapNullable toString;
+        default = null;
+        description = ''
+          Path to file containing the database password.
+        '';
+      };
+    };
+
+    nginx.enable = lib.mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to configure nginx as a reverse proxy for Invidious.
+
+        It serves it under the domain specified in <option>services.invidious.settings.domain</option> with enabled TLS and ACME.
+        Further configuration can be done through <option>services.nginx.virtualHosts.''${config.services.invidious.settings.domain}.*</option>,
+        which can also be used to disable AMCE and TLS.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable (lib.mkMerge [
+    serviceConfig
+    localDatabaseConfig
+    nginxConfig
+  ]);
+}
diff --git a/nixos/modules/services/web-apps/invoiceplane.nix b/nixos/modules/services/web-apps/invoiceplane.nix
new file mode 100644
index 00000000000..095eec36dec
--- /dev/null
+++ b/nixos/modules/services/web-apps/invoiceplane.nix
@@ -0,0 +1,305 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.invoiceplane;
+  eachSite = cfg.sites;
+  user = "invoiceplane";
+  webserver = config.services.${cfg.webserver};
+
+  invoiceplane-config = hostName: cfg: pkgs.writeText "ipconfig.php" ''
+    IP_URL=http://${hostName}
+    ENABLE_DEBUG=false
+    DISABLE_SETUP=false
+    REMOVE_INDEXPHP=false
+    DB_HOSTNAME=${cfg.database.host}
+    DB_USERNAME=${cfg.database.user}
+    # NOTE: file_get_contents adds newline at the end of returned string
+    DB_PASSWORD=${if cfg.database.passwordFile == null then "" else "trim(file_get_contents('${cfg.database.passwordFile}'), \"\\r\\n\")"}
+    DB_DATABASE=${cfg.database.name}
+    DB_PORT=${toString cfg.database.port}
+    SESS_EXPIRATION=864000
+    ENABLE_INVOICE_DELETION=false
+    DISABLE_READ_ONLY=false
+    ENCRYPTION_KEY=
+    ENCRYPTION_CIPHER=AES-256
+    SETUP_COMPLETED=false
+  '';
+
+  extraConfig = hostName: cfg: pkgs.writeText "extraConfig.php" ''
+    ${toString cfg.extraConfig}
+  '';
+
+  pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
+    pname = "invoiceplane-${hostName}";
+    version = src.version;
+    src = pkgs.invoiceplane;
+
+    patchPhase = ''
+      # Patch index.php file to load additional config file
+      substituteInPlace index.php \
+        --replace "require('vendor/autoload.php');" "require('vendor/autoload.php'); \$dotenv = new \Dotenv\Dotenv(__DIR__, 'extraConfig.php'); \$dotenv->load();";
+    '';
+
+    installPhase = ''
+      mkdir -p $out
+      cp -r * $out/
+
+      # symlink uploads and log directories
+      rm -r $out/uploads $out/application/logs $out/vendor/mpdf/mpdf/tmp
+      ln -sf ${cfg.stateDir}/uploads $out/
+      ln -sf ${cfg.stateDir}/logs $out/application/
+      ln -sf ${cfg.stateDir}/tmp $out/vendor/mpdf/mpdf/
+
+      # symlink the InvoicePlane config
+      ln -s ${cfg.stateDir}/ipconfig.php $out/ipconfig.php
+
+      # symlink the extraConfig file
+      ln -s ${extraConfig hostName cfg} $out/extraConfig.php
+
+      # symlink additional templates
+      ${concatMapStringsSep "\n" (template: "cp -r ${template}/. $out/application/views/invoice_templates/pdf/") cfg.invoiceTemplates}
+    '';
+  };
+
+  siteOpts = { lib, name, ... }:
+    {
+      options = {
+
+        enable = mkEnableOption "InvoicePlane web application";
+
+        stateDir = mkOption {
+          type = types.path;
+          default = "/var/lib/invoiceplane/${name}";
+          description = ''
+            This directory is used for uploads of attachements and cache.
+            The directory passed here is automatically created and permissions
+            adjusted as required.
+          '';
+        };
+
+        database = {
+          host = mkOption {
+            type = types.str;
+            default = "localhost";
+            description = "Database host address.";
+          };
+
+          port = mkOption {
+            type = types.port;
+            default = 3306;
+            description = "Database host port.";
+          };
+
+          name = mkOption {
+            type = types.str;
+            default = "invoiceplane";
+            description = "Database name.";
+          };
+
+          user = mkOption {
+            type = types.str;
+            default = "invoiceplane";
+            description = "Database user.";
+          };
+
+          passwordFile = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            example = "/run/keys/invoiceplane-dbpassword";
+            description = ''
+              A file containing the password corresponding to
+              <option>database.user</option>.
+            '';
+          };
+
+          createLocally = mkOption {
+            type = types.bool;
+            default = true;
+            description = "Create the database and database user locally.";
+          };
+        };
+
+        invoiceTemplates = mkOption {
+          type = types.listOf types.path;
+          default = [];
+          description = ''
+            List of path(s) to respective template(s) which are copied from the 'invoice_templates/pdf' directory.
+            <note><para>These templates need to be packaged before use, see example.</para></note>
+          '';
+          example = literalExpression ''
+            let
+              # Let's package an example template
+              template-vtdirektmarketing = pkgs.stdenv.mkDerivation {
+                name = "vtdirektmarketing";
+                # Download the template from a public repository
+                src = pkgs.fetchgit {
+                  url = "https://git.project-insanity.org/onny/invoiceplane-vtdirektmarketing.git";
+                  sha256 = "1hh0q7wzsh8v8x03i82p6qrgbxr4v5fb05xylyrpp975l8axyg2z";
+                };
+                sourceRoot = ".";
+                # Installing simply means copying template php file to the output directory
+                installPhase = ""
+                  mkdir -p $out
+                  cp invoiceplane-vtdirektmarketing/vtdirektmarketing.php $out/
+                "";
+              };
+            # And then pass this package to the template list like this:
+            in [ template-vtdirektmarketing ]
+          '';
+        };
+
+        poolConfig = mkOption {
+          type = with types; attrsOf (oneOf [ str int bool ]);
+          default = {
+            "pm" = "dynamic";
+            "pm.max_children" = 32;
+            "pm.start_servers" = 2;
+            "pm.min_spare_servers" = 2;
+            "pm.max_spare_servers" = 4;
+            "pm.max_requests" = 500;
+          };
+          description = ''
+            Options for the InvoicePlane PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+            for details on configuration directives.
+          '';
+        };
+
+        extraConfig = mkOption {
+          type = types.nullOr types.lines;
+          default = null;
+          example = ''
+            SETUP_COMPLETED=true
+            DISABLE_SETUP=true
+            IP_URL=https://invoice.example.com
+          '';
+          description = ''
+            InvoicePlane configuration. Refer to
+            <link xlink:href="https://github.com/InvoicePlane/InvoicePlane/blob/master/ipconfig.php.example"/>
+            for details on supported values.
+          '';
+        };
+
+      };
+
+    };
+in
+{
+  # interface
+  options = {
+    services.invoiceplane = mkOption {
+      type = types.submodule {
+
+        options.sites = mkOption {
+          type = types.attrsOf (types.submodule siteOpts);
+          default = {};
+          description = "Specification of one or more WordPress sites to serve";
+        };
+
+        options.webserver = mkOption {
+          type = types.enum [ "caddy" ];
+          default = "caddy";
+          description = ''
+            Which webserver to use for virtual host management. Currently only
+            caddy is supported.
+          '';
+        };
+      };
+      default = {};
+      description = "InvoicePlane configuration.";
+    };
+
+  };
+
+  # implementation
+  config = mkIf (eachSite != {}) (mkMerge [{
+
+    assertions = flatten (mapAttrsToList (hostName: cfg:
+      [{ assertion = cfg.database.createLocally -> cfg.database.user == user;
+        message = ''services.invoiceplane.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned'';
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
+        message = ''services.invoiceplane.sites."${hostName}".database.passwordFile cannot be specified if services.invoiceplane.sites."${hostName}".database.createLocally is set to true.'';
+      }]
+    ) eachSite);
+
+    services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = mapAttrsToList (hostName: cfg: cfg.database.name) eachSite;
+      ensureUsers = mapAttrsToList (hostName: cfg:
+        { name = cfg.database.user;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }
+      ) eachSite;
+    };
+
+    services.phpfpm = {
+      phpPackage = pkgs.php74;
+      pools = mapAttrs' (hostName: cfg: (
+        nameValuePair "invoiceplane-${hostName}" {
+          inherit user;
+          group = webserver.group;
+          settings = {
+            "listen.owner" = webserver.user;
+            "listen.group" = webserver.group;
+          } // cfg.poolConfig;
+        }
+      )) eachSite;
+    };
+
+  }
+
+  {
+    systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
+      "d ${cfg.stateDir} 0750 ${user} ${webserver.group} - -"
+      "f ${cfg.stateDir}/ipconfig.php 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/logs 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/uploads 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/uploads/archive 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/uploads/customer_files 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/uploads/temp 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/uploads/temp/mpdf 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/tmp 0750 ${user} ${webserver.group} - -"
+    ]) eachSite);
+
+    systemd.services.invoiceplane-config = {
+      serviceConfig.Type = "oneshot";
+      script = concatStrings (mapAttrsToList (hostName: cfg:
+        ''
+          mkdir -p ${cfg.stateDir}/logs \
+                   ${cfg.stateDir}/uploads
+          if ! grep -q IP_URL "${cfg.stateDir}/ipconfig.php"; then
+            cp "${invoiceplane-config hostName cfg}" "${cfg.stateDir}/ipconfig.php"
+          fi
+        '') eachSite);
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    users.users.${user} = {
+      group = webserver.group;
+      isSystemUser = true;
+    };
+  }
+
+  (mkIf (cfg.webserver == "caddy") {
+    services.caddy = {
+      enable = true;
+      virtualHosts = mapAttrs' (hostName: cfg: (
+        nameValuePair "http://${hostName}" {
+          extraConfig = ''
+            root    * ${pkg hostName cfg}
+            file_server
+
+            php_fastcgi unix/${config.services.phpfpm.pools."invoiceplane-${hostName}".socket}
+          '';
+        }
+      )) eachSite;
+    };
+  })
+
+
+  ]);
+}
+
diff --git a/nixos/modules/services/web-apps/isso.nix b/nixos/modules/services/web-apps/isso.nix
new file mode 100644
index 00000000000..4c01781a6a2
--- /dev/null
+++ b/nixos/modules/services/web-apps/isso.nix
@@ -0,0 +1,69 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkEnableOption mkIf mkOption types literalExpression;
+
+  cfg = config.services.isso;
+
+  settingsFormat = pkgs.formats.ini { };
+  configFile = settingsFormat.generate "isso.conf" cfg.settings;
+in {
+
+  options = {
+    services.isso = {
+      enable = mkEnableOption ''
+        A commenting server similar to Disqus.
+
+        Note: The application's author suppose to run isso behind a reverse proxy.
+        The embedded solution offered by NixOS is also only suitable for small installations
+        below 20 requests per second.
+      '';
+
+      settings = mkOption {
+        description = ''
+          Configuration for <package>isso</package>.
+
+          See <link xlink:href="https://posativ.org/isso/docs/configuration/server/">Isso Server Configuration</link>
+          for supported values.
+        '';
+
+        type = types.submodule {
+          freeformType = settingsFormat.type;
+        };
+
+        example = literalExpression ''
+          {
+            general = {
+              host = "http://localhost";
+            };
+          }
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.isso.settings.general.dbpath = lib.mkDefault "/var/lib/isso/comments.db";
+
+    systemd.services.isso = {
+      description = "isso, a commenting server similar to Disqus";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        User = "isso";
+        Group = "isso";
+
+        DynamicUser = true;
+
+        StateDirectory = "isso";
+
+        ExecStart = ''
+          ${pkgs.isso}/bin/isso -c ${configFile}
+        '';
+
+        Restart = "on-failure";
+        RestartSec = 1;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/jirafeau.nix b/nixos/modules/services/web-apps/jirafeau.nix
new file mode 100644
index 00000000000..328c61c8e64
--- /dev/null
+++ b/nixos/modules/services/web-apps/jirafeau.nix
@@ -0,0 +1,173 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.jirafeau;
+
+  group = config.services.nginx.group;
+  user = config.services.nginx.user;
+
+  withTrailingSlash = str: if hasSuffix "/" str then str else "${str}/";
+
+  localConfig = pkgs.writeText "config.local.php" ''
+    <?php
+      $cfg['admin_password'] = '${cfg.adminPasswordSha256}';
+      $cfg['web_root'] = 'http://${withTrailingSlash cfg.hostName}';
+      $cfg['var_root'] = '${withTrailingSlash cfg.dataDir}';
+      $cfg['maximal_upload_size'] = ${builtins.toString cfg.maxUploadSizeMegabytes};
+      $cfg['installation_done'] = true;
+
+      ${cfg.extraConfig}
+  '';
+in
+{
+  options.services.jirafeau = {
+    adminPasswordSha256 = mkOption {
+      type = types.str;
+      default = "";
+      description = ''
+        SHA-256 of the desired administration password. Leave blank/unset for no password.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/jirafeau/data/";
+      description = "Location of Jirafeau storage directory.";
+    };
+
+    enable = mkEnableOption "Jirafeau file upload application.";
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        $cfg['style'] = 'courgette';
+        $cfg['organisation'] = 'ACME';
+      '';
+      description = let
+        documentationLink =
+          "https://gitlab.com/mojo42/Jirafeau/-/blob/${cfg.package.version}/lib/config.original.php";
+      in
+        ''
+          Jirefeau configuration. Refer to <link xlink:href="${documentationLink}"/> for supported
+          values.
+        '';
+    };
+
+    hostName = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = "URL of instance. Must have trailing slash.";
+    };
+
+    maxUploadSizeMegabytes = mkOption {
+      type = types.int;
+      default = 0;
+      description = "Maximum upload size of accepted files.";
+    };
+
+    maxUploadTimeout = mkOption {
+      type = types.str;
+      default = "30m";
+      description = let
+        nginxCoreDocumentation = "http://nginx.org/en/docs/http/ngx_http_core_module.html";
+      in
+        ''
+          Timeout for reading client request bodies and headers. Refer to
+          <link xlink:href="${nginxCoreDocumentation}#client_body_timeout"/> and
+          <link xlink:href="${nginxCoreDocumentation}#client_header_timeout"/> for accepted values.
+        '';
+    };
+
+    nginxConfig = mkOption {
+      type = types.submodule
+        (import ../web-servers/nginx/vhost-options.nix { inherit config lib; });
+      default = {};
+      example = literalExpression ''
+        {
+          serverAliases = [ "wiki.''${config.networking.domain}" ];
+        }
+      '';
+      description = "Extra configuration for the nginx virtual host of Jirafeau.";
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.jirafeau;
+      defaultText = literalExpression "pkgs.jirafeau";
+      description = "Jirafeau package to use";
+    };
+
+    poolConfig = mkOption {
+      type = with types; attrsOf (oneOf [ str int bool ]);
+      default = {
+        "pm" = "dynamic";
+        "pm.max_children" = 32;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 2;
+        "pm.max_spare_servers" = 4;
+        "pm.max_requests" = 500;
+      };
+      description = ''
+        Options for Jirafeau PHP pool. See documentation on <literal>php-fpm.conf</literal> for
+        details on configuration directives.
+      '';
+    };
+  };
+
+
+  config = mkIf cfg.enable {
+    services = {
+      nginx = {
+        enable = true;
+        virtualHosts."${cfg.hostName}" = mkMerge [
+          cfg.nginxConfig
+          {
+            extraConfig = let
+              clientMaxBodySize =
+                if cfg.maxUploadSizeMegabytes == 0 then "0" else "${cfg.maxUploadSizeMegabytes}m";
+            in
+              ''
+                index index.php;
+                client_max_body_size ${clientMaxBodySize};
+                client_body_timeout ${cfg.maxUploadTimeout};
+                client_header_timeout ${cfg.maxUploadTimeout};
+              '';
+            locations = {
+              "~ \\.php$".extraConfig = ''
+                include ${config.services.nginx.package}/conf/fastcgi_params;
+                fastcgi_split_path_info ^(.+\.php)(/.+)$;
+                fastcgi_index index.php;
+                fastcgi_pass unix:${config.services.phpfpm.pools.jirafeau.socket};
+                fastcgi_param PATH_INFO $fastcgi_path_info;
+                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+              '';
+            };
+            root = mkForce "${cfg.package}";
+          }
+        ];
+      };
+
+      phpfpm.pools.jirafeau = {
+        inherit group user;
+        phpEnv."JIRAFEAU_CONFIG" = "${localConfig}";
+        settings = {
+          "listen.mode" = "0660";
+          "listen.owner" = user;
+          "listen.group" = group;
+        } // cfg.poolConfig;
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d ${cfg.dataDir} 0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/files/ 0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/links/ 0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/async/ 0750 ${user} ${group} - -"
+    ];
+  };
+
+  # uses attributes of the linked package
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/web-apps/jitsi-meet.nix b/nixos/modules/services/web-apps/jitsi-meet.nix
new file mode 100644
index 00000000000..2f1c4acec1e
--- /dev/null
+++ b/nixos/modules/services/web-apps/jitsi-meet.nix
@@ -0,0 +1,452 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.jitsi-meet;
+
+  # The configuration files are JS of format "var <<string>> = <<JSON>>;". In order to
+  # override only some settings, we need to extract the JSON, use jq to merge it with
+  # the config provided by user, and then reconstruct the file.
+  overrideJs =
+    source: varName: userCfg: appendExtra:
+    let
+      extractor = pkgs.writeText "extractor.js" ''
+        var fs = require("fs");
+        eval(fs.readFileSync(process.argv[2], 'utf8'));
+        process.stdout.write(JSON.stringify(eval(process.argv[3])));
+      '';
+      userJson = pkgs.writeText "user.json" (builtins.toJSON userCfg);
+    in (pkgs.runCommand "${varName}.js" { } ''
+      ${pkgs.nodejs}/bin/node ${extractor} ${source} ${varName} > default.json
+      (
+        echo "var ${varName} = "
+        ${pkgs.jq}/bin/jq -s '.[0] * .[1]' default.json ${userJson}
+        echo ";"
+        echo ${escapeShellArg appendExtra}
+      ) > $out
+    '');
+
+  # Essential config - it's probably not good to have these as option default because
+  # types.attrs doesn't do merging. Let's merge explicitly, can still be overriden if
+  # user desires.
+  defaultCfg = {
+    hosts = {
+      domain = cfg.hostName;
+      muc = "conference.${cfg.hostName}";
+      focus = "focus.${cfg.hostName}";
+    };
+    bosh = "//${cfg.hostName}/http-bind";
+    websocket = "wss://${cfg.hostName}/xmpp-websocket";
+
+    fileRecordingsEnabled = true;
+    liveStreamingEnabled = true;
+    hiddenDomain = "recorder.${cfg.hostName}";
+  };
+in
+{
+  options.services.jitsi-meet = with types; {
+    enable = mkEnableOption "Jitsi Meet - Secure, Simple and Scalable Video Conferences";
+
+    hostName = mkOption {
+      type = str;
+      example = "meet.example.org";
+      description = ''
+        FQDN of the Jitsi Meet instance.
+      '';
+    };
+
+    config = mkOption {
+      type = attrs;
+      default = { };
+      example = literalExpression ''
+        {
+          enableWelcomePage = false;
+          defaultLang = "fi";
+        }
+      '';
+      description = ''
+        Client-side web application settings that override the defaults in <filename>config.js</filename>.
+
+        See <link xlink:href="https://github.com/jitsi/jitsi-meet/blob/master/config.js" /> for default
+        configuration with comments.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = lines;
+      default = "";
+      description = ''
+        Text to append to <filename>config.js</filename> web application config file.
+
+        Can be used to insert JavaScript logic to determine user's region in cascading bridges setup.
+      '';
+    };
+
+    interfaceConfig = mkOption {
+      type = attrs;
+      default = { };
+      example = literalExpression ''
+        {
+          SHOW_JITSI_WATERMARK = false;
+          SHOW_WATERMARK_FOR_GUESTS = false;
+        }
+      '';
+      description = ''
+        Client-side web-app interface settings that override the defaults in <filename>interface_config.js</filename>.
+
+        See <link xlink:href="https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js" /> for
+        default configuration with comments.
+      '';
+    };
+
+    videobridge = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = ''
+          Whether to enable Jitsi Videobridge instance and configure it to connect to Prosody.
+
+          Additional configuration is possible with <option>services.jitsi-videobridge</option>.
+        '';
+      };
+
+      passwordFile = mkOption {
+        type = nullOr str;
+        default = null;
+        example = "/run/keys/videobridge";
+        description = ''
+          File containing password to the Prosody account for videobridge.
+
+          If <literal>null</literal>, a file with password will be generated automatically. Setting
+          this option is useful if you plan to connect additional videobridges to the XMPP server.
+        '';
+      };
+    };
+
+    jicofo.enable = mkOption {
+      type = bool;
+      default = true;
+      description = ''
+        Whether to enable JiCoFo instance and configure it to connect to Prosody.
+
+        Additional configuration is possible with <option>services.jicofo</option>.
+      '';
+    };
+
+    jibri.enable = mkOption {
+      type = bool;
+      default = false;
+      description = ''
+        Whether to enable a Jibri instance and configure it to connect to Prosody.
+
+        Additional configuration is possible with <option>services.jibri</option>, and
+        <option>services.jibri.finalizeScript</option> is especially useful.
+      '';
+    };
+
+    nginx.enable = mkOption {
+      type = bool;
+      default = true;
+      description = ''
+        Whether to enable nginx virtual host that will serve the javascript application and act as
+        a proxy for the XMPP server. Further nginx configuration can be done by adapting
+        <option>services.nginx.virtualHosts.&lt;hostName&gt;</option>.
+        When this is enabled, ACME will be used to retrieve a TLS certificate by default. To disable
+        this, set the <option>services.nginx.virtualHosts.&lt;hostName&gt;.enableACME</option> to
+        <literal>false</literal> and if appropriate do the same for
+        <option>services.nginx.virtualHosts.&lt;hostName&gt;.forceSSL</option>.
+      '';
+    };
+
+    caddy.enable = mkEnableOption "Whether to enablle caddy reverse proxy to expose jitsi-meet";
+
+    prosody.enable = mkOption {
+      type = bool;
+      default = true;
+      description = ''
+        Whether to configure Prosody to relay XMPP messages between Jitsi Meet components. Turn this
+        off if you want to configure it manually.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.prosody = mkIf cfg.prosody.enable {
+      enable = mkDefault true;
+      xmppComplianceSuite = mkDefault false;
+      modules = {
+        admin_adhoc = mkDefault false;
+        bosh = mkDefault true;
+        ping = mkDefault true;
+        roster = mkDefault true;
+        saslauth = mkDefault true;
+        smacks = mkDefault true;
+        tls = mkDefault true;
+        websocket = mkDefault true;
+      };
+      muc = [
+        {
+          domain = "conference.${cfg.hostName}";
+          name = "Jitsi Meet MUC";
+          roomLocking = false;
+          roomDefaultPublicJids = true;
+          extraConfig = ''
+            storage = "memory"
+          '';
+        }
+        {
+          domain = "internal.${cfg.hostName}";
+          name = "Jitsi Meet Videobridge MUC";
+          extraConfig = ''
+            storage = "memory"
+            admins = { "focus@auth.${cfg.hostName}", "jvb@auth.${cfg.hostName}" }
+          '';
+          #-- muc_room_cache_size = 1000
+        }
+      ];
+      extraModules = [ "pubsub" "smacks" ];
+      extraPluginPaths = [ "${pkgs.jitsi-meet-prosody}/share/prosody-plugins" ];
+      extraConfig = lib.mkMerge [ (mkAfter ''
+        Component "focus.${cfg.hostName}" "client_proxy"
+          target_address = "focus@auth.${cfg.hostName}"
+        '')
+        (mkBefore ''
+          cross_domain_websocket = true;
+          consider_websocket_secure = true;
+        '')
+      ];
+      virtualHosts.${cfg.hostName} = {
+        enabled = true;
+        domain = cfg.hostName;
+        extraConfig = ''
+          authentication = "anonymous"
+          c2s_require_encryption = false
+          admins = { "focus@auth.${cfg.hostName}" }
+          smacks_max_unacked_stanzas = 5
+          smacks_hibernation_time = 60
+          smacks_max_hibernated_sessions = 1
+          smacks_max_old_sessions = 1
+        '';
+        ssl = {
+          cert = "/var/lib/jitsi-meet/jitsi-meet.crt";
+          key = "/var/lib/jitsi-meet/jitsi-meet.key";
+        };
+      };
+      virtualHosts."auth.${cfg.hostName}" = {
+        enabled = true;
+        domain = "auth.${cfg.hostName}";
+        extraConfig = ''
+          authentication = "internal_plain"
+        '';
+        ssl = {
+          cert = "/var/lib/jitsi-meet/jitsi-meet.crt";
+          key = "/var/lib/jitsi-meet/jitsi-meet.key";
+        };
+      };
+      virtualHosts."recorder.${cfg.hostName}" = {
+        enabled = true;
+        domain = "recorder.${cfg.hostName}";
+        extraConfig = ''
+          authentication = "internal_plain"
+          c2s_require_encryption = false
+        '';
+      };
+    };
+    systemd.services.prosody.serviceConfig = mkIf cfg.prosody.enable {
+      EnvironmentFile = [ "/var/lib/jitsi-meet/secrets-env" ];
+      SupplementaryGroups = [ "jitsi-meet" ];
+    };
+
+    users.groups.jitsi-meet = {};
+    systemd.tmpfiles.rules = [
+      "d '/var/lib/jitsi-meet' 0750 root jitsi-meet - -"
+    ];
+
+    systemd.services.jitsi-meet-init-secrets = {
+      wantedBy = [ "multi-user.target" ];
+      before = [ "jicofo.service" "jitsi-videobridge2.service" ] ++ (optional cfg.prosody.enable "prosody.service");
+      path = [ config.services.prosody.package ];
+      serviceConfig = {
+        Type = "oneshot";
+      };
+
+      script = let
+        secrets = [ "jicofo-component-secret" "jicofo-user-secret" "jibri-auth-secret" "jibri-recorder-secret" ] ++ (optional (cfg.videobridge.passwordFile == null) "videobridge-secret");
+        videobridgeSecret = if cfg.videobridge.passwordFile != null then cfg.videobridge.passwordFile else "/var/lib/jitsi-meet/videobridge-secret";
+      in
+      ''
+        cd /var/lib/jitsi-meet
+        ${concatMapStringsSep "\n" (s: ''
+          if [ ! -f ${s} ]; then
+            tr -dc a-zA-Z0-9 </dev/urandom | head -c 64 > ${s}
+            chown root:jitsi-meet ${s}
+            chmod 640 ${s}
+          fi
+        '') secrets}
+
+        # for easy access in prosody
+        echo "JICOFO_COMPONENT_SECRET=$(cat jicofo-component-secret)" > secrets-env
+        chown root:jitsi-meet secrets-env
+        chmod 640 secrets-env
+      ''
+      + optionalString cfg.prosody.enable ''
+        prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)"
+        prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})"
+        prosodyctl mod_roster_command subscribe focus.${cfg.hostName} focus@auth.${cfg.hostName}
+        prosodyctl register jibri auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-auth-secret)"
+        prosodyctl register recorder recorder.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-recorder-secret)"
+
+        # generate self-signed certificates
+        if [ ! -f /var/lib/jitsi-meet.crt ]; then
+          ${getBin pkgs.openssl}/bin/openssl req \
+            -x509 \
+            -newkey rsa:4096 \
+            -keyout /var/lib/jitsi-meet/jitsi-meet.key \
+            -out /var/lib/jitsi-meet/jitsi-meet.crt \
+            -days 36500 \
+            -nodes \
+            -subj '/CN=${cfg.hostName}/CN=auth.${cfg.hostName}'
+          chmod 640 /var/lib/jitsi-meet/jitsi-meet.{crt,key}
+          chown root:jitsi-meet /var/lib/jitsi-meet/jitsi-meet.{crt,key}
+        fi
+      '';
+    };
+
+    services.nginx = mkIf cfg.nginx.enable {
+      enable = mkDefault true;
+      virtualHosts.${cfg.hostName} = {
+        enableACME = mkDefault true;
+        forceSSL = mkDefault true;
+        root = pkgs.jitsi-meet;
+        extraConfig = ''
+          ssi on;
+        '';
+        locations."@root_path".extraConfig = ''
+          rewrite ^/(.*)$ / break;
+        '';
+        locations."~ ^/([^/\\?&:'\"]+)$".tryFiles = "$uri @root_path";
+        locations."^~ /xmpp-websocket" = {
+          priority = 100;
+          proxyPass = "http://localhost:5280/xmpp-websocket";
+          proxyWebsockets = true;
+        };
+        locations."=/http-bind" = {
+          proxyPass = "http://localhost:5280/http-bind";
+          extraConfig = ''
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header Host $host;
+          '';
+        };
+        locations."=/external_api.js" = mkDefault {
+          alias = "${pkgs.jitsi-meet}/libs/external_api.min.js";
+        };
+        locations."=/config.js" = mkDefault {
+          alias = overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig;
+        };
+        locations."=/interface_config.js" = mkDefault {
+          alias = overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig "";
+        };
+      };
+    };
+
+    services.caddy = mkIf cfg.caddy.enable {
+      enable = mkDefault true;
+      virtualHosts.${cfg.hostName} = {
+        extraConfig =
+        let
+          templatedJitsiMeet = pkgs.runCommand "templated-jitsi-meet" {} ''
+            cp -R ${pkgs.jitsi-meet}/* .
+            for file in *.html **/*.html ; do
+              ${pkgs.sd}/bin/sd '<!--#include virtual="(.*)" -->' '{{ include "$1" }}' $file
+            done
+            rm config.js
+            rm interface_config.js
+            cp -R . $out
+            cp ${overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig} $out/config.js
+            cp ${overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig ""} $out/interface_config.js
+            cp ./libs/external_api.min.js $out/external_api.js
+          '';
+        in ''
+          handle /http-bind {
+            header Host ${cfg.hostName}
+            reverse_proxy 127.0.0.1:5280
+          }
+          handle /xmpp-websocket {
+            reverse_proxy 127.0.0.1:5280
+          }
+          handle {
+            templates
+            root * ${templatedJitsiMeet}
+            try_files {path} {path}
+            try_files {path} /index.html
+            file_server
+          }
+        '';
+      };
+    };
+
+    services.jitsi-videobridge = mkIf cfg.videobridge.enable {
+      enable = true;
+      xmppConfigs."localhost" = {
+        userName = "jvb";
+        domain = "auth.${cfg.hostName}";
+        passwordFile = "/var/lib/jitsi-meet/videobridge-secret";
+        mucJids = "jvbbrewery@internal.${cfg.hostName}";
+        disableCertificateVerification = true;
+      };
+    };
+
+    services.jicofo = mkIf cfg.jicofo.enable {
+      enable = true;
+      xmppHost = "localhost";
+      xmppDomain = cfg.hostName;
+      userDomain = "auth.${cfg.hostName}";
+      userName = "focus";
+      userPasswordFile = "/var/lib/jitsi-meet/jicofo-user-secret";
+      componentPasswordFile = "/var/lib/jitsi-meet/jicofo-component-secret";
+      bridgeMuc = "jvbbrewery@internal.${cfg.hostName}";
+      config = mkMerge [{
+        "org.jitsi.jicofo.ALWAYS_TRUST_MODE_ENABLED" = "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";
+      })];
+    };
+
+    services.jibri = mkIf cfg.jibri.enable {
+      enable = true;
+
+      xmppEnvironments."jitsi-meet" = {
+        xmppServerHosts = [ "localhost" ];
+        xmppDomain = cfg.hostName;
+
+        control.muc = {
+          domain = "internal.${cfg.hostName}";
+          roomName = "JibriBrewery";
+          nickname = "jibri";
+        };
+
+        control.login = {
+          domain = "auth.${cfg.hostName}";
+          username = "jibri";
+          passwordFile = "/var/lib/jitsi-meet/jibri-auth-secret";
+        };
+
+        call.login = {
+          domain = "recorder.${cfg.hostName}";
+          username = "recorder";
+          passwordFile = "/var/lib/jitsi-meet/jibri-recorder-secret";
+        };
+
+        usageTimeout = "0";
+        disableCertificateVerification = true;
+        stripFromRoomDomain = "conference.";
+      };
+    };
+  };
+
+  meta.doc = ./jitsi-meet.xml;
+  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
new file mode 100644
index 00000000000..ff44c724adf
--- /dev/null
+++ b/nixos/modules/services/web-apps/jitsi-meet.xml
@@ -0,0 +1,55 @@
+<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/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix
new file mode 100644
index 00000000000..22c16be7613
--- /dev/null
+++ b/nixos/modules/services/web-apps/keycloak.nix
@@ -0,0 +1,819 @@
+{ config, options, pkgs, lib, ... }:
+
+let
+  cfg = config.services.keycloak;
+  opt = options.services.keycloak;
+
+  inherit (lib) types mkOption concatStringsSep mapAttrsToList
+    escapeShellArg recursiveUpdate optionalAttrs boolToString mkOrder
+    sort filterAttrs concatMapStringsSep concatStrings mkIf
+    optionalString optionals mkDefault literalExpression hasSuffix
+    foldl' isAttrs filter attrNames elem literalDocBook
+    maintainers;
+
+  inherit (builtins) match typeOf;
+in
+{
+  options.services.keycloak =
+    let
+      inherit (types) bool str nullOr attrsOf path enum anything
+        package port;
+    in
+    {
+      enable = mkOption {
+        type = bool;
+        default = false;
+        example = true;
+        description = ''
+          Whether to enable the Keycloak identity and access management
+          server.
+        '';
+      };
+
+      bindAddress = mkOption {
+        type = str;
+        default = "\${jboss.bind.address:0.0.0.0}";
+        example = "127.0.0.1";
+        description = ''
+          On which address Keycloak should accept new connections.
+
+          A special syntax can be used to allow command line Java system
+          properties to override the value: ''${property.name:value}
+        '';
+      };
+
+      httpPort = mkOption {
+        type = str;
+        default = "\${jboss.http.port:80}";
+        example = "8080";
+        description = ''
+          On which port Keycloak should listen for new HTTP connections.
+
+          A special syntax can be used to allow command line Java system
+          properties to override the value: ''${property.name:value}
+        '';
+      };
+
+      httpsPort = mkOption {
+        type = str;
+        default = "\${jboss.https.port:443}";
+        example = "8443";
+        description = ''
+          On which port Keycloak should listen for new HTTPS connections.
+
+          A special syntax can be used to allow command line Java system
+          properties to override the value: ''${property.name:value}
+        '';
+      };
+
+      frontendUrl = mkOption {
+        type = str;
+        apply = x:
+          if x == "" || hasSuffix "/" x then
+            x
+          else
+            x + "/";
+        example = "keycloak.example.com/auth";
+        description = ''
+          The public URL used as base for all frontend requests. Should
+          normally include a trailing <literal>/auth</literal>.
+
+          See <link xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">the
+          Hostname section of the Keycloak server installation
+          manual</link> for more information.
+        '';
+      };
+
+      forceBackendUrlToFrontendUrl = mkOption {
+        type = bool;
+        default = false;
+        example = true;
+        description = ''
+          Whether Keycloak should force all requests to go through the
+          frontend URL configured in <xref
+          linkend="opt-services.keycloak.frontendUrl" />. By default,
+          Keycloak allows backend requests to instead use its local
+          hostname or IP address and may also advertise it to clients
+          through its OpenID Connect Discovery endpoint.
+
+          See <link
+          xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">the
+          Hostname section of the Keycloak server installation
+          manual</link> for more information.
+        '';
+      };
+
+      sslCertificate = mkOption {
+        type = nullOr path;
+        default = null;
+        example = "/run/keys/ssl_cert";
+        description = ''
+          The path to a PEM formatted certificate to use for TLS/SSL
+          connections.
+
+          This should be a string, not a Nix path, since Nix paths are
+          copied into the world-readable Nix store.
+        '';
+      };
+
+      sslCertificateKey = mkOption {
+        type = nullOr path;
+        default = null;
+        example = "/run/keys/ssl_key";
+        description = ''
+          The path to a PEM formatted private key to use for TLS/SSL
+          connections.
+
+          This should be a string, not a Nix path, since Nix paths are
+          copied into the world-readable Nix store.
+        '';
+      };
+
+      database = {
+        type = mkOption {
+          type = enum [ "mysql" "postgresql" ];
+          default = "postgresql";
+          example = "mysql";
+          description = ''
+            The type of database Keycloak should connect to.
+          '';
+        };
+
+        host = mkOption {
+          type = str;
+          default = "localhost";
+          description = ''
+            Hostname of the database to connect to.
+          '';
+        };
+
+        port =
+          let
+            dbPorts = {
+              postgresql = 5432;
+              mysql = 3306;
+            };
+          in
+          mkOption {
+            type = port;
+            default = dbPorts.${cfg.database.type};
+            defaultText = literalDocBook "default port of selected database";
+            description = ''
+              Port of the database to connect to.
+            '';
+          };
+
+        useSSL = mkOption {
+          type = bool;
+          default = cfg.database.host != "localhost";
+          defaultText = literalExpression ''config.${opt.database.host} != "localhost"'';
+          description = ''
+            Whether the database connection should be secured by SSL /
+            TLS.
+          '';
+        };
+
+        caCert = mkOption {
+          type = nullOr path;
+          default = null;
+          description = ''
+            The SSL / TLS CA certificate that verifies the identity of the
+            database server.
+
+            Required when PostgreSQL is used and SSL is turned on.
+
+            For MySQL, if left at <literal>null</literal>, the default
+            Java keystore is used, which should suffice if the server
+            certificate is issued by an official CA.
+          '';
+        };
+
+        createLocally = mkOption {
+          type = bool;
+          default = true;
+          description = ''
+            Whether a database should be automatically created on the
+            local host. Set this to false if you plan on provisioning a
+            local database yourself. This has no effect if
+            services.keycloak.database.host is customized.
+          '';
+        };
+
+        username = mkOption {
+          type = str;
+          default = "keycloak";
+          description = ''
+            Username to use when connecting to an external or manually
+            provisioned database; has no effect when a local database is
+            automatically provisioned.
+
+            To use this with a local database, set <xref
+            linkend="opt-services.keycloak.database.createLocally" /> to
+            <literal>false</literal> and create the database and user
+            manually. The database should be called
+            <literal>keycloak</literal>.
+          '';
+        };
+
+        passwordFile = mkOption {
+          type = path;
+          example = "/run/keys/db_password";
+          description = ''
+            File containing the database password.
+
+            This should be a string, not a Nix path, since Nix paths are
+            copied into the world-readable Nix store.
+          '';
+        };
+      };
+
+      package = mkOption {
+        type = package;
+        default = pkgs.keycloak;
+        defaultText = literalExpression "pkgs.keycloak";
+        description = ''
+          Keycloak package to use.
+        '';
+      };
+
+      initialAdminPassword = mkOption {
+        type = str;
+        default = "changeme";
+        description = ''
+          Initial password set for the <literal>admin</literal>
+          user. The password is not stored safely and should be changed
+          immediately in the admin panel.
+        '';
+      };
+
+      themes = mkOption {
+        type = attrsOf package;
+        default = { };
+        description = ''
+          Additional theme packages for Keycloak. Each theme is linked into
+          subdirectory with a corresponding attribute name.
+
+          Theme packages consist of several subdirectories which provide
+          different theme types: for example, <literal>account</literal>,
+          <literal>login</literal> etc. After adding a theme to this option you
+          can select it by its name in Keycloak administration console.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = attrsOf anything;
+        default = { };
+        example = literalExpression ''
+          {
+            "subsystem=keycloak-server" = {
+              "spi=hostname" = {
+                "provider=default" = null;
+                "provider=fixed" = {
+                  enabled = true;
+                  properties.hostname = "keycloak.example.com";
+                };
+                default-provider = "fixed";
+              };
+            };
+          }
+        '';
+        description = ''
+          Additional Keycloak configuration options to set in
+          <literal>standalone.xml</literal>.
+
+          Options are expressed as a Nix attribute set which matches the
+          structure of the jboss-cli configuration. The configuration is
+          effectively overlayed on top of the default configuration
+          shipped with Keycloak. To remove existing nodes and undefine
+          attributes from the default configuration, set them to
+          <literal>null</literal>.
+
+          The example configuration does the equivalent of the following
+          script, which removes the hostname provider
+          <literal>default</literal>, adds the deprecated hostname
+          provider <literal>fixed</literal> and defines it the default:
+
+          <programlisting>
+          /subsystem=keycloak-server/spi=hostname/provider=default:remove()
+          /subsystem=keycloak-server/spi=hostname/provider=fixed:add(enabled = true, properties = { hostname = "keycloak.example.com" })
+          /subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value="fixed")
+          </programlisting>
+
+          You can discover available options by using the <link
+          xlink:href="http://docs.wildfly.org/21/Admin_Guide.html#Command_Line_Interface">jboss-cli.sh</link>
+          program and by referring to the <link
+          xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html">Keycloak
+          Server Installation and Configuration Guide</link>.
+        '';
+      };
+
+    };
+
+  config =
+    let
+      # We only want to create a database if we're actually going to connect to it.
+      databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "localhost";
+      createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.database.type == "postgresql";
+      createLocalMySQL = databaseActuallyCreateLocally && cfg.database.type == "mysql";
+
+      mySqlCaKeystore = pkgs.runCommand "mysql-ca-keystore" { } ''
+        ${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.database.caCert} -keystore $out -storepass notsosecretpassword -noprompt
+      '';
+
+      # Both theme and theme type directories need to be actual directories in one hierarchy to pass Keycloak checks.
+      themesBundle = pkgs.runCommand "keycloak-themes" { } ''
+        linkTheme() {
+          theme="$1"
+          name="$2"
+
+          mkdir "$out/$name"
+          for typeDir in "$theme"/*; do
+            if [ -d "$typeDir" ]; then
+              type="$(basename "$typeDir")"
+              mkdir "$out/$name/$type"
+              for file in "$typeDir"/*; do
+                ln -sn "$file" "$out/$name/$type/$(basename "$file")"
+              done
+            fi
+          done
+        }
+
+        mkdir -p "$out"
+        for theme in ${cfg.package}/themes/*; do
+          if [ -d "$theme" ]; then
+            linkTheme "$theme" "$(basename "$theme")"
+          fi
+        done
+
+        ${concatStringsSep "\n" (mapAttrsToList (name: theme: "linkTheme ${theme} ${escapeShellArg name}") cfg.themes)}
+      '';
+
+      keycloakConfig' = foldl' recursiveUpdate
+        {
+          "interface=public".inet-address = cfg.bindAddress;
+          "socket-binding-group=standard-sockets"."socket-binding=http".port = cfg.httpPort;
+          "subsystem=keycloak-server" = {
+            "spi=hostname"."provider=default" = {
+              enabled = true;
+              properties = {
+                inherit (cfg) frontendUrl forceBackendUrlToFrontendUrl;
+              };
+            };
+            "theme=defaults".dir = toString themesBundle;
+          };
+          "subsystem=datasources"."data-source=KeycloakDS" = {
+            max-pool-size = "20";
+            user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username;
+            password = "@db-password@";
+          };
+        } [
+        (optionalAttrs (cfg.database.type == "postgresql") {
+          "subsystem=datasources" = {
+            "jdbc-driver=postgresql" = {
+              driver-module-name = "org.postgresql";
+              driver-name = "postgresql";
+              driver-xa-datasource-class-name = "org.postgresql.xa.PGXADataSource";
+            };
+            "data-source=KeycloakDS" = {
+              connection-url = "jdbc:postgresql://${cfg.database.host}:${toString cfg.database.port}/keycloak";
+              driver-name = "postgresql";
+              "connection-properties=ssl".value = boolToString cfg.database.useSSL;
+            } // (optionalAttrs (cfg.database.caCert != null) {
+              "connection-properties=sslrootcert".value = cfg.database.caCert;
+              "connection-properties=sslmode".value = "verify-ca";
+            });
+          };
+        })
+        (optionalAttrs (cfg.database.type == "mysql") {
+          "subsystem=datasources" = {
+            "jdbc-driver=mysql" = {
+              driver-module-name = "com.mysql";
+              driver-name = "mysql";
+              driver-class-name = "com.mysql.jdbc.Driver";
+            };
+            "data-source=KeycloakDS" = {
+              connection-url = "jdbc:mysql://${cfg.database.host}:${toString cfg.database.port}/keycloak";
+              driver-name = "mysql";
+              "connection-properties=useSSL".value = boolToString cfg.database.useSSL;
+              "connection-properties=requireSSL".value = boolToString cfg.database.useSSL;
+              "connection-properties=verifyServerCertificate".value = boolToString cfg.database.useSSL;
+              "connection-properties=characterEncoding".value = "UTF-8";
+              valid-connection-checker-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker";
+              validate-on-match = true;
+              exception-sorter-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLExceptionSorter";
+            } // (optionalAttrs (cfg.database.caCert != null) {
+              "connection-properties=trustCertificateKeyStoreUrl".value = "file:${mySqlCaKeystore}";
+              "connection-properties=trustCertificateKeyStorePassword".value = "notsosecretpassword";
+            });
+          };
+        })
+        (optionalAttrs (cfg.sslCertificate != null && cfg.sslCertificateKey != null) {
+          "socket-binding-group=standard-sockets"."socket-binding=https".port = cfg.httpsPort;
+          "subsystem=elytron" = mkOrder 900 {
+            "key-store=httpsKS" = mkOrder 900 {
+              path = "/run/keycloak/ssl/certificate_private_key_bundle.p12";
+              credential-reference.clear-text = "notsosecretpassword";
+              type = "JKS";
+            };
+            "key-manager=httpsKM" = mkOrder 901 {
+              key-store = "httpsKS";
+              credential-reference.clear-text = "notsosecretpassword";
+            };
+            "server-ssl-context=httpsSSC" = mkOrder 902 {
+              key-manager = "httpsKM";
+            };
+          };
+          "subsystem=undertow" = mkOrder 901 {
+            "server=default-server"."https-listener=https".ssl-context = "httpsSSC";
+          };
+        })
+        cfg.extraConfig
+      ];
+
+
+      /* Produces a JBoss CLI script that creates paths and sets
+         attributes matching those described by `attrs`. When the
+         script is run, the existing settings are effectively overlayed
+         by those from `attrs`. Existing attributes can be unset by
+         defining them `null`.
+
+         JBoss paths and attributes / maps are distinguished by their
+         name, where paths follow a `key=value` scheme.
+
+         Example:
+           mkJbossScript {
+             "subsystem=keycloak-server"."spi=hostname" = {
+               "provider=fixed" = null;
+               "provider=default" = {
+                 enabled = true;
+                 properties = {
+                   inherit frontendUrl;
+                   forceBackendUrlToFrontendUrl = false;
+                 };
+               };
+             };
+           }
+           => ''
+             if (outcome != success) of /:read-resource()
+                 /:add()
+             end-if
+             if (outcome != success) of /subsystem=keycloak-server:read-resource()
+                 /subsystem=keycloak-server:add()
+             end-if
+             if (outcome != success) of /subsystem=keycloak-server/spi=hostname:read-resource()
+                 /subsystem=keycloak-server/spi=hostname:add()
+             end-if
+             if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=default:read-resource()
+                 /subsystem=keycloak-server/spi=hostname/provider=default:add(enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" })
+             end-if
+             if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled")
+               /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true)
+             end-if
+             if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl")
+               /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false)
+             end-if
+             if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl")
+               /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth")
+             end-if
+             if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=fixed:read-resource()
+                 /subsystem=keycloak-server/spi=hostname/provider=fixed:remove()
+             end-if
+           ''
+      */
+      mkJbossScript = attrs:
+        let
+          /* From a JBoss path and an attrset, produces a JBoss CLI
+             snippet that writes the corresponding attributes starting
+             at `path`. Recurses down into subattrsets as necessary,
+             producing the variable name from its full path in the
+             attrset.
+
+             Example:
+               writeAttributes "/subsystem=keycloak-server/spi=hostname/provider=default" {
+                 enabled = true;
+                 properties = {
+                   forceBackendUrlToFrontendUrl = false;
+                   frontendUrl = "https://keycloak.example.com/auth";
+                 };
+               }
+               => ''
+                 if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled")
+                   /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true)
+                 end-if
+                 if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl")
+                   /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false)
+                 end-if
+                 if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl")
+                   /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth")
+                 end-if
+               ''
+          */
+          writeAttributes = path: set:
+            let
+              # JBoss expressions like `${var}` need to be prefixed
+              # with `expression` to evaluate.
+              prefixExpression = string:
+                let
+                  matchResult = match ''"\$\{.*}"'' string;
+                in
+                if matchResult != null then
+                  "expression " + string
+                else
+                  string;
+
+              writeAttribute = attribute: value:
+                let
+                  type = typeOf value;
+                in
+                if type == "set" then
+                  let
+                    names = attrNames value;
+                  in
+                  foldl' (text: name: text + (writeAttribute "${attribute}.${name}" value.${name})) "" names
+                else if value == null then ''
+                  if (outcome == success) of ${path}:read-attribute(name="${attribute}")
+                      ${path}:undefine-attribute(name="${attribute}")
+                  end-if
+                ''
+                else if elem type [ "string" "path" "bool" ] then
+                  let
+                    value' = if type == "bool" then boolToString value else ''"${value}"'';
+                  in
+                  ''
+                    if (result != ${prefixExpression value'}) of ${path}:read-attribute(name="${attribute}")
+                      ${path}:write-attribute(name=${attribute}, value=${value'})
+                    end-if
+                  ''
+                else throw "Unsupported type '${type}' for path '${path}'!";
+            in
+            concatStrings
+              (mapAttrsToList
+                (attribute: value: (writeAttribute attribute value))
+                set);
+
+
+          /* Produces an argument list for the JBoss `add()` function,
+             which adds a JBoss path and takes as its arguments the
+             required subpaths and attributes.
+
+             Example:
+               makeArgList {
+                 enabled = true;
+                 properties = {
+                   forceBackendUrlToFrontendUrl = false;
+                   frontendUrl = "https://keycloak.example.com/auth";
+                 };
+               }
+               => ''
+                 enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" }
+               ''
+          */
+          makeArgList = set:
+            let
+              makeArg = attribute: value:
+                let
+                  type = typeOf value;
+                in
+                if type == "set" then
+                  "${attribute} = { " + (makeArgList value) + " }"
+                else if elem type [ "string" "path" "bool" ] then
+                  "${attribute} = ${if type == "bool" then boolToString value else ''"${value}"''}"
+                else if value == null then
+                  ""
+                else
+                  throw "Unsupported type '${type}' for attribute '${attribute}'!";
+
+            in
+            concatStringsSep ", " (mapAttrsToList makeArg set);
+
+
+          /* Recurses into the `nodeValue` attrset. Only subattrsets that
+             are JBoss paths, i.e. follows the `key=value` format, are recursed
+             into - the rest are considered JBoss attributes / maps.
+          */
+          recurse = nodePath: nodeValue:
+            let
+              nodeContent =
+                if isAttrs nodeValue && nodeValue._type or "" == "order" then
+                  nodeValue.content
+                else
+                  nodeValue;
+              isPath = name:
+                let
+                  value = nodeContent.${name};
+                in
+                if (match ".*([=]).*" name) == [ "=" ] then
+                  if isAttrs value || value == null then
+                    true
+                  else
+                    throw "Parsing path '${concatStringsSep "." (nodePath ++ [ name ])}' failed: JBoss attributes cannot contain '='!"
+                else
+                  false;
+              jbossPath = "/" + concatStringsSep "/" nodePath;
+              children = if !isAttrs nodeContent then { } else nodeContent;
+              subPaths = filter isPath (attrNames children);
+              getPriority = name:
+                let
+                  value = children.${name};
+                in
+                if value._type or "" == "order" then value.priority else 1000;
+              orderedSubPaths = sort (a: b: getPriority a < getPriority b) subPaths;
+              jbossAttrs = filterAttrs (name: _: !(isPath name)) children;
+              text =
+                if nodeContent != null then
+                  ''
+                    if (outcome != success) of ${jbossPath}:read-resource()
+                        ${jbossPath}:add(${makeArgList jbossAttrs})
+                    end-if
+                  '' + writeAttributes jbossPath jbossAttrs
+                else
+                  ''
+                    if (outcome == success) of ${jbossPath}:read-resource()
+                        ${jbossPath}:remove()
+                    end-if
+                  '';
+            in
+            text + concatMapStringsSep "\n" (name: recurse (nodePath ++ [ name ]) children.${name}) orderedSubPaths;
+        in
+        recurse [ ] attrs;
+
+      jbossCliScript = pkgs.writeText "jboss-cli-script" (mkJbossScript keycloakConfig');
+
+      keycloakConfig = pkgs.runCommand "keycloak-config"
+        {
+          nativeBuildInputs = [ cfg.package ];
+        }
+        ''
+          export JBOSS_BASE_DIR="$(pwd -P)";
+          export JBOSS_MODULEPATH="${cfg.package}/modules";
+          export JBOSS_LOG_DIR="$JBOSS_BASE_DIR/log";
+
+          cp -r ${cfg.package}/standalone/configuration .
+          chmod -R u+rwX ./configuration
+
+          mkdir -p {deployments,ssl}
+
+          standalone.sh&
+
+          attempt=1
+          max_attempts=30
+          while ! jboss-cli.sh --connect ':read-attribute(name=server-state)'; do
+              if [[ "$attempt" == "$max_attempts" ]]; then
+                  echo "ERROR: Could not connect to Keycloak after $attempt attempts! Failing.." >&2
+                  exit 1
+              fi
+              echo "Keycloak not fully started yet, retrying.. ($attempt/$max_attempts)"
+              sleep 1
+              (( attempt++ ))
+          done
+
+          jboss-cli.sh --connect --file=${jbossCliScript} --echo-command
+
+          cp configuration/standalone.xml $out
+        '';
+    in
+    mkIf cfg.enable
+      {
+        assertions = [
+          {
+            assertion = (cfg.database.useSSL && cfg.database.type == "postgresql") -> (cfg.database.caCert != null);
+            message = "A CA certificate must be specified (in 'services.keycloak.database.caCert') when PostgreSQL is used with SSL";
+          }
+        ];
+
+        environment.systemPackages = [ cfg.package ];
+
+        systemd.services.keycloakPostgreSQLInit = mkIf createLocalPostgreSQL {
+          after = [ "postgresql.service" ];
+          before = [ "keycloak.service" ];
+          bindsTo = [ "postgresql.service" ];
+          path = [ config.services.postgresql.package ];
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+            User = "postgres";
+            Group = "postgres";
+            LoadCredential = [ "db_password:${cfg.database.passwordFile}" ];
+          };
+          script = ''
+            set -o errexit -o pipefail -o nounset -o errtrace
+            shopt -s inherit_errexit
+
+            create_role="$(mktemp)"
+            trap 'rm -f "$create_role"' ERR EXIT
+
+            db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")"
+            echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$db_password' CREATEDB" > "$create_role"
+            psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || psql -tA --file="$create_role"
+            psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || psql -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"'
+          '';
+        };
+
+        systemd.services.keycloakMySQLInit = mkIf createLocalMySQL {
+          after = [ "mysql.service" ];
+          before = [ "keycloak.service" ];
+          bindsTo = [ "mysql.service" ];
+          path = [ config.services.mysql.package ];
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+            User = config.services.mysql.user;
+            Group = config.services.mysql.group;
+            LoadCredential = [ "db_password:${cfg.database.passwordFile}" ];
+          };
+          script = ''
+            set -o errexit -o pipefail -o nounset -o errtrace
+            shopt -s inherit_errexit
+            db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")"
+            ( echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';"
+              echo "CREATE DATABASE IF NOT EXISTS keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;"
+              echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';"
+            ) | mysql -N
+          '';
+        };
+
+        systemd.services.keycloak =
+          let
+            databaseServices =
+              if createLocalPostgreSQL then [
+                "keycloakPostgreSQLInit.service"
+                "postgresql.service"
+              ]
+              else if createLocalMySQL then [
+                "keycloakMySQLInit.service"
+                "mysql.service"
+              ]
+              else [ ];
+          in
+          {
+            after = databaseServices;
+            bindsTo = databaseServices;
+            wantedBy = [ "multi-user.target" ];
+            path = with pkgs; [
+              cfg.package
+              openssl
+              replace-secret
+            ];
+            environment = {
+              JBOSS_LOG_DIR = "/var/log/keycloak";
+              JBOSS_BASE_DIR = "/run/keycloak";
+              JBOSS_MODULEPATH = "${cfg.package}/modules";
+            };
+            serviceConfig = {
+              LoadCredential = [
+                "db_password:${cfg.database.passwordFile}"
+              ] ++ optionals (cfg.sslCertificate != null && cfg.sslCertificateKey != null) [
+                "ssl_cert:${cfg.sslCertificate}"
+                "ssl_key:${cfg.sslCertificateKey}"
+              ];
+              User = "keycloak";
+              Group = "keycloak";
+              DynamicUser = true;
+              RuntimeDirectory = map (p: "keycloak/" + p) [
+                "configuration"
+                "deployments"
+                "data"
+                "ssl"
+                "log"
+                "tmp"
+              ];
+              RuntimeDirectoryMode = 0700;
+              LogsDirectory = "keycloak";
+              AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+            };
+            script = ''
+              set -o errexit -o pipefail -o nounset -o errtrace
+              shopt -s inherit_errexit
+
+              umask u=rwx,g=,o=
+
+              install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration
+              install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml
+
+              replace-secret '@db-password@' "$CREDENTIALS_DIRECTORY/db_password" /run/keycloak/configuration/standalone.xml
+
+              export JAVA_OPTS=-Djboss.server.config.user.dir=/run/keycloak/configuration
+              add-user-keycloak.sh -u admin -p '${cfg.initialAdminPassword}'
+            '' + optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) ''
+              pushd /run/keycloak/ssl/
+              cat "$CREDENTIALS_DIRECTORY/ssl_cert" <(echo) \
+                  "$CREDENTIALS_DIRECTORY/ssl_key" <(echo) \
+                  /etc/ssl/certs/ca-certificates.crt \
+                  > allcerts.pem
+              openssl pkcs12 -export -in "$CREDENTIALS_DIRECTORY/ssl_cert" -inkey "$CREDENTIALS_DIRECTORY/ssl_key" -chain \
+                             -name "${cfg.frontendUrl}" -out certificate_private_key_bundle.p12 \
+                             -CAfile allcerts.pem -passout pass:notsosecretpassword
+              popd
+            '' + ''
+              ${cfg.package}/bin/standalone.sh
+            '';
+          };
+
+        services.postgresql.enable = mkDefault createLocalPostgreSQL;
+        services.mysql.enable = mkDefault createLocalMySQL;
+        services.mysql.package = mkIf createLocalMySQL pkgs.mariadb;
+      };
+
+  meta.doc = ./keycloak.xml;
+  meta.maintainers = [ maintainers.talyz ];
+}
diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml
new file mode 100644
index 00000000000..cb706932f48
--- /dev/null
+++ b/nixos/modules/services/web-apps/keycloak.xml
@@ -0,0 +1,222 @@
+<chapter xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xmlns:xi="http://www.w3.org/2001/XInclude"
+         version="5.0"
+         xml:id="module-services-keycloak">
+ <title>Keycloak</title>
+ <para>
+   <link xlink:href="https://www.keycloak.org/">Keycloak</link> is an
+   open source identity and access management server with support for
+   <link xlink:href="https://openid.net/connect/">OpenID
+   Connect</link>, <link xlink:href="https://oauth.net/2/">OAUTH
+   2.0</link> and <link
+   xlink:href="https://en.wikipedia.org/wiki/SAML_2.0">SAML
+   2.0</link>.
+ </para>
+   <section xml:id="module-services-keycloak-admin">
+     <title>Administration</title>
+     <para>
+       An administrative user with the username
+       <literal>admin</literal> is automatically created in the
+       <literal>master</literal> realm. Its initial password can be
+       configured by setting <xref linkend="opt-services.keycloak.initialAdminPassword" />
+       and defaults to <literal>changeme</literal>. The password is
+       not stored safely and should be changed immediately in the
+       admin panel.
+     </para>
+
+     <para>
+       Refer to the <link
+       xlink:href="https://www.keycloak.org/docs/latest/server_admin/index.html#admin-console">Admin
+       Console section of the Keycloak Server Administration Guide</link> for
+       information on how to administer your
+       <productname>Keycloak</productname> instance.
+     </para>
+   </section>
+
+   <section xml:id="module-services-keycloak-database">
+     <title>Database access</title>
+     <para>
+       <productname>Keycloak</productname> can be used with either
+       <productname>PostgreSQL</productname> or
+       <productname>MySQL</productname>. Which one is used can be
+       configured in <xref
+       linkend="opt-services.keycloak.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.username" />, <xref
+       linkend="opt-services.keycloak.database.useSSL" /> and <xref
+       linkend="opt-services.keycloak.database.caCert" /> as
+       appropriate. Note that you need to manually create a database
+       called <literal>keycloak</literal> and allow the configured
+       database user full access to it.
+     </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-frontendurl">
+     <title>Frontend URL</title>
+     <para>
+       The frontend URL is used as base for all frontend requests and
+       must be configured through <xref linkend="opt-services.keycloak.frontendUrl" />.
+       It should normally include a trailing <literal>/auth</literal>
+       (the default web context). If you use a reverse proxy, you need
+       to set this option to <literal>""</literal>, so that frontend URL
+       is derived from HTTP headers. <literal>X-Forwarded-*</literal> headers
+       support also should be enabled, using <link
+       xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html#identifying-client-ip-addresses">
+       respective guidelines</link>.
+     </para>
+
+     <para>
+       <xref linkend="opt-services.keycloak.forceBackendUrlToFrontendUrl" />
+       determines whether Keycloak should force all requests to go
+       through the frontend URL. By default,
+       <productname>Keycloak</productname> allows backend requests to
+       instead use its local hostname or IP address and may also
+       advertise it to clients through its OpenID Connect Discovery
+       endpoint.
+     </para>
+
+     <para>
+       See the <link
+       xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">Hostname
+       section of the Keycloak Server Installation and Configuration
+       Guide</link> for more information.
+     </para>
+   </section>
+
+   <section xml:id="module-services-keycloak-tls">
+     <title>Setting up TLS/SSL</title>
+     <para>
+       By default, <productname>Keycloak</productname> won't accept
+       unsecured HTTP connections originating from outside its local
+       network.
+     </para>
+
+     <para>
+       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 via
+        <xref linkend="opt-services.keycloak.themes" />
+        option. See the <link xlink:href="https://www.keycloak.org/docs/latest/server_development/#_themes">
+        Themes section of the Keycloak Server Development Guide</link>
+        and respective NixOS option description for more information.
+     </para>
+   </section>
+
+   <section xml:id="module-services-keycloak-extra-config">
+     <title>Additional configuration</title>
+     <para>
+       Additional Keycloak configuration options, for which no
+       explicit <productname>NixOS</productname> options are provided,
+       can be set in <xref linkend="opt-services.keycloak.extraConfig" />.
+     </para>
+
+     <para>
+       Options are expressed as a Nix attribute set which matches the
+       structure of the jboss-cli configuration. The configuration is
+       effectively overlayed on top of the default configuration
+       shipped with Keycloak. To remove existing nodes and undefine
+       attributes from the default configuration, set them to
+       <literal>null</literal>.
+     </para>
+     <para>
+       For example, the following script, which removes the hostname
+       provider <literal>default</literal>, adds the deprecated
+       hostname provider <literal>fixed</literal> and defines it the
+       default:
+
+<programlisting>
+/subsystem=keycloak-server/spi=hostname/provider=default:remove()
+/subsystem=keycloak-server/spi=hostname/provider=fixed:add(enabled = true, properties = { hostname = "keycloak.example.com" })
+/subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value="fixed")
+</programlisting>
+
+       would be expressed as
+
+<programlisting>
+services.keycloak.extraConfig = {
+  "subsystem=keycloak-server" = {
+    "spi=hostname" = {
+      "provider=default" = null;
+      "provider=fixed" = {
+        enabled = true;
+        properties.hostname = "keycloak.example.com";
+      };
+      default-provider = "fixed";
+    };
+  };
+};
+</programlisting>
+     </para>
+     <para>
+       You can discover available options by using the <link
+       xlink:href="http://docs.wildfly.org/21/Admin_Guide.html#Command_Line_Interface">jboss-cli.sh</link>
+       program and by referring to the <link
+       xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html">Keycloak
+       Server Installation and Configuration Guide</link>.
+     </para>
+   </section>
+
+   <section xml:id="module-services-keycloak-example-config">
+     <title>Example configuration</title>
+     <para>
+       A basic configuration with some custom settings could look like this:
+<programlisting>
+services.keycloak = {
+  <link linkend="opt-services.keycloak.enable">enable</link> = true;
+  <link linkend="opt-services.keycloak.initialAdminPassword">initialAdminPassword</link> = "e6Wcm0RrtegMEHl";  # change on first login
+  <link linkend="opt-services.keycloak.frontendUrl">frontendUrl</link> = "https://keycloak.example.com/auth";
+  <link linkend="opt-services.keycloak.forceBackendUrlToFrontendUrl">forceBackendUrlToFrontendUrl</link> = true;
+  <link linkend="opt-services.keycloak.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.md b/nixos/modules/services/web-apps/lemmy.md
new file mode 100644
index 00000000000..e6599cd843e
--- /dev/null
+++ b/nixos/modules/services/web-apps/lemmy.md
@@ -0,0 +1,34 @@
+# Lemmy {#module-services-lemmy}
+
+Lemmy is a federated alternative to reddit in rust.
+
+## Quickstart {#module-services-lemmy-quickstart}
+
+the minimum to start lemmy is
+
+```nix
+services.lemmy = {
+  enable = true;
+  settings = {
+    hostname = "lemmy.union.rocks";
+    database.createLocally = true;
+  };
+  jwtSecretPath = "/run/secrets/lemmyJwt";
+  caddy.enable = true;
+}
+```
+
+(note that you can use something like agenix to get your secret jwt to the specified path)
+
+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.
+
+## Usage {#module-services-lemmy-usage}
+
+On first connection you will be asked to define an admin user.
+
+## Missing {#module-services-lemmy-missing}
+
+- Exposing with nginx is not implemented yet.
+- This has been tested using a local database with a unix socket connection. Using different database settings will likely require modifications
diff --git a/nixos/modules/services/web-apps/lemmy.nix b/nixos/modules/services/web-apps/lemmy.nix
new file mode 100644
index 00000000000..7cd2357c455
--- /dev/null
+++ b/nixos/modules/services/web-apps/lemmy.nix
@@ -0,0 +1,236 @@
+{ lib, pkgs, config, ... }:
+with lib;
+let
+  cfg = config.services.lemmy;
+  settingsFormat = pkgs.formats.json { };
+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;
+
+  options.services.lemmy = {
+
+    enable = mkEnableOption "lemmy a federated alternative to reddit in rust";
+
+    jwtSecretPath = mkOption {
+      type = types.path;
+      description = "Path to read the jwt secret from.";
+    };
+
+    ui = {
+      port = mkOption {
+        type = types.port;
+        default = 1234;
+        description = "Port where lemmy-ui should listen for incoming requests.";
+      };
+    };
+
+    caddy.enable = mkEnableOption "exposing lemmy with the caddy reverse proxy";
+
+    settings = mkOption {
+      default = { };
+      description = "Lemmy configuration";
+
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+
+        options.hostname = mkOption {
+          type = types.str;
+          default = null;
+          description = "The domain name of your instance (eg 'lemmy.ml').";
+        };
+
+        options.port = mkOption {
+          type = types.port;
+          default = 8536;
+          description = "Port where lemmy should listen for incoming requests.";
+        };
+
+        options.federation = {
+          enabled = mkEnableOption "activitypub federation";
+        };
+
+        options.captcha = {
+          enabled = mkOption {
+            type = types.bool;
+            default = true;
+            description = "Enable Captcha.";
+          };
+          difficulty = mkOption {
+            type = types.enum [ "easy" "medium" "hard" ];
+            default = "medium";
+            description = "The difficultly of the captcha to solve.";
+          };
+        };
+
+        options.database.createLocally = mkEnableOption "creation of database on the instance";
+
+      };
+    };
+
+  };
+
+  config =
+    let
+      localPostgres = (cfg.settings.database.host == "localhost" || cfg.settings.database.host == "/run/postgresql");
+    in
+    lib.mkIf cfg.enable {
+      services.lemmy.settings = (mapAttrs (name: mkDefault)
+        {
+          bind = "127.0.0.1";
+          tls_enabled = true;
+          pictrs_url = with config.services.pict-rs; "http://${address}:${toString port}";
+          actor_name_max_length = 20;
+
+          rate_limit.message = 180;
+          rate_limit.message_per_second = 60;
+          rate_limit.post = 6;
+          rate_limit.post_per_second = 600;
+          rate_limit.register = 3;
+          rate_limit.register_per_second = 3600;
+          rate_limit.image = 6;
+          rate_limit.image_per_second = 3600;
+        } // {
+        database = mapAttrs (name: mkDefault) {
+          user = "lemmy";
+          host = "/run/postgresql";
+          port = 5432;
+          database = "lemmy";
+          pool_size = 5;
+        };
+      });
+
+      services.postgresql = mkIf localPostgres {
+        enable = mkDefault true;
+      };
+
+      services.pict-rs.enable = true;
+
+      services.caddy = mkIf cfg.caddy.enable {
+        enable = mkDefault true;
+        virtualHosts."${cfg.settings.hostname}" = {
+          extraConfig = ''
+            handle_path /static/* {
+              root * ${pkgs.lemmy-ui}/dist
+              file_server
+            }
+            @for_backend {
+              path /api/* /pictrs/* feeds/* nodeinfo/*
+            }
+            handle @for_backend {
+              reverse_proxy 127.0.0.1:${toString cfg.settings.port}
+            }
+            @post {
+              method POST
+            }
+            handle @post {
+              reverse_proxy 127.0.0.1:${toString cfg.settings.port}
+            }
+            @jsonld {
+              header Accept "application/activity+json"
+              header Accept "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
+            }
+            handle @jsonld {
+              reverse_proxy 127.0.0.1:${toString cfg.settings.port}
+            }
+            handle {
+              reverse_proxy 127.0.0.1:${toString cfg.ui.port}
+            }
+          '';
+        };
+      };
+
+      assertions = [{
+        assertion = cfg.settings.database.createLocally -> localPostgres;
+        message = "if you want to create the database locally, you need to use a local database";
+      }];
+
+      systemd.services.lemmy = {
+        description = "Lemmy server";
+
+        environment = {
+          LEMMY_CONFIG_LOCATION = "/run/lemmy/config.hjson";
+
+          # Verify how this is used, and don't put the password in the nix store
+          LEMMY_DATABASE_URL = with cfg.settings.database;"postgres:///${database}?host=${host}";
+        };
+
+        documentation = [
+          "https://join-lemmy.org/docs/en/administration/from_scratch.html"
+          "https://join-lemmy.org/docs"
+        ];
+
+        wantedBy = [ "multi-user.target" ];
+
+        after = [ "pict-rs.service " ] ++ lib.optionals cfg.settings.database.createLocally [ "lemmy-postgresql.service" ];
+
+        requires = lib.optionals cfg.settings.database.createLocally [ "lemmy-postgresql.service" ];
+
+        # script is needed here since loadcredential is not accessible on ExecPreStart
+        script = ''
+          ${pkgs.coreutils}/bin/install -m 600 ${settingsFormat.generate "config.hjson" cfg.settings} /run/lemmy/config.hjson
+          jwtSecret="$(< $CREDENTIALS_DIRECTORY/jwt_secret )"
+          ${pkgs.jq}/bin/jq ".jwt_secret = \"$jwtSecret\"" /run/lemmy/config.hjson | ${pkgs.moreutils}/bin/sponge /run/lemmy/config.hjson
+          ${pkgs.lemmy-server}/bin/lemmy_server
+        '';
+
+        serviceConfig = {
+          DynamicUser = true;
+          RuntimeDirectory = "lemmy";
+          LoadCredential = "jwt_secret:${cfg.jwtSecretPath}";
+        };
+      };
+
+      systemd.services.lemmy-ui = {
+        description = "Lemmy ui";
+
+        environment = {
+          LEMMY_UI_HOST = "127.0.0.1:${toString cfg.ui.port}";
+          LEMMY_INTERNAL_HOST = "127.0.0.1:${toString cfg.settings.port}";
+          LEMMY_EXTERNAL_HOST = cfg.settings.hostname;
+          LEMMY_HTTPS = "false";
+        };
+
+        documentation = [
+          "https://join-lemmy.org/docs/en/administration/from_scratch.html"
+          "https://join-lemmy.org/docs"
+        ];
+
+        wantedBy = [ "multi-user.target" ];
+
+        after = [ "lemmy.service" ];
+
+        requires = [ "lemmy.service" ];
+
+        serviceConfig = {
+          DynamicUser = true;
+          WorkingDirectory = "${pkgs.lemmy-ui}";
+          ExecStart = "${pkgs.nodejs}/bin/node ${pkgs.lemmy-ui}/dist/js/server.js";
+        };
+      };
+
+      systemd.services.lemmy-postgresql = mkIf cfg.settings.database.createLocally {
+        description = "Lemmy postgresql db";
+        after = [ "postgresql.service" ];
+        partOf = [ "lemmy.service" ];
+        script = with cfg.settings.database; ''
+          PSQL() {
+            ${config.services.postgresql.package}/bin/psql --port=${toString cfg.settings.database.port} "$@"
+          }
+          # check if the database already exists
+          if ! PSQL -lqt | ${pkgs.coreutils}/bin/cut -d \| -f 1 | ${pkgs.gnugrep}/bin/grep -qw ${database} ; then
+            PSQL -tAc "CREATE ROLE ${user} WITH LOGIN;"
+            PSQL -tAc "CREATE DATABASE ${database} WITH OWNER ${user};"
+          fi
+        '';
+        serviceConfig = {
+          User = config.services.postgresql.superUser;
+          Type = "oneshot";
+          RemainAfterExit = true;
+        };
+      };
+    };
+
+}
diff --git a/nixos/modules/services/web-apps/lemmy.xml b/nixos/modules/services/web-apps/lemmy.xml
new file mode 100644
index 00000000000..0be9fb8aefa
--- /dev/null
+++ b/nixos/modules/services/web-apps/lemmy.xml
@@ -0,0 +1,56 @@
+<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="bash">
+services.lemmy = {
+  enable = true;
+  settings = {
+    hostname = &quot;lemmy.union.rocks&quot;;
+    database.createLocally = true;
+  };
+  jwtSecretPath = &quot;/run/secrets/lemmyJwt&quot;;
+  caddy.enable = true;
+}
+</programlisting>
+    <para>
+      (note that you can use something like agenix to get your secret
+      jwt to the specified path)
+    </para>
+    <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
new file mode 100644
index 00000000000..5ccd742a303
--- /dev/null
+++ b/nixos/modules/services/web-apps/limesurvey.nix
@@ -0,0 +1,280 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+  inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption;
+  inherit (lib) literalExpression mapAttrs optional optionalString types;
+
+  cfg = config.services.limesurvey;
+  fpm = config.services.phpfpm.pools.limesurvey;
+
+  user = "limesurvey";
+  group = config.services.httpd.group;
+  stateDir = "/var/lib/limesurvey";
+
+  pkg = pkgs.limesurvey;
+
+  configType = with types; oneOf [ (attrsOf configType) str int bool ] // {
+    description = "limesurvey config type (str, int, bool or attribute set thereof)";
+  };
+
+  limesurveyConfig = pkgs.writeText "config.php" ''
+    <?php
+      return json_decode('${builtins.toJSON cfg.config}', true);
+    ?>
+  '';
+
+  mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
+  pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
+
+in
+{
+  # interface
+
+  options.services.limesurvey = {
+    enable = mkEnableOption "Limesurvey web application.";
+
+    database = {
+      type = mkOption {
+        type = types.enum [ "mysql" "pgsql" "odbc" "mssql" ];
+        example = "pgsql";
+        default = "mysql";
+        description = "Database engine to use.";
+      };
+
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Database host address.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = if cfg.database.type == "pgsql" then 5442 else 3306;
+        defaultText = literalExpression "3306";
+        description = "Database host port.";
+      };
+
+      name = mkOption {
+        type = types.str;
+        default = "limesurvey";
+        description = "Database name.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "limesurvey";
+        description = "Database user.";
+      };
+
+      passwordFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/run/keys/limesurvey-dbpassword";
+        description = ''
+          A file containing the password corresponding to
+          <option>database.user</option>.
+        '';
+      };
+
+      socket = mkOption {
+        type = types.nullOr types.path;
+        default =
+          if mysqlLocal then "/run/mysqld/mysqld.sock"
+          else if pgsqlLocal then "/run/postgresql"
+          else null
+        ;
+        defaultText = literalExpression "/run/mysqld/mysqld.sock";
+        description = "Path to the unix socket file to use for authentication.";
+      };
+
+      createLocally = mkOption {
+        type = types.bool;
+        default = cfg.database.type == "mysql";
+        defaultText = literalExpression "true";
+        description = ''
+          Create the database and database user locally.
+          This currently only applies if database type "mysql" is selected.
+        '';
+      };
+    };
+
+    virtualHost = mkOption {
+      type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
+      example = literalExpression ''
+        {
+          hostName = "survey.example.org";
+          adminAddr = "webmaster@example.org";
+          forceSSL = true;
+          enableACME = true;
+        }
+      '';
+      description = ''
+        Apache configuration can be done by adapting <literal>services.httpd.virtualHosts.&lt;name&gt;</literal>.
+        See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+      '';
+    };
+
+    poolConfig = mkOption {
+      type = with types; attrsOf (oneOf [ str int bool ]);
+      default = {
+        "pm" = "dynamic";
+        "pm.max_children" = 32;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 2;
+        "pm.max_spare_servers" = 4;
+        "pm.max_requests" = 500;
+      };
+      description = ''
+        Options for the LimeSurvey PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+        for details on configuration directives.
+      '';
+    };
+
+    config = mkOption {
+      type = configType;
+      default = {};
+      description = ''
+        LimeSurvey configuration. Refer to
+        <link xlink:href="https://manual.limesurvey.org/Optional_settings"/>
+        for details on supported values.
+      '';
+    };
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = cfg.database.createLocally -> cfg.database.type == "mysql";
+        message = "services.limesurvey.createLocally is currently only supported for database type 'mysql'";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.user == user;
+        message = "services.limesurvey.database.user must be set to ${user} if services.limesurvey.database.createLocally is set true";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.socket != null;
+        message = "services.limesurvey.database.socket must be set if services.limesurvey.database.createLocally is set to true";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
+        message = "a password cannot be specified if services.limesurvey.database.createLocally is set to true";
+      }
+    ];
+
+    services.limesurvey.config = mapAttrs (name: mkDefault) {
+      runtimePath = "${stateDir}/tmp/runtime";
+      components = {
+        db = {
+          connectionString = "${cfg.database.type}:dbname=${cfg.database.name};host=${if pgsqlLocal then cfg.database.socket else cfg.database.host};port=${toString cfg.database.port}" +
+            optionalString mysqlLocal ";socket=${cfg.database.socket}";
+          username = cfg.database.user;
+          password = mkIf (cfg.database.passwordFile != null) "file_get_contents(\"${toString cfg.database.passwordFile}\");";
+          tablePrefix = "limesurvey_";
+        };
+        assetManager.basePath = "${stateDir}/tmp/assets";
+        urlManager = {
+          urlFormat = "path";
+          showScriptName = false;
+        };
+      };
+      config = {
+        tempdir = "${stateDir}/tmp";
+        uploaddir = "${stateDir}/upload";
+        force_ssl = mkIf (cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL) "on";
+        config.defaultlang = "en";
+      };
+    };
+
+    services.mysql = mkIf mysqlLocal {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = {
+            "${cfg.database.name}.*" = "SELECT, CREATE, INSERT, UPDATE, DELETE, ALTER, DROP, INDEX";
+          };
+        }
+      ];
+    };
+
+    services.phpfpm.pools.limesurvey = {
+      inherit user group;
+      phpEnv.LIMESURVEY_CONFIG = "${limesurveyConfig}";
+      settings = {
+        "listen.owner" = config.services.httpd.user;
+        "listen.group" = config.services.httpd.group;
+      } // cfg.poolConfig;
+    };
+
+    services.httpd = {
+      enable = true;
+      adminAddr = mkDefault cfg.virtualHost.adminAddr;
+      extraModules = [ "proxy_fcgi" ];
+      virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
+        documentRoot = mkForce "${pkg}/share/limesurvey";
+        extraConfig = ''
+          Alias "/tmp" "${stateDir}/tmp"
+          <Directory "${stateDir}">
+            AllowOverride all
+            Require all granted
+            Options -Indexes +FollowSymlinks
+          </Directory>
+
+          Alias "/upload" "${stateDir}/upload"
+          <Directory "${stateDir}/upload">
+            AllowOverride all
+            Require all granted
+            Options -Indexes
+          </Directory>
+
+          <Directory "${pkg}/share/limesurvey">
+            <FilesMatch "\.php$">
+              <If "-f %{REQUEST_FILENAME}">
+                SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
+              </If>
+            </FilesMatch>
+
+            AllowOverride all
+            Options -Indexes
+            DirectoryIndex index.php
+          </Directory>
+        '';
+      } ];
+    };
+
+    systemd.tmpfiles.rules = [
+      "d ${stateDir} 0750 ${user} ${group} - -"
+      "d ${stateDir}/tmp 0750 ${user} ${group} - -"
+      "d ${stateDir}/tmp/assets 0750 ${user} ${group} - -"
+      "d ${stateDir}/tmp/runtime 0750 ${user} ${group} - -"
+      "d ${stateDir}/tmp/upload 0750 ${user} ${group} - -"
+      "C ${stateDir}/upload 0750 ${user} ${group} - ${pkg}/share/limesurvey/upload"
+    ];
+
+    systemd.services.limesurvey-init = {
+      wantedBy = [ "multi-user.target" ];
+      before = [ "phpfpm-limesurvey.service" ];
+      after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
+      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
+      '';
+      serviceConfig = {
+        User = user;
+        Group = group;
+        Type = "oneshot";
+      };
+    };
+
+    systemd.services.httpd.after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
+
+    users.users.${user} = {
+      group = group;
+      isSystemUser = true;
+    };
+
+  };
+}
diff --git a/nixos/modules/services/web-apps/mastodon.nix b/nixos/modules/services/web-apps/mastodon.nix
new file mode 100644
index 00000000000..8208c85bfd7
--- /dev/null
+++ b/nixos/modules/services/web-apps/mastodon.nix
@@ -0,0 +1,636 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.mastodon;
+  # We only want to create a database if we're actually going to connect to it.
+  databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "/run/postgresql";
+
+  env = {
+    RAILS_ENV = "production";
+    NODE_ENV = "production";
+
+    # mastodon-web concurrency.
+    WEB_CONCURRENCY = toString cfg.webProcesses;
+    MAX_THREADS = toString cfg.webThreads;
+
+    # mastodon-streaming concurrency.
+    STREAMING_CLUSTER_NUM = toString cfg.streamingProcesses;
+
+    DB_USER = cfg.database.user;
+
+    REDIS_HOST = cfg.redis.host;
+    REDIS_PORT = toString(cfg.redis.port);
+    DB_HOST = cfg.database.host;
+    DB_PORT = toString(cfg.database.port);
+    DB_NAME = cfg.database.name;
+    LOCAL_DOMAIN = cfg.localDomain;
+    SMTP_SERVER = cfg.smtp.host;
+    SMTP_PORT = toString(cfg.smtp.port);
+    SMTP_FROM_ADDRESS = cfg.smtp.fromAddress;
+    PAPERCLIP_ROOT_PATH = "/var/lib/mastodon/public-system";
+    PAPERCLIP_ROOT_URL = "/system";
+    ES_ENABLED = if (cfg.elasticsearch.host != null) then "true" else "false";
+    ES_HOST = cfg.elasticsearch.host;
+    ES_PORT = toString(cfg.elasticsearch.port);
+
+    TRUSTED_PROXY_IP = cfg.trustedProxy;
+  }
+  // (if cfg.smtp.authenticate then { SMTP_LOGIN  = cfg.smtp.user; } else {})
+  // cfg.extraConfig;
+
+  systemCallsList = [ "@cpu-emulation" "@debug" "@keyring" "@ipc" "@mount" "@obsolete" "@privileged" "@setuid" ];
+
+  cfgService = {
+    # User and group
+    User = cfg.user;
+    Group = cfg.group;
+    # State directory and mode
+    StateDirectory = "mastodon";
+    StateDirectoryMode = "0750";
+    # Logs directory and mode
+    LogsDirectory = "mastodon";
+    LogsDirectoryMode = "0750";
+    # Proc filesystem
+    ProcSubset = "pid";
+    ProtectProc = "invisible";
+    # Access write directories
+    UMask = "0027";
+    # Capabilities
+    CapabilityBoundingSet = "";
+    # Security
+    NoNewPrivileges = true;
+    # Sandboxing
+    ProtectSystem = "strict";
+    ProtectHome = true;
+    PrivateTmp = true;
+    PrivateDevices = true;
+    PrivateUsers = true;
+    ProtectClock = true;
+    ProtectHostname = true;
+    ProtectKernelLogs = true;
+    ProtectKernelModules = true;
+    ProtectKernelTunables = true;
+    ProtectControlGroups = true;
+    RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
+    RestrictNamespaces = true;
+    LockPersonality = true;
+    MemoryDenyWriteExecute = false;
+    RestrictRealtime = true;
+    RestrictSUIDSGID = true;
+    RemoveIPC = true;
+    PrivateMounts = true;
+    # System Call Filtering
+    SystemCallArchitectures = "native";
+  };
+
+  envFile = pkgs.writeText "mastodon.env" (lib.concatMapStrings (s: s + "\n") (
+    (lib.concatLists (lib.mapAttrsToList (name: value:
+      if value != null then [
+        "${name}=\"${toString value}\""
+      ] else []
+    ) env))));
+
+  mastodonEnv = pkgs.writeShellScriptBin "mastodon-env" ''
+    set -a
+    export RAILS_ROOT="${cfg.package}"
+    source "${envFile}"
+    source /var/lib/mastodon/.secrets_env
+    eval -- "\$@"
+  '';
+
+in {
+
+  options = {
+    services.mastodon = {
+      enable = lib.mkEnableOption "Mastodon, a federated social network server";
+
+      configureNginx = lib.mkOption {
+        description = ''
+          Configure nginx as a reverse proxy for mastodon.
+          Note that this makes some assumptions on your setup, and sets settings that will
+          affect other virtualHosts running on your nginx instance, if any.
+          Alternatively you can configure a reverse-proxy of your choice to serve these paths:
+
+          <code>/ -> $(nix-instantiate --eval '&lt;nixpkgs&gt;' -A mastodon.outPath)/public</code>
+
+          <code>/ -> 127.0.0.1:{{ webPort }} </code>(If there was no file in the directory above.)
+
+          <code>/system/ -> /var/lib/mastodon/public-system/</code>
+
+          <code>/api/v1/streaming/ -> 127.0.0.1:{{ streamingPort }}</code>
+
+          Make sure that websockets are forwarded properly. You might want to set up caching
+          of some requests. Take a look at mastodon's provided nginx configuration at
+          <code>https://github.com/tootsuite/mastodon/blob/master/dist/nginx.conf</code>.
+        '';
+        type = lib.types.bool;
+        default = false;
+      };
+
+      user = lib.mkOption {
+        description = ''
+          User under which mastodon runs. If it is set to "mastodon",
+          that user will be created, otherwise it should be set to the
+          name of a user created elsewhere.  In both cases,
+          <package>mastodon</package> and a package containing only
+          the shell script <code>mastodon-env</code> will be added to
+          the user's package set. To run a command from
+          <package>mastodon</package> such as <code>tootctl</code>
+          with the environment configured by this module use
+          <code>mastodon-env</code>, as in:
+
+          <code>mastodon-env tootctl accounts create newuser --email newuser@example.com</code>
+        '';
+        type = lib.types.str;
+        default = "mastodon";
+      };
+
+      group = lib.mkOption {
+        description = ''
+          Group under which mastodon runs.
+        '';
+        type = lib.types.str;
+        default = "mastodon";
+      };
+
+      streamingPort = lib.mkOption {
+        description = "TCP port used by the mastodon-streaming service.";
+        type = lib.types.port;
+        default = 55000;
+      };
+      streamingProcesses = lib.mkOption {
+        description = ''
+          Processes used by the mastodon-streaming service.
+          Defaults to the number of CPU cores minus one.
+        '';
+        type = lib.types.nullOr lib.types.int;
+        default = null;
+      };
+
+      webPort = lib.mkOption {
+        description = "TCP port used by the mastodon-web service.";
+        type = lib.types.port;
+        default = 55001;
+      };
+      webProcesses = lib.mkOption {
+        description = "Processes used by the mastodon-web service.";
+        type = lib.types.int;
+        default = 2;
+      };
+      webThreads = lib.mkOption {
+        description = "Threads per process used by the mastodon-web service.";
+        type = lib.types.int;
+        default = 5;
+      };
+
+      sidekiqPort = lib.mkOption {
+        description = "TCP port used by the mastodon-sidekiq service.";
+        type = lib.types.port;
+        default = 55002;
+      };
+      sidekiqThreads = lib.mkOption {
+        description = "Worker threads used by the mastodon-sidekiq service.";
+        type = lib.types.int;
+        default = 25;
+      };
+
+      vapidPublicKeyFile = lib.mkOption {
+        description = ''
+          Path to file containing the public key used for Web Push
+          Voluntary Application Server Identification.  A new keypair can
+          be generated by running:
+
+          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake webpush:generate_keys</code>
+
+          If <option>mastodon.vapidPrivateKeyFile</option>does not
+          exist, it and this file will be created with a new keypair.
+        '';
+        default = "/var/lib/mastodon/secrets/vapid-public-key";
+        type = lib.types.str;
+      };
+
+      localDomain = lib.mkOption {
+        description = "The domain serving your Mastodon instance.";
+        example = "social.example.org";
+        type = lib.types.str;
+      };
+
+      secretKeyBaseFile = lib.mkOption {
+        description = ''
+          Path to file containing the secret key base.
+          A new secret key base can be generated by running:
+
+          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake secret</code>
+
+          If this file does not exist, it will be created with a new secret key base.
+        '';
+        default = "/var/lib/mastodon/secrets/secret-key-base";
+        type = lib.types.str;
+      };
+
+      otpSecretFile = lib.mkOption {
+        description = ''
+          Path to file containing the OTP secret.
+          A new OTP secret can be generated by running:
+
+          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake secret</code>
+
+          If this file does not exist, it will be created with a new OTP secret.
+        '';
+        default = "/var/lib/mastodon/secrets/otp-secret";
+        type = lib.types.str;
+      };
+
+      vapidPrivateKeyFile = lib.mkOption {
+        description = ''
+          Path to file containing the private key used for Web Push
+          Voluntary Application Server Identification.  A new keypair can
+          be generated by running:
+
+          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake webpush:generate_keys</code>
+
+          If this file does not exist, it will be created with a new
+          private key.
+        '';
+        default = "/var/lib/mastodon/secrets/vapid-private-key";
+        type = lib.types.str;
+      };
+
+      trustedProxy = lib.mkOption {
+        description = ''
+          You need to set it to the IP from which your reverse proxy sends requests to Mastodon's web process,
+          otherwise Mastodon will record the reverse proxy's own IP as the IP of all requests, which would be
+          bad because IP addresses are used for important rate limits and security functions.
+        '';
+        type = lib.types.str;
+        default = "127.0.0.1";
+      };
+
+      enableUnixSocket = lib.mkOption {
+        description = ''
+          Instead of binding to an IP address like 127.0.0.1, you may bind to a Unix socket. This variable
+          is process-specific, e.g. you need different values for every process, and it works for both web (Puma)
+          processes and streaming API (Node.js) processes.
+        '';
+        type = lib.types.bool;
+        default = true;
+      };
+
+      redis = {
+        createLocally = lib.mkOption {
+          description = "Configure local Redis server for Mastodon.";
+          type = lib.types.bool;
+          default = true;
+        };
+
+        host = lib.mkOption {
+          description = "Redis host.";
+          type = lib.types.str;
+          default = "127.0.0.1";
+        };
+
+        port = lib.mkOption {
+          description = "Redis port.";
+          type = lib.types.port;
+          default = 6379;
+        };
+      };
+
+      database = {
+        createLocally = lib.mkOption {
+          description = "Configure local PostgreSQL database server for Mastodon.";
+          type = lib.types.bool;
+          default = true;
+        };
+
+        host = lib.mkOption {
+          type = lib.types.str;
+          default = "/run/postgresql";
+          example = "192.168.23.42";
+          description = "Database host address or unix socket.";
+        };
+
+        port = lib.mkOption {
+          type = lib.types.int;
+          default = 5432;
+          description = "Database host port.";
+        };
+
+        name = lib.mkOption {
+          type = lib.types.str;
+          default = "mastodon";
+          description = "Database name.";
+        };
+
+        user = lib.mkOption {
+          type = lib.types.str;
+          default = "mastodon";
+          description = "Database user.";
+        };
+
+        passwordFile = lib.mkOption {
+          type = lib.types.nullOr lib.types.path;
+          default = "/var/lib/mastodon/secrets/db-password";
+          example = "/run/keys/mastodon-db-password";
+          description = ''
+            A file containing the password corresponding to
+            <option>database.user</option>.
+          '';
+        };
+      };
+
+      smtp = {
+        createLocally = lib.mkOption {
+          description = "Configure local Postfix SMTP server for Mastodon.";
+          type = lib.types.bool;
+          default = true;
+        };
+
+        authenticate = lib.mkOption {
+          description = "Authenticate with the SMTP server using username and password.";
+          type = lib.types.bool;
+          default = false;
+        };
+
+        host = lib.mkOption {
+          description = "SMTP host used when sending emails to users.";
+          type = lib.types.str;
+          default = "127.0.0.1";
+        };
+
+        port = lib.mkOption {
+          description = "SMTP port used when sending emails to users.";
+          type = lib.types.port;
+          default = 25;
+        };
+
+        fromAddress = lib.mkOption {
+          description = ''"From" address used when sending Emails to users.'';
+          type = lib.types.str;
+        };
+
+        user = lib.mkOption {
+          description = "SMTP login name.";
+          type = lib.types.str;
+        };
+
+        passwordFile = lib.mkOption {
+          description = ''
+            Path to file containing the SMTP password.
+          '';
+          default = "/var/lib/mastodon/secrets/smtp-password";
+          example = "/run/keys/mastodon-smtp-password";
+          type = lib.types.str;
+        };
+      };
+
+      elasticsearch = {
+        host = lib.mkOption {
+          description = ''
+            Elasticsearch host.
+            If it is not null, Elasticsearch full text search will be enabled.
+          '';
+          type = lib.types.nullOr lib.types.str;
+          default = null;
+        };
+
+        port = lib.mkOption {
+          description = "Elasticsearch port.";
+          type = lib.types.port;
+          default = 9200;
+        };
+      };
+
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.mastodon;
+        defaultText = lib.literalExpression "pkgs.mastodon";
+        description = "Mastodon package to use.";
+      };
+
+      extraConfig = lib.mkOption {
+        type = lib.types.attrs;
+        default = {};
+        description = ''
+          Extra environment variables to pass to all mastodon services.
+        '';
+      };
+
+      automaticMigrations = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = ''
+          Do automatic database migrations.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.database.user);
+        message = ''For local automatic database provisioning (services.mastodon.database.createLocally == true) with peer authentication (services.mastodon.database.host == "/run/postgresql") to work services.mastodon.user and services.mastodon.database.user must be identical.'';
+      }
+    ];
+
+    systemd.services.mastodon-init-dirs = {
+      script = ''
+        umask 077
+
+        if ! test -f ${cfg.secretKeyBaseFile}; then
+          mkdir -p $(dirname ${cfg.secretKeyBaseFile})
+          bin/rake secret > ${cfg.secretKeyBaseFile}
+        fi
+        if ! test -f ${cfg.otpSecretFile}; then
+          mkdir -p $(dirname ${cfg.otpSecretFile})
+          bin/rake secret > ${cfg.otpSecretFile}
+        fi
+        if ! test -f ${cfg.vapidPrivateKeyFile}; then
+          mkdir -p $(dirname ${cfg.vapidPrivateKeyFile}) $(dirname ${cfg.vapidPublicKeyFile})
+          keypair=$(bin/rake webpush:generate_keys)
+          echo $keypair | grep --only-matching "Private -> [^ ]\+" | sed 's/^Private -> //' > ${cfg.vapidPrivateKeyFile}
+          echo $keypair | grep --only-matching "Public -> [^ ]\+" | sed 's/^Public -> //' > ${cfg.vapidPublicKeyFile}
+        fi
+
+        cat > /var/lib/mastodon/.secrets_env <<EOF
+        SECRET_KEY_BASE="$(cat ${cfg.secretKeyBaseFile})"
+        OTP_SECRET="$(cat ${cfg.otpSecretFile})"
+        VAPID_PRIVATE_KEY="$(cat ${cfg.vapidPrivateKeyFile})"
+        VAPID_PUBLIC_KEY="$(cat ${cfg.vapidPublicKeyFile})"
+        DB_PASS="$(cat ${cfg.database.passwordFile})"
+      '' + (if cfg.smtp.authenticate then ''
+        SMTP_PASSWORD="$(cat ${cfg.smtp.passwordFile})"
+      '' else "") + ''
+        EOF
+      '';
+      environment = env;
+      serviceConfig = {
+        Type = "oneshot";
+        WorkingDirectory = cfg.package;
+        # System Call Filtering
+        SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
+      } // cfgService;
+
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    systemd.services.mastodon-init-db = lib.mkIf cfg.automaticMigrations {
+      script = ''
+        if [ `psql ${cfg.database.name} -c \
+                "select count(*) from pg_class c \
+                join pg_namespace s on s.oid = c.relnamespace \
+                where s.nspname not in ('pg_catalog', 'pg_toast', 'information_schema') \
+                and s.nspname not like 'pg_temp%';" | sed -n 3p` -eq 0 ]; then
+          SAFETY_ASSURED=1 rails db:schema:load
+          rails db:seed
+        else
+          rails db:migrate
+        fi
+      '';
+      path = [ cfg.package pkgs.postgresql ];
+      environment = env;
+      serviceConfig = {
+        Type = "oneshot";
+        EnvironmentFile = "/var/lib/mastodon/.secrets_env";
+        WorkingDirectory = cfg.package;
+        # System Call Filtering
+        SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
+      } // cfgService;
+      after = [ "mastodon-init-dirs.service" "network.target" ] ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else []);
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    systemd.services.mastodon-streaming = {
+      after = [ "network.target" ]
+        ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else [])
+        ++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]);
+      description = "Mastodon streaming";
+      wantedBy = [ "multi-user.target" ];
+      environment = env // (if cfg.enableUnixSocket
+        then { SOCKET = "/run/mastodon-streaming/streaming.socket"; }
+        else { PORT = toString(cfg.streamingPort); }
+      );
+      serviceConfig = {
+        ExecStart = "${cfg.package}/run-streaming.sh";
+        Restart = "always";
+        RestartSec = 20;
+        EnvironmentFile = "/var/lib/mastodon/.secrets_env";
+        WorkingDirectory = cfg.package;
+        # Runtime directory and mode
+        RuntimeDirectory = "mastodon-streaming";
+        RuntimeDirectoryMode = "0750";
+        # System Call Filtering
+        SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@memlock" "@resources" ])) "pipe" "pipe2" ];
+      } // cfgService;
+    };
+
+    systemd.services.mastodon-web = {
+      after = [ "network.target" ]
+        ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else [])
+        ++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]);
+      description = "Mastodon web";
+      wantedBy = [ "multi-user.target" ];
+      environment = env // (if cfg.enableUnixSocket
+        then { SOCKET = "/run/mastodon-web/web.socket"; }
+        else { PORT = toString(cfg.webPort); }
+      );
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/puma -C config/puma.rb";
+        Restart = "always";
+        RestartSec = 20;
+        EnvironmentFile = "/var/lib/mastodon/.secrets_env";
+        WorkingDirectory = cfg.package;
+        # Runtime directory and mode
+        RuntimeDirectory = "mastodon-web";
+        RuntimeDirectoryMode = "0750";
+        # System Call Filtering
+        SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "@chown" "pipe" "pipe2" ];
+      } // cfgService;
+      path = with pkgs; [ file imagemagick ffmpeg ];
+    };
+
+    systemd.services.mastodon-sidekiq = {
+      after = [ "network.target" ]
+        ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else [])
+        ++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]);
+      description = "Mastodon sidekiq";
+      wantedBy = [ "multi-user.target" ];
+      environment = env // {
+        PORT = toString(cfg.sidekiqPort);
+        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 ];
+    };
+
+    services.nginx = lib.mkIf cfg.configureNginx {
+      enable = true;
+      recommendedProxySettings = true; # required for redirections to work
+      virtualHosts."${cfg.localDomain}" = {
+        root = "${cfg.package}/public/";
+        forceSSL = true; # mastodon only supports https
+        enableACME = true;
+
+        locations."/system/".alias = "/var/lib/mastodon/public-system/";
+
+        locations."/" = {
+          tryFiles = "$uri @proxy";
+        };
+
+        locations."@proxy" = {
+          proxyPass = (if cfg.enableUnixSocket then "http://unix:/run/mastodon-web/web.socket" else "http://127.0.0.1:${toString(cfg.webPort)}");
+          proxyWebsockets = true;
+        };
+
+        locations."/api/v1/streaming/" = {
+          proxyPass = (if cfg.enableUnixSocket then "http://unix:/run/mastodon-streaming/streaming.socket" else "http://127.0.0.1:${toString(cfg.streamingPort)}/");
+          proxyWebsockets = true;
+        };
+      };
+    };
+
+    services.postfix = lib.mkIf (cfg.smtp.createLocally && cfg.smtp.host == "127.0.0.1") {
+      enable = true;
+      hostname = lib.mkDefault "${cfg.localDomain}";
+    };
+    services.redis = lib.mkIf (cfg.redis.createLocally && cfg.redis.host == "127.0.0.1") {
+      enable = true;
+    };
+    services.postgresql = lib.mkIf databaseActuallyCreateLocally {
+      enable = true;
+      ensureUsers = [
+        {
+          name = cfg.database.user;
+          ensurePermissions."DATABASE ${cfg.database.name}" = "ALL PRIVILEGES";
+        }
+      ];
+      ensureDatabases = [ cfg.database.name ];
+    };
+
+    users.users = lib.mkMerge [
+      (lib.mkIf (cfg.user == "mastodon") {
+        mastodon = {
+          isSystemUser = true;
+          home = cfg.package;
+          inherit (cfg) group;
+        };
+      })
+      (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package mastodonEnv ])
+    ];
+
+    users.groups.${cfg.group}.members = lib.optional cfg.configureNginx config.services.nginx.user;
+  };
+
+  meta.maintainers = with lib.maintainers; [ happy-river erictapen ];
+
+}
diff --git a/nixos/modules/services/web-apps/matomo-doc.xml b/nixos/modules/services/web-apps/matomo-doc.xml
new file mode 100644
index 00000000000..69d1170e452
--- /dev/null
+++ b/nixos/modules/services/web-apps/matomo-doc.xml
@@ -0,0 +1,107 @@
+<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.nix b/nixos/modules/services/web-apps/matomo.nix
new file mode 100644
index 00000000000..c6d4ed6d39d
--- /dev/null
+++ b/nixos/modules/services/web-apps/matomo.nix
@@ -0,0 +1,335 @@
+{ config, lib, options, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.matomo;
+  fpm = config.services.phpfpm.pools.${pool};
+
+  user = "matomo";
+  dataDir = "/var/lib/${user}";
+  deprecatedDataDir = "/var/lib/piwik";
+
+  pool = user;
+  phpExecutionUnit = "phpfpm-${pool}";
+  databaseService = "mysql.service";
+
+  fqdn = if config.networking.domain != null then config.networking.fqdn else config.networking.hostName;
+
+in {
+  imports = [
+    (mkRenamedOptionModule [ "services" "piwik" "enable" ] [ "services" "matomo" "enable" ])
+    (mkRenamedOptionModule [ "services" "piwik" "webServerUser" ] [ "services" "matomo" "webServerUser" ])
+    (mkRemovedOptionModule [ "services" "piwik" "phpfpmProcessManagerConfig" ] "Use services.phpfpm.pools.<name>.settings")
+    (mkRemovedOptionModule [ "services" "matomo" "phpfpmProcessManagerConfig" ] "Use services.phpfpm.pools.<name>.settings")
+    (mkRenamedOptionModule [ "services" "piwik" "nginx" ] [ "services" "matomo" "nginx" ])
+    (mkRenamedOptionModule [ "services" "matomo" "periodicArchiveProcessingUrl" ] [ "services" "matomo" "hostname" ])
+  ];
+
+  options = {
+    services.matomo = {
+      # NixOS PR for database setup: https://github.com/NixOS/nixpkgs/pull/6963
+      # Matomo issue for automatic Matomo setup: https://github.com/matomo-org/matomo/issues/10257
+      # TODO: find a nice way to do this when more NixOS MySQL and / or Matomo automatic setup stuff is implemented.
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable Matomo web analytics with php-fpm backend.
+          Either the nginx option or the webServerUser option is mandatory.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        description = ''
+          Matomo package for the service to use.
+          This can be used to point to newer releases from nixos-unstable,
+          as they don't get backported if they are not security-relevant.
+        '';
+        default = pkgs.matomo;
+        defaultText = literalExpression "pkgs.matomo";
+      };
+
+      webServerUser = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "lighttpd";
+        description = ''
+          Name of the web server user that forwards requests to <option>services.phpfpm.pools.&lt;name&gt;.socket</option> the fastcgi socket for Matomo if the nginx
+          option is not used. Either this option or the nginx option is mandatory.
+          If you want to use another webserver than nginx, you need to set this to that server's user
+          and pass fastcgi requests to `index.php`, `matomo.php` and `piwik.php` (legacy name) to this socket.
+        '';
+      };
+
+      periodicArchiveProcessing = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable periodic archive processing, which generates aggregated reports from the visits.
+
+          This means that you can safely disable browser triggers for Matomo archiving,
+          and safely enable to delete old visitor logs.
+          Before deleting visitor logs,
+          make sure though that you run <literal>systemctl start matomo-archive-processing.service</literal>
+          at least once without errors if you have already collected data before.
+        '';
+      };
+
+      hostname = mkOption {
+        type = types.str;
+        default = "${user}.${fqdn}";
+        defaultText = literalExpression ''
+          if config.${options.networking.domain} != null
+          then "${user}.''${config.${options.networking.fqdn}}"
+          else "${user}.''${config.${options.networking.hostName}}"
+        '';
+        example = "matomo.yourdomain.org";
+        description = ''
+          URL of the host, without https prefix. You may want to change it if you
+          run Matomo on a different URL than matomo.yourdomain.
+        '';
+      };
+
+      nginx = mkOption {
+        type = types.nullOr (types.submodule (
+          recursiveUpdate
+            (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
+            {
+              # enable encryption by default,
+              # as sensitive login and Matomo data should not be transmitted in clear text.
+              options.forceSSL.default = true;
+              options.enableACME.default = true;
+            }
+        )
+        );
+        default = null;
+        example = literalExpression ''
+          {
+            serverAliases = [
+              "matomo.''${config.networking.domain}"
+              "stats.''${config.networking.domain}"
+            ];
+            enableACME = false;
+          }
+        '';
+        description = ''
+            With this option, you can customize an nginx virtualHost which already has sensible defaults for Matomo.
+            Either this option or the webServerUser option is mandatory.
+            Set this to {} to just enable the virtualHost if you don't need any customization.
+            If enabled, then by default, the <option>serverName</option> is
+            <literal>''${user}.''${config.networking.hostName}.''${config.networking.domain}</literal>,
+            SSL is active, and certificates are acquired via ACME.
+            If this is set to null (the default), no nginx virtualHost will be configured.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    warnings = mkIf (cfg.nginx != null && cfg.webServerUser != null) [
+      "If services.matomo.nginx is set, services.matomo.nginx.webServerUser is ignored and should be removed."
+    ];
+
+    assertions = [ {
+        assertion = cfg.nginx != null || cfg.webServerUser != null;
+        message = "Either services.matomo.nginx or services.matomo.nginx.webServerUser is mandatory";
+    }];
+
+    users.users.${user} = {
+      isSystemUser = true;
+      createHome = true;
+      home = dataDir;
+      group  = user;
+    };
+    users.groups.${user} = {};
+
+    systemd.services.matomo-setup-update = {
+      # everything needs to set up and up to date before Matomo php files are executed
+      requiredBy = [ "${phpExecutionUnit}.service" ];
+      before = [ "${phpExecutionUnit}.service" ];
+      # the update part of the script can only work if the database is already up and running
+      requires = [ databaseService ];
+      after = [ databaseService ];
+      path = [ cfg.package ];
+      environment.PIWIK_USER_PATH = dataDir;
+      serviceConfig = {
+        Type = "oneshot";
+        User = user;
+        # hide especially config.ini.php from other
+        UMask = "0007";
+        # TODO: might get renamed to MATOMO_USER_PATH in future versions
+        # chown + chmod in preStart needs root
+        PermissionsStartOnly = true;
+      };
+
+      # correct ownership and permissions in case they're not correct anymore,
+      # e.g. after restoring from backup or moving from another system.
+      # Note that ${dataDir}/config/config.ini.php might contain the MySQL password.
+      preStart = ''
+        # migrate data from piwik to Matomo folder
+        if [ -d ${deprecatedDataDir} ]; then
+          echo "Migrating from ${deprecatedDataDir} to ${dataDir}"
+          mv -T ${deprecatedDataDir} ${dataDir}
+        fi
+        chown -R ${user}:${user} ${dataDir}
+        chmod -R ug+rwX,o-rwx ${dataDir}
+
+        if [ -e ${dataDir}/current-package ]; then
+          CURRENT_PACKAGE=$(readlink ${dataDir}/current-package)
+          NEW_PACKAGE=${cfg.package}
+          if [ "$CURRENT_PACKAGE" != "$NEW_PACKAGE" ]; then
+            # keeping tmp arround between upgrades seems to bork stuff, so delete it
+            rm -rf ${dataDir}/tmp
+          fi
+        elif [ -e ${dataDir}/tmp ]; then
+          # upgrade from 4.4.1
+          rm -rf ${dataDir}/tmp
+        fi
+        ln -sfT ${cfg.package} ${dataDir}/current-package
+        '';
+      script = ''
+            # Use User-Private Group scheme to protect Matomo data, but allow administration / backup via 'matomo' group
+            # Copy config folder
+            chmod g+s "${dataDir}"
+            cp -r "${cfg.package}/share/config" "${dataDir}/"
+            mkdir -p "${dataDir}/misc"
+            chmod -R u+rwX,g+rwX,o-rwx "${dataDir}"
+
+            # check whether user setup has already been done
+            if test -f "${dataDir}/config/config.ini.php"; then
+              # then execute possibly pending database upgrade
+              matomo-console core:update --yes
+            fi
+      '';
+    };
+
+    # If this is run regularly via the timer,
+    # 'Browser trigger archiving' can be disabled in Matomo UI > Settings > General Settings.
+    systemd.services.matomo-archive-processing = {
+      description = "Archive Matomo reports";
+      # the archiving can only work if the database is already up and running
+      requires = [ databaseService ];
+      after = [ databaseService ];
+
+      # TODO: might get renamed to MATOMO_USER_PATH in future versions
+      environment.PIWIK_USER_PATH = dataDir;
+      serviceConfig = {
+        Type = "oneshot";
+        User = user;
+        UMask = "0007";
+        CPUSchedulingPolicy = "idle";
+        IOSchedulingClass = "idle";
+        ExecStart = "${cfg.package}/bin/matomo-console core:archive --url=https://${cfg.hostname}";
+      };
+    };
+
+    systemd.timers.matomo-archive-processing = mkIf cfg.periodicArchiveProcessing {
+      description = "Automatically archive Matomo reports every hour";
+
+      wantedBy = [ "timers.target" ];
+      timerConfig = {
+        OnCalendar = "hourly";
+        Persistent = "yes";
+        AccuracySec = "10m";
+      };
+    };
+
+    systemd.services.${phpExecutionUnit} = {
+      # stop phpfpm on package upgrade, do database upgrade via matomo-setup-update, and then restart
+      restartTriggers = [ cfg.package ];
+      # stop config.ini.php from getting written with read permission for others
+      serviceConfig.UMask = "0007";
+    };
+
+    services.phpfpm.pools = let
+      # workaround for when both are null and need to generate a string,
+      # which is illegal, but as assertions apparently are being triggered *after* config generation,
+      # we have to avoid already throwing errors at this previous stage.
+      socketOwner = if (cfg.nginx != null) then config.services.nginx.user
+      else if (cfg.webServerUser != null) then cfg.webServerUser else "";
+    in {
+      ${pool} = {
+        inherit user;
+        phpOptions = ''
+          error_log = 'stderr'
+          log_errors = on
+        '';
+        settings = mapAttrs (name: mkDefault) {
+          "listen.owner" = socketOwner;
+          "listen.group" = "root";
+          "listen.mode" = "0660";
+          "pm" = "dynamic";
+          "pm.max_children" = 75;
+          "pm.start_servers" = 10;
+          "pm.min_spare_servers" = 5;
+          "pm.max_spare_servers" = 20;
+          "pm.max_requests" = 500;
+          "catch_workers_output" = true;
+        };
+        phpEnv.PIWIK_USER_PATH = dataDir;
+      };
+    };
+
+
+    services.nginx.virtualHosts = mkIf (cfg.nginx != null) {
+      # References:
+      # https://fralef.me/piwik-hardening-with-nginx-and-php-fpm.html
+      # https://github.com/perusio/piwik-nginx
+      "${cfg.hostname}" = mkMerge [ cfg.nginx {
+        # don't allow to override the root easily, as it will almost certainly break Matomo.
+        # disadvantage: not shown as default in docs.
+        root = mkForce "${cfg.package}/share";
+
+        # define locations here instead of as the submodule option's default
+        # so that they can easily be extended with additional locations if required
+        # without needing to redefine the Matomo ones.
+        # disadvantage: not shown as default in docs.
+        locations."/" = {
+          index = "index.php";
+        };
+        # allow index.php for webinterface
+        locations."= /index.php".extraConfig = ''
+          fastcgi_pass unix:${fpm.socket};
+        '';
+        # allow matomo.php for tracking
+        locations."= /matomo.php".extraConfig = ''
+          fastcgi_pass unix:${fpm.socket};
+        '';
+        # allow piwik.php for tracking (deprecated name)
+        locations."= /piwik.php".extraConfig = ''
+          fastcgi_pass unix:${fpm.socket};
+        '';
+        # Any other attempt to access any php files is forbidden
+        locations."~* ^.+\\.php$".extraConfig = ''
+          return 403;
+        '';
+        # Disallow access to unneeded directories
+        # config and tmp are already removed
+        locations."~ ^/(?:core|lang|misc)/".extraConfig = ''
+          return 403;
+        '';
+        # Disallow access to several helper files
+        locations."~* \\.(?:bat|git|ini|sh|txt|tpl|xml|md)$".extraConfig = ''
+          return 403;
+        '';
+        # No crawling of this site for bots that obey robots.txt - no useful information here.
+        locations."= /robots.txt".extraConfig = ''
+          return 200 "User-agent: *\nDisallow: /\n";
+        '';
+        # let browsers cache matomo.js
+        locations."= /matomo.js".extraConfig = ''
+          expires 1M;
+        '';
+        # let browsers cache piwik.js (deprecated name)
+        locations."= /piwik.js".extraConfig = ''
+          expires 1M;
+        '';
+      }];
+    };
+  };
+
+  meta = {
+    doc = ./matomo-doc.xml;
+    maintainers = with lib.maintainers; [ florianjacob ];
+  };
+}
diff --git a/nixos/modules/services/web-apps/mattermost.nix b/nixos/modules/services/web-apps/mattermost.nix
new file mode 100644
index 00000000000..2901f307dc5
--- /dev/null
+++ b/nixos/modules/services/web-apps/mattermost.nix
@@ -0,0 +1,344 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.mattermost;
+
+  database = "postgres://${cfg.localDatabaseUser}:${cfg.localDatabasePassword}@localhost:5432/${cfg.localDatabaseName}?sslmode=disable&connect_timeout=10";
+
+  postgresPackage = config.services.postgresql.package;
+
+  createDb = {
+    statePath ? cfg.statePath,
+    localDatabaseUser ? cfg.localDatabaseUser,
+    localDatabasePassword ? cfg.localDatabasePassword,
+    localDatabaseName ? cfg.localDatabaseName,
+    useSudo ? true
+  }: ''
+    if ! test -e ${escapeShellArg "${statePath}/.db-created"}; then
+      ${lib.optionalString useSudo "${pkgs.sudo}/bin/sudo -u ${escapeShellArg config.services.postgresql.superUser} \\"}
+        ${postgresPackage}/bin/psql postgres -c \
+          "CREATE ROLE ${localDatabaseUser} WITH LOGIN NOCREATEDB NOCREATEROLE ENCRYPTED PASSWORD '${localDatabasePassword}'"
+      ${lib.optionalString useSudo "${pkgs.sudo}/bin/sudo -u ${escapeShellArg config.services.postgresql.superUser} \\"}
+        ${postgresPackage}/bin/createdb \
+          --owner ${escapeShellArg localDatabaseUser} ${escapeShellArg localDatabaseName}
+      touch ${escapeShellArg "${statePath}/.db-created"}
+    fi
+  '';
+
+  mattermostPluginDerivations = with pkgs;
+    map (plugin: stdenv.mkDerivation {
+      name = "mattermost-plugin";
+      installPhase = ''
+        mkdir -p $out/share
+        cp ${plugin} $out/share/plugin.tar.gz
+      '';
+      dontUnpack = true;
+      dontPatch = true;
+      dontConfigure = true;
+      dontBuild = true;
+      preferLocalBuild = true;
+    }) cfg.plugins;
+
+  mattermostPlugins = with pkgs;
+    if mattermostPluginDerivations == [] then null
+    else stdenv.mkDerivation {
+      name = "${cfg.package.name}-plugins";
+      nativeBuildInputs = [
+        autoPatchelfHook
+      ] ++ mattermostPluginDerivations;
+      buildInputs = [
+        cfg.package
+      ];
+      installPhase = ''
+        mkdir -p $out/data/plugins
+        plugins=(${escapeShellArgs (map (plugin: "${plugin}/share/plugin.tar.gz") mattermostPluginDerivations)})
+        for plugin in "''${plugins[@]}"; do
+          hash="$(sha256sum "$plugin" | cut -d' ' -f1)"
+          mkdir -p "$hash"
+          tar -C "$hash" -xzf "$plugin"
+          autoPatchelf "$hash"
+          GZIP_OPT=-9 tar -C "$hash" -cvzf "$out/data/plugins/$hash.tar.gz" .
+          rm -rf "$hash"
+        done
+      '';
+
+      dontUnpack = true;
+      dontPatch = true;
+      dontConfigure = true;
+      dontBuild = true;
+      preferLocalBuild = true;
+    };
+
+  mattermostConfWithoutPlugins = recursiveUpdate
+    { ServiceSettings.SiteURL = cfg.siteUrl;
+      ServiceSettings.ListenAddress = cfg.listenAddress;
+      TeamSettings.SiteName = cfg.siteName;
+      SqlSettings.DriverName = "postgres";
+      SqlSettings.DataSource = database;
+      PluginSettings.Directory = "${cfg.statePath}/plugins/server";
+      PluginSettings.ClientDirectory = "${cfg.statePath}/plugins/client";
+    }
+    cfg.extraConfig;
+
+  mattermostConf = recursiveUpdate
+    mattermostConfWithoutPlugins
+    (
+      if mattermostPlugins == null then {}
+      else {
+        PluginSettings = {
+          Enable = true;
+        };
+      }
+    );
+
+  mattermostConfJSON = pkgs.writeText "mattermost-config.json" (builtins.toJSON mattermostConf);
+
+in
+
+{
+  options = {
+    services.mattermost = {
+      enable = mkEnableOption "Mattermost chat server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.mattermost;
+        defaultText = "pkgs.mattermost";
+        description = "Mattermost derivation to use.";
+      };
+
+      statePath = mkOption {
+        type = types.str;
+        default = "/var/lib/mattermost";
+        description = "Mattermost working directory";
+      };
+
+      siteUrl = mkOption {
+        type = types.str;
+        example = "https://chat.example.com";
+        description = ''
+          URL this Mattermost instance is reachable under, without trailing slash.
+        '';
+      };
+
+      siteName = mkOption {
+        type = types.str;
+        default = "Mattermost";
+        description = "Name of this Mattermost site.";
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = ":8065";
+        example = "[::1]:8065";
+        description = ''
+          Address and port this Mattermost instance listens to.
+        '';
+      };
+
+      mutableConfig = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether the Mattermost config.json is writeable by Mattermost.
+
+          Most of the settings can be edited in the system console of
+          Mattermost if this option is enabled. A template config using
+          the options specified in services.mattermost will be generated
+          but won't be overwritten on changes or rebuilds.
+
+          If this option is disabled, changes in the system console won't
+          be possible (default). If an config.json is present, it will be
+          overwritten!
+        '';
+      };
+
+      preferNixConfig = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If both mutableConfig and this option are set, the Nix configuration
+          will take precedence over any settings configured in the server
+          console.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.attrs;
+        default = { };
+        description = ''
+          Addtional configuration options as Nix attribute set in config.json schema.
+        '';
+      };
+
+      plugins = mkOption {
+        type = types.listOf (types.oneOf [types.path types.package]);
+        default = [];
+        example = "[ ./com.github.moussetc.mattermost.plugin.giphy-2.0.0.tar.gz ]";
+        description = ''
+          Plugins to add to the configuration. Overrides any installed if non-null.
+          This is a list of paths to .tar.gz files or derivations evaluating to
+          .tar.gz files.
+        '';
+      };
+
+      localDatabaseCreate = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Create a local PostgreSQL database for Mattermost automatically.
+        '';
+      };
+
+      localDatabaseName = mkOption {
+        type = types.str;
+        default = "mattermost";
+        description = ''
+          Local Mattermost database name.
+        '';
+      };
+
+      localDatabaseUser = mkOption {
+        type = types.str;
+        default = "mattermost";
+        description = ''
+          Local Mattermost database username.
+        '';
+      };
+
+      localDatabasePassword = mkOption {
+        type = types.str;
+        default = "mmpgsecret";
+        description = ''
+          Password for local Mattermost database user.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "mattermost";
+        description = ''
+          User which runs the Mattermost service.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "mattermost";
+        description = ''
+          Group which runs the Mattermost service.
+        '';
+      };
+
+      matterircd = {
+        enable = mkEnableOption "Mattermost IRC bridge";
+        package = mkOption {
+          type = types.package;
+          default = pkgs.matterircd;
+          defaultText = "pkgs.matterircd";
+          description = "matterircd derivation to use.";
+        };
+        parameters = mkOption {
+          type = types.listOf types.str;
+          default = [ ];
+          example = [ "-mmserver chat.example.com" "-bind [::]:6667" ];
+          description = ''
+            Set commandline parameters to pass to matterircd. See
+            https://github.com/42wim/matterircd#usage for more information.
+          '';
+        };
+      };
+    };
+  };
+
+  config = mkMerge [
+    (mkIf cfg.enable {
+      users.users = optionalAttrs (cfg.user == "mattermost") {
+        mattermost = {
+          group = cfg.group;
+          uid = config.ids.uids.mattermost;
+          home = cfg.statePath;
+        };
+      };
+
+      users.groups = optionalAttrs (cfg.group == "mattermost") {
+        mattermost.gid = config.ids.gids.mattermost;
+      };
+
+      services.postgresql.enable = cfg.localDatabaseCreate;
+
+      # The systemd service will fail to execute the preStart hook
+      # if the WorkingDirectory does not exist
+      system.activationScripts.mattermost = ''
+        mkdir -p "${cfg.statePath}"
+      '';
+
+      systemd.services.mattermost = {
+        description = "Mattermost chat service";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" "postgresql.service" ];
+
+        preStart = ''
+          mkdir -p "${cfg.statePath}"/{data,config,logs,plugins}
+          mkdir -p "${cfg.statePath}/plugins"/{client,server}
+          ln -sf ${cfg.package}/{bin,fonts,i18n,templates,client} "${cfg.statePath}"
+        '' + lib.optionalString (mattermostPlugins != null) ''
+          rm -rf "${cfg.statePath}/data/plugins"
+          ln -sf ${mattermostPlugins}/data/plugins "${cfg.statePath}/data"
+        '' + lib.optionalString (!cfg.mutableConfig) ''
+          rm -f "${cfg.statePath}/config/config.json"
+          ${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${cfg.package}/config/config.json ${mattermostConfJSON} > "${cfg.statePath}/config/config.json"
+        '' + lib.optionalString cfg.mutableConfig ''
+          if ! test -e "${cfg.statePath}/config/.initial-created"; then
+            rm -f ${cfg.statePath}/config/config.json
+            ${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${cfg.package}/config/config.json ${mattermostConfJSON} > "${cfg.statePath}/config/config.json"
+            touch "${cfg.statePath}/config/.initial-created"
+          fi
+        '' + lib.optionalString (cfg.mutableConfig && cfg.preferNixConfig) ''
+          new_config="$(${pkgs.jq}/bin/jq -s '.[0] * .[1]' "${cfg.statePath}/config/config.json" ${mattermostConfJSON})"
+
+          rm -f "${cfg.statePath}/config/config.json"
+          echo "$new_config" > "${cfg.statePath}/config/config.json"
+        '' + lib.optionalString cfg.localDatabaseCreate (createDb {}) + ''
+          # Don't change permissions recursively on the data, current, and symlinked directories (see ln -sf command above).
+          # This dramatically decreases startup times for installations with a lot of files.
+          find . -maxdepth 1 -not -name data -not -name client -not -name templates -not -name i18n -not -name fonts -not -name bin -not -name . \
+            -exec chown "${cfg.user}:${cfg.group}" -R {} \; -exec chmod u+rw,g+r,o-rwx -R {} \;
+
+          chown "${cfg.user}:${cfg.group}" "${cfg.statePath}/data" .
+          chmod u+rw,g+r,o-rwx "${cfg.statePath}/data" .
+        '';
+
+        serviceConfig = {
+          PermissionsStartOnly = true;
+          User = cfg.user;
+          Group = cfg.group;
+          ExecStart = "${cfg.package}/bin/mattermost";
+          WorkingDirectory = "${cfg.statePath}";
+          Restart = "always";
+          RestartSec = "10";
+          LimitNOFILE = "49152";
+        };
+        unitConfig.JoinsNamespaceOf = mkIf cfg.localDatabaseCreate "postgresql.service";
+      };
+    })
+    (mkIf cfg.matterircd.enable {
+      systemd.services.matterircd = {
+        description = "Mattermost IRC bridge service";
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          User = "nobody";
+          Group = "nogroup";
+          ExecStart = "${cfg.matterircd.package}/bin/matterircd ${escapeShellArgs cfg.matterircd.parameters}";
+          WorkingDirectory = "/tmp";
+          PrivateTmp = true;
+          Restart = "always";
+          RestartSec = "5";
+        };
+      };
+    })
+  ];
+}
diff --git a/nixos/modules/services/web-apps/mediawiki.nix b/nixos/modules/services/web-apps/mediawiki.nix
new file mode 100644
index 00000000000..977b6f60b23
--- /dev/null
+++ b/nixos/modules/services/web-apps/mediawiki.nix
@@ -0,0 +1,475 @@
+{ config, pkgs, lib, ... }:
+
+let
+
+  inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption;
+  inherit (lib) concatStringsSep literalExpression mapAttrsToList optional optionals optionalString types;
+
+  cfg = config.services.mediawiki;
+  fpm = config.services.phpfpm.pools.mediawiki;
+  user = "mediawiki";
+  group = config.services.httpd.group;
+  cacheDir = "/var/cache/mediawiki";
+  stateDir = "/var/lib/mediawiki";
+
+  pkg = pkgs.stdenv.mkDerivation rec {
+    pname = "mediawiki-full";
+    version = src.version;
+    src = cfg.package;
+
+    installPhase = ''
+      mkdir -p $out
+      cp -r * $out/
+
+      rm -rf $out/share/mediawiki/skins/*
+      rm -rf $out/share/mediawiki/extensions/*
+
+      ${concatStringsSep "\n" (mapAttrsToList (k: v: ''
+        ln -s ${v} $out/share/mediawiki/skins/${k}
+      '') cfg.skins)}
+
+      ${concatStringsSep "\n" (mapAttrsToList (k: v: ''
+        ln -s ${if v != null then v else "$src/share/mediawiki/extensions/${k}"} $out/share/mediawiki/extensions/${k}
+      '') cfg.extensions)}
+    '';
+  };
+
+  mediawikiScripts = pkgs.runCommand "mediawiki-scripts" {
+    buildInputs = [ pkgs.makeWrapper ];
+    preferLocalBuild = true;
+  } ''
+    mkdir -p $out/bin
+    for i in changePassword.php createAndPromote.php userOptions.php edit.php nukePage.php update.php; do
+      makeWrapper ${pkgs.php}/bin/php $out/bin/mediawiki-$(basename $i .php) \
+        --set MEDIAWIKI_CONFIG ${mediawikiConfig} \
+        --add-flags ${pkg}/share/mediawiki/maintenance/$i
+    done
+  '';
+
+  mediawikiConfig = pkgs.writeText "LocalSettings.php" ''
+    <?php
+      # Protect against web entry
+      if ( !defined( 'MEDIAWIKI' ) ) {
+        exit;
+      }
+
+      $wgSitename = "${cfg.name}";
+      $wgMetaNamespace = false;
+
+      ## The URL base path to the directory containing the wiki;
+      ## defaults for all runtime URL paths are based off of this.
+      ## For more information on customizing the URLs
+      ## (like /w/index.php/Page_title to /wiki/Page_title) please see:
+      ## https://www.mediawiki.org/wiki/Manual:Short_URL
+      $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}";
+
+      ## The URL path to static resources (images, scripts, etc.)
+      $wgResourceBasePath = $wgScriptPath;
+
+      ## The URL path to the logo.  Make sure you change this from the default,
+      ## or else you'll overwrite your logo when you upgrade!
+      $wgLogo = "$wgResourceBasePath/resources/assets/wiki.png";
+
+      ## UPO means: this is also a user preference option
+
+      $wgEnableEmail = true;
+      $wgEnableUserEmail = true; # UPO
+
+      $wgEmergencyContact = "${if cfg.virtualHost.adminAddr != null then cfg.virtualHost.adminAddr else config.services.httpd.adminAddr}";
+      $wgPasswordSender = $wgEmergencyContact;
+
+      $wgEnotifUserTalk = false; # UPO
+      $wgEnotifWatchlist = false; # UPO
+      $wgEmailAuthentication = true;
+
+      ## Database settings
+      $wgDBtype = "${cfg.database.type}";
+      $wgDBserver = "${cfg.database.host}:${if cfg.database.socket != null then cfg.database.socket else toString cfg.database.port}";
+      $wgDBname = "${cfg.database.name}";
+      $wgDBuser = "${cfg.database.user}";
+      ${optionalString (cfg.database.passwordFile != null) "$wgDBpassword = file_get_contents(\"${cfg.database.passwordFile}\");"}
+
+      ${optionalString (cfg.database.type == "mysql" && cfg.database.tablePrefix != null) ''
+        # MySQL specific settings
+        $wgDBprefix = "${cfg.database.tablePrefix}";
+      ''}
+
+      ${optionalString (cfg.database.type == "mysql") ''
+        # MySQL table options to use during installation or update
+        $wgDBTableOptions = "ENGINE=InnoDB, DEFAULT CHARSET=binary";
+      ''}
+
+      ## Shared memory settings
+      $wgMainCacheType = CACHE_NONE;
+      $wgMemCachedServers = [];
+
+      ${optionalString (cfg.uploadsDir != null) ''
+        $wgEnableUploads = true;
+        $wgUploadDirectory = "${cfg.uploadsDir}";
+      ''}
+
+      $wgUseImageMagick = true;
+      $wgImageMagickConvertCommand = "${pkgs.imagemagick}/bin/convert";
+
+      # InstantCommons allows wiki to use images from https://commons.wikimedia.org
+      $wgUseInstantCommons = false;
+
+      # Periodically send a pingback to https://www.mediawiki.org/ with basic data
+      # about this MediaWiki instance. The Wikimedia Foundation shares this data
+      # with MediaWiki developers to help guide future development efforts.
+      $wgPingback = true;
+
+      ## If you use ImageMagick (or any other shell command) on a
+      ## Linux server, this will need to be set to the name of an
+      ## available UTF-8 locale
+      $wgShellLocale = "C.UTF-8";
+
+      ## Set $wgCacheDirectory to a writable directory on the web server
+      ## to make your wiki go slightly faster. The directory should not
+      ## be publically accessible from the web.
+      $wgCacheDirectory = "${cacheDir}";
+
+      # Site language code, should be one of the list in ./languages/data/Names.php
+      $wgLanguageCode = "en";
+
+      $wgSecretKey = file_get_contents("${stateDir}/secret.key");
+
+      # Changing this will log out all existing sessions.
+      $wgAuthenticationTokenVersion = "";
+
+      ## For attaching licensing metadata to pages, and displaying an
+      ## appropriate copyright notice / icon. GNU Free Documentation
+      ## License and Creative Commons licenses are supported so far.
+      $wgRightsPage = ""; # Set to the title of a wiki page that describes your license/copyright
+      $wgRightsUrl = "";
+      $wgRightsText = "";
+      $wgRightsIcon = "";
+
+      # Path to the GNU diff3 utility. Used for conflict resolution.
+      $wgDiff = "${pkgs.diffutils}/bin/diff";
+      $wgDiff3 = "${pkgs.diffutils}/bin/diff3";
+
+      # Enabled skins.
+      ${concatStringsSep "\n" (mapAttrsToList (k: v: "wfLoadSkin('${k}');") cfg.skins)}
+
+      # Enabled extensions.
+      ${concatStringsSep "\n" (mapAttrsToList (k: v: "wfLoadExtension('${k}');") cfg.extensions)}
+
+
+      # End of automatically generated settings.
+      # Add more configuration options below.
+
+      ${cfg.extraConfig}
+  '';
+
+in
+{
+  # interface
+  options = {
+    services.mediawiki = {
+
+      enable = mkEnableOption "MediaWiki";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.mediawiki;
+        defaultText = literalExpression "pkgs.mediawiki";
+        description = "Which MediaWiki package to use.";
+      };
+
+      name = mkOption {
+        type = types.str;
+        default = "MediaWiki";
+        example = "Foobar Wiki";
+        description = "Name of the wiki.";
+      };
+
+      uploadsDir = mkOption {
+        type = types.nullOr types.path;
+        default = "${stateDir}/uploads";
+        description = ''
+          This directory is used for uploads of pictures. The directory passed here is automatically
+          created and permissions adjusted as required.
+        '';
+      };
+
+      passwordFile = mkOption {
+        type = types.path;
+        description = "A file containing the initial password for the admin user.";
+        example = "/run/keys/mediawiki-password";
+      };
+
+      skins = mkOption {
+        default = {};
+        type = types.attrsOf types.path;
+        description = ''
+          Attribute set of paths whose content is copied to the <filename>skins</filename>
+          subdirectory of the MediaWiki installation in addition to the default skins.
+        '';
+      };
+
+      extensions = mkOption {
+        default = {};
+        type = types.attrsOf (types.nullOr types.path);
+        description = ''
+          Attribute set of paths whose content is copied to the <filename>extensions</filename>
+          subdirectory of the MediaWiki installation and enabled in configuration.
+
+          Use <literal>null</literal> instead of path to enable extensions that are part of MediaWiki.
+        '';
+        example = literalExpression ''
+          {
+            Matomo = pkgs.fetchzip {
+              url = "https://github.com/DaSchTour/matomo-mediawiki-extension/archive/v4.0.1.tar.gz";
+              sha256 = "0g5rd3zp0avwlmqagc59cg9bbkn3r7wx7p6yr80s644mj6dlvs1b";
+            };
+            ParserFunctions = null;
+          }
+        '';
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum [ "mysql" "postgres" "sqlite" "mssql" "oracle" ];
+          default = "mysql";
+          description = "Database engine to use. MySQL/MariaDB is the database of choice by MediaWiki developers.";
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = "Database host address.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 3306;
+          description = "Database host port.";
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "mediawiki";
+          description = "Database name.";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "mediawiki";
+          description = "Database user.";
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          example = "/run/keys/mediawiki-dbpassword";
+          description = ''
+            A file containing the password corresponding to
+            <option>database.user</option>.
+          '';
+        };
+
+        tablePrefix = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            If you only have access to a single database and wish to install more than
+            one version of MediaWiki, or have other applications that also use the
+            database, you can give the table names a unique prefix to stop any naming
+            conflicts or confusion.
+            See <link xlink:href='https://www.mediawiki.org/wiki/Manual:$wgDBprefix'/>.
+          '';
+        };
+
+        socket = mkOption {
+          type = types.nullOr types.path;
+          default = if cfg.database.createLocally then "/run/mysqld/mysqld.sock" else null;
+          defaultText = literalExpression "/run/mysqld/mysqld.sock";
+          description = "Path to the unix socket file to use for authentication.";
+        };
+
+        createLocally = mkOption {
+          type = types.bool;
+          default = cfg.database.type == "mysql";
+          defaultText = literalExpression "true";
+          description = ''
+            Create the database and database user locally.
+            This currently only applies if database type "mysql" is selected.
+          '';
+        };
+      };
+
+      virtualHost = mkOption {
+        type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
+        example = literalExpression ''
+          {
+            hostName = "mediawiki.example.org";
+            adminAddr = "webmaster@example.org";
+            forceSSL = true;
+            enableACME = true;
+          }
+        '';
+        description = ''
+          Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>.
+          See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+        '';
+      };
+
+      poolConfig = mkOption {
+        type = with types; attrsOf (oneOf [ str int bool ]);
+        default = {
+          "pm" = "dynamic";
+          "pm.max_children" = 32;
+          "pm.start_servers" = 2;
+          "pm.min_spare_servers" = 2;
+          "pm.max_spare_servers" = 4;
+          "pm.max_requests" = 500;
+        };
+        description = ''
+          Options for the MediaWiki PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+          for details on configuration directives.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        description = ''
+          Any additional text to be appended to MediaWiki's
+          LocalSettings.php configuration file. For configuration
+          settings, see <link xlink:href="https://www.mediawiki.org/wiki/Manual:Configuration_settings"/>.
+        '';
+        default = "";
+        example = ''
+          $wgEnableEmail = false;
+        '';
+      };
+
+    };
+  };
+
+  # 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.user == user;
+        message = "services.mediawiki.database.user must be set to ${user} if services.mediawiki.database.createLocally is set true";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.socket != null;
+        message = "services.mediawiki.database.socket must be set if services.mediawiki.database.createLocally is set to true";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
+        message = "a password cannot be specified if services.mediawiki.database.createLocally is set to true";
+      }
+    ];
+
+    services.mediawiki.skins = {
+      MonoBook = "${cfg.package}/share/mediawiki/skins/MonoBook";
+      Timeless = "${cfg.package}/share/mediawiki/skins/Timeless";
+      Vector = "${cfg.package}/share/mediawiki/skins/Vector";
+    };
+
+    services.mysql = mkIf cfg.database.createLocally {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    services.phpfpm.pools.mediawiki = {
+      inherit user group;
+      phpEnv.MEDIAWIKI_CONFIG = "${mediawikiConfig}";
+      settings = {
+        "listen.owner" = config.services.httpd.user;
+        "listen.group" = config.services.httpd.group;
+      } // cfg.poolConfig;
+    };
+
+    services.httpd = {
+      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>
+        '';
+      } ];
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${stateDir}' 0750 ${user} ${group} - -"
+      "d '${cacheDir}' 0750 ${user} ${group} - -"
+    ] ++ optionals (cfg.uploadsDir != null) [
+      "d '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
+      "Z '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
+    ];
+
+    systemd.services.mediawiki-init = {
+      wantedBy = [ "multi-user.target" ];
+      before = [ "phpfpm-mediawiki.service" ];
+      after = optional cfg.database.createLocally "mysql.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
+        fi
+
+        echo "exit( wfGetDB( DB_MASTER )->tableExists( 'user' ) ? 1 : 0 );" | \
+        ${pkgs.php}/bin/php ${pkg}/share/mediawiki/maintenance/eval.php --conf ${mediawikiConfig} && \
+        ${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}"} \
+          --dbport ${toString cfg.database.port} \
+          --dbname ${cfg.database.name} \
+          ${optionalString (cfg.database.tablePrefix != null) "--dbprefix ${cfg.database.tablePrefix}"} \
+          --dbuser ${cfg.database.user} \
+          ${optionalString (cfg.database.passwordFile != null) "--dbpassfile ${cfg.database.passwordFile}"} \
+          --passfile ${cfg.passwordFile} \
+          ${cfg.name} \
+          admin
+
+        ${pkgs.php}/bin/php ${pkg}/share/mediawiki/maintenance/update.php --conf ${mediawikiConfig} --quick
+      '';
+
+      serviceConfig = {
+        Type = "oneshot";
+        User = user;
+        Group = group;
+        PrivateTmp = true;
+      };
+    };
+
+    systemd.services.httpd.after = optional (cfg.database.createLocally && cfg.database.type == "mysql") "mysql.service";
+
+    users.users.${user} = {
+      group = group;
+      isSystemUser = true;
+    };
+
+    environment.systemPackages = [ mediawikiScripts ];
+  };
+}
diff --git a/nixos/modules/services/web-apps/miniflux.nix b/nixos/modules/services/web-apps/miniflux.nix
new file mode 100644
index 00000000000..641c9be85d8
--- /dev/null
+++ b/nixos/modules/services/web-apps/miniflux.nix
@@ -0,0 +1,127 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.miniflux;
+
+  defaultAddress = "localhost:8080";
+
+  dbUser = "miniflux";
+  dbName = "miniflux";
+
+  pgbin = "${config.services.postgresql.package}/bin";
+  preStart = pkgs.writeScript "miniflux-pre-start" ''
+    #!${pkgs.runtimeShell}
+    ${pgbin}/psql "${dbName}" -c "CREATE EXTENSION IF NOT EXISTS hstore"
+  '';
+in
+
+{
+  options = {
+    services.miniflux = {
+      enable = mkEnableOption "miniflux and creates a local postgres database for it";
+
+      config = mkOption {
+        type = types.attrsOf types.str;
+        example = literalExpression ''
+          {
+            CLEANUP_FREQUENCY = "48";
+            LISTEN_ADDR = "localhost:8080";
+          }
+        '';
+        description = ''
+          Configuration for Miniflux, refer to
+          <link xlink:href="https://miniflux.app/docs/configuration.html"/>
+          for documentation on the supported values.
+
+          Correct configuration for the database is already provided.
+          By default, listens on ${defaultAddress}.
+        '';
+      };
+
+      adminCredentialsFile = mkOption  {
+        type = types.path;
+        description = ''
+          File containing the ADMIN_USERNAME and
+          ADMIN_PASSWORD (length >= 6) in the format of
+          an EnvironmentFile=, as described by systemd.exec(5).
+        '';
+        example = "/etc/nixos/miniflux-admin-credentials";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    services.miniflux.config =  {
+      LISTEN_ADDR = mkDefault defaultAddress;
+      DATABASE_URL = "user=${dbUser} host=/run/postgresql dbname=${dbName}";
+      RUN_MIGRATIONS = "1";
+      CREATE_ADMIN = "1";
+    };
+
+    services.postgresql = {
+      enable = true;
+      ensureUsers = [ {
+        name = dbUser;
+        ensurePermissions = {
+          "DATABASE ${dbName}" = "ALL PRIVILEGES";
+        };
+      } ];
+      ensureDatabases = [ dbName ];
+    };
+
+    systemd.services.miniflux-dbsetup = {
+      description = "Miniflux database setup";
+      requires = [ "postgresql.service" ];
+      after = [ "network.target" "postgresql.service" ];
+      serviceConfig = {
+        Type = "oneshot";
+        User = config.services.postgresql.superUser;
+        ExecStart = preStart;
+      };
+    };
+
+    systemd.services.miniflux = {
+      description = "Miniflux service";
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "miniflux-dbsetup.service" ];
+      after = [ "network.target" "postgresql.service" "miniflux-dbsetup.service" ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.miniflux}/bin/miniflux";
+        User = dbUser;
+        DynamicUser = true;
+        RuntimeDirectory = "miniflux";
+        RuntimeDirectoryMode = "0700";
+        EnvironmentFile = cfg.adminCredentialsFile;
+        # Hardening
+        CapabilityBoundingSet = [ "" ];
+        DeviceAllow = [ "" ];
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+        UMask = "0077";
+      };
+
+      environment = cfg.config;
+    };
+    environment.systemPackages = [ pkgs.miniflux ];
+  };
+}
diff --git a/nixos/modules/services/web-apps/moodle.nix b/nixos/modules/services/web-apps/moodle.nix
new file mode 100644
index 00000000000..19f3e754691
--- /dev/null
+++ b/nixos/modules/services/web-apps/moodle.nix
@@ -0,0 +1,315 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types;
+  inherit (lib) concatStringsSep literalExpression mapAttrsToList optional optionalString;
+
+  cfg = config.services.moodle;
+  fpm = config.services.phpfpm.pools.moodle;
+
+  user = "moodle";
+  group = config.services.httpd.group;
+  stateDir = "/var/lib/moodle";
+
+  moodleConfig = pkgs.writeText "config.php" ''
+  <?php  // Moodle configuration file
+
+  unset($CFG);
+  global $CFG;
+  $CFG = new stdClass();
+
+  $CFG->dbtype    = '${ { mysql = "mariadb"; pgsql = "pgsql"; }.${cfg.database.type} }';
+  $CFG->dblibrary = 'native';
+  $CFG->dbhost    = '${cfg.database.host}';
+  $CFG->dbname    = '${cfg.database.name}';
+  $CFG->dbuser    = '${cfg.database.user}';
+  ${optionalString (cfg.database.passwordFile != null) "$CFG->dbpass = file_get_contents('${cfg.database.passwordFile}');"}
+  $CFG->prefix    = 'mdl_';
+  $CFG->dboptions = array (
+    'dbpersist' => 0,
+    'dbport' => '${toString cfg.database.port}',
+    ${optionalString (cfg.database.socket != null) "'dbsocket' => '${cfg.database.socket}',"}
+    'dbcollation' => 'utf8mb4_unicode_ci',
+  );
+
+  $CFG->wwwroot   = '${if cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL then "https" else "http"}://${cfg.virtualHost.hostName}';
+  $CFG->dataroot  = '${stateDir}';
+  $CFG->admin     = 'admin';
+
+  $CFG->directorypermissions = 02777;
+  $CFG->disableupdateautodeploy = true;
+
+  $CFG->pathtogs = '${pkgs.ghostscript}/bin/gs';
+  $CFG->pathtophp = '${phpExt}/bin/php';
+  $CFG->pathtodu = '${pkgs.coreutils}/bin/du';
+  $CFG->aspellpath = '${pkgs.aspell}/bin/aspell';
+  $CFG->pathtodot = '${pkgs.graphviz}/bin/dot';
+
+  ${cfg.extraConfig}
+
+  require_once('${cfg.package}/share/moodle/lib/setup.php');
+
+  // There is no php closing tag in this file,
+  // it is intentional because it prevents trailing whitespace problems!
+  '';
+
+  mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
+  pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
+
+  phpExt = pkgs.php74.withExtensions
+        ({ enabled, all }: with all; [ iconv mbstring curl openssl tokenizer xmlrpc soap ctype zip gd simplexml dom  intl json sqlite3 pgsql pdo_sqlite pdo_pgsql pdo_odbc pdo_mysql pdo mysqli session zlib xmlreader fileinfo filter opcache ]);
+in
+{
+  # interface
+  options.services.moodle = {
+    enable = mkEnableOption "Moodle web application";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.moodle;
+      defaultText = literalExpression "pkgs.moodle";
+      description = "The Moodle package to use.";
+    };
+
+    initialPassword = mkOption {
+      type = types.str;
+      example = "correcthorsebatterystaple";
+      description = ''
+        Specifies the initial password for the admin, i.e. the password assigned if the user does not already exist.
+        The password specified here is world-readable in the Nix store, so it should be changed promptly.
+      '';
+    };
+
+    database = {
+      type = mkOption {
+        type = types.enum [ "mysql" "pgsql" ];
+        default = "mysql";
+        description = "Database engine to use.";
+      };
+
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Database host address.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        description = "Database host port.";
+        default = {
+          mysql = 3306;
+          pgsql = 5432;
+        }.${cfg.database.type};
+        defaultText = literalExpression "3306";
+      };
+
+      name = mkOption {
+        type = types.str;
+        default = "moodle";
+        description = "Database name.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "moodle";
+        description = "Database user.";
+      };
+
+      passwordFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/run/keys/moodle-dbpassword";
+        description = ''
+          A file containing the password corresponding to
+          <option>database.user</option>.
+        '';
+      };
+
+      socket = mkOption {
+        type = types.nullOr types.path;
+        default =
+          if mysqlLocal then "/run/mysqld/mysqld.sock"
+          else if pgsqlLocal then "/run/postgresql"
+          else null;
+        defaultText = literalExpression "/run/mysqld/mysqld.sock";
+        description = "Path to the unix socket file to use for authentication.";
+      };
+
+      createLocally = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Create the database and database user locally.";
+      };
+    };
+
+    virtualHost = mkOption {
+      type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
+      example = literalExpression ''
+        {
+          hostName = "moodle.example.org";
+          adminAddr = "webmaster@example.org";
+          forceSSL = true;
+          enableACME = true;
+        }
+      '';
+      description = ''
+        Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>.
+        See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+      '';
+    };
+
+    poolConfig = mkOption {
+      type = with types; attrsOf (oneOf [ str int bool ]);
+      default = {
+        "pm" = "dynamic";
+        "pm.max_children" = 32;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 2;
+        "pm.max_spare_servers" = 4;
+        "pm.max_requests" = 500;
+      };
+      description = ''
+        Options for the Moodle PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+        for details on configuration directives.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Any additional text to be appended to the config.php
+        configuration file. This is a PHP script. For configuration
+        details, see <link xlink:href="https://docs.moodle.org/37/en/Configuration_file"/>.
+      '';
+      example = ''
+        $CFG->disableupdatenotifications = true;
+      '';
+    };
+  };
+
+  # implementation
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = cfg.database.createLocally -> cfg.database.user == user;
+        message = "services.moodle.database.user must be set to ${user} if services.moodle.database.createLocally is set true";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
+        message = "a password cannot be specified if services.moodle.database.createLocally is set to true";
+      }
+    ];
+
+    services.mysql = mkIf mysqlLocal {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = {
+            "${cfg.database.name}.*" = "SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER";
+          };
+        }
+      ];
+    };
+
+    services.postgresql = mkIf pgsqlLocal {
+      enable = true;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    services.phpfpm.pools.moodle = {
+      inherit user group;
+      phpPackage = phpExt;
+      phpEnv.MOODLE_CONFIG = "${moodleConfig}";
+      phpOptions = ''
+        zend_extension = opcache.so
+        opcache.enable = 1
+      '';
+      settings = {
+        "listen.owner" = config.services.httpd.user;
+        "listen.group" = config.services.httpd.group;
+      } // cfg.poolConfig;
+    };
+
+    services.httpd = {
+      enable = true;
+      adminAddr = mkDefault cfg.virtualHost.adminAddr;
+      extraModules = [ "proxy_fcgi" ];
+      virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
+        documentRoot = mkForce "${cfg.package}/share/moodle";
+        extraConfig = ''
+          <Directory "${cfg.package}/share/moodle">
+            <FilesMatch "\.php$">
+              <If "-f %{REQUEST_FILENAME}">
+                SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
+              </If>
+            </FilesMatch>
+            Options -Indexes
+            DirectoryIndex index.php
+          </Directory>
+        '';
+      } ];
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${stateDir}' 0750 ${user} ${group} - -"
+    ];
+
+    systemd.services.moodle-init = {
+      wantedBy = [ "multi-user.target" ];
+      before = [ "phpfpm-moodle.service" ];
+      after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
+      environment.MOODLE_CONFIG = moodleConfig;
+      script = ''
+        ${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/check_database_schema.php && rc=$? || rc=$?
+
+        [ "$rc" == 1 ] && ${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/upgrade.php \
+          --non-interactive \
+          --allow-unstable
+
+        [ "$rc" == 2 ] && ${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/install_database.php \
+          --agree-license \
+          --adminpass=${cfg.initialPassword}
+
+        true
+      '';
+      serviceConfig = {
+        User = user;
+        Group = group;
+        Type = "oneshot";
+      };
+    };
+
+    systemd.services.moodle-cron = {
+      description = "Moodle cron service";
+      after = [ "moodle-init.service" ];
+      environment.MOODLE_CONFIG = moodleConfig;
+      serviceConfig = {
+        User = user;
+        Group = group;
+        ExecStart = "${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/cron.php";
+      };
+    };
+
+    systemd.timers.moodle-cron = {
+      description = "Moodle cron timer";
+      wantedBy = [ "timers.target" ];
+      timerConfig = {
+        OnCalendar = "minutely";
+      };
+    };
+
+    systemd.services.httpd.after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
+
+    users.users.${user} = {
+      group = group;
+      isSystemUser = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
new file mode 100644
index 00000000000..b32220a5e57
--- /dev/null
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -0,0 +1,933 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nextcloud;
+  fpm = config.services.phpfpm.pools.nextcloud;
+
+  inherit (cfg) datadir;
+
+  phpPackage = cfg.phpPackage.buildEnv {
+    extensions = { enabled, all }:
+      (with all;
+        enabled
+        ++ optional cfg.enableImagemagick imagick
+        # Optionally enabled depending on caching settings
+        ++ optional cfg.caching.apcu apcu
+        ++ optional cfg.caching.redis redis
+        ++ optional cfg.caching.memcached memcached
+      )
+      ++ cfg.phpExtraExtensions all; # Enabled by user
+    extraConfig = toKeyValue phpOptions;
+  };
+
+  toKeyValue = generators.toKeyValue {
+    mkKeyValue = generators.mkKeyValueDefault {} " = ";
+  };
+
+  phpOptions = {
+    upload_max_filesize = cfg.maxUploadSize;
+    post_max_size = cfg.maxUploadSize;
+    memory_limit = cfg.maxUploadSize;
+  } // cfg.phpOptions
+    // optionalAttrs cfg.caching.apcu {
+      "apc.enable_cli" = "1";
+    };
+
+  occ = pkgs.writeScriptBin "nextcloud-occ" ''
+    #! ${pkgs.runtimeShell}
+    cd ${cfg.package}
+    sudo=exec
+    if [[ "$USER" != nextcloud ]]; then
+      sudo='exec /run/wrappers/bin/sudo -u nextcloud --preserve-env=NEXTCLOUD_CONFIG_DIR --preserve-env=OC_PASS'
+    fi
+    export NEXTCLOUD_CONFIG_DIR="${datadir}/config"
+    $sudo \
+      ${phpPackage}/bin/php \
+      occ "$@"
+  '';
+
+  inherit (config.system) stateVersion;
+
+in {
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "nextcloud" "config" "adminpass" ] ''
+      Please use `services.nextcloud.config.adminpassFile' instead!
+    '')
+    (mkRemovedOptionModule [ "services" "nextcloud" "config" "dbpass" ] ''
+      Please use `services.nextcloud.config.dbpassFile' instead!
+    '')
+    (mkRemovedOptionModule [ "services" "nextcloud" "nginx" "enable" ] ''
+      The nextcloud module supports `nginx` as reverse-proxy by default and doesn't
+      support other reverse-proxies officially.
+
+      However it's possible to use an alternative reverse-proxy by
+
+        * disabling nginx
+        * setting `listen.owner` & `listen.group` in the phpfpm-pool to a different value
+
+      Further details about this can be found in the `Nextcloud`-section of the NixOS-manual
+      (which can be openend e.g. by running `nixos-help`).
+    '')
+    (mkRemovedOptionModule [ "services" "nextcloud" "disableImagemagick" ] ''
+      Use services.nextcloud.nginx.enableImagemagick instead.
+    '')
+  ];
+
+  options.services.nextcloud = {
+    enable = mkEnableOption "nextcloud";
+    hostName = mkOption {
+      type = types.str;
+      description = "FQDN for the nextcloud instance.";
+    };
+    home = mkOption {
+      type = types.str;
+      default = "/var/lib/nextcloud";
+      description = "Storage path of nextcloud.";
+    };
+    datadir = mkOption {
+      type = types.str;
+      defaultText = "config.services.nextcloud.home";
+      description = ''
+        Data storage path of nextcloud.  Will be <xref linkend="opt-services.nextcloud.home" /> by default.
+        This folder will be populated with a config.php and data folder which contains the state of the instance (excl the database).";
+      '';
+      example = "/mnt/nextcloud-file";
+    };
+    extraApps = mkOption {
+      type = types.attrsOf types.package;
+      default = { };
+      description = ''
+        Extra apps to install. Should be an attrSet of appid to packages generated by fetchNextcloudApp.
+        The appid must be identical to the "id" value in the apps appinfo/info.xml.
+        Using this will disable the appstore to prevent Nextcloud from updating these apps (see <xref linkend="opt-services.nextcloud.appstoreEnable" />).
+      '';
+      example = literalExpression ''
+        {
+          maps = pkgs.fetchNextcloudApp {
+            name = "maps";
+            sha256 = "007y80idqg6b6zk6kjxg4vgw0z8fsxs9lajnv49vv1zjy6jx2i1i";
+            url = "https://github.com/nextcloud/maps/releases/download/v0.1.9/maps-0.1.9.tar.gz";
+            version = "0.1.9";
+          };
+          phonetrack = pkgs.fetchNextcloudApp {
+            name = "phonetrack";
+            sha256 = "0qf366vbahyl27p9mshfma1as4nvql6w75zy2zk5xwwbp343vsbc";
+            url = "https://gitlab.com/eneiluj/phonetrack-oc/-/wikis/uploads/931aaaf8dca24bf31a7e169a83c17235/phonetrack-0.6.9.tar.gz";
+            version = "0.6.9";
+          };
+        }
+        '';
+    };
+    extraAppsEnable = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Automatically enable the apps in <xref linkend="opt-services.nextcloud.extraApps" /> every time nextcloud starts.
+        If set to false, apps need to be enabled in the Nextcloud user interface or with nextcloud-occ app:enable.
+      '';
+    };
+    appstoreEnable = mkOption {
+      type = types.nullOr types.bool;
+      default = null;
+      example = true;
+      description = ''
+        Allow the installation of apps and app updates from the store.
+        Enabled by default unless there are packages in <xref linkend="opt-services.nextcloud.extraApps" />.
+        Set to true to force enable the store even if <xref linkend="opt-services.nextcloud.extraApps" /> is used.
+        Set to false to disable the installation of apps from the global appstore. App management is always enabled regardless of this setting.
+      '';
+    };
+    logLevel = mkOption {
+      type = types.ints.between 0 4;
+      default = 2;
+      description = "Log level value between 0 (DEBUG) and 4 (FATAL).";
+    };
+    https = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Use https for generated links.";
+    };
+    package = mkOption {
+      type = types.package;
+      description = "Which package to use for the Nextcloud instance.";
+      relatedPackages = [ "nextcloud22" "nextcloud23" ];
+    };
+    phpPackage = mkOption {
+      type = types.package;
+      relatedPackages = [ "php74" "php80" ];
+      defaultText = "pkgs.php";
+      description = ''
+        PHP package to use for Nextcloud.
+      '';
+    };
+
+    maxUploadSize = mkOption {
+      default = "512M";
+      type = types.str;
+      description = ''
+        Defines the upload limit for files. This changes the relevant options
+        in php.ini and nginx if enabled.
+      '';
+    };
+
+    skeletonDirectory = mkOption {
+      default = "";
+      type = types.str;
+      description = ''
+        The directory where the skeleton files are located. These files will be
+        copied to the data directory of new users. Leave empty to not copy any
+        skeleton files.
+      '';
+    };
+
+    webfinger = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable this option if you plan on using the webfinger plugin.
+        The appropriate nginx rewrite rules will be added to your configuration.
+      '';
+    };
+
+    phpExtraExtensions = mkOption {
+      type = with types; functionTo (listOf package);
+      default = all: [];
+      defaultText = literalExpression "all: []";
+      description = ''
+        Additional PHP extensions to use for nextcloud.
+        By default, only extensions necessary for a vanilla nextcloud installation are enabled,
+        but you may choose from the list of available extensions and add further ones.
+        This is sometimes necessary to be able to install a certain nextcloud app that has additional requirements.
+      '';
+      example = literalExpression ''
+        all: [ all.pdlib all.bz2 ]
+      '';
+    };
+
+    phpOptions = mkOption {
+      type = types.attrsOf types.str;
+      default = {
+        short_open_tag = "Off";
+        expose_php = "Off";
+        error_reporting = "E_ALL & ~E_DEPRECATED & ~E_STRICT";
+        display_errors = "stderr";
+        "opcache.enable_cli" = "1";
+        "opcache.interned_strings_buffer" = "8";
+        "opcache.max_accelerated_files" = "10000";
+        "opcache.memory_consumption" = "128";
+        "opcache.revalidate_freq" = "1";
+        "opcache.fast_shutdown" = "1";
+        "openssl.cafile" = "/etc/ssl/certs/ca-certificates.crt";
+        catch_workers_output = "yes";
+      };
+      description = ''
+        Options for PHP's php.ini file for nextcloud.
+      '';
+    };
+
+    poolSettings = mkOption {
+      type = with types; attrsOf (oneOf [ str int bool ]);
+      default = {
+        "pm" = "dynamic";
+        "pm.max_children" = "32";
+        "pm.start_servers" = "2";
+        "pm.min_spare_servers" = "2";
+        "pm.max_spare_servers" = "4";
+        "pm.max_requests" = "500";
+      };
+      description = ''
+        Options for nextcloud's PHP pool. See the documentation on <literal>php-fpm.conf</literal> for details on configuration directives.
+      '';
+    };
+
+    poolConfig = mkOption {
+      type = types.nullOr types.lines;
+      default = null;
+      description = ''
+        Options for nextcloud's PHP pool. See the documentation on <literal>php-fpm.conf</literal> for details on configuration directives.
+      '';
+    };
+
+    config = {
+      dbtype = mkOption {
+        type = types.enum [ "sqlite" "pgsql" "mysql" ];
+        default = "sqlite";
+        description = "Database type.";
+      };
+      dbname = mkOption {
+        type = types.nullOr types.str;
+        default = "nextcloud";
+        description = "Database name.";
+      };
+      dbuser = mkOption {
+        type = types.nullOr types.str;
+        default = "nextcloud";
+        description = "Database user.";
+      };
+      dbpassFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The full path to a file that contains the database password.
+        '';
+      };
+      dbhost = mkOption {
+        type = types.nullOr types.str;
+        default = "localhost";
+        description = ''
+          Database host.
+
+          Note: for using Unix authentication with PostgreSQL, this should be
+          set to <literal>/run/postgresql</literal>.
+        '';
+      };
+      dbport = mkOption {
+        type = with types; nullOr (either int str);
+        default = null;
+        description = "Database port.";
+      };
+      dbtableprefix = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Table prefix in Nextcloud database.";
+      };
+      adminuser = mkOption {
+        type = types.str;
+        default = "root";
+        description = "Admin username.";
+      };
+      adminpassFile = mkOption {
+        type = types.str;
+        description = ''
+          The full path to a file that contains the admin's password. Must be
+          readable by user <literal>nextcloud</literal>.
+        '';
+      };
+
+      extraTrustedDomains = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Trusted domains, from which the nextcloud installation will be
+          acessible.  You don't need to add
+          <literal>services.nextcloud.hostname</literal> here.
+        '';
+      };
+
+      trustedProxies = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Trusted proxies, to provide if the nextcloud installation is being
+          proxied to secure against e.g. spoofing.
+        '';
+      };
+
+      overwriteProtocol = mkOption {
+        type = types.nullOr (types.enum [ "http" "https" ]);
+        default = null;
+        example = "https";
+
+        description = ''
+          Force Nextcloud to always use HTTPS i.e. for link generation. Nextcloud
+          uses the currently used protocol by default, but when behind a reverse-proxy,
+          it may use <literal>http</literal> for everything although Nextcloud
+          may be served via HTTPS.
+        '';
+      };
+
+      defaultPhoneRegion = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        example = "DE";
+        description = ''
+          <warning>
+           <para>This option exists since Nextcloud 21! If older versions are used,
+            this will throw an eval-error!</para>
+          </warning>
+
+          <link xlink:href="https://www.iso.org/iso-3166-country-codes.html">ISO 3611-1</link>
+          country codes for automatic phone-number detection without a country code.
+
+          With e.g. <literal>DE</literal> set, the <literal>+49</literal> can be omitted for
+          phone-numbers.
+        '';
+      };
+
+      objectstore = {
+        s3 = {
+          enable = mkEnableOption ''
+            S3 object storage as primary storage.
+
+            This mounts a bucket on an Amazon S3 object storage or compatible
+            implementation into the virtual filesystem.
+
+            Further details about this feature can be found in the
+            <link xlink:href="https://docs.nextcloud.com/server/22/admin_manual/configuration_files/primary_storage.html">upstream documentation</link>.
+          '';
+          bucket = mkOption {
+            type = types.str;
+            example = "nextcloud";
+            description = ''
+              The name of the S3 bucket.
+            '';
+          };
+          autocreate = mkOption {
+            type = types.bool;
+            description = ''
+              Create the objectstore if it does not exist.
+            '';
+          };
+          key = mkOption {
+            type = types.str;
+            example = "EJ39ITYZEUH5BGWDRUFY";
+            description = ''
+              The access key for the S3 bucket.
+            '';
+          };
+          secretFile = mkOption {
+            type = types.str;
+            example = "/var/nextcloud-objectstore-s3-secret";
+            description = ''
+              The full path to a file that contains the access secret. Must be
+              readable by user <literal>nextcloud</literal>.
+            '';
+          };
+          hostname = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "example.com";
+            description = ''
+              Required for some non-Amazon implementations.
+            '';
+          };
+          port = mkOption {
+            type = types.nullOr types.port;
+            default = null;
+            description = ''
+              Required for some non-Amazon implementations.
+            '';
+          };
+          useSsl = mkOption {
+            type = types.bool;
+            default = true;
+            description = ''
+              Use SSL for objectstore access.
+            '';
+          };
+          region = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "REGION";
+            description = ''
+              Required for some non-Amazon implementations.
+            '';
+          };
+          usePathStyle = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Required for some non-Amazon S3 implementations.
+
+              Ordinarily, requests will be made with
+              <literal>http://bucket.hostname.domain/</literal>, but with path style
+              enabled requests are made with
+              <literal>http://hostname.domain/bucket</literal> instead.
+            '';
+          };
+        };
+      };
+    };
+
+    enableImagemagick = mkEnableOption ''
+        the ImageMagick module for PHP.
+        This is used by the theming app and for generating previews of certain images (e.g. SVG and HEIF).
+        You may want to disable it for increased security. In that case, previews will still be available
+        for some images (e.g. JPEG and PNG).
+        See <link xlink:href="https://github.com/nextcloud/server/issues/13099" />.
+    '' // {
+      default = true;
+    };
+
+    caching = {
+      apcu = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to load the APCu module into PHP.
+        '';
+      };
+      redis = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to load the Redis module into PHP.
+          You still need to enable Redis in your config.php.
+          See https://docs.nextcloud.com/server/14/admin_manual/configuration_server/caching_configuration.html
+        '';
+      };
+      memcached = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to load the Memcached module into PHP.
+          You still need to enable Memcached in your config.php.
+          See https://docs.nextcloud.com/server/14/admin_manual/configuration_server/caching_configuration.html
+        '';
+      };
+    };
+    autoUpdateApps = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Run regular auto update of all apps installed from the nextcloud app store.
+        '';
+      };
+      startAt = mkOption {
+        type = with types; either str (listOf str);
+        default = "05:00:00";
+        example = "Sun 14:00:00";
+        description = ''
+          When to run the update. See `systemd.services.&lt;name&gt;.startAt`.
+        '';
+      };
+    };
+    occ = mkOption {
+      type = types.package;
+      default = occ;
+      defaultText = literalDocBook "generated script";
+      internal = true;
+      description = ''
+        The nextcloud-occ program preconfigured to target this Nextcloud instance.
+      '';
+    };
+
+    nginx.recommendedHttpHeaders = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Enable additional recommended HTTP response headers";
+    };
+  };
+
+  config = mkIf cfg.enable (mkMerge [
+    { warnings = let
+        latest = 23;
+        upgradeWarning = major: nixos:
+          ''
+            A legacy Nextcloud install (from before NixOS ${nixos}) may be installed.
+
+            After nextcloud${toString major} is installed successfully, you can safely upgrade
+            to ${toString (major + 1)}. The latest version available is nextcloud${toString latest}.
+
+            Please note that Nextcloud doesn't support upgrades across multiple major versions
+            (i.e. an upgrade from 16 is possible to 17, but not 16 to 18).
+
+            The package can be upgraded by explicitly declaring the service-option
+            `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.
+        '')
+        ++ (optional (versionOlder cfg.package.version "21") (upgradeWarning 20 "21.05"))
+        ++ (optional (versionOlder cfg.package.version "22") (upgradeWarning 21 "21.11"))
+        ++ (optional (versionOlder cfg.package.version "23") (upgradeWarning 22 "22.05"))
+        ++ (optional 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
+          '');
+
+      services.nextcloud.package = with pkgs;
+        mkDefault (
+          if pkgs ? nextcloud
+            then throw ''
+              The `pkgs.nextcloud`-attribute has been removed. If it's supposed to be the default
+              nextcloud defined in an overlay, please set `services.nextcloud.package` to
+              `pkgs.nextcloud`.
+            ''
+          else if versionOlder stateVersion "21.11" then nextcloud21
+          else if versionOlder stateVersion "22.05" then nextcloud22
+          else nextcloud23
+        );
+
+      services.nextcloud.datadir = mkOptionDefault config.services.nextcloud.home;
+
+      services.nextcloud.phpPackage =
+        if versionOlder cfg.package.version "21" then pkgs.php74
+        else pkgs.php80;
+    }
+
+    { systemd.timers.nextcloud-cron = {
+        wantedBy = [ "timers.target" ];
+        timerConfig.OnBootSec = "5m";
+        timerConfig.OnUnitActiveSec = "5m";
+        timerConfig.Unit = "nextcloud-cron.service";
+      };
+
+      systemd.tmpfiles.rules = ["d ${cfg.home} 0750 nextcloud nextcloud"];
+
+      systemd.services = {
+        # When upgrading the Nextcloud package, Nextcloud can report errors such as
+        # "The files of the app [all apps in /var/lib/nextcloud/apps] were not replaced correctly"
+        # Restarting phpfpm on Nextcloud package update fixes these issues (but this is a workaround).
+        phpfpm-nextcloud.restartTriggers = [ cfg.package ];
+
+        nextcloud-setup = let
+          c = cfg.config;
+          writePhpArrary = a: "[${concatMapStringsSep "," (val: ''"${toString val}"'') a}]";
+          requiresReadSecretFunction = c.dbpassFile != null || c.objectstore.s3.enable;
+          objectstoreConfig = let s3 = c.objectstore.s3; in optionalString s3.enable ''
+            'objectstore' => [
+              'class' => '\\OC\\Files\\ObjectStore\\S3',
+              'arguments' => [
+                'bucket' => '${s3.bucket}',
+                'autocreate' => ${boolToString s3.autocreate},
+                'key' => '${s3.key}',
+                'secret' => nix_read_secret('${s3.secretFile}'),
+                ${optionalString (s3.hostname != null) "'hostname' => '${s3.hostname}',"}
+                ${optionalString (s3.port != null) "'port' => ${toString s3.port},"}
+                'use_ssl' => ${boolToString s3.useSsl},
+                ${optionalString (s3.region != null) "'region' => '${s3.region}',"}
+                'use_path_style' => ${boolToString s3.usePathStyle},
+              ],
+            ]
+          '';
+
+          showAppStoreSetting = cfg.appstoreEnable != null || cfg.extraApps != {};
+          renderedAppStoreSetting =
+            let
+              x = cfg.appstoreEnable;
+            in
+              if x == null then "false"
+              else boolToString x;
+
+          overrideConfig = pkgs.writeText "nextcloud-config.php" ''
+            <?php
+            ${optionalString requiresReadSecretFunction ''
+              function nix_read_secret($file) {
+                if (!file_exists($file)) {
+                  throw new \RuntimeException(sprintf(
+                    "Cannot start Nextcloud, secret file %s set by NixOS doesn't seem to "
+                    . "exist! Please make sure that the file exists and has appropriate "
+                    . "permissions for user & group 'nextcloud'!",
+                    $file
+                  ));
+                }
+
+                return trim(file_get_contents($file));
+              }
+            ''}
+            $CONFIG = [
+              'apps_paths' => [
+                ${optionalString (cfg.extraApps != { }) "[ 'path' => '${cfg.home}/nix-apps', 'url' => '/nix-apps', 'writable' => false ],"}
+                [ 'path' => '${cfg.home}/apps', 'url' => '/apps', 'writable' => false ],
+                [ 'path' => '${cfg.home}/store-apps', 'url' => '/store-apps', 'writable' => true ],
+              ],
+              ${optionalString (showAppStoreSetting) "'appstoreenabled' => ${renderedAppStoreSetting},"}
+              'datadirectory' => '${datadir}/data',
+              'skeletondirectory' => '${cfg.skeletonDirectory}',
+              ${optionalString cfg.caching.apcu "'memcache.local' => '\\OC\\Memcache\\APCu',"}
+              'log_type' => 'syslog',
+              'log_level' => '${builtins.toString cfg.logLevel}',
+              ${optionalString (c.overwriteProtocol != null) "'overwriteprotocol' => '${c.overwriteProtocol}',"}
+              ${optionalString (c.dbname != null) "'dbname' => '${c.dbname}',"}
+              ${optionalString (c.dbhost != null) "'dbhost' => '${c.dbhost}',"}
+              ${optionalString (c.dbport != null) "'dbport' => '${toString c.dbport}',"}
+              ${optionalString (c.dbuser != null) "'dbuser' => '${c.dbuser}',"}
+              ${optionalString (c.dbtableprefix != null) "'dbtableprefix' => '${toString c.dbtableprefix}',"}
+              ${optionalString (c.dbpassFile != null) "'dbpassword' => nix_read_secret('${c.dbpassFile}'),"}
+              'dbtype' => '${c.dbtype}',
+              'trusted_domains' => ${writePhpArrary ([ cfg.hostName ] ++ c.extraTrustedDomains)},
+              'trusted_proxies' => ${writePhpArrary (c.trustedProxies)},
+              ${optionalString (c.defaultPhoneRegion != null) "'default_phone_region' => '${c.defaultPhoneRegion}',"}
+              ${objectstoreConfig}
+            ];
+          '';
+          occInstallCmd = let
+            mkExport = { arg, value }: "export ${arg}=${value}";
+            dbpass = {
+              arg = "DBPASS";
+              value = if c.dbpassFile != null
+                then ''"$(<"${toString c.dbpassFile}")"''
+                else ''""'';
+            };
+            adminpass = {
+              arg = "ADMINPASS";
+              value = ''"$(<"${toString c.adminpassFile}")"'';
+            };
+            installFlags = concatStringsSep " \\\n    "
+              (mapAttrsToList (k: v: "${k} ${toString v}") {
+              "--database" = ''"${c.dbtype}"'';
+              # The following attributes are optional depending on the type of
+              # database.  Those that evaluate to null on the left hand side
+              # will be omitted.
+              ${if c.dbname != null then "--database-name" else null} = ''"${c.dbname}"'';
+              ${if c.dbhost != null then "--database-host" else null} = ''"${c.dbhost}"'';
+              ${if c.dbport != null then "--database-port" else null} = ''"${toString c.dbport}"'';
+              ${if c.dbuser != null then "--database-user" else null} = ''"${c.dbuser}"'';
+              "--database-pass" = "\$${dbpass.arg}";
+              "--admin-user" = ''"${c.adminuser}"'';
+              "--admin-pass" = "\$${adminpass.arg}";
+              "--data-dir" = ''"${datadir}/data"'';
+            });
+          in ''
+            ${mkExport dbpass}
+            ${mkExport adminpass}
+            ${occ}/bin/nextcloud-occ maintenance:install \
+                ${installFlags}
+          '';
+          occSetTrustedDomainsCmd = concatStringsSep "\n" (imap0
+            (i: v: ''
+              ${occ}/bin/nextcloud-occ config:system:set trusted_domains \
+                ${toString i} --value="${toString v}"
+            '') ([ cfg.hostName ] ++ cfg.config.extraTrustedDomains));
+
+        in {
+          wantedBy = [ "multi-user.target" ];
+          before = [ "phpfpm-nextcloud.service" ];
+          path = [ occ ];
+          script = ''
+            ${optionalString (c.dbpassFile != null) ''
+              if [ ! -r "${c.dbpassFile}" ]; then
+                echo "dbpassFile ${c.dbpassFile} is not readable by nextcloud:nextcloud! Aborting..."
+                exit 1
+              fi
+              if [ -z "$(<${c.dbpassFile})" ]; then
+                echo "dbpassFile ${c.dbpassFile} is empty!"
+                exit 1
+              fi
+            ''}
+            if [ ! -r "${c.adminpassFile}" ]; then
+              echo "adminpassFile ${c.adminpassFile} is not readable by nextcloud:nextcloud! Aborting..."
+              exit 1
+            fi
+            if [ -z "$(<${c.adminpassFile})" ]; then
+              echo "adminpassFile ${c.adminpassFile} is empty!"
+              exit 1
+            fi
+
+            ln -sf ${cfg.package}/apps ${cfg.home}/
+
+            # Install extra apps
+            ln -sfT \
+              ${pkgs.linkFarm "nix-apps"
+                (mapAttrsToList (name: path: { inherit name path; }) cfg.extraApps)} \
+              ${cfg.home}/nix-apps
+
+            # create nextcloud directories.
+            # if the directories exist already with wrong permissions, we fix that
+            for dir in ${datadir}/config ${datadir}/data ${cfg.home}/store-apps ${cfg.home}/nix-apps; do
+              if [ ! -e $dir ]; then
+                install -o nextcloud -g nextcloud -d $dir
+              elif [ $(stat -c "%G" $dir) != "nextcloud" ]; then
+                chgrp -R nextcloud $dir
+              fi
+            done
+
+            ln -sf ${overrideConfig} ${datadir}/config/override.config.php
+
+            # Do not install if already installed
+            if [[ ! -e ${datadir}/config/config.php ]]; then
+              ${occInstallCmd}
+            fi
+
+            ${occ}/bin/nextcloud-occ upgrade
+
+            ${occ}/bin/nextcloud-occ config:system:delete trusted_domains
+
+            ${optionalString (cfg.extraAppsEnable && cfg.extraApps != { }) ''
+                # Try to enable apps (don't fail when one of them cannot be enabled , eg. due to incompatible version)
+                ${occ}/bin/nextcloud-occ app:enable ${concatStringsSep " " (attrNames cfg.extraApps)}
+            ''}
+
+            ${occSetTrustedDomainsCmd}
+          '';
+          serviceConfig.Type = "oneshot";
+          serviceConfig.User = "nextcloud";
+        };
+        nextcloud-cron = {
+          environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config";
+          serviceConfig.Type = "oneshot";
+          serviceConfig.User = "nextcloud";
+          serviceConfig.ExecStart = "${phpPackage}/bin/php -f ${cfg.package}/cron.php";
+        };
+        nextcloud-update-plugins = mkIf cfg.autoUpdateApps.enable {
+          serviceConfig.Type = "oneshot";
+          serviceConfig.ExecStart = "${occ}/bin/nextcloud-occ app:update --all";
+          serviceConfig.User = "nextcloud";
+          startAt = cfg.autoUpdateApps.startAt;
+        };
+      };
+
+      services.phpfpm = {
+        pools.nextcloud = {
+          user = "nextcloud";
+          group = "nextcloud";
+          phpPackage = phpPackage;
+          phpEnv = {
+            NEXTCLOUD_CONFIG_DIR = "${datadir}/config";
+            PATH = "/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin";
+          };
+          settings = mapAttrs (name: mkDefault) {
+            "listen.owner" = config.services.nginx.user;
+            "listen.group" = config.services.nginx.group;
+          } // cfg.poolSettings;
+          extraConfig = cfg.poolConfig;
+        };
+      };
+
+      users.users.nextcloud = {
+        home = "${cfg.home}";
+        group = "nextcloud";
+        isSystemUser = true;
+      };
+      users.groups.nextcloud.members = [ "nextcloud" config.services.nginx.user ];
+
+      environment.systemPackages = [ occ ];
+
+      services.nginx.enable = mkDefault true;
+
+      services.nginx.virtualHosts.${cfg.hostName} = {
+        root = cfg.package;
+        locations = {
+          "= /robots.txt" = {
+            priority = 100;
+            extraConfig = ''
+              allow all;
+              log_not_found off;
+              access_log off;
+            '';
+          };
+          "= /" = {
+            priority = 100;
+            extraConfig = ''
+              if ( $http_user_agent ~ ^DavClnt ) {
+                return 302 /remote.php/webdav/$is_args$args;
+              }
+            '';
+          };
+          "/" = {
+            priority = 900;
+            extraConfig = "rewrite ^ /index.php;";
+          };
+          "~ ^/store-apps" = {
+            priority = 201;
+            extraConfig = "root ${cfg.home};";
+          };
+          "~ ^/nix-apps" = {
+            priority = 201;
+            extraConfig = "root ${cfg.home};";
+          };
+          "^~ /.well-known" = {
+            priority = 210;
+            extraConfig = ''
+              absolute_redirect off;
+              location = /.well-known/carddav {
+                return 301 /remote.php/dav;
+              }
+              location = /.well-known/caldav {
+                return 301 /remote.php/dav;
+              }
+              location ~ ^/\.well-known/(?!acme-challenge|pki-validation) {
+                return 301 /index.php$request_uri;
+              }
+              try_files $uri $uri/ =404;
+            '';
+          };
+          "~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)".extraConfig = ''
+            return 404;
+          '';
+          "~ ^/(?:\\.(?!well-known)|autotest|occ|issue|indie|db_|console)".extraConfig = ''
+            return 404;
+          '';
+          "~ ^\\/(?:index|remote|public|cron|core\\/ajax\\/update|status|ocs\\/v[12]|updater\\/.+|oc[ms]-provider\\/.+|.+\\/richdocumentscode\\/proxy)\\.php(?:$|\\/)" = {
+            priority = 500;
+            extraConfig = ''
+              include ${config.services.nginx.package}/conf/fastcgi.conf;
+              fastcgi_split_path_info ^(.+?\.php)(\\/.*)$;
+              set $path_info $fastcgi_path_info;
+              try_files $fastcgi_script_name =404;
+              fastcgi_param PATH_INFO $path_info;
+              fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+              fastcgi_param HTTPS ${if cfg.https then "on" else "off"};
+              fastcgi_param modHeadersAvailable true;
+              fastcgi_param front_controller_active true;
+              fastcgi_pass unix:${fpm.socket};
+              fastcgi_intercept_errors on;
+              fastcgi_request_buffering off;
+              fastcgi_read_timeout 120s;
+            '';
+          };
+          "~ \\.(?:css|js|woff2?|svg|gif|map)$".extraConfig = ''
+            try_files $uri /index.php$request_uri;
+            expires 6M;
+            access_log off;
+          '';
+          "~ ^\\/(?:updater|ocs-provider|ocm-provider)(?:$|\\/)".extraConfig = ''
+            try_files $uri/ =404;
+            index index.php;
+          '';
+          "~ \\.(?:png|html|ttf|ico|jpg|jpeg|bcmap|mp4|webm)$".extraConfig = ''
+            try_files $uri /index.php$request_uri;
+            access_log off;
+          '';
+        };
+        extraConfig = ''
+          index index.php index.html /index.php$request_uri;
+          ${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-Download-Options noopen;
+            add_header X-Permitted-Cross-Domain-Policies none;
+            add_header X-Frame-Options sameorigin;
+            add_header Referrer-Policy no-referrer;
+            add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
+          ''}
+          client_max_body_size ${cfg.maxUploadSize};
+          fastcgi_buffers 64 4K;
+          fastcgi_hide_header X-Powered-By;
+          gzip on;
+          gzip_vary on;
+          gzip_comp_level 4;
+          gzip_min_length 256;
+          gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
+          gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
+
+          ${optionalString cfg.webfinger ''
+            rewrite ^/.well-known/host-meta /public.php?service=host-meta last;
+            rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last;
+          ''}
+        '';
+      };
+    }
+  ]);
+
+  meta.doc = ./nextcloud.xml;
+}
diff --git a/nixos/modules/services/web-apps/nextcloud.xml b/nixos/modules/services/web-apps/nextcloud.xml
new file mode 100644
index 00000000000..8f55086a2bd
--- /dev/null
+++ b/nixos/modules/services/web-apps/nextcloud.xml
@@ -0,0 +1,291 @@
+<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>nextcloud23</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>
+  </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 shold 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/nexus.nix b/nixos/modules/services/web-apps/nexus.nix
new file mode 100644
index 00000000000..dc50a06705f
--- /dev/null
+++ b/nixos/modules/services/web-apps/nexus.nix
@@ -0,0 +1,156 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.nexus;
+
+in
+
+{
+  options = {
+    services.nexus = {
+      enable = mkEnableOption "Sonatype Nexus3 OSS service";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.nexus;
+        defaultText = literalExpression "pkgs.nexus";
+        description = "Package which runs Nexus3";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "nexus";
+        description = "User which runs Nexus3.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "nexus";
+        description = "Group which runs Nexus3.";
+      };
+
+      home = mkOption {
+        type = types.str;
+        default = "/var/lib/sonatype-work";
+        description = "Home directory of the Nexus3 instance.";
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "Address to listen on.";
+      };
+
+      listenPort = mkOption {
+        type = types.int;
+        default = 8081;
+        description = "Port to listen on.";
+      };
+
+      jvmOpts = mkOption {
+        type = types.lines;
+        default = ''
+          -Xms1200M
+          -Xmx1200M
+          -XX:MaxDirectMemorySize=2G
+          -XX:+UnlockDiagnosticVMOptions
+          -XX:+UnsyncloadClass
+          -XX:+LogVMOutput
+          -XX:LogFile=${cfg.home}/nexus3/log/jvm.log
+          -XX:-OmitStackTraceInFastThrow
+          -Djava.net.preferIPv4Stack=true
+          -Dkaraf.home=${cfg.package}
+          -Dkaraf.base=${cfg.package}
+          -Dkaraf.etc=${cfg.package}/etc/karaf
+          -Djava.util.logging.config.file=${cfg.package}/etc/karaf/java.util.logging.properties
+          -Dkaraf.data=${cfg.home}/nexus3
+          -Djava.io.tmpdir=${cfg.home}/nexus3/tmp
+          -Dkaraf.startLocalConsole=false
+          -Djava.endorsed.dirs=${cfg.package}/lib/endorsed
+        '';
+        defaultText = literalExpression ''
+          '''
+            -Xms1200M
+            -Xmx1200M
+            -XX:MaxDirectMemorySize=2G
+            -XX:+UnlockDiagnosticVMOptions
+            -XX:+UnsyncloadClass
+            -XX:+LogVMOutput
+            -XX:LogFile=''${home}/nexus3/log/jvm.log
+            -XX:-OmitStackTraceInFastThrow
+            -Djava.net.preferIPv4Stack=true
+            -Dkaraf.home=''${package}
+            -Dkaraf.base=''${package}
+            -Dkaraf.etc=''${package}/etc/karaf
+            -Djava.util.logging.config.file=''${package}/etc/karaf/java.util.logging.properties
+            -Dkaraf.data=''${home}/nexus3
+            -Djava.io.tmpdir=''${home}/nexus3/tmp
+            -Dkaraf.startLocalConsole=false
+            -Djava.endorsed.dirs=''${package}/lib/endorsed
+          '''
+        '';
+
+        description = ''
+          Options for the JVM written to `nexus.jvmopts`.
+          Please refer to the docs (https://help.sonatype.com/repomanager3/installation/configuring-the-runtime-environment)
+          for further information.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.${cfg.user} = {
+      isSystemUser = true;
+      group = cfg.group;
+      home = cfg.home;
+      createHome = true;
+    };
+
+    users.groups.${cfg.group} = {};
+
+    systemd.services.nexus = {
+      description = "Sonatype Nexus3";
+
+      wantedBy = [ "multi-user.target" ];
+
+      path = [ cfg.home ];
+
+      environment = {
+        NEXUS_USER = cfg.user;
+        NEXUS_HOME = cfg.home;
+
+        VM_OPTS_FILE = pkgs.writeText "nexus.vmoptions" cfg.jvmOpts;
+      };
+
+      preStart = ''
+        mkdir -p ${cfg.home}/nexus3/etc
+
+        if [ ! -f ${cfg.home}/nexus3/etc/nexus.properties ]; then
+          echo "# Jetty section" > ${cfg.home}/nexus3/etc/nexus.properties
+          echo "application-port=${toString cfg.listenPort}" >> ${cfg.home}/nexus3/etc/nexus.properties
+          echo "application-host=${toString cfg.listenAddress}" >> ${cfg.home}/nexus3/etc/nexus.properties
+        else
+          sed 's/^application-port=.*/application-port=${toString cfg.listenPort}/' -i ${cfg.home}/nexus3/etc/nexus.properties
+          sed 's/^# application-port=.*/application-port=${toString cfg.listenPort}/' -i ${cfg.home}/nexus3/etc/nexus.properties
+          sed 's/^application-host=.*/application-host=${toString cfg.listenAddress}/' -i ${cfg.home}/nexus3/etc/nexus.properties
+          sed 's/^# application-host=.*/application-host=${toString cfg.listenAddress}/' -i ${cfg.home}/nexus3/etc/nexus.properties
+        fi
+      '';
+
+      script = "${cfg.package}/bin/nexus run";
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        PrivateTmp = true;
+        LimitNOFILE = 102642;
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ ironpinguin ];
+}
diff --git a/nixos/modules/services/web-apps/node-red.nix b/nixos/modules/services/web-apps/node-red.nix
new file mode 100644
index 00000000000..4512907f027
--- /dev/null
+++ b/nixos/modules/services/web-apps/node-red.nix
@@ -0,0 +1,149 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.node-red;
+  defaultUser = "node-red";
+  finalPackage = if cfg.withNpmAndGcc then node-red_withNpmAndGcc else cfg.package;
+  node-red_withNpmAndGcc = pkgs.runCommand "node-red" {
+    nativeBuildInputs = [ pkgs.makeWrapper ];
+  }
+  ''
+    mkdir -p $out/bin
+    makeWrapper ${pkgs.nodePackages.node-red}/bin/node-red $out/bin/node-red \
+      --set PATH '${lib.makeBinPath [ pkgs.nodePackages.npm pkgs.gcc ]}:$PATH' \
+  '';
+in
+{
+  options.services.node-red = {
+    enable = mkEnableOption "the Node-RED service";
+
+    package = mkOption {
+      default = pkgs.nodePackages.node-red;
+      defaultText = literalExpression "pkgs.nodePackages.node-red";
+      type = types.package;
+      description = "Node-RED package to use.";
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Open ports in the firewall for the server.
+      '';
+    };
+
+    withNpmAndGcc = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Give Node-RED access to NPM and GCC at runtime, so 'Nodes' can be
+        downloaded and managed imperatively via the 'Palette Manager'.
+      '';
+    };
+
+    configFile = mkOption {
+      type = types.path;
+      default = "${cfg.package}/lib/node_modules/node-red/settings.js";
+      defaultText = literalExpression ''"''${package}/lib/node_modules/node-red/settings.js"'';
+      description = ''
+        Path to the JavaScript configuration file.
+        See <link
+        xlink:href="https://github.com/node-red/node-red/blob/master/packages/node_modules/node-red/settings.js"/>
+        for a configuration example.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 1880;
+      description = "Listening port.";
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = ''
+        User under which Node-RED runs.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 = defaultUser;
+      description = ''
+        Group under which Node-RED runs.If left as the default value this group
+        will automatically be created on system activation, otherwise the
+        sysadmin is responsible for ensuring the group exists.
+      '';
+    };
+
+    userDir = mkOption {
+      type = types.path;
+      default = "/var/lib/node-red";
+      description = ''
+        The directory to store all user data, such as flow and credential files and all library data. If left
+        as the default value this directory will automatically be created before the node-red service starts,
+        otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership
+        and permissions.
+      '';
+    };
+
+    safe = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Whether to launch Node-RED in --safe mode.";
+    };
+
+    define = mkOption {
+      type = types.attrs;
+      default = {};
+      description = "List of settings.js overrides to pass via -D to Node-RED.";
+      example = literalExpression ''
+        {
+          "logging.console.level" = "trace";
+        }
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} = {
+        isSystemUser = true;
+        group = defaultUser;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == defaultUser) {
+      ${defaultUser} = { };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+
+    systemd.services.node-red = {
+      description = "Node-RED Service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+      environment = {
+        HOME = cfg.userDir;
+      };
+      serviceConfig = mkMerge [
+        {
+          User = cfg.user;
+          Group = cfg.group;
+          ExecStart = "${finalPackage}/bin/node-red ${pkgs.lib.optionalString cfg.safe "--safe"} --settings ${cfg.configFile} --port ${toString cfg.port} --userDir ${cfg.userDir} ${concatStringsSep " " (mapAttrsToList (name: value: "-D ${name}=${value}") cfg.define)}";
+          PrivateTmp = true;
+          Restart = "always";
+          WorkingDirectory = cfg.userDir;
+        }
+        (mkIf (cfg.userDir == "/var/lib/node-red") { StateDirectory = "node-red"; })
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/openwebrx.nix b/nixos/modules/services/web-apps/openwebrx.nix
new file mode 100644
index 00000000000..9e90c01e0bb
--- /dev/null
+++ b/nixos/modules/services/web-apps/openwebrx.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.openwebrx;
+in
+{
+  options.services.openwebrx = with lib; {
+    enable = mkEnableOption "OpenWebRX Web interface for Software-Defined Radios on http://localhost:8073";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.openwebrx;
+      defaultText = literalExpression "pkgs.openwebrx";
+      description = "OpenWebRX package to use for the service";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.openwebrx = {
+      wantedBy = [ "multi-user.target" ];
+      path = with pkgs; [
+        csdr
+        alsaUtils
+        netcat
+      ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/openwebrx";
+        Restart = "always";
+        DynamicUser = true;
+        # openwebrx uses /var/lib/openwebrx by default
+        StateDirectory = [ "openwebrx" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/peertube.nix b/nixos/modules/services/web-apps/peertube.nix
new file mode 100644
index 00000000000..e195e6e6e82
--- /dev/null
+++ b/nixos/modules/services/web-apps/peertube.nix
@@ -0,0 +1,475 @@
+{ lib, pkgs, config, options, ... }:
+
+let
+  cfg = config.services.peertube;
+  opt = options.services.peertube;
+
+  settingsFormat = pkgs.formats.json {};
+  configFile = settingsFormat.generate "production.json" cfg.settings;
+
+  env = {
+    NODE_CONFIG_DIR = "/var/lib/peertube/config";
+    NODE_ENV = "production";
+    NODE_EXTRA_CA_CERTS = "/etc/ssl/certs/ca-certificates.crt";
+    NPM_CONFIG_PREFIX = cfg.package;
+    HOME = cfg.package;
+  };
+
+  systemCallsList = [ "@cpu-emulation" "@debug" "@keyring" "@ipc" "@memlock" "@mount" "@obsolete" "@privileged" "@setuid" ];
+
+  cfgService = {
+    # Proc filesystem
+    ProcSubset = "pid";
+    ProtectProc = "invisible";
+    # Access write directories
+    UMask = "0027";
+    # Capabilities
+    CapabilityBoundingSet = "";
+    # Security
+    NoNewPrivileges = true;
+    # Sandboxing
+    ProtectSystem = "strict";
+    ProtectHome = true;
+    PrivateTmp = true;
+    PrivateDevices = true;
+    PrivateUsers = true;
+    ProtectClock = true;
+    ProtectHostname = true;
+    ProtectKernelLogs = true;
+    ProtectKernelModules = true;
+    ProtectKernelTunables = true;
+    ProtectControlGroups = true;
+    RestrictNamespaces = true;
+    LockPersonality = true;
+    RestrictRealtime = true;
+    RestrictSUIDSGID = true;
+    RemoveIPC = true;
+    PrivateMounts = true;
+    # System Call Filtering
+    SystemCallArchitectures = "native";
+  };
+
+  envFile = pkgs.writeText "peertube.env" (lib.concatMapStrings (s: s + "\n") (
+    (lib.concatLists (lib.mapAttrsToList (name: value:
+      if value != null then [
+        "${name}=\"${toString value}\""
+      ] else []
+    ) env))));
+
+  peertubeEnv = pkgs.writeShellScriptBin "peertube-env" ''
+    set -a
+    source "${envFile}"
+    eval -- "\$@"
+  '';
+
+  peertubeCli = pkgs.writeShellScriptBin "peertube" ''
+    node ~/dist/server/tools/peertube.js $@
+  '';
+
+in {
+  options.services.peertube = {
+    enable = lib.mkEnableOption "Enable Peertube’s service";
+
+    user = lib.mkOption {
+      type = lib.types.str;
+      default = "peertube";
+      description = "User account under which Peertube runs.";
+    };
+
+    group = lib.mkOption {
+      type = lib.types.str;
+      default = "peertube";
+      description = "Group under which Peertube runs.";
+    };
+
+    localDomain = lib.mkOption {
+      type = lib.types.str;
+      example = "peertube.example.com";
+      description = "The domain serving your PeerTube instance.";
+    };
+
+    listenHttp = lib.mkOption {
+      type = lib.types.int;
+      default = 9000;
+      description = "listen port for HTTP server.";
+    };
+
+    listenWeb = lib.mkOption {
+      type = lib.types.int;
+      default = 9000;
+      description = "listen port for WEB server.";
+    };
+
+    enableWebHttps = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = "Enable or disable HTTPS protocol.";
+    };
+
+    dataDirs = lib.mkOption {
+      type = lib.types.listOf lib.types.path;
+      default = [ ];
+      example = [ "/opt/peertube/storage" "/var/cache/peertube" ];
+      description = "Allow access to custom data locations.";
+    };
+
+    serviceEnvironmentFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      example = "/run/keys/peertube/password-init-root";
+      description = ''
+        Set environment variables for the service. Mainly useful for setting the initial root password.
+        For example write to file:
+        PT_INITIAL_ROOT_PASSWORD=changeme
+      '';
+    };
+
+    settings = lib.mkOption {
+      type = settingsFormat.type;
+      example = lib.literalExpression ''
+        {
+          listen = {
+            hostname = "0.0.0.0";
+          };
+          log = {
+            level = "debug";
+          };
+          storage = {
+            tmp = "/opt/data/peertube/storage/tmp/";
+            logs = "/opt/data/peertube/storage/logs/";
+            cache = "/opt/data/peertube/storage/cache/";
+          };
+        }
+      '';
+      description = "Configuration for peertube.";
+    };
+
+    database = {
+      createLocally = lib.mkOption {
+        type = lib.types.bool;
+        default = false;
+        description = "Configure local PostgreSQL database server for PeerTube.";
+      };
+
+      host = lib.mkOption {
+        type = lib.types.str;
+        default = if cfg.database.createLocally then "/run/postgresql" else null;
+        defaultText = lib.literalExpression ''
+          if config.${opt.database.createLocally}
+          then "/run/postgresql"
+          else null
+        '';
+        example = "192.168.15.47";
+        description = "Database host address or unix socket.";
+      };
+
+      port = lib.mkOption {
+        type = lib.types.int;
+        default = 5432;
+        description = "Database host port.";
+      };
+
+      name = lib.mkOption {
+        type = lib.types.str;
+        default = "peertube";
+        description = "Database name.";
+      };
+
+      user = lib.mkOption {
+        type = lib.types.str;
+        default = "peertube";
+        description = "Database user.";
+      };
+
+      passwordFile = lib.mkOption {
+        type = lib.types.nullOr lib.types.path;
+        default = null;
+        example = "/run/keys/peertube/password-posgressql-db";
+        description = "Password for PostgreSQL database.";
+      };
+    };
+
+    redis = {
+      createLocally = lib.mkOption {
+        type = lib.types.bool;
+        default = false;
+        description = "Configure local Redis server for PeerTube.";
+      };
+
+      host = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        default = if cfg.redis.createLocally && !cfg.redis.enableUnixSocket then "127.0.0.1" else null;
+        defaultText = lib.literalExpression ''
+          if config.${opt.redis.createLocally} && !config.${opt.redis.enableUnixSocket}
+          then "127.0.0.1"
+          else null
+        '';
+        description = "Redis host.";
+      };
+
+      port = lib.mkOption {
+        type = lib.types.nullOr lib.types.port;
+        default = if cfg.redis.createLocally && cfg.redis.enableUnixSocket then null else 6379;
+        defaultText = lib.literalExpression ''
+          if config.${opt.redis.createLocally} && config.${opt.redis.enableUnixSocket}
+          then null
+          else 6379
+        '';
+        description = "Redis port.";
+      };
+
+      passwordFile = lib.mkOption {
+        type = lib.types.nullOr lib.types.path;
+        default = null;
+        example = "/run/keys/peertube/password-redis-db";
+        description = "Password for redis database.";
+      };
+
+      enableUnixSocket = lib.mkOption {
+        type = lib.types.bool;
+        default = cfg.redis.createLocally;
+        defaultText = lib.literalExpression "config.${opt.redis.createLocally}";
+        description = "Use Unix socket.";
+      };
+    };
+
+    smtp = {
+      createLocally = lib.mkOption {
+        type = lib.types.bool;
+        default = false;
+        description = "Configure local Postfix SMTP server for PeerTube.";
+      };
+
+      passwordFile = lib.mkOption {
+        type = lib.types.nullOr lib.types.path;
+        default = null;
+        example = "/run/keys/peertube/password-smtp";
+        description = "Password for smtp server.";
+      };
+    };
+
+    package = lib.mkOption {
+      type = lib.types.package;
+      default = pkgs.peertube;
+      defaultText = lib.literalExpression "pkgs.peertube";
+      description = "Peertube package to use.";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.serviceEnvironmentFile == null || !lib.hasPrefix builtins.storeDir cfg.serviceEnvironmentFile;
+          message = ''
+            <option>services.peertube.serviceEnvironmentFile</option> points to
+            a file in the Nix store. You should use a quoted absolute path to
+            prevent this.
+          '';
+      }
+      { 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.
+        '';
+      }
+      { assertion = cfg.redis.enableUnixSocket || (cfg.redis.host != null && cfg.redis.port != null);
+          message = ''
+            <option>services.peertube.redis.host</option> and <option>services.peertube.redis.port</option> needs to be set if <option>services.peertube.redis.enableUnixSocket</option> is not enabled.
+        '';
+      }
+      { assertion = cfg.redis.passwordFile == null || !lib.hasPrefix builtins.storeDir cfg.redis.passwordFile;
+          message = ''
+            <option>services.peertube.redis.passwordFile</option> points to
+            a file in the Nix store. You should use a quoted absolute path to
+            prevent this.
+          '';
+      }
+      { assertion = cfg.database.passwordFile == null || !lib.hasPrefix builtins.storeDir cfg.database.passwordFile;
+          message = ''
+            <option>services.peertube.database.passwordFile</option> points to
+            a file in the Nix store. You should use a quoted absolute path to
+            prevent this.
+          '';
+      }
+      { assertion = cfg.smtp.passwordFile == null || !lib.hasPrefix builtins.storeDir cfg.smtp.passwordFile;
+          message = ''
+            <option>services.peertube.smtp.passwordFile</option> points to
+            a file in the Nix store. You should use a quoted absolute path to
+            prevent this.
+          '';
+      }
+    ];
+
+    services.peertube.settings = lib.mkMerge [
+      {
+        listen = {
+          port = cfg.listenHttp;
+        };
+        webserver = {
+          https = (if cfg.enableWebHttps then true else false);
+          hostname = "${cfg.localDomain}";
+          port = cfg.listenWeb;
+        };
+        database = {
+          hostname = "${cfg.database.host}";
+          port = cfg.database.port;
+          name = "${cfg.database.name}";
+          username = "${cfg.database.user}";
+        };
+        redis = {
+          hostname = "${toString cfg.redis.host}";
+          port = (if cfg.redis.port == null then "" else cfg.redis.port);
+        };
+        storage = {
+          tmp = lib.mkDefault "/var/lib/peertube/storage/tmp/";
+          bin = lib.mkDefault "/var/lib/peertube/storage/bin/";
+          avatars = lib.mkDefault "/var/lib/peertube/storage/avatars/";
+          videos = lib.mkDefault "/var/lib/peertube/storage/videos/";
+          streaming_playlists = lib.mkDefault "/var/lib/peertube/storage/streaming-playlists/";
+          redundancy = lib.mkDefault "/var/lib/peertube/storage/redundancy/";
+          logs = lib.mkDefault "/var/lib/peertube/storage/logs/";
+          previews = lib.mkDefault "/var/lib/peertube/storage/previews/";
+          thumbnails = lib.mkDefault "/var/lib/peertube/storage/thumbnails/";
+          torrents = lib.mkDefault "/var/lib/peertube/storage/torrents/";
+          captions = lib.mkDefault "/var/lib/peertube/storage/captions/";
+          cache = lib.mkDefault "/var/lib/peertube/storage/cache/";
+          plugins = lib.mkDefault "/var/lib/peertube/storage/plugins/";
+          client_overrides = lib.mkDefault "/var/lib/peertube/storage/client-overrides/";
+        };
+        import = {
+          videos = {
+            http = {
+              youtube_dl_release = {
+                python_path = "${pkgs.python3}/bin/python";
+              };
+            };
+          };
+        };
+      }
+      (lib.mkIf cfg.redis.enableUnixSocket { redis = { socket = "/run/redis/redis.sock"; }; })
+    ];
+
+    systemd.tmpfiles.rules = [
+      "d '/var/lib/peertube/config' 0700 ${cfg.user} ${cfg.group} - -"
+      "z '/var/lib/peertube/config' 0700 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.peertube-init-db = lib.mkIf cfg.database.createLocally {
+      description = "Initialization database for PeerTube daemon";
+      after = [ "network.target" "postgresql.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      script = let
+        psqlSetupCommands = pkgs.writeText "peertube-init.sql" ''
+          SELECT 'CREATE USER "${cfg.database.user}"' WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${cfg.database.user}')\gexec
+          SELECT 'CREATE DATABASE "${cfg.database.name}" OWNER "${cfg.database.user}" TEMPLATE template0 ENCODING UTF8' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${cfg.database.name}')\gexec
+          \c '${cfg.database.name}'
+          CREATE EXTENSION IF NOT EXISTS pg_trgm;
+          CREATE EXTENSION IF NOT EXISTS unaccent;
+        '';
+      in "${config.services.postgresql.package}/bin/psql -f ${psqlSetupCommands}";
+
+      serviceConfig = {
+        Type = "oneshot";
+        WorkingDirectory = cfg.package;
+        # User and group
+        User = "postgres";
+        Group = "postgres";
+        # Sandboxing
+        RestrictAddressFamilies = [ "AF_UNIX" ];
+        MemoryDenyWriteExecute = true;
+        # System Call Filtering
+        SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ]);
+      } // cfgService;
+    };
+
+    systemd.services.peertube = {
+      description = "PeerTube daemon";
+      after = [ "network.target" ]
+        ++ lib.optionals cfg.redis.createLocally [ "redis.service" ]
+        ++ lib.optionals cfg.database.createLocally [ "postgresql.service" "peertube-init-db.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment = env;
+
+      path = with pkgs; [ bashInteractive ffmpeg nodejs-16_x openssl yarn python3 ];
+
+      script = ''
+        #!/bin/sh
+        umask 077
+        cat > /var/lib/peertube/config/local.yaml <<EOF
+        ${lib.optionalString ((!cfg.database.createLocally) && (cfg.database.passwordFile != null)) ''
+        database:
+          password: '$(cat ${cfg.database.passwordFile})'
+        ''}
+        ${lib.optionalString (cfg.redis.passwordFile != null) ''
+        redis:
+          auth: '$(cat ${cfg.redis.passwordFile})'
+        ''}
+        ${lib.optionalString (cfg.smtp.passwordFile != null) ''
+        smtp:
+          password: '$(cat ${cfg.smtp.passwordFile})'
+        ''}
+        EOF
+        ln -sf ${cfg.package}/config/default.yaml /var/lib/peertube/config/default.yaml
+        ln -sf ${configFile} /var/lib/peertube/config/production.json
+        npm start
+      '';
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+        RestartSec = 20;
+        TimeoutSec = 60;
+        WorkingDirectory = cfg.package;
+        # User and group
+        User = cfg.user;
+        Group = cfg.group;
+        # State directory and mode
+        StateDirectory = "peertube";
+        StateDirectoryMode = "0750";
+        # Access write directories
+        ReadWritePaths = cfg.dataDirs;
+        # Environment
+        EnvironmentFile = cfg.serviceEnvironmentFile;
+        # Sandboxing
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
+        MemoryDenyWriteExecute = false;
+        # System Call Filtering
+        SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "pipe" "pipe2" ];
+      } // cfgService;
+    };
+
+    services.postgresql = lib.mkIf cfg.database.createLocally {
+      enable = true;
+    };
+
+    services.redis = lib.mkMerge [
+      (lib.mkIf cfg.redis.createLocally {
+        enable = true;
+      })
+      (lib.mkIf (cfg.redis.createLocally && cfg.redis.enableUnixSocket) {
+        unixSocket = "/run/redis/redis.sock";
+        unixSocketPerm = 770;
+      })
+    ];
+
+    services.postfix = lib.mkIf cfg.smtp.createLocally {
+      enable = true;
+      hostname = lib.mkDefault "${cfg.localDomain}";
+    };
+
+    users.users = lib.mkMerge [
+      (lib.mkIf (cfg.user == "peertube") {
+        peertube = {
+          isSystemUser = true;
+          group = cfg.group;
+          home = cfg.package;
+        };
+      })
+      (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package peertubeEnv peertubeCli pkgs.ffmpeg pkgs.nodejs-16_x pkgs.yarn ])
+      (lib.mkIf cfg.redis.enableUnixSocket {${config.services.peertube.user}.extraGroups = [ "redis" ];})
+    ];
+
+    users.groups = lib.optionalAttrs (cfg.group == "peertube") {
+      peertube = { };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/pgpkeyserver-lite.nix b/nixos/modules/services/web-apps/pgpkeyserver-lite.nix
new file mode 100644
index 00000000000..faf0ce13238
--- /dev/null
+++ b/nixos/modules/services/web-apps/pgpkeyserver-lite.nix
@@ -0,0 +1,78 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.pgpkeyserver-lite;
+  sksCfg = config.services.sks;
+  sksOpt = options.services.sks;
+
+  webPkg = cfg.package;
+
+in
+
+{
+
+  options = {
+
+    services.pgpkeyserver-lite = {
+
+      enable = mkEnableOption "pgpkeyserver-lite on a nginx vHost proxying to a gpg keyserver";
+
+      package = mkOption {
+        default = pkgs.pgpkeyserver-lite;
+        defaultText = literalExpression "pkgs.pgpkeyserver-lite";
+        type = types.package;
+        description = "
+          Which webgui derivation to use.
+        ";
+      };
+
+      hostname = mkOption {
+        type = types.str;
+        description = "
+          Which hostname to set the vHost to that is proxying to sks.
+        ";
+      };
+
+      hkpAddress = mkOption {
+        default = builtins.head sksCfg.hkpAddress;
+        defaultText = literalExpression "head config.${sksOpt.hkpAddress}";
+        type = types.str;
+        description = "
+          Wich ip address the sks-keyserver is listening on.
+        ";
+      };
+
+      hkpPort = mkOption {
+        default = sksCfg.hkpPort;
+        defaultText = literalExpression "config.${sksOpt.hkpPort}";
+        type = types.int;
+        description = "
+          Which port the sks-keyserver is listening on.
+        ";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    services.nginx.enable = true;
+
+    services.nginx.virtualHosts = let
+      hkpPort = builtins.toString cfg.hkpPort;
+    in {
+      ${cfg.hostname} = {
+        root = webPkg;
+        locations = {
+          "/pks".extraConfig = ''
+            proxy_pass         http://${cfg.hkpAddress}:${hkpPort};
+            proxy_pass_header  Server;
+            add_header         Via "1.1 ${cfg.hostname}";
+          '';
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/pict-rs.md b/nixos/modules/services/web-apps/pict-rs.md
new file mode 100644
index 00000000000..4b622049909
--- /dev/null
+++ b/nixos/modules/services/web-apps/pict-rs.md
@@ -0,0 +1,88 @@
+# Pict-rs {#module-services-pict-rs}
+
+pict-rs is a  a simple image hosting service.
+
+## Quickstart {#module-services-pict-rs-quickstart}
+
+the minimum to start pict-rs is
+
+```nix
+services.pict-rs.enable = true;
+```
+
+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
+
+    This endpoint returns the following JSON structure on success with a 201 Created status
+    ```json
+    {
+        "files": [
+            {
+                "delete_token": "JFvFhqJA98",
+                "file": "lkWZDRvugm.jpg"
+            },
+            {
+                "delete_token": "kAYy9nk2WK",
+                "file": "8qFS0QooAn.jpg"
+            },
+            {
+                "delete_token": "OxRpM3sf0Y",
+                "file": "1hJaYfGE01.jpg"
+            }
+        ],
+        "msg": "ok"
+    }
+    ```
+- `GET /image/download?url=...` Download an image from a remote server, returning the same JSON
+    payload as the `POST` endpoint
+- `GET /image/original/{file}` for getting a full-resolution image. `file` here is the `file` key from the
+    `/image` endpoint's JSON
+- `GET /image/details/original/{file}` for getting the details of a full-resolution image.
+    The returned JSON is structured like so:
+    ```json
+    {
+        "width": 800,
+        "height": 537,
+        "content_type": "image/webp",
+        "created_at": [
+            2020,
+            345,
+            67376,
+            394363487
+        ]
+    }
+    ```
+- `GET /image/process.{ext}?src={file}&...` get a file with transformations applied.
+    existing transformations include
+    - `identity=true`: apply no changes
+    - `blur={float}`: apply a gaussian blur to the file
+    - `thumbnail={int}`: produce a thumbnail of the image fitting inside an `{int}` by `{int}`
+        square using raw pixel sampling
+    - `resize={int}`: produce a thumbnail of the image fitting inside an `{int}` by `{int}` square
+        using a Lanczos2 filter. This is slower than sampling but looks a bit better in some cases
+    - `crop={int-w}x{int-h}`: produce a cropped version of the image with an `{int-w}` by `{int-h}`
+        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.
+
+    Supported `ext` file extensions include `png`, `jpg`, and `webp`
+
+    An example of usage could be
+    ```
+    GET /image/process.jpg?src=asdf.png&thumbnail=256&blur=3.0
+    ```
+    which would create a 256x256px JPEG thumbnail and blur it
+- `GET /image/details/process.{ext}?src={file}&...` for getting the details of a processed image.
+    The returned JSON is the same format as listed for the full-resolution details endpoint.
+- `DELETE /image/delete/{delete_token}/{file}` or `GET /image/delete/{delete_token}/{file}` to
+    delete a file, where `delete_token` and `file` are from the `/image` endpoint's JSON
+
+## Missing {#module-services-pict-rs-missing}
+
+- 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.
diff --git a/nixos/modules/services/web-apps/pict-rs.nix b/nixos/modules/services/web-apps/pict-rs.nix
new file mode 100644
index 00000000000..e1847fbd531
--- /dev/null
+++ b/nixos/modules/services/web-apps/pict-rs.nix
@@ -0,0 +1,50 @@
+{ lib, pkgs, config, ... }:
+with lib;
+let
+  cfg = config.services.pict-rs;
+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;
+
+  options.services.pict-rs = {
+    enable = mkEnableOption "pict-rs server";
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/pict-rs";
+      description = ''
+        The directory where to store the uploaded images.
+      '';
+    };
+    address = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = ''
+        The IPv4 address to deploy the service to.
+      '';
+    };
+    port = mkOption {
+      type = types.port;
+      default = 8080;
+      description = ''
+        The port which to bind the service to.
+      '';
+    };
+  };
+  config = lib.mkIf cfg.enable {
+    systemd.services.pict-rs = {
+      environment = {
+        PICTRS_PATH = cfg.dataDir;
+        PICTRS_ADDR = "${cfg.address}:${toString cfg.port}";
+      };
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "pict-rs";
+        ExecStart = "${pkgs.pict-rs}/bin/pict-rs";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/pict-rs.xml b/nixos/modules/services/web-apps/pict-rs.xml
new file mode 100644
index 00000000000..bf129f5cc2a
--- /dev/null
+++ b/nixos/modules/services/web-apps/pict-rs.xml
@@ -0,0 +1,162 @@
+<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/plantuml-server.nix b/nixos/modules/services/web-apps/plantuml-server.nix
new file mode 100644
index 00000000000..9ea37b8a4ca
--- /dev/null
+++ b/nixos/modules/services/web-apps/plantuml-server.nix
@@ -0,0 +1,140 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.plantuml-server;
+
+in
+
+{
+  options = {
+    services.plantuml-server = {
+      enable = mkEnableOption "PlantUML server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.plantuml-server;
+        defaultText = literalExpression "pkgs.plantuml-server";
+        description = "PlantUML server package to use";
+      };
+
+      packages = {
+        jdk = mkOption {
+          type = types.package;
+          default = pkgs.jdk;
+          defaultText = literalExpression "pkgs.jdk";
+          description = "JDK package to use for the server";
+        };
+        jetty = mkOption {
+          type = types.package;
+          default = pkgs.jetty;
+          defaultText = literalExpression "pkgs.jetty";
+          description = "Jetty package to use for the server";
+        };
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "plantuml";
+        description = "User which runs PlantUML server.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "plantuml";
+        description = "Group which runs PlantUML server.";
+      };
+
+      home = mkOption {
+        type = types.str;
+        default = "/var/lib/plantuml";
+        description = "Home directory of the PlantUML server instance.";
+      };
+
+      listenHost = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "Host to listen on.";
+      };
+
+      listenPort = mkOption {
+        type = types.int;
+        default = 8080;
+        description = "Port to listen on.";
+      };
+
+      plantumlLimitSize = mkOption {
+        type = types.int;
+        default = 4096;
+        description = "Limits image width and height.";
+      };
+
+      graphvizPackage = mkOption {
+        type = types.package;
+        default = pkgs.graphviz;
+        defaultText = literalExpression "pkgs.graphviz";
+        description = "Package containing the dot executable.";
+      };
+
+      plantumlStats = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Set it to on to enable statistics report (https://plantuml.com/statistics-report).";
+      };
+
+      httpAuthorization = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "When calling the proxy endpoint, the value of HTTP_AUTHORIZATION will be used to set the HTTP Authorization header.";
+      };
+
+      allowPlantumlInclude = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enables !include processing which can read files from the server into diagrams. Files are read relative to the current working directory.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.${cfg.user} = {
+      isSystemUser = true;
+      group = cfg.group;
+      home = cfg.home;
+      createHome = true;
+    };
+
+    users.groups.${cfg.group} = {};
+
+    systemd.services.plantuml-server = {
+      description = "PlantUML server";
+      wantedBy = [ "multi-user.target" ];
+      path = [ cfg.home ];
+      environment = {
+        PLANTUML_LIMIT_SIZE = builtins.toString cfg.plantumlLimitSize;
+        GRAPHVIZ_DOT = "${cfg.graphvizPackage}/bin/dot";
+        PLANTUML_STATS = if cfg.plantumlStats then "on" else "off";
+        HTTP_AUTHORIZATION = cfg.httpAuthorization;
+        ALLOW_PLANTUML_INCLUDE = if cfg.allowPlantumlInclude then "true" else "false";
+      };
+      script = ''
+      ${cfg.packages.jdk}/bin/java \
+        -jar ${cfg.packages.jetty}/start.jar \
+          --module=deploy,http,jsp \
+          jetty.home=${cfg.packages.jetty} \
+          jetty.base=${cfg.package} \
+          jetty.http.host=${cfg.listenHost} \
+          jetty.http.port=${builtins.toString cfg.listenPort}
+      '';
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        PrivateTmp = true;
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ truh ];
+}
diff --git a/nixos/modules/services/web-apps/plausible.nix b/nixos/modules/services/web-apps/plausible.nix
new file mode 100644
index 00000000000..5d550ae5ca8
--- /dev/null
+++ b/nixos/modules/services/web-apps/plausible.nix
@@ -0,0 +1,292 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.plausible;
+
+in {
+  options.services.plausible = {
+    enable = mkEnableOption "plausible";
+
+    releaseCookiePath = mkOption {
+      type = with types; either str path;
+      description = ''
+        The path to the file with release cookie. (used for remote connection to the running node).
+      '';
+    };
+
+    adminUser = {
+      name = mkOption {
+        default = "admin";
+        type = types.str;
+        description = ''
+          Name of the admin user that plausible will created on initial startup.
+        '';
+      };
+
+      email = mkOption {
+        type = types.str;
+        example = "admin@localhost";
+        description = ''
+          Email-address of the admin-user.
+        '';
+      };
+
+      passwordFile = mkOption {
+        type = types.either types.str types.path;
+        description = ''
+          Path to the file which contains the password of the admin user.
+        '';
+      };
+
+      activate = mkEnableOption "activating the freshly created admin-user";
+    };
+
+    database = {
+      clickhouse = {
+        setup = mkEnableOption "creating a clickhouse instance" // { default = true; };
+        url = mkOption {
+          default = "http://localhost:8123/default";
+          type = types.str;
+          description = ''
+            The URL to be used to connect to <package>clickhouse</package>.
+          '';
+        };
+      };
+      postgres = {
+        setup = mkEnableOption "creating a postgresql instance" // { default = true; };
+        dbname = mkOption {
+          default = "plausible";
+          type = types.str;
+          description = ''
+            Name of the database to use.
+          '';
+        };
+        socket = mkOption {
+          default = "/run/postgresql";
+          type = types.str;
+          description = ''
+            Path to the UNIX domain-socket to communicate with <package>postgres</package>.
+          '';
+        };
+      };
+    };
+
+    server = {
+      disableRegistration = mkOption {
+        default = true;
+        type = types.bool;
+        description = ''
+          Whether to prohibit creating an account in plausible's UI.
+        '';
+      };
+      secretKeybaseFile = mkOption {
+        type = types.either types.path types.str;
+        description = ''
+          Path to the secret used by the <literal>phoenix</literal>-framework. Instructions
+          how to generate one are documented in the
+          <link xlink:href="https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Secret.html#content">
+          framework docs</link>.
+        '';
+      };
+      port = mkOption {
+        default = 8000;
+        type = types.port;
+        description = ''
+          Port where the service should be available.
+        '';
+      };
+      baseUrl = mkOption {
+        type = types.str;
+        description = ''
+          Public URL where plausible is available.
+
+          Note that <literal>/path</literal> components are currently ignored:
+          <link xlink:href="https://github.com/plausible/analytics/issues/1182">
+            https://github.com/plausible/analytics/issues/1182
+          </link>.
+        '';
+      };
+    };
+
+    mail = {
+      email = mkOption {
+        default = "hello@plausible.local";
+        type = types.str;
+        description = ''
+          The email id to use for as <emphasis>from</emphasis> address of all communications
+          from Plausible.
+        '';
+      };
+      smtp = {
+        hostAddr = mkOption {
+          default = "localhost";
+          type = types.str;
+          description = ''
+            The host address of your smtp server.
+          '';
+        };
+        hostPort = mkOption {
+          default = 25;
+          type = types.port;
+          description = ''
+            The port of your smtp server.
+          '';
+        };
+        user = mkOption {
+          default = null;
+          type = types.nullOr types.str;
+          description = ''
+            The username/email in case SMTP auth is enabled.
+          '';
+        };
+        passwordFile = mkOption {
+          default = null;
+          type = with types; nullOr (either str path);
+          description = ''
+            The path to the file with the password in case SMTP auth is enabled.
+          '';
+        };
+        enableSSL = mkEnableOption "SSL when connecting to the SMTP server";
+        retries = mkOption {
+          type = types.ints.unsigned;
+          default = 2;
+          description = ''
+            Number of retries to make until mailer gives up.
+          '';
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.adminUser.activate -> cfg.database.postgres.setup;
+        message = ''
+          Unable to automatically activate the admin-user if no locally managed DB for
+          postgres (`services.plausible.database.postgres.setup') is enabled!
+        '';
+      }
+    ];
+
+    services.postgresql = mkIf cfg.database.postgres.setup {
+      enable = true;
+    };
+
+    services.clickhouse = mkIf cfg.database.clickhouse.setup {
+      enable = true;
+    };
+
+    services.epmd.enable = true;
+
+    environment.systemPackages = [ pkgs.plausible ];
+
+    systemd.services = mkMerge [
+      {
+        plausible = {
+          inherit (pkgs.plausible.meta) description;
+          documentation = [ "https://plausible.io/docs/self-hosting" ];
+          wantedBy = [ "multi-user.target" ];
+          after = optionals cfg.database.postgres.setup [ "postgresql.service" "plausible-postgres.service" ];
+          requires = optional cfg.database.clickhouse.setup "clickhouse.service"
+            ++ optionals cfg.database.postgres.setup [
+              "postgresql.service"
+              "plausible-postgres.service"
+            ];
+
+          environment = {
+            # NixOS specific option to avoid that it's trying to write into its store-path.
+            # See also https://github.com/lau/tzdata#data-directory-and-releases
+            STORAGE_DIR = "/var/lib/plausible/elixir_tzdata";
+
+            # Configuration options from
+            # https://plausible.io/docs/self-hosting-configuration
+            PORT = toString cfg.server.port;
+            DISABLE_REGISTRATION = boolToString cfg.server.disableRegistration;
+
+            RELEASE_TMP = "/var/lib/plausible/tmp";
+            # Home is needed to connect to the node with iex
+            HOME = "/var/lib/plausible";
+
+            ADMIN_USER_NAME = cfg.adminUser.name;
+            ADMIN_USER_EMAIL = cfg.adminUser.email;
+
+            DATABASE_SOCKET_DIR = cfg.database.postgres.socket;
+            DATABASE_NAME = cfg.database.postgres.dbname;
+            CLICKHOUSE_DATABASE_URL = cfg.database.clickhouse.url;
+
+            BASE_URL = cfg.server.baseUrl;
+
+            MAILER_EMAIL = cfg.mail.email;
+            SMTP_HOST_ADDR = cfg.mail.smtp.hostAddr;
+            SMTP_HOST_PORT = toString cfg.mail.smtp.hostPort;
+            SMTP_RETRIES = toString cfg.mail.smtp.retries;
+            SMTP_HOST_SSL_ENABLED = boolToString cfg.mail.smtp.enableSSL;
+
+            SELFHOST = "true";
+          } // (optionalAttrs (cfg.mail.smtp.user != null) {
+            SMTP_USER_NAME = cfg.mail.smtp.user;
+          });
+
+          path = [ pkgs.plausible ]
+            ++ optional cfg.database.postgres.setup config.services.postgresql.package;
+          script = ''
+            export CONFIG_DIR=$CREDENTIALS_DIRECTORY
+
+            export RELEASE_COOKIE="$(< $CREDENTIALS_DIRECTORY/RELEASE_COOKIE )"
+
+            # setup
+            ${pkgs.plausible}/createdb.sh
+            ${pkgs.plausible}/migrate.sh
+            ${optionalString cfg.adminUser.activate ''
+              if ! ${pkgs.plausible}/init-admin.sh | grep 'already exists'; then
+                psql -d plausible <<< "UPDATE users SET email_verified=true;"
+              fi
+            ''}
+
+            exec plausible start
+          '';
+
+          serviceConfig = {
+            DynamicUser = true;
+            PrivateTmp = true;
+            WorkingDirectory = "/var/lib/plausible";
+            StateDirectory = "plausible";
+            LoadCredential = [
+              "ADMIN_USER_PWD:${cfg.adminUser.passwordFile}"
+              "SECRET_KEY_BASE:${cfg.server.secretKeybaseFile}"
+              "RELEASE_COOKIE:${cfg.releaseCookiePath}"
+            ] ++ lib.optionals (cfg.mail.smtp.passwordFile != null) [ "SMTP_USER_PWD:${cfg.mail.smtp.passwordFile}"];
+          };
+        };
+      }
+      (mkIf cfg.database.postgres.setup {
+        # `plausible' requires the `citext'-extension.
+        plausible-postgres = {
+          after = [ "postgresql.service" ];
+          partOf = [ "plausible.service" ];
+          serviceConfig = {
+            Type = "oneshot";
+            User = config.services.postgresql.superUser;
+            RemainAfterExit = true;
+          };
+          script = with cfg.database.postgres; ''
+            PSQL() {
+              ${config.services.postgresql.package}/bin/psql --port=5432 "$@"
+            }
+            # check if the database already exists
+            if ! PSQL -lqt | ${pkgs.coreutils}/bin/cut -d \| -f 1 | ${pkgs.gnugrep}/bin/grep -qw ${dbname} ; then
+              PSQL -tAc "CREATE ROLE plausible WITH LOGIN;"
+              PSQL -tAc "CREATE DATABASE ${dbname} WITH OWNER plausible;"
+              PSQL -d ${dbname} -tAc "CREATE EXTENSION IF NOT EXISTS citext;"
+            fi
+          '';
+        };
+      })
+    ];
+  };
+
+  meta.maintainers = with maintainers; [ ma27 ];
+  meta.doc = ./plausible.xml;
+}
diff --git a/nixos/modules/services/web-apps/plausible.xml b/nixos/modules/services/web-apps/plausible.xml
new file mode 100644
index 00000000000..92a571b9fbd
--- /dev/null
+++ b/nixos/modules/services/web-apps/plausible.xml
@@ -0,0 +1,51 @@
+<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/powerdns-admin.nix b/nixos/modules/services/web-apps/powerdns-admin.nix
new file mode 100644
index 00000000000..4661ba80c5d
--- /dev/null
+++ b/nixos/modules/services/web-apps/powerdns-admin.nix
@@ -0,0 +1,152 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.powerdns-admin;
+
+  configText = ''
+    ${cfg.config}
+  ''
+  + optionalString (cfg.secretKeyFile != null) ''
+    with open('${cfg.secretKeyFile}') as file:
+      SECRET_KEY = file.read()
+  ''
+  + optionalString (cfg.saltFile != null) ''
+    with open('${cfg.saltFile}') as file:
+      SALT = file.read()
+  '';
+in
+{
+  options.services.powerdns-admin = {
+    enable = mkEnableOption "the PowerDNS web interface";
+
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = literalExpression ''
+        [ "-b" "127.0.0.1:8000" ]
+      '';
+      description = ''
+        Extra arguments passed to powerdns-admin.
+      '';
+    };
+
+    config = mkOption {
+      type = types.str;
+      default = "";
+      example = ''
+        BIND_ADDRESS = '127.0.0.1'
+        PORT = 8000
+        SQLALCHEMY_DATABASE_URI = 'postgresql://powerdnsadmin@/powerdnsadmin?host=/run/postgresql'
+      '';
+      description = ''
+        Configuration python file.
+        See <link xlink:href="https://github.com/ngoduykhanh/PowerDNS-Admin/blob/v${pkgs.powerdns-admin.version}/configs/development.py">the example configuration</link>
+        for options.
+      '';
+    };
+
+    secretKeyFile = mkOption {
+      type = types.nullOr types.path;
+      example = "/etc/powerdns-admin/secret";
+      description = ''
+        The secret used to create cookies.
+        This needs to be set, otherwise the default is used and everyone can forge valid login cookies.
+        Set this to null to ignore this setting and configure it through another way.
+      '';
+    };
+
+    saltFile = mkOption {
+      type = types.nullOr types.path;
+      example = "/etc/powerdns-admin/salt";
+      description = ''
+        The salt used for serialization.
+        This should be set, otherwise the default is used.
+        Set this to null to ignore this setting and configure it through another way.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.powerdns-admin = {
+      description = "PowerDNS web interface";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+
+      environment.FLASK_CONF = builtins.toFile "powerdns-admin-config.py" configText;
+      environment.PYTHONPATH = pkgs.powerdns-admin.pythonPath;
+      serviceConfig = {
+        ExecStart = "${pkgs.powerdns-admin}/bin/powerdns-admin --pid /run/powerdns-admin/pid ${escapeShellArgs cfg.extraArgs}";
+        ExecStartPre = "${pkgs.coreutils}/bin/env FLASK_APP=${pkgs.powerdns-admin}/share/powerdnsadmin/__init__.py ${pkgs.python3Packages.flask}/bin/flask db upgrade -d ${pkgs.powerdns-admin}/share/migrations";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        ExecStop = "${pkgs.coreutils}/bin/kill -TERM $MAINPID";
+        PIDFile = "/run/powerdns-admin/pid";
+        RuntimeDirectory = "powerdns-admin";
+        User = "powerdnsadmin";
+        Group = "powerdnsadmin";
+
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        BindReadOnlyPaths = [
+          "/nix/store"
+          "-/etc/resolv.conf"
+          "-/etc/nsswitch.conf"
+          "-/etc/hosts"
+          "-/etc/localtime"
+        ]
+        ++ (optional (cfg.secretKeyFile != null) cfg.secretKeyFile)
+        ++ (optional (cfg.saltFile != null) cfg.saltFile);
+        CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        # Implies ProtectSystem=strict, which re-mounts all paths
+        #DynamicUser = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        # Needs to start a server
+        #PrivateNetwork = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        # Would re-mount paths ignored by temporary root
+        #ProtectSystem = "strict";
+        ProtectControlGroups = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        # gunicorn needs setuid
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged @resources @keyring"
+          # These got removed by the line above but are needed
+          "@setuid @chown"
+        ];
+        TemporaryFileSystem = "/:ro";
+        # Does not work well with the temporary root
+        #UMask = "0066";
+      };
+    };
+
+    users.groups.powerdnsadmin = { };
+    users.users.powerdnsadmin = {
+      description = "PowerDNS web interface user";
+      isSystemUser = true;
+      group = "powerdnsadmin";
+    };
+  };
+
+  # uses attributes of the linked package
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/web-apps/prosody-filer.nix b/nixos/modules/services/web-apps/prosody-filer.nix
new file mode 100644
index 00000000000..a901a95fd5f
--- /dev/null
+++ b/nixos/modules/services/web-apps/prosody-filer.nix
@@ -0,0 +1,86 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+
+  cfg = config.services.prosody-filer;
+
+  settingsFormat = pkgs.formats.toml { };
+  configFile = settingsFormat.generate "prosody-filer.toml" cfg.settings;
+in {
+
+  options = {
+    services.prosody-filer = {
+      enable = mkEnableOption "Prosody Filer XMPP upload file server";
+
+      settings = mkOption {
+        description = ''
+          Configuration for Prosody Filer.
+          Refer to <link xlink:href="https://github.com/ThomasLeister/prosody-filer#configure-prosody-filer"/> for details on supported values.
+        '';
+
+        type = settingsFormat.type;
+
+        example = {
+          secret = "mysecret";
+          storeDir = "/srv/http/nginx/prosody-upload";
+        };
+
+        defaultText = literalExpression ''
+          {
+            listenport = mkDefault "127.0.0.1:5050";
+            uploadSubDir = mkDefault "upload/";
+          }
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.prosody-filer.settings = {
+      listenport = mkDefault "127.0.0.1:5050";
+      uploadSubDir = mkDefault "upload/";
+    };
+
+    users.users.prosody-filer = {
+      group = "prosody-filer";
+      isSystemUser = true;
+    };
+
+    users.groups.prosody-filer = { };
+
+    systemd.services.prosody-filer = {
+      description = "Prosody file upload server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        User = "prosody-filer";
+        Group = "prosody-filer";
+        ExecStart = "${pkgs.prosody-filer}/bin/prosody-filer -config ${configFile}";
+        Restart = "on-failure";
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = 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" "~@resources" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/restya-board.nix b/nixos/modules/services/web-apps/restya-board.nix
new file mode 100644
index 00000000000..4b36cc8754c
--- /dev/null
+++ b/nixos/modules/services/web-apps/restya-board.nix
@@ -0,0 +1,380 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+# TODO: are these php-packages needed?
+#imagick
+#php-geoip -> php.ini: extension = geoip.so
+#expat
+
+let
+  cfg = config.services.restya-board;
+  fpm = config.services.phpfpm.pools.${poolName};
+
+  runDir = "/run/restya-board";
+
+  poolName = "restya-board";
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.restya-board = {
+
+      enable = mkEnableOption "restya-board";
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/restya-board";
+        description = ''
+          Data of the application.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "restya-board";
+        description = ''
+          User account under which the web-application runs.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "nginx";
+        description = ''
+          Group account under which the web-application runs.
+        '';
+      };
+
+      virtualHost = {
+        serverName = mkOption {
+          type = types.str;
+          default = "restya.board";
+          description = ''
+            Name of the nginx virtualhost to use.
+          '';
+        };
+
+        listenHost = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = ''
+            Listen address for the virtualhost to use.
+          '';
+        };
+
+        listenPort = mkOption {
+          type = types.int;
+          default = 3000;
+          description = ''
+            Listen port for the virtualhost to use.
+          '';
+        };
+      };
+
+      database = {
+        host = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            Host of the database. Leave 'null' to use a local PostgreSQL database.
+            A local PostgreSQL database is initialized automatically.
+          '';
+        };
+
+        port = mkOption {
+          type = types.nullOr types.int;
+          default = 5432;
+          description = ''
+            The database's port.
+          '';
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "restya_board";
+          description = ''
+            Name of the database. The database must exist.
+          '';
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "restya_board";
+          description = ''
+            The database user. The user must exist and have access to
+            the specified database.
+          '';
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          description = ''
+            The database user's password. 'null' if no password is set.
+          '';
+        };
+      };
+
+      email = {
+        server = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          example = "localhost";
+          description = ''
+            Hostname to send outgoing mail. Null to use the system MTA.
+          '';
+        };
+
+        port = mkOption {
+          type = types.int;
+          default = 25;
+          description = ''
+            Port used to connect to SMTP server.
+          '';
+        };
+
+        login = mkOption {
+          type = types.str;
+          default = "";
+          description = ''
+            SMTP authentication login used when sending outgoing mail.
+          '';
+        };
+
+        password = mkOption {
+          type = types.str;
+          default = "";
+          description = ''
+            SMTP authentication password used when sending outgoing mail.
+
+            ATTENTION: The password is stored world-readable in the nix-store!
+          '';
+        };
+      };
+
+      timezone = mkOption {
+        type = types.lines;
+        default = "GMT";
+        description = ''
+          Timezone the web-app runs in.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.phpfpm.pools = {
+      ${poolName} = {
+        inherit (cfg) user group;
+
+        phpOptions = ''
+          date.timezone = "CET"
+
+          ${optionalString (cfg.email.server != null) ''
+            SMTP = ${cfg.email.server}
+            smtp_port = ${toString cfg.email.port}
+            auth_username = ${cfg.email.login}
+            auth_password = ${cfg.email.password}
+          ''}
+        '';
+        settings = mapAttrs (name: mkDefault) {
+          "listen.owner" = "nginx";
+          "listen.group" = "nginx";
+          "listen.mode" = "0600";
+          "pm" = "dynamic";
+          "pm.max_children" = 75;
+          "pm.start_servers" = 10;
+          "pm.min_spare_servers" = 5;
+          "pm.max_spare_servers" = 20;
+          "pm.max_requests" = 500;
+          "catch_workers_output" = 1;
+        };
+      };
+    };
+
+    services.nginx.enable = true;
+    services.nginx.virtualHosts.${cfg.virtualHost.serverName} = {
+      listen = [ { addr = cfg.virtualHost.listenHost; port = cfg.virtualHost.listenPort; } ];
+      serverName = cfg.virtualHost.serverName;
+      root = runDir;
+      extraConfig = ''
+        index index.html index.php;
+
+        gzip on;
+
+        gzip_comp_level 6;
+        gzip_min_length  1100;
+        gzip_buffers 16 8k;
+        gzip_proxied any;
+        gzip_types text/plain application/xml text/css text/js text/xml application/x-javascript text/javascript application/json application/xml+rss;
+
+        client_max_body_size 300M;
+
+        rewrite ^/oauth/authorize$ /server/php/authorize.php last;
+        rewrite ^/oauth_callback/([a-zA-Z0-9_\.]*)/([a-zA-Z0-9_\.]*)$ /server/php/oauth_callback.php?plugin=$1&code=$2 last;
+        rewrite ^/download/([0-9]*)/([a-zA-Z0-9_\.]*)$ /server/php/download.php?id=$1&hash=$2 last;
+        rewrite ^/ical/([0-9]*)/([0-9]*)/([a-z0-9]*).ics$ /server/php/ical.php?board_id=$1&user_id=$2&hash=$3 last;
+        rewrite ^/api/(.*)$ /server/php/R/r.php?_url=$1&$args last;
+        rewrite ^/api_explorer/api-docs/$ /client/api_explorer/api-docs/index.php last;
+      '';
+
+      locations."/".root = "${runDir}/client";
+
+      locations."~ \\.php$" = {
+        tryFiles = "$uri =404";
+        extraConfig = ''
+          include ${config.services.nginx.package}/conf/fastcgi_params;
+          fastcgi_pass    unix:${fpm.socket};
+          fastcgi_index   index.php;
+          fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
+          fastcgi_param   PHP_VALUE "upload_max_filesize=9G \n post_max_size=9G \n max_execution_time=200 \n max_input_time=200 \n memory_limit=256M";
+        '';
+      };
+
+      locations."~* \\.(css|js|less|html|ttf|woff|jpg|jpeg|gif|png|bmp|ico)" = {
+        root = "${runDir}/client";
+        extraConfig = ''
+          if (-f $request_filename) {
+                  break;
+          }
+          rewrite ^/img/([a-zA-Z_]*)/([a-zA-Z_]*)/([a-zA-Z0-9_\.]*)$ /server/php/image.php?size=$1&model=$2&filename=$3 last;
+          add_header        Cache-Control public;
+          add_header        Cache-Control must-revalidate;
+          expires           7d;
+        '';
+      };
+    };
+
+    systemd.services.restya-board-init = {
+      description = "Restya board initialization";
+      serviceConfig.Type = "oneshot";
+      serviceConfig.RemainAfterExit = true;
+
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "postgresql.service" ];
+      after = [ "network.target" "postgresql.service" ];
+
+      script = ''
+        rm -rf "${runDir}"
+        mkdir -m 750 -p "${runDir}"
+        cp -r "${pkgs.restya-board}/"* "${runDir}"
+        sed -i "s/@restya.com/@${cfg.virtualHost.serverName}/g" "${runDir}/sql/restyaboard_with_empty_data.sql"
+        rm -rf "${runDir}/media"
+        rm -rf "${runDir}/client/img"
+        chmod -R 0750 "${runDir}"
+
+        sed -i "s@^php@${config.services.phpfpm.phpPackage}/bin/php@" "${runDir}/server/php/shell/"*.sh
+
+        ${if (cfg.database.host == null) then ''
+          sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', 'localhost');/g" "${runDir}/server/php/config.inc.php"
+          sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', 'restya');/g" "${runDir}/server/php/config.inc.php"
+        '' else ''
+          sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', '${cfg.database.host}');/g" "${runDir}/server/php/config.inc.php"
+          sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', ${if cfg.database.passwordFile == null then "''" else "'file_get_contents(${cfg.database.passwordFile})'"});/g" "${runDir}/server/php/config.inc.php
+        ''}
+        sed -i "s/^.*'R_DB_PORT'.*$/define('R_DB_PORT', '${toString cfg.database.port}');/g" "${runDir}/server/php/config.inc.php"
+        sed -i "s/^.*'R_DB_NAME'.*$/define('R_DB_NAME', '${cfg.database.name}');/g" "${runDir}/server/php/config.inc.php"
+        sed -i "s/^.*'R_DB_USER'.*$/define('R_DB_USER', '${cfg.database.user}');/g" "${runDir}/server/php/config.inc.php"
+
+        chmod 0400 "${runDir}/server/php/config.inc.php"
+
+        ln -sf "${cfg.dataDir}/media" "${runDir}/media"
+        ln -sf "${cfg.dataDir}/client/img" "${runDir}/client/img"
+
+        chmod g+w "${runDir}/tmp/cache"
+        chown -R "${cfg.user}"."${cfg.group}" "${runDir}"
+
+
+        mkdir -m 0750 -p "${cfg.dataDir}"
+        mkdir -m 0750 -p "${cfg.dataDir}/media"
+        mkdir -m 0750 -p "${cfg.dataDir}/client/img"
+        cp -r "${pkgs.restya-board}/media/"* "${cfg.dataDir}/media"
+        cp -r "${pkgs.restya-board}/client/img/"* "${cfg.dataDir}/client/img"
+        chown "${cfg.user}"."${cfg.group}" "${cfg.dataDir}"
+        chown -R "${cfg.user}"."${cfg.group}" "${cfg.dataDir}/media"
+        chown -R "${cfg.user}"."${cfg.group}" "${cfg.dataDir}/client/img"
+
+        ${optionalString (cfg.database.host == null) ''
+          if ! [ -e "${cfg.dataDir}/.db-initialized" ]; then
+            ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \
+              ${config.services.postgresql.package}/bin/psql -U ${config.services.postgresql.superUser} \
+              -c "CREATE USER ${cfg.database.user} WITH ENCRYPTED PASSWORD 'restya'"
+
+            ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \
+              ${config.services.postgresql.package}/bin/psql -U ${config.services.postgresql.superUser} \
+              -c "CREATE DATABASE ${cfg.database.name} OWNER ${cfg.database.user} ENCODING 'UTF8' TEMPLATE template0"
+
+            ${pkgs.sudo}/bin/sudo -u ${cfg.user} \
+              ${config.services.postgresql.package}/bin/psql -U ${cfg.database.user} \
+              -d ${cfg.database.name} -f "${runDir}/sql/restyaboard_with_empty_data.sql"
+
+            touch "${cfg.dataDir}/.db-initialized"
+          fi
+        ''}
+      '';
+    };
+
+    systemd.timers.restya-board = {
+      description = "restya-board scripts for e.g. email notification";
+      wantedBy = [ "timers.target" ];
+      after = [ "restya-board-init.service" ];
+      requires = [ "restya-board-init.service" ];
+      timerConfig = {
+        OnUnitInactiveSec = "60s";
+        Unit = "restya-board-timers.service";
+      };
+    };
+
+    systemd.services.restya-board-timers = {
+      description = "restya-board scripts for e.g. email notification";
+      serviceConfig.Type = "oneshot";
+      serviceConfig.User = cfg.user;
+
+      after = [ "restya-board-init.service" ];
+      requires = [ "restya-board-init.service" ];
+
+      script = ''
+        /bin/sh ${runDir}/server/php/shell/instant_email_notification.sh 2> /dev/null || true
+        /bin/sh ${runDir}/server/php/shell/periodic_email_notification.sh 2> /dev/null || true
+        /bin/sh ${runDir}/server/php/shell/imap.sh 2> /dev/null || true
+        /bin/sh ${runDir}/server/php/shell/webhook.sh 2> /dev/null || true
+        /bin/sh ${runDir}/server/php/shell/card_due_notification.sh 2> /dev/null || true
+      '';
+    };
+
+    users.users.restya-board = {
+      isSystemUser = true;
+      createHome = false;
+      home = runDir;
+      group  = "restya-board";
+    };
+    users.groups.restya-board = {};
+
+    services.postgresql.enable = mkIf (cfg.database.host == null) true;
+
+    services.postgresql.identMap = optionalString (cfg.database.host == null)
+      ''
+        restya-board-users restya-board restya_board
+      '';
+
+    services.postgresql.authentication = optionalString (cfg.database.host == null)
+      ''
+        local restya_board all ident map=restya-board-users
+      '';
+
+  };
+
+}
+
diff --git a/nixos/modules/services/web-apps/rss-bridge.nix b/nixos/modules/services/web-apps/rss-bridge.nix
new file mode 100644
index 00000000000..f2b6d955982
--- /dev/null
+++ b/nixos/modules/services/web-apps/rss-bridge.nix
@@ -0,0 +1,125 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.rss-bridge;
+
+  poolName = "rss-bridge";
+
+  whitelist = pkgs.writeText "rss-bridge_whitelist.txt"
+    (concatStringsSep "\n" cfg.whitelist);
+in
+{
+  options = {
+    services.rss-bridge = {
+      enable = mkEnableOption "rss-bridge";
+
+      user = mkOption {
+        type = types.str;
+        default = "nginx";
+        description = ''
+          User account under which both the service and the web-application run.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "nginx";
+        description = ''
+          Group under which the web-application run.
+        '';
+      };
+
+      pool = mkOption {
+        type = types.str;
+        default = poolName;
+        description = ''
+          Name of existing phpfpm pool that is used to run web-application.
+          If not specified a pool will be created automatically with
+          default values.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/rss-bridge";
+        description = ''
+          Location in which cache directory will be created.
+          You can put <literal>config.ini.php</literal> in here.
+        '';
+      };
+
+      virtualHost = mkOption {
+        type = types.nullOr types.str;
+        default = "rss-bridge";
+        description = ''
+          Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
+        '';
+      };
+
+      whitelist = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = options.literalExpression ''
+          [
+            "Facebook"
+            "Instagram"
+            "Twitter"
+          ]
+        '';
+        description = ''
+          List of bridges to be whitelisted.
+          If the list is empty, rss-bridge will use whitelist.default.txt.
+          Use <literal>[ "*" ]</literal> to whitelist all.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.phpfpm.pools = mkIf (cfg.pool == poolName) {
+      ${poolName} = {
+        user = cfg.user;
+        settings = mapAttrs (name: mkDefault) {
+          "listen.owner" = cfg.user;
+          "listen.group" = cfg.user;
+          "listen.mode" = "0600";
+          "pm" = "dynamic";
+          "pm.max_children" = 75;
+          "pm.start_servers" = 10;
+          "pm.min_spare_servers" = 5;
+          "pm.max_spare_servers" = 20;
+          "pm.max_requests" = 500;
+          "catch_workers_output" = 1;
+        };
+      };
+    };
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}/cache' 0750 ${cfg.user} ${cfg.group} - -"
+      (mkIf (cfg.whitelist != []) "L+ ${cfg.dataDir}/whitelist.txt - - - - ${whitelist}")
+      "z '${cfg.dataDir}/config.ini.php' 0750 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    services.nginx = mkIf (cfg.virtualHost != null) {
+      enable = true;
+      virtualHosts = {
+        ${cfg.virtualHost} = {
+          root = "${pkgs.rss-bridge}";
+
+          locations."/" = {
+            tryFiles = "$uri /index.php$is_args$args";
+          };
+
+          locations."~ ^/index.php(/|$)" = {
+            extraConfig = ''
+              include ${config.services.nginx.package}/conf/fastcgi_params;
+              fastcgi_split_path_info ^(.+\.php)(/.+)$;
+              fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
+              fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+              fastcgi_param RSSBRIDGE_DATA ${cfg.dataDir};
+            '';
+          };
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/selfoss.nix b/nixos/modules/services/web-apps/selfoss.nix
new file mode 100644
index 00000000000..899976ac696
--- /dev/null
+++ b/nixos/modules/services/web-apps/selfoss.nix
@@ -0,0 +1,164 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.selfoss;
+
+  poolName = "selfoss_pool";
+
+  dataDir = "/var/lib/selfoss";
+
+  selfoss-config =
+  let
+    db_type = cfg.database.type;
+    default_port = if (db_type == "mysql") then 3306 else 5342;
+  in
+  pkgs.writeText "selfoss-config.ini" ''
+    [globals]
+    ${lib.optionalString (db_type != "sqlite") ''
+      db_type=${db_type}
+      db_host=${cfg.database.host}
+      db_database=${cfg.database.name}
+      db_username=${cfg.database.user}
+      db_password=${cfg.database.password}
+      db_port=${toString (if (cfg.database.port != null) then cfg.database.port
+                    else default_port)}
+    ''
+    }
+    ${cfg.extraConfig}
+  '';
+in
+  {
+    options = {
+      services.selfoss = {
+        enable = mkEnableOption "selfoss";
+
+        user = mkOption {
+          type = types.str;
+          default = "nginx";
+          description = ''
+            User account under which both the service and the web-application run.
+          '';
+        };
+
+        pool = mkOption {
+          type = types.str;
+          default = "${poolName}";
+          description = ''
+            Name of existing phpfpm pool that is used to run web-application.
+            If not specified a pool will be created automatically with
+            default values.
+          '';
+        };
+
+      database = {
+        type = mkOption {
+          type = types.enum ["pgsql" "mysql" "sqlite"];
+          default = "sqlite";
+          description = ''
+            Database to store feeds. Supported are sqlite, pgsql and mysql.
+          '';
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = ''
+            Host of the database (has no effect if type is "sqlite").
+          '';
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "tt_rss";
+          description = ''
+            Name of the existing database (has no effect if type is "sqlite").
+          '';
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "tt_rss";
+          description = ''
+            The database user. The user must exist and has access to
+            the specified database (has no effect if type is "sqlite").
+          '';
+        };
+
+        password = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            The database user's password (has no effect if type is "sqlite").
+          '';
+        };
+
+        port = mkOption {
+          type = types.nullOr types.int;
+          default = null;
+          description = ''
+            The database's port. If not set, the default ports will be
+            provided (5432 and 3306 for pgsql and mysql respectively)
+            (has no effect if type is "sqlite").
+          '';
+        };
+      };
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration added to config.ini
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
+      ${poolName} = {
+        user = "nginx";
+        settings = mapAttrs (name: mkDefault) {
+          "listen.owner" = "nginx";
+          "listen.group" = "nginx";
+          "listen.mode" = "0600";
+          "pm" = "dynamic";
+          "pm.max_children" = 75;
+          "pm.start_servers" = 10;
+          "pm.min_spare_servers" = 5;
+          "pm.max_spare_servers" = 20;
+          "pm.max_requests" = 500;
+          "catch_workers_output" = 1;
+        };
+      };
+    };
+
+    systemd.services.selfoss-config = {
+      serviceConfig.Type = "oneshot";
+      script = ''
+        mkdir -m 755 -p ${dataDir}
+        cd ${dataDir}
+
+        # Delete all but the "data" folder
+        ls | grep -v data | while read line; do rm -rf $line; done || true
+
+        # Create the files
+        cp -r "${pkgs.selfoss}/"* "${dataDir}"
+        ln -sf "${selfoss-config}" "${dataDir}/config.ini"
+        chown -R "${cfg.user}" "${dataDir}"
+        chmod -R 755 "${dataDir}"
+      '';
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    systemd.services.selfoss-update = {
+      serviceConfig = {
+        ExecStart = "${pkgs.php}/bin/php ${dataDir}/cliupdate.php";
+        User = "${cfg.user}";
+      };
+      startAt = "hourly";
+      after = [ "selfoss-config.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+    };
+
+  };
+}
diff --git a/nixos/modules/services/web-apps/shiori.nix b/nixos/modules/services/web-apps/shiori.nix
new file mode 100644
index 00000000000..bb2fc684e83
--- /dev/null
+++ b/nixos/modules/services/web-apps/shiori.nix
@@ -0,0 +1,96 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.shiori;
+in {
+  options = {
+    services.shiori = {
+      enable = mkEnableOption "Shiori simple bookmarks manager";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.shiori;
+        defaultText = literalExpression "pkgs.shiori";
+        description = "The Shiori package to use.";
+      };
+
+      address = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          The IP address on which Shiori will listen.
+          If empty, listens on all interfaces.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 8080;
+        description = "The port of the Shiori web application";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.shiori = with cfg; {
+      description = "Shiori simple bookmarks manager";
+      wantedBy = [ "multi-user.target" ];
+
+      environment.SHIORI_DIR = "/var/lib/shiori";
+
+      serviceConfig = {
+        ExecStart = "${package}/bin/shiori serve --address '${address}' --port '${toString port}'";
+
+        DynamicUser = true;
+        StateDirectory = "shiori";
+        # As the RootDirectory
+        RuntimeDirectory = "shiori";
+
+        # Security options
+
+        BindReadOnlyPaths = [
+          "/nix/store"
+
+          # For SSL certificates, and the resolv.conf
+          "/etc"
+        ];
+
+        CapabilityBoundingSet = "";
+
+        DeviceAllow = "";
+
+        LockPersonality = true;
+
+        MemoryDenyWriteExecute = true;
+
+        PrivateDevices = true;
+        PrivateUsers = true;
+
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+
+        RestrictNamespaces = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+
+        RootDirectory = "/run/shiori";
+
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [
+          "@system-service"
+          "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@resources" "~@setuid"
+        ];
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ minijackson ];
+}
diff --git a/nixos/modules/services/web-apps/sogo.nix b/nixos/modules/services/web-apps/sogo.nix
new file mode 100644
index 00000000000..4610bb96cb5
--- /dev/null
+++ b/nixos/modules/services/web-apps/sogo.nix
@@ -0,0 +1,271 @@
+{ config, pkgs, lib, ... }: with lib; let
+  cfg = config.services.sogo;
+
+  preStart = pkgs.writeShellScriptBin "sogo-prestart" ''
+    touch /etc/sogo/sogo.conf
+    chown sogo:sogo /etc/sogo/sogo.conf
+    chmod 640 /etc/sogo/sogo.conf
+
+    ${if (cfg.configReplaces != {}) then ''
+      # Insert secrets
+      ${concatStringsSep "\n" (mapAttrsToList (k: v: ''export ${k}="$(cat "${v}" | tr -d '\n')"'') cfg.configReplaces)}
+
+      ${pkgs.perl}/bin/perl -p ${concatStringsSep " " (mapAttrsToList (k: v: '' -e 's/${k}/''${ENV{"${k}"}}/g;' '') cfg.configReplaces)} /etc/sogo/sogo.conf.raw > /etc/sogo/sogo.conf
+    '' else ''
+      cp /etc/sogo/sogo.conf.raw /etc/sogo/sogo.conf
+    ''}
+  '';
+
+in {
+  options.services.sogo = with types; {
+    enable = mkEnableOption "SOGo groupware";
+
+    vhostName = mkOption {
+      description = "Name of the nginx vhost";
+      type = str;
+      default = "sogo";
+    };
+
+    timezone = mkOption {
+      description = "Timezone of your SOGo instance";
+      type = str;
+      example = "America/Montreal";
+    };
+
+    language = mkOption {
+      description = "Language of SOGo";
+      type = str;
+      default = "English";
+    };
+
+    ealarmsCredFile = mkOption {
+      description = "Optional path to a credentials file for email alarms";
+      type = nullOr str;
+      default = null;
+    };
+
+    configReplaces = mkOption {
+      description = ''
+        Replacement-filepath mapping for sogo.conf.
+        Every key is replaced with the contents of the file specified as value.
+
+        In the example, every occurence of LDAP_BINDPW will be replaced with the text of the
+        specified file.
+      '';
+      type = attrsOf str;
+      default = {};
+      example = {
+        LDAP_BINDPW = "/var/lib/secrets/sogo/ldappw";
+      };
+    };
+
+    extraConfig = mkOption {
+      description = "Extra sogo.conf configuration lines";
+      type = lines;
+      default = "";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.sogo ];
+
+    environment.etc."sogo/sogo.conf.raw".text = ''
+      {
+        // Mandatory parameters
+        SOGoTimeZone = "${cfg.timezone}";
+        SOGoLanguage = "${cfg.language}";
+        // Paths
+        WOSendMail = "/run/wrappers/bin/sendmail";
+        SOGoMailSpoolPath = "/var/lib/sogo/spool";
+        // Enable CSRF protection
+        SOGoXSRFValidationEnabled = YES;
+        // Remove dates from log (jornald does that)
+        NGLogDefaultLogEventFormatterClass = "NGLogEventFormatter";
+        // Extra config
+        ${cfg.extraConfig}
+      }
+    '';
+
+    systemd.services.sogo = {
+      description = "SOGo groupware";
+      after = [ "postgresql.service" "mysql.service" "memcached.service" "openldap.service" "dovecot2.service" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ config.environment.etc."sogo/sogo.conf.raw".source ];
+
+      environment.LDAPTLS_CACERT = "/etc/ssl/certs/ca-certificates.crt";
+
+      serviceConfig = {
+        Type = "forking";
+        ExecStartPre = "+" + preStart + "/bin/sogo-prestart";
+        ExecStart = "${pkgs.sogo}/bin/sogod -WOLogFile - -WOPidFile /run/sogo/sogo.pid";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        RuntimeDirectory = "sogo";
+        StateDirectory = "sogo/spool";
+
+        User = "sogo";
+        Group = "sogo";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+
+        LockPersonality = true;
+        RestrictRealtime = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+        MemoryDenyWriteExecute = true;
+        SystemCallFilter = "@basic-io @file-system @network-io @system-service @timer";
+        SystemCallArchitectures = "native";
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
+      };
+    };
+
+    systemd.services.sogo-tmpwatch = {
+      description = "SOGo tmpwatch";
+
+      startAt = [ "hourly" ];
+      script = ''
+        SOGOSPOOL=/var/lib/sogo/spool
+
+        find "$SOGOSPOOL" -type f -user sogo -atime +23 -delete > /dev/null
+        find "$SOGOSPOOL" -mindepth 1 -type d -user sogo -empty -delete > /dev/null
+      '';
+
+      serviceConfig = {
+        Type = "oneshot";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        StateDirectory = "sogo/spool";
+
+        User = "sogo";
+        Group = "sogo";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+
+        LockPersonality = true;
+        RestrictRealtime = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+        PrivateNetwork = true;
+        SystemCallFilter = "@basic-io @file-system @system-service";
+        SystemCallArchitectures = "native";
+        RestrictAddressFamilies = "";
+      };
+    };
+
+    systemd.services.sogo-ealarms = {
+      description = "SOGo email alarms";
+
+      after = [ "postgresql.service" "mysqld.service" "memcached.service" "openldap.service" "dovecot2.service" "sogo.service" ];
+      restartTriggers = [ config.environment.etc."sogo/sogo.conf.raw".source ];
+
+      startAt = [ "minutely" ];
+
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkgs.sogo}/bin/sogo-ealarms-notify${optionalString (cfg.ealarmsCredFile != null) " -p ${cfg.ealarmsCredFile}"}";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        StateDirectory = "sogo/spool";
+
+        User = "sogo";
+        Group = "sogo";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+
+        LockPersonality = true;
+        RestrictRealtime = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+        MemoryDenyWriteExecute = true;
+        SystemCallFilter = "@basic-io @file-system @network-io @system-service";
+        SystemCallArchitectures = "native";
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
+      };
+    };
+
+    # nginx vhost
+    services.nginx.virtualHosts."${cfg.vhostName}" = {
+      locations."/".extraConfig = ''
+        rewrite ^ https://$server_name/SOGo;
+        allow all;
+      '';
+
+      # For iOS 7
+      locations."/principals/".extraConfig = ''
+        rewrite ^ https://$server_name/SOGo/dav;
+        allow all;
+      '';
+
+      locations."^~/SOGo".extraConfig = ''
+        proxy_pass http://127.0.0.1:20000;
+        proxy_redirect http://127.0.0.1:20000 default;
+
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header Host $host;
+        proxy_set_header x-webobjects-server-protocol HTTP/1.0;
+        proxy_set_header x-webobjects-remote-host 127.0.0.1;
+        proxy_set_header x-webobjects-server-port $server_port;
+        proxy_set_header x-webobjects-server-name $server_name;
+        proxy_set_header x-webobjects-server-url $scheme://$host;
+        proxy_connect_timeout 90;
+        proxy_send_timeout 90;
+        proxy_read_timeout 90;
+        proxy_buffer_size 4k;
+        proxy_buffers 4 32k;
+        proxy_busy_buffers_size 64k;
+        proxy_temp_file_write_size 64k;
+        client_max_body_size 50m;
+        client_body_buffer_size 128k;
+        break;
+      '';
+
+      locations."/SOGo.woa/WebServerResources/".extraConfig = ''
+        alias ${pkgs.sogo}/lib/GNUstep/SOGo/WebServerResources/;
+        allow all;
+      '';
+
+      locations."/SOGo/WebServerResources/".extraConfig = ''
+        alias ${pkgs.sogo}/lib/GNUstep/SOGo/WebServerResources/;
+        allow all;
+      '';
+
+      locations."~ ^/SOGo/so/ControlPanel/Products/([^/]*)/Resources/(.*)$".extraConfig = ''
+        alias ${pkgs.sogo}/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
+      '';
+
+      locations."~ ^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\\.(jpg|png|gif|css|js)$".extraConfig = ''
+        alias ${pkgs.sogo}/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
+      '';
+    };
+
+    # User and group
+    users.groups.sogo = {};
+    users.users.sogo = {
+      group = "sogo";
+      isSystemUser = true;
+      description = "SOGo service user";
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/timetagger.nix b/nixos/modules/services/web-apps/timetagger.nix
new file mode 100644
index 00000000000..373f4fcd52f
--- /dev/null
+++ b/nixos/modules/services/web-apps/timetagger.nix
@@ -0,0 +1,80 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkEnableOption mkIf mkOption types literalExpression;
+
+  cfg = config.services.timetagger;
+in {
+
+  options = {
+    services.timetagger = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Tag your time, get the insight
+
+          <note><para>
+            This app does not do authentication.
+            You must setup authentication yourself or run it in an environment where
+            only allowed users have access.
+          </para></note>
+        '';
+      };
+
+      bindAddr = mkOption {
+        description = "Address to bind to.";
+        type = types.str;
+        default = "127.0.0.1";
+      };
+
+      port = mkOption {
+        description = "Port to bind to.";
+        type = types.port;
+        default = 8080;
+      };
+
+      package = mkOption {
+        description = ''
+          Use own package for starting timetagger web application.
+
+          The ${literalExpression ''pkgs.timetagger''} package only provides a
+          "run.py" script for the actual package
+          ${literalExpression ''pkgs.python3Packages.timetagger''}.
+
+          If you want to provide a "run.py" script for starting timetagger
+          yourself, you can do so with this option.
+          If you do so, the 'bindAddr' and 'port' options are ignored.
+        '';
+
+        default = pkgs.timetagger.override { addr = cfg.bindAddr; port = cfg.port; };
+        defaultText = literalExpression ''
+          pkgs.timetagger.override {
+            addr = ${cfg.bindAddr};
+            port = ${cfg.port};
+          };
+        '';
+        type = types.package;
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.timetagger = {
+      description = "Timetagger service";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        User = "timetagger";
+        Group = "timetagger";
+        StateDirectory = "timetagger";
+
+        ExecStart = "${cfg.package}/bin/timetagger";
+
+        Restart = "on-failure";
+        RestartSec = 1;
+      };
+    };
+  };
+}
+
diff --git a/nixos/modules/services/web-apps/trilium.nix b/nixos/modules/services/web-apps/trilium.nix
new file mode 100644
index 00000000000..35383c992fe
--- /dev/null
+++ b/nixos/modules/services/web-apps/trilium.nix
@@ -0,0 +1,146 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.trilium-server;
+  configIni = pkgs.writeText "trilium-config.ini" ''
+    [General]
+    # Instance name can be used to distinguish between different instances
+    instanceName=${cfg.instanceName}
+
+    # Disable automatically generating desktop icon
+    noDesktopIcon=true
+    noBackup=${lib.boolToString cfg.noBackup}
+
+    [Network]
+    # host setting is relevant only for web deployments - set the host on which the server will listen
+    host=${cfg.host}
+    # port setting is relevant only for web deployments, desktop builds run on random free port
+    port=${toString cfg.port}
+    # true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
+    https=false
+  '';
+in
+{
+
+  options.services.trilium-server = with lib; {
+    enable = mkEnableOption "trilium-server";
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/trilium";
+      description = ''
+        The directory storing the notes database and the configuration.
+      '';
+    };
+
+    instanceName = mkOption {
+      type = types.str;
+      default = "Trilium";
+      description = ''
+        Instance name used to distinguish between different instances
+      '';
+    };
+
+    noBackup = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Disable periodic database backups.
+      '';
+    };
+
+    host = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = ''
+        The host address to bind to (defaults to localhost).
+      '';
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = 8080;
+      description = ''
+        The port number to bind to.
+      '';
+    };
+
+    nginx = mkOption {
+      default = {};
+      description = ''
+        Configuration for nginx reverse proxy.
+      '';
+
+      type = types.submodule {
+        options = {
+          enable = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Configure the nginx reverse proxy settings.
+            '';
+          };
+
+          hostName = mkOption {
+            type = types.str;
+            description = ''
+              The hostname use to setup the virtualhost configuration
+            '';
+          };
+        };
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable (lib.mkMerge [
+  {
+    meta.maintainers = with lib.maintainers; [ fliegendewurst ];
+
+    users.groups.trilium = {};
+    users.users.trilium = {
+      description = "Trilium User";
+      group = "trilium";
+      home = cfg.dataDir;
+      isSystemUser = true;
+    };
+
+    systemd.services.trilium-server = {
+      wantedBy = [ "multi-user.target" ];
+      environment.TRILIUM_DATA_DIR = cfg.dataDir;
+      serviceConfig = {
+        ExecStart = "${pkgs.trilium-server}/bin/trilium-server";
+        User = "trilium";
+        Group = "trilium";
+        PrivateTmp = "true";
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d  ${cfg.dataDir}            0750 trilium trilium - -"
+      "L+ ${cfg.dataDir}/config.ini -    -       -       - ${configIni}"
+    ];
+
+  }
+
+  (lib.mkIf cfg.nginx.enable {
+    services.nginx = {
+      enable = true;
+      virtualHosts."${cfg.nginx.hostName}" = {
+        locations."/" = {
+          proxyPass = "http://${cfg.host}:${toString cfg.port}/";
+          extraConfig = ''
+            proxy_http_version 1.1;
+            proxy_set_header Upgrade $http_upgrade;
+            proxy_set_header Connection 'upgrade';
+            proxy_set_header Host $host;
+            proxy_cache_bypass $http_upgrade;
+          '';
+        };
+        extraConfig = ''
+          client_max_body_size 0;
+        '';
+      };
+    };
+  })
+  ]);
+}
diff --git a/nixos/modules/services/web-apps/tt-rss.nix b/nixos/modules/services/web-apps/tt-rss.nix
new file mode 100644
index 00000000000..9aa38ab25c9
--- /dev/null
+++ b/nixos/modules/services/web-apps/tt-rss.nix
@@ -0,0 +1,686 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.tt-rss;
+
+  configVersion = 26;
+
+  dbPort = if cfg.database.port == null
+    then (if cfg.database.type == "pgsql" then 5432 else 3306)
+    else cfg.database.port;
+
+  poolName = "tt-rss";
+
+  mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
+  pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
+
+  tt-rss-config = let
+    password =
+      if (cfg.database.password != null) then
+        "'${(escape ["'" "\\"] cfg.database.password)}'"
+      else if (cfg.database.passwordFile != null) then
+        "file_get_contents('${cfg.database.passwordFile}')"
+      else
+        null
+      ;
+  in pkgs.writeText "config.php" ''
+    <?php
+      putenv('TTRSS_PHP_EXECUTABLE=${pkgs.php}/bin/php');
+
+      putenv('TTRSS_LOCK_DIRECTORY=${cfg.root}/lock');
+      putenv('TTRSS_CACHE_DIR=${cfg.root}/cache');
+      putenv('TTRSS_ICONS_DIR=${cfg.root}/feed-icons');
+      putenv('TTRSS_ICONS_URL=feed-icons');
+      putenv('TTRSS_SELF_URL_PATH=${cfg.selfUrlPath}');
+
+      putenv('TTRSS_MYSQL_CHARSET=UTF8');
+
+      putenv('TTRSS_DB_TYPE=${cfg.database.type}');
+      putenv('TTRSS_DB_HOST=${optionalString (cfg.database.host != null) cfg.database.host}');
+      putenv('TTRSS_DB_USER=${cfg.database.user}');
+      putenv('TTRSS_DB_NAME=${cfg.database.name}');
+      putenv('TTRSS_DB_PASS=' ${optionalString (password != null) ". ${password}"});
+      putenv('TTRSS_DB_PORT=${toString dbPort}');
+
+      putenv('TTRSS_AUTH_AUTO_CREATE=${boolToString cfg.auth.autoCreate}');
+      putenv('TTRSS_AUTH_AUTO_LOGIN=${boolToString cfg.auth.autoLogin}');
+
+      putenv('TTRSS_FEED_CRYPT_KEY=${escape ["'" "\\"] cfg.feedCryptKey}');
+
+
+      putenv('TTRSS_SINGLE_USER_MODE=${boolToString cfg.singleUserMode}');
+
+      putenv('TTRSS_SIMPLE_UPDATE_MODE=${boolToString cfg.simpleUpdateMode}');
+
+      # Never check for updates - the running version of the code should
+      # be controlled entirely by the version of TT-RSS active in the
+      # current Nix profile. If TT-RSS updates itself to a version
+      # requiring a database schema upgrade, and then the SystemD
+      # tt-rss.service is restarted, the old code copied from the Nix
+      # store will overwrite the updated version, causing the code to
+      # detect the need for a schema "upgrade" (since the schema version
+      # in the database is different than in the code), but the update
+      # schema operation in TT-RSS will do nothing because the schema
+      # version in the database is newer than that in the code.
+      putenv('TTRSS_CHECK_FOR_UPDATES=false');
+
+      putenv('TTRSS_FORCE_ARTICLE_PURGE=${toString cfg.forceArticlePurge}');
+      putenv('TTRSS_SESSION_COOKIE_LIFETIME=${toString cfg.sessionCookieLifetime}');
+      putenv('TTRSS_ENABLE_GZIP_OUTPUT=${boolToString cfg.enableGZipOutput}');
+
+      putenv('TTRSS_PLUGINS=${builtins.concatStringsSep "," cfg.plugins}');
+
+      putenv('TTRSS_LOG_DESTINATION=${cfg.logDestination}');
+      putenv('TTRSS_CONFIG_VERSION=${toString configVersion}');
+
+
+      putenv('TTRSS_PUBSUBHUBBUB_ENABLED=${boolToString cfg.pubSubHubbub.enable}');
+      putenv('TTRSS_PUBSUBHUBBUB_HUB=${cfg.pubSubHubbub.hub}');
+
+      putenv('TTRSS_SPHINX_SERVER=${cfg.sphinx.server}');
+      putenv('TTRSS_SPHINX_INDEX=${builtins.concatStringsSep "," cfg.sphinx.index}');
+
+      putenv('TTRSS_ENABLE_REGISTRATION=${boolToString cfg.registration.enable}');
+      putenv('TTRSS_REG_NOTIFY_ADDRESS=${cfg.registration.notifyAddress}');
+      putenv('TTRSS_REG_MAX_USERS=${toString cfg.registration.maxUsers}');
+
+      putenv('TTRSS_SMTP_SERVER=${cfg.email.server}');
+      putenv('TTRSS_SMTP_LOGIN=${cfg.email.login}');
+      putenv('TTRSS_SMTP_PASSWORD=${escape ["'" "\\"] cfg.email.password}');
+      putenv('TTRSS_SMTP_SECURE=${cfg.email.security}');
+
+      putenv('TTRSS_SMTP_FROM_NAME=${escape ["'" "\\"] cfg.email.fromName}');
+      putenv('TTRSS_SMTP_FROM_ADDRESS=${escape ["'" "\\"] cfg.email.fromAddress}');
+      putenv('TTRSS_DIGEST_SUBJECT=${escape ["'" "\\"] cfg.email.digestSubject}');
+
+      ${cfg.extraConfig}
+  '';
+
+  # tt-rss and plugins and themes and config.php
+  servedRoot = pkgs.runCommand "tt-rss-served-root" {} ''
+    cp --no-preserve=mode -r ${pkgs.tt-rss} $out
+    cp ${tt-rss-config} $out/config.php
+    ${optionalString (cfg.pluginPackages != []) ''
+    for plugin in ${concatStringsSep " " cfg.pluginPackages}; do
+    cp -r "$plugin"/* "$out/plugins.local/"
+    done
+    ''}
+    ${optionalString (cfg.themePackages != []) ''
+    for theme in ${concatStringsSep " " cfg.themePackages}; do
+    cp -r "$theme"/* "$out/themes.local/"
+    done
+    ''}
+  '';
+
+ in {
+
+  ###### interface
+
+  options = {
+
+    services.tt-rss = {
+
+      enable = mkEnableOption "tt-rss";
+
+      root = mkOption {
+        type = types.path;
+        default = "/var/lib/tt-rss";
+        description = ''
+          Root of the application.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "tt_rss";
+        description = ''
+          User account under which both the update daemon and the web-application run.
+        '';
+      };
+
+      pool = mkOption {
+        type = types.str;
+        default = "${poolName}";
+        description = ''
+          Name of existing phpfpm pool that is used to run web-application.
+          If not specified a pool will be created automatically with
+          default values.
+        '';
+      };
+
+      virtualHost = mkOption {
+        type = types.nullOr types.str;
+        default = "tt-rss";
+        description = ''
+          Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
+        '';
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum ["pgsql" "mysql"];
+          default = "pgsql";
+          description = ''
+            Database to store feeds. Supported are pgsql and mysql.
+          '';
+        };
+
+        host = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            Host of the database. Leave null to use Unix domain socket.
+          '';
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "tt_rss";
+          description = ''
+            Name of the existing database.
+          '';
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "tt_rss";
+          description = ''
+            The database user. The user must exist and has access to
+            the specified database.
+          '';
+        };
+
+        password = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            The database user's password.
+          '';
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            The database user's password.
+          '';
+        };
+
+        port = mkOption {
+          type = types.nullOr types.int;
+          default = null;
+          description = ''
+            The database's port. If not set, the default ports will be provided (5432
+            and 3306 for pgsql and mysql respectively).
+          '';
+        };
+
+        createLocally = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Create the database and database user locally.";
+        };
+      };
+
+      auth = {
+        autoCreate = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Allow authentication modules to auto-create users in tt-rss internal
+            database when authenticated successfully.
+          '';
+        };
+
+        autoLogin = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Automatically login user on remote or other kind of externally supplied
+            authentication, otherwise redirect to login form as normal.
+            If set to true, users won't be able to set application language
+            and settings profile.
+          '';
+        };
+      };
+
+      pubSubHubbub = {
+        hub = mkOption {
+          type = types.str;
+          default = "";
+          description = ''
+            URL to a PubSubHubbub-compatible hub server. If defined, "Published
+            articles" generated feed would automatically become PUSH-enabled.
+          '';
+        };
+
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Enable client PubSubHubbub support in tt-rss. When disabled, tt-rss
+            won't try to subscribe to PUSH feed updates.
+          '';
+        };
+      };
+
+      sphinx = {
+        server = mkOption {
+          type = types.str;
+          default = "localhost:9312";
+          description = ''
+            Hostname:port combination for the Sphinx server.
+          '';
+        };
+
+        index = mkOption {
+          type = types.listOf types.str;
+          default = ["ttrss" "delta"];
+          description = ''
+            Index names in Sphinx configuration. Example configuration
+            files are available on tt-rss wiki.
+          '';
+        };
+      };
+
+      registration = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Allow users to register themselves. Please be aware that allowing
+            random people to access your tt-rss installation is a security risk
+            and potentially might lead to data loss or server exploit. Disabled
+            by default.
+          '';
+        };
+
+        notifyAddress = mkOption {
+          type = types.str;
+          default = "";
+          description = ''
+            Email address to send new user notifications to.
+          '';
+        };
+
+        maxUsers = mkOption {
+          type = types.int;
+          default = 0;
+          description = ''
+            Maximum amount of users which will be allowed to register on this
+            system. 0 - no limit.
+          '';
+        };
+      };
+
+      email = {
+        server = mkOption {
+          type = types.str;
+          default = "";
+          example = "localhost:25";
+          description = ''
+            Hostname:port combination to send outgoing mail. Blank - use system
+            MTA.
+          '';
+        };
+
+        login = mkOption {
+          type = types.str;
+          default = "";
+          description = ''
+            SMTP authentication login used when sending outgoing mail.
+          '';
+        };
+
+        password = mkOption {
+          type = types.str;
+          default = "";
+          description = ''
+            SMTP authentication password used when sending outgoing mail.
+          '';
+        };
+
+        security = mkOption {
+          type = types.enum ["" "ssl" "tls"];
+          default = "";
+          description = ''
+            Used to select a secure SMTP connection. Allowed values: ssl, tls,
+            or empty.
+          '';
+        };
+
+        fromName = mkOption {
+          type = types.str;
+          default = "Tiny Tiny RSS";
+          description = ''
+            Name for sending outgoing mail. This applies to password reset
+            notifications, digest emails and any other mail.
+          '';
+        };
+
+        fromAddress = mkOption {
+          type = types.str;
+          default = "";
+          description = ''
+            Address for sending outgoing mail. This applies to password reset
+            notifications, digest emails and any other mail.
+          '';
+        };
+
+        digestSubject = mkOption {
+          type = types.str;
+          default = "[tt-rss] New headlines for last 24 hours";
+          description = ''
+            Subject line for email digests.
+          '';
+        };
+      };
+
+      sessionCookieLifetime = mkOption {
+        type = types.int;
+        default = 86400;
+        description = ''
+          Default lifetime of a session (e.g. login) cookie. In seconds,
+          0 means cookie will be deleted when browser closes.
+        '';
+      };
+
+      selfUrlPath = mkOption {
+        type = types.str;
+        description = ''
+          Full URL of your tt-rss installation. This should be set to the
+          location of tt-rss directory, e.g. http://example.org/tt-rss/
+          You need to set this option correctly otherwise several features
+          including PUSH, bookmarklets and browser integration will not work properly.
+        '';
+        example = "http://localhost";
+      };
+
+      feedCryptKey = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Key used for encryption of passwords for password-protected feeds
+          in the database. A string of 24 random characters. If left blank, encryption
+          is not used. Requires mcrypt functions.
+          Warning: changing this key will make your stored feed passwords impossible
+          to decrypt.
+        '';
+      };
+
+      singleUserMode = mkOption {
+        type = types.bool;
+        default = false;
+
+        description = ''
+          Operate in single user mode, disables all functionality related to
+          multiple users and authentication. Enabling this assumes you have
+          your tt-rss directory protected by other means (e.g. http auth).
+        '';
+      };
+
+      simpleUpdateMode = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enables fallback update mode where tt-rss tries to update feeds in
+          background while tt-rss is open in your browser.
+          If you don't have a lot of feeds and don't want to or can't run
+          background processes while not running tt-rss, this method is generally
+          viable to keep your feeds up to date.
+          Still, there are more robust (and recommended) updating methods
+          available, you can read about them here: http://tt-rss.org/wiki/UpdatingFeeds
+        '';
+      };
+
+      forceArticlePurge = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          When this option is not 0, users ability to control feed purging
+          intervals is disabled and all articles (which are not starred)
+          older than this amount of days are purged.
+        '';
+      };
+
+      enableGZipOutput = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Selectively gzip output to improve wire performance. This requires
+          PHP Zlib extension on the server.
+          Enabling this can break tt-rss in several httpd/php configurations,
+          if you experience weird errors and tt-rss failing to start, blank pages
+          after login, or content encoding errors, disable it.
+        '';
+      };
+
+      plugins = mkOption {
+        type = types.listOf types.str;
+        default = ["auth_internal" "note"];
+        description = ''
+          List of plugins to load automatically for all users.
+          System plugins have to be specified here. Please enable at least one
+          authentication plugin here (auth_*).
+          Users may enable other user plugins from Preferences/Plugins but may not
+          disable plugins specified in this list.
+          Disabling auth_internal in this list would automatically disable
+          reset password link on the login form.
+        '';
+      };
+
+      pluginPackages = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        description = ''
+          List of plugins to install. The list elements are expected to
+          be derivations. All elements in this derivation are automatically
+          copied to the <literal>plugins.local</literal> directory.
+        '';
+      };
+
+      themePackages = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        description = ''
+          List of themes to install. The list elements are expected to
+          be derivations. All elements in this derivation are automatically
+          copied to the <literal>themes.local</literal> directory.
+        '';
+      };
+
+      logDestination = mkOption {
+        type = types.enum ["" "sql" "syslog"];
+        default = "sql";
+        description = ''
+          Log destination to use. Possible values: sql (uses internal logging
+          you can read in Preferences -> System), syslog - logs to system log.
+          Setting this to blank uses PHP logging (usually to http server
+          error.log).
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Additional lines to append to <literal>config.php</literal>.
+        '';
+      };
+    };
+  };
+
+  imports = [
+    (mkRemovedOptionModule ["services" "tt-rss" "checkForUpdates"] ''
+      This option was removed because setting this to true will cause TT-RSS
+      to be unable to start if an automatic update of the code in
+      services.tt-rss.root leads to a database schema upgrade that is not
+      supported by the code active in the Nix store.
+    '')
+  ];
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = cfg.database.password != null -> cfg.database.passwordFile == null;
+        message = "Cannot set both password and passwordFile";
+      }
+    ];
+
+    services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
+      ${poolName} = {
+        inherit (cfg) user;
+        settings = mapAttrs (name: mkDefault) {
+          "listen.owner" = "nginx";
+          "listen.group" = "nginx";
+          "listen.mode" = "0600";
+          "pm" = "dynamic";
+          "pm.max_children" = 75;
+          "pm.start_servers" = 10;
+          "pm.min_spare_servers" = 5;
+          "pm.max_spare_servers" = 20;
+          "pm.max_requests" = 500;
+          "catch_workers_output" = 1;
+        };
+      };
+    };
+
+    # NOTE: No configuration is done if not using virtual host
+    services.nginx = mkIf (cfg.virtualHost != null) {
+      enable = true;
+      virtualHosts = {
+        ${cfg.virtualHost} = {
+          root = "${cfg.root}/www";
+
+          locations."/" = {
+            index = "index.php";
+          };
+
+          locations."^~ /feed-icons" = {
+            root = "${cfg.root}";
+          };
+
+          locations."~ \\.php$" = {
+            extraConfig = ''
+              fastcgi_split_path_info ^(.+\.php)(/.+)$;
+              fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
+              fastcgi_index index.php;
+            '';
+          };
+        };
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.root}' 0555 ${cfg.user} tt_rss - -"
+      "d '${cfg.root}/lock' 0755 ${cfg.user} tt_rss - -"
+      "d '${cfg.root}/cache' 0755 ${cfg.user} tt_rss - -"
+      "d '${cfg.root}/cache/upload' 0755 ${cfg.user} tt_rss - -"
+      "d '${cfg.root}/cache/images' 0755 ${cfg.user} tt_rss - -"
+      "d '${cfg.root}/cache/export' 0755 ${cfg.user} tt_rss - -"
+      "d '${cfg.root}/feed-icons' 0755 ${cfg.user} tt_rss - -"
+      "L+ '${cfg.root}/www' - - - - ${servedRoot}"
+    ];
+
+    systemd.services = {
+      phpfpm-tt-rss = mkIf (cfg.pool == "${poolName}") {
+        restartTriggers = [ servedRoot ];
+      };
+
+      tt-rss = {
+        description = "Tiny Tiny RSS feeds update daemon";
+
+        preStart = let
+          callSql = e:
+              if cfg.database.type == "pgsql" then ''
+                  ${optionalString (cfg.database.password != null) "PGPASSWORD=${cfg.database.password}"} \
+                  ${optionalString (cfg.database.passwordFile != null) "PGPASSWORD=$(cat ${cfg.database.passwordFile})"} \
+                  ${config.services.postgresql.package}/bin/psql \
+                    -U ${cfg.database.user} \
+                    ${optionalString (cfg.database.host != null) "-h ${cfg.database.host} --port ${toString dbPort}"} \
+                    -c '${e}' \
+                    ${cfg.database.name}''
+
+              else if cfg.database.type == "mysql" then ''
+                  echo '${e}' | ${config.services.mysql.package}/bin/mysql \
+                    -u ${cfg.database.user} \
+                    ${optionalString (cfg.database.password != null) "-p${cfg.database.password}"} \
+                    ${optionalString (cfg.database.host != null) "-h ${cfg.database.host} -P ${toString dbPort}"} \
+                    ${cfg.database.name}''
+
+              else "";
+
+        in (optionalString (cfg.database.type == "pgsql") ''
+          exists=$(${callSql "select count(*) > 0 from pg_tables where tableowner = user"} \
+          | tail -n+3 | head -n-2 | sed -e 's/[ \n\t]*//')
+
+          if [ "$exists" == 'f' ]; then
+            ${callSql "\\i ${pkgs.tt-rss}/schema/ttrss_schema_${cfg.database.type}.sql"}
+          else
+            echo 'The database contains some data. Leaving it as it is.'
+          fi;
+        '')
+
+        + (optionalString (cfg.database.type == "mysql") ''
+          exists=$(${callSql "select count(*) > 0 from information_schema.tables where table_schema = schema()"} \
+          | tail -n+2 | sed -e 's/[ \n\t]*//')
+
+          if [ "$exists" == '0' ]; then
+            ${callSql "\\. ${pkgs.tt-rss}/schema/ttrss_schema_${cfg.database.type}.sql"}
+          else
+            echo 'The database contains some data. Leaving it as it is.'
+          fi;
+        '');
+
+        serviceConfig = {
+          User = "${cfg.user}";
+          Group = "tt_rss";
+          ExecStart = "${pkgs.php}/bin/php ${cfg.root}/www/update.php --daemon --quiet";
+          Restart = "on-failure";
+          RestartSec = "60";
+          SyslogIdentifier = "tt-rss";
+        };
+
+        wantedBy = [ "multi-user.target" ];
+        requires = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
+        after = [ "network.target" ] ++ optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
+      };
+    };
+
+    services.mysql = mkIf mysqlLocal {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        {
+          name = cfg.user;
+          ensurePermissions = {
+            "${cfg.database.name}.*" = "ALL PRIVILEGES";
+          };
+        }
+      ];
+    };
+
+    services.postgresql = mkIf pgsqlLocal {
+      enable = mkDefault true;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.user;
+          ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    users.users.tt_rss = optionalAttrs (cfg.user == "tt_rss") {
+      description = "tt-rss service user";
+      isSystemUser = true;
+      group = "tt_rss";
+    };
+
+    users.groups.tt_rss = {};
+  };
+}
diff --git a/nixos/modules/services/web-apps/vikunja.nix b/nixos/modules/services/web-apps/vikunja.nix
new file mode 100644
index 00000000000..7575e96ca81
--- /dev/null
+++ b/nixos/modules/services/web-apps/vikunja.nix
@@ -0,0 +1,145 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.vikunja;
+  format = pkgs.formats.yaml {};
+  configFile = format.generate "config.yaml" cfg.settings;
+  useMysql = cfg.database.type == "mysql";
+  usePostgresql = cfg.database.type == "postgres";
+in {
+  options.services.vikunja = with lib; {
+    enable = mkEnableOption "vikunja service";
+    package-api = mkOption {
+      default = pkgs.vikunja-api;
+      type = types.package;
+      defaultText = literalExpression "pkgs.vikunja-api";
+      description = "vikunja-api derivation to use.";
+    };
+    package-frontend = mkOption {
+      default = pkgs.vikunja-frontend;
+      type = types.package;
+      defaultText = literalExpression "pkgs.vikunja-frontend";
+      description = "vikunja-frontend derivation to use.";
+    };
+    environmentFiles = mkOption {
+      type = types.listOf types.path;
+      default = [ ];
+      description = ''
+        List of environment files set in the vikunja systemd service.
+        For example passwords should be set in one of these files.
+      '';
+    };
+    setupNginx = mkOption {
+      type = types.bool;
+      default = config.services.nginx.enable;
+      defaultText = literalExpression "config.services.nginx.enable";
+      description = ''
+        Whether to setup NGINX.
+        Further nginx configuration can be done by changing
+        <option>services.nginx.virtualHosts.&lt;frontendHostname&gt;</option>.
+        This does not enable TLS or ACME by default. To enable this, set the
+        <option>services.nginx.virtualHosts.&lt;frontendHostname&gt;.enableACME</option> to
+        <literal>true</literal> and if appropriate do the same for
+        <option>services.nginx.virtualHosts.&lt;frontendHostname&gt;.forceSSL</option>.
+      '';
+    };
+    frontendScheme = mkOption {
+      type = types.enum [ "http" "https" ];
+      description = ''
+        Whether the site is available via http or https.
+        This does not configure https or ACME in nginx!
+      '';
+    };
+    frontendHostname = mkOption {
+      type = types.str;
+      description = "The Hostname under which the frontend is running.";
+    };
+
+    settings = mkOption {
+      type = format.type;
+      default = {};
+      description = ''
+        Vikunja configuration. Refer to
+        <link xlink:href="https://vikunja.io/docs/config-options/"/>
+        for details on supported values.
+        '';
+    };
+    database = {
+      type = mkOption {
+        type = types.enum [ "sqlite" "mysql" "postgres" ];
+        example = "postgres";
+        default = "sqlite";
+        description = "Database engine to use.";
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Database host address. Can also be a socket.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = "vikunja";
+        description = "Database user.";
+      };
+      database = mkOption {
+        type = types.str;
+        default = "vikunja";
+        description = "Database name.";
+      };
+      path = mkOption {
+        type = types.str;
+        default = "/var/lib/vikunja/vikunja.db";
+        description = "Path to the sqlite3 database file.";
+      };
+    };
+  };
+  config = lib.mkIf cfg.enable {
+    services.vikunja.settings = {
+      database = {
+        inherit (cfg.database) type host user database path;
+      };
+      service = {
+        frontendurl = "${cfg.frontendScheme}://${cfg.frontendHostname}/";
+      };
+      files = {
+        basepath = "/var/lib/vikunja/files";
+      };
+    };
+
+    systemd.services.vikunja-api = {
+      description = "vikunja-api";
+      after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
+      wantedBy = [ "multi-user.target" ];
+      path = [ cfg.package-api ];
+      restartTriggers = [ configFile ];
+
+      serviceConfig = {
+        Type = "simple";
+        DynamicUser = true;
+        StateDirectory = "vikunja";
+        ExecStart = "${cfg.package-api}/bin/vikunja";
+        Restart = "always";
+        EnvironmentFile = cfg.environmentFiles;
+      };
+    };
+
+    services.nginx.virtualHosts."${cfg.frontendHostname}" = mkIf cfg.setupNginx {
+      locations = {
+        "/" = {
+          root = cfg.package-frontend;
+          tryFiles = "try_files $uri $uri/ /";
+        };
+        "~* ^/(api|dav|\\.well-known)/" = {
+          proxyPass = "http://localhost:3456";
+          extraConfig = ''
+            client_max_body_size 20M;
+          '';
+        };
+      };
+    };
+
+    environment.etc."vikunja/config.yaml".source = configFile;
+  };
+}
diff --git a/nixos/modules/services/web-apps/virtlyst.nix b/nixos/modules/services/web-apps/virtlyst.nix
new file mode 100644
index 00000000000..37bdbb0e3b4
--- /dev/null
+++ b/nixos/modules/services/web-apps/virtlyst.nix
@@ -0,0 +1,73 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.virtlyst;
+  stateDir = "/var/lib/virtlyst";
+
+  ini = pkgs.writeText "virtlyst-config.ini" ''
+    [wsgi]
+    master = true
+    threads = auto
+    http-socket = ${cfg.httpSocket}
+    application = ${pkgs.virtlyst}/lib/libVirtlyst.so
+    chdir2 = ${stateDir}
+    static-map = /static=${pkgs.virtlyst}/root/static
+
+    [Cutelyst]
+    production = true
+    DatabasePath = virtlyst.sqlite
+    TemplatePath = ${pkgs.virtlyst}/root/src
+
+    [Rules]
+    cutelyst.* = true
+    virtlyst.* = true
+  '';
+
+in
+
+{
+
+  options.services.virtlyst = {
+    enable = mkEnableOption "Virtlyst libvirt web interface";
+
+    adminPassword = mkOption {
+      type = types.str;
+      description = ''
+        Initial admin password with which the database will be seeded.
+      '';
+    };
+
+    httpSocket = mkOption {
+      type = types.str;
+      default = "localhost:3000";
+      description = ''
+        IP and/or port to which to bind the http socket.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.virtlyst = {
+      home = stateDir;
+      createHome = true;
+      group = mkIf config.virtualisation.libvirtd.enable "libvirtd";
+      isSystemUser = true;
+    };
+
+    systemd.services.virtlyst = {
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        VIRTLYST_ADMIN_PASSWORD = cfg.adminPassword;
+      };
+      serviceConfig = {
+        ExecStart = "${pkgs.cutelyst}/bin/cutelyst-wsgi2 --ini ${ini}";
+        User = "virtlyst";
+        WorkingDirectory = stateDir;
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/services/web-apps/whitebophir.nix b/nixos/modules/services/web-apps/whitebophir.nix
new file mode 100644
index 00000000000..f9db6fe379b
--- /dev/null
+++ b/nixos/modules/services/web-apps/whitebophir.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.whitebophir;
+in {
+  options = {
+    services.whitebophir = {
+      enable = mkEnableOption "whitebophir, an online collaborative whiteboard server (persistent state will be maintained under <filename>/var/lib/whitebophir</filename>)";
+
+      package = mkOption {
+        default = pkgs.whitebophir;
+        defaultText = literalExpression "pkgs.whitebophir";
+        type = types.package;
+        description = "Whitebophir package to use.";
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "Address to listen on (use 0.0.0.0 to allow access from any address).";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 5001;
+        description = "Port to bind to.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.whitebophir = {
+      description = "Whitebophir Service";
+      wantedBy    = [ "multi-user.target" ];
+      after       = [ "network.target" ];
+      environment = {
+        PORT            = toString cfg.port;
+        HOST            = toString cfg.listenAddress;
+        WBO_HISTORY_DIR = "/var/lib/whitebophir";
+      };
+
+      serviceConfig = {
+        DynamicUser    = true;
+        ExecStart      = "${cfg.package}/bin/whitebophir";
+        Restart        = "always";
+        StateDirectory = "whitebophir";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/wiki-js.nix b/nixos/modules/services/web-apps/wiki-js.nix
new file mode 100644
index 00000000000..1a6259dffee
--- /dev/null
+++ b/nixos/modules/services/web-apps/wiki-js.nix
@@ -0,0 +1,139 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.wiki-js;
+
+  format = pkgs.formats.json { };
+
+  configFile = format.generate "wiki-js.yml" cfg.settings;
+in {
+  options.services.wiki-js = {
+    enable = mkEnableOption "wiki-js";
+
+    environmentFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/root/wiki-js.env";
+      description = ''
+        Environment fiel to inject e.g. secrets into the configuration.
+      '';
+    };
+
+    stateDirectoryName = mkOption {
+      default = "wiki-js";
+      type = types.str;
+      description = ''
+        Name of the directory in <filename>/var/lib</filename>.
+      '';
+    };
+
+    settings = mkOption {
+      default = {};
+      type = types.submodule {
+        freeformType = format.type;
+        options = {
+          port = mkOption {
+            type = types.port;
+            default = 3000;
+            description = ''
+              TCP port the process should listen to.
+            '';
+          };
+
+          bindIP = mkOption {
+            default = "0.0.0.0";
+            type = types.str;
+            description = ''
+              IPs the service should listen to.
+            '';
+          };
+
+          db = {
+            type = mkOption {
+              default = "postgres";
+              type = types.enum [ "postgres" "mysql" "mariadb" "mssql" ];
+              description = ''
+                Database driver to use for persistence. Please note that <literal>sqlite</literal>
+                is currently not supported as the build process for it is currently not implemented
+                in <package>pkgs.wiki-js</package> and it's not recommended by upstream for
+                production use.
+              '';
+            };
+            host = mkOption {
+              type = types.str;
+              example = "/run/postgresql";
+              description = ''
+                Hostname or socket-path to connect to.
+              '';
+            };
+            db = mkOption {
+              default = "wiki";
+              type = types.str;
+              description = ''
+                Name of the database to use.
+              '';
+            };
+          };
+
+          logLevel = mkOption {
+            default = "info";
+            type = types.enum [ "error" "warn" "info" "verbose" "debug" "silly" ];
+            description = ''
+              Define how much detail is supposed to be logged at runtime.
+            '';
+          };
+
+          offline = mkEnableOption "offline mode" // {
+            description = ''
+              Disable latest file updates and enable
+              <link xlink:href="https://docs.requarks.io/install/sideload">sideloading</link>.
+            '';
+          };
+        };
+      };
+      description = ''
+        Settings to configure <package>wiki-js</package>. This directly
+        corresponds to <link xlink:href="https://docs.requarks.io/install/config">the upstream
+        configuration options</link>.
+
+        Secrets can be injected via the environment by
+        <itemizedlist>
+          <listitem><para>specifying <xref linkend="opt-services.wiki-js.environmentFile" />
+          to contain secrets</para></listitem>
+          <listitem><para>and setting sensitive values to <literal>$(ENVIRONMENT_VAR)</literal>
+          with this value defined in the environment-file.</para></listitem>
+        </itemizedlist>
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.wiki-js.settings.dataPath = "/var/lib/${cfg.stateDirectoryName}";
+    systemd.services.wiki-js = {
+      description = "A modern and powerful wiki app built on Node.js";
+      documentation = [ "https://docs.requarks.io/" ];
+      wantedBy = [ "multi-user.target" ];
+
+      path = with pkgs; [ coreutils ];
+      preStart = ''
+        ln -sf ${configFile} /var/lib/${cfg.stateDirectoryName}/config.yml
+        ln -sf ${pkgs.wiki-js}/server /var/lib/${cfg.stateDirectoryName}
+        ln -sf ${pkgs.wiki-js}/assets /var/lib/${cfg.stateDirectoryName}
+        ln -sf ${pkgs.wiki-js}/package.json /var/lib/${cfg.stateDirectoryName}/package.json
+      '';
+
+      serviceConfig = {
+        EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
+        StateDirectory = cfg.stateDirectoryName;
+        WorkingDirectory = "/var/lib/${cfg.stateDirectoryName}";
+        DynamicUser = true;
+        PrivateTmp = true;
+        ExecStart = "${pkgs.nodejs}/bin/node ${pkgs.wiki-js}/server";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ ma27 ];
+}
diff --git a/nixos/modules/services/web-apps/wordpress.nix b/nixos/modules/services/web-apps/wordpress.nix
new file mode 100644
index 00000000000..59471a739cb
--- /dev/null
+++ b/nixos/modules/services/web-apps/wordpress.nix
@@ -0,0 +1,480 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.wordpress;
+  eachSite = cfg.sites;
+  user = "wordpress";
+  webserver = config.services.${cfg.webserver};
+  stateDir = hostName: "/var/lib/wordpress/${hostName}";
+
+  pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
+    pname = "wordpress-${hostName}";
+    version = src.version;
+    src = cfg.package;
+
+    installPhase = ''
+      mkdir -p $out
+      cp -r * $out/
+
+      # symlink the wordpress config
+      ln -s ${wpConfig hostName cfg} $out/share/wordpress/wp-config.php
+      # symlink uploads directory
+      ln -s ${cfg.uploadsDir} $out/share/wordpress/wp-content/uploads
+
+      # https://github.com/NixOS/nixpkgs/pull/53399
+      #
+      # Symlinking works for most plugins and themes, but Avada, for instance, fails to
+      # understand the symlink, causing its file path stripping to fail. This results in
+      # requests that look like: https://example.com/wp-content//nix/store/...plugin/path/some-file.js
+      # Since hard linking directories is not allowed, copying is the next best thing.
+
+      # copy additional plugin(s) and theme(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}
+    '';
+  };
+
+  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__) . '/');
+
+      require_once(ABSPATH . 'wp-settings.php');
+    ?>
+  '';
+
+  secretsVars = [ "AUTH_KEY" "SECURE_AUTH_KEY" "LOGGED_IN_KEY" "NONCE_KEY" "AUTH_SALT" "SECURE_AUTH_SALT" "LOGGED_IN_SALT" "NONCE_SALT" ];
+  secretsScript = hostStateDir: ''
+    # The match in this line is not a typo, see https://github.com/NixOS/nixpkgs/pull/124839
+    grep -q "LOOGGED_IN_KEY" "${hostStateDir}/secret-keys.php" && rm "${hostStateDir}/secret-keys.php"
+    if ! test -e "${hostStateDir}/secret-keys.php"; then
+      umask 0177
+      echo "<?php" >> "${hostStateDir}/secret-keys.php"
+      ${concatMapStringsSep "\n" (var: ''
+        echo "define('${var}', '`tr -dc a-zA-Z0-9 </dev/urandom | head -c 64`');" >> "${hostStateDir}/secret-keys.php"
+      '') secretsVars}
+      echo "?>" >> "${hostStateDir}/secret-keys.php"
+      chmod 440 "${hostStateDir}/secret-keys.php"
+    fi
+  '';
+
+  siteOpts = { lib, name, ... }:
+    {
+      options = {
+        package = mkOption {
+          type = types.package;
+          default = pkgs.wordpress;
+          defaultText = literalExpression "pkgs.wordpress";
+          description = "Which WordPress package to use.";
+        };
+
+        uploadsDir = mkOption {
+          type = types.path;
+          default = "/var/lib/wordpress/${name}/uploads";
+          description = ''
+            This directory is used for uploads of pictures. The directory passed here is automatically
+            created and permissions adjusted as required.
+          '';
+        };
+
+        plugins = mkOption {
+          type = types.listOf types.path;
+          default = [];
+          description = ''
+            List of path(s) to respective plugin(s) which are copied from the 'plugins' directory.
+            <note><para>These plugins need to be packaged before use, see example.</para></note>
+          '';
+          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 ]
+          '';
+        };
+
+        themes = mkOption {
+          type = types.listOf types.path;
+          default = [];
+          description = ''
+            List of path(s) to respective theme(s) which are copied from the 'theme' directory.
+            <note><para>These themes need to be packaged before use, see example.</para></note>
+          '';
+          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 ]
+          '';
+        };
+
+        database = {
+          host = mkOption {
+            type = types.str;
+            default = "localhost";
+            description = "Database host address.";
+          };
+
+          port = mkOption {
+            type = types.port;
+            default = 3306;
+            description = "Database host port.";
+          };
+
+          name = mkOption {
+            type = types.str;
+            default = "wordpress";
+            description = "Database name.";
+          };
+
+          user = mkOption {
+            type = types.str;
+            default = "wordpress";
+            description = "Database user.";
+          };
+
+          passwordFile = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            example = "/run/keys/wordpress-dbpassword";
+            description = ''
+              A file containing the password corresponding to
+              <option>database.user</option>.
+            '';
+          };
+
+          tablePrefix = mkOption {
+            type = types.str;
+            default = "wp_";
+            description = ''
+              The $table_prefix is the value placed in the front of your database tables.
+              Change the value if you want to use something other than wp_ for your database
+              prefix. Typically this is changed if you are installing multiple WordPress blogs
+              in the same database.
+
+              See <link xlink:href='https://codex.wordpress.org/Editing_wp-config.php#table_prefix'/>.
+            '';
+          };
+
+          socket = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            defaultText = literalExpression "/run/mysqld/mysqld.sock";
+            description = "Path to the unix socket file to use for authentication.";
+          };
+
+          createLocally = mkOption {
+            type = types.bool;
+            default = true;
+            description = "Create the database and database user locally.";
+          };
+        };
+
+        virtualHost = mkOption {
+          type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
+          example = literalExpression ''
+            {
+              adminAddr = "webmaster@example.org";
+              forceSSL = true;
+              enableACME = true;
+            }
+          '';
+          description = ''
+            Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>.
+          '';
+        };
+
+        poolConfig = mkOption {
+          type = with types; attrsOf (oneOf [ str int bool ]);
+          default = {
+            "pm" = "dynamic";
+            "pm.max_children" = 32;
+            "pm.start_servers" = 2;
+            "pm.min_spare_servers" = 2;
+            "pm.max_spare_servers" = 4;
+            "pm.max_requests" = 500;
+          };
+          description = ''
+            Options for the WordPress PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+            for details on configuration directives.
+          '';
+        };
+
+        extraConfig = mkOption {
+          type = types.lines;
+          default = "";
+          description = ''
+            Any additional text to be appended to the wp-config.php
+            configuration file. This is a PHP script. For configuration
+            settings, see <link xlink:href='https://codex.wordpress.org/Editing_wp-config.php'/>.
+          '';
+          example = ''
+            define( 'AUTOSAVE_INTERVAL', 60 ); // Seconds
+          '';
+        };
+      };
+
+      config.virtualHost.hostName = mkDefault name;
+    };
+in
+{
+  # interface
+  options = {
+    services.wordpress = {
+
+      sites = mkOption {
+        type = types.attrsOf (types.submodule siteOpts);
+        default = {};
+        description = "Specification of one or more WordPress sites to serve";
+      };
+
+      webserver = mkOption {
+        type = types.enum [ "httpd" "nginx" "caddy" ];
+        default = "httpd";
+        description = ''
+          Whether to use apache2 or nginx for virtual host management.
+
+          Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.&lt;name&gt;</literal>.
+          See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
+
+          Further apache2 configuration can be done by adapting <literal>services.httpd.virtualHosts.&lt;name&gt;</literal>.
+          See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+        '';
+      };
+
+    };
+  };
+
+  # implementation
+  config = mkIf (eachSite != {}) (mkMerge [{
+
+    assertions =
+      (mapAttrsToList (hostName: cfg:
+        { assertion = cfg.database.createLocally -> cfg.database.user == user;
+          message = ''services.wordpress.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned'';
+        }) eachSite) ++
+      (mapAttrsToList (hostName: cfg:
+        { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
+          message = ''services.wordpress.sites."${hostName}".database.passwordFile cannot be specified if services.wordpress.sites."${hostName}".database.createLocally is set to true.'';
+        }) eachSite);
+
+
+    services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = mapAttrsToList (hostName: cfg: cfg.database.name) eachSite;
+      ensureUsers = mapAttrsToList (hostName: cfg:
+        { name = cfg.database.user;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }
+      ) eachSite;
+    };
+
+    services.phpfpm.pools = mapAttrs' (hostName: cfg: (
+      nameValuePair "wordpress-${hostName}" {
+        inherit user;
+        group = webserver.group;
+        settings = {
+          "listen.owner" = webserver.user;
+          "listen.group" = webserver.group;
+        } // cfg.poolConfig;
+      }
+    )) eachSite;
+
+  }
+
+  (mkIf (cfg.webserver == "httpd") {
+    services.httpd = {
+      enable = true;
+      extraModules = [ "proxy_fcgi" ];
+      virtualHosts = mapAttrs (hostName: cfg: mkMerge [ cfg.virtualHost {
+        documentRoot = mkForce "${pkg hostName cfg}/share/wordpress";
+        extraConfig = ''
+          <Directory "${pkg hostName cfg}/share/wordpress">
+            <FilesMatch "\.php$">
+              <If "-f %{REQUEST_FILENAME}">
+                SetHandler "proxy:unix:${config.services.phpfpm.pools."wordpress-${hostName}".socket}|fcgi://localhost/"
+              </If>
+            </FilesMatch>
+
+            # standard wordpress .htaccess contents
+            <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 -Indexes
+          </Directory>
+
+          # https://wordpress.org/support/article/hardening-wordpress/#securing-wp-config-php
+          <Files wp-config.php>
+            Require all denied
+          </Files>
+        '';
+      } ]) eachSite;
+    };
+  })
+
+  {
+    systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
+      "d '${stateDir hostName}' 0750 ${user} ${webserver.group} - -"
+      "d '${cfg.uploadsDir}' 0750 ${user} ${webserver.group} - -"
+      "Z '${cfg.uploadsDir}' 0750 ${user} ${webserver.group} - -"
+    ]) eachSite);
+
+    systemd.services = mkMerge [
+      (mapAttrs' (hostName: cfg: (
+        nameValuePair "wordpress-init-${hostName}" {
+          wantedBy = [ "multi-user.target" ];
+          before = [ "phpfpm-wordpress-${hostName}.service" ];
+          after = optional cfg.database.createLocally "mysql.service";
+          script = secretsScript (stateDir hostName);
+
+          serviceConfig = {
+            Type = "oneshot";
+            User = user;
+            Group = webserver.group;
+          };
+      })) eachSite)
+
+      (optionalAttrs (any (v: v.database.createLocally) (attrValues eachSite)) {
+        httpd.after = [ "mysql.service" ];
+      })
+    ];
+
+    users.users.${user} = {
+      group = webserver.group;
+      isSystemUser = true;
+    };
+  }
+
+  (mkIf (cfg.webserver == "nginx") {
+    services.nginx = {
+      enable = true;
+      virtualHosts = mapAttrs (hostName: cfg: {
+        serverName = mkDefault hostName;
+        root = "${pkg hostName cfg}/share/wordpress";
+        extraConfig = ''
+          index index.php;
+        '';
+        locations = {
+          "/" = {
+            priority = 200;
+            extraConfig = ''
+              try_files $uri $uri/ /index.php$is_args$args;
+            '';
+          };
+          "~ \\.php$" = {
+            priority = 500;
+            extraConfig = ''
+              fastcgi_split_path_info ^(.+\.php)(/.+)$;
+              fastcgi_pass unix:${config.services.phpfpm.pools."wordpress-${hostName}".socket};
+              fastcgi_index index.php;
+              include "${config.services.nginx.package}/conf/fastcgi.conf";
+              fastcgi_param PATH_INFO $fastcgi_path_info;
+              fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
+              # Mitigate https://httpoxy.org/ vulnerabilities
+              fastcgi_param HTTP_PROXY "";
+              fastcgi_intercept_errors off;
+              fastcgi_buffer_size 16k;
+              fastcgi_buffers 4 16k;
+              fastcgi_connect_timeout 300;
+              fastcgi_send_timeout 300;
+              fastcgi_read_timeout 300;
+            '';
+          };
+          "~ /\\." = {
+            priority = 800;
+            extraConfig = "deny all;";
+          };
+          "~* /(?:uploads|files)/.*\\.php$" = {
+            priority = 900;
+            extraConfig = "deny all;";
+          };
+          "~* \\.(js|css|png|jpg|jpeg|gif|ico)$" = {
+            priority = 1000;
+            extraConfig = ''
+              expires max;
+              log_not_found off;
+            '';
+          };
+        };
+      }) eachSite;
+    };
+  })
+
+  (mkIf (cfg.webserver == "caddy") {
+    services.caddy = {
+      enable = true;
+      virtualHosts = mapAttrs' (hostName: cfg: (
+        nameValuePair "http://${hostName}" {
+          extraConfig = ''
+            root    * /${pkg hostName cfg}/share/wordpress
+            file_server
+
+            php_fastcgi unix/${config.services.phpfpm.pools."wordpress-${hostName}".socket}
+
+            @uploads {
+              path_regexp path /uploads\/(.*)\.php
+            }
+            rewrite @uploads /
+
+            @wp-admin {
+              path  not ^\/wp-admin/*
+            }
+            rewrite @wp-admin {path}/index.php?{query}
+          '';
+        }
+      )) eachSite;
+    };
+  })
+
+
+  ]);
+}
diff --git a/nixos/modules/services/web-apps/youtrack.nix b/nixos/modules/services/web-apps/youtrack.nix
new file mode 100644
index 00000000000..b83265ffeab
--- /dev/null
+++ b/nixos/modules/services/web-apps/youtrack.nix
@@ -0,0 +1,181 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.youtrack;
+
+  extraAttr = concatStringsSep " " (mapAttrsToList (k: v: "-D${k}=${v}") (stdParams // cfg.extraParams));
+  mergeAttrList = lib.foldl' lib.mergeAttrs {};
+
+  stdParams = mergeAttrList [
+    (optionalAttrs (cfg.baseUrl != null) {
+      "jetbrains.youtrack.baseUrl" = cfg.baseUrl;
+    })
+    {
+    "java.aws.headless" = "true";
+    "jetbrains.youtrack.disableBrowser" = "true";
+    }
+  ];
+in
+{
+  options.services.youtrack = {
+
+    enable = mkEnableOption "YouTrack service";
+
+    address = mkOption {
+      description = ''
+        The interface youtrack will listen on.
+      '';
+      default = "127.0.0.1";
+      type = types.str;
+    };
+
+    baseUrl = mkOption {
+      description = ''
+        Base URL for youtrack. Will be auto-detected and stored in database.
+      '';
+      type = types.nullOr types.str;
+      default = null;
+    };
+
+    extraParams = mkOption {
+      default = {};
+      description = ''
+        Extra parameters to pass to youtrack. See
+        https://www.jetbrains.com/help/youtrack/standalone/YouTrack-Java-Start-Parameters.html
+        for more information.
+      '';
+      example = literalExpression ''
+        {
+          "jetbrains.youtrack.overrideRootPassword" = "tortuga";
+        }
+      '';
+      type = types.attrsOf types.str;
+    };
+
+    package = mkOption {
+      description = ''
+        Package to use.
+      '';
+      type = types.package;
+      default = pkgs.youtrack;
+      defaultText = literalExpression "pkgs.youtrack";
+    };
+
+    port = mkOption {
+      description = ''
+        The port youtrack will listen on.
+      '';
+      default = 8080;
+      type = types.int;
+    };
+
+    statePath = mkOption {
+      description = ''
+        Where to keep the youtrack database.
+      '';
+      type = types.path;
+      default = "/var/lib/youtrack";
+    };
+
+    virtualHost = mkOption {
+      description = ''
+        Name of the nginx virtual host to use and setup.
+        If null, do not setup anything.
+      '';
+      default = null;
+      type = types.nullOr types.str;
+    };
+
+    jvmOpts = mkOption {
+      description = ''
+        Extra options to pass to the JVM.
+        See https://www.jetbrains.com/help/youtrack/standalone/Configure-JVM-Options.html
+        for more information.
+      '';
+      type = types.separatedString " ";
+      example = "-XX:MetaspaceSize=250m";
+      default = "";
+    };
+
+    maxMemory = mkOption {
+      description = ''
+        Maximum Java heap size
+      '';
+      type = types.str;
+      default = "1g";
+    };
+
+    maxMetaspaceSize = mkOption {
+      description = ''
+        Maximum java Metaspace memory.
+      '';
+      type = types.str;
+      default = "350m";
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.youtrack = {
+      environment.HOME = cfg.statePath;
+      environment.YOUTRACK_JVM_OPTS = "${extraAttr}";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = with pkgs; [ unixtools.hostname ];
+      serviceConfig = {
+        Type = "simple";
+        User = "youtrack";
+        Group = "youtrack";
+        Restart = "on-failure";
+        ExecStart = ''${cfg.package}/bin/youtrack --J-Xmx${cfg.maxMemory} --J-XX:MaxMetaspaceSize=${cfg.maxMetaspaceSize} ${cfg.jvmOpts} ${cfg.address}:${toString cfg.port}'';
+      };
+    };
+
+    users.users.youtrack = {
+      description = "Youtrack service user";
+      isSystemUser = true;
+      home = cfg.statePath;
+      createHome = true;
+      group = "youtrack";
+    };
+
+    users.groups.youtrack = {};
+
+    services.nginx = mkIf (cfg.virtualHost != null) {
+      upstreams.youtrack.servers."${cfg.address}:${toString cfg.port}" = {};
+      virtualHosts.${cfg.virtualHost}.locations = {
+        "/" = {
+          proxyPass = "http://youtrack";
+          extraConfig = ''
+            client_max_body_size 10m;
+            proxy_http_version 1.1;
+            proxy_set_header X-Forwarded-Host $http_host;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header X-Forwarded-Proto $scheme;
+          '';
+        };
+
+        "/api/eventSourceBus" = {
+          proxyPass = "http://youtrack";
+          extraConfig = ''
+            proxy_cache off;
+            proxy_buffering off;
+            proxy_read_timeout 86400s;
+            proxy_send_timeout 86400s;
+            proxy_set_header Connection "";
+            chunked_transfer_encoding off;
+            client_max_body_size 10m;
+            proxy_http_version 1.1;
+            proxy_set_header X-Forwarded-Host $http_host;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header X-Forwarded-Proto $scheme;
+          '';
+        };
+
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/web-apps/zabbix.nix b/nixos/modules/services/web-apps/zabbix.nix
new file mode 100644
index 00000000000..538dac0d5be
--- /dev/null
+++ b/nixos/modules/services/web-apps/zabbix.nix
@@ -0,0 +1,238 @@
+{ config, lib, options, pkgs, ... }:
+
+let
+
+  inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types;
+  inherit (lib) literalExpression mapAttrs optionalString versionAtLeast;
+
+  cfg = config.services.zabbixWeb;
+  opt = options.services.zabbixWeb;
+  fpm = config.services.phpfpm.pools.zabbix;
+
+  user = "zabbix";
+  group = "zabbix";
+  stateDir = "/var/lib/zabbix";
+
+  zabbixConfig = pkgs.writeText "zabbix.conf.php" ''
+    <?php
+    // Zabbix GUI configuration file.
+    global $DB;
+    $DB['TYPE'] = '${ { mysql = "MYSQL"; pgsql = "POSTGRESQL"; oracle = "ORACLE"; }.${cfg.database.type} }';
+    $DB['SERVER'] = '${cfg.database.host}';
+    $DB['PORT'] = '${toString cfg.database.port}';
+    $DB['DATABASE'] = '${cfg.database.name}';
+    $DB['USER'] = '${cfg.database.user}';
+    # NOTE: file_get_contents adds newline at the end of returned string
+    $DB['PASSWORD'] = ${if cfg.database.passwordFile != null then "trim(file_get_contents('${cfg.database.passwordFile}'), \"\\r\\n\")" else "''"};
+    // Schema name. Used for IBM DB2 and PostgreSQL.
+    $DB['SCHEMA'] = ''';
+    $ZBX_SERVER = '${cfg.server.address}';
+    $ZBX_SERVER_PORT = '${toString cfg.server.port}';
+    $ZBX_SERVER_NAME = ''';
+    $IMAGE_FORMAT_DEFAULT = IMAGE_FORMAT_PNG;
+
+    ${cfg.extraConfig}
+  '';
+
+in
+{
+  # interface
+
+  options.services = {
+    zabbixWeb = {
+      enable = mkEnableOption "the Zabbix web interface";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.zabbix.web;
+        defaultText = literalExpression "zabbix.web";
+        description = "Which Zabbix package to use.";
+      };
+
+      server = {
+        port = mkOption {
+          type = types.int;
+          description = "The port of the Zabbix server to connect to.";
+          default = 10051;
+        };
+
+        address = mkOption {
+          type = types.str;
+          description = "The IP address or hostname of the Zabbix server to connect to.";
+          default = "localhost";
+        };
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum [ "mysql" "pgsql" "oracle" ];
+          example = "mysql";
+          default = "pgsql";
+          description = "Database engine to use.";
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "";
+          description = "Database host address.";
+        };
+
+        port = mkOption {
+          type = types.int;
+          default =
+            if cfg.database.type == "mysql" then config.services.mysql.port
+            else if cfg.database.type == "pgsql" then config.services.postgresql.port
+            else 1521;
+          defaultText = literalExpression ''
+            if config.${opt.database.type} == "mysql" then config.${options.services.mysql.port}
+            else if config.${opt.database.type} == "pgsql" then config.${options.services.postgresql.port}
+            else 1521
+          '';
+          description = "Database host port.";
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "zabbix";
+          description = "Database name.";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "zabbix";
+          description = "Database user.";
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          example = "/run/keys/zabbix-dbpassword";
+          description = ''
+            A file containing the password corresponding to
+            <option>database.user</option>.
+          '';
+        };
+
+        socket = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          example = "/run/postgresql";
+          description = "Path to the unix socket file to use for authentication.";
+        };
+      };
+
+      virtualHost = mkOption {
+        type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
+        example = literalExpression ''
+          {
+            hostName = "zabbix.example.org";
+            adminAddr = "webmaster@example.org";
+            forceSSL = true;
+            enableACME = true;
+          }
+        '';
+        description = ''
+          Apache configuration can be done by adapting <literal>services.httpd.virtualHosts.&lt;name&gt;</literal>.
+          See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+        '';
+      };
+
+      poolConfig = mkOption {
+        type = with types; attrsOf (oneOf [ str int bool ]);
+        default = {
+          "pm" = "dynamic";
+          "pm.max_children" = 32;
+          "pm.start_servers" = 2;
+          "pm.min_spare_servers" = 2;
+          "pm.max_spare_servers" = 4;
+          "pm.max_requests" = 500;
+        };
+        description = ''
+          Options for the Zabbix PHP pool. See the documentation on <literal>php-fpm.conf</literal> for details on configuration directives.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Additional configuration to be copied verbatim into <filename>zabbix.conf.php</filename>.
+        '';
+      };
+
+    };
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+
+    services.zabbixWeb.extraConfig = optionalString ((versionAtLeast config.system.stateVersion "20.09") && (versionAtLeast cfg.package.version "5.0.0")) ''
+      $DB['DOUBLE_IEEE754'] = 'true';
+    '';
+
+    systemd.tmpfiles.rules = [
+      "d '${stateDir}' 0750 ${user} ${group} - -"
+      "d '${stateDir}/session' 0750 ${user} ${config.services.httpd.group} - -"
+    ];
+
+    services.phpfpm.pools.zabbix = {
+      inherit user;
+      group = config.services.httpd.group;
+      phpOptions = ''
+        # https://www.zabbix.com/documentation/current/manual/installation/install
+        memory_limit = 128M
+        post_max_size = 16M
+        upload_max_filesize = 2M
+        max_execution_time = 300
+        max_input_time = 300
+        session.auto_start = 0
+        mbstring.func_overload = 0
+        always_populate_raw_post_data = -1
+        # https://bbs.archlinux.org/viewtopic.php?pid=1745214#p1745214
+        session.save_path = ${stateDir}/session
+      '' + optionalString (config.time.timeZone != null) ''
+        date.timezone = "${config.time.timeZone}"
+      '' + optionalString (cfg.database.type == "oracle") ''
+        extension=${pkgs.phpPackages.oci8}/lib/php/extensions/oci8.so
+      '';
+      phpEnv.ZABBIX_CONFIG = "${zabbixConfig}";
+      settings = {
+        "listen.owner" = config.services.httpd.user;
+        "listen.group" = config.services.httpd.group;
+      } // cfg.poolConfig;
+    };
+
+    services.httpd = {
+      enable = true;
+      adminAddr = mkDefault cfg.virtualHost.adminAddr;
+      extraModules = [ "proxy_fcgi" ];
+      virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
+        documentRoot = mkForce "${cfg.package}/share/zabbix";
+        extraConfig = ''
+          <Directory "${cfg.package}/share/zabbix">
+            <FilesMatch "\.php$">
+              <If "-f %{REQUEST_FILENAME}">
+                SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
+              </If>
+            </FilesMatch>
+            AllowOverride all
+            Options -Indexes
+            DirectoryIndex index.php
+          </Directory>
+        '';
+      } ];
+    };
+
+    users.users.${user} = mapAttrs (name: mkDefault) {
+      description = "Zabbix daemon user";
+      uid = config.ids.uids.zabbix;
+      inherit group;
+    };
+
+    users.groups.${group} = mapAttrs (name: mkDefault) {
+      gid = config.ids.gids.zabbix;
+    };
+
+  };
+}
diff --git a/nixos/modules/services/web-servers/agate.nix b/nixos/modules/services/web-servers/agate.nix
new file mode 100644
index 00000000000..3afdb561c0b
--- /dev/null
+++ b/nixos/modules/services/web-servers/agate.nix
@@ -0,0 +1,148 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.agate;
+in
+{
+  options = {
+    services.agate = {
+      enable = mkEnableOption "Agate Server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.agate;
+        defaultText = literalExpression "pkgs.agate";
+        description = "The package to use";
+      };
+
+      addresses = mkOption {
+        type = types.listOf types.str;
+        default = [ "0.0.0.0:1965" ];
+        description = ''
+          Addresses to listen on, IP:PORT, if you haven't disabled forwarding
+          only set IPv4.
+        '';
+      };
+
+      contentDir = mkOption {
+        default = "/var/lib/agate/content";
+        type = types.path;
+        description = "Root of the content directory.";
+      };
+
+      certificatesDir = mkOption {
+        default = "/var/lib/agate/certificates";
+        type = types.path;
+        description = "Root of the certificate directory.";
+      };
+
+      hostnames = mkOption {
+        default = [ ];
+        type = types.listOf types.str;
+        description = ''
+          Domain name of this Gemini server, enables checking hostname and port
+          in requests. (multiple occurences means basic vhosts)
+        '';
+      };
+
+      language = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description = "RFC 4646 Language code for text/gemini documents.";
+      };
+
+      onlyTls_1_3 = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Only use TLSv1.3 (default also allows TLSv1.2).";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ "" ];
+        example = [ "--log-ip" ];
+        description = "Extra arguments to use running agate.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # available for generating certs by hand
+    # it can be a bit arduous with openssl
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.agate = {
+      description = "Agate";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "network-online.target" ];
+
+      script =
+        let
+          prefixKeyList = key: list: concatMap (v: [ key v ]) list;
+          addresses = prefixKeyList "--addr" cfg.addresses;
+          hostnames = prefixKeyList "--hostname" cfg.hostnames;
+        in
+        ''
+          exec ${cfg.package}/bin/agate ${
+            escapeShellArgs (
+              [
+                "--content" "${cfg.contentDir}"
+                "--certs" "${cfg.certificatesDir}"
+              ] ++
+              addresses ++
+              (optionals (cfg.hostnames != []) hostnames) ++
+              (optionals (cfg.language != null) [ "--lang" cfg.language ]) ++
+              (optionals cfg.onlyTls_1_3 [ "--only-tls13" ]) ++
+              (optionals (cfg.extraArgs != []) cfg.extraArgs)
+            )
+          }
+        '';
+
+      serviceConfig = {
+        Restart = "always";
+        RestartSec = "5s";
+        DynamicUser = true;
+        StateDirectory = "agate";
+
+        # Security options:
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+
+        LockPersonality = true;
+
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+
+        RestrictNamespaces = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [
+          "@system-service"
+          "~@cpu-emulation"
+          "~@debug"
+          "~@keyring"
+          "~@memlock"
+          "~@obsolete"
+          "~@privileged"
+          "~@setuid"
+        ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
new file mode 100644
index 00000000000..d817ff6019a
--- /dev/null
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -0,0 +1,839 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.httpd;
+
+  certs = config.security.acme.certs;
+
+  runtimeDir = "/run/httpd";
+
+  pkg = cfg.package.out;
+
+  apachectl = pkgs.runCommand "apachectl" { meta.priority = -1; } ''
+    mkdir -p $out/bin
+    cp ${pkg}/bin/apachectl $out/bin/apachectl
+    sed -i $out/bin/apachectl -e 's|$HTTPD -t|$HTTPD -t -f /etc/httpd/httpd.conf|'
+  '';
+
+  php = cfg.phpPackage.override { apacheHttpd = pkg; };
+
+  phpModuleName = let
+    majorVersion = lib.versions.major (lib.getVersion php);
+  in (if majorVersion == "8" then "php" else "php${majorVersion}");
+
+  mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = pkg; };
+
+  vhosts = attrValues cfg.virtualHosts;
+
+  # certName is used later on to determine systemd service names.
+  acmeEnabledVhosts = map (hostOpts: hostOpts // {
+    certName = if hostOpts.useACMEHost != null then hostOpts.useACMEHost else hostOpts.hostName;
+  }) (filter (hostOpts: hostOpts.enableACME || hostOpts.useACMEHost != null) vhosts);
+
+  dependentCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts);
+
+  mkListenInfo = hostOpts:
+    if hostOpts.listen != [] then
+      hostOpts.listen
+    else
+      optionals (hostOpts.onlySSL || hostOpts.addSSL || hostOpts.forceSSL) (map (addr: { ip = addr; port = 443; ssl = true; }) hostOpts.listenAddresses) ++
+      optionals (!hostOpts.onlySSL) (map (addr: { ip = addr; port = 80; ssl = false; }) hostOpts.listenAddresses)
+    ;
+
+  listenInfo = unique (concatMap mkListenInfo vhosts);
+
+  enableHttp2 = any (vhost: vhost.http2) vhosts;
+  enableSSL = any (listen: listen.ssl) listenInfo;
+  enableUserDir = any (vhost: vhost.enableUserDir) vhosts;
+
+  # NOTE: generally speaking order of modules is very important
+  modules =
+    [ # required apache modules our httpd service cannot run without
+      "authn_core" "authz_core"
+      "log_config"
+      "mime" "autoindex" "negotiation" "dir"
+      "alias" "rewrite"
+      "unixd" "slotmem_shm" "socache_shmcb"
+      "mpm_${cfg.mpm}"
+    ]
+    ++ (if cfg.mpm == "prefork" then [ "cgi" ] else [ "cgid" ])
+    ++ optional enableHttp2 "http2"
+    ++ optional enableSSL "ssl"
+    ++ optional enableUserDir "userdir"
+    ++ optional cfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; }
+    ++ optional cfg.enablePHP { name = phpModuleName; path = "${php}/modules/lib${phpModuleName}.so"; }
+    ++ optional cfg.enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; }
+    ++ cfg.extraModules;
+
+  loggingConf = (if cfg.logFormat != "none" then ''
+    ErrorLog ${cfg.logDir}/error.log
+
+    LogLevel notice
+
+    LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
+    LogFormat "%h %l %u %t \"%r\" %>s %b" common
+    LogFormat "%{Referer}i -> %U" referer
+    LogFormat "%{User-agent}i" agent
+
+    CustomLog ${cfg.logDir}/access.log ${cfg.logFormat}
+  '' else ''
+    ErrorLog /dev/null
+  '');
+
+
+  browserHacks = ''
+    <IfModule mod_setenvif.c>
+        BrowserMatch "Mozilla/2" nokeepalive
+        BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0
+        BrowserMatch "RealPlayer 4\.0" force-response-1.0
+        BrowserMatch "Java/1\.0" force-response-1.0
+        BrowserMatch "JDK/1\.0" force-response-1.0
+        BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully
+        BrowserMatch "^WebDrive" redirect-carefully
+        BrowserMatch "^WebDAVFS/1.[012]" redirect-carefully
+        BrowserMatch "^gnome-vfs" redirect-carefully
+    </IfModule>
+  '';
+
+
+  sslConf = ''
+    <IfModule mod_ssl.c>
+        SSLSessionCache shmcb:${runtimeDir}/ssl_scache(512000)
+
+        Mutex posixsem
+
+        SSLRandomSeed startup builtin
+        SSLRandomSeed connect builtin
+
+        SSLProtocol ${cfg.sslProtocols}
+        SSLCipherSuite ${cfg.sslCiphers}
+        SSLHonorCipherOrder on
+    </IfModule>
+  '';
+
+
+  mimeConf = ''
+    TypesConfig ${pkg}/conf/mime.types
+
+    AddType application/x-x509-ca-cert .crt
+    AddType application/x-pkcs7-crl    .crl
+    AddType application/x-httpd-php    .php .phtml
+
+    <IfModule mod_mime_magic.c>
+        MIMEMagicFile ${pkg}/conf/magic
+    </IfModule>
+  '';
+
+  luaSetPaths = let
+    # support both lua and lua.withPackages derivations
+    luaversion = cfg.package.lua5.lua.luaversion or cfg.package.lua5.luaversion;
+    in
+  ''
+    <IfModule mod_lua.c>
+      LuaPackageCPath ${cfg.package.lua5}/lib/lua/${luaversion}/?.so
+      LuaPackagePath  ${cfg.package.lua5}/share/lua/${luaversion}/?.lua
+    </IfModule>
+  '';
+
+  mkVHostConf = hostOpts:
+    let
+      adminAddr = if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr;
+      listen = filter (listen: !listen.ssl) (mkListenInfo hostOpts);
+      listenSSL = filter (listen: listen.ssl) (mkListenInfo hostOpts);
+
+      useACME = hostOpts.enableACME || hostOpts.useACMEHost != null;
+      sslCertDir =
+        if hostOpts.enableACME then certs.${hostOpts.hostName}.directory
+        else if hostOpts.useACMEHost != null then certs.${hostOpts.useACMEHost}.directory
+        else abort "This case should never happen.";
+
+      sslServerCert = if useACME then "${sslCertDir}/fullchain.pem" else hostOpts.sslServerCert;
+      sslServerKey = if useACME then "${sslCertDir}/key.pem" else hostOpts.sslServerKey;
+      sslServerChain = if useACME then "${sslCertDir}/chain.pem" else hostOpts.sslServerChain;
+
+      acmeChallenge = optionalString (useACME && hostOpts.acmeRoot != null) ''
+        Alias /.well-known/acme-challenge/ "${hostOpts.acmeRoot}/.well-known/acme-challenge/"
+        <Directory "${hostOpts.acmeRoot}">
+            AllowOverride None
+            Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
+            Require method GET POST OPTIONS
+            Require all granted
+        </Directory>
+      '';
+    in
+      optionalString (listen != []) ''
+        <VirtualHost ${concatMapStringsSep " " (listen: "${listen.ip}:${toString listen.port}") listen}>
+            ServerName ${hostOpts.hostName}
+            ${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases}
+            ServerAdmin ${adminAddr}
+            <IfModule mod_ssl.c>
+                SSLEngine off
+            </IfModule>
+            ${acmeChallenge}
+            ${if hostOpts.forceSSL then ''
+              <IfModule mod_rewrite.c>
+                  RewriteEngine on
+                  RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge [NC]
+                  RewriteCond %{HTTPS} off
+                  RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
+              </IfModule>
+            '' else mkVHostCommonConf hostOpts}
+        </VirtualHost>
+      '' +
+      optionalString (listenSSL != []) ''
+        <VirtualHost ${concatMapStringsSep " " (listen: "${listen.ip}:${toString listen.port}") listenSSL}>
+            ServerName ${hostOpts.hostName}
+            ${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases}
+            ServerAdmin ${adminAddr}
+            SSLEngine on
+            SSLCertificateFile ${sslServerCert}
+            SSLCertificateKeyFile ${sslServerKey}
+            ${optionalString (sslServerChain != null) "SSLCertificateChainFile ${sslServerChain}"}
+            ${optionalString hostOpts.http2 "Protocols h2 h2c http/1.1"}
+            ${acmeChallenge}
+            ${mkVHostCommonConf hostOpts}
+        </VirtualHost>
+      ''
+  ;
+
+  mkVHostCommonConf = hostOpts:
+    let
+      documentRoot = if hostOpts.documentRoot != null
+        then hostOpts.documentRoot
+        else pkgs.emptyDirectory
+      ;
+
+      mkLocations = locations: concatStringsSep "\n" (map (config: ''
+        <Location ${config.location}>
+          ${optionalString (config.proxyPass != null) ''
+            <IfModule mod_proxy.c>
+                ProxyPass ${config.proxyPass}
+                ProxyPassReverse ${config.proxyPass}
+            </IfModule>
+          ''}
+          ${optionalString (config.index != null) ''
+            <IfModule mod_dir.c>
+                DirectoryIndex ${config.index}
+            </IfModule>
+          ''}
+          ${optionalString (config.alias != null) ''
+            <IfModule mod_alias.c>
+                Alias "${config.alias}"
+            </IfModule>
+          ''}
+          ${config.extraConfig}
+        </Location>
+      '') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations)));
+    in
+      ''
+        ${optionalString cfg.logPerVirtualHost ''
+          ErrorLog ${cfg.logDir}/error-${hostOpts.hostName}.log
+          CustomLog ${cfg.logDir}/access-${hostOpts.hostName}.log ${hostOpts.logFormat}
+        ''}
+
+        ${optionalString (hostOpts.robotsEntries != "") ''
+          Alias /robots.txt ${pkgs.writeText "robots.txt" hostOpts.robotsEntries}
+        ''}
+
+        DocumentRoot "${documentRoot}"
+
+        <Directory "${documentRoot}">
+            Options Indexes FollowSymLinks
+            AllowOverride None
+            Require all granted
+        </Directory>
+
+        ${optionalString hostOpts.enableUserDir ''
+          UserDir public_html
+          UserDir disabled root
+          <Directory "/home/*/public_html">
+              AllowOverride FileInfo AuthConfig Limit Indexes
+              Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
+              <Limit GET POST OPTIONS>
+                  Require all granted
+              </Limit>
+              <LimitExcept GET POST OPTIONS>
+                  Require all denied
+              </LimitExcept>
+          </Directory>
+        ''}
+
+        ${optionalString (hostOpts.globalRedirect != null && hostOpts.globalRedirect != "") ''
+          RedirectPermanent / ${hostOpts.globalRedirect}
+        ''}
+
+        ${
+          let makeDirConf = elem: ''
+                Alias ${elem.urlPath} ${elem.dir}/
+                <Directory ${elem.dir}>
+                    Options +Indexes
+                    Require all granted
+                    AllowOverride All
+                </Directory>
+              '';
+          in concatMapStrings makeDirConf hostOpts.servedDirs
+        }
+
+        ${mkLocations hostOpts.locations}
+        ${hostOpts.extraConfig}
+      ''
+  ;
+
+
+  confFile = pkgs.writeText "httpd.conf" ''
+
+    ServerRoot ${pkg}
+    ServerName ${config.networking.hostName}
+    DefaultRuntimeDir ${runtimeDir}/runtime
+
+    PidFile ${runtimeDir}/httpd.pid
+
+    ${optionalString (cfg.mpm != "prefork") ''
+      # mod_cgid requires this.
+      ScriptSock ${runtimeDir}/cgisock
+    ''}
+
+    <IfModule prefork.c>
+        MaxClients           ${toString cfg.maxClients}
+        MaxRequestsPerChild  ${toString cfg.maxRequestsPerChild}
+    </IfModule>
+
+    ${let
+        toStr = listen: "Listen ${listen.ip}:${toString listen.port} ${if listen.ssl then "https" else "http"}";
+        uniqueListen = uniqList {inputList = map toStr listenInfo;};
+      in concatStringsSep "\n" uniqueListen
+    }
+
+    User ${cfg.user}
+    Group ${cfg.group}
+
+    ${let
+        mkModule = module:
+          if isString module then { name = module; path = "${pkg}/modules/mod_${module}.so"; }
+          else if isAttrs module then { inherit (module) name path; }
+          else throw "Expecting either a string or attribute set including a name and path.";
+      in
+        concatMapStringsSep "\n" (module: "LoadModule ${module.name}_module ${module.path}") (unique (map mkModule modules))
+    }
+
+    AddHandler type-map var
+
+    <Files ~ "^\.ht">
+        Require all denied
+    </Files>
+
+    ${mimeConf}
+    ${loggingConf}
+    ${browserHacks}
+
+    Include ${pkg}/conf/extra/httpd-default.conf
+    Include ${pkg}/conf/extra/httpd-autoindex.conf
+    Include ${pkg}/conf/extra/httpd-multilang-errordoc.conf
+    Include ${pkg}/conf/extra/httpd-languages.conf
+
+    TraceEnable off
+
+    ${sslConf}
+
+    ${optionalString cfg.package.luaSupport luaSetPaths}
+
+    # Fascist default - deny access to everything.
+    <Directory />
+        Options FollowSymLinks
+        AllowOverride None
+        Require all denied
+    </Directory>
+
+    # But do allow access to files in the store so that we don't have
+    # to generate <Directory> clauses for every generated file that we
+    # want to serve.
+    <Directory /nix/store>
+        Require all granted
+    </Directory>
+
+    ${cfg.extraConfig}
+
+    ${concatMapStringsSep "\n" mkVHostConf vhosts}
+  '';
+
+  # Generate the PHP configuration file.  Should probably be factored
+  # out into a separate module.
+  phpIni = pkgs.runCommand "php.ini"
+    { options = cfg.phpOptions;
+      preferLocalBuild = true;
+    }
+    ''
+      cat ${php}/etc/php.ini > $out
+      cat ${php.phpIni} > $out
+      echo "$options" >> $out
+    '';
+
+  mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix;
+in
+
+
+{
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "httpd" "extraSubservices" ] "Most existing subservices have been ported to the NixOS module system. Please update your configuration accordingly.")
+    (mkRemovedOptionModule [ "services" "httpd" "stateDir" ] "The httpd module now uses /run/httpd as a runtime directory.")
+    (mkRenamedOptionModule [ "services" "httpd" "multiProcessingModule" ] [ "services" "httpd" "mpm" ])
+
+    # virtualHosts options
+    (mkRemovedOptionModule [ "services" "httpd" "documentRoot" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "enableSSL" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "enableUserDir" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "globalRedirect" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "hostName" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "listen" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "robotsEntries" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "servedDirs" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "servedFiles" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "serverAliases" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "sslServerCert" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "sslServerChain" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "sslServerKey" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+  ];
+
+  # interface
+
+  options = {
+
+    services.httpd = {
+
+      enable = mkEnableOption "the Apache HTTP Server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.apacheHttpd;
+        defaultText = literalExpression "pkgs.apacheHttpd";
+        description = ''
+          Overridable attribute of the Apache HTTP Server package to use.
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.path;
+        default = confFile;
+        defaultText = literalExpression "confFile";
+        example = literalExpression ''pkgs.writeText "httpd.conf" "# my custom config file ..."'';
+        description = ''
+          Override the configuration file used by Apache. By default,
+          NixOS generates one automatically.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Configuration lines appended to the generated Apache
+          configuration file. Note that this mechanism will not work
+          when <option>configFile</option> is overridden.
+        '';
+      };
+
+      extraModules = mkOption {
+        type = types.listOf types.unspecified;
+        default = [];
+        example = literalExpression ''
+          [
+            "proxy_connect"
+            { name = "jk"; path = "''${pkgs.tomcat_connectors}/modules/mod_jk.so"; }
+          ]
+        '';
+        description = ''
+          Additional Apache modules to be used. These can be
+          specified as a string in the case of modules distributed
+          with Apache, or as an attribute set specifying the
+          <varname>name</varname> and <varname>path</varname> of the
+          module.
+        '';
+      };
+
+      adminAddr = mkOption {
+        type = types.str;
+        example = "admin@example.org";
+        description = "E-mail address of the server administrator.";
+      };
+
+      logFormat = mkOption {
+        type = types.str;
+        default = "common";
+        example = "combined";
+        description = ''
+          Log format for log files. Possible values are: combined, common, referer, agent, none.
+          See <link xlink:href="https://httpd.apache.org/docs/2.4/logs.html"/> for more details.
+        '';
+      };
+
+      logPerVirtualHost = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          If enabled, each virtual host gets its own
+          <filename>access.log</filename> and
+          <filename>error.log</filename>, namely suffixed by the
+          <option>hostName</option> of the virtual host.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "wwwrun";
+        description = ''
+          User account under which httpd children processes run.
+
+          If you require the main httpd process to run as
+          <literal>root</literal> add the following configuration:
+          <programlisting>
+          systemd.services.httpd.serviceConfig.User = lib.mkForce "root";
+          </programlisting>
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "wwwrun";
+        description = ''
+          Group under which httpd children processes run.
+        '';
+      };
+
+      logDir = mkOption {
+        type = types.path;
+        default = "/var/log/httpd";
+        description = ''
+          Directory for Apache's log files. It is created automatically.
+        '';
+      };
+
+      virtualHosts = mkOption {
+        type = with types; attrsOf (submodule (import ./vhost-options.nix));
+        default = {
+          localhost = {
+            documentRoot = "${pkg}/htdocs";
+          };
+        };
+        defaultText = literalExpression ''
+          {
+            localhost = {
+              documentRoot = "''${package.out}/htdocs";
+            };
+          }
+        '';
+        example = literalExpression ''
+          {
+            "foo.example.com" = {
+              forceSSL = true;
+              documentRoot = "/var/www/foo.example.com"
+            };
+            "bar.example.com" = {
+              addSSL = true;
+              documentRoot = "/var/www/bar.example.com";
+            };
+          }
+        '';
+        description = ''
+          Specification of the virtual hosts served by Apache. Each
+          element should be an attribute set specifying the
+          configuration of the virtual host.
+        '';
+      };
+
+      enableMellon = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the mod_auth_mellon module.";
+      };
+
+      enablePHP = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the PHP module.";
+      };
+
+      phpPackage = mkOption {
+        type = types.package;
+        default = pkgs.php;
+        defaultText = literalExpression "pkgs.php";
+        description = ''
+          Overridable attribute of the PHP package to use.
+        '';
+      };
+
+      enablePerl = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the Perl module (mod_perl).";
+      };
+
+      phpOptions = mkOption {
+        type = types.lines;
+        default = "";
+        example =
+          ''
+            date.timezone = "CET"
+          '';
+        description = ''
+          Options appended to the PHP configuration file <filename>php.ini</filename>.
+        '';
+      };
+
+      mpm = mkOption {
+        type = types.enum [ "event" "prefork" "worker" ];
+        default = "event";
+        example = "worker";
+        description =
+          ''
+            Multi-processing module to be used by Apache. Available
+            modules are <literal>prefork</literal> (handles each
+            request in a separate child process), <literal>worker</literal>
+            (hybrid approach that starts a number of child processes
+            each running a number of threads) and <literal>event</literal>
+            (the default; a recent variant of <literal>worker</literal>
+            that handles persistent connections more efficiently).
+          '';
+      };
+
+      maxClients = mkOption {
+        type = types.int;
+        default = 150;
+        example = 8;
+        description = "Maximum number of httpd processes (prefork)";
+      };
+
+      maxRequestsPerChild = mkOption {
+        type = types.int;
+        default = 0;
+        example = 500;
+        description = ''
+          Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited.
+        '';
+      };
+
+      sslCiphers = mkOption {
+        type = types.str;
+        default = "HIGH:!aNULL:!MD5:!EXP";
+        description = "Cipher Suite available for negotiation in SSL proxy handshake.";
+      };
+
+      sslProtocols = mkOption {
+        type = types.str;
+        default = "All -SSLv2 -SSLv3 -TLSv1 -TLSv1.1";
+        example = "All -SSLv2 -SSLv3";
+        description = "Allowed SSL/TLS protocol versions.";
+      };
+    };
+
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = all (hostOpts: !hostOpts.enableSSL) vhosts;
+        message = ''
+          The option `services.httpd.virtualHosts.<name>.enableSSL` no longer has any effect; please remove it.
+          Select one of `services.httpd.virtualHosts.<name>.addSSL`, `services.httpd.virtualHosts.<name>.forceSSL`,
+          or `services.httpd.virtualHosts.<name>.onlySSL`.
+        '';
+      }
+      {
+        assertion = all (hostOpts: with hostOpts; !(addSSL && onlySSL) && !(forceSSL && onlySSL) && !(addSSL && forceSSL)) vhosts;
+        message = ''
+          Options `services.httpd.virtualHosts.<name>.addSSL`,
+          `services.httpd.virtualHosts.<name>.onlySSL` and `services.httpd.virtualHosts.<name>.forceSSL`
+          are mutually exclusive.
+        '';
+      }
+      {
+        assertion = all (hostOpts: !(hostOpts.enableACME && hostOpts.useACMEHost != null)) vhosts;
+        message = ''
+          Options `services.httpd.virtualHosts.<name>.enableACME` and
+          `services.httpd.virtualHosts.<name>.useACMEHost` are mutually exclusive.
+        '';
+      }
+    ] ++ map (name: mkCertOwnershipAssertion {
+      inherit (cfg) group user;
+      cert = config.security.acme.certs.${name};
+      groups = config.users.groups;
+    }) dependentCertNames;
+
+    warnings =
+      mapAttrsToList (name: hostOpts: ''
+        Using config.services.httpd.virtualHosts."${name}".servedFiles is deprecated and will become unsupported in a future release. Your configuration will continue to work as is but please migrate your configuration to config.services.httpd.virtualHosts."${name}".locations before the 20.09 release of NixOS.
+      '') (filterAttrs (name: hostOpts: hostOpts.servedFiles != []) cfg.virtualHosts);
+
+    users.users = optionalAttrs (cfg.user == "wwwrun") {
+      wwwrun = {
+        group = cfg.group;
+        description = "Apache httpd user";
+        uid = config.ids.uids.wwwrun;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "wwwrun") {
+      wwwrun.gid = config.ids.gids.wwwrun;
+    };
+
+    security.acme.certs = let
+      acmePairs = map (hostOpts: let
+        hasRoot = hostOpts.acmeRoot != null;
+      in nameValuePair hostOpts.hostName {
+        group = mkDefault cfg.group;
+        # if acmeRoot is null inherit config.security.acme
+        # Since config.security.acme.certs.<cert>.webroot's own default value
+        # should take precedence set priority higher than mkOptionDefault
+        webroot = mkOverride (if hasRoot then 1000 else 2000) hostOpts.acmeRoot;
+        # Also nudge dnsProvider to null in case it is inherited
+        dnsProvider = mkOverride (if hasRoot then 1000 else 2000) null;
+        extraDomainNames = hostOpts.serverAliases;
+        # Use the vhost-specific email address if provided, otherwise let
+        # security.acme.email or security.acme.certs.<cert>.email be used.
+        email = mkOverride 2000 (if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr);
+      # Filter for enableACME-only vhosts. Don't want to create dud certs
+      }) (filter (hostOpts: hostOpts.useACMEHost == null) acmeEnabledVhosts);
+    in listToAttrs acmePairs;
+
+    # httpd requires a stable path to the configuration file for reloads
+    environment.etc."httpd/httpd.conf".source = cfg.configFile;
+    environment.systemPackages = [
+      apachectl
+      pkg
+    ];
+
+    services.logrotate = optionalAttrs (cfg.logFormat != "none") {
+      enable = mkDefault true;
+      paths.httpd = {
+        path = "${cfg.logDir}/*.log";
+        user = cfg.user;
+        group = cfg.group;
+        frequency = "daily";
+        keep = 28;
+        extraConfig = ''
+          sharedscripts
+          compress
+          delaycompress
+          postrotate
+            systemctl reload httpd.service > /dev/null 2>/dev/null || true
+          endscript
+        '';
+      };
+    };
+
+    services.httpd.phpOptions =
+      ''
+        ; Don't advertise PHP
+        expose_php = off
+      '' + optionalString (config.time.timeZone != null) ''
+
+        ; Apparently PHP doesn't use $TZ.
+        date.timezone = "${config.time.timeZone}"
+      '';
+
+    services.httpd.extraModules = mkBefore [
+      # HTTP authentication mechanisms: basic and digest.
+      "auth_basic" "auth_digest"
+
+      # Authentication: is the user who he claims to be?
+      "authn_file" "authn_dbm" "authn_anon"
+
+      # Authorization: is the user allowed access?
+      "authz_user" "authz_groupfile" "authz_host"
+
+      # Other modules.
+      "ext_filter" "include" "env" "mime_magic"
+      "cern_meta" "expires" "headers" "usertrack" "setenvif"
+      "dav" "status" "asis" "info" "dav_fs"
+      "vhost_alias" "imagemap" "actions" "speling"
+      "proxy" "proxy_http"
+      "cache" "cache_disk"
+
+      # For compatibility with old configurations, the new module mod_access_compat is provided.
+      "access_compat"
+    ];
+
+    systemd.tmpfiles.rules =
+      let
+        svc = config.systemd.services.httpd.serviceConfig;
+      in
+        [
+          "d '${cfg.logDir}' 0700 ${svc.User} ${svc.Group}"
+          "Z '${cfg.logDir}' - ${svc.User} ${svc.Group}"
+        ];
+
+    systemd.services.httpd = {
+        description = "Apache HTTPD";
+        wantedBy = [ "multi-user.target" ];
+        wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) dependentCertNames);
+        after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") dependentCertNames;
+        before = map (certName: "acme-${certName}.service") dependentCertNames;
+        restartTriggers = [ cfg.configFile ];
+
+        path = [ pkg pkgs.coreutils pkgs.gnugrep ];
+
+        environment =
+          optionalAttrs cfg.enablePHP { PHPRC = phpIni; }
+          // optionalAttrs cfg.enableMellon { LD_LIBRARY_PATH  = "${pkgs.xmlsec}/lib"; };
+
+        preStart =
+          ''
+            # Get rid of old semaphores.  These tend to accumulate across
+            # server restarts, eventually preventing it from restarting
+            # successfully.
+            for i in $(${pkgs.util-linux}/bin/ipcs -s | grep ' ${cfg.user} ' | cut -f2 -d ' '); do
+                ${pkgs.util-linux}/bin/ipcrm -s $i
+            done
+          '';
+
+        serviceConfig = {
+          ExecStart = "@${pkg}/bin/httpd httpd -f /etc/httpd/httpd.conf";
+          ExecStop = "${pkg}/bin/httpd -f /etc/httpd/httpd.conf -k graceful-stop";
+          ExecReload = "${pkg}/bin/httpd -f /etc/httpd/httpd.conf -k graceful";
+          User = cfg.user;
+          Group = cfg.group;
+          Type = "forking";
+          PIDFile = "${runtimeDir}/httpd.pid";
+          Restart = "always";
+          RestartSec = "5s";
+          RuntimeDirectory = "httpd httpd/runtime";
+          RuntimeDirectoryMode = "0750";
+          AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        };
+      };
+
+    # postRun hooks on cert renew can't be used to restart Apache since renewal
+    # runs as the unprivileged acme user. sslTargets are added to wantedBy + before
+    # which allows the acme-finished-$cert.target to signify the successful updating
+    # of certs end-to-end.
+    systemd.services.httpd-config-reload = let
+      sslServices = map (certName: "acme-${certName}.service") dependentCertNames;
+      sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames;
+    in mkIf (sslServices != []) {
+      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 = [ cfg.configFile ];
+      # Block reloading if not all certs exist yet.
+      # Happens when config changes add new vhosts/certs.
+      unitConfig.ConditionPathExists = map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames;
+      serviceConfig = {
+        Type = "oneshot";
+        TimeoutSec = 60;
+        ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active httpd.service";
+        ExecStartPre = "${pkg}/bin/httpd -f /etc/httpd/httpd.conf -t";
+        ExecStart = "/run/current-system/systemd/bin/systemctl reload httpd.service";
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/web-servers/apache-httpd/location-options.nix b/nixos/modules/services/web-servers/apache-httpd/location-options.nix
new file mode 100644
index 00000000000..8ea88f94f97
--- /dev/null
+++ b/nixos/modules/services/web-servers/apache-httpd/location-options.nix
@@ -0,0 +1,54 @@
+{ config, lib, name, ... }:
+let
+  inherit (lib) mkOption types;
+in
+{
+  options = {
+
+    proxyPass = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "http://www.example.org/";
+      description = ''
+        Sets up a simple reverse proxy as described by <link xlink:href="https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html#simple" />.
+      '';
+    };
+
+    index = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "index.php index.html";
+      description = ''
+        Adds DirectoryIndex directive. See <link xlink:href="https://httpd.apache.org/docs/2.4/mod/mod_dir.html#directoryindex" />.
+      '';
+    };
+
+    alias = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = "/your/alias/directory";
+      description = ''
+        Alias directory for requests. See <link xlink:href="https://httpd.apache.org/docs/2.4/mod/mod_alias.html#alias" />.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        These lines go to the end of the location verbatim.
+      '';
+    };
+
+    priority = mkOption {
+      type = types.int;
+      default = 1000;
+      description = ''
+        Order of this location block in relation to the others in the vhost.
+        The semantics are the same as with `lib.mkOrder`. Smaller values have
+        a greater priority.
+      '';
+    };
+
+  };
+}
diff --git a/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
new file mode 100644
index 00000000000..c52ab2c596e
--- /dev/null
+++ b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
@@ -0,0 +1,295 @@
+{ config, lib, name, ... }:
+let
+  inherit (lib) literalExpression mkOption nameValuePair types;
+in
+{
+  options = {
+
+    hostName = mkOption {
+      type = types.str;
+      default = name;
+      description = "Canonical hostname for the server.";
+    };
+
+    serverAliases = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = ["www.example.org" "www.example.org:8080" "example.org"];
+      description = ''
+        Additional names of virtual hosts served by this virtual host configuration.
+      '';
+    };
+
+    listen = mkOption {
+      type = with types; listOf (submodule ({
+        options = {
+          port = mkOption {
+            type = types.port;
+            description = "Port to listen on";
+          };
+          ip = mkOption {
+            type = types.str;
+            default = "*";
+            description = "IP to listen on. 0.0.0.0 for IPv4 only, * for all.";
+          };
+          ssl = mkOption {
+            type = types.bool;
+            default = false;
+            description = "Whether to enable SSL (https) support.";
+          };
+        };
+      }));
+      default = [];
+      example = [
+        { ip = "195.154.1.1"; port = 443; ssl = true;}
+        { ip = "192.154.1.1"; port = 80; }
+        { ip = "*"; port = 8080; }
+      ];
+      description = ''
+        Listen addresses and ports for this virtual host.
+        <note>
+        <para>
+          This option overrides <literal>addSSL</literal>, <literal>forceSSL</literal> and <literal>onlySSL</literal>.
+        </para>
+        <para>
+          If you only want to set the addresses manually and not the ports, take a look at <literal>listenAddresses</literal>.
+        </para>
+        </note>
+      '';
+    };
+
+    listenAddresses = mkOption {
+      type = with types; nonEmptyListOf str;
+
+      description = ''
+        Listen addresses for this virtual host.
+        Compared to <literal>listen</literal> this only sets the addreses
+        and the ports are chosen automatically.
+      '';
+      default = [ "*" ];
+      example = [ "127.0.0.1" ];
+    };
+
+    enableSSL = mkOption {
+      type = types.bool;
+      visible = false;
+      default = false;
+    };
+
+    addSSL = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable HTTPS in addition to plain HTTP. This will set defaults for
+        <literal>listen</literal> to listen on all interfaces on the respective default
+        ports (80, 443).
+      '';
+    };
+
+    onlySSL = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable HTTPS and reject plain HTTP connections. This will set
+        defaults for <literal>listen</literal> to listen on all interfaces on port 443.
+      '';
+    };
+
+    forceSSL = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to add a separate nginx server block that permanently redirects (301)
+        all plain HTTP traffic to HTTPS. This will set defaults for
+        <literal>listen</literal> to listen on all interfaces on the respective default
+        ports (80, 443), where the non-SSL listens are used for the redirect vhosts.
+      '';
+    };
+
+    enableACME = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to ask Let's Encrypt to sign a certificate for this vhost.
+        Alternately, you can use an existing certificate through <option>useACMEHost</option>.
+      '';
+    };
+
+    useACMEHost = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        A host of an existing Let's Encrypt certificate to use.
+        This is useful if you have many subdomains and want to avoid hitting the
+        <link xlink:href="https://letsencrypt.org/docs/rate-limits/">rate limit</link>.
+        Alternately, you can generate a certificate through <option>enableACME</option>.
+        <emphasis>Note that this option does not create any certificates, nor it does add subdomains to existing ones – you will need to create them manually using  <xref linkend="opt-security.acme.certs"/>.</emphasis>
+      '';
+    };
+
+    acmeRoot = mkOption {
+      type = types.nullOr types.str;
+      default = "/var/lib/acme/acme-challenge";
+      description = ''
+        Directory for the acme challenge which is PUBLIC, don't put certs or keys in here.
+        Set to null to inherit from config.security.acme.
+      '';
+    };
+
+    sslServerCert = mkOption {
+      type = types.path;
+      example = "/var/host.cert";
+      description = "Path to server SSL certificate.";
+    };
+
+    sslServerKey = mkOption {
+      type = types.path;
+      example = "/var/host.key";
+      description = "Path to server SSL certificate key.";
+    };
+
+    sslServerChain = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/var/ca.pem";
+      description = "Path to server SSL chain file.";
+    };
+
+    http2 = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to enable HTTP 2. HTTP/2 is supported in all multi-processing modules that come with httpd. <emphasis>However, if you use the prefork mpm, there will
+        be severe restrictions.</emphasis> Refer to <link xlink:href="https://httpd.apache.org/docs/2.4/howto/http2.html#mpm-config"/> for details.
+      '';
+    };
+
+    adminAddr = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "admin@example.org";
+      description = "E-mail address of the server administrator.";
+    };
+
+    documentRoot = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/data/webserver/docs";
+      description = ''
+        The path of Apache's document root directory.  If left undefined,
+        an empty directory in the Nix store will be used as root.
+      '';
+    };
+
+    servedDirs = mkOption {
+      type = types.listOf types.attrs;
+      default = [];
+      example = [
+        { urlPath = "/nix";
+          dir = "/home/eelco/Dev/nix-homepage";
+        }
+      ];
+      description = ''
+        This option provides a simple way to serve static directories.
+      '';
+    };
+
+    servedFiles = mkOption {
+      type = types.listOf types.attrs;
+      default = [];
+      example = [
+        { urlPath = "/foo/bar.png";
+          file = "/home/eelco/some-file.png";
+        }
+      ];
+      description = ''
+        This option provides a simple way to serve individual, static files.
+
+        <note><para>
+          This option has been deprecated and will be removed in a future
+          version of NixOS. You can achieve the same result by making use of
+          the <literal>locations.&lt;name&gt;.alias</literal> option.
+        </para></note>
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        <Directory /home>
+          Options FollowSymlinks
+          AllowOverride All
+        </Directory>
+      '';
+      description = ''
+        These lines go to httpd.conf verbatim. They will go after
+        directories and directory aliases defined by default.
+      '';
+    };
+
+    enableUserDir = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable serving <filename>~/public_html</filename> as
+        <literal>/~<replaceable>username</replaceable></literal>.
+      '';
+    };
+
+    globalRedirect = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "http://newserver.example.org/";
+      description = ''
+        If set, all requests for this host are redirected permanently to
+        the given URL.
+      '';
+    };
+
+    logFormat = mkOption {
+      type = types.str;
+      default = "common";
+      example = "combined";
+      description = ''
+        Log format for Apache's log files. Possible values are: combined, common, referer, agent.
+      '';
+    };
+
+    robotsEntries = mkOption {
+      type = types.lines;
+      default = "";
+      example = "Disallow: /foo/";
+      description = ''
+        Specification of pages to be ignored by web crawlers. See <link
+        xlink:href='http://www.robotstxt.org/'/> for details.
+      '';
+    };
+
+    locations = mkOption {
+      type = with types; attrsOf (submodule (import ./location-options.nix));
+      default = {};
+      example = literalExpression ''
+        {
+          "/" = {
+            proxyPass = "http://localhost:3000";
+          };
+          "/foo/bar.png" = {
+            alias = "/home/eelco/some-file.png";
+          };
+        };
+      '';
+      description = ''
+        Declarative location config. See <link
+        xlink:href="https://httpd.apache.org/docs/2.4/mod/core.html#location"/> for details.
+      '';
+    };
+
+  };
+
+  config = {
+
+    locations = builtins.listToAttrs (map (elem: nameValuePair elem.urlPath { alias = elem.file; }) config.servedFiles);
+
+  };
+}
diff --git a/nixos/modules/services/web-servers/caddy/default.nix b/nixos/modules/services/web-servers/caddy/default.nix
new file mode 100644
index 00000000000..2b8c6f2e308
--- /dev/null
+++ b/nixos/modules/services/web-servers/caddy/default.nix
@@ -0,0 +1,339 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.caddy;
+
+  virtualHosts = attrValues cfg.virtualHosts;
+  acmeVHosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts;
+
+  mkVHostConf = hostOpts:
+    let
+      sslCertDir = config.security.acme.certs.${hostOpts.useACMEHost}.directory;
+    in
+      ''
+        ${hostOpts.hostName} ${concatStringsSep " " hostOpts.serverAliases} {
+          bind ${concatStringsSep " " hostOpts.listenAddresses}
+          ${optionalString (hostOpts.useACMEHost != null) "tls ${sslCertDir}/cert.pem ${sslCertDir}/key.pem"}
+          log {
+            ${hostOpts.logFormat}
+          }
+
+          ${hostOpts.extraConfig}
+        }
+      '';
+
+  configFile =
+    let
+      Caddyfile = pkgs.writeText "Caddyfile" ''
+        {
+          ${cfg.globalConfig}
+        }
+        ${cfg.extraConfig}
+      '';
+
+      Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { nativeBuildInputs = [ cfg.package ]; } ''
+        ${cfg.package}/bin/caddy fmt ${Caddyfile} > $out
+      '';
+    in
+      if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then Caddyfile-formatted else Caddyfile;
+
+  acmeHosts = unique (catAttrs "useACMEHost" acmeVHosts);
+
+  mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix;
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "caddy" "agree" ] "this option is no longer necessary for Caddy 2")
+    (mkRenamedOptionModule [ "services" "caddy" "ca" ] [ "services" "caddy" "acmeCA" ])
+    (mkRenamedOptionModule [ "services" "caddy" "config" ] [ "services" "caddy" "extraConfig" ])
+  ];
+
+  # interface
+  options.services.caddy = {
+    enable = mkEnableOption "Caddy web server";
+
+    user = mkOption {
+      default = "caddy";
+      type = types.str;
+      description = ''
+        User account under which caddy runs.
+
+        <note><para>
+          If left as the default value this user will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the user exists before the Caddy service starts.
+        </para></note>
+      '';
+    };
+
+    group = mkOption {
+      default = "caddy";
+      type = types.str;
+      description = ''
+        Group account under which caddy runs.
+
+        <note><para>
+          If left as the default value this user will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the user exists before the Caddy service starts.
+        </para></note>
+      '';
+    };
+
+    package = mkOption {
+      default = pkgs.caddy;
+      defaultText = literalExpression "pkgs.caddy";
+      type = types.package;
+      description = ''
+        Caddy package to use.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/caddy";
+      description = ''
+        The data directory for caddy.
+
+        <note>
+          <para>
+            If left as the default value this directory will automatically be created
+            before the Caddy server starts, otherwise you are responsible for ensuring
+            the directory exists with appropriate ownership and permissions.
+          </para>
+          <para>
+            Caddy v2 replaced <literal>CADDYPATH</literal> with XDG directories.
+            See <link xlink:href="https://caddyserver.com/docs/conventions#file-locations"/>.
+          </para>
+        </note>
+      '';
+    };
+
+    logDir = mkOption {
+      type = types.path;
+      default = "/var/log/caddy";
+      description = ''
+        Directory for storing Caddy access logs.
+
+        <note><para>
+          If left as the default value this directory will automatically be created
+          before the Caddy server starts, otherwise the sysadmin is responsible for
+          ensuring the directory exists with appropriate ownership and permissions.
+        </para></note>
+      '';
+    };
+
+    logFormat = mkOption {
+      type = types.lines;
+      default = ''
+        level ERROR
+      '';
+      example = literalExpression ''
+        mkForce "level INFO";
+      '';
+      description = ''
+        Configuration for the default logger. See
+        <link xlink:href="https://caddyserver.com/docs/caddyfile/options#log"/>
+        for details.
+      '';
+    };
+
+    configFile = mkOption {
+      type = types.path;
+      default = configFile;
+      defaultText = "A Caddyfile automatically generated by values from services.caddy.*";
+      example = literalExpression ''
+        pkgs.writeText "Caddyfile" '''
+          example.com
+
+          root * /var/www/wordpress
+          php_fastcgi unix//run/php/php-version-fpm.sock
+          file_server
+        ''';
+      '';
+      description = ''
+        Override the configuration file used by Caddy. By default,
+        NixOS generates one automatically.
+      '';
+    };
+
+    adapter = mkOption {
+      default = "caddyfile";
+      example = "nginx";
+      type = types.str;
+      description = ''
+        Name of the config adapter to use.
+        See <link xlink:href="https://caddyserver.com/docs/config-adapters"/>
+        for the full list.
+
+        <note><para>
+          Any value other than <literal>caddyfile</literal> is only valid when
+          providing your own <option>configFile</option>.
+        </para></note>
+      '';
+    };
+
+    resume = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Use saved config, if any (and prefer over any specified configuration passed with <literal>--config</literal>).
+      '';
+    };
+
+    globalConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        debug
+        servers {
+          protocol {
+            experimental_http3
+          }
+        }
+      '';
+      description = ''
+        Additional lines of configuration appended to the global config section
+        of the <literal>Caddyfile</literal>.
+
+        Refer to <link xlink:href="https://caddyserver.com/docs/caddyfile/options#global-options"/>
+        for details on supported values.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        example.com {
+          encode gzip
+          log
+          root /srv/http
+        }
+      '';
+      description = ''
+        Additional lines of configuration appended to the automatically
+        generated <literal>Caddyfile</literal>.
+      '';
+    };
+
+    virtualHosts = mkOption {
+      type = with types; attrsOf (submodule (import ./vhost-options.nix { inherit cfg; }));
+      default = {};
+      example = literalExpression ''
+        {
+          "hydra.example.com" = {
+            serverAliases = [ "www.hydra.example.com" ];
+            extraConfig = '''
+              encode gzip
+              root /srv/http
+            ''';
+          };
+        };
+      '';
+      description = ''
+        Declarative specification of virtual hosts served by Caddy.
+      '';
+    };
+
+    acmeCA = mkOption {
+      default = "https://acme-v02.api.letsencrypt.org/directory";
+      example = "https://acme-staging-v02.api.letsencrypt.org/directory";
+      type = with types; nullOr str;
+      description = ''
+        The URL to the ACME CA's directory. It is strongly recommended to set
+        this to Let's Encrypt's staging endpoint for testing or development.
+
+        Set it to <literal>null</literal> if you want to write a more
+        fine-grained configuration manually.
+      '';
+    };
+
+    email = mkOption {
+      default = null;
+      type = with types; nullOr str;
+      description = ''
+        Your email address. Mainly used when creating an ACME account with your
+        CA, and is highly recommended in case there are problems with your
+        certificates.
+      '';
+    };
+
+  };
+
+  # implementation
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = cfg.adapter != "caddyfile" -> cfg.configFile != configFile;
+        message = "Any value other than 'caddyfile' is only valid when providing your own `services.caddy.configFile`";
+      }
+    ] ++ map (name: mkCertOwnershipAssertion {
+      inherit (cfg) group user;
+      cert = config.security.acme.certs.${name};
+      groups = config.users.groups;
+    }) acmeHosts;
+
+    services.caddy.extraConfig = concatMapStringsSep "\n" mkVHostConf virtualHosts;
+    services.caddy.globalConfig = ''
+      ${optionalString (cfg.email != null) "email ${cfg.email}"}
+      ${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"}
+      log {
+        ${cfg.logFormat}
+      }
+    '';
+
+    systemd.packages = [ cfg.package ];
+    systemd.services.caddy = {
+      wants = map (hostOpts: "acme-finished-${hostOpts.useACMEHost}.target") acmeVHosts;
+      after = map (hostOpts: "acme-selfsigned-${hostOpts.useACMEHost}.service") acmeVHosts;
+      before = map (hostOpts: "acme-${hostOpts.useACMEHost}.service") acmeVHosts;
+
+      wantedBy = [ "multi-user.target" ];
+      startLimitIntervalSec = 14400;
+      startLimitBurst = 10;
+
+      serviceConfig = {
+        # https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart=
+        # If the empty string is assigned to this option, the list of commands to start is reset, prior assignments of this option will have no effect.
+        ExecStart = [ "" "${cfg.package}/bin/caddy run --config ${cfg.configFile} --adapter ${cfg.adapter} ${optionalString cfg.resume "--resume"}" ];
+        ExecReload = [ "" "${cfg.package}/bin/caddy reload --config ${cfg.configFile} --adapter ${cfg.adapter}" ];
+
+        ExecStartPre = "${cfg.package}/bin/caddy validate --config ${cfg.configFile} --adapter ${cfg.adapter}";
+        User = cfg.user;
+        Group = cfg.group;
+        ReadWriteDirectories = cfg.dataDir;
+        StateDirectory = mkIf (cfg.dataDir == "/var/lib/caddy") [ "caddy" ];
+        LogsDirectory = mkIf (cfg.logDir == "/var/log/caddy") [ "caddy" ];
+        Restart = "on-abnormal";
+        SupplementaryGroups = mkIf (length acmeVHosts != 0) [ "acme" ];
+
+        # TODO: attempt to upstream these options
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        ProtectHome = true;
+      };
+    };
+
+    users.users = optionalAttrs (cfg.user == "caddy") {
+      caddy = {
+        group = cfg.group;
+        uid = config.ids.uids.caddy;
+        home = cfg.dataDir;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "caddy") {
+      caddy.gid = config.ids.gids.caddy;
+    };
+
+    security.acme.certs =
+      let
+        reloads = map (useACMEHost: nameValuePair useACMEHost { reloadServices = [ "caddy.service" ]; }) acmeHosts;
+      in
+        listToAttrs reloads;
+
+  };
+}
diff --git a/nixos/modules/services/web-servers/caddy/vhost-options.nix b/nixos/modules/services/web-servers/caddy/vhost-options.nix
new file mode 100644
index 00000000000..f240ec605c2
--- /dev/null
+++ b/nixos/modules/services/web-servers/caddy/vhost-options.nix
@@ -0,0 +1,79 @@
+{ cfg }:
+{ config, lib, name, ... }:
+let
+  inherit (lib) literalExpression mkOption types;
+in
+{
+  options = {
+
+    hostName = mkOption {
+      type = types.str;
+      default = name;
+      description = "Canonical hostname for the server.";
+    };
+
+    serverAliases = mkOption {
+      type = with types; listOf str;
+      default = [ ];
+      example = [ "www.example.org" "example.org" ];
+      description = ''
+        Additional names of virtual hosts served by this virtual host configuration.
+      '';
+    };
+
+    listenAddresses = mkOption {
+      type = with types; listOf str;
+      description = ''
+        A list of host interfaces to bind to for this virtual host.
+      '';
+      default = [ ];
+      example = [ "127.0.0.1" "::1" ];
+    };
+
+    useACMEHost = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        A host of an existing Let's Encrypt certificate to use.
+        This is mostly useful if you use DNS challenges but Caddy does not
+        currently support your provider.
+
+        <emphasis>Note that this option does not create any certificates, nor
+        does it add subdomains to existing ones – you will need to create them
+        manually using <xref linkend="opt-security.acme.certs"/>. Additionally,
+        you should probably add the <literal>caddy</literal> user to the
+        <literal>acme</literal> group to grant access to the certificates.</emphasis>
+      '';
+    };
+
+    logFormat = mkOption {
+      type = types.lines;
+      default = ''
+        output file ${cfg.logDir}/access-${config.hostName}.log
+      '';
+      defaultText = ''
+        output file ''${config.services.caddy.logDir}/access-''${hostName}.log
+      '';
+      example = literalExpression ''
+        mkForce '''
+          output discard
+        ''';
+      '';
+      description = ''
+        Configuration for HTTP request logging (also known as access logs). See
+        <link xlink:href="https://caddyserver.com/docs/caddyfile/directives/log#log"/>
+        for details.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Additional lines of configuration appended to this virtual host in the
+        automatically generated <literal>Caddyfile</literal>.
+      '';
+    };
+
+  };
+}
diff --git a/nixos/modules/services/web-servers/darkhttpd.nix b/nixos/modules/services/web-servers/darkhttpd.nix
new file mode 100644
index 00000000000..f6b693139a1
--- /dev/null
+++ b/nixos/modules/services/web-servers/darkhttpd.nix
@@ -0,0 +1,77 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.darkhttpd;
+
+  args = concatStringsSep " " ([
+    cfg.rootDir
+    "--port ${toString cfg.port}"
+    "--addr ${cfg.address}"
+  ] ++ cfg.extraArgs
+    ++ optional cfg.hideServerId             "--no-server-id"
+    ++ optional config.networking.enableIPv6 "--ipv6");
+
+in {
+  options.services.darkhttpd = with types; {
+    enable = mkEnableOption "DarkHTTPd web server";
+
+    port = mkOption {
+      default = 80;
+      type = types.port;
+      description = ''
+        Port to listen on.
+        Pass 0 to let the system choose any free port for you.
+      '';
+    };
+
+    address = mkOption {
+      default = "127.0.0.1";
+      type = str;
+      description = ''
+        Address to listen on.
+        Pass `all` to listen on all interfaces.
+      '';
+    };
+
+    rootDir = mkOption {
+      type = path;
+      description = ''
+        Path from which to serve files.
+      '';
+    };
+
+    hideServerId = mkOption {
+      type = bool;
+      default = true;
+      description = ''
+        Don't identify the server type in headers or directory listings.
+      '';
+    };
+
+    extraArgs = mkOption {
+      type = listOf str;
+      default = [];
+      description = ''
+        Additional configuration passed to the executable.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.darkhttpd = {
+      description = "Dark HTTPd";
+      wants = [ "network.target" ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${pkgs.darkhttpd}/bin/darkhttpd ${args}";
+        AmbientCapabilities = lib.mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+        Restart = "on-failure";
+        RestartSec = "2s";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-servers/fcgiwrap.nix b/nixos/modules/services/web-servers/fcgiwrap.nix
new file mode 100644
index 00000000000..a64a187255a
--- /dev/null
+++ b/nixos/modules/services/web-servers/fcgiwrap.nix
@@ -0,0 +1,72 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.fcgiwrap;
+in {
+
+  options = {
+    services.fcgiwrap = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable fcgiwrap, a server for running CGI applications over FastCGI.";
+      };
+
+      preforkProcesses = mkOption {
+        type = types.int;
+        default = 1;
+        description = "Number of processes to prefork.";
+      };
+
+      socketType = mkOption {
+        type = types.enum [ "unix" "tcp" "tcp6" ];
+        default = "unix";
+        description = "Socket type: 'unix', 'tcp' or 'tcp6'.";
+      };
+
+      socketAddress = mkOption {
+        type = types.str;
+        default = "/run/fcgiwrap.sock";
+        example = "1.2.3.4:5678";
+        description = "Socket address. In case of a UNIX socket, this should be its filesystem path.";
+      };
+
+      user = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "User permissions for the socket.";
+      };
+
+      group = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Group permissions for the socket.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.fcgiwrap = {
+      after = [ "nss-user-lookup.target" ];
+      wantedBy = optional (cfg.socketType != "unix") "multi-user.target";
+
+      serviceConfig = {
+        ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${builtins.toString cfg.preforkProcesses} ${
+          if (cfg.socketType != "unix") then "-s ${cfg.socketType}:${cfg.socketAddress}" else ""
+        }";
+      } // (if cfg.user != null && cfg.group != null then {
+        User = cfg.user;
+        Group = cfg.group;
+      } else { } );
+    };
+
+    systemd.sockets = if (cfg.socketType == "unix") then {
+      fcgiwrap = {
+        wantedBy = [ "sockets.target" ];
+        socketConfig.ListenStream = cfg.socketAddress;
+      };
+    } else { };
+  };
+}
diff --git a/nixos/modules/services/web-servers/hitch/default.nix b/nixos/modules/services/web-servers/hitch/default.nix
new file mode 100644
index 00000000000..1812f225b74
--- /dev/null
+++ b/nixos/modules/services/web-servers/hitch/default.nix
@@ -0,0 +1,111 @@
+{ config, lib, pkgs, ...}:
+let
+  cfg = config.services.hitch;
+  ocspDir = lib.optionalString cfg.ocsp-stapling.enabled "/var/cache/hitch/ocsp";
+  hitchConfig = with lib; pkgs.writeText "hitch.conf" (concatStringsSep "\n" [
+    ("backend = \"${cfg.backend}\"")
+    (concatMapStrings (s: "frontend = \"${s}\"\n") cfg.frontend)
+    (concatMapStrings (s: "pem-file = \"${s}\"\n") cfg.pem-files)
+    ("ciphers = \"${cfg.ciphers}\"")
+    ("ocsp-dir = \"${ocspDir}\"")
+    "user = \"${cfg.user}\""
+    "group = \"${cfg.group}\""
+    cfg.extraConfig
+  ]);
+in
+with lib;
+{
+  options = {
+    services.hitch = {
+      enable = mkEnableOption "Hitch Server";
+
+      backend = mkOption {
+        type = types.str;
+        description = ''
+          The host and port Hitch connects to when receiving
+          a connection in the form [HOST]:PORT
+        '';
+      };
+
+      ciphers = mkOption {
+        type = types.str;
+        default = "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
+        description = "The list of ciphers to use";
+      };
+
+      frontend = mkOption {
+        type = types.either types.str (types.listOf types.str);
+        default = "[127.0.0.1]:443";
+        description = ''
+          The port and interface of the listen endpoint in the
++         form [HOST]:PORT[+CERT].
+        '';
+        apply = toList;
+      };
+
+      pem-files = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = "PEM files to use";
+      };
+
+      ocsp-stapling = {
+        enabled = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Whether to enable OCSP Stapling";
+        };
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "hitch";
+        description = "The user to run as";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "hitch";
+        description = "The group to run as";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Additional configuration lines";
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.hitch = {
+      description = "Hitch";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      preStart = ''
+        ${pkgs.hitch}/sbin/hitch -t --config ${hitchConfig}
+      '' + (optionalString cfg.ocsp-stapling.enabled ''
+        mkdir -p ${ocspDir}
+        chown -R hitch:hitch ${ocspDir}
+      '');
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${pkgs.hitch}/sbin/hitch --daemon --config ${hitchConfig}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        Restart = "always";
+        RestartSec = "5s";
+        LimitNOFILE = 131072;
+      };
+    };
+
+    environment.systemPackages = [ pkgs.hitch ];
+
+    users.users.hitch = {
+      group = "hitch";
+      isSystemUser = true;
+    };
+    users.groups.hitch = {};
+  };
+}
diff --git a/nixos/modules/services/web-servers/hydron.nix b/nixos/modules/services/web-servers/hydron.nix
new file mode 100644
index 00000000000..a4a5a435b2e
--- /dev/null
+++ b/nixos/modules/services/web-servers/hydron.nix
@@ -0,0 +1,165 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.hydron;
+in with lib; {
+  options.services.hydron = {
+    enable = mkEnableOption "hydron";
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/hydron";
+      example = "/home/okina/hydron";
+      description = "Location where hydron runs and stores data.";
+    };
+
+    interval = mkOption {
+      type = types.str;
+      default = "weekly";
+      example = "06:00";
+      description = ''
+        How often we run hydron import and possibly fetch tags. Runs by default every week.
+
+        The format is described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.
+      '';
+    };
+
+    password = mkOption {
+      type = types.str;
+      default = "hydron";
+      example = "dumbpass";
+      description = "Password for the hydron database.";
+    };
+
+    passwordFile = mkOption {
+      type = types.path;
+      default = "/run/keys/hydron-password-file";
+      example = "/home/okina/hydron/keys/pass";
+      description = "Password file for the hydron database.";
+    };
+
+    postgresArgs = mkOption {
+      type = types.str;
+      description = "Postgresql connection arguments.";
+      example = ''
+        {
+          "driver": "postgres",
+          "connection": "user=hydron password=dumbpass dbname=hydron sslmode=disable"
+        }
+      '';
+    };
+
+    postgresArgsFile = mkOption {
+      type = types.path;
+      default = "/run/keys/hydron-postgres-args";
+      example = "/home/okina/hydron/keys/postgres";
+      description = "Postgresql connection arguments file.";
+    };
+
+    listenAddress = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "127.0.0.1:8010";
+      description = "Listen on a specific IP address and port.";
+    };
+
+    importPaths = mkOption {
+      type = types.listOf types.path;
+      default = [];
+      example = [ "/home/okina/Pictures" ];
+      description = "Paths that hydron will recursively import.";
+    };
+
+    fetchTags = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Fetch tags for imported images and webm from gelbooru.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.hydron.passwordFile = mkDefault (pkgs.writeText "hydron-password-file" cfg.password);
+    services.hydron.postgresArgsFile = mkDefault (pkgs.writeText "hydron-postgres-args" cfg.postgresArgs);
+    services.hydron.postgresArgs = mkDefault ''
+      {
+        "driver": "postgres",
+        "connection": "user=hydron password=${cfg.password} host=/run/postgresql dbname=hydron sslmode=disable"
+      }
+    '';
+
+    services.postgresql = {
+      enable = true;
+      ensureDatabases = [ "hydron" ];
+      ensureUsers = [
+        { name = "hydron";
+          ensurePermissions = { "DATABASE hydron" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0750 hydron hydron - -"
+      "d '${cfg.dataDir}/.hydron' - hydron hydron - -"
+      "d '${cfg.dataDir}/images' - hydron hydron - -"
+      "Z '${cfg.dataDir}' - hydron hydron - -"
+
+      "L+ '${cfg.dataDir}/.hydron/db_conf.json' - - - - ${cfg.postgresArgsFile}"
+    ];
+
+    systemd.services.hydron = {
+      description = "hydron";
+      after = [ "network.target" "postgresql.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        User = "hydron";
+        Group = "hydron";
+        ExecStart = "${pkgs.hydron}/bin/hydron serve"
+        + optionalString (cfg.listenAddress != null) " -a ${cfg.listenAddress}";
+      };
+    };
+
+    systemd.services.hydron-fetch = {
+      description = "Import paths into hydron and possibly fetch tags";
+
+      serviceConfig = {
+        Type = "oneshot";
+        User = "hydron";
+        Group = "hydron";
+        ExecStart = "${pkgs.hydron}/bin/hydron import "
+        + optionalString cfg.fetchTags "-f "
+        + (escapeShellArg cfg.dataDir) + "/images " + (escapeShellArgs cfg.importPaths);
+      };
+    };
+
+    systemd.timers.hydron-fetch = {
+      description = "Automatically import paths into hydron and possibly fetch tags";
+      after = [ "network.target" "hydron.service" ];
+      wantedBy = [ "timers.target" ];
+
+      timerConfig = {
+        Persistent = true;
+        OnCalendar = cfg.interval;
+      };
+    };
+
+    users = {
+      groups.hydron.gid = config.ids.gids.hydron;
+
+      users.hydron = {
+        description = "hydron server service user";
+        home = cfg.dataDir;
+        group = "hydron";
+        uid = config.ids.uids.hydron;
+      };
+    };
+  };
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "hydron" "baseDir" ] [ "services" "hydron" "dataDir" ])
+  ];
+
+  meta.maintainers = with maintainers; [ chiiruno ];
+}
diff --git a/nixos/modules/services/web-servers/jboss/builder.sh b/nixos/modules/services/web-servers/jboss/builder.sh
new file mode 100644
index 00000000000..0e5af324c13
--- /dev/null
+++ b/nixos/modules/services/web-servers/jboss/builder.sh
@@ -0,0 +1,72 @@
+set -e
+
+source $stdenv/setup
+
+mkdir -p $out/bin
+
+cat > $out/bin/control <<EOF
+mkdir -p $logDir
+chown -R $user $logDir
+export PATH=$PATH:$su/bin
+
+start()
+{
+  su $user -s /bin/sh -c "$jboss/bin/run.sh \
+      -Djboss.server.base.dir=$serverDir \
+      -Djboss.server.base.url=file://$serverDir \
+      -Djboss.server.temp.dir=$tempDir \
+      -Djboss.server.log.dir=$logDir \
+      -Djboss.server.lib.url=$libUrl \
+      -c default"
+}
+
+stop()
+{
+  su $user -s /bin/sh -c "$jboss/bin/shutdown.sh -S"
+}
+
+if test "\$1" = start
+then
+  trap stop 15
+
+  start
+elif test "\$1" = stop
+then
+  stop
+elif test "\$1" = init
+then
+  echo "Are you sure you want to create a new server instance (old server instance will be lost!)?"
+  read answer
+
+  if ! test \$answer = "yes"
+  then
+    exit 1
+  fi
+
+  rm -rf $serverDir
+  mkdir -p $serverDir
+  cd $serverDir
+  cp -av $jboss/server/default .
+  sed -i -e "s|deploy/|$deployDir|" default/conf/jboss-service.xml
+
+  if ! test "$useJK" = ""
+  then
+    sed -i -e 's|<attribute name="UseJK">false</attribute>|<attribute name="UseJK">true</attribute>|' default/deploy/jboss-web.deployer/META-INF/jboss-service.xml
+    sed -i -e 's|<Engine name="jboss.web" defaultHost="localhost">|<Engine name="jboss.web" defaultHost="localhost" jvmRoute="node1">|' default/deploy/jboss-web.deployer/server.xml
+  fi
+
+  # Make files accessible for the server user
+
+  chown -R $user $serverDir
+  for i in \`find $serverDir -type d\`
+  do
+    chmod 755 \$i
+  done
+  for i in \`find $serverDir -type f\`
+  do
+    chmod 644 \$i
+  done
+fi
+EOF
+
+chmod +x $out/bin/*
diff --git a/nixos/modules/services/web-servers/jboss/default.nix b/nixos/modules/services/web-servers/jboss/default.nix
new file mode 100644
index 00000000000..d243e0f3f1b
--- /dev/null
+++ b/nixos/modules/services/web-servers/jboss/default.nix
@@ -0,0 +1,88 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.jboss;
+
+  jbossService = pkgs.stdenv.mkDerivation {
+    name = "jboss-server";
+    builder = ./builder.sh;
+    inherit (pkgs) jboss su;
+    inherit (cfg) tempDir logDir libUrl deployDir serverDir user useJK;
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.jboss = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable JBoss. WARNING : this package is outdated and is known to have vulnerabilities.";
+      };
+
+      tempDir = mkOption {
+        default = "/tmp";
+        type = types.str;
+        description = "Location where JBoss stores its temp files";
+      };
+
+      logDir = mkOption {
+        default = "/var/log/jboss";
+        type = types.str;
+        description = "Location of the logfile directory of JBoss";
+      };
+
+      serverDir = mkOption {
+        description = "Location of the server instance files";
+        default = "/var/jboss/server";
+        type = types.str;
+      };
+
+      deployDir = mkOption {
+        description = "Location of the deployment files";
+        default = "/nix/var/nix/profiles/default/server/default/deploy/";
+        type = types.str;
+      };
+
+      libUrl = mkOption {
+        default = "file:///nix/var/nix/profiles/default/server/default/lib";
+        description = "Location where the shared library JARs are stored";
+        type = types.str;
+      };
+
+      user = mkOption {
+        default = "nobody";
+        description = "User account under which jboss runs.";
+        type = types.str;
+      };
+
+      useJK = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to use to connector to the Apache HTTP server";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.jboss.enable {
+    systemd.services.jboss = {
+      description = "JBoss server";
+      script = "${jbossService}/bin/control start";
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/web-servers/lighttpd/cgit.nix b/nixos/modules/services/web-servers/lighttpd/cgit.nix
new file mode 100644
index 00000000000..8cd6d020940
--- /dev/null
+++ b/nixos/modules/services/web-servers/lighttpd/cgit.nix
@@ -0,0 +1,93 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.lighttpd.cgit;
+  pathPrefix = if stringLength cfg.subdir == 0 then "" else "/" + cfg.subdir;
+  configFile = pkgs.writeText "cgitrc"
+    ''
+      # default paths to static assets
+      css=${pathPrefix}/cgit.css
+      logo=${pathPrefix}/cgit.png
+      favicon=${pathPrefix}/favicon.ico
+
+      # user configuration
+      ${cfg.configText}
+    '';
+in
+{
+
+  options.services.lighttpd.cgit = {
+
+    enable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        If true, enable cgit (fast web interface for git repositories) as a
+        sub-service in lighttpd.
+      '';
+    };
+
+    subdir = mkOption {
+      default = "cgit";
+      example = "";
+      type = types.str;
+      description = ''
+        The subdirectory in which to serve cgit. The web application will be
+        accessible at http://yourserver/''${subdir}
+      '';
+    };
+
+    configText = mkOption {
+      default = "";
+      example = literalExpression ''
+        '''
+          source-filter=''${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py
+          about-filter=''${pkgs.cgit}/lib/cgit/filters/about-formatting.sh
+          cache-size=1000
+          scan-path=/srv/git
+        '''
+      '';
+      type = types.lines;
+      description = ''
+        Verbatim contents of the cgit runtime configuration file. Documentation
+        (with cgitrc example file) is available in "man cgitrc". Or online:
+        http://git.zx2c4.com/cgit/tree/cgitrc.5.txt
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    # make the cgitrc manpage available
+    environment.systemPackages = [ pkgs.cgit ];
+
+    # declare module dependencies
+    services.lighttpd.enableModules = [ "mod_cgi" "mod_alias" "mod_setenv" ];
+
+    services.lighttpd.extraConfig = ''
+      $HTTP["url"] =~ "^/${cfg.subdir}" {
+          cgi.assign = (
+              "cgit.cgi" => "${pkgs.cgit}/cgit/cgit.cgi"
+          )
+          alias.url = (
+              "${pathPrefix}/cgit.css" => "${pkgs.cgit}/cgit/cgit.css",
+              "${pathPrefix}/cgit.png" => "${pkgs.cgit}/cgit/cgit.png",
+              "${pathPrefix}"          => "${pkgs.cgit}/cgit/cgit.cgi"
+          )
+          setenv.add-environment = (
+              "CGIT_CONFIG" => "${configFile}"
+          )
+      }
+    '';
+
+    systemd.services.lighttpd.preStart = ''
+      mkdir -p /var/cache/cgit
+      chown lighttpd:lighttpd /var/cache/cgit
+    '';
+
+  };
+
+}
diff --git a/nixos/modules/services/web-servers/lighttpd/collectd.nix b/nixos/modules/services/web-servers/lighttpd/collectd.nix
new file mode 100644
index 00000000000..5f091591daf
--- /dev/null
+++ b/nixos/modules/services/web-servers/lighttpd/collectd.nix
@@ -0,0 +1,62 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.lighttpd.collectd;
+  opt = options.services.lighttpd.collectd;
+
+  collectionConf = pkgs.writeText "collection.conf" ''
+    datadir: "${config.services.collectd.dataDir}"
+    libdir: "${config.services.collectd.package}/lib/collectd"
+  '';
+
+  defaultCollectionCgi = config.services.collectd.package.overrideDerivation(old: {
+    name = "collection.cgi";
+    dontConfigure = true;
+    buildPhase = "true";
+    installPhase = ''
+      substituteInPlace contrib/collection.cgi --replace '"/etc/collection.conf"' '$ENV{COLLECTION_CONF}'
+      cp contrib/collection.cgi $out
+    '';
+  });
+in
+{
+
+  options.services.lighttpd.collectd = {
+
+    enable = mkEnableOption "collectd subservice accessible at http://yourserver/collectd";
+
+    collectionCgi = mkOption {
+      type = types.path;
+      default = defaultCollectionCgi;
+      defaultText = literalDocBook ''
+        <literal>config.${options.services.collectd.package}</literal> configured for lighttpd
+      '';
+      description = ''
+        Path to collection.cgi script from (collectd sources)/contrib/collection.cgi
+        This option allows to use a customized version
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.lighttpd.enableModules = [ "mod_cgi" "mod_alias" "mod_setenv" ];
+
+    services.lighttpd.extraConfig = ''
+      $HTTP["url"] =~ "^/collectd" {
+        cgi.assign = (
+          ".cgi" => "${pkgs.perl}/bin/perl"
+        )
+        alias.url = (
+          "/collectd" => "${cfg.collectionCgi}"
+        )
+        setenv.add-environment = (
+          "PERL5LIB" => "${with pkgs.perlPackages; makePerlPath [ CGI HTMLParser URI pkgs.rrdtool ]}",
+          "COLLECTION_CONF" => "${collectionConf}"
+        )
+      }
+    '';
+  };
+
+}
diff --git a/nixos/modules/services/web-servers/lighttpd/default.nix b/nixos/modules/services/web-servers/lighttpd/default.nix
new file mode 100644
index 00000000000..05e897c8cc9
--- /dev/null
+++ b/nixos/modules/services/web-servers/lighttpd/default.nix
@@ -0,0 +1,268 @@
+# NixOS module for lighttpd web server
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.lighttpd;
+
+  # List of known lighttpd modules, ordered by how the lighttpd documentation
+  # recommends them being imported:
+  # http://redmine.lighttpd.net/projects/1/wiki/Server_modulesDetails
+  #
+  # Some modules are always imported and should not appear in the config:
+  # disallowedModules = [ "mod_indexfile" "mod_dirlisting" "mod_staticfile" ];
+  #
+  # For full module list, see the output of running ./configure in the lighttpd
+  # source.
+  allKnownModules = [
+    "mod_rewrite"
+    "mod_redirect"
+    "mod_alias"
+    "mod_access"
+    "mod_auth"
+    "mod_status"
+    "mod_simple_vhost"
+    "mod_evhost"
+    "mod_userdir"
+    "mod_secdownload"
+    "mod_fastcgi"
+    "mod_proxy"
+    "mod_cgi"
+    "mod_ssi"
+    "mod_compress"
+    "mod_usertrack"
+    "mod_expire"
+    "mod_rrdtool"
+    "mod_accesslog"
+    # Remaining list of modules, order assumed to be unimportant.
+    "mod_authn_dbi"
+    "mod_authn_file"
+    "mod_authn_gssapi"
+    "mod_authn_ldap"
+    "mod_authn_mysql"
+    "mod_authn_pam"
+    "mod_authn_sasl"
+    "mod_cml"
+    "mod_deflate"
+    "mod_evasive"
+    "mod_extforward"
+    "mod_flv_streaming"
+    "mod_geoip"
+    "mod_magnet"
+    "mod_mysql_vhost"
+    "mod_openssl"  # since v1.4.46
+    "mod_scgi"
+    "mod_setenv"
+    "mod_trigger_b4_dl"
+    "mod_uploadprogress"
+    "mod_vhostdb"  # since v1.4.46
+    "mod_webdav"
+    "mod_wstunnel"  # since v1.4.46
+  ];
+
+  maybeModuleString = moduleName:
+    if elem moduleName cfg.enableModules then ''"${moduleName}"'' else "";
+
+  modulesIncludeString = concatStringsSep ",\n"
+    (filter (x: x != "") (map maybeModuleString allKnownModules));
+
+  configFile = if cfg.configText != "" then
+    pkgs.writeText "lighttpd.conf" ''
+      ${cfg.configText}
+    ''
+    else
+    pkgs.writeText "lighttpd.conf" ''
+      server.document-root = "${cfg.document-root}"
+      server.port = ${toString cfg.port}
+      server.username = "lighttpd"
+      server.groupname = "lighttpd"
+
+      # As for why all modules are loaded here, instead of having small
+      # server.modules += () entries in each sub-service extraConfig snippet,
+      # read this:
+      #
+      #   http://redmine.lighttpd.net/projects/1/wiki/Server_modulesDetails
+      #   http://redmine.lighttpd.net/issues/2337
+      #
+      # Basically, lighttpd doesn't want to load (or even silently ignore) a
+      # module for a second time, and there is no way to check if a module has
+      # been loaded already. So if two services were to put the same module in
+      # server.modules += (), that would break the lighttpd configuration.
+      server.modules = (
+          ${modulesIncludeString}
+      )
+
+      # Logging (logs end up in systemd journal)
+      accesslog.use-syslog = "enable"
+      server.errorlog-use-syslog = "enable"
+
+      ${lib.optionalString cfg.enableUpstreamMimeTypes ''
+      include "${pkgs.lighttpd}/share/lighttpd/doc/config/conf.d/mime.conf"
+      ''}
+
+      static-file.exclude-extensions = ( ".fcgi", ".php", ".rb", "~", ".inc" )
+      index-file.names = ( "index.html" )
+
+      ${if cfg.mod_userdir then ''
+        userdir.path = "public_html"
+      '' else ""}
+
+      ${if cfg.mod_status then ''
+        status.status-url = "/server-status"
+        status.statistics-url = "/server-statistics"
+        status.config-url = "/server-config"
+      '' else ""}
+
+      ${cfg.extraConfig}
+    '';
+
+in
+
+{
+
+  options = {
+
+    services.lighttpd = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable the lighttpd web server.
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.lighttpd;
+        defaultText = "pkgs.lighttpd";
+        type = types.package;
+        description = ''
+          lighttpd package to use.
+        '';
+      };
+
+      port = mkOption {
+        default = 80;
+        type = types.port;
+        description = ''
+          TCP port number for lighttpd to bind to.
+        '';
+      };
+
+      document-root = mkOption {
+        default = "/srv/www";
+        type = types.path;
+        description = ''
+          Document-root of the web server. Must be readable by the "lighttpd" user.
+        '';
+      };
+
+      mod_userdir = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If true, requests in the form /~user/page.html are rewritten to take
+          the file public_html/page.html from the home directory of the user.
+        '';
+      };
+
+      enableModules = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "mod_cgi" "mod_status" ];
+        description = ''
+          List of lighttpd modules to enable. Sub-services take care of
+          enabling modules as needed, so this option is mainly for when you
+          want to add custom stuff to
+          <option>services.lighttpd.extraConfig</option> that depends on a
+          certain module.
+        '';
+      };
+
+      enableUpstreamMimeTypes = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to include the list of mime types bundled with lighttpd
+          (upstream). If you disable this, no mime types will be added by
+          NixOS and you will have to add your own mime types in
+          <option>services.lighttpd.extraConfig</option>.
+        '';
+      };
+
+      mod_status = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Show server status overview at /server-status, statistics at
+          /server-statistics and list of loaded modules at /server-config.
+        '';
+      };
+
+      configText = mkOption {
+        default = "";
+        type = types.lines;
+        example = "...verbatim config file contents...";
+        description = ''
+          Overridable config file contents to use for lighttpd. By default, use
+          the contents automatically generated by NixOS.
+        '';
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          These configuration lines will be appended to the generated lighttpd
+          config file. Note that this mechanism does not work when the manual
+          <option>configText</option> option is used.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = all (x: elem x allKnownModules) cfg.enableModules;
+        message = ''
+          One (or more) modules in services.lighttpd.enableModules are
+          unrecognized.
+
+          Known modules: ${toString allKnownModules}
+
+          services.lighttpd.enableModules: ${toString cfg.enableModules}
+        '';
+      }
+    ];
+
+    services.lighttpd.enableModules = mkMerge
+      [ (mkIf cfg.mod_status [ "mod_status" ])
+        (mkIf cfg.mod_userdir [ "mod_userdir" ])
+        # always load mod_accesslog so that we can log to the journal
+        [ "mod_accesslog" ]
+      ];
+
+    systemd.services.lighttpd = {
+      description = "Lighttpd Web Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig.ExecStart = "${cfg.package}/sbin/lighttpd -D -f ${configFile}";
+      # SIGINT => graceful shutdown
+      serviceConfig.KillSignal = "SIGINT";
+    };
+
+    users.users.lighttpd = {
+      group = "lighttpd";
+      description = "lighttpd web server privilege separation user";
+      uid = config.ids.uids.lighttpd;
+    };
+
+    users.groups.lighttpd.gid = config.ids.gids.lighttpd;
+  };
+}
diff --git a/nixos/modules/services/web-servers/lighttpd/gitweb.nix b/nixos/modules/services/web-servers/lighttpd/gitweb.nix
new file mode 100644
index 00000000000..c494d6966a7
--- /dev/null
+++ b/nixos/modules/services/web-servers/lighttpd/gitweb.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gitweb;
+  package = pkgs.gitweb.override (optionalAttrs cfg.gitwebTheme {
+    gitwebTheme = true;
+  });
+
+in
+{
+
+  options.services.lighttpd.gitweb = {
+
+    enable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        If true, enable gitweb in lighttpd. Access it at http://yourserver/gitweb
+      '';
+    };
+
+  };
+
+  config = mkIf config.services.lighttpd.gitweb.enable {
+
+    # declare module dependencies
+    services.lighttpd.enableModules = [ "mod_cgi" "mod_redirect" "mod_alias" "mod_setenv" ];
+
+    services.lighttpd.extraConfig = ''
+      $HTTP["url"] =~ "^/gitweb" {
+          cgi.assign = (
+              ".cgi" => "${pkgs.perl}/bin/perl"
+          )
+          url.redirect = (
+              "^/gitweb$" => "/gitweb/"
+          )
+          alias.url = (
+              "/gitweb/static/" => "${package}/static/",
+              "/gitweb/"        => "${package}/gitweb.cgi"
+          )
+          setenv.add-environment = (
+              "GITWEB_CONFIG" => "${cfg.gitwebConfigFile}",
+              "HOME" => "${cfg.projectroot}"
+          )
+      }
+    '';
+
+  };
+
+}
diff --git a/nixos/modules/services/web-servers/mighttpd2.nix b/nixos/modules/services/web-servers/mighttpd2.nix
new file mode 100644
index 00000000000..f9b1a8b6ccc
--- /dev/null
+++ b/nixos/modules/services/web-servers/mighttpd2.nix
@@ -0,0 +1,132 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mighttpd2;
+  configFile = pkgs.writeText "mighty-config" cfg.config;
+  routingFile = pkgs.writeText "mighty-routing" cfg.routing;
+in {
+  options.services.mighttpd2 = {
+    enable = mkEnableOption "Mighttpd2 web server";
+
+    config = mkOption {
+      default = "";
+      example = ''
+        # Example configuration for Mighttpd 2
+        Port: 80
+        # IP address or "*"
+        Host: *
+        Debug_Mode: Yes # Yes or No
+        # If available, "nobody" is much more secure for User:.
+        User: root
+        # If available, "nobody" is much more secure for Group:.
+        Group: root
+        Pid_File: /run/mighty.pid
+        Logging: Yes # Yes or No
+        Log_File: /var/log/mighty # The directory must be writable by User:
+        Log_File_Size: 16777216 # bytes
+        Log_Backup_Number: 10
+        Index_File: index.html
+        Index_Cgi: index.cgi
+        Status_File_Dir: /usr/local/share/mighty/status
+        Connection_Timeout: 30 # seconds
+        Fd_Cache_Duration: 10 # seconds
+        # Server_Name: Mighttpd/3.x.y
+        Tls_Port: 443
+        Tls_Cert_File: cert.pem # should change this with an absolute path
+        # should change this with comma-separated absolute paths
+        Tls_Chain_Files: chain.pem
+        # Currently, Tls_Key_File must not be encrypted.
+        Tls_Key_File: privkey.pem # should change this with an absolute path
+        Service: 0 # 0 is HTTP only, 1 is HTTPS only, 2 is both
+      '';
+      type = types.lines;
+      description = ''
+        Verbatim config file to use
+        (see http://www.mew.org/~kazu/proj/mighttpd/en/config.html)
+      '';
+    };
+
+    routing = mkOption {
+      default = "";
+      example = ''
+        # Example routing for Mighttpd 2
+
+        # Domain lists
+        [localhost www.example.com]
+
+        # Entries are looked up in the specified order
+        # All paths must end with "/"
+
+        # A path to CGI scripts should be specified with "=>"
+        /~alice/cgi-bin/ => /home/alice/public_html/cgi-bin/
+
+        # A path to static files should be specified with "->"
+        /~alice/         -> /home/alice/public_html/
+        /cgi-bin/        => /export/cgi-bin/
+
+        # Reverse proxy rules should be specified with ">>"
+        # /path >> host:port/path2
+        # Either "host" or ":port" can be committed, but not both.
+        /app/cal/        >> example.net/calendar/
+        # Yesod app in the same server
+        /app/wiki/       >> 127.0.0.1:3000/
+
+        /                -> /export/www/
+      '';
+      type = types.lines;
+      description = ''
+        Verbatim routing file to use
+        (see http://www.mew.org/~kazu/proj/mighttpd/en/config.html)
+      '';
+    };
+
+    cores = mkOption {
+      default = null;
+      type = types.nullOr types.int;
+      description = ''
+        How many cores to use.
+        If null it will be determined automatically
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    assertions =
+      [ { assertion = cfg.routing != "";
+          message = "You need at least one rule in mighttpd2.routing";
+        }
+      ];
+    systemd.services.mighttpd2 = {
+      description = "Mighttpd2 web server";
+      after = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.haskellPackages.mighttpd2}/bin/mighty \
+            ${configFile} \
+            ${routingFile} \
+            +RTS -N${optionalString (cfg.cores != null) "${cfg.cores}"}
+        '';
+        Type = "simple";
+        User = "mighttpd2";
+        Group = "mighttpd2";
+        Restart = "on-failure";
+        AmbientCapabilities = "cap_net_bind_service";
+        CapabilityBoundingSet = "cap_net_bind_service";
+      };
+    };
+
+    users.users.mighttpd2 = {
+      group = "mighttpd2";
+      uid = config.ids.uids.mighttpd2;
+      isSystemUser = true;
+    };
+
+    users.groups.mighttpd2.gid = config.ids.gids.mighttpd2;
+  };
+
+  meta.maintainers = with lib.maintainers; [ fgaz ];
+}
diff --git a/nixos/modules/services/web-servers/minio.nix b/nixos/modules/services/web-servers/minio.nix
new file mode 100644
index 00000000000..c345e3f2467
--- /dev/null
+++ b/nixos/modules/services/web-servers/minio.nix
@@ -0,0 +1,130 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.minio;
+
+  legacyCredentials = cfg: pkgs.writeText "minio-legacy-credentials" ''
+    MINIO_ROOT_USER=${cfg.accessKey}
+    MINIO_ROOT_PASSWORD=${cfg.secretKey}
+  '';
+in
+{
+  meta.maintainers = [ maintainers.bachp ];
+
+  options.services.minio = {
+    enable = mkEnableOption "Minio Object Storage";
+
+    listenAddress = mkOption {
+      default = ":9000";
+      type = types.str;
+      description = "IP address and port of the server.";
+    };
+
+    consoleAddress = mkOption {
+      default = ":9001";
+      type = types.str;
+      description = "IP address and port of the web UI (console).";
+    };
+
+    dataDir = mkOption {
+      default = [ "/var/lib/minio/data" ];
+      type = types.listOf types.path;
+      description = "The list of data directories for storing the objects. Use one path for regular operation and the minimum of 4 endpoints for Erasure Code mode.";
+    };
+
+    configDir = mkOption {
+      default = "/var/lib/minio/config";
+      type = types.path;
+      description = "The config directory, for the access keys and other settings.";
+    };
+
+    accessKey = mkOption {
+      default = "";
+      type = types.str;
+      description = ''
+        Access key of 5 to 20 characters in length that clients use to access the server.
+        This overrides the access key that is generated by minio on first startup and stored inside the
+        <literal>configDir</literal> directory.
+      '';
+    };
+
+    secretKey = mkOption {
+      default = "";
+      type = types.str;
+      description = ''
+        Specify the Secret key of 8 to 40 characters in length that clients use to access the server.
+        This overrides the secret key that is generated by minio on first startup and stored inside the
+        <literal>configDir</literal> directory.
+      '';
+    };
+
+    rootCredentialsFile = mkOption  {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        File containing the MINIO_ROOT_USER, default is "minioadmin", and
+        MINIO_ROOT_PASSWORD (length >= 8), default is "minioadmin"; in the format of
+        an EnvironmentFile=, as described by systemd.exec(5).
+      '';
+      example = "/etc/nixos/minio-root-credentials";
+    };
+
+    region = mkOption {
+      default = "us-east-1";
+      type = types.str;
+      description = ''
+        The physical location of the server. By default it is set to us-east-1, which is same as AWS S3's and Minio's default region.
+      '';
+    };
+
+    browser = mkOption {
+      default = true;
+      type = types.bool;
+      description = "Enable or disable access to web UI.";
+    };
+
+    package = mkOption {
+      default = pkgs.minio;
+      defaultText = literalExpression "pkgs.minio";
+      type = types.package;
+      description = "Minio package to use.";
+    };
+  };
+
+  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.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"}";
+      };
+    };
+
+    users.users.minio = {
+      group = "minio";
+      uid = config.ids.uids.minio;
+    };
+
+    users.groups.minio.gid = config.ids.uids.minio;
+  };
+}
diff --git a/nixos/modules/services/web-servers/molly-brown.nix b/nixos/modules/services/web-servers/molly-brown.nix
new file mode 100644
index 00000000000..0bd8b3316cb
--- /dev/null
+++ b/nixos/modules/services/web-servers/molly-brown.nix
@@ -0,0 +1,101 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.molly-brown;
+  settingsFormat = pkgs.formats.toml { };
+ configFile = settingsFormat.generate "molly-brown.toml" cfg.settings;
+in {
+
+  options.services.molly-brown = {
+
+    enable = mkEnableOption "Molly-Brown Gemini server";
+
+    port = mkOption {
+      default = 1965;
+      type = types.port;
+      description = ''
+        TCP port for molly-brown to bind to.
+      '';
+    };
+
+    hostName = mkOption {
+      type = types.str;
+      default = config.networking.hostName;
+      defaultText = literalExpression "config.networking.hostName";
+      description = ''
+        The hostname to respond to requests for. Requests for URLs with
+        other hosts will result in a status 53 (PROXY REQUEST REFUSED)
+        response.
+      '';
+    };
+
+    certPath = mkOption {
+      type = types.path;
+      example = "/var/lib/acme/example.com/cert.pem";
+      description = ''
+        Path to TLS certificate. An ACME certificate and key may be
+        shared with an HTTP server, but only if molly-brown has
+        permissions allowing it to read such keys.
+
+        As an example:
+        <programlisting>
+        systemd.services.molly-brown.serviceConfig.SupplementaryGroups =
+          [ config.security.acme.certs."example.com".group ];
+        </programlisting>
+      '';
+    };
+
+    keyPath = mkOption {
+      type = types.path;
+      example = "/var/lib/acme/example.com/key.pem";
+      description = "Path to TLS key. See <option>CertPath</option>.";
+    };
+
+    docBase = mkOption {
+      type = types.path;
+      example = "/var/lib/molly-brown";
+      description = "Base directory for Gemini content.";
+    };
+
+    settings = mkOption {
+      inherit (settingsFormat) type;
+      default = { };
+      description = ''
+        molly-brown configuration. Refer to
+        <link xlink:href="https://tildegit.org/solderpunk/molly-brown/src/branch/master/example.conf"/>
+        for details on supported values.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    services.molly-brown.settings = let logDir = "/var/log/molly-brown";
+    in {
+      Port = cfg.port;
+      Hostname = cfg.hostName;
+      CertPath = cfg.certPath;
+      KeyPath = cfg.keyPath;
+      DocBase = cfg.docBase;
+      AccessLog = "${logDir}/access.log";
+      ErrorLog = "${logDir}/error.log";
+    };
+
+    systemd.services.molly-brown = {
+      description = "Molly Brown gemini server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        LogsDirectory = "molly-brown";
+        ExecStart = "${pkgs.molly-brown}/bin/molly-brown -c ${configFile}";
+        Restart = "always";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
new file mode 100644
index 00000000000..e046c28dd6b
--- /dev/null
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -0,0 +1,1005 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nginx;
+  certs = 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);
+  virtualHosts = mapAttrs (vhostName: vhostConfig:
+    let
+      serverName = if vhostConfig.serverName != null
+        then vhostConfig.serverName
+        else vhostName;
+      certName = if vhostConfig.useACMEHost != null
+        then vhostConfig.useACMEHost
+        else serverName;
+    in
+    vhostConfig // {
+      inherit serverName certName;
+    } // (optionalAttrs (vhostConfig.enableACME || vhostConfig.useACMEHost != null) {
+      sslCertificate = "${certs.${certName}.directory}/fullchain.pem";
+      sslCertificateKey = "${certs.${certName}.directory}/key.pem";
+      sslTrustedCertificate = if vhostConfig.sslTrustedCertificate != null
+                              then vhostConfig.sslTrustedCertificate
+                              else "${certs.${certName}.directory}/chain.pem";
+    })
+  ) cfg.virtualHosts;
+  enableIPv6 = config.networking.enableIPv6;
+
+  defaultFastcgiParams = {
+    SCRIPT_FILENAME   = "$document_root$fastcgi_script_name";
+    QUERY_STRING      = "$query_string";
+    REQUEST_METHOD    = "$request_method";
+    CONTENT_TYPE      = "$content_type";
+    CONTENT_LENGTH    = "$content_length";
+
+    SCRIPT_NAME       = "$fastcgi_script_name";
+    REQUEST_URI       = "$request_uri";
+    DOCUMENT_URI      = "$document_uri";
+    DOCUMENT_ROOT     = "$document_root";
+    SERVER_PROTOCOL   = "$server_protocol";
+    REQUEST_SCHEME    = "$scheme";
+    HTTPS             = "$https if_not_empty";
+
+    GATEWAY_INTERFACE = "CGI/1.1";
+    SERVER_SOFTWARE   = "nginx/$nginx_version";
+
+    REMOTE_ADDR       = "$remote_addr";
+    REMOTE_PORT       = "$remote_port";
+    SERVER_ADDR       = "$server_addr";
+    SERVER_PORT       = "$server_port";
+    SERVER_NAME       = "$server_name";
+
+    REDIRECT_STATUS   = "200";
+  };
+
+  recommendedProxyConfig = pkgs.writeText "nginx-recommended-proxy-headers.conf" ''
+    proxy_set_header        Host $host;
+    proxy_set_header        X-Real-IP $remote_addr;
+    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
+    proxy_set_header        X-Forwarded-Proto $scheme;
+    proxy_set_header        X-Forwarded-Host $host;
+    proxy_set_header        X-Forwarded-Server $host;
+  '';
+
+  upstreamConfig = toString (flip mapAttrsToList cfg.upstreams (name: upstream: ''
+    upstream ${name} {
+      ${toString (flip mapAttrsToList upstream.servers (name: server: ''
+        server ${name} ${optionalString server.backup "backup"};
+      ''))}
+      ${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;
+      # 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
+      # overwrite it because nginx does not allow duplicated settings.
+      types_hash_max_size 4096;
+
+      include ${cfg.package}/conf/fastcgi.conf;
+      include ${cfg.package}/conf/uwsgi_params;
+
+      default_type application/octet-stream;
+  '';
+
+  configFile = pkgs.writers.writeNginxConfig "nginx.conf" ''
+    pid /run/nginx/nginx.pid;
+    error_log ${cfg.logError};
+    daemon off;
+
+    ${cfg.config}
+
+    ${optionalString (cfg.eventsConfig != "" || cfg.config == "") ''
+    events {
+      ${cfg.eventsConfig}
+    }
+    ''}
+
+    ${optionalString (cfg.httpConfig == "" && cfg.config == "") ''
+    http {
+      ${commonHttpConfig}
+
+      ${optionalString (cfg.resolver.addresses != []) ''
+        resolver ${toString cfg.resolver.addresses} ${optionalString (cfg.resolver.valid != "") "valid=${cfg.resolver.valid}"} ${optionalString (!cfg.resolver.ipv6) "ipv6=off"};
+      ''}
+      ${upstreamConfig}
+
+      ${optionalString (cfg.recommendedOptimisation) ''
+        # optimisation
+        sendfile on;
+        tcp_nopush on;
+        tcp_nodelay on;
+        keepalive_timeout 65;
+      ''}
+
+      ssl_protocols ${cfg.sslProtocols};
+      ${optionalString (cfg.sslCiphers != null) "ssl_ciphers ${cfg.sslCiphers};"}
+      ${optionalString (cfg.sslDhparam != null) "ssl_dhparam ${cfg.sslDhparam};"}
+
+      ${optionalString (cfg.recommendedTlsSettings) ''
+        # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate
+
+        ssl_session_timeout 1d;
+        ssl_session_cache shared:SSL:10m;
+        # Breaks forward secrecy: https://github.com/mozilla/server-side-tls/issues/135
+        ssl_session_tickets off;
+        # We don't enable insecure ciphers by default, so this allows
+        # clients to pick the most performant, per https://github.com/mozilla/server-side-tls/issues/260
+        ssl_prefer_server_ciphers off;
+
+        # OCSP stapling
+        ssl_stapling on;
+        ssl_stapling_verify on;
+      ''}
+
+      ${optionalString (cfg.recommendedGzipSettings) ''
+        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_vary on;
+      ''}
+
+      ${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;
+        include ${recommendedProxyConfig};
+      ''}
+
+      ${optionalString (cfg.mapHashBucketSize != null) ''
+        map_hash_bucket_size ${toString cfg.mapHashBucketSize};
+      ''}
+
+      ${optionalString (cfg.mapHashMaxSize != null) ''
+        map_hash_max_size ${toString cfg.mapHashMaxSize};
+      ''}
+
+      ${optionalString (cfg.serverNamesHashBucketSize != null) ''
+        server_names_hash_bucket_size ${toString cfg.serverNamesHashBucketSize};
+      ''}
+
+      ${optionalString (cfg.serverNamesHashMaxSize != null) ''
+        server_names_hash_max_size ${toString cfg.serverNamesHashMaxSize};
+      ''}
+
+      # $connection_upgrade is used for websocket proxying
+      map $http_upgrade $connection_upgrade {
+          default upgrade;
+          '''      close;
+      }
+      client_max_body_size ${cfg.clientMaxBodySize};
+
+      server_tokens ${if cfg.serverTokens then "on" else "off"};
+
+      ${cfg.commonHttpConfig}
+
+      ${vhosts}
+
+      ${optionalString cfg.statusPage ''
+        server {
+          listen 80;
+          ${optionalString enableIPv6 "listen [::]:80;" }
+
+          server_name localhost;
+
+          location /nginx_status {
+            stub_status on;
+            access_log off;
+            allow 127.0.0.1;
+            ${optionalString enableIPv6 "allow ::1;"}
+            deny all;
+          }
+        }
+      ''}
+
+      ${cfg.appendHttpConfig}
+    }''}
+
+    ${optionalString (cfg.httpConfig != "") ''
+    http {
+      ${commonHttpConfig}
+      ${cfg.httpConfig}
+    }''}
+
+    ${optionalString (cfg.streamConfig != "") ''
+    stream {
+      ${cfg.streamConfig}
+    }
+    ''}
+
+    ${cfg.appendConfig}
+  '';
+
+  configPath = if cfg.enableReload
+    then "/etc/nginx/nginx.conf"
+    else configFile;
+
+  execCommand = "${cfg.package}/bin/nginx -c '${configPath}'";
+
+  vhosts = concatStringsSep "\n" (mapAttrsToList (vhostName: vhost:
+    let
+        onlySSL = vhost.onlySSL || vhost.enableSSL;
+        hasSSL = onlySSL || vhost.addSSL || vhost.forceSSL;
+
+        defaultListen =
+          if vhost.listen != [] then vhost.listen
+          else
+            let addrs = if vhost.listenAddresses != [] then vhost.listenAddresses else cfg.defaultListenAddresses;
+            in optionals (hasSSL || vhost.rejectSSL) (map (addr: { inherit addr; port = 443; ssl = true; }) addrs)
+              ++ optionals (!onlySSL) (map (addr: { inherit addr; port = 80; ssl = false; }) addrs);
+
+        hostListen =
+          if vhost.forceSSL
+            then filter (x: x.ssl) defaultListen
+            else defaultListen;
+
+        listenString = { addr, port, ssl, extraParameters ? [], ... }:
+          "listen ${addr}:${toString port} "
+          + optionalString ssl "ssl "
+          + optionalString (ssl && vhost.http2) "http2 "
+          + optionalString vhost.default "default_server "
+          + optionalString (extraParameters != []) (concatStringsSep " " extraParameters)
+          + ";"
+          + (if ssl && vhost.http3 then ''
+          # UDP listener for **QUIC+HTTP/3
+          listen ${addr}:${toString port} http3 reuseport;
+          # Advertise that HTTP/3 is available
+          add_header Alt-Svc 'h3=":443"';
+          # Sent when QUIC was used
+          add_header QUIC-Status $quic;
+          '' else "");
+
+        redirectListen = filter (x: !x.ssl) defaultListen;
+
+        acmeLocation = optionalString (vhost.enableACME || vhost.useACMEHost != null) ''
+          location /.well-known/acme-challenge {
+            ${optionalString (vhost.acmeFallbackHost != null) "try_files $uri @acme-fallback;"}
+            ${optionalString (vhost.acmeRoot != null) "root ${vhost.acmeRoot};"}
+            auth_basic off;
+          }
+          ${optionalString (vhost.acmeFallbackHost != null) ''
+            location @acme-fallback {
+              auth_basic off;
+              proxy_pass http://${vhost.acmeFallbackHost};
+            }
+          ''}
+        '';
+
+      in ''
+        ${optionalString vhost.forceSSL ''
+          server {
+            ${concatMapStringsSep "\n" listenString redirectListen}
+
+            server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases};
+            ${acmeLocation}
+            location / {
+              return 301 https://$host$request_uri;
+            }
+          }
+        ''}
+
+        server {
+          ${concatMapStringsSep "\n" listenString hostListen}
+          server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases};
+          ${acmeLocation}
+          ${optionalString (vhost.root != null) "root ${vhost.root};"}
+          ${optionalString (vhost.globalRedirect != null) ''
+            return 301 http${optionalString hasSSL "s"}://${vhost.globalRedirect}$request_uri;
+          ''}
+          ${optionalString hasSSL ''
+            ssl_certificate ${vhost.sslCertificate};
+            ssl_certificate_key ${vhost.sslCertificateKey};
+          ''}
+          ${optionalString (hasSSL && vhost.sslTrustedCertificate != null) ''
+            ssl_trusted_certificate ${vhost.sslTrustedCertificate};
+          ''}
+          ${optionalString vhost.rejectSSL ''
+            ssl_reject_handshake on;
+          ''}
+          ${optionalString (hasSSL && vhost.kTLS) ''
+            ssl_conf_command Options KTLS;
+          ''}
+
+          ${mkBasicAuth vhostName vhost}
+
+          ${mkLocations vhost.locations}
+
+          ${vhost.extraConfig}
+        }
+      ''
+  ) virtualHosts);
+  mkLocations = locations: concatStringsSep "\n" (map (config: ''
+    location ${config.location} {
+      ${optionalString (config.proxyPass != null && !cfg.proxyResolveWhileRunning)
+        "proxy_pass ${config.proxyPass};"
+      }
+      ${optionalString (config.proxyPass != null && cfg.proxyResolveWhileRunning) ''
+        set $nix_proxy_target "${config.proxyPass}";
+        proxy_pass $nix_proxy_target;
+      ''}
+      ${optionalString config.proxyWebsockets ''
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection $connection_upgrade;
+      ''}
+      ${concatStringsSep "\n"
+        (mapAttrsToList (n: v: ''fastcgi_param ${n} "${v}";'')
+          (optionalAttrs (config.fastcgiParams != {})
+            (defaultFastcgiParams // config.fastcgiParams)))}
+      ${optionalString (config.index != null) "index ${config.index};"}
+      ${optionalString (config.tryFiles != null) "try_files ${config.tryFiles};"}
+      ${optionalString (config.root != null) "root ${config.root};"}
+      ${optionalString (config.alias != null) "alias ${config.alias};"}
+      ${optionalString (config.return != null) "return ${config.return};"}
+      ${config.extraConfig}
+      ${optionalString (config.proxyPass != null && cfg.recommendedProxySettings) "include ${recommendedProxyConfig};"}
+      ${mkBasicAuth "sublocation" config}
+    }
+  '') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations)));
+
+  mkBasicAuth = name: zone: optionalString (zone.basicAuthFile != null || zone.basicAuth != {}) (let
+    auth_file = if zone.basicAuthFile != null
+      then zone.basicAuthFile
+      else mkHtpasswd name zone.basicAuth;
+  in ''
+    auth_basic secured;
+    auth_basic_user_file ${auth_file};
+  '');
+  mkHtpasswd = name: authDef: pkgs.writeText "${name}.htpasswd" (
+    concatStringsSep "\n" (mapAttrsToList (user: password: ''
+      ${user}:{PLAIN}${password}
+    '') authDef)
+  );
+
+  mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix;
+in
+
+{
+  options = {
+    services.nginx = {
+      enable = mkEnableOption "Nginx Web Server";
+
+      statusPage = mkOption {
+        default = false;
+        type = types.bool;
+        description = "
+          Enable status page reachable from localhost on http://127.0.0.1/nginx_status.
+        ";
+      };
+
+      recommendedTlsSettings = mkOption {
+        default = false;
+        type = types.bool;
+        description = "
+          Enable recommended TLS settings.
+        ";
+      };
+
+      recommendedOptimisation = mkOption {
+        default = false;
+        type = types.bool;
+        description = "
+          Enable recommended optimisation settings.
+        ";
+      };
+
+      recommendedGzipSettings = mkOption {
+        default = false;
+        type = types.bool;
+        description = "
+          Enable recommended gzip settings.
+        ";
+      };
+
+      recommendedProxySettings = mkOption {
+        default = false;
+        type = types.bool;
+        description = "
+          Enable recommended proxy settings.
+        ";
+      };
+
+      proxyTimeout = mkOption {
+        type = types.str;
+        default = "60s";
+        example = "20s";
+        description = "
+          Change the proxy related timeouts in recommendedProxySettings.
+        ";
+      };
+
+      defaultListenAddresses = mkOption {
+        type = types.listOf types.str;
+        default = [ "0.0.0.0" ] ++ optional enableIPv6 "[::0]";
+        defaultText = literalExpression ''[ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]"'';
+        example = literalExpression ''[ "10.0.0.12" "[2002:a00:1::]" ]'';
+        description = "
+          If vhosts do not specify listenAddresses, use these addresses by default.
+        ";
+      };
+
+      package = mkOption {
+        default = pkgs.nginxStable;
+        defaultText = literalExpression "pkgs.nginxStable";
+        type = types.package;
+        apply = p: p.override {
+          modules = p.modules ++ cfg.additionalModules;
+        };
+        description = "
+          Nginx package to use. This defaults to the stable version. Note
+          that the nginx team recommends to use the mainline version which
+          available in nixpkgs as <literal>nginxMainline</literal>.
+        ";
+      };
+
+      additionalModules = mkOption {
+        default = [];
+        type = types.listOf (types.attrsOf types.anything);
+        example = literalExpression "[ pkgs.nginxModules.brotli ]";
+        description = ''
+          Additional <link xlink:href="https://www.nginx.com/resources/wiki/modules/">third-party nginx modules</link>
+          to install. Packaged modules are available in
+          <literal>pkgs.nginxModules</literal>.
+        '';
+      };
+
+      logError = mkOption {
+        default = "stderr";
+        type = types.str;
+        description = "
+          Configures logging.
+          The first parameter defines a file that will store the log. The
+          special value stderr selects the standard error file. Logging to
+          syslog can be configured by specifying the “syslog:†prefix.
+          The second parameter determines the level of logging, and can be
+          one of the following: debug, info, notice, warn, error, crit,
+          alert, or emerg. Log levels above are listed in the order of
+          increasing severity. Setting a certain log level will cause all
+          messages of the specified and more severe log levels to be logged.
+          If this parameter is omitted then error is used.
+        ";
+      };
+
+      preStart =  mkOption {
+        type = types.lines;
+        default = "";
+        description = "
+          Shell commands executed before the service's nginx is started.
+        ";
+      };
+
+      config = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Verbatim <filename>nginx.conf</filename> configuration.
+          This is mutually exclusive to any other config option for
+          <filename>nginx.conf</filename> except for
+          <itemizedlist>
+          <listitem><para><xref linkend="opt-services.nginx.appendConfig" />
+          </para></listitem>
+          <listitem><para><xref linkend="opt-services.nginx.httpConfig" />
+          </para></listitem>
+          <listitem><para><xref linkend="opt-services.nginx.logError" />
+          </para></listitem>
+          </itemizedlist>
+
+          If additional verbatim config in addition to other options is needed,
+          <xref linkend="opt-services.nginx.appendConfig" /> should be used instead.
+        '';
+      };
+
+      appendConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Configuration lines appended to the generated Nginx
+          configuration file. Commonly used by different modules
+          providing http snippets. <option>appendConfig</option>
+          can be specified more than once and it's value will be
+          concatenated (contrary to <option>config</option> which
+          can be set only once).
+        '';
+      };
+
+      commonHttpConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          resolver 127.0.0.1 valid=5s;
+
+          log_format myformat '$remote_addr - $remote_user [$time_local] '
+                              '"$request" $status $body_bytes_sent '
+                              '"$http_referer" "$http_user_agent"';
+        '';
+        description = ''
+          With nginx you must provide common http context definitions before
+          they are used, e.g. log_format, resolver, etc. inside of server
+          or location contexts. Use this attribute to set these definitions
+          at the appropriate location.
+        '';
+      };
+
+      httpConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "
+          Configuration lines to be set inside the http block.
+          This is mutually exclusive with the structured configuration
+          via virtualHosts and the recommendedXyzSettings configuration
+          options. See appendHttpConfig for appending to the generated http block.
+        ";
+      };
+
+      streamConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          server {
+            listen 127.0.0.1:53 udp reuseport;
+            proxy_timeout 20s;
+            proxy_pass 192.168.0.1:53535;
+          }
+        '';
+        description = "
+          Configuration lines to be set inside the stream block.
+        ";
+      };
+
+      eventsConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Configuration lines to be set inside the events block.
+        '';
+      };
+
+      appendHttpConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "
+          Configuration lines to be appended to the generated http block.
+          This is mutually exclusive with using config and httpConfig for
+          specifying the whole http block verbatim.
+        ";
+      };
+
+      enableReload = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Reload nginx when configuration file changes (instead of restart).
+          The configuration file is exposed at <filename>/etc/nginx/nginx.conf</filename>.
+          See also <literal>systemd.services.*.restartIfChanged</literal>.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "nginx";
+        description = "User account under which nginx runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "nginx";
+        description = "Group account under which nginx runs.";
+      };
+
+      serverTokens = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Show nginx version in headers and error pages.";
+      };
+
+      clientMaxBodySize = mkOption {
+        type = types.str;
+        default = "10m";
+        description = "Set nginx global client_max_body_size.";
+      };
+
+      sslCiphers = mkOption {
+        type = types.nullOr types.str;
+        # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate
+        default = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
+        description = "Ciphers to choose from when negotiating TLS handshakes.";
+      };
+
+      sslProtocols = mkOption {
+        type = types.str;
+        default = "TLSv1.2 TLSv1.3";
+        example = "TLSv1 TLSv1.1 TLSv1.2 TLSv1.3";
+        description = "Allowed TLS protocol versions.";
+      };
+
+      sslDhparam = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/path/to/dhparams.pem";
+        description = "Path to DH parameters file.";
+      };
+
+      proxyResolveWhileRunning = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Resolves domains of proxyPass targets at runtime
+          and not only at start, you have to set
+          services.nginx.resolver, too.
+        '';
+      };
+
+      mapHashBucketSize = mkOption {
+        type = types.nullOr (types.enum [ 32 64 128 ]);
+        default = null;
+        description = ''
+            Sets the bucket size for the map variables hash tables. Default
+            value depends on the processor’s cache line size.
+          '';
+      };
+
+      mapHashMaxSize = mkOption {
+        type = types.nullOr types.ints.positive;
+        default = null;
+        description = ''
+            Sets the maximum size of the map variables hash tables.
+          '';
+      };
+
+      serverNamesHashBucketSize = mkOption {
+        type = types.nullOr types.ints.positive;
+        default = null;
+        description = ''
+            Sets the bucket size for the server names hash tables. Default
+            value depends on the processor’s cache line size.
+          '';
+      };
+
+      serverNamesHashMaxSize = mkOption {
+        type = types.nullOr types.ints.positive;
+        default = null;
+        description = ''
+            Sets the maximum size of the server names hash tables.
+          '';
+      };
+
+      resolver = mkOption {
+        type = types.submodule {
+          options = {
+            addresses = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = literalExpression ''[ "[::1]" "127.0.0.1:5353" ]'';
+              description = "List of resolvers to use";
+            };
+            valid = mkOption {
+              type = types.str;
+              default = "";
+              example = "30s";
+              description = ''
+                By default, nginx caches answers using the TTL value of a response.
+                An optional valid parameter allows overriding it
+              '';
+            };
+            ipv6 = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                By default, nginx will look up both IPv4 and IPv6 addresses while resolving.
+                If looking up of IPv6 addresses is not desired, the ipv6=off parameter can be
+                specified.
+              '';
+            };
+          };
+        };
+        description = ''
+          Configures name servers used to resolve names of upstream servers into addresses
+        '';
+        default = {};
+      };
+
+      upstreams = mkOption {
+        type = types.attrsOf (types.submodule {
+          options = {
+            servers = mkOption {
+              type = types.attrsOf (types.submodule {
+                options = {
+                  backup = mkOption {
+                    type = types.bool;
+                    default = false;
+                    description = ''
+                      Marks the server as a backup server. It will be passed
+                      requests when the primary servers are unavailable.
+                    '';
+                  };
+                };
+              });
+              description = ''
+                Defines the address and other parameters of the upstream servers.
+              '';
+              default = {};
+              example = { "127.0.0.1:8000" = {}; };
+            };
+            extraConfig = mkOption {
+              type = types.lines;
+              default = "";
+              description = ''
+                These lines go to the end of the upstream verbatim.
+              '';
+            };
+          };
+        });
+        description = ''
+          Defines a group of servers to use as proxy target.
+        '';
+        default = {};
+        example = literalExpression ''
+          "backend_server" = {
+            servers = { "127.0.0.1:8000" = {}; };
+            extraConfig = ''''
+              keepalive 16;
+            '''';
+          };
+        '';
+      };
+
+      virtualHosts = mkOption {
+        type = types.attrsOf (types.submodule (import ./vhost-options.nix {
+          inherit config lib;
+        }));
+        default = {
+          localhost = {};
+        };
+        example = literalExpression ''
+          {
+            "hydra.example.com" = {
+              forceSSL = true;
+              enableACME = true;
+              locations."/" = {
+                proxyPass = "http://localhost:3000";
+              };
+            };
+          };
+        '';
+        description = "Declarative vhost config";
+      };
+    };
+  };
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "nginx" "stateDir" ] ''
+      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.
+    '')
+  ];
+
+  config = mkIf cfg.enable {
+    # TODO: test user supplied config file pases syntax test
+
+    warnings =
+    let
+      deprecatedSSL = name: config: optional config.enableSSL
+      ''
+        config.services.nginx.virtualHosts.<name>.enableSSL is deprecated,
+        use config.services.nginx.virtualHosts.<name>.onlySSL instead.
+      '';
+
+    in flatten (mapAttrsToList deprecatedSSL virtualHosts);
+
+    assertions =
+    let
+      hostOrAliasIsNull = l: l.root == null || l.alias == null;
+    in [
+      {
+        assertion = all (host: all hostOrAliasIsNull (attrValues host.locations)) (attrValues virtualHosts);
+        message = "Only one of nginx root or alias can be specified on a location.";
+      }
+
+      {
+        assertion = all (host: with host;
+          count id [ addSSL (onlySSL || enableSSL) forceSSL rejectSSL ] <= 1
+        ) (attrValues virtualHosts);
+        message = ''
+          Options services.nginx.service.virtualHosts.<name>.addSSL,
+          services.nginx.virtualHosts.<name>.onlySSL,
+          services.nginx.virtualHosts.<name>.forceSSL and
+          services.nginx.virtualHosts.<name>.rejectSSL are mutually exclusive.
+        '';
+      }
+
+      {
+        assertion = any (host: host.rejectSSL) (attrValues virtualHosts) -> versionAtLeast cfg.package.version "1.19.4";
+        message = ''
+          services.nginx.virtualHosts.<name>.rejectSSL requires nginx version
+          1.19.4 or above; see the documentation for services.nginx.package.
+        '';
+      }
+
+      {
+        assertion = any (host: host.kTLS) (attrValues virtualHosts) -> versionAtLeast cfg.package.version "1.21.4";
+        message = ''
+          services.nginx.virtualHosts.<name>.kTLS requires nginx version
+          1.21.4 or above; see the documentation for services.nginx.package.
+        '';
+      }
+
+      {
+        assertion = all (host: !(host.enableACME && host.useACMEHost != null)) (attrValues virtualHosts);
+        message = ''
+          Options services.nginx.service.virtualHosts.<name>.enableACME and
+          services.nginx.virtualHosts.<name>.useACMEHost are mutually exclusive.
+        '';
+      }
+    ] ++ map (name: mkCertOwnershipAssertion {
+      inherit (cfg) group user;
+      cert = config.security.acme.certs.${name};
+      groups = config.users.groups;
+    }) dependentCertNames;
+
+    systemd.services.nginx = {
+      description = "Nginx Web Server";
+      wantedBy = [ "multi-user.target" ];
+      wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) dependentCertNames);
+      after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") dependentCertNames;
+      # Nginx needs to be started in order to be able to request certificates
+      # (it's hosting the acme challenge after all)
+      # This fixes https://github.com/NixOS/nixpkgs/issues/81842
+      before = map (certName: "acme-${certName}.service") dependentCertNames;
+      stopIfChanged = false;
+      preStart = ''
+        ${cfg.preStart}
+        ${execCommand} -t
+      '';
+
+      startLimitIntervalSec = 60;
+      serviceConfig = {
+        ExecStart = execCommand;
+        ExecReload = [
+          "${execCommand} -t"
+          "${pkgs.coreutils}/bin/kill -HUP $MAINPID"
+        ];
+        Restart = "always";
+        RestartSec = "10s";
+        # User and group
+        User = cfg.user;
+        Group = cfg.group;
+        # Runtime directory and mode
+        RuntimeDirectory = "nginx";
+        RuntimeDirectoryMode = "0750";
+        # Cache directory and mode
+        CacheDirectory = "nginx";
+        CacheDirectoryMode = "0750";
+        # Logs directory and mode
+        LogsDirectory = "nginx";
+        LogsDirectoryMode = "0750";
+        # Proc filesystem
+        ProcSubset = "pid";
+        ProtectProc = "invisible";
+        # New file permissions
+        UMask = "0027"; # 0640 / 0750
+        # Capabilities
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
+        # Security
+        NoNewPrivileges = true;
+        # Sandboxing (sorted by occurrence in https://www.freedesktop.org/software/systemd/man/systemd.exec.html)
+        ProtectSystem = "strict";
+        ProtectHome = mkDefault true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = !((builtins.any (mod: (mod.allowMemoryWriteExecute or false)) cfg.package.modules) || (cfg.package == pkgs.openresty));
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
+        PrivateMounts = true;
+        # System Call Filtering
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid" ]
+          ++ optionals ((cfg.package != pkgs.tengine) && (!lib.any (mod: (mod.disableIPC or false)) cfg.package.modules)) [ "~@ipc" ];
+      };
+    };
+
+    environment.etc."nginx/nginx.conf" = mkIf cfg.enableReload {
+      source = configFile;
+    };
+
+    # This service waits for all certificates to be available
+    # before reloading nginx configuration.
+    # sslTargets are added to wantedBy + before
+    # which allows the acme-finished-$cert.target to signify the successful updating
+    # of certs end-to-end.
+    systemd.services.nginx-config-reload = let
+      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" ];
+      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) [ 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);
+      serviceConfig = {
+        Type = "oneshot";
+        TimeoutSec = 60;
+        ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active nginx.service";
+        ExecStart = "/run/current-system/systemd/bin/systemctl reload nginx.service";
+      };
+    };
+
+    security.acme.certs = let
+      acmePairs = map (vhostConfig: let
+        hasRoot = vhostConfig.acmeRoot != null;
+      in nameValuePair vhostConfig.serverName {
+        group = mkDefault cfg.group;
+        # if acmeRoot is null inherit config.security.acme
+        # Since config.security.acme.certs.<cert>.webroot's own default value
+        # should take precedence set priority higher than mkOptionDefault
+        webroot = mkOverride (if hasRoot then 1000 else 2000) vhostConfig.acmeRoot;
+        # Also nudge dnsProvider to null in case it is inherited
+        dnsProvider = mkOverride (if hasRoot then 1000 else 2000) null;
+        extraDomainNames = vhostConfig.serverAliases;
+      # Filter for enableACME-only vhosts. Don't want to create dud certs
+      }) (filter (vhostConfig: vhostConfig.useACMEHost == null) acmeEnabledVhosts);
+    in listToAttrs acmePairs;
+
+    users.users = optionalAttrs (cfg.user == "nginx") {
+      nginx = {
+        group = cfg.group;
+        isSystemUser = true;
+        uid = config.ids.uids.nginx;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "nginx") {
+      nginx.gid = config.ids.gids.nginx;
+    };
+
+    services.logrotate.paths.nginx = mapAttrs (_: mkDefault) {
+      path = "/var/log/nginx/*.log";
+      frequency = "weekly";
+      keep = 26;
+      extraConfig = ''
+        compress
+        delaycompress
+        postrotate
+          [ ! -f /var/run/nginx/nginx.pid ] || kill -USR1 `cat /var/run/nginx/nginx.pid`
+        endscript
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/web-servers/nginx/gitweb.nix b/nixos/modules/services/web-servers/nginx/gitweb.nix
new file mode 100644
index 00000000000..db45577a46d
--- /dev/null
+++ b/nixos/modules/services/web-servers/nginx/gitweb.nix
@@ -0,0 +1,94 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nginx.gitweb;
+  gitwebConfig = config.services.gitweb;
+  package = pkgs.gitweb.override (optionalAttrs gitwebConfig.gitwebTheme {
+    gitwebTheme = true;
+  });
+
+in
+{
+
+  options.services.nginx.gitweb = {
+
+    enable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        If true, enable gitweb in nginx.
+      '';
+    };
+
+    location = mkOption {
+      default = "/gitweb";
+      type = types.str;
+      description = ''
+        Location to serve gitweb on.
+      '';
+    };
+
+    user = mkOption {
+      default = "nginx";
+      type = types.str;
+      description = ''
+        Existing user that the CGI process will belong to. (Default almost surely will do.)
+      '';
+    };
+
+    group = mkOption {
+      default = "nginx";
+      type = types.str;
+      description = ''
+        Group that the CGI process will belong to. (Set to <literal>config.services.gitolite.group</literal> if you are using gitolite.)
+      '';
+    };
+
+    virtualHost = mkOption {
+      default = "_";
+      type = types.str;
+      description = ''
+        VirtualHost to serve gitweb on. Default is catch-all.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.gitweb = {
+      description = "GitWeb service";
+      script = "${package}/gitweb.cgi --fastcgi --nproc=1";
+      environment  = {
+        FCGI_SOCKET_PATH = "/run/gitweb/gitweb.sock";
+      };
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        RuntimeDirectory = [ "gitweb" ];
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    services.nginx = {
+      virtualHosts.${cfg.virtualHost} = {
+        locations."${cfg.location}/static/" = {
+          alias = "${package}/static/";
+        };
+        locations."${cfg.location}/" = {
+          extraConfig = ''
+            include ${config.services.nginx.package}/conf/fastcgi_params;
+            fastcgi_param GITWEB_CONFIG ${gitwebConfig.gitwebConfigFile};
+            fastcgi_pass unix:/run/gitweb/gitweb.sock;
+          '';
+        };
+      };
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ ];
+
+}
diff --git a/nixos/modules/services/web-servers/nginx/location-options.nix b/nixos/modules/services/web-servers/nginx/location-options.nix
new file mode 100644
index 00000000000..6fd00b38697
--- /dev/null
+++ b/nixos/modules/services/web-servers/nginx/location-options.nix
@@ -0,0 +1,132 @@
+# This file defines the options that can be used both for the Nginx
+# main server configuration, and for the virtual hosts.  (The latter
+# has additional options that affect the web server as a whole, like
+# the user/group to run under.)
+
+{ lib }:
+
+with lib;
+
+{
+  options = {
+    basicAuth = mkOption {
+      type = types.attrsOf types.str;
+      default = {};
+      example = literalExpression ''
+        {
+          user = "password";
+        };
+      '';
+      description = ''
+        Basic Auth protection for a vhost.
+
+        WARNING: This is implemented to store the password in plain text in the
+        Nix store.
+      '';
+    };
+
+    basicAuthFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Basic Auth password file for a vhost.
+        Can be created via: <command>htpasswd -c &lt;filename&gt; &lt;username&gt;</command>.
+
+        WARNING: The generate file contains the users' passwords in a
+        non-cryptographically-securely hashed way.
+      '';
+    };
+
+    proxyPass = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "http://www.example.org/";
+      description = ''
+        Adds proxy_pass directive and sets recommended proxy headers if
+        recommendedProxySettings is enabled.
+      '';
+    };
+
+    proxyWebsockets = mkOption {
+      type = types.bool;
+      default = false;
+      example = true;
+      description = ''
+        Whether to support proxying websocket connections with HTTP/1.1.
+      '';
+    };
+
+    index = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "index.php index.html";
+      description = ''
+        Adds index directive.
+      '';
+    };
+
+    tryFiles = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "$uri =404";
+      description = ''
+        Adds try_files directive.
+      '';
+    };
+
+    root = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/your/root/directory";
+      description = ''
+        Root directory for requests.
+      '';
+    };
+
+    alias = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/your/alias/directory";
+      description = ''
+        Alias directory for requests.
+      '';
+    };
+
+    return = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "301 http://example.com$request_uri";
+      description = ''
+        Adds a return directive, for e.g. redirections.
+      '';
+    };
+
+    fastcgiParams = mkOption {
+      type = types.attrsOf (types.either types.str types.path);
+      default = {};
+      description = ''
+        FastCGI parameters to override.  Unlike in the Nginx
+        configuration file, overriding only some default parameters
+        won't unset the default values for other parameters.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        These lines go to the end of the location verbatim.
+      '';
+    };
+
+    priority = mkOption {
+      type = types.int;
+      default = 1000;
+      description = ''
+        Order of this location block in relation to the others in the vhost.
+        The semantics are the same as with `lib.mkOrder`. Smaller values have
+        a greater priority.
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix
new file mode 100644
index 00000000000..c4e8285dc48
--- /dev/null
+++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix
@@ -0,0 +1,288 @@
+# This file defines the options that can be used both for the Nginx
+# main server configuration, and for the virtual hosts.  (The latter
+# has additional options that affect the web server as a whole, like
+# the user/group to run under.)
+
+{ config, lib, ... }:
+
+with lib;
+{
+  options = {
+    serverName = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Name of this virtual host. Defaults to attribute name in virtualHosts.
+      '';
+      example = "example.org";
+    };
+
+    serverAliases = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = ["www.example.org" "example.org"];
+      description = ''
+        Additional names of virtual hosts served by this virtual host configuration.
+      '';
+    };
+
+    listen = mkOption {
+      type = with types; listOf (submodule { options = {
+        addr = mkOption { type = str;  description = "IP address.";  };
+        port = mkOption { type = int;  description = "Port number."; default = 80; };
+        ssl  = mkOption { type = bool; description = "Enable SSL.";  default = false; };
+        extraParameters = mkOption { type = listOf str; description = "Extra parameters of this listen directive."; default = []; example = [ "reuseport" "deferred" ]; };
+      }; });
+      default = [];
+      example = [
+        { addr = "195.154.1.1"; port = 443; ssl = true;}
+        { addr = "192.154.1.1"; port = 80; }
+      ];
+      description = ''
+        Listen addresses and ports for this virtual host.
+        IPv6 addresses must be enclosed in square brackets.
+        Note: this option overrides <literal>addSSL</literal>
+        and <literal>onlySSL</literal>.
+
+        If you only want to set the addresses manually and not
+        the ports, take a look at <literal>listenAddresses</literal>
+      '';
+    };
+
+    listenAddresses = mkOption {
+      type = with types; listOf str;
+
+      description = ''
+        Listen addresses for this virtual host.
+        Compared to <literal>listen</literal> this only sets the addreses
+        and the ports are choosen automatically.
+
+        Note: This option overrides <literal>enableIPv6</literal>
+      '';
+      default = [];
+      example = [ "127.0.0.1" "::1" ];
+    };
+
+    enableACME = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to ask Let's Encrypt to sign a certificate for this vhost.
+        Alternately, you can use an existing certificate through <option>useACMEHost</option>.
+      '';
+    };
+
+    useACMEHost = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        A host of an existing Let's Encrypt certificate to use.
+        This is useful if you have many subdomains and want to avoid hitting the
+        <link xlink:href="https://letsencrypt.org/docs/rate-limits/">rate limit</link>.
+        Alternately, you can generate a certificate through <option>enableACME</option>.
+        <emphasis>Note that this option does not create any certificates, nor it does add subdomains to existing ones – you will need to create them manually using  <xref linkend="opt-security.acme.certs"/>.</emphasis>
+      '';
+    };
+
+    acmeRoot = mkOption {
+      type = types.nullOr types.str;
+      default = "/var/lib/acme/acme-challenge";
+      description = ''
+        Directory for the acme challenge which is PUBLIC, don't put certs or keys in here.
+        Set to null to inherit from config.security.acme.
+      '';
+    };
+
+    acmeFallbackHost = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Host which to proxy requests to if acme challenge is not found. Useful
+        if you want multiple hosts to be able to verify the same domain name.
+      '';
+    };
+
+    addSSL = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable HTTPS in addition to plain HTTP. This will set defaults for
+        <literal>listen</literal> to listen on all interfaces on the respective default
+        ports (80, 443).
+      '';
+    };
+
+    onlySSL = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable HTTPS and reject plain HTTP connections. This will set
+        defaults for <literal>listen</literal> to listen on all interfaces on port 443.
+      '';
+    };
+
+    enableSSL = mkOption {
+      type = types.bool;
+      visible = false;
+      default = false;
+    };
+
+    forceSSL = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to add a separate nginx server block that permanently redirects (301)
+        all plain HTTP traffic to HTTPS. This will set defaults for
+        <literal>listen</literal> to listen on all interfaces on the respective default
+        ports (80, 443), where the non-SSL listens are used for the redirect vhosts.
+      '';
+    };
+
+    rejectSSL = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to listen for and reject all HTTPS connections to this vhost. Useful in
+        <link linkend="opt-services.nginx.virtualHosts._name_.default">default</link>
+        server blocks to avoid serving the certificate for another vhost. Uses the
+        <literal>ssl_reject_handshake</literal> directive available in nginx versions
+        1.19.4 and above.
+      '';
+    };
+
+    kTLS = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable kTLS support.
+        Implementing TLS in the kernel (kTLS) improves performance by significantly
+        reducing the need for copying operations between user space and the kernel.
+        Required Nginx version 1.21.4 or later.
+      '';
+    };
+
+    sslCertificate = mkOption {
+      type = types.path;
+      example = "/var/host.cert";
+      description = "Path to server SSL certificate.";
+    };
+
+    sslCertificateKey = mkOption {
+      type = types.path;
+      example = "/var/host.key";
+      description = "Path to server SSL certificate key.";
+    };
+
+    sslTrustedCertificate = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = literalExpression ''"''${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"'';
+      description = "Path to root SSL certificate for stapling and client certificates.";
+    };
+
+    http2 = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to enable HTTP 2.
+        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
+        IP address / port.
+        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.
+      '';
+    };
+
+    http3 = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable HTTP 3.
+        This requires using <literal>pkgs.nginxQuic</literal> package
+        which can be achieved by setting <literal>services.nginx.package = pkgs.nginxQuic;</literal>.
+        Note that HTTP 3 support is experimental and
+        *not* yet recommended for production.
+        Read more at https://quic.nginx.org/
+      '';
+    };
+
+    root = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/data/webserver/docs";
+      description = ''
+        The path of the web root directory.
+      '';
+    };
+
+    default = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Makes this vhost the default.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        These lines go to the end of the vhost verbatim.
+      '';
+    };
+
+    globalRedirect = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "newserver.example.org";
+      description = ''
+        If set, all requests for this host are redirected permanently to
+        the given hostname.
+      '';
+    };
+
+    basicAuth = mkOption {
+      type = types.attrsOf types.str;
+      default = {};
+      example = literalExpression ''
+        {
+          user = "password";
+        };
+      '';
+      description = ''
+        Basic Auth protection for a vhost.
+
+        WARNING: This is implemented to store the password in plain text in the
+        Nix store.
+      '';
+    };
+
+    basicAuthFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Basic Auth password file for a vhost.
+        Can be created via: <command>htpasswd -c &lt;filename&gt; &lt;username&gt;</command>.
+
+        WARNING: The generate file contains the users' passwords in a
+        non-cryptographically-securely hashed way.
+      '';
+    };
+
+    locations = mkOption {
+      type = types.attrsOf (types.submodule (import ./location-options.nix {
+        inherit lib;
+      }));
+      default = {};
+      example = literalExpression ''
+        {
+          "/" = {
+            proxyPass = "http://localhost:3000";
+          };
+        };
+      '';
+      description = "Declarative location config";
+    };
+  };
+}
diff --git a/nixos/modules/services/web-servers/phpfpm/default.nix b/nixos/modules/services/web-servers/phpfpm/default.nix
new file mode 100644
index 00000000000..87c68fa074a
--- /dev/null
+++ b/nixos/modules/services/web-servers/phpfpm/default.nix
@@ -0,0 +1,282 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.phpfpm;
+
+  runtimeDir = "/run/phpfpm";
+
+  toStr = value:
+    if true == value then "yes"
+    else if false == value then "no"
+    else toString value;
+
+  fpmCfgFile = pool: poolOpts: pkgs.writeText "phpfpm-${pool}.conf" ''
+    [global]
+    ${concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${toStr v}") cfg.settings)}
+    ${optionalString (cfg.extraConfig != null) cfg.extraConfig}
+
+    [${pool}]
+    ${concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${toStr v}") poolOpts.settings)}
+    ${concatStringsSep "\n" (mapAttrsToList (n: v: "env[${n}] = ${toStr v}") poolOpts.phpEnv)}
+    ${optionalString (poolOpts.extraConfig != null) poolOpts.extraConfig}
+  '';
+
+  phpIni = poolOpts: pkgs.runCommand "php.ini" {
+    inherit (poolOpts) phpPackage phpOptions;
+    preferLocalBuild = true;
+    passAsFile = [ "phpOptions" ];
+  } ''
+    cat ${poolOpts.phpPackage}/etc/php.ini $phpOptionsPath > $out
+  '';
+
+  poolOpts = { name, ... }:
+    let
+      poolOpts = cfg.pools.${name};
+    in
+    {
+      options = {
+        socket = mkOption {
+          type = types.str;
+          readOnly = true;
+          description = ''
+            Path to the unix socket file on which to accept FastCGI requests.
+            <note><para>This option is read-only and managed by NixOS.</para></note>
+          '';
+          example = "${runtimeDir}/<name>.sock";
+        };
+
+        listen = mkOption {
+          type = types.str;
+          default = "";
+          example = "/path/to/unix/socket";
+          description = ''
+            The address on which to accept FastCGI requests.
+          '';
+        };
+
+        phpPackage = mkOption {
+          type = types.package;
+          default = cfg.phpPackage;
+          defaultText = literalExpression "config.services.phpfpm.phpPackage";
+          description = ''
+            The PHP package to use for running this PHP-FPM pool.
+          '';
+        };
+
+        phpOptions = mkOption {
+          type = types.lines;
+          description = ''
+            "Options appended to the PHP configuration file <filename>php.ini</filename> used for this PHP-FPM pool."
+          '';
+        };
+
+        phpEnv = lib.mkOption {
+          type = with types; attrsOf str;
+          default = {};
+          description = ''
+            Environment variables used for this PHP-FPM pool.
+          '';
+          example = literalExpression ''
+            {
+              HOSTNAME = "$HOSTNAME";
+              TMP = "/tmp";
+              TMPDIR = "/tmp";
+              TEMP = "/tmp";
+            }
+          '';
+        };
+
+        user = mkOption {
+          type = types.str;
+          description = "User account under which this pool runs.";
+        };
+
+        group = mkOption {
+          type = types.str;
+          description = "Group account under which this pool runs.";
+        };
+
+        settings = mkOption {
+          type = with types; attrsOf (oneOf [ str int bool ]);
+          default = {};
+          description = ''
+            PHP-FPM pool directives. Refer to the "List of pool directives" section of
+            <link xlink:href="https://www.php.net/manual/en/install.fpm.configuration.php"/>
+            for details. Note that settings names must be enclosed in quotes (e.g.
+            <literal>"pm.max_children"</literal> instead of <literal>pm.max_children</literal>).
+          '';
+          example = literalExpression ''
+            {
+              "pm" = "dynamic";
+              "pm.max_children" = 75;
+              "pm.start_servers" = 10;
+              "pm.min_spare_servers" = 5;
+              "pm.max_spare_servers" = 20;
+              "pm.max_requests" = 500;
+            }
+          '';
+        };
+
+        extraConfig = mkOption {
+          type = with types; nullOr lines;
+          default = null;
+          description = ''
+            Extra lines that go into the pool configuration.
+            See the documentation on <literal>php-fpm.conf</literal> for
+            details on configuration directives.
+          '';
+        };
+      };
+
+      config = {
+        socket = if poolOpts.listen == "" then "${runtimeDir}/${name}.sock" else poolOpts.listen;
+        group = mkDefault poolOpts.user;
+        phpOptions = mkBefore cfg.phpOptions;
+
+        settings = mapAttrs (name: mkDefault){
+          listen = poolOpts.socket;
+          user = poolOpts.user;
+          group = poolOpts.group;
+        };
+      };
+    };
+
+in {
+  imports = [
+    (mkRemovedOptionModule [ "services" "phpfpm" "poolConfigs" ] "Use services.phpfpm.pools instead.")
+    (mkRemovedOptionModule [ "services" "phpfpm" "phpIni" ] "")
+  ];
+
+  options = {
+    services.phpfpm = {
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ str int bool ]);
+        default = {};
+        description = ''
+          PHP-FPM global directives. Refer to the "List of global php-fpm.conf directives" section of
+          <link xlink:href="https://www.php.net/manual/en/install.fpm.configuration.php"/>
+          for details. Note that settings names must be enclosed in quotes (e.g.
+          <literal>"pm.max_children"</literal> instead of <literal>pm.max_children</literal>).
+          You need not specify the options <literal>error_log</literal> or
+          <literal>daemonize</literal> here, since they are generated by NixOS.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = with types; nullOr lines;
+        default = null;
+        description = ''
+          Extra configuration that should be put in the global section of
+          the PHP-FPM configuration file. Do not specify the options
+          <literal>error_log</literal> or
+          <literal>daemonize</literal> here, since they are generated by
+          NixOS.
+        '';
+      };
+
+      phpPackage = mkOption {
+        type = types.package;
+        default = pkgs.php;
+        defaultText = literalExpression "pkgs.php";
+        description = ''
+          The PHP package to use for running the PHP-FPM service.
+        '';
+      };
+
+      phpOptions = mkOption {
+        type = types.lines;
+        default = "";
+        example =
+          ''
+            date.timezone = "CET"
+          '';
+        description = ''
+          Options appended to the PHP configuration file <filename>php.ini</filename>.
+        '';
+      };
+
+      pools = mkOption {
+        type = types.attrsOf (types.submodule poolOpts);
+        default = {};
+        example = literalExpression ''
+         {
+           mypool = {
+             user = "php";
+             group = "php";
+             phpPackage = pkgs.php;
+             settings = {
+               "pm" = "dynamic";
+               "pm.max_children" = 75;
+               "pm.start_servers" = 10;
+               "pm.min_spare_servers" = 5;
+               "pm.max_spare_servers" = 20;
+               "pm.max_requests" = 500;
+             };
+           }
+         }'';
+        description = ''
+          PHP-FPM pools. If no pools are defined, the PHP-FPM
+          service is disabled.
+        '';
+      };
+    };
+  };
+
+  config = mkIf (cfg.pools != {}) {
+
+    warnings =
+      mapAttrsToList (pool: poolOpts: ''
+        Using config.services.phpfpm.pools.${pool}.listen is deprecated and will become unsupported in a future release. Please reference the read-only option config.services.phpfpm.pools.${pool}.socket to access the path of your socket.
+      '') (filterAttrs (pool: poolOpts: poolOpts.listen != "") cfg.pools) ++
+      mapAttrsToList (pool: poolOpts: ''
+        Using config.services.phpfpm.pools.${pool}.extraConfig is deprecated and will become unsupported in a future release. Please migrate your configuration to config.services.phpfpm.pools.${pool}.settings.
+      '') (filterAttrs (pool: poolOpts: poolOpts.extraConfig != null) cfg.pools) ++
+      optional (cfg.extraConfig != null) ''
+        Using config.services.phpfpm.extraConfig is deprecated and will become unsupported in a future release. Please migrate your configuration to config.services.phpfpm.settings.
+      ''
+    ;
+
+    services.phpfpm.settings = {
+      error_log = "syslog";
+      daemonize = false;
+    };
+
+    systemd.slices.phpfpm = {
+      description = "PHP FastCGI Process manager pools slice";
+    };
+
+    systemd.targets.phpfpm = {
+      description = "PHP FastCGI Process manager pools target";
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    systemd.services = mapAttrs' (pool: poolOpts:
+      nameValuePair "phpfpm-${pool}" {
+        description = "PHP FastCGI Process Manager service for pool ${pool}";
+        after = [ "network.target" ];
+        wantedBy = [ "phpfpm.target" ];
+        partOf = [ "phpfpm.target" ];
+        serviceConfig = let
+          cfgFile = fpmCfgFile pool poolOpts;
+          iniFile = phpIni poolOpts;
+        in {
+          Slice = "phpfpm.slice";
+          PrivateDevices = true;
+          PrivateTmp = true;
+          ProtectSystem = "full";
+          ProtectHome = true;
+          # XXX: We need AF_NETLINK to make the sendmail SUID binary from postfix work
+          RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+          Type = "notify";
+          ExecStart = "${poolOpts.phpPackage}/bin/php-fpm -y ${cfgFile} -c ${iniFile}";
+          ExecReload = "${pkgs.coreutils}/bin/kill -USR2 $MAINPID";
+          RuntimeDirectory = "phpfpm";
+          RuntimeDirectoryPreserve = true; # Relevant when multiple processes are running
+          Restart = "always";
+        };
+      }
+    ) cfg.pools;
+  };
+}
diff --git a/nixos/modules/services/web-servers/pomerium.nix b/nixos/modules/services/web-servers/pomerium.nix
new file mode 100644
index 00000000000..0b460755f50
--- /dev/null
+++ b/nixos/modules/services/web-servers/pomerium.nix
@@ -0,0 +1,135 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  format = pkgs.formats.yaml {};
+in
+{
+  options.services.pomerium = {
+    enable = mkEnableOption "the Pomerium authenticating reverse proxy";
+
+    configFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      description = "Path to Pomerium config YAML. If set, overrides services.pomerium.settings.";
+    };
+
+    useACMEHost = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      description = ''
+        If set, use a NixOS-generated ACME certificate with the specified name.
+
+        Note that this will require you to use a non-HTTP-based challenge, or
+        disable Pomerium's in-built HTTP redirect server by setting
+        http_redirect_addr to null and use a different HTTP server for serving
+        the challenge response.
+
+        If you're using an HTTP-based challenge, you should use the
+        Pomerium-native autocert option instead.
+      '';
+    };
+
+    settings = mkOption {
+      description = ''
+        The contents of Pomerium's config.yaml, in Nix expressions.
+
+        Specifying configFile will override this in its entirety.
+
+        See <link xlink:href="https://pomerium.io/reference/">the Pomerium
+        configuration reference</link> for more information about what to put
+        here.
+      '';
+      default = {};
+      type = format.type;
+    };
+
+    secretsFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      description = ''
+        Path to file containing secrets for Pomerium, in systemd
+        EnvironmentFile format. See the systemd.exec(5) man page.
+      '';
+    };
+  };
+
+  config = let
+    cfg = config.services.pomerium;
+    cfgFile = if cfg.configFile != null then cfg.configFile else (format.generate "pomerium.yaml" cfg.settings);
+  in mkIf cfg.enable ({
+    systemd.services.pomerium = {
+      description = "Pomerium authenticating reverse proxy";
+      wants = [ "network.target" ] ++ (optional (cfg.useACMEHost != null) "acme-finished-${cfg.useACMEHost}.target");
+      after = [ "network.target" ] ++ (optional (cfg.useACMEHost != null) "acme-finished-${cfg.useACMEHost}.target");
+      wantedBy = [ "multi-user.target" ];
+      environment = optionalAttrs (cfg.useACMEHost != null) {
+        CERTIFICATE_FILE = "fullchain.pem";
+        CERTIFICATE_KEY_FILE = "key.pem";
+      };
+      startLimitIntervalSec = 60;
+      script = ''
+        if [[ -v CREDENTIALS_DIRECTORY ]]; then
+          cd "$CREDENTIALS_DIRECTORY"
+        fi
+        exec "${pkgs.pomerium}/bin/pomerium" -config "${cfgFile}"
+      '';
+
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = [ "pomerium" ];
+
+        PrivateUsers = false;  # breaks CAP_NET_BIND_SERVICE
+        MemoryDenyWriteExecute = false;  # breaks LuaJIT
+
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        DevicePolicy = "closed";
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectKernelLogs = true;
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        LockPersonality = true;
+        SystemCallArchitectures = "native";
+
+        EnvironmentFile = cfg.secretsFile;
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+
+        LoadCredential = optionals (cfg.useACMEHost != null) [
+          "fullchain.pem:/var/lib/acme/${cfg.useACMEHost}/fullchain.pem"
+          "key.pem:/var/lib/acme/${cfg.useACMEHost}/key.pem"
+        ];
+      };
+    };
+
+    # postRun hooks on cert renew can't be used to restart Nginx since renewal
+    # runs as the unprivileged acme user. sslTargets are added to wantedBy + before
+    # which allows the acme-finished-$cert.target to signify the successful updating
+    # of certs end-to-end.
+    systemd.services.pomerium-config-reload = mkIf (cfg.useACMEHost != null) {
+      # TODO(lukegb): figure out how to make config reloading work with credentials.
+
+      wantedBy = [ "acme-finished-${cfg.useACMEHost}.target" "multi-user.target" ];
+      # Before the finished targets, after the renew services.
+      before = [ "acme-finished-${cfg.useACMEHost}.target" ];
+      after = [ "acme-${cfg.useACMEHost}.service" ];
+      # Block reloading if not all certs exist yet.
+      unitConfig.ConditionPathExists = [ "${config.security.acme.certs.${cfg.useACMEHost}.directory}/fullchain.pem" ];
+      serviceConfig = {
+        Type = "oneshot";
+        TimeoutSec = 60;
+        ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active pomerium.service";
+        ExecStart = "/run/current-system/systemd/bin/systemctl --no-block restart pomerium.service";
+      };
+    };
+  });
+}
diff --git a/nixos/modules/services/web-servers/tomcat.nix b/nixos/modules/services/web-servers/tomcat.nix
new file mode 100644
index 00000000000..877097cf378
--- /dev/null
+++ b/nixos/modules/services/web-servers/tomcat.nix
@@ -0,0 +1,423 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.tomcat;
+  tomcat = cfg.package;
+in
+
+{
+
+  meta = {
+    maintainers = with maintainers; [ danbst ];
+  };
+
+  ###### interface
+
+  options = {
+
+    services.tomcat = {
+      enable = mkEnableOption "Apache Tomcat";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.tomcat9;
+        defaultText = literalExpression "pkgs.tomcat9";
+        example = lib.literalExpression "pkgs.tomcat9";
+        description = ''
+          Which tomcat package to use.
+        '';
+      };
+
+      purifyOnStart = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          On startup, the `baseDir` directory is populated with various files,
+          subdirectories and symlinks. If this option is enabled, these items
+          (except for the `logs` and `work` subdirectories) are first removed.
+          This prevents interference from remainders of an old configuration
+          (libraries, webapps, etc.), so it's recommended to enable this option.
+        '';
+      };
+
+      baseDir = mkOption {
+        type = lib.types.path;
+        default = "/var/tomcat";
+        description = ''
+          Location where Tomcat stores configuration files, web applications
+          and logfiles. Note that it is partially cleared on each service startup
+          if `purifyOnStart` is enabled.
+        '';
+      };
+
+      logDirs = mkOption {
+        default = [];
+        type = types.listOf types.path;
+        description = "Directories to create in baseDir/logs/";
+      };
+
+      extraConfigFiles = mkOption {
+        default = [];
+        type = types.listOf types.path;
+        description = "Extra configuration files to pull into the tomcat conf directory";
+      };
+
+      extraEnvironment = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "ENVIRONMENT=production" ];
+        description = "Environment Variables to pass to the tomcat service";
+      };
+
+      extraGroups = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        example = [ "users" ];
+        description = "Defines extra groups to which the tomcat user belongs.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "tomcat";
+        description = "User account under which Apache Tomcat runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "tomcat";
+        description = "Group account under which Apache Tomcat runs.";
+      };
+
+      javaOpts = mkOption {
+        type = types.either (types.listOf types.str) types.str;
+        default = "";
+        description = "Parameters to pass to the Java Virtual Machine which spawns Apache Tomcat";
+      };
+
+      catalinaOpts = mkOption {
+        type = types.either (types.listOf types.str) types.str;
+        default = "";
+        description = "Parameters to pass to the Java Virtual Machine which spawns the Catalina servlet container";
+      };
+
+      sharedLibs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "List containing JAR files or directories with JAR files which are libraries shared by the web applications";
+      };
+
+      serverXml = mkOption {
+        type = types.lines;
+        default = "";
+        description = "
+          Verbatim server.xml configuration.
+          This is mutually exclusive with the virtualHosts options.
+        ";
+      };
+
+      commonLibs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "List containing JAR files or directories with JAR files which are libraries shared by the web applications and the servlet container";
+      };
+
+      webapps = mkOption {
+        type = types.listOf types.path;
+        default = [ tomcat.webapps ];
+        defaultText = literalExpression "[ config.services.tomcat.package.webapps ]";
+        description = "List containing WAR files or directories with WAR files which are web applications to be deployed on Tomcat";
+      };
+
+      virtualHosts = mkOption {
+        type = types.listOf (types.submodule {
+          options = {
+            name = mkOption {
+              type = types.str;
+              description = "name of the virtualhost";
+            };
+            aliases = mkOption {
+              type = types.listOf types.str;
+              description = "aliases of the virtualhost";
+              default = [];
+            };
+            webapps = mkOption {
+              type = types.listOf types.path;
+              description = ''
+                List containing web application WAR files and/or directories containing
+                web applications and configuration files for the virtual host.
+              '';
+              default = [];
+            };
+          };
+        });
+        default = [];
+        description = "List consisting of a virtual host name and a list of web applications to deploy on each virtual host";
+      };
+
+      logPerVirtualHost = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable logging per virtual host.";
+      };
+
+      jdk = mkOption {
+        type = types.package;
+        default = pkgs.jdk;
+        defaultText = literalExpression "pkgs.jdk";
+        description = "Which JDK to use.";
+      };
+
+      axis2 = {
+
+        enable = mkOption {
+          default = false;
+          type = types.bool;
+          description = "Whether to enable an Apache Axis2 container";
+        };
+
+        services = mkOption {
+          default = [];
+          type = types.listOf types.str;
+          description = "List containing AAR files or directories with AAR files which are web services to be deployed on Axis2";
+        };
+
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.tomcat.enable {
+
+    users.groups.tomcat.gid = config.ids.gids.tomcat;
+
+    users.users.tomcat =
+      { uid = config.ids.uids.tomcat;
+        description = "Tomcat user";
+        home = "/homeless-shelter";
+        group = "tomcat";
+        extraGroups = cfg.extraGroups;
+      };
+
+    systemd.services.tomcat = {
+      description = "Apache Tomcat server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      preStart = ''
+        ${lib.optionalString cfg.purifyOnStart ''
+          # Delete most directories/symlinks we create from the existing base directory,
+          # to get rid of remainders of an old configuration.
+          # The list of directories to delete is taken from the "mkdir" command below,
+          # excluding "logs" (because logs are valuable) and "work" (because normally
+          # session files are there), and additionally including "bin".
+          rm -rf ${cfg.baseDir}/{conf,virtualhosts,temp,lib,shared/lib,webapps,bin}
+        ''}
+
+        # Create the base directory
+        mkdir -p \
+          ${cfg.baseDir}/{conf,virtualhosts,logs,temp,lib,shared/lib,webapps,work}
+        chown ${cfg.user}:${cfg.group} \
+          ${cfg.baseDir}/{conf,virtualhosts,logs,temp,lib,shared/lib,webapps,work}
+
+        # Create a symlink to the bin directory of the tomcat component
+        ln -sfn ${tomcat}/bin ${cfg.baseDir}/bin
+
+        # Symlink the config files in the conf/ directory (except for catalina.properties and server.xml)
+        for i in $(ls ${tomcat}/conf | grep -v catalina.properties | grep -v server.xml); do
+          ln -sfn ${tomcat}/conf/$i ${cfg.baseDir}/conf/`basename $i`
+        done
+
+        ${if cfg.extraConfigFiles != [] then ''
+          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
+        sed -e 's|''${catalina.home}|''${catalina.base}|g' \
+          -e 's|shared.loader=|shared.loader=''${catalina.base}/shared/lib/*.jar|' \
+          ${tomcat}/conf/catalina.properties > ${cfg.baseDir}/conf/catalina.properties
+
+        ${if cfg.serverXml != "" then ''
+          cp -f ${pkgs.writeTextDir "server.xml" cfg.serverXml}/* ${cfg.baseDir}/conf/
+        '' else
+          let
+            hostElementForVirtualHost = virtualHost: ''
+              <Host name="${virtualHost.name}" appBase="virtualhosts/${virtualHost.name}/webapps"
+                    unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false">
+            '' + concatStrings (innerElementsForVirtualHost virtualHost) + ''
+              </Host>
+            '';
+            innerElementsForVirtualHost = virtualHost:
+              (map (alias: ''
+                <Alias>${alias}</Alias>
+              '') virtualHost.aliases)
+              ++ (optional cfg.logPerVirtualHost ''
+                <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs/${virtualHost.name}"
+                       prefix="${virtualHost.name}_access_log." pattern="combined" resolveHosts="false"/>
+              '');
+            hostElementsString = concatMapStringsSep "\n" hostElementForVirtualHost cfg.virtualHosts;
+            hostElementsSedString = replaceStrings ["\n"] ["\\\n"] hostElementsString;
+          in ''
+            # Create a modified server.xml which also includes all virtual hosts
+            sed -e "/<Engine name=\"Catalina\" defaultHost=\"localhost\">/a\\"${escapeShellArg hostElementsSedString} \
+                  ${tomcat}/conf/server.xml > ${cfg.baseDir}/conf/server.xml
+          ''
+        }
+        ${optionalString (cfg.logDirs != []) ''
+          for i in ${toString cfg.logDirs}; do
+            mkdir -p ${cfg.baseDir}/logs/$i
+            chown ${cfg.user}:${cfg.group} ${cfg.baseDir}/logs/$i
+          done
+        ''}
+        ${optionalString cfg.logPerVirtualHost (toString (map (h: ''
+          mkdir -p ${cfg.baseDir}/logs/${h.name}
+          chown ${cfg.user}:${cfg.group} ${cfg.baseDir}/logs/${h.name}
+        '') cfg.virtualHosts))}
+
+        # Symlink all the given common libs files or paths into the lib/ directory
+        for i in ${tomcat} ${toString cfg.commonLibs}; do
+          if [ -f $i ]; then
+            # If the given web application is a file, symlink it into the common/lib/ directory
+            ln -sfn $i ${cfg.baseDir}/lib/`basename $i`
+          elif [ -d $i ]; then
+            # If the given web application is a directory, then iterate over the files
+            # in the special purpose directories and symlink them into the tomcat tree
+
+            for j in $i/lib/*; do
+              ln -sfn $j ${cfg.baseDir}/lib/`basename $j`
+            done
+          fi
+        done
+
+        # Symlink all the given shared libs files or paths into the shared/lib/ directory
+        for i in ${toString cfg.sharedLibs}; do
+          if [ -f $i ]; then
+            # If the given web application is a file, symlink it into the common/lib/ directory
+            ln -sfn $i ${cfg.baseDir}/shared/lib/`basename $i`
+          elif [ -d $i ]; then
+            # If the given web application is a directory, then iterate over the files
+            # in the special purpose directories and symlink them into the tomcat tree
+
+            for j in $i/shared/lib/*; do
+              ln -sfn $j ${cfg.baseDir}/shared/lib/`basename $j`
+            done
+          fi
+        done
+
+        # Symlink all the given web applications files or paths into the webapps/ directory
+        for i in ${toString cfg.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}/webapps/`basename $i`
+          elif [ -d $i ]; then
+            # If the given web application is a directory, then iterate over the files
+            # in the special purpose directories and symlink them into the tomcat tree
+
+            for j in $i/webapps/*; do
+              ln -sfn $j ${cfg.baseDir}/webapps/`basename $j`
+            done
+
+            # Also symlink the configuration files if they are included
+            if [ -d $i/conf/Catalina ]; then
+              for j in $i/conf/Catalina/*; do
+                mkdir -p ${cfg.baseDir}/conf/Catalina/localhost
+                ln -sfn $j ${cfg.baseDir}/conf/Catalina/localhost/`basename $j`
+              done
+            fi
+          fi
+        done
+
+        ${toString (map (virtualHost: ''
+          # Create webapps directory for the virtual host
+          mkdir -p ${cfg.baseDir}/virtualhosts/${virtualHost.name}/webapps
+
+          # Modify ownership
+          chown ${cfg.user}:${cfg.group} ${cfg.baseDir}/virtualhosts/${virtualHost.name}/webapps
+
+          # 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
+            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`
+            elif [ -d $i ]; then
+              # If the given web application is a directory, then iterate over the files
+              # in the special purpose directories and symlink them into the tomcat tree
+
+              for j in $i/webapps/*; do
+                ln -sfn $j ${cfg.baseDir}/virtualhosts/${virtualHost.name}/webapps/`basename $j`
+              done
+
+              # Also symlink the configuration files if they are included
+              if [ -d $i/conf/Catalina ]; then
+                for j in $i/conf/Catalina/*; do
+                  mkdir -p ${cfg.baseDir}/conf/Catalina/${virtualHost.name}
+                  ln -sfn $j ${cfg.baseDir}/conf/Catalina/${virtualHost.name}/`basename $j`
+                done
+              fi
+            fi
+          done
+        '') cfg.virtualHosts)}
+
+        ${optionalString cfg.axis2.enable ''
+          # Copy the Axis2 web application
+          cp -av ${pkgs.axis2}/webapps/axis2 ${cfg.baseDir}/webapps
+
+          # Turn off addressing, which causes many errors
+          sed -i -e 's%<module ref="addressing"/>%<!-- <module ref="addressing"/> -->%' ${cfg.baseDir}/webapps/axis2/WEB-INF/conf/axis2.xml
+
+          # Modify permissions on the Axis2 application
+          chown -R ${cfg.user}:${cfg.group} ${cfg.baseDir}/webapps/axis2
+
+          # Symlink all the given web service files or paths into the webapps/axis2/WEB-INF/services directory
+          for i in ${toString cfg.axis2.services}; do
+            if [ -f $i ]; then
+              # If the given web service is a file, symlink it into the webapps/axis2/WEB-INF/services
+              ln -sfn $i ${cfg.baseDir}/webapps/axis2/WEB-INF/services/`basename $i`
+            elif [ -d $i ]; then
+              # If the given web application is a directory, then iterate over the files
+              # in the special purpose directories and symlink them into the tomcat tree
+
+              for j in $i/webapps/axis2/WEB-INF/services/*; do
+                ln -sfn $j ${cfg.baseDir}/webapps/axis2/WEB-INF/services/`basename $j`
+              done
+
+              # Also symlink the configuration files if they are included
+              if [ -d $i/conf/Catalina ]; then
+                for j in $i/conf/Catalina/*; do
+                  ln -sfn $j ${cfg.baseDir}/conf/Catalina/localhost/`basename $j`
+                done
+              fi
+            fi
+          done
+        ''}
+      '';
+
+      serviceConfig = {
+        Type = "forking";
+        PermissionsStartOnly = true;
+        PIDFile="/run/tomcat/tomcat.pid";
+        RuntimeDirectory = "tomcat";
+        User = cfg.user;
+        Environment=[
+          "CATALINA_BASE=${cfg.baseDir}"
+          "CATALINA_PID=/run/tomcat/tomcat.pid"
+          "JAVA_HOME='${cfg.jdk}'"
+          "JAVA_OPTS='${builtins.toString cfg.javaOpts}'"
+          "CATALINA_OPTS='${builtins.toString cfg.catalinaOpts}'"
+        ] ++ cfg.extraEnvironment;
+        ExecStart = "${tomcat}/bin/startup.sh";
+        ExecStop = "${tomcat}/bin/shutdown.sh";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-servers/traefik.nix b/nixos/modules/services/web-servers/traefik.nix
new file mode 100644
index 00000000000..eb7fd0995de
--- /dev/null
+++ b/nixos/modules/services/web-servers/traefik.nix
@@ -0,0 +1,170 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.traefik;
+  jsonValue = with types;
+    let
+      valueType = nullOr (oneOf [
+        bool
+        int
+        float
+        str
+        (lazyAttrsOf valueType)
+        (listOf valueType)
+      ]) // {
+        description = "JSON value";
+        emptyValue.value = { };
+      };
+    in valueType;
+  dynamicConfigFile = if cfg.dynamicConfigFile == null then
+    pkgs.runCommand "config.toml" {
+      buildInputs = [ pkgs.remarshal ];
+      preferLocalBuild = true;
+    } ''
+      remarshal -if json -of toml \
+        < ${
+          pkgs.writeText "dynamic_config.json"
+          (builtins.toJSON cfg.dynamicConfigOptions)
+        } \
+        > $out
+    ''
+  else
+    cfg.dynamicConfigFile;
+  staticConfigFile = if cfg.staticConfigFile == null then
+    pkgs.runCommand "config.toml" {
+      buildInputs = [ pkgs.yj ];
+      preferLocalBuild = true;
+    } ''
+      yj -jt -i \
+        < ${
+          pkgs.writeText "static_config.json" (builtins.toJSON
+            (recursiveUpdate cfg.staticConfigOptions {
+              providers.file.filename = "${dynamicConfigFile}";
+            }))
+        } \
+        > $out
+    ''
+  else
+    cfg.staticConfigFile;
+in {
+  options.services.traefik = {
+    enable = mkEnableOption "Traefik web server";
+
+    staticConfigFile = mkOption {
+      default = null;
+      example = literalExpression "/path/to/static_config.toml";
+      type = types.nullOr types.path;
+      description = ''
+        Path to traefik's static configuration to use.
+        (Using that option has precedence over <literal>staticConfigOptions</literal> and <literal>dynamicConfigOptions</literal>)
+      '';
+    };
+
+    staticConfigOptions = mkOption {
+      description = ''
+        Static configuration for Traefik.
+      '';
+      type = jsonValue;
+      default = { entryPoints.http.address = ":80"; };
+      example = {
+        entryPoints.web.address = ":8080";
+        entryPoints.http.address = ":80";
+
+        api = { };
+      };
+    };
+
+    dynamicConfigFile = mkOption {
+      default = null;
+      example = literalExpression "/path/to/dynamic_config.toml";
+      type = types.nullOr types.path;
+      description = ''
+        Path to traefik's dynamic configuration to use.
+        (Using that option has precedence over <literal>dynamicConfigOptions</literal>)
+      '';
+    };
+
+    dynamicConfigOptions = mkOption {
+      description = ''
+        Dynamic configuration for Traefik.
+      '';
+      type = jsonValue;
+      default = { };
+      example = {
+        http.routers.router1 = {
+          rule = "Host(`localhost`)";
+          service = "service1";
+        };
+
+        http.services.service1.loadBalancer.servers =
+          [{ url = "http://localhost:8080"; }];
+      };
+    };
+
+    dataDir = mkOption {
+      default = "/var/lib/traefik";
+      type = types.path;
+      description = ''
+        Location for any persistent data traefik creates, ie. acme
+      '';
+    };
+
+    group = mkOption {
+      default = "traefik";
+      type = types.str;
+      example = "docker";
+      description = ''
+        Set the group that traefik runs under.
+        For the docker backend this needs to be set to <literal>docker</literal> instead.
+      '';
+    };
+
+    package = mkOption {
+      default = pkgs.traefik;
+      defaultText = literalExpression "pkgs.traefik";
+      type = types.package;
+      description = "Traefik package to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [ "d '${cfg.dataDir}' 0700 traefik traefik - -" ];
+
+    systemd.services.traefik = {
+      description = "Traefik web server";
+      after = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      startLimitIntervalSec = 86400;
+      startLimitBurst = 5;
+      serviceConfig = {
+        ExecStart =
+          "${cfg.package}/bin/traefik --configfile=${staticConfigFile}";
+        Type = "simple";
+        User = "traefik";
+        Group = cfg.group;
+        Restart = "on-failure";
+        AmbientCapabilities = "cap_net_bind_service";
+        CapabilityBoundingSet = "cap_net_bind_service";
+        NoNewPrivileges = true;
+        LimitNPROC = 64;
+        LimitNOFILE = 1048576;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHome = true;
+        ProtectSystem = "full";
+        ReadWriteDirectories = cfg.dataDir;
+      };
+    };
+
+    users.users.traefik = {
+      group = "traefik";
+      home = cfg.dataDir;
+      createHome = true;
+      isSystemUser = true;
+    };
+
+    users.groups.traefik = { };
+  };
+}
diff --git a/nixos/modules/services/web-servers/trafficserver/default.nix b/nixos/modules/services/web-servers/trafficserver/default.nix
new file mode 100644
index 00000000000..b52087fa038
--- /dev/null
+++ b/nixos/modules/services/web-servers/trafficserver/default.nix
@@ -0,0 +1,310 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.trafficserver;
+  user = config.users.users.trafficserver.name;
+  group = config.users.groups.trafficserver.name;
+
+  getManualUrl = name: "https://docs.trafficserver.apache.org/en/latest/admin-guide/files/${name}.en.html";
+
+  yaml = pkgs.formats.yaml { };
+
+  mkYamlConf = name: cfg:
+    if cfg != null then {
+      "trafficserver/${name}.yaml".source = yaml.generate "${name}.yaml" cfg;
+    } else {
+      "trafficserver/${name}.yaml".text = "";
+    };
+
+  mkRecordLines = path: value:
+    if isAttrs value then
+      lib.mapAttrsToList (n: v: mkRecordLines (path ++ [ n ]) v) value
+    else if isInt value then
+      "CONFIG ${concatStringsSep "." path} INT ${toString value}"
+    else if isFloat value then
+      "CONFIG ${concatStringsSep "." path} FLOAT ${toString value}"
+    else
+      "CONFIG ${concatStringsSep "." path} STRING ${toString value}";
+
+  mkRecordsConfig = cfg: concatStringsSep "\n" (flatten (mkRecordLines [ ] cfg));
+  mkPluginConfig = cfg: concatStringsSep "\n" (map (p: "${p.path} ${p.arg}") cfg);
+in
+{
+  options.services.trafficserver = {
+    enable = mkEnableOption "Apache Traffic Server";
+
+    cache = mkOption {
+      type = types.lines;
+      default = "";
+      example = "dest_domain=example.com suffix=js action=never-cache";
+      description = ''
+        Caching rules that overrule the origin's caching policy.
+
+        Consult the <link xlink:href="${getManualUrl "cache.config"}">upstream
+        documentation</link> for more details.
+      '';
+    };
+
+    hosting = mkOption {
+      type = types.lines;
+      default = "";
+      example = "domain=example.com volume=1";
+      description = ''
+        Partition the cache according to origin server or domain
+
+        Consult the <link xlink:href="${getManualUrl "hosting.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    ipAllow = mkOption {
+      type = types.nullOr yaml.type;
+      default = lib.importJSON ./ip_allow.json;
+      defaultText = literalDocBook "upstream defaults";
+      example = literalExpression ''
+        {
+          ip_allow = [{
+            apply = "in";
+            ip_addrs = "127.0.0.1";
+            action = "allow";
+            methods = "ALL";
+          }];
+        }
+      '';
+      description = ''
+        Control client access to Traffic Server and Traffic Server connections
+        to upstream servers.
+
+        Consult the <link xlink:href="${getManualUrl "ip_allow.yaml"}">upstream
+        documentation</link> for more details.
+      '';
+    };
+
+    logging = mkOption {
+      type = types.nullOr yaml.type;
+      default = lib.importJSON ./logging.json;
+      defaultText = literalDocBook "upstream defaults";
+      example = { };
+      description = ''
+        Configure logs.
+
+        Consult the <link xlink:href="${getManualUrl "logging.yaml"}">upstream
+        documentation</link> for more details.
+      '';
+    };
+
+    parent = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        dest_domain=. method=get parent="p1.example:8080; p2.example:8080" round_robin=true
+      '';
+      description = ''
+        Identify the parent proxies used in an cache hierarchy.
+
+        Consult the <link xlink:href="${getManualUrl "parent.config"}">upstream
+        documentation</link> for more details.
+      '';
+    };
+
+    plugins = mkOption {
+      default = [ ];
+
+      description = ''
+        Controls run-time loadable plugins available to Traffic Server, as
+        well as their configuration.
+
+        Consult the <link xlink:href="${getManualUrl "plugin.config"}">upstream
+        documentation</link> for more details.
+      '';
+
+      type = with types;
+        listOf (submodule {
+          options.path = mkOption {
+            type = str;
+            example = "xdebug.so";
+            description = ''
+              Path to plugin. The path can either be absolute, or relative to
+              the plugin directory.
+            '';
+          };
+          options.arg = mkOption {
+            type = str;
+            default = "";
+            example = "--header=ATS-My-Debug";
+            description = "arguments to pass to the plugin";
+          };
+        });
+    };
+
+    records = mkOption {
+      type = with types;
+        let valueType = (attrsOf (oneOf [ int float str valueType ])) // {
+          description = "Traffic Server records value";
+        };
+        in
+        valueType;
+      default = { };
+      example = { proxy.config.proxy_name = "my_server"; };
+      description = ''
+        List of configurable variables used by Traffic Server.
+
+        Consult the <link xlink:href="${getManualUrl "records.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    remap = mkOption {
+      type = types.lines;
+      default = "";
+      example = "map http://from.example http://origin.example";
+      description = ''
+        URL remapping rules used by Traffic Server.
+
+        Consult the <link xlink:href="${getManualUrl "remap.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    splitDns = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        dest_domain=internal.corp.example named="255.255.255.255:212 255.255.255.254" def_domain=corp.example search_list="corp.example corp1.example"
+        dest_domain=!internal.corp.example named=255.255.255.253
+      '';
+      description = ''
+        Specify the DNS server that Traffic Server should use under specific
+        conditions.
+
+        Consult the <link xlink:href="${getManualUrl "splitdns.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    sslMulticert = mkOption {
+      type = types.lines;
+      default = "";
+      example = "dest_ip=* ssl_cert_name=default.pem";
+      description = ''
+        Configure SSL server certificates to terminate the SSL sessions.
+
+        Consult the <link xlink:href="${getManualUrl "ssl_multicert.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    sni = mkOption {
+      type = types.nullOr yaml.type;
+      default = null;
+      example = literalExpression ''
+        {
+          sni = [{
+            fqdn = "no-http2.example.com";
+            https = "off";
+          }];
+        }
+      '';
+      description = ''
+        Configure aspects of TLS connection handling for both inbound and
+        outbound connections.
+
+        Consult the <link xlink:href="${getManualUrl "sni.yaml"}">upstream
+        documentation</link> for more details.
+      '';
+    };
+
+    storage = mkOption {
+      type = types.lines;
+      default = "/var/cache/trafficserver 256M";
+      example = "/dev/disk/by-id/XXXXX volume=1";
+      description = ''
+        List all the storage that make up the Traffic Server cache.
+
+        Consult the <link xlink:href="${getManualUrl "storage.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    strategies = mkOption {
+      type = types.nullOr yaml.type;
+      default = null;
+      description = ''
+        Specify the next hop proxies used in an cache hierarchy and the
+        algorithms used to select the next proxy.
+
+        Consult the <link xlink:href="${getManualUrl "strategies.yaml"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    volume = mkOption {
+      type = types.nullOr yaml.type;
+      default = "";
+      example = "volume=1 scheme=http size=20%";
+      description = ''
+        Manage cache space more efficiently and restrict disk usage by
+        creating cache volumes of different sizes.
+
+        Consult the <link xlink:href="${getManualUrl "volume.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc = {
+      "trafficserver/cache.config".text = cfg.cache;
+      "trafficserver/hosting.config".text = cfg.hosting;
+      "trafficserver/parent.config".text = cfg.parent;
+      "trafficserver/plugin.config".text = mkPluginConfig cfg.plugins;
+      "trafficserver/records.config".text = mkRecordsConfig cfg.records;
+      "trafficserver/remap.config".text = cfg.remap;
+      "trafficserver/splitdns.config".text = cfg.splitDns;
+      "trafficserver/ssl_multicert.config".text = cfg.sslMulticert;
+      "trafficserver/storage.config".text = cfg.storage;
+      "trafficserver/volume.config".text = cfg.volume;
+    } // (mkYamlConf "ip_allow" cfg.ipAllow)
+    // (mkYamlConf "logging" cfg.logging)
+    // (mkYamlConf "sni" cfg.sni)
+    // (mkYamlConf "strategies" cfg.strategies);
+
+    environment.systemPackages = [ pkgs.trafficserver ];
+    systemd.packages = [ pkgs.trafficserver ];
+
+    # Traffic Server does privilege handling independently of systemd, and
+    # therefore should be started as root
+    systemd.services.trafficserver = {
+      enable = true;
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    # These directories can't be created by systemd because:
+    #
+    #   1. Traffic Servers starts as root and switches to an unprivileged user
+    #      afterwards. The runtime directories defined below are assumed to be
+    #      owned by that user.
+    #   2. The bin/trafficserver script assumes these directories exist.
+    systemd.tmpfiles.rules = [
+      "d '/run/trafficserver' - ${user} ${group} - -"
+      "d '/var/cache/trafficserver' - ${user} ${group} - -"
+      "d '/var/lib/trafficserver' - ${user} ${group} - -"
+      "d '/var/log/trafficserver' - ${user} ${group} - -"
+    ];
+
+    services.trafficserver = {
+      records.proxy.config.admin.user_id = user;
+      records.proxy.config.body_factory.template_sets_dir =
+        "${pkgs.trafficserver}/etc/trafficserver/body_factory";
+    };
+
+    users.users.trafficserver = {
+      description = "Apache Traffic Server";
+      isSystemUser = true;
+      inherit group;
+    };
+    users.groups.trafficserver = { };
+  };
+}
diff --git a/nixos/modules/services/web-servers/trafficserver/ip_allow.json b/nixos/modules/services/web-servers/trafficserver/ip_allow.json
new file mode 100644
index 00000000000..fc2db803728
--- /dev/null
+++ b/nixos/modules/services/web-servers/trafficserver/ip_allow.json
@@ -0,0 +1,36 @@
+{
+  "ip_allow": [
+    {
+      "apply": "in",
+      "ip_addrs": "127.0.0.1",
+      "action": "allow",
+      "methods": "ALL"
+    },
+    {
+      "apply": "in",
+      "ip_addrs": "::1",
+      "action": "allow",
+      "methods": "ALL"
+    },
+    {
+      "apply": "in",
+      "ip_addrs": "0/0",
+      "action": "deny",
+      "methods": [
+        "PURGE",
+        "PUSH",
+        "DELETE"
+      ]
+    },
+    {
+      "apply": "in",
+      "ip_addrs": "::/0",
+      "action": "deny",
+      "methods": [
+        "PURGE",
+        "PUSH",
+        "DELETE"
+      ]
+    }
+  ]
+}
diff --git a/nixos/modules/services/web-servers/trafficserver/logging.json b/nixos/modules/services/web-servers/trafficserver/logging.json
new file mode 100644
index 00000000000..81e7ba0186c
--- /dev/null
+++ b/nixos/modules/services/web-servers/trafficserver/logging.json
@@ -0,0 +1,37 @@
+{
+  "logging": {
+    "formats": [
+      {
+        "name": "welf",
+        "format": "id=firewall time=\"%<cqtd> %<cqtt>\" fw=%<phn> pri=6 proto=%<cqus> duration=%<ttmsf> sent=%<psql> rcvd=%<cqhl> src=%<chi> dst=%<shi> dstname=%<shn> user=%<caun> op=%<cqhm> arg=\"%<cqup>\" result=%<pssc> ref=\"%<{Referer}cqh>\" agent=\"%<{user-agent}cqh>\" cache=%<crc>"
+      },
+      {
+        "name": "squid_seconds_only_timestamp",
+        "format": "%<cqts> %<ttms> %<chi> %<crc>/%<pssc> %<psql> %<cqhm> %<cquc> %<caun> %<phr>/%<shn> %<psct>"
+      },
+      {
+        "name": "squid",
+        "format": "%<cqtq> %<ttms> %<chi> %<crc>/%<pssc> %<psql> %<cqhm> %<cquc> %<caun> %<phr>/%<shn> %<psct>"
+      },
+      {
+        "name": "common",
+        "format": "%<chi> - %<caun> [%<cqtn>] \"%<cqtx>\" %<pssc> %<pscl>"
+      },
+      {
+        "name": "extended",
+        "format": "%<chi> - %<caun> [%<cqtn>] \"%<cqtx>\" %<pssc> %<pscl> %<sssc> %<sscl> %<cqcl> %<pqcl> %<cqhl> %<pshl> %<pqhl> %<sshl> %<tts>"
+      },
+      {
+        "name": "extended2",
+        "format": "%<chi> - %<caun> [%<cqtn>] \"%<cqtx>\" %<pssc> %<pscl> %<sssc> %<sscl> %<cqcl> %<pqcl> %<cqhl> %<pshl> %<pqhl> %<sshl> %<tts> %<phr> %<cfsc> %<pfsc> %<crc>"
+      }
+    ],
+    "logs": [
+      {
+        "filename": "squid",
+        "format": "squid",
+        "mode": "binary"
+      }
+    ]
+  }
+}
diff --git a/nixos/modules/services/web-servers/ttyd.nix b/nixos/modules/services/web-servers/ttyd.nix
new file mode 100644
index 00000000000..431509f7fd5
--- /dev/null
+++ b/nixos/modules/services/web-servers/ttyd.nix
@@ -0,0 +1,196 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.ttyd;
+
+  # Command line arguments for the ttyd daemon
+  args = [ "--port" (toString cfg.port) ]
+         ++ optionals (cfg.socket != null) [ "--interface" cfg.socket ]
+         ++ optionals (cfg.interface != null) [ "--interface" cfg.interface ]
+         ++ [ "--signal" (toString cfg.signal) ]
+         ++ (concatLists (mapAttrsToList (_k: _v: [ "--client-option" "${_k}=${_v}" ]) cfg.clientOptions))
+         ++ [ "--terminal-type" cfg.terminalType ]
+         ++ optionals cfg.checkOrigin [ "--check-origin" ]
+         ++ [ "--max-clients" (toString cfg.maxClients) ]
+         ++ optionals (cfg.indexFile != null) [ "--index" cfg.indexFile ]
+         ++ optionals cfg.enableIPv6 [ "--ipv6" ]
+         ++ optionals cfg.enableSSL [ "--ssl-cert" cfg.certFile
+                                      "--ssl-key" cfg.keyFile
+                                      "--ssl-ca" cfg.caFile ]
+         ++ [ "--debug" (toString cfg.logLevel) ];
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+    services.ttyd = {
+      enable = mkEnableOption "ttyd daemon";
+
+      port = mkOption {
+        type = types.port;
+        default = 7681;
+        description = "Port to listen on (use 0 for random port)";
+      };
+
+      socket = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/run/ttyd.sock";
+        description = "UNIX domain socket path to bind.";
+      };
+
+      interface = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "eth0";
+        description = "Network interface to bind.";
+      };
+
+      username = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Username for basic authentication.";
+      };
+
+      passwordFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        apply = value: if value == null then null else toString value;
+        description = ''
+          File containing the password to use for basic authentication.
+          For insecurely putting the password in the globally readable store use
+          <literal>pkgs.writeText "ttydpw" "MyPassword"</literal>.
+        '';
+      };
+
+      signal = mkOption {
+        type = types.ints.u8;
+        default = 1;
+        description = "Signal to send to the command on session close.";
+      };
+
+      clientOptions = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        example = literalExpression ''{
+          fontSize = "16";
+          fontFamily = "Fira Code";
+
+        }'';
+        description = ''
+          Attribute set of client options for xtermjs.
+          <link xlink:href="https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/"/>
+        '';
+      };
+
+      terminalType = mkOption {
+        type = types.str;
+        default = "xterm-256color";
+        description = "Terminal type to report.";
+      };
+
+      checkOrigin = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to allow a websocket connection from a different origin.";
+      };
+
+      maxClients = mkOption {
+        type = types.int;
+        default = 0;
+        description = "Maximum clients to support (0, no limit)";
+      };
+
+      indexFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = "Custom index.html path";
+      };
+
+      enableIPv6 = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether or not to enable IPv6 support.";
+      };
+
+      enableSSL = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether or not to enable SSL (https) support.";
+      };
+
+      certFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = "SSL certificate file path.";
+      };
+
+      keyFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        apply = value: if value == null then null else toString value;
+        description = ''
+          SSL key file path.
+          For insecurely putting the keyFile in the globally readable store use
+          <literal>pkgs.writeText "ttydKeyFile" "SSLKEY"</literal>.
+        '';
+      };
+
+      caFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = "SSL CA file path for client certificate verification.";
+      };
+
+      logLevel = mkOption {
+        type = types.int;
+        default = 7;
+        description = "Set log level.";
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions =
+      [ { assertion = cfg.enableSSL
+            -> cfg.certFile != null && cfg.keyFile != null && cfg.caFile != null;
+          message = "SSL is enabled for ttyd, but no certFile, keyFile or caFile has been specefied."; }
+        { assertion = ! (cfg.interface != null && cfg.socket != null);
+          message = "Cannot set both interface and socket for ttyd."; }
+        { assertion = (cfg.username != null) == (cfg.passwordFile != null);
+          message = "Need to set both username and passwordFile for ttyd"; }
+      ];
+
+    systemd.services.ttyd = {
+      description = "ttyd Web Server Daemon";
+
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        # Runs login which needs to be run as root
+        # login: Cannot possibly work without effective root
+        User = "root";
+      };
+
+      script = if cfg.passwordFile != null then ''
+        PASSWORD=$(cat ${escapeShellArg cfg.passwordFile})
+        ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \
+          --credential ${escapeShellArg cfg.username}:"$PASSWORD" \
+          ${pkgs.shadow}/bin/login
+      ''
+      else ''
+        ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \
+          ${pkgs.shadow}/bin/login
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/web-servers/unit/default.nix b/nixos/modules/services/web-servers/unit/default.nix
new file mode 100644
index 00000000000..b2eecdbb53e
--- /dev/null
+++ b/nixos/modules/services/web-servers/unit/default.nix
@@ -0,0 +1,155 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.unit;
+
+  configFile = pkgs.writeText "unit.json" cfg.config;
+
+in {
+  options = {
+    services.unit = {
+      enable = mkEnableOption "Unit App Server";
+      package = mkOption {
+        type = types.package;
+        default = pkgs.unit;
+        defaultText = literalExpression "pkgs.unit";
+        description = "Unit package to use.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = "unit";
+        description = "User account under which unit runs.";
+      };
+      group = mkOption {
+        type = types.str;
+        default = "unit";
+        description = "Group account under which unit runs.";
+      };
+      stateDir = mkOption {
+        type = types.path;
+        default = "/var/spool/unit";
+        description = "Unit data directory.";
+      };
+      logDir = mkOption {
+        type = types.path;
+        default = "/var/log/unit";
+        description = "Unit log directory.";
+      };
+      config = mkOption {
+        type = types.str;
+        default = ''
+          {
+            "listeners": {},
+            "applications": {}
+          }
+        '';
+        example = ''
+          {
+            "listeners": {
+              "*:8300": {
+                "application": "example-php-72"
+              }
+            },
+            "applications": {
+              "example-php-72": {
+                "type": "php 7.2",
+                "processes": 4,
+                "user": "nginx",
+                "group": "nginx",
+                "root": "/var/www",
+                "index": "index.php",
+                "options": {
+                  "file": "/etc/php.d/default.ini",
+                  "admin": {
+                    "max_execution_time": "30",
+                    "max_input_time": "30",
+                    "display_errors": "off",
+                    "display_startup_errors": "off",
+                    "open_basedir": "/dev/urandom:/proc/cpuinfo:/proc/meminfo:/etc/ssl/certs:/var/www",
+                    "disable_functions": "exec,passthru,shell_exec,system"
+                  }
+                }
+              }
+            }
+          }
+        '';
+        description = "Unit configuration in JSON format. More details here https://unit.nginx.org/configuration";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.logDir}' 0750 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.unit = {
+      description = "Unit App Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        [ ! -e '${cfg.stateDir}/conf.json' ] || rm -f '${cfg.stateDir}/conf.json'
+      '';
+      postStart = ''
+        ${pkgs.curl}/bin/curl -X PUT --data-binary '@${configFile}' --unix-socket '/run/unit/control.unit.sock' 'http://localhost/config'
+      '';
+      serviceConfig = {
+        Type = "forking";
+        PIDFile = "/run/unit/unit.pid";
+        ExecStart = ''
+          ${cfg.package}/bin/unitd --control 'unix:/run/unit/control.unit.sock' --pid '/run/unit/unit.pid' \
+                                   --log '${cfg.logDir}/unit.log' --state '${cfg.stateDir}' --tmp '/tmp' \
+                                   --user ${cfg.user} --group ${cfg.group}
+        '';
+        ExecStop = ''
+          ${pkgs.curl}/bin/curl -X DELETE --unix-socket '/run/unit/control.unit.sock' 'http://localhost/config'
+        '';
+        # Runtime directory and mode
+        RuntimeDirectory = "unit";
+        RuntimeDirectoryMode = "0750";
+        # Access write directories
+        ReadWritePaths = [ cfg.stateDir cfg.logDir ];
+        # Security
+        NoNewPrivileges = true;
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = false;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        # System Call Filtering
+        SystemCallArchitectures = "native";
+      };
+    };
+
+    users.users = optionalAttrs (cfg.user == "unit") {
+      unit = {
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "unit") {
+      unit = { };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/web-servers/uwsgi.nix b/nixos/modules/services/web-servers/uwsgi.nix
new file mode 100644
index 00000000000..1b3474f2f52
--- /dev/null
+++ b/nixos/modules/services/web-servers/uwsgi.nix
@@ -0,0 +1,229 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.uwsgi;
+
+  isEmperor = cfg.instance.type == "emperor";
+
+  imperialPowers =
+    [
+      # spawn other user processes
+      "CAP_SETUID" "CAP_SETGID"
+      "CAP_SYS_CHROOT"
+      # transfer capabilities
+      "CAP_SETPCAP"
+      # create other user sockets
+      "CAP_CHOWN"
+    ];
+
+  buildCfg = name: c:
+    let
+      plugins' =
+        if any (n: !any (m: m == n) cfg.plugins) (c.plugins or [])
+        then throw "`plugins` attribute in uWSGI configuration contains plugins not in config.services.uwsgi.plugins"
+        else c.plugins or cfg.plugins;
+      plugins = unique plugins';
+
+      hasPython = v: filter (n: n == "python${v}") plugins != [];
+      hasPython2 = hasPython "2";
+      hasPython3 = hasPython "3";
+
+      python =
+        if hasPython2 && hasPython3 then
+          throw "`plugins` attribute in uWSGI configuration shouldn't contain both python2 and python3"
+        else if hasPython2 then cfg.package.python2
+        else if hasPython3 then cfg.package.python3
+        else null;
+
+      pythonEnv = python.withPackages (c.pythonPackages or (self: []));
+
+      uwsgiCfg = {
+        uwsgi =
+          if c.type == "normal"
+            then {
+              inherit plugins;
+            } // removeAttrs c [ "type" "pythonPackages" ]
+              // optionalAttrs (python != null) {
+                pyhome = "${pythonEnv}";
+                env =
+                  # Argh, uwsgi expects list of key-values there instead of a dictionary.
+                  let envs = partition (hasPrefix "PATH=") (c.env or []);
+                      oldPaths = map (x: substring (stringLength "PATH=") (stringLength x) x) envs.right;
+                      paths = oldPaths ++ [ "${pythonEnv}/bin" ];
+                  in [ "PATH=${concatStringsSep ":" paths}" ] ++ envs.wrong;
+              }
+          else if isEmperor
+            then {
+              emperor = if builtins.typeOf c.vassals != "set" then c.vassals
+                        else pkgs.buildEnv {
+                          name = "vassals";
+                          paths = mapAttrsToList buildCfg c.vassals;
+                        };
+            } // removeAttrs c [ "type" "vassals" ]
+          else throw "`type` attribute in uWSGI configuration should be either 'normal' or 'emperor'";
+      };
+
+    in pkgs.writeTextDir "${name}.json" (builtins.toJSON uwsgiCfg);
+
+in {
+
+  options = {
+    services.uwsgi = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable uWSGI";
+      };
+
+      runDir = mkOption {
+        type = types.path;
+        default = "/run/uwsgi";
+        description = "Where uWSGI communication sockets can live";
+      };
+
+      package = mkOption {
+        type = types.package;
+        internal = true;
+      };
+
+      instance = mkOption {
+        type =  with types; let
+          valueType = nullOr (oneOf [
+            bool
+            int
+            float
+            str
+            (lazyAttrsOf valueType)
+            (listOf valueType)
+            (mkOptionType {
+              name = "function";
+              description = "function";
+              check = x: isFunction x;
+              merge = mergeOneOption;
+            })
+          ]) // {
+            description = "Json value or lambda";
+            emptyValue.value = {};
+          };
+        in valueType;
+        default = {
+          type = "normal";
+        };
+        example = literalExpression ''
+          {
+            type = "emperor";
+            vassals = {
+              moin = {
+                type = "normal";
+                pythonPackages = self: with self; [ moinmoin ];
+                socket = "''${config.services.uwsgi.runDir}/uwsgi.sock";
+              };
+            };
+          }
+        '';
+        description = ''
+          uWSGI configuration. It awaits an attribute <literal>type</literal> inside which can be either
+          <literal>normal</literal> or <literal>emperor</literal>.
+
+          For <literal>normal</literal> mode you can specify <literal>pythonPackages</literal> as a function
+          from libraries set into a list of libraries. <literal>pythonpath</literal> will be set accordingly.
+
+          For <literal>emperor</literal> mode, you should use <literal>vassals</literal> attribute
+          which should be either a set of names and configurations or a path to a directory.
+
+          Other attributes will be used in configuration file as-is. Notice that you can redefine
+          <literal>plugins</literal> setting here.
+        '';
+      };
+
+      plugins = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Plugins used with uWSGI";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "uwsgi";
+        description = "User account under which uWSGI runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "uwsgi";
+        description = "Group account under which uWSGI runs.";
+      };
+
+      capabilities = mkOption {
+        type = types.listOf types.str;
+        apply = caps: caps ++ optionals isEmperor imperialPowers;
+        default = [ ];
+        example = literalExpression ''
+          [
+            "CAP_NET_BIND_SERVICE" # bind on ports <1024
+            "CAP_NET_RAW"          # open raw sockets
+          ]
+        '';
+        description = ''
+          Grant capabilities to the uWSGI instance. See the
+          <literal>capabilities(7)</literal> for available values.
+          <note>
+            <para>
+              uWSGI runs as an unprivileged user (even as Emperor) with the minimal
+              capabilities required. This option can be used to add fine-grained
+              permissions without running the service as root.
+            </para>
+            <para>
+              When in Emperor mode, any capability to be inherited by a vassal must
+              be specified again in the vassal configuration using <literal>cap</literal>.
+              See the uWSGI <link
+              xlink:href="https://uwsgi-docs.readthedocs.io/en/latest/Capabilities.html">docs</link>
+              for more information.
+            </para>
+          </note>
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = optional (cfg.runDir != "/run/uwsgi") ''
+      d ${cfg.runDir} 775 ${cfg.user} ${cfg.group}
+    '';
+
+    systemd.services.uwsgi = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        Type = "notify";
+        ExecStart = "${cfg.package}/bin/uwsgi --json ${buildCfg "server" cfg.instance}/server.json";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
+        NotifyAccess = "main";
+        KillSignal = "SIGQUIT";
+        AmbientCapabilities = cfg.capabilities;
+        CapabilityBoundingSet = cfg.capabilities;
+        RuntimeDirectory = mkIf (cfg.runDir == "/run/uwsgi") "uwsgi";
+      };
+    };
+
+    users.users = optionalAttrs (cfg.user == "uwsgi") {
+      uwsgi = {
+        group = cfg.group;
+        uid = config.ids.uids.uwsgi;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "uwsgi") {
+      uwsgi.gid = config.ids.gids.uwsgi;
+    };
+
+    services.uwsgi.package = pkgs.uwsgi.override {
+      plugins = unique cfg.plugins;
+    };
+  };
+}
diff --git a/nixos/modules/services/web-servers/varnish/default.nix b/nixos/modules/services/web-servers/varnish/default.nix
new file mode 100644
index 00000000000..fe817313a99
--- /dev/null
+++ b/nixos/modules/services/web-servers/varnish/default.nix
@@ -0,0 +1,115 @@
+{ config, lib, pkgs, ...}:
+
+with lib;
+
+let
+  cfg = config.services.varnish;
+
+  commandLine = "-f ${pkgs.writeText "default.vcl" cfg.config}" +
+      optionalString (cfg.extraModules != []) " -p vmod_path='${makeSearchPathOutput "lib" "lib/varnish/vmods" ([cfg.package] ++ cfg.extraModules)}' -r vmod_path";
+in
+{
+  options = {
+    services.varnish = {
+      enable = mkEnableOption "Varnish Server";
+
+      enableConfigCheck = mkEnableOption "checking the config during build time" // { default = true; };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.varnish;
+        defaultText = literalExpression "pkgs.varnish";
+        description = ''
+          The package to use
+        '';
+      };
+
+      http_address = mkOption {
+        type = types.str;
+        default = "*:6081";
+        description = "
+          HTTP listen address and port.
+        ";
+      };
+
+      config = mkOption {
+        type = types.lines;
+        description = "
+          Verbatim default.vcl configuration.
+        ";
+      };
+
+      stateDir = mkOption {
+        type = types.path;
+        default = "/var/spool/varnish/${config.networking.hostName}";
+        defaultText = literalExpression ''"/var/spool/varnish/''${config.networking.hostName}"'';
+        description = "
+          Directory holding all state for Varnish to run.
+        ";
+      };
+
+      extraModules = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "[ pkgs.varnishPackages.geoip ]";
+        description = "
+          Varnish modules (except 'std').
+        ";
+      };
+
+      extraCommandLine = mkOption {
+        type = types.str;
+        default = "";
+        example = "-s malloc,256M";
+        description = "
+          Command line switches for varnishd (run 'varnishd -?' to get list of options)
+        ";
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.varnish = {
+      description = "Varnish";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      preStart = ''
+        mkdir -p ${cfg.stateDir}
+        chown -R varnish:varnish ${cfg.stateDir}
+      '';
+      postStop = ''
+        rm -rf ${cfg.stateDir}
+      '';
+      serviceConfig = {
+        Type = "simple";
+        PermissionsStartOnly = true;
+        ExecStart = "${cfg.package}/sbin/varnishd -a ${cfg.http_address} -n ${cfg.stateDir} -F ${cfg.extraCommandLine} ${commandLine}";
+        Restart = "always";
+        RestartSec = "5s";
+        User = "varnish";
+        Group = "varnish";
+        AmbientCapabilities = "cap_net_bind_service";
+        NoNewPrivileges = true;
+        LimitNOFILE = 131072;
+      };
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    # check .vcl syntax at compile time (e.g. before nixops deployment)
+    system.extraDependencies = mkIf cfg.enableConfigCheck [
+      (pkgs.runCommand "check-varnish-syntax" {} ''
+        ${cfg.package}/bin/varnishd -C ${commandLine} 2> $out || (cat $out; exit 1)
+      '')
+    ];
+
+    users.users.varnish = {
+      group = "varnish";
+      uid = config.ids.uids.varnish;
+    };
+
+    users.groups.varnish.gid = config.ids.uids.varnish;
+  };
+}
diff --git a/nixos/modules/services/web-servers/zope2.nix b/nixos/modules/services/web-servers/zope2.nix
new file mode 100644
index 00000000000..92210916022
--- /dev/null
+++ b/nixos/modules/services/web-servers/zope2.nix
@@ -0,0 +1,262 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.zope2;
+
+  zope2Opts = { name, ... }: {
+    options = {
+
+      name = mkOption {
+        default = "${name}";
+        type = types.str;
+        description = "The name of the zope2 instance. If undefined, the name of the attribute set will be used.";
+      };
+
+      threads = mkOption {
+        default = 2;
+        type = types.int;
+        description = "Specify the number of threads that Zope's ZServer web server will use to service requests. ";
+      };
+
+      http_address = mkOption {
+        default = "localhost:8080";
+        type = types.str;
+        description = "Give a port and address for the HTTP server.";
+      };
+
+      user = mkOption {
+        default = "zope2";
+        type = types.str;
+        description = "The name of the effective user for the Zope process.";
+      };
+
+      clientHome = mkOption {
+        default = "/var/lib/zope2/${name}";
+        type = types.path;
+        description = "Home directory of zope2 instance.";
+      };
+      extra = mkOption {
+        default =
+          ''
+          <zodb_db main>
+            mount-point /
+            cache-size 30000
+            <blobstorage>
+                blob-dir /var/lib/zope2/${name}/blobstorage
+                <filestorage>
+                path /var/lib/zope2/${name}/filestorage/Data.fs
+                </filestorage>
+            </blobstorage>
+          </zodb_db>
+          '';
+        type = types.lines;
+        description = "Extra zope.conf";
+      };
+
+      packages = mkOption {
+        type = types.listOf types.package;
+        description = "The list of packages you want to make available to the zope2 instance.";
+      };
+
+    };
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.zope2.instances = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule zope2Opts);
+      example = literalExpression ''
+        {
+          plone01 = {
+            http_address = "127.0.0.1:8080";
+            extra =
+              '''
+              <zodb_db main>
+                mount-point /
+                cache-size 30000
+                <blobstorage>
+                    blob-dir /var/lib/zope2/plone01/blobstorage
+                    <filestorage>
+                    path /var/lib/zope2/plone01/filestorage/Data.fs
+                    </filestorage>
+                </blobstorage>
+              </zodb_db>
+              ''';
+          };
+        }
+      '';
+      description = "zope2 instances to be created automaticaly by the system.";
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf (cfg.instances != {}) {
+
+    users.users.zope2 = {
+      isSystemUser = true;
+      group = "zope2";
+    };
+    users.groups.zope2 = {};
+
+    systemd.services =
+      let
+
+        createZope2Instance = opts: name:
+          let
+            interpreter = pkgs.writeScript "interpreter"
+              ''
+              import sys
+
+              _interactive = True
+              if len(sys.argv) > 1:
+                  _options, _args = __import__("getopt").getopt(sys.argv[1:], 'ic:m:')
+                  _interactive = False
+                  for (_opt, _val) in _options:
+                      if _opt == '-i':
+                          _interactive = True
+                      elif _opt == '-c':
+                          exec _val
+                      elif _opt == '-m':
+                          sys.argv[1:] = _args
+                          _args = []
+                          __import__("runpy").run_module(
+                              _val, {}, "__main__", alter_sys=True)
+
+                  if _args:
+                      sys.argv[:] = _args
+                      __file__ = _args[0]
+                      del _options, _args
+                      execfile(__file__)
+
+              if _interactive:
+                  del _interactive
+                  __import__("code").interact(banner="", local=globals())
+              '';
+            env = pkgs.buildEnv {
+              name = "zope2-${name}-env";
+              paths = [
+                pkgs.python27
+                pkgs.python27Packages.recursivePthLoader
+                pkgs.python27Packages."plone.recipe.zope2instance"
+              ] ++ attrValues pkgs.python27.modules
+                ++ opts.packages;
+              postBuild =
+                ''
+                echo "#!$out/bin/python" > $out/bin/interpreter
+                cat ${interpreter} >> $out/bin/interpreter
+                '';
+            };
+            conf = pkgs.writeText "zope2-${name}-conf"
+              ''
+              %define INSTANCEHOME ${env}
+              instancehome $INSTANCEHOME
+              %define CLIENTHOME ${opts.clientHome}/${opts.name}
+              clienthome $CLIENTHOME
+
+              debug-mode off
+              security-policy-implementation C
+              verbose-security off
+              default-zpublisher-encoding utf-8
+              zserver-threads ${toString opts.threads}
+              effective-user ${opts.user}
+
+              pid-filename ${opts.clientHome}/${opts.name}/pid
+              lock-filename ${opts.clientHome}/${opts.name}/lock
+              python-check-interval 1000
+              enable-product-installation off
+
+              <environment>
+                zope_i18n_compile_mo_files false
+              </environment>
+
+              <eventlog>
+              level INFO
+              <logfile>
+                  path /var/log/zope2/${name}.log
+                  level INFO
+              </logfile>
+              </eventlog>
+
+              <logger access>
+              level WARN
+              <logfile>
+                  path /var/log/zope2/${name}-Z2.log
+                  format %(message)s
+              </logfile>
+              </logger>
+
+              <http-server>
+              address ${opts.http_address}
+              </http-server>
+
+              <zodb_db temporary>
+              <temporarystorage>
+                  name temporary storage for sessioning
+              </temporarystorage>
+              mount-point /temp_folder
+              container-class Products.TemporaryFolder.TemporaryContainer
+              </zodb_db>
+
+              ${opts.extra}
+              '';
+            ctlScript = pkgs.writeScript "zope2-${name}-ctl-script"
+              ''
+              #!${env}/bin/python
+
+              import sys
+              import plone.recipe.zope2instance.ctl
+
+              if __name__ == '__main__':
+                  sys.exit(plone.recipe.zope2instance.ctl.main(
+                      ["-C", "${conf}"]
+                      + sys.argv[1:]))
+              '';
+
+            ctl = pkgs.writeScript "zope2-${name}-ctl"
+              ''
+              #!${pkgs.bash}/bin/bash -e
+              export PYTHONHOME=${env}
+              exec ${ctlScript} "$@"
+              '';
+          in {
+            #description = "${name} instance";
+            after = [ "network.target" ];  # with RelStorage also add "postgresql.service"
+            wantedBy = [ "multi-user.target" ];
+            path = opts.packages;
+            preStart =
+              ''
+              mkdir -p /var/log/zope2/
+              touch /var/log/zope2/${name}.log
+              touch /var/log/zope2/${name}-Z2.log
+              chown ${opts.user} /var/log/zope2/${name}.log
+              chown ${opts.user} /var/log/zope2/${name}-Z2.log
+
+              mkdir -p ${opts.clientHome}/filestorage ${opts.clientHome}/blobstorage
+              mkdir -p ${opts.clientHome}/${opts.name}
+              chown ${opts.user} ${opts.clientHome} -R
+
+              ${ctl} adduser admin admin
+              '';
+
+            serviceConfig.Type = "forking";
+            serviceConfig.ExecStart = "${ctl} start";
+            serviceConfig.ExecStop = "${ctl} stop";
+            serviceConfig.ExecReload = "${ctl} restart";
+          };
+
+      in listToAttrs (map (name: { name = "zope2-${name}"; value = createZope2Instance (builtins.getAttr name cfg.instances) name; }) (builtins.attrNames cfg.instances));
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/clight.nix b/nixos/modules/services/x11/clight.nix
new file mode 100644
index 00000000000..d994a658cba
--- /dev/null
+++ b/nixos/modules/services/x11/clight.nix
@@ -0,0 +1,131 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.clight;
+
+  toConf = v:
+    if builtins.isFloat v then toString v
+    else if isInt v       then toString v
+    else if isBool v      then boolToString v
+    else if isString v    then ''"${escape [''"''] v}"''
+    else if isList v      then "[ " + concatMapStringsSep ", " toConf v + " ]"
+    else if isAttrs v     then "\n{\n" + convertAttrs v + "\n}"
+    else abort "clight.toConf: unexpected type (v = ${v})";
+
+  getSep = v:
+    if isAttrs v then ":"
+    else "=";
+
+  convertAttrs = attrs: concatStringsSep "\n" (mapAttrsToList
+    (name: value: "${toString name} ${getSep value} ${toConf value};")
+    attrs);
+
+  clightConf = pkgs.writeText "clight.conf" (convertAttrs
+    (filterAttrs
+      (_: value: value != null)
+      cfg.settings));
+in {
+  options.services.clight = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable clight or not.
+      '';
+    };
+
+    temperature = {
+      day = mkOption {
+        type = types.int;
+        default = 5500;
+        description = ''
+          Colour temperature to use during the day, between
+          <literal>1000</literal> and <literal>25000</literal> K.
+        '';
+      };
+      night = mkOption {
+        type = types.int;
+        default = 3700;
+        description = ''
+          Colour temperature to use at night, between
+          <literal>1000</literal> and <literal>25000</literal> K.
+        '';
+      };
+    };
+
+    settings = let
+      validConfigTypes = with types; oneOf [ int str bool float ];
+      collectionTypes = with types; oneOf [ validConfigTypes (listOf validConfigTypes) ];
+    in mkOption {
+      type = with types; attrsOf (nullOr (either collectionTypes (attrsOf collectionTypes)));
+      default = {};
+      example = { captures = 20; gamma_long_transition = true; ac_capture_timeouts = [ 120 300 60 ]; };
+      description = ''
+        Additional configuration to extend clight.conf. See
+        <link xlink:href="https://github.com/FedeDP/Clight/blob/master/Extra/clight.conf"/> for a
+        sample configuration file.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = let
+      inRange = v: l: r: v >= l && v <= r;
+    in [
+      { assertion = config.location.provider == "manual" ->
+          inRange config.location.latitude (-90) 90 && inRange config.location.longitude (-180) 180;
+        message = "You must specify a valid latitude and longitude if manually providing location"; }
+    ];
+
+    boot.kernelModules = [ "i2c_dev" ];
+    environment.systemPackages = with pkgs; [ clight clightd ];
+    services.dbus.packages = with pkgs; [ clight clightd ];
+    services.upower.enable = true;
+
+    services.clight.settings = {
+      gamma.temp = with cfg.temperature; mkDefault [ day night ];
+    } // (optionalAttrs (config.location.provider == "manual") {
+      daytime.latitude = mkDefault config.location.latitude;
+      daytime.longitude = mkDefault config.location.longitude;
+    });
+
+    services.geoclue2.appConfig.clightc = {
+      isAllowed = true;
+      isSystem = true;
+    };
+
+    systemd.services.clightd = {
+      requires = [ "polkit.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      description = "Bus service to manage various screen related properties (gamma, dpms, backlight)";
+      serviceConfig = {
+        Type = "dbus";
+        BusName = "org.clightd.clightd";
+        Restart = "on-failure";
+        RestartSec = 5;
+        ExecStart = ''
+          ${pkgs.clightd}/bin/clightd
+        '';
+      };
+    };
+
+    systemd.user.services.clight = {
+      after = [ "upower.service" "clightd.service" ];
+      wants = [ "upower.service" "clightd.service" ];
+      partOf = [ "graphical-session.target" ];
+      wantedBy = [ "graphical-session.target" ];
+
+      description = "C daemon to adjust screen brightness to match ambient brightness, as computed capturing frames from webcam";
+      serviceConfig = {
+        Restart = "on-failure";
+        RestartSec = 5;
+        ExecStart = ''
+          ${pkgs.clight}/bin/clight --conf-file ${clightConf}
+        '';
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/x11/colord.nix b/nixos/modules/services/x11/colord.nix
new file mode 100644
index 00000000000..31ccee6aa33
--- /dev/null
+++ b/nixos/modules/services/x11/colord.nix
@@ -0,0 +1,41 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.colord;
+
+in {
+
+  options = {
+
+    services.colord = {
+      enable = mkEnableOption "colord, the color management daemon";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.colord ];
+
+    services.dbus.packages = [ pkgs.colord ];
+
+    services.udev.packages = [ pkgs.colord ];
+
+    systemd.packages = [ pkgs.colord ];
+
+    systemd.tmpfiles.packages = [ pkgs.colord ];
+
+    users.users.colord = {
+      isSystemUser = true;
+      home = "/var/lib/colord";
+      group = "colord";
+    };
+
+    users.groups.colord = {};
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/desktop-managers/cde.nix b/nixos/modules/services/x11/desktop-managers/cde.nix
new file mode 100644
index 00000000000..6c7105729cf
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/cde.nix
@@ -0,0 +1,73 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  xcfg = config.services.xserver;
+  cfg = xcfg.desktopManager.cde;
+in {
+  options.services.xserver.desktopManager.cde = {
+    enable = mkEnableOption "Common Desktop Environment";
+
+    extraPackages = mkOption {
+      type = with types; listOf package;
+      default = with pkgs.xorg; [
+        xclock bitmap xlsfonts xfd xrefresh xload xwininfo xdpyinfo xwd xwud
+      ];
+      defaultText = literalExpression ''
+        with pkgs.xorg; [
+          xclock bitmap xlsfonts xfd xrefresh xload xwininfo xdpyinfo xwd xwud
+        ]
+      '';
+      description = ''
+        Extra packages to be installed system wide.
+      '';
+    };
+  };
+
+  config = mkIf (xcfg.enable && cfg.enable) {
+    environment.systemPackages = cfg.extraPackages;
+
+    services.rpcbind.enable = true;
+
+    services.xinetd.enable = true;
+    services.xinetd.services = [
+      {
+        name = "cmsd";
+        protocol = "udp";
+        user = "root";
+        server = "${pkgs.cdesktopenv}/opt/dt/bin/rpc.cmsd";
+        extraConfig = ''
+          type  = RPC UNLISTED
+          rpc_number  = 100068
+          rpc_version = 2-5
+          only_from   = 127.0.0.1/0
+        '';
+      }
+    ];
+
+    users.groups.mail = {};
+    security.wrappers = {
+      dtmail = {
+        setgid = true;
+        owner = "root";
+        group = "mail";
+        source = "${pkgs.cdesktopenv}/bin/dtmail";
+      };
+    };
+
+    system.activationScripts.setup-cde = ''
+      mkdir -p /var/dt/{tmp,appconfig/appmanager}
+      chmod a+w+t /var/dt/{tmp,appconfig/appmanager}
+    '';
+
+    services.xserver.desktopManager.session = [
+    { name = "CDE";
+      start = ''
+        exec ${pkgs.cdesktopenv}/opt/dt/bin/Xsession
+      '';
+    }];
+  };
+
+  meta.maintainers = [ ];
+}
diff --git a/nixos/modules/services/x11/desktop-managers/cinnamon.nix b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
new file mode 100644
index 00000000000..3a78a526460
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
@@ -0,0 +1,218 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.desktopManager.cinnamon;
+  serviceCfg = config.services.cinnamon;
+
+  nixos-gsettings-overrides = pkgs.cinnamon.cinnamon-gsettings-overrides.override {
+    extraGSettingsOverridePackages = cfg.extraGSettingsOverridePackages;
+    extraGSettingsOverrides = cfg.extraGSettingsOverrides;
+  };
+
+in
+
+{
+  options = {
+    services.cinnamon = {
+      apps.enable = mkEnableOption "Cinnamon default applications";
+    };
+
+    services.xserver.desktopManager.cinnamon = {
+      enable = mkEnableOption "the cinnamon desktop manager";
+
+      sessionPath = mkOption {
+        default = [];
+        type = types.listOf types.package;
+        example = literalExpression "[ pkgs.gnome.gpaste ]";
+        description = ''
+          Additional list of packages to be added to the session search path.
+          Useful for GSettings-conditional autostart.
+
+          Note that this should be a last resort; patching the package is preferred (see GPaste).
+        '';
+      };
+
+      extraGSettingsOverrides = mkOption {
+        default = "";
+        type = types.lines;
+        description = "Additional gsettings overrides.";
+      };
+
+      extraGSettingsOverridePackages = mkOption {
+        default = [];
+        type = types.listOf types.path;
+        description = "List of packages for which gsettings are overridden.";
+      };
+    };
+
+    environment.cinnamon.excludePackages = mkOption {
+      default = [];
+      example = literalExpression "[ pkgs.cinnamon.blueberry ]";
+      type = types.listOf types.package;
+      description = "Which packages cinnamon should exclude from the default environment";
+    };
+
+  };
+
+  config = mkMerge [
+    (mkIf (cfg.enable && config.services.xserver.displayManager.lightdm.enable && config.services.xserver.displayManager.lightdm.greeters.gtk.enable) {
+      services.xserver.displayManager.lightdm.greeters.gtk.extraConfig = mkDefault (builtins.readFile "${pkgs.cinnamon.mint-artwork}/etc/lightdm/lightdm-gtk-greeter.conf.d/99_linuxmint.conf");
+      })
+
+    (mkIf cfg.enable {
+      services.xserver.displayManager.sessionPackages = [ pkgs.cinnamon.cinnamon-common ];
+
+      services.xserver.displayManager.sessionCommands = ''
+        if test "$XDG_CURRENT_DESKTOP" = "Cinnamon"; then
+            true
+            ${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}
+        fi
+      '';
+
+      # Default services
+      hardware.bluetooth.enable = mkDefault true;
+      hardware.pulseaudio.enable = mkDefault true;
+      security.polkit.enable = true;
+      services.accounts-daemon.enable = true;
+      services.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true));
+      services.dbus.packages = with pkgs.cinnamon; [
+        cinnamon-common
+        cinnamon-screensaver
+        nemo
+        xapps
+      ];
+      services.cinnamon.apps.enable = mkDefault true;
+      services.gnome.glib-networking.enable = true;
+      services.gnome.gnome-keyring.enable = true;
+      services.gvfs.enable = true;
+      services.udisks2.enable = true;
+      services.upower.enable = mkDefault config.powerManagement.enable;
+      services.xserver.libinput.enable = mkDefault true;
+      services.xserver.updateDbusEnvironment = true;
+      networking.networkmanager.enable = mkDefault true;
+
+      # Enable colord server
+      services.colord.enable = true;
+
+      # Enable dconf
+      programs.dconf.enable = true;
+
+      # Enable org.a11y.Bus
+      services.gnome.at-spi2-core.enable = true;
+
+      # Fix lockscreen
+      security.pam.services = {
+        cinnamon-screensaver = {};
+      };
+
+      environment.systemPackages = with pkgs.cinnamon // pkgs; [
+        desktop-file-utils
+        nixos-artwork.wallpapers.simple-dark-gray
+        onboard
+        sound-theme-freedesktop
+
+        # common-files
+        cinnamon-common
+        cinnamon-session
+        cinnamon-desktop
+        cinnamon-menus
+        cinnamon-translations
+
+        # utils needed by some scripts
+        killall
+
+        # session requirements
+        cinnamon-screensaver
+        # cinnamon-killer-daemon: provided by cinnamon-common
+        networkmanagerapplet # session requirement - also nm-applet not needed
+
+        # For a polkit authentication agent
+        polkit_gnome
+
+        # packages
+        nemo
+        cinnamon-control-center
+        cinnamon-settings-daemon
+        libgnomekbd
+        orca
+
+        # theme
+        gnome.adwaita-icon-theme
+        hicolor-icon-theme
+        gnome.gnome-themes-extra
+        gtk3.out
+        mint-artwork
+        mint-themes
+        mint-x-icons
+        mint-y-icons
+        vanilla-dmz
+
+        # other
+        glib # for gsettings
+        shared-mime-info # for update-mime-database
+        xdg-user-dirs
+      ];
+
+      # Override GSettings schemas
+      environment.sessionVariables.NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-overrides}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
+
+      environment.pathsToLink = [
+        # FIXME: modules should link subdirs of `/share` rather than relying on this
+        "/share" # TODO: https://github.com/NixOS/nixpkgs/issues/47173
+      ];
+
+      # Shell integration for VTE terminals
+      programs.bash.vteIntegration = mkDefault true;
+      programs.zsh.vteIntegration = mkDefault true;
+
+      # Harmonize Qt5 applications under Pantheon
+      qt5.enable = true;
+      qt5.platformTheme = "gnome";
+      qt5.style = "adwaita";
+
+      # Default Fonts
+      fonts.fonts = with pkgs; [
+        source-code-pro # Default monospace font in 3.32
+        ubuntu_font_family # required for default theme
+      ];
+    })
+
+    (mkIf serviceCfg.apps.enable {
+      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; pkgs.gnome.removePackagesByName [
+        # cinnamon team apps
+        bulky
+        blueberry
+        warpinator
+
+        # cinnamon xapps
+        xviewer
+        xreader
+        xed
+        xplayer
+        pix
+
+        # external apps shipped with linux-mint
+        hexchat
+        gnome-calculator
+      ] config.environment.cinnamon.excludePackages);
+    })
+  ];
+}
diff --git a/nixos/modules/services/x11/desktop-managers/default.nix b/nixos/modules/services/x11/desktop-managers/default.nix
new file mode 100644
index 00000000000..8247a7e381c
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/default.nix
@@ -0,0 +1,99 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  xcfg = config.services.xserver;
+  cfg = xcfg.desktopManager;
+
+  # If desktop manager `d' isn't capable of setting a background and
+  # the xserver is enabled, `feh' or `xsetroot' are used as a fallback.
+  needBGCond = d: ! (d ? bgSupport && d.bgSupport) && xcfg.enable;
+
+in
+
+{
+  # Note: the order in which desktop manager modules are imported here
+  # determines the default: later modules (if enabled) are preferred.
+  # E.g., if Plasma 5 is enabled, it supersedes xterm.
+  imports = [
+    ./none.nix ./xterm.nix ./xfce.nix ./plasma5.nix ./lumina.nix
+    ./lxqt.nix ./enlightenment.nix ./gnome.nix ./retroarch.nix ./kodi.nix
+    ./mate.nix ./pantheon.nix ./surf-display.nix ./cde.nix
+    ./cinnamon.nix
+  ];
+
+  options = {
+
+    services.xserver.desktopManager = {
+
+      wallpaper = {
+        mode = mkOption {
+          type = types.enum [ "center" "fill" "max" "scale" "tile" ];
+          default = "scale";
+          example = "fill";
+          description = ''
+            The file <filename>~/.background-image</filename> is used as a background image.
+            This option specifies the placement of this image onto your desktop.
+
+            Possible values:
+            <literal>center</literal>: Center the image on the background. If it is too small, it will be surrounded by a black border.
+            <literal>fill</literal>: Like <literal>scale</literal>, but preserves aspect ratio by zooming the image until it fits. Either a horizontal or a vertical part of the image will be cut off.
+            <literal>max</literal>: Like <literal>fill</literal>, but scale the image to the maximum size that fits the screen with black borders on one side.
+            <literal>scale</literal>: Fit the file into the background without repeating it, cutting off stuff or using borders. But the aspect ratio is not preserved either.
+            <literal>tile</literal>: Tile (repeat) the image in case it is too small for the screen.
+          '';
+        };
+
+        combineScreens = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            When set to <literal>true</literal> the wallpaper will stretch across all screens.
+            When set to <literal>false</literal> the wallpaper is duplicated to all screens.
+          '';
+        };
+      };
+
+      session = mkOption {
+        internal = true;
+        default = [];
+        example = singleton
+          { name = "kde";
+            bgSupport = true;
+            start = "...";
+          };
+        description = ''
+          Internal option used to add some common line to desktop manager
+          scripts before forwarding the value to the
+          <varname>displayManager</varname>.
+        '';
+        apply = map (d: d // {
+          manage = "desktop";
+          start = d.start
+          + optionalString (needBGCond d) ''
+            if [ -e $HOME/.background-image ]; then
+              ${pkgs.feh}/bin/feh --bg-${cfg.wallpaper.mode} ${optionalString cfg.wallpaper.combineScreens "--no-xinerama"} $HOME/.background-image
+            fi
+          '';
+        });
+      };
+
+      default = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "none";
+        description = ''
+          <emphasis role="strong">Deprecated</emphasis>, please use <xref linkend="opt-services.xserver.displayManager.defaultSession"/> instead.
+
+          Default desktop manager loaded if none have been chosen.
+        '';
+      };
+
+    };
+
+  };
+
+  config.services.xserver.displayManager.session = cfg.session;
+}
diff --git a/nixos/modules/services/x11/desktop-managers/enlightenment.nix b/nixos/modules/services/x11/desktop-managers/enlightenment.nix
new file mode 100644
index 00000000000..d1513a596b9
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/enlightenment.nix
@@ -0,0 +1,119 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  e = pkgs.enlightenment;
+  xcfg = config.services.xserver;
+  cfg = xcfg.desktopManager.enlightenment;
+  GST_PLUGIN_PATH = lib.makeSearchPathOutput "lib" "lib/gstreamer-1.0" [
+    pkgs.gst_all_1.gst-plugins-base
+    pkgs.gst_all_1.gst-plugins-good
+    pkgs.gst_all_1.gst-plugins-bad
+    pkgs.gst_all_1.gst-libav ];
+
+in
+
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "xserver" "desktopManager" "e19" "enable" ] [ "services" "xserver" "desktopManager" "enlightenment" "enable" ])
+  ];
+
+  options = {
+
+    services.xserver.desktopManager.enlightenment.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable the Enlightenment desktop environment.";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = with pkgs; [
+      enlightenment.econnman
+      enlightenment.efl
+      enlightenment.enlightenment
+      enlightenment.ecrire
+      enlightenment.ephoto
+      enlightenment.rage
+      enlightenment.terminology
+      xorg.xcursorthemes
+    ];
+
+    environment.pathsToLink = [
+      "/etc/enlightenment"
+      "/share/enlightenment"
+      "/share/elementary"
+      "/share/locale"
+    ];
+
+    services.xserver.displayManager.sessionPackages = [ pkgs.enlightenment.enlightenment ];
+
+    services.xserver.displayManager.sessionCommands = ''
+      if test "$XDG_CURRENT_DESKTOP" = "Enlightenment"; then
+        export GST_PLUGIN_PATH="${GST_PLUGIN_PATH}"
+
+        # make available for D-BUS user services
+        #export XDG_DATA_DIRS=$XDG_DATA_DIRS''${XDG_DATA_DIRS:+:}:${config.system.path}/share:${e.efl}/share
+
+        # Update user dirs as described in http://freedesktop.org/wiki/Software/xdg-user-dirs/
+        ${pkgs.xdg-user-dirs}/bin/xdg-user-dirs-update
+      fi
+    '';
+
+    # Wrappers for programs installed by enlightenment that should be setuid
+    security.wrappers = {
+      enlightenment_ckpasswd =
+        { setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${pkgs.enlightenment.enlightenment}/lib/enlightenment/utils/enlightenment_ckpasswd";
+        };
+      enlightenment_sys =
+        { setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${pkgs.enlightenment.enlightenment}/lib/enlightenment/utils/enlightenment_sys";
+        };
+      enlightenment_system =
+        { setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${pkgs.enlightenment.enlightenment}/lib/enlightenment/utils/enlightenment_system";
+        };
+    };
+
+    environment.etc."X11/xkb".source = xcfg.xkbDir;
+
+    fonts.fonts = [ pkgs.dejavu_fonts pkgs.ubuntu_font_family ];
+
+    services.udisks2.enable = true;
+    services.upower.enable = config.powerManagement.enable;
+
+    services.dbus.packages = [ e.efl ];
+
+    systemd.user.services.efreet =
+      { enable = true;
+        description = "org.enlightenment.Efreet";
+        serviceConfig =
+          { ExecStart = "${e.efl}/bin/efreetd";
+            StandardOutput = "null";
+          };
+      };
+
+    systemd.user.services.ethumb =
+      { enable = true;
+        description = "org.enlightenment.Ethumb";
+        serviceConfig =
+          { ExecStart = "${e.efl}/bin/ethumbd";
+            StandardOutput = "null";
+          };
+      };
+
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/desktop-managers/gnome.nix b/nixos/modules/services/x11/desktop-managers/gnome.nix
new file mode 100644
index 00000000000..e2323785149
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/gnome.nix
@@ -0,0 +1,608 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.desktopManager.gnome;
+  serviceCfg = config.services.gnome;
+
+  # Prioritize nautilus by default when opening directories
+  mimeAppsList = pkgs.writeTextFile {
+    name = "gnome-mimeapps";
+    destination = "/share/applications/mimeapps.list";
+    text = ''
+      [Default Applications]
+      inode/directory=nautilus.desktop;org.gnome.Nautilus.desktop
+    '';
+  };
+
+  defaultFavoriteAppsOverride = ''
+    [org.gnome.shell]
+    favorite-apps=[ 'org.gnome.Epiphany.desktop', 'org.gnome.Geary.desktop', 'org.gnome.Calendar.desktop', 'org.gnome.Music.desktop', 'org.gnome.Photos.desktop', 'org.gnome.Nautilus.desktop' ]
+  '';
+
+  nixos-gsettings-desktop-schemas = let
+    defaultPackages = with pkgs; [ gsettings-desktop-schemas gnome.gnome-shell ];
+  in
+  pkgs.runCommand "nixos-gsettings-desktop-schemas" { preferLocalBuild = true; }
+    ''
+     mkdir -p $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas
+
+     ${concatMapStrings
+        (pkg: "cp -rf ${pkg}/share/gsettings-schemas/*/glib-2.0/schemas/*.xml $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas\n")
+        (defaultPackages ++ cfg.extraGSettingsOverridePackages)}
+
+     cp -f ${pkgs.gnome.gnome-shell}/share/gsettings-schemas/*/glib-2.0/schemas/*.gschema.override $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas
+
+     ${optionalString flashbackEnabled ''
+       cp -f ${pkgs.gnome.gnome-flashback}/share/gsettings-schemas/*/glib-2.0/schemas/*.gschema.override $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas
+     ''}
+
+     chmod -R a+w $out/share/gsettings-schemas/nixos-gsettings-overrides
+     cat - > $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas/nixos-defaults.gschema.override <<- EOF
+       [org.gnome.desktop.background]
+       picture-uri='file://${pkgs.nixos-artwork.wallpapers.simple-dark-gray.gnomeFilePath}'
+
+       [org.gnome.desktop.screensaver]
+       picture-uri='file://${pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom.gnomeFilePath}'
+
+       ${cfg.favoriteAppsOverride}
+
+       ${cfg.extraGSettingsOverrides}
+     EOF
+
+     ${pkgs.glib.dev}/bin/glib-compile-schemas $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas/
+    '';
+
+  flashbackEnabled = cfg.flashback.enableMetacity || length cfg.flashback.customSessions > 0;
+  flashbackWms = optional cfg.flashback.enableMetacity {
+    wmName = "metacity";
+    wmLabel = "Metacity";
+    wmCommand = "${pkgs.gnome.metacity}/bin/metacity";
+    enableGnomePanel = true;
+  } ++ cfg.flashback.customSessions;
+
+  notExcluded = pkg: mkDefault (!(lib.elem pkg config.environment.gnome.excludePackages));
+
+in
+
+{
+
+  meta = {
+    doc = ./gnome.xml;
+    maintainers = teams.gnome.members;
+  };
+
+  imports = [
+    # Added 2021-05-07
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "core-os-services" "enable" ]
+      [ "services" "gnome" "core-os-services" "enable" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "core-shell" "enable" ]
+      [ "services" "gnome" "core-shell" "enable" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "core-utilities" "enable" ]
+      [ "services" "gnome" "core-utilities" "enable" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "core-developer-tools" "enable" ]
+      [ "services" "gnome" "core-developer-tools" "enable" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "games" "enable" ]
+      [ "services" "gnome" "games" "enable" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "experimental-features" "realtime-scheduling" ]
+      [ "services" "gnome" "experimental-features" "realtime-scheduling" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "gnome3" "enable" ]
+      [ "services" "xserver" "desktopManager" "gnome" "enable" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "gnome3" "sessionPath" ]
+      [ "services" "xserver" "desktopManager" "gnome" "sessionPath" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "gnome3" "favoriteAppsOverride" ]
+      [ "services" "xserver" "desktopManager" "gnome" "favoriteAppsOverride" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "gnome3" "extraGSettingsOverrides" ]
+      [ "services" "xserver" "desktopManager" "gnome" "extraGSettingsOverrides" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "gnome3" "extraGSettingsOverridePackages" ]
+      [ "services" "xserver" "desktopManager" "gnome" "extraGSettingsOverridePackages" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "gnome3" "debug" ]
+      [ "services" "xserver" "desktopManager" "gnome" "debug" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "gnome3" "flashback" ]
+      [ "services" "xserver" "desktopManager" "gnome" "flashback" ]
+    )
+    (mkRenamedOptionModule
+      [ "environment" "gnome3" "excludePackages" ]
+      [ "environment" "gnome" "excludePackages" ]
+    )
+  ];
+
+  options = {
+
+    services.gnome = {
+      core-os-services.enable = mkEnableOption "essential services for GNOME3";
+      core-shell.enable = mkEnableOption "GNOME Shell services";
+      core-utilities.enable = mkEnableOption "GNOME core utilities";
+      core-developer-tools.enable = mkEnableOption "GNOME core developer tools";
+      games.enable = mkEnableOption "GNOME games";
+
+      experimental-features = {
+        realtime-scheduling = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Makes mutter (which propagates to gnome-shell) request a low priority real-time
+            scheduling which is only available on the wayland session.
+            To enable this experimental feature it requires a restart of the compositor.
+            Note that enabling this option only enables the <emphasis>capability</emphasis>
+            for realtime-scheduling to be used. It doesn't automatically set the gsetting
+            so that mutter actually uses realtime-scheduling. This would require adding <literal>
+            rt-scheduler</literal> to <literal>/org/gnome/mutter/experimental-features</literal>
+            with dconf-editor. You cannot use extraGSettingsOverrides because that will only
+            change the default value of the setting.
+
+            Please be aware of these known issues with the feature in nixos:
+            <itemizedlist>
+             <listitem>
+              <para>
+               <link xlink:href="https://github.com/NixOS/nixpkgs/issues/90201">NixOS/nixpkgs#90201</link>
+              </para>
+             </listitem>
+             <listitem>
+              <para>
+               <link xlink:href="https://github.com/NixOS/nixpkgs/issues/86730">NixOS/nixpkgs#86730</link>
+              </para>
+            </listitem>
+            </itemizedlist>
+          '';
+        };
+      };
+    };
+
+    services.xserver.desktopManager.gnome = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable GNOME desktop manager.";
+      };
+
+      sessionPath = mkOption {
+        default = [];
+        type = types.listOf types.package;
+        example = literalExpression "[ pkgs.gnome.gpaste ]";
+        description = ''
+          Additional list of packages to be added to the session search path.
+          Useful for GNOME Shell extensions or GSettings-conditional autostart.
+
+          Note that this should be a last resort; patching the package is preferred (see GPaste).
+        '';
+        apply = list: list ++ [ pkgs.gnome.gnome-shell pkgs.gnome.gnome-shell-extensions ];
+      };
+
+      favoriteAppsOverride = mkOption {
+        internal = true; # this is messy
+        default = defaultFavoriteAppsOverride;
+        type = types.lines;
+        example = literalExpression ''
+          '''
+            [org.gnome.shell]
+            favorite-apps=[ 'firefox.desktop', 'org.gnome.Calendar.desktop' ]
+          '''
+        '';
+        description = "List of desktop files to put as favorite apps into gnome-shell. These need to be installed somehow globally.";
+      };
+
+      extraGSettingsOverrides = mkOption {
+        default = "";
+        type = types.lines;
+        description = "Additional gsettings overrides.";
+      };
+
+      extraGSettingsOverridePackages = mkOption {
+        default = [];
+        type = types.listOf types.path;
+        description = "List of packages for which gsettings are overridden.";
+      };
+
+      debug = mkEnableOption "gnome-session debug messages";
+
+      flashback = {
+        enableMetacity = mkEnableOption "the standard GNOME Flashback session with Metacity";
+
+        customSessions = mkOption {
+          type = types.listOf (types.submodule {
+            options = {
+              wmName = mkOption {
+                type = types.strMatching "[a-zA-Z0-9_-]+";
+                description = "A unique identifier for the window manager.";
+                example = "xmonad";
+              };
+
+              wmLabel = mkOption {
+                type = types.str;
+                description = "The name of the window manager to show in the session chooser.";
+                example = "XMonad";
+              };
+
+              wmCommand = mkOption {
+                type = types.str;
+                description = "The executable of the window manager to use.";
+                example = literalExpression ''"''${pkgs.haskellPackages.xmonad}/bin/xmonad"'';
+              };
+
+              enableGnomePanel = mkOption {
+                type = types.bool;
+                default = true;
+                example = false;
+                description = "Whether to enable the GNOME panel in this session.";
+              };
+            };
+          });
+          default = [];
+          description = "Other GNOME Flashback sessions to enable.";
+        };
+
+        panelModulePackages = mkOption {
+          default = [ pkgs.gnome.gnome-applets ];
+          defaultText = literalExpression "[ pkgs.gnome.gnome-applets ]";
+          type = types.listOf types.path;
+          description = ''
+            Packages containing modules that should be made available to <literal>gnome-panel</literal> (usually for applets).
+
+            If you're packaging something to use here, please install the modules in <literal>$out/lib/gnome-panel/modules</literal>.
+          '';
+        };
+      };
+    };
+
+    environment.gnome.excludePackages = mkOption {
+      default = [];
+      example = literalExpression "[ pkgs.gnome.totem ]";
+      type = types.listOf types.package;
+      description = "Which packages gnome should exclude from the default environment";
+    };
+
+  };
+
+  config = mkMerge [
+    (mkIf (cfg.enable || flashbackEnabled) {
+      # Seed our configuration into nixos-generate-config
+      system.nixos-generate-config.desktopConfiguration = [''
+        # Enable the GNOME Desktop Environment.
+        services.xserver.displayManager.gdm.enable = true;
+        services.xserver.desktopManager.gnome.enable = true;
+      ''];
+
+      services.gnome.core-os-services.enable = true;
+      services.gnome.core-shell.enable = true;
+      services.gnome.core-utilities.enable = mkDefault true;
+
+      services.xserver.displayManager.sessionPackages = [ pkgs.gnome.gnome-session.sessions ];
+
+      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 = cfg.sessionPath;
+
+      environment.sessionVariables.GNOME_SESSION_DEBUG = mkIf cfg.debug "1";
+
+      # Override GSettings schemas
+      environment.sessionVariables.NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-desktop-schemas}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
+
+       # If gnome is installed, build vim for gtk3 too.
+      nixpkgs.config.vim.gui = "gtk3";
+    })
+
+    (mkIf flashbackEnabled {
+      services.xserver.displayManager.sessionPackages =
+        let
+          wmNames = map (wm: wm.wmName) flashbackWms;
+          namesAreUnique = lib.unique wmNames == wmNames;
+        in
+          assert (assertMsg namesAreUnique "Flashback WM names must be unique.");
+          map
+            (wm:
+              pkgs.gnome.gnome-flashback.mkSessionForWm {
+                inherit (wm) wmName wmLabel wmCommand enableGnomePanel;
+                inherit (cfg.flashback) panelModulePackages;
+              }
+            ) flashbackWms;
+
+      security.pam.services.gnome-flashback = {
+        enableGnomeKeyring = true;
+      };
+
+      systemd.packages = with pkgs.gnome; [
+        gnome-flashback
+      ] ++ map gnome-flashback.mkSystemdTargetForWm flashbackWms;
+
+      # gnome-panel needs these for menu applet
+      environment.sessionVariables.XDG_DATA_DIRS = [ "${pkgs.gnome.gnome-flashback}/share" ];
+      # TODO: switch to sessionVariables (resolve conflict)
+      environment.variables.XDG_CONFIG_DIRS = [ "${pkgs.gnome.gnome-flashback}/etc/xdg" ];
+    })
+
+    (mkIf serviceCfg.core-os-services.enable {
+      hardware.bluetooth.enable = mkDefault true;
+      hardware.pulseaudio.enable = mkDefault true;
+      programs.dconf.enable = true;
+      security.polkit.enable = true;
+      services.accounts-daemon.enable = true;
+      services.dleyna-renderer.enable = mkDefault true;
+      services.dleyna-server.enable = mkDefault true;
+      services.power-profiles-daemon.enable = mkDefault true;
+      services.gnome.at-spi2-core.enable = true;
+      services.gnome.evolution-data-server.enable = true;
+      services.gnome.gnome-keyring.enable = true;
+      services.gnome.gnome-online-accounts.enable = mkDefault true;
+      services.gnome.gnome-online-miners.enable = true;
+      services.gnome.tracker-miners.enable = mkDefault true;
+      services.gnome.tracker.enable = mkDefault true;
+      services.hardware.bolt.enable = mkDefault true;
+      services.packagekit.enable = mkDefault true;
+      services.udisks2.enable = true;
+      services.upower.enable = config.powerManagement.enable;
+      services.xserver.libinput.enable = mkDefault true; # for controlling touchpad settings via gnome control center
+
+      xdg.portal.enable = true;
+      xdg.portal.extraPortals = [
+        pkgs.xdg-desktop-portal-gnome
+        (pkgs.xdg-desktop-portal-gtk.override {
+          # Do not build portals that we already have.
+          buildPortalsInGnome = false;
+        })
+      ];
+
+      # Harmonize Qt5 application style and also make them use the portal for file chooser dialog.
+      qt5 = {
+        enable = mkDefault true;
+        platformTheme = mkDefault "gnome";
+        style = mkDefault "adwaita";
+      };
+
+      networking.networkmanager.enable = mkDefault true;
+
+      services.xserver.updateDbusEnvironment = true;
+
+      # gnome has a custom alert theme but it still
+      # inherits from the freedesktop theme.
+      environment.systemPackages = with pkgs; [
+        sound-theme-freedesktop
+      ];
+
+      # Needed for themes and backgrounds
+      environment.pathsToLink = [
+        "/share" # TODO: https://github.com/NixOS/nixpkgs/issues/47173
+      ];
+    })
+
+    (mkIf serviceCfg.core-shell.enable {
+      services.colord.enable = mkDefault true;
+      services.gnome.chrome-gnome-shell.enable = mkDefault true;
+      services.gnome.glib-networking.enable = true;
+      services.gnome.gnome-initial-setup.enable = mkDefault true;
+      services.gnome.gnome-remote-desktop.enable = mkDefault true;
+      services.gnome.gnome-settings-daemon.enable = true;
+      services.gnome.gnome-user-share.enable = mkDefault true;
+      services.gnome.rygel.enable = mkDefault true;
+      services.gvfs.enable = true;
+      services.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true));
+      services.telepathy.enable = mkDefault true;
+
+      systemd.packages = with pkgs.gnome; [
+        gnome-session
+        gnome-shell
+      ];
+
+      services.udev.packages = with pkgs.gnome; [
+        # Force enable KMS modifiers for devices that require them.
+        # https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/1443
+        mutter
+      ];
+
+      services.avahi.enable = mkDefault true;
+
+      xdg.portal.extraPortals = [
+        pkgs.gnome.gnome-shell
+      ];
+
+      services.geoclue2.enable = mkDefault true;
+      services.geoclue2.enableDemoAgent = false; # GNOME has its own geoclue agent
+
+      services.geoclue2.appConfig.gnome-datetime-panel = {
+        isAllowed = true;
+        isSystem = true;
+      };
+      services.geoclue2.appConfig.gnome-color-panel = {
+        isAllowed = true;
+        isSystem = true;
+      };
+      services.geoclue2.appConfig."org.gnome.Shell" = {
+        isAllowed = true;
+        isSystem = true;
+      };
+
+      fonts.fonts = with pkgs; [
+        cantarell-fonts
+        dejavu_fonts
+        source-code-pro # Default monospace font in 3.32
+        source-sans
+      ];
+
+      # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/gnome-3-38/elements/core/meta-gnome-core-shell.bst
+      environment.systemPackages = with pkgs.gnome; [
+        adwaita-icon-theme
+        gnome-backgrounds
+        gnome-bluetooth
+        gnome-color-manager
+        gnome-control-center
+        gnome-shell
+        gnome-shell-extensions
+        gnome-themes-extra
+        pkgs.gnome-tour # GNOME Shell detects the .desktop file on first log-in.
+        pkgs.nixos-artwork.wallpapers.simple-dark-gray
+        pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom
+        pkgs.gnome-user-docs
+        pkgs.orca
+        pkgs.glib # for gsettings
+        pkgs.gnome-menus
+        pkgs.gtk3.out # for gtk-launch
+        pkgs.hicolor-icon-theme
+        pkgs.shared-mime-info # for update-mime-database
+        pkgs.xdg-user-dirs # Update user dirs as described in http://freedesktop.org/wiki/Software/xdg-user-dirs/
+      ];
+    })
+
+    # Enable soft realtime scheduling, only supported on wayland
+    (mkIf serviceCfg.experimental-features.realtime-scheduling {
+      security.wrappers.".gnome-shell-wrapped" = {
+        source = "${pkgs.gnome.gnome-shell}/bin/.gnome-shell-wrapped";
+        owner = "root";
+        group = "root";
+        capabilities = "cap_sys_nice=ep";
+      };
+
+      systemd.user.services.gnome-shell-wayland = let
+        gnomeShellRT = with pkgs.gnome; pkgs.runCommand "gnome-shell-rt" {} ''
+          mkdir -p $out/bin/
+          cp ${gnome-shell}/bin/gnome-shell $out/bin
+          sed -i "s@${gnome-shell}/bin/@${config.security.wrapperDir}/@" $out/bin/gnome-shell
+        '';
+      in {
+        # Note we need to clear ExecStart before overriding it
+        serviceConfig.ExecStart = ["" "${gnomeShellRT}/bin/gnome-shell"];
+        # Do not use the default environment, it provides a broken PATH
+        environment = mkForce {};
+      };
+    })
+
+    # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/gnome-3-38/elements/core/meta-gnome-core-utilities.bst
+    (mkIf serviceCfg.core-utilities.enable {
+      environment.systemPackages =
+        with pkgs.gnome;
+        removePackagesByName
+          ([
+            baobab
+            cheese
+            eog
+            epiphany
+            gedit
+            gnome-calculator
+            gnome-calendar
+            gnome-characters
+            gnome-clocks
+            gnome-contacts
+            gnome-font-viewer
+            gnome-logs
+            gnome-maps
+            gnome-music
+            pkgs.gnome-photos
+            gnome-screenshot
+            gnome-system-monitor
+            gnome-weather
+            nautilus
+            pkgs.gnome-connections
+            simple-scan
+            totem
+            yelp
+          ] ++ lib.optionals config.services.flatpak.enable [
+            # Since PackageKit Nix support is not there yet,
+            # only install gnome-software if flatpak is enabled.
+            gnome-software
+          ])
+          config.environment.gnome.excludePackages;
+
+      # Enable default program modules
+      # Since some of these have a corresponding package, we only
+      # enable that program module if the package hasn't been excluded
+      # through `environment.gnome.excludePackages`
+      programs.evince.enable = notExcluded pkgs.gnome.evince;
+      programs.file-roller.enable = notExcluded pkgs.gnome.file-roller;
+      programs.geary.enable = notExcluded pkgs.gnome.geary;
+      programs.gnome-disks.enable = notExcluded pkgs.gnome.gnome-disk-utility;
+      programs.gnome-terminal.enable = notExcluded pkgs.gnome.gnome-terminal;
+      programs.seahorse.enable = notExcluded pkgs.gnome.seahorse;
+      services.gnome.sushi.enable = notExcluded pkgs.gnome.sushi;
+
+      # Let nautilus find extensions
+      # TODO: Create nautilus-with-extensions package
+      environment.sessionVariables.NAUTILUS_EXTENSION_DIR = "${config.system.path}/lib/nautilus/extensions-3.0";
+
+      # Override default mimeapps for nautilus
+      environment.sessionVariables.XDG_DATA_DIRS = [ "${mimeAppsList}/share" ];
+
+      environment.pathsToLink = [
+        "/share/nautilus-python/extensions"
+      ];
+    })
+
+    (mkIf serviceCfg.games.enable {
+      environment.systemPackages = (with pkgs.gnome; removePackagesByName [
+        aisleriot
+        atomix
+        five-or-more
+        four-in-a-row
+        pkgs.gnome-2048
+        gnome-chess
+        gnome-klotski
+        gnome-mahjongg
+        gnome-mines
+        gnome-nibbles
+        gnome-robots
+        gnome-sudoku
+        gnome-taquin
+        gnome-tetravex
+        hitori
+        iagno
+        lightsoff
+        quadrapassel
+        swell-foop
+        tali
+      ] config.environment.gnome.excludePackages);
+    })
+
+    # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/-/blob/3.38.0/elements/core/meta-gnome-core-developer-tools.bst
+    (mkIf serviceCfg.core-developer-tools.enable {
+      environment.systemPackages = (with pkgs.gnome; removePackagesByName [
+        dconf-editor
+        devhelp
+        pkgs.gnome-builder
+        # boxes would make sense in this option, however
+        # it doesn't function well enough to be included
+        # in default configurations.
+        # https://github.com/NixOS/nixpkgs/issues/60908
+        /* gnome-boxes */
+      ] config.environment.gnome.excludePackages);
+
+      services.sysprof.enable = notExcluded pkgs.sysprof;
+    })
+  ];
+
+}
diff --git a/nixos/modules/services/x11/desktop-managers/gnome.xml b/nixos/modules/services/x11/desktop-managers/gnome.xml
new file mode 100644
index 00000000000..807c9d64e20
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/gnome.xml
@@ -0,0 +1,253 @@
+<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/kodi.nix b/nixos/modules/services/x11/desktop-managers/kodi.nix
new file mode 100644
index 00000000000..b853c94d6fd
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/kodi.nix
@@ -0,0 +1,41 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.desktopManager.kodi;
+in
+
+{
+  options = {
+    services.xserver.desktopManager.kodi = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable the kodi multimedia center.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.kodi;
+        defaultText = literalExpression "pkgs.kodi";
+        example = literalExpression "pkgs.kodi.withPackages (p: with p; [ jellyfin pvr-iptvsimple vfs-sftp ])";
+        description = ''
+          Package that should be used for Kodi.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.desktopManager.session = [{
+      name = "kodi";
+      start = ''
+        LIRC_SOCKET_PATH=/run/lirc/lircd ${cfg.package}/bin/kodi --standalone &
+        waitPID=$!
+      '';
+    }];
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/x11/desktop-managers/lumina.nix b/nixos/modules/services/x11/desktop-managers/lumina.nix
new file mode 100644
index 00000000000..419f5055d8b
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/lumina.nix
@@ -0,0 +1,42 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  xcfg = config.services.xserver;
+  cfg = xcfg.desktopManager.lumina;
+
+in
+
+{
+  options = {
+
+    services.xserver.desktopManager.lumina.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable the Lumina desktop manager";
+    };
+
+  };
+
+
+  config = mkIf cfg.enable {
+
+    services.xserver.displayManager.sessionPackages = [
+      pkgs.lumina.lumina
+    ];
+
+    environment.systemPackages =
+      pkgs.lumina.preRequisitePackages ++
+      pkgs.lumina.corePackages;
+
+    # Link some extra directories in /run/current-system/software/share
+    environment.pathsToLink = [
+      "/share/lumina"
+      # FIXME: modules should link subdirs of `/share` rather than relying on this
+      "/share"
+    ];
+
+  };
+}
diff --git a/nixos/modules/services/x11/desktop-managers/lxqt.nix b/nixos/modules/services/x11/desktop-managers/lxqt.nix
new file mode 100644
index 00000000000..720985ba0d9
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/lxqt.nix
@@ -0,0 +1,67 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  xcfg = config.services.xserver;
+  cfg = xcfg.desktopManager.lxqt;
+
+in
+
+{
+  options = {
+
+    services.xserver.desktopManager.lxqt.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable the LXQt desktop manager";
+    };
+
+    environment.lxqt.excludePackages = mkOption {
+      default = [];
+      example = literalExpression "[ pkgs.lxqt.qterminal ]";
+      type = types.listOf types.package;
+      description = "Which LXQt packages to exclude from the default environment";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    services.xserver.desktopManager.session = singleton {
+      name = "lxqt";
+      bgSupport = true;
+      start = ''
+        # Upstream installs default configuration files in
+        # $prefix/share/lxqt instead of $prefix/etc/xdg, (arguably)
+        # giving distributors freedom to ship custom default
+        # configuration files more easily. In order to let the session
+        # manager find them the share subdirectory is added to the
+        # XDG_CONFIG_DIRS environment variable.
+        #
+        # For an explanation see
+        # https://github.com/lxqt/lxqt/issues/1521#issuecomment-405097453
+        #
+        export XDG_CONFIG_DIRS=$XDG_CONFIG_DIRS''${XDG_CONFIG_DIRS:+:}${config.system.path}/share
+
+        exec ${pkgs.lxqt.lxqt-session}/bin/startlxqt
+      '';
+    };
+
+    environment.systemPackages =
+      pkgs.lxqt.preRequisitePackages ++
+      pkgs.lxqt.corePackages ++
+      (pkgs.gnome.removePackagesByName
+        pkgs.lxqt.optionalPackages
+        config.environment.lxqt.excludePackages);
+
+    # Link some extra directories in /run/current-system/software/share
+    environment.pathsToLink = [ "/share" ];
+
+    # virtual file systems support for PCManFM-QT
+    services.gvfs.enable = true;
+
+    services.upower.enable = config.powerManagement.enable;
+  };
+
+}
diff --git a/nixos/modules/services/x11/desktop-managers/mate.nix b/nixos/modules/services/x11/desktop-managers/mate.nix
new file mode 100644
index 00000000000..a7fda4be979
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/mate.nix
@@ -0,0 +1,110 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  addToXDGDirs = p: ''
+    if [ -d "${p}/share/gsettings-schemas/${p.name}" ]; then
+      export XDG_DATA_DIRS=$XDG_DATA_DIRS''${XDG_DATA_DIRS:+:}${p}/share/gsettings-schemas/${p.name}
+    fi
+
+    if [ -d "${p}/lib/girepository-1.0" ]; then
+      export GI_TYPELIB_PATH=$GI_TYPELIB_PATH''${GI_TYPELIB_PATH:+:}${p}/lib/girepository-1.0
+      export LD_LIBRARY_PATH=$LD_LIBRARY_PATH''${LD_LIBRARY_PATH:+:}${p}/lib
+    fi
+  '';
+
+  xcfg = config.services.xserver;
+  cfg = xcfg.desktopManager.mate;
+
+in
+
+{
+  options = {
+
+    services.xserver.desktopManager.mate = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable the MATE desktop environment";
+      };
+
+      debug = mkEnableOption "mate-session debug messages";
+    };
+
+    environment.mate.excludePackages = mkOption {
+      default = [];
+      example = literalExpression "[ pkgs.mate.mate-terminal pkgs.mate.pluma ]";
+      type = types.listOf types.package;
+      description = "Which MATE packages to exclude from the default environment";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    services.xserver.displayManager.sessionPackages = [
+      pkgs.mate.mate-session-manager
+    ];
+
+    services.xserver.displayManager.sessionCommands = ''
+      if test "$XDG_CURRENT_DESKTOP" = "MATE"; then
+          export XDG_MENU_PREFIX=mate-
+
+          # Let caja find extensions
+          export CAJA_EXTENSION_DIRS=$CAJA_EXTENSION_DIRS''${CAJA_EXTENSION_DIRS:+:}${config.system.path}/lib/caja/extensions-2.0
+
+          # Let caja extensions find gsettings schemas
+          ${concatMapStrings (p: ''
+          if [ -d "${p}/lib/caja/extensions-2.0" ]; then
+              ${addToXDGDirs p}
+          fi
+          '') config.environment.systemPackages}
+
+          # Add mate-control-center paths to some XDG variables because its schemas are needed by mate-settings-daemon, and mate-settings-daemon is a dependency for mate-control-center (that is, they are mutually recursive)
+          ${addToXDGDirs pkgs.mate.mate-control-center}
+      fi
+    '';
+
+    # Let mate-panel find applets
+    environment.sessionVariables."MATE_PANEL_APPLETS_DIR" = "${config.system.path}/share/mate-panel/applets";
+    environment.sessionVariables."MATE_PANEL_EXTRA_MODULES" = "${config.system.path}/lib/mate-panel/applets";
+
+    # Debugging
+    environment.sessionVariables.MATE_SESSION_DEBUG = mkIf cfg.debug "1";
+
+    environment.systemPackages = pkgs.gnome.removePackagesByName
+      (pkgs.mate.basePackages ++
+      pkgs.mate.extraPackages ++
+      [
+        pkgs.desktop-file-utils
+        pkgs.glib
+        pkgs.gtk3.out
+        pkgs.shared-mime-info
+        pkgs.xdg-user-dirs # Update user dirs as described in https://freedesktop.org/wiki/Software/xdg-user-dirs/
+        pkgs.mate.mate-settings-daemon
+        pkgs.yelp # for 'Contents' in 'Help' menus
+      ])
+      config.environment.mate.excludePackages;
+
+    programs.dconf.enable = true;
+    # Shell integration for VTE terminals
+    programs.bash.vteIntegration = mkDefault true;
+    programs.zsh.vteIntegration = mkDefault true;
+
+    # Mate uses this for printing
+    programs.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true));
+
+    services.gnome.at-spi2-core.enable = true;
+    services.gnome.gnome-keyring.enable = true;
+    services.udev.packages = [ pkgs.mate.mate-settings-daemon ];
+    services.gvfs.enable = true;
+    services.upower.enable = config.powerManagement.enable;
+
+    security.pam.services.mate-screensaver.unixAuth = true;
+
+    environment.pathsToLink = [ "/share" ];
+  };
+
+}
diff --git a/nixos/modules/services/x11/desktop-managers/none.nix b/nixos/modules/services/x11/desktop-managers/none.nix
new file mode 100644
index 00000000000..af7a376ae02
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/none.nix
@@ -0,0 +1,7 @@
+{
+  services.xserver.desktopManager.session =
+    [ { name = "none";
+        start = "";
+      }
+    ];
+}
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.nix b/nixos/modules/services/x11/desktop-managers/pantheon.nix
new file mode 100644
index 00000000000..8ff9b0b756d
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix
@@ -0,0 +1,316 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.desktopManager.pantheon;
+  serviceCfg = config.services.pantheon;
+
+  nixos-gsettings-desktop-schemas = pkgs.pantheon.elementary-gsettings-schemas.override {
+    extraGSettingsOverridePackages = cfg.extraGSettingsOverridePackages;
+    extraGSettingsOverrides = cfg.extraGSettingsOverrides;
+  };
+
+in
+
+{
+
+  meta = {
+    doc = ./pantheon.xml;
+    maintainers = teams.pantheon.members;
+  };
+
+  options = {
+
+    services.pantheon = {
+
+      contractor = {
+         enable = mkEnableOption "contractor, a desktop-wide extension service used by Pantheon";
+      };
+
+      apps.enable = mkEnableOption "Pantheon default applications";
+
+    };
+
+    services.xserver.desktopManager.pantheon = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable the pantheon desktop manager";
+      };
+
+      sessionPath = mkOption {
+        default = [];
+        type = types.listOf types.package;
+        example = literalExpression "[ pkgs.gnome.gpaste ]";
+        description = ''
+          Additional list of packages to be added to the session search path.
+          Useful for GSettings-conditional autostart.
+
+          Note that this should be a last resort; patching the package is preferred (see GPaste).
+        '';
+        apply = list: list ++
+        [
+          pkgs.pantheon.pantheon-agent-geoclue2
+        ];
+      };
+
+      extraWingpanelIndicators = mkOption {
+        default = null;
+        type = with types; nullOr (listOf package);
+        description = "Indicators to add to Wingpanel.";
+      };
+
+      extraSwitchboardPlugs = mkOption {
+        default = null;
+        type = with types; nullOr (listOf package);
+        description = "Plugs to add to Switchboard.";
+      };
+
+      extraGSettingsOverrides = mkOption {
+        default = "";
+        type = types.lines;
+        description = "Additional gsettings overrides.";
+      };
+
+      extraGSettingsOverridePackages = mkOption {
+        default = [];
+        type = types.listOf types.path;
+        description = "List of packages for which gsettings are overridden.";
+      };
+
+      debug = mkEnableOption "gnome-session debug messages";
+
+    };
+
+    environment.pantheon.excludePackages = mkOption {
+      default = [];
+      example = literalExpression "[ pkgs.pantheon.elementary-camera ]";
+      type = types.listOf types.package;
+      description = "Which packages pantheon should exclude from the default environment";
+    };
+
+  };
+
+
+  config = mkMerge [
+    (mkIf cfg.enable {
+
+      services.xserver.displayManager.sessionPackages = [ pkgs.pantheon.elementary-session-settings ];
+
+      # Ensure lightdm is used when Pantheon is enabled
+      # Without it screen locking will be nonfunctional because of the use of lightlocker
+      warnings = optional (config.services.xserver.displayManager.lightdm.enable != true)
+        ''
+          Using Pantheon without LightDM as a displayManager will break screenlocking from the UI.
+        '';
+
+      services.xserver.displayManager.lightdm.greeters.pantheon.enable = mkDefault true;
+
+      # Without this, elementary LightDM greeter will pre-select non-existent `default` session
+      # https://github.com/elementary/greeter/issues/368
+      services.xserver.displayManager.defaultSession = mkDefault "pantheon";
+
+      services.xserver.displayManager.sessionCommands = ''
+        if test "$XDG_CURRENT_DESKTOP" = "Pantheon"; then
+            ${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}
+        fi
+      '';
+
+      # Default services
+      hardware.bluetooth.enable = mkDefault true;
+      hardware.pulseaudio.enable = mkDefault true;
+      security.polkit.enable = true;
+      services.accounts-daemon.enable = true;
+      services.bamf.enable = true;
+      services.colord.enable = mkDefault true;
+      services.fwupd.enable = mkDefault true;
+      services.packagekit.enable = mkDefault true;
+      services.touchegg.enable = mkDefault true;
+      services.touchegg.package = pkgs.pantheon.touchegg;
+      services.tumbler.enable = mkDefault true;
+      services.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true));
+      services.dbus.packages = with pkgs.pantheon; [
+        switchboard-plug-power
+        elementary-default-settings # accountsservice extensions
+      ];
+      services.pantheon.apps.enable = mkDefault true;
+      services.pantheon.contractor.enable = mkDefault true;
+      services.gnome.at-spi2-core.enable = true;
+      services.gnome.evolution-data-server.enable = true;
+      services.gnome.glib-networking.enable = true;
+      services.gnome.gnome-keyring.enable = true;
+      services.gvfs.enable = true;
+      services.gnome.rygel.enable = mkDefault true;
+      services.gsignond.enable = mkDefault true;
+      services.gsignond.plugins = with pkgs.gsignondPlugins; [ lastfm mail oauth ];
+      services.udisks2.enable = true;
+      services.upower.enable = config.powerManagement.enable;
+      services.xserver.libinput.enable = mkDefault true;
+      services.xserver.updateDbusEnvironment = true;
+      services.zeitgeist.enable = mkDefault true;
+      services.geoclue2.enable = mkDefault true;
+      # pantheon has pantheon-agent-geoclue2
+      services.geoclue2.enableDemoAgent = false;
+      services.geoclue2.appConfig."io.elementary.desktop.agent-geoclue2" = {
+        isAllowed = true;
+        isSystem = true;
+      };
+      services.udev.packages = [
+        pkgs.gnome.gnome-settings-daemon338
+      ];
+      systemd.packages = [
+        pkgs.gnome.gnome-settings-daemon338
+      ];
+      programs.dconf.enable = true;
+      networking.networkmanager.enable = mkDefault true;
+
+      # Global environment
+      environment.systemPackages = with pkgs; [
+        desktop-file-utils
+        glib
+        gnome-menus
+        gnome.adwaita-icon-theme
+        gtk3.out
+        hicolor-icon-theme
+        onboard
+        qgnomeplatform
+        shared-mime-info
+        sound-theme-freedesktop
+        xdg-user-dirs
+      ] ++ (with pkgs.pantheon; [
+        # Artwork
+        elementary-gtk-theme
+        elementary-icon-theme
+        elementary-sound-theme
+        elementary-wallpapers
+
+        # Desktop
+        elementary-default-settings
+        elementary-dock
+        elementary-session-settings
+        elementary-shortcut-overlay
+        gala
+        (switchboard-with-plugs.override {
+          plugs = cfg.extraSwitchboardPlugs;
+        })
+        (wingpanel-with-indicators.override {
+          indicators = cfg.extraWingpanelIndicators;
+        })
+
+        # Services
+        elementary-capnet-assist
+        elementary-notifications
+        elementary-settings-daemon
+        pantheon-agent-geoclue2
+        pantheon-agent-polkit
+      ]) ++ (gnome.removePackagesByName [
+        gnome.gnome-font-viewer
+        gnome.gnome-settings-daemon338
+      ] config.environment.pantheon.excludePackages);
+
+      programs.evince.enable = mkDefault true;
+      programs.evince.package = pkgs.pantheon.evince;
+      programs.file-roller.enable = mkDefault true;
+      programs.file-roller.package = pkgs.pantheon.file-roller;
+
+      # Settings from elementary-default-settings
+      environment.etc."gtk-3.0/settings.ini".source = "${pkgs.pantheon.elementary-default-settings}/etc/gtk-3.0/settings.ini";
+
+      xdg.portal.enable = true;
+      xdg.portal.extraPortals = with pkgs.pantheon; [
+        elementary-files
+        elementary-settings-daemon
+        xdg-desktop-portal-pantheon
+      ];
+
+      # Override GSettings schemas
+      environment.sessionVariables.NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-desktop-schemas}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
+
+      environment.sessionVariables.GNOME_SESSION_DEBUG = mkIf cfg.debug "1";
+
+      environment.pathsToLink = [
+        # FIXME: modules should link subdirs of `/share` rather than relying on this
+        "/share"
+      ];
+
+      # Otherwise you can't store NetworkManager Secrets with
+      # "Store the password only for this user"
+      programs.nm-applet.enable = true;
+      # Pantheon has its own network indicator
+      programs.nm-applet.indicator = false;
+
+      # Shell integration for VTE terminals
+      programs.bash.vteIntegration = mkDefault true;
+      programs.zsh.vteIntegration = mkDefault true;
+
+      # Harmonize Qt5 applications under Pantheon
+      qt5.enable = true;
+      qt5.platformTheme = "gnome";
+      qt5.style = "adwaita";
+
+      # Default Fonts
+      fonts.fonts = with pkgs; [
+        inter
+        open-dyslexic
+        open-sans
+        roboto-mono
+      ];
+
+      fonts.fontconfig.defaultFonts = {
+        monospace = [ "Roboto Mono" ];
+        sansSerif = [ "Inter" ];
+      };
+    })
+
+    (mkIf serviceCfg.apps.enable {
+      environment.systemPackages = with pkgs.pantheon; pkgs.gnome.removePackagesByName ([
+        elementary-calculator
+        elementary-calendar
+        elementary-camera
+        elementary-code
+        elementary-files
+        elementary-mail
+        elementary-music
+        elementary-photos
+        elementary-screenshot
+        elementary-tasks
+        elementary-terminal
+        elementary-videos
+        epiphany
+      ] ++ lib.optionals config.services.flatpak.enable [
+        # Only install appcenter if flatpak is enabled before
+        # https://github.com/NixOS/nixpkgs/issues/15932 is resolved.
+        appcenter
+      ]) config.environment.pantheon.excludePackages;
+
+      # needed by screenshot
+      fonts.fonts = [
+        pkgs.pantheon.elementary-redacted-script
+      ];
+    })
+
+    (mkIf serviceCfg.contractor.enable {
+      environment.systemPackages = with pkgs.pantheon; [
+        contractor
+        file-roller-contract
+        gnome-bluetooth-contract
+      ];
+
+      environment.pathsToLink = [
+        "/share/contractor"
+      ];
+    })
+
+  ];
+}
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.xml b/nixos/modules/services/x11/desktop-managers/pantheon.xml
new file mode 100644
index 00000000000..202909d398f
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.xml
@@ -0,0 +1,120 @@
+<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 3 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-gnome3-and-pantheon">
+    <term>
+     I cannot enable both GNOME 3 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/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix
new file mode 100644
index 00000000000..b7aa2eba81c
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/plasma5.nix
@@ -0,0 +1,579 @@
+{ config, lib, pkgs, ... }:
+
+let
+  xcfg = config.services.xserver;
+  cfg = xcfg.desktopManager.plasma5;
+
+  # Use only for **internal** options.
+  # This is not exactly user-friendly.
+  kdeConfigurationType = with types;
+    let
+      valueTypes = (oneOf [
+        bool
+        float
+        int
+        str
+      ]) // {
+        description = "KDE Configuration value";
+        emptyValue.value = "";
+      };
+      set = (nullOr (lazyAttrsOf valueTypes)) // {
+        description = "KDE Configuration set";
+        emptyValue.value = {};
+      };
+    in (lazyAttrsOf set) // {
+        description = "KDE Configuration file";
+        emptyValue.value = {};
+      };
+
+  libsForQt5 = pkgs.plasma5Packages;
+  inherit (libsForQt5) kdeGear kdeFrameworks plasma5;
+  inherit (pkgs) writeText;
+  inherit (lib)
+    getBin optionalString
+    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;
+    };
+  };
+
+  activationScript = ''
+    ${set_XDG_CONFIG_HOME}
+
+    # The KDE icon cache is supposed to update itself automatically, but it uses
+    # the timestamp on the icon theme directory as a trigger. This doesn't work
+    # on NixOS because the timestamp never changes. As a workaround, delete the
+    # icon cache at login and session activation.
+    # See also: http://lists-archives.org/kde-devel/26175-what-when-will-icon-cache-refresh.html
+    rm -fv $HOME/.cache/icon-cache.kcache
+
+    # xdg-desktop-settings generates this empty file but
+    # it makes kbuildsyscoca5 fail silently. To fix this
+    # remove that menu if it exists.
+    rm -fv ''${XDG_CONFIG_HOME}/menus/applications-merged/xdg-desktop-menu-dummy.menu
+
+    # Qt writes a weird ‘libraryPath’ line to
+    # ~/.config/Trolltech.conf that causes the KDE plugin
+    # paths of previous KDE invocations to be searched.
+    # Obviously using mismatching KDE libraries is potentially
+    # disastrous, so here we nuke references to the Nix store
+    # in Trolltech.conf.  A better solution would be to stop
+    # Qt from doing this wackiness in the first place.
+    trolltech_conf="''${XDG_CONFIG_HOME}/Trolltech.conf"
+    if [ -e "$trolltech_conf" ]; then
+      ${getBin pkgs.gnused}/bin/sed -i "$trolltech_conf" -e '/nix\\store\|nix\/store/ d'
+    fi
+
+    # Remove the kbuildsyscoca5 cache. It will be regenerated
+    # immediately after. This is necessary for kbuildsyscoca5 to
+    # recognize that software that has been removed.
+    rm -fv $HOME/.cache/ksycoca*
+
+    ${libsForQt5.kservice}/bin/kbuildsycoca5
+  '';
+
+  set_XDG_CONFIG_HOME = ''
+    # Set the default XDG_CONFIG_HOME if it is unset.
+    # Per the XDG Base Directory Specification:
+    # https://specifications.freedesktop.org/basedir-spec/latest
+    # 1. Never export this variable! If it is unset, then child processes are
+    # expected to set the default themselves.
+    # 2. Contaminate / if $HOME is unset; do not check if $HOME is set.
+    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 = "Enable the Plasma 5 (KDE 5) desktop environment.";
+    };
+
+    phononBackend = mkOption {
+      type = types.enum [ "gstreamer" "vlc" ];
+      default = "gstreamer";
+      example = "vlc";
+      description = "Phonon audio backend to install.";
+    };
+
+    supportDDC = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Support setting monitor brightness via DDC.
+        </para>
+        <para>
+        This is not needed for controlling brightness of the internal monitor
+        of a laptop and as it is considered experimental by upstream, it is
+        disabled by default.
+      '';
+    };
+
+    useQtScaling = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable HiDPI scaling in Qt.";
+    };
+
+    runUsingSystemd = mkOption {
+      description = "Use systemd to manage the Plasma session";
+      type = types.bool;
+      default = false;
+    };
+
+    # Internally allows configuring kdeglobals globally
+    kdeglobals = 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 = ''
+        Enable support for running the Plasma Mobile shell.
+      '';
+    };
+
+    mobile.installRecommendedSoftware = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Installs software recommended for use with Plasma Mobile, but which
+        is not strictly required for Plasma Mobile to run.
+      '';
+    };
+  };
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "xserver" "desktopManager" "plasma5" "enableQt4Support" ] "Phonon no longer supports Qt 4.")
+    (mkRenamedOptionModule [ "services" "xserver" "desktopManager" "kde5" ] [ "services" "xserver" "desktopManager" "plasma5" ])
+  ];
+
+  config = mkMerge [
+    # Common Plasma dependencies
+    (mkIf (cfg.enable || cfg.mobile.enable) {
+
+      security.wrappers = {
+        kcheckpass = {
+          setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${getBin libsForQt5.kscreenlocker}/libexec/kcheckpass";
+        };
+        start_kdeinit = {
+          setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${getBin libsForQt5.kinit}/libexec/kf5/start_kdeinit";
+        };
+        kwin_wayland = {
+          owner = "root";
+          group = "root";
+          capabilities = "cap_sys_nice+ep";
+          source = "${getBin plasma5.kwin}/bin/kwin_wayland";
+        };
+      };
+
+      # 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;
+        [
+          frameworkintegration
+          kactivities
+          kauth
+          kcmutils
+          kconfig
+          kconfigwidgets
+          kcoreaddons
+          kdoctools
+          kdbusaddons
+          kdeclarative
+          kded
+          kdesu
+          kdnssd
+          kemoticons
+          kfilemetadata
+          kglobalaccel
+          kguiaddons
+          kiconthemes
+          kidletime
+          kimageformats
+          kinit
+          kirigami2 # In system profile for SDDM theme. TODO: wrapper.
+          kio
+          kjobwidgets
+          knewstuff
+          knotifications
+          knotifyconfig
+          kpackage
+          kparts
+          kpeople
+          krunner
+          kservice
+          ktextwidgets
+          kwallet
+          kwallet-pam
+          kwalletmanager
+          kwayland
+          kwayland-integration
+          kwidgetsaddons
+          kxmlgui
+          kxmlrpcclient
+          plasma-framework
+          solid
+          sonnet
+          threadweaver
+
+          breeze-qt5
+          kactivitymanagerd
+          kde-cli-tools
+          kdecoration
+          kdeplasma-addons
+          kgamma5
+          khotkeys
+          kscreen
+          kscreenlocker
+          kwayland
+          kwin
+          kwrited
+          libkscreen
+          libksysguard
+          milou
+          plasma-browser-integration
+          plasma-integration
+          polkit-kde-agent
+
+          plasma-desktop
+          plasma-workspace
+          plasma-workspace-wallpapers
+
+          konsole
+          oxygen
+
+          breeze-icons
+          pkgs.hicolor-icon-theme
+
+          kde-gtk-config
+          breeze-gtk
+
+          qtvirtualkeyboard
+
+          pkgs.xdg-user-dirs # Update user dirs as described in https://freedesktop.org/wiki/Software/xdg-user-dirs/
+        ]
+
+        # Phonon audio backend
+        ++ lib.optional (cfg.phononBackend == "gstreamer") libsForQt5.phonon-backend-gstreamer
+        ++ lib.optional (cfg.phononBackend == "vlc") libsForQt5.phonon-backend-vlc
+
+        # Optional hardware support features
+        ++ lib.optionals config.hardware.bluetooth.enable [ bluedevil bluez-qt pkgs.openobex pkgs.obexftp ]
+        ++ lib.optional config.networking.networkmanager.enable plasma-nm
+        ++ lib.optional config.hardware.pulseaudio.enable plasma-pa
+        ++ lib.optional config.services.pipewire.pulse.enable plasma-pa
+        ++ lib.optional config.powerManagement.enable powerdevil
+        ++ 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;
+
+      environment.pathsToLink = [
+        # FIXME: modules should link subdirs of `/share` rather than relying on this
+        "/share"
+      ];
+
+      environment.etc."X11/xkb".source = xcfg.xkbDir;
+
+      environment.sessionVariables.PLASMA_USE_QT_SCALING = mkIf cfg.useQtScaling "1";
+
+      # Enable GTK applications to load SVG icons
+      services.xserver.gdk-pixbuf.modulePackages = [ pkgs.librsvg ];
+
+      fonts.fonts = with pkgs; [ noto-fonts hack-font ];
+      fonts.fontconfig.defaultFonts = {
+        monospace = [ "Hack" "Noto Sans Mono" ];
+        sansSerif = [ "Noto Sans" ];
+        serif = [ "Noto Serif" ];
+      };
+
+      programs.ssh.askPassword = mkDefault "${plasma5.ksshaskpass.out}/bin/ksshaskpass";
+
+      # Enable helpful DBus services.
+      services.accounts-daemon.enable = true;
+      # when changing an account picture the accounts-daemon reads a temporary file containing the image which systemsettings5 may place under /tmp
+      systemd.services.accounts-daemon.serviceConfig.PrivateTmp = false;
+      services.udisks2.enable = true;
+      services.upower.enable = config.powerManagement.enable;
+      services.system-config-printer.enable = mkIf config.services.printing.enable (mkDefault true);
+      services.xserver.libinput.enable = mkDefault true;
+
+      # Extra UDEV rules used by Solid
+      services.udev.packages = [
+        # libmtp has "bin", "dev", "out" outputs. UDEV rules file is in "out".
+        pkgs.libmtp.out
+        pkgs.media-player-info
+      ];
+
+      services.xserver.displayManager.sddm = {
+        theme = mkDefault "breeze";
+      };
+
+      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;
+
+      systemd.user.services = {
+        plasma-early-setup = mkIf cfg.runUsingSystemd {
+          description = "Early Plasma setup";
+          wantedBy = [ "graphical-session-pre.target" ];
+          serviceConfig.Type = "oneshot";
+          script = activationScript;
+        };
+      };
+
+      xdg.portal.enable = true;
+      xdg.portal.extraPortals = [ plasma5.xdg-desktop-portal-kde ];
+
+      # 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;
+
+      environment.etc = {
+        "xdg/kwinrc".text     = lib.generators.toINI {} cfg.kwinrc;
+        "xdg/kdeglobals".text = lib.generators.toINI {} cfg.kdeglobals;
+      };
+    })
+
+    # Plasma Desktop
+    (mkIf cfg.enable {
+
+      # Seed our configuration into nixos-generate-config
+      system.nixos-generate-config.desktopConfiguration = [
+        ''
+          # Enable the Plasma 5 Desktop Environment.
+          services.xserver.displayManager.sddm.enable = true;
+          services.xserver.desktopManager.plasma5.enable = true;
+        ''
+      ];
+
+      services.xserver.displayManager.sessionPackages = [ pkgs.libsForQt5.plasma5.plasma-workspace ];
+      # Default to be `plasma` (X11) instead of `plasmawayland`, since plasma wayland currently has
+      # many tiny bugs.
+      # See: https://github.com/NixOS/nixpkgs/issues/143272
+      services.xserver.displayManager.defaultSession = mkDefault "plasma";
+
+      environment.systemPackages =
+        with libsForQt5;
+        with plasma5; with kdeGear; with kdeFrameworks;
+        [
+          ksystemstats
+          kinfocenter
+          kmenuedit
+          plasma-systemmonitor
+          spectacle
+          systemsettings
+
+          dolphin
+          dolphin-plugins
+          ffmpegthumbs
+          kdegraphics-thumbnailers
+          khelpcenter
+          kio-extras
+          print-manager
+
+          elisa
+          gwenview
+          okular
+        ]
+      ;
+
+      systemd.user.services = {
+        plasma-run-with-systemd = {
+          description = "Run KDE Plasma via systemd";
+          wantedBy = [ "basic.target" ];
+          serviceConfig.Type = "oneshot";
+          script = ''
+            ${set_XDG_CONFIG_HOME}
+
+            ${kdeFrameworks.kconfig}/bin/kwriteconfig5 \
+              --file startkderc --group General --key systemdBoot ${lib.boolToString cfg.runUsingSystemd}
+          '';
+        };
+      };
+    })
+
+    # Plasma Mobile
+    (mkIf cfg.mobile.enable {
+      assertions = [
+        {
+          # The user interface breaks without NetworkManager
+          assertion = config.networking.networkmanager.enable;
+          message = "Plasma Mobile requires NetworkManager.";
+        }
+        {
+          # The user interface breaks without bluetooth
+          assertion = config.hardware.bluetooth.enable;
+          message = "Plasma Mobile requires Bluetooth.";
+        }
+        {
+          # The user interface breaks without pulse
+          assertion = config.hardware.pulseaudio.enable;
+          message = "Plasma Mobile requires pulseaudio.";
+        }
+      ];
+
+      environment.systemPackages =
+        with libsForQt5;
+        with plasma5; with kdeApplications; with kdeFrameworks;
+        [
+          # Basic packages without which Plasma Mobile fails to work properly.
+          plasma-phone-components
+          plasma-nano
+          pkgs.maliit-framework
+          pkgs.maliit-keyboard
+        ]
+        ++ lib.optionals (cfg.mobile.installRecommendedSoftware) (with libsForQt5.plasmaMobileGear;[
+          # Additional software made for Plasma Mobile.
+          alligator
+          angelfish
+          audiotube
+          calindori
+          kalk
+          kasts
+          kclock
+          keysmith
+          koko
+          krecorder
+          ktrip
+          kweather
+          plasma-dialer
+          plasma-phonebook
+          plasma-settings
+          spacebar
+        ])
+      ;
+
+      # The following services are needed or the UI is broken.
+      hardware.bluetooth.enable = true;
+      hardware.pulseaudio.enable = true;
+      networking.networkmanager.enable = true;
+
+      # Recommendations can be found here:
+      #  - https://invent.kde.org/plasma-mobile/plasma-phone-settings/-/tree/master/etc/xdg
+      # This configuration is the minimum required for Plasma Mobile to *work*.
+      services.xserver.desktopManager.plasma5 = {
+        kdeglobals = {
+          KDE = {
+            # This forces a numeric PIN for the lockscreen, which is the
+            # recommendation from upstream.
+            LookAndFeelPackage = lib.mkDefault "org.kde.plasma.phone";
+          };
+        };
+        kwinrc = {
+          Windows = {
+            # Forces windows to be maximized
+            Placement = lib.mkDefault "Maximizing";
+          };
+          "org.kde.kdecoration2" = {
+            # No decorations (title bar)
+            NoPlugin = lib.mkDefault "true";
+          };
+        };
+      };
+
+      services.xserver.displayManager.sessionPackages = [ pkgs.libsForQt5.plasma5.plasma-phone-components ];
+    })
+  ];
+}
diff --git a/nixos/modules/services/x11/desktop-managers/retroarch.nix b/nixos/modules/services/x11/desktop-managers/retroarch.nix
new file mode 100644
index 00000000000..d471673d452
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/retroarch.nix
@@ -0,0 +1,40 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.xserver.desktopManager.retroarch;
+
+in {
+  options.services.xserver.desktopManager.retroarch = {
+    enable = mkEnableOption "RetroArch";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.retroarch;
+      defaultText = literalExpression "pkgs.retroarch";
+      example = literalExpression "pkgs.retroarch-full";
+      description = "RetroArch package to use.";
+    };
+
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = [ "--verbose" "--host" ];
+      description = "Extra arguments to pass to RetroArch.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.desktopManager.session = [{
+      name = "RetroArch";
+      start = ''
+        ${cfg.package}/bin/retroarch -f ${escapeShellArgs cfg.extraArgs} &
+        waitPID=$!
+      '';
+    }];
+
+    environment.systemPackages = [ cfg.package ];
+  };
+
+  meta.maintainers = with maintainers; [ j0hax ];
+}
diff --git a/nixos/modules/services/x11/desktop-managers/surf-display.nix b/nixos/modules/services/x11/desktop-managers/surf-display.nix
new file mode 100644
index 00000000000..4b5a04f988b
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/surf-display.nix
@@ -0,0 +1,128 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.desktopManager.surf-display;
+
+  surfDisplayConf = ''
+    # Surf Kiosk Display: Wrap around surf browser and turn your
+    # system into a browser screen in KIOSK-mode.
+
+    # default download URI for all display screens if not configured individually
+    DEFAULT_WWW_URI="${cfg.defaultWwwUri}"
+
+    # Enforce fixed resolution for all displays (default: not set):
+    #DEFAULT_RESOLUTION="1920x1080"
+
+    # HTTP proxy URL, if needed (default: not set).
+    #HTTP_PROXY_URL="http://webcache:3128"
+
+    # Setting for internal inactivity timer to restart surf-display
+    # if the user goes inactive/idle.
+    INACTIVITY_INTERVAL="${builtins.toString cfg.inactivityInterval}"
+
+    # log to syslog instead of .xsession-errors
+    LOG_TO_SYSLOG="yes"
+
+    # Launch pulseaudio daemon if not already running.
+    WITH_PULSEAUDIO="yes"
+
+    # screensaver settings, see "man 1 xset" for possible options
+    SCREENSAVER_SETTINGS="${cfg.screensaverSettings}"
+
+    # disable right and middle pointer device click in browser sessions while keeping
+    # scrolling wheels' functionality intact... (consider "pointer" subcommand on
+    # xmodmap man page for details).
+    POINTER_BUTTON_MAP="${cfg.pointerButtonMap}"
+
+    # Hide idle mouse pointer.
+    HIDE_IDLE_POINTER="${cfg.hideIdlePointer}"
+
+    ${cfg.extraConfig}
+  '';
+
+in {
+  options = {
+    services.xserver.desktopManager.surf-display = {
+      enable = mkEnableOption "surf-display as a kiosk browser session";
+
+      defaultWwwUri = mkOption {
+        type = types.str;
+        default = "${pkgs.surf-display}/share/surf-display/empty-page.html";
+        defaultText = literalExpression ''"''${pkgs.surf-display}/share/surf-display/empty-page.html"'';
+        example = "https://www.example.com/";
+        description = "Default URI to display.";
+      };
+
+      inactivityInterval = mkOption {
+        type = types.int;
+        default = 300;
+        example = 0;
+        description = ''
+          Setting for internal inactivity timer to restart surf-display if the
+          user goes inactive/idle to get a fresh session for the next user of
+          the kiosk.
+
+          If this value is set to zero, the whole feature of restarting due to
+          inactivity is disabled.
+        '';
+      };
+
+      screensaverSettings = mkOption {
+        type = types.separatedString " ";
+        default = "";
+        description = ''
+          Screensaver settings, see <literal>man 1 xset</literal> for possible options.
+        '';
+      };
+
+      pointerButtonMap = mkOption {
+        type = types.str;
+        default = "1 0 0 4 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0";
+        description = ''
+          Disable right and middle pointer device click in browser sessions
+          while keeping scrolling wheels' functionality intact. See pointer
+          subcommand on <literal>man xmodmap</literal> for details.
+        '';
+      };
+
+      hideIdlePointer = mkOption {
+        type = types.str;
+        default = "yes";
+        example = "no";
+        description = "Hide idle mouse pointer.";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          # Enforce fixed resolution for all displays (default: not set):
+          DEFAULT_RESOLUTION="1920x1080"
+
+          # HTTP proxy URL, if needed (default: not set).
+          HTTP_PROXY_URL="http://webcache:3128"
+
+          # Configure individual display screens with host specific parameters:
+          DISPLAYS['display-host-0']="www_uri=https://www.displayserver.comany.net/display-1/index.html"
+          DISPLAYS['display-host-1']="www_uri=https://www.displayserver.comany.net/display-2/index.html"
+          DISPLAYS['display-host-2']="www_uri=https://www.displayserver.comany.net/display-3/index.html|res=1920x1280"
+          DISPLAYS['display-host-3']="www_uri=https://www.displayserver.comany.net/display-4/index.html"|res=1280x1024"
+          DISPLAYS['display-host-local-file']="www_uri=file:///usr/share/doc/surf-display/empty-page.html"
+        '';
+        description = ''
+          Extra configuration options to append to <literal>/etc/default/surf-display</literal>.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.displayManager.sessionPackages = [
+      pkgs.surf-display
+    ];
+
+    environment.etc."default/surf-display".text = surfDisplayConf;
+  };
+}
diff --git a/nixos/modules/services/x11/desktop-managers/xfce.nix b/nixos/modules/services/x11/desktop-managers/xfce.nix
new file mode 100644
index 00000000000..3cf92f98c56
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/xfce.nix
@@ -0,0 +1,172 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.desktopManager.xfce;
+in
+
+{
+
+  meta = {
+    maintainers = teams.xfce.members;
+  };
+
+  imports = [
+    # added 2019-08-18
+    # needed to preserve some semblance of UI familarity
+    # with original XFCE module
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "xfce4-14" "extraSessionCommands" ]
+      [ "services" "xserver" "displayManager" "sessionCommands" ])
+
+    # added 2019-11-04
+    # xfce4-14 module removed and promoted to xfce.
+    # Needed for configs that used xfce4-14 module to migrate to this one.
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "xfce4-14" "enable" ]
+      [ "services" "xserver" "desktopManager" "xfce" "enable" ])
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "xfce4-14" "noDesktop" ]
+      [ "services" "xserver" "desktopManager" "xfce" "noDesktop" ])
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "xfce4-14" "enableXfwm" ]
+      [ "services" "xserver" "desktopManager" "xfce" "enableXfwm" ])
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "xfce" "extraSessionCommands" ]
+      [ "services" "xserver" "displayManager" "sessionCommands" ])
+    (mkRemovedOptionModule [ "services" "xserver" "desktopManager" "xfce" "screenLock" ] "")
+  ];
+
+  options = {
+    services.xserver.desktopManager.xfce = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable the Xfce desktop environment.";
+      };
+
+      thunarPlugins = mkOption {
+        default = [];
+        type = types.listOf types.package;
+        example = literalExpression "[ pkgs.xfce.thunar-archive-plugin ]";
+        description = ''
+          A list of plugin that should be installed with Thunar.
+        '';
+      };
+
+      noDesktop = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Don't install XFCE desktop components (xfdesktop and panel).";
+      };
+
+      enableXfwm = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Enable the XFWM (default) window manager.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs.xfce // pkgs; [
+      glib # for gsettings
+      gtk3.out # gtk-update-icon-cache
+
+      gnome.gnome-themes-extra
+      gnome.adwaita-icon-theme
+      hicolor-icon-theme
+      tango-icon-theme
+      xfce4-icon-theme
+
+      desktop-file-utils
+      shared-mime-info # for update-mime-database
+
+      # For a polkit authentication agent
+      polkit_gnome
+
+      # Needed by Xfce's xinitrc script
+      xdg-user-dirs # Update user dirs as described in https://freedesktop.org/wiki/Software/xdg-user-dirs/
+
+      exo
+      garcon
+      libxfce4ui
+      xfconf
+
+      mousepad
+      parole
+      ristretto
+      xfce4-appfinder
+      xfce4-notifyd
+      xfce4-screenshooter
+      xfce4-session
+      xfce4-settings
+      xfce4-taskmanager
+      xfce4-terminal
+
+      (thunar.override { thunarPlugins = cfg.thunarPlugins; })
+    ] # TODO: NetworkManager doesn't belong here
+      ++ optional config.networking.networkmanager.enable networkmanagerapplet
+      ++ optional config.powerManagement.enable xfce4-power-manager
+      ++ optionals config.hardware.pulseaudio.enable [
+        pavucontrol
+        # volume up/down keys support:
+        # xfce4-pulseaudio-plugin includes all the functionalities of xfce4-volumed-pulse
+        # but can only be used with xfce4-panel, so for no-desktop usage we still include
+        # xfce4-volumed-pulse
+        (if cfg.noDesktop then xfce4-volumed-pulse else xfce4-pulseaudio-plugin)
+      ] ++ optionals cfg.enableXfwm [
+        xfwm4
+        xfwm4-themes
+      ] ++ optionals (!cfg.noDesktop) [
+        xfce4-panel
+        xfdesktop
+      ];
+
+    environment.pathsToLink = [
+      "/share/xfce4"
+      "/lib/xfce4"
+      "/share/gtksourceview-3.0"
+      "/share/gtksourceview-4.0"
+    ];
+
+    services.xserver.desktopManager.session = [{
+      name = "xfce";
+      desktopNames = [ "XFCE" ];
+      bgSupport = true;
+      start = ''
+        ${pkgs.runtimeShell} ${pkgs.xfce.xfce4-session.xinitrc} &
+        waitPID=$!
+      '';
+    }];
+
+    services.xserver.updateDbusEnvironment = true;
+    services.xserver.gdk-pixbuf.modulePackages = [ pkgs.librsvg ];
+
+    # Enable helpful DBus services.
+    services.udisks2.enable = true;
+    security.polkit.enable = true;
+    services.accounts-daemon.enable = true;
+    services.upower.enable = config.powerManagement.enable;
+    services.gnome.glib-networking.enable = true;
+    services.gvfs.enable = true;
+    services.tumbler.enable = true;
+    services.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true));
+    services.xserver.libinput.enable = mkDefault true; # used in xfce4-settings-manager
+
+    # Enable default programs
+    programs.dconf.enable = true;
+
+    # Shell integration for VTE terminals
+    programs.bash.vteIntegration = mkDefault true;
+    programs.zsh.vteIntegration = mkDefault true;
+
+    # Systemd services
+    systemd.packages = with pkgs.xfce; [
+      (thunar.override { thunarPlugins = cfg.thunarPlugins; })
+      xfce4-notifyd
+    ];
+
+  };
+}
diff --git a/nixos/modules/services/x11/desktop-managers/xterm.nix b/nixos/modules/services/x11/desktop-managers/xterm.nix
new file mode 100644
index 00000000000..3424ee1b0e1
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/xterm.nix
@@ -0,0 +1,38 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.desktopManager.xterm;
+  xSessionEnabled = config.services.xserver.enable;
+
+in
+
+{
+  options = {
+
+    services.xserver.desktopManager.xterm.enable = mkOption {
+      type = types.bool;
+      default = versionOlder config.system.stateVersion "19.09" && xSessionEnabled;
+      defaultText = literalExpression ''versionOlder config.system.stateVersion "19.09" && config.services.xserver.enable;'';
+      description = "Enable a xterm terminal as a desktop manager.";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    services.xserver.desktopManager.session = singleton
+      { name = "xterm";
+        start = ''
+          ${pkgs.xterm}/bin/xterm -ls &
+          waitPID=$!
+        '';
+      };
+
+    environment.systemPackages = [ pkgs.xterm ];
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/display-managers/account-service-util.nix b/nixos/modules/services/x11/display-managers/account-service-util.nix
new file mode 100644
index 00000000000..861976d1186
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/account-service-util.nix
@@ -0,0 +1,44 @@
+{ accountsservice
+, glib
+, gobject-introspection
+, python3
+, wrapGAppsHook
+, lib
+}:
+
+python3.pkgs.buildPythonApplication {
+  name = "set-session";
+
+  format = "other";
+
+  src = ./set-session.py;
+
+  dontUnpack = true;
+
+  strictDeps = false;
+
+  nativeBuildInputs = [
+    wrapGAppsHook
+    gobject-introspection
+  ];
+
+  buildInputs = [
+    accountsservice
+    glib
+  ];
+
+  propagatedBuildInputs = with python3.pkgs; [
+    pygobject3
+    ordered-set
+  ];
+
+  installPhase = ''
+    mkdir -p $out/bin
+    cp $src $out/bin/set-session
+    chmod +x $out/bin/set-session
+  '';
+
+  meta = with lib; {
+    maintainers = with maintainers; [ ] ++ teams.pantheon.members;
+  };
+}
diff --git a/nixos/modules/services/x11/display-managers/default.nix b/nixos/modules/services/x11/display-managers/default.nix
new file mode 100644
index 00000000000..a5db3dd5dd4
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/default.nix
@@ -0,0 +1,503 @@
+# This module declares the options to define a *display manager*, the
+# program responsible for handling X logins (such as LightDM, GDM, or SDDM).
+# The display manager allows the user to select a *session
+# type*. When the user logs in, the display manager starts the
+# *session script* ("xsession" below) to launch the selected session
+# type. The session type defines two things: the *desktop manager*
+# (e.g., KDE, Gnome or a plain xterm), and optionally the *window
+# manager* (e.g. kwin or twm).
+
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver;
+  opt = options.services.xserver;
+  xorg = pkgs.xorg;
+
+  fontconfig = config.fonts.fontconfig;
+  xresourcesXft = pkgs.writeText "Xresources-Xft" ''
+    Xft.antialias: ${if fontconfig.antialias then "1" else "0"}
+    Xft.rgba: ${fontconfig.subpixel.rgba}
+    Xft.lcdfilter: lcd${fontconfig.subpixel.lcdfilter}
+    Xft.hinting: ${if fontconfig.hinting.enable then "1" else "0"}
+    Xft.autohint: ${if fontconfig.hinting.autohint then "1" else "0"}
+    Xft.hintstyle: hintslight
+  '';
+
+  # file provided by services.xserver.displayManager.sessionData.wrapper
+  xsessionWrapper = pkgs.writeScript "xsession-wrapper"
+    ''
+      #! ${pkgs.bash}/bin/bash
+
+      # Shared environment setup for graphical sessions.
+
+      . /etc/profile
+      cd "$HOME"
+
+      # Allow the user to execute commands at the beginning of the X session.
+      if test -f ~/.xprofile; then
+          source ~/.xprofile
+      fi
+
+      ${optionalString cfg.displayManager.job.logToJournal ''
+        if [ -z "$_DID_SYSTEMD_CAT" ]; then
+          export _DID_SYSTEMD_CAT=1
+          exec ${config.systemd.package}/bin/systemd-cat -t xsession "$0" "$@"
+        fi
+      ''}
+
+      ${optionalString cfg.displayManager.job.logToFile ''
+        exec &> >(tee ~/.xsession-errors)
+      ''}
+
+      # Load X defaults. This should probably be safe on wayland too.
+      ${xorg.xrdb}/bin/xrdb -merge ${xresourcesXft}
+      if test -e ~/.Xresources; then
+          ${xorg.xrdb}/bin/xrdb -merge ~/.Xresources
+      elif test -e ~/.Xdefaults; then
+          ${xorg.xrdb}/bin/xrdb -merge ~/.Xdefaults
+      fi
+
+      # Import environment variables into the systemd user environment.
+      ${optionalString (cfg.displayManager.importedVariables != []) (
+        "/run/current-system/systemd/bin/systemctl --user import-environment "
+          + toString (unique cfg.displayManager.importedVariables)
+      )}
+
+      # Speed up application start by 50-150ms according to
+      # http://kdemonkey.blogspot.nl/2008/04/magic-trick.html
+      compose_cache="''${XCOMPOSECACHE:-$HOME/.compose-cache}"
+      mkdir -p "$compose_cache"
+      # To avoid accidentally deleting a wrongly set up XCOMPOSECACHE directory,
+      # defensively try to delete cache *files* only, following the file format specified in
+      # https://gitlab.freedesktop.org/xorg/lib/libx11/-/blob/master/modules/im/ximcp/imLcIm.c#L353-358
+      # sprintf (*res, "%s/%c%d_%03x_%08x_%08x", dir, _XimGetMyEndian(), XIM_CACHE_VERSION, (unsigned int)sizeof (DefTree), hash, hash2);
+      ${pkgs.findutils}/bin/find "$compose_cache" -maxdepth 1 -regextype posix-extended -regex '.*/[Bl][0-9]+_[0-9a-f]{3}_[0-9a-f]{8}_[0-9a-f]{8}' -delete
+      unset compose_cache
+
+      # Work around KDE errors when a user first logs in and
+      # .local/share doesn't exist yet.
+      mkdir -p "''${XDG_DATA_HOME:-$HOME/.local/share}"
+
+      unset _DID_SYSTEMD_CAT
+
+      ${cfg.displayManager.sessionCommands}
+
+      # Start systemd user services for graphical sessions
+      /run/current-system/systemd/bin/systemctl --user start graphical-session.target
+
+      # Allow the user to setup a custom session type.
+      if test -x ~/.xsession; then
+          eval exec ~/.xsession "$@"
+      fi
+
+      if test "$1"; then
+          # Run the supplied session command. Remove any double quotes with eval.
+          eval exec "$@"
+      else
+          # TODO: Do we need this? Should not the session always exist?
+          echo "error: unknown session $1" 1>&2
+          exit 1
+      fi
+    '';
+
+  installedSessions = pkgs.runCommand "desktops"
+    { # trivial derivation
+      preferLocalBuild = true;
+      allowSubstitutes = false;
+    }
+    ''
+      mkdir -p "$out/share/"{xsessions,wayland-sessions}
+
+      ${concatMapStrings (pkg: ''
+        for n in ${concatStringsSep " " pkg.providedSessions}; do
+          if ! test -f ${pkg}/share/wayland-sessions/$n.desktop -o \
+                    -f ${pkg}/share/xsessions/$n.desktop; then
+            echo "Couldn't find provided session name, $n.desktop, in session package ${pkg.name}:"
+            echo "  ${pkg}"
+            return 1
+          fi
+        done
+
+        if test -d ${pkg}/share/xsessions; then
+          ${pkgs.buildPackages.xorg.lndir}/bin/lndir ${pkg}/share/xsessions $out/share/xsessions
+        fi
+        if test -d ${pkg}/share/wayland-sessions; then
+          ${pkgs.buildPackages.xorg.lndir}/bin/lndir ${pkg}/share/wayland-sessions $out/share/wayland-sessions
+        fi
+      '') cfg.displayManager.sessionPackages}
+    '';
+
+  dmDefault = cfg.desktopManager.default;
+  # fallback default for cases when only default wm is set
+  dmFallbackDefault = if dmDefault != null then dmDefault else "none";
+  wmDefault = cfg.windowManager.default;
+
+  defaultSessionFromLegacyOptions = dmFallbackDefault + optionalString (wmDefault != null && wmDefault != "none") "+${wmDefault}";
+
+in
+
+{
+  options = {
+
+    services.xserver.displayManager = {
+
+      xauthBin = mkOption {
+        internal = true;
+        default = "${xorg.xauth}/bin/xauth";
+        defaultText = literalExpression ''"''${pkgs.xorg.xauth}/bin/xauth"'';
+        description = "Path to the <command>xauth</command> program used by display managers.";
+      };
+
+      xserverBin = mkOption {
+        type = types.path;
+        description = "Path to the X server used by display managers.";
+      };
+
+      xserverArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "-ac" "-logverbose" "-verbose" "-nolisten tcp" ];
+        description = "List of arguments for the X server.";
+      };
+
+      setupCommands = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Shell commands executed just after the X server has started.
+
+          This option is only effective for display managers for which this feature
+          is supported; currently these are LightDM, GDM and SDDM.
+        '';
+      };
+
+      sessionCommands = mkOption {
+        type = types.lines;
+        default = "";
+        example =
+          ''
+            xmessage "Hello World!" &
+          '';
+        description = ''
+          Shell commands executed just before the window or desktop manager is
+          started. These commands are not currently sourced for Wayland sessions.
+        '';
+      };
+
+      hiddenUsers = mkOption {
+        type = types.listOf types.str;
+        default = [ "nobody" ];
+        description = ''
+          A list of users which will not be shown in the display manager.
+        '';
+      };
+
+      sessionPackages = mkOption {
+        type = with types; listOf (package // {
+          description = "package with provided sessions";
+          check = p: assertMsg
+            (package.check p && p ? providedSessions
+            && p.providedSessions != [] && all isString p.providedSessions)
+            ''
+              Package, '${p.name}', did not specify any session names, as strings, in
+              'passthru.providedSessions'. This is required when used as a session package.
+
+              The session names can be looked up in:
+                ${p}/share/xsessions
+                ${p}/share/wayland-sessions
+           '';
+        });
+        default = [];
+        description = ''
+          A list of packages containing x11 or wayland session files to be passed to the display manager.
+        '';
+      };
+
+      session = mkOption {
+        default = [];
+        type = types.listOf types.attrs;
+        example = literalExpression
+          ''
+            [ { manage = "desktop";
+                name = "xterm";
+                start = '''
+                  ''${pkgs.xterm}/bin/xterm -ls &
+                  waitPID=$!
+                ''';
+              }
+            ]
+          '';
+        description = ''
+          List of sessions supported with the command used to start each
+          session.  Each session script can set the
+          <varname>waitPID</varname> shell variable to make this script
+          wait until the end of the user session.  Each script is used
+          to define either a window manager or a desktop manager.  These
+          can be differentiated by setting the attribute
+          <varname>manage</varname> either to <literal>"window"</literal>
+          or <literal>"desktop"</literal>.
+
+          The list of desktop manager and window manager should appear
+          inside the display manager with the desktop manager name
+          followed by the window manager name.
+        '';
+      };
+
+      sessionData = mkOption {
+        description = "Data exported for display managers’ convenience";
+        internal = true;
+        default = {};
+        apply = val: {
+          wrapper = xsessionWrapper;
+          desktops = installedSessions;
+          sessionNames = concatMap (p: p.providedSessions) cfg.displayManager.sessionPackages;
+          # We do not want to force users to set defaultSession when they have only single DE.
+          autologinSession =
+            if cfg.displayManager.defaultSession != null then
+              cfg.displayManager.defaultSession
+            else if cfg.displayManager.sessionData.sessionNames != [] then
+              head cfg.displayManager.sessionData.sessionNames
+            else
+              null;
+        };
+      };
+
+      defaultSession = mkOption {
+        type = with types; nullOr str // {
+          description = "session name";
+          check = d:
+            assertMsg (d != null -> (str.check d && elem d cfg.displayManager.sessionData.sessionNames)) ''
+                Default graphical session, '${d}', not found.
+                Valid names for 'services.xserver.displayManager.defaultSession' are:
+                  ${concatStringsSep "\n  " cfg.displayManager.sessionData.sessionNames}
+              '';
+        };
+        default =
+          if dmDefault != null || wmDefault != null then
+            defaultSessionFromLegacyOptions
+          else
+            null;
+        defaultText = literalDocBook ''
+          Taken from display manager settings or window manager settings, if either is set.
+        '';
+        example = "gnome";
+        description = ''
+          Graphical session to pre-select in the session chooser (only effective for GDM, LightDM and SDDM).
+
+          On GDM, LightDM and SDDM, it will also be used as a session for auto-login.
+        '';
+      };
+
+      importedVariables = mkOption {
+        type = types.listOf (types.strMatching "[a-zA-Z_][a-zA-Z0-9_]*");
+        visible = false;
+        description = ''
+          Environment variables to import into the systemd user environment.
+        '';
+      };
+
+      job = {
+
+        preStart = mkOption {
+          type = types.lines;
+          default = "";
+          example = "rm -f /var/log/my-display-manager.log";
+          description = "Script executed before the display manager is started.";
+        };
+
+        execCmd = mkOption {
+          type = types.str;
+          example = literalExpression ''"''${pkgs.lightdm}/bin/lightdm"'';
+          description = "Command to start the display manager.";
+        };
+
+        environment = mkOption {
+          type = types.attrsOf types.unspecified;
+          default = {};
+          description = "Additional environment variables needed by the display manager.";
+        };
+
+        logToFile = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether the display manager redirects the output of the
+            session script to <filename>~/.xsession-errors</filename>.
+          '';
+        };
+
+        logToJournal = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether the display manager redirects the output of the
+            session script to the systemd journal.
+          '';
+        };
+
+      };
+
+      # Configuration for automatic login. Common for all DM.
+      autoLogin = mkOption {
+        type = types.submodule ({ config, options, ... }: {
+          options = {
+            enable = mkOption {
+              type = types.bool;
+              default = config.user != null;
+              defaultText = literalExpression "config.${options.user} != null";
+              description = ''
+                Automatically log in as <option>autoLogin.user</option>.
+              '';
+            };
+
+            user = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = ''
+                User to be used for the automatic login.
+              '';
+            };
+          };
+        });
+
+        default = {};
+        description = ''
+          Auto login configuration attrset.
+        '';
+      };
+
+    };
+
+  };
+
+  config = {
+    assertions = [
+      { assertion = cfg.displayManager.autoLogin.enable -> cfg.displayManager.autoLogin.user != null;
+        message = ''
+          services.xserver.displayManager.autoLogin.enable requires services.xserver.displayManager.autoLogin.user to be set
+        '';
+      }
+      {
+        assertion = cfg.desktopManager.default != null || cfg.windowManager.default != null -> cfg.displayManager.defaultSession == defaultSessionFromLegacyOptions;
+        message = "You cannot use both services.xserver.displayManager.defaultSession option and legacy options (services.xserver.desktopManager.default and services.xserver.windowManager.default).";
+      }
+    ];
+
+    warnings =
+      mkIf (dmDefault != null || wmDefault != null) [
+        ''
+          The following options are deprecated:
+            ${concatStringsSep "\n  " (map ({c, t}: t) (filter ({c, t}: c != null) [
+            { c = dmDefault; t = "- services.xserver.desktopManager.default"; }
+            { c = wmDefault; t = "- services.xserver.windowManager.default"; }
+            ]))}
+          Please use
+            services.xserver.displayManager.defaultSession = "${defaultSessionFromLegacyOptions}";
+          instead.
+        ''
+      ];
+
+    services.xserver.displayManager.xserverBin = "${xorg.xorgserver.out}/bin/X";
+
+    services.xserver.displayManager.importedVariables = [
+      # This is required by user units using the session bus.
+      "DBUS_SESSION_BUS_ADDRESS"
+      # These are needed by the ssh-agent unit.
+      "DISPLAY"
+      "XAUTHORITY"
+      # This is required to specify session within user units (e.g. loginctl lock-session).
+      "XDG_SESSION_ID"
+    ];
+
+    systemd.user.targets.graphical-session = {
+      unitConfig = {
+        RefuseManualStart = false;
+        StopWhenUnneeded = false;
+      };
+    };
+
+    # Create desktop files and scripts for starting sessions for WMs/DMs
+    # that do not have upstream session files (those defined using services.{display,desktop,window}Manager.session options).
+    services.xserver.displayManager.sessionPackages =
+      let
+        dms = filter (s: s.manage == "desktop") cfg.displayManager.session;
+        wms = filter (s: s.manage == "window") cfg.displayManager.session;
+
+        # Script responsible for starting the window manager and the desktop manager.
+        xsession = dm: wm: pkgs.writeScript "xsession" ''
+          #! ${pkgs.bash}/bin/bash
+
+          # Legacy session script used to construct .desktop files from
+          # `services.xserver.displayManager.session` entries. Called from
+          # `sessionWrapper`.
+
+          # Start the window manager.
+          ${wm.start}
+
+          # Start the desktop manager.
+          ${dm.start}
+
+          ${optionalString cfg.updateDbusEnvironment ''
+            ${lib.getBin pkgs.dbus}/bin/dbus-update-activation-environment --systemd --all
+          ''}
+
+          test -n "$waitPID" && wait "$waitPID"
+
+          /run/current-system/systemd/bin/systemctl --user stop graphical-session.target
+
+          exit 0
+        '';
+      in
+        # We will generate every possible pair of WM and DM.
+        concatLists (
+            builtins.map
+            ({dm, wm}: let
+              sessionName = "${dm.name}${optionalString (wm.name != "none") ("+" + wm.name)}";
+              script = xsession dm wm;
+              desktopNames = if dm ? desktopNames
+                             then concatStringsSep ";" dm.desktopNames
+                             else sessionName;
+            in
+              optional (dm.name != "none" || wm.name != "none")
+                (pkgs.writeTextFile {
+                  name = "${sessionName}-xsession";
+                  destination = "/share/xsessions/${sessionName}.desktop";
+                  # Desktop Entry Specification:
+                  # - https://standards.freedesktop.org/desktop-entry-spec/latest/
+                  # - https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
+                  text = ''
+                    [Desktop Entry]
+                    Version=1.0
+                    Type=XSession
+                    TryExec=${script}
+                    Exec=${script}
+                    Name=${sessionName}
+                    DesktopNames=${desktopNames}
+                  '';
+                } // {
+                  providedSessions = [ sessionName ];
+                })
+            )
+            (cartesianProductOfSets { dm = dms; wm = wms; })
+          );
+
+    # Make xsessions and wayland sessions available in XDG_DATA_DIRS
+    # as some programs have behavior that depends on them being present
+    environment.sessionVariables.XDG_DATA_DIRS = [
+      "${cfg.displayManager.sessionData.desktops}/share"
+    ];
+  };
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "xserver" "displayManager" "desktopManagerHandlesLidAndPower" ]
+     "The option is no longer necessary because all display managers have already delegated lid management to systemd.")
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "job" "logsXsession" ] [ "services" "xserver" "displayManager" "job" "logToFile" ])
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "logToJournal" ] [ "services" "xserver" "displayManager" "job" "logToJournal" ])
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "extraSessionFilesPackages" ] [ "services" "xserver" "displayManager" "sessionPackages" ])
+  ];
+
+}
diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix
new file mode 100644
index 00000000000..b1dc6643be8
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/gdm.nix
@@ -0,0 +1,331 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.displayManager;
+  gdm = pkgs.gnome.gdm;
+  settingsFormat = pkgs.formats.ini { };
+  configFile = settingsFormat.generate "custom.conf" cfg.gdm.settings;
+
+  xSessionWrapper = if (cfg.setupCommands == "") then null else
+    pkgs.writeScript "gdm-x-session-wrapper" ''
+      #!${pkgs.bash}/bin/bash
+      ${cfg.setupCommands}
+      exec "$@"
+    '';
+
+  # Solves problems like:
+  # https://wiki.archlinux.org/index.php/Talk:Bluetooth_headset#GDMs_pulseaudio_instance_captures_bluetooth_headset
+  # Instead of blacklisting plugins, we use Fedora's PulseAudio configuration for GDM:
+  # https://src.fedoraproject.org/rpms/gdm/blob/master/f/default.pa-for-gdm
+  pulseConfig = pkgs.writeText "default.pa" ''
+    load-module module-device-restore
+    load-module module-card-restore
+    load-module module-udev-detect
+    load-module module-native-protocol-unix
+    load-module module-default-device-restore
+    load-module module-always-sink
+    load-module module-intended-roles
+    load-module module-suspend-on-idle
+    load-module module-position-event-sounds
+  '';
+
+  defaultSessionName = config.services.xserver.displayManager.defaultSession;
+
+  setSessionScript = pkgs.callPackage ./account-service-util.nix { };
+in
+
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "gdm" "autoLogin" "enable" ] [
+      "services"
+      "xserver"
+      "displayManager"
+      "autoLogin"
+      "enable"
+    ])
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "gdm" "autoLogin" "user" ] [
+      "services"
+      "xserver"
+      "displayManager"
+      "autoLogin"
+      "user"
+    ])
+
+    (mkRemovedOptionModule [ "services" "xserver" "displayManager" "gdm" "nvidiaWayland" ] "We defer to GDM whether Wayland should be enabled.")
+  ];
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  ###### interface
+
+  options = {
+
+    services.xserver.displayManager.gdm = {
+
+      enable = mkEnableOption "GDM, the GNOME Display Manager";
+
+      debug = mkEnableOption "debugging messages in GDM";
+
+      # Auto login options specific to GDM
+      autoLogin.delay = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          Seconds of inactivity after which the autologin will be performed.
+        '';
+      };
+
+      wayland = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Allow GDM to run on Wayland instead of Xserver.
+        '';
+      };
+
+      autoSuspend = mkOption {
+        default = true;
+        description = ''
+          On the GNOME Display Manager login screen, suspend the machine after inactivity.
+          (Does not affect automatic suspend while logged in, or at lock screen.)
+        '';
+        type = types.bool;
+      };
+
+      settings = mkOption {
+        type = settingsFormat.type;
+        default = { };
+        example = {
+          debug.enable = true;
+        };
+        description = ''
+          Options passed to the gdm daemon.
+          See <link xlink:href="https://help.gnome.org/admin/gdm/stable/configuration.html.en#daemonconfig">here</link> for supported options.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.gdm.enable {
+
+    services.xserver.displayManager.lightdm.enable = false;
+
+    users.users.gdm =
+      { name = "gdm";
+        uid = config.ids.uids.gdm;
+        group = "gdm";
+        home = "/run/gdm";
+        description = "GDM user";
+      };
+
+    users.groups.gdm.gid = config.ids.gids.gdm;
+
+    # GDM needs different xserverArgs, presumable because using wayland by default.
+    services.xserver.tty = null;
+    services.xserver.display = null;
+    services.xserver.verbose = null;
+
+    services.xserver.displayManager.job =
+      {
+        environment = {
+          GDM_X_SERVER_EXTRA_ARGS = toString
+            (filter (arg: arg != "-terminate") cfg.xserverArgs);
+          # GDM is needed for gnome-login.session
+          XDG_DATA_DIRS = "${gdm}/share:${cfg.sessionData.desktops}/share";
+        } // optionalAttrs (xSessionWrapper != null) {
+          # Make GDM use this wrapper before running the session, which runs the
+          # configured setupCommands. This relies on a patched GDM which supports
+          # this environment variable.
+          GDM_X_SESSION_WRAPPER = "${xSessionWrapper}";
+        };
+        execCmd = "exec ${gdm}/bin/gdm";
+        preStart = optionalString (defaultSessionName != null) ''
+          # Set default session in session chooser to a specified values – basically ignore session history.
+          ${setSessionScript}/bin/set-session ${cfg.sessionData.autologinSession}
+        '';
+      };
+
+    systemd.tmpfiles.rules = [
+      "d /run/gdm/.config 0711 gdm gdm"
+    ] ++ optionals config.hardware.pulseaudio.enable [
+      "d /run/gdm/.config/pulse 0711 gdm gdm"
+      "L+ /run/gdm/.config/pulse/${pulseConfig.name} - - - - ${pulseConfig}"
+    ] ++ optionals config.services.gnome.gnome-initial-setup.enable [
+      # Create stamp file for gnome-initial-setup to prevent it starting in GDM.
+      "f /run/gdm/.config/gnome-initial-setup-done 0711 gdm gdm - yes"
+    ];
+
+    # Otherwise GDM will not be able to start correctly and display Wayland sessions
+    systemd.packages = with pkgs.gnome; [ gdm gnome-session gnome-shell ];
+    environment.systemPackages = [ pkgs.gnome.adwaita-icon-theme ];
+
+    # We dont use the upstream gdm service
+    # it has to be disabled since the gdm package has it
+    # https://github.com/NixOS/nixpkgs/issues/108672
+    systemd.services.gdm.enable = false;
+
+    systemd.services.display-manager.wants = [
+      # Because sd_login_monitor_new requires /run/systemd/machines
+      "systemd-machined.service"
+      # setSessionScript wants AccountsService
+      "accounts-daemon.service"
+    ];
+
+    systemd.services.display-manager.after = [
+      "rc-local.service"
+      "systemd-machined.service"
+      "systemd-user-sessions.service"
+      "getty@tty${gdm.initialVT}.service"
+      "plymouth-quit.service"
+      "plymouth-start.service"
+    ];
+    systemd.services.display-manager.conflicts = [
+      "getty@tty${gdm.initialVT}.service"
+      "plymouth-quit.service"
+    ];
+    systemd.services.display-manager.onFailure = [
+      "plymouth-quit.service"
+    ];
+
+    # Prevent nixos-rebuild switch from bringing down the graphical
+    # session. (If multi-user.target wants plymouth-quit.service which
+    # conflicts display-manager.service, then when nixos-rebuild
+    # switch starts multi-user.target, display-manager.service is
+    # stopped so plymouth-quit.service can be started.)
+    systemd.services.plymouth-quit.wantedBy = lib.mkForce [];
+
+    systemd.services.display-manager.serviceConfig = {
+      # Restart = "always"; - already defined in xserver.nix
+      KillMode = "mixed";
+      IgnoreSIGPIPE = "no";
+      BusName = "org.gnome.DisplayManager";
+      StandardError = "inherit";
+      ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
+      KeyringMode = "shared";
+      EnvironmentFile = "-/etc/locale.conf";
+    };
+
+    systemd.services.display-manager.path = [ pkgs.gnome.gnome-session ];
+
+    # Allow choosing an user account
+    services.accounts-daemon.enable = true;
+
+    services.dbus.packages = [ gdm ];
+
+    systemd.user.services.dbus.wantedBy = [ "default.target" ];
+
+    programs.dconf.profiles.gdm =
+    let
+      customDconf = pkgs.writeTextFile {
+        name = "gdm-dconf";
+        destination = "/dconf/gdm-custom";
+        text = ''
+          ${optionalString (!cfg.gdm.autoSuspend) ''
+            [org/gnome/settings-daemon/plugins/power]
+            sleep-inactive-ac-type='nothing'
+            sleep-inactive-battery-type='nothing'
+            sleep-inactive-ac-timeout=0
+            sleep-inactive-battery-timeout=0
+          ''}
+        '';
+      };
+
+      customDconfDb = pkgs.stdenv.mkDerivation {
+        name = "gdm-dconf-db";
+        buildCommand = ''
+          ${pkgs.dconf}/bin/dconf compile $out ${customDconf}/dconf
+        '';
+      };
+    in pkgs.stdenv.mkDerivation {
+      name = "dconf-gdm-profile";
+      buildCommand = ''
+        # Check that the GDM profile starts with what we expect.
+        if [ $(head -n 1 ${gdm}/share/dconf/profile/gdm) != "user-db:user" ]; then
+          echo "GDM dconf profile changed, please update gdm.nix"
+          exit 1
+        fi
+        # Insert our custom DB behind it.
+        sed '2ifile-db:${customDconfDb}' ${gdm}/share/dconf/profile/gdm > $out
+      '';
+    };
+
+    # Use AutomaticLogin if delay is zero, because it's immediate.
+    # Otherwise with TimedLogin with zero seconds the prompt is still
+    # presented and there's a little delay.
+    services.xserver.displayManager.gdm.settings = {
+      daemon = mkMerge [
+        { WaylandEnable = cfg.gdm.wayland; }
+        # nested if else didn't work
+        (mkIf (cfg.autoLogin.enable && cfg.gdm.autoLogin.delay != 0 ) {
+          TimedLoginEnable = true;
+          TimedLogin = cfg.autoLogin.user;
+          TimedLoginDelay = cfg.gdm.autoLogin.delay;
+        })
+        (mkIf (cfg.autoLogin.enable && cfg.gdm.autoLogin.delay == 0 ) {
+          AutomaticLoginEnable = true;
+          AutomaticLogin = cfg.autoLogin.user;
+        })
+      ];
+      debug = mkIf cfg.gdm.debug {
+        Enable = true;
+      };
+    };
+
+    environment.etc."gdm/custom.conf".source = configFile;
+
+    environment.etc."gdm/Xsession".source = config.services.xserver.displayManager.sessionData.wrapper;
+
+    # GDM LFS PAM modules, adapted somehow to NixOS
+    security.pam.services = {
+      gdm-launch-environment.text = ''
+        auth     required       pam_succeed_if.so audit quiet_success user = gdm
+        auth     optional       pam_permit.so
+
+        account  required       pam_succeed_if.so audit quiet_success user = gdm
+        account  sufficient     pam_unix.so
+
+        password required       pam_deny.so
+
+        session  required       pam_succeed_if.so audit quiet_success user = gdm
+        session  required       pam_env.so conffile=/etc/pam/environment readenv=0
+        session  optional       ${pkgs.systemd}/lib/security/pam_systemd.so
+        session  optional       pam_keyinit.so force revoke
+        session  optional       pam_permit.so
+      '';
+
+      gdm-password.text = ''
+        auth      substack      login
+        account   include       login
+        password  substack      login
+        session   include       login
+      '';
+
+      gdm-autologin.text = ''
+        auth      requisite     pam_nologin.so
+
+        auth      required      pam_succeed_if.so uid >= 1000 quiet
+        auth      required      pam_permit.so
+
+        account   sufficient    pam_unix.so
+
+        password  requisite     pam_unix.so nullok sha512
+
+        session   optional      pam_keyinit.so revoke
+        session   include       login
+      '';
+
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/enso-os.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/enso-os.nix
new file mode 100644
index 00000000000..930ee96b384
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/enso-os.nix
@@ -0,0 +1,140 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  dmcfg = config.services.xserver.displayManager;
+  ldmcfg = dmcfg.lightdm;
+  cfg = ldmcfg.greeters.enso;
+
+  theme = cfg.theme.package;
+  icons = cfg.iconTheme.package;
+  cursors = cfg.cursorTheme.package;
+
+  ensoGreeterConf = pkgs.writeText "lightdm-enso-os-greeter.conf" ''
+    [greeter]
+    default-wallpaper=${ldmcfg.background}
+    gtk-theme=${cfg.theme.name}
+    icon-theme=${cfg.iconTheme.name}
+    cursor-theme=${cfg.cursorTheme.name}
+    blur=${toString cfg.blur}
+    brightness=${toString cfg.brightness}
+    ${cfg.extraConfig}
+  '';
+in {
+  options = {
+    services.xserver.displayManager.lightdm.greeters.enso = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable enso-os-greeter as the lightdm greeter
+        '';
+      };
+
+      theme = {
+        package = mkOption {
+          type = types.package;
+          default = pkgs.gnome.gnome-themes-extra;
+          defaultText = literalExpression "pkgs.gnome.gnome-themes-extra";
+          description = ''
+            The package path that contains the theme given in the name option.
+          '';
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "Adwaita";
+          description = ''
+            Name of the theme to use for the lightdm-enso-os-greeter
+          '';
+        };
+      };
+
+      iconTheme = {
+        package = mkOption {
+          type = types.package;
+          default = pkgs.papirus-icon-theme;
+          defaultText = literalExpression "pkgs.papirus-icon-theme";
+          description = ''
+            The package path that contains the icon theme given in the name option.
+          '';
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "ePapirus";
+          description = ''
+            Name of the icon theme to use for the lightdm-enso-os-greeter
+          '';
+        };
+      };
+
+      cursorTheme = {
+        package = mkOption {
+          type = types.package;
+          default = pkgs.capitaine-cursors;
+          defaultText = literalExpression "pkgs.capitaine-cursors";
+          description = ''
+            The package path that contains the cursor theme given in the name option.
+          '';
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "capitane-cursors";
+          description = ''
+            Name of the cursor theme to use for the lightdm-enso-os-greeter
+          '';
+        };
+      };
+
+      blur = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether or not to enable blur
+        '';
+      };
+
+      brightness = mkOption {
+        type = types.int;
+        default = 7;
+        description = ''
+          Brightness
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration that should be put in the greeter.conf
+          configuration file
+        '';
+      };
+    };
+  };
+
+  config = mkIf (ldmcfg.enable && cfg.enable) {
+    environment.etc."lightdm/greeter.conf".source = ensoGreeterConf;
+
+    environment.systemPackages = [
+      cursors
+      icons
+      theme
+    ];
+
+    services.xserver.displayManager.lightdm = {
+      greeter = mkDefault {
+        package = pkgs.lightdm-enso-os-greeter.xgreeters;
+        name = "pantheon-greeter";
+      };
+
+      greeters = {
+        gtk = {
+          enable = mkDefault false;
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix
new file mode 100644
index 00000000000..debd4b568bf
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix
@@ -0,0 +1,174 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  dmcfg = config.services.xserver.displayManager;
+  ldmcfg = dmcfg.lightdm;
+  xcfg = config.services.xserver;
+  cfg = ldmcfg.greeters.gtk;
+
+  inherit (pkgs) writeText;
+
+  theme = cfg.theme.package;
+  icons = cfg.iconTheme.package;
+  cursors = cfg.cursorTheme.package;
+
+  gtkGreeterConf = writeText "lightdm-gtk-greeter.conf"
+    ''
+    [greeter]
+    theme-name = ${cfg.theme.name}
+    icon-theme-name = ${cfg.iconTheme.name}
+    cursor-theme-name = ${cfg.cursorTheme.name}
+    cursor-theme-size = ${toString cfg.cursorTheme.size}
+    background = ${ldmcfg.background}
+    ${optionalString (cfg.clock-format != null) "clock-format = ${cfg.clock-format}"}
+    ${optionalString (cfg.indicators != null) "indicators = ${concatStringsSep ";" cfg.indicators}"}
+    ${optionalString (xcfg.dpi != null) "xft-dpi=${toString xcfg.dpi}"}
+    ${cfg.extraConfig}
+    '';
+
+in
+{
+  options = {
+
+    services.xserver.displayManager.lightdm.greeters.gtk = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable lightdm-gtk-greeter as the lightdm greeter.
+        '';
+      };
+
+      theme = {
+
+        package = mkOption {
+          type = types.package;
+          default = pkgs.gnome.gnome-themes-extra;
+          defaultText = literalExpression "pkgs.gnome.gnome-themes-extra";
+          description = ''
+            The package path that contains the theme given in the name option.
+          '';
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "Adwaita";
+          description = ''
+            Name of the theme to use for the lightdm-gtk-greeter.
+          '';
+        };
+
+      };
+
+      iconTheme = {
+
+        package = mkOption {
+          type = types.package;
+          default = pkgs.gnome.adwaita-icon-theme;
+          defaultText = literalExpression "pkgs.gnome.adwaita-icon-theme";
+          description = ''
+            The package path that contains the icon theme given in the name option.
+          '';
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "Adwaita";
+          description = ''
+            Name of the icon theme to use for the lightdm-gtk-greeter.
+          '';
+        };
+
+      };
+
+      cursorTheme = {
+
+        package = mkOption {
+          type = types.package;
+          default = pkgs.gnome.adwaita-icon-theme;
+          defaultText = literalExpression "pkgs.gnome.adwaita-icon-theme";
+          description = ''
+            The package path that contains the cursor theme given in the name option.
+          '';
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "Adwaita";
+          description = ''
+            Name of the cursor theme to use for the lightdm-gtk-greeter.
+          '';
+        };
+
+        size = mkOption {
+          type = types.int;
+          default = 16;
+          description = ''
+            Size of the cursor theme to use for the lightdm-gtk-greeter.
+          '';
+        };
+      };
+
+      clock-format = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "%F";
+        description = ''
+          Clock format string (as expected by strftime, e.g. "%H:%M")
+          to use with the lightdm gtk greeter panel.
+
+          If set to null the default clock format is used.
+        '';
+      };
+
+      indicators = mkOption {
+        type = types.nullOr (types.listOf types.str);
+        default = null;
+        example = [ "~host" "~spacer" "~clock" "~spacer" "~session" "~language" "~a11y" "~power" ];
+        description = ''
+          List of allowed indicator modules to use for the lightdm gtk
+          greeter panel.
+
+          Built-in indicators include "~a11y", "~language", "~session",
+          "~power", "~clock", "~host", "~spacer". Unity indicators can be
+          represented by short name (e.g. "sound", "power"), service file name,
+          or absolute path.
+
+          If set to null the default indicators are used.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration that should be put in the lightdm-gtk-greeter.conf
+          configuration file.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf (ldmcfg.enable && cfg.enable) {
+
+    services.xserver.displayManager.lightdm.greeter = mkDefault {
+      package = pkgs.lightdm_gtk_greeter.xgreeters;
+      name = "lightdm-gtk-greeter";
+    };
+
+    environment.systemPackages = [
+      cursors
+      icons
+      theme
+    ];
+
+    environment.etc."lightdm/lightdm-gtk-greeter.conf".source = gtkGreeterConf;
+
+  };
+}
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/mini.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/mini.nix
new file mode 100644
index 00000000000..16d7fdf15cf
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/mini.nix
@@ -0,0 +1,100 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  dmcfg = config.services.xserver.displayManager;
+  ldmcfg = dmcfg.lightdm;
+  cfg = ldmcfg.greeters.mini;
+
+  miniGreeterConf = pkgs.writeText "lightdm-mini-greeter.conf"
+    ''
+    [greeter]
+    user = ${cfg.user}
+    show-password-label = true
+    password-label-text = Password:
+    invalid-password-text = Invalid Password
+    show-input-cursor = true
+    password-alignment = right
+
+    [greeter-hotkeys]
+    mod-key = meta
+    shutdown-key = s
+    restart-key = r
+    hibernate-key = h
+    suspend-key = u
+
+    [greeter-theme]
+    font = Sans
+    font-size = 1em
+    font-weight = bold
+    font-style = normal
+    text-color = "#080800"
+    error-color = "#F8F8F0"
+    background-image = "${ldmcfg.background}"
+    background-color = "#1B1D1E"
+    window-color = "#F92672"
+    border-color = "#080800"
+    border-width = 2px
+    layout-space = 15
+    password-color = "#F8F8F0"
+    password-background-color = "#1B1D1E"
+    password-border-color = "#080800"
+    password-border-width = 2px
+
+    ${cfg.extraConfig}
+    '';
+
+in
+{
+  options = {
+
+    services.xserver.displayManager.lightdm.greeters.mini = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable lightdm-mini-greeter as the lightdm greeter.
+
+          Note that this greeter starts only the default X session.
+          You can configure the default X session using
+          <xref linkend="opt-services.xserver.displayManager.defaultSession"/>.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "root";
+        description = ''
+          The user to login as.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration that should be put in the lightdm-mini-greeter.conf
+          configuration file.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf (ldmcfg.enable && cfg.enable) {
+
+    services.xserver.displayManager.lightdm.greeters.gtk.enable = false;
+
+    services.xserver.displayManager.lightdm.greeter = mkDefault {
+      package = pkgs.lightdm-mini-greeter.xgreeters;
+      name = "lightdm-mini-greeter";
+    };
+
+    environment.etc."lightdm/lightdm-mini-greeter.conf".source = miniGreeterConf;
+
+  };
+}
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix
new file mode 100644
index 00000000000..f18e4a914e5
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix
@@ -0,0 +1,49 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  dmcfg = config.services.xserver.displayManager;
+  ldmcfg = dmcfg.lightdm;
+  cfg = ldmcfg.greeters.pantheon;
+
+in
+{
+  meta = with lib; {
+    maintainers = with maintainers; [ ] ++ teams.pantheon.members;
+  };
+
+  options = {
+
+    services.xserver.displayManager.lightdm.greeters.pantheon = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable elementary-greeter as the lightdm greeter.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf (ldmcfg.enable && cfg.enable) {
+
+    services.xserver.displayManager.lightdm.greeters.gtk.enable = false;
+
+    services.xserver.displayManager.lightdm.greeter = mkDefault {
+      package = pkgs.pantheon.elementary-greeter.xgreeters;
+      name = "io.elementary.greeter";
+    };
+
+    # Show manual login card.
+    services.xserver.displayManager.lightdm.extraSeatDefaults = "greeter-show-manual-login=true";
+
+    environment.etc."lightdm/io.elementary.greeter.conf".source = "${pkgs.pantheon.elementary-greeter}/etc/lightdm/io.elementary.greeter.conf";
+    environment.etc."wingpanel.d/io.elementary.greeter.allowed".source = "${pkgs.pantheon.elementary-default-settings}/etc/wingpanel.d/io.elementary.greeter.allowed";
+
+  };
+}
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/tiny.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/tiny.nix
new file mode 100644
index 00000000000..a9ba8e6280d
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/tiny.nix
@@ -0,0 +1,92 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  dmcfg = config.services.xserver.displayManager;
+  ldmcfg = dmcfg.lightdm;
+  cfg = ldmcfg.greeters.tiny;
+
+in
+{
+  options = {
+
+    services.xserver.displayManager.lightdm.greeters.tiny = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable lightdm-tiny-greeter as the lightdm greeter.
+
+          Note that this greeter starts only the default X session.
+          You can configure the default X session using
+          <xref linkend="opt-services.xserver.displayManager.defaultSession"/>.
+        '';
+      };
+
+      label = {
+        user = mkOption {
+          type = types.str;
+          default = "Username";
+          description = ''
+            The string to represent the user_text label.
+          '';
+        };
+
+        pass = mkOption {
+          type = types.str;
+          default = "Password";
+          description = ''
+            The string to represent the pass_text label.
+          '';
+        };
+      };
+
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Section to describe style and ui.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf (ldmcfg.enable && cfg.enable) {
+
+    services.xserver.displayManager.lightdm.greeters.gtk.enable = false;
+
+    nixpkgs.config.lightdm-tiny-greeter.conf =
+    let
+      configHeader = ''
+        #include <gtk/gtk.h>
+        static const char *user_text = "${cfg.label.user}";
+        static const char *pass_text = "${cfg.label.pass}";
+        static const char *session = "${dmcfg.defaultSession}";
+      '';
+    in
+      optionalString (cfg.extraConfig != "")
+        (configHeader + cfg.extraConfig);
+
+    services.xserver.displayManager.lightdm.greeter =
+      mkDefault {
+        package = pkgs.lightdm-tiny-greeter.xgreeters;
+        name = "lightdm-tiny-greeter";
+      };
+
+    assertions = [
+      {
+        assertion = dmcfg.defaultSession != null;
+        message = ''
+          Please set: services.xserver.displayManager.defaultSession
+        '';
+      }
+    ];
+
+  };
+}
diff --git a/nixos/modules/services/x11/display-managers/lightdm.nix b/nixos/modules/services/x11/display-managers/lightdm.nix
new file mode 100644
index 00000000000..27dfed3cc14
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/lightdm.nix
@@ -0,0 +1,328 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  xcfg = config.services.xserver;
+  dmcfg = xcfg.displayManager;
+  xEnv = config.systemd.services.display-manager.environment;
+  cfg = dmcfg.lightdm;
+  sessionData = dmcfg.sessionData;
+
+  setSessionScript = pkgs.callPackage ./account-service-util.nix { };
+
+  inherit (pkgs) lightdm writeScript writeText;
+
+  # lightdm runs with clearenv(), but we need a few things in the environment for X to startup
+  xserverWrapper = writeScript "xserver-wrapper"
+    ''
+      #! ${pkgs.bash}/bin/bash
+      ${concatMapStrings (n: "export ${n}=\"${getAttr n xEnv}\"\n") (attrNames xEnv)}
+
+      display=$(echo "$@" | xargs -n 1 | grep -P ^:\\d\$ | head -n 1 | sed s/^://)
+      if [ -z "$display" ]
+      then additionalArgs=":0 -logfile /var/log/X.0.log"
+      else additionalArgs="-logfile /var/log/X.$display.log"
+      fi
+
+      exec ${dmcfg.xserverBin} ${toString dmcfg.xserverArgs} $additionalArgs "$@"
+    '';
+
+  usersConf = writeText "users.conf"
+    ''
+      [UserList]
+      minimum-uid=500
+      hidden-users=${concatStringsSep " " dmcfg.hiddenUsers}
+      hidden-shells=/run/current-system/sw/bin/nologin
+    '';
+
+  lightdmConf = writeText "lightdm.conf"
+    ''
+      [LightDM]
+      ${optionalString cfg.greeter.enable ''
+        greeter-user = ${config.users.users.lightdm.name}
+        greeters-directory = ${cfg.greeter.package}
+      ''}
+      sessions-directory = ${dmcfg.sessionData.desktops}/share/xsessions:${dmcfg.sessionData.desktops}/share/wayland-sessions
+      ${cfg.extraConfig}
+
+      [Seat:*]
+      xserver-command = ${xserverWrapper}
+      session-wrapper = ${dmcfg.sessionData.wrapper}
+      ${optionalString cfg.greeter.enable ''
+        greeter-session = ${cfg.greeter.name}
+      ''}
+      ${optionalString dmcfg.autoLogin.enable ''
+        autologin-user = ${dmcfg.autoLogin.user}
+        autologin-user-timeout = ${toString cfg.autoLogin.timeout}
+        autologin-session = ${sessionData.autologinSession}
+      ''}
+      ${optionalString (dmcfg.setupCommands != "") ''
+        display-setup-script=${pkgs.writeScript "lightdm-display-setup" ''
+          #!${pkgs.bash}/bin/bash
+          ${dmcfg.setupCommands}
+        ''}
+      ''}
+      ${cfg.extraSeatDefaults}
+    '';
+
+in
+{
+  meta = with lib; {
+    maintainers = with maintainers; [ ] ++ teams.pantheon.members;
+  };
+
+  # Note: the order in which lightdm greeter modules are imported
+  # here determines the default: later modules (if enable) are
+  # preferred.
+  imports = [
+    ./lightdm-greeters/gtk.nix
+    ./lightdm-greeters/mini.nix
+    ./lightdm-greeters/enso-os.nix
+    ./lightdm-greeters/pantheon.nix
+    ./lightdm-greeters/tiny.nix
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "lightdm" "autoLogin" "enable" ] [
+      "services"
+      "xserver"
+      "displayManager"
+      "autoLogin"
+      "enable"
+    ])
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "lightdm" "autoLogin" "user" ] [
+     "services"
+     "xserver"
+     "displayManager"
+     "autoLogin"
+     "user"
+    ])
+  ];
+
+  options = {
+
+    services.xserver.displayManager.lightdm = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable lightdm as the display manager.
+        '';
+      };
+
+      greeter =  {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            If set to false, run lightdm in greeterless mode. This only works if autologin
+            is enabled and autoLogin.timeout is zero.
+          '';
+        };
+        package = mkOption {
+          type = types.package;
+          description = ''
+            The LightDM greeter to login via. The package should be a directory
+            containing a .desktop file matching the name in the 'name' option.
+          '';
+
+        };
+        name = mkOption {
+          type = types.str;
+          description = ''
+            The name of a .desktop file in the directory specified
+            in the 'package' option.
+          '';
+        };
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          user-authority-in-system-dir = true
+        '';
+        description = "Extra lines to append to LightDM section.";
+      };
+
+      background = mkOption {
+        type = types.either types.path (types.strMatching "^#[0-9]\{6\}$");
+        # Manual cannot depend on packages, we are actually setting the default in config below.
+        defaultText = literalExpression "pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom.gnomeFilePath";
+        description = ''
+          The background image or color to use.
+        '';
+      };
+
+      extraSeatDefaults = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          greeter-show-manual-login=true
+        '';
+        description = "Extra lines to append to SeatDefaults section.";
+      };
+
+      # Configuration for automatic login specific to LightDM
+      autoLogin.timeout = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          Show the greeter for this many seconds before automatic login occurs.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = xcfg.enable;
+        message = ''
+          LightDM requires services.xserver.enable to be true
+        '';
+      }
+      { assertion = dmcfg.autoLogin.enable -> sessionData.autologinSession != null;
+        message = ''
+          LightDM auto-login requires that services.xserver.displayManager.defaultSession is set.
+        '';
+      }
+      { assertion = !cfg.greeter.enable -> (dmcfg.autoLogin.enable && cfg.autoLogin.timeout == 0);
+        message = ''
+          LightDM can only run without greeter if automatic login is enabled and the timeout for it
+          is set to zero.
+        '';
+      }
+    ];
+
+    # Keep in sync with the defaultText value from the option definition.
+    services.xserver.displayManager.lightdm.background = mkDefault pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom.gnomeFilePath;
+
+    # Set default session in session chooser to a specified values – basically ignore session history.
+    # Auto-login is already covered by a config value.
+    services.xserver.displayManager.job.preStart = optionalString (!dmcfg.autoLogin.enable && dmcfg.defaultSession != null) ''
+      ${setSessionScript}/bin/set-session ${dmcfg.defaultSession}
+    '';
+
+    # setSessionScript needs session-files in XDG_DATA_DIRS
+    services.xserver.displayManager.job.environment.XDG_DATA_DIRS = "${dmcfg.sessionData.desktops}/share/";
+
+    # setSessionScript wants AccountsService
+    systemd.services.display-manager.wants = [
+      "accounts-daemon.service"
+    ];
+
+    # lightdm relaunches itself via just `lightdm`, so needs to be on the PATH
+    services.xserver.displayManager.job.execCmd = ''
+      export PATH=${lightdm}/sbin:$PATH
+      exec ${lightdm}/sbin/lightdm
+    '';
+
+    # Replaces getty
+    systemd.services.display-manager.conflicts = [
+      "getty@tty7.service"
+      # TODO: Add "plymouth-quit.service" so LightDM can control when plymouth
+      # quits. Currently this breaks switching to configurations with plymouth.
+     ];
+
+    # Pull in dependencies of services we replace.
+    systemd.services.display-manager.after = [
+      "rc-local.service"
+      "systemd-machined.service"
+      "systemd-user-sessions.service"
+      "getty@tty7.service"
+      "user.slice"
+    ];
+
+    # user.slice needs to be present
+    systemd.services.display-manager.requires = [
+      "user.slice"
+    ];
+
+    # lightdm stops plymouth so when it fails make sure plymouth stops.
+    systemd.services.display-manager.onFailure = [
+      "plymouth-quit.service"
+    ];
+
+    systemd.services.display-manager.serviceConfig = {
+      BusName = "org.freedesktop.DisplayManager";
+      IgnoreSIGPIPE = "no";
+      # This allows lightdm to pass the LUKS password through to PAM.
+      # login keyring is unlocked automatic when autologin is used.
+      KeyringMode = "shared";
+      KillMode = "mixed";
+      StandardError = "inherit";
+    };
+
+    environment.etc."lightdm/lightdm.conf".source = lightdmConf;
+    environment.etc."lightdm/users.conf".source = usersConf;
+
+    services.dbus.enable = true;
+    services.dbus.packages = [ lightdm ];
+
+    # lightdm uses the accounts daemon to remember language/window-manager per user
+    services.accounts-daemon.enable = true;
+
+    # Enable the accounts daemon to find lightdm's dbus interface
+    environment.systemPackages = [ lightdm ];
+
+    security.polkit.enable = true;
+
+    security.pam.services.lightdm.text = ''
+        auth      substack      login
+        account   include       login
+        password  substack      login
+        session   include       login
+    '';
+
+    security.pam.services.lightdm-greeter.text = ''
+        auth     required       pam_succeed_if.so audit quiet_success user = lightdm
+        auth     optional       pam_permit.so
+
+        account  required       pam_succeed_if.so audit quiet_success user = lightdm
+        account  sufficient     pam_unix.so
+
+        password required       pam_deny.so
+
+        session  required       pam_succeed_if.so audit quiet_success user = lightdm
+        session  required       pam_env.so conffile=/etc/pam/environment readenv=0
+        session  optional       ${pkgs.systemd}/lib/security/pam_systemd.so
+        session  optional       pam_keyinit.so force revoke
+        session  optional       pam_permit.so
+    '';
+
+    security.pam.services.lightdm-autologin.text = ''
+        auth      requisite     pam_nologin.so
+
+        auth      required      pam_succeed_if.so uid >= 1000 quiet
+        auth      required      pam_permit.so
+
+        account   sufficient    pam_unix.so
+
+        password  requisite     pam_unix.so nullok sha512
+
+        session   optional      pam_keyinit.so revoke
+        session   include       login
+    '';
+
+    users.users.lightdm = {
+      home = "/var/lib/lightdm";
+      group = "lightdm";
+      uid = config.ids.uids.lightdm;
+      shell = pkgs.bash;
+    };
+
+    systemd.tmpfiles.rules = [
+      "d /run/lightdm 0711 lightdm lightdm -"
+      "d /var/cache/lightdm 0711 root lightdm -"
+      "d /var/lib/lightdm 1770 lightdm lightdm -"
+      "d /var/lib/lightdm-data 1775 lightdm lightdm -"
+      "d /var/log/lightdm 0711 root lightdm -"
+    ];
+
+    users.groups.lightdm.gid = config.ids.gids.lightdm;
+    services.xserver.tty     = null; # We might start multiple X servers so let the tty increment themselves..
+    services.xserver.display = null; # We specify our own display (and logfile) in xserver-wrapper up there
+  };
+}
diff --git a/nixos/modules/services/x11/display-managers/sddm.nix b/nixos/modules/services/x11/display-managers/sddm.nix
new file mode 100644
index 00000000000..529a086381f
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/sddm.nix
@@ -0,0 +1,288 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  xcfg = config.services.xserver;
+  dmcfg = xcfg.displayManager;
+  cfg = dmcfg.sddm;
+  xEnv = config.systemd.services.display-manager.environment;
+
+  sddm = pkgs.libsForQt5.sddm;
+
+  iniFmt = pkgs.formats.ini { };
+
+  xserverWrapper = pkgs.writeShellScript "xserver-wrapper" ''
+    ${concatMapStrings (n: "export ${n}=\"${getAttr n xEnv}\"\n") (attrNames xEnv)}
+    exec systemd-cat -t xserver-wrapper ${dmcfg.xserverBin} ${toString dmcfg.xserverArgs} "$@"
+  '';
+
+  Xsetup = pkgs.writeShellScript "Xsetup" ''
+    ${cfg.setupScript}
+    ${dmcfg.setupCommands}
+  '';
+
+  Xstop = pkgs.writeShellScript "Xstop" ''
+    ${cfg.stopScript}
+  '';
+
+  defaultConfig = {
+    General = {
+      HaltCommand = "/run/current-system/systemd/bin/systemctl poweroff";
+      RebootCommand = "/run/current-system/systemd/bin/systemctl reboot";
+      Numlock = if cfg.autoNumlock then "on" else "none"; # on, off none
+
+      # Implementation is done via pkgs/applications/display-managers/sddm/sddm-default-session.patch
+      DefaultSession = optionalString (dmcfg.defaultSession != null) "${dmcfg.defaultSession}.desktop";
+    };
+
+    Theme = {
+      Current = cfg.theme;
+      ThemeDir = "/run/current-system/sw/share/sddm/themes";
+      FacesDir = "/run/current-system/sw/share/sddm/faces";
+    };
+
+    Users = {
+      MaximumUid = config.ids.uids.nixbld;
+      HideUsers = concatStringsSep "," dmcfg.hiddenUsers;
+      HideShells = "/run/current-system/sw/bin/nologin";
+    };
+
+    X11 = {
+      MinimumVT = if xcfg.tty != null then xcfg.tty else 7;
+      ServerPath = toString xserverWrapper;
+      XephyrPath = "${pkgs.xorg.xorgserver.out}/bin/Xephyr";
+      SessionCommand = toString dmcfg.sessionData.wrapper;
+      SessionDir = "${dmcfg.sessionData.desktops}/share/xsessions";
+      XauthPath = "${pkgs.xorg.xauth}/bin/xauth";
+      DisplayCommand = toString Xsetup;
+      DisplayStopCommand = toString Xstop;
+      EnableHiDPI = cfg.enableHidpi;
+    };
+
+    Wayland = {
+      EnableHiDPI = cfg.enableHidpi;
+      SessionDir = "${dmcfg.sessionData.desktops}/share/wayland-sessions";
+    };
+  } // lib.optionalAttrs dmcfg.autoLogin.enable {
+    Autologin = {
+      User = dmcfg.autoLogin.user;
+      Session = autoLoginSessionName;
+      Relogin = cfg.autoLogin.relogin;
+    };
+  };
+
+  cfgFile =
+    iniFmt.generate "sddm.conf" (lib.recursiveUpdate defaultConfig cfg.settings);
+
+  autoLoginSessionName =
+    "${dmcfg.sessionData.autologinSession}.desktop";
+
+in
+{
+  imports = [
+    (mkRemovedOptionModule
+      [ "services" "xserver" "displayManager" "sddm" "themes" ]
+      "Set the option `services.xserver.displayManager.sddm.package' instead.")
+    (mkRenamedOptionModule
+      [ "services" "xserver" "displayManager" "sddm" "autoLogin" "enable" ]
+      [ "services" "xserver" "displayManager" "autoLogin" "enable" ])
+    (mkRenamedOptionModule
+      [ "services" "xserver" "displayManager" "sddm" "autoLogin" "user" ]
+      [ "services" "xserver" "displayManager" "autoLogin" "user" ])
+    (mkRemovedOptionModule
+      [ "services" "xserver" "displayManager" "sddm" "extraConfig" ]
+      "Set the option `services.xserver.displayManager.sddm.settings' instead.")
+  ];
+
+  options = {
+
+    services.xserver.displayManager.sddm = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable sddm as the display manager.
+        '';
+      };
+
+      enableHidpi = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable automatic HiDPI mode.
+        '';
+      };
+
+      settings = mkOption {
+        type = iniFmt.type;
+        default = { };
+        example = {
+          Autologin = {
+            User = "john";
+            Session = "plasma.desktop";
+          };
+        };
+        description = ''
+          Extra settings merged in and overwritting defaults in sddm.conf.
+        '';
+      };
+
+      theme = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Greeter theme to use.
+        '';
+      };
+
+      autoNumlock = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable numlock at login.
+        '';
+      };
+
+      setupScript = mkOption {
+        type = types.str;
+        default = "";
+        example = ''
+          # workaround for using NVIDIA Optimus without Bumblebee
+          xrandr --setprovideroutputsource modesetting NVIDIA-0
+          xrandr --auto
+        '';
+        description = ''
+          A script to execute when starting the display server. DEPRECATED, please
+          use <option>services.xserver.displayManager.setupCommands</option>.
+        '';
+      };
+
+      stopScript = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          A script to execute when stopping the display server.
+        '';
+      };
+
+      # Configuration for automatic login specific to SDDM
+      autoLogin = {
+        relogin = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            If true automatic login will kick in again on session exit (logout), otherwise it
+            will only log in automatically when the display-manager is started.
+          '';
+        };
+
+        minimumUid = mkOption {
+          type = types.ints.u16;
+          default = 1000;
+          description = ''
+            Minimum user ID for auto-login user.
+          '';
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = xcfg.enable;
+        message = ''
+          SDDM requires services.xserver.enable to be true
+        '';
+      }
+      {
+        assertion = dmcfg.autoLogin.enable -> autoLoginSessionName != null;
+        message = ''
+          SDDM auto-login requires that services.xserver.displayManager.defaultSession is set.
+        '';
+      }
+    ];
+
+    services.xserver.displayManager.job = {
+      environment = {
+        # Load themes from system environment
+        QT_PLUGIN_PATH = "/run/current-system/sw/" + pkgs.qt5.qtbase.qtPluginPrefix;
+        QML2_IMPORT_PATH = "/run/current-system/sw/" + pkgs.qt5.qtbase.qtQmlPrefix;
+      };
+
+      execCmd = "exec /run/current-system/sw/bin/sddm";
+    };
+
+    security.pam.services = {
+      sddm = {
+        allowNullPassword = true;
+        startSession = true;
+      };
+
+      sddm-greeter.text = ''
+        auth     required       pam_succeed_if.so audit quiet_success user = sddm
+        auth     optional       pam_permit.so
+
+        account  required       pam_succeed_if.so audit quiet_success user = sddm
+        account  sufficient     pam_unix.so
+
+        password required       pam_deny.so
+
+        session  required       pam_succeed_if.so audit quiet_success user = sddm
+        session  required       pam_env.so conffile=/etc/pam/environment readenv=0
+        session  optional       ${pkgs.systemd}/lib/security/pam_systemd.so
+        session  optional       pam_keyinit.so force revoke
+        session  optional       pam_permit.so
+      '';
+
+      sddm-autologin.text = ''
+        auth     requisite pam_nologin.so
+        auth     required  pam_succeed_if.so uid >= ${toString cfg.autoLogin.minimumUid} quiet
+        auth     required  pam_permit.so
+
+        account  include   sddm
+
+        password include   sddm
+
+        session  include   sddm
+      '';
+    };
+
+    users.users.sddm = {
+      createHome = true;
+      home = "/var/lib/sddm";
+      group = "sddm";
+      uid = config.ids.uids.sddm;
+    };
+
+    environment.etc."sddm.conf".source = cfgFile;
+    environment.pathsToLink = [
+      "/share/sddm"
+    ];
+
+    users.groups.sddm.gid = config.ids.gids.sddm;
+
+    environment.systemPackages = [ sddm ];
+    services.dbus.packages = [ sddm ];
+
+    # To enable user switching, allow sddm to allocate TTYs/displays dynamically.
+    services.xserver.tty = null;
+    services.xserver.display = null;
+
+    systemd.tmpfiles.rules = [
+      # Prior to Qt 5.9.2, there is a QML cache invalidation bug which sometimes
+      # strikes new Plasma 5 releases. If the QML cache is not invalidated, SDDM
+      # will segfault without explanation. We really tore our hair out for awhile
+      # before finding the bug:
+      # https://bugreports.qt.io/browse/QTBUG-62302
+      # We work around the problem by deleting the QML cache before startup.
+      # This was supposedly fixed in Qt 5.9.2 however it has been reported with
+      # 5.10 and 5.11 as well. The initial workaround was to delete the directory
+      # in the Xsetup script but that doesn't do anything.
+      # Instead we use tmpfiles.d to ensure it gets wiped.
+      # This causes a small but perceptible delay when SDDM starts.
+      "e ${config.users.users.sddm.home}/.cache - - - 0"
+    ];
+  };
+}
diff --git a/nixos/modules/services/x11/display-managers/set-session.py b/nixos/modules/services/x11/display-managers/set-session.py
new file mode 100755
index 00000000000..75940efe32b
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/set-session.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+
+import gi, argparse, os, logging, sys
+
+gi.require_version("AccountsService", "1.0")
+from gi.repository import AccountsService, GLib
+from ordered_set import OrderedSet
+
+
+def get_session_file(session):
+    system_data_dirs = GLib.get_system_data_dirs()
+
+    session_dirs = OrderedSet(
+        os.path.join(data_dir, session)
+        for data_dir in system_data_dirs
+        for session in {"wayland-sessions", "xsessions"}
+    )
+
+    session_files = OrderedSet(
+        os.path.join(dir, session + ".desktop")
+        for dir in session_dirs
+        if os.path.exists(os.path.join(dir, session + ".desktop"))
+    )
+
+    # Deal with duplicate wayland-sessions and xsessions.
+    # Needed for the situation in gnome-session, where there's
+    # a xsession named the same as a wayland session.
+    if any(map(is_session_wayland, session_files)):
+        session_files = OrderedSet(
+            session for session in session_files if is_session_wayland(session)
+        )
+    else:
+        session_files = OrderedSet(
+            session for session in session_files if is_session_xsession(session)
+        )
+
+    if len(session_files) == 0:
+        logging.warning("No session files are found.")
+        sys.exit(0)
+    else:
+        return session_files[0]
+
+
+def is_session_xsession(session_file):
+    return "/xsessions/" in session_file
+
+
+def is_session_wayland(session_file):
+    return "/wayland-sessions/" in session_file
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Set session type for all normal users."
+    )
+    parser.add_argument("session", help="Name of session to set.")
+
+    args = parser.parse_args()
+
+    session = getattr(args, "session")
+    session_file = get_session_file(session)
+
+    user_manager = AccountsService.UserManager.get_default()
+    users = user_manager.list_users()
+
+    for user in users:
+        if user.is_system_account():
+            continue
+        else:
+            if is_session_wayland(session_file):
+                logging.debug(
+                    f"Setting session name: {session}, as we found the existing wayland-session: {session_file}"
+                )
+                user.set_session(session)
+                user.set_session_type("wayland")
+            elif is_session_xsession(session_file):
+                logging.debug(
+                    f"Setting session name: {session}, as we found the existing xsession: {session_file}"
+                )
+                user.set_x_session(session)
+                user.set_session(session)
+                user.set_session_type("x11")
+            else:
+                logging.error(f"Couldn't figure out session type for {session_file}")
+                sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/nixos/modules/services/x11/display-managers/slim.nix b/nixos/modules/services/x11/display-managers/slim.nix
new file mode 100644
index 00000000000..4b0948a5b7a
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/slim.nix
@@ -0,0 +1,16 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  # added 2019-11-11
+  imports = [
+    (mkRemovedOptionModule [ "services" "xserver" "displayManager" "slim" ] ''
+      The SLIM project is abandoned and their last release was in 2013.
+      Because of this it poses a security risk to your system.
+      Other issues include it not fully supporting systemd and logind sessions.
+      Please use a different display manager such as LightDM, SDDM, or GDM.
+      You can also use the startx module which uses Xinitrc.
+    '')
+  ];
+}
diff --git a/nixos/modules/services/x11/display-managers/startx.nix b/nixos/modules/services/x11/display-managers/startx.nix
new file mode 100644
index 00000000000..a48566ae068
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/startx.nix
@@ -0,0 +1,54 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.displayManager.startx;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+    services.xserver.displayManager.startx = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the dummy "startx" pseudo-display manager,
+          which allows users to start X manually via the "startx" command
+          from a vt shell. The X server runs under the user's id, not as root.
+          The user must provide a ~/.xinitrc file containing session startup
+          commands, see startx(1). This is not automatically generated
+          from the desktopManager and windowManager settings.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    services.xserver = {
+      exportConfiguration = true;
+    };
+
+    # Other displayManagers log to /dev/null because they're services and put
+    # Xorg's stdout in the journal
+    #
+    # To send log to Xorg's default log location ($XDG_DATA_HOME/xorg/), we do
+    # not specify a log file when running X
+    services.xserver.logFile = mkDefault null;
+
+    # Implement xserverArgs via xinit's system-wide xserverrc
+    environment.etc."X11/xinit/xserverrc".source = pkgs.writeShellScript "xserverrc" ''
+      exec ${pkgs.xorg.xorgserver}/bin/X ${toString config.services.xserver.displayManager.xserverArgs} "$@"
+    '';
+    environment.systemPackages =  with pkgs; [ xorg.xinit ];
+  };
+
+}
diff --git a/nixos/modules/services/x11/display-managers/sx.nix b/nixos/modules/services/x11/display-managers/sx.nix
new file mode 100644
index 00000000000..e3097736430
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/sx.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.xserver.displayManager.sx;
+
+in {
+  options = {
+    services.xserver.displayManager.sx = {
+      enable = mkEnableOption "sx pseudo-display manager" // {
+        description = ''
+          Whether to enable the "sx" pseudo-display manager, which allows users
+          to start manually via the "sx" command from a vt shell. The X server
+          runs under the user's id, not as root. The user must provide a
+          ~/.config/sx/sxrc file containing session startup commands, see
+          sx(1). This is not automatically generated from the desktopManager
+          and windowManager settings. sx doesn't have a way to directly set
+          X server flags, but it can be done by overriding its xorgserver
+          dependency.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.sx ];
+    services.xserver = {
+      exportConfiguration = true;
+      logFile = mkDefault null;
+    };
+  };
+
+  meta.maintainers = with maintainers; [ figsoda ];
+}
diff --git a/nixos/modules/services/x11/display-managers/xpra.nix b/nixos/modules/services/x11/display-managers/xpra.nix
new file mode 100644
index 00000000000..c23e479140f
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/xpra.nix
@@ -0,0 +1,252 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.displayManager.xpra;
+  dmcfg = config.services.xserver.displayManager;
+
+in
+
+{
+  ###### interface
+
+  options = {
+    services.xserver.displayManager.xpra = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable xpra as display manager.";
+      };
+
+      bindTcp = mkOption {
+        default = "127.0.0.1:10000";
+        example = "0.0.0.0:10000";
+        type = types.nullOr types.str;
+        description = "Bind xpra to TCP";
+      };
+
+      auth = mkOption {
+        type = types.str;
+        default = "pam";
+        example = "password:value=mysecret";
+        description = "Authentication to use when connecting to xpra";
+      };
+
+      pulseaudio = mkEnableOption "pulseaudio audio streaming";
+
+      extraOptions = mkOption {
+        description = "Extra xpra options";
+        default = [];
+        type = types.listOf types.str;
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    services.xserver.videoDrivers = ["dummy"];
+
+    services.xserver.monitorSection = ''
+      HorizSync   1.0 - 2000.0
+      VertRefresh 1.0 - 200.0
+      #To add your own modes here, use a modeline calculator, like:
+      # cvt:
+      # http://www.x.org/archive/X11R7.5/doc/man/man1/cvt.1.html
+      # xtiming:
+      # http://xtiming.sourceforge.net/cgi-bin/xtiming.pl
+      # gtf:
+      # http://gtf.sourceforge.net/
+      #This can be used to get a specific DPI, but only for the default resolution:
+      #DisplaySize 508 317
+      #NOTE: the highest modes will not work without increasing the VideoRam
+      # for the dummy video card.
+      #Modeline "16000x15000" 300.00  16000 16408 18000 20000  15000 15003 15013 15016
+      #Modeline "15000x15000" 281.25  15000 15376 16872 18744  15000 15003 15013 15016
+      #Modeline "16384x8192" 167.75  16384 16800 18432 20480  8192 8195 8205 8208
+      #Modeline "15360x8640" 249.00 15360 15752 17280 19200 8640 8643 8648 8651
+      Modeline "8192x4096" 193.35 8192 8224 8952 8984 4096 4196 4200 4301
+      Modeline "7680x4320" 208.00 7680 7880 8640 9600 4320 4323 4328 4335
+      Modeline "6400x4096" 151.38 6400 6432 7000 7032 4096 4196 4200 4301
+      Modeline "6400x2560" 91.59 6400 6432 6776 6808 2560 2623 2626 2689
+      Modeline "6400x2160" 160.51 6400 6432 7040 7072 2160 2212 2216 2269
+      Modeline "5760x2160" 149.50 5760 5768 6320 6880 2160 2161 2164 2173
+      Modeline "5680x1440" 142.66 5680 5712 6248 6280 1440 1474 1478 1513
+      Modeline "5496x1200" 199.13 5496 5528 6280 6312 1200 1228 1233 1261
+      Modeline "5280x2560" 75.72 5280 5312 5592 5624 2560 2623 2626 2689
+      Modeline "5280x1920" 56.04 5280 5312 5520 5552 1920 1967 1969 2017
+      Modeline "5280x1200" 191.40 5280 5312 6032 6064 1200 1228 1233 1261
+      Modeline "5280x1080" 169.96 5280 5312 5952 5984 1080 1105 1110 1135
+      Modeline "5120x3200" 199.75 5120 5152 5904 5936 3200 3277 3283 3361
+      Modeline "5120x2560" 73.45 5120 5152 5424 5456 2560 2623 2626 2689
+      Modeline "5120x2880" 185.50 5120 5256 5760 6400 2880 2883 2888 2899
+      Modeline "4800x1200" 64.42 4800 4832 5072 5104 1200 1229 1231 1261
+      Modeline "4720x3840" 227.86 4720 4752 5616 5648 3840 3933 3940 4033
+      Modeline "4400x2560" 133.70 4400 4432 4936 4968 2560 2622 2627 2689
+      Modeline "4480x1440" 72.94 4480 4512 4784 4816 1440 1475 1478 1513
+      Modeline "4240x1440" 69.09 4240 4272 4528 4560 1440 1475 1478 1513
+      Modeline "4160x1440" 67.81 4160 4192 4448 4480 1440 1475 1478 1513
+      Modeline "4096x2304" 249.25 4096 4296 4720 5344 2304 2307 2312 2333
+      Modeline "4096x2160" 111.25 4096 4200 4608 5120 2160 2163 2173 2176
+      Modeline "4000x1660" 170.32 4000 4128 4536 5072 1660 1661 1664 1679
+      Modeline "4000x1440" 145.00 4000 4088 4488 4976 1440 1441 1444 1457
+      Modeline "3904x1440" 63.70 3904 3936 4176 4208 1440 1475 1478 1513
+      Modeline "3840x2880" 133.43 3840 3872 4376 4408 2880 2950 2955 3025
+      Modeline "3840x2560" 116.93 3840 3872 4312 4344 2560 2622 2627 2689
+      Modeline "3840x2160" 104.25 3840 3944 4320 4800 2160 2163 2168 2175
+      Modeline "3840x2048" 91.45 3840 3872 4216 4248 2048 2097 2101 2151
+      Modeline "3840x1200" 108.89 3840 3872 4280 4312 1200 1228 1232 1261
+      Modeline "3840x1080" 100.38 3840 3848 4216 4592 1080 1081 1084 1093
+      Modeline "3864x1050" 94.58 3864 3896 4248 4280 1050 1074 1078 1103
+      Modeline "3600x1200" 106.06 3600 3632 3984 4368 1200 1201 1204 1214
+      Modeline "3600x1080" 91.02 3600 3632 3976 4008 1080 1105 1109 1135
+      Modeline "3520x1196" 99.53 3520 3552 3928 3960 1196 1224 1228 1256
+      Modeline "3360x2560" 102.55 3360 3392 3776 3808 2560 2622 2627 2689
+      Modeline "3360x1050" 293.75 3360 3576 3928 4496 1050 1053 1063 1089
+      Modeline "3288x1080" 39.76 3288 3320 3464 3496 1080 1106 1108 1135
+      Modeline "3200x1800" 233.00 3200 3384 3720 4240  1800 1803 1808 1834
+      Modeline "3200x1080" 236.16 3200 3232 4128 4160 1080 1103 1112 1135
+      Modeline "3120x2560" 95.36 3120 3152 3512 3544 2560 2622 2627 2689
+      Modeline "3120x1050" 272.75 3120 3320 3648 4176 1050 1053 1063 1089
+      Modeline "3072x2560" 93.92 3072 3104 3456 3488 2560 2622 2627 2689
+      Modeline "3008x1692" 130.93 3008 3112 3416 3824 1692 1693 1696 1712
+      Modeline "3000x2560" 91.77 3000 3032 3376 3408 2560 2622 2627 2689
+      Modeline "2880x1620" 396.25 2880 3096 3408 3936 1620 1623 1628 1679
+      Modeline "2728x1680" 148.02 2728 2760 3320 3352 1680 1719 1726 1765
+      Modeline "2560x2240" 151.55 2560 2688 2952 3344 2240 2241 2244 2266
+      Modeline "2560x1600" 47.12 2560 2592 2768 2800 1600 1639 1642 1681
+      Modeline "2560x1440" 42.12 2560 2592 2752 2784 1440 1475 1478 1513
+      Modeline "2560x1400" 267.86 2560 2592 3608 3640 1400 1429 1441 1471
+      Modeline "2048x2048" 49.47 2048 2080 2264 2296 2048 2097 2101 2151
+      Modeline "2048x1536" 80.06 2048 2104 2312 2576 1536 1537 1540 1554
+      Modeline "2048x1152" 197.97 2048 2184 2408 2768 1152 1153 1156 1192
+      Modeline "2048x1152" 165.92 2048 2080 2704 2736 1152 1176 1186 1210
+      Modeline "1920x1440" 69.47 1920 1960 2152 2384 1440 1441 1444 1457
+      Modeline "1920x1200" 26.28 1920 1952 2048 2080 1200 1229 1231 1261
+      Modeline "1920x1080" 23.53 1920 1952 2040 2072 1080 1106 1108 1135
+      Modeline "1728x1520" 205.42 1728 1760 2536 2568 1520 1552 1564 1597
+      Modeline "1680x1050" 20.08 1680 1712 1784 1816 1050 1075 1077 1103
+      Modeline "1600x1200" 22.04 1600 1632 1712 1744 1200 1229 1231 1261
+      Modeline "1600x900" 33.92 1600 1632 1760 1792 900 921 924 946
+      Modeline "1440x900" 30.66 1440 1472 1584 1616 900 921 924 946
+      Modeline "1400x900" 103.50 1400 1480 1624 1848 900 903 913 934
+      ModeLine "1366x768" 72.00 1366 1414 1446 1494  768 771 777 803
+      Modeline "1360x768" 24.49 1360 1392 1480 1512 768 786 789 807
+      Modeline "1280x1024" 31.50 1280 1312 1424 1456 1024 1048 1052 1076
+      Modeline "1280x800" 24.15 1280 1312 1400 1432 800 819 822 841
+      Modeline "1280x768" 23.11 1280 1312 1392 1424 768 786 789 807
+      Modeline "1280x720" 59.42 1280 1312 1536 1568 720 735 741 757
+      Modeline "1024x768" 18.71 1024 1056 1120 1152 768 786 789 807
+      Modeline "1024x640" 41.98 1024 1056 1208 1240 640 653 659 673
+      Modeline "1024x576" 46.50 1024 1064 1160 1296  576 579 584 599
+      Modeline "768x1024" 19.50 768 800 872 904 1024 1048 1052 1076
+      Modeline "960x540" 40.75 960 992 1088 1216 540 543 548 562
+      Modeline "864x486"  32.50 864 888 968 1072 486 489 494 506
+      Modeline "720x405" 22.50 720 744 808 896  405 408 413 422
+      Modeline "640x360" 14.75 640 664 720 800 360 363 368 374
+      #common resolutions for android devices (both orientations):
+      Modeline "800x1280" 25.89 800 832 928 960 1280 1310 1315 1345
+      Modeline "1280x800" 24.15 1280 1312 1400 1432 800 819 822 841
+      Modeline "720x1280" 30.22 720 752 864 896 1280 1309 1315 1345
+      Modeline "1280x720" 27.41 1280 1312 1416 1448 720 737 740 757
+      Modeline "768x1024" 24.93 768 800 888 920 1024 1047 1052 1076
+      Modeline "1024x768" 23.77 1024 1056 1144 1176 768 785 789 807
+      Modeline "600x1024" 19.90 600 632 704 736 1024 1047 1052 1076
+      Modeline "1024x600" 18.26 1024 1056 1120 1152 600 614 617 631
+      Modeline "536x960" 16.74 536 568 624 656 960 982 986 1009
+      Modeline "960x536" 15.23 960 992 1048 1080 536 548 551 563
+      Modeline "600x800" 15.17 600 632 688 720 800 818 822 841
+      Modeline "800x600" 14.50 800 832 880 912 600 614 617 631
+      Modeline "480x854" 13.34 480 512 560 592 854 873 877 897
+      Modeline "848x480" 12.09 848 880 920 952 480 491 493 505
+      Modeline "480x800" 12.43 480 512 552 584 800 818 822 841
+      Modeline "800x480" 11.46 800 832 872 904 480 491 493 505
+      #resolutions for android devices (both orientations)
+      #minus the status bar
+      #38px status bar (and width rounded up)
+      Modeline "800x1242" 25.03 800 832 920 952 1242 1271 1275 1305
+      Modeline "1280x762" 22.93 1280 1312 1392 1424 762 780 783 801
+      Modeline "720x1242" 29.20 720 752 856 888 1242 1271 1276 1305
+      Modeline "1280x682" 25.85 1280 1312 1408 1440 682 698 701 717
+      Modeline "768x986" 23.90 768 800 888 920 986 1009 1013 1036
+      Modeline "1024x730" 22.50 1024 1056 1136 1168 730 747 750 767
+      Modeline "600x986" 19.07 600 632 704 736 986 1009 1013 1036
+      Modeline "1024x562" 17.03 1024 1056 1120 1152 562 575 578 591
+      Modeline "536x922" 16.01 536 568 624 656 922 943 947 969
+      Modeline "960x498" 14.09 960 992 1040 1072 498 509 511 523
+      Modeline "600x762" 14.39 600 632 680 712 762 779 783 801
+      Modeline "800x562" 13.52 800 832 880 912 562 575 578 591
+      Modeline "480x810" 12.59 480 512 552 584 810 828 832 851
+      Modeline "848x442" 11.09 848 880 920 952 442 452 454 465
+      Modeline "480x762" 11.79 480 512 552 584 762 779 783 801
+    '';
+
+    services.xserver.resolutions = [
+      {x="8192"; y="4096";}
+      {x="5120"; y="3200";}
+      {x="3840"; y="2880";}
+      {x="3840"; y="2560";}
+      {x="3840"; y="2048";}
+      {x="3840"; y="2160";}
+      {x="2048"; y="2048";}
+      {x="2560"; y="1600";}
+      {x="1920"; y="1440";}
+      {x="1920"; y="1200";}
+      {x="1920"; y="1080";}
+      {x="1600"; y="1200";}
+      {x="1680"; y="1050";}
+      {x="1600"; y="900";}
+      {x="1400"; y="1050";}
+      {x="1440"; y="900";}
+      {x="1280"; y="1024";}
+      {x="1366"; y="768";}
+      {x="1280"; y="800";}
+      {x="1024"; y="768";}
+      {x="1024"; y="600";}
+      {x="800"; y="600";}
+      {x="320"; y="200";}
+    ];
+
+    services.xserver.serverFlagsSection = ''
+      Option "DontVTSwitch" "true"
+      Option "PciForceNone" "true"
+      Option "AutoEnableDevices" "false"
+      Option "AutoAddDevices" "false"
+    '';
+
+    services.xserver.deviceSection = ''
+      VideoRam 192000
+    '';
+
+    services.xserver.displayManager.job.execCmd = ''
+      ${optionalString (cfg.pulseaudio)
+        "export PULSE_COOKIE=/run/pulse/.config/pulse/cookie"}
+      exec ${pkgs.xpra}/bin/xpra start \
+        --daemon=off \
+        --log-dir=/var/log \
+        --log-file=xpra.log \
+        --opengl=on \
+        --clipboard=on \
+        --notifications=on \
+        --speaker=yes \
+        --mdns=no \
+        --pulseaudio=no \
+        ${optionalString (cfg.pulseaudio) "--sound-source=pulse"} \
+        --socket-dirs=/run/xpra \
+        --xvfb="xpra_Xdummy ${concatStringsSep " " dmcfg.xserverArgs}" \
+        ${optionalString (cfg.bindTcp != null) "--bind-tcp=${cfg.bindTcp}"} \
+        --auth=${cfg.auth} \
+        ${concatStringsSep " " cfg.extraOptions}
+    '';
+
+    services.xserver.terminateOnReset = false;
+
+    environment.systemPackages = [pkgs.xpra];
+
+    virtualisation.virtualbox.guest.x11 = false;
+    hardware.pulseaudio.enable = mkDefault cfg.pulseaudio;
+    hardware.pulseaudio.systemWide = mkDefault cfg.pulseaudio;
+  };
+
+}
diff --git a/nixos/modules/services/x11/extra-layouts.nix b/nixos/modules/services/x11/extra-layouts.nix
new file mode 100644
index 00000000000..159bed63e13
--- /dev/null
+++ b/nixos/modules/services/x11/extra-layouts.nix
@@ -0,0 +1,135 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  layouts = config.services.xserver.extraLayouts;
+
+  layoutOpts = {
+    options = {
+      description = mkOption {
+        type = types.str;
+        description = "A short description of the layout.";
+      };
+
+      languages = mkOption {
+        type = types.listOf types.str;
+        description =
+        ''
+          A list of languages provided by the layout.
+          (Use ISO 639-2 codes, for example: "eng" for english)
+        '';
+      };
+
+      compatFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          The path to the xkb compat file.
+          This file sets the compatibility state, used to preserve
+          compatibility with xkb-unaware programs.
+          It must contain a <literal>xkb_compat "name" { ... }</literal> block.
+        '';
+      };
+
+      geometryFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          The path to the xkb geometry file.
+          This (completely optional) file describes the physical layout of
+          keyboard, which maybe be used by programs to depict it.
+          It must contain a <literal>xkb_geometry "name" { ... }</literal> block.
+        '';
+      };
+
+      keycodesFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          The path to the xkb keycodes file.
+          This file specifies the range and the interpretation of the raw
+          keycodes sent by the keyboard.
+          It must contain a <literal>xkb_keycodes "name" { ... }</literal> block.
+        '';
+      };
+
+      symbolsFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          The path to the xkb symbols file.
+          This is the most important file: it defines which symbol or action
+          maps to each key and must contain a
+          <literal>xkb_symbols "name" { ... }</literal> block.
+        '';
+      };
+
+      typesFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          The path to the xkb types file.
+          This file specifies the key types that can be associated with
+          the various keyboard keys.
+          It must contain a <literal>xkb_types "name" { ... }</literal> block.
+        '';
+      };
+
+    };
+  };
+
+  xkb_patched = pkgs.xorg.xkeyboardconfig_custom {
+    layouts = config.services.xserver.extraLayouts;
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options.services.xserver = {
+    extraLayouts = mkOption {
+      type = types.attrsOf (types.submodule layoutOpts);
+      default = {};
+      example = literalExpression
+      ''
+        {
+          mine = {
+            description = "My custom xkb layout.";
+            languages = [ "eng" ];
+            symbolsFile = /path/to/my/layout;
+          };
+        }
+      '';
+      description = ''
+        Extra custom layouts that will be included in the xkb configuration.
+        Information on how to create a new layout can be found here:
+        <link xlink:href="https://www.x.org/releases/current/doc/xorg-docs/input/XKB-Enhancing.html#Defining_New_Layouts"></link>.
+        For more examples see
+        <link xlink:href="https://wiki.archlinux.org/index.php/X_KeyBoard_extension#Basic_examples"></link>
+      '';
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf (layouts != { }) {
+
+    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";
+    };
+
+    services.xserver = {
+      xkbDir = "${xkb_patched}/etc/X11/xkb";
+      exportConfiguration = config.services.xserver.displayManager.startx.enable
+        || config.services.xserver.displayManager.sx.enable;
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/fractalart.nix b/nixos/modules/services/x11/fractalart.nix
new file mode 100644
index 00000000000..448248a5879
--- /dev/null
+++ b/nixos/modules/services/x11/fractalart.nix
@@ -0,0 +1,36 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.fractalart;
+in {
+  options.services.fractalart = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      example = true;
+      description = "Enable FractalArt for generating colorful wallpapers on login";
+    };
+
+    width = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      example = 1920;
+      description = "Screen width";
+    };
+
+    height = mkOption {
+      type = types.nullOr types.int;
+      default = null;
+      example = 1080;
+      description = "Screen height";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.haskellPackages.FractalArt ];
+    services.xserver.displayManager.sessionCommands =
+      "${pkgs.haskellPackages.FractalArt}/bin/FractalArt --no-bg -f .background-image"
+        + optionalString (cfg.width  != null) " -w ${toString cfg.width}"
+        + optionalString (cfg.height != null) " -h ${toString cfg.height}";
+  };
+}
diff --git a/nixos/modules/services/x11/gdk-pixbuf.nix b/nixos/modules/services/x11/gdk-pixbuf.nix
new file mode 100644
index 00000000000..3fd6fed91e1
--- /dev/null
+++ b/nixos/modules/services/x11/gdk-pixbuf.nix
@@ -0,0 +1,45 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.gdk-pixbuf;
+
+  # Get packages to generate the cache for. We always include gdk-pixbuf.
+  effectivePackages = unique ([pkgs.gdk-pixbuf] ++ cfg.modulePackages);
+
+  # Generate the cache file by running gdk-pixbuf-query-loaders for each
+  # package and concatenating the results.
+  loadersCache = pkgs.runCommand "gdk-pixbuf-loaders.cache" { preferLocalBuild = true; } ''
+    (
+      for package in ${concatStringsSep " " effectivePackages}; do
+        module_dir="$package/${pkgs.gdk-pixbuf.moduleDir}"
+        if [[ ! -d $module_dir ]]; then
+          echo "Warning (services.xserver.gdk-pixbuf): missing module directory $module_dir" 1>&2
+          continue
+        fi
+        GDK_PIXBUF_MODULEDIR="$module_dir" \
+          ${pkgs.stdenv.hostPlatform.emulator pkgs.buildPackages} ${pkgs.gdk-pixbuf.dev}/bin/gdk-pixbuf-query-loaders
+      done
+    ) > "$out"
+  '';
+in
+
+{
+  options = {
+    services.xserver.gdk-pixbuf.modulePackages = mkOption {
+      type = types.listOf types.package;
+      default = [ ];
+      description = "Packages providing GDK-Pixbuf modules, for cache generation.";
+    };
+  };
+
+  # If there is any package configured in modulePackages, we generate the
+  # loaders.cache based on that and set the environment variable
+  # GDK_PIXBUF_MODULE_FILE to point to it.
+  config = mkIf (cfg.modulePackages != []) {
+    environment.variables = {
+      GDK_PIXBUF_MODULE_FILE = "${loadersCache}";
+    };
+  };
+}
diff --git a/nixos/modules/services/x11/hardware/cmt.nix b/nixos/modules/services/x11/hardware/cmt.nix
new file mode 100644
index 00000000000..5ac824c5e41
--- /dev/null
+++ b/nixos/modules/services/x11/hardware/cmt.nix
@@ -0,0 +1,59 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+cfg = config.services.xserver.cmt;
+etcPath = "X11/xorg.conf.d";
+
+in {
+
+  options = {
+
+    services.xserver.cmt = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable chrome multitouch input (cmt). Touchpad drivers that are configured for chromebooks.";
+      };
+      models = mkOption {
+        type = types.enum [ "atlas" "banjo" "candy" "caroline" "cave" "celes" "clapper" "cyan" "daisy" "elan" "elm" "enguarde" "eve" "expresso" "falco" "gandof" "glimmer" "gnawty" "heli" "kevin" "kip" "leon" "lulu" "orco" "pbody" "peppy" "pi" "pit" "puppy" "quawks" "rambi" "samus" "snappy" "spring" "squawks" "swanky" "winky" "wolf" "auron_paine" "auron_yuna" "daisy_skate" "nyan_big" "nyan_blaze" "veyron_jaq" "veyron_jerry" "veyron_mighty" "veyron_minnie" "veyron_speedy" ];
+        example = "banjo";
+        description = ''
+          Which models to enable cmt for. Enter the Code Name for your Chromebook.
+          Code Name can be found at <link xlink:href="https://www.chromium.org/chromium-os/developer-information-for-chrome-os-devices" />.
+        '';
+      };
+    }; #closes services
+  }; #closes options
+
+  config = mkIf cfg.enable {
+
+    services.xserver.modules = [ pkgs.xf86_input_cmt ];
+
+    environment.etc = {
+      "${etcPath}/40-touchpad-cmt.conf" = {
+        source = "${pkgs.chromium-xorg-conf}/40-touchpad-cmt.conf";
+      };
+      "${etcPath}/50-touchpad-cmt-${cfg.models}.conf" = {
+        source = "${pkgs.chromium-xorg-conf}/50-touchpad-cmt-${cfg.models}.conf";
+      };
+      "${etcPath}/60-touchpad-cmt-${cfg.models}.conf" = {
+        source = "${pkgs.chromium-xorg-conf}/60-touchpad-cmt-${cfg.models}.conf";
+      };
+    };
+
+    assertions = [
+      {
+        assertion = !config.services.xserver.libinput.enable;
+        message = ''
+          cmt and libinput are incompatible, meaning you cannot enable them both.
+          To use cmt you need to disable libinput with `services.xserver.libinput.enable = false`
+          If you haven't enabled it in configuration.nix, it's enabled by default on a
+          different xserver module.
+        '';
+      }
+    ];
+  };
+}
diff --git a/nixos/modules/services/x11/hardware/digimend.nix b/nixos/modules/services/x11/hardware/digimend.nix
new file mode 100644
index 00000000000..b1b1682f00b
--- /dev/null
+++ b/nixos/modules/services/x11/hardware/digimend.nix
@@ -0,0 +1,38 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.digimend;
+
+  pkg = config.boot.kernelPackages.digimend;
+
+in
+
+{
+
+  options = {
+
+    services.xserver.digimend = {
+
+      enable = mkEnableOption "the digimend drivers for Huion/XP-Pen/etc. tablets";
+
+    };
+
+  };
+
+
+  config = mkIf cfg.enable {
+
+    # digimend drivers use xsetwacom and wacom X11 drivers
+    services.xserver.wacom.enable = true;
+
+    boot.extraModulePackages = [ pkg ];
+
+    environment.etc."X11/xorg.conf.d/50-digimend.conf".source =
+      "${pkg}/usr/share/X11/xorg.conf.d/50-digimend.conf";
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/hardware/libinput.nix b/nixos/modules/services/x11/hardware/libinput.nix
new file mode 100644
index 00000000000..efdb7c61dfa
--- /dev/null
+++ b/nixos/modules/services/x11/hardware/libinput.nix
@@ -0,0 +1,291 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.xserver.libinput;
+
+    xorgBool = v: if v then "on" else "off";
+
+    mkConfigForDevice = deviceType: {
+      dev = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/dev/input/event0";
+        description =
+          ''
+            Path for ${deviceType} device.  Set to <literal>null</literal> to apply to any
+            auto-detected ${deviceType}.
+          '';
+      };
+
+      accelProfile = mkOption {
+        type = types.enum [ "flat" "adaptive" ];
+        default = "adaptive";
+        example = "flat";
+        description =
+          ''
+            Sets the pointer acceleration profile to the given profile.
+            Permitted values are <literal>adaptive</literal>, <literal>flat</literal>.
+            Not all devices support this option or all profiles.
+            If a profile is unsupported, the default profile for this is used.
+            <literal>flat</literal>: Pointer motion is accelerated by a constant
+            (device-specific) factor, depending on the current speed.
+            <literal>adaptive</literal>: Pointer acceleration depends on the input speed.
+            This is the default profile for most devices.
+          '';
+      };
+
+      accelSpeed = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "-0.5";
+        description = "Cursor acceleration (how fast speed increases from minSpeed to maxSpeed).";
+      };
+
+      buttonMapping = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "1 6 3 4 5 0 7";
+        description =
+          ''
+            Sets the logical button mapping for this device, see XSetPointerMapping(3). The string  must
+            be  a  space-separated  list  of  button mappings in the order of the logical buttons on the
+            device, starting with button 1.  The default mapping is "1 2 3 ... 32". A mapping of 0 deacâ€
+            tivates the button. Multiple buttons can have the same mapping.  Invalid mapping strings are
+            discarded and the default mapping is used for all buttons.  Buttons  not  specified  in  the
+            user's mapping use the default mapping. See section BUTTON MAPPING for more details.
+          '';
+      };
+
+      calibrationMatrix = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "0.5 0 0 0 0.8 0.1 0 0 1";
+        description =
+          ''
+            A string of 9 space-separated floating point numbers. Sets the calibration matrix to the
+            3x3 matrix where the first row is (abc), the second row is (def) and the third row is (ghi).
+          '';
+      };
+
+      clickMethod = mkOption {
+        type = types.nullOr (types.enum [ "none" "buttonareas" "clickfinger" ]);
+        default = null;
+        example = "buttonareas";
+        description =
+          ''
+            Enables a click method. Permitted values are <literal>none</literal>,
+            <literal>buttonareas</literal>, <literal>clickfinger</literal>.
+            Not all devices support all methods, if an option is unsupported,
+            the default click method for this device is used.
+          '';
+      };
+
+      leftHanded = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enables left-handed button orientation, i.e. swapping left and right buttons.";
+      };
+
+      middleEmulation = mkOption {
+        type = types.bool;
+        default = true;
+        description =
+          ''
+            Enables middle button emulation. When enabled, pressing the left and right buttons
+            simultaneously produces a middle mouse button click.
+          '';
+      };
+
+      naturalScrolling = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enables or disables natural scrolling behavior.";
+      };
+
+      scrollButton = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        example = 1;
+        description =
+          ''
+            Designates a button as scroll button. If the ScrollMethod is button and the button is logically
+            held down, x/y axis movement is converted into scroll events.
+          '';
+      };
+
+      scrollMethod = mkOption {
+        type = types.enum [ "twofinger" "edge" "button" "none" ];
+        default = "twofinger";
+        example = "edge";
+        description =
+          ''
+            Specify the scrolling method: <literal>twofinger</literal>, <literal>edge</literal>,
+            <literal>button</literal>, or <literal>none</literal>
+          '';
+      };
+
+      horizontalScrolling = mkOption {
+        type = types.bool;
+        default = true;
+        description =
+          ''
+            Disables horizontal scrolling. When disabled, this driver will discard any horizontal scroll
+            events from libinput. Note that this does not disable horizontal scrolling, it merely
+            discards the horizontal axis from any scroll events.
+          '';
+      };
+
+      sendEventsMode = mkOption {
+        type = types.enum [ "disabled" "enabled" "disabled-on-external-mouse" ];
+        default = "enabled";
+        example = "disabled";
+        description =
+          ''
+            Sets the send events mode to <literal>disabled</literal>, <literal>enabled</literal>,
+            or <literal>disabled-on-external-mouse</literal>
+          '';
+      };
+
+      tapping = mkOption {
+        type = types.bool;
+        default = true;
+        description =
+          ''
+            Enables or disables tap-to-click behavior.
+          '';
+      };
+
+      tappingDragLock = mkOption {
+        type = types.bool;
+        default = true;
+        description =
+          ''
+            Enables or disables drag lock during tapping behavior. When enabled, a finger up during tap-
+            and-drag will not immediately release the button. If the finger is set down again within the
+            timeout, the draging process continues.
+          '';
+      };
+
+      transformationMatrix = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "0.5 0 0 0 0.8 0.1 0 0 1";
+        description = ''
+          A string of 9 space-separated floating point numbers. Sets the transformation matrix to
+          the 3x3 matrix where the first row is (abc), the second row is (def) and the third row is (ghi).
+        '';
+      };
+
+      disableWhileTyping = mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          ''
+            Disable input method while typing.
+          '';
+      };
+
+      additionalOptions = mkOption {
+        type = types.lines;
+        default = "";
+        example =
+        ''
+          Option "DragLockButtons" "L1 B1 L2 B2"
+        '';
+        description = ''
+          Additional options for libinput ${deviceType} driver. See
+          <citerefentry><refentrytitle>libinput</refentrytitle><manvolnum>4</manvolnum></citerefentry>
+          for available options.";
+        '';
+      };
+    };
+
+    mkX11ConfigForDevice = deviceType: matchIs: ''
+      Identifier "libinput ${deviceType} configuration"
+      MatchDriver "libinput"
+      MatchIs${matchIs} "${xorgBool true}"
+      ${optionalString (cfg.${deviceType}.dev != null) ''MatchDevicePath "${cfg.${deviceType}.dev}"''}
+      Option "AccelProfile" "${cfg.${deviceType}.accelProfile}"
+      ${optionalString (cfg.${deviceType}.accelSpeed != null) ''Option "AccelSpeed" "${cfg.${deviceType}.accelSpeed}"''}
+      ${optionalString (cfg.${deviceType}.buttonMapping != null) ''Option "ButtonMapping" "${cfg.${deviceType}.buttonMapping}"''}
+      ${optionalString (cfg.${deviceType}.calibrationMatrix != null) ''Option "CalibrationMatrix" "${cfg.${deviceType}.calibrationMatrix}"''}
+      ${optionalString (cfg.${deviceType}.transformationMatrix != null) ''Option "TransformationMatrix" "${cfg.${deviceType}.transformationMatrix}"''}
+      ${optionalString (cfg.${deviceType}.clickMethod != null) ''Option "ClickMethod" "${cfg.${deviceType}.clickMethod}"''}
+      Option "LeftHanded" "${xorgBool cfg.${deviceType}.leftHanded}"
+      Option "MiddleEmulation" "${xorgBool cfg.${deviceType}.middleEmulation}"
+      Option "NaturalScrolling" "${xorgBool cfg.${deviceType}.naturalScrolling}"
+      ${optionalString (cfg.${deviceType}.scrollButton != null) ''Option "ScrollButton" "${toString cfg.${deviceType}.scrollButton}"''}
+      Option "ScrollMethod" "${cfg.${deviceType}.scrollMethod}"
+      Option "HorizontalScrolling" "${xorgBool cfg.${deviceType}.horizontalScrolling}"
+      Option "SendEventsMode" "${cfg.${deviceType}.sendEventsMode}"
+      Option "Tapping" "${xorgBool cfg.${deviceType}.tapping}"
+      Option "TappingDragLock" "${xorgBool cfg.${deviceType}.tappingDragLock}"
+      Option "DisableWhileTyping" "${xorgBool cfg.${deviceType}.disableWhileTyping}"
+      ${cfg.${deviceType}.additionalOptions}
+    '';
+in {
+
+  imports =
+    (map (option: mkRenamedOptionModule ([ "services" "xserver" "libinput" option ]) [ "services" "xserver" "libinput" "touchpad" option ]) [
+      "accelProfile"
+      "accelSpeed"
+      "buttonMapping"
+      "calibrationMatrix"
+      "clickMethod"
+      "leftHanded"
+      "middleEmulation"
+      "naturalScrolling"
+      "scrollButton"
+      "scrollMethod"
+      "horizontalScrolling"
+      "sendEventsMode"
+      "tapping"
+      "tappingDragLock"
+      "transformationMatrix"
+      "disableWhileTyping"
+      "additionalOptions"
+    ]);
+
+  options = {
+
+    services.xserver.libinput = {
+      enable = mkEnableOption "libinput";
+      mouse = mkConfigForDevice "mouse";
+      touchpad = mkConfigForDevice "touchpad";
+    };
+  };
+
+
+  config = mkIf cfg.enable {
+
+    services.xserver.modules = [ pkgs.xorg.xf86inputlibinput ];
+
+    environment.systemPackages = [ pkgs.xorg.xf86inputlibinput ];
+
+    environment.etc =
+      let cfgPath = "X11/xorg.conf.d/40-libinput.conf";
+      in {
+        ${cfgPath} = {
+          source = pkgs.xorg.xf86inputlibinput.out + "/share/" + cfgPath;
+        };
+      };
+
+    services.udev.packages = [ pkgs.libinput.out ];
+
+    services.xserver.inputClassSections = [
+      (mkX11ConfigForDevice "mouse" "Pointer")
+      (mkX11ConfigForDevice "touchpad" "Touchpad")
+    ];
+
+    assertions = [
+      # already present in synaptics.nix
+      /* {
+        assertion = !config.services.xserver.synaptics.enable;
+        message = "Synaptics and libinput are incompatible, you cannot enable both (in services.xserver).";
+      } */
+    ];
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/hardware/synaptics.nix b/nixos/modules/services/x11/hardware/synaptics.nix
new file mode 100644
index 00000000000..93dd560bca4
--- /dev/null
+++ b/nixos/modules/services/x11/hardware/synaptics.nix
@@ -0,0 +1,218 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.xserver.synaptics;
+    opt = options.services.xserver.synaptics;
+    tapConfig = if cfg.tapButtons then enabledTapConfig else disabledTapConfig;
+    enabledTapConfig = ''
+      Option "MaxTapTime" "180"
+      Option "MaxTapMove" "220"
+      Option "TapButton1" "${builtins.elemAt cfg.fingersMap 0}"
+      Option "TapButton2" "${builtins.elemAt cfg.fingersMap 1}"
+      Option "TapButton3" "${builtins.elemAt cfg.fingersMap 2}"
+    '';
+    disabledTapConfig = ''
+      Option "MaxTapTime" "0"
+      Option "MaxTapMove" "0"
+      Option "TapButton1" "0"
+      Option "TapButton2" "0"
+      Option "TapButton3" "0"
+    '';
+  pkg = pkgs.xorg.xf86inputsynaptics;
+  etcFile = "X11/xorg.conf.d/70-synaptics.conf";
+in {
+
+  options = {
+
+    services.xserver.synaptics = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable touchpad support. Deprecated: Consider services.xserver.libinput.enable.";
+      };
+
+      dev = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/dev/input/event0";
+        description =
+          ''
+            Path for touchpad device.  Set to null to apply to any
+            auto-detected touchpad.
+          '';
+      };
+
+      accelFactor = mkOption {
+        type = types.nullOr types.str;
+        default = "0.001";
+        description = "Cursor acceleration (how fast speed increases from minSpeed to maxSpeed).";
+      };
+
+      minSpeed = mkOption {
+        type = types.nullOr types.str;
+        default = "0.6";
+        description = "Cursor speed factor for precision finger motion.";
+      };
+
+      maxSpeed = mkOption {
+        type = types.nullOr types.str;
+        default = "1.0";
+        description = "Cursor speed factor for highest-speed finger motion.";
+      };
+
+      scrollDelta = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        example = 75;
+        description = "Move distance of the finger for a scroll event.";
+      };
+
+      twoFingerScroll = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable two-finger drag-scrolling. Overridden by horizTwoFingerScroll and vertTwoFingerScroll.";
+      };
+
+      horizTwoFingerScroll = mkOption {
+        type = types.bool;
+        default = cfg.twoFingerScroll;
+        defaultText = literalExpression "config.${opt.twoFingerScroll}";
+        description = "Whether to enable horizontal two-finger drag-scrolling.";
+      };
+
+      vertTwoFingerScroll = mkOption {
+        type = types.bool;
+        default = cfg.twoFingerScroll;
+        defaultText = literalExpression "config.${opt.twoFingerScroll}";
+        description = "Whether to enable vertical two-finger drag-scrolling.";
+      };
+
+      horizEdgeScroll = mkOption {
+        type = types.bool;
+        default = ! cfg.horizTwoFingerScroll;
+        defaultText = literalExpression "! config.${opt.horizTwoFingerScroll}";
+        description = "Whether to enable horizontal edge drag-scrolling.";
+      };
+
+      vertEdgeScroll = mkOption {
+        type = types.bool;
+        default = ! cfg.vertTwoFingerScroll;
+        defaultText = literalExpression "! config.${opt.vertTwoFingerScroll}";
+        description = "Whether to enable vertical edge drag-scrolling.";
+      };
+
+      tapButtons = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to enable tap buttons.";
+      };
+
+      buttonsMap = mkOption {
+        type = types.listOf types.int;
+        default = [1 2 3];
+        example = [1 3 2];
+        description = "Remap touchpad buttons.";
+        apply = map toString;
+      };
+
+      fingersMap = mkOption {
+        type = types.listOf types.int;
+        default = [1 2 3];
+        example = [1 3 2];
+        description = "Remap several-fingers taps.";
+        apply = map toString;
+      };
+
+      palmDetect = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable palm detection (hardware support required)";
+      };
+
+      palmMinWidth = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        example = 5;
+        description = "Minimum finger width at which touch is considered a palm";
+      };
+
+      palmMinZ = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        example = 20;
+        description = "Minimum finger pressure at which touch is considered a palm";
+      };
+
+      horizontalScroll = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to enable horizontal scrolling (on touchpad)";
+      };
+
+      additionalOptions = mkOption {
+        type = types.str;
+        default = "";
+        example = ''
+          Option "RTCornerButton" "2"
+          Option "RBCornerButton" "3"
+        '';
+        description = ''
+          Additional options for synaptics touchpad driver.
+        '';
+      };
+
+    };
+
+  };
+
+
+  config = mkIf cfg.enable {
+
+    services.xserver.modules = [ pkg.out ];
+
+    environment.etc.${etcFile}.source =
+      "${pkg.out}/share/X11/xorg.conf.d/70-synaptics.conf";
+
+    environment.systemPackages = [ pkg ];
+
+    services.xserver.config =
+      ''
+        # Automatically enable the synaptics driver for all touchpads.
+        Section "InputClass"
+          Identifier "synaptics touchpad catchall"
+          MatchIsTouchpad "on"
+          ${optionalString (cfg.dev != null) ''MatchDevicePath "${cfg.dev}"''}
+          Driver "synaptics"
+          ${optionalString (cfg.minSpeed != null) ''Option "MinSpeed" "${cfg.minSpeed}"''}
+          ${optionalString (cfg.maxSpeed != null) ''Option "MaxSpeed" "${cfg.maxSpeed}"''}
+          ${optionalString (cfg.accelFactor != null) ''Option "AccelFactor" "${cfg.accelFactor}"''}
+          ${optionalString cfg.tapButtons tapConfig}
+          Option "ClickFinger1" "${builtins.elemAt cfg.buttonsMap 0}"
+          Option "ClickFinger2" "${builtins.elemAt cfg.buttonsMap 1}"
+          Option "ClickFinger3" "${builtins.elemAt cfg.buttonsMap 2}"
+          Option "VertTwoFingerScroll" "${if cfg.vertTwoFingerScroll then "1" else "0"}"
+          Option "HorizTwoFingerScroll" "${if cfg.horizTwoFingerScroll then "1" else "0"}"
+          Option "VertEdgeScroll" "${if cfg.vertEdgeScroll then "1" else "0"}"
+          Option "HorizEdgeScroll" "${if cfg.horizEdgeScroll then "1" else "0"}"
+          ${optionalString cfg.palmDetect ''Option "PalmDetect" "1"''}
+          ${optionalString (cfg.palmMinWidth != null) ''Option "PalmMinWidth" "${toString cfg.palmMinWidth}"''}
+          ${optionalString (cfg.palmMinZ != null) ''Option "PalmMinZ" "${toString cfg.palmMinZ}"''}
+          ${optionalString (cfg.scrollDelta != null) ''Option "VertScrollDelta" "${toString cfg.scrollDelta}"''}
+          ${if !cfg.horizontalScroll then ''Option "HorizScrollDelta" "0"''
+            else (optionalString (cfg.scrollDelta != null) ''Option "HorizScrollDelta" "${toString cfg.scrollDelta}"'')}
+          ${cfg.additionalOptions}
+        EndSection
+      '';
+
+    assertions = [
+      {
+        assertion = !config.services.xserver.libinput.enable;
+        message = "Synaptics and libinput are incompatible, you cannot enable both (in services.xserver).";
+      }
+    ];
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/hardware/wacom.nix b/nixos/modules/services/x11/hardware/wacom.nix
new file mode 100644
index 00000000000..dad2b308d1b
--- /dev/null
+++ b/nixos/modules/services/x11/hardware/wacom.nix
@@ -0,0 +1,48 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.wacom;
+
+in
+
+{
+
+  options = {
+
+    services.xserver.wacom = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the Wacom touchscreen/digitizer/tablet.
+          If you ever have any issues such as, try switching to terminal (ctrl-alt-F1) and back
+          which will make Xorg reconfigure the device ?
+
+          If you're not satisfied by the default behaviour you can override
+          <option>environment.etc."X11/xorg.conf.d/70-wacom.conf"</option> in
+          configuration.nix easily.
+        '';
+      };
+
+    };
+
+  };
+
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.xf86_input_wacom ]; # provides xsetwacom
+
+    services.xserver.modules = [ pkgs.xf86_input_wacom ];
+
+    services.udev.packages = [ pkgs.xf86_input_wacom ];
+
+    environment.etc."X11/xorg.conf.d/70-wacom.conf".source = "${pkgs.xf86_input_wacom}/share/X11/xorg.conf.d/70-wacom.conf";
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/imwheel.nix b/nixos/modules/services/x11/imwheel.nix
new file mode 100644
index 00000000000..ae990141a50
--- /dev/null
+++ b/nixos/modules/services/x11/imwheel.nix
@@ -0,0 +1,71 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.xserver.imwheel;
+in
+  {
+    options = {
+      services.xserver.imwheel = {
+        enable = mkEnableOption "IMWheel service";
+
+        extraOptions = mkOption {
+          type = types.listOf types.str;
+          default = [ "--buttons=45" ];
+          example = [ "--debug" ];
+          description = ''
+            Additional command-line arguments to pass to
+            <command>imwheel</command>.
+          '';
+        };
+
+        rules = mkOption {
+          type = types.attrsOf types.str;
+          default = {};
+          example = literalExpression ''
+            {
+              ".*" = '''
+                None,      Up,   Button4, 8
+                None,      Down, Button5, 8
+                Shift_L,   Up,   Shift_L|Button4, 4
+                Shift_L,   Down, Shift_L|Button5, 4
+                Control_L, Up,   Control_L|Button4
+                Control_L, Down, Control_L|Button5
+              ''';
+            }
+          '';
+          description = ''
+            Window class translation rules.
+            /etc/X11/imwheelrc is generated based on this config
+            which means this config is global for all users.
+            See <link xlink:href="http://imwheel.sourceforge.net/imwheel.1.html">offical man pages</link>
+            for more informations.
+          '';
+        };
+      };
+    };
+
+    config = mkIf cfg.enable {
+      environment.systemPackages = [ pkgs.imwheel ];
+
+      environment.etc."X11/imwheel/imwheelrc".source =
+        pkgs.writeText "imwheelrc" (concatStringsSep "\n\n"
+          (mapAttrsToList
+            (rule: conf: "\"${rule}\"\n${conf}") cfg.rules
+          ));
+
+      systemd.user.services.imwheel = {
+        description = "imwheel service";
+        wantedBy = [ "graphical-session.target" ];
+        partOf = [ "graphical-session.target" ];
+        serviceConfig = {
+          ExecStart = "${pkgs.imwheel}/bin/imwheel " + escapeShellArgs ([
+            "--detach"
+            "--kill"
+          ] ++ cfg.extraOptions);
+          ExecStop = "${pkgs.procps}/bin/pkill imwheel";
+          RestartSec = 3;
+          Restart = "always";
+        };
+      };
+    };
+  }
diff --git a/nixos/modules/services/x11/picom.nix b/nixos/modules/services/x11/picom.nix
new file mode 100644
index 00000000000..b40e20bcd35
--- /dev/null
+++ b/nixos/modules/services/x11/picom.nix
@@ -0,0 +1,335 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.picom;
+  opt = options.services.picom;
+
+  pairOf = x: with types;
+    addCheck (listOf x) (y: length y == 2)
+    // { description = "pair of ${x.description}"; };
+
+  floatBetween = a: b: with types;
+    let
+      # toString prints floats with hardcoded high precision
+      floatToString = f: builtins.toJSON f;
+    in
+      addCheck float (x: x <= b && x >= a)
+      // { description = "a floating point number in " +
+                         "range [${floatToString a}, ${floatToString b}]"; };
+
+  mkDefaultAttrs = mapAttrs (n: v: mkDefault v);
+
+  # Basically a tinkered lib.generators.mkKeyValueDefault
+  # It either serializes a top-level definition "key: { values };"
+  # or an expression "key = { values };"
+  mkAttrsString = top:
+    mapAttrsToList (k: v:
+      let sep = if (top && isAttrs v) then ":" else "=";
+      in "${escape [ sep ] k}${sep}${mkValueString v};");
+
+  # This serializes a Nix expression to the libconfig format.
+  mkValueString = v:
+         if types.bool.check  v then boolToString v
+    else if types.int.check   v then toString v
+    else if types.float.check v then toString v
+    else if types.str.check   v then "\"${escape [ "\"" ] v}\""
+    else if builtins.isList   v then "[ ${concatMapStringsSep " , " mkValueString v} ]"
+    else if types.attrs.check v then "{ ${concatStringsSep " " (mkAttrsString false v) } }"
+    else throw ''
+                 invalid expression used in option services.picom.settings:
+                 ${v}
+               '';
+
+  toConf = attrs: concatStringsSep "\n" (mkAttrsString true cfg.settings);
+
+  configFile = pkgs.writeText "picom.conf" (toConf cfg.settings);
+
+in {
+
+  imports = [
+    (mkAliasOptionModule [ "services" "compton" ] [ "services" "picom" ])
+  ];
+
+  options.services.picom = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether or not to enable Picom as the X.org composite manager.
+      '';
+    };
+
+    experimentalBackends = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to use the unstable new reimplementation of the backends.
+      '';
+    };
+
+    fade = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Fade windows in and out.
+      '';
+    };
+
+    fadeDelta = mkOption {
+      type = types.ints.positive;
+      default = 10;
+      example = 5;
+      description = ''
+        Time between fade animation step (in ms).
+      '';
+    };
+
+    fadeSteps = mkOption {
+      type = pairOf (floatBetween 0.01 1);
+      default = [ 0.028 0.03 ];
+      example = [ 0.04 0.04 ];
+      description = ''
+        Opacity change between fade steps (in and out).
+      '';
+    };
+
+    fadeExclude = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [
+        "window_type *= 'menu'"
+        "name ~= 'Firefox$'"
+        "focused = 1"
+      ];
+      description = ''
+        List of conditions of windows that should not be faded.
+        See <literal>picom(1)</literal> man page for more examples.
+      '';
+    };
+
+    shadow = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Draw window shadows.
+      '';
+    };
+
+    shadowOffsets = mkOption {
+      type = pairOf types.int;
+      default = [ (-15) (-15) ];
+      example = [ (-10) (-15) ];
+      description = ''
+        Left and right offset for shadows (in pixels).
+      '';
+    };
+
+    shadowOpacity = mkOption {
+      type = floatBetween 0 1;
+      default = 0.75;
+      example = 0.8;
+      description = ''
+        Window shadows opacity.
+      '';
+    };
+
+    shadowExclude = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [
+        "window_type *= 'menu'"
+        "name ~= 'Firefox$'"
+        "focused = 1"
+      ];
+      description = ''
+        List of conditions of windows that should have no shadow.
+        See <literal>picom(1)</literal> man page for more examples.
+      '';
+    };
+
+    activeOpacity = mkOption {
+      type = floatBetween 0 1;
+      default = 1.0;
+      example = 0.8;
+      description = ''
+        Opacity of active windows.
+      '';
+    };
+
+    inactiveOpacity = mkOption {
+      type = floatBetween 0.1 1;
+      default = 1.0;
+      example = 0.8;
+      description = ''
+        Opacity of inactive windows.
+      '';
+    };
+
+    menuOpacity = mkOption {
+      type = floatBetween 0 1;
+      default = 1.0;
+      example = 0.8;
+      description = ''
+        Opacity of dropdown and popup menu.
+      '';
+    };
+
+    wintypes = mkOption {
+      type = types.attrs;
+      default = {
+        popup_menu = { opacity = cfg.menuOpacity; };
+        dropdown_menu = { opacity = cfg.menuOpacity; };
+      };
+      defaultText = literalExpression ''
+        {
+          popup_menu = { opacity = config.${opt.menuOpacity}; };
+          dropdown_menu = { opacity = config.${opt.menuOpacity}; };
+        }
+      '';
+      example = {};
+      description = ''
+        Rules for specific window types.
+      '';
+    };
+
+    opacityRules = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [
+        "95:class_g = 'URxvt' && !_NET_WM_STATE@:32a"
+        "0:_NET_WM_STATE@:32a *= '_NET_WM_STATE_HIDDEN'"
+      ];
+      description = ''
+        Rules that control the opacity of windows, in format PERCENT:PATTERN.
+      '';
+    };
+
+    backend = mkOption {
+      type = types.enum [ "glx" "xrender" "xr_glx_hybrid" ];
+      default = "xrender";
+      description = ''
+        Backend to use: <literal>glx</literal>, <literal>xrender</literal> or <literal>xr_glx_hybrid</literal>.
+      '';
+    };
+
+    vSync = mkOption {
+      type = with types; either bool
+        (enum [ "none" "drm" "opengl" "opengl-oml" "opengl-swc" "opengl-mswc" ]);
+      default = false;
+      apply = x:
+        let
+          res = x != "none";
+          msg = "The type of services.picom.vSync has changed to bool:"
+                + " interpreting ${x} as ${boolToString res}";
+        in
+          if isBool x then x
+          else warn msg res;
+
+      description = ''
+        Enable vertical synchronization. Chooses the best method
+        (drm, opengl, opengl-oml, opengl-swc, opengl-mswc) automatically.
+        The bool value should be used, the others are just for backwards compatibility.
+      '';
+    };
+
+    refreshRate = mkOption {
+      type = types.ints.unsigned;
+      default = 0;
+      example = 60;
+      description = ''
+       Screen refresh rate (0 = automatically detect).
+      '';
+    };
+
+    settings = with types;
+    let
+      scalar = oneOf [ bool int float str ]
+        // { description = "scalar types"; };
+
+      libConfig = oneOf [ scalar (listOf libConfig) (attrsOf libConfig) ]
+        // { description = "libconfig type"; };
+
+      topLevel = attrsOf libConfig
+        // { description = ''
+               libconfig configuration. The format consists of an attributes
+               set (called a group) of settings. Each setting can be a scalar type
+               (boolean, integer, floating point number or string), a list of
+               scalars or a group itself
+             '';
+           };
+
+    in mkOption {
+      type = topLevel;
+      default = { };
+      example = literalExpression ''
+        blur =
+          { method = "gaussian";
+            size = 10;
+            deviation = 5.0;
+          };
+      '';
+      description = ''
+        Picom settings. Use this option to configure Picom settings not exposed
+        in a NixOS option or to bypass one.  For the available options see the
+        CONFIGURATION FILES section at <literal>picom(1)</literal>.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.picom.settings = mkDefaultAttrs {
+      # fading
+      fading           = cfg.fade;
+      fade-delta       = cfg.fadeDelta;
+      fade-in-step     = elemAt cfg.fadeSteps 0;
+      fade-out-step    = elemAt cfg.fadeSteps 1;
+      fade-exclude     = cfg.fadeExclude;
+
+      # shadows
+      shadow           = cfg.shadow;
+      shadow-offset-x  = elemAt cfg.shadowOffsets 0;
+      shadow-offset-y  = elemAt cfg.shadowOffsets 1;
+      shadow-opacity   = cfg.shadowOpacity;
+      shadow-exclude   = cfg.shadowExclude;
+
+      # opacity
+      active-opacity   = cfg.activeOpacity;
+      inactive-opacity = cfg.inactiveOpacity;
+
+      wintypes         = cfg.wintypes;
+
+      opacity-rule     = cfg.opacityRules;
+
+      # other options
+      backend          = cfg.backend;
+      vsync            = cfg.vSync;
+      refresh-rate     = cfg.refreshRate;
+    };
+
+    systemd.user.services.picom = {
+      description = "Picom composite manager";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+
+      # Temporarily fixes corrupt colours with Mesa 18
+      environment = mkIf (cfg.backend == "glx") {
+        allow_rgb10_configs = "false";
+      };
+
+      serviceConfig = {
+        ExecStart = "${pkgs.picom}/bin/picom --config ${configFile}"
+          + (optionalString cfg.experimentalBackends " --experimental-backends");
+        RestartSec = 3;
+        Restart = "always";
+      };
+    };
+
+    environment.systemPackages = [ pkgs.picom ];
+  };
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+}
diff --git a/nixos/modules/services/x11/redshift.nix b/nixos/modules/services/x11/redshift.nix
new file mode 100644
index 00000000000..cc9f964754f
--- /dev/null
+++ b/nixos/modules/services/x11/redshift.nix
@@ -0,0 +1,138 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.redshift;
+  lcfg = config.location;
+
+in {
+
+  imports = [
+    (mkChangedOptionModule [ "services" "redshift" "latitude" ] [ "location" "latitude" ]
+      (config:
+        let value = getAttrFromPath [ "services" "redshift" "latitude" ] config;
+        in if value == null then
+          throw "services.redshift.latitude is set to null, you can remove this"
+          else builtins.fromJSON value))
+    (mkChangedOptionModule [ "services" "redshift" "longitude" ] [ "location" "longitude" ]
+      (config:
+        let value = getAttrFromPath [ "services" "redshift" "longitude" ] config;
+        in if value == null then
+          throw "services.redshift.longitude is set to null, you can remove this"
+          else builtins.fromJSON value))
+    (mkRenamedOptionModule [ "services" "redshift" "provider" ] [ "location" "provider" ])
+  ];
+
+  options.services.redshift = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable Redshift to change your screen's colour temperature depending on
+        the time of day.
+      '';
+    };
+
+    temperature = {
+      day = mkOption {
+        type = types.int;
+        default = 5500;
+        description = ''
+          Colour temperature to use during the day, between
+          <literal>1000</literal> and <literal>25000</literal> K.
+        '';
+      };
+      night = mkOption {
+        type = types.int;
+        default = 3700;
+        description = ''
+          Colour temperature to use at night, between
+          <literal>1000</literal> and <literal>25000</literal> K.
+        '';
+      };
+    };
+
+    brightness = {
+      day = mkOption {
+        type = types.str;
+        default = "1";
+        description = ''
+          Screen brightness to apply during the day,
+          between <literal>0.1</literal> and <literal>1.0</literal>.
+        '';
+      };
+      night = mkOption {
+        type = types.str;
+        default = "1";
+        description = ''
+          Screen brightness to apply during the night,
+          between <literal>0.1</literal> and <literal>1.0</literal>.
+        '';
+      };
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.redshift;
+      defaultText = literalExpression "pkgs.redshift";
+      description = ''
+        redshift derivation to use.
+      '';
+    };
+
+    executable = mkOption {
+      type = types.str;
+      default = "/bin/redshift";
+      example = "/bin/redshift-gtk";
+      description = ''
+        Redshift executable to use within the package.
+      '';
+    };
+
+    extraOptions = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "-v" "-m randr" ];
+      description = ''
+        Additional command-line arguments to pass to
+        <command>redshift</command>.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # needed so that .desktop files are installed, which geoclue cares about
+    environment.systemPackages = [ cfg.package ];
+
+    services.geoclue2.appConfig.redshift = {
+      isAllowed = true;
+      isSystem = true;
+    };
+
+    systemd.user.services.redshift =
+    let
+      providerString = if lcfg.provider == "manual"
+        then "${toString lcfg.latitude}:${toString lcfg.longitude}"
+        else lcfg.provider;
+    in
+    {
+      description = "Redshift colour temperature adjuster";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.package}${cfg.executable} \
+            -l ${providerString} \
+            -t ${toString cfg.temperature.day}:${toString cfg.temperature.night} \
+            -b ${toString cfg.brightness.day}:${toString cfg.brightness.night} \
+            ${lib.strings.concatStringsSep " " cfg.extraOptions}
+        '';
+        RestartSec = 3;
+        Restart = "always";
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/services/x11/terminal-server.nix b/nixos/modules/services/x11/terminal-server.nix
new file mode 100644
index 00000000000..e6b50c21a95
--- /dev/null
+++ b/nixos/modules/services/x11/terminal-server.nix
@@ -0,0 +1,56 @@
+# This module implements a terminal service based on ‘x11vnc’.  It
+# listens on port 5900 for VNC connections.  It then presents a login
+# screen to the user.  If the user successfully authenticates, x11vnc
+# checks to see if a X server is already running for that user.  If
+# not, a X server (Xvfb) is started for that user.  The Xvfb instances
+# persist across VNC sessions.
+
+{ lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  config = {
+
+    services.xserver.enable = true;
+    services.xserver.videoDrivers = [];
+
+    # Enable GDM.  Any display manager will do as long as it supports XDMCP.
+    services.xserver.displayManager.gdm.enable = true;
+
+    systemd.sockets.terminal-server =
+      { description = "Terminal Server Socket";
+        wantedBy = [ "sockets.target" ];
+        before = [ "multi-user.target" ];
+        socketConfig.Accept = true;
+        socketConfig.ListenStream = 5900;
+      };
+
+    systemd.services."terminal-server@" =
+      { description = "Terminal Server";
+
+        path =
+          [ pkgs.xorg.xorgserver.out pkgs.gawk pkgs.which pkgs.openssl pkgs.xorg.xauth
+            pkgs.nettools pkgs.shadow pkgs.procps pkgs.util-linux pkgs.bash
+          ];
+
+        environment.FD_GEOM = "1024x786x24";
+        environment.FD_XDMCP_IF = "127.0.0.1";
+        #environment.FIND_DISPLAY_OUTPUT = "/tmp/foo"; # to debug the "find display" script
+
+        serviceConfig =
+          { StandardInput = "socket";
+            StandardOutput = "socket";
+            StandardError = "journal";
+            ExecStart = "@${pkgs.x11vnc}/bin/x11vnc x11vnc -inetd -display WAIT:1024x786:cmd=FINDCREATEDISPLAY-Xvfb.xdmcp -unixpw -ssl SAVE";
+            # Don't kill the X server when the user quits the VNC
+            # connection.  FIXME: the X server should run in a
+            # separate systemd session.
+            KillMode = "process";
+          };
+      };
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/touchegg.nix b/nixos/modules/services/x11/touchegg.nix
new file mode 100644
index 00000000000..9d3678e7696
--- /dev/null
+++ b/nixos/modules/services/x11/touchegg.nix
@@ -0,0 +1,38 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.touchegg;
+
+in {
+  meta = {
+    maintainers = teams.pantheon.members;
+  };
+
+  ###### interface
+  options.services.touchegg = {
+    enable = mkEnableOption "touchegg, a multi-touch gesture recognizer";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.touchegg;
+      defaultText = literalExpression "pkgs.touchegg";
+      description = "touchegg derivation to use.";
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    systemd.services.touchegg = {
+      description = "Touchegg Daemon";
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${cfg.package}/bin/touchegg --daemon";
+        Restart = "on-failure";
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/x11/unclutter-xfixes.nix b/nixos/modules/services/x11/unclutter-xfixes.nix
new file mode 100644
index 00000000000..0b4d06f640d
--- /dev/null
+++ b/nixos/modules/services/x11/unclutter-xfixes.nix
@@ -0,0 +1,58 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.unclutter-xfixes;
+
+in {
+  options.services.unclutter-xfixes = {
+
+    enable = mkOption {
+      description = "Enable unclutter-xfixes to hide your mouse cursor when inactive.";
+      type = types.bool;
+      default = false;
+    };
+
+    package = mkOption {
+      description = "unclutter-xfixes derivation to use.";
+      type = types.package;
+      default = pkgs.unclutter-xfixes;
+      defaultText = literalExpression "pkgs.unclutter-xfixes";
+    };
+
+    timeout = mkOption {
+      description = "Number of seconds before the cursor is marked inactive.";
+      type = types.int;
+      default = 1;
+    };
+
+    threshold = mkOption {
+      description = "Minimum number of pixels considered cursor movement.";
+      type = types.int;
+      default = 1;
+    };
+
+    extraOptions = mkOption {
+      description = "More arguments to pass to the unclutter-xfixes command.";
+      type = types.listOf types.str;
+      default = [];
+      example = [ "exclude-root" "ignore-scrolling" "fork" ];
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.unclutter-xfixes = {
+      description = "unclutter-xfixes";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+      serviceConfig.ExecStart = ''
+        ${cfg.package}/bin/unclutter \
+          --timeout ${toString cfg.timeout} \
+          --jitter ${toString (cfg.threshold - 1)} \
+          ${concatMapStrings (x: " --"+x) cfg.extraOptions} \
+      '';
+      serviceConfig.RestartSec = 3;
+      serviceConfig.Restart = "always";
+    };
+  };
+}
diff --git a/nixos/modules/services/x11/unclutter.nix b/nixos/modules/services/x11/unclutter.nix
new file mode 100644
index 00000000000..bdb5fa7b50c
--- /dev/null
+++ b/nixos/modules/services/x11/unclutter.nix
@@ -0,0 +1,82 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.unclutter;
+
+in {
+  options.services.unclutter = {
+
+    enable = mkOption {
+      description = "Enable unclutter to hide your mouse cursor when inactive";
+      type = types.bool;
+      default = false;
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.unclutter;
+      defaultText = literalExpression "pkgs.unclutter";
+      description = "unclutter derivation to use.";
+    };
+
+    keystroke = mkOption {
+      description = "Wait for a keystroke before hiding the cursor";
+      type = types.bool;
+      default = false;
+    };
+
+    timeout = mkOption {
+      description = "Number of seconds before the cursor is marked inactive";
+      type = types.int;
+      default = 1;
+    };
+
+    threshold = mkOption {
+      description = "Minimum number of pixels considered cursor movement";
+      type = types.int;
+      default = 1;
+    };
+
+    excluded = mkOption {
+      description = "Names of windows where unclutter should not apply";
+      type = types.listOf types.str;
+      default = [];
+      example = [ "" ];
+    };
+
+    extraOptions = mkOption {
+      description = "More arguments to pass to the unclutter command";
+      type = types.listOf types.str;
+      default = [];
+      example = [ "noevent" "grab" ];
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.unclutter = {
+      description = "unclutter";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+      serviceConfig.ExecStart = ''
+        ${cfg.package}/bin/unclutter \
+          -idle ${toString cfg.timeout} \
+          -jitter ${toString (cfg.threshold - 1)} \
+          ${optionalString cfg.keystroke "-keystroke"} \
+          ${concatMapStrings (x: " -"+x) cfg.extraOptions} \
+          -not ${concatStringsSep " " cfg.excluded} \
+      '';
+      serviceConfig.PassEnvironment = "DISPLAY";
+      serviceConfig.RestartSec = 3;
+      serviceConfig.Restart = "always";
+    };
+  };
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "unclutter" "threeshold" ]
+                           [ "services"  "unclutter" "threshold" ])
+  ];
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+}
diff --git a/nixos/modules/services/x11/urserver.nix b/nixos/modules/services/x11/urserver.nix
new file mode 100644
index 00000000000..0beb62eb766
--- /dev/null
+++ b/nixos/modules/services/x11/urserver.nix
@@ -0,0 +1,38 @@
+# urserver service
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.urserver;
+in {
+
+  options.services.urserver.enable = lib.mkEnableOption "urserver";
+
+  config = lib.mkIf cfg.enable {
+
+    networking.firewall = {
+      allowedTCPPorts = [ 9510 9512 ];
+      allowedUDPPorts = [ 9511 9512 ];
+    };
+
+    systemd.user.services.urserver =  {
+      description = ''
+        Server for Unified Remote: The one-and-only remote for your computer.
+      '';
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = ''
+          ${pkgs.urserver}/bin/urserver --daemon
+        '';
+        ExecStop = ''
+          ${pkgs.procps}/bin/pkill urserver
+        '';
+        RestartSec = 3;
+        Restart = "on-failure";
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/services/x11/urxvtd.nix b/nixos/modules/services/x11/urxvtd.nix
new file mode 100644
index 00000000000..0a0df447f4e
--- /dev/null
+++ b/nixos/modules/services/x11/urxvtd.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, ... }:
+
+# maintainer: siddharthist
+
+with lib;
+
+let
+  cfg = config.services.urxvtd;
+in {
+  options.services.urxvtd = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable urxvtd, the urxvt terminal daemon. To use urxvtd, run
+        "urxvtc".
+      '';
+    };
+
+    package = mkOption {
+      default = pkgs.rxvt-unicode;
+      defaultText = literalExpression "pkgs.rxvt-unicode";
+      description = ''
+        Package to install. Usually pkgs.rxvt-unicode.
+      '';
+      type = types.package;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.urxvtd = {
+      description = "urxvt terminal daemon";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+      path = [ pkgs.xsel ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/urxvtd -o";
+        Environment = "RXVT_SOCKET=%t/urxvtd-socket";
+        Restart = "on-failure";
+        RestartSec = "5s";
+      };
+    };
+
+    environment.systemPackages = [ cfg.package ];
+    environment.variables.RXVT_SOCKET = "/run/user/$(id -u)/urxvtd-socket";
+  };
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+}
diff --git a/nixos/modules/services/x11/window-managers/2bwm.nix b/nixos/modules/services/x11/window-managers/2bwm.nix
new file mode 100644
index 00000000000..fdbdf35b0f5
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/2bwm.nix
@@ -0,0 +1,37 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.windowManager."2bwm";
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+    services.xserver.windowManager."2bwm".enable = mkEnableOption "2bwm";
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.xserver.windowManager.session = singleton
+      { name = "2bwm";
+        start =
+          ''
+            ${pkgs._2bwm}/bin/2bwm &
+            waitPID=$!
+          '';
+      };
+
+    environment.systemPackages = [ pkgs._2bwm ];
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/window-managers/afterstep.nix b/nixos/modules/services/x11/window-managers/afterstep.nix
new file mode 100644
index 00000000000..ba88a64c702
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/afterstep.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.afterstep;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.afterstep.enable = mkEnableOption "afterstep";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "afterstep";
+      start = ''
+        ${pkgs.afterstep}/bin/afterstep &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.afterstep ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/awesome.nix b/nixos/modules/services/x11/window-managers/awesome.nix
new file mode 100644
index 00000000000..c6c0c934f9a
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/awesome.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.windowManager.awesome;
+  awesome = cfg.package;
+  getLuaPath = lib : dir : "${lib}/${dir}/lua/${pkgs.luaPackages.lua.luaversion}";
+  makeSearchPath = lib.concatMapStrings (path:
+    " --search " + (getLuaPath path "share") +
+    " --search " + (getLuaPath path "lib")
+  );
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.xserver.windowManager.awesome = {
+
+      enable = mkEnableOption "Awesome window manager";
+
+      luaModules = mkOption {
+        default = [];
+        type = types.listOf types.package;
+        description = "List of lua packages available for being used in the Awesome configuration.";
+        example = literalExpression "[ pkgs.luaPackages.vicious ]";
+      };
+
+      package = mkOption {
+        default = null;
+        type = types.nullOr types.package;
+        description = "Package to use for running the Awesome WM.";
+        apply = pkg: if pkg == null then pkgs.awesome else pkg;
+      };
+
+      noArgb = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Disable client transparency support, which can be greatly detrimental to performance in some setups";
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.xserver.windowManager.session = singleton
+      { name = "awesome";
+        start =
+          ''
+            ${awesome}/bin/awesome ${lib.optionalString cfg.noArgb "--no-argb"} ${makeSearchPath cfg.luaModules} &
+            waitPID=$!
+          '';
+      };
+
+    environment.systemPackages = [ awesome ];
+
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/berry.nix b/nixos/modules/services/x11/window-managers/berry.nix
new file mode 100644
index 00000000000..0d2285e7a60
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/berry.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.berry;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.berry.enable = mkEnableOption "berry";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "berry";
+      start = ''
+        ${pkgs.berry}/bin/berry &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.berry ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/bspwm.nix b/nixos/modules/services/x11/window-managers/bspwm.nix
new file mode 100644
index 00000000000..ade24061a06
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/bspwm.nix
@@ -0,0 +1,77 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.bspwm;
+in
+
+{
+  options = {
+    services.xserver.windowManager.bspwm = {
+      enable = mkEnableOption "bspwm";
+
+      package = mkOption {
+        type        = types.package;
+        default     = pkgs.bspwm;
+        defaultText = literalExpression "pkgs.bspwm";
+        example     = literalExpression "pkgs.bspwm-unstable";
+        description = ''
+          bspwm package to use.
+        '';
+      };
+      configFile = mkOption {
+        type        = with types; nullOr path;
+        example     = literalExpression ''"''${pkgs.bspwm}/share/doc/bspwm/examples/bspwmrc"'';
+        default     = null;
+        description = ''
+          Path to the bspwm configuration file.
+          If null, $HOME/.config/bspwm/bspwmrc will be used.
+        '';
+      };
+
+      sxhkd = {
+        package = mkOption {
+          type        = types.package;
+          default     = pkgs.sxhkd;
+          defaultText = literalExpression "pkgs.sxhkd";
+          example     = literalExpression "pkgs.sxhkd-unstable";
+          description = ''
+            sxhkd package to use.
+          '';
+        };
+        configFile = mkOption {
+          type        = with types; nullOr path;
+          example     = literalExpression ''"''${pkgs.bspwm}/share/doc/bspwm/examples/sxhkdrc"'';
+          default     = null;
+          description = ''
+            Path to the sxhkd configuration file.
+            If null, $HOME/.config/sxhkd/sxhkdrc will be used.
+          '';
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name  = "bspwm";
+      start = ''
+        export _JAVA_AWT_WM_NONREPARENTING=1
+        SXHKD_SHELL=/bin/sh ${cfg.sxhkd.package}/bin/sxhkd ${optionalString (cfg.sxhkd.configFile != null) "-c \"${cfg.sxhkd.configFile}\""} &
+        ${cfg.package}/bin/bspwm ${optionalString (cfg.configFile != null) "-c \"${cfg.configFile}\""} &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ cfg.package ];
+  };
+
+  imports = [
+   (mkRemovedOptionModule [ "services" "xserver" "windowManager" "bspwm-unstable" "enable" ]
+     "Use services.xserver.windowManager.bspwm.enable and set services.xserver.windowManager.bspwm.package to pkgs.bspwm-unstable to use the unstable version of bspwm.")
+   (mkRemovedOptionModule [ "services" "xserver" "windowManager" "bspwm" "startThroughSession" ]
+     "bspwm package does not provide bspwm-session anymore.")
+   (mkRemovedOptionModule [ "services" "xserver" "windowManager" "bspwm" "sessionScript" ]
+     "bspwm package does not provide bspwm-session anymore.")
+  ];
+}
diff --git a/nixos/modules/services/x11/window-managers/clfswm.nix b/nixos/modules/services/x11/window-managers/clfswm.nix
new file mode 100644
index 00000000000..78772c79974
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/clfswm.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.clfswm;
+in
+
+{
+  options = {
+    services.xserver.windowManager.clfswm = {
+      enable = mkEnableOption "clfswm";
+      package = mkOption {
+        type        = types.package;
+        default     = pkgs.lispPackages.clfswm;
+        defaultText = literalExpression "pkgs.lispPackages.clfswm";
+        description = ''
+          clfswm package to use.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "clfswm";
+      start = ''
+        ${cfg.package}/bin/clfswm &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/cwm.nix b/nixos/modules/services/x11/window-managers/cwm.nix
new file mode 100644
index 00000000000..03375a226bb
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/cwm.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.cwm;
+in
+{
+  options = {
+    services.xserver.windowManager.cwm.enable = mkEnableOption "cwm";
+  };
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton
+      { name = "cwm";
+        start =
+          ''
+            cwm &
+            waitPID=$!
+          '';
+      };
+    environment.systemPackages = [ pkgs.cwm ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/default.nix b/nixos/modules/services/x11/window-managers/default.nix
new file mode 100644
index 00000000000..d71738ea633
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/default.nix
@@ -0,0 +1,88 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager;
+in
+
+{
+  imports = [
+    ./2bwm.nix
+    ./afterstep.nix
+    ./berry.nix
+    ./bspwm.nix
+    ./cwm.nix
+    ./clfswm.nix
+    ./dwm.nix
+    ./e16.nix
+    ./evilwm.nix
+    ./exwm.nix
+    ./fluxbox.nix
+    ./fvwm.nix
+    ./herbstluftwm.nix
+    ./i3.nix
+    ./jwm.nix
+    ./leftwm.nix
+    ./lwm.nix
+    ./metacity.nix
+    ./mlvwm.nix
+    ./mwm.nix
+    ./openbox.nix
+    ./pekwm.nix
+    ./notion.nix
+    ./ratpoison.nix
+    ./sawfish.nix
+    ./smallwm.nix
+    ./stumpwm.nix
+    ./spectrwm.nix
+    ./tinywm.nix
+    ./twm.nix
+    ./windowmaker.nix
+    ./wmderland.nix
+    ./wmii.nix
+    ./xmonad.nix
+    ./yeahwm.nix
+    ./qtile.nix
+    ./none.nix ];
+
+  options = {
+
+    services.xserver.windowManager = {
+
+      session = mkOption {
+        internal = true;
+        default = [];
+        example = [{
+          name = "wmii";
+          start = "...";
+        }];
+        description = ''
+          Internal option used to add some common line to window manager
+          scripts before forwarding the value to the
+          <varname>displayManager</varname>.
+        '';
+        apply = map (d: d // {
+          manage = "window";
+        });
+      };
+
+      default = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "wmii";
+        description = ''
+          <emphasis role="strong">Deprecated</emphasis>, please use <xref linkend="opt-services.xserver.displayManager.defaultSession"/> instead.
+
+          Default window manager loaded if none have been chosen.
+        '';
+      };
+
+    };
+
+  };
+
+  config = {
+    services.xserver.displayManager.session = cfg.session;
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/dwm.nix b/nixos/modules/services/x11/window-managers/dwm.nix
new file mode 100644
index 00000000000..7777913ce1e
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/dwm.nix
@@ -0,0 +1,37 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.windowManager.dwm;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+    services.xserver.windowManager.dwm.enable = mkEnableOption "dwm";
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.xserver.windowManager.session = singleton
+      { name = "dwm";
+        start =
+          ''
+            dwm &
+            waitPID=$!
+          '';
+      };
+
+    environment.systemPackages = [ pkgs.dwm ];
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/window-managers/e16.nix b/nixos/modules/services/x11/window-managers/e16.nix
new file mode 100644
index 00000000000..3e1a22c4dab
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/e16.nix
@@ -0,0 +1,26 @@
+{ config , lib , pkgs , ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.e16;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.e16.enable = mkEnableOption "e16";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "E16";
+      start = ''
+        ${pkgs.e16}/bin/e16 &
+        waitPID=$!
+      '';
+    };
+
+    environment.systemPackages = [ pkgs.e16 ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/evilwm.nix b/nixos/modules/services/x11/window-managers/evilwm.nix
new file mode 100644
index 00000000000..6f1db2110f8
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/evilwm.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.evilwm;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.evilwm.enable = mkEnableOption "evilwm";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "evilwm";
+      start = ''
+        ${pkgs.evilwm}/bin/evilwm &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.evilwm ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/exwm.nix b/nixos/modules/services/x11/window-managers/exwm.nix
new file mode 100644
index 00000000000..b505f720f04
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/exwm.nix
@@ -0,0 +1,69 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.exwm;
+  loadScript = pkgs.writeText "emacs-exwm-load" ''
+    ${cfg.loadScript}
+    ${optionalString cfg.enableDefaultConfig ''
+      (require 'exwm-config)
+      (exwm-config-default)
+    ''}
+  '';
+  packages = epkgs: cfg.extraPackages epkgs ++ [ epkgs.exwm ];
+  exwm-emacs = pkgs.emacsWithPackages packages;
+in
+
+{
+  options = {
+    services.xserver.windowManager.exwm = {
+      enable = mkEnableOption "exwm";
+      loadScript = mkOption {
+        default = "(require 'exwm)";
+        type = types.lines;
+        example = ''
+          (require 'exwm)
+          (exwm-enable)
+        '';
+        description = ''
+          Emacs lisp code to be run after loading the user's init
+          file. If enableDefaultConfig is true, this will be run
+          before loading the default config.
+        '';
+      };
+      enableDefaultConfig = mkOption {
+        default = true;
+        type = lib.types.bool;
+        description = "Enable an uncustomised exwm configuration.";
+      };
+      extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
+        default = epkgs: [];
+        defaultText = literalExpression "epkgs: []";
+        example = literalExpression ''
+          epkgs: [
+            epkgs.emms
+            epkgs.magit
+            epkgs.proofgeneral
+          ]
+        '';
+        description = ''
+          Extra packages available to Emacs. The value must be a
+          function which receives the attrset defined in
+          <varname>emacs.pkgs</varname> as the sole argument.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "exwm";
+      start = ''
+        ${exwm-emacs}/bin/emacs -l ${loadScript}
+      '';
+    };
+    environment.systemPackages = [ exwm-emacs ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/fluxbox.nix b/nixos/modules/services/x11/window-managers/fluxbox.nix
new file mode 100644
index 00000000000..b409335702a
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/fluxbox.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.fluxbox;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.fluxbox.enable = mkEnableOption "fluxbox";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "fluxbox";
+      start = ''
+        ${pkgs.fluxbox}/bin/startfluxbox &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.fluxbox ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/fvwm.nix b/nixos/modules/services/x11/window-managers/fvwm.nix
new file mode 100644
index 00000000000..e283886ecc4
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/fvwm.nix
@@ -0,0 +1,41 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.fvwm;
+  fvwm = pkgs.fvwm.override { enableGestures = cfg.gestures; };
+in
+
+{
+
+  ###### interface
+
+  options = {
+    services.xserver.windowManager.fvwm = {
+      enable = mkEnableOption "Fvwm window manager";
+
+      gestures = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether or not to enable libstroke for gesture support";
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton
+      { name = "fvwm";
+        start =
+          ''
+            ${fvwm}/bin/fvwm &
+            waitPID=$!
+          '';
+      };
+
+    environment.systemPackages = [ fvwm ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/herbstluftwm.nix b/nixos/modules/services/x11/window-managers/herbstluftwm.nix
new file mode 100644
index 00000000000..354d70c695c
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/herbstluftwm.nix
@@ -0,0 +1,47 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.herbstluftwm;
+in
+
+{
+  options = {
+    services.xserver.windowManager.herbstluftwm = {
+      enable = mkEnableOption "herbstluftwm";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.herbstluftwm;
+        defaultText = literalExpression "pkgs.herbstluftwm";
+        description = ''
+          Herbstluftwm package to use.
+        '';
+      };
+
+      configFile = mkOption {
+        default     = null;
+        type        = with types; nullOr path;
+        description = ''
+          Path to the herbstluftwm configuration file.  If left at the
+          default value, $XDG_CONFIG_HOME/herbstluftwm/autostart will
+          be used.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "herbstluftwm";
+      start =
+        let configFileClause = optionalString
+            (cfg.configFile != null)
+            ''-c "${cfg.configFile}"''
+            ;
+        in "${cfg.package}/bin/herbstluftwm ${configFileClause}";
+    };
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/i3.nix b/nixos/modules/services/x11/window-managers/i3.nix
new file mode 100644
index 00000000000..99f9997024f
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/i3.nix
@@ -0,0 +1,78 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.i3;
+in
+
+{
+  options.services.xserver.windowManager.i3 = {
+    enable = mkEnableOption "i3 window manager";
+
+    configFile = mkOption {
+      default     = null;
+      type        = with types; nullOr path;
+      description = ''
+        Path to the i3 configuration file.
+        If left at the default value, $HOME/.i3/config will be used.
+      '';
+    };
+
+    extraSessionCommands = mkOption {
+      default     = "";
+      type        = types.lines;
+      description = ''
+        Shell commands executed just before i3 is started.
+      '';
+    };
+
+    package = mkOption {
+      type        = types.package;
+      default     = pkgs.i3;
+      defaultText = literalExpression "pkgs.i3";
+      example     = literalExpression "pkgs.i3-gaps";
+      description = ''
+        i3 package to use.
+      '';
+    };
+
+    extraPackages = mkOption {
+      type = with types; listOf package;
+      default = with pkgs; [ dmenu i3status i3lock ];
+      defaultText = literalExpression ''
+        with pkgs; [
+          dmenu
+          i3status
+          i3lock
+        ]
+      '';
+      description = ''
+        Extra packages to be installed system wide.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = [{
+      name  = "i3";
+      start = ''
+        ${cfg.extraSessionCommands}
+
+        ${cfg.package}/bin/i3 ${optionalString (cfg.configFile != null)
+          "-c /etc/i3/config"
+        } &
+        waitPID=$!
+      '';
+    }];
+    environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages;
+    environment.etc."i3/config" = mkIf (cfg.configFile != null) {
+      source = cfg.configFile;
+    };
+  };
+
+  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.")
+  ];
+}
diff --git a/nixos/modules/services/x11/window-managers/icewm.nix b/nixos/modules/services/x11/window-managers/icewm.nix
new file mode 100644
index 00000000000..f4ae9222df6
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/icewm.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.icewm;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.icewm.enable = mkEnableOption "icewm";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton
+      { name = "icewm";
+        start =
+          ''
+            ${pkgs.icewm}/bin/icewm &
+            waitPID=$!
+          '';
+      };
+
+    environment.systemPackages = [ pkgs.icewm ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/jwm.nix b/nixos/modules/services/x11/window-managers/jwm.nix
new file mode 100644
index 00000000000..0e8dab2e922
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/jwm.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.jwm;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.jwm.enable = mkEnableOption "jwm";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "jwm";
+      start = ''
+        ${pkgs.jwm}/bin/jwm &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.jwm ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/leftwm.nix b/nixos/modules/services/x11/window-managers/leftwm.nix
new file mode 100644
index 00000000000..3ef40df95df
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/leftwm.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.leftwm;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.leftwm.enable = mkEnableOption "leftwm";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "leftwm";
+      start = ''
+        ${pkgs.leftwm}/bin/leftwm &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.leftwm ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/lwm.nix b/nixos/modules/services/x11/window-managers/lwm.nix
new file mode 100644
index 00000000000..e2aa062fd13
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/lwm.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.lwm;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.lwm.enable = mkEnableOption "lwm";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "lwm";
+      start = ''
+        ${pkgs.lwm}/bin/lwm &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.lwm ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/metacity.nix b/nixos/modules/services/x11/window-managers/metacity.nix
new file mode 100644
index 00000000000..600afe759b2
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/metacity.nix
@@ -0,0 +1,30 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.windowManager.metacity;
+  inherit (pkgs) gnome;
+in
+
+{
+  options = {
+    services.xserver.windowManager.metacity.enable = mkEnableOption "metacity";
+  };
+
+  config = mkIf cfg.enable {
+
+    services.xserver.windowManager.session = singleton
+      { name = "metacity";
+        start = ''
+          ${gnome.metacity}/bin/metacity &
+          waitPID=$!
+        '';
+      };
+
+    environment.systemPackages = [ gnome.metacity ];
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/window-managers/mlvwm.nix b/nixos/modules/services/x11/window-managers/mlvwm.nix
new file mode 100644
index 00000000000..08dd0402029
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/mlvwm.nix
@@ -0,0 +1,41 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.xserver.windowManager.mlvwm;
+
+in
+{
+
+  options.services.xserver.windowManager.mlvwm = {
+    enable = mkEnableOption "Macintosh-like Virtual Window Manager";
+
+    configFile = mkOption {
+      default = null;
+      type = with types; nullOr path;
+      description = ''
+        Path to the mlvwm configuration file.
+        If left at the default value, $HOME/.mlvwmrc will be used.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    services.xserver.windowManager.session = [{
+      name = "mlvwm";
+      start = ''
+        ${pkgs.mlvwm}/bin/mlvwm ${optionalString (cfg.configFile != null)
+          "-f /etc/mlvwm/mlvwmrc"
+        } &
+        waitPID=$!
+      '';
+    }];
+
+    environment.etc."mlvwm/mlvwmrc" = mkIf (cfg.configFile != null) {
+      source = cfg.configFile;
+    };
+
+    environment.systemPackages = [ pkgs.mlvwm ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/mwm.nix b/nixos/modules/services/x11/window-managers/mwm.nix
new file mode 100644
index 00000000000..31f7b725f74
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/mwm.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.mwm;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.mwm.enable = mkEnableOption "mwm";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "mwm";
+      start = ''
+        ${pkgs.motif}/bin/mwm &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.motif ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/none.nix b/nixos/modules/services/x11/window-managers/none.nix
new file mode 100644
index 00000000000..84cf1d77077
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/none.nix
@@ -0,0 +1,12 @@
+{
+  services = {
+    xserver = {
+      windowManager = {
+        session = [{
+          name = "none";
+          start = "";
+        }];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/notion.nix b/nixos/modules/services/x11/window-managers/notion.nix
new file mode 100644
index 00000000000..4ece0d241c9
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/notion.nix
@@ -0,0 +1,26 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.notion;
+in
+
+{
+  options = {
+    services.xserver.windowManager.notion.enable = mkEnableOption "notion";
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager = {
+      session = [{
+        name = "notion";
+        start = ''
+          ${pkgs.notion}/bin/notion &
+          waitPID=$!
+        '';
+      }];
+    };
+    environment.systemPackages = [ pkgs.notion ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/openbox.nix b/nixos/modules/services/x11/window-managers/openbox.nix
new file mode 100644
index 00000000000..165772d1aa0
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/openbox.nix
@@ -0,0 +1,24 @@
+{lib, pkgs, config, ...}:
+
+with lib;
+let
+  cfg = config.services.xserver.windowManager.openbox;
+in
+
+{
+  options = {
+    services.xserver.windowManager.openbox.enable = mkEnableOption "openbox";
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager = {
+      session = [{
+        name = "openbox";
+        start = "
+          ${pkgs.openbox}/bin/openbox-session
+        ";
+      }];
+    };
+    environment.systemPackages = [ pkgs.openbox ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/oroborus.nix b/nixos/modules/services/x11/window-managers/oroborus.nix
new file mode 100644
index 00000000000..bd7e3396864
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/oroborus.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.oroborus;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.oroborus.enable = mkEnableOption "oroborus";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "oroborus";
+      start = ''
+        ${pkgs.oroborus}/bin/oroborus &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.oroborus ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/pekwm.nix b/nixos/modules/services/x11/window-managers/pekwm.nix
new file mode 100644
index 00000000000..850335ce7dd
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/pekwm.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.pekwm;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.pekwm.enable = mkEnableOption "pekwm";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "pekwm";
+      start = ''
+        ${pkgs.pekwm}/bin/pekwm &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.pekwm ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/qtile.nix b/nixos/modules/services/x11/window-managers/qtile.nix
new file mode 100644
index 00000000000..835b41d4ada
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/qtile.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.qtile;
+in
+
+{
+  options = {
+    services.xserver.windowManager.qtile.enable = mkEnableOption "qtile";
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = [{
+      name = "qtile";
+      start = ''
+        ${pkgs.qtile}/bin/qtile start &
+        waitPID=$!
+      '';
+    }];
+
+    environment.systemPackages = [ pkgs.qtile ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/ratpoison.nix b/nixos/modules/services/x11/window-managers/ratpoison.nix
new file mode 100644
index 00000000000..0d58481d457
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/ratpoison.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.ratpoison;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.ratpoison.enable = mkEnableOption "ratpoison";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "ratpoison";
+      start = ''
+        ${pkgs.ratpoison}/bin/ratpoison &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.ratpoison ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/sawfish.nix b/nixos/modules/services/x11/window-managers/sawfish.nix
new file mode 100644
index 00000000000..b988b5e1829
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/sawfish.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.sawfish;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.sawfish.enable = mkEnableOption "sawfish";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "sawfish";
+      start = ''
+        ${pkgs.sawfish}/bin/sawfish &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.sawfish ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/smallwm.nix b/nixos/modules/services/x11/window-managers/smallwm.nix
new file mode 100644
index 00000000000..091ba4f92b9
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/smallwm.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.smallwm;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.smallwm.enable = mkEnableOption "smallwm";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "smallwm";
+      start = ''
+        ${pkgs.smallwm}/bin/smallwm &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.smallwm ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/spectrwm.nix b/nixos/modules/services/x11/window-managers/spectrwm.nix
new file mode 100644
index 00000000000..a1dc298d242
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/spectrwm.nix
@@ -0,0 +1,27 @@
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.spectrwm;
+in
+
+{
+  options = {
+    services.xserver.windowManager.spectrwm.enable = mkEnableOption "spectrwm";
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager = {
+      session = [{
+        name = "spectrwm";
+        start = ''
+          ${pkgs.spectrwm}/bin/spectrwm &
+          waitPID=$!
+        '';
+      }];
+    };
+    environment.systemPackages = [ pkgs.spectrwm ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/stumpwm.nix b/nixos/modules/services/x11/window-managers/stumpwm.nix
new file mode 100644
index 00000000000..27a17178476
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/stumpwm.nix
@@ -0,0 +1,24 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.stumpwm;
+in
+
+{
+  options = {
+    services.xserver.windowManager.stumpwm.enable = mkEnableOption "stumpwm";
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "stumpwm";
+      start = ''
+        ${pkgs.lispPackages.stumpwm}/bin/stumpwm &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.lispPackages.stumpwm ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/tinywm.nix b/nixos/modules/services/x11/window-managers/tinywm.nix
new file mode 100644
index 00000000000..8e5d9b9170c
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/tinywm.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.tinywm;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.tinywm.enable = mkEnableOption "tinywm";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "tinywm";
+      start = ''
+        ${pkgs.tinywm}/bin/tinywm &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.tinywm ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/twm.nix b/nixos/modules/services/x11/window-managers/twm.nix
new file mode 100644
index 00000000000..fc09901aae3
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/twm.nix
@@ -0,0 +1,37 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.windowManager.twm;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+    services.xserver.windowManager.twm.enable = mkEnableOption "twm";
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.xserver.windowManager.session = singleton
+      { name = "twm";
+        start =
+          ''
+            ${pkgs.xorg.twm}/bin/twm &
+            waitPID=$!
+          '';
+      };
+
+    environment.systemPackages = [ pkgs.xorg.twm ];
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/window-managers/windowlab.nix b/nixos/modules/services/x11/window-managers/windowlab.nix
new file mode 100644
index 00000000000..fb891a39fa4
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/windowlab.nix
@@ -0,0 +1,22 @@
+{lib, pkgs, config, ...}:
+
+let
+  cfg = config.services.xserver.windowManager.windowlab;
+in
+
+{
+  options = {
+    services.xserver.windowManager.windowlab.enable =
+      lib.mkEnableOption "windowlab";
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.xserver.windowManager = {
+      session =
+        [{ name  = "windowlab";
+           start = "${pkgs.windowlab}/bin/windowlab";
+        }];
+    };
+    environment.systemPackages = [ pkgs.windowlab ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/windowmaker.nix b/nixos/modules/services/x11/window-managers/windowmaker.nix
new file mode 100644
index 00000000000..b6272375805
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/windowmaker.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.windowmaker;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.windowmaker.enable = mkEnableOption "windowmaker";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "windowmaker";
+      start = ''
+        ${pkgs.windowmaker}/bin/wmaker &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.windowmaker ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/wmderland.nix b/nixos/modules/services/x11/window-managers/wmderland.nix
new file mode 100644
index 00000000000..56b69220965
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/wmderland.nix
@@ -0,0 +1,61 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.wmderland;
+in
+
+{
+  options.services.xserver.windowManager.wmderland = {
+    enable = mkEnableOption "wmderland";
+
+    extraSessionCommands = mkOption {
+      default = "";
+      type = types.lines;
+      description = ''
+        Shell commands executed just before wmderland is started.
+      '';
+    };
+
+    extraPackages = mkOption {
+      type = with types; listOf package;
+      default = with pkgs; [
+        rofi
+        dunst
+        light
+        hsetroot
+        feh
+        rxvt-unicode
+      ];
+      defaultText = literalExpression ''
+        with pkgs; [
+          rofi
+          dunst
+          light
+          hsetroot
+          feh
+          rxvt-unicode
+        ]
+      '';
+      description = ''
+        Extra packages to be installed system wide.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "wmderland";
+      start = ''
+        ${cfg.extraSessionCommands}
+
+        ${pkgs.wmderland}/bin/wmderland &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [
+      pkgs.wmderland pkgs.wmderlandc
+    ] ++ cfg.extraPackages;
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/wmii.nix b/nixos/modules/services/x11/window-managers/wmii.nix
new file mode 100644
index 00000000000..9b50a99bf23
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/wmii.nix
@@ -0,0 +1,39 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.xserver.windowManager.wmii;
+  wmii = pkgs.wmii_hg;
+in
+{
+  options = {
+    services.xserver.windowManager.wmii.enable = mkEnableOption "wmii";
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton
+      # stop wmii by
+      #   $wmiir xwrite /ctl quit
+      # this will cause wmii exiting with exit code 0
+      # (or "mod+a quit", which is bound to do the same thing in wmiirc
+      # by default)
+      #
+      # why this loop?
+      # wmii crashes once a month here. That doesn't matter that much
+      # wmii can recover very well. However without loop the X session
+      # terminates and then your workspace setup is lost and all
+      # applications running on X will terminate.
+      # Another use case is kill -9 wmii; after rotating screen.
+      # Note: we don't like kill for that purpose. But it works (->
+      # subject "wmii and xrandr" on mailinglist)
+      { name = "wmii";
+        start = ''
+          while :; do
+            ${wmii}/bin/wmii && break
+          done
+        '';
+      };
+
+    environment.systemPackages = [ wmii ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/xmonad.nix b/nixos/modules/services/x11/window-managers/xmonad.nix
new file mode 100644
index 00000000000..68f97c2f504
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/xmonad.nix
@@ -0,0 +1,203 @@
+{pkgs, lib, config, ...}:
+
+with lib;
+let
+  inherit (lib) mkOption mkIf optionals literalExpression optionalString;
+  cfg = config.services.xserver.windowManager.xmonad;
+
+  ghcWithPackages = cfg.haskellPackages.ghcWithPackages;
+  packages = self: cfg.extraPackages self ++
+                   optionals cfg.enableContribAndExtras
+                   [ self.xmonad-contrib self.xmonad-extras ];
+
+  xmonad-vanilla = pkgs.xmonad-with-packages.override {
+    inherit ghcWithPackages packages;
+  };
+
+  xmonad-config =
+    let
+      xmonadAndPackages = self: [ self.xmonad ] ++ packages self;
+      xmonadEnv = ghcWithPackages xmonadAndPackages;
+      configured = pkgs.writers.writeHaskellBin "xmonad" {
+        ghc = cfg.haskellPackages.ghc;
+        libraries = xmonadAndPackages cfg.haskellPackages;
+        inherit (cfg) ghcArgs;
+      } cfg.config;
+    in
+      pkgs.runCommandLocal "xmonad" {
+        nativeBuildInputs = [ pkgs.makeWrapper ];
+      } (''
+        install -D ${xmonadEnv}/share/man/man1/xmonad.1.gz $out/share/man/man1/xmonad.1.gz
+        makeWrapper ${configured}/bin/xmonad $out/bin/xmonad \
+      '' + optionalString cfg.enableConfiguredRecompile ''
+          --set NIX_GHC "${xmonadEnv}/bin/ghc" \
+      '' + ''
+          --set XMONAD_XMESSAGE "${pkgs.xorg.xmessage}/bin/xmessage"
+      '');
+
+  xmonad = if (cfg.config != null) then xmonad-config else xmonad-vanilla;
+in {
+  meta.maintainers = with maintainers; [ lassulus xaverdh ivanbrennan ];
+
+  options = {
+    services.xserver.windowManager.xmonad = {
+      enable = mkEnableOption "xmonad";
+
+      haskellPackages = mkOption {
+        default = pkgs.haskellPackages;
+        defaultText = literalExpression "pkgs.haskellPackages";
+        example = literalExpression "pkgs.haskell.packages.ghc784";
+        type = types.attrs;
+        description = ''
+          haskellPackages used to build Xmonad and other packages.
+          This can be used to change the GHC version used to build
+          Xmonad and the packages listed in
+          <varname>extraPackages</varname>.
+        '';
+      };
+
+      extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
+        default = self: [];
+        defaultText = literalExpression "self: []";
+        example = literalExpression ''
+          haskellPackages: [
+            haskellPackages.xmonad-contrib
+            haskellPackages.monad-logger
+          ]
+        '';
+        description = ''
+          Extra packages available to ghc when rebuilding Xmonad. The
+          value must be a function which receives the attrset defined
+          in <varname>haskellPackages</varname> as the sole argument.
+        '';
+      };
+
+      enableContribAndExtras = mkOption {
+        default = false;
+        type = lib.types.bool;
+        description = "Enable xmonad-{contrib,extras} in Xmonad.";
+      };
+
+      config = mkOption {
+        default = null;
+        type = with lib.types; nullOr (either path str);
+        description = ''
+          Configuration from which XMonad gets compiled. If no value is
+          specified, a vanilla xmonad binary is put in PATH, which will
+          attempt to recompile and exec your xmonad config from $HOME/.xmonad.
+          This setup is then analogous to other (non-NixOS) linux distributions.
+
+          If you do set this option, you likely want to use "launch" as your
+          entry point for xmonad (as in the example), to avoid xmonad's
+          recompilation logic on startup. Doing so will render the default
+          "mod+q" restart key binding dysfunctional though, because that attempts
+          to call your binary with the "--restart" command line option, unless
+          you implement that yourself. You way mant to bind "mod+q" to
+          <literal>(restart "xmonad" True)</literal> instead, which will just restart
+          xmonad from PATH. This allows e.g. switching to the new xmonad binary
+          after rebuilding your system with nixos-rebuild.
+          For the same reason, ghc is not added to the environment when this
+          option is set, unless <option>enableConfiguredRecompile</option> is
+          set to <literal>true</literal>.
+
+          If you actually want to run xmonad with a config specified here, but
+          also be able to recompile and restart it from a copy of that source in
+          $HOME/.xmonad on the fly, set <option>enableConfiguredRecompile</option>
+          to <literal>true</literal> and implement something like "compileRestart"
+          from the example.
+          This should allow you to switch at will between the local xmonad and
+          the one NixOS puts in your PATH.
+        '';
+        example = ''
+          import XMonad
+          import XMonad.Util.EZConfig (additionalKeys)
+          import Control.Monad (when)
+          import Text.Printf (printf)
+          import System.Posix.Process (executeFile)
+          import System.Info (arch,os)
+          import System.Environment (getArgs)
+          import System.FilePath ((</>))
+
+          compiledConfig = printf "xmonad-%s-%s" arch os
+
+          myConfig = defaultConfig
+            { modMask = mod4Mask -- Use Super instead of Alt
+            , terminal = "urxvt" }
+            `additionalKeys`
+            [ ( (mod4Mask,xK_r), compileRestart True)
+            , ( (mod4Mask,xK_q), restart "xmonad" True ) ]
+
+          --------------------------------------------
+          {- version 0.17.0 -}
+          --------------------------------------------
+          -- compileRestart resume =
+          --   dirs <- io getDirectories
+          --   whenX (recompile dirs True) $
+          --     when resume writeStateToFile
+          --       *> catchIO
+          --         ( do
+          --             args <- getArgs
+          --             executeFile (cacheDir dirs </> compiledConfig) False args Nothing
+          --         )
+          --
+          -- main = getDirectories >>= launch myConfig
+          --------------------------------------------
+
+          compileRestart resume =
+            whenX (recompile True) $
+              when resume writeStateToFile
+                *> catchIO
+                  ( do
+                      dir <- getXMonadDataDir
+                      args <- getArgs
+                      executeFile (dir </> compiledConfig) False args Nothing
+                  )
+
+          main = launch myConfig
+        '';
+      };
+
+      enableConfiguredRecompile = mkOption {
+        default = false;
+        type = lib.types.bool;
+        description = ''
+          Enable recompilation even if <option>config</option> is set to a
+          non-null value. This adds the necessary Haskell dependencies (GHC with
+          packages) to the xmonad binary's environment.
+        '';
+      };
+
+      xmonadCliArgs = mkOption {
+        default = [];
+        type = with lib.types; listOf str;
+        description = ''
+          Command line arguments passed to the xmonad binary.
+        '';
+      };
+
+      ghcArgs = mkOption {
+        default = [];
+        type = with lib.types; listOf str;
+        description = ''
+          Command line arguments passed to the compiler (ghc)
+          invocation when xmonad.config is set.
+        '';
+      };
+
+    };
+  };
+  config = mkIf cfg.enable {
+    services.xserver.windowManager = {
+      session = [{
+        name = "xmonad";
+        start = ''
+           systemd-cat -t xmonad -- ${xmonad}/bin/xmonad ${lib.escapeShellArgs cfg.xmonadCliArgs} &
+           waitPID=$!
+        '';
+      }];
+    };
+
+    environment.systemPackages = [ xmonad ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/yeahwm.nix b/nixos/modules/services/x11/window-managers/yeahwm.nix
new file mode 100644
index 00000000000..351bd7dfe48
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/yeahwm.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.yeahwm;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.yeahwm.enable = mkEnableOption "yeahwm";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "yeahwm";
+      start = ''
+        ${pkgs.yeahwm}/bin/yeahwm &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.yeahwm ];
+  };
+}
diff --git a/nixos/modules/services/x11/xautolock.nix b/nixos/modules/services/x11/xautolock.nix
new file mode 100644
index 00000000000..947d8f4edfb
--- /dev/null
+++ b/nixos/modules/services/x11/xautolock.nix
@@ -0,0 +1,141 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.xautolock;
+in
+  {
+    options = {
+      services.xserver.xautolock = {
+        enable = mkEnableOption "xautolock";
+        enableNotifier = mkEnableOption "xautolock.notify" // {
+          description = ''
+            Whether to enable the notifier feature of xautolock.
+            This publishes a notification before the autolock.
+          '';
+        };
+
+        time = mkOption {
+          default = 15;
+          type = types.int;
+
+          description = ''
+            Idle time (in minutes) to wait until xautolock locks the computer.
+          '';
+        };
+
+        locker = mkOption {
+          default = "${pkgs.xlockmore}/bin/xlock"; # default according to `man xautolock`
+          defaultText = literalExpression ''"''${pkgs.xlockmore}/bin/xlock"'';
+          example = literalExpression ''"''${pkgs.i3lock}/bin/i3lock -i /path/to/img"'';
+          type = types.str;
+
+          description = ''
+            The script to use when automatically locking the computer.
+          '';
+        };
+
+        nowlocker = mkOption {
+          default = null;
+          example = literalExpression ''"''${pkgs.i3lock}/bin/i3lock -i /path/to/img"'';
+          type = types.nullOr types.str;
+
+          description = ''
+            The script to use when manually locking the computer with <command>xautolock -locknow</command>.
+          '';
+        };
+
+        notify = mkOption {
+          default = 10;
+          type = types.int;
+
+          description = ''
+            Time (in seconds) before the actual lock when the notification about the pending lock should be published.
+          '';
+        };
+
+        notifier = mkOption {
+          default = null;
+          example = literalExpression ''"''${pkgs.libnotify}/bin/notify-send 'Locking in 10 seconds'"'';
+          type = types.nullOr types.str;
+
+          description = ''
+            Notification script to be used to warn about the pending autolock.
+          '';
+        };
+
+        killer = mkOption {
+          default = null; # default according to `man xautolock` is none
+          example = "/run/current-system/systemd/bin/systemctl suspend";
+          type = types.nullOr types.str;
+
+          description = ''
+            The script to use when nothing has happend for as long as <option>killtime</option>
+          '';
+        };
+
+        killtime = mkOption {
+          default = 20; # default according to `man xautolock`
+          type = types.int;
+
+          description = ''
+            Minutes xautolock waits until it executes the script specified in <option>killer</option>
+            (Has to be at least 10 minutes)
+          '';
+        };
+
+        extraOptions = mkOption {
+          type = types.listOf types.str;
+          default = [ ];
+          example = [ "-detectsleep" ];
+          description = ''
+            Additional command-line arguments to pass to
+            <command>xautolock</command>.
+          '';
+        };
+      };
+    };
+
+    config = mkIf cfg.enable {
+      environment.systemPackages = with pkgs; [ xautolock ];
+      systemd.user.services.xautolock = {
+        description = "xautolock service";
+        wantedBy = [ "graphical-session.target" ];
+        partOf = [ "graphical-session.target" ];
+        serviceConfig = with lib; {
+          ExecStart = strings.concatStringsSep " " ([
+            "${pkgs.xautolock}/bin/xautolock"
+            "-noclose"
+            "-time ${toString cfg.time}"
+            "-locker '${cfg.locker}'"
+          ] ++ optionals cfg.enableNotifier [
+            "-notify ${toString cfg.notify}"
+            "-notifier '${cfg.notifier}'"
+          ] ++ optionals (cfg.nowlocker != null) [
+            "-nowlocker '${cfg.nowlocker}'"
+          ] ++ optionals (cfg.killer != null) [
+            "-killer '${cfg.killer}'"
+            "-killtime ${toString cfg.killtime}"
+          ] ++ cfg.extraOptions);
+          Restart = "always";
+        };
+      };
+      assertions = [
+        {
+          assertion = cfg.enableNotifier -> cfg.notifier != null;
+          message = "When enabling the notifier for xautolock, you also need to specify the notify script";
+        }
+        {
+          assertion = cfg.killer != null -> cfg.killtime >= 10;
+          message = "killtime has to be at least 10 minutes according to `man xautolock`";
+        }
+      ] ++ (lib.forEach [ "locker" "notifier" "nowlocker" "killer" ]
+        (option:
+        {
+          assertion = cfg.${option} != null -> builtins.substring 0 1 cfg.${option} == "/";
+          message = "Please specify a canonical path for `services.xserver.xautolock.${option}`";
+        })
+      );
+    };
+  }
diff --git a/nixos/modules/services/x11/xbanish.nix b/nixos/modules/services/x11/xbanish.nix
new file mode 100644
index 00000000000..b95fac68f16
--- /dev/null
+++ b/nixos/modules/services/x11/xbanish.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.xbanish;
+
+in {
+  options.services.xbanish = {
+
+    enable = mkEnableOption "xbanish";
+
+    arguments = mkOption {
+      description = "Arguments to pass to xbanish command";
+      default = "";
+      example = "-d -i shift";
+      type = types.str;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.xbanish = {
+      description = "xbanish hides the mouse pointer";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+      serviceConfig.ExecStart = ''
+        ${pkgs.xbanish}/bin/xbanish ${cfg.arguments}
+      '';
+      serviceConfig.Restart = "always";
+    };
+  };
+}
diff --git a/nixos/modules/services/x11/xfs.conf b/nixos/modules/services/x11/xfs.conf
new file mode 100644
index 00000000000..13dcf803db2
--- /dev/null
+++ b/nixos/modules/services/x11/xfs.conf
@@ -0,0 +1,15 @@
+# font server configuration file
+# $Xorg: config.cpp,v 1.3 2000/08/17 19:54:19 cpqbld Exp $
+
+clone-self = on
+use-syslog = off
+error-file = /var/log/xfs.log
+# in decipoints
+default-point-size = 120
+default-resolutions = 75,75,100,100
+
+# font cache control, specified in KB
+cache-hi-mark = 2048
+cache-low-mark = 1433
+cache-balance = 70
+catalogue = /run/current-system/sw/share/X11-fonts/
diff --git a/nixos/modules/services/x11/xfs.nix b/nixos/modules/services/x11/xfs.nix
new file mode 100644
index 00000000000..ea7cfa1aa43
--- /dev/null
+++ b/nixos/modules/services/x11/xfs.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  configFile = ./xfs.conf;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.xfs = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the X Font Server.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.xfs.enable {
+    assertions = singleton
+      { assertion = config.fonts.enableFontDir;
+        message = "Please enable fonts.enableFontDir to use the X Font Server.";
+      };
+
+    systemd.services.xfs = {
+      description = "X Font Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.xorg.xfs ];
+      script = "xfs -config ${configFile}";
+    };
+  };
+}
diff --git a/nixos/modules/services/x11/xserver.nix b/nixos/modules/services/x11/xserver.nix
new file mode 100644
index 00000000000..0c50d82b23b
--- /dev/null
+++ b/nixos/modules/services/x11/xserver.nix
@@ -0,0 +1,867 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  # Abbreviations.
+  cfg = config.services.xserver;
+  xorg = pkgs.xorg;
+
+
+  # Map video driver names to driver packages. FIXME: move into card-specific modules.
+  knownVideoDrivers = {
+    # Alias so people can keep using "virtualbox" instead of "vboxvideo".
+    virtualbox = { modules = [ xorg.xf86videovboxvideo ]; driverName = "vboxvideo"; };
+
+    # Alias so that "radeon" uses the xf86-video-ati driver.
+    radeon = { modules = [ xorg.xf86videoati ]; driverName = "ati"; };
+
+    # modesetting does not have a xf86videomodesetting package as it is included in xorgserver
+    modesetting = {};
+  };
+
+  fontsForXServer =
+    config.fonts.fonts ++
+    # We don't want these fonts in fonts.conf, because then modern,
+    # fontconfig-based applications will get horrible bitmapped
+    # Helvetica fonts.  It's better to get a substitution (like Nimbus
+    # Sans) than that horror.  But we do need the Adobe fonts for some
+    # old non-fontconfig applications.  (Possibly this could be done
+    # better using a fontconfig rule.)
+    [ pkgs.xorg.fontadobe100dpi
+      pkgs.xorg.fontadobe75dpi
+    ];
+
+  xrandrOptions = {
+    output = mkOption {
+      type = types.str;
+      example = "DVI-0";
+      description = ''
+        The output name of the monitor, as shown by <citerefentry>
+          <refentrytitle>xrandr</refentrytitle>
+          <manvolnum>1</manvolnum>
+        </citerefentry> invoked without arguments.
+      '';
+    };
+
+    primary = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether this head is treated as the primary monitor,
+      '';
+    };
+
+    monitorConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        DisplaySize 408 306
+        Option "DPMS" "false"
+      '';
+      description = ''
+        Extra lines to append to the <literal>Monitor</literal> section
+        verbatim. Available options are documented in the MONITOR section in
+        <citerefentry><refentrytitle>xorg.conf</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry>.
+      '';
+    };
+  };
+
+  # Just enumerate all heads without discarding XRandR output information.
+  xrandrHeads = let
+    mkHead = num: config: {
+      name = "multihead${toString num}";
+      inherit config;
+    };
+  in imap1 mkHead cfg.xrandrHeads;
+
+  xrandrDeviceSection = let
+    monitors = forEach xrandrHeads (h: ''
+      Option "monitor-${h.config.output}" "${h.name}"
+    '');
+  in concatStrings monitors;
+
+  # Here we chain every monitor from the left to right, so we have:
+  # m4 right of m3 right of m2 right of m1   .----.----.----.----.
+  # Which will end up in reverse ----------> | m1 | m2 | m3 | m4 |
+  #                                          `----^----^----^----'
+  xrandrMonitorSections = let
+    mkMonitor = previous: current: singleton {
+      inherit (current) name;
+      value = ''
+        Section "Monitor"
+          Identifier "${current.name}"
+          ${optionalString (current.config.primary) ''
+          Option "Primary" "true"
+          ''}
+          ${optionalString (previous != []) ''
+          Option "RightOf" "${(head previous).name}"
+          ''}
+          ${current.config.monitorConfig}
+        EndSection
+      '';
+    } ++ previous;
+    monitors = reverseList (foldl mkMonitor [] xrandrHeads);
+  in concatMapStrings (getAttr "value") monitors;
+
+  configFile = pkgs.runCommand "xserver.conf"
+    { fontpath = optionalString (cfg.fontPath != null)
+        ''FontPath "${cfg.fontPath}"'';
+      inherit (cfg) config;
+      preferLocalBuild = true;
+    }
+      ''
+        echo 'Section "Files"' >> $out
+        echo $fontpath >> $out
+
+        for i in ${toString fontsForXServer}; do
+          if test "''${i:0:''${#NIX_STORE}}" == "$NIX_STORE"; then
+            for j in $(find $i -name fonts.dir); do
+              echo "  FontPath \"$(dirname $j)\"" >> $out
+            done
+          fi
+        done
+
+        for i in $(find ${toString cfg.modules} -type d); do
+          if test $(echo $i/*.so* | wc -w) -ne 0; then
+            echo "  ModulePath \"$i\"" >> $out
+          fi
+        done
+
+        echo '${cfg.filesSection}' >> $out
+        echo 'EndSection' >> $out
+        echo >> $out
+
+        echo "$config" >> $out
+      ''; # */
+
+  prefixStringLines = prefix: str:
+    concatMapStringsSep "\n" (line: prefix + line) (splitString "\n" str);
+
+  indent = prefixStringLines "  ";
+in
+
+{
+
+  imports =
+    [ ./display-managers/default.nix
+      ./window-managers/default.nix
+      ./desktop-managers/default.nix
+      (mkRemovedOptionModule [ "services" "xserver" "startGnuPGAgent" ]
+        "See the 16.09 release notes for more information.")
+      (mkRemovedOptionModule
+        [ "services" "xserver" "startDbusSession" ]
+        "The user D-Bus session is now always socket activated and this option can safely be removed.")
+      (mkRemovedOptionModule ["services" "xserver" "useXFS" ]
+        "Use services.xserver.fontPath instead of useXFS")
+    ];
+
+
+  ###### interface
+
+  options = {
+
+    services.xserver = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the X server.
+        '';
+      };
+
+      autorun = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to start the X server automatically.
+        '';
+      };
+
+      exportConfiguration = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to symlink the X server configuration under
+          <filename>/etc/X11/xorg.conf</filename>.
+        '';
+      };
+
+      enableTCP = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to allow the X server to accept TCP connections.
+        '';
+      };
+
+      autoRepeatDelay = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          Sets the autorepeat delay (length of time in milliseconds that a key must be depressed before autorepeat starts).
+        '';
+      };
+
+      autoRepeatInterval = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          Sets the autorepeat interval (length of time in milliseconds that should elapse between autorepeat-generated keystrokes).
+        '';
+      };
+
+      inputClassSections = mkOption {
+        type = types.listOf types.lines;
+        default = [];
+        example = literalExpression ''
+          [ '''
+              Identifier      "Trackpoint Wheel Emulation"
+              MatchProduct    "ThinkPad USB Keyboard with TrackPoint"
+              Option          "EmulateWheel"          "true"
+              Option          "EmulateWheelButton"    "2"
+              Option          "Emulate3Buttons"       "false"
+            '''
+          ]
+        '';
+        description = "Content of additional InputClass sections of the X server configuration file.";
+      };
+
+      modules = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        example = literalExpression "[ pkgs.xf86_input_wacom ]";
+        description = "Packages to be added to the module search path of the X server.";
+      };
+
+      resolutions = mkOption {
+        type = types.listOf types.attrs;
+        default = [];
+        example = [ { x = 1600; y = 1200; } { x = 1024; y = 786; } ];
+        description = ''
+          The screen resolutions for the X server.  The first element
+          is the default resolution.  If this list is empty, the X
+          server will automatically configure the resolution.
+        '';
+      };
+
+      videoDrivers = mkOption {
+        type = types.listOf types.str;
+        default = [ "amdgpu" "radeon" "nouveau" "modesetting" "fbdev" ];
+        example = [
+          "nvidia" "nvidiaLegacy390" "nvidiaLegacy340" "nvidiaLegacy304"
+          "amdgpu-pro"
+        ];
+        # TODO(@oxij): think how to easily add the rest, like those nvidia things
+        relatedPackages = concatLists
+          (mapAttrsToList (n: v:
+            optional (hasPrefix "xf86video" n) {
+              path  = [ "xorg" n ];
+              title = removePrefix "xf86video" n;
+            }) pkgs.xorg);
+        description = ''
+          The names of the video drivers the configuration
+          supports. They will be tried in order until one that
+          supports your card is found.
+          Don't combine those with "incompatible" OpenGL implementations,
+          e.g. free ones (mesa-based) with proprietary ones.
+
+          For unfree "nvidia*", the supported GPU lists are on
+          https://www.nvidia.com/object/unix.html
+        '';
+      };
+
+      videoDriver = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "i810";
+        description = ''
+          The name of the video driver for your graphics card.  This
+          option is obsolete; please set the
+          <option>services.xserver.videoDrivers</option> instead.
+        '';
+      };
+
+      drivers = mkOption {
+        type = types.listOf types.attrs;
+        internal = true;
+        description = ''
+          A list of attribute sets specifying drivers to be loaded by
+          the X11 server.
+        '';
+      };
+
+      dpi = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          Force global DPI resolution to use for X server. It's recommended to
+          use this only when DPI is detected incorrectly; also consider using
+          <literal>Monitor</literal> section in configuration file instead.
+        '';
+      };
+
+      updateDbusEnvironment = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to update the DBus activation environment after launching the
+          desktop manager.
+        '';
+      };
+
+      layout = mkOption {
+        type = types.str;
+        default = "us";
+        description = ''
+          Keyboard layout, or multiple keyboard layouts separated by commas.
+        '';
+      };
+
+      xkbModel = mkOption {
+        type = types.str;
+        default = "pc104";
+        example = "presario";
+        description = ''
+          Keyboard model.
+        '';
+      };
+
+      xkbOptions = mkOption {
+        type = types.commas;
+        default = "terminate:ctrl_alt_bksp";
+        example = "grp:caps_toggle,grp_led:scroll";
+        description = ''
+          X keyboard options; layout switching goes here.
+        '';
+      };
+
+      xkbVariant = mkOption {
+        type = types.str;
+        default = "";
+        example = "colemak";
+        description = ''
+          X keyboard variant.
+        '';
+      };
+
+      xkbDir = mkOption {
+        type = types.path;
+        default = "${pkgs.xkeyboard_config}/etc/X11/xkb";
+        defaultText = literalExpression ''"''${pkgs.xkeyboard_config}/etc/X11/xkb"'';
+        description = ''
+          Path used for -xkbdir xserver parameter.
+        '';
+      };
+
+      config = mkOption {
+        type = types.lines;
+        description = ''
+          The contents of the configuration file of the X server
+          (<filename>xorg.conf</filename>).
+
+          This option is set by multiple modules, and the configs are
+          concatenated together.
+
+          In Xorg configs the last config entries take precedence,
+          so you may want to use <literal>lib.mkAfter</literal> on this option
+          to override NixOS's defaults.
+        '';
+      };
+
+      filesSection = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''FontPath "/path/to/my/fonts"'';
+        description = "Contents of the first <literal>Files</literal> section of the X server configuration file.";
+      };
+
+      deviceSection = mkOption {
+        type = types.lines;
+        default = "";
+        example = "VideoRAM 131072";
+        description = "Contents of the first Device section of the X server configuration file.";
+      };
+
+      screenSection = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          Option "RandRRotation" "on"
+        '';
+        description = "Contents of the first Screen section of the X server configuration file.";
+      };
+
+      monitorSection = mkOption {
+        type = types.lines;
+        default = "";
+        example = "HorizSync 28-49";
+        description = "Contents of the first Monitor section of the X server configuration file.";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Additional contents (sections) included in the X server configuration file";
+      };
+
+      xrandrHeads = mkOption {
+        default = [];
+        example = [
+          "HDMI-0"
+          { output = "DVI-0"; primary = true; }
+          { output = "DVI-1"; monitorConfig = "Option \"Rotate\" \"left\""; }
+        ];
+        type = with types; listOf (coercedTo str (output: {
+          inherit output;
+        }) (submodule { options = xrandrOptions; }));
+        # Set primary to true for the first head if no other has been set
+        # primary already.
+        apply = heads: let
+          hasPrimary = any (x: x.primary) heads;
+          firstPrimary = head heads // { primary = true; };
+          newHeads = singleton firstPrimary ++ tail heads;
+        in if heads != [] && !hasPrimary then newHeads else heads;
+        description = ''
+          Multiple monitor configuration, just specify a list of XRandR
+          outputs. The individual elements should be either simple strings or
+          an attribute set of output options.
+
+          If the element is a string, it is denoting the physical output for a
+          monitor, if it's an attribute set, you must at least provide the
+          <option>output</option> option.
+
+          The monitors will be mapped from left to right in the order of the
+          list.
+
+          By default, the first monitor will be set as the primary monitor if
+          none of the elements contain an option that has set
+          <option>primary</option> to <literal>true</literal>.
+
+          <note><para>Only one monitor is allowed to be primary.</para></note>
+
+          Be careful using this option with multiple graphic adapters or with
+          drivers that have poor support for XRandR, unexpected things might
+          happen with those.
+        '';
+      };
+
+      serverFlagsSection = mkOption {
+        default = "";
+        type = types.lines;
+        example =
+          ''
+          Option "BlankTime" "0"
+          Option "StandbyTime" "0"
+          Option "SuspendTime" "0"
+          Option "OffTime" "0"
+          '';
+        description = "Contents of the ServerFlags section of the X server configuration file.";
+      };
+
+      moduleSection = mkOption {
+        type = types.lines;
+        default = "";
+        example =
+          ''
+            SubSection "extmod"
+            EndSubsection
+          '';
+        description = "Contents of the Module section of the X server configuration file.";
+      };
+
+      serverLayoutSection = mkOption {
+        type = types.lines;
+        default = "";
+        example =
+          ''
+            Option "AIGLX" "true"
+          '';
+        description = "Contents of the ServerLayout section of the X server configuration file.";
+      };
+
+      extraDisplaySettings = mkOption {
+        type = types.lines;
+        default = "";
+        example = "Virtual 2048 2048";
+        description = "Lines to be added to every Display subsection of the Screen section.";
+      };
+
+      defaultDepth = mkOption {
+        type = types.int;
+        default = 0;
+        example = 8;
+        description = "Default colour depth.";
+      };
+
+      fontPath = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "unix/:7100";
+        description = ''
+          Set the X server FontPath. Defaults to null, which
+          means the compiled in defaults will be used. See
+          man xorg.conf for details.
+        '';
+      };
+
+      tty = mkOption {
+        type = types.nullOr types.int;
+        default = 7;
+        description = "Virtual console for the X server.";
+      };
+
+      display = mkOption {
+        type = types.nullOr types.int;
+        default = 0;
+        description = "Display number for the X server.";
+      };
+
+      virtualScreen = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        example = { x = 2048; y = 2048; };
+        description = ''
+          Virtual screen size for Xrandr.
+        '';
+      };
+
+      logFile = mkOption {
+        type = types.nullOr types.str;
+        default = "/dev/null";
+        example = "/var/log/Xorg.0.log";
+        description = ''
+          Controls the file Xorg logs to.
+
+          The default of <literal>/dev/null</literal> is set so that systemd services (like <literal>displayManagers</literal>) only log to the journal and don't create their own log files.
+
+          Setting this to <literal>null</literal> will not pass the <literal>-logfile</literal> argument to Xorg which allows it to log to its default logfile locations instead (see <literal>man Xorg</literal>). You probably only want this behaviour when running Xorg manually (e.g. via <literal>startx</literal>).
+        '';
+      };
+
+      verbose = mkOption {
+        type = types.nullOr types.int;
+        default = 3;
+        example = 7;
+        description = ''
+          Controls verbosity of X logging.
+        '';
+      };
+
+      useGlamor = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to use the Glamor module for 2D acceleration,
+          if possible.
+        '';
+      };
+
+      enableCtrlAltBackspace = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the DontZap option, which binds Ctrl+Alt+Backspace
+          to forcefully kill X. This can lead to data loss and is disabled
+          by default.
+        '';
+      };
+
+      terminateOnReset = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to terminate X upon server reset.
+        '';
+      };
+    };
+
+  };
+
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.xserver.displayManager.lightdm.enable =
+      let dmConf = cfg.displayManager;
+          default = !(dmConf.gdm.enable
+                    || dmConf.sddm.enable
+                    || dmConf.xpra.enable
+                    || dmConf.sx.enable
+                    || dmConf.startx.enable);
+      in mkIf (default) (mkDefault true);
+
+    # so that the service won't be enabled when only startx is used
+    systemd.services.display-manager.enable  =
+      let dmConf = cfg.displayManager;
+          noDmUsed = !(dmConf.gdm.enable
+                    || dmConf.sddm.enable
+                    || dmConf.xpra.enable
+                    || dmConf.lightdm.enable);
+      in mkIf (noDmUsed) (mkDefault false);
+
+    hardware.opengl.enable = mkDefault true;
+
+    services.xserver.videoDrivers = mkIf (cfg.videoDriver != null) [ cfg.videoDriver ];
+
+    # FIXME: somehow check for unknown driver names.
+    services.xserver.drivers = flip concatMap cfg.videoDrivers (name:
+      let driver =
+        attrByPath [name]
+          (if xorg ? ${"xf86video" + name}
+           then { modules = [xorg.${"xf86video" + name}]; }
+           else null)
+          knownVideoDrivers;
+      in optional (driver != null) ({ inherit name; modules = []; driverName = name; display = true; } // driver));
+
+    assertions = [
+      (let primaryHeads = filter (x: x.primary) cfg.xrandrHeads; in {
+        assertion = length primaryHeads < 2;
+        message = "Only one head is allowed to be primary in "
+                + "‘services.xserver.xrandrHeads’, but there are "
+                + "${toString (length primaryHeads)} heads set to primary: "
+                + concatMapStringsSep ", " (x: x.output) primaryHeads;
+      })
+    ];
+
+    environment.etc =
+      (optionalAttrs cfg.exportConfiguration
+        {
+          "X11/xorg.conf".source = "${configFile}";
+          # -xkbdir command line option does not seems to be passed to xkbcomp.
+          "X11/xkb".source = "${cfg.xkbDir}";
+        })
+      # localectl looks into 00-keyboard.conf
+      //{
+          "X11/xorg.conf.d/00-keyboard.conf".text = ''
+            Section "InputClass"
+              Identifier "Keyboard catchall"
+              MatchIsKeyboard "on"
+              Option "XkbModel" "${cfg.xkbModel}"
+              Option "XkbLayout" "${cfg.layout}"
+              Option "XkbOptions" "${cfg.xkbOptions}"
+              Option "XkbVariant" "${cfg.xkbVariant}"
+            EndSection
+          '';
+        }
+      # Needed since 1.18; see https://bugs.freedesktop.org/show_bug.cgi?id=89023#c5
+      // (let cfgPath = "/X11/xorg.conf.d/10-evdev.conf"; in
+        {
+          ${cfgPath}.source = xorg.xf86inputevdev.out + "/share" + cfgPath;
+        });
+
+    environment.systemPackages =
+      [ xorg.xorgserver.out
+        xorg.xrandr
+        xorg.xrdb
+        xorg.setxkbmap
+        xorg.iceauth # required for KDE applications (it's called by dcopserver)
+        xorg.xlsclients
+        xorg.xset
+        xorg.xsetroot
+        xorg.xinput
+        xorg.xprop
+        xorg.xauth
+        pkgs.xterm
+        pkgs.xdg-utils
+        xorg.xf86inputevdev.out # get evdev.4 man page
+        pkgs.nixos-icons # needed for gnome and pantheon about dialog, nixos-manual and maybe more
+      ]
+      ++ optional (elem "virtualbox" cfg.videoDrivers) xorg.xrefresh;
+
+    environment.pathsToLink = [ "/share/X11" ];
+
+    xdg = {
+      autostart.enable = true;
+      menus.enable = true;
+      mime.enable = true;
+      icons.enable = true;
+    };
+
+    # The default max inotify watches is 8192.
+    # Nowadays most apps require a good number of inotify watches,
+    # the value below is used by default on several other distros.
+    boot.kernel.sysctl."fs.inotify.max_user_instances" = mkDefault 524288;
+    boot.kernel.sysctl."fs.inotify.max_user_watches" = mkDefault 524288;
+
+    systemd.defaultUnit = mkIf cfg.autorun "graphical.target";
+
+    systemd.services.display-manager =
+      { description = "X11 Server";
+
+        after = [ "acpid.service" "systemd-logind.service" "systemd-user-sessions.service" ];
+
+        restartIfChanged = false;
+
+        environment =
+          optionalAttrs config.hardware.opengl.setLdLibraryPath
+            { LD_LIBRARY_PATH = lib.makeLibraryPath [ pkgs.addOpenGLRunpath.driverLink ]; }
+          // cfg.displayManager.job.environment;
+
+        preStart =
+          ''
+            ${cfg.displayManager.job.preStart}
+
+            rm -f /tmp/.X0-lock
+          '';
+
+        # TODO: move declaring the systemd service to its own mkIf
+        script = mkIf (config.systemd.services.display-manager.enable == true) "${cfg.displayManager.job.execCmd}";
+
+        # Stop restarting if the display manager stops (crashes) 2 times
+        # in one minute. Starting X typically takes 3-4s.
+        startLimitIntervalSec = 30;
+        startLimitBurst = 3;
+        serviceConfig = {
+          Restart = "always";
+          RestartSec = "200ms";
+          SyslogIdentifier = "display-manager";
+        };
+      };
+
+    services.xserver.displayManager.xserverArgs =
+      [ "-config ${configFile}"
+        "-xkbdir" "${cfg.xkbDir}"
+      ] ++ optional (cfg.display != null) ":${toString cfg.display}"
+        ++ optional (cfg.tty     != null) "vt${toString cfg.tty}"
+        ++ optional (cfg.dpi     != null) "-dpi ${toString cfg.dpi}"
+        ++ optional (cfg.logFile != null) "-logfile ${toString cfg.logFile}"
+        ++ optional (cfg.verbose != null) "-verbose ${toString cfg.verbose}"
+        ++ optional (!cfg.enableTCP) "-nolisten tcp"
+        ++ optional (cfg.autoRepeatDelay != null) "-ardelay ${toString cfg.autoRepeatDelay}"
+        ++ optional (cfg.autoRepeatInterval != null) "-arinterval ${toString cfg.autoRepeatInterval}"
+        ++ optional cfg.terminateOnReset "-terminate";
+
+    services.xserver.modules =
+      concatLists (catAttrs "modules" cfg.drivers) ++
+      [ xorg.xorgserver.out
+        xorg.xf86inputevdev.out
+      ];
+
+    system.extraDependencies = singleton (pkgs.runCommand "xkb-validated" {
+      inherit (cfg) xkbModel layout xkbVariant xkbOptions;
+      nativeBuildInputs = with pkgs.buildPackages; [ xkbvalidate ];
+      preferLocalBuild = true;
+    } ''
+      ${optionalString (config.environment.sessionVariables ? XKB_CONFIG_ROOT)
+        "export XKB_CONFIG_ROOT=${config.environment.sessionVariables.XKB_CONFIG_ROOT}"
+      }
+      xkbvalidate "$xkbModel" "$layout" "$xkbVariant" "$xkbOptions"
+      touch "$out"
+    '');
+
+    services.xserver.config =
+      ''
+        Section "ServerFlags"
+          Option "AllowMouseOpenFail" "on"
+          Option "DontZap" "${if cfg.enableCtrlAltBackspace then "off" else "on"}"
+        ${indent cfg.serverFlagsSection}
+        EndSection
+
+        Section "Module"
+        ${indent cfg.moduleSection}
+        EndSection
+
+        Section "Monitor"
+          Identifier "Monitor[0]"
+        ${indent cfg.monitorSection}
+        EndSection
+
+        # Additional "InputClass" sections
+        ${flip (concatMapStringsSep "\n") cfg.inputClassSections (inputClassSection: ''
+          Section "InputClass"
+          ${indent inputClassSection}
+          EndSection
+        '')}
+
+
+        Section "ServerLayout"
+          Identifier "Layout[all]"
+        ${indent cfg.serverLayoutSection}
+          # Reference the Screen sections for each driver.  This will
+          # cause the X server to try each in turn.
+          ${flip concatMapStrings (filter (d: d.display) cfg.drivers) (d: ''
+            Screen "Screen-${d.name}[0]"
+          '')}
+        EndSection
+
+        ${if cfg.useGlamor then ''
+          Section "Module"
+            Load "dri2"
+            Load "glamoregl"
+          EndSection
+        '' else ""}
+
+        # For each supported driver, add a "Device" and "Screen"
+        # section.
+        ${flip concatMapStrings cfg.drivers (driver: ''
+
+          Section "Device"
+            Identifier "Device-${driver.name}[0]"
+            Driver "${driver.driverName or driver.name}"
+            ${if cfg.useGlamor then ''Option "AccelMethod" "glamor"'' else ""}
+          ${indent cfg.deviceSection}
+          ${indent (driver.deviceSection or "")}
+          ${indent xrandrDeviceSection}
+          EndSection
+          ${optionalString driver.display ''
+
+            Section "Screen"
+              Identifier "Screen-${driver.name}[0]"
+              Device "Device-${driver.name}[0]"
+              ${optionalString (cfg.monitorSection != "") ''
+                Monitor "Monitor[0]"
+              ''}
+
+            ${indent cfg.screenSection}
+            ${indent (driver.screenSection or "")}
+
+              ${optionalString (cfg.defaultDepth != 0) ''
+                DefaultDepth ${toString cfg.defaultDepth}
+              ''}
+
+              ${optionalString
+                (
+                  driver.name != "virtualbox"
+                  &&
+                  (cfg.resolutions != [] ||
+                    cfg.extraDisplaySettings != "" ||
+                    cfg.virtualScreen != null
+                  )
+                )
+                (let
+                  f = depth:
+                    ''
+                      SubSection "Display"
+                        Depth ${toString depth}
+                        ${optionalString (cfg.resolutions != [])
+                          "Modes ${concatMapStrings (res: ''"${toString res.x}x${toString res.y}"'') cfg.resolutions}"}
+                      ${indent cfg.extraDisplaySettings}
+                        ${optionalString (cfg.virtualScreen != null)
+                          "Virtual ${toString cfg.virtualScreen.x} ${toString cfg.virtualScreen.y}"}
+                      EndSubSection
+                    '';
+                in concatMapStrings f [8 16 24]
+              )}
+
+            EndSection
+          ''}
+        '')}
+
+        ${xrandrMonitorSections}
+
+        ${cfg.extraConfig}
+      '';
+
+    fonts.enableDefaultFonts = mkDefault true;
+
+  };
+
+  # uses relatedPackages
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/system/activation/activation-script.nix b/nixos/modules/system/activation/activation-script.nix
new file mode 100644
index 00000000000..c04d0fc16b2
--- /dev/null
+++ b/nixos/modules/system/activation/activation-script.nix
@@ -0,0 +1,272 @@
+# generate the script used to activate the configuration.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  addAttributeName = mapAttrs (a: v: v // {
+    text = ''
+      #### Activation script snippet ${a}:
+      _localstatus=0
+      ${v.text}
+
+      if (( _localstatus > 0 )); then
+        printf "Activation script snippet '%s' failed (%s)\n" "${a}" "$_localstatus"
+      fi
+    '';
+  });
+
+  systemActivationScript = set: onlyDry: let
+    set' = mapAttrs (_: v: if isString v then (noDepEntry v) // { supportsDryActivation = false; } else v) set;
+    withHeadlines = addAttributeName set';
+    # When building a dry activation script, this replaces all activation scripts
+    # that do not support dry mode with a comment that does nothing. Filtering these
+    # activation scripts out so they don't get generated into the dry activation script
+    # does not work because when an activation script that supports dry mode depends on
+    # an activation script that does not, the dependency cannot be resolved and the eval
+    # fails.
+    withDrySnippets = mapAttrs (a: v: if onlyDry && !v.supportsDryActivation then v // {
+      text = "#### Activation script snippet ${a} does not support dry activation.";
+    } else v) withHeadlines;
+  in
+    ''
+      #!${pkgs.runtimeShell}
+
+      systemConfig='@out@'
+
+      export PATH=/empty
+      for i in ${toString path}; do
+          PATH=$PATH:$i/bin:$i/sbin
+      done
+
+      _status=0
+      trap "_status=1 _localstatus=\$?" ERR
+
+      # Ensure a consistent umask.
+      umask 0022
+
+      ${textClosureMap id (withDrySnippets) (attrNames withDrySnippets)}
+
+    '' + optionalString (!onlyDry) ''
+      # Make this configuration the current configuration.
+      # The readlink is there to ensure that when $systemConfig = /system
+      # (which is a symlink to the store), /run/current-system is still
+      # used as a garbage collection root.
+      ln -sfn "$(readlink -f "$systemConfig")" /run/current-system
+
+      # Prevent the current configuration from being garbage-collected.
+      mkdir -p /nix/var/nix/gcroots
+      ln -sfn /run/current-system /nix/var/nix/gcroots/current-system
+
+      exit $_status
+    '';
+
+  path = with pkgs; map getBin
+    [ coreutils
+      gnugrep
+      findutils
+      getent
+      stdenv.cc.libc # nscd in update-users-groups.pl
+      shadow
+      nettools # needed for hostname
+      util-linux # needed for mount and mountpoint
+    ];
+
+  scriptType = withDry: with types;
+    let scriptOptions =
+      { deps = mkOption
+          { type = types.listOf types.str;
+            default = [ ];
+            description = "List of dependencies. The script will run after these.";
+          };
+        text = mkOption
+          { type = types.lines;
+            description = "The content of the script.";
+          };
+      } // optionalAttrs withDry {
+        supportsDryActivation = mkOption
+          { type = types.bool;
+            default = false;
+            description = ''
+              Whether this activation script supports being dry-activated.
+              These activation scripts will also be executed on dry-activate
+              activations with the environment variable
+              <literal>NIXOS_ACTION</literal> being set to <literal>dry-activate
+              </literal>.  it's important that these activation scripts  don't
+              modify anything about the system when the variable is set.
+            '';
+          };
+      };
+    in either str (submodule { options = scriptOptions; });
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    system.activationScripts = mkOption {
+      default = {};
+
+      example = literalExpression ''
+        { stdio.text =
+          '''
+            # Needed by some programs.
+            ln -sfn /proc/self/fd /dev/fd
+            ln -sfn /proc/self/fd/0 /dev/stdin
+            ln -sfn /proc/self/fd/1 /dev/stdout
+            ln -sfn /proc/self/fd/2 /dev/stderr
+          ''';
+        }
+      '';
+
+      description = ''
+        A set of shell script fragments that are executed when a NixOS
+        system configuration is activated.  Examples are updating
+        /etc, creating accounts, and so on.  Since these are executed
+        every time you boot the system or run
+        <command>nixos-rebuild</command>, it's important that they are
+        idempotent and fast.
+      '';
+
+      type = types.attrsOf (scriptType true);
+      apply = set: set // {
+        script = systemActivationScript set false;
+      };
+    };
+
+    system.dryActivationScript = mkOption {
+      description = "The shell script that is to be run when dry-activating a system.";
+      readOnly = true;
+      internal = true;
+      default = systemActivationScript (removeAttrs config.system.activationScripts [ "script" ]) true;
+      defaultText = literalDocBook "generated activation script";
+    };
+
+    system.userActivationScripts = mkOption {
+      default = {};
+
+      example = literalExpression ''
+        { plasmaSetup = {
+            text = '''
+              ''${pkgs.libsForQt5.kservice}/bin/kbuildsycoca5"
+            ''';
+            deps = [];
+          };
+        }
+      '';
+
+      description = ''
+        A set of shell script fragments that are executed by a systemd user
+        service when a NixOS system configuration is activated. Examples are
+        rebuilding the .desktop file cache for showing applications in the menu.
+        Since these are executed every time you run
+        <command>nixos-rebuild</command>, it's important that they are
+        idempotent and fast.
+      '';
+
+      type = with types; attrsOf (scriptType false);
+
+      apply = set: {
+        script = ''
+          unset PATH
+          for i in ${toString path}; do
+            PATH=$PATH:$i/bin:$i/sbin
+          done
+
+          _status=0
+          trap "_status=1 _localstatus=\$?" ERR
+
+          ${
+            let
+              set' = mapAttrs (n: v: if isString v then noDepEntry v else v) set;
+              withHeadlines = addAttributeName set';
+            in textClosureMap id (withHeadlines) (attrNames withHeadlines)
+          }
+
+          exit $_status
+        '';
+      };
+
+    };
+
+    environment.usrbinenv = mkOption {
+      default = "${pkgs.coreutils}/bin/env";
+      defaultText = literalExpression ''"''${pkgs.coreutils}/bin/env"'';
+      example = literalExpression ''"''${pkgs.busybox}/bin/env"'';
+      type = types.nullOr types.path;
+      visible = false;
+      description = ''
+        The env(1) executable that is linked system-wide to
+        <literal>/usr/bin/env</literal>.
+      '';
+    };
+  };
+
+
+  ###### implementation
+
+  config = {
+
+    system.activationScripts.stdio = ""; # obsolete
+
+    system.activationScripts.var =
+      ''
+        # Various log/runtime directories.
+
+        mkdir -m 1777 -p /var/tmp
+
+        # Empty, immutable home directory of many system accounts.
+        mkdir -p /var/empty
+        # Make sure it's really empty
+        ${pkgs.e2fsprogs}/bin/chattr -f -i /var/empty || true
+        find /var/empty -mindepth 1 -delete
+        chmod 0555 /var/empty
+        chown root:root /var/empty
+        ${pkgs.e2fsprogs}/bin/chattr -f +i /var/empty || true
+      '';
+
+    system.activationScripts.usrbinenv = if config.environment.usrbinenv != null
+      then ''
+        mkdir -m 0755 -p /usr/bin
+        ln -sfn ${config.environment.usrbinenv} /usr/bin/.env.tmp
+        mv /usr/bin/.env.tmp /usr/bin/env # atomically replace /usr/bin/env
+      ''
+      else ''
+        rm -f /usr/bin/env
+        rmdir --ignore-fail-on-non-empty /usr/bin /usr
+      '';
+
+    system.activationScripts.specialfs =
+      ''
+        specialMount() {
+          local device="$1"
+          local mountPoint="$2"
+          local options="$3"
+          local fsType="$4"
+
+          if mountpoint -q "$mountPoint"; then
+            local options="remount,$options"
+          else
+            mkdir -m 0755 -p "$mountPoint"
+          fi
+          mount -t "$fsType" -o "$options" "$device" "$mountPoint"
+        }
+        source ${config.system.build.earlyMountScript}
+      '';
+
+    systemd.user = {
+      services.nixos-activation = {
+        description = "Run user-specific NixOS activation";
+        script = config.system.userActivationScripts.script;
+        unitConfig.ConditionUser = "!@system";
+        serviceConfig.Type = "oneshot";
+        wantedBy = [ "default.target" ];
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/system/activation/no-clone.nix b/nixos/modules/system/activation/no-clone.nix
new file mode 100644
index 00000000000..912420347dc
--- /dev/null
+++ b/nixos/modules/system/activation/no-clone.nix
@@ -0,0 +1,8 @@
+{ lib, ... }:
+
+with lib;
+
+{
+  boot.loader.grub.device = mkOverride 0 "nodev";
+  specialisation = mkOverride 0 {};
+}
diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl
new file mode 100755
index 00000000000..459d09faa53
--- /dev/null
+++ b/nixos/modules/system/activation/switch-to-configuration.pl
@@ -0,0 +1,856 @@
+#! @perl@/bin/perl
+
+use strict;
+use warnings;
+use Config::IniFiles;
+use File::Path qw(make_path);
+use File::Basename;
+use File::Slurp qw(read_file write_file edit_file);
+use Net::DBus;
+use Sys::Syslog qw(:standard :macros);
+use Cwd 'abs_path';
+
+## no critic(CodeLayout::ProhibitParensWithBuiltins)
+
+my $out = "@out@";
+
+my $curSystemd = abs_path("/run/current-system/sw/bin");
+
+# To be robust against interruption, record what units need to be started etc.
+my $startListFile = "/run/nixos/start-list";
+my $restartListFile = "/run/nixos/restart-list";
+my $reloadListFile = "/run/nixos/reload-list";
+
+# Parse restart/reload requests by the activation script.
+# Activation scripts may write newline-separated units to the restart
+# file and switch-to-configuration will handle them. While
+# `stopIfChanged = true` is ignored, switch-to-configuration will
+# handle `restartIfChanged = false` and `reloadIfChanged = true`.
+# This is the same as specifying a restart trigger in the NixOS module.
+#
+# The reload file asks the script to reload a unit. This is the same as
+# specifying a reload trigger in the NixOS module and can be ignored if
+# the unit is restarted in this activation.
+my $restartByActivationFile = "/run/nixos/activation-restart-list";
+my $reloadByActivationFile = "/run/nixos/activation-reload-list";
+my $dryRestartByActivationFile = "/run/nixos/dry-activation-restart-list";
+my $dryReloadByActivationFile = "/run/nixos/dry-activation-reload-list";
+
+make_path("/run/nixos", { mode => oct(755) });
+
+my $action = shift(@ARGV);
+
+if ("@localeArchive@" ne "") {
+    $ENV{LOCALE_ARCHIVE} = "@localeArchive@";
+}
+
+if (!defined($action) || ($action ne "switch" && $action ne "boot" && $action ne "test" && $action ne "dry-activate")) {
+    print STDERR <<EOF;
+Usage: $0 [switch|boot|test]
+
+switch:       make the configuration the boot default and activate now
+boot:         make the configuration the boot default
+test:         activate the configuration, but don\'t make it the boot default
+dry-activate: show what would be done if this configuration were activated
+EOF
+    exit(1);
+}
+
+$ENV{NIXOS_ACTION} = $action;
+
+# This is a NixOS installation if it has /etc/NIXOS or a proper
+# /etc/os-release.
+die("This is not a NixOS installation!\n") unless
+    -f "/etc/NIXOS" || (read_file("/etc/os-release", err_mode => 'quiet') // "") =~ /ID="?nixos"?/s;
+
+openlog("nixos", "", LOG_USER);
+
+# Install or update the bootloader.
+if ($action eq "switch" || $action eq "boot") {
+    chomp(my $installBootLoader = <<'EOFBOOTLOADER');
+@installBootLoader@
+EOFBOOTLOADER
+    system("$installBootLoader $out") == 0 or exit 1;
+}
+
+# Just in case the new configuration hangs the system, do a sync now.
+system("@coreutils@/bin/sync", "-f", "/nix/store") unless ($ENV{"NIXOS_NO_SYNC"} // "") eq "1";
+
+exit(0) if $action eq "boot";
+
+# Check if we can activate the new configuration.
+my $oldVersion = read_file("/run/current-system/init-interface-version", err_mode => 'quiet') // "";
+my $newVersion = read_file("$out/init-interface-version");
+
+if ($newVersion ne $oldVersion) {
+    print STDERR <<EOF;
+Warning: the new NixOS configuration has an ‘init’ that is
+incompatible with the current configuration.  The new configuration
+won\'t take effect until you reboot the system.
+EOF
+    exit(100);
+}
+
+# Ignore SIGHUP so that we're not killed if we're running on (say)
+# virtual console 1 and we restart the "tty1" unit.
+$SIG{PIPE} = "IGNORE";
+
+sub getActiveUnits {
+    my $mgr = Net::DBus->system->get_service("org.freedesktop.systemd1")->get_object("/org/freedesktop/systemd1");
+    my $units = $mgr->ListUnitsByPatterns([], []);
+    my $res = {};
+    for my $item (@$units) {
+        my ($id, $description, $load_state, $active_state, $sub_state,
+            $following, $unit_path, $job_id, $job_type, $job_path) = @$item;
+        next unless $following eq '';
+        next if $job_id == 0 and $active_state eq 'inactive';
+        $res->{$id} = { load => $load_state, state => $active_state, substate => $sub_state };
+    }
+    return $res;
+}
+
+# Returns whether a systemd unit is active
+sub unit_is_active {
+    my ($unit_name) = @_;
+
+    my $mgr = Net::DBus->system->get_service('org.freedesktop.systemd1')->get_object('/org/freedesktop/systemd1');
+    my $units = $mgr->ListUnitsByNames([$unit_name]);
+    if (scalar(@{$units}) == 0) {
+        return 0;
+    }
+    my $active_state = $units->[0]->[3]; ## no critic (ValuesAndExpressions::ProhibitMagicNumbers)
+    return $active_state eq 'active' || $active_state eq 'activating';
+}
+
+sub parseFstab {
+    my ($filename) = @_;
+    my ($fss, $swaps);
+    foreach my $line (read_file($filename, err_mode => 'quiet')) {
+        chomp($line);
+        $line =~ s/^\s*#.*//;
+        next if $line =~ /^\s*$/;
+        my @xs = split(/ /, $line);
+        if ($xs[2] eq "swap") {
+            $swaps->{$xs[0]} = { options => $xs[3] // "" };
+        } else {
+            $fss->{$xs[1]} = { device => $xs[0], fsType => $xs[2], options => $xs[3] // "" };
+        }
+    }
+    return ($fss, $swaps);
+}
+
+# This subroutine takes a single ini file that specified systemd configuration
+# like unit configuration and parses it into a hash where the keys are the sections
+# of the unit file and the values are hashes themselves. These hashes have the unit file
+# keys as their keys (left side of =) and an array of all values that were set as their
+# values. If a value is empty (for example `ExecStart=`), then all current definitions are
+# removed.
+#
+# Instead of returning the hash, this subroutine takes a hashref to return the data in. This
+# allows calling the subroutine multiple times with the same hash to parse override files.
+sub parseSystemdIni {
+    my ($unitContents, $path) = @_;
+    # Tie the ini file to a hash for easier access
+    tie(my %fileContents, 'Config::IniFiles', (-file => $path, -allowempty => 1, -allowcontinue => 1)); ## no critic(Miscellanea::ProhibitTies)
+
+    # Copy over all sections
+    foreach my $sectionName (keys(%fileContents)) {
+        if ($sectionName eq "Install") {
+            # Skip the [Install] section because it has no relevant keys for us
+            next;
+        }
+        # Copy over all keys
+        foreach my $iniKey (keys(%{$fileContents{$sectionName}})) {
+            # Ensure the value is an array so it's easier to work with
+            my $iniValue = $fileContents{$sectionName}{$iniKey};
+            my @iniValues;
+            if (ref($iniValue) eq "ARRAY") {
+                @iniValues = @{$iniValue};
+            } else {
+                @iniValues = $iniValue;
+            }
+            # Go over all values
+            for my $iniValue (@iniValues) {
+                # If a value is empty, it's an override that tells us to clean the value
+                if ($iniValue eq "") {
+                    delete $unitContents->{$sectionName}->{$iniKey};
+                    next;
+                }
+                push(@{$unitContents->{$sectionName}->{$iniKey}}, $iniValue);
+            }
+        }
+    }
+    return;
+}
+
+# This subroutine takes the path to a systemd configuration file (like a unit configuration),
+# parses it, and returns a hash that contains the contents. The contents of this hash are
+# explained in the `parseSystemdIni` subroutine. Neither the sections nor the keys inside
+# the sections are consistently sorted.
+#
+# If a directory with the same basename ending in .d exists next to the unit file, it will be
+# assumed to contain override files which will be parsed as well and handled properly.
+sub parse_unit {
+    my ($unit_path) = @_;
+
+    # Parse the main unit and all overrides
+    my %unit_data;
+    # Replace \ with \\ so glob() still works with units that have a \ in them
+    # Valid characters in unit names are ASCII letters, digits, ":", "-", "_", ".", and "\"
+    $unit_path =~ s/\\/\\\\/gmsx;
+    foreach (glob("${unit_path}{,.d/*.conf}")) {
+        parseSystemdIni(\%unit_data, "$_")
+    }
+    return %unit_data;
+}
+
+# Checks whether a specified boolean in a systemd unit is true
+# or false, with a default that is applied when the value is not set.
+sub parseSystemdBool {
+    my ($unitConfig, $sectionName, $boolName, $default) = @_;
+
+    my @values = @{$unitConfig->{$sectionName}{$boolName} // []};
+    # Return default if value is not set
+    if (scalar(@values) lt 1 || not defined($values[-1])) {
+        return $default;
+    }
+    # If value is defined multiple times, use the last definition
+    my $last = $values[-1];
+    # These are valid values as of systemd.syntax(7)
+    return $last eq "1" || $last eq "yes" || $last eq "true" || $last eq "on";
+}
+
+sub recordUnit {
+    my ($fn, $unit) = @_;
+    write_file($fn, { append => 1 }, "$unit\n") if $action ne "dry-activate";
+}
+
+# The opposite of recordUnit, removes a unit name from a file
+sub unrecord_unit {
+    my ($fn, $unit) = @_;
+    edit_file(sub { s/^$unit\n//msx }, $fn) if $action ne "dry-activate";
+}
+
+# Compare the contents of two unit files and return whether the unit
+# needs to be restarted or reloaded. If the units differ, the service
+# is restarted unless the only difference is `X-Reload-Triggers` in the
+# `Unit` section. If this is the only modification, the unit is reloaded
+# instead of restarted.
+# Returns:
+# - 0 if the units are equal
+# - 1 if the units are different and a restart action is required
+# - 2 if the units are different and a reload action is required
+sub compare_units {
+    my ($old_unit, $new_unit) = @_;
+    my $ret = 0;
+    # Keys to ignore in the [Unit] section
+    my %unit_section_ignores = map { $_ => 1 } qw(
+        X-Reload-Triggers
+        Description Documentation
+        OnFailure OnSuccess OnFailureJobMode
+        IgnoreOnIsolate StopWhenUnneeded
+        RefuseManualStart RefuseManualStop
+        AllowIsolate CollectMode
+        SourcePath
+    );
+
+    my $comp_array = sub {
+      my ($a, $b) = @_;
+      return join("\0", @{$a}) eq join("\0", @{$b});
+    };
+
+    # Comparison hash for the sections
+    my %section_cmp = map { $_ => 1 } keys(%{$new_unit});
+    # Iterate over the sections
+    foreach my $section_name (keys(%{$old_unit})) {
+        # Missing section in the new unit?
+        if (not exists($section_cmp{$section_name})) {
+            # If the [Unit] section was removed, make sure that only keys
+            # were in it that are ignored
+            if ($section_name eq 'Unit') {
+                foreach my $ini_key (keys(%{$old_unit->{'Unit'}})) {
+                    if (not defined($unit_section_ignores{$ini_key})) {
+                        return 1;
+                    }
+                }
+                next; # check the next section
+            } else {
+                return 1;
+            }
+            if ($section_name eq 'Unit' and %{$old_unit->{'Unit'}} == 1 and defined(%{$old_unit->{'Unit'}}{'X-Reload-Triggers'})) {
+                # If a new [Unit] section was removed that only contained X-Reload-Triggers,
+                # do nothing.
+                next;
+            } else {
+                return 1;
+            }
+        }
+        delete $section_cmp{$section_name};
+        # Comparison hash for the section contents
+        my %ini_cmp = map { $_ => 1 } keys(%{$new_unit->{$section_name}});
+        # Iterate over the keys of the section
+        foreach my $ini_key (keys(%{$old_unit->{$section_name}})) {
+            delete $ini_cmp{$ini_key};
+            my @old_value = @{$old_unit->{$section_name}{$ini_key}};
+            # If the key is missing in the new unit, they are different...
+            if (not $new_unit->{$section_name}{$ini_key}) {
+                # ... unless the key that is now missing is one of the ignored keys
+                if ($section_name eq 'Unit' and defined($unit_section_ignores{$ini_key})) {
+                    next;
+                }
+                return 1;
+            }
+            my @new_value = @{$new_unit->{$section_name}{$ini_key}};
+            # If the contents are different, the units are different
+            if (not $comp_array->(\@old_value, \@new_value)) {
+                # Check if only the reload triggers changed or one of the ignored keys
+                if ($section_name eq 'Unit') {
+                    if ($ini_key eq 'X-Reload-Triggers') {
+                        $ret = 2;
+                        next;
+                    } elsif (defined($unit_section_ignores{$ini_key})) {
+                        next;
+                    }
+                }
+                return 1;
+            }
+        }
+        # A key was introduced that was missing in the old unit
+        if (%ini_cmp) {
+            if ($section_name eq 'Unit') {
+                foreach my $ini_key (keys(%ini_cmp)) {
+                    if ($ini_key eq 'X-Reload-Triggers') {
+                        $ret = 2;
+                    } elsif (defined($unit_section_ignores{$ini_key})) {
+                        next;
+                    } else {
+                        return 1;
+                    }
+                }
+            } else {
+                return 1;
+            }
+        };
+    }
+    # A section was introduced that was missing in the old unit
+    if (%section_cmp) {
+        if (%section_cmp == 1 and defined($section_cmp{'Unit'})) {
+            foreach my $ini_key (keys(%{$new_unit->{'Unit'}})) {
+                if (not defined($unit_section_ignores{$ini_key})) {
+                    return 1;
+                } elsif ($ini_key eq 'X-Reload-Triggers') {
+                    $ret = 2;
+                }
+            }
+        } else {
+            return 1;
+        }
+    }
+
+    return $ret;
+}
+
+sub handleModifiedUnit {
+    my ($unit, $baseName, $newUnitFile, $newUnitInfo, $activePrev, $unitsToStop, $unitsToStart, $unitsToReload, $unitsToRestart, $unitsToSkip) = @_;
+
+    if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target" || $unit =~ /\.path$/ || $unit =~ /\.slice$/) {
+        # Do nothing.  These cannot be restarted directly.
+
+        # Slices and Paths don't have to be restarted since
+        # properties (resource limits and inotify watches)
+        # seem to get applied on daemon-reload.
+    } elsif ($unit =~ /\.mount$/) {
+        # Reload the changed mount unit to force a remount.
+        # FIXME: only reload when Options= changed, restart otherwise
+        $unitsToReload->{$unit} = 1;
+        recordUnit($reloadListFile, $unit);
+    } elsif ($unit =~ /\.socket$/) {
+        # FIXME: do something?
+        # Attempt to fix this: https://github.com/NixOS/nixpkgs/pull/141192
+        # Revert of the attempt: https://github.com/NixOS/nixpkgs/pull/147609
+        # More details: https://github.com/NixOS/nixpkgs/issues/74899#issuecomment-981142430
+    } else {
+        my %unitInfo = $newUnitInfo ? %{$newUnitInfo} : parse_unit($newUnitFile);
+        if (parseSystemdBool(\%unitInfo, "Service", "X-ReloadIfChanged", 0) and not $unitsToRestart->{$unit} and not $unitsToStop->{$unit}) {
+            $unitsToReload->{$unit} = 1;
+            recordUnit($reloadListFile, $unit);
+        }
+        elsif (!parseSystemdBool(\%unitInfo, "Service", "X-RestartIfChanged", 1) || parseSystemdBool(\%unitInfo, "Unit", "RefuseManualStop", 0) || parseSystemdBool(\%unitInfo, "Unit", "X-OnlyManualStart", 0)) {
+            $unitsToSkip->{$unit} = 1;
+        } else {
+            # It doesn't make sense to stop and start non-services because
+            # they can't have ExecStop=
+            if (!parseSystemdBool(\%unitInfo, "Service", "X-StopIfChanged", 1) || $unit !~ /\.service$/) {
+                # This unit should be restarted instead of
+                # stopped and started.
+                $unitsToRestart->{$unit} = 1;
+                recordUnit($restartListFile, $unit);
+                # Remove from units to reload so we don't restart and reload
+                if ($unitsToReload->{$unit}) {
+                    delete $unitsToReload->{$unit};
+                    unrecord_unit($reloadListFile, $unit);
+                }
+            } else {
+                # If this unit is socket-activated, then stop the
+                # socket unit(s) as well, and restart the
+                # socket(s) instead of the service.
+                my $socket_activated = 0;
+                if ($unit =~ /\.service$/) {
+                    my @sockets = split(/ /, join(" ", @{$unitInfo{Service}{Sockets} // []}));
+                    if (scalar(@sockets) == 0) {
+                        @sockets = ("$baseName.socket");
+                    }
+                    foreach my $socket (@sockets) {
+                        if (defined($activePrev->{$socket})) {
+                            # We can now be sure this is a socket-activate unit
+
+                            $unitsToStop->{$socket} = 1;
+                            # Only restart sockets that actually
+                            # exist in new configuration:
+                            if (-e "$out/etc/systemd/system/$socket") {
+                                $unitsToStart->{$socket} = 1;
+                                if ($unitsToStart eq $unitsToRestart) {
+                                    recordUnit($restartListFile, $socket);
+                                } else {
+                                    recordUnit($startListFile, $socket);
+                                }
+                                $socket_activated = 1;
+                            }
+                            # Remove from units to reload so we don't restart and reload
+                            if ($unitsToReload->{$unit}) {
+                                delete $unitsToReload->{$unit};
+                                unrecord_unit($reloadListFile, $unit);
+                            }
+                        }
+                    }
+                }
+
+                # If the unit is not socket-activated, record
+                # that this unit needs to be started below.
+                # We write this to a file to ensure that the
+                # service gets restarted if we're interrupted.
+                if (!$socket_activated) {
+                    $unitsToStart->{$unit} = 1;
+                    if ($unitsToStart eq $unitsToRestart) {
+                        recordUnit($restartListFile, $unit);
+                    } else {
+                        recordUnit($startListFile, $unit);
+                    }
+                }
+
+                $unitsToStop->{$unit} = 1;
+                # Remove from units to reload so we don't restart and reload
+                if ($unitsToReload->{$unit}) {
+                    delete $unitsToReload->{$unit};
+                    unrecord_unit($reloadListFile, $unit);
+                }
+            }
+        }
+    }
+}
+
+# Figure out what units need to be stopped, started, restarted or reloaded.
+my (%unitsToStop, %unitsToSkip, %unitsToStart, %unitsToRestart, %unitsToReload);
+
+my %unitsToFilter; # units not shown
+
+$unitsToStart{$_} = 1 foreach
+    split('\n', read_file($startListFile, err_mode => 'quiet') // "");
+
+$unitsToRestart{$_} = 1 foreach
+    split('\n', read_file($restartListFile, err_mode => 'quiet') // "");
+
+$unitsToReload{$_} = 1 foreach
+    split('\n', read_file($reloadListFile, err_mode => 'quiet') // "");
+
+my $activePrev = getActiveUnits();
+while (my ($unit, $state) = each(%{$activePrev})) {
+    my $baseUnit = $unit;
+
+    my $prevUnitFile = "/etc/systemd/system/$baseUnit";
+    my $newUnitFile = "$out/etc/systemd/system/$baseUnit";
+
+    # Detect template instances.
+    if (!-e $prevUnitFile && !-e $newUnitFile && $unit =~ /^(.*)@[^\.]*\.(.*)$/) {
+      $baseUnit = "$1\@.$2";
+      $prevUnitFile = "/etc/systemd/system/$baseUnit";
+      $newUnitFile = "$out/etc/systemd/system/$baseUnit";
+    }
+
+    my $baseName = $baseUnit;
+    $baseName =~ s/\.[a-z]*$//;
+
+    if (-e $prevUnitFile && ($state->{state} eq "active" || $state->{state} eq "activating")) {
+        if (! -e $newUnitFile || abs_path($newUnitFile) eq "/dev/null") {
+            my %unitInfo = parse_unit($prevUnitFile);
+            $unitsToStop{$unit} = 1 if parseSystemdBool(\%unitInfo, "Unit", "X-StopOnRemoval", 1);
+        }
+
+        elsif ($unit =~ /\.target$/) {
+            my %unitInfo = parse_unit($newUnitFile);
+
+            # Cause all active target units to be restarted below.
+            # This should start most changed units we stop here as
+            # well as any new dependencies (including new mounts and
+            # swap devices).  FIXME: the suspend target is sometimes
+            # active after the system has resumed, which probably
+            # should not be the case.  Just ignore it.
+            if ($unit ne "suspend.target" && $unit ne "hibernate.target" && $unit ne "hybrid-sleep.target") {
+                unless (parseSystemdBool(\%unitInfo, "Unit", "RefuseManualStart", 0) || parseSystemdBool(\%unitInfo, "Unit", "X-OnlyManualStart", 0)) {
+                    $unitsToStart{$unit} = 1;
+                    recordUnit($startListFile, $unit);
+                    # Don't spam the user with target units that always get started.
+                    $unitsToFilter{$unit} = 1;
+                }
+            }
+
+            # Stop targets that have X-StopOnReconfiguration set.
+            # This is necessary to respect dependency orderings
+            # involving targets: if unit X starts after target Y and
+            # target Y starts after unit Z, then if X and Z have both
+            # changed, then X should be restarted after Z.  However,
+            # if target Y is in the "active" state, X and Z will be
+            # restarted at the same time because X's dependency on Y
+            # is already satisfied.  Thus, we need to stop Y first.
+            # Stopping a target generally has no effect on other units
+            # (unless there is a PartOf dependency), so this is just a
+            # bookkeeping thing to get systemd to do the right thing.
+            if (parseSystemdBool(\%unitInfo, "Unit", "X-StopOnReconfiguration", 0)) {
+                $unitsToStop{$unit} = 1;
+            }
+        }
+
+        else {
+            my %old_unit_info = parse_unit($prevUnitFile);
+            my %new_unit_info = parse_unit($newUnitFile);
+            my $diff = compare_units(\%old_unit_info, \%new_unit_info);
+            if ($diff == 1) {
+                handleModifiedUnit($unit, $baseName, $newUnitFile, \%new_unit_info, $activePrev, \%unitsToStop, \%unitsToStart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
+            } elsif ($diff == 2 and not $unitsToRestart{$unit}) {
+                $unitsToReload{$unit} = 1;
+                recordUnit($reloadListFile, $unit);
+            }
+        }
+    }
+}
+
+sub pathToUnitName {
+    my ($path) = @_;
+    # Use current version of systemctl binary before daemon is reexeced.
+    open(my $cmd, "-|", "$curSystemd/systemd-escape", "--suffix=mount", "-p", $path)
+        or die "Unable to escape $path!\n";
+    my $escaped = join("", <$cmd>);
+    chomp($escaped);
+    close($cmd) or die('Unable to close systemd-escape pipe');
+    return $escaped;
+}
+
+# Compare the previous and new fstab to figure out which filesystems
+# need a remount or need to be unmounted.  New filesystems are mounted
+# automatically by starting local-fs.target.  FIXME: might be nicer if
+# we generated units for all mounts; then we could unify this with the
+# unit checking code above.
+my ($prevFss, $prevSwaps) = parseFstab("/etc/fstab");
+my ($newFss, $newSwaps) = parseFstab("$out/etc/fstab");
+foreach my $mountPoint (keys(%$prevFss)) {
+    my $prev = $prevFss->{$mountPoint};
+    my $new = $newFss->{$mountPoint};
+    my $unit = pathToUnitName($mountPoint);
+    if (!defined($new)) {
+        # Filesystem entry disappeared, so unmount it.
+        $unitsToStop{$unit} = 1;
+    } elsif ($prev->{fsType} ne $new->{fsType} || $prev->{device} ne $new->{device}) {
+        # Filesystem type or device changed, so unmount and mount it.
+        $unitsToStop{$unit} = 1;
+        $unitsToStart{$unit} = 1;
+        recordUnit($startListFile, $unit);
+    } elsif ($prev->{options} ne $new->{options}) {
+        # Mount options changes, so remount it.
+        $unitsToReload{$unit} = 1;
+        recordUnit($reloadListFile, $unit);
+    }
+}
+
+# Also handles swap devices.
+foreach my $device (keys(%$prevSwaps)) {
+    my $prev = $prevSwaps->{$device};
+    my $new = $newSwaps->{$device};
+    if (!defined($new)) {
+        # Swap entry disappeared, so turn it off.  Can't use
+        # "systemctl stop" here because systemd has lots of alias
+        # units that prevent a stop from actually calling
+        # "swapoff".
+        if ($action ne "dry-activate") {
+            print STDERR "would stop swap device: $device\n";
+        } else {
+            print STDERR "stopping swap device: $device\n";
+            system("@utillinux@/sbin/swapoff", $device);
+        }
+    }
+    # FIXME: update swap options (i.e. its priority).
+}
+
+
+# Should we have systemd re-exec itself?
+my $prevSystemd = abs_path("/proc/1/exe") // "/unknown";
+my $prevSystemdSystemConfig = abs_path("/etc/systemd/system.conf") // "/unknown";
+my $newSystemd = abs_path("@systemd@/lib/systemd/systemd") or die;
+my $newSystemdSystemConfig = abs_path("$out/etc/systemd/system.conf") // "/unknown";
+
+my $restartSystemd = $prevSystemd ne $newSystemd;
+if ($prevSystemdSystemConfig ne $newSystemdSystemConfig) {
+    $restartSystemd = 1;
+}
+
+
+sub filterUnits {
+    my ($units) = @_;
+    my @res;
+    foreach my $unit (sort(keys(%{$units}))) {
+        push(@res, $unit) if !defined($unitsToFilter{$unit});
+    }
+    return @res;
+}
+
+my @unitsToStopFiltered = filterUnits(\%unitsToStop);
+
+
+# Show dry-run actions.
+if ($action eq "dry-activate") {
+    print STDERR "would stop the following units: ", join(", ", @unitsToStopFiltered), "\n"
+        if scalar(@unitsToStopFiltered) > 0;
+    print STDERR "would NOT stop the following changed units: ", join(", ", sort(keys(%unitsToSkip))), "\n"
+        if scalar(keys(%unitsToSkip)) > 0;
+
+    print STDERR "would activate the configuration...\n";
+    system("$out/dry-activate", "$out");
+
+    # Handle the activation script requesting the restart or reload of a unit.
+    foreach (split('\n', read_file($dryRestartByActivationFile, err_mode => 'quiet') // "")) {
+        my $unit = $_;
+        my $baseUnit = $unit;
+        my $newUnitFile = "$out/etc/systemd/system/$baseUnit";
+
+        # Detect template instances.
+        if (!-e $newUnitFile && $unit =~ /^(.*)@[^\.]*\.(.*)$/) {
+          $baseUnit = "$1\@.$2";
+          $newUnitFile = "$out/etc/systemd/system/$baseUnit";
+        }
+
+        my $baseName = $baseUnit;
+        $baseName =~ s/\.[a-z]*$//;
+
+        # Start units if they were not active previously
+        if (not defined($activePrev->{$unit})) {
+            $unitsToStart{$unit} = 1;
+            next;
+        }
+
+        handleModifiedUnit($unit, $baseName, $newUnitFile, undef, $activePrev, \%unitsToRestart, \%unitsToRestart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
+    }
+    unlink($dryRestartByActivationFile);
+
+    foreach (split('\n', read_file($dryReloadByActivationFile, err_mode => 'quiet') // "")) {
+        my $unit = $_;
+
+        if (defined($activePrev->{$unit}) and not $unitsToRestart{$unit} and not $unitsToStop{$unit}) {
+            $unitsToReload{$unit} = 1;
+            recordUnit($reloadListFile, $unit);
+        }
+    }
+    unlink($dryReloadByActivationFile);
+
+    print STDERR "would restart systemd\n" if $restartSystemd;
+    print STDERR "would reload the following units: ", join(", ", sort(keys(%unitsToReload))), "\n"
+        if scalar(keys(%unitsToReload)) > 0;
+    print STDERR "would restart the following units: ", join(", ", sort(keys(%unitsToRestart))), "\n"
+        if scalar(keys(%unitsToRestart)) > 0;
+    my @unitsToStartFiltered = filterUnits(\%unitsToStart);
+    print STDERR "would start the following units: ", join(", ", @unitsToStartFiltered), "\n"
+        if scalar(@unitsToStartFiltered);
+    exit 0;
+}
+
+
+syslog(LOG_NOTICE, "switching to system configuration $out");
+
+if (scalar(keys(%unitsToStop)) > 0) {
+    print STDERR "stopping the following units: ", join(", ", @unitsToStopFiltered), "\n"
+        if scalar(@unitsToStopFiltered);
+    # Use current version of systemctl binary before daemon is reexeced.
+    system("$curSystemd/systemctl", "stop", "--", sort(keys(%unitsToStop)));
+}
+
+print STDERR "NOT restarting the following changed units: ", join(", ", sort(keys(%unitsToSkip))), "\n"
+    if scalar(keys(%unitsToSkip)) > 0;
+
+# Activate the new configuration (i.e., update /etc, make accounts,
+# and so on).
+my $res = 0;
+print STDERR "activating the configuration...\n";
+system("$out/activate", "$out") == 0 or $res = 2;
+
+# Handle the activation script requesting the restart or reload of a unit.
+foreach (split('\n', read_file($restartByActivationFile, err_mode => 'quiet') // "")) {
+    my $unit = $_;
+    my $baseUnit = $unit;
+    my $newUnitFile = "$out/etc/systemd/system/$baseUnit";
+
+    # Detect template instances.
+    if (!-e $newUnitFile && $unit =~ /^(.*)@[^\.]*\.(.*)$/) {
+      $baseUnit = "$1\@.$2";
+      $newUnitFile = "$out/etc/systemd/system/$baseUnit";
+    }
+
+    my $baseName = $baseUnit;
+    $baseName =~ s/\.[a-z]*$//;
+
+    # Start units if they were not active previously
+    if (not defined($activePrev->{$unit})) {
+        $unitsToStart{$unit} = 1;
+        recordUnit($startListFile, $unit);
+        next;
+    }
+
+    handleModifiedUnit($unit, $baseName, $newUnitFile, undef, $activePrev, \%unitsToRestart, \%unitsToRestart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
+}
+# We can remove the file now because it has been propagated to the other restart/reload files
+unlink($restartByActivationFile);
+
+foreach (split('\n', read_file($reloadByActivationFile, err_mode => 'quiet') // "")) {
+    my $unit = $_;
+
+    if (defined($activePrev->{$unit}) and not $unitsToRestart{$unit} and not $unitsToStop{$unit}) {
+        $unitsToReload{$unit} = 1;
+        recordUnit($reloadListFile, $unit);
+    }
+}
+# We can remove the file now because it has been propagated to the other reload file
+unlink($reloadByActivationFile);
+
+# Restart systemd if necessary. Note that this is done using the
+# current version of systemd, just in case the new one has trouble
+# communicating with the running pid 1.
+if ($restartSystemd) {
+    print STDERR "restarting systemd...\n";
+    system("$curSystemd/systemctl", "daemon-reexec") == 0 or $res = 2;
+}
+
+# Forget about previously failed services.
+system("@systemd@/bin/systemctl", "reset-failed");
+
+# Make systemd reload its units.
+system("@systemd@/bin/systemctl", "daemon-reload") == 0 or $res = 3;
+
+# Reload user units
+open(my $listActiveUsers, '-|', '@systemd@/bin/loginctl', 'list-users', '--no-legend');
+while (my $f = <$listActiveUsers>) {
+    next unless $f =~ /^\s*(?<uid>\d+)\s+(?<user>\S+)/;
+    my ($uid, $name) = ($+{uid}, $+{user});
+    print STDERR "reloading user units for $name...\n";
+
+    system("@su@", "-s", "@shell@", "-l", $name, "-c",
+           "export XDG_RUNTIME_DIR=/run/user/$uid; " .
+           "$curSystemd/systemctl --user daemon-reexec; " .
+           "@systemd@/bin/systemctl --user start nixos-activation.service");
+}
+
+close($listActiveUsers);
+
+# Set the new tmpfiles
+print STDERR "setting up tmpfiles\n";
+system("@systemd@/bin/systemd-tmpfiles", "--create", "--remove", "--exclude-prefix=/dev") == 0 or $res = 3;
+
+# Before reloading we need to ensure that the units are still active. They may have been
+# deactivated because one of their requirements got stopped. If they are inactive
+# but should have been reloaded, the user probably expects them to be started.
+if (scalar(keys(%unitsToReload)) > 0) {
+    for my $unit (keys(%unitsToReload)) {
+        if (!unit_is_active($unit)) {
+            # Figure out if we need to start the unit
+            my %unit_info = parse_unit("$out/etc/systemd/system/$unit");
+            if (!(parseSystemdBool(\%unit_info, 'Unit', 'RefuseManualStart', 0) || parseSystemdBool(\%unit_info, 'Unit', 'X-OnlyManualStart', 0))) {
+                $unitsToStart{$unit} = 1;
+                recordUnit($startListFile, $unit);
+            }
+            # Don't reload the unit, reloading would fail
+            delete %unitsToReload{$unit};
+            unrecord_unit($reloadListFile, $unit);
+        }
+    }
+}
+# Reload units that need it. This includes remounting changed mount
+# units.
+if (scalar(keys(%unitsToReload)) > 0) {
+    print STDERR "reloading the following units: ", join(", ", sort(keys(%unitsToReload))), "\n";
+    system("@systemd@/bin/systemctl", "reload", "--", sort(keys(%unitsToReload))) == 0 or $res = 4;
+    unlink($reloadListFile);
+}
+
+# Restart changed services (those that have to be restarted rather
+# than stopped and started).
+if (scalar(keys(%unitsToRestart)) > 0) {
+    print STDERR "restarting the following units: ", join(", ", sort(keys(%unitsToRestart))), "\n";
+    system("@systemd@/bin/systemctl", "restart", "--", sort(keys(%unitsToRestart))) == 0 or $res = 4;
+    unlink($restartListFile);
+}
+
+# Start all active targets, as well as changed units we stopped above.
+# The latter is necessary because some may not be dependencies of the
+# targets (i.e., they were manually started).  FIXME: detect units
+# that are symlinks to other units.  We shouldn't start both at the
+# same time because we'll get a "Failed to add path to set" error from
+# systemd.
+my @unitsToStartFiltered = filterUnits(\%unitsToStart);
+print STDERR "starting the following units: ", join(", ", @unitsToStartFiltered), "\n"
+    if scalar(@unitsToStartFiltered);
+system("@systemd@/bin/systemctl", "start", "--", sort(keys(%unitsToStart))) == 0 or $res = 4;
+unlink($startListFile);
+
+
+# Print failed and new units.
+my (@failed, @new);
+my $activeNew = getActiveUnits();
+while (my ($unit, $state) = each(%{$activeNew})) {
+    if ($state->{state} eq "failed") {
+        push(@failed, $unit);
+        next;
+    }
+
+    if ($state->{substate} eq "auto-restart") {
+        # A unit in auto-restart substate is a failure *if* it previously failed to start
+        my $main_status = `@systemd@/bin/systemctl show --value --property=ExecMainStatus '$unit'`;
+        chomp($main_status);
+
+        if ($main_status ne "0") {
+            push(@failed, $unit);
+            next;
+        }
+    }
+
+    # Ignore scopes since they are not managed by this script but rather
+    # created and managed by third-party services via the systemd dbus API.
+    # This only lists units that are not failed (including ones that are in auto-restart but have not failed previously)
+    if ($state->{state} ne "failed" && !defined($activePrev->{$unit}) && $unit !~ /\.scope$/msx) {
+        push(@new, $unit);
+    }
+}
+
+if (scalar(@new) > 0) {
+    print STDERR "the following new units were started: ", join(", ", sort(@new)), "\n"
+}
+
+if (scalar(@failed) > 0) {
+    my @failed_sorted = sort(@failed);
+    print STDERR "warning: the following units failed: ", join(", ", @failed_sorted), "\n\n";
+    system("@systemd@/bin/systemctl status --no-pager --full '" . join("' '", @failed_sorted) . "' >&2");
+    $res = 4;
+}
+
+if ($res == 0) {
+    syslog(LOG_NOTICE, "finished switching to system configuration $out");
+} else {
+    syslog(LOG_ERR, "switching to system configuration $out failed (status $res)");
+}
+
+exit($res);
diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix
new file mode 100644
index 00000000000..b8aeee8c11b
--- /dev/null
+++ b/nixos/modules/system/activation/top-level.nix
@@ -0,0 +1,355 @@
+{ config, lib, pkgs, extendModules, noUserModules, ... }:
+
+with lib;
+
+let
+
+
+  # This attribute is responsible for creating boot entries for
+  # child configuration. They are only (directly) accessible
+  # when the parent configuration is boot default. For example,
+  # you can provide an easy way to boot the same configuration
+  # as you use, but with another kernel
+  # !!! fix this
+  children =
+    mapAttrs
+      (childName: childConfig: childConfig.configuration.system.build.toplevel)
+      config.specialisation;
+
+  systemBuilder =
+    let
+      kernelPath = "${config.boot.kernelPackages.kernel}/" +
+        "${config.system.boot.loader.kernelFile}";
+      initrdPath = "${config.system.build.initialRamdisk}/" +
+        "${config.system.boot.loader.initrdFile}";
+    in ''
+      mkdir $out
+
+      # Containers don't have their own kernel or initrd.  They boot
+      # directly into stage 2.
+      ${optionalString (!config.boot.isContainer) ''
+        if [ ! -f ${kernelPath} ]; then
+          echo "The bootloader cannot find the proper kernel image."
+          echo "(Expecting ${kernelPath})"
+          false
+        fi
+
+        ln -s ${kernelPath} $out/kernel
+        ln -s ${config.system.modulesTree} $out/kernel-modules
+        ${optionalString (config.hardware.deviceTree.package != null) ''
+          ln -s ${config.hardware.deviceTree.package} $out/dtbs
+        ''}
+
+        echo -n "$kernelParams" > $out/kernel-params
+
+        ln -s ${initrdPath} $out/initrd
+
+        ln -s ${config.system.build.initialRamdiskSecretAppender}/bin/append-initrd-secrets $out
+
+        ln -s ${config.hardware.firmware}/lib/firmware $out/firmware
+      ''}
+
+      echo "$activationScript" > $out/activate
+      echo "$dryActivationScript" > $out/dry-activate
+      substituteInPlace $out/activate --subst-var out
+      substituteInPlace $out/dry-activate --subst-var out
+      chmod u+x $out/activate $out/dry-activate
+      unset activationScript dryActivationScript
+      ${pkgs.stdenv.shellDryRun} $out/activate
+      ${pkgs.stdenv.shellDryRun} $out/dry-activate
+
+      cp ${config.system.build.bootStage2} $out/init
+      substituteInPlace $out/init --subst-var-by systemConfig $out
+
+      ln -s ${config.system.build.etc}/etc $out/etc
+      ln -s ${config.system.path} $out/sw
+      ln -s "$systemd" $out/systemd
+
+      echo -n "$configurationName" > $out/configuration-name
+      echo -n "systemd ${toString config.systemd.package.interfaceVersion}" > $out/init-interface-version
+      echo -n "$nixosLabel" > $out/nixos-version
+      echo -n "${config.boot.kernelPackages.stdenv.hostPlatform.system}" > $out/system
+
+      mkdir $out/specialisation
+      ${concatStringsSep "\n"
+      (mapAttrsToList (name: path: "ln -s ${path} $out/specialisation/${name}") children)}
+
+      mkdir $out/bin
+      export localeArchive="${config.i18n.glibcLocales}/lib/locale/locale-archive"
+      substituteAll ${./switch-to-configuration.pl} $out/bin/switch-to-configuration
+      chmod +x $out/bin/switch-to-configuration
+      ${optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
+        if ! output=$($perl/bin/perl -c $out/bin/switch-to-configuration 2>&1); then
+          echo "switch-to-configuration syntax is not valid:"
+          echo "$output"
+          exit 1
+        fi
+      ''}
+
+      echo -n "${toString config.system.extraDependencies}" > $out/extra-dependencies
+
+      ${config.system.extraSystemBuilderCmds}
+    '';
+
+  # Putting it all together.  This builds a store path containing
+  # symlinks to the various parts of the built configuration (the
+  # kernel, systemd units, init scripts, etc.) as well as a script
+  # `switch-to-configuration' that activates the configuration and
+  # makes it bootable.
+  baseSystem = pkgs.stdenvNoCC.mkDerivation {
+    name = "nixos-system-${config.system.name}-${config.system.nixos.label}";
+    preferLocalBuild = true;
+    allowSubstitutes = false;
+    buildCommand = systemBuilder;
+
+    inherit (pkgs) coreutils;
+    systemd = config.systemd.package;
+    shell = "${pkgs.bash}/bin/sh";
+    su = "${pkgs.shadow.su}/bin/su";
+    utillinux = pkgs.util-linux;
+
+    kernelParams = config.boot.kernelParams;
+    installBootLoader = config.system.build.installBootLoader;
+    activationScript = config.system.activationScripts.script;
+    dryActivationScript = config.system.dryActivationScript;
+    nixosLabel = config.system.nixos.label;
+
+    configurationName = config.boot.loader.grub.configurationName;
+
+    # Needed by switch-to-configuration.
+    perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp NetDBus ]);
+  };
+
+  # Handle assertions and warnings
+
+  failedAssertions = map (x: x.message) (filter (x: !x.assertion) config.assertions);
+
+  baseSystemAssertWarn = if failedAssertions != []
+    then throw "\nFailed assertions:\n${concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}"
+    else showWarnings config.warnings baseSystem;
+
+  # Replace runtime dependencies
+  system = foldr ({ oldDependency, newDependency }: drv:
+      pkgs.replaceDependency { inherit oldDependency newDependency drv; }
+    ) baseSystemAssertWarn config.system.replaceRuntimeDependencies;
+
+  /* Workaround until https://github.com/NixOS/nixpkgs/pull/156533
+     Call can be replaced by argument when that's merged.
+  */
+  tmpFixupSubmoduleBoundary = subopts:
+    lib.mkOption {
+      type = lib.types.submoduleWith {
+        modules = [ { options = subopts; } ];
+      };
+    };
+
+in
+
+{
+  imports = [
+    ../build.nix
+    (mkRemovedOptionModule [ "nesting" "clone" ] "Use `specialisation.«name» = { inheritParentConfig = true; configuration = { ... }; }` instead.")
+    (mkRemovedOptionModule [ "nesting" "children" ] "Use `specialisation.«name».configuration = { ... }` instead.")
+  ];
+
+  options = {
+
+    specialisation = mkOption {
+      default = {};
+      example = lib.literalExpression "{ fewJobsManyCores.configuration = { nix.settings = { core = 0; max-jobs = 1; }; }";
+      description = ''
+        Additional configurations to build. If
+        <literal>inheritParentConfig</literal> is true, the system
+        will be based on the overall system configuration.
+
+        To switch to a specialised configuration
+        (e.g. <literal>fewJobsManyCores</literal>) at runtime, run:
+
+        <screen>
+        <prompt># </prompt>sudo /run/current-system/specialisation/fewJobsManyCores/bin/switch-to-configuration test
+        </screen>
+      '';
+      type = types.attrsOf (types.submodule (
+        local@{ ... }: let
+          extend = if local.config.inheritParentConfig
+            then extendModules
+            else noUserModules.extendModules;
+        in {
+          options.inheritParentConfig = mkOption {
+            type = types.bool;
+            default = true;
+            description = "Include the entire system's configuration. Set to false to make a completely differently configured system.";
+          };
+
+          options.configuration = mkOption {
+            default = {};
+            description = ''
+              Arbitrary NixOS configuration.
+
+              Anything you can add to a normal NixOS configuration, you can add
+              here, including imports and config values, although nested
+              specialisations will be ignored.
+            '';
+            visible = "shallow";
+            inherit (extend { modules = [ ./no-clone.nix ]; }) type;
+          };
+        })
+      );
+    };
+
+    system.boot.loader.id = mkOption {
+      internal = true;
+      default = "";
+      description = ''
+        Id string of the used bootloader.
+      '';
+    };
+
+    system.boot.loader.kernelFile = mkOption {
+      internal = true;
+      default = pkgs.stdenv.hostPlatform.linux-kernel.target;
+      defaultText = literalExpression "pkgs.stdenv.hostPlatform.linux-kernel.target";
+      type = types.str;
+      description = ''
+        Name of the kernel file to be passed to the bootloader.
+      '';
+    };
+
+    system.boot.loader.initrdFile = mkOption {
+      internal = true;
+      default = "initrd";
+      type = types.str;
+      description = ''
+        Name of the initrd file to be passed to the bootloader.
+      '';
+    };
+
+    system.build = tmpFixupSubmoduleBoundary {
+      installBootLoader = mkOption {
+        internal = true;
+        # "; true" => make the `$out` argument from switch-to-configuration.pl
+        #             go to `true` instead of `echo`, hiding the useless path
+        #             from the log.
+        default = "echo 'Warning: do not know how to make this configuration bootable; please enable a boot loader.' 1>&2; true";
+        description = ''
+          A program that writes a bootloader installation script to the path passed in the first command line argument.
+
+          See <literal>nixos/modules/system/activation/switch-to-configuration.pl</literal>.
+        '';
+        type = types.unique {
+          message = ''
+            Only one bootloader can be enabled at a time. This requirement has not
+            been checked until NixOS 22.05. Earlier versions defaulted to the last
+            definition. Change your configuration to enable only one bootloader.
+          '';
+        } (types.either types.str types.package);
+      };
+
+      toplevel = mkOption {
+        type = types.package;
+        readOnly = true;
+        description = ''
+          This option contains the store path that typically represents a NixOS system.
+
+          You can read this path in a custom deployment tool for example.
+        '';
+      };
+    };
+
+
+    system.copySystemConfiguration = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        If enabled, copies the NixOS configuration file
+        (usually <filename>/etc/nixos/configuration.nix</filename>)
+        and links it from the resulting system
+        (getting to <filename>/run/current-system/configuration.nix</filename>).
+        Note that only this single file is copied, even if it imports others.
+      '';
+    };
+
+    system.extraSystemBuilderCmds = mkOption {
+      type = types.lines;
+      internal = true;
+      default = "";
+      description = ''
+        This code will be added to the builder creating the system store path.
+      '';
+    };
+
+    system.extraDependencies = mkOption {
+      type = types.listOf types.package;
+      default = [];
+      description = ''
+        A list of packages that should be included in the system
+        closure but not otherwise made available to users. This is
+        primarily used by the installation tests.
+      '';
+    };
+
+    system.replaceRuntimeDependencies = mkOption {
+      default = [];
+      example = lib.literalExpression "[ ({ original = pkgs.openssl; replacement = pkgs.callPackage /path/to/openssl { }; }) ]";
+      type = types.listOf (types.submodule (
+        { ... }: {
+          options.original = mkOption {
+            type = types.package;
+            description = "The original package to override.";
+          };
+
+          options.replacement = mkOption {
+            type = types.package;
+            description = "The replacement package.";
+          };
+        })
+      );
+      apply = map ({ original, replacement, ... }: {
+        oldDependency = original;
+        newDependency = replacement;
+      });
+      description = ''
+        List of packages to override without doing a full rebuild.
+        The original derivation and replacement derivation must have the same
+        name length, and ideally should have close-to-identical directory layout.
+      '';
+    };
+
+    system.name = mkOption {
+      type = types.str;
+      default =
+        if config.networking.hostName == ""
+        then "unnamed"
+        else config.networking.hostName;
+      defaultText = literalExpression ''
+        if config.networking.hostName == ""
+        then "unnamed"
+        else config.networking.hostName;
+      '';
+      description = ''
+        The name of the system used in the <option>system.build.toplevel</option> derivation.
+        </para><para>
+        That derivation has the following name:
+        <literal>"nixos-system-''${config.system.name}-''${config.system.nixos.label}"</literal>
+      '';
+    };
+
+  };
+
+
+  config = {
+
+    system.extraSystemBuilderCmds =
+      optionalString
+        config.system.copySystemConfiguration
+        ''ln -s '${import ../../../lib/from-env.nix "NIXOS_CONFIG" <nixos-config>}' \
+            "$out/configuration.nix"
+        '';
+
+    system.build.toplevel = system;
+
+  };
+
+  # uses extendModules to generate a type
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/system/boot/binfmt.nix b/nixos/modules/system/boot/binfmt.nix
new file mode 100644
index 00000000000..33748358e45
--- /dev/null
+++ b/nixos/modules/system/boot/binfmt.nix
@@ -0,0 +1,325 @@
+{ config, lib, pkgs, ... }:
+let
+  inherit (lib) mkOption types optionalString stringAfter;
+
+  cfg = config.boot.binfmt;
+
+  makeBinfmtLine = name: { recognitionType, offset, magicOrExtension
+                         , mask, preserveArgvZero, openBinary
+                         , matchCredentials, fixBinary, ...
+                         }: let
+    type = if recognitionType == "magic" then "M" else "E";
+    offset' = toString offset;
+    mask' = toString mask;
+    interpreter = "/run/binfmt/${name}";
+    flags = if !(matchCredentials -> openBinary)
+              then throw "boot.binfmt.registrations.${name}: you can't specify openBinary = false when matchCredentials = true."
+            else optionalString preserveArgvZero "P" +
+                 optionalString (openBinary && !matchCredentials) "O" +
+                 optionalString matchCredentials "C" +
+                 optionalString fixBinary "F";
+  in ":${name}:${type}:${offset'}:${magicOrExtension}:${mask'}:${interpreter}:${flags}";
+
+  activationSnippet = name: { interpreter, wrapInterpreterInShell, ... }: if wrapInterpreterInShell then ''
+    rm -f /run/binfmt/${name}
+    cat > /run/binfmt/${name} << 'EOF'
+    #!${pkgs.bash}/bin/sh
+    exec -- ${interpreter} "$@"
+    EOF
+    chmod +x /run/binfmt/${name}
+  '' else ''
+    rm -f /run/binfmt/${name}
+    ln -s ${interpreter} /run/binfmt/${name}
+  '';
+
+  getEmulator = system: (lib.systems.elaborate { inherit system; }).emulator pkgs;
+  getQemuArch = system: (lib.systems.elaborate { inherit system; }).qemuArch;
+
+  # Mapping of systems to “magicOrExtension†and “maskâ€. Mostly taken from:
+  # - https://github.com/cleverca22/nixos-configs/blob/master/qemu.nix
+  # and
+  # - https://github.com/qemu/qemu/blob/master/scripts/qemu-binfmt-conf.sh
+  # TODO: maybe put these in a JSON file?
+  magics = {
+    armv6l-linux = {
+      magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\x00\xff\xfe\xff\xff\xff'';
+    };
+    armv7l-linux = {
+      magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\x00\xff\xfe\xff\xff\xff'';
+    };
+    aarch64-linux = {
+      magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7\x00'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\x00\xff\xfe\xff\xff\xff'';
+    };
+    aarch64_be-linux = {
+      magicOrExtension = ''\x7fELF\x02\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
+    };
+    i386-linux = {
+      magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x03\x00'';
+      mask = ''\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
+    };
+    i486-linux = {
+      magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x06\x00'';
+      mask = ''\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
+    };
+    i586-linux = {
+      magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x06\x00'';
+      mask = ''\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
+    };
+    i686-linux = {
+      magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x06\x00'';
+      mask = ''\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
+    };
+    x86_64-linux = {
+      magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x3e\x00'';
+      mask = ''\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
+    };
+    alpha-linux = {
+      magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x26\x90'';
+      mask = ''\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
+    };
+    sparc64-linux = {
+      magicOrExtension = ''\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x02'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
+    };
+    sparc-linux = {
+      magicOrExtension = ''\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x12'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
+    };
+    powerpc-linux = {
+      magicOrExtension = ''\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x14'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
+    };
+    powerpc64-linux = {
+      magicOrExtension = ''\x7fELF\x02\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x15'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
+    };
+    powerpc64le-linux = {
+      magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x15\x00'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\x00'';
+    };
+    mips-linux = {
+      magicOrExtension = ''\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
+    };
+    mipsel-linux = {
+      magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
+    };
+    mips64-linux = {
+      magicOrExtension = ''\x7fELF\x02\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
+    };
+    mips64el-linux = {
+      magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
+    };
+    riscv32-linux = {
+      magicOrExtension = ''\x7fELF\x01\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'';
+    };
+    riscv64-linux = {
+      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'';
+    };
+    wasm32-wasi = {
+      magicOrExtension = ''\x00asm'';
+      mask = ''\xff\xff\xff\xff'';
+    };
+    wasm64-wasi = {
+      magicOrExtension = ''\x00asm'';
+      mask = ''\xff\xff\xff\xff'';
+    };
+    x86_64-windows = {
+      magicOrExtension = ".exe";
+      recognitionType = "extension";
+    };
+    i686-windows = {
+      magicOrExtension = ".exe";
+      recognitionType = "extension";
+    };
+  };
+
+in {
+  imports = [
+    (lib.mkRenamedOptionModule [ "boot" "binfmtMiscRegistrations" ] [ "boot" "binfmt" "registrations" ])
+  ];
+
+  options = {
+    boot.binfmt = {
+      registrations = mkOption {
+        default = {};
+
+        description = ''
+          Extra binary formats to register with the kernel.
+          See https://www.kernel.org/doc/html/latest/admin-guide/binfmt-misc.html for more details.
+        '';
+
+        type = types.attrsOf (types.submodule ({ config, ... }: {
+          options = {
+            recognitionType = mkOption {
+              default = "magic";
+              description = "Whether to recognize executables by magic number or extension.";
+              type = types.enum [ "magic" "extension" ];
+            };
+
+            offset = mkOption {
+              default = null;
+              description = "The byte offset of the magic number used for recognition.";
+              type = types.nullOr types.int;
+            };
+
+            magicOrExtension = mkOption {
+              description = "The magic number or extension to match on.";
+              type = types.str;
+            };
+
+            mask = mkOption {
+              default = null;
+              description =
+                "A mask to be ANDed with the byte sequence of the file before matching";
+              type = types.nullOr types.str;
+            };
+
+            interpreter = mkOption {
+              description = ''
+                The interpreter to invoke to run the program.
+
+                Note that the actual registration will point to
+                /run/binfmt/''${name}, so the kernel interpreter length
+                limit doesn't apply.
+              '';
+              type = types.path;
+            };
+
+            preserveArgvZero = mkOption {
+              default = false;
+              description = ''
+                Whether to pass the original argv[0] to the interpreter.
+
+                See the description of the 'P' flag in the kernel docs
+                for more details;
+              '';
+              type = types.bool;
+            };
+
+            openBinary = mkOption {
+              default = config.matchCredentials;
+              description = ''
+                Whether to pass the binary to the interpreter as an open
+                file descriptor, instead of a path.
+              '';
+              type = types.bool;
+            };
+
+            matchCredentials = mkOption {
+              default = false;
+              description = ''
+                Whether to launch with the credentials and security
+                token of the binary, not the interpreter (e.g. setuid
+                bit).
+
+                See the description of the 'C' flag in the kernel docs
+                for more details.
+
+                Implies/requires openBinary = true.
+              '';
+              type = types.bool;
+            };
+
+            fixBinary = mkOption {
+              default = false;
+              description = ''
+                Whether to open the interpreter file as soon as the
+                registration is loaded, rather than waiting for a
+                relevant file to be invoked.
+
+                See the description of the 'F' flag in the kernel docs
+                for more details.
+              '';
+              type = types.bool;
+            };
+
+            wrapInterpreterInShell = mkOption {
+              default = true;
+              description = ''
+                Whether to wrap the interpreter in a shell script.
+
+                This allows a shell command to be set as the interpreter.
+              '';
+              type = types.bool;
+            };
+
+            interpreterSandboxPath = mkOption {
+              internal = true;
+              default = null;
+              description = ''
+                Path of the interpreter to expose in the build sandbox.
+              '';
+              type = types.nullOr types.path;
+            };
+          };
+        }));
+      };
+
+      emulatedSystems = mkOption {
+        default = [];
+        example = [ "wasm32-wasi" "x86_64-windows" "aarch64-linux" ];
+        description = ''
+          List of systems to emulate. Will also configure Nix to
+          support your new systems.
+          Warning: the builder can execute all emulated systems within the same build, which introduces impurities in the case of cross compilation.
+        '';
+        type = types.listOf types.str;
+      };
+    };
+  };
+
+  config = {
+    boot.binfmt.registrations = builtins.listToAttrs (map (system: {
+      name = system;
+      value = let
+        interpreter = getEmulator system;
+        qemuArch = getQemuArch system;
+
+        preserveArgvZero = "qemu-${qemuArch}" == baseNameOf interpreter;
+        interpreterReg = let
+          wrapperName = "qemu-${qemuArch}-binfmt-P";
+          wrapper = pkgs.wrapQemuBinfmtP wrapperName interpreter;
+        in
+          if preserveArgvZero then "${wrapper}/bin/${wrapperName}"
+          else interpreter;
+      in {
+        inherit preserveArgvZero;
+
+        interpreter = interpreterReg;
+        wrapInterpreterInShell = !preserveArgvZero;
+        interpreterSandboxPath = dirOf (dirOf interpreterReg);
+      } // (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";
+      extra-sandbox-paths = let
+        ruleFor = system: cfg.registrations.${system};
+        hasWrappedRule = lib.any (system: (ruleFor system).wrapInterpreterInShell) cfg.emulatedSystems;
+      in [ "/run/binfmt" ]
+        ++ lib.optional hasWrappedRule "${pkgs.bash}"
+        ++ (map (system: (ruleFor system).interpreterSandboxPath) cfg.emulatedSystems);
+    };
+
+    environment.etc."binfmt.d/nixos.conf".source = builtins.toFile "binfmt_nixos.conf"
+      (lib.concatStringsSep "\n" (lib.mapAttrsToList makeBinfmtLine config.boot.binfmt.registrations));
+    system.activationScripts.binfmt = stringAfter [ "specialfs" ] ''
+      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"
+    ];
+  };
+}
diff --git a/nixos/modules/system/boot/emergency-mode.nix b/nixos/modules/system/boot/emergency-mode.nix
new file mode 100644
index 00000000000..ec697bcee26
--- /dev/null
+++ b/nixos/modules/system/boot/emergency-mode.nix
@@ -0,0 +1,37 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    systemd.enableEmergencyMode = mkOption {
+      default = true;
+      type = types.bool;
+      description = ''
+        Whether to enable emergency mode, which is an
+        <command>sulogin</command> shell started on the console if
+        mounting a filesystem fails.  Since some machines (like EC2
+        instances) have no console of any kind, emergency mode doesn't
+        make sense, and it's better to continue with the boot insofar
+        as possible.
+      '';
+    };
+
+  };
+
+  ###### implementation
+
+  config = {
+
+    systemd.additionalUpstreamSystemUnits = optionals
+      config.systemd.enableEmergencyMode [
+        "emergency.target" "emergency.service"
+      ];
+
+  };
+
+}
diff --git a/nixos/modules/system/boot/grow-partition.nix b/nixos/modules/system/boot/grow-partition.nix
new file mode 100644
index 00000000000..87c981b24ce
--- /dev/null
+++ b/nixos/modules/system/boot/grow-partition.nix
@@ -0,0 +1,53 @@
+# This module automatically grows the root partition.
+# This allows an instance to be created with a bigger root filesystem
+# than provided by the machine image.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  imports = [
+    (mkRenamedOptionModule [ "virtualisation" "growPartition" ] [ "boot" "growPartition" ])
+  ];
+
+  options = {
+    boot.growPartition = mkEnableOption "grow the root partition on boot";
+  };
+
+  config = mkIf config.boot.growPartition {
+
+    boot.initrd.extraUtilsCommands = ''
+      copy_bin_and_libs ${pkgs.gawk}/bin/gawk
+      copy_bin_and_libs ${pkgs.gnused}/bin/sed
+      copy_bin_and_libs ${pkgs.util-linux}/sbin/sfdisk
+      copy_bin_and_libs ${pkgs.util-linux}/sbin/lsblk
+
+      substitute "${pkgs.cloud-utils.guest}/bin/.growpart-wrapped" "$out/bin/growpart" \
+        --replace "${pkgs.bash}/bin/sh" "/bin/sh" \
+        --replace "awk" "gawk" \
+        --replace "sed" "gnused"
+
+      ln -s sed $out/bin/gnused
+    '';
+
+    boot.initrd.postDeviceCommands = ''
+      rootDevice="${config.fileSystems."/".device}"
+      if waitDevice "$rootDevice"; then
+        rootDevice="$(readlink -f "$rootDevice")"
+        parentDevice="$rootDevice"
+        while [ "''${parentDevice%[0-9]}" != "''${parentDevice}" ]; do
+          parentDevice="''${parentDevice%[0-9]}";
+        done
+        partNum="''${rootDevice#''${parentDevice}}"
+        if [ "''${parentDevice%[0-9]p}" != "''${parentDevice}" ] && [ -b "''${parentDevice%p}" ]; then
+          parentDevice="''${parentDevice%p}"
+        fi
+        TMPDIR=/run sh $(type -P growpart) "$parentDevice" "$partNum"
+        udevadm settle
+      fi
+    '';
+
+  };
+
+}
diff --git a/nixos/modules/system/boot/initrd-network.nix b/nixos/modules/system/boot/initrd-network.nix
new file mode 100644
index 00000000000..2a7417ed371
--- /dev/null
+++ b/nixos/modules/system/boot/initrd-network.nix
@@ -0,0 +1,148 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.boot.initrd.network;
+
+  dhcpInterfaces = lib.attrNames (lib.filterAttrs (iface: v: v.useDHCP == true) (config.networking.interfaces or {}));
+  doDhcp = config.networking.useDHCP || dhcpInterfaces != [];
+  dhcpIfShellExpr = if config.networking.useDHCP
+                      then "$(ls /sys/class/net/ | grep -v ^lo$)"
+                      else lib.concatMapStringsSep " " lib.escapeShellArg dhcpInterfaces;
+
+  udhcpcScript = pkgs.writeScript "udhcp-script"
+    ''
+      #! /bin/sh
+      if [ "$1" = bound ]; then
+        ip address add "$ip/$mask" dev "$interface"
+        if [ -n "$mtu" ]; then
+          ip link set mtu "$mtu" dev "$interface"
+        fi
+        if [ -n "$staticroutes" ]; then
+          echo "$staticroutes" \
+            | sed -r "s@(\S+) (\S+)@ ip route add \"\1\" via \"\2\" dev \"$interface\" ; @g" \
+            | sed -r "s@ via \"0\.0\.0\.0\"@@g" \
+            | /bin/sh
+        fi
+        if [ -n "$router" ]; then
+          ip route add "$router" dev "$interface" # just in case if "$router" is not within "$ip/$mask" (e.g. Hetzner Cloud)
+          ip route add default via "$router" dev "$interface"
+        fi
+        if [ -n "$dns" ]; then
+          rm -f /etc/resolv.conf
+          for server in $dns; do
+            echo "nameserver $server" >> /etc/resolv.conf
+          done
+        fi
+      fi
+    '';
+
+  udhcpcArgs = toString cfg.udhcpc.extraArgs;
+
+in
+
+{
+
+  options = {
+
+    boot.initrd.network.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Add network connectivity support to initrd. The network may be
+        configured using the <literal>ip</literal> kernel parameter,
+        as described in <link
+        xlink:href="https://www.kernel.org/doc/Documentation/filesystems/nfs/nfsroot.txt">the
+        kernel documentation</link>.  Otherwise, if
+        <option>networking.useDHCP</option> is enabled, an IP address
+        is acquired using DHCP.
+
+        You should add the module(s) required for your network card to
+        boot.initrd.availableKernelModules.
+        <literal>lspci -v | grep -iA8 'network\|ethernet'</literal>
+        will tell you which.
+      '';
+    };
+
+    boot.initrd.network.flushBeforeStage2 = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        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.
+      '';
+    };
+
+    boot.initrd.network.udhcpc.extraArgs = mkOption {
+      default = [];
+      type = types.listOf types.str;
+      description = ''
+        Additional command-line arguments passed verbatim to udhcpc if
+        <option>boot.initrd.network.enable</option> and <option>networking.useDHCP</option>
+        are enabled.
+      '';
+    };
+
+    boot.initrd.network.postCommands = mkOption {
+      default = "";
+      type = types.lines;
+      description = ''
+        Shell commands to be executed after stage 1 of the
+        boot has initialised the network.
+      '';
+    };
+
+
+  };
+
+  config = mkIf cfg.enable {
+
+    boot.initrd.kernelModules = [ "af_packet" ];
+
+    boot.initrd.extraUtilsCommands = ''
+      copy_bin_and_libs ${pkgs.klibc}/lib/klibc/bin.static/ipconfig
+    '';
+
+    boot.initrd.preLVMCommands = mkBefore (
+      # Search for interface definitions in command line.
+      ''
+        ifaces=""
+        for o in $(cat /proc/cmdline); do
+          case $o in
+            ip=*)
+              ipconfig $o && ifaces="$ifaces $(echo $o | cut -d: -f6)"
+              ;;
+          esac
+        done
+      ''
+
+      # Otherwise, use DHCP.
+      + optionalString doDhcp ''
+        # Bring up all interfaces.
+        for iface in ${dhcpIfShellExpr}; do
+          echo "bringing up network interface $iface..."
+          ip link set "$iface" up && ifaces="$ifaces $iface"
+        done
+
+        # Acquire DHCP leases.
+        for iface in ${dhcpIfShellExpr}; do
+          echo "acquiring IP address via DHCP on $iface..."
+          udhcpc --quit --now -i $iface -O staticroutes --script ${udhcpcScript} ${udhcpcArgs}
+        done
+      ''
+
+      + cfg.postCommands);
+
+    boot.initrd.postMountCommands = mkIf cfg.flushBeforeStage2 ''
+      for iface in $ifaces; do
+        ip address flush "$iface"
+        ip link set "$iface" down
+      done
+    '';
+
+  };
+
+}
diff --git a/nixos/modules/system/boot/initrd-openvpn.nix b/nixos/modules/system/boot/initrd-openvpn.nix
new file mode 100644
index 00000000000..9b52d4bbdb1
--- /dev/null
+++ b/nixos/modules/system/boot/initrd-openvpn.nix
@@ -0,0 +1,81 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.boot.initrd.network.openvpn;
+
+in
+
+{
+
+  options = {
+
+    boot.initrd.network.openvpn.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Starts an OpenVPN client during initrd boot. It can be used to e.g.
+        remotely accessing the SSH service controlled by
+        <option>boot.initrd.network.ssh</option> or other network services
+        included. Service is killed when stage-1 boot is finished.
+      '';
+    };
+
+    boot.initrd.network.openvpn.configuration = mkOption {
+      type = types.path; # Same type as boot.initrd.secrets
+      description = ''
+        The configuration file for OpenVPN.
+
+        <warning>
+          <para>
+            Unless your bootloader supports initrd secrets, this configuration
+            is stored insecurely in the global Nix store.
+          </para>
+        </warning>
+      '';
+      example = literalExpression "./configuration.ovpn";
+    };
+
+  };
+
+  config = mkIf (config.boot.initrd.network.enable && cfg.enable) {
+    assertions = [
+      {
+        assertion = cfg.configuration != null;
+        message = "You should specify a configuration for initrd OpenVPN";
+      }
+    ];
+
+    # Add kernel modules needed for OpenVPN
+    boot.initrd.kernelModules = [ "tun" "tap" ];
+
+    # Add openvpn and ip binaries to the initrd
+    # The shared libraries are required for DNS resolution
+    boot.initrd.extraUtilsCommands = ''
+      copy_bin_and_libs ${pkgs.openvpn}/bin/openvpn
+      copy_bin_and_libs ${pkgs.iproute2}/bin/ip
+
+      cp -pv ${pkgs.glibc}/lib/libresolv.so.2 $out/lib
+      cp -pv ${pkgs.glibc}/lib/libnss_dns.so.2 $out/lib
+    '';
+
+    boot.initrd.secrets = {
+      "/etc/initrd.ovpn" = cfg.configuration;
+    };
+
+    # openvpn --version would exit with 1 instead of 0
+    boot.initrd.extraUtilsCommandsTest = ''
+      $out/bin/openvpn --show-gateway
+    '';
+
+    # Add `iproute /bin/ip` to the config, to ensure that openvpn
+    # is able to set the routes
+    boot.initrd.network.postCommands = ''
+      (cat /etc/initrd.ovpn; echo -e '\niproute /bin/ip') | \
+        openvpn /dev/stdin &
+    '';
+  };
+
+}
diff --git a/nixos/modules/system/boot/initrd-ssh.nix b/nixos/modules/system/boot/initrd-ssh.nix
new file mode 100644
index 00000000000..0999142de86
--- /dev/null
+++ b/nixos/modules/system/boot/initrd-ssh.nix
@@ -0,0 +1,215 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.boot.initrd.network.ssh;
+
+in
+
+{
+
+  options.boot.initrd.network.ssh = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Start SSH service during initrd boot. It can be used to debug failing
+        boot on a remote server, enter pasphrase for an encrypted partition etc.
+        Service is killed when stage-1 boot is finished.
+
+        The sshd configuration is largely inherited from
+        <option>services.openssh</option>.
+      '';
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = 22;
+      description = ''
+        Port on which SSH initrd service should listen.
+      '';
+    };
+
+    shell = mkOption {
+      type = types.str;
+      default = "/bin/ash";
+      description = ''
+        Login shell of the remote user. Can be used to limit actions user can do.
+      '';
+    };
+
+    hostKeys = mkOption {
+      type = types.listOf (types.either types.str types.path);
+      default = [];
+      example = [
+        "/etc/secrets/initrd/ssh_host_rsa_key"
+        "/etc/secrets/initrd/ssh_host_ed25519_key"
+      ];
+      description = ''
+        Specify SSH host keys to import into the initrd.
+
+        To generate keys, use
+        <citerefentry><refentrytitle>ssh-keygen</refentrytitle><manvolnum>1</manvolnum></citerefentry>:
+
+        <screen>
+        <prompt># </prompt>ssh-keygen -t rsa -N "" -f /etc/secrets/initrd/ssh_host_rsa_key
+        <prompt># </prompt>ssh-keygen -t ed25519 -N "" -f /etc/secrets/initrd/ssh_host_ed25519_key
+        </screen>
+
+        <warning>
+          <para>
+            Unless your bootloader supports initrd secrets, these keys
+            are stored insecurely in the global Nix store. Do NOT use
+            your regular SSH host private keys for this purpose or
+            you'll expose them to regular users!
+          </para>
+          <para>
+            Additionally, even if your initrd supports secrets, if
+            you're using initrd SSH to unlock an encrypted disk then
+            using your regular host keys exposes the private keys on
+            your unencrypted boot partition.
+          </para>
+        </warning>
+      '';
+    };
+
+    authorizedKeys = mkOption {
+      type = types.listOf types.str;
+      default = config.users.users.root.openssh.authorizedKeys.keys;
+      defaultText = literalExpression "config.users.users.root.openssh.authorizedKeys.keys";
+      description = ''
+        Authorized keys for the root user on initrd.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = "Verbatim contents of <filename>sshd_config</filename>.";
+    };
+  };
+
+  imports =
+    map (opt: mkRemovedOptionModule ([ "boot" "initrd" "network" "ssh" ] ++ [ opt ]) ''
+      The initrd SSH functionality now uses OpenSSH rather than Dropbear.
+
+      If you want to keep your existing initrd SSH host keys, convert them with
+        $ dropbearconvert dropbear openssh dropbear_host_$type_key ssh_host_$type_key
+      and then set options.boot.initrd.network.ssh.hostKeys.
+    '') [ "hostRSAKey" "hostDSSKey" "hostECDSAKey" ];
+
+  config = let
+    # Nix complains if you include a store hash in initrd path names, so
+    # as an awful hack we drop the first character of the hash.
+    initrdKeyPath = path: if isString path
+      then path
+      else let name = builtins.baseNameOf path; in
+        builtins.unsafeDiscardStringContext ("/etc/ssh/" +
+          substring 1 (stringLength name) name);
+
+    sshdCfg = config.services.openssh;
+
+    sshdConfig = ''
+      Port ${toString cfg.port}
+
+      PasswordAuthentication no
+      ChallengeResponseAuthentication no
+
+      ${flip concatMapStrings cfg.hostKeys (path: ''
+        HostKey ${initrdKeyPath path}
+      '')}
+
+      KexAlgorithms ${concatStringsSep "," sshdCfg.kexAlgorithms}
+      Ciphers ${concatStringsSep "," sshdCfg.ciphers}
+      MACs ${concatStringsSep "," sshdCfg.macs}
+
+      LogLevel ${sshdCfg.logLevel}
+
+      ${if sshdCfg.useDns then ''
+        UseDNS yes
+      '' else ''
+        UseDNS no
+      ''}
+
+      ${cfg.extraConfig}
+    '';
+  in mkIf (config.boot.initrd.network.enable && cfg.enable) {
+    assertions = [
+      {
+        assertion = cfg.authorizedKeys != [];
+        message = "You should specify at least one authorized key for initrd SSH";
+      }
+
+      {
+        assertion = cfg.hostKeys != [];
+        message = ''
+          You must now pre-generate the host keys for initrd SSH.
+          See the boot.initrd.network.ssh.hostKeys documentation
+          for instructions.
+        '';
+      }
+    ];
+
+    boot.initrd.extraUtilsCommands = ''
+      copy_bin_and_libs ${pkgs.openssh}/bin/sshd
+      cp -pv ${pkgs.glibc.out}/lib/libnss_files.so.* $out/lib
+    '';
+
+    boot.initrd.extraUtilsCommandsTest = ''
+      # sshd requires a host key to check config, so we pass in the test's
+      tmpkey="$(mktemp initrd-ssh-testkey.XXXXXXXXXX)"
+      cp "${../../../tests/initrd-network-ssh/ssh_host_ed25519_key}" "$tmpkey"
+      # keys from Nix store are world-readable, which sshd doesn't like
+      chmod 600 "$tmpkey"
+      echo -n ${escapeShellArg sshdConfig} |
+        $out/bin/sshd -t -f /dev/stdin \
+        -h "$tmpkey"
+      rm "$tmpkey"
+    '';
+
+    boot.initrd.network.postCommands = ''
+      echo '${cfg.shell}' > /etc/shells
+      echo 'root:x:0:0:root:/root:${cfg.shell}' > /etc/passwd
+      echo 'sshd:x:1:1:sshd:/var/empty:/bin/nologin' >> /etc/passwd
+      echo 'passwd: files' > /etc/nsswitch.conf
+
+      mkdir -p /var/log /var/empty
+      touch /var/log/lastlog
+
+      mkdir -p /etc/ssh
+      echo -n ${escapeShellArg sshdConfig} > /etc/ssh/sshd_config
+
+      echo "export PATH=$PATH" >> /etc/profile
+      echo "export LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> /etc/profile
+
+      mkdir -p /root/.ssh
+      ${concatStrings (map (key: ''
+        echo ${escapeShellArg key} >> /root/.ssh/authorized_keys
+      '') cfg.authorizedKeys)}
+
+      ${flip concatMapStrings cfg.hostKeys (path: ''
+        # keys from Nix store are world-readable, which sshd doesn't like
+        chmod 0600 "${initrdKeyPath path}"
+      '')}
+
+      /bin/sshd -e
+    '';
+
+    boot.initrd.postMountCommands = ''
+      # Stop sshd cleanly before stage 2.
+      #
+      # If you want to keep it around to debug post-mount SSH issues,
+      # run `touch /.keep_sshd` (either from an SSH session or in
+      # another initrd hook like preDeviceCommands).
+      if ! [ -e /.keep_sshd ]; then
+        pkill -x sshd
+      fi
+    '';
+
+    boot.initrd.secrets = listToAttrs
+      (map (path: nameValuePair (initrdKeyPath path) path) cfg.hostKeys);
+  };
+
+}
diff --git a/nixos/modules/system/boot/kernel.nix b/nixos/modules/system/boot/kernel.nix
new file mode 100644
index 00000000000..db00244ca0a
--- /dev/null
+++ b/nixos/modules/system/boot/kernel.nix
@@ -0,0 +1,350 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inherit (config.boot) kernelPatches;
+  inherit (config.boot.kernel) features randstructSeed;
+  inherit (config.boot.kernelPackages) kernel;
+
+  kernelModulesConf = pkgs.writeText "nixos.conf"
+    ''
+      ${concatStringsSep "\n" config.boot.kernelModules}
+    '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    boot.kernel.features = mkOption {
+      default = {};
+      example = literalExpression "{ debug = true; }";
+      internal = true;
+      description = ''
+        This option allows to enable or disable certain kernel features.
+        It's not API, because it's about kernel feature sets, that
+        make sense for specific use cases. Mostly along with programs,
+        which would have separate nixos options.
+        `grep features pkgs/os-specific/linux/kernel/common-config.nix`
+      '';
+    };
+
+    boot.kernelPackages = mkOption {
+      default = pkgs.linuxPackages;
+      type = types.raw;
+      apply = kernelPackages: kernelPackages.extend (self: super: {
+        kernel = super.kernel.override (originalArgs: {
+          inherit randstructSeed;
+          kernelPatches = (originalArgs.kernelPatches or []) ++ kernelPatches;
+          features = lib.recursiveUpdate super.kernel.features features;
+        });
+      });
+      # We don't want to evaluate all of linuxPackages for the manual
+      # - some of it might not even evaluate correctly.
+      defaultText = literalExpression "pkgs.linuxPackages";
+      example = literalExpression "pkgs.linuxKernel.packages.linux_5_10";
+      description = ''
+        This option allows you to override the Linux kernel used by
+        NixOS.  Since things like external kernel module packages are
+        tied to the kernel you're using, it also overrides those.
+        This option is a function that takes Nixpkgs as an argument
+        (as a convenience), and returns an attribute set containing at
+        the very least an attribute <varname>kernel</varname>.
+        Additional attributes may be needed depending on your
+        configuration.  For instance, if you use the NVIDIA X driver,
+        then it also needs to contain an attribute
+        <varname>nvidia_x11</varname>.
+      '';
+    };
+
+    boot.kernelPatches = mkOption {
+      type = types.listOf types.attrs;
+      default = [];
+      example = literalExpression "[ pkgs.kernelPatches.ubuntu_fan_4_4 ]";
+      description = "A list of additional patches to apply to the kernel.";
+    };
+
+    boot.kernel.randstructSeed = mkOption {
+      type = types.str;
+      default = "";
+      example = "my secret seed";
+      description = ''
+        Provides a custom seed for the <varname>RANDSTRUCT</varname> security
+        option of the Linux kernel. Note that <varname>RANDSTRUCT</varname> is
+        only enabled in NixOS hardened kernels. Using a custom seed requires
+        building the kernel and dependent packages locally, since this
+        customization happens at build time.
+      '';
+    };
+
+    boot.kernelParams = mkOption {
+      type = types.listOf (types.strMatching ''([^"[:space:]]|"[^"]*")+'' // {
+        name = "kernelParam";
+        description = "string, with spaces inside double quotes";
+      });
+      default = [ ];
+      description = "Parameters added to the kernel command line.";
+    };
+
+    boot.consoleLogLevel = mkOption {
+      type = types.int;
+      default = 4;
+      description = ''
+        The kernel console <literal>loglevel</literal>. All Kernel Messages with a log level smaller
+        than this setting will be printed to the console.
+      '';
+    };
+
+    boot.vesa = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        (Deprecated) This option, if set, activates the VESA 800x600 video
+        mode on boot and disables kernel modesetting. It is equivalent to
+        specifying <literal>[ "vga=0x317" "nomodeset" ]</literal> in the
+        <option>boot.kernelParams</option> option. This option is
+        deprecated as of 2020: Xorg now works better with modesetting, and
+        you might want a different VESA vga setting, anyway.
+      '';
+    };
+
+    boot.extraModulePackages = mkOption {
+      type = types.listOf types.package;
+      default = [];
+      example = literalExpression "[ config.boot.kernelPackages.nvidia_x11 ]";
+      description = "A list of additional packages supplying kernel modules.";
+    };
+
+    boot.kernelModules = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        The set of kernel modules to be loaded in the second stage of
+        the boot process.  Note that modules that are needed to
+        mount the root file system should be added to
+        <option>boot.initrd.availableKernelModules</option> or
+        <option>boot.initrd.kernelModules</option>.
+      '';
+    };
+
+    boot.initrd.availableKernelModules = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "sata_nv" "ext3" ];
+      description = ''
+        The set of kernel modules in the initial ramdisk used during the
+        boot process.  This set must include all modules necessary for
+        mounting the root device.  That is, it should include modules
+        for the physical device (e.g., SCSI drivers) and for the file
+        system (e.g., ext3).  The set specified here is automatically
+        closed under the module dependency relation, i.e., all
+        dependencies of the modules list here are included
+        automatically.  The modules listed here are available in the
+        initrd, but are only loaded on demand (e.g., the ext3 module is
+        loaded automatically when an ext3 filesystem is mounted, and
+        modules for PCI devices are loaded when they match the PCI ID
+        of a device in your system).  To force a module to be loaded,
+        include it in <option>boot.initrd.kernelModules</option>.
+      '';
+    };
+
+    boot.initrd.kernelModules = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = "List of modules that are always loaded by the initrd.";
+    };
+
+    boot.initrd.includeDefaultModules = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        This option, if set, adds a collection of default kernel modules
+        to <option>boot.initrd.availableKernelModules</option> and
+        <option>boot.initrd.kernelModules</option>.
+      '';
+    };
+
+    system.modulesTree = mkOption {
+      type = types.listOf types.path;
+      internal = true;
+      default = [];
+      description = ''
+        Tree of kernel modules.  This includes the kernel, plus modules
+        built outside of the kernel.  Combine these into a single tree of
+        symlinks because modprobe only supports one directory.
+      '';
+      # Convert the list of path to only one path.
+      apply = pkgs.aggregateModules;
+    };
+
+    system.requiredKernelConfig = mkOption {
+      default = [];
+      example = literalExpression ''
+        with config.lib.kernelConfig; [
+          (isYes "MODULES")
+          (isEnabled "FB_CON_DECOR")
+          (isEnabled "BLK_DEV_INITRD")
+        ]
+      '';
+      internal = true;
+      type = types.listOf types.attrs;
+      description = ''
+        This option allows modules to specify the kernel config options that
+        must be set (or unset) for the module to work. Please use the
+        lib.kernelConfig functions to build list elements.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkMerge
+    [ (mkIf config.boot.initrd.enable {
+        boot.initrd.availableKernelModules =
+          optionals config.boot.initrd.includeDefaultModules ([
+            # Note: most of these (especially the SATA/PATA modules)
+            # shouldn't be included by default since nixos-generate-config
+            # detects them, but I'm keeping them for now for backwards
+            # compatibility.
+
+            # Some SATA/PATA stuff.
+            "ahci"
+            "sata_nv"
+            "sata_via"
+            "sata_sis"
+            "sata_uli"
+            "ata_piix"
+            "pata_marvell"
+
+            # Standard SCSI stuff.
+            "sd_mod"
+            "sr_mod"
+
+            # SD cards and internal eMMC drives.
+            "mmc_block"
+
+            # Support USB keyboards, in case the boot fails and we only have
+            # a USB keyboard, or for LUKS passphrase prompt.
+            "uhci_hcd"
+            "ehci_hcd"
+            "ehci_pci"
+            "ohci_hcd"
+            "ohci_pci"
+            "xhci_hcd"
+            "xhci_pci"
+            "usbhid"
+            "hid_generic" "hid_lenovo" "hid_apple" "hid_roccat"
+            "hid_logitech_hidpp" "hid_logitech_dj" "hid_microsoft"
+
+          ] ++ optionals pkgs.stdenv.hostPlatform.isx86 [
+            # Misc. x86 keyboard stuff.
+            "pcips2" "atkbd" "i8042"
+
+            # x86 RTC needed by the stage 2 init script.
+            "rtc_cmos"
+          ]);
+
+        boot.initrd.kernelModules =
+          optionals config.boot.initrd.includeDefaultModules [
+            # For LVM.
+            "dm_mod"
+          ];
+      })
+
+      (mkIf (!config.boot.isContainer) {
+        system.build = { inherit kernel; };
+
+        system.modulesTree = [ kernel ] ++ config.boot.extraModulePackages;
+
+        # Implement consoleLogLevel both in early boot and using sysctl
+        # (so you don't need to reboot to have changes take effect).
+        boot.kernelParams =
+          [ "loglevel=${toString config.boot.consoleLogLevel}" ] ++
+          optionals config.boot.vesa [ "vga=0x317" "nomodeset" ];
+
+        boot.kernel.sysctl."kernel.printk" = mkDefault config.boot.consoleLogLevel;
+
+        boot.kernelModules = [ "loop" "atkbd" ];
+
+        # The Linux kernel >= 2.6.27 provides firmware.
+        hardware.firmware = [ kernel ];
+
+        # Create /etc/modules-load.d/nixos.conf, which is read by
+        # systemd-modules-load.service to load required kernel modules.
+        environment.etc =
+          { "modules-load.d/nixos.conf".source = kernelModulesConf;
+          };
+
+        systemd.services.systemd-modules-load =
+          { wantedBy = [ "multi-user.target" ];
+            restartTriggers = [ kernelModulesConf ];
+            serviceConfig =
+              { # Ignore failed module loads.  Typically some of the
+                # modules in ‘boot.kernelModules’ are "nice to have but
+                # not required" (e.g. acpi-cpufreq), so we don't want to
+                # barf on those.
+                SuccessExitStatus = "0 1";
+              };
+          };
+
+        lib.kernelConfig = {
+          isYes = option: {
+            assertion = config: config.isYes option;
+            message = "CONFIG_${option} is not yes!";
+            configLine = "CONFIG_${option}=y";
+          };
+
+          isNo = option: {
+            assertion = config: config.isNo option;
+            message = "CONFIG_${option} is not no!";
+            configLine = "CONFIG_${option}=n";
+          };
+
+          isModule = option: {
+            assertion = config: config.isModule option;
+            message = "CONFIG_${option} is not built as a module!";
+            configLine = "CONFIG_${option}=m";
+          };
+
+          ### Usually you will just want to use these two
+          # True if yes or module
+          isEnabled = option: {
+            assertion = config: config.isEnabled option;
+            message = "CONFIG_${option} is not enabled!";
+            configLine = "CONFIG_${option}=y";
+          };
+
+          # True if no or omitted
+          isDisabled = option: {
+            assertion = config: config.isDisabled option;
+            message = "CONFIG_${option} is not disabled!";
+            configLine = "CONFIG_${option}=n";
+          };
+        };
+
+        # The config options that all modules can depend upon
+        system.requiredKernelConfig = with config.lib.kernelConfig;
+          [
+            # !!! Should this really be needed?
+            (isYes "MODULES")
+            (isYes "BINFMT_ELF")
+          ] ++ (optional (randstructSeed != "") (isYes "GCC_PLUGIN_RANDSTRUCT"));
+
+        # nixpkgs kernels are assumed to have all required features
+        assertions = if config.boot.kernelPackages.kernel ? features then [] else
+          let cfg = config.boot.kernelPackages.kernel.config; in map (attrs:
+            { assertion = attrs.assertion cfg; inherit (attrs) message; }
+          ) config.system.requiredKernelConfig;
+
+      })
+
+    ];
+
+}
diff --git a/nixos/modules/system/boot/kernel_config.nix b/nixos/modules/system/boot/kernel_config.nix
new file mode 100644
index 00000000000..495fe74bc21
--- /dev/null
+++ b/nixos/modules/system/boot/kernel_config.nix
@@ -0,0 +1,117 @@
+{ lib, config, ... }:
+
+with lib;
+let
+  mergeFalseByDefault = locs: defs:
+    if defs == [] then abort "This case should never happen."
+    else if any (x: x == false) (getValues defs) then false
+    else true;
+
+  kernelItem = types.submodule {
+    options = {
+      tristate = mkOption {
+        type = types.enum [ "y" "m" "n" null ];
+        default = null;
+        internal = true;
+        visible = true;
+        description = ''
+          Use this field for tristate kernel options expecting a "y" or "m" or "n".
+        '';
+      };
+
+      freeform = mkOption {
+        type = types.nullOr types.str // {
+          merge = mergeEqualOption;
+        };
+        default = null;
+        example = ''MMC_BLOCK_MINORS.freeform = "32";'';
+        description = ''
+          Freeform description of a kernel configuration item value.
+        '';
+      };
+
+      optional = mkOption {
+        type = types.bool // { merge = mergeFalseByDefault; };
+        default = false;
+        description = ''
+          Whether option should generate a failure when unused.
+          Upon merging values, mandatory wins over optional.
+        '';
+      };
+    };
+  };
+
+  mkValue = with lib; val:
+  let
+    isNumber = c: elem c ["0" "1" "2" "3" "4" "5" "6" "7" "8" "9"];
+
+  in
+    if (val == "") then "\"\""
+    else if val == "y" || val == "m" || val == "n" then val
+    else if all isNumber (stringToCharacters val) then val
+    else if substring 0 2 val == "0x" then val
+    else val; # FIXME: fix quoting one day
+
+
+  # generate nix intermediate kernel config file of the form
+  #
+  #       VIRTIO_MMIO m
+  #       VIRTIO_BLK y
+  #       VIRTIO_CONSOLE n
+  #       NET_9P_VIRTIO? y
+  #
+  # Borrowed from copumpkin https://github.com/NixOS/nixpkgs/pull/12158
+  # returns a string, expr should be an attribute set
+  # Use mkValuePreprocess to preprocess option values, aka mark 'modules' as 'yes' or vice-versa
+  # use the identity if you don't want to override the configured values
+  generateNixKConf = exprs:
+  let
+    mkConfigLine = key: item:
+      let
+        val = if item.freeform != null then item.freeform else item.tristate;
+      in
+        if val == null
+          then ""
+          else if (item.optional)
+            then "${key}? ${mkValue val}\n"
+            else "${key} ${mkValue val}\n";
+
+    mkConf = cfg: concatStrings (mapAttrsToList mkConfigLine cfg);
+  in mkConf exprs;
+
+in
+{
+
+  options = {
+
+    intermediateNixConfig = mkOption {
+      readOnly = true;
+      type = types.lines;
+      example = ''
+        USB? y
+        DEBUG n
+      '';
+      description = ''
+        The result of converting the structured kernel configuration in settings
+        to an intermediate string that can be parsed by generate-config.pl to
+        answer the kernel `make defconfig`.
+      '';
+    };
+
+    settings = mkOption {
+      type = types.attrsOf kernelItem;
+      example = literalExpression '' with lib.kernel; {
+        "9P_NET" = yes;
+        USB = option yes;
+        MMC_BLOCK_MINORS = freeform "32";
+      }'';
+      description = ''
+        Structured kernel configuration.
+      '';
+    };
+  };
+
+  config = {
+    intermediateNixConfig = generateNixKConf config.settings;
+  };
+}
diff --git a/nixos/modules/system/boot/kexec.nix b/nixos/modules/system/boot/kexec.nix
new file mode 100644
index 00000000000..02c2713ede1
--- /dev/null
+++ b/nixos/modules/system/boot/kexec.nix
@@ -0,0 +1,32 @@
+{ pkgs, lib, ... }:
+
+{
+  config = lib.mkIf (lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.kexec-tools) {
+    environment.systemPackages = [ pkgs.kexec-tools ];
+
+    systemd.services.prepare-kexec =
+      { description = "Preparation for kexec";
+        wantedBy = [ "kexec.target" ];
+        before = [ "systemd-kexec.service" ];
+        unitConfig.DefaultDependencies = false;
+        serviceConfig.Type = "oneshot";
+        path = [ pkgs.kexec-tools ];
+        script =
+          ''
+            # Don't load the current system profile if we already have a kernel loaded
+            if [[ 1 = "$(</sys/kernel/kexec_loaded)" ]] ; then
+              echo "kexec kernel has already been loaded, prepare-kexec skipped"
+              exit 0
+            fi
+
+            p=$(readlink -f /nix/var/nix/profiles/system)
+            if ! [[ -d $p ]]; then
+              echo "Could not find system profile for prepare-kexec"
+              exit 1
+            fi
+            echo "Loading NixOS system via kexec."
+            exec kexec --load $p/kernel --initrd=$p/initrd --append="$(cat $p/kernel-params) init=$p/init"
+          '';
+      };
+  };
+}
diff --git a/nixos/modules/system/boot/loader/efi.nix b/nixos/modules/system/boot/loader/efi.nix
new file mode 100644
index 00000000000..6043c904c45
--- /dev/null
+++ b/nixos/modules/system/boot/loader/efi.nix
@@ -0,0 +1,20 @@
+{ lib, ... }:
+
+with lib;
+
+{
+  options.boot.loader.efi = {
+
+    canTouchEfiVariables = mkOption {
+      default = false;
+      type = types.bool;
+      description = "Whether the installation process is allowed to modify EFI boot variables.";
+    };
+
+    efiSysMountPoint = mkOption {
+      default = "/boot";
+      type = types.str;
+      description = "Where the EFI System Partition is mounted.";
+    };
+  };
+}
diff --git a/nixos/modules/system/boot/loader/generations-dir/generations-dir-builder.sh b/nixos/modules/system/boot/loader/generations-dir/generations-dir-builder.sh
new file mode 100644
index 00000000000..8ae23dc988c
--- /dev/null
+++ b/nixos/modules/system/boot/loader/generations-dir/generations-dir-builder.sh
@@ -0,0 +1,106 @@
+#! @bash@/bin/sh -e
+
+shopt -s nullglob
+
+export PATH=/empty
+for i in @path@; do PATH=$PATH:$i/bin; done
+
+default=$1
+if test -z "$1"; then
+    echo "Syntax: generations-dir-builder.sh <DEFAULT-CONFIG>"
+    exit 1
+fi
+
+echo "updating the boot generations directory..."
+
+mkdir -p /boot
+
+rm -Rf /boot/system* || true
+
+target=/boot/grub/menu.lst
+tmp=$target.tmp
+
+# Convert a path to a file in the Nix store such as
+# /nix/store/<hash>-<name>/file to <hash>-<name>-<file>.
+cleanName() {
+    local path="$1"
+    echo "$path" | sed 's|^/nix/store/||' | sed 's|/|-|g'
+}
+
+# Copy a file from the Nix store to /boot/kernels.
+declare -A filesCopied
+
+copyToKernelsDir() {
+    local src="$1"
+    local dst="/boot/kernels/$(cleanName $src)"
+    # Don't copy the file if $dst already exists.  This means that we
+    # have to create $dst atomically to prevent partially copied
+    # kernels or initrd if this script is ever interrupted.
+    if ! test -e $dst; then
+        local dstTmp=$dst.tmp.$$
+        cp $src $dstTmp
+        mv $dstTmp $dst
+    fi
+    filesCopied[$dst]=1
+    result=$dst
+}
+
+
+# Copy its kernel and initrd to /boot/kernels.
+addEntry() {
+    local path="$1"
+    local generation="$2"
+    local outdir=/boot/system-$generation
+
+    if ! test -e $path/kernel -a -e $path/initrd; then
+        return
+    fi
+
+    local kernel=$(readlink -f $path/kernel)
+    local initrd=$(readlink -f $path/initrd)
+
+    if test -n "@copyKernels@"; then
+        copyToKernelsDir $kernel; kernel=$result
+        copyToKernelsDir $initrd; initrd=$result
+    fi
+
+    mkdir -p $outdir
+    ln -sf $(readlink -f $path) $outdir/system
+    ln -sf $(readlink -f $path/init) $outdir/init
+    ln -sf $initrd $outdir/initrd
+    ln -sf $kernel $outdir/kernel
+
+    if test $(readlink -f "$path") = "$default"; then
+      cp "$kernel" /boot/nixos-kernel
+      cp "$initrd" /boot/nixos-initrd
+      cp "$(readlink -f "$path/init")" /boot/nixos-init
+
+      mkdir -p /boot/default
+      # ln -sfT: overrides target even if it exists.
+      ln -sfT $(readlink -f $path) /boot/default/system
+      ln -sfT $(readlink -f $path/init) /boot/default/init
+      ln -sfT $initrd /boot/default/initrd
+      ln -sfT $kernel /boot/default/kernel
+    fi
+}
+
+if test -n "@copyKernels@"; then
+    mkdir -p /boot/kernels
+fi
+
+# Add all generations of the system profile to the menu, in reverse
+# (most recent to least recent) order.
+for generation in $(
+    (cd /nix/var/nix/profiles && ls -d system-*-link) \
+    | sed 's/system-\([0-9]\+\)-link/\1/' \
+    | sort -n -r); do
+    link=/nix/var/nix/profiles/system-$generation-link
+    addEntry $link $generation
+done
+
+# Remove obsolete files from /boot/kernels.
+for fn in /boot/kernels/*; do
+    if ! test "${filesCopied[$fn]}" = 1; then
+        rm -vf -- "$fn"
+    fi
+done
diff --git a/nixos/modules/system/boot/loader/generations-dir/generations-dir.nix b/nixos/modules/system/boot/loader/generations-dir/generations-dir.nix
new file mode 100644
index 00000000000..1437ab38770
--- /dev/null
+++ b/nixos/modules/system/boot/loader/generations-dir/generations-dir.nix
@@ -0,0 +1,62 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  generationsDirBuilder = pkgs.substituteAll {
+    src = ./generations-dir-builder.sh;
+    isExecutable = true;
+    inherit (pkgs) bash;
+    path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep];
+    inherit (config.boot.loader.generationsDir) copyKernels;
+  };
+
+in
+
+{
+  options = {
+
+    boot.loader.generationsDir = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to create symlinks to the system generations under
+          <literal>/boot</literal>.  When enabled,
+          <literal>/boot/default/kernel</literal>,
+          <literal>/boot/default/initrd</literal>, etc., are updated to
+          point to the current generation's kernel image, initial RAM
+          disk, and other bootstrap files.
+
+          This optional is not necessary with boot loaders such as GNU GRUB
+          for which the menu is updated to point to the latest bootstrap
+          files.  However, it is needed for U-Boot on platforms where the
+          boot command line is stored in flash memory rather than in a
+          menu file.
+        '';
+      };
+
+      copyKernels = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether copy the necessary boot files into /boot, so
+          /nix/store is not needed by the boot loader.
+        '';
+      };
+
+    };
+
+  };
+
+
+  config = mkIf config.boot.loader.generationsDir.enable {
+
+    system.build.installBootLoader = generationsDirBuilder;
+    system.boot.loader.id = "generationsDir";
+    system.boot.loader.kernelFile = pkgs.stdenv.hostPlatform.linux-kernel.target;
+
+  };
+}
diff --git a/nixos/modules/system/boot/loader/generic-extlinux-compatible/default.nix b/nixos/modules/system/boot/loader/generic-extlinux-compatible/default.nix
new file mode 100644
index 00000000000..545b594674f
--- /dev/null
+++ b/nixos/modules/system/boot/loader/generic-extlinux-compatible/default.nix
@@ -0,0 +1,82 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  blCfg = config.boot.loader;
+  dtCfg = config.hardware.deviceTree;
+  cfg = blCfg.generic-extlinux-compatible;
+
+  timeoutStr = if blCfg.timeout == null then "-1" else toString blCfg.timeout;
+
+  # The builder used to write during system activation
+  builder = import ./extlinux-conf-builder.nix { inherit pkgs; };
+  # The builder exposed in populateCmd, which runs on the build architecture
+  populateBuilder = import ./extlinux-conf-builder.nix { pkgs = pkgs.buildPackages; };
+in
+{
+  options = {
+    boot.loader.generic-extlinux-compatible = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to generate an extlinux-compatible configuration file
+          under <literal>/boot/extlinux.conf</literal>.  For instance,
+          U-Boot's generic distro boot support uses this file format.
+
+          See <link xlink:href="http://git.denx.de/?p=u-boot.git;a=blob;f=doc/README.distro;hb=refs/heads/master">U-boot's documentation</link>
+          for more information.
+        '';
+      };
+
+      useGenerationDeviceTree = mkOption {
+        default = true;
+        type = types.bool;
+        description = ''
+          Whether to generate Device Tree-related directives in the
+          extlinux configuration.
+
+          When enabled, the bootloader will attempt to load the device
+          tree binaries from the generation's kernel.
+
+          Note that this affects all generations, regardless of the
+          setting value used in their configurations.
+        '';
+      };
+
+      configurationLimit = mkOption {
+        default = 20;
+        example = 10;
+        type = types.int;
+        description = ''
+          Maximum number of configurations in the boot menu.
+        '';
+      };
+
+      populateCmd = mkOption {
+        type = types.str;
+        readOnly = true;
+        description = ''
+          Contains the builder command used to populate an image,
+          honoring all options except the <literal>-c &lt;path-to-default-configuration&gt;</literal>
+          argument.
+          Useful to have for sdImage.populateRootCommands
+        '';
+      };
+
+    };
+  };
+
+  config = let
+    builderArgs = "-g ${toString cfg.configurationLimit} -t ${timeoutStr}"
+      + lib.optionalString (dtCfg.name != null) " -n ${dtCfg.name}"
+      + lib.optionalString (!cfg.useGenerationDeviceTree) " -r";
+  in
+    mkIf cfg.enable {
+      system.build.installBootLoader = "${builder} ${builderArgs} -c";
+      system.boot.loader.id = "generic-extlinux-compatible";
+
+      boot.loader.generic-extlinux-compatible.populateCmd = "${populateBuilder} ${builderArgs}";
+    };
+}
diff --git a/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.nix b/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.nix
new file mode 100644
index 00000000000..576a07c1d27
--- /dev/null
+++ b/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.nix
@@ -0,0 +1,8 @@
+{ pkgs }:
+
+pkgs.substituteAll {
+  src = ./extlinux-conf-builder.sh;
+  isExecutable = true;
+  path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep];
+  inherit (pkgs) bash;
+}
diff --git a/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh b/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh
new file mode 100644
index 00000000000..1a0da005029
--- /dev/null
+++ b/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh
@@ -0,0 +1,157 @@
+#! @bash@/bin/sh -e
+
+shopt -s nullglob
+
+export PATH=/empty
+for i in @path@; do PATH=$PATH:$i/bin; done
+
+usage() {
+    echo "usage: $0 -t <timeout> -c <path-to-default-configuration> [-d <boot-dir>] [-g <num-generations>] [-n <dtbName>] [-r]" >&2
+    exit 1
+}
+
+timeout=                # Timeout in centiseconds
+default=                # Default configuration
+target=/boot            # Target directory
+numGenerations=0        # Number of other generations to include in the menu
+
+while getopts "t:c:d:g:n:r" opt; do
+    case "$opt" in
+        t) # U-Boot interprets '0' as infinite and negative as instant boot
+            if [ "$OPTARG" -lt 0 ]; then
+                timeout=0
+            elif [ "$OPTARG" = 0 ]; then
+                timeout=-10
+            else
+                timeout=$((OPTARG * 10))
+            fi
+            ;;
+        c) default="$OPTARG" ;;
+        d) target="$OPTARG" ;;
+        g) numGenerations="$OPTARG" ;;
+        n) dtbName="$OPTARG" ;;
+        r) noDeviceTree=1 ;;
+        \?) usage ;;
+    esac
+done
+
+[ "$timeout" = "" -o "$default" = "" ] && usage
+
+mkdir -p $target/nixos
+mkdir -p $target/extlinux
+
+# Convert a path to a file in the Nix store such as
+# /nix/store/<hash>-<name>/file to <hash>-<name>-<file>.
+cleanName() {
+    local path="$1"
+    echo "$path" | sed 's|^/nix/store/||' | sed 's|/|-|g'
+}
+
+# Copy a file from the Nix store to $target/nixos.
+declare -A filesCopied
+
+copyToKernelsDir() {
+    local src=$(readlink -f "$1")
+    local dst="$target/nixos/$(cleanName $src)"
+    # Don't copy the file if $dst already exists.  This means that we
+    # have to create $dst atomically to prevent partially copied
+    # kernels or initrd if this script is ever interrupted.
+    if ! test -e $dst; then
+        local dstTmp=$dst.tmp.$$
+        cp -r $src $dstTmp
+        mv $dstTmp $dst
+    fi
+    filesCopied[$dst]=1
+    result=$dst
+}
+
+# Copy its kernel, initrd and dtbs to $target/nixos, and echo out an
+# extlinux menu entry
+addEntry() {
+    local path=$(readlink -f "$1")
+    local tag="$2" # Generation number or 'default'
+
+    if ! test -e $path/kernel -a -e $path/initrd; then
+        return
+    fi
+
+    copyToKernelsDir "$path/kernel"; kernel=$result
+    copyToKernelsDir "$path/initrd"; initrd=$result
+    dtbDir=$(readlink -m "$path/dtbs")
+    if [ -e "$dtbDir" ]; then
+        copyToKernelsDir "$dtbDir"; dtbs=$result
+    fi
+
+    timestampEpoch=$(stat -L -c '%Z' $path)
+
+    timestamp=$(date "+%Y-%m-%d %H:%M" -d @$timestampEpoch)
+    nixosLabel="$(cat $path/nixos-version)"
+    extraParams="$(cat $path/kernel-params)"
+
+    echo
+    echo "LABEL nixos-$tag"
+    if [ "$tag" = "default" ]; then
+        echo "  MENU LABEL NixOS - Default"
+    else
+        echo "  MENU LABEL NixOS - Configuration $tag ($timestamp - $nixosLabel)"
+    fi
+    echo "  LINUX ../nixos/$(basename $kernel)"
+    echo "  INITRD ../nixos/$(basename $initrd)"
+    echo "  APPEND init=$path/init $extraParams"
+
+    if [ -n "$noDeviceTree" ]; then
+        return
+    fi
+
+    if [ -d "$dtbDir" ]; then
+        # if a dtbName was specified explicitly, use that, else use FDTDIR
+        if [ -n "$dtbName" ]; then
+            echo "  FDT ../nixos/$(basename $dtbs)/${dtbName}"
+        else
+            echo "  FDTDIR ../nixos/$(basename $dtbs)"
+        fi
+    else
+        if [ -n "$dtbName" ]; then
+            echo "Explicitly requested dtbName $dtbName, but there's no FDTDIR - bailing out." >&2
+            exit 1
+        fi
+    fi
+}
+
+tmpFile="$target/extlinux/extlinux.conf.tmp.$$"
+
+cat > $tmpFile <<EOF
+# Generated file, all changes will be lost on nixos-rebuild!
+
+# Change this to e.g. nixos-42 to temporarily boot to an older configuration.
+DEFAULT nixos-default
+
+MENU TITLE ------------------------------------------------------------
+TIMEOUT $timeout
+EOF
+
+addEntry $default default >> $tmpFile
+
+if [ "$numGenerations" -gt 0 ]; then
+    # Add up to $numGenerations generations of the system profile to the menu,
+    # in reverse (most recent to least recent) order.
+    for generation in $(
+            (cd /nix/var/nix/profiles && ls -d system-*-link) \
+            | sed 's/system-\([0-9]\+\)-link/\1/' \
+            | sort -n -r \
+            | head -n $numGenerations); do
+        link=/nix/var/nix/profiles/system-$generation-link
+        addEntry $link $generation
+    done >> $tmpFile
+fi
+
+mv -f $tmpFile $target/extlinux/extlinux.conf
+
+# Remove obsolete files from $target/nixos.
+for fn in $target/nixos/*; do
+    if ! test "${filesCopied[$fn]}" = 1; then
+        echo "Removing no longer needed boot file: $fn"
+        chmod +w -- "$fn"
+        rm -rf -- "$fn"
+    fi
+done
diff --git a/nixos/modules/system/boot/loader/grub/grub.nix b/nixos/modules/system/boot/loader/grub/grub.nix
new file mode 100644
index 00000000000..8db271f8713
--- /dev/null
+++ b/nixos/modules/system/boot/loader/grub/grub.nix
@@ -0,0 +1,848 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.boot.loader.grub;
+
+  efi = config.boot.loader.efi;
+
+  grubPkgs =
+    # Package set of targeted architecture
+    if cfg.forcei686 then pkgs.pkgsi686Linux else pkgs;
+
+  realGrub = if cfg.version == 1 then grubPkgs.grub
+    else if cfg.zfsSupport then grubPkgs.grub2.override { zfsSupport = true; }
+    else if cfg.trustedBoot.enable
+         then if cfg.trustedBoot.isHPLaptop
+              then grubPkgs.trustedGrub-for-HP
+              else grubPkgs.trustedGrub
+         else grubPkgs.grub2;
+
+  grub =
+    # Don't include GRUB if we're only generating a GRUB menu (e.g.,
+    # in EC2 instances).
+    if cfg.devices == ["nodev"]
+    then null
+    else realGrub;
+
+  grubEfi =
+    # EFI version of Grub v2
+    if cfg.efiSupport && (cfg.version == 2)
+    then realGrub.override { efiSupport = cfg.efiSupport; }
+    else null;
+
+  f = x: if x == null then "" else "" + x;
+
+  grubConfig = args:
+    let
+      efiSysMountPoint = if args.efiSysMountPoint == null then args.path else args.efiSysMountPoint;
+      efiSysMountPoint' = replaceChars [ "/" ] [ "-" ] efiSysMountPoint;
+    in
+    pkgs.writeText "grub-config.xml" (builtins.toXML
+    { splashImage = f cfg.splashImage;
+      splashMode = f cfg.splashMode;
+      backgroundColor = f cfg.backgroundColor;
+      grub = f grub;
+      grubTarget = f (grub.grubTarget or "");
+      shell = "${pkgs.runtimeShell}";
+      fullName = lib.getName realGrub;
+      fullVersion = lib.getVersion realGrub;
+      grubEfi = f grubEfi;
+      grubTargetEfi = if cfg.efiSupport && (cfg.version == 2) then f (grubEfi.grubTarget or "") else "";
+      bootPath = args.path;
+      storePath = config.boot.loader.grub.storePath;
+      bootloaderId = if args.efiBootloaderId == null then "NixOS${efiSysMountPoint'}" else args.efiBootloaderId;
+      timeout = if config.boot.loader.timeout == null then -1 else config.boot.loader.timeout;
+      users = if cfg.users == {} || cfg.version != 1 then cfg.users else throw "GRUB version 1 does not support user accounts.";
+      theme = f cfg.theme;
+      inherit efiSysMountPoint;
+      inherit (args) devices;
+      inherit (efi) canTouchEfiVariables;
+      inherit (cfg)
+        version extraConfig extraPerEntryConfig extraEntries forceInstall useOSProber
+        extraGrubInstallArgs
+        extraEntriesBeforeNixOS extraPrepareConfig configurationLimit copyKernels
+        default fsIdentifier efiSupport efiInstallAsRemovable gfxmodeEfi gfxmodeBios gfxpayloadEfi gfxpayloadBios;
+      path = with pkgs; makeBinPath (
+        [ coreutils gnused gnugrep findutils diffutils btrfs-progs util-linux mdadm ]
+        ++ optional (cfg.efiSupport && (cfg.version == 2)) efibootmgr
+        ++ optionals cfg.useOSProber [ busybox os-prober ]);
+      font = if cfg.font == null then ""
+        else (if lib.last (lib.splitString "." cfg.font) == "pf2"
+             then cfg.font
+             else "${convertedFont}");
+    });
+
+  bootDeviceCounters = foldr (device: attr: attr // { ${device} = (attr.${device} or 0) + 1; }) {}
+    (concatMap (args: args.devices) cfg.mirroredBoots);
+
+  convertedFont = (pkgs.runCommand "grub-font-converted.pf2" {}
+           (builtins.concatStringsSep " "
+             ([ "${realGrub}/bin/grub-mkfont"
+               cfg.font
+               "--output" "$out"
+             ] ++ (optional (cfg.fontSize!=null) "--size ${toString cfg.fontSize}")))
+         );
+
+  defaultSplash = pkgs.nixos-artwork.wallpapers.simple-dark-gray-bootloader.gnomeFilePath;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    boot.loader.grub = {
+
+      enable = mkOption {
+        default = !config.boot.isContainer;
+        defaultText = literalExpression "!config.boot.isContainer";
+        type = types.bool;
+        description = ''
+          Whether to enable the GNU GRUB boot loader.
+        '';
+      };
+
+      version = mkOption {
+        default = 2;
+        example = 1;
+        type = types.int;
+        description = ''
+          The version of GRUB to use: <literal>1</literal> for GRUB
+          Legacy (versions 0.9x), or <literal>2</literal> (the
+          default) for GRUB 2.
+        '';
+      };
+
+      device = mkOption {
+        default = "";
+        example = "/dev/disk/by-id/wwn-0x500001234567890a";
+        type = types.str;
+        description = ''
+          The device on which the GRUB boot loader will be installed.
+          The special value <literal>nodev</literal> means that a GRUB
+          boot menu will be generated, but GRUB itself will not
+          actually be installed.  To install GRUB on multiple devices,
+          use <literal>boot.loader.grub.devices</literal>.
+        '';
+      };
+
+      devices = mkOption {
+        default = [];
+        example = [ "/dev/disk/by-id/wwn-0x500001234567890a" ];
+        type = types.listOf types.str;
+        description = ''
+          The devices on which the boot loader, GRUB, will be
+          installed. Can be used instead of <literal>device</literal> to
+          install GRUB onto multiple devices.
+        '';
+      };
+
+      users = mkOption {
+        default = {};
+        example = {
+          root = { hashedPasswordFile = "/path/to/file"; };
+        };
+        description = ''
+          User accounts for GRUB. When specified, the GRUB command line and
+          all boot options except the default are password-protected.
+          All passwords and hashes provided will be stored in /boot/grub/grub.cfg,
+          and will be visible to any local user who can read this file. Additionally,
+          any passwords and hashes provided directly in a Nix configuration
+          (as opposed to external files) will be copied into the Nix store, and
+          will be visible to all local users.
+        '';
+        type = with types; attrsOf (submodule {
+          options = {
+            hashedPasswordFile = mkOption {
+              example = "/path/to/file";
+              default = null;
+              type = with types; uniq (nullOr str);
+              description = ''
+                Specifies the path to a file containing the password hash
+                for the account, generated with grub-mkpasswd-pbkdf2.
+                This hash will be stored in /boot/grub/grub.cfg, and will
+                be visible to any local user who can read this file.
+              '';
+            };
+            hashedPassword = mkOption {
+              example = "grub.pbkdf2.sha512.10000.674DFFDEF76E13EA...2CC972B102CF4355";
+              default = null;
+              type = with types; uniq (nullOr str);
+              description = ''
+                Specifies the password hash for the account,
+                generated with grub-mkpasswd-pbkdf2.
+                This hash will be copied to the Nix store, and will be visible to all local users.
+              '';
+            };
+            passwordFile = mkOption {
+              example = "/path/to/file";
+              default = null;
+              type = with types; uniq (nullOr str);
+              description = ''
+                Specifies the path to a file containing the
+                clear text password for the account.
+                This password will be stored in /boot/grub/grub.cfg, and will
+                be visible to any local user who can read this file.
+              '';
+            };
+            password = mkOption {
+              example = "Pa$$w0rd!";
+              default = null;
+              type = with types; uniq (nullOr str);
+              description = ''
+                Specifies the clear text password for the account.
+                This password will be copied to the Nix store, and will be visible to all local users.
+              '';
+            };
+          };
+        });
+      };
+
+      mirroredBoots = mkOption {
+        default = [ ];
+        example = [
+          { path = "/boot1"; devices = [ "/dev/disk/by-id/wwn-0x500001234567890a" ]; }
+          { path = "/boot2"; devices = [ "/dev/disk/by-id/wwn-0x500009876543210a" ]; }
+        ];
+        description = ''
+          Mirror the boot configuration to multiple partitions and install grub
+          to the respective devices corresponding to those partitions.
+        '';
+
+        type = with types; listOf (submodule {
+          options = {
+
+            path = mkOption {
+              example = "/boot1";
+              type = types.str;
+              description = ''
+                The path to the boot directory where GRUB will be written. Generally
+                this boot path should double as an EFI path.
+              '';
+            };
+
+            efiSysMountPoint = mkOption {
+              default = null;
+              example = "/boot1/efi";
+              type = types.nullOr types.str;
+              description = ''
+                The path to the efi system mount point. Usually this is the same
+                partition as the above path and can be left as null.
+              '';
+            };
+
+            efiBootloaderId = mkOption {
+              default = null;
+              example = "NixOS-fsid";
+              type = types.nullOr types.str;
+              description = ''
+                The id of the bootloader to store in efi nvram.
+                The default is to name it NixOS and append the path or efiSysMountPoint.
+                This is only used if <literal>boot.loader.efi.canTouchEfiVariables</literal> is true.
+              '';
+            };
+
+            devices = mkOption {
+              default = [ ];
+              example = [ "/dev/disk/by-id/wwn-0x500001234567890a" "/dev/disk/by-id/wwn-0x500009876543210a" ];
+              type = types.listOf types.str;
+              description = ''
+                The path to the devices which will have the GRUB MBR written.
+                Note these are typically device paths and not paths to partitions.
+              '';
+            };
+
+          };
+        });
+      };
+
+      configurationName = mkOption {
+        default = "";
+        example = "Stable 2.6.21";
+        type = types.str;
+        description = ''
+          GRUB entry name instead of default.
+        '';
+      };
+
+      storePath = mkOption {
+        default = "/nix/store";
+        type = types.str;
+        description = ''
+          Path to the Nix store when looking for kernels at boot.
+          Only makes sense when copyKernels is false.
+        '';
+      };
+
+      extraPrepareConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Additional bash commands to be run at the script that
+          prepares the GRUB menu entries.
+        '';
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        example = ''
+          serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1
+          terminal_input --append serial
+          terminal_output --append serial
+        '';
+        type = types.lines;
+        description = ''
+          Additional GRUB commands inserted in the configuration file
+          just before the menu entries.
+        '';
+      };
+
+      extraGrubInstallArgs = mkOption {
+        default = [ ];
+        example = [ "--modules=nativedisk ahci pata part_gpt part_msdos diskfilter mdraid1x lvm ext2" ];
+        type = types.listOf types.str;
+        description = ''
+          Additional arguments passed to <literal>grub-install</literal>.
+
+          A use case for this is to build specific GRUB2 modules
+          directly into the GRUB2 kernel image, so that they are available
+          and activated even in the <literal>grub rescue</literal> shell.
+
+          They are also necessary when the BIOS/UEFI is bugged and cannot
+          correctly read large disks (e.g. above 2 TB), so GRUB2's own
+          <literal>nativedisk</literal> and related modules can be used
+          to use its own disk drivers. The example shows one such case.
+          This is also useful for booting from USB.
+          See the
+          <link xlink:href="http://git.savannah.gnu.org/cgit/grub.git/tree/grub-core/commands/nativedisk.c?h=grub-2.04#n326">
+          GRUB source code
+          </link>
+          for which disk modules are available.
+
+          The list elements are passed directly as <literal>argv</literal>
+          arguments to the <literal>grub-install</literal> program, in order.
+        '';
+      };
+
+      extraInstallCommands = mkOption {
+        default = "";
+        example = ''
+          # the example below generates detached signatures that GRUB can verify
+          # https://www.gnu.org/software/grub/manual/grub/grub.html#Using-digital-signatures
+          ''${pkgs.findutils}/bin/find /boot -not -path "/boot/efi/*" -type f -name '*.sig' -delete
+          old_gpg_home=$GNUPGHOME
+          export GNUPGHOME="$(mktemp -d)"
+          ''${pkgs.gnupg}/bin/gpg --import ''${priv_key} > /dev/null 2>&1
+          ''${pkgs.findutils}/bin/find /boot -not -path "/boot/efi/*" -type f -exec ''${pkgs.gnupg}/bin/gpg --detach-sign "{}" \; > /dev/null 2>&1
+          rm -rf $GNUPGHOME
+          export GNUPGHOME=$old_gpg_home
+        '';
+        type = types.lines;
+        description = ''
+          Additional shell commands inserted in the bootloader installer
+          script after generating menu entries.
+        '';
+      };
+
+      extraPerEntryConfig = mkOption {
+        default = "";
+        example = "root (hd0)";
+        type = types.lines;
+        description = ''
+          Additional GRUB commands inserted in the configuration file
+          at the start of each NixOS menu entry.
+        '';
+      };
+
+      extraEntries = mkOption {
+        default = "";
+        type = types.lines;
+        example = ''
+          # GRUB 1 example (not GRUB 2 compatible)
+          title Windows
+            chainloader (hd0,1)+1
+
+          # GRUB 2 example
+          menuentry "Windows 7" {
+            chainloader (hd0,4)+1
+          }
+
+          # GRUB 2 with UEFI example, chainloading another distro
+          menuentry "Fedora" {
+            set root=(hd1,1)
+            chainloader /efi/fedora/grubx64.efi
+          }
+        '';
+        description = ''
+          Any additional entries you want added to the GRUB boot menu.
+        '';
+      };
+
+      extraEntriesBeforeNixOS = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether extraEntries are included before the default option.
+        '';
+      };
+
+      extraFiles = mkOption {
+        type = types.attrsOf types.path;
+        default = {};
+        example = literalExpression ''
+          { "memtest.bin" = "''${pkgs.memtest86plus}/memtest.bin"; }
+        '';
+        description = ''
+          A set of files to be copied to <filename>/boot</filename>.
+          Each attribute name denotes the destination file name in
+          <filename>/boot</filename>, while the corresponding
+          attribute value specifies the source file.
+        '';
+      };
+
+      useOSProber = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If set to true, append entries for other OSs detected by os-prober.
+        '';
+      };
+
+      splashImage = mkOption {
+        type = types.nullOr types.path;
+        example = literalExpression "./my-background.png";
+        description = ''
+          Background image used for GRUB.
+          Set to <literal>null</literal> to run GRUB in text mode.
+
+          <note><para>
+          For grub 1:
+          It must be a 640x480,
+          14-colour image in XPM format, optionally compressed with
+          <command>gzip</command> or <command>bzip2</command>.
+          </para></note>
+
+          <note><para>
+          For grub 2:
+          File must be one of .png, .tga, .jpg, or .jpeg. JPEG images must
+          not be progressive.
+          The image will be scaled if necessary to fit the screen.
+          </para></note>
+        '';
+      };
+
+      backgroundColor = mkOption {
+        type = types.nullOr types.str;
+        example = "#7EBAE4";
+        default = null;
+        description = ''
+          Background color to be used for GRUB to fill the areas the image isn't filling.
+
+          <note><para>
+          This options has no effect for GRUB 1.
+          </para></note>
+        '';
+      };
+
+      theme = mkOption {
+        type = types.nullOr types.path;
+        example = literalExpression "pkgs.nixos-grub2-theme";
+        default = null;
+        description = ''
+          Grub theme to be used.
+
+          <note><para>
+          This options has no effect for GRUB 1.
+          </para></note>
+        '';
+      };
+
+      splashMode = mkOption {
+        type = types.enum [ "normal" "stretch" ];
+        default = "stretch";
+        description = ''
+          Whether to stretch the image or show the image in the top-left corner unstretched.
+
+          <note><para>
+          This options has no effect for GRUB 1.
+          </para></note>
+        '';
+      };
+
+      font = mkOption {
+        type = types.nullOr types.path;
+        default = "${realGrub}/share/grub/unicode.pf2";
+        defaultText = literalExpression ''"''${pkgs.grub2}/share/grub/unicode.pf2"'';
+        description = ''
+          Path to a TrueType, OpenType, or pf2 font to be used by Grub.
+        '';
+      };
+
+      fontSize = mkOption {
+        type = types.nullOr types.int;
+        example = 16;
+        default = null;
+        description = ''
+          Font size for the grub menu. Ignored unless <literal>font</literal>
+          is set to a ttf or otf font.
+        '';
+      };
+
+      gfxmodeEfi = mkOption {
+        default = "auto";
+        example = "1024x768";
+        type = types.str;
+        description = ''
+          The gfxmode to pass to GRUB when loading a graphical boot interface under EFI.
+        '';
+      };
+
+      gfxmodeBios = mkOption {
+        default = "1024x768";
+        example = "auto";
+        type = types.str;
+        description = ''
+          The gfxmode to pass to GRUB when loading a graphical boot interface under BIOS.
+        '';
+      };
+
+      gfxpayloadEfi = mkOption {
+        default = "keep";
+        example = "text";
+        type = types.str;
+        description = ''
+          The gfxpayload to pass to GRUB when loading a graphical boot interface under EFI.
+        '';
+      };
+
+      gfxpayloadBios = mkOption {
+        default = "text";
+        example = "keep";
+        type = types.str;
+        description = ''
+          The gfxpayload to pass to GRUB when loading a graphical boot interface under BIOS.
+        '';
+      };
+
+      configurationLimit = mkOption {
+        default = 100;
+        example = 120;
+        type = types.int;
+        description = ''
+          Maximum of configurations in boot menu. GRUB has problems when
+          there are too many entries.
+        '';
+      };
+
+      copyKernels = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether the GRUB menu builder should copy kernels and initial
+          ramdisks to /boot.  This is done automatically if /boot is
+          on a different partition than /.
+        '';
+      };
+
+      default = mkOption {
+        default = "0";
+        type = types.either types.int types.str;
+        apply = toString;
+        description = ''
+          Index of the default menu item to be booted.
+          Can also be set to "saved", which will make GRUB select
+          the menu item that was used at the last boot.
+        '';
+      };
+
+      fsIdentifier = mkOption {
+        default = "uuid";
+        type = types.enum [ "uuid" "label" "provided" ];
+        description = ''
+          Determines how GRUB will identify devices when generating the
+          configuration file. A value of uuid / label signifies that grub
+          will always resolve the uuid or label of the device before using
+          it in the configuration. A value of provided means that GRUB will
+          use the device name as show in <command>df</command> or
+          <command>mount</command>. Note, zfs zpools / datasets are ignored
+          and will always be mounted using their labels.
+        '';
+      };
+
+      zfsSupport = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether GRUB should be built against libzfs.
+          ZFS support is only available for GRUB v2.
+          This option is ignored for GRUB v1.
+        '';
+      };
+
+      efiSupport = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether GRUB should be built with EFI support.
+          EFI support is only available for GRUB v2.
+          This option is ignored for GRUB v1.
+        '';
+      };
+
+      efiInstallAsRemovable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to invoke <literal>grub-install</literal> with
+          <literal>--removable</literal>.</para>
+
+          <para>Unless you turn this on, GRUB will install itself somewhere in
+          <literal>boot.loader.efi.efiSysMountPoint</literal> (exactly where
+          depends on other config variables). If you've set
+          <literal>boot.loader.efi.canTouchEfiVariables</literal> *AND* you
+          are currently booted in UEFI mode, then GRUB will use
+          <literal>efibootmgr</literal> to modify the boot order in the
+          EFI variables of your firmware to include this location. If you are
+          *not* booted in UEFI mode at the time GRUB is being installed, the
+          NVRAM will not be modified, and your system will not find GRUB at
+          boot time. However, GRUB will still return success so you may miss
+          the warning that gets printed ("<literal>efibootmgr: EFI variables
+          are not supported on this system.</literal>").</para>
+
+          <para>If you turn this feature on, GRUB will install itself in a
+          special location within <literal>efiSysMountPoint</literal> (namely
+          <literal>EFI/boot/boot$arch.efi</literal>) which the firmwares
+          are hardcoded to try first, regardless of NVRAM EFI variables.</para>
+
+          <para>To summarize, turn this on if:
+          <itemizedlist>
+            <listitem><para>You are installing NixOS and want it to boot in UEFI mode,
+            but you are currently booted in legacy mode</para></listitem>
+            <listitem><para>You want to make a drive that will boot regardless of
+            the NVRAM state of the computer (like a USB "removable" drive)</para></listitem>
+            <listitem><para>You simply dislike the idea of depending on NVRAM
+            state to make your drive bootable</para></listitem>
+          </itemizedlist>
+        '';
+      };
+
+      enableCryptodisk = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable support for encrypted partitions. GRUB should automatically
+          unlock the correct encrypted partition and look for filesystems.
+        '';
+      };
+
+      forceInstall = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to try and forcibly install GRUB even if problems are
+          detected. It is not recommended to enable this unless you know what
+          you are doing.
+        '';
+      };
+
+      forcei686 = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to force the use of a ia32 boot loader on x64 systems. Required
+          to install and run NixOS on 64bit x86 systems with 32bit (U)EFI.
+        '';
+      };
+
+      trustedBoot = {
+
+        enable = mkOption {
+          default = false;
+          type = types.bool;
+          description = ''
+            Enable trusted boot. GRUB will measure all critical components during
+            the boot process to offer TCG (TPM) support.
+          '';
+        };
+
+        systemHasTPM = mkOption {
+          default = "";
+          example = "YES_TPM_is_activated";
+          type = types.str;
+          description = ''
+            Assertion that the target system has an activated TPM. It is a safety
+            check before allowing the activation of 'trustedBoot.enable'. TrustedBoot
+            WILL FAIL TO BOOT YOUR SYSTEM if no TPM is available.
+          '';
+        };
+
+        isHPLaptop = mkOption {
+          default = false;
+          type = types.bool;
+          description = ''
+            Use a special version of TrustedGRUB that is needed by some HP laptops
+            and works only for the HP laptops.
+          '';
+        };
+
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkMerge [
+
+    { boot.loader.grub.splashImage = mkDefault (
+        if cfg.version == 1 then pkgs.fetchurl {
+          url = "http://www.gnome-look.org/CONTENT/content-files/36909-soft-tux.xpm.gz";
+          sha256 = "14kqdx2lfqvh40h6fjjzqgff1mwk74dmbjvmqphi6azzra7z8d59";
+        }
+        # GRUB 1.97 doesn't support gzipped XPMs.
+        else defaultSplash);
+    }
+
+    (mkIf (cfg.splashImage == defaultSplash) {
+      boot.loader.grub.backgroundColor = mkDefault "#2F302F";
+      boot.loader.grub.splashMode = mkDefault "normal";
+    })
+
+    (mkIf cfg.enable {
+
+      boot.loader.grub.devices = optional (cfg.device != "") cfg.device;
+
+      boot.loader.grub.mirroredBoots = optionals (cfg.devices != [ ]) [
+        { path = "/boot"; inherit (cfg) devices; inherit (efi) efiSysMountPoint; }
+      ];
+
+      boot.loader.supportsInitrdSecrets = true;
+
+      system.build.installBootLoader =
+        let
+          install-grub-pl = pkgs.substituteAll {
+            src = ./install-grub.pl;
+            utillinux = pkgs.util-linux;
+            btrfsprogs = pkgs.btrfs-progs;
+          };
+          perl = pkgs.perl.withPackages (p: with p; [
+            FileSlurp FileCopyRecursive
+            XMLLibXML XMLSAX XMLSAXBase
+            ListCompare JSON
+          ]);
+        in pkgs.writeScript "install-grub.sh" (''
+        #!${pkgs.runtimeShell}
+        set -e
+        ${optionalString cfg.enableCryptodisk "export GRUB_ENABLE_CRYPTODISK=y"}
+      '' + flip concatMapStrings cfg.mirroredBoots (args: ''
+        ${perl}/bin/perl ${install-grub-pl} ${grubConfig args} $@
+      '') + cfg.extraInstallCommands);
+
+      system.build.grub = grub;
+
+      # Common attribute for boot loaders so only one of them can be
+      # set at once.
+      system.boot.loader.id = "grub";
+
+      environment.systemPackages = optional (grub != null) grub;
+
+      boot.loader.grub.extraPrepareConfig =
+        concatStrings (mapAttrsToList (n: v: ''
+          ${pkgs.coreutils}/bin/cp -pf "${v}" "@bootPath@/${n}"
+        '') config.boot.loader.grub.extraFiles);
+
+      assertions = [
+        {
+          assertion = !cfg.zfsSupport || cfg.version == 2;
+          message = "Only GRUB version 2 provides ZFS support";
+        }
+        {
+          assertion = cfg.mirroredBoots != [ ];
+          message = "You must set the option ‘boot.loader.grub.devices’ or "
+            + "'boot.loader.grub.mirroredBoots' to make the system bootable.";
+        }
+        {
+          assertion = cfg.efiSupport || all (c: c < 2) (mapAttrsToList (n: c: if n == "nodev" then 0 else c) bootDeviceCounters);
+          message = "You cannot have duplicated devices in mirroredBoots";
+        }
+        {
+          assertion = !cfg.trustedBoot.enable || cfg.version == 2;
+          message = "Trusted GRUB is only available for GRUB 2";
+        }
+        {
+          assertion = !cfg.efiSupport || !cfg.trustedBoot.enable;
+          message = "Trusted GRUB does not have EFI support";
+        }
+        {
+          assertion = !cfg.zfsSupport || !cfg.trustedBoot.enable;
+          message = "Trusted GRUB does not have ZFS support";
+        }
+        {
+          assertion = !cfg.trustedBoot.enable || cfg.trustedBoot.systemHasTPM == "YES_TPM_is_activated";
+          message = "Trusted GRUB can break the system! Confirm that the system has an activated TPM by setting 'systemHasTPM'.";
+        }
+        {
+          assertion = cfg.efiInstallAsRemovable -> cfg.efiSupport;
+          message = "If you wish to to use boot.loader.grub.efiInstallAsRemovable, then turn on boot.loader.grub.efiSupport";
+        }
+        {
+          assertion = cfg.efiInstallAsRemovable -> !config.boot.loader.efi.canTouchEfiVariables;
+          message = "If you wish to to use boot.loader.grub.efiInstallAsRemovable, then turn off boot.loader.efi.canTouchEfiVariables";
+        }
+      ] ++ flip concatMap cfg.mirroredBoots (args: [
+        {
+          assertion = args.devices != [ ];
+          message = "A boot path cannot have an empty devices string in ${args.path}";
+        }
+        {
+          assertion = hasPrefix "/" args.path;
+          message = "Boot paths must be absolute, not ${args.path}";
+        }
+        {
+          assertion = if args.efiSysMountPoint == null then true else hasPrefix "/" args.efiSysMountPoint;
+          message = "EFI paths must be absolute, not ${args.efiSysMountPoint}";
+        }
+      ] ++ forEach args.devices (device: {
+        assertion = device == "nodev" || hasPrefix "/" device;
+        message = "GRUB devices must be absolute paths, not ${device} in ${args.path}";
+      }));
+    })
+
+  ];
+
+
+  imports =
+    [ (mkRemovedOptionModule [ "boot" "loader" "grub" "bootDevice" ] "")
+      (mkRenamedOptionModule [ "boot" "copyKernels" ] [ "boot" "loader" "grub" "copyKernels" ])
+      (mkRenamedOptionModule [ "boot" "extraGrubEntries" ] [ "boot" "loader" "grub" "extraEntries" ])
+      (mkRenamedOptionModule [ "boot" "extraGrubEntriesBeforeNixos" ] [ "boot" "loader" "grub" "extraEntriesBeforeNixOS" ])
+      (mkRenamedOptionModule [ "boot" "grubDevice" ] [ "boot" "loader" "grub" "device" ])
+      (mkRenamedOptionModule [ "boot" "bootMount" ] [ "boot" "loader" "grub" "bootDevice" ])
+      (mkRenamedOptionModule [ "boot" "grubSplashImage" ] [ "boot" "loader" "grub" "splashImage" ])
+      (mkRemovedOptionModule [ "boot" "loader" "grub" "extraInitrd" ] ''
+        This option has been replaced with the bootloader agnostic
+        boot.initrd.secrets option. To migrate to the initrd secrets system,
+        extract the extraInitrd archive into your main filesystem:
+
+          # zcat /boot/extra_initramfs.gz | cpio -idvmD /etc/secrets/initrd
+          /path/to/secret1
+          /path/to/secret2
+
+        then replace boot.loader.grub.extraInitrd with boot.initrd.secrets:
+
+          boot.initrd.secrets = {
+            "/path/to/secret1" = "/etc/secrets/initrd/path/to/secret1";
+            "/path/to/secret2" = "/etc/secrets/initrd/path/to/secret2";
+          };
+
+        See the boot.initrd.secrets option documentation for more information.
+      '')
+    ];
+
+}
diff --git a/nixos/modules/system/boot/loader/grub/install-grub.pl b/nixos/modules/system/boot/loader/grub/install-grub.pl
new file mode 100644
index 00000000000..0c93b288fc6
--- /dev/null
+++ b/nixos/modules/system/boot/loader/grub/install-grub.pl
@@ -0,0 +1,780 @@
+use strict;
+use warnings;
+use Class::Struct;
+use XML::LibXML;
+use File::Basename;
+use File::Path;
+use File::stat;
+use File::Copy;
+use File::Copy::Recursive qw(rcopy pathrm);
+use File::Slurp;
+use File::Temp;
+use JSON;
+use File::Find;
+require List::Compare;
+use POSIX;
+use Cwd;
+
+# system.build.toplevel path
+my $defaultConfig = $ARGV[1] or die;
+
+# Grub config XML generated by grubConfig function in grub.nix
+my $dom = XML::LibXML->load_xml(location => $ARGV[0]);
+
+sub get { my ($name) = @_; return $dom->findvalue("/expr/attrs/attr[\@name = '$name']/*/\@value"); }
+
+sub getList {
+    my ($name) = @_;
+    my @list = ();
+    foreach my $entry ($dom->findnodes("/expr/attrs/attr[\@name = '$name']/list/string/\@value")) {
+        $entry = $entry->findvalue(".") or die;
+        push(@list, $entry);
+    }
+    return @list;
+}
+
+sub readFile {
+    my ($fn) = @_; local $/ = undef;
+    open FILE, "<$fn" or return undef; my $s = <FILE>; close FILE;
+    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;
+}
+
+sub runCommand {
+    my ($cmd) = @_;
+    open FILE, "$cmd 2>/dev/null |" or die "Failed to execute: $cmd\n";
+    my @ret = <FILE>;
+    close FILE;
+    return ($?, @ret);
+}
+
+my $grub = get("grub");
+my $grubVersion = int(get("version"));
+my $grubTarget = get("grubTarget");
+my $extraConfig = get("extraConfig");
+my $extraPrepareConfig = get("extraPrepareConfig");
+my $extraPerEntryConfig = get("extraPerEntryConfig");
+my $extraEntries = get("extraEntries");
+my $extraEntriesBeforeNixOS = get("extraEntriesBeforeNixOS") eq "true";
+my $splashImage = get("splashImage");
+my $splashMode = get("splashMode");
+my $backgroundColor = get("backgroundColor");
+my $configurationLimit = int(get("configurationLimit"));
+my $copyKernels = get("copyKernels") eq "true";
+my $timeout = int(get("timeout"));
+my $defaultEntry = get("default");
+my $fsIdentifier = get("fsIdentifier");
+my $grubEfi = get("grubEfi");
+my $grubTargetEfi = get("grubTargetEfi");
+my $bootPath = get("bootPath");
+my $storePath = get("storePath");
+my $canTouchEfiVariables = get("canTouchEfiVariables");
+my $efiInstallAsRemovable = get("efiInstallAsRemovable");
+my $efiSysMountPoint = get("efiSysMountPoint");
+my $gfxmodeEfi = get("gfxmodeEfi");
+my $gfxmodeBios = get("gfxmodeBios");
+my $gfxpayloadEfi = get("gfxpayloadEfi");
+my $gfxpayloadBios = get("gfxpayloadBios");
+my $bootloaderId = get("bootloaderId");
+my $forceInstall = get("forceInstall");
+my $font = get("font");
+my $theme = get("theme");
+my $saveDefault = $defaultEntry eq "saved";
+$ENV{'PATH'} = get("path");
+
+die "unsupported GRUB version\n" if $grubVersion != 1 && $grubVersion != 2;
+
+print STDERR "updating GRUB $grubVersion menu...\n";
+
+mkpath("$bootPath/grub", 0, 0700);
+
+# Discover whether the bootPath is on the same filesystem as / and
+# /nix/store.  If not, then all kernels and initrds must be copied to
+# the bootPath.
+if (stat($bootPath)->dev != stat("/nix/store")->dev) {
+    $copyKernels = 1;
+}
+
+# Discover information about the location of the bootPath
+struct(Fs => {
+    device => '$',
+    type => '$',
+    mount => '$',
+});
+sub PathInMount {
+    my ($path, $mount) = @_;
+    my @splitMount = split /\//, $mount;
+    my @splitPath = split /\//, $path;
+    if ($#splitPath < $#splitMount) {
+        return 0;
+    }
+    for (my $i = 0; $i <= $#splitMount; $i++) {
+        if ($splitMount[$i] ne $splitPath[$i]) {
+            return 0;
+        }
+    }
+    return 1;
+}
+
+# Figure out what filesystem is used for the directory with init/initrd/kernel files
+sub GetFs {
+    my ($dir) = @_;
+    my $bestFs = Fs->new(device => "", type => "", mount => "");
+    foreach my $fs (read_file("/proc/self/mountinfo")) {
+        chomp $fs;
+        my @fields = split / /, $fs;
+        my $mountPoint = $fields[4];
+        next unless -d $mountPoint;
+        my @mountOptions = split /,/, $fields[5];
+
+        # Skip the optional fields.
+        my $n = 6; $n++ while $fields[$n] ne "-"; $n++;
+        my $fsType = $fields[$n];
+        my $device = $fields[$n + 1];
+        my @superOptions = split /,/, $fields[$n + 2];
+
+        # Skip the bind-mount on /nix/store.
+        next if $mountPoint eq "/nix/store" && (grep { $_ eq "rw" } @superOptions);
+        # Skip mount point generated by systemd-efi-boot-generator?
+        next if $fsType eq "autofs";
+
+        # Ensure this matches the intended directory
+        next unless PathInMount($dir, $mountPoint);
+
+        # Is it better than our current match?
+        if (length($mountPoint) > length($bestFs->mount)) {
+            $bestFs = Fs->new(device => $device, type => $fsType, mount => $mountPoint);
+        }
+    }
+    return $bestFs;
+}
+struct (Grub => {
+    path => '$',
+    search => '$',
+});
+my $driveid = 1;
+sub GrubFs {
+    my ($dir) = @_;
+    my $fs = GetFs($dir);
+    my $path = substr($dir, length($fs->mount));
+    if (substr($path, 0, 1) ne "/") {
+        $path = "/$path";
+    }
+    my $search = "";
+
+    if ($grubVersion > 1) {
+        # ZFS is completely separate logic as zpools are always identified by a label
+        # or custom UUID
+        if ($fs->type eq 'zfs') {
+            my $sid = index($fs->device, '/');
+
+            if ($sid < 0) {
+                $search = '--label ' . $fs->device;
+                $path = '/@' . $path;
+            } else {
+                $search = '--label ' . substr($fs->device, 0, $sid);
+                $path = '/' . substr($fs->device, $sid) . '/@' . $path;
+            }
+        } else {
+            my %types = ('uuid' => '--fs-uuid', 'label' => '--label');
+
+            if ($fsIdentifier eq 'provided') {
+                # If the provided dev is identifying the partition using a label or uuid,
+                # we should get the label / uuid and do a proper search
+                my @matches = $fs->device =~ m/\/dev\/disk\/by-(label|uuid)\/(.*)/;
+                if ($#matches > 1) {
+                    die "Too many matched devices"
+                } elsif ($#matches == 1) {
+                    $search = "$types{$matches[0]} $matches[1]"
+                }
+            } else {
+                # Determine the identifying type
+                $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]}");
+                if ($status != 0) {
+                    die "Failed to get blkid info (returned $status) for @{[$fs->mount]} on @{[$fs->device]}";
+                }
+                my @matches = join("", @devInfo) =~ m/@{[uc $fsIdentifier]}=([^\n]*)/;
+                if ($#matches != 0) {
+                    die "Couldn't find a $types{$fsIdentifier} for @{[$fs->device]}\n"
+                }
+                $search .= $matches[0];
+            }
+
+            # 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]}");
+                if ($status != 0) {
+                    die "Failed to retrieve subvolume info for @{[$fs->mount]}\n";
+                }
+                my @ids = join("\n", @id_info) =~ m/^(?!\/\n).*Subvolume ID:[ \t\n]*([0-9]+)/s;
+                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]}");
+                    if ($status != 0) {
+                        die "Failed to find @{[$fs->mount]} subvolume id from btrfs\n";
+                    }
+                    my @paths = join("", @path_info) =~ m/ID $ids[0] [^\n]* path ([^\n]*)/;
+                    if ($#paths > 0) {
+                        die "Btrfs returned multiple paths for a single subvolume id, mountpoint @{[$fs->mount]}\n";
+                    } elsif ($#paths != 0) {
+                        die "Btrfs did not return a path for the subvolume at @{[$fs->mount]}\n";
+                    }
+                    $path = "/$paths[0]$path";
+                }
+            }
+        }
+        if (not $search eq "") {
+            $search = "search --set=drive$driveid " . $search;
+            $path = "(\$drive$driveid)$path";
+            $driveid += 1;
+        }
+    }
+    return Grub->new(path => $path, search => $search);
+}
+my $grubBoot = GrubFs($bootPath);
+my $grubStore;
+if ($copyKernels == 0) {
+    $grubStore = GrubFs($storePath);
+}
+
+# Generate the header.
+my $conf .= "# Automatically generated.  DO NOT EDIT THIS FILE!\n";
+
+if ($grubVersion == 1) {
+    # $defaultEntry might be "saved", indicating that we want to use the last selected configuration as default.
+    # Incidentally this is already the correct value for the grub 1 config to achieve this behaviour.
+    $conf .= "
+        default $defaultEntry
+        timeout $timeout
+    ";
+    if ($splashImage) {
+        copy $splashImage, "$bootPath/background.xpm.gz" or die "cannot copy $splashImage to $bootPath: $!\n";
+        $conf .= "splashimage " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background.xpm.gz\n";
+    }
+}
+
+else {
+    my @users = ();
+    foreach my $user ($dom->findnodes('/expr/attrs/attr[@name = "users"]/attrs/attr')) {
+        my $name = $user->findvalue('@name') or die;
+        my $hashedPassword = $user->findvalue('./attrs/attr[@name = "hashedPassword"]/string/@value');
+        my $hashedPasswordFile = $user->findvalue('./attrs/attr[@name = "hashedPasswordFile"]/string/@value');
+        my $password = $user->findvalue('./attrs/attr[@name = "password"]/string/@value');
+        my $passwordFile = $user->findvalue('./attrs/attr[@name = "passwordFile"]/string/@value');
+
+        if ($hashedPasswordFile) {
+            open(my $f, '<', $hashedPasswordFile) or die "Can't read file '$hashedPasswordFile'!";
+            $hashedPassword = <$f>;
+            chomp $hashedPassword;
+        }
+        if ($passwordFile) {
+            open(my $f, '<', $passwordFile) or die "Can't read file '$passwordFile'!";
+            $password = <$f>;
+            chomp $password;
+        }
+
+        if ($hashedPassword) {
+            if (index($hashedPassword, "grub.pbkdf2.") == 0) {
+                $conf .= "\npassword_pbkdf2 $name $hashedPassword";
+            }
+            else {
+                die "Password hash for GRUB user '$name' is not valid!";
+            }
+        }
+        elsif ($password) {
+            $conf .= "\npassword $name $password";
+        }
+        else {
+            die "GRUB user '$name' has no password!";
+        }
+        push(@users, $name);
+    }
+    if (@users) {
+        $conf .= "\nset superusers=\"" . join(' ',@users) . "\"\n";
+    }
+
+    if ($copyKernels == 0) {
+        $conf .= "
+            " . $grubStore->search;
+    }
+    # FIXME: should use grub-mkconfig.
+    my $defaultEntryText = $defaultEntry;
+    if ($saveDefault) {
+        $defaultEntryText = "\"\${saved_entry}\"";
+    }
+    $conf .= "
+        " . $grubBoot->search . "
+        if [ -s \$prefix/grubenv ]; then
+          load_env
+        fi
+
+        # ‘grub-reboot’ sets a one-time saved entry, which we process here and
+        # then delete.
+        if [ \"\${next_entry}\" ]; then
+          set default=\"\${next_entry}\"
+          set next_entry=
+          save_env next_entry
+          set timeout=1
+          set boot_once=true
+        else
+          set default=$defaultEntryText
+          set timeout=$timeout
+        fi
+
+        function savedefault {
+            if [ -z \"\${boot_once}\"]; then
+            saved_entry=\"\${chosen}\"
+            save_env saved_entry
+            fi
+        }
+
+        # Setup the graphics stack for bios and efi systems
+        if [ \"\${grub_platform}\" = \"efi\" ]; then
+          insmod efi_gop
+          insmod efi_uga
+        else
+          insmod vbe
+        fi
+    ";
+
+    if ($font) {
+        copy $font, "$bootPath/converted-font.pf2" or die "cannot copy $font to $bootPath: $!\n";
+        $conf .= "
+            insmod font
+            if loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/converted-font.pf2; then
+              insmod gfxterm
+              if [ \"\${grub_platform}\" = \"efi\" ]; then
+                set gfxmode=$gfxmodeEfi
+                set gfxpayload=$gfxpayloadEfi
+              else
+                set gfxmode=$gfxmodeBios
+                set gfxpayload=$gfxpayloadBios
+              fi
+              terminal_output gfxterm
+            fi
+        ";
+    }
+    if ($splashImage) {
+        # Keeps the image's extension.
+        my ($filename, $dirs, $suffix) = fileparse($splashImage, qr"\..[^.]*$");
+        # The module for jpg is jpeg.
+        if ($suffix eq ".jpg") {
+            $suffix = ".jpeg";
+        }
+        if ($backgroundColor) {
+            $conf .= "
+            background_color '$backgroundColor'
+            ";
+        }
+        copy $splashImage, "$bootPath/background$suffix" or die "cannot copy $splashImage to $bootPath: $!\n";
+        $conf .= "
+            insmod " . substr($suffix, 1) . "
+            if background_image --mode '$splashMode' " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background$suffix; then
+              set color_normal=white/black
+              set color_highlight=black/white
+            else
+              set menu_color_normal=cyan/blue
+              set menu_color_highlight=white/blue
+            fi
+        ";
+    }
+
+    rmtree("$bootPath/theme") or die "cannot clean up theme folder in $bootPath\n" if -e "$bootPath/theme";
+
+    if ($theme) {
+        # Copy theme
+        rcopy($theme, "$bootPath/theme") or die "cannot copy $theme to $bootPath\n";
+        $conf .= "
+            # Sets theme.
+            set theme=" . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/theme.txt
+            export theme
+            # Load theme fonts, if any
+        ";
+
+        find( { wanted => sub {
+            if ($_ =~ /\.pf2$/i) {
+                $font = File::Spec->abs2rel($File::Find::name, $theme);
+                $conf .= "
+                    loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/$font
+                ";
+            }
+        }, no_chdir => 1 }, $theme );
+    }
+}
+
+$conf .= "$extraConfig\n";
+
+
+# Generate the menu entries.
+$conf .= "\n";
+
+my %copied;
+mkpath("$bootPath/kernels", 0, 0755) if $copyKernels;
+
+sub copyToKernelsDir {
+    my ($path) = @_;
+    return $grubStore->path . substr($path, length("/nix/store")) unless $copyKernels;
+    $path =~ /\/nix\/store\/(.*)/ or die;
+    my $name = $1; $name =~ s/\//-/g;
+    my $dst = "$bootPath/kernels/$name";
+    # Don't copy the file if $dst already exists.  This means that we
+    # have to create $dst atomically to prevent partially copied
+    # kernels or initrd if this script is ever interrupted.
+    if (! -e $dst) {
+        my $tmp = "$dst.tmp";
+        copy $path, $tmp or die "cannot copy $path to $tmp: $!\n";
+        rename $tmp, $dst or die "cannot rename $tmp to $dst: $!\n";
+    }
+    $copied{$dst} = 1;
+    return ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$name";
+}
+
+sub addEntry {
+    my ($name, $path, $options) = @_;
+    return unless -e "$path/kernel" && -e "$path/initrd";
+
+    my $kernel = copyToKernelsDir(Cwd::abs_path("$path/kernel"));
+    my $initrd = copyToKernelsDir(Cwd::abs_path("$path/initrd"));
+
+    # Include second initrd with secrets
+    if (-e -x "$path/append-initrd-secrets") {
+        my $initrdName = basename($initrd);
+        my $initrdSecretsPath = "$bootPath/kernels/$initrdName-secrets";
+
+        mkpath(dirname($initrdSecretsPath), 0, 0755);
+        my $oldUmask = umask;
+        # Make sure initrd is not world readable (won't work if /boot is FAT)
+        umask 0137;
+        my $initrdSecretsPathTemp = File::Temp::mktemp("$initrdSecretsPath.XXXXXXXX");
+        system("$path/append-initrd-secrets", $initrdSecretsPathTemp) == 0 or die "failed to create initrd secrets: $!\n";
+        # Check whether any secrets were actually added
+        if (-e $initrdSecretsPathTemp && ! -z _) {
+            rename $initrdSecretsPathTemp, $initrdSecretsPath or die "failed to move initrd secrets into place: $!\n";
+            $copied{$initrdSecretsPath} = 1;
+            $initrd .= " " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$initrdName-secrets";
+        } else {
+            unlink $initrdSecretsPathTemp;
+            rmdir dirname($initrdSecretsPathTemp);
+        }
+        umask $oldUmask;
+    }
+
+    my $xen = -e "$path/xen.gz" ? copyToKernelsDir(Cwd::abs_path("$path/xen.gz")) : undef;
+
+    # FIXME: $confName
+
+    my $kernelParams =
+        "init=" . Cwd::abs_path("$path/init") . " " .
+        readFile("$path/kernel-params");
+    my $xenParams = $xen && -e "$path/xen-params" ? readFile("$path/xen-params") : "";
+
+    if ($grubVersion == 1) {
+        $conf .= "title $name\n";
+        $conf .= "  $extraPerEntryConfig\n" if $extraPerEntryConfig;
+        $conf .= "  kernel $xen $xenParams\n" if $xen;
+        $conf .= "  " . ($xen ? "module" : "kernel") . " $kernel $kernelParams\n";
+        $conf .= "  " . ($xen ? "module" : "initrd") . " $initrd\n";
+        if ($saveDefault) {
+            $conf .= "  savedefault\n";
+        }
+        $conf .= "\n";
+    } else {
+        $conf .= "menuentry \"$name\" " . ($options||"") . " {\n";
+        if ($saveDefault) {
+            $conf .= "  savedefault\n";
+        }
+        $conf .= $grubBoot->search . "\n";
+        if ($copyKernels == 0) {
+            $conf .= $grubStore->search . "\n";
+        }
+        $conf .= "  $extraPerEntryConfig\n" if $extraPerEntryConfig;
+        $conf .= "  multiboot $xen $xenParams\n" if $xen;
+        $conf .= "  " . ($xen ? "module" : "linux") . " $kernel $kernelParams\n";
+        $conf .= "  " . ($xen ? "module" : "initrd") . " $initrd\n";
+        $conf .= "}\n\n";
+    }
+}
+
+
+# Add default entries.
+$conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS;
+
+addEntry("NixOS - Default", $defaultConfig, "--unrestricted");
+
+$conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS;
+
+# Find all the children of the current default configuration
+# Do not search for grand children
+my @links = sort (glob "$defaultConfig/specialisation/*");
+foreach my $link (@links) {
+
+    my $entryName = "";
+
+    my $cfgName = readFile("$link/configuration-name");
+
+    my $date = strftime("%F", localtime(lstat($link)->mtime));
+    my $version =
+        -e "$link/nixos-version"
+        ? readFile("$link/nixos-version")
+        : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]);
+
+    if ($cfgName) {
+        $entryName = $cfgName;
+    } else {
+        my $linkname = basename($link);
+        $entryName = "($linkname - $date - $version)";
+    }
+    addEntry("NixOS - $entryName", $link);
+}
+
+my $grubBootPath = $grubBoot->path;
+# extraEntries could refer to @bootRoot@, which we have to substitute
+$conf =~ s/\@bootRoot\@/$grubBootPath/g;
+
+# Emit submenus for all system profiles.
+sub addProfile {
+    my ($profile, $description) = @_;
+
+    # Add entries for all generations of this profile.
+    $conf .= "submenu \"$description\" {\n" if $grubVersion == 2;
+
+    sub nrFromGen { my ($x) = @_; $x =~ /\/\w+-(\d+)-link/; return $1; }
+
+    my @links = sort
+        { nrFromGen($b) <=> nrFromGen($a) }
+        (glob "$profile-*-link");
+
+    my $curEntry = 0;
+    foreach my $link (@links) {
+        last if $curEntry++ >= $configurationLimit;
+        if (! -e "$link/nixos-version") {
+            warn "skipping corrupt system profile entry ‘$link’\n";
+            next;
+        }
+        my $date = strftime("%F", localtime(lstat($link)->mtime));
+        my $version =
+            -e "$link/nixos-version"
+            ? readFile("$link/nixos-version")
+            : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]);
+        addEntry("NixOS - Configuration " . nrFromGen($link) . " ($date - $version)", $link);
+    }
+
+    $conf .= "}\n" if $grubVersion == 2;
+}
+
+addProfile "/nix/var/nix/profiles/system", "NixOS - 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'";
+    }
+}
+
+# extraPrepareConfig could refer to @bootPath@, which we have to substitute
+$extraPrepareConfig =~ s/\@bootPath\@/$bootPath/g;
+
+# Run extraPrepareConfig in sh
+if ($extraPrepareConfig ne "") {
+    system((get("shell"), "-c", $extraPrepareConfig));
+}
+
+# write the GRUB config.
+my $confFile = $grubVersion == 1 ? "$bootPath/grub/menu.lst" : "$bootPath/grub/grub.cfg";
+my $tmpFile = $confFile . ".tmp";
+writeFile($tmpFile, $conf);
+
+
+# check whether to install GRUB EFI or not
+sub getEfiTarget {
+    if ($grubVersion == 1) {
+        return "no"
+    } elsif (($grub ne "") && ($grubEfi ne "")) {
+        # EFI can only be installed when target is set;
+        # A target is also required then for non-EFI grub
+        if (($grubTarget eq "") || ($grubTargetEfi eq "")) { die }
+        else { return "both" }
+    } elsif (($grub ne "") && ($grubEfi eq "")) {
+        # TODO: It would be safer to disallow non-EFI grub installation if no taget is given.
+        #       If no target is given, then grub auto-detects the target which can lead to errors.
+        #       E.g. it seems as if grub would auto-detect a EFI target based on the availability
+        #       of a EFI partition.
+        #       However, it seems as auto-detection is currently relied on for non-x86_64 and non-i386
+        #       architectures in NixOS. That would have to be fixed in the nixos modules first.
+        return "no"
+    } elsif (($grub eq "") && ($grubEfi ne "")) {
+        # EFI can only be installed when target is set;
+        if ($grubTargetEfi eq "") { die }
+        else {return "only" }
+    } else {
+        # prevent an installation if neither grub nor grubEfi is given
+        return "neither"
+    }
+}
+
+my $efiTarget = getEfiTarget();
+
+# Append entries detected by os-prober
+if (get("useOSProber") eq "true") {
+    if ($saveDefault) {
+        # os-prober will read this to determine if "savedefault" should be added to generated entries
+        $ENV{'GRUB_SAVEDEFAULT'} = "true";
+    }
+
+    my $targetpackage = ($efiTarget eq "no") ? $grub : $grubEfi;
+    system(get("shell"), "-c", "pkgdatadir=$targetpackage/share/grub $targetpackage/etc/grub.d/30_os-prober >> $tmpFile");
+}
+
+# Atomically switch to the new config
+rename $tmpFile, $confFile or die "cannot rename $tmpFile to $confFile: $!\n";
+
+
+# Remove obsolete files from $bootPath/kernels.
+foreach my $fn (glob "$bootPath/kernels/*") {
+    next if defined $copied{$fn};
+    print STDERR "removing obsolete file $fn\n";
+    unlink $fn;
+}
+
+
+#
+# Install GRUB if the parameters changed from the last time we installed it.
+#
+
+struct(GrubState => {
+    name => '$',
+    version => '$',
+    efi => '$',
+    devices => '$',
+    efiMountPoint => '$',
+    extraGrubInstallArgs => '@',
+});
+# If you add something to the state file, only add it to the end
+# because it is read line-by-line.
+sub readGrubState {
+    my $defaultGrubState = GrubState->new(name => "", version => "", efi => "", devices => "", efiMountPoint => "", extraGrubInstallArgs => () );
+    open FILE, "<$bootPath/grub/state" or return $defaultGrubState;
+    local $/ = "\n";
+    my $name = <FILE>;
+    chomp($name);
+    my $version = <FILE>;
+    chomp($version);
+    my $efi = <FILE>;
+    chomp($efi);
+    my $devices = <FILE>;
+    chomp($devices);
+    my $efiMountPoint = <FILE>;
+    chomp($efiMountPoint);
+    # Historically, arguments in the state file were one per each line, but that
+    # gets really messy when newlines are involved, structured arguments
+    # like lists are needed (they have to have a separator encoding), or even worse,
+    # when we need to remove a setting in the future. Thus, the 6th line is a JSON
+    # object that can store structured data, with named keys, and all new state
+    # should go in there.
+    my $jsonStateLine = <FILE>;
+    # For historical reasons we do not check the values above for un-definedness
+    # (that is, when the state file has too few lines and EOF is reached),
+    # because the above come from the first version of this logic and are thus
+    # guaranteed to be present.
+    $jsonStateLine = defined $jsonStateLine ? $jsonStateLine : '{}'; # empty JSON object
+    chomp($jsonStateLine);
+    if ($jsonStateLine eq "") {
+        $jsonStateLine = '{}'; # empty JSON object
+    }
+    my %jsonState = %{decode_json($jsonStateLine)};
+    my @extraGrubInstallArgs = exists($jsonState{'extraGrubInstallArgs'}) ? @{$jsonState{'extraGrubInstallArgs'}} : ();
+    close FILE;
+    my $grubState = GrubState->new(name => $name, version => $version, efi => $efi, devices => $devices, efiMountPoint => $efiMountPoint, extraGrubInstallArgs => \@extraGrubInstallArgs );
+    return $grubState
+}
+
+my @deviceTargets = getList('devices');
+my $prevGrubState = readGrubState();
+my @prevDeviceTargets = split/,/, $prevGrubState->devices;
+my @extraGrubInstallArgs = getList('extraGrubInstallArgs');
+my @prevExtraGrubInstallArgs = @{$prevGrubState->extraGrubInstallArgs};
+
+my $devicesDiffer = scalar (List::Compare->new( '-u', '-a', \@deviceTargets, \@prevDeviceTargets)->get_symmetric_difference());
+my $extraGrubInstallArgsDiffer = scalar (List::Compare->new( '-u', '-a', \@extraGrubInstallArgs, \@prevExtraGrubInstallArgs)->get_symmetric_difference());
+my $nameDiffer = get("fullName") ne $prevGrubState->name;
+my $versionDiffer = get("fullVersion") ne $prevGrubState->version;
+my $efiDiffer = $efiTarget ne $prevGrubState->efi;
+my $efiMountPointDiffer = $efiSysMountPoint ne $prevGrubState->efiMountPoint;
+if (($ENV{'NIXOS_INSTALL_GRUB'} // "") eq "1") {
+    warn "NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER";
+    $ENV{'NIXOS_INSTALL_BOOTLOADER'} = "1";
+}
+my $requireNewInstall = $devicesDiffer || $extraGrubInstallArgsDiffer || $nameDiffer || $versionDiffer || $efiDiffer || $efiMountPointDiffer || (($ENV{'NIXOS_INSTALL_BOOTLOADER'} // "") eq "1");
+
+# install a symlink so that grub can detect the boot drive
+my $tmpDir = File::Temp::tempdir(CLEANUP => 1) or die "Failed to create temporary space: $!";
+symlink "$bootPath", "$tmpDir/boot" or die "Failed to symlink $tmpDir/boot: $!";
+
+# install non-EFI GRUB
+if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) {
+    foreach my $dev (@deviceTargets) {
+        next if $dev eq "nodev";
+        print STDERR "installing the GRUB $grubVersion boot loader on $dev...\n";
+        my @command = ("$grub/sbin/grub-install", "--recheck", "--root-directory=$tmpDir", Cwd::abs_path($dev), @extraGrubInstallArgs);
+        if ($forceInstall eq "true") {
+            push @command, "--force";
+        }
+        if ($grubTarget ne "") {
+            push @command, "--target=$grubTarget";
+        }
+        (system @command) == 0 or die "$0: installation of GRUB on $dev failed: $!\n";
+    }
+}
+
+
+# install EFI GRUB
+if (($requireNewInstall != 0) && ($efiTarget eq "only" || $efiTarget eq "both")) {
+    print STDERR "installing the GRUB $grubVersion EFI boot loader into $efiSysMountPoint...\n";
+    my @command = ("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--boot-directory=$bootPath", "--efi-directory=$efiSysMountPoint", @extraGrubInstallArgs);
+    if ($forceInstall eq "true") {
+        push @command, "--force";
+    }
+    if ($canTouchEfiVariables eq "true") {
+        push @command, "--bootloader-id=$bootloaderId";
+    } else {
+        push @command, "--no-nvram";
+        push @command, "--removable" if $efiInstallAsRemovable eq "true";
+    }
+
+    (system @command) == 0 or die "$0: installation of GRUB EFI into $efiSysMountPoint failed: $!\n";
+}
+
+
+# update GRUB state file
+if ($requireNewInstall != 0) {
+    # Temp file for atomic rename.
+    my $stateFile = "$bootPath/grub/state";
+    my $stateFileTmp = $stateFile . ".tmp";
+
+    open FILE, ">$stateFileTmp" or die "cannot create $stateFileTmp: $!\n";
+    print FILE get("fullName"), "\n" or die;
+    print FILE get("fullVersion"), "\n" or die;
+    print FILE $efiTarget, "\n" or die;
+    print FILE join( ",", @deviceTargets ), "\n" or die;
+    print FILE $efiSysMountPoint, "\n" or die;
+    my %jsonState = (
+        extraGrubInstallArgs => \@extraGrubInstallArgs
+    );
+    my $jsonStateLine = encode_json(\%jsonState);
+    print FILE $jsonStateLine, "\n" or die;
+    close FILE or die;
+
+    # Atomically switch to the new state file
+    rename $stateFileTmp, $stateFile or die "cannot rename $stateFileTmp to $stateFile: $!\n";
+}
diff --git a/nixos/modules/system/boot/loader/grub/ipxe.nix b/nixos/modules/system/boot/loader/grub/ipxe.nix
new file mode 100644
index 00000000000..ef8595592f4
--- /dev/null
+++ b/nixos/modules/system/boot/loader/grub/ipxe.nix
@@ -0,0 +1,64 @@
+# This module adds a scripted iPXE entry to the GRUB boot menu.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  scripts = builtins.attrNames config.boot.loader.grub.ipxe;
+
+  grubEntry = name:
+    ''
+      menuentry "iPXE - ${name}" {
+        linux16 @bootRoot@/ipxe.lkrn
+        initrd16 @bootRoot@/${name}.ipxe
+      }
+
+    '';
+
+  scriptFile = name:
+    let
+      value = builtins.getAttr name config.boot.loader.grub.ipxe;
+    in
+    if builtins.typeOf value == "path" then value
+    else builtins.toFile "${name}.ipxe" value;
+in
+{
+  options =
+    { boot.loader.grub.ipxe = mkOption {
+        type = types.attrsOf (types.either types.path types.str);
+        description =
+          ''
+            Set of iPXE scripts available for
+            booting from the GRUB boot menu.
+          '';
+        default = { };
+        example = literalExpression ''
+          { demo = '''
+              #!ipxe
+              dhcp
+              chain http://boot.ipxe.org/demo/boot.php
+            ''';
+          }
+        '';
+      };
+    };
+
+  config = mkIf (builtins.length scripts != 0) {
+
+    boot.loader.grub.extraEntries =
+      if config.boot.loader.grub.version == 2 then
+        toString (map grubEntry scripts)
+      else
+        throw "iPXE is not supported with GRUB 1.";
+
+    boot.loader.grub.extraFiles =
+      { "ipxe.lkrn" = "${pkgs.ipxe}/ipxe.lkrn"; }
+      //
+      builtins.listToAttrs ( map
+        (name: { name = name+".ipxe"; value = scriptFile name; })
+        scripts
+      );
+  };
+
+}
diff --git a/nixos/modules/system/boot/loader/grub/memtest.nix b/nixos/modules/system/boot/loader/grub/memtest.nix
new file mode 100644
index 00000000000..71e50dd0577
--- /dev/null
+++ b/nixos/modules/system/boot/loader/grub/memtest.nix
@@ -0,0 +1,116 @@
+# This module adds Memtest86+/Memtest86 to the GRUB boot menu.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  memtest86 = pkgs.memtest86plus;
+  efiSupport = config.boot.loader.grub.efiSupport;
+  cfg = config.boot.loader.grub.memtest86;
+in
+
+{
+  options = {
+
+    boot.loader.grub.memtest86 = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Make Memtest86+ (or MemTest86 if EFI support is enabled),
+          a memory testing program, available from the
+          GRUB boot menu. MemTest86 is an unfree program, so
+          this requires <literal>allowUnfree</literal> to be set to
+          <literal>true</literal>.
+        '';
+      };
+
+      params = mkOption {
+        default = [];
+        example = [ "console=ttyS0,115200" ];
+        type = types.listOf types.str;
+        description = ''
+          Parameters added to the Memtest86+ command line. As of memtest86+ 5.01
+          the following list of (apparently undocumented) parameters are
+          accepted:
+
+          <itemizedlist>
+
+          <listitem>
+            <para><literal>console=...</literal>, set up a serial console.
+            Examples:
+            <literal>console=ttyS0</literal>,
+            <literal>console=ttyS0,9600</literal> or
+            <literal>console=ttyS0,115200n8</literal>.</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>btrace</literal>, enable boot trace.</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>maxcpus=N</literal>, limit number of CPUs.</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>onepass</literal>, run one pass and exit if there
+            are no errors.</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>tstlist=...</literal>, list of tests to run.
+            Example: <literal>0,1,2</literal>.</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>cpumask=...</literal>, set a CPU mask, to select CPUs
+            to use for testing.</para>
+          </listitem>
+
+          </itemizedlist>
+
+          This list of command line options was obtained by reading the
+          Memtest86+ source code.
+        '';
+      };
+
+    };
+  };
+
+  config = mkMerge [
+    (mkIf (cfg.enable && efiSupport) {
+      assertions = [
+        {
+          assertion = cfg.params == [];
+          message = "Parameters are not available for MemTest86";
+        }
+      ];
+
+      boot.loader.grub.extraFiles = {
+        "memtest86.efi" = "${pkgs.memtest86-efi}/BOOTX64.efi";
+      };
+
+      boot.loader.grub.extraEntries = ''
+        menuentry "Memtest86" {
+          chainloader /memtest86.efi
+        }
+      '';
+    })
+
+    (mkIf (cfg.enable && !efiSupport) {
+      boot.loader.grub.extraEntries =
+        if config.boot.loader.grub.version == 2 then
+          ''
+            menuentry "Memtest86+" {
+              linux16 @bootRoot@/memtest.bin ${toString cfg.params}
+            }
+          ''
+        else
+          throw "Memtest86+ is not supported with GRUB 1.";
+
+      boot.loader.grub.extraFiles."memtest.bin" = "${memtest86}/memtest.bin";
+    })
+  ];
+}
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
new file mode 100644
index 00000000000..bd3fc64999d
--- /dev/null
+++ b/nixos/modules/system/boot/loader/init-script/init-script-builder.sh
@@ -0,0 +1,92 @@
+#! @bash@/bin/sh -e
+
+shopt -s nullglob
+
+export PATH=/empty
+for i in @path@; do PATH=$PATH:$i/bin; done
+
+if test $# -ne 1; then
+    echo "Usage: init-script-builder.sh DEFAULT-CONFIG"
+    exit 1
+fi
+
+defaultConfig="$1"
+
+
+[ "$(stat -f -c '%i' /)" = "$(stat -f -c '%i' /boot)" ] || {
+  # see grub-menu-builder.sh
+  echo "WARNING: /boot being on a different filesystem not supported by init-script-builder.sh"
+}
+
+
+
+target="/sbin/init"
+targetOther="/boot/init-other-configurations-contents.txt"
+
+tmp="$target.tmp"
+tmpOther="$targetOther.tmp"
+
+
+configurationCounter=0
+numAlienEntries=`cat <<EOF | egrep '^[[:space:]]*title' | wc -l
+@extraEntries@
+EOF`
+
+
+
+
+# Add an entry to $targetOther
+addEntry() {
+    local name="$1"
+    local path="$2"
+    local shortSuffix="$3"
+
+    configurationCounter=$((configurationCounter + 1))
+
+    local stage2=$path/init
+
+    content="$(
+      echo "#!/bin/sh"
+      echo "# $name"
+      echo "# created by init-script-builder.sh"
+      echo "exec $stage2"
+    )"
+
+    [ "$path" != "$defaultConfig" ] || {
+      echo "$content" > $tmp
+      echo "# older configurations: $targetOther" >> $tmp
+      chmod +x $tmp
+    }
+
+    echo -e "$content\n\n" >> $tmpOther
+}
+
+
+mkdir -p /boot /sbin
+
+addEntry "NixOS - 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 ""
+done
+
+for generation in $(
+    (cd /nix/var/nix/profiles && ls -d system-*-link) \
+    | sed 's/system-\([0-9]\+\)-link/\1/' \
+    | sort -n -r); do
+    link=/nix/var/nix/profiles/system-$generation-link
+    date=$(stat --printf="%y\n" $link | sed 's/\..*//')
+    if [ -d $link/kernel ]; then
+      kernelVersion=$(cd $(dirname $(readlink -f $link/kernel))/lib/modules && echo *)
+      suffix="($date - $kernelVersion)"
+    else
+      suffix="($date)"
+    fi
+    addEntry "NixOS - Configuration $generation $suffix" $link "$generation ($date)"
+done
+
+mv $tmpOther $targetOther
+mv $tmp $target
diff --git a/nixos/modules/system/boot/loader/init-script/init-script.nix b/nixos/modules/system/boot/loader/init-script/init-script.nix
new file mode 100644
index 00000000000..374d9524ff1
--- /dev/null
+++ b/nixos/modules/system/boot/loader/init-script/init-script.nix
@@ -0,0 +1,51 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  initScriptBuilder = pkgs.substituteAll {
+    src = ./init-script-builder.sh;
+    isExecutable = true;
+    inherit (pkgs) bash;
+    path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep];
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    boot.loader.initScript = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Some systems require a /sbin/init script which is started.
+          Or having it makes starting NixOS easier.
+          This applies to some kind of hosting services and user mode linux.
+
+          Additionally this script will create
+          /boot/init-other-configurations-contents.txt containing
+          contents of remaining configurations. You can copy paste them into
+          /sbin/init manually running a rescue system or such.
+        '';
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.boot.loader.initScript.enable {
+
+    system.build.installBootLoader = initScriptBuilder;
+
+  };
+
+}
diff --git a/nixos/modules/system/boot/loader/loader.nix b/nixos/modules/system/boot/loader/loader.nix
new file mode 100644
index 00000000000..01475f79b9c
--- /dev/null
+++ b/nixos/modules/system/boot/loader/loader.nix
@@ -0,0 +1,20 @@
+{ lib, ... }:
+
+with lib;
+
+{
+  imports = [
+    (mkRenamedOptionModule [ "boot" "loader" "grub" "timeout" ] [ "boot" "loader" "timeout" ])
+    (mkRenamedOptionModule [ "boot" "loader" "gummiboot" "timeout" ] [ "boot" "loader" "timeout" ])
+  ];
+
+    options = {
+        boot.loader.timeout =  mkOption {
+            default = 5;
+            type = types.nullOr types.int;
+            description = ''
+              Timeout (in seconds) until loader boots the default menu item. Use null if the loader menu should be displayed indefinitely.
+            '';
+        };
+    };
+}
diff --git a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.nix b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.nix
new file mode 100644
index 00000000000..64e106036ab
--- /dev/null
+++ b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.nix
@@ -0,0 +1,9 @@
+{ pkgs, configTxt, firmware ? pkgs.raspberrypifw }:
+
+pkgs.substituteAll {
+  src = ./raspberrypi-builder.sh;
+  isExecutable = true;
+  inherit (pkgs) bash;
+  path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep];
+  inherit firmware configTxt;
+}
diff --git a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.sh b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.sh
new file mode 100644
index 00000000000..0541ca1ba62
--- /dev/null
+++ b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi-builder.sh
@@ -0,0 +1,143 @@
+#! @bash@/bin/sh
+
+# This can end up being called disregarding the shebang.
+set -e
+
+shopt -s nullglob
+
+export PATH=/empty
+for i in @path@; do PATH=$PATH:$i/bin; done
+
+usage() {
+    echo "usage: $0 -c <path-to-default-configuration> [-d <boot-dir>]" >&2
+    exit 1
+}
+
+default=                # Default configuration
+target=/boot            # Target directory
+
+while getopts "c:d:" opt; do
+    case "$opt" in
+        c) default="$OPTARG" ;;
+        d) target="$OPTARG" ;;
+        \?) usage ;;
+    esac
+done
+
+echo "updating the boot generations directory..."
+
+mkdir -p $target/old
+
+# Convert a path to a file in the Nix store such as
+# /nix/store/<hash>-<name>/file to <hash>-<name>-<file>.
+cleanName() {
+    local path="$1"
+    echo "$path" | sed 's|^/nix/store/||' | sed 's|/|-|g'
+}
+
+# Copy a file from the Nix store to $target/kernels.
+declare -A filesCopied
+
+copyToKernelsDir() {
+    local src="$1"
+    local dst="$target/old/$(cleanName $src)"
+    # Don't copy the file if $dst already exists.  This means that we
+    # have to create $dst atomically to prevent partially copied
+    # kernels or initrd if this script is ever interrupted.
+    if ! test -e $dst; then
+        local dstTmp=$dst.tmp.$$
+        cp $src $dstTmp
+        mv $dstTmp $dst
+    fi
+    filesCopied[$dst]=1
+    result=$dst
+}
+
+copyForced() {
+    local src="$1"
+    local dst="$2"
+    cp $src $dst.tmp
+    mv $dst.tmp $dst
+}
+
+outdir=$target/old
+mkdir -p $outdir || true
+
+# Copy its kernel and initrd to $target/old.
+addEntry() {
+    local path="$1"
+    local generation="$2"
+
+    if ! test -e $path/kernel -a -e $path/initrd; then
+        return
+    fi
+
+    local kernel=$(readlink -f $path/kernel)
+    local initrd=$(readlink -f $path/initrd)
+    local dtb_path=$(readlink -f $path/dtbs)
+
+    if test -n "@copyKernels@"; then
+        copyToKernelsDir $kernel; kernel=$result
+        copyToKernelsDir $initrd; initrd=$result
+    fi
+
+    echo $(readlink -f $path) > $outdir/$generation-system
+    echo $(readlink -f $path/init) > $outdir/$generation-init
+    cp $path/kernel-params $outdir/$generation-cmdline.txt
+    echo $initrd > $outdir/$generation-initrd
+    echo $kernel > $outdir/$generation-kernel
+
+    if test "$generation" = "default"; then
+      copyForced $kernel $target/kernel.img
+      copyForced $initrd $target/initrd
+      for dtb in $dtb_path/{broadcom,}/bcm*.dtb; do
+        dst="$target/$(basename $dtb)"
+        copyForced $dtb "$dst"
+        filesCopied[$dst]=1
+      done
+      cp "$(readlink -f "$path/init")" $target/nixos-init
+      echo "`cat $path/kernel-params` init=$path/init" >$target/cmdline.txt
+    fi
+}
+
+addEntry $default default
+
+# Add all generations of the system profile to the menu, in reverse
+# (most recent to least recent) order.
+for generation in $(
+    (cd /nix/var/nix/profiles && ls -d system-*-link) \
+    | sed 's/system-\([0-9]\+\)-link/\1/' \
+    | sort -n -r); do
+    link=/nix/var/nix/profiles/system-$generation-link
+    addEntry $link $generation
+done
+
+# Add the firmware files
+fwdir=@firmware@/share/raspberrypi/boot/
+copyForced $fwdir/bootcode.bin  $target/bootcode.bin
+copyForced $fwdir/fixup.dat     $target/fixup.dat
+copyForced $fwdir/fixup4.dat    $target/fixup4.dat
+copyForced $fwdir/fixup4cd.dat  $target/fixup4cd.dat
+copyForced $fwdir/fixup4db.dat  $target/fixup4db.dat
+copyForced $fwdir/fixup4x.dat   $target/fixup4x.dat
+copyForced $fwdir/fixup_cd.dat  $target/fixup_cd.dat
+copyForced $fwdir/fixup_db.dat  $target/fixup_db.dat
+copyForced $fwdir/fixup_x.dat   $target/fixup_x.dat
+copyForced $fwdir/start.elf     $target/start.elf
+copyForced $fwdir/start4.elf    $target/start4.elf
+copyForced $fwdir/start4cd.elf  $target/start4cd.elf
+copyForced $fwdir/start4db.elf  $target/start4db.elf
+copyForced $fwdir/start4x.elf   $target/start4x.elf
+copyForced $fwdir/start_cd.elf  $target/start_cd.elf
+copyForced $fwdir/start_db.elf  $target/start_db.elf
+copyForced $fwdir/start_x.elf   $target/start_x.elf
+
+# Add the config.txt
+copyForced @configTxt@ $target/config.txt
+
+# Remove obsolete files from $target and $target/old.
+for fn in $target/old/*linux* $target/old/*initrd-initrd* $target/bcm*.dtb; do
+    if ! test "${filesCopied[$fn]}" = 1; then
+        rm -vf -- "$fn"
+    fi
+done
diff --git a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
new file mode 100644
index 00000000000..1023361f0b1
--- /dev/null
+++ b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
@@ -0,0 +1,105 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.boot.loader.raspberryPi;
+
+  builderUboot = import ./uboot-builder.nix { inherit pkgs configTxt; inherit (cfg) version; };
+  builderGeneric = import ./raspberrypi-builder.nix { inherit pkgs configTxt; };
+
+  builder =
+    if cfg.uboot.enable then
+      "${builderUboot} -g ${toString cfg.uboot.configurationLimit} -t ${timeoutStr} -c"
+    else
+      "${builderGeneric} -c";
+
+  blCfg = config.boot.loader;
+  timeoutStr = if blCfg.timeout == null then "-1" else toString blCfg.timeout;
+
+  isAarch64 = pkgs.stdenv.hostPlatform.isAarch64;
+  optional = pkgs.lib.optionalString;
+
+  configTxt =
+    pkgs.writeText "config.txt" (''
+      # U-Boot used to need this to work, regardless of whether UART is actually used or not.
+      # TODO: check when/if this can be removed.
+      enable_uart=1
+
+      # Prevent the firmware from smashing the framebuffer setup done by the mainline kernel
+      # when attempting to show low-voltage or overtemperature warnings.
+      avoid_warnings=1
+    '' + optional isAarch64 ''
+      # Boot in 64-bit mode.
+      arm_64bit=1
+    '' + (if cfg.uboot.enable then ''
+      kernel=u-boot-rpi.bin
+    '' else ''
+      kernel=kernel.img
+      initramfs initrd followkernel
+    '') + optional (cfg.firmwareConfig != null) cfg.firmwareConfig);
+
+in
+
+{
+  options = {
+
+    boot.loader.raspberryPi = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to create files with the system generations in
+          <literal>/boot</literal>.
+          <literal>/boot/old</literal> will hold files from old generations.
+        '';
+      };
+
+      version = mkOption {
+        default = 2;
+        type = types.enum [ 0 1 2 3 4 ];
+        description = "";
+      };
+
+      uboot = {
+        enable = mkOption {
+          default = false;
+          type = types.bool;
+          description = ''
+            Enable using uboot as bootmanager for the raspberry pi.
+          '';
+        };
+
+        configurationLimit = mkOption {
+          default = 20;
+          example = 10;
+          type = types.int;
+          description = ''
+            Maximum number of configurations in the boot menu.
+          '';
+        };
+
+      };
+
+      firmwareConfig = mkOption {
+        default = null;
+        type = types.nullOr types.lines;
+        description = ''
+          Extra options that will be appended to <literal>/boot/config.txt</literal> file.
+          For possible values, see: https://www.raspberrypi.org/documentation/configuration/config-txt/
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = singleton {
+      assertion = !pkgs.stdenv.hostPlatform.isAarch64 || cfg.version >= 3;
+      message = "Only Raspberry Pi >= 3 supports aarch64.";
+    };
+
+    system.build.installBootLoader = builder;
+    system.boot.loader.id = "raspberrypi";
+    system.boot.loader.kernelFile = pkgs.stdenv.hostPlatform.linux-kernel.target;
+  };
+}
diff --git a/nixos/modules/system/boot/loader/raspberrypi/uboot-builder.nix b/nixos/modules/system/boot/loader/raspberrypi/uboot-builder.nix
new file mode 100644
index 00000000000..a4352ab9a24
--- /dev/null
+++ b/nixos/modules/system/boot/loader/raspberrypi/uboot-builder.nix
@@ -0,0 +1,37 @@
+{ pkgs, version, configTxt }:
+
+let
+  isAarch64 = pkgs.stdenv.hostPlatform.isAarch64;
+
+  uboot =
+    if version == 0 then
+      pkgs.ubootRaspberryPiZero
+    else if version == 1 then
+      pkgs.ubootRaspberryPi
+    else if version == 2 then
+      pkgs.ubootRaspberryPi2
+    else if version == 3 then
+      if isAarch64 then
+        pkgs.ubootRaspberryPi3_64bit
+      else
+        pkgs.ubootRaspberryPi3_32bit
+    else
+      throw "U-Boot is not yet supported on the raspberry pi 4.";
+
+  extlinuxConfBuilder =
+    import ../generic-extlinux-compatible/extlinux-conf-builder.nix {
+      pkgs = pkgs.buildPackages;
+    };
+in
+pkgs.substituteAll {
+  src = ./uboot-builder.sh;
+  isExecutable = true;
+  inherit (pkgs) bash;
+  path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep];
+  firmware = pkgs.raspberrypifw;
+  inherit uboot;
+  inherit configTxt;
+  inherit extlinuxConfBuilder;
+  inherit version;
+}
+
diff --git a/nixos/modules/system/boot/loader/raspberrypi/uboot-builder.sh b/nixos/modules/system/boot/loader/raspberrypi/uboot-builder.sh
new file mode 100644
index 00000000000..ea591427179
--- /dev/null
+++ b/nixos/modules/system/boot/loader/raspberrypi/uboot-builder.sh
@@ -0,0 +1,38 @@
+#! @bash@/bin/sh -e
+
+target=/boot # Target directory
+
+while getopts "t:c:d:g:" opt; do
+    case "$opt" in
+        d) target="$OPTARG" ;;
+        *) ;;
+    esac
+done
+
+copyForced() {
+    local src="$1"
+    local dst="$2"
+    cp $src $dst.tmp
+    mv $dst.tmp $dst
+}
+
+# Call the extlinux builder
+"@extlinuxConfBuilder@" "$@"
+
+# Add the firmware files
+fwdir=@firmware@/share/raspberrypi/boot/
+copyForced $fwdir/bootcode.bin  $target/bootcode.bin
+copyForced $fwdir/fixup.dat     $target/fixup.dat
+copyForced $fwdir/fixup_cd.dat  $target/fixup_cd.dat
+copyForced $fwdir/fixup_db.dat  $target/fixup_db.dat
+copyForced $fwdir/fixup_x.dat   $target/fixup_x.dat
+copyForced $fwdir/start.elf     $target/start.elf
+copyForced $fwdir/start_cd.elf  $target/start_cd.elf
+copyForced $fwdir/start_db.elf  $target/start_db.elf
+copyForced $fwdir/start_x.elf   $target/start_x.elf
+
+# Add the uboot file
+copyForced @uboot@/u-boot.bin $target/u-boot-rpi.bin
+
+# Add the config.txt
+copyForced @configTxt@ $target/config.txt
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
new file mode 100644
index 00000000000..fa879437fd8
--- /dev/null
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
@@ -0,0 +1,316 @@
+#! @python3@/bin/python3 -B
+import argparse
+import shutil
+import os
+import sys
+import errno
+import subprocess
+import glob
+import tempfile
+import errno
+import warnings
+import ctypes
+libc = ctypes.CDLL("libc.so.6")
+import re
+import datetime
+import glob
+import os.path
+from typing import NamedTuple, List, Optional
+
+class SystemIdentifier(NamedTuple):
+    profile: Optional[str]
+    generation: int
+    specialisation: Optional[str]
+
+
+def copy_if_not_exists(source: str, dest: str) -> None:
+    if not os.path.exists(dest):
+        shutil.copyfile(source, dest)
+
+
+def generation_dir(profile: Optional[str], generation: int) -> str:
+    if profile:
+        return "/nix/var/nix/profiles/system-profiles/%s-%d-link" % (profile, generation)
+    else:
+        return "/nix/var/nix/profiles/system-%d-link" % (generation)
+
+def system_dir(profile: Optional[str], generation: int, specialisation: Optional[str]) -> str:
+    d = generation_dir(profile, generation)
+    if specialisation:
+        return os.path.join(d, "specialisation", specialisation)
+    else:
+        return d
+
+BOOT_ENTRY = """title NixOS{profile}{specialisation}
+version Generation {generation} {description}
+linux {kernel}
+initrd {initrd}
+options {kernel_params}
+"""
+
+def generation_conf_filename(profile: Optional[str], generation: int, specialisation: Optional[str]) -> str:
+    pieces = [
+        "nixos",
+        profile or None,
+        "generation",
+        str(generation),
+        f"specialisation-{specialisation}" if specialisation else None,
+    ]
+    return "-".join(p for p in pieces if p) + ".conf"
+
+
+def write_loader_conf(profile: Optional[str], generation: int, specialisation: Optional[str]) -> None:
+    with open("@efiSysMountPoint@/loader/loader.conf.tmp", 'w') as f:
+        if "@timeout@" != "":
+            f.write("timeout @timeout@\n")
+        f.write("default %s\n" % generation_conf_filename(profile, generation, specialisation))
+        if not @editor@:
+            f.write("editor 0\n");
+        f.write("console-mode @consoleMode@\n");
+    os.rename("@efiSysMountPoint@/loader/loader.conf.tmp", "@efiSysMountPoint@/loader/loader.conf")
+
+
+def profile_path(profile: Optional[str], generation: int, specialisation: Optional[str], name: str) -> str:
+    return os.path.realpath("%s/%s" % (system_dir(profile, generation, specialisation), name))
+
+
+def copy_from_profile(profile: Optional[str], generation: int, specialisation: Optional[str], name: str, dry_run: bool = False) -> str:
+    store_file_path = profile_path(profile, generation, specialisation, name)
+    suffix = os.path.basename(store_file_path)
+    store_dir = os.path.basename(os.path.dirname(store_file_path))
+    efi_file_path = "/efi/nixos/%s-%s.efi" % (store_dir, suffix)
+    if not dry_run:
+        copy_if_not_exists(store_file_path, "@efiSysMountPoint@%s" % (efi_file_path))
+    return efi_file_path
+
+
+def describe_generation(generation_dir: str) -> str:
+    try:
+        with open("%s/nixos-version" % generation_dir) as f:
+            nixos_version = f.read()
+    except IOError:
+        nixos_version = "Unknown"
+
+    kernel_dir = os.path.dirname(os.path.realpath("%s/kernel" % generation_dir))
+    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_date = datetime.datetime.fromtimestamp(build_time).strftime('%F')
+
+    description = "NixOS {}, 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:
+    kernel = copy_from_profile(profile, generation, specialisation, "kernel")
+    initrd = copy_from_profile(profile, generation, specialisation, "initrd")
+    try:
+        append_initrd_secrets = profile_path(profile, generation, specialisation, "append-initrd-secrets")
+        subprocess.check_call([append_initrd_secrets, "@efiSysMountPoint@%s" % (initrd)])
+    except FileNotFoundError:
+        pass
+    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
+
+    with open("%s/kernel-params" % (generation_dir)) 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 "",
+                    generation=generation,
+                    kernel=kernel,
+                    initrd=initrd,
+                    kernel_params=kernel_params,
+                    description=describe_generation(generation_dir)))
+        if machine_id is not None:
+            f.write("machine-id %s\n" % machine_id)
+    os.rename(tmp_path, entry_file)
+
+
+def mkdir_p(path: str) -> None:
+    try:
+        os.makedirs(path)
+    except OSError as e:
+        if e.errno != errno.EEXIST or not os.path.isdir(path):
+            raise
+
+
+def get_generations(profile: Optional[str] = None) -> List[SystemIdentifier]:
+    gen_list = subprocess.check_output([
+        "@nix@/bin/nix-env",
+        "--list-generations",
+        "-p",
+        "/nix/var/nix/profiles/%s" % ("system-profiles/" + profile if profile else "system"),
+        "--option", "build-users-group", ""],
+        universal_newlines=True)
+    gen_lines = gen_list.split('\n')
+    gen_lines.pop()
+
+    configurationLimit = @configurationLimit@
+    configurations = [
+        SystemIdentifier(
+            profile=profile,
+            generation=int(line.split()[0]),
+            specialisation=None
+        )
+        for line in gen_lines
+    ]
+    return configurations[-configurationLimit:]
+
+
+def get_specialisations(profile: Optional[str], generation: int, _: Optional[str]) -> List[SystemIdentifier]:
+    specialisations_dir = os.path.join(
+            system_dir(profile, generation, None), "specialisation")
+    if not os.path.exists(specialisations_dir):
+        return []
+    return [SystemIdentifier(profile, generation, spec) for spec in os.listdir(specialisations_dir)]
+
+
+def remove_old_entries(gens: List[SystemIdentifier]) -> None:
+    rex_profile = re.compile("^@efiSysMountPoint@/loader/entries/nixos-(.*)-generation-.*\.conf$")
+    rex_generation = re.compile("^@efiSysMountPoint@/loader/entries/nixos.*-generation-(.*)\.conf$")
+    known_paths = []
+    for gen in gens:
+        known_paths.append(copy_from_profile(*gen, "kernel", True))
+        known_paths.append(copy_from_profile(*gen, "initrd", True))
+    for path in glob.iglob("@efiSysMountPoint@/loader/entries/nixos*-generation-[1-9]*.conf"):
+        try:
+            if rex_profile.match(path):
+                prof = rex_profile.sub(r"\1", path)
+            else:
+                prof = "system"
+            gen_number = int(rex_generation.sub(r"\1", path))
+            if not (prof, gen_number) in gens:
+                os.unlink(path)
+        except ValueError:
+            pass
+    for path in glob.iglob("@efiSysMountPoint@/efi/nixos/*"):
+        if not path in known_paths and not os.path.isdir(path):
+            os.unlink(path)
+
+
+def get_profiles() -> List[str]:
+    if os.path.isdir("/nix/var/nix/profiles/system-profiles/"):
+        return [x
+            for x in os.listdir("/nix/var/nix/profiles/system-profiles/")
+            if not x.endswith("-link")]
+    else:
+        return []
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description='Update NixOS-related systemd-boot files')
+    parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help='The default NixOS config to boot')
+    args = parser.parse_args()
+
+    try:
+        with open("/etc/machine-id") as machine_file:
+            machine_id = machine_file.readlines()[0]
+    except IOError as e:
+        if e.errno != errno.ENOENT:
+            raise
+        # Since systemd version 232 a machine ID is required and it might not
+        # be there on newly installed systems, so let's generate one so that
+        # bootctl can find it and we can also pass it to write_entry() later.
+        cmd = ["@systemd@/bin/systemd-machine-id-setup", "--print"]
+        machine_id = subprocess.run(
+          cmd, text=True, check=True, stdout=subprocess.PIPE
+        ).stdout.rstrip()
+
+    if os.getenv("NIXOS_INSTALL_GRUB") == "1":
+        warnings.warn("NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER", DeprecationWarning)
+        os.environ["NIXOS_INSTALL_BOOTLOADER"] = "1"
+
+    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", "--path=@efiSysMountPoint@"] + flags + ["install"])
+    else:
+        # Update bootloader to latest if needed
+        systemd_version = subprocess.check_output(["@systemd@/bin/bootctl", "--version"], universal_newlines=True).split()[2]
+        sdboot_status = subprocess.check_output(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "status"], universal_newlines=True)
+
+        # See status_binaries() in systemd bootctl.c for code which generates this
+        m = re.search("^\W+File:.*/EFI/(BOOT|systemd)/.*\.efi \(systemd-boot ([\d.]+[^)]*)\)$",
+                      sdboot_status, re.IGNORECASE | re.MULTILINE)
+
+        needs_install = False
+
+        if m is None:
+            print("could not find any previously installed systemd-boot, installing.")
+            # Let systemd-boot attempt an installation if a previous one wasn't found
+            needs_install = True
+        else:
+            sdboot_version = f'({m.group(2)})'
+            if systemd_version != sdboot_version:
+                print("updating systemd-boot from %s to %s" % (sdboot_version, systemd_version))
+                needs_install = True
+
+        if needs_install:
+            subprocess.check_call(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "update"])
+
+    mkdir_p("@efiSysMountPoint@/efi/nixos")
+    mkdir_p("@efiSysMountPoint@/loader/entries")
+
+    gens = get_generations()
+    for profile in get_profiles():
+        gens += get_generations(profile)
+    remove_old_entries(gens)
+    for gen in gens:
+        try:
+            write_entry(*gen, machine_id)
+            for specialisation in get_specialisations(*gen):
+                write_entry(*specialisation, machine_id)
+            if os.readlink(system_dir(*gen)) == args.default_config:
+                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)
+
+    for root, _, files in os.walk('@efiSysMountPoint@/efi/nixos/.extra-files', topdown=False):
+        relative_root = root.removeprefix("@efiSysMountPoint@/efi/nixos/.extra-files").removeprefix("/")
+        actual_root = os.path.join("@efiSysMountPoint@", relative_root)
+
+        for file in files:
+            actual_file = os.path.join(actual_root, file)
+
+            if os.path.exists(actual_file):
+                os.unlink(actual_file)
+            os.unlink(os.path.join(root, file))
+
+        if not len(os.listdir(actual_root)):
+            os.rmdir(actual_root)
+        os.rmdir(root)
+
+    mkdir_p("@efiSysMountPoint@/efi/nixos/.extra-files")
+
+    subprocess.check_call("@copyExtraFiles@")
+
+    # Since fat32 provides little recovery facilities after a crash,
+    # it can leave the system in an unbootable state, when a crash/outage
+    # happens shortly after an update. To decrease the likelihood of this
+    # event sync the efi filesystem after each update.
+    rc = libc.syncfs(os.open("@efiSysMountPoint@", os.O_RDONLY))
+    if rc != 0:
+        print("could not sync @efiSysMountPoint@: {}".format(os.strerror(rc)), file=sys.stderr)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
new file mode 100644
index 00000000000..c07567ec82e
--- /dev/null
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
@@ -0,0 +1,303 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.boot.loader.systemd-boot;
+
+  efi = config.boot.loader.efi;
+
+  systemdBootBuilder = pkgs.substituteAll {
+    src = ./systemd-boot-builder.py;
+
+    isExecutable = true;
+
+    inherit (pkgs) python3;
+
+    systemd = config.systemd.package;
+
+    nix = config.nix.package.out;
+
+    timeout = if config.boot.loader.timeout != null then config.boot.loader.timeout else "";
+
+    editor = if cfg.editor then "True" else "False";
+
+    configurationLimit = if cfg.configurationLimit == null then 0 else cfg.configurationLimit;
+
+    inherit (cfg) consoleMode graceful;
+
+    inherit (efi) efiSysMountPoint canTouchEfiVariables;
+
+    memtest86 = if cfg.memtest86.enable then pkgs.memtest86-efi else "";
+
+    netbootxyz = if cfg.netbootxyz.enable then pkgs.netbootxyz-efi else "";
+
+    copyExtraFiles = pkgs.writeShellScript "copy-extra-files" ''
+      empty_file=$(mktemp)
+
+      ${concatStrings (mapAttrsToList (n: v: ''
+        ${pkgs.coreutils}/bin/install -Dp "${v}" "${efi.efiSysMountPoint}/"${escapeShellArg n}
+        ${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/"${escapeShellArg n}
+      '') cfg.extraFiles)}
+
+      ${concatStrings (mapAttrsToList (n: v: ''
+        ${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${efi.efiSysMountPoint}/loader/entries/"${escapeShellArg n}
+        ${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/loader/entries/"${escapeShellArg n}
+      '') cfg.extraEntries)}
+    '';
+  };
+
+  checkedSystemdBootBuilder = pkgs.runCommand "systemd-boot" {
+    nativeBuildInputs = [ pkgs.mypy ];
+  } ''
+    install -m755 ${systemdBootBuilder} $out
+    mypy \
+      --no-implicit-optional \
+      --disallow-untyped-calls \
+      --disallow-untyped-defs \
+      $out
+  '';
+in {
+
+  imports =
+    [ (mkRenamedOptionModule [ "boot" "loader" "gummiboot" "enable" ] [ "boot" "loader" "systemd-boot" "enable" ])
+    ];
+
+  options.boot.loader.systemd-boot = {
+    enable = mkOption {
+      default = false;
+
+      type = types.bool;
+
+      description = "Whether to enable the systemd-boot (formerly gummiboot) EFI boot manager";
+    };
+
+    editor = mkOption {
+      default = true;
+
+      type = types.bool;
+
+      description = ''
+        Whether to allow editing the kernel command-line before
+        boot. It is recommended to set this to false, as it allows
+        gaining root access by passing init=/bin/sh as a kernel
+        parameter. However, it is enabled by default for backwards
+        compatibility.
+      '';
+    };
+
+    configurationLimit = mkOption {
+      default = null;
+      example = 120;
+      type = types.nullOr types.int;
+      description = ''
+        Maximum number of latest generations in the boot menu.
+        Useful to prevent boot partition running out of disk space.
+
+        <literal>null</literal> means no limit i.e. all generations
+        that were not garbage collected yet.
+      '';
+    };
+
+    consoleMode = mkOption {
+      default = "keep";
+
+      type = types.enum [ "0" "1" "2" "auto" "max" "keep" ];
+
+      description = ''
+        The resolution of the console. The following values are valid:
+
+        <itemizedlist>
+          <listitem><para>
+            <literal>"0"</literal>: Standard UEFI 80x25 mode
+          </para></listitem>
+          <listitem><para>
+            <literal>"1"</literal>: 80x50 mode, not supported by all devices
+          </para></listitem>
+          <listitem><para>
+            <literal>"2"</literal>: The first non-standard mode provided by the device firmware, if any
+          </para></listitem>
+          <listitem><para>
+            <literal>"auto"</literal>: Pick a suitable mode automatically using heuristics
+          </para></listitem>
+          <listitem><para>
+            <literal>"max"</literal>: Pick the highest-numbered available mode
+          </para></listitem>
+          <listitem><para>
+            <literal>"keep"</literal>: Keep the mode selected by firmware (the default)
+          </para></listitem>
+        </itemizedlist>
+      '';
+    };
+
+    memtest86 = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Make MemTest86 available from the systemd-boot menu. MemTest86 is a
+          program for testing memory.  MemTest86 is an unfree program, so
+          this requires <literal>allowUnfree</literal> to be set to
+          <literal>true</literal>.
+        '';
+      };
+
+      entryFilename = mkOption {
+        default = "memtest86.conf";
+        type = types.str;
+        description = ''
+          <literal>systemd-boot</literal> orders the menu entries by the config file names,
+          so if you want something to appear after all the NixOS entries,
+          it should start with <filename>o</filename> or onwards.
+        '';
+      };
+    };
+
+    netbootxyz = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Make <literal>netboot.xyz</literal> available from the
+          <literal>systemd-boot</literal> menu. <literal>netboot.xyz</literal>
+          is a menu system that allows you to boot OS installers and
+          utilities over the network.
+        '';
+      };
+
+      entryFilename = mkOption {
+        default = "o_netbootxyz.conf";
+        type = types.str;
+        description = ''
+          <literal>systemd-boot</literal> orders the menu entries by the config file names,
+          so if you want something to appear after all the NixOS entries,
+          it should start with <filename>o</filename> or onwards.
+        '';
+      };
+    };
+
+    extraEntries = mkOption {
+      type = types.attrsOf types.lines;
+      default = {};
+      example = literalExpression ''
+        { "memtest86.conf" = '''
+          title MemTest86
+          efi /efi/memtest86/memtest86.efi
+        '''; }
+      '';
+      description = ''
+        Any additional entries you want added to the <literal>systemd-boot</literal> menu.
+        These entries will be copied to <filename>/boot/loader/entries</filename>.
+        Each attribute name denotes the destination file name,
+        and the corresponding attribute value is the contents of the entry.
+
+        <literal>systemd-boot</literal> orders the menu entries by the config file names,
+        so if you want something to appear after all the NixOS entries,
+        it should start with <filename>o</filename> or onwards.
+      '';
+    };
+
+    extraFiles = mkOption {
+      type = types.attrsOf types.path;
+      default = {};
+      example = literalExpression ''
+        { "efi/memtest86/memtest86.efi" = "''${pkgs.memtest86-efi}/BOOTX64.efi"; }
+      '';
+      description = ''
+        A set of files to be copied to <filename>/boot</filename>.
+        Each attribute name denotes the destination file name in
+        <filename>/boot</filename>, while the corresponding
+        attribute value specifies the source file.
+      '';
+    };
+
+    graceful = mkOption {
+      default = false;
+
+      type = types.bool;
+
+      description = ''
+        Invoke <literal>bootctl install</literal> with the <literal>--graceful</literal> option,
+        which ignores errors when EFI variables cannot be written or when the EFI System Partition
+        cannot be found. Currently only applies to random seed operations.
+
+        Only enable this option if <literal>systemd-boot</literal> otherwise fails to install, as the
+        scope or implication of the <literal>--graceful</literal> option may change in the future.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub;
+        message = "This kernel does not support the EFI boot stub";
+      }
+    ] ++ concatMap (filename: [
+      {
+        assertion = !(hasInfix "/" filename);
+        message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries within folders are not supported";
+      }
+      {
+        assertion = hasSuffix ".conf" filename;
+        message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries must have a .conf file extension";
+      }
+    ]) (builtins.attrNames cfg.extraEntries)
+      ++ concatMap (filename: [
+        {
+          assertion = !(hasPrefix "/" filename);
+          message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: paths must not begin with a slash";
+        }
+        {
+          assertion = !(hasInfix ".." filename);
+          message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: paths must not reference the parent directory";
+        }
+        {
+          assertion = !(hasInfix "nixos/.extra-files" (toLower filename));
+          message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: files cannot be placed in the nixos/.extra-files directory";
+        }
+      ]) (builtins.attrNames cfg.extraFiles);
+
+    boot.loader.grub.enable = mkDefault false;
+
+    boot.loader.supportsInitrdSecrets = true;
+
+    boot.loader.systemd-boot.extraFiles = mkMerge [
+      # TODO: This is hard-coded to use the 64-bit EFI app, but it could probably
+      # be updated to use the 32-bit EFI app on 32-bit systems.  The 32-bit EFI
+      # app filename is BOOTIA32.efi.
+      (mkIf cfg.memtest86.enable {
+        "efi/memtest86/BOOTX64.efi" = "${pkgs.memtest86-efi}/BOOTX64.efi";
+      })
+      (mkIf cfg.netbootxyz.enable {
+        "efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}";
+      })
+    ];
+
+    boot.loader.systemd-boot.extraEntries = mkMerge [
+      (mkIf cfg.memtest86.enable {
+        "${cfg.memtest86.entryFilename}" = ''
+          title  MemTest86
+          efi    /efi/memtest86/BOOTX64.efi
+        '';
+      })
+      (mkIf cfg.netbootxyz.enable {
+        "${cfg.netbootxyz.entryFilename}" = ''
+          title  netboot.xyz
+          efi    /efi/netbootxyz/netboot.xyz.efi
+        '';
+      })
+    ];
+
+    system = {
+      build.installBootLoader = checkedSystemdBootBuilder;
+
+      boot.loader.id = "systemd-boot";
+
+      requiredKernelConfig = with config.lib.kernelConfig; [
+        (isYes "EFI_STUB")
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix
new file mode 100644
index 00000000000..f0d3170dc5a
--- /dev/null
+++ b/nixos/modules/system/boot/luksroot.nix
@@ -0,0 +1,941 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  luks = config.boot.initrd.luks;
+  kernelPackages = config.boot.kernelPackages;
+
+  commonFunctions = ''
+    die() {
+        echo "$@" >&2
+        exit 1
+    }
+
+    dev_exist() {
+        local target="$1"
+        if [ -e $target ]; then
+            return 0
+        else
+            local uuid=$(echo -n $target | sed -e 's,UUID=\(.*\),\1,g')
+            blkid --uuid $uuid >/dev/null
+            return $?
+        fi
+    }
+
+    wait_target() {
+        local name="$1"
+        local target="$2"
+        local secs="''${3:-10}"
+        local desc="''${4:-$name $target to appear}"
+
+        if ! dev_exist $target; then
+            echo -n "Waiting $secs seconds for $desc..."
+            local success=false;
+            for try in $(seq $secs); do
+                echo -n "."
+                sleep 1
+                if dev_exist $target; then
+                    success=true
+                    break
+                fi
+            done
+            if [ $success == true ]; then
+                echo " - success";
+                return 0
+            else
+                echo " - failure";
+                return 1
+            fi
+        fi
+        return 0
+    }
+
+    wait_yubikey() {
+        local secs="''${1:-10}"
+
+        ykinfo -v 1>/dev/null 2>&1
+        if [ $? != 0 ]; then
+            echo -n "Waiting $secs seconds for YubiKey to appear..."
+            local success=false
+            for try in $(seq $secs); do
+                echo -n .
+                sleep 1
+                ykinfo -v 1>/dev/null 2>&1
+                if [ $? == 0 ]; then
+                    success=true
+                    break
+                fi
+            done
+            if [ $success == true ]; then
+                echo " - success";
+                return 0
+            else
+                echo " - failure";
+                return 1
+            fi
+        fi
+        return 0
+    }
+
+    wait_gpgcard() {
+        local secs="''${1:-10}"
+
+        gpg --card-status > /dev/null 2> /dev/null
+        if [ $? != 0 ]; then
+            echo -n "Waiting $secs seconds for GPG Card to appear"
+            local success=false
+            for try in $(seq $secs); do
+                echo -n .
+                sleep 1
+                gpg --card-status > /dev/null 2> /dev/null
+                if [ $? == 0 ]; then
+                    success=true
+                    break
+                fi
+            done
+            if [ $success == true ]; then
+                echo " - success";
+                return 0
+            else
+                echo " - failure";
+                return 1
+            fi
+        fi
+        return 0
+    }
+  '';
+
+  preCommands = ''
+    # A place to store crypto things
+
+    # A ramfs is used here to ensure that the file used to update
+    # the key slot with cryptsetup will never get swapped out.
+    # Warning: Do NOT replace with tmpfs!
+    mkdir -p /crypt-ramfs
+    mount -t ramfs none /crypt-ramfs
+
+    # Cryptsetup locking directory
+    mkdir -p /run/cryptsetup
+
+    # For YubiKey salt storage
+    mkdir -p /crypt-storage
+
+    ${optionalString luks.gpgSupport ''
+    export GPG_TTY=$(tty)
+    export GNUPGHOME=/crypt-ramfs/.gnupg
+
+    gpg-agent --daemon --scdaemon-program $out/bin/scdaemon > /dev/null 2> /dev/null
+    ''}
+
+    # Disable all input echo for the whole stage. We could use read -s
+    # instead but that would ocasionally leak characters between read
+    # invocations.
+    stty -echo
+  '';
+
+  postCommands = ''
+    stty echo
+    umount /crypt-storage 2>/dev/null
+    umount /crypt-ramfs 2>/dev/null
+  '';
+
+  openCommand = name: dev: assert name == dev.name;
+  let
+    csopen = "cryptsetup luksOpen ${dev.device} ${dev.name}"
+           + optionalString dev.allowDiscards " --allow-discards"
+           + optionalString dev.bypassWorkqueues " --perf-no_read_workqueue --perf-no_write_workqueue"
+           + optionalString (dev.header != null) " --header=${dev.header}";
+    cschange = "cryptsetup luksChangeKey ${dev.device} ${optionalString (dev.header != null) "--header=${dev.header}"}";
+  in ''
+    # Wait for luksRoot (and optionally keyFile and/or header) to appear, e.g.
+    # if on a USB drive.
+    wait_target "device" ${dev.device} || die "${dev.device} is unavailable"
+
+    ${optionalString (dev.header != null) ''
+      wait_target "header" ${dev.header} || die "${dev.header} is unavailable"
+    ''}
+
+    do_open_passphrase() {
+        local passphrase
+
+        while true; do
+            echo -n "Passphrase for ${dev.device}: "
+            passphrase=
+            while true; do
+                if [ -e /crypt-ramfs/passphrase ]; then
+                    echo "reused"
+                    passphrase=$(cat /crypt-ramfs/passphrase)
+                    break
+                else
+                    # ask cryptsetup-askpass
+                    echo -n "${dev.device}" > /crypt-ramfs/device
+
+                    # and try reading it from /dev/console with a timeout
+                    IFS= read -t 1 -r passphrase
+                    if [ -n "$passphrase" ]; then
+                       ${if luks.reusePassphrases then ''
+                         # remember it for the next device
+                         echo -n "$passphrase" > /crypt-ramfs/passphrase
+                       '' else ''
+                         # Don't save it to ramfs. We are very paranoid
+                       ''}
+                       echo
+                       break
+                    fi
+                fi
+            done
+            echo -n "Verifying passphrase for ${dev.device}..."
+            echo -n "$passphrase" | ${csopen} --key-file=-
+            if [ $? == 0 ]; then
+                echo " - success"
+                ${if luks.reusePassphrases then ''
+                  # we don't rm here because we might reuse it for the next device
+                '' else ''
+                  rm -f /crypt-ramfs/passphrase
+                ''}
+                break
+            else
+                echo " - failure"
+                # ask for a different one
+                rm -f /crypt-ramfs/passphrase
+            fi
+        done
+    }
+
+    # LUKS
+    open_normally() {
+        ${if (dev.keyFile != null) then ''
+        if wait_target "key file" ${dev.keyFile}; then
+            ${csopen} --key-file=${dev.keyFile} \
+              ${optionalString (dev.keyFileSize != null) "--keyfile-size=${toString dev.keyFileSize}"} \
+              ${optionalString (dev.keyFileOffset != null) "--keyfile-offset=${toString dev.keyFileOffset}"}
+        else
+            ${if dev.fallbackToPassword then "echo" else "die"} "${dev.keyFile} is unavailable"
+            echo " - failing back to interactive password prompt"
+            do_open_passphrase
+        fi
+        '' else ''
+        do_open_passphrase
+        ''}
+    }
+
+    ${optionalString (luks.yubikeySupport && (dev.yubikey != null)) ''
+    # YubiKey
+    rbtohex() {
+        ( od -An -vtx1 | tr -d ' \n' )
+    }
+
+    hextorb() {
+        ( tr '[:lower:]' '[:upper:]' | sed -e 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/gI' | xargs printf )
+    }
+
+    do_open_yubikey() {
+        # Make all of these local to this function
+        # to prevent their values being leaked
+        local salt
+        local iterations
+        local k_user
+        local challenge
+        local response
+        local k_luks
+        local opened
+        local new_salt
+        local new_iterations
+        local new_challenge
+        local new_response
+        local new_k_luks
+
+        mount -t ${dev.yubikey.storage.fsType} ${dev.yubikey.storage.device} /crypt-storage || \
+          die "Failed to mount YubiKey salt storage device"
+
+        salt="$(cat /crypt-storage${dev.yubikey.storage.path} | sed -n 1p | tr -d '\n')"
+        iterations="$(cat /crypt-storage${dev.yubikey.storage.path} | sed -n 2p | tr -d '\n')"
+        challenge="$(echo -n $salt | openssl-wrap dgst -binary -sha512 | rbtohex)"
+        response="$(ykchalresp -${toString dev.yubikey.slot} -x $challenge 2>/dev/null)"
+
+        for try in $(seq 3); do
+            ${optionalString dev.yubikey.twoFactor ''
+            echo -n "Enter two-factor passphrase: "
+            k_user=
+            while true; do
+                if [ -e /crypt-ramfs/passphrase ]; then
+                    echo "reused"
+                    k_user=$(cat /crypt-ramfs/passphrase)
+                    break
+                else
+                    # Try reading it from /dev/console with a timeout
+                    IFS= read -t 1 -r k_user
+                    if [ -n "$k_user" ]; then
+                       ${if luks.reusePassphrases then ''
+                         # Remember it for the next device
+                         echo -n "$k_user" > /crypt-ramfs/passphrase
+                       '' else ''
+                         # Don't save it to ramfs. We are very paranoid
+                       ''}
+                       echo
+                       break
+                    fi
+                fi
+            done
+            ''}
+
+            if [ ! -z "$k_user" ]; then
+                k_luks="$(echo -n $k_user | pbkdf2-sha512 ${toString dev.yubikey.keyLength} $iterations $response | rbtohex)"
+            else
+                k_luks="$(echo | pbkdf2-sha512 ${toString dev.yubikey.keyLength} $iterations $response | rbtohex)"
+            fi
+
+            echo -n "$k_luks" | hextorb | ${csopen} --key-file=-
+
+            if [ $? == 0 ]; then
+                opened=true
+                ${if luks.reusePassphrases then ''
+                  # We don't rm here because we might reuse it for the next device
+                '' else ''
+                  rm -f /crypt-ramfs/passphrase
+                ''}
+                break
+            else
+                opened=false
+                echo "Authentication failed!"
+            fi
+        done
+
+        [ "$opened" == false ] && die "Maximum authentication errors reached"
+
+        echo -n "Gathering entropy for new salt (please enter random keys to generate entropy if this blocks for long)..."
+        for i in $(seq ${toString dev.yubikey.saltLength}); do
+            byte="$(dd if=/dev/random bs=1 count=1 2>/dev/null | rbtohex)";
+            new_salt="$new_salt$byte";
+            echo -n .
+        done;
+        echo "ok"
+
+        new_iterations="$iterations"
+        ${optionalString (dev.yubikey.iterationStep > 0) ''
+        new_iterations="$(($new_iterations + ${toString dev.yubikey.iterationStep}))"
+        ''}
+
+        new_challenge="$(echo -n $new_salt | openssl-wrap dgst -binary -sha512 | rbtohex)"
+
+        new_response="$(ykchalresp -${toString dev.yubikey.slot} -x $new_challenge 2>/dev/null)"
+
+        if [ ! -z "$k_user" ]; then
+            new_k_luks="$(echo -n $k_user | pbkdf2-sha512 ${toString dev.yubikey.keyLength} $new_iterations $new_response | rbtohex)"
+        else
+            new_k_luks="$(echo | pbkdf2-sha512 ${toString dev.yubikey.keyLength} $new_iterations $new_response | rbtohex)"
+        fi
+
+        echo -n "$new_k_luks" | hextorb > /crypt-ramfs/new_key
+        echo -n "$k_luks" | hextorb | ${cschange} --key-file=- /crypt-ramfs/new_key
+
+        if [ $? == 0 ]; then
+            echo -ne "$new_salt\n$new_iterations" > /crypt-storage${dev.yubikey.storage.path}
+            sync /crypt-storage${dev.yubikey.storage.path}
+        else
+            echo "Warning: Could not update LUKS key, current challenge persists!"
+        fi
+
+        rm -f /crypt-ramfs/new_key
+        umount /crypt-storage
+    }
+
+    open_with_hardware() {
+        if wait_yubikey ${toString dev.yubikey.gracePeriod}; then
+            do_open_yubikey
+        else
+            echo "No YubiKey found, falling back to non-YubiKey open procedure"
+            open_normally
+        fi
+    }
+    ''}
+
+    ${optionalString (luks.gpgSupport && (dev.gpgCard != null)) ''
+
+    do_open_gpg_card() {
+        # Make all of these local to this function
+        # to prevent their values being leaked
+        local pin
+        local opened
+
+        gpg --import /gpg-keys/${dev.device}/pubkey.asc > /dev/null 2> /dev/null
+
+        gpg --card-status > /dev/null 2> /dev/null
+
+        for try in $(seq 3); do
+            echo -n "PIN for GPG Card associated with device ${dev.device}: "
+            pin=
+            while true; do
+                if [ -e /crypt-ramfs/passphrase ]; then
+                    echo "reused"
+                    pin=$(cat /crypt-ramfs/passphrase)
+                    break
+                else
+                    # and try reading it from /dev/console with a timeout
+                    IFS= read -t 1 -r pin
+                    if [ -n "$pin" ]; then
+                       ${if luks.reusePassphrases then ''
+                         # remember it for the next device
+                         echo -n "$pin" > /crypt-ramfs/passphrase
+                       '' else ''
+                         # Don't save it to ramfs. We are very paranoid
+                       ''}
+                       echo
+                       break
+                    fi
+                fi
+            done
+            echo -n "Verifying passphrase for ${dev.device}..."
+            echo -n "$pin" | gpg -q --batch --passphrase-fd 0 --pinentry-mode loopback -d /gpg-keys/${dev.device}/cryptkey.gpg 2> /dev/null | ${csopen} --key-file=- > /dev/null 2> /dev/null
+            if [ $? == 0 ]; then
+                echo " - success"
+                ${if luks.reusePassphrases then ''
+                  # we don't rm here because we might reuse it for the next device
+                '' else ''
+                  rm -f /crypt-ramfs/passphrase
+                ''}
+                break
+            else
+                echo " - failure"
+                # ask for a different one
+                rm -f /crypt-ramfs/passphrase
+            fi
+        done
+
+        [ "$opened" == false ] && die "Maximum authentication errors reached"
+    }
+
+    open_with_hardware() {
+        if wait_gpgcard ${toString dev.gpgCard.gracePeriod}; then
+            do_open_gpg_card
+        else
+            echo "No GPG Card found, falling back to normal open procedure"
+            open_normally
+        fi
+    }
+    ''}
+
+    ${optionalString (luks.fido2Support && (dev.fido2.credential != null)) ''
+
+    open_with_hardware() {
+      local passsphrase
+
+        ${if dev.fido2.passwordLess then ''
+          export passphrase=""
+        '' else ''
+          read -rsp "FIDO2 salt for ${dev.device}: " passphrase
+          echo
+        ''}
+        ${optionalString (lib.versionOlder kernelPackages.kernel.version "5.4") ''
+          echo "On systems with Linux Kernel < 5.4, it might take a while to initialize the CRNG, you might want to use linuxPackages_latest."
+          echo "Please move your mouse to create needed randomness."
+        ''}
+          echo "Waiting for your FIDO2 device..."
+          fido2luks open ${dev.device} ${dev.name} ${dev.fido2.credential} --await-dev ${toString dev.fido2.gracePeriod} --salt string:$passphrase
+        if [ $? -ne 0 ]; then
+          echo "No FIDO2 key found, falling back to normal open procedure"
+          open_normally
+        fi
+    }
+    ''}
+
+    # commands to run right before we mount our device
+    ${dev.preOpenCommands}
+
+    ${if (luks.yubikeySupport && (dev.yubikey != null)) || (luks.gpgSupport && (dev.gpgCard != null)) || (luks.fido2Support && (dev.fido2.credential != null)) then ''
+    open_with_hardware
+    '' else ''
+    open_normally
+    ''}
+
+    # commands to run right after we mounted our device
+    ${dev.postOpenCommands}
+  '';
+
+  askPass = pkgs.writeScriptBin "cryptsetup-askpass" ''
+    #!/bin/sh
+
+    ${commonFunctions}
+
+    while true; do
+        wait_target "luks" /crypt-ramfs/device 10 "LUKS to request a passphrase" || die "Passphrase is not requested now"
+        device=$(cat /crypt-ramfs/device)
+
+        echo -n "Passphrase for $device: "
+        IFS= read -rs passphrase
+        echo
+
+        rm /crypt-ramfs/device
+        echo -n "$passphrase" > /crypt-ramfs/passphrase
+    done
+  '';
+
+  preLVM = filterAttrs (n: v: v.preLVM) luks.devices;
+  postLVM = filterAttrs (n: v: !v.preLVM) luks.devices;
+
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "boot" "initrd" "luks" "enable" ] "")
+  ];
+
+  options = {
+
+    boot.initrd.luks.mitigateDMAAttacks = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Unless enabled, encryption keys can be easily recovered by an attacker with physical
+        access to any machine with PCMCIA, ExpressCard, ThunderBolt or FireWire port.
+        More information is available at <link xlink:href="http://en.wikipedia.org/wiki/DMA_attack"/>.
+
+        This option blacklists FireWire drivers, but doesn't remove them. You can manually
+        load the drivers if you need to use a FireWire device, but don't forget to unload them!
+      '';
+    };
+
+    boot.initrd.luks.cryptoModules = mkOption {
+      type = types.listOf types.str;
+      default =
+        [ "aes" "aes_generic" "blowfish" "twofish"
+          "serpent" "cbc" "xts" "lrw" "sha1" "sha256" "sha512"
+          "af_alg" "algif_skcipher"
+        ];
+      description = ''
+        A list of cryptographic kernel modules needed to decrypt the root device(s).
+        The default includes all common modules.
+      '';
+    };
+
+    boot.initrd.luks.forceLuksSupportInInitrd = mkOption {
+      type = types.bool;
+      default = false;
+      internal = true;
+      description = ''
+        Whether to configure luks support in the initrd, when no luks
+        devices are configured.
+      '';
+    };
+
+    boot.initrd.luks.reusePassphrases = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        When opening a new LUKS device try reusing last successful
+        passphrase.
+
+        Useful for mounting a number of devices that use the same
+        passphrase without retyping it several times.
+
+        Such setup can be useful if you use <command>cryptsetup
+        luksSuspend</command>. Different LUKS devices will still have
+        different master keys even when using the same passphrase.
+      '';
+    };
+
+    boot.initrd.luks.devices = mkOption {
+      default = { };
+      example = { luksroot.device = "/dev/disk/by-uuid/430e9eff-d852-4f68-aa3b-2fa3599ebe08"; };
+      description = ''
+        The encrypted disk that should be opened before the root
+        filesystem is mounted. Both LVM-over-LUKS and LUKS-over-LVM
+        setups are supported. The unencrypted devices can be accessed as
+        <filename>/dev/mapper/<replaceable>name</replaceable></filename>.
+      '';
+
+      type = with types; attrsOf (submodule (
+        { name, ... }: { options = {
+
+          name = mkOption {
+            visible = false;
+            default = name;
+            example = "luksroot";
+            type = types.str;
+            description = "Name of the unencrypted device in <filename>/dev/mapper</filename>.";
+          };
+
+          device = mkOption {
+            example = "/dev/disk/by-uuid/430e9eff-d852-4f68-aa3b-2fa3599ebe08";
+            type = types.str;
+            description = "Path of the underlying encrypted block device.";
+          };
+
+          header = mkOption {
+            default = null;
+            example = "/root/header.img";
+            type = types.nullOr types.str;
+            description = ''
+              The name of the file or block device that
+              should be used as header for the encrypted device.
+            '';
+          };
+
+          keyFile = mkOption {
+            default = null;
+            example = "/dev/sdb1";
+            type = types.nullOr types.str;
+            description = ''
+              The name of the file (can be a raw device or a partition) that
+              should be used as the decryption key for the encrypted device. If
+              not specified, you will be prompted for a passphrase instead.
+            '';
+          };
+
+          keyFileSize = mkOption {
+            default = null;
+            example = 4096;
+            type = types.nullOr types.int;
+            description = ''
+              The size of the key file. Use this if only the beginning of the
+              key file should be used as a key (often the case if a raw device
+              or partition is used as key file). If not specified, the whole
+              <literal>keyFile</literal> will be used decryption, instead of just
+              the first <literal>keyFileSize</literal> bytes.
+            '';
+          };
+
+          keyFileOffset = mkOption {
+            default = null;
+            example = 4096;
+            type = types.nullOr types.int;
+            description = ''
+              The offset of the key file. Use this in combination with
+              <literal>keyFileSize</literal> to use part of a file as key file
+              (often the case if a raw device or partition is used as a key file).
+              If not specified, the key begins at the first byte of
+              <literal>keyFile</literal>.
+            '';
+          };
+
+          # FIXME: get rid of this option.
+          preLVM = mkOption {
+            default = true;
+            type = types.bool;
+            description = "Whether the luksOpen will be attempted before LVM scan or after it.";
+          };
+
+          allowDiscards = mkOption {
+            default = false;
+            type = types.bool;
+            description = ''
+              Whether to allow TRIM requests to the underlying device. This option
+              has security implications; please read the LUKS documentation before
+              activating it.
+              This option is incompatible with authenticated encryption (dm-crypt
+              stacked over dm-integrity).
+            '';
+          };
+
+          bypassWorkqueues = mkOption {
+            default = false;
+            type = types.bool;
+            description = ''
+              Whether to bypass dm-crypt's internal read and write workqueues.
+              Enabling this should improve performance on SSDs; see
+              <link xlink:href="https://wiki.archlinux.org/index.php/Dm-crypt/Specialties#Disable_workqueue_for_increased_solid_state_drive_(SSD)_performance">here</link>
+              for more information. Needs Linux 5.9 or later.
+            '';
+          };
+
+          fallbackToPassword = mkOption {
+            default = false;
+            type = types.bool;
+            description = ''
+              Whether to fallback to interactive passphrase prompt if the keyfile
+              cannot be found. This will prevent unattended boot should the keyfile
+              go missing.
+            '';
+          };
+
+          gpgCard = mkOption {
+            default = null;
+            description = ''
+              The option to use this LUKS device with a GPG encrypted luks password by the GPG Smartcard.
+              If null (the default), GPG-Smartcard will be disabled for this device.
+            '';
+
+            type = with types; nullOr (submodule {
+              options = {
+                gracePeriod = mkOption {
+                  default = 10;
+                  type = types.int;
+                  description = "Time in seconds to wait for the GPG Smartcard.";
+                };
+
+                encryptedPass = mkOption {
+                  type = types.path;
+                  description = "Path to the GPG encrypted passphrase.";
+                };
+
+                publicKey = mkOption {
+                  type = types.path;
+                  description = "Path to the Public Key.";
+                };
+              };
+            });
+          };
+
+          fido2 = {
+            credential = mkOption {
+              default = null;
+              example = "f1d00200d8dc783f7fb1e10ace8da27f8312d72692abfca2f7e4960a73f48e82e1f7571f6ebfcee9fb434f9886ccc8fcc52a6614d8d2";
+              type = types.nullOr types.str;
+              description = "The FIDO2 credential ID.";
+            };
+
+            gracePeriod = mkOption {
+              default = 10;
+              type = types.int;
+              description = "Time in seconds to wait for the FIDO2 key.";
+            };
+
+            passwordLess = mkOption {
+              default = false;
+              type = types.bool;
+              description = ''
+                Defines whatever to use an empty string as a default salt.
+
+                Enable only when your device is PIN protected, such as <link xlink:href="https://trezor.io/">Trezor</link>.
+              '';
+            };
+          };
+
+          yubikey = mkOption {
+            default = null;
+            description = ''
+              The options to use for this LUKS device in YubiKey-PBA.
+              If null (the default), YubiKey-PBA will be disabled for this device.
+            '';
+
+            type = with types; nullOr (submodule {
+              options = {
+                twoFactor = mkOption {
+                  default = true;
+                  type = types.bool;
+                  description = "Whether to use a passphrase and a YubiKey (true), or only a YubiKey (false).";
+                };
+
+                slot = mkOption {
+                  default = 2;
+                  type = types.int;
+                  description = "Which slot on the YubiKey to challenge.";
+                };
+
+                saltLength = mkOption {
+                  default = 16;
+                  type = types.int;
+                  description = "Length of the new salt in byte (64 is the effective maximum).";
+                };
+
+                keyLength = mkOption {
+                  default = 64;
+                  type = types.int;
+                  description = "Length of the LUKS slot key derived with PBKDF2 in byte.";
+                };
+
+                iterationStep = mkOption {
+                  default = 0;
+                  type = types.int;
+                  description = "How much the iteration count for PBKDF2 is increased at each successful authentication.";
+                };
+
+                gracePeriod = mkOption {
+                  default = 10;
+                  type = types.int;
+                  description = "Time in seconds to wait for the YubiKey.";
+                };
+
+                /* TODO: Add to the documentation of the current module:
+
+                   Options related to the storing the salt.
+                */
+                storage = {
+                  device = mkOption {
+                    default = "/dev/sda1";
+                    type = types.path;
+                    description = ''
+                      An unencrypted device that will temporarily be mounted in stage-1.
+                      Must contain the current salt to create the challenge for this LUKS device.
+                    '';
+                  };
+
+                  fsType = mkOption {
+                    default = "vfat";
+                    type = types.str;
+                    description = "The filesystem of the unencrypted device.";
+                  };
+
+                  path = mkOption {
+                    default = "/crypt-storage/default";
+                    type = types.str;
+                    description = ''
+                      Absolute path of the salt on the unencrypted device with
+                      that device's root directory as "/".
+                    '';
+                  };
+                };
+              };
+            });
+          };
+
+          preOpenCommands = mkOption {
+            type = types.lines;
+            default = "";
+            example = ''
+              mkdir -p /tmp/persistent
+              mount -t zfs rpool/safe/persistent /tmp/persistent
+            '';
+            description = ''
+              Commands that should be run right before we try to mount our LUKS device.
+              This can be useful, if the keys needed to open the drive is on another partion.
+            '';
+          };
+
+          postOpenCommands = mkOption {
+            type = types.lines;
+            default = "";
+            example = ''
+              umount /tmp/persistent
+            '';
+            description = ''
+              Commands that should be run right after we have mounted our LUKS device.
+            '';
+          };
+        };
+      }));
+    };
+
+    boot.initrd.luks.gpgSupport = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Enables support for authenticating with a GPG encrypted password.
+      '';
+    };
+
+    boot.initrd.luks.yubikeySupport = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+            Enables support for authenticating with a YubiKey on LUKS devices.
+            See the NixOS wiki for information on how to properly setup a LUKS device
+            and a YubiKey to work with this feature.
+          '';
+    };
+
+    boot.initrd.luks.fido2Support = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Enables support for authenticating with FIDO2 devices.
+      '';
+    };
+
+  };
+
+  config = mkIf (luks.devices != {} || luks.forceLuksSupportInInitrd) {
+
+    assertions =
+      [ { assertion = !(luks.gpgSupport && luks.yubikeySupport);
+          message = "YubiKey and GPG Card may not be used at the same time.";
+        }
+
+        { assertion = !(luks.gpgSupport && luks.fido2Support);
+          message = "FIDO2 and GPG Card may not be used at the same time.";
+        }
+
+        { assertion = !(luks.fido2Support && luks.yubikeySupport);
+          message = "FIDO2 and YubiKey may not be used at the same time.";
+        }
+
+        { assertion = any (dev: dev.bypassWorkqueues) (attrValues luks.devices)
+                      -> versionAtLeast kernelPackages.kernel.version "5.9";
+          message = "boot.initrd.luks.devices.<name>.bypassWorkqueues is not supported for kernels older than 5.9";
+        }
+      ];
+
+    # actually, sbp2 driver is the one enabling the DMA attack, but this needs to be tested
+    boot.blacklistedKernelModules = optionals luks.mitigateDMAAttacks
+      ["firewire_ohci" "firewire_core" "firewire_sbp2"];
+
+    # Some modules that may be needed for mounting anything ciphered
+    boot.initrd.availableKernelModules = [ "dm_mod" "dm_crypt" "cryptd" "input_leds" ]
+      ++ luks.cryptoModules
+      # workaround until https://marc.info/?l=linux-crypto-vger&m=148783562211457&w=4 is merged
+      # remove once 'modprobe --show-depends xts' shows ecb as a dependency
+      ++ (if builtins.elem "xts" luks.cryptoModules then ["ecb"] else []);
+
+    # copy the cryptsetup binary and it's dependencies
+    boot.initrd.extraUtilsCommands = ''
+      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
+
+      ${optionalString luks.yubikeySupport ''
+        copy_bin_and_libs ${pkgs.yubikey-personalization}/bin/ykchalresp
+        copy_bin_and_libs ${pkgs.yubikey-personalization}/bin/ykinfo
+        copy_bin_and_libs ${pkgs.openssl.bin}/bin/openssl
+
+        cc -O3 -I${pkgs.openssl.dev}/include -L${pkgs.openssl.out}/lib ${./pbkdf2-sha512.c} -o pbkdf2-sha512 -lcrypto
+        strip -s pbkdf2-sha512
+        copy_bin_and_libs pbkdf2-sha512
+
+        mkdir -p $out/etc/ssl
+        cp -pdv ${pkgs.openssl.out}/etc/ssl/openssl.cnf $out/etc/ssl
+
+        cat > $out/bin/openssl-wrap <<EOF
+        #!$out/bin/sh
+        export OPENSSL_CONF=$out/etc/ssl/openssl.cnf
+        $out/bin/openssl "\$@"
+        EOF
+        chmod +x $out/bin/openssl-wrap
+      ''}
+
+      ${optionalString luks.fido2Support ''
+        copy_bin_and_libs ${pkgs.fido2luks}/bin/fido2luks
+      ''}
+
+
+      ${optionalString luks.gpgSupport ''
+        copy_bin_and_libs ${pkgs.gnupg}/bin/gpg
+        copy_bin_and_libs ${pkgs.gnupg}/bin/gpg-agent
+        copy_bin_and_libs ${pkgs.gnupg}/libexec/scdaemon
+
+        ${concatMapStringsSep "\n" (x:
+          if x.gpgCard != null then
+            ''
+              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)
+        }
+      ''}
+    '';
+
+    boot.initrd.extraUtilsCommandsTest = ''
+      $out/bin/cryptsetup --version
+      ${optionalString luks.yubikeySupport ''
+        $out/bin/ykchalresp -V
+        $out/bin/ykinfo -V
+        $out/bin/openssl-wrap version
+      ''}
+      ${optionalString luks.gpgSupport ''
+        $out/bin/gpg --version
+        $out/bin/gpg-agent --version
+        $out/bin/scdaemon --version
+      ''}
+      ${optionalString luks.fido2Support ''
+        $out/bin/fido2luks --version
+      ''}
+    '';
+
+    boot.initrd.preFailCommands = postCommands;
+    boot.initrd.preLVMCommands = commonFunctions + preCommands + concatStrings (mapAttrsToList openCommand preLVM) + postCommands;
+    boot.initrd.postDeviceCommands = commonFunctions + preCommands + concatStrings (mapAttrsToList openCommand postLVM) + postCommands;
+
+    environment.systemPackages = [ pkgs.cryptsetup ];
+  };
+}
diff --git a/nixos/modules/system/boot/modprobe.nix b/nixos/modules/system/boot/modprobe.nix
new file mode 100644
index 00000000000..e683d181729
--- /dev/null
+++ b/nixos/modules/system/boot/modprobe.nix
@@ -0,0 +1,70 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    boot.blacklistedKernelModules = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "cirrusfb" "i2c_piix4" ];
+      description = ''
+        List of names of kernel modules that should not be loaded
+        automatically by the hardware probing code.
+      '';
+    };
+
+    boot.extraModprobeConfig = mkOption {
+      default = "";
+      example =
+        ''
+          options parport_pc io=0x378 irq=7 dma=1
+        '';
+      description = ''
+        Any additional configuration to be appended to the generated
+        <filename>modprobe.conf</filename>.  This is typically used to
+        specify module options.  See
+        <citerefentry><refentrytitle>modprobe.d</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+      type = types.lines;
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf (!config.boot.isContainer) {
+
+    environment.etc."modprobe.d/ubuntu.conf".source = "${pkgs.kmod-blacklist-ubuntu}/modprobe.conf";
+
+    environment.etc."modprobe.d/nixos.conf".text =
+      ''
+        ${flip concatMapStrings config.boot.blacklistedKernelModules (name: ''
+          blacklist ${name}
+        '')}
+        ${config.boot.extraModprobeConfig}
+      '';
+    environment.etc."modprobe.d/debian.conf".source = pkgs.kmod-debian-aliases;
+
+    environment.etc."modprobe.d/systemd.conf".source = "${pkgs.systemd}/lib/modprobe.d/systemd.conf";
+
+    environment.systemPackages = [ pkgs.kmod ];
+
+    system.activationScripts.modprobe = stringAfter ["specialfs"]
+      ''
+        # Allow the kernel to find our wrapped modprobe (which searches
+        # in the right location in the Nix store for kernel modules).
+        # We need this when the kernel (or some module) auto-loads a
+        # module.
+        echo ${pkgs.kmod}/bin/modprobe > /proc/sys/kernel/modprobe
+      '';
+
+  };
+
+}
diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix
new file mode 100644
index 00000000000..ac1e4ef34b4
--- /dev/null
+++ b/nixos/modules/system/boot/networkd.nix
@@ -0,0 +1,1797 @@
+{ config, lib, pkgs, utils, ... }:
+
+with utils.systemdUtils.unitOptions;
+with utils.systemdUtils.lib;
+with lib;
+
+let
+
+  cfg = config.systemd.network;
+
+  check = {
+
+    link = {
+
+      sectionLink = checkUnitConfig "Link" [
+        (assertOnlyFields [
+          "Description"
+          "Alias"
+          "MACAddressPolicy"
+          "MACAddress"
+          "NamePolicy"
+          "Name"
+          "AlternativeNamesPolicy"
+          "AlternativeName"
+          "MTUBytes"
+          "BitsPerSecond"
+          "Duplex"
+          "AutoNegotiation"
+          "WakeOnLan"
+          "Port"
+          "Advertise"
+          "ReceiveChecksumOffload"
+          "TransmitChecksumOffload"
+          "TCPSegmentationOffload"
+          "TCP6SegmentationOffload"
+          "GenericSegmentationOffload"
+          "GenericReceiveOffload"
+          "LargeReceiveOffload"
+          "RxChannels"
+          "TxChannels"
+          "OtherChannels"
+          "CombinedChannels"
+          "RxBufferSize"
+          "TxBufferSize"
+        ])
+        (assertValueOneOf "MACAddressPolicy" ["persistent" "random" "none"])
+        (assertMacAddress "MACAddress")
+        (assertByteFormat "MTUBytes")
+        (assertByteFormat "BitsPerSecond")
+        (assertValueOneOf "Duplex" ["half" "full"])
+        (assertValueOneOf "AutoNegotiation" boolValues)
+        (assertValueOneOf "WakeOnLan" ["phy" "unicast" "multicast" "broadcast" "arp" "magic" "secureon" "off"])
+        (assertValueOneOf "Port" ["tp" "aui" "bnc" "mii" "fibre"])
+        (assertValueOneOf "ReceiveChecksumOffload" boolValues)
+        (assertValueOneOf "TransmitChecksumOffload" boolValues)
+        (assertValueOneOf "TCPSegmentationOffload" boolValues)
+        (assertValueOneOf "TCP6SegmentationOffload" boolValues)
+        (assertValueOneOf "GenericSegmentationOffload" boolValues)
+        (assertValueOneOf "GenericReceiveOffload" boolValues)
+        (assertValueOneOf "LargeReceiveOffload" boolValues)
+        (assertInt "RxChannels")
+        (assertRange "RxChannels" 1 4294967295)
+        (assertInt "TxChannels")
+        (assertRange "TxChannels" 1 4294967295)
+        (assertInt "OtherChannels")
+        (assertRange "OtherChannels" 1 4294967295)
+        (assertInt "CombinedChannels")
+        (assertRange "CombinedChannels" 1 4294967295)
+        (assertInt "RxBufferSize")
+        (assertInt "TxBufferSize")
+      ];
+    };
+
+    netdev = let
+
+      tunChecks = [
+        (assertOnlyFields [
+          "MultiQueue"
+          "PacketInfo"
+          "VNetHeader"
+          "User"
+          "Group"
+        ])
+        (assertValueOneOf "MultiQueue" boolValues)
+        (assertValueOneOf "PacketInfo" boolValues)
+        (assertValueOneOf "VNetHeader" boolValues)
+      ];
+    in {
+
+      sectionNetdev = checkUnitConfig "Netdev" [
+        (assertOnlyFields [
+          "Description"
+          "Name"
+          "Kind"
+          "MTUBytes"
+          "MACAddress"
+        ])
+        (assertHasField "Name")
+        (assertHasField "Kind")
+        (assertValueOneOf "Kind" [
+          "bond"
+          "bridge"
+          "dummy"
+          "gre"
+          "gretap"
+          "erspan"
+          "ip6gre"
+          "ip6tnl"
+          "ip6gretap"
+          "ipip"
+          "ipvlan"
+          "macvlan"
+          "macvtap"
+          "sit"
+          "tap"
+          "tun"
+          "veth"
+          "vlan"
+          "vti"
+          "vti6"
+          "vxlan"
+          "geneve"
+          "l2tp"
+          "macsec"
+          "vrf"
+          "vcan"
+          "vxcan"
+          "wireguard"
+          "netdevsim"
+          "nlmon"
+          "fou"
+          "xfrm"
+          "ifb"
+          "batadv"
+        ])
+        (assertByteFormat "MTUBytes")
+        (assertMacAddress "MACAddress")
+      ];
+
+      sectionVLAN = checkUnitConfig "VLAN" [
+        (assertOnlyFields [
+          "Id"
+          "GVRP"
+          "MVRP"
+          "LooseBinding"
+          "ReorderHeader"
+        ])
+        (assertInt "Id")
+        (assertRange "Id" 0 4094)
+        (assertValueOneOf "GVRP" boolValues)
+        (assertValueOneOf "MVRP" boolValues)
+        (assertValueOneOf "LooseBinding" boolValues)
+        (assertValueOneOf "ReorderHeader" boolValues)
+      ];
+
+      sectionMACVLAN = checkUnitConfig "MACVLAN" [
+        (assertOnlyFields [
+          "Mode"
+        ])
+        (assertValueOneOf "Mode" ["private" "vepa" "bridge" "passthru"])
+      ];
+
+      sectionVXLAN = checkUnitConfig "VXLAN" [
+        (assertOnlyFields [
+          "VNI"
+          "Remote"
+          "Local"
+          "Group"
+          "TOS"
+          "TTL"
+          "MacLearning"
+          "FDBAgeingSec"
+          "MaximumFDBEntries"
+          "ReduceARPProxy"
+          "L2MissNotification"
+          "L3MissNotification"
+          "RouteShortCircuit"
+          "UDPChecksum"
+          "UDP6ZeroChecksumTx"
+          "UDP6ZeroChecksumRx"
+          "RemoteChecksumTx"
+          "RemoteChecksumRx"
+          "GroupPolicyExtension"
+          "GenericProtocolExtension"
+          "DestinationPort"
+          "PortRange"
+          "FlowLabel"
+          "IPDoNotFragment"
+        ])
+        (assertInt "VNI")
+        (assertRange "VNI" 1 16777215)
+        (assertValueOneOf "MacLearning" boolValues)
+        (assertInt "MaximumFDBEntries")
+        (assertValueOneOf "ReduceARPProxy" boolValues)
+        (assertValueOneOf "L2MissNotification" boolValues)
+        (assertValueOneOf "L3MissNotification" boolValues)
+        (assertValueOneOf "RouteShortCircuit" boolValues)
+        (assertValueOneOf "UDPChecksum" boolValues)
+        (assertValueOneOf "UDP6ZeroChecksumTx" boolValues)
+        (assertValueOneOf "UDP6ZeroChecksumRx" boolValues)
+        (assertValueOneOf "RemoteChecksumTx" boolValues)
+        (assertValueOneOf "RemoteChecksumRx" boolValues)
+        (assertValueOneOf "GroupPolicyExtension" boolValues)
+        (assertValueOneOf "GenericProtocolExtension" boolValues)
+        (assertInt "FlowLabel")
+        (assertRange "FlowLabel" 0 1048575)
+        (assertValueOneOf "IPDoNotFragment" (boolValues + ["inherit"]))
+      ];
+
+      sectionTunnel = checkUnitConfig "Tunnel" [
+        (assertOnlyFields [
+          "Local"
+          "Remote"
+          "TOS"
+          "TTL"
+          "DiscoverPathMTU"
+          "IPv6FlowLabel"
+          "CopyDSCP"
+          "EncapsulationLimit"
+          "Key"
+          "InputKey"
+          "OutputKey"
+          "Mode"
+          "Independent"
+          "AssignToLoopback"
+          "AllowLocalRemote"
+          "FooOverUDP"
+          "FOUDestinationPort"
+          "FOUSourcePort"
+          "Encapsulation"
+          "IPv6RapidDeploymentPrefix"
+          "ISATAP"
+          "SerializeTunneledPackets"
+          "ERSPANIndex"
+        ])
+        (assertInt "TTL")
+        (assertRange "TTL" 0 255)
+        (assertValueOneOf "DiscoverPathMTU" boolValues)
+        (assertValueOneOf "CopyDSCP" boolValues)
+        (assertValueOneOf "Mode" ["ip6ip6" "ipip6" "any"])
+        (assertValueOneOf "Independent" boolValues)
+        (assertValueOneOf "AssignToLoopback" boolValues)
+        (assertValueOneOf "AllowLocalRemote" boolValues)
+        (assertValueOneOf "FooOverUDP" boolValues)
+        (assertPort "FOUDestinationPort")
+        (assertPort "FOUSourcePort")
+        (assertValueOneOf "Encapsulation" ["FooOverUDP" "GenericUDPEncapsulation"])
+        (assertValueOneOf "ISATAP" boolValues)
+        (assertValueOneOf "SerializeTunneledPackets" boolValues)
+        (assertInt "ERSPANIndex")
+        (assertRange "ERSPANIndex" 1 1048575)
+      ];
+
+      sectionFooOverUDP = checkUnitConfig "FooOverUDP" [
+        (assertOnlyFields [
+          "Port"
+          "Encapsulation"
+          "Protocol"
+        ])
+        (assertPort "Port")
+        (assertValueOneOf "Encapsulation" ["FooOverUDP" "GenericUDPEncapsulation"])
+      ];
+
+      sectionPeer = checkUnitConfig "Peer" [
+        (assertOnlyFields [
+          "Name"
+          "MACAddress"
+        ])
+        (assertMacAddress "MACAddress")
+      ];
+
+      sectionTun = checkUnitConfig "Tun" tunChecks;
+
+      sectionTap = checkUnitConfig "Tap" tunChecks;
+
+      # NOTE The PrivateKey directive is missing on purpose here, please
+      # do not add it to this list. The nix store is world-readable let's
+      # refrain ourselves from providing a footgun.
+      sectionWireGuard = checkUnitConfig "WireGuard" [
+        (assertOnlyFields [
+          "PrivateKeyFile"
+          "ListenPort"
+          "FirewallMark"
+        ])
+        (assertInt "FirewallMark")
+        (assertRange "FirewallMark" 1 4294967295)
+      ];
+
+      # NOTE The PresharedKey directive is missing on purpose here, please
+      # do not add it to this list. The nix store is world-readable,let's
+      # refrain ourselves from providing a footgun.
+      sectionWireGuardPeer = checkUnitConfig "WireGuardPeer" [
+        (assertOnlyFields [
+          "PublicKey"
+          "PresharedKeyFile"
+          "AllowedIPs"
+          "Endpoint"
+          "PersistentKeepalive"
+        ])
+        (assertInt "PersistentKeepalive")
+        (assertRange "PersistentKeepalive" 0 65535)
+      ];
+
+      sectionBond = checkUnitConfig "Bond" [
+        (assertOnlyFields [
+          "Mode"
+          "TransmitHashPolicy"
+          "LACPTransmitRate"
+          "MIIMonitorSec"
+          "UpDelaySec"
+          "DownDelaySec"
+          "LearnPacketIntervalSec"
+          "AdSelect"
+          "AdActorSystemPriority"
+          "AdUserPortKey"
+          "AdActorSystem"
+          "FailOverMACPolicy"
+          "ARPValidate"
+          "ARPIntervalSec"
+          "ARPIPTargets"
+          "ARPAllTargets"
+          "PrimaryReselectPolicy"
+          "ResendIGMP"
+          "PacketsPerSlave"
+          "GratuitousARP"
+          "AllSlavesActive"
+          "DynamicTransmitLoadBalancing"
+          "MinLinks"
+        ])
+        (assertValueOneOf "Mode" [
+          "balance-rr"
+          "active-backup"
+          "balance-xor"
+          "broadcast"
+          "802.3ad"
+          "balance-tlb"
+          "balance-alb"
+        ])
+        (assertValueOneOf "TransmitHashPolicy" [
+          "layer2"
+          "layer3+4"
+          "layer2+3"
+          "encap2+3"
+          "encap3+4"
+        ])
+        (assertValueOneOf "LACPTransmitRate" ["slow" "fast"])
+        (assertValueOneOf "AdSelect" ["stable" "bandwidth" "count"])
+        (assertInt "AdActorSystemPriority")
+        (assertRange "AdActorSystemPriority" 1 65535)
+        (assertInt "AdUserPortKey")
+        (assertRange "AdUserPortKey" 0 1023)
+        (assertValueOneOf "FailOverMACPolicy" ["none" "active" "follow"])
+        (assertValueOneOf "ARPValidate" ["none" "active" "backup" "all"])
+        (assertValueOneOf "ARPAllTargets" ["any" "all"])
+        (assertValueOneOf "PrimaryReselectPolicy" ["always" "better" "failure"])
+        (assertInt "ResendIGMP")
+        (assertRange "ResendIGMP" 0 255)
+        (assertInt "PacketsPerSlave")
+        (assertRange "PacketsPerSlave" 0 65535)
+        (assertInt "GratuitousARP")
+        (assertRange "GratuitousARP" 0 255)
+        (assertValueOneOf "AllSlavesActive" boolValues)
+        (assertValueOneOf "DynamicTransmitLoadBalancing" boolValues)
+        (assertInt "MinLinks")
+        (assertMinimum "MinLinks" 0)
+      ];
+
+      sectionXfrm = checkUnitConfig "Xfrm" [
+        (assertOnlyFields [
+          "InterfaceId"
+          "Independent"
+        ])
+        (assertInt "InterfaceId")
+        (assertRange "InterfaceId" 1 4294967295)
+        (assertValueOneOf "Independent" boolValues)
+      ];
+
+      sectionVRF = checkUnitConfig "VRF" [
+        (assertOnlyFields [
+          "Table"
+        ])
+        (assertInt "Table")
+        (assertMinimum "Table" 0)
+      ];
+
+      sectionBatmanAdvanced = checkUnitConfig "BatmanAdvanced" [
+        (assertOnlyFields [
+          "GatewayMode"
+          "Aggregation"
+          "BridgeLoopAvoidance"
+          "DistributedArpTable"
+          "Fragmentation"
+          "HopPenalty"
+          "OriginatorIntervalSec"
+          "GatewayBandwithDown"
+          "GatewayBandwithUp"
+          "RoutingAlgorithm"
+        ])
+        (assertValueOneOf "GatewayMode" ["off" "client" "server"])
+        (assertValueOneOf "Aggregation" boolValues)
+        (assertValueOneOf "BridgeLoopAvoidance" boolValues)
+        (assertValueOneOf "DistributedArpTable" boolValues)
+        (assertValueOneOf "Fragmentation" boolValues)
+        (assertInt "HopPenalty")
+        (assertRange "HopPenalty" 0 255)
+        (assertValueOneOf "RoutingAlgorithm" ["batman-v" "batman-iv"])
+      ];
+    };
+
+    network = {
+
+      sectionLink = checkUnitConfig "Link" [
+        (assertOnlyFields [
+          "MACAddress"
+          "MTUBytes"
+          "ARP"
+          "Multicast"
+          "AllMulticast"
+          "Unmanaged"
+          "RequiredForOnline"
+          "ActivationPolicy"
+        ])
+        (assertMacAddress "MACAddress")
+        (assertByteFormat "MTUBytes")
+        (assertValueOneOf "ARP" boolValues)
+        (assertValueOneOf "Multicast" boolValues)
+        (assertValueOneOf "AllMulticast" boolValues)
+        (assertValueOneOf "Unmanaged" boolValues)
+        (assertValueOneOf "RequiredForOnline" (boolValues ++ [
+          "missing"
+          "off"
+          "no-carrier"
+          "dormant"
+          "degraded-carrier"
+          "carrier"
+          "degraded"
+          "enslaved"
+          "routable"
+        ]))
+        (assertValueOneOf "ActivationPolicy" ([
+          "up"
+          "always-up"
+          "manual"
+          "always-down"
+          "down"
+          "bound"
+        ]))
+      ];
+
+      sectionNetwork = checkUnitConfig "Network" [
+        (assertOnlyFields [
+          "Description"
+          "DHCP"
+          "DHCPServer"
+          "LinkLocalAddressing"
+          "IPv4LLRoute"
+          "DefaultRouteOnDevice"
+          "IPv6Token"
+          "LLMNR"
+          "MulticastDNS"
+          "DNSOverTLS"
+          "DNSSEC"
+          "DNSSECNegativeTrustAnchors"
+          "LLDP"
+          "EmitLLDP"
+          "BindCarrier"
+          "Address"
+          "Gateway"
+          "DNS"
+          "Domains"
+          "DNSDefaultRoute"
+          "NTP"
+          "IPForward"
+          "IPMasquerade"
+          "IPv6PrivacyExtensions"
+          "IPv6AcceptRA"
+          "IPv6DuplicateAddressDetection"
+          "IPv6HopLimit"
+          "IPv4ProxyARP"
+          "IPv6ProxyNDP"
+          "IPv6ProxyNDPAddress"
+          "IPv6SendRA"
+          "DHCPv6PrefixDelegation"
+          "IPv6MTUBytes"
+          "Bridge"
+          "Bond"
+          "VRF"
+          "VLAN"
+          "IPVLAN"
+          "MACVLAN"
+          "VXLAN"
+          "Tunnel"
+          "MACsec"
+          "ActiveSlave"
+          "PrimarySlave"
+          "ConfigureWithoutCarrier"
+          "IgnoreCarrierLoss"
+          "Xfrm"
+          "KeepConfiguration"
+          "BatmanAdvanced"
+        ])
+        # Note: For DHCP the values both, none, v4, v6 are deprecated
+        (assertValueOneOf "DHCP" ["yes" "no" "ipv4" "ipv6"])
+        (assertValueOneOf "DHCPServer" boolValues)
+        (assertValueOneOf "LinkLocalAddressing" ["yes" "no" "ipv4" "ipv6" "fallback" "ipv4-fallback"])
+        (assertValueOneOf "IPv4LLRoute" boolValues)
+        (assertValueOneOf "DefaultRouteOnDevice" boolValues)
+        (assertValueOneOf "LLMNR" (boolValues ++ ["resolve"]))
+        (assertValueOneOf "MulticastDNS" (boolValues ++ ["resolve"]))
+        (assertValueOneOf "DNSOverTLS" (boolValues ++ ["opportunistic"]))
+        (assertValueOneOf "DNSSEC" (boolValues ++ ["allow-downgrade"]))
+        (assertValueOneOf "LLDP" (boolValues ++ ["routers-only"]))
+        (assertValueOneOf "EmitLLDP" (boolValues ++ ["nearest-bridge" "non-tpmr-bridge" "customer-bridge"]))
+        (assertValueOneOf "DNSDefaultRoute" boolValues)
+        (assertValueOneOf "IPForward" (boolValues ++ ["ipv4" "ipv6"]))
+        (assertValueOneOf "IPMasquerade" (boolValues ++ ["ipv4" "ipv6" "both"]))
+        (assertValueOneOf "IPv6PrivacyExtensions" (boolValues ++ ["prefer-public" "kernel"]))
+        (assertValueOneOf "IPv6AcceptRA" boolValues)
+        (assertInt "IPv6DuplicateAddressDetection")
+        (assertMinimum "IPv6DuplicateAddressDetection" 0)
+        (assertInt "IPv6HopLimit")
+        (assertMinimum "IPv6HopLimit" 0)
+        (assertValueOneOf "IPv4ProxyARP" boolValues)
+        (assertValueOneOf "IPv6ProxyNDP" boolValues)
+        (assertValueOneOf "IPv6SendRA" boolValues)
+        (assertValueOneOf "DHCPv6PrefixDelegation" boolValues)
+        (assertByteFormat "IPv6MTUBytes")
+        (assertValueOneOf "ActiveSlave" boolValues)
+        (assertValueOneOf "PrimarySlave" boolValues)
+        (assertValueOneOf "ConfigureWithoutCarrier" boolValues)
+        (assertValueOneOf "IgnoreCarrierLoss" boolValues)
+        (assertValueOneOf "KeepConfiguration" (boolValues ++ ["static" "dhcp-on-stop" "dhcp"]))
+      ];
+
+      sectionAddress = checkUnitConfig "Address" [
+        (assertOnlyFields [
+          "Address"
+          "Peer"
+          "Broadcast"
+          "Label"
+          "PreferredLifetime"
+          "Scope"
+          "HomeAddress"
+          "DuplicateAddressDetection"
+          "ManageTemporaryAddress"
+          "AddPrefixRoute"
+          "AutoJoin"
+        ])
+        (assertHasField "Address")
+        (assertValueOneOf "PreferredLifetime" ["forever" "infinity" "0" 0])
+        (assertValueOneOf "HomeAddress" boolValues)
+        (assertValueOneOf "DuplicateAddressDetection" ["ipv4" "ipv6" "both" "none"])
+        (assertValueOneOf "ManageTemporaryAddress" boolValues)
+        (assertValueOneOf "AddPrefixRoute" boolValues)
+        (assertValueOneOf "AutoJoin" boolValues)
+      ];
+
+      sectionRoutingPolicyRule = checkUnitConfig "RoutingPolicyRule" [
+        (assertOnlyFields [
+          "TypeOfService"
+          "From"
+          "To"
+          "FirewallMark"
+          "Table"
+          "Priority"
+          "IncomingInterface"
+          "OutgoingInterface"
+          "SourcePort"
+          "DestinationPort"
+          "IPProtocol"
+          "InvertRule"
+          "Family"
+          "User"
+          "SuppressPrefixLength"
+          "Type"
+        ])
+        (assertInt "TypeOfService")
+        (assertRange "TypeOfService" 0 255)
+        (assertInt "FirewallMark")
+        (assertRange "FirewallMark" 1 4294967295)
+        (assertInt "Priority")
+        (assertPort "SourcePort")
+        (assertPort "DestinationPort")
+        (assertValueOneOf "InvertRule" boolValues)
+        (assertValueOneOf "Family" ["ipv4" "ipv6" "both"])
+        (assertInt "SuppressPrefixLength")
+        (assertRange "SuppressPrefixLength" 0 128)
+        (assertValueOneOf "Type" ["blackhole" "unreachable" "prohibit"])
+      ];
+
+      sectionRoute = checkUnitConfig "Route" [
+        (assertOnlyFields [
+          "Gateway"
+          "GatewayOnLink"
+          "Destination"
+          "Source"
+          "Metric"
+          "IPv6Preference"
+          "Scope"
+          "PreferredSource"
+          "Table"
+          "Protocol"
+          "Type"
+          "InitialCongestionWindow"
+          "InitialAdvertisedReceiveWindow"
+          "QuickAck"
+          "FastOpenNoCookie"
+          "TTLPropagate"
+          "MTUBytes"
+          "IPServiceType"
+          "MultiPathRoute"
+        ])
+        (assertValueOneOf "GatewayOnLink" boolValues)
+        (assertInt "Metric")
+        (assertValueOneOf "IPv6Preference" ["low" "medium" "high"])
+        (assertValueOneOf "Scope" ["global" "site" "link" "host" "nowhere"])
+        (assertValueOneOf "Type" [
+          "unicast"
+          "local"
+          "broadcast"
+          "anycast"
+          "multicast"
+          "blackhole"
+          "unreachable"
+          "prohibit"
+          "throw"
+          "nat"
+          "xresolve"
+        ])
+        (assertValueOneOf "QuickAck" boolValues)
+        (assertValueOneOf "FastOpenNoCookie" boolValues)
+        (assertValueOneOf "TTLPropagate" boolValues)
+        (assertByteFormat "MTUBytes")
+        (assertValueOneOf "IPServiceType" ["CS6" "CS4"])
+      ];
+
+      sectionDHCPv4 = checkUnitConfig "DHCPv4" [
+        (assertOnlyFields [
+          "UseDNS"
+          "RoutesToDNS"
+          "UseNTP"
+          "UseSIP"
+          "UseMTU"
+          "Anonymize"
+          "SendHostname"
+          "UseHostname"
+          "Hostname"
+          "UseDomains"
+          "UseRoutes"
+          "UseTimezone"
+          "ClientIdentifier"
+          "VendorClassIdentifier"
+          "UserClass"
+          "MaxAttempts"
+          "DUIDType"
+          "DUIDRawData"
+          "IAID"
+          "RequestBroadcast"
+          "RouteMetric"
+          "RouteTable"
+          "RouteMTUBytes"
+          "ListenPort"
+          "SendRelease"
+          "SendDecline"
+          "BlackList"
+          "RequestOptions"
+          "SendOption"
+        ])
+        (assertValueOneOf "UseDNS" boolValues)
+        (assertValueOneOf "RoutesToDNS" boolValues)
+        (assertValueOneOf "UseNTP" boolValues)
+        (assertValueOneOf "UseSIP" boolValues)
+        (assertValueOneOf "UseMTU" boolValues)
+        (assertValueOneOf "Anonymize" boolValues)
+        (assertValueOneOf "SendHostname" boolValues)
+        (assertValueOneOf "UseHostname" boolValues)
+        (assertValueOneOf "UseDomains" (boolValues ++ ["route"]))
+        (assertValueOneOf "UseRoutes" boolValues)
+        (assertValueOneOf "UseTimezone" boolValues)
+        (assertValueOneOf "ClientIdentifier" ["mac" "duid" "duid-only"])
+        (assertInt "IAID")
+        (assertValueOneOf "RequestBroadcast" boolValues)
+        (assertInt "RouteMetric")
+        (assertInt "RouteTable")
+        (assertRange "RouteTable" 0 4294967295)
+        (assertByteFormat "RouteMTUBytes")
+        (assertPort "ListenPort")
+        (assertValueOneOf "SendRelease" boolValues)
+        (assertValueOneOf "SendDecline" boolValues)
+      ];
+
+      sectionDHCPv6 = checkUnitConfig "DHCPv6" [
+        (assertOnlyFields [
+          "UseAddress"
+          "UseDNS"
+          "UseNTP"
+          "RouteMetric"
+          "RapidCommit"
+          "MUDURL"
+          "RequestOptions"
+          "SendVendorOption"
+          "ForceDHCPv6PDOtherInformation"
+          "PrefixDelegationHint"
+          "WithoutRA"
+          "SendOption"
+          "UserClass"
+          "VendorClass"
+          "DUIDType"
+          "DUIDRawData"
+          "IAID"
+        ])
+        (assertValueOneOf "UseAddress" boolValues)
+        (assertValueOneOf "UseDNS" boolValues)
+        (assertValueOneOf "UseNTP" boolValues)
+        (assertInt "RouteMetric")
+        (assertValueOneOf "RapidCommit" boolValues)
+        (assertValueOneOf "ForceDHCPv6PDOtherInformation" boolValues)
+        (assertValueOneOf "WithoutRA" ["solicit" "information-request"])
+        (assertRange "SendOption" 1 65536)
+        (assertInt "IAID")
+      ];
+
+      sectionDHCPv6PrefixDelegation = checkUnitConfig "DHCPv6PrefixDelegation" [
+        (assertOnlyFields [
+          "SubnetId"
+          "Announce"
+          "Assign"
+          "Token"
+        ])
+        (assertValueOneOf "Announce" boolValues)
+        (assertValueOneOf "Assign" boolValues)
+      ];
+
+      sectionIPv6AcceptRA = checkUnitConfig "IPv6AcceptRA" [
+        (assertOnlyFields [
+          "UseDNS"
+          "UseDomains"
+          "RouteTable"
+          "UseAutonomousPrefix"
+          "UseOnLinkPrefix"
+          "RouterDenyList"
+          "RouterAllowList"
+          "PrefixDenyList"
+          "PrefixAllowList"
+          "RouteDenyList"
+          "RouteAllowList"
+          "DHCPv6Client"
+        ])
+        (assertValueOneOf "UseDNS" boolValues)
+        (assertValueOneOf "UseDomains" (boolValues ++ ["route"]))
+        (assertRange "RouteTable" 0 4294967295)
+        (assertValueOneOf "UseAutonomousPrefix" boolValues)
+        (assertValueOneOf "UseOnLinkPrefix" boolValues)
+        (assertValueOneOf "DHCPv6Client" (boolValues ++ ["always"]))
+      ];
+
+      sectionDHCPServer = checkUnitConfig "DHCPServer" [
+        (assertOnlyFields [
+          "PoolOffset"
+          "PoolSize"
+          "DefaultLeaseTimeSec"
+          "MaxLeaseTimeSec"
+          "EmitDNS"
+          "DNS"
+          "EmitNTP"
+          "NTP"
+          "EmitSIP"
+          "SIP"
+          "EmitPOP3"
+          "POP3"
+          "EmitSMTP"
+          "SMTP"
+          "EmitLPR"
+          "LPR"
+          "EmitRouter"
+          "EmitTimezone"
+          "Timezone"
+          "SendOption"
+          "SendVendorOption"
+        ])
+        (assertInt "PoolOffset")
+        (assertMinimum "PoolOffset" 0)
+        (assertInt "PoolSize")
+        (assertMinimum "PoolSize" 0)
+        (assertValueOneOf "EmitDNS" boolValues)
+        (assertValueOneOf "EmitNTP" boolValues)
+        (assertValueOneOf "EmitSIP" boolValues)
+        (assertValueOneOf "EmitPOP3" boolValues)
+        (assertValueOneOf "EmitSMTP" boolValues)
+        (assertValueOneOf "EmitLPR" boolValues)
+        (assertValueOneOf "EmitRouter" boolValues)
+        (assertValueOneOf "EmitTimezone" boolValues)
+      ];
+
+      sectionIPv6SendRA = checkUnitConfig "IPv6SendRA" [
+        (assertOnlyFields [
+          "Managed"
+          "OtherInformation"
+          "RouterLifetimeSec"
+          "RouterPreference"
+          "EmitDNS"
+          "DNS"
+          "EmitDomains"
+          "Domains"
+          "DNSLifetimeSec"
+        ])
+        (assertValueOneOf "Managed" boolValues)
+        (assertValueOneOf "OtherInformation" boolValues)
+        (assertValueOneOf "RouterPreference" ["high" "medium" "low" "normal" "default"])
+        (assertValueOneOf "EmitDNS" boolValues)
+        (assertValueOneOf "EmitDomains" boolValues)
+      ];
+
+      sectionIPv6Prefix = checkUnitConfig "IPv6Prefix" [
+        (assertOnlyFields [
+          "AddressAutoconfiguration"
+          "OnLink"
+          "Prefix"
+          "PreferredLifetimeSec"
+          "ValidLifetimeSec"
+        ])
+        (assertValueOneOf "AddressAutoconfiguration" boolValues)
+        (assertValueOneOf "OnLink" boolValues)
+      ];
+
+      sectionDHCPServerStaticLease = checkUnitConfig "DHCPServerStaticLease" [
+        (assertOnlyFields [
+          "MACAddress"
+          "Address"
+        ])
+        (assertHasField "MACAddress")
+        (assertHasField "Address")
+        (assertMacAddress "MACAddress")
+      ];
+
+    };
+  };
+
+  commonNetworkOptions = {
+
+    enable = mkOption {
+      default = true;
+      type = types.bool;
+      description = ''
+        Whether to manage network configuration using <command>systemd-network</command>.
+      '';
+    };
+
+    matchConfig = mkOption {
+      default = {};
+      example = { Name = "eth0"; };
+      type = types.attrsOf unitOption;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Match]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.link</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+        <citerefentry><refentrytitle>systemd.network</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+        for details.
+      '';
+    };
+
+    extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      description = "Extra configuration append to unit";
+    };
+  };
+
+  linkOptions = commonNetworkOptions // {
+    # overwrite enable option from above
+    enable = mkOption {
+      default = true;
+      type = types.bool;
+      description = ''
+        Whether to enable this .link unit. It's handled by udev no matter if <command>systemd-networkd</command> is enabled or not
+      '';
+    };
+
+    linkConfig = mkOption {
+      default = {};
+      example = { MACAddress = "00:ff:ee:aa:cc:dd"; };
+      type = types.addCheck (types.attrsOf unitOption) check.link.sectionLink;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Link]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.link</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+  };
+
+  wireguardPeerOptions = {
+    options = {
+      wireguardPeerConfig = mkOption {
+        default = {};
+        type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionWireGuardPeer;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[WireGuardPeer]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.network</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
+    };
+  };
+
+  netdevOptions = commonNetworkOptions // {
+
+    netdevConfig = mkOption {
+      example = { Name = "mybridge"; Kind = "bridge"; };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionNetdev;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Netdev]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    vlanConfig = mkOption {
+      default = {};
+      example = { Id = 4; };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionVLAN;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[VLAN]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    macvlanConfig = mkOption {
+      default = {};
+      example = { Mode = "private"; };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionMACVLAN;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[MACVLAN]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    vxlanConfig = mkOption {
+      default = {};
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionVXLAN;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[VXLAN]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    tunnelConfig = mkOption {
+      default = {};
+      example = { Remote = "192.168.1.1"; };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionTunnel;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Tunnel]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    fooOverUDPConfig = mkOption {
+      default = { };
+      example = { Port = 9001; };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionFooOverUDP;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[FooOverUDP]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    peerConfig = mkOption {
+      default = {};
+      example = { Name = "veth2"; };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionPeer;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Peer]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    tunConfig = mkOption {
+      default = {};
+      example = { User = "openvpn"; };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionTun;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Tun]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    tapConfig = mkOption {
+      default = {};
+      example = { User = "openvpn"; };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionTap;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Tap]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    wireguardConfig = mkOption {
+      default = {};
+      example = {
+        PrivateKeyFile = "/etc/wireguard/secret.key";
+        ListenPort = 51820;
+        FirewallMark = 42;
+      };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionWireGuard;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[WireGuard]</literal> section of the unit. See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+        Use <literal>PrivateKeyFile</literal> instead of
+        <literal>PrivateKey</literal>: the nix store is
+        world-readable.
+      '';
+    };
+
+    wireguardPeers = mkOption {
+      default = [];
+      example = [ { wireguardPeerConfig={
+        Endpoint = "192.168.1.1:51820";
+        PublicKey = "27s0OvaBBdHoJYkH9osZpjpgSOVNw+RaKfboT/Sfq0g=";
+        PresharedKeyFile = "/etc/wireguard/psk.key";
+        AllowedIPs = [ "10.0.0.1/32" ];
+        PersistentKeepalive = 15;
+      };}];
+      type = with types; listOf (submodule wireguardPeerOptions);
+      description = ''
+        Each item in this array specifies an option in the
+        <literal>[WireGuardPeer]</literal> section of the unit. See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+        Use <literal>PresharedKeyFile</literal> instead of
+        <literal>PresharedKey</literal>: the nix store is
+        world-readable.
+      '';
+    };
+
+    bondConfig = mkOption {
+      default = {};
+      example = { Mode = "802.3ad"; };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionBond;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Bond]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    xfrmConfig = mkOption {
+      default = {};
+      example = { InterfaceId = 1; };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionXfrm;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Xfrm]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    vrfConfig = mkOption {
+      default = {};
+      example = { Table = 2342; };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionVRF;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[VRF]</literal> section of the unit. See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+        A detailed explanation about how VRFs work can be found in the
+        <link xlink:href="https://www.kernel.org/doc/Documentation/networking/vrf.txt">kernel
+        docs</link>.
+      '';
+    };
+
+    batmanAdvancedConfig = mkOption {
+      default = {};
+      example = {
+        GatewayMode = "server";
+        RoutingAlgorithm = "batman-v";
+      };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionBatmanAdvanced;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[BatmanAdvanced]</literal> section of the unit. See
+        <citerefentry><refentrytitle>systemd.netdev</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+  };
+
+  addressOptions = {
+    options = {
+      addressConfig = mkOption {
+        example = { Address = "192.168.0.100/24"; };
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionAddress;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[Address]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.network</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
+    };
+  };
+
+  routingPolicyRulesOptions = {
+    options = {
+      routingPolicyRuleConfig = mkOption {
+        default = { };
+        example = { Table = 10; IncomingInterface = "eth1"; Family = "both"; };
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionRoutingPolicyRule;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[RoutingPolicyRule]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.network</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
+    };
+  };
+
+  routeOptions = {
+    options = {
+      routeConfig = mkOption {
+        default = {};
+        example = { Gateway = "192.168.0.1"; };
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionRoute;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[Route]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.network</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
+    };
+  };
+
+  ipv6PrefixOptions = {
+    options = {
+      ipv6PrefixConfig = mkOption {
+        default = {};
+        example = { Prefix = "fd00::/64"; };
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionIPv6Prefix;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[IPv6Prefix]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.network</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
+    };
+  };
+
+  dhcpServerStaticLeaseOptions = {
+    options = {
+      dhcpServerStaticLeaseConfig = mkOption {
+        default = {};
+        example = { MACAddress = "65:43:4a:5b:d8:5f"; Address = "192.168.1.42"; };
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionDHCPServerStaticLease;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[DHCPServerStaticLease]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.network</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+
+          Make sure to configure the corresponding client interface to use
+          <literal>ClientIdentifier=mac</literal>.
+        '';
+      };
+    };
+  };
+
+  networkOptions = commonNetworkOptions // {
+
+    linkConfig = mkOption {
+      default = {};
+      example = { Unmanaged = true; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionLink;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Link]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    networkConfig = mkOption {
+      default = {};
+      example = { Description = "My Network"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionNetwork;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[Network]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    # systemd.network.networks.*.dhcpConfig has been deprecated in favor of ….dhcpV4Config
+    # Produce a nice warning message so users know it is gone.
+    dhcpConfig = mkOption {
+      visible = false;
+      apply = _: throw "The option `systemd.network.networks.*.dhcpConfig` can no longer be used since it's been removed. Please use `systemd.network.networks.*.dhcpV4Config` instead.";
+    };
+
+    dhcpV4Config = mkOption {
+      default = {};
+      example = { UseDNS = true; UseRoutes = true; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionDHCPv4;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[DHCPv4]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    dhcpV6Config = mkOption {
+      default = {};
+      example = { UseDNS = true; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionDHCPv6;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[DHCPv6]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    dhcpV6PrefixDelegationConfig = mkOption {
+      default = {};
+      example = { SubnetId = "auto"; Announce = true; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionDHCPv6PrefixDelegation;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[DHCPv6PrefixDelegation]</literal> section of the unit. See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    ipv6AcceptRAConfig = mkOption {
+      default = {};
+      example = { UseDNS = true; DHCPv6Client = "always"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionIPv6AcceptRA;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[IPv6AcceptRA]</literal> section of the unit. See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    dhcpServerConfig = mkOption {
+      default = {};
+      example = { PoolOffset = 50; EmitDNS = false; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionDHCPServer;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[DHCPServer]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    # systemd.network.networks.*.ipv6PrefixDelegationConfig has been deprecated
+    # in 247 in favor of systemd.network.networks.*.ipv6SendRAConfig.
+    ipv6PrefixDelegationConfig = mkOption {
+      visible = false;
+      apply = _: throw "The option `systemd.network.networks.*.ipv6PrefixDelegationConfig` has been replaced by `systemd.network.networks.*.ipv6SendRAConfig`.";
+    };
+
+    ipv6SendRAConfig = mkOption {
+      default = {};
+      example = { EmitDNS = true; Managed = true; OtherInformation = true; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionIPv6SendRA;
+      description = ''
+        Each attribute in this set specifies an option in the
+        <literal>[IPv6SendRA]</literal> section of the unit.  See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    dhcpServerStaticLeases = mkOption {
+      default = [];
+      example = [ { MACAddress = "65:43:4a:5b:d8:5f"; Address = "192.168.1.42"; } ];
+      type = with types; listOf (submodule dhcpServerStaticLeaseOptions);
+      description = ''
+        A list of DHCPServerStaticLease sections to be added to the unit.  See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    ipv6Prefixes = mkOption {
+      default = [];
+      example = [ { AddressAutoconfiguration = true; OnLink = true; } ];
+      type = with types; listOf (submodule ipv6PrefixOptions);
+      description = ''
+        A list of ipv6Prefix sections to be added to the unit.  See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    name = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The name of the network interface to match against.
+      '';
+    };
+
+    DHCP = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Whether to enable DHCP on the interfaces matched.
+      '';
+    };
+
+    domains = mkOption {
+      type = types.nullOr (types.listOf types.str);
+      default = null;
+      description = ''
+        A list of domains to pass to the network config.
+      '';
+    };
+
+    address = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      description = ''
+        A list of addresses to be added to the network section of the
+        unit.  See <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    gateway = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      description = ''
+        A list of gateways to be added to the network section of the
+        unit.  See <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    dns = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      description = ''
+        A list of dns servers to be added to the network section of the
+        unit.  See <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    ntp = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      description = ''
+        A list of ntp servers to be added to the network section of the
+        unit.  See <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    bridge = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      description = ''
+        A list of bridge interfaces to be added to the network section of the
+        unit.  See <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    bond = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      description = ''
+        A list of bond interfaces to be added to the network section of the
+        unit.  See <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    vrf = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      description = ''
+        A list of vrf interfaces to be added to the network section of the
+        unit.  See <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    vlan = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      description = ''
+        A list of vlan interfaces to be added to the network section of the
+        unit.  See <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    macvlan = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      description = ''
+        A list of macvlan interfaces to be added to the network section of the
+        unit.  See <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    vxlan = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      description = ''
+        A list of vxlan interfaces to be added to the network section of the
+        unit.  See <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    tunnel = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      description = ''
+        A list of tunnel interfaces to be added to the network section of the
+        unit.  See <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    xfrm = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      description = ''
+        A list of xfrm interfaces to be added to the network section of the
+        unit.  See <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    addresses = mkOption {
+      default = [ ];
+      type = with types; listOf (submodule addressOptions);
+      description = ''
+        A list of address sections to be added to the unit.  See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    routingPolicyRules = mkOption {
+      default = [ ];
+      type = with types; listOf (submodule routingPolicyRulesOptions);
+      description = ''
+        A list of routing policy rules sections to be added to the unit.  See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+    routes = mkOption {
+      default = [ ];
+      type = with types; listOf (submodule routeOptions);
+      description = ''
+        A list of route sections to be added to the unit.  See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
+  };
+
+  networkConfig = { config, ... }: {
+    config = {
+      matchConfig = optionalAttrs (config.name != null) {
+        Name = config.name;
+      };
+      networkConfig = optionalAttrs (config.DHCP != null) {
+        DHCP = config.DHCP;
+      } // optionalAttrs (config.domains != null) {
+        Domains = concatStringsSep " " config.domains;
+      };
+    };
+  };
+
+  commonMatchText = def: optionalString (def.matchConfig != { }) ''
+    [Match]
+    ${attrsToSection def.matchConfig}
+  '';
+
+  linkToUnit = name: def:
+    { inherit (def) enable;
+      text = commonMatchText def
+        + ''
+          [Link]
+          ${attrsToSection def.linkConfig}
+        ''
+        + def.extraConfig;
+    };
+
+  netdevToUnit = name: def:
+    { inherit (def) enable;
+      text = commonMatchText def
+        + ''
+          [NetDev]
+          ${attrsToSection def.netdevConfig}
+        ''
+        + optionalString (def.vlanConfig != { }) ''
+          [VLAN]
+          ${attrsToSection def.vlanConfig}
+        ''
+        + optionalString (def.macvlanConfig != { }) ''
+          [MACVLAN]
+          ${attrsToSection def.macvlanConfig}
+        ''
+        + optionalString (def.vxlanConfig != { }) ''
+          [VXLAN]
+          ${attrsToSection def.vxlanConfig}
+        ''
+        + optionalString (def.tunnelConfig != { }) ''
+          [Tunnel]
+          ${attrsToSection def.tunnelConfig}
+        ''
+        + optionalString (def.fooOverUDPConfig != { }) ''
+          [FooOverUDP]
+          ${attrsToSection def.fooOverUDPConfig}
+        ''
+        + optionalString (def.peerConfig != { }) ''
+          [Peer]
+          ${attrsToSection def.peerConfig}
+        ''
+        + optionalString (def.tunConfig != { }) ''
+          [Tun]
+          ${attrsToSection def.tunConfig}
+        ''
+        + optionalString (def.tapConfig != { }) ''
+          [Tap]
+          ${attrsToSection def.tapConfig}
+        ''
+        + optionalString (def.wireguardConfig != { }) ''
+          [WireGuard]
+          ${attrsToSection def.wireguardConfig}
+        ''
+        + flip concatMapStrings def.wireguardPeers (x: ''
+          [WireGuardPeer]
+          ${attrsToSection x.wireguardPeerConfig}
+        '')
+        + optionalString (def.bondConfig != { }) ''
+          [Bond]
+          ${attrsToSection def.bondConfig}
+        ''
+        + optionalString (def.xfrmConfig != { }) ''
+          [Xfrm]
+          ${attrsToSection def.xfrmConfig}
+        ''
+        + optionalString (def.vrfConfig != { }) ''
+          [VRF]
+          ${attrsToSection def.vrfConfig}
+        ''
+        + optionalString (def.batmanAdvancedConfig != { }) ''
+          [BatmanAdvanced]
+          ${attrsToSection def.batmanAdvancedConfig}
+        ''
+        + def.extraConfig;
+    };
+
+  networkToUnit = name: def:
+    { inherit (def) enable;
+      text = commonMatchText def
+        + optionalString (def.linkConfig != { }) ''
+          [Link]
+          ${attrsToSection def.linkConfig}
+        ''
+        + ''
+          [Network]
+        ''
+        + attrsToSection def.networkConfig
+        + optionalString (def.address != [ ]) ''
+          ${concatStringsSep "\n" (map (s: "Address=${s}") def.address)}
+        ''
+        + optionalString (def.gateway != [ ]) ''
+          ${concatStringsSep "\n" (map (s: "Gateway=${s}") def.gateway)}
+        ''
+        + optionalString (def.dns != [ ]) ''
+          ${concatStringsSep "\n" (map (s: "DNS=${s}") def.dns)}
+        ''
+        + optionalString (def.ntp != [ ]) ''
+          ${concatStringsSep "\n" (map (s: "NTP=${s}") def.ntp)}
+        ''
+        + optionalString (def.bridge != [ ]) ''
+          ${concatStringsSep "\n" (map (s: "Bridge=${s}") def.bridge)}
+        ''
+        + optionalString (def.bond != [ ]) ''
+          ${concatStringsSep "\n" (map (s: "Bond=${s}") def.bond)}
+        ''
+        + optionalString (def.vrf != [ ]) ''
+          ${concatStringsSep "\n" (map (s: "VRF=${s}") def.vrf)}
+        ''
+        + optionalString (def.vlan != [ ]) ''
+          ${concatStringsSep "\n" (map (s: "VLAN=${s}") def.vlan)}
+        ''
+        + optionalString (def.macvlan != [ ]) ''
+          ${concatStringsSep "\n" (map (s: "MACVLAN=${s}") def.macvlan)}
+        ''
+        + optionalString (def.vxlan != [ ]) ''
+          ${concatStringsSep "\n" (map (s: "VXLAN=${s}") def.vxlan)}
+        ''
+        + optionalString (def.tunnel != [ ]) ''
+          ${concatStringsSep "\n" (map (s: "Tunnel=${s}") def.tunnel)}
+        ''
+        + optionalString (def.xfrm != [ ]) ''
+          ${concatStringsSep "\n" (map (s: "Xfrm=${s}") def.xfrm)}
+        ''
+        + ''
+
+        ''
+        + flip concatMapStrings def.addresses (x: ''
+          [Address]
+          ${attrsToSection x.addressConfig}
+        '')
+        + flip concatMapStrings def.routingPolicyRules (x: ''
+          [RoutingPolicyRule]
+          ${attrsToSection x.routingPolicyRuleConfig}
+        '')
+        + flip concatMapStrings def.routes (x: ''
+          [Route]
+          ${attrsToSection x.routeConfig}
+        '')
+        + optionalString (def.dhcpV4Config != { }) ''
+          [DHCPv4]
+          ${attrsToSection def.dhcpV4Config}
+        ''
+        + optionalString (def.dhcpV6Config != { }) ''
+          [DHCPv6]
+          ${attrsToSection def.dhcpV6Config}
+        ''
+        + optionalString (def.dhcpV6PrefixDelegationConfig != { }) ''
+          [DHCPv6PrefixDelegation]
+          ${attrsToSection def.dhcpV6PrefixDelegationConfig}
+        ''
+        + optionalString (def.ipv6AcceptRAConfig != { }) ''
+          [IPv6AcceptRA]
+          ${attrsToSection def.ipv6AcceptRAConfig}
+        ''
+        + optionalString (def.dhcpServerConfig != { }) ''
+          [DHCPServer]
+          ${attrsToSection def.dhcpServerConfig}
+        ''
+        + optionalString (def.ipv6SendRAConfig != { }) ''
+          [IPv6SendRA]
+          ${attrsToSection def.ipv6SendRAConfig}
+        ''
+        + flip concatMapStrings def.ipv6Prefixes (x: ''
+          [IPv6Prefix]
+          ${attrsToSection x.ipv6PrefixConfig}
+        '')
+        + flip concatMapStrings def.dhcpServerStaticLeases (x: ''
+          [DHCPServerStaticLease]
+          ${attrsToSection x.dhcpServerStaticLeaseConfig}
+        '')
+        + def.extraConfig;
+    };
+
+  unitFiles = listToAttrs (map (name: {
+    name = "systemd/network/${name}";
+    value.source = "${cfg.units.${name}.unit}/${name}";
+  }) (attrNames cfg.units));
+in
+
+{
+  options = {
+
+    systemd.network.enable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to enable networkd or not.
+      '';
+    };
+
+    systemd.network.links = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule [ { options = linkOptions; } ]);
+      description = "Definition of systemd network links.";
+    };
+
+    systemd.network.netdevs = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule [ { options = netdevOptions; } ]);
+      description = "Definition of systemd network devices.";
+    };
+
+    systemd.network.networks = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule [ { options = networkOptions; } networkConfig ]);
+      description = "Definition of systemd networks.";
+    };
+
+    systemd.network.units = mkOption {
+      description = "Definition of networkd units.";
+      default = {};
+      internal = true;
+      type = with types; attrsOf (submodule (
+        { name, config, ... }:
+        { options = mapAttrs (_: x: x // { internal = true; }) concreteUnitOptions;
+          config = {
+            unit = mkDefault (makeUnit name config);
+          };
+        }));
+    };
+
+  };
+
+  config = 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;
+    }
+
+    (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;
+
+      # systemd-networkd is socket-activated by kernel netlink route change
+      # messages. It is important to have systemd buffer those on behalf of
+      # networkd.
+      systemd.sockets.systemd-networkd.wantedBy = [ "sockets.target" ];
+
+      systemd.services.systemd-networkd = {
+        wantedBy = [ "multi-user.target" ];
+        aliases = [ "dbus-org.freedesktop.network1.service" ];
+        restartTriggers = map (x: x.source) (attrValues unitFiles);
+      };
+
+      systemd.services.systemd-networkd-wait-online = {
+        wantedBy = [ "network-online.target" ];
+      };
+
+      systemd.services."systemd-network-wait-online@" = {
+        description = "Wait for Network Interface %I to be Configured";
+        conflicts = [ "shutdown.target" ];
+        requisite = [ "systemd-networkd.service" ];
+        after = [ "systemd-networkd.service" ];
+        serviceConfig = {
+          Type = "oneshot";
+          RemainAfterExit = true;
+          ExecStart = "${config.systemd.package}/lib/systemd/systemd-networkd-wait-online -i %I";
+        };
+      };
+
+      services.resolved.enable = mkDefault true;
+    })
+  ];
+}
diff --git a/nixos/modules/system/boot/pbkdf2-sha512.c b/nixos/modules/system/boot/pbkdf2-sha512.c
new file mode 100644
index 00000000000..67e989957ba
--- /dev/null
+++ b/nixos/modules/system/boot/pbkdf2-sha512.c
@@ -0,0 +1,38 @@
+#include <stdint.h>
+#include <string.h>
+#include <stdio.h>
+#include <openssl/evp.h>
+
+void hextorb(uint8_t* hex, uint8_t* rb)
+{
+	while(sscanf(hex, "%2x", rb) == 1)
+	{
+		hex += 2;
+		rb += 1;
+	}
+	*rb = '\0';
+}
+
+int main(int argc, char** argv)
+{
+	uint8_t k_user[2048];
+	uint8_t salt[2048];
+	uint8_t key[4096];
+
+	uint32_t key_length = atoi(argv[1]);
+	uint32_t iteration_count = atoi(argv[2]);
+
+	hextorb(argv[3], salt);
+	uint32_t salt_length = strlen(argv[3]) / 2;
+
+	fgets(k_user, 2048, stdin);
+	uint32_t k_user_length = strlen(k_user);
+	if(k_user[k_user_length - 1] == '\n') {
+			k_user[k_user_length - 1] = '\0';
+	}
+
+	PKCS5_PBKDF2_HMAC(k_user, k_user_length, salt, salt_length, iteration_count, EVP_sha512(), key_length, key);
+	fwrite(key, 1, key_length, stdout);
+
+	return 0;
+}
diff --git a/nixos/modules/system/boot/plymouth.nix b/nixos/modules/system/boot/plymouth.nix
new file mode 100644
index 00000000000..78ae8e9d20b
--- /dev/null
+++ b/nixos/modules/system/boot/plymouth.nix
@@ -0,0 +1,237 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  inherit (pkgs) plymouth nixos-icons;
+
+  cfg = config.boot.plymouth;
+  opt = options.boot.plymouth;
+
+  nixosBreezePlymouth = pkgs.plasma5Packages.breeze-plymouth.override {
+    logoFile = cfg.logo;
+    logoName = "nixos";
+    osName = "NixOS";
+    osVersion = config.system.nixos.release;
+  };
+
+  plymouthLogos = pkgs.runCommand "plymouth-logos" { inherit (cfg) logo; } ''
+    mkdir -p $out
+
+    # For themes that are compiled with PLYMOUTH_LOGO_FILE
+    mkdir -p $out/etc/plymouth
+    ln -s $logo $out/etc/plymouth/logo.png
+
+    # Logo for bgrt theme
+    # Note this is technically an abuse of watermark for the bgrt theme
+    # See: https://gitlab.freedesktop.org/plymouth/plymouth/-/issues/95#note_813768
+    mkdir -p $out/share/plymouth/themes/spinner
+    ln -s $logo $out/share/plymouth/themes/spinner/watermark.png
+
+    # Logo for spinfinity theme
+    # See: https://gitlab.freedesktop.org/plymouth/plymouth/-/issues/106
+    mkdir -p $out/share/plymouth/themes/spinfinity
+    ln -s $logo $out/share/plymouth/themes/spinfinity/header-image.png
+  '';
+
+  themesEnv = pkgs.buildEnv {
+    name = "plymouth-themes";
+    paths = [
+      plymouth
+      plymouthLogos
+    ] ++ cfg.themePackages;
+  };
+
+  configFile = pkgs.writeText "plymouthd.conf" ''
+    [Daemon]
+    ShowDelay=0
+    DeviceTimeout=8
+    Theme=${cfg.theme}
+    ${cfg.extraConfig}
+  '';
+
+in
+
+{
+
+  options = {
+
+    boot.plymouth = {
+
+      enable = mkEnableOption "Plymouth boot splash screen";
+
+      font = mkOption {
+        default = "${pkgs.dejavu_fonts.minimal}/share/fonts/truetype/DejaVuSans.ttf";
+        defaultText = literalExpression ''"''${pkgs.dejavu_fonts.minimal}/share/fonts/truetype/DejaVuSans.ttf"'';
+        type = types.path;
+        description = ''
+          Font file made available for displaying text on the splash screen.
+        '';
+      };
+
+      themePackages = mkOption {
+        default = lib.optional (cfg.theme == "breeze") nixosBreezePlymouth;
+        defaultText = literalDocBook ''
+          A NixOS branded variant of the breeze theme when
+          <literal>config.${opt.theme} == "breeze"</literal>, otherwise
+          <literal>[ ]</literal>.
+        '';
+        type = types.listOf types.package;
+        description = ''
+          Extra theme packages for plymouth.
+        '';
+      };
+
+      theme = mkOption {
+        default = "bgrt";
+        type = types.str;
+        description = ''
+          Splash screen theme.
+        '';
+      };
+
+      logo = mkOption {
+        type = types.path;
+        # Dimensions are 48x48 to match GDM logo
+        default = "${nixos-icons}/share/icons/hicolor/48x48/apps/nix-snowflake-white.png";
+        defaultText = literalExpression ''pkgs.fetchurl {
+          url = "https://nixos.org/logo/nixos-hires.png";
+          sha256 = "1ivzgd7iz0i06y36p8m5w48fd8pjqwxhdaavc0pxs7w1g7mcy5si";
+        }'';
+        description = ''
+          Logo which is displayed on the splash screen.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Literal string to append to <literal>configFile</literal>
+          and the config file generated by the plymouth module.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    boot.kernelParams = [ "splash" ];
+
+    # To be discoverable by systemd.
+    environment.systemPackages = [ plymouth ];
+
+    environment.etc."plymouth/plymouthd.conf".source = configFile;
+    environment.etc."plymouth/plymouthd.defaults".source = "${plymouth}/share/plymouth/plymouthd.defaults";
+    environment.etc."plymouth/logo.png".source = cfg.logo;
+    environment.etc."plymouth/themes".source = "${themesEnv}/share/plymouth/themes";
+    # XXX: Needed because we supply a different set of plugins in initrd.
+    environment.etc."plymouth/plugins".source = "${plymouth}/lib/plymouth";
+
+    systemd.packages = [ plymouth ];
+
+    systemd.services.plymouth-kexec.wantedBy = [ "kexec.target" ];
+    systemd.services.plymouth-halt.wantedBy = [ "halt.target" ];
+    systemd.services.plymouth-quit-wait.wantedBy = [ "multi-user.target" ];
+    systemd.services.plymouth-quit.wantedBy = [ "multi-user.target" ];
+    systemd.services.plymouth-poweroff.wantedBy = [ "poweroff.target" ];
+    systemd.services.plymouth-reboot.wantedBy = [ "reboot.target" ];
+    systemd.services.plymouth-read-write.wantedBy = [ "sysinit.target" ];
+    systemd.services.systemd-ask-password-plymouth.wantedBy = [ "multi-user.target" ];
+    systemd.paths.systemd-ask-password-plymouth.wantedBy = [ "multi-user.target" ];
+
+    boot.initrd.extraUtilsCommands = ''
+      copy_bin_and_libs ${plymouth}/bin/plymouth
+      copy_bin_and_libs ${plymouth}/bin/plymouthd
+
+      # Check if the actual requested theme is here
+      if [[ ! -d ${themesEnv}/share/plymouth/themes/${cfg.theme} ]]; then
+          echo "The requested theme: ${cfg.theme} is not provided by any of the packages in boot.plymouth.themePackages"
+          exit 1
+      fi
+
+      moduleName="$(sed -n 's,ModuleName *= *,,p' ${themesEnv}/share/plymouth/themes/${cfg.theme}/${cfg.theme}.plymouth)"
+
+      mkdir -p $out/lib/plymouth/renderers
+      # module might come from a theme
+      cp ${themesEnv}/lib/plymouth/{text,details,label,$moduleName}.so $out/lib/plymouth
+      cp ${plymouth}/lib/plymouth/renderers/{drm,frame-buffer}.so $out/lib/plymouth/renderers
+
+      mkdir -p $out/share/plymouth/themes
+      cp ${plymouth}/share/plymouth/plymouthd.defaults $out/share/plymouth
+
+      # Copy themes into working directory for patching
+      mkdir themes
+
+      # Use -L to copy the directories proper, not the symlinks to them.
+      # Copy all themes because they're not large assets, and bgrt depends on the ImageDir of
+      # the spinner theme.
+      cp -r -L ${themesEnv}/share/plymouth/themes/* themes
+
+      # Patch out any attempted references to the theme or plymouth's themes directory
+      chmod -R +w themes
+      find themes -type f | while read file
+      do
+        sed -i "s,/nix/.*/share/plymouth/themes,$out/share/plymouth/themes,g" $file
+      done
+
+      # Install themes
+      cp -r themes/* $out/share/plymouth/themes
+
+      # Install logo
+      mkdir -p $out/etc/plymouth
+      cp -r -L ${themesEnv}/etc/plymouth $out
+
+      # Setup font
+      mkdir -p $out/share/fonts
+      cp ${cfg.font} $out/share/fonts
+      mkdir -p $out/etc/fonts
+      cat > $out/etc/fonts/fonts.conf <<EOF
+      <?xml version="1.0"?>
+      <!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
+      <fontconfig>
+          <dir>$out/share/fonts</dir>
+      </fontconfig>
+      EOF
+    '';
+
+    boot.initrd.extraUtilsCommandsTest = ''
+      $out/bin/plymouthd --help >/dev/null
+      $out/bin/plymouth --help >/dev/null
+    '';
+
+    boot.initrd.extraUdevRulesCommands = ''
+      cp ${config.systemd.package}/lib/udev/rules.d/{70-uaccess,71-seat}.rules $out
+      sed -i '/loginctl/d' $out/71-seat.rules
+    '';
+
+    # We use `mkAfter` to ensure that LUKS password prompt would be shown earlier than the splash screen.
+    boot.initrd.preLVMCommands = mkAfter ''
+      mkdir -p /etc/plymouth
+      mkdir -p /run/plymouth
+      ln -s ${configFile} /etc/plymouth/plymouthd.conf
+      ln -s $extraUtils/share/plymouth/plymouthd.defaults /etc/plymouth/plymouthd.defaults
+      ln -s $extraUtils/share/plymouth/logo.png /etc/plymouth/logo.png
+      ln -s $extraUtils/share/plymouth/themes /etc/plymouth/themes
+      ln -s $extraUtils/lib/plymouth /etc/plymouth/plugins
+      ln -s $extraUtils/etc/fonts /etc/fonts
+
+      plymouthd --mode=boot --pid-file=/run/plymouth/pid --attach-to-session
+      plymouth show-splash
+    '';
+
+    boot.initrd.postMountCommands = ''
+      plymouth update-root-fs --new-root-dir="$targetRoot"
+    '';
+
+    # `mkBefore` to ensure that any custom prompts would be visible.
+    boot.initrd.preFailCommands = mkBefore ''
+      plymouth quit --wait
+    '';
+
+  };
+
+}
diff --git a/nixos/modules/system/boot/resolved.nix b/nixos/modules/system/boot/resolved.nix
new file mode 100644
index 00000000000..21d3fab2f35
--- /dev/null
+++ b/nixos/modules/system/boot/resolved.nix
@@ -0,0 +1,183 @@
+{ config, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.resolved;
+
+  dnsmasqResolve = config.services.dnsmasq.enable &&
+                   config.services.dnsmasq.resolveLocalQueries;
+
+in
+{
+
+  options = {
+
+    services.resolved.enable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to enable the systemd DNS resolver daemon.
+      '';
+    };
+
+    services.resolved.fallbackDns = mkOption {
+      default = [ ];
+      example = [ "8.8.8.8" "2001:4860:4860::8844" ];
+      type = types.listOf types.str;
+      description = ''
+        A list of IPv4 and IPv6 addresses to use as the fallback DNS servers.
+        If this option is empty, a compiled-in list of DNS servers is used instead.
+      '';
+    };
+
+    services.resolved.domains = mkOption {
+      default = config.networking.search;
+      defaultText = literalExpression "config.networking.search";
+      example = [ "example.com" ];
+      type = types.listOf types.str;
+      description = ''
+        A list of domains. These domains are used as search suffixes
+        when resolving single-label host names (domain names which
+        contain no dot), in order to qualify them into fully-qualified
+        domain names (FQDNs).
+
+        For compatibility reasons, if this setting is not specified,
+        the search domains listed in
+        <filename>/etc/resolv.conf</filename> are used instead, if
+        that file exists and any domains are configured in it.
+      '';
+    };
+
+    services.resolved.llmnr = mkOption {
+      default = "true";
+      example = "false";
+      type = types.enum [ "true" "resolve" "false" ];
+      description = ''
+        Controls Link-Local Multicast Name Resolution support
+        (RFC 4795) on the local host.
+
+        If set to
+
+        <variablelist>
+        <varlistentry>
+          <term><literal>"true"</literal></term>
+          <listitem><para>
+            Enables full LLMNR responder and resolver support.
+          </para></listitem>
+        </varlistentry>
+        <varlistentry>
+          <term><literal>"false"</literal></term>
+          <listitem><para>
+            Disables both.
+          </para></listitem>
+        </varlistentry>
+        <varlistentry>
+          <term><literal>"resolve"</literal></term>
+          <listitem><para>
+            Only resolution support is enabled, but responding is disabled.
+          </para></listitem>
+        </varlistentry>
+        </variablelist>
+      '';
+    };
+
+    services.resolved.dnssec = mkOption {
+      default = "allow-downgrade";
+      example = "true";
+      type = types.enum [ "true" "allow-downgrade" "false" ];
+      description = ''
+        If set to
+        <variablelist>
+        <varlistentry>
+          <term><literal>"true"</literal></term>
+          <listitem><para>
+            all DNS lookups are DNSSEC-validated locally (excluding
+            LLMNR and Multicast DNS). Note that this mode requires a
+            DNS server that supports DNSSEC. If the DNS server does
+            not properly support DNSSEC all validations will fail.
+          </para></listitem>
+        </varlistentry>
+        <varlistentry>
+          <term><literal>"allow-downgrade"</literal></term>
+          <listitem><para>
+            DNSSEC validation is attempted, but if the server does not
+            support DNSSEC properly, DNSSEC mode is automatically
+            disabled. Note that this mode makes DNSSEC validation
+            vulnerable to "downgrade" attacks, where an attacker might
+            be able to trigger a downgrade to non-DNSSEC mode by
+            synthesizing a DNS response that suggests DNSSEC was not
+            supported.
+          </para></listitem>
+        </varlistentry>
+        <varlistentry>
+          <term><literal>"false"</literal></term>
+          <listitem><para>
+            DNS lookups are not DNSSEC validated.
+          </para></listitem>
+        </varlistentry>
+        </variablelist>
+      '';
+    };
+
+    services.resolved.extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      description = ''
+        Extra config to append to resolved.conf.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = !config.networking.useHostResolvConf;
+        message = "Using host resolv.conf is not supported with systemd-resolved";
+      }
+    ];
+
+    users.users.systemd-resolve.group = "systemd-resolve";
+
+    # add resolve to nss hosts database if enabled and nscd enabled
+    # system.nssModules is configured in nixos/modules/system/boot/systemd.nix
+    # added with order 501 to allow modules to go before with mkBefore
+    system.nssDatabases.hosts = (mkOrder 501 ["resolve [!UNAVAIL=return]"]);
+
+    systemd.additionalUpstreamSystemUnits = [
+      "systemd-resolved.service"
+    ];
+
+    systemd.services.systemd-resolved = {
+      wantedBy = [ "multi-user.target" ];
+      aliases = [ "dbus-org.freedesktop.resolve1.service" ];
+      restartTriggers = [ config.environment.etc."systemd/resolved.conf".source ];
+    };
+
+    environment.etc = {
+      "systemd/resolved.conf".text = ''
+        [Resolve]
+        ${optionalString (config.networking.nameservers != [])
+          "DNS=${concatStringsSep " " config.networking.nameservers}"}
+        ${optionalString (cfg.fallbackDns != [])
+          "FallbackDNS=${concatStringsSep " " cfg.fallbackDns}"}
+        ${optionalString (cfg.domains != [])
+          "Domains=${concatStringsSep " " cfg.domains}"}
+        LLMNR=${cfg.llmnr}
+        DNSSEC=${cfg.dnssec}
+        ${config.services.resolved.extraConfig}
+      '';
+
+      # symlink the dynamic stub resolver of resolv.conf as recommended by upstream:
+      # https://www.freedesktop.org/software/systemd/man/systemd-resolved.html#/etc/resolv.conf
+      "resolv.conf".source = "/run/systemd/resolve/stub-resolv.conf";
+    } // optionalAttrs dnsmasqResolve {
+      "dnsmasq-resolv.conf".source = "/run/systemd/resolve/resolv.conf";
+    };
+
+    # If networkmanager is enabled, ask it to interface with resolved.
+    networking.networkmanager.dns = "systemd-resolved";
+
+  };
+
+}
diff --git a/nixos/modules/system/boot/shutdown.nix b/nixos/modules/system/boot/shutdown.nix
new file mode 100644
index 00000000000..8cda7b3aabe
--- /dev/null
+++ b/nixos/modules/system/boot/shutdown.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  # This unit saves the value of the system clock to the hardware
+  # clock on shutdown.
+  systemd.services.save-hwclock =
+    { description = "Save Hardware Clock";
+
+      wantedBy = [ "shutdown.target" ];
+
+      unitConfig = {
+        DefaultDependencies = false;
+        ConditionPathExists = "/dev/rtc";
+      };
+
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkgs.util-linux}/sbin/hwclock --systohc ${if config.time.hardwareClockInLocalTime then "--localtime" else "--utc"}";
+      };
+    };
+
+  boot.kernel.sysctl."kernel.poweroff_cmd" = "${config.systemd.package}/sbin/poweroff";
+
+}
diff --git a/nixos/modules/system/boot/stage-1-init.sh b/nixos/modules/system/boot/stage-1-init.sh
new file mode 100644
index 00000000000..8fcc1f02972
--- /dev/null
+++ b/nixos/modules/system/boot/stage-1-init.sh
@@ -0,0 +1,642 @@
+#! @shell@
+
+targetRoot=/mnt-root
+console=tty1
+verbose="@verbose@"
+
+info() {
+    if [[ -n "$verbose" ]]; then
+        echo "$@"
+    fi
+}
+
+extraUtils="@extraUtils@"
+export LD_LIBRARY_PATH=@extraUtils@/lib
+export PATH=@extraUtils@/bin
+ln -s @extraUtils@/bin /bin
+
+# Copy the secrets to their needed location
+if [ -d "@extraUtils@/secrets" ]; then
+    for secret in $(cd "@extraUtils@/secrets"; find . -type f); do
+        mkdir -p $(dirname "/$secret")
+        ln -s "@extraUtils@/secrets/$secret" "$secret"
+    done
+fi
+
+# Stop LVM complaining about fd3
+export LVM_SUPPRESS_FD_WARNINGS=true
+
+fail() {
+    if [ -n "$panicOnFail" ]; then exit 1; fi
+
+    @preFailCommands@
+
+    # If starting stage 2 failed, allow the user to repair the problem
+    # in an interactive shell.
+    cat <<EOF
+
+An error occurred in stage 1 of the boot process, which must mount the
+root filesystem on \`$targetRoot' and then start stage 2.  Press one
+of the following keys:
+
+EOF
+    if [ -n "$allowShell" ]; then cat <<EOF
+  i) to launch an interactive shell
+  f) to start an interactive shell having pid 1 (needed if you want to
+     start stage 2's init manually)
+EOF
+    fi
+    cat <<EOF
+  r) to reboot immediately
+  *) to ignore the error and continue
+EOF
+
+    read -n 1 reply
+
+    if [ -n "$allowShell" -a "$reply" = f ]; then
+        exec setsid @shell@ -c "exec @shell@ < /dev/$console >/dev/$console 2>/dev/$console"
+    elif [ -n "$allowShell" -a "$reply" = i ]; then
+        echo "Starting interactive shell..."
+        setsid @shell@ -c "exec @shell@ < /dev/$console >/dev/$console 2>/dev/$console" || fail
+    elif [ "$reply" = r ]; then
+        echo "Rebooting..."
+        reboot -f
+    else
+        info "Continuing..."
+    fi
+}
+
+trap 'fail' 0
+
+
+# Print a greeting.
+info
+info "<<< NixOS Stage 1 >>>"
+info
+
+# Make several required directories.
+mkdir -p /etc/udev
+touch /etc/fstab # to shut up mount
+ln -s /proc/mounts /etc/mtab # to shut up mke2fs
+touch /etc/udev/hwdb.bin # to shut up udev
+touch /etc/initrd-release
+
+# Function for waiting for device(s) to appear.
+waitDevice() {
+    local device="$1"
+    # Split device string using ':' as a delimiter as bcachefs
+    # uses this for multi-device filesystems, i.e. /dev/sda1:/dev/sda2:/dev/sda3
+    local IFS=':'
+
+    # USB storage devices tend to appear with some delay.  It would be
+    # great if we had a way to synchronously wait for them, but
+    # alas...  So just wait for a few seconds for the device to
+    # appear.
+    for dev in $device; do
+        if test ! -e $dev; then
+            echo -n "waiting for device $dev to appear..."
+            try=20
+            while [ $try -gt 0 ]; do
+                sleep 1
+                # also re-try lvm activation now that new block devices might have appeared
+                lvm vgchange -ay
+                # and tell udev to create nodes for the new LVs
+                udevadm trigger --action=add
+                if test -e $dev; then break; fi
+                echo -n "."
+                try=$((try - 1))
+            done
+            echo
+            [ $try -ne 0 ]
+        fi
+    done
+}
+
+# Mount special file systems.
+specialMount() {
+  local device="$1"
+  local mountPoint="$2"
+  local options="$3"
+  local fsType="$4"
+
+  mkdir -m 0755 -p "$mountPoint"
+  mount -n -t "$fsType" -o "$options" "$device" "$mountPoint"
+}
+source @earlyMountScript@
+
+# Copy initrd secrets from /.initrd-secrets to their actual destinations
+if [ -d "/.initrd-secrets" ]; then
+    #
+    # Secrets are named by their full destination pathname and stored
+    # under /.initrd-secrets/
+    #
+    for secret in $(cd "/.initrd-secrets"; find . -type f); do
+        mkdir -p $(dirname "/$secret")
+        cp "/.initrd-secrets/$secret" "$secret"
+    done
+fi
+
+# Log the script output to /dev/kmsg or /run/log/stage-1-init.log.
+mkdir -p /tmp
+mkfifo /tmp/stage-1-init.log.fifo
+logOutFd=8 && logErrFd=9
+eval "exec $logOutFd>&1 $logErrFd>&2"
+if test -w /dev/kmsg; then
+    tee -i < /tmp/stage-1-init.log.fifo /proc/self/fd/"$logOutFd" | while read -r line; do
+        if test -n "$line"; then
+            echo "<7>stage-1-init: [$(date)] $line" > /dev/kmsg
+        fi
+    done &
+else
+    mkdir -p /run/log
+    tee -i < /tmp/stage-1-init.log.fifo /run/log/stage-1-init.log &
+fi
+exec > /tmp/stage-1-init.log.fifo 2>&1
+
+
+# Process the kernel command line.
+export stage2Init=/init
+for o in $(cat /proc/cmdline); do
+    case $o in
+        console=*)
+            set -- $(IFS==; echo $o)
+            params=$2
+            set -- $(IFS=,; echo $params)
+            console=$1
+            ;;
+        init=*)
+            set -- $(IFS==; echo $o)
+            stage2Init=$2
+            ;;
+        boot.persistence=*)
+            set -- $(IFS==; echo $o)
+            persistence=$2
+            ;;
+        boot.persistence.opt=*)
+            set -- $(IFS==; echo $o)
+            persistence_opt=$2
+            ;;
+        boot.trace|debugtrace)
+            # Show each command.
+            set -x
+            ;;
+        boot.shell_on_fail)
+            allowShell=1
+            ;;
+        boot.debug1|debug1) # stop right away
+            allowShell=1
+            fail
+            ;;
+        boot.debug1devices) # stop after loading modules and creating device nodes
+            allowShell=1
+            debug1devices=1
+            ;;
+        boot.debug1mounts) # stop after mounting file systems
+            allowShell=1
+            debug1mounts=1
+            ;;
+        boot.panic_on_fail|stage1panic=1)
+            panicOnFail=1
+            ;;
+        root=*)
+            # If a root device is specified on the kernel command
+            # line, make it available through the symlink /dev/root.
+            # Recognise LABEL= and UUID= to support UNetbootin.
+            set -- $(IFS==; echo $o)
+            if [ $2 = "LABEL" ]; then
+                root="/dev/disk/by-label/$3"
+            elif [ $2 = "UUID" ]; then
+                root="/dev/disk/by-uuid/$3"
+            else
+                root=$2
+            fi
+            ln -s "$root" /dev/root
+            ;;
+        copytoram)
+            copytoram=1
+            ;;
+        findiso=*)
+            # if an iso name is supplied, try to find the device where
+            # the iso resides on
+            set -- $(IFS==; echo $o)
+            isoPath=$2
+            ;;
+    esac
+done
+
+# Set hostid before modules are loaded.
+# This is needed by the spl/zfs modules.
+@setHostId@
+
+# Load the required kernel modules.
+mkdir -p /lib
+ln -s @modulesClosure@/lib/modules /lib/modules
+ln -s @modulesClosure@/lib/firmware /lib/firmware
+echo @extraUtils@/bin/modprobe > /proc/sys/kernel/modprobe
+for i in @kernelModules@; do
+    info "loading module $(basename $i)..."
+    modprobe $i
+done
+
+
+# Create device nodes in /dev.
+@preDeviceCommands@
+info "running udev..."
+ln -sfn /proc/self/fd /dev/fd
+ln -sfn /proc/self/fd/0 /dev/stdin
+ln -sfn /proc/self/fd/1 /dev/stdout
+ln -sfn /proc/self/fd/2 /dev/stderr
+mkdir -p /etc/systemd
+ln -sfn @linkUnits@ /etc/systemd/network
+mkdir -p /etc/udev
+ln -sfn @udevRules@ /etc/udev/rules.d
+mkdir -p /dev/.mdadm
+systemd-udevd --daemon
+udevadm trigger --action=add
+udevadm settle
+
+
+# XXX: Use case usb->lvm will still fail, usb->luks->lvm is covered
+@preLVMCommands@
+
+info "starting device mapper and LVM..."
+lvm vgchange -ay
+
+if test -n "$debug1devices"; then fail; fi
+
+
+@postDeviceCommands@
+
+
+# Check the specified file system, if appropriate.
+checkFS() {
+    local device="$1"
+    local fsType="$2"
+
+    # Only check block devices.
+    if [ ! -b "$device" ]; then return 0; fi
+
+    # Don't check ROM filesystems.
+    if [ "$fsType" = iso9660 -o "$fsType" = udf ]; then return 0; fi
+
+    # Don't check resilient COWs as they validate the fs structures at mount time
+    if [ "$fsType" = btrfs -o "$fsType" = zfs -o "$fsType" = bcachefs ]; then return 0; fi
+
+    # Skip fsck for apfs as the fsck utility does not support repairing the filesystem (no -a option)
+    if [ "$fsType" = apfs ]; then return 0; fi
+
+    # Skip fsck for nilfs2 - not needed by design and no fsck tool for this filesystem.
+    if [ "$fsType" = nilfs2 ]; then return 0; fi
+
+    # Skip fsck for inherently readonly filesystems.
+    if [ "$fsType" = squashfs ]; then return 0; fi
+
+    # If we couldn't figure out the FS type, then skip fsck.
+    if [ "$fsType" = auto ]; then
+        echo 'cannot check filesystem with type "auto"!'
+        return 0
+    fi
+
+    # Device might be already mounted manually
+    # e.g. NBD-device or the host filesystem of the file which contains encrypted root fs
+    if mount | grep -q "^$device on "; then
+        echo "skip checking already mounted $device"
+        return 0
+    fi
+
+    # Optionally, skip fsck on journaling filesystems.  This option is
+    # a hack - it's mostly because e2fsck on ext3 takes much longer to
+    # recover the journal than the ext3 implementation in the kernel
+    # does (minutes versus seconds).
+    if test -z "@checkJournalingFS@" -a \
+        \( "$fsType" = ext3 -o "$fsType" = ext4 -o "$fsType" = reiserfs \
+        -o "$fsType" = xfs -o "$fsType" = jfs -o "$fsType" = f2fs \)
+    then
+        return 0
+    fi
+
+    echo "checking $device..."
+
+    fsckFlags=
+    if test "$fsType" != "btrfs"; then
+        fsckFlags="-V -a"
+    fi
+    fsck $fsckFlags "$device"
+    fsckResult=$?
+
+    if test $(($fsckResult | 2)) = $fsckResult; then
+        echo "fsck finished, rebooting..."
+        sleep 3
+        reboot -f
+    fi
+
+    if test $(($fsckResult | 4)) = $fsckResult; then
+        echo "$device has unrepaired errors, please fix them manually."
+        fail
+    fi
+
+    if test $fsckResult -ge 8; then
+        echo "fsck on $device failed."
+        fail
+    fi
+
+    return 0
+}
+
+
+# Function for mounting a file system.
+mountFS() {
+    local device="$1"
+    local mountPoint="$2"
+    local options="$3"
+    local fsType="$4"
+
+    if [ "$fsType" = auto ]; then
+        fsType=$(blkid -o value -s TYPE "$device")
+        if [ -z "$fsType" ]; then fsType=auto; fi
+    fi
+
+    # Filter out x- options, which busybox doesn't do yet.
+    local optionsFiltered="$(IFS=,; for i in $options; do if [ "${i:0:2}" != "x-" ]; then echo -n $i,; fi; done)"
+    # Prefix (lower|upper|work)dir with /mnt-root (overlayfs)
+    local optionsPrefixed="$( echo "$optionsFiltered" | sed -E 's#\<(lowerdir|upperdir|workdir)=#\1=/mnt-root#g' )"
+
+    echo "$device /mnt-root$mountPoint $fsType $optionsPrefixed" >> /etc/fstab
+
+    checkFS "$device" "$fsType"
+
+    # Optionally resize the filesystem.
+    case $options in
+        *x-nixos.autoresize*)
+            if [ "$fsType" = ext2 -o "$fsType" = ext3 -o "$fsType" = ext4 ]; then
+                modprobe "$fsType"
+                echo "resizing $device..."
+                e2fsck -fp "$device"
+                resize2fs "$device"
+            elif [ "$fsType" = f2fs ]; then
+                echo "resizing $device..."
+                fsck.f2fs -fp "$device"
+                resize.f2fs "$device"
+            fi
+            ;;
+    esac
+
+    # Create backing directories for overlayfs
+    if [ "$fsType" = overlay ]; then
+        for i in upper work; do
+             dir="$( echo "$optionsPrefixed" | grep -o "${i}dir=[^,]*" )"
+             mkdir -m 0700 -p "${dir##*=}"
+        done
+    fi
+
+    info "mounting $device on $mountPoint..."
+
+    mkdir -p "/mnt-root$mountPoint"
+
+    # For ZFS and CIFS mounts, retry a few times before giving up.
+    # We do this for ZFS as a workaround for issue NixOS/nixpkgs#25383.
+    local n=0
+    while true; do
+        mount "/mnt-root$mountPoint" && break
+        if [ \( "$fsType" != cifs -a "$fsType" != zfs \) -o "$n" -ge 10 ]; then fail; break; fi
+        echo "retrying..."
+        sleep 1
+        n=$((n + 1))
+    done
+
+    [ "$mountPoint" == "/" ] &&
+        [ -f "/mnt-root/etc/NIXOS_LUSTRATE" ] &&
+        lustrateRoot "/mnt-root"
+
+    true
+}
+
+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
+
+    mkdir -m 0755 -p "$root/old-root.tmp"
+
+    echo
+    echo "Moving impurities out of the way:"
+    for d in "$root"/*
+    do
+        [ "$d" == "$root/nix"          ] && continue
+        [ "$d" == "$root/boot"         ] && continue # Don't render the system unbootable
+        [ "$d" == "$root/old-root.tmp" ] && continue
+
+        mv -v "$d" "$root/old-root.tmp"
+    done
+
+    # Use .tmp to make sure subsequent invokations don't clash
+    mv -v "$root/old-root.tmp" "$root/old-root"
+
+    mkdir -m 0755 -p "$root/etc"
+    touch "$root/etc/NIXOS"
+
+    exec 4< "$root/old-root/etc/NIXOS_LUSTRATE"
+
+    echo
+    echo "Restoring selected impurities:"
+    while read -u 4 keeper; do
+        dirname="$(dirname "$keeper")"
+        mkdir -m 0755 -p "$root/$dirname"
+        cp -av "$root/old-root/$keeper" "$root/$keeper"
+    done
+
+    exec 4>&-
+}
+
+
+
+if test -e /sys/power/resume -a -e /sys/power/disk; then
+    if test -n "@resumeDevice@" && waitDevice "@resumeDevice@"; then
+        resumeDev="@resumeDevice@"
+        resumeInfo="$(udevadm info -q property "$resumeDev" )"
+    else
+        for sd in @resumeDevices@; do
+            # Try to detect resume device. According to Ubuntu bug:
+            # https://bugs.launchpad.net/ubuntu/+source/pm-utils/+bug/923326/comments/1
+            # when there are multiple swap devices, we can't know where the hibernate
+            # image will reside. We can check all of them for swsuspend blkid.
+            if waitDevice "$sd"; then
+                resumeInfo="$(udevadm info -q property "$sd")"
+                if [ "$(echo "$resumeInfo" | sed -n 's/^ID_FS_TYPE=//p')" = "swsuspend" ]; then
+                    resumeDev="$sd"
+                    break
+                fi
+            fi
+        done
+    fi
+    if test -n "$resumeDev"; then
+        resumeMajor="$(echo "$resumeInfo" | sed -n 's/^MAJOR=//p')"
+        resumeMinor="$(echo "$resumeInfo" | sed -n 's/^MINOR=//p')"
+        echo "$resumeMajor:$resumeMinor" > /sys/power/resume 2> /dev/null || echo "failed to resume..."
+    fi
+fi
+
+# If we have a path to an iso file, find the iso and link it to /dev/root
+if [ -n "$isoPath" ]; then
+  mkdir -p /findiso
+
+  for delay in 5 10; do
+    blkid | while read -r line; do
+      device=$(echo "$line" | sed 's/:.*//')
+      type=$(echo "$line" | sed 's/.*TYPE="\([^"]*\)".*/\1/')
+
+      mount -t "$type" "$device" /findiso
+      if [ -e "/findiso$isoPath" ]; then
+        ln -sf "/findiso$isoPath" /dev/root
+        break 2
+      else
+        umount /findiso
+      fi
+    done
+
+    sleep "$delay"
+  done
+fi
+
+# Try to find and mount the root device.
+mkdir -p $targetRoot
+
+exec 3< @fsInfo@
+
+while read -u 3 mountPoint; do
+    read -u 3 device
+    read -u 3 fsType
+    read -u 3 options
+
+    # !!! Really quick hack to support bind mounts, i.e., where the
+    # "device" should be taken relative to /mnt-root, not /.  Assume
+    # that every device that starts with / but doesn't start with /dev
+    # is a bind mount.
+    pseudoDevice=
+    case $device in
+        /dev/*)
+            ;;
+        //*)
+            # Don't touch SMB/CIFS paths.
+            pseudoDevice=1
+            ;;
+        /*)
+            device=/mnt-root$device
+            ;;
+        *)
+            # Not an absolute path; assume that it's a pseudo-device
+            # like an NFS path (e.g. "server:/path").
+            pseudoDevice=1
+            ;;
+    esac
+
+    if test -z "$pseudoDevice" && ! waitDevice "$device"; then
+        # If it doesn't appear, try to mount it anyway (and
+        # probably fail).  This is a fallback for non-device "devices"
+        # that we don't properly recognise.
+        echo "Timed out waiting for device $device, trying to mount anyway."
+    fi
+
+    # Wait once more for the udev queue to empty, just in case it's
+    # doing something with $device right now.
+    udevadm settle
+
+    # If copytoram is enabled: skip mounting the ISO and copy its content to a tmpfs.
+    if [ -n "$copytoram" ] && [ "$device" = /dev/root ] && [ "$mountPoint" = /iso ]; then
+      fsType=$(blkid -o value -s TYPE "$device")
+      fsSize=$(blockdev --getsize64 "$device" || stat -Lc '%s' "$device")
+
+      mkdir -p /tmp-iso
+      mount -t "$fsType" /dev/root /tmp-iso
+      mountFS tmpfs /iso size="$fsSize" tmpfs
+
+      cp -r /tmp-iso/* /mnt-root/iso/
+
+      umount /tmp-iso
+      rmdir /tmp-iso
+      continue
+    fi
+
+    if [ "$mountPoint" = / ] && [ "$device" = tmpfs ] && [ ! -z "$persistence" ]; then
+        echo persistence...
+        waitDevice "$persistence"
+        echo enabling persistence...
+        mountFS "$persistence" "$mountPoint" "$persistence_opt" "auto"
+        continue
+    fi
+
+    mountFS "$device" "$mountPoint" "$options" "$fsType"
+done
+
+exec 3>&-
+
+
+@postMountCommands@
+
+
+# Emit a udev rule for /dev/root to prevent systemd from complaining.
+if [ -e /mnt-root/iso ]; then
+    eval $(udevadm info --export --export-prefix=ROOT_ --device-id-of-file=/mnt-root/iso)
+else
+    eval $(udevadm info --export --export-prefix=ROOT_ --device-id-of-file=$targetRoot)
+fi
+if [ "$ROOT_MAJOR" -a "$ROOT_MINOR" -a "$ROOT_MAJOR" != 0 ]; then
+    mkdir -p /run/udev/rules.d
+    echo 'ACTION=="add|change", SUBSYSTEM=="block", ENV{MAJOR}=="'$ROOT_MAJOR'", ENV{MINOR}=="'$ROOT_MINOR'", SYMLINK+="root"' > /run/udev/rules.d/61-dev-root-link.rules
+fi
+
+
+# Stop udevd.
+udevadm control --exit
+
+# Reset the logging file descriptors.
+# Do this just before pkill, which will kill the tee process.
+exec 1>&$logOutFd 2>&$logErrFd
+eval "exec $logOutFd>&- $logErrFd>&-"
+
+# Kill any remaining processes, just to be sure we're not taking any
+# with us into stage 2. But keep storage daemons like unionfs-fuse.
+#
+# Storage daemons are distinguished by an @ in front of their command line:
+# https://www.freedesktop.org/wiki/Software/systemd/RootStorageDaemons/
+for pid in $(pgrep -v -f '^@'); do
+    # Make sure we don't kill kernel processes, see #15226 and:
+    # http://stackoverflow.com/questions/12213445/identifying-kernel-threads
+    readlink "/proc/$pid/exe" &> /dev/null || continue
+    # Try to avoid killing ourselves.
+    [ $pid -eq $$ ] && continue
+    kill -9 "$pid"
+done
+
+if test -n "$debug1mounts"; then fail; fi
+
+
+# Restore /proc/sys/kernel/modprobe to its original value.
+echo /sbin/modprobe > /proc/sys/kernel/modprobe
+
+
+# Start stage 2.  `switch_root' deletes all files in the ramfs on the
+# current root.  The path has to be valid in the chroot not outside.
+if [ ! -e "$targetRoot/$stage2Init" ]; then
+    stage2Check=${stage2Init}
+    while [ "$stage2Check" != "${stage2Check%/*}" ] && [ ! -L "$targetRoot/$stage2Check" ]; do
+        stage2Check=${stage2Check%/*}
+    done
+    if [ ! -L "$targetRoot/$stage2Check" ]; then
+        echo "stage 2 init script ($targetRoot/$stage2Init) not found"
+        fail
+    fi
+fi
+
+mkdir -m 0755 -p $targetRoot/proc $targetRoot/sys $targetRoot/dev $targetRoot/run
+
+mount --move /proc $targetRoot/proc
+mount --move /sys $targetRoot/sys
+mount --move /dev $targetRoot/dev
+mount --move /run $targetRoot/run
+
+exec env -i $(type -P switch_root) "$targetRoot" "$stage2Init"
+
+fail # should never be reached
diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix
new file mode 100644
index 00000000000..8b011d91563
--- /dev/null
+++ b/nixos/modules/system/boot/stage-1.nix
@@ -0,0 +1,717 @@
+# This module builds the initial ramdisk, which contains an init
+# script that performs the first stage of booting the system: it loads
+# the modules necessary to mount the root file system, then calls the
+# init in the root file system to start the second boot stage.
+
+{ config, lib, utils, pkgs, ... }:
+
+with lib;
+
+let
+
+  udev = config.systemd.package;
+
+  kernel-name = config.boot.kernelPackages.kernel.name or "kernel";
+
+  modulesTree = config.system.modulesTree.override { name = kernel-name + "-modules"; };
+  firmware = config.hardware.firmware;
+
+
+  # Determine the set of modules that we need to mount the root FS.
+  modulesClosure = pkgs.makeModulesClosure {
+    rootModules = config.boot.initrd.availableKernelModules ++ config.boot.initrd.kernelModules;
+    kernel = modulesTree;
+    firmware = firmware;
+    allowMissing = false;
+  };
+
+
+  # The initrd only has to mount `/` or any FS marked as necessary for
+  # booting (such as the FS containing `/nix/store`, or an FS needed for
+  # mounting `/`, like `/` on a loopback).
+  fileSystems = filter utils.fsNeededForBoot config.system.build.fileSystems;
+
+  # A utility for enumerating the shared-library dependencies of a program
+  findLibs = pkgs.buildPackages.writeShellScriptBin "find-libs" ''
+    set -euo pipefail
+
+    declare -A seen
+    left=()
+
+    patchelf="${pkgs.buildPackages.patchelf}/bin/patchelf"
+
+    function add_needed {
+      rpath="$($patchelf --print-rpath $1)"
+      dir="$(dirname $1)"
+      for lib in $($patchelf --print-needed $1); do
+        left+=("$lib" "$rpath" "$dir")
+      done
+    }
+
+    add_needed "$1"
+
+    while [ ''${#left[@]} -ne 0 ]; do
+      next=''${left[0]}
+      rpath=''${left[1]}
+      ORIGIN=''${left[2]}
+      left=("''${left[@]:3}")
+      if [ -z ''${seen[$next]+x} ]; then
+        seen[$next]=1
+
+        # Ignore the dynamic linker which for some reason appears as a DT_NEEDED of glibc but isn't in glibc's RPATH.
+        case "$next" in
+          ld*.so.?) continue;;
+        esac
+
+        IFS=: read -ra paths <<< $rpath
+        res=
+        for path in "''${paths[@]}"; do
+          path=$(eval "echo $path")
+          if [ -f "$path/$next" ]; then
+              res="$path/$next"
+              echo "$res"
+              add_needed "$res"
+              break
+          fi
+        done
+        if [ -z "$res" ]; then
+          echo "Couldn't satisfy dependency $next" >&2
+          exit 1
+        fi
+      fi
+    done
+  '';
+
+  # Some additional utilities needed in stage 1, like mount, lvm, fsck
+  # etc.  We don't want to bring in all of those packages, so we just
+  # 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"
+    { nativeBuildInputs = [pkgs.buildPackages.nukeReferences];
+      allowedReferences = [ "out" ]; # prevent accidents like glibc being included in the initrd
+    }
+    ''
+      set +o pipefail
+
+      mkdir -p $out/bin $out/lib
+      ln -s $out/bin $out/sbin
+
+      copy_bin_and_libs () {
+        [ -f "$out/bin/$(basename $1)" ] && rm "$out/bin/$(basename $1)"
+        cp -pdv $1 $out/bin
+      }
+
+      # Copy BusyBox.
+      for BIN in ${pkgs.busybox}/{s,}bin/*; do
+        copy_bin_and_libs $BIN
+      done
+
+      # Copy some util-linux stuff.
+      copy_bin_and_libs ${pkgs.util-linux}/sbin/blkid
+
+      # Copy dmsetup and lvm.
+      copy_bin_and_libs ${getBin pkgs.lvm2}/bin/dmsetup
+      copy_bin_and_libs ${getBin pkgs.lvm2}/bin/lvm
+
+      # Add RAID mdadm tool.
+      copy_bin_and_libs ${pkgs.mdadm}/sbin/mdadm
+      copy_bin_and_libs ${pkgs.mdadm}/sbin/mdmon
+
+      # Copy udev.
+      copy_bin_and_libs ${udev}/bin/udevadm
+      copy_bin_and_libs ${udev}/lib/systemd/systemd-sysctl
+      for BIN in ${udev}/lib/udev/*_id; do
+        copy_bin_and_libs $BIN
+      done
+      # systemd-udevd is only a symlink to udevadm these days
+      ln -sf udevadm $out/bin/systemd-udevd
+
+      # Copy modprobe.
+      copy_bin_and_libs ${pkgs.kmod}/bin/kmod
+      ln -sf kmod $out/bin/modprobe
+
+      # 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.
+        copy_bin_and_libs ${pkgs.e2fsprogs}/sbin/resize2fs
+      ''}
+
+      # Copy multipath.
+      ${optionalString config.services.multipath.enable ''
+        copy_bin_and_libs ${config.services.multipath.package}/bin/multipath
+        copy_bin_and_libs ${config.services.multipath.package}/bin/multipathd
+        # Copy lib/multipath manually.
+        cp -rpv ${config.services.multipath.package}/lib/multipath $out/lib
+      ''}
+
+      # Copy secrets if needed.
+      #
+      # TODO: move out to a separate script; see #85000.
+      ${optionalString (!config.boot.loader.supportsInitrdSecrets)
+          (concatStringsSep "\n" (mapAttrsToList (dest: source:
+             let source' = if source == null then dest else source; in
+               ''
+                  mkdir -p $(dirname "$out/secrets/${dest}")
+                  # Some programs (e.g. ssh) doesn't like secrets to be
+                  # symlinks, so we use `cp -L` here to match the
+                  # behaviour when secrets are natively supported.
+                  cp -Lr ${source'} "$out/secrets/${dest}"
+                ''
+          ) config.boot.initrd.secrets))
+       }
+
+      ${config.boot.initrd.extraUtilsCommands}
+
+      # Copy ld manually since it isn't detected correctly
+      cp -pv ${pkgs.stdenv.cc.libc.out}/lib/ld*.so.? $out/lib
+
+      # Copy all of the needed libraries
+      find $out/bin $out/lib -type f | while read BIN; do
+        echo "Copying libs for executable $BIN"
+        for LIB in $(${findLibs}/bin/find-libs $BIN); do
+          TGT="$out/lib/$(basename $LIB)"
+          if [ ! -f "$TGT" ]; then
+            SRC="$(readlink -e $LIB)"
+            cp -pdv "$SRC" "$TGT"
+          fi
+        done
+      done
+
+      # Strip binaries further than normal.
+      chmod -R u+w $out
+      stripDirs "$STRIP" "lib bin" "-s"
+
+      # Run patchelf to make the programs refer to the copied libraries.
+      find $out/bin $out/lib -type f | while read i; do
+        if ! test -L $i; then
+          nuke-refs -e $out $i
+        fi
+      done
+
+      find $out/bin -type f | while read i; do
+        if ! test -L $i; then
+          echo "patching $i..."
+          patchelf --set-interpreter $out/lib/ld*.so.? --set-rpath $out/lib $i || true
+        fi
+      done
+
+      if [ -z "${toString (pkgs.stdenv.hostPlatform != pkgs.stdenv.buildPlatform)}" ]; then
+      # Make sure that the patchelf'ed binaries still work.
+      echo "testing patched programs..."
+      $out/bin/ash -c 'echo hello world' | grep "hello world"
+      export LD_LIBRARY_PATH=$out/lib
+      $out/bin/mount --help 2>&1 | grep -q "BusyBox"
+      $out/bin/blkid -V 2>&1 | grep -q 'libblkid'
+      $out/bin/udevadm --version
+      $out/bin/dmsetup --version 2>&1 | tee -a log | grep -q "version:"
+      LVM_SYSTEM_DIR=$out $out/bin/lvm version 2>&1 | tee -a log | grep -q "LVM"
+      $out/bin/mdadm --version
+      ${optionalString config.services.multipath.enable ''
+        ($out/bin/multipath || true) 2>&1 | grep -q 'need to be root'
+        ($out/bin/multipathd || true) 2>&1 | grep -q 'need to be root'
+      ''}
+
+      ${config.boot.initrd.extraUtilsCommandsTest}
+      fi
+    ''; # */
+
+
+  # Networkd link files are used early by udev to set up interfaces early.
+  # This must be done in stage 1 to avoid race conditions between udev and
+  # network daemons.
+  linkUnits = pkgs.runCommand "link-units" {
+      allowedReferences = [ extraUtils ];
+      preferLocalBuild = true;
+    } (''
+      mkdir -p $out
+      cp -v ${udev}/lib/systemd/network/*.link $out/
+      '' + (
+      let
+        links = filterAttrs (n: v: hasSuffix ".link" n) config.systemd.network.units;
+        files = mapAttrsToList (n: v: "${v.unit}/${n}") links;
+      in
+        concatMapStringsSep "\n" (file: "cp -v ${file} $out/") files
+      ));
+
+  udevRules = pkgs.runCommand "udev-rules" {
+      allowedReferences = [ extraUtils ];
+      preferLocalBuild = true;
+    } ''
+      mkdir -p $out
+
+      echo 'ENV{LD_LIBRARY_PATH}="${extraUtils}/lib"' > $out/00-env.rules
+
+      cp -v ${udev}/lib/udev/rules.d/60-cdrom_id.rules $out/
+      cp -v ${udev}/lib/udev/rules.d/60-persistent-storage.rules $out/
+      cp -v ${udev}/lib/udev/rules.d/75-net-description.rules $out/
+      cp -v ${udev}/lib/udev/rules.d/80-drivers.rules $out/
+      cp -v ${udev}/lib/udev/rules.d/80-net-setup-link.rules $out/
+      cp -v ${pkgs.lvm2}/lib/udev/rules.d/*.rules $out/
+      ${config.boot.initrd.extraUdevRulesCommands}
+
+      for i in $out/*.rules; do
+          substituteInPlace $i \
+            --replace ata_id ${extraUtils}/bin/ata_id \
+            --replace scsi_id ${extraUtils}/bin/scsi_id \
+            --replace cdrom_id ${extraUtils}/bin/cdrom_id \
+            --replace ${pkgs.coreutils}/bin/basename ${extraUtils}/bin/basename \
+            --replace ${pkgs.util-linux}/bin/blkid ${extraUtils}/bin/blkid \
+            --replace ${getBin pkgs.lvm2}/bin ${extraUtils}/bin \
+            --replace ${pkgs.mdadm}/sbin ${extraUtils}/sbin \
+            --replace ${pkgs.bash}/bin/sh ${extraUtils}/bin/sh \
+            --replace ${udev} ${extraUtils}
+      done
+
+      # Work around a bug in QEMU, which doesn't implement the "READ
+      # DISC INFORMATION" SCSI command:
+      #   https://bugzilla.redhat.com/show_bug.cgi?id=609049
+      # As a result, `cdrom_id' doesn't print
+      # ID_CDROM_MEDIA_TRACK_COUNT_DATA, which in turn prevents the
+      # /dev/disk/by-label symlinks from being created.  We need these
+      # in the NixOS installation CD, so use ID_CDROM_MEDIA in the
+      # corresponding udev rules for now.  This was the behaviour in
+      # udev <= 154.  See also
+      #   http://www.spinics.net/lists/hotplug/msg03935.html
+      substituteInPlace $out/60-persistent-storage.rules \
+        --replace ID_CDROM_MEDIA_TRACK_COUNT_DATA ID_CDROM_MEDIA
+    ''; # */
+
+
+  # The init script of boot stage 1 (loading kernel modules for
+  # mounting the root FS).
+  bootStage1 = pkgs.substituteAll {
+    src = ./stage-1-init.sh;
+
+    shell = "${extraUtils}/bin/ash";
+
+    isExecutable = true;
+
+    postInstall = ''
+      echo checking syntax
+      # check both with bash
+      ${pkgs.buildPackages.bash}/bin/sh -n $target
+      # and with ash shell, just in case
+      ${pkgs.buildPackages.busybox}/bin/ash -n $target
+    '';
+
+    inherit linkUnits udevRules extraUtils modulesClosure;
+
+    inherit (config.boot) resumeDevice;
+
+    inherit (config.system.build) earlyMountScript;
+
+    inherit (config.boot.initrd) checkJournalingFS verbose
+      preLVMCommands preDeviceCommands postDeviceCommands postMountCommands preFailCommands kernelModules;
+
+    resumeDevices = map (sd: if sd ? device then sd.device else "/dev/disk/by-label/${sd.label}")
+                    (filter (sd: hasPrefix "/dev/" sd.device && !sd.randomEncryption.enable
+                             # Don't include zram devices
+                             && !(hasPrefix "/dev/zram" sd.device)
+                            ) config.swapDevices);
+
+    fsInfo =
+      let f = fs: [ fs.mountPoint (if fs.device != null then fs.device else "/dev/disk/by-label/${fs.label}") fs.fsType (builtins.concatStringsSep "," fs.options) ];
+      in pkgs.writeText "initrd-fsinfo" (concatStringsSep "\n" (concatMap f fileSystems));
+
+    setHostId = optionalString (config.networking.hostId != null) ''
+      hi="${config.networking.hostId}"
+      ${if pkgs.stdenv.isBigEndian then ''
+        echo -ne "\x''${hi:0:2}\x''${hi:2:2}\x''${hi:4:2}\x''${hi:6:2}" > /etc/hostid
+      '' else ''
+        echo -ne "\x''${hi:6:2}\x''${hi:4:2}\x''${hi:2:2}\x''${hi:0:2}" > /etc/hostid
+      ''}
+    '';
+  };
+
+
+  # The closure of the init script of boot stage 1 is what we put in
+  # the initial RAM disk.
+  initialRamdisk = pkgs.makeInitrd {
+    name = "initrd-${kernel-name}";
+    inherit (config.boot.initrd) compressor compressorArgs prepend;
+
+    contents =
+      [ { object = bootStage1;
+          symlink = "/init";
+        }
+        { object = pkgs.writeText "mdadm.conf" config.boot.initrd.mdadmConf;
+          symlink = "/etc/mdadm.conf";
+        }
+        { object = pkgs.runCommand "initrd-kmod-blacklist-ubuntu" {
+              src = "${pkgs.kmod-blacklist-ubuntu}/modprobe.conf";
+              preferLocalBuild = true;
+            } ''
+              target=$out
+              ${pkgs.buildPackages.perl}/bin/perl -0pe 's/## file: iwlwifi.conf(.+?)##/##/s;' $src > $out
+            '';
+          symlink = "/etc/modprobe.d/ubuntu.conf";
+        }
+        { object = config.environment.etc."modprobe.d/nixos.conf".source;
+          symlink = "/etc/modprobe.d/nixos.conf";
+        }
+        { object = pkgs.kmod-debian-aliases;
+          symlink = "/etc/modprobe.d/debian.conf";
+        }
+      ] ++ lib.optionals config.services.multipath.enable [
+        { object = pkgs.runCommand "multipath.conf" {
+              src = config.environment.etc."multipath.conf".text;
+              preferLocalBuild = true;
+            } ''
+              target=$out
+              printf "$src" > $out
+              substituteInPlace $out \
+                --replace ${config.services.multipath.package}/lib ${extraUtils}/lib
+            '';
+          symlink = "/etc/multipath.conf";
+        }
+      ] ++ (lib.mapAttrsToList
+        (symlink: options:
+          {
+            inherit symlink;
+            object = options.source;
+          }
+        )
+        config.boot.initrd.extraFiles);
+  };
+
+  # Script to add secret files to the initrd at bootloader update time
+  initialRamdiskSecretAppender =
+    let
+      compressorExe = initialRamdisk.compressorExecutableFunction pkgs;
+    in pkgs.writeScriptBin "append-initrd-secrets"
+      ''
+        #!${pkgs.bash}/bin/bash -e
+        function usage {
+          echo "USAGE: $0 INITRD_FILE" >&2
+          echo "Appends this configuration's secrets to INITRD_FILE" >&2
+        }
+
+        if [ $# -ne 1 ]; then
+          usage
+          exit 1
+        fi
+
+        if [ "$1"x = "--helpx" ]; then
+          usage
+          exit 0
+        fi
+
+        ${lib.optionalString (config.boot.initrd.secrets == {})
+            "exit 0"}
+
+        export PATH=${pkgs.coreutils}/bin:${pkgs.cpio}/bin:${pkgs.gzip}/bin:${pkgs.findutils}/bin
+
+        function cleanup {
+          if [ -n "$tmp" -a -d "$tmp" ]; then
+            rm -fR "$tmp"
+          fi
+        }
+        trap cleanup EXIT
+
+        tmp=$(mktemp -d ''${TMPDIR:-/tmp}/initrd-secrets.XXXXXXXXXX)
+
+        ${lib.concatStringsSep "\n" (mapAttrsToList (dest: source:
+            let source' = if source == null then dest else toString source; in
+              ''
+                mkdir -p $(dirname "$tmp/.initrd-secrets/${dest}")
+                cp -a ${source'} "$tmp/.initrd-secrets/${dest}"
+              ''
+          ) config.boot.initrd.secrets)
+         }
+
+        (cd "$tmp" && find . -print0 | sort -z | cpio --quiet -o -H newc -R +0:+0 --reproducible --null) | \
+          ${compressorExe} ${lib.escapeShellArgs initialRamdisk.compressorArgs} >> "$1"
+      '';
+
+in
+
+{
+  options = {
+
+    boot.resumeDevice = mkOption {
+      type = types.str;
+      default = "";
+      example = "/dev/sda3";
+      description = ''
+        Device for manual resume attempt during boot. This should be used primarily
+        if you want to resume from file. If left empty, the swap partitions are used.
+        Specify here the device where the file resides.
+        You should also use <varname>boot.kernelParams</varname> to specify
+        <literal><replaceable>resume_offset</replaceable></literal>.
+      '';
+    };
+
+    boot.initrd.enable = mkOption {
+      type = types.bool;
+      default = !config.boot.isContainer;
+      defaultText = literalExpression "!config.boot.isContainer";
+      description = ''
+        Whether to enable the NixOS initial RAM disk (initrd). This may be
+        needed to perform some initialisation tasks (like mounting
+        network/encrypted file systems) before continuing the boot process.
+      '';
+    };
+
+    boot.initrd.extraFiles = mkOption {
+      default = { };
+      type = types.attrsOf
+        (types.submodule {
+          options = {
+            source = mkOption {
+              type = types.package;
+              description = "The object to make available inside the initrd.";
+            };
+          };
+        });
+      description = ''
+        Extra files to link and copy in to the initrd.
+      '';
+    };
+
+    boot.initrd.prepend = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      description = ''
+        Other initrd files to prepend to the final initrd we are building.
+      '';
+    };
+
+    boot.initrd.checkJournalingFS = mkOption {
+      default = true;
+      type = types.bool;
+      description = ''
+        Whether to run <command>fsck</command> on journaling filesystems such as ext3.
+      '';
+    };
+
+    boot.initrd.mdadmConf = mkOption {
+      default = "";
+      type = types.lines;
+      description = ''
+        Contents of <filename>/etc/mdadm.conf</filename> in stage 1.
+      '';
+    };
+
+    boot.initrd.preLVMCommands = mkOption {
+      default = "";
+      type = types.lines;
+      description = ''
+        Shell commands to be executed immediately before LVM discovery.
+      '';
+    };
+
+    boot.initrd.preDeviceCommands = mkOption {
+      default = "";
+      type = types.lines;
+      description = ''
+        Shell commands to be executed before udev is started to create
+        device nodes.
+      '';
+    };
+
+    boot.initrd.postDeviceCommands = mkOption {
+      default = "";
+      type = types.lines;
+      description = ''
+        Shell commands to be executed immediately after stage 1 of the
+        boot has loaded kernel modules and created device nodes in
+        <filename>/dev</filename>.
+      '';
+    };
+
+    boot.initrd.postMountCommands = mkOption {
+      default = "";
+      type = types.lines;
+      description = ''
+        Shell commands to be executed immediately after the stage 1
+        filesystems have been mounted.
+      '';
+    };
+
+    boot.initrd.preFailCommands = mkOption {
+      default = "";
+      type = types.lines;
+      description = ''
+        Shell commands to be executed before the failure prompt is shown.
+      '';
+    };
+
+    boot.initrd.extraUtilsCommands = mkOption {
+      internal = true;
+      default = "";
+      type = types.lines;
+      description = ''
+        Shell commands to be executed in the builder of the
+        extra-utils derivation.  This can be used to provide
+        additional utilities in the initial ramdisk.
+      '';
+    };
+
+    boot.initrd.extraUtilsCommandsTest = mkOption {
+      internal = true;
+      default = "";
+      type = types.lines;
+      description = ''
+        Shell commands to be executed in the builder of the
+        extra-utils derivation after patchelf has done its
+        job.  This can be used to test additional utilities
+        copied in extraUtilsCommands.
+      '';
+    };
+
+    boot.initrd.extraUdevRulesCommands = mkOption {
+      internal = true;
+      default = "";
+      type = types.lines;
+      description = ''
+        Shell commands to be executed in the builder of the
+        udev-rules derivation.  This can be used to add
+        additional udev rules in the initial ramdisk.
+      '';
+    };
+
+    boot.initrd.compressor = mkOption {
+      default = (
+        if lib.versionAtLeast config.boot.kernelPackages.kernel.version "5.9"
+        then "zstd"
+        else "gzip"
+      );
+      defaultText = literalDocBook "<literal>zstd</literal> if the kernel supports it (5.9+), <literal>gzip</literal> if not";
+      type = types.either types.str (types.functionTo types.str);
+      description = ''
+        The compressor to use on the initrd image. May be any of:
+
+        <itemizedlist>
+         <listitem><para>The name of one of the predefined compressors, see <filename>pkgs/build-support/kernel/initrd-compressor-meta.nix</filename> for the definitions.</para></listitem>
+         <listitem><para>A function which, given the nixpkgs package set, returns the path to a compressor tool, e.g. <literal>pkgs: "''${pkgs.pigz}/bin/pigz"</literal></para></listitem>
+         <listitem><para>(not recommended, because it does not work when cross-compiling) the full path to a compressor tool, e.g. <literal>"''${pkgs.pigz}/bin/pigz"</literal></para></listitem>
+        </itemizedlist>
+
+        The given program should read data from stdin and write it to stdout compressed.
+      '';
+      example = "xz";
+    };
+
+    boot.initrd.compressorArgs = mkOption {
+      default = null;
+      type = types.nullOr (types.listOf types.str);
+      description = "Arguments to pass to the compressor for the initrd image, or null to use the compressor's defaults.";
+    };
+
+    boot.initrd.secrets = mkOption
+      { default = {};
+        type = types.attrsOf (types.nullOr types.path);
+        description =
+          ''
+            Secrets to append to the initrd. The attribute name is the
+            path the secret should have inside the initrd, the value
+            is the path it should be copied from (or null for the same
+            path inside and out).
+          '';
+        example = literalExpression
+          ''
+            { "/etc/dropbear/dropbear_rsa_host_key" =
+                ./secret-dropbear-key;
+            }
+          '';
+      };
+
+    boot.initrd.supportedFilesystems = mkOption {
+      default = [ ];
+      example = [ "btrfs" ];
+      type = types.listOf types.str;
+      description = "Names of supported filesystem types in the initial ramdisk.";
+    };
+
+    boot.initrd.verbose = mkOption {
+      default = true;
+      type = types.bool;
+      description =
+        ''
+          Verbosity of the initrd. Please note that disabling verbosity removes
+          only the mandatory messages generated by the NixOS scripts. For a
+          completely silent boot, you might also want to set the two following
+          configuration options:
+
+          <itemizedlist>
+            <listitem><para><literal>boot.consoleLogLevel = 0;</literal></para></listitem>
+            <listitem><para><literal>boot.kernelParams = [ "quiet" "udev.log_level=3" ];</literal></para></listitem>
+          </itemizedlist>
+        '';
+    };
+
+    boot.loader.supportsInitrdSecrets = mkOption
+      { internal = true;
+        default = false;
+        type = types.bool;
+        description =
+          ''
+            Whether the bootloader setup runs append-initrd-secrets.
+            If not, any needed secrets must be copied into the initrd
+            and thus added to the store.
+          '';
+      };
+
+    fileSystems = mkOption {
+      type = with lib.types; attrsOf (submodule {
+        options.neededForBoot = mkOption {
+          default = false;
+          type = types.bool;
+          description = ''
+            If set, this file system will be mounted in the initial ramdisk.
+            Note that the file system will always be mounted in the initial
+            ramdisk if its mount point is one of the following:
+            ${concatStringsSep ", " (
+              forEach utils.pathsNeededForBoot (i: "<filename>${i}</filename>")
+            )}.
+          '';
+        };
+      });
+    };
+
+  };
+
+  config = mkIf config.boot.initrd.enable {
+    assertions = [
+      { assertion = any (fs: fs.mountPoint == "/") fileSystems;
+        message = "The ‘fileSystems’ option does not specify your root file system.";
+      }
+      { assertion = let inherit (config.boot) resumeDevice; in
+          resumeDevice == "" || builtins.substring 0 1 resumeDevice == "/";
+        message = "boot.resumeDevice has to be an absolute path."
+          + " Old \"x:y\" style is no longer supported.";
+      }
+      # TODO: remove when #85000 is fixed
+      { assertion = !config.boot.loader.supportsInitrdSecrets ->
+          all (source:
+            builtins.isPath source ||
+            (builtins.isString source && hasPrefix builtins.storeDir source))
+          (attrValues config.boot.initrd.secrets);
+        message = ''
+          boot.loader.initrd.secrets values must be unquoted paths when
+          using a bootloader that doesn't natively support initrd
+          secrets, e.g.:
+
+            boot.initrd.secrets = {
+              "/etc/secret" = /path/to/secret;
+            };
+
+          Note that this will result in all secrets being stored
+          world-readable in the Nix store!
+        '';
+      }
+    ];
+
+    system.build =
+      { inherit bootStage1 initialRamdisk initialRamdiskSecretAppender extraUtils; };
+
+    system.requiredKernelConfig = with config.lib.kernelConfig; [
+      (isYes "TMPFS")
+      (isYes "BLK_DEV_INITRD")
+    ];
+
+    boot.initrd.supportedFilesystems = map (fs: fs.fsType) fileSystems;
+
+  };
+}
diff --git a/nixos/modules/system/boot/stage-2-init.sh b/nixos/modules/system/boot/stage-2-init.sh
new file mode 100755
index 00000000000..a90f58042d2
--- /dev/null
+++ b/nixos/modules/system/boot/stage-2-init.sh
@@ -0,0 +1,176 @@
+#! @shell@
+
+systemConfig=@systemConfig@
+
+export HOME=/root PATH="@path@"
+
+
+# Process the kernel command line.
+for o in $(</proc/cmdline); do
+    case $o in
+        boot.debugtrace)
+            # Show each command.
+            set -x
+            ;;
+        resume=*)
+            set -- $(IFS==; echo $o)
+            resumeDevice=$2
+            ;;
+    esac
+done
+
+
+# Print a greeting.
+echo
+echo -e "\e[1;32m<<< NixOS Stage 2 >>>\e[0m"
+echo
+
+
+# Normally, stage 1 mounts the root filesystem read/writable.
+# However, in some environments, stage 2 is executed directly, and the
+# root is read-only.  So make it writable here.
+if [ -z "$container" ]; then
+    mount -n -o remount,rw none /
+fi
+
+
+# Likewise, stage 1 mounts /proc, /dev and /sys, so if we don't have a
+# stage 1, we need to do that here.
+if [ ! -e /proc/1 ]; then
+    specialMount() {
+        local device="$1"
+        local mountPoint="$2"
+        local options="$3"
+        local fsType="$4"
+
+        install -m 0755 -d "$mountPoint"
+        mount -n -t "$fsType" -o "$options" "$device" "$mountPoint"
+    }
+    source @earlyMountScript@
+fi
+
+
+echo "booting system configuration $systemConfig" > /dev/kmsg
+
+
+# Make /nix/store a read-only bind mount to enforce immutability of
+# the Nix store.  Note that we can't use "chown root:nixbld" here
+# because users/groups might not exist yet.
+# Silence chown/chmod to fail gracefully on a readonly filesystem
+# like squashfs.
+chown -f 0:30000 /nix/store
+chmod -f 1775 /nix/store
+if [ -n "@readOnlyStore@" ]; then
+    if ! [[ "$(findmnt --noheadings --output OPTIONS /nix/store)" =~ ro(,|$) ]]; then
+        if [ -z "$container" ]; then
+            mount --bind /nix/store /nix/store
+        else
+            mount --rbind /nix/store /nix/store
+        fi
+        mount -o remount,ro,bind /nix/store
+    fi
+fi
+
+
+# Provide a /etc/mtab.
+install -m 0755 -d /etc
+test -e /etc/fstab || touch /etc/fstab # to shut up mount
+rm -f /etc/mtab* # not that we care about stale locks
+ln -s /proc/mounts /etc/mtab
+
+
+# More special file systems, initialise required directories.
+[ -e /proc/bus/usb ] && mount -t usbfs usbfs /proc/bus/usb # UML doesn't have USB by default
+install -m 01777 -d /tmp
+install -m 0755 -d /var/{log,lib,db} /nix/var /etc/nixos/ \
+    /run/lock /home /bin # for the /bin/sh symlink
+
+
+# Miscellaneous boot time cleanup.
+rm -rf /var/run /var/lock
+rm -f /etc/{group,passwd,shadow}.lock
+
+
+# Also get rid of temporary GC roots.
+rm -rf /nix/var/nix/gcroots/tmp /nix/var/nix/temproots
+
+
+# For backwards compatibility, symlink /var/run to /run, and /var/lock
+# to /run/lock.
+ln -s /run /var/run
+ln -s /run/lock /var/lock
+
+
+# Clear the resume device.
+if test -n "$resumeDevice"; then
+    mkswap "$resumeDevice" || echo 'Failed to clear saved image.'
+fi
+
+
+# Use /etc/resolv.conf supplied by systemd-nspawn, if applicable.
+if [ -n "@useHostResolvConf@" ] && [ -e /etc/resolv.conf ]; then
+    resolvconf -m 1000 -a host </etc/resolv.conf
+fi
+
+# Log the script output to /dev/kmsg or /run/log/stage-2-init.log.
+# Only at this point are all the necessary prerequisites ready for these commands.
+exec {logOutFd}>&1 {logErrFd}>&2
+if test -w /dev/kmsg; then
+    exec > >(tee -i /proc/self/fd/"$logOutFd" | while read -r line; do
+        if test -n "$line"; then
+            echo "<7>stage-2-init: $line" > /dev/kmsg
+        fi
+    done) 2>&1
+else
+    mkdir -p /run/log
+    exec > >(tee -i /run/log/stage-2-init.log) 2>&1
+fi
+
+
+# Run the script that performs all configuration activation that does
+# not have to be done at boot time.
+echo "running activation script..."
+$systemConfig/activate
+
+
+# Restore the system time from the hardware clock.  We do this after
+# running the activation script to be sure that /etc/localtime points
+# at the current time zone.
+if [ -e /dev/rtc ]; then
+    hwclock --hctosys
+fi
+
+
+# Record the boot configuration.
+ln -sfn "$systemConfig" /run/booted-system
+
+# Prevent the booted system from being garbage-collected. If it weren't
+# a gcroot, if we were running a different kernel, switched system,
+# and garbage collected all, we could not load kernel modules anymore.
+ln -sfn /run/booted-system /nix/var/nix/gcroots/booted-system
+
+
+# Run any user-specified commands.
+@shell@ @postBootCommands@
+
+
+# Ensure systemd doesn't try to populate /etc, by forcing its first-boot
+# heuristic off. It doesn't matter what's in /etc/machine-id for this purpose,
+# and systemd will immediately fill in the file when it starts, so just
+# creating it is enough. This `: >>` pattern avoids forking and avoids changing
+# the mtime if the file already exists.
+: >> /etc/machine-id
+
+
+# Reset the logging file descriptors.
+exec 1>&$logOutFd 2>&$logErrFd
+exec {logOutFd}>&- {logErrFd}>&-
+
+
+# Start systemd.
+echo "starting systemd..."
+
+PATH=/run/current-system/systemd/lib/systemd:@fsPackagesPath@ \
+    LOCALE_ARCHIVE=/run/current-system/sw/lib/locale/locale-archive @systemdUnitPathEnvVar@ \
+    TZDIR=/etc/zoneinfo \
+    exec @systemdExecutable@
diff --git a/nixos/modules/system/boot/stage-2.nix b/nixos/modules/system/boot/stage-2.nix
new file mode 100644
index 00000000000..f6b6a8e4b0b
--- /dev/null
+++ b/nixos/modules/system/boot/stage-2.nix
@@ -0,0 +1,108 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  useHostResolvConf = config.networking.resolvconf.enable && config.networking.useHostResolvConf;
+
+  bootStage2 = pkgs.substituteAll {
+    src = ./stage-2-init.sh;
+    shellDebug = "${pkgs.bashInteractive}/bin/bash";
+    shell = "${pkgs.bash}/bin/bash";
+    inherit (config.boot) systemdExecutable extraSystemdUnitPaths;
+    isExecutable = true;
+    inherit (config.nix) readOnlyStore;
+    inherit useHostResolvConf;
+    inherit (config.system.build) earlyMountScript;
+    path = lib.makeBinPath ([
+      pkgs.coreutils
+      pkgs.util-linux
+    ] ++ lib.optional useHostResolvConf pkgs.openresolv);
+    fsPackagesPath = lib.makeBinPath config.system.fsPackages;
+    systemdUnitPathEnvVar = lib.optionalString (config.boot.extraSystemdUnitPaths != [])
+      ("SYSTEMD_UNIT_PATH="
+      + builtins.concatStringsSep ":" config.boot.extraSystemdUnitPaths
+      + ":"); # If SYSTEMD_UNIT_PATH ends with an empty component (":"), the usual unit load path will be appended to the contents of the variable
+    postBootCommands = pkgs.writeText "local-cmds"
+      ''
+        ${config.boot.postBootCommands}
+        ${config.powerManagement.powerUpCommands}
+      '';
+  };
+
+in
+
+{
+  options = {
+
+    boot = {
+
+      postBootCommands = mkOption {
+        default = "";
+        example = "rm -f /var/log/messages";
+        type = types.lines;
+        description = ''
+          Shell commands to be executed just before systemd is started.
+        '';
+      };
+
+      devSize = mkOption {
+        default = "5%";
+        example = "32m";
+        type = types.str;
+        description = ''
+          Size limit for the /dev tmpfs. Look at mount(8), tmpfs size option,
+          for the accepted syntax.
+        '';
+      };
+
+      devShmSize = mkOption {
+        default = "50%";
+        example = "256m";
+        type = types.str;
+        description = ''
+          Size limit for the /dev/shm tmpfs. Look at mount(8), tmpfs size option,
+          for the accepted syntax.
+        '';
+      };
+
+      runSize = mkOption {
+        default = "25%";
+        example = "256m";
+        type = types.str;
+        description = ''
+          Size limit for the /run tmpfs. Look at mount(8), tmpfs size option,
+          for the accepted syntax.
+        '';
+      };
+
+      systemdExecutable = mkOption {
+        default = "systemd";
+        type = types.str;
+        description = ''
+          The program to execute to start systemd. Typically
+          <literal>systemd</literal>, which will find systemd in the
+          PATH.
+        '';
+      };
+
+      extraSystemdUnitPaths = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        description = ''
+          Additional paths that get appended to the SYSTEMD_UNIT_PATH environment variable
+          that can contain mutable unit files.
+        '';
+      };
+    };
+
+  };
+
+
+  config = {
+
+    system.build.bootStage2 = bootStage2;
+
+  };
+}
diff --git a/nixos/modules/system/boot/systemd-nspawn.nix b/nixos/modules/system/boot/systemd-nspawn.nix
new file mode 100644
index 00000000000..0c6822319a5
--- /dev/null
+++ b/nixos/modules/system/boot/systemd-nspawn.nix
@@ -0,0 +1,125 @@
+{ config, lib, pkgs, utils, ...}:
+
+with utils.systemdUtils.unitOptions;
+with utils.systemdUtils.lib;
+with lib;
+
+let
+  cfg = config.systemd.nspawn;
+
+  checkExec = checkUnitConfig "Exec" [
+    (assertOnlyFields [
+      "Boot" "ProcessTwo" "Parameters" "Environment" "User" "WorkingDirectory"
+      "PivotRoot" "Capability" "DropCapability" "NoNewPrivileges" "KillSignal"
+      "Personality" "MachineId" "PrivateUsers" "NotifyReady" "SystemCallFilter"
+      "LimitCPU" "LimitFSIZE" "LimitDATA" "LimitSTACK" "LimitCORE" "LimitRSS"
+      "LimitNOFILE" "LimitAS" "LimitNPROC" "LimitMEMLOCK" "LimitLOCKS"
+      "LimitSIGPENDING" "LimitMSGQUEUE" "LimitNICE" "LimitRTPRIO" "LimitRTTIME"
+      "OOMScoreAdjust" "CPUAffinity" "Hostname" "ResolvConf" "Timezone"
+      "LinkJournal"
+    ])
+    (assertValueOneOf "Boot" boolValues)
+    (assertValueOneOf "ProcessTwo" boolValues)
+    (assertValueOneOf "NotifyReady" boolValues)
+  ];
+
+  checkFiles = checkUnitConfig "Files" [
+    (assertOnlyFields [
+      "ReadOnly" "Volatile" "Bind" "BindReadOnly" "TemporaryFileSystem"
+      "Overlay" "OverlayReadOnly" "PrivateUsersChown"
+    ])
+    (assertValueOneOf "ReadOnly" boolValues)
+    (assertValueOneOf "Volatile" (boolValues ++ [ "state" ]))
+    (assertValueOneOf "PrivateUsersChown" boolValues)
+  ];
+
+  checkNetwork = checkUnitConfig "Network" [
+    (assertOnlyFields [
+      "Private" "VirtualEthernet" "VirtualEthernetExtra" "Interface" "MACVLAN"
+      "IPVLAN" "Bridge" "Zone" "Port"
+    ])
+    (assertValueOneOf "Private" boolValues)
+    (assertValueOneOf "VirtualEthernet" boolValues)
+  ];
+
+  instanceOptions = {
+    options = sharedOptions // {
+      execConfig = mkOption {
+        default = {};
+        example = { Parameters = "/bin/sh"; };
+        type = types.addCheck (types.attrsOf unitOption) checkExec;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[Exec]</literal> section of this unit. See
+          <citerefentry><refentrytitle>systemd.nspawn</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
+
+      filesConfig = mkOption {
+        default = {};
+        example = { Bind = [ "/home/alice" ]; };
+        type = types.addCheck (types.attrsOf unitOption) checkFiles;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[Files]</literal> section of this unit. See
+          <citerefentry><refentrytitle>systemd.nspawn</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
+
+      networkConfig = mkOption {
+        default = {};
+        example = { Private = false; };
+        type = types.addCheck (types.attrsOf unitOption) checkNetwork;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[Network]</literal> section of this unit. See
+          <citerefentry><refentrytitle>systemd.nspawn</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
+    };
+
+  };
+
+  instanceToUnit = name: def:
+    let base = {
+      text = ''
+        [Exec]
+        ${attrsToSection def.execConfig}
+
+        [Files]
+        ${attrsToSection def.filesConfig}
+
+        [Network]
+        ${attrsToSection def.networkConfig}
+      '';
+    } // def;
+    in base // { unit = makeUnit name base; };
+
+in {
+
+  options = {
+
+    systemd.nspawn = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule instanceOptions);
+      description = "Definition of systemd-nspawn configurations.";
+    };
+
+  };
+
+  config =
+    let
+      units = mapAttrs' (n: v: let nspawnFile = "${n}.nspawn"; in nameValuePair nspawnFile (instanceToUnit nspawnFile v)) cfg;
+    in
+      mkMerge [
+        (mkIf (cfg != {}) {
+          environment.etc."systemd/nspawn".source = mkIf (cfg != {}) (generateUnits' false "nspawn" units [] []);
+        })
+        {
+          systemd.targets.multi-user.wants = [ "machines.target" ];
+        }
+      ];
+}
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
new file mode 100644
index 00000000000..ff002d87a12
--- /dev/null
+++ b/nixos/modules/system/boot/systemd.nix
@@ -0,0 +1,1064 @@
+{ config, lib, pkgs, utils, ... }:
+
+with utils;
+with systemdUtils.unitOptions;
+with systemdUtils.lib;
+with lib;
+
+let
+
+  cfg = config.systemd;
+
+  systemd = cfg.package;
+
+  inherit (systemdUtils.lib)
+    makeJobScript
+    unitConfig
+    serviceConfig
+    mountConfig
+    automountConfig
+    commonUnitText
+    targetToUnit
+    serviceToUnit
+    socketToUnit
+    timerToUnit
+    pathToUnit
+    mountToUnit
+    automountToUnit
+    sliceToUnit;
+
+  upstreamSystemUnits =
+    [ # Targets.
+      "basic.target"
+      "sysinit.target"
+      "sockets.target"
+      "exit.target"
+      "graphical.target"
+      "multi-user.target"
+      "network.target"
+      "network-pre.target"
+      "network-online.target"
+      "nss-lookup.target"
+      "nss-user-lookup.target"
+      "time-sync.target"
+    ] ++ (optionals cfg.package.withCryptsetup [
+      "cryptsetup.target"
+      "cryptsetup-pre.target"
+      "remote-cryptsetup.target"
+    ]) ++ [
+      "sigpwr.target"
+      "timers.target"
+      "paths.target"
+      "rpcbind.target"
+
+      # Rescue mode.
+      "rescue.target"
+      "rescue.service"
+
+      # Udev.
+      "systemd-udevd-control.socket"
+      "systemd-udevd-kernel.socket"
+      "systemd-udevd.service"
+      "systemd-udev-settle.service"
+      ] ++ (optional (!config.boot.isContainer) "systemd-udev-trigger.service") ++ [
+      # hwdb.bin is managed by NixOS
+      # "systemd-hwdb-update.service"
+
+      # Consoles.
+      "getty.target"
+      "getty-pre.target"
+      "getty@.service"
+      "serial-getty@.service"
+      "console-getty.service"
+      "container-getty@.service"
+      "systemd-vconsole-setup.service"
+
+      # Hardware (started by udev when a relevant device is plugged in).
+      "sound.target"
+      "bluetooth.target"
+      "printer.target"
+      "smartcard.target"
+
+      # Login stuff.
+      "systemd-logind.service"
+      "autovt@.service"
+      "systemd-user-sessions.service"
+      "dbus-org.freedesktop.import1.service"
+      "dbus-org.freedesktop.machine1.service"
+      "dbus-org.freedesktop.login1.service"
+      "user@.service"
+      "user-runtime-dir@.service"
+
+      # Journal.
+      "systemd-journald.socket"
+      "systemd-journald@.socket"
+      "systemd-journald-varlink@.socket"
+      "systemd-journald.service"
+      "systemd-journald@.service"
+      "systemd-journal-flush.service"
+      "systemd-journal-catalog-update.service"
+      ] ++ (optional (!config.boot.isContainer) "systemd-journald-audit.socket") ++ [
+      "systemd-journald-dev-log.socket"
+      "syslog.socket"
+
+      # Coredumps.
+      "systemd-coredump.socket"
+      "systemd-coredump@.service"
+
+      # Kernel module loading.
+      "systemd-modules-load.service"
+      "kmod-static-nodes.service"
+      "modprobe@.service"
+
+      # Filesystems.
+      "systemd-fsck@.service"
+      "systemd-fsck-root.service"
+      "systemd-remount-fs.service"
+      "systemd-pstore.service"
+      "local-fs.target"
+      "local-fs-pre.target"
+      "remote-fs.target"
+      "remote-fs-pre.target"
+      "swap.target"
+      "dev-hugepages.mount"
+      "dev-mqueue.mount"
+      "sys-fs-fuse-connections.mount"
+      ] ++ (optional (!config.boot.isContainer) "sys-kernel-config.mount") ++ [
+      "sys-kernel-debug.mount"
+
+      # Maintaining state across reboots.
+      "systemd-random-seed.service"
+      "systemd-backlight@.service"
+      "systemd-rfkill.service"
+      "systemd-rfkill.socket"
+
+      # Hibernate / suspend.
+      "hibernate.target"
+      "suspend.target"
+      "suspend-then-hibernate.target"
+      "sleep.target"
+      "hybrid-sleep.target"
+      "systemd-hibernate.service"
+      "systemd-hybrid-sleep.service"
+      "systemd-suspend.service"
+      "systemd-suspend-then-hibernate.service"
+
+      # Reboot stuff.
+      "reboot.target"
+      "systemd-reboot.service"
+      "poweroff.target"
+      "systemd-poweroff.service"
+      "halt.target"
+      "systemd-halt.service"
+      "shutdown.target"
+      "umount.target"
+      "final.target"
+      "kexec.target"
+      "systemd-kexec.service"
+      "systemd-update-utmp.service"
+
+      # Password entry.
+      "systemd-ask-password-console.path"
+      "systemd-ask-password-console.service"
+      "systemd-ask-password-wall.path"
+      "systemd-ask-password-wall.service"
+
+      # Slices / containers.
+      "slices.target"
+      "user.slice"
+      "machine.slice"
+      "machines.target"
+      "systemd-importd.service"
+      "systemd-machined.service"
+      "systemd-nspawn@.service"
+
+      # Temporary file creation / cleanup.
+      "systemd-tmpfiles-clean.service"
+      "systemd-tmpfiles-clean.timer"
+      "systemd-tmpfiles-setup.service"
+      "systemd-tmpfiles-setup-dev.service"
+
+      # Misc.
+      "systemd-sysctl.service"
+      "dbus-org.freedesktop.timedate1.service"
+      "dbus-org.freedesktop.locale1.service"
+      "dbus-org.freedesktop.hostname1.service"
+      "systemd-timedated.service"
+      "systemd-localed.service"
+      "systemd-hostnamed.service"
+      "systemd-exit.service"
+      "systemd-update-done.service"
+    ] ++ optionals config.services.journald.enableHttpGateway [
+      "systemd-journal-gatewayd.socket"
+      "systemd-journal-gatewayd.service"
+    ] ++ cfg.additionalUpstreamSystemUnits;
+
+  upstreamSystemWants =
+    [ "sysinit.target.wants"
+      "sockets.target.wants"
+      "local-fs.target.wants"
+      "multi-user.target.wants"
+      "timers.target.wants"
+    ];
+
+    upstreamUserUnits = [
+      "app.slice"
+      "background.slice"
+      "basic.target"
+      "bluetooth.target"
+      "default.target"
+      "exit.target"
+      "graphical-session-pre.target"
+      "graphical-session.target"
+      "paths.target"
+      "printer.target"
+      "session.slice"
+      "shutdown.target"
+      "smartcard.target"
+      "sockets.target"
+      "sound.target"
+      "systemd-exit.service"
+      "systemd-tmpfiles-clean.service"
+      "systemd-tmpfiles-clean.timer"
+      "systemd-tmpfiles-setup.service"
+      "timers.target"
+      "xdg-desktop-autostart.target"
+    ];
+
+
+  logindHandlerType = types.enum [
+    "ignore" "poweroff" "reboot" "halt" "kexec" "suspend"
+    "hibernate" "hybrid-sleep" "suspend-then-hibernate" "lock"
+  ];
+
+  proxy_env = config.networking.proxy.envVars;
+
+in
+
+{
+  ###### interface
+
+  options = {
+
+    systemd.package = mkOption {
+      default = pkgs.systemd;
+      defaultText = literalExpression "pkgs.systemd";
+      type = types.package;
+      description = "The systemd package.";
+    };
+
+    systemd.units = mkOption {
+      description = "Definition of systemd units.";
+      default = {};
+      type = with types; attrsOf (submodule (
+        { name, config, ... }:
+        { options = concreteUnitOptions;
+          config = {
+            unit = mkDefault (makeUnit name config);
+          };
+        }));
+    };
+
+    systemd.packages = mkOption {
+      default = [];
+      type = types.listOf types.package;
+      example = literalExpression "[ pkgs.systemd-cryptsetup-generator ]";
+      description = "Packages providing systemd units and hooks.";
+    };
+
+    systemd.targets = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule [ { options = targetOptions; } unitConfig] );
+      description = "Definition of systemd target units.";
+    };
+
+    systemd.services = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule [ { options = serviceOptions; } unitConfig serviceConfig ]);
+      description = "Definition of systemd service units.";
+    };
+
+    systemd.sockets = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule [ { options = socketOptions; } unitConfig ]);
+      description = "Definition of systemd socket units.";
+    };
+
+    systemd.timers = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule [ { options = timerOptions; } unitConfig ]);
+      description = "Definition of systemd timer units.";
+    };
+
+    systemd.paths = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule [ { options = pathOptions; } unitConfig ]);
+      description = "Definition of systemd path units.";
+    };
+
+    systemd.mounts = mkOption {
+      default = [];
+      type = with types; listOf (submodule [ { options = mountOptions; } unitConfig mountConfig ]);
+      description = ''
+        Definition of systemd mount units.
+        This is a list instead of an attrSet, because systemd mandates the names to be derived from
+        the 'where' attribute.
+      '';
+    };
+
+    systemd.automounts = mkOption {
+      default = [];
+      type = with types; listOf (submodule [ { options = automountOptions; } unitConfig automountConfig ]);
+      description = ''
+        Definition of systemd automount units.
+        This is a list instead of an attrSet, because systemd mandates the names to be derived from
+        the 'where' attribute.
+      '';
+    };
+
+    systemd.slices = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule [ { options = sliceOptions; } unitConfig] );
+      description = "Definition of slice configurations.";
+    };
+
+    systemd.generators = mkOption {
+      type = types.attrsOf types.path;
+      default = {};
+      example = { systemd-gpt-auto-generator = "/dev/null"; };
+      description = ''
+        Definition of systemd generators.
+        For each <literal>NAME = VALUE</literal> pair of the attrSet, a link is generated from
+        <literal>/etc/systemd/system-generators/NAME</literal> to <literal>VALUE</literal>.
+      '';
+    };
+
+    systemd.shutdown = mkOption {
+      type = types.attrsOf types.path;
+      default = {};
+      description = ''
+        Definition of systemd shutdown executables.
+        For each <literal>NAME = VALUE</literal> pair of the attrSet, a link is generated from
+        <literal>/etc/systemd/system-shutdown/NAME</literal> to <literal>VALUE</literal>.
+      '';
+    };
+
+    systemd.defaultUnit = mkOption {
+      default = "multi-user.target";
+      type = types.str;
+      description = "Default unit started when the system boots.";
+    };
+
+    systemd.ctrlAltDelUnit = mkOption {
+      default = "reboot.target";
+      type = types.str;
+      example = "poweroff.target";
+      description = ''
+        Target that should be started when Ctrl-Alt-Delete is pressed.
+      '';
+    };
+
+    systemd.globalEnvironment = mkOption {
+      type = with types; attrsOf (nullOr (oneOf [ str path package ]));
+      default = {};
+      example = { TZ = "CET"; };
+      description = ''
+        Environment variables passed to <emphasis>all</emphasis> systemd units.
+      '';
+    };
+
+    systemd.enableCgroupAccounting = mkOption {
+      default = true;
+      type = types.bool;
+      description = ''
+        Whether to enable cgroup accounting.
+      '';
+    };
+
+    systemd.enableUnifiedCgroupHierarchy = mkOption {
+      default = true;
+      type = types.bool;
+      description = ''
+        Whether to enable the unified cgroup hierarchy (cgroupsv2).
+      '';
+    };
+
+    systemd.coredump.enable = mkOption {
+      default = true;
+      type = types.bool;
+      description = ''
+        Whether core dumps should be processed by
+        <command>systemd-coredump</command>. If disabled, core dumps
+        appear in the current directory of the crashing process.
+      '';
+    };
+
+    systemd.coredump.extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      example = "Storage=journal";
+      description = ''
+        Extra config options for systemd-coredump. See coredump.conf(5) man page
+        for available options.
+      '';
+    };
+
+    systemd.extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      example = "DefaultLimitCORE=infinity";
+      description = ''
+        Extra config options for systemd. See man systemd-system.conf for
+        available options.
+      '';
+    };
+
+    services.journald.console = mkOption {
+      default = "";
+      type = types.str;
+      description = "If non-empty, write log messages to the specified TTY device.";
+    };
+
+    services.journald.rateLimitInterval = mkOption {
+      default = "30s";
+      type = types.str;
+      description = ''
+        Configures the rate limiting interval that is applied to all
+        messages generated on the system. This rate limiting is applied
+        per-service, so that two services which log do not interfere with
+        each other's limit. The value may be specified in the following
+        units: s, min, h, ms, us. To turn off any kind of rate limiting,
+        set either value to 0.
+
+        See <option>services.journald.rateLimitBurst</option> for important
+        considerations when setting this value.
+      '';
+    };
+
+    services.journald.rateLimitBurst = mkOption {
+      default = 10000;
+      type = types.int;
+      description = ''
+        Configures the rate limiting burst limit (number of messages per
+        interval) that is applied to all messages generated on the system.
+        This rate limiting is applied per-service, so that two services
+        which log do not interfere with each other's limit.
+
+        Note that the effective rate limit is multiplied by a factor derived
+        from the available free disk space for the journal as described on
+        <link xlink:href="https://www.freedesktop.org/software/systemd/man/journald.conf.html">
+        journald.conf(5)</link>.
+
+        Note that the total amount of logs stored is limited by journald settings
+        such as <literal>SystemMaxUse</literal>, which defaults to a 4 GB cap.
+
+        It is thus recommended to compute what period of time that you will be
+        able to store logs for when an application logs at full burst rate.
+        With default settings for log lines that are 100 Bytes long, this can
+        amount to just a few hours.
+      '';
+    };
+
+    services.journald.extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      example = "Storage=volatile";
+      description = ''
+        Extra config options for systemd-journald. See man journald.conf
+        for available options.
+      '';
+    };
+
+    services.journald.enableHttpGateway = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to enable the HTTP gateway to the journal.
+      '';
+    };
+
+    services.journald.forwardToSyslog = mkOption {
+      default = config.services.rsyslogd.enable || config.services.syslog-ng.enable;
+      defaultText = literalExpression "services.rsyslogd.enable || services.syslog-ng.enable";
+      type = types.bool;
+      description = ''
+        Whether to forward log messages to syslog.
+      '';
+    };
+
+    services.logind.extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      example = "IdleAction=lock";
+      description = ''
+        Extra config options for systemd-logind. See
+        <link xlink:href="https://www.freedesktop.org/software/systemd/man/logind.conf.html">
+        logind.conf(5)</link> for available options.
+      '';
+    };
+
+    services.logind.killUserProcesses = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Specifies whether the processes of a user should be killed
+        when the user logs out.  If true, the scope unit corresponding
+        to the session and all processes inside that scope will be
+        terminated.  If false, the scope is "abandoned" (see
+        <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.scope.html#">
+        systemd.scope(5)</link>), and processes are not killed.
+        </para>
+
+        <para>
+        See <link xlink:href="https://www.freedesktop.org/software/systemd/man/logind.conf.html#KillUserProcesses=">logind.conf(5)</link>
+        for more details.
+      '';
+    };
+
+    services.logind.lidSwitch = mkOption {
+      default = "suspend";
+      example = "ignore";
+      type = logindHandlerType;
+
+      description = ''
+        Specifies what to be done when the laptop lid is closed.
+      '';
+    };
+
+    services.logind.lidSwitchDocked = mkOption {
+      default = "ignore";
+      example = "suspend";
+      type = logindHandlerType;
+
+      description = ''
+        Specifies what to be done when the laptop lid is closed
+        and another screen is added.
+      '';
+    };
+
+    services.logind.lidSwitchExternalPower = mkOption {
+      default = config.services.logind.lidSwitch;
+      defaultText = literalExpression "services.logind.lidSwitch";
+      example = "ignore";
+      type = logindHandlerType;
+
+      description = ''
+        Specifies what to do when the laptop lid is closed and the system is
+        on external power. By default use the same action as specified in
+        services.logind.lidSwitch.
+      '';
+    };
+
+    systemd.sleep.extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      example = "HibernateDelaySec=1h";
+      description = ''
+        Extra config options for systemd sleep state logic.
+        See sleep.conf.d(5) man page for available options.
+      '';
+    };
+
+    systemd.user.extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      example = "DefaultCPUAccounting=yes";
+      description = ''
+        Extra config options for systemd user instances. See man systemd-user.conf for
+        available options.
+      '';
+    };
+
+    systemd.tmpfiles.rules = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "d /tmp 1777 root root 10d" ];
+      description = ''
+        Rules for creation, deletion and cleaning of volatile and temporary files
+        automatically. See
+        <citerefentry><refentrytitle>tmpfiles.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+        for the exact format.
+      '';
+    };
+
+    systemd.tmpfiles.packages = mkOption {
+      type = types.listOf types.package;
+      default = [];
+      example = literalExpression "[ pkgs.lvm2 ]";
+      apply = map getLib;
+      description = ''
+        List of packages containing <command>systemd-tmpfiles</command> rules.
+
+        All files ending in .conf found in
+        <filename><replaceable>pkg</replaceable>/lib/tmpfiles.d</filename>
+        will be included.
+        If this folder does not exist or does not contain any files an error will be returned instead.
+
+        If a <filename>lib</filename> output is available, rules are searched there and only there.
+        If there is no <filename>lib</filename> output it will fall back to <filename>out</filename>
+        and if that does not exist either, the default output will be used.
+      '';
+    };
+
+    systemd.user.units = mkOption {
+      description = "Definition of systemd per-user units.";
+      default = {};
+      type = with types; attrsOf (submodule (
+        { name, config, ... }:
+        { options = concreteUnitOptions;
+          config = {
+            unit = mkDefault (makeUnit name config);
+          };
+        }));
+    };
+
+    systemd.user.paths = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule [ { options = pathOptions; } unitConfig ]);
+      description = "Definition of systemd per-user path units.";
+    };
+
+    systemd.user.services = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule [ { options = serviceOptions; } unitConfig serviceConfig ] );
+      description = "Definition of systemd per-user service units.";
+    };
+
+    systemd.user.slices = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule [ { options = sliceOptions; } unitConfig ] );
+      description = "Definition of systemd per-user slice units.";
+    };
+
+    systemd.user.sockets = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule [ { options = socketOptions; } unitConfig ] );
+      description = "Definition of systemd per-user socket units.";
+    };
+
+    systemd.user.targets = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule [ { options = targetOptions; } unitConfig] );
+      description = "Definition of systemd per-user target units.";
+    };
+
+    systemd.user.timers = mkOption {
+      default = {};
+      type = with types; attrsOf (submodule [ { options = timerOptions; } unitConfig ] );
+      description = "Definition of systemd per-user timer units.";
+    };
+
+    systemd.additionalUpstreamSystemUnits = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      example = [ "debug-shell.service" "systemd-quotacheck.service" ];
+      description = ''
+        Additional units shipped with systemd that shall be enabled.
+      '';
+    };
+
+    systemd.suppressedSystemUnits = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      example = [ "systemd-backlight@.service" ];
+      description = ''
+        A list of units to suppress when generating system systemd configuration directory. This has
+        priority over upstream units, <option>systemd.units</option>, and
+        <option>systemd.additionalUpstreamSystemUnits</option>. The main purpose of this is to
+        suppress a upstream systemd unit with any modifications made to it by other NixOS modules.
+      '';
+    };
+
+    systemd.watchdog.device = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/dev/watchdog";
+      description = ''
+        The path to a hardware watchdog device which will be managed by systemd.
+        If not specified, systemd will default to /dev/watchdog.
+      '';
+    };
+
+    systemd.watchdog.runtimeTime = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "30s";
+      description = ''
+        The amount of time which can elapse before a watchdog hardware device
+        will automatically reboot the system. Valid time units include "ms",
+        "s", "min", "h", "d", and "w".
+      '';
+    };
+
+    systemd.watchdog.rebootTime = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "10m";
+      description = ''
+        The amount of time which can elapse after a reboot has been triggered
+        before a watchdog hardware device will automatically reboot the system.
+        Valid time units include "ms", "s", "min", "h", "d", and "w".
+      '';
+    };
+
+    systemd.watchdog.kexecTime = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "10m";
+      description = ''
+        The amount of time which can elapse when kexec is being executed before
+        a watchdog hardware device will automatically reboot the system. This
+        option should only be enabled if reloadTime is also enabled. Valid
+        time units include "ms", "s", "min", "h", "d", and "w".
+      '';
+    };
+  };
+
+
+  ###### implementation
+
+  config = {
+
+    warnings = concatLists (
+      mapAttrsToList
+        (name: service:
+          let
+            type = service.serviceConfig.Type or "";
+            restart = service.serviceConfig.Restart or "no";
+            hasDeprecated = builtins.hasAttr "StartLimitInterval" service.serviceConfig;
+          in
+            concatLists [
+              (optional (type == "oneshot" && (restart == "always" || restart == "on-success"))
+                "Service '${name}.service' with 'Type=oneshot' cannot have 'Restart=always' or 'Restart=on-success'"
+              )
+              (optional hasDeprecated
+                "Service '${name}.service' uses the attribute 'StartLimitInterval' in the Service section, which is deprecated. See https://github.com/NixOS/nixpkgs/issues/45786."
+              )
+              (optional (service.reloadIfChanged && service.reloadTriggers != [])
+                "Service '${name}.service' has both 'reloadIfChanged' and 'reloadTriggers' set. This is probably not what you want, because 'reloadTriggers' behave the same whay as 'restartTriggers' if 'reloadIfChanged' is set."
+              )
+            ]
+        )
+        cfg.services
+    );
+
+    system.build.units = cfg.units;
+
+    system.nssModules = [ systemd.out ];
+    system.nssDatabases = {
+      hosts = (mkMerge [
+        (mkOrder 400 ["mymachines"]) # 400 to ensure it comes before resolve (which is mkBefore'd)
+        (mkOrder 999 ["myhostname"]) # after files (which is 998), but before regular nss modules
+      ]);
+      passwd = (mkMerge [
+        (mkAfter [ "systemd" ])
+      ]);
+      group = (mkMerge [
+        (mkAfter [ "systemd" ])
+      ]);
+    };
+
+    environment.systemPackages = [ systemd ];
+
+    environment.etc = let
+      # generate contents for /etc/systemd/system-${type} from attrset of links and packages
+      hooks = type: links: pkgs.runCommand "system-${type}" {
+          preferLocalBuild = true;
+          packages = cfg.packages;
+      } ''
+        set -e
+        mkdir -p $out
+        for package in $packages
+        do
+          for hook in $package/lib/systemd/system-${type}/*
+          do
+            ln -s $hook $out/
+          done
+        done
+        ${concatStrings (mapAttrsToList (exec: target: "ln -s ${target} $out/${exec};\n") links)}
+      '';
+
+      enabledUpstreamSystemUnits = filter (n: ! elem n cfg.suppressedSystemUnits) upstreamSystemUnits;
+      enabledUnits = filterAttrs (n: v: ! elem n cfg.suppressedSystemUnits) cfg.units;
+    in ({
+      "systemd/system".source = generateUnits "system" enabledUnits enabledUpstreamSystemUnits upstreamSystemWants;
+
+      "systemd/user".source = generateUnits "user" cfg.user.units upstreamUserUnits [];
+
+      "systemd/system.conf".text = ''
+        [Manager]
+        ${optionalString config.systemd.enableCgroupAccounting ''
+          DefaultCPUAccounting=yes
+          DefaultIOAccounting=yes
+          DefaultBlockIOAccounting=yes
+          DefaultIPAccounting=yes
+        ''}
+        DefaultLimitCORE=infinity
+        ${optionalString (config.systemd.watchdog.device != null) ''
+          WatchdogDevice=${config.systemd.watchdog.device}
+        ''}
+        ${optionalString (config.systemd.watchdog.runtimeTime != null) ''
+          RuntimeWatchdogSec=${config.systemd.watchdog.runtimeTime}
+        ''}
+        ${optionalString (config.systemd.watchdog.rebootTime != null) ''
+          RebootWatchdogSec=${config.systemd.watchdog.rebootTime}
+        ''}
+        ${optionalString (config.systemd.watchdog.kexecTime != null) ''
+          KExecWatchdogSec=${config.systemd.watchdog.kexecTime}
+        ''}
+
+        ${config.systemd.extraConfig}
+      '';
+
+      "systemd/user.conf".text = ''
+        [Manager]
+        ${config.systemd.user.extraConfig}
+      '';
+
+      "systemd/journald.conf".text = ''
+        [Journal]
+        Storage=persistent
+        RateLimitInterval=${config.services.journald.rateLimitInterval}
+        RateLimitBurst=${toString config.services.journald.rateLimitBurst}
+        ${optionalString (config.services.journald.console != "") ''
+          ForwardToConsole=yes
+          TTYPath=${config.services.journald.console}
+        ''}
+        ${optionalString (config.services.journald.forwardToSyslog) ''
+          ForwardToSyslog=yes
+        ''}
+        ${config.services.journald.extraConfig}
+      '';
+
+      "systemd/coredump.conf".text =
+        ''
+          [Coredump]
+          ${config.systemd.coredump.extraConfig}
+        '';
+
+      "systemd/logind.conf".text = ''
+        [Login]
+        KillUserProcesses=${if config.services.logind.killUserProcesses then "yes" else "no"}
+        HandleLidSwitch=${config.services.logind.lidSwitch}
+        HandleLidSwitchDocked=${config.services.logind.lidSwitchDocked}
+        HandleLidSwitchExternalPower=${config.services.logind.lidSwitchExternalPower}
+        ${config.services.logind.extraConfig}
+      '';
+
+      "systemd/sleep.conf".text = ''
+        [Sleep]
+        ${config.systemd.sleep.extraConfig}
+      '';
+
+      # install provided sysctl snippets
+      "sysctl.d/50-coredump.conf".source = "${systemd}/example/sysctl.d/50-coredump.conf";
+      "sysctl.d/50-default.conf".source = "${systemd}/example/sysctl.d/50-default.conf";
+
+      "tmpfiles.d".source = (pkgs.symlinkJoin {
+        name = "tmpfiles.d";
+        paths = map (p: p + "/lib/tmpfiles.d") cfg.tmpfiles.packages;
+        postBuild = ''
+          for i in $(cat $pathsPath); do
+            (test -d "$i" && test $(ls "$i"/*.conf | wc -l) -ge 1) || (
+              echo "ERROR: The path '$i' from systemd.tmpfiles.packages contains no *.conf files."
+              exit 1
+            )
+          done
+        '' + concatMapStrings (name: optionalString (hasPrefix "tmpfiles.d/" name) ''
+          rm -f $out/${removePrefix "tmpfiles.d/" name}
+        '') config.system.build.etc.passthru.targets;
+      }) + "/*";
+
+      "systemd/system-generators" = { source = hooks "generators" cfg.generators; };
+      "systemd/system-shutdown" = { source = hooks "shutdown" cfg.shutdown; };
+    });
+
+    services.dbus.enable = true;
+
+    users.users.systemd-coredump = {
+      uid = config.ids.uids.systemd-coredump;
+      group = "systemd-coredump";
+    };
+    users.groups.systemd-coredump = {};
+    users.users.systemd-network = {
+      uid = config.ids.uids.systemd-network;
+      group = "systemd-network";
+    };
+    users.groups.systemd-network.gid = config.ids.gids.systemd-network;
+    users.users.systemd-resolve = {
+      uid = config.ids.uids.systemd-resolve;
+      group = "systemd-resolve";
+    };
+    users.groups.systemd-resolve.gid = config.ids.gids.systemd-resolve;
+
+    # Target for ‘charon send-keys’ to hook into.
+    users.groups.keys.gid = config.ids.gids.keys;
+
+    systemd.targets.keys =
+      { description = "Security Keys";
+        unitConfig.X-StopOnReconfiguration = true;
+      };
+
+    systemd.tmpfiles.packages = [
+      # Default tmpfiles rules provided by systemd
+      (pkgs.runCommand "systemd-default-tmpfiles" {} ''
+        mkdir -p $out/lib/tmpfiles.d
+        cd $out/lib/tmpfiles.d
+
+        ln -s "${systemd}/example/tmpfiles.d/home.conf"
+        ln -s "${systemd}/example/tmpfiles.d/journal-nocow.conf"
+        ln -s "${systemd}/example/tmpfiles.d/static-nodes-permissions.conf"
+        ln -s "${systemd}/example/tmpfiles.d/systemd.conf"
+        ln -s "${systemd}/example/tmpfiles.d/systemd-nologin.conf"
+        ln -s "${systemd}/example/tmpfiles.d/systemd-nspawn.conf"
+        ln -s "${systemd}/example/tmpfiles.d/systemd-tmp.conf"
+        ln -s "${systemd}/example/tmpfiles.d/tmp.conf"
+        ln -s "${systemd}/example/tmpfiles.d/var.conf"
+        ln -s "${systemd}/example/tmpfiles.d/x11.conf"
+      '')
+      # User-specified tmpfiles rules
+      (pkgs.writeTextFile {
+        name = "nixos-tmpfiles.d";
+        destination = "/lib/tmpfiles.d/00-nixos.conf";
+        text = ''
+          # This file is created automatically and should not be modified.
+          # Please change the option ‘systemd.tmpfiles.rules’ instead.
+
+          ${concatStringsSep "\n" cfg.tmpfiles.rules}
+        '';
+      })
+    ];
+
+    systemd.units =
+         mapAttrs' (n: v: nameValuePair "${n}.path"    (pathToUnit    n v)) cfg.paths
+      // mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services
+      // mapAttrs' (n: v: nameValuePair "${n}.slice"   (sliceToUnit   n v)) cfg.slices
+      // mapAttrs' (n: v: nameValuePair "${n}.socket"  (socketToUnit  n v)) cfg.sockets
+      // mapAttrs' (n: v: nameValuePair "${n}.target"  (targetToUnit  n v)) cfg.targets
+      // mapAttrs' (n: v: nameValuePair "${n}.timer"   (timerToUnit   n v)) cfg.timers
+      // listToAttrs (map
+                   (v: let n = escapeSystemdPath v.where;
+                       in nameValuePair "${n}.mount" (mountToUnit n v)) cfg.mounts)
+      // listToAttrs (map
+                   (v: let n = escapeSystemdPath v.where;
+                       in nameValuePair "${n}.automount" (automountToUnit n v)) cfg.automounts);
+
+    systemd.user.units =
+         mapAttrs' (n: v: nameValuePair "${n}.path"    (pathToUnit    n v)) cfg.user.paths
+      // mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.user.services
+      // mapAttrs' (n: v: nameValuePair "${n}.slice"   (sliceToUnit   n v)) cfg.user.slices
+      // mapAttrs' (n: v: nameValuePair "${n}.socket"  (socketToUnit  n v)) cfg.user.sockets
+      // mapAttrs' (n: v: nameValuePair "${n}.target"  (targetToUnit  n v)) cfg.user.targets
+      // mapAttrs' (n: v: nameValuePair "${n}.timer"   (timerToUnit   n v)) cfg.user.timers;
+
+    system.requiredKernelConfig = map config.lib.kernelConfig.isEnabled
+      [ "DEVTMPFS" "CGROUPS" "INOTIFY_USER" "SIGNALFD" "TIMERFD" "EPOLL" "NET"
+        "SYSFS" "PROC_FS" "FHANDLE" "CRYPTO_USER_API_HASH" "CRYPTO_HMAC"
+        "CRYPTO_SHA256" "DMIID" "AUTOFS4_FS" "TMPFS_POSIX_ACL"
+        "TMPFS_XATTR" "SECCOMP"
+      ];
+
+    users.groups.systemd-journal.gid = config.ids.gids.systemd-journal;
+    users.users.systemd-journal-gateway.uid = config.ids.uids.systemd-journal-gateway;
+    users.users.systemd-journal-gateway.group = "systemd-journal-gateway";
+    users.groups.systemd-journal-gateway.gid = config.ids.gids.systemd-journal-gateway;
+
+    # Generate timer units for all services that have a ‘startAt’ value.
+    systemd.timers =
+      mapAttrs (name: service:
+        { wantedBy = [ "timers.target" ];
+          timerConfig.OnCalendar = service.startAt;
+        })
+        (filterAttrs (name: service: service.enable && service.startAt != []) cfg.services);
+
+    # Generate timer units for all services that have a ‘startAt’ value.
+    systemd.user.timers =
+      mapAttrs (name: service:
+        { wantedBy = [ "timers.target" ];
+          timerConfig.OnCalendar = service.startAt;
+        })
+        (filterAttrs (name: service: service.startAt != []) cfg.user.services);
+
+    systemd.sockets.systemd-journal-gatewayd.wantedBy =
+      optional config.services.journald.enableHttpGateway "sockets.target";
+
+    # Provide the systemd-user PAM service, required to run systemd
+    # user instances.
+    security.pam.services.systemd-user =
+      { # Ensure that pam_systemd gets included. This is special-cased
+        # in systemd to provide XDG_RUNTIME_DIR.
+        startSession = true;
+      };
+
+    # Some overrides to upstream units.
+    systemd.services."systemd-backlight@".restartIfChanged = false;
+    systemd.services."systemd-fsck@".restartIfChanged = false;
+    systemd.services."systemd-fsck@".path = [ config.system.path ];
+    systemd.services."user@".restartIfChanged = false;
+    systemd.services.systemd-journal-flush.restartIfChanged = false;
+    systemd.services.systemd-random-seed.restartIfChanged = false;
+    systemd.services.systemd-remount-fs.restartIfChanged = false;
+    systemd.services.systemd-update-utmp.restartIfChanged = false;
+    systemd.services.systemd-user-sessions.restartIfChanged = false; # Restart kills all active sessions.
+    systemd.services.systemd-udev-settle.restartIfChanged = false; # Causes long delays in nixos-rebuild
+    # Restarting systemd-logind breaks X11
+    # - upstream commit: https://cgit.freedesktop.org/xorg/xserver/commit/?id=dc48bd653c7e101
+    # - systemd announcement: https://github.com/systemd/systemd/blob/22043e4317ecd2bc7834b48a6d364de76bb26d91/NEWS#L103-L112
+    # - this might be addressed in the future by xorg
+    #systemd.services.systemd-logind.restartTriggers = [ config.environment.etc."systemd/logind.conf".source ];
+    systemd.services.systemd-logind.restartIfChanged = false;
+    systemd.services.systemd-logind.stopIfChanged = false;
+    # The user-runtime-dir@ service is managed by systemd-logind we should not touch it or else we break the users' sessions.
+    systemd.services."user-runtime-dir@".stopIfChanged = false;
+    systemd.services."user-runtime-dir@".restartIfChanged = false;
+    systemd.services.systemd-journald.restartTriggers = [ config.environment.etc."systemd/journald.conf".source ];
+    systemd.services.systemd-journald.stopIfChanged = false;
+    systemd.services."systemd-journald@".restartTriggers = [ config.environment.etc."systemd/journald.conf".source ];
+    systemd.services."systemd-journald@".stopIfChanged = false;
+    systemd.targets.local-fs.unitConfig.X-StopOnReconfiguration = true;
+    systemd.targets.remote-fs.unitConfig.X-StopOnReconfiguration = true;
+    systemd.targets.network-online.wantedBy = [ "multi-user.target" ];
+    systemd.services.systemd-importd.environment = proxy_env;
+    systemd.services.systemd-pstore.wantedBy = [ "sysinit.target" ]; # see #81138
+
+    # Don't bother with certain units in containers.
+    systemd.services.systemd-remount-fs.unitConfig.ConditionVirtualization = "!container";
+    systemd.services.systemd-random-seed.unitConfig.ConditionVirtualization = "!container";
+
+    boot.kernel.sysctl."kernel.core_pattern" = mkIf (!cfg.coredump.enable) "core";
+
+    # Increase numeric PID range (set directly instead of copying a one-line file from systemd)
+    # https://github.com/systemd/systemd/pull/12226
+    boot.kernel.sysctl."kernel.pid_max" = mkIf pkgs.stdenv.is64bit (lib.mkDefault 4194304);
+
+    boot.kernelParams = optional (!cfg.enableUnifiedCgroupHierarchy) "systemd.unified_cgroup_hierarchy=0";
+
+    services.logrotate.paths = {
+      "/var/log/btmp" = mapAttrs (_: mkDefault) {
+        frequency = "monthly";
+        keep = 1;
+        extraConfig = ''
+          create 0660 root ${config.users.groups.utmp.name}
+          minsize 1M
+        '';
+      };
+      "/var/log/wtmp" = mapAttrs (_: mkDefault) {
+        frequency = "monthly";
+        keep = 1;
+        extraConfig = ''
+          create 0664 root ${config.users.groups.utmp.name}
+          minsize 1M
+        '';
+      };
+    };
+  };
+
+  # FIXME: Remove these eventually.
+  imports =
+    [ (mkRenamedOptionModule [ "boot" "systemd" "sockets" ] [ "systemd" "sockets" ])
+      (mkRenamedOptionModule [ "boot" "systemd" "targets" ] [ "systemd" "targets" ])
+      (mkRenamedOptionModule [ "boot" "systemd" "services" ] [ "systemd" "services" ])
+      (mkRenamedOptionModule [ "jobs" ] [ "systemd" "services" ])
+      (mkRemovedOptionModule [ "systemd" "generator-packages" ] "Use systemd.packages instead.")
+    ];
+}
diff --git a/nixos/modules/system/boot/timesyncd.nix b/nixos/modules/system/boot/timesyncd.nix
new file mode 100644
index 00000000000..5f35a154769
--- /dev/null
+++ b/nixos/modules/system/boot/timesyncd.nix
@@ -0,0 +1,74 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+
+  options = {
+
+    services.timesyncd = {
+      enable = mkOption {
+        default = !config.boot.isContainer;
+        defaultText = literalExpression "!config.boot.isContainer";
+        type = types.bool;
+        description = ''
+          Enables the systemd NTP client daemon.
+        '';
+      };
+      servers = mkOption {
+        default = config.networking.timeServers;
+        defaultText = literalExpression "config.networking.timeServers";
+        type = types.listOf types.str;
+        description = ''
+          The set of NTP servers from which to synchronise.
+        '';
+      };
+      extraConfig = mkOption {
+        default = "";
+        type = types.lines;
+        example = ''
+          PollIntervalMaxSec=180
+        '';
+        description = ''
+          Extra config options for systemd-timesyncd. See
+          <link xlink:href="https://www.freedesktop.org/software/systemd/man/timesyncd.conf.html">
+          timesyncd.conf(5)</link> for available options.
+        '';
+      };
+    };
+  };
+
+  config = mkIf config.services.timesyncd.enable {
+
+    systemd.additionalUpstreamSystemUnits = [ "systemd-timesyncd.service" ];
+
+    systemd.services.systemd-timesyncd = {
+      wantedBy = [ "sysinit.target" ];
+      aliases = [ "dbus-org.freedesktop.timesync1.service" ];
+      restartTriggers = [ config.environment.etc."systemd/timesyncd.conf".source ];
+    };
+
+    environment.etc."systemd/timesyncd.conf".text = ''
+      [Time]
+      NTP=${concatStringsSep " " config.services.timesyncd.servers}
+      ${config.services.timesyncd.extraConfig}
+    '';
+
+    users.users.systemd-timesync = {
+      uid = config.ids.uids.systemd-timesync;
+      group = "systemd-timesync";
+    };
+    users.groups.systemd-timesync.gid = config.ids.gids.systemd-timesync;
+
+    system.activationScripts.systemd-timesyncd-migration = mkIf (versionOlder config.system.stateVersion "19.09") ''
+      # workaround an issue of systemd-timesyncd not starting due to upstream systemd reverting their dynamic users changes
+      #  - https://github.com/NixOS/nixpkgs/pull/61321#issuecomment-492423742
+      #  - https://github.com/systemd/systemd/issues/12131
+      if [ -L /var/lib/systemd/timesync ]; then
+        rm /var/lib/systemd/timesync
+        mv /var/lib/private/systemd/timesync /var/lib/systemd/timesync
+      fi
+    '';
+  };
+
+}
diff --git a/nixos/modules/system/boot/tmp.nix b/nixos/modules/system/boot/tmp.nix
new file mode 100644
index 00000000000..cf6d19eb5f0
--- /dev/null
+++ b/nixos/modules/system/boot/tmp.nix
@@ -0,0 +1,64 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.boot;
+in
+{
+
+  ###### interface
+
+  options = {
+
+    boot.cleanTmpDir = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to delete all files in <filename>/tmp</filename> during boot.
+      '';
+    };
+
+    boot.tmpOnTmpfs = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+         Whether to mount a tmpfs on <filename>/tmp</filename> during boot.
+      '';
+    };
+
+    boot.tmpOnTmpfsSize = mkOption {
+      type = types.oneOf [ types.str types.types.ints.positive ];
+      default = "50%";
+      description = ''
+        Size of tmpfs in percentage.
+        Percentage is defined by systemd.
+      '';
+    };
+
+  };
+
+  ###### implementation
+
+  config = {
+
+    # When changing remember to update /tmp mount in virtualisation/qemu-vm.nix
+    systemd.mounts = mkIf cfg.tmpOnTmpfs [
+      {
+        what = "tmpfs";
+        where = "/tmp";
+        type = "tmpfs";
+        mountConfig.Options = concatStringsSep "," [ "mode=1777"
+                                                     "strictatime"
+                                                     "rw"
+                                                     "nosuid"
+                                                     "nodev"
+                                                     "size=${toString cfg.tmpOnTmpfsSize}" ];
+      }
+    ];
+
+    systemd.tmpfiles.rules = optional config.boot.cleanTmpDir "D! /tmp 1777 root root";
+
+  };
+
+}
diff --git a/nixos/modules/system/build.nix b/nixos/modules/system/build.nix
new file mode 100644
index 00000000000..58dc3f0d411
--- /dev/null
+++ b/nixos/modules/system/build.nix
@@ -0,0 +1,21 @@
+{ lib, ... }:
+let
+  inherit (lib) mkOption types;
+in
+{
+  options = {
+
+    system.build = mkOption {
+      default = {};
+      description = ''
+        Attribute set of derivations used to set up the system.
+      '';
+      type = types.submoduleWith {
+        modules = [{
+          freeformType = with types; lazyAttrsOf (uniq unspecified);
+        }];
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/system/etc/etc-activation.nix b/nixos/modules/system/etc/etc-activation.nix
new file mode 100644
index 00000000000..78010495018
--- /dev/null
+++ b/nixos/modules/system/etc/etc-activation.nix
@@ -0,0 +1,12 @@
+{ config, lib, ... }:
+let
+  inherit (lib) stringAfter;
+in {
+
+  imports = [ ./etc.nix ];
+
+  config = {
+    system.activationScripts.etc =
+      stringAfter [ "users" "groups" ] config.system.build.etcActivationCommands;
+  };
+}
diff --git a/nixos/modules/system/etc/etc.nix b/nixos/modules/system/etc/etc.nix
new file mode 100644
index 00000000000..ed552fecec5
--- /dev/null
+++ b/nixos/modules/system/etc/etc.nix
@@ -0,0 +1,201 @@
+# Management of static files in /etc.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  etc' = filter (f: f.enable) (attrValues config.environment.etc);
+
+  etc = pkgs.runCommandLocal "etc" {
+    # This is needed for the systemd module
+    passthru.targets = map (x: x.target) etc';
+  } /* sh */ ''
+    set -euo pipefail
+
+    makeEtcEntry() {
+      src="$1"
+      target="$2"
+      mode="$3"
+      user="$4"
+      group="$5"
+
+      if [[ "$src" = *'*'* ]]; then
+        # If the source name contains '*', perform globbing.
+        mkdir -p "$out/etc/$target"
+        for fn in $src; do
+            ln -s "$fn" "$out/etc/$target/"
+        done
+      else
+
+        mkdir -p "$out/etc/$(dirname "$target")"
+        if ! [ -e "$out/etc/$target" ]; then
+          ln -s "$src" "$out/etc/$target"
+        else
+          echo "duplicate entry $target -> $src"
+          if [ "$(readlink "$out/etc/$target")" != "$src" ]; then
+            echo "mismatched duplicate entry $(readlink "$out/etc/$target") <-> $src"
+            ret=1
+
+            continue
+          fi
+        fi
+
+        if [ "$mode" != symlink ]; then
+          echo "$mode" > "$out/etc/$target.mode"
+          echo "$user" > "$out/etc/$target.uid"
+          echo "$group" > "$out/etc/$target.gid"
+        fi
+      fi
+    }
+
+    mkdir -p "$out/etc"
+    ${concatMapStringsSep "\n" (etcEntry: escapeShellArgs [
+      "makeEtcEntry"
+      # Force local source paths to be added to the store
+      "${etcEntry.source}"
+      etcEntry.target
+      etcEntry.mode
+      etcEntry.user
+      etcEntry.group
+    ]) etc'}
+  '';
+
+in
+
+{
+
+  imports = [ ../build.nix ];
+
+  ###### interface
+
+  options = {
+
+    environment.etc = mkOption {
+      default = {};
+      example = literalExpression ''
+        { example-configuration-file =
+            { source = "/nix/store/.../etc/dir/file.conf.example";
+              mode = "0440";
+            };
+          "default/useradd".text = "GROUP=100 ...";
+        }
+      '';
+      description = ''
+        Set of files that have to be linked in <filename>/etc</filename>.
+      '';
+
+      type = with types; attrsOf (submodule (
+        { name, config, options, ... }:
+        { options = {
+
+            enable = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                Whether this /etc file should be generated.  This
+                option allows specific /etc files to be disabled.
+              '';
+            };
+
+            target = mkOption {
+              type = types.str;
+              description = ''
+                Name of symlink (relative to
+                <filename>/etc</filename>).  Defaults to the attribute
+                name.
+              '';
+            };
+
+            text = mkOption {
+              default = null;
+              type = types.nullOr types.lines;
+              description = "Text of the file.";
+            };
+
+            source = mkOption {
+              type = types.path;
+              description = "Path of the source file.";
+            };
+
+            mode = mkOption {
+              type = types.str;
+              default = "symlink";
+              example = "0600";
+              description = ''
+                If set to something else than <literal>symlink</literal>,
+                the file is copied instead of symlinked, with the given
+                file mode.
+              '';
+            };
+
+            uid = mkOption {
+              default = 0;
+              type = types.int;
+              description = ''
+                UID of created file. Only takes effect when the file is
+                copied (that is, the mode is not 'symlink').
+                '';
+            };
+
+            gid = mkOption {
+              default = 0;
+              type = types.int;
+              description = ''
+                GID of created file. Only takes effect when the file is
+                copied (that is, the mode is not 'symlink').
+              '';
+            };
+
+            user = mkOption {
+              default = "+${toString config.uid}";
+              type = types.str;
+              description = ''
+                User name of created file.
+                Only takes effect when the file is copied (that is, the mode is not 'symlink').
+                Changing this option takes precedence over <literal>uid</literal>.
+              '';
+            };
+
+            group = mkOption {
+              default = "+${toString config.gid}";
+              type = types.str;
+              description = ''
+                Group name of created file.
+                Only takes effect when the file is copied (that is, the mode is not 'symlink').
+                Changing this option takes precedence over <literal>gid</literal>.
+              '';
+            };
+
+          };
+
+          config = {
+            target = mkDefault name;
+            source = mkIf (config.text != null) (
+              let name' = "etc-" + baseNameOf name;
+              in mkDerivedConfig options.text (pkgs.writeText name')
+            );
+          };
+
+        }));
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = {
+
+    system.build.etc = etc;
+    system.build.etcActivationCommands =
+      ''
+        # Set up the statically computed bits of /etc.
+        echo "setting up /etc..."
+        ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc
+      '';
+  };
+
+}
diff --git a/nixos/modules/system/etc/setup-etc.pl b/nixos/modules/system/etc/setup-etc.pl
new file mode 100644
index 00000000000..be6b2d9ae71
--- /dev/null
+++ b/nixos/modules/system/etc/setup-etc.pl
@@ -0,0 +1,146 @@
+use strict;
+use File::Find;
+use File::Copy;
+use File::Path;
+use File::Basename;
+use File::Slurp;
+
+my $etc = $ARGV[0] or die;
+my $static = "/etc/static";
+
+sub atomicSymlink {
+    my ($source, $target) = @_;
+    my $tmp = "$target.tmp";
+    unlink $tmp;
+    symlink $source, $tmp or return 0;
+    rename $tmp, $target or return 0;
+    return 1;
+}
+
+
+# Atomically update /etc/static to point at the etc files of the
+# current configuration.
+atomicSymlink $etc, $static or die;
+
+# Returns 1 if the argument points to the files in /etc/static.  That
+# means either argument is a symlink to a file in /etc/static or a
+# directory with all children being static.
+sub isStatic {
+    my $path = shift;
+
+    if (-l $path) {
+        my $target = readlink $path;
+        return substr($target, 0, length "/etc/static/") eq "/etc/static/";
+    }
+
+    if (-d $path) {
+        opendir DIR, "$path" or return 0;
+        my @names = readdir DIR or die;
+        closedir DIR;
+
+        foreach my $name (@names) {
+            next if $name eq "." || $name eq "..";
+            unless (isStatic("$path/$name")) {
+                return 0;
+            }
+        }
+        return 1;
+    }
+
+    return 0;
+}
+
+# Remove dangling symlinks that point to /etc/static.  These are
+# configuration files that existed in a previous configuration but not
+# in the current one.  For efficiency, don't look under /etc/nixos
+# (where all the NixOS sources live).
+sub cleanup {
+    if ($File::Find::name eq "/etc/nixos") {
+        $File::Find::prune = 1;
+        return;
+    }
+    if (-l $_) {
+        my $target = readlink $_;
+        if (substr($target, 0, length $static) eq $static) {
+            my $x = "/etc/static/" . substr($File::Find::name, length "/etc/");
+            unless (-l $x) {
+                print STDERR "removing obsolete symlink ‘$File::Find::name’...\n";
+                unlink "$_";
+            }
+        }
+    }
+}
+
+find(\&cleanup, "/etc");
+
+
+# Use /etc/.clean to keep track of copied files.
+my @oldCopied = read_file("/etc/.clean", chomp => 1, err_mode => 'quiet');
+open CLEAN, ">>/etc/.clean";
+
+
+# For every file in the etc tree, create a corresponding symlink in
+# /etc to /etc/static.  The indirection through /etc/static is to make
+# switching to a new configuration somewhat more atomic.
+my %created;
+my @copied;
+
+sub link {
+    my $fn = substr $File::Find::name, length($etc) + 1 or next;
+    my $target = "/etc/$fn";
+    File::Path::make_path(dirname $target);
+    $created{$fn} = 1;
+
+    # Rename doesn't work if target is directory.
+    if (-l $_ && -d $target) {
+        if (isStatic $target) {
+            rmtree $target or warn;
+        } else {
+            warn "$target directory contains user files. Symlinking may fail.";
+        }
+    }
+
+    if (-e "$_.mode") {
+        my $mode = read_file("$_.mode"); chomp $mode;
+        if ($mode eq "direct-symlink") {
+            atomicSymlink readlink("$static/$fn"), $target or warn;
+        } else {
+            my $uid = read_file("$_.uid"); chomp $uid;
+            my $gid = read_file("$_.gid"); chomp $gid;
+            copy "$static/$fn", "$target.tmp" or warn;
+            $uid = getpwnam $uid unless $uid =~ /^\+/;
+            $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;
+        }
+        push @copied, $fn;
+        print CLEAN "$fn\n";
+    } elsif (-l "$_") {
+        atomicSymlink "$static/$fn", $target or warn;
+    }
+}
+
+find(\&link, $etc);
+
+
+# Delete files that were copied in a previous version but not in the
+# current.
+foreach my $fn (@oldCopied) {
+    if (!defined $created{$fn}) {
+        $fn = "/etc/$fn";
+        print STDERR "removing obsolete file ‘$fn’...\n";
+        unlink "$fn";
+    }
+}
+
+
+# Rewrite /etc/.clean.
+close CLEAN;
+write_file("/etc/.clean", map { "$_\n" } @copied);
+
+# Create /etc/NIXOS tag if not exists.
+# When /etc is not on a persistent filesystem, it will be wiped after reboot,
+# so we need to check and re-create it during activation.
+open TAG, ">>/etc/NIXOS";
+close TAG;
diff --git a/nixos/modules/system/etc/test.nix b/nixos/modules/system/etc/test.nix
new file mode 100644
index 00000000000..5e43b155038
--- /dev/null
+++ b/nixos/modules/system/etc/test.nix
@@ -0,0 +1,70 @@
+{ lib
+, coreutils
+, fakechroot
+, fakeroot
+, evalMinimalConfig
+, pkgsModule
+, runCommand
+, util-linux
+, vmTools
+, writeText
+}:
+let
+  node = evalMinimalConfig ({ config, ... }: {
+    imports = [ pkgsModule ../etc/etc.nix ];
+    environment.etc."passwd" = {
+      text = passwdText;
+    };
+    environment.etc."hosts" = {
+      text = hostsText;
+      mode = "0751";
+    };
+  });
+  passwdText = ''
+    root:x:0:0:System administrator:/root:/run/current-system/sw/bin/bash
+  '';
+  hostsText = ''
+    127.0.0.1 localhost
+    ::1 localhost
+    # testing...
+  '';
+in
+lib.recurseIntoAttrs {
+  test-etc-vm =
+    vmTools.runInLinuxVM (runCommand "test-etc-vm" { } ''
+      mkdir -p /etc
+      ${node.config.system.build.etcActivationCommands}
+      set -x
+      [[ -L /etc/passwd ]]
+      diff /etc/passwd ${writeText "expected-passwd" passwdText}
+      [[ 751 = $(stat --format %a /etc/hosts) ]]
+      diff /etc/hosts ${writeText "expected-hosts" hostsText}
+      set +x
+      touch $out
+    '');
+
+  # fakeroot is behaving weird
+  test-etc-fakeroot =
+    runCommand "test-etc"
+      {
+        nativeBuildInputs = [
+          fakeroot
+          fakechroot
+          # for chroot
+          coreutils
+          # fakechroot needs getopt, which is provided by util-linux
+          util-linux
+        ];
+        fakeRootCommands = ''
+          mkdir -p /etc
+          ${node.config.system.build.etcActivationCommands}
+          diff /etc/hosts ${writeText "expected-hosts" hostsText}
+          touch $out
+        '';
+      } ''
+      mkdir fake-root
+      export FAKECHROOT_EXCLUDE_PATH=/dev:/proc:/sys:${builtins.storeDir}:$out
+      fakechroot fakeroot chroot $PWD/fake-root bash -c 'source $stdenv/setup; eval "$fakeRootCommands"'
+    '';
+
+}
diff --git a/nixos/modules/tasks/auto-upgrade.nix b/nixos/modules/tasks/auto-upgrade.nix
new file mode 100644
index 00000000000..1404dcbaf7c
--- /dev/null
+++ b/nixos/modules/tasks/auto-upgrade.nix
@@ -0,0 +1,228 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.system.autoUpgrade;
+
+in {
+
+  options = {
+
+    system.autoUpgrade = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to periodically upgrade NixOS to the latest
+          version. If enabled, a systemd timer will run
+          <literal>nixos-rebuild switch --upgrade</literal> once a
+          day.
+        '';
+      };
+
+      flake = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "github:kloenk/nix";
+        description = ''
+          The Flake URI of the NixOS configuration to build.
+          Disables the option <option>system.autoUpgrade.channel</option>.
+        '';
+      };
+
+      channel = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "https://nixos.org/channels/nixos-14.12-small";
+        description = ''
+          The URI of the NixOS channel to use for automatic
+          upgrades. By default, this is the channel set using
+          <command>nix-channel</command> (run <literal>nix-channel
+          --list</literal> to see the current value).
+        '';
+      };
+
+      flags = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [
+          "-I"
+          "stuff=/home/alice/nixos-stuff"
+          "--option"
+          "extra-binary-caches"
+          "http://my-cache.example.org/"
+        ];
+        description = ''
+          Any additional flags passed to <command>nixos-rebuild</command>.
+
+          If you are using flakes and use a local repo you can add
+          <command>[ "--update-input" "nixpkgs" "--commit-lock-file" ]</command>
+          to update nixpkgs.
+        '';
+      };
+
+      dates = mkOption {
+        default = "04:40";
+        type = types.str;
+        description = ''
+          Specification (in the format described by
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>) of the time at
+          which the update will occur.
+        '';
+      };
+
+      allowReboot = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Reboot the system into the new generation instead of a switch
+          if the new generation uses a different kernel, kernel modules
+          or initrd than the booted system.
+          See <option>rebootWindow</option> for configuring the times at which a reboot is allowed.
+        '';
+      };
+
+      randomizedDelaySec = mkOption {
+        default = "0";
+        type = types.str;
+        example = "45min";
+        description = ''
+          Add a randomized delay before each automatic upgrade.
+          The delay will be chozen between zero and this value.
+          This value must be a time span in the format specified by
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>
+        '';
+      };
+
+      rebootWindow = mkOption {
+        description = ''
+          Define a lower and upper time value (in HH:MM format) which
+          constitute a time window during which reboots are allowed after an upgrade.
+          This option only has an effect when <option>allowReboot</option> is enabled.
+          The default value of <literal>null</literal> means that reboots are allowed at any time.
+        '';
+        default = null;
+        example = { lower = "01:00"; upper = "05:00"; };
+        type = with types; nullOr (submodule {
+          options = {
+            lower = mkOption {
+              description = "Lower limit of the reboot window";
+              type = types.strMatching "[[:digit:]]{2}:[[:digit:]]{2}";
+              example = "01:00";
+            };
+
+            upper = mkOption {
+              description = "Upper limit of the reboot window";
+              type = types.strMatching "[[:digit:]]{2}:[[:digit:]]{2}";
+              example = "05:00";
+            };
+          };
+        });
+      };
+
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    assertions = [{
+      assertion = !((cfg.channel != null) && (cfg.flake != null));
+      message = ''
+        The options 'system.autoUpgrade.channels' and 'system.autoUpgrade.flake' cannot both be set.
+      '';
+    }];
+
+    system.autoUpgrade.flags = (if cfg.flake == null then
+        [ "--no-build-output" ] ++ optionals (cfg.channel != null) [
+          "-I"
+          "nixpkgs=${cfg.channel}/nixexprs.tar.xz"
+        ]
+      else
+        [ "--flake ${cfg.flake}" ]);
+
+    systemd.services.nixos-upgrade = {
+      description = "NixOS Upgrade";
+
+      restartIfChanged = false;
+      unitConfig.X-StopOnRemoval = false;
+
+      serviceConfig.Type = "oneshot";
+
+      environment = config.nix.envVars // {
+        inherit (config.environment.sessionVariables) NIX_PATH;
+        HOME = "/root";
+      } // config.networking.proxy.envVars;
+
+      path = with pkgs; [
+        coreutils
+        gnutar
+        xz.bin
+        gzip
+        gitMinimal
+        config.nix.package.out
+        config.programs.ssh.package
+      ];
+
+      script = let
+        nixos-rebuild = "${config.system.build.nixos-rebuild}/bin/nixos-rebuild";
+        date     = "${pkgs.coreutils}/bin/date";
+        readlink = "${pkgs.coreutils}/bin/readlink";
+        shutdown = "${pkgs.systemd}/bin/shutdown";
+        upgradeFlag = optional (cfg.channel == null) "--upgrade";
+      in if cfg.allowReboot then ''
+        ${nixos-rebuild} boot ${toString (cfg.flags ++ upgradeFlag)}
+        booted="$(${readlink} /run/booted-system/{initrd,kernel,kernel-modules})"
+        built="$(${readlink} /nix/var/nix/profiles/system/{initrd,kernel,kernel-modules})"
+
+        ${optionalString (cfg.rebootWindow != null) ''
+          current_time="$(${date} +%H:%M)"
+
+          lower="${cfg.rebootWindow.lower}"
+          upper="${cfg.rebootWindow.upper}"
+
+          if [[ "''${lower}" < "''${upper}" ]]; then
+            if [[ "''${current_time}" > "''${lower}" ]] && \
+               [[ "''${current_time}" < "''${upper}" ]]; then
+              do_reboot="true"
+            else
+              do_reboot="false"
+            fi
+          else
+            # lower > upper, so we are crossing midnight (e.g. lower=23h, upper=6h)
+            # we want to reboot if cur > 23h or cur < 6h
+            if [[ "''${current_time}" < "''${upper}" ]] || \
+               [[ "''${current_time}" > "''${lower}" ]]; then
+              do_reboot="true"
+            else
+              do_reboot="false"
+            fi
+          fi
+        ''}
+
+        if [ "''${booted}" = "''${built}" ]; then
+          ${nixos-rebuild} switch ${toString cfg.flags}
+        ${optionalString (cfg.rebootWindow != null) ''
+          elif [ "''${do_reboot}" != true ]; then
+            echo "Outside of configured reboot window, skipping."
+        ''}
+        else
+          ${shutdown} -r +1
+        fi
+      '' else ''
+        ${nixos-rebuild} switch ${toString (cfg.flags ++ upgradeFlag)}
+      '';
+
+      startAt = cfg.dates;
+    };
+
+    systemd.timers.nixos-upgrade.timerConfig.RandomizedDelaySec =
+      cfg.randomizedDelaySec;
+
+  };
+
+}
+
diff --git a/nixos/modules/tasks/bcache.nix b/nixos/modules/tasks/bcache.nix
new file mode 100644
index 00000000000..41fb7664f3d
--- /dev/null
+++ b/nixos/modules/tasks/bcache.nix
@@ -0,0 +1,13 @@
+{ pkgs, ... }:
+
+{
+
+  environment.systemPackages = [ pkgs.bcache-tools ];
+
+  services.udev.packages = [ pkgs.bcache-tools ];
+
+  boot.initrd.extraUdevRulesCommands = ''
+    cp -v ${pkgs.bcache-tools}/lib/udev/rules.d/*.rules $out/
+  '';
+
+}
diff --git a/nixos/modules/tasks/cpu-freq.nix b/nixos/modules/tasks/cpu-freq.nix
new file mode 100644
index 00000000000..f1219c07c50
--- /dev/null
+++ b/nixos/modules/tasks/cpu-freq.nix
@@ -0,0 +1,90 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cpupower = config.boot.kernelPackages.cpupower;
+  cfg = config.powerManagement;
+in
+
+{
+  ###### interface
+
+  options.powerManagement = {
+
+    # TODO: This should be aliased to powerManagement.cpufreq.governor.
+    # https://github.com/NixOS/nixpkgs/pull/53041#commitcomment-31825338
+    cpuFreqGovernor = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "ondemand";
+      description = ''
+        Configure the governor used to regulate the frequency of the
+        available CPUs. By default, the kernel configures the
+        performance governor, although this may be overwritten in your
+        hardware-configuration.nix file.
+
+        Often used values: "ondemand", "powersave", "performance"
+      '';
+    };
+
+    cpufreq = {
+
+      max = mkOption {
+        type = types.nullOr types.ints.unsigned;
+        default = null;
+        example = 2200000;
+        description = ''
+          The maximum frequency the CPU will use.  Defaults to the maximum possible.
+        '';
+      };
+
+      min = mkOption {
+        type = types.nullOr types.ints.unsigned;
+        default = null;
+        example = 800000;
+        description = ''
+          The minimum frequency the CPU will use.
+        '';
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config =
+    let
+      governorEnable = cfg.cpuFreqGovernor != null;
+      maxEnable = cfg.cpufreq.max != null;
+      minEnable = cfg.cpufreq.min != null;
+      enable =
+        !config.boot.isContainer &&
+        (governorEnable || maxEnable || minEnable);
+    in
+    mkIf enable {
+
+      boot.kernelModules = optional governorEnable "cpufreq_${cfg.cpuFreqGovernor}";
+
+      environment.systemPackages = [ cpupower ];
+
+      systemd.services.cpufreq = {
+        description = "CPU Frequency Setup";
+        after = [ "systemd-modules-load.service" ];
+        wantedBy = [ "multi-user.target" ];
+        path = [ cpupower pkgs.kmod ];
+        unitConfig.ConditionVirtualization = false;
+        serviceConfig = {
+          Type = "oneshot";
+          RemainAfterExit = "yes";
+          ExecStart = "${cpupower}/bin/cpupower frequency-set " +
+            optionalString governorEnable "--governor ${cfg.cpuFreqGovernor} " +
+            optionalString maxEnable "--max ${toString cfg.cpufreq.max} " +
+            optionalString minEnable "--min ${toString cfg.cpufreq.min} ";
+          SuccessExitStatus = "0 237";
+        };
+      };
+
+  };
+}
diff --git a/nixos/modules/tasks/encrypted-devices.nix b/nixos/modules/tasks/encrypted-devices.nix
new file mode 100644
index 00000000000..06117d19af4
--- /dev/null
+++ b/nixos/modules/tasks/encrypted-devices.nix
@@ -0,0 +1,87 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  fileSystems = config.system.build.fileSystems ++ config.swapDevices;
+  encDevs = filter (dev: dev.encrypted.enable) fileSystems;
+  keyedEncDevs = filter (dev: dev.encrypted.keyFile != null) encDevs;
+  keylessEncDevs = filter (dev: dev.encrypted.keyFile == null) encDevs;
+  anyEncrypted =
+    foldr (j: v: v || j.encrypted.enable) false encDevs;
+
+  encryptedFSOptions = {
+
+    options.encrypted = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = "The block device is backed by an encrypted one, adds this device as a initrd luks entry.";
+      };
+
+      blkDev = mkOption {
+        default = null;
+        example = "/dev/sda1";
+        type = types.nullOr types.str;
+        description = "Location of the backing encrypted device.";
+      };
+
+      label = mkOption {
+        default = null;
+        example = "rootfs";
+        type = types.nullOr types.str;
+        description = "Label of the unlocked encrypted device. Set <literal>fileSystems.&lt;name?&gt;.device</literal> to <literal>/dev/mapper/&lt;label&gt;</literal> to mount the unlocked device.";
+      };
+
+      keyFile = mkOption {
+        default = null;
+        example = "/mnt-root/root/.swapkey";
+        type = types.nullOr types.str;
+        description = ''
+          Path to a keyfile used to unlock the backing encrypted
+          device. At the time this keyfile is accessed, the
+          <literal>neededForBoot</literal> filesystems (see
+          <literal>fileSystems.&lt;name?&gt;.neededForBoot</literal>)
+          will have been mounted under <literal>/mnt-root</literal>,
+          so the keyfile path should usually start with "/mnt-root/".
+        '';
+      };
+    };
+  };
+in
+
+{
+
+  options = {
+    fileSystems = mkOption {
+      type = with lib.types; attrsOf (submodule encryptedFSOptions);
+    };
+    swapDevices = mkOption {
+      type = with lib.types; listOf (submodule encryptedFSOptions);
+    };
+  };
+
+  config = mkIf anyEncrypted {
+    assertions = map (dev: {
+      assertion = dev.encrypted.label != null;
+      message = ''
+        The filesystem for ${dev.mountPoint} has encrypted.enable set to true, but no encrypted.label set
+      '';
+    }) encDevs;
+
+    boot.initrd = {
+      luks = {
+        devices =
+          builtins.listToAttrs (map (dev: {
+            name = dev.encrypted.label;
+            value = { device = dev.encrypted.blkDev; };
+          }) keylessEncDevs);
+        forceLuksSupportInInitrd = true;
+      };
+      postMountCommands =
+        concatMapStrings (dev:
+          "cryptsetup luksOpen --key-file ${dev.encrypted.keyFile} ${dev.encrypted.blkDev} ${dev.encrypted.label};\n"
+        ) keyedEncDevs;
+    };
+  };
+}
diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix
new file mode 100644
index 00000000000..f3da6771197
--- /dev/null
+++ b/nixos/modules/tasks/filesystems.nix
@@ -0,0 +1,385 @@
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+with utils;
+
+let
+
+  addCheckDesc = desc: elemType: check: types.addCheck elemType check
+    // { description = "${elemType.description} (with check: ${desc})"; };
+
+  isNonEmpty = s: (builtins.match "[ \t\n]*" s) == null;
+  nonEmptyStr = addCheckDesc "non-empty" types.str isNonEmpty;
+
+  fileSystems' = toposort fsBefore (attrValues config.fileSystems);
+
+  fileSystems = if fileSystems' ? result
+                then # use topologically sorted fileSystems everywhere
+                     fileSystems'.result
+                else # the assertion below will catch this,
+                     # but we fall back to the original order
+                     # anyway so that other modules could check
+                     # their assertions too
+                     (attrValues config.fileSystems);
+
+  specialFSTypes = [ "proc" "sysfs" "tmpfs" "ramfs" "devtmpfs" "devpts" ];
+
+  nonEmptyWithoutTrailingSlash = addCheckDesc "non-empty without trailing slash" types.str
+    (s: isNonEmpty s && (builtins.match ".+/" s) == null);
+
+  coreFileSystemOpts = { name, config, ... }: {
+
+    options = {
+      mountPoint = mkOption {
+        example = "/mnt/usb";
+        type = nonEmptyWithoutTrailingSlash;
+        description = "Location of the mounted the file system.";
+      };
+
+      device = mkOption {
+        default = null;
+        example = "/dev/sda";
+        type = types.nullOr nonEmptyStr;
+        description = "Location of the device.";
+      };
+
+      fsType = mkOption {
+        default = "auto";
+        example = "ext3";
+        type = nonEmptyStr;
+        description = "Type of the file system.";
+      };
+
+      options = mkOption {
+        default = [ "defaults" ];
+        example = [ "data=journal" ];
+        description = "Options used to mount the file system.";
+        type = types.listOf nonEmptyStr;
+      };
+
+      depends = mkOption {
+        default = [ ];
+        example = [ "/persist" ];
+        type = types.listOf nonEmptyWithoutTrailingSlash;
+        description = ''
+          List of paths that should be mounted before this one. This filesystem's
+          <option>device</option> and <option>mountPoint</option> are always
+          checked and do not need to be included explicitly. If a path is added
+          to this list, any other filesystem whose mount point is a parent of
+          the path will be mounted before this filesystem. The paths do not need
+          to actually be the <option>mountPoint</option> of some other filesystem.
+        '';
+      };
+
+    };
+
+    config = {
+      mountPoint = mkDefault name;
+      device = mkIf (elem config.fsType specialFSTypes) (mkDefault config.fsType);
+    };
+
+  };
+
+  fileSystemOpts = { config, ... }: {
+
+    options = {
+
+      label = mkOption {
+        default = null;
+        example = "root-partition";
+        type = types.nullOr nonEmptyStr;
+        description = "Label of the device (if any).";
+      };
+
+      autoFormat = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If the device does not currently contain a filesystem (as
+          determined by <command>blkid</command>, then automatically
+          format it with the filesystem type specified in
+          <option>fsType</option>.  Use with caution.
+        '';
+      };
+
+      formatOptions = mkOption {
+        default = "";
+        type = types.str;
+        description = ''
+          If <option>autoFormat</option> option is set specifies
+          extra options passed to mkfs.
+        '';
+      };
+
+      autoResize = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If set, the filesystem is grown to its maximum size before
+          being mounted. (This is typically the size of the containing
+          partition.) This is currently only supported for ext2/3/4
+          filesystems that are mounted during early boot.
+        '';
+      };
+
+      noCheck = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Disable running fsck on this filesystem.";
+      };
+
+    };
+
+    config = let
+      defaultFormatOptions =
+        # -F needed to allow bare block device without partitions
+        if (builtins.substring 0 3 config.fsType) == "ext" then "-F"
+        # -q needed for non-interactive operations
+        else if config.fsType == "jfs" then "-q"
+        # (same here)
+        else if config.fsType == "reiserfs" then "-q"
+        else null;
+    in {
+      options = mkIf config.autoResize [ "x-nixos.autoresize" ];
+      formatOptions = mkIf (defaultFormatOptions != null) (mkDefault defaultFormatOptions);
+    };
+
+  };
+
+  # Makes sequence of `specialMount device mountPoint options fsType` commands.
+  # `systemMount` should be defined in the sourcing script.
+  makeSpecialMounts = mounts:
+    pkgs.writeText "mounts.sh" (concatMapStringsSep "\n" (mount: ''
+      specialMount "${mount.device}" "${mount.mountPoint}" "${concatStringsSep "," mount.options}" "${mount.fsType}"
+    '') mounts);
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    fileSystems = mkOption {
+      default = {};
+      example = literalExpression ''
+        {
+          "/".device = "/dev/hda1";
+          "/data" = {
+            device = "/dev/hda2";
+            fsType = "ext3";
+            options = [ "data=journal" ];
+          };
+          "/bigdisk".label = "bigdisk";
+        }
+      '';
+      type = types.attrsOf (types.submodule [coreFileSystemOpts fileSystemOpts]);
+      description = ''
+        The file systems to be mounted.  It must include an entry for
+        the root directory (<literal>mountPoint = "/"</literal>).  Each
+        entry in the list is an attribute set with the following fields:
+        <literal>mountPoint</literal>, <literal>device</literal>,
+        <literal>fsType</literal> (a file system type recognised by
+        <command>mount</command>; defaults to
+        <literal>"auto"</literal>), and <literal>options</literal>
+        (the mount options passed to <command>mount</command> using the
+        <option>-o</option> flag; defaults to <literal>[ "defaults" ]</literal>).
+
+        Instead of specifying <literal>device</literal>, you can also
+        specify a volume label (<literal>label</literal>) for file
+        systems that support it, such as ext2/ext3 (see <command>mke2fs
+        -L</command>).
+      '';
+    };
+
+    system.fsPackages = mkOption {
+      internal = true;
+      default = [ ];
+      description = "Packages supplying file system mounters and checkers.";
+    };
+
+    boot.supportedFilesystems = mkOption {
+      default = [ ];
+      example = [ "btrfs" ];
+      type = types.listOf types.str;
+      description = "Names of supported filesystem types.";
+    };
+
+    boot.specialFileSystems = mkOption {
+      default = {};
+      type = types.attrsOf (types.submodule coreFileSystemOpts);
+      internal = true;
+      description = ''
+        Special filesystems that are mounted very early during boot.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = {
+
+    assertions = let
+      ls = sep: concatMapStringsSep sep (x: x.mountPoint);
+      notAutoResizable = fs: fs.autoResize && !(hasPrefix "ext" fs.fsType || fs.fsType == "f2fs");
+    in [
+      { assertion = ! (fileSystems' ? cycle);
+        message = "The ‘fileSystems’ option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}";
+      }
+      { assertion = ! (any notAutoResizable fileSystems);
+        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.";
+      }
+    ];
+
+    # Export for use in other modules
+    system.build.fileSystems = fileSystems;
+    system.build.earlyMountScript = makeSpecialMounts (toposort fsBefore (attrValues config.boot.specialFileSystems)).result;
+
+    boot.supportedFilesystems = map (fs: fs.fsType) fileSystems;
+
+    # Add the mount helpers to the system path so that `mount' can find them.
+    system.fsPackages = [ pkgs.dosfstools ];
+
+    environment.systemPackages = with pkgs; [ fuse3 fuse ] ++ config.system.fsPackages;
+
+    environment.etc.fstab.text =
+      let
+        fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "vboxsf" "glusterfs" "apfs" ];
+        skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck;
+        # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces
+        escape = string: builtins.replaceStrings [ " " "\t" ] [ "\\040" "\\011" ] string;
+        swapOptions = sw: concatStringsSep "," (
+          sw.options
+          ++ optional (sw.priority != null) "pri=${toString sw.priority}"
+          ++ optional (sw.discardPolicy != null) "discard${optionalString (sw.discardPolicy != "both") "=${toString sw.discardPolicy}"}"
+        );
+      in ''
+        # This is a generated file.  Do not edit!
+        #
+        # To make changes, edit the fileSystems and swapDevices NixOS options
+        # in your /etc/nixos/configuration.nix file.
+        #
+        # <file system> <mount point>   <type>  <options>       <dump>  <pass>
+
+        # Filesystems.
+        ${concatMapStrings (fs:
+            (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 fs.mountPoint
+            + " " + fs.fsType
+            + " " + builtins.concatStringsSep "," fs.options
+            + " 0"
+            + " " + (if skipCheck fs then "0" else
+                     if fs.mountPoint == "/" then "1" else "2")
+            + "\n"
+        ) fileSystems}
+
+        # Swap devices.
+        ${flip concatMapStrings config.swapDevices (sw:
+            "${sw.realDevice} none swap ${swapOptions sw}\n"
+        )}
+      '';
+
+    # Provide a target that pulls in all filesystems.
+    systemd.targets.fs =
+      { description = "All File Systems";
+        wants = [ "local-fs.target" "remote-fs.target" ];
+      };
+
+    systemd.services =
+
+    # Emit systemd services to format requested filesystems.
+      let
+        formatDevice = fs:
+          let
+            mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount";
+            device'  = escapeSystemdPath fs.device;
+            device'' = "${device'}.device";
+          in nameValuePair "mkfs-${device'}"
+          { description = "Initialisation of Filesystem ${fs.device}";
+            wantedBy = [ mountPoint' ];
+            before = [ mountPoint' "systemd-fsck@${device'}.service" ];
+            requires = [ device'' ];
+            after = [ device'' ];
+            path = [ pkgs.util-linux ] ++ config.system.fsPackages;
+            script =
+              ''
+                if ! [ -e "${fs.device}" ]; then exit 1; fi
+                # FIXME: this is scary.  The test could be more robust.
+                type=$(blkid -p -s TYPE -o value "${fs.device}" || true)
+                if [ -z "$type" ]; then
+                  echo "creating ${fs.fsType} filesystem on ${fs.device}..."
+                  mkfs.${fs.fsType} ${fs.formatOptions} "${fs.device}"
+                fi
+              '';
+            unitConfig.RequiresMountsFor = [ "${dirOf fs.device}" ];
+            unitConfig.DefaultDependencies = false; # needed to prevent a cycle
+            serviceConfig.Type = "oneshot";
+          };
+      in listToAttrs (map formatDevice (filter (fs: fs.autoFormat) fileSystems)) // {
+    # Mount /sys/fs/pstore for evacuating panic logs and crashdumps from persistent storage onto the disk using systemd-pstore.
+    # This cannot be done with the other special filesystems because the pstore module (which creates the mount point) is not loaded then.
+        "mount-pstore" = {
+          serviceConfig = {
+            Type = "oneshot";
+            # skip on kernels without the pstore module
+            ExecCondition = "${pkgs.kmod}/bin/modprobe -b pstore";
+            ExecStart = pkgs.writeShellScript "mount-pstore.sh" ''
+              set -eu
+              # if the pstore module is builtin it will have mounted the persistent store automatically. it may also be already mounted for other reasons.
+              ${pkgs.util-linux}/bin/mountpoint -q /sys/fs/pstore || ${pkgs.util-linux}/bin/mount -t pstore -o nosuid,noexec,nodev pstore /sys/fs/pstore
+              # wait up to 1.5 seconds for the backend to be registered and the files to appear. a systemd path unit cannot detect this happening; and succeeding after a restart would not start dependent units.
+              TRIES=15
+              while [ "$(cat /sys/module/pstore/parameters/backend)" = "(null)" ]; do
+                if (( $TRIES )); then
+                  sleep 0.1
+                  TRIES=$((TRIES-1))
+                else
+                  echo "Persistent Storage backend was not registered in time." >&2
+                  break
+                fi
+              done
+            '';
+            RemainAfterExit = true;
+          };
+          unitConfig = {
+            ConditionVirtualization = "!container";
+            DefaultDependencies = false; # needed to prevent a cycle
+          };
+          before = [ "systemd-pstore.service" ];
+          wantedBy = [ "systemd-pstore.service" ];
+        };
+      };
+
+    systemd.tmpfiles.rules = [
+      "d /run/keys 0750 root ${toString config.ids.gids.keys}"
+      "z /run/keys 0750 root ${toString config.ids.gids.keys}"
+    ];
+
+    # Sync mount options with systemd's src/core/mount-setup.c: mount_table.
+    boot.specialFileSystems = {
+      "/proc" = { fsType = "proc"; options = [ "nosuid" "noexec" "nodev" ]; };
+      "/run" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=755" "size=${config.boot.runSize}" ]; };
+      "/dev" = { fsType = "devtmpfs"; options = [ "nosuid" "strictatime" "mode=755" "size=${config.boot.devSize}" ]; };
+      "/dev/shm" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=1777" "size=${config.boot.devShmSize}" ]; };
+      "/dev/pts" = { fsType = "devpts"; options = [ "nosuid" "noexec" "mode=620" "ptmxmode=0666" "gid=${toString config.ids.gids.tty}" ]; };
+
+      # To hold secrets that shouldn't be written to disk
+      "/run/keys" = { fsType = "ramfs"; options = [ "nosuid" "nodev" "mode=750" ]; };
+    } // optionalAttrs (!config.boot.isContainer) {
+      # systemd-nspawn populates /sys by itself, and remounting it causes all
+      # kinds of weird issues (most noticeably, waiting for host disk device
+      # nodes).
+      "/sys" = { fsType = "sysfs"; options = [ "nosuid" "noexec" "nodev" ]; };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/tasks/filesystems/apfs.nix b/nixos/modules/tasks/filesystems/apfs.nix
new file mode 100644
index 00000000000..2f2be351df6
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/apfs.nix
@@ -0,0 +1,22 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inInitrd = any (fs: fs == "apfs") config.boot.initrd.supportedFilesystems;
+
+in
+
+{
+  config = mkIf (any (fs: fs == "apfs") config.boot.supportedFilesystems) {
+
+    system.fsPackages = [ pkgs.apfsprogs ];
+
+    boot.extraModulePackages = [ config.boot.kernelPackages.apfs ];
+
+    boot.initrd.kernelModules = mkIf inInitrd [ "apfs" ];
+
+    # Don't copy apfsck into the initramfs since it does not support repairing the filesystem
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/bcachefs.nix b/nixos/modules/tasks/filesystems/bcachefs.nix
new file mode 100644
index 00000000000..ac41ba5f93a
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/bcachefs.nix
@@ -0,0 +1,65 @@
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+
+let
+
+  bootFs = filterAttrs (n: fs: (fs.fsType == "bcachefs") && (utils.fsNeededForBoot fs)) config.fileSystems;
+
+  commonFunctions = ''
+    prompt() {
+        local name="$1"
+        printf "enter passphrase for $name: "
+    }
+    tryUnlock() {
+        local name="$1"
+        local path="$2"
+        if bcachefs unlock -c $path > /dev/null 2> /dev/null; then    # test for encryption
+            prompt $name
+            until bcachefs unlock $path 2> /dev/null; do              # repeat until sucessfully unlocked
+                printf "unlocking failed!\n"
+                prompt $name
+            done
+            printf "unlocking successful.\n"
+        fi
+    }
+  '';
+
+  openCommand = name: fs:
+    let
+      # we need only unlock one device manually, and cannot pass multiple at once
+      # remove this adaptation when bcachefs implements mounting by filesystem uuid
+      # also, implement automatic waiting for the constituent devices when that happens
+      # bcachefs does not support mounting devices with colons in the path, ergo we don't (see #49671)
+      firstDevice = head (splitString ":" fs.device);
+    in
+      ''
+        tryUnlock ${name} ${firstDevice}
+      '';
+
+in
+
+{
+  config = mkIf (elem "bcachefs" config.boot.supportedFilesystems) (mkMerge [
+    {
+      system.fsPackages = [ pkgs.bcachefs-tools ];
+
+      # use kernel package with bcachefs support until it's in mainline
+      boot.kernelPackages = pkgs.linuxPackages_testing_bcachefs;
+    }
+
+    (mkIf ((elem "bcachefs" config.boot.initrd.supportedFilesystems) || (bootFs != {})) {
+      # chacha20 and poly1305 are required only for decryption attempts
+      boot.initrd.availableKernelModules = [ "bcachefs" "sha256" "chacha20" "poly1305" ];
+
+      boot.initrd.extraUtilsCommands = ''
+        copy_bin_and_libs ${pkgs.bcachefs-tools}/bin/bcachefs
+      '';
+      boot.initrd.extraUtilsCommandsTest = ''
+        $out/bin/bcachefs version
+      '';
+
+      boot.initrd.postDeviceCommands = commonFunctions + concatStrings (mapAttrsToList openCommand bootFs);
+    })
+  ]);
+}
diff --git a/nixos/modules/tasks/filesystems/btrfs.nix b/nixos/modules/tasks/filesystems/btrfs.nix
new file mode 100644
index 00000000000..ae1dab5b8d8
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/btrfs.nix
@@ -0,0 +1,149 @@
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+
+let
+
+  inInitrd = any (fs: fs == "btrfs") config.boot.initrd.supportedFilesystems;
+  inSystem = any (fs: fs == "btrfs") config.boot.supportedFilesystems;
+
+  cfgScrub = config.services.btrfs.autoScrub;
+
+  enableAutoScrub = cfgScrub.enable;
+  enableBtrfs = inInitrd || inSystem || enableAutoScrub;
+
+in
+
+{
+  options = {
+    # One could also do regular btrfs balances, but that shouldn't be necessary
+    # during normal usage and as long as the filesystems aren't filled near capacity
+    services.btrfs.autoScrub = {
+      enable = mkEnableOption "regular btrfs scrub";
+
+      fileSystems = mkOption {
+        type = types.listOf types.path;
+        example = [ "/" ];
+        description = ''
+          List of paths to btrfs filesystems to regularily call <command>btrfs scrub</command> on.
+          Defaults to all mount points with btrfs filesystems.
+          If you mount a filesystem multiple times or additionally mount subvolumes,
+          you need to manually specify this list to avoid scrubbing multiple times.
+        '';
+      };
+
+      interval = mkOption {
+        default = "monthly";
+        type = types.str;
+        example = "weekly";
+        description = ''
+          Systemd calendar expression for when to scrub btrfs filesystems.
+          The recommended period is a month but could be less
+          (<citerefentry><refentrytitle>btrfs-scrub</refentrytitle>
+          <manvolnum>8</manvolnum></citerefentry>).
+          See
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>
+          for more information on the syntax.
+        '';
+      };
+
+    };
+  };
+
+  config = mkMerge [
+    (mkIf enableBtrfs {
+      system.fsPackages = [ pkgs.btrfs-progs ];
+
+      boot.initrd.kernelModules = mkIf inInitrd [ "btrfs" ];
+      boot.initrd.availableKernelModules = mkIf inInitrd (
+        [ "crc32c" ]
+        ++ optionals (config.boot.kernelPackages.kernel.kernelAtLeast "5.5") [
+          # Needed for mounting filesystems with new checksums
+          "xxhash_generic"
+          "blake2b_generic"
+          "sha256_generic" # Should be baked into our kernel, just to be sure
+        ]
+      );
+
+      boot.initrd.extraUtilsCommands = mkIf inInitrd
+      ''
+        copy_bin_and_libs ${pkgs.btrfs-progs}/bin/btrfs
+        ln -sv btrfs $out/bin/btrfsck
+        ln -sv btrfsck $out/bin/fsck.btrfs
+      '';
+
+      boot.initrd.extraUtilsCommandsTest = mkIf inInitrd
+      ''
+        $out/bin/btrfs --version
+      '';
+
+      boot.initrd.postDeviceCommands = mkIf inInitrd
+      ''
+        btrfs device scan
+      '';
+    })
+
+    (mkIf enableAutoScrub {
+      assertions = [
+        {
+          assertion = cfgScrub.enable -> (cfgScrub.fileSystems != []);
+          message = ''
+            If 'services.btrfs.autoScrub' is enabled, you need to have at least one
+            btrfs file system mounted via 'fileSystems' or specify a list manually
+            in 'services.btrfs.autoScrub.fileSystems'.
+          '';
+        }
+      ];
+
+      # This will yield duplicated units if the user mounts a filesystem multiple times
+      # or additionally mounts subvolumes, but going the other way around via devices would
+      # yield duplicated units when a filesystem spans multiple devices.
+      # This way around seems like the more sensible default.
+      services.btrfs.autoScrub.fileSystems = mkDefault (mapAttrsToList (name: fs: fs.mountPoint)
+      (filterAttrs (name: fs: fs.fsType == "btrfs") config.fileSystems));
+
+      # TODO: Did not manage to do it via the usual btrfs-scrub@.timer/.service
+      # template units due to problems enabling the parameterized units,
+      # so settled with many units and templating via nix for now.
+      # https://github.com/NixOS/nixpkgs/pull/32496#discussion_r156527544
+      systemd.timers = let
+        scrubTimer = fs: let
+          fs' = utils.escapeSystemdPath fs;
+        in nameValuePair "btrfs-scrub-${fs'}" {
+          description = "regular btrfs scrub timer on ${fs}";
+
+          wantedBy = [ "timers.target" ];
+          timerConfig = {
+            OnCalendar = cfgScrub.interval;
+            AccuracySec = "1d";
+            Persistent = true;
+          };
+        };
+      in listToAttrs (map scrubTimer cfgScrub.fileSystems);
+
+      systemd.services = let
+        scrubService = fs: let
+          fs' = utils.escapeSystemdPath fs;
+        in nameValuePair "btrfs-scrub-${fs'}" {
+          description = "btrfs scrub on ${fs}";
+          # scrub prevents suspend2ram or proper shutdown
+          conflicts = [ "shutdown.target" "sleep.target" ];
+          before = [ "shutdown.target" "sleep.target" ];
+
+          serviceConfig = {
+            # simple and not oneshot, otherwise ExecStop is not used
+            Type = "simple";
+            Nice = 19;
+            IOSchedulingClass = "idle";
+            ExecStart = "${pkgs.btrfs-progs}/bin/btrfs scrub start -B ${fs}";
+            # if the service is stopped before scrub end, cancel it
+            ExecStop  = pkgs.writeShellScript "btrfs-scrub-maybe-cancel" ''
+              (${pkgs.btrfs-progs}/bin/btrfs scrub status ${fs} | ${pkgs.gnugrep}/bin/grep finished) || ${pkgs.btrfs-progs}/bin/btrfs scrub cancel ${fs}
+            '';
+          };
+        };
+      in listToAttrs (map scrubService cfgScrub.fileSystems);
+    })
+  ];
+}
diff --git a/nixos/modules/tasks/filesystems/cifs.nix b/nixos/modules/tasks/filesystems/cifs.nix
new file mode 100644
index 00000000000..47ba0c03c56
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/cifs.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inInitrd = any (fs: fs == "cifs") config.boot.initrd.supportedFilesystems;
+
+in
+
+{
+  config = {
+
+    system.fsPackages = mkIf (any (fs: fs == "cifs") config.boot.supportedFilesystems) [ pkgs.cifs-utils ];
+
+    boot.initrd.availableKernelModules = mkIf inInitrd
+      [ "cifs" "nls_utf8" "hmac" "md4" "ecb" "des_generic" "sha256" ];
+
+    boot.initrd.extraUtilsCommands = mkIf inInitrd
+      ''
+        copy_bin_and_libs ${pkgs.cifs-utils}/sbin/mount.cifs
+      '';
+
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/ecryptfs.nix b/nixos/modules/tasks/filesystems/ecryptfs.nix
new file mode 100644
index 00000000000..8138e659161
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/ecryptfs.nix
@@ -0,0 +1,24 @@
+{ config, lib, pkgs, ... }:
+# TODO: make ecryptfs work in initramfs?
+
+with lib;
+
+{
+  config = mkIf (any (fs: fs == "ecryptfs") config.boot.supportedFilesystems) {
+    system.fsPackages = [ pkgs.ecryptfs ];
+    security.wrappers = {
+      "mount.ecryptfs_private" =
+        { setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${pkgs.ecryptfs.out}/bin/mount.ecryptfs_private";
+        };
+      "umount.ecryptfs_private" =
+        { setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${pkgs.ecryptfs.out}/bin/umount.ecryptfs_private";
+        };
+    };
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/exfat.nix b/nixos/modules/tasks/filesystems/exfat.nix
new file mode 100644
index 00000000000..540b9b91c3e
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/exfat.nix
@@ -0,0 +1,13 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  config = mkIf (any (fs: fs == "exfat") config.boot.supportedFilesystems) {
+    system.fsPackages = if config.boot.kernelPackages.kernelOlder "5.7" then [
+      pkgs.exfat # FUSE
+    ] else [
+      pkgs.exfatprogs # non-FUSE
+    ];
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/ext.nix b/nixos/modules/tasks/filesystems/ext.nix
new file mode 100644
index 00000000000..a14a3ac3854
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/ext.nix
@@ -0,0 +1,22 @@
+{ pkgs, ... }:
+
+{
+  config = {
+
+    system.fsPackages = [ pkgs.e2fsprogs ];
+
+    # As of kernel 4.3, there is no separate ext3 driver (they're also handled by ext4.ko)
+    boot.initrd.availableKernelModules = [ "ext2" "ext4" ];
+
+    boot.initrd.extraUtilsCommands =
+      ''
+        # Copy e2fsck and friends.
+        copy_bin_and_libs ${pkgs.e2fsprogs}/sbin/e2fsck
+        copy_bin_and_libs ${pkgs.e2fsprogs}/sbin/tune2fs
+        ln -sv e2fsck $out/bin/fsck.ext2
+        ln -sv e2fsck $out/bin/fsck.ext3
+        ln -sv e2fsck $out/bin/fsck.ext4
+      '';
+
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/f2fs.nix b/nixos/modules/tasks/filesystems/f2fs.nix
new file mode 100644
index 00000000000..a305235979a
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/f2fs.nix
@@ -0,0 +1,25 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  inInitrd = any (fs: fs == "f2fs") config.boot.initrd.supportedFilesystems;
+  fileSystems = filter (x: x.fsType == "f2fs") config.system.build.fileSystems;
+in
+{
+  config = mkIf (any (fs: fs == "f2fs") config.boot.supportedFilesystems) {
+
+    system.fsPackages = [ pkgs.f2fs-tools ];
+
+    boot.initrd.availableKernelModules = mkIf inInitrd [ "f2fs" "crc32" ];
+
+    boot.initrd.extraUtilsCommands = mkIf inInitrd ''
+      copy_bin_and_libs ${pkgs.f2fs-tools}/sbin/fsck.f2fs
+      ${optionalString (any (fs: fs.autoResize) fileSystems) ''
+        # We need f2fs-tools' tools to resize filesystems
+        copy_bin_and_libs ${pkgs.f2fs-tools}/sbin/resize.f2fs
+      ''}
+
+    '';
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/glusterfs.nix b/nixos/modules/tasks/filesystems/glusterfs.nix
new file mode 100644
index 00000000000..e8c7fa8efba
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/glusterfs.nix
@@ -0,0 +1,11 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  config = mkIf (any (fs: fs == "glusterfs") config.boot.supportedFilesystems) {
+
+    system.fsPackages = [ pkgs.glusterfs ];
+
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/jfs.nix b/nixos/modules/tasks/filesystems/jfs.nix
new file mode 100644
index 00000000000..fc3905c7dc2
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/jfs.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  inInitrd = any (fs: fs == "jfs") config.boot.initrd.supportedFilesystems;
+in
+{
+  config = mkIf (any (fs: fs == "jfs") config.boot.supportedFilesystems) {
+
+    system.fsPackages = [ pkgs.jfsutils ];
+
+    boot.initrd.kernelModules = mkIf inInitrd [ "jfs" ];
+
+    boot.initrd.extraUtilsCommands = mkIf inInitrd ''
+      copy_bin_and_libs ${pkgs.jfsutils}/sbin/fsck.jfs
+    '';
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/nfs.nix b/nixos/modules/tasks/filesystems/nfs.nix
new file mode 100644
index 00000000000..38c3920a78a
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/nfs.nix
@@ -0,0 +1,135 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inInitrd = any (fs: fs == "nfs") config.boot.initrd.supportedFilesystems;
+
+  nfsStateDir = "/var/lib/nfs";
+
+  rpcMountpoint = "${nfsStateDir}/rpc_pipefs";
+
+  format = pkgs.formats.ini {};
+
+  idmapdConfFile = format.generate "idmapd.conf" cfg.idmapd.settings;
+  nfsConfFile = pkgs.writeText "nfs.conf" cfg.extraConfig;
+  requestKeyConfFile = pkgs.writeText "request-key.conf" ''
+    create id_resolver * * ${pkgs.nfs-utils}/bin/nfsidmap -t 600 %k %d
+  '';
+
+  cfg = config.services.nfs;
+
+in
+
+{
+  ###### interface
+
+  options = {
+    services.nfs = {
+      idmapd.settings = mkOption {
+        type = format.type;
+        default = {};
+        description = ''
+          libnfsidmap configuration. Refer to
+          <link xlink:href="https://linux.die.net/man/5/idmapd.conf"/>
+          for details.
+        '';
+        example = literalExpression ''
+          {
+            Translation = {
+              GSS-Methods = "static,nsswitch";
+            };
+            Static = {
+              "root/hostname.domain.com@REALM.COM" = "root";
+            };
+          }
+        '';
+      };
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra nfs-utils configuration.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf (any (fs: fs == "nfs" || fs == "nfs4") config.boot.supportedFilesystems) {
+
+    services.rpcbind.enable = true;
+
+    services.nfs.idmapd.settings = {
+      General = mkMerge [
+        { Pipefs-Directory = rpcMountpoint; }
+        (mkIf (config.networking.domain != null) { Domain = config.networking.domain; })
+      ];
+      Mapping = {
+        Nobody-User = "nobody";
+        Nobody-Group = "nogroup";
+      };
+      Translation = {
+        Method = "nsswitch";
+      };
+    };
+
+    system.fsPackages = [ pkgs.nfs-utils ];
+
+    boot.initrd.kernelModules = mkIf inInitrd [ "nfs" ];
+
+    systemd.packages = [ pkgs.nfs-utils ];
+
+    environment.systemPackages = [ pkgs.keyutils ];
+
+    environment.etc = {
+      "idmapd.conf".source = idmapdConfFile;
+      "nfs.conf".source = nfsConfFile;
+      "request-key.conf".source = requestKeyConfFile;
+    };
+
+    systemd.services.nfs-blkmap =
+      { restartTriggers = [ nfsConfFile ];
+      };
+
+    systemd.targets.nfs-client =
+      { wantedBy = [ "multi-user.target" "remote-fs.target" ];
+      };
+
+    systemd.services.nfs-idmapd =
+      { restartTriggers = [ idmapdConfFile ];
+      };
+
+    systemd.services.nfs-mountd =
+      { restartTriggers = [ nfsConfFile ];
+        enable = mkDefault false;
+      };
+
+    systemd.services.nfs-server =
+      { restartTriggers = [ nfsConfFile ];
+        enable = mkDefault false;
+      };
+
+    systemd.services.auth-rpcgss-module =
+      {
+        unitConfig.ConditionPathExists = [ "" "/etc/krb5.keytab" ];
+      };
+
+    systemd.services.rpc-gssd =
+      { restartTriggers = [ nfsConfFile ];
+        unitConfig.ConditionPathExists = [ "" "/etc/krb5.keytab" ];
+      };
+
+    systemd.services.rpc-statd =
+      { restartTriggers = [ nfsConfFile ];
+
+        preStart =
+          ''
+            mkdir -p /var/lib/nfs/{sm,sm.bak}
+          '';
+      };
+
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/ntfs.nix b/nixos/modules/tasks/filesystems/ntfs.nix
new file mode 100644
index 00000000000..c40d2a1a80b
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/ntfs.nix
@@ -0,0 +1,11 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  config = mkIf (any (fs: fs == "ntfs" || fs == "ntfs-3g") config.boot.supportedFilesystems) {
+
+    system.fsPackages = [ pkgs.ntfs3g ];
+
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/reiserfs.nix b/nixos/modules/tasks/filesystems/reiserfs.nix
new file mode 100644
index 00000000000..ab4c43e2ab8
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/reiserfs.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inInitrd = any (fs: fs == "reiserfs") config.boot.initrd.supportedFilesystems;
+
+in
+
+{
+  config = mkIf (any (fs: fs == "reiserfs") config.boot.supportedFilesystems) {
+
+    system.fsPackages = [ pkgs.reiserfsprogs ];
+
+    boot.initrd.kernelModules = mkIf inInitrd [ "reiserfs" ];
+
+    boot.initrd.extraUtilsCommands = mkIf inInitrd
+      ''
+        copy_bin_and_libs ${pkgs.reiserfsprogs}/sbin/reiserfsck
+        ln -s reiserfsck $out/bin/fsck.reiserfs
+      '';
+
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/unionfs-fuse.nix b/nixos/modules/tasks/filesystems/unionfs-fuse.nix
new file mode 100644
index 00000000000..f54f3559c34
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/unionfs-fuse.nix
@@ -0,0 +1,32 @@
+{ config, pkgs, lib, ... }:
+
+{
+  config = lib.mkMerge [
+
+    (lib.mkIf (lib.any (fs: fs == "unionfs-fuse") config.boot.initrd.supportedFilesystems) {
+      boot.initrd.kernelModules = [ "fuse" ];
+
+      boot.initrd.extraUtilsCommands = ''
+        copy_bin_and_libs ${pkgs.fuse}/sbin/mount.fuse
+        copy_bin_and_libs ${pkgs.unionfs-fuse}/bin/unionfs
+        substitute ${pkgs.unionfs-fuse}/sbin/mount.unionfs-fuse $out/bin/mount.unionfs-fuse \
+          --replace '${pkgs.bash}/bin/bash' /bin/sh \
+          --replace '${pkgs.fuse}/sbin' /bin \
+          --replace '${pkgs.unionfs-fuse}/bin' /bin
+        chmod +x $out/bin/mount.unionfs-fuse
+      '';
+
+      boot.initrd.postDeviceCommands = ''
+          # Hacky!!! fuse hard-codes the path to mount
+          mkdir -p /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${pkgs.util-linux.name}-bin/bin
+          ln -s $(which mount) /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${pkgs.util-linux.name}-bin/bin
+          ln -s $(which umount) /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${pkgs.util-linux.name}-bin/bin
+        '';
+    })
+
+    (lib.mkIf (lib.any (fs: fs == "unionfs-fuse") config.boot.supportedFilesystems) {
+      system.fsPackages = [ pkgs.unionfs-fuse ];
+    })
+
+  ];
+}
diff --git a/nixos/modules/tasks/filesystems/vboxsf.nix b/nixos/modules/tasks/filesystems/vboxsf.nix
new file mode 100644
index 00000000000..5497194f6a8
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/vboxsf.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inInitrd = any (fs: fs == "vboxsf") config.boot.initrd.supportedFilesystems;
+
+  package = pkgs.runCommand "mount.vboxsf" { preferLocalBuild = true; } ''
+    mkdir -p $out/bin
+    cp ${pkgs.linuxPackages.virtualboxGuestAdditions}/bin/mount.vboxsf $out/bin
+  '';
+in
+
+{
+  config = mkIf (any (fs: fs == "vboxsf") config.boot.supportedFilesystems) {
+
+    system.fsPackages = [ package ];
+
+    boot.initrd.kernelModules = mkIf inInitrd [ "vboxsf" ];
+
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/vfat.nix b/nixos/modules/tasks/filesystems/vfat.nix
new file mode 100644
index 00000000000..958e27ae8a3
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/vfat.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inInitrd = any (fs: fs == "vfat") config.boot.initrd.supportedFilesystems;
+
+in
+
+{
+  config = mkIf (any (fs: fs == "vfat") config.boot.supportedFilesystems) {
+
+    system.fsPackages = [ pkgs.dosfstools ];
+
+    boot.initrd.kernelModules = mkIf inInitrd [ "vfat" "nls_cp437" "nls_iso8859-1" ];
+
+    boot.initrd.extraUtilsCommands = mkIf inInitrd
+      ''
+        copy_bin_and_libs ${pkgs.dosfstools}/sbin/dosfsck
+        ln -sv dosfsck $out/bin/fsck.vfat
+      '';
+
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/xfs.nix b/nixos/modules/tasks/filesystems/xfs.nix
new file mode 100644
index 00000000000..98038701ca5
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/xfs.nix
@@ -0,0 +1,30 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  inInitrd = any (fs: fs == "xfs") config.boot.initrd.supportedFilesystems;
+
+in
+
+{
+  config = mkIf (any (fs: fs == "xfs") config.boot.supportedFilesystems) {
+
+    system.fsPackages = [ pkgs.xfsprogs.bin ];
+
+    boot.initrd.availableKernelModules = mkIf inInitrd [ "xfs" "crc32c" ];
+
+    boot.initrd.extraUtilsCommands = mkIf inInitrd
+      ''
+        copy_bin_and_libs ${pkgs.xfsprogs.bin}/bin/fsck.xfs
+        copy_bin_and_libs ${pkgs.xfsprogs.bin}/bin/xfs_repair
+      '';
+
+    # Trick just to set 'sh' after the extraUtils nuke-refs.
+    boot.initrd.extraUtilsCommandsTest = mkIf inInitrd
+      ''
+        sed -i -e 's,^#!.*,#!'$out/bin/sh, $out/bin/fsck.xfs
+      '';
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix
new file mode 100644
index 00000000000..3bc0dedec00
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/zfs.nix
@@ -0,0 +1,795 @@
+{ config, lib, options, pkgs, utils, ... }:
+#
+# TODO: zfs tunables
+
+with utils;
+with lib;
+
+let
+
+  cfgZfs = config.boot.zfs;
+  optZfs = options.boot.zfs;
+  cfgExpandOnBoot = config.services.zfs.expandOnBoot;
+  cfgSnapshots = config.services.zfs.autoSnapshot;
+  cfgSnapFlags = cfgSnapshots.flags;
+  cfgScrub = config.services.zfs.autoScrub;
+  cfgTrim = config.services.zfs.trim;
+  cfgZED = config.services.zfs.zed;
+
+  inInitrd = any (fs: fs == "zfs") config.boot.initrd.supportedFilesystems;
+  inSystem = any (fs: fs == "zfs") config.boot.supportedFilesystems;
+
+  autosnapPkg = pkgs.zfstools.override {
+    zfs = cfgZfs.package;
+  };
+
+  zfsAutoSnap = "${autosnapPkg}/bin/zfs-auto-snapshot";
+
+  datasetToPool = x: elemAt (splitString "/" x) 0;
+
+  fsToPool = fs: datasetToPool fs.device;
+
+  zfsFilesystems = filter (x: x.fsType == "zfs") config.system.build.fileSystems;
+
+  allPools = unique ((map fsToPool zfsFilesystems) ++ cfgZfs.extraPools);
+
+  rootPools = unique (map fsToPool (filter fsNeededForBoot zfsFilesystems));
+
+  dataPools = unique (filter (pool: !(elem pool rootPools)) allPools);
+
+  snapshotNames = [ "frequent" "hourly" "daily" "weekly" "monthly" ];
+
+  # When importing ZFS pools, there's one difficulty: These scripts may run
+  # before the backing devices (physical HDDs, etc.) of the pool have been
+  # scanned and initialized.
+  #
+  # An attempted import with all devices missing will just fail, and can be
+  # retried, but an import where e.g. two out of three disks in a three-way
+  # mirror are missing, will succeed. This is a problem: When the missing disks
+  # are later discovered, they won't be automatically set online, rendering the
+  # pool redundancy-less (and far slower) until such time as the system reboots.
+  #
+  # The solution is the below. poolReady checks the status of an un-imported
+  # pool, to see if *every* device is available -- in which case the pool will be
+  # in state ONLINE, as opposed to DEGRADED, FAULTED or MISSING.
+  #
+  # The import scripts then loop over this, waiting until the pool is ready or a
+  # sufficient amount of time has passed that we can assume it won't be. In the
+  # latter case it makes one last attempt at importing, allowing the system to
+  # (eventually) boot even with a degraded pool.
+  importLib = {zpoolCmd, awkCmd, cfgZfs}: ''
+    poolReady() {
+      pool="$1"
+      state="$("${zpoolCmd}" import 2>/dev/null | "${awkCmd}" "/pool: $pool/ { found = 1 }; /state:/ { if (found == 1) { print \$2; exit } }; END { if (found == 0) { print \"MISSING\" } }")"
+      if [[ "$state" = "ONLINE" ]]; then
+        return 0
+      else
+        echo "Pool $pool in state $state, waiting"
+        return 1
+      fi
+    }
+    poolImported() {
+      pool="$1"
+      "${zpoolCmd}" list "$pool" >/dev/null 2>/dev/null
+    }
+    poolImport() {
+      pool="$1"
+      "${zpoolCmd}" import -d "${cfgZfs.devNodes}" -N $ZFS_FORCE "$pool"
+    }
+  '';
+
+  zedConf = generators.toKeyValue {
+    mkKeyValue = generators.mkKeyValueDefault {
+      mkValueString = v:
+        if isInt           v then toString v
+        else if isString   v then "\"${v}\""
+        else if true  ==   v then "1"
+        else if false ==   v then "0"
+        else if isList     v then "\"" + (concatStringsSep " " v) + "\""
+        else err "this value is" (toString v);
+    } "=";
+  } cfgZED.settings;
+in
+
+{
+
+  imports = [
+    (mkRemovedOptionModule [ "boot" "zfs" "enableLegacyCrypto" ] "The corresponding package was removed from nixpkgs.")
+  ];
+
+  ###### interface
+
+  options = {
+    boot.zfs = {
+      package = mkOption {
+        readOnly = true;
+        type = types.package;
+        default = if config.boot.zfs.enableUnstable then pkgs.zfsUnstable else pkgs.zfs;
+        defaultText = literalExpression "if config.boot.zfs.enableUnstable then pkgs.zfsUnstable else pkgs.zfs";
+        description = "Configured ZFS userland tools package.";
+      };
+
+      enabled = mkOption {
+        readOnly = true;
+        type = types.bool;
+        default = inInitrd || inSystem;
+        defaultText = literalDocBook "<literal>true</literal> if ZFS filesystem support is enabled";
+        description = "True if ZFS filesystem support is enabled";
+      };
+
+      enableUnstable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Use the unstable zfs package. This might be an option, if the latest
+          kernel is not yet supported by a published release of ZFS. Enabling
+          this option will install a development version of ZFS on Linux. The
+          version will have already passed an extensive test suite, but it is
+          more likely to hit an undiscovered bug compared to running a released
+          version of ZFS on Linux.
+          '';
+      };
+
+      extraPools = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "tank" "data" ];
+        description = ''
+          Name or GUID of extra ZFS pools that you wish to import during boot.
+
+          Usually this is not necessary. Instead, you should set the mountpoint property
+          of ZFS filesystems to <literal>legacy</literal> and add the ZFS filesystems to
+          NixOS's <option>fileSystems</option> option, which makes NixOS automatically
+          import the associated pool.
+
+          However, in some cases (e.g. if you have many filesystems) it may be preferable
+          to exclusively use ZFS commands to manage filesystems. If so, since NixOS/systemd
+          will not be managing those filesystems, you will need to specify the ZFS pool here
+          so that NixOS automatically imports it on every boot.
+        '';
+      };
+
+      devNodes = mkOption {
+        type = types.path;
+        default = "/dev/disk/by-id";
+        description = ''
+          Name of directory from which to import ZFS devices.
+
+          This should be a path under /dev containing stable names for all devices needed, as
+          import may fail if device nodes are renamed concurrently with a device failing.
+        '';
+      };
+
+      forceImportRoot = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Forcibly import the ZFS root pool(s) during early boot.
+
+          This is enabled by default for backwards compatibility purposes, but it is highly
+          recommended to disable this option, as it bypasses some of the safeguards ZFS uses
+          to protect your ZFS pools.
+
+          If you set this option to <literal>false</literal> and NixOS subsequently fails to
+          boot because it cannot import the root pool, you should boot with the
+          <literal>zfs_force=1</literal> option as a kernel parameter (e.g. by manually
+          editing the kernel params in grub during boot). You should only need to do this
+          once.
+        '';
+      };
+
+      forceImportAll = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Forcibly import all ZFS pool(s).
+
+          If you set this option to <literal>false</literal> and NixOS subsequently fails to
+          import your non-root ZFS pool(s), you should manually import each pool with
+          "zpool import -f &lt;pool-name&gt;", and then reboot. You should only need to do
+          this once.
+        '';
+      };
+
+      requestEncryptionCredentials = mkOption {
+        type = types.either types.bool (types.listOf types.str);
+        default = true;
+        example = [ "tank" "data" ];
+        description = ''
+          If true on import encryption keys or passwords for all encrypted datasets
+          are requested. To only decrypt selected datasets supply a list of dataset
+          names instead. For root pools the encryption key can be supplied via both
+          an interactive prompt (keylocation=prompt) and from a file (keylocation=file://).
+        '';
+      };
+    };
+
+    services.zfs.autoSnapshot = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable the (OpenSolaris-compatible) ZFS auto-snapshotting service.
+          Note that you must set the <literal>com.sun:auto-snapshot</literal>
+          property to <literal>true</literal> on all datasets which you wish
+          to auto-snapshot.
+
+          You can override a child dataset to use, or not use auto-snapshotting
+          by setting its flag with the given interval:
+          <literal>zfs set com.sun:auto-snapshot:weekly=false DATASET</literal>
+        '';
+      };
+
+      flags = mkOption {
+        default = "-k -p";
+        example = "-k -p --utc";
+        type = types.str;
+        description = ''
+          Flags to pass to the zfs-auto-snapshot command.
+
+          Run <literal>zfs-auto-snapshot</literal> (without any arguments) to
+          see available flags.
+
+          If it's not too inconvenient for snapshots to have timestamps in UTC,
+          it is suggested that you append <literal>--utc</literal> to the list
+          of default options (see example).
+
+          Otherwise, snapshot names can cause name conflicts or apparent time
+          reversals due to daylight savings, timezone or other date/time changes.
+        '';
+      };
+
+      frequent = mkOption {
+        default = 4;
+        type = types.int;
+        description = ''
+          Number of frequent (15-minute) auto-snapshots that you wish to keep.
+        '';
+      };
+
+      hourly = mkOption {
+        default = 24;
+        type = types.int;
+        description = ''
+          Number of hourly auto-snapshots that you wish to keep.
+        '';
+      };
+
+      daily = mkOption {
+        default = 7;
+        type = types.int;
+        description = ''
+          Number of daily auto-snapshots that you wish to keep.
+        '';
+      };
+
+      weekly = mkOption {
+        default = 4;
+        type = types.int;
+        description = ''
+          Number of weekly auto-snapshots that you wish to keep.
+        '';
+      };
+
+      monthly = mkOption {
+        default = 12;
+        type = types.int;
+        description = ''
+          Number of monthly auto-snapshots that you wish to keep.
+        '';
+      };
+    };
+
+    services.zfs.trim = {
+      enable = mkOption {
+        description = "Whether to enable periodic TRIM on all ZFS pools.";
+        default = true;
+        example = false;
+        type = types.bool;
+      };
+
+      interval = mkOption {
+        default = "weekly";
+        type = types.str;
+        example = "daily";
+        description = ''
+          How often we run trim. For most desktop and server systems
+          a sufficient trimming frequency is once a week.
+
+          The format is described in
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+    };
+
+    services.zfs.autoScrub = {
+      enable = mkEnableOption "periodic scrubbing of ZFS pools";
+
+      interval = mkOption {
+        default = "Sun, 02:00";
+        type = types.str;
+        example = "daily";
+        description = ''
+          Systemd calendar expression when to scrub ZFS pools. See
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+
+      pools = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        example = [ "tank" ];
+        description = ''
+          List of ZFS pools to periodically scrub. If empty, all pools
+          will be scrubbed.
+        '';
+      };
+    };
+
+    services.zfs.expandOnBoot = mkOption {
+      type = types.either (types.enum [ "disabled" "all" ]) (types.listOf types.str);
+      default = "disabled";
+      example = [ "tank" "dozer" ];
+      description = ''
+        After importing, expand each device in the specified pools.
+
+        Set the value to the plain string "all" to expand all pools on boot:
+
+            services.zfs.expandOnBoot = "all";
+
+        or set the value to a list of pools to expand the disks of specific pools:
+
+            services.zfs.expandOnBoot = [ "tank" "dozer" ];
+      '';
+    };
+
+    services.zfs.zed = {
+      enableMail = mkEnableOption "ZED's ability to send emails" // {
+        default = cfgZfs.package.enableMail;
+        defaultText = literalExpression "config.${optZfs.package}.enableMail";
+      };
+
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ str int bool (listOf str) ]);
+        example = literalExpression ''
+          {
+            ZED_DEBUG_LOG = "/tmp/zed.debug.log";
+
+            ZED_EMAIL_ADDR = [ "root" ];
+            ZED_EMAIL_PROG = "mail";
+            ZED_EMAIL_OPTS = "-s '@SUBJECT@' @ADDRESS@";
+
+            ZED_NOTIFY_INTERVAL_SECS = 3600;
+            ZED_NOTIFY_VERBOSE = false;
+
+            ZED_USE_ENCLOSURE_LEDS = true;
+            ZED_SCRUB_AFTER_RESILVER = false;
+          }
+        '';
+        description = ''
+          ZFS Event Daemon /etc/zfs/zed.d/zed.rc content
+
+          See
+          <citerefentry><refentrytitle>zed</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+          for details on ZED and the scripts in /etc/zfs/zed.d to find the possible variables
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkMerge [
+    (mkIf cfgZfs.enabled {
+      assertions = [
+        {
+          assertion = cfgZED.enableMail -> cfgZfs.package.enableMail;
+          message = ''
+            To allow ZED to send emails, ZFS needs to be configured to enable
+            this. To do so, one must override the `zfs` package and set
+            `enableMail` to true.
+          '';
+        }
+        {
+          assertion = config.networking.hostId != null;
+          message = "ZFS requires networking.hostId to be set";
+        }
+        {
+          assertion = !cfgZfs.forceImportAll || cfgZfs.forceImportRoot;
+          message = "If you enable boot.zfs.forceImportAll, you must also enable boot.zfs.forceImportRoot";
+        }
+      ];
+
+      boot = {
+        kernelModules = [ "zfs" ];
+
+        extraModulePackages = [
+          (if config.boot.zfs.enableUnstable then
+            config.boot.kernelPackages.zfsUnstable
+           else
+            config.boot.kernelPackages.zfs)
+        ];
+      };
+
+      boot.initrd = mkIf inInitrd {
+        kernelModules = [ "zfs" ] ++ optional (!cfgZfs.enableUnstable) "spl";
+        extraUtilsCommands =
+          ''
+            copy_bin_and_libs ${cfgZfs.package}/sbin/zfs
+            copy_bin_and_libs ${cfgZfs.package}/sbin/zdb
+            copy_bin_and_libs ${cfgZfs.package}/sbin/zpool
+          '';
+        extraUtilsCommandsTest = mkIf inInitrd
+          ''
+            $out/bin/zfs --help >/dev/null 2>&1
+            $out/bin/zpool --help >/dev/null 2>&1
+          '';
+        postDeviceCommands = concatStringsSep "\n" ([''
+            ZFS_FORCE="${optionalString cfgZfs.forceImportRoot "-f"}"
+
+            for o in $(cat /proc/cmdline); do
+              case $o in
+                zfs_force|zfs_force=1)
+                  ZFS_FORCE="-f"
+                  ;;
+              esac
+            done
+          ''] ++ [(importLib {
+            # See comments at importLib definition.
+            zpoolCmd = "zpool";
+            awkCmd = "awk";
+            inherit cfgZfs;
+          })] ++ (map (pool: ''
+            echo -n "importing root ZFS pool \"${pool}\"..."
+            # Loop across the import until it succeeds, because the devices needed may not be discovered yet.
+            if ! poolImported "${pool}"; then
+              for trial in `seq 1 60`; do
+                poolReady "${pool}" > /dev/null && msg="$(poolImport "${pool}" 2>&1)" && break
+                sleep 1
+                echo -n .
+              done
+              echo
+              if [[ -n "$msg" ]]; then
+                echo "$msg";
+              fi
+              poolImported "${pool}" || poolImport "${pool}"  # Try one last time, e.g. to import a degraded pool.
+            fi
+            ${if isBool cfgZfs.requestEncryptionCredentials
+              then optionalString cfgZfs.requestEncryptionCredentials ''
+                zfs load-key -a
+              ''
+              else concatMapStrings (fs: ''
+                zfs load-key ${fs}
+              '') cfgZfs.requestEncryptionCredentials}
+        '') rootPools));
+      };
+
+      # TODO FIXME See https://github.com/NixOS/nixpkgs/pull/99386#issuecomment-798813567. To not break people's bootloader and as probably not everybody would read release notes that thoroughly add inSystem.
+      boot.loader.grub = mkIf (inInitrd || inSystem) {
+        zfsSupport = true;
+      };
+
+      services.zfs.zed.settings = {
+        ZED_EMAIL_PROG = mkIf cfgZED.enableMail (mkDefault "${pkgs.mailutils}/bin/mail");
+        PATH = lib.makeBinPath [
+          cfgZfs.package
+          pkgs.coreutils
+          pkgs.curl
+          pkgs.gawk
+          pkgs.gnugrep
+          pkgs.gnused
+          pkgs.nettools
+          pkgs.util-linux
+        ];
+      };
+
+      environment.etc = genAttrs
+        (map
+          (file: "zfs/zed.d/${file}")
+          [
+            "all-syslog.sh"
+            "pool_import-led.sh"
+            "resilver_finish-start-scrub.sh"
+            "statechange-led.sh"
+            "vdev_attach-led.sh"
+            "zed-functions.sh"
+            "data-notify.sh"
+            "resilver_finish-notify.sh"
+            "scrub_finish-notify.sh"
+            "statechange-notify.sh"
+            "vdev_clear-led.sh"
+          ]
+        )
+        (file: { source = "${cfgZfs.package}/etc/${file}"; })
+      // {
+        "zfs/zed.d/zed.rc".text = zedConf;
+        "zfs/zpool.d".source = "${cfgZfs.package}/etc/zfs/zpool.d/";
+      };
+
+      system.fsPackages = [ cfgZfs.package ]; # XXX: needed? zfs doesn't have (need) a fsck
+      environment.systemPackages = [ cfgZfs.package ]
+        ++ optional cfgSnapshots.enable autosnapPkg; # so the user can run the command to see flags
+
+      services.udev.packages = [ cfgZfs.package ]; # to hook zvol naming, etc.
+      systemd.packages = [ cfgZfs.package ];
+
+      systemd.services = let
+        getPoolFilesystems = pool:
+          filter (x: x.fsType == "zfs" && (fsToPool x) == pool) config.system.build.fileSystems;
+
+        getPoolMounts = pool:
+          let
+            mountPoint = fs: escapeSystemdPath fs.mountPoint;
+          in
+            map (x: "${mountPoint x}.mount") (getPoolFilesystems pool);
+
+        createImportService = pool:
+          nameValuePair "zfs-import-${pool}" {
+            description = "Import ZFS pool \"${pool}\"";
+            # we need systemd-udev-settle until https://github.com/zfsonlinux/zfs/pull/4943 is merged
+            requires = [ "systemd-udev-settle.service" ];
+            after = [
+              "systemd-udev-settle.service"
+              "systemd-modules-load.service"
+              "systemd-ask-password-console.service"
+            ];
+            wantedBy = (getPoolMounts pool) ++ [ "local-fs.target" ];
+            before = (getPoolMounts pool) ++ [ "local-fs.target" ];
+            unitConfig = {
+              DefaultDependencies = "no";
+            };
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+            };
+            environment.ZFS_FORCE = optionalString cfgZfs.forceImportAll "-f";
+            script = (importLib {
+              # See comments at importLib definition.
+              zpoolCmd = "${cfgZfs.package}/sbin/zpool";
+              awkCmd = "${pkgs.gawk}/bin/awk";
+              inherit cfgZfs;
+            }) + ''
+              poolImported "${pool}" && exit
+              echo -n "importing ZFS pool \"${pool}\"..."
+              # Loop across the import until it succeeds, because the devices needed may not be discovered yet.
+              for trial in `seq 1 60`; do
+                poolReady "${pool}" && poolImport "${pool}" && break
+                sleep 1
+              done
+              poolImported "${pool}" || poolImport "${pool}"  # Try one last time, e.g. to import a degraded pool.
+              if poolImported "${pool}"; then
+                ${optionalString (if isBool cfgZfs.requestEncryptionCredentials
+                                  then cfgZfs.requestEncryptionCredentials
+                                  else cfgZfs.requestEncryptionCredentials != []) ''
+                  ${cfgZfs.package}/sbin/zfs list -rHo name,keylocation ${pool} | while IFS=$'\t' read ds kl; do
+                    {
+                      ${optionalString (!isBool cfgZfs.requestEncryptionCredentials) ''
+                         if ! echo '${concatStringsSep "\n" cfgZfs.requestEncryptionCredentials}' | grep -qFx "$ds"; then
+                           continue
+                         fi
+                       ''}
+                    case "$kl" in
+                      none )
+                        ;;
+                      prompt )
+                        ${config.systemd.package}/bin/systemd-ask-password "Enter key for $ds:" | ${cfgZfs.package}/sbin/zfs load-key "$ds"
+                        ;;
+                      * )
+                        ${cfgZfs.package}/sbin/zfs load-key "$ds"
+                        ;;
+                    esac
+                    } < /dev/null # To protect while read ds kl in case anything reads stdin
+                  done
+                ''}
+                echo "Successfully imported ${pool}"
+              else
+                exit 1
+              fi
+            '';
+          };
+
+        # This forces a sync of any ZFS pools prior to poweroff, even if they're set
+        # to sync=disabled.
+        createSyncService = pool:
+          nameValuePair "zfs-sync-${pool}" {
+            description = "Sync ZFS pool \"${pool}\"";
+            wantedBy = [ "shutdown.target" ];
+            unitConfig = {
+              DefaultDependencies = false;
+            };
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+            };
+            script = ''
+              ${cfgZfs.package}/sbin/zfs set nixos:shutdown-time="$(date)" "${pool}"
+            '';
+          };
+
+        createZfsService = serv:
+          nameValuePair serv {
+            after = [ "systemd-modules-load.service" ];
+            wantedBy = [ "zfs.target" ];
+          };
+
+      in listToAttrs (map createImportService dataPools ++
+                      map createSyncService allPools ++
+                      map createZfsService [ "zfs-mount" "zfs-share" "zfs-zed" ]);
+
+      systemd.targets.zfs-import =
+        let
+          services = map (pool: "zfs-import-${pool}.service") dataPools;
+        in
+          {
+            requires = services;
+            after = services;
+            wantedBy = [ "zfs.target" ];
+          };
+
+      systemd.targets.zfs.wantedBy = [ "multi-user.target" ];
+    })
+
+    (mkIf (cfgZfs.enabled && cfgExpandOnBoot != "disabled") {
+      systemd.services."zpool-expand@" = {
+        description = "Expand ZFS pools";
+        after = [ "zfs.target" ];
+
+        serviceConfig = {
+          Type = "oneshot";
+          RemainAfterExit = true;
+        };
+
+        scriptArgs = "%i";
+        path = [ pkgs.gawk cfgZfs.package ];
+
+        # ZFS has no way of enumerating just devices in a pool in a way
+        # that 'zpool online -e' supports. Thus, we've implemented a
+        # bit of a strange approach of highlighting just devices.
+        # See: https://github.com/openzfs/zfs/issues/12505
+        script = let
+          # This UUID has been chosen at random and is to provide a
+          # collision-proof, predictable token to search for
+          magicIdentifier = "NIXOS-ZFS-ZPOOL-DEVICE-IDENTIFIER-37108bec-aff6-4b58-9e5e-53c7c9766f05";
+          zpoolScripts = pkgs.writeShellScriptBin "device-highlighter" ''
+            echo "${magicIdentifier}"
+          '';
+        in ''
+          pool=$1
+
+          echo "Expanding all devices for $pool."
+
+          # Put our device-highlighter script it to the PATH
+          export ZPOOL_SCRIPTS_PATH=${zpoolScripts}/bin
+
+          # Enable running our precisely specified zpool script as root
+          export ZPOOL_SCRIPTS_AS_ROOT=1
+
+          devices() (
+            zpool status -c device-highlighter "$pool" \
+             | awk '($2 == "ONLINE" && $6 == "${magicIdentifier}") { print $1; }'
+          )
+
+          for device in $(devices); do
+            echo "Attempting to expand $device of $pool..."
+            if ! zpool online -e "$pool" "$device"; then
+              echo "Failed to expand '$device' of '$pool'."
+            fi
+          done
+        '';
+      };
+
+      systemd.services."zpool-expand-pools" =
+        let
+          # Create a string, to be interpolated in a bash script
+          # which enumerates all of the pools to expand.
+          # If the `pools` option is `true`, we want to dynamically
+          # expand every pool. Otherwise we want to enumerate
+          # just the specifically provided list of pools.
+          poolListProvider = if cfgExpandOnBoot == "all"
+            then "$(zpool list -H | awk '{print $1}')"
+            else lib.escapeShellArgs cfgExpandOnBoot;
+        in
+        {
+          description = "Expand specified ZFS pools";
+          wantedBy = [ "default.target" ];
+          after = [ "zfs.target" ];
+
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+          };
+
+          path = [ pkgs.gawk cfgZfs.package ];
+
+          script = ''
+            for pool in ${poolListProvider}; do
+              systemctl start --no-block "zpool-expand@$pool"
+            done
+          '';
+        };
+    })
+
+    (mkIf (cfgZfs.enabled && cfgSnapshots.enable) {
+      systemd.services = let
+                           descr = name: if name == "frequent" then "15 mins"
+                                    else if name == "hourly" then "hour"
+                                    else if name == "daily" then "day"
+                                    else if name == "weekly" then "week"
+                                    else if name == "monthly" then "month"
+                                    else throw "unknown snapshot name";
+                           numSnapshots = name: builtins.getAttr name cfgSnapshots;
+                         in builtins.listToAttrs (map (snapName:
+                              {
+                                name = "zfs-snapshot-${snapName}";
+                                value = {
+                                  description = "ZFS auto-snapshotting every ${descr snapName}";
+                                  after = [ "zfs-import.target" ];
+                                  serviceConfig = {
+                                    Type = "oneshot";
+                                    ExecStart = "${zfsAutoSnap} ${cfgSnapFlags} ${snapName} ${toString (numSnapshots snapName)}";
+                                  };
+                                  restartIfChanged = false;
+                                };
+                              }) snapshotNames);
+
+      systemd.timers = let
+                         timer = name: if name == "frequent" then "*:0,15,30,45" else name;
+                       in builtins.listToAttrs (map (snapName:
+                            {
+                              name = "zfs-snapshot-${snapName}";
+                              value = {
+                                wantedBy = [ "timers.target" ];
+                                timerConfig = {
+                                  OnCalendar = timer snapName;
+                                  Persistent = "yes";
+                                };
+                              };
+                            }) snapshotNames);
+    })
+
+    (mkIf (cfgZfs.enabled && cfgScrub.enable) {
+      systemd.services.zfs-scrub = {
+        description = "ZFS pools scrubbing";
+        after = [ "zfs-import.target" ];
+        serviceConfig = {
+          Type = "oneshot";
+        };
+        script = ''
+          ${cfgZfs.package}/bin/zpool scrub ${
+            if cfgScrub.pools != [] then
+              (concatStringsSep " " cfgScrub.pools)
+            else
+              "$(${cfgZfs.package}/bin/zpool list -H -o name)"
+            }
+        '';
+      };
+
+      systemd.timers.zfs-scrub = {
+        wantedBy = [ "timers.target" ];
+        after = [ "multi-user.target" ]; # Apparently scrubbing before boot is complete hangs the system? #53583
+        timerConfig = {
+          OnCalendar = cfgScrub.interval;
+          Persistent = "yes";
+        };
+      };
+    })
+
+    (mkIf (cfgZfs.enabled && cfgTrim.enable) {
+      systemd.services.zpool-trim = {
+        description = "ZFS pools trim";
+        after = [ "zfs-import.target" ];
+        path = [ cfgZfs.package ];
+        startAt = cfgTrim.interval;
+        # By default we ignore errors returned by the trim command, in case:
+        # - HDDs are mixed with SSDs
+        # - There is a SSDs in a pool that is currently trimmed.
+        # - There are only HDDs and we would set the system in a degraded state
+        serviceConfig.ExecStart = "${pkgs.runtimeShell} -c 'for pool in $(zpool list -H -o name); do zpool trim $pool;  done || true' ";
+      };
+
+      systemd.timers.zpool-trim.timerConfig.Persistent = "yes";
+    })
+  ];
+}
diff --git a/nixos/modules/tasks/lvm.nix b/nixos/modules/tasks/lvm.nix
new file mode 100644
index 00000000000..35316603c38
--- /dev/null
+++ b/nixos/modules/tasks/lvm.nix
@@ -0,0 +1,84 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.lvm;
+in {
+  options.services.lvm = {
+    package = mkOption {
+      type = types.package;
+      default = if cfg.dmeventd.enable then pkgs.lvm2_dmeventd else pkgs.lvm2;
+      internal = true;
+      defaultText = literalExpression "pkgs.lvm2";
+      description = ''
+        This option allows you to override the LVM package that's used on the system
+        (udev rules, tmpfiles, systemd services).
+        Defaults to pkgs.lvm2, or pkgs.lvm2_dmeventd if dmeventd is enabled.
+      '';
+    };
+    dmeventd.enable = mkEnableOption "the LVM dmevent daemon";
+    boot.thin.enable = mkEnableOption "support for booting from ThinLVs";
+  };
+
+  config = mkMerge [
+    ({
+      # minimal configuration file to make lvmconfig/lvm2-activation-generator happy
+      environment.etc."lvm/lvm.conf".text = "config {}";
+    })
+    (mkIf (!config.boot.isContainer) {
+      systemd.tmpfiles.packages = [ cfg.package.out ];
+      environment.systemPackages = [ cfg.package ];
+      systemd.packages = [ cfg.package ];
+
+      # TODO: update once https://github.com/NixOS/nixpkgs/pull/93006 was merged
+      services.udev.packages = [ cfg.package.out ];
+    })
+    (mkIf cfg.dmeventd.enable {
+      systemd.sockets."dm-event".wantedBy = [ "sockets.target" ];
+      systemd.services."lvm2-monitor".wantedBy = [ "sysinit.target" ];
+
+      environment.etc."lvm/lvm.conf".text = ''
+        dmeventd/executable = "${cfg.package}/bin/dmeventd"
+      '';
+    })
+    (mkIf cfg.boot.thin.enable {
+      boot.initrd = {
+        kernelModules = [ "dm-snapshot" "dm-thin-pool" ];
+
+        extraUtilsCommands = ''
+          for BIN in ${pkgs.thin-provisioning-tools}/bin/*; do
+            copy_bin_and_libs $BIN
+          done
+        '';
+
+        extraUtilsCommandsTest = ''
+          ls ${pkgs.thin-provisioning-tools}/bin/ | grep -v pdata_tools | while read BIN; do
+            $out/bin/$(basename $BIN) --help > /dev/null
+          done
+        '';
+      };
+
+      environment.etc."lvm/lvm.conf".text = concatMapStringsSep "\n"
+        (bin: "global/${bin}_executable = ${pkgs.thin-provisioning-tools}/bin/${bin}")
+        [ "thin_check" "thin_dump" "thin_repair" "cache_check" "cache_dump" "cache_repair" ];
+    })
+    (mkIf (cfg.dmeventd.enable || cfg.boot.thin.enable) {
+      boot.initrd.preLVMCommands = ''
+          mkdir -p /etc/lvm
+          cat << EOF >> /etc/lvm/lvm.conf
+          ${optionalString cfg.boot.thin.enable (
+            concatMapStringsSep "\n"
+              (bin: "global/${bin}_executable = $(command -v ${bin})")
+              [ "thin_check" "thin_dump" "thin_repair" "cache_check" "cache_dump" "cache_repair" ]
+            )
+          }
+          ${optionalString cfg.dmeventd.enable ''
+            dmeventd/executable = "$(command -v false)"
+            activation/monitoring = 0
+          ''}
+          EOF
+      '';
+    })
+  ];
+
+}
diff --git a/nixos/modules/tasks/network-interfaces-scripted.nix b/nixos/modules/tasks/network-interfaces-scripted.nix
new file mode 100644
index 00000000000..19f2be2c4a2
--- /dev/null
+++ b/nixos/modules/tasks/network-interfaces-scripted.nix
@@ -0,0 +1,625 @@
+{ config, lib, pkgs, utils, ... }:
+
+with utils;
+with lib;
+
+let
+
+  cfg = config.networking;
+  interfaces = attrValues cfg.interfaces;
+
+  slaves = concatMap (i: i.interfaces) (attrValues cfg.bonds)
+    ++ concatMap (i: i.interfaces) (attrValues cfg.bridges)
+    ++ concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues cfg.vswitches)
+    ++ concatMap (i: [i.interface]) (attrValues cfg.macvlans)
+    ++ concatMap (i: [i.interface]) (attrValues cfg.vlans);
+
+  # We must escape interfaces due to the systemd interpretation
+  subsystemDevice = interface:
+    "sys-subsystem-net-devices-${escapeSystemdPath interface}.device";
+
+  interfaceIps = i:
+    i.ipv4.addresses
+    ++ optionals cfg.enableIPv6 i.ipv6.addresses;
+
+  destroyBond = i: ''
+    while true; do
+      UPDATED=1
+      SLAVES=$(ip link | grep 'master ${i}' | awk -F: '{print $2}')
+      for I in $SLAVES; do
+        UPDATED=0
+        ip link set "$I" nomaster
+      done
+      [ "$UPDATED" -eq "1" ] && break
+    done
+    ip link set "${i}" down 2>/dev/null || true
+    ip link del "${i}" 2>/dev/null || true
+  '';
+
+  # warn that these attributes are deprecated (2017-2-2)
+  # Should be removed in the release after next
+  bondDeprecation = rec {
+    deprecated = [ "lacp_rate" "miimon" "mode" "xmit_hash_policy" ];
+    filterDeprecated = bond: (filterAttrs (attrName: attr:
+                         elem attrName deprecated && attr != null) bond);
+  };
+
+  bondWarnings =
+    let oneBondWarnings = bondName: bond:
+          mapAttrsToList (bondText bondName) (bondDeprecation.filterDeprecated bond);
+        bondText = bondName: optName: _:
+          "${bondName}.${optName} is deprecated, use ${bondName}.driverOptions";
+    in {
+      warnings = flatten (mapAttrsToList oneBondWarnings cfg.bonds);
+    };
+
+  normalConfig = {
+    systemd.network.links = let
+      createNetworkLink = i: nameValuePair "40-${i.name}" {
+        matchConfig.OriginalName = i.name;
+        linkConfig = optionalAttrs (i.macAddress != null) {
+          MACAddress = i.macAddress;
+        } // optionalAttrs (i.mtu != null) {
+          MTUBytes = toString i.mtu;
+        } // optionalAttrs (i.wakeOnLan.enable == true) {
+          WakeOnLan = "magic";
+        };
+      };
+    in listToAttrs (map createNetworkLink interfaces);
+    systemd.services =
+      let
+
+        deviceDependency = dev:
+          # Use systemd service if we manage device creation, else
+          # trust udev when not in a container
+          if (hasAttr dev (filterAttrs (k: v: v.virtual) cfg.interfaces)) ||
+             (hasAttr dev cfg.bridges) ||
+             (hasAttr dev cfg.bonds) ||
+             (hasAttr dev cfg.macvlans) ||
+             (hasAttr dev cfg.sits) ||
+             (hasAttr dev cfg.vlans) ||
+             (hasAttr dev cfg.vswitches)
+          then [ "${dev}-netdev.service" ]
+          else optional (dev != null && dev != "lo" && !config.boot.isContainer) (subsystemDevice dev);
+
+        hasDefaultGatewaySet = (cfg.defaultGateway != null && cfg.defaultGateway.address != "")
+                            || (cfg.enableIPv6 && cfg.defaultGateway6 != null && cfg.defaultGateway6.address != "");
+
+        networkLocalCommands = {
+          after = [ "network-setup.service" ];
+          bindsTo = [ "network-setup.service" ];
+        };
+
+        networkSetup =
+          { description = "Networking Setup";
+
+            after = [ "network-pre.target" "systemd-udevd.service" "systemd-sysctl.service" ];
+            before = [ "network.target" "shutdown.target" ];
+            wants = [ "network.target" ];
+            # exclude bridges from the partOf relationship to fix container networking bug #47210
+            partOf = map (i: "network-addresses-${i.name}.service") (filter (i: !(hasAttr i.name cfg.bridges)) interfaces);
+            conflicts = [ "shutdown.target" ];
+            wantedBy = [ "multi-user.target" ] ++ optional hasDefaultGatewaySet "network-online.target";
+
+            unitConfig.ConditionCapability = "CAP_NET_ADMIN";
+
+            path = [ pkgs.iproute2 ];
+
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+            };
+
+            unitConfig.DefaultDependencies = false;
+
+            script =
+              ''
+                ${optionalString config.networking.resolvconf.enable ''
+                  # Set the static DNS configuration, if given.
+                  ${pkgs.openresolv}/sbin/resolvconf -m 1 -a static <<EOF
+                  ${optionalString (cfg.nameservers != [] && cfg.domain != null) ''
+                    domain ${cfg.domain}
+                  ''}
+                  ${optionalString (cfg.search != []) ("search " + concatStringsSep " " cfg.search)}
+                  ${flip concatMapStrings cfg.nameservers (ns: ''
+                    nameserver ${ns}
+                  '')}
+                  EOF
+                ''}
+
+                # Set the default gateway.
+                ${optionalString (cfg.defaultGateway != null && cfg.defaultGateway.address != "") ''
+                  ${optionalString (cfg.defaultGateway.interface != null) ''
+                    ip route replace ${cfg.defaultGateway.address} dev ${cfg.defaultGateway.interface} ${optionalString (cfg.defaultGateway.metric != null)
+                      "metric ${toString cfg.defaultGateway.metric}"
+                    } proto static
+                  ''}
+                  ip route replace default ${optionalString (cfg.defaultGateway.metric != null)
+                      "metric ${toString cfg.defaultGateway.metric}"
+                    } via "${cfg.defaultGateway.address}" ${
+                    optionalString (cfg.defaultGatewayWindowSize != null)
+                      "window ${toString cfg.defaultGatewayWindowSize}"} ${
+                    optionalString (cfg.defaultGateway.interface != null)
+                      "dev ${cfg.defaultGateway.interface}"} proto static
+                ''}
+                ${optionalString (cfg.defaultGateway6 != null && cfg.defaultGateway6.address != "") ''
+                  ${optionalString (cfg.defaultGateway6.interface != null) ''
+                    ip -6 route replace ${cfg.defaultGateway6.address} dev ${cfg.defaultGateway6.interface} ${optionalString (cfg.defaultGateway6.metric != null)
+                      "metric ${toString cfg.defaultGateway6.metric}"
+                    } proto static
+                  ''}
+                  ip -6 route replace default ${optionalString (cfg.defaultGateway6.metric != null)
+                      "metric ${toString cfg.defaultGateway6.metric}"
+                    } via "${cfg.defaultGateway6.address}" ${
+                    optionalString (cfg.defaultGatewayWindowSize != null)
+                      "window ${toString cfg.defaultGatewayWindowSize}"} ${
+                    optionalString (cfg.defaultGateway6.interface != null)
+                      "dev ${cfg.defaultGateway6.interface}"} proto static
+                ''}
+              '';
+          };
+
+        # For each interface <foo>, create a job ‘network-addresses-<foo>.service"
+        # that performs static address configuration.  It has a "wants"
+        # dependency on ‘<foo>.service’, which is supposed to create
+        # the interface and need not exist (i.e. for hardware
+        # interfaces).  It has a binds-to dependency on the actual
+        # network device, so it only gets started after the interface
+        # has appeared, and it's stopped when the interface
+        # disappears.
+        configureAddrs = i:
+          let
+            ips = interfaceIps i;
+          in
+          nameValuePair "network-addresses-${i.name}"
+          { description = "Address configuration of ${i.name}";
+            wantedBy = [
+              "network-setup.service"
+              "network.target"
+            ];
+            # order before network-setup because the routes that are configured
+            # there may need ip addresses configured
+            before = [ "network-setup.service" ];
+            bindsTo = deviceDependency i.name;
+            after = [ "network-pre.target" ] ++ (deviceDependency i.name);
+            serviceConfig.Type = "oneshot";
+            serviceConfig.RemainAfterExit = true;
+            # Restart rather than stop+start this unit to prevent the
+            # network from dying during switch-to-configuration.
+            stopIfChanged = false;
+            path = [ pkgs.iproute2 ];
+            script =
+              ''
+                state="/run/nixos/network/addresses/${i.name}"
+                mkdir -p $(dirname "$state")
+
+                ip link set "${i.name}" up
+
+                ${flip concatMapStrings ips (ip:
+                  let
+                    cidr = "${ip.address}/${toString ip.prefixLength}";
+                  in
+                  ''
+                    echo "${cidr}" >> $state
+                    echo -n "adding address ${cidr}... "
+                    if out=$(ip addr add "${cidr}" dev "${i.name}" 2>&1); then
+                      echo "done"
+                    elif ! echo "$out" | grep "File exists" >/dev/null 2>&1; then
+                      echo "'ip addr add "${cidr}" dev "${i.name}"' failed: $out"
+                      exit 1
+                    fi
+                  ''
+                )}
+
+                state="/run/nixos/network/routes/${i.name}"
+                mkdir -p $(dirname "$state")
+
+                ${flip concatMapStrings (i.ipv4.routes ++ i.ipv6.routes) (route:
+                  let
+                    cidr = "${route.address}/${toString route.prefixLength}";
+                    via = optionalString (route.via != null) ''via "${route.via}"'';
+                    options = concatStrings (mapAttrsToList (name: val: "${name} ${val} ") route.options);
+                  in
+                  ''
+                     echo "${cidr}" >> $state
+                     echo -n "adding route ${cidr}... "
+                     if out=$(ip route add "${cidr}" ${options} ${via} dev "${i.name}" proto static 2>&1); then
+                       echo "done"
+                     elif ! echo "$out" | grep "File exists" >/dev/null 2>&1; then
+                       echo "'ip route add "${cidr}" ${options} ${via} dev "${i.name}"' failed: $out"
+                       exit 1
+                     fi
+                  ''
+                )}
+              '';
+            preStop = ''
+              state="/run/nixos/network/routes/${i.name}"
+              if [ -e "$state" ]; then
+                while read cidr; do
+                  echo -n "deleting route $cidr... "
+                  ip route del "$cidr" dev "${i.name}" >/dev/null 2>&1 && echo "done" || echo "failed"
+                done < "$state"
+                rm -f "$state"
+              fi
+
+              state="/run/nixos/network/addresses/${i.name}"
+              if [ -e "$state" ]; then
+                while read cidr; do
+                  echo -n "deleting address $cidr... "
+                  ip addr del "$cidr" dev "${i.name}" >/dev/null 2>&1 && echo "done" || echo "failed"
+                done < "$state"
+                rm -f "$state"
+              fi
+            '';
+          };
+
+        createTunDevice = i: nameValuePair "${i.name}-netdev"
+          { description = "Virtual Network Interface ${i.name}";
+            bindsTo = optional (!config.boot.isContainer) "dev-net-tun.device";
+            after = optional (!config.boot.isContainer) "dev-net-tun.device" ++ [ "network-pre.target" ];
+            wantedBy = [ "network-setup.service" (subsystemDevice i.name) ];
+            partOf = [ "network-setup.service" ];
+            before = [ "network-setup.service" ];
+            path = [ pkgs.iproute2 ];
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+            };
+            script = ''
+              ip tuntap add dev "${i.name}" mode "${i.virtualType}" user "${i.virtualOwner}"
+            '';
+            postStop = ''
+              ip link del ${i.name} || true
+            '';
+          };
+
+        createBridgeDevice = n: v: nameValuePair "${n}-netdev"
+          (let
+            deps = concatLists (map deviceDependency v.interfaces);
+          in
+          { description = "Bridge Interface ${n}";
+            wantedBy = [ "network-setup.service" (subsystemDevice n) ];
+            bindsTo = deps ++ optional v.rstp "mstpd.service";
+            partOf = [ "network-setup.service" ] ++ optional v.rstp "mstpd.service";
+            after = [ "network-pre.target" ] ++ deps ++ optional v.rstp "mstpd.service"
+              ++ map (i: "network-addresses-${i}.service") v.interfaces;
+            before = [ "network-setup.service" ];
+            serviceConfig.Type = "oneshot";
+            serviceConfig.RemainAfterExit = true;
+            path = [ pkgs.iproute2 ];
+            script = ''
+              # Remove Dead Interfaces
+              echo "Removing old bridge ${n}..."
+              ip link show "${n}" >/dev/null 2>&1 && ip link del "${n}"
+
+              echo "Adding bridge ${n}..."
+              ip link add name "${n}" type bridge
+
+              # Enslave child interfaces
+              ${flip concatMapStrings v.interfaces (i: ''
+                ip link set "${i}" master "${n}"
+                ip link set "${i}" up
+              '')}
+              # Save list of enslaved interfaces
+              echo "${flip concatMapStrings v.interfaces (i: ''
+                ${i}
+              '')}" > /run/${n}.interfaces
+
+              ${optionalString config.virtualisation.libvirtd.enable ''
+                  # Enslave dynamically added interfaces which may be lost on nixos-rebuild
+                  #
+                  # if `libvirtd.service` is not running, do not use `virsh` which would try activate it via 'libvirtd.socket' and thus start it out-of-order.
+                  # `libvirtd.service` will set up bridge interfaces when it will start normally.
+                  #
+                  if /run/current-system/systemd/bin/systemctl --quiet is-active 'libvirtd.service'; then
+                    for uri in qemu:///system lxc:///; do
+                      for dom in $(${pkgs.libvirt}/bin/virsh -c $uri list --name); do
+                        ${pkgs.libvirt}/bin/virsh -c $uri dumpxml "$dom" | \
+                        ${pkgs.xmlstarlet}/bin/xmlstarlet sel -t -m "//domain/devices/interface[@type='bridge'][source/@bridge='${n}'][target/@dev]" -v "concat('ip link set ',target/@dev,' master ',source/@bridge,';')" | \
+                        ${pkgs.bash}/bin/bash
+                      done
+                    done
+                  fi
+                ''}
+
+              # Enable stp on the interface
+              ${optionalString v.rstp ''
+                echo 2 >/sys/class/net/${n}/bridge/stp_state
+              ''}
+
+              ip link set "${n}" up
+            '';
+            postStop = ''
+              ip link set "${n}" down || true
+              ip link del "${n}" || true
+              rm -f /run/${n}.interfaces
+            '';
+            reload = ''
+              # Un-enslave child interfaces (old list of interfaces)
+              for interface in `cat /run/${n}.interfaces`; do
+                ip link set "$interface" nomaster up
+              done
+
+              # Enslave child interfaces (new list of interfaces)
+              ${flip concatMapStrings v.interfaces (i: ''
+                ip link set "${i}" master "${n}"
+                ip link set "${i}" up
+              '')}
+              # Save list of enslaved interfaces
+              echo "${flip concatMapStrings v.interfaces (i: ''
+                ${i}
+              '')}" > /run/${n}.interfaces
+
+              # (Un-)set stp on the bridge
+              echo ${if v.rstp then "2" else "0"} > /sys/class/net/${n}/bridge/stp_state
+            '';
+            reloadIfChanged = true;
+          });
+
+        createVswitchDevice = n: v: nameValuePair "${n}-netdev"
+          (let
+            deps = concatLists (map deviceDependency (attrNames (filterAttrs (_: config: config.type != "internal") v.interfaces)));
+            internalConfigs = map (i: "network-addresses-${i}.service") (attrNames (filterAttrs (_: config: config.type == "internal") v.interfaces));
+            ofRules = pkgs.writeText "vswitch-${n}-openFlowRules" v.openFlowRules;
+          in
+          { description = "Open vSwitch Interface ${n}";
+            wantedBy = [ "network-setup.service" (subsystemDevice n) ] ++ internalConfigs;
+            # before = [ "network-setup.service" ];
+            # should work without internalConfigs dependencies because address/link configuration depends
+            # on the device, which is created by ovs-vswitchd with type=internal, but it does not...
+            before = [ "network-setup.service" ] ++ internalConfigs;
+            partOf = [ "network-setup.service" ]; # shutdown the bridge when network is shutdown
+            bindsTo = [ "ovs-vswitchd.service" ]; # requires ovs-vswitchd to be alive at all times
+            after = [ "network-pre.target" "ovs-vswitchd.service" ] ++ deps; # start switch after physical interfaces and vswitch daemon
+            wants = deps; # if one or more interface fails, the switch should continue to run
+            serviceConfig.Type = "oneshot";
+            serviceConfig.RemainAfterExit = true;
+            path = [ pkgs.iproute2 config.virtualisation.vswitch.package ];
+            preStart = ''
+              echo "Resetting Open vSwitch ${n}..."
+              ovs-vsctl --if-exists del-br ${n} -- add-br ${n} \
+                        -- set bridge ${n} protocols=${concatStringsSep "," v.supportedOpenFlowVersions}
+            '';
+            script = ''
+              echo "Configuring Open vSwitch ${n}..."
+              ovs-vsctl ${concatStrings (mapAttrsToList (name: config: " -- add-port ${n} ${name}" + optionalString (config.vlan != null) " tag=${toString config.vlan}") v.interfaces)} \
+                ${concatStrings (mapAttrsToList (name: config: optionalString (config.type != null) " -- set interface ${name} type=${config.type}") v.interfaces)} \
+                ${concatMapStrings (x: " -- set-controller ${n} " + x)  v.controllers} \
+                ${concatMapStrings (x: " -- " + x) (splitString "\n" v.extraOvsctlCmds)}
+
+
+              echo "Adding OpenFlow rules for Open vSwitch ${n}..."
+              ovs-ofctl --protocols=${v.openFlowVersion} add-flows ${n} ${ofRules}
+            '';
+            postStop = ''
+              echo "Cleaning Open vSwitch ${n}"
+              echo "Shuting down internal ${n} interface"
+              ip link set ${n} down || true
+              echo "Deleting flows for ${n}"
+              ovs-ofctl --protocols=${v.openFlowVersion} del-flows ${n} || true
+              echo "Deleting Open vSwitch ${n}"
+              ovs-vsctl --if-exists del-br ${n} || true
+            '';
+          });
+
+        createBondDevice = n: v: nameValuePair "${n}-netdev"
+          (let
+            deps = concatLists (map deviceDependency v.interfaces);
+          in
+          { description = "Bond Interface ${n}";
+            wantedBy = [ "network-setup.service" (subsystemDevice n) ];
+            bindsTo = deps;
+            partOf = [ "network-setup.service" ];
+            after = [ "network-pre.target" ] ++ deps
+              ++ map (i: "network-addresses-${i}.service") v.interfaces;
+            before = [ "network-setup.service" ];
+            serviceConfig.Type = "oneshot";
+            serviceConfig.RemainAfterExit = true;
+            path = [ pkgs.iproute2 pkgs.gawk ];
+            script = ''
+              echo "Destroying old bond ${n}..."
+              ${destroyBond n}
+
+              echo "Creating new bond ${n}..."
+              ip link add name "${n}" type bond \
+              ${let opts = (mapAttrs (const toString)
+                             (bondDeprecation.filterDeprecated v))
+                           // v.driverOptions;
+                 in concatStringsSep "\n"
+                      (mapAttrsToList (set: val: "  ${set} ${val} \\") opts)}
+
+              # !!! There must be a better way to wait for the interface
+              while [ ! -d "/sys/class/net/${n}" ]; do sleep 0.1; done;
+
+              # Bring up the bond and enslave the specified interfaces
+              ip link set "${n}" up
+              ${flip concatMapStrings v.interfaces (i: ''
+                ip link set "${i}" down
+                ip link set "${i}" master "${n}"
+              '')}
+            '';
+            postStop = destroyBond n;
+          });
+
+        createMacvlanDevice = n: v: nameValuePair "${n}-netdev"
+          (let
+            deps = deviceDependency v.interface;
+          in
+          { description = "Vlan Interface ${n}";
+            wantedBy = [ "network-setup.service" (subsystemDevice n) ];
+            bindsTo = deps;
+            partOf = [ "network-setup.service" ];
+            after = [ "network-pre.target" ] ++ deps;
+            before = [ "network-setup.service" ];
+            serviceConfig.Type = "oneshot";
+            serviceConfig.RemainAfterExit = true;
+            path = [ pkgs.iproute2 ];
+            script = ''
+              # Remove Dead Interfaces
+              ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
+              ip link add link "${v.interface}" name "${n}" type macvlan \
+                ${optionalString (v.mode != null) "mode ${v.mode}"}
+              ip link set "${n}" up
+            '';
+            postStop = ''
+              ip link delete "${n}" || true
+            '';
+          });
+
+        createFouEncapsulation = n: v: nameValuePair "${n}-fou-encap"
+          (let
+            # if we have a device to bind to we can wait for its addresses to be
+            # configured, otherwise external sequencing is required.
+            deps = optionals (v.local != null && v.local.dev != null)
+              (deviceDependency v.local.dev ++ [ "network-addresses-${v.local.dev}.service" ]);
+            fouSpec = "port ${toString v.port} ${
+              if v.protocol != null then "ipproto ${toString v.protocol}" else "gue"
+            } ${
+              optionalString (v.local != null) "local ${escapeShellArg v.local.address} ${
+                optionalString (v.local.dev != null) "dev ${escapeShellArg v.local.dev}"
+              }"
+            }";
+          in
+          { description = "FOU endpoint ${n}";
+            wantedBy = [ "network-setup.service" (subsystemDevice n) ];
+            bindsTo = deps;
+            partOf = [ "network-setup.service" ];
+            after = [ "network-pre.target" ] ++ deps;
+            before = [ "network-setup.service" ];
+            serviceConfig.Type = "oneshot";
+            serviceConfig.RemainAfterExit = true;
+            path = [ pkgs.iproute2 ];
+            script = ''
+              # always remove previous incarnation since show can't filter
+              ip fou del ${fouSpec} >/dev/null 2>&1 || true
+              ip fou add ${fouSpec}
+            '';
+            postStop = ''
+              ip fou del ${fouSpec} || true
+            '';
+          });
+
+        createSitDevice = n: v: nameValuePair "${n}-netdev"
+          (let
+            deps = deviceDependency v.dev;
+          in
+          { description = "6-to-4 Tunnel Interface ${n}";
+            wantedBy = [ "network-setup.service" (subsystemDevice n) ];
+            bindsTo = deps;
+            partOf = [ "network-setup.service" ];
+            after = [ "network-pre.target" ] ++ deps;
+            before = [ "network-setup.service" ];
+            serviceConfig.Type = "oneshot";
+            serviceConfig.RemainAfterExit = true;
+            path = [ pkgs.iproute2 ];
+            script = ''
+              # Remove Dead Interfaces
+              ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
+              ip link add name "${n}" type sit \
+                ${optionalString (v.remote != null) "remote \"${v.remote}\""} \
+                ${optionalString (v.local != null) "local \"${v.local}\""} \
+                ${optionalString (v.ttl != null) "ttl ${toString v.ttl}"} \
+                ${optionalString (v.dev != null) "dev \"${v.dev}\""} \
+                ${optionalString (v.encapsulation != null)
+                  "encap ${v.encapsulation.type} encap-dport ${toString v.encapsulation.port} ${
+                    optionalString (v.encapsulation.sourcePort != null)
+                      "encap-sport ${toString v.encapsulation.sourcePort}"
+                  }"}
+              ip link set "${n}" up
+            '';
+            postStop = ''
+              ip link delete "${n}" || true
+            '';
+          });
+
+        createGreDevice = n: v: nameValuePair "${n}-netdev"
+          (let
+            deps = deviceDependency v.dev;
+          in
+          { description = "GRE Tunnel Interface ${n}";
+            wantedBy = [ "network-setup.service" (subsystemDevice n) ];
+            bindsTo = deps;
+            partOf = [ "network-setup.service" ];
+            after = [ "network-pre.target" ] ++ deps;
+            before = [ "network-setup.service" ];
+            serviceConfig.Type = "oneshot";
+            serviceConfig.RemainAfterExit = true;
+            path = [ pkgs.iproute2 ];
+            script = ''
+              # Remove Dead Interfaces
+              ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
+              ip link add name "${n}" type ${v.type} \
+                ${optionalString (v.remote != null) "remote \"${v.remote}\""} \
+                ${optionalString (v.local != null) "local \"${v.local}\""} \
+                ${optionalString (v.dev != null) "dev \"${v.dev}\""}
+              ip link set "${n}" up
+            '';
+            postStop = ''
+              ip link delete "${n}" || true
+            '';
+          });
+
+        createVlanDevice = n: v: nameValuePair "${n}-netdev"
+          (let
+            deps = deviceDependency v.interface;
+          in
+          { description = "Vlan Interface ${n}";
+            wantedBy = [ "network-setup.service" (subsystemDevice n) ];
+            bindsTo = deps;
+            partOf = [ "network-setup.service" ];
+            after = [ "network-pre.target" ] ++ deps;
+            before = [ "network-setup.service" ];
+            serviceConfig.Type = "oneshot";
+            serviceConfig.RemainAfterExit = true;
+            path = [ pkgs.iproute2 ];
+            script = ''
+              # Remove Dead Interfaces
+              ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
+              ip link add link "${v.interface}" name "${n}" type vlan id "${toString v.id}"
+
+              # We try to bring up the logical VLAN interface. If the master
+              # interface the logical interface is dependent upon is not up yet we will
+              # fail to immediately bring up the logical interface. The resulting logical
+              # interface will brought up later when the master interface is up.
+              ip link set "${n}" up || true
+            '';
+            postStop = ''
+              ip link delete "${n}" || true
+            '';
+          });
+
+      in listToAttrs (
+           map configureAddrs interfaces ++
+           map createTunDevice (filter (i: i.virtual) interfaces))
+         // mapAttrs' createBridgeDevice cfg.bridges
+         // mapAttrs' createVswitchDevice cfg.vswitches
+         // mapAttrs' createBondDevice cfg.bonds
+         // mapAttrs' createMacvlanDevice cfg.macvlans
+         // mapAttrs' createFouEncapsulation cfg.fooOverUDP
+         // mapAttrs' createSitDevice cfg.sits
+         // mapAttrs' createGreDevice cfg.greTunnels
+         // mapAttrs' createVlanDevice cfg.vlans
+         // {
+           network-setup = networkSetup;
+           network-local-commands = networkLocalCommands;
+         };
+
+    services.udev.extraRules =
+      ''
+        KERNEL=="tun", TAG+="systemd"
+      '';
+
+
+  };
+
+in
+
+{
+  config = mkMerge [
+    bondWarnings
+    (mkIf (!cfg.useNetworkd) normalConfig)
+    { # Ensure slave interfaces are brought up
+      networking.interfaces = genAttrs slaves (i: {});
+    }
+  ];
+}
diff --git a/nixos/modules/tasks/network-interfaces-systemd.nix b/nixos/modules/tasks/network-interfaces-systemd.nix
new file mode 100644
index 00000000000..8a5e1b5af11
--- /dev/null
+++ b/nixos/modules/tasks/network-interfaces-systemd.nix
@@ -0,0 +1,406 @@
+{ config, lib, utils, pkgs, ... }:
+
+with utils;
+with lib;
+
+let
+
+  cfg = config.networking;
+  interfaces = attrValues cfg.interfaces;
+
+  interfaceIps = i:
+    i.ipv4.addresses
+    ++ optionals cfg.enableIPv6 i.ipv6.addresses;
+
+  interfaceRoutes = i:
+    i.ipv4.routes
+    ++ optionals cfg.enableIPv6 i.ipv6.routes;
+
+  dhcpStr = useDHCP: if useDHCP == true || useDHCP == null then "yes" else "no";
+
+  slaves =
+    concatLists (map (bond: bond.interfaces) (attrValues cfg.bonds))
+    ++ concatLists (map (bridge: bridge.interfaces) (attrValues cfg.bridges))
+    ++ map (sit: sit.dev) (attrValues cfg.sits)
+    ++ map (gre: gre.dev) (attrValues cfg.greTunnels)
+    ++ map (vlan: vlan.interface) (attrValues cfg.vlans)
+    # add dependency to physical or independently created vswitch member interface
+    # 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);
+
+in
+
+{
+
+  config = mkIf cfg.useNetworkd {
+
+    assertions = [ {
+      assertion = cfg.defaultGatewayWindowSize == null;
+      message = "networking.defaultGatewayWindowSize is not supported by networkd.";
+    } {
+      assertion = cfg.defaultGateway == null || cfg.defaultGateway.interface == null;
+      message = "networking.defaultGateway.interface is not supported by networkd.";
+    } {
+      assertion = cfg.defaultGateway6 == null || cfg.defaultGateway6.interface == null;
+      message = "networking.defaultGateway6.interface is not supported by networkd.";
+    } {
+      assertion = cfg.useDHCP == false;
+      message = ''
+        networking.useDHCP is not supported by networkd.
+        Please use per interface configuration and set the global option to false.
+      '';
+    } ] ++ flip mapAttrsToList cfg.bridges (n: { rstp, ... }: {
+      assertion = !rstp;
+      message = "networking.bridges.${n}.rstp is not supported by networkd.";
+    }) ++ flip mapAttrsToList cfg.fooOverUDP (n: { local, ... }: {
+      assertion = local == null;
+      message = "networking.fooOverUDP.${n}.local is not supported by networkd.";
+    });
+
+    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;
+          in optionalAttrs (gateway != [ ]) {
+            routes = override [
+              {
+                routeConfig = {
+                  Gateway = gateway;
+                  GatewayOnLink = false;
+                };
+              }
+            ];
+          } // optionalAttrs (domains != [ ]) {
+            domains = override domains;
+          };
+      in mkMerge [ {
+        enable = true;
+      }
+      (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.prefixLength > 0) {
+                  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.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;
+          };
+        }];
+      })))
+      (mkMerge (flip mapAttrsToList cfg.bridges (name: bridge: {
+        netdevs."40-${name}" = {
+          netdevConfig = {
+            Name = name;
+            Kind = "bridge";
+          };
+        };
+        networks = listToAttrs (forEach bridge.interfaces (bi:
+          nameValuePair "40-${bi}" (mkMerge [ (genericNetwork (mkOverride 999)) {
+            DHCP = mkOverride 0 (dhcpStr false);
+            networkConfig.Bridge = name;
+          } ])));
+      })))
+      (mkMerge (flip mapAttrsToList cfg.bonds (name: bond: {
+        netdevs."40-${name}" = {
+          netdevConfig = {
+            Name = name;
+            Kind = "bond";
+          };
+          bondConfig = let
+            # manual mapping as of 2017-02-03
+            # man 5 systemd.netdev [BOND]
+            # to https://www.kernel.org/doc/Documentation/networking/bonding.txt
+            # driver options.
+            driverOptionMapping = let
+              trans = f: optName: { valTransform = f; optNames = [optName]; };
+              simp  = trans id;
+              ms    = trans (v: v + "ms");
+              in {
+                Mode                       = simp "mode";
+                TransmitHashPolicy         = simp "xmit_hash_policy";
+                LACPTransmitRate           = simp "lacp_rate";
+                MIIMonitorSec              = ms "miimon";
+                UpDelaySec                 = ms "updelay";
+                DownDelaySec               = ms "downdelay";
+                LearnPacketIntervalSec     = simp "lp_interval";
+                AdSelect                   = simp "ad_select";
+                FailOverMACPolicy          = simp "fail_over_mac";
+                ARPValidate                = simp "arp_validate";
+                # apparently in ms for this value?! Upstream bug?
+                ARPIntervalSec             = simp "arp_interval";
+                ARPIPTargets               = simp "arp_ip_target";
+                ARPAllTargets              = simp "arp_all_targets";
+                PrimaryReselectPolicy      = simp "primary_reselect";
+                ResendIGMP                 = simp "resend_igmp";
+                PacketsPerSlave            = simp "packets_per_slave";
+                GratuitousARP = { valTransform = id;
+                                  optNames = [ "num_grat_arp" "num_unsol_na" ]; };
+                AllSlavesActive            = simp "all_slaves_active";
+                MinLinks                   = simp "min_links";
+              };
+
+            do = bond.driverOptions;
+            assertNoUnknownOption = let
+              knownOptions = flatten (mapAttrsToList (_: kOpts: kOpts.optNames)
+                                                     driverOptionMapping);
+              # options that apparently don’t exist in the networkd config
+              unknownOptions = [ "primary" ];
+              assertTrace = bool: msg: if bool then true else builtins.trace msg false;
+              in assert all (driverOpt: assertTrace
+                               (elem driverOpt (knownOptions ++ unknownOptions))
+                               "The bond.driverOption `${driverOpt}` cannot be mapped to the list of known networkd bond options. Please add it to the mapping above the assert or to `unknownOptions` should it not exist in networkd.")
+                            (mapAttrsToList (k: _: k) do); "";
+            # get those driverOptions that have been set
+            filterSystemdOptions = filterAttrs (sysDOpt: kOpts:
+                                     any (kOpt: do ? ${kOpt}) kOpts.optNames);
+            # build final set of systemd options to bond values
+            buildOptionSet = mapAttrs (_: kOpts: with kOpts;
+                               # we simply take the first set kernel bond option
+                               # (one option has multiple names, which is silly)
+                               head (map (optN: valTransform (do.${optN}))
+                                 # only map those that exist
+                                 (filter (o: do ? ${o}) optNames)));
+            in seq assertNoUnknownOption
+                   (buildOptionSet (filterSystemdOptions driverOptionMapping));
+
+        };
+
+        networks = listToAttrs (forEach bond.interfaces (bi:
+          nameValuePair "40-${bi}" (mkMerge [ (genericNetwork (mkOverride 999)) {
+            DHCP = mkOverride 0 (dhcpStr false);
+            networkConfig.Bond = name;
+          } ])));
+      })))
+      (mkMerge (flip mapAttrsToList cfg.macvlans (name: macvlan: {
+        netdevs."40-${name}" = {
+          netdevConfig = {
+            Name = name;
+            Kind = "macvlan";
+          };
+          macvlanConfig = optionalAttrs (macvlan.mode != null) { Mode = macvlan.mode; };
+        };
+        networks."40-${macvlan.interface}" = (mkMerge [ (genericNetwork (mkOverride 999)) {
+          macvlan = [ name ];
+        } ]);
+      })))
+      (mkMerge (flip mapAttrsToList cfg.fooOverUDP (name: fou: {
+        netdevs."40-${name}" = {
+          netdevConfig = {
+            Name = name;
+            Kind = "fou";
+          };
+          # unfortunately networkd cannot encode dependencies of netdevs on addresses/routes,
+          # so we cannot specify Local=, Peer=, PeerPort=. this looks like a missing feature
+          # in networkd.
+          fooOverUDPConfig = {
+            Port = fou.port;
+            Encapsulation = if fou.protocol != null then "FooOverUDP" else "GenericUDPEncapsulation";
+          } // (optionalAttrs (fou.protocol != null) {
+            Protocol = fou.protocol;
+          });
+        };
+      })))
+      (mkMerge (flip mapAttrsToList cfg.sits (name: sit: {
+        netdevs."40-${name}" = {
+          netdevConfig = {
+            Name = name;
+            Kind = "sit";
+          };
+          tunnelConfig =
+            (optionalAttrs (sit.remote != null) {
+              Remote = sit.remote;
+            }) // (optionalAttrs (sit.local != null) {
+              Local = sit.local;
+            }) // (optionalAttrs (sit.ttl != null) {
+              TTL = sit.ttl;
+            }) // (optionalAttrs (sit.encapsulation != null) (
+              {
+                FooOverUDP = true;
+                Encapsulation =
+                  if sit.encapsulation.type == "fou"
+                  then "FooOverUDP"
+                  else "GenericUDPEncapsulation";
+                FOUDestinationPort = sit.encapsulation.port;
+              } // (optionalAttrs (sit.encapsulation.sourcePort != null) {
+                FOUSourcePort = sit.encapsulation.sourcePort;
+              })));
+        };
+        networks = mkIf (sit.dev != null) {
+          "40-${sit.dev}" = (mkMerge [ (genericNetwork (mkOverride 999)) {
+            tunnel = [ name ];
+          } ]);
+        };
+      })))
+      (mkMerge (flip mapAttrsToList cfg.greTunnels (name: gre: {
+        netdevs."40-${name}" = {
+          netdevConfig = {
+            Name = name;
+            Kind = gre.type;
+          };
+          tunnelConfig =
+            (optionalAttrs (gre.remote != null) {
+              Remote = gre.remote;
+            }) // (optionalAttrs (gre.local != null) {
+              Local = gre.local;
+            });
+        };
+        networks = mkIf (gre.dev != null) {
+          "40-${gre.dev}" = (mkMerge [ (genericNetwork (mkOverride 999)) {
+            tunnel = [ name ];
+          } ]);
+        };
+      })))
+      (mkMerge (flip mapAttrsToList cfg.vlans (name: vlan: {
+        netdevs."40-${name}" = {
+          netdevConfig = {
+            Name = name;
+            Kind = "vlan";
+          };
+          vlanConfig.Id = vlan.id;
+        };
+        networks."40-${vlan.interface}" = (mkMerge [ (genericNetwork (mkOverride 999)) {
+          vlan = [ name ];
+        } ]);
+      })))
+    ];
+
+    # We need to prefill the slaved devices with networking options
+    # This forces the network interface creator to initialize slaves.
+    networking.interfaces = listToAttrs (map (i: nameValuePair i { }) slaves);
+
+    systemd.services = let
+      # We must escape interfaces due to the systemd interpretation
+      subsystemDevice = interface:
+        "sys-subsystem-net-devices-${escapeSystemdPath interface}.device";
+      # support for creating openvswitch switches
+      createVswitchDevice = n: v: nameValuePair "${n}-netdev"
+          (let
+            deps = map subsystemDevice (attrNames (filterAttrs (_: config: config.type != "internal") v.interfaces));
+            ofRules = pkgs.writeText "vswitch-${n}-openFlowRules" v.openFlowRules;
+          in
+          { description = "Open vSwitch Interface ${n}";
+            wantedBy = [ "network.target" (subsystemDevice n) ];
+            # and create bridge before systemd-networkd starts because it might create internal interfaces
+            before = [ "systemd-networkd.service" ];
+            # shutdown the bridge when network is shutdown
+            partOf = [ "network.target" ];
+            # requires ovs-vswitchd to be alive at all times
+            bindsTo = [ "ovs-vswitchd.service" ];
+            # start switch after physical interfaces and vswitch daemon
+            after = [ "network-pre.target" "ovs-vswitchd.service" ] ++ deps;
+            wants = deps; # if one or more interface fails, the switch should continue to run
+            serviceConfig.Type = "oneshot";
+            serviceConfig.RemainAfterExit = true;
+            path = [ pkgs.iproute2 config.virtualisation.vswitch.package ];
+            preStart = ''
+              echo "Resetting Open vSwitch ${n}..."
+              ovs-vsctl --if-exists del-br ${n} -- add-br ${n} \
+                        -- set bridge ${n} protocols=${concatStringsSep "," v.supportedOpenFlowVersions}
+            '';
+            script = ''
+              echo "Configuring Open vSwitch ${n}..."
+              ovs-vsctl ${concatStrings (mapAttrsToList (name: config: " -- add-port ${n} ${name}" + optionalString (config.vlan != null) " tag=${toString config.vlan}") v.interfaces)} \
+                ${concatStrings (mapAttrsToList (name: config: optionalString (config.type != null) " -- set interface ${name} type=${config.type}") v.interfaces)} \
+                ${concatMapStrings (x: " -- set-controller ${n} " + x)  v.controllers} \
+                ${concatMapStrings (x: " -- " + x) (splitString "\n" v.extraOvsctlCmds)}
+
+
+              echo "Adding OpenFlow rules for Open vSwitch ${n}..."
+              ovs-ofctl --protocols=${v.openFlowVersion} add-flows ${n} ${ofRules}
+            '';
+            postStop = ''
+              echo "Cleaning Open vSwitch ${n}"
+              echo "Shuting down internal ${n} interface"
+              ip link set ${n} down || true
+              echo "Deleting flows for ${n}"
+              ovs-ofctl --protocols=${v.openFlowVersion} del-flows ${n} || true
+              echo "Deleting Open vSwitch ${n}"
+              ovs-vsctl --if-exists del-br ${n} || true
+            '';
+          });
+    in mapAttrs' createVswitchDevice cfg.vswitches
+      // {
+            "network-local-commands" = {
+              after = [ "systemd-networkd.service" ];
+              bindsTo = [ "systemd-networkd.service" ];
+          };
+      };
+  };
+
+}
diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix
new file mode 100644
index 00000000000..01980b80f1c
--- /dev/null
+++ b/nixos/modules/tasks/network-interfaces.nix
@@ -0,0 +1,1537 @@
+{ config, options, lib, pkgs, utils, ... }:
+
+with lib;
+with utils;
+
+let
+
+  cfg = config.networking;
+  opt = options.networking;
+  interfaces = attrValues cfg.interfaces;
+  hasVirtuals = any (i: i.virtual) interfaces;
+  hasSits = cfg.sits != { };
+  hasGres = cfg.greTunnels != { };
+  hasBonds = cfg.bonds != { };
+  hasFous = cfg.fooOverUDP != { }
+    || filterAttrs (_: s: s.encapsulation != null) cfg.sits != { };
+
+  slaves = concatMap (i: i.interfaces) (attrValues cfg.bonds)
+    ++ concatMap (i: i.interfaces) (attrValues cfg.bridges)
+    ++ concatMap (i: attrNames (filterAttrs (name: config: ! (config.type == "internal" || hasAttr name cfg.interfaces)) i.interfaces)) (attrValues cfg.vswitches);
+
+  slaveIfs = map (i: cfg.interfaces.${i}) (filter (i: cfg.interfaces ? ${i}) slaves);
+
+  rstpBridges = flip filterAttrs cfg.bridges (_: { rstp, ... }: rstp);
+
+  needsMstpd = rstpBridges != { };
+
+  bridgeStp = optional needsMstpd (pkgs.writeTextFile {
+    name = "bridge-stp";
+    executable = true;
+    destination = "/bin/bridge-stp";
+    text = ''
+      #!${pkgs.runtimeShell} -e
+      export PATH="${pkgs.mstpd}/bin"
+
+      BRIDGES=(${concatStringsSep " " (attrNames rstpBridges)})
+      for BRIDGE in $BRIDGES; do
+        if [ "$BRIDGE" = "$1" ]; then
+          if [ "$2" = "start" ]; then
+            mstpctl addbridge "$BRIDGE"
+            exit 0
+          elif [ "$2" = "stop" ]; then
+            mstpctl delbridge "$BRIDGE"
+            exit 0
+          fi
+          exit 1
+        fi
+      done
+      exit 1
+    '';
+  });
+
+  # We must escape interfaces due to the systemd interpretation
+  subsystemDevice = interface:
+    "sys-subsystem-net-devices-${escapeSystemdPath interface}.device";
+
+  addrOpts = v:
+    assert v == 4 || v == 6;
+    { options = {
+        address = mkOption {
+          type = types.str;
+          description = ''
+            IPv${toString v} address of the interface. Leave empty to configure the
+            interface using DHCP.
+          '';
+        };
+
+        prefixLength = mkOption {
+          type = types.addCheck types.int (n: n >= 0 && n <= (if v == 4 then 32 else 128));
+          description = ''
+            Subnet mask of the interface, specified as the number of
+            bits in the prefix (<literal>${if v == 4 then "24" else "64"}</literal>).
+          '';
+        };
+      };
+    };
+
+  routeOpts = v:
+  { options = {
+      address = mkOption {
+        type = types.str;
+        description = "IPv${toString v} address of the network.";
+      };
+
+      prefixLength = mkOption {
+        type = types.addCheck types.int (n: n >= 0 && n <= (if v == 4 then 32 else 128));
+        description = ''
+          Subnet mask of the network, specified as the number of
+          bits in the prefix (<literal>${if v == 4 then "24" else "64"}</literal>).
+        '';
+      };
+
+      via = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "IPv${toString v} address of the next hop.";
+      };
+
+      options = mkOption {
+        type = types.attrsOf types.str;
+        default = { };
+        example = { mtu = "1492"; window = "524288"; };
+        description = ''
+          Other route options. See the symbol <literal>OPTIONS</literal>
+          in the <literal>ip-route(8)</literal> manual page for the details.
+          You may also specify <literal>metric</literal>,
+          <literal>src</literal>, <literal>protocol</literal>,
+          <literal>scope</literal>, <literal>from</literal>
+          and <literal>table</literal>, which are technically
+          not route options, in the sense used in the manual.
+        '';
+      };
+
+    };
+  };
+
+  gatewayCoerce = address: { inherit address; };
+
+  gatewayOpts = { ... }: {
+
+    options = {
+
+      address = mkOption {
+        type = types.str;
+        description = "The default gateway address.";
+      };
+
+      interface = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "enp0s3";
+        description = "The default gateway interface.";
+      };
+
+      metric = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        example = 42;
+        description = "The default gateway metric/preference.";
+      };
+
+    };
+
+  };
+
+  interfaceOpts = { name, ... }: {
+
+    options = {
+      name = mkOption {
+        example = "eth0";
+        type = types.str;
+        description = "Name of the interface.";
+      };
+
+      tempAddress = mkOption {
+        type = types.enum (lib.attrNames tempaddrValues);
+        default = cfg.tempAddresses;
+        defaultText = literalExpression ''config.networking.tempAddresses'';
+        description = ''
+          When IPv6 is enabled with SLAAC, this option controls the use of
+          temporary address (aka privacy extensions) on this
+          interface. This is used to reduce tracking.
+
+          See also the global option
+          <xref linkend="opt-networking.tempAddresses"/>, which
+          applies to all interfaces where this is not set.
+
+          Possible values are:
+          ${tempaddrDoc}
+        '';
+      };
+
+      useDHCP = mkOption {
+        type = types.nullOr types.bool;
+        default = null;
+        description = ''
+          Whether this interface should be configured with dhcp.
+          Null implies the old behavior which depends on whether ip addresses
+          are specified or not.
+        '';
+      };
+
+      ipv4.addresses = mkOption {
+        default = [ ];
+        example = [
+          { address = "10.0.0.1"; prefixLength = 16; }
+          { address = "192.168.1.1"; prefixLength = 24; }
+        ];
+        type = with types; listOf (submodule (addrOpts 4));
+        description = ''
+          List of IPv4 addresses that will be statically assigned to the interface.
+        '';
+      };
+
+      ipv6.addresses = mkOption {
+        default = [ ];
+        example = [
+          { address = "fdfd:b3f0:482::1"; prefixLength = 48; }
+          { address = "2001:1470:fffd:2098::e006"; prefixLength = 64; }
+        ];
+        type = with types; listOf (submodule (addrOpts 6));
+        description = ''
+          List of IPv6 addresses that will be statically assigned to the interface.
+        '';
+      };
+
+      ipv4.routes = mkOption {
+        default = [];
+        example = [
+          { address = "10.0.0.0"; prefixLength = 16; }
+          { address = "192.168.2.0"; prefixLength = 24; via = "192.168.1.1"; }
+        ];
+        type = with types; listOf (submodule (routeOpts 4));
+        description = ''
+          List of extra IPv4 static routes that will be assigned to the interface.
+          <warning><para>If the route type is the default <literal>unicast</literal>, then the scope
+          is set differently depending on the value of <option>networking.useNetworkd</option>:
+          the script-based backend sets it to <literal>link</literal>, while networkd sets
+          it to <literal>global</literal>.</para></warning>
+          If you want consistency between the two implementations,
+          set the scope of the route manually with
+          <literal>networking.interfaces.eth0.ipv4.routes = [{ options.scope = "global"; }]</literal>
+          for example.
+        '';
+      };
+
+      ipv6.routes = mkOption {
+        default = [];
+        example = [
+          { address = "fdfd:b3f0::"; prefixLength = 48; }
+          { address = "2001:1470:fffd:2098::"; prefixLength = 64; via = "fdfd:b3f0::1"; }
+        ];
+        type = with types; listOf (submodule (routeOpts 6));
+        description = ''
+          List of extra IPv6 static routes that will be assigned to the interface.
+        '';
+      };
+
+      macAddress = mkOption {
+        default = null;
+        example = "00:11:22:33:44:55";
+        type = types.nullOr (types.str);
+        description = ''
+          MAC address of the interface. Leave empty to use the default.
+        '';
+      };
+
+      mtu = mkOption {
+        default = null;
+        example = 9000;
+        type = types.nullOr types.int;
+        description = ''
+          MTU size for packets leaving the interface. Leave empty to use the default.
+        '';
+      };
+
+      virtual = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether this interface is virtual and should be created by tunctl.
+          This is mainly useful for creating bridges between a host and a virtual
+          network such as VPN or a virtual machine.
+        '';
+      };
+
+      virtualOwner = mkOption {
+        default = "root";
+        type = types.str;
+        description = ''
+          In case of a virtual device, the user who owns it.
+        '';
+      };
+
+      virtualType = mkOption {
+        default = if hasPrefix "tun" name then "tun" else "tap";
+        defaultText = literalExpression ''if hasPrefix "tun" name then "tun" else "tap"'';
+        type = with types; enum [ "tun" "tap" ];
+        description = ''
+          The type of interface to create.
+          The default is TUN for an interface name starting
+          with "tun", otherwise TAP.
+        '';
+      };
+
+      proxyARP = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Turn on proxy_arp for this device.
+          This is mainly useful for creating pseudo-bridges between a real
+          interface and a virtual network such as VPN or a virtual machine for
+          interfaces that don't support real bridging (most wlan interfaces).
+          As ARP proxying acts slightly above the link-layer, below-ip traffic
+          isn't bridged, so things like DHCP won't work. The advantage above
+          using NAT lies in the fact that no IP addresses are shared, so all
+          hosts are reachable/routeable.
+
+          WARNING: turns on ip-routing, so if you have multiple interfaces, you
+          should think of the consequence and setup firewall rules to limit this.
+        '';
+      };
+
+      wakeOnLan = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Whether to enable wol on this interface.";
+        };
+      };
+    };
+
+    config = {
+      name = mkDefault name;
+    };
+
+    # Renamed or removed options
+    imports =
+      let
+        defined = x: x != "_mkMergedOptionModule";
+      in [
+        (mkChangedOptionModule [ "preferTempAddress" ] [ "tempAddress" ]
+         (config:
+          let bool = getAttrFromPath [ "preferTempAddress" ] config;
+          in if bool then "default" else "enabled"
+        ))
+        (mkRenamedOptionModule [ "ip4" ] [ "ipv4" "addresses"])
+        (mkRenamedOptionModule [ "ip6" ] [ "ipv6" "addresses"])
+        (mkRemovedOptionModule [ "subnetMask" ] ''
+          Supply a prefix length instead; use option
+          networking.interfaces.<name>.ipv{4,6}.addresses'')
+        (mkMergedOptionModule
+          [ [ "ipAddress" ] [ "prefixLength" ] ]
+          [ "ipv4" "addresses" ]
+          (cfg: with cfg;
+            optional (defined ipAddress && defined prefixLength)
+            { address = ipAddress; prefixLength = prefixLength; }))
+        (mkMergedOptionModule
+          [ [ "ipv6Address" ] [ "ipv6PrefixLength" ] ]
+          [ "ipv6" "addresses" ]
+          (cfg: with cfg;
+            optional (defined ipv6Address && defined ipv6PrefixLength)
+            { address = ipv6Address; prefixLength = ipv6PrefixLength; }))
+
+        ({ options.warnings = options.warnings; options.assertions = options.assertions; })
+      ];
+
+  };
+
+  vswitchInterfaceOpts = {name, ...}: {
+
+    options = {
+
+      name = mkOption {
+        description = "Name of the interface";
+        example = "eth0";
+        type = types.str;
+      };
+
+      vlan = mkOption {
+        description = "Vlan tag to apply to interface";
+        example = 10;
+        type = types.nullOr types.int;
+        default = null;
+      };
+
+      type = mkOption {
+        description = "Openvswitch type to assign to interface";
+        example = "internal";
+        type = types.nullOr types.str;
+        default = null;
+      };
+    };
+  };
+
+  hexChars = stringToCharacters "0123456789abcdef";
+
+  isHexString = s: all (c: elem c hexChars) (stringToCharacters (toLower s));
+
+  tempaddrValues = {
+    disabled = {
+      sysctl = "0";
+      description = "completely disable IPv6 temporary addresses";
+    };
+    enabled = {
+      sysctl = "1";
+      description = "generate IPv6 temporary addresses but still use EUI-64 addresses as source addresses";
+    };
+    default = {
+      sysctl = "2";
+      description = "generate IPv6 temporary addresses and use these as source addresses in routing";
+    };
+  };
+  tempaddrDoc = ''
+    <itemizedlist>
+     ${concatStringsSep "\n" (mapAttrsToList (name: { description, ... }: ''
+       <listitem>
+         <para>
+           <literal>"${name}"</literal> to ${description};
+         </para>
+       </listitem>
+     '') tempaddrValues)}
+    </itemizedlist>
+  '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    networking.hostName = mkOption {
+      default = "nixos";
+      # Only allow hostnames without the domain name part (i.e. no FQDNs, see
+      # e.g. "man 5 hostname") and require valid DNS labels (recommended
+      # syntax). Note: We also allow underscores for compatibility/legacy
+      # reasons (as undocumented feature):
+      type = types.strMatching
+        "^$|^[[:alnum:]]([[:alnum:]_-]{0,61}[[:alnum:]])?$";
+      description = ''
+        The name of the machine. Leave it empty if you want to obtain it from a
+        DHCP server (if using DHCP). The hostname must be a valid DNS label (see
+        RFC 1035 section 2.3.1: "Preferred name syntax", RFC 1123 section 2.1:
+        "Host Names and Numbers") 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 (uname --nodename) the option
+        boot.kernel.sysctl."kernel.hostname" can be used as a workaround (but
+        the 64 character limit still applies).
+
+        WARNING: Do not use underscores (_) or you may run into unexpected issues.
+      '';
+       # warning until the issues in https://github.com/NixOS/nixpkgs/pull/138978
+       # are resolved
+    };
+
+    networking.fqdn = mkOption {
+      readOnly = true;
+      type = types.str;
+      default = if (cfg.hostName != "" && cfg.domain != null)
+        then "${cfg.hostName}.${cfg.domain}"
+        else throw ''
+          The FQDN is required but cannot be determined. Please make sure that
+          both networking.hostName and networking.domain are set properly.
+        '';
+      defaultText = literalExpression ''"''${networking.hostName}.''${networking.domain}"'';
+      description = ''
+        The fully qualified domain name (FQDN) of this host. It is the result
+        of combining networking.hostName and networking.domain. Using this
+        option will result in an evaluation error if the hostname is empty or
+        no domain is specified.
+      '';
+    };
+
+    networking.hostId = mkOption {
+      default = null;
+      example = "4e98920d";
+      type = types.nullOr types.str;
+      description = ''
+        The 32-bit host ID of the machine, formatted as 8 hexadecimal characters.
+
+        You should try to make this ID unique among your machines. You can
+        generate a random 32-bit ID using the following commands:
+
+        <literal>head -c 8 /etc/machine-id</literal>
+
+        (this derives it from the machine-id that systemd generates) or
+
+        <literal>head -c4 /dev/urandom | od -A none -t x4</literal>
+
+        The primary use case is to ensure when using ZFS that a pool isn't imported
+        accidentally on a wrong machine.
+      '';
+    };
+
+    networking.enableIPv6 = mkOption {
+      default = true;
+      type = types.bool;
+      description = ''
+        Whether to enable support for IPv6.
+      '';
+    };
+
+    networking.defaultGateway = mkOption {
+      default = null;
+      example = {
+        address = "131.211.84.1";
+        interface = "enp3s0";
+      };
+      type = types.nullOr (types.coercedTo types.str gatewayCoerce (types.submodule gatewayOpts));
+      description = ''
+        The default gateway. It can be left empty if it is auto-detected through DHCP.
+        It can be specified as a string or an option set along with a network interface.
+      '';
+    };
+
+    networking.defaultGateway6 = mkOption {
+      default = null;
+      example = {
+        address = "2001:4d0:1e04:895::1";
+        interface = "enp3s0";
+      };
+      type = types.nullOr (types.coercedTo types.str gatewayCoerce (types.submodule gatewayOpts));
+      description = ''
+        The default ipv6 gateway. It can be left empty if it is auto-detected through DHCP.
+        It can be specified as a string or an option set along with a network interface.
+      '';
+    };
+
+    networking.defaultGatewayWindowSize = mkOption {
+      default = null;
+      example = 524288;
+      type = types.nullOr types.int;
+      description = ''
+        The window size of the default gateway. It limits maximal data bursts that TCP peers
+        are allowed to send to us.
+      '';
+    };
+
+    networking.nameservers = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = ["130.161.158.4" "130.161.33.17"];
+      description = ''
+        The list of nameservers.  It can be left empty if it is auto-detected through DHCP.
+      '';
+    };
+
+    networking.search = mkOption {
+      default = [];
+      example = [ "example.com" "home.arpa" ];
+      type = types.listOf types.str;
+      description = ''
+        The list of search paths used when resolving domain names.
+      '';
+    };
+
+    networking.domain = mkOption {
+      default = null;
+      example = "home.arpa";
+      type = types.nullOr types.str;
+      description = ''
+        The domain.  It can be left empty if it is auto-detected through DHCP.
+      '';
+    };
+
+    networking.useHostResolvConf = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        In containers, whether to use the
+        <filename>resolv.conf</filename> supplied by the host.
+      '';
+    };
+
+    networking.localCommands = mkOption {
+      type = types.lines;
+      default = "";
+      example = "text=anything; echo You can put $text here.";
+      description = ''
+        Shell commands to be executed at the end of the
+        <literal>network-setup</literal> systemd service.  Note that if
+        you are using DHCP to obtain the network configuration,
+        interfaces may not be fully configured yet.
+      '';
+    };
+
+    networking.interfaces = mkOption {
+      default = {};
+      example =
+        { eth0.ipv4.addresses = [ {
+            address = "131.211.84.78";
+            prefixLength = 25;
+          } ];
+        };
+      description = ''
+        The configuration for each network interface.  If
+        <option>networking.useDHCP</option> is true, then every
+        interface not listed here will be configured using DHCP.
+      '';
+      type = with types; attrsOf (submodule interfaceOpts);
+    };
+
+    networking.vswitches = mkOption {
+      default = { };
+      example =
+        { vs0.interfaces = { eth0 = { }; lo1 = { type="internal"; }; };
+          vs1.interfaces = [ { name = "eth2"; } { name = "lo2"; type="internal"; } ];
+        };
+      description =
+        ''
+          This option allows you to define Open vSwitches that connect
+          physical networks together. The value of this option is an
+          attribute set. Each attribute specifies a vswitch, with the
+          attribute name specifying the name of the vswitch's network
+          interface.
+        '';
+
+      type = with types; attrsOf (submodule {
+
+        options = {
+
+          interfaces = mkOption {
+            description = "The physical network interfaces connected by the vSwitch.";
+            type = with types; attrsOf (submodule vswitchInterfaceOpts);
+          };
+
+          controllers = mkOption {
+            type = types.listOf types.str;
+            default = [];
+            example = [ "ptcp:6653:[::1]" ];
+            description = ''
+              Specify the controller targets. For the allowed options see <literal>man 8 ovs-vsctl</literal>.
+            '';
+          };
+
+          openFlowRules = mkOption {
+            type = types.lines;
+            default = "";
+            example = ''
+              actions=normal
+            '';
+            description = ''
+              OpenFlow rules to insert into the Open vSwitch. All <literal>openFlowRules</literal> are
+              loaded with <literal>ovs-ofctl</literal> within one atomic operation.
+            '';
+          };
+
+          # TODO: custom "openflow version" type, with list from existing openflow protocols
+          supportedOpenFlowVersions = mkOption {
+            type = types.listOf types.str;
+            example = [ "OpenFlow10" "OpenFlow13" "OpenFlow14" ];
+            default = [ "OpenFlow13" ];
+            description = ''
+              Supported versions to enable on this switch.
+            '';
+          };
+
+          # TODO: use same type as elements from supportedOpenFlowVersions
+          openFlowVersion = mkOption {
+            type = types.str;
+            default = "OpenFlow13";
+            description = ''
+              Version of OpenFlow protocol to use when communicating with the switch internally (e.g. with <literal>openFlowRules</literal>).
+            '';
+          };
+
+          extraOvsctlCmds = mkOption {
+            type = types.lines;
+            default = "";
+            example = ''
+              set-fail-mode <switch_name> secure
+              set Bridge <switch_name> stp_enable=true
+            '';
+            description = ''
+              Commands to manipulate the Open vSwitch database. Every line executed with <literal>ovs-vsctl</literal>.
+              All commands are bundled together with the operations for adding the interfaces
+              into one atomic operation.
+            '';
+          };
+
+        };
+
+      });
+
+    };
+
+    networking.bridges = mkOption {
+      default = { };
+      example =
+        { br0.interfaces = [ "eth0" "eth1" ];
+          br1.interfaces = [ "eth2" "wlan0" ];
+        };
+      description =
+        ''
+          This option allows you to define Ethernet bridge devices
+          that connect physical networks together.  The value of this
+          option is an attribute set.  Each attribute specifies a
+          bridge, with the attribute name specifying the name of the
+          bridge's network interface.
+        '';
+
+      type = with types; attrsOf (submodule {
+
+        options = {
+
+          interfaces = mkOption {
+            example = [ "eth0" "eth1" ];
+            type = types.listOf types.str;
+            description =
+              "The physical network interfaces connected by the bridge.";
+          };
+
+          rstp = mkOption {
+            default = false;
+            type = types.bool;
+            description = "Whether the bridge interface should enable rstp.";
+          };
+
+        };
+
+      });
+
+    };
+
+    networking.bonds =
+      let
+        driverOptionsExample =  ''
+          {
+            miimon = "100";
+            mode = "active-backup";
+          }
+        '';
+      in mkOption {
+        default = { };
+        example = literalExpression ''
+          {
+            bond0 = {
+              interfaces = [ "eth0" "wlan0" ];
+              driverOptions = ${driverOptionsExample};
+            };
+            anotherBond.interfaces = [ "enp4s0f0" "enp4s0f1" "enp5s0f0" "enp5s0f1" ];
+          }
+        '';
+        description = ''
+          This option allows you to define bond devices that aggregate multiple,
+          underlying networking interfaces together. The value of this option is
+          an attribute set. Each attribute specifies a bond, with the attribute
+          name specifying the name of the bond's network interface
+        '';
+
+        type = with types; attrsOf (submodule {
+
+          options = {
+
+            interfaces = mkOption {
+              example = [ "enp4s0f0" "enp4s0f1" "wlan0" ];
+              type = types.listOf types.str;
+              description = "The interfaces to bond together";
+            };
+
+            driverOptions = mkOption {
+              type = types.attrsOf types.str;
+              default = {};
+              example = literalExpression driverOptionsExample;
+              description = ''
+                Options for the bonding driver.
+                Documentation can be found in
+                <link xlink:href="https://www.kernel.org/doc/Documentation/networking/bonding.txt" />
+              '';
+
+            };
+
+            lacp_rate = mkOption {
+              default = null;
+              example = "fast";
+              type = types.nullOr types.str;
+              description = ''
+                DEPRECATED, use `driverOptions`.
+                Option specifying the rate in which we'll ask our link partner
+                to transmit LACPDU packets in 802.3ad mode.
+              '';
+            };
+
+            miimon = mkOption {
+              default = null;
+              example = 100;
+              type = types.nullOr types.int;
+              description = ''
+                DEPRECATED, use `driverOptions`.
+                Miimon is the number of millisecond in between each round of polling
+                by the device driver for failed links. By default polling is not
+                enabled and the driver is trusted to properly detect and handle
+                failure scenarios.
+              '';
+            };
+
+            mode = mkOption {
+              default = null;
+              example = "active-backup";
+              type = types.nullOr types.str;
+              description = ''
+                DEPRECATED, use `driverOptions`.
+                The mode which the bond will be running. The default mode for
+                the bonding driver is balance-rr, optimizing for throughput.
+                More information about valid modes can be found at
+                https://www.kernel.org/doc/Documentation/networking/bonding.txt
+              '';
+            };
+
+            xmit_hash_policy = mkOption {
+              default = null;
+              example = "layer2+3";
+              type = types.nullOr types.str;
+              description = ''
+                DEPRECATED, use `driverOptions`.
+                Selects the transmit hash policy to use for slave selection in
+                balance-xor, 802.3ad, and tlb modes.
+              '';
+            };
+
+          };
+
+        });
+      };
+
+    networking.macvlans = mkOption {
+      default = { };
+      example = literalExpression ''
+        {
+          wan = {
+            interface = "enp2s0";
+            mode = "vepa";
+          };
+        }
+      '';
+      description = ''
+        This option allows you to define macvlan interfaces which should
+        be automatically created.
+      '';
+      type = with types; attrsOf (submodule {
+        options = {
+
+          interface = mkOption {
+            example = "enp4s0";
+            type = types.str;
+            description = "The interface the macvlan will transmit packets through.";
+          };
+
+          mode = mkOption {
+            default = null;
+            type = types.nullOr types.str;
+            example = "vepa";
+            description = "The mode of the macvlan device.";
+          };
+
+        };
+
+      });
+    };
+
+    networking.fooOverUDP = mkOption {
+      default = { };
+      example =
+        {
+          primary = { port = 9001; local = { address = "192.0.2.1"; dev = "eth0"; }; };
+          backup =  { port = 9002; };
+        };
+      description = ''
+        This option allows you to configure Foo Over UDP and Generic UDP Encapsulation
+        endpoints. See <citerefentry><refentrytitle>ip-fou</refentrytitle>
+        <manvolnum>8</manvolnum></citerefentry> for details.
+      '';
+      type = with types; attrsOf (submodule {
+        options = {
+          port = mkOption {
+            type = port;
+            description = ''
+              Local port of the encapsulation UDP socket.
+            '';
+          };
+
+          protocol = mkOption {
+            type = nullOr (ints.between 1 255);
+            default = null;
+            description = ''
+              Protocol number of the encapsulated packets. Specifying <literal>null</literal>
+              (the default) creates a GUE endpoint, specifying a protocol number will create
+              a FOU endpoint.
+            '';
+          };
+
+          local = mkOption {
+            type = nullOr (submodule {
+              options = {
+                address = mkOption {
+                  type = types.str;
+                  description = ''
+                    Local address to bind to. The address must be available when the FOU
+                    endpoint is created, using the scripted network setup this can be achieved
+                    either by setting <literal>dev</literal> or adding dependency information to
+                    <literal>systemd.services.&lt;name&gt;-fou-encap</literal>; it isn't supported
+                    when using networkd.
+                  '';
+                };
+
+                dev = mkOption {
+                  type = nullOr str;
+                  default = null;
+                  example = "eth0";
+                  description = ''
+                    Network device to bind to.
+                  '';
+                };
+              };
+            });
+            default = null;
+            example = { address = "203.0.113.22"; };
+            description = ''
+              Local address (and optionally device) to bind to using the given port.
+            '';
+          };
+        };
+      });
+    };
+
+    networking.sits = mkOption {
+      default = { };
+      example = literalExpression ''
+        {
+          hurricane = {
+            remote = "10.0.0.1";
+            local = "10.0.0.22";
+            ttl = 255;
+          };
+          msipv6 = {
+            remote = "192.168.0.1";
+            dev = "enp3s0";
+            ttl = 127;
+          };
+        }
+      '';
+      description = ''
+        This option allows you to define 6-to-4 interfaces which should be automatically created.
+      '';
+      type = with types; attrsOf (submodule {
+        options = {
+
+          remote = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "10.0.0.1";
+            description = ''
+              The address of the remote endpoint to forward traffic over.
+            '';
+          };
+
+          local = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "10.0.0.22";
+            description = ''
+              The address of the local endpoint which the remote
+              side should send packets to.
+            '';
+          };
+
+          ttl = mkOption {
+            type = types.nullOr types.int;
+            default = null;
+            example = 255;
+            description = ''
+              The time-to-live of the connection to the remote tunnel endpoint.
+            '';
+          };
+
+          dev = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "enp4s0f0";
+            description = ''
+              The underlying network device on which the tunnel resides.
+            '';
+          };
+
+          encapsulation = with types; mkOption {
+            type = nullOr (submodule {
+              options = {
+                type = mkOption {
+                  type = enum [ "fou" "gue" ];
+                  description = ''
+                    Selects encapsulation type. See
+                    <citerefentry><refentrytitle>ip-link</refentrytitle>
+                    <manvolnum>8</manvolnum></citerefentry> for details.
+                  '';
+                };
+
+                port = mkOption {
+                  type = port;
+                  example = 9001;
+                  description = ''
+                    Destination port for encapsulated packets.
+                  '';
+                };
+
+                sourcePort = mkOption {
+                  type = nullOr types.port;
+                  default = null;
+                  example = 9002;
+                  description = ''
+                    Source port for encapsulated packets. Will be chosen automatically by
+                    the kernel if unset.
+                  '';
+                };
+              };
+            });
+            default = null;
+            example = { type = "fou"; port = 9001; };
+            description = ''
+              Configures encapsulation in UDP packets.
+            '';
+          };
+
+        };
+
+      });
+    };
+
+    networking.greTunnels = mkOption {
+      default = { };
+      example = literalExpression ''
+        {
+          greBridge = {
+            remote = "10.0.0.1";
+            local = "10.0.0.22";
+            dev = "enp4s0f0";
+            type = "tap";
+          };
+          gre6Tunnel = {
+            remote = "fd7a:5634::1";
+            local = "fd7a:5634::2";
+            dev = "enp4s0f0";
+            type = "tun6";
+          };
+        }
+      '';
+      description = ''
+        This option allows you to define Generic Routing Encapsulation (GRE) tunnels.
+      '';
+      type = with types; attrsOf (submodule {
+        options = {
+
+          remote = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "10.0.0.1";
+            description = ''
+              The address of the remote endpoint to forward traffic over.
+            '';
+          };
+
+          local = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "10.0.0.22";
+            description = ''
+              The address of the local endpoint which the remote
+              side should send packets to.
+            '';
+          };
+
+          dev = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "enp4s0f0";
+            description = ''
+              The underlying network device on which the tunnel resides.
+            '';
+          };
+
+          type = mkOption {
+            type = with types; enum [ "tun" "tap" "tun6" "tap6" ];
+            default = "tap";
+            example = "tap";
+            apply = v: {
+              tun = "gre";
+              tap = "gretap";
+              tun6 = "ip6gre";
+              tap6 = "ip6gretap";
+            }.${v};
+            description = ''
+              Whether the tunnel routes layer 2 (tap) or layer 3 (tun) traffic.
+            '';
+          };
+        };
+      });
+    };
+
+    networking.vlans = mkOption {
+      default = { };
+      example = literalExpression ''
+        {
+          vlan0 = {
+            id = 3;
+            interface = "enp3s0";
+          };
+          vlan1 = {
+            id = 1;
+            interface = "wlan0";
+          };
+        }
+      '';
+      description =
+        ''
+          This option allows you to define vlan devices that tag packets
+          on top of a physical interface. The value of this option is an
+          attribute set. Each attribute specifies a vlan, with the name
+          specifying the name of the vlan interface.
+        '';
+
+      type = with types; attrsOf (submodule {
+
+        options = {
+
+          id = mkOption {
+            example = 1;
+            type = types.int;
+            description = "The vlan identifier";
+          };
+
+          interface = mkOption {
+            example = "enp4s0";
+            type = types.str;
+            description = "The interface the vlan will transmit packets through.";
+          };
+
+        };
+
+      });
+
+    };
+
+    networking.wlanInterfaces = mkOption {
+      default = { };
+      example = literalExpression ''
+        {
+          wlan-station0 = {
+              device = "wlp6s0";
+          };
+          wlan-adhoc0 = {
+              type = "ibss";
+              device = "wlp6s0";
+              mac = "02:00:00:00:00:01";
+          };
+          wlan-p2p0 = {
+              device = "wlp6s0";
+              mac = "02:00:00:00:00:02";
+          };
+          wlan-ap0 = {
+              device = "wlp6s0";
+              mac = "02:00:00:00:00:03";
+          };
+        }
+      '';
+      description =
+        ''
+          Creating multiple WLAN interfaces on top of one physical WLAN device (NIC).
+
+          The name of the WLAN interface corresponds to the name of the attribute.
+          A NIC is referenced by the persistent device name of the WLAN interface that
+          <literal>udev</literal> assigns to a NIC by default.
+          If a NIC supports multiple WLAN interfaces, then the one NIC can be used as
+          <literal>device</literal> for multiple WLAN interfaces.
+          If a NIC is used for creating WLAN interfaces, then the default WLAN interface
+          with a persistent device name form <literal>udev</literal> is not created.
+          A WLAN interface with the persistent name assigned from <literal>udev</literal>
+          would have to be created explicitly.
+        '';
+
+      type = with types; attrsOf (submodule {
+
+        options = {
+
+          device = mkOption {
+            type = types.str;
+            example = "wlp6s0";
+            description = "The name of the underlying hardware WLAN device as assigned by <literal>udev</literal>.";
+          };
+
+          type = mkOption {
+            type = types.enum [ "managed" "ibss" "monitor" "mesh" "wds" ];
+            default = "managed";
+            example = "ibss";
+            description = ''
+              The type of the WLAN interface.
+              The type has to be supported by the underlying hardware of the device.
+            '';
+          };
+
+          meshID = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            description = "MeshID of interface with type <literal>mesh</literal>.";
+          };
+
+          flags = mkOption {
+            type = with types; nullOr (enum [ "none" "fcsfail" "control" "otherbss" "cook" "active" ]);
+            default = null;
+            example = "control";
+            description = ''
+              Flags for interface of type <literal>monitor</literal>.
+            '';
+          };
+
+          fourAddr = mkOption {
+            type = types.nullOr types.bool;
+            default = null;
+            description = "Whether to enable <literal>4-address mode</literal> with type <literal>managed</literal>.";
+          };
+
+          mac = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "02:00:00:00:00:01";
+            description = ''
+              MAC address to use for the device. If <literal>null</literal>, then the MAC of the
+              underlying hardware WLAN device is used.
+
+              INFO: Locally administered MAC addresses are of the form:
+              <itemizedlist>
+              <listitem><para>x2:xx:xx:xx:xx:xx</para></listitem>
+              <listitem><para>x6:xx:xx:xx:xx:xx</para></listitem>
+              <listitem><para>xA:xx:xx:xx:xx:xx</para></listitem>
+              <listitem><para>xE:xx:xx:xx:xx:xx</para></listitem>
+              </itemizedlist>
+            '';
+          };
+
+        };
+
+      });
+
+    };
+
+    networking.useDHCP = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to use DHCP to obtain an IP address and other
+        configuration for all network interfaces that are not manually
+        configured.
+
+        Using this option is highly discouraged and also incompatible with
+        <option>networking.useNetworkd</option>. Please use
+        <option>networking.interfaces.&lt;name&gt;.useDHCP</option> instead
+        and set this to false.
+      '';
+    };
+
+    networking.useNetworkd = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether we should use networkd as the network configuration backend or
+        the legacy script based system. Note that this option is experimental,
+        enable at your own risk.
+      '';
+    };
+
+    networking.tempAddresses = mkOption {
+      default = if cfg.enableIPv6 then "default" else "disabled";
+      defaultText = literalExpression ''
+        if ''${config.${opt.enableIPv6}} then "default" else "disabled"
+      '';
+      type = types.enum (lib.attrNames tempaddrValues);
+      description = ''
+        Whether to enable IPv6 Privacy Extensions for interfaces not
+        configured explicitly in
+        <xref linkend="opt-networking.interfaces._name_.tempAddress" />.
+
+        This sets the ipv6.conf.*.use_tempaddr sysctl for all
+        interfaces. Possible values are:
+
+        ${tempaddrDoc}
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = {
+
+    warnings = concatMap (i: i.warnings) interfaces;
+
+    assertions =
+      (forEach interfaces (i: {
+        # With the linux kernel, interface name length is limited by IFNAMSIZ
+        # to 16 bytes, including the trailing null byte.
+        # See include/linux/if.h in the kernel sources
+        assertion = stringLength i.name < 16;
+        message = ''
+          The name of networking.interfaces."${i.name}" is too long, it needs to be less than 16 characters.
+        '';
+      })) ++ (forEach slaveIfs (i: {
+        assertion = i.ipv4.addresses == [ ] && i.ipv6.addresses == [ ];
+        message = ''
+          The networking.interfaces."${i.name}" must not have any defined ips when it is a slave.
+        '';
+      })) ++ (forEach interfaces (i: {
+        assertion = i.tempAddress != "disabled" -> cfg.enableIPv6;
+        message = ''
+          Temporary addresses are only needed when IPv6 is enabled.
+        '';
+      })) ++ (forEach interfaces (i: {
+        assertion = (i.virtual && i.virtualType == "tun") -> i.macAddress == null;
+        message = ''
+          Setting a MAC Address for tun device ${i.name} isn't supported.
+        '';
+      })) ++ [
+        {
+          assertion = cfg.hostId == null || (stringLength cfg.hostId == 8 && isHexString cfg.hostId);
+          message = "Invalid value given to the networking.hostId option.";
+        }
+      ];
+
+    boot.kernelModules = [ ]
+      ++ optional hasVirtuals "tun"
+      ++ optional hasSits "sit"
+      ++ optional hasGres "gre"
+      ++ optional hasBonds "bonding"
+      ++ optional hasFous "fou";
+
+    boot.extraModprobeConfig =
+      # This setting is intentional as it prevents default bond devices
+      # from being created.
+      optionalString hasBonds "options bonding max_bonds=0";
+
+    boot.kernel.sysctl = {
+      "net.ipv4.conf.all.forwarding" = mkDefault (any (i: i.proxyARP) interfaces);
+      "net.ipv6.conf.all.disable_ipv6" = mkDefault (!cfg.enableIPv6);
+      "net.ipv6.conf.default.disable_ipv6" = mkDefault (!cfg.enableIPv6);
+      # networkmanager falls back to "/proc/sys/net/ipv6/conf/default/use_tempaddr"
+      "net.ipv6.conf.default.use_tempaddr" = tempaddrValues.${cfg.tempAddresses}.sysctl;
+    } // listToAttrs (flip concatMap (filter (i: i.proxyARP) interfaces)
+        (i: [(nameValuePair "net.ipv4.conf.${replaceChars ["."] ["/"] i.name}.proxy_arp" true)]))
+      // listToAttrs (forEach interfaces
+        (i: let
+          opt = i.tempAddress;
+          val = tempaddrValues.${opt}.sysctl;
+         in nameValuePair "net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr" val));
+
+    security.wrappers = {
+      ping = {
+        owner = "root";
+        group = "root";
+        capabilities = "cap_net_raw+p";
+        source = "${pkgs.iputils.out}/bin/ping";
+      };
+    };
+    security.apparmor.policies."bin.ping".profile = lib.mkIf config.security.apparmor.policies."bin.ping".enable (lib.mkAfter ''
+      /run/wrappers/bin/ping {
+        include <abstractions/base>
+        include <nixos/security.wrappers>
+        rpx /run/wrappers/wrappers.*/ping,
+      }
+      /run/wrappers/wrappers.*/ping {
+        include <abstractions/base>
+        include <nixos/security.wrappers>
+        r /run/wrappers/wrappers.*/ping.real,
+        mrpx ${config.security.wrappers.ping.source},
+        capability net_raw,
+        capability setpcap,
+      }
+    '');
+
+    # Set the host and domain names in the activation script.  Don't
+    # clear it if it's not configured in the NixOS configuration,
+    # since it may have been set by dhcpcd in the meantime.
+    system.activationScripts.hostname =
+      optionalString (cfg.hostName != "") ''
+        hostname "${cfg.hostName}"
+      '';
+    system.activationScripts.domain =
+      optionalString (cfg.domain != null) ''
+        domainname "${cfg.domain}"
+      '';
+
+    environment.etc.hostid = mkIf (cfg.hostId != null)
+      { source = pkgs.runCommand "gen-hostid" { preferLocalBuild = true; } ''
+          hi="${cfg.hostId}"
+          ${if pkgs.stdenv.isBigEndian then ''
+            echo -ne "\x''${hi:0:2}\x''${hi:2:2}\x''${hi:4:2}\x''${hi:6:2}" > $out
+          '' else ''
+            echo -ne "\x''${hi:6:2}\x''${hi:4:2}\x''${hi:2:2}\x''${hi:0:2}" > $out
+          ''}
+        '';
+      };
+
+    # static hostname configuration needed for hostnamectl and the
+    # org.freedesktop.hostname1 dbus service (both provided by systemd)
+    environment.etc.hostname = mkIf (cfg.hostName != "")
+      {
+        text = cfg.hostName + "\n";
+      };
+
+    environment.systemPackages =
+      [ pkgs.host
+        pkgs.iproute2
+        pkgs.iputils
+        pkgs.nettools
+      ]
+      ++ optionals config.networking.wireless.enable [
+        pkgs.wirelesstools # FIXME: obsolete?
+        pkgs.iw
+      ]
+      ++ bridgeStp;
+
+    # The network-interfaces target is kept for backwards compatibility.
+    # New modules must NOT use it.
+    systemd.targets.network-interfaces =
+      { description = "All Network Interfaces (deprecated)";
+        wantedBy = [ "network.target" ];
+        before = [ "network.target" ];
+        after = [ "network-pre.target" ];
+        unitConfig.X-StopOnReconfiguration = true;
+      };
+
+    systemd.services = {
+      network-local-commands = {
+        description = "Extra networking commands.";
+        before = [ "network.target" ];
+        wantedBy = [ "network.target" ];
+        after = [ "network-pre.target" ];
+        unitConfig.ConditionCapability = "CAP_NET_ADMIN";
+        path = [ pkgs.iproute2 ];
+        serviceConfig.Type = "oneshot";
+        serviceConfig.RemainAfterExit = true;
+        script = ''
+          # Run any user-specified commands.
+          ${cfg.localCommands}
+        '';
+      };
+    };
+    services.mstpd = mkIf needsMstpd { enable = true; };
+
+    virtualisation.vswitch = mkIf (cfg.vswitches != { }) { enable = true; };
+
+    services.udev.packages =  [
+      (pkgs.writeTextFile rec {
+        name = "ipv6-privacy-extensions.rules";
+        destination = "/etc/udev/rules.d/98-${name}";
+        text = let
+          sysctl-value = tempaddrValues.${cfg.tempAddresses}.sysctl;
+        in ''
+          # enable and prefer IPv6 privacy addresses by default
+          ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.bash}/bin/sh -c 'echo ${sysctl-value} > /proc/sys/net/ipv6/conf/%k/use_tempaddr'"
+        '';
+      })
+      (pkgs.writeTextFile rec {
+        name = "ipv6-privacy-extensions.rules";
+        destination = "/etc/udev/rules.d/99-${name}";
+        text = concatMapStrings (i:
+          let
+            opt = i.tempAddress;
+            val = tempaddrValues.${opt}.sysctl;
+            msg = tempaddrValues.${opt}.description;
+          in
+          ''
+            # override to ${msg} for ${i.name}
+            ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.procps}/bin/sysctl net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr=${val}"
+          '') (filter (i: i.tempAddress != cfg.tempAddresses) interfaces);
+      })
+    ] ++ lib.optional (cfg.wlanInterfaces != {})
+      (pkgs.writeTextFile {
+        name = "99-zzz-40-wlanInterfaces.rules";
+        destination = "/etc/udev/rules.d/99-zzz-40-wlanInterfaces.rules";
+        text =
+          let
+            # Collect all interfaces that are defined for a device
+            # as device:interface key:value pairs.
+            wlanDeviceInterfaces =
+              let
+                allDevices = unique (mapAttrsToList (_: v: v.device) cfg.wlanInterfaces);
+                interfacesOfDevice = d: filterAttrs (_: v: v.device == d) cfg.wlanInterfaces;
+              in
+                genAttrs allDevices (d: interfacesOfDevice d);
+
+            # Convert device:interface key:value pairs into a list, and if it exists,
+            # place the interface which is named after the device at the beginning.
+            wlanListDeviceFirst = device: interfaces:
+              if hasAttr device interfaces
+              then mapAttrsToList (n: v: v//{_iName=n;}) (filterAttrs (n: _: n==device) interfaces) ++ mapAttrsToList (n: v: v//{_iName=n;}) (filterAttrs (n: _: n!=device) interfaces)
+              else mapAttrsToList (n: v: v // {_iName = n;}) interfaces;
+
+            # Udev script to execute for the default WLAN interface with the persistend udev name.
+            # The script creates the required, new WLAN interfaces interfaces and configures the
+            # existing, default interface.
+            curInterfaceScript = device: current: new: pkgs.writeScript "udev-run-script-wlan-interfaces-${device}.sh" ''
+              #!${pkgs.runtimeShell}
+              # Change the wireless phy device to a predictable name.
+              ${pkgs.iw}/bin/iw phy `${pkgs.coreutils}/bin/cat /sys/class/net/$INTERFACE/phy80211/name` set name ${device}
+
+              # Add new WLAN interfaces
+              ${flip concatMapStrings new (i: ''
+              ${pkgs.iw}/bin/iw phy ${device} interface add ${i._iName} type managed
+              '')}
+
+              # Configure the current interface
+              ${pkgs.iw}/bin/iw dev ${device} set type ${current.type}
+              ${optionalString (current.type == "mesh" && current.meshID!=null) "${pkgs.iw}/bin/iw dev ${device} set meshid ${current.meshID}"}
+              ${optionalString (current.type == "monitor" && current.flags!=null) "${pkgs.iw}/bin/iw dev ${device} set monitor ${current.flags}"}
+              ${optionalString (current.type == "managed" && current.fourAddr!=null) "${pkgs.iw}/bin/iw dev ${device} set 4addr ${if current.fourAddr then "on" else "off"}"}
+              ${optionalString (current.mac != null) "${pkgs.iproute2}/bin/ip link set dev ${device} address ${current.mac}"}
+            '';
+
+            # Udev script to execute for a new WLAN interface. The script configures the new WLAN interface.
+            newInterfaceScript = new: pkgs.writeScript "udev-run-script-wlan-interfaces-${new._iName}.sh" ''
+              #!${pkgs.runtimeShell}
+              # Configure the new interface
+              ${pkgs.iw}/bin/iw dev ${new._iName} set type ${new.type}
+              ${optionalString (new.type == "mesh" && new.meshID!=null) "${pkgs.iw}/bin/iw dev ${new._iName} set meshid ${new.meshID}"}
+              ${optionalString (new.type == "monitor" && new.flags!=null) "${pkgs.iw}/bin/iw dev ${new._iName} set monitor ${new.flags}"}
+              ${optionalString (new.type == "managed" && new.fourAddr!=null) "${pkgs.iw}/bin/iw dev ${new._iName} set 4addr ${if new.fourAddr then "on" else "off"}"}
+              ${optionalString (new.mac != null) "${pkgs.iproute2}/bin/ip link set dev ${new._iName} address ${new.mac}"}
+            '';
+
+            # Udev attributes for systemd to name the device and to create a .device target.
+            systemdAttrs = n: ''NAME:="${n}", ENV{INTERFACE}="${n}", ENV{SYSTEMD_ALIAS}="/sys/subsystem/net/devices/${n}", TAG+="systemd"'';
+          in
+          flip (concatMapStringsSep "\n") (attrNames wlanDeviceInterfaces) (device:
+            let
+              interfaces = wlanListDeviceFirst device wlanDeviceInterfaces.${device};
+              curInterface = elemAt interfaces 0;
+              newInterfaces = drop 1 interfaces;
+            in ''
+            # It is important to have that rule first as overwriting the NAME attribute also prevents the
+            # next rules from matching.
+            ${flip (concatMapStringsSep "\n") (wlanListDeviceFirst device wlanDeviceInterfaces.${device}) (interface:
+            ''ACTION=="add", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", ENV{INTERFACE}=="${interface._iName}", ${systemdAttrs interface._iName}, RUN+="${newInterfaceScript interface}"'')}
+
+            # Add the required, new WLAN interfaces to the default WLAN interface with the
+            # persistent, default name as assigned by udev.
+            ACTION=="add", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", NAME=="${device}", ${systemdAttrs curInterface._iName}, RUN+="${curInterfaceScript device curInterface newInterfaces}"
+            # Generate the same systemd events for both 'add' and 'move' udev events.
+            ACTION=="move", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", NAME=="${device}", ${systemdAttrs curInterface._iName}
+          '');
+      });
+  };
+
+}
diff --git a/nixos/modules/tasks/powertop.nix b/nixos/modules/tasks/powertop.nix
new file mode 100644
index 00000000000..e8064f9fa80
--- /dev/null
+++ b/nixos/modules/tasks/powertop.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.powerManagement.powertop;
+in {
+  ###### interface
+
+  options.powerManagement.powertop.enable = mkEnableOption "powertop auto tuning on startup";
+
+  ###### implementation
+
+  config = mkIf (cfg.enable) {
+    systemd.services = {
+      powertop = {
+        wantedBy = [ "multi-user.target" ];
+        after = [ "multi-user.target" ];
+        description = "Powertop tunings";
+        path = [ pkgs.kmod ];
+        serviceConfig = {
+          Type = "oneshot";
+          RemainAfterExit = "yes";
+          ExecStart = "${pkgs.powertop}/bin/powertop --auto-tune";
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/tasks/scsi-link-power-management.nix b/nixos/modules/tasks/scsi-link-power-management.nix
new file mode 100644
index 00000000000..a9d987780ee
--- /dev/null
+++ b/nixos/modules/tasks/scsi-link-power-management.nix
@@ -0,0 +1,54 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.powerManagement.scsiLinkPolicy;
+
+  kernel = config.boot.kernelPackages.kernel;
+
+  allowedValues = [
+    "min_power"
+    "max_performance"
+    "medium_power"
+    "med_power_with_dipm"
+  ];
+
+in
+
+{
+  ###### interface
+
+  options = {
+
+    powerManagement.scsiLinkPolicy = mkOption {
+      default = null;
+      type = types.nullOr (types.enum allowedValues);
+      description = ''
+        SCSI link power management policy. The kernel default is
+        "max_performance".
+        </para><para>
+        "med_power_with_dipm" is supported by kernel versions
+        4.15 and newer.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf (cfg != null) {
+
+    assertions = singleton {
+      assertion = (cfg == "med_power_with_dipm") -> versionAtLeast kernel.version "4.15";
+      message = "med_power_with_dipm is not supported for kernels older than 4.15";
+    };
+
+    services.udev.extraRules = ''
+      SUBSYSTEM=="scsi_host", ACTION=="add", KERNEL=="host*", ATTR{link_power_management_policy}="${cfg}"
+    '';
+  };
+
+}
diff --git a/nixos/modules/tasks/snapraid.nix b/nixos/modules/tasks/snapraid.nix
new file mode 100644
index 00000000000..c8dde5b4899
--- /dev/null
+++ b/nixos/modules/tasks/snapraid.nix
@@ -0,0 +1,230 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.snapraid;
+in
+{
+  options.snapraid = with types; {
+    enable = mkEnableOption "SnapRAID";
+    dataDisks = mkOption {
+      default = { };
+      example = {
+        d1 = "/mnt/disk1/";
+        d2 = "/mnt/disk2/";
+        d3 = "/mnt/disk3/";
+      };
+      description = "SnapRAID data disks.";
+      type = attrsOf str;
+    };
+    parityFiles = mkOption {
+      default = [ ];
+      example = [
+        "/mnt/diskp/snapraid.parity"
+        "/mnt/diskq/snapraid.2-parity"
+        "/mnt/diskr/snapraid.3-parity"
+        "/mnt/disks/snapraid.4-parity"
+        "/mnt/diskt/snapraid.5-parity"
+        "/mnt/disku/snapraid.6-parity"
+      ];
+      description = "SnapRAID parity files.";
+      type = listOf str;
+    };
+    contentFiles = mkOption {
+      default = [ ];
+      example = [
+        "/var/snapraid.content"
+        "/mnt/disk1/snapraid.content"
+        "/mnt/disk2/snapraid.content"
+      ];
+      description = "SnapRAID content list files.";
+      type = listOf str;
+    };
+    exclude = mkOption {
+      default = [ ];
+      example = [ "*.unrecoverable" "/tmp/" "/lost+found/" ];
+      description = "SnapRAID exclude directives.";
+      type = listOf str;
+    };
+    touchBeforeSync = mkOption {
+      default = true;
+      example = false;
+      description =
+        "Whether <command>snapraid touch</command> should be run before <command>snapraid sync</command>.";
+      type = bool;
+    };
+    sync.interval = mkOption {
+      default = "01:00";
+      example = "daily";
+      description = "How often to run <command>snapraid sync</command>.";
+      type = str;
+    };
+    scrub = {
+      interval = mkOption {
+        default = "Mon *-*-* 02:00:00";
+        example = "weekly";
+        description = "How often to run <command>snapraid scrub</command>.";
+        type = str;
+      };
+      plan = mkOption {
+        default = 8;
+        example = 5;
+        description =
+          "Percent of the array that should be checked by <command>snapraid scrub</command>.";
+        type = int;
+      };
+      olderThan = mkOption {
+        default = 10;
+        example = 20;
+        description =
+          "Number of days since data was last scrubbed before it can be scrubbed again.";
+        type = int;
+      };
+    };
+    extraConfig = mkOption {
+      default = "";
+      example = ''
+        nohidden
+        blocksize 256
+        hashsize 16
+        autosave 500
+        pool /pool
+      '';
+      description = "Extra config options for SnapRAID.";
+      type = lines;
+    };
+  };
+
+  config =
+    let
+      nParity = builtins.length cfg.parityFiles;
+      mkPrepend = pre: s: pre + s;
+    in
+    mkIf cfg.enable {
+      assertions = [
+        {
+          assertion = nParity <= 6;
+          message = "You can have no more than six SnapRAID parity files.";
+        }
+        {
+          assertion = builtins.length cfg.contentFiles >= nParity + 1;
+          message =
+            "There must be at least one SnapRAID content file for each SnapRAID parity file plus one.";
+        }
+      ];
+
+      environment = {
+        systemPackages = with pkgs; [ snapraid ];
+
+        etc."snapraid.conf" = {
+          text = with cfg;
+            let
+              prependData = mkPrepend "data ";
+              prependContent = mkPrepend "content ";
+              prependExclude = mkPrepend "exclude ";
+            in
+            concatStringsSep "\n"
+              (map prependData
+                ((mapAttrsToList (name: value: name + " " + value)) dataDisks)
+              ++ zipListsWith (a: b: a + b)
+                ([ "parity " ] ++ map (i: toString i + "-parity ") (range 2 6))
+                parityFiles ++ map prependContent contentFiles
+              ++ map prependExclude exclude) + "\n" + extraConfig;
+        };
+      };
+
+      systemd.services = with cfg; {
+        snapraid-scrub = {
+          description = "Scrub the SnapRAID array";
+          startAt = scrub.interval;
+          serviceConfig = {
+            Type = "oneshot";
+            ExecStart = "${pkgs.snapraid}/bin/snapraid scrub -p ${
+              toString scrub.plan
+            } -o ${toString scrub.olderThan}";
+            Nice = 19;
+            IOSchedulingPriority = 7;
+            CPUSchedulingPolicy = "batch";
+
+            LockPersonality = true;
+            MemoryDenyWriteExecute = true;
+            NoNewPrivileges = true;
+            PrivateDevices = true;
+            PrivateTmp = true;
+            ProtectClock = true;
+            ProtectControlGroups = true;
+            ProtectHostname = true;
+            ProtectKernelLogs = true;
+            ProtectKernelModules = true;
+            ProtectKernelTunables = true;
+            RestrictAddressFamilies = "none";
+            RestrictNamespaces = true;
+            RestrictRealtime = true;
+            RestrictSUIDSGID = true;
+            SystemCallArchitectures = "native";
+            SystemCallFilter = "@system-service";
+            SystemCallErrorNumber = "EPERM";
+            CapabilityBoundingSet = "CAP_DAC_OVERRIDE";
+
+            ProtectSystem = "strict";
+            ProtectHome = "read-only";
+            ReadWritePaths =
+              # scrub requires access to directories containing content files
+              # to remove them if they are stale
+              let
+                contentDirs = map dirOf contentFiles;
+              in
+              unique (
+                attrValues dataDisks ++ contentDirs
+              );
+          };
+          unitConfig.After = "snapraid-sync.service";
+        };
+        snapraid-sync = {
+          description = "Synchronize the state of the SnapRAID array";
+          startAt = sync.interval;
+          serviceConfig = {
+            Type = "oneshot";
+            ExecStart = "${pkgs.snapraid}/bin/snapraid sync";
+            Nice = 19;
+            IOSchedulingPriority = 7;
+            CPUSchedulingPolicy = "batch";
+
+            LockPersonality = true;
+            MemoryDenyWriteExecute = true;
+            NoNewPrivileges = true;
+            PrivateTmp = true;
+            ProtectClock = true;
+            ProtectControlGroups = true;
+            ProtectHostname = true;
+            ProtectKernelLogs = true;
+            ProtectKernelModules = true;
+            ProtectKernelTunables = true;
+            RestrictAddressFamilies = "none";
+            RestrictNamespaces = true;
+            RestrictRealtime = true;
+            RestrictSUIDSGID = true;
+            SystemCallArchitectures = "native";
+            SystemCallFilter = "@system-service";
+            SystemCallErrorNumber = "EPERM";
+            CapabilityBoundingSet = "CAP_DAC_OVERRIDE" +
+              lib.optionalString cfg.touchBeforeSync " CAP_FOWNER";
+
+            ProtectSystem = "strict";
+            ProtectHome = "read-only";
+            ReadWritePaths =
+              # sync requires access to directories containing content files
+              # to remove them if they are stale
+              let
+                contentDirs = map dirOf contentFiles;
+              in
+              unique (
+                attrValues dataDisks ++ parityFiles ++ contentDirs
+              );
+          } // optionalAttrs touchBeforeSync {
+            ExecStartPre = "${pkgs.snapraid}/bin/snapraid touch";
+          };
+        };
+      };
+    };
+}
diff --git a/nixos/modules/tasks/swraid.nix b/nixos/modules/tasks/swraid.nix
new file mode 100644
index 00000000000..8fa19194bed
--- /dev/null
+++ b/nixos/modules/tasks/swraid.nix
@@ -0,0 +1,17 @@
+{ pkgs, ... }:
+
+{
+
+  environment.systemPackages = [ pkgs.mdadm ];
+
+  services.udev.packages = [ pkgs.mdadm ];
+
+  systemd.packages = [ pkgs.mdadm ];
+
+  boot.initrd.availableKernelModules = [ "md_mod" "raid0" "raid1" "raid10" "raid456" ];
+
+  boot.initrd.extraUdevRulesCommands = ''
+    cp -v ${pkgs.mdadm}/lib/udev/rules.d/*.rules $out/
+  '';
+
+}
diff --git a/nixos/modules/tasks/trackpoint.nix b/nixos/modules/tasks/trackpoint.nix
new file mode 100644
index 00000000000..029d8a00295
--- /dev/null
+++ b/nixos/modules/tasks/trackpoint.nix
@@ -0,0 +1,108 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+  ###### interface
+
+  options = {
+
+    hardware.trackpoint = {
+
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable sensitivity and speed configuration for trackpoints.
+        '';
+      };
+
+      sensitivity = mkOption {
+        default = 128;
+        example = 255;
+        type = types.int;
+        description = ''
+          Configure the trackpoint sensitivity. By default, the kernel
+          configures 128.
+        '';
+      };
+
+      speed = mkOption {
+        default = 97;
+        example = 255;
+        type = types.int;
+        description = ''
+          Configure the trackpoint speed. By default, the kernel
+          configures 97.
+        '';
+      };
+
+      emulateWheel = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable scrolling while holding the middle mouse button.
+        '';
+      };
+
+      fakeButtons = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Switch to "bare" PS/2 mouse support in case Trackpoint buttons are not recognized
+          properly. This can happen for example on models like the L430, T450, T450s, on
+          which the Trackpoint buttons are actually a part of the Synaptics touchpad.
+        '';
+      };
+
+      device = mkOption {
+        default = "TPPS/2 IBM TrackPoint";
+        type = types.str;
+        description = ''
+          The device name of the trackpoint. You can check with xinput.
+          Some newer devices (example x1c6) use "TPPS/2 Elan TrackPoint".
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config =
+  let cfg = config.hardware.trackpoint; in
+  mkMerge [
+    (mkIf cfg.enable {
+      services.udev.extraRules =
+      ''
+        ACTION=="add|change", SUBSYSTEM=="input", ATTR{name}=="${cfg.device}", ATTR{device/speed}="${toString cfg.speed}", ATTR{device/sensitivity}="${toString cfg.sensitivity}"
+      '';
+
+      system.activationScripts.trackpoint =
+        ''
+          ${config.systemd.package}/bin/udevadm trigger --attr-match=name="${cfg.device}"
+        '';
+    })
+
+    (mkIf (cfg.emulateWheel) {
+      services.xserver.inputClassSections = [
+        ''
+          Identifier "Trackpoint Wheel Emulation"
+          MatchProduct "${if cfg.fakeButtons then "PS/2 Generic Mouse" else "ETPS/2 Elantech TrackPoint|Elantech PS/2 TrackPoint|TPPS/2 IBM TrackPoint|DualPoint Stick|Synaptics Inc. Composite TouchPad / TrackPoint|ThinkPad USB Keyboard with TrackPoint|USB Trackpoint pointing device|Composite TouchPad / TrackPoint|${cfg.device}"}"
+          MatchDevicePath "/dev/input/event*"
+          Option "EmulateWheel" "true"
+          Option "EmulateWheelButton" "2"
+          Option "Emulate3Buttons" "false"
+          Option "XAxisMapping" "6 7"
+          Option "YAxisMapping" "4 5"
+        ''
+      ];
+    })
+
+    (mkIf cfg.fakeButtons {
+      boot.extraModprobeConfig = "options psmouse proto=bare";
+    })
+  ];
+}
diff --git a/nixos/modules/tasks/tty-backgrounds-combine.sh b/nixos/modules/tasks/tty-backgrounds-combine.sh
new file mode 100644
index 00000000000..55c3a1ebfa8
--- /dev/null
+++ b/nixos/modules/tasks/tty-backgrounds-combine.sh
@@ -0,0 +1,32 @@
+source $stdenv/setup
+
+ttys=($ttys)
+themes=($themes)
+
+mkdir -p $out
+
+defaultName=$(cd $default && ls | grep -v default)
+echo $defaultName
+ln -s $default/$defaultName $out/$defaultName
+ln -s $defaultName $out/default
+
+for ((n = 0; n < ${#ttys[*]}; n++)); do
+    tty=${ttys[$n]}
+    theme=${themes[$n]}
+
+    echo "TTY $tty -> $theme"
+
+    if [ "$theme" != default ]; then
+        themeName=$(cd $theme && ls | grep -v default)
+        ln -sfn $theme/$themeName $out/$themeName
+    else
+        themeName=default
+    fi
+
+    if test -e $out/$tty; then
+        echo "Multiple themes defined for the same TTY!"
+        exit 1
+    fi
+
+    ln -sfn $themeName $out/$tty
+done
diff --git a/nixos/modules/testing/minimal-kernel.nix b/nixos/modules/testing/minimal-kernel.nix
new file mode 100644
index 00000000000..7c2b9c05cf9
--- /dev/null
+++ b/nixos/modules/testing/minimal-kernel.nix
@@ -0,0 +1,28 @@
+{ 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/service-runner.nix b/nixos/modules/testing/service-runner.nix
new file mode 100644
index 00000000000..9060be3cca1
--- /dev/null
+++ b/nixos/modules/testing/service-runner.nix
@@ -0,0 +1,127 @@
+{ lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  makeScript = name: service: pkgs.writeScript "${name}-runner"
+    ''
+      #! ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl -w
+
+      use File::Slurp;
+
+      sub run {
+          my ($cmd) = @_;
+          my @args = ();
+          while ($cmd =~ /([^ \t\n']+)|(\'([^'])\')\s*/g) {
+            push @args, $1;
+          }
+          my $prog;
+          if (substr($args[0], 0, 1) eq "@") {
+              $prog = substr($args[0], 1);
+              shift @args;
+          } else {
+              $prog = $args[0];
+          }
+          my $pid = fork;
+          if ($pid == 0) {
+              setpgrp; # don't receive SIGINT etc. from terminal
+              exec { $prog } @args;
+              die "failed to exec $prog\n";
+          } elsif (!defined $pid) {
+              die "failed to fork: $!\n";
+          }
+          return $pid;
+      };
+
+      sub run_wait {
+          my ($cmd) = @_;
+          my $pid = run $cmd;
+          die if waitpid($pid, 0) != $pid;
+          return $?;
+      };
+
+      # Set the environment.  FIXME: escaping.
+      foreach my $key (keys %ENV) {
+          next if $key eq 'LOCALE_ARCHIVE';
+          delete $ENV{$key};
+      }
+      ${concatStrings (mapAttrsToList (n: v: ''
+        $ENV{'${n}'} = '${v}';
+      '') service.environment)}
+
+      # Run the ExecStartPre program.  FIXME: this could be a list.
+      my $preStart = <<END_CMD;
+      ${concatStringsSep "\n" (service.serviceConfig.ExecStartPre or [])}
+      END_CMD
+      if (defined $preStart && $preStart ne "\n") {
+          print STDERR "running ExecStartPre: $preStart\n";
+          my $res = run_wait $preStart;
+          die "$0: ExecStartPre failed with status $res\n" if $res;
+      };
+
+      # Run the ExecStart program.
+      my $cmd = <<END_CMD;
+      ${service.serviceConfig.ExecStart}
+      END_CMD
+
+      print STDERR "running ExecStart: $cmd\n";
+      my $mainPid = run $cmd;
+      $ENV{'MAINPID'} = $mainPid;
+
+      # Catch SIGINT, propagate to the main program.
+      sub intHandler {
+          print STDERR "got SIGINT, stopping service...\n";
+          kill 'INT', $mainPid;
+      };
+      $SIG{'INT'} = \&intHandler;
+      $SIG{'QUIT'} = \&intHandler;
+
+      # Run the ExecStartPost program.
+      my $postStart = <<END_CMD;
+      ${concatStringsSep "\n" (service.serviceConfig.ExecStartPost or [])}
+      END_CMD
+      if (defined $postStart && $postStart ne "\n") {
+          print STDERR "running ExecStartPost: $postStart\n";
+          my $res = run_wait $postStart;
+          die "$0: ExecStartPost failed with status $res\n" if $res;
+      }
+
+      # Wait for the main program to exit.
+      die if waitpid($mainPid, 0) != $mainPid;
+      my $mainRes = $?;
+
+      # Run the ExecStopPost program.
+      my $postStop = <<END_CMD;
+      ${service.serviceConfig.ExecStopPost or ""}
+      END_CMD
+      if (defined $postStop && $postStop ne "\n") {
+          print STDERR "running ExecStopPost: $postStop\n";
+          my $res = run_wait $postStop;
+          die "$0: ExecStopPost failed with status $res\n" if $res;
+      }
+
+      exit($mainRes & 127 ? 255 : $mainRes << 8);
+    '';
+
+  opts = { config, name, ... }: {
+    options.runner = mkOption {
+    internal = true;
+    description = ''
+        A script that runs the service outside of systemd,
+        useful for testing or for using NixOS services outside
+        of NixOS.
+    '';
+    };
+    config.runner = makeScript name config;
+  };
+
+in
+
+{
+  options = {
+    systemd.services = mkOption {
+      type = with types; attrsOf (submodule opts);
+    };
+  };
+}
diff --git a/nixos/modules/testing/test-instrumentation.nix b/nixos/modules/testing/test-instrumentation.nix
new file mode 100644
index 00000000000..01447e6ada8
--- /dev/null
+++ b/nixos/modules/testing/test-instrumentation.nix
@@ -0,0 +1,141 @@
+# This module allows the test driver to connect to the virtual machine
+# via a root shell attached to port 514.
+
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  qemu-common = import ../../lib/qemu-common.nix { inherit lib pkgs; };
+in
+
+{
+
+  config = {
+
+    systemd.services.backdoor =
+      { wantedBy = [ "multi-user.target" ];
+        requires = [ "dev-hvc0.device" "dev-${qemu-common.qemuSerialDevice}.device" ];
+        after = [ "dev-hvc0.device" "dev-${qemu-common.qemuSerialDevice}.device" ];
+        script =
+          ''
+            export USER=root
+            export HOME=/root
+            export DISPLAY=:0.0
+
+            source /etc/profile
+
+            # Don't use a pager when executing backdoor
+            # actions. Because we use a tty, commands like systemctl
+            # or nix-store get confused into thinking they're running
+            # interactively.
+            export PAGER=
+
+            cd /tmp
+            exec < /dev/hvc0 > /dev/hvc0
+            while ! exec 2> /dev/${qemu-common.qemuSerialDevice}; do sleep 0.1; done
+            echo "connecting to host..." >&2
+            stty -F /dev/hvc0 raw -echo # prevent nl -> cr/nl conversion
+            echo
+            PS1= exec /bin/sh
+          '';
+        serviceConfig.KillSignal = "SIGHUP";
+      };
+
+    # Prevent agetty from being instantiated on the serial device, since it
+    # interferes with the backdoor (writes to it will randomly fail
+    # with EIO).  Likewise for hvc0.
+    systemd.services."serial-getty@${qemu-common.qemuSerialDevice}".enable = false;
+    systemd.services."serial-getty@hvc0".enable = false;
+
+    # Only set these settings when the options exist. Some tests (e.g. those
+    # that do not specify any nodes, or an empty attr set as nodes) will not
+    # have the QEMU module loaded and thuse these options can't and should not
+    # be set.
+    virtualisation = lib.optionalAttrs (options ? virtualisation.qemu) {
+      qemu = {
+        # Only use a serial console, no TTY.
+        # NOTE: optionalAttrs
+        #       test-instrumentation.nix appears to be used without qemu-vm.nix, so
+        #       we avoid defining consoles if not possible.
+        # TODO: refactor such that test-instrumentation can import qemu-vm
+        #       or declare virtualisation.qemu.console option in a module that's always imported
+        consoles = [ qemu-common.qemuSerialDevice ];
+        package  = lib.mkDefault pkgs.qemu_test;
+      };
+    };
+
+    boot.initrd.preDeviceCommands =
+      ''
+        echo 600 > /proc/sys/kernel/hung_task_timeout_secs
+      '';
+
+    boot.initrd.postDeviceCommands =
+      ''
+        # Using acpi_pm as a clock source causes the guest clock to
+        # slow down under high host load.  This is usually a bad
+        # thing, but for VM tests it should provide a bit more
+        # determinism (e.g. if the VM runs at lower speed, then
+        # timeouts in the VM should also be delayed).
+        echo acpi_pm > /sys/devices/system/clocksource/clocksource0/current_clocksource
+      '';
+
+    boot.postBootCommands =
+      ''
+        # Panic on out-of-memory conditions rather than letting the
+        # OOM killer randomly get rid of processes, since this leads
+        # to failures that are hard to diagnose.
+        echo 2 > /proc/sys/vm/panic_on_oom
+      '';
+
+    # Panic if an error occurs in stage 1 (rather than waiting for
+    # user intervention).
+    boot.kernelParams =
+      [ "console=${qemu-common.qemuSerialDevice}" "panic=1" "boot.panic_on_fail" ];
+
+    # `xwininfo' is used by the test driver to query open windows.
+    environment.systemPackages = [ pkgs.xorg.xwininfo ];
+
+    # Log everything to the serial console.
+    services.journald.extraConfig =
+      ''
+        ForwardToConsole=yes
+        MaxLevelConsole=debug
+      '';
+
+    systemd.extraConfig = ''
+      # Don't clobber the console with duplicate systemd messages.
+      ShowStatus=no
+      # Allow very slow start
+      DefaultTimeoutStartSec=300
+    '';
+    systemd.user.extraConfig = ''
+      # Allow very slow start
+      DefaultTimeoutStartSec=300
+    '';
+
+    boot.consoleLogLevel = 7;
+
+    # Prevent tests from accessing the Internet.
+    networking.defaultGateway = mkOverride 150 "";
+    networking.nameservers = mkOverride 150 [ ];
+
+    system.requiredKernelConfig = with config.lib.kernelConfig; [
+      (isYes "SERIAL_8250_CONSOLE")
+      (isYes "SERIAL_8250")
+      (isEnabled "VIRTIO_CONSOLE")
+    ];
+
+    networking.usePredictableInterfaceNames = false;
+
+    # Make it easy to log in as root when running the test interactively.
+    users.users.root.initialHashedPassword = mkOverride 150 "";
+
+    services.xserver.displayManager.job.logToJournal = true;
+
+    # Make sure we use the Guest Agent from the QEMU package for testing
+    # to reduce the closure size required for the tests.
+    services.qemuGuest.package = pkgs.qemu_test.ga;
+  };
+
+}
diff --git a/nixos/modules/virtualisation/amazon-ec2-amis.nix b/nixos/modules/virtualisation/amazon-ec2-amis.nix
new file mode 100644
index 00000000000..91b5237e337
--- /dev/null
+++ b/nixos/modules/virtualisation/amazon-ec2-amis.nix
@@ -0,0 +1,444 @@
+let self = {
+  "14.04".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-71c6f470";
+  "14.04".ap-northeast-1.x86_64-linux.pv-ebs = "ami-4dcbf84c";
+  "14.04".ap-northeast-1.x86_64-linux.pv-s3 = "ami-8fc4f68e";
+  "14.04".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-da280888";
+  "14.04".ap-southeast-1.x86_64-linux.pv-ebs = "ami-7a9dbc28";
+  "14.04".ap-southeast-1.x86_64-linux.pv-s3 = "ami-c4290996";
+  "14.04".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-ab523e91";
+  "14.04".ap-southeast-2.x86_64-linux.pv-ebs = "ami-6769055d";
+  "14.04".ap-southeast-2.x86_64-linux.pv-s3 = "ami-15533f2f";
+  "14.04".eu-central-1.x86_64-linux.hvm-ebs = "ami-ba0234a7";
+  "14.04".eu-west-1.x86_64-linux.hvm-ebs = "ami-96cb63e1";
+  "14.04".eu-west-1.x86_64-linux.pv-ebs = "ami-b48c25c3";
+  "14.04".eu-west-1.x86_64-linux.pv-s3 = "ami-06cd6571";
+  "14.04".sa-east-1.x86_64-linux.hvm-ebs = "ami-01b90e1c";
+  "14.04".sa-east-1.x86_64-linux.pv-ebs = "ami-69e35474";
+  "14.04".sa-east-1.x86_64-linux.pv-s3 = "ami-61b90e7c";
+  "14.04".us-east-1.x86_64-linux.hvm-ebs = "ami-58ba3a30";
+  "14.04".us-east-1.x86_64-linux.pv-ebs = "ami-9e0583f6";
+  "14.04".us-east-1.x86_64-linux.pv-s3 = "ami-9cbe3ef4";
+  "14.04".us-west-1.x86_64-linux.hvm-ebs = "ami-0bc3d74e";
+  "14.04".us-west-1.x86_64-linux.pv-ebs = "ami-8b1703ce";
+  "14.04".us-west-1.x86_64-linux.pv-s3 = "ami-27ccd862";
+  "14.04".us-west-2.x86_64-linux.hvm-ebs = "ami-3bf1bf0b";
+  "14.04".us-west-2.x86_64-linux.pv-ebs = "ami-259bd515";
+  "14.04".us-west-2.x86_64-linux.pv-s3 = "ami-07094037";
+
+  "14.12".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-24435f25";
+  "14.12".ap-northeast-1.x86_64-linux.pv-ebs = "ami-b0425eb1";
+  "14.12".ap-northeast-1.x86_64-linux.pv-s3 = "ami-fed3c6ff";
+  "14.12".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-6c765d3e";
+  "14.12".ap-southeast-1.x86_64-linux.pv-ebs = "ami-6a765d38";
+  "14.12".ap-southeast-1.x86_64-linux.pv-s3 = "ami-d1bf9183";
+  "14.12".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-af86f395";
+  "14.12".ap-southeast-2.x86_64-linux.pv-ebs = "ami-b386f389";
+  "14.12".ap-southeast-2.x86_64-linux.pv-s3 = "ami-69c5ae53";
+  "14.12".eu-central-1.x86_64-linux.hvm-ebs = "ami-4a497a57";
+  "14.12".eu-central-1.x86_64-linux.pv-ebs = "ami-4c497a51";
+  "14.12".eu-central-1.x86_64-linux.pv-s3 = "ami-60f2c27d";
+  "14.12".eu-west-1.x86_64-linux.hvm-ebs = "ami-d126a5a6";
+  "14.12".eu-west-1.x86_64-linux.pv-ebs = "ami-0126a576";
+  "14.12".eu-west-1.x86_64-linux.pv-s3 = "ami-deda5fa9";
+  "14.12".sa-east-1.x86_64-linux.hvm-ebs = "ami-2d239e30";
+  "14.12".sa-east-1.x86_64-linux.pv-ebs = "ami-35239e28";
+  "14.12".sa-east-1.x86_64-linux.pv-s3 = "ami-81e3519c";
+  "14.12".us-east-1.x86_64-linux.hvm-ebs = "ami-0c463a64";
+  "14.12".us-east-1.x86_64-linux.pv-ebs = "ami-ac473bc4";
+  "14.12".us-east-1.x86_64-linux.pv-s3 = "ami-00e18a68";
+  "14.12".us-west-1.x86_64-linux.hvm-ebs = "ami-ca534a8f";
+  "14.12".us-west-1.x86_64-linux.pv-ebs = "ami-3e534a7b";
+  "14.12".us-west-1.x86_64-linux.pv-s3 = "ami-2905196c";
+  "14.12".us-west-2.x86_64-linux.hvm-ebs = "ami-fb9dc3cb";
+  "14.12".us-west-2.x86_64-linux.pv-ebs = "ami-899dc3b9";
+  "14.12".us-west-2.x86_64-linux.pv-s3 = "ami-cb7f2dfb";
+
+  "15.09".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-58cac236";
+  "15.09".ap-northeast-1.x86_64-linux.hvm-s3 = "ami-39c8c057";
+  "15.09".ap-northeast-1.x86_64-linux.pv-ebs = "ami-5ac9c134";
+  "15.09".ap-northeast-1.x86_64-linux.pv-s3 = "ami-03cec66d";
+  "15.09".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-2fc2094c";
+  "15.09".ap-southeast-1.x86_64-linux.hvm-s3 = "ami-9ec308fd";
+  "15.09".ap-southeast-1.x86_64-linux.pv-ebs = "ami-95c00bf6";
+  "15.09".ap-southeast-1.x86_64-linux.pv-s3 = "ami-bfc00bdc";
+  "15.09".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-996c4cfa";
+  "15.09".ap-southeast-2.x86_64-linux.hvm-s3 = "ami-3f6e4e5c";
+  "15.09".ap-southeast-2.x86_64-linux.pv-ebs = "ami-066d4d65";
+  "15.09".ap-southeast-2.x86_64-linux.pv-s3 = "ami-cc6e4eaf";
+  "15.09".eu-central-1.x86_64-linux.hvm-ebs = "ami-3f8c6b50";
+  "15.09".eu-central-1.x86_64-linux.hvm-s3 = "ami-5b836434";
+  "15.09".eu-central-1.x86_64-linux.pv-ebs = "ami-118c6b7e";
+  "15.09".eu-central-1.x86_64-linux.pv-s3 = "ami-2c977043";
+  "15.09".eu-west-1.x86_64-linux.hvm-ebs = "ami-9cf04aef";
+  "15.09".eu-west-1.x86_64-linux.hvm-s3 = "ami-2bea5058";
+  "15.09".eu-west-1.x86_64-linux.pv-ebs = "ami-c9e852ba";
+  "15.09".eu-west-1.x86_64-linux.pv-s3 = "ami-c6f64cb5";
+  "15.09".sa-east-1.x86_64-linux.hvm-ebs = "ami-6e52df02";
+  "15.09".sa-east-1.x86_64-linux.hvm-s3 = "ami-1852df74";
+  "15.09".sa-east-1.x86_64-linux.pv-ebs = "ami-4368e52f";
+  "15.09".sa-east-1.x86_64-linux.pv-s3 = "ami-f15ad79d";
+  "15.09".us-east-1.x86_64-linux.hvm-ebs = "ami-84a6a0ee";
+  "15.09".us-east-1.x86_64-linux.hvm-s3 = "ami-06a7a16c";
+  "15.09".us-east-1.x86_64-linux.pv-ebs = "ami-a4a1a7ce";
+  "15.09".us-east-1.x86_64-linux.pv-s3 = "ami-5ba8ae31";
+  "15.09".us-west-1.x86_64-linux.hvm-ebs = "ami-22c8bb42";
+  "15.09".us-west-1.x86_64-linux.hvm-s3 = "ami-a2ccbfc2";
+  "15.09".us-west-1.x86_64-linux.pv-ebs = "ami-10cebd70";
+  "15.09".us-west-1.x86_64-linux.pv-s3 = "ami-fa30429a";
+  "15.09".us-west-2.x86_64-linux.hvm-ebs = "ami-ce57b9ae";
+  "15.09".us-west-2.x86_64-linux.hvm-s3 = "ami-2956b849";
+  "15.09".us-west-2.x86_64-linux.pv-ebs = "ami-005fb160";
+  "15.09".us-west-2.x86_64-linux.pv-s3 = "ami-cd55bbad";
+
+  "16.03".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-40619d21";
+  "16.03".ap-northeast-1.x86_64-linux.hvm-s3 = "ami-ce629eaf";
+  "16.03".ap-northeast-1.x86_64-linux.pv-ebs = "ami-ef639f8e";
+  "16.03".ap-northeast-1.x86_64-linux.pv-s3 = "ami-a1609cc0";
+  "16.03".ap-northeast-2.x86_64-linux.hvm-ebs = "ami-deca00b0";
+  "16.03".ap-northeast-2.x86_64-linux.hvm-s3 = "ami-a3b77dcd";
+  "16.03".ap-northeast-2.x86_64-linux.pv-ebs = "ami-7bcb0115";
+  "16.03".ap-northeast-2.x86_64-linux.pv-s3 = "ami-a2b77dcc";
+  "16.03".ap-south-1.x86_64-linux.hvm-ebs = "ami-0dff9562";
+  "16.03".ap-south-1.x86_64-linux.hvm-s3 = "ami-13f69c7c";
+  "16.03".ap-south-1.x86_64-linux.pv-ebs = "ami-0ef39961";
+  "16.03".ap-south-1.x86_64-linux.pv-s3 = "ami-e0c8a28f";
+  "16.03".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-5e964a3d";
+  "16.03".ap-southeast-1.x86_64-linux.hvm-s3 = "ami-4d964a2e";
+  "16.03".ap-southeast-1.x86_64-linux.pv-ebs = "ami-ec9b478f";
+  "16.03".ap-southeast-1.x86_64-linux.pv-s3 = "ami-999b47fa";
+  "16.03".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-9f7359fc";
+  "16.03".ap-southeast-2.x86_64-linux.hvm-s3 = "ami-987359fb";
+  "16.03".ap-southeast-2.x86_64-linux.pv-ebs = "ami-a2705ac1";
+  "16.03".ap-southeast-2.x86_64-linux.pv-s3 = "ami-a3705ac0";
+  "16.03".eu-central-1.x86_64-linux.hvm-ebs = "ami-17a45178";
+  "16.03".eu-central-1.x86_64-linux.hvm-s3 = "ami-f9a55096";
+  "16.03".eu-central-1.x86_64-linux.pv-ebs = "ami-c8a550a7";
+  "16.03".eu-central-1.x86_64-linux.pv-s3 = "ami-6ea45101";
+  "16.03".eu-west-1.x86_64-linux.hvm-ebs = "ami-b5b3d5c6";
+  "16.03".eu-west-1.x86_64-linux.hvm-s3 = "ami-c986e0ba";
+  "16.03".eu-west-1.x86_64-linux.pv-ebs = "ami-b083e5c3";
+  "16.03".eu-west-1.x86_64-linux.pv-s3 = "ami-3c83e54f";
+  "16.03".sa-east-1.x86_64-linux.hvm-ebs = "ami-f6eb7f9a";
+  "16.03".sa-east-1.x86_64-linux.hvm-s3 = "ami-93e773ff";
+  "16.03".sa-east-1.x86_64-linux.pv-ebs = "ami-cbb82ca7";
+  "16.03".sa-east-1.x86_64-linux.pv-s3 = "ami-abb82cc7";
+  "16.03".us-east-1.x86_64-linux.hvm-ebs = "ami-c123a3d6";
+  "16.03".us-east-1.x86_64-linux.hvm-s3 = "ami-bc25a5ab";
+  "16.03".us-east-1.x86_64-linux.pv-ebs = "ami-bd25a5aa";
+  "16.03".us-east-1.x86_64-linux.pv-s3 = "ami-a325a5b4";
+  "16.03".us-west-1.x86_64-linux.hvm-ebs = "ami-748bcd14";
+  "16.03".us-west-1.x86_64-linux.hvm-s3 = "ami-a68dcbc6";
+  "16.03".us-west-1.x86_64-linux.pv-ebs = "ami-048acc64";
+  "16.03".us-west-1.x86_64-linux.pv-s3 = "ami-208dcb40";
+  "16.03".us-west-2.x86_64-linux.hvm-ebs = "ami-8263a0e2";
+  "16.03".us-west-2.x86_64-linux.hvm-s3 = "ami-925c9ff2";
+  "16.03".us-west-2.x86_64-linux.pv-ebs = "ami-5e61a23e";
+  "16.03".us-west-2.x86_64-linux.pv-s3 = "ami-734c8f13";
+
+  # 16.09.1508.3909827
+  "16.09".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-68453b0f";
+  "16.09".ap-northeast-1.x86_64-linux.hvm-s3 = "ami-f9bec09e";
+  "16.09".ap-northeast-1.x86_64-linux.pv-ebs = "ami-254a3442";
+  "16.09".ap-northeast-1.x86_64-linux.pv-s3 = "ami-ef473988";
+  "16.09".ap-northeast-2.x86_64-linux.hvm-ebs = "ami-18ae7f76";
+  "16.09".ap-northeast-2.x86_64-linux.hvm-s3 = "ami-9eac7df0";
+  "16.09".ap-northeast-2.x86_64-linux.pv-ebs = "ami-57aa7b39";
+  "16.09".ap-northeast-2.x86_64-linux.pv-s3 = "ami-5cae7f32";
+  "16.09".ap-south-1.x86_64-linux.hvm-ebs = "ami-b3f98fdc";
+  "16.09".ap-south-1.x86_64-linux.hvm-s3 = "ami-98e690f7";
+  "16.09".ap-south-1.x86_64-linux.pv-ebs = "ami-aef98fc1";
+  "16.09".ap-south-1.x86_64-linux.pv-s3 = "ami-caf88ea5";
+  "16.09".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-80fb51e3";
+  "16.09".ap-southeast-1.x86_64-linux.hvm-s3 = "ami-2df3594e";
+  "16.09".ap-southeast-1.x86_64-linux.pv-ebs = "ami-37f05a54";
+  "16.09".ap-southeast-1.x86_64-linux.pv-s3 = "ami-27f35944";
+  "16.09".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-57ece834";
+  "16.09".ap-southeast-2.x86_64-linux.hvm-s3 = "ami-87f4f0e4";
+  "16.09".ap-southeast-2.x86_64-linux.pv-ebs = "ami-d8ede9bb";
+  "16.09".ap-southeast-2.x86_64-linux.pv-s3 = "ami-a6ebefc5";
+  "16.09".ca-central-1.x86_64-linux.hvm-ebs = "ami-9f863bfb";
+  "16.09".ca-central-1.x86_64-linux.hvm-s3 = "ami-ea85388e";
+  "16.09".ca-central-1.x86_64-linux.pv-ebs = "ami-ce8a37aa";
+  "16.09".ca-central-1.x86_64-linux.pv-s3 = "ami-448a3720";
+  "16.09".eu-central-1.x86_64-linux.hvm-ebs = "ami-1b884774";
+  "16.09".eu-central-1.x86_64-linux.hvm-s3 = "ami-b08c43df";
+  "16.09".eu-central-1.x86_64-linux.pv-ebs = "ami-888946e7";
+  "16.09".eu-central-1.x86_64-linux.pv-s3 = "ami-06874869";
+  "16.09".eu-west-1.x86_64-linux.hvm-ebs = "ami-1ed3e76d";
+  "16.09".eu-west-1.x86_64-linux.hvm-s3 = "ami-73d1e500";
+  "16.09".eu-west-1.x86_64-linux.pv-ebs = "ami-44c0f437";
+  "16.09".eu-west-1.x86_64-linux.pv-s3 = "ami-f3d8ec80";
+  "16.09".eu-west-2.x86_64-linux.hvm-ebs = "ami-2c9c9648";
+  "16.09".eu-west-2.x86_64-linux.hvm-s3 = "ami-6b9e940f";
+  "16.09".eu-west-2.x86_64-linux.pv-ebs = "ami-f1999395";
+  "16.09".eu-west-2.x86_64-linux.pv-s3 = "ami-bb9f95df";
+  "16.09".sa-east-1.x86_64-linux.hvm-ebs = "ami-a11882cd";
+  "16.09".sa-east-1.x86_64-linux.hvm-s3 = "ami-7726bc1b";
+  "16.09".sa-east-1.x86_64-linux.pv-ebs = "ami-9725bffb";
+  "16.09".sa-east-1.x86_64-linux.pv-s3 = "ami-b027bddc";
+  "16.09".us-east-1.x86_64-linux.hvm-ebs = "ami-854ca593";
+  "16.09".us-east-1.x86_64-linux.hvm-s3 = "ami-2241a834";
+  "16.09".us-east-1.x86_64-linux.pv-ebs = "ami-a441a8b2";
+  "16.09".us-east-1.x86_64-linux.pv-s3 = "ami-e841a8fe";
+  "16.09".us-east-2.x86_64-linux.hvm-ebs = "ami-3f41645a";
+  "16.09".us-east-2.x86_64-linux.hvm-s3 = "ami-804065e5";
+  "16.09".us-east-2.x86_64-linux.pv-ebs = "ami-f1466394";
+  "16.09".us-east-2.x86_64-linux.pv-s3 = "ami-05426760";
+  "16.09".us-west-1.x86_64-linux.hvm-ebs = "ami-c2efbca2";
+  "16.09".us-west-1.x86_64-linux.hvm-s3 = "ami-d71042b7";
+  "16.09".us-west-1.x86_64-linux.pv-ebs = "ami-04e8bb64";
+  "16.09".us-west-1.x86_64-linux.pv-s3 = "ami-31e9ba51";
+  "16.09".us-west-2.x86_64-linux.hvm-ebs = "ami-6449f504";
+  "16.09".us-west-2.x86_64-linux.hvm-s3 = "ami-344af654";
+  "16.09".us-west-2.x86_64-linux.pv-ebs = "ami-6d4af60d";
+  "16.09".us-west-2.x86_64-linux.pv-s3 = "ami-de48f4be";
+
+  # 17.03.885.6024dd4067
+  "17.03".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-dbd0f7bc";
+  "17.03".ap-northeast-1.x86_64-linux.hvm-s3 = "ami-7cdff81b";
+  "17.03".ap-northeast-2.x86_64-linux.hvm-ebs = "ami-c59a48ab";
+  "17.03".ap-northeast-2.x86_64-linux.hvm-s3 = "ami-0b944665";
+  "17.03".ap-south-1.x86_64-linux.hvm-ebs = "ami-4f413220";
+  "17.03".ap-south-1.x86_64-linux.hvm-s3 = "ami-864033e9";
+  "17.03".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-e08c3383";
+  "17.03".ap-southeast-1.x86_64-linux.hvm-s3 = "ami-c28f30a1";
+  "17.03".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-fca9a69f";
+  "17.03".ap-southeast-2.x86_64-linux.hvm-s3 = "ami-3daaa55e";
+  "17.03".ca-central-1.x86_64-linux.hvm-ebs = "ami-9b00bdff";
+  "17.03".ca-central-1.x86_64-linux.hvm-s3 = "ami-e800bd8c";
+  "17.03".eu-central-1.x86_64-linux.hvm-ebs = "ami-5450803b";
+  "17.03".eu-central-1.x86_64-linux.hvm-s3 = "ami-6e2efe01";
+  "17.03".eu-west-1.x86_64-linux.hvm-ebs = "ami-10754c76";
+  "17.03".eu-west-1.x86_64-linux.hvm-s3 = "ami-11734a77";
+  "17.03".eu-west-2.x86_64-linux.hvm-ebs = "ami-ff1d099b";
+  "17.03".eu-west-2.x86_64-linux.hvm-s3 = "ami-fe1d099a";
+  "17.03".sa-east-1.x86_64-linux.hvm-ebs = "ami-d95d3eb5";
+  "17.03".sa-east-1.x86_64-linux.hvm-s3 = "ami-fca2c190";
+  "17.03".us-east-1.x86_64-linux.hvm-ebs = "ami-0940c61f";
+  "17.03".us-east-1.x86_64-linux.hvm-s3 = "ami-674fc971";
+  "17.03".us-east-2.x86_64-linux.hvm-ebs = "ami-afc2e6ca";
+  "17.03".us-east-2.x86_64-linux.hvm-s3 = "ami-a1cde9c4";
+  "17.03".us-west-1.x86_64-linux.hvm-ebs = "ami-587b2138";
+  "17.03".us-west-1.x86_64-linux.hvm-s3 = "ami-70411b10";
+  "17.03".us-west-2.x86_64-linux.hvm-ebs = "ami-a93daac9";
+  "17.03".us-west-2.x86_64-linux.hvm-s3 = "ami-5139ae31";
+
+  # 17.09.2681.59661f21be6
+  "17.09".eu-west-1.x86_64-linux.hvm-ebs = "ami-a30192da";
+  "17.09".eu-west-2.x86_64-linux.hvm-ebs = "ami-295a414d";
+  "17.09".eu-west-3.x86_64-linux.hvm-ebs = "ami-8c0eb9f1";
+  "17.09".eu-central-1.x86_64-linux.hvm-ebs = "ami-266cfe49";
+  "17.09".us-east-1.x86_64-linux.hvm-ebs = "ami-40bee63a";
+  "17.09".us-east-2.x86_64-linux.hvm-ebs = "ami-9d84aff8";
+  "17.09".us-west-1.x86_64-linux.hvm-ebs = "ami-d14142b1";
+  "17.09".us-west-2.x86_64-linux.hvm-ebs = "ami-3eb40346";
+  "17.09".ca-central-1.x86_64-linux.hvm-ebs = "ami-ca8207ae";
+  "17.09".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-84bccff8";
+  "17.09".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-0dc5386f";
+  "17.09".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-89b921ef";
+  "17.09".ap-northeast-2.x86_64-linux.hvm-ebs = "ami-179b3b79";
+  "17.09".sa-east-1.x86_64-linux.hvm-ebs = "ami-4762202b";
+  "17.09".ap-south-1.x86_64-linux.hvm-ebs = "ami-4e376021";
+
+  # 18.03.132946.1caae7247b8
+  "18.03".eu-west-1.x86_64-linux.hvm-ebs = "ami-065c46ec";
+  "18.03".eu-west-2.x86_64-linux.hvm-ebs = "ami-64f31903";
+  "18.03".eu-west-3.x86_64-linux.hvm-ebs = "ami-5a8d3d27";
+  "18.03".eu-central-1.x86_64-linux.hvm-ebs = "ami-09faf9e2";
+  "18.03".us-east-1.x86_64-linux.hvm-ebs = "ami-8b3538f4";
+  "18.03".us-east-2.x86_64-linux.hvm-ebs = "ami-150b3170";
+  "18.03".us-west-1.x86_64-linux.hvm-ebs = "ami-ce06ebad";
+  "18.03".us-west-2.x86_64-linux.hvm-ebs = "ami-586c3520";
+  "18.03".ca-central-1.x86_64-linux.hvm-ebs = "ami-aca72ac8";
+  "18.03".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-aa0b4d40";
+  "18.03".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-d0f254b2";
+  "18.03".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-456511a8";
+  "18.03".ap-northeast-2.x86_64-linux.hvm-ebs = "ami-3366d15d";
+  "18.03".sa-east-1.x86_64-linux.hvm-ebs = "ami-163e1f7a";
+  "18.03".ap-south-1.x86_64-linux.hvm-ebs = "ami-6a390b05";
+
+  # 18.09.910.c15e342304a
+  "18.09".eu-west-1.x86_64-linux.hvm-ebs = "ami-0f412186fb8a0ec97";
+  "18.09".eu-west-2.x86_64-linux.hvm-ebs = "ami-0dada3805ce43c55e";
+  "18.09".eu-west-3.x86_64-linux.hvm-ebs = "ami-074df85565f2e02e2";
+  "18.09".eu-central-1.x86_64-linux.hvm-ebs = "ami-07c9b884e679df4f8";
+  "18.09".us-east-1.x86_64-linux.hvm-ebs = "ami-009c9c3f1af480ff3";
+  "18.09".us-east-2.x86_64-linux.hvm-ebs = "ami-08199961085ea8bc6";
+  "18.09".us-west-1.x86_64-linux.hvm-ebs = "ami-07aa7f56d612ddd38";
+  "18.09".us-west-2.x86_64-linux.hvm-ebs = "ami-01c84b7c368ac24d1";
+  "18.09".ca-central-1.x86_64-linux.hvm-ebs = "ami-04f66113f76198f6c";
+  "18.09".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-0892c7e24ebf2194f";
+  "18.09".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-010730f36424b0a2c";
+  "18.09".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-0cdba8e998f076547";
+  "18.09".ap-northeast-2.x86_64-linux.hvm-ebs = "ami-0400a698e6a9f4a15";
+  "18.09".sa-east-1.x86_64-linux.hvm-ebs = "ami-0e4a8a47fd6db6112";
+  "18.09".ap-south-1.x86_64-linux.hvm-ebs = "ami-0880a678d3f555313";
+
+  # 19.03.172286.8ea36d73256
+  "19.03".eu-west-1.x86_64-linux.hvm-ebs = "ami-0fe40176548ff0940";
+  "19.03".eu-west-2.x86_64-linux.hvm-ebs = "ami-03a40fd3a02fe95ba";
+  "19.03".eu-west-3.x86_64-linux.hvm-ebs = "ami-0436f9da0f20a638e";
+  "19.03".eu-central-1.x86_64-linux.hvm-ebs = "ami-0022b8ea9efde5de4";
+  "19.03".us-east-1.x86_64-linux.hvm-ebs = "ami-0efc58fb70ae9a217";
+  "19.03".us-east-2.x86_64-linux.hvm-ebs = "ami-0abf711b1b34da1af";
+  "19.03".us-west-1.x86_64-linux.hvm-ebs = "ami-07d126e8838c40ec5";
+  "19.03".us-west-2.x86_64-linux.hvm-ebs = "ami-03f8a737546e47fb0";
+  "19.03".ca-central-1.x86_64-linux.hvm-ebs = "ami-03f9fd0ef2e035ede";
+  "19.03".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-0cff66114c652c262";
+  "19.03".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-054c73a7f8d773ea9";
+  "19.03".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-00db62688900456a4";
+  "19.03".ap-northeast-2.x86_64-linux.hvm-ebs = "ami-0485cdd1a5fdd2117";
+  "19.03".sa-east-1.x86_64-linux.hvm-ebs = "ami-0c6a43c6e0ad1f4e2";
+  "19.03".ap-south-1.x86_64-linux.hvm-ebs = "ami-0303deb1b5890f878";
+
+  # 19.09.2243.84af403f54f
+  "19.09".eu-west-1.x86_64-linux.hvm-ebs = "ami-071082f0fa035374f";
+  "19.09".eu-west-2.x86_64-linux.hvm-ebs = "ami-0d9dc33c54d1dc4c3";
+  "19.09".eu-west-3.x86_64-linux.hvm-ebs = "ami-09566799591d1bfed";
+  "19.09".eu-central-1.x86_64-linux.hvm-ebs = "ami-015f8efc2be419b79";
+  "19.09".eu-north-1.x86_64-linux.hvm-ebs = "ami-07fc0a32d885e01ed";
+  "19.09".us-east-1.x86_64-linux.hvm-ebs = "ami-03330d8b51287412f";
+  "19.09".us-east-2.x86_64-linux.hvm-ebs = "ami-0518b4c84972e967f";
+  "19.09".us-west-1.x86_64-linux.hvm-ebs = "ami-06ad07e61a353b4a6";
+  "19.09".us-west-2.x86_64-linux.hvm-ebs = "ami-0e31e30925cf3ce4e";
+  "19.09".ca-central-1.x86_64-linux.hvm-ebs = "ami-07df50fc76702a36d";
+  "19.09".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-0f71ae5d4b0b78d95";
+  "19.09".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-057bbf2b4bd62d210";
+  "19.09".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-02a62555ca182fb5b";
+  "19.09".ap-northeast-2.x86_64-linux.hvm-ebs = "ami-0219dde0e6b7b7b93";
+  "19.09".ap-south-1.x86_64-linux.hvm-ebs = "ami-066f7f2a895c821a1";
+  "19.09".ap-east-1.x86_64-linux.hvm-ebs = "ami-055b2348db2827ff1";
+  "19.09".sa-east-1.x86_64-linux.hvm-ebs = "ami-018aab68377227e06";
+
+  # 20.03.1554.94e39623a49
+  "20.03".eu-west-1.x86_64-linux.hvm-ebs = "ami-02c34db5766cc7013";
+  "20.03".eu-west-2.x86_64-linux.hvm-ebs = "ami-0e32bd8c7853883f1";
+  "20.03".eu-west-3.x86_64-linux.hvm-ebs = "ami-061edb1356c1d69fd";
+  "20.03".eu-central-1.x86_64-linux.hvm-ebs = "ami-0a1a94722dcbff94c";
+  "20.03".eu-north-1.x86_64-linux.hvm-ebs = "ami-02699abfacbb6464b";
+  "20.03".us-east-1.x86_64-linux.hvm-ebs = "ami-0c5e7760748b74e85";
+  "20.03".us-east-2.x86_64-linux.hvm-ebs = "ami-030296bb256764655";
+  "20.03".us-west-1.x86_64-linux.hvm-ebs = "ami-050be818e0266b741";
+  "20.03".us-west-2.x86_64-linux.hvm-ebs = "ami-06562f78dca68eda2";
+  "20.03".ca-central-1.x86_64-linux.hvm-ebs = "ami-02365684a173255c7";
+  "20.03".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-0dbf353e168d155f7";
+  "20.03".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-04c0f3a75f63daddd";
+  "20.03".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-093d9cc49c191eb6c";
+  "20.03".ap-northeast-2.x86_64-linux.hvm-ebs = "ami-0087df91a7b6ebd45";
+  "20.03".ap-south-1.x86_64-linux.hvm-ebs = "ami-0a1a6b569af04af9d";
+  "20.03".ap-east-1.x86_64-linux.hvm-ebs = "ami-0d18fdd309cdefa86";
+  "20.03".sa-east-1.x86_64-linux.hvm-ebs = "ami-09859378158ae971d";
+  # 20.03.2351.f8248ab6d9e-aarch64-linux
+  "20.03".eu-west-1.aarch64-linux.hvm-ebs = "ami-0a4c46dfdfe921aab";
+  "20.03".eu-west-2.aarch64-linux.hvm-ebs = "ami-0b47871912b7d36f9";
+  "20.03".eu-west-3.aarch64-linux.hvm-ebs = "ami-01031e1aa505b8935";
+  "20.03".eu-central-1.aarch64-linux.hvm-ebs = "ami-0bb4669de1f477fd1";
+  # missing "20.03".eu-north-1.aarch64-linux.hvm-ebs = "ami-";
+  "20.03".us-east-1.aarch64-linux.hvm-ebs = "ami-01d2de16a1878271c";
+  "20.03".us-east-2.aarch64-linux.hvm-ebs = "ami-0eade0158b1ff49c0";
+  "20.03".us-west-1.aarch64-linux.hvm-ebs = "ami-0913bf30cb9a764a4";
+  "20.03".us-west-2.aarch64-linux.hvm-ebs = "ami-073449580ff8e82b5";
+  "20.03".ca-central-1.aarch64-linux.hvm-ebs = "ami-050f2e923c4d703c0";
+  "20.03".ap-southeast-1.aarch64-linux.hvm-ebs = "ami-0d11ef6705a9a11a7";
+  "20.03".ap-southeast-2.aarch64-linux.hvm-ebs = "ami-05446a2f818cd3263";
+  "20.03".ap-northeast-1.aarch64-linux.hvm-ebs = "ami-0c057f010065d2453";
+  "20.03".ap-northeast-2.aarch64-linux.hvm-ebs = "ami-0e90eda7f24eb33ab";
+  "20.03".ap-south-1.aarch64-linux.hvm-ebs = "ami-03ba7e9f093f568bc";
+  "20.03".sa-east-1.aarch64-linux.hvm-ebs = "ami-0a8344c6ce6d0c902";
+
+  # 20.09.2016.19db3e5ea27
+  "20.09".eu-west-1.x86_64-linux.hvm-ebs = "ami-0057cb7d614329fa2";
+  "20.09".eu-west-2.x86_64-linux.hvm-ebs = "ami-0d46f16e0bb0ec8fd";
+  "20.09".eu-west-3.x86_64-linux.hvm-ebs = "ami-0e8985c3ea42f87fe";
+  "20.09".eu-central-1.x86_64-linux.hvm-ebs = "ami-0eed77c38432886d2";
+  "20.09".eu-north-1.x86_64-linux.hvm-ebs = "ami-0be5bcadd632bea14";
+  "20.09".us-east-1.x86_64-linux.hvm-ebs = "ami-0a2cce52b42daccc8";
+  "20.09".us-east-2.x86_64-linux.hvm-ebs = "ami-09378bf487b07a4d8";
+  "20.09".us-west-1.x86_64-linux.hvm-ebs = "ami-09b4337b2a9e77485";
+  "20.09".us-west-2.x86_64-linux.hvm-ebs = "ami-081d3bb5fbee0a1ac";
+  "20.09".ca-central-1.x86_64-linux.hvm-ebs = "ami-020c24c6c607e7ac7";
+  "20.09".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-08f648d5db009e67d";
+  "20.09".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-0be390efaccbd40f9";
+  "20.09".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-0c3311601cbe8f927";
+  "20.09".ap-northeast-2.x86_64-linux.hvm-ebs = "ami-0020146701f4d56cf";
+  "20.09".ap-south-1.x86_64-linux.hvm-ebs = "ami-0117e2bd876bb40d1";
+  "20.09".ap-east-1.x86_64-linux.hvm-ebs = "ami-0c42f97e5b1fda92f";
+  "20.09".sa-east-1.x86_64-linux.hvm-ebs = "ami-021637976b094959d";
+  # 20.09.2016.19db3e5ea27-aarch64-linux
+  "20.09".eu-west-1.aarch64-linux.hvm-ebs = "ami-00a02608ff45ff8f9";
+  "20.09".eu-west-2.aarch64-linux.hvm-ebs = "ami-0e991d0f8dca21e20";
+  "20.09".eu-west-3.aarch64-linux.hvm-ebs = "ami-0d18eec4dc48c6f3b";
+  "20.09".eu-central-1.aarch64-linux.hvm-ebs = "ami-01691f25d08f48c9e";
+  "20.09".eu-north-1.aarch64-linux.hvm-ebs = "ami-09bb5aabe567ec6f4";
+  "20.09".us-east-1.aarch64-linux.hvm-ebs = "ami-0504bd006f9eaae42";
+  "20.09".us-east-2.aarch64-linux.hvm-ebs = "ami-00f0f8f2ab2d695ad";
+  "20.09".us-west-1.aarch64-linux.hvm-ebs = "ami-02d147d2cb992f878";
+  "20.09".us-west-2.aarch64-linux.hvm-ebs = "ami-07f40006cf4d4820e";
+  "20.09".ca-central-1.aarch64-linux.hvm-ebs = "ami-0e5f563919a987894";
+  "20.09".ap-southeast-1.aarch64-linux.hvm-ebs = "ami-083e35d1acecae5c1";
+  "20.09".ap-southeast-2.aarch64-linux.hvm-ebs = "ami-052cdc008b245b067";
+  "20.09".ap-northeast-1.aarch64-linux.hvm-ebs = "ami-05e137f373bd72c0c";
+  "20.09".ap-northeast-2.aarch64-linux.hvm-ebs = "ami-020791fe4c32f851a";
+  "20.09".ap-south-1.aarch64-linux.hvm-ebs = "ami-0285bb96a0f2c3955";
+  "20.09".sa-east-1.aarch64-linux.hvm-ebs = "ami-0a55ab650c32be058";
+
+
+  # 21.05.740.aa576357673
+  "21.05".eu-west-1.x86_64-linux.hvm-ebs = "ami-048dbc738074a3083";
+  "21.05".eu-west-2.x86_64-linux.hvm-ebs = "ami-0234cf81fec68315d";
+  "21.05".eu-west-3.x86_64-linux.hvm-ebs = "ami-020e459baf709107d";
+  "21.05".eu-central-1.x86_64-linux.hvm-ebs = "ami-0857d5d1309ab8b77";
+  "21.05".eu-north-1.x86_64-linux.hvm-ebs = "ami-05403e3ae53d3716f";
+  "21.05".us-east-1.x86_64-linux.hvm-ebs = "ami-0d3002ba40b5b9897";
+  "21.05".us-east-2.x86_64-linux.hvm-ebs = "ami-069a0ca1bde6dea52";
+  "21.05".us-west-1.x86_64-linux.hvm-ebs = "ami-0b415460a84bcf9bc";
+  "21.05".us-west-2.x86_64-linux.hvm-ebs = "ami-093cba49754abd7f8";
+  "21.05".ca-central-1.x86_64-linux.hvm-ebs = "ami-065c13e1d52d60b33";
+  "21.05".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-04f570c70ff9b665e";
+  "21.05".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-02a3d1df595df5ef6";
+  "21.05".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-027836fddb5c56012";
+  "21.05".ap-northeast-2.x86_64-linux.hvm-ebs = "ami-0edacd41dc7700c39";
+  "21.05".ap-south-1.x86_64-linux.hvm-ebs = "ami-0b279b5bb55288059";
+  "21.05".ap-east-1.x86_64-linux.hvm-ebs = "ami-06dc98082bc55c1fc";
+  "21.05".sa-east-1.x86_64-linux.hvm-ebs = "ami-04737dd49b98936c6";
+
+  # 21.11.333823.96b4157790f-x86_64-linux
+  "21.11".eu-west-1.x86_64-linux.hvm-ebs = "ami-01d0304a712f2f3f0";
+  "21.11".eu-west-2.x86_64-linux.hvm-ebs = "ami-00e828bfc1e5d09ac";
+  "21.11".eu-west-3.x86_64-linux.hvm-ebs = "ami-0e1ea64430d8103f2";
+  "21.11".eu-central-1.x86_64-linux.hvm-ebs = "ami-0fcf28c07e86142c5";
+  "21.11".eu-north-1.x86_64-linux.hvm-ebs = "ami-0ee83a3c6590fd6b1";
+  "21.11".us-east-1.x86_64-linux.hvm-ebs = "ami-099756bfda4540da0";
+  "21.11".us-east-2.x86_64-linux.hvm-ebs = "ami-0b20a80b82052d23f";
+  "21.11".us-west-1.x86_64-linux.hvm-ebs = "ami-088ea590004b01752";
+  "21.11".us-west-2.x86_64-linux.hvm-ebs = "ami-0025b9d4831b911a7";
+  "21.11".ca-central-1.x86_64-linux.hvm-ebs = "ami-0e67089f898e74443";
+  "21.11".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-0dc8d718279d3402d";
+  "21.11".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-0155e842329970187";
+  "21.11".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-07c95eda953bf5435";
+  "21.11".ap-northeast-2.x86_64-linux.hvm-ebs = "ami-04167df3cd952b3bd";
+  "21.11".ap-south-1.x86_64-linux.hvm-ebs = "ami-0680e05531b3db677";
+  "21.11".ap-east-1.x86_64-linux.hvm-ebs = "ami-0835a3e481dc240f9";
+  "21.11".sa-east-1.x86_64-linux.hvm-ebs = "ami-0f7c354c421348e51";
+
+  # 21.11.333823.96b4157790f-aarch64-linux
+  "21.11".eu-west-1.aarch64-linux.hvm-ebs = "ami-048f3eea6a12c4b3b";
+  "21.11".eu-west-2.aarch64-linux.hvm-ebs = "ami-0e6f18f2009806add";
+  "21.11".eu-west-3.aarch64-linux.hvm-ebs = "ami-0a28d593f5e938d80";
+  "21.11".eu-central-1.aarch64-linux.hvm-ebs = "ami-0b9c95d926ab9474c";
+  "21.11".eu-north-1.aarch64-linux.hvm-ebs = "ami-0f2d400b4a2368a1a";
+  "21.11".us-east-1.aarch64-linux.hvm-ebs = "ami-05afb75585567d386";
+  "21.11".us-east-2.aarch64-linux.hvm-ebs = "ami-07f360673c2fccf8d";
+  "21.11".us-west-1.aarch64-linux.hvm-ebs = "ami-0a6892c61d85774db";
+  "21.11".us-west-2.aarch64-linux.hvm-ebs = "ami-04eaf20283432e852";
+  "21.11".ca-central-1.aarch64-linux.hvm-ebs = "ami-036b69828502e7fdf";
+  "21.11".ap-southeast-1.aarch64-linux.hvm-ebs = "ami-0d52e51e68b6954ef";
+  "21.11".ap-southeast-2.aarch64-linux.hvm-ebs = "ami-000a3019e003f4fb9";
+  "21.11".ap-northeast-1.aarch64-linux.hvm-ebs = "ami-09b0c7928780e25b6";
+  "21.11".ap-northeast-2.aarch64-linux.hvm-ebs = "ami-05f80f3c83083ff62";
+  "21.11".ap-south-1.aarch64-linux.hvm-ebs = "ami-05b2a3ff8489c3f59";
+  "21.11".ap-east-1.aarch64-linux.hvm-ebs = "ami-0aa3b50a4f2822a00";
+  "21.11".sa-east-1.aarch64-linux.hvm-ebs = "ami-00f68eff453d3fe69";
+
+  latest = self."21.11";
+}; in self
diff --git a/nixos/modules/virtualisation/amazon-image.nix b/nixos/modules/virtualisation/amazon-image.nix
new file mode 100644
index 00000000000..9a56b695015
--- /dev/null
+++ b/nixos/modules/virtualisation/amazon-image.nix
@@ -0,0 +1,180 @@
+# Configuration for Amazon EC2 instances. (Note that this file is a
+# misnomer - it should be "amazon-config.nix" or so, not
+# "amazon-image.nix", since it's used not only to build images but
+# also to reconfigure instances. However, we can't rename it because
+# existing "configuration.nix" files on EC2 instances refer to it.)
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.ec2;
+  metadataFetcher = import ./ec2-metadata-fetcher.nix {
+    inherit (pkgs) curl;
+    targetRoot = "$targetRoot/";
+    wgetExtraOptions = "-q";
+  };
+in
+
+{
+  imports = [
+    ../profiles/headless.nix
+    # Note: While we do use the headless profile, we also explicitly
+    # turn on the serial console on ttyS0 below. This is because
+    # AWS does support accessing the serial console:
+    # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configure-access-to-serial-console.html
+    ./ec2-data.nix
+    ./amazon-init.nix
+  ];
+
+  config = {
+
+    assertions = [
+      { assertion = cfg.hvm;
+        message = "Paravirtualized EC2 instances are no longer supported.";
+      }
+      { assertion = cfg.efi -> cfg.hvm;
+        message = "EC2 instances using EFI must be HVM instances.";
+      }
+      { assertion = versionOlder config.boot.kernelPackages.kernel.version "5.15";
+        message = "ENA driver fails to build with kernel >= 5.15";
+      }
+    ];
+
+    boot.kernelPackages = pkgs.linuxKernel.packages.linux_5_10;
+
+    boot.growPartition = cfg.hvm;
+
+    fileSystems."/" = mkIf (!cfg.zfs.enable) {
+      device = "/dev/disk/by-label/nixos";
+      fsType = "ext4";
+      autoResize = true;
+    };
+
+    fileSystems."/boot" = mkIf (cfg.efi || cfg.zfs.enable) {
+      # The ZFS image uses a partition labeled ESP whether or not we're
+      # booting with EFI.
+      device = "/dev/disk/by-label/ESP";
+      fsType = "vfat";
+    };
+
+    services.zfs.expandOnBoot = mkIf cfg.zfs.enable "all";
+
+    boot.zfs.devNodes = mkIf cfg.zfs.enable "/dev/";
+
+    boot.extraModulePackages = [
+      config.boot.kernelPackages.ena
+    ];
+    boot.initrd.kernelModules = [ "xen-blkfront" "xen-netfront" ];
+    boot.initrd.availableKernelModules = [ "ixgbevf" "ena" "nvme" ];
+    boot.kernelParams = mkIf cfg.hvm [ "console=ttyS0,115200n8" "random.trust_cpu=on" ];
+
+    # Prevent the nouveau kernel module from being loaded, as it
+    # interferes with the nvidia/nvidia-uvm modules needed for CUDA.
+    # Also blacklist xen_fbfront to prevent a 30 second delay during
+    # boot.
+    boot.blacklistedKernelModules = [ "nouveau" "xen_fbfront" ];
+
+    # Generate a GRUB menu.  Amazon's pv-grub uses this to boot our kernel/initrd.
+    boot.loader.grub.version = if cfg.hvm then 2 else 1;
+    boot.loader.grub.device = if (cfg.hvm && !cfg.efi) then "/dev/xvda" else "nodev";
+    boot.loader.grub.extraPerEntryConfig = mkIf (!cfg.hvm) "root (hd0)";
+    boot.loader.grub.efiSupport = cfg.efi;
+    boot.loader.grub.efiInstallAsRemovable = cfg.efi;
+    boot.loader.timeout = 1;
+    boot.loader.grub.extraConfig = ''
+      serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1
+      terminal_output console serial
+      terminal_input console serial
+    '';
+
+    boot.initrd.network.enable = true;
+
+    # Mount all formatted ephemeral disks and activate all swap devices.
+    # We cannot do this with the ‘fileSystems’ and ‘swapDevices’ options
+    # because the set of devices is dependent on the instance type
+    # (e.g. "m1.small" has one ephemeral filesystem and one swap device,
+    # while "m1.large" has two ephemeral filesystems and no swap
+    # devices).  Also, put /tmp and /var on /disk0, since it has a lot
+    # more space than the root device.  Similarly, "move" /nix to /disk0
+    # by layering a unionfs-fuse mount on top of it so we have a lot more space for
+    # Nix operations.
+    boot.initrd.postMountCommands =
+      ''
+        ${metadataFetcher}
+
+        diskNr=0
+        diskForUnionfs=
+        for device in /dev/xvd[abcde]*; do
+            if [ "$device" = /dev/xvda -o "$device" = /dev/xvda1 ]; then continue; fi
+            fsType=$(blkid -o value -s TYPE "$device" || true)
+            if [ "$fsType" = swap ]; then
+                echo "activating swap device $device..."
+                swapon "$device" || true
+            elif [ "$fsType" = ext3 ]; then
+                mp="/disk$diskNr"
+                diskNr=$((diskNr + 1))
+                if mountFS "$device" "$mp" "" ext3; then
+                    if [ -z "$diskForUnionfs" ]; then diskForUnionfs="$mp"; fi
+                fi
+            else
+                echo "skipping unknown device type $device"
+            fi
+        done
+
+        if [ -n "$diskForUnionfs" ]; then
+            mkdir -m 755 -p $targetRoot/$diskForUnionfs/root
+
+            mkdir -m 1777 -p $targetRoot/$diskForUnionfs/root/tmp $targetRoot/tmp
+            mount --bind $targetRoot/$diskForUnionfs/root/tmp $targetRoot/tmp
+
+            if [ "$(cat "$metaDir/ami-manifest-path")" != "(unknown)" ]; then
+                mkdir -m 755 -p $targetRoot/$diskForUnionfs/root/var $targetRoot/var
+                mount --bind $targetRoot/$diskForUnionfs/root/var $targetRoot/var
+
+                mkdir -p /unionfs-chroot/ro-nix
+                mount --rbind $targetRoot/nix /unionfs-chroot/ro-nix
+
+                mkdir -m 755 -p $targetRoot/$diskForUnionfs/root/nix
+                mkdir -p /unionfs-chroot/rw-nix
+                mount --rbind $targetRoot/$diskForUnionfs/root/nix /unionfs-chroot/rw-nix
+
+                unionfs -o allow_other,cow,nonempty,chroot=/unionfs-chroot,max_files=32768 /rw-nix=RW:/ro-nix=RO $targetRoot/nix
+            fi
+        fi
+      '';
+
+    boot.initrd.extraUtilsCommands =
+      ''
+        # We need swapon in the initrd.
+        copy_bin_and_libs ${pkgs.util-linux}/sbin/swapon
+      '';
+
+    # 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";
+
+    # Enable the serial console on ttyS0
+    systemd.services."serial-getty@ttyS0".enable = true;
+
+    # Creates symlinks for block device names.
+    services.udev.packages = [ pkgs.amazon-ec2-utils ];
+
+    # Force getting the hostname from EC2.
+    networking.hostName = mkDefault "";
+
+    # Always include cryptsetup so that Charon can use it.
+    environment.systemPackages = [ pkgs.cryptsetup ];
+
+    boot.initrd.supportedFilesystems = [ "unionfs-fuse" ];
+
+    # EC2 has its own NTP server provided by the hypervisor
+    networking.timeServers = [ "169.254.169.123" ];
+
+    # udisks has become too bloated to have in a headless system
+    # (e.g. it depends on GTK).
+    services.udisks2.enable = false;
+  };
+}
diff --git a/nixos/modules/virtualisation/amazon-init.nix b/nixos/modules/virtualisation/amazon-init.nix
new file mode 100644
index 00000000000..4f2f8df90eb
--- /dev/null
+++ b/nixos/modules/virtualisation/amazon-init.nix
@@ -0,0 +1,87 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.virtualisation.amazon-init;
+
+  script = ''
+    #!${pkgs.runtimeShell} -eu
+
+    echo "attempting to fetch configuration from EC2 user data..."
+
+    export HOME=/root
+    export PATH=${pkgs.lib.makeBinPath [ config.nix.package pkgs.systemd pkgs.gnugrep pkgs.git pkgs.gnutar pkgs.gzip pkgs.gnused pkgs.xz config.system.build.nixos-rebuild]}:$PATH
+    export NIX_PATH=nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos:nixos-config=/etc/nixos/configuration.nix:/nix/var/nix/profiles/per-user/root/channels
+
+    userData=/etc/ec2-metadata/user-data
+
+    # Check if user-data looks like a shell script and execute it with the
+    # runtime shell if it does. Otherwise treat it as a nixos configuration
+    # expression
+    if IFS= LC_ALL=C read -rN2 shebang < $userData && [ "$shebang" = '#!' ]; then
+      # NB: we cannot chmod the $userData file, this is why we execute it via
+      # `pkgs.runtimeShell`. This means we have only limited support for shell
+      # scripts compatible with the `pkgs.runtimeShell`.
+      exec ${pkgs.runtimeShell} $userData
+    fi
+
+    if [ -s "$userData" ]; then
+      # If the user-data looks like it could be a nix expression,
+      # copy it over. Also, look for a magic three-hash comment and set
+      # that as the channel.
+      if sed '/^\(#\|SSH_HOST_.*\)/d' < "$userData" | grep -q '\S'; then
+        channels="$(grep '^###' "$userData" | sed 's|###\s*||')"
+        while IFS= read -r channel; do
+          echo "writing channel: $channel"
+        done < <(printf "%s\n" "$channels")
+
+        if [[ -n "$channels" ]]; then
+          printf "%s" "$channels" > /root/.nix-channels
+          nix-channel --update
+        fi
+
+        echo "setting configuration from EC2 user data"
+        cp "$userData" /etc/nixos/configuration.nix
+      else
+        echo "user data does not appear to be a Nix expression; ignoring"
+        exit
+      fi
+    else
+      echo "no user data is available"
+      exit
+    fi
+
+    nixos-rebuild switch
+  '';
+in {
+
+  options.virtualisation.amazon-init = {
+    enable = mkOption {
+      default = true;
+      type = types.bool;
+      description = ''
+        Enable or disable the amazon-init service.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.amazon-init = {
+      inherit script;
+      description = "Reconfigure the system from EC2 userdata on startup";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "multi-user.target" ];
+      requires = [ "network-online.target" ];
+
+      restartIfChanged = false;
+      unitConfig.X-StopOnRemoval = false;
+
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/amazon-options.nix b/nixos/modules/virtualisation/amazon-options.nix
new file mode 100644
index 00000000000..0465571ca92
--- /dev/null
+++ b/nixos/modules/virtualisation/amazon-options.nix
@@ -0,0 +1,74 @@
+{ config, lib, pkgs, ... }:
+let
+  inherit (lib) literalExpression types;
+in {
+  options = {
+    ec2 = {
+      zfs = {
+        enable = lib.mkOption {
+          default = false;
+          internal = true;
+          description = ''
+            Whether the EC2 instance uses a ZFS root.
+          '';
+        };
+
+        datasets = lib.mkOption {
+          description = ''
+            Datasets to create under the `tank` and `boot` zpools.
+
+            **NOTE:** This option is used only at image creation time, and
+            does not attempt to declaratively create or manage datasets
+            on an existing system.
+          '';
+
+          default = {};
+
+          type = types.attrsOf (types.submodule {
+            options = {
+              mount = lib.mkOption {
+                description = "Where to mount this dataset.";
+                type = types.nullOr types.string;
+                default = null;
+              };
+
+              properties = lib.mkOption {
+                description = "Properties to set on this dataset.";
+                type = types.attrsOf types.string;
+                default = {};
+              };
+            };
+          });
+        };
+      };
+      hvm = lib.mkOption {
+        default = lib.versionAtLeast config.system.stateVersion "17.03";
+        internal = true;
+        description = ''
+          Whether the EC2 instance is a HVM instance.
+        '';
+      };
+      efi = lib.mkOption {
+        default = pkgs.stdenv.hostPlatform.isAarch64;
+        defaultText = literalExpression "pkgs.stdenv.hostPlatform.isAarch64";
+        internal = true;
+        description = ''
+          Whether the EC2 instance is using EFI.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf config.ec2.zfs.enable {
+    networking.hostId = lib.mkDefault "00000000";
+
+    fileSystems = let
+      mountable = lib.filterAttrs (_: value: ((value.mount or null) != null)) config.ec2.zfs.datasets;
+    in lib.mapAttrs'
+      (dataset: opts: lib.nameValuePair opts.mount {
+        device = dataset;
+        fsType = "zfs";
+      })
+      mountable;
+  };
+}
diff --git a/nixos/modules/virtualisation/anbox.nix b/nixos/modules/virtualisation/anbox.nix
new file mode 100644
index 00000000000..a4da62eb5f7
--- /dev/null
+++ b/nixos/modules/virtualisation/anbox.nix
@@ -0,0 +1,138 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.virtualisation.anbox;
+  kernelPackages = config.boot.kernelPackages;
+  addrOpts = v: addr: pref: name: {
+    address = mkOption {
+      default = addr;
+      type = types.str;
+      description = ''
+        IPv${toString v} ${name} address.
+      '';
+    };
+
+    prefixLength = mkOption {
+      default = pref;
+      type = types.addCheck types.int (n: n >= 0 && n <= (if v == 4 then 32 else 128));
+      description = ''
+        Subnet mask of the ${name} address, specified as the number of
+        bits in the prefix (<literal>${if v == 4 then "24" else "64"}</literal>).
+      '';
+    };
+  };
+
+in
+
+{
+
+  options.virtualisation.anbox = {
+
+    enable = mkEnableOption "Anbox";
+
+    image = mkOption {
+      default = pkgs.anbox.image;
+      defaultText = literalExpression "pkgs.anbox.image";
+      type = types.package;
+      description = ''
+        Base android image for Anbox.
+      '';
+    };
+
+    extraInit = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Extra shell commands to be run inside the container image during init.
+      '';
+    };
+
+    ipv4 = {
+      container = addrOpts 4 "192.168.250.2" 24 "Container";
+      gateway = addrOpts 4 "192.168.250.1" 24 "Host";
+
+      dns = mkOption {
+        default = "1.1.1.1";
+        type = types.str;
+        description = ''
+          Container DNS server.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = singleton {
+      assertion = versionAtLeast (getVersion config.boot.kernelPackages.kernel) "4.18";
+      message = "Anbox needs user namespace support to work properly";
+    };
+
+    environment.systemPackages = with pkgs; [ anbox ];
+
+    boot.kernelModules = [ "ashmem_linux" "binder_linux" ];
+    boot.extraModulePackages = [ kernelPackages.anbox ];
+
+    services.udev.extraRules = ''
+      KERNEL=="ashmem", NAME="%k", MODE="0666"
+      KERNEL=="binder*", NAME="%k", MODE="0666"
+    '';
+
+    virtualisation.lxc.enable = true;
+    networking.bridges.anbox0.interfaces = [];
+    networking.interfaces.anbox0.ipv4.addresses = [ cfg.ipv4.gateway ];
+
+    networking.nat = {
+      enable = true;
+      internalInterfaces = [ "anbox0" ];
+    };
+
+    systemd.services.anbox-container-manager = let
+      anboxloc = "/var/lib/anbox";
+    in {
+      description = "Anbox Container Management Daemon";
+
+      environment.XDG_RUNTIME_DIR="${anboxloc}";
+
+      wantedBy = [ "multi-user.target" ];
+      preStart = let
+        initsh = pkgs.writeText "nixos-init" (''
+          #!/system/bin/sh
+          setprop nixos.version ${config.system.nixos.version}
+
+          # we don't have radio
+          setprop ro.radio.noril yes
+          stop ril-daemon
+
+          # speed up boot
+          setprop debug.sf.nobootanimation 1
+        '' + cfg.extraInit);
+        initshloc = "${anboxloc}/rootfs-overlay/system/etc/init.goldfish.sh";
+      in ''
+        mkdir -p ${anboxloc}
+        mkdir -p $(dirname ${initshloc})
+        [ -f ${initshloc} ] && rm ${initshloc}
+        cp ${initsh} ${initshloc}
+        chown 100000:100000 ${initshloc}
+        chmod +x ${initshloc}
+      '';
+
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.anbox}/bin/anbox container-manager \
+            --data-path=${anboxloc} \
+            --android-image=${cfg.image} \
+            --container-network-address=${cfg.ipv4.container.address} \
+            --container-network-gateway=${cfg.ipv4.gateway.address} \
+            --container-network-dns-servers=${cfg.ipv4.dns} \
+            --use-rootfs-overlay \
+            --privileged
+        '';
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/virtualisation/azure-agent-entropy.patch b/nixos/modules/virtualisation/azure-agent-entropy.patch
new file mode 100644
index 00000000000..2a7ad08a4af
--- /dev/null
+++ b/nixos/modules/virtualisation/azure-agent-entropy.patch
@@ -0,0 +1,17 @@
+--- 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
new file mode 100644
index 00000000000..bd8c7f8c1ee
--- /dev/null
+++ b/nixos/modules/virtualisation/azure-agent.nix
@@ -0,0 +1,198 @@
+{ 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 ];
+
+    buildInputs = [ 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
+  '';
+
+in
+
+{
+
+  ###### interface
+
+  options.virtualisation.azure.agent = {
+    enable = mkOption {
+      default = false;
+      description = "Whether to enable the Windows Azure Linux Agent.";
+    };
+    verboseLogging = mkOption {
+      default = false;
+      description = "Whether to enable verbose logging.";
+    };
+    mountResourceDisk = mkOption {
+      default = true;
+      description = "Whether the agent should format (ext4) and mount the resource disk to /mnt/resource.";
+    };
+  };
+
+  ###### implementation
+
+  config = 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";
+    } ];
+
+    boot.initrd.kernelModules = [ "ata_piix" ];
+    networking.firewall.allowedUDPPorts = [ 68 ];
+
+
+    environment.etc."waagent.conf".text = ''
+        #
+        # Windows Azure Linux Agent Configuration
+        #
+
+        Role.StateConsumer=${provisionedHook}
+
+        # Enable instance creation
+        Provisioning.Enabled=y
+
+        # Password authentication for root account will be unavailable.
+        Provisioning.DeleteRootPassword=n
+
+        # Generate fresh host key pair.
+        Provisioning.RegenerateSshHostKeyPair=n
+
+        # Supported values are "rsa", "dsa" and "ecdsa".
+        Provisioning.SshHostKeyPairType=ed25519
+
+        # Monitor host name changes and publish changes via DHCP requests.
+        Provisioning.MonitorHostName=y
+
+        # Decode CustomData from Base64.
+        Provisioning.DecodeCustomData=n
+
+        # Execute CustomData after provisioning.
+        Provisioning.ExecuteCustomData=n
+
+        # Format if unformatted. If 'n', resource disk will not be mounted.
+        ResourceDisk.Format=${if cfg.mountResourceDisk then "y" else "n"}
+
+        # File system on the resource disk
+        # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here.
+        ResourceDisk.Filesystem=ext4
+
+        # Mount point for the resource disk
+        ResourceDisk.MountPoint=/mnt/resource
+
+        # Respond to load balancer probes if requested by Windows Azure.
+        LBProbeResponder=y
+
+        # Enable logging to serial console (y|n)
+        # When stdout is not enough...
+        # 'y' if not set
+        Logs.Console=y
+
+        # Enable verbose logging (y|n)
+        Logs.Verbose=${if cfg.verboseLogging then "y" else "n"}
+
+        # Root device timeout in seconds.
+        OS.RootDeviceScsiTimeout=300
+    '';
+
+    services.udev.packages = [ waagent ];
+
+    networking.dhcpcd.persistent = true;
+
+    services.logrotate = {
+      enable = true;
+      extraConfig = ''
+        /var/log/waagent.log {
+            compress
+            monthly
+            rotate 6
+            notifempty
+            missingok
+        }
+      '';
+    };
+
+    systemd.targets.provisioned = {
+      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.waagent = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" "sshd.service" ];
+      wants = [ "network-online.target" ];
+
+      path = [ pkgs.e2fsprogs pkgs.bash ];
+      description = "Windows Azure Agent Service";
+      unitConfig.ConditionPathExists = "/etc/waagent.conf";
+      serviceConfig = {
+        ExecStart = "${waagent}/bin/waagent -daemon";
+        Type = "simple";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/virtualisation/azure-bootstrap-blobs.nix b/nixos/modules/virtualisation/azure-bootstrap-blobs.nix
new file mode 100644
index 00000000000..281be9a1231
--- /dev/null
+++ b/nixos/modules/virtualisation/azure-bootstrap-blobs.nix
@@ -0,0 +1,3 @@
+{
+    "16.03" = "https://nixos.blob.core.windows.net/images/nixos-image-16.03.847.8688c17-x86_64-linux.vhd";
+}
diff --git a/nixos/modules/virtualisation/azure-common.nix b/nixos/modules/virtualisation/azure-common.nix
new file mode 100644
index 00000000000..8efa177e30d
--- /dev/null
+++ b/nixos/modules/virtualisation/azure-common.nix
@@ -0,0 +1,66 @@
+{ lib, pkgs, ... }:
+
+with lib;
+{
+  imports = [ ../profiles/headless.nix ];
+
+  require = [ ./azure-agent.nix ];
+  virtualisation.azure.agent.enable = true;
+
+  boot.kernelParams = [ "console=ttyS0" "earlyprintk=ttyS0" "rootdelay=300" "panic=1" "boot.panic_on_fail" ];
+  boot.initrd.kernelModules = [ "hv_vmbus" "hv_netvsc" "hv_utils" "hv_storvsc" ];
+
+  # Generate a GRUB menu.
+  boot.loader.grub.device = "/dev/sda";
+  boot.loader.grub.version = 2;
+  boot.loader.timeout = 0;
+
+  boot.growPartition = true;
+
+  # Don't put old configurations in the GRUB menu.  The user has no
+  # way to select them anyway.
+  boot.loader.grub.configurationLimit = 0;
+
+  fileSystems."/".device = "/dev/disk/by-label/nixos";
+
+  # 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
+  '';
+
+  # Force getting the hostname from Azure
+  networking.hostName = mkDefault "";
+
+  # Always include cryptsetup so that NixOps can use it.
+  # sg_scan is needed to finalize disk removal on older kernels
+  environment.systemPackages = [ pkgs.cryptsetup pkgs.sg3_utils ];
+
+  networking.usePredictableInterfaceNames = false;
+
+  services.udev.extraRules = ''
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:0", ATTR{removable}=="0", SYMLINK+="disk/by-lun/0",
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:1", ATTR{removable}=="0", SYMLINK+="disk/by-lun/1",
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:2", ATTR{removable}=="0", SYMLINK+="disk/by-lun/2"
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:3", ATTR{removable}=="0", SYMLINK+="disk/by-lun/3"
+
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:4", ATTR{removable}=="0", SYMLINK+="disk/by-lun/4"
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:5", ATTR{removable}=="0", SYMLINK+="disk/by-lun/5"
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:6", ATTR{removable}=="0", SYMLINK+="disk/by-lun/6"
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:7", ATTR{removable}=="0", SYMLINK+="disk/by-lun/7"
+
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:8", ATTR{removable}=="0", SYMLINK+="disk/by-lun/8"
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:9", ATTR{removable}=="0", SYMLINK+="disk/by-lun/9"
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:10", ATTR{removable}=="0", SYMLINK+="disk/by-lun/10"
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:11", ATTR{removable}=="0", SYMLINK+="disk/by-lun/11"
+
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:12", ATTR{removable}=="0", SYMLINK+="disk/by-lun/12"
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:13", ATTR{removable}=="0", SYMLINK+="disk/by-lun/13"
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:14", ATTR{removable}=="0", SYMLINK+="disk/by-lun/14"
+    ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:15", ATTR{removable}=="0", SYMLINK+="disk/by-lun/15"
+
+  '';
+
+}
diff --git a/nixos/modules/virtualisation/azure-config-user.nix b/nixos/modules/virtualisation/azure-config-user.nix
new file mode 100644
index 00000000000..267ba50ae02
--- /dev/null
+++ b/nixos/modules/virtualisation/azure-config-user.nix
@@ -0,0 +1,12 @@
+{ modulesPath, ... }:
+
+{
+  # To build the configuration or use nix-env, you need to run
+  # either nixos-rebuild --upgrade or nix-channel --update
+  # to fetch the nixos channel.
+
+  # This configures everything but bootstrap services,
+  # which only need to be run once and have already finished
+  # if you are able to see this comment.
+  imports = [ "${modulesPath}/virtualisation/azure-common.nix" ];
+}
diff --git a/nixos/modules/virtualisation/azure-config.nix b/nixos/modules/virtualisation/azure-config.nix
new file mode 100644
index 00000000000..780bd1b78dc
--- /dev/null
+++ b/nixos/modules/virtualisation/azure-config.nix
@@ -0,0 +1,5 @@
+{ modulesPath, ... }:
+
+{
+  imports = [ "${modulesPath}/virtualisation/azure-image.nix" ];
+}
diff --git a/nixos/modules/virtualisation/azure-image.nix b/nixos/modules/virtualisation/azure-image.nix
new file mode 100644
index 00000000000..03dd3c05130
--- /dev/null
+++ b/nixos/modules/virtualisation/azure-image.nix
@@ -0,0 +1,71 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.virtualisation.azureImage;
+in
+{
+  imports = [ ./azure-common.nix ];
+
+  options = {
+    virtualisation.azureImage.diskSize = mkOption {
+      type = with types; either (enum [ "auto" ]) int;
+      default = "auto";
+      example = 2048;
+      description = ''
+        Size of disk image. Unit is MB.
+      '';
+    };
+  };
+  config = {
+    system.build.azureImage = import ../../lib/make-disk-image.nix {
+      name = "azure-image";
+      postVM = ''
+        ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -o subformat=fixed,force_size -O vpc $diskImage $out/disk.vhd
+        rm $diskImage
+      '';
+      configFile = ./azure-config-user.nix;
+      format = "raw";
+      inherit (cfg) diskSize;
+      inherit config lib pkgs;
+    };
+
+    # Azure metadata is available as a CD-ROM drive.
+    fileSystems."/metadata".device = "/dev/sr0";
+
+    systemd.services.fetch-ssh-keys = {
+      description = "Fetch host keys and authorized_keys for root user";
+
+      wantedBy = [ "sshd.service" "waagent.service" ];
+      before = [ "sshd.service" "waagent.service" ];
+
+      path  = [ pkgs.coreutils ];
+      script =
+        ''
+          eval "$(cat /metadata/CustomData.bin)"
+          if ! [ -z "$ssh_host_ecdsa_key" ]; then
+            echo "downloaded ssh_host_ecdsa_key"
+            echo "$ssh_host_ecdsa_key" > /etc/ssh/ssh_host_ed25519_key
+            chmod 600 /etc/ssh/ssh_host_ed25519_key
+          fi
+
+          if ! [ -z "$ssh_host_ecdsa_key_pub" ]; then
+            echo "downloaded ssh_host_ecdsa_key_pub"
+            echo "$ssh_host_ecdsa_key_pub" > /etc/ssh/ssh_host_ed25519_key.pub
+            chmod 644 /etc/ssh/ssh_host_ed25519_key.pub
+          fi
+
+          if ! [ -z "$ssh_root_auth_key" ]; then
+            echo "downloaded ssh_root_auth_key"
+            mkdir -m 0700 -p /root/.ssh
+            echo "$ssh_root_auth_key" > /root/.ssh/authorized_keys
+            chmod 600 /root/.ssh/authorized_keys
+          fi
+        '';
+      serviceConfig.Type = "oneshot";
+      serviceConfig.RemainAfterExit = true;
+      serviceConfig.StandardError = "journal+console";
+      serviceConfig.StandardOutput = "journal+console";
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/azure-images.nix b/nixos/modules/virtualisation/azure-images.nix
new file mode 100644
index 00000000000..22c82fc14f6
--- /dev/null
+++ b/nixos/modules/virtualisation/azure-images.nix
@@ -0,0 +1,5 @@
+let self = {
+  "16.09" = "https://nixos.blob.core.windows.net/images/nixos-image-16.09.1694.019dcc3-x86_64-linux.vhd";
+
+  latest = self."16.09";
+}; in self
diff --git a/nixos/modules/virtualisation/brightbox-config.nix b/nixos/modules/virtualisation/brightbox-config.nix
new file mode 100644
index 00000000000..0a018e4cd69
--- /dev/null
+++ b/nixos/modules/virtualisation/brightbox-config.nix
@@ -0,0 +1,5 @@
+{ modulesPath, ... }:
+
+{
+  imports = [ "${modulesPath}/virtualisation/brightbox-image.nix" ];
+}
diff --git a/nixos/modules/virtualisation/brightbox-image.nix b/nixos/modules/virtualisation/brightbox-image.nix
new file mode 100644
index 00000000000..9641b693f18
--- /dev/null
+++ b/nixos/modules/virtualisation/brightbox-image.nix
@@ -0,0 +1,166 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  diskSize = "20G";
+in
+{
+  imports = [ ../profiles/headless.nix ../profiles/qemu-guest.nix ];
+
+  system.build.brightboxImage =
+    pkgs.vmTools.runInLinuxVM (
+      pkgs.runCommand "brightbox-image"
+        { preVM =
+            ''
+              mkdir $out
+              diskImage=$out/$diskImageBase
+              truncate $diskImage --size ${diskSize}
+              mv closure xchg/
+            '';
+
+          postVM =
+            ''
+              PATH=$PATH:${lib.makeBinPath [ pkgs.gnutar pkgs.gzip ]}
+              pushd $out
+              ${pkgs.qemu_kvm}/bin/qemu-img convert -c -O qcow2 $diskImageBase nixos.qcow2
+              rm $diskImageBase
+              popd
+            '';
+          diskImageBase = "nixos-image-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.raw";
+          buildInputs = [ pkgs.util-linux pkgs.perl ];
+          exportReferencesGraph =
+            [ "closure" config.system.build.toplevel ];
+        }
+        ''
+          # Create partition table
+          ${pkgs.parted}/sbin/parted --script /dev/vda mklabel msdos
+          ${pkgs.parted}/sbin/parted --script /dev/vda mkpart primary ext4 1 ${diskSize}
+          ${pkgs.parted}/sbin/parted --script /dev/vda print
+          . /sys/class/block/vda1/uevent
+          mknod /dev/vda1 b $MAJOR $MINOR
+
+          # Create an empty filesystem and mount it.
+          ${pkgs.e2fsprogs}/sbin/mkfs.ext4 -L nixos /dev/vda1
+          ${pkgs.e2fsprogs}/sbin/tune2fs -c 0 -i 0 /dev/vda1
+
+          mkdir /mnt
+          mount /dev/vda1 /mnt
+
+          # The initrd expects these directories to exist.
+          mkdir /mnt/dev /mnt/proc /mnt/sys
+
+          mount --bind /proc /mnt/proc
+          mount --bind /dev /mnt/dev
+          mount --bind /sys /mnt/sys
+
+          # Copy all paths in the closure to the filesystem.
+          storePaths=$(perl ${pkgs.pathsFromGraph} /tmp/xchg/closure)
+
+          mkdir -p /mnt/nix/store
+          echo "copying everything (will take a while)..."
+          cp -prd $storePaths /mnt/nix/store/
+
+          # Register the paths in the Nix database.
+          printRegistration=1 perl ${pkgs.pathsFromGraph} /tmp/xchg/closure | \
+              chroot /mnt ${config.nix.package.out}/bin/nix-store --load-db --option build-users-group ""
+
+          # Create the system profile to allow nixos-rebuild to work.
+          chroot /mnt ${config.nix.package.out}/bin/nix-env \
+              -p /nix/var/nix/profiles/system --set ${config.system.build.toplevel} \
+              --option build-users-group ""
+
+          # `nixos-rebuild' requires an /etc/NIXOS.
+          mkdir -p /mnt/etc
+          touch /mnt/etc/NIXOS
+
+          # `switch-to-configuration' requires a /bin/sh
+          mkdir -p /mnt/bin
+          ln -s ${config.system.build.binsh}/bin/sh /mnt/bin/sh
+
+          # Install a configuration.nix.
+          mkdir -p /mnt/etc/nixos /mnt/boot/grub
+          cp ${./brightbox-config.nix} /mnt/etc/nixos/configuration.nix
+
+          # Generate the GRUB menu.
+          ln -s vda /dev/sda
+          chroot /mnt ${config.system.build.toplevel}/bin/switch-to-configuration boot
+
+          umount /mnt/proc /mnt/dev /mnt/sys
+          umount /mnt
+        ''
+    );
+
+  fileSystems."/".label = "nixos";
+
+  # Generate a GRUB menu.  Amazon's pv-grub uses this to boot our kernel/initrd.
+  boot.loader.grub.device = "/dev/vda";
+  boot.loader.timeout = 0;
+
+  # Don't put old configurations in the GRUB menu.  The user has no
+  # way to select them anyway.
+  boot.loader.grub.configurationLimit = 0;
+
+  # 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";
+
+  # Force getting the hostname from Google Compute.
+  networking.hostName = mkDefault "";
+
+  # Always include cryptsetup so that NixOps can use it.
+  environment.systemPackages = [ pkgs.cryptsetup ];
+
+  systemd.services.fetch-ec2-data =
+    { description = "Fetch EC2 Data";
+
+      wantedBy = [ "multi-user.target" "sshd.service" ];
+      before = [ "sshd.service" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+
+      path = [ pkgs.wget pkgs.iproute2 ];
+
+      script =
+        ''
+          wget="wget -q --retry-connrefused -O -"
+
+          ${optionalString (config.networking.hostName == "") ''
+            echo "setting host name..."
+            ${pkgs.nettools}/bin/hostname $($wget http://169.254.169.254/latest/meta-data/hostname)
+          ''}
+
+          # Don't download the SSH key if it has already been injected
+          # into the image (a Nova feature).
+          if ! [ -e /root/.ssh/authorized_keys ]; then
+              echo "obtaining SSH key..."
+              mkdir -m 0700 -p /root/.ssh
+              $wget http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key > /root/key.pub
+              if [ $? -eq 0 -a -e /root/key.pub ]; then
+                  if ! grep -q -f /root/key.pub /root/.ssh/authorized_keys; then
+                      cat /root/key.pub >> /root/.ssh/authorized_keys
+                      echo "new key added to authorized_keys"
+                  fi
+                  chmod 600 /root/.ssh/authorized_keys
+                  rm -f /root/key.pub
+              fi
+          fi
+
+          # Extract the intended SSH host key for this machine from
+          # the supplied user data, if available.  Otherwise sshd will
+          # generate one normally.
+          $wget http://169.254.169.254/2011-01-01/user-data > /root/user-data || true
+          key="$(sed 's/|/\n/g; s/SSH_HOST_DSA_KEY://; t; d' /root/user-data)"
+          key_pub="$(sed 's/SSH_HOST_DSA_KEY_PUB://; t; d' /root/user-data)"
+          if [ -n "$key" -a -n "$key_pub" -a ! -e /etc/ssh/ssh_host_dsa_key ]; then
+              mkdir -m 0755 -p /etc/ssh
+              (umask 077; echo "$key" > /etc/ssh/ssh_host_dsa_key)
+              echo "$key_pub" > /etc/ssh/ssh_host_dsa_key.pub
+          fi
+        '';
+
+      serviceConfig.Type = "oneshot";
+      serviceConfig.RemainAfterExit = true;
+    };
+
+}
diff --git a/nixos/modules/virtualisation/build-vm.nix b/nixos/modules/virtualisation/build-vm.nix
new file mode 100644
index 00000000000..4a4694950f9
--- /dev/null
+++ b/nixos/modules/virtualisation/build-vm.nix
@@ -0,0 +1,58 @@
+{ config, extendModules, lib, ... }:
+let
+
+  inherit (lib)
+    mkOption
+    ;
+
+  vmVariant = extendModules {
+    modules = [ ./qemu-vm.nix ];
+  };
+
+  vmVariantWithBootLoader = vmVariant.extendModules {
+    modules = [
+      ({ config, ... }: {
+        _file = "nixos/default.nix##vmWithBootLoader";
+        virtualisation.useBootLoader = true;
+        virtualisation.useEFIBoot =
+          config.boot.loader.systemd-boot.enable ||
+          config.boot.loader.efi.canTouchEfiVariables;
+      })
+    ];
+  };
+in
+{
+  options = {
+
+    virtualisation.vmVariant = mkOption {
+      description = ''
+        Machine configuration to be added for the vm script produced by <literal>nixos-rebuild build-vm</literal>.
+      '';
+      inherit (vmVariant) type;
+      default = {};
+      visible = "shallow";
+    };
+
+    virtualisation.vmVariantWithBootLoader = mkOption {
+      description = ''
+        Machine configuration to be added for the vm script produced by <literal>nixos-rebuild build-vm-with-bootloader</literal>.
+      '';
+      inherit (vmVariantWithBootLoader) type;
+      default = {};
+      visible = "shallow";
+    };
+
+  };
+
+  config = {
+
+    system.build = {
+      vm = lib.mkDefault config.virtualisation.vmVariant.system.build.vm;
+      vmWithBootLoader = lib.mkDefault config.virtualisation.vmVariantWithBootLoader.system.build.vm;
+    };
+
+  };
+
+  # uses extendModules
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/virtualisation/cloudstack-config.nix b/nixos/modules/virtualisation/cloudstack-config.nix
new file mode 100644
index 00000000000..78afebdc5dd
--- /dev/null
+++ b/nixos/modules/virtualisation/cloudstack-config.nix
@@ -0,0 +1,40 @@
+{ lib, pkgs, ... }:
+
+with lib;
+
+{
+  imports = [
+    ../profiles/qemu-guest.nix
+  ];
+
+  config = {
+    fileSystems."/" = {
+      device = "/dev/disk/by-label/nixos";
+      autoResize = true;
+    };
+
+    boot.growPartition = true;
+    boot.kernelParams = [ "console=tty0" ];
+    boot.loader.grub.device = "/dev/vda";
+    boot.loader.timeout = 0;
+
+    # Allow root logins
+    services.openssh = {
+      enable = true;
+      permitRootLogin = "prohibit-password";
+    };
+
+    # Cloud-init configuration.
+    services.cloud-init.enable = true;
+    # Wget is needed for setting password. This is of little use as
+    # root password login is disabled above.
+    environment.systemPackages = [ pkgs.wget ];
+    # Only enable CloudStack datasource for faster boot speed.
+    environment.etc."cloud/cloud.cfg.d/99_cloudstack.cfg".text = ''
+      datasource:
+        CloudStack: {}
+        None: {}
+      datasource_list: ["CloudStack"]
+    '';
+  };
+}
diff --git a/nixos/modules/virtualisation/container-config.nix b/nixos/modules/virtualisation/container-config.nix
new file mode 100644
index 00000000000..0966ef84827
--- /dev/null
+++ b/nixos/modules/virtualisation/container-config.nix
@@ -0,0 +1,31 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  config = mkIf config.boot.isContainer {
+
+    # Disable some features that are not useful in a container.
+    nix.optimise.automatic = mkDefault false; # the store is host managed
+    services.udisks2.enable = mkDefault false;
+    powerManagement.enable = mkDefault false;
+    documentation.nixos.enable = mkDefault false;
+
+    networking.useHostResolvConf = mkDefault true;
+
+    # Containers should be light-weight, so start sshd on demand.
+    services.openssh.startWhenNeeded = mkDefault true;
+
+    # Shut up warnings about not having a boot loader.
+    system.build.installBootLoader = lib.mkDefault "${pkgs.coreutils}/bin/true";
+
+    # Not supported in systemd-nspawn containers.
+    security.audit.enable = false;
+
+    # Use the host's nix-daemon.
+    environment.variables.NIX_REMOTE = "daemon";
+
+  };
+
+}
diff --git a/nixos/modules/virtualisation/containerd.nix b/nixos/modules/virtualisation/containerd.nix
new file mode 100644
index 00000000000..ea89a994b17
--- /dev/null
+++ b/nixos/modules/virtualisation/containerd.nix
@@ -0,0 +1,101 @@
+{ pkgs, lib, config, ... }:
+let
+  cfg = config.virtualisation.containerd;
+
+  configFile = if cfg.configFile == null then
+    settingsFormat.generate "containerd.toml" cfg.settings
+  else
+    cfg.configFile;
+
+  containerdConfigChecked = pkgs.runCommand "containerd-config-checked.toml" {
+    nativeBuildInputs = [ pkgs.containerd ];
+  } ''
+    containerd -c ${configFile} config dump >/dev/null
+    ln -s ${configFile} $out
+  '';
+
+  settingsFormat = pkgs.formats.toml {};
+in
+{
+
+  options.virtualisation.containerd = with lib.types; {
+    enable = lib.mkEnableOption "containerd container runtime";
+
+    configFile = lib.mkOption {
+      default = null;
+      description = ''
+       Path to containerd config file.
+       Setting this option will override any configuration applied by the settings option.
+      '';
+      type = nullOr path;
+    };
+
+    settings = lib.mkOption {
+      type = settingsFormat.type;
+      default = {};
+      description = ''
+        Verbatim lines to add to containerd.toml
+      '';
+    };
+
+    args = lib.mkOption {
+      default = {};
+      description = "extra args to append to the containerd cmdline";
+      type = attrsOf str;
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    warnings = lib.optional (cfg.configFile != null) ''
+      `virtualisation.containerd.configFile` is deprecated. use `virtualisation.containerd.settings` instead.
+    '';
+
+    virtualisation.containerd = {
+      args.config = toString containerdConfigChecked;
+      settings = {
+        version = 2;
+        plugins."io.containerd.grpc.v1.cri" = {
+         containerd.snapshotter =
+           lib.mkIf config.boot.zfs.enabled (lib.mkOptionDefault "zfs");
+         cni.bin_dir = lib.mkOptionDefault "${pkgs.cni-plugins}/bin";
+        };
+      };
+    };
+
+    environment.systemPackages = [ pkgs.containerd ];
+
+    systemd.services.containerd = {
+      description = "containerd - container runtime";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = with pkgs; [
+        containerd
+        runc
+        iptables
+      ] ++ lib.optional config.boot.zfs.enabled config.boot.zfs.package;
+      serviceConfig = {
+        ExecStart = ''${pkgs.containerd}/bin/containerd ${lib.concatStringsSep " " (lib.cli.toGNUCommandLine {} cfg.args)}'';
+        Delegate = "yes";
+        KillMode = "process";
+        Type = "notify";
+        Restart = "always";
+        RestartSec = "10";
+
+        # "limits" defined below are adopted from upstream: https://github.com/containerd/containerd/blob/master/containerd.service
+        LimitNPROC = "infinity";
+        LimitCORE = "infinity";
+        LimitNOFILE = "infinity";
+        TasksMax = "infinity";
+        OOMScoreAdjust = "-999";
+
+        StateDirectory = "containerd";
+        RuntimeDirectory = "containerd";
+        RuntimeDirectoryPreserve = "yes";
+      };
+      unitConfig = {
+        StartLimitBurst = "16";
+        StartLimitIntervalSec = "120s";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/containers.nix b/nixos/modules/virtualisation/containers.nix
new file mode 100644
index 00000000000..cea3d51d3ae
--- /dev/null
+++ b/nixos/modules/virtualisation/containers.nix
@@ -0,0 +1,156 @@
+{ config, lib, pkgs, utils, ... }:
+let
+  cfg = config.virtualisation.containers;
+
+  inherit (lib) literalExpression mkOption types;
+
+  toml = pkgs.formats.toml { };
+in
+{
+  meta = {
+    maintainers = [] ++ lib.teams.podman.members;
+  };
+
+
+  imports = [
+    (
+      lib.mkRemovedOptionModule
+      [ "virtualisation" "containers" "users" ]
+      "All users with `isNormalUser = true` set now get appropriate subuid/subgid mappings."
+    )
+    (
+      lib.mkRemovedOptionModule
+      [ "virtualisation" "containers" "containersConf" "extraConfig" ]
+      "Use virtualisation.containers.containersConf.settings instead."
+    )
+  ];
+
+  options.virtualisation.containers = {
+
+    enable =
+      mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          This option enables the common /etc/containers configuration module.
+        '';
+      };
+
+    ociSeccompBpfHook.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable the OCI seccomp BPF hook";
+    };
+
+    containersConf.settings = mkOption {
+      type = toml.type;
+      default = { };
+      description = "containers.conf configuration";
+    };
+
+    containersConf.cniPlugins = mkOption {
+      type = types.listOf types.package;
+      defaultText = literalExpression ''
+        [
+          pkgs.cni-plugins
+        ]
+      '';
+      example = literalExpression ''
+        [
+          pkgs.cniPlugins.dnsname
+        ]
+      '';
+      description = ''
+        CNI plugins to install on the system.
+      '';
+    };
+
+    storage.settings = mkOption {
+      type = toml.type;
+      default = {
+        storage = {
+          driver = "overlay";
+          graphroot = "/var/lib/containers/storage";
+          runroot = "/run/containers/storage";
+        };
+      };
+      description = "storage.conf configuration";
+    };
+
+    registries = {
+      search = mkOption {
+        type = types.listOf types.str;
+        default = [ "docker.io" "quay.io" ];
+        description = ''
+          List of repositories to search.
+        '';
+      };
+
+      insecure = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        description = ''
+          List of insecure repositories.
+        '';
+      };
+
+      block = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        description = ''
+          List of blocked repositories.
+        '';
+      };
+    };
+
+    policy = mkOption {
+      default = {};
+      type = types.attrs;
+      example = literalExpression ''
+        {
+          default = [ { type = "insecureAcceptAnything"; } ];
+          transports = {
+            docker-daemon = {
+              "" = [ { type = "insecureAcceptAnything"; } ];
+            };
+          };
+        }
+      '';
+      description = ''
+        Signature verification policy file.
+        If this option is empty the default policy file from
+        <literal>skopeo</literal> will be used.
+      '';
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    virtualisation.containers.containersConf.cniPlugins = [ pkgs.cni-plugins ];
+
+    virtualisation.containers.containersConf.settings = {
+      network.cni_plugin_dirs = map (p: "${lib.getBin p}/bin") cfg.containersConf.cniPlugins;
+      engine = {
+        init_path = "${pkgs.catatonit}/bin/catatonit";
+      } // lib.optionalAttrs cfg.ociSeccompBpfHook.enable {
+        hooks_dir = [ config.boot.kernelPackages.oci-seccomp-bpf-hook ];
+      };
+    };
+
+    environment.etc."containers/containers.conf".source =
+      toml.generate "containers.conf" cfg.containersConf.settings;
+
+    environment.etc."containers/storage.conf".source =
+      toml.generate "storage.conf" cfg.storage.settings;
+
+    environment.etc."containers/registries.conf".source = toml.generate "registries.conf" {
+      registries = lib.mapAttrs (n: v: { registries = v; }) cfg.registries;
+    };
+
+    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";
+  };
+
+}
diff --git a/nixos/modules/virtualisation/cri-o.nix b/nixos/modules/virtualisation/cri-o.nix
new file mode 100644
index 00000000000..cf511000150
--- /dev/null
+++ b/nixos/modules/virtualisation/cri-o.nix
@@ -0,0 +1,163 @@
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+let
+  cfg = config.virtualisation.cri-o;
+
+  crioPackage = (pkgs.cri-o.override { inherit (cfg) extraPackages; });
+
+  format = pkgs.formats.toml { };
+
+  cfgFile = format.generate "00-default.conf" cfg.settings;
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "virtualisation" "cri-o" "registries" ] [ "virtualisation" "containers" "registries" "search" ])
+  ];
+
+  meta = {
+    maintainers = teams.podman.members;
+  };
+
+  options.virtualisation.cri-o = {
+    enable = mkEnableOption "Container Runtime Interface for OCI (CRI-O)";
+
+    storageDriver = mkOption {
+      type = types.enum [ "btrfs" "overlay" "vfs" ];
+      default = "overlay";
+      description = "Storage driver to be used";
+    };
+
+    logLevel = mkOption {
+      type = types.enum [ "trace" "debug" "info" "warn" "error" "fatal" ];
+      default = "info";
+      description = "Log level to be used";
+    };
+
+    pauseImage = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Override the default pause image for pod sandboxes";
+      example = "k8s.gcr.io/pause:3.2";
+    };
+
+    pauseCommand = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Override the default pause command";
+      example = "/pause";
+    };
+
+    runtime = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Override the default runtime";
+      example = "crun";
+    };
+
+    extraPackages = mkOption {
+      type = with types; listOf package;
+      default = [ ];
+      example = literalExpression ''
+        [
+          pkgs.gvisor
+        ]
+      '';
+      description = ''
+        Extra packages to be installed in the CRI-O wrapper.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = crioPackage;
+      defaultText = literalDocBook ''
+        <literal>pkgs.cri-o</literal> built with
+        <literal>config.${opt.extraPackages}</literal>.
+      '';
+      internal = true;
+      description = ''
+        The final CRI-O package (including extra packages).
+      '';
+    };
+
+    networkDir = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = "Override the network_dir option.";
+      internal = true;
+    };
+
+    settings = mkOption {
+      type = format.type;
+      default = { };
+      description = ''
+        Configuration for cri-o, see
+        <link xlink:href="https://github.com/cri-o/cri-o/blob/master/docs/crio.conf.5.md"/>.
+      '';
+    };
+  };
+
+  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";
+
+    virtualisation.cri-o.settings.crio = {
+      storage_driver = cfg.storageDriver;
+
+      image = {
+        pause_image = mkIf (cfg.pauseImage != null) cfg.pauseImage;
+        pause_command = mkIf (cfg.pauseCommand != null) cfg.pauseCommand;
+      };
+
+      network = {
+        plugin_dirs = [ "${pkgs.cni-plugins}/bin" ];
+        network_dir = mkIf (cfg.networkDir != null) cfg.networkDir;
+      };
+
+      runtime = {
+        cgroup_manager = "systemd";
+        log_level = cfg.logLevel;
+        manage_ns_lifecycle = true;
+        pinns_path = "${cfg.package}/bin/pinns";
+        hooks_dir =
+          optional (config.virtualisation.containers.ociSeccompBpfHook.enable)
+            config.boot.kernelPackages.oci-seccomp-bpf-hook;
+
+        default_runtime = mkIf (cfg.runtime != null) cfg.runtime;
+        runtimes = mkIf (cfg.runtime != null) {
+          "${cfg.runtime}" = { };
+        };
+      };
+    };
+
+    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."crio/crio.conf.d/00-default.conf".source = cfgFile;
+
+    # Enable common /etc/containers configuration
+    virtualisation.containers.enable = true;
+
+    systemd.services.crio = {
+      description = "Container Runtime Interface for OCI (CRI-O)";
+      documentation = [ "https://github.com/cri-o/cri-o" ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = [ cfg.package ];
+      serviceConfig = {
+        Type = "notify";
+        ExecStart = "${cfg.package}/bin/crio";
+        ExecReload = "/bin/kill -s HUP $MAINPID";
+        TasksMax = "infinity";
+        LimitNOFILE = "1048576";
+        LimitNPROC = "1048576";
+        LimitCORE = "infinity";
+        OOMScoreAdjust = "-999";
+        TimeoutStartSec = "0";
+        Restart = "on-abnormal";
+      };
+      restartTriggers = [ cfgFile ];
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/digital-ocean-config.nix b/nixos/modules/virtualisation/digital-ocean-config.nix
new file mode 100644
index 00000000000..88cb0cd450e
--- /dev/null
+++ b/nixos/modules/virtualisation/digital-ocean-config.nix
@@ -0,0 +1,197 @@
+{ config, pkgs, lib, modulesPath, ... }:
+with lib;
+{
+  imports = [
+    (modulesPath + "/profiles/qemu-guest.nix")
+    (modulesPath + "/virtualisation/digital-ocean-init.nix")
+  ];
+  options.virtualisation.digitalOcean = with types; {
+    setRootPassword = mkOption {
+      type = bool;
+      default = false;
+      example = true;
+      description = "Whether to set the root password from the Digital Ocean metadata";
+    };
+    setSshKeys = mkOption {
+      type = bool;
+      default = true;
+      example = true;
+      description = "Whether to fetch ssh keys from Digital Ocean";
+    };
+    seedEntropy = mkOption {
+      type = bool;
+      default = true;
+      example = true;
+      description = "Whether to run the kernel RNG entropy seeding script from the Digital Ocean vendor data";
+    };
+  };
+  config =
+    let
+      cfg = config.virtualisation.digitalOcean;
+      hostName = config.networking.hostName;
+      doMetadataFile = "/run/do-metadata/v1.json";
+    in mkMerge [{
+      fileSystems."/" = {
+        device = "/dev/disk/by-label/nixos";
+        autoResize = true;
+        fsType = "ext4";
+      };
+      boot = {
+        growPartition = true;
+        kernelParams = [ "console=ttyS0" "panic=1" "boot.panic_on_fail" ];
+        initrd.kernelModules = [ "virtio_scsi" ];
+        kernelModules = [ "virtio_pci" "virtio_net" ];
+        loader = {
+          grub.device = "/dev/vda";
+          timeout = 0;
+          grub.configurationLimit = 0;
+        };
+      };
+      services.openssh = {
+        enable = mkDefault true;
+        passwordAuthentication = mkDefault false;
+      };
+      services.do-agent.enable = mkDefault true;
+      networking = {
+        hostName = mkDefault ""; # use Digital Ocean metadata server
+      };
+
+      /* Check for and wait for the metadata server to become reachable.
+       * This serves as a dependency for all the other metadata services. */
+      systemd.services.digitalocean-metadata = {
+        path = [ pkgs.curl ];
+        description = "Get host metadata provided by Digitalocean";
+        script = ''
+          set -eu
+          DO_DELAY_ATTEMPTS=0
+          while ! curl -fsSL -o $RUNTIME_DIRECTORY/v1.json http://169.254.169.254/metadata/v1.json; do
+            DO_DELAY_ATTEMPTS=$((DO_DELAY_ATTEMPTS + 1))
+            if (( $DO_DELAY_ATTEMPTS >= $DO_DELAY_ATTEMPTS_MAX )); then
+              echo "giving up"
+              exit 1
+            fi
+
+            echo "metadata unavailable, trying again in 1s..."
+            sleep 1
+          done
+          chmod 600 $RUNTIME_DIRECTORY/v1.json
+          '';
+        environment = {
+          DO_DELAY_ATTEMPTS_MAX = "10";
+        };
+        serviceConfig = {
+          Type = "oneshot";
+          RemainAfterExit = true;
+          RuntimeDirectory = "do-metadata";
+          RuntimeDirectoryPreserve = "yes";
+        };
+        unitConfig = {
+          ConditionPathExists = "!${doMetadataFile}";
+          After = [ "network-pre.target" ] ++
+            optional config.networking.dhcpcd.enable "dhcpcd.service" ++
+            optional config.systemd.network.enable "systemd-networkd.service";
+        };
+      };
+
+      /* Fetch the root password from the digital ocean metadata.
+       * There is no specific route for this, so we use jq to get
+       * it from the One Big JSON metadata blob */
+      systemd.services.digitalocean-set-root-password = mkIf cfg.setRootPassword {
+        path = [ pkgs.shadow pkgs.jq ];
+        description = "Set root password provided by Digitalocean";
+        wantedBy = [ "multi-user.target" ];
+        script = ''
+          set -eo pipefail
+          ROOT_PASSWORD=$(jq -er '.auth_key' ${doMetadataFile})
+          echo "root:$ROOT_PASSWORD" | chpasswd
+          mkdir -p /etc/do-metadata/set-root-password
+          '';
+        unitConfig = {
+          ConditionPathExists = "!/etc/do-metadata/set-root-password";
+          Before = optional config.services.openssh.enable "sshd.service";
+          After = [ "digitalocean-metadata.service" ];
+          Requires = [ "digitalocean-metadata.service" ];
+        };
+        serviceConfig = {
+          Type = "oneshot";
+        };
+      };
+
+      /* Set the hostname from Digital Ocean, unless the user configured it in
+       * the NixOS configuration. The cached metadata file isn't used here
+       * because the hostname is a mutable part of the droplet. */
+      systemd.services.digitalocean-set-hostname = mkIf (hostName == "") {
+        path = [ pkgs.curl pkgs.nettools ];
+        description = "Set hostname provided by Digitalocean";
+        wantedBy = [ "network.target" ];
+        script = ''
+          set -e
+          DIGITALOCEAN_HOSTNAME=$(curl -fsSL http://169.254.169.254/metadata/v1/hostname)
+          hostname "$DIGITALOCEAN_HOSTNAME"
+          if [[ ! -e /etc/hostname || -w /etc/hostname ]]; then
+            printf "%s\n" "$DIGITALOCEAN_HOSTNAME" > /etc/hostname
+          fi
+        '';
+        unitConfig = {
+          Before = [ "network.target" ];
+          After = [ "digitalocean-metadata.service" ];
+          Wants = [ "digitalocean-metadata.service" ];
+        };
+        serviceConfig = {
+          Type = "oneshot";
+        };
+      };
+
+      /* Fetch the ssh keys for root from Digital Ocean */
+      systemd.services.digitalocean-ssh-keys = mkIf cfg.setSshKeys {
+        description = "Set root ssh keys provided by Digital Ocean";
+        wantedBy = [ "multi-user.target" ];
+        path = [ pkgs.jq ];
+        script = ''
+          set -e
+          mkdir -m 0700 -p /root/.ssh
+          jq -er '.public_keys[]' ${doMetadataFile} > /root/.ssh/authorized_keys
+          chmod 600 /root/.ssh/authorized_keys
+        '';
+        serviceConfig = {
+          Type = "oneshot";
+          RemainAfterExit = true;
+        };
+        unitConfig = {
+          ConditionPathExists = "!/root/.ssh/authorized_keys";
+          Before = optional config.services.openssh.enable "sshd.service";
+          After = [ "digitalocean-metadata.service" ];
+          Requires = [ "digitalocean-metadata.service" ];
+        };
+      };
+
+      /* Initialize the RNG by running the entropy-seed script from the
+       * Digital Ocean metadata
+       */
+      systemd.services.digitalocean-entropy-seed = mkIf cfg.seedEntropy {
+        description = "Run the kernel RNG entropy seeding script from the Digital Ocean vendor data";
+        wantedBy = [ "network.target" ];
+        path = [ pkgs.jq pkgs.mpack ];
+        script = ''
+          set -eo pipefail
+          TEMPDIR=$(mktemp -d)
+          jq -er '.vendor_data' ${doMetadataFile} | munpack -tC $TEMPDIR
+          ENTROPY_SEED=$(grep -rl "DigitalOcean Entropy Seed script" $TEMPDIR)
+          ${pkgs.runtimeShell} $ENTROPY_SEED
+          rm -rf $TEMPDIR
+          '';
+        unitConfig = {
+          Before = [ "network.target" ];
+          After = [ "digitalocean-metadata.service" ];
+          Requires = [ "digitalocean-metadata.service" ];
+        };
+        serviceConfig = {
+          Type = "oneshot";
+        };
+      };
+
+    }
+  ];
+  meta.maintainers = with maintainers; [ arianvp eamsden ];
+}
+
diff --git a/nixos/modules/virtualisation/digital-ocean-image.nix b/nixos/modules/virtualisation/digital-ocean-image.nix
new file mode 100644
index 00000000000..0ff2ee591f2
--- /dev/null
+++ b/nixos/modules/virtualisation/digital-ocean-image.nix
@@ -0,0 +1,70 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.virtualisation.digitalOceanImage;
+in
+{
+
+  imports = [ ./digital-ocean-config.nix ];
+
+  options = {
+    virtualisation.digitalOceanImage.diskSize = mkOption {
+      type = with types; either (enum [ "auto" ]) int;
+      default = "auto";
+      example = 4096;
+      description = ''
+        Size of disk image. Unit is MB.
+      '';
+    };
+
+    virtualisation.digitalOceanImage.configFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      description = ''
+        A path to a configuration file which will be placed at
+        <literal>/etc/nixos/configuration.nix</literal> and be used when switching
+        to a new configuration. If set to <literal>null</literal>, a default
+        configuration is used that imports
+        <literal>(modulesPath + "/virtualisation/digital-ocean-config.nix")</literal>.
+      '';
+    };
+
+    virtualisation.digitalOceanImage.compressionMethod = mkOption {
+      type = types.enum [ "gzip" "bzip2" ];
+      default = "gzip";
+      example = "bzip2";
+      description = ''
+        Disk image compression method. Choose bzip2 to generate smaller images that
+        take longer to generate but will consume less metered storage space on your
+        Digital Ocean account.
+      '';
+    };
+  };
+
+  #### implementation
+  config = {
+
+    system.build.digitalOceanImage = import ../../lib/make-disk-image.nix {
+      name = "digital-ocean-image";
+      format = "qcow2";
+      postVM = let
+        compress = {
+          "gzip" = "${pkgs.gzip}/bin/gzip";
+          "bzip2" = "${pkgs.bzip2}/bin/bzip2";
+        }.${cfg.compressionMethod};
+      in ''
+        ${compress} $diskImage
+      '';
+      configFile = if cfg.configFile == null
+        then config.virtualisation.digitalOcean.defaultConfigFile
+        else cfg.configFile;
+      inherit (cfg) diskSize;
+      inherit config lib pkgs;
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ arianvp eamsden ];
+
+}
diff --git a/nixos/modules/virtualisation/digital-ocean-init.nix b/nixos/modules/virtualisation/digital-ocean-init.nix
new file mode 100644
index 00000000000..4339d91de16
--- /dev/null
+++ b/nixos/modules/virtualisation/digital-ocean-init.nix
@@ -0,0 +1,95 @@
+{ config, pkgs, lib, ... }:
+with lib;
+let
+  cfg = config.virtualisation.digitalOcean;
+  defaultConfigFile = pkgs.writeText "digitalocean-configuration.nix" ''
+    { modulesPath, lib, ... }:
+    {
+      imports = lib.optional (builtins.pathExists ./do-userdata.nix) ./do-userdata.nix ++ [
+        (modulesPath + "/virtualisation/digital-ocean-config.nix")
+      ];
+    }
+  '';
+in {
+  options.virtualisation.digitalOcean.rebuildFromUserData = mkOption {
+    type = types.bool;
+    default = true;
+    example = true;
+    description = "Whether to reconfigure the system from Digital Ocean user data";
+  };
+  options.virtualisation.digitalOcean.defaultConfigFile = mkOption {
+    type = types.path;
+    default = defaultConfigFile;
+    defaultText = literalDocBook ''
+      The default configuration imports user-data if applicable and
+      <literal>(modulesPath + "/virtualisation/digital-ocean-config.nix")</literal>.
+    '';
+    description = ''
+      A path to a configuration file which will be placed at
+      <literal>/etc/nixos/configuration.nix</literal> and be used when switching to
+      a new configuration.
+    '';
+  };
+
+  config = {
+    systemd.services.digitalocean-init = mkIf cfg.rebuildFromUserData {
+      description = "Reconfigure the system from Digital Ocean userdata on startup";
+      wantedBy = [ "network-online.target" ];
+      unitConfig = {
+        ConditionPathExists = "!/etc/nixos/do-userdata.nix";
+        After = [ "digitalocean-metadata.service" "network-online.target" ];
+        Requires = [ "digitalocean-metadata.service" ];
+        X-StopOnRemoval = false;
+      };
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+      };
+      restartIfChanged = false;
+      path = [ pkgs.jq pkgs.gnused pkgs.gnugrep pkgs.systemd config.nix.package config.system.build.nixos-rebuild ];
+      environment = {
+        HOME = "/root";
+        NIX_PATH = concatStringsSep ":" [
+          "/nix/var/nix/profiles/per-user/root/channels/nixos"
+          "nixos-config=/etc/nixos/configuration.nix"
+          "/nix/var/nix/profiles/per-user/root/channels"
+        ];
+      };
+      script = ''
+        set -e
+        echo "attempting to fetch configuration from Digital Ocean user data..."
+        userData=$(mktemp)
+        if jq -er '.user_data' /run/do-metadata/v1.json > $userData; then
+          # If the user-data looks like it could be a nix expression,
+          # copy it over. Also, look for a magic three-hash comment and set
+          # that as the channel.
+          if nix-instantiate --parse $userData > /dev/null; then
+            channels="$(grep '^###' "$userData" | sed 's|###\s*||')"
+            printf "%s" "$channels" | while read channel; do
+              echo "writing channel: $channel"
+            done
+
+            if [[ -n "$channels" ]]; then
+              printf "%s" "$channels" > /root/.nix-channels
+              nix-channel --update
+            fi
+
+            echo "setting configuration from Digital Ocean user data"
+            cp "$userData" /etc/nixos/do-userdata.nix
+            if [[ ! -e /etc/nixos/configuration.nix ]]; then
+              install -m0644 ${cfg.defaultConfigFile} /etc/nixos/configuration.nix
+            fi
+          else
+            echo "user data does not appear to be a Nix expression; ignoring"
+            exit
+          fi
+
+          nixos-rebuild switch
+        else
+          echo "no user data is available"
+        fi
+        '';
+    };
+  };
+  meta.maintainers = with maintainers; [ arianvp eamsden ];
+}
diff --git a/nixos/modules/virtualisation/docker-image.nix b/nixos/modules/virtualisation/docker-image.nix
new file mode 100644
index 00000000000..baac3a35a78
--- /dev/null
+++ b/nixos/modules/virtualisation/docker-image.nix
@@ -0,0 +1,57 @@
+{ ... }:
+
+{
+  imports = [
+    ../profiles/docker-container.nix # FIXME, shouldn't include something from profiles/
+  ];
+
+  boot.postBootCommands =
+    ''
+      # Set virtualisation to docker
+      echo "docker" > /run/systemd/container
+    '';
+
+  # Iptables do not work in Docker.
+  networking.firewall.enable = false;
+
+  # Socket activated ssh presents problem in Docker.
+  services.openssh.startWhenNeeded = false;
+}
+
+# Example usage:
+#
+## default.nix
+# let
+#   nixos = import <nixpkgs/nixos> {
+#     configuration = ./configuration.nix;
+#     system = "x86_64-linux";
+#   };
+# in
+# nixos.config.system.build.tarball
+#
+## configuration.nix
+# { pkgs, config, lib, ... }:
+# {
+#   imports = [
+#     <nixpkgs/nixos/modules/virtualisation/docker-image.nix>
+#     <nixpkgs/nixos/modules/installer/cd-dvd/channel.nix>
+#   ];
+#
+#   documentation.doc.enable = false;
+#
+#   environment.systemPackages = with pkgs; [
+#     bashInteractive
+#     cacert
+#     nix
+#   ];
+# }
+#
+## Run
+# Build the tarball:
+# $ nix-build default.nix
+# Load into docker:
+# $ docker import result/tarball/nixos-system-*.tar.xz nixos-docker
+# Boots into systemd
+# $ docker run --privileged -it nixos-docker /init
+# Log into the container
+# $ docker exec -it <container-name> /run/current-system/sw/bin/bash
diff --git a/nixos/modules/virtualisation/docker-rootless.nix b/nixos/modules/virtualisation/docker-rootless.nix
new file mode 100644
index 00000000000..d371f67ecdc
--- /dev/null
+++ b/nixos/modules/virtualisation/docker-rootless.nix
@@ -0,0 +1,102 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.virtualisation.docker.rootless;
+  proxy_env = config.networking.proxy.envVars;
+  settingsFormat = pkgs.formats.json {};
+  daemonSettingsFile = settingsFormat.generate "daemon.json" cfg.daemon.settings;
+
+in
+
+{
+  ###### interface
+
+  options.virtualisation.docker.rootless = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        This option enables docker in a rootless mode, a daemon that manages
+        linux containers. To interact with the daemon, one needs to set
+        <command>DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock</command>.
+      '';
+    };
+
+    setSocketVariable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Point <command>DOCKER_HOST</command> to rootless Docker instance for
+        normal users by default.
+      '';
+    };
+
+    daemon.settings = mkOption {
+      type = settingsFormat.type;
+      default = { };
+      example = {
+        ipv6 = true;
+        "fixed-cidr-v6" = "fd00::/80";
+      };
+      description = ''
+        Configuration for docker daemon. The attributes are serialized to JSON used as daemon.conf.
+        See https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file
+      '';
+    };
+
+    package = mkOption {
+      default = pkgs.docker;
+      defaultText = literalExpression "pkgs.docker";
+      type = types.package;
+      example = literalExpression "pkgs.docker-edge";
+      description = ''
+        Docker package to be used in the module.
+      '';
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    environment.extraInit = optionalString cfg.setSocketVariable ''
+      if [ -z "$DOCKER_HOST" -a -n "$XDG_RUNTIME_DIR" ]; then
+        export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/docker.sock"
+      fi
+    '';
+
+    # Taken from https://github.com/moby/moby/blob/master/contrib/dockerd-rootless-setuptool.sh
+    systemd.user.services.docker = {
+      wantedBy = [ "default.target" ];
+      description = "Docker Application Container Engine (Rootless)";
+      # needs newuidmap from pkgs.shadow
+      path = [ "/run/wrappers" ];
+      environment = proxy_env;
+      unitConfig = {
+        # docker-rootless doesn't support running as root.
+        ConditionUser = "!root";
+        StartLimitInterval = "60s";
+      };
+      serviceConfig = {
+        Type = "notify";
+        ExecStart = "${cfg.package}/bin/dockerd-rootless --config-file=${daemonSettingsFile}";
+        ExecReload = "${pkgs.procps}/bin/kill -s HUP $MAINPID";
+        TimeoutSec = 0;
+        RestartSec = 2;
+        Restart = "always";
+        StartLimitBurst = 3;
+        LimitNOFILE = "infinity";
+        LimitNPROC = "infinity";
+        LimitCORE = "infinity";
+        Delegate = true;
+        NotifyAccess = "all";
+        KillMode = "mixed";
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/virtualisation/docker.nix b/nixos/modules/virtualisation/docker.nix
new file mode 100644
index 00000000000..a69cbe55c78
--- /dev/null
+++ b/nixos/modules/virtualisation/docker.nix
@@ -0,0 +1,252 @@
+# Systemd services for docker.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.virtualisation.docker;
+  proxy_env = config.networking.proxy.envVars;
+  settingsFormat = pkgs.formats.json {};
+  daemonSettingsFile = settingsFormat.generate "daemon.json" cfg.daemon.settings;
+in
+
+{
+  ###### interface
+
+  options.virtualisation.docker = {
+    enable =
+      mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          ''
+            This option enables docker, a daemon that manages
+            linux containers. Users in the "docker" group can interact with
+            the daemon (e.g. to start or stop containers) using the
+            <command>docker</command> command line tool.
+          '';
+      };
+
+    listenOptions =
+      mkOption {
+        type = types.listOf types.str;
+        default = ["/run/docker.sock"];
+        description =
+          ''
+            A list of unix and tcp docker should listen to. The format follows
+            ListenStream as described in systemd.socket(5).
+          '';
+      };
+
+    enableOnBoot =
+      mkOption {
+        type = types.bool;
+        default = true;
+        description =
+          ''
+            When enabled dockerd is started on boot. This is required for
+            containers which are created with the
+            <literal>--restart=always</literal> flag to work. If this option is
+            disabled, docker might be started on demand by socket activation.
+          '';
+      };
+
+    daemon.settings =
+      mkOption {
+        type = settingsFormat.type;
+        default = { };
+        example = {
+          ipv6 = true;
+          "fixed-cidr-v6" = "fd00::/80";
+        };
+        description = ''
+          Configuration for docker daemon. The attributes are serialized to JSON used as daemon.conf.
+          See https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file
+        '';
+      };
+
+    enableNvidia =
+      mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable nvidia-docker wrapper, supporting NVIDIA GPUs inside docker containers.
+        '';
+      };
+
+    liveRestore =
+      mkOption {
+        type = types.bool;
+        default = true;
+        description =
+          ''
+            Allow dockerd to be restarted without affecting running container.
+            This option is incompatible with docker swarm.
+          '';
+      };
+
+    storageDriver =
+      mkOption {
+        type = types.nullOr (types.enum ["aufs" "btrfs" "devicemapper" "overlay" "overlay2" "zfs"]);
+        default = null;
+        description =
+          ''
+            This option determines which Docker storage driver to use. By default
+            it let's docker automatically choose preferred storage driver.
+          '';
+      };
+
+    logDriver =
+      mkOption {
+        type = types.enum ["none" "json-file" "syslog" "journald" "gelf" "fluentd" "awslogs" "splunk" "etwlogs" "gcplogs"];
+        default = "journald";
+        description =
+          ''
+            This option determines which Docker log driver to use.
+          '';
+      };
+
+    extraOptions =
+      mkOption {
+        type = types.separatedString " ";
+        default = "";
+        description =
+          ''
+            The extra command-line options to pass to
+            <command>docker</command> daemon.
+          '';
+      };
+
+    autoPrune = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to periodically prune Docker resources. If enabled, a
+          systemd timer will run <literal>docker system prune -f</literal>
+          as specified by the <literal>dates</literal> option.
+        '';
+      };
+
+      flags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "--all" ];
+        description = ''
+          Any additional flags passed to <command>docker system prune</command>.
+        '';
+      };
+
+      dates = mkOption {
+        default = "weekly";
+        type = types.str;
+        description = ''
+          Specification (in the format described by
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>) of the time at
+          which the prune will occur.
+        '';
+      };
+    };
+
+    package = mkOption {
+      default = pkgs.docker;
+      defaultText = literalExpression "pkgs.docker";
+      type = types.package;
+      example = literalExpression "pkgs.docker-edge";
+      description = ''
+        Docker package to be used in the module.
+      '';
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable (mkMerge [{
+      boot.kernelModules = [ "bridge" "veth" ];
+      boot.kernel.sysctl = {
+        "net.ipv4.conf.all.forwarding" = mkOverride 98 true;
+        "net.ipv4.conf.default.forwarding" = mkOverride 98 true;
+      };
+      environment.systemPackages = [ cfg.package ]
+        ++ optional cfg.enableNvidia pkgs.nvidia-docker;
+      users.groups.docker.gid = config.ids.gids.docker;
+      systemd.packages = [ cfg.package ];
+
+      systemd.services.docker = {
+        wantedBy = optional cfg.enableOnBoot "multi-user.target";
+        after = [ "network.target" "docker.socket" ];
+        requires = [ "docker.socket" ];
+        environment = proxy_env;
+        serviceConfig = {
+          Type = "notify";
+          ExecStart = [
+            ""
+            ''
+              ${cfg.package}/bin/dockerd \
+                --config-file=${daemonSettingsFile} \
+                ${cfg.extraOptions}
+            ''];
+          ExecReload=[
+            ""
+            "${pkgs.procps}/bin/kill -s HUP $MAINPID"
+          ];
+        };
+
+        path = [ pkgs.kmod ] ++ optional (cfg.storageDriver == "zfs") pkgs.zfs
+          ++ optional cfg.enableNvidia pkgs.nvidia-docker;
+      };
+
+      systemd.sockets.docker = {
+        description = "Docker Socket for the API";
+        wantedBy = [ "sockets.target" ];
+        socketConfig = {
+          ListenStream = cfg.listenOptions;
+          SocketMode = "0660";
+          SocketUser = "root";
+          SocketGroup = "docker";
+        };
+      };
+
+      systemd.services.docker-prune = {
+        description = "Prune docker resources";
+
+        restartIfChanged = false;
+        unitConfig.X-StopOnRemoval = false;
+
+        serviceConfig.Type = "oneshot";
+
+        script = ''
+          ${cfg.package}/bin/docker system prune -f ${toString cfg.autoPrune.flags}
+        '';
+
+        startAt = optional cfg.autoPrune.enable cfg.autoPrune.dates;
+      };
+
+      assertions = [
+        { assertion = cfg.enableNvidia -> config.hardware.opengl.driSupport32Bit or false;
+          message = "Option enableNvidia requires 32bit support libraries";
+        }];
+
+      virtualisation.docker.daemon.settings = {
+        group = "docker";
+        hosts = [ "fd://" ];
+        log-driver = mkDefault cfg.logDriver;
+        storage-driver = mkIf (cfg.storageDriver != null) (mkDefault cfg.storageDriver);
+        live-restore = mkDefault cfg.liveRestore;
+        runtimes = mkIf cfg.enableNvidia {
+          nvidia = {
+            path = "${pkgs.nvidia-docker}/bin/nvidia-container-runtime";
+          };
+        };
+      };
+    }
+  ]);
+
+  imports = [
+    (mkRemovedOptionModule ["virtualisation" "docker" "socketActivation"] "This option was removed and socket activation is now always active")
+  ];
+
+}
diff --git a/nixos/modules/virtualisation/ec2-amis.nix b/nixos/modules/virtualisation/ec2-amis.nix
new file mode 100644
index 00000000000..1ffb326ba7a
--- /dev/null
+++ b/nixos/modules/virtualisation/ec2-amis.nix
@@ -0,0 +1,9 @@
+# Compatibility shim
+let
+  lib = import ../../../lib;
+  inherit (lib) mapAttrs;
+  everything = import ./amazon-ec2-amis.nix;
+  doAllVersions = mapAttrs (versionName: doRegion);
+  doRegion = mapAttrs (regionName: systems: systems.x86_64-linux);
+in
+  doAllVersions everything
diff --git a/nixos/modules/virtualisation/ec2-data.nix b/nixos/modules/virtualisation/ec2-data.nix
new file mode 100644
index 00000000000..1b764e7e4d8
--- /dev/null
+++ b/nixos/modules/virtualisation/ec2-data.nix
@@ -0,0 +1,91 @@
+# This module defines a systemd service that sets the SSH host key and
+# authorized client key and host name of virtual machines running on
+# Amazon EC2, Eucalyptus and OpenStack Compute (Nova).
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  imports = [
+    (mkRemovedOptionModule [ "ec2" "metadata" ] "")
+  ];
+
+  config = {
+
+    systemd.services.apply-ec2-data =
+      { description = "Apply EC2 Data";
+
+        wantedBy = [ "multi-user.target" "sshd.service" ];
+        before = [ "sshd.service" ];
+
+        path = [ pkgs.iproute2 ];
+
+        script =
+          ''
+            ${optionalString (config.networking.hostName == "") ''
+              echo "setting host name..."
+              if [ -s /etc/ec2-metadata/hostname ]; then
+                  ${pkgs.nettools}/bin/hostname $(cat /etc/ec2-metadata/hostname)
+              fi
+            ''}
+
+            if ! [ -e /root/.ssh/authorized_keys ]; then
+                echo "obtaining SSH key..."
+                mkdir -m 0700 -p /root/.ssh
+                if [ -s /etc/ec2-metadata/public-keys-0-openssh-key ]; then
+                    cat /etc/ec2-metadata/public-keys-0-openssh-key >> /root/.ssh/authorized_keys
+                    echo "new key added to authorized_keys"
+                    chmod 600 /root/.ssh/authorized_keys
+                fi
+            fi
+
+            # Extract the intended SSH host key for this machine from
+            # the supplied user data, if available.  Otherwise sshd will
+            # generate one normally.
+            userData=/etc/ec2-metadata/user-data
+
+            mkdir -m 0755 -p /etc/ssh
+
+            if [ -s "$userData" ]; then
+              key="$(sed 's/|/\n/g; s/SSH_HOST_DSA_KEY://; t; d' $userData)"
+              key_pub="$(sed 's/SSH_HOST_DSA_KEY_PUB://; t; d' $userData)"
+              if [ -n "$key" -a -n "$key_pub" -a ! -e /etc/ssh/ssh_host_dsa_key ]; then
+                  (umask 077; echo "$key" > /etc/ssh/ssh_host_dsa_key)
+                  echo "$key_pub" > /etc/ssh/ssh_host_dsa_key.pub
+              fi
+
+              key="$(sed 's/|/\n/g; s/SSH_HOST_ED25519_KEY://; t; d' $userData)"
+              key_pub="$(sed 's/SSH_HOST_ED25519_KEY_PUB://; t; d' $userData)"
+              if [ -n "$key" -a -n "$key_pub" -a ! -e /etc/ssh/ssh_host_ed25519_key ]; then
+                  (umask 077; echo "$key" > /etc/ssh/ssh_host_ed25519_key)
+                  echo "$key_pub" > /etc/ssh/ssh_host_ed25519_key.pub
+              fi
+            fi
+          '';
+
+        serviceConfig.Type = "oneshot";
+        serviceConfig.RemainAfterExit = true;
+      };
+
+    systemd.services.print-host-key =
+      { description = "Print SSH Host Key";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "sshd.service" ];
+        script =
+          ''
+            # Print the host public key on the console so that the user
+            # can obtain it securely by parsing the output of
+            # ec2-get-console-output.
+            echo "-----BEGIN SSH HOST KEY FINGERPRINTS-----" > /dev/console
+            for i in /etc/ssh/ssh_host_*_key.pub; do
+                ${config.programs.ssh.package}/bin/ssh-keygen -l -f $i > /dev/console
+            done
+            echo "-----END SSH HOST KEY FINGERPRINTS-----" > /dev/console
+          '';
+        serviceConfig.Type = "oneshot";
+        serviceConfig.RemainAfterExit = true;
+      };
+
+  };
+}
diff --git a/nixos/modules/virtualisation/ec2-metadata-fetcher.nix b/nixos/modules/virtualisation/ec2-metadata-fetcher.nix
new file mode 100644
index 00000000000..760f024f33f
--- /dev/null
+++ b/nixos/modules/virtualisation/ec2-metadata-fetcher.nix
@@ -0,0 +1,77 @@
+{ curl, targetRoot, wgetExtraOptions }:
+# Note: be very cautious about dependencies, each dependency grows
+# the closure of the initrd. Ideally we would not even require curl,
+# but there is no reasonable way to send an HTTP PUT request without
+# it. Note: do not be fooled: the wget referenced in this script
+# is busybox's wget, not the fully featured one with --method support.
+#
+# Make sure that every package you depend on here is already listed as
+# a channel blocker for both the full-sized and small channels.
+# Otherwise, we risk breaking user deploys in released channels.
+#
+# Also note: OpenStack's metadata service for its instances aims to be
+# compatible with the EC2 IMDS. Where possible, try to keep the set of
+# fetched metadata in sync with ./openstack-metadata-fetcher.nix .
+''
+  metaDir=${targetRoot}etc/ec2-metadata
+  mkdir -m 0755 -p "$metaDir"
+  rm -f "$metaDir/*"
+
+  get_imds_token() {
+    # retry-delay of 1 selected to give the system a second to get going,
+    # but not add a lot to the bootup time
+    ${curl}/bin/curl \
+      -v \
+      --retry 3 \
+      --retry-delay 1 \
+      --fail \
+      -X PUT \
+      --connect-timeout 1 \
+      -H "X-aws-ec2-metadata-token-ttl-seconds: 600" \
+      http://169.254.169.254/latest/api/token
+  }
+
+  preflight_imds_token() {
+    # retry-delay of 1 selected to give the system a second to get going,
+    # but not add a lot to the bootup time
+    ${curl}/bin/curl \
+      -v \
+      --retry 3 \
+      --retry-delay 1 \
+      --fail \
+      --connect-timeout 1 \
+      -H "X-aws-ec2-metadata-token: $IMDS_TOKEN" \
+      http://169.254.169.254/1.0/meta-data/instance-id
+  }
+
+  try=1
+  while [ $try -le 3 ]; do
+    echo "(attempt $try/3) getting an EC2 instance metadata service v2 token..."
+    IMDS_TOKEN=$(get_imds_token) && break
+    try=$((try + 1))
+    sleep 1
+  done
+
+  if [ "x$IMDS_TOKEN" == "x" ]; then
+    echo "failed to fetch an IMDS2v token."
+  fi
+
+  try=1
+  while [ $try -le 10 ]; do
+    echo "(attempt $try/10) validating the EC2 instance metadata service v2 token..."
+    preflight_imds_token && break
+    try=$((try + 1))
+    sleep 1
+  done
+
+  echo "getting EC2 instance metadata..."
+
+  wget_imds() {
+    wget ${wgetExtraOptions} --header "X-aws-ec2-metadata-token: $IMDS_TOKEN" "$@";
+  }
+
+  wget_imds -O "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path
+  (umask 077 && wget_imds -O "$metaDir/user-data" http://169.254.169.254/1.0/user-data)
+  wget_imds -O "$metaDir/hostname" http://169.254.169.254/1.0/meta-data/hostname
+  wget_imds -O "$metaDir/public-keys-0-openssh-key" http://169.254.169.254/1.0/meta-data/public-keys/0/openssh-key
+''
diff --git a/nixos/modules/virtualisation/ecs-agent.nix b/nixos/modules/virtualisation/ecs-agent.nix
new file mode 100644
index 00000000000..aa38a02ea08
--- /dev/null
+++ b/nixos/modules/virtualisation/ecs-agent.nix
@@ -0,0 +1,45 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ecs-agent;
+in {
+  options.services.ecs-agent = {
+    enable = mkEnableOption "Amazon ECS agent";
+
+    package = mkOption {
+      type = types.path;
+      description = "The ECS agent package to use";
+      default = pkgs.ecs-agent;
+      defaultText = literalExpression "pkgs.ecs-agent";
+    };
+
+    extra-environment = mkOption {
+      type = types.attrsOf types.str;
+      description = "The environment the ECS agent should run with. See the ECS agent documentation for keys that work here.";
+      default = {};
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    # This service doesn't run if docker isn't running, and unlike potentially remote services like e.g., postgresql, docker has
+    # to be running locally so `docker.enable` will always be set if the ECS agent is enabled.
+    virtualisation.docker.enable = true;
+
+    systemd.services.ecs-agent = {
+      inherit (cfg.package.meta) description;
+      after    = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment = cfg.extra-environment;
+
+      script = ''
+        if [ ! -z "$ECS_DATADIR" ]; then
+          mkdir -p "$ECS_DATADIR"
+        fi
+        ${cfg.package}/bin/agent
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/gce-images.nix b/nixos/modules/virtualisation/gce-images.nix
new file mode 100644
index 00000000000..7b027619a44
--- /dev/null
+++ b/nixos/modules/virtualisation/gce-images.nix
@@ -0,0 +1,17 @@
+let self = {
+  "14.12" = "gs://nixos-cloud-images/nixos-14.12.471.1f09b77-x86_64-linux.raw.tar.gz";
+  "15.09" = "gs://nixos-cloud-images/nixos-15.09.425.7870f20-x86_64-linux.raw.tar.gz";
+  "16.03" = "gs://nixos-cloud-images/nixos-image-16.03.847.8688c17-x86_64-linux.raw.tar.gz";
+  "17.03" = "gs://nixos-cloud-images/nixos-image-17.03.1082.4aab5c5798-x86_64-linux.raw.tar.gz";
+  "18.03" = "gs://nixos-cloud-images/nixos-image-18.03.132536.fdb5ba4cdf9-x86_64-linux.raw.tar.gz";
+  "18.09" = "gs://nixos-cloud-images/nixos-image-18.09.1228.a4c4cbb613c-x86_64-linux.raw.tar.gz";
+
+  # This format will be handled by the upcoming NixOPS 2.0 release.
+  # The old images based on a GS object are deprecated.
+  "20.09" = {
+    project = "nixos-cloud";
+    name = "nixos-image-20-09-3531-3858fbc08e6-x86-64-linux";
+  };
+
+  latest = self."20.09";
+}; in self
diff --git a/nixos/modules/virtualisation/google-compute-config.nix b/nixos/modules/virtualisation/google-compute-config.nix
new file mode 100644
index 00000000000..44d2a589511
--- /dev/null
+++ b/nixos/modules/virtualisation/google-compute-config.nix
@@ -0,0 +1,102 @@
+{ config, lib, pkgs, ... }:
+with lib;
+{
+  imports = [
+    ../profiles/headless.nix
+    ../profiles/qemu-guest.nix
+  ];
+
+
+  fileSystems."/" = {
+    fsType = "ext4";
+    device = "/dev/disk/by-label/nixos";
+    autoResize = true;
+  };
+
+  boot.growPartition = true;
+  boot.kernelParams = [ "console=ttyS0" "panic=1" "boot.panic_on_fail" ];
+  boot.initrd.kernelModules = [ "virtio_scsi" ];
+  boot.kernelModules = [ "virtio_pci" "virtio_net" ];
+
+  # Generate a GRUB menu.
+  boot.loader.grub.device = "/dev/sda";
+  boot.loader.timeout = 0;
+
+  # Don't put old configurations in the GRUB menu.  The user has no
+  # way to select them anyway.
+  boot.loader.grub.configurationLimit = 0;
+
+  # 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;
+
+  # enable OS Login. This also requires setting enable-oslogin=TRUE metadata on
+  # instance or project level
+  security.googleOsLogin.enable = true;
+
+  # Use GCE udev rules for dynamic disk volumes
+  services.udev.packages = [ pkgs.google-guest-configs ];
+  services.udev.path = [ pkgs.google-guest-configs ];
+
+  # Force getting the hostname from Google Compute.
+  networking.hostName = mkDefault "";
+
+  # Always include cryptsetup so that NixOps can use it.
+  environment.systemPackages = [ pkgs.cryptsetup ];
+
+  # Rely on GCP's firewall instead
+  networking.firewall.enable = mkDefault false;
+
+  # Configure default metadata hostnames
+  networking.extraHosts = ''
+    169.254.169.254 metadata.google.internal metadata
+  '';
+
+  networking.timeServers = [ "metadata.google.internal" ];
+
+  networking.usePredictableInterfaceNames = false;
+
+  # GC has 1460 MTU
+  networking.interfaces.eth0.mtu = 1460;
+
+  systemd.packages = [ pkgs.google-guest-agent ];
+  systemd.services.google-guest-agent = {
+    wantedBy = [ "multi-user.target" ];
+    restartTriggers = [ config.environment.etc."default/instance_configs.cfg".source ];
+    path = lib.optional config.users.mutableUsers pkgs.shadow;
+  };
+  systemd.services.google-startup-scripts.wantedBy = [ "multi-user.target" ];
+  systemd.services.google-shutdown-scripts.wantedBy = [ "multi-user.target" ];
+
+  security.sudo.extraRules = mkIf config.users.mutableUsers [
+    { groups = [ "google-sudoers" ]; commands = [ { command = "ALL"; options = [ "NOPASSWD" ]; } ]; }
+  ];
+
+  users.groups.google-sudoers = mkIf config.users.mutableUsers { };
+
+  boot.extraModprobeConfig = lib.readFile "${pkgs.google-guest-configs}/etc/modprobe.d/gce-blacklist.conf";
+
+  environment.etc."sysctl.d/60-gce-network-security.conf".source = "${pkgs.google-guest-configs}/etc/sysctl.d/60-gce-network-security.conf";
+
+  environment.etc."default/instance_configs.cfg".text = ''
+    [Accounts]
+    useradd_cmd = useradd -m -s /run/current-system/sw/bin/bash -p * {user}
+
+    [Daemons]
+    accounts_daemon = ${boolToString config.users.mutableUsers}
+
+    [InstanceSetup]
+    # Make sure GCE image does not replace host key that NixOps sets.
+    set_host_keys = false
+
+    [MetadataScripts]
+    default_shell = ${pkgs.stdenv.shell}
+
+    [NetworkInterfaces]
+    dhclient_script = ${pkgs.google-guest-configs}/bin/google-dhclient-script
+    # We set up network interfaces declaratively.
+    setup = false
+  '';
+}
diff --git a/nixos/modules/virtualisation/google-compute-image.nix b/nixos/modules/virtualisation/google-compute-image.nix
new file mode 100644
index 00000000000..0c72696f802
--- /dev/null
+++ b/nixos/modules/virtualisation/google-compute-image.nix
@@ -0,0 +1,71 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.virtualisation.googleComputeImage;
+  defaultConfigFile = pkgs.writeText "configuration.nix" ''
+    { ... }:
+    {
+      imports = [
+        <nixpkgs/nixos/modules/virtualisation/google-compute-image.nix>
+      ];
+    }
+  '';
+in
+{
+
+  imports = [ ./google-compute-config.nix ];
+
+  options = {
+    virtualisation.googleComputeImage.diskSize = mkOption {
+      type = with types; either (enum [ "auto" ]) int;
+      default = "auto";
+      example = 1536;
+      description = ''
+        Size of disk image. Unit is MB.
+      '';
+    };
+
+    virtualisation.googleComputeImage.configFile = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      description = ''
+        A path to a configuration file which will be placed at `/etc/nixos/configuration.nix`
+        and be used when switching to a new configuration.
+        If set to `null`, a default configuration is used, where the only import is
+        `<nixpkgs/nixos/modules/virtualisation/google-compute-image.nix>`.
+      '';
+    };
+
+    virtualisation.googleComputeImage.compressionLevel = mkOption {
+      type = types.int;
+      default = 6;
+      description = ''
+        GZIP compression level of the resulting disk image (1-9).
+      '';
+    };
+  };
+
+  #### implementation
+  config = {
+
+    system.build.googleComputeImage = import ../../lib/make-disk-image.nix {
+      name = "google-compute-image";
+      postVM = ''
+        PATH=$PATH:${with pkgs; lib.makeBinPath [ gnutar gzip ]}
+        pushd $out
+        mv $diskImage disk.raw
+        tar -Sc disk.raw | gzip -${toString cfg.compressionLevel} > \
+          nixos-image-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.raw.tar.gz
+        rm $out/disk.raw
+        popd
+      '';
+      format = "raw";
+      configFile = if cfg.configFile == null then defaultConfigFile else cfg.configFile;
+      inherit (cfg) diskSize;
+      inherit config lib pkgs;
+    };
+
+  };
+
+}
diff --git a/nixos/modules/virtualisation/grow-partition.nix b/nixos/modules/virtualisation/grow-partition.nix
new file mode 100644
index 00000000000..444c0bc1630
--- /dev/null
+++ b/nixos/modules/virtualisation/grow-partition.nix
@@ -0,0 +1,3 @@
+# This profile is deprecated, use boot.growPartition directly.
+builtins.trace "the profile <nixos/modules/virtualisation/grow-partition.nix> is deprecated, use boot.growPartition instead"
+{ }
diff --git a/nixos/modules/virtualisation/hyperv-guest.nix b/nixos/modules/virtualisation/hyperv-guest.nix
new file mode 100644
index 00000000000..fb6502644b8
--- /dev/null
+++ b/nixos/modules/virtualisation/hyperv-guest.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.virtualisation.hypervGuest;
+
+in {
+  options = {
+    virtualisation.hypervGuest = {
+      enable = mkEnableOption "Hyper-V Guest Support";
+
+      videoMode = mkOption {
+        type = types.str;
+        default = "1152x864";
+        example = "1024x768";
+        description = ''
+          Resolution at which to initialize the video adapter.
+
+          Supports screen resolution up to Full HD 1920x1080 with 32 bit color
+          on Windows Server 2012, and 1600x1200 with 16 bit color on Windows
+          Server 2008 R2 or earlier.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    boot = {
+      initrd.kernelModules = [
+        "hv_balloon" "hv_netvsc" "hv_storvsc" "hv_utils" "hv_vmbus"
+      ];
+
+      initrd.availableKernelModules = [ "hyperv_keyboard" ];
+
+      kernelParams = [
+        "video=hyperv_fb:${cfg.videoMode}" "elevator=noop"
+      ];
+    };
+
+    environment.systemPackages = [ config.boot.kernelPackages.hyperv-daemons.bin ];
+
+    # enable hotadding cpu/memory
+    services.udev.packages = lib.singleton (pkgs.writeTextFile {
+      name = "hyperv-cpu-and-memory-hotadd-udev-rules";
+      destination = "/etc/udev/rules.d/99-hyperv-cpu-and-memory-hotadd.rules";
+      text = ''
+        # Memory hotadd
+        SUBSYSTEM=="memory", ACTION=="add", DEVPATH=="/devices/system/memory/memory[0-9]*", TEST=="state", ATTR{state}="online"
+
+        # CPU hotadd
+        SUBSYSTEM=="cpu", ACTION=="add", DEVPATH=="/devices/system/cpu/cpu[0-9]*", TEST=="online", ATTR{online}="1"
+      '';
+    });
+
+    systemd = {
+      packages = [ config.boot.kernelPackages.hyperv-daemons.lib ];
+
+      services.hv-vss.unitConfig.ConditionPathExists = [ "/dev/vmbus/hv_vss" ];
+
+      targets.hyperv-daemons = {
+        wantedBy = [ "multi-user.target" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/hyperv-image.nix b/nixos/modules/virtualisation/hyperv-image.nix
new file mode 100644
index 00000000000..6845d675009
--- /dev/null
+++ b/nixos/modules/virtualisation/hyperv-image.nix
@@ -0,0 +1,71 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.hyperv;
+
+in {
+  options = {
+    hyperv = {
+      baseImageSize = mkOption {
+        type = with types; either (enum [ "auto" ]) int;
+        default = "auto";
+        example = 2048;
+        description = ''
+          The size of the hyper-v base image in MiB.
+        '';
+      };
+      vmDerivationName = mkOption {
+        type = types.str;
+        default = "nixos-hyperv-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}";
+        description = ''
+          The name of the derivation for the hyper-v appliance.
+        '';
+      };
+      vmFileName = mkOption {
+        type = types.str;
+        default = "nixos-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.vhdx";
+        description = ''
+          The file name of the hyper-v appliance.
+        '';
+      };
+    };
+  };
+
+  config = {
+    system.build.hypervImage = import ../../lib/make-disk-image.nix {
+      name = cfg.vmDerivationName;
+      postVM = ''
+        ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -o subformat=dynamic -O vhdx $diskImage $out/${cfg.vmFileName}
+        rm $diskImage
+      '';
+      format = "raw";
+      diskSize = cfg.baseImageSize;
+      partitionTableType = "efi";
+      inherit config lib pkgs;
+    };
+
+    fileSystems."/" = {
+      device = "/dev/disk/by-label/nixos";
+      autoResize = true;
+      fsType = "ext4";
+    };
+
+    fileSystems."/boot" = {
+      device = "/dev/disk/by-label/ESP";
+      fsType = "vfat";
+    };
+
+    boot.growPartition = true;
+
+    boot.loader.grub = {
+      version = 2;
+      device = "nodev";
+      efiSupport = true;
+      efiInstallAsRemovable = true;
+    };
+
+    virtualisation.hypervGuest.enable = true;
+  };
+}
diff --git a/nixos/modules/virtualisation/kubevirt.nix b/nixos/modules/virtualisation/kubevirt.nix
new file mode 100644
index 00000000000..408822b6af0
--- /dev/null
+++ b/nixos/modules/virtualisation/kubevirt.nix
@@ -0,0 +1,30 @@
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ../profiles/qemu-guest.nix
+  ];
+
+  config = {
+    fileSystems."/" = {
+      device = "/dev/disk/by-label/nixos";
+      fsType = "ext4";
+      autoResize = true;
+    };
+
+    boot.growPartition = true;
+    boot.kernelParams = [ "console=ttyS0" ];
+    boot.loader.grub.device = "/dev/vda";
+    boot.loader.timeout = 0;
+
+    services.qemuGuest.enable = true;
+    services.openssh.enable = true;
+    services.cloud-init.enable = true;
+    systemd.services."serial-getty@ttyS0".enable = true;
+
+    system.build.kubevirtImage = import ../../lib/make-disk-image.nix {
+      inherit lib config pkgs;
+      format = "qcow2";
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/kvmgt.nix b/nixos/modules/virtualisation/kvmgt.nix
new file mode 100644
index 00000000000..5e7a73bec90
--- /dev/null
+++ b/nixos/modules/virtualisation/kvmgt.nix
@@ -0,0 +1,86 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.virtualisation.kvmgt;
+
+  kernelPackages = config.boot.kernelPackages;
+
+  vgpuOptions = {
+    uuid = mkOption {
+      type = with types; listOf str;
+      description = "UUID(s) of VGPU device. You can generate one with <package>libossp_uuid</package>.";
+    };
+  };
+
+in {
+  options = {
+    virtualisation.kvmgt = {
+      enable = mkEnableOption ''
+        KVMGT (iGVT-g) VGPU support. Allows Qemu/KVM guests to share host's Intel integrated graphics card.
+        Currently only one graphical device can be shared. To allow users to access the device without root add them
+        to the kvm group: <literal>users.extraUsers.&lt;yourusername&gt;.extraGroups = [ "kvm" ];</literal>
+      '';
+      # multi GPU support is under the question
+      device = mkOption {
+        type = types.str;
+        default = "0000:00:02.0";
+        description = "PCI ID of graphics card. You can figure it with <command>ls /sys/class/mdev_bus</command>.";
+      };
+      vgpus = mkOption {
+        default = {};
+        type = with types; attrsOf (submodule [ { options = vgpuOptions; } ]);
+        description = ''
+          Virtual GPUs to be used in Qemu. You can find devices via <command>ls /sys/bus/pci/devices/*/mdev_supported_types</command>
+          and find info about device via <command>cat /sys/bus/pci/devices/*/mdev_supported_types/i915-GVTg_V5_4/description</command>
+        '';
+        example = {
+          i915-GVTg_V5_8.uuid = [ "a297db4a-f4c2-11e6-90f6-d3b88d6c9525" ];
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = singleton {
+      assertion = versionAtLeast kernelPackages.kernel.version "4.16";
+      message = "KVMGT is not properly supported for kernels older than 4.16";
+    };
+
+    boot.kernelModules = [ "kvmgt" ];
+    boot.kernelParams = [ "i915.enable_gvt=1" ];
+
+    services.udev.extraRules = ''
+      SUBSYSTEM=="vfio", OWNER="root", GROUP="kvm"
+    '';
+
+    systemd = let
+      vgpus = listToAttrs (flatten (mapAttrsToList
+        (mdev: opt: map (id: nameValuePair "kvmgt-${id}" { inherit mdev; uuid = id; }) opt.uuid)
+        cfg.vgpus));
+    in {
+      paths = mapAttrs (_: opt:
+        {
+          description = "KVMGT VGPU ${opt.uuid} path";
+          wantedBy = [ "multi-user.target" ];
+          pathConfig = {
+            PathExists = "/sys/bus/pci/devices/${cfg.device}/mdev_supported_types/${opt.mdev}/create";
+          };
+        }) vgpus;
+
+      services = mapAttrs (_: opt:
+        {
+          description = "KVMGT VGPU ${opt.uuid}";
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+            ExecStart = "${pkgs.runtimeShell} -c 'echo ${opt.uuid} > /sys/bus/pci/devices/${cfg.device}/mdev_supported_types/${opt.mdev}/create'";
+            ExecStop = "${pkgs.runtimeShell} -c 'echo 1 > /sys/bus/pci/devices/${cfg.device}/${opt.uuid}/remove'";
+          };
+        }) vgpus;
+    };
+  };
+
+  meta.maintainers = with maintainers; [ patryk27 ];
+}
diff --git a/nixos/modules/virtualisation/libvirtd.nix b/nixos/modules/virtualisation/libvirtd.nix
new file mode 100644
index 00000000000..ab87394a30e
--- /dev/null
+++ b/nixos/modules/virtualisation/libvirtd.nix
@@ -0,0 +1,392 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.virtualisation.libvirtd;
+  vswitch = config.virtualisation.vswitch;
+  configFile = pkgs.writeText "libvirtd.conf" ''
+    auth_unix_ro = "polkit"
+    auth_unix_rw = "polkit"
+    ${cfg.extraConfig}
+  '';
+  ovmfFilePrefix = if pkgs.stdenv.isAarch64 then "AAVMF" else "OVMF";
+  qemuConfigFile = pkgs.writeText "qemu.conf" ''
+    ${optionalString cfg.qemu.ovmf.enable ''
+      nvram = [ "/run/libvirt/nix-ovmf/${ovmfFilePrefix}_CODE.fd:/run/libvirt/nix-ovmf/${ovmfFilePrefix}_VARS.fd" ]
+    ''}
+    ${optionalString (!cfg.qemu.runAsRoot) ''
+      user = "qemu-libvirtd"
+      group = "qemu-libvirtd"
+    ''}
+    ${cfg.qemu.verbatimConfig}
+  '';
+  dirName = "libvirt";
+  subDirs = list: [ dirName ] ++ map (e: "${dirName}/${e}") list;
+
+  ovmfModule = types.submodule {
+    options = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Allows libvirtd to take advantage of OVMF when creating new
+          QEMU VMs with UEFI boot.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.OVMF;
+        defaultText = literalExpression "pkgs.OVMF";
+        example = literalExpression "pkgs.OVMFFull";
+        description = ''
+          OVMF package to use.
+        '';
+      };
+    };
+  };
+
+  swtpmModule = types.submodule {
+    options = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Allows libvirtd to use swtpm to create an emulated TPM.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.swtpm;
+        defaultText = literalExpression "pkgs.swtpm";
+        description = ''
+          swtpm package to use.
+        '';
+      };
+    };
+  };
+
+  qemuModule = types.submodule {
+    options = {
+      package = mkOption {
+        type = types.package;
+        default = pkgs.qemu;
+        defaultText = literalExpression "pkgs.qemu";
+        description = ''
+          Qemu package to use with libvirt.
+          `pkgs.qemu` can emulate alien architectures (e.g. aarch64 on x86)
+          `pkgs.qemu_kvm` saves disk space allowing to emulate only host architectures.
+        '';
+      };
+
+      runAsRoot = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          If true,  libvirtd runs qemu as root.
+          If false, libvirtd runs qemu as unprivileged user qemu-libvirtd.
+          Changing this option to false may cause file permission issues
+          for existing guests. To fix these, manually change ownership
+          of affected files in /var/lib/libvirt/qemu to qemu-libvirtd.
+        '';
+      };
+
+      verbatimConfig = mkOption {
+        type = types.lines;
+        default = ''
+          namespaces = []
+        '';
+        description = ''
+          Contents written to the qemu configuration file, qemu.conf.
+          Make sure to include a proper namespace configuration when
+          supplying custom configuration.
+        '';
+      };
+
+      ovmf = mkOption {
+        type = ovmfModule;
+        default = { };
+        description = ''
+          QEMU's OVMF options.
+        '';
+      };
+
+      swtpm = mkOption {
+        type = swtpmModule;
+        default = { };
+        description = ''
+          QEMU's swtpm options.
+        '';
+      };
+    };
+  };
+in
+{
+
+  imports = [
+    (mkRemovedOptionModule [ "virtualisation" "libvirtd" "enableKVM" ]
+      "Set the option `virtualisation.libvirtd.qemu.package' instead.")
+    (mkRenamedOptionModule
+      [ "virtualisation" "libvirtd" "qemuPackage" ]
+      [ "virtualisation" "libvirtd" "qemu" "package" ])
+    (mkRenamedOptionModule
+      [ "virtualisation" "libvirtd" "qemuRunAsRoot" ]
+      [ "virtualisation" "libvirtd" "qemu" "runAsRoot" ])
+    (mkRenamedOptionModule
+      [ "virtualisation" "libvirtd" "qemuVerbatimConfig" ]
+      [ "virtualisation" "libvirtd" "qemu" "verbatimConfig" ])
+    (mkRenamedOptionModule
+      [ "virtualisation" "libvirtd" "qemuOvmf" ]
+      [ "virtualisation" "libvirtd" "qemu" "ovmf" "enable" ])
+    (mkRenamedOptionModule
+      [ "virtualisation" "libvirtd" "qemuOvmfPackage" ]
+      [ "virtualisation" "libvirtd" "qemu" "ovmf" "package" ])
+    (mkRenamedOptionModule
+      [ "virtualisation" "libvirtd" "qemuSwtpm" ]
+      [ "virtualisation" "libvirtd" "qemu" "swtpm" "enable" ])
+  ];
+
+  ###### interface
+
+  options.virtualisation.libvirtd = {
+
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        This option enables libvirtd, a daemon that manages
+        virtual machines. Users in the "libvirtd" group can interact with
+        the daemon (e.g. to start or stop VMs) using the
+        <command>virsh</command> command line tool, among others.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.libvirt;
+      defaultText = literalExpression "pkgs.libvirt";
+      description = ''
+        libvirt package to use.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Extra contents appended to the libvirtd configuration file,
+        libvirtd.conf.
+      '';
+    };
+
+    extraOptions = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = [ "--verbose" ];
+      description = ''
+        Extra command line arguments passed to libvirtd on startup.
+      '';
+    };
+
+    onBoot = mkOption {
+      type = types.enum [ "start" "ignore" ];
+      default = "start";
+      description = ''
+        Specifies the action to be done to / on the guests when the host boots.
+        The "start" option starts all guests that were running prior to shutdown
+        regardless of their autostart settings. The "ignore" option will not
+        start the formerly running guest on boot. However, any guest marked as
+        autostart will still be automatically started by libvirtd.
+      '';
+    };
+
+    onShutdown = mkOption {
+      type = types.enum [ "shutdown" "suspend" ];
+      default = "suspend";
+      description = ''
+        When shutting down / restarting the host what method should
+        be used to gracefully halt the guests. Setting to "shutdown"
+        will cause an ACPI shutdown of each guest. "suspend" will
+        attempt to save the state of the guests ready to restore on boot.
+      '';
+    };
+
+    allowedBridges = mkOption {
+      type = types.listOf types.str;
+      default = [ "virbr0" ];
+      description = ''
+        List of bridge devices that can be used by qemu:///session
+      '';
+    };
+
+    qemu = mkOption {
+      type = qemuModule;
+      default = { };
+      description = ''
+        QEMU related options.
+      '';
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = config.security.polkit.enable;
+        message = "The libvirtd module currently requires Polkit to be enabled ('security.polkit.enable = true').";
+      }
+      {
+        assertion = builtins.elem "fd" cfg.qemu.ovmf.package.outputs;
+        message = "The option 'virtualisation.libvirtd.qemuOvmfPackage' needs a package that has an 'fd' output.";
+      }
+    ];
+
+    environment = {
+      # this file is expected in /etc/qemu and not sysconfdir (/var/lib)
+      etc."qemu/bridge.conf".text = lib.concatMapStringsSep "\n"
+        (e:
+          "allow ${e}")
+        cfg.allowedBridges;
+      systemPackages = with pkgs; [ libressl.nc iptables cfg.package cfg.qemu.package ];
+      etc.ethertypes.source = "${pkgs.iptables}/etc/ethertypes";
+    };
+
+    boot.kernelModules = [ "tun" ];
+
+    users.groups.libvirtd.gid = config.ids.gids.libvirtd;
+
+    # libvirtd runs qemu as this user and group by default
+    users.extraGroups.qemu-libvirtd.gid = config.ids.gids.qemu-libvirtd;
+    users.extraUsers.qemu-libvirtd = {
+      uid = config.ids.uids.qemu-libvirtd;
+      isNormalUser = false;
+      group = "qemu-libvirtd";
+    };
+
+    security.wrappers.qemu-bridge-helper = {
+      setuid = true;
+      owner = "root";
+      group = "root";
+      source = "/run/${dirName}/nix-helpers/qemu-bridge-helper";
+    };
+
+    systemd.packages = [ cfg.package ];
+
+    systemd.services.libvirtd-config = {
+      description = "Libvirt Virtual Machine Management Daemon - configuration";
+      script = ''
+        # Copy default libvirt network config .xml files to /var/lib
+        # Files modified by the user will not be overwritten
+        for i in $(cd ${cfg.package}/var/lib && echo \
+            libvirt/qemu/networks/*.xml libvirt/qemu/networks/autostart/*.xml \
+            libvirt/nwfilter/*.xml );
+        do
+            mkdir -p /var/lib/$(dirname $i) -m 755
+            cp -npd ${cfg.package}/var/lib/$i /var/lib/$i
+        done
+
+        # Copy generated qemu config to libvirt directory
+        cp -f ${qemuConfigFile} /var/lib/${dirName}/qemu.conf
+
+        # stable (not GC'able as in /nix/store) paths for using in <emulator> section of xml configs
+        for emulator in ${cfg.package}/libexec/libvirt_lxc ${cfg.qemu.package}/bin/qemu-kvm ${cfg.qemu.package}/bin/qemu-system-*; do
+          ln -s --force "$emulator" /run/${dirName}/nix-emulators/
+        done
+
+        for helper in libexec/qemu-bridge-helper bin/qemu-pr-helper; do
+          ln -s --force ${cfg.qemu.package}/$helper /run/${dirName}/nix-helpers/
+        done
+
+        ${optionalString cfg.qemu.ovmf.enable ''
+          ln -s --force ${cfg.qemu.ovmf.package.fd}/FV/${ovmfFilePrefix}_CODE.fd /run/${dirName}/nix-ovmf/
+          ln -s --force ${cfg.qemu.ovmf.package.fd}/FV/${ovmfFilePrefix}_VARS.fd /run/${dirName}/nix-ovmf/
+        ''}
+      '';
+
+      serviceConfig = {
+        Type = "oneshot";
+        RuntimeDirectoryPreserve = "yes";
+        LogsDirectory = subDirs [ "qemu" ];
+        RuntimeDirectory = subDirs [ "nix-emulators" "nix-helpers" "nix-ovmf" ];
+        StateDirectory = subDirs [ "dnsmasq" ];
+      };
+    };
+
+    systemd.services.libvirtd = {
+      requires = [ "libvirtd-config.service" ];
+      after = [ "libvirtd-config.service" ]
+        ++ optional vswitch.enable "ovs-vswitchd.service";
+
+      environment.LIBVIRTD_ARGS = escapeShellArgs (
+        [
+          "--config"
+          configFile
+          "--timeout"
+          "120" # from ${libvirt}/var/lib/sysconfig/libvirtd
+        ] ++ cfg.extraOptions
+      );
+
+      path = [ cfg.qemu.package ] # libvirtd requires qemu-img to manage disk images
+        ++ optional vswitch.enable vswitch.package
+        ++ optional cfg.qemu.swtpm.enable cfg.qemu.swtpm.package;
+
+      serviceConfig = {
+        Type = "notify";
+        KillMode = "process"; # when stopping, leave the VMs alone
+        Restart = "no";
+      };
+      restartIfChanged = false;
+    };
+
+    systemd.services.libvirt-guests = {
+      wantedBy = [ "multi-user.target" ];
+      path = with pkgs; [ coreutils gawk cfg.package ];
+      restartIfChanged = false;
+
+      environment.ON_BOOT = "${cfg.onBoot}";
+      environment.ON_SHUTDOWN = "${cfg.onShutdown}";
+    };
+
+    systemd.sockets.virtlogd = {
+      description = "Virtual machine log manager socket";
+      wantedBy = [ "sockets.target" ];
+      listenStreams = [ "/run/${dirName}/virtlogd-sock" ];
+    };
+
+    systemd.services.virtlogd = {
+      description = "Virtual machine log manager";
+      serviceConfig.ExecStart = "@${cfg.package}/sbin/virtlogd virtlogd";
+      restartIfChanged = false;
+    };
+
+    systemd.sockets.virtlockd = {
+      description = "Virtual machine lock manager socket";
+      wantedBy = [ "sockets.target" ];
+      listenStreams = [ "/run/${dirName}/virtlockd-sock" ];
+    };
+
+    systemd.services.virtlockd = {
+      description = "Virtual machine lock manager";
+      serviceConfig.ExecStart = "@${cfg.package}/sbin/virtlockd virtlockd";
+      restartIfChanged = false;
+    };
+
+    # https://libvirt.org/daemons.html#monolithic-systemd-integration
+    systemd.sockets.libvirtd.wantedBy = [ "sockets.target" ];
+
+    security.polkit.extraConfig = ''
+      polkit.addRule(function(action, subject) {
+        if (action.id == "org.libvirt.unix.manage" &&
+          subject.isInGroup("libvirtd")) {
+          return polkit.Result.YES;
+        }
+      });
+    '';
+  };
+}
diff --git a/nixos/modules/virtualisation/lxc-container.nix b/nixos/modules/virtualisation/lxc-container.nix
new file mode 100644
index 00000000000..9816cc2332f
--- /dev/null
+++ b/nixos/modules/virtualisation/lxc-container.nix
@@ -0,0 +1,174 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+
+let
+  templateSubmodule = { ... }: {
+    options = {
+      enable = mkEnableOption "this template";
+
+      target = mkOption {
+        description = "Path in the container";
+        type = types.path;
+      };
+      template = mkOption {
+        description = ".tpl file for rendering the target";
+        type = types.path;
+      };
+      when = mkOption {
+        description = "Events which trigger a rewrite (create, copy)";
+        type = types.listOf (types.str);
+      };
+      properties = mkOption {
+        description = "Additional properties";
+        type = types.attrs;
+        default = {};
+      };
+    };
+  };
+
+  toYAML = name: data: pkgs.writeText name (generators.toYAML {} data);
+
+  cfg = config.virtualisation.lxc;
+  templates = if cfg.templates != {} then let
+    list = mapAttrsToList (name: value: { inherit name; } // value)
+      (filterAttrs (name: value: value.enable) cfg.templates);
+  in
+    {
+      files = map (tpl: {
+        source = tpl.template;
+        target = "/templates/${tpl.name}.tpl";
+      }) list;
+      properties = listToAttrs (map (tpl: nameValuePair tpl.target {
+        when = tpl.when;
+        template = "${tpl.name}.tpl";
+        properties = tpl.properties;
+      }) list);
+    }
+  else { files = []; properties = {}; };
+
+in
+{
+  imports = [
+    ../installer/cd-dvd/channel.nix
+    ../profiles/minimal.nix
+    ../profiles/clone-config.nix
+  ];
+
+  options = {
+    virtualisation.lxc = {
+      templates = mkOption {
+        description = "Templates for LXD";
+        type = types.attrsOf (types.submodule (templateSubmodule));
+        default = {};
+        example = literalExpression ''
+          {
+            # create /etc/hostname on container creation
+            "hostname" = {
+              enable = true;
+              target = "/etc/hostname";
+              template = builtins.writeFile "hostname.tpl" "{{ container.name }}";
+              when = [ "create" ];
+            };
+            # create /etc/nixos/hostname.nix with a configuration for keeping the hostname applied
+            "hostname-nix" = {
+              enable = true;
+              target = "/etc/nixos/hostname.nix";
+              template = builtins.writeFile "hostname-nix.tpl" "{ ... }: { networking.hostName = "{{ container.name }}"; }";
+              # copy keeps the file updated when the container is changed
+              when = [ "create" "copy" ];
+            };
+            # copy allow the user to specify a custom configuration.nix
+            "configuration-nix" = {
+              enable = true;
+              target = "/etc/nixos/configuration.nix";
+              template = builtins.writeFile "configuration-nix" "{{ config_get(\"user.user-data\", properties.default) }}";
+              when = [ "create" ];
+            };
+          };
+        '';
+      };
+    };
+  };
+
+  config = {
+    boot.isContainer = true;
+    boot.postBootCommands =
+      ''
+        # After booting, register the contents of the Nix store in the Nix
+        # database.
+        if [ -f /nix-path-registration ]; then
+          ${config.nix.package.out}/bin/nix-store --load-db < /nix-path-registration &&
+          rm /nix-path-registration
+        fi
+
+        # nixos-rebuild also requires a "system" profile
+        ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
+      '';
+
+    system.build.metadata = pkgs.callPackage ../../lib/make-system-tarball.nix {
+      contents = [
+        {
+          source = toYAML "metadata.yaml" {
+            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";
+              release = "${config.system.nixos.codeName}";
+            };
+            templates = templates.properties;
+          };
+          target = "/metadata.yaml";
+        }
+      ] ++ templates.files;
+    };
+
+    # TODO: build rootfs as squashfs for faster unpack
+    system.build.tarball = pkgs.callPackage ../../lib/make-system-tarball.nix {
+      extraArgs = "--owner=0";
+
+      storeContents = [
+        {
+          object = config.system.build.toplevel;
+          symlink = "none";
+        }
+      ];
+
+      contents = [
+        {
+          source = config.system.build.toplevel + "/init";
+          target = "/sbin/init";
+        }
+      ];
+
+      extraCommands = "mkdir -p proc sys dev";
+    };
+
+    # Add the overrides from lxd distrobuilder
+    systemd.extraConfig = ''
+      [Service]
+      ProtectProc=default
+      ProtectControlGroups=no
+      ProtectKernelTunables=no
+    '';
+
+    # Allow the user to login as root without password.
+    users.users.root.initialHashedPassword = mkOverride 150 "";
+
+    system.activationScripts.installInitScript = mkForce ''
+      ln -fs $systemConfig/init /sbin/init
+    '';
+
+    # Some more help text.
+    services.getty.helpLine =
+      ''
+
+        Log in as "root" with an empty password.
+      '';
+
+    # Containers should be light-weight, so start sshd on demand.
+    services.openssh.enable = mkDefault true;
+    services.openssh.startWhenNeeded = mkDefault true;
+  };
+}
diff --git a/nixos/modules/virtualisation/lxc.nix b/nixos/modules/virtualisation/lxc.nix
new file mode 100644
index 00000000000..0f8b22a45df
--- /dev/null
+++ b/nixos/modules/virtualisation/lxc.nix
@@ -0,0 +1,86 @@
+# LXC Configuration
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.virtualisation.lxc;
+
+in
+
+{
+  ###### interface
+
+  options.virtualisation.lxc = {
+    enable =
+      mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          ''
+            This enables Linux Containers (LXC), which provides tools
+            for creating and managing system or application containers
+            on Linux.
+          '';
+      };
+
+    systemConfig =
+      mkOption {
+        type = types.lines;
+        default = "";
+        description =
+          ''
+            This is the system-wide LXC config. See
+            <citerefentry><refentrytitle>lxc.system.conf</refentrytitle>
+            <manvolnum>5</manvolnum></citerefentry>.
+          '';
+      };
+
+    defaultConfig =
+      mkOption {
+        type = types.lines;
+        default = "";
+        description =
+          ''
+            Default config (default.conf) for new containers, i.e. for
+            network config. See <citerefentry><refentrytitle>lxc.container.conf
+            </refentrytitle><manvolnum>5</manvolnum></citerefentry>.
+          '';
+      };
+
+    usernetConfig =
+      mkOption {
+        type = types.lines;
+        default = "";
+        description =
+          ''
+            This is the config file for managing unprivileged user network
+            administration access in LXC. See <citerefentry>
+            <refentrytitle>lxc-usernet</refentrytitle><manvolnum>5</manvolnum>
+            </citerefentry>.
+          '';
+      };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.lxc ];
+    environment.etc."lxc/lxc.conf".text = cfg.systemConfig;
+    environment.etc."lxc/lxc-usernet".text = cfg.usernetConfig;
+    environment.etc."lxc/default.conf".text = cfg.defaultConfig;
+    systemd.tmpfiles.rules = [ "d /var/lib/lxc/rootfs 0755 root root -" ];
+
+    security.apparmor.packages = [ pkgs.lxc ];
+    security.apparmor.policies = {
+      "bin.lxc-start".profile = ''
+        include ${pkgs.lxc}/etc/apparmor.d/usr.bin.lxc-start
+      '';
+      "lxc-containers".profile = ''
+        include ${pkgs.lxc}/etc/apparmor.d/lxc-containers
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/lxcfs.nix b/nixos/modules/virtualisation/lxcfs.nix
new file mode 100644
index 00000000000..b2457403463
--- /dev/null
+++ b/nixos/modules/virtualisation/lxcfs.nix
@@ -0,0 +1,45 @@
+# LXC Configuration
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.virtualisation.lxc.lxcfs;
+in {
+  meta.maintainers = [ maintainers.mic92 ];
+
+  ###### interface
+  options.virtualisation.lxc.lxcfs = {
+    enable =
+      mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          This enables LXCFS, a FUSE filesystem for LXC.
+          To use lxcfs in include the following configuration in your
+          container configuration:
+          <code>
+            virtualisation.lxc.defaultConfig = "lxc.include = ''${pkgs.lxcfs}/share/lxc/config/common.conf.d/00-lxcfs.conf";
+          </code>
+        '';
+      };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    systemd.services.lxcfs = {
+      description = "FUSE filesystem for LXC";
+      wantedBy = [ "multi-user.target" ];
+      before = [ "lxc.service" ];
+      restartIfChanged = false;
+      serviceConfig = {
+        ExecStartPre="${pkgs.coreutils}/bin/mkdir -p /var/lib/lxcfs";
+        ExecStart="${pkgs.lxcfs}/bin/lxcfs /var/lib/lxcfs";
+        ExecStopPost="-${pkgs.fuse}/bin/fusermount -u /var/lib/lxcfs";
+        KillMode="process";
+        Restart="on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/lxd.nix b/nixos/modules/virtualisation/lxd.nix
new file mode 100644
index 00000000000..18451b147ff
--- /dev/null
+++ b/nixos/modules/virtualisation/lxd.nix
@@ -0,0 +1,182 @@
+# Systemd services for lxd.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.virtualisation.lxd;
+in {
+  imports = [
+    (mkRemovedOptionModule [ "virtualisation" "lxd" "zfsPackage" ] "Override zfs in an overlay instead to override it globally")
+  ];
+
+  ###### interface
+
+  options = {
+    virtualisation.lxd = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          This option enables lxd, a daemon that manages
+          containers. Users in the "lxd" group can interact with
+          the daemon (e.g. to start or stop containers) using the
+          <command>lxc</command> command line tool, among others.
+
+          Most of the time, you'll also want to start lxcfs, so
+          that containers can "see" the limits:
+          <code>
+            virtualisation.lxc.lxcfs.enable = true;
+          </code>
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.lxd;
+        defaultText = literalExpression "pkgs.lxd";
+        description = ''
+          The LXD package to use.
+        '';
+      };
+
+      lxcPackage = mkOption {
+        type = types.package;
+        default = pkgs.lxc;
+        defaultText = literalExpression "pkgs.lxc";
+        description = ''
+          The LXC package to use with LXD (required for AppArmor profiles).
+        '';
+      };
+
+      zfsSupport = mkOption {
+        type = types.bool;
+        default = config.boot.zfs.enabled;
+        defaultText = literalExpression "config.boot.zfs.enabled";
+        description = ''
+          Enables lxd to use zfs as a storage for containers.
+
+          This option is enabled by default if a zfs pool is configured
+          with nixos.
+        '';
+      };
+
+      recommendedSysctlSettings = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enables various settings to avoid common pitfalls when
+          running containers requiring many file operations.
+          Fixes errors like "Too many open files" or
+          "neighbour: ndisc_cache: neighbor table overflow!".
+          See https://lxd.readthedocs.io/en/latest/production-setup/
+          for details.
+        '';
+      };
+
+      startTimeout = mkOption {
+        type = types.int;
+        default = 600;
+        apply = toString;
+        description = ''
+          Time to wait (in seconds) for LXD to become ready to process requests.
+          If LXD does not reply within the configured time, lxd.service will be
+          considered failed and systemd will attempt to restart it.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    # Note: the following options are also declared in virtualisation.lxc, but
+    # the latter can't be simply enabled to reuse the formers, because it
+    # does a bunch of unrelated things.
+    systemd.tmpfiles.rules = [ "d /var/lib/lxc/rootfs 0755 root root -" ];
+
+    security.apparmor = {
+      packages = [ cfg.lxcPackage ];
+      policies = {
+        "bin.lxc-start".profile = ''
+          include ${cfg.lxcPackage}/etc/apparmor.d/usr.bin.lxc-start
+        '';
+        "lxc-containers".profile = ''
+          include ${cfg.lxcPackage}/etc/apparmor.d/lxc-containers
+        '';
+      };
+    };
+
+    # TODO: remove once LXD gets proper support for cgroupsv2
+    # (currently most of the e.g. CPU accounting stuff doesn't work)
+    systemd.enableUnifiedCgroupHierarchy = false;
+
+    systemd.sockets.lxd = {
+      description = "LXD UNIX socket";
+      wantedBy = [ "sockets.target" ];
+
+      socketConfig = {
+        ListenStream = "/var/lib/lxd/unix.socket";
+        SocketMode = "0660";
+        SocketGroup = "lxd";
+        Service = "lxd.service";
+      };
+    };
+
+    systemd.services.lxd = {
+      description = "LXD Container Management Daemon";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" "lxcfs.service" ];
+      requires = [ "network-online.target" "lxd.socket"  "lxcfs.service" ];
+      documentation = [ "man:lxd(1)" ];
+
+      path = optional cfg.zfsSupport config.boot.zfs.package;
+
+      serviceConfig = {
+        ExecStart = "@${cfg.package}/bin/lxd lxd --group lxd";
+        ExecStartPost = "${cfg.package}/bin/lxd waitready --timeout=${cfg.startTimeout}";
+        ExecStop = "${cfg.package}/bin/lxd shutdown";
+
+        KillMode = "process"; # when stopping, leave the containers alone
+        LimitMEMLOCK = "infinity";
+        LimitNOFILE = "1048576";
+        LimitNPROC = "infinity";
+        TasksMax = "infinity";
+
+        Restart = "on-failure";
+        TimeoutStartSec = "${cfg.startTimeout}s";
+        TimeoutStopSec = "30s";
+
+        # By default, `lxd` loads configuration files from hard-coded
+        # `/usr/share/lxc/config` - since this is a no-go for us, we have to
+        # explicitly tell it where the actual configuration files are
+        Environment = mkIf (config.virtualisation.lxc.lxcfs.enable)
+          "LXD_LXC_TEMPLATE_CONFIG=${pkgs.lxcfs}/share/lxc/config";
+      };
+    };
+
+    users.groups.lxd = {};
+
+    users.users.root = {
+      subUidRanges = [ { startUid = 1000000; count = 65536; } ];
+      subGidRanges = [ { startGid = 1000000; count = 65536; } ];
+    };
+
+    boot.kernel.sysctl = mkIf cfg.recommendedSysctlSettings {
+      "fs.inotify.max_queued_events" = 1048576;
+      "fs.inotify.max_user_instances" = 1048576;
+      "fs.inotify.max_user_watches" = 1048576;
+      "vm.max_map_count" = 262144;
+      "kernel.dmesg_restrict" = 1;
+      "net.ipv4.neigh.default.gc_thresh3" = 8192;
+      "net.ipv6.neigh.default.gc_thresh3" = 8192;
+      "kernel.keys.maxkeys" = 2000;
+    };
+
+    boot.kernelModules = [ "veth" "xt_comment" "xt_CHECKSUM" "xt_MASQUERADE" ]
+      ++ optionals (!config.networking.nftables.enable) [ "iptable_mangle" ];
+  };
+}
diff --git a/nixos/modules/virtualisation/nixos-containers.nix b/nixos/modules/virtualisation/nixos-containers.nix
new file mode 100644
index 00000000000..0838a57f0f3
--- /dev/null
+++ b/nixos/modules/virtualisation/nixos-containers.nix
@@ -0,0 +1,866 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  # The container's init script, a small wrapper around the regular
+  # NixOS stage-2 init script.
+  containerInit = (cfg:
+    let
+      renderExtraVeth = (name: cfg:
+        ''
+        echo "Bringing ${name} up"
+        ip link set dev ${name} up
+        ${optionalString (cfg.localAddress != null) ''
+          echo "Setting ip for ${name}"
+          ip addr add ${cfg.localAddress} dev ${name}
+        ''}
+        ${optionalString (cfg.localAddress6 != null) ''
+          echo "Setting ip6 for ${name}"
+          ip -6 addr add ${cfg.localAddress6} dev ${name}
+        ''}
+        ${optionalString (cfg.hostAddress != null) ''
+          echo "Setting route to host for ${name}"
+          ip route add ${cfg.hostAddress} dev ${name}
+        ''}
+        ${optionalString (cfg.hostAddress6 != null) ''
+          echo "Setting route6 to host for ${name}"
+          ip -6 route add ${cfg.hostAddress6} dev ${name}
+        ''}
+        ''
+        );
+    in
+      pkgs.writeScript "container-init"
+      ''
+        #! ${pkgs.runtimeShell} -e
+
+        # Exit early if we're asked to shut down.
+        trap "exit 0" SIGRTMIN+3
+
+        # Initialise the container side of the veth pair.
+        if [ -n "$HOST_ADDRESS" ]   || [ -n "$HOST_ADDRESS6" ]  ||
+           [ -n "$LOCAL_ADDRESS" ]  || [ -n "$LOCAL_ADDRESS6" ] ||
+           [ -n "$HOST_BRIDGE" ]; then
+          ip link set host0 name eth0
+          ip link set dev eth0 up
+
+          if [ -n "$LOCAL_ADDRESS" ]; then
+            ip addr add $LOCAL_ADDRESS dev eth0
+          fi
+          if [ -n "$LOCAL_ADDRESS6" ]; then
+            ip -6 addr add $LOCAL_ADDRESS6 dev eth0
+          fi
+          if [ -n "$HOST_ADDRESS" ]; then
+            ip route add $HOST_ADDRESS dev eth0
+            ip route add default via $HOST_ADDRESS
+          fi
+          if [ -n "$HOST_ADDRESS6" ]; then
+            ip -6 route add $HOST_ADDRESS6 dev eth0
+            ip -6 route add default via $HOST_ADDRESS6
+          fi
+        fi
+
+        ${concatStringsSep "\n" (mapAttrsToList renderExtraVeth cfg.extraVeths)}
+
+        # Start the regular stage 2 script.
+        # We source instead of exec to not lose an early stop signal, which is
+        # also the only _reliable_ shutdown signal we have since early stop
+        # does not execute ExecStop* commands.
+        set +e
+        . "$1"
+      ''
+    );
+
+  nspawnExtraVethArgs = (name: cfg: "--network-veth-extra=${name}");
+
+  startScript = cfg:
+    ''
+      mkdir -p -m 0755 "$root/etc" "$root/var/lib"
+      mkdir -p -m 0700 "$root/var/lib/private" "$root/root" /run/containers
+      if ! [ -e "$root/etc/os-release" ]; then
+        touch "$root/etc/os-release"
+      fi
+
+      if ! [ -e "$root/etc/machine-id" ]; then
+        touch "$root/etc/machine-id"
+      fi
+
+      mkdir -p -m 0755 \
+        "/nix/var/nix/profiles/per-container/$INSTANCE" \
+        "/nix/var/nix/gcroots/per-container/$INSTANCE"
+
+      cp --remove-destination /etc/resolv.conf "$root/etc/resolv.conf"
+
+      if [ "$PRIVATE_NETWORK" = 1 ]; then
+        extraFlags+=" --private-network"
+      fi
+
+      if [ -n "$HOST_ADDRESS" ]  || [ -n "$LOCAL_ADDRESS" ] ||
+         [ -n "$HOST_ADDRESS6" ] || [ -n "$LOCAL_ADDRESS6" ]; then
+        extraFlags+=" --network-veth"
+      fi
+
+      if [ -n "$HOST_PORT" ]; then
+        OIFS=$IFS
+        IFS=","
+        for i in $HOST_PORT
+        do
+            extraFlags+=" --port=$i"
+        done
+        IFS=$OIFS
+      fi
+
+      if [ -n "$HOST_BRIDGE" ]; then
+        extraFlags+=" --network-bridge=$HOST_BRIDGE"
+      fi
+
+      extraFlags+=" ${concatStringsSep " " (mapAttrsToList nspawnExtraVethArgs cfg.extraVeths)}"
+
+      for iface in $INTERFACES; do
+        extraFlags+=" --network-interface=$iface"
+      done
+
+      for iface in $MACVLANS; do
+        extraFlags+=" --network-macvlan=$iface"
+      done
+
+      # If the host is 64-bit and the container is 32-bit, add a
+      # --personality flag.
+      ${optionalString (config.nixpkgs.localSystem.system == "x86_64-linux") ''
+        if [ "$(< ''${SYSTEM_PATH:-/nix/var/nix/profiles/per-container/$INSTANCE/system}/system)" = i686-linux ]; then
+          extraFlags+=" --personality=x86"
+        fi
+      ''}
+
+      # Run systemd-nspawn without startup notification (we'll
+      # wait for the container systemd to signal readiness)
+      # Kill signal handling means systemd-nspawn will pass a system-halt signal
+      # to the container systemd when it receives SIGTERM for container shutdown;
+      # containerInit and stage2 have to handle this as well.
+      exec ${config.systemd.package}/bin/systemd-nspawn \
+        --keep-unit \
+        -M "$INSTANCE" -D "$root" $extraFlags \
+        $EXTRA_NSPAWN_FLAGS \
+        --notify-ready=yes \
+        --kill-signal=SIGRTMIN+3 \
+        --bind-ro=/nix/store \
+        --bind-ro=/nix/var/nix/db \
+        --bind-ro=/nix/var/nix/daemon-socket \
+        --bind="/nix/var/nix/profiles/per-container/$INSTANCE:/nix/var/nix/profiles" \
+        --bind="/nix/var/nix/gcroots/per-container/$INSTANCE:/nix/var/nix/gcroots" \
+        ${optionalString (!cfg.ephemeral) "--link-journal=try-guest"} \
+        --setenv PRIVATE_NETWORK="$PRIVATE_NETWORK" \
+        --setenv HOST_BRIDGE="$HOST_BRIDGE" \
+        --setenv HOST_ADDRESS="$HOST_ADDRESS" \
+        --setenv LOCAL_ADDRESS="$LOCAL_ADDRESS" \
+        --setenv HOST_ADDRESS6="$HOST_ADDRESS6" \
+        --setenv LOCAL_ADDRESS6="$LOCAL_ADDRESS6" \
+        --setenv HOST_PORT="$HOST_PORT" \
+        --setenv PATH="$PATH" \
+        ${optionalString cfg.ephemeral "--ephemeral"} \
+        ${if cfg.additionalCapabilities != null && cfg.additionalCapabilities != [] then
+          ''--capability="${concatStringsSep "," cfg.additionalCapabilities}"'' else ""
+        } \
+        ${if cfg.tmpfs != null && cfg.tmpfs != [] then
+          ''--tmpfs=${concatStringsSep " --tmpfs=" cfg.tmpfs}'' else ""
+        } \
+        ${containerInit cfg} "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/init"
+    '';
+
+  preStartScript = cfg:
+    ''
+      # Clean up existing machined registration and interfaces.
+      machinectl terminate "$INSTANCE" 2> /dev/null || true
+
+      if [ -n "$HOST_ADDRESS" ]  || [ -n "$LOCAL_ADDRESS" ] ||
+         [ -n "$HOST_ADDRESS6" ] || [ -n "$LOCAL_ADDRESS6" ]; then
+        ip link del dev "ve-$INSTANCE" 2> /dev/null || true
+        ip link del dev "vb-$INSTANCE" 2> /dev/null || true
+      fi
+
+      ${concatStringsSep "\n" (
+        mapAttrsToList (name: cfg:
+          "ip link del dev ${name} 2> /dev/null || true "
+        ) cfg.extraVeths
+      )}
+   '';
+
+  postStartScript = (cfg:
+    let
+      ipcall = cfg: ipcmd: variable: attribute:
+        if cfg.${attribute} == null then
+          ''
+            if [ -n "${variable}" ]; then
+              ${ipcmd} add ${variable} dev $ifaceHost
+            fi
+          ''
+        else
+          "${ipcmd} add ${cfg.${attribute}} dev $ifaceHost";
+      renderExtraVeth = name: cfg:
+        if cfg.hostBridge != null then
+          ''
+            # Add ${name} to bridge ${cfg.hostBridge}
+            ip link set dev ${name} master ${cfg.hostBridge} up
+          ''
+        else
+          ''
+            echo "Bring ${name} up"
+            ip link set dev ${name} up
+            # Set IPs and routes for ${name}
+            ${optionalString (cfg.hostAddress != null) ''
+              ip addr add ${cfg.hostAddress} dev ${name}
+            ''}
+            ${optionalString (cfg.hostAddress6 != null) ''
+              ip -6 addr add ${cfg.hostAddress6} dev ${name}
+            ''}
+            ${optionalString (cfg.localAddress != null) ''
+              ip route add ${cfg.localAddress} dev ${name}
+            ''}
+            ${optionalString (cfg.localAddress6 != null) ''
+              ip -6 route add ${cfg.localAddress6} dev ${name}
+            ''}
+          '';
+    in
+      ''
+        if [ -n "$HOST_ADDRESS" ]  || [ -n "$LOCAL_ADDRESS" ] ||
+           [ -n "$HOST_ADDRESS6" ] || [ -n "$LOCAL_ADDRESS6" ]; then
+          if [ -z "$HOST_BRIDGE" ]; then
+            ifaceHost=ve-$INSTANCE
+            ip link set dev $ifaceHost up
+
+            ${ipcall cfg "ip addr" "$HOST_ADDRESS" "hostAddress"}
+            ${ipcall cfg "ip -6 addr" "$HOST_ADDRESS6" "hostAddress6"}
+            ${ipcall cfg "ip route" "$LOCAL_ADDRESS" "localAddress"}
+            ${ipcall cfg "ip -6 route" "$LOCAL_ADDRESS6" "localAddress6"}
+          fi
+        fi
+        ${concatStringsSep "\n" (mapAttrsToList renderExtraVeth cfg.extraVeths)}
+      ''
+  );
+
+  serviceDirectives = cfg: {
+    ExecReload = pkgs.writeScript "reload-container"
+      ''
+        #! ${pkgs.runtimeShell} -e
+        ${pkgs.nixos-container}/bin/nixos-container run "$INSTANCE" -- \
+          bash --login -c "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/bin/switch-to-configuration test"
+      '';
+
+    SyslogIdentifier = "container %i";
+
+    EnvironmentFile = "-/etc/containers/%i.conf";
+
+    Type = "notify";
+
+    RuntimeDirectory = lib.optional cfg.ephemeral "containers/%i";
+
+    # Note that on reboot, systemd-nspawn returns 133, so this
+    # unit will be restarted. On poweroff, it returns 0, so the
+    # unit won't be restarted.
+    RestartForceExitStatus = "133";
+    SuccessExitStatus = "133";
+
+    # Some containers take long to start
+    # especially when you automatically start many at once
+    TimeoutStartSec = cfg.timeoutStartSec;
+
+    Restart = "on-failure";
+
+    Slice = "machine.slice";
+    Delegate = true;
+
+    # We rely on systemd-nspawn turning a SIGTERM to itself into a shutdown
+    # signal (SIGRTMIN+3) for the inner container.
+    KillMode = "mixed";
+    KillSignal = "TERM";
+
+    DevicePolicy = "closed";
+    DeviceAllow = map (d: "${d.node} ${d.modifier}") cfg.allowedDevices;
+  };
+
+  system = config.nixpkgs.localSystem.system;
+  kernelVersion = config.boot.kernelPackages.kernel.version;
+
+  bindMountOpts = { name, ... }: {
+
+    options = {
+      mountPoint = mkOption {
+        example = "/mnt/usb";
+        type = types.str;
+        description = "Mount point on the container file system.";
+      };
+      hostPath = mkOption {
+        default = null;
+        example = "/home/alice";
+        type = types.nullOr types.str;
+        description = "Location of the host path to be mounted.";
+      };
+      isReadOnly = mkOption {
+        default = true;
+        type = types.bool;
+        description = "Determine whether the mounted path will be accessed in read-only mode.";
+      };
+    };
+
+    config = {
+      mountPoint = mkDefault name;
+    };
+
+  };
+
+  allowedDeviceOpts = { ... }: {
+    options = {
+      node = mkOption {
+        example = "/dev/net/tun";
+        type = types.str;
+        description = "Path to device node";
+      };
+      modifier = mkOption {
+        example = "rw";
+        type = types.str;
+        description = ''
+          Device node access modifier. Takes a combination
+          <literal>r</literal> (read), <literal>w</literal> (write), and
+          <literal>m</literal> (mknod). See the
+          <literal>systemd.resource-control(5)</literal> man page for more
+          information.'';
+      };
+    };
+  };
+
+  mkBindFlag = d:
+               let flagPrefix = if d.isReadOnly then " --bind-ro=" else " --bind=";
+                   mountstr = if d.hostPath != null then "${d.hostPath}:${d.mountPoint}" else "${d.mountPoint}";
+               in flagPrefix + mountstr ;
+
+  mkBindFlags = bs: concatMapStrings mkBindFlag (lib.attrValues bs);
+
+  networkOptions = {
+    hostBridge = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "br0";
+      description = ''
+        Put the host-side of the veth-pair into the named bridge.
+        Only one of hostAddress* or hostBridge can be given.
+      '';
+    };
+
+    forwardPorts = mkOption {
+      type = types.listOf (types.submodule {
+        options = {
+          protocol = mkOption {
+            type = types.str;
+            default = "tcp";
+            description = "The protocol specifier for port forwarding between host and container";
+          };
+          hostPort = mkOption {
+            type = types.int;
+            description = "Source port of the external interface on host";
+          };
+          containerPort = mkOption {
+            type = types.nullOr types.int;
+            default = null;
+            description = "Target port of container";
+          };
+        };
+      });
+      default = [];
+      example = [ { protocol = "tcp"; hostPort = 8080; containerPort = 80; } ];
+      description = ''
+        List of forwarded ports from host to container. Each forwarded port
+        is specified by protocol, hostPort and containerPort. By default,
+        protocol is tcp and hostPort and containerPort are assumed to be
+        the same if containerPort is not explicitly given.
+      '';
+    };
+
+
+    hostAddress = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "10.231.136.1";
+      description = ''
+        The IPv4 address assigned to the host interface.
+        (Not used when hostBridge is set.)
+      '';
+    };
+
+    hostAddress6 = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "fc00::1";
+      description = ''
+        The IPv6 address assigned to the host interface.
+        (Not used when hostBridge is set.)
+      '';
+    };
+
+    localAddress = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "10.231.136.2";
+      description = ''
+        The IPv4 address assigned to the interface in the container.
+        If a hostBridge is used, this should be given with netmask to access
+        the whole network. Otherwise the default netmask is /32 and routing is
+        set up from localAddress to hostAddress and back.
+      '';
+    };
+
+    localAddress6 = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "fc00::2";
+      description = ''
+        The IPv6 address assigned to the interface in the container.
+        If a hostBridge is used, this should be given with netmask to access
+        the whole network. Otherwise the default netmask is /128 and routing is
+        set up from localAddress6 to hostAddress6 and back.
+      '';
+    };
+
+  };
+
+  dummyConfig =
+    {
+      extraVeths = {};
+      additionalCapabilities = [];
+      ephemeral = false;
+      timeoutStartSec = "1min";
+      allowedDevices = [];
+      hostAddress = null;
+      hostAddress6 = null;
+      localAddress = null;
+      localAddress6 = null;
+      tmpfs = null;
+    };
+
+in
+
+{
+  options = {
+
+    boot.isContainer = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether this NixOS machine is a lightweight container running
+        in another NixOS system.
+      '';
+    };
+
+    boot.enableContainers = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to enable support for NixOS containers. Defaults to true
+        (at no cost if containers are not actually used).
+      '';
+    };
+
+    containers = mkOption {
+      type = types.attrsOf (types.submodule (
+        { config, options, name, ... }:
+        {
+          options = {
+            config = mkOption {
+              description = ''
+                A specification of the desired configuration of this
+                container, as a NixOS module.
+              '';
+              type = lib.mkOptionType {
+                name = "Toplevel NixOS config";
+                merge = loc: defs: (import "${toString config.nixpkgs}/nixos/lib/eval-config.nix" {
+                  inherit system;
+                  modules =
+                    let
+                      extraConfig = {
+                        _file = "module at ${__curPos.file}:${toString __curPos.line}";
+                        config = {
+                          boot.isContainer = true;
+                          networking.hostName = mkDefault name;
+                          networking.useDHCP = false;
+                          assertions = [
+                            {
+                              assertion =
+                                (builtins.compareVersions kernelVersion "5.8" <= 0)
+                                -> config.privateNetwork
+                                -> stringLength name <= 11;
+                              message = ''
+                                Container name `${name}` is too long: When `privateNetwork` is enabled, container names can
+                                not be longer than 11 characters, because the container's interface name is derived from it.
+                                You should either make the container name shorter or upgrade to a more recent kernel that
+                                supports interface altnames (i.e. at least Linux 5.8 - please see https://github.com/NixOS/nixpkgs/issues/38509
+                                for details).
+                              '';
+                            }
+                          ];
+                        };
+                      };
+                    in [ extraConfig ] ++ (map (x: x.value) defs);
+                  prefix = [ "containers" name ];
+                }).config;
+              };
+            };
+
+            path = mkOption {
+              type = types.path;
+              example = "/nix/var/nix/profiles/per-container/webserver";
+              description = ''
+                As an alternative to specifying
+                <option>config</option>, you can specify the path to
+                the evaluated NixOS system configuration, typically a
+                symlink to a system profile.
+              '';
+            };
+
+            additionalCapabilities = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [ "CAP_NET_ADMIN" "CAP_MKNOD" ];
+              description = ''
+                Grant additional capabilities to the container.  See the
+                capabilities(7) and systemd-nspawn(1) man pages for more
+                information.
+              '';
+            };
+
+            nixpkgs = mkOption {
+              type = types.path;
+              default = pkgs.path;
+              defaultText = literalExpression "pkgs.path";
+              description = ''
+                A path to the nixpkgs that provide the modules, pkgs and lib for evaluating the container.
+
+                To only change the <literal>pkgs</literal> argument used inside the container modules,
+                set the <literal>nixpkgs.*</literal> options in the container <option>config</option>.
+                Setting <literal>config.nixpkgs.pkgs = pkgs</literal> speeds up the container evaluation
+                by reusing the system pkgs, but the <literal>nixpkgs.config</literal> option in the
+                container config is ignored in this case.
+              '';
+            };
+
+            ephemeral = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Runs container in ephemeral mode with the empty root filesystem at boot.
+                This way container will be bootstrapped from scratch on each boot
+                and will be cleaned up on shutdown leaving no traces behind.
+                Useful for completely stateless, reproducible containers.
+
+                Note that this option might require to do some adjustments to the container configuration,
+                e.g. you might want to set
+                <varname>systemd.network.networks.$interface.dhcpV4Config.ClientIdentifier</varname> to "mac"
+                if you use <varname>macvlans</varname> option.
+                This way dhcp client identifier will be stable between the container restarts.
+
+                Note that the container journal will not be linked to the host if this option is enabled.
+              '';
+            };
+
+            enableTun = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Allows the container to create and setup tunnel interfaces
+                by granting the <literal>NET_ADMIN</literal> capability and
+                enabling access to <literal>/dev/net/tun</literal>.
+              '';
+            };
+
+            privateNetwork = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Whether to give the container its own private virtual
+                Ethernet interface.  The interface is called
+                <literal>eth0</literal>, and is hooked up to the interface
+                <literal>ve-<replaceable>container-name</replaceable></literal>
+                on the host.  If this option is not set, then the
+                container shares the network interfaces of the host,
+                and can bind to any port on any interface.
+              '';
+            };
+
+            interfaces = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [ "eth1" "eth2" ];
+              description = ''
+                The list of interfaces to be moved into the container.
+              '';
+            };
+
+            macvlans = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [ "eth1" "eth2" ];
+              description = ''
+                The list of host interfaces from which macvlans will be
+                created. For each interface specified, a macvlan interface
+                will be created and moved to the container.
+              '';
+            };
+
+            extraVeths = mkOption {
+              type = with types; attrsOf (submodule { options = networkOptions; });
+              default = {};
+              description = ''
+                Extra veth-pairs to be created for the container.
+              '';
+            };
+
+            autoStart = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Whether the container is automatically started at boot-time.
+              '';
+            };
+
+            timeoutStartSec = mkOption {
+              type = types.str;
+              default = "1min";
+              description = ''
+                Time for the container to start. In case of a timeout,
+                the container processes get killed.
+                See <citerefentry><refentrytitle>systemd.time</refentrytitle>
+                <manvolnum>7</manvolnum></citerefentry>
+                for more information about the format.
+               '';
+            };
+
+            bindMounts = mkOption {
+              type = with types; attrsOf (submodule bindMountOpts);
+              default = {};
+              example = literalExpression ''
+                { "/home" = { hostPath = "/home/alice";
+                              isReadOnly = false; };
+                }
+              '';
+
+              description =
+                ''
+                  An extra list of directories that is bound to the container.
+                '';
+            };
+
+            allowedDevices = mkOption {
+              type = with types; listOf (submodule allowedDeviceOpts);
+              default = [];
+              example = [ { node = "/dev/net/tun"; modifier = "rw"; } ];
+              description = ''
+                A list of device nodes to which the containers has access to.
+              '';
+            };
+
+            tmpfs = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [ "/var" ];
+              description = ''
+                Mounts a set of tmpfs file systems into the container.
+                Multiple paths can be specified.
+                Valid items must conform to the --tmpfs argument
+                of systemd-nspawn. See systemd-nspawn(1) for details.
+              '';
+            };
+
+            extraFlags = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [ "--drop-capability=CAP_SYS_CHROOT" ];
+              description = ''
+                Extra flags passed to the systemd-nspawn command.
+                See systemd-nspawn(1) for details.
+              '';
+            };
+
+            # Removed option. See `checkAssertion` below for the accompanying error message.
+            pkgs = mkOption { visible = false; };
+          } // networkOptions;
+
+          config = let
+            # Throw an error when removed option `pkgs` is used.
+            # Because this is a submodule we cannot use `mkRemovedOptionModule` or option `assertions`.
+            optionPath = "containers.${name}.pkgs";
+            files = showFiles options.pkgs.files;
+            checkAssertion = if options.pkgs.isDefined then throw ''
+              The option definition `${optionPath}' in ${files} no longer has any effect; please remove it.
+
+              Alternatively, you can use the following options:
+              - containers.${name}.nixpkgs
+                This sets the nixpkgs (and thereby the modules, pkgs and lib) that
+                are used for evaluating the container.
+
+              - containers.${name}.config.nixpkgs.pkgs
+                This only sets the `pkgs` argument used inside the container modules.
+            ''
+            else null;
+          in {
+            path = builtins.seq checkAssertion
+              mkIf options.config.isDefined config.config.system.build.toplevel;
+          };
+        }));
+
+      default = {};
+      example = literalExpression
+        ''
+          { webserver =
+              { path = "/nix/var/nix/profiles/webserver";
+              };
+            database =
+              { config =
+                  { config, pkgs, ... }:
+                  { services.postgresql.enable = true;
+                    services.postgresql.package = pkgs.postgresql_10;
+
+                    system.stateVersion = "21.05";
+                  };
+              };
+          }
+        '';
+      description = ''
+        A set of NixOS system configurations to be run as lightweight
+        containers.  Each container appears as a service
+        <literal>container-<replaceable>name</replaceable></literal>
+        on the host system, allowing it to be started and stopped via
+        <command>systemctl</command>.
+      '';
+    };
+
+  };
+
+
+  config = mkIf (config.boot.enableContainers) (let
+
+    unit = {
+      description = "Container '%i'";
+
+      unitConfig.RequiresMountsFor = "/var/lib/containers/%i";
+
+      path = [ pkgs.iproute2 ];
+
+      environment = {
+        root = "/var/lib/containers/%i";
+        INSTANCE = "%i";
+      };
+
+      preStart = preStartScript dummyConfig;
+
+      script = startScript dummyConfig;
+
+      postStart = postStartScript dummyConfig;
+
+      restartIfChanged = false;
+
+      serviceConfig = serviceDirectives dummyConfig;
+    };
+  in {
+    systemd.targets.multi-user.wants = [ "machines.target" ];
+
+    systemd.services = listToAttrs (filter (x: x.value != null) (
+      # The generic container template used by imperative containers
+      [{ name = "container@"; value = unit; }]
+      # declarative containers
+      ++ (mapAttrsToList (name: cfg: nameValuePair "container@${name}" (let
+          containerConfig = cfg // (
+          if cfg.enableTun then
+            {
+              allowedDevices = cfg.allowedDevices
+                ++ [ { node = "/dev/net/tun"; modifier = "rw"; } ];
+              additionalCapabilities = cfg.additionalCapabilities
+                ++ [ "CAP_NET_ADMIN" ];
+            }
+          else {});
+        in
+          recursiveUpdate unit {
+            preStart = preStartScript containerConfig;
+            script = startScript containerConfig;
+            postStart = postStartScript containerConfig;
+            serviceConfig = serviceDirectives containerConfig;
+            unitConfig.RequiresMountsFor = lib.optional (!containerConfig.ephemeral) "/var/lib/containers/%i";
+            environment.root = if containerConfig.ephemeral then "/run/containers/%i" else "/var/lib/containers/%i";
+          } // (
+          if containerConfig.autoStart then
+            {
+              wantedBy = [ "machines.target" ];
+              wants = [ "network.target" ];
+              after = [ "network.target" ];
+              restartTriggers = [
+                containerConfig.path
+                config.environment.etc."containers/${name}.conf".source
+              ];
+              restartIfChanged = true;
+            }
+          else {})
+      )) config.containers)
+    ));
+
+    # Generate a configuration file in /etc/containers for each
+    # container so that container@.target can get the container
+    # configuration.
+    environment.etc =
+      let mkPortStr = p: p.protocol + ":" + (toString p.hostPort) + ":" + (if p.containerPort == null then toString p.hostPort else toString p.containerPort);
+      in mapAttrs' (name: cfg: nameValuePair "containers/${name}.conf"
+      { text =
+          ''
+            SYSTEM_PATH=${cfg.path}
+            ${optionalString cfg.privateNetwork ''
+              PRIVATE_NETWORK=1
+              ${optionalString (cfg.hostBridge != null) ''
+                HOST_BRIDGE=${cfg.hostBridge}
+              ''}
+              ${optionalString (length cfg.forwardPorts > 0) ''
+                HOST_PORT=${concatStringsSep "," (map mkPortStr cfg.forwardPorts)}
+              ''}
+              ${optionalString (cfg.hostAddress != null) ''
+                HOST_ADDRESS=${cfg.hostAddress}
+              ''}
+              ${optionalString (cfg.hostAddress6 != null) ''
+                HOST_ADDRESS6=${cfg.hostAddress6}
+              ''}
+              ${optionalString (cfg.localAddress != null) ''
+                LOCAL_ADDRESS=${cfg.localAddress}
+              ''}
+              ${optionalString (cfg.localAddress6 != null) ''
+                LOCAL_ADDRESS6=${cfg.localAddress6}
+              ''}
+            ''}
+            INTERFACES="${toString cfg.interfaces}"
+            MACVLANS="${toString cfg.macvlans}"
+            ${optionalString cfg.autoStart ''
+              AUTO_START=1
+            ''}
+            EXTRA_NSPAWN_FLAGS="${mkBindFlags cfg.bindMounts +
+              optionalString (cfg.extraFlags != [])
+                (" " + concatStringsSep " " cfg.extraFlags)}"
+          '';
+      }) config.containers;
+
+    # Generate /etc/hosts entries for the containers.
+    networking.extraHosts = concatStrings (mapAttrsToList (name: cfg: optionalString (cfg.localAddress != null)
+      ''
+        ${head (splitString "/" cfg.localAddress)} ${name}.containers
+      '') config.containers);
+
+    networking.dhcpcd.denyInterfaces = [ "ve-*" "vb-*" ];
+
+    services.udev.extraRules = optionalString config.networking.networkmanager.enable ''
+      # Don't manage interfaces created by nixos-container.
+      ENV{INTERFACE}=="v[eb]-*", ENV{NM_UNMANAGED}="1"
+    '';
+
+    environment.systemPackages = [ pkgs.nixos-container ];
+
+    boot.kernelModules = [
+      "bridge"
+      "macvlan"
+      "tap"
+      "tun"
+    ];
+  });
+}
diff --git a/nixos/modules/virtualisation/oci-containers.nix b/nixos/modules/virtualisation/oci-containers.nix
new file mode 100644
index 00000000000..f4048172783
--- /dev/null
+++ b/nixos/modules/virtualisation/oci-containers.nix
@@ -0,0 +1,369 @@
+{ config, options, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.virtualisation.oci-containers;
+  proxy_env = config.networking.proxy.envVars;
+
+  defaultBackend = options.virtualisation.oci-containers.backend.default;
+
+  containerOptions =
+    { ... }: {
+
+      options = {
+
+        image = mkOption {
+          type = with types; str;
+          description = "OCI image to run.";
+          example = "library/hello-world";
+        };
+
+        imageFile = mkOption {
+          type = with types; nullOr package;
+          default = null;
+          description = ''
+            Path to an image file to load before running the image. This can
+            be used to bypass pulling the image from the registry.
+
+            The <literal>image</literal> attribute must match the name and
+            tag of the image contained in this file, as they will be used to
+            run the container with that image. If they do not match, the
+            image will be pulled from the registry as usual.
+          '';
+          example = literalExpression "pkgs.dockerTools.buildImage {...};";
+        };
+
+        login = {
+
+          username = mkOption {
+            type = with types; nullOr str;
+            default = null;
+            description = "Username for login.";
+          };
+
+          passwordFile = mkOption {
+            type = with types; nullOr str;
+            default = null;
+            description = "Path to file containing password.";
+            example = "/etc/nixos/dockerhub-password.txt";
+          };
+
+          registry = mkOption {
+            type = with types; nullOr str;
+            default = null;
+            description = "Registry where to login to.";
+            example = "https://docker.pkg.github.com";
+          };
+
+        };
+
+        cmd = mkOption {
+          type =  with types; listOf str;
+          default = [];
+          description = "Commandline arguments to pass to the image's entrypoint.";
+          example = literalExpression ''
+            ["--port=9000"]
+          '';
+        };
+
+        entrypoint = mkOption {
+          type = with types; nullOr str;
+          description = "Override the default entrypoint of the image.";
+          default = null;
+          example = "/bin/my-app";
+        };
+
+        environment = mkOption {
+          type = with types; attrsOf str;
+          default = {};
+          description = "Environment variables to set for this container.";
+          example = literalExpression ''
+            {
+              DATABASE_HOST = "db.example.com";
+              DATABASE_PORT = "3306";
+            }
+        '';
+        };
+
+        environmentFiles = mkOption {
+          type = with types; listOf path;
+          default = [];
+          description = "Environment files for this container.";
+          example = literalExpression ''
+            [
+              /path/to/.env
+              /path/to/.env.secret
+            ]
+        '';
+        };
+
+        log-driver = mkOption {
+          type = types.str;
+          default = "journald";
+          description = ''
+            Logging driver for the container.  The default of
+            <literal>"journald"</literal> means that the container's logs will be
+            handled as part of the systemd unit.
+
+            For more details and a full list of logging drivers, refer to respective backends documentation.
+
+            For Docker:
+            <link xlink:href="https://docs.docker.com/engine/reference/run/#logging-drivers---log-driver">Docker engine documentation</link>
+
+            For Podman:
+            Refer to the docker-run(1) man page.
+          '';
+        };
+
+        ports = mkOption {
+          type = with types; listOf str;
+          default = [];
+          description = ''
+            Network ports to publish from the container to the outer host.
+
+            Valid formats:
+
+            <itemizedlist>
+              <listitem>
+                <para>
+                  <literal>&lt;ip&gt;:&lt;hostPort&gt;:&lt;containerPort&gt;</literal>
+                </para>
+              </listitem>
+              <listitem>
+                <para>
+                  <literal>&lt;ip&gt;::&lt;containerPort&gt;</literal>
+                </para>
+              </listitem>
+              <listitem>
+                <para>
+                  <literal>&lt;hostPort&gt;:&lt;containerPort&gt;</literal>
+                </para>
+              </listitem>
+              <listitem>
+                <para>
+                  <literal>&lt;containerPort&gt;</literal>
+                </para>
+              </listitem>
+            </itemizedlist>
+
+            Both <literal>hostPort</literal> and
+            <literal>containerPort</literal> can be specified as a range of
+            ports.  When specifying ranges for both, the number of container
+            ports in the range must match the number of host ports in the
+            range.  Example: <literal>1234-1236:1234-1236/tcp</literal>
+
+            When specifying a range for <literal>hostPort</literal> only, the
+            <literal>containerPort</literal> must <emphasis>not</emphasis> be a
+            range.  In this case, the container port is published somewhere
+            within the specified <literal>hostPort</literal> range.  Example:
+            <literal>1234-1236:1234/tcp</literal>
+
+            Refer to the
+            <link xlink:href="https://docs.docker.com/engine/reference/run/#expose-incoming-ports">
+            Docker engine documentation</link> for full details.
+          '';
+          example = literalExpression ''
+            [
+              "8080:9000"
+            ]
+          '';
+        };
+
+        user = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = ''
+            Override the username or UID (and optionally groupname or GID) used
+            in the container.
+          '';
+          example = "nobody:nogroup";
+        };
+
+        volumes = mkOption {
+          type = with types; listOf str;
+          default = [];
+          description = ''
+            List of volumes to attach to this container.
+
+            Note that this is a list of <literal>"src:dst"</literal> strings to
+            allow for <literal>src</literal> to refer to
+            <literal>/nix/store</literal> paths, which would be difficult with an
+            attribute set.  There are also a variety of mount options available
+            as a third field; please refer to the
+            <link xlink:href="https://docs.docker.com/engine/reference/run/#volume-shared-filesystems">
+            docker engine documentation</link> for details.
+          '';
+          example = literalExpression ''
+            [
+              "volume_name:/path/inside/container"
+              "/path/on/host:/path/inside/container"
+            ]
+          '';
+        };
+
+        workdir = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = "Override the default working directory for the container.";
+          example = "/var/lib/hello_world";
+        };
+
+        dependsOn = mkOption {
+          type = with types; listOf str;
+          default = [];
+          description = ''
+            Define which other containers this one depends on. They will be added to both After and Requires for the unit.
+
+            Use the same name as the attribute under <literal>virtualisation.oci-containers.containers</literal>.
+          '';
+          example = literalExpression ''
+            virtualisation.oci-containers.containers = {
+              node1 = {};
+              node2 = {
+                dependsOn = [ "node1" ];
+              }
+            }
+          '';
+        };
+
+        extraOptions = mkOption {
+          type = with types; listOf str;
+          default = [];
+          description = "Extra options for <command>${defaultBackend} run</command>.";
+          example = literalExpression ''
+            ["--network=host"]
+          '';
+        };
+
+        autoStart = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            When enabled, the container is automatically started on boot.
+            If this option is set to false, the container has to be started on-demand via its service.
+          '';
+        };
+      };
+    };
+
+  isValidLogin = login: login.username != null && login.passwordFile != null && login.registry != null;
+
+  mkService = name: container: let
+    dependsOn = map (x: "${cfg.backend}-${x}.service") container.dependsOn;
+  in {
+    wantedBy = [] ++ optional (container.autoStart) "multi-user.target";
+    after = lib.optionals (cfg.backend == "docker") [ "docker.service" "docker.socket" ] ++ dependsOn;
+    requires = dependsOn;
+    environment = proxy_env;
+
+    path =
+      if cfg.backend == "docker" then [ config.virtualisation.docker.package ]
+      else if cfg.backend == "podman" then [ config.virtualisation.podman.package ]
+      else throw "Unhandled backend: ${cfg.backend}";
+
+    preStart = ''
+      ${cfg.backend} rm -f ${name} || true
+      ${optionalString (isValidLogin container.login) ''
+        cat ${container.login.passwordFile} | \
+          ${cfg.backend} login \
+            ${container.login.registry} \
+            --username ${container.login.username} \
+            --password-stdin
+        ''}
+      ${optionalString (container.imageFile != null) ''
+        ${cfg.backend} load -i ${container.imageFile}
+        ''}
+      '';
+
+    script = concatStringsSep " \\\n  " ([
+      "exec ${cfg.backend} run"
+      "--rm"
+      "--name=${escapeShellArg name}"
+      "--log-driver=${container.log-driver}"
+    ] ++ optional (container.entrypoint != null)
+      "--entrypoint=${escapeShellArg container.entrypoint}"
+      ++ (mapAttrsToList (k: v: "-e ${escapeShellArg k}=${escapeShellArg v}") container.environment)
+      ++ map (f: "--env-file ${escapeShellArg f}") container.environmentFiles
+      ++ map (p: "-p ${escapeShellArg p}") container.ports
+      ++ optional (container.user != null) "-u ${escapeShellArg container.user}"
+      ++ map (v: "-v ${escapeShellArg v}") container.volumes
+      ++ optional (container.workdir != null) "-w ${escapeShellArg container.workdir}"
+      ++ map escapeShellArg container.extraOptions
+      ++ [container.image]
+      ++ map escapeShellArg container.cmd
+    );
+
+    preStop = "[ $SERVICE_RESULT = success ] || ${cfg.backend} stop ${name}";
+    postStop = "${cfg.backend} rm -f ${name} || true";
+
+    serviceConfig = {
+      ### There is no generalized way of supporting `reload` for docker
+      ### containers. Some containers may respond well to SIGHUP sent to their
+      ### init process, but it is not guaranteed; some apps have other reload
+      ### mechanisms, some don't have a reload signal at all, and some docker
+      ### images just have broken signal handling.  The best compromise in this
+      ### case is probably to leave ExecReload undefined, so `systemctl reload`
+      ### will at least result in an error instead of potentially undefined
+      ### behaviour.
+      ###
+      ### Advanced users can still override this part of the unit to implement
+      ### a custom reload handler, since the result of all this is a normal
+      ### systemd service from the perspective of the NixOS module system.
+      ###
+      # ExecReload = ...;
+      ###
+
+      TimeoutStartSec = 0;
+      TimeoutStopSec = 120;
+      Restart = "always";
+    };
+  };
+
+in {
+  imports = [
+    (
+      lib.mkChangedOptionModule
+      [ "docker-containers"  ]
+      [ "virtualisation" "oci-containers" ]
+      (oldcfg: {
+        backend = "docker";
+        containers = lib.mapAttrs (n: v: builtins.removeAttrs (v // {
+          extraOptions = v.extraDockerOptions or [];
+        }) [ "extraDockerOptions" ]) oldcfg.docker-containers;
+      })
+    )
+  ];
+
+  options.virtualisation.oci-containers = {
+
+    backend = mkOption {
+      type = types.enum [ "podman" "docker" ];
+      default =
+        # TODO: Once https://github.com/NixOS/nixpkgs/issues/77925 is resolved default to podman
+        # if versionAtLeast config.system.stateVersion "20.09" then "podman"
+        # else "docker";
+        "docker";
+      description = "The underlying Docker implementation to use.";
+    };
+
+    containers = mkOption {
+      default = {};
+      type = types.attrsOf (types.submodule containerOptions);
+      description = "OCI (Docker) containers to run as systemd services.";
+    };
+
+  };
+
+  config = lib.mkIf (cfg.containers != {}) (lib.mkMerge [
+    {
+      systemd.services = mapAttrs' (n: v: nameValuePair "${cfg.backend}-${n}" (mkService n v)) cfg.containers;
+    }
+    (lib.mkIf (cfg.backend == "podman") {
+      virtualisation.podman.enable = true;
+    })
+    (lib.mkIf (cfg.backend == "docker") {
+      virtualisation.docker.enable = true;
+    })
+  ]);
+
+}
diff --git a/nixos/modules/virtualisation/openstack-config.nix b/nixos/modules/virtualisation/openstack-config.nix
new file mode 100644
index 00000000000..d01e0f23aba
--- /dev/null
+++ b/nixos/modules/virtualisation/openstack-config.nix
@@ -0,0 +1,58 @@
+{ pkgs, lib, ... }:
+
+with lib;
+
+let
+  metadataFetcher = import ./openstack-metadata-fetcher.nix {
+    targetRoot = "/";
+    wgetExtraOptions = "--retry-connrefused";
+  };
+in
+{
+  imports = [
+    ../profiles/qemu-guest.nix
+    ../profiles/headless.nix
+    # The Openstack Metadata service exposes data on an EC2 API also.
+    ./ec2-data.nix
+    ./amazon-init.nix
+  ];
+
+  config = {
+    fileSystems."/" = {
+      device = "/dev/disk/by-label/nixos";
+      fsType = "ext4";
+      autoResize = true;
+    };
+
+    boot.growPartition = true;
+    boot.kernelParams = [ "console=ttyS0" ];
+    boot.loader.grub.device = "/dev/vda";
+    boot.loader.timeout = 0;
+
+    # Allow root logins
+    services.openssh = {
+      enable = true;
+      permitRootLogin = "prohibit-password";
+      passwordAuthentication = mkDefault false;
+    };
+
+    # Force getting the hostname from Openstack metadata.
+    networking.hostName = mkDefault "";
+
+    systemd.services.openstack-init = {
+      path = [ pkgs.wget ];
+      description = "Fetch Metadata on startup";
+      wantedBy = [ "multi-user.target" ];
+      before = [ "apply-ec2-data.service" "amazon-init.service"];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+      script = metadataFetcher;
+      restartIfChanged = false;
+      unitConfig.X-StopOnRemoval = false;
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/openstack-metadata-fetcher.nix b/nixos/modules/virtualisation/openstack-metadata-fetcher.nix
new file mode 100644
index 00000000000..25104bb4766
--- /dev/null
+++ b/nixos/modules/virtualisation/openstack-metadata-fetcher.nix
@@ -0,0 +1,22 @@
+{ targetRoot, wgetExtraOptions }:
+
+# OpenStack's metadata service aims to be EC2-compatible. Where
+# possible, try to keep the set of fetched metadata in sync with
+# ./ec2-metadata-fetcher.nix .
+''
+  metaDir=${targetRoot}etc/ec2-metadata
+  mkdir -m 0755 -p "$metaDir"
+  rm -f "$metaDir/*"
+
+  echo "getting instance metadata..."
+
+  wget_imds() {
+    wget ${wgetExtraOptions} "$@"
+  }
+
+  wget_imds -O "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path
+  # When no user-data is provided, the OpenStack metadata server doesn't expose the user-data route.
+  (umask 077 && wget_imds -O "$metaDir/user-data" http://169.254.169.254/1.0/user-data || rm -f "$metaDir/user-data")
+  wget_imds -O "$metaDir/hostname" http://169.254.169.254/1.0/meta-data/hostname
+  wget_imds -O "$metaDir/public-keys-0-openssh-key" http://169.254.169.254/1.0/meta-data/public-keys/0/openssh-key
+''
diff --git a/nixos/modules/virtualisation/openvswitch.nix b/nixos/modules/virtualisation/openvswitch.nix
new file mode 100644
index 00000000000..436a375fb5e
--- /dev/null
+++ b/nixos/modules/virtualisation/openvswitch.nix
@@ -0,0 +1,145 @@
+# Systemd services for openvswitch
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.virtualisation.vswitch;
+
+in {
+
+  options.virtualisation.vswitch = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable Open vSwitch. A configuration daemon (ovs-server)
+        will be started.
+        '';
+    };
+
+    resetOnStart = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to reset the Open vSwitch configuration database to a default
+        configuration on every start of the systemd <literal>ovsdb.service</literal>.
+        '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.openvswitch;
+      defaultText = literalExpression "pkgs.openvswitch";
+      description = ''
+        Open vSwitch package to use.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable (let
+
+    # Where the communication sockets live
+    runDir = "/run/openvswitch";
+
+    # The path to the an initialized version of the database
+    db = pkgs.stdenv.mkDerivation {
+      name = "vswitch.db";
+      dontUnpack = true;
+      buildPhase = "true";
+      buildInputs = with pkgs; [
+        cfg.package
+      ];
+      installPhase = "mkdir -p $out";
+    };
+
+  in {
+    environment.systemPackages = [ cfg.package ];
+    boot.kernelModules = [ "tun" "openvswitch" ];
+
+    boot.extraModulePackages = [ cfg.package ];
+
+    systemd.services.ovsdb = {
+      description = "Open_vSwitch Database Server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "systemd-udev-settle.service" ];
+      path = [ cfg.package ];
+      restartTriggers = [ db cfg.package ];
+      # Create the config database
+      preStart =
+        ''
+        mkdir -p ${runDir}
+        mkdir -p /var/db/openvswitch
+        chmod +w /var/db/openvswitch
+        ${optionalString cfg.resetOnStart "rm -f /var/db/openvswitch/conf.db"}
+        if [[ ! -e /var/db/openvswitch/conf.db ]]; then
+          ${cfg.package}/bin/ovsdb-tool create \
+            "/var/db/openvswitch/conf.db" \
+            "${cfg.package}/share/openvswitch/vswitch.ovsschema"
+        fi
+        chmod -R +w /var/db/openvswitch
+        if ${cfg.package}/bin/ovsdb-tool needs-conversion /var/db/openvswitch/conf.db | grep -q "yes"
+        then
+          echo "Performing database upgrade"
+          ${cfg.package}/bin/ovsdb-tool convert /var/db/openvswitch/conf.db
+        else
+          echo "Database already up to date"
+        fi
+        '';
+      serviceConfig = {
+        ExecStart =
+          ''
+          ${cfg.package}/bin/ovsdb-server \
+            --remote=punix:${runDir}/db.sock \
+            --private-key=db:Open_vSwitch,SSL,private_key \
+            --certificate=db:Open_vSwitch,SSL,certificate \
+            --bootstrap-ca-cert=db:Open_vSwitch,SSL,ca_cert \
+            --unixctl=ovsdb.ctl.sock \
+            --pidfile=/run/openvswitch/ovsdb.pid \
+            --detach \
+            /var/db/openvswitch/conf.db
+          '';
+        Restart = "always";
+        RestartSec = 3;
+        PIDFile = "/run/openvswitch/ovsdb.pid";
+        # Use service type 'forking' to correctly determine when ovsdb-server is ready.
+        Type = "forking";
+      };
+      postStart = ''
+        ${cfg.package}/bin/ovs-vsctl --timeout 3 --retry --no-wait init
+      '';
+    };
+
+    systemd.services.ovs-vswitchd = {
+      description = "Open_vSwitch Daemon";
+      wantedBy = [ "multi-user.target" ];
+      bindsTo = [ "ovsdb.service" ];
+      after = [ "ovsdb.service" ];
+      path = [ cfg.package ];
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.package}/bin/ovs-vswitchd \
+          --pidfile=/run/openvswitch/ovs-vswitchd.pid \
+          --detach
+        '';
+        PIDFile = "/run/openvswitch/ovs-vswitchd.pid";
+        # Use service type 'forking' to correctly determine when vswitchd is ready.
+        Type = "forking";
+        Restart = "always";
+        RestartSec = 3;
+      };
+    };
+
+  });
+
+  imports = [
+    (mkRemovedOptionModule [ "virtualisation" "vswitch" "ipsec" ] ''
+      OpenVSwitch IPSec functionality has been removed, because it depended on racoon,
+      which was removed from nixpkgs, because it was abanoded upstream.
+    '')
+  ];
+
+  meta.maintainers = with maintainers; [ netixx ];
+
+}
diff --git a/nixos/modules/virtualisation/parallels-guest.nix b/nixos/modules/virtualisation/parallels-guest.nix
new file mode 100644
index 00000000000..d950cecff6f
--- /dev/null
+++ b/nixos/modules/virtualisation/parallels-guest.nix
@@ -0,0 +1,155 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  prl-tools = config.hardware.parallels.package;
+in
+
+{
+
+  options = {
+    hardware.parallels = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          This enables Parallels Tools for Linux guests, along with provided
+          video, mouse and other hardware drivers.
+        '';
+      };
+
+      autoMountShares = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Control prlfsmountd service. When this service is running, shares can not be manually
+          mounted through `mount -t prl_fs ...` as this service will remount and trample any set options.
+          Recommended to enable for simple file sharing, but extended share use such as for code should
+          disable this to manually mount shares.
+        '';
+      };
+
+      package = mkOption {
+        type = types.nullOr types.package;
+        default = config.boot.kernelPackages.prl-tools;
+        defaultText = literalExpression "config.boot.kernelPackages.prl-tools";
+        description = ''
+          Defines which package to use for prl-tools. Override to change the version.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf config.hardware.parallels.enable {
+    services.xserver = {
+      drivers = singleton
+        { name = "prlvideo"; modules = [ prl-tools ]; };
+
+      screenSection = ''
+        Option "NoMTRR"
+      '';
+
+      config = ''
+        Section "InputClass"
+          Identifier "prlmouse"
+          MatchIsPointer "on"
+          MatchTag "prlmouse"
+          Driver "prlmouse"
+        EndSection
+      '';
+    };
+
+    hardware.opengl.package = prl-tools;
+    hardware.opengl.package32 = pkgs.pkgsi686Linux.linuxPackages.prl-tools.override { libsOnly = true; kernel = null; };
+    hardware.opengl.setLdLibraryPath = true;
+
+    services.udev.packages = [ prl-tools ];
+
+    environment.systemPackages = [ prl-tools ];
+
+    boot.extraModulePackages = [ prl-tools ];
+
+    boot.kernelModules = [ "prl_tg" "prl_eth" "prl_fs" "prl_fs_freeze" ];
+
+    services.timesyncd.enable = false;
+
+    systemd.services.prltoolsd = {
+      description = "Parallels Tools' service";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${prl-tools}/bin/prltoolsd -f";
+        PIDFile = "/var/run/prltoolsd.pid";
+      };
+    };
+
+    systemd.services.prlfsmountd = mkIf config.hardware.parallels.autoMountShares {
+      description = "Parallels Shared Folders Daemon";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = rec {
+        ExecStart = "${prl-tools}/sbin/prlfsmountd ${PIDFile}";
+        ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p /media";
+        ExecStopPost = "${prl-tools}/sbin/prlfsmountd -u";
+        PIDFile = "/run/prlfsmountd.pid";
+      };
+    };
+
+    systemd.services.prlshprint = {
+      description = "Parallels Shared Printer Tool";
+      wantedBy = [ "multi-user.target" ];
+      bindsTo = [ "cups.service" ];
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${prl-tools}/bin/prlshprint";
+      };
+    };
+
+    systemd.user.services = {
+      prlcc = {
+        description = "Parallels Control Center";
+        wantedBy = [ "graphical-session.target" ];
+        serviceConfig = {
+          ExecStart = "${prl-tools}/bin/prlcc";
+        };
+      };
+      prldnd = {
+        description = "Parallels Control Center";
+        wantedBy = [ "graphical-session.target" ];
+        serviceConfig = {
+          ExecStart = "${prl-tools}/bin/prldnd";
+        };
+      };
+      prl_wmouse_d  = {
+        description = "Parallels Walking Mouse Daemon";
+        wantedBy = [ "graphical-session.target" ];
+        serviceConfig = {
+          ExecStart = "${prl-tools}/bin/prl_wmouse_d";
+        };
+      };
+      prlcp = {
+        description = "Parallels CopyPaste Tool";
+        wantedBy = [ "graphical-session.target" ];
+        serviceConfig = {
+          ExecStart = "${prl-tools}/bin/prlcp";
+        };
+      };
+      prlsga = {
+        description = "Parallels Shared Guest Applications Tool";
+        wantedBy = [ "graphical-session.target" ];
+        serviceConfig = {
+          ExecStart = "${prl-tools}/bin/prlsga";
+        };
+      };
+      prlshprof = {
+        description = "Parallels Shared Profile Tool";
+        wantedBy = [ "graphical-session.target" ];
+        serviceConfig = {
+          ExecStart = "${prl-tools}/bin/prlshprof";
+        };
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/virtualisation/podman/default.nix b/nixos/modules/virtualisation/podman/default.nix
new file mode 100644
index 00000000000..94fd727a4b5
--- /dev/null
+++ b/nixos/modules/virtualisation/podman/default.nix
@@ -0,0 +1,184 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.virtualisation.podman;
+  toml = pkgs.formats.toml { };
+  json = pkgs.formats.json { };
+
+  inherit (lib) mkOption types;
+
+  podmanPackage = (pkgs.podman.override { inherit (cfg) extraPackages; });
+
+  # Provides a fake "docker" binary mapping to podman
+  dockerCompat = pkgs.runCommand "${podmanPackage.pname}-docker-compat-${podmanPackage.version}" {
+    outputs = [ "out" "man" ];
+    inherit (podmanPackage) meta;
+  } ''
+    mkdir -p $out/bin
+    ln -s ${podmanPackage}/bin/podman $out/bin/docker
+
+    mkdir -p $man/share/man/man1
+    for f in ${podmanPackage.man}/share/man/man1/*; do
+      basename=$(basename $f | sed s/podman/docker/g)
+      ln -s $f $man/share/man/man1/$basename
+    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
+    ./network-socket.nix
+    (lib.mkRenamedOptionModule [ "virtualisation" "podman" "libpod" ] [ "virtualisation" "containers" "containersConf" ])
+  ];
+
+  meta = {
+    maintainers = lib.teams.podman.members;
+  };
+
+  options.virtualisation.podman = {
+
+    enable =
+      mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          This option enables Podman, a daemonless container engine for
+          developing, managing, and running OCI Containers on your Linux System.
+
+          It is a drop-in replacement for the <command>docker</command> command.
+        '';
+      };
+
+    dockerSocket.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Make the Podman socket available in place of the Docker socket, so
+        Docker tools can find the Podman socket.
+
+        Podman implements the Docker API.
+
+        Users must be in the <code>podman</code> group in order to connect. As
+        with Docker, members of this group can gain root access.
+      '';
+    };
+
+    dockerCompat = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Create an alias mapping <command>docker</command> to <command>podman</command>.
+      '';
+    };
+
+    enableNvidia = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable use of NVidia GPUs from within podman containers.
+      '';
+    };
+
+    extraPackages = mkOption {
+      type = with types; listOf package;
+      default = [ ];
+      example = lib.literalExpression ''
+        [
+          pkgs.gvisor
+        ]
+      '';
+      description = ''
+        Extra packages to be installed in the Podman wrapper.
+      '';
+    };
+
+    package = lib.mkOption {
+      type = types.package;
+      default = podmanPackage;
+      internal = true;
+      description = ''
+        The final Podman package (including extra packages).
+      '';
+    };
+
+    defaultNetwork.extraPlugins = lib.mkOption {
+      type = types.listOf json.type;
+      default = [];
+      description = ''
+        Extra CNI plugin configurations to add to podman's default network.
+      '';
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable (lib.mkMerge [
+    {
+      environment.systemPackages = [ cfg.package ]
+        ++ lib.optional cfg.dockerCompat dockerCompat;
+
+      environment.etc."cni/net.d/87-podman-bridge.conflist".source = net-conflist;
+
+      virtualisation.containers = {
+        enable = true; # Enable common /etc/containers configuration
+        containersConf.settings = lib.optionalAttrs cfg.enableNvidia {
+          engine = {
+            conmon_env_vars = [ "PATH=${lib.makeBinPath [ pkgs.nvidia-podman ]}" ];
+            runtimes.nvidia = [ "${pkgs.nvidia-podman}/bin/nvidia-container-runtime" ];
+          };
+        };
+      };
+
+      systemd.packages = [ cfg.package ];
+
+      systemd.services.podman.serviceConfig = {
+        ExecStart = [ "" "${cfg.package}/bin/podman $LOGGING system service" ];
+      };
+
+      systemd.sockets.podman.wantedBy = [ "sockets.target" ];
+      systemd.sockets.podman.socketConfig.SocketGroup = "podman";
+
+      systemd.tmpfiles.packages = [
+        # The /run/podman rule interferes with our podman group, so we remove
+        # it and let the systemd socket logic take care of it.
+        (pkgs.runCommand "podman-tmpfiles-nixos" { package = cfg.package; } ''
+          mkdir -p $out/lib/tmpfiles.d/
+          grep -v 'D! /run/podman 0700 root root' \
+            <$package/lib/tmpfiles.d/podman.conf \
+            >$out/lib/tmpfiles.d/podman.conf
+        '') ];
+
+      systemd.tmpfiles.rules =
+        lib.optionals cfg.dockerSocket.enable [
+          "L! /run/docker.sock - - - - /run/podman/podman.sock"
+        ];
+
+      users.groups.podman = {};
+
+      assertions = [
+        {
+          assertion = cfg.dockerCompat -> !config.virtualisation.docker.enable;
+          message = "Option dockerCompat conflicts with docker";
+        }
+        {
+          assertion = cfg.dockerSocket.enable -> !config.virtualisation.docker.enable;
+          message = ''
+            The options virtualisation.podman.dockerSocket.enable and virtualisation.docker.enable conflict, because only one can serve the socket.
+          '';
+        }
+      ];
+    }
+  ]);
+}
diff --git a/nixos/modules/virtualisation/podman/dnsname.nix b/nixos/modules/virtualisation/podman/dnsname.nix
new file mode 100644
index 00000000000..beef1975507
--- /dev/null
+++ b/nixos/modules/virtualisation/podman/dnsname.nix
@@ -0,0 +1,36 @@
+{ 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 = ''
+          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/podman/network-socket-ghostunnel.nix b/nixos/modules/virtualisation/podman/network-socket-ghostunnel.nix
new file mode 100644
index 00000000000..a0e7e433164
--- /dev/null
+++ b/nixos/modules/virtualisation/podman/network-socket-ghostunnel.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkg, ... }:
+let
+  inherit (lib)
+    mkOption
+    types
+    ;
+
+  cfg = config.virtualisation.podman.networkSocket;
+
+in
+{
+  options.virtualisation.podman.networkSocket = {
+    server = mkOption {
+      type = types.enum [ "ghostunnel" ];
+    };
+  };
+
+  config = lib.mkIf (cfg.enable && cfg.server == "ghostunnel") {
+
+    services.ghostunnel = {
+      enable = true;
+      servers."podman-socket" = {
+        inherit (cfg.tls) cert key cacert;
+        listen = "${cfg.listenAddress}:${toString cfg.port}";
+        target = "unix:/run/podman/podman.sock";
+        allowAll = lib.mkDefault true;
+      };
+    };
+    systemd.services.ghostunnel-server-podman-socket.serviceConfig.SupplementaryGroups = ["podman"];
+
+  };
+
+  meta.maintainers = lib.teams.podman.members ++ [ lib.maintainers.roberth ];
+}
diff --git a/nixos/modules/virtualisation/podman/network-socket.nix b/nixos/modules/virtualisation/podman/network-socket.nix
new file mode 100644
index 00000000000..94d8da9d2b6
--- /dev/null
+++ b/nixos/modules/virtualisation/podman/network-socket.nix
@@ -0,0 +1,95 @@
+{ config, lib, pkg, ... }:
+let
+  inherit (lib)
+    mkOption
+    types
+    ;
+
+  cfg = config.virtualisation.podman.networkSocket;
+
+in
+{
+  imports = [
+    ./network-socket-ghostunnel.nix
+  ];
+
+  options.virtualisation.podman.networkSocket = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Make the Podman and Docker compatibility API available over the network
+        with TLS client certificate authentication.
+
+        This allows Docker clients to connect with the equivalents of the Docker
+        CLI <code>-H</code> and <code>--tls*</code> family of options.
+
+        For certificate setup, see https://docs.docker.com/engine/security/protect-access/
+
+        This option is independent of <xref linkend="opt-virtualisation.podman.dockerSocket.enable"/>.
+      '';
+    };
+
+    server = mkOption {
+      type = types.enum [];
+      description = ''
+        Choice of TLS proxy server.
+      '';
+      example = "ghostunnel";
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to open the port in the firewall.
+      '';
+    };
+
+    tls.cacert = mkOption {
+      type = types.path;
+      description = ''
+        Path to CA certificate to use for client authentication.
+      '';
+    };
+
+    tls.cert = mkOption {
+      type = types.path;
+      description = ''
+        Path to certificate describing the server.
+      '';
+    };
+
+    tls.key = mkOption {
+      type = types.path;
+      description = ''
+        Path to the private key corresponding to the server certificate.
+
+        Use a string for this setting. Otherwise it will be copied to the Nix
+        store first, where it is readable by any system process.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 2376;
+      description = ''
+        TCP port number for receiving TLS connections.
+      '';
+    };
+    listenAddress = mkOption {
+      type = types.str;
+      default = "0.0.0.0";
+      description = ''
+        Interface address for receiving TLS connections.
+      '';
+    };
+  };
+
+  config = {
+    networking.firewall.allowedTCPPorts =
+      lib.optional (cfg.enable && cfg.openFirewall) cfg.port;
+  };
+
+  meta.maintainers = lib.teams.podman.members ++ [ lib.maintainers.roberth ];
+}
diff --git a/nixos/modules/virtualisation/proxmox-image.nix b/nixos/modules/virtualisation/proxmox-image.nix
new file mode 100644
index 00000000000..c537d5aed44
--- /dev/null
+++ b/nixos/modules/virtualisation/proxmox-image.nix
@@ -0,0 +1,169 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+  options.proxmox = {
+    qemuConf = {
+      # essential configs
+      boot = mkOption {
+        type = types.str;
+        default = "";
+        example = "order=scsi0;net0";
+        description = ''
+          Default boot device. PVE will try all devices in its default order if this value is empty.
+        '';
+      };
+      scsihw = mkOption {
+        type = types.str;
+        default = "virtio-scsi-pci";
+        example = "lsi";
+        description = ''
+          SCSI controller type. Must be one of the supported values given in
+          <link xlink:href="https://pve.proxmox.com/wiki/Qemu/KVM_Virtual_Machines"/>
+        '';
+      };
+      virtio0 = mkOption {
+        type = types.str;
+        default = "local-lvm:vm-9999-disk-0";
+        example = "ceph:vm-123-disk-0";
+        description = ''
+          Configuration for the default virtio disk. It can be used as a cue for PVE to autodetect the target sotrage.
+          This parameter is required by PVE even if it isn't used.
+        '';
+      };
+      ostype = mkOption {
+        type = types.str;
+        default = "l26";
+        description = ''
+          Guest OS type
+        '';
+      };
+      cores = mkOption {
+        type = types.ints.positive;
+        default = 1;
+        description = ''
+          Guest core count
+        '';
+      };
+      memory = mkOption {
+        type = types.ints.positive;
+        default = 1024;
+        description = ''
+          Guest memory in MB
+        '';
+      };
+
+      # optional configs
+      name = mkOption {
+        type = types.str;
+        default = "nixos-${config.system.nixos.label}";
+        description = ''
+          VM name
+        '';
+      };
+      net0 = mkOption {
+        type = types.commas;
+        default = "virtio=00:00:00:00:00:00,bridge=vmbr0,firewall=1";
+        description = ''
+          Configuration for the default interface. When restoring from VMA, check the
+          "unique" box to ensure device mac is randomized.
+        '';
+      };
+      serial0 = mkOption {
+        type = types.str;
+        default = "socket";
+        example = "/dev/ttyS0";
+        description = ''
+          Create a serial device inside the VM (n is 0 to 3), and pass through a host serial device (i.e. /dev/ttyS0),
+          or create a unix socket on the host side (use qm terminal to open a terminal connection).
+        '';
+      };
+      agent = mkOption {
+        type = types.bool;
+        apply = x: if x then "1" else "0";
+        default = true;
+        description = ''
+          Expect guest to have qemu agent running
+        '';
+      };
+    };
+    qemuExtraConf = mkOption {
+      type = with types; attrsOf (oneOf [ str int ]);
+      default = {};
+      example = literalExpression ''{
+        cpu = "host";
+        onboot = 1;
+      }'';
+      description = ''
+        Additional options appended to qemu-server.conf
+      '';
+    };
+    filenameSuffix = mkOption {
+      type = types.str;
+      default = config.proxmox.qemuConf.name;
+      example = "999-nixos_template";
+      description = ''
+        Filename of the image will be vzdump-qemu-''${filenameSuffix}.vma.zstd.
+        This will also determine the default name of the VM on restoring the VMA.
+        Start this value with a number if you want the VMA to be detected as a backup of
+        any specific VMID.
+      '';
+    };
+  };
+
+  config = let
+    cfg = config.proxmox;
+    cfgLine = name: value: ''
+      ${name}: ${builtins.toString value}
+    '';
+    cfgFile = fileName: properties: pkgs.writeTextDir fileName ''
+      # generated by NixOS
+      ${lib.concatStrings (lib.mapAttrsToList cfgLine properties)}
+      #qmdump#map:virtio0:drive-virtio0:local-lvm:raw:
+    '';
+  in {
+    system.build.VMA = import ../../lib/make-disk-image.nix {
+      name = "proxmox-${cfg.filenameSuffix}";
+      postVM = let
+        # Build qemu with PVE's patch that adds support for the VMA format
+        vma = pkgs.qemu_kvm.overrideAttrs ( super: {
+          patches = let
+            rev = "cc707c362ea5c8d832aac270d1ffa7ac66a8908f";
+            path = "debian/patches/pve/0025-PVE-Backup-add-vma-backup-format-code.patch";
+            vma-patch = pkgs.fetchpatch {
+              url = "https://git.proxmox.com/?p=pve-qemu.git;a=blob_plain;hb=${rev};f=${path}";
+              sha256 = "1z467xnmfmry3pjy7p34psd5xdil9x0apnbvfz8qbj0bf9fgc8zf";
+            };
+          in super.patches ++ [ vma-patch ];
+          buildInputs = super.buildInputs ++ [ pkgs.libuuid ];
+        });
+      in
+      ''
+        ${vma}/bin/vma create "vzdump-qemu-${cfg.filenameSuffix}.vma" \
+          -c ${cfgFile "qemu-server.conf" (cfg.qemuConf // cfg.qemuExtraConf)}/qemu-server.conf drive-virtio0=$diskImage
+        rm $diskImage
+        ${pkgs.zstd}/bin/zstd "vzdump-qemu-${cfg.filenameSuffix}.vma"
+        mv "vzdump-qemu-${cfg.filenameSuffix}.vma.zst" $out/
+      '';
+      format = "raw";
+      inherit config lib pkgs;
+    };
+
+    boot = {
+      growPartition = true;
+      kernelParams = [ "console=ttyS0" ];
+      loader.grub.device = lib.mkDefault "/dev/vda";
+      loader.timeout = 0;
+      initrd.availableKernelModules = [ "uas" "virtio_blk" "virtio_pci" ];
+    };
+
+    fileSystems."/" = {
+      device = "/dev/disk/by-label/nixos";
+      autoResize = true;
+      fsType = "ext4";
+    };
+
+    services.qemuGuest.enable = lib.mkDefault true;
+  };
+}
diff --git a/nixos/modules/virtualisation/proxmox-lxc.nix b/nixos/modules/virtualisation/proxmox-lxc.nix
new file mode 100644
index 00000000000..3913b474afb
--- /dev/null
+++ b/nixos/modules/virtualisation/proxmox-lxc.nix
@@ -0,0 +1,64 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+  options.proxmoxLXC = {
+    privileged = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable privileged mounts
+      '';
+    };
+    manageNetwork = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to manage network interfaces through nix options
+        When false, systemd-networkd is enabled to accept network
+        configuration from proxmox.
+      '';
+    };
+  };
+
+  config =
+    let
+      cfg = config.proxmoxLXC;
+    in
+    {
+      system.build.tarball = pkgs.callPackage ../../lib/make-system-tarball.nix {
+        storeContents = [{
+          object = config.system.build.toplevel;
+          symlink = "none";
+        }];
+
+        contents = [{
+          source = config.system.build.toplevel + "/init";
+          target = "/sbin/init";
+        }];
+
+        extraCommands = "mkdir -p root etc/systemd/network";
+      };
+
+      boot = {
+        isContainer = true;
+        loader.initScript.enable = true;
+      };
+
+      networking = mkIf (!cfg.manageNetwork) {
+        useDHCP = false;
+        useHostResolvConf = false;
+        useNetworkd = true;
+      };
+
+      services.openssh = {
+        enable = mkDefault true;
+        startWhenNeeded = mkDefault true;
+      };
+
+      systemd.mounts = mkIf (!cfg.privileged)
+        [{ where = "/sys/kernel/debug"; enable = false; }];
+
+    };
+}
diff --git a/nixos/modules/virtualisation/qemu-guest-agent.nix b/nixos/modules/virtualisation/qemu-guest-agent.nix
new file mode 100644
index 00000000000..39273e523e8
--- /dev/null
+++ b/nixos/modules/virtualisation/qemu-guest-agent.nix
@@ -0,0 +1,45 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.qemuGuest;
+in {
+
+  options.services.qemuGuest = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the qemu guest agent.";
+      };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.qemu_kvm.ga;
+        defaultText = literalExpression "pkgs.qemu_kvm.ga";
+        description = "The QEMU guest agent package.";
+      };
+  };
+
+  config = mkIf cfg.enable (
+      mkMerge [
+    {
+
+      services.udev.extraRules = ''
+        SUBSYSTEM=="virtio-ports", ATTR{name}=="org.qemu.guest_agent.0", TAG+="systemd" ENV{SYSTEMD_WANTS}="qemu-guest-agent.service"
+      '';
+
+      systemd.services.qemu-guest-agent = {
+        description = "Run the QEMU Guest Agent";
+        serviceConfig = {
+          ExecStart = "${cfg.package}/bin/qemu-ga --statedir /run/qemu-ga";
+          Restart = "always";
+          RestartSec = 0;
+          # Runtime directory and mode
+          RuntimeDirectory = "qemu-ga";
+          RuntimeDirectoryMode = "0755";
+        };
+      };
+    }
+  ]
+  );
+}
diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix
new file mode 100644
index 00000000000..51438935894
--- /dev/null
+++ b/nixos/modules/virtualisation/qemu-vm.nix
@@ -0,0 +1,1016 @@
+# This module creates a virtual machine from the NixOS configuration.
+# Building the `config.system.build.vm' attribute gives you a command
+# that starts a KVM/QEMU VM running the NixOS configuration defined in
+# `config'.  The Nix store is shared read-only with the host, which
+# makes (re)building VMs very efficient.  However, it also means you
+# can't reconfigure the guest inside the guest - you need to rebuild
+# the VM in the host.  On the other hand, the root filesystem is a
+# read/writable disk image persistent across VM reboots.
+
+{ config, lib, pkgs, options, ... }:
+
+with lib;
+
+let
+
+  qemu-common = import ../../lib/qemu-common.nix { inherit lib pkgs; };
+
+  cfg = config.virtualisation;
+
+  qemu = cfg.qemu.package;
+
+  consoles = lib.concatMapStringsSep " " (c: "console=${c}") cfg.qemu.consoles;
+
+  driveOpts = { ... }: {
+
+    options = {
+
+      file = mkOption {
+        type = types.str;
+        description = "The file image used for this drive.";
+      };
+
+      driveExtraOpts = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        description = "Extra options passed to drive flag.";
+      };
+
+      deviceExtraOpts = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        description = "Extra options passed to device flag.";
+      };
+
+      name = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description =
+          "A name for the drive. Must be unique in the drives list. Not passed to qemu.";
+      };
+
+    };
+
+  };
+
+  driveCmdline = idx: { file, driveExtraOpts, deviceExtraOpts, ... }:
+    let
+      drvId = "drive${toString idx}";
+      mkKeyValue = generators.mkKeyValueDefault {} "=";
+      mkOpts = opts: concatStringsSep "," (mapAttrsToList mkKeyValue opts);
+      driveOpts = mkOpts (driveExtraOpts // {
+        index = idx;
+        id = drvId;
+        "if" = "none";
+        inherit file;
+      });
+      deviceOpts = mkOpts (deviceExtraOpts // {
+        drive = drvId;
+      });
+      device =
+        if cfg.qemu.diskInterface == "scsi" then
+          "-device lsi53c895a -device scsi-hd,${deviceOpts}"
+        else
+          "-device virtio-blk-pci,${deviceOpts}";
+    in
+      "-drive ${driveOpts} ${device}";
+
+  drivesCmdLine = drives: concatStringsSep "\\\n    " (imap1 driveCmdline drives);
+
+
+  # Creates a device name from a 1-based a numerical index, e.g.
+  # * `driveDeviceName 1` -> `/dev/vda`
+  # * `driveDeviceName 2` -> `/dev/vdb`
+  driveDeviceName = idx:
+    let letter = elemAt lowerChars (idx - 1);
+    in if cfg.qemu.diskInterface == "scsi" then
+      "/dev/sd${letter}"
+    else
+      "/dev/vd${letter}";
+
+  lookupDriveDeviceName = driveName: driveList:
+    (findSingle (drive: drive.name == driveName)
+      (throw "Drive ${driveName} not found")
+      (throw "Multiple drives named ${driveName}") driveList).device;
+
+  addDeviceNames =
+    imap1 (idx: drive: drive // { device = driveDeviceName idx; });
+
+  efiPrefix =
+    if pkgs.stdenv.hostPlatform.isx86 then "${pkgs.OVMF.fd}/FV/OVMF"
+    else if pkgs.stdenv.isAarch64 then "${pkgs.OVMF.fd}/FV/AAVMF"
+    else throw "No EFI firmware available for platform";
+  efiFirmware = "${efiPrefix}_CODE.fd";
+  efiVarsDefault = "${efiPrefix}_VARS.fd";
+
+  # Shell script to start the VM.
+  startVM =
+    ''
+      #! ${pkgs.runtimeShell}
+
+      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
+      fi
+
+      # Create a directory for storing temporary data of the running VM.
+      if [ -z "$TMPDIR" ] || [ -z "$USE_TMPDIR" ]; then
+          TMPDIR=$(mktemp -d nix-vm.XXXXXXXXXX --tmpdir)
+      fi
+
+      ${lib.optionalString cfg.useNixStoreImage
+      ''
+        # Create a writable copy/snapshot of the store image.
+        ${qemu}/bin/qemu-img create -f qcow2 -F qcow2 -b ${storeImage}/nixos.qcow2 "$TMPDIR"/store.img
+      ''}
+
+      # Create a directory for exchanging data with the VM.
+      mkdir -p "$TMPDIR/xchg"
+
+      ${lib.optionalString cfg.useBootLoader
+      ''
+        # 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
+        ''}
+      ''}
+
+      cd "$TMPDIR"
+
+      ${lib.optionalString (cfg.emptyDiskImages != []) "idx=0"}
+      ${flip concatMapStrings cfg.emptyDiskImages (size: ''
+        if ! test -e "empty$idx.qcow2"; then
+            ${qemu}/bin/qemu-img create -f qcow2 "empty$idx.qcow2" "${toString size}M"
+        fi
+        idx=$((idx + 1))
+      '')}
+
+      # Start QEMU.
+      exec ${qemu-common.qemuBinary qemu} \
+          -name ${config.system.name} \
+          -m ${toString config.virtualisation.memorySize} \
+          -smp ${toString config.virtualisation.cores} \
+          -device virtio-rng-pci \
+          ${concatStringsSep " " config.virtualisation.qemu.networkingOptions} \
+          ${concatStringsSep " \\\n    "
+            (mapAttrsToList
+              (tag: share: "-virtfs local,path=${share.source},security_model=none,mount_tag=${tag}")
+              config.virtualisation.sharedDirectories)} \
+          ${drivesCmdLine config.virtualisation.qemu.drives} \
+          ${concatStringsSep " \\\n    " config.virtualisation.qemu.options} \
+          $QEMU_OPTS \
+          "$@"
+    '';
+
+
+  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 ${efiVarsDefault} $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=${efiFirmware}"
+                      + " -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
+        '' # */
+    );
+
+  storeImage = import ../../lib/make-disk-image.nix {
+    inherit pkgs config lib;
+    additionalPaths = [ regInfo ];
+    format = "qcow2";
+    onlyNixStore = true;
+    partitionTableType = "none";
+    installBootLoader = false;
+    diskSize = "auto";
+    additionalSpace = "0M";
+    copyChannel = false;
+  };
+
+in
+
+{
+  imports = [
+    ../profiles/qemu-guest.nix
+    (mkRenamedOptionModule [ "virtualisation" "pathsInNixDB" ] [ "virtualisation" "additionalPaths" ])
+  ];
+
+  options = {
+
+    virtualisation.fileSystems = options.fileSystems;
+
+    virtualisation.memorySize =
+      mkOption {
+        type = types.ints.positive;
+        default = 1024;
+        description =
+          ''
+            The memory size in megabytes of the virtual machine.
+          '';
+      };
+
+    virtualisation.msize =
+      mkOption {
+        type = types.ints.positive;
+        default = 16384;
+        description =
+          ''
+            The msize (maximum packet size) option passed to 9p file systems, in
+            bytes. Increasing this should increase performance significantly,
+            at the cost of higher RAM usage.
+          '';
+      };
+
+    virtualisation.diskSize =
+      mkOption {
+        type = types.nullOr types.ints.positive;
+        default = 1024;
+        description =
+          ''
+            The disk size in megabytes of the virtual machine.
+          '';
+      };
+
+    virtualisation.diskImage =
+      mkOption {
+        type = types.str;
+        default = "./${config.system.name}.qcow2";
+        defaultText = literalExpression ''"./''${config.system.name}.qcow2"'';
+        description =
+          ''
+            Path to the disk image containing the root filesystem.
+            The image will be created on startup if it does not
+            exist.
+          '';
+      };
+
+    virtualisation.bootDevice =
+      mkOption {
+        type = types.path;
+        example = "/dev/vda";
+        description =
+          ''
+            The disk to be used for the root filesystem.
+          '';
+      };
+
+    virtualisation.emptyDiskImages =
+      mkOption {
+        type = types.listOf types.ints.positive;
+        default = [];
+        description =
+          ''
+            Additional disk images to provide to the VM. The value is
+            a list of size in megabytes of each disk. These disks are
+            writeable by the VM.
+          '';
+      };
+
+    virtualisation.graphics =
+      mkOption {
+        type = types.bool;
+        default = true;
+        description =
+          ''
+            Whether to run QEMU with a graphics window, or in nographic mode.
+            Serial console will be enabled on both settings, but this will
+            change the preferred console.
+            '';
+      };
+
+    virtualisation.resolution =
+      mkOption {
+        type = options.services.xserver.resolutions.type.nestedTypes.elemType;
+        default = { x = 1024; y = 768; };
+        description =
+          ''
+            The resolution of the virtual machine display.
+          '';
+      };
+
+    virtualisation.cores =
+      mkOption {
+        type = types.ints.positive;
+        default = 1;
+        description =
+          ''
+            Specify the number of cores the guest is permitted to use.
+            The number can be higher than the available cores on the
+            host system.
+          '';
+      };
+
+    virtualisation.sharedDirectories =
+      mkOption {
+        type = types.attrsOf
+          (types.submodule {
+            options.source = mkOption {
+              type = types.str;
+              description = "The path of the directory to share, can be a shell variable";
+            };
+            options.target = mkOption {
+              type = types.path;
+              description = "The mount point of the directory inside the virtual machine";
+            };
+          });
+        default = { };
+        example = {
+          my-share = { source = "/path/to/be/shared"; target = "/mnt/shared"; };
+        };
+        description =
+          ''
+            An attributes set of directories that will be shared with the
+            virtual machine using VirtFS (9P filesystem over VirtIO).
+            The attribute name will be used as the 9P mount tag.
+          '';
+      };
+
+    virtualisation.additionalPaths =
+      mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description =
+          ''
+            A list of paths whose closure should be made available to
+            the VM.
+
+            When 9p is used, the closure is registered in the Nix
+            database in the VM. All other paths in the host Nix store
+            appear in the guest Nix store as well, but are considered
+            garbage (because they are not registered in the Nix
+            database of the guest).
+
+            When <option>virtualisation.useNixStoreImage</option> is
+            set, the closure is copied to the Nix store image.
+          '';
+      };
+
+    virtualisation.forwardPorts = mkOption {
+      type = types.listOf
+        (types.submodule {
+          options.from = mkOption {
+            type = types.enum [ "host" "guest" ];
+            default = "host";
+            description =
+              ''
+                Controls the direction in which the ports are mapped:
+
+                - <literal>"host"</literal> means traffic from the host ports
+                is forwarded to the given guest port.
+
+                - <literal>"guest"</literal> means traffic from the guest ports
+                is forwarded to the given host port.
+              '';
+          };
+          options.proto = mkOption {
+            type = types.enum [ "tcp" "udp" ];
+            default = "tcp";
+            description = "The protocol to forward.";
+          };
+          options.host.address = mkOption {
+            type = types.str;
+            default = "";
+            description = "The IPv4 address of the host.";
+          };
+          options.host.port = mkOption {
+            type = types.port;
+            description = "The host port to be mapped.";
+          };
+          options.guest.address = mkOption {
+            type = types.str;
+            default = "";
+            description = "The IPv4 address on the guest VLAN.";
+          };
+          options.guest.port = mkOption {
+            type = types.port;
+            description = "The guest port to be mapped.";
+          };
+        });
+      default = [];
+      example = lib.literalExpression
+        ''
+        [ # forward local port 2222 -> 22, to ssh into the VM
+          { from = "host"; host.port = 2222; guest.port = 22; }
+
+          # forward local port 80 -> 10.0.2.10:80 in the VLAN
+          { from = "guest";
+            guest.address = "10.0.2.10"; guest.port = 80;
+            host.address = "127.0.0.1"; host.port = 80;
+          }
+        ]
+        '';
+      description =
+        ''
+          When using the SLiRP user networking (default), this option allows to
+          forward ports to/from the host/guest.
+
+          <warning><para>
+            If the NixOS firewall on the virtual machine is enabled, you also
+            have to open the guest ports to enable the traffic between host and
+            guest.
+          </para></warning>
+
+          <note><para>Currently QEMU supports only IPv4 forwarding.</para></note>
+        '';
+    };
+
+    virtualisation.vlans =
+      mkOption {
+        type = types.listOf types.ints.unsigned;
+        default = [ 1 ];
+        example = [ 1 2 ];
+        description =
+          ''
+            Virtual networks to which the VM is connected.  Each
+            number <replaceable>N</replaceable> in this list causes
+            the VM to have a virtual Ethernet interface attached to a
+            separate virtual network on which it will be assigned IP
+            address
+            <literal>192.168.<replaceable>N</replaceable>.<replaceable>M</replaceable></literal>,
+            where <replaceable>M</replaceable> is the index of this VM
+            in the list of VMs.
+          '';
+      };
+
+    virtualisation.writableStore =
+      mkOption {
+        type = types.bool;
+        default = true; # FIXME
+        description =
+          ''
+            If enabled, the Nix store in the VM is made writable by
+            layering an overlay filesystem on top of the host's Nix
+            store.
+          '';
+      };
+
+    virtualisation.writableStoreUseTmpfs =
+      mkOption {
+        type = types.bool;
+        default = true;
+        description =
+          ''
+            Use a tmpfs for the writable store instead of writing to the VM's
+            own filesystem.
+          '';
+      };
+
+    networking.primaryIPAddress =
+      mkOption {
+        type = types.str;
+        default = "";
+        internal = true;
+        description = "Primary IP address used in /etc/hosts.";
+      };
+
+    virtualisation.qemu = {
+      package =
+        mkOption {
+          type = types.package;
+          default = pkgs.qemu_kvm;
+          example = "pkgs.qemu_test";
+          description = "QEMU package to use.";
+        };
+
+      options =
+        mkOption {
+          type = types.listOf types.str;
+          default = [];
+          example = [ "-vga std" ];
+          description = "Options passed to QEMU.";
+        };
+
+      consoles = mkOption {
+        type = types.listOf types.str;
+        default = let
+          consoles = [ "${qemu-common.qemuSerialDevice},115200n8" "tty0" ];
+        in if cfg.graphics then consoles else reverseList consoles;
+        example = [ "console=tty1" ];
+        description = ''
+          The output console devices to pass to the kernel command line via the
+          <literal>console</literal> parameter, the primary console is the last
+          item of this list.
+
+          By default it enables both serial console and
+          <literal>tty0</literal>. The preferred console (last one) is based on
+          the value of <option>virtualisation.graphics</option>.
+        '';
+      };
+
+      networkingOptions =
+        mkOption {
+          type = types.listOf types.str;
+          default = [ ];
+          example = [
+            "-net nic,netdev=user.0,model=virtio"
+            "-netdev user,id=user.0,\${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}"
+          ];
+          description = ''
+            Networking-related command-line options that should be passed to qemu.
+            The default is to use userspace networking (SLiRP).
+
+            If you override this option, be advised to keep
+            ''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS} (as seen in the example)
+            to keep the default runtime behaviour.
+          '';
+        };
+
+      drives =
+        mkOption {
+          type = types.listOf (types.submodule driveOpts);
+          description = "Drives passed to qemu.";
+          apply = addDeviceNames;
+        };
+
+      diskInterface =
+        mkOption {
+          type = types.enum [ "virtio" "scsi" "ide" ];
+          default = "virtio";
+          example = "scsi";
+          description = "The interface used for the virtual hard disks.";
+        };
+
+      guestAgent.enable =
+        mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Enable the Qemu guest agent.
+          '';
+        };
+
+      virtioKeyboard =
+        mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Enable the virtio-keyboard device.
+          '';
+        };
+    };
+
+    virtualisation.useNixStoreImage =
+      mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Build and use a disk image for the Nix store, instead of
+          accessing the host's one through 9p.
+
+          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.
+        '';
+      };
+
+    virtualisation.useBootLoader =
+      mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          ''
+            If enabled, the virtual machine will be booted using the
+            regular boot loader (i.e., GRUB 1 or 2).  This allows
+            testing of the boot loader.  If
+            disabled (the default), the VM directly boots the NixOS
+            kernel and initial ramdisk, bypassing the boot loader
+            altogether.
+          '';
+      };
+
+    virtualisation.useEFIBoot =
+      mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          ''
+            If enabled, the virtual machine will provide a EFI boot
+            manager.
+            useEFIBoot is ignored if useBootLoader == false.
+          '';
+      };
+
+    virtualisation.efiVars =
+      mkOption {
+        type = types.str;
+        default = "./${config.system.name}-efi-vars.fd";
+        defaultText = literalExpression ''"./''${config.system.name}-efi-vars.fd"'';
+        description =
+          ''
+            Path to nvram image containing UEFI variables.  The will be created
+            on startup if it does not exist.
+          '';
+      };
+
+    virtualisation.bios =
+      mkOption {
+        type = types.nullOr types.package;
+        default = null;
+        description =
+          ''
+            An alternate BIOS (such as <package>qboot</package>) with which to start the VM.
+            Should contain a file named <literal>bios.bin</literal>.
+            If <literal>null</literal>, QEMU's builtin SeaBIOS will be used.
+          '';
+      };
+
+  };
+
+  config = {
+
+    assertions =
+      lib.concatLists (lib.flip lib.imap cfg.forwardPorts (i: rule:
+        [
+          { assertion = rule.from == "guest" -> rule.proto == "tcp";
+            message =
+              ''
+                Invalid virtualisation.forwardPorts.<entry ${toString i}>.proto:
+                  Guest forwarding supports only TCP connections.
+              '';
+          }
+          { assertion = rule.from == "guest" -> lib.hasPrefix "10.0.2." rule.guest.address;
+            message =
+              ''
+                Invalid virtualisation.forwardPorts.<entry ${toString i}>.guest.address:
+                  The address must be in the default VLAN (10.0.2.0/24).
+              '';
+          }
+        ]));
+
+    # Note [Disk layout with `useBootLoader`]
+    #
+    # If `useBootLoader = true`, we configure 2 drives:
+    # `/dev/?da` for the root disk, and `/dev/?db` for the boot disk
+    # which has the `/boot` partition and the boot loader.
+    # Concretely:
+    #
+    # * The second drive's image `disk.img` is created in `bootDisk = ...`
+    #   using a throwaway VM. Note that there the disk is always `/dev/vda`,
+    #   even though in the final VM it will be at `/dev/*b`.
+    # * The disks are attached in `virtualisation.qemu.drives`.
+    #   Their order makes them appear as devices `a`, `b`, etc.
+    # * `fileSystems."/boot"` is adjusted to be on device `b`.
+
+    # If `useBootLoader`, GRUB goes to the second disk, see
+    # note [Disk layout with `useBootLoader`].
+    boot.loader.grub.device = mkVMOverride (
+      if cfg.useBootLoader
+        then driveDeviceName 2 # second disk
+        else cfg.bootDevice
+    );
+    boot.loader.grub.gfxmodeBios = with cfg.resolution; "${toString x}x${toString y}";
+
+    boot.initrd.extraUtilsCommands =
+      ''
+        # We need mke2fs in the initrd.
+        copy_bin_and_libs ${pkgs.e2fsprogs}/bin/mke2fs
+      '';
+
+    boot.initrd.postDeviceCommands =
+      ''
+        # If the disk image appears to be empty, run mke2fs to
+        # initialise.
+        FSTYPE=$(blkid -o value -s TYPE ${cfg.bootDevice} || true)
+        if test -z "$FSTYPE"; then
+            mke2fs -t ext4 ${cfg.bootDevice}
+        fi
+      '';
+
+    boot.initrd.postMountCommands =
+      ''
+        # Mark this as a NixOS machine.
+        mkdir -p $targetRoot/etc
+        echo -n > $targetRoot/etc/NIXOS
+
+        # Fix the permissions on /tmp.
+        chmod 1777 $targetRoot/tmp
+
+        mkdir -p $targetRoot/boot
+
+        ${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
+          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
+        ''}
+      '';
+
+    # After booting, register the closure of the paths in
+    # `virtualisation.additionalPaths' in the Nix database in the VM.  This
+    # allows Nix operations to work in the VM.  The path to the
+    # registration file is passed through the kernel command line to
+    # allow `system.build.toplevel' to be included.  (If we had a direct
+    # reference to ${regInfo} here, then we would get a cyclic
+    # dependency.)
+    boot.postBootCommands =
+      ''
+        if [[ "$(cat /proc/cmdline)" =~ regInfo=([^ ]*) ]]; then
+          ${config.nix.package.out}/bin/nix-store --load-db < ''${BASH_REMATCH[1]}
+        fi
+      '';
+
+    boot.initrd.availableKernelModules =
+      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) {
+        source = builtins.storeDir;
+        target = "/nix/store";
+      };
+      xchg = {
+        source = ''"$TMPDIR"/xchg'';
+        target = "/tmp/xchg";
+      };
+      shared = {
+        source = ''"''${SHARED_DIR:-$TMPDIR/xchg}"'';
+        target = "/tmp/shared";
+      };
+    };
+
+    virtualisation.qemu.networkingOptions =
+      let
+        forwardingOptions = flip concatMapStrings cfg.forwardPorts
+          ({ proto, from, host, guest }:
+            if from == "host"
+              then "hostfwd=${proto}:${host.address}:${toString host.port}-" +
+                   "${guest.address}:${toString guest.port},"
+              else "'guestfwd=${proto}:${guest.address}:${toString guest.port}-" +
+                   "cmd:${pkgs.netcat}/bin/nc ${host.address} ${toString host.port}',"
+          );
+      in
+      [
+        "-net nic,netdev=user.0,model=virtio"
+        "-netdev user,id=user.0,${forwardingOptions}\"$QEMU_NET_OPTS\""
+      ];
+
+    # FIXME: Consolidate this one day.
+    virtualisation.qemu.options = mkMerge [
+      (mkIf cfg.qemu.virtioKeyboard [
+        "-device virtio-keyboard"
+      ])
+      (mkIf pkgs.stdenv.hostPlatform.isx86 [
+        "-usb" "-device usb-tablet,bus=usb-bus.0"
+      ])
+      (mkIf (pkgs.stdenv.isAarch32 || pkgs.stdenv.isAarch64) [
+        "-device virtio-gpu-pci" "-device usb-ehci,id=usb0" "-device usb-kbd" "-device usb-tablet"
+      ])
+      (mkIf (!cfg.useBootLoader) [
+        "-kernel ${config.system.build.toplevel}/kernel"
+        "-initrd ${config.system.build.toplevel}/initrd"
+        ''-append "$(cat ${config.system.build.toplevel}/kernel-params) init=${config.system.build.toplevel}/init regInfo=${regInfo}/registration ${consoles} $QEMU_KERNEL_PARAMS"''
+      ])
+      (mkIf cfg.useEFIBoot [
+        "-drive if=pflash,format=raw,unit=0,readonly=on,file=${efiFirmware}"
+        "-drive if=pflash,format=raw,unit=1,file=$NIX_EFI_VARS"
+      ])
+      (mkIf (cfg.bios != null) [
+        "-bios ${cfg.bios}/bios.bin"
+      ])
+      (mkIf (!cfg.graphics) [
+        "-nographic"
+      ])
+    ];
+
+    virtualisation.qemu.drives = mkMerge [
+      [{
+        name = "root";
+        file = ''"$NIX_DISK_IMAGE"'';
+        driveExtraOpts.cache = "writeback";
+        driveExtraOpts.werror = "report";
+      }]
+      (mkIf cfg.useNixStoreImage [{
+        name = "nix-store";
+        file = ''"$TMPDIR"/store.img'';
+        deviceExtraOpts.bootindex = if cfg.useBootLoader then "3" else "2";
+      }])
+      (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)
+    ];
+
+    # 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
+      mkSharedDir = tag: share:
+        {
+          name =
+            if tag == "nix-store" && cfg.writableStore
+              then "/nix/.ro-store"
+              else share.target;
+          value.device = tag;
+          value.fsType = "9p";
+          value.neededForBoot = true;
+          value.options =
+            [ "trans=virtio" "version=9p2000.L"  "msize=${toString cfg.msize}" ]
+            ++ lib.optional (tag == "nix-store") "cache=loose";
+        };
+    in
+      mkVMOverride (cfg.fileSystems //
+      {
+        "/".device = cfg.bootDevice;
+
+        "/tmp" = mkIf config.boot.tmpOnTmpfs
+          { device = "tmpfs";
+            fsType = "tmpfs";
+            neededForBoot = true;
+            # Sync with systemd's tmp.mount;
+            options = [ "mode=1777" "strictatime" "nosuid" "nodev" "size=${toString config.boot.tmpOnTmpfsSize}" ];
+          };
+
+        "/nix/${if cfg.writableStore then ".ro-store" else "store"}" =
+          mkIf cfg.useNixStoreImage
+            { device = "${lookupDriveDeviceName "nix-store" cfg.qemu.drives}";
+              neededForBoot = true;
+              options = [ "ro" ];
+            };
+
+        "/nix/.rw-store" = mkIf (cfg.writableStore && cfg.writableStoreUseTmpfs)
+          { fsType = "tmpfs";
+            options = [ "mode=0755" ];
+            neededForBoot = true;
+          };
+
+        "/boot" = mkIf cfg.useBootLoader
+          # see note [Disk layout with `useBootLoader`]
+          { device = "${lookupDriveDeviceName "boot" cfg.qemu.drives}2"; # 2 for e.g. `vdb2`, as created in `bootDisk`
+            fsType = "vfat";
+            noCheck = true; # fsck fails on a r/o filesystem
+          };
+      } // lib.mapAttrs' mkSharedDir cfg.sharedDirectories);
+
+    swapDevices = mkVMOverride [ ];
+    boot.initrd.luks.devices = mkVMOverride {};
+
+    # Don't run ntpd in the guest.  It should get the correct time from KVM.
+    services.timesyncd.enable = false;
+
+    services.qemuGuest.enable = cfg.qemu.guestAgent.enable;
+
+    system.build.vm = pkgs.runCommand "nixos-vm" { preferLocalBuild = true; }
+      ''
+        mkdir -p $out/bin
+        ln -s ${config.system.build.toplevel} $out/system
+        ln -s ${pkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${config.system.name}-vm
+      '';
+
+    # When building a regular system configuration, override whatever
+    # video driver the host uses.
+    services.xserver.videoDrivers = mkVMOverride [ "modesetting" ];
+    services.xserver.defaultDepth = mkVMOverride 0;
+    services.xserver.resolutions = mkVMOverride [ cfg.resolution ];
+    services.xserver.monitorSection =
+      ''
+        # Set a higher refresh rate so that resolutions > 800x600 work.
+        HorizSync 30-140
+        VertRefresh 50-160
+      '';
+
+    # Wireless won't work in the VM.
+    networking.wireless.enable = mkVMOverride false;
+    services.connman.enable = mkVMOverride false;
+
+    # Speed up booting by not waiting for ARP.
+    networking.dhcpcd.extraConfig = "noarp";
+
+    networking.usePredictableInterfaceNames = false;
+
+    system.requiredKernelConfig = with config.lib.kernelConfig;
+      [ (isEnabled "VIRTIO_BLK")
+        (isEnabled "VIRTIO_PCI")
+        (isEnabled "VIRTIO_NET")
+        (isEnabled "EXT4_FS")
+        (isEnabled "NET_9P_VIRTIO")
+        (isEnabled "9P_FS")
+        (isYes "BLK_DEV")
+        (isYes "PCI")
+        (isYes "NETDEVICES")
+        (isYes "NET_CORE")
+        (isYes "INET")
+        (isYes "NETWORK_FILESYSTEMS")
+      ] ++ optionals (!cfg.graphics) [
+        (isYes "SERIAL_8250_CONSOLE")
+        (isYes "SERIAL_8250")
+      ] ++ optionals (cfg.writableStore) [
+        (isEnabled "OVERLAY_FS")
+      ];
+
+  };
+
+  # uses types of services/x11/xserver.nix
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/virtualisation/railcar.nix b/nixos/modules/virtualisation/railcar.nix
new file mode 100644
index 00000000000..e719e25650d
--- /dev/null
+++ b/nixos/modules/virtualisation/railcar.nix
@@ -0,0 +1,124 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.railcar;
+  generateUnit = name: containerConfig:
+    let
+      container = pkgs.ociTools.buildContainer {
+        args = [
+          (pkgs.writeShellScript "run.sh" containerConfig.cmd).outPath
+        ];
+      };
+    in
+      nameValuePair "railcar-${name}" {
+        enable = true;
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+            ExecStart = ''
+              ${cfg.package}/bin/railcar -r ${cfg.stateDir} run ${name} -b ${container}
+            '';
+            Type = containerConfig.runType;
+          };
+      };
+  mount = with types; (submodule {
+    options = {
+      type = mkOption {
+        type = str;
+        default = "none";
+        description = ''
+          The type of the filesystem to be mounted.
+          Linux: filesystem types supported by the kernel as listed in
+          `/proc/filesystems` (e.g., "minix", "ext2", "ext3", "jfs", "xfs",
+          "reiserfs", "msdos", "proc", "nfs", "iso9660"). For bind mounts
+          (when options include either bind or rbind), the type is a dummy,
+          often "none" (not listed in /proc/filesystems).
+        '';
+      };
+      source = mkOption {
+        type = str;
+        description = "Source for the in-container mount";
+      };
+      options = mkOption {
+        type = listOf str;
+        default = [ "bind" ];
+        description = ''
+          Mount options of the filesystem to be used.
+
+          Support options are listed in the mount(8) man page. Note that
+          both filesystem-independent and filesystem-specific options
+          are listed.
+        '';
+      };
+    };
+  });
+in
+{
+  options.services.railcar = {
+    enable = mkEnableOption "railcar";
+
+    containers = mkOption {
+      default = {};
+      description = "Declarative container configuration";
+      type = with types; attrsOf (submodule ({ name, config, ... }: {
+        options = {
+          cmd = mkOption {
+            type = types.lines;
+            description = "Command or script to run inside the container";
+          };
+
+          mounts = mkOption {
+            type = with types; attrsOf mount;
+            default = {};
+            description = ''
+              A set of mounts inside the container.
+
+              The defaults have been chosen for simple bindmounts, meaning
+              that you only need to provide the "source" parameter.
+            '';
+            example = { "/data" = { source = "/var/lib/data"; }; };
+          };
+
+          runType = mkOption {
+            type = types.str;
+            default = "oneshot";
+            description = "The systemd service run type";
+          };
+
+          os = mkOption {
+            type = types.str;
+            default = "linux";
+            description = "OS type of the container";
+          };
+
+          arch = mkOption {
+            type = types.str;
+            default = "x86_64";
+            description = "Computer architecture type of the container";
+          };
+        };
+      }));
+    };
+
+    stateDir = mkOption {
+      type = types.path;
+      default = "/var/railcar";
+      description = "Railcar persistent state directory";
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.railcar;
+      defaultText = literalExpression "pkgs.railcar";
+      description = "Railcar package to use";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services = flip mapAttrs' cfg.containers (name: containerConfig:
+      generateUnit name containerConfig
+    );
+  };
+}
+
diff --git a/nixos/modules/virtualisation/spice-usb-redirection.nix b/nixos/modules/virtualisation/spice-usb-redirection.nix
new file mode 100644
index 00000000000..255327f2622
--- /dev/null
+++ b/nixos/modules/virtualisation/spice-usb-redirection.nix
@@ -0,0 +1,26 @@
+{ config, pkgs, lib, ... }:
+{
+  options.virtualisation.spiceUSBRedirection.enable = lib.mkOption {
+    type = lib.types.bool;
+    default = false;
+    description = ''
+      Install the SPICE USB redirection helper with setuid
+      privileges. This allows unprivileged users to pass USB devices
+      connected to this machine to libvirt VMs, both local and
+      remote. Note that this allows users arbitrary access to USB
+      devices.
+    '';
+  };
+
+  config = lib.mkIf config.virtualisation.spiceUSBRedirection.enable {
+    environment.systemPackages = [ pkgs.spice-gtk ]; # For polkit actions
+    security.wrappers.spice-client-glib-usb-acl-helper = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_fowner+ep";
+      source = "${pkgs.spice-gtk}/bin/spice-client-glib-usb-acl-helper";
+    };
+  };
+
+  meta.maintainers = [ lib.maintainers.lheckemann ];
+}
diff --git a/nixos/modules/virtualisation/vagrant-guest.nix b/nixos/modules/virtualisation/vagrant-guest.nix
new file mode 100644
index 00000000000..263b1ebca08
--- /dev/null
+++ b/nixos/modules/virtualisation/vagrant-guest.nix
@@ -0,0 +1,58 @@
+# Minimal configuration that vagrant depends on
+
+{ config, pkgs, ... }:
+let
+  # Vagrant uses an insecure shared private key by default, but we
+  # don't use the authorizedKeys attribute under users because it should be
+  # removed on first boot and replaced with a random one. This script sets
+  # the correct permissions and installs the temporary key if no
+  # ~/.ssh/authorized_keys exists.
+  install-vagrant-ssh-key = pkgs.writeScriptBin "install-vagrant-ssh-key" ''
+    #!${pkgs.runtimeShell}
+    if [ ! -e ~/.ssh/authorized_keys ]; then
+      mkdir -m 0700 -p ~/.ssh
+      echo "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key" >> ~/.ssh/authorized_keys
+      chmod 0600 ~/.ssh/authorized_keys
+    fi
+  '';
+in
+{
+  # Enable the OpenSSH daemon.
+  services.openssh.enable = true;
+
+  # Packages used by Vagrant
+  environment.systemPackages = with pkgs; [
+    findutils
+    iputils
+    nettools
+    netcat
+    nfs-utils
+    rsync
+  ];
+
+  users.extraUsers.vagrant = {
+    isNormalUser    = true;
+    createHome      = true;
+    description     = "Vagrant user account";
+    extraGroups     = [ "users" "wheel" ];
+    home            = "/home/vagrant";
+    password        = "vagrant";
+    useDefaultShell = true;
+    uid             = 1000;
+  };
+
+  systemd.services.install-vagrant-ssh-key = {
+    description = "Vagrant SSH key install (if needed)";
+    after = [ "fs.target" ];
+    wants = [ "fs.target" ];
+    wantedBy = [ "multi-user.target" ];
+    serviceConfig = {
+      ExecStart = "${install-vagrant-ssh-key}/bin/install-vagrant-ssh-key";
+      User = "vagrant";
+      # So it won't be (needlessly) restarted:
+      RemainAfterExit = true;
+    };
+  };
+
+  security.sudo.wheelNeedsPassword = false;
+}
diff --git a/nixos/modules/virtualisation/vagrant-virtualbox-image.nix b/nixos/modules/virtualisation/vagrant-virtualbox-image.nix
new file mode 100644
index 00000000000..2a921894ab6
--- /dev/null
+++ b/nixos/modules/virtualisation/vagrant-virtualbox-image.nix
@@ -0,0 +1,60 @@
+# Vagrant + VirtualBox
+
+{ config, pkgs, ... }:
+
+{
+  imports = [
+    ./vagrant-guest.nix
+    ./virtualbox-image.nix
+  ];
+
+  virtualbox.params = {
+    audio = "none";
+    audioin = "off";
+    audioout = "off";
+    usb = "off";
+    usbehci = "off";
+  };
+  sound.enable = false;
+  documentation.man.enable = false;
+  documentation.nixos.enable = false;
+
+  users.extraUsers.vagrant.extraGroups = [ "vboxsf" ];
+
+  # generate the box v1 format which is much easier to generate
+  # https://www.vagrantup.com/docs/boxes/format.html
+  system.build.vagrantVirtualbox = pkgs.runCommand
+    "virtualbox-vagrant.box"
+    {}
+    ''
+      mkdir workdir
+      cd workdir
+
+      # 1. create that metadata.json file
+      echo '{"provider":"virtualbox"}' > metadata.json
+
+      # 2. create a default Vagrantfile config
+      cat <<VAGRANTFILE > Vagrantfile
+      Vagrant.configure("2") do |config|
+        config.vm.base_mac = "0800275F0936"
+      end
+      VAGRANTFILE
+
+      # 3. add the exported VM files
+      tar xvf ${config.system.build.virtualBoxOVA}/*.ova
+
+      # 4. move the ovf to the fixed location
+      mv *.ovf box.ovf
+
+      # 5. generate OVF manifest file
+      rm *.mf
+      touch box.mf
+      for fname in *; do
+        checksum=$(sha256sum $fname | cut -d' ' -f 1)
+        echo "SHA256($fname)= $checksum" >> box.mf
+      done
+
+      # 6. compress everything back together
+      tar --owner=0 --group=0 --sort=name --numeric-owner -czf $out .
+    '';
+}
diff --git a/nixos/modules/virtualisation/virtualbox-guest.nix b/nixos/modules/virtualisation/virtualbox-guest.nix
new file mode 100644
index 00000000000..7b55b3b9759
--- /dev/null
+++ b/nixos/modules/virtualisation/virtualbox-guest.nix
@@ -0,0 +1,93 @@
+# Module for VirtualBox guests.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.virtualisation.virtualbox.guest;
+  kernel = config.boot.kernelPackages;
+
+in
+
+{
+
+  ###### interface
+
+  options.virtualisation.virtualbox.guest = {
+    enable = mkOption {
+      default = false;
+      type = types.bool;
+      description = "Whether to enable the VirtualBox service and other guest additions.";
+    };
+
+    x11 = mkOption {
+      default = true;
+      type = types.bool;
+      description = "Whether to enable x11 graphics";
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable (mkMerge [{
+    assertions = [{
+      assertion = pkgs.stdenv.hostPlatform.isx86;
+      message = "Virtualbox not currently supported on ${pkgs.stdenv.hostPlatform.system}";
+    }];
+
+    environment.systemPackages = [ kernel.virtualboxGuestAdditions ];
+
+    boot.extraModulePackages = [ kernel.virtualboxGuestAdditions ];
+
+    boot.supportedFilesystems = [ "vboxsf" ];
+    boot.initrd.supportedFilesystems = [ "vboxsf" ];
+
+    users.groups.vboxsf.gid = config.ids.gids.vboxsf;
+
+    systemd.services.virtualbox =
+      { description = "VirtualBox Guest Services";
+
+        wantedBy = [ "multi-user.target" ];
+        requires = [ "dev-vboxguest.device" ];
+        after = [ "dev-vboxguest.device" ];
+
+        unitConfig.ConditionVirtualization = "oracle";
+
+        serviceConfig.ExecStart = "@${kernel.virtualboxGuestAdditions}/bin/VBoxService VBoxService --foreground";
+      };
+
+    services.udev.extraRules =
+      ''
+        # /dev/vboxuser is necessary for VBoxClient to work.  Maybe we
+        # should restrict this to logged-in users.
+        KERNEL=="vboxuser",  OWNER="root", GROUP="root", MODE="0666"
+
+        # Allow systemd dependencies on vboxguest.
+        SUBSYSTEM=="misc", KERNEL=="vboxguest", TAG+="systemd"
+      '';
+  } (mkIf cfg.x11 {
+    services.xserver.videoDrivers = [ "vmware" "virtualbox" "modesetting" ];
+
+    services.xserver.config =
+      ''
+        Section "InputDevice"
+          Identifier "VBoxMouse"
+          Driver "vboxmouse"
+        EndSection
+      '';
+
+    services.xserver.serverLayoutSection =
+      ''
+        InputDevice "VBoxMouse"
+      '';
+
+    services.xserver.displayManager.sessionCommands =
+      ''
+        PATH=${makeBinPath [ pkgs.gnugrep pkgs.which pkgs.xorg.xorgserver.out ]}:$PATH \
+          ${kernel.virtualboxGuestAdditions}/bin/VBoxClient-all
+      '';
+  })]);
+
+}
diff --git a/nixos/modules/virtualisation/virtualbox-host.nix b/nixos/modules/virtualisation/virtualbox-host.nix
new file mode 100644
index 00000000000..2acf54aae2e
--- /dev/null
+++ b/nixos/modules/virtualisation/virtualbox-host.nix
@@ -0,0 +1,168 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.virtualisation.virtualbox.host;
+
+  virtualbox = cfg.package.override {
+    inherit (cfg) enableHardening headless enableWebService;
+    extensionPack = if cfg.enableExtensionPack then pkgs.virtualboxExtpack else null;
+  };
+
+  kernelModules = config.boot.kernelPackages.virtualbox.override {
+    inherit virtualbox;
+  };
+
+in
+
+{
+  options.virtualisation.virtualbox.host = {
+    enable = mkEnableOption "VirtualBox" // {
+      description = ''
+        Whether to enable VirtualBox.
+
+        <note><para>
+          In order to pass USB devices from the host to the guests, the user
+          needs to be in the <literal>vboxusers</literal> group.
+        </para></note>
+      '';
+    };
+
+    enableExtensionPack = mkEnableOption "VirtualBox extension pack" // {
+      description = ''
+        Whether to install the Oracle Extension Pack for VirtualBox.
+
+        <important><para>
+          You must set <literal>nixpkgs.config.allowUnfree = true</literal> in
+          order to use this.  This requires you accept the VirtualBox PUEL.
+        </para></important>
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.virtualbox;
+      defaultText = literalExpression "pkgs.virtualbox";
+      description = ''
+        Which VirtualBox package to use.
+      '';
+    };
+
+    addNetworkInterface = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Automatically set up a vboxnet0 host-only network interface.
+      '';
+    };
+
+    enableHardening = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Enable hardened VirtualBox, which ensures that only the binaries in the
+        system path get access to the devices exposed by the kernel modules
+        instead of all users in the vboxusers group.
+
+        <important><para>
+          Disabling this can put your system's security at risk, as local users
+          in the vboxusers group can tamper with the VirtualBox device files.
+        </para></important>
+      '';
+    };
+
+    headless = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Use VirtualBox installation without GUI and Qt dependency. Useful to enable on servers
+        and when virtual machines are controlled only via SSH.
+      '';
+    };
+
+    enableWebService = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Build VirtualBox web service tool (vboxwebsrv) to allow managing VMs via other webpage frontend tools. Useful for headless servers.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable (mkMerge [{
+    warnings = mkIf (config.nixpkgs.config.virtualbox.enableExtensionPack or false)
+      ["'nixpkgs.virtualbox.enableExtensionPack' has no effect, please use 'virtualisation.virtualbox.host.enableExtensionPack'"];
+    boot.kernelModules = [ "vboxdrv" "vboxnetadp" "vboxnetflt" ];
+    boot.extraModulePackages = [ kernelModules ];
+    environment.systemPackages = [ virtualbox ];
+
+    security.wrappers = let
+      mkSuid = program: {
+        source = "${virtualbox}/libexec/virtualbox/${program}";
+        owner = "root";
+        group = "vboxusers";
+        setuid = true;
+      };
+    in mkIf cfg.enableHardening
+      (builtins.listToAttrs (map (x: { name = x; value = mkSuid x; }) [
+      "VBoxHeadless"
+      "VBoxNetAdpCtl"
+      "VBoxNetDHCP"
+      "VBoxNetNAT"
+      "VBoxSDL"
+      "VBoxVolInfo"
+      "VirtualBoxVM"
+    ]));
+
+    users.groups.vboxusers.gid = config.ids.gids.vboxusers;
+
+    services.udev.extraRules =
+      ''
+        KERNEL=="vboxdrv",    OWNER="root", GROUP="vboxusers", MODE="0660", TAG+="systemd"
+        KERNEL=="vboxdrvu",   OWNER="root", GROUP="root",      MODE="0666", TAG+="systemd"
+        KERNEL=="vboxnetctl", OWNER="root", GROUP="vboxusers", MODE="0660", TAG+="systemd"
+        SUBSYSTEM=="usb_device", ACTION=="add", RUN+="${virtualbox}/libexec/virtualbox/VBoxCreateUSBNode.sh $major $minor $attr{bDeviceClass}"
+        SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", RUN+="${virtualbox}/libexec/virtualbox/VBoxCreateUSBNode.sh $major $minor $attr{bDeviceClass}"
+        SUBSYSTEM=="usb_device", ACTION=="remove", RUN+="${virtualbox}/libexec/virtualbox/VBoxCreateUSBNode.sh --remove $major $minor"
+        SUBSYSTEM=="usb", ACTION=="remove", ENV{DEVTYPE}=="usb_device", RUN+="${virtualbox}/libexec/virtualbox/VBoxCreateUSBNode.sh --remove $major $minor"
+      '';
+
+    # Since we lack the right setuid/setcap binaries, set up a host-only network by default.
+  } (mkIf cfg.addNetworkInterface {
+    systemd.services.vboxnet0 =
+      { description = "VirtualBox vboxnet0 Interface";
+        requires = [ "dev-vboxnetctl.device" ];
+        after = [ "dev-vboxnetctl.device" ];
+        wantedBy = [ "network.target" "sys-subsystem-net-devices-vboxnet0.device" ];
+        path = [ virtualbox ];
+        serviceConfig.RemainAfterExit = true;
+        serviceConfig.Type = "oneshot";
+        serviceConfig.PrivateTmp = true;
+        environment.VBOX_USER_HOME = "/tmp";
+        script =
+          ''
+            if ! [ -e /sys/class/net/vboxnet0 ]; then
+              VBoxManage hostonlyif create
+              cat /tmp/VBoxSVC.log >&2
+            fi
+          '';
+        postStop =
+          ''
+            VBoxManage hostonlyif remove vboxnet0
+          '';
+      };
+
+    networking.interfaces.vboxnet0.ipv4.addresses = [{ address = "192.168.56.1"; prefixLength = 24; }];
+    # Make sure NetworkManager won't assume this interface being up
+    # means we have internet access.
+    networking.networkmanager.unmanaged = ["vboxnet0"];
+  }) (mkIf config.networking.useNetworkd {
+    systemd.network.networks."40-vboxnet0".extraConfig = ''
+      [Link]
+      RequiredForOnline=no
+    '';
+  })
+
+]);
+}
diff --git a/nixos/modules/virtualisation/virtualbox-image.nix b/nixos/modules/virtualisation/virtualbox-image.nix
new file mode 100644
index 00000000000..1a0c4df42cb
--- /dev/null
+++ b/nixos/modules/virtualisation/virtualbox-image.nix
@@ -0,0 +1,215 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.virtualbox;
+
+in {
+
+  options = {
+    virtualbox = {
+      baseImageSize = mkOption {
+        type = with types; either (enum [ "auto" ]) int;
+        default = "auto";
+        example = 50 * 1024;
+        description = ''
+          The size of the VirtualBox base image in MiB.
+        '';
+      };
+      baseImageFreeSpace = mkOption {
+        type = with types; int;
+        default = 30 * 1024;
+        description = ''
+          Free space in the VirtualBox base image in MiB.
+        '';
+      };
+      memorySize = mkOption {
+        type = types.int;
+        default = 1536;
+        description = ''
+          The amount of RAM the VirtualBox appliance can use in MiB.
+        '';
+      };
+      vmDerivationName = mkOption {
+        type = types.str;
+        default = "nixos-ova-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}";
+        description = ''
+          The name of the derivation for the VirtualBox appliance.
+        '';
+      };
+      vmName = mkOption {
+        type = types.str;
+        default = "NixOS ${config.system.nixos.label} (${pkgs.stdenv.hostPlatform.system})";
+        description = ''
+          The name of the VirtualBox appliance.
+        '';
+      };
+      vmFileName = mkOption {
+        type = types.str;
+        default = "nixos-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.ova";
+        description = ''
+          The file name of the VirtualBox appliance.
+        '';
+      };
+      params = mkOption {
+        type = with types; attrsOf (oneOf [ str int bool (listOf str) ]);
+        example = {
+          audio = "alsa";
+          rtcuseutc = "on";
+          usb = "off";
+        };
+        description = ''
+          Parameters passed to the Virtualbox appliance.
+
+          Run <literal>VBoxManage modifyvm --help</literal> to see more options.
+        '';
+      };
+      exportParams = mkOption {
+        type = with types; listOf (oneOf [ str int bool (listOf str) ]);
+        example = [
+          "--vsys" "0" "--vendor" "ACME Inc."
+        ];
+        default = [];
+        description = ''
+          Parameters passed to the Virtualbox export command.
+
+          Run <literal>VBoxManage export --help</literal> to see more options.
+        '';
+      };
+      extraDisk = mkOption {
+        description = ''
+          Optional extra disk/hdd configuration.
+          The disk will be an 'ext4' partition on a separate VMDK file.
+        '';
+        default = null;
+        example = {
+          label = "storage";
+          mountPoint = "/home/demo/storage";
+          size = 100 * 1024;
+        };
+        type = types.nullOr (types.submodule {
+          options = {
+            size = mkOption {
+              type = types.int;
+              description = "Size in MiB";
+            };
+            label = mkOption {
+              type = types.str;
+              default = "vm-extra-storage";
+              description = "Label for the disk partition";
+            };
+            mountPoint = mkOption {
+              type = types.str;
+              description = "Path where to mount this disk.";
+            };
+          };
+        });
+      };
+    };
+  };
+
+  config = {
+
+    virtualbox.params = mkMerge [
+      (mapAttrs (name: mkDefault) {
+        acpi = "on";
+        vram = 32;
+        nictype1 = "virtio";
+        nic1 = "nat";
+        audiocontroller = "ac97";
+        audio = "alsa";
+        audioout = "on";
+        graphicscontroller = "vmsvga";
+        rtcuseutc = "on";
+        usb = "on";
+        usbehci = "on";
+        mouse = "usbtablet";
+      })
+      (mkIf (pkgs.stdenv.hostPlatform.system == "i686-linux") { pae = "on"; })
+    ];
+
+    system.build.virtualBoxOVA = import ../../lib/make-disk-image.nix {
+      name = cfg.vmDerivationName;
+
+      inherit pkgs lib config;
+      partitionTableType = "legacy";
+      diskSize = cfg.baseImageSize;
+      additionalSpace = "${toString cfg.baseImageFreeSpace}M";
+
+      postVM =
+        ''
+          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
+
+          ${optionalString (cfg.extraDisk != null) ''
+            echo "creating extra disk: data-disk.raw"
+            dataDiskImage=data-disk.raw
+            truncate -s ${toString cfg.extraDisk.size}M $dataDiskImage
+
+            parted --script $dataDiskImage -- \
+              mklabel msdos \
+              mkpart primary ext4 1MiB -1
+            eval $(partx $dataDiskImage -o START,SECTORS --nr 1 --pairs)
+            mkfs.ext4 -F -L ${cfg.extraDisk.label} $dataDiskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
+            echo "creating extra disk: data-disk.vmdk"
+            VBoxManage internalcommands createrawvmdk -filename data-disk.vmdk -rawdisk $dataDiskImage
+          ''}
+
+          echo "creating VirtualBox VM..."
+          vmName="${cfg.vmName}";
+          VBoxManage createvm --name "$vmName" --register \
+            --ostype ${if pkgs.stdenv.hostPlatform.system == "x86_64-linux" then "Linux26_64" else "Linux26"}
+          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
+          ${optionalString (cfg.extraDisk != null) ''
+            VBoxManage storageattach "$vmName" --storagectl SATA --port 1 --device 0 --type hdd \
+            --medium data-disk.vmdk
+          ''}
+
+          echo "exporting VirtualBox VM..."
+          mkdir -p $out
+          fn="$out/${cfg.vmFileName}"
+          VBoxManage export "$vmName" --output "$fn" --options manifest ${escapeShellArgs cfg.exportParams}
+
+          rm -v $diskImage
+
+          mkdir -p $out/nix-support
+          echo "file ova $fn" >> $out/nix-support/hydra-build-products
+        '';
+    };
+
+    fileSystems = {
+      "/" = {
+        device = "/dev/disk/by-label/nixos";
+        autoResize = true;
+        fsType = "ext4";
+      };
+    } // (lib.optionalAttrs (cfg.extraDisk != null) {
+      ${cfg.extraDisk.mountPoint} = {
+        device = "/dev/disk/by-label/" + cfg.extraDisk.label;
+        autoResize = true;
+        fsType = "ext4";
+      };
+    });
+
+    boot.growPartition = true;
+    boot.loader.grub.device = "/dev/sda";
+
+    swapDevices = [{
+      device = "/var/swap";
+      size = 2048;
+    }];
+
+    virtualisation.virtualbox.guest.enable = true;
+
+  };
+}
diff --git a/nixos/modules/virtualisation/vmware-guest.nix b/nixos/modules/virtualisation/vmware-guest.nix
new file mode 100644
index 00000000000..3caed746ca9
--- /dev/null
+++ b/nixos/modules/virtualisation/vmware-guest.nix
@@ -0,0 +1,86 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.virtualisation.vmware.guest;
+  open-vm-tools = if cfg.headless then pkgs.open-vm-tools-headless else pkgs.open-vm-tools;
+  xf86inputvmmouse = pkgs.xorg.xf86inputvmmouse;
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "vmwareGuest" ] [ "virtualisation" "vmware" "guest" ])
+  ];
+
+  options.virtualisation.vmware.guest = {
+    enable = mkEnableOption "VMWare Guest Support";
+    headless = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Whether to disable X11-related features.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [ {
+      assertion = pkgs.stdenv.hostPlatform.isx86;
+      message = "VMWare guest is not currently supported on ${pkgs.stdenv.hostPlatform.system}";
+    } ];
+
+    boot.initrd.availableKernelModules = [ "mptspi" ];
+    boot.initrd.kernelModules = [ "vmw_pvscsi" ];
+
+    environment.systemPackages = [ open-vm-tools ];
+
+    systemd.services.vmware =
+      { description = "VMWare Guest Service";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "display-manager.service" ];
+        unitConfig.ConditionVirtualization = "vmware";
+        serviceConfig.ExecStart = "${open-vm-tools}/bin/vmtoolsd";
+      };
+
+    # Mount the vmblock for drag-and-drop and copy-and-paste.
+    systemd.mounts = mkIf (!cfg.headless) [
+      {
+        description = "VMware vmblock fuse mount";
+        documentation = [ "https://github.com/vmware/open-vm-tools/blob/master/open-vm-tools/vmblock-fuse/design.txt" ];
+        unitConfig.ConditionVirtualization = "vmware";
+        what = "${open-vm-tools}/bin/vmware-vmblock-fuse";
+        where = "/run/vmblock-fuse";
+        type = "fuse";
+        options = "subtype=vmware-vmblock,default_permissions,allow_other";
+        wantedBy = [ "multi-user.target" ];
+      }
+    ];
+
+    security.wrappers.vmware-user-suid-wrapper = mkIf (!cfg.headless) {
+        setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${open-vm-tools}/bin/vmware-user-suid-wrapper";
+      };
+
+    environment.etc.vmware-tools.source = "${open-vm-tools}/etc/vmware-tools/*";
+
+    services.xserver = mkIf (!cfg.headless) {
+      videoDrivers = mkOverride 50 [ "vmware" ];
+      modules = [ xf86inputvmmouse ];
+
+      config = ''
+          Section "InputClass"
+            Identifier "VMMouse"
+            MatchDevicePath "/dev/input/event*"
+            MatchProduct "ImPS/2 Generic Wheel Mouse"
+            Driver "vmmouse"
+          EndSection
+        '';
+
+      displayManager.sessionCommands = ''
+          ${open-vm-tools}/bin/vmware-user-suid-wrapper
+        '';
+    };
+
+    services.udev.packages = [ open-vm-tools ];
+  };
+}
diff --git a/nixos/modules/virtualisation/vmware-image.nix b/nixos/modules/virtualisation/vmware-image.nix
new file mode 100644
index 00000000000..f6cd12e2bb7
--- /dev/null
+++ b/nixos/modules/virtualisation/vmware-image.nix
@@ -0,0 +1,91 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  boolToStr = value: if value then "on" else "off";
+  cfg = config.vmware;
+
+  subformats = [
+    "monolithicSparse"
+    "monolithicFlat"
+    "twoGbMaxExtentSparse"
+    "twoGbMaxExtentFlat"
+    "streamOptimized"
+  ];
+
+in {
+  options = {
+    vmware = {
+      baseImageSize = mkOption {
+        type = with types; either (enum [ "auto" ]) int;
+        default = "auto";
+        example = 2048;
+        description = ''
+          The size of the VMWare base image in MiB.
+        '';
+      };
+      vmDerivationName = mkOption {
+        type = types.str;
+        default = "nixos-vmware-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}";
+        description = ''
+          The name of the derivation for the VMWare appliance.
+        '';
+      };
+      vmFileName = mkOption {
+        type = types.str;
+        default = "nixos-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.vmdk";
+        description = ''
+          The file name of the VMWare appliance.
+        '';
+      };
+      vmSubformat = mkOption {
+        type = types.enum subformats;
+        default = "monolithicSparse";
+        description = "Specifies which VMDK subformat to use.";
+      };
+      vmCompat6 = mkOption {
+        type = types.bool;
+        default = false;
+        example = true;
+        description = "Create a VMDK version 6 image (instead of version 4).";
+      };
+    };
+  };
+
+  config = {
+    system.build.vmwareImage = import ../../lib/make-disk-image.nix {
+      name = cfg.vmDerivationName;
+      postVM = ''
+        ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -o compat6=${boolToStr cfg.vmCompat6},subformat=${cfg.vmSubformat} -O vmdk $diskImage $out/${cfg.vmFileName}
+        rm $diskImage
+      '';
+      format = "raw";
+      diskSize = cfg.baseImageSize;
+      partitionTableType = "efi";
+      inherit config lib pkgs;
+    };
+
+    fileSystems."/" = {
+      device = "/dev/disk/by-label/nixos";
+      autoResize = true;
+      fsType = "ext4";
+    };
+
+    fileSystems."/boot" = {
+      device = "/dev/disk/by-label/ESP";
+      fsType = "vfat";
+    };
+
+    boot.growPartition = true;
+
+    boot.loader.grub = {
+      version = 2;
+      device = "nodev";
+      efiSupport = true;
+      efiInstallAsRemovable = true;
+    };
+
+    virtualisation.vmware.guest.enable = true;
+  };
+}
diff --git a/nixos/modules/virtualisation/waydroid.nix b/nixos/modules/virtualisation/waydroid.nix
new file mode 100644
index 00000000000..4fc798ff39f
--- /dev/null
+++ b/nixos/modules/virtualisation/waydroid.nix
@@ -0,0 +1,73 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.virtualisation.waydroid;
+  kernelPackages = config.boot.kernelPackages;
+  waydroidGbinderConf = pkgs.writeText "waydroid.conf" ''
+    [Protocol]
+    /dev/binder = aidl2
+    /dev/vndbinder = aidl2
+    /dev/hwbinder = hidl
+
+    [ServiceManager]
+    /dev/binder = aidl2
+    /dev/vndbinder = aidl2
+    /dev/hwbinder = hidl
+  '';
+
+in
+{
+
+  options.virtualisation.waydroid = {
+    enable = mkEnableOption "Waydroid";
+  };
+
+  config = mkIf cfg.enable {
+    assertions = singleton {
+      assertion = versionAtLeast (getVersion config.boot.kernelPackages.kernel) "4.18";
+      message = "Waydroid needs user namespace support to work properly";
+    };
+
+    system.requiredKernelConfig = with config.lib.kernelConfig; [
+      (isEnabled "ANDROID_BINDER_IPC")
+      (isEnabled "ANDROID_BINDERFS")
+      (isEnabled "ASHMEM")
+    ];
+
+    /* NOTE: we always enable this flag even if CONFIG_PSI_DEFAULT_DISABLED is not on
+      as reading the kernel config is not always possible and on kernels where it's
+      already on it will be no-op
+    */
+    boot.kernelParams = [ "psi=1" ];
+
+    environment.etc."gbinder.d/waydroid.conf".source = waydroidGbinderConf;
+
+    environment.systemPackages = with pkgs; [ waydroid ];
+
+    networking.firewall.trustedInterfaces = [ "waydroid0" ];
+
+    virtualisation.lxc.enable = true;
+
+    systemd.services.waydroid-container = {
+      description = "Waydroid Container";
+
+      wantedBy = [ "multi-user.target" ];
+
+      path = with pkgs; [ getent iptables iproute kmod nftables util-linux which ];
+
+      unitConfig = {
+        ConditionPathExists = "/var/lib/waydroid/lxc/waydroid";
+      };
+
+      serviceConfig = {
+        ExecStart = "${pkgs.waydroid}/bin/waydroid container start";
+        ExecStop = "${pkgs.waydroid}/bin/waydroid container stop";
+        ExecStopPost = "${pkgs.waydroid}/bin/waydroid session stop";
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/virtualisation/xe-guest-utilities.nix b/nixos/modules/virtualisation/xe-guest-utilities.nix
new file mode 100644
index 00000000000..25ccbaebc07
--- /dev/null
+++ b/nixos/modules/virtualisation/xe-guest-utilities.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.xe-guest-utilities;
+in {
+  options = {
+    services.xe-guest-utilities = {
+      enable = mkEnableOption "the Xen guest utilities daemon";
+    };
+  };
+  config = mkIf cfg.enable {
+    services.udev.packages = [ pkgs.xe-guest-utilities ];
+    systemd.tmpfiles.rules = [ "d /run/xenstored 0755 - - -" ];
+
+    systemd.services.xe-daemon = {
+      description = "xen daemon file";
+      wantedBy    = [ "multi-user.target" ];
+      after = [ "xe-linux-distribution.service" ];
+      requires = [ "proc-xen.mount" ];
+      path = [ pkgs.coreutils pkgs.iproute2 ];
+      serviceConfig = {
+        PIDFile = "/run/xe-daemon.pid";
+        ExecStart = "${pkgs.xe-guest-utilities}/bin/xe-daemon -p /run/xe-daemon.pid";
+        ExecStop = "${pkgs.procps}/bin/pkill -TERM -F /run/xe-daemon.pid";
+      };
+    };
+
+    systemd.services.xe-linux-distribution = {
+      description = "xen linux distribution service";
+      wantedBy    = [ "multi-user.target" ];
+      before = [ "xend.service" ];
+      path = [ pkgs.xe-guest-utilities pkgs.coreutils pkgs.gawk pkgs.gnused ];
+      serviceConfig = {
+        Type = "simple";
+        RemainAfterExit = "yes";
+        ExecStart = "${pkgs.xe-guest-utilities}/bin/xe-linux-distribution /var/cache/xe-linux-distribution";
+      };
+    };
+
+    systemd.mounts = [
+      { description = "Mount /proc/xen files";
+        what = "xenfs";
+        where = "/proc/xen";
+        type = "xenfs";
+        unitConfig = {
+          ConditionPathExists = "/proc/xen";
+          RefuseManualStop = "true";
+        };
+      }
+    ];
+  };
+}
diff --git a/nixos/modules/virtualisation/xen-dom0.nix b/nixos/modules/virtualisation/xen-dom0.nix
new file mode 100644
index 00000000000..975eed10cd2
--- /dev/null
+++ b/nixos/modules/virtualisation/xen-dom0.nix
@@ -0,0 +1,453 @@
+# Xen hypervisor (Dom0) support.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.virtualisation.xen;
+in
+
+{
+  imports = [
+    (mkRemovedOptionModule [ "virtualisation" "xen" "qemu" ] "You don't need this option anymore, it will work without it.")
+    (mkRenamedOptionModule [ "virtualisation" "xen" "qemu-package" ] [ "virtualisation" "xen" "package-qemu" ])
+  ];
+
+  ###### interface
+
+  options = {
+
+    virtualisation.xen.enable =
+      mkOption {
+        default = false;
+        type = types.bool;
+        description =
+          ''
+            Setting this option enables the Xen hypervisor, a
+            virtualisation technology that allows multiple virtual
+            machines, known as <emphasis>domains</emphasis>, to run
+            concurrently on the physical machine.  NixOS runs as the
+            privileged <emphasis>Domain 0</emphasis>.  This option
+            requires a reboot to take effect.
+          '';
+      };
+
+    virtualisation.xen.package = mkOption {
+      type = types.package;
+      defaultText = literalExpression "pkgs.xen";
+      example = literalExpression "pkgs.xen-light";
+      description = ''
+        The package used for Xen binary.
+      '';
+      relatedPackages = [ "xen" "xen-light" ];
+    };
+
+    virtualisation.xen.package-qemu = mkOption {
+      type = types.package;
+      defaultText = literalExpression "pkgs.xen";
+      example = literalExpression "pkgs.qemu_xen-light";
+      description = ''
+        The package with qemu binaries for dom0 qemu and xendomains.
+      '';
+      relatedPackages = [ "xen"
+                          { name = "qemu_xen-light"; comment = "For use with pkgs.xen-light."; }
+                        ];
+    };
+
+    virtualisation.xen.bootParams =
+      mkOption {
+        default = [];
+        type = types.listOf types.str;
+        description =
+          ''
+            Parameters passed to the Xen hypervisor at boot time.
+          '';
+      };
+
+    virtualisation.xen.domain0MemorySize =
+      mkOption {
+        default = 0;
+        example = 512;
+        type = types.addCheck types.int (n: n >= 0);
+        description =
+          ''
+            Amount of memory (in MiB) allocated to Domain 0 on boot.
+            If set to 0, all memory is assigned to Domain 0.
+          '';
+      };
+
+    virtualisation.xen.bridge = {
+        name = mkOption {
+          default = "xenbr0";
+          type = types.str;
+          description = ''
+              Name of bridge the Xen domUs connect to.
+            '';
+        };
+
+        address = mkOption {
+          type = types.str;
+          default = "172.16.0.1";
+          description = ''
+            IPv4 address of the bridge.
+          '';
+        };
+
+        prefixLength = mkOption {
+          type = types.addCheck types.int (n: n >= 0 && n <= 32);
+          default = 16;
+          description = ''
+            Subnet mask of the bridge interface, specified as the number of
+            bits in the prefix (<literal>24</literal>).
+            A DHCP server will provide IP addresses for the whole, remaining
+            subnet.
+          '';
+        };
+
+        forwardDns = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            If set to <literal>true</literal>, the DNS queries from the
+            hosts connected to the bridge will be forwarded to the DNS
+            servers specified in /etc/resolv.conf .
+            '';
+        };
+
+      };
+
+    virtualisation.xen.stored =
+      mkOption {
+        type = types.path;
+        description =
+          ''
+            Xen Store daemon to use. Defaults to oxenstored of the xen package.
+          '';
+      };
+
+    virtualisation.xen.domains = {
+        extraConfig = mkOption {
+          type = types.lines;
+          default = "";
+          description =
+            ''
+              Options defined here will override the defaults for xendomains.
+              The default options can be seen in the file included from
+              /etc/default/xendomains.
+            '';
+          };
+      };
+
+    virtualisation.xen.trace = mkEnableOption "Xen tracing";
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    assertions = [ {
+      assertion = pkgs.stdenv.isx86_64;
+      message = "Xen currently not supported on ${pkgs.stdenv.hostPlatform.system}";
+    } {
+      assertion = config.boot.loader.grub.enable && (config.boot.loader.grub.efiSupport == false);
+      message = "Xen currently does not support EFI boot";
+    } ];
+
+    virtualisation.xen.package = mkDefault pkgs.xen;
+    virtualisation.xen.package-qemu = mkDefault pkgs.xen;
+    virtualisation.xen.stored = mkDefault "${cfg.package}/bin/oxenstored";
+
+    environment.systemPackages = [ cfg.package ];
+
+    boot.kernelModules =
+      [ "xen-evtchn" "xen-gntdev" "xen-gntalloc" "xen-blkback" "xen-netback"
+        "xen-pciback" "evtchn" "gntdev" "netbk" "blkbk" "xen-scsibk"
+        "usbbk" "pciback" "xen-acpi-processor" "blktap2" "tun" "netxen_nic"
+        "xen_wdt" "xen-acpi-processor" "xen-privcmd" "xen-scsiback"
+        "xenfs"
+      ];
+
+    # The xenfs module is needed in system.activationScripts.xen, but
+    # the modprobe command there fails silently. Include xenfs in the
+    # initrd as a work around.
+    boot.initrd.kernelModules = [ "xenfs" ];
+
+    # The radeonfb kernel module causes the screen to go black as soon
+    # as it's loaded, so don't load it.
+    boot.blacklistedKernelModules = [ "radeonfb" ];
+
+    # Increase the number of loopback devices from the default (8),
+    # which is way too small because every VM virtual disk requires a
+    # loopback device.
+    boot.extraModprobeConfig =
+      ''
+        options loop max_loop=64
+      '';
+
+    virtualisation.xen.bootParams = [] ++
+      optionals cfg.trace [ "loglvl=all" "guest_loglvl=all" ] ++
+      optional (cfg.domain0MemorySize != 0) "dom0_mem=${toString cfg.domain0MemorySize}M";
+
+    system.extraSystemBuilderCmds =
+      ''
+        ln -s ${cfg.package}/boot/xen.gz $out/xen.gz
+        echo "${toString cfg.bootParams}" > $out/xen-params
+      '';
+
+    # Mount the /proc/xen pseudo-filesystem.
+    system.activationScripts.xen =
+      ''
+        if [ -d /proc/xen ]; then
+            ${pkgs.kmod}/bin/modprobe xenfs 2> /dev/null
+            ${pkgs.util-linux}/bin/mountpoint -q /proc/xen || \
+                ${pkgs.util-linux}/bin/mount -t xenfs none /proc/xen
+        fi
+      '';
+
+    # Domain 0 requires a pvops-enabled kernel.
+    system.requiredKernelConfig = with config.lib.kernelConfig;
+      [ (isYes "XEN")
+        (isYes "X86_IO_APIC")
+        (isYes "ACPI")
+        (isYes "XEN_DOM0")
+        (isYes "PCI_XEN")
+        (isYes "XEN_DEV_EVTCHN")
+        (isYes "XENFS")
+        (isYes "XEN_COMPAT_XENFS")
+        (isYes "XEN_SYS_HYPERVISOR")
+        (isYes "XEN_GNTDEV")
+        (isYes "XEN_BACKEND")
+        (isModule "XEN_NETDEV_BACKEND")
+        (isModule "XEN_BLKDEV_BACKEND")
+        (isModule "XEN_PCIDEV_BACKEND")
+        (isYes "XEN_BALLOON")
+        (isYes "XEN_SCRUB_PAGES")
+      ];
+
+
+    environment.etc =
+      {
+        "xen/xl.conf".source = "${cfg.package}/etc/xen/xl.conf";
+        "xen/scripts".source = "${cfg.package}/etc/xen/scripts";
+        "default/xendomains".text = ''
+          source ${cfg.package}/etc/default/xendomains
+
+          ${cfg.domains.extraConfig}
+        '';
+      }
+      // optionalAttrs (builtins.compareVersions cfg.package.version "4.10" >= 0) {
+        # in V 4.10 oxenstored requires /etc/xen/oxenstored.conf to start
+        "xen/oxenstored.conf".source = "${cfg.package}/etc/xen/oxenstored.conf";
+      };
+
+    # Xen provides udev rules.
+    services.udev.packages = [ cfg.package ];
+
+    services.udev.path = [ pkgs.bridge-utils pkgs.iproute2 ];
+
+    systemd.services.xen-store = {
+      description = "Xen Store Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "xen-store.socket" ];
+      requires = [ "xen-store.socket" ];
+      preStart = ''
+        export XENSTORED_ROOTDIR="/var/lib/xenstored"
+        rm -f "$XENSTORED_ROOTDIR"/tdb* &>/dev/null
+
+        mkdir -p /var/run
+        mkdir -p /var/log/xen # Running xl requires /var/log/xen and /var/lib/xen,
+        mkdir -p /var/lib/xen # so we create them here unconditionally.
+        grep -q control_d /proc/xen/capabilities
+        '';
+      serviceConfig = if (builtins.compareVersions cfg.package.version "4.8" < 0) then
+        { ExecStart = ''
+            ${cfg.stored}${optionalString cfg.trace " -T /var/log/xen/xenstored-trace.log"} --no-fork
+            '';
+        } else {
+          ExecStart = ''
+            ${cfg.package}/etc/xen/scripts/launch-xenstore
+            '';
+          Type            = "notify";
+          RemainAfterExit = true;
+          NotifyAccess    = "all";
+        };
+      postStart = ''
+        ${optionalString (builtins.compareVersions cfg.package.version "4.8" < 0) ''
+          time=0
+          timeout=30
+          # Wait for xenstored to actually come up, timing out after 30 seconds
+          while [ $time -lt $timeout ] && ! `${cfg.package}/bin/xenstore-read -s / >/dev/null 2>&1` ; do
+              time=$(($time+1))
+              sleep 1
+          done
+
+          # Exit if we timed out
+          if ! [ $time -lt $timeout ] ; then
+              echo "Could not start Xenstore Daemon"
+              exit 1
+          fi
+        ''}
+        echo "executing xen-init-dom0"
+        ${cfg.package}/lib/xen/bin/xen-init-dom0
+        '';
+    };
+
+    systemd.sockets.xen-store = {
+      description = "XenStore Socket for userspace API";
+      wantedBy = [ "sockets.target" ];
+      socketConfig = {
+        ListenStream = [ "/var/run/xenstored/socket" "/var/run/xenstored/socket_ro" ];
+        SocketMode = "0660";
+        SocketUser = "root";
+        SocketGroup = "root";
+      };
+    };
+
+
+    systemd.services.xen-console = {
+      description = "Xen Console Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "xen-store.service" ];
+      requires = [ "xen-store.service" ];
+      preStart = ''
+        mkdir -p /var/run/xen
+        ${optionalString cfg.trace "mkdir -p /var/log/xen"}
+        grep -q control_d /proc/xen/capabilities
+        '';
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.package}/bin/xenconsoled\
+            ${optionalString ((builtins.compareVersions cfg.package.version "4.8" >= 0)) " -i"}\
+            ${optionalString cfg.trace " --log=all --log-dir=/var/log/xen"}
+          '';
+      };
+    };
+
+
+    systemd.services.xen-qemu = {
+      description = "Xen Qemu Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "xen-console.service" ];
+      requires = [ "xen-store.service" ];
+      serviceConfig.ExecStart = ''
+        ${cfg.package-qemu}/${cfg.package-qemu.qemu-system-i386} \
+           -xen-attach -xen-domid 0 -name dom0 -M xenpv \
+           -nographic -monitor /dev/null -serial /dev/null -parallel /dev/null
+        '';
+    };
+
+
+    systemd.services.xen-watchdog = {
+      description = "Xen Watchdog Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "xen-qemu.service" "xen-domains.service" ];
+      serviceConfig.ExecStart = "${cfg.package}/bin/xenwatchdogd 30 15";
+      serviceConfig.Type = "forking";
+      serviceConfig.RestartSec = "1";
+      serviceConfig.Restart = "on-failure";
+    };
+
+
+    systemd.services.xen-bridge = {
+      description = "Xen bridge";
+      wantedBy = [ "multi-user.target" ];
+      before = [ "xen-domains.service" ];
+      preStart = ''
+        mkdir -p /var/run/xen
+        touch /var/run/xen/dnsmasq.pid
+        touch /var/run/xen/dnsmasq.etherfile
+        touch /var/run/xen/dnsmasq.leasefile
+
+        IFS='-' read -a data <<< `${pkgs.sipcalc}/bin/sipcalc ${cfg.bridge.address}/${toString cfg.bridge.prefixLength} | grep Usable\ range`
+        export XEN_BRIDGE_IP_RANGE_START="${"\${data[1]//[[:blank:]]/}"}"
+        export XEN_BRIDGE_IP_RANGE_END="${"\${data[2]//[[:blank:]]/}"}"
+
+        IFS='-' read -a data <<< `${pkgs.sipcalc}/bin/sipcalc ${cfg.bridge.address}/${toString cfg.bridge.prefixLength} | grep Network\ address`
+        export XEN_BRIDGE_NETWORK_ADDRESS="${"\${data[1]//[[:blank:]]/}"}"
+
+        IFS='-' read -a data <<< `${pkgs.sipcalc}/bin/sipcalc ${cfg.bridge.address}/${toString cfg.bridge.prefixLength} | grep Network\ mask`
+        export XEN_BRIDGE_NETMASK="${"\${data[1]//[[:blank:]]/}"}"
+
+        echo "${cfg.bridge.address} host gw dns" > /var/run/xen/dnsmasq.hostsfile
+
+        cat <<EOF > /var/run/xen/dnsmasq.conf
+        no-daemon
+        pid-file=/var/run/xen/dnsmasq.pid
+        interface=${cfg.bridge.name}
+        except-interface=lo
+        bind-interfaces
+        auth-zone=xen.local,$XEN_BRIDGE_NETWORK_ADDRESS/${toString cfg.bridge.prefixLength}
+        domain=xen.local
+        addn-hosts=/var/run/xen/dnsmasq.hostsfile
+        expand-hosts
+        strict-order
+        no-hosts
+        bogus-priv
+        ${optionalString (!cfg.bridge.forwardDns) ''
+          no-resolv
+          no-poll
+          auth-server=dns.xen.local,${cfg.bridge.name}
+        ''}
+        filterwin2k
+        clear-on-reload
+        domain-needed
+        dhcp-hostsfile=/var/run/xen/dnsmasq.etherfile
+        dhcp-authoritative
+        dhcp-range=$XEN_BRIDGE_IP_RANGE_START,$XEN_BRIDGE_IP_RANGE_END
+        dhcp-no-override
+        no-ping
+        dhcp-leasefile=/var/run/xen/dnsmasq.leasefile
+        EOF
+
+        # DHCP
+        ${pkgs.iptables}/bin/iptables -w -I INPUT  -i ${cfg.bridge.name} -p tcp -s $XEN_BRIDGE_NETWORK_ADDRESS/${toString cfg.bridge.prefixLength} --sport 68 --dport 67 -j ACCEPT
+        ${pkgs.iptables}/bin/iptables -w -I INPUT  -i ${cfg.bridge.name} -p udp -s $XEN_BRIDGE_NETWORK_ADDRESS/${toString cfg.bridge.prefixLength} --sport 68 --dport 67 -j ACCEPT
+        # DNS
+        ${pkgs.iptables}/bin/iptables -w -I INPUT  -i ${cfg.bridge.name} -p tcp -d ${cfg.bridge.address} --dport 53 -m state --state NEW,ESTABLISHED -j ACCEPT
+        ${pkgs.iptables}/bin/iptables -w -I INPUT  -i ${cfg.bridge.name} -p udp -d ${cfg.bridge.address} --dport 53 -m state --state NEW,ESTABLISHED -j ACCEPT
+
+        ${pkgs.bridge-utils}/bin/brctl addbr ${cfg.bridge.name}
+        ${pkgs.inetutils}/bin/ifconfig ${cfg.bridge.name} ${cfg.bridge.address}
+        ${pkgs.inetutils}/bin/ifconfig ${cfg.bridge.name} netmask $XEN_BRIDGE_NETMASK
+        ${pkgs.inetutils}/bin/ifconfig ${cfg.bridge.name} up
+      '';
+      serviceConfig.ExecStart = "${pkgs.dnsmasq}/bin/dnsmasq --conf-file=/var/run/xen/dnsmasq.conf";
+      postStop = ''
+        IFS='-' read -a data <<< `${pkgs.sipcalc}/bin/sipcalc ${cfg.bridge.address}/${toString cfg.bridge.prefixLength} | grep Network\ address`
+        export XEN_BRIDGE_NETWORK_ADDRESS="${"\${data[1]//[[:blank:]]/}"}"
+
+        ${pkgs.inetutils}/bin/ifconfig ${cfg.bridge.name} down
+        ${pkgs.bridge-utils}/bin/brctl delbr ${cfg.bridge.name}
+
+        # DNS
+        ${pkgs.iptables}/bin/iptables -w -D INPUT  -i ${cfg.bridge.name} -p udp -d ${cfg.bridge.address} --dport 53 -m state --state NEW,ESTABLISHED -j ACCEPT
+        ${pkgs.iptables}/bin/iptables -w -D INPUT  -i ${cfg.bridge.name} -p tcp -d ${cfg.bridge.address} --dport 53 -m state --state NEW,ESTABLISHED -j ACCEPT
+        # DHCP
+        ${pkgs.iptables}/bin/iptables -w -D INPUT  -i ${cfg.bridge.name} -p udp -s $XEN_BRIDGE_NETWORK_ADDRESS/${toString cfg.bridge.prefixLength} --sport 68 --dport 67 -j ACCEPT
+        ${pkgs.iptables}/bin/iptables -w -D INPUT  -i ${cfg.bridge.name} -p tcp -s $XEN_BRIDGE_NETWORK_ADDRESS/${toString cfg.bridge.prefixLength} --sport 68 --dport 67 -j ACCEPT
+      '';
+    };
+
+
+    systemd.services.xen-domains = {
+      description = "Xen domains - automatically starts, saves and restores Xen domains";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "xen-bridge.service" "xen-qemu.service" ];
+      requires = [ "xen-bridge.service" "xen-qemu.service" ];
+      ## To prevent a race between dhcpcd and xend's bridge setup script
+      ## (which renames eth* to peth* and recreates eth* as a virtual
+      ## device), start dhcpcd after xend.
+      before = [ "dhcpd.service" ];
+      restartIfChanged = false;
+      serviceConfig.RemainAfterExit = "yes";
+      path = [ cfg.package cfg.package-qemu ];
+      environment.XENDOM_CONFIG = "${cfg.package}/etc/sysconfig/xendomains";
+      preStart = "mkdir -p /var/lock/subsys -m 755";
+      serviceConfig.ExecStart = "${cfg.package}/etc/init.d/xendomains start";
+      serviceConfig.ExecStop = "${cfg.package}/etc/init.d/xendomains stop";
+    };
+
+  };
+}
diff --git a/nixos/modules/virtualisation/xen-domU.nix b/nixos/modules/virtualisation/xen-domU.nix
new file mode 100644
index 00000000000..c00b984c2ce
--- /dev/null
+++ b/nixos/modules/virtualisation/xen-domU.nix
@@ -0,0 +1,19 @@
+# Common configuration for Xen DomU NixOS virtual machines.
+
+{ ... }:
+
+{
+  boot.loader.grub.version = 2;
+  boot.loader.grub.device = "nodev";
+
+  boot.initrd.kernelModules =
+    [ "xen-blkfront" "xen-tpmfront" "xen-kbdfront" "xen-fbfront"
+      "xen-netfront" "xen-pcifront" "xen-scsifront"
+    ];
+
+  # Send syslog messages to the Xen console.
+  services.syslogd.tty = "hvc0";
+
+  # Don't run ntpd, since we should get the correct time from Dom0.
+  services.timesyncd.enable = false;
+}
diff --git a/nixos/release-combined.nix b/nixos/release-combined.nix
new file mode 100644
index 00000000000..fd8a39cfb92
--- /dev/null
+++ b/nixos/release-combined.nix
@@ -0,0 +1,160 @@
+# This jobset defines the main NixOS channels (such as nixos-unstable
+# and nixos-14.04). The channel is updated every time the ‘tested’ job
+# succeeds, and all other jobs have finished (they may fail).
+
+{ nixpkgs ? { outPath = (import ../lib).cleanSource ./..; revCount = 56789; shortRev = "gfedcba"; }
+, stableBranch ? false
+, supportedSystems ? [ "x86_64-linux" ]
+, limitedSupportedSystems ? [ "i686-linux" "aarch64-linux" ]
+}:
+
+let
+
+  nixpkgsSrc = nixpkgs; # urgh
+
+  pkgs = import ./.. {};
+
+  removeMaintainers = set: if builtins.isAttrs set
+    then if (set.type or "") == "derivation"
+      then set // { meta = builtins.removeAttrs (set.meta or {}) [ "maintainers" ]; }
+      else pkgs.lib.mapAttrs (n: v: removeMaintainers v) set
+    else set;
+
+in rec {
+
+  nixos = removeMaintainers (import ./release.nix {
+    inherit stableBranch;
+    supportedSystems = supportedSystems ++ limitedSupportedSystems;
+    nixpkgs = nixpkgsSrc;
+  });
+
+  nixpkgs = builtins.removeAttrs (removeMaintainers (import ../pkgs/top-level/release.nix {
+    inherit supportedSystems;
+    nixpkgs = nixpkgsSrc;
+  })) [ "unstable" ];
+
+  tested =
+    let
+      onFullSupported = x: map (system: "${x}.${system}") supportedSystems;
+      onAllSupported = x: map (system: "${x}.${system}") (supportedSystems ++ limitedSupportedSystems);
+      onSystems = systems: x: map (system: "${x}.${system}")
+        (pkgs.lib.intersectLists systems (supportedSystems ++ limitedSupportedSystems));
+    in pkgs.releaseTools.aggregate {
+      name = "nixos-${nixos.channel.version}";
+      meta = {
+        description = "Release-critical builds for the NixOS channel";
+        maintainers = with pkgs.lib.maintainers; [ eelco fpletz ];
+      };
+      constituents = pkgs.lib.concatLists [
+        [ "nixos.channel" ]
+        (onFullSupported "nixos.dummy")
+        (onAllSupported "nixos.iso_minimal")
+        (onSystems ["x86_64-linux" "aarch64-linux"] "nixos.amazonImage")
+        (onSystems ["x86_64-linux"] "nixos.iso_plasma5")
+        (onSystems ["x86_64-linux"] "nixos.iso_gnome")
+        (onFullSupported "nixos.manual")
+        (onSystems ["x86_64-linux"] "nixos.ova")
+        (onSystems ["aarch64-linux"] "nixos.sd_image")
+        (onSystems ["x86_64-linux"] "nixos.tests.boot.biosCdrom")
+        (onSystems ["x86_64-linux"] "nixos.tests.boot.biosUsb")
+        (onFullSupported "nixos.tests.boot-stage1")
+        (onSystems ["x86_64-linux"] "nixos.tests.boot.uefiCdrom")
+        (onSystems ["x86_64-linux"] "nixos.tests.boot.uefiUsb")
+        (onSystems ["x86_64-linux"] "nixos.tests.chromium")
+        (onFullSupported "nixos.tests.containers-imperative")
+        (onFullSupported "nixos.tests.containers-ip")
+        (onSystems ["x86_64-linux"] "nixos.tests.docker")
+        (onFullSupported "nixos.tests.ecryptfs")
+        (onFullSupported "nixos.tests.env")
+        (onFullSupported "nixos.tests.firefox-esr")
+        (onFullSupported "nixos.tests.firefox")
+        (onFullSupported "nixos.tests.firewall")
+        (onFullSupported "nixos.tests.fontconfig-default-fonts")
+        (onFullSupported "nixos.tests.gnome")
+        (onFullSupported "nixos.tests.gnome-xorg")
+        (onSystems ["x86_64-linux"] "nixos.tests.hibernate")
+        (onFullSupported "nixos.tests.i3wm")
+        (onSystems ["x86_64-linux"] "nixos.tests.installer.btrfsSimple")
+        (onSystems ["x86_64-linux"] "nixos.tests.installer.btrfsSubvolDefault")
+        (onSystems ["x86_64-linux"] "nixos.tests.installer.btrfsSubvols")
+        (onSystems ["x86_64-linux"] "nixos.tests.installer.luksroot")
+        (onSystems ["x86_64-linux"] "nixos.tests.installer.lvm")
+        (onSystems ["x86_64-linux"] "nixos.tests.installer.separateBootFat")
+        (onSystems ["x86_64-linux"] "nixos.tests.installer.separateBoot")
+        (onSystems ["x86_64-linux"] "nixos.tests.installer.simpleLabels")
+        (onSystems ["x86_64-linux"] "nixos.tests.installer.simpleProvided")
+        (onSystems ["x86_64-linux"] "nixos.tests.installer.simpleUefiSystemdBoot")
+        (onSystems ["x86_64-linux"] "nixos.tests.installer.simple")
+        (onSystems ["x86_64-linux"] "nixos.tests.installer.swraid")
+        (onFullSupported "nixos.tests.ipv6")
+        (onFullSupported "nixos.tests.keymap.azerty")
+        (onFullSupported "nixos.tests.keymap.colemak")
+        (onFullSupported "nixos.tests.keymap.dvorak")
+        (onFullSupported "nixos.tests.keymap.dvorak-programmer")
+        (onFullSupported "nixos.tests.keymap.neo")
+        (onFullSupported "nixos.tests.keymap.qwertz")
+        (onFullSupported "nixos.tests.latestKernel.login")
+        (onFullSupported "nixos.tests.lightdm")
+        (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")
+        (onFullSupported "nixos.tests.networking.scripted.bridge")
+        (onFullSupported "nixos.tests.networking.scripted.dhcpOneIf")
+        (onFullSupported "nixos.tests.networking.scripted.dhcpSimple")
+        (onFullSupported "nixos.tests.networking.scripted.link")
+        (onFullSupported "nixos.tests.networking.scripted.loopback")
+        (onFullSupported "nixos.tests.networking.scripted.macvlan")
+        (onFullSupported "nixos.tests.networking.scripted.privacy")
+        (onFullSupported "nixos.tests.networking.scripted.routes")
+        (onFullSupported "nixos.tests.networking.scripted.sit")
+        (onFullSupported "nixos.tests.networking.scripted.static")
+        (onFullSupported "nixos.tests.networking.scripted.virtual")
+        (onFullSupported "nixos.tests.networking.scripted.vlan")
+        (onFullSupported "nixos.tests.networking.networkd.bond")
+        (onFullSupported "nixos.tests.networking.networkd.bridge")
+        (onFullSupported "nixos.tests.networking.networkd.dhcpOneIf")
+        (onFullSupported "nixos.tests.networking.networkd.dhcpSimple")
+        (onFullSupported "nixos.tests.networking.networkd.link")
+        (onFullSupported "nixos.tests.networking.networkd.loopback")
+        # Fails nondeterministically (https://github.com/NixOS/nixpkgs/issues/96709)
+        #(onFullSupported "nixos.tests.networking.networkd.macvlan")
+        (onFullSupported "nixos.tests.networking.networkd.privacy")
+        (onFullSupported "nixos.tests.networking.networkd.routes")
+        (onFullSupported "nixos.tests.networking.networkd.sit")
+        (onFullSupported "nixos.tests.networking.networkd.static")
+        (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.nfs4.simple")
+        (onFullSupported "nixos.tests.openssh")
+        (onFullSupported "nixos.tests.pantheon")
+        (onFullSupported "nixos.tests.php.fpm")
+        (onFullSupported "nixos.tests.php.httpd")
+        (onFullSupported "nixos.tests.php.pcre")
+        (onFullSupported "nixos.tests.plasma5")
+        (onFullSupported "nixos.tests.predictable-interface-names.predictableNetworkd")
+        (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.proxy")
+        (onFullSupported "nixos.tests.sddm.default")
+        (onFullSupported "nixos.tests.shadow")
+        (onFullSupported "nixos.tests.simple")
+        (onFullSupported "nixos.tests.sway")
+        (onFullSupported "nixos.tests.switchTest")
+        (onFullSupported "nixos.tests.udisks2")
+        (onFullSupported "nixos.tests.xfce")
+        (onSystems ["i686-linux"] "nixos.tests.zfs.installer")
+        (onFullSupported "nixpkgs.emacs")
+        (onFullSupported "nixpkgs.jdk")
+        ["nixpkgs.tarball"]
+      ];
+    };
+}
diff --git a/nixos/release-small.nix b/nixos/release-small.nix
new file mode 100644
index 00000000000..1d51b4e7f28
--- /dev/null
+++ b/nixos/release-small.nix
@@ -0,0 +1,127 @@
+# 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.
+
+{ nixpkgs ? { outPath = (import ../lib).cleanSource ./..; revCount = 56789; shortRev = "gfedcba"; }
+, stableBranch ? false
+, supportedSystems ? [ "x86_64-linux" ] # no i686-linux
+}:
+
+let
+
+  nixpkgsSrc = nixpkgs; # urgh
+
+  pkgs = import ./.. { system = "x86_64-linux"; };
+
+  lib = pkgs.lib;
+
+  nixos' = import ./release.nix {
+    inherit stableBranch supportedSystems;
+    nixpkgs = nixpkgsSrc;
+  };
+
+  nixpkgs' = builtins.removeAttrs (import ../pkgs/top-level/release.nix {
+    inherit supportedSystems;
+    nixpkgs = nixpkgsSrc;
+  }) [ "unstable" ];
+
+in rec {
+
+  nixos = {
+    inherit (nixos') channel manual options iso_minimal amazonImage dummy;
+    tests = {
+      inherit (nixos'.tests)
+        containers-imperative
+        containers-ip
+        firewall
+        ipv6
+        login
+        misc
+        nat
+        # fails with kernel >= 5.15 https://github.com/NixOS/nixpkgs/pull/152505#issuecomment-1005049314
+        #nfs3
+        openssh
+        php
+        predictable-interface-names
+        proxy
+        simple;
+      installer = {
+        inherit (nixos'.tests.installer)
+          lvm
+          separateBoot
+          simple;
+      };
+      boot = {
+        inherit (nixos'.tests.boot)
+          biosCdrom;
+      };
+    };
+  };
+
+  nixpkgs = {
+    inherit (nixpkgs')
+      apacheHttpd
+      cmake
+      cryptsetup
+      emacs
+      gettext
+      git
+      imagemagick
+      jdk
+      linux
+      mysql
+      nginx
+      nodejs
+      openssh
+      php
+      postgresql
+      python
+      rsyslog
+      stdenv
+      subversion
+      tarball
+      vim;
+  };
+
+  tested = pkgs.releaseTools.aggregate {
+    name = "nixos-${nixos.channel.version}";
+    meta = {
+      description = "Release-critical builds for the NixOS channel";
+      maintainers = [ lib.maintainers.eelco ];
+    };
+    constituents =
+      [ "nixos.channel"
+        "nixos.dummy.x86_64-linux"
+        "nixos.iso_minimal.x86_64-linux"
+        "nixos.amazonImage.x86_64-linux"
+        "nixos.manual.x86_64-linux"
+        "nixos.tests.boot.biosCdrom.x86_64-linux"
+        "nixos.tests.containers-imperative.x86_64-linux"
+        "nixos.tests.containers-ip.x86_64-linux"
+        "nixos.tests.firewall.x86_64-linux"
+        "nixos.tests.installer.lvm.x86_64-linux"
+        "nixos.tests.installer.separateBoot.x86_64-linux"
+        "nixos.tests.installer.simple.x86_64-linux"
+        "nixos.tests.ipv6.x86_64-linux"
+        "nixos.tests.login.x86_64-linux"
+        "nixos.tests.misc.x86_64-linux"
+        "nixos.tests.nat.firewall-conntrack.x86_64-linux"
+        "nixos.tests.nat.firewall.x86_64-linux"
+        "nixos.tests.nat.standalone.x86_64-linux"
+        # fails with kernel >= 5.15 https://github.com/NixOS/nixpkgs/pull/152505#issuecomment-1005049314
+        #"nixos.tests.nfs3.simple.x86_64-linux"
+        "nixos.tests.openssh.x86_64-linux"
+        "nixos.tests.php.fpm.x86_64-linux"
+        "nixos.tests.php.pcre.x86_64-linux"
+        "nixos.tests.predictable-interface-names.predictable.x86_64-linux"
+        "nixos.tests.predictable-interface-names.predictableNetworkd.x86_64-linux"
+        "nixos.tests.predictable-interface-names.unpredictable.x86_64-linux"
+        "nixos.tests.predictable-interface-names.unpredictableNetworkd.x86_64-linux"
+        "nixos.tests.proxy.x86_64-linux"
+        "nixos.tests.simple.x86_64-linux"
+        "nixpkgs.jdk.x86_64-linux"
+        "nixpkgs.tarball"
+      ];
+  };
+
+}
diff --git a/nixos/release.nix b/nixos/release.nix
new file mode 100644
index 00000000000..6b7564a9b97
--- /dev/null
+++ b/nixos/release.nix
@@ -0,0 +1,386 @@
+with import ../lib;
+
+{ nixpkgs ? { outPath = cleanSource ./..; revCount = 130979; shortRev = "gfedcba"; }
+, stableBranch ? false
+, supportedSystems ? [ "x86_64-linux" "aarch64-linux" ]
+, configuration ? {}
+}:
+
+with import ../pkgs/top-level/release-lib.nix { inherit supportedSystems; };
+
+let
+
+  version = fileContents ../.version;
+  versionSuffix =
+    (if stableBranch then "." else "pre") + "${toString nixpkgs.revCount}.${nixpkgs.shortRev}";
+
+  # Run the tests for each platform.  You can run a test by doing
+  # e.g. ‘nix-build -A tests.login.x86_64-linux’, or equivalently,
+  # ‘nix-build tests/login.nix -A result’.
+  allTestsForSystem = system:
+    import ./tests/all-tests.nix {
+      inherit system;
+      pkgs = import ./.. { inherit system; };
+      callTest = t: {
+        ${system} = hydraJob t.test;
+      };
+    };
+  allTests =
+    foldAttrs recursiveUpdate {} (map allTestsForSystem supportedSystems);
+
+  pkgs = import ./.. { system = "x86_64-linux"; };
+
+
+  versionModule =
+    { system.nixos.versionSuffix = versionSuffix;
+      system.nixos.revision = nixpkgs.rev or nixpkgs.shortRev;
+    };
+
+  makeModules = module: rest: [ configuration versionModule module rest ];
+
+  makeIso =
+    { module, type, system, ... }:
+
+    with import ./.. { inherit system; };
+
+    hydraJob ((import lib/eval-config.nix {
+      inherit system;
+      modules = makeModules module {
+        isoImage.isoBaseName = "nixos-${type}";
+      };
+    }).config.system.build.isoImage);
+
+
+  makeSdImage =
+    { module, system, ... }:
+
+    with import ./.. { inherit system; };
+
+    hydraJob ((import lib/eval-config.nix {
+      inherit system;
+      modules = makeModules module {};
+    }).config.system.build.sdImage);
+
+
+  makeSystemTarball =
+    { module, maintainers ? ["viric"], system }:
+
+    with import ./.. { inherit system; };
+
+    let
+
+      config = (import lib/eval-config.nix {
+        inherit system;
+        modules = makeModules module {};
+      }).config;
+
+      tarball = config.system.build.tarball;
+
+    in
+      tarball //
+        { meta = {
+            description = "NixOS system tarball for ${system} - ${stdenv.hostPlatform.linux-kernel.name}";
+            maintainers = map (x: lib.maintainers.${x}) maintainers;
+          };
+          inherit config;
+        };
+
+
+  makeClosure = module: buildFromConfig module (config: config.system.build.toplevel);
+
+
+  buildFromConfig = module: sel: forAllSystems (system: hydraJob (sel (import ./lib/eval-config.nix {
+    inherit system;
+    modules = makeModules module
+      ({ ... }:
+      { fileSystems."/".device  = mkDefault "/dev/sda1";
+        boot.loader.grub.device = mkDefault "/dev/sda";
+      });
+  }).config));
+
+  makeNetboot = { module, system, ... }:
+    let
+      configEvaled = import lib/eval-config.nix {
+        inherit system;
+        modules = makeModules module {};
+      };
+      build = configEvaled.config.system.build;
+      kernelTarget = configEvaled.pkgs.stdenv.hostPlatform.linux-kernel.target;
+    in
+      pkgs.symlinkJoin {
+        name = "netboot";
+        paths = [
+          build.netbootRamdisk
+          build.kernel
+          build.netbootIpxeScript
+        ];
+        postBuild = ''
+          mkdir -p $out/nix-support
+          echo "file ${kernelTarget} ${build.kernel}/${kernelTarget}" >> $out/nix-support/hydra-build-products
+          echo "file initrd ${build.netbootRamdisk}/initrd" >> $out/nix-support/hydra-build-products
+          echo "file ipxe ${build.netbootIpxeScript}/netboot.ipxe" >> $out/nix-support/hydra-build-products
+        '';
+        preferLocalBuild = true;
+      };
+
+in rec {
+
+  channel = import lib/make-channel.nix { inherit pkgs nixpkgs version versionSuffix; };
+
+  manualHTML = buildFromConfig ({ ... }: { }) (config: config.system.build.manual.manualHTML);
+  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;
+
+
+  # Build the initial ramdisk so Hydra can keep track of its size over time.
+  initialRamdisk = buildFromConfig ({ ... }: { }) (config: config.system.build.initialRamdisk);
+
+  netboot = forMatchingSystems supportedSystems (system: makeNetboot {
+    module = ./modules/installer/netboot/netboot-minimal.nix;
+    inherit system;
+  });
+
+  iso_minimal = forAllSystems (system: makeIso {
+    module = ./modules/installer/cd-dvd/installation-cd-minimal.nix;
+    type = "minimal";
+    inherit system;
+  });
+
+  iso_plasma5 = forMatchingSystems [ "x86_64-linux" ] (system: makeIso {
+    module = ./modules/installer/cd-dvd/installation-cd-graphical-plasma5.nix;
+    type = "plasma5";
+    inherit system;
+  });
+
+  iso_gnome = forMatchingSystems [ "x86_64-linux" ] (system: makeIso {
+    module = ./modules/installer/cd-dvd/installation-cd-graphical-gnome.nix;
+    type = "gnome";
+    inherit system;
+  });
+
+  # A variant with a more recent (but possibly less stable) kernel
+  # that might support more hardware.
+  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;
+  });
+
+  sd_image = forMatchingSystems [ "armv6l-linux" "armv7l-linux" "aarch64-linux" ] (system: makeSdImage {
+    module = {
+        armv6l-linux = ./modules/installer/sd-card/sd-image-raspberrypi-installer.nix;
+        armv7l-linux = ./modules/installer/sd-card/sd-image-armv7l-multiplatform-installer.nix;
+        aarch64-linux = ./modules/installer/sd-card/sd-image-aarch64-installer.nix;
+      }.${system};
+    inherit system;
+  });
+
+  sd_image_new_kernel = forMatchingSystems [ "aarch64-linux" ] (system: makeSdImage {
+    module = {
+        aarch64-linux = ./modules/installer/sd-card/sd-image-aarch64-new-kernel-installer.nix;
+      }.${system};
+    type = "minimal-new-kernel";
+    inherit system;
+  });
+
+  # A bootable VirtualBox virtual appliance as an OVA file (i.e. packaged OVF).
+  ova = forMatchingSystems [ "x86_64-linux" ] (system:
+
+    with import ./.. { inherit system; };
+
+    hydraJob ((import lib/eval-config.nix {
+      inherit system;
+      modules =
+        [ versionModule
+          ./modules/installer/virtualbox-demo.nix
+        ];
+    }).config.system.build.virtualBoxOVA)
+
+  );
+
+
+  # A disk image that can be imported to Amazon EC2 and registered as an AMI
+  amazonImage = forMatchingSystems [ "x86_64-linux" "aarch64-linux" ] (system:
+
+    with import ./.. { inherit system; };
+
+    hydraJob ((import lib/eval-config.nix {
+      inherit system;
+      modules =
+        [ configuration
+          versionModule
+          ./maintainers/scripts/ec2/amazon-image.nix
+        ];
+    }).config.system.build.amazonImage)
+
+  );
+  amazonImageZfs = forMatchingSystems [ "x86_64-linux" "aarch64-linux" ] (system:
+
+    with import ./.. { inherit system; };
+
+    hydraJob ((import lib/eval-config.nix {
+      inherit system;
+      modules =
+        [ configuration
+          versionModule
+          ./maintainers/scripts/ec2/amazon-image-zfs.nix
+        ];
+    }).config.system.build.amazonImage)
+
+  );
+
+
+  # Test job for https://github.com/NixOS/nixpkgs/issues/121354 to test
+  # automatic sizing without blocking the channel.
+  amazonImageAutomaticSize = forMatchingSystems [ "x86_64-linux" "aarch64-linux" ] (system:
+
+    with import ./.. { inherit system; };
+
+    hydraJob ((import lib/eval-config.nix {
+      inherit system;
+      modules =
+        [ configuration
+          versionModule
+          ./maintainers/scripts/ec2/amazon-image.nix
+          ({ ... }: { amazonImage.sizeMB = "auto"; })
+        ];
+    }).config.system.build.amazonImage)
+
+  );
+
+  # An image that can be imported into lxd and used for container creation
+  lxdImage = forMatchingSystems [ "x86_64-linux" "aarch64-linux" ] (system:
+
+    with import ./.. { inherit system; };
+
+    hydraJob ((import lib/eval-config.nix {
+      inherit system;
+      modules =
+        [ configuration
+          versionModule
+          ./maintainers/scripts/lxd/lxd-image.nix
+        ];
+    }).config.system.build.tarball)
+
+  );
+
+  # Metadata for the lxd image
+  lxdMeta = forMatchingSystems [ "x86_64-linux" "aarch64-linux" ] (system:
+
+    with import ./.. { inherit system; };
+
+    hydraJob ((import lib/eval-config.nix {
+      inherit system;
+      modules =
+        [ configuration
+          versionModule
+          ./maintainers/scripts/lxd/lxd-image.nix
+        ];
+    }).config.system.build.metadata)
+
+  );
+
+  # Ensure that all packages used by the minimal NixOS config end up in the channel.
+  dummy = forAllSystems (system: pkgs.runCommand "dummy"
+    { toplevel = (import lib/eval-config.nix {
+        inherit system;
+        modules = singleton ({ ... }:
+          { fileSystems."/".device  = mkDefault "/dev/sda1";
+            boot.loader.grub.device = mkDefault "/dev/sda";
+            system.stateVersion = mkDefault "18.03";
+          });
+      }).config.system.build.toplevel;
+      preferLocalBuild = true;
+    }
+    "mkdir $out; ln -s $toplevel $out/dummy");
+
+
+  # Provide a tarball that can be unpacked into an SD card, and easily
+  # boot that system from uboot (like for the sheevaplug).
+  # The pc variant helps preparing the expression for the system tarball
+  # in a machine faster than the sheevpalug
+  /*
+  system_tarball_pc = forAllSystems (system: makeSystemTarball {
+    module = ./modules/installer/cd-dvd/system-tarball-pc.nix;
+    inherit system;
+  });
+  */
+
+  # Provide container tarball for lxc, libvirt-lxc, docker-lxc, ...
+  containerTarball = forAllSystems (system: makeSystemTarball {
+    module = ./modules/virtualisation/lxc-container.nix;
+    inherit system;
+  });
+
+  /*
+  system_tarball_fuloong2f =
+    assert builtins.currentSystem == "mips64-linux";
+    makeSystemTarball {
+      module = ./modules/installer/cd-dvd/system-tarball-fuloong2f.nix;
+      system = "mips64-linux";
+    };
+
+  system_tarball_sheevaplug =
+    assert builtins.currentSystem == "armv5tel-linux";
+    makeSystemTarball {
+      module = ./modules/installer/cd-dvd/system-tarball-sheevaplug.nix;
+      system = "armv5tel-linux";
+    };
+  */
+
+  tests = allTests;
+
+  /* Build a bunch of typical closures so that Hydra can keep track of
+     the evolution of closure sizes. */
+
+  closures = {
+
+    smallContainer = makeClosure ({ ... }:
+      { boot.isContainer = true;
+        services.openssh.enable = true;
+      });
+
+    tinyContainer = makeClosure ({ ... }:
+      { boot.isContainer = true;
+        imports = [ modules/profiles/minimal.nix ];
+      });
+
+    ec2 = makeClosure ({ ... }:
+      { imports = [ modules/virtualisation/amazon-image.nix ];
+      });
+
+    kde = makeClosure ({ ... }:
+      { services.xserver.enable = true;
+        services.xserver.displayManager.sddm.enable = true;
+        services.xserver.desktopManager.plasma5.enable = true;
+      });
+
+    xfce = makeClosure ({ ... }:
+      { services.xserver.enable = true;
+        services.xserver.desktopManager.xfce.enable = true;
+      });
+
+    gnome = makeClosure ({ ... }:
+      { services.xserver.enable = true;
+        services.xserver.displayManager.gdm.enable = true;
+        services.xserver.desktopManager.gnome.enable = true;
+      });
+
+    pantheon = makeClosure ({ ... }:
+      { services.xserver.enable = true;
+        services.xserver.desktopManager.pantheon.enable = true;
+      });
+
+    # Linux/Apache/PostgreSQL/PHP stack.
+    lapp = makeClosure ({ pkgs, ... }:
+      { services.httpd.enable = true;
+        services.httpd.adminAddr = "foo@example.org";
+        services.httpd.enablePHP = true;
+        services.postgresql.enable = true;
+        services.postgresql.package = pkgs.postgresql;
+      });
+  };
+}
diff --git a/nixos/tests/3proxy.nix b/nixos/tests/3proxy.nix
new file mode 100644
index 00000000000..dfc4b35a772
--- /dev/null
+++ b/nixos/tests/3proxy.nix
@@ -0,0 +1,189 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "3proxy";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ misuzu ];
+  };
+
+  nodes = {
+    peer0 = { lib, ... }: {
+      networking.useDHCP = false;
+      networking.interfaces.eth1 = {
+        ipv4.addresses = [
+          {
+            address = "192.168.0.1";
+            prefixLength = 24;
+          }
+          {
+            address = "216.58.211.111";
+            prefixLength = 24;
+          }
+        ];
+      };
+    };
+
+    peer1 = { lib, ... }: {
+      networking.useDHCP = false;
+      networking.interfaces.eth1 = {
+        ipv4.addresses = [
+          {
+            address = "192.168.0.2";
+            prefixLength = 24;
+          }
+          {
+            address = "216.58.211.112";
+            prefixLength = 24;
+          }
+        ];
+      };
+      # test that binding to [::] is working when ipv6 is disabled
+      networking.enableIPv6 = false;
+      services._3proxy = {
+        enable = true;
+        services = [
+          {
+            type = "admin";
+            bindPort = 9999;
+            auth = [ "none" ];
+          }
+          {
+            type = "proxy";
+            bindPort = 3128;
+            auth = [ "none" ];
+          }
+        ];
+      };
+      networking.firewall.allowedTCPPorts = [ 3128 9999 ];
+    };
+
+    peer2 = { lib, ... }: {
+      networking.useDHCP = false;
+      networking.interfaces.eth1 = {
+        ipv4.addresses = [
+          {
+            address = "192.168.0.3";
+            prefixLength = 24;
+          }
+          {
+            address = "216.58.211.113";
+            prefixLength = 24;
+          }
+        ];
+      };
+      services._3proxy = {
+        enable = true;
+        services = [
+          {
+            type = "admin";
+            bindPort = 9999;
+            auth = [ "none" ];
+          }
+          {
+            type = "proxy";
+            bindPort = 3128;
+            auth = [ "iponly" ];
+            acl = [
+              {
+                rule = "allow";
+              }
+            ];
+          }
+        ];
+      };
+      networking.firewall.allowedTCPPorts = [ 3128 9999 ];
+    };
+
+    peer3 = { lib, ... }: {
+      networking.useDHCP = false;
+      networking.interfaces.eth1 = {
+        ipv4.addresses = [
+          {
+            address = "192.168.0.4";
+            prefixLength = 24;
+          }
+          {
+            address = "216.58.211.114";
+            prefixLength = 24;
+          }
+        ];
+      };
+      services._3proxy = {
+        enable = true;
+        usersFile = pkgs.writeText "3proxy.passwd" ''
+          admin:CR:$1$.GUV4Wvk$WnEVQtaqutD9.beO5ar1W/
+        '';
+        services = [
+          {
+            type = "admin";
+            bindPort = 9999;
+            auth = [ "none" ];
+          }
+          {
+            type = "proxy";
+            bindPort = 3128;
+            auth = [ "strong" ];
+            acl = [
+              {
+                rule = "allow";
+              }
+            ];
+          }
+        ];
+      };
+      networking.firewall.allowedTCPPorts = [ 3128 9999 ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    peer0.wait_for_unit("network-online.target")
+
+    peer1.wait_for_unit("3proxy.service")
+    peer1.wait_for_open_port("9999")
+
+    # test none auth
+    peer0.succeed(
+        "${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.2:3128 -S -O /dev/null http://216.58.211.112:9999"
+    )
+    peer0.succeed(
+        "${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.2:3128 -S -O /dev/null http://192.168.0.2:9999"
+    )
+    peer0.succeed(
+        "${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.2:3128 -S -O /dev/null http://127.0.0.1:9999"
+    )
+
+    peer2.wait_for_unit("3proxy.service")
+    peer2.wait_for_open_port("9999")
+
+    # test iponly auth
+    peer0.succeed(
+        "${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.3:3128 -S -O /dev/null http://216.58.211.113:9999"
+    )
+    peer0.fail(
+        "${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.3:3128 -S -O /dev/null http://192.168.0.3:9999"
+    )
+    peer0.fail(
+        "${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.3:3128 -S -O /dev/null http://127.0.0.1:9999"
+    )
+
+    peer3.wait_for_unit("3proxy.service")
+    peer3.wait_for_open_port("9999")
+
+    # test strong auth
+    peer0.succeed(
+        "${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://admin:bigsecret\@192.168.0.4:3128 -S -O /dev/null http://216.58.211.114:9999"
+    )
+    peer0.fail(
+        "${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://admin:bigsecret\@192.168.0.4:3128 -S -O /dev/null http://192.168.0.4:9999"
+    )
+    peer0.fail(
+        "${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.4:3128 -S -O /dev/null http://216.58.211.114:9999"
+    )
+    peer0.fail(
+        "${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.4:3128 -S -O /dev/null http://192.168.0.4:9999"
+    )
+    peer0.fail(
+        "${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.4:3128 -S -O /dev/null http://127.0.0.1:9999"
+    )
+  '';
+})
diff --git a/nixos/tests/acme.nix b/nixos/tests/acme.nix
new file mode 100644
index 00000000000..2dd06a50f40
--- /dev/null
+++ b/nixos/tests/acme.nix
@@ -0,0 +1,597 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: let
+  commonConfig = ./common/acme/client;
+
+  dnsServerIP = nodes: nodes.dnsserver.config.networking.primaryIPAddress;
+
+  dnsScript = nodes: let
+    dnsAddress = dnsServerIP nodes;
+  in pkgs.writeShellScript "dns-hook.sh" ''
+    set -euo pipefail
+    echo '[INFO]' "[$2]" 'dns-hook.sh' $*
+    if [ "$1" = "present" ]; then
+      ${pkgs.curl}/bin/curl --data '{"host": "'"$2"'", "value": "'"$3"'"}' http://${dnsAddress}:8055/set-txt
+    else
+      ${pkgs.curl}/bin/curl --data '{"host": "'"$2"'"}' http://${dnsAddress}:8055/clear-txt
+    fi
+  '';
+
+  dnsConfig = nodes: {
+    dnsProvider = "exec";
+    dnsPropagationCheck = false;
+    credentialsFile = pkgs.writeText "wildcard.env" ''
+      EXEC_PATH=${dnsScript nodes}
+      EXEC_POLLING_INTERVAL=1
+      EXEC_PROPAGATION_TIMEOUT=1
+      EXEC_SEQUENCE_INTERVAL=1
+    '';
+  };
+
+  documentRoot = pkgs.runCommand "docroot" {} ''
+    mkdir -p "$out"
+    echo hello world > "$out/index.html"
+  '';
+
+  vhostBase = {
+    forceSSL = true;
+    locations."/".root = documentRoot;
+  };
+
+  vhostBaseHttpd = {
+    forceSSL = true;
+    inherit documentRoot;
+  };
+
+  # Base specialisation config for testing general ACME features
+  webserverBasicConfig = {
+    services.nginx.enable = true;
+    services.nginx.virtualHosts."a.example.test" = vhostBase // {
+      enableACME = true;
+    };
+  };
+
+  # Generate specialisations for testing a web server
+  mkServerConfigs = { server, group, vhostBaseData, extraConfig ? {} }: let
+    baseConfig = { nodes, config, specialConfig ? {} }: lib.mkMerge [
+      {
+        security.acme = {
+          defaults = (dnsConfig nodes);
+          # One manual wildcard cert
+          certs."example.test" = {
+            domain = "*.example.test";
+          };
+        };
+
+        users.users."${config.services."${server}".user}".extraGroups = ["acme"];
+
+        services."${server}" = {
+          enable = true;
+          virtualHosts = {
+            # Run-of-the-mill vhost using HTTP-01 validation
+            "${server}-http.example.test" = vhostBaseData // {
+              serverAliases = [ "${server}-http-alias.example.test" ];
+              enableACME = true;
+            };
+
+            # Another which inherits the DNS-01 config
+            "${server}-dns.example.test" = vhostBaseData // {
+              serverAliases = [ "${server}-dns-alias.example.test" ];
+              enableACME = true;
+              # Set acmeRoot to null instead of using the default of "/var/lib/acme/acme-challenge"
+              # webroot + dnsProvider are mutually exclusive.
+              acmeRoot = null;
+            };
+
+            # One using the wildcard certificate
+            "${server}-wildcard.example.test" = vhostBaseData // {
+              serverAliases = [ "${server}-wildcard-alias.example.test" ];
+              useACMEHost = "example.test";
+            };
+          };
+        };
+
+        # Used to determine if service reload was triggered
+        systemd.targets."test-renew-${server}" = {
+          wants = [ "acme-${server}-http.example.test.service" ];
+          after = [ "acme-${server}-http.example.test.service" "${server}-config-reload.service" ];
+        };
+      }
+      specialConfig
+      extraConfig
+    ];
+  in {
+    "${server}".configuration = { nodes, config, ... }: baseConfig {
+      inherit nodes config;
+    };
+
+    # Test that server reloads when an alias is removed (and subsequently test removal works in acme)
+    "${server}-remove-alias".configuration = { nodes, config, ... }: baseConfig {
+      inherit nodes config;
+      specialConfig = {
+        # Remove an alias, but create a standalone vhost in its place for testing.
+        # This configuration results in certificate errors as useACMEHost does not imply
+        # append extraDomains, and thus we can validate the SAN is removed.
+        services."${server}" = {
+          virtualHosts."${server}-http.example.test".serverAliases = lib.mkForce [];
+          virtualHosts."${server}-http-alias.example.test" = vhostBaseData // {
+            useACMEHost = "${server}-http.example.test";
+          };
+        };
+      };
+    };
+
+    # Test that the server reloads when only the acme configuration is changed.
+    "${server}-change-acme-conf".configuration = { nodes, config, ... }: baseConfig {
+      inherit nodes config;
+      specialConfig = {
+        security.acme.certs."${server}-http.example.test" = {
+          keyType = "ec384";
+          # Also test that postRun is exec'd as root
+          postRun = "id | grep root";
+        };
+      };
+    };
+  };
+
+in {
+  name = "acme";
+  meta.maintainers = lib.teams.acme.members;
+
+  nodes = {
+    # The fake ACME server which will respond to client requests
+    acme = { nodes, ... }: {
+      imports = [ ./common/acme/server ];
+      networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
+    };
+
+    # A fake DNS server which can be configured with records as desired
+    # Used to test DNS-01 challenge
+    dnsserver = { nodes, ... }: {
+      networking.firewall.allowedTCPPorts = [ 8055 53 ];
+      networking.firewall.allowedUDPPorts = [ 53 ];
+      systemd.services.pebble-challtestsrv = {
+        enable = true;
+        description = "Pebble ACME challenge test server";
+        wantedBy = [ "network.target" ];
+        serviceConfig = {
+          ExecStart = "${pkgs.pebble}/bin/pebble-challtestsrv -dns01 ':53' -defaultIPv6 '' -defaultIPv4 '${nodes.webserver.config.networking.primaryIPAddress}'";
+          # Required to bind on privileged ports.
+          AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        };
+      };
+    };
+
+    # A web server which will be the node requesting certs
+    webserver = { nodes, config, ... }: {
+      imports = [ commonConfig ];
+      networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
+      networking.firewall.allowedTCPPorts = [ 80 443 ];
+
+      # OpenSSL will be used for more thorough certificate validation
+      environment.systemPackages = [ pkgs.openssl ];
+
+      # Set log level to info so that we can see when the service is reloaded
+      services.nginx.logError = "stderr info";
+
+      specialisation = {
+        # First derivation used to test general ACME features
+        general.configuration = { ... }: let
+          caDomain = nodes.acme.config.test-support.acme.caDomain;
+          email = config.security.acme.defaults.email;
+          # Exit 99 to make it easier to track if this is the reason a renew failed
+          accountCreateTester = ''
+            test -e accounts/${caDomain}/${email}/account.json || exit 99
+          '';
+        in lib.mkMerge [
+          webserverBasicConfig
+          {
+            # Used to test that account creation is collated into one service.
+            # These should not run until after acme-finished-a.example.test.target
+            systemd.services."b.example.test".preStart = accountCreateTester;
+            systemd.services."c.example.test".preStart = accountCreateTester;
+
+            services.nginx.virtualHosts."b.example.test" = vhostBase // {
+              enableACME = true;
+            };
+            services.nginx.virtualHosts."c.example.test" = vhostBase // {
+              enableACME = true;
+            };
+          }
+        ];
+
+        # Test OCSP Stapling
+        ocsp-stapling.configuration = { ... }: lib.mkMerge [
+          webserverBasicConfig
+          {
+            security.acme.certs."a.example.test".ocspMustStaple = true;
+            services.nginx.virtualHosts."a.example.test" = {
+              extraConfig = ''
+                ssl_stapling on;
+                ssl_stapling_verify on;
+              '';
+            };
+          }
+        ];
+
+        # Validate service relationships by adding a slow start service to nginx' wants.
+        # Reproducer for https://github.com/NixOS/nixpkgs/issues/81842
+        slow-startup.configuration = { ... }: lib.mkMerge [
+          webserverBasicConfig
+          {
+            systemd.services.my-slow-service = {
+              wantedBy = [ "multi-user.target" "nginx.service" ];
+              before = [ "nginx.service" ];
+              preStart = "sleep 5";
+              script = "${pkgs.python3}/bin/python -m http.server";
+            };
+
+            services.nginx.virtualHosts."slow.example.test" = {
+              forceSSL = true;
+              enableACME = true;
+              locations."/".proxyPass = "http://localhost:8000";
+            };
+          }
+        ];
+
+        # Test lego internal server (listenHTTP option)
+        # Also tests useRoot option
+        lego-server.configuration = { ... }: {
+          security.acme.useRoot = true;
+          security.acme.certs."lego.example.test" = {
+            listenHTTP = ":80";
+            group = "nginx";
+          };
+          services.nginx.enable = true;
+          services.nginx.virtualHosts."lego.example.test" = {
+            useACMEHost = "lego.example.test";
+            onlySSL = true;
+          };
+        };
+
+      # Test compatiblity with Caddy
+      # It only supports useACMEHost, hence not using mkServerConfigs
+      } // (let
+        baseCaddyConfig = { nodes, config, ... }: {
+          security.acme = {
+            defaults = (dnsConfig nodes);
+            # One manual wildcard cert
+            certs."example.test" = {
+              domain = "*.example.test";
+            };
+          };
+
+          users.users."${config.services.caddy.user}".extraGroups = ["acme"];
+
+          services.caddy = {
+            enable = true;
+            virtualHosts."a.exmaple.test" = {
+              useACMEHost = "example.test";
+              extraConfig = ''
+                root * ${documentRoot}
+              '';
+            };
+          };
+        };
+      in {
+        caddy.configuration = baseCaddyConfig;
+
+        # Test that the server reloads when only the acme configuration is changed.
+        "caddy-change-acme-conf".configuration = { nodes, config, ... }: lib.mkMerge [
+          (baseCaddyConfig {
+            inherit nodes config;
+          })
+          {
+            security.acme.certs."example.test" = {
+              keyType = "ec384";
+            };
+          }
+        ];
+
+      # Test compatibility with Nginx
+      }) // (mkServerConfigs {
+          server = "nginx";
+          group = "nginx";
+          vhostBaseData = vhostBase;
+        })
+
+      # Test compatibility with Apache HTTPD
+        // (mkServerConfigs {
+          server = "httpd";
+          group = "wwwrun";
+          vhostBaseData = vhostBaseHttpd;
+          extraConfig = {
+            services.httpd.adminAddr = config.security.acme.defaults.email;
+          };
+        });
+    };
+
+    # The client will be used to curl the webserver to validate configuration
+    client = { nodes, ... }: {
+      imports = [ commonConfig ];
+      networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
+
+      # OpenSSL will be used for more thorough certificate validation
+      environment.systemPackages = [ pkgs.openssl ];
+    };
+  };
+
+  testScript = { nodes, ... }:
+    let
+      caDomain = nodes.acme.config.test-support.acme.caDomain;
+      newServerSystem = nodes.webserver.config.system.build.toplevel;
+      switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test";
+    in
+    # Note, wait_for_unit does not work for oneshot services that do not have RemainAfterExit=true,
+    # this is because a oneshot goes from inactive => activating => inactive, and never
+    # reaches the active state. Targets do not have this issue.
+    ''
+      import time
+
+
+      def switch_to(node, name):
+          # On first switch, this will create a symlink to the current system so that we can
+          # quickly switch between derivations
+          root_specs = "/tmp/specialisation"
+          node.execute(
+            f"test -e {root_specs}"
+            f" || ln -s $(readlink /run/current-system)/specialisation {root_specs}"
+          )
+
+          switcher_path = f"/run/current-system/specialisation/{name}/bin/switch-to-configuration"
+          rc, _ = node.execute(f"test -e '{switcher_path}'")
+          if rc > 0:
+              switcher_path = f"/tmp/specialisation/{name}/bin/switch-to-configuration"
+
+          node.succeed(
+              f"{switcher_path} test"
+          )
+
+
+      # Ensures the issuer of our cert matches the chain
+      # and matches the issuer we expect it to be.
+      # It's a good validation to ensure the cert.pem and fullchain.pem
+      # are not still selfsigned afer verification
+      def check_issuer(node, cert_name, issuer):
+          for fname in ("cert.pem", "fullchain.pem"):
+              actual_issuer = node.succeed(
+                  f"openssl x509 -noout -issuer -in /var/lib/acme/{cert_name}/{fname}"
+              ).partition("=")[2]
+              print(f"{fname} issuer: {actual_issuer}")
+              assert issuer.lower() in actual_issuer.lower()
+
+
+      # Ensure cert comes before chain in fullchain.pem
+      def check_fullchain(node, cert_name):
+          subject_data = node.succeed(
+              f"openssl crl2pkcs7 -nocrl -certfile /var/lib/acme/{cert_name}/fullchain.pem"
+              " | openssl pkcs7 -print_certs -noout"
+          )
+          for line in subject_data.lower().split("\n"):
+              if "subject" in line:
+                  print(f"First subject in fullchain.pem: {line}")
+                  assert cert_name.lower() in line
+                  return
+
+          assert False
+
+
+      def check_connection(node, domain, retries=3):
+          assert retries >= 0, f"Failed to connect to https://{domain}"
+
+          result = node.succeed(
+              "openssl s_client -brief -verify 2 -CAfile /tmp/ca.crt"
+              f" -servername {domain} -connect {domain}:443 < /dev/null 2>&1"
+          )
+
+          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)
+
+
+      def check_connection_key_bits(node, domain, bits, retries=3):
+          assert retries >= 0, f"Did not find expected number of bits ({bits}) in key"
+
+          result = node.succeed(
+              "openssl s_client -CAfile /tmp/ca.crt"
+              f" -servername {domain} -connect {domain}:443 < /dev/null"
+              " | openssl x509 -noout -text | grep -i Public-Key"
+          )
+          print("Key type:", result)
+
+          if bits not in result:
+              time.sleep(3)
+              return check_connection_key_bits(node, domain, bits, retries - 1)
+
+
+      def check_stapling(node, domain, retries=3):
+          assert retries >= 0, "OCSP Stapling check failed"
+
+          # Pebble doesn't provide a full OCSP responder, so just check the URL
+          result = node.succeed(
+              "openssl s_client -CAfile /tmp/ca.crt"
+              f" -servername {domain} -connect {domain}:443 < /dev/null"
+              " | openssl x509 -noout -ocsp_uri"
+          )
+          print("OCSP Responder URL:", result)
+
+          if "${caDomain}:4002" not in result.lower():
+              time.sleep(3)
+              return check_stapling(node, domain, retries - 1)
+
+
+      def download_ca_certs(node, retries=5):
+          assert retries >= 0, "Failed to connect to pebble to download root CA certs"
+
+          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)
+
+
+      start_all()
+
+      dnsserver.wait_for_unit("pebble-challtestsrv.service")
+      client.wait_for_unit("default.target")
+
+      client.succeed(
+          'curl --data \'{"host": "${caDomain}", "addresses": ["${nodes.acme.config.networking.primaryIPAddress}"]}\' http://${dnsServerIP nodes}:8055/add-a'
+      )
+
+      acme.wait_for_unit("network-online.target")
+      acme.wait_for_unit("pebble.service")
+
+      download_ca_certs(client)
+
+      # Perform general tests first
+      switch_to(webserver, "general")
+
+      with subtest("Can request certificate with HTTP-01 challenge"):
+          webserver.wait_for_unit("acme-finished-a.example.test.target")
+          check_fullchain(webserver, "a.example.test")
+          check_issuer(webserver, "a.example.test", "pebble")
+          webserver.wait_for_unit("nginx.service")
+          check_connection(client, "a.example.test")
+
+      with subtest("Runs 1 cert for account creation before others"):
+          webserver.wait_for_unit("acme-finished-b.example.test.target")
+          webserver.wait_for_unit("acme-finished-c.example.test.target")
+          check_connection(client, "b.example.test")
+          check_connection(client, "c.example.test")
+
+      with subtest("Certificates and accounts have safe + valid permissions"):
+          # Nginx will set the group appropriately when enableACME is used
+          group = "nginx"
+          webserver.succeed(
+              f"test $(stat -L -c '%a %U %G' /var/lib/acme/a.example.test/*.pem | tee /dev/stderr | grep '640 acme {group}' | wc -l) -eq 5"
+          )
+          webserver.succeed(
+              f"test $(stat -L -c '%a %U %G' /var/lib/acme/.lego/a.example.test/**/a.example.test* | tee /dev/stderr | grep '600 acme {group}' | wc -l) -eq 4"
+          )
+          webserver.succeed(
+              f"test $(stat -L -c '%a %U %G' /var/lib/acme/a.example.test | tee /dev/stderr | grep '750 acme {group}' | wc -l) -eq 1"
+          )
+          webserver.succeed(
+              f"test $(find /var/lib/acme/accounts -type f -exec stat -L -c '%a %U %G' {{}} \\; | tee /dev/stderr | grep -v '600 acme {group}' | wc -l) -eq 0"
+          )
+
+      # Selfsigned certs tests happen late so we aren't fighting the system init triggering cert renewal
+      with subtest("Can generate valid selfsigned certs"):
+          webserver.succeed("systemctl clean acme-a.example.test.service --what=state")
+          webserver.succeed("systemctl start acme-selfsigned-a.example.test.service")
+          check_fullchain(webserver, "a.example.test")
+          check_issuer(webserver, "a.example.test", "minica")
+          # Check selfsigned permissions
+          webserver.succeed(
+              f"test $(stat -L -c '%a %U %G' /var/lib/acme/a.example.test/*.pem | tee /dev/stderr | grep '640 acme {group}' | wc -l) -eq 5"
+          )
+          # Will succeed if nginx can load the certs
+          webserver.succeed("systemctl start nginx-config-reload.service")
+
+      with subtest("Correctly implements OCSP stapling"):
+          switch_to(webserver, "ocsp-stapling")
+          webserver.wait_for_unit("acme-finished-a.example.test.target")
+          check_stapling(client, "a.example.test")
+
+      with subtest("Can request certificate with HTTP-01 using lego's internal web server"):
+          switch_to(webserver, "lego-server")
+          webserver.wait_for_unit("acme-finished-lego.example.test.target")
+          webserver.wait_for_unit("nginx.service")
+          webserver.succeed("echo HENLO && systemctl cat nginx.service")
+          webserver.succeed("test \"$(stat -c '%U' /var/lib/acme/* | uniq)\" = \"root\"")
+          check_connection(client, "a.example.test")
+          check_connection(client, "lego.example.test")
+
+      with subtest("Can request certificate with HTTP-01 when nginx startup is delayed"):
+          webserver.execute("systemctl stop nginx")
+          switch_to(webserver, "slow-startup")
+          webserver.wait_for_unit("acme-finished-slow.example.test.target")
+          check_issuer(webserver, "slow.example.test", "pebble")
+          webserver.wait_for_unit("nginx.service")
+          check_connection(client, "slow.example.test")
+
+      with subtest("Works with caddy"):
+          switch_to(webserver, "caddy")
+          webserver.wait_for_unit("acme-finished-example.test.target")
+          webserver.wait_for_unit("caddy.service")
+          # FIXME reloading caddy is not sufficient to load new certs.
+          # Restart it manually until this is fixed.
+          webserver.succeed("systemctl restart caddy.service")
+          check_connection(client, "a.example.test")
+
+      with subtest("security.acme changes reflect on caddy"):
+          switch_to(webserver, "caddy-change-acme-conf")
+          webserver.wait_for_unit("acme-finished-example.test.target")
+          webserver.wait_for_unit("caddy.service")
+          # FIXME reloading caddy is not sufficient to load new certs.
+          # Restart it manually until this is fixed.
+          webserver.succeed("systemctl restart caddy.service")
+          check_connection_key_bits(client, "a.example.test", "384")
+
+      domains = ["http", "dns", "wildcard"]
+      for server, logsrc in [
+          ("nginx", "journalctl -n 30 -u nginx.service"),
+          ("httpd", "tail -n 30 /var/log/httpd/*.log"),
+      ]:
+          wait_for_server = lambda: webserver.wait_for_unit(f"{server}.service")
+          with subtest(f"Works with {server}"):
+              try:
+                  switch_to(webserver, server)
+                  # Skip wildcard domain for this check ([:-1])
+                  for domain in domains[:-1]:
+                      webserver.wait_for_unit(
+                          f"acme-finished-{server}-{domain}.example.test.target"
+                      )
+              except Exception as err:
+                  _, output = webserver.execute(
+                      f"{logsrc} && ls -al /var/lib/acme/acme-challenge"
+                  )
+                  print(output)
+                  raise err
+
+              wait_for_server()
+
+              for domain in domains[:-1]:
+                  check_issuer(webserver, f"{server}-{domain}.example.test", "pebble")
+              for domain in domains:
+                  check_connection(client, f"{server}-{domain}.example.test")
+                  check_connection(client, f"{server}-{domain}-alias.example.test")
+
+          test_domain = f"{server}-{domains[0]}.example.test"
+
+          with subtest(f"Can reload {server} when timer triggers renewal"):
+              # Switch to selfsigned first
+              webserver.succeed(f"systemctl clean acme-{test_domain}.service --what=state")
+              webserver.succeed(f"systemctl start acme-selfsigned-{test_domain}.service")
+              check_issuer(webserver, test_domain, "minica")
+              webserver.succeed(f"systemctl start {server}-config-reload.service")
+              webserver.succeed(f"systemctl start test-renew-{server}.target")
+              check_issuer(webserver, test_domain, "pebble")
+              check_connection(client, test_domain)
+
+          with subtest("Can remove an alias from a domain + cert is updated"):
+              test_alias = f"{server}-{domains[0]}-alias.example.test"
+              switch_to(webserver, f"{server}-remove-alias")
+              webserver.wait_for_unit(f"acme-finished-{test_domain}.target")
+              wait_for_server()
+              check_connection(client, test_domain)
+              rc, _ = client.execute(
+                  f"openssl s_client -CAfile /tmp/ca.crt -connect {test_alias}:443"
+                  " </dev/null 2>/dev/null | openssl x509 -noout -text"
+                  f" | grep DNS: | grep {test_alias}"
+              )
+              assert rc > 0, "Removed extraDomainName was not removed from the cert"
+
+          with subtest("security.acme changes reflect on web server"):
+              # Switch back to normal server config first, reset everything.
+              switch_to(webserver, server)
+              wait_for_server()
+              switch_to(webserver, f"{server}-change-acme-conf")
+              webserver.wait_for_unit(f"acme-finished-{test_domain}.target")
+              wait_for_server()
+              check_connection_key_bits(client, test_domain, "384")
+    '';
+})
diff --git a/nixos/tests/adguardhome.nix b/nixos/tests/adguardhome.nix
new file mode 100644
index 00000000000..ddbe8ff9c11
--- /dev/null
+++ b/nixos/tests/adguardhome.nix
@@ -0,0 +1,57 @@
+import ./make-test-python.nix {
+  name = "adguardhome";
+
+  nodes = {
+    minimalConf = { ... }: {
+      services.adguardhome = { enable = true; };
+    };
+
+    declarativeConf = { ... }: {
+      services.adguardhome = {
+        enable = true;
+
+        mutableSettings = false;
+        settings = {
+          dns = {
+            bind_host = "0.0.0.0";
+            bootstrap_dns = "127.0.0.1";
+          };
+        };
+      };
+    };
+
+    mixedConf = { ... }: {
+      services.adguardhome = {
+        enable = true;
+
+        mutableSettings = true;
+        settings = {
+          dns = {
+            bind_host = "0.0.0.0";
+            bootstrap_dns = "127.0.0.1";
+          };
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    with subtest("Minimal config test"):
+        minimalConf.wait_for_unit("adguardhome.service")
+        minimalConf.wait_for_open_port(3000)
+
+    with subtest("Declarative config test, DNS will be reachable"):
+        declarativeConf.wait_for_unit("adguardhome.service")
+        declarativeConf.wait_for_open_port(53)
+        declarativeConf.wait_for_open_port(3000)
+
+    with subtest("Mixed config test, check whether merging works"):
+        mixedConf.wait_for_unit("adguardhome.service")
+        mixedConf.wait_for_open_port(53)
+        mixedConf.wait_for_open_port(3000)
+        # Test whether merging works properly, even if nothing is changed
+        mixedConf.systemctl("restart adguardhome.service")
+        mixedConf.wait_for_unit("adguardhome.service")
+        mixedConf.wait_for_open_port(3000)
+  '';
+}
diff --git a/nixos/tests/aesmd.nix b/nixos/tests/aesmd.nix
new file mode 100644
index 00000000000..59c04fe7e96
--- /dev/null
+++ b/nixos/tests/aesmd.nix
@@ -0,0 +1,62 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "aesmd";
+  meta = {
+    maintainers = with lib.maintainers; [ veehaitch ];
+  };
+
+  machine = { lib, ... }: {
+    services.aesmd = {
+      enable = true;
+      settings = {
+        defaultQuotingType = "ecdsa_256";
+        proxyType = "direct";
+        whitelistUrl = "http://nixos.org";
+      };
+    };
+
+    # Should have access to the AESM socket
+    users.users."sgxtest" = {
+      isNormalUser = true;
+      extraGroups = [ "sgx" ];
+    };
+
+    # Should NOT have access to the AESM socket
+    users.users."nosgxtest".isNormalUser = true;
+
+    # We don't have a real SGX machine in NixOS tests
+    systemd.services.aesmd.unitConfig.AssertPathExists = lib.mkForce [ ];
+  };
+
+  testScript = ''
+    with subtest("aesmd.service starts"):
+      machine.wait_for_unit("aesmd.service")
+      status, main_pid = machine.systemctl("show --property MainPID --value aesmd.service")
+      assert status == 0, "Could not get MainPID of aesmd.service"
+      main_pid = main_pid.strip()
+
+    with subtest("aesmd.service runtime directory permissions"):
+      runtime_dir = "/run/aesmd";
+      res = machine.succeed(f"stat -c '%a %U %G' {runtime_dir}").strip()
+      assert "750 aesmd sgx" == res, f"{runtime_dir} does not have the expected permissions: {res}"
+
+    with subtest("aesm.socket available on host"):
+      socket_path = "/var/run/aesmd/aesm.socket"
+      machine.wait_until_succeeds(f"test -S {socket_path}")
+      machine.succeed(f"test 777 -eq $(stat -c '%a' {socket_path})")
+      for op in [ "-r", "-w", "-x" ]:
+        machine.succeed(f"sudo -u sgxtest test {op} {socket_path}")
+        machine.fail(f"sudo -u nosgxtest test {op} {socket_path}")
+
+    with subtest("Copies white_list_cert_to_be_verify.bin"):
+      whitelist_path = "/var/opt/aesmd/data/white_list_cert_to_be_verify.bin"
+      whitelist_perms = machine.succeed(
+        f"nsenter -m -t {main_pid} ${pkgs.coreutils}/bin/stat -c '%a' {whitelist_path}"
+      ).strip()
+      assert "644" == whitelist_perms, f"white_list_cert_to_be_verify.bin has permissions {whitelist_perms}"
+
+    with subtest("Writes and binds aesm.conf in service namespace"):
+      aesmd_config = machine.succeed(f"nsenter -m -t {main_pid} ${pkgs.coreutils}/bin/cat /etc/aesmd.conf")
+
+      assert aesmd_config == "whitelist url = http://nixos.org\nproxy type = direct\ndefault quoting type = ecdsa_256\n", "aesmd.conf differs"
+  '';
+})
diff --git a/nixos/tests/agda.nix b/nixos/tests/agda.nix
new file mode 100644
index 00000000000..ec61af2afe7
--- /dev/null
+++ b/nixos/tests/agda.nix
@@ -0,0 +1,50 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+  hello-world = pkgs.writeText "hello-world" ''
+    {-# OPTIONS --guardedness #-}
+    open import IO
+    open import Level
+
+    main = run {0â„“} (putStrLn "Hello World!")
+  '';
+in
+{
+  name = "agda";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ alexarice turion ];
+  };
+
+  machine = { pkgs, ... }: {
+    environment.systemPackages = [
+      (pkgs.agda.withPackages {
+        pkgs = p: [ p.standard-library ];
+      })
+    ];
+    virtualisation.memorySize = 2000; # Agda uses a lot of memory
+  };
+
+  testScript = ''
+    assert (
+        "${pkgs.agdaPackages.lib.interfaceFile "Everything.agda"}" == "Everything.agdai"
+    ), "wrong interface file for Everything.agda"
+    assert (
+        "${pkgs.agdaPackages.lib.interfaceFile "tmp/Everything.agda.md"}" == "tmp/Everything.agdai"
+    ), "wrong interface file for tmp/Everything.agda.md"
+
+    # Minimal script that typechecks
+    machine.succeed("touch TestEmpty.agda")
+    machine.succeed("agda TestEmpty.agda")
+
+    # Hello world
+    machine.succeed(
+        "cp ${hello-world} HelloWorld.agda"
+    )
+    machine.succeed("agda -l standard-library -i . -c HelloWorld.agda")
+    # Check execution
+    assert "Hello World!" in machine.succeed(
+        "./HelloWorld"
+    ), "HelloWorld does not run properly"
+  '';
+}
+)
diff --git a/nixos/tests/airsonic.nix b/nixos/tests/airsonic.nix
new file mode 100644
index 00000000000..d8df092c2ec
--- /dev/null
+++ b/nixos/tests/airsonic.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "airsonic";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ sumnerevans ];
+  };
+
+  machine =
+    { pkgs, ... }:
+    {
+      services.airsonic = {
+        enable = true;
+        maxMemory = 800;
+      };
+    };
+
+  testScript = ''
+    def airsonic_is_up(_) -> bool:
+        return machine.succeed("curl --fail http://localhost:4040/login")
+
+
+    machine.start()
+    machine.wait_for_unit("airsonic.service")
+    machine.wait_for_open_port(4040)
+
+    with machine.nested("Waiting for UI to work"):
+        retry(airsonic_is_up)
+  '';
+})
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
new file mode 100644
index 00000000000..474bb423379
--- /dev/null
+++ b/nixos/tests/all-tests.nix
@@ -0,0 +1,590 @@
+{ system, pkgs, callTest }:
+# The return value of this function will be an attrset with arbitrary depth and
+# the `anything` returned by callTest at its test leafs.
+# The tests not supported by `system` will be replaced with `{}`, so that
+# `passthru.tests` can contain links to those without breaking on architectures
+# where said tests are unsupported.
+# Example callTest that just extracts the derivation from the test:
+#   callTest = t: t.test;
+
+with pkgs.lib;
+
+let
+  discoverTests = val:
+    if !isAttrs val then val
+    else if hasAttr "test" val then callTest val
+    else mapAttrs (n: s: discoverTests s) val;
+  handleTest = path: args:
+    discoverTests (import path ({ inherit system pkgs; } // args));
+  handleTestOn = systems: path: args:
+    if elem system systems then handleTest path args
+    else {};
+
+  nixosLib = import ../lib {
+    # Experimental features need testing too, but there's no point in warning
+    # about it, so we enable the feature flag.
+    featureFlags.minimalModules = {};
+  };
+  evalMinimalConfig = module: nixosLib.evalModules { modules = [ module ]; };
+in
+{
+  _3proxy = handleTest ./3proxy.nix {};
+  acme = handleTest ./acme.nix {};
+  adguardhome = handleTest ./adguardhome.nix {};
+  aesmd = handleTest ./aesmd.nix {};
+  agate = handleTest ./web-servers/agate.nix {};
+  agda = handleTest ./agda.nix {};
+  airsonic = handleTest ./airsonic.nix {};
+  amazon-init-shell = handleTest ./amazon-init-shell.nix {};
+  apfs = handleTest ./apfs.nix {};
+  apparmor = handleTest ./apparmor.nix {};
+  atd = handleTest ./atd.nix {};
+  atop = handleTest ./atop.nix {};
+  avahi = handleTest ./avahi.nix {};
+  avahi-with-resolved = handleTest ./avahi.nix { networkd = true; };
+  babeld = handleTest ./babeld.nix {};
+  bazarr = handleTest ./bazarr.nix {};
+  bcachefs = handleTestOn ["x86_64-linux" "aarch64-linux"] ./bcachefs.nix {};
+  beanstalkd = handleTest ./beanstalkd.nix {};
+  bees = handleTest ./bees.nix {};
+  bind = handleTest ./bind.nix {};
+  bird = handleTest ./bird.nix {};
+  bitcoind = handleTest ./bitcoind.nix {};
+  bittorrent = handleTest ./bittorrent.nix {};
+  blockbook-frontend = handleTest ./blockbook-frontend.nix {};
+  blocky = handleTest ./blocky.nix {};
+  boot = handleTestOn ["x86_64-linux" "aarch64-linux"] ./boot.nix {};
+  boot-stage1 = handleTest ./boot-stage1.nix {};
+  borgbackup = handleTest ./borgbackup.nix {};
+  botamusique = handleTest ./botamusique.nix {};
+  bpf = handleTestOn ["x86_64-linux" "aarch64-linux"] ./bpf.nix {};
+  breitbandmessung = handleTest ./breitbandmessung.nix {};
+  brscan5 = handleTest ./brscan5.nix {};
+  btrbk = handleTest ./btrbk.nix {};
+  buildbot = handleTest ./buildbot.nix {};
+  buildkite-agents = handleTest ./buildkite-agents.nix {};
+  caddy = handleTest ./caddy.nix {};
+  cadvisor = handleTestOn ["x86_64-linux"] ./cadvisor.nix {};
+  cage = handleTest ./cage.nix {};
+  cagebreak = handleTest ./cagebreak.nix {};
+  calibre-web = handleTest ./calibre-web.nix {};
+  cassandra_2_1 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_2_1; };
+  cassandra_2_2 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_2_2; };
+  cassandra_3_0 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_3_0; };
+  cassandra_3_11 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_3_11; };
+  ceph-multi-node = handleTestOn ["x86_64-linux"] ./ceph-multi-node.nix {};
+  ceph-single-node = handleTestOn ["x86_64-linux"] ./ceph-single-node.nix {};
+  ceph-single-node-bluestore = handleTestOn ["x86_64-linux"] ./ceph-single-node-bluestore.nix {};
+  certmgr = handleTest ./certmgr.nix {};
+  cfssl = handleTestOn ["x86_64-linux"] ./cfssl.nix {};
+  charliecloud = handleTest ./charliecloud.nix {};
+  chromium = (handleTestOn ["x86_64-linux"] ./chromium.nix {}).stable or {};
+  cjdns = handleTest ./cjdns.nix {};
+  clickhouse = handleTest ./clickhouse.nix {};
+  cloud-init = handleTest ./cloud-init.nix {};
+  cntr = handleTest ./cntr.nix {};
+  cockroachdb = handleTestOn ["x86_64-linux"] ./cockroachdb.nix {};
+  collectd = handleTest ./collectd.nix {};
+  consul = handleTest ./consul.nix {};
+  containers-bridge = handleTest ./containers-bridge.nix {};
+  containers-custom-pkgs.nix = handleTest ./containers-custom-pkgs.nix {};
+  containers-ephemeral = handleTest ./containers-ephemeral.nix {};
+  containers-extra_veth = handleTest ./containers-extra_veth.nix {};
+  containers-hosts = handleTest ./containers-hosts.nix {};
+  containers-imperative = handleTest ./containers-imperative.nix {};
+  containers-ip = handleTest ./containers-ip.nix {};
+  containers-macvlans = handleTest ./containers-macvlans.nix {};
+  containers-names = handleTest ./containers-names.nix {};
+  containers-nested = handleTest ./containers-nested.nix {};
+  containers-physical_interfaces = handleTest ./containers-physical_interfaces.nix {};
+  containers-portforward = handleTest ./containers-portforward.nix {};
+  containers-reloadable = handleTest ./containers-reloadable.nix {};
+  containers-restart_networking = handleTest ./containers-restart_networking.nix {};
+  containers-tmpfs = handleTest ./containers-tmpfs.nix {};
+  convos = handleTest ./convos.nix {};
+  corerad = handleTest ./corerad.nix {};
+  coturn = handleTest ./coturn.nix {};
+  couchdb = handleTest ./couchdb.nix {};
+  cri-o = handleTestOn ["x86_64-linux"] ./cri-o.nix {};
+  custom-ca = handleTest ./custom-ca.nix {};
+  croc = handleTest ./croc.nix {};
+  cryptpad = handleTest ./cryptpad.nix {};
+  deluge = handleTest ./deluge.nix {};
+  dendrite = handleTest ./dendrite.nix {};
+  dex-oidc = handleTest ./dex-oidc.nix {};
+  dhparams = handleTest ./dhparams.nix {};
+  disable-installer-tools = handleTest ./disable-installer-tools.nix {};
+  discourse = handleTest ./discourse.nix {};
+  dnscrypt-proxy2 = handleTestOn ["x86_64-linux"] ./dnscrypt-proxy2.nix {};
+  dnscrypt-wrapper = handleTestOn ["x86_64-linux"] ./dnscrypt-wrapper {};
+  dnsdist = handleTest ./dnsdist.nix {};
+  doas = handleTest ./doas.nix {};
+  docker = handleTestOn ["x86_64-linux"] ./docker.nix {};
+  docker-rootless = handleTestOn ["x86_64-linux"] ./docker-rootless.nix {};
+  docker-edge = handleTestOn ["x86_64-linux"] ./docker-edge.nix {};
+  docker-registry = handleTest ./docker-registry.nix {};
+  docker-tools = handleTestOn ["x86_64-linux"] ./docker-tools.nix {};
+  docker-tools-cross = handleTestOn ["x86_64-linux" "aarch64-linux"] ./docker-tools-cross.nix {};
+  docker-tools-overlay = handleTestOn ["x86_64-linux"] ./docker-tools-overlay.nix {};
+  documize = handleTest ./documize.nix {};
+  doh-proxy-rust = handleTest ./doh-proxy-rust.nix {};
+  dokuwiki = handleTest ./dokuwiki.nix {};
+  domination = handleTest ./domination.nix {};
+  dovecot = handleTest ./dovecot.nix {};
+  drbd = handleTest ./drbd.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 {};
+  ejabberd = handleTest ./xmpp/ejabberd.nix {};
+  elk = handleTestOn ["x86_64-linux"] ./elk.nix {};
+  emacs-daemon = handleTest ./emacs-daemon.nix {};
+  engelsystem = handleTest ./engelsystem.nix {};
+  enlightenment = handleTest ./enlightenment.nix {};
+  env = handleTest ./env.nix {};
+  ergo = handleTest ./ergo.nix {};
+  ergochat = handleTest ./ergochat.nix {};
+  etc = pkgs.callPackage ../modules/system/etc/test.nix { inherit evalMinimalConfig; };
+  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 {};
+  fancontrol = handleTest ./fancontrol.nix {};
+  fcitx = handleTest ./fcitx {};
+  fenics = handleTest ./fenics.nix {};
+  ferm = handleTest ./ferm.nix {};
+  firefox = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox; };
+  firefox-esr    = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr; }; # used in `tested` job
+  firefox-esr-91 = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr-91; };
+  firejail = handleTest ./firejail.nix {};
+  firewall = handleTest ./firewall.nix {};
+  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 {};
+  freeswitch = handleTest ./freeswitch.nix {};
+  frr = handleTest ./frr.nix {};
+  fsck = handleTest ./fsck.nix {};
+  ft2-clone = handleTest ./ft2-clone.nix {};
+  gerrit = handleTest ./gerrit.nix {};
+  geth = handleTest ./geth.nix {};
+  ghostunnel = handleTest ./ghostunnel.nix {};
+  gitdaemon = handleTest ./gitdaemon.nix {};
+  gitea = handleTest ./gitea.nix {};
+  gitlab = handleTest ./gitlab.nix {};
+  gitolite = handleTest ./gitolite.nix {};
+  gitolite-fcgiwrap = handleTest ./gitolite-fcgiwrap.nix {};
+  glusterfs = handleTest ./glusterfs.nix {};
+  gnome = handleTest ./gnome.nix {};
+  gnome-xorg = handleTest ./gnome-xorg.nix {};
+  go-neb = handleTest ./go-neb.nix {};
+  gobgpd = handleTest ./gobgpd.nix {};
+  gocd-agent = handleTest ./gocd-agent.nix {};
+  gocd-server = handleTest ./gocd-server.nix {};
+  google-oslogin = handleTest ./google-oslogin {};
+  gotify-server = handleTest ./gotify-server.nix {};
+  grafana = handleTest ./grafana.nix {};
+  graphite = handleTest ./graphite.nix {};
+  graylog = handleTest ./graylog.nix {};
+  grocy = handleTest ./grocy.nix {};
+  grub = handleTest ./grub.nix {};
+  gvisor = handleTest ./gvisor.nix {};
+  hadoop = import ./hadoop { inherit handleTestOn; package=pkgs.hadoop; };
+  hadoop_3_2 = import ./hadoop { inherit handleTestOn; package=pkgs.hadoop_3_2; };
+  hadoop2 = import ./hadoop { inherit handleTestOn; package=pkgs.hadoop2; };
+  haka = handleTest ./haka.nix {};
+  haproxy = handleTest ./haproxy.nix {};
+  hardened = handleTest ./hardened.nix {};
+  hedgedoc = handleTest ./hedgedoc.nix {};
+  herbstluftwm = handleTest ./herbstluftwm.nix {};
+  installed-tests = pkgs.recurseIntoAttrs (handleTest ./installed-tests {});
+  invidious = handleTest ./invidious.nix {};
+  oci-containers = handleTestOn ["x86_64-linux"] ./oci-containers.nix {};
+  odoo = handleTest ./odoo.nix {};
+  # 9pnet_virtio used to mount /nix partition doesn't support
+  # hibernation. This test happens to work on x86_64-linux but
+  # not on other platforms.
+  hibernate = handleTestOn ["x86_64-linux"] ./hibernate.nix {};
+  hitch = handleTest ./hitch {};
+  hledger-web = handleTest ./hledger-web.nix {};
+  hocker-fetchdocker = handleTest ./hocker-fetchdocker {};
+  hockeypuck = handleTest ./hockeypuck.nix { };
+  home-assistant = handleTest ./home-assistant.nix {};
+  hostname = handleTest ./hostname.nix {};
+  hound = handleTest ./hound.nix {};
+  hub = handleTest ./git/hub.nix {};
+  hydra = handleTest ./hydra {};
+  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 {};
+  initrdNetwork = handleTest ./initrd-network.nix {};
+  initrd-secrets = handleTest ./initrd-secrets.nix {};
+  input-remapper = handleTest ./input-remapper.nix {};
+  inspircd = handleTest ./inspircd.nix {};
+  installer = handleTest ./installer.nix {};
+  invoiceplane = handleTest ./invoiceplane.nix {};
+  iodine = handleTest ./iodine.nix {};
+  ipfs = handleTest ./ipfs.nix {};
+  ipv6 = handleTest ./ipv6.nix {};
+  iscsi-multipath-root = handleTest ./iscsi-multipath-root.nix {};
+  iscsi-root = handleTest ./iscsi-root.nix {};
+  isso = handleTest ./isso.nix {};
+  jackett = handleTest ./jackett.nix {};
+  jellyfin = handleTest ./jellyfin.nix {};
+  jenkins = handleTest ./jenkins.nix {};
+  jenkins-cli = handleTest ./jenkins-cli.nix {};
+  jibri = handleTest ./jibri.nix {};
+  jirafeau = handleTest ./jirafeau.nix {};
+  jitsi-meet = handleTest ./jitsi-meet.nix {};
+  k3s-single-node = handleTest ./k3s-single-node.nix {};
+  k3s-single-node-docker = handleTest ./k3s-single-node-docker.nix {};
+  kafka = handleTest ./kafka.nix {};
+  kbd-setfont-decompress = handleTest ./kbd-setfont-decompress.nix {};
+  kbd-update-search-paths-patch = handleTest ./kbd-update-search-paths-patch.nix {};
+  kea = handleTest ./kea.nix {};
+  keepalived = handleTest ./keepalived.nix {};
+  keepassxc = handleTest ./keepassxc.nix {};
+  kerberos = handleTest ./kerberos/default.nix {};
+  kernel-generic = handleTest ./kernel-generic.nix {};
+  kernel-latest-ath-user-regd = handleTest ./kernel-latest-ath-user-regd.nix {};
+  kexec = handleTest ./kexec.nix {};
+  keycloak = discoverTests (import ./keycloak.nix);
+  keymap = handleTest ./keymap.nix {};
+  knot = handleTest ./knot.nix {};
+  krb5 = discoverTests (import ./krb5 {});
+  ksm = handleTest ./ksm.nix {};
+  kubernetes = handleTestOn ["x86_64-linux"] ./kubernetes {};
+  latestKernel.login = handleTest ./login.nix { latestKernel = true; };
+  leaps = handleTest ./leaps.nix {};
+  libinput = handleTest ./libinput.nix {};
+  libreddit = handleTest ./libreddit.nix {};
+  libresprite = handleTest ./libresprite.nix {};
+  libreswan = handleTest ./libreswan.nix {};
+  lidarr = handleTest ./lidarr.nix {};
+  lightdm = handleTest ./lightdm.nix {};
+  limesurvey = handleTest ./limesurvey.nix {};
+  litestream = handleTest ./litestream.nix {};
+  locate = handleTest ./locate.nix {};
+  login = handleTest ./login.nix {};
+  logrotate = handleTest ./logrotate.nix {};
+  loki = handleTest ./loki.nix {};
+  lxd = handleTest ./lxd.nix {};
+  lxd-image = handleTest ./lxd-image.nix {};
+  lxd-nftables = handleTest ./lxd-nftables.nix {};
+  lxd-image-server = handleTest ./lxd-image-server.nix {};
+  #logstash = handleTest ./logstash.nix {};
+  lorri = handleTest ./lorri/default.nix {};
+  maddy = handleTest ./maddy.nix {};
+  magic-wormhole-mailbox-server = handleTest ./magic-wormhole-mailbox-server.nix {};
+  magnetico = handleTest ./magnetico.nix {};
+  mailcatcher = handleTest ./mailcatcher.nix {};
+  mailhog = handleTest ./mailhog.nix {};
+  man = handleTest ./man.nix {};
+  mariadb-galera = handleTest ./mysql/mariadb-galera.nix {};
+  mastodon = handleTestOn ["x86_64-linux" "i686-linux" "aarch64-linux"] ./web-apps/mastodon.nix {};
+  matomo = handleTest ./matomo.nix {};
+  matrix-appservice-irc = handleTest ./matrix-appservice-irc.nix {};
+  matrix-conduit = handleTest ./matrix-conduit.nix {};
+  matrix-synapse = handleTest ./matrix-synapse.nix {};
+  mattermost = handleTest ./mattermost.nix {};
+  mediatomb = handleTest ./mediatomb.nix {};
+  mediawiki = handleTest ./mediawiki.nix {};
+  meilisearch = handleTest ./meilisearch.nix {};
+  memcached = handleTest ./memcached.nix {};
+  metabase = handleTest ./metabase.nix {};
+  minecraft = handleTest ./minecraft.nix {};
+  minecraft-server = handleTest ./minecraft-server.nix {};
+  minidlna = handleTest ./minidlna.nix {};
+  miniflux = handleTest ./miniflux.nix {};
+  minio = handleTest ./minio.nix {};
+  misc = handleTest ./misc.nix {};
+  mjolnir = handleTest ./matrix/mjolnir.nix {};
+  mod_perl = handleTest ./mod_perl.nix {};
+  molly-brown = handleTest ./molly-brown.nix {};
+  mongodb = handleTest ./mongodb.nix {};
+  moodle = handleTest ./moodle.nix {};
+  morty = handleTest ./morty.nix {};
+  mosquitto = handleTest ./mosquitto.nix {};
+  moosefs = handleTest ./moosefs.nix {};
+  mpd = handleTest ./mpd.nix {};
+  mpv = handleTest ./mpv.nix {};
+  mumble = handleTest ./mumble.nix {};
+  musescore = handleTest ./musescore.nix {};
+  munin = handleTest ./munin.nix {};
+  mutableUsers = handleTest ./mutable-users.nix {};
+  mxisd = handleTest ./mxisd.nix {};
+  mysql = handleTest ./mysql/mysql.nix {};
+  mysql-autobackup = handleTest ./mysql/mysql-autobackup.nix {};
+  mysql-backup = handleTest ./mysql/mysql-backup.nix {};
+  mysql-replication = handleTest ./mysql/mysql-replication.nix {};
+  n8n = handleTest ./n8n.nix {};
+  nagios = handleTest ./nagios.nix {};
+  nar-serve = handleTest ./nar-serve.nix {};
+  nat.firewall = handleTest ./nat.nix { withFirewall = true; };
+  nat.firewall-conntrack = handleTest ./nat.nix { withFirewall = true; withConntrackHelpers = true; };
+  nat.standalone = handleTest ./nat.nix { withFirewall = false; };
+  nats = handleTest ./nats.nix {};
+  navidrome = handleTest ./navidrome.nix {};
+  nbd = handleTest ./nbd.nix {};
+  ncdns = handleTest ./ncdns.nix {};
+  ndppd = handleTest ./ndppd.nix {};
+  nebula = handleTest ./nebula.nix {};
+  neo4j = handleTest ./neo4j.nix {};
+  netdata = handleTest ./netdata.nix {};
+  networking.networkd = handleTest ./networking.nix { networkd = true; };
+  networking.scripted = handleTest ./networking.nix { networkd = false; };
+  specialisation = handleTest ./specialisation.nix {};
+  # TODO: put in networking.nix after the test becomes more complete
+  networkingProxy = handleTest ./networking-proxy.nix {};
+  nextcloud = handleTest ./nextcloud {};
+  nexus = handleTest ./nexus.nix {};
+  # TODO: Test nfsv3 + Kerberos
+  nfs3 = handleTest ./nfs { version = 3; };
+  nfs4 = handleTest ./nfs { version = 4; };
+  nghttpx = handleTest ./nghttpx.nix {};
+  nginx = handleTest ./nginx.nix {};
+  nginx-auth = handleTest ./nginx-auth.nix {};
+  nginx-etag = handleTest ./nginx-etag.nix {};
+  nginx-modsecurity = handleTest ./nginx-modsecurity.nix {};
+  nginx-pubhtml = handleTest ./nginx-pubhtml.nix {};
+  nginx-sandbox = handleTestOn ["x86_64-linux"] ./nginx-sandbox.nix {};
+  nginx-sso = handleTest ./nginx-sso.nix {};
+  nginx-variants = handleTest ./nginx-variants.nix {};
+  nitter = handleTest ./nitter.nix {};
+  nix-serve = handleTest ./nix-serve.nix {};
+  nix-serve-ssh = handleTest ./nix-serve-ssh.nix {};
+  nixops = handleTest ./nixops/default.nix {};
+  nixos-generate-config = handleTest ./nixos-generate-config.nix {};
+  nixpkgs = pkgs.callPackage ../modules/misc/nixpkgs/test.nix { inherit evalMinimalConfig; };
+  node-red = handleTest ./node-red.nix {};
+  nomad = handleTest ./nomad.nix {};
+  noto-fonts = handleTest ./noto-fonts.nix {};
+  novacomd = handleTestOn ["x86_64-linux"] ./novacomd.nix {};
+  nsd = handleTest ./nsd.nix {};
+  nzbget = handleTest ./nzbget.nix {};
+  nzbhydra2 = handleTest ./nzbhydra2.nix {};
+  oh-my-zsh = handleTest ./oh-my-zsh.nix {};
+  ombi = handleTest ./ombi.nix {};
+  openarena = handleTest ./openarena.nix {};
+  openldap = handleTest ./openldap.nix {};
+  openresty-lua = handleTest ./openresty-lua.nix {};
+  opensmtpd = handleTest ./opensmtpd.nix {};
+  opensmtpd-rspamd = handleTest ./opensmtpd-rspamd.nix {};
+  openssh = handleTest ./openssh.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 {};
+  owncast = handleTest ./owncast.nix {};
+  image-contents = handleTest ./image-contents.nix {};
+  orangefs = handleTest ./orangefs.nix {};
+  os-prober = handleTestOn ["x86_64-linux"] ./os-prober.nix {};
+  osrm-backend = handleTest ./osrm-backend.nix {};
+  overlayfs = handleTest ./overlayfs.nix {};
+  pacemaker = handleTest ./pacemaker.nix {};
+  packagekit = handleTest ./packagekit.nix {};
+  pam-file-contents = handleTest ./pam/pam-file-contents.nix {};
+  pam-oath-login = handleTest ./pam/pam-oath-login.nix {};
+  pam-u2f = handleTest ./pam/pam-u2f.nix {};
+  pantalaimon = handleTest ./matrix/pantalaimon.nix {};
+  pantheon = handleTest ./pantheon.nix {};
+  paperless-ng = handleTest ./paperless-ng.nix {};
+  parsedmarc = handleTest ./parsedmarc {};
+  pdns-recursor = handleTest ./pdns-recursor.nix {};
+  peerflix = handleTest ./peerflix.nix {};
+  peertube = handleTestOn ["x86_64-linux"] ./web-apps/peertube.nix {};
+  pgadmin4 = handleTest ./pgadmin4.nix {};
+  pgadmin4-standalone = handleTest ./pgadmin4-standalone.nix {};
+  pgjwt = handleTest ./pgjwt.nix {};
+  pgmanage = handleTest ./pgmanage.nix {};
+  php = handleTest ./php {};
+  php74 = handleTest ./php { php = pkgs.php74; };
+  php80 = handleTest ./php { php = pkgs.php80; };
+  php81 = handleTest ./php { php = pkgs.php81; };
+  pict-rs = handleTest ./pict-rs.nix {};
+  pinnwand = handleTest ./pinnwand.nix {};
+  plasma5 = handleTest ./plasma5.nix {};
+  plasma5-systemd-start = handleTest ./plasma5-systemd-start.nix {};
+  plausible = handleTest ./plausible.nix {};
+  pleroma = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./pleroma.nix {};
+  plikd = handleTest ./plikd.nix {};
+  plotinus = handleTest ./plotinus.nix {};
+  podgrab = handleTest ./podgrab.nix {};
+  podman = handleTestOn ["x86_64-linux"] ./podman/default.nix {};
+  podman-dnsname = handleTestOn ["x86_64-linux"] ./podman/dnsname.nix {};
+  podman-tls-ghostunnel = handleTestOn ["x86_64-linux"] ./podman/tls-ghostunnel.nix {};
+  pomerium = handleTestOn ["x86_64-linux"] ./pomerium.nix {};
+  postfix = handleTest ./postfix.nix {};
+  postfix-raise-smtpd-tls-security-level = handleTest ./postfix-raise-smtpd-tls-security-level.nix {};
+  postfixadmin = handleTest ./postfixadmin.nix {};
+  postgis = handleTest ./postgis.nix {};
+  postgresql = handleTest ./postgresql.nix {};
+  postgresql-wal-receiver = handleTest ./postgresql-wal-receiver.nix {};
+  powerdns = handleTest ./powerdns.nix {};
+  powerdns-admin = handleTest ./powerdns-admin.nix {};
+  power-profiles-daemon = handleTest ./power-profiles-daemon.nix {};
+  pppd = handleTest ./pppd.nix {};
+  predictable-interface-names = handleTest ./predictable-interface-names.nix {};
+  printing = handleTest ./printing.nix {};
+  privacyidea = handleTest ./privacyidea.nix {};
+  privoxy = handleTest ./privoxy.nix {};
+  prometheus = handleTest ./prometheus.nix {};
+  prometheus-exporters = handleTest ./prometheus-exporters.nix {};
+  prosody = handleTest ./xmpp/prosody.nix {};
+  prosody-mysql = handleTest ./xmpp/prosody-mysql.nix {};
+  proxy = handleTest ./proxy.nix {};
+  prowlarr = handleTest ./prowlarr.nix {};
+  pt2-clone = handleTest ./pt2-clone.nix {};
+  pulseaudio = discoverTests (import ./pulseaudio.nix);
+  qboot = handleTestOn ["x86_64-linux" "i686-linux"] ./qboot.nix {};
+  quorum = handleTest ./quorum.nix {};
+  rabbitmq = handleTest ./rabbitmq.nix {};
+  radarr = handleTest ./radarr.nix {};
+  radicale = handleTest ./radicale.nix {};
+  rasdaemon = handleTest ./rasdaemon.nix {};
+  redis = handleTest ./redis.nix {};
+  redmine = handleTest ./redmine.nix {};
+  resolv = handleTest ./resolv.nix {};
+  restartByActivationScript = handleTest ./restart-by-activation-script.nix {};
+  restic = handleTest ./restic.nix {};
+  retroarch = handleTest ./retroarch.nix {};
+  riak = handleTest ./riak.nix {};
+  robustirc-bridge = handleTest ./robustirc-bridge.nix {};
+  roundcube = handleTest ./roundcube.nix {};
+  rspamd = handleTest ./rspamd.nix {};
+  rss2email = handleTest ./rss2email.nix {};
+  rstudio-server = handleTest ./rstudio-server.nix {};
+  rsyncd = handleTest ./rsyncd.nix {};
+  rsyslogd = handleTest ./rsyslogd.nix {};
+  rxe = handleTest ./rxe.nix {};
+  sabnzbd = handleTest ./sabnzbd.nix {};
+  samba = handleTest ./samba.nix {};
+  samba-wsdd = handleTest ./samba-wsdd.nix {};
+  sanoid = handleTest ./sanoid.nix {};
+  sddm = handleTest ./sddm.nix {};
+  seafile = handleTest ./seafile.nix {};
+  searx = handleTest ./searx.nix {};
+  service-runner = handleTest ./service-runner.nix {};
+  shadow = handleTest ./shadow.nix {};
+  shadowsocks = handleTest ./shadowsocks {};
+  shattered-pixel-dungeon = handleTest ./shattered-pixel-dungeon.nix {};
+  shiori = handleTest ./shiori.nix {};
+  signal-desktop = handleTest ./signal-desktop.nix {};
+  simple = handleTest ./simple.nix {};
+  slurm = handleTest ./slurm.nix {};
+  smokeping = handleTest ./smokeping.nix {};
+  snapcast = handleTest ./snapcast.nix {};
+  snapper = handleTest ./snapper.nix {};
+  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 {};
+  spark = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./spark {};
+  sslh = handleTest ./sslh.nix {};
+  sssd = handleTestOn ["x86_64-linux"] ./sssd.nix {};
+  sssd-ldap = handleTestOn ["x86_64-linux"] ./sssd-ldap.nix {};
+  starship = handleTest ./starship.nix {};
+  step-ca = handleTestOn ["x86_64-linux"] ./step-ca.nix {};
+  strongswan-swanctl = handleTest ./strongswan-swanctl.nix {};
+  sudo = handleTest ./sudo.nix {};
+  sway = handleTest ./sway.nix {};
+  switchTest = handleTest ./switch-test.nix {};
+  sympa = handleTest ./sympa.nix {};
+  syncthing = handleTest ./syncthing.nix {};
+  syncthing-init = handleTest ./syncthing-init.nix {};
+  syncthing-relay = handleTest ./syncthing-relay.nix {};
+  systemd = handleTest ./systemd.nix {};
+  systemd-analyze = handleTest ./systemd-analyze.nix {};
+  systemd-binfmt = handleTestOn ["x86_64-linux"] ./systemd-binfmt.nix {};
+  systemd-boot = handleTest ./systemd-boot.nix {};
+  systemd-confinement = handleTest ./systemd-confinement.nix {};
+  systemd-cryptenroll = handleTest ./systemd-cryptenroll.nix {};
+  systemd-escaping = handleTest ./systemd-escaping.nix {};
+  systemd-journal = handleTest ./systemd-journal.nix {};
+  systemd-machinectl = handleTest ./systemd-machinectl.nix {};
+  systemd-networkd = handleTest ./systemd-networkd.nix {};
+  systemd-networkd-dhcpserver = handleTest ./systemd-networkd-dhcpserver.nix {};
+  systemd-networkd-dhcpserver-static-leases = handleTest ./systemd-networkd-dhcpserver-static-leases.nix {};
+  systemd-networkd-ipv6-prefix-delegation = handleTest ./systemd-networkd-ipv6-prefix-delegation.nix {};
+  systemd-networkd-vrf = handleTest ./systemd-networkd-vrf.nix {};
+  systemd-nspawn = handleTest ./systemd-nspawn.nix {};
+  systemd-timesyncd = handleTest ./systemd-timesyncd.nix {};
+  systemd-unit-path = handleTest ./systemd-unit-path.nix {};
+  taskserver = handleTest ./taskserver.nix {};
+  teeworlds = handleTest ./teeworlds.nix {};
+  telegraf = handleTest ./telegraf.nix {};
+  teleport = handleTest ./teleport.nix {};
+  thelounge = handleTest ./thelounge.nix {};
+  terminal-emulators = handleTest ./terminal-emulators.nix {};
+  tiddlywiki = handleTest ./tiddlywiki.nix {};
+  tigervnc = handleTest ./tigervnc.nix {};
+  timezone = handleTest ./timezone.nix {};
+  tinc = handleTest ./tinc {};
+  tinydns = handleTest ./tinydns.nix {};
+  tinywl = handleTest ./tinywl.nix {};
+  tomcat = handleTest ./tomcat.nix {};
+  tor = handleTest ./tor.nix {};
+  # traefik test relies on docker-containers
+  traefik = handleTestOn ["x86_64-linux"] ./traefik.nix {};
+  trafficserver = handleTest ./trafficserver.nix {};
+  transmission = handleTest ./transmission.nix {};
+  trezord = handleTest ./trezord.nix {};
+  trickster = handleTest ./trickster.nix {};
+  trilium-server = handleTestOn ["x86_64-linux"] ./trilium-server.nix {};
+  tsm-client-gui = handleTest ./tsm-client-gui.nix {};
+  txredisapi = handleTest ./txredisapi.nix {};
+  tuptime = handleTest ./tuptime.nix {};
+  turbovnc-headless-server = handleTest ./turbovnc-headless-server.nix {};
+  tuxguitar = handleTest ./tuxguitar.nix {};
+  ucarp = handleTest ./ucarp.nix {};
+  udisks2 = handleTest ./udisks2.nix {};
+  unbound = handleTest ./unbound.nix {};
+  unifi = handleTest ./unifi.nix {};
+  unit-php = handleTest ./web-servers/unit-php.nix {};
+  upnp = handleTest ./upnp.nix {};
+  usbguard = handleTest ./usbguard.nix {};
+  user-activation-scripts = handleTest ./user-activation-scripts.nix {};
+  uwsgi = handleTest ./uwsgi.nix {};
+  v2ray = handleTest ./v2ray.nix {};
+  vault = handleTest ./vault.nix {};
+  vault-postgresql = handleTest ./vault-postgresql.nix {};
+  vaultwarden = handleTest ./vaultwarden.nix {};
+  vector = handleTest ./vector.nix {};
+  vengi-tools = handleTest ./vengi-tools.nix {};
+  victoriametrics = handleTest ./victoriametrics.nix {};
+  vikunja = handleTest ./vikunja.nix {};
+  virtualbox = handleTestOn ["x86_64-linux"] ./virtualbox.nix {};
+  vscodium = discoverTests (import ./vscodium.nix);
+  vsftpd = handleTest ./vsftpd.nix {};
+  wasabibackend = handleTest ./wasabibackend.nix {};
+  wiki-js = handleTest ./wiki-js.nix {};
+  wine = handleTest ./wine.nix {};
+  wireguard = handleTest ./wireguard {};
+  without-nix = handleTest ./without-nix.nix {};
+  wmderland = handleTest ./wmderland.nix {};
+  wpa_supplicant = handleTest ./wpa_supplicant.nix {};
+  wordpress = handleTest ./wordpress.nix {};
+  xandikos = handleTest ./xandikos.nix {};
+  xautolock = handleTest ./xautolock.nix {};
+  xfce = handleTest ./xfce.nix {};
+  xmonad = handleTest ./xmonad.nix {};
+  xrdp = handleTest ./xrdp.nix {};
+  xss-lock = handleTest ./xss-lock.nix {};
+  xterm = handleTest ./xterm.nix {};
+  xxh = handleTest ./xxh.nix {};
+  yabar = handleTest ./yabar.nix {};
+  yggdrasil = handleTest ./yggdrasil.nix {};
+  zammad = handleTest ./zammad.nix {};
+  zfs = handleTest ./zfs.nix {};
+  zigbee2mqtt = handleTest ./zigbee2mqtt.nix {};
+  zoneminder = handleTest ./zoneminder.nix {};
+  zookeeper = handleTest ./zookeeper.nix {};
+  zsh-history = handleTest ./zsh-history.nix {};
+}
diff --git a/nixos/tests/amazon-init-shell.nix b/nixos/tests/amazon-init-shell.nix
new file mode 100644
index 00000000000..f9268b2f3a0
--- /dev/null
+++ b/nixos/tests/amazon-init-shell.nix
@@ -0,0 +1,40 @@
+# This test verifies that the amazon-init service can treat the `user-data` ec2
+# metadata file as a shell script. If amazon-init detects that `user-data` is a
+# script (based on the presence of the shebang #! line) it executes it and
+# exits.
+# Note that other tests verify that amazon-init can treat user-data as a nixos
+# configuration expression.
+
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+makeTest {
+  name = "amazon-init";
+  meta = with maintainers; {
+    maintainers = [ urbas ];
+  };
+  machine = { ... }:
+  {
+    imports = [ ../modules/profiles/headless.nix ../modules/virtualisation/amazon-init.nix ];
+    services.openssh.enable = true;
+    networking.hostName = "";
+    environment.etc."ec2-metadata/user-data" = {
+      text = ''
+        #!/usr/bin/bash
+
+        echo successful > /tmp/evidence
+      '';
+    };
+  };
+  testScript = ''
+    # To wait until amazon-init terminates its run
+    unnamed.wait_for_unit("amazon-init.service")
+
+    unnamed.succeed("grep -q successful /tmp/evidence")
+  '';
+}
diff --git a/nixos/tests/apfs.nix b/nixos/tests/apfs.nix
new file mode 100644
index 00000000000..a82886cbe73
--- /dev/null
+++ b/nixos/tests/apfs.nix
@@ -0,0 +1,54 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "apfs";
+  meta.maintainers = with pkgs.lib.maintainers; [ Luflosi ];
+
+  machine = { pkgs, ... }: {
+    virtualisation.emptyDiskImages = [ 1024 ];
+
+    boot.supportedFilesystems = [ "apfs" ];
+  };
+
+  testScript = ''
+    machine.wait_for_unit("basic.target")
+    machine.succeed("mkdir /tmp/mnt")
+
+    with subtest("mkapfs refuses to work with a label that is too long"):
+      machine.fail( "mkapfs -L '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F' /dev/vdb")
+
+    with subtest("mkapfs works with the maximum label length"):
+      machine.succeed("mkapfs -L '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7' /dev/vdb")
+
+    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",
+          "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",
+          "echo 'Hello World 2' > /tmp/mnt/\u0061\u0301.txt",
+          "echo 'Hello World 2' | diff - /tmp/mnt/\u0061\u0301.txt",
+          "[ ! -f /tmp/mnt/\u00e1.txt ] || false", # Test Unicode normalization sensitivity
+          "umount /tmp/mnt",
+          "apfsck /dev/vdb",
+      )
+    with subtest("Disable case sensitivity and normalization sensitivity"):
+      machine.succeed(
+          "mkapfs /dev/vdb",
+          "mount -o 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",
+          "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",
+          "echo 'Hello World 2' | diff - /tmp/mnt/\u00e1.txt", # Test Unicode normalization
+          "umount /tmp/mnt",
+          "apfsck /dev/vdb",
+      )
+  '';
+})
diff --git a/nixos/tests/apparmor.nix b/nixos/tests/apparmor.nix
new file mode 100644
index 00000000000..c6daa8e67de
--- /dev/null
+++ b/nixos/tests/apparmor.nix
@@ -0,0 +1,82 @@
+import ./make-test-python.nix ({ pkgs, ... } : {
+  name = "apparmor";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ julm ];
+  };
+
+  machine =
+    { lib, pkgs, config, ... }:
+    with lib;
+    {
+      security.apparmor.enable = mkDefault true;
+    };
+
+  testScript =
+    ''
+      machine.wait_for_unit("multi-user.target")
+
+      with subtest("AppArmor profiles are loaded"):
+          machine.succeed("systemctl status apparmor.service")
+
+      # AppArmor securityfs
+      with subtest("AppArmor securityfs is mounted"):
+          machine.succeed("mountpoint -q /sys/kernel/security")
+          machine.succeed("cat /sys/kernel/security/apparmor/profiles")
+
+      # Test apparmorRulesFromClosure by:
+      # 1. Prepending a string of the relevant packages' name and version on each line.
+      # 2. Sorting according to those strings.
+      # 3. Removing those prepended strings.
+      # 4. Using `diff` against the expected output.
+      with subtest("apparmorRulesFromClosure"):
+          machine.succeed(
+              "${pkgs.diffutils}/bin/diff ${pkgs.writeText "expected.rules" ''
+                  mr ${pkgs.bash}/lib/**.so*,
+                  r ${pkgs.bash},
+                  r ${pkgs.bash}/etc/**,
+                  r ${pkgs.bash}/lib/**,
+                  r ${pkgs.bash}/share/**,
+                  x ${pkgs.bash}/foo/**,
+                  mr ${pkgs.glibc}/lib/**.so*,
+                  r ${pkgs.glibc},
+                  r ${pkgs.glibc}/etc/**,
+                  r ${pkgs.glibc}/lib/**,
+                  r ${pkgs.glibc}/share/**,
+                  x ${pkgs.glibc}/foo/**,
+                  mr ${pkgs.libcap}/lib/**.so*,
+                  r ${pkgs.libcap},
+                  r ${pkgs.libcap}/etc/**,
+                  r ${pkgs.libcap}/lib/**,
+                  r ${pkgs.libcap}/share/**,
+                  x ${pkgs.libcap}/foo/**,
+                  mr ${pkgs.libcap.lib}/lib/**.so*,
+                  r ${pkgs.libcap.lib},
+                  r ${pkgs.libcap.lib}/etc/**,
+                  r ${pkgs.libcap.lib}/lib/**,
+                  r ${pkgs.libcap.lib}/share/**,
+                  x ${pkgs.libcap.lib}/foo/**,
+                  mr ${pkgs.libidn2.out}/lib/**.so*,
+                  r ${pkgs.libidn2.out},
+                  r ${pkgs.libidn2.out}/etc/**,
+                  r ${pkgs.libidn2.out}/lib/**,
+                  r ${pkgs.libidn2.out}/share/**,
+                  x ${pkgs.libidn2.out}/foo/**,
+                  mr ${pkgs.libunistring}/lib/**.so*,
+                  r ${pkgs.libunistring},
+                  r ${pkgs.libunistring}/etc/**,
+                  r ${pkgs.libunistring}/lib/**,
+                  r ${pkgs.libunistring}/share/**,
+                  x ${pkgs.libunistring}/foo/**,
+              ''} ${pkgs.runCommand "actual.rules" { preferLocalBuild = true; } ''
+                  ${pkgs.gnused}/bin/sed -e 's:^[^ ]* ${builtins.storeDir}/[^,/-]*-\([^/,]*\):\1 \0:' ${
+                      pkgs.apparmorRulesFromClosure {
+                        name = "ping";
+                        additionalRules = ["x $path/foo/**"];
+                      } [ pkgs.libcap ]
+                  } |
+                  ${pkgs.coreutils}/bin/sort -n -k1 |
+                  ${pkgs.gnused}/bin/sed -e 's:^[^ ]* ::' >$out
+              ''}"
+          )
+    '';
+})
diff --git a/nixos/tests/atd.nix b/nixos/tests/atd.nix
new file mode 100644
index 00000000000..ad4d60067cf
--- /dev/null
+++ b/nixos/tests/atd.nix
@@ -0,0 +1,31 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "atd";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ bjornfor ];
+  };
+
+  machine =
+    { ... }:
+    { services.atd.enable = true;
+      users.users.alice = { isNormalUser = true; };
+    };
+
+  # "at" has a resolution of 1 minute
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("atd.service")  # wait for atd to start
+    machine.fail("test -f ~root/at-1")
+    machine.fail("test -f ~alice/at-1")
+
+    machine.succeed("echo 'touch ~root/at-1' | at now+1min")
+    machine.succeed("su - alice -c \"echo 'touch at-1' | at now+1min\"")
+
+    machine.succeed("sleep 1.5m")
+
+    machine.succeed("test -f ~root/at-1")
+    machine.succeed("test -f ~alice/at-1")
+  '';
+})
diff --git a/nixos/tests/atop.nix b/nixos/tests/atop.nix
new file mode 100644
index 00000000000..f7a90346f3d
--- /dev/null
+++ b/nixos/tests/atop.nix
@@ -0,0 +1,234 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let assertions = rec {
+  path = program: path: ''
+    with subtest("The path of ${program} should be ${path}"):
+        p = machine.succeed("type -p \"${program}\" | head -c -1")
+        assert p == "${path}", f"${program} is {p}, expected ${path}"
+  '';
+  unit = name: state: ''
+    with subtest("Unit ${name} should be ${state}"):
+        if "${state}" == "active":
+            machine.wait_for_unit("${name}")
+        else:
+            machine.require_unit_state("${name}", "${state}")
+  '';
+  version = ''
+    import re
+
+    with subtest("binary should report the correct version"):
+        pkgver = "${pkgs.atop.version}"
+        ver = re.sub(r'(?s)^Version: (\d\.\d\.\d).*', r'\1', machine.succeed("atop -V"))
+        assert ver == pkgver, f"Version is `{ver}`, expected `{pkgver}`"
+  '';
+  atoprc = contents:
+    if builtins.stringLength contents > 0 then ''
+      with subtest("/etc/atoprc should have the correct contents"):
+          f = machine.succeed("cat /etc/atoprc")
+          assert f == "${contents}", f"/etc/atoprc contents: '{f}', expected '${contents}'"
+    '' else ''
+      with subtest("/etc/atoprc should not be present"):
+          machine.succeed("test ! -e /etc/atoprc")
+    '';
+  wrapper = present:
+    if present then path "atop" "/run/wrappers/bin/atop" + ''
+      with subtest("Wrapper should be setuid root"):
+          stat = machine.succeed("stat --printf '%a %u' /run/wrappers/bin/atop")
+          assert stat == "4511 0", f"Wrapper stat is {stat}, expected '4511 0'"
+    ''
+    else path "atop" "/run/current-system/sw/bin/atop";
+  atopService = present:
+    if present then
+      unit "atop.service" "active"
+      + ''
+        with subtest("atop.service should write some data to /var/log/atop"):
+
+            def has_data_files(last: bool) -> bool:
+                files = int(machine.succeed("ls -1 /var/log/atop | wc -l"))
+                if files == 0:
+                    machine.log("Did not find at least one 1 data file")
+                    if not last:
+                        machine.log("Will retry...")
+                    return False
+                return True
+
+            with machine.nested("Waiting for data files"):
+                retry(has_data_files)
+      '' else unit "atop.service" "inactive";
+  atopRotateTimer = present:
+    unit "atop-rotate.timer" (if present then "active" else "inactive");
+  atopacctService = present:
+    if present then
+      unit "atopacct.service" "active"
+      + ''
+        with subtest("atopacct.service should enable process accounting"):
+            machine.wait_until_succeeds("test -f /run/pacct_source")
+
+        with subtest("atopacct.service should write data to /run/pacct_shadow.d"):
+
+            def has_data_files(last: bool) -> bool:
+                files = int(machine.succeed("ls -1 /run/pacct_shadow.d | wc -l"))
+                if files == 0:
+                    machine.log("Did not find at least one 1 data file")
+                    if not last:
+                        machine.log("Will retry...")
+                    return False
+                return True
+
+            with machine.nested("Waiting for data files"):
+                retry(has_data_files)
+      '' else unit "atopacct.service" "inactive";
+  netatop = present:
+    if present then
+      unit "netatop.service" "active"
+      + ''
+        with subtest("The netatop kernel module should be loaded"):
+            out = machine.succeed("modprobe -n -v netatop")
+            assert out == "", f"Module should be loaded already, but modprobe would have done {out}."
+      '' else ''
+      with subtest("The netatop kernel module should be absent"):
+          machine.fail("modprobe -n -v netatop")
+    '';
+  atopgpu = present:
+    if present then
+      (unit "atopgpu.service" "active") + (path "atopgpud" "/run/current-system/sw/bin/atopgpud")
+    else (unit "atopgpu.service" "inactive") + ''
+      with subtest("atopgpud should not be present"):
+          machine.fail("type -p atopgpud")
+    '';
+};
+in
+{
+  justThePackage = makeTest {
+    name = "atop-justThePackage";
+    machine = {
+      environment.systemPackages = [ pkgs.atop ];
+    };
+    testScript = with assertions; builtins.concatStringsSep "\n" [
+      version
+      (atoprc "")
+      (wrapper false)
+      (atopService false)
+      (atopRotateTimer false)
+      (atopacctService false)
+      (netatop false)
+      (atopgpu false)
+    ];
+  };
+  defaults = makeTest {
+    name = "atop-defaults";
+    machine = {
+      programs.atop = {
+        enable = true;
+      };
+    };
+    testScript = with assertions; builtins.concatStringsSep "\n" [
+      version
+      (atoprc "")
+      (wrapper false)
+      (atopService true)
+      (atopRotateTimer true)
+      (atopacctService true)
+      (netatop false)
+      (atopgpu false)
+    ];
+  };
+  minimal = makeTest {
+    name = "atop-minimal";
+    machine = {
+      programs.atop = {
+        enable = true;
+        atopService.enable = false;
+        atopRotateTimer.enable = false;
+        atopacctService.enable = false;
+      };
+    };
+    testScript = with assertions; builtins.concatStringsSep "\n" [
+      version
+      (atoprc "")
+      (wrapper false)
+      (atopService false)
+      (atopRotateTimer false)
+      (atopacctService false)
+      (netatop false)
+      (atopgpu false)
+    ];
+  };
+  netatop = makeTest {
+    name = "atop-netatop";
+    machine = {
+      programs.atop = {
+        enable = true;
+        netatop.enable = true;
+      };
+    };
+    testScript = with assertions; builtins.concatStringsSep "\n" [
+      version
+      (atoprc "")
+      (wrapper false)
+      (atopService true)
+      (atopRotateTimer true)
+      (atopacctService true)
+      (netatop true)
+      (atopgpu false)
+    ];
+  };
+  atopgpu = makeTest {
+    name = "atop-atopgpu";
+    machine = {
+      nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (getName pkg) [
+        "cudatoolkit"
+      ];
+
+      programs.atop = {
+        enable = true;
+        atopgpu.enable = true;
+      };
+    };
+    testScript = with assertions; builtins.concatStringsSep "\n" [
+      version
+      (atoprc "")
+      (wrapper false)
+      (atopService true)
+      (atopRotateTimer true)
+      (atopacctService true)
+      (netatop false)
+      (atopgpu true)
+    ];
+  };
+  everything = makeTest {
+    name = "atop-everthing";
+    machine = {
+      nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (getName pkg) [
+        "cudatoolkit"
+      ];
+
+      programs.atop = {
+        enable = true;
+        settings = {
+          flags = "faf1";
+          interval = 2;
+        };
+        setuidWrapper.enable = true;
+        netatop.enable = true;
+        atopgpu.enable = true;
+      };
+    };
+    testScript = with assertions; builtins.concatStringsSep "\n" [
+      version
+      (atoprc "flags faf1\\ninterval 2\\n")
+      (wrapper true)
+      (atopService true)
+      (atopRotateTimer true)
+      (atopacctService true)
+      (netatop true)
+      (atopgpu true)
+    ];
+  };
+}
diff --git a/nixos/tests/avahi.nix b/nixos/tests/avahi.nix
new file mode 100644
index 00000000000..ebb46838325
--- /dev/null
+++ b/nixos/tests/avahi.nix
@@ -0,0 +1,79 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+# bool: whether to use networkd in the tests
+, networkd ? false
+} @ args:
+
+# Test whether `avahi-daemon' and `libnss-mdns' work as expected.
+import ./make-test-python.nix {
+  name = "avahi";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eelco ];
+  };
+
+  nodes = let
+    cfg = { ... }: {
+      services.avahi = {
+        enable = true;
+        nssmdns = true;
+        publish.addresses = true;
+        publish.domain = true;
+        publish.enable = true;
+        publish.userServices = true;
+        publish.workstation = true;
+        extraServiceFiles.ssh = "${pkgs.avahi}/etc/avahi/services/ssh.service";
+      };
+    } // pkgs.lib.optionalAttrs (networkd) {
+      networking = {
+        useNetworkd = true;
+        useDHCP = false;
+      };
+    };
+  in {
+    one = cfg;
+    two = cfg;
+  };
+
+  testScript = ''
+    start_all()
+
+    # mDNS.
+    one.wait_for_unit("network.target")
+    two.wait_for_unit("network.target")
+
+    one.succeed("avahi-resolve-host-name one.local | tee out >&2")
+    one.succeed('test "`cut -f1 < out`" = one.local')
+    one.succeed("avahi-resolve-host-name two.local | tee out >&2")
+    one.succeed('test "`cut -f1 < out`" = two.local')
+
+    two.succeed("avahi-resolve-host-name one.local | tee out >&2")
+    two.succeed('test "`cut -f1 < out`" = one.local')
+    two.succeed("avahi-resolve-host-name two.local | tee out >&2")
+    two.succeed('test "`cut -f1 < out`" = two.local')
+
+    # Basic DNS-SD.
+    one.succeed("avahi-browse -r -t _workstation._tcp | tee out >&2")
+    one.succeed("test `wc -l < out` -gt 0")
+    two.succeed("avahi-browse -r -t _workstation._tcp | tee out >&2")
+    two.succeed("test `wc -l < out` -gt 0")
+
+    # More DNS-SD.
+    one.execute('avahi-publish -s "This is a test" _test._tcp 123 one=1 &')
+    one.sleep(5)
+    two.succeed("avahi-browse -r -t _test._tcp | tee out >&2")
+    two.succeed("test `wc -l < out` -gt 0")
+
+    # NSS-mDNS.
+    one.succeed("getent hosts one.local >&2")
+    one.succeed("getent hosts two.local >&2")
+    two.succeed("getent hosts one.local >&2")
+    two.succeed("getent hosts two.local >&2")
+
+    # extra service definitions
+    one.succeed("avahi-browse -r -t _ssh._tcp | tee out >&2")
+    one.succeed("test `wc -l < out` -gt 0")
+    two.succeed("avahi-browse -r -t _ssh._tcp | tee out >&2")
+    two.succeed("test `wc -l < out` -gt 0")
+  '';
+} args
diff --git a/nixos/tests/babeld.nix b/nixos/tests/babeld.nix
new file mode 100644
index 00000000000..d4df6f86d08
--- /dev/null
+++ b/nixos/tests/babeld.nix
@@ -0,0 +1,142 @@
+
+import ./make-test-python.nix ({ pkgs, lib, ...} : {
+  name = "babeld";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ hexa ];
+  };
+
+  nodes =
+    { client = { pkgs, lib, ... }:
+      {
+        virtualisation.vlans = [ 10 ];
+
+        networking = {
+          useDHCP = false;
+          interfaces."eth1" = {
+            ipv4.addresses = lib.mkForce [ { address = "192.168.10.2"; prefixLength = 24; } ];
+            ipv4.routes = lib.mkForce [ { address = "0.0.0.0"; prefixLength = 0; via = "192.168.10.1"; } ];
+            ipv6.addresses = lib.mkForce [ { address = "2001:db8:10::2"; prefixLength = 64; } ];
+            ipv6.routes = lib.mkForce [ { address = "::"; prefixLength = 0; via = "2001:db8:10::1"; } ];
+          };
+        };
+      };
+
+      local_router = { pkgs, lib, ... }:
+      {
+        virtualisation.vlans = [ 10 20 ];
+
+        networking = {
+          useDHCP = false;
+          firewall.enable = false;
+
+          interfaces."eth1" = {
+            ipv4.addresses = lib.mkForce [ { address = "192.168.10.1"; prefixLength = 24; } ];
+            ipv6.addresses = lib.mkForce [ { address = "2001:db8:10::1"; prefixLength = 64; } ];
+          };
+
+          interfaces."eth2" = {
+            ipv4.addresses = lib.mkForce [ { address = "192.168.20.1"; prefixLength = 24; } ];
+            ipv6.addresses = lib.mkForce [ { address = "2001:db8:20::1"; prefixLength = 64; } ];
+          };
+        };
+
+        services.babeld = {
+          enable = true;
+          interfaces.eth2 = {
+            hello-interval = 1;
+            type = "wired";
+          };
+          extraConfig = ''
+            local-port-readwrite 33123
+
+            import-table 254 # main
+            export-table 254 # main
+
+            in ip 192.168.10.0/24 deny
+            in ip 192.168.20.0/24 deny
+            in ip 2001:db8:10::/64 deny
+            in ip 2001:db8:20::/64 deny
+
+            in ip 192.168.30.0/24 allow
+            in ip 2001:db8:30::/64 allow
+
+            in deny
+
+            redistribute local proto 2
+            redistribute local deny
+          '';
+        };
+      };
+      remote_router = { pkgs, lib, ... }:
+      {
+        virtualisation.vlans = [ 20 30 ];
+
+        networking = {
+          useDHCP = false;
+          firewall.enable = false;
+
+          interfaces."eth1" = {
+            ipv4.addresses = lib.mkForce [ { address = "192.168.20.2"; prefixLength = 24; } ];
+            ipv6.addresses = lib.mkForce [ { address = "2001:db8:20::2"; prefixLength = 64; } ];
+          };
+
+          interfaces."eth2" = {
+            ipv4.addresses = lib.mkForce [ { address = "192.168.30.1"; prefixLength = 24; } ];
+            ipv6.addresses = lib.mkForce [ { address = "2001:db8:30::1"; prefixLength = 64; } ];
+          };
+        };
+
+        services.babeld = {
+          enable = true;
+          interfaces.eth1 = {
+            hello-interval = 1;
+            type = "wired";
+          };
+          extraConfig = ''
+            local-port-readwrite 33123
+
+            import-table 254 # main
+            export-table 254 # main
+
+            in ip 192.168.20.0/24 deny
+            in ip 192.168.30.0/24 deny
+            in ip 2001:db8:20::/64 deny
+            in ip 2001:db8:30::/64 deny
+
+            in ip 192.168.10.0/24 allow
+            in ip 2001:db8:10::/64 allow
+
+            in deny
+
+            redistribute local proto 2
+            redistribute local deny
+          '';
+        };
+
+      };
+    };
+
+  testScript =
+    ''
+      start_all()
+
+      client.wait_for_unit("network-online.target")
+      local_router.wait_for_unit("network-online.target")
+      remote_router.wait_for_unit("network-online.target")
+
+      local_router.wait_for_unit("babeld.service")
+      remote_router.wait_for_unit("babeld.service")
+
+      local_router.wait_until_succeeds("ip route get 192.168.30.1")
+      local_router.wait_until_succeeds("ip route get 2001:db8:30::1")
+
+      remote_router.wait_until_succeeds("ip route get 192.168.10.1")
+      remote_router.wait_until_succeeds("ip route get 2001:db8:10::1")
+
+      client.succeed("ping -c1 192.168.30.1")
+      client.succeed("ping -c1 2001:db8:30::1")
+
+      remote_router.succeed("ping -c1 192.168.10.2")
+      remote_router.succeed("ping -c1 2001:db8:10::2")
+    '';
+})
diff --git a/nixos/tests/bazarr.nix b/nixos/tests/bazarr.nix
new file mode 100644
index 00000000000..c3337611aa2
--- /dev/null
+++ b/nixos/tests/bazarr.nix
@@ -0,0 +1,26 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+let
+  port = 42069;
+in
+{
+  name = "bazarr";
+  meta.maintainers = with maintainers; [ d-xo ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    {
+      services.bazarr = {
+        enable = true;
+        listenPort = port;
+      };
+    };
+
+  testScript = ''
+    machine.wait_for_unit("bazarr.service")
+    machine.wait_for_open_port("${toString port}")
+    machine.succeed("curl --fail http://localhost:${toString port}/")
+  '';
+})
diff --git a/nixos/tests/bcachefs.nix b/nixos/tests/bcachefs.nix
new file mode 100644
index 00000000000..44997a74687
--- /dev/null
+++ b/nixos/tests/bcachefs.nix
@@ -0,0 +1,33 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "bcachefs";
+  meta.maintainers = with pkgs.lib.maintainers; [ chiiruno ];
+
+  machine = { pkgs, ... }: {
+    virtualisation.emptyDiskImages = [ 4096 ];
+    networking.hostId = "deadbeef";
+    boot.supportedFilesystems = [ "bcachefs" ];
+    environment.systemPackages = with pkgs; [ parted keyutils ];
+  };
+
+  testScript = ''
+    machine.succeed("modprobe bcachefs")
+    machine.succeed("bcachefs version")
+    machine.succeed("ls /dev")
+
+    machine.succeed(
+        "mkdir /tmp/mnt",
+        "udevadm settle",
+        "parted --script /dev/vdb mklabel msdos",
+        "parted --script /dev/vdb -- mkpart primary 1024M 50% mkpart primary 50% -1s",
+        "udevadm settle",
+        "keyctl link @u @s",
+        "echo password | bcachefs format --encrypted --metadata_replicas 2 --label vtest /dev/vdb1 /dev/vdb2",
+        "echo password | bcachefs unlock /dev/vdb1",
+        "mount -t bcachefs /dev/vdb1:/dev/vdb2 /tmp/mnt",
+        "udevadm settle",
+        "bcachefs fs usage /tmp/mnt",
+        "umount /tmp/mnt",
+        "udevadm settle",
+    )
+  '';
+})
diff --git a/nixos/tests/beanstalkd.nix b/nixos/tests/beanstalkd.nix
new file mode 100644
index 00000000000..4f4a454fb47
--- /dev/null
+++ b/nixos/tests/beanstalkd.nix
@@ -0,0 +1,49 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+let
+  pythonEnv = pkgs.python3.withPackages (p: [p.beanstalkc]);
+
+  produce = pkgs.writeScript "produce.py" ''
+    #!${pythonEnv.interpreter}
+    import beanstalkc
+
+    queue = beanstalkc.Connection(host='localhost', port=11300, parse_yaml=False);
+    queue.put(b'this is a job')
+    queue.put(b'this is another job')
+  '';
+
+  consume = pkgs.writeScript "consume.py" ''
+    #!${pythonEnv.interpreter}
+    import beanstalkc
+
+    queue = beanstalkc.Connection(host='localhost', port=11300, parse_yaml=False);
+
+    job = queue.reserve(timeout=0)
+    print(job.body.decode('utf-8'))
+    job.delete()
+  '';
+
+in
+{
+  name = "beanstalkd";
+  meta.maintainers = [ lib.maintainers.aanderse ];
+
+  machine =
+    { ... }:
+    { services.beanstalkd.enable = true;
+    };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("beanstalkd.service")
+
+    machine.succeed("${produce}")
+    assert "this is a job\n" == machine.succeed(
+        "${consume}"
+    )
+    assert "this is another job\n" == machine.succeed(
+        "${consume}"
+    )
+  '';
+})
diff --git a/nixos/tests/bees.nix b/nixos/tests/bees.nix
new file mode 100644
index 00000000000..58a9c295135
--- /dev/null
+++ b/nixos/tests/bees.nix
@@ -0,0 +1,62 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+{
+  name = "bees";
+
+  machine = { config, pkgs, ... }: {
+    boot.initrd.postDeviceCommands = ''
+      ${pkgs.btrfs-progs}/bin/mkfs.btrfs -f -L aux1 /dev/vdb
+      ${pkgs.btrfs-progs}/bin/mkfs.btrfs -f -L aux2 /dev/vdc
+    '';
+    virtualisation.emptyDiskImages = [ 4096 4096 ];
+    virtualisation.fileSystems = {
+      "/aux1" = { # filesystem configured to be deduplicated
+        device = "/dev/disk/by-label/aux1";
+        fsType = "btrfs";
+      };
+      "/aux2" = { # filesystem not configured to be deduplicated
+        device = "/dev/disk/by-label/aux2";
+        fsType = "btrfs";
+      };
+    };
+    services.beesd.filesystems = {
+      aux1 = {
+        spec = "LABEL=aux1";
+        hashTableSizeMB = 16;
+        verbosity = "debug";
+      };
+    };
+  };
+
+  testScript =
+  let
+    someContentIsShared = loc: pkgs.writeShellScript "some-content-is-shared" ''
+      [[ $(btrfs fi du -s --raw ${lib.escapeShellArg loc}/dedup-me-{1,2} | awk 'BEGIN { count=0; } NR>1 && $3 == 0 { count++ } END { print count }') -eq 0 ]]
+    '';
+  in ''
+    # shut down the instance started by systemd at boot, so we can test our test procedure
+    machine.succeed("systemctl stop beesd@aux1.service")
+
+    machine.succeed(
+        "dd if=/dev/urandom of=/aux1/dedup-me-1 bs=1M count=8",
+        "cp --reflink=never /aux1/dedup-me-1 /aux1/dedup-me-2",
+        "cp --reflink=never /aux1/* /aux2/",
+        "sync",
+    )
+    machine.fail(
+        "${someContentIsShared "/aux1"}",
+        "${someContentIsShared "/aux2"}",
+    )
+    machine.succeed("systemctl start beesd@aux1.service")
+
+    # assert that "Set Shared" column is nonzero
+    machine.wait_until_succeeds(
+        "${someContentIsShared "/aux1"}",
+    )
+    machine.fail("${someContentIsShared "/aux2"}")
+
+    # assert that 16MB hash table size requested was honored
+    machine.succeed(
+        "[[ $(stat -c %s /aux1/.beeshome/beeshash.dat) = $(( 16 * 1024 * 1024)) ]]"
+    )
+  '';
+})
diff --git a/nixos/tests/bind.nix b/nixos/tests/bind.nix
new file mode 100644
index 00000000000..7234f56a1c3
--- /dev/null
+++ b/nixos/tests/bind.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix {
+  name = "bind";
+
+  machine = { pkgs, lib, ... }: {
+    services.bind.enable = true;
+    services.bind.extraOptions = "empty-zones-enable no;";
+    services.bind.zones = lib.singleton {
+      name = ".";
+      master = true;
+      file = pkgs.writeText "root.zone" ''
+        $TTL 3600
+        . IN SOA ns.example.org. admin.example.org. ( 1 3h 1h 1w 1d )
+        . IN NS ns.example.org.
+
+        ns.example.org. IN A    192.168.0.1
+        ns.example.org. IN AAAA abcd::1
+
+        1.0.168.192.in-addr.arpa IN PTR ns.example.org.
+      '';
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("bind.service")
+    machine.wait_for_open_port(53)
+    machine.succeed("host 192.168.0.1 127.0.0.1 | grep -qF ns.example.org")
+  '';
+}
diff --git a/nixos/tests/bird.nix b/nixos/tests/bird.nix
new file mode 100644
index 00000000000..822a7caea9b
--- /dev/null
+++ b/nixos/tests/bird.nix
@@ -0,0 +1,129 @@
+# This test does a basic functionality check for all bird variants and demonstrates a use
+# of the preCheckConfig option.
+
+{ system ? builtins.currentSystem
+, pkgs ? import ../.. { inherit system; config = { }; }
+}:
+
+let
+  inherit (import ../lib/testing-python.nix { inherit system pkgs; }) makeTest;
+  inherit (pkgs.lib) optionalString;
+
+  makeBird2Host = hostId: { pkgs, ... }: {
+    virtualisation.vlans = [ 1 ];
+
+    environment.systemPackages = with pkgs; [ jq ];
+
+    networking = {
+      useNetworkd = true;
+      useDHCP = false;
+      firewall.enable = false;
+    };
+
+    systemd.network.networks."01-eth1" = {
+      name = "eth1";
+      networkConfig.Address = "10.0.0.${hostId}/24";
+    };
+
+    services.bird2 = {
+      enable = true;
+
+      config = ''
+        log syslog all;
+
+        debug protocols all;
+
+        router id 10.0.0.${hostId};
+
+        protocol device {
+        }
+
+        protocol kernel kernel4 {
+          ipv4 {
+            import none;
+            export all;
+          };
+        }
+
+        protocol static static4 {
+          ipv4;
+          include "static4.conf";
+        }
+
+        protocol ospf v2 ospf4 {
+          ipv4 {
+            export all;
+          };
+          area 0 {
+            interface "eth1" {
+              hello 5;
+              wait 5;
+            };
+          };
+        }
+
+        protocol kernel kernel6 {
+          ipv6 {
+            import none;
+            export all;
+          };
+        }
+
+        protocol static static6 {
+          ipv6;
+          include "static6.conf";
+        }
+
+        protocol ospf v3 ospf6 {
+          ipv6 {
+            export all;
+          };
+          area 0 {
+            interface "eth1" {
+              hello 5;
+              wait 5;
+            };
+          };
+        }
+      '';
+
+      preCheckConfig = ''
+        echo "route 1.2.3.4/32 blackhole;" > static4.conf
+        echo "route fd00::/128 blackhole;" > static6.conf
+      '';
+    };
+
+    systemd.tmpfiles.rules = [
+      "f /etc/bird/static4.conf - - - - route 10.10.0.${hostId}/32 blackhole;"
+      "f /etc/bird/static6.conf - - - - route fdff::${hostId}/128 blackhole;"
+    ];
+  };
+in
+makeTest {
+  name = "bird2";
+
+  nodes.host1 = makeBird2Host "1";
+  nodes.host2 = makeBird2Host "2";
+
+  testScript = ''
+    start_all()
+
+    host1.wait_for_unit("bird2.service")
+    host2.wait_for_unit("bird2.service")
+    host1.succeed("systemctl reload bird2.service")
+
+    with subtest("Waiting for advertised IPv4 routes"):
+      host1.wait_until_succeeds("ip --json r | jq -e 'map(select(.dst == \"10.10.0.2\")) | any'")
+      host2.wait_until_succeeds("ip --json r | jq -e 'map(select(.dst == \"10.10.0.1\")) | any'")
+    with subtest("Waiting for advertised IPv6 routes"):
+      host1.wait_until_succeeds("ip --json -6 r | jq -e 'map(select(.dst == \"fdff::2\")) | any'")
+      host2.wait_until_succeeds("ip --json -6 r | jq -e 'map(select(.dst == \"fdff::1\")) | any'")
+
+    with subtest("Check fake routes in preCheckConfig do not exists"):
+      host1.fail("ip --json r | jq -e 'map(select(.dst == \"1.2.3.4\")) | any'")
+      host2.fail("ip --json r | jq -e 'map(select(.dst == \"1.2.3.4\")) | any'")
+
+      host1.fail("ip --json -6 r | jq -e 'map(select(.dst == \"fd00::\")) | any'")
+      host2.fail("ip --json -6 r | jq -e 'map(select(.dst == \"fd00::\")) | any'")
+  '';
+}
diff --git a/nixos/tests/bitcoind.nix b/nixos/tests/bitcoind.nix
new file mode 100644
index 00000000000..3e9e085287a
--- /dev/null
+++ b/nixos/tests/bitcoind.nix
@@ -0,0 +1,46 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "bitcoind";
+  meta = with pkgs.lib; {
+    maintainers = with maintainers; [ _1000101 ];
+  };
+
+  machine = { ... }: {
+    services.bitcoind."mainnet" = {
+      enable = true;
+      rpc = {
+        port = 8332;
+        users.rpc.passwordHMAC = "acc2374e5f9ba9e62a5204d3686616cf$53abdba5e67a9005be6a27ca03a93ce09e58854bc2b871523a0d239a72968033";
+        users.rpc2.passwordHMAC = "1495e4a3ad108187576c68f7f9b5ddc5$accce0881c74aa01bb8960ff3bdbd39f607fd33178147679e055a4ac35f53225";
+      };
+    };
+    services.bitcoind."testnet" = {
+      enable = true;
+      configFile = "/test.blank";
+      testnet = true;
+      rpc = {
+        port = 18332;
+      };
+      extraCmdlineOptions = [ "-rpcuser=rpc" "-rpcpassword=rpc" "-rpcauth=rpc2:1495e4a3ad108187576c68f7f9b5ddc5$accce0881c74aa01bb8960ff3bdbd39f607fd33178147679e055a4ac35f53225" ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("bitcoind-mainnet.service")
+    machine.wait_for_unit("bitcoind-testnet.service")
+
+    machine.wait_until_succeeds(
+        'curl --fail --user rpc:rpc --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:8332 |  grep \'"chain":"main"\' '
+    )
+    machine.wait_until_succeeds(
+        'curl --fail --user rpc2:rpc2 --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:8332 |  grep \'"chain":"main"\' '
+    )
+    machine.wait_until_succeeds(
+        'curl --fail --user rpc:rpc --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:18332 |  grep \'"chain":"test"\' '
+    )
+    machine.wait_until_succeeds(
+        'curl --fail --user rpc2:rpc2 --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:18332 |  grep \'"chain":"test"\' '
+    )
+  '';
+})
diff --git a/nixos/tests/bittorrent.nix b/nixos/tests/bittorrent.nix
new file mode 100644
index 00000000000..11420cba9dc
--- /dev/null
+++ b/nixos/tests/bittorrent.nix
@@ -0,0 +1,164 @@
+# This test runs a Bittorrent tracker on one machine, and verifies
+# that two client machines can download the torrent using
+# `transmission'.  The first client (behind a NAT router) downloads
+# from the initial seeder running on the tracker.  Then we kill the
+# initial seeder.  The second client downloads from the first client,
+# which only works if the first client successfully uses the UPnP-IGD
+# protocol to poke a hole in the NAT.
+
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+
+  # Some random file to serve.
+  file = pkgs.hello.src;
+
+  internalRouterAddress = "192.168.3.1";
+  internalClient1Address = "192.168.3.2";
+  externalRouterAddress = "80.100.100.1";
+  externalClient2Address = "80.100.100.2";
+  externalTrackerAddress = "80.100.100.3";
+
+  download-dir = "/var/lib/transmission/Downloads";
+  transmissionConfig = { ... }: {
+    environment.systemPackages = [ pkgs.transmission ];
+    services.transmission = {
+      enable = true;
+      settings = {
+        dht-enabled = false;
+        message-level = 2;
+        inherit download-dir;
+      };
+    };
+  };
+in
+
+{
+  name = "bittorrent";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ domenkozar eelco rob bobvanderlinden ];
+  };
+
+  nodes = {
+    tracker = { pkgs, ... }: {
+      imports = [ transmissionConfig ];
+
+      virtualisation.vlans = [ 1 ];
+      networking.firewall.enable = false;
+      networking.interfaces.eth1.ipv4.addresses = [
+        { address = externalTrackerAddress; prefixLength = 24; }
+      ];
+
+      # We need Apache on the tracker to serve the torrents.
+      services.httpd = {
+        enable = true;
+        virtualHosts = {
+          "torrentserver.org" = {
+            adminAddr = "foo@example.org";
+            documentRoot = "/tmp";
+          };
+        };
+      };
+      services.opentracker.enable = true;
+    };
+
+    router = { pkgs, nodes, ... }: {
+      virtualisation.vlans = [ 1 2 ];
+      networking.nat.enable = true;
+      networking.nat.internalInterfaces = [ "eth2" ];
+      networking.nat.externalInterface = "eth1";
+      networking.firewall.enable = true;
+      networking.firewall.trustedInterfaces = [ "eth2" ];
+      networking.interfaces.eth0.ipv4.addresses = [];
+      networking.interfaces.eth1.ipv4.addresses = [
+        { address = externalRouterAddress; prefixLength = 24; }
+      ];
+      networking.interfaces.eth2.ipv4.addresses = [
+        { address = internalRouterAddress; prefixLength = 24; }
+      ];
+      services.miniupnpd = {
+        enable = true;
+        externalInterface = "eth1";
+        internalIPs = [ "eth2" ];
+        appendConfig = ''
+          ext_ip=${externalRouterAddress}
+        '';
+      };
+    };
+
+    client1 = { pkgs, nodes, ... }: {
+      imports = [ transmissionConfig ];
+      environment.systemPackages = [ pkgs.miniupnpc ];
+
+      virtualisation.vlans = [ 2 ];
+      networking.interfaces.eth0.ipv4.addresses = [];
+      networking.interfaces.eth1.ipv4.addresses = [
+        { address = internalClient1Address; prefixLength = 24; }
+      ];
+      networking.defaultGateway = internalRouterAddress;
+      networking.firewall.enable = false;
+    };
+
+    client2 = { pkgs, ... }: {
+      imports = [ transmissionConfig ];
+
+      virtualisation.vlans = [ 1 ];
+      networking.interfaces.eth0.ipv4.addresses = [];
+      networking.interfaces.eth1.ipv4.addresses = [
+        { address = externalClient2Address; prefixLength = 24; }
+      ];
+      networking.firewall.enable = false;
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+      start_all()
+
+      # Wait for network and miniupnpd.
+      router.wait_for_unit("network-online.target")
+      router.wait_for_unit("miniupnpd")
+
+      # Create the torrent.
+      tracker.succeed("mkdir ${download-dir}/data")
+      tracker.succeed(
+          "cp ${file} ${download-dir}/data/test.tar.bz2"
+      )
+      tracker.succeed(
+          "transmission-create ${download-dir}/data/test.tar.bz2 --private --tracker http://${externalTrackerAddress}:6969/announce --outfile /tmp/test.torrent"
+      )
+      tracker.succeed("chmod 644 /tmp/test.torrent")
+
+      # Start the tracker.  !!! use a less crappy tracker
+      tracker.wait_for_unit("network-online.target")
+      tracker.wait_for_unit("opentracker.service")
+      tracker.wait_for_open_port(6969)
+
+      # Start the initial seeder.
+      tracker.succeed(
+          "transmission-remote --add /tmp/test.torrent --no-portmap --no-dht --download-dir ${download-dir}/data"
+      )
+
+      # Now we should be able to download from the client behind the NAT.
+      tracker.wait_for_unit("httpd")
+      client1.wait_for_unit("network-online.target")
+      client1.succeed("transmission-remote --add http://${externalTrackerAddress}/test.torrent >&2 &")
+      client1.wait_for_file("${download-dir}/test.tar.bz2")
+      client1.succeed(
+          "cmp ${download-dir}/test.tar.bz2 ${file}"
+      )
+
+      # Bring down the initial seeder.
+      # tracker.stop_job("transmission")
+
+      # Now download from the second client.  This can only succeed if
+      # the first client created a NAT hole in the router.
+      client2.wait_for_unit("network-online.target")
+      client2.succeed(
+          "transmission-remote --add http://${externalTrackerAddress}/test.torrent --no-portmap --no-dht >&2 &"
+      )
+      client2.wait_for_file("${download-dir}/test.tar.bz2")
+      client2.succeed(
+          "cmp ${download-dir}/test.tar.bz2 ${file}"
+      )
+    '';
+})
diff --git a/nixos/tests/blockbook-frontend.nix b/nixos/tests/blockbook-frontend.nix
new file mode 100644
index 00000000000..e17a2d05779
--- /dev/null
+++ b/nixos/tests/blockbook-frontend.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "blockbook-frontend";
+  meta = with pkgs.lib; {
+    maintainers = with maintainers; [ _1000101 ];
+  };
+
+  machine = { ... }: {
+    services.blockbook-frontend."test" = {
+      enable = true;
+    };
+    services.bitcoind.mainnet = {
+      enable = true;
+      rpc = {
+        port = 8030;
+        users.rpc.passwordHMAC = "acc2374e5f9ba9e62a5204d3686616cf$53abdba5e67a9005be6a27ca03a93ce09e58854bc2b871523a0d239a72968033";
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("blockbook-frontend-test.service")
+
+    machine.wait_for_open_port(9030)
+
+    machine.succeed("curl -sSfL http://localhost:9030 | grep 'Blockbook'")
+  '';
+})
diff --git a/nixos/tests/blocky.nix b/nixos/tests/blocky.nix
new file mode 100644
index 00000000000..18e7f45e1c7
--- /dev/null
+++ b/nixos/tests/blocky.nix
@@ -0,0 +1,34 @@
+import ./make-test-python.nix {
+  name = "blocky";
+
+  nodes = {
+    server = { pkgs, ... }: {
+      environment.systemPackages = [ pkgs.dnsutils ];
+      services.blocky = {
+        enable = true;
+
+        settings = {
+          customDNS = {
+            mapping = {
+              "printer.lan" = "192.168.178.3,2001:0db8:85a3:08d3:1319:8a2e:0370:7344";
+            };
+          };
+          upstream = {
+            default = [ "8.8.8.8" "1.1.1.1" ];
+          };
+          port = 53;
+          httpPort = 5000;
+          logLevel = "info";
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    with subtest("Service test"):
+        server.wait_for_unit("blocky.service")
+        server.wait_for_open_port(53)
+        server.wait_for_open_port(5000)
+        server.succeed("dig @127.0.0.1 +short -x 192.168.178.3 | grep -qF printer.lan")
+  '';
+}
diff --git a/nixos/tests/boot-stage1.nix b/nixos/tests/boot-stage1.nix
new file mode 100644
index 00000000000..756decd2039
--- /dev/null
+++ b/nixos/tests/boot-stage1.nix
@@ -0,0 +1,164 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "boot-stage1";
+
+  machine = { config, pkgs, lib, ... }: {
+    boot.extraModulePackages = let
+      compileKernelModule = name: source: pkgs.runCommandCC name rec {
+        inherit source;
+        kdev = config.boot.kernelPackages.kernel.dev;
+        kver = config.boot.kernelPackages.kernel.modDirVersion;
+        ksrc = "${kdev}/lib/modules/${kver}/build";
+        hardeningDisable = [ "pic" ];
+        nativeBuildInputs = kdev.moduleBuildDependencies;
+      } ''
+        echo "obj-m += $name.o" > Makefile
+        echo "$source" > "$name.c"
+        make -C "$ksrc" M=$(pwd) modules
+        install -vD "$name.ko" "$out/lib/modules/$kver/$name.ko"
+      '';
+
+      # This spawns a kthread which just waits until it gets a signal and
+      # terminates if that is the case. We want to make sure that nothing during
+      # the boot process kills any kthread by accident, like what happened in
+      # issue #15226.
+      kcanary = compileKernelModule "kcanary" ''
+        #include <linux/version.h>
+        #include <linux/init.h>
+        #include <linux/module.h>
+        #include <linux/kernel.h>
+        #include <linux/kthread.h>
+        #include <linux/sched.h>
+        #include <linux/signal.h>
+        #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 10, 0)
+        #include <linux/sched/signal.h>
+        #endif
+
+        MODULE_LICENSE("GPL");
+
+        struct task_struct *canaryTask;
+
+        static int kcanary(void *nothing)
+        {
+          allow_signal(SIGINT);
+          allow_signal(SIGTERM);
+          allow_signal(SIGKILL);
+          while (!kthread_should_stop()) {
+            set_current_state(TASK_INTERRUPTIBLE);
+            schedule_timeout_interruptible(msecs_to_jiffies(100));
+            if (signal_pending(current)) break;
+          }
+          return 0;
+        }
+
+        static int kcanaryInit(void)
+        {
+          kthread_run(&kcanary, NULL, "kcanary");
+          return 0;
+        }
+
+        static void kcanaryExit(void)
+        {
+          kthread_stop(canaryTask);
+        }
+
+        module_init(kcanaryInit);
+        module_exit(kcanaryExit);
+      '';
+
+    in lib.singleton kcanary;
+
+    boot.initrd.kernelModules = [ "kcanary" ];
+
+    boot.initrd.extraUtilsCommands = let
+      compile = name: source: pkgs.runCommandCC name { inherit source; } ''
+        mkdir -p "$out/bin"
+        echo "$source" | gcc -Wall -o "$out/bin/$name" -xc -
+      '';
+
+      daemonize = name: source: compile name ''
+        #include <stdio.h>
+        #include <unistd.h>
+
+        void runSource(void) {
+        ${source}
+        }
+
+        int main(void) {
+          if (fork() > 0) return 0;
+          setsid();
+          runSource();
+          return 1;
+        }
+      '';
+
+      mkCmdlineCanary = { name, cmdline ? "", source ? "" }: (daemonize name ''
+        char *argv[] = {"${cmdline}", NULL};
+        execvp("${name}-child", argv);
+      '') // {
+        child = compile "${name}-child" ''
+          #include <stdio.h>
+          #include <unistd.h>
+
+          int main(void) {
+            ${source}
+            while (1) sleep(1);
+            return 1;
+          }
+        '';
+      };
+
+      copyCanaries = with lib; concatMapStrings (canary: ''
+        ${optionalString (canary ? child) ''
+          copy_bin_and_libs "${canary.child}/bin/${canary.child.name}"
+        ''}
+        copy_bin_and_libs "${canary}/bin/${canary.name}"
+      '');
+
+    in copyCanaries [
+      # Simple canary process which just sleeps forever and should be killed by
+      # stage 2.
+      (daemonize "canary1" "while (1) sleep(1);")
+
+      # We want this canary process to try mimicking a kthread using a cmdline
+      # with a zero length so we can make sure that the process is properly
+      # killed in stage 1.
+      (mkCmdlineCanary {
+        name = "canary2";
+        source = ''
+          FILE *f;
+          f = fopen("/run/canary2.pid", "w");
+          fprintf(f, "%d\n", getpid());
+          fclose(f);
+        '';
+      })
+
+      # This canary process mimicks a storage daemon, which we do NOT want to be
+      # killed before going into stage 2. For more on root storage daemons, see:
+      # https://www.freedesktop.org/wiki/Software/systemd/RootStorageDaemons/
+      (mkCmdlineCanary {
+        name = "canary3";
+        cmdline = "@canary3";
+      })
+    ];
+
+    boot.initrd.postMountCommands = ''
+      canary1
+      canary2
+      canary3
+      # Make sure the pidfile of canary 2 is created so that we still can get
+      # its former pid after the killing spree starts next within stage 1.
+      while [ ! -s /run/canary2.pid ]; do sleep 0.1; done
+    '';
+  };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed("test -s /run/canary2.pid")
+    machine.fail("pgrep -a canary1")
+    machine.fail("kill -0 $(< /run/canary2.pid)")
+    machine.succeed('pgrep -a -f "^@canary3$"')
+    machine.succeed('pgrep -a -f "^kcanary$"')
+  '';
+
+  meta.maintainers = with pkgs.lib.maintainers; [ aszlig ];
+})
diff --git a/nixos/tests/boot.nix b/nixos/tests/boot.nix
new file mode 100644
index 00000000000..cf556566713
--- /dev/null
+++ b/nixos/tests/boot.nix
@@ -0,0 +1,149 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  qemu-common = import ../lib/qemu-common.nix { inherit (pkgs) lib pkgs; };
+
+  iso =
+    (import ../lib/eval-config.nix {
+      inherit system;
+      modules = [
+        ../modules/installer/cd-dvd/installation-cd-minimal.nix
+        ../modules/testing/test-instrumentation.nix
+      ];
+    }).config.system.build.isoImage;
+
+  sd =
+    (import ../lib/eval-config.nix {
+      inherit system;
+      modules = [
+        ../modules/installer/sd-card/sd-image-x86_64.nix
+        ../modules/testing/test-instrumentation.nix
+        { sdImage.compressImage = false; }
+      ];
+    }).config.system.build.sdImage;
+
+  pythonDict = params: "\n    {\n        ${concatStringsSep ",\n        " (mapAttrsToList (name: param: "\"${name}\": \"${param}\"") params)},\n    }\n";
+
+  makeBootTest = name: extraConfig:
+    let
+      machineConfig = pythonDict ({
+        qemuBinary = qemu-common.qemuBinary pkgs.qemu_test;
+        qemuFlags = "-m 768";
+      } // extraConfig);
+    in
+      makeTest {
+        inherit iso;
+        name = "boot-" + name;
+        nodes = { };
+        testScript =
+          ''
+            machine = create_machine(${machineConfig})
+            machine.start()
+            machine.wait_for_unit("multi-user.target")
+            machine.succeed("nix store verify --no-trust -r --option experimental-features nix-command /run/current-system")
+
+            with subtest("Check whether the channel got installed correctly"):
+                machine.succeed("nix-instantiate --dry-run '<nixpkgs>' -A hello")
+                machine.succeed("nix-env --dry-run -iA nixos.procps")
+
+            machine.shutdown()
+          '';
+      };
+
+  makeNetbootTest = name: extraConfig:
+    let
+      config = (import ../lib/eval-config.nix {
+          inherit system;
+          modules =
+            [ ../modules/installer/netboot/netboot.nix
+              ../modules/testing/test-instrumentation.nix
+              { key = "serial"; }
+            ];
+        }).config;
+      ipxeBootDir = pkgs.symlinkJoin {
+        name = "ipxeBootDir";
+        paths = [
+          config.system.build.netbootRamdisk
+          config.system.build.kernel
+          config.system.build.netbootIpxeScript
+        ];
+      };
+      machineConfig = pythonDict ({
+        qemuBinary = qemu-common.qemuBinary pkgs.qemu_test;
+        qemuFlags = "-boot order=n -m 2000";
+        netBackendArgs = "tftp=${ipxeBootDir},bootfile=netboot.ipxe";
+      } // extraConfig);
+    in
+      makeTest {
+        name = "boot-netboot-" + name;
+        nodes = { };
+        testScript = ''
+            machine = create_machine(${machineConfig})
+            machine.start()
+            machine.wait_for_unit("multi-user.target")
+            machine.shutdown()
+          '';
+      };
+  uefiBinary = {
+    x86_64-linux = "${pkgs.OVMF.fd}/FV/OVMF.fd";
+    aarch64-linux = "${pkgs.OVMF.fd}/FV/QEMU_EFI.fd";
+  }.${pkgs.stdenv.hostPlatform.system};
+in {
+    uefiCdrom = makeBootTest "uefi-cdrom" {
+      cdrom = "${iso}/iso/${iso.isoName}";
+      bios = uefiBinary;
+    };
+
+    uefiUsb = makeBootTest "uefi-usb" {
+      usb = "${iso}/iso/${iso.isoName}";
+      bios = uefiBinary;
+    };
+
+    uefiNetboot = makeNetbootTest "uefi" {
+      bios = uefiBinary;
+      # Custom ROM is needed for EFI PXE boot. I failed to understand exactly why, because QEMU should still use iPXE for EFI.
+      netFrontendArgs = "romfile=${pkgs.ipxe}/ipxe.efirom";
+    };
+} // optionalAttrs (pkgs.stdenv.hostPlatform.system == "x86_64-linux") {
+    biosCdrom = makeBootTest "bios-cdrom" {
+      cdrom = "${iso}/iso/${iso.isoName}";
+    };
+
+    biosUsb = makeBootTest "bios-usb" {
+      usb = "${iso}/iso/${iso.isoName}";
+    };
+
+    biosNetboot = makeNetbootTest "bios" {};
+
+    ubootExtlinux = let
+      sdImage = "${sd}/sd-image/${sd.imageName}";
+      mutableImage = "/tmp/linked-image.qcow2";
+
+      machineConfig = pythonDict {
+        bios = "${pkgs.ubootQemuX86}/u-boot.rom";
+        qemuFlags = "-m 768 -machine type=pc,accel=tcg -drive file=${mutableImage},if=ide,format=qcow2";
+      };
+    in makeTest {
+      name = "boot-uboot-extlinux";
+      nodes = { };
+      testScript = ''
+        import os
+
+        # Create a mutable linked image backed by the read-only SD image
+        if os.system("qemu-img create -f qcow2 -F raw -b ${sdImage} ${mutableImage}") != 0:
+            raise RuntimeError("Could not create mutable linked image")
+
+        machine = create_machine(${machineConfig})
+        machine.start()
+        machine.wait_for_unit("multi-user.target")
+        machine.succeed("nix store verify -r --no-trust --option experimental-features nix-command /run/current-system")
+        machine.shutdown()
+      '';
+    };
+}
diff --git a/nixos/tests/borgbackup.nix b/nixos/tests/borgbackup.nix
new file mode 100644
index 00000000000..d3cd6c66bfe
--- /dev/null
+++ b/nixos/tests/borgbackup.nix
@@ -0,0 +1,208 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+  passphrase = "supersecret";
+  dataDir = "/ran:dom/data";
+  excludeFile = "not_this_file";
+  keepFile = "important_file";
+  keepFileData = "important_data";
+  localRepo = "/root/back:up";
+  archiveName = "my_archive";
+  remoteRepo = "borg@server:."; # No need to specify path
+  privateKey = pkgs.writeText "id_ed25519" ''
+    -----BEGIN OPENSSH PRIVATE KEY-----
+    b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+    QyNTUxOQAAACBx8UB04Q6Q/fwDFjakHq904PYFzG9pU2TJ9KXpaPMcrwAAAJB+cF5HfnBe
+    RwAAAAtzc2gtZWQyNTUxOQAAACBx8UB04Q6Q/fwDFjakHq904PYFzG9pU2TJ9KXpaPMcrw
+    AAAEBN75NsJZSpt63faCuaD75Unko0JjlSDxMhYHAPJk2/xXHxQHThDpD9/AMWNqQer3Tg
+    9gXMb2lTZMn0pelo8xyvAAAADXJzY2h1ZXR6QGt1cnQ=
+    -----END OPENSSH PRIVATE KEY-----
+  '';
+  publicKey = ''
+    ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHHxQHThDpD9/AMWNqQer3Tg9gXMb2lTZMn0pelo8xyv root@client
+  '';
+  privateKeyAppendOnly = pkgs.writeText "id_ed25519" ''
+    -----BEGIN OPENSSH PRIVATE KEY-----
+    b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+    QyNTUxOQAAACBacZuz1ELGQdhI7PF6dGFafCDlvh8pSEc4cHjkW0QjLwAAAJC9YTxxvWE8
+    cQAAAAtzc2gtZWQyNTUxOQAAACBacZuz1ELGQdhI7PF6dGFafCDlvh8pSEc4cHjkW0QjLw
+    AAAEAAhV7wTl5dL/lz+PF/d4PnZXuG1Id6L/mFEiGT1tZsuFpxm7PUQsZB2Ejs8Xp0YVp8
+    IOW+HylIRzhweORbRCMvAAAADXJzY2h1ZXR6QGt1cnQ=
+    -----END OPENSSH PRIVATE KEY-----
+  '';
+  publicKeyAppendOnly = ''
+    ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFpxm7PUQsZB2Ejs8Xp0YVp8IOW+HylIRzhweORbRCMv root@client
+  '';
+
+in {
+  name = "borgbackup";
+  meta = with pkgs.lib; {
+    maintainers = with maintainers; [ dotlambda ];
+  };
+
+  nodes = {
+    client = { ... }: {
+      services.borgbackup.jobs = {
+
+        local = {
+          paths = dataDir;
+          repo = localRepo;
+          preHook = ''
+            # Don't append a timestamp
+            archiveName="${archiveName}"
+          '';
+          encryption = {
+            mode = "repokey";
+            inherit passphrase;
+          };
+          compression = "auto,zlib,9";
+          prune.keep = {
+            within = "1y";
+            yearly = 5;
+          };
+          exclude = [ "*/${excludeFile}" ];
+          postHook = "echo post";
+          startAt = [ ]; # Do not run automatically
+        };
+
+        remote = {
+          paths = dataDir;
+          repo = remoteRepo;
+          encryption.mode = "none";
+          startAt = [ ];
+          environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
+        };
+
+        remoteAppendOnly = {
+          paths = dataDir;
+          repo = remoteRepo;
+          encryption.mode = "none";
+          startAt = [ ];
+          environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519.appendOnly";
+        };
+
+        commandSuccess = {
+          dumpCommand = pkgs.writeScript "commandSuccess" ''
+            echo -n test
+          '';
+          repo = remoteRepo;
+          encryption.mode = "none";
+          startAt = [ ];
+          environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
+        };
+
+        commandFail = {
+          dumpCommand = "${pkgs.coreutils}/bin/false";
+          repo = remoteRepo;
+          encryption.mode = "none";
+          startAt = [ ];
+          environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
+        };
+
+      };
+    };
+
+    server = { ... }: {
+      services.openssh = {
+        enable = true;
+        passwordAuthentication = false;
+        kbdInteractiveAuthentication = false;
+      };
+
+      services.borgbackup.repos.repo1 = {
+        authorizedKeys = [ publicKey ];
+        path = "/data/borgbackup";
+      };
+
+      # Second repo to make sure the authorizedKeys options are merged correctly
+      services.borgbackup.repos.repo2 = {
+        authorizedKeysAppendOnly = [ publicKeyAppendOnly ];
+        path = "/data/borgbackup";
+        quota = ".5G";
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    client.fail('test -d "${remoteRepo}"')
+
+    client.succeed(
+        "cp ${privateKey} /root/id_ed25519"
+    )
+    client.succeed("chmod 0600 /root/id_ed25519")
+    client.succeed(
+        "cp ${privateKeyAppendOnly} /root/id_ed25519.appendOnly"
+    )
+    client.succeed("chmod 0600 /root/id_ed25519.appendOnly")
+
+    client.succeed("mkdir -p ${dataDir}")
+    client.succeed("touch ${dataDir}/${excludeFile}")
+    client.succeed("echo '${keepFileData}' > ${dataDir}/${keepFile}")
+
+    with subtest("local"):
+        borg = "BORG_PASSPHRASE='${passphrase}' borg"
+        client.systemctl("start --wait borgbackup-job-local")
+        client.fail("systemctl is-failed borgbackup-job-local")
+        # Make sure exactly one archive has been created
+        assert int(client.succeed("{} list '${localRepo}' | wc -l".format(borg))) > 0
+        # Make sure excludeFile has been excluded
+        client.fail(
+            "{} list '${localRepo}::${archiveName}' | grep -qF '${excludeFile}'".format(borg)
+        )
+        # Make sure keepFile has the correct content
+        client.succeed("{} extract '${localRepo}::${archiveName}'".format(borg))
+        assert "${keepFileData}" in client.succeed("cat ${dataDir}/${keepFile}")
+        # Make sure the same is true when using `borg mount`
+        client.succeed(
+            "mkdir -p /mnt/borg && {} mount '${localRepo}::${archiveName}' /mnt/borg".format(
+                borg
+            )
+        )
+        assert "${keepFileData}" in client.succeed(
+            "cat /mnt/borg/${dataDir}/${keepFile}"
+        )
+
+    with subtest("remote"):
+        borg = "BORG_RSH='ssh -oStrictHostKeyChecking=no -i /root/id_ed25519' borg"
+        server.wait_for_unit("sshd.service")
+        client.wait_for_unit("network.target")
+        client.systemctl("start --wait borgbackup-job-remote")
+        client.fail("systemctl is-failed borgbackup-job-remote")
+
+        # Make sure we can't access repos other than the specified one
+        client.fail("{} list borg\@server:wrong".format(borg))
+
+        # TODO: Make sure that data is actually deleted
+
+    with subtest("remoteAppendOnly"):
+        borg = (
+            "BORG_RSH='ssh -oStrictHostKeyChecking=no -i /root/id_ed25519.appendOnly' borg"
+        )
+        server.wait_for_unit("sshd.service")
+        client.wait_for_unit("network.target")
+        client.systemctl("start --wait borgbackup-job-remoteAppendOnly")
+        client.fail("systemctl is-failed borgbackup-job-remoteAppendOnly")
+
+        # Make sure we can't access repos other than the specified one
+        client.fail("{} list borg\@server:wrong".format(borg))
+
+        # TODO: Make sure that data is not actually deleted
+
+    with subtest("commandSuccess"):
+        server.wait_for_unit("sshd.service")
+        client.wait_for_unit("network.target")
+        client.systemctl("start --wait borgbackup-job-commandSuccess")
+        client.fail("systemctl is-failed borgbackup-job-commandSuccess")
+        id = client.succeed("borg-job-commandSuccess list | tail -n1 | cut -d' ' -f1").strip()
+        client.succeed(f"borg-job-commandSuccess extract ::{id} stdin")
+        assert "test" == client.succeed("cat stdin")
+
+    with subtest("commandFail"):
+        server.wait_for_unit("sshd.service")
+        client.wait_for_unit("network.target")
+        client.systemctl("start --wait borgbackup-job-commandFail")
+        client.succeed("systemctl is-failed borgbackup-job-commandFail")
+  '';
+})
diff --git a/nixos/tests/botamusique.nix b/nixos/tests/botamusique.nix
new file mode 100644
index 00000000000..ccb105dc142
--- /dev/null
+++ b/nixos/tests/botamusique.nix
@@ -0,0 +1,47 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} :
+
+{
+  name = "botamusique";
+  meta.maintainers = with lib.maintainers; [ hexa ];
+
+  nodes = {
+    machine = { config, ... }: {
+      services.murmur = {
+        enable = true;
+        registerName = "NixOS tests";
+      };
+
+      services.botamusique = {
+        enable = true;
+        settings = {
+          server = {
+            channel = "NixOS tests";
+          };
+          bot = {
+            version = false;
+            auto_check_update = false;
+          };
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("murmur.service")
+    machine.wait_for_unit("botamusique.service")
+
+    machine.sleep(10)
+
+    machine.wait_until_succeeds(
+        "journalctl -u murmur.service -e | grep -q '<1:botamusique(-1)> Authenticated'"
+    )
+
+    with subtest("Check systemd hardening"):
+        output = machine.execute("systemctl show botamusique.service")[1]
+        machine.log(output)
+        output = machine.execute("systemd-analyze security botamusique.service")[1]
+        machine.log(output)
+  '';
+})
diff --git a/nixos/tests/bpf.nix b/nixos/tests/bpf.nix
new file mode 100644
index 00000000000..e479cd05792
--- /dev/null
+++ b/nixos/tests/bpf.nix
@@ -0,0 +1,29 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "bpf";
+  meta.maintainers = with pkgs.lib.maintainers; [ martinetd ];
+
+  machine = { pkgs, ... }: {
+    programs.bcc.enable = true;
+    environment.systemPackages = with pkgs; [ bpftrace ];
+  };
+
+  testScript = ''
+    ## bcc
+    # syscount -d 1 stops 1s after probe started so is good for that
+    print(machine.succeed("syscount -d 1"))
+
+    ## bpftrace
+    # list probes
+    machine.succeed("bpftrace -l")
+    # simple BEGIN probe (user probe on bpftrace itself)
+    print(machine.succeed("bpftrace -e 'BEGIN { print(\"ok\"); exit(); }'"))
+    # tracepoint
+    print(machine.succeed("bpftrace -e 'tracepoint:syscalls:sys_enter_* { print(probe); exit() }'"))
+    # kprobe
+    print(machine.succeed("bpftrace -e 'kprobe:schedule { print(probe); exit() }'"))
+    # BTF
+    print(machine.succeed("bpftrace -e 'kprobe:schedule { "
+        "    printf(\"tgid: %d\", ((struct task_struct*) curtask)->tgid); exit() "
+        "}'"))
+  '';
+})
diff --git a/nixos/tests/breitbandmessung.nix b/nixos/tests/breitbandmessung.nix
new file mode 100644
index 00000000000..12b1a094839
--- /dev/null
+++ b/nixos/tests/breitbandmessung.nix
@@ -0,0 +1,33 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "breitbandmessung";
+  meta.maintainers = with lib.maintainers; [ b4dm4n ];
+
+  machine = { pkgs, ... }: {
+    imports = [
+      ./common/user-account.nix
+      ./common/x11.nix
+    ];
+
+    # increase screen size to make the whole program visible
+    virtualisation.resolution = { x = 1280; y = 1024; };
+
+    test-support.displayManager.auto.user = "alice";
+
+    environment.systemPackages = with pkgs; [ breitbandmessung ];
+    environment.variables.XAUTHORITY = "/home/alice/.Xauthority";
+
+    # breitbandmessung is unfree
+    nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "breitbandmessung" ];
+  };
+
+  enableOCR = true;
+
+  testScript = ''
+    machine.wait_for_x()
+    machine.execute("su - alice -c breitbandmessung >&2  &")
+    machine.wait_for_window("Breitbandmessung")
+    machine.wait_for_text("Breitbandmessung")
+    machine.wait_for_text("Datenschutz")
+    machine.screenshot("breitbandmessung")
+  '';
+})
diff --git a/nixos/tests/brscan5.nix b/nixos/tests/brscan5.nix
new file mode 100644
index 00000000000..9aed742f6de
--- /dev/null
+++ b/nixos/tests/brscan5.nix
@@ -0,0 +1,43 @@
+# integration tests for brscan5 sane driver
+#
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "brscan5";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mattchrist ];
+  };
+
+  machine = { pkgs, ... }:
+    {
+      nixpkgs.config.allowUnfree = true;
+      hardware.sane = {
+        enable = true;
+        brscan5 = {
+          enable = true;
+          netDevices = {
+            "a" = { model="ADS-1200"; nodename="BRW0080927AFBCE"; };
+            "b" = { model="ADS-1200"; ip="192.168.1.2"; };
+          };
+        };
+      };
+    };
+
+  testScript = ''
+    import re
+    # sane loads libsane-brother5.so.1 successfully, and scanimage doesn't die
+    strace = machine.succeed('strace scanimage -L 2>&1').split("\n")
+    regexp = 'openat\(.*libsane-brother5.so.1", O_RDONLY|O_CLOEXEC\) = \d\d*$'
+    assert len([x for x in strace if re.match(regexp,x)]) > 0
+
+    # module creates a config
+    cfg = machine.succeed('cat /etc/opt/brother/scanner/brscan5/brsanenetdevice.cfg')
+    assert 'DEVICE=a , "ADS-1200" , 0x4f9:0x459 , NODENAME=BRW0080927AFBCE' in cfg
+    assert 'DEVICE=b , "ADS-1200" , 0x4f9:0x459 , IP-ADDRESS=192.168.1.2' in cfg
+
+    # scanimage lists the two network scanners
+    scanimage = machine.succeed("scanimage -L")
+    print(scanimage)
+    assert """device `brother5:net1;dev0' is a Brother b ADS-1200""" in scanimage
+    assert """device `brother5:net1;dev1' is a Brother a ADS-1200""" in scanimage
+  '';
+})
diff --git a/nixos/tests/btrbk.nix b/nixos/tests/btrbk.nix
new file mode 100644
index 00000000000..9f34f7dfbe3
--- /dev/null
+++ b/nixos/tests/btrbk.nix
@@ -0,0 +1,110 @@
+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";
+    meta = with pkgs.lib; {
+      maintainers = with maintainers; [ symphorien ];
+    };
+
+    nodes = {
+      archive = { ... }: {
+        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 = { ... }: {
+        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/buildbot.nix b/nixos/tests/buildbot.nix
new file mode 100644
index 00000000000..977c728835f
--- /dev/null
+++ b/nixos/tests/buildbot.nix
@@ -0,0 +1,113 @@
+# Test ensures buildbot master comes up correctly and workers can connect
+
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+import ./make-test-python.nix {
+  name = "buildbot";
+
+  nodes = {
+    bbmaster = { pkgs, ... }: {
+      services.buildbot-master = {
+        enable = true;
+
+        # NOTE: use fake repo due to no internet in hydra ci
+        factorySteps = [
+          "steps.Git(repourl='git://gitrepo/fakerepo.git', mode='incremental')"
+          "steps.ShellCommand(command=['bash', 'fakerepo.sh'])"
+        ];
+        changeSource = [
+          "changes.GitPoller('git://gitrepo/fakerepo.git', workdir='gitpoller-workdir', branch='master', pollinterval=300)"
+        ];
+      };
+      networking.firewall.allowedTCPPorts = [ 8010 8011 9989 ];
+      environment.systemPackages = with pkgs; [ git python3Packages.buildbot-full ];
+    };
+
+    bbworker = { pkgs, ... }: {
+      services.buildbot-worker = {
+        enable = true;
+        masterUrl = "bbmaster:9989";
+      };
+      environment.systemPackages = with pkgs; [ git python3Packages.buildbot-worker ];
+    };
+
+    gitrepo = { pkgs, ... }: {
+      services.openssh.enable = true;
+      networking.firewall.allowedTCPPorts = [ 22 9418 ];
+      environment.systemPackages = with pkgs; [ git ];
+      systemd.services.git-daemon = {
+        description   = "Git daemon for the test";
+        wantedBy      = [ "multi-user.target" ];
+        after         = [ "network.target" "sshd.service" ];
+
+        serviceConfig.Restart = "always";
+        path = with pkgs; [ coreutils git openssh ];
+        environment = { HOME = "/root"; };
+        preStart = ''
+          git config --global user.name 'Nobody Fakeuser'
+          git config --global user.email 'nobody\@fakerepo.com'
+          rm -rvf /srv/repos/fakerepo.git /tmp/fakerepo
+          mkdir -pv /srv/repos/fakerepo ~/.ssh
+          ssh-keyscan -H gitrepo > ~/.ssh/known_hosts
+          cat ~/.ssh/known_hosts
+
+          mkdir -p /src/repos/fakerepo
+          cd /srv/repos/fakerepo
+          rm -rf *
+          git init
+          echo -e '#!/bin/sh\necho fakerepo' > fakerepo.sh
+          cat fakerepo.sh
+          touch .git/git-daemon-export-ok
+          git add fakerepo.sh .git/git-daemon-export-ok
+          git commit -m fakerepo
+        '';
+        script = ''
+          git daemon --verbose --export-all --base-path=/srv/repos --reuseaddr
+        '';
+      };
+    };
+  };
+
+  testScript = ''
+    gitrepo.wait_for_unit("git-daemon.service")
+    gitrepo.wait_for_unit("multi-user.target")
+
+    with subtest("Repo is accessible via git daemon"):
+        bbmaster.wait_for_unit("network-online.target")
+        bbmaster.succeed("rm -rfv /tmp/fakerepo")
+        bbmaster.succeed("git clone git://gitrepo/fakerepo /tmp/fakerepo")
+
+    with subtest("Master service and worker successfully connect"):
+        bbmaster.wait_for_unit("buildbot-master.service")
+        bbmaster.wait_until_succeeds("curl --fail -s --head http://bbmaster:8010")
+        bbworker.wait_for_unit("network-online.target")
+        bbworker.succeed("nc -z bbmaster 8010")
+        bbworker.succeed("nc -z bbmaster 9989")
+        bbworker.wait_for_unit("buildbot-worker.service")
+
+    with subtest("Stop buildbot worker"):
+        bbmaster.succeed("systemctl -l --no-pager status buildbot-master")
+        bbmaster.succeed("systemctl stop buildbot-master")
+        bbworker.fail("nc -z bbmaster 8010")
+        bbworker.fail("nc -z bbmaster 9989")
+        bbworker.succeed("systemctl -l --no-pager status buildbot-worker")
+        bbworker.succeed("systemctl stop buildbot-worker")
+
+    with subtest("Buildbot daemon mode works"):
+        bbmaster.succeed(
+            "buildbot create-master /tmp",
+            "mv -fv /tmp/master.cfg.sample /tmp/master.cfg",
+            "sed -i 's/8010/8011/' /tmp/master.cfg",
+            "buildbot start /tmp",
+            "nc -z bbmaster 8011",
+        )
+        bbworker.wait_until_succeeds("curl --fail -s --head http://bbmaster:8011")
+        bbmaster.wait_until_succeeds("buildbot stop /tmp")
+        bbworker.fail("nc -z bbmaster 8011")
+  '';
+
+  meta.maintainers = with pkgs.lib.maintainers; [ ];
+} {}
diff --git a/nixos/tests/buildkite-agents.nix b/nixos/tests/buildkite-agents.nix
new file mode 100644
index 00000000000..6674a0e884e
--- /dev/null
+++ b/nixos/tests/buildkite-agents.nix
@@ -0,0 +1,31 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "buildkite-agent";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ flokli ];
+  };
+
+  machine = { pkgs, ... }: {
+    services.buildkite-agents = {
+      one = {
+        privateSshKeyPath = (import ./ssh-keys.nix pkgs).snakeOilPrivateKey;
+        tokenPath = (pkgs.writeText "my-token" "5678");
+      };
+      two = {
+        tokenPath = (pkgs.writeText "my-token" "1234");
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    # we can't wait on the unit to start up, as we obviously can't connect to buildkite,
+    # but we can look whether files are set up correctly
+
+    machine.wait_for_file("/var/lib/buildkite-agent-one/buildkite-agent.cfg")
+    machine.wait_for_file("/var/lib/buildkite-agent-one/.ssh/id_rsa")
+
+    machine.wait_for_file("/var/lib/buildkite-agent-two/buildkite-agent.cfg")
+  '';
+})
diff --git a/nixos/tests/caddy.nix b/nixos/tests/caddy.nix
new file mode 100644
index 00000000000..0902904b208
--- /dev/null
+++ b/nixos/tests/caddy.nix
@@ -0,0 +1,107 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "caddy";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ xfix Br1ght0ne ];
+  };
+
+  nodes = {
+    webserver = { pkgs, lib, ... }: {
+      services.caddy.enable = true;
+      services.caddy.config = ''
+        http://localhost {
+          encode gzip
+
+          file_server
+          root * ${
+            pkgs.runCommand "testdir" {} ''
+              mkdir "$out"
+              echo hello world > "$out/example.html"
+            ''
+          }
+        }
+      '';
+
+      specialisation.etag.configuration = {
+        services.caddy.config = lib.mkForce ''
+          http://localhost {
+            encode gzip
+
+            file_server
+            root * ${
+              pkgs.runCommand "testdir2" {} ''
+                mkdir "$out"
+                echo changed > "$out/example.html"
+              ''
+            }
+          }
+        '';
+      };
+
+      specialisation.config-reload.configuration = {
+        services.caddy.config = ''
+          http://localhost:8080 {
+          }
+        '';
+      };
+      specialisation.multiple-configs.configuration = {
+        services.caddy.virtualHosts = {
+          "http://localhost:8080" = { };
+          "http://localhost:8081" = { };
+        };
+      };
+    };
+  };
+
+  testScript = { nodes, ... }:
+    let
+      etagSystem = "${nodes.webserver.config.system.build.toplevel}/specialisation/etag";
+      justReloadSystem = "${nodes.webserver.config.system.build.toplevel}/specialisation/config-reload";
+      multipleConfigs = "${nodes.webserver.config.system.build.toplevel}/specialisation/multiple-configs";
+    in
+    ''
+      url = "http://localhost/example.html"
+      webserver.wait_for_unit("caddy")
+      webserver.wait_for_open_port("80")
+
+
+      def check_etag(url):
+          etag = webserver.succeed(
+              "curl --fail -v '{}' 2>&1 | sed -n -e \"s/^< [Ee][Tt][Aa][Gg]: *//p\"".format(
+                  url
+              )
+          )
+          etag = etag.replace("\r\n", " ")
+          http_code = webserver.succeed(
+              "curl --fail --silent --show-error -o /dev/null -w \"%{{http_code}}\" --head -H 'If-None-Match: {}' {}".format(
+                  etag, url
+              )
+          )
+          assert int(http_code) == 304, "HTTP code is {}, expected 304".format(http_code)
+          return etag
+
+
+      with subtest("check ETag if serving Nix store paths"):
+          old_etag = check_etag(url)
+          webserver.succeed(
+              "${etagSystem}/bin/switch-to-configuration test >&2"
+          )
+          webserver.sleep(1)
+          new_etag = check_etag(url)
+          assert old_etag != new_etag, "Old ETag {} is the same as {}".format(
+              old_etag, new_etag
+          )
+
+      with subtest("config is reloaded on nixos-rebuild switch"):
+          webserver.succeed(
+              "${justReloadSystem}/bin/switch-to-configuration test >&2"
+          )
+          webserver.wait_for_open_port("8080")
+
+      with subtest("multiple configs are correctly merged"):
+          webserver.succeed(
+              "${multipleConfigs}/bin/switch-to-configuration test >&2"
+          )
+          webserver.wait_for_open_port("8080")
+          webserver.wait_for_open_port("8081")
+    '';
+})
diff --git a/nixos/tests/cadvisor.nix b/nixos/tests/cadvisor.nix
new file mode 100644
index 00000000000..c372dea301d
--- /dev/null
+++ b/nixos/tests/cadvisor.nix
@@ -0,0 +1,34 @@
+import ./make-test-python.nix ({ pkgs, ... } : {
+  name = "cadvisor";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ offline ];
+  };
+
+  nodes = {
+    machine = { ... }: {
+      services.cadvisor.enable = true;
+    };
+
+    influxdb = { lib, ... }: with lib; {
+      services.cadvisor.enable = true;
+      services.cadvisor.storageDriver = "influxdb";
+      services.influxdb.enable = true;
+    };
+  };
+
+  testScript =  ''
+      start_all()
+      machine.wait_for_unit("cadvisor.service")
+      machine.succeed("curl -f http://localhost:8080/containers/")
+
+      influxdb.wait_for_unit("influxdb.service")
+
+      # create influxdb database
+      influxdb.succeed(
+          'curl -f -XPOST http://localhost:8086/query --data-urlencode "q=CREATE DATABASE root"'
+      )
+
+      influxdb.wait_for_unit("cadvisor.service")
+      influxdb.succeed("curl -f http://localhost:8080/containers/")
+    '';
+})
diff --git a/nixos/tests/cage.nix b/nixos/tests/cage.nix
new file mode 100644
index 00000000000..83bae3deeea
--- /dev/null
+++ b/nixos/tests/cage.nix
@@ -0,0 +1,36 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+{
+  name = "cage";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ matthewbauer ];
+  };
+
+  machine = { ... }:
+
+  {
+    imports = [ ./common/user-account.nix ];
+    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";
+    };
+
+    # Need to switch to a different GPU driver than the default one (-vga std) so that Cage can launch:
+    virtualisation.qemu.options = [ "-vga none -device virtio-gpu-pci" ];
+  };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+  in ''
+    with subtest("Wait for cage to boot up"):
+        start_all()
+        machine.wait_for_file("/run/user/${toString user.uid}/wayland-0.lock")
+        machine.wait_until_succeeds("pgrep xterm")
+        machine.wait_for_text("alice@machine")
+        machine.screenshot("screen")
+  '';
+})
diff --git a/nixos/tests/cagebreak.nix b/nixos/tests/cagebreak.nix
new file mode 100644
index 00000000000..c6c2c632b61
--- /dev/null
+++ b/nixos/tests/cagebreak.nix
@@ -0,0 +1,64 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} :
+
+let
+  cagebreakConfigfile = pkgs.writeText "config" ''
+    workspaces 1
+    escape C-t
+    bind t exec env DISPLAY=:0 ${pkgs.xterm}/bin/xterm -cm -pc
+  '';
+in
+{
+  name = "cagebreak";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ berbiche ];
+  };
+
+  machine = { config, ... }:
+  let
+    alice = config.users.users.alice;
+  in {
+    # Automatically login on tty1 as a normal user:
+    imports = [ ./common/user-account.nix ];
+    services.getty.autologinUser = "alice";
+    programs.bash.loginShellInit = ''
+      if [ "$(tty)" = "/dev/tty1" ]; then
+        set -e
+
+        mkdir -p ~/.config/cagebreak
+        cp -f ${cagebreakConfigfile} ~/.config/cagebreak/config
+
+        cagebreak
+      fi
+    '';
+
+    hardware.opengl.enable = true;
+    programs.xwayland.enable = true;
+    environment.systemPackages = [ pkgs.cagebreak pkgs.wayland-utils ];
+
+    # Need to switch to a different GPU driver than the default one (-vga std) so that Cagebreak can launch:
+    virtualisation.qemu.options = [ "-vga none -device virtio-gpu-pci" ];
+  };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+    XDG_RUNTIME_DIR = "/run/user/${toString user.uid}";
+  in ''
+    start_all()
+    machine.wait_for_unit("multi-user.target")
+    machine.wait_for_file("${XDG_RUNTIME_DIR}/wayland-0")
+
+    with subtest("ensure wayland works with wayinfo from wallutils"):
+        print(machine.succeed("env XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR} wayland-info"))
+
+    # TODO: Fix the XWayland test (log the cagebreak output to debug):
+    # with subtest("ensure xwayland works with xterm"):
+    #     machine.send_key("ctrl-t")
+    #     machine.send_key("t")
+    #     machine.wait_until_succeeds("pgrep xterm")
+    #     machine.wait_for_text("${user.name}@machine")
+    #     machine.screenshot("screen")
+    #     machine.send_key("ctrl-d")
+  '';
+})
diff --git a/nixos/tests/calibre-web.nix b/nixos/tests/calibre-web.nix
new file mode 100644
index 00000000000..9832d546978
--- /dev/null
+++ b/nixos/tests/calibre-web.nix
@@ -0,0 +1,43 @@
+import ./make-test-python.nix (
+  { pkgs, lib, ... }:
+
+    let
+      port = 3142;
+      defaultPort = 8083;
+    in
+      with lib;
+      {
+        name = "calibre-web";
+        meta.maintainers = with pkgs.lib.maintainers; [ pborzenkov ];
+
+        nodes = {
+          customized = { pkgs, ... }: {
+            services.calibre-web = {
+              enable = true;
+              listen.port = port;
+              options = {
+                calibreLibrary = "/tmp/books";
+                reverseProxyAuth = {
+                  enable = true;
+                  header = "X-User";
+                };
+              };
+            };
+            environment.systemPackages = [ pkgs.calibre ];
+          };
+        };
+        testScript = ''
+          start_all()
+
+          customized.succeed(
+              "mkdir /tmp/books && calibredb --library-path /tmp/books add -e --title test-book"
+          )
+          customized.succeed("systemctl restart calibre-web")
+          customized.wait_for_unit("calibre-web.service")
+          customized.wait_for_open_port(${toString port})
+          customized.succeed(
+              "curl --fail -H X-User:admin 'http://localhost:${toString port}' | grep test-book"
+          )
+        '';
+      }
+)
diff --git a/nixos/tests/cassandra.nix b/nixos/tests/cassandra.nix
new file mode 100644
index 00000000000..a19d525c343
--- /dev/null
+++ b/nixos/tests/cassandra.nix
@@ -0,0 +1,132 @@
+import ./make-test-python.nix ({ pkgs, lib, testPackage ? pkgs.cassandra, ... }:
+let
+  clusterName = "NixOS Automated-Test Cluster";
+
+  testRemoteAuth = lib.versionAtLeast testPackage.version "3.11";
+  jmxRoles = [{ username = "me"; password = "password"; }];
+  jmxRolesFile = ./cassandra-jmx-roles;
+  jmxAuthArgs = "-u ${(builtins.elemAt jmxRoles 0).username} -pw ${(builtins.elemAt jmxRoles 0).password}";
+  jmxPort = 7200;  # Non-standard port so it doesn't accidentally work
+  jmxPortStr = toString jmxPort;
+
+  # Would usually be assigned to 512M.
+  # Set it to a different value, so that we can check whether our config
+  # actually changes it.
+  numMaxHeapSize = "400";
+  getHeapLimitCommand = ''
+    nodetool info -p ${jmxPortStr} | grep "^Heap Memory" | awk '{print $NF}'
+  '';
+  checkHeapLimitCommand = pkgs.writeShellScript "check-heap-limit.sh" ''
+    [ 1 -eq "$(echo "$(${getHeapLimitCommand}) < ${numMaxHeapSize}" | ${pkgs.bc}/bin/bc)" ]
+  '';
+
+  cassandraCfg = ipAddress:
+    { enable = true;
+      inherit clusterName;
+      listenAddress = ipAddress;
+      rpcAddress = ipAddress;
+      seedAddresses = [ "192.168.1.1" ];
+      package = testPackage;
+      maxHeapSize = "${numMaxHeapSize}M";
+      heapNewSize = "100M";
+      inherit jmxPort;
+    };
+  nodeCfg = ipAddress: extra: {pkgs, config, ...}: rec {
+    environment.systemPackages = [ testPackage ];
+    networking = {
+      firewall.allowedTCPPorts = [ 7000 9042 services.cassandra.jmxPort ];
+      useDHCP = false;
+      interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+        { address = ipAddress; prefixLength = 24; }
+      ];
+    };
+    services.cassandra = cassandraCfg ipAddress // extra;
+  };
+in
+{
+  name = "cassandra-${testPackage.version}";
+  meta = {
+    maintainers = with lib.maintainers; [ johnazoidberg ];
+  };
+
+  nodes = {
+    cass0 = nodeCfg "192.168.1.1" {};
+    cass1 = nodeCfg "192.168.1.2" (lib.optionalAttrs testRemoteAuth { inherit jmxRoles; remoteJmx = true; });
+    cass2 = nodeCfg "192.168.1.3" { jvmOpts = [ "-Dcassandra.replace_address=cass1" ]; };
+  };
+
+  testScript = ''
+    # Check configuration
+    with subtest("Timers exist"):
+        cass0.succeed("systemctl list-timers | grep cassandra-full-repair.timer")
+        cass0.succeed("systemctl list-timers | grep cassandra-incremental-repair.timer")
+
+    with subtest("Can connect via cqlsh"):
+        cass0.wait_for_unit("cassandra.service")
+        cass0.wait_until_succeeds("nc -z cass0 9042")
+        cass0.succeed("echo 'show version;' | cqlsh cass0")
+
+    with subtest("Nodetool is operational"):
+        cass0.wait_for_unit("cassandra.service")
+        cass0.wait_until_succeeds("nc -z localhost ${jmxPortStr}")
+        cass0.succeed("nodetool status -p ${jmxPortStr} --resolve-ip | egrep '^UN[[:space:]]+cass0'")
+
+    with subtest("Cluster name was set"):
+        cass0.wait_for_unit("cassandra.service")
+        cass0.wait_until_succeeds("nc -z localhost ${jmxPortStr}")
+        cass0.wait_until_succeeds(
+            "nodetool describecluster -p ${jmxPortStr} | grep 'Name: ${clusterName}'"
+        )
+
+    with subtest("Heap limit set correctly"):
+        # Nodetool takes a while until it can display info
+        cass0.wait_until_succeeds("nodetool info -p ${jmxPortStr}")
+        cass0.succeed("${checkHeapLimitCommand}")
+
+    # Check cluster interaction
+    with subtest("Bring up cluster"):
+        cass1.wait_for_unit("cassandra.service")
+        cass1.wait_until_succeeds(
+            "nodetool -p ${jmxPortStr} ${jmxAuthArgs} status | egrep -c '^UN' | grep 2"
+        )
+        cass0.succeed("nodetool status -p ${jmxPortStr} --resolve-ip | egrep '^UN[[:space:]]+cass1'")
+  '' + lib.optionalString testRemoteAuth ''
+    with subtest("Remote authenticated jmx"):
+        # Doesn't work if not enabled
+        cass0.wait_until_succeeds("nc -z localhost ${jmxPortStr}")
+        cass1.fail("nc -z 192.168.1.1 ${jmxPortStr}")
+        cass1.fail("nodetool -p ${jmxPortStr} -h 192.168.1.1 status")
+
+        # Works if enabled
+        cass1.wait_until_succeeds("nc -z localhost ${jmxPortStr}")
+        cass0.succeed("nodetool -p ${jmxPortStr} -h 192.168.1.2 ${jmxAuthArgs} status")
+  '' + ''
+    with subtest("Break and fix node"):
+        cass1.block()
+        cass0.wait_until_succeeds(
+            "nodetool status -p ${jmxPortStr} --resolve-ip | egrep -c '^DN[[:space:]]+cass1'"
+        )
+        cass0.succeed("nodetool status -p ${jmxPortStr} | egrep -c '^UN'  | grep 1")
+        cass1.unblock()
+        cass1.wait_until_succeeds(
+            "nodetool -p ${jmxPortStr} ${jmxAuthArgs} status | egrep -c '^UN'  | grep 2"
+        )
+        cass0.succeed("nodetool status -p ${jmxPortStr} | egrep -c '^UN'  | grep 2")
+
+    with subtest("Replace crashed node"):
+        cass1.block()  # .crash() waits until it's fully shutdown
+        cass2.start()
+        cass0.wait_until_fails(
+            "nodetool status -p ${jmxPortStr} --resolve-ip | egrep '^UN[[:space:]]+cass1'"
+        )
+
+        cass2.wait_for_unit("cassandra.service")
+        cass0.wait_until_succeeds(
+            "nodetool status -p ${jmxPortStr} --resolve-ip | egrep '^UN[[:space:]]+cass2'"
+        )
+  '';
+
+  passthru = {
+    inherit testPackage;
+  };
+})
diff --git a/nixos/tests/ceph-multi-node.nix b/nixos/tests/ceph-multi-node.nix
new file mode 100644
index 00000000000..29e7c279d69
--- /dev/null
+++ b/nixos/tests/ceph-multi-node.nix
@@ -0,0 +1,233 @@
+import ./make-test-python.nix ({pkgs, lib, ...}:
+
+let
+  cfg = {
+    clusterId = "066ae264-2a5d-4729-8001-6ad265f50b03";
+    monA = {
+      name = "a";
+      ip = "192.168.1.1";
+    };
+    osd0 = {
+      name = "0";
+      ip = "192.168.1.2";
+      key = "AQBCEJNa3s8nHRAANvdsr93KqzBznuIWm2gOGg==";
+      uuid = "55ba2294-3e24-478f-bee0-9dca4c231dd9";
+    };
+    osd1 = {
+      name = "1";
+      ip = "192.168.1.3";
+      key = "AQBEEJNac00kExAAXEgy943BGyOpVH1LLlHafQ==";
+      uuid = "5e97a838-85b6-43b0-8950-cb56d554d1e5";
+    };
+    osd2 = {
+      name = "2";
+      ip = "192.168.1.4";
+      key = "AQAdyhZeIaUlARAAGRoidDAmS6Vkp546UFEf5w==";
+      uuid = "ea999274-13d0-4dd5-9af9-ad25a324f72f";
+    };
+  };
+  generateCephConfig = { daemonConfig }: {
+    enable = true;
+    global = {
+      fsid = cfg.clusterId;
+      monHost = cfg.monA.ip;
+      monInitialMembers = cfg.monA.name;
+    };
+  } // daemonConfig;
+
+  generateHost = { pkgs, cephConfig, networkConfig, ... }: {
+    virtualisation = {
+      emptyDiskImages = [ 20480 ];
+      vlans = [ 1 ];
+    };
+
+    networking = networkConfig;
+
+    environment.systemPackages = with pkgs; [
+      bash
+      sudo
+      ceph
+      xfsprogs
+      netcat-openbsd
+    ];
+
+    boot.kernelModules = [ "xfs" ];
+
+    services.ceph = cephConfig;
+  };
+
+  networkMonA = {
+    dhcpcd.enable = false;
+    interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+      { address = cfg.monA.ip; prefixLength = 24; }
+    ];
+    firewall = {
+      allowedTCPPorts = [ 6789 3300 ];
+      allowedTCPPortRanges = [ { from = 6800; to = 7300; } ];
+    };
+  };
+  cephConfigMonA = generateCephConfig { daemonConfig = {
+    mon = {
+      enable = true;
+      daemons = [ cfg.monA.name ];
+    };
+    mgr = {
+      enable = true;
+      daemons = [ cfg.monA.name ];
+    };
+  }; };
+
+  networkOsd = osd: {
+    dhcpcd.enable = false;
+    interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+      { address = osd.ip; prefixLength = 24; }
+    ];
+    firewall = {
+      allowedTCPPortRanges = [ { from = 6800; to = 7300; } ];
+    };
+  };
+
+  cephConfigOsd = osd: generateCephConfig { daemonConfig = {
+    osd = {
+      enable = true;
+      daemons = [ osd.name ];
+    };
+  }; };
+
+  # Following deployment is based on the manual deployment described here:
+  # https://docs.ceph.com/docs/master/install/manual-deployment/
+  # For other ways to deploy a ceph cluster, look at the documentation at
+  # https://docs.ceph.com/docs/master/
+  testscript = { ... }: ''
+    start_all()
+
+    monA.wait_for_unit("network.target")
+    osd0.wait_for_unit("network.target")
+    osd1.wait_for_unit("network.target")
+    osd2.wait_for_unit("network.target")
+
+    # Bootstrap ceph-mon daemon
+    monA.succeed(
+        "sudo -u ceph ceph-authtool --create-keyring /tmp/ceph.mon.keyring --gen-key -n mon. --cap mon 'allow *'",
+        "sudo -u ceph ceph-authtool --create-keyring /etc/ceph/ceph.client.admin.keyring --gen-key -n client.admin --cap mon 'allow *' --cap osd 'allow *' --cap mds 'allow *' --cap mgr 'allow *'",
+        "sudo -u ceph ceph-authtool /tmp/ceph.mon.keyring --import-keyring /etc/ceph/ceph.client.admin.keyring",
+        "monmaptool --create --add ${cfg.monA.name} ${cfg.monA.ip} --fsid ${cfg.clusterId} /tmp/monmap",
+        "sudo -u ceph ceph-mon --mkfs -i ${cfg.monA.name} --monmap /tmp/monmap --keyring /tmp/ceph.mon.keyring",
+        "sudo -u ceph mkdir -p /var/lib/ceph/mgr/ceph-${cfg.monA.name}/",
+        "sudo -u ceph touch /var/lib/ceph/mon/ceph-${cfg.monA.name}/done",
+        "systemctl start ceph-mon-${cfg.monA.name}",
+    )
+    monA.wait_for_unit("ceph-mon-${cfg.monA.name}")
+    monA.succeed("ceph mon enable-msgr2")
+    monA.succeed("ceph config set mon auth_allow_insecure_global_id_reclaim false")
+
+    # Can't check ceph status until a mon is up
+    monA.succeed("ceph -s | grep 'mon: 1 daemons'")
+
+    # Start the ceph-mgr daemon, it has no deps and hardly any setup
+    monA.succeed(
+        "ceph auth get-or-create mgr.${cfg.monA.name} mon 'allow profile mgr' osd 'allow *' mds 'allow *' > /var/lib/ceph/mgr/ceph-${cfg.monA.name}/keyring",
+        "systemctl start ceph-mgr-${cfg.monA.name}",
+    )
+    monA.wait_for_unit("ceph-mgr-a")
+    monA.wait_until_succeeds("ceph -s | grep 'quorum ${cfg.monA.name}'")
+    monA.wait_until_succeeds("ceph -s | grep 'mgr: ${cfg.monA.name}(active,'")
+
+    # Send the admin keyring to the OSD machines
+    monA.succeed("cp /etc/ceph/ceph.client.admin.keyring /tmp/shared")
+    osd0.succeed("cp /tmp/shared/ceph.client.admin.keyring /etc/ceph")
+    osd1.succeed("cp /tmp/shared/ceph.client.admin.keyring /etc/ceph")
+    osd2.succeed("cp /tmp/shared/ceph.client.admin.keyring /etc/ceph")
+
+    # Bootstrap OSDs
+    osd0.succeed(
+        "mkfs.xfs /dev/vdb",
+        "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd0.name}",
+        "mount /dev/vdb /var/lib/ceph/osd/ceph-${cfg.osd0.name}",
+        "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd0.name}/keyring --name osd.${cfg.osd0.name} --add-key ${cfg.osd0.key}",
+        'echo \'{"cephx_secret": "${cfg.osd0.key}"}\' | ceph osd new ${cfg.osd0.uuid} -i -',
+    )
+    osd1.succeed(
+        "mkfs.xfs /dev/vdb",
+        "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd1.name}",
+        "mount /dev/vdb /var/lib/ceph/osd/ceph-${cfg.osd1.name}",
+        "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd1.name}/keyring --name osd.${cfg.osd1.name} --add-key ${cfg.osd1.key}",
+        'echo \'{"cephx_secret": "${cfg.osd1.key}"}\' | ceph osd new ${cfg.osd1.uuid} -i -',
+    )
+    osd2.succeed(
+        "mkfs.xfs /dev/vdb",
+        "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd2.name}",
+        "mount /dev/vdb /var/lib/ceph/osd/ceph-${cfg.osd2.name}",
+        "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd2.name}/keyring --name osd.${cfg.osd2.name} --add-key ${cfg.osd2.key}",
+        'echo \'{"cephx_secret": "${cfg.osd2.key}"}\' | ceph osd new ${cfg.osd2.uuid} -i -',
+    )
+
+    # Initialize the OSDs with regular filestore
+    osd0.succeed(
+        "ceph-osd -i ${cfg.osd0.name} --mkfs --osd-uuid ${cfg.osd0.uuid}",
+        "chown -R ceph:ceph /var/lib/ceph/osd",
+        "systemctl start ceph-osd-${cfg.osd0.name}",
+    )
+    osd1.succeed(
+        "ceph-osd -i ${cfg.osd1.name} --mkfs --osd-uuid ${cfg.osd1.uuid}",
+        "chown -R ceph:ceph /var/lib/ceph/osd",
+        "systemctl start ceph-osd-${cfg.osd1.name}",
+    )
+    osd2.succeed(
+        "ceph-osd -i ${cfg.osd2.name} --mkfs --osd-uuid ${cfg.osd2.uuid}",
+        "chown -R ceph:ceph /var/lib/ceph/osd",
+        "systemctl start ceph-osd-${cfg.osd2.name}",
+    )
+    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'")
+
+    monA.succeed(
+        "ceph osd pool create multi-node-test 32 32",
+        "ceph osd pool ls | grep 'multi-node-test'",
+        "ceph osd pool rename multi-node-test multi-node-other-test",
+        "ceph osd pool ls | grep 'multi-node-other-test'",
+    )
+    monA.wait_until_succeeds("ceph -s | grep '2 pools, 33 pgs'")
+    monA.succeed("ceph osd pool set multi-node-other-test size 2")
+    monA.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
+    monA.wait_until_succeeds("ceph -s | grep '33 active+clean'")
+    monA.fail(
+        "ceph osd pool ls | grep 'multi-node-test'",
+        "ceph osd pool delete multi-node-other-test multi-node-other-test --yes-i-really-really-mean-it",
+    )
+
+    # Shut down ceph on all machines in a very unpolite way
+    monA.crash()
+    osd0.crash()
+    osd1.crash()
+    osd2.crash()
+
+    # Start it up
+    osd0.start()
+    osd1.start()
+    osd2.start()
+    monA.start()
+
+    # Ensure the cluster comes back up again
+    monA.succeed("ceph -s | grep 'mon: 1 daemons'")
+    monA.wait_until_succeeds("ceph -s | grep 'quorum ${cfg.monA.name}'")
+    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'")
+  '';
+in {
+  name = "basic-multi-node-ceph-cluster";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lejonet ];
+  };
+
+  nodes = {
+    monA = generateHost { pkgs = pkgs; cephConfig = cephConfigMonA; networkConfig = networkMonA; };
+    osd0 = generateHost { pkgs = pkgs; cephConfig = cephConfigOsd cfg.osd0; networkConfig = networkOsd cfg.osd0; };
+    osd1 = generateHost { pkgs = pkgs; cephConfig = cephConfigOsd cfg.osd1; networkConfig = networkOsd cfg.osd1; };
+    osd2 = generateHost { pkgs = pkgs; cephConfig = cephConfigOsd cfg.osd2; networkConfig = networkOsd cfg.osd2; };
+  };
+
+  testScript = testscript;
+})
diff --git a/nixos/tests/ceph-single-node-bluestore.nix b/nixos/tests/ceph-single-node-bluestore.nix
new file mode 100644
index 00000000000..acaae4cf300
--- /dev/null
+++ b/nixos/tests/ceph-single-node-bluestore.nix
@@ -0,0 +1,196 @@
+import ./make-test-python.nix ({pkgs, lib, ...}:
+
+let
+  cfg = {
+    clusterId = "066ae264-2a5d-4729-8001-6ad265f50b03";
+    monA = {
+      name = "a";
+      ip = "192.168.1.1";
+    };
+    osd0 = {
+      name = "0";
+      key = "AQBCEJNa3s8nHRAANvdsr93KqzBznuIWm2gOGg==";
+      uuid = "55ba2294-3e24-478f-bee0-9dca4c231dd9";
+    };
+    osd1 = {
+      name = "1";
+      key = "AQBEEJNac00kExAAXEgy943BGyOpVH1LLlHafQ==";
+      uuid = "5e97a838-85b6-43b0-8950-cb56d554d1e5";
+    };
+    osd2 = {
+      name = "2";
+      key = "AQAdyhZeIaUlARAAGRoidDAmS6Vkp546UFEf5w==";
+      uuid = "ea999274-13d0-4dd5-9af9-ad25a324f72f";
+    };
+  };
+  generateCephConfig = { daemonConfig }: {
+    enable = true;
+    global = {
+      fsid = cfg.clusterId;
+      monHost = cfg.monA.ip;
+      monInitialMembers = cfg.monA.name;
+    };
+  } // daemonConfig;
+
+  generateHost = { pkgs, cephConfig, networkConfig, ... }: {
+    virtualisation = {
+      emptyDiskImages = [ 20480 20480 20480 ];
+      vlans = [ 1 ];
+    };
+
+    networking = networkConfig;
+
+    environment.systemPackages = with pkgs; [
+      bash
+      sudo
+      ceph
+      xfsprogs
+    ];
+
+    boot.kernelModules = [ "xfs" ];
+
+    services.ceph = cephConfig;
+  };
+
+  networkMonA = {
+    dhcpcd.enable = false;
+    interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+      { address = cfg.monA.ip; prefixLength = 24; }
+    ];
+  };
+  cephConfigMonA = generateCephConfig { daemonConfig = {
+    mon = {
+      enable = true;
+      daemons = [ cfg.monA.name ];
+    };
+    mgr = {
+      enable = true;
+      daemons = [ cfg.monA.name ];
+    };
+    osd = {
+      enable = true;
+      daemons = [ cfg.osd0.name cfg.osd1.name cfg.osd2.name ];
+    };
+  }; };
+
+  # Following deployment is based on the manual deployment described here:
+  # https://docs.ceph.com/docs/master/install/manual-deployment/
+  # For other ways to deploy a ceph cluster, look at the documentation at
+  # https://docs.ceph.com/docs/master/
+  testscript = { ... }: ''
+    start_all()
+
+    monA.wait_for_unit("network.target")
+
+    # Bootstrap ceph-mon daemon
+    monA.succeed(
+        "sudo -u ceph ceph-authtool --create-keyring /tmp/ceph.mon.keyring --gen-key -n mon. --cap mon 'allow *'",
+        "sudo -u ceph ceph-authtool --create-keyring /etc/ceph/ceph.client.admin.keyring --gen-key -n client.admin --cap mon 'allow *' --cap osd 'allow *' --cap mds 'allow *' --cap mgr 'allow *'",
+        "sudo -u ceph ceph-authtool /tmp/ceph.mon.keyring --import-keyring /etc/ceph/ceph.client.admin.keyring",
+        "monmaptool --create --add ${cfg.monA.name} ${cfg.monA.ip} --fsid ${cfg.clusterId} /tmp/monmap",
+        "sudo -u ceph ceph-mon --mkfs -i ${cfg.monA.name} --monmap /tmp/monmap --keyring /tmp/ceph.mon.keyring",
+        "sudo -u ceph touch /var/lib/ceph/mon/ceph-${cfg.monA.name}/done",
+        "systemctl start ceph-mon-${cfg.monA.name}",
+    )
+    monA.wait_for_unit("ceph-mon-${cfg.monA.name}")
+    monA.succeed("ceph mon enable-msgr2")
+    monA.succeed("ceph config set mon auth_allow_insecure_global_id_reclaim false")
+
+    # Can't check ceph status until a mon is up
+    monA.succeed("ceph -s | grep 'mon: 1 daemons'")
+
+    # Start the ceph-mgr daemon, after copying in the keyring
+    monA.succeed(
+        "sudo -u ceph mkdir -p /var/lib/ceph/mgr/ceph-${cfg.monA.name}/",
+        "ceph auth get-or-create mgr.${cfg.monA.name} mon 'allow profile mgr' osd 'allow *' mds 'allow *' > /var/lib/ceph/mgr/ceph-${cfg.monA.name}/keyring",
+        "systemctl start ceph-mgr-${cfg.monA.name}",
+    )
+    monA.wait_for_unit("ceph-mgr-a")
+    monA.wait_until_succeeds("ceph -s | grep 'quorum ${cfg.monA.name}'")
+    monA.wait_until_succeeds("ceph -s | grep 'mgr: ${cfg.monA.name}(active,'")
+
+    # Bootstrap OSDs
+    monA.succeed(
+        "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd0.name}",
+        "echo bluestore > /var/lib/ceph/osd/ceph-${cfg.osd0.name}/type",
+        "ln -sf /dev/vdb /var/lib/ceph/osd/ceph-${cfg.osd0.name}/block",
+        "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd1.name}",
+        "echo bluestore > /var/lib/ceph/osd/ceph-${cfg.osd1.name}/type",
+        "ln -sf /dev/vdc /var/lib/ceph/osd/ceph-${cfg.osd1.name}/block",
+        "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd2.name}",
+        "echo bluestore > /var/lib/ceph/osd/ceph-${cfg.osd2.name}/type",
+        "ln -sf /dev/vdd /var/lib/ceph/osd/ceph-${cfg.osd2.name}/block",
+        "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd0.name}/keyring --name osd.${cfg.osd0.name} --add-key ${cfg.osd0.key}",
+        "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd1.name}/keyring --name osd.${cfg.osd1.name} --add-key ${cfg.osd1.key}",
+        "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd2.name}/keyring --name osd.${cfg.osd2.name} --add-key ${cfg.osd2.key}",
+        'echo \'{"cephx_secret": "${cfg.osd0.key}"}\' | ceph osd new ${cfg.osd0.uuid} -i -',
+        'echo \'{"cephx_secret": "${cfg.osd1.key}"}\' | ceph osd new ${cfg.osd1.uuid} -i -',
+        'echo \'{"cephx_secret": "${cfg.osd2.key}"}\' | ceph osd new ${cfg.osd2.uuid} -i -',
+    )
+
+    # Initialize the OSDs with regular filestore
+    monA.succeed(
+        "ceph-osd -i ${cfg.osd0.name} --mkfs --osd-uuid ${cfg.osd0.uuid}",
+        "ceph-osd -i ${cfg.osd1.name} --mkfs --osd-uuid ${cfg.osd1.uuid}",
+        "ceph-osd -i ${cfg.osd2.name} --mkfs --osd-uuid ${cfg.osd2.uuid}",
+        "chown -R ceph:ceph /var/lib/ceph/osd",
+        "systemctl start ceph-osd-${cfg.osd0.name}",
+        "systemctl start ceph-osd-${cfg.osd1.name}",
+        "systemctl start ceph-osd-${cfg.osd2.name}",
+    )
+    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'")
+
+    monA.succeed(
+        "ceph osd pool create single-node-test 32 32",
+        "ceph osd pool ls | grep 'single-node-test'",
+        "ceph osd pool rename single-node-test single-node-other-test",
+        "ceph osd pool ls | grep 'single-node-other-test'",
+    )
+    monA.wait_until_succeeds("ceph -s | grep '2 pools, 33 pgs'")
+    monA.succeed(
+        "ceph osd getcrushmap -o crush",
+        "crushtool -d crush -o decrushed",
+        "sed 's/step chooseleaf firstn 0 type host/step chooseleaf firstn 0 type osd/' decrushed > modcrush",
+        "crushtool -c modcrush -o recrushed",
+        "ceph osd setcrushmap -i recrushed",
+        "ceph osd pool set single-node-other-test size 2",
+    )
+    monA.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
+    monA.wait_until_succeeds("ceph -s | grep '33 active+clean'")
+    monA.fail(
+        "ceph osd pool ls | grep 'multi-node-test'",
+        "ceph osd pool delete single-node-other-test single-node-other-test --yes-i-really-really-mean-it",
+    )
+
+    # Shut down ceph by stopping ceph.target.
+    monA.succeed("systemctl stop ceph.target")
+
+    # Start it up
+    monA.succeed("systemctl start ceph.target")
+    monA.wait_for_unit("ceph-mon-${cfg.monA.name}")
+    monA.wait_for_unit("ceph-mgr-${cfg.monA.name}")
+    monA.wait_for_unit("ceph-osd-${cfg.osd0.name}")
+    monA.wait_for_unit("ceph-osd-${cfg.osd1.name}")
+    monA.wait_for_unit("ceph-osd-${cfg.osd2.name}")
+
+    # Ensure the cluster comes back up again
+    monA.succeed("ceph -s | grep 'mon: 1 daemons'")
+    monA.wait_until_succeeds("ceph -s | grep 'quorum ${cfg.monA.name}'")
+    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'")
+  '';
+in {
+  name = "basic-single-node-ceph-cluster-bluestore";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lukegb ];
+  };
+
+  nodes = {
+    monA = generateHost { pkgs = pkgs; cephConfig = cephConfigMonA; networkConfig = networkMonA; };
+  };
+
+  testScript = testscript;
+})
diff --git a/nixos/tests/ceph-single-node.nix b/nixos/tests/ceph-single-node.nix
new file mode 100644
index 00000000000..4fe5dc59ff8
--- /dev/null
+++ b/nixos/tests/ceph-single-node.nix
@@ -0,0 +1,196 @@
+import ./make-test-python.nix ({pkgs, lib, ...}:
+
+let
+  cfg = {
+    clusterId = "066ae264-2a5d-4729-8001-6ad265f50b03";
+    monA = {
+      name = "a";
+      ip = "192.168.1.1";
+    };
+    osd0 = {
+      name = "0";
+      key = "AQBCEJNa3s8nHRAANvdsr93KqzBznuIWm2gOGg==";
+      uuid = "55ba2294-3e24-478f-bee0-9dca4c231dd9";
+    };
+    osd1 = {
+      name = "1";
+      key = "AQBEEJNac00kExAAXEgy943BGyOpVH1LLlHafQ==";
+      uuid = "5e97a838-85b6-43b0-8950-cb56d554d1e5";
+    };
+    osd2 = {
+      name = "2";
+      key = "AQAdyhZeIaUlARAAGRoidDAmS6Vkp546UFEf5w==";
+      uuid = "ea999274-13d0-4dd5-9af9-ad25a324f72f";
+    };
+  };
+  generateCephConfig = { daemonConfig }: {
+    enable = true;
+    global = {
+      fsid = cfg.clusterId;
+      monHost = cfg.monA.ip;
+      monInitialMembers = cfg.monA.name;
+    };
+  } // daemonConfig;
+
+  generateHost = { pkgs, cephConfig, networkConfig, ... }: {
+    virtualisation = {
+      emptyDiskImages = [ 20480 20480 20480 ];
+      vlans = [ 1 ];
+    };
+
+    networking = networkConfig;
+
+    environment.systemPackages = with pkgs; [
+      bash
+      sudo
+      ceph
+      xfsprogs
+    ];
+
+    boot.kernelModules = [ "xfs" ];
+
+    services.ceph = cephConfig;
+  };
+
+  networkMonA = {
+    dhcpcd.enable = false;
+    interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+      { address = cfg.monA.ip; prefixLength = 24; }
+    ];
+  };
+  cephConfigMonA = generateCephConfig { daemonConfig = {
+    mon = {
+      enable = true;
+      daemons = [ cfg.monA.name ];
+    };
+    mgr = {
+      enable = true;
+      daemons = [ cfg.monA.name ];
+    };
+    osd = {
+      enable = true;
+      daemons = [ cfg.osd0.name cfg.osd1.name cfg.osd2.name ];
+    };
+  }; };
+
+  # Following deployment is based on the manual deployment described here:
+  # https://docs.ceph.com/docs/master/install/manual-deployment/
+  # For other ways to deploy a ceph cluster, look at the documentation at
+  # https://docs.ceph.com/docs/master/
+  testscript = { ... }: ''
+    start_all()
+
+    monA.wait_for_unit("network.target")
+
+    # Bootstrap ceph-mon daemon
+    monA.succeed(
+        "sudo -u ceph ceph-authtool --create-keyring /tmp/ceph.mon.keyring --gen-key -n mon. --cap mon 'allow *'",
+        "sudo -u ceph ceph-authtool --create-keyring /etc/ceph/ceph.client.admin.keyring --gen-key -n client.admin --cap mon 'allow *' --cap osd 'allow *' --cap mds 'allow *' --cap mgr 'allow *'",
+        "sudo -u ceph ceph-authtool /tmp/ceph.mon.keyring --import-keyring /etc/ceph/ceph.client.admin.keyring",
+        "monmaptool --create --add ${cfg.monA.name} ${cfg.monA.ip} --fsid ${cfg.clusterId} /tmp/monmap",
+        "sudo -u ceph ceph-mon --mkfs -i ${cfg.monA.name} --monmap /tmp/monmap --keyring /tmp/ceph.mon.keyring",
+        "sudo -u ceph touch /var/lib/ceph/mon/ceph-${cfg.monA.name}/done",
+        "systemctl start ceph-mon-${cfg.monA.name}",
+    )
+    monA.wait_for_unit("ceph-mon-${cfg.monA.name}")
+    monA.succeed("ceph mon enable-msgr2")
+    monA.succeed("ceph config set mon auth_allow_insecure_global_id_reclaim false")
+
+    # Can't check ceph status until a mon is up
+    monA.succeed("ceph -s | grep 'mon: 1 daemons'")
+
+    # Start the ceph-mgr daemon, after copying in the keyring
+    monA.succeed(
+        "sudo -u ceph mkdir -p /var/lib/ceph/mgr/ceph-${cfg.monA.name}/",
+        "ceph auth get-or-create mgr.${cfg.monA.name} mon 'allow profile mgr' osd 'allow *' mds 'allow *' > /var/lib/ceph/mgr/ceph-${cfg.monA.name}/keyring",
+        "systemctl start ceph-mgr-${cfg.monA.name}",
+    )
+    monA.wait_for_unit("ceph-mgr-a")
+    monA.wait_until_succeeds("ceph -s | grep 'quorum ${cfg.monA.name}'")
+    monA.wait_until_succeeds("ceph -s | grep 'mgr: ${cfg.monA.name}(active,'")
+
+    # Bootstrap OSDs
+    monA.succeed(
+        "mkfs.xfs /dev/vdb",
+        "mkfs.xfs /dev/vdc",
+        "mkfs.xfs /dev/vdd",
+        "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd0.name}",
+        "mount /dev/vdb /var/lib/ceph/osd/ceph-${cfg.osd0.name}",
+        "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd1.name}",
+        "mount /dev/vdc /var/lib/ceph/osd/ceph-${cfg.osd1.name}",
+        "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd2.name}",
+        "mount /dev/vdd /var/lib/ceph/osd/ceph-${cfg.osd2.name}",
+        "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd0.name}/keyring --name osd.${cfg.osd0.name} --add-key ${cfg.osd0.key}",
+        "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd1.name}/keyring --name osd.${cfg.osd1.name} --add-key ${cfg.osd1.key}",
+        "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd2.name}/keyring --name osd.${cfg.osd2.name} --add-key ${cfg.osd2.key}",
+        'echo \'{"cephx_secret": "${cfg.osd0.key}"}\' | ceph osd new ${cfg.osd0.uuid} -i -',
+        'echo \'{"cephx_secret": "${cfg.osd1.key}"}\' | ceph osd new ${cfg.osd1.uuid} -i -',
+        'echo \'{"cephx_secret": "${cfg.osd2.key}"}\' | ceph osd new ${cfg.osd2.uuid} -i -',
+    )
+
+    # Initialize the OSDs with regular filestore
+    monA.succeed(
+        "ceph-osd -i ${cfg.osd0.name} --mkfs --osd-uuid ${cfg.osd0.uuid}",
+        "ceph-osd -i ${cfg.osd1.name} --mkfs --osd-uuid ${cfg.osd1.uuid}",
+        "ceph-osd -i ${cfg.osd2.name} --mkfs --osd-uuid ${cfg.osd2.uuid}",
+        "chown -R ceph:ceph /var/lib/ceph/osd",
+        "systemctl start ceph-osd-${cfg.osd0.name}",
+        "systemctl start ceph-osd-${cfg.osd1.name}",
+        "systemctl start ceph-osd-${cfg.osd2.name}",
+    )
+    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'")
+
+    monA.succeed(
+        "ceph osd pool create single-node-test 32 32",
+        "ceph osd pool ls | grep 'single-node-test'",
+        "ceph osd pool rename single-node-test single-node-other-test",
+        "ceph osd pool ls | grep 'single-node-other-test'",
+    )
+    monA.wait_until_succeeds("ceph -s | grep '2 pools, 33 pgs'")
+    monA.succeed(
+        "ceph osd getcrushmap -o crush",
+        "crushtool -d crush -o decrushed",
+        "sed 's/step chooseleaf firstn 0 type host/step chooseleaf firstn 0 type osd/' decrushed > modcrush",
+        "crushtool -c modcrush -o recrushed",
+        "ceph osd setcrushmap -i recrushed",
+        "ceph osd pool set single-node-other-test size 2",
+    )
+    monA.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
+    monA.wait_until_succeeds("ceph -s | grep '33 active+clean'")
+    monA.fail(
+        "ceph osd pool ls | grep 'multi-node-test'",
+        "ceph osd pool delete single-node-other-test single-node-other-test --yes-i-really-really-mean-it",
+    )
+
+    # Shut down ceph by stopping ceph.target.
+    monA.succeed("systemctl stop ceph.target")
+
+    # Start it up
+    monA.succeed("systemctl start ceph.target")
+    monA.wait_for_unit("ceph-mon-${cfg.monA.name}")
+    monA.wait_for_unit("ceph-mgr-${cfg.monA.name}")
+    monA.wait_for_unit("ceph-osd-${cfg.osd0.name}")
+    monA.wait_for_unit("ceph-osd-${cfg.osd1.name}")
+    monA.wait_for_unit("ceph-osd-${cfg.osd2.name}")
+
+    # Ensure the cluster comes back up again
+    monA.succeed("ceph -s | grep 'mon: 1 daemons'")
+    monA.wait_until_succeeds("ceph -s | grep 'quorum ${cfg.monA.name}'")
+    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'")
+  '';
+in {
+  name = "basic-single-node-ceph-cluster";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lejonet johanot ];
+  };
+
+  nodes = {
+    monA = generateHost { pkgs = pkgs; cephConfig = cephConfigMonA; networkConfig = networkMonA; };
+  };
+
+  testScript = testscript;
+})
diff --git a/nixos/tests/certmgr.nix b/nixos/tests/certmgr.nix
new file mode 100644
index 00000000000..8f5b8948779
--- /dev/null
+++ b/nixos/tests/certmgr.nix
@@ -0,0 +1,155 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+let
+  mkSpec = { host, service ? null, action }: {
+    inherit action;
+    authority = {
+      file = {
+        group = "nginx";
+        owner = "nginx";
+        path = "/var/ssl/${host}-ca.pem";
+      };
+      label = "www_ca";
+      profile = "three-month";
+      remote = "localhost:8888";
+    };
+    certificate = {
+      group = "nginx";
+      owner = "nginx";
+      path = "/var/ssl/${host}-cert.pem";
+    };
+    private_key = {
+      group = "nginx";
+      mode = "0600";
+      owner = "nginx";
+      path = "/var/ssl/${host}-key.pem";
+    };
+    request = {
+      CN = host;
+      hosts = [ host "www.${host}" ];
+      key = {
+        algo = "rsa";
+        size = 2048;
+      };
+      names = [
+        {
+          C = "US";
+          L = "San Francisco";
+          O = "Example, LLC";
+          ST = "CA";
+        }
+      ];
+    };
+    inherit service;
+  };
+
+  mkCertmgrTest = { svcManager, specs, testScript }: makeTest {
+    name = "certmgr-" + svcManager;
+    nodes = {
+      machine = { config, lib, pkgs, ... }: {
+        networking.firewall.allowedTCPPorts = with config.services; [ cfssl.port certmgr.metricsPort ];
+        networking.extraHosts = "127.0.0.1 imp.example.org decl.example.org";
+
+        services.cfssl.enable = true;
+        systemd.services.cfssl.after = [ "cfssl-init.service" "networking.target" ];
+
+        systemd.tmpfiles.rules = [ "d /var/ssl 777 root root" ];
+
+        systemd.services.cfssl-init = {
+          description = "Initialize the cfssl CA";
+          wantedBy    = [ "multi-user.target" ];
+          serviceConfig = {
+            User             = "cfssl";
+            Type             = "oneshot";
+            WorkingDirectory = config.services.cfssl.dataDir;
+          };
+          script = ''
+            ${pkgs.cfssl}/bin/cfssl genkey -initca ${pkgs.writeText "ca.json" (builtins.toJSON {
+              hosts = [ "ca.example.com" ];
+              key = {
+                algo = "rsa"; size = 4096; };
+                names = [
+                  {
+                    C = "US";
+                    L = "San Francisco";
+                    O = "Internet Widgets, LLC";
+                    OU = "Certificate Authority";
+                    ST = "California";
+                  }
+                ];
+            })} | ${pkgs.cfssl}/bin/cfssljson -bare ca
+          '';
+        };
+
+        services.nginx = {
+          enable = true;
+          virtualHosts = lib.mkMerge (map (host: {
+            ${host} = {
+              sslCertificate = "/var/ssl/${host}-cert.pem";
+              sslCertificateKey = "/var/ssl/${host}-key.pem";
+              extraConfig = ''
+                ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+              '';
+              onlySSL = true;
+              serverName = host;
+              root = pkgs.writeTextDir "index.html" "It works!";
+            };
+          }) [ "imp.example.org" "decl.example.org" ]);
+        };
+
+        systemd.services.nginx.wantedBy = lib.mkForce [];
+
+        systemd.services.certmgr.after = [ "cfssl.service" ];
+        services.certmgr = {
+          enable = true;
+          inherit svcManager;
+          inherit specs;
+        };
+
+      };
+    };
+    inherit testScript;
+  };
+in
+{
+  systemd = mkCertmgrTest {
+    svcManager = "systemd";
+    specs = {
+      decl = mkSpec { host = "decl.example.org"; service = "nginx"; action ="restart"; };
+      imp = toString (pkgs.writeText "test.json" (builtins.toJSON (
+        mkSpec { host = "imp.example.org"; service = "nginx"; action = "restart"; }
+      )));
+    };
+    testScript = ''
+      machine.wait_for_unit("cfssl.service")
+      machine.wait_until_succeeds("ls /var/ssl/decl.example.org-ca.pem")
+      machine.wait_until_succeeds("ls /var/ssl/decl.example.org-key.pem")
+      machine.wait_until_succeeds("ls /var/ssl/decl.example.org-cert.pem")
+      machine.wait_until_succeeds("ls /var/ssl/imp.example.org-ca.pem")
+      machine.wait_until_succeeds("ls /var/ssl/imp.example.org-key.pem")
+      machine.wait_until_succeeds("ls /var/ssl/imp.example.org-cert.pem")
+      machine.wait_for_unit("nginx.service")
+      assert 1 < int(machine.succeed('journalctl -u nginx | grep "Starting Nginx" | wc -l'))
+      machine.succeed("curl --cacert /var/ssl/imp.example.org-ca.pem https://imp.example.org")
+      machine.succeed(
+          "curl --cacert /var/ssl/decl.example.org-ca.pem https://decl.example.org"
+      )
+    '';
+  };
+
+  command = mkCertmgrTest {
+    svcManager = "command";
+    specs = {
+      test = mkSpec { host = "command.example.org"; action = "touch /tmp/command.executed"; };
+    };
+    testScript = ''
+      machine.wait_for_unit("cfssl.service")
+      machine.wait_until_succeeds("stat /tmp/command.executed")
+    '';
+  };
+
+}
diff --git a/nixos/tests/cfssl.nix b/nixos/tests/cfssl.nix
new file mode 100644
index 00000000000..170f09d9b76
--- /dev/null
+++ b/nixos/tests/cfssl.nix
@@ -0,0 +1,67 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "cfssl";
+
+  machine = { config, lib, pkgs, ... }:
+  {
+    networking.firewall.allowedTCPPorts = [ config.services.cfssl.port ];
+
+    services.cfssl.enable = true;
+    systemd.services.cfssl.after = [ "cfssl-init.service" ];
+
+    systemd.services.cfssl-init = {
+      description = "Initialize the cfssl CA";
+      wantedBy    = [ "multi-user.target" ];
+      serviceConfig = {
+        User             = "cfssl";
+        Type             = "oneshot";
+        WorkingDirectory = config.services.cfssl.dataDir;
+      };
+      script = with pkgs; ''
+        ${cfssl}/bin/cfssl genkey -initca ${pkgs.writeText "ca.json" (builtins.toJSON {
+          hosts = [ "ca.example.com" ];
+          key = {
+            algo = "rsa"; size = 4096; };
+            names = [
+              {
+                C = "US";
+                L = "San Francisco";
+                O = "Internet Widgets, LLC";
+                OU = "Certificate Authority";
+                ST = "California";
+              }
+            ];
+        })} | ${cfssl}/bin/cfssljson -bare ca
+      '';
+    };
+  };
+
+  testScript =
+  let
+    cfsslrequest = with pkgs; writeScript "cfsslrequest" ''
+      curl -f -X POST -H "Content-Type: application/json" -d @${csr} \
+        http://localhost:8888/api/v1/cfssl/newkey | ${cfssl}/bin/cfssljson /tmp/certificate
+    '';
+    csr = pkgs.writeText "csr.json" (builtins.toJSON {
+      CN = "www.example.com";
+      hosts = [ "example.com" "www.example.com" ];
+      key = {
+        algo = "rsa";
+        size = 2048;
+      };
+      names = [
+        {
+          C = "US";
+          L = "San Francisco";
+          O = "Example Company, LLC";
+          OU = "Operations";
+          ST = "California";
+        }
+      ];
+    });
+  in
+    ''
+      machine.wait_for_unit("cfssl.service")
+      machine.wait_until_succeeds("${cfsslrequest}")
+      machine.succeed("ls /tmp/certificate-key.pem")
+    '';
+})
diff --git a/nixos/tests/charliecloud.nix b/nixos/tests/charliecloud.nix
new file mode 100644
index 00000000000..28c3e2f2dbf
--- /dev/null
+++ b/nixos/tests/charliecloud.nix
@@ -0,0 +1,43 @@
+# This test checks charliecloud image construction and run
+
+import ./make-test-python.nix ({ pkgs, ...} : let
+
+  dockerfile = pkgs.writeText "Dockerfile" ''
+    FROM nix
+    RUN mkdir /home /tmp
+    RUN touch /etc/passwd /etc/group
+    CMD ["true"]
+  '';
+
+in {
+  name = "charliecloud";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ bzizou ];
+  };
+
+  nodes = {
+    host = { ... }: {
+      environment.systemPackages = [ pkgs.charliecloud ];
+      virtualisation.docker.enable = true;
+      users.users.alice = {
+        isNormalUser = true;
+        extraGroups = [ "docker" ];
+      };
+    };
+  };
+
+  testScript = ''
+    host.start()
+    host.wait_for_unit("docker.service")
+    host.succeed(
+        'su - alice -c "docker load --input=${pkgs.dockerTools.examples.nix}"'
+    )
+    host.succeed(
+        "cp ${dockerfile} /home/alice/Dockerfile"
+    )
+    host.succeed('su - alice -c "ch-build -t hello ."')
+    host.succeed('su - alice -c "ch-builder2tar hello /var/tmp"')
+    host.succeed('su - alice -c "ch-tar2dir /var/tmp/hello.tar.gz /var/tmp"')
+    host.succeed('su - alice -c "ch-run /var/tmp/hello -- echo Running_From_Container_OK"')
+  '';
+})
diff --git a/nixos/tests/chromium.nix b/nixos/tests/chromium.nix
new file mode 100644
index 00000000000..8965646bc5d
--- /dev/null
+++ b/nixos/tests/chromium.nix
@@ -0,0 +1,258 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+, channelMap ? { # Maps "channels" to packages
+    stable        = pkgs.chromium;
+    beta          = pkgs.chromiumBeta;
+    dev           = pkgs.chromiumDev;
+    ungoogled     = pkgs.ungoogled-chromium;
+    chrome-stable = pkgs.google-chrome;
+    chrome-beta   = pkgs.google-chrome-beta;
+    chrome-dev    = pkgs.google-chrome-dev;
+  }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+mapAttrs (channel: chromiumPkg: makeTest rec {
+  name = "chromium-${channel}";
+  meta = {
+    maintainers = with maintainers; [ aszlig primeos ];
+    # https://github.com/NixOS/hydra/issues/591#issuecomment-435125621
+    inherit (chromiumPkg.meta) timeout;
+  };
+
+  enableOCR = true;
+
+  user = "alice";
+
+  machine.imports = [ ./common/user-account.nix ./common/x11.nix ];
+  machine.virtualisation.memorySize = 2047;
+  machine.test-support.displayManager.auto.user = user;
+  machine.environment = {
+    systemPackages = [ chromiumPkg ];
+    variables."XAUTHORITY" = "/home/alice/.Xauthority";
+  };
+
+  startupHTML = pkgs.writeText "chromium-startup.html" ''
+    <!DOCTYPE html>
+    <html>
+    <head>
+    <meta charset="UTF-8">
+    <title>Chromium startup notifier</title>
+    </head>
+    <body onload="javascript:document.title='startup done'">
+      <img src="file://${pkgs.fetchurl {
+        url = "https://nixos.org/logo/nixos-hex.svg";
+        sha256 = "07ymq6nw8kc22m7kzxjxldhiq8gzmc7f45kq2bvhbdm0w5s112s4";
+      }}" />
+    </body>
+    </html>
+  '';
+
+  testScript = let
+    xdo = name: text: let
+      xdoScript = pkgs.writeText "${name}.xdo" text;
+    in "${pkgs.xdotool}/bin/xdotool ${xdoScript}";
+  in ''
+    import shlex
+    import re
+    from contextlib import contextmanager
+
+
+    # Run as user alice
+    def ru(cmd):
+        return "su - ${user} -c " + shlex.quote(cmd)
+
+
+    def launch_browser():
+        """Launches the web browser with the correct options."""
+        # Determine the name of the binary:
+        pname = "${getName chromiumPkg.name}"
+        if pname.find("chromium") != -1:
+            binary = "chromium"  # Same name for all channels and ungoogled-chromium
+        elif pname == "google-chrome":
+            binary = "google-chrome-stable"
+        elif pname == "google-chrome-dev":
+            binary = "google-chrome-unstable"
+        else:  # For google-chrome-beta and as fallback:
+            binary = pname
+        # Add optional CLI options:
+        options = []
+        major_version = "${versions.major (getVersion chromiumPkg.name)}"
+        if major_version > "95" and not pname.startswith("google-chrome"):
+            # Workaround to avoid a GPU crash:
+            options.append("--use-gl=swiftshader")
+        # Launch the process:
+        options.append("file://${startupHTML}")
+        machine.succeed(ru(f'ulimit -c unlimited; {binary} {shlex.join(options)} >&2 & disown'))
+        if binary.startswith("google-chrome"):
+            # Need to click away the first window:
+            machine.wait_for_text("Make Google Chrome the default browser")
+            machine.screenshot("google_chrome_default_browser_prompt")
+            machine.send_key("ret")
+
+
+    def create_new_win():
+        """Creates a new Chromium window."""
+        with machine.nested("Creating a new Chromium window"):
+            machine.wait_until_succeeds(
+                ru(
+                    "${xdo "create_new_win-select_main_window" ''
+                      search --onlyvisible --name "startup done"
+                      windowfocus --sync
+                      windowactivate --sync
+                    ''}"
+                )
+            )
+            machine.send_key("ctrl-n")
+            # Wait until the new window appears:
+            machine.wait_until_succeeds(
+                ru(
+                    "${xdo "create_new_win-wait_for_window" ''
+                      search --onlyvisible --name "New Tab"
+                      windowfocus --sync
+                      windowactivate --sync
+                    ''}"
+                )
+            )
+
+
+    def close_new_tab_win():
+        """Closes the Chromium window with the title "New Tab"."""
+        machine.wait_until_succeeds(
+            ru(
+                "${xdo "close_new_tab_win-select_main_window" ''
+                  search --onlyvisible --name "New Tab"
+                  windowfocus --sync
+                  windowactivate --sync
+                ''}"
+            )
+        )
+        machine.send_key("ctrl-w")
+        # Wait until the closed window disappears:
+        machine.wait_until_fails(
+            ru(
+                "${xdo "close_new_tab_win-wait_for_close" ''
+                  search --onlyvisible --name "New Tab"
+                ''}"
+            )
+        )
+
+
+    @contextmanager
+    def test_new_win(description, url, window_name):
+        create_new_win()
+        machine.wait_for_window("New Tab")
+        machine.send_chars(f"{url}\n")
+        machine.wait_for_window(window_name)
+        machine.screenshot(description)
+        machine.succeed(
+            ru(
+                "${xdo "copy-all" ''
+                  key --delay 1000 Ctrl+a Ctrl+c
+                ''}"
+            )
+        )
+        clipboard = machine.succeed(
+            ru("${pkgs.xclip}/bin/xclip -o")
+        )
+        print(f"{description} window content:\n{clipboard}")
+        with machine.nested(description):
+            yield clipboard
+        # Close the newly created window:
+        machine.send_key("ctrl-w")
+
+
+    machine.wait_for_x()
+
+    launch_browser()
+
+    machine.wait_for_text("startup done")
+    machine.wait_until_succeeds(
+        ru(
+            "${xdo "check-startup" ''
+              search --sync --onlyvisible --name "startup done"
+              # close first start help popup
+              key -delay 1000 Escape
+              windowfocus --sync
+              windowactivate --sync
+            ''}"
+        )
+    )
+
+    create_new_win()
+    # Optional: Wait for the new tab page to fully load before taking the screenshot:
+    machine.wait_for_text("Web Store")
+    machine.screenshot("empty_windows")
+    close_new_tab_win()
+
+    machine.screenshot("startup_done")
+
+    with test_new_win("sandbox_info", "chrome://sandbox", "Sandbox Status") as clipboard:
+        filters = [
+            "layer 1 sandbox.*namespace",
+            "pid namespaces.*yes",
+            "network namespaces.*yes",
+            "seccomp.*sandbox.*yes",
+            "you are adequately sandboxed",
+        ]
+        if not all(
+            re.search(filter, clipboard, flags=re.DOTALL | re.IGNORECASE)
+            for filter in filters
+        ):
+            assert False, f"sandbox not working properly: {clipboard}"
+
+        machine.sleep(1)
+        machine.succeed(
+            ru(
+                "${xdo "find-window-after-copy" ''
+                  search --onlyvisible --name "Sandbox Status"
+                ''}"
+            )
+        )
+
+        clipboard = machine.succeed(
+            ru(
+                "echo void | ${pkgs.xclip}/bin/xclip -i >&2"
+            )
+        )
+        machine.succeed(
+            ru(
+                "${xdo "copy-sandbox-info" ''
+                  key --delay 1000 Ctrl+a Ctrl+c
+                ''}"
+            )
+        )
+
+        clipboard = machine.succeed(
+            ru("${pkgs.xclip}/bin/xclip -o")
+        )
+        if not all(
+            re.search(filter, clipboard, flags=re.DOTALL | re.IGNORECASE)
+            for filter in filters
+        ):
+            assert False, f"copying twice in a row does not work properly: {clipboard}"
+
+        machine.screenshot("after_copy_from_chromium")
+
+
+    with test_new_win("gpu_info", "chrome://gpu", "chrome://gpu"):
+        # To check the text rendering (catches regressions like #131074):
+        machine.wait_for_text("Graphics Feature Status")
+
+
+    with test_new_win("version_info", "chrome://version", "About Version") as clipboard:
+        filters = [
+            r"${chromiumPkg.version} \(Official Build",
+        ]
+        if not all(
+            re.search(filter, clipboard) for filter in filters
+        ):
+            assert False, "Version info not correct."
+
+
+    machine.shutdown()
+  '';
+}) channelMap
diff --git a/nixos/tests/cjdns.nix b/nixos/tests/cjdns.nix
new file mode 100644
index 00000000000..dc5f371c74d
--- /dev/null
+++ b/nixos/tests/cjdns.nix
@@ -0,0 +1,121 @@
+let
+  carolKey = "2d2a338b46f8e4a8c462f0c385b481292a05f678e19a2b82755258cf0f0af7e2";
+  carolPubKey = "n932l3pjvmhtxxcdrqq2qpw5zc58f01vvjx01h4dtd1bb0nnu2h0.k";
+  carolPassword = "678287829ce4c67bc8b227e56d94422ee1b85fa11618157b2f591de6c6322b52";
+
+  basicConfig =
+    { ... }:
+    { services.cjdns.enable = true;
+
+      # Turning off DHCP isn't very realistic but makes
+      # the sequence of address assignment less stochastic.
+      networking.useDHCP = false;
+
+      # CJDNS output is incompatible with the XML log.
+      systemd.services.cjdns.serviceConfig.StandardOutput = "null";
+    };
+
+in
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "cjdns";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ehmry ];
+  };
+
+  nodes = { # Alice finds peers over over ETHInterface.
+      alice =
+        { ... }:
+        { imports = [ basicConfig ];
+
+          services.cjdns.ETHInterface.bind = "eth1";
+
+          services.httpd.enable = true;
+          services.httpd.adminAddr = "foo@example.org";
+          networking.firewall.allowedTCPPorts = [ 80 ];
+        };
+
+      # Bob explicitly connects to Carol over UDPInterface.
+      bob =
+        { ... }:
+
+        { imports = [ basicConfig ];
+
+          networking.interfaces.eth1.ipv4.addresses = [
+            { address = "192.168.0.2"; prefixLength = 24; }
+          ];
+
+          services.cjdns =
+            { UDPInterface =
+                { bind = "0.0.0.0:1024";
+                  connectTo."192.168.0.1:1024" =
+                    { password = carolPassword;
+                      publicKey = carolPubKey;
+                    };
+                };
+            };
+        };
+
+      # Carol listens on ETHInterface and UDPInterface,
+      # but knows neither Alice or Bob.
+      carol =
+        { ... }:
+        { imports = [ basicConfig ];
+
+          environment.etc."cjdns.keys".text = ''
+            CJDNS_PRIVATE_KEY=${carolKey}
+            CJDNS_ADMIN_PASSWORD=FOOBAR
+          '';
+
+          networking.interfaces.eth1.ipv4.addresses = [
+            { address = "192.168.0.1"; prefixLength = 24; }
+          ];
+
+          services.cjdns =
+            { authorizedPasswords = [ carolPassword ];
+              ETHInterface.bind = "eth1";
+              UDPInterface.bind = "192.168.0.1:1024";
+            };
+          networking.firewall.allowedUDPPorts = [ 1024 ];
+        };
+
+    };
+
+  testScript =
+    ''
+      import re
+
+      start_all()
+
+      alice.wait_for_unit("cjdns.service")
+      bob.wait_for_unit("cjdns.service")
+      carol.wait_for_unit("cjdns.service")
+
+
+      def cjdns_ip(machine):
+          res = machine.succeed("ip -o -6 addr show dev tun0")
+          ip = re.split("\s+|/", res)[3]
+          machine.log("has ip {}".format(ip))
+          return ip
+
+
+      alice_ip6 = cjdns_ip(alice)
+      bob_ip6 = cjdns_ip(bob)
+      carol_ip6 = cjdns_ip(carol)
+
+      # ping a few times each to let the routing table establish itself
+
+      alice.succeed("ping -c 4 {}".format(carol_ip6))
+      bob.succeed("ping -c 4 {}".format(carol_ip6))
+
+      carol.succeed("ping -c 4 {}".format(alice_ip6))
+      carol.succeed("ping -c 4 {}".format(bob_ip6))
+
+      alice.succeed("ping -c 4 {}".format(bob_ip6))
+      bob.succeed("ping -c 4 {}".format(alice_ip6))
+
+      alice.wait_for_unit("httpd.service")
+
+      bob.succeed("curl --fail -g http://[{}]".format(alice_ip6))
+    '';
+})
diff --git a/nixos/tests/clickhouse.nix b/nixos/tests/clickhouse.nix
new file mode 100644
index 00000000000..017f2ee35da
--- /dev/null
+++ b/nixos/tests/clickhouse.nix
@@ -0,0 +1,32 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "clickhouse";
+  meta.maintainers = with pkgs.lib.maintainers; [ ma27 ];
+
+  machine = {
+    services.clickhouse.enable = true;
+    virtualisation.memorySize = 4096;
+  };
+
+  testScript =
+    let
+      # work around quote/substitution complexity by Nix, Perl, bash and SQL.
+      tableDDL = pkgs.writeText "ddl.sql" "CREATE TABLE `demo` (`value` FixedString(10)) engine = MergeTree PARTITION BY value ORDER BY tuple();";
+      insertQuery = pkgs.writeText "insert.sql" "INSERT INTO `demo` (`value`) VALUES ('foo');";
+      selectQuery = pkgs.writeText "select.sql" "SELECT * from `demo`";
+    in
+      ''
+        machine.start()
+        machine.wait_for_unit("clickhouse.service")
+        machine.wait_for_open_port(9000)
+
+        machine.succeed(
+            "cat ${tableDDL} | clickhouse-client"
+        )
+        machine.succeed(
+            "cat ${insertQuery} | clickhouse-client"
+        )
+        machine.succeed(
+            "cat ${selectQuery} | clickhouse-client | grep foo"
+        )
+      '';
+})
diff --git a/nixos/tests/cloud-init.nix b/nixos/tests/cloud-init.nix
new file mode 100644
index 00000000000..3f191ff5616
--- /dev/null
+++ b/nixos/tests/cloud-init.nix
@@ -0,0 +1,109 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  inherit (import ./ssh-keys.nix pkgs)
+    snakeOilPrivateKey snakeOilPublicKey;
+
+  metadataDrive = pkgs.stdenv.mkDerivation {
+    name = "metadata";
+    buildCommand = ''
+      mkdir -p $out/iso
+
+      cat << EOF > $out/iso/user-data
+      #cloud-config
+      write_files:
+      -   content: |
+                cloudinit
+          path: /tmp/cloudinit-write-file
+
+      users:
+        - default
+        - name: nixos
+          ssh_authorized_keys:
+            - "${snakeOilPublicKey}"
+      EOF
+
+      cat << EOF > $out/iso/meta-data
+      instance-id: iid-local01
+      local-hostname: "test"
+      public-keys:
+        - "${snakeOilPublicKey}"
+      EOF
+
+      cat << EOF > $out/iso/network-config
+      version: 1
+      config:
+          - type: physical
+            name: eth0
+            mac_address: '52:54:00:12:34:56'
+            subnets:
+            - type: static
+              address: '12.34.56.78'
+              netmask: '255.255.255.0'
+              gateway: '12.34.56.9'
+          - type: nameserver
+            address:
+            - '8.8.8.8'
+            search:
+            - 'example.com'
+      EOF
+      ${pkgs.cdrkit}/bin/genisoimage -volid cidata -joliet -rock -o $out/metadata.iso $out/iso
+      '';
+  };
+in makeTest {
+  name = "cloud-init";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lewo ];
+  };
+  machine = { ... }:
+  {
+    virtualisation.qemu.options = [ "-cdrom" "${metadataDrive}/metadata.iso" ];
+    services.cloud-init = {
+      enable = true;
+      network.enable = true;
+    };
+    services.openssh.enable = true;
+    networking.hostName = "";
+    networking.useDHCP = false;
+  };
+  testScript = ''
+    # To wait until cloud-init terminates its run
+    unnamed.wait_for_unit("cloud-final.service")
+
+    unnamed.succeed("cat /tmp/cloudinit-write-file | grep -q 'cloudinit'")
+
+    # install snakeoil ssh key and provision .ssh/config file
+    unnamed.succeed("mkdir -p ~/.ssh")
+    unnamed.succeed(
+        "cat ${snakeOilPrivateKey} > ~/.ssh/id_snakeoil"
+    )
+    unnamed.succeed("chmod 600 ~/.ssh/id_snakeoil")
+
+    unnamed.wait_for_unit("sshd.service")
+
+    # we should be able to log in as the root user, as well as the created nixos user
+    unnamed.succeed(
+        "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/id_snakeoil root@localhost 'true'"
+    )
+    unnamed.succeed(
+        "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/id_snakeoil nixos@localhost 'true'"
+    )
+
+    # test changing hostname via cloud-init worked
+    assert (
+        unnamed.succeed(
+            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/id_snakeoil nixos@localhost 'hostname'"
+        ).strip()
+        == "test"
+    )
+
+    assert "default via 12.34.56.9 dev eth0 proto static" in unnamed.succeed("ip route")
+    assert "12.34.56.0/24 dev eth0 proto kernel scope link src 12.34.56.78" in unnamed.succeed("ip route")
+  '';
+}
diff --git a/nixos/tests/cntr.nix b/nixos/tests/cntr.nix
new file mode 100644
index 00000000000..e4e13545b87
--- /dev/null
+++ b/nixos/tests/cntr.nix
@@ -0,0 +1,75 @@
+# Test for cntr tool
+{ system ? builtins.currentSystem, config ? { }
+, pkgs ? import ../.. { inherit system config; }, lib ? pkgs.lib }:
+
+let
+  inherit (import ../lib/testing-python.nix { inherit system pkgs; }) makeTest;
+
+  mkOCITest = backend:
+    makeTest {
+      name = "cntr-${backend}";
+
+      meta = { maintainers = with lib.maintainers; [ sorki mic92 ]; };
+
+      nodes = {
+        ${backend} = { pkgs, ... }: {
+          environment.systemPackages = [ pkgs.cntr ];
+          virtualisation.oci-containers = {
+            inherit backend;
+            containers.nginx = {
+              image = "nginx-container";
+              imageFile = pkgs.dockerTools.examples.nginx;
+              ports = [ "8181:80" ];
+            };
+          };
+        };
+      };
+
+      testScript = ''
+        start_all()
+        ${backend}.wait_for_unit("${backend}-nginx.service")
+        ${backend}.wait_for_open_port(8181)
+        # For some reason, the cntr command hangs when run without the &.
+        # As such, we have to do some messy things to ensure we check the exitcode and output in a race-condition-safe manner
+        ${backend}.execute(
+            "(cntr attach -t ${backend} nginx sh -- -c 'curl localhost | grep Hello' > /tmp/result; echo $? > /tmp/exitcode; touch /tmp/done) &"
+        )
+
+        ${backend}.wait_for_file("/tmp/done")
+        assert "0" == ${backend}.succeed("cat /tmp/exitcode").strip(), "non-zero exit code"
+        assert "Hello" in ${backend}.succeed("cat /tmp/result"), "no greeting in output"
+      '';
+    };
+
+  mkContainersTest = makeTest {
+    name = "cntr-containers";
+
+    meta = with pkgs.lib.maintainers; { maintainers = [ sorki mic92 ]; };
+
+    machine = { lib, ... }: {
+      environment.systemPackages = [ pkgs.cntr ];
+      containers.test = {
+        autoStart = true;
+        privateNetwork = true;
+        hostAddress = "172.16.0.1";
+        localAddress = "172.16.0.2";
+        config = { };
+      };
+    };
+
+    testScript = ''
+      machine.start()
+      machine.wait_for_unit("container@test.service")
+      # I haven't observed the same hanging behaviour in this version as in the OCI version which necessetates this messy invocation, but it's probably better to be safe than sorry and use it here as well
+      machine.execute(
+          "(cntr attach test sh -- -c 'ping -c5 172.16.0.1'; echo $? > /tmp/exitcode; touch /tmp/done) &"
+      )
+
+      machine.wait_for_file("/tmp/done")
+      assert "0" == machine.succeed("cat /tmp/exitcode").strip(), "non-zero exit code"
+    '';
+  };
+in {
+  nixos-container = mkContainersTest;
+} // (lib.foldl' (attrs: backend: attrs // { ${backend} = mkOCITest backend; })
+  { } [ "docker" "podman" ])
diff --git a/nixos/tests/cockroachdb.nix b/nixos/tests/cockroachdb.nix
new file mode 100644
index 00000000000..d793842f0ab
--- /dev/null
+++ b/nixos/tests/cockroachdb.nix
@@ -0,0 +1,124 @@
+# This performs a full 'end-to-end' test of a multi-node CockroachDB cluster
+# using the built-in 'cockroach workload' command, to simulate a semi-realistic
+# test load. It generally takes anywhere from 3-5 minutes to run and 1-2GB of
+# RAM (though each of 3 workers gets 2GB allocated)
+#
+# CockroachDB requires synchronized system clocks within a small error window
+# (~500ms by default) on each node in order to maintain a multi-node cluster.
+# Cluster joins that are outside this window will fail, and nodes that skew
+# outside the window after joining will promptly get kicked out.
+#
+# To accomodate this, we use QEMU/virtio infrastructure and load the 'ptp_kvm'
+# driver inside a guest. This driver allows the host machine to pass its clock
+# through to the guest as a hardware clock that appears as a Precision Time
+# Protocol (PTP) Clock device, generally /dev/ptp0. PTP devices can be measured
+# and used as hardware reference clocks (similar to an on-board GPS clock) by
+# NTP software. In our case, we use Chrony to synchronize to the reference
+# clock.
+#
+# This test is currently NOT enabled as a continuously-checked NixOS test.
+# Ideally, this test would be run by Hydra and Borg on all relevant changes,
+# except:
+#
+#   - Not every build machine is compatible with the ptp_kvm driver.
+#     Virtualized EC2 instances, for example, do not support loading the ptp_kvm
+#     driver into guests. However, bare metal builders (e.g. Packet) do seem to
+#     work just fine. In practice, this means x86_64-linux builds would fail
+#     randomly, depending on which build machine got the job. (This is probably
+#     worth some investigation; I imagine it's based on ptp_kvm's usage of paravirt
+#     support which may not be available in 'nested' environments.)
+#
+#   - ptp_kvm is not supported on aarch64, otherwise it seems likely Cockroach
+#     could be tested there, as well. This seems to be due to the usage of
+#     the TSC in ptp_kvm, which isn't supported (easily) on AArch64. (And:
+#     testing stuff, not just making sure it builds, is important to ensure
+#     aarch64 support remains viable.)
+#
+# For future developers who are reading this message, are daring and would want
+# to fix this, some options are:
+#
+#   - Just test a single node cluster instead (boring and less thorough).
+#   - Move all CI to bare metal packet builders, and we can at least do x86_64-linux.
+#   - Get virtualized clocking working in aarch64, somehow.
+#   - Add a 4th node that acts as an NTP service and uses no PTP clocks for
+#     references, at the client level. This bloats the node and memory
+#     requirements, but would probably allow both aarch64/x86_64 to work.
+#
+
+let
+
+  # Creates a node. If 'joinNode' parameter, a string containing an IP address,
+  # is non-null, then the CockroachDB server will attempt to join/connect to
+  # the cluster node specified at that address.
+  makeNode = locality: myAddr: joinNode:
+    { nodes, pkgs, lib, config, ... }:
+
+    {
+      # Bank/TPC-C benchmarks take some memory to complete
+      virtualisation.memorySize = 2048;
+
+      # Install the KVM PTP "Virtualized Clock" driver. This allows a /dev/ptp0
+      # device to appear as a reference clock, synchronized to the host clock.
+      # Because CockroachDB *requires* a time-synchronization mechanism for
+      # the system time in a cluster scenario, this is necessary to work.
+      boot.kernelModules = [ "ptp_kvm" ];
+
+      # Enable and configure Chrony, using the given virtualized clock passed
+      # through by KVM.
+      services.chrony.enable = true;
+      services.chrony.servers = lib.mkForce [ ];
+      services.chrony.extraConfig = ''
+        refclock PHC /dev/ptp0 poll 2 prefer require refid KVM
+        makestep 0.1 3
+      '';
+
+      # Enable CockroachDB. In order to ensure that Chrony has performed its
+      # first synchronization at boot-time (which may take ~10 seconds) before
+      # starting CockroachDB, we block the ExecStartPre directive using the
+      # 'waitsync' command. This ensures Cockroach doesn't have its system time
+      # leap forward out of nowhere during startup/execution.
+      #
+      # Note that the default threshold for NTP-based skew in CockroachDB is
+      # ~500ms by default, so making sure it's started *after* accurate time
+      # synchronization is extremely important.
+      services.cockroachdb.enable = true;
+      services.cockroachdb.insecure = true;
+      services.cockroachdb.openPorts = true;
+      services.cockroachdb.locality = locality;
+      services.cockroachdb.listen.address = myAddr;
+      services.cockroachdb.join = lib.mkIf (joinNode != null) joinNode;
+
+      systemd.services.chronyd.unitConfig.ConditionPathExists = "/dev/ptp0";
+
+      # Hold startup until Chrony has performed its first measurement (which
+      # will probably result in a full timeskip, thanks to makestep)
+      systemd.services.cockroachdb.preStart = ''
+        ${pkgs.chrony}/bin/chronyc waitsync
+      '';
+    };
+
+in import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "cockroachdb";
+  meta.maintainers = with pkgs.lib.maintainers;
+    [ thoughtpolice ];
+
+  nodes = {
+    node1 = makeNode "country=us,region=east,dc=1"  "192.168.1.1" null;
+    node2 = makeNode "country=us,region=west,dc=2b" "192.168.1.2" "192.168.1.1";
+    node3 = makeNode "country=eu,region=west,dc=2"  "192.168.1.3" "192.168.1.1";
+  };
+
+  # NOTE: All the nodes must start in order and you must NOT use startAll, because
+  # there's otherwise no way to guarantee that node1 will start before the others try
+  # to join it.
+  testScript = ''
+    for node in node1, node2, node3:
+        node.start()
+        node.wait_for_unit("cockroachdb")
+    node1.succeed(
+        "cockroach sql --host=192.168.1.1 --insecure -e 'SHOW ALL CLUSTER SETTINGS' 2>&1",
+        "cockroach workload init bank 'postgresql://root@192.168.1.1:26257?sslmode=disable'",
+        "cockroach workload run bank --duration=1m 'postgresql://root@192.168.1.1:26257?sslmode=disable'",
+    )
+  '';
+})
diff --git a/nixos/tests/collectd.nix b/nixos/tests/collectd.nix
new file mode 100644
index 00000000000..cb196224a23
--- /dev/null
+++ b/nixos/tests/collectd.nix
@@ -0,0 +1,33 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "collectd";
+  meta = { };
+
+  machine =
+    { pkgs, ... }:
+
+    {
+      services.collectd = {
+        enable = true;
+        plugins = {
+          rrdtool = ''
+            DataDir "/var/lib/collectd/rrd"
+          '';
+          load = "";
+        };
+      };
+      environment.systemPackages = [ pkgs.rrdtool ];
+    };
+
+  testScript = ''
+    machine.wait_for_unit("collectd.service")
+    hostname = machine.succeed("hostname").strip()
+    file = f"/var/lib/collectd/rrd/{hostname}/load/load.rrd"
+    machine.wait_for_file(file);
+    machine.succeed(f"rrdinfo {file} | logger")
+    # check that this file contains a shortterm metric
+    machine.succeed(f"rrdinfo {file} | grep -F 'ds[shortterm].min = '")
+    # check that there are frequent updates
+    machine.succeed(f"cp {file} before")
+    machine.wait_until_fails(f"cmp before {file}")
+  '';
+})
diff --git a/nixos/tests/common/acme/client/default.nix b/nixos/tests/common/acme/client/default.nix
new file mode 100644
index 00000000000..9dbe345e7a0
--- /dev/null
+++ b/nixos/tests/common/acme/client/default.nix
@@ -0,0 +1,16 @@
+{ lib, nodes, pkgs, ... }:
+let
+  caCert = nodes.acme.config.test-support.acme.caCert;
+  caDomain = nodes.acme.config.test-support.acme.caDomain;
+
+in {
+  security.acme = {
+    acceptTerms = true;
+    defaults = {
+      server = "https://${caDomain}/dir";
+      email = "hostmaster@example.test";
+    };
+  };
+
+  security.pki.certificateFiles = [ caCert ];
+}
diff --git a/nixos/tests/common/acme/server/README.md b/nixos/tests/common/acme/server/README.md
new file mode 100644
index 00000000000..9de2b2c7102
--- /dev/null
+++ b/nixos/tests/common/acme/server/README.md
@@ -0,0 +1,21 @@
+# Fake Certificate Authority for ACME testing
+
+This will set up a test node running [pebble](https://github.com/letsencrypt/pebble)
+to serve ACME certificate requests.
+
+## "Snake oil" certs
+
+The snake oil certs are hard coded into the repo for reasons explained [here](https://github.com/NixOS/nixpkgs/pull/91121#discussion_r505410235).
+The root of the issue is that Nix will hash the derivation based on the arguments
+to mkDerivation, not the output. [Minica](https://github.com/jsha/minica) will
+always generate a random certificate even if the arguments are unchanged. As a
+result, it's possible to end up in a situation where the cached and local
+generated certs mismatch and cause issues with testing.
+
+To generate new certificates, run the following commands:
+
+```bash
+nix-build generate-certs.nix
+cp result/* .
+rm result
+```
diff --git a/nixos/tests/common/acme/server/acme.test.cert.pem b/nixos/tests/common/acme/server/acme.test.cert.pem
new file mode 100644
index 00000000000..76b0d916a81
--- /dev/null
+++ b/nixos/tests/common/acme/server/acme.test.cert.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDLDCCAhSgAwIBAgIIRDAN3FHH//IwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
+AxMVbWluaWNhIHJvb3QgY2EgNzg3NDZmMB4XDTIwMTAyMTEzMjgzNloXDTIyMTEy
+MDEzMjgzNlowFDESMBAGA1UEAxMJYWNtZS50ZXN0MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEAo8XjMVUaljcaqQ5MFhfPuQgSwdyXEUbpSHz+5yPkE0h9
+Z4Xu5BJF1Oq7h5ggCtadVsIspiY6Jm6aWDOjlh4myzW5UNBNUG3OPEk50vmmHFeH
+pImHO/d8yb33QoF9VRcTZs4tuJYg7l9bSs4jNG72vYvv2YiGAcmjJcsmAZIfniCN
+Xf/LjIm+Cxykn+Vo3UuzO1w5/iuofdgWO/aZxMezmXUivlL3ih4cNzCJei8WlB/l
+EnHrkcy3ogRmmynP5zcz7vmGIJX2ji6dhCa4Got5B7eZK76o2QglhQXqPatG0AOY
+H+RfQfzKemqPG5om9MgJtwFtTOU1LoaiBw//jXKESQIDAQABo3YwdDAOBgNVHQ8B
+Af8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB
+/wQCMAAwHwYDVR0jBBgwFoAU+8IZlLV/Qp5CXqpXMLvtxWlxcJwwFAYDVR0RBA0w
+C4IJYWNtZS50ZXN0MA0GCSqGSIb3DQEBCwUAA4IBAQB0pe8I5/VDkB5VMgQB2GJV
+GKzyigfWbVez9uLmqMj9PPP/zzYKSYeq+91aMuOZrnH7NqBxSTwanULkmqAmhbJJ
+YkXw+FlFekf9FyxcuArzwzzNZDSGcjcdXpN8S2K1qkBd00iSJF9kU7pdZYCIKR20
+QirdBrELEfsJ3GU62a6N3a2YsrisZUvq5TbjGJDcytAtt+WG3gmV7RInLdFfPwbw
+bEHPCnx0uiV0nxLjd/aVT+RceVrFQVt4hR99jLoMlBitSKluZ1ljsrpIyroBhQT0
+pp/pVi6HJdijG0fsPrC325NEGAwcpotLUhczoeM/rffKJd54wLhDkfYxOyRZXivs
+-----END CERTIFICATE-----
diff --git a/nixos/tests/common/acme/server/acme.test.key.pem b/nixos/tests/common/acme/server/acme.test.key.pem
new file mode 100644
index 00000000000..741df99a372
--- /dev/null
+++ b/nixos/tests/common/acme/server/acme.test.key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAo8XjMVUaljcaqQ5MFhfPuQgSwdyXEUbpSHz+5yPkE0h9Z4Xu
+5BJF1Oq7h5ggCtadVsIspiY6Jm6aWDOjlh4myzW5UNBNUG3OPEk50vmmHFeHpImH
+O/d8yb33QoF9VRcTZs4tuJYg7l9bSs4jNG72vYvv2YiGAcmjJcsmAZIfniCNXf/L
+jIm+Cxykn+Vo3UuzO1w5/iuofdgWO/aZxMezmXUivlL3ih4cNzCJei8WlB/lEnHr
+kcy3ogRmmynP5zcz7vmGIJX2ji6dhCa4Got5B7eZK76o2QglhQXqPatG0AOYH+Rf
+QfzKemqPG5om9MgJtwFtTOU1LoaiBw//jXKESQIDAQABAoIBADox/2FwVFo8ioS4
+R+Ex5OZjMAcjU6sX/516jTmlT05q2+UFerYgqB/YqXqtW/V9/brulN8VhmRRuRbO
+grq9TBu5o3hMDK0f18EkZB/MBnLbx594H033y6gEkPBZAyhRYtuNOEH3VwxdZhtW
+1Lu1EoiYSUqLcNMBy6+KWJ8GRaXyacMYBlj2lMHmyzkA/t1+2mwTGC3lT6zN0F5Y
+E5umXOxsn6Tb6q3KM9O5IvtmMMKpgj4HIHZLZ6j40nNgHwGRaAv4Sha/vx0DeBw3
+6VlNiTTPdShEkhESlM5/ocqTfI92VHJpM5gkqTYOWBi2aKIPfAopXoqoJdWl4pQ/
+NCFIu2ECgYEAzntNKIcQtf0ewe0/POo07SIFirvz6jVtYNMTzeQfL6CoEjYArJeu
+Vzc4wEQfA4ZFVerBb1/O6M449gI3zex1PH4AX0h8q8DSjrppK1Jt2TnpVh97k7Gg
+Tnat/M/yW3lWYkcMVJJ3AYurXLFTT1dYP0HvBwZN04yInrEcPNXKfmcCgYEAywyJ
+51d4AE94PrANathKqSI/gk8sP+L1gzylZCcUEAiGk/1r45iYB4HN2gvWbS+CvSdp
+F7ShlDWrTaNh2Bm1dgTjc4pWb4J+CPy/KN2sgLwIuM4+ZWIZmEDcio6khrM/gNqK
+aR7xUsvWsqU26O84woY/xR8IHjSNF7cFWE1H2c8CgYEAt6SSi2kVQ8dMg84uYE8t
+o3qO00U3OycpkOQqyQQLeKC62veMwfRl6swCfX4Y11mkcTXJtPTRYd2Ia8StPUkB
+PDwUuKoPt/JXUvoYb59wc7M+BIsbrdBdc2u6cw+/zfutCNuH6/AYSBeg4WAVaIuW
+wSwzG1xP+8cR+5IqOzEqWCECgYATweeVTCyQEyuHJghYMi2poXx+iIesu7/aAkex
+pB/Oo5W8xrb90XZRnK7UHbzCqRHWqAQQ23Gxgztk9ZXqui2vCzC6qGZauV7cLwPG
+zTMg36sVmHP314DYEM+k59ZYiQ6P0jQPoIQo407D2VGrfsOOIhQIcUmP7tsfyJ5L
+hlGMfwKBgGq4VNnnuX8I5kl03NpaKfG+M8jEHmVwtI9RkPTCCX9bMjeG0cDxqPTF
+TRkf3r8UWQTZ5QfAfAXYAOlZvmGhHjSembRbXMrMdi3rGsYRSrQL6n5NHnORUaMy
+FCWo4gyAnniry7tx9dVNgmHmbjEHuQnf8AC1r3dibRCjvJWUiQ8H
+-----END RSA PRIVATE KEY-----
diff --git a/nixos/tests/common/acme/server/ca.cert.pem b/nixos/tests/common/acme/server/ca.cert.pem
new file mode 100644
index 00000000000..5c33e879b67
--- /dev/null
+++ b/nixos/tests/common/acme/server/ca.cert.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDSzCCAjOgAwIBAgIIeHRvRrNvbGQwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
+AxMVbWluaWNhIHJvb3QgY2EgNzg3NDZmMCAXDTIwMTAyMTEzMjgzNloYDzIxMjAx
+MDIxMTMyODM2WjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSA3ODc0NmYwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrNTzVLDJOKtGYGLU98EEcLKps
+tXHCLC6G54LKbEcU80fn+ArX8qsPSHyhdXQkcYjq6Vh/EDJ1TctyRSnvAjwyG4Aa
+1Zy1QFc/JnjMjvzimCkUc9lQ+wkLwHSM/KGwR1cGjmtQ/EMClZTA0NwulJsXMKVz
+bd5asXbq/yJTQ5Ww25HtdNjwRQXTvB7r3IKcY+DsED9CvFvC9oG/ZhtZqZuyyRdC
+kFUrrv8WNUDkWSN+lMR6xMx8v0583IN6f11IhX0b+svK98G81B2eswBdkzvVyv9M
+unZBO0JuJG8sdM502KhWLmzBC1ZbvgUBF9BumDRpMFH4DCj7+qQ2taWeGyc7AgMB
+AAGjgYYwgYMwDgYDVR0PAQH/BAQDAgKEMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr
+BgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBT7whmUtX9CnkJe
+qlcwu+3FaXFwnDAfBgNVHSMEGDAWgBT7whmUtX9CnkJeqlcwu+3FaXFwnDANBgkq
+hkiG9w0BAQsFAAOCAQEARMe1wKmF33GjEoLLw0oDDS4EdAv26BzCwtrlljsEtwQN
+95oSzUNd6o4Js7WCG2o543OX6cxzM+yju8TES3+vJKDgsbNMU0bWCv//tdrb0/G8
+OkU3Kfi5q4fOauZ1pqGv/pXdfYhZ5ieB/zwis3ykANe5JfB0XqwCb1Vd0C3UCIS2
+NPKngRwNSzphIsbzfvxGDkdM1enuGl5CVyDhrwTMqGaJGDSOv6U5jKFxKRvigqTN
+Ls9lPmT5NXYETduWLBR3yUIdH6kZXrcozZ02B9vjOB2Cv4RMDc+9eM30CLIWpf1I
+097e7JkhzxFhfC/bMMt3P1FeQc+fwH91wdBmNi7tQw==
+-----END CERTIFICATE-----
diff --git a/nixos/tests/common/acme/server/ca.key.pem b/nixos/tests/common/acme/server/ca.key.pem
new file mode 100644
index 00000000000..ed46f5dccf4
--- /dev/null
+++ b/nixos/tests/common/acme/server/ca.key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAqzU81SwyTirRmBi1PfBBHCyqbLVxwiwuhueCymxHFPNH5/gK
+1/KrD0h8oXV0JHGI6ulYfxAydU3LckUp7wI8MhuAGtWctUBXPyZ4zI784pgpFHPZ
+UPsJC8B0jPyhsEdXBo5rUPxDApWUwNDcLpSbFzClc23eWrF26v8iU0OVsNuR7XTY
+8EUF07we69yCnGPg7BA/QrxbwvaBv2YbWambsskXQpBVK67/FjVA5FkjfpTEesTM
+fL9OfNyDen9dSIV9G/rLyvfBvNQdnrMAXZM71cr/TLp2QTtCbiRvLHTOdNioVi5s
+wQtWW74FARfQbpg0aTBR+Awo+/qkNrWlnhsnOwIDAQABAoIBAA3ykVkgd5ysmlSU
+trcsCnHcJaojgff6l3PACoSpG4VWaGY6a8+54julgRm6MtMBONFCX0ZCsImj484U
+Wl0xRmwil2YYPuL5MeJgJPktMObY1IfpBCw3tz3w2M3fiuCMf0d2dMGtO1xLiUnH
++hgFXTkfamsj6ThkOrbcQBSebeRxbKM5hqyCaQoieV+0IJnyxUVq/apib8N50VsH
+SHd4oqLUuEZgg6N70+l5DpzedJUb4nrwS/KhUHUBgnoPItYBCiGPmrwLk7fUhPs6
+kTDqJDtc/xW/JbjmzhWEpVvtumcC/OEKULss7HLdeQqwVBrRQkznb0M9AnSra3d0
+X11/Y4ECgYEA3FC8SquLPFb2lHK4+YbJ4Ac6QVWeYFEHiZ0Rj+CmONmjcAvOGLPE
+SblRLm3Nbrkxbm8FF6/AfXa/rviAKEVPs5xqGfSDw/3n1uInPcmShiBCLwM/jHH5
+NeVG+R5mTg5zyQ/pQMLWRcs+Ail+ZAnZuoGpW3Cdc8OtCUYFQ7XB6nsCgYEAxvBJ
+zFxcTtsDzWbMWXejugQiUqJcEbKWwEfkRbf3J2rAVO2+EFr7LxdRfN2VwPiTQcWc
+LnN2QN+ouOjqBMTh3qm5oQY+TLLHy86k9g1k0gXWkMRQgP2ZdfWH1HyrwjLUgLe1
+VezFN7N1azgy6xFkInAAvuA4loxElZNvkGBgekECgYA/Xw26ILvNIGqO6qzgQXAh
++5I7JsiGheg4IjDiBMlrQtbrLMoceuD0H9UFGNplhel9DXwWgxxIOncKejpK2x0A
+2fX+/0FDh+4+9hA5ipiV8gN3iGSoHkSDxy5yC9d7jlapt+TtFt4Rd1OfxZWwatDw
+/8jaH3t6yAcmyrhK8KYVrwKBgAE5KwsBqmOlvyE9N5Z5QN189wUREIXfVkP6bTHs
+jq2EX4hmKdwJ4y+H8i1VY31bSfSGlY5HkXuWpH/2lrHO0CDBZG3UDwADvWzIaYVF
+0c/kz0v2mRQh+xaZmus4lQnNrDbaalgL666LAPbW0qFVaws3KxoBYPe0BxvwWyhF
+H3LBAoGBAKRRNsq2pWQ8Gqxc0rVoH0FlexU9U2ci3lsLmgEB0A/o/kQkSyAxaRM+
+VdKp3sWfO8o8lX5CVQslCNBSjDTNcat3Co4NEBLg6Xv1yKN/WN1GhusnchP9szsP
+oU47gC89QhUyWSd6vvr2z2NG9C3cACxe4dhDSHQcE4nHSldzCKv2
+-----END RSA PRIVATE KEY-----
diff --git a/nixos/tests/common/acme/server/default.nix b/nixos/tests/common/acme/server/default.nix
new file mode 100644
index 00000000000..450d49e6039
--- /dev/null
+++ b/nixos/tests/common/acme/server/default.nix
@@ -0,0 +1,141 @@
+# The certificate for the ACME service is exported as:
+#
+#   config.test-support.acme.caCert
+#
+# This value can be used inside the configuration of other test nodes to inject
+# the test certificate into security.pki.certificateFiles or into package
+# overlays.
+#
+# Another value that's needed if you don't use a custom resolver (see below for
+# notes on that) is to add the acme node as a nameserver to every node
+# that needs to acquire certificates using ACME, because otherwise the API host
+# for acme.test can't be resolved.
+#
+# A configuration example of a full node setup using this would be this:
+#
+# {
+#   acme = import ./common/acme/server;
+#
+#   example = { nodes, ... }: {
+#     networking.nameservers = [
+#       nodes.acme.config.networking.primaryIPAddress
+#     ];
+#     security.pki.certificateFiles = [
+#       nodes.acme.config.test-support.acme.caCert
+#     ];
+#   };
+# }
+#
+# By default, this module runs a local resolver, generated using resolver.nix
+# from the parent directory to automatically discover all zones in the network.
+#
+# If you do not want this and want to use your own resolver, you can just
+# override networking.nameservers like this:
+#
+# {
+#   acme = { nodes, lib, ... }: {
+#     imports = [ ./common/acme/server ];
+#     networking.nameservers = lib.mkForce [
+#       nodes.myresolver.config.networking.primaryIPAddress
+#     ];
+#   };
+#
+#   myresolver = ...;
+# }
+#
+# Keep in mind, that currently only _one_ resolver is supported, if you have
+# more than one resolver in networking.nameservers only the first one will be
+# used.
+#
+# Also make sure that whenever you use a resolver from a different test node
+# that it has to be started _before_ the ACME service.
+{ config, pkgs, lib, ... }:
+let
+  testCerts = import ./snakeoil-certs.nix;
+  domain = testCerts.domain;
+
+  resolver = let
+    message = "You need to define a resolver for the acme test module.";
+    firstNS = lib.head config.networking.nameservers;
+  in if config.networking.nameservers == [] then throw message else firstNS;
+
+  pebbleConf.pebble = {
+    listenAddress = "0.0.0.0:443";
+    managementListenAddress = "0.0.0.0:15000";
+    # These certs and keys are used for the Web Front End (WFE)
+    certificate = testCerts.${domain}.cert;
+    privateKey = testCerts.${domain}.key;
+    httpPort = 80;
+    tlsPort = 443;
+    ocspResponderURL = "http://${domain}:4002";
+    strict = true;
+  };
+
+  pebbleConfFile = pkgs.writeText "pebble.conf" (builtins.toJSON pebbleConf);
+
+in {
+  imports = [ ../../resolver.nix ];
+
+  options.test-support.acme = with lib; {
+    caDomain = mkOption {
+      type = types.str;
+      readOnly = true;
+      default = domain;
+      description = ''
+        A domain name to use with the <literal>nodes</literal> attribute to
+        identify the CA server.
+      '';
+    };
+    caCert = mkOption {
+      type = types.path;
+      readOnly = true;
+      default = testCerts.ca.cert;
+      description = ''
+        A certificate file to use with the <literal>nodes</literal> attribute to
+        inject the test CA certificate used in the ACME server into
+        <option>security.pki.certificateFiles</option>.
+      '';
+    };
+  };
+
+  config = {
+    test-support = {
+      resolver.enable = let
+        isLocalResolver = config.networking.nameservers == [ "127.0.0.1" ];
+      in lib.mkOverride 900 isLocalResolver;
+    };
+
+    # This has priority 140, because modules/testing/test-instrumentation.nix
+    # already overrides this with priority 150.
+    networking.nameservers = lib.mkOverride 140 [ "127.0.0.1" ];
+    networking.firewall.allowedTCPPorts = [ 80 443 15000 4002 ];
+
+    networking.extraHosts = ''
+      127.0.0.1 ${domain}
+      ${config.networking.primaryIPAddress} ${domain}
+    '';
+
+    systemd.services = {
+      pebble = {
+        enable = true;
+        description = "Pebble ACME server";
+        wantedBy = [ "network.target" ];
+        environment = {
+          # We're not testing lego, we're just testing our configuration.
+          # No need to sleep.
+          PEBBLE_VA_NOSLEEP = "1";
+        };
+
+        serviceConfig = {
+          RuntimeDirectory = "pebble";
+          WorkingDirectory = "/run/pebble";
+
+          # Required to bind on privileged ports.
+          AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+
+          ExecStart = "${pkgs.pebble}/bin/pebble -config ${pebbleConfFile}";
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/tests/common/acme/server/generate-certs.nix b/nixos/tests/common/acme/server/generate-certs.nix
new file mode 100644
index 00000000000..cd8fe0dffca
--- /dev/null
+++ b/nixos/tests/common/acme/server/generate-certs.nix
@@ -0,0 +1,29 @@
+# Minica can provide a CA key and cert, plus a key
+# and cert for our fake CA server's Web Front End (WFE).
+{
+  pkgs ? import <nixpkgs> {},
+  minica ? pkgs.minica,
+  mkDerivation ? pkgs.stdenv.mkDerivation
+}:
+let
+  conf = import ./snakeoil-certs.nix;
+  domain = conf.domain;
+in mkDerivation {
+  name = "test-certs";
+  buildInputs = [ minica ];
+  phases = [ "buildPhase" "installPhase" ];
+
+  buildPhase = ''
+    minica \
+      --ca-key ca.key.pem \
+      --ca-cert ca.cert.pem \
+      --domains ${domain}
+  '';
+
+  installPhase = ''
+    mkdir -p $out
+    mv ca.*.pem $out/
+    mv ${domain}/key.pem $out/${domain}.key.pem
+    mv ${domain}/cert.pem $out/${domain}.cert.pem
+  '';
+}
diff --git a/nixos/tests/common/acme/server/snakeoil-certs.nix b/nixos/tests/common/acme/server/snakeoil-certs.nix
new file mode 100644
index 00000000000..11c3f7fc929
--- /dev/null
+++ b/nixos/tests/common/acme/server/snakeoil-certs.nix
@@ -0,0 +1,13 @@
+let
+  domain = "acme.test";
+in {
+  inherit domain;
+  ca = {
+    cert = ./ca.cert.pem;
+    key = ./ca.key.pem;
+  };
+  "${domain}" = {
+    cert = ./. + "/${domain}.cert.pem";
+    key = ./. + "/${domain}.key.pem";
+  };
+}
diff --git a/nixos/tests/common/auto.nix b/nixos/tests/common/auto.nix
new file mode 100644
index 00000000000..da6b14e9f16
--- /dev/null
+++ b/nixos/tests/common/auto.nix
@@ -0,0 +1,68 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+
+  dmcfg = config.services.xserver.displayManager;
+  cfg = config.test-support.displayManager.auto;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    test-support.displayManager.auto = {
+
+      enable = mkOption {
+        default = false;
+        description = ''
+          Whether to enable the fake "auto" display manager, which
+          automatically logs in the user specified in the
+          <option>user</option> option.  This is mostly useful for
+          automated tests.
+        '';
+      };
+
+      user = mkOption {
+        default = "root";
+        description = "The user account to login automatically.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.xserver.displayManager = {
+      lightdm.enable = true;
+      autoLogin = {
+        enable = true;
+        user = cfg.user;
+      };
+    };
+
+    # lightdm by default doesn't allow auto login for root, which is
+    # required by some nixos tests. Override it here.
+    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
+    '';
+
+  };
+
+}
diff --git a/nixos/tests/common/ec2.nix b/nixos/tests/common/ec2.nix
new file mode 100644
index 00000000000..64b0a91ac1f
--- /dev/null
+++ b/nixos/tests/common/ec2.nix
@@ -0,0 +1,66 @@
+{ pkgs, makeTest }:
+
+with pkgs.lib;
+
+{
+  makeEc2Test = { name, image, userData, script, hostname ? "ec2-instance", sshPublicKey ? null, meta ? {} }:
+    let
+      metaData = pkgs.stdenv.mkDerivation {
+        name = "metadata";
+        buildCommand = ''
+          mkdir -p $out/1.0/meta-data
+          ln -s ${pkgs.writeText "userData" userData} $out/1.0/user-data
+          echo "${hostname}" > $out/1.0/meta-data/hostname
+          echo "(unknown)" > $out/1.0/meta-data/ami-manifest-path
+        '' + optionalString (sshPublicKey != null) ''
+          mkdir -p $out/1.0/meta-data/public-keys/0
+          ln -s ${pkgs.writeText "sshPublicKey" sshPublicKey} $out/1.0/meta-data/public-keys/0/openssh-key
+        '';
+      };
+    in makeTest {
+      name = "ec2-" + name;
+      nodes = {};
+      testScript = ''
+        import os
+        import subprocess
+        import tempfile
+
+        image_dir = os.path.join(
+            os.environ.get("TMPDIR", tempfile.gettempdir()), "tmp", "vm-state-machine"
+        )
+        os.makedirs(image_dir, mode=0o700, exist_ok=True)
+        disk_image = os.path.join(image_dir, "machine.qcow2")
+        subprocess.check_call(
+            [
+                "qemu-img",
+                "create",
+                "-f",
+                "qcow2",
+                "-o",
+                "backing_file=${image}",
+                disk_image,
+            ]
+        )
+        subprocess.check_call(["qemu-img", "resize", disk_image, "10G"])
+
+        # Note: we use net=169.0.0.0/8 rather than
+        # net=169.254.0.0/16 to prevent dhcpcd from getting horribly
+        # confused. (It would get a DHCP lease in the 169.254.*
+        # range, which it would then configure and prompty delete
+        # again when it deletes link-local addresses.) Ideally we'd
+        # turn off the DHCP server, but qemu does not have an option
+        # to do that.
+        start_command = (
+            "qemu-kvm -m 1024"
+            + " -device virtio-net-pci,netdev=vlan0"
+            + " -netdev 'user,id=vlan0,net=169.0.0.0/8,guestfwd=tcp:169.254.169.254:80-cmd:${pkgs.micro-httpd}/bin/micro_httpd ${metaData}'"
+            + f" -drive file={disk_image},if=virtio,werror=report"
+            + " $QEMU_OPTS"
+        )
+
+        machine = create_machine({"startCommand": start_command})
+      '' + script;
+
+      inherit meta;
+    };
+}
diff --git a/nixos/tests/common/resolver.nix b/nixos/tests/common/resolver.nix
new file mode 100644
index 00000000000..09a74de20fa
--- /dev/null
+++ b/nixos/tests/common/resolver.nix
@@ -0,0 +1,141 @@
+# This module automatically discovers zones in BIND and NSD NixOS
+# configurations and creates zones for all definitions of networking.extraHosts
+# (except those that point to 127.0.0.1 or ::1) within the current test network
+# and delegates these zones using a fake root zone served by a BIND recursive
+# name server.
+{ config, nodes, pkgs, lib, ... }:
+
+{
+  options.test-support.resolver.enable = lib.mkOption {
+    type = lib.types.bool;
+    default = true;
+    internal = true;
+    description = ''
+      Whether to enable the resolver that automatically discovers zone in the
+      test network.
+
+      This option is <literal>true</literal> by default, because the module
+      defining this option needs to be explicitly imported.
+
+      The reason this option exists is for the
+      <filename>nixos/tests/common/acme/server</filename> module, which
+      needs that option to disable the resolver once the user has set its own
+      resolver.
+    '';
+  };
+
+  config = lib.mkIf config.test-support.resolver.enable {
+    networking.firewall.enable = false;
+    services.bind.enable = true;
+    services.bind.cacheNetworks = lib.mkForce [ "any" ];
+    services.bind.forwarders = lib.mkForce [];
+    services.bind.zones = lib.singleton {
+      name = ".";
+      file = let
+        addDot = zone: zone + lib.optionalString (!lib.hasSuffix "." zone) ".";
+        mkNsdZoneNames = zones: map addDot (lib.attrNames zones);
+        mkBindZoneNames = zones: map (zone: addDot zone.name) zones;
+        getZones = cfg: mkNsdZoneNames cfg.services.nsd.zones
+                     ++ mkBindZoneNames cfg.services.bind.zones;
+
+        getZonesForNode = attrs: {
+          ip = attrs.config.networking.primaryIPAddress;
+          zones = lib.filter (zone: zone != ".") (getZones attrs.config);
+        };
+
+        zoneInfo = lib.mapAttrsToList (lib.const getZonesForNode) nodes;
+
+        # A and AAAA resource records for all the definitions of
+        # networking.extraHosts except those for 127.0.0.1 or ::1.
+        #
+        # The result is an attribute set with keys being the host name and the
+        # values are either { ipv4 = ADDR; } or { ipv6 = ADDR; } where ADDR is
+        # the IP address for the corresponding key.
+        recordsFromExtraHosts = let
+          getHostsForNode = lib.const (n: n.config.networking.extraHosts);
+          allHostsList = lib.mapAttrsToList getHostsForNode nodes;
+          allHosts = lib.concatStringsSep "\n" allHostsList;
+
+          reIp = "[a-fA-F0-9.:]+";
+          reHost = "[a-zA-Z0-9.-]+";
+
+          matchAliases = str: let
+            matched = builtins.match "[ \t]+(${reHost})(.*)" str;
+            continue = lib.singleton (lib.head matched)
+                    ++ matchAliases (lib.last matched);
+          in if matched == null then [] else continue;
+
+          matchLine = str: let
+            result = builtins.match "[ \t]*(${reIp})[ \t]+(${reHost})(.*)" str;
+          in if result == null then null else {
+            ipAddr = lib.head result;
+            hosts = lib.singleton (lib.elemAt result 1)
+                 ++ matchAliases (lib.last result);
+          };
+
+          skipLine = str: let
+            rest = builtins.match "[^\n]*\n(.*)" str;
+          in if rest == null then "" else lib.head rest;
+
+          getEntries = str: acc: let
+            result = matchLine str;
+            next = getEntries (skipLine str);
+            newEntry = acc ++ lib.singleton result;
+            continue = if result == null then next acc else next newEntry;
+          in if str == "" then acc else continue;
+
+          isIPv6 = str: builtins.match ".*:.*" str != null;
+          loopbackIps = [ "127.0.0.1" "::1" ];
+          filterLoopback = lib.filter (e: !lib.elem e.ipAddr loopbackIps);
+
+          allEntries = lib.concatMap (entry: map (host: {
+            inherit host;
+            ${if isIPv6 entry.ipAddr then "ipv6" else "ipv4"} = entry.ipAddr;
+          }) entry.hosts) (filterLoopback (getEntries (allHosts + "\n") []));
+
+          mkRecords = entry: let
+            records = lib.optional (entry ? ipv6) "AAAA ${entry.ipv6}"
+                   ++ lib.optional (entry ? ipv4) "A ${entry.ipv4}";
+            mkRecord = typeAndData: "${entry.host}. IN ${typeAndData}";
+          in lib.concatMapStringsSep "\n" mkRecord records;
+
+        in lib.concatMapStringsSep "\n" mkRecords allEntries;
+
+        # All of the zones that are subdomains of existing zones.
+        # For example if there is only "example.com" the following zones would
+        # be 'subZones':
+        #
+        #  * foo.example.com.
+        #  * bar.example.com.
+        #
+        # While the following would *not* be 'subZones':
+        #
+        #  * example.com.
+        #  * com.
+        #
+        subZones = let
+          allZones = lib.concatMap (zi: zi.zones) zoneInfo;
+          isSubZoneOf = z1: z2: lib.hasSuffix z2 z1 && z1 != z2;
+        in lib.filter (z: lib.any (isSubZoneOf z) allZones) allZones;
+
+        # All the zones without 'subZones'.
+        filteredZoneInfo = map (zi: zi // {
+          zones = lib.filter (x: !lib.elem x subZones) zi.zones;
+        }) zoneInfo;
+
+      in pkgs.writeText "fake-root.zone" ''
+        $TTL 3600
+        . IN SOA ns.fakedns. admin.fakedns. ( 1 3h 1h 1w 1d )
+        ns.fakedns. IN A ${config.networking.primaryIPAddress}
+        . IN NS ns.fakedns.
+        ${lib.concatImapStrings (num: { ip, zones }: ''
+          ns${toString num}.fakedns. IN A ${ip}
+          ${lib.concatMapStrings (zone: ''
+          ${zone} IN NS ns${toString num}.fakedns.
+          '') zones}
+        '') (lib.filter (zi: zi.zones != []) filteredZoneInfo)}
+        ${recordsFromExtraHosts}
+      '';
+    };
+  };
+}
diff --git a/nixos/tests/common/user-account.nix b/nixos/tests/common/user-account.nix
new file mode 100644
index 00000000000..a57ee2d59ae
--- /dev/null
+++ b/nixos/tests/common/user-account.nix
@@ -0,0 +1,15 @@
+{ ... }:
+
+{ users.users.alice =
+    { isNormalUser = true;
+      description = "Alice Foobar";
+      password = "foobar";
+      uid = 1000;
+    };
+
+  users.users.bob =
+    { isNormalUser = true;
+      description = "Bob Foobar";
+      password = "foobar";
+    };
+}
diff --git a/nixos/tests/common/wayland-cage.nix b/nixos/tests/common/wayland-cage.nix
new file mode 100644
index 00000000000..fd070094139
--- /dev/null
+++ b/nixos/tests/common/wayland-cage.nix
@@ -0,0 +1,13 @@
+{ ... }:
+
+{
+  imports = [ ./user-account.nix ];
+  services.cage = {
+    enable = true;
+    user = "alice";
+  };
+
+  virtualisation = {
+    qemu.options = [ "-vga virtio" ];
+  };
+}
diff --git a/nixos/tests/common/webroot/news-rss.xml b/nixos/tests/common/webroot/news-rss.xml
new file mode 100644
index 00000000000..b8099bf0364
--- /dev/null
+++ b/nixos/tests/common/webroot/news-rss.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:blogChannel="http://backend.userland.com/blogChannelModule" version="2.0">
+ <channel>
+  <title>NixOS News</title><link>https://nixos.org</link>
+  <description>News for NixOS, the purely functional Linux distribution.</description>
+  <image>
+   <title>NixOS</title>
+   <url>https://nixos.org/logo/nixos-logo-only-hires.png</url><link>https://nixos.org/</link>
+  </image>
+  <item>
+   <title>NixOS 18.09 released</title><link>https://nixos.org/news.html</link>
+   <description>
+    <a href="https://github.com/NixOS/nixos-artwork/blob/master/releases/18.09-jellyfish/jellyfish.png">
+     <img class="inline" src="logo/nixos-logo-18.09-jellyfish-lores.png" alt="18.09 Jellyfish logo" with="100" height="87"/>
+    </a>
+      NixOS 18.09 “Jellyfish†has been released, the tenth stable release branch.
+      See the <a href="/nixos/manual/release-notes.html#sec-release-18.09">release notes</a>
+      for details. You can get NixOS 18.09 ISOs and VirtualBox appliances
+      from the <a href="nixos/download.html">download page</a>.
+      For information on how to upgrade from older release branches
+      to 18.09, check out the
+      <a href="/nixos/manual/index.html#sec-upgrading">manual section on upgrading</a>.
+    </description>
+   <pubDate>Sat Oct 06 2018 00:00:00 GMT</pubDate>
+  </item>
+ </channel>
+</rss>
diff --git a/nixos/tests/common/x11.nix b/nixos/tests/common/x11.nix
new file mode 100644
index 00000000000..0d76a0e972f
--- /dev/null
+++ b/nixos/tests/common/x11.nix
@@ -0,0 +1,17 @@
+{ lib, ... }:
+
+{
+  imports = [
+    ./auto.nix
+  ];
+
+  services.xserver.enable = true;
+
+  # Automatically log in.
+  test-support.displayManager.auto.enable = true;
+
+  # Use IceWM as the window manager.
+  # Don't use a desktop manager.
+  services.xserver.displayManager.defaultSession = lib.mkDefault "none+icewm";
+  services.xserver.windowManager.icewm.enable = true;
+}
diff --git a/nixos/tests/consul.nix b/nixos/tests/consul.nix
new file mode 100644
index 00000000000..ee85f1d0b91
--- /dev/null
+++ b/nixos/tests/consul.nix
@@ -0,0 +1,229 @@
+import ./make-test-python.nix ({pkgs, lib, ...}:
+
+let
+  # Settings for both servers and agents
+  webUi = true;
+  retry_interval = "1s";
+  raft_multiplier = 1;
+
+  defaultExtraConfig = {
+    inherit retry_interval;
+    performance = {
+      inherit raft_multiplier;
+    };
+  };
+
+  allConsensusServerHosts = [
+    "192.168.1.1"
+    "192.168.1.2"
+    "192.168.1.3"
+  ];
+
+  allConsensusClientHosts = [
+    "192.168.2.1"
+    "192.168.2.2"
+  ];
+
+  firewallSettings = {
+    # See https://www.consul.io/docs/install/ports.html
+    allowedTCPPorts = [ 8301 8302 8600 8500 8300 ];
+    allowedUDPPorts = [ 8301 8302 8600 ];
+  };
+
+  client = index: { pkgs, ... }:
+    let
+      ip = builtins.elemAt allConsensusClientHosts index;
+    in
+      {
+        environment.systemPackages = [ pkgs.consul ];
+
+        networking.interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+          { address = ip; prefixLength = 16; }
+        ];
+        networking.firewall = firewallSettings;
+
+        services.consul = {
+          enable = true;
+          inherit webUi;
+          extraConfig = defaultExtraConfig // {
+            server = false;
+            retry_join = allConsensusServerHosts;
+            bind_addr = ip;
+          };
+        };
+      };
+
+  server = index: { pkgs, ... }:
+    let
+      numConsensusServers = builtins.length allConsensusServerHosts;
+      thisConsensusServerHost = builtins.elemAt allConsensusServerHosts index;
+      ip = thisConsensusServerHost; # since we already use IPs to identify servers
+    in
+      {
+        networking.interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+          { address = ip; prefixLength = 16; }
+        ];
+        networking.firewall = firewallSettings;
+
+        services.consul =
+          assert builtins.elem thisConsensusServerHost allConsensusServerHosts;
+          {
+            enable = true;
+            inherit webUi;
+            extraConfig = defaultExtraConfig // {
+              server = true;
+              bootstrap_expect = numConsensusServers;
+              # Tell Consul that we never intend to drop below this many servers.
+              # Ensures to not permanently lose consensus after temporary loss.
+              # See https://github.com/hashicorp/consul/issues/8118#issuecomment-645330040
+              autopilot.min_quorum = numConsensusServers;
+              retry_join =
+                # If there's only 1 node in the network, we allow self-join;
+                # otherwise, the node must not try to join itself, and join only the other servers.
+                # See https://github.com/hashicorp/consul/issues/2868
+                if numConsensusServers == 1
+                  then allConsensusServerHosts
+                  else builtins.filter (h: h != thisConsensusServerHost) allConsensusServerHosts;
+              bind_addr = ip;
+            };
+          };
+      };
+in {
+  name = "consul";
+
+  nodes = {
+    server1 = server 0;
+    server2 = server 1;
+    server3 = server 2;
+
+    client1 = client 0;
+    client2 = client 1;
+  };
+
+  testScript = ''
+    servers = [server1, server2, server3]
+    machines = [server1, server2, server3, client1, client2]
+
+    for m in machines:
+        m.wait_for_unit("consul.service")
+
+
+    def wait_for_healthy_servers():
+        # See https://github.com/hashicorp/consul/issues/8118#issuecomment-645330040
+        # for why the `Voter` column of `list-peers` has that info.
+        # TODO: The `grep true` relies on the fact that currently in
+        #       the output like
+        #           # consul operator raft list-peers
+        #           Node     ID   Address           State     Voter  RaftProtocol
+        #           server3  ...  192.168.1.3:8300  leader    true   3
+        #           server2  ...  192.168.1.2:8300  follower  true   3
+        #           server1  ...  192.168.1.1:8300  follower  false  3
+        #       `Voter`is the only boolean column.
+        #       Change this to the more reliable way to be defined by
+        #       https://github.com/hashicorp/consul/issues/8118
+        #       once that ticket is closed.
+        for m in machines:
+            m.wait_until_succeeds(
+                "[ $(consul operator raft list-peers | grep true | wc -l) == 3 ]"
+            )
+
+
+    def wait_for_all_machines_alive():
+        """
+        Note that Serf-"alive" does not mean "Raft"-healthy;
+        see `wait_for_healthy_servers()` for that instead.
+        """
+        for m in machines:
+            m.wait_until_succeeds("[ $(consul members | grep -o alive | wc -l) == 5 ]")
+
+
+    wait_for_healthy_servers()
+    # Also wait for clients to be alive.
+    wait_for_all_machines_alive()
+
+    client1.succeed("consul kv put testkey 42")
+    client2.succeed("[ $(consul kv get testkey) == 42 ]")
+
+
+    def rolling_reboot_test(proper_rolling_procedure=True):
+        """
+        Tests that the cluster can tolearate failures of any single server,
+        following the recommended rolling upgrade procedure from
+        https://www.consul.io/docs/upgrading#standard-upgrades.
+
+        Optionally, `proper_rolling_procedure=False` can be given
+        to wait only for each server to be back `Healthy`, not `Stable`
+        in the Raft consensus, see Consul setting `ServerStabilizationTime` and
+        https://github.com/hashicorp/consul/issues/8118#issuecomment-645330040.
+        """
+
+        for server in servers:
+            server.crash()
+
+            # For each client, wait until they have connection again
+            # using `kv get -recurse` before issuing commands.
+            client1.wait_until_succeeds("consul kv get -recurse")
+            client2.wait_until_succeeds("consul kv get -recurse")
+
+            # Do some consul actions while one server is down.
+            client1.succeed("consul kv put testkey 43")
+            client2.succeed("[ $(consul kv get testkey) == 43 ]")
+            client2.succeed("consul kv delete testkey")
+
+            # Restart crashed machine.
+            server.start()
+
+            if proper_rolling_procedure:
+                # Wait for recovery.
+                wait_for_healthy_servers()
+            else:
+                # NOT proper rolling upgrade procedure, see above.
+                wait_for_all_machines_alive()
+
+            # Wait for client connections.
+            client1.wait_until_succeeds("consul kv get -recurse")
+            client2.wait_until_succeeds("consul kv get -recurse")
+
+            # Do some consul actions with server back up.
+            client1.succeed("consul kv put testkey 44")
+            client2.succeed("[ $(consul kv get testkey) == 44 ]")
+            client2.succeed("consul kv delete testkey")
+
+
+    def all_servers_crash_simultaneously_test():
+        """
+        Tests that the cluster will eventually come back after all
+        servers crash simultaneously.
+        """
+
+        for server in servers:
+            server.crash()
+
+        for server in servers:
+            server.start()
+
+        # Wait for recovery.
+        wait_for_healthy_servers()
+
+        # Wait for client connections.
+        client1.wait_until_succeeds("consul kv get -recurse")
+        client2.wait_until_succeeds("consul kv get -recurse")
+
+        # Do some consul actions with servers back up.
+        client1.succeed("consul kv put testkey 44")
+        client2.succeed("[ $(consul kv get testkey) == 44 ]")
+        client2.succeed("consul kv delete testkey")
+
+
+    # Run the tests.
+
+    print("rolling_reboot_test()")
+    rolling_reboot_test()
+
+    print("all_servers_crash_simultaneously_test()")
+    all_servers_crash_simultaneously_test()
+
+    print("rolling_reboot_test(proper_rolling_procedure=False)")
+    rolling_reboot_test(proper_rolling_procedure=False)
+  '';
+})
diff --git a/nixos/tests/containers-bridge.nix b/nixos/tests/containers-bridge.nix
new file mode 100644
index 00000000000..b8661fd7997
--- /dev/null
+++ b/nixos/tests/containers-bridge.nix
@@ -0,0 +1,99 @@
+let
+  hostIp = "192.168.0.1";
+  containerIp = "192.168.0.100/24";
+  hostIp6 = "fc00::1";
+  containerIp6 = "fc00::2/7";
+in
+
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "containers-bridge";
+  meta = {
+    maintainers = with lib.maintainers; [ aristid aszlig eelco kampfschlaefer ];
+  };
+
+  machine =
+    { pkgs, ... }:
+    { imports = [ ../modules/installer/cd-dvd/channel.nix ];
+      virtualisation.writableStore = true;
+
+      networking.bridges = {
+        br0 = {
+          interfaces = [];
+        };
+      };
+      networking.interfaces = {
+        br0 = {
+          ipv4.addresses = [{ address = hostIp; prefixLength = 24; }];
+          ipv6.addresses = [{ address = hostIp6; prefixLength = 7; }];
+        };
+      };
+
+      containers.webserver =
+        {
+          autoStart = true;
+          privateNetwork = true;
+          hostBridge = "br0";
+          localAddress = containerIp;
+          localAddress6 = containerIp6;
+          config =
+            { services.httpd.enable = true;
+              services.httpd.adminAddr = "foo@example.org";
+              networking.firewall.allowedTCPPorts = [ 80 ];
+            };
+        };
+
+      containers.web-noip =
+        {
+          autoStart = true;
+          privateNetwork = true;
+          hostBridge = "br0";
+          config =
+            { services.httpd.enable = true;
+              services.httpd.adminAddr = "foo@example.org";
+              networking.firewall.allowedTCPPorts = [ 80 ];
+            };
+        };
+
+
+      virtualisation.additionalPaths = [ pkgs.stdenv ];
+    };
+
+  testScript = ''
+    machine.wait_for_unit("default.target")
+    assert "webserver" in machine.succeed("nixos-container list")
+
+    with subtest("Start the webserver container"):
+        assert "up" in machine.succeed("nixos-container status webserver")
+
+    with subtest("Bridges exist inside containers"):
+        machine.succeed(
+            "nixos-container run webserver -- ip link show eth0",
+            "nixos-container run web-noip -- ip link show eth0",
+        )
+
+    ip = "${containerIp}".split("/")[0]
+    machine.succeed(f"ping -n -c 1 {ip}")
+    machine.succeed(f"curl --fail http://{ip}/ > /dev/null")
+
+    ip6 = "${containerIp6}".split("/")[0]
+    machine.succeed(f"ping -n -c 1 {ip6}")
+    machine.succeed(f"curl --fail http://[{ip6}]/ > /dev/null")
+
+    with subtest(
+        "nixos-container show-ip works in case of an ipv4 address "
+        + "with subnetmask in CIDR notation."
+    ):
+        result = machine.succeed("nixos-container show-ip webserver").rstrip()
+        assert result == ip
+
+    with subtest("Stop the container"):
+        machine.succeed("nixos-container stop webserver")
+        machine.fail(
+            f"curl --fail --connect-timeout 2 http://{ip}/ > /dev/null",
+            f"curl --fail --connect-timeout 2 http://[{ip6}]/ > /dev/null",
+        )
+
+    # Destroying a declarative container should fail.
+    machine.fail("nixos-container destroy webserver")
+  '';
+})
diff --git a/nixos/tests/containers-custom-pkgs.nix b/nixos/tests/containers-custom-pkgs.nix
new file mode 100644
index 00000000000..1627a2c70c3
--- /dev/null
+++ b/nixos/tests/containers-custom-pkgs.nix
@@ -0,0 +1,34 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: let
+
+  customPkgs = pkgs.appendOverlays [ (self: super: {
+    hello = super.hello.overrideAttrs (old: {
+       name = "custom-hello";
+    });
+  }) ];
+
+in {
+  name = "containers-custom-pkgs";
+  meta = {
+    maintainers = with lib.maintainers; [ adisbladis earvstedt ];
+  };
+
+  machine = { config, ... }: {
+    assertions = let
+      helloName = (builtins.head config.containers.test.config.system.extraDependencies).name;
+    in [ {
+      assertion = helloName == "custom-hello";
+      message = "Unexpected value: ${helloName}";
+    } ];
+
+    containers.test = {
+      autoStart = true;
+      config = { pkgs, config, ... }: {
+        nixpkgs.pkgs = customPkgs;
+        system.extraDependencies = [ pkgs.hello ];
+      };
+    };
+  };
+
+  # This test only consists of evaluating the test machine
+  testScript = "pass";
+})
diff --git a/nixos/tests/containers-ephemeral.nix b/nixos/tests/containers-ephemeral.nix
new file mode 100644
index 00000000000..db1631cf5b5
--- /dev/null
+++ b/nixos/tests/containers-ephemeral.nix
@@ -0,0 +1,54 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "containers-ephemeral";
+  meta = {
+    maintainers = with lib.maintainers; [ patryk27 ];
+  };
+
+  machine = { pkgs, ... }: {
+    virtualisation.writableStore = true;
+
+    containers.webserver = {
+      ephemeral = true;
+      privateNetwork = true;
+      hostAddress = "10.231.136.1";
+      localAddress = "10.231.136.2";
+      config = {
+        services.nginx = {
+          enable = true;
+          virtualHosts.localhost = {
+            root = pkgs.runCommand "localhost" {} ''
+              mkdir "$out"
+              echo hello world > "$out/index.html"
+            '';
+          };
+        };
+        networking.firewall.allowedTCPPorts = [ 80 ];
+      };
+    };
+  };
+
+  testScript = ''
+    assert "webserver" in machine.succeed("nixos-container list")
+
+    machine.succeed("nixos-container start webserver")
+
+    with subtest("Container got its own root folder"):
+        machine.succeed("ls /run/containers/webserver")
+
+    with subtest("Container persistent directory is not created"):
+        machine.fail("ls /var/lib/containers/webserver")
+
+    # Since "start" returns after the container has reached
+    # multi-user.target, we should now be able to access it.
+    ip = machine.succeed("nixos-container show-ip webserver").rstrip()
+    machine.succeed(f"ping -n -c1 {ip}")
+    machine.succeed(f"curl --fail http://{ip}/ > /dev/null")
+
+    with subtest("Stop the container"):
+        machine.succeed("nixos-container stop webserver")
+        machine.fail(f"curl --fail --connect-timeout 2 http://{ip}/ > /dev/null")
+
+    with subtest("Container's root folder was removed"):
+        machine.fail("ls /run/containers/webserver")
+  '';
+})
diff --git a/nixos/tests/containers-extra_veth.nix b/nixos/tests/containers-extra_veth.nix
new file mode 100644
index 00000000000..b8f3d984406
--- /dev/null
+++ b/nixos/tests/containers-extra_veth.nix
@@ -0,0 +1,91 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "containers-extra_veth";
+  meta = {
+    maintainers = with lib.maintainers; [ kampfschlaefer ];
+  };
+
+  machine =
+    { pkgs, ... }:
+    { imports = [ ../modules/installer/cd-dvd/channel.nix ];
+      virtualisation.writableStore = true;
+      virtualisation.vlans = [];
+
+      networking.useDHCP = false;
+      networking.bridges = {
+        br0 = {
+          interfaces = [];
+        };
+        br1 = { interfaces = []; };
+      };
+      networking.interfaces = {
+        br0 = {
+          ipv4.addresses = [{ address = "192.168.0.1"; prefixLength = 24; }];
+          ipv6.addresses = [{ address = "fc00::1"; prefixLength = 7; }];
+        };
+        br1 = {
+          ipv4.addresses = [{ address = "192.168.1.1"; prefixLength = 24; }];
+        };
+      };
+
+      containers.webserver =
+        {
+          autoStart = true;
+          privateNetwork = true;
+          hostBridge = "br0";
+          localAddress = "192.168.0.100/24";
+          localAddress6 = "fc00::2/7";
+          extraVeths = {
+            veth1 = { hostBridge = "br1"; localAddress = "192.168.1.100/24"; };
+            veth2 = { hostAddress = "192.168.2.1"; localAddress = "192.168.2.100"; };
+          };
+          config =
+            {
+              networking.firewall.allowedTCPPorts = [ 80 ];
+            };
+        };
+
+      virtualisation.additionalPaths = [ pkgs.stdenv ];
+    };
+
+  testScript =
+    ''
+      machine.wait_for_unit("default.target")
+      assert "webserver" in machine.succeed("nixos-container list")
+
+      with subtest("Status of the webserver container is up"):
+          assert "up" in machine.succeed("nixos-container status webserver")
+
+      with subtest("Ensure that the veths are inside the container"):
+          assert "state UP" in machine.succeed(
+              "nixos-container run webserver -- ip link show veth1"
+          )
+          assert "state UP" in machine.succeed(
+              "nixos-container run webserver -- ip link show veth2"
+          )
+
+      with subtest("Ensure the presence of the extra veths"):
+          assert "state UP" in machine.succeed("ip link show veth1")
+          assert "state UP" in machine.succeed("ip link show veth2")
+
+      with subtest("Ensure the veth1 is part of br1 on the host"):
+          assert "master br1" in machine.succeed("ip link show veth1")
+
+      with subtest("Ping on main veth"):
+          machine.succeed("ping -n -c 1 192.168.0.100")
+          machine.succeed("ping -n -c 1 fc00::2")
+
+      with subtest("Ping on the first extra veth"):
+          machine.succeed("ping -n -c 1 192.168.1.100 >&2")
+
+      with subtest("Ping on the second extra veth"):
+          machine.succeed("ping -n -c 1 192.168.2.100 >&2")
+
+      with subtest("Container can be stopped"):
+          machine.succeed("nixos-container stop webserver")
+          machine.fail("ping -n -c 1 192.168.1.100 >&2")
+          machine.fail("ping -n -c 1 192.168.2.100 >&2")
+
+      with subtest("Destroying a declarative container should fail"):
+          machine.fail("nixos-container destroy webserver")
+    '';
+})
diff --git a/nixos/tests/containers-hosts.nix b/nixos/tests/containers-hosts.nix
new file mode 100644
index 00000000000..3c6a1571002
--- /dev/null
+++ b/nixos/tests/containers-hosts.nix
@@ -0,0 +1,49 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "containers-hosts";
+  meta = {
+    maintainers = with lib.maintainers; [ montag451 ];
+  };
+
+  machine =
+    { lib, ... }:
+    {
+      virtualisation.vlans = [];
+
+      networking.bridges.br0.interfaces = [];
+      networking.interfaces.br0.ipv4.addresses = [
+        { address = "10.11.0.254"; prefixLength = 24; }
+      ];
+
+      # Force /etc/hosts to be the only source for host name resolution
+      environment.etc."nsswitch.conf".text = lib.mkForce ''
+        hosts: files
+      '';
+
+      containers.simple = {
+        autoStart = true;
+        privateNetwork = true;
+        localAddress = "10.10.0.1";
+        hostAddress = "10.10.0.254";
+
+        config = {};
+      };
+
+      containers.netmask = {
+        autoStart = true;
+        privateNetwork = true;
+        hostBridge = "br0";
+        localAddress = "10.11.0.1/24";
+
+        config = {};
+      };
+    };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("default.target")
+
+    with subtest("Ping the containers using the entries added in /etc/hosts"):
+        for host in "simple.containers", "netmask.containers":
+            machine.succeed(f"ping -n -c 1 {host}")
+  '';
+})
diff --git a/nixos/tests/containers-imperative.nix b/nixos/tests/containers-imperative.nix
new file mode 100644
index 00000000000..14001657bee
--- /dev/null
+++ b/nixos/tests/containers-imperative.nix
@@ -0,0 +1,166 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "containers-imperative";
+  meta = {
+    maintainers = with lib.maintainers; [ aristid aszlig eelco kampfschlaefer ];
+  };
+
+  machine =
+    { config, pkgs, lib, ... }:
+    { imports = [ ../modules/installer/cd-dvd/channel.nix ];
+
+      # XXX: Sandbox setup fails while trying to hardlink files from the host's
+      #      store file system into the prepared chroot directory.
+      nix.settings.sandbox = false;
+      nix.settings.substituters = []; # don't try to access cache.nixos.org
+
+      virtualisation.writableStore = true;
+      # Make sure we always have all the required dependencies for creating a
+      # container available within the VM, because we don't have network access.
+      virtualisation.additionalPaths = let
+        emptyContainer = import ../lib/eval-config.nix {
+          inherit (config.nixpkgs.localSystem) system;
+          modules = lib.singleton {
+            containers.foo.config = {
+              system.stateVersion = "18.03";
+            };
+          };
+        };
+      in with pkgs; [
+        stdenv stdenvNoCC emptyContainer.config.containers.foo.path
+        libxslt desktop-file-utils texinfo docbook5 libxml2
+        docbook_xsl_ns xorg.lndir documentation-highlighter
+      ];
+    };
+
+  testScript = let
+      tmpfilesContainerConfig = pkgs.writeText "container-config-tmpfiles" ''
+        {
+          systemd.tmpfiles.rules = [ "d /foo - - - - -" ];
+          systemd.services.foo = {
+            serviceConfig.Type = "oneshot";
+            script = "ls -al /foo";
+            wantedBy = [ "multi-user.target" ];
+          };
+        }
+      '';
+      brokenCfg = pkgs.writeText "broken.nix" ''
+        {
+          assertions = [
+            { assertion = false;
+              message = "I never evaluate";
+            }
+          ];
+        }
+      '';
+    in ''
+      with subtest("Make sure we have a NixOS tree (required by ‘nixos-container create’)"):
+          machine.succeed("PAGER=cat nix-env -qa -A nixos.hello >&2")
+
+      id1, id2 = None, None
+
+      with subtest("Create some containers imperatively"):
+          id1 = machine.succeed("nixos-container create foo --ensure-unique-name").rstrip()
+          machine.log(f"created container {id1}")
+
+          id2 = machine.succeed("nixos-container create foo --ensure-unique-name").rstrip()
+          machine.log(f"created container {id2}")
+
+          assert id1 != id2
+
+      with subtest(f"Put the root of {id2} into a bind mount"):
+          machine.succeed(
+              f"mv /var/lib/containers/{id2} /id2-bindmount",
+              f"mount --bind /id2-bindmount /var/lib/containers/{id1}",
+          )
+
+          ip1 = machine.succeed(f"nixos-container show-ip {id1}").rstrip()
+          ip2 = machine.succeed(f"nixos-container show-ip {id2}").rstrip()
+          assert ip1 != ip2
+
+      with subtest(
+          "Create a directory and a file we can later check if it still exists "
+          + "after destruction of the container"
+      ):
+          machine.succeed("mkdir /nested-bindmount")
+          machine.succeed("echo important data > /nested-bindmount/dummy")
+
+      with subtest(
+          "Create a directory with a dummy file and bind-mount it into both containers."
+      ):
+          for id in id1, id2:
+              important_path = f"/var/lib/containers/{id}/very/important/data"
+              machine.succeed(
+                  f"mkdir -p {important_path}",
+                  f"mount --bind /nested-bindmount {important_path}",
+              )
+
+      with subtest("Start one of them"):
+          machine.succeed(f"nixos-container start {id1}")
+
+      with subtest("Execute commands via the root shell"):
+          assert "Linux" in machine.succeed(f"nixos-container run {id1} -- uname")
+
+      with subtest("Execute a nix command via the root shell. (regression test for #40355)"):
+          machine.succeed(
+              f"nixos-container run {id1} -- nix-instantiate -E "
+              + '\'derivation { name = "empty"; builder = "false"; system = "false"; }\' '
+          )
+
+      with subtest("Stop and start (regression test for #4989)"):
+          machine.succeed(f"nixos-container stop {id1}")
+          machine.succeed(f"nixos-container start {id1}")
+
+      # clear serial backlog for next tests
+      machine.succeed("logger eat console backlog 3ea46eb2-7f82-4f70-b810-3f00e3dd4c4d")
+      machine.wait_for_console_text(
+          "eat console backlog 3ea46eb2-7f82-4f70-b810-3f00e3dd4c4d"
+      )
+
+      with subtest("Stop a container early"):
+          machine.succeed(f"nixos-container stop {id1}")
+          machine.succeed(f"nixos-container start {id1} >&2 &")
+          machine.wait_for_console_text("Stage 2")
+          machine.succeed(f"nixos-container stop {id1}")
+          machine.wait_for_console_text(f"Container {id1} exited successfully")
+          machine.succeed(f"nixos-container start {id1}")
+
+      with subtest("Stop a container without machined (regression test for #109695)"):
+          machine.systemctl("stop systemd-machined")
+          machine.succeed(f"nixos-container stop {id1}")
+          machine.wait_for_console_text(f"Container {id1} has been shut down")
+          machine.succeed(f"nixos-container start {id1}")
+
+      with subtest("tmpfiles are present"):
+          machine.log("creating container tmpfiles")
+          machine.succeed(
+              "nixos-container create tmpfiles --config-file ${tmpfilesContainerConfig}"
+          )
+          machine.log("created, starting…")
+          machine.succeed("nixos-container start tmpfiles")
+          machine.log("done starting, investigating…")
+          machine.succeed(
+              "echo $(nixos-container run tmpfiles -- systemctl is-active foo.service) | grep -q active;"
+          )
+          machine.succeed("nixos-container destroy tmpfiles")
+
+      with subtest("Execute commands via the root shell"):
+          assert "Linux" in machine.succeed(f"nixos-container run {id1} -- uname")
+
+      with subtest("Destroy the containers"):
+          for id in id1, id2:
+              machine.succeed(f"nixos-container destroy {id}")
+
+      with subtest("Check whether destruction of any container has killed important data"):
+          machine.succeed("grep -qF 'important data' /nested-bindmount/dummy")
+
+      with subtest("Ensure that the container path is gone"):
+          print(machine.succeed("ls -lsa /var/lib/containers"))
+          machine.succeed(f"test ! -e /var/lib/containers/{id1}")
+
+      with subtest("Ensure that a failed container creation doesn'leave any state"):
+          machine.fail(
+              "nixos-container create b0rk --config-file ${brokenCfg}"
+          )
+          machine.succeed("test ! -e /var/lib/containers/b0rk")
+    '';
+})
diff --git a/nixos/tests/containers-ip.nix b/nixos/tests/containers-ip.nix
new file mode 100644
index 00000000000..91fdda0392a
--- /dev/null
+++ b/nixos/tests/containers-ip.nix
@@ -0,0 +1,74 @@
+let
+  webserverFor = hostAddress: localAddress: {
+    inherit hostAddress localAddress;
+    privateNetwork = true;
+    config = {
+      services.httpd = {
+        enable = true;
+        adminAddr = "foo@example.org";
+      };
+      networking.firewall.allowedTCPPorts = [ 80 ];
+    };
+  };
+
+in import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "containers-ipv4-ipv6";
+  meta = {
+    maintainers = with lib.maintainers; [ aristid aszlig eelco kampfschlaefer ];
+  };
+
+  machine =
+    { pkgs, ... }: {
+      imports = [ ../modules/installer/cd-dvd/channel.nix ];
+      virtualisation = {
+        writableStore = true;
+      };
+
+      containers.webserver4 = webserverFor "10.231.136.1" "10.231.136.2";
+      containers.webserver6 = webserverFor "fc00::2" "fc00::1";
+      virtualisation.additionalPaths = [ pkgs.stdenv ];
+    };
+
+  testScript = { nodes, ... }: ''
+    import time
+
+
+    def curl_host(ip):
+        # put [] around ipv6 addresses for curl
+        host = ip if ":" not in ip else f"[{ip}]"
+        return f"curl --fail --connect-timeout 2 http://{host}/ > /dev/null"
+
+
+    def get_ip(container):
+        # need to distinguish because show-ip won't work for ipv6
+        if container == "webserver4":
+            ip = machine.succeed(f"nixos-container show-ip {container}").rstrip()
+            assert ip == "${nodes.machine.config.containers.webserver4.localAddress}"
+            return ip
+        return "${nodes.machine.config.containers.webserver6.localAddress}"
+
+
+    for container in "webserver4", "webserver6":
+        assert container in machine.succeed("nixos-container list")
+
+        with subtest(f"Start container {container}"):
+            machine.succeed(f"nixos-container start {container}")
+            # wait 2s for container to start and network to be up
+            time.sleep(2)
+
+        # Since "start" returns after the container has reached
+        # multi-user.target, we should now be able to access it.
+
+        ip = get_ip(container)
+        with subtest(f"{container} reacts to pings and HTTP requests"):
+            machine.succeed(f"ping -n -c1 {ip}")
+            machine.succeed(curl_host(ip))
+
+        with subtest(f"Stop container {container}"):
+            machine.succeed(f"nixos-container stop {container}")
+            machine.fail(curl_host(ip))
+
+        # Destroying a declarative container should fail.
+        machine.fail(f"nixos-container destroy {container}")
+  '';
+})
diff --git a/nixos/tests/containers-macvlans.nix b/nixos/tests/containers-macvlans.nix
new file mode 100644
index 00000000000..a0cea8db4a1
--- /dev/null
+++ b/nixos/tests/containers-macvlans.nix
@@ -0,0 +1,82 @@
+let
+  # containers IP on VLAN 1
+  containerIp1 = "192.168.1.253";
+  containerIp2 = "192.168.1.254";
+in
+
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "containers-macvlans";
+  meta = {
+    maintainers = with lib.maintainers; [ montag451 ];
+  };
+
+  nodes = {
+
+    machine1 =
+      { lib, ... }:
+      {
+        virtualisation.vlans = [ 1 ];
+
+        # To be able to ping containers from the host, it is necessary
+        # to create a macvlan on the host on the VLAN 1 network.
+        networking.macvlans.mv-eth1-host = {
+          interface = "eth1";
+          mode = "bridge";
+        };
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce [];
+        networking.interfaces.mv-eth1-host = {
+          ipv4.addresses = [ { address = "192.168.1.1"; prefixLength = 24; } ];
+        };
+
+        containers.test1 = {
+          autoStart = true;
+          macvlans = [ "eth1" ];
+
+          config = {
+            networking.interfaces.mv-eth1 = {
+              ipv4.addresses = [ { address = containerIp1; prefixLength = 24; } ];
+            };
+          };
+        };
+
+        containers.test2 = {
+          autoStart = true;
+          macvlans = [ "eth1" ];
+
+          config = {
+            networking.interfaces.mv-eth1 = {
+              ipv4.addresses = [ { address = containerIp2; prefixLength = 24; } ];
+            };
+          };
+        };
+      };
+
+    machine2 =
+      { ... }:
+      {
+        virtualisation.vlans = [ 1 ];
+      };
+
+  };
+
+  testScript = ''
+    start_all()
+    machine1.wait_for_unit("default.target")
+    machine2.wait_for_unit("default.target")
+
+    with subtest(
+        "Ping between containers to check that macvlans are created in bridge mode"
+    ):
+        machine1.succeed("nixos-container run test1 -- ping -n -c 1 ${containerIp2}")
+
+    with subtest("Ping containers from the host (machine1)"):
+        machine1.succeed("ping -n -c 1 ${containerIp1}")
+        machine1.succeed("ping -n -c 1 ${containerIp2}")
+
+    with subtest(
+        "Ping containers from the second machine to check that containers are reachable from the outside"
+    ):
+        machine2.succeed("ping -n -c 1 ${containerIp1}")
+        machine2.succeed("ping -n -c 1 ${containerIp2}")
+  '';
+})
diff --git a/nixos/tests/containers-names.nix b/nixos/tests/containers-names.nix
new file mode 100644
index 00000000000..9ad2bfb748a
--- /dev/null
+++ b/nixos/tests/containers-names.nix
@@ -0,0 +1,37 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "containers-names";
+  meta = {
+    maintainers = with lib.maintainers; [ patryk27 ];
+  };
+
+  machine = { ... }: {
+    # We're using the newest kernel, so that we can test containers with long names.
+    # Please see https://github.com/NixOS/nixpkgs/issues/38509 for details.
+    boot.kernelPackages = pkgs.linuxPackages_latest;
+
+    containers = let
+      container = subnet: {
+        autoStart = true;
+        privateNetwork = true;
+        hostAddress = "192.168.${subnet}.1";
+        localAddress = "192.168.${subnet}.2";
+        config = { };
+      };
+
+     in {
+      first = container "1";
+      second = container "2";
+      really-long-name = container "3";
+      really-long-long-name-2 = container "4";
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("default.target")
+
+    machine.succeed("ip link show | grep ve-first")
+    machine.succeed("ip link show | grep ve-second")
+    machine.succeed("ip link show | grep ve-really-lFYWO")
+    machine.succeed("ip link show | grep ve-really-l3QgY")
+  '';
+})
diff --git a/nixos/tests/containers-nested.nix b/nixos/tests/containers-nested.nix
new file mode 100644
index 00000000000..a653361494f
--- /dev/null
+++ b/nixos/tests/containers-nested.nix
@@ -0,0 +1,30 @@
+# Test for NixOS' container nesting.
+
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "nested";
+
+  meta = with pkgs.lib.maintainers; { maintainers = [ sorki ]; };
+
+  machine = { lib, ... }:
+    let
+      makeNested = subConf: {
+        containers.nested = {
+          autoStart = true;
+          privateNetwork = true;
+          config = subConf;
+        };
+      };
+    in makeNested (makeNested { });
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("container@nested.service")
+    machine.succeed("systemd-run --pty --machine=nested -- machinectl list | grep nested")
+    print(
+        machine.succeed(
+            "systemd-run --pty --machine=nested -- systemd-run --pty --machine=nested -- systemctl status"
+        )
+    )
+  '';
+})
+
diff --git a/nixos/tests/containers-physical_interfaces.nix b/nixos/tests/containers-physical_interfaces.nix
new file mode 100644
index 00000000000..e203f88786a
--- /dev/null
+++ b/nixos/tests/containers-physical_interfaces.nix
@@ -0,0 +1,131 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "containers-physical_interfaces";
+  meta = {
+    maintainers = with lib.maintainers; [ kampfschlaefer ];
+  };
+
+  nodes = {
+    server = { ... }:
+      {
+        virtualisation.vlans = [ 1 ];
+
+        containers.server = {
+          privateNetwork = true;
+          interfaces = [ "eth1" ];
+
+          config = {
+            networking.interfaces.eth1.ipv4.addresses = [
+              { address = "10.10.0.1"; prefixLength = 24; }
+            ];
+            networking.firewall.enable = false;
+          };
+        };
+      };
+    bridged = { ... }: {
+      virtualisation.vlans = [ 1 ];
+
+      containers.bridged = {
+        privateNetwork = true;
+        interfaces = [ "eth1" ];
+
+        config = {
+          networking.bridges.br0.interfaces = [ "eth1" ];
+          networking.interfaces.br0.ipv4.addresses = [
+            { address = "10.10.0.2"; prefixLength = 24; }
+          ];
+          networking.firewall.enable = false;
+        };
+      };
+    };
+
+    bonded = { ... }: {
+      virtualisation.vlans = [ 1 ];
+
+      containers.bonded = {
+        privateNetwork = true;
+        interfaces = [ "eth1" ];
+
+        config = {
+          networking.bonds.bond0 = {
+            interfaces = [ "eth1" ];
+            driverOptions.mode = "active-backup";
+          };
+          networking.interfaces.bond0.ipv4.addresses = [
+            { address = "10.10.0.3"; prefixLength = 24; }
+          ];
+          networking.firewall.enable = false;
+        };
+      };
+    };
+
+    bridgedbond = { ... }: {
+      virtualisation.vlans = [ 1 ];
+
+      containers.bridgedbond = {
+        privateNetwork = true;
+        interfaces = [ "eth1" ];
+
+        config = {
+          networking.bonds.bond0 = {
+            interfaces = [ "eth1" ];
+            driverOptions.mode = "active-backup";
+          };
+          networking.bridges.br0.interfaces = [ "bond0" ];
+          networking.interfaces.br0.ipv4.addresses = [
+            { address = "10.10.0.4"; prefixLength = 24; }
+          ];
+          networking.firewall.enable = false;
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    with subtest("Prepare server"):
+        server.wait_for_unit("default.target")
+        server.succeed("ip link show dev eth1 >&2")
+
+    with subtest("Simple physical interface is up"):
+        server.succeed("nixos-container start server")
+        server.wait_for_unit("container@server")
+        server.succeed(
+            "systemctl -M server list-dependencies network-addresses-eth1.service >&2"
+        )
+
+        # The other tests will ping this container on its ip. Here we just check
+        # that the device is present in the container.
+        server.succeed("nixos-container run server -- ip a show dev eth1 >&2")
+
+    with subtest("Physical device in bridge in container can ping server"):
+        bridged.wait_for_unit("default.target")
+        bridged.succeed("nixos-container start bridged")
+        bridged.wait_for_unit("container@bridged")
+        bridged.succeed(
+            "systemctl -M bridged list-dependencies network-addresses-br0.service >&2",
+            "systemctl -M bridged status -n 30 -l network-addresses-br0.service",
+            "nixos-container run bridged -- ping -w 10 -c 1 -n 10.10.0.1",
+        )
+
+    with subtest("Physical device in bond in container can ping server"):
+        bonded.wait_for_unit("default.target")
+        bonded.succeed("nixos-container start bonded")
+        bonded.wait_for_unit("container@bonded")
+        bonded.succeed(
+            "systemctl -M bonded list-dependencies network-addresses-bond0 >&2",
+            "systemctl -M bonded status -n 30 -l network-addresses-bond0 >&2",
+            "nixos-container run bonded -- ping -w 10 -c 1 -n 10.10.0.1",
+        )
+
+    with subtest("Physical device in bond in bridge in container can ping server"):
+        bridgedbond.wait_for_unit("default.target")
+        bridgedbond.succeed("nixos-container start bridgedbond")
+        bridgedbond.wait_for_unit("container@bridgedbond")
+        bridgedbond.succeed(
+            "systemctl -M bridgedbond list-dependencies network-addresses-br0.service >&2",
+            "systemctl -M bridgedbond status -n 30 -l network-addresses-br0.service",
+            "nixos-container run bridgedbond -- ping -w 10 -c 1 -n 10.10.0.1",
+        )
+  '';
+})
diff --git a/nixos/tests/containers-portforward.nix b/nixos/tests/containers-portforward.nix
new file mode 100644
index 00000000000..6cecd72f1bd
--- /dev/null
+++ b/nixos/tests/containers-portforward.nix
@@ -0,0 +1,59 @@
+let
+  hostIp = "192.168.0.1";
+  hostPort = 10080;
+  containerIp = "192.168.0.100";
+  containerPort = 80;
+in
+
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "containers-portforward";
+  meta = {
+    maintainers = with lib.maintainers; [ aristid aszlig eelco kampfschlaefer ianwookim ];
+  };
+
+  machine =
+    { pkgs, ... }:
+    { imports = [ ../modules/installer/cd-dvd/channel.nix ];
+      virtualisation.writableStore = true;
+
+      containers.webserver =
+        { privateNetwork = true;
+          hostAddress = hostIp;
+          localAddress = containerIp;
+          forwardPorts = [ { protocol = "tcp"; hostPort = hostPort; containerPort = containerPort; } ];
+          config =
+            { services.httpd.enable = true;
+              services.httpd.adminAddr = "foo@example.org";
+              networking.firewall.allowedTCPPorts = [ 80 ];
+            };
+        };
+
+      virtualisation.additionalPaths = [ pkgs.stdenv ];
+    };
+
+  testScript =
+    ''
+      container_list = machine.succeed("nixos-container list")
+      assert "webserver" in container_list
+
+      # Start the webserver container.
+      machine.succeed("nixos-container start webserver")
+
+      # wait two seconds for the container to start and the network to be up
+      machine.sleep(2)
+
+      # Since "start" returns after the container has reached
+      # multi-user.target, we should now be able to access it.
+      # ip = machine.succeed("nixos-container show-ip webserver").strip()
+      machine.succeed("ping -n -c1 ${hostIp}")
+      machine.succeed("curl --fail http://${hostIp}:${toString hostPort}/ > /dev/null")
+
+      # Stop the container.
+      machine.succeed("nixos-container stop webserver")
+      machine.fail("curl --fail --connect-timeout 2 http://${hostIp}:${toString hostPort}/ > /dev/null")
+
+      # Destroying a declarative container should fail.
+      machine.fail("nixos-container destroy webserver")
+    '';
+
+})
diff --git a/nixos/tests/containers-reloadable.nix b/nixos/tests/containers-reloadable.nix
new file mode 100644
index 00000000000..876e62c1da9
--- /dev/null
+++ b/nixos/tests/containers-reloadable.nix
@@ -0,0 +1,71 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+let
+  client_base = {
+    containers.test1 = {
+      autoStart = true;
+      config = {
+        environment.etc.check.text = "client_base";
+      };
+    };
+
+    # prevent make-test-python.nix to change IP
+    networking.interfaces = {
+      eth1.ipv4.addresses = lib.mkOverride 0 [ ];
+    };
+  };
+in {
+  name = "containers-reloadable";
+  meta = {
+    maintainers = with lib.maintainers; [ danbst ];
+  };
+
+  nodes = {
+    client = { ... }: {
+      imports = [ client_base ];
+    };
+
+    client_c1 = { lib, ... }: {
+      imports = [ client_base ];
+
+      containers.test1.config = {
+        environment.etc.check.text = lib.mkForce "client_c1";
+        services.httpd.enable = true;
+        services.httpd.adminAddr = "nixos@example.com";
+      };
+    };
+    client_c2 = { lib, ... }: {
+      imports = [ client_base ];
+
+      containers.test1.config = {
+        environment.etc.check.text = lib.mkForce "client_c2";
+        services.nginx.enable = true;
+      };
+    };
+  };
+
+  testScript = {nodes, ...}: let
+    c1System = nodes.client_c1.config.system.build.toplevel;
+    c2System = nodes.client_c2.config.system.build.toplevel;
+  in ''
+    client.start()
+    client.wait_for_unit("default.target")
+
+    assert "client_base" in client.succeed("nixos-container run test1 cat /etc/check")
+
+    with subtest("httpd is available after activating config1"):
+        client.succeed(
+            "${c1System}/bin/switch-to-configuration test >&2",
+            "[[ $(nixos-container run test1 cat /etc/check) == client_c1 ]] >&2",
+            "systemctl status httpd -M test1 >&2",
+        )
+
+    with subtest("httpd is not available any longer after switching to config2"):
+        client.succeed(
+            "${c2System}/bin/switch-to-configuration test >&2",
+            "[[ $(nixos-container run test1 cat /etc/check) == client_c2 ]] >&2",
+            "systemctl status nginx -M test1 >&2",
+        )
+        client.fail("systemctl status httpd -M test1 >&2")
+  '';
+
+})
diff --git a/nixos/tests/containers-restart_networking.nix b/nixos/tests/containers-restart_networking.nix
new file mode 100644
index 00000000000..e1ad8157b28
--- /dev/null
+++ b/nixos/tests/containers-restart_networking.nix
@@ -0,0 +1,113 @@
+let
+  client_base = {
+    networking.firewall.enable = false;
+
+    containers.webserver = {
+      autoStart = true;
+      privateNetwork = true;
+      hostBridge = "br0";
+      config = {
+        networking.firewall.enable = false;
+        networking.interfaces.eth0.ipv4.addresses = [
+          { address = "192.168.1.122"; prefixLength = 24; }
+        ];
+      };
+    };
+  };
+in import ./make-test-python.nix ({ pkgs, lib, ... }:
+{
+  name = "containers-restart_networking";
+  meta = {
+    maintainers = with lib.maintainers; [ kampfschlaefer ];
+  };
+
+  nodes = {
+    client = { lib, ... }: client_base // {
+      virtualisation.vlans = [ 1 ];
+
+      networking.bridges.br0 = {
+        interfaces = [];
+        rstp = false;
+      };
+      networking.interfaces = {
+        eth1.ipv4.addresses = lib.mkOverride 0 [ ];
+        br0.ipv4.addresses = [ { address = "192.168.1.1"; prefixLength = 24; } ];
+      };
+
+    };
+    client_eth1 = { lib, ... }: client_base // {
+      networking.bridges.br0 = {
+        interfaces = [ "eth1" ];
+        rstp = false;
+      };
+      networking.interfaces = {
+        eth1.ipv4.addresses = lib.mkOverride 0 [ ];
+        br0.ipv4.addresses = [ { address = "192.168.1.2"; prefixLength = 24; } ];
+      };
+    };
+    client_eth1_rstp = { lib, ... }: client_base // {
+      networking.bridges.br0 = {
+        interfaces = [ "eth1" ];
+        rstp = true;
+      };
+      networking.interfaces = {
+        eth1.ipv4.addresses = lib.mkOverride 0 [ ];
+        br0.ipv4.addresses =  [ { address = "192.168.1.2"; prefixLength = 24; } ];
+      };
+    };
+  };
+
+  testScript = {nodes, ...}: let
+    originalSystem = nodes.client.config.system.build.toplevel;
+    eth1_bridged = nodes.client_eth1.config.system.build.toplevel;
+    eth1_rstp = nodes.client_eth1_rstp.config.system.build.toplevel;
+  in ''
+    client.start()
+
+    client.wait_for_unit("default.target")
+
+    with subtest("Initial configuration connectivity check"):
+        client.succeed("ping 192.168.1.122 -c 1 -n >&2")
+        client.succeed("nixos-container run webserver -- ping -c 1 -n 192.168.1.1 >&2")
+
+        client.fail("ip l show eth1 |grep 'master br0' >&2")
+        client.fail("grep eth1 /run/br0.interfaces >&2")
+
+    with subtest("Bridged configuration without STP preserves connectivity"):
+        client.succeed(
+            "${eth1_bridged}/bin/switch-to-configuration test >&2"
+        )
+
+        client.succeed(
+            "ping 192.168.1.122 -c 1 -n >&2",
+            "nixos-container run webserver -- ping -c 1 -n 192.168.1.2 >&2",
+            "ip l show eth1 |grep 'master br0' >&2",
+            "grep eth1 /run/br0.interfaces >&2",
+        )
+
+    #  activating rstp needs another service, therefore the bridge will restart and the container will lose its connectivity
+    # with subtest("Bridged configuration with STP"):
+    #     client.succeed("${eth1_rstp}/bin/switch-to-configuration test >&2")
+    #     client.execute("ip -4 a >&2")
+    #     client.execute("ip l >&2")
+    #
+    #     client.succeed(
+    #         "ping 192.168.1.122 -c 1 -n >&2",
+    #         "nixos-container run webserver -- ping -c 1 -n 192.168.1.2 >&2",
+    #         "ip l show eth1 |grep 'master br0' >&2",
+    #         "grep eth1 /run/br0.interfaces >&2",
+    #     )
+
+    with subtest("Reverting to initial configuration preserves connectivity"):
+        client.succeed(
+            "${originalSystem}/bin/switch-to-configuration test >&2"
+        )
+
+        client.succeed("ping 192.168.1.122 -c 1 -n >&2")
+        client.succeed("nixos-container run webserver -- ping -c 1 -n 192.168.1.1 >&2")
+
+        client.fail("ip l show eth1 |grep 'master br0' >&2")
+        client.fail("grep eth1 /run/br0.interfaces >&2")
+  '';
+
+})
diff --git a/nixos/tests/containers-tmpfs.nix b/nixos/tests/containers-tmpfs.nix
new file mode 100644
index 00000000000..d95178d1ff5
--- /dev/null
+++ b/nixos/tests/containers-tmpfs.nix
@@ -0,0 +1,90 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "containers-tmpfs";
+  meta = {
+    maintainers = with lib.maintainers; [ patryk27 ];
+  };
+
+  machine =
+    { pkgs, ... }:
+    { imports = [ ../modules/installer/cd-dvd/channel.nix ];
+      virtualisation.writableStore = true;
+
+      containers.tmpfs =
+        {
+          autoStart = true;
+          tmpfs = [
+            # Mount var as a tmpfs
+            "/var"
+
+            # Add a nested mount inside a tmpfs
+            "/var/log"
+
+            # Add a tmpfs on a path that does not exist
+            "/some/random/path"
+          ];
+          config = { };
+        };
+
+      virtualisation.additionalPaths = [ pkgs.stdenv ];
+    };
+
+  testScript = ''
+      machine.wait_for_unit("default.target")
+      assert "tmpfs" in machine.succeed("nixos-container list")
+
+      with subtest("tmpfs container is up"):
+          assert "up" in machine.succeed("nixos-container status tmpfs")
+
+
+      def tmpfs_cmd(command):
+          return f"nixos-container run tmpfs -- {command} 2>/dev/null"
+
+
+      with subtest("/var is mounted as a tmpfs"):
+          machine.succeed(tmpfs_cmd("mountpoint -q /var"))
+
+      with subtest("/var/log is mounted as a tmpfs"):
+          assert "What: tmpfs" in machine.succeed(
+              tmpfs_cmd("systemctl status var-log.mount --no-pager")
+          )
+          machine.succeed(tmpfs_cmd("mountpoint -q /var/log"))
+
+      with subtest("/some/random/path is mounted as a tmpfs"):
+          assert "What: tmpfs" in machine.succeed(
+              tmpfs_cmd("systemctl status some-random-path.mount --no-pager")
+          )
+          machine.succeed(tmpfs_cmd("mountpoint -q /some/random/path"))
+
+      with subtest(
+          "files created in the container in a non-tmpfs directory are visible on the host."
+      ):
+          # This establishes legitimacy for the following tests
+          machine.succeed(
+              tmpfs_cmd("touch /root/test.file"),
+              tmpfs_cmd("ls -l  /root | grep -q test.file"),
+              "test -e /var/lib/containers/tmpfs/root/test.file",
+          )
+
+      with subtest(
+          "/some/random/path is writable and that files created there are not "
+          + "in the hosts container dir but in the tmpfs"
+      ):
+          machine.succeed(
+              tmpfs_cmd("touch /some/random/path/test.file"),
+              tmpfs_cmd("test -e /some/random/path/test.file"),
+          )
+          machine.fail("test -e /var/lib/containers/tmpfs/some/random/path/test.file")
+
+      with subtest(
+          "files created in the hosts container dir in a path where a tmpfs "
+          + "file system has been mounted are not visible to the container as "
+          + "the do not exist in the tmpfs"
+      ):
+          machine.succeed(
+              "touch /var/lib/containers/tmpfs/var/test.file",
+              "test -e /var/lib/containers/tmpfs/var/test.file",
+              "ls -l /var/lib/containers/tmpfs/var/ | grep -q test.file 2>/dev/null",
+          )
+          machine.fail(tmpfs_cmd("ls -l /var | grep -q test.file"))
+    '';
+})
diff --git a/nixos/tests/convos.nix b/nixos/tests/convos.nix
new file mode 100644
index 00000000000..a13870d1708
--- /dev/null
+++ b/nixos/tests/convos.nix
@@ -0,0 +1,30 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+
+with lib;
+let
+  port = 3333;
+in
+{
+  name = "convos";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ sgo ];
+  };
+
+  nodes = {
+    machine =
+      { pkgs, ... }:
+      {
+        services.convos = {
+          enable = true;
+          listenPort = port;
+        };
+      };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("convos")
+    machine.wait_for_open_port("${toString port}")
+    machine.succeed("journalctl -u convos | grep -q 'Listening at.*${toString port}'")
+    machine.succeed("curl -f http://localhost:${toString port}/")
+  '';
+})
diff --git a/nixos/tests/corerad.nix b/nixos/tests/corerad.nix
new file mode 100644
index 00000000000..638010f92f4
--- /dev/null
+++ b/nixos/tests/corerad.nix
@@ -0,0 +1,89 @@
+import ./make-test-python.nix (
+  {
+    nodes = {
+      router = {config, pkgs, ...}: {
+        config = {
+          # This machine simulates a router with IPv6 forwarding and a static IPv6 address.
+          boot.kernel.sysctl = {
+            "net.ipv6.conf.all.forwarding" = true;
+          };
+          networking.interfaces.eth1 = {
+            ipv6.addresses = [ { address = "fd00:dead:beef:dead::1"; prefixLength = 64; } ];
+          };
+          services.corerad = {
+            enable = true;
+            # Serve router advertisements to the client machine with prefix information matching
+            # any IPv6 /64 prefixes configured on this interface.
+            #
+            # This configuration is identical to the example in the CoreRAD NixOS module.
+            settings = {
+              interfaces = [
+                {
+                  name = "eth0";
+                  monitor = true;
+                }
+                {
+                  name = "eth1";
+                  advertise = true;
+                  prefix = [{ prefix = "::/64"; }];
+                }
+              ];
+              debug = {
+                address = "localhost:9430";
+                prometheus = true;
+              };
+            };
+          };
+        };
+      };
+      client = {config, pkgs, ...}: {
+        # Use IPv6 SLAAC from router advertisements, and install rdisc6 so we can
+        # trigger one immediately.
+        config = {
+          boot.kernel.sysctl = {
+            "net.ipv6.conf.all.autoconf" = true;
+          };
+          environment.systemPackages = with pkgs; [
+            ndisc6
+          ];
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      with subtest("Wait for CoreRAD and network ready"):
+          # Ensure networking is online and CoreRAD is ready.
+          router.wait_for_unit("network-online.target")
+          client.wait_for_unit("network-online.target")
+          router.wait_for_unit("corerad.service")
+
+          # Ensure the client can reach the router.
+          client.wait_until_succeeds("ping -c 1 fd00:dead:beef:dead::1")
+
+      with subtest("Verify SLAAC on client"):
+          # Trigger a router solicitation and verify a SLAAC address is assigned from
+          # the prefix configured on the router.
+          client.wait_until_succeeds("rdisc6 -1 -r 10 eth1")
+          client.wait_until_succeeds(
+              "ip -6 addr show dev eth1 | grep -q 'fd00:dead:beef:dead:'"
+          )
+
+          addrs = client.succeed("ip -6 addr show dev eth1")
+
+          assert (
+              "fd00:dead:beef:dead:" in addrs
+          ), "SLAAC prefix was not found in client addresses after router advertisement"
+          assert (
+              "/64 scope global temporary" in addrs
+          ), "SLAAC temporary address was not configured on client after router advertisement"
+
+      with subtest("Verify HTTP debug server is configured"):
+          out = router.succeed("curl -f localhost:9430/metrics")
+
+          assert (
+              "corerad_build_info" in out
+          ), "Build info metric was not found in Prometheus output"
+    '';
+  })
diff --git a/nixos/tests/coturn.nix b/nixos/tests/coturn.nix
new file mode 100644
index 00000000000..dff832281c7
--- /dev/null
+++ b/nixos/tests/coturn.nix
@@ -0,0 +1,29 @@
+import ./make-test-python.nix ({ ... }: {
+  name = "coturn";
+  nodes = {
+    default = {
+      services.coturn.enable = true;
+    };
+    secretsfile = {
+      boot.postBootCommands = ''
+        echo "some-very-secret-string" > /run/coturn-secret
+      '';
+      services.coturn = {
+        enable = true;
+        static-auth-secret-file = "/run/coturn-secret";
+      };
+    };
+  };
+
+  testScript =
+    ''
+      start_all()
+
+      with subtest("by default works without configuration"):
+          default.wait_for_unit("coturn.service")
+
+      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")
+    '';
+})
diff --git a/nixos/tests/couchdb.nix b/nixos/tests/couchdb.nix
new file mode 100644
index 00000000000..453f5dcd66e
--- /dev/null
+++ b/nixos/tests/couchdb.nix
@@ -0,0 +1,63 @@
+let
+
+  makeNode = couchpkg: user: passwd:
+    { pkgs, ... } :
+
+      { environment.systemPackages = with pkgs; [ jq ];
+        services.couchdb.enable = true;
+        services.couchdb.package = couchpkg;
+        services.couchdb.adminUser = user;
+        services.couchdb.adminPass = passwd;
+      };
+  testuser = "testadmin";
+  testpass = "cowabunga";
+  testlogin = "${testuser}:${testpass}@";
+
+in import ./make-test-python.nix ({ pkgs, lib, ...}:
+
+with lib;
+
+{
+  name = "couchdb";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fpletz ];
+  };
+
+  nodes = {
+    couchdb3 = makeNode pkgs.couchdb3 testuser testpass;
+  };
+
+  testScript = let
+    curlJqCheck = login: action: path: jqexpr: result:
+      pkgs.writeScript "curl-jq-check-${action}-${path}.sh" ''
+        RESULT=$(curl -X ${action} http://${login}127.0.0.1:5984/${path} | jq -r '${jqexpr}')
+        echo $RESULT >&2
+        if [ "$RESULT" != "${result}" ]; then
+          exit 1
+        fi
+      '';
+  in ''
+    start_all()
+
+    couchdb3.wait_for_unit("couchdb.service")
+    couchdb3.wait_until_succeeds(
+        "${curlJqCheck testlogin "GET" "" ".couchdb" "Welcome"}"
+    )
+    couchdb3.wait_until_succeeds(
+        "${curlJqCheck testlogin "GET" "_all_dbs" ". | length" "0"}"
+    )
+    couchdb3.succeed("${curlJqCheck testlogin "PUT" "foo" ".ok" "true"}")
+    couchdb3.succeed(
+        "${curlJqCheck testlogin "GET" "_all_dbs" ". | length" "1"}"
+    )
+    couchdb3.succeed(
+        "${curlJqCheck testlogin "DELETE" "foo" ".ok" "true"}"
+    )
+    couchdb3.succeed(
+        "${curlJqCheck testlogin "GET" "_all_dbs" ". | length" "0"}"
+    )
+    couchdb3.succeed(
+        "${curlJqCheck testlogin "GET" "_node/couchdb@127.0.0.1" ".couchdb" "Welcome"}"
+    )
+  '';
+})
diff --git a/nixos/tests/cri-o.nix b/nixos/tests/cri-o.nix
new file mode 100644
index 00000000000..91d46657f24
--- /dev/null
+++ b/nixos/tests/cri-o.nix
@@ -0,0 +1,19 @@
+# This test runs CRI-O and verifies via critest
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "cri-o";
+  maintainers = with pkgs.lib.maintainers; teams.podman.members;
+
+  nodes = {
+    crio = {
+      virtualisation.cri-o.enable = true;
+    };
+  };
+
+  testScript = ''
+    start_all()
+    crio.wait_for_unit("crio.service")
+    crio.succeed(
+        "critest --ginkgo.focus='Runtime info' --runtime-endpoint unix:///var/run/crio/crio.sock"
+    )
+  '';
+})
diff --git a/nixos/tests/croc.nix b/nixos/tests/croc.nix
new file mode 100644
index 00000000000..5d709eb3d1c
--- /dev/null
+++ b/nixos/tests/croc.nix
@@ -0,0 +1,51 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+let
+  client = { pkgs, ... }: {
+    environment.systemPackages = [ pkgs.croc ];
+  };
+  pass = pkgs.writeText "pass" "PassRelay";
+in {
+  name = "croc";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ hax404 julm ];
+  };
+
+  nodes = {
+    relay = {
+      services.croc = {
+        enable = true;
+        pass = pass;
+        openFirewall = true;
+      };
+    };
+    sender = client;
+    receiver = client;
+  };
+
+  testScript = ''
+    start_all()
+
+    # wait until relay is up
+    relay.wait_for_unit("croc")
+    relay.wait_for_open_port(9009)
+    relay.wait_for_open_port(9010)
+    relay.wait_for_open_port(9011)
+    relay.wait_for_open_port(9012)
+    relay.wait_for_open_port(9013)
+
+    # generate testfiles and send them
+    sender.wait_for_unit("multi-user.target")
+    sender.execute("echo Hello World > testfile01.txt")
+    sender.execute("echo Hello Earth > testfile02.txt")
+    sender.execute(
+        "croc --pass ${pass} --relay relay send --code topSecret testfile01.txt testfile02.txt >&2 &"
+    )
+
+    # receive the testfiles and check them
+    receiver.succeed(
+        "croc --pass ${pass} --yes --relay relay topSecret"
+    )
+    assert "Hello World" in receiver.succeed("cat testfile01.txt")
+    assert "Hello Earth" in receiver.succeed("cat testfile02.txt")
+  '';
+})
diff --git a/nixos/tests/cryptpad.nix b/nixos/tests/cryptpad.nix
new file mode 100644
index 00000000000..895f291abac
--- /dev/null
+++ b/nixos/tests/cryptpad.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "cryptpad";
+  meta.maintainers = with maintainers; [ davhau ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    { services.cryptpad.enable = true; };
+
+  testScript = ''
+    machine.wait_for_unit("cryptpad.service")
+    machine.wait_for_open_port("3000")
+    machine.succeed("curl -L --fail http://localhost:3000/sheet")
+  '';
+})
diff --git a/nixos/tests/custom-ca.nix b/nixos/tests/custom-ca.nix
new file mode 100644
index 00000000000..a55449a397a
--- /dev/null
+++ b/nixos/tests/custom-ca.nix
@@ -0,0 +1,179 @@
+# Checks that `security.pki` options are working in curl and the main browser
+# engines: Gecko (via Firefox), Chromium, QtWebEngine (Falkon) and WebKitGTK
+# (via Midori). The test checks that certificates issued by a custom trusted
+# CA are accepted but those from an unknown CA are rejected.
+
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+let
+  makeCert = { caName, domain }: pkgs.runCommand "example-cert"
+  { buildInputs = [ pkgs.gnutls ]; }
+  ''
+    mkdir $out
+
+    # CA cert template
+    cat >ca.template <<EOF
+    organization = "${caName}"
+    cn = "${caName}"
+    expiration_days = 365
+    ca
+    cert_signing_key
+    crl_signing_key
+    EOF
+
+    # server cert template
+    cat >server.template <<EOF
+    organization = "An example company"
+    cn = "${domain}"
+    expiration_days = 30
+    dns_name = "${domain}"
+    encryption_key
+    signing_key
+    EOF
+
+    # generate CA keypair
+    certtool                \
+      --generate-privkey    \
+      --key-type rsa        \
+      --sec-param High      \
+      --outfile $out/ca.key
+    certtool                     \
+      --generate-self-signed     \
+      --load-privkey $out/ca.key \
+      --template ca.template     \
+      --outfile $out/ca.crt
+
+    # generate server keypair
+    certtool                    \
+      --generate-privkey        \
+      --key-type rsa            \
+      --sec-param High          \
+      --outfile $out/server.key
+    certtool                            \
+      --generate-certificate            \
+      --load-privkey $out/server.key    \
+      --load-ca-privkey $out/ca.key     \
+      --load-ca-certificate $out/ca.crt \
+      --template server.template        \
+      --outfile $out/server.crt
+  '';
+
+  example-good-cert = makeCert
+    { caName = "Example good CA";
+      domain = "good.example.com";
+    };
+
+  example-bad-cert = makeCert
+    { caName = "Unknown CA";
+      domain = "bad.example.com";
+    };
+
+in
+
+{
+  name = "custom-ca";
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+  enableOCR = true;
+
+  machine = { pkgs, ... }:
+    { imports = [ ./common/user-account.nix ./common/x11.nix ];
+
+      # chromium-based browsers refuse to run as root
+      test-support.displayManager.auto.user = "alice";
+
+      # browsers may hang with the default memory
+      virtualisation.memorySize = 600;
+
+      networking.hosts."127.0.0.1" = [ "good.example.com" "bad.example.com" ];
+      security.pki.certificateFiles = [ "${example-good-cert}/ca.crt" ];
+
+      services.nginx.enable = true;
+      services.nginx.virtualHosts."good.example.com" =
+        { onlySSL = true;
+          sslCertificate = "${example-good-cert}/server.crt";
+          sslCertificateKey = "${example-good-cert}/server.key";
+          locations."/".extraConfig = ''
+            add_header Content-Type text/plain;
+            return 200 'It works!';
+          '';
+        };
+      services.nginx.virtualHosts."bad.example.com" =
+        { onlySSL = true;
+          sslCertificate = "${example-bad-cert}/server.crt";
+          sslCertificateKey = "${example-bad-cert}/server.key";
+          locations."/".extraConfig = ''
+            add_header Content-Type text/plain;
+            return 200 'It does not work!';
+          '';
+        };
+
+      environment.systemPackages = with pkgs; [
+        xdotool
+        firefox
+        chromium
+        qutebrowser
+        midori
+      ];
+    };
+
+  testScript = ''
+    from typing import Tuple
+    def execute_as(user: str, cmd: str) -> Tuple[int, str]:
+        """
+        Run a shell command as a specific user.
+        """
+        return machine.execute(f"sudo -u {user} {cmd}")
+
+
+    def wait_for_window_as(user: str, cls: str) -> None:
+        """
+        Wait until a X11 window of a given user appears.
+        """
+
+        def window_is_visible(last_try: bool) -> bool:
+            ret, stdout = execute_as(user, f"xdotool search --onlyvisible --class {cls}")
+            if last_try:
+                machine.log(f"Last chance to match {cls} on the window list")
+            return ret == 0
+
+        with machine.nested("Waiting for a window to appear"):
+            retry(window_is_visible)
+
+
+    machine.start()
+
+    with subtest("Good certificate is trusted in curl"):
+        machine.wait_for_unit("nginx")
+        machine.wait_for_open_port(443)
+        machine.succeed("curl -fv https://good.example.com")
+
+    with subtest("Unknown CA is untrusted in curl"):
+        machine.fail("curl -fv https://bad.example.com")
+
+    browsers = {
+      "firefox": "Security Risk",
+      "chromium": "not private",
+      "qutebrowser -T": "Certificate error",
+      "midori": "Security"
+    }
+
+    machine.wait_for_x()
+    for command, error in browsers.items():
+        browser = command.split()[0]
+        with subtest("Good certificate is trusted in " + browser):
+            execute_as(
+                "alice", f"{command} https://good.example.com >&2 &"
+            )
+            wait_for_window_as("alice", browser)
+            machine.wait_for_text("It works!")
+            machine.screenshot("good" + browser)
+            execute_as("alice", "xdotool key ctrl+w")  # close tab
+
+        with subtest("Unknown CA is untrusted in " + browser):
+            execute_as("alice", f"{command} https://bad.example.com >&2 &")
+            machine.wait_for_text(error)
+            machine.screenshot("bad" + browser)
+            machine.succeed("pkill -f " + browser)
+  '';
+})
diff --git a/nixos/tests/deluge.nix b/nixos/tests/deluge.nix
new file mode 100644
index 00000000000..33c57ce7c36
--- /dev/null
+++ b/nixos/tests/deluge.nix
@@ -0,0 +1,61 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "deluge";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ flokli ];
+  };
+
+  nodes = {
+    simple = {
+      services.deluge = {
+        enable = true;
+        package = pkgs.deluge-2_x;
+        web = {
+          enable = true;
+          openFirewall = true;
+        };
+      };
+    };
+
+    declarative = {
+      services.deluge = {
+        enable = true;
+        package = pkgs.deluge-2_x;
+        openFirewall = true;
+        declarative = true;
+        config = {
+          allow_remote = true;
+          download_location = "/var/lib/deluge/my-download";
+          daemon_port = 58846;
+          listen_ports = [ 6881 6889 ];
+        };
+        web = {
+          enable = true;
+          port =  3142;
+        };
+        authFile = pkgs.writeText "deluge-auth" ''
+          localclient:a7bef72a890:10
+          andrew:password:10
+          user3:anotherpass:5
+        '';
+      };
+    };
+
+  };
+
+  testScript = ''
+    start_all()
+
+    simple.wait_for_unit("deluged")
+    simple.wait_for_unit("delugeweb")
+    simple.wait_for_open_port("8112")
+    declarative.wait_for_unit("network.target")
+    declarative.wait_until_succeeds("curl --fail http://simple:8112")
+
+    declarative.wait_for_unit("deluged")
+    declarative.wait_for_unit("delugeweb")
+    declarative.wait_until_succeeds("curl --fail http://declarative:3142")
+    declarative.succeed(
+        "deluge-console 'connect 127.0.0.1:58846 andrew password; help' | grep -q 'rm.*Remove a torrent'"
+    )
+  '';
+})
diff --git a/nixos/tests/dendrite.nix b/nixos/tests/dendrite.nix
new file mode 100644
index 00000000000..a444c9b2001
--- /dev/null
+++ b/nixos/tests/dendrite.nix
@@ -0,0 +1,99 @@
+import ./make-test-python.nix (
+  { pkgs, ... }:
+    let
+      homeserverUrl = "http://homeserver:8008";
+
+      private_key = pkgs.runCommand "matrix_key.pem" {
+        buildInputs = [ pkgs.dendrite ];
+      } "generate-keys --private-key $out";
+    in
+      {
+        name = "dendrite";
+        meta = with pkgs.lib; {
+          maintainers = teams.matrix.members;
+        };
+
+        nodes = {
+          homeserver = { pkgs, ... }: {
+            services.dendrite = {
+              enable = true;
+              settings = {
+                global.server_name = "test-dendrite-server.com";
+                global.private_key = private_key;
+                client_api.registration_disabled = false;
+              };
+            };
+
+            networking.firewall.allowedTCPPorts = [ 8008 ];
+          };
+
+          client = { pkgs, ... }: {
+            environment.systemPackages = [
+              (
+                pkgs.writers.writePython3Bin "do_test"
+                  { libraries = [ pkgs.python3Packages.matrix-nio ]; } ''
+                  import asyncio
+
+                  from nio import AsyncClient
+
+
+                  async def main() -> None:
+                      # Connect to dendrite
+                      client = AsyncClient("http://homeserver:8008", "alice")
+
+                      # Register as user alice
+                      response = await client.register("alice", "my-secret-password")
+
+                      # Log in as user alice
+                      response = await client.login("my-secret-password")
+
+                      # Create a new room
+                      response = await client.room_create(federate=False)
+                      room_id = response.room_id
+
+                      # Join the room
+                      response = await client.join(room_id)
+
+                      # Send a message to the room
+                      response = await client.room_send(
+                          room_id=room_id,
+                          message_type="m.room.message",
+                          content={
+                              "msgtype": "m.text",
+                              "body": "Hello world!"
+                          }
+                      )
+
+                      # Sync responses
+                      response = await client.sync(timeout=30000)
+
+                      # Check the message was received by dendrite
+                      last_message = response.rooms.join[room_id].timeline.events[-1].body
+                      assert last_message == "Hello world!"
+
+                      # Leave the room
+                      response = await client.room_leave(room_id)
+
+                      # Close the client
+                      await client.close()
+
+                  asyncio.get_event_loop().run_until_complete(main())
+                ''
+              )
+            ];
+          };
+        };
+
+        testScript = ''
+          start_all()
+
+          with subtest("start the homeserver"):
+              homeserver.wait_for_unit("dendrite.service")
+              homeserver.wait_for_open_port(8008)
+
+          with subtest("ensure messages can be exchanged"):
+              client.succeed("do_test")
+        '';
+
+      }
+)
diff --git a/nixos/tests/dex-oidc.nix b/nixos/tests/dex-oidc.nix
new file mode 100644
index 00000000000..37275a97ef0
--- /dev/null
+++ b/nixos/tests/dex-oidc.nix
@@ -0,0 +1,78 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "dex-oidc";
+  meta.maintainers = with lib.maintainers; [ Flakebi ];
+
+  nodes.machine = { pkgs, ... }: {
+    environment.systemPackages = with pkgs; [ jq ];
+    services.dex = {
+      enable = true;
+      settings = {
+        issuer = "http://127.0.0.1:8080/dex";
+        storage = {
+          type = "postgres";
+          config.host = "/var/run/postgresql";
+        };
+        web.http = "127.0.0.1:8080";
+        oauth2.skipApprovalScreen = true;
+        staticClients = [
+          {
+            id = "oidcclient";
+            name = "Client";
+            redirectURIs = [ "https://example.com/callback" ];
+            secretFile = "/etc/dex/oidcclient";
+          }
+        ];
+        connectors = [
+          {
+            type = "mockPassword";
+            id = "mock";
+            name = "Example";
+            config = {
+              username = "admin";
+              password = "password";
+            };
+          }
+        ];
+      };
+    };
+
+    # This should not be set from nix but through other means to not leak the secret.
+    environment.etc."dex/oidcclient" = {
+      mode = "0400";
+      user = "dex";
+      text = "oidcclientsecret";
+    };
+
+    services.postgresql = {
+      enable = true;
+      ensureDatabases =[ "dex" ];
+      ensureUsers = [
+        {
+          name = "dex";
+          ensurePermissions = { "DATABASE dex" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+  };
+
+  testScript = ''
+    with subtest("Web server gets ready"):
+        machine.wait_for_unit("dex.service")
+        # Wait until server accepts connections
+        machine.wait_until_succeeds("curl -fs 'localhost:8080/dex/auth/mock?client_id=oidcclient&response_type=code&redirect_uri=https://example.com/callback&scope=openid'")
+
+    with subtest("Login"):
+        state = machine.succeed("curl -fs 'localhost:8080/dex/auth/mock?client_id=oidcclient&response_type=code&redirect_uri=https://example.com/callback&scope=openid' | sed -n 's/.*state=\\(.*\\)\">.*/\\1/p'").strip()
+        print(f"Got state {state}")
+        machine.succeed(f"curl -fs 'localhost:8080/dex/auth/mock/login?back=&state={state}' -d 'login=admin&password=password'")
+        code = machine.succeed(f"curl -fs localhost:8080/dex/approval?req={state} | sed -n 's/.*code=\\(.*\\)&amp;.*/\\1/p'").strip()
+        print(f"Got approval code {code}")
+        bearer = machine.succeed(f"curl -fs localhost:8080/dex/token -u oidcclient:oidcclientsecret -d 'grant_type=authorization_code&redirect_uri=https://example.com/callback&code={code}' | jq .access_token -r").strip()
+        print(f"Got access token {bearer}")
+
+    with subtest("Get userinfo"):
+        assert '"sub"' in machine.succeed(
+            f"curl -fs localhost:8080/dex/userinfo --oauth2-bearer {bearer}"
+        )
+  '';
+})
diff --git a/nixos/tests/dhparams.nix b/nixos/tests/dhparams.nix
new file mode 100644
index 00000000000..a0de2911777
--- /dev/null
+++ b/nixos/tests/dhparams.nix
@@ -0,0 +1,142 @@
+let
+  common = { pkgs, ... }: {
+    security.dhparams.enable = true;
+    environment.systemPackages = [ pkgs.openssl ];
+  };
+
+in import ./make-test-python.nix {
+  name = "dhparams";
+
+  nodes.generation1 = { pkgs, config, ... }: {
+    imports = [ common ];
+    security.dhparams.params = {
+      # Use low values here because we don't want the test to run for ages.
+      foo.bits = 16;
+      # Also use the old format to make sure the type is coerced in the right
+      # way.
+      bar = 17;
+    };
+
+    systemd.services.foo = {
+      description = "Check systemd Ordering";
+      wantedBy = [ "multi-user.target" ];
+      unitConfig = {
+        # This is to make sure that the dhparams generation of foo occurs
+        # before this service so we need this service to start as early as
+        # possible to provoke a race condition.
+        DefaultDependencies = false;
+
+        # We check later whether the service has been started or not.
+        ConditionPathExists = config.security.dhparams.params.foo.path;
+      };
+      serviceConfig.Type = "oneshot";
+      serviceConfig.RemainAfterExit = true;
+      # The reason we only provide an ExecStop here is to ensure that we don't
+      # accidentally trigger an error because a file system is not yet ready
+      # during very early startup (we might not even have the Nix store
+      # available, for example if future changes in NixOS use systemd mount
+      # units to do early file system initialisation).
+      serviceConfig.ExecStop = "${pkgs.coreutils}/bin/true";
+    };
+  };
+
+  nodes.generation2 = {
+    imports = [ common ];
+    security.dhparams.params.foo.bits = 18;
+  };
+
+  nodes.generation3 = common;
+
+  nodes.generation4 = {
+    imports = [ common ];
+    security.dhparams.stateful = false;
+    security.dhparams.params.foo2.bits = 18;
+    security.dhparams.params.bar2.bits = 19;
+  };
+
+  nodes.generation5 = {
+    imports = [ common ];
+    security.dhparams.defaultBitSize = 30;
+    security.dhparams.params.foo3 = {};
+    security.dhparams.params.bar3 = {};
+  };
+
+  testScript = { nodes, ... }: let
+    getParamPath = gen: name: let
+      node = "generation${toString gen}";
+    in nodes.${node}.config.security.dhparams.params.${name}.path;
+
+    switchToGeneration = gen: let
+      node = "generation${toString gen}";
+      inherit (nodes.${node}.config.system.build) toplevel;
+      switchCmd = "${toplevel}/bin/switch-to-configuration test";
+    in ''
+      with machine.nested("switch to generation ${toString gen}"):
+          machine.succeed(
+              "${switchCmd}"
+          )
+          machine = ${node}
+    '';
+
+  in ''
+    import re
+
+
+    def assert_param_bits(path, bits):
+        with machine.nested(f"check bit size of {path}"):
+            output = machine.succeed(f"openssl dhparam -in {path} -text")
+            pattern = re.compile(r"^\s*DH Parameters:\s+\((\d+)\s+bit\)\s*$", re.M)
+            match = pattern.match(output)
+            if match is None:
+                raise Exception("bla")
+            if match[1] != str(bits):
+                raise Exception(f"bit size should be {bits} but it is {match[1]} instead.")
+
+
+    machine = generation1
+
+    machine.wait_for_unit("multi-user.target")
+
+    with subtest("verify startup order"):
+        machine.succeed("systemctl is-active foo.service")
+
+    with subtest("check bit sizes of dhparam files"):
+        assert_param_bits("${getParamPath 1 "foo"}", 16)
+        assert_param_bits("${getParamPath 1 "bar"}", 17)
+
+    ${switchToGeneration 2}
+
+    with subtest("check whether bit size has changed"):
+        assert_param_bits("${getParamPath 2 "foo"}", 18)
+
+    with subtest("ensure that dhparams file for 'bar' was deleted"):
+        machine.fail("test -e ${getParamPath 1 "bar"}")
+
+    ${switchToGeneration 3}
+
+    with subtest("ensure that 'security.dhparams.path' has been deleted"):
+        machine.fail("test -e ${nodes.generation3.config.security.dhparams.path}")
+
+    ${switchToGeneration 4}
+
+    with subtest("check bit sizes dhparam files"):
+        assert_param_bits(
+            "${getParamPath 4 "foo2"}", 18
+        )
+        assert_param_bits(
+            "${getParamPath 4 "bar2"}", 19
+        )
+
+    with subtest("check whether dhparam files are in the Nix store"):
+        machine.succeed(
+            "expr match ${getParamPath 4 "foo2"} ${builtins.storeDir}",
+            "expr match ${getParamPath 4 "bar2"} ${builtins.storeDir}",
+        )
+
+    ${switchToGeneration 5}
+
+    with subtest("check whether defaultBitSize works as intended"):
+        assert_param_bits("${getParamPath 5 "foo3"}", 30)
+        assert_param_bits("${getParamPath 5 "bar3"}", 30)
+  '';
+}
diff --git a/nixos/tests/disable-installer-tools.nix b/nixos/tests/disable-installer-tools.nix
new file mode 100644
index 00000000000..23c15faa8d3
--- /dev/null
+++ b/nixos/tests/disable-installer-tools.nix
@@ -0,0 +1,29 @@
+import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }:
+
+{
+  name = "disable-installer-tools";
+
+  machine =
+    { pkgs, lib, ... }:
+    {
+        system.disableInstallerTools = true;
+        boot.enableContainers = false;
+        environment.defaultPackages = [];
+    };
+
+  testScript = ''
+      machine.wait_for_unit("multi-user.target")
+      machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
+
+      with subtest("nixos installer tools should not be included"):
+          machine.fail("which nixos-rebuild")
+          machine.fail("which nixos-install")
+          machine.fail("which nixos-generate-config")
+          machine.fail("which nixos-enter")
+          machine.fail("which nixos-version")
+          machine.fail("which nixos-build-vms")
+
+      with subtest("perl should not be included"):
+          machine.fail("which perl")
+  '';
+})
diff --git a/nixos/tests/discourse.nix b/nixos/tests/discourse.nix
new file mode 100644
index 00000000000..cfac5f84a62
--- /dev/null
+++ b/nixos/tests/discourse.nix
@@ -0,0 +1,201 @@
+# This tests Discourse by:
+#  1. logging in as the admin user
+#  2. sending a private message to the admin user through the API
+#  3. replying to that message via email.
+
+import ./make-test-python.nix (
+  { pkgs, lib, package ? pkgs.discourse, ... }:
+  let
+    certs = import ./common/acme/server/snakeoil-certs.nix;
+    clientDomain = "client.fake.domain";
+    discourseDomain = certs.domain;
+    adminPassword = "eYAX85qmMJ5GZIHLaXGDAoszD7HSZp5d";
+    secretKeyBase = "381f4ac6d8f5e49d804dae72aa9c046431d2f34c656a705c41cd52fed9b4f6f76f51549f0b55db3b8b0dded7a00d6a381ebe9a4367d2d44f5e743af6628b4d42";
+    admin = {
+      email = "alice@${clientDomain}";
+      username = "alice";
+      fullName = "Alice Admin";
+      passwordFile = "${pkgs.writeText "admin-pass" adminPassword}";
+    };
+  in
+  {
+    name = "discourse";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ talyz ];
+    };
+
+    nodes.discourse =
+      { nodes, ... }:
+      {
+        virtualisation.memorySize = 2048;
+        virtualisation.cores = 4;
+        virtualisation.useNixStoreImage = true;
+
+        imports = [ common/user-account.nix ];
+
+        security.pki.certificateFiles = [
+          certs.ca.cert
+        ];
+
+        networking.extraHosts = ''
+          127.0.0.1 ${discourseDomain}
+          ${nodes.client.config.networking.primaryIPAddress} ${clientDomain}
+        '';
+
+        services.postfix = {
+          enableSubmission = true;
+          enableSubmissions = true;
+          submissionsOptions = {
+            smtpd_sasl_auth_enable = "yes";
+            smtpd_client_restrictions = "permit";
+          };
+        };
+
+        environment.systemPackages = [ pkgs.jq ];
+
+        services.postgresql.package = pkgs.postgresql_13;
+
+        services.discourse = {
+          enable = true;
+          inherit admin package;
+          hostname = discourseDomain;
+          sslCertificate = "${certs.${discourseDomain}.cert}";
+          sslCertificateKey = "${certs.${discourseDomain}.key}";
+          secretKeyBaseFile = "${pkgs.writeText "secret-key-base" secretKeyBase}";
+          enableACME = false;
+          mail.outgoing.serverAddress = clientDomain;
+          mail.incoming.enable = true;
+          siteSettings = {
+            posting = {
+              min_post_length = 5;
+              min_first_post_length = 5;
+              min_personal_message_post_length = 5;
+            };
+          };
+          unicornTimeout = 900;
+        };
+
+        networking.firewall.allowedTCPPorts = [ 25 465 ];
+      };
+
+    nodes.client =
+      { nodes, ... }:
+      {
+        imports = [ common/user-account.nix ];
+
+        security.pki.certificateFiles = [
+          certs.ca.cert
+        ];
+
+        networking.extraHosts = ''
+          127.0.0.1 ${clientDomain}
+          ${nodes.discourse.config.networking.primaryIPAddress} ${discourseDomain}
+        '';
+
+        services.dovecot2 = {
+          enable = true;
+          protocols = [ "imap" ];
+          modules = [ pkgs.dovecot_pigeonhole ];
+        };
+
+        services.postfix = {
+          enable = true;
+          origin = clientDomain;
+          relayDomains = [ clientDomain ];
+          config = {
+            compatibility_level = "2";
+            smtpd_banner = "ESMTP server";
+            myhostname = clientDomain;
+            mydestination = clientDomain;
+          };
+        };
+
+        environment.systemPackages =
+          let
+            replyToEmail = pkgs.writeScriptBin "reply-to-email" ''
+              #!${pkgs.python3.interpreter}
+              import imaplib
+              import smtplib
+              import ssl
+              import email.header
+              from email import message_from_bytes
+              from email.message import EmailMessage
+
+              with imaplib.IMAP4('localhost') as imap:
+                  imap.login('alice', 'foobar')
+                  imap.select()
+                  status, data = imap.search(None, 'ALL')
+                  assert status == 'OK'
+
+                  nums = data[0].split()
+                  assert len(nums) == 1
+
+                  status, msg_data = imap.fetch(nums[0], '(RFC822)')
+                  assert status == 'OK'
+
+              msg = email.message_from_bytes(msg_data[0][1])
+              subject = str(email.header.make_header(email.header.decode_header(msg['Subject'])))
+              reply_to = email.header.decode_header(msg['Reply-To'])[0][0]
+              message_id = email.header.decode_header(msg['Message-ID'])[0][0]
+              date = email.header.decode_header(msg['Date'])[0][0]
+
+              ctx = ssl.create_default_context()
+              with smtplib.SMTP_SSL(host='${discourseDomain}', context=ctx) as smtp:
+                  reply = EmailMessage()
+                  reply['Subject'] = 'Re: ' + subject
+                  reply['To'] = reply_to
+                  reply['From'] = 'alice@${clientDomain}'
+                  reply['In-Reply-To'] = message_id
+                  reply['References'] = message_id
+                  reply['Date'] = date
+                  reply.set_content("Test reply.")
+
+                  smtp.send_message(reply)
+                  smtp.quit()
+            '';
+          in
+            [ replyToEmail ];
+
+        networking.firewall.allowedTCPPorts = [ 25 ];
+      };
+
+
+    testScript = { nodes }:
+      let
+        request = builtins.toJSON {
+          title = "Private message";
+          raw = "This is a test message.";
+          target_usernames = admin.username;
+          archetype = "private_message";
+        };
+      in ''
+        discourse.start()
+        client.start()
+
+        discourse.wait_for_unit("discourse.service")
+        discourse.wait_for_file("/run/discourse/sockets/unicorn.sock")
+        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}",
+        )
+
+        client.wait_for_unit("postfix.service")
+        client.wait_for_unit("dovecot2.service")
+
+        discourse.succeed(
+            "sudo -u discourse discourse-rake api_key:create_master[master] >api_key",
+            'curl -sS -f https://${discourseDomain}/posts -X POST -H "Content-Type: application/json" -H "Api-Key: $(<api_key)" -H "Api-Username: system" -d \'${request}\' ',
+        )
+
+        client.wait_until_succeeds("reply-to-email")
+
+        discourse.wait_until_succeeds(
+            'curl -sS -f https://${discourseDomain}/topics/private-messages/system -H "Accept: application/json" -H "Api-Key: $(<api_key)" -H "Api-Username: system" | jq -e \'if .topic_list.topics[0].id != null then .topic_list.topics[0].id else null end\' >topic_id'
+        )
+        discourse.succeed(
+            'curl -sS -f https://${discourseDomain}/t/$(<topic_id) -H "Accept: application/json" -H "Api-Key: $(<api_key)" -H "Api-Username: system" | jq -e \'if .post_stream.posts[1].cooked == "<p>Test reply.</p>" then true else null end\' '
+        )
+      '';
+  })
diff --git a/nixos/tests/dnscrypt-proxy2.nix b/nixos/tests/dnscrypt-proxy2.nix
new file mode 100644
index 00000000000..1ba5d983e9b
--- /dev/null
+++ b/nixos/tests/dnscrypt-proxy2.nix
@@ -0,0 +1,36 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "dnscrypt-proxy2";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ joachifm ];
+  };
+
+  nodes = {
+    # A client running the recommended setup: DNSCrypt proxy as a forwarder
+    # for a caching DNS client.
+    client =
+    { ... }:
+    let localProxyPort = 43; in
+    {
+      security.apparmor.enable = true;
+
+      services.dnscrypt-proxy2.enable = true;
+      services.dnscrypt-proxy2.settings = {
+        listen_addresses = [ "127.0.0.1:${toString localProxyPort}" ];
+        sources.public-resolvers = {
+          urls = [ "https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md" ];
+          cache_file = "public-resolvers.md";
+          minisign_key = "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3";
+          refresh_delay = 72;
+        };
+      };
+
+      services.dnsmasq.enable = true;
+      services.dnsmasq.servers = [ "127.0.0.1#${toString localProxyPort}" ];
+    };
+  };
+
+  testScript = ''
+    client.wait_for_unit("dnsmasq")
+    client.wait_for_unit("dnscrypt-proxy2")
+  '';
+})
diff --git a/nixos/tests/dnscrypt-wrapper/default.nix b/nixos/tests/dnscrypt-wrapper/default.nix
new file mode 100644
index 00000000000..1bdd064e113
--- /dev/null
+++ b/nixos/tests/dnscrypt-wrapper/default.nix
@@ -0,0 +1,72 @@
+import ../make-test-python.nix ({ pkgs, ... }: {
+  name = "dnscrypt-wrapper";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ rnhmjoj ];
+  };
+
+  nodes = {
+    server = { lib, ... }:
+      { services.dnscrypt-wrapper = with builtins;
+          { enable = true;
+            address = "192.168.1.1";
+            keys.expiration = 5; # days
+            keys.checkInterval = 2;  # min
+            # The keypair was generated by the command:
+            # dnscrypt-wrapper --gen-provider-keypair \
+            #  --provider-name=2.dnscrypt-cert.server \
+            #  --ext-address=192.168.1.1:5353
+            providerKey.public = toFile "public.key" (readFile ./public.key);
+            providerKey.secret = toFile "secret.key" (readFile ./secret.key);
+          };
+        services.tinydns.enable = true;
+        services.tinydns.data = ''
+          ..:192.168.1.1:a
+          +it.works:1.2.3.4
+        '';
+        networking.firewall.allowedUDPPorts = [ 5353 ];
+        networking.firewall.allowedTCPPorts = [ 5353 ];
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce
+          [ { address = "192.168.1.1"; prefixLength = 24; } ];
+      };
+
+    client = { lib, ... }:
+      { services.dnscrypt-proxy2.enable = true;
+        services.dnscrypt-proxy2.upstreamDefaults = false;
+        services.dnscrypt-proxy2.settings = {
+          server_names = [ "server" ];
+          static.server.stamp = "sdns://AQAAAAAAAAAAEDE5Mi4xNjguMS4xOjUzNTMgFEHYOv0SCKSuqR5CDYa7-58cCBuXO2_5uTSVU9wNQF0WMi5kbnNjcnlwdC1jZXJ0LnNlcnZlcg";
+        };
+        networking.nameservers = [ "127.0.0.1" ];
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce
+          [ { address = "192.168.1.2"; prefixLength = 24; } ];
+      };
+
+  };
+
+  testScript = ''
+    start_all()
+
+    with subtest("The server can generate the ephemeral keypair"):
+        server.wait_for_unit("dnscrypt-wrapper")
+        server.wait_for_file("/var/lib/dnscrypt-wrapper/2.dnscrypt-cert.server.key")
+        server.wait_for_file("/var/lib/dnscrypt-wrapper/2.dnscrypt-cert.server.crt")
+
+    with subtest("The client can connect to the server"):
+        server.wait_for_unit("tinydns")
+        client.wait_for_unit("dnscrypt-proxy2")
+        assert "1.2.3.4" in client.succeed(
+            "host it.works"
+        ), "The IP address of 'it.works' does not match 1.2.3.4"
+
+    with subtest("The server rotates the ephemeral keys"):
+        # advance time by a little less than 5 days
+        server.succeed("date -s \"$(date --date '4 days 6 hours')\"")
+        client.succeed("date -s \"$(date --date '4 days 6 hours')\"")
+        server.wait_for_file("/var/lib/dnscrypt-wrapper/oldkeys")
+
+    with subtest("The client can still connect to the server"):
+        server.wait_for_unit("dnscrypt-wrapper")
+        client.succeed("host it.works")
+  '';
+})
+
diff --git a/nixos/tests/dnscrypt-wrapper/public.key b/nixos/tests/dnscrypt-wrapper/public.key
new file mode 100644
index 00000000000..80232b97f52
--- /dev/null
+++ b/nixos/tests/dnscrypt-wrapper/public.key
@@ -0,0 +1 @@
+AØ:ý¤®©B
†»ûŸ—;où¹4•SÜ
@]
\ No newline at end of file
diff --git a/nixos/tests/dnscrypt-wrapper/secret.key b/nixos/tests/dnscrypt-wrapper/secret.key
new file mode 100644
index 00000000000..01fbf8e08b7
--- /dev/null
+++ b/nixos/tests/dnscrypt-wrapper/secret.key
@@ -0,0 +1 @@
+G½>Æ©» ì>Ðà¥(Ò²‡¼J•«º=Ÿ„ÝÁlìAØ:ý¤®©B
†»ûŸ—;où¹4•SÜ
@]
\ No newline at end of file
diff --git a/nixos/tests/dnsdist.nix b/nixos/tests/dnsdist.nix
new file mode 100644
index 00000000000..cfc41c13864
--- /dev/null
+++ b/nixos/tests/dnsdist.nix
@@ -0,0 +1,48 @@
+import ./make-test-python.nix (
+  { pkgs, ... }: {
+    name = "dnsdist";
+    meta = with pkgs.lib; {
+      maintainers = with maintainers; [ jojosch ];
+    };
+
+    machine = { pkgs, lib, ... }: {
+      services.bind = {
+        enable = true;
+        extraOptions = "empty-zones-enable no;";
+        zones = lib.singleton {
+          name = ".";
+          master = true;
+          file = pkgs.writeText "root.zone" ''
+            $TTL 3600
+            . IN SOA ns.example.org. admin.example.org. ( 1 3h 1h 1w 1d )
+            . IN NS ns.example.org.
+
+            ns.example.org. IN A    192.168.0.1
+            ns.example.org. IN AAAA abcd::1
+
+            1.0.168.192.in-addr.arpa IN PTR ns.example.org.
+          '';
+        };
+      };
+      services.dnsdist = {
+        enable = true;
+        listenPort = 5353;
+        extraConfig = ''
+          newServer({address="127.0.0.1:53", name="local-bind"})
+        '';
+      };
+
+      environment.systemPackages = with pkgs; [ dig ];
+    };
+
+    testScript = ''
+      machine.wait_for_unit("bind.service")
+      machine.wait_for_open_port(53)
+      machine.succeed("dig @127.0.0.1 +short -x 192.168.0.1 | grep -qF ns.example.org")
+
+      machine.wait_for_unit("dnsdist.service")
+      machine.wait_for_open_port(5353)
+      machine.succeed("dig @127.0.0.1 -p 5353 +short -x 192.168.0.1 | grep -qF ns.example.org")
+    '';
+  }
+)
diff --git a/nixos/tests/doas.nix b/nixos/tests/doas.nix
new file mode 100644
index 00000000000..7f038b2bee2
--- /dev/null
+++ b/nixos/tests/doas.nix
@@ -0,0 +1,98 @@
+# Some tests to ensure doas is working properly.
+import ./make-test-python.nix (
+  { lib, ... }: {
+    name = "doas";
+    meta = with lib.maintainers; {
+      maintainers = [ cole-h ];
+    };
+
+    machine =
+      { ... }:
+        {
+          users.groups = { foobar = {}; barfoo = {}; baz = { gid = 1337; }; };
+          users.users = {
+            test0 = { isNormalUser = true; extraGroups = [ "wheel" ]; };
+            test1 = { isNormalUser = true; };
+            test2 = { isNormalUser = true; extraGroups = [ "foobar" ]; };
+            test3 = { isNormalUser = true; extraGroups = [ "barfoo" ]; };
+            test4 = { isNormalUser = true; extraGroups = [ "baz" ]; };
+            test5 = { isNormalUser = true; };
+            test6 = { isNormalUser = true; };
+            test7 = { isNormalUser = true; };
+          };
+
+          security.doas = {
+            enable = true;
+            wheelNeedsPassword = false;
+
+            extraRules = [
+              { users = [ "test1" ]; groups = [ "foobar" ]; }
+              { users = [ "test2" ]; noPass = true; setEnv = [ "CORRECT" "HORSE=BATTERY" ]; }
+              { groups = [ "barfoo" 1337 ]; noPass = true; }
+              { users = [ "test5" ]; noPass = true; keepEnv = true; runAs = "test1"; }
+              { users = [ "test6" ]; noPass = true; keepEnv = true; setEnv = [ "-STAPLE" ]; }
+              { users = [ "test7" ]; noPass = true; setEnv = [ "-SSH_AUTH_SOCK" ]; }
+            ];
+          };
+        };
+
+    testScript = ''
+      with subtest("users in wheel group should have passwordless doas"):
+          machine.succeed('su - test0 -c "doas -u root true"')
+
+      with subtest("test1 user should not be able to use doas without password"):
+          machine.fail('su - test1 -c "doas -n -u root true"')
+
+      with subtest("test2 user should be able to keep some env"):
+          if "CORRECT=1" not in machine.succeed('su - test2 -c "CORRECT=1 doas env"'):
+              raise Exception("failed to keep CORRECT")
+
+          if "HORSE=BATTERY" not in machine.succeed('su - test2 -c "doas env"'):
+              raise Exception("failed to setenv HORSE=BATTERY")
+
+      with subtest("users in group 'barfoo' shouldn't require password"):
+          machine.succeed("doas -u test3 doas -n -u root true")
+
+      with subtest("users in group 'baz' (GID 1337) shouldn't require password"):
+          machine.succeed("doas -u test4 doas -n -u root echo true")
+
+      with subtest("test5 user should be able to run commands under test1"):
+          machine.succeed("doas -u test5 doas -n -u test1 true")
+
+      with subtest("test5 user should not be able to run commands under root"):
+          machine.fail("doas -u test5 doas -n -u root true")
+
+      with subtest("test6 user should be able to keepenv"):
+          envs = ["BATTERY=HORSE", "CORRECT=false"]
+          out = machine.succeed(
+              'su - test6 -c "BATTERY=HORSE CORRECT=false STAPLE=Tr0ub4dor doas env"'
+          )
+
+          if not all(env in out for env in envs):
+              raise Exception("failed to keep BATTERY or CORRECT")
+          if "STAPLE=Tr0ub4dor" in out:
+              raise Exception("failed to exclude STAPLE")
+
+      with subtest("test7 should not have access to SSH_AUTH_SOCK"):
+          if "SSH_AUTH_SOCK=HOLEY" in machine.succeed(
+              'su - test7 -c "SSH_AUTH_SOCK=HOLEY doas env"'
+          ):
+              raise Exception("failed to exclude SSH_AUTH_SOCK")
+
+      # Test that the doas setuid wrapper precedes the unwrapped version in PATH after
+      # calling doas.
+      # The PATH set by doas is defined in
+      # ../../pkgs/tools/security/doas/0001-add-NixOS-specific-dirs-to-safe-PATH.patch
+      with subtest("recursive calls to doas from subprocesses should succeed"):
+          machine.succeed('doas -u test0 sh -c "doas -u test0 true"')
+
+      with subtest("test0 should inherit TERMINFO_DIRS from the user environment"):
+          dirs = machine.succeed(
+               "su - test0 -c 'doas -u root $SHELL -c \"echo \$TERMINFO_DIRS\"'"
+          )
+
+          if not "test0" in dirs:
+             raise Exception(f"user profile TERMINFO_DIRS is not preserved: {dirs}")
+    '';
+  }
+)
diff --git a/nixos/tests/docker-edge.nix b/nixos/tests/docker-edge.nix
new file mode 100644
index 00000000000..c6a1a083018
--- /dev/null
+++ b/nixos/tests/docker-edge.nix
@@ -0,0 +1,49 @@
+# This test runs docker and checks if simple container starts
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "docker";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ nequissimus offline ];
+  };
+
+  nodes = {
+    docker =
+      { pkgs, ... }:
+        {
+          virtualisation.docker.enable = true;
+          virtualisation.docker.package = pkgs.docker-edge;
+
+          users.users = {
+            noprivs = {
+              isNormalUser = true;
+              description = "Can't access the docker daemon";
+              password = "foobar";
+            };
+
+            hasprivs = {
+              isNormalUser = true;
+              description = "Can access the docker daemon";
+              password = "foobar";
+              extraGroups = [ "docker" ];
+            };
+          };
+        };
+    };
+
+  testScript = ''
+    start_all()
+
+    docker.wait_for_unit("sockets.target")
+    docker.succeed("tar cv --files-from /dev/null | docker import - scratchimg")
+    docker.succeed(
+        "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+    )
+    docker.succeed("docker ps | grep sleeping")
+    docker.succeed("sudo -u hasprivs docker ps")
+    docker.fail("sudo -u noprivs docker ps")
+    docker.succeed("docker stop sleeping")
+
+    # Must match version 4 times to ensure client and server git commits and versions are correct
+    docker.succeed('[ $(docker version | grep ${pkgs.docker-edge.version} | wc -l) = "4" ]')
+  '';
+})
diff --git a/nixos/tests/docker-registry.nix b/nixos/tests/docker-registry.nix
new file mode 100644
index 00000000000..1d449db4519
--- /dev/null
+++ b/nixos/tests/docker-registry.nix
@@ -0,0 +1,61 @@
+# This test runs docker-registry and check if it works
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "docker-registry";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ globin ma27 ironpinguin ];
+  };
+
+  nodes = {
+    registry = { ... }: {
+      services.dockerRegistry.enable = true;
+      services.dockerRegistry.enableDelete = true;
+      services.dockerRegistry.port = 8080;
+      services.dockerRegistry.listenAddress = "0.0.0.0";
+      services.dockerRegistry.enableGarbageCollect = true;
+      networking.firewall.allowedTCPPorts = [ 8080 ];
+    };
+
+    client1 = { ... }: {
+      virtualisation.docker.enable = true;
+      virtualisation.docker.extraOptions = "--insecure-registry registry:8080";
+    };
+
+    client2 = { ... }: {
+      virtualisation.docker.enable = true;
+      virtualisation.docker.extraOptions = "--insecure-registry registry:8080";
+    };
+  };
+
+  testScript = ''
+    client1.start()
+    client1.wait_for_unit("docker.service")
+    client1.succeed("tar cv --files-from /dev/null | docker import - scratch")
+    client1.succeed("docker tag scratch registry:8080/scratch")
+
+    registry.start()
+    registry.wait_for_unit("docker-registry.service")
+    registry.wait_for_open_port("8080")
+    client1.succeed("docker push registry:8080/scratch")
+
+    client2.start()
+    client2.wait_for_unit("docker.service")
+    client2.succeed("docker pull registry:8080/scratch")
+    client2.succeed("docker images | grep scratch")
+
+    client2.succeed(
+        "curl -fsS -X DELETE registry:8080/v2/scratch/manifests/$(curl -fsS -I -H\"Accept: application/vnd.docker.distribution.manifest.v2+json\" registry:8080/v2/scratch/manifests/latest | grep Docker-Content-Digest | sed -e 's/Docker-Content-Digest: //' | tr -d '\\r')"
+    )
+
+    registry.systemctl("start docker-registry-garbage-collect.service")
+    registry.wait_until_fails("systemctl status docker-registry-garbage-collect.service")
+    registry.wait_for_unit("docker-registry.service")
+
+    registry.fail("ls -l /var/lib/docker-registry/docker/registry/v2/blobs/sha256/*/*/data")
+
+    client1.succeed("docker push registry:8080/scratch")
+    registry.succeed(
+        "ls -l /var/lib/docker-registry/docker/registry/v2/blobs/sha256/*/*/data"
+    )
+  '';
+})
diff --git a/nixos/tests/docker-rootless.nix b/nixos/tests/docker-rootless.nix
new file mode 100644
index 00000000000..e2a926eb3cb
--- /dev/null
+++ b/nixos/tests/docker-rootless.nix
@@ -0,0 +1,41 @@
+# This test runs docker and checks if simple container starts
+
+import ./make-test-python.nix ({ lib, pkgs, ...} : {
+  name = "docker-rootless";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ abbradar ];
+  };
+
+  nodes = {
+    machine = { pkgs, ... }: {
+      virtualisation.docker.rootless.enable = true;
+
+      users.users.alice = {
+        uid = 1000;
+        isNormalUser = true;
+      };
+    };
+  };
+
+  testScript = { nodes, ... }:
+    let
+      user = nodes.machine.config.users.users.alice;
+      sudo = lib.concatStringsSep " " [
+        "XDG_RUNTIME_DIR=/run/user/${toString user.uid}"
+        "DOCKER_HOST=unix:///run/user/${toString user.uid}/docker.sock"
+        "sudo" "--preserve-env=XDG_RUNTIME_DIR,DOCKER_HOST" "-u" "alice"
+      ];
+    in ''
+      machine.wait_for_unit("multi-user.target")
+
+      machine.succeed("loginctl enable-linger alice")
+      machine.wait_until_succeeds("${sudo} systemctl --user is-active docker.service")
+
+      machine.succeed("tar cv --files-from /dev/null | ${sudo} docker import - scratchimg")
+      machine.succeed(
+          "${sudo} docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+      )
+      machine.succeed("${sudo} docker ps | grep sleeping")
+      machine.succeed("${sudo} docker stop sleeping")
+    '';
+})
diff --git a/nixos/tests/docker-tools-cross.nix b/nixos/tests/docker-tools-cross.nix
new file mode 100644
index 00000000000..a7a6a31475d
--- /dev/null
+++ b/nixos/tests/docker-tools-cross.nix
@@ -0,0 +1,76 @@
+# Not everyone has a suitable remote builder set up, so the cross-compilation
+# tests that _include_ running the result are separate. That way, most people
+# can run the majority of the test suite without the extra setup.
+
+
+import ./make-test-python.nix ({ pkgs, ... }:
+let
+
+  remoteSystem =
+    if pkgs.system == "aarch64-linux"
+    then "x86_64-linux"
+    else "aarch64-linux";
+
+  remoteCrossPkgs = import ../.. /*nixpkgs*/ {
+    # NOTE: This is the machine that runs the build -  local from the
+    #       'perspective' of the build script.
+    localSystem = remoteSystem;
+
+    # NOTE: Since this file can't control where the test will be _run_ we don't
+    #       cross-compile _to_ a different system but _from_ a different system
+    crossSystem = pkgs.system;
+  };
+
+  hello1 = remoteCrossPkgs.dockerTools.buildImage {
+    name = "hello1";
+    tag = "latest";
+    contents = remoteCrossPkgs.hello;
+  };
+
+  hello2 = remoteCrossPkgs.dockerTools.buildLayeredImage {
+    name = "hello2";
+    tag = "latest";
+    contents = remoteCrossPkgs.hello;
+  };
+
+in {
+  name = "docker-tools";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ roberth ];
+  };
+
+  nodes = {
+    docker = { ... }: {
+      virtualisation = {
+        diskSize = 2048;
+        docker.enable = true;
+      };
+    };
+  };
+
+  testScript = ''
+    docker.wait_for_unit("sockets.target")
+
+    with subtest("Ensure cross compiled buildImage image can run."):
+        docker.succeed(
+            "docker load --input='${hello1}'"
+        )
+        assert "Hello, world!" in docker.succeed(
+            "docker run --rm ${hello1.imageName} hello",
+        )
+        docker.succeed(
+            "docker rmi ${hello1.imageName}",
+        )
+
+    with subtest("Ensure cross compiled buildLayeredImage image can run."):
+        docker.succeed(
+            "docker load --input='${hello2}'"
+        )
+        assert "Hello, world!" in docker.succeed(
+            "docker run --rm ${hello2.imageName} hello",
+        )
+        docker.succeed(
+            "docker rmi ${hello2.imageName}",
+        )
+  '';
+})
diff --git a/nixos/tests/docker-tools-overlay.nix b/nixos/tests/docker-tools-overlay.nix
new file mode 100644
index 00000000000..6781388e639
--- /dev/null
+++ b/nixos/tests/docker-tools-overlay.nix
@@ -0,0 +1,33 @@
+# this test creates a simple GNU image with docker tools and sees if it executes
+
+import ./make-test-python.nix ({ pkgs, ... }:
+{
+  name = "docker-tools-overlay";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lnl7 roberth ];
+  };
+
+  nodes = {
+    docker =
+      { ... }:
+      {
+        virtualisation.docker.enable = true;
+        virtualisation.docker.storageDriver = "overlay";  # defaults to overlay2
+      };
+  };
+
+  testScript = ''
+      docker.wait_for_unit("sockets.target")
+
+      docker.succeed(
+          "docker load --input='${pkgs.dockerTools.examples.bash}'",
+          "docker run --rm ${pkgs.dockerTools.examples.bash.imageName} bash --version",
+      )
+
+      # Check if the nix store has correct user permissions depending on what
+      # storage driver is used, incorrectly built images can show up as readonly.
+      # drw-------  3 0 0   3 Apr 14 11:36 /nix
+      # drw------- 99 0 0 100 Apr 14 11:36 /nix/store
+      docker.succeed("docker run --rm -u 1000:1000 ${pkgs.dockerTools.examples.bash.imageName} bash --version")
+    '';
+})
diff --git a/nixos/tests/docker-tools.nix b/nixos/tests/docker-tools.nix
new file mode 100644
index 00000000000..8a240ddb17f
--- /dev/null
+++ b/nixos/tests/docker-tools.nix
@@ -0,0 +1,423 @@
+# this test creates a simple GNU image with docker tools and sees if it executes
+
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "docker-tools";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lnl7 roberth ];
+  };
+
+  nodes = {
+    docker = { ... }: {
+      virtualisation = {
+        diskSize = 2048;
+        docker.enable = true;
+      };
+    };
+  };
+
+  testScript = with pkgs.dockerTools; ''
+    unix_time_second1 = "1970-01-01T00:00:01Z"
+
+    docker.wait_for_unit("sockets.target")
+
+    with subtest("includeStorePath"):
+        with subtest("assumption"):
+            docker.succeed("${examples.helloOnRoot} | docker load")
+            docker.succeed("docker run --rm hello | grep -i hello")
+            docker.succeed("docker image rm hello:latest")
+        with subtest("includeStorePath = false; breaks example"):
+            docker.succeed("${examples.helloOnRootNoStore} | docker load")
+            docker.fail("docker run --rm hello | grep -i hello")
+            docker.succeed("docker image rm hello:latest")
+        with subtest("includeStorePath = false; works with mounted store"):
+            docker.succeed("${examples.helloOnRootNoStore} | docker load")
+            docker.succeed("docker run --rm --volume ${builtins.storeDir}:${builtins.storeDir}:ro hello | grep -i hello")
+            docker.succeed("docker image rm hello:latest")
+
+    with subtest("Ensure Docker images use a stable date by default"):
+        docker.succeed(
+            "docker load --input='${examples.bash}'"
+        )
+        assert unix_time_second1 in docker.succeed(
+            "docker inspect ${examples.bash.imageName} "
+            + "| ${pkgs.jq}/bin/jq -r .[].Created",
+        )
+
+    docker.succeed("docker run --rm ${examples.bash.imageName} bash --version")
+    # Check imageTag attribute matches image
+    docker.succeed("docker images --format '{{.Tag}}' | grep -F '${examples.bash.imageTag}'")
+    docker.succeed("docker rmi ${examples.bash.imageName}")
+
+    # The remaining combinations
+    with subtest("Ensure imageTag attribute matches image"):
+        docker.succeed(
+            "docker load --input='${examples.bashNoTag}'"
+        )
+        docker.succeed(
+            "docker images --format '{{.Tag}}' | grep -F '${examples.bashNoTag.imageTag}'"
+        )
+        docker.succeed("docker rmi ${examples.bashNoTag.imageName}:${examples.bashNoTag.imageTag}")
+
+        docker.succeed(
+            "docker load --input='${examples.bashNoTagLayered}'"
+        )
+        docker.succeed(
+            "docker images --format '{{.Tag}}' | grep -F '${examples.bashNoTagLayered.imageTag}'"
+        )
+        docker.succeed("docker rmi ${examples.bashNoTagLayered.imageName}:${examples.bashNoTagLayered.imageTag}")
+
+        docker.succeed(
+            "${examples.bashNoTagStreamLayered} | docker load"
+        )
+        docker.succeed(
+            "docker images --format '{{.Tag}}' | grep -F '${examples.bashNoTagStreamLayered.imageTag}'"
+        )
+        docker.succeed(
+            "docker rmi ${examples.bashNoTagStreamLayered.imageName}:${examples.bashNoTagStreamLayered.imageTag}"
+        )
+
+        docker.succeed(
+            "docker load --input='${examples.nixLayered}'"
+        )
+        docker.succeed("docker images --format '{{.Tag}}' | grep -F '${examples.nixLayered.imageTag}'")
+        docker.succeed("docker rmi ${examples.nixLayered.imageName}")
+
+
+    with subtest(
+        "Check if the nix store is correctly initialized by listing "
+        "dependencies of the installed Nix binary"
+    ):
+        docker.succeed(
+            "docker load --input='${examples.nix}'",
+            "docker run --rm ${examples.nix.imageName} nix-store -qR ${pkgs.nix}",
+            "docker rmi ${examples.nix.imageName}",
+        )
+
+    with subtest(
+        "Ensure (layered) nix store has correct permissions "
+        "and that the container starts when its process does not have uid 0"
+    ):
+        docker.succeed(
+            "docker load --input='${examples.bashLayeredWithUser}'",
+            "docker run -u somebody --rm ${examples.bashLayeredWithUser.imageName} ${pkgs.bash}/bin/bash -c 'test 555 == $(stat --format=%a /nix) && test 555 == $(stat --format=%a /nix/store)'",
+            "docker rmi ${examples.bashLayeredWithUser.imageName}",
+        )
+
+    with subtest("The nix binary symlinks are intact"):
+        docker.succeed(
+            "docker load --input='${examples.nix}'",
+            "docker run --rm ${examples.nix.imageName} ${pkgs.bash}/bin/bash -c 'test nix == $(readlink ${pkgs.nix}/bin/nix-daemon)'",
+            "docker rmi ${examples.nix.imageName}",
+        )
+
+    with subtest("The nix binary symlinks are intact when the image is layered"):
+        docker.succeed(
+            "docker load --input='${examples.nixLayered}'",
+            "docker run --rm ${examples.nixLayered.imageName} ${pkgs.bash}/bin/bash -c 'test nix == $(readlink ${pkgs.nix}/bin/nix-daemon)'",
+            "docker rmi ${examples.nixLayered.imageName}",
+        )
+
+    with subtest("The pullImage tool works"):
+        docker.succeed(
+            "docker load --input='${examples.testNixFromDockerHub}'",
+            "docker run --rm nix:2.2.1 nix-store --version",
+            "docker rmi nix:2.2.1",
+        )
+
+    with subtest("runAsRoot and entry point work"):
+        docker.succeed(
+            "docker load --input='${examples.nginx}'",
+            "docker run --name nginx -d -p 8000:80 ${examples.nginx.imageName}",
+        )
+        docker.wait_until_succeeds("curl -f http://localhost:8000/")
+        docker.succeed(
+            "docker rm --force nginx",
+            "docker rmi '${examples.nginx.imageName}'",
+        )
+
+    with subtest("A pulled image can be used as base image"):
+        docker.succeed(
+            "docker load --input='${examples.onTopOfPulledImage}'",
+            "docker run --rm ontopofpulledimage hello",
+            "docker rmi ontopofpulledimage",
+        )
+
+    with subtest("Regression test for issue #34779"):
+        docker.succeed(
+            "docker load --input='${examples.runAsRootExtraCommands}'",
+            "docker run --rm runasrootextracommands cat extraCommands",
+            "docker run --rm runasrootextracommands cat runAsRoot",
+            "docker rmi '${examples.runAsRootExtraCommands.imageName}'",
+        )
+
+    with subtest("Ensure Docker images can use an unstable date"):
+        docker.succeed(
+            "docker load --input='${examples.unstableDate}'"
+        )
+        assert unix_time_second1 not in docker.succeed(
+            "docker inspect ${examples.unstableDate.imageName} "
+            + "| ${pkgs.jq}/bin/jq -r .[].Created"
+        )
+
+    with subtest("Ensure Layered Docker images can use an unstable date"):
+        docker.succeed(
+            "docker load --input='${examples.unstableDateLayered}'"
+        )
+        assert unix_time_second1 not in docker.succeed(
+            "docker inspect ${examples.unstableDateLayered.imageName} "
+            + "| ${pkgs.jq}/bin/jq -r .[].Created"
+        )
+
+    with subtest("Ensure Layered Docker images work"):
+        docker.succeed(
+            "docker load --input='${examples.layered-image}'",
+            "docker run --rm ${examples.layered-image.imageName}",
+            "docker run --rm ${examples.layered-image.imageName} cat extraCommands",
+        )
+
+    with subtest("Ensure images built on top of layered Docker images work"):
+        docker.succeed(
+            "docker load --input='${examples.layered-on-top}'",
+            "docker run --rm ${examples.layered-on-top.imageName}",
+        )
+
+    with subtest("Ensure layered images built on top of layered Docker images work"):
+        docker.succeed(
+            "docker load --input='${examples.layered-on-top-layered}'",
+            "docker run --rm ${examples.layered-on-top-layered.imageName}",
+        )
+
+
+    def set_of_layers(image_name):
+        return set(
+            docker.succeed(
+                f"docker inspect {image_name} "
+                + "| ${pkgs.jq}/bin/jq -r '.[] | .RootFS.Layers | .[]'"
+            ).split()
+        )
+
+
+    with subtest("Ensure layers are shared between images"):
+        docker.succeed(
+            "docker load --input='${examples.another-layered-image}'"
+        )
+        layers1 = set_of_layers("${examples.layered-image.imageName}")
+        layers2 = set_of_layers("${examples.another-layered-image.imageName}")
+        assert bool(layers1 & layers2)
+
+    with subtest("Ensure order of layers is correct"):
+        docker.succeed(
+            "docker load --input='${examples.layersOrder}'"
+        )
+
+        for index in 1, 2, 3:
+            assert f"layer{index}" in docker.succeed(
+                f"docker run --rm  ${examples.layersOrder.imageName} cat /tmp/layer{index}"
+            )
+
+    with subtest("Ensure layers unpacked in correct order before runAsRoot runs"):
+        assert "abc" in docker.succeed(
+            "docker load --input='${examples.layersUnpackOrder}'",
+            "docker run --rm ${examples.layersUnpackOrder.imageName} cat /layer-order"
+        )
+
+    with subtest("Ensure environment variables are correctly inherited"):
+        docker.succeed(
+            "docker load --input='${examples.environmentVariables}'"
+        )
+        out = docker.succeed("docker run --rm ${examples.environmentVariables.imageName} env")
+        env = out.splitlines()
+        assert "FROM_PARENT=true" in env, "envvars from the parent should be preserved"
+        assert "FROM_CHILD=true" in env, "envvars from the child should be preserved"
+        assert "LAST_LAYER=child" in env, "envvars from the child should take priority"
+
+    with subtest("Ensure environment variables of layered images are correctly inherited"):
+        docker.succeed(
+            "docker load --input='${examples.environmentVariablesLayered}'"
+        )
+        out = docker.succeed("docker run --rm ${examples.environmentVariablesLayered.imageName} env")
+        env = out.splitlines()
+        assert "FROM_PARENT=true" in env, "envvars from the parent should be preserved"
+        assert "FROM_CHILD=true" in env, "envvars from the child should be preserved"
+        assert "LAST_LAYER=child" in env, "envvars from the child should take priority"
+
+    with subtest(
+        "Ensure inherited environment variables of layered images are correctly resolved"
+    ):
+        # Read environment variables as stored in image config
+        config = docker.succeed(
+            "tar -xOf ${examples.environmentVariablesLayered} manifest.json | ${pkgs.jq}/bin/jq -r .[].Config"
+        ).strip()
+        out = docker.succeed(
+            f"tar -xOf ${examples.environmentVariablesLayered} {config} | ${pkgs.jq}/bin/jq -r '.config.Env | .[]'"
+        )
+        env = out.splitlines()
+        assert (
+            sum(entry.startswith("LAST_LAYER") for entry in env) == 1
+        ), "envvars overridden by child should be unique"
+
+    with subtest("Ensure image with only 2 layers can be loaded"):
+        docker.succeed(
+            "docker load --input='${examples.two-layered-image}'"
+        )
+
+    with subtest(
+        "Ensure the bulk layer doesn't miss store paths (regression test for #78744)"
+    ):
+        docker.succeed(
+            "docker load --input='${pkgs.dockerTools.examples.bulk-layer}'",
+            # Ensure the two output paths (ls and hello) are in the layer
+            "docker run bulk-layer ls /bin/hello",
+        )
+
+    with subtest(
+        "Ensure the bulk layer with a base image respects the number of maxLayers"
+    ):
+        docker.succeed(
+            "docker load --input='${pkgs.dockerTools.examples.layered-bulk-layer}'",
+            # Ensure the image runs correctly
+            "docker run layered-bulk-layer ls /bin/hello",
+        )
+
+        # Ensure the image has the correct number of layers
+        assert len(set_of_layers("layered-bulk-layer")) == 4
+
+    with subtest("Ensure only minimal paths are added to the store"):
+        # TODO: make an example that has no store paths, for example by making
+        #       busybox non-self-referential.
+
+        # This check tests that buildLayeredImage can build images that don't need a store.
+        docker.succeed(
+            "docker load --input='${pkgs.dockerTools.examples.no-store-paths}'"
+        )
+
+        docker.succeed("docker run --rm no-store-paths ls / >/dev/console")
+
+        # If busybox isn't self-referential, we need this line
+        #   docker.fail("docker run --rm no-store-paths ls /nix/store >/dev/console")
+        # However, it currently is self-referential, so we check that it is the
+        # only store path.
+        docker.succeed("diff <(docker run --rm no-store-paths ls /nix/store) <(basename ${pkgs.pkgsStatic.busybox}) >/dev/console")
+
+    with subtest("Ensure buildLayeredImage does not change store path contents."):
+        docker.succeed(
+            "docker load --input='${pkgs.dockerTools.examples.filesInStore}'",
+            "docker run --rm file-in-store nix-store --verify --check-contents",
+            "docker run --rm file-in-store |& grep 'some data'",
+        )
+
+    with subtest("Ensure cross compiled image can be loaded and has correct arch."):
+        docker.succeed(
+            "docker load --input='${pkgs.dockerTools.examples.cross}'",
+        )
+        assert (
+            docker.succeed(
+                "docker inspect ${pkgs.dockerTools.examples.cross.imageName} "
+                + "| ${pkgs.jq}/bin/jq -r .[].Architecture"
+            ).strip()
+            == "${if pkgs.system == "aarch64-linux" then "amd64" else "arm64"}"
+        )
+
+    with subtest("buildLayeredImage doesn't dereference /nix/store symlink layers"):
+        docker.succeed(
+            "docker load --input='${examples.layeredStoreSymlink}'",
+            "docker run --rm ${examples.layeredStoreSymlink.imageName} bash -c 'test -L ${examples.layeredStoreSymlink.passthru.symlink}'",
+            "docker rmi ${examples.layeredStoreSymlink.imageName}",
+        )
+
+    with subtest("buildImage supports registry/ prefix in image name"):
+        docker.succeed(
+            "docker load --input='${examples.prefixedImage}'"
+        )
+        docker.succeed(
+            "docker images --format '{{.Repository}}' | grep -F '${examples.prefixedImage.imageName}'"
+        )
+
+    with subtest("buildLayeredImage supports registry/ prefix in image name"):
+        docker.succeed(
+            "docker load --input='${examples.prefixedLayeredImage}'"
+        )
+        docker.succeed(
+            "docker images --format '{{.Repository}}' | grep -F '${examples.prefixedLayeredImage.imageName}'"
+        )
+
+    with subtest("buildLayeredImage supports running chown with fakeRootCommands"):
+        docker.succeed(
+            "docker load --input='${examples.layeredImageWithFakeRootCommands}'"
+        )
+        docker.succeed(
+            "docker run --rm ${examples.layeredImageWithFakeRootCommands.imageName} sh -c 'stat -c '%u' /home/jane | grep -E ^1000$'"
+        )
+
+    with subtest("Ensure docker load on merged images loads all of the constituent images"):
+        docker.succeed(
+            "docker load --input='${examples.mergedBashAndRedis}'"
+        )
+        docker.succeed(
+            "docker images --format '{{.Repository}}-{{.Tag}}' | grep -F '${examples.bash.imageName}-${examples.bash.imageTag}'"
+        )
+        docker.succeed(
+            "docker images --format '{{.Repository}}-{{.Tag}}' | grep -F '${examples.redis.imageName}-${examples.redis.imageTag}'"
+        )
+        docker.succeed("docker run --rm ${examples.bash.imageName} bash --version")
+        docker.succeed("docker run --rm ${examples.redis.imageName} redis-cli --version")
+        docker.succeed("docker rmi ${examples.bash.imageName}")
+        docker.succeed("docker rmi ${examples.redis.imageName}")
+
+    with subtest(
+        "Ensure docker load on merged images loads all of the constituent images (missing tags)"
+    ):
+        docker.succeed(
+            "docker load --input='${examples.mergedBashNoTagAndRedis}'"
+        )
+        docker.succeed(
+            "docker images --format '{{.Repository}}-{{.Tag}}' | grep -F '${examples.bashNoTag.imageName}-${examples.bashNoTag.imageTag}'"
+        )
+        docker.succeed(
+            "docker images --format '{{.Repository}}-{{.Tag}}' | grep -F '${examples.redis.imageName}-${examples.redis.imageTag}'"
+        )
+        # we need to explicitly specify the generated tag here
+        docker.succeed(
+            "docker run --rm ${examples.bashNoTag.imageName}:${examples.bashNoTag.imageTag} bash --version"
+        )
+        docker.succeed("docker run --rm ${examples.redis.imageName} redis-cli --version")
+        docker.succeed("docker rmi ${examples.bashNoTag.imageName}:${examples.bashNoTag.imageTag}")
+        docker.succeed("docker rmi ${examples.redis.imageName}")
+
+    with subtest("mergeImages preserves owners of the original images"):
+        docker.succeed(
+            "docker load --input='${examples.mergedBashFakeRoot}'"
+        )
+        docker.succeed(
+            "docker run --rm ${examples.layeredImageWithFakeRootCommands.imageName} sh -c 'stat -c '%u' /home/jane | grep -E ^1000$'"
+        )
+
+    with subtest("The image contains store paths referenced by the fakeRootCommands output"):
+        docker.succeed(
+            "docker run --rm ${examples.layeredImageWithFakeRootCommands.imageName} /hello/bin/layeredImageWithFakeRootCommands-hello"
+        )
+
+    with subtest("exportImage produces a valid tarball"):
+        docker.succeed(
+            "tar -tf ${examples.exportBash} | grep '\./bin/bash' > /dev/null"
+        )
+
+    with subtest("layered image fakeRootCommands with fakechroot works"):
+        docker.succeed("${examples.imageViaFakeChroot} | docker load")
+        docker.succeed("docker run --rm image-via-fake-chroot | grep -i hello")
+        docker.succeed("docker image rm image-via-fake-chroot:latest")
+
+    with subtest("Ensure bare paths in contents are loaded correctly"):
+        docker.succeed(
+            "docker load --input='${examples.build-image-with-path}'",
+            "docker run --rm build-image-with-path bash -c '[[ -e /hello.txt ]]'",
+            "docker rmi build-image-with-path",
+        )
+        docker.succeed(
+            "${examples.layered-image-with-path} | docker load",
+            "docker run --rm layered-image-with-path bash -c '[[ -e /hello.txt ]]'",
+            "docker rmi layered-image-with-path",
+        )
+
+  '';
+})
diff --git a/nixos/tests/docker.nix b/nixos/tests/docker.nix
new file mode 100644
index 00000000000..dee7480eb4a
--- /dev/null
+++ b/nixos/tests/docker.nix
@@ -0,0 +1,52 @@
+# This test runs docker and checks if simple container starts
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "docker";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ nequissimus offline ];
+  };
+
+  nodes = {
+    docker =
+      { pkgs, ... }:
+        {
+          virtualisation.docker.enable = true;
+          virtualisation.docker.package = pkgs.docker;
+
+          users.users = {
+            noprivs = {
+              isNormalUser = true;
+              description = "Can't access the docker daemon";
+              password = "foobar";
+            };
+
+            hasprivs = {
+              isNormalUser = true;
+              description = "Can access the docker daemon";
+              password = "foobar";
+              extraGroups = [ "docker" ];
+            };
+          };
+        };
+    };
+
+  testScript = ''
+    start_all()
+
+    docker.wait_for_unit("sockets.target")
+    docker.succeed("tar cv --files-from /dev/null | docker import - scratchimg")
+    docker.succeed(
+        "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+    )
+    docker.succeed("docker ps | grep sleeping")
+    docker.succeed("sudo -u hasprivs docker ps")
+    docker.fail("sudo -u noprivs docker ps")
+    docker.succeed("docker stop sleeping")
+
+    # Must match version 4 times to ensure client and server git commits and versions are correct
+    docker.succeed('[ $(docker version | grep ${pkgs.docker.version} | wc -l) = "4" ]')
+    docker.succeed("systemctl restart systemd-sysctl")
+    docker.succeed("grep 1 /proc/sys/net/ipv4/conf/all/forwarding")
+    docker.succeed("grep 1 /proc/sys/net/ipv4/conf/default/forwarding")
+  '';
+})
diff --git a/nixos/tests/documize.nix b/nixos/tests/documize.nix
new file mode 100644
index 00000000000..d5a77ffcd4f
--- /dev/null
+++ b/nixos/tests/documize.nix
@@ -0,0 +1,62 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} : {
+  name = "documize";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ma27 ];
+  };
+
+  machine = { pkgs, ... }: {
+    environment.systemPackages = [ pkgs.jq ];
+
+    services.documize = {
+      enable = true;
+      port = 3000;
+      dbtype = "postgresql";
+      db = "host=localhost port=5432 sslmode=disable user=documize password=documize dbname=documize";
+    };
+
+    systemd.services.documize-server = {
+      after = [ "postgresql.service" ];
+      requires = [ "postgresql.service" ];
+    };
+
+    services.postgresql = {
+      enable = true;
+      initialScript = pkgs.writeText "psql-init" ''
+        CREATE ROLE documize WITH LOGIN PASSWORD 'documize';
+        CREATE DATABASE documize WITH OWNER documize;
+      '';
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("documize-server.service")
+    machine.wait_for_open_port(3000)
+
+    dbhash = machine.succeed(
+        "curl -f localhost:3000 | grep 'property=\"dbhash' | grep -Po 'content=\"\\K[^\"]*'"
+    )
+
+    dbhash = dbhash.strip()
+
+    machine.succeed(
+        (
+            "curl -X POST"
+            " --data 'dbname=documize'"
+            " --data 'dbhash={}'"
+            " --data 'title=NixOS'"
+            " --data 'message=Docs'"
+            " --data 'firstname=John'"
+            " --data 'lastname=Doe'"
+            " --data 'email=john.doe@nixos.org'"
+            " --data 'password=verysafe'"
+            " -f localhost:3000/api/setup"
+        ).format(dbhash)
+    )
+
+    machine.succeed(
+        'test "$(curl -f localhost:3000/api/public/meta | jq ".title" | xargs echo)" = "NixOS"'
+    )
+  '';
+})
diff --git a/nixos/tests/doh-proxy-rust.nix b/nixos/tests/doh-proxy-rust.nix
new file mode 100644
index 00000000000..11ed87d23bb
--- /dev/null
+++ b/nixos/tests/doh-proxy-rust.nix
@@ -0,0 +1,43 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "doh-proxy-rust";
+  meta = with lib.maintainers; {
+    maintainers = [ stephank ];
+  };
+
+  nodes = {
+    machine = { pkgs, lib, ... }: {
+      services.bind = {
+        enable = true;
+        extraOptions = "empty-zones-enable no;";
+        zones = lib.singleton {
+          name = ".";
+          master = true;
+          file = pkgs.writeText "root.zone" ''
+            $TTL 3600
+            . IN SOA ns.example.org. admin.example.org. ( 1 3h 1h 1w 1d )
+            . IN NS ns.example.org.
+            ns.example.org. IN A    192.168.0.1
+          '';
+        };
+      };
+      services.doh-proxy-rust = {
+        enable = true;
+        flags = [
+          "--server-address=127.0.0.1:53"
+        ];
+      };
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+    url = "http://localhost:3000/dns-query"
+    query = "AAABAAABAAAAAAAAAm5zB2V4YW1wbGUDb3JnAAABAAE="  # IN A ns.example.org.
+    bin_ip = r"$'\xC0\xA8\x00\x01'"  # 192.168.0.1, as shell binary string
+
+    machine.wait_for_unit("bind.service")
+    machine.wait_for_unit("doh-proxy-rust.service")
+    machine.wait_for_open_port(53)
+    machine.wait_for_open_port(3000)
+    machine.succeed(f"curl --fail -H 'Accept: application/dns-message' '{url}?dns={query}' | grep -F {bin_ip}")
+  '';
+})
diff --git a/nixos/tests/dokuwiki.nix b/nixos/tests/dokuwiki.nix
new file mode 100644
index 00000000000..67657e89f74
--- /dev/null
+++ b/nixos/tests/dokuwiki.nix
@@ -0,0 +1,111 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+  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
+    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 {
+    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";
+    };
+    # 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/";
+  };
+
+in {
+  name = "dokuwiki";
+  meta = with pkgs.lib; {
+    maintainers = with maintainers; [
+      _1000101
+      onny
+    ];
+  };
+
+  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" ];
+    };
+
+  };
+
+  testScript = ''
+
+    start_all()
+
+    dokuwiki_nginx.wait_for_unit("nginx")
+    dokuwiki_caddy.wait_for_unit("caddy")
+
+    site_names = ["site1.local", "site2.local"]
+
+    for machine in (dokuwiki_nginx, dokuwiki_caddy):
+      for site_name in site_names:
+        machine.wait_for_unit(f"phpfpm-dokuwiki-{site_name}")
+
+        machine.succeed("curl -sSfL http://site1.local/ | grep 'DokuWiki'")
+        machine.fail("curl -sSfL 'http://site1.local/doku.php?do=login' | grep 'Login'")
+
+        machine.succeed("curl -sSfL http://site2.local/ | grep 'DokuWiki'")
+        machine.succeed("curl -sSfL 'http://site2.local/doku.php?do=login' | grep 'Login'")
+
+        machine.succeed(
+            "echo 'admin:$2y$10$ijdBQMzSVV20SrKtCna8gue36vnsbVm2wItAXvdm876sshI4uwy6S:Admin:admin@example.test:user' >> /var/lib/dokuwiki/site2.local/users.auth.php",
+            "curl -sSfL -d 'u=admin&p=password' --cookie-jar cjar 'http://site2.local/doku.php?do=login'",
+            "curl -sSfL --cookie cjar --cookie-jar cjar 'http://site2.local/doku.php?do=login' | grep 'Logged in as: <bdi>Admin</bdi>'",
+        )
+  '';
+})
diff --git a/nixos/tests/domination.nix b/nixos/tests/domination.nix
new file mode 100644
index 00000000000..c76d4ed8c61
--- /dev/null
+++ b/nixos/tests/domination.nix
@@ -0,0 +1,26 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "domination";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fgaz ];
+  };
+
+  machine = { config, pkgs, ... }: {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    environment.systemPackages = [ pkgs.domination ];
+  };
+
+  enableOCR = true;
+
+  testScript =
+    ''
+      machine.wait_for_x()
+      machine.execute("domination >&2 &")
+      machine.wait_for_window("Menu")
+      machine.wait_for_text("New Game")
+      machine.screenshot("screen")
+    '';
+})
diff --git a/nixos/tests/dovecot.nix b/nixos/tests/dovecot.nix
new file mode 100644
index 00000000000..8913c2a6a7e
--- /dev/null
+++ b/nixos/tests/dovecot.nix
@@ -0,0 +1,82 @@
+import ./make-test-python.nix {
+  name = "dovecot";
+
+  machine = { pkgs, ... }: {
+    imports = [ common/user-account.nix ];
+    services.postfix.enable = true;
+    services.dovecot2 = {
+      enable = true;
+      protocols = [ "imap" "pop3" ];
+      modules = [ pkgs.dovecot_pigeonhole ];
+      mailUser = "vmail";
+      mailGroup = "vmail";
+    };
+    environment.systemPackages = let
+      sendTestMail = pkgs.writeScriptBin "send-testmail" ''
+        #!${pkgs.runtimeShell}
+        exec sendmail -vt <<MAIL
+        From: root@localhost
+        To: alice@localhost
+        Subject: Very important!
+
+        Hello world!
+        MAIL
+      '';
+
+      sendTestMailViaDeliveryAgent = pkgs.writeScriptBin "send-lda" ''
+        #!${pkgs.runtimeShell}
+
+        exec ${pkgs.dovecot}/libexec/dovecot/deliver -d bob <<MAIL
+        From: root@localhost
+        To: bob@localhost
+        Subject: Something else...
+
+        I'm running short of ideas!
+        MAIL
+      '';
+
+      testImap = pkgs.writeScriptBin "test-imap" ''
+        #!${pkgs.python3.interpreter}
+        import imaplib
+
+        with imaplib.IMAP4('localhost') as imap:
+          imap.login('alice', 'foobar')
+          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!'
+      '';
+
+      testPop = pkgs.writeScriptBin "test-pop" ''
+        #!${pkgs.python3.interpreter}
+        import poplib
+
+        pop = poplib.POP3('localhost')
+        try:
+          pop.user('bob')
+          pop.pass_('foobar')
+          assert len(pop.list()[1]) == 1
+          status, fullmail, size = pop.retr(1)
+          assert status.startswith(b'+OK ')
+          body = b"".join(fullmail[fullmail.index(b""):]).strip()
+          assert body == b"I'm running short of ideas!"
+        finally:
+          pop.quit()
+      '';
+
+    in [ sendTestMail sendTestMailViaDeliveryAgent testImap testPop ];
+  };
+
+  testScript = ''
+    machine.wait_for_unit("postfix.service")
+    machine.wait_for_unit("dovecot2.service")
+    machine.succeed("send-testmail")
+    machine.succeed("send-lda")
+    machine.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
+    machine.succeed("test-imap")
+    machine.succeed("test-pop")
+  '';
+}
diff --git a/nixos/tests/drbd.nix b/nixos/tests/drbd.nix
new file mode 100644
index 00000000000..bede7206d70
--- /dev/null
+++ b/nixos/tests/drbd.nix
@@ -0,0 +1,87 @@
+import ./make-test-python.nix (
+  { pkgs, lib, ... }:
+  let
+    drbdPort = 7789;
+
+    drbdConfig =
+      { nodes, ... }:
+      {
+        virtualisation.emptyDiskImages = [ 1 ];
+        networking.firewall.allowedTCPPorts = [ drbdPort ];
+
+        services.drbd = {
+          enable = true;
+          config = ''
+            global {
+              usage-count yes;
+            }
+
+            common {
+              net {
+                protocol C;
+                ping-int 1;
+              }
+            }
+
+            resource r0 {
+              volume 0 {
+                device    /dev/drbd0;
+                disk      /dev/vdb;
+                meta-disk internal;
+              }
+
+              on drbd1 {
+                address ${nodes.drbd1.config.networking.primaryIPAddress}:${toString drbdPort};
+              }
+
+              on drbd2 {
+                address ${nodes.drbd2.config.networking.primaryIPAddress}:${toString drbdPort};
+              }
+            }
+          '';
+        };
+      };
+  in
+  {
+    name = "drbd";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ ryantm astro ];
+    };
+
+    nodes.drbd1 = drbdConfig;
+    nodes.drbd2 = drbdConfig;
+
+    testScript = { nodes }: ''
+      drbd1.start()
+      drbd2.start()
+
+      drbd1.wait_for_unit("network.target")
+      drbd2.wait_for_unit("network.target")
+
+      drbd1.succeed(
+          "drbdadm create-md r0",
+          "drbdadm up r0",
+          "drbdadm primary r0 --force",
+      )
+
+      drbd2.succeed("drbdadm create-md r0", "drbdadm up r0")
+
+      drbd1.succeed(
+          "mkfs.ext4 /dev/drbd0",
+          "mkdir -p /mnt/drbd",
+          "mount /dev/drbd0 /mnt/drbd",
+          "touch /mnt/drbd/hello",
+          "umount /mnt/drbd",
+          "drbdadm secondary r0",
+      )
+      drbd1.sleep(1)
+
+      drbd2.succeed(
+          "drbdadm primary r0",
+          "mkdir -p /mnt/drbd",
+          "mount /dev/drbd0 /mnt/drbd",
+          "ls /mnt/drbd/hello",
+      )
+    '';
+  }
+)
diff --git a/nixos/tests/ec2.nix b/nixos/tests/ec2.nix
new file mode 100644
index 00000000000..aa3c2b7051f
--- /dev/null
+++ b/nixos/tests/ec2.nix
@@ -0,0 +1,158 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+with import common/ec2.nix { inherit makeTest pkgs; };
+
+let
+  imageCfg = (import ../lib/eval-config.nix {
+    inherit system;
+    modules = [
+      ../maintainers/scripts/ec2/amazon-image.nix
+      ../modules/testing/test-instrumentation.nix
+      ../modules/profiles/qemu-guest.nix
+      {
+        ec2.hvm = true;
+
+        # Hack to make the partition resizing work in QEMU.
+        boot.initrd.postDeviceCommands = mkBefore ''
+          ln -s vda /dev/xvda
+          ln -s vda1 /dev/xvda1
+        '';
+
+        # In a NixOS test the serial console is occupied by the "backdoor"
+        # (see testing/test-instrumentation.nix) and is incompatible with
+        # the configuration in virtualisation/amazon-image.nix.
+        systemd.services."serial-getty@ttyS0".enable = mkForce false;
+
+        # Needed by nixos-rebuild due to the lack of network
+        # access. Determined by trial and error.
+        system.extraDependencies = with pkgs; ( [
+          # Needed for a nixos-rebuild.
+          busybox
+          cloud-utils
+          desktop-file-utils
+          libxslt.bin
+          mkinitcpio-nfs-utils
+          stdenv
+          stdenvNoCC
+          texinfo
+          unionfs-fuse
+          xorg.lndir
+
+          # These are used in the configure-from-userdata tests
+          # for EC2. Httpd and valgrind are requested by the
+          # configuration.
+          apacheHttpd
+          apacheHttpd.doc
+          apacheHttpd.man
+          valgrind.doc
+        ]);
+      }
+    ];
+  }).config;
+  image = "${imageCfg.system.build.amazonImage}/${imageCfg.amazonImage.name}.vhd";
+
+  sshKeys = import ./ssh-keys.nix pkgs;
+  snakeOilPrivateKey = sshKeys.snakeOilPrivateKey.text;
+  snakeOilPrivateKeyFile = pkgs.writeText "private-key" snakeOilPrivateKey;
+  snakeOilPublicKey = sshKeys.snakeOilPublicKey;
+
+in {
+  boot-ec2-nixops = makeEc2Test {
+    name         = "nixops-userdata";
+    inherit image;
+    sshPublicKey = snakeOilPublicKey; # That's right folks! My user's key is also the host key!
+
+    userData = ''
+      SSH_HOST_ED25519_KEY_PUB:${snakeOilPublicKey}
+      SSH_HOST_ED25519_KEY:${replaceStrings ["\n"] ["|"] snakeOilPrivateKey}
+    '';
+    script = ''
+      machine.start()
+      machine.wait_for_file("/etc/ec2-metadata/user-data")
+      machine.wait_for_unit("sshd.service")
+
+      machine.succeed("grep unknown /etc/ec2-metadata/ami-manifest-path")
+
+      # We have no keys configured on the client side yet, so this should fail
+      machine.fail("ssh -o BatchMode=yes localhost exit")
+
+      # Let's install our client private key
+      machine.succeed("mkdir -p ~/.ssh")
+
+      machine.copy_from_host_via_shell(
+          "${snakeOilPrivateKeyFile}", "~/.ssh/id_ed25519"
+      )
+      machine.succeed("chmod 600 ~/.ssh/id_ed25519")
+
+      # We haven't configured the host key yet, so this should still fail
+      machine.fail("ssh -o BatchMode=yes localhost exit")
+
+      # Add the host key; ssh should finally succeed
+      machine.succeed(
+          "echo localhost,127.0.0.1 ${snakeOilPublicKey} > ~/.ssh/known_hosts"
+      )
+      machine.succeed("ssh -o BatchMode=yes localhost exit")
+
+      # Test whether the root disk was resized.
+      blocks, block_size = map(int, machine.succeed("stat -c %b:%S -f /").split(":"))
+      GB = 1024 ** 3
+      assert 9.7 * GB <= blocks * block_size <= 10 * GB
+
+      # Just to make sure resizing is idempotent.
+      machine.shutdown()
+      machine.start()
+      machine.wait_for_file("/etc/ec2-metadata/user-data")
+    '';
+  };
+
+  boot-ec2-config = makeEc2Test {
+    name         = "config-userdata";
+    meta.broken = true; # amazon-init wants to download from the internet while building the system
+    inherit image;
+    sshPublicKey = snakeOilPublicKey;
+
+    # ### https://nixos.org/channels/nixos-unstable nixos
+    userData = ''
+      { pkgs, ... }:
+
+      {
+        imports = [
+          <nixpkgs/nixos/modules/virtualisation/amazon-image.nix>
+          <nixpkgs/nixos/modules/testing/test-instrumentation.nix>
+          <nixpkgs/nixos/modules/profiles/qemu-guest.nix>
+        ];
+        environment.etc.testFile = {
+          text = "whoa";
+        };
+
+        networking.hostName = "ec2-test-vm"; # required by services.httpd
+
+        services.httpd = {
+          enable = true;
+          adminAddr = "test@example.org";
+          virtualHosts.localhost.documentRoot = "''${pkgs.valgrind.doc}/share/doc/valgrind/html";
+        };
+        networking.firewall.allowedTCPPorts = [ 80 ];
+      }
+    '';
+    script = ''
+      machine.start()
+
+      # amazon-init must succeed. if it fails, make the test fail
+      # immediately instead of timing out in wait_for_file.
+      machine.wait_for_unit("amazon-init.service")
+
+      machine.wait_for_file("/etc/testFile")
+      assert "whoa" in machine.succeed("cat /etc/testFile")
+
+      machine.wait_for_unit("httpd.service")
+      assert "Valgrind" in machine.succeed("curl http://localhost")
+    '';
+  };
+}
diff --git a/nixos/tests/ecryptfs.nix b/nixos/tests/ecryptfs.nix
new file mode 100644
index 00000000000..ef7bd13eb92
--- /dev/null
+++ b/nixos/tests/ecryptfs.nix
@@ -0,0 +1,85 @@
+import ./make-test-python.nix ({ ... }:
+{
+  name = "ecryptfs";
+
+  machine = { pkgs, ... }: {
+    imports = [ ./common/user-account.nix ];
+    boot.kernelModules = [ "ecryptfs" ];
+    security.pam.enableEcryptfs = true;
+    environment.systemPackages = with pkgs; [ keyutils ];
+  };
+
+  testScript = ''
+    def login_as_alice():
+        machine.wait_until_tty_matches(1, "login: ")
+        machine.send_chars("alice\n")
+        machine.wait_until_tty_matches(1, "Password: ")
+        machine.send_chars("foobar\n")
+        machine.wait_until_tty_matches(1, "alice\@machine")
+
+
+    def logout():
+        machine.send_chars("logout\n")
+        machine.wait_until_tty_matches(1, "login: ")
+
+
+    machine.wait_for_unit("default.target")
+
+    with subtest("Set alice up with a password and a home"):
+        machine.succeed("(echo foobar; echo foobar) | passwd alice")
+        machine.succeed("chown -R alice.users ~alice")
+
+    with subtest("Migrate alice's home"):
+        out = machine.succeed("echo foobar | ecryptfs-migrate-home -u alice")
+        machine.log(f"ecryptfs-migrate-home said: {out}")
+
+    with subtest("Log alice in (ecryptfs passwhrase is wrapped during first login)"):
+        login_as_alice()
+        machine.send_chars("logout\n")
+        machine.wait_until_tty_matches(1, "login: ")
+
+    # Why do I need to do this??
+    machine.succeed("su alice -c ecryptfs-umount-private || true")
+    machine.sleep(1)
+
+    with subtest("check that encrypted home is not mounted"):
+        machine.fail("mount | grep ecryptfs")
+
+    with subtest("Show contents of the user keyring"):
+        out = machine.succeed("su - alice -c 'keyctl list \@u'")
+        machine.log(f"keyctl unlink said: {out}")
+
+    with subtest("Log alice again"):
+        login_as_alice()
+
+    with subtest("Create some files in encrypted home"):
+        machine.succeed("su alice -c 'touch ~alice/a'")
+        machine.succeed("su alice -c 'echo c > ~alice/b'")
+
+    with subtest("Logout"):
+        logout()
+
+    # Why do I need to do this??
+    machine.succeed("su alice -c ecryptfs-umount-private || true")
+    machine.sleep(1)
+
+    with subtest("Check that the filesystem is not accessible"):
+        machine.fail("mount | grep ecryptfs")
+        machine.succeed("su alice -c 'test \! -f ~alice/a'")
+        machine.succeed("su alice -c 'test \! -f ~alice/b'")
+
+    with subtest("Log alice once more"):
+        login_as_alice()
+
+    with subtest("Check that the files are there"):
+        machine.sleep(1)
+        machine.succeed("su alice -c 'test -f ~alice/a'")
+        machine.succeed("su alice -c 'test -f ~alice/b'")
+        machine.succeed('test "$(cat ~alice/b)" = "c"')
+
+    with subtest("Catch https://github.com/NixOS/nixpkgs/issues/16766"):
+        machine.succeed("su alice -c 'ls -lh ~alice/'")
+
+    logout()
+  '';
+})
diff --git a/nixos/tests/elk.nix b/nixos/tests/elk.nix
new file mode 100644
index 00000000000..f42be00f23b
--- /dev/null
+++ b/nixos/tests/elk.nix
@@ -0,0 +1,305 @@
+# To run the test on the unfree ELK use the folllowing command:
+# cd path/to/nixpkgs
+# NIXPKGS_ALLOW_UNFREE=1 nix-build -A nixosTests.elk.unfree.ELK-6
+
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; },
+}:
+
+let
+  inherit (pkgs) lib;
+
+  esUrl = "http://localhost:9200";
+
+  mkElkTest = name : elk :
+    import ./make-test-python.nix ({
+    inherit name;
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ eelco offline basvandijk ];
+    };
+    nodes = {
+      one =
+        { pkgs, lib, ... }: {
+            # Not giving the machine at least 2060MB results in elasticsearch failing with the following error:
+            #
+            #   OpenJDK 64-Bit Server VM warning:
+            #     INFO: os::commit_memory(0x0000000085330000, 2060255232, 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 2060255232 bytes for committing reserved memory.
+            #
+            # When setting this to 2500 I got "Kernel panic - not syncing: Out of
+            # memory: compulsory panic_on_oom is enabled" so lets give it even a
+            # bit more room:
+            virtualisation.memorySize = 3000;
+
+            # For querying JSON objects returned from elasticsearch and kibana.
+            environment.systemPackages = [ pkgs.jq ];
+
+            services = {
+
+              journalbeat = {
+                enable = elk ? journalbeat;
+                package = elk.journalbeat;
+                extraConfig = pkgs.lib.mkOptionDefault (''
+                  logging:
+                    to_syslog: true
+                    level: warning
+                    metrics.enabled: false
+                  output.elasticsearch:
+                    hosts: [ "127.0.0.1:9200" ]
+                  journalbeat.inputs:
+                  - paths: []
+                    seek: cursor
+                '');
+              };
+
+              filebeat = {
+                enable = elk ? filebeat;
+                package = elk.filebeat;
+                inputs.journald.id = "everything";
+
+                inputs.log = {
+                  enabled = true;
+                  paths = [
+                    "/var/lib/filebeat/test"
+                  ];
+                };
+
+                settings = {
+                  logging.level = "info";
+                };
+              };
+
+              metricbeat = {
+                enable = true;
+                package = elk.metricbeat;
+                modules.system = {
+                  metricsets = ["cpu" "load" "memory" "network" "process" "process_summary" "uptime" "socket_summary"];
+                  enabled = true;
+                  period = "5s";
+                  processes = [".*"];
+                  cpu.metrics = ["percentages" "normalized_percentages"];
+                  core.metrics = ["percentages"];
+                };
+                settings = {
+                  output.elasticsearch = {
+                    hosts = ["127.0.0.1:9200"];
+                  };
+                };
+              };
+
+              logstash = {
+                enable = true;
+                package = elk.logstash;
+                inputConfig = ''
+                  exec { command => "echo -n flowers" interval => 1 type => "test" }
+                  exec { command => "echo -n dragons" interval => 1 type => "test" }
+                '';
+                filterConfig = ''
+                  if [message] =~ /dragons/ {
+                    drop {}
+                  }
+                '';
+                outputConfig = ''
+                  file {
+                    path => "/tmp/logstash.out"
+                    codec => line { format => "%{message}" }
+                  }
+                  elasticsearch {
+                    hosts => [ "${esUrl}" ]
+                  }
+                '';
+              };
+
+              elasticsearch = {
+                enable = true;
+                package = elk.elasticsearch;
+              };
+
+              kibana = {
+                enable = true;
+                package = elk.kibana;
+              };
+
+              elasticsearch-curator = {
+                enable = true;
+                actionYAML = ''
+                ---
+                actions:
+                  1:
+                    action: delete_indices
+                    description: >-
+                      Delete indices older than 1 second (based on index name), for logstash-
+                      prefixed indices. Ignore the error if the filter does not result in an
+                      actionable list of indices (ignore_empty_list) and exit cleanly.
+                    options:
+                      allow_ilm_indices: true
+                      ignore_empty_list: True
+                      disable_action: False
+                    filters:
+                    - filtertype: pattern
+                      kind: prefix
+                      value: logstash-
+                    - filtertype: age
+                      source: name
+                      direction: older
+                      timestring: '%Y.%m.%d'
+                      unit: seconds
+                      unit_count: 1
+                '';
+              };
+            };
+          };
+      };
+
+    passthru.elkPackages = elk;
+    testScript =
+      let
+        valueObject = lib.optionalString (lib.versionAtLeast elk.elasticsearch.version "7") ".value";
+      in ''
+      import json
+
+
+      def expect_hits(message):
+          dictionary = {"query": {"match": {"message": message}}}
+          return (
+              "curl --silent --show-error --fail-with-body '${esUrl}/_search' "
+              + "-H 'Content-Type: application/json' "
+              + "-d '{}' ".format(json.dumps(dictionary))
+              + " | tee /dev/console"
+              + " | jq -es 'if . == [] then null else .[] | .hits.total${valueObject} > 0 end'"
+          )
+
+
+      def expect_no_hits(message):
+          dictionary = {"query": {"match": {"message": message}}}
+          return (
+              "curl --silent --show-error --fail-with-body '${esUrl}/_search' "
+              + "-H 'Content-Type: application/json' "
+              + "-d '{}' ".format(json.dumps(dictionary))
+              + " | tee /dev/console"
+              + " | jq -es 'if . == [] then null else .[] | .hits.total${valueObject} == 0 end'"
+          )
+
+
+      def has_metricbeat():
+          dictionary = {"query": {"match": {"event.dataset": {"query": "system.cpu"}}}}
+          return (
+              "curl --silent --show-error --fail-with-body '${esUrl}/_search' "
+              + "-H 'Content-Type: application/json' "
+              + "-d '{}' ".format(json.dumps(dictionary))
+              + " | tee /dev/console"
+              + " | jq -es 'if . == [] then null else .[] | .hits.total${valueObject} > 0 end'"
+          )
+
+
+      start_all()
+
+      one.wait_for_unit("elasticsearch.service")
+      one.wait_for_open_port(9200)
+
+      # Continue as long as the status is not "red". The status is probably
+      # "yellow" instead of "green" because we are using a single elasticsearch
+      # node which elasticsearch considers risky.
+      #
+      # TODO: extend this test with multiple elasticsearch nodes
+      #       and see if the status turns "green".
+      one.wait_until_succeeds(
+          "curl --silent --show-error --fail-with-body '${esUrl}/_cluster/health'"
+          + " | jq -es 'if . == [] then null else .[] | .status != \"red\" end'"
+      )
+
+      with subtest("Perform some simple logstash tests"):
+          one.wait_for_unit("logstash.service")
+          one.wait_until_succeeds("cat /tmp/logstash.out | grep flowers")
+          one.wait_until_succeeds("cat /tmp/logstash.out | grep -v dragons")
+
+      with subtest("Kibana is healthy"):
+          one.wait_for_unit("kibana.service")
+          one.wait_until_succeeds(
+              "curl --silent --show-error --fail-with-body 'http://localhost:5601/api/status'"
+              + " | jq -es 'if . == [] then null else .[] | .status.overall.state == \"green\" end'"
+          )
+
+      with subtest("Metricbeat is running"):
+          one.wait_for_unit("metricbeat.service")
+
+      with subtest("Metricbeat metrics arrive in elasticsearch"):
+          one.wait_until_succeeds(has_metricbeat())
+
+      with subtest("Logstash messages arive in elasticsearch"):
+          one.wait_until_succeeds(expect_hits("flowers"))
+          one.wait_until_succeeds(expect_no_hits("dragons"))
+
+    '' + lib.optionalString (elk ? journalbeat) ''
+      with subtest(
+          "A message logged to the journal is ingested by elasticsearch via journalbeat"
+      ):
+          one.wait_for_unit("journalbeat.service")
+          one.execute("echo 'Supercalifragilisticexpialidocious' | systemd-cat")
+          one.wait_until_succeeds(
+              expect_hits("Supercalifragilisticexpialidocious")
+          )
+    '' + lib.optionalString (elk ? filebeat) ''
+      with subtest(
+          "A message logged to the journal is ingested by elasticsearch via filebeat"
+      ):
+          one.wait_for_unit("filebeat.service")
+          one.execute("echo 'Superdupercalifragilisticexpialidocious' | systemd-cat")
+          one.wait_until_succeeds(
+              expect_hits("Superdupercalifragilisticexpialidocious")
+          )
+          one.execute(
+              "echo 'SuperdupercalifragilisticexpialidociousIndeed' >> /var/lib/filebeat/test"
+          )
+          one.wait_until_succeeds(
+              expect_hits("SuperdupercalifragilisticexpialidociousIndeed")
+          )
+    '' + ''
+      with subtest("Elasticsearch-curator works"):
+          one.systemctl("stop logstash")
+          one.systemctl("start elasticsearch-curator")
+          one.wait_until_succeeds(
+              '! curl --silent --show-error --fail-with-body "${esUrl}/_cat/indices" | grep logstash | grep ^'
+          )
+    '';
+  }) { 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" {
+  #   name = "elk-7";
+  #   elasticsearch = pkgs.elasticsearch7-oss;
+  #   logstash      = pkgs.logstash7-oss;
+  #   kibana        = pkgs.kibana7-oss;
+  #   filebeat      = pkgs.filebeat7;
+  #   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;
+      kibana        = pkgs.kibana7;
+      filebeat      = pkgs.filebeat7;
+      metricbeat    = pkgs.metricbeat7;
+    };
+  };
+}
diff --git a/nixos/tests/emacs-daemon.nix b/nixos/tests/emacs-daemon.nix
new file mode 100644
index 00000000000..e12da56021d
--- /dev/null
+++ b/nixos/tests/emacs-daemon.nix
@@ -0,0 +1,48 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "emacs-daemon";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ];
+  };
+
+  enableOCR = true;
+
+  machine =
+    { ... }:
+
+    { imports = [ ./common/x11.nix ];
+      services.emacs = {
+        enable = true;
+        defaultEditor = true;
+      };
+
+      # Important to get the systemd service running for root
+      environment.variables.XDG_RUNTIME_DIR = "/run/user/0";
+
+      environment.variables.TEST_SYSTEM_VARIABLE = "system variable";
+    };
+
+  testScript = ''
+      machine.wait_for_unit("multi-user.target")
+
+      # checks that the EDITOR environment variable is set
+      machine.succeed('test $(basename "$EDITOR") = emacseditor')
+
+      # waits for the emacs service to be ready
+      machine.wait_until_succeeds(
+          "systemctl --user status emacs.service | grep 'Active: active'"
+      )
+
+      # connects to the daemon
+      machine.succeed("emacsclient --create-frame $EDITOR >&2 &")
+
+      # checks that Emacs shows the edited filename
+      machine.wait_for_text("emacseditor")
+
+      # makes sure environment variables are accessible from Emacs
+      machine.succeed(
+          "emacsclient --eval '(getenv \"TEST_SYSTEM_VARIABLE\")' | grep -q 'system variable'"
+      )
+
+      machine.screenshot("emacsclient")
+    '';
+})
diff --git a/nixos/tests/empty-file b/nixos/tests/empty-file
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/nixos/tests/empty-file
diff --git a/nixos/tests/engelsystem.nix b/nixos/tests/engelsystem.nix
new file mode 100644
index 00000000000..7be3b8a5a1f
--- /dev/null
+++ b/nixos/tests/engelsystem.nix
@@ -0,0 +1,41 @@
+import ./make-test-python.nix (
+  { pkgs, lib, ... }:
+  {
+    name = "engelsystem";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ talyz ];
+    };
+
+    nodes.engelsystem =
+      { ... }:
+      {
+        services.engelsystem = {
+          enable = true;
+          domain = "engelsystem";
+          createDatabase = true;
+        };
+        networking.firewall.allowedTCPPorts = [ 80 443 ];
+        environment.systemPackages = with pkgs; [
+          xmlstarlet
+          libxml2
+        ];
+      };
+
+    testScript = ''
+      engelsystem.start()
+      engelsystem.wait_for_unit("phpfpm-engelsystem.service")
+      engelsystem.wait_until_succeeds("curl engelsystem/login -sS -f")
+      engelsystem.succeed(
+          "curl engelsystem/login -sS -f -c cookie | xmllint -html -xmlout - >login"
+      )
+      engelsystem.succeed(
+          "xml sel -T -t -m \"html/head/meta[@name='csrf-token']\" -v @content login >token"
+      )
+      engelsystem.succeed(
+          "curl engelsystem/login -sS -f -b cookie -F 'login=admin' -F 'password=asdfasdf' -F '_token=<token' -L | xmllint -html -xmlout - >news"
+      )
+      engelsystem.succeed(
+          "test 'News - Engelsystem' = \"$(xml sel -T -t -c html/head/title news)\""
+      )
+    '';
+  })
diff --git a/nixos/tests/enlightenment.nix b/nixos/tests/enlightenment.nix
new file mode 100644
index 00000000000..8506c348246
--- /dev/null
+++ b/nixos/tests/enlightenment.nix
@@ -0,0 +1,96 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+{
+  name = "enlightenment";
+
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ romildo ];
+  };
+
+  machine = { ... }:
+  {
+    imports = [ ./common/user-account.nix ];
+    services.xserver.enable = true;
+    services.xserver.desktopManager.enlightenment.enable = true;
+    services.xserver.displayManager = {
+      lightdm.enable = true;
+      autoLogin = {
+        enable = true;
+        user = "alice";
+      };
+    };
+    hardware.pulseaudio.enable = true; # needed for the factl test, /dev/snd/* exists without them but udev doesn't care then
+    environment.systemPackages = [ pkgs.xdotool ];
+    services.acpid.enable = true;
+    services.connman.enable = true;
+    services.connman.package = pkgs.connmanMinimal;
+  };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+  in ''
+    with subtest("Ensure x starts"):
+        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("First time wizard"):
+        machine.wait_for_text("Default")  # Language
+        machine.screenshot("wizard1")
+        machine.succeed("xdotool mousemove 512 740 click 1")  # Next
+        machine.screenshot("wizard2")
+
+        machine.wait_for_text("English")  # Keyboard (default)
+        machine.screenshot("wizard3")
+        machine.succeed("xdotool mousemove 512 740 click 1")  # Next
+
+        machine.wait_for_text("Standard")  # Profile (default)
+        machine.screenshot("wizard4")
+        machine.succeed("xdotool mousemove 512 740 click 1")  # Next
+
+        machine.wait_for_text("Title")  # Sizing (default)
+        machine.screenshot("wizard5")
+        machine.succeed("xdotool mousemove 512 740 click 1")  # Next
+
+        machine.wait_for_text("clicked")  # Windows Focus
+        machine.succeed("xdotool mousemove 512 370 click 1")  # Click
+        machine.screenshot("wizard6")
+        machine.succeed("xdotool mousemove 512 740 click 1")  # Next
+
+        machine.wait_for_text("Connman")  # Network Management (default)
+        machine.screenshot("wizard7")
+        machine.succeed("xdotool mousemove 512 740 click 1")  # Next
+
+        machine.wait_for_text("BlusZ")  # Bluetooh Management (default)
+        machine.screenshot("wizard8")
+        machine.succeed("xdotool mousemove 512 740 click 1")  # Next
+
+        machine.wait_for_text("OpenGL")  # Compositing (default)
+        machine.screenshot("wizard9")
+        machine.succeed("xdotool mousemove 512 740 click 1")  # Next
+
+        machine.wait_for_text("update")  # Updates
+        machine.succeed("xdotool mousemove 512 495 click 1")  # Disable
+        machine.screenshot("wizard10")
+        machine.succeed("xdotool mousemove 512 740 click 1")  # Next
+
+        machine.wait_for_text("taskbar")  # Taskbar
+        machine.succeed("xdotool mousemove 480 410 click 1")  # Enable
+        machine.screenshot("wizard11")
+        machine.succeed("xdotool mousemove 512 740 click 1")  # Next
+
+        machine.wait_for_text("Home")  # The desktop
+        machine.screenshot("wizard12")
+
+    with subtest("Run Terminology"):
+        machine.succeed("terminology >&2 &")
+        machine.sleep(5)
+        machine.send_chars("ls --color -alF\n")
+        machine.sleep(2)
+        machine.screenshot("terminology")
+  '';
+})
diff --git a/nixos/tests/env.nix b/nixos/tests/env.nix
new file mode 100644
index 00000000000..fc96ace6b2d
--- /dev/null
+++ b/nixos/tests/env.nix
@@ -0,0 +1,36 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "environment";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ nequissimus ];
+  };
+
+  machine = { pkgs, ... }:
+    {
+      boot.kernelPackages = pkgs.linuxPackages;
+      environment.etc.plainFile.text = ''
+        Hello World
+      '';
+      environment.etc."folder/with/file".text = ''
+        Foo Bar!
+      '';
+
+      environment.sessionVariables = {
+        TERMINFO_DIRS = "/run/current-system/sw/share/terminfo";
+        NIXCON = "awesome";
+      };
+    };
+
+  testScript = ''
+    machine.succeed('[ -L "/etc/plainFile" ]')
+    assert "Hello World" in machine.succeed('cat "/etc/plainFile"')
+    machine.succeed('[ -d "/etc/folder" ]')
+    machine.succeed('[ -d "/etc/folder/with" ]')
+    machine.succeed('[ -L "/etc/folder/with/file" ]')
+    assert "Hello World" in machine.succeed('cat "/etc/plainFile"')
+
+    assert "/run/current-system/sw/share/terminfo" in machine.succeed(
+        "echo ''${TERMINFO_DIRS}"
+    )
+    assert "awesome" in machine.succeed("echo ''${NIXCON}")
+  '';
+})
diff --git a/nixos/tests/ergo.nix b/nixos/tests/ergo.nix
new file mode 100644
index 00000000000..b49e0c9dfed
--- /dev/null
+++ b/nixos/tests/ergo.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "ergo";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mmahut ];
+  };
+
+  nodes = {
+    machine = { ... }: {
+      services.ergo.enable = true;
+      services.ergo.api.keyHash = "324dcf027dd4a30a932c441f365a25e86b173defa4b8e58948253471b81b72cf";
+    };
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("ergo.service")
+  '';
+})
diff --git a/nixos/tests/ergochat.nix b/nixos/tests/ergochat.nix
new file mode 100644
index 00000000000..2e9dc55e648
--- /dev/null
+++ b/nixos/tests/ergochat.nix
@@ -0,0 +1,97 @@
+let
+  clients = [
+    "ircclient1"
+    "ircclient2"
+  ];
+  server = "ergochat";
+  ircPort = 6667;
+  channel = "nixos-cat";
+  iiDir = "/tmp/irc";
+in
+
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "ergochat";
+  nodes = {
+    "${server}" = {
+      networking.firewall.allowedTCPPorts = [ ircPort ];
+      services.ergochat = {
+        enable = true;
+        settings.server.motd = pkgs.writeText "ergo.motd" ''
+          The default MOTD doesn't contain the word "nixos" in it.
+          This one does.
+        '';
+      };
+    };
+  } // lib.listToAttrs (builtins.map (client: lib.nameValuePair client {
+    imports = [
+      ./common/user-account.nix
+    ];
+
+    systemd.services.ii = {
+      requires = [ "network.target" ];
+      wantedBy = [ "default.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        ExecPreStartPre = "mkdir -p ${iiDir}";
+        ExecStart = ''
+          ${lib.getBin pkgs.ii}/bin/ii -n ${client} -s ${server} -i ${iiDir}
+        '';
+        User = "alice";
+      };
+    };
+  }) clients);
+
+  testScript =
+    let
+      msg = client: "Hello, my name is ${client}";
+      clientScript = client: [
+        ''
+          ${client}.wait_for_unit("network.target")
+          ${client}.systemctl("start ii")
+          ${client}.wait_for_unit("ii")
+          ${client}.wait_for_file("${iiDir}/${server}/out")
+        ''
+        # look for the custom text in the MOTD.
+        ''
+          ${client}.wait_until_succeeds("grep 'nixos' ${iiDir}/${server}/out")
+        ''
+        # wait until first PING from server arrives before joining,
+        # so we don't try it too early
+        ''
+          ${client}.wait_until_succeeds("grep 'PING' ${iiDir}/${server}/out")
+        ''
+        # join ${channel}
+        ''
+          ${client}.succeed("echo '/j #${channel}' > ${iiDir}/${server}/in")
+          ${client}.wait_for_file("${iiDir}/${server}/#${channel}/in")
+        ''
+        # send a greeting
+        ''
+          ${client}.succeed(
+              "echo '${msg client}' > ${iiDir}/${server}/#${channel}/in"
+          )
+        ''
+        # check that all greetings arrived on all clients
+      ] ++ builtins.map (other: ''
+        ${client}.succeed(
+            "grep '${msg other}$' ${iiDir}/${server}/#${channel}/out"
+        )
+      '') clients;
+
+      # foldl', but requires a non-empty list instead of a start value
+      reduce = f: list:
+        builtins.foldl' f (builtins.head list) (builtins.tail list);
+    in ''
+      start_all()
+      ${server}.systemctl("status ergochat")
+      ${server}.wait_for_open_port(${toString ircPort})
+
+      # run clientScript for all clients so that every list
+      # entry is executed by every client before advancing
+      # to the next one.
+    '' + lib.concatStrings
+      (reduce
+        (lib.zipListsWith (cs: c: cs + c))
+        (builtins.map clientScript clients));
+})
diff --git a/nixos/tests/etcd-cluster.nix b/nixos/tests/etcd-cluster.nix
new file mode 100644
index 00000000000..410cb654794
--- /dev/null
+++ b/nixos/tests/etcd-cluster.nix
@@ -0,0 +1,154 @@
+# This test runs simple etcd cluster
+
+import ./make-test-python.nix ({ pkgs, ... } : let
+
+  runWithOpenSSL = file: cmd: pkgs.runCommand file {
+    buildInputs = [ pkgs.openssl ];
+  } cmd;
+
+  ca_key = runWithOpenSSL "ca-key.pem" "openssl genrsa -out $out 2048";
+  ca_pem = runWithOpenSSL "ca.pem" ''
+    openssl req \
+      -x509 -new -nodes -key ${ca_key} \
+      -days 10000 -out $out -subj "/CN=etcd-ca"
+  '';
+  etcd_key = runWithOpenSSL "etcd-key.pem" "openssl genrsa -out $out 2048";
+  etcd_csr = runWithOpenSSL "etcd.csr" ''
+    openssl req \
+       -new -key ${etcd_key} \
+       -out $out -subj "/CN=etcd" \
+       -config ${openssl_cnf}
+  '';
+  etcd_cert = runWithOpenSSL "etcd.pem" ''
+    openssl x509 \
+      -req -in ${etcd_csr} \
+      -CA ${ca_pem} -CAkey ${ca_key} \
+      -CAcreateserial -out $out \
+      -days 365 -extensions v3_req \
+      -extfile ${openssl_cnf}
+  '';
+
+  etcd_client_key = runWithOpenSSL "etcd-client-key.pem"
+    "openssl genrsa -out $out 2048";
+
+  etcd_client_csr = runWithOpenSSL "etcd-client-key.pem" ''
+    openssl req \
+      -new -key ${etcd_client_key} \
+      -out $out -subj "/CN=etcd-client" \
+      -config ${client_openssl_cnf}
+  '';
+
+  etcd_client_cert = runWithOpenSSL "etcd-client.crt" ''
+    openssl x509 \
+      -req -in ${etcd_client_csr} \
+      -CA ${ca_pem} -CAkey ${ca_key} -CAcreateserial \
+      -out $out -days 365 -extensions v3_req \
+      -extfile ${client_openssl_cnf}
+  '';
+
+  openssl_cnf = pkgs.writeText "openssl.cnf" ''
+    ions = v3_req
+    distinguished_name = req_distinguished_name
+    [req_distinguished_name]
+    [ v3_req ]
+    basicConstraints = CA:FALSE
+    keyUsage = digitalSignature, keyEncipherment
+    extendedKeyUsage = serverAuth
+    subjectAltName = @alt_names
+    [alt_names]
+    DNS.1 = node1
+    DNS.2 = node2
+    DNS.3 = node3
+    IP.1 = 127.0.0.1
+  '';
+
+  client_openssl_cnf = pkgs.writeText "client-openssl.cnf" ''
+    ions = v3_req
+    distinguished_name = req_distinguished_name
+    [req_distinguished_name]
+    [ v3_req ]
+    basicConstraints = CA:FALSE
+    keyUsage = digitalSignature, keyEncipherment
+    extendedKeyUsage = clientAuth
+  '';
+
+  nodeConfig = {
+    services = {
+      etcd = {
+        enable = true;
+        keyFile = etcd_key;
+        certFile = etcd_cert;
+        trustedCaFile = ca_pem;
+        peerClientCertAuth = true;
+        listenClientUrls = ["https://127.0.0.1:2379"];
+        listenPeerUrls = ["https://0.0.0.0:2380"];
+      };
+    };
+
+    environment.variables = {
+      ETCDCTL_CERT_FILE = "${etcd_client_cert}";
+      ETCDCTL_KEY_FILE = "${etcd_client_key}";
+      ETCDCTL_CA_FILE = "${ca_pem}";
+      ETCDCTL_PEERS = "https://127.0.0.1:2379";
+    };
+
+    networking.firewall.allowedTCPPorts = [ 2380 ];
+  };
+in {
+  name = "etcd";
+
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ offline ];
+  };
+
+  nodes = {
+    node1 = { ... }: {
+      require = [nodeConfig];
+      services.etcd = {
+        initialCluster = ["node1=https://node1:2380" "node2=https://node2:2380"];
+        initialAdvertisePeerUrls = ["https://node1:2380"];
+      };
+    };
+
+    node2 = { ... }: {
+      require = [nodeConfig];
+      services.etcd = {
+        initialCluster = ["node1=https://node1:2380" "node2=https://node2:2380"];
+        initialAdvertisePeerUrls = ["https://node2:2380"];
+      };
+    };
+
+    node3 = { ... }: {
+      require = [nodeConfig];
+      services.etcd = {
+        initialCluster = ["node1=https://node1:2380" "node2=https://node2:2380" "node3=https://node3:2380"];
+        initialAdvertisePeerUrls = ["https://node3:2380"];
+        initialClusterState = "existing";
+      };
+    };
+  };
+
+  testScript = ''
+    with subtest("should start etcd cluster"):
+        node1.start()
+        node2.start()
+        node1.wait_for_unit("etcd.service")
+        node2.wait_for_unit("etcd.service")
+        node2.wait_until_succeeds("etcdctl cluster-health")
+        node1.succeed("etcdctl set /foo/bar 'Hello world'")
+        node2.succeed("etcdctl get /foo/bar | grep 'Hello world'")
+
+    with subtest("should add another member"):
+        node1.wait_until_succeeds("etcdctl member add node3 https://node3:2380")
+        node3.start()
+        node3.wait_for_unit("etcd.service")
+        node3.wait_until_succeeds("etcdctl member list | grep 'node3'")
+        node3.succeed("etcdctl cluster-health")
+
+    with subtest("should survive member crash"):
+        node3.crash()
+        node1.succeed("etcdctl cluster-health")
+        node1.succeed("etcdctl set /foo/bar 'Hello degraded world'")
+        node1.succeed("etcdctl get /foo/bar | grep 'Hello degraded world'")
+  '';
+})
diff --git a/nixos/tests/etcd.nix b/nixos/tests/etcd.nix
new file mode 100644
index 00000000000..702bbb668f5
--- /dev/null
+++ b/nixos/tests/etcd.nix
@@ -0,0 +1,25 @@
+# This test runs simple etcd node
+
+import ./make-test-python.nix ({ pkgs, ... } : {
+  name = "etcd";
+
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ offline ];
+  };
+
+  nodes = {
+    node = { ... }: {
+      services.etcd.enable = true;
+    };
+  };
+
+  testScript = ''
+    with subtest("should start etcd node"):
+        node.start()
+        node.wait_for_unit("etcd.service")
+
+    with subtest("should write and read some values to etcd"):
+        node.succeed("etcdctl set /foo/bar 'Hello world'")
+        node.succeed("etcdctl get /foo/bar | grep 'Hello world'")
+  '';
+})
diff --git a/nixos/tests/etebase-server.nix b/nixos/tests/etebase-server.nix
new file mode 100644
index 00000000000..4fc3c1f6392
--- /dev/null
+++ b/nixos/tests/etebase-server.nix
@@ -0,0 +1,50 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+  dataDir = "/var/lib/foobar";
+
+in {
+    name = "etebase-server";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ felschr ];
+    };
+
+    machine = { pkgs, ... }:
+      {
+        services.etebase-server = {
+          inherit dataDir;
+          enable = true;
+          settings.global.secret_file =
+            toString (pkgs.writeText "secret" "123456");
+        };
+      };
+
+    testScript = ''
+      machine.wait_for_unit("etebase-server.service")
+      machine.wait_for_open_port(8001)
+
+      with subtest("Database & src-version were created"):
+          machine.wait_for_file("${dataDir}/src-version")
+          assert (
+              "${pkgs.etebase-server}"
+              in machine.succeed("cat ${dataDir}/src-version")
+          )
+          machine.wait_for_file("${dataDir}/db.sqlite3")
+          machine.wait_for_file("${dataDir}/static")
+
+      with subtest("Only allow access from allowed_hosts"):
+          machine.succeed("curl -sSfL http://0.0.0.0:8001/")
+          machine.fail("curl -sSfL http://127.0.0.1:8001/")
+          machine.fail("curl -sSfL http://localhost:8001/")
+
+      with subtest("Run tests"):
+          machine.succeed("etebase-server check")
+          machine.succeed("etebase-server test")
+
+      with subtest("Create superuser"):
+          machine.succeed(
+              "etebase-server createsuperuser --no-input --username admin --email root@localhost"
+          )
+    '';
+  }
+)
diff --git a/nixos/tests/etesync-dav.nix b/nixos/tests/etesync-dav.nix
new file mode 100644
index 00000000000..6a747e23f76
--- /dev/null
+++ b/nixos/tests/etesync-dav.nix
@@ -0,0 +1,21 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+
+  name = "etesync-dav";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ _3699n ];
+  };
+
+  machine = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.curl pkgs.etesync-dav ];
+  };
+
+  testScript =
+    ''
+      machine.wait_for_unit("multi-user.target")
+      machine.succeed("etesync-dav --version")
+      machine.execute("etesync-dav >&2 &")
+      machine.wait_for_open_port(37358)
+      with subtest("Check that the web interface is accessible"):
+          assert "Add User" in machine.succeed("curl -s http://localhost:37358/.web/add/")
+    '';
+})
diff --git a/nixos/tests/fancontrol.nix b/nixos/tests/fancontrol.nix
new file mode 100644
index 00000000000..296c6802641
--- /dev/null
+++ b/nixos/tests/fancontrol.nix
@@ -0,0 +1,34 @@
+import ./make-test-python.nix ({ pkgs, ... } : {
+  name = "fancontrol";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ evils ];
+  };
+
+  machine = { ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+    hardware.fancontrol.enable = true;
+    hardware.fancontrol.config = ''
+      INTERVAL=42
+      DEVPATH=hwmon1=devices/platform/dummy
+      DEVNAME=hwmon1=dummy
+      FCTEMPS=hwmon1/device/pwm1=hwmon1/device/temp1_input
+      FCFANS=hwmon1/device/pwm1=hwmon1/device/fan1_input
+      MINTEMP=hwmon1/device/pwm1=25
+      MAXTEMP=hwmon1/device/pwm1=65
+      MINSTART=hwmon1/device/pwm1=150
+      MINSTOP=hwmon1/device/pwm1=0
+    '';
+    };
+
+  # This configuration cannot be valid for the test VM, so it's expected to get an 'outdated' error.
+  testScript = ''
+    start_all()
+    # can't wait for unit fancontrol.service because it doesn't become active due to invalid config
+    # fancontrol.service is WantedBy multi-user.target
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed(
+        "journalctl -eu fancontrol | tee /dev/stderr | grep 'Configuration appears to be outdated'"
+    )
+    machine.shutdown()
+  '';
+})
diff --git a/nixos/tests/fcitx/config b/nixos/tests/fcitx/config
new file mode 100644
index 00000000000..169768994e2
--- /dev/null
+++ b/nixos/tests/fcitx/config
@@ -0,0 +1,12 @@
+[Hotkey]
+SwitchKey=Disabled
+IMSwitchHotkey=ALT_SHIFT
+TimeInterval=240
+
+[Program]
+DelayStart=5
+
+[Output]
+
+[Appearance]
+
diff --git a/nixos/tests/fcitx/default.nix b/nixos/tests/fcitx/default.nix
new file mode 100644
index 00000000000..a243be8dc19
--- /dev/null
+++ b/nixos/tests/fcitx/default.nix
@@ -0,0 +1,141 @@
+import ../make-test-python.nix (
+  {
+    pkgs, ...
+  }:
+    # copy_from_host works only for store paths
+    rec {
+        name = "fcitx";
+        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
+        ''
+            # We need config files before login session
+            # So copy first thing
+
+            # Point and click would be expensive,
+            # So configure using files
+            machine.copy_from_host(
+                "${./profile}",
+                "${fcitx_confdir}/profile",
+            )
+            machine.copy_from_host(
+                "${./config}",
+                "${fcitx_confdir}/config",
+            )
+
+            start_all()
+
+            machine.wait_for_file("${xauth}")
+            machine.succeed("xauth merge ${xauth}")
+
+            machine.sleep(5)
+
+            machine.succeed("su - ${userName} -c 'alacritty&'")
+            machine.succeed("su - ${userName} -c 'fcitx&'")
+            machine.sleep(10)
+
+            ### Type on terminal
+            machine.send_chars("echo ")
+            machine.sleep(1)
+
+            ### Start fcitx Unicode input
+            machine.send_key("ctrl-alt-shift-u")
+            machine.sleep(5)
+            machine.sleep(1)
+
+            ### Search for smiling face
+            machine.send_chars("smil")
+            machine.sleep(1)
+
+            ### Navigate to the second one
+            machine.send_key("tab")
+            machine.sleep(1)
+
+            ### Choose it
+            machine.send_key("\n")
+            machine.sleep(1)
+
+            ### Start fcitx language input
+            machine.send_key("ctrl-spc")
+            machine.sleep(1)
+
+            ### Default zhengma, enter 一下
+            machine.send_chars("a2")
+            machine.sleep(1)
+
+            ### Switch to Harvard Kyoto
+            machine.send_key("alt-shift")
+            machine.sleep(1)
+
+            ### Enter क
+            machine.send_chars("ka ")
+            machine.sleep(1)
+
+            machine.send_key("alt-shift")
+            machine.sleep(1)
+
+            ### Turn off Fcitx
+            machine.send_key("ctrl-spc")
+            machine.sleep(1)
+
+            ### Redirect typed characters to a file
+            machine.send_chars(" > fcitx_test.out\n")
+            machine.sleep(1)
+            machine.screenshot("terminal_chars")
+
+            ### Verify that file contents are as expected
+            file_content = machine.succeed("cat ${userHome}/fcitx_test.out")
+            assert file_content == "☺一下क\n"
+            ''
+    ;
+  }
+)
diff --git a/nixos/tests/fcitx/profile b/nixos/tests/fcitx/profile
new file mode 100644
index 00000000000..77497a1496b
--- /dev/null
+++ b/nixos/tests/fcitx/profile
@@ -0,0 +1,4 @@
+[Profile]
+IMName=zhengma-large
+EnabledIMList=fcitx-keyboard-us:True,zhengma-large:True,m17n_sa_harvard-kyoto:True
+PreeditStringInClientWindow=False
diff --git a/nixos/tests/fenics.nix b/nixos/tests/fenics.nix
new file mode 100644
index 00000000000..f0a8c32c7cd
--- /dev/null
+++ b/nixos/tests/fenics.nix
@@ -0,0 +1,49 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+  fenicsScript = pkgs.writeScript "poisson.py" ''
+    #!/usr/bin/env python
+    from dolfin import *
+
+    mesh = UnitSquareMesh(4, 4)
+    V = FunctionSpace(mesh, "Lagrange", 1)
+
+    def boundary(x):
+        return x[0] < DOLFIN_EPS or x[0] > 1.0 - DOLFIN_EPS
+
+    u0 = Constant(0.0)
+    bc = DirichletBC(V, u0, boundary)
+
+    u = TrialFunction(V)
+    v = TestFunction(V)
+    f = Expression("10*exp(-(pow(x[0] - 0.5, 2) + pow(x[1] - 0.5, 2)) / 0.02)", degree=2)
+    g = Expression("sin(5*x[0])", degree=2)
+    a = inner(grad(u), grad(v))*dx
+    L = f*v*dx + g*v*ds
+
+    u = Function(V)
+    solve(a == L, u, bc)
+    print(u)
+  '';
+in
+{
+  name = "fenics";
+  meta = {
+    maintainers = with pkgs.lib.maintainers; [ knedlsepp ];
+  };
+
+  nodes = {
+    fenicsnode = { pkgs, ... }: {
+      environment.systemPackages = with pkgs; [
+        gcc
+        (python3.withPackages (ps: with ps; [ fenics ]))
+      ];
+    };
+  };
+  testScript =
+    { nodes, ... }:
+    ''
+      start_all()
+      node1.succeed("${fenicsScript}")
+    '';
+})
diff --git a/nixos/tests/ferm.nix b/nixos/tests/ferm.nix
new file mode 100644
index 00000000000..be43877445e
--- /dev/null
+++ b/nixos/tests/ferm.nix
@@ -0,0 +1,75 @@
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "ferm";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mic92 ];
+  };
+
+  nodes =
+    { client =
+        { pkgs, ... }:
+        with pkgs.lib;
+        {
+          networking = {
+            dhcpcd.enable = false;
+            interfaces.eth1.ipv6.addresses = mkOverride 0 [ { address = "fd00::2"; prefixLength = 64; } ];
+            interfaces.eth1.ipv4.addresses = mkOverride 0 [ { address = "192.168.1.2"; prefixLength = 24; } ];
+          };
+      };
+      server =
+        { pkgs, ... }:
+        with pkgs.lib;
+        {
+          networking = {
+            dhcpcd.enable = false;
+            useNetworkd = true;
+            useDHCP = false;
+            interfaces.eth1.ipv6.addresses = mkOverride 0 [ { address = "fd00::1"; prefixLength = 64; } ];
+            interfaces.eth1.ipv4.addresses = mkOverride 0 [ { address = "192.168.1.1"; prefixLength = 24; } ];
+          };
+
+          services = {
+            ferm.enable = true;
+            ferm.config = ''
+              domain (ip ip6) table filter chain INPUT {
+                interface lo ACCEPT;
+                proto tcp dport 8080 REJECT reject-with tcp-reset;
+              }
+            '';
+            nginx.enable = true;
+            nginx.httpConfig = ''
+              server {
+                listen 80;
+                listen [::]:80;
+                listen 8080;
+                listen [::]:8080;
+
+                location /status { stub_status on; }
+              }
+            '';
+          };
+        };
+    };
+
+  testScript =
+    ''
+      start_all()
+
+      client.wait_for_unit("network-online.target")
+      server.wait_for_unit("network-online.target")
+      server.wait_for_unit("ferm.service")
+      server.wait_for_unit("nginx.service")
+      server.wait_until_succeeds("ss -ntl | grep -q 80")
+
+      with subtest("port 80 is allowed"):
+          client.succeed("curl --fail -g http://192.168.1.1:80/status")
+          client.succeed("curl --fail -g http://[fd00::1]:80/status")
+
+      with subtest("port 8080 is not allowed"):
+          server.succeed("curl --fail -g http://192.168.1.1:8080/status")
+          server.succeed("curl --fail -g http://[fd00::1]:8080/status")
+
+          client.fail("curl --fail -g http://192.168.1.1:8080/status")
+          client.fail("curl --fail -g http://[fd00::1]:8080/status")
+    '';
+})
diff --git a/nixos/tests/firefox.nix b/nixos/tests/firefox.nix
new file mode 100644
index 00000000000..6101fc97356
--- /dev/null
+++ b/nixos/tests/firefox.nix
@@ -0,0 +1,116 @@
+import ./make-test-python.nix ({ pkgs, firefoxPackage, ... }: {
+  name = "firefox";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eelco shlevy ];
+  };
+
+  machine =
+    { pkgs, ... }:
+
+    { imports = [ ./common/x11.nix ];
+      environment.systemPackages = [
+        firefoxPackage
+        pkgs.xdotool
+      ];
+
+      # Create a virtual sound device, with mixing
+      # and all, for recording audio.
+      boot.kernelModules = [ "snd-aloop" ];
+      sound.enable = true;
+      sound.extraConfig = ''
+        pcm.!default {
+          type plug
+          slave.pcm pcm.dmixer
+        }
+        pcm.dmixer {
+          type dmix
+          ipc_key 1
+          slave {
+            pcm "hw:Loopback,0,0"
+            rate 48000
+            periods 128
+            period_time 0
+            period_size 1024
+            buffer_size 8192
+          }
+        }
+        pcm.recorder {
+          type hw
+          card "Loopback"
+          device 1
+          subdevice 0
+        }
+      '';
+
+      systemd.services.audio-recorder = {
+        description = "Record NixOS test audio to /tmp/record.wav";
+        script = "${pkgs.alsa-utils}/bin/arecord -D recorder -f S16_LE -r48000 /tmp/record.wav";
+      };
+
+    };
+
+  testScript = ''
+      from contextlib import contextmanager
+
+
+      @contextmanager
+      def audio_recording(machine: Machine) -> None:
+          """
+          Perform actions while recording the
+          machine audio output.
+          """
+          machine.systemctl("start audio-recorder")
+          yield
+          machine.systemctl("stop audio-recorder")
+
+
+      def wait_for_sound(machine: Machine) -> None:
+          """
+          Wait until any sound has been emitted.
+          """
+          machine.wait_for_file("/tmp/record.wav")
+          while True:
+              # Get at most 2M of the recording
+              machine.execute("tail -c 2M /tmp/record.wav > /tmp/last")
+              # Get the exact size
+              size = int(machine.succeed("stat -c '%s' /tmp/last").strip())
+              # Compare it against /dev/zero using `cmp` (skipping 50B of WAVE header).
+              # If some non-NULL bytes are found it returns 1.
+              status, output = machine.execute(
+                  f"cmp -i 50 -n {size - 50} /tmp/last /dev/zero 2>&1"
+              )
+              if status == 1:
+                  break
+              machine.sleep(2)
+
+
+      machine.wait_for_x()
+
+      with subtest("Wait until Firefox has finished loading the Valgrind docs page"):
+          machine.execute(
+              "xterm -e 'firefox file://${pkgs.valgrind.doc}/share/doc/valgrind/html/index.html' >&2 &"
+          )
+          machine.wait_for_window("Valgrind")
+          machine.sleep(40)
+
+      with subtest("Check whether Firefox can play sound"):
+          with audio_recording(machine):
+              machine.succeed(
+                  "firefox file://${pkgs.sound-theme-freedesktop}/share/sounds/freedesktop/stereo/phone-incoming-call.oga >&2 &"
+              )
+              wait_for_sound(machine)
+          machine.copy_from_vm("/tmp/record.wav")
+
+      with subtest("Close sound test tab"):
+          machine.execute("xdotool key ctrl+w")
+
+      with subtest("Close default browser prompt"):
+          machine.execute("xdotool key space")
+
+      with subtest("Wait until Firefox draws the developer tool panel"):
+          machine.sleep(10)
+          machine.succeed("xwininfo -root -tree | grep Valgrind")
+          machine.screenshot("screen")
+    '';
+
+})
diff --git a/nixos/tests/firejail.nix b/nixos/tests/firejail.nix
new file mode 100644
index 00000000000..6c42c37b281
--- /dev/null
+++ b/nixos/tests/firejail.nix
@@ -0,0 +1,91 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "firejail";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ sgo ];
+  };
+
+  nodes.machine = { ... }: {
+    imports = [ ./common/user-account.nix ];
+
+    programs.firejail = {
+      enable = true;
+      wrappedBinaries = {
+        bash-jailed  = "${pkgs.bash}/bin/bash";
+        bash-jailed2  = {
+          executable = "${pkgs.bash}/bin/bash";
+          extraArgs = [ "--private=~/firejail-home" ];
+        };
+      };
+    };
+
+    systemd.services.setupFirejailTest = {
+      wantedBy = [ "multi-user.target" ];
+      before = [ "multi-user.target" ];
+
+      environment = {
+        HOME = "/home/alice";
+      };
+
+      unitConfig = {
+        type = "oneshot";
+        RemainAfterExit = true;
+        user = "alice";
+      };
+
+      script = ''
+        cd $HOME
+
+        mkdir .password-store && echo s3cret > .password-store/secret
+        mkdir my-secrets && echo s3cret > my-secrets/secret
+
+        echo publ1c > public
+
+        mkdir -p .config/firejail
+        echo 'blacklist ''${HOME}/my-secrets' > .config/firejail/globals.local
+      '';
+    };
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("multi-user.target")
+
+    # Test path acl with wrapper
+    machine.succeed("sudo -u alice bash-jailed -c 'cat ~/public' | grep -q publ1c")
+    machine.fail(
+        "sudo -u alice bash-jailed -c 'cat ~/.password-store/secret' | grep -q s3cret"
+    )
+    machine.fail("sudo -u alice bash-jailed -c 'cat ~/my-secrets/secret' | grep -q s3cret")
+
+    # Test extraArgs
+    machine.succeed("sudo -u alice mkdir /home/alice/firejail-home")
+    machine.succeed("sudo -u alice bash-jailed2 -c 'echo test > /home/alice/foo'")
+    machine.fail("sudo -u alice cat /home/alice/foo")
+    machine.succeed("sudo -u alice cat /home/alice/firejail-home/foo | grep test")
+
+    # Test path acl with firejail executable
+    machine.succeed("sudo -u alice firejail -- bash -c 'cat ~/public' | grep -q publ1c")
+    machine.fail(
+        "sudo -u alice firejail -- bash -c 'cat ~/.password-store/secret' | grep -q s3cret"
+    )
+    machine.fail(
+        "sudo -u alice firejail -- bash -c 'cat ~/my-secrets/secret' | grep -q s3cret"
+    )
+
+    # Disabling profiles
+    machine.succeed(
+        "sudo -u alice bash -c 'firejail --noprofile -- cat ~/.password-store/secret' | grep -q s3cret"
+    )
+
+    # CVE-2020-17367
+    machine.fail(
+        "sudo -u alice firejail --private-tmp id --output=/tmp/vuln1 && cat /tmp/vuln1"
+    )
+
+    # CVE-2020-17368
+    machine.fail(
+        "sudo -u alice firejail --private-tmp --output=/tmp/foo 'bash -c $(id>/tmp/vuln2;echo id)' && cat /tmp/vuln2"
+    )
+  '';
+})
+
diff --git a/nixos/tests/firewall.nix b/nixos/tests/firewall.nix
new file mode 100644
index 00000000000..5c434c1cb6d
--- /dev/null
+++ b/nixos/tests/firewall.nix
@@ -0,0 +1,65 @@
+# Test the firewall module.
+
+import ./make-test-python.nix ( { pkgs, ... } : {
+  name = "firewall";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eelco ];
+  };
+
+  nodes =
+    { walled =
+        { ... }:
+        { networking.firewall.enable = true;
+          networking.firewall.logRefusedPackets = true;
+          services.httpd.enable = true;
+          services.httpd.adminAddr = "foo@example.org";
+        };
+
+      # Dummy configuration to check whether firewall.service will be honored
+      # during system activation. This only needs to be different to the
+      # original walled configuration so that there is a change in the service
+      # file.
+      walled2 =
+        { ... }:
+        { networking.firewall.enable = true;
+          networking.firewall.rejectPackets = true;
+        };
+
+      attacker =
+        { ... }:
+        { services.httpd.enable = true;
+          services.httpd.adminAddr = "foo@example.org";
+          networking.firewall.enable = false;
+        };
+    };
+
+  testScript = { nodes, ... }: let
+    newSystem = nodes.walled2.config.system.build.toplevel;
+  in ''
+    start_all()
+
+    walled.wait_for_unit("firewall")
+    walled.wait_for_unit("httpd")
+    attacker.wait_for_unit("network.target")
+
+    # Local connections should still work.
+    walled.succeed("curl -v http://localhost/ >&2")
+
+    # Connections to the firewalled machine should fail, but ping should succeed.
+    attacker.fail("curl --fail --connect-timeout 2 http://walled/ >&2")
+    attacker.succeed("ping -c 1 walled >&2")
+
+    # Outgoing connections/pings should still work.
+    walled.succeed("curl -v http://attacker/ >&2")
+    walled.succeed("ping -c 1 attacker >&2")
+
+    # If we stop the firewall, then connections should succeed.
+    walled.stop_job("firewall")
+    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"
+    )
+  '';
+})
diff --git a/nixos/tests/fish.nix b/nixos/tests/fish.nix
new file mode 100644
index 00000000000..68fba428439
--- /dev/null
+++ b/nixos/tests/fish.nix
@@ -0,0 +1,24 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "fish";
+
+  machine =
+    { pkgs, ... }:
+
+    {
+      programs.fish.enable = true;
+      environment.systemPackages = with pkgs; [
+        coreutils
+        procps # kill collides with coreutils' to test https://github.com/NixOS/nixpkgs/issues/56432
+      ];
+    };
+
+  testScript =
+    ''
+      start_all()
+      machine.wait_for_file("/etc/fish/generated_completions/coreutils.fish")
+      machine.wait_for_file("/etc/fish/generated_completions/kill.fish")
+      machine.succeed(
+          "fish -ic 'echo $fish_complete_path' | grep -q '/share/fish/completions /etc/fish/generated_completions /root/.local/share/fish/generated_completions$'"
+      )
+    '';
+})
diff --git a/nixos/tests/flannel.nix b/nixos/tests/flannel.nix
new file mode 100644
index 00000000000..7615732c20c
--- /dev/null
+++ b/nixos/tests/flannel.nix
@@ -0,0 +1,57 @@
+import ./make-test-python.nix ({ lib, ...} : {
+  name = "flannel";
+
+  meta = with lib.maintainers; {
+    maintainers = [ offline ];
+  };
+
+  nodes = let
+    flannelConfig = { pkgs, ... } : {
+      services.flannel = {
+        enable = true;
+        backend = {
+          Type = "udp";
+          Port = 8285;
+        };
+        network = "10.1.0.0/16";
+        iface = "eth1";
+        etcd.endpoints = ["http://etcd:2379"];
+      };
+
+      networking.firewall.allowedUDPPorts = [ 8285 ];
+    };
+  in {
+    etcd = { ... }: {
+      services = {
+        etcd = {
+          enable = true;
+          listenClientUrls = ["http://0.0.0.0:2379"]; # requires ip-address for binding
+          listenPeerUrls = ["http://0.0.0.0:2380"]; # requires ip-address for binding
+          advertiseClientUrls = ["http://etcd:2379"];
+          initialAdvertisePeerUrls = ["http://etcd:2379"];
+          initialCluster = ["etcd=http://etcd:2379"];
+        };
+      };
+
+      networking.firewall.allowedTCPPorts = [ 2379 ];
+    };
+
+    node1 = flannelConfig;
+    node2 = flannelConfig;
+  };
+
+  testScript = ''
+    start_all()
+
+    node1.wait_for_unit("flannel.service")
+    node2.wait_for_unit("flannel.service")
+
+    node1.wait_until_succeeds("ip l show dev flannel0")
+    ip1 = node1.succeed("ip -4 addr show flannel0 | grep -oP '(?<=inet).*(?=/)'")
+    node2.wait_until_succeeds("ip l show dev flannel0")
+    ip2 = node2.succeed("ip -4 addr show flannel0 | grep -oP '(?<=inet).*(?=/)'")
+
+    node1.wait_until_succeeds(f"ping -c 1 {ip2}")
+    node2.wait_until_succeeds(f"ping -c 1 {ip1}")
+  '';
+})
diff --git a/nixos/tests/fluentd.nix b/nixos/tests/fluentd.nix
new file mode 100644
index 00000000000..918f2f87db1
--- /dev/null
+++ b/nixos/tests/fluentd.nix
@@ -0,0 +1,49 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "fluentd";
+
+  machine = { pkgs, ... }: {
+    services.fluentd = {
+      enable = true;
+      config = ''
+        <source>
+          @type http
+          port 9880
+        </source>
+
+        <match **>
+          type copy
+          <store>
+            @type file
+            format json
+            path /tmp/fluentd
+            symlink_path /tmp/current-log
+          </store>
+          <store>
+            @type stdout
+          </store>
+        </match>
+      '';
+    };
+  };
+
+  testScript = let
+    testMessage = "an example log message";
+
+    payload = pkgs.writeText "test-message.json" (builtins.toJSON {
+      inherit testMessage;
+    });
+  in ''
+    machine.start()
+    machine.wait_for_unit("fluentd.service")
+    machine.wait_for_open_port(9880)
+
+    machine.succeed(
+        "curl -fsSL -X POST -H 'Content-type: application/json' -d @${payload} http://localhost:9880/test.tag"
+    )
+
+    # blocking flush
+    machine.succeed("systemctl stop fluentd")
+
+    machine.succeed("grep '${testMessage}' /tmp/current-log")
+  '';
+})
diff --git a/nixos/tests/fluidd.nix b/nixos/tests/fluidd.nix
new file mode 100644
index 00000000000..f49a4110d71
--- /dev/null
+++ b/nixos/tests/fluidd.nix
@@ -0,0 +1,21 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "fluidd";
+  meta.maintainers = with maintainers; [ vtuan10 ];
+
+  nodes.machine = { pkgs, ... }: {
+    services.fluidd = {
+      enable = true;
+    };
+  };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("nginx.service")
+    machine.wait_for_open_port(80)
+    machine.succeed("curl -sSfL http://localhost/ | grep 'fluidd'")
+  '';
+})
diff --git a/nixos/tests/fontconfig-default-fonts.nix b/nixos/tests/fontconfig-default-fonts.nix
new file mode 100644
index 00000000000..58d0f6227cc
--- /dev/null
+++ b/nixos/tests/fontconfig-default-fonts.nix
@@ -0,0 +1,32 @@
+import ./make-test-python.nix ({ lib, ... }:
+{
+  name = "fontconfig-default-fonts";
+
+  meta.maintainers = with lib.maintainers; [
+    jtojnar
+  ];
+
+  machine = { config, pkgs, ... }: {
+    fonts.enableDefaultFonts = true; # Background fonts
+    fonts.fonts = with pkgs; [
+      noto-fonts-emoji
+      cantarell-fonts
+      twitter-color-emoji
+      source-code-pro
+      gentium
+    ];
+    fonts.fontconfig.defaultFonts = {
+      serif = [ "Gentium Plus" ];
+      sansSerif = [ "Cantarell" ];
+      monospace = [ "Source Code Pro" ];
+      emoji = [ "Twitter Color Emoji" ];
+    };
+  };
+
+  testScript = ''
+    machine.succeed("fc-match serif | grep '\"Gentium Plus\"'")
+    machine.succeed("fc-match sans-serif | grep '\"Cantarell\"'")
+    machine.succeed("fc-match monospace | grep '\"Source Code Pro\"'")
+    machine.succeed("fc-match emoji | grep '\"Twitter Color Emoji\"'")
+  '';
+})
diff --git a/nixos/tests/freeswitch.nix b/nixos/tests/freeswitch.nix
new file mode 100644
index 00000000000..bcc6a9cb358
--- /dev/null
+++ b/nixos/tests/freeswitch.nix
@@ -0,0 +1,29 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "freeswitch";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ misuzu ];
+  };
+  nodes = {
+    node0 = { config, lib, ... }: {
+      networking.useDHCP = false;
+      networking.interfaces.eth1 = {
+        ipv4.addresses = [
+          {
+            address = "192.168.0.1";
+            prefixLength = 24;
+          }
+        ];
+      };
+      services.freeswitch = {
+        enable = true;
+        enableReload = true;
+        configTemplate = "${config.services.freeswitch.package}/share/freeswitch/conf/minimal";
+      };
+    };
+  };
+  testScript = ''
+    node0.wait_for_unit("freeswitch.service")
+    # Wait for SIP port to be open
+    node0.wait_for_open_port("5060")
+  '';
+})
diff --git a/nixos/tests/frr.nix b/nixos/tests/frr.nix
new file mode 100644
index 00000000000..598d7a7d286
--- /dev/null
+++ b/nixos/tests/frr.nix
@@ -0,0 +1,104 @@
+# This test runs FRR and checks if OSPF routing works.
+#
+# Network topology:
+#   [ client ]--net1--[ router1 ]--net2--[ router2 ]--net3--[ server ]
+#
+# All interfaces are in OSPF Area 0.
+
+import ./make-test-python.nix ({ pkgs, ... }:
+  let
+
+    ifAddr = node: iface: (pkgs.lib.head node.config.networking.interfaces.${iface}.ipv4.addresses).address;
+
+    ospfConf1 = ''
+      router ospf
+        network 192.168.0.0/16 area 0
+    '';
+
+    ospfConf2 = ''
+      interface eth2
+        ip ospf hello-interval 1
+        ip ospf dead-interval 5
+      !
+      router ospf
+        network 192.168.0.0/16 area 0
+    '';
+
+  in
+    {
+      name = "frr";
+
+      meta = with pkgs.lib.maintainers; {
+        maintainers = [ hexa ];
+      };
+
+      nodes = {
+
+        client =
+          { nodes, ... }:
+          {
+            virtualisation.vlans = [ 1 ];
+            networking.defaultGateway = ifAddr nodes.router1 "eth1";
+          };
+
+        router1 =
+          { ... }:
+          {
+            virtualisation.vlans = [ 1 2 ];
+            boot.kernel.sysctl."net.ipv4.ip_forward" = "1";
+            networking.firewall.extraCommands = "iptables -A nixos-fw -i eth2 -p ospfigp -j ACCEPT";
+            services.frr.ospf = {
+              enable = true;
+              config = ospfConf1;
+            };
+
+            specialisation.ospf.configuration = {
+              services.frr.ospf.config = ospfConf2;
+            };
+          };
+
+        router2 =
+          { ... }:
+          {
+            virtualisation.vlans = [ 3 2 ];
+            boot.kernel.sysctl."net.ipv4.ip_forward" = "1";
+            networking.firewall.extraCommands = "iptables -A nixos-fw -i eth2 -p ospfigp -j ACCEPT";
+            services.frr.ospf = {
+              enable = true;
+              config = ospfConf2;
+            };
+          };
+
+        server =
+          { nodes, ... }:
+          {
+            virtualisation.vlans = [ 3 ];
+            networking.defaultGateway = ifAddr nodes.router2 "eth1";
+          };
+      };
+
+      testScript =
+        { nodes, ... }:
+        ''
+          start_all()
+
+          # Wait for the networking to start on all machines
+          for machine in client, router1, router2, server:
+              machine.wait_for_unit("network.target")
+
+          with subtest("Wait for Zebra and OSPFD"):
+              for gw in router1, router2:
+                  gw.wait_for_unit("zebra")
+                  gw.wait_for_unit("ospfd")
+
+          router1.succeed("${nodes.router1.config.system.build.toplevel}/specialisation/ospf/bin/switch-to-configuration test >&2")
+
+          with subtest("Wait for OSPF to form adjacencies"):
+              for gw in router1, router2:
+                  gw.wait_until_succeeds("vtysh -c 'show ip ospf neighbor' | grep Full")
+                  gw.wait_until_succeeds("vtysh -c 'show ip route' | grep '^O>'")
+
+          with subtest("Test ICMP"):
+              client.wait_until_succeeds("ping -c 3 server >&2")
+        '';
+    })
diff --git a/nixos/tests/fsck.nix b/nixos/tests/fsck.nix
new file mode 100644
index 00000000000..5453f3bc48b
--- /dev/null
+++ b/nixos/tests/fsck.nix
@@ -0,0 +1,31 @@
+import ./make-test-python.nix {
+  name = "fsck";
+
+  machine = { lib, ... }: {
+    virtualisation.emptyDiskImages = [ 1 ];
+
+    virtualisation.fileSystems = {
+      "/mnt" = {
+        device = "/dev/vdb";
+        fsType = "ext4";
+        autoFormat = true;
+      };
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("default.target")
+
+    with subtest("root fs is fsckd"):
+        machine.succeed("journalctl -b | grep 'fsck.ext4.*/dev/vda'")
+
+    with subtest("mnt fs is fsckd"):
+        machine.succeed("journalctl -b | grep 'fsck.*/dev/vdb.*clean'")
+        machine.succeed(
+            "grep 'Requires=systemd-fsck@dev-vdb.service' /run/systemd/generator/mnt.mount"
+        )
+        machine.succeed(
+            "grep 'After=systemd-fsck@dev-vdb.service' /run/systemd/generator/mnt.mount"
+        )
+  '';
+}
diff --git a/nixos/tests/ft2-clone.nix b/nixos/tests/ft2-clone.nix
new file mode 100644
index 00000000000..71eda43e2b2
--- /dev/null
+++ b/nixos/tests/ft2-clone.nix
@@ -0,0 +1,35 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "ft2-clone";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fgaz ];
+  };
+
+  machine = { config, pkgs, ... }: {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    sound.enable = true;
+    environment.systemPackages = [ pkgs.ft2-clone ];
+  };
+
+  enableOCR = true;
+
+  testScript =
+    ''
+      machine.wait_for_x()
+      # Add a dummy sound card, or the program won't start
+      machine.execute("modprobe snd-dummy")
+
+      machine.execute("ft2-clone >&2 &")
+
+      machine.wait_for_window(r"Fasttracker")
+      machine.sleep(5)
+      # One of the few words that actually get recognized
+      if "Songlen" not in machine.get_screen_text():
+          raise Exception("Program did not start successfully")
+      machine.screenshot("screen")
+    '';
+})
+
diff --git a/nixos/tests/gerrit.nix b/nixos/tests/gerrit.nix
new file mode 100644
index 00000000000..8ae9e89cf6b
--- /dev/null
+++ b/nixos/tests/gerrit.nix
@@ -0,0 +1,54 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+  lfs = pkgs.fetchurl {
+    url = "https://gerrit-ci.gerritforge.com/job/plugin-lfs-bazel-master/90/artifact/bazel-bin/plugins/lfs/lfs.jar";
+    sha256 = "023b0kd8djm3cn1lf1xl67yv3j12yl8bxccn42lkfmwxjwjfqw6h";
+  };
+
+in {
+  name = "gerrit";
+
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ flokli zimbatm ];
+  };
+
+  nodes = {
+    server =
+      { config, pkgs, ... }: {
+        networking.firewall.allowedTCPPorts = [ 80 2222 ];
+
+
+        services.gerrit = {
+          enable = true;
+          serverId = "aa76c84b-50b0-4711-a0a0-1ee30e45bbd0";
+          listenAddress = "[::]:80";
+          jvmHeapLimit = "1g";
+
+          plugins = [ lfs ];
+          builtinPlugins = [ "hooks" "webhooks" ];
+          settings = {
+            gerrit.canonicalWebUrl = "http://server";
+            lfs.plugin = "lfs";
+            plugins.allowRemoteAdmin = true;
+            sshd.listenAddress = "[::]:2222";
+            sshd.advertisedAddress = "[::]:2222";
+          };
+        };
+      };
+
+    client =
+      { ... }: {
+      };
+  };
+
+  testScript = ''
+    start_all()
+    server.wait_for_unit("gerrit.service")
+    server.wait_for_open_port(80)
+    client.succeed("curl http://server")
+
+    server.wait_for_open_port(2222)
+    client.succeed("nc -z server 2222")
+  '';
+})
diff --git a/nixos/tests/geth.nix b/nixos/tests/geth.nix
new file mode 100644
index 00000000000..af8230553bb
--- /dev/null
+++ b/nixos/tests/geth.nix
@@ -0,0 +1,41 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "geth";
+  meta = with pkgs.lib; {
+    maintainers = with maintainers; [bachp ];
+  };
+
+  machine = { ... }: {
+    services.geth."mainnet" = {
+      enable = true;
+      http = {
+        enable = true;
+      };
+    };
+    services.geth."testnet" = {
+      enable = true;
+      port = 30304;
+      network = "goerli";
+      http = {
+        enable = true;
+        port = 18545;
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("geth-mainnet.service")
+    machine.wait_for_unit("geth-testnet.service")
+    machine.wait_for_open_port(8545)
+    machine.wait_for_open_port(18545)
+
+    machine.succeed(
+        'geth attach --exec eth.blockNumber http://localhost:8545 | grep \'^0$\' '
+    )
+
+    machine.succeed(
+        'geth attach --exec "eth.chainId()" http://localhost:18545 | grep \'"0x5"\' '
+    )
+  '';
+})
diff --git a/nixos/tests/ghostunnel.nix b/nixos/tests/ghostunnel.nix
new file mode 100644
index 00000000000..8bea6485402
--- /dev/null
+++ b/nixos/tests/ghostunnel.nix
@@ -0,0 +1,103 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  nodes = {
+    backend = { pkgs, ... }: {
+      services.nginx.enable = true;
+      services.nginx.virtualHosts."backend".root = pkgs.runCommand "webroot" {} ''
+        mkdir $out
+        echo hi >$out/hi.txt
+      '';
+      networking.firewall.allowedTCPPorts = [ 80 ];
+    };
+    service = { ... }: {
+      services.ghostunnel.enable = true;
+      services.ghostunnel.servers."plain-old" = {
+        listen = "0.0.0.0:443";
+        cert = "/root/service-cert.pem";
+        key = "/root/service-key.pem";
+        disableAuthentication = true;
+        target = "backend:80";
+        unsafeTarget = true;
+      };
+      services.ghostunnel.servers."client-cert" = {
+        listen = "0.0.0.0:1443";
+        cert = "/root/service-cert.pem";
+        key = "/root/service-key.pem";
+        cacert = "/root/ca.pem";
+        target = "backend:80";
+        allowCN = ["client"];
+        unsafeTarget = true;
+      };
+      networking.firewall.allowedTCPPorts = [ 443 1443 ];
+    };
+    client = { pkgs, ... }: {
+      environment.systemPackages = [
+        pkgs.curl
+      ];
+    };
+  };
+
+  testScript = ''
+
+    # prepare certificates
+
+    def cmd(command):
+      print(f"+{command}")
+      r = os.system(command)
+      if r != 0:
+        raise Exception(f"Command {command} failed with exit code {r}")
+
+    # Create CA
+    cmd("${pkgs.openssl}/bin/openssl genrsa -out ca-key.pem 4096")
+    cmd("${pkgs.openssl}/bin/openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -subj '/C=NL/ST=Zuid-Holland/L=The Hague/O=Stevige Balken en Planken B.V./OU=OpSec/CN=Certificate Authority' -out ca.pem")
+
+    # Create service
+    cmd("${pkgs.openssl}/bin/openssl genrsa -out service-key.pem 4096")
+    cmd("${pkgs.openssl}/bin/openssl req -subj '/CN=service' -sha256 -new -key service-key.pem -out service.csr")
+    cmd("echo subjectAltName = DNS:service,IP:127.0.0.1 >> extfile.cnf")
+    cmd("echo extendedKeyUsage = serverAuth >> extfile.cnf")
+    cmd("${pkgs.openssl}/bin/openssl x509 -req -days 365 -sha256 -in service.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out service-cert.pem -extfile extfile.cnf")
+
+    # Create client
+    cmd("${pkgs.openssl}/bin/openssl genrsa -out client-key.pem 4096")
+    cmd("${pkgs.openssl}/bin/openssl req -subj '/CN=client' -new -key client-key.pem -out client.csr")
+    cmd("echo extendedKeyUsage = clientAuth > extfile-client.cnf")
+    cmd("${pkgs.openssl}/bin/openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -extfile extfile-client.cnf")
+
+    cmd("ls -al")
+
+    start_all()
+
+    # Configuration
+    service.copy_from_host("ca.pem", "/root/ca.pem")
+    service.copy_from_host("service-cert.pem", "/root/service-cert.pem")
+    service.copy_from_host("service-key.pem", "/root/service-key.pem")
+    client.copy_from_host("ca.pem", "/root/ca.pem")
+    client.copy_from_host("service-cert.pem", "/root/service-cert.pem")
+    client.copy_from_host("client-cert.pem", "/root/client-cert.pem")
+    client.copy_from_host("client-key.pem", "/root/client-key.pem")
+
+    backend.wait_for_unit("nginx.service")
+    service.wait_for_unit("multi-user.target")
+    service.wait_for_unit("multi-user.target")
+    client.wait_for_unit("multi-user.target")
+
+    # Check assumptions before the real test
+    client.succeed("bash -c 'diff <(curl -v --no-progress-meter http://backend/hi.txt) <(echo hi)'")
+
+    # Plain old simple TLS can connect, ignoring cert
+    client.succeed("bash -c 'diff <(curl -v --no-progress-meter --insecure https://service/hi.txt) <(echo hi)'")
+
+    # Plain old simple TLS provides correct signature with its cert
+    client.succeed("bash -c 'diff <(curl -v --no-progress-meter --cacert /root/ca.pem https://service/hi.txt) <(echo hi)'")
+
+    # Client can authenticate with certificate
+    client.succeed("bash -c 'diff <(curl -v --no-progress-meter --cert /root/client-cert.pem --key /root/client-key.pem --cacert /root/ca.pem https://service:1443/hi.txt) <(echo hi)'")
+
+    # Client must authenticate with certificate
+    client.fail("bash -c 'diff <(curl -v --no-progress-meter --cacert /root/ca.pem https://service:1443/hi.txt) <(echo hi)'")
+  '';
+
+  meta.maintainers = with pkgs.lib.maintainers; [
+    roberth
+  ];
+})
diff --git a/nixos/tests/git/hub.nix b/nixos/tests/git/hub.nix
new file mode 100644
index 00000000000..4f3189861a0
--- /dev/null
+++ b/nixos/tests/git/hub.nix
@@ -0,0 +1,17 @@
+import ../make-test-python.nix ({ pkgs, ...} : {
+  name = "hub";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ nequissimus ];
+  };
+
+  nodes.hub = { pkgs, ... }:
+    {
+      environment.systemPackages = [ pkgs.hub ];
+    };
+
+  testScript =
+    ''
+      assert "git version ${pkgs.git.version}\nhub version ${pkgs.hub.version}\n" in hub.succeed("hub version")
+      assert "These GitHub commands are provided by hub" in hub.succeed("hub help")
+    '';
+})
diff --git a/nixos/tests/gitdaemon.nix b/nixos/tests/gitdaemon.nix
new file mode 100644
index 00000000000..bb07b6e97b7
--- /dev/null
+++ b/nixos/tests/gitdaemon.nix
@@ -0,0 +1,71 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+  hashes = pkgs.writeText "hashes" ''
+    b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c  /project/bar
+  '';
+in {
+  name = "gitdaemon";
+
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ tilpner ];
+  };
+
+  nodes = {
+    server =
+      { config, ... }: {
+        networking.firewall.allowedTCPPorts = [ config.services.gitDaemon.port ];
+
+        environment.systemPackages = [ pkgs.git ];
+
+        systemd.tmpfiles.rules = [
+          # type path mode user group age arg
+          " d    /git 0755 root root  -   -"
+        ];
+
+        services.gitDaemon = {
+          enable = true;
+          basePath = "/git";
+        };
+      };
+
+    client =
+      { pkgs, ... }: {
+        environment.systemPackages = [ pkgs.git ];
+      };
+  };
+
+  testScript = ''
+    start_all()
+
+    with subtest("create project.git"):
+        server.succeed(
+            "git init --bare /git/project.git",
+            "touch /git/project.git/git-daemon-export-ok",
+        )
+
+    with subtest("add file to project.git"):
+        server.succeed(
+            "git clone /git/project.git /project",
+            "echo foo > /project/bar",
+            "git config --global user.email 'you@example.com'",
+            "git config --global user.name 'Your Name'",
+            "git -C /project add bar",
+            "git -C /project commit -m 'quux'",
+            "git -C /project push",
+            "rm -r /project",
+        )
+
+    with subtest("git daemon starts"):
+        server.wait_for_unit("git-daemon.service")
+
+    server.wait_for_unit("network-online.target")
+    client.wait_for_unit("network-online.target")
+
+    with subtest("client can clone project.git"):
+        client.succeed(
+            "git clone git://server/project.git /project",
+            "sha256sum -c ${hashes}",
+        )
+  '';
+})
diff --git a/nixos/tests/gitea.nix b/nixos/tests/gitea.nix
new file mode 100644
index 00000000000..037fc7b31bf
--- /dev/null
+++ b/nixos/tests/gitea.nix
@@ -0,0 +1,110 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  supportedDbTypes = [ "mysql" "postgres" "sqlite3" ];
+  makeGiteaTest = type: nameValuePair type (makeTest {
+    name = "gitea-${type}";
+    meta.maintainers = with maintainers; [ aanderse kolaente ma27 ];
+
+    nodes = {
+      server = { config, pkgs, ... }: {
+        virtualisation.memorySize = 2048;
+        services.gitea = {
+          enable = true;
+          database = { inherit type; };
+          disableRegistration = true;
+        };
+        environment.systemPackages = [ pkgs.gitea pkgs.jq ];
+        services.openssh.enable = true;
+      };
+      client1 = { config, pkgs, ... }: {
+        environment.systemPackages = [ pkgs.git ];
+      };
+      client2 = { config, pkgs, ... }: {
+        environment.systemPackages = [ pkgs.git ];
+      };
+    };
+
+    testScript = let
+      inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
+    in ''
+      GIT_SSH_COMMAND = "ssh -i $HOME/.ssh/privk -o StrictHostKeyChecking=no"
+      REPO = "gitea@server:test/repo"
+      PRIVK = "${snakeOilPrivateKey}"
+
+      start_all()
+
+      client1.succeed("mkdir /tmp/repo")
+      client1.succeed("mkdir -p $HOME/.ssh")
+      client1.succeed(f"cat {PRIVK} > $HOME/.ssh/privk")
+      client1.succeed("chmod 0400 $HOME/.ssh/privk")
+      client1.succeed("git -C /tmp/repo init")
+      client1.succeed("echo hello world > /tmp/repo/testfile")
+      client1.succeed("git -C /tmp/repo add .")
+      client1.succeed("git config --global user.email test@localhost")
+      client1.succeed("git config --global user.name test")
+      client1.succeed("git -C /tmp/repo commit -m 'Initial import'")
+      client1.succeed(f"git -C /tmp/repo remote add origin {REPO}")
+
+      server.wait_for_unit("gitea.service")
+      server.wait_for_open_port(3000)
+      server.succeed("curl --fail http://localhost:3000/")
+
+      server.succeed(
+          "curl --fail http://localhost:3000/user/sign_up | grep 'Registration is disabled. "
+          + "Please contact your site administrator.'"
+      )
+      server.succeed(
+          "su -l gitea -c 'GITEA_WORK_DIR=/var/lib/gitea gitea admin user create "
+          + "--username test --password totallysafe --email test@localhost'"
+      )
+
+      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"
+      )
+
+      server.succeed(
+          "curl --fail -X POST http://localhost:3000/api/v1/user/repos "
+          + "-H 'Accept: application/json' -H 'Content-Type: application/json' "
+          + f"-H 'Authorization: token {api_token}'"
+          + ' -d \'{"auto_init":false, "description":"string", "license":"mit", "name":"repo", "private":false}\'''
+      )
+
+      server.succeed(
+          "curl --fail -X POST http://localhost:3000/api/v1/user/keys "
+          + "-H 'Accept: application/json' -H 'Content-Type: application/json' "
+          + f"-H 'Authorization: token {api_token}'"
+          + ' -d \'{"key":"${snakeOilPublicKey}","read_only":true,"title":"SSH"}\'''
+      )
+
+      client1.succeed(
+          f"GIT_SSH_COMMAND='{GIT_SSH_COMMAND}' git -C /tmp/repo push origin master"
+      )
+
+      client2.succeed("mkdir -p $HOME/.ssh")
+      client2.succeed(f"cat {PRIVK} > $HOME/.ssh/privk")
+      client2.succeed("chmod 0400 $HOME/.ssh/privk")
+      client2.succeed(f"GIT_SSH_COMMAND='{GIT_SSH_COMMAND}' git clone {REPO}")
+      client2.succeed('test "$(cat repo/testfile | xargs echo -n)" = "hello world"')
+
+      server.succeed(
+          'test "$(curl http://localhost:3000/api/v1/repos/test/repo/commits '
+          + '-H "Accept: application/json" | jq length)" = "1"'
+      )
+
+      client1.shutdown()
+      client2.shutdown()
+      server.shutdown()
+    '';
+  });
+in
+
+listToAttrs (map makeGiteaTest supportedDbTypes)
diff --git a/nixos/tests/gitlab.nix b/nixos/tests/gitlab.nix
new file mode 100644
index 00000000000..dc3b889c8e8
--- /dev/null
+++ b/nixos/tests/gitlab.nix
@@ -0,0 +1,159 @@
+# This test runs gitlab and checks if it works
+
+let
+  initialRootPassword = "notproduction";
+in
+import ./make-test-python.nix ({ pkgs, lib, ...} : with lib; {
+  name = "gitlab";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ globin ];
+  };
+
+  nodes = {
+    gitlab = { ... }: {
+      imports = [ common/user-account.nix ];
+
+      virtualisation.memorySize = if pkgs.stdenv.is64bit then 4096 else 2047;
+      virtualisation.cores = 4;
+      virtualisation.useNixStoreImage = true;
+      systemd.services.gitlab.serviceConfig.Restart = mkForce "no";
+      systemd.services.gitlab-workhorse.serviceConfig.Restart = mkForce "no";
+      systemd.services.gitaly.serviceConfig.Restart = mkForce "no";
+      systemd.services.gitlab-sidekiq.serviceConfig.Restart = mkForce "no";
+
+      services.nginx = {
+        enable = true;
+        recommendedProxySettings = true;
+        virtualHosts = {
+          localhost = {
+            locations."/".proxyPass = "http://unix:/run/gitlab/gitlab-workhorse.socket";
+          };
+        };
+      };
+
+      services.dovecot2 = {
+        enable = true;
+        enableImap = true;
+      };
+
+      systemd.services.gitlab-backup.environment.BACKUP = "dump";
+
+      services.gitlab = {
+        enable = true;
+        databasePasswordFile = pkgs.writeText "dbPassword" "xo0daiF4";
+        initialRootPasswordFile = pkgs.writeText "rootPassword" initialRootPassword;
+        smtp.enable = true;
+        extraConfig = {
+          incoming_email = {
+            enabled = true;
+            mailbox = "inbox";
+            address = "alice@localhost";
+            user = "alice";
+            password = "foobar";
+            host = "localhost";
+            port = 143;
+          };
+          # https://github.com/NixOS/nixpkgs/issues/132295
+          # pages = {
+          #   enabled = true;
+          #   host = "localhost";
+          # };
+        };
+        secrets = {
+          secretFile = pkgs.writeText "secret" "Aig5zaic";
+          otpFile = pkgs.writeText "otpsecret" "Riew9mue";
+          dbFile = pkgs.writeText "dbsecret" "we2quaeZ";
+          jwsFile = pkgs.runCommand "oidcKeyBase" {} "${pkgs.openssl}/bin/openssl genrsa 2048 > $out";
+        };
+      };
+    };
+  };
+
+  testScript = { nodes, ... }:
+    let
+      auth = pkgs.writeText "auth.json" (builtins.toJSON {
+        grant_type = "password";
+        username = "root";
+        password = initialRootPassword;
+      });
+
+      createProject = pkgs.writeText "create-project.json" (builtins.toJSON {
+        name = "test";
+      });
+
+      putFile = pkgs.writeText "put-file.json" (builtins.toJSON {
+        branch = "master";
+        author_email = "author@example.com";
+        author_name = "Firstname Lastname";
+        content = "some content";
+        commit_message = "create a new file";
+      });
+
+      # Wait for all GitLab services to be fully started.
+      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-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")
+      '';
+
+      # The actual test of GitLab. Only push data to GitLab if
+      # `doSetup` is is true.
+      test = doSetup: ''
+        gitlab.succeed(
+            "curl -isSf http://gitlab | grep -i location | grep http://gitlab/users/sign_in"
+        )
+        gitlab.succeed(
+            "${pkgs.sudo}/bin/sudo -u gitlab -H gitlab-rake gitlab:check 1>&2"
+        )
+        gitlab.succeed(
+            "echo \"Authorization: Bearer \$(curl -X POST -H 'Content-Type: application/json' -d @${auth} http://gitlab/oauth/token | ${pkgs.jq}/bin/jq -r '.access_token')\" >/tmp/headers"
+        )
+      '' + optionalString doSetup ''
+        gitlab.succeed(
+            "curl -X POST -H 'Content-Type: application/json' -H @/tmp/headers -d @${createProject} http://gitlab/api/v4/projects"
+        )
+        gitlab.succeed(
+            "curl -X POST -H 'Content-Type: application/json' -H @/tmp/headers -d @${putFile} http://gitlab/api/v4/projects/1/repository/files/some-file.txt"
+        )
+      '' + ''
+        gitlab.succeed(
+            "curl -H @/tmp/headers http://gitlab/api/v4/projects/1/repository/archive.tar.gz > /tmp/archive.tar.gz"
+        )
+        gitlab.succeed(
+            "curl -H @/tmp/headers http://gitlab/api/v4/projects/1/repository/archive.tar.bz2 > /tmp/archive.tar.bz2"
+        )
+        gitlab.succeed("test -s /tmp/archive.tar.gz")
+        gitlab.succeed("test -s /tmp/archive.tar.bz2")
+      '';
+
+  in ''
+      gitlab.start()
+    ''
+    + waitForServices
+    + test true
+    + ''
+      gitlab.systemctl("start gitlab-backup.service")
+      gitlab.wait_for_unit("gitlab-backup.service")
+      gitlab.wait_for_file("${nodes.gitlab.config.services.gitlab.statePath}/backup/dump_gitlab_backup.tar")
+      gitlab.systemctl("stop postgresql.service gitlab.target")
+      gitlab.succeed(
+          "find ${nodes.gitlab.config.services.gitlab.statePath} -mindepth 1 -maxdepth 1 -not -name backup -execdir rm -r {} +"
+      )
+      gitlab.succeed("systemd-tmpfiles --create")
+      gitlab.succeed("rm -rf ${nodes.gitlab.config.services.postgresql.dataDir}")
+      gitlab.systemctl("start gitlab-config.service gitaly.service gitlab-postgresql.service")
+      gitlab.wait_for_file("${nodes.gitlab.config.services.gitlab.statePath}/tmp/sockets/gitaly.socket")
+      gitlab.succeed(
+          "sudo -u gitlab -H gitlab-rake gitlab:backup:restore RAILS_ENV=production BACKUP=dump force=yes"
+      )
+      gitlab.systemctl("start gitlab.target")
+    ''
+    + waitForServices
+    + test false;
+})
diff --git a/nixos/tests/gitolite-fcgiwrap.nix b/nixos/tests/gitolite-fcgiwrap.nix
new file mode 100644
index 00000000000..38f8d5c883f
--- /dev/null
+++ b/nixos/tests/gitolite-fcgiwrap.nix
@@ -0,0 +1,93 @@
+import ./make-test-python.nix (
+  { pkgs, ... }:
+
+    let
+      user = "gitolite-admin";
+      password = "some_password";
+
+      # not used but needed to setup gitolite
+      adminPublicKey = ''
+        ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO7urFhAA90BTpGuEHeWWTY3W/g9PBxXNxfWhfbrm4Le root@client
+      '';
+    in
+      {
+        name = "gitolite-fcgiwrap";
+
+        meta = with pkgs.lib.maintainers; {
+          maintainers = [ bbigras ];
+        };
+
+        nodes = {
+
+          server =
+            { ... }:
+              {
+                networking.firewall.allowedTCPPorts = [ 80 ];
+
+                services.fcgiwrap.enable = true;
+                services.gitolite = {
+                  enable = true;
+                  adminPubkey = adminPublicKey;
+                };
+
+                services.nginx = {
+                  enable = true;
+                  recommendedProxySettings = true;
+                  virtualHosts."server".locations."/git".extraConfig = ''
+                    # turn off gzip as git objects are already well compressed
+                    gzip off;
+
+                    # use file based basic authentication
+                    auth_basic "Git Repository Authentication";
+                    auth_basic_user_file /etc/gitolite/htpasswd;
+
+                    # common FastCGI parameters are required
+                    include ${config.services.nginx.package}/conf/fastcgi_params;
+
+                    # strip the CGI program prefix
+                    fastcgi_split_path_info ^(/git)(.*)$;
+                    fastcgi_param PATH_INFO $fastcgi_path_info;
+
+                    # pass authenticated user login(mandatory) to Gitolite
+                    fastcgi_param REMOTE_USER $remote_user;
+
+                    # pass git repository root directory and hosting user directory
+                    # these env variables can be set in a wrapper script
+                    fastcgi_param GIT_HTTP_EXPORT_ALL "";
+                    fastcgi_param GIT_PROJECT_ROOT /var/lib/gitolite/repositories;
+                    fastcgi_param GITOLITE_HTTP_HOME /var/lib/gitolite;
+                    fastcgi_param SCRIPT_FILENAME ${pkgs.gitolite}/bin/gitolite-shell;
+
+                    # use Unix domain socket or inet socket
+                    fastcgi_pass unix:/run/fcgiwrap.sock;
+                  '';
+                };
+
+                # WARNING: DON'T DO THIS IN PRODUCTION!
+                # This puts unhashed secrets directly into the Nix store for ease of testing.
+                environment.etc."gitolite/htpasswd".source = pkgs.runCommand "htpasswd" {} ''
+                  ${pkgs.apacheHttpd}/bin/htpasswd -bc "$out" ${user} ${password}
+                '';
+              };
+
+          client =
+            { pkgs, ... }:
+              {
+                environment.systemPackages = [ pkgs.git ];
+              };
+        };
+
+        testScript = ''
+          start_all()
+
+          server.wait_for_unit("gitolite-init.service")
+          server.wait_for_unit("nginx.service")
+          server.wait_for_file("/run/fcgiwrap.sock")
+
+          client.wait_for_unit("multi-user.target")
+          client.succeed(
+              "git clone http://${user}:${password}@server/git/gitolite-admin.git"
+          )
+        '';
+      }
+)
diff --git a/nixos/tests/gitolite.nix b/nixos/tests/gitolite.nix
new file mode 100644
index 00000000000..128677cebde
--- /dev/null
+++ b/nixos/tests/gitolite.nix
@@ -0,0 +1,138 @@
+import ./make-test-python.nix ({ pkgs, ...}:
+
+let
+  adminPrivateKey = pkgs.writeText "id_ed25519" ''
+    -----BEGIN OPENSSH PRIVATE KEY-----
+    b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+    QyNTUxOQAAACDu7qxYQAPdAU6RrhB3llk2N1v4PTwcVzcX1oX265uC3gAAAJBJiYxDSYmM
+    QwAAAAtzc2gtZWQyNTUxOQAAACDu7qxYQAPdAU6RrhB3llk2N1v4PTwcVzcX1oX265uC3g
+    AAAEDE1W6vMwSEUcF1r7Hyypm/+sCOoDmKZgPxi3WOa1mD2u7urFhAA90BTpGuEHeWWTY3
+    W/g9PBxXNxfWhfbrm4LeAAAACGJmb0BtaW5pAQIDBAU=
+    -----END OPENSSH PRIVATE KEY-----
+  '';
+
+  adminPublicKey = ''
+    ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO7urFhAA90BTpGuEHeWWTY3W/g9PBxXNxfWhfbrm4Le root@client
+  '';
+
+  alicePrivateKey = pkgs.writeText "id_ed25519" ''
+    -----BEGIN OPENSSH PRIVATE KEY-----
+    b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+    QyNTUxOQAAACBbeWvHh/AWGWI6EIc1xlSihyXtacNQ9KeztlW/VUy8wQAAAJAwVQ5VMFUO
+    VQAAAAtzc2gtZWQyNTUxOQAAACBbeWvHh/AWGWI6EIc1xlSihyXtacNQ9KeztlW/VUy8wQ
+    AAAEB7lbfkkdkJoE+4TKHPdPQWBKLSx+J54Eg8DaTr+3KoSlt5a8eH8BYZYjoQhzXGVKKH
+    Je1pw1D0p7O2Vb9VTLzBAAAACGJmb0BtaW5pAQIDBAU=
+    -----END OPENSSH PRIVATE KEY-----
+  '';
+
+  alicePublicKey = pkgs.writeText "id_ed25519.pub" ''
+    ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFt5a8eH8BYZYjoQhzXGVKKHJe1pw1D0p7O2Vb9VTLzB alice@client
+  '';
+
+  bobPrivateKey = pkgs.writeText "id_ed25519" ''
+    -----BEGIN OPENSSH PRIVATE KEY-----
+    b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+    QyNTUxOQAAACCWTaJ1D9Xjxy6759FvQ9oXTes1lmWBciXPkEeqTikBMAAAAJDQBmNV0AZj
+    VQAAAAtzc2gtZWQyNTUxOQAAACCWTaJ1D9Xjxy6759FvQ9oXTes1lmWBciXPkEeqTikBMA
+    AAAEDM1IYYFUwk/IVxauha9kuR6bbRtT3gZ6ZA0GLb9txb/pZNonUP1ePHLrvn0W9D2hdN
+    6zWWZYFyJc+QR6pOKQEwAAAACGJmb0BtaW5pAQIDBAU=
+    -----END OPENSSH PRIVATE KEY-----
+  '';
+
+  bobPublicKey = pkgs.writeText "id_ed25519.pub" ''
+    ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJZNonUP1ePHLrvn0W9D2hdN6zWWZYFyJc+QR6pOKQEw bob@client
+  '';
+
+  gitoliteAdminConfSnippet = pkgs.writeText "gitolite-admin-conf-snippet" ''
+    repo alice-project
+        RW+     =   alice
+  '';
+in
+{
+  name = "gitolite";
+
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ bjornfor ];
+  };
+
+  nodes = {
+
+    server =
+      { ... }:
+      {
+        services.gitolite = {
+          enable = true;
+          adminPubkey = adminPublicKey;
+        };
+        services.openssh.enable = true;
+      };
+
+    client =
+      { pkgs, ... }:
+      {
+        environment.systemPackages = [ pkgs.git ];
+        programs.ssh.extraConfig = ''
+          Host *
+            UserKnownHostsFile /dev/null
+            StrictHostKeyChecking no
+            # there's nobody around that can input password
+            PreferredAuthentications publickey
+        '';
+        users.users.alice = { isNormalUser = true; };
+        users.users.bob = { isNormalUser = true; };
+      };
+
+  };
+
+  testScript = ''
+    start_all()
+
+    with subtest("can setup ssh keys on system"):
+        client.succeed(
+            "mkdir -p ~root/.ssh",
+            "cp ${adminPrivateKey} ~root/.ssh/id_ed25519",
+            "chmod 600 ~root/.ssh/id_ed25519",
+        )
+        client.succeed(
+            "sudo -u alice mkdir -p ~alice/.ssh",
+            "sudo -u alice cp ${alicePrivateKey} ~alice/.ssh/id_ed25519",
+            "sudo -u alice chmod 600 ~alice/.ssh/id_ed25519",
+        )
+        client.succeed(
+            "sudo -u bob mkdir -p ~bob/.ssh",
+            "sudo -u bob cp ${bobPrivateKey} ~bob/.ssh/id_ed25519",
+            "sudo -u bob chmod 600 ~bob/.ssh/id_ed25519",
+        )
+
+    with subtest("gitolite server starts"):
+        server.wait_for_unit("gitolite-init.service")
+        server.wait_for_unit("sshd.service")
+        client.succeed("ssh gitolite@server info")
+
+    with subtest("admin can clone and configure gitolite-admin.git"):
+        client.succeed(
+            "git clone gitolite@server:gitolite-admin.git",
+            "git config --global user.name 'System Administrator'",
+            "git config --global user.email root\@domain.example",
+            "cp ${alicePublicKey} gitolite-admin/keydir/alice.pub",
+            "cp ${bobPublicKey} gitolite-admin/keydir/bob.pub",
+            "(cd gitolite-admin && git add . && git commit -m 'Add keys for alice, bob' && git push)",
+            "cat ${gitoliteAdminConfSnippet} >> gitolite-admin/conf/gitolite.conf",
+            "(cd gitolite-admin && git add . && git commit -m 'Add repo for alice' && git push)",
+        )
+
+    with subtest("non-admins cannot clone gitolite-admin.git"):
+        client.fail("sudo -i -u alice git clone gitolite@server:gitolite-admin.git")
+        client.fail("sudo -i -u bob git clone gitolite@server:gitolite-admin.git")
+
+    with subtest("non-admins can clone testing.git"):
+        client.succeed("sudo -i -u alice git clone gitolite@server:testing.git")
+        client.succeed("sudo -i -u bob git clone gitolite@server:testing.git")
+
+    with subtest("alice can clone alice-project.git"):
+        client.succeed("sudo -i -u alice git clone gitolite@server:alice-project.git")
+
+    with subtest("bob cannot clone alice-project.git"):
+        client.fail("sudo -i -u bob git clone gitolite@server:alice-project.git")
+  '';
+})
diff --git a/nixos/tests/glusterfs.nix b/nixos/tests/glusterfs.nix
new file mode 100644
index 00000000000..ef09264a021
--- /dev/null
+++ b/nixos/tests/glusterfs.nix
@@ -0,0 +1,68 @@
+import ./make-test-python.nix ({pkgs, lib, ...}:
+
+let
+  client = { pkgs, ... } : {
+    environment.systemPackages = [ pkgs.glusterfs ];
+    virtualisation.fileSystems =
+      { "/gluster" =
+          { device = "server1:/gv0";
+            fsType = "glusterfs";
+          };
+      };
+  };
+
+  server = { pkgs, ... } : {
+    networking.firewall.enable = false;
+    services.glusterfs.enable = true;
+
+    # create a mount point for the volume
+    boot.initrd.postDeviceCommands = ''
+      ${pkgs.e2fsprogs}/bin/mkfs.ext4 -L data /dev/vdb
+    '';
+
+    virtualisation.emptyDiskImages = [ 1024 ];
+
+    virtualisation.fileSystems =
+      { "/data" =
+          { device = "/dev/disk/by-label/data";
+            fsType = "ext4";
+          };
+      };
+  };
+in {
+  name = "glusterfs";
+
+  nodes = {
+    server1 = server;
+    server2 = server;
+    client1 = client;
+    client2 = client;
+  };
+
+  testScript = ''
+    server1.wait_for_unit("glusterd.service")
+    server2.wait_for_unit("glusterd.service")
+
+    server1.wait_until_succeeds("gluster peer status")
+    server2.wait_until_succeeds("gluster peer status")
+
+    # establish initial contact
+    server1.succeed("gluster peer probe server2")
+    server1.succeed("gluster peer probe server1")
+
+    server1.succeed("gluster peer status | grep Connected")
+
+    # create volumes
+    server1.succeed("mkdir -p /data/vg0")
+    server2.succeed("mkdir -p /data/vg0")
+    server1.succeed("gluster volume create gv0 server1:/data/vg0 server2:/data/vg0")
+    server1.succeed("gluster volume start gv0")
+
+    # test clients
+    client1.wait_for_unit("gluster.mount")
+    client2.wait_for_unit("gluster.mount")
+
+    client1.succeed("echo test > /gluster/file1")
+    client2.succeed("grep test /gluster/file1")
+  '';
+})
diff --git a/nixos/tests/gnome-xorg.nix b/nixos/tests/gnome-xorg.nix
new file mode 100644
index 00000000000..6264b87af4e
--- /dev/null
+++ b/nixos/tests/gnome-xorg.nix
@@ -0,0 +1,95 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} : {
+  name = "gnome-xorg";
+  meta = with lib; {
+    maintainers = teams.gnome.members;
+  };
+
+  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.displayManager.defaultSession = "gnome-xorg";
+
+      systemd.user.services = {
+        "org.gnome.Shell@x11" = {
+          serviceConfig = {
+            ExecStart = [
+              # Clear the list before overriding it.
+              ""
+              # Eval API is now internal so Shell needs to run in unsafe mode.
+              # TODO: improve test driver so that it supports openqa-like manipulation
+              # that would allow us to drop this mess.
+              "${pkgs.gnome.gnome-shell}/bin/gnome-shell --unsafe-mode"
+            ];
+          };
+        };
+      };
+
+    };
+
+  testScript = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+    uid = toString user.uid;
+    bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${uid}/bus";
+    xauthority = "/run/user/${uid}/gdm/Xauthority";
+    display = "DISPLAY=:0.0";
+    env = "${bus} XAUTHORITY=${xauthority} ${display}";
+    gdbus = "${env} gdbus";
+    su = command: "su - ${user.name} -c '${env} ${command}'";
+
+    # Call javascript in gnome shell, returns a tuple (success, output), where
+    # `success` is true if the dbus call was successful and output is what the
+    # javascript evaluates to.
+    eval = "call --session -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval";
+
+    # False when startup is done
+    startingUp = su "${gdbus} ${eval} Main.layoutManager._startingUp";
+
+    # Start gnome-terminal
+    gnomeTerminalCommand = su "gnome-terminal";
+
+    # Hopefully gnome-terminal's wm class
+    wmClass = su "${gdbus} ${eval} global.display.focus_window.wm_class";
+  in ''
+      with subtest("Login to GNOME Xorg 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 GNOME Shell"):
+          # correct output should be (true, 'false')
+          machine.wait_until_succeeds(
+              "${startingUp} | grep -q 'true,..false'"
+          )
+
+      with subtest("Open Gnome Terminal"):
+          machine.succeed(
+              "${gnomeTerminalCommand}"
+          )
+          # correct output should be (true, '"Gnome-terminal"')
+          machine.wait_until_succeeds(
+              "${wmClass} | grep -q  'true,...Gnome-terminal'"
+          )
+          machine.sleep(20)
+          machine.screenshot("screen")
+    '';
+})
diff --git a/nixos/tests/gnome.nix b/nixos/tests/gnome.nix
new file mode 100644
index 00000000000..06f387ecad6
--- /dev/null
+++ b/nixos/tests/gnome.nix
@@ -0,0 +1,96 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} : {
+  name = "gnome";
+  meta = with lib; {
+    maintainers = teams.gnome.members;
+  };
+
+  machine =
+    { ... }:
+
+    { imports = [ ./common/user-account.nix ];
+
+      services.xserver.enable = true;
+
+      services.xserver.displayManager = {
+        gdm.enable = true;
+        gdm.debug = true;
+        autoLogin = {
+          enable = true;
+          user = "alice";
+        };
+      };
+
+      services.xserver.desktopManager.gnome.enable = true;
+      services.xserver.desktopManager.gnome.debug = true;
+
+      environment.systemPackages = [
+        (pkgs.makeAutostartItem {
+          name = "org.gnome.Terminal";
+          package = pkgs.gnome.gnome-terminal;
+        })
+      ];
+
+      systemd.user.services = {
+        "org.gnome.Shell@wayland" = {
+          serviceConfig = {
+            ExecStart = [
+              # Clear the list before overriding it.
+              ""
+              # Eval API is now internal so Shell needs to run in unsafe mode.
+              # TODO: improve test driver so that it supports openqa-like manipulation
+              # that would allow us to drop this mess.
+              "${pkgs.gnome.gnome-shell}/bin/gnome-shell --unsafe-mode"
+            ];
+          };
+        };
+      };
+
+    };
+
+  testScript = { nodes, ... }: let
+    # Keep line widths somewhat managable
+    user = nodes.machine.config.users.users.alice;
+    uid = toString user.uid;
+    bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${uid}/bus";
+    gdbus = "${bus} gdbus";
+    su = command: "su - ${user.name} -c '${command}'";
+
+    # Call javascript in gnome shell, returns a tuple (success, output), where
+    # `success` is true if the dbus call was successful and output is what the
+    # javascript evaluates to.
+    eval = "call --session -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval";
+
+    # False when startup is done
+    startingUp = su "${gdbus} ${eval} Main.layoutManager._startingUp";
+
+    # Start gnome-terminal
+    gnomeTerminalCommand = su "${bus} gnome-terminal";
+
+    # Hopefully gnome-terminal's wm class
+    wmClass = su "${gdbus} ${eval} global.display.focus_window.wm_class";
+  in ''
+      with subtest("Login to GNOME with GDM"):
+          # wait for gdm to start
+          machine.wait_for_unit("display-manager.service")
+          # wait for the wayland server
+          machine.wait_for_file("/run/user/${uid}/wayland-0")
+          # wait for alice to be logged in
+          machine.wait_for_unit("default.target", "${user.name}")
+          # 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 GNOME Shell"):
+          # correct output should be (true, 'false')
+          machine.wait_until_succeeds(
+              "${startingUp} | grep -q 'true,..false'"
+          )
+
+      with subtest("Open Gnome Terminal"):
+          # correct output should be (true, '"gnome-terminal-server"')
+          machine.wait_until_succeeds(
+              "${wmClass} | grep -q 'gnome-terminal-server'"
+          )
+          machine.sleep(20)
+          machine.screenshot("screen")
+    '';
+})
diff --git a/nixos/tests/go-neb.nix b/nixos/tests/go-neb.nix
new file mode 100644
index 00000000000..4bd03dcf3c6
--- /dev/null
+++ b/nixos/tests/go-neb.nix
@@ -0,0 +1,44 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+{
+  name = "go-neb";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ hexa maralorn ];
+  };
+
+  nodes = {
+    server = {
+      services.go-neb = {
+        enable = true;
+        baseUrl = "http://localhost";
+        secretFile = pkgs.writeText "secrets" "ACCESS_TOKEN=changeme";
+        config = {
+          clients = [ {
+            UserId = "@test:localhost";
+            AccessToken = "$ACCESS_TOKEN";
+            HomeServerUrl = "http://localhost";
+            Sync = false;
+            AutoJoinRooms = false;
+            DisplayName = "neverbeseen";
+          } ];
+          services = [ {
+            ID = "wikipedia_service";
+            Type = "wikipedia";
+            UserID = "@test:localhost";
+            Config = { };
+          } ];
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    server.wait_for_unit("go-neb.service")
+    server.wait_until_succeeds("curl -fL http://localhost:4050/services/hooks/d2lraXBlZGlhX3NlcnZpY2U")
+    server.succeed(
+        "journalctl -eu go-neb -o cat | grep -q service_id=wikipedia_service",
+        "grep -q changeme /var/run/go-neb/config.yaml",
+    )
+  '';
+
+})
diff --git a/nixos/tests/gobgpd.nix b/nixos/tests/gobgpd.nix
new file mode 100644
index 00000000000..775f65d1199
--- /dev/null
+++ b/nixos/tests/gobgpd.nix
@@ -0,0 +1,71 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+  let
+    ifAddr = node: iface: (pkgs.lib.head node.config.networking.interfaces.${iface}.ipv4.addresses).address;
+  in {
+    name = "gobgpd";
+
+    meta = with pkgs.lib.maintainers; { maintainers = [ higebu ]; };
+
+    nodes = {
+      node1 = { nodes, ... }: {
+        environment.systemPackages = [ pkgs.gobgp ];
+        networking.firewall.allowedTCPPorts = [ 179 ];
+        services.gobgpd = {
+          enable = true;
+          settings = {
+            global = {
+              config = {
+                as = 64512;
+                router-id = "192.168.255.1";
+              };
+            };
+            neighbors = [{
+              config = {
+                neighbor-address = ifAddr nodes.node2 "eth1";
+                peer-as = 64513;
+              };
+            }];
+          };
+        };
+      };
+      node2 = { nodes, ... }: {
+        environment.systemPackages = [ pkgs.gobgp ];
+        networking.firewall.allowedTCPPorts = [ 179 ];
+        services.gobgpd = {
+          enable = true;
+          settings = {
+            global = {
+              config = {
+                as = 64513;
+                router-id = "192.168.255.2";
+              };
+            };
+            neighbors = [{
+              config = {
+                neighbor-address = ifAddr nodes.node1 "eth1";
+                peer-as = 64512;
+              };
+            }];
+          };
+        };
+      };
+    };
+
+    testScript = { nodes, ... }: let
+      addr1 = ifAddr nodes.node1 "eth1";
+      addr2 = ifAddr nodes.node2 "eth1";
+    in
+      ''
+      start_all()
+
+      for node in node1, node2:
+          with subtest("should start gobgpd node"):
+              node.wait_for_unit("gobgpd.service")
+          with subtest("should open port 179"):
+              node.wait_for_open_port(179)
+
+      with subtest("should show neighbors by gobgp cli and BGP state should be ESTABLISHED"):
+          node1.wait_until_succeeds("gobgp neighbor ${addr2} | grep -q ESTABLISHED")
+          node2.wait_until_succeeds("gobgp neighbor ${addr1} | grep -q ESTABLISHED")
+    '';
+  })
diff --git a/nixos/tests/gocd-agent.nix b/nixos/tests/gocd-agent.nix
new file mode 100644
index 00000000000..686d0b971d3
--- /dev/null
+++ b/nixos/tests/gocd-agent.nix
@@ -0,0 +1,48 @@
+# verifies:
+#   1. GoCD agent starts
+#   2. GoCD agent responds
+#   3. GoCD agent is available on GoCD server using GoCD API
+#     3.1. https://api.go.cd/current/#get-all-agents
+
+let
+  serverUrl = "localhost:8153/go/api/agents";
+  header = "Accept: application/vnd.go.cd.v2+json";
+in
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "gocd-agent";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ grahamc swarren83 ];
+
+    # gocd agent needs to register with the autoregister key created on first server startup,
+    # but NixOS module doesn't seem to allow to pass during runtime currently
+    broken = true;
+  };
+
+  nodes = {
+    agent =
+      { ... }:
+      {
+        virtualisation.memorySize = 2046;
+        services.gocd-agent = {
+          enable = true;
+        };
+        services.gocd-server = {
+          enable = true;
+        };
+      };
+  };
+
+  testScript = ''
+    start_all()
+    agent.wait_for_unit("gocd-server")
+    agent.wait_for_open_port("8153")
+    agent.wait_for_unit("gocd-agent")
+    agent.wait_until_succeeds(
+        "curl ${serverUrl} -H '${header}' | ${pkgs.jq}/bin/jq -e ._embedded.agents[0].uuid"
+    )
+    agent.succeed(
+        "curl ${serverUrl} -H '${header}' | ${pkgs.jq}/bin/jq -e ._embedded.agents[0].agent_state | grep Idle"
+    )
+  '';
+})
diff --git a/nixos/tests/gocd-server.nix b/nixos/tests/gocd-server.nix
new file mode 100644
index 00000000000..aff651c5278
--- /dev/null
+++ b/nixos/tests/gocd-server.nix
@@ -0,0 +1,28 @@
+# verifies:
+#   1. GoCD server starts
+#   2. GoCD server responds
+
+import ./make-test-python.nix ({ pkgs, ...} :
+
+{
+  name = "gocd-server";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ swarren83 ];
+  };
+
+  nodes = {
+    server =
+      { ... }:
+      {
+        virtualisation.memorySize = 2046;
+        services.gocd-server.enable = true;
+      };
+  };
+
+  testScript = ''
+    server.start()
+    server.wait_for_unit("gocd-server")
+    server.wait_for_open_port(8153)
+    server.wait_until_succeeds("curl -s -f localhost:8153/go")
+  '';
+})
diff --git a/nixos/tests/google-oslogin/default.nix b/nixos/tests/google-oslogin/default.nix
new file mode 100644
index 00000000000..72c87d7153b
--- /dev/null
+++ b/nixos/tests/google-oslogin/default.nix
@@ -0,0 +1,74 @@
+import ../make-test-python.nix ({ pkgs, ... } :
+let
+  inherit (import ./../ssh-keys.nix pkgs)
+    snakeOilPrivateKey snakeOilPublicKey;
+
+    # don't check host keys or known hosts, use the snakeoil ssh key
+    ssh-config = builtins.toFile "ssh.conf" ''
+      UserKnownHostsFile=/dev/null
+      StrictHostKeyChecking=no
+      IdentityFile=~/.ssh/id_snakeoil
+    '';
+in {
+  name = "google-oslogin";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ adisbladis flokli ];
+  };
+
+  nodes = {
+    # the server provides both the the mocked google metadata server and the ssh server
+    server = (import ./server.nix pkgs);
+
+    client = { ... }: {};
+  };
+  testScript =  ''
+    MOCKUSER = "mockuser_nixos_org"
+    MOCKADMIN = "mockadmin_nixos_org"
+    start_all()
+
+    server.wait_for_unit("mock-google-metadata.service")
+    server.wait_for_open_port(80)
+
+    # mockserver should return a non-expired ssh key for both mockuser and mockadmin
+    server.succeed(
+        f'${pkgs.google-guest-oslogin}/bin/google_authorized_keys {MOCKUSER} | grep -q "${snakeOilPublicKey}"'
+    )
+    server.succeed(
+        f'${pkgs.google-guest-oslogin}/bin/google_authorized_keys {MOCKADMIN} | grep -q "${snakeOilPublicKey}"'
+    )
+
+    # install snakeoil ssh key on the client, and provision .ssh/config file
+    client.succeed("mkdir -p ~/.ssh")
+    client.succeed(
+        "cat ${snakeOilPrivateKey} > ~/.ssh/id_snakeoil"
+    )
+    client.succeed("chmod 600 ~/.ssh/id_snakeoil")
+    client.succeed("cp ${ssh-config} ~/.ssh/config")
+
+    client.wait_for_unit("network.target")
+    server.wait_for_unit("sshd.service")
+
+    # we should not be able to connect as non-existing user
+    client.fail("ssh ghost@server 'true'")
+
+    # we should be able to connect as mockuser
+    client.succeed(f"ssh {MOCKUSER}@server 'true'")
+    # but we shouldn't be able to sudo
+    client.fail(
+        f"ssh {MOCKUSER}@server '/run/wrappers/bin/sudo /run/current-system/sw/bin/id' | grep -q 'root'"
+    )
+
+    # we should also be able to log in as mockadmin
+    client.succeed(f"ssh {MOCKADMIN}@server 'true'")
+    # pam_oslogin_admin.so should now have generated a sudoers file
+    server.succeed(
+        f"find /run/google-sudoers.d | grep -q '/run/google-sudoers.d/{MOCKADMIN}'"
+    )
+
+    # and we should be able to sudo
+    client.succeed(
+        f"ssh {MOCKADMIN}@server '/run/wrappers/bin/sudo /run/current-system/sw/bin/id' | grep -q 'root'"
+    )
+  '';
+  })
+
diff --git a/nixos/tests/google-oslogin/server.nix b/nixos/tests/google-oslogin/server.nix
new file mode 100644
index 00000000000..faf5e847d7e
--- /dev/null
+++ b/nixos/tests/google-oslogin/server.nix
@@ -0,0 +1,27 @@
+{ pkgs, ... }:
+let
+  inherit (import ./../ssh-keys.nix pkgs)
+    snakeOilPrivateKey snakeOilPublicKey;
+in {
+  networking.firewall.allowedTCPPorts = [ 80 ];
+
+  systemd.services.mock-google-metadata = {
+    description = "Mock Google metadata service";
+    serviceConfig.Type = "simple";
+    serviceConfig.ExecStart = "${pkgs.python3}/bin/python ${./server.py}";
+    environment = {
+      SNAKEOIL_PUBLIC_KEY = snakeOilPublicKey;
+    };
+    wantedBy = [ "multi-user.target" ];
+    after = [ "network.target" ];
+  };
+
+  services.openssh.enable = true;
+  services.openssh.kbdInteractiveAuthentication = false;
+  services.openssh.passwordAuthentication = false;
+
+  security.googleOsLogin.enable = true;
+
+  # Mock google service
+  networking.interfaces.lo.ipv4.addresses = [ { address = "169.254.169.254"; prefixLength = 32; } ];
+}
diff --git a/nixos/tests/google-oslogin/server.py b/nixos/tests/google-oslogin/server.py
new file mode 100755
index 00000000000..5ea9bbd2c96
--- /dev/null
+++ b/nixos/tests/google-oslogin/server.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python3
+import json
+import sys
+import time
+import os
+import hashlib
+import base64
+
+from http.server import BaseHTTPRequestHandler, HTTPServer
+from urllib.parse import urlparse, parse_qs
+from typing import Dict
+
+SNAKEOIL_PUBLIC_KEY = os.environ['SNAKEOIL_PUBLIC_KEY']
+MOCKUSER="mockuser_nixos_org"
+MOCKADMIN="mockadmin_nixos_org"
+
+
+def w(msg: bytes):
+    sys.stderr.write(f"{msg}\n")
+    sys.stderr.flush()
+
+
+def gen_fingerprint(pubkey: str):
+    decoded_key = base64.b64decode(pubkey.encode("ascii").split()[1])
+    return hashlib.sha256(decoded_key).hexdigest()
+
+
+def gen_email(username: str):
+    """username seems to be a 21 characters long number string, so mimic that in a reproducible way"""
+    return str(int(hashlib.sha256(username.encode()).hexdigest(), 16))[0:21]
+
+
+def gen_mockuser(username: str, uid: str, gid: str, home_directory: str, snakeoil_pubkey: str) -> Dict:
+    snakeoil_pubkey_fingerprint = gen_fingerprint(snakeoil_pubkey)
+    # seems to be a 21 characters long numberstring, so mimic that in a reproducible way
+    email = gen_email(username)
+    return {
+        "loginProfiles": [
+            {
+                "name": email,
+                "posixAccounts": [
+                    {
+                        "primary": True,
+                        "username": username,
+                        "uid": uid,
+                        "gid": gid,
+                        "homeDirectory": home_directory,
+                        "operatingSystemType": "LINUX"
+                    }
+                ],
+                "sshPublicKeys": {
+                    snakeoil_pubkey_fingerprint: {
+                        "key": snakeoil_pubkey,
+                        "expirationTimeUsec": str((time.time() + 600) * 1000000),  # 10 minutes in the future
+                        "fingerprint": snakeoil_pubkey_fingerprint
+                    }
+                }
+            }
+        ]
+    }
+
+
+class ReqHandler(BaseHTTPRequestHandler):
+
+    def _send_json_ok(self, data: dict):
+        self.send_response(200)
+        self.send_header('Content-type', 'application/json')
+        self.end_headers()
+        out = json.dumps(data).encode()
+        w(out)
+        self.wfile.write(out)
+
+    def _send_json_success(self, success=True):
+        self.send_response(200)
+        self.send_header('Content-type', 'application/json')
+        self.end_headers()
+        out = json.dumps({"success": success}).encode()
+        w(out)
+        self.wfile.write(out)
+
+    def _send_404(self):
+        self.send_response(404)
+        self.end_headers()
+
+    def do_GET(self):
+        p = str(self.path)
+        pu = urlparse(p)
+        params = parse_qs(pu.query)
+
+        # users endpoint
+        if pu.path == "/computeMetadata/v1/oslogin/users":
+            # mockuser and mockadmin are allowed to login, both use the same snakeoil public key
+            if params.get('username') == [MOCKUSER] or params.get('uid') == ["1009719690"]:
+                username = MOCKUSER
+                uid = "1009719690"
+            elif params.get('username') == [MOCKADMIN] or params.get('uid') == ["1009719691"]:
+                username = MOCKADMIN
+                uid = "1009719691"
+            else:
+                self._send_404()
+                return
+
+            self._send_json_ok(gen_mockuser(username=username, uid=uid, gid=uid, home_directory=f"/home/{username}", snakeoil_pubkey=SNAKEOIL_PUBLIC_KEY))
+            return
+
+        # authorize endpoint
+        elif pu.path == "/computeMetadata/v1/oslogin/authorize":
+            # is user allowed to login?
+            if params.get("policy") == ["login"]:
+                # mockuser and mockadmin are allowed to login
+                if params.get('email') == [gen_email(MOCKUSER)] or params.get('email') == [gen_email(MOCKADMIN)]:
+                    self._send_json_success()
+                    return
+                self._send_json_success(False)
+                return
+            # is user allowed to become root?
+            elif params.get("policy") == ["adminLogin"]:
+                # only mockadmin is allowed to become admin
+                self._send_json_success((params['email'] == [gen_email(MOCKADMIN)]))
+                return
+            # send 404 for other policies
+            else:
+                self._send_404()
+                return
+        else:
+            sys.stderr.write(f"Unhandled path: {p}\n")
+            sys.stderr.flush()
+            self.send_response(404)
+            self.end_headers()
+            self.wfile.write(b'')
+
+
+if __name__ == '__main__':
+    s = HTTPServer(('0.0.0.0', 80), ReqHandler)
+    s.serve_forever()
diff --git a/nixos/tests/gotify-server.nix b/nixos/tests/gotify-server.nix
new file mode 100644
index 00000000000..051666fbe72
--- /dev/null
+++ b/nixos/tests/gotify-server.nix
@@ -0,0 +1,50 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} : {
+  name = "gotify-server";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ma27 ];
+  };
+
+  machine = { pkgs, ... }: {
+    environment.systemPackages = [ pkgs.jq ];
+
+    services.gotify = {
+      enable = true;
+      port = 3000;
+    };
+  };
+
+  testScript = ''
+    machine.start()
+
+    machine.wait_for_unit("gotify-server.service")
+    machine.wait_for_open_port(3000)
+
+    token = machine.succeed(
+        "curl --fail -sS -X POST localhost:3000/application -F name=nixos "
+        + '-H "Authorization: Basic $(echo -ne "admin:admin" | base64 --wrap 0)" '
+        + "| jq .token | xargs echo -n"
+    )
+
+    usertoken = machine.succeed(
+        "curl --fail -sS -X POST localhost:3000/client -F name=nixos "
+        + '-H "Authorization: Basic $(echo -ne "admin:admin" | base64 --wrap 0)" '
+        + "| jq .token | xargs echo -n"
+    )
+
+    machine.succeed(
+        f"curl --fail -sS -X POST 'localhost:3000/message?token={token}' -H 'Accept: application/json' "
+        + "-F title=Gotify -F message=Works"
+    )
+
+    title = machine.succeed(
+        f"curl --fail -sS 'localhost:3000/message?since=0&token={usertoken}' | jq '.messages|.[0]|.title' | xargs echo -n"
+    )
+
+    assert title == "Gotify"
+
+    # Ensure that the UI responds with a successfuly code and that the
+    # response is not empty
+    result = machine.succeed("curl -fsS localhost:3000")
+    assert result, "HTTP response from localhost:3000 must not be empty!"
+  '';
+})
diff --git a/nixos/tests/grafana.nix b/nixos/tests/grafana.nix
new file mode 100644
index 00000000000..174d664d877
--- /dev/null
+++ b/nixos/tests/grafana.nix
@@ -0,0 +1,109 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+
+let
+  inherit (lib) mkMerge nameValuePair maintainers;
+
+  baseGrafanaConf = {
+    services.grafana = {
+      enable = true;
+      addr = "localhost";
+      analytics.reporting.enable = false;
+      domain = "localhost";
+      security = {
+        adminUser = "testadmin";
+        adminPassword = "snakeoilpwd";
+      };
+    };
+  };
+
+  extraNodeConfs = {
+    declarativePlugins = {
+      services.grafana.declarativePlugins = [ pkgs.grafanaPlugins.grafana-clock-panel ];
+    };
+
+    postgresql = {
+      services.grafana.database = {
+        host = "127.0.0.1:5432";
+        user = "grafana";
+      };
+      services.postgresql = {
+        enable = true;
+        ensureDatabases = [ "grafana" ];
+        ensureUsers = [{
+          name = "grafana";
+          ensurePermissions."DATABASE grafana" = "ALL PRIVILEGES";
+        }];
+      };
+      systemd.services.grafana.after = [ "postgresql.service" ];
+    };
+
+    mysql = {
+      services.grafana.database.user = "grafana";
+      services.mysql = {
+        enable = true;
+        ensureDatabases = [ "grafana" ];
+        ensureUsers = [{
+          name = "grafana";
+          ensurePermissions."grafana.*" = "ALL PRIVILEGES";
+        }];
+        package = pkgs.mariadb;
+      };
+      systemd.services.grafana.after = [ "mysql.service" ];
+    };
+  };
+
+  nodes = builtins.listToAttrs (map (dbName:
+    nameValuePair dbName (mkMerge [
+    baseGrafanaConf
+    (extraNodeConfs.${dbName} or {})
+  ])) [ "sqlite" "declarativePlugins" "postgresql" "mysql" ]);
+
+in {
+  name = "grafana";
+
+  meta = with maintainers; {
+    maintainers = [ willibutz ];
+  };
+
+  inherit nodes;
+
+  testScript = ''
+    start_all()
+
+    with subtest("Declarative plugins installed"):
+        declarativePlugins.wait_for_unit("grafana.service")
+        declarativePlugins.wait_for_open_port(3000)
+        declarativePlugins.succeed(
+            "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/plugins | grep grafana-clock-panel"
+        )
+        declarativePlugins.shutdown()
+
+    with subtest("Successful API query as admin user with sqlite db"):
+        sqlite.wait_for_unit("grafana.service")
+        sqlite.wait_for_open_port(3000)
+        sqlite.succeed(
+            "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/org/users | grep testadmin\@localhost"
+        )
+        sqlite.shutdown()
+
+    with subtest("Successful API query as admin user with postgresql db"):
+        postgresql.wait_for_unit("grafana.service")
+        postgresql.wait_for_unit("postgresql.service")
+        postgresql.wait_for_open_port(3000)
+        postgresql.wait_for_open_port(5432)
+        postgresql.succeed(
+            "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/org/users | grep testadmin\@localhost"
+        )
+        postgresql.shutdown()
+
+    with subtest("Successful API query as admin user with mysql db"):
+        mysql.wait_for_unit("grafana.service")
+        mysql.wait_for_unit("mysql.service")
+        mysql.wait_for_open_port(3000)
+        mysql.wait_for_open_port(3306)
+        mysql.succeed(
+            "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/org/users | grep testadmin\@localhost"
+        )
+        mysql.shutdown()
+  '';
+})
diff --git a/nixos/tests/graphite.nix b/nixos/tests/graphite.nix
new file mode 100644
index 00000000000..496f16846ea
--- /dev/null
+++ b/nixos/tests/graphite.nix
@@ -0,0 +1,48 @@
+import ./make-test-python.nix ({ pkgs, ... } :
+{
+  name = "graphite";
+  nodes = {
+    one =
+      { ... }: {
+        time.timeZone = "UTC";
+        services.graphite = {
+          web = {
+            enable = true;
+            extraConfig = ''
+              SECRET_KEY = "abcd";
+            '';
+          };
+          api = {
+            enable = true;
+            port = 8082;
+            finders = [ ];
+          };
+          carbon.enableCache = true;
+          seyren.enable = false;  # Implicitely requires openssl-1.0.2u which is marked insecure
+          beacon.enable = true;
+        };
+      };
+  };
+
+  testScript = ''
+    start_all()
+    one.wait_for_unit("default.target")
+    one.wait_for_unit("graphiteWeb.service")
+    one.wait_for_unit("graphiteApi.service")
+    one.wait_for_unit("graphite-beacon.service")
+    one.wait_for_unit("carbonCache.service")
+    # The services above are of type "simple". systemd considers them active immediately
+    # even if they're still in preStart (which takes quite long for graphiteWeb).
+    # Wait for ports to open so we're sure the services are up and listening.
+    one.wait_for_open_port(8080)
+    one.wait_for_open_port(8082)
+    one.wait_for_open_port(2003)
+    one.succeed('echo "foo 1 `date +%s`" | nc -N localhost 2003')
+    one.wait_until_succeeds(
+        "curl 'http://localhost:8080/metrics/find/?query=foo&format=treejson' --silent | grep foo >&2"
+    )
+    one.wait_until_succeeds(
+        "curl 'http://localhost:8082/metrics/find/?query=foo&format=treejson' --silent | grep foo >&2"
+    )
+  '';
+})
diff --git a/nixos/tests/graylog.nix b/nixos/tests/graylog.nix
new file mode 100644
index 00000000000..572904f60d5
--- /dev/null
+++ b/nixos/tests/graylog.nix
@@ -0,0 +1,115 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "graylog";
+  meta.maintainers = with lib.maintainers; [ ];
+
+  machine = { pkgs, ... }: {
+    virtualisation.memorySize = 4096;
+    virtualisation.diskSize = 4096;
+
+    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
+    '';
+
+    services.graylog = {
+      enable = true;
+      passwordSecret = "YGhZ59wXMrYOojx5xdgEpBpDw2N6FbhM4lTtaJ1KPxxmKrUvSlDbtWArwAWMQ5LKx1ojHEVrQrBMVRdXbRyZLqffoUzHfssc";
+      elasticsearchHosts = [ "http://localhost:9200" ];
+
+      # `echo -n "nixos" | shasum -a 256`
+      rootPasswordSha2 = "6ed332bcfa615381511d4d5ba44a293bb476f368f7e9e304f0dff50230d1a85b";
+    };
+
+    environment.systemPackages = [ pkgs.jq ];
+
+    systemd.services.graylog.path = [ pkgs.netcat ];
+    systemd.services.graylog.preStart = ''
+      until nc -z localhost 9200; do
+        sleep 2
+      done
+    '';
+  };
+
+  testScript = let
+    payloads.login = pkgs.writeText "login.json" (builtins.toJSON {
+      host = "127.0.0.1:9000";
+      username = "admin";
+      password = "nixos";
+    });
+
+    payloads.input = pkgs.writeText "input.json" (builtins.toJSON {
+      title = "Demo";
+      global = false;
+      type = "org.graylog2.inputs.gelf.udp.GELFUDPInput";
+      node = "@node@";
+      configuration = {
+        bind_address = "0.0.0.0";
+        decompress_size_limit = 8388608;
+        number_worker_threads = 1;
+        override_source = null;
+        port = 12201;
+        recv_buffer_size = 262144;
+      };
+    });
+
+    payloads.gelf_message = pkgs.writeText "gelf.json" (builtins.toJSON {
+      host = "example.org";
+      short_message = "A short message";
+      full_message = "A long message";
+      version = "1.1";
+      level = 5;
+      facility = "Test";
+    });
+  in ''
+    machine.start()
+    machine.wait_for_unit("graylog.service")
+    machine.wait_for_open_port(9000)
+    machine.succeed("curl -sSfL http://127.0.0.1:9000/")
+
+    session = machine.succeed(
+        "curl -X POST "
+        + "-sSfL http://127.0.0.1:9000/api/system/sessions "
+        + "-d $(cat ${payloads.login}) "
+        + "-H 'Content-Type: application/json' "
+        + "-H 'Accept: application/json' "
+        + "-H 'x-requested-by: cli' "
+        + "| jq .session_id | xargs echo"
+    ).rstrip()
+
+    machine.succeed(
+        "curl -X POST "
+        + f"-sSfL http://127.0.0.1:9000/api/system/inputs -u {session}:session "
+        + '-d $(cat ${payloads.input} | sed -e "s,@node@,$(cat /var/lib/graylog/server/node-id),") '
+        + "-H 'Accept: application/json' "
+        + "-H 'Content-Type: application/json' "
+        + "-H 'x-requested-by: cli' "
+    )
+
+    machine.wait_until_succeeds(
+        "test \"$(curl -sSfL 'http://127.0.0.1:9000/api/cluster/inputstates' "
+        + f"-u {session}:session "
+        + "-H 'Accept: application/json' "
+        + "-H 'Content-Type: application/json' "
+        + "-H 'x-requested-by: cli'"
+        + "| jq 'to_entries[]|.value|.[0]|.state' | xargs echo"
+        + ')" = "RUNNING"'
+    )
+
+    machine.succeed(
+        "echo -n $(cat ${payloads.gelf_message}) | nc -w10 -u 127.0.0.1 12201"
+    )
+
+    machine.succeed(
+        'test "$(curl -X GET '
+        + "-sSfL 'http://127.0.0.1:9000/api/search/universal/relative?query=*' "
+        + f"-u {session}:session "
+        + "-H 'Accept: application/json' "
+        + "-H 'Content-Type: application/json' "
+        + "-H 'x-requested-by: cli'"
+        + ' | jq \'.total_results\' | xargs echo)" = "1"'
+    )
+  '';
+})
diff --git a/nixos/tests/grocy.nix b/nixos/tests/grocy.nix
new file mode 100644
index 00000000000..2be5c24ecb5
--- /dev/null
+++ b/nixos/tests/grocy.nix
@@ -0,0 +1,47 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "grocy";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ma27 ];
+  };
+
+  machine = { pkgs, ... }: {
+    services.grocy = {
+      enable = true;
+      hostName = "localhost";
+      nginx.enableSSL = false;
+    };
+    environment.systemPackages = [ pkgs.jq ];
+  };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_open_port(80)
+    machine.wait_for_unit("multi-user.target")
+
+    machine.succeed("curl -sSf http://localhost")
+
+    machine.succeed(
+        "curl -c cookies -sSf -X POST http://localhost/login -d 'username=admin&password=admin'"
+    )
+
+    cookie = machine.succeed(
+        "grep -v '^#' cookies | awk '{ print $7 }' | sed -e '/^$/d' | perl -pe 'chomp'"
+    )
+
+    machine.succeed(
+        f"curl -sSf -X POST http://localhost/api/objects/tasks -b 'grocy_session={cookie}' "
+        + '-d \'{"assigned_to_user_id":1,"name":"Test Task","due_date":"1970-01-01"}\'''
+        + " --header 'Content-Type: application/json'"
+    )
+
+    task_name = machine.succeed(
+        f"curl -sSf http://localhost/api/tasks -b 'grocy_session={cookie}' --header 'Accept: application/json' | jq '.[].name' | xargs echo | perl -pe 'chomp'"
+    )
+
+    assert task_name == "Test Task"
+
+    machine.succeed("curl -sSI http://localhost/api/tasks 2>&1 | grep '401 Unauthorized'")
+
+    machine.shutdown()
+  '';
+})
diff --git a/nixos/tests/grub.nix b/nixos/tests/grub.nix
new file mode 100644
index 00000000000..84bfc90955b
--- /dev/null
+++ b/nixos/tests/grub.nix
@@ -0,0 +1,60 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "grub";
+
+  meta = with lib.maintainers; {
+    maintainers = [ rnhmjoj ];
+  };
+
+  machine = { ... }: {
+    virtualisation.useBootLoader = true;
+
+    boot.loader.timeout = null;
+    boot.loader.grub = {
+      enable = true;
+      users.alice.password = "supersecret";
+
+      # OCR is not accurate enough
+      extraConfig = "serial; terminal_output serial";
+    };
+  };
+
+  testScript = ''
+    def grub_login_as(user, password):
+        """
+        Enters user and password to log into GRUB
+        """
+        machine.wait_for_console_text("Enter username:")
+        machine.send_chars(user + "\n")
+        machine.wait_for_console_text("Enter password:")
+        machine.send_chars(password + "\n")
+
+
+    def grub_select_all_configurations():
+        """
+        Selects "All configurations" from the GRUB menu
+        to trigger a login request.
+        """
+        machine.send_monitor_command("sendkey down")
+        machine.send_monitor_command("sendkey ret")
+
+
+    machine.start()
+
+    # wait for grub screen
+    machine.wait_for_console_text("GNU GRUB")
+
+    grub_select_all_configurations()
+    with subtest("Invalid credentials are rejected"):
+        grub_login_as("wronguser", "wrongsecret")
+        machine.wait_for_console_text("error: access denied.")
+
+    grub_select_all_configurations()
+    with subtest("Valid credentials are accepted"):
+        grub_login_as("alice", "supersecret")
+        machine.send_chars("\n")  # press enter to boot
+        machine.wait_for_console_text("Linux version")
+
+    with subtest("Machine boots correctly"):
+        machine.wait_for_unit("multi-user.target")
+  '';
+})
diff --git a/nixos/tests/gvisor.nix b/nixos/tests/gvisor.nix
new file mode 100644
index 00000000000..77ff29341be
--- /dev/null
+++ b/nixos/tests/gvisor.nix
@@ -0,0 +1,49 @@
+# This test runs a container through gvisor and checks if simple container starts
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "gvisor";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ andrew-d ];
+  };
+
+  nodes = {
+    gvisor =
+      { pkgs, ... }:
+        {
+          virtualisation.docker = {
+            enable = true;
+            extraOptions = "--add-runtime runsc=${pkgs.gvisor}/bin/runsc";
+          };
+
+          networking = {
+            dhcpcd.enable = false;
+            defaultGateway = "192.168.1.1";
+            interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+              { address = "192.168.1.2"; prefixLength = 24; }
+            ];
+          };
+        };
+    };
+
+  testScript = ''
+    start_all()
+
+    gvisor.wait_for_unit("network.target")
+    gvisor.wait_for_unit("sockets.target")
+
+    # Start by verifying that gvisor itself works
+    output = gvisor.succeed(
+        "${pkgs.gvisor}/bin/runsc -alsologtostderr do ${pkgs.coreutils}/bin/echo hello world"
+    )
+    assert output.strip() == "hello world"
+
+    # Also test the Docker runtime
+    gvisor.succeed("tar cv --files-from /dev/null | docker import - scratchimg")
+    gvisor.succeed(
+        "docker run -d --name=sleeping --runtime=runsc -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+    )
+    gvisor.succeed("docker ps | grep sleeping")
+    gvisor.succeed("docker stop sleeping")
+  '';
+})
+
diff --git a/nixos/tests/hadoop/default.nix b/nixos/tests/hadoop/default.nix
new file mode 100644
index 00000000000..d2a97cbeffb
--- /dev/null
+++ b/nixos/tests/hadoop/default.nix
@@ -0,0 +1,7 @@
+{ handleTestOn, package, ... }:
+
+{
+  all = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./hadoop.nix { inherit package; };
+  hdfs = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./hdfs.nix { inherit package; };
+  yarn = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./yarn.nix { inherit package; };
+}
diff --git a/nixos/tests/hadoop/hadoop.nix b/nixos/tests/hadoop/hadoop.nix
new file mode 100644
index 00000000000..b132f4fa58b
--- /dev/null
+++ b/nixos/tests/hadoop/hadoop.nix
@@ -0,0 +1,255 @@
+# This test is very comprehensive. It tests whether all hadoop services work well with each other.
+# Run this when updating the Hadoop package or making significant changes to the hadoop module.
+# For a more basic test, see hdfs.nix and yarn.nix
+import ../make-test-python.nix ({ package, ... }: {
+  name = "hadoop-combined";
+
+  nodes =
+    let
+      coreSite = {
+        "fs.defaultFS" = "hdfs://ns1";
+      };
+      hdfsSite = {
+        # HA Quorum Journal Manager configuration
+        "dfs.nameservices" = "ns1";
+        "dfs.ha.namenodes.ns1" = "nn1,nn2";
+        "dfs.namenode.shared.edits.dir.ns1" = "qjournal://jn1:8485;jn2:8485;jn3:8485/ns1";
+        "dfs.namenode.rpc-address.ns1.nn1" = "nn1:8020";
+        "dfs.namenode.rpc-address.ns1.nn2" = "nn2:8020";
+        "dfs.namenode.servicerpc-address.ns1.nn1" = "nn1:8022";
+        "dfs.namenode.servicerpc-address.ns1.nn2" = "nn2:8022";
+        "dfs.namenode.http-address.ns1.nn1" = "nn1:9870";
+        "dfs.namenode.http-address.ns1.nn2" = "nn2:9870";
+
+        # Automatic failover configuration
+        "dfs.client.failover.proxy.provider.ns1" = "org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider";
+        "dfs.ha.automatic-failover.enabled.ns1" = "true";
+        "dfs.ha.fencing.methods" = "shell(true)";
+        "ha.zookeeper.quorum" = "zk1:2181";
+      };
+      yarnSite = {
+        "yarn.resourcemanager.zk-address" = "zk1:2181";
+        "yarn.resourcemanager.ha.enabled" = "true";
+        "yarn.resourcemanager.ha.rm-ids" = "rm1,rm2";
+        "yarn.resourcemanager.hostname.rm1" = "rm1";
+        "yarn.resourcemanager.hostname.rm2" = "rm2";
+        "yarn.resourcemanager.ha.automatic-failover.enabled" = "true";
+        "yarn.resourcemanager.cluster-id" = "cluster1";
+        # yarn.resourcemanager.webapp.address needs to be defined even though yarn.resourcemanager.hostname is set. This shouldn't be necessary, but there's a bug in
+        # hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/amfilter/AmFilterInitializer.java:70
+        # that causes AM containers to fail otherwise.
+        "yarn.resourcemanager.webapp.address.rm1" = "rm1:8088";
+        "yarn.resourcemanager.webapp.address.rm2" = "rm2:8088";
+      };
+    in
+    {
+      zk1 = { ... }: {
+        services.zookeeper.enable = true;
+        networking.firewall.allowedTCPPorts = [ 2181 ];
+      };
+
+      # HDFS cluster
+      nn1 = { ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite;
+          hdfs.namenode = {
+            enable = true;
+            openFirewall = true;
+          };
+          hdfs.zkfc.enable = true;
+        };
+      };
+      nn2 = { ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite;
+          hdfs.namenode = {
+            enable = true;
+            openFirewall = true;
+          };
+          hdfs.zkfc.enable = true;
+        };
+      };
+
+      jn1 = { ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite;
+          hdfs.journalnode = {
+            enable = true;
+            openFirewall = true;
+          };
+        };
+      };
+      jn2 = { ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite;
+          hdfs.journalnode = {
+            enable = true;
+            openFirewall = true;
+          };
+        };
+      };
+      jn3 = { ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite;
+          hdfs.journalnode = {
+            enable = true;
+            openFirewall = true;
+          };
+        };
+      };
+
+      dn1 = { ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite;
+          hdfs.datanode = {
+            enable = true;
+            openFirewall = true;
+          };
+        };
+      };
+
+      # YARN cluster
+      rm1 = { options, ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite yarnSite;
+          yarn.resourcemanager = {
+            enable = true;
+            openFirewall = true;
+          };
+        };
+      };
+      rm2 = { options, ... }: {
+        services.hadoop = {
+          inherit package coreSite hdfsSite yarnSite;
+          yarn.resourcemanager = {
+            enable = true;
+            openFirewall = true;
+          };
+        };
+      };
+      nm1 = { options, ... }: {
+        virtualisation.memorySize = 2048;
+        services.hadoop = {
+          inherit package coreSite hdfsSite yarnSite;
+          yarn.nodemanager = {
+            enable = true;
+            openFirewall = true;
+          };
+        };
+      };
+      client = { options, ... }: {
+        services.hadoop = {
+          gatewayRole.enable = true;
+          inherit package coreSite hdfsSite yarnSite;
+        };
+      };
+  };
+
+  testScript = ''
+    start_all()
+
+    #### HDFS tests ####
+
+    zk1.wait_for_unit("network.target")
+    jn1.wait_for_unit("network.target")
+    jn2.wait_for_unit("network.target")
+    jn3.wait_for_unit("network.target")
+    nn1.wait_for_unit("network.target")
+    nn2.wait_for_unit("network.target")
+    dn1.wait_for_unit("network.target")
+
+    zk1.wait_for_unit("zookeeper")
+    jn1.wait_for_unit("hdfs-journalnode")
+    jn2.wait_for_unit("hdfs-journalnode")
+    jn3.wait_for_unit("hdfs-journalnode")
+
+    zk1.wait_for_open_port(2181)
+    jn1.wait_for_open_port(8480)
+    jn1.wait_for_open_port(8485)
+    jn2.wait_for_open_port(8480)
+    jn2.wait_for_open_port(8485)
+
+    # Namenodes must be stopped before initializing the cluster
+    nn1.succeed("systemctl stop hdfs-namenode")
+    nn2.succeed("systemctl stop hdfs-namenode")
+    nn1.succeed("systemctl stop hdfs-zkfc")
+    nn2.succeed("systemctl stop hdfs-zkfc")
+
+    # Initialize zookeeper for failover controller
+    nn1.succeed("sudo -u hdfs hdfs zkfc -formatZK 2>&1 | systemd-cat")
+
+    # Format NN1 and start it
+    nn1.succeed("sudo -u hdfs hadoop namenode -format 2>&1 | systemd-cat")
+    nn1.succeed("systemctl start hdfs-namenode")
+    nn1.wait_for_open_port(9870)
+    nn1.wait_for_open_port(8022)
+    nn1.wait_for_open_port(8020)
+
+    # Bootstrap NN2 from NN1 and start it
+    nn2.succeed("sudo -u hdfs hdfs namenode -bootstrapStandby 2>&1 | systemd-cat")
+    nn2.succeed("systemctl start hdfs-namenode")
+    nn2.wait_for_open_port(9870)
+    nn2.wait_for_open_port(8022)
+    nn2.wait_for_open_port(8020)
+    nn1.succeed("netstat -tulpne | systemd-cat")
+
+    # Start failover controllers
+    nn1.succeed("systemctl start hdfs-zkfc")
+    nn2.succeed("systemctl start hdfs-zkfc")
+
+    # DN should have started by now, but confirm anyway
+    dn1.wait_for_unit("hdfs-datanode")
+    # Print states of namenodes
+    client.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState | systemd-cat")
+    # Wait for cluster to exit safemode
+    client.succeed("sudo -u hdfs hdfs dfsadmin -safemode wait")
+    client.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState | systemd-cat")
+    # test R/W
+    client.succeed("echo testfilecontents | sudo -u hdfs hdfs dfs -put - /testfile")
+    assert "testfilecontents" in client.succeed("sudo -u hdfs hdfs dfs -cat /testfile")
+
+    # Test NN failover
+    nn1.succeed("systemctl stop hdfs-namenode")
+    assert "active" in client.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState")
+    client.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState | systemd-cat")
+    assert "testfilecontents" in client.succeed("sudo -u hdfs hdfs dfs -cat /testfile")
+
+    nn1.succeed("systemctl start hdfs-namenode")
+    nn1.wait_for_open_port(9870)
+    nn1.wait_for_open_port(8022)
+    nn1.wait_for_open_port(8020)
+    assert "standby" in client.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState")
+    client.succeed("sudo -u hdfs hdfs haadmin -getAllServiceState | systemd-cat")
+
+    #### YARN tests ####
+
+    rm1.wait_for_unit("network.target")
+    rm2.wait_for_unit("network.target")
+    nm1.wait_for_unit("network.target")
+
+    rm1.wait_for_unit("yarn-resourcemanager")
+    rm1.wait_for_open_port(8088)
+    rm2.wait_for_unit("yarn-resourcemanager")
+    rm2.wait_for_open_port(8088)
+
+    nm1.wait_for_unit("yarn-nodemanager")
+    nm1.wait_for_open_port(8042)
+    nm1.wait_for_open_port(8040)
+    client.wait_until_succeeds("yarn node -list | grep Nodes:1")
+    client.succeed("sudo -u yarn yarn rmadmin -getAllServiceState | systemd-cat")
+    client.succeed("sudo -u yarn yarn node -list | systemd-cat")
+
+    # Test RM failover
+    rm1.succeed("systemctl stop yarn-resourcemanager")
+    assert "standby" not in client.succeed("sudo -u yarn yarn rmadmin -getAllServiceState")
+    client.succeed("sudo -u yarn yarn rmadmin -getAllServiceState | systemd-cat")
+    rm1.succeed("systemctl start yarn-resourcemanager")
+    rm1.wait_for_unit("yarn-resourcemanager")
+    rm1.wait_for_open_port(8088)
+    assert "standby" in client.succeed("sudo -u yarn yarn rmadmin -getAllServiceState")
+    client.succeed("sudo -u yarn yarn rmadmin -getAllServiceState | systemd-cat")
+
+    assert "Estimated value of Pi is" in client.succeed("HADOOP_USER_NAME=hdfs yarn jar $(readlink $(which yarn) | sed -r 's~bin/yarn~lib/hadoop-*/share/hadoop/mapreduce/hadoop-mapreduce-examples-*.jar~g') pi 2 10")
+    assert "SUCCEEDED" in client.succeed("yarn application -list -appStates FINISHED")
+  '';
+})
diff --git a/nixos/tests/hadoop/hdfs.nix b/nixos/tests/hadoop/hdfs.nix
new file mode 100644
index 00000000000..9415500463d
--- /dev/null
+++ b/nixos/tests/hadoop/hdfs.nix
@@ -0,0 +1,84 @@
+# Test a minimal HDFS cluster with no HA
+import ../make-test-python.nix ({ package, lib, ... }:
+with lib;
+{
+  name = "hadoop-hdfs";
+
+  nodes = let
+    coreSite = {
+      "fs.defaultFS" = "hdfs://namenode:8020";
+      "hadoop.proxyuser.httpfs.groups" = "*";
+      "hadoop.proxyuser.httpfs.hosts" = "*";
+    };
+    in {
+    namenode = { pkgs, ... }: {
+      services.hadoop = {
+        inherit package;
+        hdfs = {
+          namenode = {
+            enable = true;
+            openFirewall = true;
+            formatOnInit = true;
+          };
+          httpfs = {
+            # The NixOS hadoop module only support webHDFS on 3.3 and newer
+            enable = mkIf (versionAtLeast package.version "3.3") true;
+            openFirewall = true;
+          };
+        };
+        inherit coreSite;
+      };
+    };
+    datanode = { pkgs, ... }: {
+      services.hadoop = {
+        inherit package;
+        hdfs.datanode = {
+          enable = true;
+          openFirewall = true;
+          dataDirs = [{
+            type = "DISK";
+            path = "/tmp/dn1";
+          }];
+        };
+        inherit coreSite;
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    namenode.wait_for_unit("hdfs-namenode")
+    namenode.wait_for_unit("network.target")
+    namenode.wait_for_open_port(8020)
+    namenode.succeed("ss -tulpne | systemd-cat")
+    namenode.succeed("cat /etc/hadoop*/hdfs-site.xml | systemd-cat")
+    namenode.wait_for_open_port(9870)
+
+    datanode.wait_for_unit("hdfs-datanode")
+    datanode.wait_for_unit("network.target")
+  '' + ( if versionAtLeast package.version "3" then ''
+    datanode.wait_for_open_port(9864)
+    datanode.wait_for_open_port(9866)
+    datanode.wait_for_open_port(9867)
+
+    datanode.succeed("curl -f http://datanode:9864")
+  '' else ''
+    datanode.wait_for_open_port(50075)
+    datanode.wait_for_open_port(50010)
+    datanode.wait_for_open_port(50020)
+
+    datanode.succeed("curl -f http://datanode:50075")
+  '' ) + ''
+    namenode.succeed("curl -f http://namenode:9870")
+
+    datanode.succeed("sudo -u hdfs hdfs dfsadmin -safemode wait")
+    datanode.succeed("echo testfilecontents | sudo -u hdfs hdfs dfs -put - /testfile")
+    assert "testfilecontents" in datanode.succeed("sudo -u hdfs hdfs dfs -cat /testfile")
+
+  '' + optionalString ( versionAtLeast package.version "3.3" ) ''
+    namenode.wait_for_unit("hdfs-httpfs")
+    namenode.wait_for_open_port(14000)
+    assert "testfilecontents" in datanode.succeed("curl -f \"http://namenode:14000/webhdfs/v1/testfile?user.name=hdfs&op=OPEN\" 2>&1")
+  '';
+})
diff --git a/nixos/tests/hadoop/yarn.nix b/nixos/tests/hadoop/yarn.nix
new file mode 100644
index 00000000000..1bf8e3831f6
--- /dev/null
+++ b/nixos/tests/hadoop/yarn.nix
@@ -0,0 +1,45 @@
+# This only tests if YARN is able to start its services
+import ../make-test-python.nix ({ package, ... }: {
+  name = "hadoop-yarn";
+
+  nodes = {
+    resourcemanager = { ... }: {
+      services.hadoop = {
+        inherit package;
+        yarn.resourcemanager = {
+          enable = true;
+          openFirewall = true;
+        };
+      };
+    };
+    nodemanager = { options, lib, ... }: {
+      services.hadoop = {
+        inherit package;
+        yarn.nodemanager = {
+          enable = true;
+          openFirewall = true;
+        };
+        yarnSite = options.services.hadoop.yarnSite.default // {
+          "yarn.resourcemanager.hostname" = "resourcemanager";
+          "yarn.nodemanager.log-dirs" = "/tmp/userlogs";
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    resourcemanager.wait_for_unit("yarn-resourcemanager")
+    resourcemanager.wait_for_unit("network.target")
+    resourcemanager.wait_for_open_port(8031)
+    resourcemanager.wait_for_open_port(8088)
+
+    nodemanager.wait_for_unit("yarn-nodemanager")
+    nodemanager.wait_for_unit("network.target")
+    nodemanager.wait_for_open_port(8042)
+
+    resourcemanager.succeed("curl -f http://localhost:8088")
+    nodemanager.succeed("curl -f http://localhost:8042")
+  '';
+})
diff --git a/nixos/tests/haka.nix b/nixos/tests/haka.nix
new file mode 100644
index 00000000000..dd65a6bcf11
--- /dev/null
+++ b/nixos/tests/haka.nix
@@ -0,0 +1,24 @@
+# This test runs haka and probes it with hakactl
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "haka";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ tvestelind ];
+  };
+
+  nodes = {
+    haka =
+      { ... }:
+        {
+          services.haka.enable = true;
+        };
+    };
+
+  testScript = ''
+    start_all()
+
+    haka.wait_for_unit("haka.service")
+    haka.succeed("hakactl status")
+    haka.succeed("hakactl stop")
+  '';
+})
diff --git a/nixos/tests/haproxy.nix b/nixos/tests/haproxy.nix
new file mode 100644
index 00000000000..b6ff4102fe6
--- /dev/null
+++ b/nixos/tests/haproxy.nix
@@ -0,0 +1,54 @@
+import ./make-test-python.nix ({ pkgs, ...}: {
+  name = "haproxy";
+  nodes = {
+    machine = { ... }: {
+      imports = [ ../modules/profiles/minimal.nix ];
+      services.haproxy = {
+        enable = true;
+        config = ''
+          defaults
+            timeout connect 10s
+
+          backend http_server
+            mode http
+            server httpd [::1]:8000
+
+          frontend http
+            bind *:80
+            mode http
+            http-request use-service prometheus-exporter if { path /metrics }
+            use_backend http_server
+        '';
+      };
+      services.httpd = {
+        enable = true;
+        virtualHosts.localhost = {
+          documentRoot = pkgs.writeTextDir "index.txt" "We are all good!";
+          adminAddr = "notme@yourhost.local";
+          listen = [{
+            ip = "::1";
+            port = 8000;
+          }];
+        };
+      };
+    };
+  };
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("multi-user.target")
+    machine.wait_for_unit("haproxy.service")
+    machine.wait_for_unit("httpd.service")
+    assert "We are all good!" in machine.succeed("curl -fk http://localhost:80/index.txt")
+    assert "haproxy_process_pool_allocated_bytes" in machine.succeed(
+        "curl -fk http://localhost:80/metrics"
+    )
+
+    with subtest("reload"):
+        machine.succeed("systemctl reload haproxy")
+        # wait some time to ensure the following request hits the reloaded haproxy
+        machine.sleep(5)
+        assert "We are all good!" in machine.succeed(
+            "curl -fk http://localhost:80/index.txt"
+        )
+  '';
+})
diff --git a/nixos/tests/hardened.nix b/nixos/tests/hardened.nix
new file mode 100644
index 00000000000..dc455f971f5
--- /dev/null
+++ b/nixos/tests/hardened.nix
@@ -0,0 +1,101 @@
+import ./make-test-python.nix ({ pkgs, ... } : {
+  name = "hardened";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ joachifm ];
+  };
+
+  machine =
+    { lib, pkgs, config, ... }:
+    with lib;
+    { users.users.alice = { isNormalUser = true; extraGroups = [ "proc" ]; };
+      users.users.sybil = { isNormalUser = true; group = "wheel"; };
+      imports = [ ../modules/profiles/hardened.nix ];
+      environment.memoryAllocator.provider = "graphene-hardened";
+      nix.settings.sandbox = false;
+      virtualisation.emptyDiskImages = [ 4096 ];
+      boot.initrd.postDeviceCommands = ''
+        ${pkgs.dosfstools}/bin/mkfs.vfat -n EFISYS /dev/vdb
+      '';
+      virtualisation.fileSystems = {
+        "/efi" = {
+          device = "/dev/disk/by-label/EFISYS";
+          fsType = "vfat";
+          options = [ "noauto" ];
+        };
+      };
+      boot.extraModulePackages =
+        optional (versionOlder config.boot.kernelPackages.kernel.version "5.6")
+          config.boot.kernelPackages.wireguard;
+      boot.kernelModules = [ "wireguard" ];
+    };
+
+  testScript =
+    let
+      hardened-malloc-tests = pkgs.graphene-hardened-malloc.ld-preload-tests;
+    in
+    ''
+      machine.wait_for_unit("multi-user.target")
+
+
+      with subtest("AppArmor profiles are loaded"):
+          machine.succeed("systemctl status apparmor.service")
+
+
+      # AppArmor securityfs
+      with subtest("AppArmor securityfs is mounted"):
+          machine.succeed("mountpoint -q /sys/kernel/security")
+          machine.succeed("cat /sys/kernel/security/apparmor/profiles")
+
+
+      # Test loading out-of-tree modules
+      with subtest("Out-of-tree modules can be loaded"):
+          machine.succeed("grep -Fq wireguard /proc/modules")
+
+
+      # Test kernel module hardening
+      with subtest("No more kernel modules can be loaded"):
+          # note: this better a be module we normally wouldn't load ...
+          machine.wait_for_unit("disable-kernel-module-loading.service")
+          machine.fail("modprobe dccp")
+
+
+      # Test userns
+      with subtest("User namespaces are restricted"):
+          machine.succeed("unshare --user true")
+          machine.fail("su -l alice -c 'unshare --user true'")
+
+
+      # Test dmesg restriction
+      with subtest("Regular users cannot access dmesg"):
+          machine.fail("su -l alice -c dmesg")
+
+
+      # Test access to kcore
+      with subtest("Kcore is inaccessible as root"):
+          machine.fail("cat /proc/kcore")
+
+
+      # Test deferred mount
+      with subtest("Deferred mounts work"):
+          machine.fail("mountpoint -q /efi")  # was deferred
+          machine.execute("mkdir -p /efi")
+          machine.succeed("mount /dev/disk/by-label/EFISYS /efi")
+          machine.succeed("mountpoint -q /efi")  # now mounted
+
+
+      # Test Nix dæmon usage
+      with subtest("nix-daemon cannot be used by all users"):
+          machine.fail("su -l nobody -s /bin/sh -c 'nix ping-store'")
+          machine.succeed("su -l alice -c 'nix ping-store'")
+
+
+      # Test kernel image protection
+      with subtest("The kernel image is protected"):
+          machine.fail("systemctl hibernate")
+          machine.fail("systemctl kexec")
+
+
+      with subtest("The hardened memory allocator works"):
+          machine.succeed("${hardened-malloc-tests}/bin/run-tests")
+    '';
+})
diff --git a/nixos/tests/hedgedoc.nix b/nixos/tests/hedgedoc.nix
new file mode 100644
index 00000000000..657d49c555e
--- /dev/null
+++ b/nixos/tests/hedgedoc.nix
@@ -0,0 +1,60 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+{
+  name = "hedgedoc";
+
+  meta = with lib.maintainers; {
+    maintainers = [ willibutz ];
+  };
+
+  nodes = {
+    hedgedocSqlite = { ... }: {
+      services = {
+        hedgedoc = {
+          enable = true;
+          configuration.dbURL = "sqlite:///var/lib/hedgedoc/hedgedoc.db";
+        };
+      };
+    };
+
+    hedgedocPostgres = { ... }: {
+      systemd.services.hedgedoc.after = [ "postgresql.service" ];
+      services = {
+        hedgedoc = {
+          enable = true;
+          configuration.dbURL = "postgres://hedgedoc:\${DB_PASSWORD}@localhost:5432/hedgedocdb";
+
+          /*
+           * Do not use pkgs.writeText for secrets as
+           * they will end up in the world-readable Nix store.
+           */
+          environmentFile = pkgs.writeText "hedgedoc-env" ''
+            DB_PASSWORD=snakeoilpassword
+          '';
+        };
+        postgresql = {
+          enable = true;
+          initialScript = pkgs.writeText "pg-init-script.sql" ''
+            CREATE ROLE hedgedoc LOGIN PASSWORD 'snakeoilpassword';
+            CREATE DATABASE hedgedocdb OWNER hedgedoc;
+          '';
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    with subtest("HedgeDoc sqlite"):
+        hedgedocSqlite.wait_for_unit("hedgedoc.service")
+        hedgedocSqlite.wait_for_open_port(3000)
+        hedgedocSqlite.wait_until_succeeds("curl -sSf http://localhost:3000/new")
+
+    with subtest("HedgeDoc postgres"):
+        hedgedocPostgres.wait_for_unit("postgresql.service")
+        hedgedocPostgres.wait_for_unit("hedgedoc.service")
+        hedgedocPostgres.wait_for_open_port(5432)
+        hedgedocPostgres.wait_for_open_port(3000)
+        hedgedocPostgres.wait_until_succeeds("curl -sSf http://localhost:3000/new")
+  '';
+})
diff --git a/nixos/tests/herbstluftwm.nix b/nixos/tests/herbstluftwm.nix
new file mode 100644
index 00000000000..7d079f4bfb6
--- /dev/null
+++ b/nixos/tests/herbstluftwm.nix
@@ -0,0 +1,37 @@
+import ./make-test-python.nix ({ lib, ...} : {
+  name = "herbstluftwm";
+
+  meta = {
+    maintainers = with lib.maintainers; [ thibautmarty ];
+  };
+
+  machine = { pkgs, lib, ... }: {
+    imports = [ ./common/x11.nix ./common/user-account.nix ];
+    test-support.displayManager.auto.user = "alice";
+    services.xserver.displayManager.defaultSession = lib.mkForce "none+herbstluftwm";
+    services.xserver.windowManager.herbstluftwm.enable = true;
+    environment.systemPackages = [ pkgs.dzen2 ]; # needed for upstream provided panel
+  };
+
+  testScript = ''
+    with subtest("ensure x starts"):
+        machine.wait_for_x()
+        machine.wait_for_file("/home/alice/.Xauthority")
+        machine.succeed("xauth merge ~alice/.Xauthority")
+
+    with subtest("ensure client is available"):
+        machine.succeed("herbstclient --version")
+
+    with subtest("ensure keybindings are set"):
+        machine.wait_until_succeeds("herbstclient list_keybinds | grep xterm")
+
+    with subtest("ensure panel starts"):
+        machine.wait_for_window("dzen title")
+
+    with subtest("ensure we can open a new terminal"):
+        machine.send_key("alt-ret")
+        machine.wait_for_window(r"alice.*?machine")
+        machine.sleep(2)
+        machine.screenshot("terminal")
+  '';
+})
diff --git a/nixos/tests/hibernate.nix b/nixos/tests/hibernate.nix
new file mode 100644
index 00000000000..3880f1649bd
--- /dev/null
+++ b/nixos/tests/hibernate.nix
@@ -0,0 +1,122 @@
+# Test whether hibernation from partition works.
+
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+
+let
+  # System configuration of the installed system, which is used for the actual
+  # hibernate testing.
+  installedConfig = with pkgs.lib; {
+    imports = [
+      ../modules/testing/test-instrumentation.nix
+      ../modules/profiles/qemu-guest.nix
+      ../modules/profiles/minimal.nix
+    ];
+
+    hardware.enableAllFirmware = mkForce false;
+    documentation.nixos.enable = false;
+    boot.loader.grub.device = "/dev/vda";
+
+    systemd.services.backdoor.conflicts = [ "sleep.target" ];
+
+    powerManagement.resumeCommands = "systemctl --no-block restart backdoor.service";
+
+    fileSystems = {
+      "/".device = "/dev/vda2";
+    };
+    swapDevices = mkOverride 0 [ { device = "/dev/vda1"; } ];
+  };
+  installedSystem = (import ../lib/eval-config.nix {
+    inherit system;
+    modules = [ installedConfig ];
+  }).config.system.build.toplevel;
+in makeTest {
+  name = "hibernate";
+
+  nodes = {
+    # System configuration used for installing the installedConfig from above.
+    machine = { config, lib, pkgs, ... }: with lib; {
+      imports = [
+        ../modules/profiles/installation-device.nix
+        ../modules/profiles/base.nix
+      ];
+
+      nix.settings = {
+        substituters = mkForce [];
+        hashed-mirrors = null;
+        connect-timeout = 1;
+      };
+
+      virtualisation.diskSize = 8 * 1024;
+      virtualisation.emptyDiskImages = [
+        # Small root disk for installer
+        512
+      ];
+      virtualisation.bootDevice = "/dev/vdb";
+    };
+  };
+
+  # 9P doesn't support reconnection to virtio transport after a hibernation.
+  # Therefore, machine just hangs on any Nix store access.
+  # To avoid this, we install NixOS onto a temporary disk with everything we need
+  # included into the store.
+
+  testScript =
+    ''
+      def create_named_machine(name):
+          machine = create_machine(
+              {
+                  "qemuFlags": "-cpu max ${
+                    if system == "x86_64-linux" then "-m 1024"
+                    else "-m 768 -enable-kvm -machine virt,gic-version=host"}",
+                  "hdaInterface": "virtio",
+                  "hda": "vm-state-machine/machine.qcow2",
+                  "name": name,
+              }
+          )
+          driver.machines.append(machine)
+          return machine
+
+
+      # Install NixOS
+      machine.start()
+      machine.succeed(
+          # Partition /dev/vda
+          "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+          + " mkpart primary linux-swap 1M 1024M"
+          + " mkpart primary ext2 1024M -1s",
+          "udevadm settle",
+          "mkfs.ext3 -L nixos /dev/vda2",
+          "mount LABEL=nixos /mnt",
+          "mkswap /dev/vda1 -L swap",
+          # Install onto /mnt
+          "nix-store --load-db < ${pkgs.closureInfo {rootPaths = [installedSystem];}}/registration",
+          "nixos-install --root /mnt --system ${installedSystem} --no-root-passwd --no-channel-copy >&2",
+      )
+      machine.shutdown()
+
+      # Start up
+      hibernate = create_named_machine("hibernate")
+
+      # Drop in file that checks if we un-hibernated properly (and not booted fresh)
+      hibernate.succeed(
+          "mkdir /run/test",
+          "mount -t ramfs -o size=1m ramfs /run/test",
+          "echo not persisted to disk > /run/test/suspended",
+      )
+
+      # Hibernate machine
+      hibernate.execute("systemctl hibernate >&2 &", check_return=False)
+      hibernate.wait_for_shutdown()
+
+      # Restore machine from hibernation, validate our ramfs file is there.
+      resume = create_named_machine("resume")
+      resume.start()
+      resume.succeed("grep 'not persisted to disk' /run/test/suspended")
+    '';
+
+}
diff --git a/nixos/tests/hitch/default.nix b/nixos/tests/hitch/default.nix
new file mode 100644
index 00000000000..a1d8e616260
--- /dev/null
+++ b/nixos/tests/hitch/default.nix
@@ -0,0 +1,33 @@
+import ../make-test-python.nix ({ pkgs, ... }:
+{
+  name = "hitch";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ jflanglois ];
+  };
+  machine = { pkgs, ... }: {
+    environment.systemPackages = [ pkgs.curl ];
+    services.hitch = {
+      enable = true;
+      backend = "[127.0.0.1]:80";
+      pem-files = [
+        ./example.pem
+      ];
+    };
+
+    services.httpd = {
+      enable = true;
+      virtualHosts.localhost.documentRoot = ./example;
+      adminAddr = "noone@testing.nowhere";
+    };
+  };
+
+  testScript =
+    ''
+      start_all()
+
+      machine.wait_for_unit("multi-user.target")
+      machine.wait_for_unit("hitch.service")
+      machine.wait_for_open_port(443)
+      assert "We are all good!" in machine.succeed("curl -fk https://localhost:443/index.txt")
+    '';
+})
diff --git a/nixos/tests/hitch/example.pem b/nixos/tests/hitch/example.pem
new file mode 100644
index 00000000000..fde6f3cbd19
--- /dev/null
+++ b/nixos/tests/hitch/example.pem
@@ -0,0 +1,53 @@
+-----BEGIN CERTIFICATE-----
+MIIEKTCCAxGgAwIBAgIJAIFAWQXSZ7lIMA0GCSqGSIb3DQEBCwUAMIGqMQswCQYD
+VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMUmVkd29vZCBD
+aXR5MRkwFwYDVQQKDBBUZXN0aW5nIDEyMyBJbmMuMRQwEgYDVQQLDAtJVCBTZXJ2
+aWNlczEYMBYGA1UEAwwPdGVzdGluZy5ub3doZXJlMSQwIgYJKoZIhvcNAQkBFhVu
+b29uZUB0ZXN0aW5nLm5vd2hlcmUwHhcNMTgwNDIzMDcxMTI5WhcNMTkwNDIzMDcx
+MTI5WjCBqjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFTATBgNV
+BAcMDFJlZHdvb2QgQ2l0eTEZMBcGA1UECgwQVGVzdGluZyAxMjMgSW5jLjEUMBIG
+A1UECwwLSVQgU2VydmljZXMxGDAWBgNVBAMMD3Rlc3Rpbmcubm93aGVyZTEkMCIG
+CSqGSIb3DQEJARYVbm9vbmVAdGVzdGluZy5ub3doZXJlMIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEAxQq6AA9o/QErMbQwfgDF4mqXcvglRTwPr2zPE6Rv
+1g0ncRBSMM8iKbPapHM6qHNfg2e1fU2SFqzD6HkyZqHHLCgLzkdzswEcEjsMqiUP
+OR++5g4CWoQrdTi31itzYzCjnQ45BrAMrLEhBQgDTNwrEE+Tit0gpOGggtj/ktLk
+OD8BKa640lkmWEUGF18fd3rYTUC4hwM5qhAVXTe21vj9ZWsgprpQKdN61v0dCUap
+C5eAgvZ8Re+Cd0Id674hK4cJ4SekqfHKv/jLyIg3Vsdc9nkhmiC4O6KH5f1Zzq2i
+E4Kd5mnJDFxfSzIErKWmbhriLWsj3KEJ983AGLJ9hxQTAwIDAQABo1AwTjAdBgNV
+HQ4EFgQU76Mm6DP/BePJRQUNrJ9z038zjocwHwYDVR0jBBgwFoAU76Mm6DP/BePJ
+RQUNrJ9z038zjocwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAZzt
+VdPaUqrvDAh5rMYqzYMJ3tj6daNYoX6CbTFoevK5J5D4FESM0D/FMKgpNiVz39kB
+8Cjaw5rPHMHY61rHz7JRDK1sWXsonwzCF21BK7Tx0G1CIfLpYHWYb/FfdWGROx+O
+hPgKuoMRWQB+txozkZp5BqWJmk5MOyFCDEXhMOmrfsJq0IYU6QaH3Lsf1oJRy4yU
+afFrT9o3DLOyYLG/j/HXijCu8DVjZVa4aboum79ecYzPjjGF1posrFUnvQiuAeYy
+t7cuHNUB8gW9lWR5J7tP8fzFWtIcyT2oRL8u3H+fXf0i4bW73wtOBOoeULBzBNE7
+6rphcSrQunSZQIc+hg==
+-----END CERTIFICATE-----
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDFCroAD2j9ASsx
+tDB+AMXiapdy+CVFPA+vbM8TpG/WDSdxEFIwzyIps9qkczqoc1+DZ7V9TZIWrMPo
+eTJmoccsKAvOR3OzARwSOwyqJQ85H77mDgJahCt1OLfWK3NjMKOdDjkGsAyssSEF
+CANM3CsQT5OK3SCk4aCC2P+S0uQ4PwEprrjSWSZYRQYXXx93ethNQLiHAzmqEBVd
+N7bW+P1layCmulAp03rW/R0JRqkLl4CC9nxF74J3Qh3rviErhwnhJ6Sp8cq/+MvI
+iDdWx1z2eSGaILg7oofl/VnOraITgp3mackMXF9LMgSspaZuGuItayPcoQn3zcAY
+sn2HFBMDAgMBAAECggEAcaR8HijFHpab+PC5vxJnDuz3KEHiDQpU6ZJR5DxEnCm+
+A8GsBaaRR4gJpCspO5o/DiS0Ue55QUanPt8XqIXJv7fhBznCiw0qyYDxDviMzR94
+FGskBFySS+tIa+dnh1+4HY7kaO0Egl0udB5o+N1KoP+kUsSyXSYcUxsgW+fx5FW9
+22Ya3HNWnWxMCSfSGGlTFXGj2whf25SkL25dM9iblO4ZOx4MX8kaXij7TaYy8hMM
+Vf6/OMnXqtPKho+ctZZVKZkE9PxdS4f/pnp5EsdoOZwNBtfQ1WqVLWd3DlGWhnsH
+7L8ZSP2HkoI4Pd1wtkpOKZc+yM2bFXWa8WY4TcmpUQKBgQD33HxGdtmtZehrexSA
+/ZwWJlMslUsNz4Ivv6s7J4WCRhdh94+r9TWQP/yHdT9Ry5bvn84I5ZLUdp+aA962
+mvjz+GIglkCGpA7HU/hqurB1O63pj2cIDB8qhV21zjVIoqXcQ7IBJ+tqD79nF8vm
+h3KfuHUhuu1rayGepbtIyNhLdwKBgQDLgw4TJBg/QB8RzYECk78QnfZpCExsQA/z
+YJpc+dF2/nsid5R2u9jWzfmgHM2Jjo2/+ofRUaTqcFYU0K57CqmQkOLIzsbNQoYt
+e2NOANNVHiZLuzTZC2r3BrrkNbo3YvQzhAesUA5lS6LfrxBLUKiwo2LU9NlmJs3b
+UPVFYI0/1QKBgCswxIcS1sOcam+wNtZzWuuRKhUuvrFdY3YmlBPuwxj8Vb7AgMya
+IgdM3xhLmgkKzPZchm6OcpOLSCxyWDDBuHfq5E6BYCUWGW0qeLNAbNdA2wFD99Qz
+KIskSjwP/sD1dql3MmF5L1CABf5U6zb0i0jBv8ds50o8lNMsVgJM3UPpAoGBAL1+
+nzllb4pdi1CJWKnspoizfQCZsIdPM0r71V/jYY36MO+MBtpz2NlSWzAiAaQm74gl
+oBdgfT2qMg0Zro11BSRONEykdOolGkj5TiMQk7b65s+3VeMPRZ8UTis2d9kgs5/Q
+PVDODkl1nwfGu1ZVmW04BUujXVZHpYCkJm1eFMetAoGAImE7gWj+qRMhpbtCCGCg
+z06gDKvMrF6S+GJsvUoSyM8oUtfdPodI6gWAC65NfYkIiqbpCaEVNzfui73f5Lnz
+p5X1IbzhuH5UZs/k5A3OR2PPDbPs3lqEw7YJdBdLVRmO1o824uaXaJJwkL/1C+lq
+8dh1wV3CnynNmZApkz4vpzQ=
+-----END PRIVATE KEY-----
diff --git a/nixos/tests/hitch/example/index.txt b/nixos/tests/hitch/example/index.txt
new file mode 100644
index 00000000000..0478b1c2635
--- /dev/null
+++ b/nixos/tests/hitch/example/index.txt
@@ -0,0 +1 @@
+We are all good!
diff --git a/nixos/tests/hledger-web.nix b/nixos/tests/hledger-web.nix
new file mode 100644
index 00000000000..f8919f7d4bd
--- /dev/null
+++ b/nixos/tests/hledger-web.nix
@@ -0,0 +1,50 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+let
+  journal = pkgs.writeText "test.journal" ''
+    2010/01/10 Loan
+        assets:cash                 500$
+        income:loan                -500$
+    2010/01/10 NixOS Foundation donation
+        expenses:donation           250$
+        assets:cash                -250$
+  '';
+in
+rec {
+  name = "hledger-web";
+  meta.maintainers = with lib.maintainers; [ marijanp ];
+
+  nodes = rec {
+    server = { config, pkgs, ... }: {
+      services.hledger-web = {
+        host = "127.0.0.1";
+        port = 5000;
+        enable = true;
+        capabilities.manage = true;
+      };
+      networking.firewall.allowedTCPPorts = [ config.services.hledger-web.port ];
+      systemd.services.hledger-web.preStart = ''
+        ln -s ${journal} /var/lib/hledger-web/.hledger.journal
+      '';
+    };
+    apiserver = { ... }: {
+      imports = [ server ];
+      services.hledger-web.serveApi = true;
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    server.wait_for_unit("hledger-web.service")
+    server.wait_for_open_port(5000)
+    with subtest("Check if web UI is accessible"):
+        page = server.succeed("curl -L http://127.0.0.1:5000")
+        assert ".hledger.journal" in page
+
+    apiserver.wait_for_unit("hledger-web.service")
+    apiserver.wait_for_open_port(5000)
+    with subtest("Check if the JSON API is served"):
+        transactions = apiserver.succeed("curl -L http://127.0.0.1:5000/transactions")
+        assert "NixOS Foundation donation" in transactions
+  '';
+})
diff --git a/nixos/tests/hocker-fetchdocker/default.nix b/nixos/tests/hocker-fetchdocker/default.nix
new file mode 100644
index 00000000000..e3979db3c60
--- /dev/null
+++ b/nixos/tests/hocker-fetchdocker/default.nix
@@ -0,0 +1,16 @@
+import ../make-test-python.nix ({ pkgs, ...} : {
+  name = "test-hocker-fetchdocker";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ixmatus ];
+    broken = true; # tries to download from registry-1.docker.io - how did this ever work?
+  };
+
+  machine = import ./machine.nix;
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("sockets.target")
+    machine.wait_until_succeeds("docker run registry-1.docker.io/v2/library/hello-world:latest")
+  '';
+})
diff --git a/nixos/tests/hocker-fetchdocker/hello-world-container.nix b/nixos/tests/hocker-fetchdocker/hello-world-container.nix
new file mode 100644
index 00000000000..a127875264e
--- /dev/null
+++ b/nixos/tests/hocker-fetchdocker/hello-world-container.nix
@@ -0,0 +1,19 @@
+{ fetchDockerConfig, fetchDockerLayer, fetchdocker }:
+fetchdocker rec {
+    name = "hello-world";
+    registry = "https://registry-1.docker.io/v2/";
+    repository = "library";
+    imageName = "hello-world";
+    tag = "latest";
+    imageConfig = fetchDockerConfig {
+      inherit tag registry repository imageName;
+      sha256 = "1ivbd23hyindkahzfw4kahgzi6ibzz2ablmgsz6340vc6qr1gagj";
+    };
+    imageLayers = let
+      layer0 = fetchDockerLayer {
+        inherit registry repository imageName;
+        layerDigest = "ca4f61b1923c10e9eb81228bd46bee1dfba02b9c7dac1844527a734752688ede";
+        sha256 = "1plfd194fwvsa921ib3xkhms1yqxxrmx92r2h7myj41wjaqn2kya";
+      };
+      in [ layer0 ];
+  }
diff --git a/nixos/tests/hocker-fetchdocker/machine.nix b/nixos/tests/hocker-fetchdocker/machine.nix
new file mode 100644
index 00000000000..885adebe149
--- /dev/null
+++ b/nixos/tests/hocker-fetchdocker/machine.nix
@@ -0,0 +1,26 @@
+{ pkgs, ... }:
+{ nixpkgs.config.packageOverrides = pkgs': {
+    hello-world-container = pkgs'.callPackage ./hello-world-container.nix { };
+  };
+
+  virtualisation.docker = {
+    enable  = true;
+    package = pkgs.docker;
+  };
+
+  systemd.services.docker-load-fetchdocker-image = {
+    description = "Docker load hello-world-container";
+    wantedBy    = [ "multi-user.target" ];
+    wants       = [ "docker.service" ];
+    after       = [ "docker.service" ];
+
+    script = ''
+      ${pkgs.hello-world-container}/compositeImage.sh | ${pkgs.docker}/bin/docker load
+    '';
+
+    serviceConfig = {
+      Type = "oneshot";
+    };
+  };
+}
+
diff --git a/nixos/tests/hockeypuck.nix b/nixos/tests/hockeypuck.nix
new file mode 100644
index 00000000000..19df9dee3d3
--- /dev/null
+++ b/nixos/tests/hockeypuck.nix
@@ -0,0 +1,63 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+let
+  gpgKeyring = (pkgs.runCommand "gpg-keyring" { buildInputs = [ pkgs.gnupg ]; } ''
+    mkdir -p $out
+    export GNUPGHOME=$out
+    cat > foo <<EOF
+      %echo Generating a basic OpenPGP key
+      %no-protection
+      Key-Type: DSA
+      Key-Length: 1024
+      Subkey-Type: ELG-E
+      Subkey-Length: 1024
+      Name-Real: Foo Example
+      Name-Email: foo@example.org
+      Expire-Date: 0
+      # Do a commit here, so that we can later print "done"
+      %commit
+      %echo done
+    EOF
+    gpg --batch --generate-key foo
+    rm $out/S.gpg-agent $out/S.gpg-agent.*
+  '');
+in {
+  name = "hockeypuck";
+  meta.maintainers = with lib.maintainers; [ etu ];
+
+  machine = { ... }: {
+    # Used for test
+    environment.systemPackages = [ pkgs.gnupg ];
+
+    services.hockeypuck.enable = true;
+
+    services.postgresql = {
+      enable = true;
+      ensureDatabases = [ "hockeypuck" ];
+      ensureUsers = [{
+        name = "hockeypuck";
+        ensurePermissions."DATABASE hockeypuck" = "ALL PRIVILEGES";
+      }];
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("hockeypuck.service")
+    machine.wait_for_open_port(11371)
+
+    response = machine.succeed("curl -vvv -s http://127.0.0.1:11371/")
+
+    assert "<title>OpenPGP Keyserver</title>" in response, "HTML title not found"
+
+    # Copy the keyring
+    machine.succeed("cp -R ${gpgKeyring} /tmp/GNUPGHOME")
+
+    # Extract our GPG key id
+    keyId = machine.succeed("GNUPGHOME=/tmp/GNUPGHOME gpg --list-keys | grep dsa1024 --after-context=1 | grep -v dsa1024").strip()
+
+    # Send the key to our local keyserver
+    machine.succeed("GNUPGHOME=/tmp/GNUPGHOME gpg --keyserver hkp://127.0.0.1:11371 --send-keys " + keyId)
+
+    # Recieve the key from our local keyserver to a separate directory
+    machine.succeed("GNUPGHOME=$(mktemp -d) gpg --keyserver hkp://127.0.0.1:11371 --recv-keys " + keyId)
+  '';
+})
diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant.nix
new file mode 100644
index 00000000000..10f9cb05c9c
--- /dev/null
+++ b/nixos/tests/home-assistant.nix
@@ -0,0 +1,156 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+let
+  configDir = "/var/lib/foobar";
+in {
+  name = "home-assistant";
+  meta.maintainers = lib.teams.home-assistant.members;
+
+  nodes.hass = { pkgs, ... }: {
+    environment.systemPackages = with pkgs; [ mosquitto ];
+
+    services.postgresql = {
+      enable = true;
+      ensureDatabases = [ "hass" ];
+      ensureUsers = [{
+        name = "hass";
+        ensurePermissions = {
+          "DATABASE hass" = "ALL PRIVILEGES";
+        };
+      }];
+    };
+
+    services.home-assistant = {
+      enable = true;
+      inherit configDir;
+
+      # tests loading components by overriding the package
+      package = (pkgs.home-assistant.override {
+        extraPackages = ps: with ps; [
+          colorama
+        ];
+        extraComponents = [ "zha" ];
+      }).overrideAttrs (oldAttrs: {
+        doInstallCheck = false;
+      });
+
+      # tests loading components from the module
+      extraComponents = [
+        "wake_on_lan"
+      ];
+
+      # test extra package passing from the module
+      extraPackages = python3Packages: with python3Packages; [
+        psycopg2
+      ];
+
+      config = {
+        homeassistant = {
+          name = "Home";
+          time_zone = "UTC";
+          latitude = "0.0";
+          longitude = "0.0";
+          elevation = 0;
+        };
+
+        # configure the recorder component to use the postgresql db
+        recorder.db_url = "postgresql://@/hass";
+
+        # we can't load default_config, because the updater requires
+        # network access and would cause an error, so load frontend
+        # here explicitly.
+        # https://www.home-assistant.io/integrations/frontend/
+        frontend = {};
+
+        # set up a wake-on-lan switch to test capset capability required
+        # for the ping suid wrapper
+        # https://www.home-assistant.io/integrations/wake_on_lan/
+        switch = [ {
+          platform = "wake_on_lan";
+          mac = "00:11:22:33:44:55";
+          host = "127.0.0.1";
+        } ];
+
+        # test component-based capability assignment (CAP_NET_BIND_SERVICE)
+        # https://www.home-assistant.io/integrations/emulated_hue/
+        emulated_hue = {
+          host_ip = "127.0.0.1";
+          listen_port = 80;
+        };
+
+        # https://www.home-assistant.io/integrations/logger/
+        logger = {
+          default = "info";
+        };
+      };
+
+      # configure the sample lovelace dashboard
+      lovelaceConfig = {
+        title = "My Awesome Home";
+        views = [{
+          title = "Example";
+          cards = [{
+            type = "markdown";
+            title = "Lovelace";
+            content = "Welcome to your **Lovelace UI**.";
+          }];
+        }];
+      };
+      lovelaceConfigWritable = true;
+    };
+  };
+
+  testScript = ''
+    import re
+
+    start_all()
+
+    # Parse the package path out of the systemd unit, as we cannot
+    # access the final package, that is overriden inside the module,
+    # by any other means.
+    pattern = re.compile(r"path=(?P<path>[\/a-z0-9-.]+)\/bin\/hass")
+    response = hass.execute("systemctl show -p ExecStart home-assistant.service")[1]
+    match = pattern.search(response)
+    package = match.group('path')
+
+    hass.wait_for_unit("home-assistant.service")
+
+    with subtest("Check that YAML configuration file is in place"):
+        hass.succeed("test -L ${configDir}/configuration.yaml")
+
+    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"):
+        hass.wait_until_succeeds("journalctl -u home-assistant.service | grep -q 'Home Assistant initialized in'")
+        hass.wait_for_open_port(8123)
+        hass.succeed("curl --fail http://localhost:8123/lovelace")
+
+    with subtest("Check that capabilities are passed for emulated_hue to bind to port 80"):
+        hass.wait_for_open_port(80)
+        hass.succeed("curl --fail http://localhost:80/description.xml")
+
+    with subtest("Check extra components are considered in systemd unit hardening"):
+        hass.succeed("systemctl show -p DeviceAllow home-assistant.service | grep -q char-ttyUSB")
+
+    with subtest("Print log to ease debugging"):
+        output_log = hass.succeed("cat ${configDir}/home-assistant.log")
+        print("\n### home-assistant.log ###\n")
+        print(output_log + "\n")
+
+    with subtest("Check that no errors were logged"):
+        assert "ERROR" not in output_log
+
+    with subtest("Check systemd unit hardening"):
+        hass.log(hass.succeed("systemctl cat home-assistant.service"))
+        hass.log(hass.succeed("systemd-analyze security home-assistant.service"))
+  '';
+})
diff --git a/nixos/tests/hostname.nix b/nixos/tests/hostname.nix
new file mode 100644
index 00000000000..2e92b4259a6
--- /dev/null
+++ b/nixos/tests/hostname.nix
@@ -0,0 +1,72 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  makeHostNameTest = hostName: domain: fqdnOrNull:
+    let
+      fqdn = hostName + (optionalString (domain != null) ".${domain}");
+      getStr = str: # maybeString2String
+        let res = builtins.tryEval str;
+        in if (res.success && res.value != null) then res.value else "null";
+    in
+      makeTest {
+        name = "hostname-${fqdn}";
+        meta = with pkgs.lib.maintainers; {
+          maintainers = [ primeos blitz ];
+        };
+
+        machine = { lib, ... }: {
+          networking.hostName = hostName;
+          networking.domain = domain;
+
+          environment.systemPackages = with pkgs; [
+            inetutils
+          ];
+        };
+
+        testScript = { nodes, ... }: ''
+          start_all()
+
+          machine = ${hostName}
+
+          machine.wait_for_unit("network-online.target")
+
+          # Test if NixOS computes the correct FQDN (either a FQDN or an error/null):
+          assert "${getStr nodes.machine.config.networking.fqdn}" == "${getStr fqdnOrNull}"
+
+          # The FQDN, domain name, and hostname detection should work as expected:
+          assert "${fqdn}" == machine.succeed("hostname --fqdn").strip()
+          assert "${optionalString (domain != null) domain}" == machine.succeed("dnsdomainname").strip()
+          assert (
+              "${hostName}"
+              == machine.succeed(
+                  'hostnamectl status | grep "Static hostname" | cut -d: -f2'
+              ).strip()
+          )
+
+          # 127.0.0.1 and ::1 should resolve back to "localhost":
+          assert (
+              "localhost" == machine.succeed("getent hosts 127.0.0.1 | awk '{print $2}'").strip()
+          )
+          assert "localhost" == machine.succeed("getent hosts ::1 | awk '{print $2}'").strip()
+
+          # 127.0.0.2 should resolve back to the FQDN and hostname:
+          fqdn_and_host_name = "${optionalString (domain != null) "${hostName}.${domain} "}${hostName}"
+          assert (
+              fqdn_and_host_name
+              == machine.succeed("getent hosts 127.0.0.2 | awk '{print $2,$3}'").strip()
+          )
+        '';
+      };
+
+in
+{
+  noExplicitDomain = makeHostNameTest "ahost" null null;
+
+  explicitDomain = makeHostNameTest "ahost" "adomain" "ahost.adomain";
+}
diff --git a/nixos/tests/hound.nix b/nixos/tests/hound.nix
new file mode 100644
index 00000000000..4f51db1de9d
--- /dev/null
+++ b/nixos/tests/hound.nix
@@ -0,0 +1,59 @@
+# Test whether `houndd` indexes nixpkgs
+import ./make-test-python.nix ({ pkgs, ... } : {
+  name = "hound";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ grahamc ];
+  };
+  machine = { pkgs, ... }: {
+    services.hound = {
+      enable = true;
+      config = ''
+        {
+          "max-concurrent-indexers": 1,
+          "dbpath": "/var/lib/hound/data",
+          "repos": {
+            "nix": {
+              "url": "file:///var/lib/hound/my-git"
+            }
+          }
+        }
+      '';
+    };
+
+    systemd.services.houndseed = {
+      description = "seed hound with a git repo";
+      requiredBy = [ "hound.service" ];
+      before = [ "hound.service" ];
+
+      serviceConfig = {
+        User = "hound";
+        Group = "hound";
+        WorkingDirectory = "/var/lib/hound";
+      };
+      path = [ pkgs.git ];
+      script = ''
+        git config --global user.email "you@example.com"
+        git config --global user.name "Your Name"
+        git init my-git --bare
+        git init my-git-clone
+        cd my-git-clone
+        echo 'hi nix!' > hello
+        git add hello
+        git commit -m "hello there :)"
+        git remote add origin /var/lib/hound/my-git
+        git push origin master
+      '';
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("network.target")
+    machine.wait_for_unit("hound.service")
+    machine.wait_for_open_port(6080)
+    machine.wait_until_succeeds(
+        "curl -f http://127.0.0.1:6080/api/v1/search\?stats\=fosho\&repos\=\*\&rng=%3A20\&q\=hi\&files\=\&i=nope | grep 'Filename' | grep 'hello'"
+    )
+  '';
+})
diff --git a/nixos/tests/hydra/common.nix b/nixos/tests/hydra/common.nix
new file mode 100644
index 00000000000..fdf2b2c6f6d
--- /dev/null
+++ b/nixos/tests/hydra/common.nix
@@ -0,0 +1,48 @@
+{ system, ... }:
+{
+  baseConfig = { pkgs, ... }: let
+    trivialJob = pkgs.writeTextDir "trivial.nix" ''
+     { trivial = builtins.derivation {
+         name = "trivial";
+         system = "${system}";
+         builder = "/bin/sh";
+         allowSubstitutes = false;
+         preferLocalBuild = true;
+         args = ["-c" "echo success > $out; exit 0"];
+       };
+     }
+    '';
+
+    createTrivialProject = pkgs.stdenv.mkDerivation {
+      name = "create-trivial-project";
+      dontUnpack = true;
+      buildInputs = [ pkgs.makeWrapper ];
+      installPhase = "install -m755 -D ${./create-trivial-project.sh} $out/bin/create-trivial-project.sh";
+      postFixup = ''
+        wrapProgram "$out/bin/create-trivial-project.sh" --prefix PATH ":" ${pkgs.lib.makeBinPath [ pkgs.curl ]} --set EXPR_PATH ${trivialJob}
+      '';
+    };
+  in {
+    virtualisation.memorySize = 2048;
+    time.timeZone = "UTC";
+    environment.systemPackages = [ createTrivialProject pkgs.jq ];
+    services.hydra = {
+      enable = true;
+      # Hydra needs those settings to start up, so we add something not harmfull.
+      hydraURL = "example.com";
+      notificationSender = "example@example.com";
+      extraConfig = ''
+        email_notification = 1
+      '';
+    };
+    services.postfix.enable = true;
+    nix = {
+      distributedBuilds = true;
+      buildMachines = [{
+        hostName = "localhost";
+        systems = [ system ];
+      }];
+      settings.substituters = [];
+    };
+  };
+}
diff --git a/nixos/tests/hydra/create-trivial-project.sh b/nixos/tests/hydra/create-trivial-project.sh
new file mode 100755
index 00000000000..5aae2d5bf90
--- /dev/null
+++ b/nixos/tests/hydra/create-trivial-project.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+#
+# This script creates a project, a jobset with an input of type local
+# path. This local path is a directory that contains a Nix expression
+# to define a job.
+# The EXPR-PATH environment variable must be set with the local path.
+
+set -e
+
+URL=http://localhost:3000
+USERNAME="admin"
+PASSWORD="admin"
+PROJECT_NAME="trivial"
+JOBSET_NAME="trivial"
+EXPR_PATH=${EXPR_PATH:-}
+
+if [ -z $EXPR_PATH ]; then
+   echo "Environment variable EXPR_PATH must be set"
+   exit 1
+fi
+
+mycurl() {
+  curl --referer $URL -H "Accept: application/json" -H "Content-Type: application/json" $@
+}
+
+cat >data.json <<EOF
+{ "username": "$USERNAME", "password": "$PASSWORD" }
+EOF
+mycurl -X POST -d '@data.json' $URL/login -c hydra-cookie.txt
+
+cat >data.json <<EOF
+{
+  "displayname":"Trivial",
+  "enabled":"1",
+  "visible":"1"
+}
+EOF
+mycurl --silent -X PUT $URL/project/$PROJECT_NAME -d @data.json -b hydra-cookie.txt
+
+cat >data.json <<EOF
+{
+  "description": "Trivial",
+  "checkinterval": "60",
+  "enabled": "1",
+  "visible": "1",
+  "keepnr": "1",
+  "enableemail": true,
+  "emailoverride": "hydra@localhost",
+  "nixexprinput": "trivial",
+  "nixexprpath": "trivial.nix",
+  "inputs": {
+    "trivial": {
+      "value": "$EXPR_PATH",
+      "type": "path"
+    }
+  }
+}
+EOF
+mycurl --silent -X PUT $URL/jobset/$PROJECT_NAME/$JOBSET_NAME -d @data.json -b hydra-cookie.txt
diff --git a/nixos/tests/hydra/default.nix b/nixos/tests/hydra/default.nix
new file mode 100644
index 00000000000..ef5e677953d
--- /dev/null
+++ b/nixos/tests/hydra/default.nix
@@ -0,0 +1,59 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../../.. { inherit system config; }
+}:
+
+with import ../../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+
+  inherit (import ./common.nix { inherit system; }) baseConfig;
+
+  hydraPkgs = {
+    inherit (pkgs) hydra-unstable;
+  };
+
+  makeHydraTest = with pkgs.lib; name: package: makeTest {
+    name = "hydra-${name}";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ lewo ma27 ];
+    };
+
+    machine = { pkgs, lib, ... }: {
+      imports = [ baseConfig ];
+      services.hydra = { inherit package; };
+    };
+
+    testScript = ''
+      # let the system boot up
+      machine.wait_for_unit("multi-user.target")
+      # test whether the database is running
+      machine.wait_for_unit("postgresql.service")
+      # test whether the actual hydra daemons are running
+      machine.wait_for_unit("hydra-init.service")
+      machine.require_unit_state("hydra-queue-runner.service")
+      machine.require_unit_state("hydra-evaluator.service")
+      machine.require_unit_state("hydra-notify.service")
+
+      machine.succeed("hydra-create-user admin --role admin --password admin")
+
+      # create a project with a trivial job
+      machine.wait_for_open_port(3000)
+
+      # make sure the build as been successfully built
+      machine.succeed("create-trivial-project.sh")
+
+      machine.wait_until_succeeds(
+          'curl -L -s http://localhost:3000/build/1 -H "Accept: application/json" |  jq .buildstatus | xargs test 0 -eq'
+      )
+
+      machine.wait_until_succeeds(
+          'journalctl -eu hydra-notify.service -o cat | grep -q "sending mail notification to hydra@localhost"'
+      )
+    '';
+  };
+
+in
+
+mapAttrs makeHydraTest hydraPkgs
diff --git a/nixos/tests/i3wm.nix b/nixos/tests/i3wm.nix
new file mode 100644
index 00000000000..59b4ffe3986
--- /dev/null
+++ b/nixos/tests/i3wm.nix
@@ -0,0 +1,46 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "i3wm";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ aszlig ];
+  };
+
+  machine = { lib, ... }: {
+    imports = [ ./common/x11.nix ./common/user-account.nix ];
+    test-support.displayManager.auto.user = "alice";
+    services.xserver.displayManager.defaultSession = lib.mkForce "none+i3";
+    services.xserver.windowManager.i3.enable = true;
+  };
+
+  testScript = { ... }: ''
+    with subtest("ensure x starts"):
+        machine.wait_for_x()
+        machine.wait_for_file("/home/alice/.Xauthority")
+        machine.succeed("xauth merge ~alice/.Xauthority")
+
+    with subtest("ensure we get first configuration window"):
+        machine.wait_for_window(r".*?first configuration.*?")
+        machine.sleep(2)
+        machine.screenshot("started")
+
+    with subtest("ensure we generate and save a config"):
+        # press return to indicate we want to gen a new config
+        machine.send_key("\n")
+        machine.sleep(2)
+        machine.screenshot("preconfig")
+        # press alt then return to indicate we want to use alt as our Mod key
+        machine.send_key("alt")
+        machine.send_key("\n")
+        machine.sleep(2)
+        # make sure the config file is created before we continue
+        machine.wait_for_file("/home/alice/.config/i3/config")
+        machine.screenshot("postconfig")
+        machine.sleep(2)
+
+    with subtest("ensure we can open a new terminal"):
+        machine.send_key("alt-ret")
+        machine.sleep(2)
+        machine.wait_for_window(r"alice.*?machine")
+        machine.sleep(2)
+        machine.screenshot("terminal")
+  '';
+})
diff --git a/nixos/tests/icingaweb2.nix b/nixos/tests/icingaweb2.nix
new file mode 100644
index 00000000000..e631e667bd5
--- /dev/null
+++ b/nixos/tests/icingaweb2.nix
@@ -0,0 +1,71 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "icingaweb2";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ das_j ];
+  };
+
+  nodes = {
+    icingaweb2 = { config, pkgs, ... }: {
+      services.icingaweb2 = {
+        enable = true;
+
+        modulePackages = with pkgs.icingaweb2Modules; {
+          particles = theme-particles;
+          spring = theme-spring;
+        };
+
+        modules = {
+          doc.enable = true;
+          migrate.enable =  true;
+          setup.enable = true;
+          test.enable = true;
+          translation.enable = true;
+        };
+
+        generalConfig = {
+          global = {
+            module_path = "${pkgs.icingaweb2}/modules";
+          };
+        };
+
+        authentications = {
+          icingaweb = {
+            backend = "external";
+          };
+        };
+
+        groupBackends = {
+          icingaweb = {
+            backend = "db";
+            resource = "icingaweb_db";
+          };
+        };
+
+        resources = {
+          # Not used, so no DB server needed
+          icingaweb_db = {
+            type = "db";
+            db = "mysql";
+            host = "localhost";
+            username = "icingaweb2";
+            password = "icingaweb2";
+            dbname = "icingaweb2";
+          };
+        };
+
+        roles = {
+          Administrators = {
+            users = "*";
+            permissions = "*";
+          };
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    icingaweb2.wait_for_unit("multi-user.target")
+    icingaweb2.succeed("curl -sSf http://icingaweb2/authentication/login")
+  '';
+})
diff --git a/nixos/tests/iftop.nix b/nixos/tests/iftop.nix
new file mode 100644
index 00000000000..6d0090b3946
--- /dev/null
+++ b/nixos/tests/iftop.nix
@@ -0,0 +1,33 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+with lib;
+
+{
+  name = "iftop";
+  meta.maintainers = with pkgs.lib.maintainers; [ ma27 ];
+
+  nodes = {
+    withIftop = {
+      imports = [ ./common/user-account.nix ];
+      programs.iftop.enable = true;
+    };
+    withoutIftop = {
+      imports = [ ./common/user-account.nix ];
+      environment.systemPackages = [ pkgs.iftop ];
+    };
+  };
+
+  testScript = ''
+    with subtest("machine with iftop enabled"):
+        withIftop.wait_for_unit("default.target")
+        # limit to eth1 (eth0 is the test driver's control interface)
+        # and don't try name lookups
+        withIftop.succeed("su -l alice -c 'iftop -t -s 1 -n -i eth1'")
+
+    with subtest("machine without iftop"):
+        withoutIftop.wait_for_unit("default.target")
+        # check that iftop is there but user alice lacks capabilitie
+        withoutIftop.succeed("iftop -t -s 1 -n -i eth1")
+        withoutIftop.fail("su -l alice -c 'iftop -t -s 1 -n -i eth1'")
+  '';
+})
diff --git a/nixos/tests/ihatemoney/default.nix b/nixos/tests/ihatemoney/default.nix
new file mode 100644
index 00000000000..78278d2e869
--- /dev/null
+++ b/nixos/tests/ihatemoney/default.nix
@@ -0,0 +1,78 @@
+{ 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}";
+    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.opensmtpd = {
+        enable = true;
+        serverConfiguration = ''
+          listen on lo
+          action foo relay
+          match from any for any action foo
+        '';
+      };
+    };
+    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
new file mode 100644
index 00000000000..ebdd2651b04
--- /dev/null
+++ b/nixos/tests/ihatemoney/rates.json
@@ -0,0 +1,39 @@
+{
+  "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
new file mode 100644
index 00000000000..10e568b14b1
--- /dev/null
+++ b/nixos/tests/ihatemoney/server.crt
@@ -0,0 +1,28 @@
+-----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
new file mode 100644
index 00000000000..72a43577d64
--- /dev/null
+++ b/nixos/tests/ihatemoney/server.key
@@ -0,0 +1,52 @@
+-----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
new file mode 100644
index 00000000000..90908968a7e
--- /dev/null
+++ b/nixos/tests/image-contents.nix
@@ -0,0 +1,51 @@
+# Tests the contents attribute of nixos/lib/make-disk-image.nix
+# including its user, group, and mode attributes.
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+with import common/ec2.nix { inherit makeTest pkgs; };
+
+let
+  config = (import ../lib/eval-config.nix {
+    inherit system;
+    modules = [
+      ../modules/testing/test-instrumentation.nix
+      ../modules/profiles/qemu-guest.nix
+      {
+        fileSystems."/".device = "/dev/disk/by-label/nixos";
+        boot.loader.grub.device = "/dev/vda";
+        boot.loader.timeout = 0;
+      }
+    ];
+  }).config;
+  image = (import ../lib/make-disk-image.nix {
+    inherit pkgs config;
+    lib = pkgs.lib;
+    format = "qcow2";
+    contents = [{
+      source = pkgs.writeText "testFile" "contents";
+      target = "/testFile";
+      user = "1234";
+      group = "5678";
+      mode = "755";
+    }];
+  }) + "/nixos.qcow2";
+
+in makeEc2Test {
+  name = "image-contents";
+  inherit image;
+  userData = null;
+  script = ''
+    machine.start()
+    assert "content" in machine.succeed("cat /testFile")
+    fileDetails = machine.succeed("ls -l /testFile")
+    assert "1234" in fileDetails
+    assert "5678" in fileDetails
+    assert "rwxr-xr-x" in fileDetails
+  '';
+}
diff --git a/nixos/tests/incron.nix b/nixos/tests/incron.nix
new file mode 100644
index 00000000000..b22ee4c9a03
--- /dev/null
+++ b/nixos/tests/incron.nix
@@ -0,0 +1,52 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+{
+  name = "incron";
+  meta.maintainers = [ lib.maintainers.aanderse ];
+
+  machine =
+    { ... }:
+    { services.incron.enable = true;
+      services.incron.extraPackages = [ pkgs.coreutils ];
+      services.incron.systab = ''
+        /test IN_CREATE,IN_MODIFY,IN_CLOSE_WRITE,IN_MOVED_FROM,IN_MOVED_TO echo "$@/$# $%" >> /root/incron.log
+      '';
+
+      # ensure the directory to be monitored exists before incron is started
+      system.activationScripts.incronTest = ''
+        mkdir /test
+      '';
+    };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("multi-user.target")
+    machine.wait_for_unit("incron.service")
+
+    machine.succeed("test -d /test")
+    # create some activity for incron to monitor
+    machine.succeed("touch /test/file")
+    machine.succeed("echo foo >> /test/file")
+    machine.succeed("mv /test/file /root")
+    machine.succeed("mv /root/file /test")
+
+    machine.sleep(1)
+
+    # touch /test/file
+    machine.succeed("grep '/test/file IN_CREATE' /root/incron.log")
+
+    # echo foo >> /test/file
+    machine.succeed("grep '/test/file IN_MODIFY' /root/incron.log")
+    machine.succeed("grep '/test/file IN_CLOSE_WRITE' /root/incron.log")
+
+    # mv /test/file /root
+    machine.succeed("grep '/test/file IN_MOVED_FROM' /root/incron.log")
+
+    # mv /root/file /test
+    machine.succeed("grep '/test/file IN_MOVED_TO' /root/incron.log")
+
+    # ensure something unexpected is not present
+    machine.fail("grep 'IN_OPEN' /root/incron.log")
+  '';
+})
diff --git a/nixos/tests/influxdb.nix b/nixos/tests/influxdb.nix
new file mode 100644
index 00000000000..03026f8404b
--- /dev/null
+++ b/nixos/tests/influxdb.nix
@@ -0,0 +1,40 @@
+# This test runs influxdb and checks if influxdb is up and running
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "influxdb";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ offline ];
+  };
+
+  nodes = {
+    one = { ... }: {
+      services.influxdb.enable = true;
+      environment.systemPackages = [ pkgs.httpie ];
+    };
+  };
+
+  testScript = ''
+    import shlex
+
+    start_all()
+
+    one.wait_for_unit("influxdb.service")
+
+    # create database
+    one.succeed(
+        "curl -XPOST http://localhost:8086/query --data-urlencode 'q=CREATE DATABASE test'"
+    )
+
+    # write some points and run simple query
+    out = one.succeed(
+        "curl -XPOST 'http://localhost:8086/write?db=test' --data-binary 'cpu_load_short,host=server01,region=us-west value=0.64 1434055562000000000'"
+    )
+
+    qv = "SELECT value FROM cpu_load_short WHERE region='us-west'"
+    cmd = f'curl -GET "http://localhost:8086/query?db=test" --data-urlencode {shlex.quote("q="+ qv)}'
+    out = one.succeed(cmd)
+
+    assert "2015-06-11T20:46:02Z" in out
+    assert "0.64" in out
+  '';
+})
diff --git a/nixos/tests/initrd-network-openvpn/default.nix b/nixos/tests/initrd-network-openvpn/default.nix
new file mode 100644
index 00000000000..bb4c41e6d70
--- /dev/null
+++ b/nixos/tests/initrd-network-openvpn/default.nix
@@ -0,0 +1,145 @@
+import ../make-test-python.nix ({ lib, ...}:
+
+{
+  name = "initrd-network-openvpn";
+
+  nodes =
+    let
+
+      # Inlining of the shared secret for the
+      # OpenVPN server and client
+      secretblock = ''
+        secret [inline]
+        <secret>
+        ${lib.readFile ./shared.key}
+        </secret>
+        '';
+
+    in
+    {
+
+      # Minimal test case to check a successful boot, even with invalid config
+      minimalboot =
+        { ... }:
+        {
+          boot.initrd.network = {
+            enable = true;
+            openvpn = {
+              enable = true;
+              configuration = "/dev/null";
+            };
+          };
+        };
+
+      # initrd VPN client
+      ovpnclient =
+        { ... }:
+        {
+          virtualisation.useBootLoader = true;
+          virtualisation.vlans = [ 1 ];
+
+          boot.initrd = {
+            # This command does not fork to keep the VM in the state where
+            # only the initramfs is loaded
+            preLVMCommands =
+            ''
+              /bin/nc -p 1234 -lke /bin/echo TESTVALUE
+            '';
+
+            network = {
+              enable = true;
+
+              # Work around udhcpc only getting a lease on eth0
+              postCommands = ''
+                /bin/ip addr add 192.168.1.2/24 dev eth1
+              '';
+
+              # Example configuration for OpenVPN
+              # This is the main reason for this test
+              openvpn = {
+                enable = true;
+                configuration = "${./initrd.ovpn}";
+              };
+            };
+          };
+        };
+
+      # VPN server and gateway for ovpnclient between vlan 1 and 2
+      ovpnserver =
+        { ... }:
+        {
+          virtualisation.vlans = [ 1 2 ];
+
+          # Enable NAT and forward port 12345 to port 1234
+          networking.nat = {
+            enable = true;
+            internalInterfaces = [ "tun0" ];
+            externalInterface = "eth2";
+            forwardPorts = [ { destination = "10.8.0.2:1234";
+                               sourcePort = 12345; } ];
+          };
+
+          # Trust tun0 and allow the VPN Server to be reached
+          networking.firewall = {
+            trustedInterfaces = [ "tun0" ];
+            allowedUDPPorts = [ 1194 ];
+          };
+
+          # Minimal OpenVPN server configuration
+          services.openvpn.servers.testserver =
+          {
+            config = ''
+              dev tun0
+              ifconfig 10.8.0.1 10.8.0.2
+              ${secretblock}
+            '';
+          };
+        };
+
+      # Client that resides in the "external" VLAN
+      testclient =
+        { ... }:
+        {
+          virtualisation.vlans = [ 2 ];
+        };
+  };
+
+
+  testScript =
+    ''
+      # Minimal test case, checks whether enabling (with invalid config) harms
+      # the boot process
+      with subtest("Check for successful boot with broken openvpn config"):
+          minimalboot.start()
+          # If we get to multi-user.target, we booted successfully
+          minimalboot.wait_for_unit("multi-user.target")
+          minimalboot.shutdown()
+
+      # Elaborated test case where the ovpnclient (where this module is used)
+      # can be reached by testclient only over ovpnserver.
+      # This is an indirect test for success.
+      with subtest("Check for connection from initrd VPN client, config as file"):
+          ovpnserver.start()
+          testclient.start()
+          ovpnclient.start()
+
+          # Wait until the OpenVPN Server is available
+          ovpnserver.wait_for_unit("openvpn-testserver.service")
+          ovpnserver.succeed("ping -c 1 10.8.0.1")
+
+          # Wait for the client to connect
+          ovpnserver.wait_until_succeeds("ping -c 1 10.8.0.2")
+
+          # Wait until the testclient has network
+          testclient.wait_for_unit("network.target")
+
+          # Check that ovpnclient is reachable over vlan 1
+          ovpnserver.succeed("nc -w 2 192.168.1.2 1234 | grep -q TESTVALUE")
+
+          # Check that ovpnclient is reachable over tun0
+          ovpnserver.succeed("nc -w 2 10.8.0.2 1234 | grep -q TESTVALUE")
+
+          # Check that ovpnclient is reachable from testclient over the gateway
+          testclient.succeed("nc -w 2 192.168.2.3 12345 | grep -q TESTVALUE")
+    '';
+})
diff --git a/nixos/tests/initrd-network-openvpn/initrd.ovpn b/nixos/tests/initrd-network-openvpn/initrd.ovpn
new file mode 100644
index 00000000000..5926a48af00
--- /dev/null
+++ b/nixos/tests/initrd-network-openvpn/initrd.ovpn
@@ -0,0 +1,29 @@
+remote 192.168.1.3
+dev tun
+ifconfig 10.8.0.2 10.8.0.1
+# Only force VLAN 2 through the VPN
+route 192.168.2.0 255.255.255.0 10.8.0.1
+secret [inline]
+<secret>
+#
+# 2048 bit OpenVPN static key
+#
+-----BEGIN OpenVPN Static key V1-----
+553aabe853acdfe51cd6fcfea93dcbb0
+c8797deadd1187606b1ea8f2315eb5e6
+67c0d7e830f50df45686063b189d6c6b
+aab8bb3430cc78f7bb1f78628d5c3742
+0cef4f53a5acab2894905f4499f95d8e
+e69b7b6748b17016f89e19e91481a9fd
+bf8c10651f41a1d4fdf5f438925a6733
+13cec8f04701eb47b8f7ffc48bc3d7af
+65f07bce766015b87c3db4d668c655ff
+be5a69522a8e60ccb217f8521681b45d
+27c0b70bdfbfbb426c7646d80adf7482
+3ddac58b25cb1c1bb100de974478b4c6
+8b45a94261a2405e99810cb2b3abd49f
+21b3198ada87ff3c4e656a008e540a8d
+e7811584363597599cce2040a68ac00e
+f2125540e0f7f4adc37cb3f0d922eeb7
+-----END OpenVPN Static key V1-----
+</secret>
\ No newline at end of file
diff --git a/nixos/tests/initrd-network-openvpn/shared.key b/nixos/tests/initrd-network-openvpn/shared.key
new file mode 100644
index 00000000000..248a91a3e3d
--- /dev/null
+++ b/nixos/tests/initrd-network-openvpn/shared.key
@@ -0,0 +1,21 @@
+#
+# 2048 bit OpenVPN static key
+#
+-----BEGIN OpenVPN Static key V1-----
+553aabe853acdfe51cd6fcfea93dcbb0
+c8797deadd1187606b1ea8f2315eb5e6
+67c0d7e830f50df45686063b189d6c6b
+aab8bb3430cc78f7bb1f78628d5c3742
+0cef4f53a5acab2894905f4499f95d8e
+e69b7b6748b17016f89e19e91481a9fd
+bf8c10651f41a1d4fdf5f438925a6733
+13cec8f04701eb47b8f7ffc48bc3d7af
+65f07bce766015b87c3db4d668c655ff
+be5a69522a8e60ccb217f8521681b45d
+27c0b70bdfbfbb426c7646d80adf7482
+3ddac58b25cb1c1bb100de974478b4c6
+8b45a94261a2405e99810cb2b3abd49f
+21b3198ada87ff3c4e656a008e540a8d
+e7811584363597599cce2040a68ac00e
+f2125540e0f7f4adc37cb3f0d922eeb7
+-----END OpenVPN Static key V1-----
diff --git a/nixos/tests/initrd-network-ssh/default.nix b/nixos/tests/initrd-network-ssh/default.nix
new file mode 100644
index 00000000000..0ad0563b0ce
--- /dev/null
+++ b/nixos/tests/initrd-network-ssh/default.nix
@@ -0,0 +1,79 @@
+import ../make-test-python.nix ({ lib, ... }:
+
+{
+  name = "initrd-network-ssh";
+  meta = with lib.maintainers; {
+    maintainers = [ willibutz emily ];
+  };
+
+  nodes = with lib; {
+    server =
+      { config, ... }:
+      {
+        boot.kernelParams = [
+          "ip=${config.networking.primaryIPAddress}:::255.255.255.0::eth1:none"
+        ];
+        boot.initrd.network = {
+          enable = true;
+          ssh = {
+            enable = true;
+            authorizedKeys = [ (readFile ./id_ed25519.pub) ];
+            port = 22;
+            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
+              poweroff
+            fi
+            sleep 1
+          done
+        '';
+      };
+
+    client =
+      { config, ... }:
+      {
+        environment.etc = {
+          knownHosts = {
+            text = concatStrings [
+              "server,"
+              "${toString (head (splitString " " (
+                toString (elemAt (splitString "\n" config.networking.extraHosts) 2)
+              )))} "
+              "${readFile ./ssh_host_ed25519_key.pub}"
+            ];
+          };
+          sshKey = {
+            source = ./id_ed25519;
+            mode = "0600";
+          };
+        };
+      };
+  };
+
+  testScript = ''
+    start_all()
+    client.wait_for_unit("network.target")
+
+
+    def ssh_is_up(_) -> bool:
+        status, _ = client.execute("nc -z server 22")
+        return status == 0
+
+
+    with client.nested("waiting for SSH server to come up"):
+        retry(ssh_is_up)
+
+
+    client.succeed(
+        "ssh -i /etc/sshKey -o UserKnownHostsFile=/etc/knownHosts server 'touch /fnord'"
+    )
+    client.shutdown()
+  '';
+})
diff --git a/nixos/tests/initrd-network-ssh/generate-keys.nix b/nixos/tests/initrd-network-ssh/generate-keys.nix
new file mode 100644
index 00000000000..3d7978890ab
--- /dev/null
+++ b/nixos/tests/initrd-network-ssh/generate-keys.nix
@@ -0,0 +1,10 @@
+with import ../../.. {};
+
+runCommand "gen-keys" {
+    buildInputs = [ openssh ];
+  }
+  ''
+    mkdir $out
+    ssh-keygen -q -t ed25519 -N "" -f $out/ssh_host_ed25519_key
+    ssh-keygen -q -t ed25519 -N "" -f $out/id_ed25519
+  ''
diff --git a/nixos/tests/initrd-network-ssh/id_ed25519 b/nixos/tests/initrd-network-ssh/id_ed25519
new file mode 100644
index 00000000000..f914b3f712f
--- /dev/null
+++ b/nixos/tests/initrd-network-ssh/id_ed25519
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACAVcX+32Yqig25RxRA8bel/f604wV0p/63um+Oku/3vfwAAAJi/AJZMvwCW
+TAAAAAtzc2gtZWQyNTUxOQAAACAVcX+32Yqig25RxRA8bel/f604wV0p/63um+Oku/3vfw
+AAAEAPLjQusjrB90Lk3996G3AbtTeK+XweNgxaegYnml/A/RVxf7fZiqKDblHFEDxt6X9/
+rTjBXSn/re6b46S7/e9/AAAAEG5peGJsZEBsb2NhbGhvc3QBAgMEBQ==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/nixos/tests/initrd-network-ssh/id_ed25519.pub b/nixos/tests/initrd-network-ssh/id_ed25519.pub
new file mode 100644
index 00000000000..40de4a8ac60
--- /dev/null
+++ b/nixos/tests/initrd-network-ssh/id_ed25519.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBVxf7fZiqKDblHFEDxt6X9/rTjBXSn/re6b46S7/e9/ nixbld@localhost
diff --git a/nixos/tests/initrd-network-ssh/ssh_host_ed25519_key b/nixos/tests/initrd-network-ssh/ssh_host_ed25519_key
new file mode 100644
index 00000000000..f1e29459b7a
--- /dev/null
+++ b/nixos/tests/initrd-network-ssh/ssh_host_ed25519_key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDP9Mz6qlxdQqA4omrgbOlVsxSGONCJstjW9zqquajlIAAAAJg0WGFGNFhh
+RgAAAAtzc2gtZWQyNTUxOQAAACDP9Mz6qlxdQqA4omrgbOlVsxSGONCJstjW9zqquajlIA
+AAAEA0Hjs7LfFPdTf3ThGx6GNKvX0ItgzgXs91Z3oGIaF6S8/0zPqqXF1CoDiiauBs6VWz
+FIY40Imy2Nb3Oqq5qOUgAAAAEG5peGJsZEBsb2NhbGhvc3QBAgMEBQ==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/nixos/tests/initrd-network-ssh/ssh_host_ed25519_key.pub b/nixos/tests/initrd-network-ssh/ssh_host_ed25519_key.pub
new file mode 100644
index 00000000000..3aa1587e1dc
--- /dev/null
+++ b/nixos/tests/initrd-network-ssh/ssh_host_ed25519_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM/0zPqqXF1CoDiiauBs6VWzFIY40Imy2Nb3Oqq5qOUg nixbld@localhost
diff --git a/nixos/tests/initrd-network.nix b/nixos/tests/initrd-network.nix
new file mode 100644
index 00000000000..14e7e7d40bc
--- /dev/null
+++ b/nixos/tests/initrd-network.nix
@@ -0,0 +1,33 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} : {
+  name = "initrd-network";
+
+  meta.maintainers = [ pkgs.lib.maintainers.eelco ];
+
+  machine = { ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+    boot.initrd.network.enable = true;
+    boot.initrd.network.postCommands =
+      ''
+        ip addr show
+        ip route show
+        ip addr | grep 10.0.2.15 || exit 1
+        ping -c1 10.0.2.2 || exit 1
+      '';
+    # Check if cleanup was done correctly
+    boot.initrd.postMountCommands = lib.mkAfter
+      ''
+        ip addr show
+        ip route show
+        ip addr | grep 10.0.2.15 && exit 1
+        ping -c1 10.0.2.2 && exit 1
+      '';
+  };
+
+  testScript =
+    ''
+      start_all()
+      machine.wait_for_unit("multi-user.target")
+      machine.succeed("ip addr show >&2")
+      machine.succeed("ip route show >&2")
+    '';
+})
diff --git a/nixos/tests/initrd-secrets.nix b/nixos/tests/initrd-secrets.nix
new file mode 100644
index 00000000000..113a9cebf78
--- /dev/null
+++ b/nixos/tests/initrd-secrets.nix
@@ -0,0 +1,41 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+, lib ? pkgs.lib
+, testing ? import ../lib/testing-python.nix { inherit system pkgs; }
+}:
+let
+  secretInStore = pkgs.writeText "topsecret" "iamasecret";
+  testWithCompressor = compressor: testing.makeTest {
+    name = "initrd-secrets-${compressor}";
+
+    meta.maintainers = [ lib.maintainers.lheckemann ];
+
+    machine = { ... }: {
+      virtualisation.useBootLoader = true;
+      boot.initrd.secrets = {
+        "/test" = secretInStore;
+
+        # This should *not* need to be copied in postMountCommands
+        "/run/keys/test" = secretInStore;
+      };
+      boot.initrd.postMountCommands = ''
+        cp /test /mnt-root/secret-from-initramfs
+      '';
+      boot.initrd.compressor = compressor;
+      # zstd compression is only supported from 5.9 onwards. Remove when 5.10 becomes default.
+      boot.kernelPackages = pkgs.linuxPackages_latest;
+    };
+
+    testScript = ''
+      start_all()
+      machine.wait_for_unit("multi-user.target")
+      machine.succeed(
+          "cmp ${secretInStore} /secret-from-initramfs",
+          "cmp ${secretInStore} /run/keys/test",
+      )
+    '';
+  };
+in lib.flip lib.genAttrs testWithCompressor [
+  "cat" "gzip" "bzip2" "xz" "lzma" "lzop" "pigz" "pixz" "zstd"
+]
diff --git a/nixos/tests/input-remapper.nix b/nixos/tests/input-remapper.nix
new file mode 100644
index 00000000000..f692564caa5
--- /dev/null
+++ b/nixos/tests/input-remapper.nix
@@ -0,0 +1,52 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+  {
+    name = "input-remapper";
+    meta = {
+      maintainers = with pkgs.lib.maintainers; [ LunNova ];
+    };
+
+    machine = { config, ... }:
+      let user = config.users.users.sybil; in
+      {
+        imports = [
+          ./common/user-account.nix
+          ./common/x11.nix
+        ];
+
+        services.xserver.enable = true;
+        services.input-remapper.enable = true;
+        users.users.sybil = { isNormalUser = true; group = "wheel"; };
+        test-support.displayManager.auto.user = user.name;
+        # workaround for pkexec not working in the test environment
+        # Error creating textual authentication agent:
+        #   Error opening current controlling terminal for the process (`/dev/tty'):
+        #   No such device or address
+        # passwordless pkexec with polkit module also doesn't work
+        # to allow the program to run, we replace pkexec with sudo
+        # and turn on passwordless sudo
+        # this is not correct in general but good enough for this test
+        security.sudo = { enable = true; wheelNeedsPassword = false; };
+        security.wrappers.pkexec = pkgs.lib.mkForce
+          {
+            setuid = true;
+            owner = "root";
+            group = "root";
+            source = "${pkgs.sudo}/bin/sudo";
+          };
+      };
+
+    enableOCR = true;
+
+    testScript = { nodes, ... }: ''
+      start_all()
+      machine.wait_for_x()
+
+      machine.succeed("systemctl status input-remapper.service")
+      machine.execute("su - sybil -c input-remapper-gtk >&2 &")
+
+      machine.wait_for_text("Input Remapper")
+      machine.wait_for_text("Preset")
+      machine.wait_for_text("Change Key")
+    '';
+  })
diff --git a/nixos/tests/inspircd.nix b/nixos/tests/inspircd.nix
new file mode 100644
index 00000000000..f4d82054011
--- /dev/null
+++ b/nixos/tests/inspircd.nix
@@ -0,0 +1,93 @@
+let
+  clients = [
+    "ircclient1"
+    "ircclient2"
+  ];
+  server = "inspircd";
+  ircPort = 6667;
+  channel = "nixos-cat";
+  iiDir = "/tmp/irc";
+in
+
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "inspircd";
+  nodes = {
+    "${server}" = {
+      networking.firewall.allowedTCPPorts = [ ircPort ];
+      services.inspircd = {
+        enable = true;
+        package = pkgs.inspircdMinimal;
+        config = ''
+          <bind address="" port="${toString ircPort}" type="clients">
+          <connect name="main" allow="*" pingfreq="15">
+        '';
+      };
+    };
+  } // lib.listToAttrs (builtins.map (client: lib.nameValuePair client {
+    imports = [
+      ./common/user-account.nix
+    ];
+
+    systemd.services.ii = {
+      requires = [ "network.target" ];
+      wantedBy = [ "default.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        ExecPreStartPre = "mkdir -p ${iiDir}";
+        ExecStart = ''
+          ${lib.getBin pkgs.ii}/bin/ii -n ${client} -s ${server} -i ${iiDir}
+        '';
+        User = "alice";
+      };
+    };
+  }) clients);
+
+  testScript =
+    let
+      msg = client: "Hello, my name is ${client}";
+      clientScript = client: [
+        ''
+          ${client}.wait_for_unit("network.target")
+          ${client}.systemctl("start ii")
+          ${client}.wait_for_unit("ii")
+          ${client}.wait_for_file("${iiDir}/${server}/out")
+        ''
+        # wait until first PING from server arrives before joining,
+        # so we don't try it too early
+        ''
+          ${client}.wait_until_succeeds("grep 'PING' ${iiDir}/${server}/out")
+        ''
+        # join ${channel}
+        ''
+          ${client}.succeed("echo '/j #${channel}' > ${iiDir}/${server}/in")
+          ${client}.wait_for_file("${iiDir}/${server}/#${channel}/in")
+        ''
+        # send a greeting
+        ''
+          ${client}.succeed(
+              "echo '${msg client}' > ${iiDir}/${server}/#${channel}/in"
+          )
+        ''
+        # check that all greetings arrived on all clients
+      ] ++ builtins.map (other: ''
+        ${client}.succeed(
+            "grep '${msg other}$' ${iiDir}/${server}/#${channel}/out"
+        )
+      '') clients;
+
+      # foldl', but requires a non-empty list instead of a start value
+      reduce = f: list:
+        builtins.foldl' f (builtins.head list) (builtins.tail list);
+    in ''
+      start_all()
+      ${server}.wait_for_open_port(${toString ircPort})
+
+      # run clientScript for all clients so that every list
+      # entry is executed by every client before advancing
+      # to the next one.
+    '' + lib.concatStrings
+      (reduce
+        (lib.zipListsWith (cs: c: cs + c))
+        (builtins.map clientScript clients));
+})
diff --git a/nixos/tests/installed-tests/appstream-qt.nix b/nixos/tests/installed-tests/appstream-qt.nix
new file mode 100644
index 00000000000..d08187bfe46
--- /dev/null
+++ b/nixos/tests/installed-tests/appstream-qt.nix
@@ -0,0 +1,9 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.libsForQt5.appstream-qt;
+
+  testConfig = {
+    appstream.enable = true;
+  };
+}
diff --git a/nixos/tests/installed-tests/appstream.nix b/nixos/tests/installed-tests/appstream.nix
new file mode 100644
index 00000000000..f71a095d445
--- /dev/null
+++ b/nixos/tests/installed-tests/appstream.nix
@@ -0,0 +1,9 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.appstream;
+
+  testConfig = {
+    appstream.enable = true;
+  };
+}
diff --git a/nixos/tests/installed-tests/colord.nix b/nixos/tests/installed-tests/colord.nix
new file mode 100644
index 00000000000..77e6b917fe6
--- /dev/null
+++ b/nixos/tests/installed-tests/colord.nix
@@ -0,0 +1,5 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.colord;
+}
diff --git a/nixos/tests/installed-tests/default.nix b/nixos/tests/installed-tests/default.nix
new file mode 100644
index 00000000000..079fd54e71e
--- /dev/null
+++ b/nixos/tests/installed-tests/default.nix
@@ -0,0 +1,111 @@
+# NixOS tests for gnome-desktop-testing-runner using software
+# See https://wiki.gnome.org/Initiatives/GnomeGoals/InstalledTests
+
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../../.. { inherit system config; }
+}:
+
+with import ../../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+
+  callInstalledTest = pkgs.newScope { inherit makeInstalledTest; };
+
+  makeInstalledTest =
+    { # Package to test. Needs to have an installedTests output
+      tested
+
+      # Config to inject into machine
+    , testConfig ? {}
+
+      # Test script snippet to inject before gnome-desktop-testing-runner begins.
+      # This is useful for extra setup the environment may need before the runner begins.
+    , preTestScript ? ""
+
+      # Does test need X11?
+    , withX11 ? false
+
+      # Extra flags to pass to gnome-desktop-testing-runner.
+    , testRunnerFlags ? ""
+
+      # Extra attributes to pass to makeTest.
+      # They will be recursively merged into the attrset created by this function.
+    , ...
+    }@args:
+    makeTest
+      (recursiveUpdate
+        rec {
+          name = tested.name;
+
+          meta = {
+            maintainers = tested.meta.maintainers;
+          };
+
+          machine = { ... }: {
+            imports = [
+              testConfig
+            ] ++ optional withX11 ../common/x11.nix;
+
+            environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
+
+            # The installed tests need to be added to the test VM’s closure.
+            # Otherwise, their dependencies might not actually be registered
+            # as valid paths in the VM’s Nix store database,
+            # and `nix-store --query` commands run as part of the tests
+            # (for example when building Flatpak runtimes) will fail.
+            environment.variables.TESTED_PACKAGE_INSTALLED_TESTS = "${tested.installedTests}/share";
+          };
+
+          testScript =
+            optionalString withX11 ''
+              machine.wait_for_x()
+            '' +
+            optionalString (preTestScript != "") ''
+              ${preTestScript}
+            '' +
+            ''
+              machine.succeed(
+                  "gnome-desktop-testing-runner ${testRunnerFlags} -d '${tested.installedTests}/share'"
+              )
+            '';
+        }
+
+        (removeAttrs args [
+          "tested"
+          "testConfig"
+          "preTestScript"
+          "withX11"
+          "testRunnerFlags"
+        ])
+      );
+
+in
+
+{
+  appstream = callInstalledTest ./appstream.nix {};
+  appstream-qt = callInstalledTest ./appstream-qt.nix {};
+  colord = callInstalledTest ./colord.nix {};
+  flatpak = callInstalledTest ./flatpak.nix {};
+  flatpak-builder = callInstalledTest ./flatpak-builder.nix {};
+  fwupd = callInstalledTest ./fwupd.nix {};
+  gcab = callInstalledTest ./gcab.nix {};
+  gdk-pixbuf = callInstalledTest ./gdk-pixbuf.nix {};
+  gjs = callInstalledTest ./gjs.nix {};
+  glib-networking = callInstalledTest ./glib-networking.nix {};
+  gnome-photos = callInstalledTest ./gnome-photos.nix {};
+  graphene = callInstalledTest ./graphene.nix {};
+  gsconnect = callInstalledTest ./gsconnect.nix {};
+  ibus = callInstalledTest ./ibus.nix {};
+  libgdata = callInstalledTest ./libgdata.nix {};
+  librsvg = callInstalledTest ./librsvg.nix {};
+  glib-testing = callInstalledTest ./glib-testing.nix {};
+  libjcat = callInstalledTest ./libjcat.nix {};
+  libxmlb = callInstalledTest ./libxmlb.nix {};
+  malcontent = callInstalledTest ./malcontent.nix {};
+  ostree = callInstalledTest ./ostree.nix {};
+  pipewire = callInstalledTest ./pipewire.nix {};
+  power-profiles-daemon = callInstalledTest ./power-profiles-daemon.nix {};
+  xdg-desktop-portal = callInstalledTest ./xdg-desktop-portal.nix {};
+}
diff --git a/nixos/tests/installed-tests/flatpak-builder.nix b/nixos/tests/installed-tests/flatpak-builder.nix
new file mode 100644
index 00000000000..31b9f2b258f
--- /dev/null
+++ b/nixos/tests/installed-tests/flatpak-builder.nix
@@ -0,0 +1,14 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.flatpak-builder;
+
+  testConfig = {
+    services.flatpak.enable = true;
+    xdg.portal.enable = true;
+    environment.systemPackages = with pkgs; [ flatpak-builder ] ++ flatpak-builder.installedTestsDependencies;
+    virtualisation.diskSize = 2048;
+  };
+
+  testRunnerFlags = "--timeout 3600";
+}
diff --git a/nixos/tests/installed-tests/flatpak.nix b/nixos/tests/installed-tests/flatpak.nix
new file mode 100644
index 00000000000..c7fe9cf4588
--- /dev/null
+++ b/nixos/tests/installed-tests/flatpak.nix
@@ -0,0 +1,17 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.flatpak;
+  withX11 = true;
+
+  testConfig = {
+    xdg.portal.enable = true;
+    xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gtk ];
+    services.flatpak.enable = true;
+    environment.systemPackages = with pkgs; [ gnupg ostree python3 ];
+    virtualisation.memorySize = 2047;
+    virtualisation.diskSize = 3072;
+  };
+
+  testRunnerFlags = "--timeout 3600";
+}
diff --git a/nixos/tests/installed-tests/fwupd.nix b/nixos/tests/installed-tests/fwupd.nix
new file mode 100644
index 00000000000..65614e2689d
--- /dev/null
+++ b/nixos/tests/installed-tests/fwupd.nix
@@ -0,0 +1,11 @@
+{ pkgs, lib, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.fwupd;
+
+  testConfig = {
+    services.fwupd.enable = true;
+    services.fwupd.disabledPlugins = lib.mkForce []; # don't disable test plugin
+    services.fwupd.enableTestRemote = true;
+  };
+}
diff --git a/nixos/tests/installed-tests/gcab.nix b/nixos/tests/installed-tests/gcab.nix
new file mode 100644
index 00000000000..b24cc2e0126
--- /dev/null
+++ b/nixos/tests/installed-tests/gcab.nix
@@ -0,0 +1,5 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.gcab;
+}
diff --git a/nixos/tests/installed-tests/gdk-pixbuf.nix b/nixos/tests/installed-tests/gdk-pixbuf.nix
new file mode 100644
index 00000000000..3d0011a427a
--- /dev/null
+++ b/nixos/tests/installed-tests/gdk-pixbuf.nix
@@ -0,0 +1,13 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.gdk-pixbuf;
+
+  testConfig = {
+    # Tests allocate a lot of memory trying to exploit a CVE
+    # but qemu-system-i386 has a 2047M memory limit
+    virtualisation.memorySize = if pkgs.stdenv.isi686 then 2047 else 4096;
+  };
+
+  testRunnerFlags = "--timeout 1800";
+}
diff --git a/nixos/tests/installed-tests/gjs.nix b/nixos/tests/installed-tests/gjs.nix
new file mode 100644
index 00000000000..1656e9de171
--- /dev/null
+++ b/nixos/tests/installed-tests/gjs.nix
@@ -0,0 +1,6 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.gjs;
+  withX11 = true;
+}
diff --git a/nixos/tests/installed-tests/glib-networking.nix b/nixos/tests/installed-tests/glib-networking.nix
new file mode 100644
index 00000000000..b58d4df21fc
--- /dev/null
+++ b/nixos/tests/installed-tests/glib-networking.nix
@@ -0,0 +1,5 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.glib-networking;
+}
diff --git a/nixos/tests/installed-tests/glib-testing.nix b/nixos/tests/installed-tests/glib-testing.nix
new file mode 100644
index 00000000000..7a06cf792bd
--- /dev/null
+++ b/nixos/tests/installed-tests/glib-testing.nix
@@ -0,0 +1,5 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.glib-testing;
+}
diff --git a/nixos/tests/installed-tests/gnome-photos.nix b/nixos/tests/installed-tests/gnome-photos.nix
new file mode 100644
index 00000000000..bcb6479ee89
--- /dev/null
+++ b/nixos/tests/installed-tests/gnome-photos.nix
@@ -0,0 +1,35 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.gnome-photos;
+
+  withX11 = true;
+
+  testConfig = {
+    programs.dconf.enable = true;
+    services.gnome.at-spi2-core.enable = true; # needed for dogtail
+    environment.systemPackages = with pkgs; [
+      # gsettings tool with access to gsettings-desktop-schemas
+      (stdenv.mkDerivation {
+        name = "desktop-gsettings";
+        dontUnpack = true;
+        nativeBuildInputs = [ glib wrapGAppsHook ];
+        buildInputs = [ gsettings-desktop-schemas ];
+        installPhase = ''
+          runHook preInstall
+          mkdir -p $out/bin
+          ln -s ${glib.bin}/bin/gsettings $out/bin/desktop-gsettings
+          runHook postInstall
+        '';
+      })
+    ];
+    services.dbus.packages = with pkgs; [ gnome-photos ];
+  };
+
+  preTestScript = ''
+    # dogtail needs accessibility enabled
+    machine.succeed(
+        "desktop-gsettings set org.gnome.desktop.interface toolkit-accessibility true 2>&1"
+    )
+  '';
+}
diff --git a/nixos/tests/installed-tests/graphene.nix b/nixos/tests/installed-tests/graphene.nix
new file mode 100644
index 00000000000..e43339abd88
--- /dev/null
+++ b/nixos/tests/installed-tests/graphene.nix
@@ -0,0 +1,5 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.graphene;
+}
diff --git a/nixos/tests/installed-tests/gsconnect.nix b/nixos/tests/installed-tests/gsconnect.nix
new file mode 100644
index 00000000000..ac39f743578
--- /dev/null
+++ b/nixos/tests/installed-tests/gsconnect.nix
@@ -0,0 +1,7 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.gnomeExtensions.gsconnect;
+
+  withX11 = true;
+}
diff --git a/nixos/tests/installed-tests/ibus.nix b/nixos/tests/installed-tests/ibus.nix
new file mode 100644
index 00000000000..a4bc2a7d7de
--- /dev/null
+++ b/nixos/tests/installed-tests/ibus.nix
@@ -0,0 +1,16 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.ibus;
+
+  testConfig = {
+    i18n.inputMethod.enabled = "ibus";
+    systemd.user.services.ibus-daemon = {
+      serviceConfig.ExecStart = "${pkgs.ibus}/bin/ibus-daemon --xim --verbose";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+    };
+  };
+
+  withX11 = true;
+}
diff --git a/nixos/tests/installed-tests/libgdata.nix b/nixos/tests/installed-tests/libgdata.nix
new file mode 100644
index 00000000000..b0d39c042be
--- /dev/null
+++ b/nixos/tests/installed-tests/libgdata.nix
@@ -0,0 +1,11 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.libgdata;
+
+  testConfig = {
+    # # GLib-GIO-DEBUG: _g_io_module_get_default: Found default implementation dummy (GDummyTlsBackend) for ‘gio-tls-backend’
+    # Bail out! libgdata:ERROR:../gdata/tests/common.c:134:gdata_test_init: assertion failed (child_error == NULL): TLS support is not available (g-tls-error-quark, 0)
+    services.gnome.glib-networking.enable = true;
+  };
+}
diff --git a/nixos/tests/installed-tests/libjcat.nix b/nixos/tests/installed-tests/libjcat.nix
new file mode 100644
index 00000000000..41493a73089
--- /dev/null
+++ b/nixos/tests/installed-tests/libjcat.nix
@@ -0,0 +1,5 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.libjcat;
+}
diff --git a/nixos/tests/installed-tests/librsvg.nix b/nixos/tests/installed-tests/librsvg.nix
new file mode 100644
index 00000000000..378e7cce3ff
--- /dev/null
+++ b/nixos/tests/installed-tests/librsvg.nix
@@ -0,0 +1,9 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.librsvg;
+
+  testConfig = {
+    virtualisation.memorySize = 2047;
+  };
+}
diff --git a/nixos/tests/installed-tests/libxmlb.nix b/nixos/tests/installed-tests/libxmlb.nix
new file mode 100644
index 00000000000..af2bbe9c35e
--- /dev/null
+++ b/nixos/tests/installed-tests/libxmlb.nix
@@ -0,0 +1,5 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.libxmlb;
+}
diff --git a/nixos/tests/installed-tests/malcontent.nix b/nixos/tests/installed-tests/malcontent.nix
new file mode 100644
index 00000000000..d4e214c4198
--- /dev/null
+++ b/nixos/tests/installed-tests/malcontent.nix
@@ -0,0 +1,5 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.malcontent;
+}
diff --git a/nixos/tests/installed-tests/ostree.nix b/nixos/tests/installed-tests/ostree.nix
new file mode 100644
index 00000000000..90e09ad4ddf
--- /dev/null
+++ b/nixos/tests/installed-tests/ostree.nix
@@ -0,0 +1,12 @@
+{ pkgs, lib, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.ostree;
+
+  testConfig = {
+    environment.systemPackages = with pkgs; [
+      gnupg
+      ostree
+    ];
+  };
+}
diff --git a/nixos/tests/installed-tests/pipewire.nix b/nixos/tests/installed-tests/pipewire.nix
new file mode 100644
index 00000000000..b04265658fc
--- /dev/null
+++ b/nixos/tests/installed-tests/pipewire.nix
@@ -0,0 +1,15 @@
+{ pkgs, lib, 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/installed-tests/power-profiles-daemon.nix b/nixos/tests/installed-tests/power-profiles-daemon.nix
new file mode 100644
index 00000000000..43629a0155d
--- /dev/null
+++ b/nixos/tests/installed-tests/power-profiles-daemon.nix
@@ -0,0 +1,9 @@
+{ pkgs, lib, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.power-profiles-daemon;
+
+  testConfig = {
+    services.power-profiles-daemon.enable = true;
+  };
+}
diff --git a/nixos/tests/installed-tests/xdg-desktop-portal.nix b/nixos/tests/installed-tests/xdg-desktop-portal.nix
new file mode 100644
index 00000000000..90529d37ee0
--- /dev/null
+++ b/nixos/tests/installed-tests/xdg-desktop-portal.nix
@@ -0,0 +1,9 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.xdg-desktop-portal;
+
+  # Ton of breakage.
+  # https://github.com/flatpak/xdg-desktop-portal/pull/428
+  meta.broken = true;
+}
diff --git a/nixos/tests/installer.nix b/nixos/tests/installer.nix
new file mode 100644
index 00000000000..5525c3117b7
--- /dev/null
+++ b/nixos/tests/installer.nix
@@ -0,0 +1,811 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+
+  # The configuration to install.
+  makeConfig = { bootLoader, grubVersion, grubDevice, grubIdentifier, grubUseEfi
+               , extraConfig, forceGrubReinstallCount ? 0
+               }:
+    pkgs.writeText "configuration.nix" ''
+      { config, lib, pkgs, modulesPath, ... }:
+
+      { imports =
+          [ ./hardware-configuration.nix
+            <nixpkgs/nixos/modules/testing/test-instrumentation.nix>
+          ];
+
+        # To ensure that we can rebuild the grub configuration on the nixos-rebuild
+        system.extraDependencies = with pkgs; [ stdenvNoCC ];
+
+        ${optionalString (bootLoader == "grub") ''
+          boot.loader.grub.version = ${toString grubVersion};
+          ${optionalString (grubVersion == 1) ''
+            boot.loader.grub.splashImage = null;
+          ''}
+
+          boot.loader.grub.extraConfig = "serial; terminal_output serial";
+          ${if grubUseEfi then ''
+            boot.loader.grub.device = "nodev";
+            boot.loader.grub.efiSupport = true;
+            boot.loader.grub.efiInstallAsRemovable = true; # XXX: needed for OVMF?
+          '' else ''
+            boot.loader.grub.device = "${grubDevice}";
+            boot.loader.grub.fsIdentifier = "${grubIdentifier}";
+          ''}
+
+          boot.loader.grub.configurationLimit = 100 + ${toString forceGrubReinstallCount};
+        ''}
+
+        ${optionalString (bootLoader == "systemd-boot") ''
+          boot.loader.systemd-boot.enable = true;
+        ''}
+
+        users.users.alice = {
+          isNormalUser = true;
+          home = "/home/alice";
+          description = "Alice Foobar";
+        };
+
+        hardware.enableAllFirmware = lib.mkForce false;
+
+        ${replaceChars ["\n"] ["\n  "] extraConfig}
+      }
+    '';
+
+
+  # The test script boots a NixOS VM, installs NixOS on an empty hard
+  # disk, and then reboot from the hard disk.  It's parameterized with
+  # a test script fragment `createPartitions', which must create
+  # partitions and filesystems.
+  testScriptFun = { bootLoader, createPartitions, grubVersion, grubDevice, grubUseEfi
+                  , grubIdentifier, preBootCommands, postBootCommands, extraConfig
+                  , testSpecialisationConfig
+                  }:
+    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 ''
+      def assemble_qemu_flags():
+          flags = "-cpu max"
+          ${if (system == "x86_64-linux" || system == "i686-linux")
+            then ''flags += " -m 1024"''
+            else ''flags += " -m 768 -enable-kvm -machine virt,gic-version=host"''
+          }
+          return flags
+
+
+      qemu_flags = {"qemuFlags": assemble_qemu_flags()}
+
+      hd_flags = {
+          "hdaInterface": "${iface}",
+          "hda": "vm-state-machine/machine.qcow2",
+      }
+      ${optionalString isEfi ''
+        hd_flags.update(
+            bios="${pkgs.OVMF.fd}/FV/${bios}"
+        )''
+      }
+      default_flags = {**hd_flags, **qemu_flags}
+
+
+      def create_machine_named(name):
+          return create_machine({**default_flags, "name": name})
+
+
+      machine.start()
+
+      with subtest("Assert readiness of login prompt"):
+          machine.succeed("echo hello")
+
+      with subtest("Wait for hard disks to appear in /dev"):
+          machine.succeed("udevadm settle")
+
+      ${createPartitions}
+
+      with subtest("Create the NixOS configuration"):
+          machine.succeed("nixos-generate-config --root /mnt")
+          machine.succeed("cat /mnt/etc/nixos/hardware-configuration.nix >&2")
+          machine.copy_from_host(
+              "${ makeConfig {
+                    inherit bootLoader grubVersion grubDevice grubIdentifier
+                            grubUseEfi extraConfig;
+                  }
+              }",
+              "/mnt/etc/nixos/configuration.nix",
+          )
+
+      with subtest("Perform the installation"):
+          machine.succeed("nixos-install < /dev/null >&2")
+
+      with subtest("Do it again to make sure it's idempotent"):
+          machine.succeed("nixos-install < /dev/null >&2")
+
+      with subtest("Shutdown system after installation"):
+          machine.succeed("umount /mnt/boot || true")
+          machine.succeed("umount /mnt")
+          machine.succeed("sync")
+          machine.shutdown()
+
+      # Now see if we can boot the installation.
+      machine = create_machine_named("boot-after-install")
+
+      # For example to enter LUKS passphrase.
+      ${preBootCommands}
+
+      with subtest("Assert that /boot get mounted"):
+          machine.wait_for_unit("local-fs.target")
+          ${if bootLoader == "grub"
+              then ''machine.succeed("test -e /boot/grub")''
+              else ''machine.succeed("test -e /boot/loader/loader.conf")''
+          }
+
+      with subtest("Check whether /root has correct permissions"):
+          assert "700" in machine.succeed("stat -c '%a' /root")
+
+      with subtest("Assert swap device got activated"):
+          # uncomment once https://bugs.freedesktop.org/show_bug.cgi?id=86930 is resolved
+          machine.wait_for_unit("swap.target")
+          machine.succeed("cat /proc/swaps | grep -q /dev")
+
+      with subtest("Check that the store is in good shape"):
+          machine.succeed("nix-store --verify --check-contents >&2")
+
+      with subtest("Check whether the channel works"):
+          machine.succeed("nix-env -iA nixos.procps >&2")
+          assert ".nix-profile" in machine.succeed("type -tP ps | tee /dev/stderr")
+
+      with subtest(
+          "Check that the daemon works, and that non-root users can run builds "
+          "(this will build a new profile generation through the daemon)"
+      ):
+          machine.succeed("su alice -l -c 'nix-env -iA nixos.procps' >&2")
+
+      with subtest("Configure system with writable Nix store on next boot"):
+          # we're not using copy_from_host here because the installer image
+          # doesn't know about the host-guest sharing mechanism.
+          machine.copy_from_host_via_shell(
+              "${ makeConfig {
+                    inherit bootLoader grubVersion grubDevice grubIdentifier
+                            grubUseEfi extraConfig;
+                    forceGrubReinstallCount = 1;
+                  }
+              }",
+              "/etc/nixos/configuration.nix",
+          )
+
+      with subtest("Check whether nixos-rebuild works"):
+          machine.succeed("nixos-rebuild switch >&2")
+
+      # FIXME: Nix 2.4 broke nixos-option, someone has to fix it.
+      # with subtest("Test nixos-option"):
+      #     kernel_modules = machine.succeed("nixos-option boot.initrd.kernelModules")
+      #     assert "virtio_console" in kernel_modules
+      #     assert "List of modules" in kernel_modules
+      #     assert "qemu-guest.nix" in kernel_modules
+
+      machine.shutdown()
+
+      # Check whether a writable store build works
+      machine = create_machine_named("rebuild-switch")
+      ${preBootCommands}
+      machine.wait_for_unit("multi-user.target")
+
+      # we're not using copy_from_host here because the installer image
+      # doesn't know about the host-guest sharing mechanism.
+      machine.copy_from_host_via_shell(
+          "${ makeConfig {
+                inherit bootLoader grubVersion grubDevice grubIdentifier
+                grubUseEfi extraConfig;
+                forceGrubReinstallCount = 2;
+              }
+          }",
+          "/etc/nixos/configuration.nix",
+      )
+      machine.succeed("nixos-rebuild boot >&2")
+      machine.shutdown()
+
+      # And just to be sure, check that the machine still boots after
+      # "nixos-rebuild switch".
+      machine = create_machine_named("boot-after-rebuild-switch")
+      ${preBootCommands}
+      machine.wait_for_unit("network.target")
+      ${postBootCommands}
+      machine.shutdown()
+
+      # Tests for validating clone configuration entries in grub menu
+    ''
+    + optionalString testSpecialisationConfig ''
+      # Reboot Machine
+      machine = create_machine_named("clone-default-config")
+      ${preBootCommands}
+      machine.wait_for_unit("multi-user.target")
+
+      with subtest("Booted configuration name should be 'Home'"):
+          # This is not the name that shows in the grub menu.
+          # The default configuration is always shown as "Default"
+          machine.succeed("cat /run/booted-system/configuration-name >&2")
+          assert "Home" in machine.succeed("cat /run/booted-system/configuration-name")
+
+      with subtest("We should **not** find a file named /etc/gitconfig"):
+          machine.fail("test -e /etc/gitconfig")
+
+      with subtest("Set grub to boot the second configuration"):
+          machine.succeed("grub-reboot 1")
+
+      ${postBootCommands}
+      machine.shutdown()
+
+      # Reboot Machine
+      machine = create_machine_named("clone-alternate-config")
+      ${preBootCommands}
+
+      machine.wait_for_unit("multi-user.target")
+      with subtest("Booted configuration name should be Work"):
+          machine.succeed("cat /run/booted-system/configuration-name >&2")
+          assert "Work" in machine.succeed("cat /run/booted-system/configuration-name")
+
+      with subtest("We should find a file named /etc/gitconfig"):
+          machine.succeed("test -e /etc/gitconfig")
+
+      ${postBootCommands}
+      machine.shutdown()
+    '';
+
+
+  makeInstallerTest = name:
+    { createPartitions, preBootCommands ? "", postBootCommands ? "", extraConfig ? ""
+    , extraInstallerConfig ? {}
+    , bootLoader ? "grub" # either "grub" or "systemd-boot"
+    , grubVersion ? 2, grubDevice ? "/dev/vda", grubIdentifier ? "uuid", grubUseEfi ? false
+    , enableOCR ? false, meta ? {}
+    , testSpecialisationConfig ? false
+    }:
+    makeTest {
+      inherit enableOCR;
+      name = "installer-" + name;
+      meta = with pkgs.lib.maintainers; {
+        # put global maintainers here, individuals go into makeInstallerTest fkt call
+        maintainers = (meta.maintainers or []);
+      };
+      nodes = {
+
+        # The configuration of the machine used to run "nixos-install".
+        machine = { pkgs, ... }: {
+          imports = [
+            ../modules/profiles/installation-device.nix
+            ../modules/profiles/base.nix
+            extraInstallerConfig
+          ];
+
+          # builds stuff in the VM, needs more juice
+          virtualisation.diskSize = 8 * 1024;
+          virtualisation.cores = 8;
+          virtualisation.memorySize = 1536;
+
+          # Use a small /dev/vdb as the root disk for the
+          # installer. This ensures the target disk (/dev/vda) is
+          # the same during and after installation.
+          virtualisation.emptyDiskImages = [ 512 ];
+          virtualisation.bootDevice =
+            if grubVersion == 1 then "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive2" else "/dev/vdb";
+          virtualisation.qemu.diskInterface =
+            if grubVersion == 1 then "scsi" else "virtio";
+
+          boot.loader.systemd-boot.enable = mkIf (bootLoader == "systemd-boot") true;
+
+          hardware.enableAllFirmware = mkForce false;
+
+          # The test cannot access the network, so any packages we
+          # need must be included in the VM.
+          system.extraDependencies = with pkgs; [
+            brotli
+            brotli.dev
+            brotli.lib
+            desktop-file-utils
+            docbook5
+            docbook_xsl_ns
+            libxml2.bin
+            libxslt.bin
+            nixos-artwork.wallpapers.simple-dark-gray-bottom
+            ntp
+            perlPackages.ListCompare
+            perlPackages.XMLLibXML
+            python3Minimal
+            shared-mime-info
+            sudo
+            texinfo
+            unionfs-fuse
+            xorg.lndir
+
+            # add curl so that rather than seeing the test attempt to download
+            # curl's tarball, we see what it's trying to download
+            curl
+          ]
+          ++ optional (bootLoader == "grub" && grubVersion == 1) pkgs.grub
+          ++ optionals (bootLoader == "grub" && grubVersion == 2) (let
+            zfsSupport = lib.any (x: x == "zfs")
+              (extraInstallerConfig.boot.supportedFilesystems or []);
+          in [
+            (pkgs.grub2.override { inherit zfsSupport; })
+            (pkgs.grub2_efi.override { inherit zfsSupport; })
+          ]);
+
+          nix.settings = {
+            substituters = mkForce [];
+            hashed-mirrors = null;
+            connect-timeout = 1;
+          };
+        };
+
+      };
+
+      testScript = testScriptFun {
+        inherit bootLoader createPartitions preBootCommands postBootCommands
+                grubVersion grubDevice grubIdentifier grubUseEfi extraConfig
+                testSpecialisationConfig;
+      };
+    };
+
+    makeLuksRootTest = name: luksFormatOpts: makeInstallerTest name {
+      createPartitions = ''
+        machine.succeed(
+            "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+            + " mkpart primary ext2 1M 100MB"  # /boot
+            + " mkpart primary linux-swap 100M 1024M"
+            + " mkpart primary 1024M -1s",  # LUKS
+            "udevadm settle",
+            "mkswap /dev/vda2 -L swap",
+            "swapon -L swap",
+            "modprobe dm_mod dm_crypt",
+            "echo -n supersecret | cryptsetup luksFormat ${luksFormatOpts} -q /dev/vda3 -",
+            "echo -n supersecret | cryptsetup luksOpen --key-file - /dev/vda3 cryptroot",
+            "mkfs.ext3 -L nixos /dev/mapper/cryptroot",
+            "mount LABEL=nixos /mnt",
+            "mkfs.ext3 -L boot /dev/vda1",
+            "mkdir -p /mnt/boot",
+            "mount LABEL=boot /mnt/boot",
+        )
+      '';
+      extraConfig = ''
+        boot.kernelParams = lib.mkAfter [ "console=tty0" ];
+      '';
+      enableOCR = true;
+      preBootCommands = ''
+        machine.start()
+        machine.wait_for_text("Passphrase for")
+        machine.send_chars("supersecret\n")
+      '';
+    };
+
+  # The (almost) simplest partitioning scheme: a swap partition and
+  # one big filesystem partition.
+  simple-test-config = {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+          + " mkpart primary linux-swap 1M 1024M"
+          + " mkpart primary ext2 1024M -1s",
+          "udevadm settle",
+          "mkswap /dev/vda1 -L swap",
+          "swapon -L swap",
+          "mkfs.ext3 -L nixos /dev/vda2",
+          "mount LABEL=nixos /mnt",
+      )
+    '';
+  };
+
+  simple-uefi-grub-config = {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel gpt"
+          + " mkpart ESP fat32 1M 100MiB"  # /boot
+          + " set 1 boot on"
+          + " mkpart primary linux-swap 100MiB 1024MiB"
+          + " mkpart primary ext2 1024MiB -1MiB",  # /
+          "udevadm settle",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.ext3 -L nixos /dev/vda3",
+          "mount LABEL=nixos /mnt",
+          "mkfs.vfat -n BOOT /dev/vda1",
+          "mkdir -p /mnt/boot",
+          "mount LABEL=BOOT /mnt/boot",
+      )
+    '';
+    bootLoader = "grub";
+    grubUseEfi = true;
+  };
+
+  specialisation-test-extraconfig = {
+    extraConfig = ''
+      environment.systemPackages = [ pkgs.grub2 ];
+      boot.loader.grub.configurationName = "Home";
+      specialisation.work.configuration = {
+        boot.loader.grub.configurationName = lib.mkForce "Work";
+
+        environment.etc = {
+          "gitconfig".text = "
+            [core]
+              gitproxy = none for work.com
+              ";
+        };
+      };
+    '';
+    testSpecialisationConfig = true;
+  };
+
+
+in {
+
+  # !!! `parted mkpart' seems to silently create overlapping partitions.
+
+
+  # The (almost) simplest partitioning scheme: a swap partition and
+  # one big filesystem partition.
+  simple = makeInstallerTest "simple" simple-test-config;
+
+  # Test cloned configurations with the simple grub configuration
+  simpleSpecialised = makeInstallerTest "simpleSpecialised" (simple-test-config // specialisation-test-extraconfig);
+
+  # Simple GPT/UEFI configuration using systemd-boot with 3 partitions: ESP, swap & root filesystem
+  simpleUefiSystemdBoot = makeInstallerTest "simpleUefiSystemdBoot" {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel gpt"
+          + " mkpart ESP fat32 1M 100MiB"  # /boot
+          + " set 1 boot on"
+          + " mkpart primary linux-swap 100MiB 1024MiB"
+          + " mkpart primary ext2 1024MiB -1MiB",  # /
+          "udevadm settle",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.ext3 -L nixos /dev/vda3",
+          "mount LABEL=nixos /mnt",
+          "mkfs.vfat -n BOOT /dev/vda1",
+          "mkdir -p /mnt/boot",
+          "mount LABEL=BOOT /mnt/boot",
+      )
+    '';
+    bootLoader = "systemd-boot";
+  };
+
+  simpleUefiGrub = makeInstallerTest "simpleUefiGrub" simple-uefi-grub-config;
+
+  # Test cloned configurations with the uefi grub configuration
+  simpleUefiGrubSpecialisation = makeInstallerTest "simpleUefiGrubSpecialisation" (simple-uefi-grub-config // specialisation-test-extraconfig);
+
+  # Same as the previous, but now with a separate /boot partition.
+  separateBoot = makeInstallerTest "separateBoot" {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+          + " mkpart primary ext2 1M 100MB"  # /boot
+          + " mkpart primary linux-swap 100MB 1024M"
+          + " mkpart primary ext2 1024M -1s",  # /
+          "udevadm settle",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.ext3 -L nixos /dev/vda3",
+          "mount LABEL=nixos /mnt",
+          "mkfs.ext3 -L boot /dev/vda1",
+          "mkdir -p /mnt/boot",
+          "mount LABEL=boot /mnt/boot",
+      )
+    '';
+  };
+
+  # Same as the previous, but with fat32 /boot.
+  separateBootFat = makeInstallerTest "separateBootFat" {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+          + " mkpart primary ext2 1M 100MB"  # /boot
+          + " mkpart primary linux-swap 100MB 1024M"
+          + " mkpart primary ext2 1024M -1s",  # /
+          "udevadm settle",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.ext3 -L nixos /dev/vda3",
+          "mount LABEL=nixos /mnt",
+          "mkfs.vfat -n BOOT /dev/vda1",
+          "mkdir -p /mnt/boot",
+          "mount LABEL=BOOT /mnt/boot",
+      )
+    '';
+  };
+
+  # zfs on / with swap
+  zfsroot = makeInstallerTest "zfs-root" {
+    extraInstallerConfig = {
+      boot.supportedFilesystems = [ "zfs" ];
+    };
+
+    extraConfig = ''
+      boot.supportedFilesystems = [ "zfs" ];
+
+      # Using by-uuid overrides the default of by-id, and is unique
+      # to the qemu disks, as they don't produce by-id paths for
+      # some reason.
+      boot.zfs.devNodes = "/dev/disk/by-uuid/";
+      networking.hostId = "00000000";
+    '';
+
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+          + " mkpart primary linux-swap 1M 1024M"
+          + " mkpart primary 1024M -1s",
+          "udevadm settle",
+          "mkswap /dev/vda1 -L swap",
+          "swapon -L swap",
+          "zpool create rpool /dev/vda2",
+          "zfs create -o mountpoint=legacy rpool/root",
+          "mount -t zfs rpool/root /mnt",
+          "udevadm settle",
+      )
+    '';
+  };
+
+  # Create two physical LVM partitions combined into one volume group
+  # that contains the logical swap and root partitions.
+  lvm = makeInstallerTest "lvm" {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+          + " mkpart primary 1M 2048M"  # PV1
+          + " set 1 lvm on"
+          + " mkpart primary 2048M -1s"  # PV2
+          + " set 2 lvm on",
+          "udevadm settle",
+          "pvcreate /dev/vda1 /dev/vda2",
+          "vgcreate MyVolGroup /dev/vda1 /dev/vda2",
+          "lvcreate --size 1G --name swap MyVolGroup",
+          "lvcreate --size 3G --name nixos MyVolGroup",
+          "mkswap -f /dev/MyVolGroup/swap -L swap",
+          "swapon -L swap",
+          "mkfs.xfs -L nixos /dev/MyVolGroup/nixos",
+          "mount LABEL=nixos /mnt",
+      )
+    '';
+  };
+
+  # Boot off an encrypted root partition with the default LUKS header format
+  luksroot = makeLuksRootTest "luksroot-format1" "";
+
+  # Boot off an encrypted root partition with LUKS1 format
+  luksroot-format1 = makeLuksRootTest "luksroot-format1" "--type=LUKS1";
+
+  # Boot off an encrypted root partition with LUKS2 format
+  luksroot-format2 = makeLuksRootTest "luksroot-format2" "--type=LUKS2";
+
+  # Test whether opening encrypted filesystem with keyfile
+  # Checks for regression of missing cryptsetup, when no luks device without
+  # keyfile is configured
+  encryptedFSWithKeyfile = makeInstallerTest "encryptedFSWithKeyfile" {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+          + " mkpart primary ext2 1M 100MB"  # /boot
+          + " mkpart primary linux-swap 100M 1024M"
+          + " mkpart primary 1024M 1280M"  # LUKS with keyfile
+          + " mkpart primary 1280M -1s",
+          "udevadm settle",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.ext3 -L nixos /dev/vda4",
+          "mount LABEL=nixos /mnt",
+          "mkfs.ext3 -L boot /dev/vda1",
+          "mkdir -p /mnt/boot",
+          "mount LABEL=boot /mnt/boot",
+          "modprobe dm_mod dm_crypt",
+          "echo -n supersecret > /mnt/keyfile",
+          "cryptsetup luksFormat -q /dev/vda3 --key-file /mnt/keyfile",
+          "cryptsetup luksOpen --key-file /mnt/keyfile /dev/vda3 crypt",
+          "mkfs.ext3 -L test /dev/mapper/crypt",
+          "cryptsetup luksClose crypt",
+          "mkdir -p /mnt/test",
+      )
+    '';
+    extraConfig = ''
+      fileSystems."/test" = {
+        device = "/dev/disk/by-label/test";
+        fsType = "ext3";
+        encrypted.enable = true;
+        encrypted.blkDev = "/dev/vda3";
+        encrypted.label = "crypt";
+        encrypted.keyFile = "/mnt-root/keyfile";
+      };
+    '';
+  };
+
+  swraid = makeInstallerTest "swraid" {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda --"
+          + " mklabel msdos"
+          + " mkpart primary ext2 1M 100MB"  # /boot
+          + " mkpart extended 100M -1s"
+          + " mkpart logical 102M 3102M"  # md0 (root), first device
+          + " mkpart logical 3103M 6103M"  # md0 (root), second device
+          + " mkpart logical 6104M 6360M"  # md1 (swap), first device
+          + " mkpart logical 6361M 6617M",  # md1 (swap), second device
+          "udevadm settle",
+          "ls -l /dev/vda* >&2",
+          "cat /proc/partitions >&2",
+          "udevadm control --stop-exec-queue",
+          "mdadm --create --force /dev/md0 --metadata 1.2 --level=raid1 "
+          + "--raid-devices=2 /dev/vda5 /dev/vda6",
+          "mdadm --create --force /dev/md1 --metadata 1.2 --level=raid1 "
+          + "--raid-devices=2 /dev/vda7 /dev/vda8",
+          "udevadm control --start-exec-queue",
+          "udevadm settle",
+          "mkswap -f /dev/md1 -L swap",
+          "swapon -L swap",
+          "mkfs.ext3 -L nixos /dev/md0",
+          "mount LABEL=nixos /mnt",
+          "mkfs.ext3 -L boot /dev/vda1",
+          "mkdir /mnt/boot",
+          "mount LABEL=boot /mnt/boot",
+          "udevadm settle",
+      )
+    '';
+    preBootCommands = ''
+      machine.start()
+      machine.fail("dmesg | grep 'immediate safe mode'")
+    '';
+  };
+
+  bcache = makeInstallerTest "bcache" {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda --"
+          + " mklabel msdos"
+          + " mkpart primary ext2 1M 100MB"  # /boot
+          + " mkpart primary 100MB 512MB  "  # swap
+          + " mkpart primary 512MB 1024MB"  # Cache (typically SSD)
+          + " mkpart primary 1024MB -1s ",  # Backing device (typically HDD)
+          "modprobe bcache",
+          "udevadm settle",
+          "make-bcache -B /dev/vda4 -C /dev/vda3",
+          "udevadm settle",
+          "mkfs.ext3 -L nixos /dev/bcache0",
+          "mount LABEL=nixos /mnt",
+          "mkfs.ext3 -L boot /dev/vda1",
+          "mkdir /mnt/boot",
+          "mount LABEL=boot /mnt/boot",
+          "mkswap -f /dev/vda2 -L swap",
+          "swapon -L swap",
+      )
+    '';
+  };
+
+  # Test a basic install using GRUB 1.
+  grub1 = makeInstallerTest "grub1" rec {
+    createPartitions = ''
+      machine.succeed(
+          "flock ${grubDevice} parted --script ${grubDevice} -- mklabel msdos"
+          + " mkpart primary linux-swap 1M 1024M"
+          + " mkpart primary ext2 1024M -1s",
+          "udevadm settle",
+          "mkswap ${grubDevice}-part1 -L swap",
+          "swapon -L swap",
+          "mkfs.ext3 -L nixos ${grubDevice}-part2",
+          "mount LABEL=nixos /mnt",
+          "mkdir -p /mnt/tmp",
+      )
+    '';
+    grubVersion = 1;
+    # /dev/sda is not stable, even when the SCSI disk number is.
+    grubDevice = "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive1";
+  };
+
+  # Test using labels to identify volumes in grub
+  simpleLabels = makeInstallerTest "simpleLabels" {
+    createPartitions = ''
+      machine.succeed(
+          "sgdisk -Z /dev/vda",
+          "sgdisk -n 1:0:+1M -n 2:0:+1G -N 3 -t 1:ef02 -t 2:8200 -t 3:8300 -c 3:root /dev/vda",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.ext4 -L root /dev/vda3",
+          "mount LABEL=root /mnt",
+      )
+    '';
+    grubIdentifier = "label";
+  };
+
+  # Test using the provided disk name within grub
+  # TODO: Fix udev so the symlinks are unneeded in /dev/disks
+  simpleProvided = makeInstallerTest "simpleProvided" {
+    createPartitions = ''
+      uuid = "$(blkid -s UUID -o value /dev/vda2)"
+      machine.succeed(
+          "sgdisk -Z /dev/vda",
+          "sgdisk -n 1:0:+1M -n 2:0:+100M -n 3:0:+1G -N 4 -t 1:ef02 -t 2:8300 "
+          + "-t 3:8200 -t 4:8300 -c 2:boot -c 4:root /dev/vda",
+          "mkswap /dev/vda3 -L swap",
+          "swapon -L swap",
+          "mkfs.ext4 -L boot /dev/vda2",
+          "mkfs.ext4 -L root /dev/vda4",
+      )
+      machine.execute(f"ln -s ../../vda2 /dev/disk/by-uuid/{uuid}")
+      machine.execute("ln -s ../../vda4 /dev/disk/by-label/root")
+      machine.succeed(
+          "mount /dev/disk/by-label/root /mnt",
+          "mkdir /mnt/boot",
+          f"mount /dev/disk/by-uuid/{uuid} /mnt/boot",
+      )
+    '';
+    grubIdentifier = "provided";
+  };
+
+  # Simple btrfs grub testing
+  btrfsSimple = makeInstallerTest "btrfsSimple" {
+    createPartitions = ''
+      machine.succeed(
+          "sgdisk -Z /dev/vda",
+          "sgdisk -n 1:0:+1M -n 2:0:+1G -N 3 -t 1:ef02 -t 2:8200 -t 3:8300 -c 3:root /dev/vda",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.btrfs -L root /dev/vda3",
+          "mount LABEL=root /mnt",
+      )
+    '';
+  };
+
+  # Test to see if we can detect /boot and /nix on subvolumes
+  btrfsSubvols = makeInstallerTest "btrfsSubvols" {
+    createPartitions = ''
+      machine.succeed(
+          "sgdisk -Z /dev/vda",
+          "sgdisk -n 1:0:+1M -n 2:0:+1G -N 3 -t 1:ef02 -t 2:8200 -t 3:8300 -c 3:root /dev/vda",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.btrfs -L root /dev/vda3",
+          "btrfs device scan",
+          "mount LABEL=root /mnt",
+          "btrfs subvol create /mnt/boot",
+          "btrfs subvol create /mnt/nixos",
+          "btrfs subvol create /mnt/nixos/default",
+          "umount /mnt",
+          "mount -o defaults,subvol=nixos/default LABEL=root /mnt",
+          "mkdir /mnt/boot",
+          "mount -o defaults,subvol=boot LABEL=root /mnt/boot",
+      )
+    '';
+  };
+
+  # Test to see if we can detect default and aux subvolumes correctly
+  btrfsSubvolDefault = makeInstallerTest "btrfsSubvolDefault" {
+    createPartitions = ''
+      machine.succeed(
+          "sgdisk -Z /dev/vda",
+          "sgdisk -n 1:0:+1M -n 2:0:+1G -N 3 -t 1:ef02 -t 2:8200 -t 3:8300 -c 3:root /dev/vda",
+          "mkswap /dev/vda2 -L swap",
+          "swapon -L swap",
+          "mkfs.btrfs -L root /dev/vda3",
+          "btrfs device scan",
+          "mount LABEL=root /mnt",
+          "btrfs subvol create /mnt/badpath",
+          "btrfs subvol create /mnt/badpath/boot",
+          "btrfs subvol create /mnt/nixos",
+          "btrfs subvol set-default "
+          + "$(btrfs subvol list /mnt | grep 'nixos' | awk '{print $2}') /mnt",
+          "umount /mnt",
+          "mount -o defaults LABEL=root /mnt",
+          "mkdir -p /mnt/badpath/boot",  # Help ensure the detection mechanism
+          # is actually looking up subvolumes
+          "mkdir /mnt/boot",
+          "mount -o defaults,subvol=badpath/boot LABEL=root /mnt/boot",
+      )
+    '';
+  };
+}
diff --git a/nixos/tests/invidious.nix b/nixos/tests/invidious.nix
new file mode 100644
index 00000000000..8b831715a44
--- /dev/null
+++ b/nixos/tests/invidious.nix
@@ -0,0 +1,81 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "invidious";
+
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ sbruder ];
+  };
+
+  machine = { config, lib, pkgs, ... }: {
+    services.invidious = {
+      enable = true;
+    };
+
+    specialisation = {
+      nginx.configuration = {
+        services.invidious = {
+          nginx.enable = true;
+          domain = "invidious.example.com";
+        };
+        services.nginx.virtualHosts."invidious.example.com" = {
+          forceSSL = false;
+          enableACME = false;
+        };
+        networking.hosts."127.0.0.1" = [ "invidious.example.com" ];
+      };
+      postgres-tcp.configuration = {
+        services.invidious = {
+          database = {
+            createLocally = false;
+            host = "127.0.0.1";
+            passwordFile = toString (pkgs.writeText "database-password" "correct horse battery staple");
+          };
+        };
+        # Normally not needed because when connecting to postgres over TCP/IP
+        # the database is most likely on another host.
+        systemd.services.invidious = {
+          after = [ "postgresql.service" ];
+          requires = [ "postgresql.service" ];
+        };
+        services.postgresql =
+          let
+            inherit (config.services.invidious.settings.db) dbname user;
+          in
+          {
+            enable = true;
+            initialScript = pkgs.writeText "init-postgres-with-password" ''
+              CREATE USER kemal WITH PASSWORD 'correct horse battery staple';
+              CREATE DATABASE invidious;
+              GRANT ALL PRIVILEGES ON DATABASE invidious TO kemal;
+            '';
+          };
+      };
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+    def curl_assert_status_code(url, code, form=None):
+        assert int(machine.succeed(f"curl -s -o /dev/null -w %{{http_code}} {'-F ' + form + ' ' if form else '''}{url}")) == code
+
+
+    def activate_specialisation(name: str):
+        machine.succeed(f"${nodes.machine.config.system.build.toplevel}/specialisation/{name}/bin/switch-to-configuration test >&2")
+
+
+    url = "http://localhost:${toString nodes.machine.config.services.invidious.port}"
+    port = ${toString nodes.machine.config.services.invidious.port}
+
+    machine.wait_for_open_port(port)
+    curl_assert_status_code(f"{url}/search", 200)
+
+    activate_specialisation("nginx")
+    machine.wait_for_open_port(80)
+    curl_assert_status_code("http://invidious.example.com/search", 200)
+
+    # Remove the state so the `initialScript` gets run
+    machine.succeed("systemctl stop postgresql")
+    machine.succeed("rm -r /var/lib/postgresql")
+    activate_specialisation("postgres-tcp")
+    machine.wait_for_open_port(port)
+    curl_assert_status_code(f"{url}/search", 200)
+  '';
+})
diff --git a/nixos/tests/invoiceplane.nix b/nixos/tests/invoiceplane.nix
new file mode 100644
index 00000000000..4e63f8ac21c
--- /dev/null
+++ b/nixos/tests/invoiceplane.nix
@@ -0,0 +1,82 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "invoiceplane";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [
+      onny
+    ];
+  };
+
+  nodes = {
+    invoiceplane_caddy = { ... }: {
+      services.invoiceplane.webserver = "caddy";
+      services.invoiceplane.sites = {
+        "site1.local" = {
+          #database.name = "invoiceplane1";
+          database.createLocally = true;
+          enable = true;
+        };
+        "site2.local" = {
+          #database.name = "invoiceplane2";
+          database.createLocally = true;
+          enable = true;
+        };
+      };
+
+      networking.firewall.allowedTCPPorts = [ 80 ];
+      networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    invoiceplane_caddy.wait_for_unit("caddy")
+    invoiceplane_caddy.wait_for_open_port(80)
+    invoiceplane_caddy.wait_for_open_port(3306)
+
+    site_names = ["site1.local", "site2.local"]
+
+    for site_name in site_names:
+        machine.wait_for_unit(f"phpfpm-invoiceplane-{site_name}")
+
+        with subtest("Website returns welcome screen"):
+            assert "Please install InvoicePlane" in machine.succeed(f"curl -L {site_name}")
+
+        with subtest("Finish InvoicePlane setup"):
+          machine.succeed(
+            f"curl -sSfL --cookie-jar cjar {site_name}/index.php/setup/language"
+          )
+          csrf_token = machine.succeed(
+            "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'"
+          )
+          machine.succeed(
+            f"curl -sSfL --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&ip_lang=english&btn_continue=Continue' {site_name}/index.php/setup/language"
+          )
+          csrf_token = machine.succeed(
+            "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'"
+          )
+          machine.succeed(
+            f"curl -sSfL --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/index.php/setup/prerequisites"
+          )
+          csrf_token = machine.succeed(
+            "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'"
+          )
+          machine.succeed(
+            f"curl -sSfL --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/index.php/setup/configure_database"
+          )
+          csrf_token = machine.succeed(
+            "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'"
+          )
+          machine.succeed(
+            f"curl -sSfl --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/index.php/setup/install_tables"
+          )
+          csrf_token = machine.succeed(
+            "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'"
+          )
+          machine.succeed(
+            f"curl -sSfl --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/index.php/setup/upgrade_tables"
+          )
+  '';
+})
diff --git a/nixos/tests/iodine.nix b/nixos/tests/iodine.nix
new file mode 100644
index 00000000000..41fb2e7778d
--- /dev/null
+++ b/nixos/tests/iodine.nix
@@ -0,0 +1,64 @@
+import ./make-test-python.nix (
+  { pkgs, ... }: let
+    domain = "whatever.example.com";
+    password = "false;foo;exit;withspecialcharacters";
+  in
+    {
+      name = "iodine";
+      nodes = {
+        server =
+          { ... }:
+
+            {
+              networking.firewall = {
+                allowedUDPPorts = [ 53 ];
+                trustedInterfaces = [ "dns0" ];
+              };
+              boot.kernel.sysctl = {
+                "net.ipv4.ip_forward" = 1;
+                "net.ipv6.ip_forward" = 1;
+              };
+
+              services.iodine.server = {
+                enable = true;
+                ip = "10.53.53.1/24";
+                passwordFile = "${builtins.toFile "password" password}";
+                inherit domain;
+              };
+
+              # test resource: accessible only via tunnel
+              services.openssh = {
+                enable = true;
+                openFirewall = false;
+              };
+            };
+
+        client =
+          { ... }: {
+            services.iodine.clients.testClient = {
+              # test that ProtectHome is "read-only"
+              passwordFile = "/root/pw";
+              relay = "server";
+              server = domain;
+            };
+            systemd.tmpfiles.rules = [
+              "f /root/pw 0666 root root - ${password}"
+            ];
+            environment.systemPackages = [
+              pkgs.nagiosPluginsOfficial
+            ];
+          };
+
+      };
+
+      testScript = ''
+        start_all()
+
+        server.wait_for_unit("sshd")
+        server.wait_for_unit("iodined")
+        client.wait_for_unit("iodine-testClient")
+
+        client.succeed("check_ssh -H 10.53.53.1")
+      '';
+    }
+)
diff --git a/nixos/tests/ipfs.nix b/nixos/tests/ipfs.nix
new file mode 100644
index 00000000000..f8683b0a858
--- /dev/null
+++ b/nixos/tests/ipfs.nix
@@ -0,0 +1,39 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "ipfs";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mguentner ];
+  };
+
+  nodes.machine = { ... }: {
+    services.ipfs = {
+      enable = true;
+      # Also will add a unix domain socket socket API address, see module.
+      startWhenNeeded = true;
+      apiAddress = "/ip4/127.0.0.1/tcp/2324";
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    # IPv4 activation
+
+    machine.succeed("ipfs --api /ip4/127.0.0.1/tcp/2324 id")
+    ipfs_hash = machine.succeed(
+        "echo fnord | ipfs --api /ip4/127.0.0.1/tcp/2324 add | awk '{ print $2 }'"
+    )
+
+    machine.succeed(f"ipfs cat /ipfs/{ipfs_hash.strip()} | grep fnord")
+
+    # Unix domain socket activation
+
+    machine.stop_job("ipfs")
+
+    ipfs_hash = machine.succeed(
+        "echo fnord2 | ipfs --api /unix/run/ipfs.sock add | awk '{ print $2 }'"
+    )
+    machine.succeed(
+        f"ipfs --api /unix/run/ipfs.sock cat /ipfs/{ipfs_hash.strip()} | grep fnord2"
+    )
+  '';
+})
diff --git a/nixos/tests/ipv6.nix b/nixos/tests/ipv6.nix
new file mode 100644
index 00000000000..75faa6f6020
--- /dev/null
+++ b/nixos/tests/ipv6.nix
@@ -0,0 +1,130 @@
+# Test of IPv6 functionality in NixOS, including whether router
+# solicication/advertisement using radvd works.
+
+import ./make-test-python.nix ({ pkgs, lib, ...} : {
+  name = "ipv6";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eelco ];
+  };
+
+  nodes =
+    {
+      # We use lib.mkForce here to remove the interface configuration
+      # provided by makeTest, so that the interfaces are all configured
+      # implicitly.
+
+      # This client should use privacy extensions fully, having a
+      # completely-default network configuration.
+      client_defaults.networking.interfaces = lib.mkForce {};
+
+      # Both of these clients should obtain temporary addresses, but
+      # not use them as the default source IP. We thus run the same
+      # checks against them — but the configuration resulting in this
+      # behaviour is different.
+
+      # Here, by using an altered default value for the global setting...
+      client_global_setting = {
+        networking.interfaces = lib.mkForce {};
+        networking.tempAddresses = "enabled";
+      };
+      # and here, by setting this on the interface explicitly.
+      client_interface_setting = {
+        networking.tempAddresses = "disabled";
+        networking.interfaces = lib.mkForce {
+          eth1.tempAddress = "enabled";
+        };
+      };
+
+      server =
+        { services.httpd.enable = true;
+          services.httpd.adminAddr = "foo@example.org";
+          networking.firewall.allowedTCPPorts = [ 80 ];
+        };
+
+      router =
+        { ... }:
+        { services.radvd.enable = true;
+          services.radvd.config =
+            ''
+              interface eth1 {
+                AdvSendAdvert on;
+                # ULA prefix (RFC 4193).
+                prefix fd60:cc69:b537:1::/64 { };
+              };
+            '';
+        };
+    };
+
+  testScript =
+    ''
+      import re
+
+      # Start the router first so that it respond to router solicitations.
+      router.wait_for_unit("radvd")
+
+      clients = [client_defaults, client_global_setting, client_interface_setting]
+
+      start_all()
+
+      for client in clients:
+          client.wait_for_unit("network.target")
+      server.wait_for_unit("network.target")
+      server.wait_for_unit("httpd.service")
+
+      # Wait until the given interface has a non-tentative address of
+      # the desired scope (i.e. has completed Duplicate Address
+      # Detection).
+      def wait_for_address(machine, iface, scope, temporary=False):
+          temporary_flag = "temporary" if temporary else "-temporary"
+          cmd = f"ip -o -6 addr show dev {iface} scope {scope} -tentative {temporary_flag}"
+
+          machine.wait_until_succeeds(f"[ `{cmd} | wc -l` -eq 1 ]")
+          output = machine.succeed(cmd)
+          ip = re.search(r"inet6 ([0-9a-f:]{2,})/", output).group(1)
+
+          if temporary:
+              scope = scope + " temporary"
+          machine.log(f"{scope} address on {iface} is {ip}")
+          return ip
+
+
+      with subtest("Loopback address can be pinged"):
+          client_defaults.succeed("ping -c 1 ::1 >&2")
+          client_defaults.fail("ping -c 1 2001:db8:: >&2")
+
+      with subtest("Local link addresses can be obtained and pinged"):
+          for client in clients:
+              client_ip = wait_for_address(client, "eth1", "link")
+              server_ip = wait_for_address(server, "eth1", "link")
+              client.succeed(f"ping -c 1 {client_ip}%eth1 >&2")
+              client.succeed(f"ping -c 1 {server_ip}%eth1 >&2")
+
+      with subtest("Global addresses can be obtained, pinged, and reached via http"):
+          for client in clients:
+              client_ip = wait_for_address(client, "eth1", "global")
+              server_ip = wait_for_address(server, "eth1", "global")
+              client.succeed(f"ping -c 1 {client_ip} >&2")
+              client.succeed(f"ping -c 1 {server_ip} >&2")
+              client.succeed(f"curl --fail -g http://[{server_ip}]")
+              client.fail(f"curl --fail -g http://[{client_ip}]")
+
+      with subtest(
+          "Privacy extensions: Global temporary address is used as default source address"
+      ):
+          ip = wait_for_address(client_defaults, "eth1", "global", temporary=True)
+          # Default route should have "src <temporary address>" in it
+          client_defaults.succeed(f"ip route get 2001:db8:: | grep 'src {ip}'")
+
+      for client, setting_desc in (
+          (client_global_setting, "global"),
+          (client_interface_setting, "interface"),
+      ):
+          with subtest(f'Privacy extensions: "enabled" through {setting_desc} setting)'):
+              # We should be obtaining both a temporary address and an EUI-64 address...
+              ip = wait_for_address(client, "eth1", "global")
+              assert "ff:fe" in ip
+              ip_temp = wait_for_address(client, "eth1", "global", temporary=True)
+              # But using the EUI-64 one.
+              client.succeed(f"ip route get 2001:db8:: | grep 'src {ip}'")
+    '';
+})
diff --git a/nixos/tests/iscsi-multipath-root.nix b/nixos/tests/iscsi-multipath-root.nix
new file mode 100644
index 00000000000..92ae9990c94
--- /dev/null
+++ b/nixos/tests/iscsi-multipath-root.nix
@@ -0,0 +1,267 @@
+import ./make-test-python.nix (
+  { pkgs, lib, ... }:
+  let
+    initiatorName = "iqn.2020-08.org.linux-iscsi.initiatorhost:example";
+    targetName = "iqn.2003-01.org.linux-iscsi.target.x8664:sn.acf8fd9c23af";
+  in
+  {
+    name = "iscsi";
+    meta = {
+      maintainers = pkgs.lib.teams.deshaw.members;
+    };
+
+    nodes = {
+      target = { config, pkgs, lib, ... }: {
+        virtualisation.vlans = [ 1 2 ];
+        services.target = {
+          enable = true;
+          config = {
+            fabric_modules = [ ];
+            storage_objects = [
+              {
+                dev = "/dev/vdb";
+                name = "test";
+                plugin = "block";
+                write_back = true;
+                wwn = "92b17c3f-6b40-4168-b082-ceeb7b495522";
+              }
+            ];
+            targets = [
+              {
+                fabric = "iscsi";
+                tpgs = [
+                  {
+                    enable = true;
+                    attributes = {
+                      authentication = 0;
+                      generate_node_acls = 1;
+                    };
+                    luns = [
+                      {
+                        alias = "94dfe06967";
+                        alua_tg_pt_gp_name = "default_tg_pt_gp";
+                        index = 0;
+                        storage_object = "/backstores/block/test";
+                      }
+                    ];
+                    node_acls = [
+                      {
+                        mapped_luns = [
+                          {
+                            alias = "d42f5bdf8a";
+                            index = 0;
+                            tpg_lun = 0;
+                            write_protect = false;
+                          }
+                        ];
+                        node_wwn = initiatorName;
+                      }
+                    ];
+                    portals = [
+                      {
+                        ip_address = "0.0.0.0";
+                        iser = false;
+                        offload = false;
+                        port = 3260;
+                      }
+                    ];
+                    tag = 1;
+                  }
+                ];
+                wwn = targetName;
+              }
+            ];
+          };
+        };
+
+        networking.firewall.allowedTCPPorts = [ 3260 ];
+        networking.firewall.allowedUDPPorts = [ 3260 ];
+
+        virtualisation.memorySize = 2048;
+        virtualisation.emptyDiskImages = [ 2048 ];
+      };
+
+      initiatorAuto = { nodes, config, pkgs, ... }: {
+        virtualisation.vlans = [ 1 2 ];
+
+        services.multipath = {
+          enable = true;
+          defaults = ''
+            find_multipaths yes
+            user_friendly_names yes
+          '';
+          pathGroups = [
+            {
+              alias = 123456;
+              wwid = "3600140592b17c3f6b404168b082ceeb7";
+            }
+          ];
+        };
+
+        services.openiscsi = {
+          enable = true;
+          enableAutoLoginOut = true;
+          discoverPortal = "target";
+          name = initiatorName;
+        };
+
+        environment.systemPackages = with pkgs; [
+          xfsprogs
+        ];
+
+        environment.etc."initiator-root-disk-closure".source = nodes.initiatorRootDisk.config.system.build.toplevel;
+
+        nix.settings = {
+          substituters = lib.mkForce [ ];
+          hashed-mirrors = null;
+          connect-timeout = 1;
+        };
+      };
+
+      initiatorRootDisk = { config, pkgs, modulesPath, lib, ... }: {
+        boot.initrd.network.enable = true;
+        boot.loader.grub.enable = false;
+
+        boot.kernelParams = lib.mkOverride 5 (
+          [
+            "boot.shell_on_fail"
+            "console=tty1"
+            "ip=192.168.1.1:::255.255.255.0::ens9:none"
+            "ip=192.168.2.1:::255.255.255.0::ens10:none"
+          ]
+        );
+
+        # defaults to true, puts some code in the initrd that tries to mount an overlayfs on /nix/store
+        virtualisation.writableStore = false;
+        virtualisation.vlans = [ 1 2 ];
+
+        services.multipath = {
+          enable = true;
+          defaults = ''
+            find_multipaths yes
+            user_friendly_names yes
+          '';
+          pathGroups = [
+            {
+              alias = 123456;
+              wwid = "3600140592b17c3f6b404168b082ceeb7";
+            }
+          ];
+        };
+
+        fileSystems = lib.mkOverride 5 {
+          "/" = {
+            fsType = "xfs";
+            device = "/dev/mapper/123456";
+            options = [ "_netdev" ];
+          };
+        };
+
+        boot.initrd.extraFiles."etc/multipath/wwids".source = pkgs.writeText "wwids" "/3600140592b17c3f6b404168b082ceeb7/";
+
+        boot.iscsi-initiator = {
+          discoverPortal = "target";
+          name = initiatorName;
+          target = targetName;
+          extraIscsiCommands = ''
+            iscsiadm -m discovery -o update -t sendtargets -p 192.168.2.3 --login
+          '';
+        };
+      };
+
+    };
+
+    testScript = { nodes, ... }: ''
+      target.start()
+      target.wait_for_unit("iscsi-target.service")
+
+      initiatorAuto.start()
+
+      initiatorAuto.wait_for_unit("iscsid.service")
+      initiatorAuto.wait_for_unit("iscsi.service")
+      initiatorAuto.get_unit_info("iscsi")
+
+      # Expecting this to fail since we should already know about 192.168.1.3
+      initiatorAuto.fail("iscsiadm -m discovery -o update -t sendtargets -p 192.168.1.3 --login")
+      # Expecting this to succeed since we don't yet know about 192.168.2.3
+      initiatorAuto.succeed("iscsiadm -m discovery -o update -t sendtargets -p 192.168.2.3 --login")
+
+      # /dev/sda is provided by iscsi on target
+      initiatorAuto.succeed("set -x; while ! test -e /dev/sda; do sleep 1; done")
+
+      initiatorAuto.succeed("mkfs.xfs /dev/sda")
+      initiatorAuto.succeed("mkdir /mnt")
+
+      # Start by verifying /dev/sda and /dev/sdb are both the same disk
+      initiatorAuto.succeed("mount /dev/sda /mnt")
+      initiatorAuto.succeed("touch /mnt/hi")
+      initiatorAuto.succeed("umount /mnt")
+
+      initiatorAuto.succeed("mount /dev/sdb /mnt")
+      initiatorAuto.succeed("test -e /mnt/hi")
+      initiatorAuto.succeed("umount /mnt")
+
+      initiatorAuto.succeed("systemctl restart multipathd")
+      initiatorAuto.succeed("multipath -ll | systemd-cat")
+
+      # Install our RootDisk machine to 123456, the alias to the device that multipath is now managing
+      initiatorAuto.succeed("mount /dev/mapper/123456 /mnt")
+      initiatorAuto.succeed("mkdir -p /mnt/etc/{multipath,iscsi}")
+      initiatorAuto.succeed("cp -r /etc/multipath/wwids /mnt/etc/multipath/wwids")
+      initiatorAuto.succeed("cp -r /etc/iscsi/{nodes,send_targets} /mnt/etc/iscsi")
+      initiatorAuto.succeed(
+        "nixos-install --no-bootloader --no-root-passwd --system /etc/initiator-root-disk-closure"
+      )
+      initiatorAuto.succeed("umount /mnt")
+      initiatorAuto.shutdown()
+
+      initiatorRootDisk.start()
+      initiatorRootDisk.wait_for_unit("multi-user.target")
+      initiatorRootDisk.wait_for_unit("iscsid")
+
+      # Log in over both nodes
+      initiatorRootDisk.fail("iscsiadm -m discovery -o update -t sendtargets -p 192.168.1.3 --login")
+      initiatorRootDisk.fail("iscsiadm -m discovery -o update -t sendtargets -p 192.168.2.3 --login")
+      initiatorRootDisk.succeed("systemctl restart multipathd")
+      initiatorRootDisk.succeed("multipath -ll | systemd-cat")
+
+      # Verify we can write and sync the root disk
+      initiatorRootDisk.succeed("mkdir /scratch")
+      initiatorRootDisk.succeed("touch /scratch/both-up")
+      initiatorRootDisk.succeed("sync /scratch")
+
+      # Verify we can write to the root with ens9 (sda, 192.168.1.3) down
+      initiatorRootDisk.succeed("ip link set ens9 down")
+      initiatorRootDisk.succeed("touch /scratch/ens9-down")
+      initiatorRootDisk.succeed("sync /scratch")
+      initiatorRootDisk.succeed("ip link set ens9 up")
+
+      # todo: better way to wait until multipath notices the link is back
+      initiatorRootDisk.succeed("sleep 5")
+      initiatorRootDisk.succeed("touch /scratch/both-down")
+      initiatorRootDisk.succeed("sync /scratch")
+
+      # Verify we can write to the root with ens10 (sdb, 192.168.2.3) down
+      initiatorRootDisk.succeed("ip link set ens10 down")
+      initiatorRootDisk.succeed("touch /scratch/ens10-down")
+      initiatorRootDisk.succeed("sync /scratch")
+      initiatorRootDisk.succeed("ip link set ens10 up")
+      initiatorRootDisk.succeed("touch /scratch/ens10-down")
+      initiatorRootDisk.succeed("sync /scratch")
+
+      initiatorRootDisk.succeed("ip link set ens9 up")
+      initiatorRootDisk.succeed("ip link set ens10 up")
+      initiatorRootDisk.shutdown()
+
+      # Verify we can boot with the target's eth1 down, forcing
+      # it to multipath via the second link
+      target.succeed("ip link set eth1 down")
+      initiatorRootDisk.start()
+      initiatorRootDisk.wait_for_unit("multi-user.target")
+      initiatorRootDisk.wait_for_unit("iscsid")
+      initiatorRootDisk.succeed("test -e /scratch/both-up")
+    '';
+  }
+)
+
+
diff --git a/nixos/tests/iscsi-root.nix b/nixos/tests/iscsi-root.nix
new file mode 100644
index 00000000000..eb0719edc37
--- /dev/null
+++ b/nixos/tests/iscsi-root.nix
@@ -0,0 +1,161 @@
+import ./make-test-python.nix (
+  { pkgs, lib, ... }:
+    let
+      initiatorName = "iqn.2020-08.org.linux-iscsi.initiatorhost:example";
+      targetName = "iqn.2003-01.org.linux-iscsi.target.x8664:sn.acf8fd9c23af";
+    in
+      {
+        name = "iscsi";
+        meta = {
+          maintainers = pkgs.lib.teams.deshaw.members
+          ++ (with pkgs.lib.maintainers; [ ajs124 ]);
+        };
+
+        nodes = {
+          target = { config, pkgs, lib, ... }: {
+            services.target = {
+              enable = true;
+              config = {
+                fabric_modules = [];
+                storage_objects = [
+                  {
+                    dev = "/dev/vdb";
+                    name = "test";
+                    plugin = "block";
+                    write_back = true;
+                    wwn = "92b17c3f-6b40-4168-b082-ceeb7b495522";
+                  }
+                ];
+                targets = [
+                  {
+                    fabric = "iscsi";
+                    tpgs = [
+                      {
+                        enable = true;
+                        attributes = {
+                          authentication = 0;
+                          generate_node_acls = 1;
+                        };
+                        luns = [
+                          {
+                            alias = "94dfe06967";
+                            alua_tg_pt_gp_name = "default_tg_pt_gp";
+                            index = 0;
+                            storage_object = "/backstores/block/test";
+                          }
+                        ];
+                        node_acls = [
+                          {
+                            mapped_luns = [
+                              {
+                                alias = "d42f5bdf8a";
+                                index = 0;
+                                tpg_lun = 0;
+                                write_protect = false;
+                              }
+                            ];
+                            node_wwn = initiatorName;
+                          }
+                        ];
+                        portals = [
+                          {
+                            ip_address = "0.0.0.0";
+                            iser = false;
+                            offload = false;
+                            port = 3260;
+                          }
+                        ];
+                        tag = 1;
+                      }
+                    ];
+                    wwn = targetName;
+                  }
+                ];
+              };
+            };
+
+            networking.firewall.allowedTCPPorts = [ 3260 ];
+            networking.firewall.allowedUDPPorts = [ 3260 ];
+
+            virtualisation.memorySize = 2048;
+            virtualisation.emptyDiskImages = [ 2048 ];
+          };
+
+          initiatorAuto = { nodes, config, pkgs, ... }: {
+            services.openiscsi = {
+              enable = true;
+              enableAutoLoginOut = true;
+              discoverPortal = "target";
+              name = initiatorName;
+            };
+
+            environment.systemPackages = with pkgs; [
+              xfsprogs
+            ];
+
+            system.extraDependencies = [ nodes.initiatorRootDisk.config.system.build.toplevel ];
+
+            nix.settings = {
+              substituters = lib.mkForce [];
+              hashed-mirrors = null;
+              connect-timeout = 1;
+            };
+          };
+
+          initiatorRootDisk = { config, pkgs, modulesPath, lib, ... }: {
+            boot.loader.grub.enable = false;
+            boot.kernelParams = lib.mkOverride 5 (
+              [
+                "boot.shell_on_fail"
+                "console=tty1"
+                "ip=${config.networking.primaryIPAddress}:::255.255.255.0::ens9:none"
+              ]
+            );
+
+            # defaults to true, puts some code in the initrd that tries to mount an overlayfs on /nix/store
+            virtualisation.writableStore = false;
+
+            fileSystems = lib.mkOverride 5 {
+              "/" = {
+                fsType = "xfs";
+                device = "/dev/sda";
+                options = [ "_netdev" ];
+              };
+            };
+
+            boot.iscsi-initiator = {
+              discoverPortal = "target";
+              name = initiatorName;
+              target = targetName;
+            };
+          };
+        };
+
+        testScript = { nodes, ... }: ''
+          target.start()
+          target.wait_for_unit("iscsi-target.service")
+
+          initiatorAuto.start()
+
+          initiatorAuto.wait_for_unit("iscsid.service")
+          initiatorAuto.wait_for_unit("iscsi.service")
+          initiatorAuto.get_unit_info("iscsi")
+
+          initiatorAuto.succeed("set -x; while ! test -e /dev/sda; do sleep 1; done")
+
+          initiatorAuto.succeed("mkfs.xfs /dev/sda")
+          initiatorAuto.succeed("mkdir /mnt && mount /dev/sda /mnt")
+          initiatorAuto.succeed(
+              "nixos-install --no-bootloader --no-root-passwd --system ${nodes.initiatorRootDisk.config.system.build.toplevel}"
+          )
+          initiatorAuto.succeed("umount /mnt && rmdir /mnt")
+          initiatorAuto.shutdown()
+
+          initiatorRootDisk.start()
+          initiatorRootDisk.wait_for_unit("multi-user.target")
+          initiatorRootDisk.wait_for_unit("iscsid")
+          initiatorRootDisk.succeed("touch test")
+          initiatorRootDisk.shutdown()
+        '';
+      }
+)
diff --git a/nixos/tests/isso.nix b/nixos/tests/isso.nix
new file mode 100644
index 00000000000..99dc8009ae0
--- /dev/null
+++ b/nixos/tests/isso.nix
@@ -0,0 +1,30 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "isso";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ asbachb ];
+  };
+
+  machine = { config, pkgs, ... }: {
+    services.isso = {
+      enable = true;
+      settings = {
+        general = {
+          dbpath = "/var/lib/isso/comments.db";
+          host = "http://localhost";
+        };
+      };
+    };
+  };
+
+  testScript = let
+    port = 8080;
+  in
+  ''
+    machine.wait_for_unit("isso.service")
+
+    machine.wait_for_open_port("${toString port}")
+
+    machine.succeed("curl --fail http://localhost:${toString port}/?uri")
+    machine.succeed("curl --fail http://localhost:${toString port}/js/embed.min.js")
+  '';
+})
diff --git a/nixos/tests/jackett.nix b/nixos/tests/jackett.nix
new file mode 100644
index 00000000000..0a706c99b99
--- /dev/null
+++ b/nixos/tests/jackett.nix
@@ -0,0 +1,19 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "jackett";
+  meta.maintainers = with maintainers; [ etu ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    { services.jackett.enable = true; };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("jackett.service")
+    machine.wait_for_open_port(9117)
+    machine.succeed("curl --fail http://localhost:9117/")
+  '';
+})
diff --git a/nixos/tests/jellyfin.nix b/nixos/tests/jellyfin.nix
new file mode 100644
index 00000000000..cae31a71925
--- /dev/null
+++ b/nixos/tests/jellyfin.nix
@@ -0,0 +1,155 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+
+  {
+    name = "jellyfin";
+    meta.maintainers = with lib.maintainers; [ minijackson ];
+
+    machine =
+      { ... }:
+      {
+        services.jellyfin.enable = true;
+        environment.systemPackages = with pkgs; [ ffmpeg ];
+      };
+
+    # Documentation of the Jellyfin API: https://api.jellyfin.org/
+    # Beware, this link can be resource intensive
+    testScript =
+      let
+        payloads = {
+          auth = pkgs.writeText "auth.json" (builtins.toJSON {
+            Username = "jellyfin";
+          });
+          empty = pkgs.writeText "empty.json" (builtins.toJSON { });
+        };
+      in
+      ''
+        import json
+        from urllib.parse import urlencode
+
+        machine.wait_for_unit("jellyfin.service")
+        machine.wait_for_open_port(8096)
+        machine.succeed("curl --fail http://localhost:8096/")
+
+        machine.wait_until_succeeds("curl --fail http://localhost:8096/health | grep Healthy")
+
+        auth_header = 'MediaBrowser Client="NixOS Integration Tests", DeviceId="1337", Device="Apple II", Version="20.09"'
+
+
+        def api_get(path):
+            return f"curl --fail 'http://localhost:8096{path}' -H 'X-Emby-Authorization:{auth_header}'"
+
+
+        def api_post(path, json_file=None):
+            if json_file:
+                return f"curl --fail -X post 'http://localhost:8096{path}' -d '@{json_file}' -H Content-Type:application/json -H 'X-Emby-Authorization:{auth_header}'"
+            else:
+                return f"curl --fail -X post 'http://localhost:8096{path}' -H 'X-Emby-Authorization:{auth_header}'"
+
+
+        with machine.nested("Wizard completes"):
+            machine.wait_until_succeeds(api_get("/Startup/Configuration"))
+            machine.succeed(api_get("/Startup/FirstUser"))
+            machine.succeed(api_post("/Startup/Complete"))
+
+        with machine.nested("Can login"):
+            auth_result = machine.succeed(
+                api_post(
+                    "/Users/AuthenticateByName",
+                    "${payloads.auth}",
+                )
+            )
+            auth_result = json.loads(auth_result)
+            auth_token = auth_result["AccessToken"]
+            auth_header += f", Token={auth_token}"
+
+            sessions_result = machine.succeed(api_get("/Sessions"))
+            sessions_result = json.loads(sessions_result)
+
+            this_session = [
+                session for session in sessions_result if session["DeviceId"] == "1337"
+            ]
+            if len(this_session) != 1:
+                raise Exception("Session not created")
+
+            me = machine.succeed(api_get("/Users/Me"))
+            me = json.loads(me)["Id"]
+
+        with machine.nested("Can add library"):
+            tempdir = machine.succeed("mktemp -d -p /var/lib/jellyfin").strip()
+            machine.succeed(f"chmod 755 '{tempdir}'")
+
+            # Generate a dummy video that we can test later
+            videofile = f"{tempdir}/Big Buck Bunny (2008) [1080p].mkv"
+            machine.succeed(f"ffmpeg -f lavfi -i testsrc2=duration=5 '{videofile}'")
+
+            add_folder_query = urlencode(
+                {
+                    "name": "My Library",
+                    "collectionType": "Movies",
+                    "paths": tempdir,
+                    "refreshLibrary": "true",
+                }
+            )
+
+            machine.succeed(
+                api_post(
+                    f"/Library/VirtualFolders?{add_folder_query}",
+                    "${payloads.empty}",
+                )
+            )
+
+
+        def is_refreshed(_):
+            folders = machine.succeed(api_get("/Library/VirtualFolders"))
+            folders = json.loads(folders)
+            print(folders)
+            return all(folder["RefreshStatus"] == "Idle" for folder in folders)
+
+
+        retry(is_refreshed)
+
+        with machine.nested("Can identify videos"):
+            items = []
+
+            # For some reason, having the folder refreshed doesn't mean the
+            # movie was scanned
+            def has_movie(_):
+                global items
+
+                items = machine.succeed(
+                    api_get(f"/Users/{me}/Items?IncludeItemTypes=Movie&Recursive=true")
+                )
+                items = json.loads(items)["Items"]
+
+                return len(items) == 1
+
+            retry(has_movie)
+
+            video = items[0]["Id"]
+
+            item_info = machine.succeed(api_get(f"/Users/{me}/Items/{video}"))
+            item_info = json.loads(item_info)
+
+            if item_info["Name"] != "Big Buck Bunny":
+                raise Exception("Jellyfin failed to properly identify file")
+
+        with machine.nested("Can read videos"):
+            media_source_id = item_info["MediaSources"][0]["Id"]
+
+            machine.succeed(
+                "ffmpeg"
+                + f" -headers 'X-Emby-Authorization:{auth_header}'"
+                + f" -i http://localhost:8096/Videos/{video}/master.m3u8?mediaSourceId={media_source_id}"
+                + " /tmp/test.mkv"
+            )
+
+            duration = machine.succeed(
+                "ffprobe /tmp/test.mkv"
+                + " -show_entries format=duration"
+                + " -of compact=print_section=0:nokey=1"
+            )
+
+            if duration.strip() != "5.000000":
+                raise Exception("Downloaded video has wrong duration")
+      '';
+  })
diff --git a/nixos/tests/jenkins-cli.nix b/nixos/tests/jenkins-cli.nix
new file mode 100644
index 00000000000..f25e1604da3
--- /dev/null
+++ b/nixos/tests/jenkins-cli.nix
@@ -0,0 +1,30 @@
+import ./make-test-python.nix ({ pkgs, ...} : rec {
+  name = "jenkins-cli";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ pamplemousse ];
+  };
+
+  nodes = {
+    machine =
+      { ... }:
+      {
+        services.jenkins = {
+          enable = true;
+          withCLI = true;
+        };
+      };
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("jenkins")
+
+    assert "JENKINS_URL" in machine.succeed("env")
+    assert "http://0.0.0.0:8080" in machine.succeed("echo $JENKINS_URL")
+
+    machine.succeed(
+        "jenkins-cli -auth admin:$(cat /var/lib/jenkins/secrets/initialAdminPassword)"
+    )
+  '';
+})
diff --git a/nixos/tests/jenkins.nix b/nixos/tests/jenkins.nix
new file mode 100644
index 00000000000..cb4207c6e77
--- /dev/null
+++ b/nixos/tests/jenkins.nix
@@ -0,0 +1,130 @@
+# verifies:
+#   1. jenkins service starts on master node
+#   2. jenkins user can be extended on both master and slave
+#   3. jenkins service not started on slave node
+#   4. declarative jobs can be added and removed
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "jenkins";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ bjornfor coconnor domenkozar eelco ];
+  };
+
+  nodes = {
+
+    master =
+      { ... }:
+      { services.jenkins = {
+          enable = true;
+          jobBuilder = {
+            enable = true;
+            nixJobs = [
+              { job = {
+                  name = "job-1";
+                  builders = [
+                    { shell = ''
+                        echo "Running job-1"
+                      '';
+                    }
+                  ];
+                };
+              }
+
+              { job = {
+                  name = "folder-1";
+                  project-type = "folder";
+                };
+              }
+
+              { job = {
+                  name = "folder-1/job-2";
+                  builders = [
+                    { shell = ''
+                        echo "Running job-2"
+                      '';
+                    }
+                  ];
+                };
+              }
+            ];
+          };
+        };
+
+        specialisation.noJenkinsJobs.configuration = {
+          services.jenkins.jobBuilder.nixJobs = pkgs.lib.mkForce [];
+        };
+
+        # should have no effect
+        services.jenkinsSlave.enable = true;
+
+        users.users.jenkins.extraGroups = [ "users" ];
+
+        systemd.services.jenkins.serviceConfig.TimeoutStartSec = "6min";
+      };
+
+    slave =
+      { ... }:
+      { services.jenkinsSlave.enable = true;
+
+        users.users.jenkins.extraGroups = [ "users" ];
+      };
+
+  };
+
+  testScript = { nodes, ... }:
+    let
+      configWithoutJobs = "${nodes.master.config.system.build.toplevel}/specialisation/noJenkinsJobs";
+      jenkinsPort = nodes.master.config.services.jenkins.port;
+      jenkinsUrl = "http://localhost:${toString jenkinsPort}";
+    in ''
+    start_all()
+
+    master.wait_for_unit("jenkins")
+
+    assert "Authentication required" in master.succeed("curl http://localhost:8080")
+
+    for host in master, slave:
+        groups = host.succeed("sudo -u jenkins groups")
+        assert "jenkins" in groups
+        assert "users" in groups
+
+    slave.fail("systemctl is-enabled jenkins.service")
+
+    with subtest("jobs are declarative"):
+        # Check that jobs are created on disk.
+        master.wait_for_unit("jenkins-job-builder")
+        master.wait_until_fails("systemctl is-active jenkins-job-builder")
+        master.wait_until_succeeds("test -f /var/lib/jenkins/jobs/job-1/config.xml")
+        master.wait_until_succeeds("test -f /var/lib/jenkins/jobs/folder-1/config.xml")
+        master.wait_until_succeeds("test -f /var/lib/jenkins/jobs/folder-1/jobs/job-2/config.xml")
+
+        # Wait until jenkins is ready, reload configuration and verify it also
+        # sees the jobs.
+        master.succeed("curl --fail ${jenkinsUrl}/cli")
+        master.succeed("curl ${jenkinsUrl}/jnlpJars/jenkins-cli.jar -O")
+        master.succeed("${pkgs.jre}/bin/java -jar jenkins-cli.jar -s ${jenkinsUrl} -auth admin:$(cat /var/lib/jenkins/secrets/initialAdminPassword) reload-configuration")
+        out = master.succeed("${pkgs.jre}/bin/java -jar jenkins-cli.jar -s ${jenkinsUrl} -auth admin:$(cat /var/lib/jenkins/secrets/initialAdminPassword) list-jobs")
+        jobs = [x.strip() for x in out.splitlines()]
+        # Seeing jobs inside folders requires the Folders plugin
+        # (https://plugins.jenkins.io/cloudbees-folder/), which we don't have
+        # in this vanilla jenkins install, so limit ourself to non-folder jobs.
+        assert jobs == ['job-1'], f"jobs != ['job-1']: {jobs}"
+
+        master.succeed(
+            "${configWithoutJobs}/bin/switch-to-configuration test >&2"
+        )
+
+        # Check that jobs are removed from disk.
+        master.wait_for_unit("jenkins-job-builder")
+        master.wait_until_fails("systemctl is-active jenkins-job-builder")
+        master.wait_until_fails("test -f /var/lib/jenkins/jobs/job-1/config.xml")
+        master.wait_until_fails("test -f /var/lib/jenkins/jobs/folder-1/config.xml")
+        master.wait_until_fails("test -f /var/lib/jenkins/jobs/folder-1/jobs/job-2/config.xml")
+
+        # Reload jenkins' configuration and verify it also sees the jobs as removed.
+        master.succeed("${pkgs.jre}/bin/java -jar jenkins-cli.jar -s ${jenkinsUrl} -auth admin:$(cat /var/lib/jenkins/secrets/initialAdminPassword) reload-configuration")
+        out = master.succeed("${pkgs.jre}/bin/java -jar jenkins-cli.jar -s ${jenkinsUrl} -auth admin:$(cat /var/lib/jenkins/secrets/initialAdminPassword) list-jobs")
+        jobs = [x.strip() for x in out.splitlines()]
+        assert jobs == [], f"jobs != []: {jobs}"
+  '';
+})
diff --git a/nixos/tests/jibri.nix b/nixos/tests/jibri.nix
new file mode 100644
index 00000000000..af20e639d30
--- /dev/null
+++ b/nixos/tests/jibri.nix
@@ -0,0 +1,69 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "jibri";
+  meta = with pkgs.lib; {
+    maintainers = teams.jitsi.members;
+  };
+
+    machine = { config, pkgs, ... }: {
+      virtualisation.memorySize = 5120;
+
+      services.jitsi-meet = {
+        enable = true;
+        hostName = "machine";
+        jibri.enable = true;
+      };
+      services.jibri.ignoreCert = true;
+      services.jitsi-videobridge.openFirewall = true;
+
+      networking.firewall.allowedTCPPorts = [ 80 443 ];
+
+      services.nginx.virtualHosts.machine = {
+        enableACME = true;
+        forceSSL = true;
+      };
+
+      security.acme.email = "me@example.org";
+      security.acme.acceptTerms = true;
+      security.acme.server = "https://example.com"; # self-signed only
+    };
+
+  testScript = ''
+    machine.wait_for_unit("jitsi-videobridge2.service")
+    machine.wait_for_unit("jicofo.service")
+    machine.wait_for_unit("nginx.service")
+    machine.wait_for_unit("prosody.service")
+    machine.wait_for_unit("jibri.service")
+
+    machine.wait_until_succeeds(
+        "journalctl -b -u jitsi-videobridge2 -o cat | grep -q 'Performed a successful health check'", timeout=30
+    )
+    machine.wait_until_succeeds(
+        "journalctl -b -u prosody -o cat | grep -q 'Authenticated as focus@auth.machine'", timeout=31
+    )
+    machine.wait_until_succeeds(
+        "journalctl -b -u prosody -o cat | grep -q 'Authenticated as jvb@auth.machine'", timeout=32
+    )
+    machine.wait_until_succeeds(
+        "journalctl -b -u prosody -o cat | grep -q 'Authenticated as jibri@auth.machine'", timeout=33
+    )
+    machine.wait_until_succeeds(
+        "cat /var/log/jitsi/jibri/log.0.txt | grep -q 'Joined MUC: jibribrewery@internal.machine'", timeout=34
+    )
+
+    assert '"busyStatus":"IDLE","health":{"healthStatus":"HEALTHY"' in machine.succeed(
+        "curl -X GET http://machine:2222/jibri/api/v1.0/health"
+    )
+    machine.succeed(
+        """curl -H "Content-Type: application/json" -X POST http://localhost:2222/jibri/api/v1.0/startService -d '{"sessionId": "RecordTest","callParams":{"callUrlInfo":{"baseUrl": "https://machine","callName": "TestCall"}},"callLoginParams":{"domain": "recorder.machine", "username": "recorder", "password": "'"$(cat /var/lib/jitsi-meet/jibri-recorder-secret)"'" },"sinkType": "file"}'"""
+    )
+    machine.wait_until_succeeds(
+        "cat /var/log/jitsi/jibri/log.0.txt | grep -q 'File recording service transitioning from state Starting up to Running'", timeout=35
+    )
+    machine.succeed(
+        """sleep 15 && curl -H "Content-Type: application/json" -X POST http://localhost:2222/jibri/api/v1.0/stopService -d '{"sessionId": "RecordTest","callParams":{"callUrlInfo":{"baseUrl": "https://machine","callName": "TestCall"}},"callLoginParams":{"domain": "recorder.machine", "username": "recorder", "password": "'"$(cat /var/lib/jitsi-meet/jibri-recorder-secret)"'" },"sinkType": "file"}'"""
+    )
+    machine.wait_until_succeeds(
+        "cat /var/log/jitsi/jibri/log.0.txt | grep -q 'Finalize script finished with exit value 0'", timeout=36
+    )
+  '';
+})
diff --git a/nixos/tests/jirafeau.nix b/nixos/tests/jirafeau.nix
new file mode 100644
index 00000000000..0f5af7f718a
--- /dev/null
+++ b/nixos/tests/jirafeau.nix
@@ -0,0 +1,22 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "jirafeau";
+  meta.maintainers = with maintainers; [ davidtwco ];
+
+  nodes.machine = { pkgs, ... }: {
+    services.jirafeau = {
+      enable = true;
+    };
+  };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("phpfpm-jirafeau.service")
+    machine.wait_for_unit("nginx.service")
+    machine.wait_for_open_port(80)
+    machine.succeed("curl -sSfL http://localhost/ | grep 'Jirafeau'")
+  '';
+})
diff --git a/nixos/tests/jitsi-meet.nix b/nixos/tests/jitsi-meet.nix
new file mode 100644
index 00000000000..d95f7c2ea9e
--- /dev/null
+++ b/nixos/tests/jitsi-meet.nix
@@ -0,0 +1,49 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "jitsi-meet";
+  meta = with pkgs.lib; {
+    maintainers = teams.jitsi.members;
+  };
+
+  nodes = {
+    client = { nodes, pkgs, ... }: {
+    };
+    server = { config, pkgs, ... }: {
+      services.jitsi-meet = {
+        enable = true;
+        hostName = "server";
+      };
+      services.jitsi-videobridge.openFirewall = true;
+
+      networking.firewall.allowedTCPPorts = [ 80 443 ];
+
+      services.nginx.virtualHosts.server = {
+        enableACME = true;
+        forceSSL = true;
+      };
+
+      security.acme.email = "me@example.org";
+      security.acme.acceptTerms = true;
+      security.acme.server = "https://example.com"; # self-signed only
+    };
+  };
+
+  testScript = ''
+    server.wait_for_unit("jitsi-videobridge2.service")
+    server.wait_for_unit("jicofo.service")
+    server.wait_for_unit("nginx.service")
+    server.wait_for_unit("prosody.service")
+
+    server.wait_until_succeeds(
+        "journalctl -b -u jitsi-videobridge2 -o cat | grep -q 'Performed a successful health check'"
+    )
+    server.wait_until_succeeds(
+        "journalctl -b -u prosody -o cat | grep -q 'Authenticated as focus@auth.server'"
+    )
+    server.wait_until_succeeds(
+        "journalctl -b -u prosody -o cat | grep -q 'Authenticated as jvb@auth.server'"
+    )
+
+    client.wait_for_unit("network.target")
+    assert "<title>Jitsi Meet</title>" in client.succeed("curl -sSfkL http://server/")
+  '';
+})
diff --git a/nixos/tests/k3s-single-node-docker.nix b/nixos/tests/k3s-single-node-docker.nix
new file mode 100644
index 00000000000..7f3d15788b0
--- /dev/null
+++ b/nixos/tests/k3s-single-node-docker.nix
@@ -0,0 +1,84 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+  let
+    imageEnv = pkgs.buildEnv {
+      name = "k3s-pause-image-env";
+      paths = with pkgs; [ tini (hiPrio coreutils) busybox ];
+    };
+    pauseImage = pkgs.dockerTools.streamLayeredImage {
+      name = "test.local/pause";
+      tag = "local";
+      contents = imageEnv;
+      config.Entrypoint = [ "/bin/tini" "--" "/bin/sleep" "inf" ];
+    };
+    # Don't use the default service account because there's a race where it may
+    # not be created yet; make our own instead.
+    testPodYaml = pkgs.writeText "test.yml" ''
+      apiVersion: v1
+      kind: ServiceAccount
+      metadata:
+        name: test
+      ---
+      apiVersion: v1
+      kind: Pod
+      metadata:
+        name: test
+      spec:
+        serviceAccountName: test
+        containers:
+        - name: test
+          image: test.local/pause:local
+          imagePullPolicy: Never
+          command: ["sh", "-c", "sleep inf"]
+    '';
+  in
+  {
+    name = "k3s";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ euank ];
+    };
+
+    machine = { pkgs, ... }: {
+      environment.systemPackages = with pkgs; [ k3s gzip ];
+
+      # k3s uses enough resources the default vm fails.
+      virtualisation.memorySize = 1536;
+      virtualisation.diskSize = 4096;
+
+      services.k3s = {
+        enable = true;
+        role = "server";
+        docker = true;
+        # Slightly reduce resource usage
+        extraFlags = "--no-deploy coredns,servicelb,traefik,local-storage,metrics-server --pause-image test.local/pause:local";
+      };
+
+      users.users = {
+        noprivs = {
+          isNormalUser = true;
+          description = "Can't access k3s by default";
+          password = "*";
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      machine.wait_for_unit("k3s")
+      machine.succeed("k3s kubectl cluster-info")
+      machine.fail("sudo -u noprivs k3s kubectl cluster-info")
+      # FIXME: this fails with the current nixos kernel config; once it passes, we should uncomment it
+      # machine.succeed("k3s check-config")
+
+      machine.succeed(
+          "${pauseImage} | docker load"
+      )
+
+      machine.succeed("k3s kubectl apply -f ${testPodYaml}")
+      machine.succeed("k3s kubectl wait --for 'condition=Ready' pod/test")
+      machine.succeed("k3s kubectl delete -f ${testPodYaml}")
+
+      machine.shutdown()
+    '';
+  })
diff --git a/nixos/tests/k3s-single-node.nix b/nixos/tests/k3s-single-node.nix
new file mode 100644
index 00000000000..d98f20d468c
--- /dev/null
+++ b/nixos/tests/k3s-single-node.nix
@@ -0,0 +1,82 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+  let
+    imageEnv = pkgs.buildEnv {
+      name = "k3s-pause-image-env";
+      paths = with pkgs; [ tini (hiPrio coreutils) busybox ];
+    };
+    pauseImage = pkgs.dockerTools.streamLayeredImage {
+      name = "test.local/pause";
+      tag = "local";
+      contents = imageEnv;
+      config.Entrypoint = [ "/bin/tini" "--" "/bin/sleep" "inf" ];
+    };
+    # Don't use the default service account because there's a race where it may
+    # not be created yet; make our own instead.
+    testPodYaml = pkgs.writeText "test.yml" ''
+      apiVersion: v1
+      kind: ServiceAccount
+      metadata:
+        name: test
+      ---
+      apiVersion: v1
+      kind: Pod
+      metadata:
+        name: test
+      spec:
+        serviceAccountName: test
+        containers:
+        - name: test
+          image: test.local/pause:local
+          imagePullPolicy: Never
+          command: ["sh", "-c", "sleep inf"]
+    '';
+  in
+  {
+    name = "k3s";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ euank ];
+    };
+
+    machine = { pkgs, ... }: {
+      environment.systemPackages = with pkgs; [ k3s gzip ];
+
+      # k3s uses enough resources the default vm fails.
+      virtualisation.memorySize = 1536;
+      virtualisation.diskSize = 4096;
+
+      services.k3s.enable = true;
+      services.k3s.role = "server";
+      services.k3s.package = pkgs.k3s;
+      # Slightly reduce resource usage
+      services.k3s.extraFlags = "--no-deploy coredns,servicelb,traefik,local-storage,metrics-server --pause-image test.local/pause:local";
+
+      users.users = {
+        noprivs = {
+          isNormalUser = true;
+          description = "Can't access k3s by default";
+          password = "*";
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      machine.wait_for_unit("k3s")
+      machine.succeed("k3s kubectl cluster-info")
+      machine.fail("sudo -u noprivs k3s kubectl cluster-info")
+      # FIXME: this fails with the current nixos kernel config; once it passes, we should uncomment it
+      # machine.succeed("k3s check-config")
+
+      machine.succeed(
+          "${pauseImage} | k3s ctr image import -"
+      )
+
+      machine.succeed("k3s kubectl apply -f ${testPodYaml}")
+      machine.succeed("k3s kubectl wait --for 'condition=Ready' pod/test")
+      machine.succeed("k3s kubectl delete -f ${testPodYaml}")
+
+      machine.shutdown()
+    '';
+  })
diff --git a/nixos/tests/kafka.nix b/nixos/tests/kafka.nix
new file mode 100644
index 00000000000..5def759ca24
--- /dev/null
+++ b/nixos/tests/kafka.nix
@@ -0,0 +1,79 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with pkgs.lib;
+
+let
+  makeKafkaTest = name: kafkaPackage: (import ./make-test-python.nix ({
+    inherit name;
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ nequissimus ];
+    };
+
+    nodes = {
+      zookeeper1 = { ... }: {
+        services.zookeeper = {
+          enable = true;
+        };
+
+        networking.firewall.allowedTCPPorts = [ 2181 ];
+      };
+      kafka = { ... }: {
+        services.apache-kafka = {
+          enable = true;
+          extraProperties = ''
+            offsets.topic.replication.factor = 1
+            zookeeper.session.timeout.ms = 600000
+          '';
+          package = kafkaPackage;
+          zookeeper = "zookeeper1:2181";
+        };
+
+        networking.firewall.allowedTCPPorts = [ 9092 ];
+        # i686 tests: qemu-system-i386 can simulate max 2047MB RAM (not 2048)
+        virtualisation.memorySize = 2047;
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      zookeeper1.wait_for_unit("default.target")
+      zookeeper1.wait_for_unit("zookeeper.service")
+      zookeeper1.wait_for_open_port(2181)
+
+      kafka.wait_for_unit("default.target")
+      kafka.wait_for_unit("apache-kafka.service")
+      kafka.wait_for_open_port(9092)
+
+      kafka.wait_until_succeeds(
+          "${kafkaPackage}/bin/kafka-topics.sh --create "
+          + "--zookeeper zookeeper1:2181 --partitions 1 "
+          + "--replication-factor 1 --topic testtopic"
+      )
+      kafka.succeed(
+          "echo 'test 1' | "
+          + "${kafkaPackage}/bin/kafka-console-producer.sh "
+          + "--broker-list localhost:9092 --topic testtopic"
+      )
+    '' + (if name == "kafka_0_9" then ''
+      assert "test 1" in kafka.succeed(
+          "${kafkaPackage}/bin/kafka-console-consumer.sh "
+          + "--zookeeper zookeeper1:2181 --topic testtopic "
+          + "--from-beginning --max-messages 1"
+      )
+    '' else ''
+      assert "test 1" in kafka.succeed(
+          "${kafkaPackage}/bin/kafka-console-consumer.sh "
+          + "--bootstrap-server localhost:9092 --topic testtopic "
+          + "--from-beginning --max-messages 1"
+      )
+    '');
+  }) { inherit system; });
+
+in with pkgs; {
+  kafka_2_7  = makeKafkaTest "kafka_2_7"  apacheKafka_2_7;
+  kafka_2_8  = makeKafkaTest "kafka_2_8"  apacheKafka_2_8;
+}
diff --git a/nixos/tests/kbd-setfont-decompress.nix b/nixos/tests/kbd-setfont-decompress.nix
new file mode 100644
index 00000000000..c3a495afac8
--- /dev/null
+++ b/nixos/tests/kbd-setfont-decompress.nix
@@ -0,0 +1,21 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+{
+  name = "kbd-setfont-decompress";
+
+  meta.maintainers = with lib.maintainers; [ oxalica ];
+
+  machine = { ... }: {};
+
+  testScript = ''
+    machine.succeed("gzip -cd ${pkgs.terminus_font}/share/consolefonts/ter-v16b.psf.gz >font.psf")
+    machine.succeed("gzip <font.psf >font.psf.gz")
+    machine.succeed("bzip2 <font.psf >font.psf.bz2")
+    machine.succeed("xz <font.psf >font.psf.xz")
+    machine.succeed("zstd <font.psf >font.psf.zst")
+    # setfont returns 0 even on error.
+    assert machine.succeed("PATH= ${pkgs.kbd}/bin/setfont font.psf.gz  2>&1") == ""
+    assert machine.succeed("PATH= ${pkgs.kbd}/bin/setfont font.psf.bz2 2>&1") == ""
+    assert machine.succeed("PATH= ${pkgs.kbd}/bin/setfont font.psf.xz  2>&1") == ""
+    assert machine.succeed("PATH= ${pkgs.kbd}/bin/setfont font.psf.zst 2>&1") == ""
+  '';
+})
diff --git a/nixos/tests/kbd-update-search-paths-patch.nix b/nixos/tests/kbd-update-search-paths-patch.nix
new file mode 100644
index 00000000000..2cdb12340b1
--- /dev/null
+++ b/nixos/tests/kbd-update-search-paths-patch.nix
@@ -0,0 +1,19 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "kbd-update-search-paths-patch";
+
+  machine = { pkgs, options, ... }: {
+    console = {
+      packages = options.console.packages.default ++ [ pkgs.terminus_font ];
+    };
+  };
+
+  testScript = ''
+    command = "${pkgs.kbd}/bin/setfont ter-112n 2>&1"
+    (status, out) = machine.execute(command)
+    import re
+    pattern = re.compile(r".*Unable to find file:.*")
+    match = pattern.match(out)
+    if match:
+        raise Exception("command `{}` failed".format(command))
+  '';
+})
diff --git a/nixos/tests/kea.nix b/nixos/tests/kea.nix
new file mode 100644
index 00000000000..6b345893108
--- /dev/null
+++ b/nixos/tests/kea.nix
@@ -0,0 +1,73 @@
+import ./make-test-python.nix ({ pkgs, lib, ...}: {
+  meta.maintainers = with lib.maintainers; [ hexa ];
+
+  nodes = {
+    router = { config, pkgs, ... }: {
+      virtualisation.vlans = [ 1 ];
+
+      networking = {
+        useNetworkd = true;
+        useDHCP = false;
+        firewall.allowedUDPPorts = [ 67 ];
+      };
+
+      systemd.network = {
+        networks = {
+          "01-eth1" = {
+            name = "eth1";
+            networkConfig = {
+              Address = "10.0.0.1/30";
+            };
+          };
+        };
+      };
+
+      services.kea.dhcp4 = {
+        enable = true;
+        settings = {
+          valid-lifetime = 3600;
+          renew-timer = 900;
+          rebind-timer = 1800;
+
+          lease-database = {
+            type = "memfile";
+            persist = true;
+            name = "/var/lib/kea/dhcp4.leases";
+          };
+
+          interfaces-config = {
+            dhcp-socket-type = "raw";
+            interfaces = [
+              "eth1"
+            ];
+          };
+
+          subnet4 = [ {
+            subnet = "10.0.0.0/30";
+            pools = [ {
+              pool = "10.0.0.2 - 10.0.0.2";
+            } ];
+          } ];
+        };
+      };
+    };
+
+    client = { config, pkgs, ... }: {
+      virtualisation.vlans = [ 1 ];
+      systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
+      networking = {
+        useNetworkd = true;
+        useDHCP = false;
+        firewall.enable = false;
+        interfaces.eth1.useDHCP = true;
+      };
+    };
+  };
+  testScript = { ... }: ''
+    start_all()
+    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")
+  '';
+})
diff --git a/nixos/tests/keepalived.nix b/nixos/tests/keepalived.nix
new file mode 100644
index 00000000000..d0bf9d46520
--- /dev/null
+++ b/nixos/tests/keepalived.nix
@@ -0,0 +1,42 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "keepalived";
+
+  nodes = {
+    node1 = { pkgs, ... }: {
+      networking.firewall.extraCommands = "iptables -A INPUT -p vrrp -j ACCEPT";
+      services.keepalived.enable = true;
+      services.keepalived.vrrpInstances.test = {
+        interface = "eth1";
+        state = "MASTER";
+        priority = 50;
+        virtualIps = [{ addr = "192.168.1.200"; }];
+        virtualRouterId = 1;
+      };
+      environment.systemPackages = [ pkgs.tcpdump ];
+    };
+    node2 = { pkgs, ... }: {
+      networking.firewall.extraCommands = "iptables -A INPUT -p vrrp -j ACCEPT";
+      services.keepalived.enable = true;
+      services.keepalived.vrrpInstances.test = {
+        interface = "eth1";
+        state = "MASTER";
+        priority = 100;
+        virtualIps = [{ addr = "192.168.1.200"; }];
+        virtualRouterId = 1;
+      };
+      environment.systemPackages = [ pkgs.tcpdump ];
+    };
+  };
+
+  testScript = ''
+    # wait for boot time delay to pass
+    for node in [node1, node2]:
+        node.wait_until_succeeds(
+            "systemctl show -p LastTriggerUSecMonotonic keepalived-boot-delay.timer | grep -vq 'LastTriggerUSecMonotonic=0'"
+        )
+        node.wait_for_unit("keepalived")
+    node2.wait_until_succeeds("ip addr show dev eth1 | grep -q 192.168.1.200")
+    node1.fail("ip addr show dev eth1 | grep -q 192.168.1.200")
+    node1.succeed("ping -c1 192.168.1.200")
+  '';
+})
diff --git a/nixos/tests/keepassxc.nix b/nixos/tests/keepassxc.nix
new file mode 100644
index 00000000000..685a200b318
--- /dev/null
+++ b/nixos/tests/keepassxc.nix
@@ -0,0 +1,34 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+{
+  name = "keepassxc";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ turion ];
+  };
+
+  machine = { ... }:
+
+  {
+    imports = [
+      ./common/user-account.nix
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    test-support.displayManager.auto.user = "alice";
+    environment.systemPackages = [ pkgs.keepassxc ];
+  };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }: ''
+    start_all()
+    machine.wait_for_x()
+
+    # start KeePassXC window
+    machine.execute("su - alice -c keepassxc >&2 &")
+
+    machine.wait_for_text("KeePassXC ${pkgs.keepassxc.version}")
+    machine.screenshot("KeePassXC")
+  '';
+})
diff --git a/nixos/tests/kerberos/default.nix b/nixos/tests/kerberos/default.nix
new file mode 100644
index 00000000000..f2f1a438918
--- /dev/null
+++ b/nixos/tests/kerberos/default.nix
@@ -0,0 +1,7 @@
+{ system ? builtins.currentSystem
+, pkgs ? import ../../.. { inherit system; }
+}:
+{
+  mit = import ./mit.nix { inherit system pkgs; };
+  heimdal = import ./heimdal.nix { inherit system pkgs; };
+}
diff --git a/nixos/tests/kerberos/heimdal.nix b/nixos/tests/kerberos/heimdal.nix
new file mode 100644
index 00000000000..391a61cc9a9
--- /dev/null
+++ b/nixos/tests/kerberos/heimdal.nix
@@ -0,0 +1,42 @@
+import ../make-test-python.nix ({pkgs, ...}: {
+  name = "kerberos_server-heimdal";
+  machine = { config, libs, pkgs, ...}:
+  { services.kerberos_server =
+    { enable = true;
+      realms = {
+        "FOO.BAR".acl = [{principal = "admin"; access = ["add" "cpw"];}];
+      };
+    };
+    krb5 = {
+      enable = true;
+      kerberos = pkgs.heimdal;
+      libdefaults = {
+        default_realm = "FOO.BAR";
+      };
+      realms = {
+        "FOO.BAR" = {
+          admin_server = "machine";
+          kdc = "machine";
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    machine.succeed(
+        "kadmin -l init --realm-max-ticket-life='8 day' --realm-max-renewable-life='10 day' FOO.BAR",
+        "systemctl restart kadmind.service kdc.service",
+    )
+
+    for unit in ["kadmind", "kdc", "kpasswdd"]:
+        machine.wait_for_unit(f"{unit}.service")
+
+    machine.succeed(
+        "kadmin -l add --password=admin_pw --use-defaults admin",
+        "kadmin -l ext_keytab --keytab=admin.keytab admin",
+        "kadmin -p admin -K admin.keytab add --password=alice_pw --use-defaults alice",
+        "kadmin -l ext_keytab --keytab=alice.keytab alice",
+        "kinit -kt alice.keytab alice",
+    )
+  '';
+})
diff --git a/nixos/tests/kerberos/mit.nix b/nixos/tests/kerberos/mit.nix
new file mode 100644
index 00000000000..93b4020d499
--- /dev/null
+++ b/nixos/tests/kerberos/mit.nix
@@ -0,0 +1,41 @@
+import ../make-test-python.nix ({pkgs, ...}: {
+  name = "kerberos_server-mit";
+  machine = { config, libs, pkgs, ...}:
+  { services.kerberos_server =
+    { enable = true;
+      realms = {
+        "FOO.BAR".acl = [{principal = "admin"; access = ["add" "cpw"];}];
+      };
+    };
+    krb5 = {
+      enable = true;
+      kerberos = pkgs.krb5Full;
+      libdefaults = {
+        default_realm = "FOO.BAR";
+      };
+      realms = {
+        "FOO.BAR" = {
+          admin_server = "machine";
+          kdc = "machine";
+        };
+      };
+    };
+    users.extraUsers.alice = { isNormalUser = true; };
+  };
+
+  testScript = ''
+    machine.succeed(
+        "kdb5_util create -s -r FOO.BAR -P master_key",
+        "systemctl restart kadmind.service kdc.service",
+    )
+
+    for unit in ["kadmind", "kdc"]:
+        machine.wait_for_unit(f"{unit}.service")
+
+    machine.succeed(
+        "kadmin.local add_principal -pw admin_pw admin",
+        "kadmin -p admin -w admin_pw addprinc -pw alice_pw alice",
+        "echo alice_pw | sudo -u alice kinit",
+    )
+  '';
+})
diff --git a/nixos/tests/kernel-generic.nix b/nixos/tests/kernel-generic.nix
new file mode 100644
index 00000000000..45c5c1963a0
--- /dev/null
+++ b/nixos/tests/kernel-generic.nix
@@ -0,0 +1,41 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
+}@args:
+
+with pkgs.lib;
+
+let
+  testsForLinuxPackages = linuxPackages: (import ./make-test-python.nix ({ pkgs, ... }: {
+    name = "kernel-${linuxPackages.kernel.version}";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ nequissimus atemu ];
+    };
+
+    machine = { ... }:
+      {
+        boot.kernelPackages = linuxPackages;
+      };
+
+    testScript =
+      ''
+        assert "Linux" in machine.succeed("uname -s")
+        assert "${linuxPackages.kernel.modDirVersion}" in machine.succeed("uname -a")
+      '';
+  }) args);
+  kernels = pkgs.linuxKernel.vanillaPackages // {
+    inherit (pkgs.linuxKernel.packages)
+      linux_4_14_hardened
+      linux_4_19_hardened
+      linux_5_4_hardened
+      linux_5_10_hardened
+      linux_5_15_hardened
+
+      linux_testing;
+  };
+
+in mapAttrs (_: lP: testsForLinuxPackages lP) kernels // {
+  inherit testsForLinuxPackages;
+
+  testsForKernel = kernel: testsForLinuxPackages (pkgs.linuxPackagesFor kernel);
+}
diff --git a/nixos/tests/kernel-latest-ath-user-regd.nix b/nixos/tests/kernel-latest-ath-user-regd.nix
new file mode 100644
index 00000000000..11a3959e692
--- /dev/null
+++ b/nixos/tests/kernel-latest-ath-user-regd.nix
@@ -0,0 +1,17 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "kernel-latest-ath-user-regd";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ veehaitch ];
+  };
+
+  machine = { pkgs, ... }:
+    {
+      boot.kernelPackages = pkgs.linuxPackages_latest;
+      networking.wireless.athUserRegulatoryDomain = true;
+    };
+
+  testScript =
+    ''
+      assert "CONFIG_ATH_USER_REGD=y" in machine.succeed("zcat /proc/config.gz")
+    '';
+})
diff --git a/nixos/tests/kexec.nix b/nixos/tests/kexec.nix
new file mode 100644
index 00000000000..010f3da4984
--- /dev/null
+++ b/nixos/tests/kexec.nix
@@ -0,0 +1,22 @@
+# Test whether fast reboots via kexec work.
+
+import ./make-test-python.nix ({ pkgs, lib, ...} : {
+  name = "kexec";
+  meta = with lib.maintainers; {
+    maintainers = [ eelco ];
+  };
+
+  machine = { ... }:
+    { virtualisation.vlans = [ ]; };
+
+  testScript =
+    ''
+      machine.wait_for_unit("multi-user.target")
+      machine.succeed('kexec --load /run/current-system/kernel --initrd /run/current-system/initrd --command-line "$(</proc/cmdline)"')
+      machine.execute("systemctl kexec >&2 &", check_return=False)
+      machine.connected = False
+      machine.connect()
+      machine.wait_for_unit("multi-user.target")
+      machine.shutdown()
+    '';
+})
diff --git a/nixos/tests/keycloak.nix b/nixos/tests/keycloak.nix
new file mode 100644
index 00000000000..6367ed808e0
--- /dev/null
+++ b/nixos/tests/keycloak.nix
@@ -0,0 +1,160 @@
+# This tests Keycloak: it starts the service, creates a realm with an
+# OIDC client and a user, and simulates the user logging in to the
+# client using their Keycloak login.
+
+let
+  certs = import ./common/acme/server/snakeoil-certs.nix;
+  frontendUrl = "https://${certs.domain}/auth";
+  initialAdminPassword = "h4IhoJFnt2iQIR9";
+
+  keycloakTest = import ./make-test-python.nix (
+    { pkgs, databaseType, ... }:
+    {
+      name = "keycloak";
+      meta = with pkgs.lib.maintainers; {
+        maintainers = [ talyz ];
+      };
+
+      nodes = {
+        keycloak = { ... }: {
+
+          security.pki.certificateFiles = [
+            certs.ca.cert
+          ];
+
+          networking.extraHosts = ''
+            127.0.0.1 ${certs.domain}
+          '';
+
+          services.keycloak = {
+            enable = true;
+            inherit frontendUrl initialAdminPassword;
+            sslCertificate = certs.${certs.domain}.cert;
+            sslCertificateKey = certs.${certs.domain}.key;
+            database = {
+              type = databaseType;
+              username = "bogus";
+              passwordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH";
+            };
+          };
+
+          environment.systemPackages = with pkgs; [
+            xmlstarlet
+            html-tidy
+            jq
+          ];
+        };
+      };
+
+      testScript =
+        let
+          client = {
+            clientId = "test-client";
+            name = "test-client";
+            redirectUris = [ "urn:ietf:wg:oauth:2.0:oob" ];
+          };
+
+          user = {
+            firstName = "Chuck";
+            lastName = "Testa";
+            username = "chuck.testa";
+            email = "chuck.testa@example.com";
+          };
+
+          password = "password1234";
+
+          realm = {
+            enabled = true;
+            realm = "test-realm";
+            clients = [ client ];
+            users = [(
+              user // {
+                enabled = true;
+                credentials = [{
+                  type = "password";
+                  temporary = false;
+                  value = password;
+                }];
+              }
+            )];
+          };
+
+          realmDataJson = pkgs.writeText "realm-data.json" (builtins.toJSON realm);
+
+          jqCheckUserinfo = pkgs.writeText "check-userinfo.jq" ''
+            if {
+              "firstName": .given_name,
+              "lastName": .family_name,
+              "username": .preferred_username,
+              "email": .email
+            } != ${builtins.toJSON user} then
+              error("Wrong user info!")
+            else
+              empty
+            end
+          '';
+        in ''
+          keycloak.start()
+          keycloak.wait_for_unit("keycloak.service")
+          keycloak.wait_until_succeeds("curl -sSf ${frontendUrl}")
+
+
+          ### Realm Setup ###
+
+          # Get an admin interface access token
+          keycloak.succeed(
+              "curl -sSf -d 'client_id=admin-cli' -d 'username=admin' -d 'password=${initialAdminPassword}' -d 'grant_type=password' '${frontendUrl}/realms/master/protocol/openid-connect/token' | jq -r '\"Authorization: bearer \" + .access_token' >admin_auth_header"
+          )
+
+          # Publish the realm, including a test OIDC client and user
+          keycloak.succeed(
+              "curl -sSf -H @admin_auth_header -X POST -H 'Content-Type: application/json' -d @${realmDataJson} '${frontendUrl}/admin/realms/'"
+          )
+
+          # Generate and save the client secret. To do this we need
+          # Keycloak's internal id for the client.
+          keycloak.succeed(
+              "curl -sSf -H @admin_auth_header '${frontendUrl}/admin/realms/${realm.realm}/clients?clientId=${client.name}' | jq -r '.[].id' >client_id",
+              "curl -sSf -H @admin_auth_header -X POST '${frontendUrl}/admin/realms/${realm.realm}/clients/'$(<client_id)'/client-secret' | jq -r .value >client_secret",
+          )
+
+
+          ### Authentication Testing ###
+
+          # Start the login process by sending an initial request to the
+          # OIDC authentication endpoint, saving the returned page. Tidy
+          # up the HTML (XmlStarlet is picky) and extract the login form
+          # post url.
+          keycloak.succeed(
+              "curl -sSf -c cookie '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/auth?client_id=${client.name}&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=openid+email&response_type=code&response_mode=query&nonce=qw4o89g3qqm' >login_form",
+              "tidy -q -m login_form || true",
+              "xml sel -T -t -m \"_:html/_:body/_:div/_:div/_:div/_:div/_:div/_:div/_:form[@id='kc-form-login']\" -v @action login_form >form_post_url",
+          )
+
+          # Post the login form and save the response. Once again tidy up
+          # the HTML, then extract the authorization code.
+          keycloak.succeed(
+              "curl -sSf -L -b cookie -d 'username=${user.username}' -d 'password=${password}' -d 'credentialId=' \"$(<form_post_url)\" >auth_code_html",
+              "tidy -q -m auth_code_html || true",
+              "xml sel -T -t -m \"_:html/_:body/_:div/_:div/_:div/_:div/_:div/_:input[@id='code']\" -v @value auth_code_html >auth_code",
+          )
+
+          # Exchange the authorization code for an access token.
+          keycloak.succeed(
+              "curl -sSf -d grant_type=authorization_code -d code=$(<auth_code) -d client_id=${client.name} -d client_secret=$(<client_secret) -d redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/token' | jq -r '\"Authorization: bearer \" + .access_token' >auth_header"
+          )
+
+          # Use the access token on the OIDC userinfo endpoint and check
+          # that the returned user info matches what we initialized the
+          # realm with.
+          keycloak.succeed(
+              "curl -sSf -H @auth_header '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/userinfo' | jq -f ${jqCheckUserinfo}"
+          )
+        '';
+    }
+  );
+in
+{
+  postgres = keycloakTest { databaseType = "postgresql"; };
+  mysql = keycloakTest { databaseType = "mysql"; };
+}
diff --git a/nixos/tests/keymap.nix b/nixos/tests/keymap.nix
new file mode 100644
index 00000000000..4306a9ae2cf
--- /dev/null
+++ b/nixos/tests/keymap.nix
@@ -0,0 +1,196 @@
+{ 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 = layout: { extraConfig ? {}, tests }: with pkgs.lib; makeTest {
+    name = "keymap-${layout}";
+
+    machine.console.keyMap = mkOverride 900 layout;
+    machine.services.xserver.desktopManager.xterm.enable = false;
+    machine.services.xserver.layout = mkOverride 900 layout;
+    machine.imports = [ ./common/x11.nix extraConfig ];
+
+    testScript = ''
+      import json
+      import shlex
+
+
+      def run_test_case(cmd, xorg_keymap, 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)),
+                  )
+              )
+
+              if xorg_keymap:
+                  # make sure the xterm window is open and has focus
+                  machine.wait_for_window("testterm")
+                  machine.wait_until_succeeds(
+                      "${pkgs.xdotool}/bin/xdotool search --sync --onlyvisible "
+                      "--class testterm windowfocus --sync"
+                  )
+
+              # wait for reader to be ready
+              machine.wait_for_file("${readyFile}")
+              machine.sleep(1)
+
+              # 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}")
+
+
+      with open("${pkgs.writeText "tests.json" (builtins.toJSON tests)}") as json_file:
+          tests = json.load(json_file)
+
+      keymap_environments = {
+          "VT Keymap": "openvt -sw --",
+          "Xorg Keymap": "DISPLAY=:0 xterm -title testterm -class testterm -fullscreen -e",
+      }
+
+      machine.wait_for_x()
+
+      for keymap_env_name, command in keymap_environments.items():
+          with subtest(keymap_env_name):
+              for test_case_name, test_data in tests.items():
+                  run_test_case(
+                      command,
+                      False,
+                      test_case_name,
+                      test_data["qwerty"],
+                      test_data["expect"],
+                  )
+    '';
+  };
+
+in pkgs.lib.mapAttrs mkKeyboardTest {
+  azerty = {
+    tests = {
+      azqw.qwerty = [ "q" "w" ];
+      azqw.expect = [ "a" "z" ];
+      altgr.qwerty = [ "alt_r-2" "alt_r-3" "alt_r-4" "alt_r-5" "alt_r-6" ];
+      altgr.expect = [ "~"       "#"       "{"       "["       "|"       ];
+    };
+
+    extraConfig.console.keyMap = "fr";
+    extraConfig.services.xserver.layout = "fr";
+  };
+
+  bone = {
+    tests = {
+      layer1.qwerty = [ "f"           "j"                     ];
+      layer1.expect = [ "e"           "n"                     ];
+      layer2.qwerty = [ "shift-f"     "shift-j"     "shift-6" ];
+      layer2.expect = [ "E"           "N"           "$"       ];
+      layer3.qwerty = [ "caps_lock-d" "caps_lock-f"           ];
+      layer3.expect = [ "{"           "}"                     ];
+    };
+
+    extraConfig.console.keyMap = "bone";
+    extraConfig.services.xserver.layout = "de";
+    extraConfig.services.xserver.xkbVariant = "bone";
+  };
+
+  colemak = {
+    tests = {
+      homerow.qwerty = [ "a" "s" "d" "f" "j" "k" "l" "semicolon" ];
+      homerow.expect = [ "a" "r" "s" "t" "n" "e" "i" "o"         ];
+    };
+
+    extraConfig.console.keyMap = "colemak";
+    extraConfig.services.xserver.layout = "us";
+    extraConfig.services.xserver.xkbVariant = "colemak";
+  };
+
+  dvorak = {
+    tests = {
+      homerow.qwerty = [ "a" "s" "d" "f" "j" "k" "l" "semicolon" ];
+      homerow.expect = [ "a" "o" "e" "u" "h" "t" "n" "s"         ];
+      symbols.qwerty = [ "q" "w" "e" "minus" "equal" ];
+      symbols.expect = [ "'" "," "." "["     "]"     ];
+    };
+
+    extraConfig.console.keyMap = "dvorak";
+    extraConfig.services.xserver.layout = "us";
+    extraConfig.services.xserver.xkbVariant = "dvorak";
+  };
+
+  dvorak-programmer = {
+    tests = {
+      homerow.qwerty = [ "a" "s" "d" "f" "j" "k" "l" "semicolon" ];
+      homerow.expect = [ "a" "o" "e" "u" "h" "t" "n" "s"         ];
+      numbers.qwerty = map (x: "shift-${x}")
+                       [ "1" "2" "3" "4" "5" "6" "7" "8" "9" "0" "minus" ];
+      numbers.expect = [ "%" "7" "5" "3" "1" "9" "0" "2" "4" "6" "8" ];
+      symbols.qwerty = [ "1" "2" "3" "4" "5" "6" "7" "8" "9" "0" "minus" ];
+      symbols.expect = [ "&" "[" "{" "}" "(" "=" "*" ")" "+" "]" "!" ];
+    };
+
+    extraConfig.console.keyMap = "dvorak-programmer";
+    extraConfig.services.xserver.layout = "us";
+    extraConfig.services.xserver.xkbVariant = "dvp";
+  };
+
+  neo = {
+    tests = {
+      layer1.qwerty = [ "f"           "j"                     ];
+      layer1.expect = [ "e"           "n"                     ];
+      layer2.qwerty = [ "shift-f"     "shift-j"     "shift-6" ];
+      layer2.expect = [ "E"           "N"           "$"       ];
+      layer3.qwerty = [ "caps_lock-d" "caps_lock-f"           ];
+      layer3.expect = [ "{"           "}"                     ];
+    };
+
+    extraConfig.console.keyMap = "neo";
+    extraConfig.services.xserver.layout = "de";
+    extraConfig.services.xserver.xkbVariant = "neo";
+  };
+
+  qwertz = {
+    tests = {
+      zy.qwerty = [ "z" "y" ];
+      zy.expect = [ "y" "z" ];
+      altgr.qwerty = map (x: "alt_r-${x}")
+                     [ "q" "less" "7" "8" "9" "0" ];
+      altgr.expect = [ "@" "|"    "{" "[" "]" "}" ];
+    };
+
+    extraConfig.console.keyMap = "de";
+    extraConfig.services.xserver.layout = "de";
+  };
+}
diff --git a/nixos/tests/knot.nix b/nixos/tests/knot.nix
new file mode 100644
index 00000000000..203fd03fac2
--- /dev/null
+++ b/nixos/tests/knot.nix
@@ -0,0 +1,216 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} :
+let
+  common = {
+    networking.firewall.enable = false;
+    networking.useDHCP = false;
+  };
+  exampleZone = pkgs.writeTextDir "example.com.zone" ''
+      @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800
+      @       NS      ns1
+      @       NS      ns2
+      ns1     A       192.168.0.1
+      ns1     AAAA    fd00::1
+      ns2     A       192.168.0.2
+      ns2     AAAA    fd00::2
+      www     A       192.0.2.1
+      www     AAAA    2001:DB8::1
+      sub     NS      ns.example.com.
+  '';
+  delegatedZone = pkgs.writeTextDir "sub.example.com.zone" ''
+      @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800
+      @       NS      ns1.example.com.
+      @       NS      ns2.example.com.
+      @       A       192.0.2.2
+      @       AAAA    2001:DB8::2
+  '';
+
+  knotZonesEnv = pkgs.buildEnv {
+    name = "knot-zones";
+    paths = [ exampleZone delegatedZone ];
+  };
+  # DO NOT USE pkgs.writeText IN PRODUCTION. This put secrets in the nix store!
+  tsigFile = pkgs.writeText "tsig.conf" ''
+    key:
+      - id: slave_key
+        algorithm: hmac-sha256
+        secret: zOYgOgnzx3TGe5J5I/0kxd7gTcxXhLYMEq3Ek3fY37s=
+  '';
+in {
+  name = "knot";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ hexa ];
+  };
+
+
+  nodes = {
+    master = { lib, ... }: {
+      imports = [ common ];
+
+      # trigger sched_setaffinity syscall
+      virtualisation.cores = 2;
+
+      networking.interfaces.eth1 = {
+        ipv4.addresses = lib.mkForce [
+          { address = "192.168.0.1"; prefixLength = 24; }
+        ];
+        ipv6.addresses = lib.mkForce [
+          { address = "fd00::1"; prefixLength = 64; }
+        ];
+      };
+      services.knot.enable = true;
+      services.knot.extraArgs = [ "-v" ];
+      services.knot.keyFiles = [ tsigFile ];
+      services.knot.extraConfig = ''
+        server:
+            listen: 0.0.0.0@53
+            listen: ::@53
+
+        acl:
+          - id: slave_acl
+            address: 192.168.0.2
+            key: slave_key
+            action: transfer
+
+        remote:
+          - id: slave
+            address: 192.168.0.2@53
+
+        template:
+          - id: default
+            storage: ${knotZonesEnv}
+            notify: [slave]
+            acl: [slave_acl]
+            dnssec-signing: on
+            # Input-only zone files
+            # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-3
+            # prevents modification of the zonefiles, since the zonefiles are immutable
+            zonefile-sync: -1
+            zonefile-load: difference
+            journal-content: changes
+            # move databases below the state directory, because they need to be writable
+            journal-db: /var/lib/knot/journal
+            kasp-db: /var/lib/knot/kasp
+            timer-db: /var/lib/knot/timer
+
+        zone:
+          - domain: example.com
+            file: example.com.zone
+
+          - domain: sub.example.com
+            file: sub.example.com.zone
+
+        log:
+          - target: syslog
+            any: info
+      '';
+    };
+
+    slave = { lib, ... }: {
+      imports = [ common ];
+      networking.interfaces.eth1 = {
+        ipv4.addresses = lib.mkForce [
+          { address = "192.168.0.2"; prefixLength = 24; }
+        ];
+        ipv6.addresses = lib.mkForce [
+          { address = "fd00::2"; prefixLength = 64; }
+        ];
+      };
+      services.knot.enable = true;
+      services.knot.keyFiles = [ tsigFile ];
+      services.knot.extraArgs = [ "-v" ];
+      services.knot.extraConfig = ''
+        server:
+            listen: 0.0.0.0@53
+            listen: ::@53
+
+        acl:
+          - id: notify_from_master
+            address: 192.168.0.1
+            action: notify
+
+        remote:
+          - id: master
+            address: 192.168.0.1@53
+            key: slave_key
+
+        template:
+          - id: default
+            master: master
+            acl: [notify_from_master]
+            # zonefileless setup
+            # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-2
+            zonefile-sync: -1
+            zonefile-load: none
+            journal-content: all
+            # move databases below the state directory, because they need to be writable
+            journal-db: /var/lib/knot/journal
+            kasp-db: /var/lib/knot/kasp
+            timer-db: /var/lib/knot/timer
+
+        zone:
+          - domain: example.com
+            file: example.com.zone
+
+          - domain: sub.example.com
+            file: sub.example.com.zone
+
+        log:
+          - target: syslog
+            any: info
+      '';
+    };
+    client = { lib, nodes, ... }: {
+      imports = [ common ];
+      networking.interfaces.eth1 = {
+        ipv4.addresses = [
+          { address = "192.168.0.3"; prefixLength = 24; }
+        ];
+        ipv6.addresses = [
+          { address = "fd00::3"; prefixLength = 64; }
+        ];
+      };
+      environment.systemPackages = [ pkgs.knot-dns ];
+    };
+  };
+
+  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;
+
+    slave4 = (lib.head nodes.slave.config.networking.interfaces.eth1.ipv4.addresses).address;
+    slave6 = (lib.head nodes.slave.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")
+
+
+    def test(host, query_type, query, pattern):
+        out = client.succeed(f"khost -t {query_type} {query} {host}").strip()
+        client.log(f"{host} replied with: {out}")
+        assert re.search(pattern, out), f'Did not match "{pattern}"'
+
+
+    for host in ("${master4}", "${master6}", "${slave4}", "${slave6}"):
+        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")
+            test(host, "AAAA", "example.com", r"has no [^ ]+ record")
+
+            test(host, "A", "www.example.com", r"address 192.0.2.1$")
+            test(host, "AAAA", "www.example.com", r"address 2001:db8::1$")
+
+            test(host, "NS", "sub.example.com", r"nameserver is ns\d\.example\.com.$")
+            test(host, "A", "sub.example.com", r"address 192.0.2.2$")
+            test(host, "AAAA", "sub.example.com", r"address 2001:db8::2$")
+
+            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 '✓'"))
+  '';
+})
diff --git a/nixos/tests/krb5/default.nix b/nixos/tests/krb5/default.nix
new file mode 100644
index 00000000000..dd5b2f37202
--- /dev/null
+++ b/nixos/tests/krb5/default.nix
@@ -0,0 +1,5 @@
+{ system ? builtins.currentSystem }:
+{
+  example-config = import ./example-config.nix { inherit system; };
+  deprecated-config = import ./deprecated-config.nix { inherit system; };
+}
diff --git a/nixos/tests/krb5/deprecated-config.nix b/nixos/tests/krb5/deprecated-config.nix
new file mode 100644
index 00000000000..9a9cafd4b13
--- /dev/null
+++ b/nixos/tests/krb5/deprecated-config.nix
@@ -0,0 +1,50 @@
+# Verifies that the configuration suggested in deprecated example values
+# will result in the expected output.
+
+import ../make-test-python.nix ({ pkgs, ...} : {
+  name = "krb5-with-deprecated-config";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eqyiel ];
+  };
+
+  machine =
+    { ... }: {
+      krb5 = {
+        enable = true;
+        defaultRealm = "ATHENA.MIT.EDU";
+        domainRealm = "athena.mit.edu";
+        kdc = "kerberos.mit.edu";
+        kerberosAdminServer = "kerberos.mit.edu";
+      };
+    };
+
+  testScript =
+    let snapshot = pkgs.writeText "krb5-with-deprecated-config.conf" ''
+      [libdefaults]
+        default_realm = ATHENA.MIT.EDU
+
+      [realms]
+        ATHENA.MIT.EDU = {
+          admin_server = kerberos.mit.edu
+          kdc = kerberos.mit.edu
+        }
+
+      [domain_realm]
+        .athena.mit.edu = ATHENA.MIT.EDU
+        athena.mit.edu = ATHENA.MIT.EDU
+
+      [capaths]
+
+
+      [appdefaults]
+
+
+      [plugins]
+
+    '';
+  in ''
+    machine.succeed(
+        "diff /etc/krb5.conf ${snapshot}"
+    )
+  '';
+})
diff --git a/nixos/tests/krb5/example-config.nix b/nixos/tests/krb5/example-config.nix
new file mode 100644
index 00000000000..0932c71dd97
--- /dev/null
+++ b/nixos/tests/krb5/example-config.nix
@@ -0,0 +1,112 @@
+# Verifies that the configuration suggested in (non-deprecated) example values
+# will result in the expected output.
+
+import ../make-test-python.nix ({ pkgs, ...} : {
+  name = "krb5-with-example-config";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eqyiel ];
+  };
+
+  machine =
+    { pkgs, ... }: {
+      krb5 = {
+        enable = true;
+        kerberos = pkgs.krb5Full;
+        libdefaults = {
+          default_realm = "ATHENA.MIT.EDU";
+        };
+        realms = {
+          "ATHENA.MIT.EDU" = {
+            admin_server = "athena.mit.edu";
+            kdc = [
+              "athena01.mit.edu"
+              "athena02.mit.edu"
+            ];
+          };
+        };
+        domain_realm = {
+          "example.com" = "EXAMPLE.COM";
+          ".example.com" = "EXAMPLE.COM";
+        };
+        capaths = {
+          "ATHENA.MIT.EDU" = {
+            "EXAMPLE.COM" = ".";
+          };
+          "EXAMPLE.COM" = {
+            "ATHENA.MIT.EDU" = ".";
+          };
+        };
+        appdefaults = {
+          pam = {
+            debug = false;
+            ticket_lifetime = 36000;
+            renew_lifetime = 36000;
+            max_timeout = 30;
+            timeout_shift = 2;
+            initial_timeout = 1;
+          };
+        };
+        plugins = {
+          ccselect = {
+            disable = "k5identity";
+          };
+        };
+        extraConfig = ''
+          [logging]
+            kdc          = SYSLOG:NOTICE
+            admin_server = SYSLOG:NOTICE
+            default      = SYSLOG:NOTICE
+        '';
+      };
+    };
+
+  testScript =
+    let snapshot = pkgs.writeText "krb5-with-example-config.conf" ''
+      [libdefaults]
+        default_realm = ATHENA.MIT.EDU
+
+      [realms]
+        ATHENA.MIT.EDU = {
+          admin_server = athena.mit.edu
+          kdc = athena01.mit.edu
+          kdc = athena02.mit.edu
+        }
+
+      [domain_realm]
+        .example.com = EXAMPLE.COM
+        example.com = EXAMPLE.COM
+
+      [capaths]
+        ATHENA.MIT.EDU = {
+          EXAMPLE.COM = .
+        }
+        EXAMPLE.COM = {
+          ATHENA.MIT.EDU = .
+        }
+
+      [appdefaults]
+        pam = {
+          debug = false
+          initial_timeout = 1
+          max_timeout = 30
+          renew_lifetime = 36000
+          ticket_lifetime = 36000
+          timeout_shift = 2
+        }
+
+      [plugins]
+        ccselect = {
+          disable = k5identity
+        }
+
+      [logging]
+        kdc          = SYSLOG:NOTICE
+        admin_server = SYSLOG:NOTICE
+        default      = SYSLOG:NOTICE
+    '';
+  in ''
+    machine.succeed(
+        "diff /etc/krb5.conf ${snapshot}"
+    )
+  '';
+})
diff --git a/nixos/tests/ksm.nix b/nixos/tests/ksm.nix
new file mode 100644
index 00000000000..8f84b32020a
--- /dev/null
+++ b/nixos/tests/ksm.nix
@@ -0,0 +1,22 @@
+import ./make-test-python.nix ({ lib, ...} :
+
+{
+  name = "ksm";
+  meta = with lib.maintainers; {
+    maintainers = [ rnhmjoj ];
+  };
+
+  machine = { ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+
+    hardware.ksm.enable = true;
+    hardware.ksm.sleep = 300;
+  };
+
+  testScript =
+    ''
+      machine.start()
+      machine.wait_until_succeeds("test $(</sys/kernel/mm/ksm/run) -eq 1")
+      machine.wait_until_succeeds("test $(</sys/kernel/mm/ksm/sleep_millisecs) -eq 300")
+    '';
+})
diff --git a/nixos/tests/kubernetes/base.nix b/nixos/tests/kubernetes/base.nix
new file mode 100644
index 00000000000..d4410beb937
--- /dev/null
+++ b/nixos/tests/kubernetes/base.nix
@@ -0,0 +1,107 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../../.. { inherit system config; }
+}:
+
+with import ../../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  mkKubernetesBaseTest =
+    { name, domain ? "my.zyx", test, machines
+    , extraConfiguration ? null }:
+    let
+      masterName = head (filter (machineName: any (role: role == "master") machines.${machineName}.roles) (attrNames machines));
+      master = machines.${masterName};
+      extraHosts = ''
+        ${master.ip}  etcd.${domain}
+        ${master.ip}  api.${domain}
+        ${concatMapStringsSep "\n" (machineName: "${machines.${machineName}.ip}  ${machineName}.${domain}") (attrNames machines)}
+      '';
+      wrapKubectl = with pkgs; runCommand "wrap-kubectl" { buildInputs = [ makeWrapper ]; } ''
+        mkdir -p $out/bin
+        makeWrapper ${pkgs.kubernetes}/bin/kubectl $out/bin/kubectl --set KUBECONFIG "/etc/kubernetes/cluster-admin.kubeconfig"
+      '';
+    in makeTest {
+      inherit name;
+
+      nodes = mapAttrs (machineName: machine:
+        { config, pkgs, lib, nodes, ... }:
+          mkMerge [
+            {
+              boot.postBootCommands = "rm -fr /var/lib/kubernetes/secrets /tmp/shared/*";
+              virtualisation.memorySize = mkDefault 1536;
+              virtualisation.diskSize = mkDefault 4096;
+              networking = {
+                inherit domain extraHosts;
+                primaryIPAddress = mkForce machine.ip;
+
+                firewall = {
+                  allowedTCPPorts = [
+                    10250 # kubelet
+                  ];
+                  trustedInterfaces = ["mynet"];
+
+                  extraCommands = concatMapStrings  (node: ''
+                    iptables -A INPUT -s ${node.config.networking.primaryIPAddress} -j ACCEPT
+                  '') (attrValues nodes);
+                };
+              };
+              programs.bash.enableCompletion = true;
+              environment.systemPackages = [ wrapKubectl ];
+              services.flannel.iface = "eth1";
+              services.kubernetes = {
+                proxy.hostname = "${masterName}.${domain}";
+
+                easyCerts = true;
+                inherit (machine) roles;
+                apiserver = {
+                  securePort = 443;
+                  advertiseAddress = master.ip;
+                };
+                masterAddress = "${masterName}.${config.networking.domain}";
+              };
+            }
+            (optionalAttrs (any (role: role == "master") machine.roles) {
+              networking.firewall.allowedTCPPorts = [
+                443 # kubernetes apiserver
+              ];
+            })
+            (optionalAttrs (machine ? extraConfiguration) (machine.extraConfiguration { inherit config pkgs lib nodes; }))
+            (optionalAttrs (extraConfiguration != null) (extraConfiguration { inherit config pkgs lib nodes; }))
+          ]
+      ) machines;
+
+      testScript = ''
+        start_all()
+      '' + test;
+    };
+
+  mkKubernetesMultiNodeTest = attrs: mkKubernetesBaseTest ({
+    machines = {
+      machine1 = {
+        roles = ["master"];
+        ip = "192.168.1.1";
+      };
+      machine2 = {
+        roles = ["node"];
+        ip = "192.168.1.2";
+      };
+    };
+  } // attrs // {
+    name = "kubernetes-${attrs.name}-multinode";
+  });
+
+  mkKubernetesSingleNodeTest = attrs: mkKubernetesBaseTest ({
+    machines = {
+      machine1 = {
+        roles = ["master" "node"];
+        ip = "192.168.1.1";
+      };
+    };
+  } // attrs // {
+    name = "kubernetes-${attrs.name}-singlenode";
+  });
+in {
+  inherit mkKubernetesBaseTest mkKubernetesSingleNodeTest mkKubernetesMultiNodeTest;
+}
diff --git a/nixos/tests/kubernetes/default.nix b/nixos/tests/kubernetes/default.nix
new file mode 100644
index 00000000000..60ba482758f
--- /dev/null
+++ b/nixos/tests/kubernetes/default.nix
@@ -0,0 +1,15 @@
+{ system ? builtins.currentSystem
+, pkgs ? import ../../.. { inherit system; }
+}:
+let
+  dns = import ./dns.nix { inherit system pkgs; };
+  rbac = import ./rbac.nix { inherit system pkgs; };
+  # TODO kubernetes.e2e should eventually replace kubernetes.rbac when it works
+  # e2e = import ./e2e.nix { inherit system pkgs; };
+in
+{
+  dns-single-node = dns.singlenode.test;
+  dns-multi-node = dns.multinode.test;
+  rbac-single-node = rbac.singlenode.test;
+  rbac-multi-node = rbac.multinode.test;
+}
diff --git a/nixos/tests/kubernetes/dns.nix b/nixos/tests/kubernetes/dns.nix
new file mode 100644
index 00000000000..3fd1dd31f74
--- /dev/null
+++ b/nixos/tests/kubernetes/dns.nix
@@ -0,0 +1,151 @@
+{ system ? builtins.currentSystem, pkgs ? import ../../.. { inherit system; } }:
+with import ./base.nix { inherit system; };
+let
+  domain = "my.zyx";
+
+  redisPod = pkgs.writeText "redis-pod.json" (builtins.toJSON {
+    kind = "Pod";
+    apiVersion = "v1";
+    metadata.name = "redis";
+    metadata.labels.name = "redis";
+    spec.containers = [{
+      name = "redis";
+      image = "redis";
+      args = ["--bind" "0.0.0.0"];
+      imagePullPolicy = "Never";
+      ports = [{
+        name = "redis-server";
+        containerPort = 6379;
+      }];
+    }];
+  });
+
+  redisService = pkgs.writeText "redis-service.json" (builtins.toJSON {
+    kind = "Service";
+    apiVersion = "v1";
+    metadata.name = "redis";
+    spec = {
+      ports = [{port = 6379; targetPort = 6379;}];
+      selector = {name = "redis";};
+    };
+  });
+
+  redisImage = pkgs.dockerTools.buildImage {
+    name = "redis";
+    tag = "latest";
+    contents = [ pkgs.redis pkgs.bind.host ];
+    config.Entrypoint = ["/bin/redis-server"];
+  };
+
+  probePod = pkgs.writeText "probe-pod.json" (builtins.toJSON {
+    kind = "Pod";
+    apiVersion = "v1";
+    metadata.name = "probe";
+    metadata.labels.name = "probe";
+    spec.containers = [{
+      name = "probe";
+      image = "probe";
+      args = [ "-f" ];
+      tty = true;
+      imagePullPolicy = "Never";
+    }];
+  });
+
+  probeImage = pkgs.dockerTools.buildImage {
+    name = "probe";
+    tag = "latest";
+    contents = [ pkgs.bind.host pkgs.busybox ];
+    config.Entrypoint = ["/bin/tail"];
+  };
+
+  extraConfiguration = { config, pkgs, lib, ... }: {
+    environment.systemPackages = [ pkgs.bind.host ];
+    services.dnsmasq.enable = true;
+    services.dnsmasq.servers = [
+      "/cluster.local/${config.services.kubernetes.addons.dns.clusterIp}#53"
+    ];
+  };
+
+  base = {
+    name = "dns";
+    inherit domain extraConfiguration;
+  };
+
+  singleNodeTest = {
+    test = ''
+      # prepare machine1 for test
+      machine1.wait_until_succeeds("kubectl get node machine1.${domain} | grep -w Ready")
+      machine1.wait_until_succeeds(
+          "${pkgs.gzip}/bin/zcat ${redisImage} | ${pkgs.containerd}/bin/ctr -n k8s.io image import -"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${redisPod}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${redisService}"
+      )
+      machine1.wait_until_succeeds(
+          "${pkgs.gzip}/bin/zcat ${probeImage} | ${pkgs.containerd}/bin/ctr -n k8s.io image import -"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${probePod}"
+      )
+
+      # check if pods are running
+      machine1.wait_until_succeeds("kubectl get pod redis | grep Running")
+      machine1.wait_until_succeeds("kubectl get pod probe | grep Running")
+      machine1.wait_until_succeeds("kubectl get pods -n kube-system | grep 'coredns.*1/1'")
+
+      # check dns on host (dnsmasq)
+      machine1.succeed("host redis.default.svc.cluster.local")
+
+      # check dns inside the container
+      machine1.succeed("kubectl exec probe -- /bin/host redis.default.svc.cluster.local")
+    '';
+  };
+
+  multiNodeTest = {
+    test = ''
+      # Node token exchange
+      machine1.wait_until_succeeds(
+          "cp -f /var/lib/cfssl/apitoken.secret /tmp/shared/apitoken.secret"
+      )
+      machine2.wait_until_succeeds(
+          "cat /tmp/shared/apitoken.secret | nixos-kubernetes-node-join"
+      )
+
+      # prepare machines for test
+      machine1.wait_until_succeeds("kubectl get node machine2.${domain} | grep -w Ready")
+      machine2.wait_until_succeeds(
+          "${pkgs.gzip}/bin/zcat ${redisImage} | ${pkgs.containerd}/bin/ctr -n k8s.io image import -"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${redisPod}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${redisService}"
+      )
+      machine2.wait_until_succeeds(
+          "${pkgs.gzip}/bin/zcat ${probeImage} | ${pkgs.containerd}/bin/ctr -n k8s.io image import -"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${probePod}"
+      )
+
+      # check if pods are running
+      machine1.wait_until_succeeds("kubectl get pod redis | grep Running")
+      machine1.wait_until_succeeds("kubectl get pod probe | grep Running")
+      machine1.wait_until_succeeds("kubectl get pods -n kube-system | grep 'coredns.*1/1'")
+
+      # check dns on hosts (dnsmasq)
+      machine1.succeed("host redis.default.svc.cluster.local")
+      machine2.succeed("host redis.default.svc.cluster.local")
+
+      # check dns inside the container
+      machine1.succeed("kubectl exec probe -- /bin/host redis.default.svc.cluster.local")
+    '';
+  };
+in {
+  singlenode = mkKubernetesSingleNodeTest (base // singleNodeTest);
+  multinode = mkKubernetesMultiNodeTest (base // multiNodeTest);
+}
diff --git a/nixos/tests/kubernetes/e2e.nix b/nixos/tests/kubernetes/e2e.nix
new file mode 100644
index 00000000000..fb29d9cc695
--- /dev/null
+++ b/nixos/tests/kubernetes/e2e.nix
@@ -0,0 +1,40 @@
+{ system ? builtins.currentSystem, pkgs ? import ../../.. { inherit system; } }:
+with import ./base.nix { inherit system; };
+let
+  domain = "my.zyx";
+  certs = import ./certs.nix { externalDomain = domain; kubelets = ["machine1" "machine2"]; };
+  kubeconfig = pkgs.writeText "kubeconfig.json" (builtins.toJSON {
+    apiVersion = "v1";
+    kind = "Config";
+    clusters = [{
+      name = "local";
+      cluster.certificate-authority = "${certs.master}/ca.pem";
+      cluster.server = "https://api.${domain}";
+    }];
+    users = [{
+      name = "kubelet";
+      user = {
+        client-certificate = "${certs.admin}/admin.pem";
+        client-key = "${certs.admin}/admin-key.pem";
+      };
+    }];
+    contexts = [{
+      context = {
+        cluster = "local";
+        user = "kubelet";
+      };
+      current-context = "kubelet-context";
+    }];
+  });
+
+  base = {
+    name = "e2e";
+    inherit domain certs;
+    test = ''
+      $machine1->succeed("e2e.test -kubeconfig ${kubeconfig} -provider local -ginkgo.focus '\\[Conformance\\]' -ginkgo.skip '\\[Flaky\\]|\\[Serial\\]'");
+    '';
+  };
+in {
+  singlenode = mkKubernetesSingleNodeTest base;
+  multinode = mkKubernetesMultiNodeTest base;
+}
diff --git a/nixos/tests/kubernetes/rbac.nix b/nixos/tests/kubernetes/rbac.nix
new file mode 100644
index 00000000000..9e73fbbd32a
--- /dev/null
+++ b/nixos/tests/kubernetes/rbac.nix
@@ -0,0 +1,164 @@
+{ system ? builtins.currentSystem, pkgs ? import ../../.. { inherit system; } }:
+with import ./base.nix { inherit system; };
+let
+
+  roServiceAccount = pkgs.writeText "ro-service-account.json" (builtins.toJSON {
+    kind = "ServiceAccount";
+    apiVersion = "v1";
+    metadata = {
+      name = "read-only";
+      namespace = "default";
+    };
+  });
+
+  roRoleBinding = pkgs.writeText "ro-role-binding.json" (builtins.toJSON {
+    apiVersion = "rbac.authorization.k8s.io/v1";
+    kind = "RoleBinding";
+    metadata = {
+      name = "read-pods";
+      namespace = "default";
+    };
+    roleRef = {
+      apiGroup = "rbac.authorization.k8s.io";
+      kind = "Role";
+      name = "pod-reader";
+    };
+    subjects = [{
+      kind = "ServiceAccount";
+      name = "read-only";
+      namespace = "default";
+    }];
+  });
+
+  roRole = pkgs.writeText "ro-role.json" (builtins.toJSON {
+    apiVersion = "rbac.authorization.k8s.io/v1";
+    kind = "Role";
+    metadata = {
+      name = "pod-reader";
+      namespace = "default";
+    };
+    rules = [{
+      apiGroups = [""];
+      resources = ["pods"];
+      verbs = ["get" "list" "watch"];
+    }];
+  });
+
+  kubectlPod = pkgs.writeText "kubectl-pod.json" (builtins.toJSON {
+    kind = "Pod";
+    apiVersion = "v1";
+    metadata.name = "kubectl";
+    metadata.namespace = "default";
+    metadata.labels.name = "kubectl";
+    spec.serviceAccountName = "read-only";
+    spec.containers = [{
+      name = "kubectl";
+      image = "kubectl:latest";
+      command = ["/bin/tail" "-f"];
+      imagePullPolicy = "Never";
+      tty = true;
+    }];
+  });
+
+  kubectlPod2 = pkgs.writeTextDir "kubectl-pod-2.json" (builtins.toJSON {
+    kind = "Pod";
+    apiVersion = "v1";
+    metadata.name = "kubectl-2";
+    metadata.namespace = "default";
+    metadata.labels.name = "kubectl-2";
+    spec.serviceAccountName = "read-only";
+    spec.containers = [{
+      name = "kubectl-2";
+      image = "kubectl:latest";
+      command = ["/bin/tail" "-f"];
+      imagePullPolicy = "Never";
+      tty = true;
+    }];
+  });
+
+  copyKubectl = pkgs.runCommand "copy-kubectl" { } ''
+    mkdir -p $out/bin
+    cp ${pkgs.kubernetes}/bin/kubectl $out/bin/kubectl
+  '';
+
+  kubectlImage = pkgs.dockerTools.buildImage {
+    name = "kubectl";
+    tag = "latest";
+    contents = [ copyKubectl pkgs.busybox kubectlPod2 ];
+    config.Entrypoint = ["/bin/sh"];
+  };
+
+  base = {
+    name = "rbac";
+  };
+
+  singlenode = base // {
+    test = ''
+      machine1.wait_until_succeeds("kubectl get node machine1.my.zyx | grep -w Ready")
+
+      machine1.wait_until_succeeds(
+          "${pkgs.gzip}/bin/zcat ${kubectlImage} | ${pkgs.containerd}/bin/ctr -n k8s.io image import -"
+      )
+
+      machine1.wait_until_succeeds(
+          "kubectl apply -f ${roServiceAccount}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl apply -f ${roRole}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl apply -f ${roRoleBinding}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${kubectlPod}"
+      )
+
+      machine1.wait_until_succeeds("kubectl get pod kubectl | grep Running")
+
+      machine1.wait_until_succeeds("kubectl exec kubectl -- kubectl get pods")
+      machine1.fail("kubectl exec kubectl -- kubectl create -f /kubectl-pod-2.json")
+      machine1.fail("kubectl exec kubectl -- kubectl delete pods -l name=kubectl")
+    '';
+  };
+
+  multinode = base // {
+    test = ''
+      # Node token exchange
+      machine1.wait_until_succeeds(
+          "cp -f /var/lib/cfssl/apitoken.secret /tmp/shared/apitoken.secret"
+      )
+      machine2.wait_until_succeeds(
+          "cat /tmp/shared/apitoken.secret | nixos-kubernetes-node-join"
+      )
+
+      machine1.wait_until_succeeds("kubectl get node machine2.my.zyx | grep -w Ready")
+
+      machine2.wait_until_succeeds(
+          "${pkgs.gzip}/bin/zcat ${kubectlImage} | ${pkgs.containerd}/bin/ctr -n k8s.io image import -"
+      )
+
+      machine1.wait_until_succeeds(
+          "kubectl apply -f ${roServiceAccount}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl apply -f ${roRole}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl apply -f ${roRoleBinding}"
+      )
+      machine1.wait_until_succeeds(
+          "kubectl create -f ${kubectlPod}"
+      )
+
+      machine1.wait_until_succeeds("kubectl get pod kubectl | grep Running")
+
+      machine1.wait_until_succeeds("kubectl exec kubectl -- kubectl get pods")
+      machine1.fail("kubectl exec kubectl -- kubectl create -f /kubectl-pod-2.json")
+      machine1.fail("kubectl exec kubectl -- kubectl delete pods -l name=kubectl")
+    '';
+  };
+
+in {
+  singlenode = mkKubernetesSingleNodeTest singlenode;
+  multinode = mkKubernetesMultiNodeTest multinode;
+}
diff --git a/nixos/tests/leaps.nix b/nixos/tests/leaps.nix
new file mode 100644
index 00000000000..5cc387c86a4
--- /dev/null
+++ b/nixos/tests/leaps.nix
@@ -0,0 +1,32 @@
+import ./make-test-python.nix ({ pkgs,  ... }:
+
+{
+  name = "leaps";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ qknight ];
+  };
+
+  nodes =
+    {
+      client = { };
+
+      server =
+        { services.leaps = {
+            enable = true;
+            port = 6666;
+            path = "/leaps/";
+          };
+          networking.firewall.enable = false;
+        };
+    };
+
+  testScript =
+    ''
+      start_all()
+      server.wait_for_open_port(6666)
+      client.wait_for_unit("network.target")
+      assert "leaps" in client.succeed(
+          "${pkgs.curl}/bin/curl -f http://server:6666/leaps/"
+      )
+    '';
+})
diff --git a/nixos/tests/libinput.nix b/nixos/tests/libinput.nix
new file mode 100644
index 00000000000..2f84aaadcd0
--- /dev/null
+++ b/nixos/tests/libinput.nix
@@ -0,0 +1,38 @@
+import ./make-test-python.nix ({ ... }:
+
+{
+  name = "libinput";
+
+  machine = { ... }:
+    {
+      imports = [
+        ./common/x11.nix
+        ./common/user-account.nix
+      ];
+
+      test-support.displayManager.auto.user = "alice";
+
+      services.xserver.libinput = {
+        enable = true;
+        mouse = {
+          naturalScrolling = true;
+          leftHanded = true;
+          middleEmulation = false;
+          horizontalScrolling = false;
+        };
+      };
+    };
+
+  testScript = ''
+    def expect_xserver_option(option, value):
+        machine.succeed(f"""cat /var/log/X.0.log | grep -F 'Option "{option}" "{value}"'""")
+
+    machine.start()
+    machine.wait_for_x()
+    machine.succeed("""cat /var/log/X.0.log | grep -F "Using input driver 'libinput'" """)
+    expect_xserver_option("NaturalScrolling", "on")
+    expect_xserver_option("LeftHanded", "on")
+    expect_xserver_option("MiddleEmulation", "off")
+    expect_xserver_option("HorizontalScrolling", "off")
+  '';
+})
diff --git a/nixos/tests/libreddit.nix b/nixos/tests/libreddit.nix
new file mode 100644
index 00000000000..f7ef701d086
--- /dev/null
+++ b/nixos/tests/libreddit.nix
@@ -0,0 +1,19 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "libreddit";
+  meta.maintainers = with maintainers; [ fab ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    { services.libreddit.enable = true; };
+
+  testScript = ''
+    machine.wait_for_unit("libreddit.service")
+    machine.wait_for_open_port("8080")
+    # The service wants to get data from https://www.reddit.com
+    machine.succeed("curl http://localhost:8080/")
+  '';
+})
diff --git a/nixos/tests/libresprite.nix b/nixos/tests/libresprite.nix
new file mode 100644
index 00000000000..1a6210e3671
--- /dev/null
+++ b/nixos/tests/libresprite.nix
@@ -0,0 +1,30 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "libresprite";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fgaz ];
+  };
+
+  machine = { config, pkgs, ... }: {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    environment.systemPackages = [
+      pkgs.imagemagick
+      pkgs.libresprite
+    ];
+  };
+
+  enableOCR = true;
+
+  testScript =
+    ''
+      machine.wait_for_x()
+      machine.succeed("convert -font DejaVu-Sans +antialias label:'IT WORKS' image.png")
+      machine.execute("libresprite image.png >&2 &")
+      machine.wait_for_window("LibreSprite v${pkgs.libresprite.version}")
+      machine.wait_for_text("IT WORKS")
+      machine.screenshot("screen")
+    '';
+})
diff --git a/nixos/tests/libreswan.nix b/nixos/tests/libreswan.nix
new file mode 100644
index 00000000000..ff3d2344a67
--- /dev/null
+++ b/nixos/tests/libreswan.nix
@@ -0,0 +1,134 @@
+# This test sets up a host-to-host IPsec VPN between Alice and Bob, each on its
+# own network and with Eve as the only route between each other. We check that
+# Eve can eavesdrop the plaintext traffic between Alice and Bob, but once they
+# enable the secure tunnel Eve's spying becomes ineffective.
+
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+
+let
+
+  # IPsec tunnel between Alice and Bob
+  tunnelConfig = {
+    services.libreswan.enable = true;
+    services.libreswan.connections.tunnel =
+      ''
+        leftid=@alice
+        left=fd::a
+        rightid=@bob
+        right=fd::b
+        authby=secret
+        auto=add
+      '';
+    environment.etc."ipsec.d/tunnel.secrets" =
+      { text = ''@alice @bob : PSK "j1JbIi9WY07rxwcNQ6nbyThKCf9DGxWOyokXIQcAQUnafsNTUJxfsxwk9WYK8fHj"'';
+        mode = "600";
+      };
+  };
+
+  # Common network setup
+  baseNetwork = {
+    # shared hosts file
+    extraHosts = lib.mkVMOverride ''
+      fd::a alice
+      fd::b bob
+      fd::e eve
+    '';
+    # remove all automatic addresses
+    useDHCP = false;
+    interfaces.eth1.ipv4.addresses = lib.mkVMOverride [];
+    interfaces.eth2.ipv4.addresses = lib.mkVMOverride [];
+    # open a port for testing
+    firewall.allowedUDPPorts = [ 1234 ];
+  };
+
+  # Adds an address and route from a to b via Eve
+  addRoute = a: b: {
+    interfaces.eth1.ipv6.addresses =
+      [ { address = a; prefixLength = 64; } ];
+    interfaces.eth1.ipv6.routes =
+      [ { address = b; prefixLength = 128; via = "fd::e"; } ];
+  };
+
+in
+
+{
+  name = "libreswan";
+  meta = with lib.maintainers; {
+    maintainers = [ rnhmjoj ];
+  };
+
+  # Our protagonist
+  nodes.alice = { ... }: {
+    virtualisation.vlans = [ 1 ];
+    networking = baseNetwork // addRoute "fd::a" "fd::b";
+  } // tunnelConfig;
+
+  # Her best friend
+  nodes.bob = { ... }: {
+    virtualisation.vlans = [ 2 ];
+    networking = baseNetwork // addRoute "fd::b" "fd::a";
+  } // tunnelConfig;
+
+  # The malicious network operator
+  nodes.eve = { ... }: {
+    virtualisation.vlans = [ 1 2 ];
+    networking = lib.mkMerge
+      [ baseNetwork
+        { interfaces.br0.ipv6.addresses =
+            [ { address = "fd::e"; prefixLength = 64; } ];
+          bridges.br0.interfaces = [ "eth1" "eth2" ];
+        }
+      ];
+    environment.systemPackages = [ pkgs.tcpdump ];
+    boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = true;
+  };
+
+  testScript =
+    ''
+      def alice_to_bob(msg: str):
+          """
+          Sends a message as Alice to Bob
+          """
+          bob.execute("nc -lu ::0 1234 >/tmp/msg &")
+          alice.sleep(1)
+          alice.succeed(f"echo '{msg}' | nc -uw 0 bob 1234")
+          bob.succeed(f"grep '{msg}' /tmp/msg")
+
+
+      def eavesdrop():
+          """
+          Starts eavesdropping on Alice and Bob
+          """
+          match = "src host alice and dst host bob"
+          eve.execute(f"tcpdump -i br0 -c 1 -Avv {match} >/tmp/log &")
+
+
+      start_all()
+
+      with subtest("Network is up"):
+          alice.wait_until_succeeds("ping -c1 bob")
+
+      with subtest("Eve can eavesdrop cleartext traffic"):
+          eavesdrop()
+          alice_to_bob("I secretly love turnip")
+          eve.sleep(1)
+          eve.succeed("grep turnip /tmp/log")
+
+      with subtest("Libreswan is ready"):
+          alice.wait_for_unit("ipsec")
+          bob.wait_for_unit("ipsec")
+          alice.succeed("ipsec verify 1>&2")
+
+      with subtest("Alice and Bob can start the tunnel"):
+          alice.execute("ipsec auto --start tunnel >&2 &")
+          bob.succeed("ipsec auto --start tunnel")
+          # apparently this is needed to "wake" the tunnel
+          bob.execute("ping -c1 alice")
+
+      with subtest("Eve no longer can eavesdrop"):
+          eavesdrop()
+          alice_to_bob("Just kidding, I actually like rhubarb")
+          eve.sleep(1)
+          eve.fail("grep rhubarb /tmp/log")
+    '';
+})
diff --git a/nixos/tests/lidarr.nix b/nixos/tests/lidarr.nix
new file mode 100644
index 00000000000..d3f83e5d914
--- /dev/null
+++ b/nixos/tests/lidarr.nix
@@ -0,0 +1,20 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "lidarr";
+  meta.maintainers = with maintainers; [ etu ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    { services.lidarr.enable = true; };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("lidarr.service")
+    machine.wait_for_open_port("8686")
+    machine.succeed("curl --fail http://localhost:8686/")
+  '';
+})
diff --git a/nixos/tests/lightdm.nix b/nixos/tests/lightdm.nix
new file mode 100644
index 00000000000..e98230ecb17
--- /dev/null
+++ b/nixos/tests/lightdm.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "lightdm";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ aszlig ];
+  };
+
+  machine = { ... }: {
+    imports = [ ./common/user-account.nix ];
+    services.xserver.enable = true;
+    services.xserver.displayManager.lightdm.enable = true;
+    services.xserver.displayManager.defaultSession = "none+icewm";
+    services.xserver.windowManager.icewm.enable = true;
+  };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+  in ''
+    start_all()
+    machine.wait_for_text("${user.description}")
+    machine.screenshot("lightdm")
+    machine.send_chars("${user.password}\n")
+    machine.wait_for_file("${user.home}/.Xauthority")
+    machine.succeed("xauth merge ${user.home}/.Xauthority")
+    machine.wait_for_window("^IceWM ")
+  '';
+})
diff --git a/nixos/tests/limesurvey.nix b/nixos/tests/limesurvey.nix
new file mode 100644
index 00000000000..b60e80be244
--- /dev/null
+++ b/nixos/tests/limesurvey.nix
@@ -0,0 +1,26 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "limesurvey";
+  meta.maintainers = [ pkgs.lib.maintainers.aanderse ];
+
+  machine = { ... }: {
+    services.limesurvey = {
+      enable = true;
+      virtualHost = {
+        hostName = "example.local";
+        adminAddr = "root@example.local";
+      };
+    };
+
+    # limesurvey won't work without a dot in the hostname
+    networking.hosts."127.0.0.1" = [ "example.local" ];
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("phpfpm-limesurvey.service")
+    assert "The following surveys are available" in machine.succeed(
+        "curl -f http://example.local/"
+    )
+  '';
+})
diff --git a/nixos/tests/litestream.nix b/nixos/tests/litestream.nix
new file mode 100644
index 00000000000..886fbfef9cf
--- /dev/null
+++ b/nixos/tests/litestream.nix
@@ -0,0 +1,93 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "litestream";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ jwygoda ];
+  };
+
+  machine =
+    { pkgs, ... }:
+    { services.litestream = {
+        enable = true;
+        settings = {
+          dbs = [
+            {
+              path = "/var/lib/grafana/data/grafana.db";
+              replicas = [{
+                url = "sftp://foo:bar@127.0.0.1:22/home/foo/grafana";
+              }];
+            }
+          ];
+        };
+      };
+      systemd.services.grafana.serviceConfig.ExecStartPost = "+" + pkgs.writeShellScript "grant-grafana-permissions" ''
+        timeout=10
+
+        while [ ! -f /var/lib/grafana/data/grafana.db ];
+        do
+          if [ "$timeout" == 0 ]; then
+            echo "ERROR: Timeout while waiting for /var/lib/grafana/data/grafana.db."
+            exit 1
+          fi
+
+          sleep 1
+
+          ((timeout--))
+        done
+
+        find /var/lib/grafana -type d -exec chmod -v 775 {} \;
+        find /var/lib/grafana -type f -exec chmod -v 660 {} \;
+      '';
+      services.openssh = {
+        enable = true;
+        allowSFTP = true;
+        listenAddresses = [ { addr = "127.0.0.1"; port = 22; } ];
+      };
+      services.grafana = {
+        enable = true;
+        security = {
+          adminUser = "admin";
+          adminPassword = "admin";
+        };
+        addr = "localhost";
+        port = 3000;
+        extraOptions = {
+          DATABASE_URL = "sqlite3:///var/lib/grafana/data/grafana.db?cache=private&mode=rwc&_journal_mode=WAL";
+        };
+      };
+      users.users.foo = {
+        isNormalUser = true;
+        password = "bar";
+      };
+      users.users.litestream.extraGroups = [ "grafana" ];
+    };
+
+  testScript = ''
+    start_all()
+    machine.wait_until_succeeds("test -d /home/foo/grafana")
+    machine.wait_for_open_port(3000)
+    machine.succeed("""
+        curl -sSfN -X PUT -H "Content-Type: application/json" -d '{
+          "oldPassword": "admin",
+          "newPassword": "newpass",
+          "confirmNew": "newpass"
+        }' http://admin:admin@127.0.0.1:3000/api/user/password
+    """)
+    # https://litestream.io/guides/systemd/#simulating-a-disaster
+    machine.systemctl("stop litestream.service")
+    machine.succeed(
+        "rm -f /var/lib/grafana/data/grafana.db "
+        "/var/lib/grafana/data/grafana.db-shm "
+        "/var/lib/grafana/data/grafana.db-wal"
+    )
+    machine.succeed(
+        "litestream restore /var/lib/grafana/data/grafana.db "
+        "&& chown grafana:grafana /var/lib/grafana/data/grafana.db "
+        "&& chmod 660 /var/lib/grafana/data/grafana.db"
+    )
+    machine.systemctl("restart grafana.service")
+    machine.wait_for_open_port(3000)
+    machine.succeed(
+        "curl -sSfN -u admin:newpass http://127.0.0.1:3000/api/org/users | grep admin\@localhost"
+    )
+  '';
+})
diff --git a/nixos/tests/locate.nix b/nixos/tests/locate.nix
new file mode 100644
index 00000000000..e8ba41812a8
--- /dev/null
+++ b/nixos/tests/locate.nix
@@ -0,0 +1,62 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+  let inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
+  in {
+    name = "locate";
+    meta.maintainers = with pkgs.lib.maintainers; [ chkno ];
+
+    nodes = rec {
+      a = {
+        environment.systemPackages = with pkgs; [ sshfs ];
+        virtualisation.fileSystems = {
+          "/ssh" = {
+            device = "alice@b:/";
+            fsType = "fuse.sshfs";
+            options = [
+              "allow_other"
+              "IdentityFile=/privkey"
+              "noauto"
+              "StrictHostKeyChecking=no"
+              "UserKnownHostsFile=/dev/null"
+            ];
+          };
+        };
+        services.locate = {
+          enable = true;
+          interval = "*:*:0/5";
+        };
+      };
+      b = {
+        services.openssh.enable = true;
+        users.users.alice = {
+          isNormalUser = true;
+          openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      # Set up sshfs mount
+      a.succeed(
+          "(umask 077; cat ${snakeOilPrivateKey} > /privkey)"
+      )
+      b.succeed("touch /file-on-b-machine")
+      b.wait_for_open_port(22)
+      a.succeed("mkdir /ssh")
+      a.succeed("mount /ssh")
+
+      # Core locatedb functionality
+      a.succeed("touch /file-on-a-machine-1")
+      a.wait_for_file("/var/cache/locatedb")
+      a.wait_until_succeeds("locate file-on-a-machine-1")
+
+      # Wait for a second update to make sure we're using a locatedb from a run
+      # that began after the sshfs mount
+      a.succeed("touch /file-on-a-machine-2")
+      a.wait_until_succeeds("locate file-on-a-machine-2")
+
+      # We shouldn't be able to see files on the other machine
+      a.fail("locate file-on-b-machine")
+    '';
+  })
diff --git a/nixos/tests/login.nix b/nixos/tests/login.nix
new file mode 100644
index 00000000000..4d1dcc8cc32
--- /dev/null
+++ b/nixos/tests/login.nix
@@ -0,0 +1,59 @@
+import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }:
+
+{
+  name = "login";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eelco ];
+  };
+
+  machine =
+    { pkgs, lib, ... }:
+    { boot.kernelPackages = lib.mkIf latestKernel pkgs.linuxPackages_latest;
+      sound.enable = true; # needed for the factl test, /dev/snd/* exists without them but udev doesn't care then
+    };
+
+  testScript = ''
+      machine.wait_for_unit("multi-user.target")
+      machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
+      machine.screenshot("postboot")
+
+      with subtest("create user"):
+          machine.succeed("useradd -m alice")
+          machine.succeed("(echo foobar; echo foobar) | passwd alice")
+
+      with subtest("Check whether switching VTs works"):
+          machine.fail("pgrep -f 'agetty.*tty2'")
+          machine.send_key("alt-f2")
+          machine.wait_until_succeeds("[ $(fgconsole) = 2 ]")
+          machine.wait_for_unit("getty@tty2.service")
+          machine.wait_until_succeeds("pgrep -f 'agetty.*tty2'")
+
+      with subtest("Log in as alice on a virtual console"):
+          machine.wait_until_tty_matches(2, "login: ")
+          machine.send_chars("alice\n")
+          machine.wait_until_tty_matches(2, "login: alice")
+          machine.wait_until_succeeds("pgrep login")
+          machine.wait_until_tty_matches(2, "Password: ")
+          machine.send_chars("foobar\n")
+          machine.wait_until_succeeds("pgrep -u alice bash")
+          machine.send_chars("touch done\n")
+          machine.wait_for_file("/home/alice/done")
+
+      with subtest("Systemd gives and removes device ownership as needed"):
+          machine.succeed("getfacl /dev/snd/timer | grep -q alice")
+          machine.send_key("alt-f1")
+          machine.wait_until_succeeds("[ $(fgconsole) = 1 ]")
+          machine.fail("getfacl /dev/snd/timer | grep -q alice")
+          machine.succeed("chvt 2")
+          machine.wait_until_succeeds("getfacl /dev/snd/timer | grep -q alice")
+
+      with subtest("Virtual console logout"):
+          machine.send_chars("exit\n")
+          machine.wait_until_fails("pgrep -u alice bash")
+          machine.screenshot("getty")
+
+      with subtest("Check whether ctrl-alt-delete works"):
+          machine.send_key("ctrl-alt-delete")
+          machine.wait_for_shutdown()
+  '';
+})
diff --git a/nixos/tests/logrotate.nix b/nixos/tests/logrotate.nix
new file mode 100644
index 00000000000..38da8d53527
--- /dev/null
+++ b/nixos/tests/logrotate.nix
@@ -0,0 +1,37 @@
+# Test logrotate service works and is enabled by default
+
+import ./make-test-python.nix ({ pkgs, ...} : rec {
+  name = "logrotate";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ martinetd ];
+  };
+
+  # default machine
+  machine = { ... }: {
+  };
+
+  testScript =
+    ''
+      with subtest("whether logrotate works"):
+          machine.succeed(
+              # we must rotate once first to create logrotate stamp
+              "systemctl start logrotate.service")
+          # we need to wait for console text once here to
+          # clear console buffer up to this point for next wait
+          machine.wait_for_console_text('logrotate.service: Deactivated successfully')
+
+          machine.succeed(
+              # wtmp is present in default config.
+              "rm -f /var/log/wtmp*",
+              # we need to give it at least 1MB
+              "dd if=/dev/zero of=/var/log/wtmp bs=2M count=1",
+
+              # move into the future and check rotation.
+              "date -s 'now + 1 month + 1 day'")
+          machine.wait_for_console_text('logrotate.service: Deactivated successfully')
+          machine.succeed(
+              # check rotate worked
+              "[ -e /var/log/wtmp.1 ]",
+          )
+    '';
+})
diff --git a/nixos/tests/loki.nix b/nixos/tests/loki.nix
new file mode 100644
index 00000000000..0c6dff3fdf1
--- /dev/null
+++ b/nixos/tests/loki.nix
@@ -0,0 +1,56 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+
+{
+  name = "loki";
+
+  meta = with lib.maintainers; {
+    maintainers = [ willibutz ];
+  };
+
+  machine = { ... }: {
+    services.loki = {
+      enable = true;
+      configFile = "${pkgs.grafana-loki.src}/cmd/loki/loki-local-config.yaml";
+    };
+    services.promtail = {
+      enable = true;
+      configuration = {
+        server = {
+          http_listen_port = 9080;
+          grpc_listen_port = 0;
+        };
+        clients = [ { url = "http://localhost:3100/loki/api/v1/push"; } ];
+        scrape_configs = [
+          {
+            job_name = "system";
+            static_configs = [
+              {
+                targets = [ "localhost" ];
+                labels = {
+                  job = "varlogs";
+                  __path__ = "/var/log/*log";
+                };
+              }
+            ];
+          }
+        ];
+      };
+    };
+  };
+
+  testScript = ''
+    machine.start
+    machine.wait_for_unit("loki.service")
+    machine.wait_for_unit("promtail.service")
+    machine.wait_for_open_port(3100)
+    machine.wait_for_open_port(9080)
+    machine.succeed("echo 'Loki Ingestion Test' > /var/log/testlog")
+    # should not have access to journal unless specified
+    machine.fail(
+        "systemctl show --property=SupplementaryGroups promtail | grep -q systemd-journal"
+    )
+    machine.wait_until_succeeds(
+        "${pkgs.grafana-loki}/bin/logcli --addr='http://localhost:3100' query --no-labels '{job=\"varlogs\",filename=\"/var/log/testlog\"}' | grep -q 'Loki Ingestion Test'"
+    )
+  '';
+})
diff --git a/nixos/tests/lorri/builder.sh b/nixos/tests/lorri/builder.sh
new file mode 100644
index 00000000000..b586b2bf798
--- /dev/null
+++ b/nixos/tests/lorri/builder.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+printf "%s" "${name:?}" > "${out:?}"
diff --git a/nixos/tests/lorri/default.nix b/nixos/tests/lorri/default.nix
new file mode 100644
index 00000000000..c33c7503993
--- /dev/null
+++ b/nixos/tests/lorri/default.nix
@@ -0,0 +1,26 @@
+import ../make-test-python.nix {
+  machine = { pkgs, ... }: {
+    imports = [ ../../modules/profiles/minimal.nix ];
+    environment.systemPackages = [ pkgs.lorri ];
+  };
+
+  testScript = ''
+    # Copy files over
+    machine.succeed(
+        "cp '${./fake-shell.nix}' shell.nix"
+    )
+    machine.succeed(
+        "cp '${./builder.sh}' builder.sh"
+    )
+
+    # Start the daemon and wait until it is ready
+    machine.execute("lorri daemon > lorri.stdout 2> lorri.stderr &")
+    machine.wait_until_succeeds("grep --fixed-strings 'ready' lorri.stdout")
+
+    # Ping the daemon
+    machine.succeed("lorri internal ping shell.nix")
+
+    # Wait for the daemon to finish the build
+    machine.wait_until_succeeds("grep --fixed-strings 'Completed' lorri.stdout")
+  '';
+}
diff --git a/nixos/tests/lorri/fake-shell.nix b/nixos/tests/lorri/fake-shell.nix
new file mode 100644
index 00000000000..9de9d247e54
--- /dev/null
+++ b/nixos/tests/lorri/fake-shell.nix
@@ -0,0 +1,5 @@
+derivation {
+  system = builtins.currentSystem;
+  name = "fake-shell";
+  builder = ./builder.sh;
+}
diff --git a/nixos/tests/lxd-image-server.nix b/nixos/tests/lxd-image-server.nix
new file mode 100644
index 00000000000..9f060fed38d
--- /dev/null
+++ b/nixos/tests/lxd-image-server.nix
@@ -0,0 +1,127 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+let
+  # Since we don't have access to the internet during the tests, we have to
+  # pre-fetch lxd containers beforehand.
+  #
+  # I've chosen to import Alpine Linux, because its image is turbo-tiny and,
+  # generally, sufficient for our tests.
+  alpine-meta = pkgs.fetchurl {
+    url = "https://tarballs.nixos.org/alpine/3.12/lxd.tar.xz";
+    hash = "sha256-1tcKaO9lOkvqfmG/7FMbfAEToAuFy2YMewS8ysBKuLA=";
+  };
+
+  alpine-rootfs = pkgs.fetchurl {
+    url = "https://tarballs.nixos.org/alpine/3.12/rootfs.tar.xz";
+    hash = "sha256-Tba9sSoaiMtQLY45u7p5DMqXTSDgs/763L/SQp0bkCA=";
+  };
+
+  lxd-config = pkgs.writeText "config.yaml" ''
+    storage_pools:
+      - name: default
+        driver: dir
+        config:
+          source: /var/lxd-pool
+
+    networks:
+      - name: lxdbr0
+        type: bridge
+        config:
+          ipv4.address: auto
+          ipv6.address: none
+
+    profiles:
+      - name: default
+        devices:
+          eth0:
+            name: eth0
+            network: lxdbr0
+            type: nic
+          root:
+            path: /
+            pool: default
+            type: disk
+  '';
+
+
+in {
+  name = "lxd-image-server";
+
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mkg20001 ];
+  };
+
+  machine = { lib, ... }: {
+    virtualisation = {
+      cores = 2;
+
+      memorySize = 2048;
+      diskSize = 4096;
+
+      lxc.lxcfs.enable = true;
+      lxd.enable = true;
+    };
+
+    security.pki.certificates = [
+      (builtins.readFile ./common/acme/server/ca.cert.pem)
+    ];
+
+    services.nginx = {
+      enable = true;
+    };
+
+    services.lxd-image-server = {
+      enable = true;
+      nginx = {
+        enable = true;
+        domain = "acme.test";
+      };
+    };
+
+    services.nginx.virtualHosts."acme.test" = {
+      enableACME = false;
+      sslCertificate = ./common/acme/server/acme.test.cert.pem;
+      sslCertificateKey = ./common/acme/server/acme.test.key.pem;
+    };
+
+    networking.hosts = {
+      "::1" = [ "acme.test" ];
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("sockets.target")
+    machine.wait_for_unit("lxd.service")
+    machine.wait_for_file("/var/lib/lxd/unix.socket")
+
+    # It takes additional second for lxd to settle
+    machine.sleep(1)
+
+    # lxd expects the pool's directory to already exist
+    machine.succeed("mkdir /var/lxd-pool")
+
+
+    machine.succeed(
+        "cat ${lxd-config} | lxd init --preseed"
+    )
+
+    machine.succeed(
+        "lxc image import ${alpine-meta} ${alpine-rootfs} --alias alpine"
+    )
+
+    loc = "/var/www/simplestreams/images/iats/alpine/amd64/default/v1"
+
+    with subtest("push image to server"):
+        machine.succeed("lxc launch alpine test")
+        machine.succeed("lxc stop test")
+        machine.succeed("lxc publish --public test --alias=testimg")
+        machine.succeed("lxc image export testimg")
+        machine.succeed("ls >&2")
+        machine.succeed("mkdir -p " + loc)
+        machine.succeed("mv *.tar.gz " + loc)
+
+    with subtest("pull image from server"):
+        machine.succeed("lxc remote add img https://acme.test --protocol=simplestreams")
+        machine.succeed("lxc image list img: >&2")
+  '';
+})
diff --git a/nixos/tests/lxd-image.nix b/nixos/tests/lxd-image.nix
new file mode 100644
index 00000000000..096b9d9aba9
--- /dev/null
+++ b/nixos/tests/lxd-image.nix
@@ -0,0 +1,89 @@
+# This test ensures that the nixOS lxd images builds and functions properly
+# It has been extracted from `lxd.nix` to seperate failures of just the image and the lxd software
+
+import ./make-test-python.nix ({ pkgs, ...} : let
+  release = import ../release.nix {
+    /* configuration = {
+      environment.systemPackages = with pkgs; [ stdenv ]; # inject stdenv so rebuild test works
+    }; */
+  };
+
+  metadata = release.lxdMeta.${pkgs.system};
+  image = release.lxdImage.${pkgs.system};
+
+  lxd-config = pkgs.writeText "config.yaml" ''
+    storage_pools:
+      - name: default
+        driver: dir
+        config:
+          source: /var/lxd-pool
+
+    networks:
+      - name: lxdbr0
+        type: bridge
+        config:
+          ipv4.address: auto
+          ipv6.address: none
+
+    profiles:
+      - name: default
+        devices:
+          eth0:
+            name: eth0
+            network: lxdbr0
+            type: nic
+          root:
+            path: /
+            pool: default
+            type: disk
+  '';
+in {
+  name = "lxd-image";
+
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mkg20001 ];
+  };
+
+  machine = { lib, ... }: {
+    virtualisation = {
+      # disk full otherwise
+      diskSize = 2048;
+
+      lxc.lxcfs.enable = true;
+      lxd.enable = true;
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("sockets.target")
+    machine.wait_for_unit("lxd.service")
+    machine.wait_for_file("/var/lib/lxd/unix.socket")
+
+    # It takes additional second for lxd to settle
+    machine.sleep(1)
+
+    # lxd expects the pool's directory to already exist
+    machine.succeed("mkdir /var/lxd-pool")
+
+    machine.succeed(
+        "cat ${lxd-config} | lxd init --preseed"
+    )
+
+    # TODO: test custom built container aswell
+
+    with subtest("importing container works"):
+        machine.succeed("lxc image import ${metadata}/*/*.tar.xz ${image}/*/*.tar.xz --alias nixos")
+
+    with subtest("launching container works"):
+        machine.succeed("lxc launch nixos machine -c security.nesting=true")
+        # make sure machine boots up properly
+        machine.sleep(5)
+
+    with subtest("container shell works"):
+        machine.succeed("echo true | lxc exec machine /run/current-system/sw/bin/bash -")
+        machine.succeed("lxc exec machine /run/current-system/sw/bin/true")
+
+    # with subtest("rebuilding works"):
+    #     machine.succeed("lxc exec machine /run/current-system/sw/bin/nixos-rebuild switch")
+  '';
+})
diff --git a/nixos/tests/lxd-nftables.nix b/nixos/tests/lxd-nftables.nix
new file mode 100644
index 00000000000..a62d5a3064d
--- /dev/null
+++ b/nixos/tests/lxd-nftables.nix
@@ -0,0 +1,51 @@
+# This test makes sure that lxd stops implicitly depending on iptables when
+# user enabled nftables.
+#
+# It has been extracted from `lxd.nix` for clarity, and because switching from
+# iptables to nftables requires a full reboot, which is a bit hard inside NixOS
+# tests.
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "lxd-nftables";
+
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ patryk27 ];
+  };
+
+  machine = { lib, ... }: {
+    virtualisation = {
+      lxd.enable = true;
+    };
+
+    networking = {
+      firewall.enable = false;
+      nftables.enable = true;
+      nftables.ruleset = ''
+        table inet filter {
+          chain incoming {
+            type filter hook input priority 0;
+            policy accept;
+          }
+
+          chain forward {
+            type filter hook forward priority 0;
+            policy accept;
+          }
+
+          chain output {
+            type filter hook output priority 0;
+            policy accept;
+          }
+        }
+      '';
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("network.target")
+
+    with subtest("When nftables are enabled, lxd doesn't depend on iptables anymore"):
+        machine.succeed("lsmod | grep nf_tables")
+        machine.fail("lsmod | grep ip_tables")
+  '';
+})
diff --git a/nixos/tests/lxd.nix b/nixos/tests/lxd.nix
new file mode 100644
index 00000000000..1a3b84a85cf
--- /dev/null
+++ b/nixos/tests/lxd.nix
@@ -0,0 +1,137 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+let
+  # Since we don't have access to the internet during the tests, we have to
+  # pre-fetch lxd containers beforehand.
+  #
+  # I've chosen to import Alpine Linux, because its image is turbo-tiny and,
+  # generally, sufficient for our tests.
+  alpine-meta = pkgs.fetchurl {
+    url = "https://tarballs.nixos.org/alpine/3.12/lxd.tar.xz";
+    hash = "sha256-1tcKaO9lOkvqfmG/7FMbfAEToAuFy2YMewS8ysBKuLA=";
+  };
+
+  alpine-rootfs = pkgs.fetchurl {
+    url = "https://tarballs.nixos.org/alpine/3.12/rootfs.tar.xz";
+    hash = "sha256-Tba9sSoaiMtQLY45u7p5DMqXTSDgs/763L/SQp0bkCA=";
+  };
+
+  lxd-config = pkgs.writeText "config.yaml" ''
+    storage_pools:
+      - name: default
+        driver: dir
+        config:
+          source: /var/lxd-pool
+
+    networks:
+      - name: lxdbr0
+        type: bridge
+        config:
+          ipv4.address: auto
+          ipv6.address: none
+
+    profiles:
+      - name: default
+        devices:
+          eth0:
+            name: eth0
+            network: lxdbr0
+            type: nic
+          root:
+            path: /
+            pool: default
+            type: disk
+  '';
+
+
+in {
+  name = "lxd";
+
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ patryk27 ];
+  };
+
+  machine = { lib, ... }: {
+    virtualisation = {
+      # Since we're testing `limits.cpu`, we've gotta have a known number of
+      # cores to lean on
+      cores = 2;
+
+      # Ditto, for `limits.memory`
+      memorySize = 512;
+
+      lxc.lxcfs.enable = true;
+      lxd.enable = true;
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("sockets.target")
+    machine.wait_for_unit("lxd.service")
+    machine.wait_for_file("/var/lib/lxd/unix.socket")
+
+    # It takes additional second for lxd to settle
+    machine.sleep(1)
+
+    # lxd expects the pool's directory to already exist
+    machine.succeed("mkdir /var/lxd-pool")
+
+    machine.succeed(
+        "cat ${lxd-config} | lxd init --preseed"
+    )
+
+    machine.succeed(
+        "lxc image import ${alpine-meta} ${alpine-rootfs} --alias alpine"
+    )
+
+    with subtest("Containers can be launched and destroyed"):
+        machine.succeed("lxc launch alpine test")
+        machine.succeed("lxc exec test true")
+        machine.succeed("lxc delete -f test")
+
+    with subtest("Containers are being mounted with lxcfs inside"):
+        machine.succeed("lxc launch alpine test")
+
+        ## ---------- ##
+        ## limits.cpu ##
+
+        machine.succeed("lxc config set test limits.cpu 1")
+        machine.succeed("lxc restart test")
+
+        # Since Alpine doesn't have `nproc` pre-installed, we've gotta resort
+        # to the primal methods
+        assert (
+            "1"
+            == machine.succeed("lxc exec test grep -- -c ^processor /proc/cpuinfo").strip()
+        )
+
+        machine.succeed("lxc config set test limits.cpu 2")
+        machine.succeed("lxc restart test")
+
+        assert (
+            "2"
+            == machine.succeed("lxc exec test grep -- -c ^processor /proc/cpuinfo").strip()
+        )
+
+        ## ------------- ##
+        ## limits.memory ##
+
+        machine.succeed("lxc config set test limits.memory 64MB")
+        machine.succeed("lxc restart test")
+
+        assert (
+            "MemTotal:          62500 kB"
+            == machine.succeed("lxc exec test grep -- MemTotal /proc/meminfo").strip()
+        )
+
+        machine.succeed("lxc config set test limits.memory 128MB")
+        machine.succeed("lxc restart test")
+
+        assert (
+            "MemTotal:         125000 kB"
+            == machine.succeed("lxc exec test grep -- MemTotal /proc/meminfo").strip()
+        )
+
+        machine.succeed("lxc delete -f test")
+  '';
+})
diff --git a/nixos/tests/maddy.nix b/nixos/tests/maddy.nix
new file mode 100644
index 00000000000..581748c1fa5
--- /dev/null
+++ b/nixos/tests/maddy.nix
@@ -0,0 +1,58 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "maddy";
+  meta = with pkgs.lib.maintainers; { maintainers = [ onny ]; };
+
+  nodes = {
+    server = { ... }: {
+      services.maddy = {
+        enable = true;
+        hostname = "server";
+        primaryDomain = "server";
+        openFirewall = true;
+      };
+    };
+
+    client = { ... }: {
+      environment.systemPackages = [
+        (pkgs.writers.writePython3Bin "send-testmail" { } ''
+          import smtplib
+          from email.mime.text import MIMEText
+
+          msg = MIMEText("Hello World")
+          msg['Subject'] = 'Test'
+          msg['From'] = "postmaster@server"
+          msg['To'] = "postmaster@server"
+          with smtplib.SMTP('server', 587) as smtp:
+              smtp.login('postmaster@server', 'test')
+              smtp.sendmail('postmaster@server', 'postmaster@server', msg.as_string())
+        '')
+        (pkgs.writers.writePython3Bin "test-imap" { } ''
+          import imaplib
+
+          with imaplib.IMAP4('server') as imap:
+              imap.login('postmaster@server', 'test')
+              imap.select()
+              status, refs = imap.search(None, 'ALL')
+              assert status == 'OK'
+              assert len(refs) == 1
+              status, msg = imap.fetch(refs[0], 'BODY[TEXT]')
+              assert status == 'OK'
+              assert msg[0][1].strip() == b"Hello World"
+        '')
+      ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+    server.wait_for_unit("maddy.service")
+    server.wait_for_open_port(143)
+    server.wait_for_open_port(587)
+
+    server.succeed("echo test | maddyctl creds create postmaster@server")
+    server.succeed("maddyctl imap-acct create postmaster@server")
+
+    client.succeed("send-testmail")
+    client.succeed("test-imap")
+  '';
+})
diff --git a/nixos/tests/magic-wormhole-mailbox-server.nix b/nixos/tests/magic-wormhole-mailbox-server.nix
new file mode 100644
index 00000000000..54088ac60f2
--- /dev/null
+++ b/nixos/tests/magic-wormhole-mailbox-server.nix
@@ -0,0 +1,38 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "magic-wormhole-mailbox-server";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mmahut ];
+  };
+
+  nodes = {
+    server = { ... }: {
+      networking.firewall.allowedTCPPorts = [ 4000 ];
+      services.magic-wormhole-mailbox-server.enable = true;
+    };
+
+    client_alice = { ... }: {
+      networking.firewall.enable = false;
+      environment.systemPackages = [ pkgs.magic-wormhole ];
+    };
+
+    client_bob = { ... }: {
+      environment.systemPackages = [ pkgs.magic-wormhole ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    # Start the wormhole relay server
+    server.wait_for_unit("magic-wormhole-mailbox-server.service")
+    server.wait_for_open_port(4000)
+
+    # Create a secret file and send it to Bob
+    client_alice.succeed("echo mysecret > secretfile")
+    client_alice.succeed("wormhole --relay-url=ws://server:4000/v1 send -0 secretfile >&2 &")
+
+    # Retrieve a secret file from Alice and check its content
+    client_bob.succeed("wormhole --relay-url=ws://server:4000/v1 receive -0 --accept-file")
+    client_bob.succeed("grep mysecret secretfile")
+  '';
+})
diff --git a/nixos/tests/magnetico.nix b/nixos/tests/magnetico.nix
new file mode 100644
index 00000000000..8433a974f45
--- /dev/null
+++ b/nixos/tests/magnetico.nix
@@ -0,0 +1,41 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+let
+  port = 8081;
+in
+{
+  name = "magnetico";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ rnhmjoj ];
+  };
+
+  machine = { ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+
+    networking.firewall.allowedTCPPorts = [ 9000 ];
+
+    services.magnetico = {
+      enable = true;
+      crawler.port = 9000;
+      web.port = port;
+      web.credentials.user = "$2y$12$P88ZF6soFthiiAeXnz64aOWDsY3Dw7Yw8fZ6GtiqFNjknD70zDmNe";
+    };
+  };
+
+  testScript =
+    ''
+      start_all()
+      machine.wait_for_unit("magneticod")
+      machine.wait_for_unit("magneticow")
+      machine.wait_for_open_port(${toString port})
+      machine.succeed(
+          "${pkgs.curl}/bin/curl --fail "
+          + "-u user:password http://localhost:${toString port}"
+      )
+      machine.fail(
+          "${pkgs.curl}/bin/curl --fail "
+          + "-u user:wrongpwd http://localhost:${toString port}"
+      )
+      machine.shutdown()
+    '';
+})
diff --git a/nixos/tests/mailcatcher.nix b/nixos/tests/mailcatcher.nix
new file mode 100644
index 00000000000..a55fba8a995
--- /dev/null
+++ b/nixos/tests/mailcatcher.nix
@@ -0,0 +1,30 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+{
+  name = "mailcatcher";
+  meta.maintainers = [ lib.maintainers.aanderse ];
+
+  machine =
+    { pkgs, ... }:
+    {
+      services.mailcatcher.enable = true;
+
+      services.ssmtp.enable = true;
+      services.ssmtp.hostName = "localhost:1025";
+
+      environment.systemPackages = [ pkgs.mailutils ];
+    };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("mailcatcher.service")
+    machine.wait_for_open_port("1025")
+    machine.succeed(
+        'echo "this is the body of the email" | mail -s "subject" root@example.org'
+    )
+    assert "this is the body of the email" in machine.succeed(
+        "curl -f http://localhost:1080/messages/1.source"
+    )
+  '';
+})
diff --git a/nixos/tests/mailhog.nix b/nixos/tests/mailhog.nix
new file mode 100644
index 00000000000..aece57178dd
--- /dev/null
+++ b/nixos/tests/mailhog.nix
@@ -0,0 +1,24 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "mailhog";
+  meta.maintainers = with lib.maintainers; [ jojosch ];
+
+  machine = { pkgs, ... }: {
+    services.mailhog.enable = true;
+
+    environment.systemPackages = with pkgs; [ swaks ];
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("mailhog.service")
+    machine.wait_for_open_port("1025")
+    machine.wait_for_open_port("8025")
+    machine.succeed(
+        'echo "this is the body of the email" | swaks --to root@example.org --body - --server localhost:1025'
+    )
+    assert "this is the body of the email" in machine.succeed(
+        "curl --fail http://localhost:8025/api/v2/messages"
+    )
+  '';
+})
diff --git a/nixos/tests/make-test-python.nix b/nixos/tests/make-test-python.nix
new file mode 100644
index 00000000000..7a96f538d8d
--- /dev/null
+++ b/nixos/tests/make-test-python.nix
@@ -0,0 +1,9 @@
+f: {
+  system ? builtins.currentSystem,
+  pkgs ? import ../.. { inherit system; },
+  ...
+} @ args:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+
+makeTest (if pkgs.lib.isFunction f then f (args // { inherit pkgs; inherit (pkgs) lib; }) else f)
diff --git a/nixos/tests/man.nix b/nixos/tests/man.nix
new file mode 100644
index 00000000000..1ff5af4e805
--- /dev/null
+++ b/nixos/tests/man.nix
@@ -0,0 +1,100 @@
+
+import ./make-test-python.nix ({ pkgs, lib, ... }: let
+  manImplementations = [
+    "mandoc"
+    "man-db"
+  ];
+
+  machineNames = builtins.map machineSafe manImplementations;
+
+  makeConfig = useImpl: {
+    # Note: mandoc currently can't index symlinked section directories.
+    # So if a man section comes from one package exclusively (e. g.
+    # 1p from man-pages-posix and 2 from man-pages), it isn't searchable.
+    environment.systemPackages = [
+      pkgs.man-pages
+      pkgs.openssl
+      pkgs.libunwind
+    ];
+
+    documentation = {
+      enable = true;
+      nixos.enable = lib.mkForce true;
+      dev.enable = true;
+      man = {
+        enable = true;
+        generateCaches = true;
+      } // lib.listToAttrs (builtins.map (impl: {
+        name = impl;
+        value = {
+          enable = useImpl == impl;
+        };
+      }) manImplementations);
+    };
+  };
+
+  machineSafe = builtins.replaceStrings [ "-" ] [ "_" ];
+in {
+  name = "man";
+  meta.maintainers = [ lib.maintainers.sternenseemann ];
+
+  nodes = lib.listToAttrs (builtins.map (i: {
+    name = machineSafe i;
+    value = makeConfig i;
+  }) manImplementations);
+
+  testScript = ''
+    import re
+    start_all()
+
+    def match_man_k(page, section, haystack):
+      """
+      Check if the man page {page}({section}) occurs in
+      the output of `man -k` given as haystack. Note:
+      This is not super reliable, e. g. it can't deal
+      with man pages that are in multiple sections.
+      """
+
+      for line in haystack.split("\n"):
+        # man -k can look like this:
+        # page(3) - bla
+        # page (3) - bla
+        # pagea, pageb (3, 3P) - foo
+        # pagea, pageb, pagec(3) - bar
+        pages = line.split("(")[0]
+        sections = re.search("\\([a-zA-Z1-9, ]+\\)", line)
+        if sections is None:
+          continue
+        else:
+          sections = sections.group(0)[1:-1]
+
+        if page in pages and f'{section}' in sections:
+          return True
+
+      return False
+
+  '' + lib.concatMapStrings (machine: ''
+    with subtest("Test direct man page lookups in ${machine}"):
+      # man works
+      ${machine}.succeed("man man > /dev/null")
+      # devman works
+      ${machine}.succeed("man 3 libunwind > /dev/null")
+      # NixOS configuration man page is installed
+      ${machine}.succeed("man configuration.nix > /dev/null")
+
+    with subtest("Test generateCaches via man -k in ${machine}"):
+      expected = [
+        ("openssl", "ssl", 3),
+        ("unwind", "libunwind", 3),
+        ("user", "useradd", 8),
+        ("user", "userdel", 8),
+        ("mem", "free", 3),
+        ("mem", "free", 1),
+      ]
+
+      for (keyword, page, section) in expected:
+        matches = ${machine}.succeed(f"man -k {keyword}")
+        if not match_man_k(page, section, matches):
+          raise Exception(f"{page}({section}) missing in matches: {matches}")
+  '') machineNames;
+})
diff --git a/nixos/tests/matomo.nix b/nixos/tests/matomo.nix
new file mode 100644
index 00000000000..f6b0845749c
--- /dev/null
+++ b/nixos/tests/matomo.nix
@@ -0,0 +1,48 @@
+{ system ? builtins.currentSystem, config ? { }
+, pkgs ? import ../.. { inherit system config; } }:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  matomoTest = package:
+  makeTest {
+    machine = { config, pkgs, ... }: {
+      services.matomo = {
+        package = package;
+        enable = true;
+        nginx = {
+          forceSSL = false;
+          enableACME = false;
+        };
+      };
+      services.mysql = {
+        enable = true;
+        package = pkgs.mariadb;
+      };
+      services.nginx.enable = true;
+    };
+
+    testScript = ''
+      start_all()
+      machine.wait_for_unit("mysql.service")
+      machine.wait_for_unit("phpfpm-matomo.service")
+      machine.wait_for_unit("nginx.service")
+
+      # without the grep the command does not produce valid utf-8 for some reason
+      with subtest("welcome screen loads"):
+          machine.succeed(
+              "curl -sSfL http://localhost/ | grep '<title>Matomo[^<]*Installation'"
+          )
+    '';
+  };
+in {
+  matomo = matomoTest pkgs.matomo // {
+    name = "matomo";
+    meta.maintainers = with maintainers; [ florianjacob kiwi mmilata ];
+  };
+  matomo-beta = matomoTest pkgs.matomo-beta // {
+    name = "matomo-beta";
+    meta.maintainers = with maintainers; [ florianjacob kiwi mmilata ];
+  };
+}
diff --git a/nixos/tests/matrix-appservice-irc.nix b/nixos/tests/matrix-appservice-irc.nix
new file mode 100644
index 00000000000..d1c561f95db
--- /dev/null
+++ b/nixos/tests/matrix-appservice-irc.nix
@@ -0,0 +1,221 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+  let
+    homeserverUrl = "http://homeserver:8008";
+  in
+  {
+    name = "matrix-appservice-irc";
+    meta = {
+      maintainers = pkgs.matrix-appservice-irc.meta.maintainers;
+    };
+
+    nodes = {
+      homeserver = { pkgs, ... }: {
+        # We'll switch to this once the config is copied into place
+        specialisation.running.configuration = {
+          services.matrix-synapse = {
+            enable = true;
+            settings = {
+              database.name = "sqlite3";
+              app_service_config_files = [ "/registration.yml" ];
+
+              enable_registration = true;
+
+              listeners = [ {
+                # The default but tls=false
+                bind_addresses = [
+                  "0.0.0.0"
+                ];
+                port = 8008;
+                resources = [ {
+                  "compress" = true;
+                  "names" = [ "client" ];
+                } {
+                  "compress" = false;
+                  "names" = [ "federation" ];
+                } ];
+                tls = false;
+                type = "http";
+              } ];
+            };
+          };
+
+          networking.firewall.allowedTCPPorts = [ 8008 ];
+        };
+      };
+
+      ircd = { pkgs, ... }: {
+        services.ngircd = {
+          enable = true;
+          config = ''
+            [Global]
+              Name = ircd.ircd
+              Info = Server Info Text
+              AdminInfo1 = _
+
+            [Channel]
+              Name = #test
+              Topic = a cool place
+
+            [Options]
+              PAM = no
+          '';
+        };
+        networking.firewall.allowedTCPPorts = [ 6667 ];
+      };
+
+      appservice = { pkgs, ... }: {
+        services.matrix-appservice-irc = {
+          enable = true;
+          registrationUrl = "http://appservice:8009";
+
+          settings = {
+            homeserver.url = homeserverUrl;
+            homeserver.domain = "homeserver";
+
+            ircService.servers."ircd" = {
+              name = "IRCd";
+              port = 6667;
+              dynamicChannels = {
+                enabled = true;
+                aliasTemplate = "#irc_$CHANNEL";
+              };
+            };
+          };
+        };
+
+        networking.firewall.allowedTCPPorts = [ 8009 ];
+      };
+
+      client = { pkgs, ... }: {
+        environment.systemPackages = [
+          (pkgs.writers.writePython3Bin "do_test"
+          {
+            libraries = [ pkgs.python3Packages.matrix-nio ];
+            flakeIgnore = [
+              # We don't live in the dark ages anymore.
+              # Languages like Python that are whitespace heavy will overrun
+              # 79 characters..
+              "E501"
+            ];
+          } ''
+              import sys
+              import socket
+              import functools
+              from time import sleep
+              import asyncio
+
+              from nio import AsyncClient, RoomMessageText, JoinResponse
+
+
+              async def matrix_room_message_text_callback(matrix: AsyncClient, msg: str, _r, e):
+                  print("Received matrix text message: ", e)
+                  if msg in e.body:
+                      print("Received hi from IRC")
+                      await matrix.close()
+                      exit(0)  # Actual exit point
+
+
+              class IRC:
+                  def __init__(self):
+                      sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+                      sock.connect(("ircd", 6667))
+                      sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+                      sock.send(b"USER bob bob bob :bob\n")
+                      sock.send(b"NICK bob\n")
+                      self.sock = sock
+
+                  def join(self, room: str):
+                      self.sock.send(f"JOIN {room}\n".encode())
+
+                  def privmsg(self, room: str, msg: str):
+                      self.sock.send(f"PRIVMSG {room} :{msg}\n".encode())
+
+                  def expect_msg(self, body: str):
+                      buffer = ""
+                      while True:
+                          buf = self.sock.recv(1024).decode()
+                          buffer += buf
+                          if body in buffer:
+                              return
+
+
+              async def run(homeserver: str):
+                  irc = IRC()
+
+                  matrix = AsyncClient(homeserver)
+                  response = await matrix.register("alice", "foobar")
+                  print("Matrix register response: ", response)
+
+                  response = await matrix.join("#irc_#test:homeserver")
+                  print("Matrix join room response:", response)
+                  assert isinstance(response, JoinResponse)
+                  room_id = response.room_id
+
+                  irc.join("#test")
+                  # FIXME: what are we waiting on here? Matrix? IRC? Both?
+                  # 10s seem bad for busy hydra machines.
+                  sleep(10)
+
+                  # Exchange messages
+                  print("Sending text message to matrix room")
+                  response = await matrix.room_send(
+                      room_id=room_id,
+                      message_type="m.room.message",
+                      content={"msgtype": "m.text", "body": "hi from matrix"},
+                  )
+                  print("Matrix room send response: ", response)
+                  irc.privmsg("#test", "hi from irc")
+
+                  print("Waiting for the matrix message to appear on the IRC side...")
+                  irc.expect_msg("hi from matrix")
+
+                  callback = functools.partial(
+                      matrix_room_message_text_callback, matrix, "hi from irc"
+                  )
+                  matrix.add_event_callback(callback, RoomMessageText)
+
+                  print("Waiting for matrix message...")
+                  await matrix.sync_forever()
+
+                  exit(1)  # Unreachable
+
+
+              if __name__ == "__main__":
+                  asyncio.run(run(sys.argv[1]))
+            ''
+          )
+        ];
+      };
+    };
+
+    testScript = ''
+      import pathlib
+
+      start_all()
+
+      ircd.wait_for_unit("ngircd.service")
+      ircd.wait_for_open_port(6667)
+
+      with subtest("start the appservice"):
+          appservice.wait_for_unit("matrix-appservice-irc.service")
+          appservice.wait_for_open_port(8009)
+
+      with subtest("copy the registration file"):
+          appservice.copy_from_vm("/var/lib/matrix-appservice-irc/registration.yml")
+          homeserver.copy_from_host(
+              pathlib.Path(os.environ.get("out", os.getcwd())) / "registration.yml", "/"
+          )
+          homeserver.succeed("chmod 444 /registration.yml")
+
+      with subtest("start the homeserver"):
+          homeserver.succeed(
+              "/run/current-system/specialisation/running/bin/switch-to-configuration test >&2"
+          )
+
+          homeserver.wait_for_unit("matrix-synapse.service")
+          homeserver.wait_for_open_port(8008)
+
+      with subtest("ensure messages can be exchanged"):
+          client.succeed("do_test ${homeserverUrl} >&2")
+    '';
+  })
diff --git a/nixos/tests/matrix-conduit.nix b/nixos/tests/matrix-conduit.nix
new file mode 100644
index 00000000000..d159fbaa480
--- /dev/null
+++ b/nixos/tests/matrix-conduit.nix
@@ -0,0 +1,95 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+  let
+    name = "conduit";
+  in
+  {
+    nodes = {
+      conduit = args: {
+        services.matrix-conduit = {
+          enable = true;
+          settings.global.server_name = name;
+          settings.global.allow_registration = true;
+          extraEnvironment.RUST_BACKTRACE = "yes";
+        };
+        services.nginx = {
+          enable = true;
+          virtualHosts.${name} = {
+            enableACME = false;
+            forceSSL = false;
+            enableSSL = false;
+
+            locations."/_matrix" = {
+              proxyPass = "http://[::1]:6167";
+            };
+          };
+        };
+        networking.firewall.allowedTCPPorts = [ 80 ];
+      };
+      client = { pkgs, ... }: {
+        environment.systemPackages = [
+          (
+            pkgs.writers.writePython3Bin "do_test"
+              { libraries = [ pkgs.python3Packages.matrix-nio ]; } ''
+              import asyncio
+
+              from nio import AsyncClient
+
+
+              async def main() -> None:
+                  # Connect to conduit
+                  client = AsyncClient("http://conduit:80", "alice")
+
+                  # Register as user alice
+                  response = await client.register("alice", "my-secret-password")
+
+                  # Log in as user alice
+                  response = await client.login("my-secret-password")
+
+                  # Create a new room
+                  response = await client.room_create(federate=False)
+                  room_id = response.room_id
+
+                  # Join the room
+                  response = await client.join(room_id)
+
+                  # Send a message to the room
+                  response = await client.room_send(
+                      room_id=room_id,
+                      message_type="m.room.message",
+                      content={
+                          "msgtype": "m.text",
+                          "body": "Hello conduit!"
+                      }
+                  )
+
+                  # Sync responses
+                  response = await client.sync(timeout=30000)
+
+                  # Check the message was received by conduit
+                  last_message = response.rooms.join[room_id].timeline.events[-1].body
+                  assert last_message == "Hello conduit!"
+
+                  # Leave the room
+                  response = await client.room_leave(room_id)
+
+                  # Close the client
+                  await client.close()
+
+              asyncio.get_event_loop().run_until_complete(main())
+            ''
+          )
+        ];
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      with subtest("start conduit"):
+            conduit.wait_for_unit("conduit.service")
+            conduit.wait_for_open_port(80)
+
+      with subtest("ensure messages can be exchanged"):
+            client.succeed("do_test")
+    '';
+  })
diff --git a/nixos/tests/matrix-synapse.nix b/nixos/tests/matrix-synapse.nix
new file mode 100644
index 00000000000..1ff1e47b284
--- /dev/null
+++ b/nixos/tests/matrix-synapse.nix
@@ -0,0 +1,221 @@
+import ./make-test-python.nix ({ pkgs, ... } : let
+
+
+  runWithOpenSSL = file: cmd: pkgs.runCommand file {
+    buildInputs = [ pkgs.openssl ];
+  } cmd;
+
+
+  ca_key = runWithOpenSSL "ca-key.pem" "openssl genrsa -out $out 2048";
+  ca_pem = runWithOpenSSL "ca.pem" ''
+    openssl req \
+      -x509 -new -nodes -key ${ca_key} \
+      -days 10000 -out $out -subj "/CN=snakeoil-ca"
+  '';
+  key = runWithOpenSSL "matrix_key.pem" "openssl genrsa -out $out 2048";
+  csr = runWithOpenSSL "matrix.csr" ''
+    openssl req \
+       -new -key ${key} \
+       -out $out -subj "/CN=localhost" \
+  '';
+  cert = runWithOpenSSL "matrix_cert.pem" ''
+    openssl x509 \
+      -req -in ${csr} \
+      -CA ${ca_pem} -CAkey ${ca_key} \
+      -CAcreateserial -out $out \
+      -days 365
+  '';
+
+
+  mailerCerts = import ./common/acme/server/snakeoil-certs.nix;
+  mailerDomain = mailerCerts.domain;
+  registrationSharedSecret = "unsecure123";
+  testUser = "alice";
+  testPassword = "alicealice";
+  testEmail = "alice@example.com";
+
+  listeners = [ {
+    port = 8448;
+    bind_addresses = [
+      "127.0.0.1"
+      "::1"
+    ];
+    type = "http";
+    tls = true;
+    x_forwarded = false;
+    resources = [ {
+      names = [
+        "client"
+      ];
+      compress = true;
+    } {
+      names = [
+        "federation"
+      ];
+      compress = false;
+    } ];
+  } ];
+
+in {
+
+  name = "matrix-synapse";
+  meta = with pkgs.lib; {
+    maintainers = teams.matrix.members;
+  };
+
+  nodes = {
+    # Since 0.33.0, matrix-synapse doesn't allow underscores in server names
+    serverpostgres = { pkgs, nodes, ... }: let
+      mailserverIP = nodes.mailserver.config.networking.primaryIPAddress;
+    in
+    {
+      services.matrix-synapse = {
+        enable = true;
+        settings = {
+          inherit listeners;
+          database = {
+            name = "psycopg2";
+            args.password = "synapse";
+          };
+          tls_certificate_path = "${cert}";
+          tls_private_key_path = "${key}";
+          registration_shared_secret = registrationSharedSecret;
+          public_baseurl = "https://example.com";
+          email = {
+            smtp_host = mailerDomain;
+            smtp_port = 25;
+            require_transport_security = true;
+            notif_from = "matrix <matrix@${mailerDomain}>";
+            app_name = "Matrix";
+          };
+        };
+      };
+      services.postgresql = {
+        enable = true;
+
+        # The database name and user are configured by the following options:
+        #   - services.matrix-synapse.database_name
+        #   - services.matrix-synapse.database_user
+        #
+        # The values used here represent the default values of the module.
+        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";
+        '';
+      };
+
+      networking.extraHosts = ''
+        ${mailserverIP} ${mailerDomain}
+      '';
+
+      security.pki.certificateFiles = [
+        mailerCerts.ca.cert ca_pem
+      ];
+
+      environment.systemPackages = let
+        sendTestMailStarttls = pkgs.writeScriptBin "send-testmail-starttls" ''
+          #!${pkgs.python3.interpreter}
+          import smtplib
+          import ssl
+
+          ctx = ssl.create_default_context()
+
+          with smtplib.SMTP('${mailerDomain}') as smtp:
+            smtp.ehlo()
+            smtp.starttls(context=ctx)
+            smtp.ehlo()
+            smtp.sendmail('matrix@${mailerDomain}', '${testEmail}', 'Subject: Test STARTTLS\n\nTest data.')
+            smtp.quit()
+         '';
+
+        obtainTokenAndRegisterEmail = let
+          # adding the email through the API is quite complicated as it involves more than one step and some
+          # client-side calculation
+          insertEmailForAlice = pkgs.writeText "alice-email.sql" ''
+            INSERT INTO user_threepids (user_id, medium, address, validated_at, added_at) VALUES ('${testUser}@serverpostgres', 'email', '${testEmail}', '1629149927271', '1629149927270');
+          '';
+        in
+        pkgs.writeScriptBin "obtain-token-and-register-email" ''
+          #!${pkgs.runtimeShell}
+          set -o errexit
+          set -o pipefail
+          set -o nounset
+          su postgres -c "psql -d matrix-synapse -f ${insertEmailForAlice}"
+          curl --fail -XPOST 'https://localhost:8448/_matrix/client/r0/account/password/email/requestToken' -d '{"email":"${testEmail}","client_secret":"foobar","send_attempt":1}' -v
+        '';
+        in [ sendTestMailStarttls pkgs.matrix-synapse obtainTokenAndRegisterEmail ];
+    };
+
+    # test mail delivery
+    mailserver = args: let
+    in
+    {
+      security.pki.certificateFiles = [
+        mailerCerts.ca.cert
+      ];
+
+      networking.firewall.enable = false;
+
+      services.postfix = {
+        enable = true;
+        hostname = "${mailerDomain}";
+        # open relay for subnet
+        networksStyle = "subnet";
+        enableSubmission = true;
+        tlsTrustedAuthorities = "${mailerCerts.ca.cert}";
+        sslCert = "${mailerCerts.${mailerDomain}.cert}";
+        sslKey = "${mailerCerts.${mailerDomain}.key}";
+
+        # blackhole transport
+        transport = "example.com discard:silently";
+
+        config = {
+          debug_peer_level = "10";
+          smtpd_relay_restrictions = [
+            "permit_mynetworks" "reject_unauth_destination"
+          ];
+
+          # disable obsolete protocols, something old versions of twisted are still using
+          smtpd_tls_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+          smtp_tls_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+          smtpd_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+          smtp_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+        };
+      };
+    };
+
+    serversqlite = args: {
+      services.matrix-synapse = {
+        enable = true;
+        settings = {
+          inherit listeners;
+          database.name = "sqlite3";
+          tls_certificate_path = "${cert}";
+          tls_private_key_path = "${key}";
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    mailserver.wait_for_unit("postfix.service")
+    serverpostgres.succeed("send-testmail-starttls")
+    serverpostgres.wait_for_unit("matrix-synapse.service")
+    serverpostgres.wait_until_succeeds(
+        "curl --fail -L --cacert ${ca_pem} https://localhost:8448/"
+    )
+    serverpostgres.require_unit_state("postgresql.service")
+    serverpostgres.succeed("register_new_matrix_user -u ${testUser} -p ${testPassword} -a -k ${registrationSharedSecret} ")
+    serverpostgres.succeed("obtain-token-and-register-email")
+    serversqlite.wait_for_unit("matrix-synapse.service")
+    serversqlite.wait_until_succeeds(
+        "curl --fail -L --cacert ${ca_pem} https://localhost:8448/"
+    )
+    serversqlite.succeed("[ -e /var/lib/matrix-synapse/homeserver.db ]")
+  '';
+
+})
diff --git a/nixos/tests/matrix/mjolnir.nix b/nixos/tests/matrix/mjolnir.nix
new file mode 100644
index 00000000000..54094ab9d61
--- /dev/null
+++ b/nixos/tests/matrix/mjolnir.nix
@@ -0,0 +1,170 @@
+import ../make-test-python.nix (
+  { pkgs, ... }:
+  let
+    # Set up SSL certs for Synapse to be happy.
+    runWithOpenSSL = file: cmd: pkgs.runCommand file
+      {
+        buildInputs = [ pkgs.openssl ];
+      }
+      cmd;
+
+    ca_key = runWithOpenSSL "ca-key.pem" "openssl genrsa -out $out 2048";
+    ca_pem = runWithOpenSSL "ca.pem" ''
+      openssl req \
+        -x509 -new -nodes -key ${ca_key} \
+        -days 10000 -out $out -subj "/CN=snakeoil-ca"
+    '';
+    key = runWithOpenSSL "matrix_key.pem" "openssl genrsa -out $out 2048";
+    csr = runWithOpenSSL "matrix.csr" ''
+      openssl req \
+         -new -key ${key} \
+         -out $out -subj "/CN=localhost" \
+    '';
+    cert = runWithOpenSSL "matrix_cert.pem" ''
+      openssl x509 \
+        -req -in ${csr} \
+        -CA ${ca_pem} -CAkey ${ca_key} \
+        -CAcreateserial -out $out \
+        -days 365
+    '';
+  in
+  {
+    name = "mjolnir";
+    meta = with pkgs.lib; {
+      maintainers = teams.matrix.members;
+    };
+
+    nodes = {
+      homeserver = { pkgs, ... }: {
+        services.matrix-synapse = {
+          enable = true;
+          settings = {
+            database.name = "sqlite3";
+            tls_certificate_path = "${cert}";
+            tls_private_key_path = "${key}";
+            enable_registration = true;
+            registration_shared_secret = "supersecret-registration";
+
+            listeners = [ {
+              # The default but tls=false
+              bind_addresses = [
+                "0.0.0.0"
+              ];
+              port = 8448;
+              resources = [ {
+                compress = true;
+                names = [ "client" ];
+              } {
+                compress = false;
+                names = [ "federation" ];
+              } ];
+              tls = false;
+              type = "http";
+              x_forwarded = false;
+            } ];
+          };
+        };
+
+        networking.firewall.allowedTCPPorts = [ 8448 ];
+
+        environment.systemPackages = [
+          (pkgs.writeShellScriptBin "register_mjolnir_user" ''
+            exec ${pkgs.matrix-synapse}/bin/register_new_matrix_user \
+              -u mjolnir \
+              -p mjolnir-password \
+              --admin \
+              --shared-secret supersecret-registration \
+              http://localhost:8448
+          ''
+          )
+          (pkgs.writeShellScriptBin "register_moderator_user" ''
+            exec ${pkgs.matrix-synapse}/bin/register_new_matrix_user \
+              -u moderator \
+              -p moderator-password \
+              --no-admin \
+              --shared-secret supersecret-registration \
+              http://localhost:8448
+          ''
+          )
+        ];
+      };
+
+      mjolnir = { pkgs, ... }: {
+        services.mjolnir = {
+          enable = true;
+          homeserverUrl = "http://homeserver:8448";
+          pantalaimon = {
+            enable = true;
+            username = "mjolnir";
+            passwordFile = pkgs.writeText "password.txt" "mjolnir-password";
+          };
+          managementRoom = "#moderators:homeserver";
+        };
+      };
+
+      client = { pkgs, ... }: {
+        environment.systemPackages = [
+          (pkgs.writers.writePython3Bin "create_management_room_and_invite_mjolnir"
+            { libraries = [ pkgs.python3Packages.matrix-nio ]; } ''
+            import asyncio
+
+            from nio import (
+                AsyncClient,
+                EnableEncryptionBuilder
+            )
+
+
+            async def main() -> None:
+                client = AsyncClient("http://homeserver:8448", "moderator")
+
+                await client.login("moderator-password")
+
+                room = await client.room_create(
+                    name="Moderators",
+                    alias="moderators",
+                    initial_state=[EnableEncryptionBuilder().as_dict()],
+                )
+
+                await client.join(room.room_id)
+                await client.room_invite(room.room_id, "@mjolnir:homeserver")
+
+            asyncio.run(main())
+          ''
+          )
+        ];
+      };
+    };
+
+    testScript = ''
+      with subtest("start homeserver"):
+        homeserver.start()
+
+        homeserver.wait_for_unit("matrix-synapse.service")
+        homeserver.wait_until_succeeds("curl --fail -L http://localhost:8448/")
+
+      with subtest("register users"):
+        # register mjolnir user
+        homeserver.succeed("register_mjolnir_user")
+        # register moderator user
+        homeserver.succeed("register_moderator_user")
+
+      with subtest("start mjolnir"):
+        mjolnir.start()
+
+        # wait for pantalaimon to be ready
+        mjolnir.wait_for_unit("pantalaimon-mjolnir.service")
+        mjolnir.wait_for_unit("mjolnir.service")
+
+        mjolnir.wait_until_succeeds("curl --fail -L http://localhost:8009/")
+
+      with subtest("ensure mjolnir can be invited to the management room"):
+        client.start()
+
+        client.wait_until_succeeds("curl --fail -L http://homeserver:8448/")
+
+        client.succeed("create_management_room_and_invite_mjolnir")
+
+        mjolnir.wait_for_console_text("Startup complete. Now monitoring rooms")
+    '';
+  }
+)
diff --git a/nixos/tests/matrix/pantalaimon.nix b/nixos/tests/matrix/pantalaimon.nix
new file mode 100644
index 00000000000..1a9894dd215
--- /dev/null
+++ b/nixos/tests/matrix/pantalaimon.nix
@@ -0,0 +1,88 @@
+import ../make-test-python.nix (
+  { pkgs, ... }:
+  let
+    pantalaimonInstanceName = "testing";
+
+    # Set up SSL certs for Synapse to be happy.
+    runWithOpenSSL = file: cmd: pkgs.runCommand file
+      {
+        buildInputs = [ pkgs.openssl ];
+      }
+      cmd;
+
+    ca_key = runWithOpenSSL "ca-key.pem" "openssl genrsa -out $out 2048";
+    ca_pem = runWithOpenSSL "ca.pem" ''
+      openssl req \
+        -x509 -new -nodes -key ${ca_key} \
+        -days 10000 -out $out -subj "/CN=snakeoil-ca"
+    '';
+    key = runWithOpenSSL "matrix_key.pem" "openssl genrsa -out $out 2048";
+    csr = runWithOpenSSL "matrix.csr" ''
+      openssl req \
+         -new -key ${key} \
+         -out $out -subj "/CN=localhost" \
+    '';
+    cert = runWithOpenSSL "matrix_cert.pem" ''
+      openssl x509 \
+        -req -in ${csr} \
+        -CA ${ca_pem} -CAkey ${ca_key} \
+        -CAcreateserial -out $out \
+        -days 365
+    '';
+  in
+  {
+    name = "pantalaimon";
+    meta = with pkgs.lib; {
+      maintainers = teams.matrix.members;
+    };
+
+    machine = { pkgs, ... }: {
+      services.pantalaimon-headless.instances.${pantalaimonInstanceName} = {
+        homeserver = "https://localhost:8448";
+        listenAddress = "0.0.0.0";
+        listenPort = 8888;
+        logLevel = "debug";
+        ssl = false;
+      };
+
+      services.matrix-synapse = {
+        enable = true;
+        settings = {
+          listeners = [ {
+            port = 8448;
+            bind_addresses = [
+              "127.0.0.1"
+              "::1"
+            ];
+            type = "http";
+            tls = true;
+            x_forwarded = false;
+            resources = [ {
+              names = [
+                "client"
+              ];
+              compress = true;
+            } {
+              names = [
+                "federation"
+              ];
+              compress = false;
+            } ];
+          } ];
+          database.name = "sqlite3";
+          tls_certificate_path = "${cert}";
+          tls_private_key_path = "${key}";
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+      machine.wait_for_unit("pantalaimon-${pantalaimonInstanceName}.service")
+      machine.wait_for_unit("matrix-synapse.service")
+      machine.wait_until_succeeds(
+          "curl --fail -L http://localhost:8888/"
+      )
+    '';
+  }
+)
diff --git a/nixos/tests/mattermost.nix b/nixos/tests/mattermost.nix
new file mode 100644
index 00000000000..49b418d9fff
--- /dev/null
+++ b/nixos/tests/mattermost.nix
@@ -0,0 +1,124 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+let
+  host = "smoke.test";
+  port = "8065";
+  url = "http://${host}:${port}";
+  siteName = "NixOS Smoke Tests, Inc.";
+
+  makeMattermost = mattermostConfig:
+    { config, ... }: {
+      environment.systemPackages = [
+        pkgs.mattermost
+        pkgs.curl
+        pkgs.jq
+      ];
+      networking.hosts = {
+        "127.0.0.1" = [ host ];
+      };
+      services.mattermost = lib.recursiveUpdate {
+        enable = true;
+        inherit siteName;
+        listenAddress = "0.0.0.0:${port}";
+        siteUrl = url;
+        extraConfig = {
+          SupportSettings.AboutLink = "https://nixos.org";
+        };
+      } mattermostConfig;
+    };
+in
+{
+  name = "mattermost";
+
+  nodes = {
+    mutable = makeMattermost {
+      mutableConfig = true;
+      extraConfig.SupportSettings.HelpLink = "https://search.nixos.org";
+    };
+    mostlyMutable = makeMattermost {
+      mutableConfig = true;
+      preferNixConfig = true;
+      plugins = let
+        mattermostDemoPlugin = pkgs.fetchurl {
+          url = "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.9.0/com.mattermost.demo-plugin-0.9.0.tar.gz";
+          sha256 = "1h4qi34gcxcx63z8wiqcf2aaywmvv8lys5g8gvsk13kkqhlmag25";
+        };
+      in [
+        mattermostDemoPlugin
+      ];
+    };
+    immutable = makeMattermost {
+      mutableConfig = false;
+      extraConfig.SupportSettings.HelpLink = "https://search.nixos.org";
+    };
+  };
+
+  testScript = let
+    expectConfig = jqExpression: pkgs.writeShellScript "expect-config" ''
+      set -euo pipefail
+      echo "Expecting config to match: "${lib.escapeShellArg jqExpression} >&2
+      curl ${lib.escapeShellArg url} >/dev/null
+      config="$(curl ${lib.escapeShellArg "${url}/api/v4/config/client?format=old"})"
+      echo "Config: $(echo "$config" | ${pkgs.jq}/bin/jq)" >&2
+      [[ "$(echo "$config" | ${pkgs.jq}/bin/jq -r ${lib.escapeShellArg ".SiteName == $siteName and .Version == ($mattermostName / $sep)[-1] and (${jqExpression})"} --arg siteName ${lib.escapeShellArg siteName} --arg mattermostName ${lib.escapeShellArg pkgs.mattermost.name} --arg sep '-')" = "true" ]]
+    '';
+
+    setConfig = jqExpression: pkgs.writeShellScript "set-config" ''
+      set -euo pipefail
+      mattermostConfig=/var/lib/mattermost/config/config.json
+      newConfig="$(${pkgs.jq}/bin/jq -r ${lib.escapeShellArg jqExpression} $mattermostConfig)"
+      rm -f $mattermostConfig
+      echo "$newConfig" > "$mattermostConfig"
+    '';
+  in
+  ''
+    start_all()
+
+    ## Mutable node tests ##
+    mutable.wait_for_unit("mattermost.service")
+    mutable.wait_for_open_port(8065)
+
+    # Get the initial config
+    mutable.succeed("${expectConfig ''.AboutLink == "https://nixos.org" and .HelpLink == "https://search.nixos.org"''}")
+
+    # Edit the config
+    mutable.succeed("${setConfig ''.SupportSettings.AboutLink = "https://mattermost.com"''}")
+    mutable.succeed("${setConfig ''.SupportSettings.HelpLink = "https://nixos.org/nixos/manual"''}")
+    mutable.systemctl("restart mattermost.service")
+    mutable.wait_for_open_port(8065)
+
+    # AboutLink and HelpLink should be changed
+    mutable.succeed("${expectConfig ''.AboutLink == "https://mattermost.com" and .HelpLink == "https://nixos.org/nixos/manual"''}")
+
+    ## Mostly mutable node tests ##
+    mostlyMutable.wait_for_unit("mattermost.service")
+    mostlyMutable.wait_for_open_port(8065)
+
+    # Get the initial config
+    mostlyMutable.succeed("${expectConfig ''.AboutLink == "https://nixos.org"''}")
+
+    # Edit the config
+    mostlyMutable.succeed("${setConfig ''.SupportSettings.AboutLink = "https://mattermost.com"''}")
+    mostlyMutable.succeed("${setConfig ''.SupportSettings.HelpLink = "https://nixos.org/nixos/manual"''}")
+    mostlyMutable.systemctl("restart mattermost.service")
+    mostlyMutable.wait_for_open_port(8065)
+
+    # AboutLink should be overridden by NixOS configuration; HelpLink should be what we set above
+    mostlyMutable.succeed("${expectConfig ''.AboutLink == "https://nixos.org" and .HelpLink == "https://nixos.org/nixos/manual"''}")
+
+    ## Immutable node tests ##
+    immutable.wait_for_unit("mattermost.service")
+    immutable.wait_for_open_port(8065)
+
+    # Get the initial config
+    immutable.succeed("${expectConfig ''.AboutLink == "https://nixos.org" and .HelpLink == "https://search.nixos.org"''}")
+
+    # Edit the config
+    immutable.succeed("${setConfig ''.SupportSettings.AboutLink = "https://mattermost.com"''}")
+    immutable.succeed("${setConfig ''.SupportSettings.HelpLink = "https://nixos.org/nixos/manual"''}")
+    immutable.systemctl("restart mattermost.service")
+    immutable.wait_for_open_port(8065)
+
+    # Our edits should be ignored on restart
+    immutable.succeed("${expectConfig ''.AboutLink == "https://nixos.org" and .HelpLink == "https://search.nixos.org"''}")
+  '';
+})
diff --git a/nixos/tests/mediatomb.nix b/nixos/tests/mediatomb.nix
new file mode 100644
index 00000000000..b7a126a01ad
--- /dev/null
+++ b/nixos/tests/mediatomb.nix
@@ -0,0 +1,81 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "mediatomb";
+
+  nodes = {
+    serverGerbera =
+      { ... }:
+      let port = 49152;
+      in {
+        imports = [ ../modules/profiles/minimal.nix ];
+        services.mediatomb = {
+          enable = true;
+          serverName = "Gerbera";
+          package = pkgs.gerbera;
+          interface = "eth1";  # accessible from test
+          openFirewall = true;
+          mediaDirectories = [
+            { path = "/var/lib/gerbera/pictures"; recursive = false; hidden-files = false; }
+            { path = "/var/lib/gerbera/audio"; recursive = true; hidden-files = false; }
+          ];
+        };
+      };
+
+    serverMediatomb =
+      { ... }:
+      let port = 49151;
+      in {
+        imports = [ ../modules/profiles/minimal.nix ];
+        services.mediatomb = {
+          enable = true;
+          serverName = "Mediatomb";
+          package = pkgs.mediatomb;
+          interface = "eth1";
+          inherit port;
+          mediaDirectories = [
+            { path = "/var/lib/mediatomb/pictures"; recursive = false; hidden-files = false; }
+            { path = "/var/lib/mediatomb/audio"; recursive = true; hidden-files = false; }
+          ];
+        };
+        networking.firewall.interfaces.eth1 = {
+          allowedUDPPorts = [ 1900 port ];
+          allowedTCPPorts = [ port ];
+        };
+      };
+
+      client = { ... }: { };
+  };
+
+  testScript =
+  ''
+    start_all()
+
+    port = 49151
+    serverMediatomb.succeed("mkdir -p /var/lib/mediatomb/{pictures,audio}")
+    serverMediatomb.succeed("chown -R mediatomb:mediatomb /var/lib/mediatomb")
+    serverMediatomb.wait_for_unit("mediatomb")
+    serverMediatomb.wait_for_open_port(port)
+    serverMediatomb.succeed(f"curl --fail http://serverMediatomb:{port}/")
+    page = client.succeed(f"curl --fail http://serverMediatomb:{port}/")
+    assert "MediaTomb" in page and "Gerbera" not in page
+    serverMediatomb.shutdown()
+
+    port = 49152
+    serverGerbera.succeed("mkdir -p /var/lib/mediatomb/{pictures,audio}")
+    serverGerbera.succeed("chown -R mediatomb:mediatomb /var/lib/mediatomb")
+    # service running gerbera fails the first time claiming something is already bound
+    # gerbera[715]: 2020-07-18 23:52:14   info: Please check if another instance of Gerbera or
+    # gerbera[715]: 2020-07-18 23:52:14   info: another application is running on port TCP 49152 or UDP 1900.
+    # I did not find anything so here I work around this
+    serverGerbera.succeed("sleep 2")
+    serverGerbera.wait_until_succeeds("systemctl restart mediatomb")
+    serverGerbera.wait_for_unit("mediatomb")
+    serverGerbera.succeed(f"curl --fail http://serverGerbera:{port}/")
+    page = client.succeed(f"curl --fail http://serverGerbera:{port}/")
+    assert "Gerbera" in page and "MediaTomb" not in page
+
+    serverGerbera.shutdown()
+    client.shutdown()
+  '';
+})
diff --git a/nixos/tests/mediawiki.nix b/nixos/tests/mediawiki.nix
new file mode 100644
index 00000000000..702fefefa16
--- /dev/null
+++ b/nixos/tests/mediawiki.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "mediawiki";
+  meta.maintainers = [ lib.maintainers.aanderse ];
+
+  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;
+      };
+    };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("phpfpm-mediawiki.service")
+
+    page = machine.succeed("curl -fL http://localhost/")
+    assert "MediaWiki has been installed" in page
+  '';
+})
diff --git a/nixos/tests/meilisearch.nix b/nixos/tests/meilisearch.nix
new file mode 100644
index 00000000000..c379bda74c5
--- /dev/null
+++ b/nixos/tests/meilisearch.nix
@@ -0,0 +1,60 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+  let
+    listenAddress = "127.0.0.1";
+    listenPort = 7700;
+    apiUrl = "http://${listenAddress}:${toString listenPort}";
+    uid = "movies";
+    indexJSON = pkgs.writeText "index.json" (builtins.toJSON { inherit uid; });
+    moviesJSON = pkgs.runCommand "movies.json" {} ''
+      sed -n '1,5p;$p' ${pkgs.meilisearch.src}/datasets/movies/movies.json > $out
+    '';
+  in {
+    name = "meilisearch";
+    meta.maintainers = with lib.maintainers; [ Br1ght0ne ];
+
+    machine = { ... }: {
+      environment.systemPackages = with pkgs; [ curl jq ];
+      services.meilisearch = {
+        enable = true;
+        inherit listenAddress listenPort;
+      };
+    };
+
+    testScript = ''
+      import json
+
+      start_all()
+
+      machine.wait_for_unit("meilisearch")
+      machine.wait_for_open_port("7700")
+
+      with subtest("check version"):
+          version = json.loads(machine.succeed("curl ${apiUrl}/version"))
+          assert version["pkgVersion"] == "${pkgs.meilisearch.version}"
+
+      with subtest("create index"):
+          machine.succeed(
+              "curl -XPOST ${apiUrl}/indexes --data @${indexJSON}"
+          )
+          indexes = json.loads(machine.succeed("curl ${apiUrl}/indexes"))
+          assert len(indexes) == 1, "index wasn't created"
+
+      with subtest("add documents"):
+          response = json.loads(
+              machine.succeed(
+                  "curl -XPOST ${apiUrl}/indexes/${uid}/documents --data @${moviesJSON}"
+              )
+          )
+          update_id = response["updateId"]
+          machine.wait_until_succeeds(
+              f"curl ${apiUrl}/indexes/${uid}/updates/{update_id} | jq -e '.status == \"processed\"'"
+          )
+
+      with subtest("search"):
+          response = json.loads(
+              machine.succeed("curl ${apiUrl}/indexes/movies/search?q=hero")
+          )
+          print(response)
+          assert len(response["hits"]) >= 1, "no results found"
+    '';
+  })
diff --git a/nixos/tests/memcached.nix b/nixos/tests/memcached.nix
new file mode 100644
index 00000000000..31f5627d25c
--- /dev/null
+++ b/nixos/tests/memcached.nix
@@ -0,0 +1,24 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "memcached";
+
+  machine = {
+    imports = [ ../modules/profiles/minimal.nix ];
+    services.memcached.enable = true;
+  };
+
+  testScript = let
+    testScript = pkgs.writers.writePython3 "test_memcache" {
+      libraries = with pkgs.python3Packages; [ memcached ];
+    } ''
+      import memcache
+      c = memcache.Client(['localhost:11211'])
+      c.set('key', 'value')
+      assert 'value' == c.get('key')
+    '';
+  in ''
+    machine.start()
+    machine.wait_for_unit("memcached.service")
+    machine.wait_for_open_port(11211)
+    machine.succeed("${testScript}")
+  '';
+})
diff --git a/nixos/tests/metabase.nix b/nixos/tests/metabase.nix
new file mode 100644
index 00000000000..1b25071902e
--- /dev/null
+++ b/nixos/tests/metabase.nix
@@ -0,0 +1,19 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "metabase";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mmahut ];
+  };
+
+  nodes = {
+    machine = { ... }: {
+      services.metabase.enable = true;
+    };
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("metabase.service")
+    machine.wait_for_open_port(3000)
+    machine.wait_until_succeeds("curl -fL http://localhost:3000/setup | grep Metabase")
+  '';
+})
diff --git a/nixos/tests/minecraft-server.nix b/nixos/tests/minecraft-server.nix
new file mode 100644
index 00000000000..dbe2cd6d56f
--- /dev/null
+++ b/nixos/tests/minecraft-server.nix
@@ -0,0 +1,37 @@
+let
+  seed = "2151901553968352745";
+  rcon-pass = "foobar";
+  rcon-port = 43000;
+in import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "minecraft-server";
+  meta = with pkgs.lib.maintainers; { maintainers = [ nequissimus ]; };
+
+  nodes.server = { ... }: {
+    environment.systemPackages = [ pkgs.mcrcon ];
+
+    nixpkgs.config.allowUnfree = true;
+
+    services.minecraft-server = {
+      declarative = true;
+      enable = true;
+      eula = true;
+      serverProperties = {
+        enable-rcon = true;
+        level-seed = seed;
+        online-mode = false;
+        "rcon.password" = rcon-pass;
+        "rcon.port" = rcon-port;
+      };
+    };
+
+    virtualisation.memorySize = 2047;
+  };
+
+  testScript = ''
+    server.wait_for_unit("minecraft-server")
+    server.wait_for_open_port(${toString rcon-port})
+    assert "${seed}" in server.succeed(
+        "mcrcon -H localhost -P ${toString rcon-port} -p '${rcon-pass}' -c 'seed'"
+    )
+  '';
+})
diff --git a/nixos/tests/minecraft.nix b/nixos/tests/minecraft.nix
new file mode 100644
index 00000000000..1c34f04b4df
--- /dev/null
+++ b/nixos/tests/minecraft.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "minecraft";
+  meta = with lib.maintainers; { maintainers = [ nequissimus ]; };
+
+  nodes.client = { nodes, ... }:
+      let user = nodes.client.config.users.users.alice;
+      in {
+        imports = [ ./common/user-account.nix ./common/x11.nix ];
+
+        environment.systemPackages = [ pkgs.minecraft ];
+
+        nixpkgs.config.allowUnfree = true;
+
+        test-support.displayManager.auto.user = user.name;
+      };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }:
+    let user = nodes.client.config.users.users.alice;
+    in ''
+      client.wait_for_x()
+      client.execute("su - alice -c minecraft-launcher >&2 &")
+      client.wait_for_text("Create a new Microsoft account")
+      client.sleep(10)
+      client.screenshot("launcher")
+    '';
+})
diff --git a/nixos/tests/minidlna.nix b/nixos/tests/minidlna.nix
new file mode 100644
index 00000000000..104b79078fd
--- /dev/null
+++ b/nixos/tests/minidlna.nix
@@ -0,0 +1,41 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "minidlna";
+
+  nodes = {
+    server =
+      { ... }:
+      {
+        imports = [ ../modules/profiles/minimal.nix ];
+        networking.firewall.allowedTCPPorts = [ 8200 ];
+        services.minidlna = {
+          enable = true;
+          loglevel = "error";
+          mediaDirs = [
+           "PV,/tmp/stuff"
+          ];
+          friendlyName = "rpi3";
+          rootContainer = "B";
+          extraConfig =
+          ''
+            album_art_names=Cover.jpg/cover.jpg/AlbumArtSmall.jpg/albumartsmall.jpg
+            album_art_names=AlbumArt.jpg/albumart.jpg/Album.jpg/album.jpg
+            album_art_names=Folder.jpg/folder.jpg/Thumb.jpg/thumb.jpg
+            notify_interval=60
+          '';
+        };
+      };
+      client = { ... }: { };
+  };
+
+  testScript =
+  ''
+    start_all()
+    server.succeed("mkdir -p /tmp/stuff && chown minidlna: /tmp/stuff")
+    server.wait_for_unit("minidlna")
+    server.wait_for_open_port("8200")
+    # requests must be made *by IP* to avoid triggering minidlna's
+    # DNS-rebinding protection
+    server.succeed("curl --fail http://$(getent ahostsv4 localhost | head -n1 | cut -f 1 -d ' '):8200/")
+    client.succeed("curl --fail http://$(getent ahostsv4 server | head -n1 | cut -f 1 -d ' '):8200/")
+  '';
+})
diff --git a/nixos/tests/miniflux.nix b/nixos/tests/miniflux.nix
new file mode 100644
index 00000000000..d905aea048a
--- /dev/null
+++ b/nixos/tests/miniflux.nix
@@ -0,0 +1,82 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+let
+  port = 3142;
+  username = "alice";
+  password = "correcthorsebatterystaple";
+  defaultPort = 8080;
+  defaultUsername = "admin";
+  defaultPassword = "password";
+  adminCredentialsFile = pkgs.writeText "admin-credentials" ''
+            ADMIN_USERNAME=${defaultUsername}
+            ADMIN_PASSWORD=${defaultPassword}
+          '';
+  customAdminCredentialsFile = pkgs.writeText "admin-credentials" ''
+            ADMIN_USERNAME=${username}
+            ADMIN_PASSWORD=${password}
+          '';
+
+in
+with lib;
+{
+  name = "miniflux";
+  meta.maintainers = with pkgs.lib.maintainers; [ ];
+
+  nodes = {
+    default =
+      { ... }:
+      {
+        services.miniflux = {
+          enable = true;
+          inherit adminCredentialsFile;
+        };
+      };
+
+    withoutSudo =
+      { ... }:
+      {
+        services.miniflux = {
+          enable = true;
+          inherit adminCredentialsFile;
+        };
+        security.sudo.enable = false;
+      };
+
+    customized =
+      { ... }:
+      {
+        services.miniflux = {
+          enable = true;
+          config = {
+            CLEANUP_FREQUENCY = "48";
+            LISTEN_ADDR = "localhost:${toString port}";
+          };
+          adminCredentialsFile = customAdminCredentialsFile;
+        };
+      };
+  };
+  testScript = ''
+    start_all()
+
+    default.wait_for_unit("miniflux.service")
+    default.wait_for_open_port(${toString defaultPort})
+    default.succeed("curl --fail 'http://localhost:${toString defaultPort}/healthcheck' | grep OK")
+    default.succeed(
+        "curl 'http://localhost:${toString defaultPort}/v1/me' -u '${defaultUsername}:${defaultPassword}' -H Content-Type:application/json | grep '\"is_admin\":true'"
+    )
+
+    withoutSudo.wait_for_unit("miniflux.service")
+    withoutSudo.wait_for_open_port(${toString defaultPort})
+    withoutSudo.succeed("curl --fail 'http://localhost:${toString defaultPort}/healthcheck' | grep OK")
+    withoutSudo.succeed(
+        "curl 'http://localhost:${toString defaultPort}/v1/me' -u '${defaultUsername}:${defaultPassword}' -H Content-Type:application/json | grep '\"is_admin\":true'"
+    )
+
+    customized.wait_for_unit("miniflux.service")
+    customized.wait_for_open_port(${toString port})
+    customized.succeed("curl --fail 'http://localhost:${toString port}/healthcheck' | grep OK")
+    customized.succeed(
+        "curl 'http://localhost:${toString port}/v1/me' -u '${username}:${password}' -H Content-Type:application/json | grep '\"is_admin\":true'"
+    )
+  '';
+})
diff --git a/nixos/tests/minio.nix b/nixos/tests/minio.nix
new file mode 100644
index 00000000000..ad51f738d49
--- /dev/null
+++ b/nixos/tests/minio.nix
@@ -0,0 +1,58 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+let
+    accessKey = "BKIKJAA5BMMU2RHO6IBB";
+    secretKey = "V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12";
+    minioPythonScript = pkgs.writeScript "minio-test.py" ''
+      #! ${pkgs.python3.withPackages(ps: [ ps.minio ])}/bin/python
+      import io
+      import os
+      from minio import Minio
+      minioClient = Minio('localhost:9000',
+                    access_key='${accessKey}',
+                    secret_key='${secretKey}',
+                    secure=False)
+      sio = io.BytesIO()
+      sio.write(b'Test from Python')
+      sio.seek(0, os.SEEK_END)
+      sio_len = sio.tell()
+      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 ];
+  };
+
+  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 ];
+
+      # 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)
+
+    # 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/misc.nix b/nixos/tests/misc.nix
new file mode 100644
index 00000000000..0587912c9a2
--- /dev/null
+++ b/nixos/tests/misc.nix
@@ -0,0 +1,163 @@
+# Miscellaneous small tests that don't warrant their own VM run.
+
+import ./make-test-python.nix ({ pkgs, ...} : rec {
+  name = "misc";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eelco ];
+  };
+
+  foo = pkgs.writeText "foo" "Hello World";
+
+  machine =
+    { lib, ... }:
+    with lib;
+    { swapDevices = mkOverride 0
+        [ { device = "/root/swapfile"; size = 128; } ];
+      environment.variables.EDITOR = mkOverride 0 "emacs";
+      documentation.nixos.enable = mkOverride 0 true;
+      systemd.tmpfiles.rules = [ "d /tmp 1777 root root 10d" ];
+      virtualisation.fileSystems = { "/tmp2" =
+        { fsType = "tmpfs";
+          options = [ "mode=1777" "noauto" ];
+        };
+        # Tests https://discourse.nixos.org/t/how-to-make-a-derivations-executables-have-the-s-permission/8555
+        "/user-mount/point" = {
+          device = "/user-mount/source";
+          fsType = "none";
+          options = [ "bind" "rw" "user" "noauto" ];
+        };
+        "/user-mount/denied-point" = {
+          device = "/user-mount/denied-source";
+          fsType = "none";
+          options = [ "bind" "rw" "noauto" ];
+        };
+      };
+      systemd.automounts = singleton
+        { wantedBy = [ "multi-user.target" ];
+          where = "/tmp2";
+        };
+      users.users.sybil = { isNormalUser = true; group = "wheel"; };
+      users.users.alice = { isNormalUser = true; };
+      security.sudo = { enable = true; wheelNeedsPassword = false; };
+      boot.kernel.sysctl."vm.swappiness" = 1;
+      boot.kernelParams = [ "vsyscall=emulate" ];
+      system.extraDependencies = [ foo ];
+    };
+
+  testScript =
+    ''
+      import json
+
+
+      def get_path_info(path):
+          result = machine.succeed(f"nix --option experimental-features nix-command path-info --json {path}")
+          parsed = json.loads(result)
+          return parsed
+
+
+      with subtest("nix-db"):
+          info = get_path_info("${foo}")
+          print(info)
+
+          if (
+              info[0]["narHash"]
+              != "sha256-BdMdnb/0eWy3EddjE83rdgzWWpQjfWPAj3zDIFMD3Ck="
+          ):
+              raise Exception("narHash not set")
+
+          if info[0]["narSize"] != 128:
+              raise Exception("narSize not set")
+
+      with subtest("nixos-version"):
+          machine.succeed("[ `nixos-version | wc -w` = 2 ]")
+
+      with subtest("nixos-rebuild"):
+          assert "NixOS module" in machine.succeed("nixos-rebuild --help")
+
+      with subtest("Sanity check for uid/gid assignment"):
+          assert "4" == machine.succeed("id -u messagebus").strip()
+          assert "4" == machine.succeed("id -g messagebus").strip()
+          assert "users:x:100:" == machine.succeed("getent group users").strip()
+
+      with subtest("Regression test for GMP aborts on QEMU."):
+          machine.succeed("expr 1 + 2")
+
+      with subtest("the swap file got created"):
+          machine.wait_for_unit("root-swapfile.swap")
+          machine.succeed("ls -l /root/swapfile | grep 134217728")
+
+      with subtest("whether kernel.poweroff_cmd is set"):
+          machine.succeed('[ -x "$(cat /proc/sys/kernel/poweroff_cmd)" ]')
+
+      with subtest("whether the io cgroupv2 controller is properly enabled"):
+          machine.succeed("grep -q '\\bio\\b' /sys/fs/cgroup/cgroup.controllers")
+
+      with subtest("whether we have a reboot record in wtmp"):
+          machine.shutdown
+          machine.wait_for_unit("multi-user.target")
+          machine.succeed("last | grep reboot >&2")
+
+      with subtest("whether we can override environment variables"):
+          machine.succeed('[ "$EDITOR" = emacs ]')
+
+      with subtest("whether hostname (and by extension nss_myhostname) works"):
+          assert "machine" == machine.succeed("hostname").strip()
+          assert "machine" == machine.succeed("hostname -s").strip()
+
+      with subtest("whether systemd-udevd automatically loads modules for our hardware"):
+          machine.succeed("systemctl start systemd-udev-settle.service")
+          machine.wait_for_unit("systemd-udev-settle.service")
+          assert "mousedev" in machine.succeed("lsmod")
+
+      with subtest("whether systemd-tmpfiles-clean works"):
+          machine.succeed(
+              "touch /tmp/foo", "systemctl start systemd-tmpfiles-clean", "[ -e /tmp/foo ]"
+          )
+          # move into the future
+          machine.succeed(
+              'date -s "@$(($(date +%s) + 1000000))"',
+              "systemctl start systemd-tmpfiles-clean",
+          )
+          machine.fail("[ -e /tmp/foo ]")
+
+      with subtest("whether automounting works"):
+          machine.fail("grep '/tmp2 tmpfs' /proc/mounts")
+          machine.succeed("touch /tmp2/x")
+          machine.succeed("grep '/tmp2 tmpfs' /proc/mounts")
+
+      with subtest(
+          "Whether mounting by a user is possible with the `user` option in fstab (#95444)"
+      ):
+          machine.succeed("mkdir -p /user-mount/source")
+          machine.succeed("touch /user-mount/source/file")
+          machine.succeed("chmod -R a+Xr /user-mount/source")
+          machine.succeed("mkdir /user-mount/point")
+          machine.succeed("chown alice:users /user-mount/point")
+          machine.succeed("su - alice -c 'mount /user-mount/point'")
+          machine.succeed("su - alice -c 'ls /user-mount/point/file'")
+      with subtest(
+          "Whether mounting by a user is denied without the `user` option in  fstab"
+      ):
+          machine.succeed("mkdir -p /user-mount/denied-source")
+          machine.succeed("touch /user-mount/denied-source/file")
+          machine.succeed("chmod -R a+Xr /user-mount/denied-source")
+          machine.succeed("mkdir /user-mount/denied-point")
+          machine.succeed("chown alice:users /user-mount/denied-point")
+          machine.fail("su - alice -c 'mount /user-mount/denied-point'")
+
+      with subtest("shell-vars"):
+          machine.succeed('[ -n "$NIX_PATH" ]')
+
+      with subtest("nix-db"):
+          machine.succeed("nix-store -qR /run/current-system | grep nixos-")
+
+      with subtest("Test sysctl"):
+          machine.wait_for_unit("systemd-sysctl.service")
+          assert "1" == machine.succeed("sysctl -ne vm.swappiness").strip()
+          machine.execute("sysctl vm.swappiness=60")
+          assert "60" == machine.succeed("sysctl -ne vm.swappiness").strip()
+
+      with subtest("Test boot parameters"):
+          assert "vsyscall=emulate" in machine.succeed("cat /proc/cmdline")
+    '';
+})
diff --git a/nixos/tests/mod_perl.nix b/nixos/tests/mod_perl.nix
new file mode 100644
index 00000000000..29a1eb6503f
--- /dev/null
+++ b/nixos/tests/mod_perl.nix
@@ -0,0 +1,53 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "mod_perl";
+
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ sgo ];
+  };
+
+  machine = { config, lib, pkgs, ... }: {
+    services.httpd = {
+      enable = true;
+      adminAddr = "admin@localhost";
+      virtualHosts."modperl" =
+        let
+          inc = pkgs.writeTextDir "ModPerlTest.pm" ''
+            package ModPerlTest;
+            use strict;
+            use Apache2::RequestRec ();
+            use Apache2::RequestIO ();
+            use Apache2::Const -compile => qw(OK);
+            sub handler {
+              my $r = shift;
+              $r->content_type('text/plain');
+              print "Hello mod_perl!\n";
+              return Apache2::Const::OK;
+            }
+            1;
+          '';
+          startup = pkgs.writeScript "startup.pl" ''
+            use lib "${inc}",
+              split ":","${with pkgs.perl.pkgs; makeFullPerlPath ([ mod_perl2 ])}";
+            1;
+          '';
+        in
+        {
+          extraConfig = ''
+            PerlRequire ${startup}
+          '';
+          locations."/modperl" = {
+            extraConfig = ''
+              SetHandler perl-script
+              PerlResponseHandler ModPerlTest
+            '';
+          };
+        };
+      enablePerl = true;
+    };
+  };
+  testScript = { ... }: ''
+    machine.wait_for_unit("httpd.service")
+    response = machine.succeed("curl -fvvv -s http://127.0.0.1:80/modperl")
+    assert "Hello mod_perl!" in response, "/modperl handler did not respond"
+  '';
+})
diff --git a/nixos/tests/molly-brown.nix b/nixos/tests/molly-brown.nix
new file mode 100644
index 00000000000..bfc036e81ba
--- /dev/null
+++ b/nixos/tests/molly-brown.nix
@@ -0,0 +1,71 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+  let testString = "NixOS Gemini test successful";
+  in {
+
+    name = "molly-brown";
+    meta = with pkgs.lib.maintainers; { maintainers = [ ehmry ]; };
+
+    nodes = {
+
+      geminiServer = { config, pkgs, ... }:
+        let
+          inherit (config.networking) hostName;
+          cfg = config.services.molly-brown;
+        in {
+
+          environment.systemPackages = [
+            (pkgs.writeScriptBin "test-gemini" ''
+              #!${pkgs.python3}/bin/python
+
+              import socket
+              import ssl
+              import tempfile
+              import textwrap
+              import urllib.parse
+
+              url = "gemini://geminiServer/init.gmi"
+              parsed_url = urllib.parse.urlparse(url)
+
+              s = socket.create_connection((parsed_url.netloc, 1965))
+              context = ssl.SSLContext()
+              context.check_hostname = False
+              context.verify_mode = ssl.CERT_NONE
+              s = context.wrap_socket(s, server_hostname=parsed_url.netloc)
+              s.sendall((url + "\r\n").encode("UTF-8"))
+              fp = s.makefile("rb")
+              print(fp.readline().strip())
+              print(fp.readline().strip())
+              print(fp.readline().strip())
+            '')
+          ];
+
+          networking.firewall.allowedTCPPorts = [ cfg.settings.Port ];
+
+          services.molly-brown = {
+            enable = true;
+            docBase = "/tmp/docs";
+            certPath = "/tmp/cert.pem";
+            keyPath = "/tmp/key.pem";
+          };
+
+          systemd.services.molly-brown.preStart = ''
+            ${pkgs.openssl}/bin/openssl genrsa -out "/tmp/key.pem"
+            ${pkgs.openssl}/bin/openssl req -new \
+              -subj "/CN=${config.networking.hostName}" \
+              -key "/tmp/key.pem" -out /tmp/request.pem
+            ${pkgs.openssl}/bin/openssl x509 -req -days 3650 \
+              -in /tmp/request.pem -signkey "/tmp/key.pem" -out "/tmp/cert.pem"
+
+            mkdir -p "${cfg.settings.DocBase}"
+            echo "${testString}" > "${cfg.settings.DocBase}/test.gmi"
+          '';
+        };
+    };
+    testScript = ''
+      geminiServer.wait_for_unit("molly-brown")
+      geminiServer.wait_for_open_port(1965)
+      geminiServer.succeed("test-gemini")
+    '';
+
+  })
diff --git a/nixos/tests/mongodb.nix b/nixos/tests/mongodb.nix
new file mode 100644
index 00000000000..9c6fdfb1ca7
--- /dev/null
+++ b/nixos/tests/mongodb.nix
@@ -0,0 +1,54 @@
+# This test start mongodb, runs a query using mongo shell
+
+import ./make-test-python.nix ({ pkgs, ... }:
+  let
+    testQuery = pkgs.writeScript "nixtest.js" ''
+      db.greetings.insert({ "greeting": "hello" });
+      print(db.greetings.findOne().greeting);
+    '';
+
+    runMongoDBTest = pkg: ''
+      node.execute("(rm -rf data || true) && mkdir data")
+      node.execute(
+          "${pkg}/bin/mongod --fork --logpath logs --dbpath data"
+      )
+      node.wait_for_open_port(27017)
+
+      assert "hello" in node.succeed(
+          "${pkg}/bin/mongo ${testQuery}"
+      )
+
+      node.execute(
+          "${pkg}/bin/mongod --shutdown --dbpath data"
+      )
+      node.wait_for_closed_port(27017)
+    '';
+
+  in {
+    name = "mongodb";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ bluescreen303 offline cstrahan rvl phile314 ];
+    };
+
+    nodes = {
+      node = {...}: {
+        environment.systemPackages = with pkgs; [
+          mongodb-3_4
+          mongodb-3_6
+          mongodb-4_0
+          mongodb-4_2
+        ];
+      };
+    };
+
+    testScript = ''
+      node.start()
+    ''
+      + runMongoDBTest pkgs.mongodb-3_4
+      + runMongoDBTest pkgs.mongodb-3_6
+      + runMongoDBTest pkgs.mongodb-4_0
+      + runMongoDBTest pkgs.mongodb-4_2
+      + ''
+        node.shutdown()
+      '';
+  })
diff --git a/nixos/tests/moodle.nix b/nixos/tests/moodle.nix
new file mode 100644
index 00000000000..56aa62596c0
--- /dev/null
+++ b/nixos/tests/moodle.nix
@@ -0,0 +1,22 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "moodle";
+  meta.maintainers = [ lib.maintainers.aanderse ];
+
+  machine =
+    { ... }:
+    { services.moodle.enable = true;
+      services.moodle.virtualHost.hostName = "localhost";
+      services.moodle.virtualHost.adminAddr = "root@example.com";
+      services.moodle.initialPassword = "correcthorsebatterystaple";
+
+      # Ensure the virtual machine has enough memory to avoid errors like:
+      # Fatal error: Out of memory (allocated 152047616) (tried to allocate 33554440 bytes)
+      virtualisation.memorySize = 2000;
+    };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("phpfpm-moodle.service")
+    machine.wait_until_succeeds("curl http://localhost/ | grep 'You are not logged in'")
+  '';
+})
diff --git a/nixos/tests/moosefs.nix b/nixos/tests/moosefs.nix
new file mode 100644
index 00000000000..0dc08748b82
--- /dev/null
+++ b/nixos/tests/moosefs.nix
@@ -0,0 +1,89 @@
+import ./make-test-python.nix ({ pkgs, ... } :
+
+let
+  master = { pkgs, ... } : {
+    # data base is stored in memory
+    # server crashes with default memory size
+    virtualisation.memorySize = 1024;
+
+    services.moosefs.master = {
+      enable = true;
+      openFirewall = true;
+      exports = [
+        "* / rw,alldirs,admin,maproot=0:0"
+        "* . rw"
+      ];
+    };
+  };
+
+  chunkserver = { pkgs, ... } : {
+    virtualisation.emptyDiskImages = [ 4096 ];
+    boot.initrd.postDeviceCommands = ''
+      ${pkgs.e2fsprogs}/bin/mkfs.ext4 -L data /dev/vdb
+    '';
+
+    fileSystems = pkgs.lib.mkVMOverride {
+      "/data" = {
+        device = "/dev/disk/by-label/data";
+        fsType = "ext4";
+      };
+    };
+
+    services.moosefs = {
+      masterHost = "master";
+      chunkserver = {
+        openFirewall = true;
+        enable = true;
+        hdds = [ "~/data" ];
+      };
+    };
+  };
+
+  metalogger = { pkgs, ... } : {
+    services.moosefs = {
+      masterHost = "master";
+      metalogger.enable = true;
+    };
+  };
+
+  client = { pkgs, ... } : {
+    services.moosefs.client.enable = true;
+  };
+
+in {
+  name = "moosefs";
+
+  nodes= {
+    inherit master;
+    inherit metalogger;
+    chunkserver1 = chunkserver;
+    chunkserver2 = chunkserver;
+    client1 = client;
+    client2 = client;
+  };
+
+  testScript = ''
+    # prepare master server
+    master.start()
+    master.wait_for_unit("multi-user.target")
+    master.succeed("mfsmaster-init")
+    master.succeed("systemctl restart mfs-master")
+    master.wait_for_unit("mfs-master.service")
+
+    metalogger.wait_for_unit("mfs-metalogger.service")
+
+    for chunkserver in [chunkserver1, chunkserver2]:
+        chunkserver.wait_for_unit("multi-user.target")
+        chunkserver.succeed("chown moosefs:moosefs /data")
+        chunkserver.succeed("systemctl restart mfs-chunkserver")
+        chunkserver.wait_for_unit("mfs-chunkserver.service")
+
+    for client in [client1, client2]:
+        client.wait_for_unit("multi-user.target")
+        client.succeed("mkdir /moosefs")
+        client.succeed("mount -t moosefs master:/ /moosefs")
+
+    client1.succeed("echo test > /moosefs/file")
+    client2.succeed("grep test /moosefs/file")
+  '';
+})
diff --git a/nixos/tests/morty.nix b/nixos/tests/morty.nix
new file mode 100644
index 00000000000..9909596820d
--- /dev/null
+++ b/nixos/tests/morty.nix
@@ -0,0 +1,30 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "morty";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ leenaars ];
+  };
+
+  nodes =
+    { mortyProxyWithKey =
+
+      { ... }:
+      { services.morty = {
+        enable = true;
+        key = "78a9cd0cfee20c672f78427efb2a2a96036027f0";
+        port = 3001;
+        };
+      };
+
+    };
+
+  testScript =
+    { ... }:
+    ''
+      mortyProxyWithKey.wait_for_unit("default.target")
+      mortyProxyWithKey.wait_for_open_port(3001)
+      mortyProxyWithKey.succeed("curl -fL 127.0.0.1:3001 | grep MortyProxy")
+    '';
+
+})
diff --git a/nixos/tests/mosquitto.nix b/nixos/tests/mosquitto.nix
new file mode 100644
index 00000000000..36cc8e3e3d9
--- /dev/null
+++ b/nixos/tests/mosquitto.nix
@@ -0,0 +1,208 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+let
+  port = 1888;
+  tlsPort = 1889;
+  anonPort = 1890;
+  password = "VERY_secret";
+  hashedPassword = "$7$101$/WJc4Mp+I+uYE9sR$o7z9rD1EYXHPwEP5GqQj6A7k4W1yVbePlb8TqNcuOLV9WNCiDgwHOB0JHC1WCtdkssqTBduBNUnUGd6kmZvDSw==";
+  topic = "test/foo";
+
+  snakeOil = pkgs.runCommand "snakeoil-certs" {
+    buildInputs = [ pkgs.gnutls.bin ];
+    caTemplate = pkgs.writeText "snakeoil-ca.template" ''
+      cn = server
+      expiration_days = -1
+      cert_signing_key
+      ca
+    '';
+    certTemplate = pkgs.writeText "snakeoil-cert.template" ''
+      cn = server
+      expiration_days = -1
+      tls_www_server
+      encryption_key
+      signing_key
+    '';
+    userCertTemplate = pkgs.writeText "snakeoil-user-cert.template" ''
+      organization = snakeoil
+      cn = client1
+      expiration_days = -1
+      tls_www_client
+      encryption_key
+      signing_key
+    '';
+  } ''
+    mkdir "$out"
+
+    certtool -p --bits 2048 --outfile "$out/ca.key"
+    certtool -s --template "$caTemplate" --load-privkey "$out/ca.key" \
+                --outfile "$out/ca.crt"
+    certtool -p --bits 2048 --outfile "$out/server.key"
+    certtool -c --template "$certTemplate" \
+                --load-ca-privkey "$out/ca.key" \
+                --load-ca-certificate "$out/ca.crt" \
+                --load-privkey "$out/server.key" \
+                --outfile "$out/server.crt"
+
+    certtool -p --bits 2048 --outfile "$out/client1.key"
+    certtool -c --template "$userCertTemplate" \
+                --load-privkey "$out/client1.key" \
+                --load-ca-privkey "$out/ca.key" \
+                --load-ca-certificate "$out/ca.crt" \
+                --outfile "$out/client1.crt"
+  '';
+
+in {
+  name = "mosquitto";
+  meta = with pkgs.lib; {
+    maintainers = with maintainers; [ pennae peterhoeg ];
+  };
+
+  nodes = let
+    client = { pkgs, ... }: {
+      environment.systemPackages = with pkgs; [ mosquitto ];
+    };
+  in {
+    server = { pkgs, ... }: {
+      networking.firewall.allowedTCPPorts = [ port tlsPort anonPort ];
+      services.mosquitto = {
+        enable = true;
+        settings = {
+          sys_interval = 1;
+        };
+        listeners = [
+          {
+            inherit port;
+            users = {
+              password_store = {
+                inherit password;
+              };
+              password_file = {
+                passwordFile = pkgs.writeText "mqtt-password" password;
+              };
+              hashed_store = {
+                inherit hashedPassword;
+              };
+              hashed_file = {
+                hashedPasswordFile = pkgs.writeText "mqtt-hashed-password" hashedPassword;
+              };
+
+              reader = {
+                inherit password;
+                acl = [
+                  "read ${topic}"
+                  "read $SYS/#" # so we always have something to read
+                ];
+              };
+              writer = {
+                inherit password;
+                acl = [ "write ${topic}" ];
+              };
+            };
+          }
+          {
+            port = tlsPort;
+            users.client1 = {
+              acl = [ "read $SYS/#" ];
+            };
+            settings = {
+              cafile = "${snakeOil}/ca.crt";
+              certfile = "${snakeOil}/server.crt";
+              keyfile = "${snakeOil}/server.key";
+              require_certificate = true;
+              use_identity_as_username = true;
+            };
+          }
+          {
+            port = anonPort;
+            omitPasswordAuth = true;
+            settings.allow_anonymous = true;
+            acl = [ "pattern read #" ];
+            users = {
+              anonWriter = {
+                password = "<ignored>" + password;
+                acl = [ "write ${topic}" ];
+              };
+            };
+          }
+        ];
+      };
+    };
+
+    client1 = client;
+    client2 = client;
+  };
+
+  testScript = ''
+    def mosquitto_cmd(binary, user, topic, port):
+        return (
+            "mosquitto_{} "
+            "-V mqttv311 "
+            "-h server "
+            "-p {} "
+            "-u {} "
+            "-P '${password}' "
+            "-t '{}'"
+        ).format(binary, port, user, topic)
+
+
+    def publish(args, user, topic="${topic}", port=${toString port}):
+        return "{} {}".format(mosquitto_cmd("pub", user, topic, port), args)
+
+    def subscribe(args, user, topic="${topic}", port=${toString port}):
+        return "{} -W 5 -C 1 {}".format(mosquitto_cmd("sub", user, topic, port), args)
+
+    def parallel(*fns):
+        from threading import Thread
+        threads = [ Thread(target=fn) for fn in fns ]
+        for t in threads: t.start()
+        for t in threads: t.join()
+
+
+    start_all()
+    server.wait_for_unit("mosquitto.service")
+
+    with subtest("check passwords"):
+        client1.succeed(publish("-m test", "password_store"))
+        client1.succeed(publish("-m test", "password_file"))
+        client1.succeed(publish("-m test", "hashed_store"))
+        client1.succeed(publish("-m test", "hashed_file"))
+
+    with subtest("check acl"):
+        client1.succeed(subscribe("", "reader", topic="$SYS/#"))
+        client1.fail(subscribe("", "writer", topic="$SYS/#"))
+
+        parallel(
+            lambda: client1.succeed(subscribe("-i 3688cdd7-aa07-42a4-be22-cb9352917e40", "reader")),
+            lambda: [
+                server.wait_for_console_text("3688cdd7-aa07-42a4-be22-cb9352917e40"),
+                client2.succeed(publish("-m test", "writer"))
+            ])
+
+        parallel(
+            lambda: client1.fail(subscribe("-i 24ff16a2-ae33-4a51-9098-1b417153c712", "reader")),
+            lambda: [
+                server.wait_for_console_text("24ff16a2-ae33-4a51-9098-1b417153c712"),
+                client2.succeed(publish("-m test", "reader"))
+            ])
+
+    with subtest("check tls"):
+        client1.succeed(
+            subscribe(
+                "--cafile ${snakeOil}/ca.crt "
+                "--cert ${snakeOil}/client1.crt "
+                "--key ${snakeOil}/client1.key",
+                topic="$SYS/#",
+                port=${toString tlsPort},
+                user="no_such_user"))
+
+    with subtest("check omitPasswordAuth"):
+        parallel(
+            lambda: client1.succeed(subscribe("-i fd56032c-d9cb-4813-a3b4-6be0e04c8fc3",
+                "anonReader", port=${toString anonPort})),
+            lambda: [
+                server.wait_for_console_text("fd56032c-d9cb-4813-a3b4-6be0e04c8fc3"),
+                client2.succeed(publish("-m test", "anonWriter", port=${toString anonPort}))
+            ])
+  '';
+})
diff --git a/nixos/tests/mpd.nix b/nixos/tests/mpd.nix
new file mode 100644
index 00000000000..52d9c7fd33a
--- /dev/null
+++ b/nixos/tests/mpd.nix
@@ -0,0 +1,134 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+  let
+    track = pkgs.fetchurl {
+      # Sourced from http://freemusicarchive.org/music/Blue_Wave_Theory/Surf_Music_Month_Challenge/Skyhawk_Beach_fade_in
+      # License: http://creativecommons.org/licenses/by-sa/4.0/
+
+      name = "Blue_Wave_Theory-Skyhawk_Beach.mp3";
+      url = "https://freemusicarchive.org/file/music/ccCommunity/Blue_Wave_Theory/Surf_Music_Month_Challenge/Blue_Wave_Theory_-_04_-_Skyhawk_Beach.mp3";
+      sha256 = "0xw417bxkx4gqqy139bb21yldi37xx8xjfxrwaqa0gyw19dl6mgp";
+    };
+
+    defaultCfg = rec {
+      user = "mpd";
+      group = "mpd";
+      dataDir = "/var/lib/mpd";
+      musicDirectory = "${dataDir}/music";
+    };
+
+    defaultMpdCfg = with defaultCfg; {
+      inherit dataDir musicDirectory user group;
+      enable = true;
+    };
+
+    musicService = { user, group, musicDirectory }: {
+      description = "Sets up the music file(s) for MPD to use.";
+      requires = [ "mpd.service" ];
+      after = [ "mpd.service" ];
+      wantedBy = [ "default.target" ];
+      script = ''
+        cp ${track} ${musicDirectory}
+      '';
+      serviceConfig = {
+        User = user;
+        Group = group;
+      };
+    };
+
+    mkServer = { mpd, musicService, }:
+      { boot.kernelModules = [ "snd-dummy" ];
+        sound.enable = true;
+        services.mpd = mpd;
+        systemd.services.musicService = musicService;
+      };
+  in {
+    name = "mpd";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ emmanuelrosa ];
+    };
+
+  nodes =
+    { client =
+      { ... }: { };
+
+      serverALSA =
+        { ... }: lib.mkMerge [
+          (mkServer {
+            mpd = defaultMpdCfg // {
+              network.listenAddress = "any";
+              extraConfig = ''
+                audio_output {
+                  type "alsa"
+                  name "ALSA"
+                  mixer_type "null"
+                }
+              '';
+            };
+            musicService = with defaultMpdCfg; musicService { inherit user group musicDirectory; };
+          })
+          { networking.firewall.allowedTCPPorts = [ 6600 ]; }
+        ];
+
+      serverPulseAudio =
+        { ... }: lib.mkMerge [
+          (mkServer {
+            mpd = defaultMpdCfg // {
+              extraConfig = ''
+                audio_output {
+                  type "pulse"
+                  name "The Pulse"
+                }
+              '';
+            };
+
+            musicService = with defaultCfg; musicService { inherit user group musicDirectory; };
+          })
+          {
+            hardware.pulseaudio = {
+              enable = true;
+              systemWide = true;
+              tcp.enable = true;
+              tcp.anonymousClients.allowAll = true;
+            };
+            systemd.services.mpd.environment.PULSE_SERVER = "localhost";
+          }
+        ];
+    };
+
+  testScript = ''
+    mpc = "${pkgs.mpc-cli}/bin/mpc --wait"
+
+    # Connects to the given server and attempts to play a tune.
+    def play_some_music(server):
+        server.wait_for_unit("mpd.service")
+        server.succeed(f"{mpc} update")
+        _, tracks = server.execute(f"{mpc} ls")
+
+        for track in tracks.splitlines():
+            server.succeed(f"{mpc} add {track}")
+
+        _, added_tracks = server.execute(f"{mpc} playlist")
+
+        # Check we succeeded adding audio tracks to the playlist
+        assert len(added_tracks.splitlines()) > 0
+
+        server.succeed(f"{mpc} play")
+
+        _, output = server.execute(f"{mpc} status")
+        # Assure audio track is playing
+        assert "playing" in output
+
+        server.succeed(f"{mpc} stop")
+
+
+    play_some_music(serverALSA)
+    play_some_music(serverPulseAudio)
+
+    client.wait_for_unit("multi-user.target")
+    client.succeed(f"{mpc} -h serverALSA status")
+
+    # The PulseAudio-based server is configured not to accept external client connections
+    # to perform the following test:
+    client.fail(f"{mpc} -h serverPulseAudio status")
+  '';
+})
diff --git a/nixos/tests/mpich-example.c b/nixos/tests/mpich-example.c
new file mode 100644
index 00000000000..c48e3c45b72
--- /dev/null
+++ b/nixos/tests/mpich-example.c
@@ -0,0 +1,21 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <mpi.h>
+
+int
+main (int argc, char *argv[])
+{
+  int rank, size, length;
+  char name[BUFSIZ];
+
+  MPI_Init (&argc, &argv);
+  MPI_Comm_rank (MPI_COMM_WORLD, &rank);
+  MPI_Comm_size (MPI_COMM_WORLD, &size);
+  MPI_Get_processor_name (name, &length);
+
+  printf ("%s: hello world from process %d of %d\n", name, rank, size);
+
+  MPI_Finalize ();
+
+  return EXIT_SUCCESS;
+}
diff --git a/nixos/tests/mpv.nix b/nixos/tests/mpv.nix
new file mode 100644
index 00000000000..a4803f3cb5b
--- /dev/null
+++ b/nixos/tests/mpv.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+let
+  port = toString 4321;
+in
+{
+  name = "mpv";
+  meta.maintainers = with maintainers; [ zopieux ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    {
+      environment.systemPackages = [
+        pkgs.curl
+        (pkgs.wrapMpv pkgs.mpv-unwrapped {
+          scripts = [ pkgs.mpvScripts.simple-mpv-webui ];
+        })
+      ];
+    };
+
+  testScript = ''
+    machine.execute("set -m; mpv --script-opts=webui-port=${port} --idle=yes >&2 &")
+    machine.wait_for_open_port(${port})
+    assert "<title>simple-mpv-webui" in machine.succeed("curl -s localhost:${port}")
+  '';
+})
diff --git a/nixos/tests/mumble.nix b/nixos/tests/mumble.nix
new file mode 100644
index 00000000000..2b5cc20163b
--- /dev/null
+++ b/nixos/tests/mumble.nix
@@ -0,0 +1,85 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+let
+  client = { pkgs, ... }: {
+    imports = [ ./common/x11.nix ];
+    environment.systemPackages = [ pkgs.mumble ];
+  };
+
+  # outside of tests, this file should obviously not come from the nix store
+  envFile = pkgs.writeText "nixos-test-mumble-murmurd.env" ''
+    MURMURD_PASSWORD=testpassword
+  '';
+
+in
+{
+  name = "mumble";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ thoughtpolice eelco ];
+  };
+
+  nodes = {
+    server = { config, ... }: {
+      services.murmur.enable = true;
+      services.murmur.registerName = "NixOS tests";
+      services.murmur.password = "$MURMURD_PASSWORD";
+      services.murmur.environmentFile = envFile;
+      networking.firewall.allowedTCPPorts = [ config.services.murmur.port ];
+    };
+
+    client1 = client;
+    client2 = client;
+  };
+
+  testScript = ''
+    start_all()
+
+    server.wait_for_unit("murmur.service")
+    client1.wait_for_x()
+    client2.wait_for_x()
+
+    client1.execute("mumble mumble://client1:testpassword\@server/test >&2 &")
+    client2.execute("mumble mumble://client2:testpassword\@server/test >&2 &")
+
+    # cancel client audio configuration
+    client1.wait_for_window(r"Audio Tuning Wizard")
+    client2.wait_for_window(r"Audio Tuning Wizard")
+    server.sleep(5)  # wait because mumble is slow to register event handlers
+    client1.send_key("esc")
+    client2.send_key("esc")
+
+    # cancel client cert configuration
+    client1.wait_for_window(r"Certificate Management")
+    client2.wait_for_window(r"Certificate Management")
+    server.sleep(5)  # wait because mumble is slow to register event handlers
+    client1.send_key("esc")
+    client2.send_key("esc")
+
+    # accept server certificate
+    client1.wait_for_window(r"^Mumble$")
+    client2.wait_for_window(r"^Mumble$")
+    server.sleep(5)  # wait because mumble is slow to register event handlers
+    client1.send_chars("y")
+    client2.send_chars("y")
+    server.sleep(5)  # wait because mumble is slow to register event handlers
+
+    # sometimes the wrong of the 2 windows is focused, we switch focus and try pressing "y" again
+    client1.send_key("alt-tab")
+    client2.send_key("alt-tab")
+    server.sleep(5)  # wait because mumble is slow to register event handlers
+    client1.send_chars("y")
+    client2.send_chars("y")
+
+    # Find clients in logs
+    server.wait_until_succeeds(
+        "journalctl -eu murmur -o cat | grep -q 'client1.\+Authenticated'"
+    )
+    server.wait_until_succeeds(
+        "journalctl -eu murmur -o cat | grep -q 'client2.\+Authenticated'"
+    )
+
+    server.sleep(5)  # wait to get screenshot
+    client1.screenshot("screen1")
+    client2.screenshot("screen2")
+  '';
+})
diff --git a/nixos/tests/munin.nix b/nixos/tests/munin.nix
new file mode 100644
index 00000000000..4ec17e0339d
--- /dev/null
+++ b/nixos/tests/munin.nix
@@ -0,0 +1,44 @@
+# This test runs basic munin setup with node and cron job running on the same
+# machine.
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "munin";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ domenkozar eelco ];
+  };
+
+  nodes = {
+    one =
+      { config, ... }:
+        {
+          services = {
+            munin-node = {
+              enable = true;
+              # disable a failing plugin to prevent irrelevant error message, see #23049
+              disabledPlugins = [ "apc_nis" ];
+            };
+            munin-cron = {
+             enable = true;
+             hosts = ''
+               [${config.networking.hostName}]
+               address localhost
+             '';
+            };
+          };
+
+          # increase the systemd timer interval so it fires more often
+          systemd.timers.munin-cron.timerConfig.OnCalendar = pkgs.lib.mkForce "*:*:0/10";
+        };
+    };
+
+  testScript = ''
+    start_all()
+
+    with subtest("ensure munin-node starts and listens on 4949"):
+        one.wait_for_unit("munin-node.service")
+        one.wait_for_open_port(4949)
+    with subtest("ensure munin-cron output is correct"):
+        one.wait_for_file("/var/lib/munin/one/one-uptime-uptime-g.rrd")
+        one.wait_for_file("/var/www/munin/one/index.html")
+  '';
+})
diff --git a/nixos/tests/musescore.nix b/nixos/tests/musescore.nix
new file mode 100644
index 00000000000..7fd80d70df1
--- /dev/null
+++ b/nixos/tests/musescore.nix
@@ -0,0 +1,86 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+let
+  # Make sure we don't have to go through the startup tutorial
+  customMuseScoreConfig = pkgs.writeText "MuseScore3.ini" ''
+    [application]
+    startup\firstStart=false
+
+    [ui]
+    application\startup\showTours=false
+    application\startup\showStartCenter=false
+    '';
+in
+{
+  name = "musescore";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ turion ];
+  };
+
+  machine = { ... }:
+
+  {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    environment.systemPackages = with pkgs; [
+      musescore
+      pdfgrep
+    ];
+  };
+
+  enableOCR = true;
+
+  testScript = { ... }: ''
+    start_all()
+    machine.wait_for_x()
+
+    # Inject custom settings
+    machine.succeed("mkdir -p /root/.config/MuseScore/")
+    machine.succeed(
+        "cp ${customMuseScoreConfig} /root/.config/MuseScore/MuseScore3.ini"
+    )
+
+    # Start MuseScore window
+    machine.execute("DISPLAY=:0.0 mscore >&2 &")
+
+    # Wait until MuseScore has launched
+    machine.wait_for_window("MuseScore")
+
+    # Wait until the window has completely initialised
+    machine.wait_for_text("MuseScore")
+
+    # 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")
+
+    # Go to the export dialogue and create a PDF
+    machine.send_key("alt-f")
+    machine.sleep(1)
+    machine.send_key("e")
+
+    # Wait until the export dialogue appears.
+    machine.wait_for_window("Export")
+    machine.screenshot("MuseScore1")
+    machine.send_key("ret")
+    machine.sleep(1)
+    machine.send_key("ret")
+
+    machine.screenshot("MuseScore2")
+
+    # Wait until PDF is exported
+    machine.wait_for_file("/root/Documents/MuseScore3/Scores/Untitled.pdf")
+
+    # Check that it contains the title of the score
+    machine.succeed("pdfgrep Title /root/Documents/MuseScore3/Scores/Untitled.pdf")
+
+    machine.screenshot("MuseScore3")
+  '';
+})
diff --git a/nixos/tests/mutable-users.nix b/nixos/tests/mutable-users.nix
new file mode 100644
index 00000000000..ebe32e6487e
--- /dev/null
+++ b/nixos/tests/mutable-users.nix
@@ -0,0 +1,73 @@
+# Mutable users tests.
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "mutable-users";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ gleber ];
+  };
+
+  nodes = {
+    machine = { ... }: {
+      users.mutableUsers = false;
+    };
+    mutable = { ... }: {
+      users.mutableUsers = true;
+      users.users.dry-test.isNormalUser = true;
+    };
+  };
+
+  testScript = {nodes, ...}: let
+    immutableSystem = nodes.machine.config.system.build.toplevel;
+    mutableSystem = nodes.mutable.config.system.build.toplevel;
+  in ''
+    machine.start()
+    machine.wait_for_unit("default.target")
+
+    # Machine starts in immutable mode. Add a user and test if reactivating
+    # configuration removes the user.
+    with subtest("Machine in immutable mode"):
+        assert "foobar" not in machine.succeed("cat /etc/passwd")
+        machine.succeed("sudo useradd foobar")
+        assert "foobar" in machine.succeed("cat /etc/passwd")
+        machine.succeed(
+            "${immutableSystem}/bin/switch-to-configuration test"
+        )
+        assert "foobar" not in machine.succeed("cat /etc/passwd")
+
+    # In immutable mode passwd is not wrapped, while in mutable mode it is
+    # wrapped.
+    with subtest("Password is wrapped in mutable mode"):
+        assert "/run/current-system/" in machine.succeed("which passwd")
+        machine.succeed(
+            "${mutableSystem}/bin/switch-to-configuration test"
+        )
+        assert "/run/wrappers/" in machine.succeed("which passwd")
+
+    with subtest("dry-activation does not change files"):
+        machine.succeed('test -e /home/dry-test')  # home was created
+        machine.succeed('rm -rf /home/dry-test')
+
+        files_to_check = ['/etc/group',
+                          '/etc/passwd',
+                          '/etc/shadow',
+                          '/etc/subuid',
+                          '/etc/subgid',
+                          '/var/lib/nixos/uid-map',
+                          '/var/lib/nixos/gid-map',
+                          '/var/lib/nixos/declarative-groups',
+                          '/var/lib/nixos/declarative-users'
+                         ]
+        expected_hashes = {}
+        expected_stats = {}
+        for file in files_to_check:
+            expected_hashes[file] = machine.succeed(f"sha256sum {file}")
+            expected_stats[file] = machine.succeed(f"stat {file}")
+
+        machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate")
+
+        machine.fail('test -e /home/dry-test')  # home was not recreated
+        for file in files_to_check:
+            assert machine.succeed(f"sha256sum {file}") == expected_hashes[file]
+            assert machine.succeed(f"stat {file}") == expected_stats[file]
+  '';
+})
diff --git a/nixos/tests/mxisd.nix b/nixos/tests/mxisd.nix
new file mode 100644
index 00000000000..354612a8a53
--- /dev/null
+++ b/nixos/tests/mxisd.nix
@@ -0,0 +1,21 @@
+import ./make-test-python.nix ({ pkgs, ... } : {
+
+  name = "mxisd";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mguentner ];
+  };
+
+  nodes = {
+    server = args : {
+      services.mxisd.enable = true;
+      services.mxisd.matrix.domain = "example.org";
+    };
+  };
+
+  testScript = ''
+    start_all()
+    server.wait_for_unit("mxisd.service")
+    server.wait_for_open_port(8090)
+    server.succeed("curl -Ssf 'http://127.0.0.1:8090/_matrix/identity/api/v1'")
+  '';
+})
diff --git a/nixos/tests/mysql/common.nix b/nixos/tests/mysql/common.nix
new file mode 100644
index 00000000000..040d360b6d9
--- /dev/null
+++ b/nixos/tests/mysql/common.nix
@@ -0,0 +1,10 @@
+{ lib, pkgs }: {
+  mariadbPackages = lib.filterAttrs (n: _: lib.hasPrefix "mariadb" n) (pkgs.callPackage ../../../pkgs/servers/sql/mariadb {
+    inherit (pkgs.darwin) cctools;
+    inherit (pkgs.darwin.apple_sdk.frameworks) CoreServices;
+  });
+  mysqlPackages = {
+    inherit (pkgs) mysql57 mysql80;
+  };
+  mkTestName = pkg: "mariadb_${builtins.replaceStrings ["."] [""] (lib.versions.majorMinor pkg.version)}";
+}
diff --git a/nixos/tests/mysql/mariadb-galera.nix b/nixos/tests/mysql/mariadb-galera.nix
new file mode 100644
index 00000000000..c9962f49c02
--- /dev/null
+++ b/nixos/tests/mysql/mariadb-galera.nix
@@ -0,0 +1,250 @@
+{
+  system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../../.. { inherit system config; },
+  lib ? pkgs.lib
+}:
+
+let
+  inherit (import ./common.nix { inherit pkgs lib; }) mkTestName mariadbPackages;
+
+  makeTest = import ./../make-test-python.nix;
+
+  # Common user configuration
+  makeGaleraTest = {
+    mariadbPackage,
+    name ? mkTestName mariadbPackage,
+    galeraPackage ? pkgs.mariadb-galera
+  }: makeTest {
+    name = "${name}-galera-mariabackup";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ izorkin ajs124 das_j ];
+    };
+
+    # The test creates a Galera cluster with 3 nodes and is checking if mariabackup-based SST works. The cluster is tested by creating a DB and an empty table on one node,
+    # and checking the table's presence on the other node.
+    nodes = let
+      mkGaleraNode = {
+        id,
+        method
+      }: let
+        address = "192.168.1.${toString id}";
+        isFirstClusterNode = id == 1 || id == 4;
+      in {
+        users = {
+          users.testuser = {
+            isSystemUser = true;
+            group = "testusers";
+          };
+          groups.testusers = { };
+        };
+
+        networking = {
+          interfaces.eth1 = {
+            ipv4.addresses = [
+              { inherit address; prefixLength = 24; }
+            ];
+          };
+          extraHosts = lib.concatMapStringsSep "\n" (i: "192.168.1.${toString i} galera_0${toString i}") (lib.range 1 6);
+          firewall.allowedTCPPorts = [ 3306 4444 4567 4568 ];
+          firewall.allowedUDPPorts = [ 4567 ];
+        };
+        systemd.services.mysql = with pkgs; {
+          path = with pkgs; [
+            bash
+            gawk
+            gnutar
+            gzip
+            inetutils
+            iproute2
+            netcat
+            procps
+            pv
+            rsync
+            socat
+            stunnel
+            which
+          ];
+        };
+        services.mysql = {
+          enable = true;
+          package = mariadbPackage;
+          ensureDatabases = lib.mkIf isFirstClusterNode [ "testdb" ];
+          ensureUsers = lib.mkIf isFirstClusterNode [{
+            name = "testuser";
+            ensurePermissions = {
+              "testdb.*" = "ALL PRIVILEGES";
+            };
+          }];
+          initialScript = lib.mkIf isFirstClusterNode (pkgs.writeText "mariadb-init.sql" ''
+            GRANT ALL PRIVILEGES ON *.* TO 'check_repl'@'localhost' IDENTIFIED BY 'check_pass' WITH GRANT OPTION;
+            FLUSH PRIVILEGES;
+          '');
+          settings = {
+            mysqld = {
+              bind_address = "0.0.0.0";
+            };
+            galera = {
+              wsrep_on = "ON";
+              wsrep_debug = "NONE";
+              wsrep_retry_autocommit = "3";
+              wsrep_provider = "${galeraPackage}/lib/galera/libgalera_smm.so";
+              wsrep_cluster_address = "gcomm://"
+                + lib.optionalString (id == 2 || id == 3) "galera_01,galera_02,galera_03"
+                + lib.optionalString (id == 5 || id == 6) "galera_04,galera_05,galera_06";
+              wsrep_cluster_name = "galera";
+              wsrep_node_address = address;
+              wsrep_node_name = "galera_0${toString id}";
+              wsrep_sst_method = method;
+              wsrep_sst_auth = "check_repl:check_pass";
+              binlog_format = "ROW";
+              enforce_storage_engine = "InnoDB";
+              innodb_autoinc_lock_mode = "2";
+            };
+          };
+        };
+      };
+    in {
+      galera_01 = mkGaleraNode {
+        id = 1;
+        method = "mariabackup";
+      };
+
+      galera_02 = mkGaleraNode {
+        id = 2;
+        method = "mariabackup";
+      };
+
+      galera_03 = mkGaleraNode {
+        id = 3;
+        method = "mariabackup";
+      };
+
+      galera_04 = mkGaleraNode {
+        id = 4;
+        method = "rsync";
+      };
+
+      galera_05 = mkGaleraNode {
+        id = 5;
+        method = "rsync";
+      };
+
+      galera_06 = mkGaleraNode {
+        id = 6;
+        method = "rsync";
+      };
+
+    };
+
+    testScript = ''
+      galera_01.start()
+      galera_01.wait_for_unit("mysql")
+      galera_01.wait_for_open_port(3306)
+      galera_01.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; create table db1 (test_id INT, PRIMARY KEY (test_id)) ENGINE = InnoDB;'"
+      )
+      galera_01.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; insert into db1 values (37);'"
+      )
+      galera_02.start()
+      galera_02.wait_for_unit("mysql")
+      galera_02.wait_for_open_port(3306)
+      galera_03.start()
+      galera_03.wait_for_unit("mysql")
+      galera_03.wait_for_open_port(3306)
+      galera_02.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db1;' -N | grep 37"
+      )
+      galera_02.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; create table db2 (test_id INT, PRIMARY KEY (test_id)) ENGINE = InnoDB;'"
+      )
+      galera_02.succeed("systemctl stop mysql")
+      galera_01.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; insert into db2 values (38);'"
+      )
+      galera_03.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; create table db3 (test_id INT, PRIMARY KEY (test_id)) ENGINE = InnoDB;'"
+      )
+      galera_01.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; insert into db3 values (39);'"
+      )
+      galera_02.succeed("systemctl start mysql")
+      galera_02.wait_for_open_port(3306)
+      galera_02.succeed(
+          "sudo -u testuser mysql -u testuser -e 'show status' -N | grep 'wsrep_cluster_size.*3'"
+      )
+      galera_03.succeed(
+          "sudo -u testuser mysql -u testuser -e 'show status' -N | grep 'wsrep_local_state_comment.*Synced'"
+      )
+      galera_01.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db3;' -N | grep 39"
+      )
+      galera_02.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db2;' -N | grep 38"
+      )
+      galera_03.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db1;' -N | grep 37"
+      )
+      galera_01.succeed("sudo -u testuser mysql -u testuser -e 'use testdb; drop table db3;'")
+      galera_02.succeed("sudo -u testuser mysql -u testuser -e 'use testdb; drop table db2;'")
+      galera_03.succeed("sudo -u testuser mysql -u testuser -e 'use testdb; drop table db1;'")
+      galera_01.crash()
+      galera_02.crash()
+      galera_03.crash()
+
+      galera_04.start()
+      galera_04.wait_for_unit("mysql")
+      galera_04.wait_for_open_port(3306)
+      galera_04.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; create table db1 (test_id INT, PRIMARY KEY (test_id)) ENGINE = InnoDB;'"
+      )
+      galera_04.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; insert into db1 values (41);'"
+      )
+      galera_05.start()
+      galera_05.wait_for_unit("mysql")
+      galera_05.wait_for_open_port(3306)
+      galera_06.start()
+      galera_06.wait_for_unit("mysql")
+      galera_06.wait_for_open_port(3306)
+      galera_05.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db1;' -N | grep 41"
+      )
+      galera_05.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; create table db2 (test_id INT, PRIMARY KEY (test_id)) ENGINE = InnoDB;'"
+      )
+      galera_05.succeed("systemctl stop mysql")
+      galera_04.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; insert into db2 values (42);'"
+      )
+      galera_06.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; create table db3 (test_id INT, PRIMARY KEY (test_id)) ENGINE = InnoDB;'"
+      )
+      galera_04.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; insert into db3 values (43);'"
+      )
+      galera_05.succeed("systemctl start mysql")
+      galera_05.wait_for_open_port(3306)
+      galera_05.succeed(
+          "sudo -u testuser mysql -u testuser -e 'show status' -N | grep 'wsrep_cluster_size.*3'"
+      )
+      galera_06.succeed(
+          "sudo -u testuser mysql -u testuser -e 'show status' -N | grep 'wsrep_local_state_comment.*Synced'"
+      )
+      galera_04.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db3;' -N | grep 43"
+      )
+      galera_05.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db2;' -N | grep 42"
+      )
+      galera_06.succeed(
+          "sudo -u testuser mysql -u testuser -e 'use testdb; select test_id from db1;' -N | grep 41"
+      )
+      galera_04.succeed("sudo -u testuser mysql -u testuser -e 'use testdb; drop table db3;'")
+      galera_05.succeed("sudo -u testuser mysql -u testuser -e 'use testdb; drop table db2;'")
+      galera_06.succeed("sudo -u testuser mysql -u testuser -e 'use testdb; drop table db1;'")
+    '';
+  };
+in
+  lib.mapAttrs (_: mariadbPackage: makeGaleraTest { inherit mariadbPackage; }) mariadbPackages
diff --git a/nixos/tests/mysql/mysql-autobackup.nix b/nixos/tests/mysql/mysql-autobackup.nix
new file mode 100644
index 00000000000..101122f7bde
--- /dev/null
+++ b/nixos/tests/mysql/mysql-autobackup.nix
@@ -0,0 +1,53 @@
+{
+  system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../../.. { inherit system config; },
+  lib ? pkgs.lib
+}:
+
+let
+  inherit (import ./common.nix { inherit pkgs lib; }) mkTestName mariadbPackages;
+
+  makeTest = import ./../make-test-python.nix;
+
+  makeAutobackupTest = {
+    package,
+    name ? mkTestName package,
+  }: makeTest {
+    name = "${name}-automysqlbackup";
+    meta.maintainers = [ lib.maintainers.aanderse ];
+
+    machine = {
+      services.mysql = {
+        inherit package;
+        enable = true;
+        initialDatabases = [ { name = "testdb"; schema = ./testdb.sql; } ];
+      };
+
+      services.automysqlbackup.enable = true;
+    };
+
+    testScript = ''
+      start_all()
+
+      # Need to have mysql started so that it can be populated with data.
+      machine.wait_for_unit("mysql.service")
+
+      with subtest("Wait for testdb to be fully populated (5 rows)."):
+          machine.wait_until_succeeds(
+              "mysql -u root -D testdb -N -B -e 'select count(id) from tests' | grep -q 5"
+          )
+
+      with subtest("Do a backup and wait for it to start"):
+          machine.start_job("automysqlbackup.service")
+          machine.wait_for_job("automysqlbackup.service")
+
+      with subtest("wait for backup file and check that data appears in backup"):
+          machine.wait_for_file("/var/backup/mysql/daily/testdb")
+          machine.succeed(
+              "${pkgs.gzip}/bin/zcat /var/backup/mysql/daily/testdb/daily_testdb_*.sql.gz | grep hello"
+          )
+      '';
+  };
+in
+  lib.mapAttrs (_: package: makeAutobackupTest { inherit package; }) mariadbPackages
diff --git a/nixos/tests/mysql/mysql-backup.nix b/nixos/tests/mysql/mysql-backup.nix
new file mode 100644
index 00000000000..9335b233327
--- /dev/null
+++ b/nixos/tests/mysql/mysql-backup.nix
@@ -0,0 +1,72 @@
+{
+  system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../../.. { inherit system config; },
+  lib ? pkgs.lib
+}:
+
+let
+  inherit (import ./common.nix { inherit pkgs lib; }) mkTestName mariadbPackages;
+
+  makeTest = import ./../make-test-python.nix;
+
+  makeBackupTest = {
+    package,
+    name ? mkTestName package
+  }: makeTest {
+    name = "${name}-backup";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ rvl ];
+    };
+
+    nodes = {
+      master = { pkgs, ... }: {
+        services.mysql = {
+          inherit package;
+          enable = true;
+          initialDatabases = [ { name = "testdb"; schema = ./testdb.sql; } ];
+        };
+
+        services.mysqlBackup = {
+          enable = true;
+          databases = [ "doesnotexist" "testdb" ];
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      # Delete backup file that may be left over from a previous test run.
+      # This is not needed on Hydra but useful for repeated local test runs.
+      master.execute("rm -f /var/backup/mysql/testdb.gz")
+
+      # Need to have mysql started so that it can be populated with data.
+      master.wait_for_unit("mysql.service")
+
+      # Wait for testdb to be fully populated (5 rows).
+      master.wait_until_succeeds(
+          "mysql -u root -D testdb -N -B -e 'select count(id) from tests' | grep -q 5"
+      )
+
+      # Do a backup and wait for it to start
+      master.start_job("mysql-backup.service")
+      master.wait_for_unit("mysql-backup.service")
+
+      # wait for backup to fail, because of database 'doesnotexist'
+      master.wait_until_fails("systemctl is-active -q mysql-backup.service")
+
+      # wait for backup file and check that data appears in backup
+      master.wait_for_file("/var/backup/mysql/testdb.gz")
+      master.succeed(
+          "${pkgs.gzip}/bin/zcat /var/backup/mysql/testdb.gz | grep hello"
+      )
+
+      # Check that a failed backup is logged
+      master.succeed(
+          "journalctl -u mysql-backup.service | grep 'fail.*doesnotexist' > /dev/null"
+      )
+    '';
+  };
+in
+  lib.mapAttrs (_: package: makeBackupTest { inherit package; }) mariadbPackages
diff --git a/nixos/tests/mysql/mysql-replication.nix b/nixos/tests/mysql/mysql-replication.nix
new file mode 100644
index 00000000000..f6014019bd5
--- /dev/null
+++ b/nixos/tests/mysql/mysql-replication.nix
@@ -0,0 +1,101 @@
+{
+  system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../../.. { inherit system config; },
+  lib ? pkgs.lib
+}:
+
+let
+  inherit (import ./common.nix { inherit pkgs lib; }) mkTestName mariadbPackages;
+
+  replicateUser = "replicate";
+  replicatePassword = "secret";
+
+  makeTest = import ./../make-test-python.nix;
+
+  makeReplicationTest = {
+    package,
+    name ? mkTestName package,
+  }: makeTest {
+    name = "${name}-replication";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ ajs124 das_j ];
+    };
+
+    nodes = {
+      primary = {
+        services.mysql = {
+          inherit package;
+          enable = true;
+          replication.role = "master";
+          replication.slaveHost = "%";
+          replication.masterUser = replicateUser;
+          replication.masterPassword = replicatePassword;
+          initialDatabases = [ { name = "testdb"; schema = ./testdb.sql; } ];
+        };
+        networking.firewall.allowedTCPPorts = [ 3306 ];
+      };
+
+      secondary1 = { nodes, ... }: {
+        services.mysql = {
+          inherit package;
+          enable = true;
+          replication.role = "slave";
+          replication.serverId = 2;
+          replication.masterHost = nodes.primary.config.networking.hostName;
+          replication.masterUser = replicateUser;
+          replication.masterPassword = replicatePassword;
+        };
+      };
+
+      secondary2 = { nodes, ... }: {
+        services.mysql = {
+          inherit package;
+          enable = true;
+          replication.role = "slave";
+          replication.serverId = 3;
+          replication.masterHost = nodes.primary.config.networking.hostName;
+          replication.masterUser = replicateUser;
+          replication.masterPassword = replicatePassword;
+        };
+      };
+    };
+
+    testScript = ''
+      primary.start()
+      primary.wait_for_unit("mysql")
+      primary.wait_for_open_port(3306)
+      # Wait for testdb to be fully populated (5 rows).
+      primary.wait_until_succeeds(
+          "sudo -u mysql mysql -u mysql -D testdb -N -B -e 'select count(id) from tests' | grep -q 5"
+      )
+
+      secondary1.start()
+      secondary2.start()
+      secondary1.wait_for_unit("mysql")
+      secondary1.wait_for_open_port(3306)
+      secondary2.wait_for_unit("mysql")
+      secondary2.wait_for_open_port(3306)
+
+      # wait for replications to finish
+      secondary1.wait_until_succeeds(
+          "sudo -u mysql mysql -u mysql -D testdb -N -B -e 'select count(id) from tests' | grep -q 5"
+      )
+      secondary2.wait_until_succeeds(
+          "sudo -u mysql mysql -u mysql -D testdb -N -B -e 'select count(id) from tests' | grep -q 5"
+      )
+
+      secondary2.succeed("systemctl stop mysql")
+      primary.succeed(
+          "echo 'insert into testdb.tests values (123, 456);' | sudo -u mysql mysql -u mysql -N"
+      )
+      secondary2.succeed("systemctl start mysql")
+      secondary2.wait_for_unit("mysql")
+      secondary2.wait_for_open_port(3306)
+      secondary2.wait_until_succeeds(
+          "echo 'select * from testdb.tests where Id = 123;' | sudo -u mysql mysql -u mysql -N | grep 456"
+      )
+    '';
+  };
+in
+  lib.mapAttrs (_: package: makeReplicationTest { inherit package; }) mariadbPackages
diff --git a/nixos/tests/mysql/mysql.nix b/nixos/tests/mysql/mysql.nix
new file mode 100644
index 00000000000..197e6da80e2
--- /dev/null
+++ b/nixos/tests/mysql/mysql.nix
@@ -0,0 +1,149 @@
+{
+  system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../../.. { inherit system config; },
+  lib ? pkgs.lib
+}:
+
+let
+  inherit (import ./common.nix { inherit pkgs lib; }) mkTestName mariadbPackages mysqlPackages;
+
+  makeTest = import ./../make-test-python.nix;
+  # Setup common users
+  makeMySQLTest = {
+    package,
+    name ? mkTestName package,
+    useSocketAuth ? true,
+    hasMroonga ? true,
+    hasRocksDB ? true
+  }: makeTest {
+    inherit name;
+    meta = with lib.maintainers; {
+      maintainers = [ ajs124 das_j ];
+    };
+
+    nodes = {
+      ${name} =
+        { pkgs, ... }: {
+
+          users = {
+            groups.testusers = { };
+
+            users.testuser = {
+              isSystemUser = true;
+              group = "testusers";
+            };
+
+            users.testuser2 = {
+              isSystemUser = true;
+              group = "testusers";
+            };
+          };
+
+          services.mysql = {
+            enable = true;
+            initialDatabases = [
+              { name = "testdb3"; schema = ./testdb.sql; }
+            ];
+            # note that using pkgs.writeText here is generally not a good idea,
+            # as it will store the password in world-readable /nix/store ;)
+            initialScript = pkgs.writeText "mysql-init.sql" (if (!useSocketAuth) then ''
+              CREATE USER 'testuser3'@'localhost' IDENTIFIED BY 'secure';
+              GRANT ALL PRIVILEGES ON testdb3.* TO 'testuser3'@'localhost';
+            '' else ''
+              ALTER USER root@localhost IDENTIFIED WITH unix_socket;
+              DELETE FROM mysql.user WHERE password = ''' AND plugin = ''';
+              DELETE FROM mysql.user WHERE user = ''';
+              FLUSH PRIVILEGES;
+            '');
+
+            ensureDatabases = [ "testdb" "testdb2" ];
+            ensureUsers = [{
+              name = "testuser";
+              ensurePermissions = {
+                "testdb.*" = "ALL PRIVILEGES";
+              };
+            } {
+              name = "testuser2";
+              ensurePermissions = {
+                "testdb2.*" = "ALL PRIVILEGES";
+              };
+            }];
+            package = package;
+            settings = {
+              mysqld = {
+                plugin-load-add = lib.optional hasMroonga "ha_mroonga.so"
+                  ++ lib.optional hasRocksDB "ha_rocksdb.so";
+              };
+            };
+          };
+        };
+
+      mariadb =        {
+        };
+    };
+
+    testScript = ''
+      start_all()
+
+      machine = ${name}
+      machine.wait_for_unit("mysql")
+      machine.succeed(
+          "echo 'use testdb; create table tests (test_id INT, PRIMARY KEY (test_id));' | sudo -u testuser mysql -u testuser"
+      )
+      machine.succeed(
+          "echo 'use testdb; insert into tests values (42);' | sudo -u testuser mysql -u testuser"
+      )
+      # Ensure testuser2 is not able to insert into testdb as mysql testuser2
+      machine.fail(
+          "echo 'use testdb; insert into tests values (23);' | sudo -u testuser2 mysql -u testuser2"
+      )
+      # Ensure testuser2 is not able to authenticate as mysql testuser
+      machine.fail(
+          "echo 'use testdb; insert into tests values (23);' | sudo -u testuser2 mysql -u testuser"
+      )
+      machine.succeed(
+          "echo 'use testdb; select test_id from tests;' | sudo -u testuser mysql -u testuser -N | grep 42"
+      )
+
+      ${lib.optionalString hasMroonga ''
+        # Check if Mroonga plugin works
+        machine.succeed(
+            "echo 'use testdb; create table mroongadb (test_id INT, PRIMARY KEY (test_id)) ENGINE = Mroonga;' | sudo -u testuser mysql -u testuser"
+        )
+        machine.succeed(
+            "echo 'use testdb; insert into mroongadb values (25);' | sudo -u testuser mysql -u testuser"
+        )
+        machine.succeed(
+            "echo 'use testdb; select test_id from mroongadb;' | sudo -u testuser mysql -u testuser -N | grep 25"
+        )
+        machine.succeed(
+            "echo 'use testdb; drop table mroongadb;' | sudo -u testuser mysql -u testuser"
+        )
+      ''}
+
+      ${lib.optionalString hasRocksDB ''
+        # Check if RocksDB plugin works
+        machine.succeed(
+            "echo 'use testdb; create table rocksdb (test_id INT, PRIMARY KEY (test_id)) ENGINE = RocksDB;' | sudo -u testuser mysql -u testuser"
+        )
+        machine.succeed(
+            "echo 'use testdb; insert into rocksdb values (28);' | sudo -u testuser mysql -u testuser"
+        )
+        machine.succeed(
+            "echo 'use testdb; select test_id from rocksdb;' | sudo -u testuser mysql -u testuser -N | grep 28"
+        )
+        machine.succeed(
+            "echo 'use testdb; drop table rocksdb;' | sudo -u testuser mysql -u testuser"
+        )
+      ''}
+    '';
+  };
+in
+  lib.mapAttrs (_: package: makeMySQLTest {
+    inherit package;
+    hasRocksDB = false; hasMroonga = false; useSocketAuth = false;
+  }) mysqlPackages
+  // (lib.mapAttrs (_: package: makeMySQLTest {
+    inherit package;
+  }) mariadbPackages)
diff --git a/nixos/tests/mysql/testdb.sql b/nixos/tests/mysql/testdb.sql
new file mode 100644
index 00000000000..3c68c49ae82
--- /dev/null
+++ b/nixos/tests/mysql/testdb.sql
@@ -0,0 +1,11 @@
+create table tests
+( Id   INTEGER      NOT NULL,
+  Name VARCHAR(255) NOT NULL,
+  primary key(Id)
+);
+
+insert into tests values (1, 'a');
+insert into tests values (2, 'b');
+insert into tests values (3, 'c');
+insert into tests values (4, 'd');
+insert into tests values (5, 'hello');
diff --git a/nixos/tests/n8n.nix b/nixos/tests/n8n.nix
new file mode 100644
index 00000000000..ed93639f2a4
--- /dev/null
+++ b/nixos/tests/n8n.nix
@@ -0,0 +1,25 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+let
+  port = 5678;
+in
+{
+  name = "n8n";
+  meta.maintainers = with maintainers; [ freezeboy ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    {
+      services.n8n = {
+        enable = true;
+      };
+    };
+
+  testScript = ''
+    machine.wait_for_unit("n8n.service")
+    machine.wait_for_open_port("${toString port}")
+    machine.succeed("curl --fail http://localhost:${toString port}/")
+  '';
+})
diff --git a/nixos/tests/nagios.nix b/nixos/tests/nagios.nix
new file mode 100644
index 00000000000..e4d8dabedf7
--- /dev/null
+++ b/nixos/tests/nagios.nix
@@ -0,0 +1,116 @@
+import ./make-test-python.nix (
+  { pkgs, ... }: {
+    name = "nagios";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ symphorien ];
+    };
+
+    machine = { lib, ... }: let
+      writer = pkgs.writeShellScript "write" ''
+        set -x
+        echo "$@"  >> /tmp/notifications
+      '';
+    in
+      {
+        # tested service
+        services.sshd.enable = true;
+        # nagios
+        services.nagios = {
+          enable = true;
+          # make state transitions faster
+          extraConfig.interval_length = "5";
+          objectDefs =
+            (map (x: "${pkgs.nagios}/etc/objects/${x}.cfg") [ "templates" "timeperiods" "commands" ]) ++ [
+              (
+                pkgs.writeText "objects.cfg" ''
+                  # notifications are written to /tmp/notifications
+                  define command {
+                  command_name notify-host-by-file
+                  command_line ${writer} "$HOSTNAME is $HOSTSTATE$"
+                  }
+                  define command {
+                  command_name notify-service-by-file
+                  command_line ${writer} "$SERVICEDESC$ is $SERVICESTATE$"
+                  }
+
+                  # nagios boilerplate
+                  define contact {
+                  contact_name                    alice
+                  alias                           alice
+                  host_notifications_enabled      1
+                  service_notifications_enabled   1
+                  service_notification_period     24x7
+                  host_notification_period        24x7
+                  service_notification_options    w,u,c,r,f,s
+                  host_notification_options       d,u,r,f,s
+                  service_notification_commands   notify-service-by-file
+                  host_notification_commands      notify-host-by-file
+                  email                           foo@example.com
+                  }
+                  define contactgroup {
+                  contactgroup_name   admins
+                  alias               Admins
+                  members alice
+                  }
+                  define hostgroup{
+                  hostgroup_name  allhosts
+                  alias  All hosts
+                  }
+
+                  # monitored objects
+                  define host {
+                  use         generic-host
+                  host_name   localhost
+                  alias       localhost
+                  address     localhost
+                  hostgroups  allhosts
+                  contact_groups admins
+                  # make state transitions faster.
+                  max_check_attempts 2
+                  check_interval 1
+                  retry_interval 1
+                  }
+                  define service {
+                  use                 generic-service
+                  host_name           localhost
+                  service_description ssh
+                  check_command       check_ssh
+                  # make state transitions faster.
+                  max_check_attempts 2
+                  check_interval 1
+                  retry_interval 1
+                  }
+                ''
+              )
+            ];
+        };
+      };
+
+    testScript = { ... }: ''
+      with subtest("ensure sshd starts"):
+          machine.wait_for_unit("sshd.service")
+
+
+      with subtest("ensure nagios starts"):
+          machine.wait_for_file("/var/log/nagios/current")
+
+
+      def assert_notify(text):
+          machine.wait_for_file("/tmp/notifications")
+          real = machine.succeed("cat /tmp/notifications").strip()
+          print(f"got {real!r}, expected {text!r}")
+          assert text == real
+
+
+      with subtest("ensure we get a notification when sshd is down"):
+          machine.succeed("systemctl stop sshd")
+          assert_notify("ssh is CRITICAL")
+
+
+      with subtest("ensure tests can succeed"):
+          machine.succeed("systemctl start sshd")
+          machine.succeed("rm /tmp/notifications")
+          assert_notify("ssh is OK")
+    '';
+  }
+)
diff --git a/nixos/tests/nar-serve.nix b/nixos/tests/nar-serve.nix
new file mode 100644
index 00000000000..9ee738ffb17
--- /dev/null
+++ b/nixos/tests/nar-serve.nix
@@ -0,0 +1,48 @@
+import ./make-test-python.nix (
+  { pkgs, lib, ... }:
+  {
+    name = "nar-serve";
+    meta.maintainers = [ lib.maintainers.rizary ];
+    nodes =
+      {
+        server = { pkgs, ... }: {
+          services.nginx = {
+            enable = true;
+            virtualHosts.default.root = "/var/www";
+          };
+          services.nar-serve = {
+            enable = true;
+            # Connect to the localhost nginx instead of the default
+            # https://cache.nixos.org
+            cacheURL = "http://localhost/";
+          };
+          environment.systemPackages = [
+            pkgs.hello
+            pkgs.curl
+          ];
+
+          networking.firewall.allowedTCPPorts = [ 8383 ];
+
+          # virtualisation.diskSize = 2 * 1024;
+        };
+      };
+    testScript = ''
+      start_all()
+
+      # Create a fake cache with Nginx service the static files
+      server.succeed(
+          "nix copy --to file:///var/www ${pkgs.hello}"
+      )
+      server.wait_for_unit("nginx.service")
+      server.wait_for_open_port(80)
+
+      # Check that nar-serve can return the content of the derivation
+      drvName = os.path.basename("${pkgs.hello}")
+      drvHash = drvName.split("-")[0]
+      server.wait_for_unit("nar-serve.service")
+      server.succeed(
+          "curl -o hello -f http://localhost:8383/nix/store/{}/bin/hello".format(drvHash)
+      )
+    '';
+  }
+)
diff --git a/nixos/tests/nat.nix b/nixos/tests/nat.nix
new file mode 100644
index 00000000000..545eb46f2bf
--- /dev/null
+++ b/nixos/tests/nat.nix
@@ -0,0 +1,120 @@
+# This is a simple distributed test involving a topology with two
+# separate virtual networks - the "inside" and the "outside" - with a
+# 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, ... }:
+  let
+    unit = if withFirewall then "firewall" else "nat";
+
+    routerBase =
+      lib.mkMerge [
+        { virtualisation.vlans = [ 2 1 ];
+          networking.firewall.enable = withFirewall;
+          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");
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ eelco rob ];
+    };
+
+    nodes =
+      { client =
+          { pkgs, nodes, ... }:
+          lib.mkMerge [
+            { virtualisation.vlans = [ 1 ];
+              networking.defaultGateway =
+                (pkgs.lib.head nodes.router.config.networking.interfaces.eth2.ipv4.addresses).address;
+            }
+            (lib.optionalAttrs withConntrackHelpers {
+              networking.firewall.connectionTrackingModules = [ "ftp" ];
+              networking.firewall.autoLoadConntrackHelpers = true;
+            })
+          ];
+
+        router =
+        { ... }: lib.mkMerge [
+          routerBase
+          { networking.nat.enable = true; }
+        ];
+
+        routerDummyNoNat =
+        { ... }: lib.mkMerge [
+          routerBase
+          { networking.nat.enable = false; }
+        ];
+
+        server =
+          { ... }:
+          { virtualisation.vlans = [ 2 ];
+            networking.firewall.enable = false;
+            services.httpd.enable = true;
+            services.httpd.adminAddr = "foo@example.org";
+            services.vsftpd.enable = true;
+            services.vsftpd.anonymousUser = true;
+          };
+      };
+
+    testScript =
+      { nodes, ... }: let
+        routerDummyNoNatClosure = nodes.routerDummyNoNat.config.system.build.toplevel;
+        routerClosure = nodes.router.config.system.build.toplevel;
+      in ''
+        client.start()
+        router.start()
+        server.start()
+
+        # The router should have access to the server.
+        server.wait_for_unit("network.target")
+        server.wait_for_unit("httpd")
+        router.wait_for_unit("network.target")
+        router.succeed("curl --fail http://server/ >&2")
+
+        # The client should be also able to connect via the NAT router.
+        router.wait_for_unit("${unit}")
+        client.wait_for_unit("network.target")
+        client.succeed("curl --fail http://server/ >&2")
+        client.succeed("ping -c 1 server >&2")
+
+        # Test whether passive FTP works.
+        server.wait_for_unit("vsftpd")
+        server.succeed("echo Hello World > /home/ftp/foo.txt")
+        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")
+
+        # Test ICMP.
+        client.succeed("ping -c 1 router >&2")
+        router.succeed("ping -c 1 client >&2")
+
+        # If we turn off NAT, the client shouldn't be able to reach the server.
+        router.succeed(
+            "${routerDummyNoNatClosure}/bin/switch-to-configuration test 2>&1"
+        )
+        client.fail("curl --fail --connect-timeout 5 http://server/ >&2")
+        client.fail("ping -c 1 server >&2")
+
+        # And make sure that reloading the NAT job works.
+        router.succeed(
+            "${routerClosure}/bin/switch-to-configuration test 2>&1"
+        )
+        # 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) ''
+          router.succeed("systemctl start nat.service")
+        ''}
+        client.succeed("curl --fail http://server/ >&2")
+        client.succeed("ping -c 1 server >&2")
+      '';
+  })
diff --git a/nixos/tests/nats.nix b/nixos/tests/nats.nix
new file mode 100644
index 00000000000..c650904e53b
--- /dev/null
+++ b/nixos/tests/nats.nix
@@ -0,0 +1,63 @@
+let
+
+  port = 4222;
+  username = "client";
+  password = "password";
+  topic = "foo.bar";
+
+in import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "nats";
+  meta = with pkgs.lib; { maintainers = with maintainers; [ c0deaddict ]; };
+
+  nodes = let
+    client = { pkgs, ... }: {
+      environment.systemPackages = with pkgs; [ natscli ];
+    };
+  in {
+    server = { pkgs, ... }: {
+      networking.firewall.allowedTCPPorts = [ port ];
+      services.nats = {
+        inherit port;
+        enable = true;
+        settings = {
+          authorization = {
+            users = [{
+              user = username;
+              inherit password;
+            }];
+          };
+        };
+      };
+    };
+
+    client1 = client;
+    client2 = client;
+  };
+
+  testScript = let file = "/tmp/msg";
+  in ''
+    def nats_cmd(*args):
+        return (
+            "nats "
+            "--server=nats://server:${toString port} "
+            "--user=${username} "
+            "--password=${password} "
+            "{}"
+        ).format(" ".join(args))
+
+    def parallel(*fns):
+        from threading import Thread
+        threads = [ Thread(target=fn) for fn in fns ]
+        for t in threads: t.start()
+        for t in threads: t.join()
+
+    start_all()
+    server.wait_for_unit("nats.service")
+
+    with subtest("pub sub"):
+        parallel(
+            lambda: client1.succeed(nats_cmd("sub", "--count", "1", "${topic}")),
+            lambda: client2.succeed("sleep 2 && {}".format(nats_cmd("pub", "${topic}", "hello"))),
+        )
+  '';
+})
diff --git a/nixos/tests/navidrome.nix b/nixos/tests/navidrome.nix
new file mode 100644
index 00000000000..42e14720b2e
--- /dev/null
+++ b/nixos/tests/navidrome.nix
@@ -0,0 +1,12 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "navidrome";
+
+  machine = { ... }: {
+    services.navidrome.enable = true;
+  };
+
+  testScript = ''
+    machine.wait_for_unit("navidrome")
+    machine.wait_for_open_port("4533")
+  '';
+})
diff --git a/nixos/tests/nbd.nix b/nixos/tests/nbd.nix
new file mode 100644
index 00000000000..16255e68e8a
--- /dev/null
+++ b/nixos/tests/nbd.nix
@@ -0,0 +1,87 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+  let
+    listenPort = 30123;
+    testString = "It works!";
+    mkCreateSmallFileService = { path, loop ? false }: {
+      script = ''
+        ${pkgs.coreutils}/bin/dd if=/dev/zero of=${path} bs=1K count=100
+        ${pkgs.lib.optionalString loop
+          "${pkgs.util-linux}/bin/losetup --find ${path}"}
+      '';
+      serviceConfig = {
+        Type = "oneshot";
+      };
+      wantedBy = [ "multi-user.target" ];
+      before = [ "nbd-server.service" ];
+    };
+  in
+  {
+    name = "nbd";
+
+    nodes = {
+      server = { config, pkgs, ... }: {
+        # Create some small files of zeros to use as the ndb disks
+        ## `vault-pub.disk` is accessible from any IP
+        systemd.services.create-pub-file =
+          mkCreateSmallFileService { path = "/vault-pub.disk"; };
+        ## `vault-priv.disk` is accessible only from localhost.
+        ## It's also a loopback device to test exporting /dev/...
+        systemd.services.create-priv-file =
+          mkCreateSmallFileService { path = "/vault-priv.disk"; loop = true; };
+
+        # Needed only for nbd-client used in the tests.
+        environment.systemPackages = [ pkgs.nbd ];
+
+        # Open the nbd port in the firewall
+        networking.firewall.allowedTCPPorts = [ listenPort ];
+
+        # Run the nbd server and expose the small file created above
+        services.nbd.server = {
+          enable = true;
+          exports = {
+            vault-pub = {
+              path = "/vault-pub.disk";
+            };
+            vault-priv = {
+              path = "/dev/loop0";
+              allowAddresses = [ "127.0.0.1" "::1" ];
+            };
+          };
+          listenAddress = "0.0.0.0";
+          listenPort = listenPort;
+        };
+      };
+
+      client = { config, pkgs, ... }: {
+        programs.nbd.enable = true;
+      };
+    };
+
+    testScript = ''
+      testString = "${testString}"
+
+      start_all()
+      server.wait_for_open_port(${toString listenPort})
+
+      # Client: Connect to the server, write a small string to the nbd disk, and cleanly disconnect
+      client.succeed("nbd-client server ${toString listenPort} /dev/nbd0 -name vault-pub -persist")
+      client.succeed(f"echo '{testString}' | dd of=/dev/nbd0 conv=notrunc")
+      client.succeed("nbd-client -d /dev/nbd0")
+
+      # Server: Check that the string written by the client is indeed in the file
+      foundString = server.succeed(f"dd status=none if=/vault-pub.disk count={len(testString)}")[:len(testString)]
+      if foundString != testString:
+         raise Exception(f"Read the wrong string from nbd disk. Expected: '{testString}'. Found: '{foundString}'")
+
+      # Client: Fail to connect to the private disk
+      client.fail("nbd-client server ${toString listenPort} /dev/nbd0 -name vault-priv -persist")
+
+      # Server: Successfully connect to the private disk
+      server.succeed("nbd-client localhost ${toString listenPort} /dev/nbd0 -name vault-priv -persist")
+      server.succeed(f"echo '{testString}' | dd of=/dev/nbd0 conv=notrunc")
+      foundString = server.succeed(f"dd status=none if=/dev/loop0 count={len(testString)}")[:len(testString)]
+      if foundString != testString:
+         raise Exception(f"Read the wrong string from nbd disk. Expected: '{testString}'. Found: '{foundString}'")
+      server.succeed("nbd-client -d /dev/nbd0")
+    '';
+  })
diff --git a/nixos/tests/ncdns.nix b/nixos/tests/ncdns.nix
new file mode 100644
index 00000000000..50193676f34
--- /dev/null
+++ b/nixos/tests/ncdns.nix
@@ -0,0 +1,96 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+let
+  fakeReply = pkgs.writeText "namecoin-reply.json" ''
+  { "error": null,
+    "id": 1,
+    "result": {
+      "address": "T31q8ucJ4dI1xzhxQ5QispfECld5c7Xw",
+      "expired": false,
+      "expires_in": 2248,
+      "height": 438155,
+      "name": "d/test",
+      "txid": "db61c0b2540ba0c1a2c8cc92af703a37002e7566ecea4dbf8727c7191421edfb",
+      "value": "{\"ip\": \"1.2.3.4\", \"email\": \"root@test.bit\",\"info\": \"Fake record\"}",
+      "vout": 0
+    }
+  }
+  '';
+
+  # Disabled because DNSSEC does not currently validate,
+  # see https://github.com/namecoin/ncdns/issues/127
+  dnssec = false;
+
+in
+
+{
+  name = "ncdns";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ rnhmjoj ];
+  };
+
+  nodes.server = { ... }: {
+    networking.nameservers = [ "127.0.0.1" ];
+
+    services.namecoind.rpc = {
+      address = "127.0.0.1";
+      user = "namecoin";
+      password = "secret";
+      port = 8332;
+    };
+
+    # Fake namecoin RPC server because we can't
+    # run a full node in a test.
+    systemd.services.namecoind = {
+      wantedBy = [ "multi-user.target" ];
+      script = ''
+        while true; do
+          echo -e "HTTP/1.1 200 OK\n\n $(<${fakeReply})\n" \
+            | ${pkgs.netcat}/bin/nc -N -l 127.0.0.1 8332
+        done
+      '';
+    };
+
+    services.ncdns = {
+      enable = true;
+      dnssec.enable = dnssec;
+      identity.hostname   = "example.com";
+      identity.hostmaster = "root@example.com";
+      identity.address    = "1.0.0.1";
+    };
+
+    services.pdns-recursor = {
+      enable = true;
+      dns.allowFrom = [ "127.0.0.0/8" ];
+      resolveNamecoin = true;
+    };
+
+    environment.systemPackages = [ pkgs.dnsutils ];
+
+  };
+
+  testScript =
+    (lib.optionalString dnssec ''
+      with subtest("DNSSEC keys have been generated"):
+          server.wait_for_unit("ncdns")
+          server.wait_for_file("/var/lib/ncdns/bit.key")
+          server.wait_for_file("/var/lib/ncdns/bit-zone.key")
+
+      with subtest("DNSKEY bit record is present"):
+          server.wait_for_unit("pdns-recursor")
+          server.wait_for_open_port("53")
+          server.succeed("host -t DNSKEY bit")
+    '') +
+    ''
+      with subtest("can resolve a .bit name"):
+          server.wait_for_unit("namecoind")
+          server.wait_for_unit("ncdns")
+          server.wait_for_open_port("8332")
+          assert "1.2.3.4" in server.succeed("dig @localhost -p 5333 test.bit")
+
+      with subtest("SOA record has identity information"):
+          assert "example.com" in server.succeed("dig SOA @localhost -p 5333 bit")
+
+      with subtest("bit. zone forwarding works"):
+          assert "1.2.3.4" in server.succeed("host test.bit")
+    '';
+})
diff --git a/nixos/tests/ndppd.nix b/nixos/tests/ndppd.nix
new file mode 100644
index 00000000000..e79e2a097b4
--- /dev/null
+++ b/nixos/tests/ndppd.nix
@@ -0,0 +1,60 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} : {
+  name = "ndppd";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fpletz ];
+  };
+
+  nodes = {
+    upstream = { pkgs, ... }: {
+      environment.systemPackages = [ pkgs.tcpdump ];
+      networking.useDHCP = false;
+      networking.interfaces = {
+        eth1 = {
+          ipv6.addresses = [
+            { address = "fd23::1"; prefixLength = 112; }
+          ];
+          ipv6.routes = [
+            { address = "fd42::";
+              prefixLength = 112;
+            }
+          ];
+        };
+      };
+    };
+    server = { pkgs, ... }: {
+      boot.kernel.sysctl = {
+        "net.ipv6.conf.all.forwarding" = "1";
+        "net.ipv6.conf.default.forwarding" = "1";
+      };
+      environment.systemPackages = [ pkgs.tcpdump ];
+      networking.useDHCP = false;
+      networking.interfaces = {
+        eth1 = {
+          ipv6.addresses = [
+            { address = "fd23::2"; prefixLength = 112; }
+          ];
+        };
+      };
+      services.ndppd = {
+        enable = true;
+        proxies.eth1.rules."fd42::/112" = {};
+      };
+      containers.client = {
+        autoStart = true;
+        privateNetwork = true;
+        hostAddress = "192.168.255.1";
+        localAddress = "192.168.255.2";
+        hostAddress6 = "fd42::1";
+        localAddress6 = "fd42::2";
+        config = {};
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    server.wait_for_unit("multi-user.target")
+    upstream.wait_for_unit("multi-user.target")
+    upstream.wait_until_succeeds("ping -c5 fd42::2")
+  '';
+})
diff --git a/nixos/tests/nebula.nix b/nixos/tests/nebula.nix
new file mode 100644
index 00000000000..372cfebdf80
--- /dev/null
+++ b/nixos/tests/nebula.nix
@@ -0,0 +1,223 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: let
+
+  # We'll need to be able to trade cert files between nodes via scp.
+  inherit (import ./ssh-keys.nix pkgs)
+    snakeOilPrivateKey snakeOilPublicKey;
+
+  makeNebulaNode = { config, ... }: name: extraConfig: lib.mkMerge [
+    {
+      # Expose nebula for doing cert signing.
+      environment.systemPackages = [ pkgs.nebula ];
+      users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
+      services.openssh.enable = true;
+
+      services.nebula.networks.smoke = {
+        # Note that these paths won't exist when the machine is first booted.
+        ca = "/etc/nebula/ca.crt";
+        cert = "/etc/nebula/${name}.crt";
+        key = "/etc/nebula/${name}.key";
+        listen = { host = "0.0.0.0"; port = 4242; };
+      };
+    }
+    extraConfig
+  ];
+
+in
+{
+  name = "nebula";
+
+  nodes = {
+
+    lighthouse = { ... } @ args:
+      makeNebulaNode args "lighthouse" {
+        networking.interfaces.eth1.ipv4.addresses = [{
+          address = "192.168.1.1";
+          prefixLength = 24;
+        }];
+
+        services.nebula.networks.smoke = {
+          isLighthouse = true;
+          firewall = {
+            outbound = [ { port = "any"; proto = "any"; host = "any"; } ];
+            inbound = [ { port = "any"; proto = "any"; host = "any"; } ];
+          };
+        };
+      };
+
+    node2 = { ... } @ args:
+      makeNebulaNode args "node2" {
+        networking.interfaces.eth1.ipv4.addresses = [{
+          address = "192.168.1.2";
+          prefixLength = 24;
+        }];
+
+        services.nebula.networks.smoke = {
+          staticHostMap = { "10.0.100.1" = [ "192.168.1.1:4242" ]; };
+          isLighthouse = false;
+          lighthouses = [ "10.0.100.1" ];
+          firewall = {
+            outbound = [ { port = "any"; proto = "any"; host = "any"; } ];
+            inbound = [ { port = "any"; proto = "any"; host = "any"; } ];
+          };
+        };
+      };
+
+    node3 = { ... } @ args:
+      makeNebulaNode args "node3" {
+        networking.interfaces.eth1.ipv4.addresses = [{
+          address = "192.168.1.3";
+          prefixLength = 24;
+        }];
+
+        services.nebula.networks.smoke = {
+          staticHostMap = { "10.0.100.1" = [ "192.168.1.1:4242" ]; };
+          isLighthouse = false;
+          lighthouses = [ "10.0.100.1" ];
+          firewall = {
+            outbound = [ { port = "any"; proto = "any"; host = "any"; } ];
+            inbound = [ { port = "any"; proto = "any"; host = "lighthouse"; } ];
+          };
+        };
+      };
+
+    node4 = { ... } @ args:
+      makeNebulaNode args "node4" {
+        networking.interfaces.eth1.ipv4.addresses = [{
+          address = "192.168.1.4";
+          prefixLength = 24;
+        }];
+
+        services.nebula.networks.smoke = {
+          enable = true;
+          staticHostMap = { "10.0.100.1" = [ "192.168.1.1:4242" ]; };
+          isLighthouse = false;
+          lighthouses = [ "10.0.100.1" ];
+          firewall = {
+            outbound = [ { port = "any"; proto = "any"; host = "lighthouse"; } ];
+            inbound = [ { port = "any"; proto = "any"; host = "any"; } ];
+          };
+        };
+      };
+
+    node5 = { ... } @ args:
+      makeNebulaNode args "node5" {
+        networking.interfaces.eth1.ipv4.addresses = [{
+          address = "192.168.1.5";
+          prefixLength = 24;
+        }];
+
+        services.nebula.networks.smoke = {
+          enable = false;
+          staticHostMap = { "10.0.100.1" = [ "192.168.1.1:4242" ]; };
+          isLighthouse = false;
+          lighthouses = [ "10.0.100.1" ];
+          firewall = {
+            outbound = [ { port = "any"; proto = "any"; host = "lighthouse"; } ];
+            inbound = [ { port = "any"; proto = "any"; host = "any"; } ];
+          };
+        };
+      };
+
+  };
+
+  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",
+    )
+    '';
+
+    # From what I can tell, StrictHostKeyChecking=no is necessary for ssh to work between machines.
+    sshOpts = "-oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -oIdentityFile=/root/.ssh/id_snakeoil";
+
+    restartAndCheckNebula = name: ip: ''
+      ${name}.systemctl("restart nebula@smoke.service")
+      ${name}.succeed("ping -c5 ${ip}")
+    '';
+
+    # Create a keypair on the client node, then use the public key to sign a cert on the lighthouse.
+    signKeysFor = name: ip: ''
+      lighthouse.wait_for_unit("sshd.service")
+      ${name}.wait_for_unit("sshd.service")
+      ${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",
+      )
+      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',
+      )
+      ${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",
+      )
+    '';
+
+  in ''
+    start_all()
+
+    # 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',
+    )
+
+    # Reboot the lighthouse and verify that the nebula service comes up on boot.
+    # Since rebooting takes a while, we'll just restart the service on the other nodes.
+    lighthouse.shutdown()
+    lighthouse.start()
+    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 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 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 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")
+
+    # The lighthouse can ping node2 and node3 but not node5
+    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")
+  '';
+})
diff --git a/nixos/tests/neo4j.nix b/nixos/tests/neo4j.nix
new file mode 100644
index 00000000000..8329e5630d7
--- /dev/null
+++ b/nixos/tests/neo4j.nix
@@ -0,0 +1,20 @@
+import ./make-test-python.nix {
+  name = "neo4j";
+
+  nodes = {
+    master =
+      { ... }:
+
+      {
+        services.neo4j.enable = true;
+      };
+  };
+
+  testScript = ''
+    start_all()
+
+    master.wait_for_unit("neo4j")
+    master.wait_for_open_port(7474)
+    master.succeed("curl -f http://localhost:7474/")
+  '';
+}
diff --git a/nixos/tests/netdata.nix b/nixos/tests/netdata.nix
new file mode 100644
index 00000000000..0f26630da9d
--- /dev/null
+++ b/nixos/tests/netdata.nix
@@ -0,0 +1,38 @@
+# This test runs netdata and checks for data via apps.plugin
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "netdata";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ cransom ];
+  };
+
+  nodes = {
+    netdata =
+      { pkgs, ... }:
+        {
+          environment.systemPackages = with pkgs; [ curl jq ];
+          services.netdata.enable = true;
+        };
+    };
+
+  testScript = ''
+    start_all()
+
+    netdata.wait_for_unit("netdata.service")
+
+    # wait for the service to listen before sending a request
+    netdata.wait_for_open_port(19999)
+
+    # check if the netdata main page loads.
+    netdata.succeed("curl --fail http://localhost:19999/")
+    netdata.succeed("sleep 4")
+
+    # check if netdata can read disk ops for root owned processes.
+    # if > 0, successful. verifies both netdata working and
+    # apps.plugin has elevated capabilities.
+    url = "http://localhost:19999/api/v1/data\?chart=users.pwrites"
+    filter = '[.data[range(10)][.labels | indices("root")[0]]] | add | . > 0'
+    cmd = f"curl -s {url} | jq -e '{filter}'"
+    netdata.wait_until_succeeds(cmd)
+  '';
+})
diff --git a/nixos/tests/networking-proxy.nix b/nixos/tests/networking-proxy.nix
new file mode 100644
index 00000000000..fcb2558cf3b
--- /dev/null
+++ b/nixos/tests/networking-proxy.nix
@@ -0,0 +1,134 @@
+# Test whether `networking.proxy' work as expected.
+
+# TODO: use a real proxy node and put this test into networking.nix
+# TODO: test whether nix tools work as expected behind a proxy
+
+let default-config = {
+        imports = [ ./common/user-account.nix ];
+
+        services.xserver.enable = false;
+
+      };
+in import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "networking-proxy";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [  ];
+  };
+
+  nodes = {
+    # no proxy
+    machine =
+      { ... }:
+
+      default-config;
+
+    # proxy default
+    machine2 =
+      { ... }:
+
+      default-config // {
+        networking.proxy.default = "http://user:pass@host:port";
+      };
+
+    # specific proxy options
+    machine3 =
+      { ... }:
+
+      default-config //
+      {
+        networking.proxy = {
+          # useless because overriden by the next options
+          default = "http://user:pass@host:port";
+          # advanced proxy setup
+          httpProxy = "123-http://user:pass@http-host:port";
+          httpsProxy = "456-http://user:pass@https-host:port";
+          rsyncProxy = "789-http://user:pass@rsync-host:port";
+          ftpProxy = "101112-http://user:pass@ftp-host:port";
+          noProxy = "131415-127.0.0.1,localhost,.localdomain";
+        };
+      };
+
+    # mix default + proxy options
+    machine4 =
+      { ... }:
+
+      default-config // {
+        networking.proxy = {
+          # open for all *_proxy env var
+          default = "000-http://user:pass@default-host:port";
+          # except for those 2
+          rsyncProxy = "123-http://user:pass@http-host:port";
+          noProxy = "131415-127.0.0.1,localhost,.localdomain";
+        };
+      };
+    };
+
+  testScript =
+    ''
+      from typing import Dict, Optional
+
+
+      def get_machine_env(machine: Machine, user: Optional[str] = None) -> Dict[str, str]:
+          """
+          Gets the environment from a given machine, and returns it as a
+          dictionary in the form:
+              {"lowercase_var_name": "value"}
+
+          Duplicate environment variables with the same name
+          (e.g. "foo" and "FOO") are handled in an undefined manner.
+          """
+          if user is not None:
+              env = machine.succeed("su - {} -c 'env -0'".format(user))
+          else:
+              env = machine.succeed("env -0")
+          ret = {}
+          for line in env.split("\0"):
+              if "=" not in line:
+                  continue
+
+              key, val = line.split("=", 1)
+              ret[key.lower()] = val
+          return ret
+
+
+      start_all()
+
+      with subtest("no proxy"):
+          assert "proxy" not in machine.succeed("env").lower()
+          assert "proxy" not in machine.succeed("su - alice -c env").lower()
+
+      with subtest("default proxy"):
+          assert "proxy" in machine2.succeed("env").lower()
+          assert "proxy" in machine2.succeed("su - alice -c env").lower()
+
+      with subtest("explicitly-set proxy"):
+          env = get_machine_env(machine3)
+          assert "123" in env["http_proxy"]
+          assert "456" in env["https_proxy"]
+          assert "789" in env["rsync_proxy"]
+          assert "101112" in env["ftp_proxy"]
+          assert "131415" in env["no_proxy"]
+
+          env = get_machine_env(machine3, "alice")
+          assert "123" in env["http_proxy"]
+          assert "456" in env["https_proxy"]
+          assert "789" in env["rsync_proxy"]
+          assert "101112" in env["ftp_proxy"]
+          assert "131415" in env["no_proxy"]
+
+      with subtest("default proxy + some other specifics"):
+          env = get_machine_env(machine4)
+          assert "000" in env["http_proxy"]
+          assert "000" in env["https_proxy"]
+          assert "123" in env["rsync_proxy"]
+          assert "000" in env["ftp_proxy"]
+          assert "131415" in env["no_proxy"]
+
+          env = get_machine_env(machine4, "alice")
+          assert "000" in env["http_proxy"]
+          assert "000" in env["https_proxy"]
+          assert "123" in env["rsync_proxy"]
+          assert "000" in env["ftp_proxy"]
+          assert "131415" in env["no_proxy"]
+    '';
+})
diff --git a/nixos/tests/networking.nix b/nixos/tests/networking.nix
new file mode 100644
index 00000000000..b763cbd4665
--- /dev/null
+++ b/nixos/tests/networking.nix
@@ -0,0 +1,925 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+# bool: whether to use networkd in the tests
+, networkd }:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  qemu-common = import ../lib/qemu-common.nix { inherit (pkgs) lib pkgs; };
+
+  router = { config, pkgs, lib, ... }:
+    with pkgs.lib;
+    let
+      vlanIfs = range 1 (length config.virtualisation.vlans);
+    in {
+      environment.systemPackages = [ pkgs.iptables ]; # to debug firewall rules
+      virtualisation.vlans = [ 1 2 3 ];
+      boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = true;
+      networking = {
+        useDHCP = false;
+        useNetworkd = networkd;
+        firewall.checkReversePath = true;
+        firewall.allowedUDPPorts = [ 547 ];
+        interfaces = mkOverride 0 (listToAttrs (forEach vlanIfs (n:
+          nameValuePair "eth${toString n}" {
+            ipv4.addresses = [ { address = "192.168.${toString n}.1"; prefixLength = 24; } ];
+            ipv6.addresses = [ { address = "fd00:1234:5678:${toString n}::1"; prefixLength = 64; } ];
+          })));
+      };
+      services.dhcpd4 = {
+        enable = true;
+        interfaces = map (n: "eth${toString n}") vlanIfs;
+        extraConfig = flip concatMapStrings vlanIfs (n: ''
+          subnet 192.168.${toString n}.0 netmask 255.255.255.0 {
+            option routers 192.168.${toString n}.1;
+            range 192.168.${toString n}.3 192.168.${toString n}.254;
+          }
+        '')
+        ;
+        machines = flip map vlanIfs (vlan:
+          {
+            hostName = "client${toString vlan}";
+            ethernetAddress = qemu-common.qemuNicMac vlan 1;
+            ipAddress = "192.168.${toString vlan}.2";
+          }
+        );
+      };
+      services.radvd = {
+        enable = true;
+        config = flip concatMapStrings vlanIfs (n: ''
+          interface eth${toString n} {
+            AdvSendAdvert on;
+            AdvManagedFlag on;
+            AdvOtherConfigFlag on;
+
+            prefix fd00:1234:5678:${toString n}::/64 {
+              AdvAutonomous off;
+            };
+          };
+        '');
+      };
+      services.dhcpd6 = {
+        enable = true;
+        interfaces = map (n: "eth${toString n}") vlanIfs;
+        extraConfig = ''
+          authoritative;
+        '' + flip concatMapStrings vlanIfs (n: ''
+          subnet6 fd00:1234:5678:${toString n}::/64 {
+            range6 fd00:1234:5678:${toString n}::2 fd00:1234:5678:${toString n}::2;
+          }
+        '');
+      };
+    };
+
+  testCases = {
+    loopback = {
+      name = "Loopback";
+      machine.networking.useDHCP = false;
+      machine.networking.useNetworkd = networkd;
+      testScript = ''
+        start_all()
+        machine.wait_for_unit("network.target")
+        loopback_addresses = machine.succeed("ip addr show lo")
+        assert "inet 127.0.0.1/8" in loopback_addresses
+        assert "inet6 ::1/128" in loopback_addresses
+      '';
+    };
+    static = {
+      name = "Static";
+      nodes.router = router;
+      nodes.client = { pkgs, ... }: with pkgs.lib; {
+        virtualisation.vlans = [ 1 2 ];
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+          defaultGateway = "192.168.1.1";
+          interfaces.eth1.ipv4.addresses = mkOverride 0 [
+            { address = "192.168.1.2"; prefixLength = 24; }
+            { address = "192.168.1.3"; prefixLength = 32; }
+            { address = "192.168.1.10"; prefixLength = 32; }
+          ];
+          interfaces.eth2.ipv4.addresses = mkOverride 0 [
+            { address = "192.168.2.2"; prefixLength = 24; }
+          ];
+        };
+      };
+      testScript = { ... }:
+        ''
+          start_all()
+
+          client.wait_for_unit("network.target")
+          router.wait_for_unit("network-online.target")
+
+          with subtest("Make sure dhcpcd is not started"):
+              client.fail("systemctl status dhcpcd.service")
+
+          with subtest("Test vlan 1"):
+              client.wait_until_succeeds("ping -c 1 192.168.1.1")
+              client.wait_until_succeeds("ping -c 1 192.168.1.2")
+              client.wait_until_succeeds("ping -c 1 192.168.1.3")
+              client.wait_until_succeeds("ping -c 1 192.168.1.10")
+
+              router.wait_until_succeeds("ping -c 1 192.168.1.1")
+              router.wait_until_succeeds("ping -c 1 192.168.1.2")
+              router.wait_until_succeeds("ping -c 1 192.168.1.3")
+              router.wait_until_succeeds("ping -c 1 192.168.1.10")
+
+          with subtest("Test vlan 2"):
+              client.wait_until_succeeds("ping -c 1 192.168.2.1")
+              client.wait_until_succeeds("ping -c 1 192.168.2.2")
+
+              router.wait_until_succeeds("ping -c 1 192.168.2.1")
+              router.wait_until_succeeds("ping -c 1 192.168.2.2")
+
+          with subtest("Test default gateway"):
+              router.wait_until_succeeds("ping -c 1 192.168.3.1")
+              client.wait_until_succeeds("ping -c 1 192.168.3.1")
+        '';
+    };
+    dhcpSimple = {
+      name = "SimpleDHCP";
+      nodes.router = router;
+      nodes.client = { pkgs, ... }: with pkgs.lib; {
+        virtualisation.vlans = [ 1 2 ];
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+          interfaces.eth1 = {
+            ipv4.addresses = mkOverride 0 [ ];
+            ipv6.addresses = mkOverride 0 [ ];
+            useDHCP = true;
+          };
+          interfaces.eth2 = {
+            ipv4.addresses = mkOverride 0 [ ];
+            ipv6.addresses = mkOverride 0 [ ];
+            useDHCP = true;
+          };
+        };
+      };
+      testScript = { ... }:
+        ''
+          start_all()
+
+          client.wait_for_unit("network.target")
+          router.wait_for_unit("network-online.target")
+
+          with subtest("Wait until we have an ip address on each interface"):
+              client.wait_until_succeeds("ip addr show dev eth1 | grep -q '192.168.1'")
+              client.wait_until_succeeds("ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'")
+              client.wait_until_succeeds("ip addr show dev eth2 | grep -q '192.168.2'")
+              client.wait_until_succeeds("ip addr show dev eth2 | grep -q 'fd00:1234:5678:2:'")
+
+          with subtest("Test vlan 1"):
+              client.wait_until_succeeds("ping -c 1 192.168.1.1")
+              client.wait_until_succeeds("ping -c 1 192.168.1.2")
+              client.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::1")
+              client.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::2")
+
+              router.wait_until_succeeds("ping -c 1 192.168.1.1")
+              router.wait_until_succeeds("ping -c 1 192.168.1.2")
+              router.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::1")
+              router.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::2")
+
+          with subtest("Test vlan 2"):
+              client.wait_until_succeeds("ping -c 1 192.168.2.1")
+              client.wait_until_succeeds("ping -c 1 192.168.2.2")
+              client.wait_until_succeeds("ping -c 1 fd00:1234:5678:2::1")
+              client.wait_until_succeeds("ping -c 1 fd00:1234:5678:2::2")
+
+              router.wait_until_succeeds("ping -c 1 192.168.2.1")
+              router.wait_until_succeeds("ping -c 1 192.168.2.2")
+              router.wait_until_succeeds("ping -c 1 fd00:1234:5678:2::1")
+              router.wait_until_succeeds("ping -c 1 fd00:1234:5678:2::2")
+        '';
+    };
+    dhcpOneIf = {
+      name = "OneInterfaceDHCP";
+      nodes.router = router;
+      nodes.client = { pkgs, ... }: with pkgs.lib; {
+        virtualisation.vlans = [ 1 2 ];
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+          interfaces.eth1 = {
+            ipv4.addresses = mkOverride 0 [ ];
+            mtu = 1343;
+            useDHCP = true;
+          };
+          interfaces.eth2.ipv4.addresses = mkOverride 0 [ ];
+        };
+      };
+      testScript = { ... }:
+        ''
+          start_all()
+
+          with subtest("Wait for networking to come up"):
+              client.wait_for_unit("network.target")
+              router.wait_for_unit("network.target")
+
+          with subtest("Wait until we have an ip address on each interface"):
+              client.wait_until_succeeds("ip addr show dev eth1 | grep -q '192.168.1'")
+
+          with subtest("ensure MTU is set"):
+              assert "mtu 1343" in client.succeed("ip link show dev eth1")
+
+          with subtest("Test vlan 1"):
+              client.wait_until_succeeds("ping -c 1 192.168.1.1")
+              client.wait_until_succeeds("ping -c 1 192.168.1.2")
+
+              router.wait_until_succeeds("ping -c 1 192.168.1.1")
+              router.wait_until_succeeds("ping -c 1 192.168.1.2")
+
+          with subtest("Test vlan 2"):
+              client.wait_until_succeeds("ping -c 1 192.168.2.1")
+              client.fail("ping -c 1 192.168.2.2")
+
+              router.wait_until_succeeds("ping -c 1 192.168.2.1")
+              router.fail("ping -c 1 192.168.2.2")
+        '';
+    };
+    bond = let
+      node = address: { pkgs, ... }: with pkgs.lib; {
+        virtualisation.vlans = [ 1 2 ];
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+          bonds.bond0 = {
+            interfaces = [ "eth1" "eth2" ];
+            driverOptions.mode = "802.3ad";
+          };
+          interfaces.eth1.ipv4.addresses = mkOverride 0 [ ];
+          interfaces.eth2.ipv4.addresses = mkOverride 0 [ ];
+          interfaces.bond0.ipv4.addresses = mkOverride 0
+            [ { inherit address; prefixLength = 30; } ];
+        };
+      };
+    in {
+      name = "Bond";
+      nodes.client1 = node "192.168.1.1";
+      nodes.client2 = node "192.168.1.2";
+      testScript = { ... }:
+        ''
+          start_all()
+
+          with subtest("Wait for networking to come up"):
+              client1.wait_for_unit("network.target")
+              client2.wait_for_unit("network.target")
+
+          with subtest("Test bonding"):
+              client1.wait_until_succeeds("ping -c 2 192.168.1.1")
+              client1.wait_until_succeeds("ping -c 2 192.168.1.2")
+
+              client2.wait_until_succeeds("ping -c 2 192.168.1.1")
+              client2.wait_until_succeeds("ping -c 2 192.168.1.2")
+
+          with subtest("Verify bonding mode"):
+              for client in client1, client2:
+                  client.succeed('grep -q "Bonding Mode: IEEE 802.3ad Dynamic link aggregation" /proc/net/bonding/bond0')
+        '';
+    };
+    bridge = let
+      node = { address, vlan }: { pkgs, ... }: with pkgs.lib; {
+        virtualisation.vlans = [ vlan ];
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+          interfaces.eth1.ipv4.addresses = mkOverride 0
+            [ { inherit address; prefixLength = 24; } ];
+        };
+      };
+    in {
+      name = "Bridge";
+      nodes.client1 = node { address = "192.168.1.2"; vlan = 1; };
+      nodes.client2 = node { address = "192.168.1.3"; vlan = 2; };
+      nodes.router = { pkgs, ... }: with pkgs.lib; {
+        virtualisation.vlans = [ 1 2 ];
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+          bridges.bridge.interfaces = [ "eth1" "eth2" ];
+          interfaces.eth1.ipv4.addresses = mkOverride 0 [ ];
+          interfaces.eth2.ipv4.addresses = mkOverride 0 [ ];
+          interfaces.bridge.ipv4.addresses = mkOverride 0
+            [ { address = "192.168.1.1"; prefixLength = 24; } ];
+        };
+      };
+      testScript = { ... }:
+        ''
+          start_all()
+
+          with subtest("Wait for networking to come up"):
+              for machine in client1, client2, router:
+                  machine.wait_for_unit("network.target")
+
+          with subtest("Test bridging"):
+              client1.wait_until_succeeds("ping -c 1 192.168.1.1")
+              client1.wait_until_succeeds("ping -c 1 192.168.1.2")
+              client1.wait_until_succeeds("ping -c 1 192.168.1.3")
+
+              client2.wait_until_succeeds("ping -c 1 192.168.1.1")
+              client2.wait_until_succeeds("ping -c 1 192.168.1.2")
+              client2.wait_until_succeeds("ping -c 1 192.168.1.3")
+
+              router.wait_until_succeeds("ping -c 1 192.168.1.1")
+              router.wait_until_succeeds("ping -c 1 192.168.1.2")
+              router.wait_until_succeeds("ping -c 1 192.168.1.3")
+        '';
+    };
+    macvlan = {
+      name = "MACVLAN";
+      nodes.router = router;
+      nodes.client = { pkgs, ... }: with pkgs.lib; {
+        environment.systemPackages = [ pkgs.iptables ]; # to debug firewall rules
+        virtualisation.vlans = [ 1 ];
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+          firewall.logReversePathDrops = true; # to debug firewall rules
+          # reverse path filtering rules for the macvlan interface seem
+          # to be incorrect, causing the test to fail. Disable temporarily.
+          firewall.checkReversePath = false;
+          macvlans.macvlan.interface = "eth1";
+          interfaces.eth1 = {
+            ipv4.addresses = mkOverride 0 [ ];
+            useDHCP = true;
+          };
+          interfaces.macvlan = {
+            useDHCP = true;
+          };
+        };
+      };
+      testScript = { ... }:
+        ''
+          start_all()
+
+          with subtest("Wait for networking to come up"):
+              client.wait_for_unit("network.target")
+              router.wait_for_unit("network.target")
+
+          with subtest("Wait until we have an ip address on each interface"):
+              client.wait_until_succeeds("ip addr show dev eth1 | grep -q '192.168.1'")
+              client.wait_until_succeeds("ip addr show dev macvlan | grep -q '192.168.1'")
+
+          with subtest("Print lots of diagnostic information"):
+              router.log("**********************************************")
+              router.succeed("ip addr >&2")
+              router.succeed("ip route >&2")
+              router.execute("iptables-save >&2")
+              client.log("==============================================")
+              client.succeed("ip addr >&2")
+              client.succeed("ip route >&2")
+              client.execute("iptables-save >&2")
+              client.log("##############################################")
+
+          with subtest("Test macvlan creates routable ips"):
+              client.wait_until_succeeds("ping -c 1 192.168.1.1")
+              client.wait_until_succeeds("ping -c 1 192.168.1.2")
+              client.wait_until_succeeds("ping -c 1 192.168.1.3")
+
+              router.wait_until_succeeds("ping -c 1 192.168.1.1")
+              router.wait_until_succeeds("ping -c 1 192.168.1.2")
+              router.wait_until_succeeds("ping -c 1 192.168.1.3")
+        '';
+    };
+    fou = {
+      name = "foo-over-udp";
+      nodes.machine = { ... }: {
+        virtualisation.vlans = [ 1 ];
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+          interfaces.eth1.ipv4.addresses = mkOverride 0
+            [ { address = "192.168.1.1"; prefixLength = 24; } ];
+          fooOverUDP = {
+            fou1 = { port = 9001; };
+            fou2 = { port = 9002; protocol = 41; };
+            fou3 = mkIf (!networkd)
+              { port = 9003; local.address = "192.168.1.1"; };
+            fou4 = mkIf (!networkd)
+              { port = 9004; local = { address = "192.168.1.1"; dev = "eth1"; }; };
+          };
+        };
+        systemd.services = {
+          fou3-fou-encap.after = optional (!networkd) "network-addresses-eth1.service";
+        };
+      };
+      testScript = { ... }:
+        ''
+          import json
+
+          machine.wait_for_unit("network.target")
+          fous = json.loads(machine.succeed("ip -json fou show"))
+          assert {"port": 9001, "gue": None, "family": "inet"} in fous, "fou1 exists"
+          assert {"port": 9002, "ipproto": 41, "family": "inet"} in fous, "fou2 exists"
+        '' + optionalString (!networkd) ''
+          assert {
+              "port": 9003,
+              "gue": None,
+              "family": "inet",
+              "local": "192.168.1.1",
+          } in fous, "fou3 exists"
+          assert {
+              "port": 9004,
+              "gue": None,
+              "family": "inet",
+              "local": "192.168.1.1",
+              "dev": "eth1",
+          } in fous, "fou4 exists"
+        '';
+    };
+    sit = let
+      node = { address4, remote, address6 }: { pkgs, ... }: with pkgs.lib; {
+        virtualisation.vlans = [ 1 ];
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+          sits.sit = {
+            inherit remote;
+            local = address4;
+            dev = "eth1";
+          };
+          interfaces.eth1.ipv4.addresses = mkOverride 0
+            [ { address = address4; prefixLength = 24; } ];
+          interfaces.sit.ipv6.addresses = mkOverride 0
+            [ { address = address6; prefixLength = 64; } ];
+        };
+      };
+    in {
+      name = "Sit";
+      # note on firewalling: the two nodes are explicitly asymmetric.
+      # client1 sends SIT packets in UDP, but accepts only proto-41 incoming.
+      # client2 does the reverse, sending in proto-41 and accepting only UDP incoming.
+      # that way we'll notice when either SIT itself or FOU breaks.
+      nodes.client1 = args@{ pkgs, ... }:
+        mkMerge [
+          (node { address4 = "192.168.1.1"; remote = "192.168.1.2"; address6 = "fc00::1"; } args)
+          {
+            networking = {
+              firewall.extraCommands = "iptables -A INPUT -p 41 -j ACCEPT";
+              sits.sit.encapsulation = { type = "fou"; port = 9001; };
+            };
+          }
+        ];
+      nodes.client2 = args@{ pkgs, ... }:
+        mkMerge [
+          (node { address4 = "192.168.1.2"; remote = "192.168.1.1"; address6 = "fc00::2"; } args)
+          {
+            networking = {
+              firewall.allowedUDPPorts = [ 9001 ];
+              fooOverUDP.fou1 = { port = 9001; protocol = 41; };
+            };
+          }
+        ];
+      testScript = { ... }:
+        ''
+          start_all()
+
+          with subtest("Wait for networking to be configured"):
+              client1.wait_for_unit("network.target")
+              client2.wait_for_unit("network.target")
+
+              # Print diagnostic information
+              client1.succeed("ip addr >&2")
+              client2.succeed("ip addr >&2")
+
+          with subtest("Test ipv6"):
+              client1.wait_until_succeeds("ping -c 1 fc00::1")
+              client1.wait_until_succeeds("ping -c 1 fc00::2")
+
+              client2.wait_until_succeeds("ping -c 1 fc00::1")
+              client2.wait_until_succeeds("ping -c 1 fc00::2")
+        '';
+    };
+    gre = let
+      node = { pkgs, ... }: with pkgs.lib; {
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+          firewall.extraCommands = "ip6tables -A nixos-fw -p gre -j nixos-fw-accept";
+        };
+      };
+    in {
+      name = "GRE";
+      nodes.client1 = args@{ pkgs, ... }:
+        mkMerge [
+          (node args)
+          {
+            virtualisation.vlans = [ 1 2 4 ];
+            networking = {
+              greTunnels = {
+                greTunnel = {
+                  local = "192.168.2.1";
+                  remote = "192.168.2.2";
+                  dev = "eth2";
+                  type = "tap";
+                };
+                gre6Tunnel = {
+                  local = "fd00:1234:5678:4::1";
+                  remote = "fd00:1234:5678:4::2";
+                  dev = "eth3";
+                  type = "tun6";
+                };
+              };
+              bridges.bridge.interfaces = [ "greTunnel" "eth1" ];
+              interfaces.eth1.ipv4.addresses = mkOverride 0 [];
+              interfaces.bridge.ipv4.addresses = mkOverride 0 [
+                { address = "192.168.1.1"; prefixLength = 24; }
+              ];
+              interfaces.eth3.ipv6.addresses = [
+                { address = "fd00:1234:5678:4::1"; prefixLength = 64; }
+              ];
+              interfaces.gre6Tunnel.ipv6.addresses = mkOverride 0 [
+                { address = "fc00::1"; prefixLength = 64; }
+              ];
+            };
+          }
+        ];
+      nodes.client2 = args@{ pkgs, ... }:
+        mkMerge [
+          (node args)
+          {
+            virtualisation.vlans = [ 2 3 4 ];
+            networking = {
+              greTunnels = {
+                greTunnel = {
+                  local = "192.168.2.2";
+                  remote = "192.168.2.1";
+                  dev = "eth1";
+                  type = "tap";
+                };
+                gre6Tunnel = {
+                  local = "fd00:1234:5678:4::2";
+                  remote = "fd00:1234:5678:4::1";
+                  dev = "eth3";
+                  type = "tun6";
+                };
+              };
+              bridges.bridge.interfaces = [ "greTunnel" "eth2" ];
+              interfaces.eth2.ipv4.addresses = mkOverride 0 [];
+              interfaces.bridge.ipv4.addresses = mkOverride 0 [
+                { address = "192.168.1.2"; prefixLength = 24; }
+              ];
+              interfaces.eth3.ipv6.addresses = [
+                { address = "fd00:1234:5678:4::2"; prefixLength = 64; }
+              ];
+              interfaces.gre6Tunnel.ipv6.addresses = mkOverride 0 [
+                { address = "fc00::2"; prefixLength = 64; }
+              ];
+            };
+          }
+        ];
+      testScript = { ... }:
+        ''
+          start_all()
+
+          with subtest("Wait for networking to be configured"):
+              client1.wait_for_unit("network.target")
+              client2.wait_for_unit("network.target")
+
+              # Print diagnostic information
+              client1.succeed("ip addr >&2")
+              client2.succeed("ip addr >&2")
+
+          with subtest("Test GRE tunnel bridge over VLAN"):
+              client1.wait_until_succeeds("ping -c 1 192.168.1.2")
+
+              client2.wait_until_succeeds("ping -c 1 192.168.1.1")
+
+              client1.wait_until_succeeds("ping -c 1 fc00::2")
+
+              client2.wait_until_succeeds("ping -c 1 fc00::1")
+        '';
+    };
+    vlan = let
+      node = address: { pkgs, ... }: with pkgs.lib; {
+        #virtualisation.vlans = [ 1 ];
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+          vlans.vlan = {
+            id = 1;
+            interface = "eth0";
+          };
+          interfaces.eth0.ipv4.addresses = mkOverride 0 [ ];
+          interfaces.eth1.ipv4.addresses = mkOverride 0 [ ];
+          interfaces.vlan.ipv4.addresses = mkOverride 0
+            [ { inherit address; prefixLength = 24; } ];
+        };
+      };
+    in {
+      name = "vlan";
+      nodes.client1 = node "192.168.1.1";
+      nodes.client2 = node "192.168.1.2";
+      testScript = { ... }:
+        ''
+          start_all()
+
+          with subtest("Wait for networking to be configured"):
+              client1.wait_for_unit("network.target")
+              client2.wait_for_unit("network.target")
+
+          with subtest("Test vlan is setup"):
+              client1.succeed("ip addr show dev vlan >&2")
+              client2.succeed("ip addr show dev vlan >&2")
+        '';
+    };
+    virtual = {
+      name = "Virtual";
+      machine = {
+        networking.useNetworkd = networkd;
+        networking.useDHCP = false;
+        networking.interfaces.tap0 = {
+          ipv4.addresses = [ { address = "192.168.1.1"; prefixLength = 24; } ];
+          ipv6.addresses = [ { address = "2001:1470:fffd:2096::"; prefixLength = 64; } ];
+          virtual = true;
+          mtu = 1342;
+          macAddress = "02:de:ad:be:ef:01";
+        };
+        networking.interfaces.tun0 = {
+          ipv4.addresses = [ { address = "192.168.1.2"; prefixLength = 24; } ];
+          ipv6.addresses = [ { address = "2001:1470:fffd:2097::"; prefixLength = 64; } ];
+          virtual = true;
+          mtu = 1343;
+        };
+      };
+
+      testScript = ''
+        targetList = """
+        tap0: tap persist user 0
+        tun0: tun persist user 0
+        """.strip()
+
+        with subtest("Wait for networking to come up"):
+            machine.start()
+            machine.wait_for_unit("network.target")
+
+        with subtest("Test interfaces set up"):
+            list = machine.succeed("ip tuntap list | sort").strip()
+            assert (
+                list == targetList
+            ), """
+            The list of virtual interfaces does not match the expected one:
+            Result:
+              {}
+            Expected:
+              {}
+            """.format(
+                list, targetList
+            )
+        with subtest("Test MTU and MAC Address are configured"):
+            machine.wait_until_succeeds("ip link show dev tap0 | grep 'mtu 1342'")
+            machine.wait_until_succeeds("ip link show dev tun0 | grep 'mtu 1343'")
+            assert "02:de:ad:be:ef:01" in machine.succeed("ip link show dev tap0")
+      '' # network-addresses-* only exist in scripted networking
+      + optionalString (!networkd) ''
+        with subtest("Test interfaces clean up"):
+            machine.succeed("systemctl stop network-addresses-tap0")
+            machine.sleep(10)
+            machine.succeed("systemctl stop network-addresses-tun0")
+            machine.sleep(10)
+            residue = machine.succeed("ip tuntap list")
+            assert (
+                residue == ""
+            ), "Some virtual interface has not been properly cleaned:\n{}".format(residue)
+      '';
+    };
+    privacy = {
+      name = "Privacy";
+      nodes.router = { ... }: {
+        virtualisation.vlans = [ 1 ];
+        boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = true;
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+          interfaces.eth1.ipv6.addresses = singleton {
+            address = "fd00:1234:5678:1::1";
+            prefixLength = 64;
+          };
+        };
+        services.radvd = {
+          enable = true;
+          config = ''
+            interface eth1 {
+              AdvSendAdvert on;
+              AdvManagedFlag on;
+              AdvOtherConfigFlag on;
+
+              prefix fd00:1234:5678:1::/64 {
+                AdvAutonomous on;
+                AdvOnLink on;
+              };
+            };
+          '';
+        };
+      };
+      nodes.client_with_privacy = { pkgs, ... }: with pkgs.lib; {
+        virtualisation.vlans = [ 1 ];
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+          interfaces.eth1 = {
+            tempAddress = "default";
+            ipv4.addresses = mkOverride 0 [ ];
+            ipv6.addresses = mkOverride 0 [ ];
+            useDHCP = true;
+          };
+        };
+      };
+      nodes.client = { pkgs, ... }: with pkgs.lib; {
+        virtualisation.vlans = [ 1 ];
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+          interfaces.eth1 = {
+            tempAddress = "enabled";
+            ipv4.addresses = mkOverride 0 [ ];
+            ipv6.addresses = mkOverride 0 [ ];
+            useDHCP = true;
+          };
+        };
+      };
+      testScript = { ... }:
+        ''
+          start_all()
+
+          client.wait_for_unit("network.target")
+          client_with_privacy.wait_for_unit("network.target")
+          router.wait_for_unit("network-online.target")
+
+          with subtest("Wait until we have an ip address"):
+              client_with_privacy.wait_until_succeeds(
+                  "ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'"
+              )
+              client.wait_until_succeeds("ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'")
+
+          with subtest("Test vlan 1"):
+              client_with_privacy.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::1")
+              client.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::1")
+
+          with subtest("Test address used is temporary"):
+              client_with_privacy.wait_until_succeeds(
+                  "! ip route get fd00:1234:5678:1::1 | grep -q ':[a-f0-9]*ff:fe[a-f0-9]*:'"
+              )
+
+          with subtest("Test address used is EUI-64"):
+              client.wait_until_succeeds(
+                  "ip route get fd00:1234:5678:1::1 | grep -q ':[a-f0-9]*ff:fe[a-f0-9]*:'"
+              )
+        '';
+    };
+    routes = {
+      name = "routes";
+      machine = {
+        networking.useNetworkd = networkd;
+        networking.useDHCP = false;
+        networking.interfaces.eth0 = {
+          ipv4.addresses = [ { address = "192.168.1.2"; prefixLength = 24; } ];
+          ipv6.addresses = [ { address = "2001:1470:fffd:2097::"; prefixLength = 64; } ];
+          ipv6.routes = [
+            { address = "fdfd:b3f0::"; prefixLength = 48; }
+            { address = "2001:1470:fffd:2098::"; prefixLength = 64; via = "fdfd:b3f0::1"; }
+          ];
+          ipv4.routes = [
+            { address = "10.0.0.0"; prefixLength = 16; options = {
+              mtu = "1500";
+              # Explicitly set scope because iproute and systemd-networkd
+              # disagree on what the scope should be
+              # if the type is the default "unicast"
+              scope = "link";
+            }; }
+            { address = "192.168.2.0"; prefixLength = 24; via = "192.168.1.1"; }
+          ];
+        };
+        virtualisation.vlans = [ ];
+      };
+
+      testScript = ''
+        targetIPv4Table = [
+            "10.0.0.0/16 proto static scope link mtu 1500",
+            "192.168.1.0/24 proto kernel scope link src 192.168.1.2",
+            "192.168.2.0/24 via 192.168.1.1 proto static",
+        ]
+
+        targetIPv6Table = [
+            "2001:1470:fffd:2097::/64 proto kernel metric 256 pref medium",
+            "2001:1470:fffd:2098::/64 via fdfd:b3f0::1 proto static metric 1024 pref medium",
+            "fdfd:b3f0::/48 proto static metric 1024 pref medium",
+        ]
+
+        machine.start()
+        machine.wait_for_unit("network.target")
+
+        with subtest("test routing tables"):
+            ipv4Table = machine.succeed("ip -4 route list dev eth0 | head -n3").strip()
+            ipv6Table = machine.succeed("ip -6 route list dev eth0 | head -n3").strip()
+            assert [
+                l.strip() for l in ipv4Table.splitlines()
+            ] == targetIPv4Table, """
+              The IPv4 routing table does not match the expected one:
+                Result:
+                  {}
+                Expected:
+                  {}
+              """.format(
+                ipv4Table, targetIPv4Table
+            )
+            assert [
+                l.strip() for l in ipv6Table.splitlines()
+            ] == targetIPv6Table, """
+              The IPv6 routing table does not match the expected one:
+                Result:
+                  {}
+                Expected:
+                  {}
+              """.format(
+                ipv6Table, targetIPv6Table
+            )
+
+      '' + optionalString (!networkd) ''
+        with subtest("test clean-up of the tables"):
+            machine.succeed("systemctl stop network-addresses-eth0")
+            ipv4Residue = machine.succeed("ip -4 route list dev eth0 | head -n-3").strip()
+            ipv6Residue = machine.succeed("ip -6 route list dev eth0 | head -n-3").strip()
+            assert (
+                ipv4Residue == ""
+            ), "The IPv4 routing table has not been properly cleaned:\n{}".format(ipv4Residue)
+            assert (
+                ipv6Residue == ""
+            ), "The IPv6 routing table has not been properly cleaned:\n{}".format(ipv6Residue)
+      '';
+    };
+    rename = {
+      name = "RenameInterface";
+      machine = { pkgs, ... }: {
+        virtualisation.vlans = [ 1 ];
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+        };
+      } //
+      (if networkd
+       then { systemd.network.links."10-custom_name" = {
+                matchConfig.MACAddress = "52:54:00:12:01:01";
+                linkConfig.Name = "custom_name";
+              };
+            }
+       else { services.udev.initrdRules = ''
+               SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="52:54:00:12:01:01", KERNEL=="eth*", NAME="custom_name"
+              '';
+            });
+      testScript = ''
+        machine.succeed("udevadm settle")
+        print(machine.succeed("ip link show dev custom_name"))
+      '';
+    };
+    # even with disabled networkd, systemd.network.links should work
+    # (as it's handled by udev, not networkd)
+    link = {
+      name = "Link";
+      nodes.client = { pkgs, ... }: {
+        virtualisation.vlans = [ 1 ];
+        networking = {
+          useNetworkd = networkd;
+          useDHCP = false;
+        };
+        systemd.network.links."50-foo" = {
+          matchConfig = {
+            Name = "foo";
+            Driver = "dummy";
+          };
+          linkConfig.MTUBytes = "1442";
+        };
+      };
+      testScript = ''
+        print(client.succeed("ip l add name foo type dummy"))
+        print(client.succeed("stat /etc/systemd/network/50-foo.link"))
+        client.succeed("udevadm settle")
+        assert "mtu 1442" in client.succeed("ip l show dev foo")
+      '';
+    };
+    wlanInterface = let
+      testMac = "06:00:00:00:02:00";
+    in {
+      name = "WlanInterface";
+      machine = { pkgs, ... }: {
+        boot.kernelModules = [ "mac80211_hwsim" ];
+        networking.wlanInterfaces = {
+          wlan0 = { device = "wlan0"; };
+          wap0 = { device = "wlan0"; mac = testMac; };
+        };
+      };
+      testScript = ''
+        machine.start()
+        machine.wait_for_unit("network.target")
+        machine.wait_until_succeeds("ip address show wap0 | grep -q ${testMac}")
+        machine.fail("ip address show wlan0 | grep -q ${testMac}")
+      '';
+    };
+  };
+
+in mapAttrs (const (attrs: makeTest (attrs // {
+  name = "${attrs.name}-Networking-${if networkd then "Networkd" else "Scripted"}";
+}))) testCases
diff --git a/nixos/tests/nextcloud/basic.nix b/nixos/tests/nextcloud/basic.nix
new file mode 100644
index 00000000000..eb37470a4c7
--- /dev/null
+++ b/nixos/tests/nextcloud/basic.nix
@@ -0,0 +1,112 @@
+args@{ pkgs, nextcloudVersion ? 22, ... }:
+
+(import ../make-test-python.nix ({ pkgs, ...}: let
+  adminpass = "notproduction";
+  adminuser = "root";
+in {
+  name = "nextcloud-basic";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ globin eqyiel ];
+  };
+
+  nodes = rec {
+    # The only thing the client needs to do is download a file.
+    client = { ... }: {
+      services.davfs2.enable = true;
+      system.activationScripts.davfs2-secrets = ''
+        echo "http://nextcloud/remote.php/webdav/ ${adminuser} ${adminpass}" > /tmp/davfs2-secrets
+        chmod 600 /tmp/davfs2-secrets
+      '';
+      virtualisation.fileSystems = {
+        "/mnt/dav" = {
+          device = "http://nextcloud/remote.php/webdav/";
+          fsType = "davfs";
+          options = let
+            davfs2Conf = (pkgs.writeText "davfs2.conf" "secrets /tmp/davfs2-secrets");
+          in [ "conf=${davfs2Conf}" "x-systemd.automount" "noauto"];
+        };
+      };
+    };
+
+    nextcloud = { config, pkgs, ... }: let
+      cfg = config;
+    in {
+      networking.firewall.allowedTCPPorts = [ 80 ];
+
+      systemd.tmpfiles.rules = [
+        "d /var/lib/nextcloud-data 0750 nextcloud nginx - -"
+      ];
+
+      services.nextcloud = {
+        enable = true;
+        datadir = "/var/lib/nextcloud-data";
+        hostName = "nextcloud";
+        config = {
+          # Don't inherit adminuser since "root" is supposed to be the default
+          adminpassFile = "${pkgs.writeText "adminpass" adminpass}"; # Don't try this at home!
+          dbtableprefix = "nixos_";
+        };
+        package = pkgs.${"nextcloud" + (toString nextcloudVersion)};
+        autoUpdateApps = {
+          enable = true;
+          startAt = "20:00";
+        };
+        phpExtraExtensions = all: [ all.bz2 ];
+      };
+
+      environment.systemPackages = [ cfg.services.nextcloud.occ ];
+    };
+
+    nextcloudWithoutMagick = args@{ config, pkgs, lib, ... }:
+      lib.mkMerge
+      [ (nextcloud args)
+        { services.nextcloud.enableImagemagick = false; } ];
+  };
+
+  testScript = { nodes, ... }: let
+    withRcloneEnv = pkgs.writeScript "with-rclone-env" ''
+      #!${pkgs.runtimeShell}
+      export RCLONE_CONFIG_NEXTCLOUD_TYPE=webdav
+      export RCLONE_CONFIG_NEXTCLOUD_URL="http://nextcloud/remote.php/webdav/"
+      export RCLONE_CONFIG_NEXTCLOUD_VENDOR="nextcloud"
+      export RCLONE_CONFIG_NEXTCLOUD_USER="${adminuser}"
+      export RCLONE_CONFIG_NEXTCLOUD_PASS="$(${pkgs.rclone}/bin/rclone obscure ${adminpass})"
+      "''${@}"
+    '';
+    copySharedFile = pkgs.writeScript "copy-shared-file" ''
+      #!${pkgs.runtimeShell}
+      echo 'hi' | ${withRcloneEnv} ${pkgs.rclone}/bin/rclone rcat nextcloud:test-shared-file
+    '';
+
+    diffSharedFile = pkgs.writeScript "diff-shared-file" ''
+      #!${pkgs.runtimeShell}
+      diff <(echo 'hi') <(${pkgs.rclone}/bin/rclone cat nextcloud:test-shared-file)
+    '';
+
+    findInClosure = what: drv: pkgs.runCommand "find-in-closure" { exportReferencesGraph = [ "graph" drv ]; inherit what; } ''
+      test -e graph
+      grep "$what" graph >$out || true
+    '';
+    nextcloudUsesImagick = findInClosure "imagick" nodes.nextcloud.config.system.build.vm;
+    nextcloudWithoutDoesntUseIt = findInClosure "imagick" nodes.nextcloudWithoutMagick.config.system.build.vm;
+  in ''
+    assert open("${nextcloudUsesImagick}").read() != ""
+    assert open("${nextcloudWithoutDoesntUseIt}").read() == ""
+
+    nextcloud.start()
+    client.start()
+    nextcloud.wait_for_unit("multi-user.target")
+    # This is just to ensure the nextcloud-occ program is working
+    nextcloud.succeed("nextcloud-occ status")
+    nextcloud.succeed("curl -sSf http://nextcloud/login")
+    nextcloud.succeed(
+        "${withRcloneEnv} ${copySharedFile}"
+    )
+    client.wait_for_unit("multi-user.target")
+    nextcloud.succeed("test -f /var/lib/nextcloud-data/data/root/files/test-shared-file")
+    client.succeed(
+        "${withRcloneEnv} ${diffSharedFile}"
+    )
+    assert "hi" in client.succeed("cat /mnt/dav/test-shared-file")
+  '';
+})) args
diff --git a/nixos/tests/nextcloud/default.nix b/nixos/tests/nextcloud/default.nix
new file mode 100644
index 00000000000..b7b1c5c6600
--- /dev/null
+++ b/nixos/tests/nextcloud/default.nix
@@ -0,0 +1,21 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../../.. { inherit system config; }
+}:
+
+with pkgs.lib;
+
+foldl
+  (matrix: ver: matrix // {
+    "basic${toString ver}" = import ./basic.nix { inherit system pkgs; nextcloudVersion = ver; };
+    "with-postgresql-and-redis${toString ver}" = import ./with-postgresql-and-redis.nix {
+      inherit system pkgs;
+      nextcloudVersion = ver;
+    };
+    "with-mysql-and-memcached${toString ver}" = import ./with-mysql-and-memcached.nix {
+      inherit system pkgs;
+      nextcloudVersion = ver;
+    };
+  })
+{ }
+  [ 22 23 ]
diff --git a/nixos/tests/nextcloud/with-mysql-and-memcached.nix b/nixos/tests/nextcloud/with-mysql-and-memcached.nix
new file mode 100644
index 00000000000..891001e30b2
--- /dev/null
+++ b/nixos/tests/nextcloud/with-mysql-and-memcached.nix
@@ -0,0 +1,110 @@
+args@{ pkgs, nextcloudVersion ? 22, ... }:
+
+(import ../make-test-python.nix ({ pkgs, ...}: let
+  adminpass = "hunter2";
+  adminuser = "root";
+in {
+  name = "nextcloud-with-mysql-and-memcached";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eqyiel ];
+  };
+
+  nodes = {
+    # The only thing the client needs to do is download a file.
+    client = { ... }: {};
+
+    nextcloud = { config, pkgs, ... }: {
+      networking.firewall.allowedTCPPorts = [ 80 ];
+
+      services.nextcloud = {
+        enable = true;
+        hostName = "nextcloud";
+        https = true;
+        package = pkgs.${"nextcloud" + (toString nextcloudVersion)};
+        caching = {
+          apcu = true;
+          redis = false;
+          memcached = 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!
+        };
+      };
+
+      services.mysql = {
+        enable = true;
+        settings.mysqld = {
+          bind-address = "127.0.0.1";
+
+          # FIXME(@Ma27) Nextcloud isn't compatible with mariadb 10.6,
+          # this is a workaround.
+          # See https://help.nextcloud.com/t/update-to-next-cloud-21-0-2-has-get-an-error/117028/22
+          innodb_read_only_compressed = 0;
+        };
+        package = pkgs.mariadb;
+
+        initialScript = pkgs.writeText "mysql-init" ''
+          CREATE USER 'nextcloud'@'localhost' IDENTIFIED BY 'hunter2';
+          CREATE DATABASE IF NOT EXISTS nextcloud;
+          GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER,
+            CREATE TEMPORARY TABLES ON nextcloud.* TO 'nextcloud'@'localhost'
+            IDENTIFIED BY 'hunter2';
+          FLUSH privileges;
+        '';
+      };
+
+      systemd.services.nextcloud-setup= {
+        requires = ["mysql.service"];
+        after = ["mysql.service"];
+      };
+
+      services.memcached.enable = true;
+    };
+  };
+
+  testScript = let
+    configureMemcached = pkgs.writeScript "configure-memcached" ''
+      #!${pkgs.runtimeShell}
+      nextcloud-occ config:system:set memcached_servers 0 0 --value 127.0.0.1 --type string
+      nextcloud-occ config:system:set memcached_servers 0 1 --value 11211 --type integer
+      nextcloud-occ config:system:set memcache.local --value '\OC\Memcache\APCu' --type string
+      nextcloud-occ config:system:set memcache.distributed --value '\OC\Memcache\Memcached' --type string
+    '';
+    withRcloneEnv = pkgs.writeScript "with-rclone-env" ''
+      #!${pkgs.runtimeShell}
+      export RCLONE_CONFIG_NEXTCLOUD_TYPE=webdav
+      export RCLONE_CONFIG_NEXTCLOUD_URL="http://nextcloud/remote.php/webdav/"
+      export RCLONE_CONFIG_NEXTCLOUD_VENDOR="nextcloud"
+      export RCLONE_CONFIG_NEXTCLOUD_USER="${adminuser}"
+      export RCLONE_CONFIG_NEXTCLOUD_PASS="$(${pkgs.rclone}/bin/rclone obscure ${adminpass})"
+    '';
+    copySharedFile = pkgs.writeScript "copy-shared-file" ''
+      #!${pkgs.runtimeShell}
+      echo 'hi' | ${pkgs.rclone}/bin/rclone rcat nextcloud:test-shared-file
+    '';
+
+    diffSharedFile = pkgs.writeScript "diff-shared-file" ''
+      #!${pkgs.runtimeShell}
+      diff <(echo 'hi') <(${pkgs.rclone}/bin/rclone cat nextcloud:test-shared-file)
+    '';
+  in ''
+    start_all()
+    nextcloud.wait_for_unit("multi-user.target")
+    nextcloud.succeed("${configureMemcached}")
+    nextcloud.succeed("curl -sSf http://nextcloud/login")
+    nextcloud.succeed(
+        "${withRcloneEnv} ${copySharedFile}"
+    )
+    client.wait_for_unit("multi-user.target")
+    client.succeed(
+        "${withRcloneEnv} ${diffSharedFile}"
+    )
+  '';
+})) args
diff --git a/nixos/tests/nextcloud/with-postgresql-and-redis.nix b/nixos/tests/nextcloud/with-postgresql-and-redis.nix
new file mode 100644
index 00000000000..36a69fda505
--- /dev/null
+++ b/nixos/tests/nextcloud/with-postgresql-and-redis.nix
@@ -0,0 +1,102 @@
+args@{ pkgs, nextcloudVersion ? 22, ... }:
+
+(import ../make-test-python.nix ({ pkgs, ...}: let
+  adminpass = "hunter2";
+  adminuser = "custom-admin-username";
+in {
+  name = "nextcloud-with-postgresql-and-redis";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eqyiel ];
+  };
+
+  nodes = {
+    # The only thing the client needs to do is download a file.
+    client = { ... }: {};
+
+    nextcloud = { config, pkgs, ... }: {
+      networking.firewall.allowedTCPPorts = [ 80 ];
+
+      services.nextcloud = {
+        enable = true;
+        hostName = "nextcloud";
+        package = pkgs.${"nextcloud" + (toString nextcloudVersion)};
+        caching = {
+          apcu = false;
+          redis = true;
+          memcached = false;
+        };
+        config = {
+          dbtype = "pgsql";
+          dbname = "nextcloud";
+          dbuser = "nextcloud";
+          dbhost = "/run/postgresql";
+          inherit adminuser;
+          adminpassFile = toString (pkgs.writeText "admin-pass-file" ''
+            ${adminpass}
+          '');
+        };
+      };
+
+      services.redis = {
+        enable = true;
+      };
+
+      systemd.services.nextcloud-setup= {
+        requires = ["postgresql.service"];
+        after = [
+          "postgresql.service"
+        ];
+      };
+
+      services.postgresql = {
+        enable = true;
+        ensureDatabases = [ "nextcloud" ];
+        ensureUsers = [
+          { name = "nextcloud";
+            ensurePermissions."DATABASE nextcloud" = "ALL PRIVILEGES";
+          }
+        ];
+      };
+    };
+  };
+
+  testScript = let
+    configureRedis = pkgs.writeScript "configure-redis" ''
+      #!${pkgs.runtimeShell}
+      nextcloud-occ config:system:set redis 'host' --value 'localhost' --type string
+      nextcloud-occ config:system:set redis 'port' --value 6379 --type integer
+      nextcloud-occ config:system:set memcache.local --value '\OC\Memcache\Redis' --type string
+      nextcloud-occ config:system:set memcache.locking --value '\OC\Memcache\Redis' --type string
+    '';
+    withRcloneEnv = pkgs.writeScript "with-rclone-env" ''
+      #!${pkgs.runtimeShell}
+      export RCLONE_CONFIG_NEXTCLOUD_TYPE=webdav
+      export RCLONE_CONFIG_NEXTCLOUD_URL="http://nextcloud/remote.php/webdav/"
+      export RCLONE_CONFIG_NEXTCLOUD_VENDOR="nextcloud"
+      export RCLONE_CONFIG_NEXTCLOUD_USER="${adminuser}"
+      export RCLONE_CONFIG_NEXTCLOUD_PASS="$(${pkgs.rclone}/bin/rclone obscure ${adminpass})"
+      "''${@}"
+    '';
+    copySharedFile = pkgs.writeScript "copy-shared-file" ''
+      #!${pkgs.runtimeShell}
+      echo 'hi' | ${pkgs.rclone}/bin/rclone rcat nextcloud:test-shared-file
+    '';
+
+    diffSharedFile = pkgs.writeScript "diff-shared-file" ''
+      #!${pkgs.runtimeShell}
+      diff <(echo 'hi') <(${pkgs.rclone}/bin/rclone cat nextcloud:test-shared-file)
+    '';
+  in ''
+    start_all()
+    nextcloud.wait_for_unit("multi-user.target")
+    nextcloud.succeed("${configureRedis}")
+    nextcloud.succeed("curl -sSf http://nextcloud/login")
+    nextcloud.succeed(
+        "${withRcloneEnv} ${copySharedFile}"
+    )
+    client.wait_for_unit("multi-user.target")
+    client.succeed(
+        "${withRcloneEnv} ${diffSharedFile}"
+    )
+  '';
+})) args
diff --git a/nixos/tests/nexus.nix b/nixos/tests/nexus.nix
new file mode 100644
index 00000000000..87bb4d2eb58
--- /dev/null
+++ b/nixos/tests/nexus.nix
@@ -0,0 +1,32 @@
+# verifies:
+#   1. nexus service starts on server
+#   2. nexus service can startup on server (creating database and all other initial stuff)
+#   3. the web application is reachable via HTTP
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "nexus";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ironpinguin ];
+  };
+
+  nodes = {
+
+    server =
+      { ... }:
+      { virtualisation.memorySize = 2047; # qemu-system-i386 has a 2047M limit
+        virtualisation.diskSize = 8192;
+
+        services.nexus.enable = true;
+      };
+
+  };
+
+  testScript = ''
+    start_all()
+
+    server.wait_for_unit("nexus")
+    server.wait_for_open_port(8081)
+
+    server.succeed("curl -f 127.0.0.1:8081")
+  '';
+})
diff --git a/nixos/tests/nfs/default.nix b/nixos/tests/nfs/default.nix
new file mode 100644
index 00000000000..6bc803c91b4
--- /dev/null
+++ b/nixos/tests/nfs/default.nix
@@ -0,0 +1,9 @@
+{ version ? 4
+, system ? builtins.currentSystem
+, pkgs ? import ../../.. { inherit system; }
+}: {
+  simple = import ./simple.nix { inherit version system pkgs; };
+} // pkgs.lib.optionalAttrs (version == 4) {
+  # TODO: Test kerberos + nfsv3
+  kerberos = import ./kerberos.nix { inherit version system pkgs; };
+}
diff --git a/nixos/tests/nfs/kerberos.nix b/nixos/tests/nfs/kerberos.nix
new file mode 100644
index 00000000000..5684131f671
--- /dev/null
+++ b/nixos/tests/nfs/kerberos.nix
@@ -0,0 +1,133 @@
+import ../make-test-python.nix ({ pkgs, lib, ... }:
+
+with lib;
+
+let
+  krb5 =
+    { enable = true;
+      domain_realm."nfs.test"   = "NFS.TEST";
+      libdefaults.default_realm = "NFS.TEST";
+      realms."NFS.TEST" =
+        { admin_server = "server.nfs.test";
+          kdc = "server.nfs.test";
+        };
+    };
+
+  hosts =
+    ''
+      192.168.1.1 client.nfs.test
+      192.168.1.2 server.nfs.test
+    '';
+
+  users = {
+    users.alice = {
+        isNormalUser = true;
+        name = "alice";
+        uid = 1000;
+      };
+  };
+
+in
+
+{
+  name = "nfsv4-with-kerberos";
+
+  nodes = {
+    client = { lib, ... }:
+      { inherit krb5 users;
+
+        networking.extraHosts = hosts;
+        networking.domain = "nfs.test";
+        networking.hostName = "client";
+
+        virtualisation.fileSystems =
+          { "/data" = {
+              device  = "server.nfs.test:/";
+              fsType  = "nfs";
+              options = [ "nfsvers=4" "sec=krb5p" "noauto" ];
+            };
+          };
+      };
+
+    server = { lib, ...}:
+      { inherit krb5 users;
+
+        networking.extraHosts = hosts;
+        networking.domain = "nfs.test";
+        networking.hostName = "server";
+
+        networking.firewall.allowedTCPPorts = [
+          111  # rpc
+          2049 # nfs
+          88   # kerberos
+          749  # kerberos admin
+        ];
+
+        services.kerberos_server.enable = true;
+        services.kerberos_server.realms =
+          { "NFS.TEST".acl =
+            [ { access = "all"; principal = "admin/admin"; } ];
+          };
+
+        services.nfs.server.enable = true;
+        services.nfs.server.createMountPoints = true;
+        services.nfs.server.exports =
+          ''
+            /data *(rw,no_root_squash,fsid=0,sec=krb5p)
+          '';
+      };
+  };
+
+  testScript =
+    ''
+      server.succeed("mkdir -p /data/alice")
+      server.succeed("chown alice:users /data/alice")
+
+      # set up kerberos database
+      server.succeed(
+          "kdb5_util create -s -r NFS.TEST -P master_key",
+          "systemctl restart kadmind.service kdc.service",
+      )
+      server.wait_for_unit("kadmind.service")
+      server.wait_for_unit("kdc.service")
+
+      # create principals
+      server.succeed(
+          "kadmin.local add_principal -randkey nfs/server.nfs.test",
+          "kadmin.local add_principal -randkey nfs/client.nfs.test",
+          "kadmin.local add_principal -pw admin_pw admin/admin",
+          "kadmin.local add_principal -pw alice_pw alice",
+      )
+
+      # add principals to server keytab
+      server.succeed("kadmin.local ktadd nfs/server.nfs.test")
+      server.succeed("systemctl start rpc-gssd.service rpc-svcgssd.service")
+      server.wait_for_unit("rpc-gssd.service")
+      server.wait_for_unit("rpc-svcgssd.service")
+
+      client.wait_for_unit("network-online.target")
+
+      # add principals to client keytab
+      client.succeed("echo admin_pw | kadmin -p admin/admin ktadd nfs/client.nfs.test")
+      client.succeed("systemctl start rpc-gssd.service")
+      client.wait_for_unit("rpc-gssd.service")
+
+      with subtest("nfs share mounts"):
+          client.succeed("systemctl restart data.mount")
+          client.wait_for_unit("data.mount")
+
+      with subtest("permissions on nfs share are enforced"):
+          client.fail("su alice -c 'ls /data'")
+          client.succeed("su alice -c 'echo alice_pw | kinit'")
+          client.succeed("su alice -c 'ls /data'")
+
+          client.fail("su alice -c 'echo bla >> /data/foo'")
+          client.succeed("su alice -c 'echo bla >> /data/alice/foo'")
+          server.succeed("test -e /data/alice/foo")
+
+      with subtest("uids/gids are mapped correctly on nfs share"):
+          ids = client.succeed("stat -c '%U %G' /data/alice").split()
+          expected = ["alice", "users"]
+          assert ids == expected, f"ids incorrect: got {ids} expected {expected}"
+    '';
+})
diff --git a/nixos/tests/nfs/simple.nix b/nixos/tests/nfs/simple.nix
new file mode 100644
index 00000000000..1e319a8eec8
--- /dev/null
+++ b/nixos/tests/nfs/simple.nix
@@ -0,0 +1,94 @@
+import ../make-test-python.nix ({ pkgs, version ? 4, ... }:
+
+let
+
+  client =
+    { pkgs, ... }:
+    { virtualisation.fileSystems =
+        { "/data" =
+           { # nfs4 exports the export with fsid=0 as a virtual root directory
+             device = if (version == 4) then "server:/" else "server:/data";
+             fsType = "nfs";
+             options = [ "vers=${toString version}" ];
+           };
+        };
+      networking.firewall.enable = false; # FIXME: only open statd
+    };
+
+in
+
+{
+  name = "nfs";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eelco ];
+  };
+
+  nodes =
+    { client1 = client;
+      client2 = client;
+
+      server =
+        { ... }:
+        { services.nfs.server.enable = true;
+          services.nfs.server.exports =
+            ''
+              /data 192.168.1.0/255.255.255.0(rw,no_root_squash,no_subtree_check,fsid=0)
+            '';
+          services.nfs.server.createMountPoints = true;
+          networking.firewall.enable = false; # FIXME: figure out what ports need to be allowed
+        };
+    };
+
+  testScript =
+    ''
+      import time
+
+      server.wait_for_unit("nfs-server")
+      server.succeed("systemctl start network-online.target")
+      server.wait_for_unit("network-online.target")
+
+      start_all()
+
+      client1.wait_for_unit("data.mount")
+      client1.succeed("echo bla > /data/foo")
+      server.succeed("test -e /data/foo")
+
+      client2.wait_for_unit("data.mount")
+      client2.succeed("echo bla > /data/bar")
+      server.succeed("test -e /data/bar")
+
+      with subtest("restarting 'nfs-server' works correctly"):
+          server.succeed("systemctl restart nfs-server")
+          # will take 90 seconds due to the NFS grace period
+          client2.succeed("echo bla >> /data/bar")
+
+      with subtest("can get a lock"):
+          client2.succeed("time flock -n -s /data/lock true")
+
+      with subtest("client 2 fails to acquire lock held by client 1"):
+          client1.succeed("flock -x /data/lock -c 'touch locked; sleep 100000' >&2 &")
+          client1.wait_for_file("locked")
+          client2.fail("flock -n -s /data/lock true")
+
+      with subtest("client 2 obtains lock after resetting client 1"):
+          client2.succeed(
+              "flock -x /data/lock -c 'echo acquired; touch locked; sleep 100000' >&2 &"
+          )
+          client1.crash()
+          client1.start()
+          client2.wait_for_file("locked")
+
+      with subtest("locks survive server reboot"):
+          client1.wait_for_unit("data.mount")
+          server.shutdown()
+          server.start()
+          client1.succeed("touch /data/xyzzy")
+          client1.fail("time flock -n -s /data/lock true")
+
+      with subtest("unmounting during shutdown happens quickly"):
+          t1 = time.monotonic()
+          client1.shutdown()
+          duration = time.monotonic() - t1
+          assert duration < 30, f"shutdown took too long ({duration} seconds)"
+    '';
+})
diff --git a/nixos/tests/nghttpx.nix b/nixos/tests/nghttpx.nix
new file mode 100644
index 00000000000..d83c1c4cae6
--- /dev/null
+++ b/nixos/tests/nghttpx.nix
@@ -0,0 +1,61 @@
+let
+  nginxRoot = "/run/nginx";
+in
+  import ./make-test-python.nix ({...}: {
+    name  = "nghttpx";
+    nodes = {
+      webserver = {
+        networking.firewall.allowedTCPPorts = [ 80 ];
+        systemd.services.nginx = {
+          preStart = ''
+            mkdir -p ${nginxRoot}
+            echo "Hello world!" > ${nginxRoot}/hello-world.txt
+          '';
+        };
+
+        services.nginx = {
+          enable = true;
+          virtualHosts.server = {
+            locations."/".root = nginxRoot;
+          };
+        };
+      };
+
+      proxy = {
+        networking.firewall.allowedTCPPorts = [ 80 ];
+        services.nghttpx = {
+          enable = true;
+          frontends = [
+            { server = {
+                host = "*";
+                port = 80;
+              };
+
+              params = {
+                tls = "no-tls";
+              };
+            }
+          ];
+          backends = [
+            { server = {
+                host = "webserver";
+                port = 80;
+              };
+              patterns = [ "/" ];
+              params.proto = "http/1.1";
+            }
+          ];
+        };
+      };
+
+      client = {};
+    };
+
+    testScript = ''
+      start_all()
+
+      webserver.wait_for_open_port("80")
+      proxy.wait_for_open_port("80")
+      client.wait_until_succeeds("curl -s --fail http://proxy/hello-world.txt")
+    '';
+  })
diff --git a/nixos/tests/nginx-auth.nix b/nixos/tests/nginx-auth.nix
new file mode 100644
index 00000000000..c0d24a20ddb
--- /dev/null
+++ b/nixos/tests/nginx-auth.nix
@@ -0,0 +1,47 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "nginx-auth";
+
+  nodes = {
+    webserver = { pkgs, lib, ... }: {
+      services.nginx = let
+        root = pkgs.runCommand "testdir" {} ''
+          mkdir "$out"
+          echo hello world > "$out/index.html"
+        '';
+      in {
+        enable = true;
+
+        virtualHosts.lockedroot = {
+          inherit root;
+          basicAuth.alice = "jane";
+        };
+
+        virtualHosts.lockedsubdir = {
+          inherit root;
+          locations."/sublocation/" = {
+            alias = "${root}/";
+            basicAuth.bob = "john";
+          };
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    webserver.wait_for_unit("nginx")
+    webserver.wait_for_open_port(80)
+
+    webserver.fail("curl --fail --resolve lockedroot:80:127.0.0.1 http://lockedroot")
+    webserver.succeed(
+        "curl --fail --resolve lockedroot:80:127.0.0.1 http://alice:jane@lockedroot"
+    )
+
+    webserver.succeed("curl --fail --resolve lockedsubdir:80:127.0.0.1 http://lockedsubdir")
+    webserver.fail(
+        "curl --fail --resolve lockedsubdir:80:127.0.0.1 http://lockedsubdir/sublocation/index.html"
+    )
+    webserver.succeed(
+        "curl --fail --resolve lockedsubdir:80:127.0.0.1 http://bob:john@lockedsubdir/sublocation/index.html"
+    )
+  '';
+})
diff --git a/nixos/tests/nginx-etag.nix b/nixos/tests/nginx-etag.nix
new file mode 100644
index 00000000000..b69511d081d
--- /dev/null
+++ b/nixos/tests/nginx-etag.nix
@@ -0,0 +1,88 @@
+import ./make-test-python.nix {
+  name = "nginx-etag";
+
+  nodes = {
+    server = { pkgs, lib, ... }: {
+      networking.firewall.enable = false;
+      services.nginx.enable = true;
+      services.nginx.virtualHosts.server = {
+        root = pkgs.runCommandLocal "testdir" {} ''
+          mkdir "$out"
+          cat > "$out/test.js" <<EOF
+          document.getElementById('foobar').setAttribute('foo', 'bar');
+          EOF
+          cat > "$out/index.html" <<EOF
+          <!DOCTYPE html>
+          <div id="foobar">test</div>
+          <script src="test.js"></script>
+          EOF
+        '';
+      };
+
+      specialisation.pass-checks.configuration = {
+        services.nginx.virtualHosts.server = {
+          root = lib.mkForce (pkgs.runCommandLocal "testdir2" {} ''
+            mkdir "$out"
+            cat > "$out/test.js" <<EOF
+            document.getElementById('foobar').setAttribute('foo', 'yay');
+            EOF
+            cat > "$out/index.html" <<EOF
+            <!DOCTYPE html>
+            <div id="foobar">test</div>
+            <script src="test.js"></script>
+            EOF
+          '');
+        };
+      };
+    };
+
+    client = { pkgs, lib, ... }: {
+      environment.systemPackages = let
+        testRunner = pkgs.writers.writePython3Bin "test-runner" {
+          libraries = [ pkgs.python3Packages.selenium ];
+        } ''
+          import os
+          import time
+
+          from selenium.webdriver import Firefox
+          from selenium.webdriver.firefox.options import Options
+
+          options = Options()
+          options.add_argument('--headless')
+          driver = Firefox(options=options)
+
+          driver.implicitly_wait(20)
+          driver.get('http://server/')
+          driver.find_element_by_xpath('//div[@foo="bar"]')
+          open('/tmp/passed_stage1', 'w')
+
+          while not os.path.exists('/tmp/proceed'):
+              time.sleep(0.5)
+
+          driver.get('http://server/')
+          driver.find_element_by_xpath('//div[@foo="yay"]')
+          open('/tmp/passed', 'w')
+        '';
+      in [ pkgs.firefox-unwrapped pkgs.geckodriver testRunner ];
+    };
+  };
+
+  testScript = { nodes, ... }: let
+    inherit (nodes.server.config.system.build) toplevel;
+    newSystem = "${toplevel}/specialisation/pass-checks";
+  in ''
+    start_all()
+
+    server.wait_for_unit("nginx.service")
+    client.wait_for_unit("multi-user.target")
+    client.execute("test-runner >&2 &")
+    client.wait_for_file("/tmp/passed_stage1")
+
+    server.succeed(
+        "${newSystem}/bin/switch-to-configuration test >&2"
+    )
+    client.succeed("touch /tmp/proceed")
+
+    client.wait_for_file("/tmp/passed")
+  '';
+}
diff --git a/nixos/tests/nginx-modsecurity.nix b/nixos/tests/nginx-modsecurity.nix
new file mode 100644
index 00000000000..8c53c0196d4
--- /dev/null
+++ b/nixos/tests/nginx-modsecurity.nix
@@ -0,0 +1,39 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "nginx-modsecurity";
+
+  machine = { config, lib, pkgs, ... }: {
+    services.nginx = {
+      enable = true;
+      additionalModules = [ pkgs.nginxModules.modsecurity-nginx ];
+      virtualHosts.localhost =
+        let modsecurity_conf = pkgs.writeText "modsecurity.conf" ''
+          SecRuleEngine On
+          SecDefaultAction "phase:1,log,auditlog,deny,status:403"
+          SecDefaultAction "phase:2,log,auditlog,deny,status:403"
+          SecRule REQUEST_METHOD   "HEAD"        "id:100, phase:1, block"
+          SecRule REQUEST_FILENAME "secret.html" "id:101, phase:2, block"
+        '';
+        testroot = pkgs.runCommand "testroot" {} ''
+          mkdir -p $out
+          echo "<html><body>Hello World!</body></html>" > $out/index.html
+          echo "s3cret" > $out/secret.html
+        '';
+      in {
+        root = testroot;
+        extraConfig = ''
+          modsecurity on;
+          modsecurity_rules_file ${modsecurity_conf};
+        '';
+      };
+    };
+  };
+  testScript = ''
+    machine.wait_for_unit("nginx")
+
+    response = machine.wait_until_succeeds("curl -fvvv -s http://127.0.0.1/")
+    assert "Hello World!" in response
+
+    machine.fail("curl -fvvv -X HEAD -s http://127.0.0.1/")
+    machine.fail("curl -fvvv -s http://127.0.0.1/secret.html")
+  '';
+})
diff --git a/nixos/tests/nginx-pubhtml.nix b/nixos/tests/nginx-pubhtml.nix
new file mode 100644
index 00000000000..6e1e605628e
--- /dev/null
+++ b/nixos/tests/nginx-pubhtml.nix
@@ -0,0 +1,21 @@
+import ./make-test-python.nix {
+  name = "nginx-pubhtml";
+
+  machine = { pkgs, ... }: {
+    systemd.services.nginx.serviceConfig.ProtectHome = "read-only";
+    services.nginx.enable = true;
+    services.nginx.virtualHosts.localhost = {
+      locations."~ ^/\\~([a-z0-9_]+)(/.*)?$".alias = "/home/$1/public_html$2";
+    };
+    users.users.foo.isNormalUser = true;
+  };
+
+  testScript = ''
+    machine.wait_for_unit("nginx")
+    machine.wait_for_open_port(80)
+    machine.succeed("chmod 0711 /home/foo")
+    machine.succeed("su -c 'mkdir -p /home/foo/public_html' foo")
+    machine.succeed("su -c 'echo bar > /home/foo/public_html/bar.txt' foo")
+    machine.succeed('test "$(curl -fvvv http://localhost/~foo/bar.txt)" = bar')
+  '';
+}
diff --git a/nixos/tests/nginx-sandbox.nix b/nixos/tests/nginx-sandbox.nix
new file mode 100644
index 00000000000..2d512725f26
--- /dev/null
+++ b/nixos/tests/nginx-sandbox.nix
@@ -0,0 +1,65 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "nginx-sandbox";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ izorkin ];
+  };
+
+  # This test checks the creation and reading of a file in sandbox mode. Used simple lua script.
+
+  machine = { pkgs, ... }: {
+    nixpkgs.overlays = [
+      (self: super: {
+        nginx-lua = super.nginx.override {
+          modules = [
+            pkgs.nginxModules.lua
+          ];
+        };
+      })
+    ];
+    services.nginx.enable = true;
+    services.nginx.package = pkgs.nginx-lua;
+    services.nginx.virtualHosts.localhost = {
+      extraConfig = ''
+        location /test1-write {
+          content_by_lua_block {
+            local create = os.execute('${pkgs.coreutils}/bin/mkdir /tmp/test1-read')
+            local create = os.execute('${pkgs.coreutils}/bin/touch /tmp/test1-read/foo.txt')
+            local echo = os.execute('${pkgs.coreutils}/bin/echo worked > /tmp/test1-read/foo.txt')
+          }
+        }
+        location /test1-read {
+          root /tmp;
+        }
+        location /test2-write {
+          content_by_lua_block {
+            local create = os.execute('${pkgs.coreutils}/bin/mkdir /var/web/test2-read')
+            local create = os.execute('${pkgs.coreutils}/bin/touch /var/web/test2-read/bar.txt')
+            local echo = os.execute('${pkgs.coreutils}/bin/echo error-worked > /var/web/test2-read/bar.txt')
+          }
+        }
+        location /test2-read {
+          root /var/web;
+        }
+      '';
+    };
+    users.users.foo.isNormalUser = true;
+  };
+
+  testScript = ''
+    machine.wait_for_unit("nginx")
+    machine.wait_for_open_port(80)
+
+    # Checking write in temporary folder
+    machine.succeed("$(curl -vvv http://localhost/test1-write)")
+    machine.succeed('test "$(curl -fvvv http://localhost/test1-read/foo.txt)" = worked')
+
+    # Checking write in protected folder. In sandbox mode for the nginx service, the folder /var/web is mounted
+    # in read-only mode.
+    machine.succeed("mkdir -p /var/web")
+    machine.succeed("chown nginx:nginx /var/web")
+    machine.succeed("$(curl -vvv http://localhost/test2-write)")
+    assert "404 Not Found" in machine.succeed(
+        "curl -vvv -s http://localhost/test2-read/bar.txt"
+    )
+  '';
+})
diff --git a/nixos/tests/nginx-sso.nix b/nixos/tests/nginx-sso.nix
new file mode 100644
index 00000000000..aeb89859c73
--- /dev/null
+++ b/nixos/tests/nginx-sso.nix
@@ -0,0 +1,48 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "nginx-sso";
+  meta = {
+    maintainers = with pkgs.lib.maintainers; [ delroth ];
+  };
+
+  machine = {
+    services.nginx.sso = {
+      enable = true;
+      configuration = {
+        listen = { addr = "127.0.0.1"; port = 8080; };
+
+        providers.token.tokens = {
+          myuser = "MyToken";
+        };
+
+        acl = {
+          rule_sets = [
+            {
+              rules = [ { field = "x-application"; equals = "MyApp"; } ];
+              allow = [ "myuser" ];
+            }
+          ];
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("nginx-sso.service")
+    machine.wait_for_open_port(8080)
+
+    with subtest("No valid user -> 401"):
+        machine.fail("curl -sSf http://localhost:8080/auth")
+
+    with subtest("Valid user but no matching ACL -> 403"):
+        machine.fail(
+            "curl -sSf -H 'Authorization: Token MyToken' http://localhost:8080/auth"
+        )
+
+    with subtest("Valid user and matching ACL -> 200"):
+        machine.succeed(
+            "curl -sSf -H 'Authorization: Token MyToken' -H 'X-Application: MyApp' http://localhost:8080/auth"
+        )
+  '';
+})
diff --git a/nixos/tests/nginx-variants.nix b/nixos/tests/nginx-variants.nix
new file mode 100644
index 00000000000..96a9a2c3b8c
--- /dev/null
+++ b/nixos/tests/nginx-variants.nix
@@ -0,0 +1,33 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+
+builtins.listToAttrs (
+  builtins.map
+    (nginxName:
+      {
+        name = nginxName;
+        value = makeTest {
+          name = "nginx-variant-${nginxName}";
+
+          machine = { pkgs, ... }: {
+            services.nginx = {
+              enable = true;
+              virtualHosts.localhost.locations."/".return = "200 'foo'";
+              package = pkgs."${nginxName}";
+            };
+          };
+
+          testScript = ''
+            machine.wait_for_unit("nginx")
+            machine.wait_for_open_port(80)
+            machine.succeed('test "$(curl -fvvv http://localhost/)" = foo')
+          '';
+        };
+      }
+    )
+    [ "nginxStable" "nginxMainline" "nginxQuic" "nginxShibboleth" "openresty" "tengine" ]
+)
diff --git a/nixos/tests/nginx.nix b/nixos/tests/nginx.nix
new file mode 100644
index 00000000000..d9d073822a1
--- /dev/null
+++ b/nixos/tests/nginx.nix
@@ -0,0 +1,129 @@
+# verifies:
+#   1. nginx generates config file with shared http context definitions above
+#      generated virtual hosts config.
+#   2. whether the ETag header is properly generated whenever we're serving
+#      files in Nix store paths
+#   3. nginx doesn't restart on configuration changes (only reloads)
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "nginx";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mbbx6spp danbst ];
+  };
+
+  nodes = {
+    webserver = { pkgs, lib, ... }: {
+      services.nginx.enable = true;
+      services.nginx.commonHttpConfig = ''
+        log_format ceeformat '@cee: {"status":"$status",'
+          '"request_time":$request_time,'
+          '"upstream_response_time":$upstream_response_time,'
+          '"pipe":"$pipe","bytes_sent":$bytes_sent,'
+          '"connection":"$connection",'
+          '"remote_addr":"$remote_addr",'
+          '"host":"$host",'
+          '"timestamp":"$time_iso8601",'
+          '"request":"$request",'
+          '"http_referer":"$http_referer",'
+          '"upstream_addr":"$upstream_addr"}';
+      '';
+      services.nginx.virtualHosts."0.my.test" = {
+        extraConfig = ''
+          access_log syslog:server=unix:/dev/log,facility=user,tag=mytag,severity=info ceeformat;
+          location /favicon.ico { allow all; access_log off; log_not_found off; }
+        '';
+      };
+
+      services.nginx.virtualHosts.localhost = {
+        root = pkgs.runCommand "testdir" {} ''
+          mkdir "$out"
+          echo hello world > "$out/index.html"
+        '';
+      };
+
+      services.nginx.enableReload = true;
+
+      specialisation.etagSystem.configuration = {
+        services.nginx.virtualHosts.localhost = {
+          root = lib.mkForce (pkgs.runCommand "testdir2" {} ''
+            mkdir "$out"
+            echo content changed > "$out/index.html"
+          '');
+        };
+      };
+
+      specialisation.justReloadSystem.configuration = {
+        services.nginx.virtualHosts."1.my.test".listen = [ { addr = "127.0.0.1"; port = 8080; }];
+      };
+
+      specialisation.reloadRestartSystem.configuration = {
+        services.nginx.package = pkgs.nginxMainline;
+      };
+
+      specialisation.reloadWithErrorsSystem.configuration = {
+        services.nginx.package = pkgs.nginxMainline;
+        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";
+  in ''
+    url = "http://localhost/index.html"
+
+
+    def check_etag():
+        etag = webserver.succeed(
+            f'curl -v {url} 2>&1 | sed -n -e "s/^< etag: *//ip"'
+        ).rstrip()
+        http_code = webserver.succeed(
+            f"curl -w '%{{http_code}}' --head --fail -H 'If-None-Match: {etag}' {url}"
+        )
+        assert http_code.split("\n")[-1] == "304"
+
+        return etag
+
+
+    webserver.wait_for_unit("nginx")
+    webserver.wait_for_open_port(80)
+
+    with subtest("check ETag if serving Nix store paths"):
+        old_etag = check_etag()
+        webserver.succeed(
+            "${etagSystem}/bin/switch-to-configuration test >&2"
+        )
+        webserver.sleep(1)
+        new_etag = check_etag()
+        assert old_etag != new_etag
+
+    with subtest("config is reloaded on nixos-rebuild switch"):
+        webserver.succeed(
+            "${justReloadSystem}/bin/switch-to-configuration test >&2"
+        )
+        webserver.wait_for_open_port(8080)
+        webserver.fail("journalctl -u nginx | grep -q -i stopped")
+        webserver.succeed("journalctl -u nginx | grep -q -i reloaded")
+
+    with subtest("restart when nginx package changes"):
+        webserver.succeed(
+            "${reloadRestartSystem}/bin/switch-to-configuration test >&2"
+        )
+        webserver.wait_for_unit("nginx")
+        webserver.succeed("journalctl -u nginx | grep -q -i stopped")
+
+    with subtest("nixos-rebuild --switch should fail when there are configuration errors"):
+        webserver.fail(
+            "${reloadWithErrorsSystem}/bin/switch-to-configuration test >&2"
+        )
+        webserver.succeed("[[ $(systemctl is-failed nginx-config-reload) == failed ]]")
+        webserver.succeed("[[ $(systemctl is-failed nginx) == active ]]")
+        # just to make sure operation is idempotent. During development I had a situation
+        # when first time it shows error, but stops showing it on subsequent rebuilds
+        webserver.fail(
+            "${reloadWithErrorsSystem}/bin/switch-to-configuration test >&2"
+        )
+  '';
+})
diff --git a/nixos/tests/nitter.nix b/nixos/tests/nitter.nix
new file mode 100644
index 00000000000..0e1a6d150f3
--- /dev/null
+++ b/nixos/tests/nitter.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "nitter";
+  meta.maintainers = with pkgs.lib.maintainers; [ erdnaxe ];
+
+  nodes.machine = {
+    services.nitter.enable = true;
+    # Test CAP_NET_BIND_SERVICE
+    services.nitter.server.port = 80;
+  };
+
+  testScript = ''
+    machine.wait_for_unit("nitter.service")
+    machine.wait_for_open_port("80")
+    machine.succeed("curl --fail http://localhost:80/")
+  '';
+})
diff --git a/nixos/tests/nix-serve-ssh.nix b/nixos/tests/nix-serve-ssh.nix
new file mode 100644
index 00000000000..1eb8d5b395b
--- /dev/null
+++ b/nixos/tests/nix-serve-ssh.nix
@@ -0,0 +1,45 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+let inherit (import ./ssh-keys.nix pkgs)
+      snakeOilPrivateKey snakeOilPublicKey;
+    ssh-config = builtins.toFile "ssh.conf" ''
+      UserKnownHostsFile=/dev/null
+      StrictHostKeyChecking=no
+    '';
+in
+   { name = "nix-ssh-serve";
+     meta.maintainers = [ lib.maintainers.shlevy ];
+     nodes =
+       { server.nix.sshServe =
+           { enable = true;
+             keys = [ snakeOilPublicKey ];
+             protocol = "ssh-ng";
+           };
+         server.nix.package = pkgs.nix;
+         client.nix.package = pkgs.nix;
+       };
+     testScript = ''
+       start_all()
+
+       client.succeed("mkdir -m 700 /root/.ssh")
+       client.succeed(
+           "cat ${ssh-config} > /root/.ssh/config"
+       )
+       client.succeed(
+           "cat ${snakeOilPrivateKey} > /root/.ssh/id_ecdsa"
+       )
+       client.succeed("chmod 600 /root/.ssh/id_ecdsa")
+
+       client.succeed("nix-store --add /etc/machine-id > mach-id-path")
+
+       server.wait_for_unit("sshd")
+
+       client.fail("diff /root/other-store$(cat mach-id-path) /etc/machine-id")
+       # Currently due to shared store this is a noop :(
+       client.succeed("nix copy --experimental-features 'nix-command' --to ssh-ng://nix-ssh@server $(cat mach-id-path)")
+       client.succeed(
+           "nix-store --realise $(cat mach-id-path) --store /root/other-store --substituters ssh-ng://nix-ssh@server"
+       )
+       client.succeed("diff /root/other-store$(cat mach-id-path) /etc/machine-id")
+     '';
+   }
+)
diff --git a/nixos/tests/nix-serve.nix b/nixos/tests/nix-serve.nix
new file mode 100644
index 00000000000..ab82f4be43e
--- /dev/null
+++ b/nixos/tests/nix-serve.nix
@@ -0,0 +1,22 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+{
+  name = "nix-serve";
+  machine = { pkgs, ... }: {
+    services.nix-serve.enable = true;
+    environment.systemPackages = [
+      pkgs.hello
+    ];
+  };
+  testScript = let
+    pkgHash = builtins.head (
+      builtins.match "${builtins.storeDir}/([^-]+).+" (toString pkgs.hello)
+    );
+  in ''
+    start_all()
+    machine.wait_for_unit("nix-serve.service")
+    machine.wait_for_open_port(5000)
+    machine.succeed(
+        "curl --fail -g http://0.0.0.0:5000/nar/${pkgHash}.nar -o /tmp/hello.nar"
+    )
+  '';
+})
diff --git a/nixos/tests/nixops/default.nix b/nixos/tests/nixops/default.nix
new file mode 100644
index 00000000000..f0834c51f0b
--- /dev/null
+++ b/nixos/tests/nixops/default.nix
@@ -0,0 +1,114 @@
+{ pkgs, ... }:
+let
+  inherit (pkgs) lib;
+
+  tests = {
+    # TODO: uncomment stable
+    #  - Blocked on https://github.com/NixOS/nixpkgs/issues/138584 which has a
+    #    PR in staging: https://github.com/NixOS/nixpkgs/pull/139986
+    #  - Alternatively, blocked on a NixOps 2 release
+    #    https://github.com/NixOS/nixops/issues/1242
+    # stable = testsLegacyNetwork { nixopsPkg = pkgs.nixops; };
+    unstable = testsForPackage { nixopsPkg = pkgs.nixops_unstable; };
+
+    # inherit testsForPackage;
+  };
+
+  testsForPackage = lib.makeOverridable (args: lib.recurseIntoAttrs {
+    legacyNetwork = testLegacyNetwork args;
+  });
+
+  testLegacyNetwork = { nixopsPkg }: pkgs.nixosTest ({
+    nodes = {
+      deployer = { config, lib, nodes, pkgs, ... }: {
+        imports = [ ../../modules/installer/cd-dvd/channel.nix ];
+        environment.systemPackages = [ nixopsPkg ];
+        nix.settings.substituters = lib.mkForce [ ];
+        users.users.person.isNormalUser = true;
+        virtualisation.writableStore = true;
+        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)
+        ];
+      };
+      server = { lib, ... }: {
+        imports = [ ./legacy/base-configuration.nix ];
+      };
+    };
+
+    testScript = { nodes }:
+      let
+        deployerSetup = pkgs.writeScript "deployerSetup" ''
+          #!${pkgs.runtimeShell}
+          set -eux -o pipefail
+          cp --no-preserve=mode -r ${./legacy} unicorn
+          cp --no-preserve=mode ${../ssh-keys.nix} unicorn/ssh-keys.nix
+          mkdir -p ~/.ssh
+          cp ${snakeOilPrivateKey} ~/.ssh/id_ed25519
+          chmod 0400 ~/.ssh/id_ed25519
+        '';
+        serverNetworkJSON = pkgs.writeText "server-network.json"
+          (builtins.toJSON nodes.server.config.system.build.networkConfig);
+      in
+      ''
+        import shlex
+
+        def deployer_do(cmd):
+            cmd = shlex.quote(cmd)
+            return deployer.succeed(f"su person -l -c {cmd} &>/dev/console")
+
+        start_all()
+
+        deployer_do("cat /etc/hosts")
+
+        deployer_do("${deployerSetup}")
+        deployer_do("cp ${serverNetworkJSON} unicorn/server-network.json")
+
+        # Establish that ssh works, regardless of nixops
+        # Easy way to accept the server host key too.
+        server.wait_for_open_port(22)
+        deployer.wait_for_unit("network.target")
+
+        # Put newlines on console, to flush the console reader's line buffer
+        # in case nixops' last output did not end in a newline, as is the case
+        # with a status line (if implemented?)
+        deployer.succeed("while sleep 60s; do echo [60s passed]; done >&2 &")
+
+        deployer_do("cd ~/unicorn; ssh -oStrictHostKeyChecking=accept-new root@server echo hi")
+
+        # Create and deploy
+        deployer_do("cd ~/unicorn; nixops create")
+
+        deployer_do("cd ~/unicorn; nixops deploy --confirm")
+
+        deployer_do("cd ~/unicorn; nixops ssh server 'hello | figlet'")
+      '';
+  });
+
+  inherit (import ../ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
+
+  /*
+    Return a store path with a closure containing everything including
+    derivations and all build dependency outputs, all the way down.
+  */
+  allDrvOutputs = pkg:
+    let name = lib.strings.sanitizeDerivationName "allDrvOutputs-${pkg.pname or pkg.name or "unknown"}";
+    in
+    pkgs.runCommand name { refs = pkgs.writeReferencesToFile pkg.drvPath; } ''
+      touch $out
+      while read ref; do
+        case $ref in
+          *.drv)
+            cat $ref >>$out
+            ;;
+        esac
+      done <$refs
+    '';
+
+in
+tests
diff --git a/nixos/tests/nixops/legacy/base-configuration.nix b/nixos/tests/nixops/legacy/base-configuration.nix
new file mode 100644
index 00000000000..7f1c07a5c4a
--- /dev/null
+++ b/nixos/tests/nixops/legacy/base-configuration.nix
@@ -0,0 +1,31 @@
+{ lib, modulesPath, pkgs, ... }:
+let
+  ssh-keys =
+    if builtins.pathExists ../../ssh-keys.nix
+    then # Outside sandbox
+      ../../ssh-keys.nix
+    else # In sandbox
+      ./ssh-keys.nix;
+
+  inherit (import ssh-keys pkgs)
+    snakeOilPrivateKey snakeOilPublicKey;
+in
+{
+  imports = [
+    (modulesPath + "/virtualisation/qemu-vm.nix")
+    (modulesPath + "/testing/test-instrumentation.nix")
+  ];
+  virtualisation.writableStore = true;
+  nix.settings.substituters = lib.mkForce [ ];
+  virtualisation.graphics = false;
+  documentation.enable = false;
+  services.qemuGuest.enable = true;
+  boot.loader.grub.enable = false;
+
+  services.openssh.enable = true;
+  users.users.root.openssh.authorizedKeys.keys = [
+    snakeOilPublicKey
+  ];
+  security.pam.services.sshd.limits =
+    [{ domain = "*"; item = "memlock"; type = "-"; value = 1024; }];
+}
diff --git a/nixos/tests/nixops/legacy/nixops.nix b/nixos/tests/nixops/legacy/nixops.nix
new file mode 100644
index 00000000000..795dc2a7182
--- /dev/null
+++ b/nixos/tests/nixops/legacy/nixops.nix
@@ -0,0 +1,15 @@
+{
+  network = {
+    description = "Legacy Network using <nixpkgs> and legacy state.";
+    # NB this is not really what makes it a legacy network; lack of flakes is.
+    storage.legacy = { };
+  };
+  server = { lib, pkgs, ... }: {
+    deployment.targetEnv = "none";
+    imports = [
+      ./base-configuration.nix
+      (lib.modules.importJSON ./server-network.json)
+    ];
+    environment.systemPackages = [ pkgs.hello pkgs.figlet ];
+  };
+}
diff --git a/nixos/tests/nixos-generate-config.nix b/nixos/tests/nixos-generate-config.nix
new file mode 100644
index 00000000000..1dadf4992ed
--- /dev/null
+++ b/nixos/tests/nixos-generate-config.nix
@@ -0,0 +1,41 @@
+import ./make-test-python.nix ({ lib, ... } : {
+  name = "nixos-generate-config";
+  meta.maintainers = with lib.maintainers; [ basvandijk ];
+  machine = {
+    system.nixos-generate-config.configuration = ''
+      # OVERRIDDEN
+      { config, pkgs, ... }: {
+        imports = [ ./hardware-configuration.nix ];
+      $bootLoaderConfig
+      $desktopConfiguration
+      }
+    '';
+
+    system.nixos-generate-config.desktopConfiguration = [''
+      # DESKTOP
+      services.xserver.displayManager.gdm.enable = true;
+      services.xserver.desktopManager.gnome.enable = true;
+    ''];
+  };
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed("nixos-generate-config")
+
+    # Test if the configuration really is overridden
+    machine.succeed("grep 'OVERRIDDEN' /etc/nixos/configuration.nix")
+
+    # Test if desktop configuration really is overridden
+    machine.succeed("grep 'DESKTOP' /etc/nixos/configuration.nix")
+
+    # Test of if the Perl variable $bootLoaderConfig is spliced correctly:
+    machine.succeed(
+        "grep 'boot\\.loader\\.grub\\.enable = true;' /etc/nixos/configuration.nix"
+    )
+
+    # Test if the Perl variable $desktopConfiguration is spliced correctly
+    machine.succeed(
+        "grep 'services\\.xserver\\.desktopManager\\.gnome\\.enable = true;' /etc/nixos/configuration.nix"
+    )
+  '';
+})
diff --git a/nixos/tests/node-red.nix b/nixos/tests/node-red.nix
new file mode 100644
index 00000000000..7660bc32f4c
--- /dev/null
+++ b/nixos/tests/node-red.nix
@@ -0,0 +1,31 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "nodered";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ matthewcroughan ];
+  };
+
+  nodes = {
+    client = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.curl ];
+    };
+    nodered = { config, pkgs, ... }: {
+      services.node-red = {
+        enable = true;
+        openFirewall = true;
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    nodered.wait_for_unit("node-red.service")
+    nodered.wait_for_open_port("1880")
+
+    client.wait_for_unit("multi-user.target")
+
+    with subtest("Check that the Node-RED webserver can be reached."):
+        assert "<title>Node-RED</title>" in client.succeed(
+            "curl -sSf http:/nodered:1880/ | grep title"
+        )
+  '';
+})
diff --git a/nixos/tests/nomad.nix b/nixos/tests/nomad.nix
new file mode 100644
index 00000000000..51b11a8fef9
--- /dev/null
+++ b/nixos/tests/nomad.nix
@@ -0,0 +1,97 @@
+import ./make-test-python.nix (
+  { lib, ... }: {
+    name = "nomad";
+    nodes = {
+      default_server = { pkgs, lib, ... }: {
+        networking = {
+          interfaces.eth1.ipv4.addresses = lib.mkOverride 0 [{
+            address = "192.168.1.1";
+            prefixLength = 16;
+          }];
+        };
+
+        environment.etc."nomad.custom.json".source =
+          (pkgs.formats.json { }).generate "nomad.custom.json" {
+            region = "universe";
+            datacenter = "earth";
+          };
+
+        services.nomad = {
+          enable = true;
+
+          settings = {
+            server = {
+              enabled = true;
+              bootstrap_expect = 1;
+            };
+          };
+
+          extraSettingsPaths = [ "/etc/nomad.custom.json" ];
+          enableDocker = false;
+        };
+      };
+
+      custom_state_dir_server = { pkgs, lib, ... }: {
+        networking = {
+          interfaces.eth1.ipv4.addresses = lib.mkOverride 0 [{
+            address = "192.168.1.1";
+            prefixLength = 16;
+          }];
+        };
+
+        environment.etc."nomad.custom.json".source =
+          (pkgs.formats.json { }).generate "nomad.custom.json" {
+            region = "universe";
+            datacenter = "earth";
+          };
+
+        services.nomad = {
+          enable = true;
+          dropPrivileges = false;
+
+          settings = {
+            data_dir = "/nomad/data/dir";
+            server = {
+              enabled = true;
+              bootstrap_expect = 1;
+            };
+          };
+
+          extraSettingsPaths = [ "/etc/nomad.custom.json" ];
+          enableDocker = false;
+        };
+
+        systemd.services.nomad.serviceConfig.ExecStartPre = "${pkgs.writeShellScript "mk_data_dir" ''
+          set -euxo pipefail
+
+          ${pkgs.coreutils}/bin/mkdir -p /nomad/data/dir
+        ''}";
+      };
+    };
+
+    testScript = ''
+      def test_nomad_server(server):
+          server.wait_for_unit("nomad.service")
+
+          # wait for healthy server
+          server.wait_until_succeeds(
+              "[ $(nomad operator raft list-peers | grep true | wc -l) == 1 ]"
+          )
+
+          # wait for server liveness
+          server.succeed("[ $(nomad server members | grep -o alive | wc -l) == 1 ]")
+
+          # check the region
+          server.succeed("nomad server members | grep -o universe")
+
+          # check the datacenter
+          server.succeed("[ $(nomad server members | grep -o earth | wc -l) == 1 ]")
+
+
+      servers = [default_server, custom_state_dir_server]
+
+      for server in servers:
+          test_nomad_server(server)
+    '';
+  }
+)
diff --git a/nixos/tests/noto-fonts.nix b/nixos/tests/noto-fonts.nix
new file mode 100644
index 00000000000..049dc766bd3
--- /dev/null
+++ b/nixos/tests/noto-fonts.nix
@@ -0,0 +1,44 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "noto-fonts";
+  meta = {
+    maintainers = with lib.maintainers; [ nickcao midchildan ];
+  };
+
+  machine = {
+    imports = [ ./common/x11.nix ];
+    environment.systemPackages = [ pkgs.gnome.gedit ];
+    fonts = {
+      enableDefaultFonts = false;
+      fonts = with pkgs;[
+        noto-fonts
+        noto-fonts-cjk-sans
+        noto-fonts-cjk-serif
+        noto-fonts-emoji
+      ];
+      fontconfig.defaultFonts = {
+        serif = [ "Noto Serif" "Noto Serif CJK SC" ];
+        sansSerif = [ "Noto Sans" "Noto Sans CJK SC" ];
+        monospace = [ "Noto Sans Mono" "Noto Sans Mono CJK SC" ];
+        emoji = [ "Noto Color Emoji" ];
+      };
+    };
+  };
+
+  testScript =
+    # extracted from http://www.clagnut.com/blog/2380/
+    let testText = builtins.toFile "test.txt" ''
+      the quick brown fox jumps over the lazy dog
+      視野無é™å»£ï¼Œçª—外有è—天
+      EÄ¥oÅanÄo ĉiuĵaÅ­de.
+      ã„ã‚ã¯ã«ã»ã¸ã¨ ã¡ã‚Šã¬ã‚‹ã‚’ ã‚ã‹ã‚ˆãŸã‚Œã ã¤ã­ãªã‚‰ã‚€ ã†ã‚ã®ãŠãã‚„ã¾ ã‘ãµã“ãˆã¦ ã‚ã•ãゆã‚ã¿ã— ã‚‘ã²ã‚‚ã›ã™
+      ë‹¤ëžŒì¥ í—Œ ì³‡ë°”í€´ì— íƒ€ê³ íŒŒ
+      中国智造,慧åŠå…¨çƒ
+    ''; in
+    ''
+      machine.wait_for_x()
+      machine.succeed("gedit ${testText} >&2 &")
+      machine.wait_for_window(".* - gedit")
+      machine.sleep(10)
+      machine.screenshot("screen")
+    '';
+})
diff --git a/nixos/tests/novacomd.nix b/nixos/tests/novacomd.nix
new file mode 100644
index 00000000000..b470c117e1e
--- /dev/null
+++ b/nixos/tests/novacomd.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "novacomd";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ dtzWill ];
+  };
+
+  machine = { ... }: {
+    services.novacomd.enable = true;
+  };
+
+  testScript = ''
+    machine.wait_for_unit("novacomd.service")
+
+    with subtest("Make sure the daemon is really listening"):
+        machine.wait_for_open_port(6968)
+        machine.succeed("novacom -l")
+
+    with subtest("Stop the daemon, double-check novacom fails if daemon isn't working"):
+        machine.stop_job("novacomd")
+        machine.fail("novacom -l")
+
+    with subtest("Make sure the daemon starts back up again"):
+        machine.start_job("novacomd")
+        # make sure the daemon is really listening
+        machine.wait_for_open_port(6968)
+        machine.succeed("novacom -l")
+  '';
+})
diff --git a/nixos/tests/nsd.nix b/nixos/tests/nsd.nix
new file mode 100644
index 00000000000..eea5a82f6f9
--- /dev/null
+++ b/nixos/tests/nsd.nix
@@ -0,0 +1,109 @@
+let
+  common = { pkgs, ... }: {
+    networking.firewall.enable = false;
+    networking.useDHCP = false;
+    # for a host utility with IPv6 support
+    environment.systemPackages = [ pkgs.bind ];
+  };
+in import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "nsd";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ aszlig ];
+  };
+
+  nodes = {
+    clientv4 = { lib, nodes, ... }: {
+      imports = [ common ];
+      networking.nameservers = lib.mkForce [
+        (lib.head nodes.server.config.networking.interfaces.eth1.ipv4.addresses).address
+      ];
+      networking.interfaces.eth1.ipv4.addresses = [
+        { address = "192.168.0.2"; prefixLength = 24; }
+      ];
+    };
+
+    clientv6 = { lib, nodes, ... }: {
+      imports = [ common ];
+      networking.nameservers = lib.mkForce [
+        (lib.head nodes.server.config.networking.interfaces.eth1.ipv6.addresses).address
+      ];
+      networking.interfaces.eth1.ipv4.addresses = [
+        { address = "dead:beef::2"; prefixLength = 24; }
+      ];
+    };
+
+    server = { lib, ... }: {
+      imports = [ common ];
+      networking.interfaces.eth1.ipv4.addresses = [
+        { address = "192.168.0.1"; prefixLength = 24; }
+      ];
+      networking.interfaces.eth1.ipv6.addresses = [
+        { address = "dead:beef::1"; prefixLength = 64; }
+      ];
+      services.nsd.enable = true;
+      services.nsd.rootServer = true;
+      services.nsd.interfaces = lib.mkForce [];
+      services.nsd.keys."tsig.example.com." = {
+        algorithm = "hmac-sha256";
+        keyFile = pkgs.writeTextFile { name = "tsig.example.com."; text = "aR3FJA92+bxRSyosadsJ8Aeeav5TngQW/H/EF9veXbc="; };
+      };
+      services.nsd.zones."example.com.".data = ''
+        @ SOA ns.example.com noc.example.com 666 7200 3600 1209600 3600
+        ipv4 A 1.2.3.4
+        ipv6 AAAA abcd::eeff
+        deleg NS ns.example.com
+        ns A 192.168.0.1
+        ns AAAA dead:beef::1
+      '';
+      services.nsd.zones."example.com.".provideXFR = [ "0.0.0.0 tsig.example.com." ];
+      services.nsd.zones."deleg.example.com.".data = ''
+        @ SOA ns.example.com noc.example.com 666 7200 3600 1209600 3600
+        @ A 9.8.7.6
+        @ AAAA fedc::bbaa
+      '';
+      services.nsd.zones.".".data = ''
+        @ SOA ns.example.com noc.example.com 666 7200 3600 1209600 3600
+        root A 1.8.7.4
+        root AAAA acbd::4
+      '';
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    clientv4.wait_for_unit("network.target")
+    clientv6.wait_for_unit("network.target")
+    server.wait_for_unit("nsd.service")
+
+    with subtest("server tsig.example.com."):
+        expected_tsig = "  secret: \"aR3FJA92+bxRSyosadsJ8Aeeav5TngQW/H/EF9veXbc=\"\n"
+        tsig=server.succeed("cat /var/lib/nsd/private/tsig.example.com.")
+        assert expected_tsig == tsig, f"Expected /var/lib/nsd/private/tsig.example.com. to contain '{expected_tsig}', but found '{tsig}'"
+
+    def assert_host(type, rr, query, expected):
+        self = clientv4 if type == 4 else clientv6
+        out = self.succeed(f"host -{type} -t {rr} {query}").rstrip()
+        self.log(f"output: {out}")
+        import re
+        assert re.search(
+            expected, out
+        ), f"DNS IPv{type} query on {query} gave '{out}' instead of '{expected}'"
+
+
+    for ipv in 4, 6:
+        with subtest(f"IPv{ipv}"):
+            assert_host(ipv, "a", "example.com", "has no [^ ]+ record")
+            assert_host(ipv, "aaaa", "example.com", "has no [^ ]+ record")
+
+            assert_host(ipv, "soa", "example.com", "SOA.*?noc\.example\.com")
+            assert_host(ipv, "a", "ipv4.example.com", "address 1.2.3.4$")
+            assert_host(ipv, "aaaa", "ipv6.example.com", "address abcd::eeff$")
+
+            assert_host(ipv, "a", "deleg.example.com", "address 9.8.7.6$")
+            assert_host(ipv, "aaaa", "deleg.example.com", "address fedc::bbaa$")
+
+            assert_host(ipv, "a", "root", "address 1.8.7.4$")
+            assert_host(ipv, "aaaa", "root", "address acbd::4$")
+  '';
+})
diff --git a/nixos/tests/nzbget.nix b/nixos/tests/nzbget.nix
new file mode 100644
index 00000000000..fe5a4bc3df9
--- /dev/null
+++ b/nixos/tests/nzbget.nix
@@ -0,0 +1,46 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "nzbget";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ aanderse flokli ];
+  };
+
+  nodes = {
+    server = { ... }: {
+      services.nzbget.enable = true;
+
+      # provide some test settings
+      services.nzbget.settings = {
+        "MainDir" = "/var/lib/nzbget";
+        "DirectRename" = true;
+        "DiskSpace" = 0;
+        "Server1.Name" = "this is a test";
+      };
+
+      # hack, don't add (unfree) unrar to nzbget's path,
+      # so we can run this test in CI
+      systemd.services.nzbget.path = pkgs.lib.mkForce [ pkgs.p7zip ];
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+    start_all()
+
+    server.wait_for_unit("nzbget.service")
+    server.wait_for_unit("network.target")
+    server.wait_for_open_port(6789)
+    assert "This file is part of nzbget" in server.succeed(
+        "curl -f -s -u nzbget:tegbzn6789 http://127.0.0.1:6789"
+    )
+    server.succeed(
+        "${pkgs.nzbget}/bin/nzbget -n -o Control_iP=127.0.0.1 -o Control_port=6789 -o Control_password=tegbzn6789 -V"
+    )
+
+    config = server.succeed("${nodes.server.config.systemd.services.nzbget.serviceConfig.ExecStart} --printconfig")
+
+    # confirm the test settings are applied
+    assert 'MainDir = "/var/lib/nzbget"' in config
+    assert 'DirectRename = "yes"' in config
+    assert 'DiskSpace = "0"' in config
+    assert 'Server1.Name = "this is a test"' in config
+  '';
+})
diff --git a/nixos/tests/nzbhydra2.nix b/nixos/tests/nzbhydra2.nix
new file mode 100644
index 00000000000..c82c756c3a1
--- /dev/null
+++ b/nixos/tests/nzbhydra2.nix
@@ -0,0 +1,17 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+  with lib;
+
+  {
+    name = "nzbhydra2";
+    meta.maintainers = with maintainers; [ jamiemagee ];
+
+    nodes.machine = { pkgs, ... }: { services.nzbhydra2.enable = true; };
+
+    testScript = ''
+      machine.start()
+      machine.wait_for_unit("nzbhydra2.service")
+      machine.wait_for_open_port(5076)
+      machine.succeed("curl --fail http://localhost:5076/")
+    '';
+  })
diff --git a/nixos/tests/oci-containers.nix b/nixos/tests/oci-containers.nix
new file mode 100644
index 00000000000..68077e3540a
--- /dev/null
+++ b/nixos/tests/oci-containers.nix
@@ -0,0 +1,43 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+, lib ? pkgs.lib
+}:
+
+let
+
+  inherit (import ../lib/testing-python.nix { inherit system pkgs; }) makeTest;
+
+  mkOCITest = backend: makeTest {
+    name = "oci-containers-${backend}";
+
+    meta = {
+      maintainers = with lib.maintainers; [ adisbladis benley ] ++ lib.teams.serokell.members;
+    };
+
+    nodes = {
+      ${backend} = { pkgs, ... }: {
+        virtualisation.oci-containers = {
+          inherit backend;
+          containers.nginx = {
+            image = "nginx-container";
+            imageFile = pkgs.dockerTools.examples.nginx;
+            ports = ["8181:80"];
+          };
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+      ${backend}.wait_for_unit("${backend}-nginx.service")
+      ${backend}.wait_for_open_port(8181)
+      ${backend}.wait_until_succeeds("curl -f http://localhost:8181 | grep Hello")
+    '';
+  };
+
+in
+lib.foldl' (attrs: backend: attrs // { ${backend} = mkOCITest backend; }) {} [
+  "docker"
+  "podman"
+]
diff --git a/nixos/tests/odoo.nix b/nixos/tests/odoo.nix
new file mode 100644
index 00000000000..96e3405482b
--- /dev/null
+++ b/nixos/tests/odoo.nix
@@ -0,0 +1,27 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} : with lib; {
+  name = "odoo";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mkg20001 ];
+  };
+
+  nodes = {
+    server = { ... }: {
+      services.nginx = {
+        enable = true;
+        recommendedProxySettings = true;
+      };
+
+      services.odoo = {
+        enable = true;
+        domain = "localhost";
+      };
+    };
+  };
+
+  testScript = { nodes, ... }:
+  ''
+    server.wait_for_unit("odoo.service")
+    server.wait_until_succeeds("curl -s http://localhost:8069/web/database/selector | grep '<title>Odoo</title>'")
+    server.succeed("curl -s http://localhost/web/database/selector | grep '<title>Odoo</title>'")
+  '';
+})
diff --git a/nixos/tests/oh-my-zsh.nix b/nixos/tests/oh-my-zsh.nix
new file mode 100644
index 00000000000..57a073b086e
--- /dev/null
+++ b/nixos/tests/oh-my-zsh.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "oh-my-zsh";
+
+  machine = { pkgs, ... }:
+
+    {
+      programs.zsh = {
+        enable = true;
+        ohMyZsh.enable = true;
+      };
+    };
+
+  testScript = ''
+    start_all()
+    machine.succeed("touch ~/.zshrc")
+    machine.succeed("zsh -c 'source /etc/zshrc && echo $ZSH | grep oh-my-zsh-${pkgs.oh-my-zsh.version}'")
+  '';
+})
diff --git a/nixos/tests/ombi.nix b/nixos/tests/ombi.nix
new file mode 100644
index 00000000000..bfca86af817
--- /dev/null
+++ b/nixos/tests/ombi.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "ombi";
+  meta.maintainers = with maintainers; [ woky ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    { services.ombi.enable = true; };
+
+  testScript = ''
+    machine.wait_for_unit("ombi.service")
+    machine.wait_for_open_port("5000")
+    machine.succeed("curl --fail http://localhost:5000/")
+  '';
+})
diff --git a/nixos/tests/openarena.nix b/nixos/tests/openarena.nix
new file mode 100644
index 00000000000..63dc1b9a685
--- /dev/null
+++ b/nixos/tests/openarena.nix
@@ -0,0 +1,71 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+let
+  client =
+    { pkgs, ... }:
+
+    { imports = [ ./common/x11.nix ];
+      hardware.opengl.driSupport = true;
+      environment.systemPackages = [ pkgs.openarena ];
+    };
+
+in {
+  name = "openarena";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fpletz ];
+  };
+
+  nodes =
+    { server =
+        { services.openarena = {
+            enable = true;
+            extraFlags = [ "+set g_gametype 0" "+map oa_dm7" "+addbot Angelyss" "+addbot Arachna" ];
+            openPorts = true;
+          };
+        };
+
+      client1 = client;
+      client2 = client;
+    };
+
+  testScript =
+    ''
+      start_all()
+
+      server.wait_for_unit("openarena")
+      server.wait_until_succeeds("ss --numeric --udp --listening | grep -q 27960")
+
+      client1.wait_for_x()
+      client2.wait_for_x()
+
+      client1.execute("openarena +set r_fullscreen 0 +set name Foo +connect server >&2 &")
+      client2.execute("openarena +set r_fullscreen 0 +set name Bar +connect server >&2 &")
+
+      server.wait_until_succeeds(
+          "journalctl -u openarena -e | grep -q 'Foo.*entered the game'"
+      )
+      server.wait_until_succeeds(
+          "journalctl -u openarena -e | grep -q 'Bar.*entered the game'"
+      )
+
+      server.sleep(10)  # wait for a while to get a nice screenshot
+
+      client1.screenshot("screen_client1_1")
+      client2.screenshot("screen_client2_1")
+
+      client1.block()
+
+      server.sleep(10)
+
+      client1.screenshot("screen_client1_2")
+      client2.screenshot("screen_client2_2")
+
+      client1.unblock()
+
+      server.sleep(10)
+
+      client1.screenshot("screen_client1_3")
+      client2.screenshot("screen_client2_3")
+    '';
+
+})
diff --git a/nixos/tests/openldap.nix b/nixos/tests/openldap.nix
new file mode 100644
index 00000000000..f1a39ad7dde
--- /dev/null
+++ b/nixos/tests/openldap.nix
@@ -0,0 +1,130 @@
+{ pkgs ? (import ../.. { inherit system; config = { }; })
+, system ? builtins.currentSystem
+, ...
+}:
+
+let
+  dbContents = ''
+    dn: dc=example
+    objectClass: domain
+    dc: example
+
+    dn: ou=users,dc=example
+    objectClass: organizationalUnit
+    ou: users
+  '';
+  testScript = ''
+    machine.wait_for_unit("openldap.service")
+    machine.succeed(
+        'ldapsearch -LLL -D "cn=root,dc=example" -w notapassword -b "dc=example"',
+    )
+  '';
+in {
+  # New-style configuration
+  current = import ./make-test-python.nix ({ pkgs, ... }: {
+    inherit testScript;
+    name = "openldap";
+
+    machine = { pkgs, ... }: {
+      environment.etc."openldap/root_password".text = "notapassword";
+      services.openldap = {
+        enable = true;
+        settings = {
+          children = {
+            "cn=schema".includes = [
+              "${pkgs.openldap}/etc/schema/core.ldif"
+              "${pkgs.openldap}/etc/schema/cosine.ldif"
+              "${pkgs.openldap}/etc/schema/inetorgperson.ldif"
+              "${pkgs.openldap}/etc/schema/nis.ldif"
+            ];
+            "olcDatabase={1}mdb" = {
+              # This tests string, base64 and path values, as well as lists of string values
+              attrs = {
+                objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
+                olcDatabase = "{1}mdb";
+                olcDbDirectory = "/var/db/openldap";
+                olcSuffix = "dc=example";
+                olcRootDN = {
+                  # cn=root,dc=example
+                  base64 = "Y249cm9vdCxkYz1leGFtcGxl";
+                };
+                olcRootPW = {
+                  path = "/etc/openldap/root_password";
+                };
+              };
+            };
+          };
+        };
+        declarativeContents."dc=example" = dbContents;
+      };
+    };
+  }) { inherit pkgs system; };
+
+  # Old-style configuration
+  oldOptions = import ./make-test-python.nix ({ pkgs, ... }: {
+    inherit testScript;
+    name = "openldap";
+
+    machine = { pkgs, ... }: {
+      services.openldap = {
+        enable = true;
+        logLevel = "stats acl";
+        defaultSchemas = true;
+        database = "mdb";
+        suffix = "dc=example";
+        rootdn = "cn=root,dc=example";
+        rootpw = "notapassword";
+        declarativeContents."dc=example" = dbContents;
+      };
+    };
+  }) { inherit system pkgs; };
+
+  # Manually managed configDir, for example if dynamic config is essential
+  manualConfigDir = import ./make-test-python.nix ({ pkgs, ... }: {
+    name = "openldap";
+
+    machine = { pkgs, ... }: {
+      services.openldap = {
+        enable = true;
+        configDir = "/var/db/slapd.d";
+      };
+    };
+
+    testScript = let
+      contents = pkgs.writeText "data.ldif" dbContents;
+      config = pkgs.writeText "config.ldif" ''
+        dn: cn=config
+        cn: config
+        objectClass: olcGlobal
+        olcLogLevel: stats
+        olcPidFile: /run/slapd/slapd.pid
+
+        dn: cn=schema,cn=config
+        cn: schema
+        objectClass: olcSchemaConfig
+
+        include: file://${pkgs.openldap}/etc/schema/core.ldif
+        include: file://${pkgs.openldap}/etc/schema/cosine.ldif
+        include: file://${pkgs.openldap}/etc/schema/inetorgperson.ldif
+
+        dn: olcDatabase={1}mdb,cn=config
+        objectClass: olcDatabaseConfig
+        objectClass: olcMdbConfig
+        olcDatabase: {1}mdb
+        olcDbDirectory: /var/db/openldap
+        olcDbIndex: objectClass eq
+        olcSuffix: dc=example
+        olcRootDN: cn=root,dc=example
+        olcRootPW: notapassword
+      '';
+    in ''
+      machine.succeed(
+          "mkdir -p /var/db/slapd.d /var/db/openldap",
+          "slapadd -F /var/db/slapd.d -n0 -l ${config}",
+          "slapadd -F /var/db/slapd.d -n1 -l ${contents}",
+          "chown -R openldap:openldap /var/db/slapd.d /var/db/openldap",
+          "systemctl restart openldap",
+      )
+    '' + testScript;
+  }) { inherit system pkgs; };
+}
diff --git a/nixos/tests/openresty-lua.nix b/nixos/tests/openresty-lua.nix
new file mode 100644
index 00000000000..b177b3c194d
--- /dev/null
+++ b/nixos/tests/openresty-lua.nix
@@ -0,0 +1,55 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+  let
+    lualibs = [
+      pkgs.lua.pkgs.markdown
+    ];
+
+    getPath = lib: type: "${lib}/share/lua/${pkgs.lua.luaversion}/?.${type}";
+    getLuaPath = lib: getPath lib "lua";
+    luaPath = lib.concatStringsSep ";" (map getLuaPath lualibs);
+  in
+  {
+    name = "openresty-lua";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ bbigras ];
+    };
+
+    nodes = {
+      webserver = { pkgs, lib, ... }: {
+        services.nginx = {
+          enable = true;
+          package = pkgs.openresty;
+
+          commonHttpConfig = ''
+            lua_package_path '${luaPath};;';
+          '';
+
+          virtualHosts."default" = {
+            default = true;
+            locations."/" = {
+              extraConfig = ''
+                default_type text/html;
+                access_by_lua '
+                  local markdown = require "markdown"
+                  markdown("source")
+                ';
+              '';
+            };
+          };
+        };
+      };
+    };
+
+    testScript = { nodes, ... }:
+      ''
+        url = "http://localhost"
+
+        webserver.wait_for_unit("nginx")
+        webserver.wait_for_open_port(80)
+
+        http_code = webserver.succeed(
+          f"curl -w '%{{http_code}}' --head --fail {url}"
+        )
+        assert http_code.split("\n")[-1] == "200"
+      '';
+  })
diff --git a/nixos/tests/opensmtpd-rspamd.nix b/nixos/tests/opensmtpd-rspamd.nix
new file mode 100644
index 00000000000..19969a7b47d
--- /dev/null
+++ b/nixos/tests/opensmtpd-rspamd.nix
@@ -0,0 +1,141 @@
+import ./make-test-python.nix {
+  name = "opensmtpd-rspamd";
+
+  nodes = {
+    smtp1 = { pkgs, ... }: {
+      imports = [ common/user-account.nix ];
+      networking = {
+        firewall.allowedTCPPorts = [ 25 143 ];
+        useDHCP = false;
+        interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+          { address = "192.168.1.1"; prefixLength = 24; }
+        ];
+      };
+      environment.systemPackages = [ pkgs.opensmtpd ];
+      services.opensmtpd = {
+        enable = true;
+        extraServerArgs = [ "-v" ];
+        serverConfiguration = ''
+          listen on 0.0.0.0
+          action dovecot_deliver mda \
+            "${pkgs.dovecot}/libexec/dovecot/deliver -d %{user.username}"
+          match from any for local action dovecot_deliver
+
+          action do_relay relay
+          # DO NOT DO THIS IN PRODUCTION!
+          # Setting up authentication requires a certificate which is painful in
+          # a test environment, but THIS WOULD BE DANGEROUS OUTSIDE OF A
+          # WELL-CONTROLLED ENVIRONMENT!
+          match from any for any action do_relay
+        '';
+      };
+      services.dovecot2 = {
+        enable = true;
+        enableImap = true;
+        mailLocation = "maildir:~/mail";
+        protocols = [ "imap" ];
+      };
+    };
+
+    smtp2 = { pkgs, ... }: {
+      imports = [ common/user-account.nix ];
+      networking = {
+        firewall.allowedTCPPorts = [ 25 143 ];
+        useDHCP = false;
+        interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+          { address = "192.168.1.2"; prefixLength = 24; }
+        ];
+      };
+      environment.systemPackages = [ pkgs.opensmtpd ];
+      services.rspamd = {
+        enable = true;
+        locals."worker-normal.inc".text = ''
+          bind_socket = "127.0.0.1:11333";
+        '';
+      };
+      services.opensmtpd = {
+        enable = true;
+        extraServerArgs = [ "-v" ];
+        serverConfiguration = ''
+          filter rspamd proc-exec "${pkgs.opensmtpd-filter-rspamd}/bin/filter-rspamd"
+          listen on 0.0.0.0 filter rspamd
+          action dovecot_deliver mda \
+            "${pkgs.dovecot}/libexec/dovecot/deliver -d %{user.username}"
+          match from any for local action dovecot_deliver
+        '';
+      };
+      services.dovecot2 = {
+        enable = true;
+        enableImap = true;
+        mailLocation = "maildir:~/mail";
+        protocols = [ "imap" ];
+      };
+    };
+
+    client = { pkgs, ... }: {
+      networking = {
+        useDHCP = false;
+        interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+          { address = "192.168.1.3"; prefixLength = 24; }
+        ];
+      };
+      environment.systemPackages = let
+        sendTestMail = pkgs.writeScriptBin "send-a-test-mail" ''
+          #!${pkgs.python3.interpreter}
+          import smtplib, sys
+
+          with smtplib.SMTP('192.168.1.1') as smtp:
+            smtp.sendmail('alice@[192.168.1.1]', 'bob@[192.168.1.2]', """
+              From: alice@smtp1
+              To: bob@smtp2
+              Subject: Test
+
+              Hello World
+              Here goes the spam test
+              XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X
+            """)
+        '';
+
+        checkMailBounced = pkgs.writeScriptBin "check-mail-bounced" ''
+          #!${pkgs.python3.interpreter}
+          import imaplib
+
+          with imaplib.IMAP4('192.168.1.1', 143) as imap:
+            imap.login('alice', 'foobar')
+            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'
+            content = msg[0][1]
+            print("===> content:", content)
+            assert b"An error has occurred while attempting to deliver a message" in content
+        '';
+      in [ sendTestMail checkMailBounced ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    client.wait_for_unit("network-online.target")
+    smtp1.wait_for_unit("opensmtpd")
+    smtp2.wait_for_unit("opensmtpd")
+    smtp2.wait_for_unit("rspamd")
+    smtp2.wait_for_unit("dovecot2")
+
+    # To prevent sporadic failures during daemon startup, make sure
+    # services are listening on their ports before sending requests
+    smtp1.wait_for_open_port(25)
+    smtp2.wait_for_open_port(25)
+    smtp2.wait_for_open_port(143)
+    smtp2.wait_for_open_port(11333)
+
+    client.succeed("send-a-test-mail")
+    smtp1.wait_until_fails("smtpctl show queue | egrep .")
+    client.succeed("check-mail-bounced >&2")
+  '';
+
+  meta.timeout = 1800;
+}
diff --git a/nixos/tests/opensmtpd.nix b/nixos/tests/opensmtpd.nix
new file mode 100644
index 00000000000..17c1a569ba0
--- /dev/null
+++ b/nixos/tests/opensmtpd.nix
@@ -0,0 +1,125 @@
+import ./make-test-python.nix {
+  name = "opensmtpd";
+
+  nodes = {
+    smtp1 = { pkgs, ... }: {
+      imports = [ common/user-account.nix ];
+      networking = {
+        firewall.allowedTCPPorts = [ 25 ];
+        useDHCP = false;
+        interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+          { address = "192.168.1.1"; prefixLength = 24; }
+        ];
+      };
+      environment.systemPackages = [ pkgs.opensmtpd ];
+      services.opensmtpd = {
+        enable = true;
+        extraServerArgs = [ "-v" ];
+        serverConfiguration = ''
+          listen on 0.0.0.0
+          action do_relay relay
+          # DO NOT DO THIS IN PRODUCTION!
+          # Setting up authentication requires a certificate which is painful in
+          # a test environment, but THIS WOULD BE DANGEROUS OUTSIDE OF A
+          # WELL-CONTROLLED ENVIRONMENT!
+          match from any for any action do_relay
+        '';
+      };
+    };
+
+    smtp2 = { pkgs, ... }: {
+      imports = [ common/user-account.nix ];
+      networking = {
+        firewall.allowedTCPPorts = [ 25 143 ];
+        useDHCP = false;
+        interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+          { address = "192.168.1.2"; prefixLength = 24; }
+        ];
+      };
+      environment.systemPackages = [ pkgs.opensmtpd ];
+      services.opensmtpd = {
+        enable = true;
+        extraServerArgs = [ "-v" ];
+        serverConfiguration = ''
+          listen on 0.0.0.0
+          action dovecot_deliver mda \
+            "${pkgs.dovecot}/libexec/dovecot/deliver -d %{user.username}"
+          match from any for local action dovecot_deliver
+        '';
+      };
+      services.dovecot2 = {
+        enable = true;
+        enableImap = true;
+        mailLocation = "maildir:~/mail";
+        protocols = [ "imap" ];
+      };
+    };
+
+    client = { pkgs, ... }: {
+      networking = {
+        useDHCP = false;
+        interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+          { address = "192.168.1.3"; prefixLength = 24; }
+        ];
+      };
+      environment.systemPackages = let
+        sendTestMail = pkgs.writeScriptBin "send-a-test-mail" ''
+          #!${pkgs.python3.interpreter}
+          import smtplib, sys
+
+          with smtplib.SMTP('192.168.1.1') as smtp:
+            smtp.sendmail('alice@[192.168.1.1]', 'bob@[192.168.1.2]', """
+              From: alice@smtp1
+              To: bob@smtp2
+              Subject: Test
+
+              Hello World
+            """)
+        '';
+
+        checkMailLanded = pkgs.writeScriptBin "check-mail-landed" ''
+          #!${pkgs.python3.interpreter}
+          import imaplib
+
+          with imaplib.IMAP4('192.168.1.2', 143) as imap:
+            imap.login('bob', 'foobar')
+            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'
+            content = msg[0][1]
+            print("===> content:", content)
+            split = content.split(b'\r\n')
+            print("===> split:", split)
+            lastline = split[-3]
+            print("===> lastline:", lastline)
+            assert lastline.strip() == b'Hello World'
+        '';
+      in [ sendTestMail checkMailLanded ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    client.wait_for_unit("network-online.target")
+    smtp1.wait_for_unit("opensmtpd")
+    smtp2.wait_for_unit("opensmtpd")
+    smtp2.wait_for_unit("dovecot2")
+
+    # To prevent sporadic failures during daemon startup, make sure
+    # services are listening on their ports before sending requests
+    smtp1.wait_for_open_port(25)
+    smtp2.wait_for_open_port(25)
+    smtp2.wait_for_open_port(143)
+
+    client.succeed("send-a-test-mail")
+    smtp1.wait_until_fails("smtpctl show queue | egrep .")
+    smtp2.wait_until_fails("smtpctl show queue | egrep .")
+    client.succeed("check-mail-landed >&2")
+  '';
+
+  meta.timeout = 1800;
+}
diff --git a/nixos/tests/openssh.nix b/nixos/tests/openssh.nix
new file mode 100644
index 00000000000..003813379e6
--- /dev/null
+++ b/nixos/tests/openssh.nix
@@ -0,0 +1,112 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let inherit (import ./ssh-keys.nix pkgs)
+      snakeOilPrivateKey snakeOilPublicKey;
+in {
+  name = "openssh";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ aszlig eelco ];
+  };
+
+  nodes = {
+
+    server =
+      { ... }:
+
+      {
+        services.openssh.enable = true;
+        security.pam.services.sshd.limits =
+          [ { domain = "*"; item = "memlock"; type = "-"; value = 1024; } ];
+        users.users.root.openssh.authorizedKeys.keys = [
+          snakeOilPublicKey
+        ];
+      };
+
+    server_lazy =
+      { ... }:
+
+      {
+        services.openssh = { enable = true; startWhenNeeded = true; };
+        security.pam.services.sshd.limits =
+          [ { domain = "*"; item = "memlock"; type = "-"; value = 1024; } ];
+        users.users.root.openssh.authorizedKeys.keys = [
+          snakeOilPublicKey
+        ];
+      };
+
+    server_localhost_only =
+      { ... }:
+
+      {
+        services.openssh = {
+          enable = true; listenAddresses = [ { addr = "127.0.0.1"; port = 22; } ];
+        };
+      };
+
+    server_localhost_only_lazy =
+      { ... }:
+
+      {
+        services.openssh = {
+          enable = true; startWhenNeeded = true; listenAddresses = [ { addr = "127.0.0.1"; port = 22; } ];
+        };
+      };
+
+    client =
+      { ... }: { };
+
+  };
+
+  testScript = ''
+    start_all()
+
+    server.wait_for_unit("sshd")
+
+    with subtest("manual-authkey"):
+        client.succeed("mkdir -m 700 /root/.ssh")
+        client.succeed(
+            '${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N ""'
+        )
+        public_key = client.succeed(
+            "${pkgs.openssh}/bin/ssh-keygen -y -f /root/.ssh/id_ed25519"
+        )
+        public_key = public_key.strip()
+        client.succeed("chmod 600 /root/.ssh/id_ed25519")
+
+        server.succeed("mkdir -m 700 /root/.ssh")
+        server.succeed("echo '{}' > /root/.ssh/authorized_keys".format(public_key))
+        server_lazy.succeed("mkdir -m 700 /root/.ssh")
+        server_lazy.succeed("echo '{}' > /root/.ssh/authorized_keys".format(public_key))
+
+        client.wait_for_unit("network.target")
+        client.succeed(
+            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server 'echo hello world' >&2"
+        )
+        client.succeed(
+            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server 'ulimit -l' | grep 1024"
+        )
+
+        client.succeed(
+            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server_lazy 'echo hello world' >&2"
+        )
+        client.succeed(
+            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server_lazy 'ulimit -l' | grep 1024"
+        )
+
+    with subtest("configured-authkey"):
+        client.succeed(
+            "cat ${snakeOilPrivateKey} > privkey.snakeoil"
+        )
+        client.succeed("chmod 600 privkey.snakeoil")
+        client.succeed(
+            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil server true"
+        )
+        client.succeed(
+            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil server_lazy true"
+        )
+
+    with subtest("localhost-only"):
+        server_localhost_only.succeed("ss -nlt | grep '127.0.0.1:22'")
+        server_localhost_only_lazy.succeed("ss -nlt | grep '127.0.0.1:22'")
+  '';
+})
diff --git a/nixos/tests/openstack-image.nix b/nixos/tests/openstack-image.nix
new file mode 100644
index 00000000000..0b57dfb8e7e
--- /dev/null
+++ b/nixos/tests/openstack-image.nix
@@ -0,0 +1,98 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+with import common/ec2.nix { inherit makeTest pkgs; };
+
+let
+  image = (import ../lib/eval-config.nix {
+    inherit system;
+    modules = [
+      ../maintainers/scripts/openstack/openstack-image.nix
+      ../modules/testing/test-instrumentation.nix
+      ../modules/profiles/qemu-guest.nix
+      {
+        # Needed by nixos-rebuild due to lack of network access.
+        system.extraDependencies = with pkgs; [
+          stdenv
+        ];
+      }
+    ];
+  }).config.system.build.openstackImage + "/nixos.qcow2";
+
+  sshKeys = import ./ssh-keys.nix pkgs;
+  snakeOilPrivateKey = sshKeys.snakeOilPrivateKey.text;
+  snakeOilPrivateKeyFile = pkgs.writeText "private-key" snakeOilPrivateKey;
+  snakeOilPublicKey = sshKeys.snakeOilPublicKey;
+
+in {
+  metadata = makeEc2Test {
+    name = "openstack-ec2-metadata";
+    inherit image;
+    sshPublicKey = snakeOilPublicKey;
+    userData = ''
+      SSH_HOST_ED25519_KEY_PUB:${snakeOilPublicKey}
+      SSH_HOST_ED25519_KEY:${replaceStrings ["\n"] ["|"] snakeOilPrivateKey}
+    '';
+    script = ''
+      machine.start()
+      machine.wait_for_file("/etc/ec2-metadata/user-data")
+      machine.wait_for_unit("sshd.service")
+
+      machine.succeed("grep unknown /etc/ec2-metadata/ami-manifest-path")
+
+      # We have no keys configured on the client side yet, so this should fail
+      machine.fail("ssh -o BatchMode=yes localhost exit")
+
+      # Let's install our client private key
+      machine.succeed("mkdir -p ~/.ssh")
+
+      machine.copy_from_host_via_shell(
+          "${snakeOilPrivateKeyFile}", "~/.ssh/id_ed25519"
+      )
+      machine.succeed("chmod 600 ~/.ssh/id_ed25519")
+
+      # We haven't configured the host key yet, so this should still fail
+      machine.fail("ssh -o BatchMode=yes localhost exit")
+
+      # Add the host key; ssh should finally succeed
+      machine.succeed(
+          "echo localhost,127.0.0.1 ${snakeOilPublicKey} > ~/.ssh/known_hosts"
+      )
+      machine.succeed("ssh -o BatchMode=yes localhost exit")
+
+      # Just to make sure resizing is idempotent.
+      machine.shutdown()
+      machine.start()
+      machine.wait_for_file("/etc/ec2-metadata/user-data")
+    '';
+  };
+
+  userdata = makeEc2Test {
+    name = "openstack-ec2-metadata";
+    inherit image;
+    sshPublicKey = snakeOilPublicKey;
+    userData = ''
+      { pkgs, ... }:
+      {
+        imports = [
+          <nixpkgs/nixos/modules/virtualisation/openstack-config.nix>
+          <nixpkgs/nixos/modules/testing/test-instrumentation.nix>
+          <nixpkgs/nixos/modules/profiles/qemu-guest.nix>
+        ];
+        environment.etc.testFile = {
+          text = "whoa";
+        };
+      }
+    '';
+    script = ''
+      machine.start()
+      machine.wait_for_file("/etc/testFile")
+      assert "whoa" in machine.succeed("cat /etc/testFile")
+    '';
+  };
+}
diff --git a/nixos/tests/opentabletdriver.nix b/nixos/tests/opentabletdriver.nix
new file mode 100644
index 00000000000..fe345a7bec7
--- /dev/null
+++ b/nixos/tests/opentabletdriver.nix
@@ -0,0 +1,30 @@
+import ./make-test-python.nix ( { pkgs, ... }: let
+  testUser = "alice";
+in {
+  name = "opentabletdriver";
+  meta = {
+    maintainers = with pkgs.lib.maintainers; [ thiagokokada ];
+  };
+
+  machine = { pkgs, ... }:
+    {
+      imports = [
+        ./common/user-account.nix
+        ./common/x11.nix
+      ];
+      test-support.displayManager.auto.user = testUser;
+      hardware.opentabletdriver.enable = true;
+    };
+
+  testScript =
+    ''
+      machine.start()
+      machine.wait_for_x()
+      machine.wait_for_unit("opentabletdriver.service", "${testUser}")
+
+      machine.succeed("cat /etc/udev/rules.d/99-opentabletdriver.rules")
+      # Will fail if service is not running
+      # Needs to run as the same user that started the service
+      machine.succeed("su - ${testUser} -c 'otd detect'")
+    '';
+})
diff --git a/nixos/tests/orangefs.nix b/nixos/tests/orangefs.nix
new file mode 100644
index 00000000000..fe9f9cc37ea
--- /dev/null
+++ b/nixos/tests/orangefs.nix
@@ -0,0 +1,82 @@
+import ./make-test-python.nix ({ ... } :
+
+let
+  server = { pkgs, ... } : {
+    networking.firewall.allowedTCPPorts = [ 3334 ];
+    boot.initrd.postDeviceCommands = ''
+      ${pkgs.e2fsprogs}/bin/mkfs.ext4 -L data /dev/vdb
+    '';
+
+    virtualisation.emptyDiskImages = [ 4096 ];
+
+    virtualisation.fileSystems =
+      { "/data" =
+          { device = "/dev/disk/by-label/data";
+            fsType = "ext4";
+          };
+      };
+
+    services.orangefs.server = {
+      enable = true;
+      dataStorageSpace = "/data/storage";
+      metadataStorageSpace = "/data/meta";
+      servers = {
+        server1 = "tcp://server1:3334";
+        server2 = "tcp://server2:3334";
+      };
+    };
+  };
+
+  client = { lib, ... } : {
+    networking.firewall.enable = true;
+
+    services.orangefs.client = {
+      enable = true;
+      fileSystems = [{
+        target = "tcp://server1:3334/orangefs";
+        mountPoint = "/orangefs";
+      }];
+    };
+  };
+
+in {
+  name = "orangefs";
+
+  nodes = {
+    server1 = server;
+    server2 = server;
+
+    client1 = client;
+    client2 = client;
+  };
+
+  testScript = ''
+    # format storage
+    for server in server1, server2:
+        server.start()
+        server.wait_for_unit("multi-user.target")
+        server.succeed("mkdir -p /data/storage /data/meta")
+        server.succeed("chown orangefs:orangefs /data/storage /data/meta")
+        server.succeed("chmod 0770 /data/storage /data/meta")
+        server.succeed(
+            "sudo -g orangefs -u orangefs pvfs2-server -f /etc/orangefs/server.conf"
+        )
+
+    # start services after storage is formated on all machines
+    for server in server1, server2:
+        server.succeed("systemctl start orangefs-server.service")
+
+    with subtest("clients can reach and mount the FS"):
+        for client in client1, client2:
+            client.start()
+            client.wait_for_unit("orangefs-client.service")
+            # Both servers need to be reachable
+            client.succeed("pvfs2-check-server -h server1 -f orangefs -n tcp -p 3334")
+            client.succeed("pvfs2-check-server -h server2 -f orangefs -n tcp -p 3334")
+            client.wait_for_unit("orangefs.mount")
+
+    with subtest("R/W test between clients"):
+        client1.succeed("echo test > /orangefs/file1")
+        client2.succeed("grep test /orangefs/file1")
+  '';
+})
diff --git a/nixos/tests/os-prober.nix b/nixos/tests/os-prober.nix
new file mode 100644
index 00000000000..90375450fe1
--- /dev/null
+++ b/nixos/tests/os-prober.nix
@@ -0,0 +1,121 @@
+import ./make-test-python.nix ({pkgs, lib, ...}:
+let
+  # A filesystem image with a (presumably) bootable debian
+  debianImage = pkgs.vmTools.diskImageFuns.debian9i386 {
+    # os-prober cannot detect systems installed on disks without a partition table
+    # so we create the disk ourselves
+    createRootFS = with pkgs; ''
+      ${parted}/bin/parted --script /dev/vda mklabel msdos
+      ${parted}/sbin/parted --script /dev/vda -- mkpart primary ext2 1M -1s
+      mkdir /mnt
+      ${e2fsprogs}/bin/mkfs.ext4 /dev/vda1
+      ${util-linux}/bin/mount -t ext4 /dev/vda1 /mnt
+
+      if test -e /mnt/.debug; then
+        exec ${bash}/bin/sh
+      fi
+      touch /mnt/.debug
+
+      mkdir /mnt/proc /mnt/dev /mnt/sys
+    '';
+    extraPackages = [
+      # /etc/os-release
+      "base-files"
+      # make the disk bootable-looking
+      "grub2" "linux-image-686"
+    ];
+    # install grub
+    postInstall = ''
+      ln -sf /proc/self/mounts > /etc/mtab
+      PATH=/usr/bin:/bin:/usr/sbin:/sbin $chroot /mnt \
+        grub-install /dev/vda --force
+      PATH=/usr/bin:/bin:/usr/sbin:/sbin $chroot /mnt \
+        update-grub
+    '';
+  };
+
+  # a part of the configuration of the test vm
+  simpleConfig = {
+    boot.loader.grub = {
+      enable = true;
+      useOSProber = true;
+      device = "/dev/vda";
+      # vda is a filesystem without partition table
+      forceInstall = true;
+    };
+    nix.settings = {
+      substituters = lib.mkForce [];
+      hashed-mirrors = null;
+      connect-timeout = 1;
+    };
+    # save some memory
+    documentation.enable = false;
+  };
+  # /etc/nixos/configuration.nix for the vm
+  configFile = pkgs.writeText "configuration.nix"  ''
+    {config, pkgs, lib, ...}: ({
+    imports =
+          [ ./hardware-configuration.nix
+            <nixpkgs/nixos/modules/testing/test-instrumentation.nix>
+          ];
+    } // lib.importJSON ${
+      pkgs.writeText "simpleConfig.json" (builtins.toJSON simpleConfig)
+    })
+  '';
+in {
+  name = "os-prober";
+
+  machine = { config, pkgs, ... }: (simpleConfig // {
+      imports = [ ../modules/profiles/installation-device.nix
+                  ../modules/profiles/base.nix ];
+      virtualisation.memorySize = 1300;
+      # To add the secondary disk:
+      virtualisation.qemu.options = [ "-drive index=2,file=${debianImage}/disk-image.qcow2,read-only,if=virtio" ];
+
+      # The test cannot access the network, so any packages
+      # nixos-rebuild needs must be included in the VM.
+      system.extraDependencies = with pkgs;
+        [ sudo
+          libxml2.bin
+          libxslt.bin
+          desktop-file-utils
+          docbook5
+          docbook_xsl_ns
+          unionfs-fuse
+          ntp
+          nixos-artwork.wallpapers.simple-dark-gray-bottom
+          perlPackages.XMLLibXML
+          perlPackages.ListCompare
+          shared-mime-info
+          texinfo
+          xorg.lndir
+          grub2
+
+          # add curl so that rather than seeing the test attempt to download
+          # curl's tarball, we see what it's trying to download
+          curl
+        ];
+  });
+
+  testScript = ''
+    machine.start()
+    machine.succeed("udevadm settle")
+    machine.wait_for_unit("multi-user.target")
+    print(machine.succeed("lsblk"))
+
+    # check that os-prober works standalone
+    machine.succeed(
+        "${pkgs.os-prober}/bin/os-prober | grep /dev/vdb1"
+    )
+
+    # rebuild and test that debian is available in the grub menu
+    machine.succeed("nixos-generate-config")
+    machine.copy_from_host(
+        "${configFile}",
+        "/etc/nixos/configuration.nix",
+    )
+    machine.succeed("nixos-rebuild boot --show-trace >&2")
+
+    machine.succeed("egrep 'menuentry.*debian' /boot/grub/grub.cfg")
+  '';
+})
diff --git a/nixos/tests/osrm-backend.nix b/nixos/tests/osrm-backend.nix
new file mode 100644
index 00000000000..4067d5b1a23
--- /dev/null
+++ b/nixos/tests/osrm-backend.nix
@@ -0,0 +1,57 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+let
+  port = 5000;
+in {
+  name = "osrm-backend";
+  meta.maintainers = [ lib.maintainers.erictapen ];
+
+  machine = { config, pkgs, ... }:{
+
+    services.osrm = {
+      enable = true;
+      inherit port;
+      dataFile = let
+        filename = "monaco";
+        osrm-data = pkgs.stdenv.mkDerivation {
+          name = "osrm-data";
+
+          buildInputs = [ pkgs.osrm-backend ];
+
+          # This is a pbf file of monaco, downloaded at 2019-01-04 from
+          # http://download.geofabrik.de/europe/monaco-latest.osm.pbf
+          # as apparently no provider of OSM files guarantees immutability,
+          # this is hosted as a gist on GitHub.
+          src = pkgs.fetchgit {
+            url = "https://gist.github.com/erictapen/01e39f73a6c856eac53ba809a94cdb83";
+            rev = "9b1ff0f24deb40e5cf7df51f843dbe860637b8ce";
+            sha256 = "1scqhmrfnpwsy5i2a9jpggqnvfgj4hv9p4qyvc79321pzkbv59nx";
+          };
+
+          buildCommand = ''
+            cp $src/${filename}.osm.pbf .
+            ${pkgs.osrm-backend}/bin/osrm-extract -p ${pkgs.osrm-backend}/share/osrm/profiles/car.lua ${filename}.osm.pbf
+            ${pkgs.osrm-backend}/bin/osrm-partition ${filename}.osrm
+            ${pkgs.osrm-backend}/bin/osrm-customize ${filename}.osrm
+            mkdir -p $out
+            cp ${filename}* $out/
+          '';
+        };
+      in "${osrm-data}/${filename}.osrm";
+    };
+
+    environment.systemPackages = [ pkgs.jq ];
+  };
+
+  testScript = let
+    query = "http://localhost:${toString port}/route/v1/driving/7.41720,43.73304;7.42463,43.73886?steps=true";
+  in ''
+    machine.wait_for_unit("osrm.service")
+    machine.wait_for_open_port(${toString port})
+    assert "Boulevard Rainier III" in machine.succeed(
+        "curl --fail --silent '${query}' | jq .waypoints[0].name"
+    )
+    assert "Avenue de la Costa" in machine.succeed(
+        "curl --fail --silent '${query}' | jq .waypoints[1].name"
+    )
+  '';
+})
diff --git a/nixos/tests/overlayfs.nix b/nixos/tests/overlayfs.nix
new file mode 100644
index 00000000000..1768f1fea1e
--- /dev/null
+++ b/nixos/tests/overlayfs.nix
@@ -0,0 +1,47 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "overlayfs";
+  meta.maintainers = with pkgs.lib.maintainers; [ bachp ];
+
+  machine = { pkgs, ... }: {
+    virtualisation.emptyDiskImages = [ 512 ];
+    networking.hostId = "deadbeef";
+    environment.systemPackages = with pkgs; [ parted ];
+  };
+
+  testScript = ''
+    machine.succeed("ls /dev")
+
+    machine.succeed("mkdir -p /tmp/mnt")
+
+    # Test ext4 + overlayfs
+    machine.succeed(
+      'mkfs.ext4 -F -L overlay-ext4 /dev/vdb',
+      'mount -t ext4 /dev/vdb /tmp/mnt',
+      'mkdir -p /tmp/mnt/upper /tmp/mnt/lower /tmp/mnt/work /tmp/mnt/merged',
+      # Setup some existing files
+      'echo Replace > /tmp/mnt/lower/replace.txt',
+      'echo Append > /tmp/mnt/lower/append.txt',
+      'echo Overwrite > /tmp/mnt/lower/overwrite.txt',
+      'mount -t overlay overlay -o lowerdir=/tmp/mnt/lower,upperdir=/tmp/mnt/upper,workdir=/tmp/mnt/work /tmp/mnt/merged',
+      # Test new
+      'echo New > /tmp/mnt/merged/new.txt',
+      '[[ "$(cat /tmp/mnt/merged/new.txt)" == New ]]',
+      # Test replace
+      '[[ "$(cat /tmp/mnt/merged/replace.txt)" == Replace ]]',
+      'echo Replaced > /tmp/mnt/merged/replace-tmp.txt',
+      'mv /tmp/mnt/merged/replace-tmp.txt /tmp/mnt/merged/replace.txt',
+      '[[ "$(cat /tmp/mnt/merged/replace.txt)" == Replaced ]]',
+      # Overwrite
+      '[[ "$(cat /tmp/mnt/merged/overwrite.txt)" == Overwrite ]]',
+      'echo Overwritten > /tmp/mnt/merged/overwrite.txt',
+      '[[ "$(cat /tmp/mnt/merged/overwrite.txt)" == Overwritten ]]',
+      # Test append
+      '[[ "$(cat /tmp/mnt/merged/append.txt)" == Append ]]',
+      'echo ed >> /tmp/mnt/merged/append.txt',
+      '[[ "$(cat /tmp/mnt/merged/append.txt)" == "Append\ned" ]]',
+      'umount /tmp/mnt/merged',
+      'umount /tmp/mnt',
+      'udevadm settle',
+    )
+  '';
+})
diff --git a/nixos/tests/owncast.nix b/nixos/tests/owncast.nix
new file mode 100644
index 00000000000..debb34f5009
--- /dev/null
+++ b/nixos/tests/owncast.nix
@@ -0,0 +1,42 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "owncast";
+  meta = with pkgs.lib.maintainers; { maintainers = [ MayNiklas ]; };
+
+  nodes = {
+    client = { pkgs, ... }: with pkgs.lib; {
+      networking = {
+        dhcpcd.enable = false;
+        interfaces.eth1.ipv6.addresses = mkOverride 0 [ { address = "fd00::2"; prefixLength = 64; } ];
+        interfaces.eth1.ipv4.addresses = mkOverride 0 [ { address = "192.168.1.2"; prefixLength = 24; } ];
+      };
+    };
+    server = { pkgs, ... }: with pkgs.lib; {
+      networking = {
+        dhcpcd.enable = false;
+        useNetworkd = true;
+        useDHCP = false;
+        interfaces.eth1.ipv6.addresses = mkOverride 0 [ { address = "fd00::1"; prefixLength = 64; } ];
+        interfaces.eth1.ipv4.addresses = mkOverride 0 [ { address = "192.168.1.1"; prefixLength = 24; } ];
+
+        firewall.allowedTCPPorts = [ 8080 ];
+      };
+
+      services.owncast = {
+        enable = true;
+        listen = "0.0.0.0";
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    client.wait_for_unit("network-online.target")
+    server.wait_for_unit("network-online.target")
+    server.wait_for_unit("owncast.service")
+    server.wait_until_succeeds("ss -ntl | grep -q 8080")
+
+    client.succeed("curl http://192.168.1.1:8080/api/status")
+    client.succeed("curl http://[fd00::1]:8080/api/status")
+  '';
+})
diff --git a/nixos/tests/pacemaker.nix b/nixos/tests/pacemaker.nix
new file mode 100644
index 00000000000..68455761495
--- /dev/null
+++ b/nixos/tests/pacemaker.nix
@@ -0,0 +1,110 @@
+import ./make-test-python.nix  ({ pkgs, lib, ... }: rec {
+  name = "pacemaker";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ astro ];
+  };
+
+  nodes =
+    let
+      node = i: {
+        networking.interfaces.eth1.ipv4.addresses = [ {
+          address = "192.168.0.${toString i}";
+          prefixLength = 24;
+        } ];
+
+        services.corosync = {
+          enable = true;
+          clusterName = "zentralwerk-network";
+          nodelist = lib.imap (i: name: {
+            nodeid = i;
+            inherit name;
+            ring_addrs = [
+              (builtins.head nodes.${name}.networking.interfaces.eth1.ipv4.addresses).address
+            ];
+          }) (builtins.attrNames nodes);
+        };
+        environment.etc."corosync/authkey" = {
+          source = builtins.toFile "authkey"
+            # minimum length: 128 bytes
+            "testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest";
+          mode = "0400";
+        };
+
+        services.pacemaker.enable = true;
+
+        # used for pacemaker resource
+        systemd.services.ha-cat = {
+          description = "Highly available netcat";
+          serviceConfig.ExecStart = "${pkgs.netcat}/bin/nc -l discard";
+        };
+      };
+    in {
+      node1 = node 1;
+      node2 = node 2;
+      node3 = node 3;
+    };
+
+  # sets up pacemaker with resources configuration, then crashes a
+  # node and waits for service restart on another node
+  testScript =
+    let
+      resources = builtins.toFile "cib-resources.xml" ''
+        <resources>
+          <primitive id="cat" class="systemd" type="ha-cat">
+            <operations>
+              <op id="stop-cat" name="start" interval="0" timeout="1s"/>
+              <op id="start-cat" name="start" interval="0" timeout="1s"/>
+              <op id="monitor-cat" name="monitor" interval="1s" timeout="1s"/>
+            </operations>
+          </primitive>
+        </resources>
+      '';
+    in ''
+      import re
+      import time
+
+      start_all()
+
+      ${lib.concatMapStrings (node: ''
+        ${node}.wait_until_succeeds("corosync-quorumtool")
+        ${node}.wait_for_unit("pacemaker.service")
+      '') (builtins.attrNames nodes)}
+
+      # No STONITH device
+      node1.succeed("crm_attribute -t crm_config -n stonith-enabled -v false")
+      # Configure the cat resource
+      node1.succeed("cibadmin --replace --scope resources --xml-file ${resources}")
+
+      # wait until the service is started
+      while True:
+        output = node1.succeed("crm_resource -r cat --locate")
+        match = re.search("is running on: (.+)", output)
+        if match:
+          for machine in machines:
+            if machine.name == match.group(1):
+              current_node = machine
+          break
+        time.sleep(1)
+
+      current_node.log("Service running here!")
+      current_node.crash()
+
+      # pick another node that's still up
+      for machine in machines:
+        if machine.booted:
+          check_node = machine
+      # find where the service has been started next
+      while True:
+        output = check_node.succeed("crm_resource -r cat --locate")
+        match = re.search("is running on: (.+)", output)
+        # output will remain the old current_node until the crash is detected by pacemaker
+        if match and match.group(1) != current_node.name:
+          for machine in machines:
+            if machine.name == match.group(1):
+              next_node = machine
+          break
+        time.sleep(1)
+
+      next_node.log("Service migrated here!")
+  '';
+})
diff --git a/nixos/tests/packagekit.nix b/nixos/tests/packagekit.nix
new file mode 100644
index 00000000000..020a4e65e6d
--- /dev/null
+++ b/nixos/tests/packagekit.nix
@@ -0,0 +1,25 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "packagekit";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ peterhoeg ];
+  };
+
+  machine = { ... }: {
+    environment.systemPackages = with pkgs; [ dbus ];
+    services.packagekit = {
+      enable = true;
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    # send a dbus message to activate the service
+    machine.succeed(
+        "dbus-send --system --type=method_call --print-reply --dest=org.freedesktop.PackageKit /org/freedesktop/PackageKit org.freedesktop.DBus.Introspectable.Introspect"
+    )
+
+    # so now it should be running
+    machine.wait_for_unit("packagekit.service")
+  '';
+})
diff --git a/nixos/tests/pam/pam-file-contents.nix b/nixos/tests/pam/pam-file-contents.nix
new file mode 100644
index 00000000000..86c61003aeb
--- /dev/null
+++ b/nixos/tests/pam/pam-file-contents.nix
@@ -0,0 +1,25 @@
+let
+  name = "pam";
+in
+import ../make-test-python.nix ({ pkgs, ... }: {
+
+  nodes.machine = { ... }: {
+    imports = [ ../../modules/profiles/minimal.nix ];
+
+    krb5.enable = true;
+
+    users = {
+      mutableUsers = false;
+      users = {
+        user = {
+          isNormalUser = true;
+        };
+      };
+    };
+  };
+
+  testScript = builtins.replaceStrings
+    [ "@@pam_ccreds@@" "@@pam_krb5@@" ]
+    [ pkgs.pam_ccreds.outPath pkgs.pam_krb5.outPath ]
+    (builtins.readFile ./test_chfn.py);
+})
diff --git a/nixos/tests/pam/pam-oath-login.nix b/nixos/tests/pam/pam-oath-login.nix
new file mode 100644
index 00000000000..597596b211b
--- /dev/null
+++ b/nixos/tests/pam/pam-oath-login.nix
@@ -0,0 +1,108 @@
+import ../make-test-python.nix ({ ... }:
+
+let
+  oathSnakeoilSecret = "cdd4083ef8ff1fa9178c6d46bfb1a3";
+
+  # With HOTP mode the password is calculated based on a counter of
+  # how many passwords have been made. In this env, we'll always be on
+  # the 0th counter, so the password is static.
+  #
+  # Generated in nix-shell -p oathToolkit
+  # via: oathtool -v -d6 -w10 cdd4083ef8ff1fa9178c6d46bfb1a3
+  # and picking a the first 4:
+  oathSnakeOilPassword1 = "143349";
+  oathSnakeOilPassword2 = "801753";
+
+  alicePassword = "foobar";
+  # Generated via: mkpasswd -m sha-512 and passing in "foobar"
+  hashedAlicePassword = "$6$MsMrE1q.1HrCgTS$Vq2e/uILzYjSN836TobAyN9xh9oi7EmCmucnZID25qgPoibkw8qTCugiAPnn4eCGvn1A.7oEBFJaaGUaJsQQY.";
+
+in
+{
+  name = "pam-oath-login";
+
+  machine =
+    { ... }:
+    {
+      security.pam.oath = {
+        enable = true;
+      };
+
+      users.users.alice = {
+        isNormalUser = true;
+        name = "alice";
+        uid = 1000;
+        hashedPassword = hashedAlicePassword;
+        extraGroups = [ "wheel" ];
+        createHome = true;
+        home = "/home/alice";
+      };
+
+
+      systemd.services.setupOathSnakeoilFile = {
+        wantedBy = [ "default.target" ];
+        before = [ "default.target" ];
+        unitConfig = {
+          type = "oneshot";
+          RemainAfterExit = true;
+        };
+        script = ''
+          touch /etc/users.oath
+          chmod 600 /etc/users.oath
+          chown root /etc/users.oath
+          echo "HOTP/E/6 alice - ${oathSnakeoilSecret}" > /etc/users.oath
+        '';
+      };
+    };
+
+  testScript = ''
+    def switch_to_tty(tty_number):
+        machine.fail(f"pgrep -f 'agetty.*tty{tty_number}'")
+        machine.send_key(f"alt-f{tty_number}")
+        machine.wait_until_succeeds(f"[ $(fgconsole) = {tty_number} ]")
+        machine.wait_for_unit(f"getty@tty{tty_number}.service")
+        machine.wait_until_succeeds(f"pgrep -f 'agetty.*tty{tty_number}'")
+
+
+    def enter_user_alice(tty_number):
+        machine.wait_until_tty_matches(tty_number, "login: ")
+        machine.send_chars("alice\n")
+        machine.wait_until_tty_matches(tty_number, "login: alice")
+        machine.wait_until_succeeds("pgrep login")
+        machine.wait_until_tty_matches(tty_number, "One-time password")
+
+
+    machine.wait_for_unit("multi-user.target")
+    machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
+    machine.screenshot("postboot")
+
+    with subtest("Invalid password"):
+        switch_to_tty(2)
+        enter_user_alice(2)
+
+        machine.send_chars("${oathSnakeOilPassword1}\n")
+        machine.wait_until_tty_matches(2, "Password: ")
+        machine.send_chars("blorg\n")
+        machine.wait_until_tty_matches(2, "Login incorrect")
+
+    with subtest("Invalid oath token"):
+        switch_to_tty(3)
+        enter_user_alice(3)
+
+        machine.send_chars("000000\n")
+        machine.wait_until_tty_matches(3, "Login incorrect")
+        machine.wait_until_tty_matches(3, "login:")
+
+    with subtest("Happy path: Both passwords are mandatory to get us in"):
+        switch_to_tty(4)
+        enter_user_alice(4)
+
+        machine.send_chars("${oathSnakeOilPassword2}\n")
+        machine.wait_until_tty_matches(4, "Password: ")
+        machine.send_chars("${alicePassword}\n")
+
+        machine.wait_until_succeeds("pgrep -u alice bash")
+        machine.send_chars("touch  done4\n")
+        machine.wait_for_file("/home/alice/done4")
+    '';
+})
diff --git a/nixos/tests/pam/pam-u2f.nix b/nixos/tests/pam/pam-u2f.nix
new file mode 100644
index 00000000000..0ac6ac17be8
--- /dev/null
+++ b/nixos/tests/pam/pam-u2f.nix
@@ -0,0 +1,25 @@
+import ../make-test-python.nix ({ ... }:
+
+{
+  name = "pam-u2f";
+
+  machine =
+    { ... }:
+    {
+      security.pam.u2f = {
+        control = "required";
+        cue = true;
+        debug = true;
+        enable = true;
+        interactive = true;
+      };
+    };
+
+  testScript =
+    ''
+      machine.wait_for_unit("multi-user.target")
+      machine.succeed(
+          'egrep "auth required .*/lib/security/pam_u2f.so.*debug.*interactive.*cue" /etc/pam.d/ -R'
+      )
+    '';
+})
diff --git a/nixos/tests/pam/test_chfn.py b/nixos/tests/pam/test_chfn.py
new file mode 100644
index 00000000000..b108a9423ca
--- /dev/null
+++ b/nixos/tests/pam/test_chfn.py
@@ -0,0 +1,27 @@
+expected_lines = {
+    "account required pam_unix.so",
+    "account sufficient @@pam_krb5@@/lib/security/pam_krb5.so",
+    "auth [default=die success=done] @@pam_ccreds@@/lib/security/pam_ccreds.so action=validate use_first_pass",
+    "auth [default=ignore success=1 service_err=reset] @@pam_krb5@@/lib/security/pam_krb5.so use_first_pass",
+    "auth required pam_deny.so",
+    "auth sufficient @@pam_ccreds@@/lib/security/pam_ccreds.so action=store use_first_pass",
+    "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",
+    "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",
+}
+actual_lines = set(machine.succeed("cat /etc/pam.d/chfn").splitlines())
+
+missing_lines = expected_lines - actual_lines
+extra_lines = actual_lines - expected_lines
+non_functional_lines = set([line for line in extra_lines if (line == "" or line.startswith("#"))])
+unexpected_functional_lines = extra_lines - non_functional_lines
+
+with subtest("All expected lines are in the file"):
+    assert not missing_lines, f"Missing lines: {missing_lines}"
+
+with subtest("All remaining lines are empty or comments"):
+    assert not unexpected_functional_lines, f"Unexpected lines: {unexpected_functional_lines}"
diff --git a/nixos/tests/pantheon.nix b/nixos/tests/pantheon.nix
new file mode 100644
index 00000000000..989d29a966d
--- /dev/null
+++ b/nixos/tests/pantheon.nix
@@ -0,0 +1,58 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} :
+
+{
+  name = "pantheon";
+
+  meta = with lib; {
+    maintainers = teams.pantheon.members;
+  };
+
+  machine = { ... }:
+
+  {
+    imports = [ ./common/user-account.nix ];
+
+    services.xserver.enable = true;
+    services.xserver.desktopManager.pantheon.enable = true;
+
+  };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+    bob = nodes.machine.config.users.users.bob;
+  in ''
+    machine.wait_for_unit("display-manager.service")
+
+    with subtest("Test we can see usernames in elementary-greeter"):
+        machine.wait_for_text("${user.description}")
+        # OCR was struggling with this one.
+        # machine.wait_for_text("${bob.description}")
+        machine.screenshot("elementary_greeter_lightdm")
+
+    with subtest("Login with elementary-greeter"):
+        machine.send_chars("${user.password}\n")
+        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}")
+
+    # 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")
+        machine.wait_until_succeeds("pgrep -f io.elementary.wingpanel")
+        machine.wait_for_window("io.elementary.wingpanel")
+        machine.wait_until_succeeds("pgrep plank")
+        machine.wait_for_window("plank")
+
+    with subtest("Open elementary terminal"):
+        machine.execute("su - ${user.name} -c 'DISPLAY=:0 io.elementary.terminal >&2 &'")
+        machine.wait_for_window("io.elementary.terminal")
+        machine.sleep(20)
+        machine.screenshot("screen")
+  '';
+})
diff --git a/nixos/tests/paperless-ng.nix b/nixos/tests/paperless-ng.nix
new file mode 100644
index 00000000000..618eeec6b12
--- /dev/null
+++ b/nixos/tests/paperless-ng.nix
@@ -0,0 +1,45 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "paperless-ng";
+  meta.maintainers = with lib.maintainers; [ earvstedt Flakebi ];
+
+  nodes.machine = { pkgs, ... }: {
+    environment.systemPackages = with pkgs; [ imagemagick jq ];
+    services.paperless-ng = {
+      enable = true;
+      passwordFile = builtins.toFile "password" "admin";
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("paperless-ng-consumer.service")
+
+    with subtest("Create test doc"):
+        machine.succeed(
+            "convert -size 400x40 xc:white -font 'DejaVu-Sans' -pointsize 20 -fill black "
+            "-annotate +5+20 'hello world 16-10-2005' /var/lib/paperless/consume/doc.png"
+        )
+
+    with subtest("Web interface gets ready"):
+        machine.wait_for_unit("paperless-ng-web.service")
+        # Wait until server accepts connections
+        machine.wait_until_succeeds("curl -fs localhost:28981")
+
+    with subtest("Create web test doc"):
+        machine.succeed(
+            "convert -size 400x40 xc:white -font 'DejaVu-Sans' -pointsize 20 -fill black "
+            "-annotate +5+20 'hello web 16-10-2005' /tmp/webdoc.png"
+        )
+        machine.wait_until_succeeds("curl -u admin:admin -F document=@/tmp/webdoc.png -fs localhost:28981/api/documents/post_document/")
+
+    with subtest("Documents are consumed"):
+        machine.wait_until_succeeds(
+            "(($(curl -u admin:admin -fs localhost:28981/api/documents/ | jq .count) == 2))"
+        )
+        assert "2005-10-16" in machine.succeed(
+            "curl -u admin:admin -fs localhost:28981/api/documents/ | jq '.results | .[0] | .created'"
+        )
+        assert "2005-10-16" in machine.succeed(
+            "curl -u admin:admin -fs localhost:28981/api/documents/ | jq '.results | .[1] | .created'"
+        )
+  '';
+})
diff --git a/nixos/tests/parsedmarc/default.nix b/nixos/tests/parsedmarc/default.nix
new file mode 100644
index 00000000000..50b977723e9
--- /dev/null
+++ b/nixos/tests/parsedmarc/default.nix
@@ -0,0 +1,235 @@
+# This tests parsedmarc by sending a report to its monitored email
+# address and reading the results out of Elasticsearch.
+
+{ pkgs, ... }@args:
+let
+  inherit (import ../../lib/testing-python.nix args) makeTest;
+  inherit (pkgs) lib;
+
+  dmarcTestReport = builtins.fetchurl {
+    name = "dmarc-test-report";
+    url = "https://github.com/domainaware/parsedmarc/raw/f45ab94e0608088e0433557608d9f4e9517d3afe/samples/aggregate/estadocuenta1.infonacot.gob.mx!example.com!1536853302!1536939702!2940.xml.zip";
+    sha256 = "0dq64cj49711kbja27pjl2hy0d3azrjxg91kqrh40x46fkn1dwkx";
+  };
+
+  sendEmail = address:
+    pkgs.writeScriptBin "send-email" ''
+      #!${pkgs.python3.interpreter}
+      import smtplib
+      from email import encoders
+      from email.mime.base import MIMEBase
+      from email.mime.multipart import MIMEMultipart
+      from email.mime.text import MIMEText
+
+      sender_email = "dmarc_tester@fake.domain"
+      receiver_email = "${address}"
+
+      message = MIMEMultipart()
+      message["From"] = sender_email
+      message["To"] = receiver_email
+      message["Subject"] = "DMARC test"
+
+      message.attach(MIMEText("Testing parsedmarc", "plain"))
+
+      attachment = MIMEBase("application", "zip")
+
+      with open("${dmarcTestReport}", "rb") as report:
+          attachment.set_payload(report.read())
+
+      encoders.encode_base64(attachment)
+
+      attachment.add_header(
+          "Content-Disposition",
+          "attachment; filename= estadocuenta1.infonacot.gob.mx!example.com!1536853302!1536939702!2940.xml.zip",
+      )
+
+      message.attach(attachment)
+      text = message.as_string()
+
+      with smtplib.SMTP('localhost') as server:
+          server.sendmail(sender_email, receiver_email, text)
+          server.quit()
+    '';
+in
+{
+  localMail = makeTest
+    {
+      name = "parsedmarc-local-mail";
+      meta = with lib.maintainers; {
+        maintainers = [ talyz ];
+      };
+
+      nodes.parsedmarc =
+        { nodes, ... }:
+        {
+          virtualisation.memorySize = 2048;
+
+          services.postfix = {
+            enableSubmission = true;
+            enableSubmissions = true;
+            submissionsOptions = {
+              smtpd_sasl_auth_enable = "yes";
+              smtpd_client_restrictions = "permit";
+            };
+          };
+
+          services.parsedmarc = {
+            enable = true;
+            provision = {
+              geoIp = false;
+              localMail = {
+                enable = true;
+                hostname = "localhost";
+              };
+            };
+          };
+
+          services.elasticsearch.package = pkgs.elasticsearch-oss;
+
+          environment.systemPackages = [
+            (sendEmail "dmarc@localhost")
+            pkgs.jq
+          ];
+        };
+
+      testScript = { nodes }:
+        let
+          esPort = toString nodes.parsedmarc.config.services.elasticsearch.port;
+          valueObject = lib.optionalString (lib.versionAtLeast nodes.parsedmarc.config.services.elasticsearch.package.version "7") ".value";
+        in ''
+          parsedmarc.start()
+          parsedmarc.wait_for_unit("postfix.service")
+          parsedmarc.wait_for_unit("dovecot2.service")
+          parsedmarc.wait_for_unit("parsedmarc.service")
+          parsedmarc.wait_until_succeeds(
+              "curl -sS -f http://localhost:${esPort}"
+          )
+
+          parsedmarc.fail(
+              "curl -sS -f http://localhost:${esPort}/_search?q=report_id:2940"
+              + " | tee /dev/console"
+              + " | jq -es 'if . == [] then null else .[] | .hits.total${valueObject} > 0 end'"
+          )
+          parsedmarc.succeed("send-email")
+          parsedmarc.wait_until_succeeds(
+              "curl -sS -f http://localhost:${esPort}/_search?q=report_id:2940"
+              + " | tee /dev/console"
+              + " | jq -es 'if . == [] then null else .[] | .hits.total${valueObject} > 0 end'"
+          )
+        '';
+    };
+
+  externalMail =
+    let
+      certs = import ../common/acme/server/snakeoil-certs.nix;
+      mailDomain = certs.domain;
+      parsedmarcDomain = "parsedmarc.fake.domain";
+    in
+      makeTest {
+        name = "parsedmarc-external-mail";
+        meta = with lib.maintainers; {
+          maintainers = [ talyz ];
+        };
+
+        nodes = {
+          parsedmarc =
+            { nodes, ... }:
+            {
+              virtualisation.memorySize = 2048;
+
+              security.pki.certificateFiles = [
+                certs.ca.cert
+              ];
+
+              networking.extraHosts = ''
+                127.0.0.1 ${parsedmarcDomain}
+                ${nodes.mail.config.networking.primaryIPAddress} ${mailDomain}
+              '';
+
+              services.parsedmarc = {
+                enable = true;
+                provision.geoIp = false;
+                settings.imap = {
+                  host = mailDomain;
+                  port = 993;
+                  ssl = true;
+                  user = "alice";
+                  password = "${pkgs.writeText "imap-password" "foobar"}";
+                  watch = true;
+                };
+              };
+
+              services.elasticsearch.package = pkgs.elasticsearch-oss;
+
+              environment.systemPackages = [
+                pkgs.jq
+              ];
+            };
+
+          mail =
+            { nodes, ... }:
+            {
+              imports = [ ../common/user-account.nix ];
+
+              networking.extraHosts = ''
+                127.0.0.1 ${mailDomain}
+                ${nodes.parsedmarc.config.networking.primaryIPAddress} ${parsedmarcDomain}
+              '';
+
+              services.dovecot2 = {
+                enable = true;
+                protocols = [ "imap" ];
+                sslCACert = "${certs.ca.cert}";
+                sslServerCert = "${certs.${mailDomain}.cert}";
+                sslServerKey = "${certs.${mailDomain}.key}";
+              };
+
+              services.postfix = {
+                enable = true;
+                origin = mailDomain;
+                config = {
+                  myhostname = mailDomain;
+                  mydestination = mailDomain;
+                };
+                enableSubmission = true;
+                enableSubmissions = true;
+                submissionsOptions = {
+                  smtpd_sasl_auth_enable = "yes";
+                  smtpd_client_restrictions = "permit";
+                };
+              };
+              environment.systemPackages = [ (sendEmail "alice@${mailDomain}") ];
+
+              networking.firewall.allowedTCPPorts = [ 993 ];
+            };
+        };
+
+        testScript = { nodes }:
+          let
+            esPort = toString nodes.parsedmarc.config.services.elasticsearch.port;
+            valueObject = lib.optionalString (lib.versionAtLeast nodes.parsedmarc.config.services.elasticsearch.package.version "7") ".value";
+          in ''
+            mail.start()
+            mail.wait_for_unit("postfix.service")
+            mail.wait_for_unit("dovecot2.service")
+
+            parsedmarc.start()
+            parsedmarc.wait_for_unit("parsedmarc.service")
+            parsedmarc.wait_until_succeeds(
+                "curl -sS -f http://localhost:${esPort}"
+            )
+
+            parsedmarc.fail(
+                "curl -sS -f http://localhost:${esPort}/_search?q=report_id:2940"
+                + " | tee /dev/console"
+                + " | jq -es 'if . == [] then null else .[] | .hits.total${valueObject} > 0 end'"
+            )
+            mail.succeed("send-email")
+            parsedmarc.wait_until_succeeds(
+                "curl -sS -f http://localhost:${esPort}/_search?q=report_id:2940"
+                + " | tee /dev/console"
+                + " | jq -es 'if . == [] then null else .[] | .hits.total${valueObject} > 0 end'"
+            )
+          '';
+      };
+}
diff --git a/nixos/tests/pdns-recursor.nix b/nixos/tests/pdns-recursor.nix
new file mode 100644
index 00000000000..de1b60e0b1c
--- /dev/null
+++ b/nixos/tests/pdns-recursor.nix
@@ -0,0 +1,12 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "powerdns";
+
+  nodes.server = { ... }: {
+    services.pdns-recursor.enable = true;
+  };
+
+  testScript = ''
+    server.wait_for_unit("pdns-recursor")
+    server.wait_for_open_port("53")
+  '';
+})
diff --git a/nixos/tests/peerflix.nix b/nixos/tests/peerflix.nix
new file mode 100644
index 00000000000..4800413783b
--- /dev/null
+++ b/nixos/tests/peerflix.nix
@@ -0,0 +1,23 @@
+# This test runs peerflix and checks if peerflix starts
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "peerflix";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ offline ];
+  };
+
+  nodes = {
+    peerflix =
+      { ... }:
+        {
+          services.peerflix.enable = true;
+        };
+    };
+
+  testScript = ''
+    start_all()
+
+    peerflix.wait_for_unit("peerflix.service")
+    peerflix.wait_until_succeeds("curl -f localhost:9000")
+  '';
+})
diff --git a/nixos/tests/pgadmin4-standalone.nix b/nixos/tests/pgadmin4-standalone.nix
new file mode 100644
index 00000000000..442570c5306
--- /dev/null
+++ b/nixos/tests/pgadmin4-standalone.nix
@@ -0,0 +1,43 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+  # This is seperate from pgadmin4 since we don't want both running at once
+
+  {
+    name = "pgadmin4-standalone";
+    meta.maintainers = with lib.maintainers; [ mkg20001 ];
+
+    nodes.machine = { pkgs, ... }: {
+      environment.systemPackages = with pkgs; [
+        curl
+      ];
+
+      services.postgresql = {
+        enable = true;
+
+        authentication = ''
+          host    all             all             localhost               trust
+        '';
+
+        ensureUsers = [
+          {
+            name = "postgres";
+            ensurePermissions = {
+              "DATABASE \"postgres\"" = "ALL PRIVILEGES";
+            };
+          }
+        ];
+      };
+
+      services.pgadmin = {
+        enable = true;
+        initialEmail = "bruh@localhost.de";
+        initialPasswordFile = pkgs.writeText "pw" "bruh2012!";
+      };
+    };
+
+    testScript = ''
+      machine.wait_for_unit("postgresql")
+      machine.wait_for_unit("pgadmin")
+
+      machine.wait_until_succeeds("curl -s localhost:5050")
+    '';
+  })
diff --git a/nixos/tests/pgadmin4.nix b/nixos/tests/pgadmin4.nix
new file mode 100644
index 00000000000..658315d3ac0
--- /dev/null
+++ b/nixos/tests/pgadmin4.nix
@@ -0,0 +1,142 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+  let
+    pgadmin4SrcDir = "/pgadmin";
+    pgadmin4Dir = "/var/lib/pgadmin";
+    pgadmin4LogDir = "/var/log/pgadmin";
+
+    python-with-needed-packages = pkgs.python3.withPackages (ps: with ps; [
+      selenium
+      testtools
+      testscenarios
+      flask
+      flask-babelex
+      flask-babel
+      flask-gravatar
+      flask_login
+      flask_mail
+      flask_migrate
+      flask_sqlalchemy
+      flask_wtf
+      flask-compress
+      passlib
+      pytz
+      simplejson
+      six
+      sqlparse
+      wtforms
+      flask-paranoid
+      psutil
+      psycopg2
+      python-dateutil
+      sqlalchemy
+      itsdangerous
+      flask-security-too
+      bcrypt
+      cryptography
+      sshtunnel
+      ldap3
+      gssapi
+      flask-socketio
+      eventlet
+      httpagentparser
+      user-agents
+      wheel
+      authlib
+      qrcode
+      pillow
+      pyotp
+    ]);
+  in
+  {
+    name = "pgadmin4";
+    meta.maintainers = with lib.maintainers; [ gador ];
+
+    nodes.machine = { pkgs, ... }: {
+      imports = [ ./common/x11.nix ];
+      environment.systemPackages = with pkgs; [
+        pgadmin4
+        postgresql
+        python-with-needed-packages
+        chromedriver
+        chromium
+      ];
+      services.postgresql = {
+        enable = true;
+        authentication = ''
+          host    all             all             localhost               trust
+        '';
+        ensureUsers = [
+          {
+            name = "postgres";
+            ensurePermissions = {
+              "DATABASE \"postgres\"" = "ALL PRIVILEGES";
+            };
+          }
+        ];
+      };
+    };
+
+    testScript = ''
+      machine.wait_for_unit("postgresql")
+
+      # pgadmin4 needs its data and log directories
+      machine.succeed(
+          "mkdir -p ${pgadmin4Dir} \
+          && mkdir -p ${pgadmin4LogDir} \
+          && mkdir -p ${pgadmin4SrcDir}"
+      )
+
+      machine.succeed(
+           "tar xvzf ${pkgs.pgadmin4.src} -C ${pgadmin4SrcDir}"
+      )
+
+      machine.wait_for_file("${pgadmin4SrcDir}/pgadmin4-${pkgs.pgadmin4.version}/README.md")
+
+      # set paths and config for tests
+      machine.succeed(
+           "cd ${pgadmin4SrcDir}/pgadmin4-${pkgs.pgadmin4.version} \
+           && cp -v web/regression/test_config.json.in web/regression/test_config.json \
+           && sed -i 's|PostgreSQL 9.4|PostgreSQL|' web/regression/test_config.json \
+           && sed -i 's|/opt/PostgreSQL/9.4/bin/|${pkgs.postgresql}/bin|' web/regression/test_config.json \
+           && sed -i 's|\"headless_chrome\": false|\"headless_chrome\": true|' web/regression/test_config.json"
+      )
+
+      # adapt chrome config to run within a sandbox without GUI
+      # see https://stackoverflow.com/questions/50642308/webdriverexception-unknown-error-devtoolsactiveport-file-doesnt-exist-while-t#50642913
+      # add chrome binary path. use spaces to satisfy python indention (tabs throw an error)
+      # this works for selenium 3 (currently used), but will need to be updated
+      # to work with "from selenium.webdriver.chrome.service import Service" in selenium 4
+      machine.succeed(
+           "cd ${pgadmin4SrcDir}/pgadmin4-${pkgs.pgadmin4.version} \
+           && sed -i '\|options.add_argument(\"--disable-infobars\")|a \ \ \ \ \ \ \ \ options.binary_location = \"${pkgs.chromium}/bin/chromium\"' web/regression/runtests.py \
+           && sed -i '\|options.add_argument(\"--no-sandbox\")|a \ \ \ \ \ \ \ \ options.add_argument(\"--headless\")' web/regression/runtests.py \
+           && sed -i '\|options.add_argument(\"--disable-infobars\")|a \ \ \ \ \ \ \ \ options.add_argument(\"--disable-dev-shm-usage\")' web/regression/runtests.py \
+           && sed -i 's|(chrome_options=options)|(executable_path=\"${pkgs.chromedriver}/bin/chromedriver\", chrome_options=options)|' web/regression/runtests.py \
+           && sed -i 's|driver_local.maximize_window()||' web/regression/runtests.py"
+      )
+
+      # don't bother to test LDAP authentification
+      with subtest("run browser test"):
+          machine.succeed(
+               'cd ${pgadmin4SrcDir}/pgadmin4-${pkgs.pgadmin4.version}/web \
+               && ${python-with-needed-packages.interpreter} regression/runtests.py --pkg browser --exclude \
+               browser.tests.test_ldap_login.LDAPLoginTestCase,browser.tests.test_ldap_login'
+          )
+
+      # 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-with-needed-packages.interpreter} regression/runtests.py --pkg feature_tests'
+          )
+
+      with subtest("run resql test"):
+          machine.succeed(
+               'cd ${pgadmin4SrcDir}/pgadmin4-${pkgs.pgadmin4.version}/web \
+               && ${python-with-needed-packages.interpreter} regression/runtests.py --pkg resql'
+          )
+    '';
+  })
diff --git a/nixos/tests/pgjwt.nix b/nixos/tests/pgjwt.nix
new file mode 100644
index 00000000000..4793a3e3150
--- /dev/null
+++ b/nixos/tests/pgjwt.nix
@@ -0,0 +1,34 @@
+import ./make-test-python.nix ({ pkgs, lib, ...}:
+
+with pkgs; {
+  name = "pgjwt";
+  meta = with lib.maintainers; {
+    maintainers = [ spinus willibutz ];
+  };
+
+  nodes = {
+    master = { ... }:
+    {
+      services.postgresql = {
+        enable = true;
+        extraPlugins = [ pgjwt pgtap ];
+      };
+    };
+  };
+
+  testScript = { nodes, ... }:
+  let
+    sqlSU = "${nodes.master.config.services.postgresql.superUser}";
+    pgProve = "${pkgs.perlPackages.TAPParserSourceHandlerpgTAP}";
+  in
+  ''
+    start_all()
+    master.wait_for_unit("postgresql")
+    master.succeed(
+        "${pkgs.gnused}/bin/sed -e '12 i CREATE EXTENSION pgcrypto;\\nCREATE EXTENSION pgtap;\\nSET search_path TO tap,public;' ${pgjwt.src}/test.sql > /tmp/test.sql"
+    )
+    master.succeed(
+        "${pkgs.sudo}/bin/sudo -u ${sqlSU} PGOPTIONS=--search_path=tap,public ${pgProve}/bin/pg_prove -d postgres -v -f /tmp/test.sql"
+    )
+  '';
+})
diff --git a/nixos/tests/pgmanage.nix b/nixos/tests/pgmanage.nix
new file mode 100644
index 00000000000..6f8f2f96534
--- /dev/null
+++ b/nixos/tests/pgmanage.nix
@@ -0,0 +1,41 @@
+import ./make-test-python.nix ({ pkgs, ... } :
+let
+  role     = "test";
+  password = "secret";
+  conn     = "local";
+in
+{
+  name = "pgmanage";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ basvandijk ];
+  };
+  nodes = {
+    one = { config, pkgs, ... }: {
+      services = {
+        postgresql = {
+          enable = true;
+          initialScript = pkgs.writeText "pg-init-script" ''
+            CREATE ROLE ${role} SUPERUSER LOGIN PASSWORD '${password}';
+          '';
+        };
+        pgmanage = {
+          enable = true;
+          connections = {
+            ${conn} = "hostaddr=127.0.0.1 port=${toString config.services.postgresql.port} dbname=postgres";
+          };
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    one.wait_for_unit("default.target")
+    one.require_unit_state("pgmanage.service", "active")
+
+    # Test if we can log in.
+    one.wait_until_succeeds(
+        "curl 'http://localhost:8080/pgmanage/auth' --data 'action=login&connname=${conn}&username=${role}&password=${password}' --fail"
+    )
+  '';
+})
diff --git a/nixos/tests/php/default.nix b/nixos/tests/php/default.nix
new file mode 100644
index 00000000000..c0386385753
--- /dev/null
+++ b/nixos/tests/php/default.nix
@@ -0,0 +1,16 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../../.. { inherit system config; }
+, php ? pkgs.php
+}:
+
+let
+  php' = php.buildEnv {
+    extensions = { enabled, all }: with all; enabled ++ [ apcu ];
+  };
+in
+{
+  fpm = import ./fpm.nix { inherit system pkgs; php = php'; };
+  httpd = import ./httpd.nix { inherit system pkgs; php = php'; };
+  pcre = import ./pcre.nix { inherit system pkgs; php = php'; };
+}
diff --git a/nixos/tests/php/fpm.nix b/nixos/tests/php/fpm.nix
new file mode 100644
index 00000000000..718a635a6c7
--- /dev/null
+++ b/nixos/tests/php/fpm.nix
@@ -0,0 +1,59 @@
+import ../make-test-python.nix ({ pkgs, lib, php, ... }: {
+  name = "php-${php.version}-fpm-nginx-test";
+  meta.maintainers = lib.teams.php.members;
+
+  machine = { config, lib, pkgs, ... }: {
+    environment.systemPackages = [ php ];
+
+    services.nginx = {
+      enable = true;
+
+      virtualHosts."phpfpm" =
+        let
+          testdir = pkgs.writeTextDir "web/index.php" "<?php phpinfo();";
+        in
+        {
+          root = "${testdir}/web";
+          locations."~ \\.php$".extraConfig = ''
+            fastcgi_pass unix:${config.services.phpfpm.pools.foobar.socket};
+            fastcgi_index index.php;
+            include ${config.services.nginx.package}/conf/fastcgi_params;
+            include ${pkgs.nginx}/conf/fastcgi.conf;
+          '';
+          locations."/" = {
+            tryFiles = "$uri $uri/ index.php";
+            index = "index.php index.html index.htm";
+          };
+        };
+    };
+
+    services.phpfpm.pools."foobar" = {
+      user = "nginx";
+      phpPackage = php;
+      settings = {
+        "listen.group" = "nginx";
+        "listen.mode" = "0600";
+        "listen.owner" = "nginx";
+        "pm" = "dynamic";
+        "pm.max_children" = 5;
+        "pm.max_requests" = 500;
+        "pm.max_spare_servers" = 3;
+        "pm.min_spare_servers" = 1;
+        "pm.start_servers" = 2;
+      };
+    };
+  };
+  testScript = { ... }: ''
+    machine.wait_for_unit("nginx.service")
+    machine.wait_for_unit("phpfpm-foobar.service")
+
+    # Check so we get an evaluated PHP back
+    response = machine.succeed("curl -fvvv -s http://127.0.0.1:80/")
+    assert "PHP Version ${php.version}" in response, "PHP version not detected"
+
+    # Check so we have database and some other extensions loaded
+    for ext in ["json", "opcache", "pdo_mysql", "pdo_pgsql", "pdo_sqlite", "apcu"]:
+        assert ext in response, f"Missing {ext} extension"
+        machine.succeed(f'test -n "$(php -m | grep -i {ext})"')
+  '';
+})
diff --git a/nixos/tests/php/httpd.nix b/nixos/tests/php/httpd.nix
new file mode 100644
index 00000000000..36d90e72d7d
--- /dev/null
+++ b/nixos/tests/php/httpd.nix
@@ -0,0 +1,34 @@
+import ../make-test-python.nix ({ pkgs, lib, php, ... }: {
+  name = "php-${php.version}-httpd-test";
+  meta.maintainers = lib.teams.php.members;
+
+  machine = { config, lib, pkgs, ... }: {
+    services.httpd = {
+      enable = true;
+      adminAddr = "admin@phpfpm";
+      virtualHosts."phpfpm" =
+        let
+          testdir = pkgs.writeTextDir "web/index.php" "<?php phpinfo();";
+        in
+        {
+          documentRoot = "${testdir}/web";
+          locations."/" = {
+            index = "index.php index.html";
+          };
+        };
+      phpPackage = php;
+      enablePHP = true;
+    };
+  };
+  testScript = { ... }: ''
+    machine.wait_for_unit("httpd.service")
+
+    # Check so we get an evaluated PHP back
+    response = machine.succeed("curl -fvvv -s http://127.0.0.1:80/")
+    assert "PHP Version ${php.version}" in response, "PHP version not detected"
+
+    # Check so we have database and some other extensions loaded
+    for ext in ["json", "opcache", "pdo_mysql", "pdo_pgsql", "pdo_sqlite"]:
+        assert ext in response, f"Missing {ext} extension"
+  '';
+})
diff --git a/nixos/tests/php/pcre.nix b/nixos/tests/php/pcre.nix
new file mode 100644
index 00000000000..917184b975e
--- /dev/null
+++ b/nixos/tests/php/pcre.nix
@@ -0,0 +1,42 @@
+let
+  testString = "can-use-subgroups";
+in
+import ../make-test-python.nix ({ lib, php, ... }: {
+  name = "php-${php.version}-httpd-pcre-jit-test";
+  meta.maintainers = lib.teams.php.members;
+
+  machine = { lib, pkgs, ... }: {
+    time.timeZone = "UTC";
+    services.httpd = {
+      enable = true;
+      adminAddr = "please@dont.contact";
+      phpPackage = php;
+      enablePHP = true;
+      phpOptions = "pcre.jit = true";
+      extraConfig =
+        let
+          testRoot = pkgs.writeText "index.php"
+            ''
+              <?php
+              preg_match('/(${testString})/', '${testString}', $result);
+              var_dump($result);
+            '';
+        in
+        ''
+          Alias / ${testRoot}/
+
+          <Directory ${testRoot}>
+            Require all granted
+          </Directory>
+        '';
+    };
+  };
+  testScript = { ... }:
+    ''
+      machine.wait_for_unit("httpd.service")
+      # Ensure php evaluation by matching on the var_dump syntax
+      response = machine.succeed("curl -fvvv -s http://127.0.0.1:80/index.php")
+      expected = 'string(${toString (builtins.stringLength testString)}) "${testString}"'
+      assert expected in response, "Does not appear to be able to use subgroups."
+    '';
+})
diff --git a/nixos/tests/pict-rs.nix b/nixos/tests/pict-rs.nix
new file mode 100644
index 00000000000..432fd6a50cc
--- /dev/null
+++ b/nixos/tests/pict-rs.nix
@@ -0,0 +1,17 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+  {
+    name = "pict-rs";
+    meta.maintainers = with lib.maintainers; [ happysalada ];
+
+    machine = { ... }: {
+      environment.systemPackages = with pkgs; [ curl jq ];
+      services.pict-rs.enable = true;
+    };
+
+    testScript = ''
+      start_all()
+
+      machine.wait_for_unit("pict-rs")
+      machine.wait_for_open_port("8080")
+    '';
+  })
diff --git a/nixos/tests/pinnwand.nix b/nixos/tests/pinnwand.nix
new file mode 100644
index 00000000000..0391c413311
--- /dev/null
+++ b/nixos/tests/pinnwand.nix
@@ -0,0 +1,94 @@
+import ./make-test-python.nix ({ pkgs, ...}:
+let
+  pythonEnv = pkgs.python3.withPackages (py: with py; [ appdirs toml ]);
+
+  port = 8000;
+  baseUrl = "http://server:${toString port}";
+
+  configureSteck = pkgs.writeScript "configure.py" ''
+    #!${pythonEnv.interpreter}
+    import appdirs
+    import toml
+    import os
+
+    CONFIG = {
+      "base": "${baseUrl}/",
+      "confirm": False,
+      "magic": True,
+      "ignore": True
+    }
+
+    os.makedirs(appdirs.user_config_dir('steck'))
+    with open(os.path.join(appdirs.user_config_dir('steck'), 'steck.toml'), "w") as fd:
+        toml.dump(CONFIG, fd)
+    '';
+in
+{
+  name = "pinnwand";
+  meta = with pkgs.lib.maintainers; {
+    maintainers =[ hexa ];
+  };
+
+  nodes = {
+    server = { config, ... }:
+    {
+      networking.firewall.allowedTCPPorts = [
+        port
+      ];
+
+      services.pinnwand = {
+        enable = true;
+        port = port;
+      };
+    };
+
+    client = { pkgs, ... }:
+    {
+      environment.systemPackages = [ pkgs.steck ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    server.wait_for_unit("pinnwand.service")
+    client.wait_for_unit("network.target")
+
+    # create steck.toml config file
+    client.succeed("${configureSteck}")
+
+    # wait until the server running pinnwand is reachable
+    client.wait_until_succeeds("ping -c1 server")
+
+    # make sure pinnwand is listening
+    server.wait_for_open_port(${toString port})
+
+    # send the contents of /etc/machine-id
+    response = client.succeed("steck paste /etc/machine-id")
+
+    # parse the steck response
+    raw_url = None
+    removal_link = None
+    for line in response.split("\n"):
+        if line.startswith("View link:"):
+            raw_url = f"${baseUrl}/raw/{line.split('/')[-1]}"
+        if line.startswith("Removal link:"):
+            removal_link = line.split(":", 1)[1]
+
+
+    # start the reaper, it shouldn't do anything meaningful here
+    server.systemctl("start pinnwand-reaper.service")
+    server.wait_until_fails("systemctl is-active -q pinnwand-reaper.service")
+    server.log(server.execute("journalctl -u pinnwand-reaper -e --no-pager")[1])
+
+    # check whether paste matches what we sent
+    client.succeed(f"curl {raw_url} > /tmp/machine-id")
+    client.succeed("diff /tmp/machine-id /etc/machine-id")
+
+    # remove paste and check that it's not available any more
+    client.succeed(f"curl {removal_link}")
+    client.fail(f"curl --fail {raw_url}")
+
+    server.log(server.succeed("systemd-analyze security pinnwand"))
+  '';
+})
diff --git a/nixos/tests/plasma5-systemd-start.nix b/nixos/tests/plasma5-systemd-start.nix
new file mode 100644
index 00000000000..72de19af70c
--- /dev/null
+++ b/nixos/tests/plasma5-systemd-start.nix
@@ -0,0 +1,42 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+{
+  name = "plasma5-systemd-start";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ oxalica ];
+  };
+
+  machine = { ... }:
+
+  {
+    imports = [ ./common/user-account.nix ];
+    services.xserver = {
+      enable = true;
+      displayManager.sddm.enable = true;
+      displayManager.defaultSession = "plasma";
+      desktopManager.plasma5.enable = true;
+      desktopManager.plasma5.runUsingSystemd = true;
+      displayManager.autoLogin = {
+        enable = true;
+        user = "alice";
+      };
+    };
+  };
+
+  testScript = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+  in ''
+    with subtest("Wait for login"):
+        start_all()
+        machine.wait_for_file("${user.home}/.Xauthority")
+        machine.succeed("xauth merge ${user.home}/.Xauthority")
+
+    with subtest("Check plasmashell started"):
+        machine.wait_until_succeeds("pgrep plasmashell")
+        machine.wait_for_window("^Desktop ")
+
+    status, result = machine.systemctl('--no-pager show plasma-plasmashell.service', user='alice')
+    assert status == 0, 'Service not found'
+    assert 'ActiveState=active' in result.split('\n'), 'Systemd service not active'
+  '';
+})
diff --git a/nixos/tests/plasma5.nix b/nixos/tests/plasma5.nix
new file mode 100644
index 00000000000..5c7ea602f79
--- /dev/null
+++ b/nixos/tests/plasma5.nix
@@ -0,0 +1,61 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+{
+  name = "plasma5";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ttuegel ];
+  };
+
+  machine = { ... }:
+
+  {
+    imports = [ ./common/user-account.nix ];
+    services.xserver.enable = true;
+    services.xserver.displayManager.sddm.enable = true;
+    services.xserver.displayManager.defaultSession = "plasma";
+    services.xserver.desktopManager.plasma5.enable = true;
+    services.xserver.displayManager.autoLogin = {
+      enable = true;
+      user = "alice";
+    };
+    hardware.pulseaudio.enable = true; # needed for the factl test, /dev/snd/* exists without them but udev doesn't care then
+  };
+
+  testScript = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+    xdo = "${pkgs.xdotool}/bin/xdotool";
+  in ''
+    with subtest("Wait for login"):
+        start_all()
+        machine.wait_for_file("${user.home}/.Xauthority")
+        machine.succeed("xauth merge ${user.home}/.Xauthority")
+
+    with subtest("Check plasmashell started"):
+        machine.wait_until_succeeds("pgrep plasmashell")
+        machine.wait_for_window("^Desktop ")
+
+    with subtest("Check that KDED is running"):
+        machine.succeed("pgrep kded5")
+
+    with subtest("Check that logging in has given the user ownership of devices"):
+        machine.succeed("getfacl -p /dev/snd/timer | grep -q ${user.name}")
+
+    with subtest("Run Dolphin"):
+        machine.execute("su - ${user.name} -c 'DISPLAY=:0.0 dolphin >&2 &'")
+        machine.wait_for_window(" Dolphin")
+
+    with subtest("Run Konsole"):
+        machine.execute("su - ${user.name} -c 'DISPLAY=:0.0 konsole >&2 &'")
+        machine.wait_for_window("Konsole")
+
+    with subtest("Run systemsettings"):
+        machine.execute("su - ${user.name} -c 'DISPLAY=:0.0 systemsettings5 >&2 &'")
+        machine.wait_for_window("Settings")
+
+    with subtest("Wait to get a screenshot"):
+        machine.execute(
+            "${xdo} key Alt+F1 sleep 10"
+        )
+        machine.screenshot("screen")
+  '';
+})
diff --git a/nixos/tests/plausible.nix b/nixos/tests/plausible.nix
new file mode 100644
index 00000000000..58c1dd5cf4a
--- /dev/null
+++ b/nixos/tests/plausible.nix
@@ -0,0 +1,49 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "plausible";
+  meta = with lib.maintainers; {
+    maintainers = [ ma27 ];
+  };
+
+  machine = { pkgs, ... }: {
+    virtualisation.memorySize = 4096;
+    services.plausible = {
+      enable = true;
+      releaseCookiePath = "${pkgs.runCommand "cookie" { } ''
+        ${pkgs.openssl}/bin/openssl rand -base64 64 >"$out"
+      ''}";
+      adminUser = {
+        email = "admin@example.org";
+        passwordFile = "${pkgs.writeText "pwd" "foobar"}";
+        activate = true;
+      };
+      server = {
+        baseUrl = "http://localhost:8000";
+        secretKeybaseFile = "${pkgs.writeText "dont-try-this-at-home" "nannannannannannannannannannannannannannannannannannannan_batman!"}";
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("plausible.service")
+    machine.wait_for_open_port(8000)
+
+    machine.succeed("curl -f localhost:8000 >&2")
+
+    csrf_token = machine.succeed(
+        "curl -c /tmp/cookies localhost:8000/login | grep '_csrf_token' | sed -E 's,.*value=\"(.*)\".*,\\1,g'"
+    )
+
+    machine.succeed(
+        f"curl -b /tmp/cookies -f -X POST localhost:8000/login -F email=admin@example.org -F password=foobar -F _csrf_token={csrf_token.strip()} -D headers"
+    )
+
+    # By ensuring that the user is redirected to the dashboard after login, we
+    # also make sure that the automatic verification of the module works.
+    machine.succeed(
+        "[[ $(grep 'location: ' headers | cut -d: -f2- | xargs echo) == /sites* ]]"
+    )
+
+    machine.shutdown()
+  '';
+})
diff --git a/nixos/tests/pleroma.nix b/nixos/tests/pleroma.nix
new file mode 100644
index 00000000000..90a9a251104
--- /dev/null
+++ b/nixos/tests/pleroma.nix
@@ -0,0 +1,249 @@
+/*
+  Pleroma E2E VM test.
+
+  Abstract:
+  =========
+  Using pleroma, postgresql, a local CA cert, a nginx reverse proxy
+  and a toot-based client, we're going to:
+
+  1. Provision a pleroma service from scratch (pleroma config + postgres db).
+  2. Create a "jamy" admin user.
+  3. Send a toot from this user.
+  4. Send a upload from this user.
+  5. Check the toot is part of the server public timeline
+
+  Notes:
+  - We need a fully functional TLS setup without having any access to
+    the internet. We do that by issuing a self-signed cert, add this
+    self-cert to the hosts pki trust store and finally spoof the
+    hostnames using /etc/hosts.
+  - For this NixOS test, we *had* to store some DB-related and
+    pleroma-related secrets to the store. Keep in mind the store is
+    world-readable, it's the worst place possible to store *any*
+    secret. **DO NOT DO THIS IN A REAL WORLD DEPLOYMENT**.
+*/
+
+import ./make-test-python.nix ({ pkgs, ... }:
+  let
+  send-toot = pkgs.writeScriptBin "send-toot" ''
+    set -eux
+    # toot is using the requests library internally. This library
+    # sadly embed its own certificate store instead of relying on the
+    # system one. Overriding this pretty bad default behaviour.
+    export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
+
+    echo "jamy-password" | toot login_cli -i "pleroma.nixos.test" -e "jamy@nixos.test"
+    echo "Login OK"
+
+    # Send a toot then verify it's part of the public timeline
+    echo "y" | toot post "hello world Jamy here"
+    echo "Send toot OK"
+    echo "y" | toot timeline | grep -c "hello world Jamy here"
+    echo "Get toot from timeline OK"
+
+    # Test file upload
+    echo "y" | toot upload ${db-seed} | grep -c "https://pleroma.nixos.test/media"
+    echo "File upload OK"
+
+    echo "====================================================="
+    echo "=                   SUCCESS                         ="
+    echo "=                                                   ="
+    echo "=    We were able to sent a toot + a upload and     ="
+    echo "=   retrieve both of them in the public timeline.   ="
+    echo "====================================================="
+  '';
+
+  provision-db = pkgs.writeScriptBin "provision-db" ''
+    set -eux
+    sudo -u postgres psql -f ${db-seed}
+  '';
+
+  test-db-passwd = "SccZOvTGM//BMrpoQj68JJkjDkMGb4pHv2cECWiI+XhVe3uGJTLI0vFV/gDlZ5jJ";
+
+  /* For this NixOS test, we *had* to store this secret to the store.
+    Keep in mind the store is world-readable, it's the worst place
+    possible to store *any* secret. **DO NOT DO THIS IN A REAL WORLD
+    DEPLOYMENT**.*/
+  db-seed = pkgs.writeText "provision.psql" ''
+    CREATE USER pleroma WITH ENCRYPTED PASSWORD '${test-db-passwd}';
+    CREATE DATABASE pleroma OWNER pleroma;
+    \c pleroma;
+    --Extensions made by ecto.migrate that need superuser access
+    CREATE EXTENSION IF NOT EXISTS citext;
+    CREATE EXTENSION IF NOT EXISTS pg_trgm;
+    CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+  '';
+
+  pleroma-conf = ''
+    import Config
+
+    config :pleroma, Pleroma.Web.Endpoint,
+       url: [host: "pleroma.nixos.test", scheme: "https", port: 443],
+       http: [ip: {127, 0, 0, 1}, port: 4000]
+
+    config :pleroma, :instance,
+      name: "NixOS test pleroma server",
+      email: "pleroma@nixos.test",
+      notify_email: "pleroma@nixos.test",
+      limit: 5000,
+      registrations_open: true
+
+    config :pleroma, :media_proxy,
+      enabled: false,
+      redirect_on_failure: true
+      #base_url: "https://cache.pleroma.social"
+
+    config :pleroma, Pleroma.Repo,
+      adapter: Ecto.Adapters.Postgres,
+      username: "pleroma",
+      password: "${test-db-passwd}",
+      database: "pleroma",
+      hostname: "localhost",
+      pool_size: 10,
+      prepare: :named,
+      parameters: [
+        plan_cache_mode: "force_custom_plan"
+      ]
+
+    config :pleroma, :database, rum_enabled: false
+    config :pleroma, :instance, static_dir: "/var/lib/pleroma/static"
+    config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads"
+    config :pleroma, configurable_from_database: false
+  '';
+
+  /* For this NixOS test, we *had* to store this secret to the store.
+    Keep in mind the store is world-readable, it's the worst place
+    possible to store *any* secret. **DO NOT DO THIS IN A REAL WORLD
+    DEPLOYMENT**.
+    In a real-word deployment, you'd handle this either by:
+    - manually upload your pleroma secrets to /var/lib/pleroma/secrets.exs
+    - use a deployment tool such as morph or NixOps to deploy your secrets.
+  */
+  pleroma-conf-secret = pkgs.writeText "secrets.exs" ''
+    import Config
+
+    config :joken, default_signer: "PS69/wMW7X6FIQPABt9lwvlZvgrJIncfiAMrK9J5mjVus/7/NJJi1DsDA1OghBE5"
+
+    config :pleroma, Pleroma.Web.Endpoint,
+       secret_key_base: "NvfmU7lYaQrmmxt4NACm0AaAfN9t6WxsrX0NCB4awkGHvr1S7jyshlEmrjaPFhhq",
+       signing_salt: "3L41+BuJ"
+
+    config :web_push_encryption, :vapid_details,
+      subject: "mailto:pleroma@nixos.test",
+      public_key: "BKjfNX9-UqAcncaNqERQtF7n9pKrB0-MO-juv6U5E5XQr_Tg5D-f8AlRjduAguDpyAngeDzG8MdrTejMSL4VF30",
+      private_key: "k7o9onKMQrgMjMb6l4fsxSaXO0BTNAer5MVSje3q60k"
+  '';
+
+  /* For this NixOS test, we *had* to store this secret to the store.
+    Keep in mind the store is world-readable, it's the worst place
+    possible to store *any* secret. **DO NOT DO THIS IN A REAL WORLD
+    DEPLOYMENT**.
+    In a real-word deployment, you'd handle this either by:
+    - manually upload your pleroma secrets to /var/lib/pleroma/secrets.exs
+    - use a deployment tool such as morph or NixOps to deploy your secrets.
+    */
+  provision-secrets = pkgs.writeScriptBin "provision-secrets" ''
+    set -eux
+    cp "${pleroma-conf-secret}" "/var/lib/pleroma/secrets.exs"
+    chown pleroma:pleroma /var/lib/pleroma/secrets.exs
+  '';
+
+  /* For this NixOS test, we *had* to store this secret to the store.
+    Keep in mind the store is world-readable, it's the worst place
+    possible to store *any* secret. **DO NOT DO THIS IN A REAL WORLD
+    DEPLOYMENT**.
+  */
+  provision-user = pkgs.writeScriptBin "provision-user" ''
+    set -eux
+
+    # Waiting for pleroma to be up.
+    timeout 5m bash -c 'while [[ "$(curl -s -o /dev/null -w '%{http_code}' https://pleroma.nixos.test/api/v1/instance)" != "200" ]]; do sleep 2; done'
+    pleroma_ctl user new jamy jamy@nixos.test --password 'jamy-password' --moderator --admin -y
+  '';
+
+  tls-cert = pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
+    openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=pleroma.nixos.test' -days 36500
+    mkdir -p $out
+    cp key.pem cert.pem $out
+  '';
+
+  hosts = nodes: ''
+    ${nodes.pleroma.config.networking.primaryIPAddress} pleroma.nixos.test
+    ${nodes.client.config.networking.primaryIPAddress} client.nixos.test
+  '';
+  in {
+  name = "pleroma";
+  nodes = {
+    client = { nodes, pkgs, config, ... }: {
+      security.pki.certificateFiles = [ "${tls-cert}/cert.pem" ];
+      networking.extraHosts = hosts nodes;
+      environment.systemPackages = with pkgs; [
+        toot
+        send-toot
+      ];
+    };
+    pleroma = { nodes, pkgs, config, ... }: {
+      security.pki.certificateFiles = [ "${tls-cert}/cert.pem" ];
+      networking.extraHosts = hosts nodes;
+      networking.firewall.enable = false;
+      environment.systemPackages = with pkgs; [
+        provision-db
+        provision-secrets
+        provision-user
+      ];
+      services = {
+        pleroma = {
+          enable = true;
+          configs = [
+            pleroma-conf
+          ];
+        };
+        postgresql = {
+          enable = true;
+          package = pkgs.postgresql_12;
+        };
+        nginx = {
+          enable = true;
+          virtualHosts."pleroma.nixos.test" = {
+            addSSL = true;
+            sslCertificate = "${tls-cert}/cert.pem";
+            sslCertificateKey = "${tls-cert}/key.pem";
+            locations."/" = {
+              proxyPass = "http://127.0.0.1:4000";
+              extraConfig = ''
+                add_header 'Access-Control-Allow-Origin' '*' always;
+                add_header 'Access-Control-Allow-Methods' 'POST, PUT, DELETE, GET, PATCH, OPTIONS' always;
+                add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Idempotency-Key' always;
+                add_header 'Access-Control-Expose-Headers' 'Link, X-RateLimit-Reset, X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-Id' always;
+                if ($request_method = OPTIONS) {
+                    return 204;
+                }
+                add_header X-XSS-Protection "1; mode=block";
+                add_header X-Permitted-Cross-Domain-Policies none;
+                add_header X-Frame-Options DENY;
+                add_header X-Content-Type-Options nosniff;
+                add_header Referrer-Policy same-origin;
+                add_header X-Download-Options noopen;
+                proxy_http_version 1.1;
+                proxy_set_header Upgrade $http_upgrade;
+                proxy_set_header Connection "upgrade";
+                proxy_set_header Host $host;
+                client_max_body_size 16m;
+              '';
+            };
+          };
+        };
+      };
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+    pleroma.wait_for_unit("postgresql.service")
+    pleroma.succeed("provision-db")
+    pleroma.succeed("provision-secrets")
+    pleroma.systemctl("restart pleroma.service")
+    pleroma.wait_for_unit("pleroma.service")
+    pleroma.succeed("provision-user")
+    client.succeed("send-toot")
+  '';
+})
diff --git a/nixos/tests/plikd.nix b/nixos/tests/plikd.nix
new file mode 100644
index 00000000000..8fec93c01f6
--- /dev/null
+++ b/nixos/tests/plikd.nix
@@ -0,0 +1,27 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "plikd";
+  meta = with lib.maintainers; {
+    maintainers = [ freezeboy ];
+  };
+
+  machine = { pkgs, ... }: let
+  in {
+    services.plikd.enable = true;
+    environment.systemPackages = [ pkgs.plik ];
+  };
+
+  testScript = ''
+    # Service basic test
+    machine.wait_for_unit("plikd")
+
+    # Network test
+    machine.wait_for_open_port("8080")
+    machine.succeed("curl --fail -v http://localhost:8080")
+
+    # Application test
+    machine.execute("echo test > /tmp/data.txt")
+    machine.succeed("plik --server http://localhost:8080 /tmp/data.txt | grep curl")
+
+    machine.succeed("diff data.txt /tmp/data.txt")
+  '';
+})
diff --git a/nixos/tests/plotinus.nix b/nixos/tests/plotinus.nix
new file mode 100644
index 00000000000..af38b41813b
--- /dev/null
+++ b/nixos/tests/plotinus.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "plotinus";
+  meta = {
+    maintainers = pkgs.plotinus.meta.maintainers;
+  };
+
+  machine =
+    { pkgs, ... }:
+
+    { imports = [ ./common/x11.nix ];
+      programs.plotinus.enable = true;
+      environment.systemPackages = [ pkgs.gnome.gnome-calculator pkgs.xdotool ];
+    };
+
+  testScript = ''
+    machine.wait_for_x()
+    machine.succeed("gnome-calculator >&2 &")
+    machine.wait_for_window("gnome-calculator")
+    machine.succeed(
+        "xdotool search --sync --onlyvisible --class gnome-calculator "
+        + "windowfocus --sync key --clearmodifiers --delay 1 'ctrl+shift+p'"
+    )
+    machine.sleep(5)  # wait for the popup
+    machine.succeed("xdotool key --delay 100 p r e f e r e n c e s Return")
+    machine.wait_for_window("Preferences")
+    machine.screenshot("screen")
+  '';
+})
diff --git a/nixos/tests/podgrab.nix b/nixos/tests/podgrab.nix
new file mode 100644
index 00000000000..e927e25fea5
--- /dev/null
+++ b/nixos/tests/podgrab.nix
@@ -0,0 +1,34 @@
+let
+  defaultPort = 8080;
+  customPort = 4242;
+in
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "podgrab";
+
+  nodes = {
+    default = { ... }: {
+      services.podgrab.enable = true;
+    };
+
+    customized = { ... }: {
+      services.podgrab = {
+        enable = true;
+        port = customPort;
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    default.wait_for_unit("podgrab")
+    default.wait_for_open_port("${toString defaultPort}")
+    default.succeed("curl --fail http://localhost:${toString defaultPort}")
+
+    customized.wait_for_unit("podgrab")
+    customized.wait_for_open_port("${toString customPort}")
+    customized.succeed("curl --fail http://localhost:${toString customPort}")
+  '';
+
+  meta.maintainers = with pkgs.lib.maintainers; [ ambroisie ];
+})
diff --git a/nixos/tests/podman/default.nix b/nixos/tests/podman/default.nix
new file mode 100644
index 00000000000..67c7823c5a3
--- /dev/null
+++ b/nixos/tests/podman/default.nix
@@ -0,0 +1,144 @@
+# This test runs podman and checks if simple container starts
+
+import ../make-test-python.nix (
+  { pkgs, lib, ... }: {
+    name = "podman";
+    meta = {
+      maintainers = lib.teams.podman.members;
+    };
+
+    nodes = {
+      podman =
+        { pkgs, ... }:
+        {
+          virtualisation.podman.enable = true;
+
+          # To test docker socket support
+          virtualisation.podman.dockerSocket.enable = true;
+          environment.systemPackages = [
+            pkgs.docker-client
+          ];
+
+          users.users.alice = {
+            isNormalUser = true;
+            home = "/home/alice";
+            description = "Alice Foobar";
+            extraGroups = [ "podman" ];
+          };
+
+          users.users.mallory = {
+            isNormalUser = true;
+            home = "/home/mallory";
+            description = "Mallory Foobar";
+          };
+
+        };
+    };
+
+    testScript = ''
+      import shlex
+
+
+      def su_cmd(cmd, user = "alice"):
+          cmd = shlex.quote(cmd)
+          return f"su {user} -l -c {cmd}"
+
+
+      podman.wait_for_unit("sockets.target")
+      start_all()
+
+      with subtest("Run container as root with runc"):
+          podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
+          podman.succeed(
+              "podman run --runtime=runc -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+          )
+          podman.succeed("podman ps | grep sleeping")
+          podman.succeed("podman stop sleeping")
+          podman.succeed("podman rm sleeping")
+
+      with subtest("Run container as root with crun"):
+          podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
+          podman.succeed(
+              "podman run --runtime=crun -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+          )
+          podman.succeed("podman ps | grep sleeping")
+          podman.succeed("podman stop sleeping")
+          podman.succeed("podman rm sleeping")
+
+      with subtest("Run container as root with the default backend"):
+          podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
+          podman.succeed(
+              "podman run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+          )
+          podman.succeed("podman ps | grep sleeping")
+          podman.succeed("podman stop sleeping")
+          podman.succeed("podman rm sleeping")
+
+      # create systemd session for rootless
+      podman.succeed("loginctl enable-linger alice")
+
+      with subtest("Run container rootless with runc"):
+          podman.succeed(su_cmd("tar cv --files-from /dev/null | podman import - scratchimg"))
+          podman.succeed(
+              su_cmd(
+                  "podman run --runtime=runc -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+              )
+          )
+          podman.succeed(su_cmd("podman ps | grep sleeping"))
+          podman.succeed(su_cmd("podman stop sleeping"))
+          podman.succeed(su_cmd("podman rm sleeping"))
+
+      with subtest("Run container rootless with crun"):
+          podman.succeed(su_cmd("tar cv --files-from /dev/null | podman import - scratchimg"))
+          podman.succeed(
+              su_cmd(
+                  "podman run --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"))
+
+      with subtest("Run container rootless with the default backend"):
+          podman.succeed(su_cmd("tar cv --files-from /dev/null | podman import - scratchimg"))
+          podman.succeed(
+              su_cmd(
+                  "podman run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+              )
+          )
+          podman.succeed(su_cmd("podman ps | grep sleeping"))
+          podman.succeed(su_cmd("podman stop sleeping"))
+          podman.succeed(su_cmd("podman rm sleeping"))
+
+      with subtest("Run container with init"):
+          podman.succeed(
+              "tar cv -C ${pkgs.pkgsStatic.busybox} . | podman import - busybox"
+          )
+          pid = podman.succeed("podman run --rm busybox readlink /proc/self").strip()
+          assert pid == "1"
+          pid = podman.succeed("podman run --rm --init busybox readlink /proc/self").strip()
+          assert pid == "2"
+
+      with subtest("A podman member can use the docker cli"):
+          podman.succeed(su_cmd("docker version"))
+
+      with subtest("Run container via docker cli"):
+          podman.succeed("docker network create default")
+          podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
+          podman.succeed(
+            "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin localhost/scratchimg /bin/sleep 10"
+          )
+          podman.succeed("docker ps | grep sleeping")
+          podman.succeed("podman ps | grep sleeping")
+          podman.succeed("docker stop sleeping")
+          podman.succeed("docker rm sleeping")
+          podman.succeed("docker network rm default")
+
+      with subtest("A podman non-member can not use the docker cli"):
+          podman.fail(su_cmd("docker version", user="mallory"))
+
+      # TODO: add docker-compose test
+
+    '';
+  }
+)
diff --git a/nixos/tests/podman/dnsname.nix b/nixos/tests/podman/dnsname.nix
new file mode 100644
index 00000000000..3768ae79e06
--- /dev/null
+++ b/nixos/tests/podman/dnsname.nix
@@ -0,0 +1,42 @@
+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
new file mode 100644
index 00000000000..268a55701cc
--- /dev/null
+++ b/nixos/tests/podman/tls-ghostunnel.nix
@@ -0,0 +1,150 @@
+/*
+  This test runs podman as a backend for the Docker CLI.
+ */
+import ../make-test-python.nix (
+  { pkgs, lib, ... }:
+
+  let gen-ca = pkgs.writeScript "gen-ca" ''
+    # Create CA
+    PATH="${pkgs.openssl}/bin:$PATH"
+    openssl genrsa -out ca-key.pem 4096
+    openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -subj '/C=NL/ST=Zuid-Holland/L=The Hague/O=Stevige Balken en Planken B.V./OU=OpSec/CN=Certificate Authority' -out ca.pem
+
+    # Create service
+    openssl genrsa -out podman-key.pem 4096
+    openssl req -subj '/CN=podman' -sha256 -new -key podman-key.pem -out service.csr
+    echo subjectAltName = DNS:podman,IP:127.0.0.1 >> extfile.cnf
+    echo extendedKeyUsage = serverAuth >> extfile.cnf
+    openssl x509 -req -days 365 -sha256 -in service.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out podman-cert.pem -extfile extfile.cnf
+
+    # Create client
+    openssl genrsa -out client-key.pem 4096
+    openssl req -subj '/CN=client' -new -key client-key.pem -out client.csr
+    echo extendedKeyUsage = clientAuth > extfile-client.cnf
+    openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -extfile extfile-client.cnf
+
+    # Create CA 2
+    PATH="${pkgs.openssl}/bin:$PATH"
+    openssl genrsa -out ca-2-key.pem 4096
+    openssl req -new -x509 -days 365 -key ca-2-key.pem -sha256 -subj '/C=NL/ST=Zuid-Holland/L=The Hague/O=Stevige Balken en Planken B.V./OU=OpSec/CN=Certificate Authority' -out ca-2.pem
+
+    # Create client signed by CA 2
+    openssl genrsa -out client-2-key.pem 4096
+    openssl req -subj '/CN=client' -new -key client-2-key.pem -out client-2.csr
+    echo extendedKeyUsage = clientAuth > extfile-client.cnf
+    openssl x509 -req -days 365 -sha256 -in client-2.csr -CA ca-2.pem -CAkey ca-2-key.pem -CAcreateserial -out client-2-cert.pem -extfile extfile-client.cnf
+
+    '';
+  in
+  {
+    name = "podman-tls-ghostunnel";
+    meta = {
+      maintainers = lib.teams.podman.members ++ [ lib.maintainers.roberth ];
+    };
+
+    nodes = {
+      podman =
+        { pkgs, ... }:
+        {
+          virtualisation.podman.enable = true;
+          virtualisation.podman.dockerSocket.enable = true;
+          virtualisation.podman.networkSocket = {
+            enable = true;
+            openFirewall = true;
+            server = "ghostunnel";
+            tls.cert = "/root/podman-cert.pem";
+            tls.key = "/root/podman-key.pem";
+            tls.cacert = "/root/ca.pem";
+          };
+
+          environment.systemPackages = [
+            pkgs.docker-client
+          ];
+
+          users.users.alice = {
+            isNormalUser = true;
+            home = "/home/alice";
+            description = "Alice Foobar";
+            extraGroups = ["podman"];
+          };
+
+        };
+
+      client = { ... }: {
+        environment.systemPackages = [
+          # Installs the docker _client_ only
+          # Normally, you'd want `virtualisation.docker.enable = true;`.
+          pkgs.docker-client
+        ];
+        environment.variables.DOCKER_HOST = "podman:2376";
+        environment.variables.DOCKER_TLS_VERIFY = "1";
+      };
+    };
+
+    testScript = ''
+      import shlex
+
+
+      def su_cmd(user, cmd):
+          cmd = shlex.quote(cmd)
+          return f"su {user} -l -c {cmd}"
+
+      def cmd(command):
+        print(f"+{command}")
+        r = os.system(command)
+        if r != 0:
+          raise Exception(f"Command {command} failed with exit code {r}")
+
+      start_all()
+      cmd("${gen-ca}")
+
+      podman.copy_from_host("ca.pem", "/root/ca.pem")
+      podman.copy_from_host("podman-cert.pem", "/root/podman-cert.pem")
+      podman.copy_from_host("podman-key.pem", "/root/podman-key.pem")
+
+      client.copy_from_host("ca.pem", "/root/.docker/ca.pem")
+      # client.copy_from_host("podman-cert.pem", "/root/podman-cert.pem")
+      client.copy_from_host("client-cert.pem", "/root/.docker/cert.pem")
+      client.copy_from_host("client-key.pem", "/root/.docker/key.pem")
+
+      # TODO (ghostunnel): add file watchers so the restart isn't necessary
+      podman.succeed("systemctl reset-failed && systemctl restart ghostunnel-server-podman-socket.service")
+
+      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")
+
+      with subtest("A podman member can also still use the docker cli"):
+          podman.succeed(su_cmd("alice", "docker version"))
+
+      with subtest("Run container remotely via docker cli"):
+          client.succeed("docker version")
+
+          # via socket would be nicer
+          podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
+
+          client.succeed(
+            "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin localhost/scratchimg /bin/sleep 10"
+          )
+          client.succeed("docker ps | grep sleeping")
+          podman.succeed("docker ps | grep sleeping")
+          client.succeed("docker stop sleeping")
+          client.succeed("docker rm sleeping")
+
+      with subtest("Clients without cert will be denied"):
+          client.succeed("rm /root/.docker/{cert,key}.pem")
+          client.fail("docker version")
+
+      with subtest("Clients with wrong cert will be denied"):
+          client.copy_from_host("client-2-cert.pem", "/root/.docker/cert.pem")
+          client.copy_from_host("client-2-key.pem", "/root/.docker/key.pem")
+          client.fail("docker version")
+
+    '';
+  }
+)
diff --git a/nixos/tests/pomerium.nix b/nixos/tests/pomerium.nix
new file mode 100644
index 00000000000..7af82832644
--- /dev/null
+++ b/nixos/tests/pomerium.nix
@@ -0,0 +1,102 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "pomerium";
+  meta = with lib.maintainers; {
+    maintainers = [ lukegb ];
+  };
+
+  nodes = let base = myIP: { pkgs, lib, ... }: {
+    virtualisation.vlans = [ 1 ];
+    networking = {
+      dhcpcd.enable = false;
+      firewall.allowedTCPPorts = [ 80 443 ];
+      hosts = {
+        "192.168.1.1" = [ "pomerium" "pom-auth" ];
+        "192.168.1.2" = [ "backend" "dummy-oidc" ];
+      };
+      interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+        { address = myIP; prefixLength = 24; }
+      ];
+    };
+  }; in {
+    pomerium = { pkgs, lib, ... }: {
+      imports = [ (base "192.168.1.1") ];
+      services.pomerium = {
+        enable = true;
+        settings = {
+          address = ":80";
+          insecure_server = true;
+          authenticate_service_url = "http://pom-auth";
+
+          idp_provider = "oidc";
+          idp_scopes = [ "oidc" ];
+          idp_client_id = "dummy";
+          idp_provider_url = "http://dummy-oidc";
+
+          policy = [{
+            from = "https://my.website";
+            to = "http://192.168.1.2";
+            allow_public_unauthenticated_access = true;
+            preserve_host_header = true;
+          } {
+            from = "https://login.required";
+            to = "http://192.168.1.2";
+            allowed_domains = [ "my.domain" ];
+            preserve_host_header = true;
+          }];
+        };
+        secretsFile = pkgs.writeText "pomerium-secrets" ''
+          # 12345678901234567890123456789012 in base64
+          COOKIE_SECRET=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=
+          IDP_CLIENT_SECRET=dummy
+        '';
+      };
+    };
+    backend = { pkgs, lib, ... }: {
+      imports = [ (base "192.168.1.2") ];
+      services.nginx.enable = true;
+      services.nginx.virtualHosts."my.website" = {
+        root = pkgs.runCommand "testdir" {} ''
+          mkdir "$out"
+          echo hello world > "$out/index.html"
+        '';
+      };
+      services.nginx.virtualHosts."dummy-oidc" = {
+        root = pkgs.runCommand "testdir" {} ''
+          mkdir -p "$out/.well-known"
+          cat <<EOF >"$out/.well-known/openid-configuration"
+            {
+              "issuer": "http://dummy-oidc",
+              "authorization_endpoint": "http://dummy-oidc/auth.txt",
+              "token_endpoint": "http://dummy-oidc/token",
+              "jwks_uri": "http://dummy-oidc/jwks.json",
+              "userinfo_endpoint": "http://dummy-oidc/userinfo",
+              "id_token_signing_alg_values_supported": ["RS256"]
+            }
+          EOF
+          echo hello I am login page >"$out/auth.txt"
+        '';
+      };
+    };
+  };
+
+  testScript = { ... }: ''
+    backend.wait_for_unit("nginx")
+    backend.wait_for_open_port(80)
+
+    pomerium.wait_for_unit("pomerium")
+    pomerium.wait_for_open_port(80)
+
+    with subtest("no authentication required"):
+        pomerium.succeed(
+            "curl --resolve my.website:80:127.0.0.1 http://my.website | grep 'hello world'"
+        )
+
+    with subtest("login required"):
+        pomerium.succeed(
+            "curl -I --resolve login.required:80:127.0.0.1 http://login.required | grep pom-auth"
+        )
+        pomerium.succeed(
+            "curl -L --resolve login.required:80:127.0.0.1 http://login.required | grep 'hello I am login page'"
+        )
+  '';
+})
diff --git a/nixos/tests/postfix-raise-smtpd-tls-security-level.nix b/nixos/tests/postfix-raise-smtpd-tls-security-level.nix
new file mode 100644
index 00000000000..5fad1fed75b
--- /dev/null
+++ b/nixos/tests/postfix-raise-smtpd-tls-security-level.nix
@@ -0,0 +1,41 @@
+import ./make-test-python.nix {
+  name = "postfix";
+
+  machine = { pkgs, ... }: {
+    imports = [ common/user-account.nix ];
+    services.postfix = {
+      enable = true;
+      enableSubmissions = true;
+      submissionsOptions = {
+        smtpd_tls_security_level = "none";
+      };
+    };
+
+    environment.systemPackages = let
+      checkConfig = pkgs.writeScriptBin "check-config" ''
+        #!${pkgs.python3.interpreter}
+        import sys
+
+        state = 1
+        success = False
+
+        with open("/etc/postfix/master.cf") as masterCf:
+          for line in masterCf:
+            if state == 1 and line.startswith("submissions"):
+              state = 2
+            elif state == 2 and line.startswith(" ") and "smtpd_tls_security_level=encrypt" in line:
+              success = True
+            elif state == 2 and not line.startswith(" "):
+              state == 3
+        if not success:
+          sys.exit(1)
+      '';
+
+    in [ checkConfig ];
+  };
+
+  testScript = ''
+    machine.wait_for_unit("postfix.service")
+    machine.succeed("check-config")
+  '';
+}
diff --git a/nixos/tests/postfix.nix b/nixos/tests/postfix.nix
new file mode 100644
index 00000000000..6d22b4edba0
--- /dev/null
+++ b/nixos/tests/postfix.nix
@@ -0,0 +1,77 @@
+let
+  certs = import ./common/acme/server/snakeoil-certs.nix;
+  domain = certs.domain;
+in
+import ./make-test-python.nix {
+  name = "postfix";
+
+  machine = { pkgs, ... }: {
+    imports = [ common/user-account.nix ];
+    services.postfix = {
+      enable = true;
+      enableSubmission = true;
+      enableSubmissions = true;
+      tlsTrustedAuthorities = "${certs.ca.cert}";
+      sslCert = "${certs.${domain}.cert}";
+      sslKey = "${certs.${domain}.key}";
+      submissionsOptions = {
+          smtpd_sasl_auth_enable = "yes";
+          smtpd_client_restrictions = "permit";
+          milter_macro_daemon_name = "ORIGINATING";
+      };
+    };
+
+    security.pki.certificateFiles = [
+      certs.ca.cert
+    ];
+
+    networking.extraHosts = ''
+      127.0.0.1 ${domain}
+    '';
+
+    environment.systemPackages = let
+      sendTestMail = pkgs.writeScriptBin "send-testmail" ''
+        #!${pkgs.python3.interpreter}
+        import smtplib
+
+        with smtplib.SMTP('${domain}') as smtp:
+          smtp.sendmail('root@localhost', 'alice@localhost', 'Subject: Test\n\nTest data.')
+          smtp.quit()
+      '';
+
+      sendTestMailStarttls = pkgs.writeScriptBin "send-testmail-starttls" ''
+        #!${pkgs.python3.interpreter}
+        import smtplib
+        import ssl
+
+        ctx = ssl.create_default_context()
+
+        with smtplib.SMTP('${domain}') as smtp:
+          smtp.ehlo()
+          smtp.starttls(context=ctx)
+          smtp.ehlo()
+          smtp.sendmail('root@localhost', 'alice@localhost', 'Subject: Test STARTTLS\n\nTest data.')
+          smtp.quit()
+      '';
+
+      sendTestMailSmtps = pkgs.writeScriptBin "send-testmail-smtps" ''
+        #!${pkgs.python3.interpreter}
+        import smtplib
+        import ssl
+
+        ctx = ssl.create_default_context()
+
+        with smtplib.SMTP_SSL(host='${domain}', context=ctx) as smtp:
+          smtp.sendmail('root@localhost', 'alice@localhost', 'Subject: Test SMTPS\n\nTest data.')
+          smtp.quit()
+      '';
+    in [ sendTestMail sendTestMailStarttls sendTestMailSmtps ];
+  };
+
+  testScript = ''
+    machine.wait_for_unit("postfix.service")
+    machine.succeed("send-testmail")
+    machine.succeed("send-testmail-starttls")
+    machine.succeed("send-testmail-smtps")
+  '';
+}
diff --git a/nixos/tests/postfixadmin.nix b/nixos/tests/postfixadmin.nix
new file mode 100644
index 00000000000..b2712f4699a
--- /dev/null
+++ b/nixos/tests/postfixadmin.nix
@@ -0,0 +1,31 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "postfixadmin";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ globin ];
+  };
+
+  nodes = {
+    postfixadmin = { config, pkgs, ... }: {
+      services.postfixadmin = {
+        enable = true;
+        hostName = "postfixadmin";
+        setupPasswordFile = pkgs.writeText "insecure-test-setup-pw-file" "$2y$10$r0p63YCjd9rb9nHrV9UtVuFgGTmPDLKu.0UIJoQTkWCZZze2iuB1m";
+      };
+      services.nginx.virtualHosts.postfixadmin = {
+        forceSSL = false;
+        enableACME = false;
+      };
+    };
+  };
+
+  testScript = ''
+    postfixadmin.start
+    postfixadmin.wait_for_unit("postgresql.service")
+    postfixadmin.wait_for_unit("phpfpm-postfixadmin.service")
+    postfixadmin.wait_for_unit("nginx.service")
+    postfixadmin.succeed(
+        "curl -sSfL http://postfixadmin/setup.php -X POST -F 'setup_password=not production'"
+    )
+    postfixadmin.succeed("curl -sSfL http://postfixadmin/ | grep 'Mail admins login here'")
+  '';
+})
diff --git a/nixos/tests/postgis.nix b/nixos/tests/postgis.nix
new file mode 100644
index 00000000000..9d81ebaad85
--- /dev/null
+++ b/nixos/tests/postgis.nix
@@ -0,0 +1,29 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "postgis";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lsix ];
+  };
+
+  nodes = {
+    master =
+      { pkgs, ... }:
+
+      {
+        services.postgresql = let mypg = pkgs.postgresql_11; in {
+            enable = true;
+            package = mypg;
+            extraPlugins = with mypg.pkgs; [
+              postgis
+            ];
+        };
+      };
+  };
+
+  testScript = ''
+    start_all()
+    master.wait_for_unit("postgresql")
+    master.sleep(10)  # Hopefully this is long enough!!
+    master.succeed("sudo -u postgres psql -c 'CREATE EXTENSION postgis;'")
+    master.succeed("sudo -u postgres psql -c 'CREATE EXTENSION postgis_topology;'")
+  '';
+})
diff --git a/nixos/tests/postgresql-wal-receiver.nix b/nixos/tests/postgresql-wal-receiver.nix
new file mode 100644
index 00000000000..0e8b3bfd6c3
--- /dev/null
+++ b/nixos/tests/postgresql-wal-receiver.nix
@@ -0,0 +1,119 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+
+let
+  lib = pkgs.lib;
+
+  # Makes a test for a PostgreSQL package, given by name and looked up from `pkgs`.
+  makePostgresqlWalReceiverTest = postgresqlPackage:
+  {
+    name = postgresqlPackage;
+    value =
+      let
+        pkg = pkgs."${postgresqlPackage}";
+        postgresqlDataDir = "/var/lib/postgresql/${pkg.psqlSchema}";
+        replicationUser = "wal_receiver_user";
+        replicationSlot = "wal_receiver_slot";
+        replicationConn = "postgresql://${replicationUser}@localhost";
+        baseBackupDir = "/tmp/pg_basebackup";
+        walBackupDir = "/tmp/pg_wal";
+        atLeast12 = lib.versionAtLeast pkg.version "12.0";
+
+        recoveryFile = if atLeast12
+            then pkgs.writeTextDir "recovery.signal" ""
+            else pkgs.writeTextDir "recovery.conf" "restore_command = 'cp ${walBackupDir}/%f %p'";
+
+      in makeTest {
+        name = "postgresql-wal-receiver-${postgresqlPackage}";
+        meta.maintainers = with lib.maintainers; [ pacien ];
+
+        machine = { ... }: {
+          services.postgresql = {
+            package = pkg;
+            enable = true;
+            settings = lib.mkMerge [
+              {
+                wal_level = "archive"; # alias for replica on pg >= 9.6
+                max_wal_senders = 10;
+                max_replication_slots = 10;
+              }
+              (lib.mkIf atLeast12 {
+                restore_command = "cp ${walBackupDir}/%f %p";
+                recovery_end_command = "touch recovery.done";
+              })
+            ];
+            authentication = ''
+              host replication ${replicationUser} all trust
+            '';
+            initialScript = pkgs.writeText "init.sql" ''
+              create user ${replicationUser} replication;
+              select * from pg_create_physical_replication_slot('${replicationSlot}');
+            '';
+          };
+
+          services.postgresqlWalReceiver.receivers.main = {
+            postgresqlPackage = pkg;
+            connection = replicationConn;
+            slot = replicationSlot;
+            directory = walBackupDir;
+          };
+          # This is only to speedup test, it isn't time racing. Service is set to autorestart always,
+          # default 60sec is fine for real system, but is too much for a test
+          systemd.services.postgresql-wal-receiver-main.serviceConfig.RestartSec = lib.mkForce 5;
+        };
+
+        testScript = ''
+          # make an initial base backup
+          machine.wait_for_unit("postgresql")
+          machine.wait_for_unit("postgresql-wal-receiver-main")
+          # WAL receiver healthchecks PG every 5 seconds, so let's be sure they have connected each other
+          # required only for 9.4
+          machine.sleep(5)
+          machine.succeed(
+              "${pkg}/bin/pg_basebackup --dbname=${replicationConn} --pgdata=${baseBackupDir}"
+          )
+
+          # create a dummy table with 100 records
+          machine.succeed(
+              "sudo -u postgres psql --command='create table dummy as select * from generate_series(1, 100) as val;'"
+          )
+
+          # stop postgres and destroy data
+          machine.systemctl("stop postgresql")
+          machine.systemctl("stop postgresql-wal-receiver-main")
+          machine.succeed("rm -r ${postgresqlDataDir}/{base,global,pg_*}")
+
+          # restore the base backup
+          machine.succeed(
+              "cp -r ${baseBackupDir}/* ${postgresqlDataDir} && chown postgres:postgres -R ${postgresqlDataDir}"
+          )
+
+          # prepare WAL and recovery
+          machine.succeed("chmod a+rX -R ${walBackupDir}")
+          machine.execute(
+              "for part in ${walBackupDir}/*.partial; do mv $part ''${part%%.*}; done"
+          )  # make use of partial segments too
+          machine.succeed(
+              "cp ${recoveryFile}/* ${postgresqlDataDir}/ && chmod 666 ${postgresqlDataDir}/recovery*"
+          )
+
+          # replay WAL
+          machine.systemctl("start postgresql")
+          machine.wait_for_file("${postgresqlDataDir}/recovery.done")
+          machine.systemctl("restart postgresql")
+          machine.wait_for_unit("postgresql")
+
+          # check that our records have been restored
+          machine.succeed(
+              "test $(sudo -u postgres psql --pset='pager=off' --tuples-only --command='select count(distinct val) from dummy;') -eq 100"
+          )
+        '';
+      };
+    };
+
+# Maps the generic function over all attributes of PostgreSQL packages
+in builtins.listToAttrs (map makePostgresqlWalReceiverTest (builtins.attrNames (import ../../pkgs/servers/sql/postgresql { })))
diff --git a/nixos/tests/postgresql.nix b/nixos/tests/postgresql.nix
new file mode 100644
index 00000000000..2b487c20a62
--- /dev/null
+++ b/nixos/tests/postgresql.nix
@@ -0,0 +1,137 @@
+{ 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 pgcrypto; -- just to check if lib loading works
+    CREATE TABLE sth (
+      id int
+    );
+    INSERT INTO sth (id) VALUES (1);
+    INSERT INTO sth (id) VALUES (1);
+    INSERT INTO sth (id) VALUES (1);
+    INSERT INTO sth (id) VALUES (1);
+    INSERT INTO sth (id) VALUES (1);
+    CREATE TABLE xmltest ( doc xml );
+    INSERT INTO xmltest (doc) VALUES ('<test>ok</test>'); -- check if libxml2 enabled
+  '';
+  make-postgresql-test = postgresql-name: postgresql-package: backup-all: makeTest {
+    name = postgresql-name;
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ zagy ];
+    };
+
+    machine = {...}:
+      {
+        services.postgresql = {
+          enable = true;
+          package = postgresql-package;
+        };
+
+        services.postgresqlBackup = {
+          enable = true;
+          databases = optional (!backup-all) "postgres";
+        };
+      };
+
+    testScript = let
+      backupName = if backup-all then "all" else "postgres";
+      backupService = if backup-all then "postgresqlBackup" else "postgresqlBackup-postgres";
+      backupFileBase = "/var/backup/postgresql/${backupName}";
+    in ''
+      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 is available just after unit start"):
+          machine.succeed(
+              "cat ${test-sql} | sudo -u postgres psql"
+          )
+
+      with subtest("Postgresql survives restart (bug #1735)"):
+          machine.shutdown()
+          import time
+          time.sleep(2)
+          machine.start()
+          machine.wait_for_unit("postgresql")
+
+      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.succeed(check_count("SELECT xpath('/test/text()', doc) FROM xmltest;", 1))
+
+      with subtest("Backup service works"):
+          machine.succeed(
+              "systemctl start ${backupService}.service",
+              "zcat ${backupFileBase}.sql.gz | grep '<test>ok</test>'",
+              "ls -hal /var/backup/postgresql/ >/dev/console",
+              "stat -c '%a' ${backupFileBase}.sql.gz | grep 600",
+          )
+      with subtest("Backup service removes prev files"):
+          machine.succeed(
+              # Create dummy prev files.
+              "touch ${backupFileBase}.prev.sql{,.gz,.zstd}",
+              "chown postgres:postgres ${backupFileBase}.prev.sql{,.gz,.zstd}",
+
+              # Run backup.
+              "systemctl start ${backupService}.service",
+              "ls -hal /var/backup/postgresql/ >/dev/console",
+
+              # Since nothing has changed in the database, the cur and prev files
+              # should match.
+              "zcat ${backupFileBase}.sql.gz | grep '<test>ok</test>'",
+              "cmp ${backupFileBase}.sql.gz ${backupFileBase}.prev.sql.gz",
+
+              # The prev files with unused suffix should be removed.
+              "[ ! -f '${backupFileBase}.prev.sql' ]",
+              "[ ! -f '${backupFileBase}.prev.sql.zstd' ]",
+
+              # Both cur and prev file should only be accessible by the postgres user.
+              "stat -c '%a' ${backupFileBase}.sql.gz | grep 600",
+              "stat -c '%a' '${backupFileBase}.prev.sql.gz' | grep 600",
+          )
+      with subtest("Backup service fails gracefully"):
+          # Sabotage the backup process
+          machine.succeed("rm /run/postgresql/.s.PGSQL.5432")
+          machine.fail(
+              "systemctl start ${backupService}.service",
+          )
+          machine.succeed(
+              "ls -hal /var/backup/postgresql/ >/dev/console",
+              "zcat ${backupFileBase}.prev.sql.gz | grep '<test>ok</test>'",
+              "stat ${backupFileBase}.in-progress.sql.gz",
+          )
+          # In a previous version, the second run would overwrite prev.sql.gz,
+          # so we test a second run as well.
+          machine.fail(
+              "systemctl start ${backupService}.service",
+          )
+          machine.succeed(
+              "stat ${backupFileBase}.in-progress.sql.gz",
+              "zcat ${backupFileBase}.prev.sql.gz | grep '<test>ok</test>'",
+          )
+
+
+      with subtest("Initdb works"):
+          machine.succeed("sudo -u postgres initdb -D /tmp/testpostgres2")
+
+      machine.shutdown()
+    '';
+
+  };
+in
+  (mapAttrs' (name: package: { inherit name; value=make-postgresql-test name package false;}) postgresql-versions) // {
+    postgresql_11-backup-all = make-postgresql-test "postgresql_11-backup-all" postgresql-versions.postgresql_11 true;
+  }
+
diff --git a/nixos/tests/power-profiles-daemon.nix b/nixos/tests/power-profiles-daemon.nix
new file mode 100644
index 00000000000..e073677bee9
--- /dev/null
+++ b/nixos/tests/power-profiles-daemon.nix
@@ -0,0 +1,45 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "power-profiles-daemon";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mvnetbiz ];
+  };
+  machine = { pkgs, ... }: {
+    services.power-profiles-daemon.enable = true;
+    environment.systemPackages = [ pkgs.glib ];
+  };
+
+  testScript = ''
+    def get_profile():
+        return machine.succeed(
+            """gdbus call --system --dest net.hadess.PowerProfiles --object-path /net/hadess/PowerProfiles \
+    --method org.freedesktop.DBus.Properties.Get 'net.hadess.PowerProfiles' 'ActiveProfile'
+    """
+        )
+
+
+    def set_profile(profile):
+        return machine.succeed(
+            """gdbus call --system --dest net.hadess.PowerProfiles --object-path /net/hadess/PowerProfiles \
+    --method org.freedesktop.DBus.Properties.Set 'net.hadess.PowerProfiles' 'ActiveProfile' "<'{profile}'>"
+    """.format(
+                profile=profile
+            )
+        )
+
+
+    machine.wait_for_unit("multi-user.target")
+
+    set_profile("power-saver")
+    profile = get_profile()
+    if not "power-saver" in profile:
+        raise Exception("Unable to set power-saver profile")
+
+
+    set_profile("balanced")
+    profile = get_profile()
+    if not "balanced" in profile:
+        raise Exception("Unable to set balanced profile")
+  '';
+})
diff --git a/nixos/tests/powerdns-admin.nix b/nixos/tests/powerdns-admin.nix
new file mode 100644
index 00000000000..4d763c9c6f6
--- /dev/null
+++ b/nixos/tests/powerdns-admin.nix
@@ -0,0 +1,117 @@
+# Test powerdns-admin
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+let
+  defaultConfig = ''
+    BIND_ADDRESS = '127.0.0.1'
+    PORT = 8000
+  '';
+
+  makeAppTest = name: configs: makeTest {
+    name = "powerdns-admin-${name}";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ Flakebi zhaofengli ];
+    };
+
+    nodes.server = { pkgs, config, ... }: mkMerge ([
+      {
+        services.powerdns-admin = {
+          enable = true;
+          secretKeyFile = "/etc/powerdns-admin/secret";
+          saltFile = "/etc/powerdns-admin/salt";
+        };
+        # It's insecure to have secrets in the world-readable nix store, but this is just a test
+        environment.etc."powerdns-admin/secret".text = "secret key";
+        environment.etc."powerdns-admin/salt".text = "salt";
+        environment.systemPackages = [
+          (pkgs.writeShellScriptBin "run-test" config.system.build.testScript)
+        ];
+      }
+    ] ++ configs);
+
+    testScript = ''
+      server.wait_for_unit("powerdns-admin.service")
+      server.wait_until_succeeds("run-test", timeout=10)
+    '';
+  };
+
+  matrix = {
+    backend = {
+      mysql = {
+        services.powerdns-admin = {
+          config = ''
+            ${defaultConfig}
+            SQLALCHEMY_DATABASE_URI = 'mysql://powerdnsadmin@/powerdnsadmin?unix_socket=/run/mysqld/mysqld.sock'
+          '';
+        };
+        systemd.services.powerdns-admin = {
+          after = [ "mysql.service" ];
+          serviceConfig.BindPaths = "/run/mysqld";
+        };
+
+        services.mysql = {
+          enable = true;
+          package = pkgs.mariadb;
+          ensureDatabases = [ "powerdnsadmin" ];
+          ensureUsers = [
+            {
+              name = "powerdnsadmin";
+              ensurePermissions = {
+                "powerdnsadmin.*" = "ALL PRIVILEGES";
+              };
+            }
+          ];
+        };
+      };
+      postgresql = {
+        services.powerdns-admin = {
+          config = ''
+            ${defaultConfig}
+            SQLALCHEMY_DATABASE_URI = 'postgresql://powerdnsadmin@/powerdnsadmin?host=/run/postgresql'
+          '';
+        };
+        systemd.services.powerdns-admin = {
+          after = [ "postgresql.service" ];
+          serviceConfig.BindPaths = "/run/postgresql";
+        };
+
+        services.postgresql = {
+          enable = true;
+          ensureDatabases = [ "powerdnsadmin" ];
+          ensureUsers = [
+            {
+              name = "powerdnsadmin";
+              ensurePermissions = {
+                "DATABASE powerdnsadmin" = "ALL PRIVILEGES";
+              };
+            }
+          ];
+        };
+      };
+    };
+    listen = {
+      tcp = {
+        services.powerdns-admin.extraArgs = [ "-b" "127.0.0.1:8000" ];
+        system.build.testScript = ''
+          curl -sSf http://127.0.0.1:8000/
+        '';
+      };
+      unix = {
+        services.powerdns-admin.extraArgs = [ "-b" "unix:/run/powerdns-admin/http.sock" ];
+        system.build.testScript = ''
+          curl -sSf --unix-socket /run/powerdns-admin/http.sock http://somehost/
+        '';
+      };
+    };
+  };
+in
+with matrix; {
+  postgresql = makeAppTest "postgresql" [ backend.postgresql listen.tcp ];
+  mysql = makeAppTest "mysql" [ backend.mysql listen.tcp ];
+  unix-listener = makeAppTest "unix-listener" [ backend.postgresql listen.unix ];
+}
diff --git a/nixos/tests/powerdns.nix b/nixos/tests/powerdns.nix
new file mode 100644
index 00000000000..d025934ad2b
--- /dev/null
+++ b/nixos/tests/powerdns.nix
@@ -0,0 +1,65 @@
+# This test runs PowerDNS authoritative server with the
+# generic MySQL backend (gmysql) to connect to a
+# MariaDB server using UNIX sockets authentication.
+
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "powerdns";
+
+  nodes.server = { ... }: {
+    services.powerdns.enable = true;
+    services.powerdns.extraConfig = ''
+      launch=gmysql
+      gmysql-user=pdns
+    '';
+
+    services.mysql = {
+      enable = true;
+      package = pkgs.mariadb;
+      ensureDatabases = [ "powerdns" ];
+      ensureUsers = lib.singleton
+        { name = "pdns";
+          ensurePermissions = { "powerdns.*" = "ALL PRIVILEGES"; };
+        };
+    };
+
+    environment.systemPackages = with pkgs;
+      [ dnsutils powerdns mariadb ];
+  };
+
+  testScript = ''
+    import re
+
+    with subtest("PowerDNS database exists"):
+        server.wait_for_unit("mysql")
+        server.succeed("echo 'SHOW DATABASES;' | sudo -u pdns mysql -u pdns >&2")
+
+    with subtest("Loading the MySQL schema works"):
+        server.succeed(
+            "sudo -u pdns mysql -u pdns -D powerdns <"
+            "${pkgs.powerdns}/share/doc/pdns/schema.mysql.sql"
+        )
+
+    with subtest("PowerDNS server starts"):
+        server.wait_for_unit("pdns")
+        server.succeed("dig version.bind txt chaos @127.0.0.1 >&2")
+
+    with subtest("Adding an example zone works"):
+        # Extract configuration file needed by pdnsutil
+        unit = server.succeed("systemctl cat pdns")
+        conf = re.search("(--config-dir=[^ ]+)", unit).group(1)
+        pdnsutil = "sudo -u pdns pdnsutil " + conf
+        server.succeed(f"{pdnsutil} create-zone example.com ns1.example.com")
+        server.succeed(f"{pdnsutil} add-record  example.com ns1 A 192.168.1.2")
+
+    with subtest("Querying the example zone works"):
+        reply = server.succeed("dig +noall +answer ns1.example.com @127.0.0.1")
+        assert (
+            "192.168.1.2" in reply
+        ), f""""
+        The reply does not contain the expected IP address:
+          Expected:
+            ns1.example.com.        3600    IN      A       192.168.1.2
+          Reply:
+            {reply}"""
+  '';
+})
diff --git a/nixos/tests/pppd.nix b/nixos/tests/pppd.nix
new file mode 100644
index 00000000000..bda0aa75bb5
--- /dev/null
+++ b/nixos/tests/pppd.nix
@@ -0,0 +1,62 @@
+import ./make-test-python.nix (
+  let
+    chap-secrets = {
+      text = ''"flynn" * "reindeerflotilla" *'';
+      mode = "0640";
+    };
+  in {
+    nodes = {
+      server = {config, pkgs, ...}: {
+        config = {
+          # Run a PPPoE access concentrator server. It will spawn an
+          # appropriate PPP server process when a PPPoE client sets up a
+          # PPPoE session.
+          systemd.services.pppoe-server = {
+            restartTriggers = [
+              config.environment.etc."ppp/pppoe-server-options".source
+              config.environment.etc."ppp/chap-secrets".source
+            ];
+            after = ["network.target"];
+            serviceConfig = {
+              ExecStart = "${pkgs.rpPPPoE}/sbin/pppoe-server -F -O /etc/ppp/pppoe-server-options -q ${pkgs.ppp}/sbin/pppd -I eth1 -L 192.0.2.1 -R 192.0.2.2";
+            };
+            wantedBy = ["multi-user.target"];
+          };
+          environment.etc = {
+            "ppp/pppoe-server-options".text = ''
+              lcp-echo-interval 10
+              lcp-echo-failure 2
+              plugin rp-pppoe.so
+              require-chap
+              nobsdcomp
+              noccp
+              novj
+            '';
+            "ppp/chap-secrets" = chap-secrets;
+          };
+        };
+      };
+      client = {config, pkgs, ...}: {
+        services.pppd = {
+          enable = true;
+          peers.test = {
+            config = ''
+              plugin rp-pppoe.so eth1
+              name "flynn"
+              noipdefault
+              persist
+              noauth
+              debug
+            '';
+          };
+        };
+        environment.etc."ppp/chap-secrets" = chap-secrets;
+      };
+    };
+
+    testScript = ''
+      start_all()
+      client.wait_until_succeeds("ping -c1 -W1 192.0.2.1")
+      server.wait_until_succeeds("ping -c1 -W1 192.0.2.2")
+    '';
+  })
diff --git a/nixos/tests/predictable-interface-names.nix b/nixos/tests/predictable-interface-names.nix
new file mode 100644
index 00000000000..c0b472638a1
--- /dev/null
+++ b/nixos/tests/predictable-interface-names.nix
@@ -0,0 +1,37 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+let
+  inherit (import ../lib/testing-python.nix { inherit system pkgs; }) makeTest;
+  testCombinations = pkgs.lib.cartesianProductOfSets {
+    predictable = [true false];
+    withNetworkd = [true false];
+  };
+in pkgs.lib.listToAttrs (builtins.map ({ predictable, withNetworkd }: {
+  name = pkgs.lib.optionalString (!predictable) "un" + "predictable"
+       + pkgs.lib.optionalString withNetworkd "Networkd";
+  value = makeTest {
+    name = "${if predictable then "" else "un"}predictableInterfaceNames${if withNetworkd then "-with-networkd" else ""}";
+    meta = {};
+
+    machine = { lib, ... }: {
+      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
+      '';
+    };
+
+    testScript = ''
+      print(machine.succeed("ip link"))
+      machine.${if predictable then "fail" else "succeed"}("ip link show eth0")
+    '';
+  };
+}) testCombinations)
diff --git a/nixos/tests/printing.nix b/nixos/tests/printing.nix
new file mode 100644
index 00000000000..6338fd8d8ac
--- /dev/null
+++ b/nixos/tests/printing.nix
@@ -0,0 +1,128 @@
+# Test printing via CUPS.
+
+import ./make-test-python.nix ({pkgs, ... }:
+let
+  printingServer = startWhenNeeded: {
+    services.printing.enable = true;
+    services.printing.startWhenNeeded = startWhenNeeded;
+    services.printing.listenAddresses = [ "*:631" ];
+    services.printing.defaultShared = true;
+    services.printing.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 = [{
+      name = "DeskjetLocal";
+      deviceUri = "usb://foobar/printers/foobar";
+      model = "drv:///sample.drv/deskjet.ppd";
+    }];
+  };
+  printingClient = startWhenNeeded: {
+    services.printing.enable = true;
+    services.printing.startWhenNeeded = startWhenNeeded;
+    # Add printer to the client as well, via IPP.
+    hardware.printers.ensurePrinters = [{
+      name = "DeskjetRemote";
+      deviceUri = "ipp://${if startWhenNeeded then "socketActivatedServer" else "serviceServer"}/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
+
+    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")
+
+
+    def test_printing(client, server):
+        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}'"
+                )
+
+                # 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)
+  '';
+})
diff --git a/nixos/tests/privacyidea.nix b/nixos/tests/privacyidea.nix
new file mode 100644
index 00000000000..c1141465ec2
--- /dev/null
+++ b/nixos/tests/privacyidea.nix
@@ -0,0 +1,43 @@
+# Miscellaneous small tests that don't warrant their own VM run.
+
+import ./make-test-python.nix ({ pkgs, ...} : rec {
+  name = "privacyidea";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fpletz ];
+  };
+
+  machine = { ... }: {
+    virtualisation.cores = 2;
+
+    services.privacyidea = {
+      enable = true;
+      secretKey = "$SECRET_KEY";
+      pepper = "$PEPPER";
+      adminPasswordFile = pkgs.writeText "admin-password" "testing";
+      adminEmail = "root@localhost";
+
+      # Don't try this at home!
+      environmentFile = pkgs.writeText "pi-secrets.env" ''
+        SECRET_KEY=testing
+        PEPPER=testing
+      '';
+    };
+    services.nginx = {
+      enable = true;
+      virtualHosts."_".locations."/".extraConfig = ''
+        uwsgi_pass unix:/run/privacyidea/socket;
+      '';
+    };
+  };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed("curl --fail http://localhost | grep privacyIDEA")
+    machine.succeed("grep \"SECRET_KEY = 'testing'\" /var/lib/privacyidea/privacyidea.cfg")
+    machine.succeed("grep \"PI_PEPPER = 'testing'\" /var/lib/privacyidea/privacyidea.cfg")
+    machine.succeed(
+        "curl --fail http://localhost/auth -F username=admin -F password=testing | grep token"
+    )
+  '';
+})
diff --git a/nixos/tests/privoxy.nix b/nixos/tests/privoxy.nix
new file mode 100644
index 00000000000..d16cc498691
--- /dev/null
+++ b/nixos/tests/privoxy.nix
@@ -0,0 +1,113 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+
+let
+  # Note: For some reason Privoxy can't issue valid
+  # certificates if the CA is generated using gnutls :(
+  certs = pkgs.runCommand "example-certs"
+    { buildInputs = [ pkgs.openssl ]; }
+    ''
+      mkdir $out
+
+      # generate CA keypair
+      openssl req -new -nodes -x509 \
+        -extensions v3_ca -keyout $out/ca.key \
+        -out $out/ca.crt -days 365 \
+        -subj "/O=Privoxy CA/CN=Privoxy CA"
+
+      # generate server key/signing request
+      openssl genrsa -out $out/server.key 3072
+      openssl req -new -key $out/server.key \
+        -out server.csr -sha256 \
+        -subj "/O=An unhappy server./CN=example.com"
+
+      # sign the request/generate the certificate
+      openssl x509 -req -in server.csr -CA $out/ca.crt \
+      -CAkey $out/ca.key -CAcreateserial -out $out/server.crt \
+      -days 500 -sha256
+    '';
+in
+
+{
+  name = "privoxy";
+  meta = with lib.maintainers; {
+    maintainers = [ rnhmjoj ];
+  };
+
+  machine = { ... }: {
+    services.nginx.enable = true;
+    services.nginx.virtualHosts."example.com" = {
+      addSSL = true;
+      sslCertificate = "${certs}/server.crt";
+      sslCertificateKey = "${certs}/server.key";
+      locations."/".root = pkgs.writeTextFile
+        { name = "bad-day";
+          destination = "/how-are-you/index.html";
+          text = "I've had a bad day!\n";
+        };
+      locations."/ads".extraConfig = ''
+        return 200 "Hot Nixpkgs PRs in your area. Click here!\n";
+      '';
+    };
+
+    services.privoxy = {
+      enable = true;
+      inspectHttps = true;
+      settings = {
+        ca-cert-file = "${certs}/ca.crt";
+        ca-key-file  = "${certs}/ca.key";
+        debug = 65536;
+      };
+      userActions = ''
+        {+filter{positive}}
+        example.com
+
+        {+block{Fake ads}}
+        example.com/ads
+      '';
+      userFilters = ''
+        FILTER: positive This is a filter example.
+        s/bad/great/ig
+      '';
+    };
+
+    security.pki.certificateFiles = [ "${certs}/ca.crt" ];
+
+    networking.hosts."::1" = [ "example.com" ];
+    networking.proxy.httpProxy = "http://localhost:8118";
+    networking.proxy.httpsProxy = "http://localhost:8118";
+  };
+
+  testScript =
+    ''
+      with subtest("Privoxy is running"):
+          machine.wait_for_unit("privoxy")
+          machine.wait_for_open_port("8118")
+          machine.succeed("curl -f http://config.privoxy.org")
+
+      with subtest("Privoxy can filter http requests"):
+          machine.wait_for_open_port("80")
+          assert "great day" in machine.succeed(
+              "curl -sfL http://example.com/how-are-you? | tee /dev/stderr"
+          )
+
+      with subtest("Privoxy can filter https requests"):
+          machine.wait_for_open_port("443")
+          assert "great day" in machine.succeed(
+              "curl -sfL https://example.com/how-are-you? | tee /dev/stderr"
+          )
+
+      with subtest("Blocks are working"):
+          machine.wait_for_open_port("443")
+          machine.fail("curl -f https://example.com/ads 1>&2")
+          machine.succeed("curl -f https://example.com/PRIVOXY-FORCE/ads 1>&2")
+
+      with subtest("Temporary certificates are cleaned"):
+          # Count current certificates
+          machine.succeed("test $(ls /run/privoxy/certs | wc -l) -gt 0")
+          # Forward in time 12 days, trigger the timer..
+          machine.succeed("date -s \"$(date --date '12 days')\"")
+          machine.systemctl("start systemd-tmpfiles-clean")
+          # ...and count again
+          machine.succeed("test $(ls /run/privoxy/certs | wc -l) -eq 0")
+    '';
+})
diff --git a/nixos/tests/prometheus-exporters.nix b/nixos/tests/prometheus-exporters.nix
new file mode 100644
index 00000000000..ce3b3fbf3bf
--- /dev/null
+++ b/nixos/tests/prometheus-exporters.nix
@@ -0,0 +1,1342 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+let
+  inherit (import ../lib/testing-python.nix { inherit system pkgs; }) makeTest;
+  inherit (pkgs.lib) concatStringsSep maintainers mapAttrs mkMerge
+    removeSuffix replaceChars singleton splitString;
+
+  /*
+    * The attrset `exporterTests` contains one attribute
+    * for each exporter test. Each of these attributes
+    * is expected to be an attrset containing:
+    *
+    *  `exporterConfig`:
+    *    this attribute set contains config for the exporter itself
+    *
+    *  `exporterTest`
+    *    this attribute set contains test instructions
+    *
+    *  `metricProvider` (optional)
+    *    this attribute contains additional machine config
+    *
+    *  `nodeName` (optional)
+    *    override an incompatible testnode name
+    *
+    *  Example:
+    *    exporterTests.<exporterName> = {
+    *      exporterConfig = {
+    *        enable = true;
+    *      };
+    *      metricProvider = {
+    *        services.<metricProvider>.enable = true;
+    *      };
+    *      exporterTest = ''
+    *        wait_for_unit("prometheus-<exporterName>-exporter.service")
+    *        wait_for_open_port("1234")
+    *        succeed("curl -sSf 'localhost:1234/metrics'")
+    *      '';
+    *    };
+    *
+    *  # this would generate the following test config:
+    *
+    *    nodes.<exporterName> = {
+    *      services.prometheus.<exporterName> = {
+    *        enable = true;
+    *      };
+    *      services.<metricProvider>.enable = true;
+    *    };
+    *
+    *    testScript = ''
+    *      <exporterName>.start()
+    *      <exporterName>.wait_for_unit("prometheus-<exporterName>-exporter.service")
+    *      <exporterName>.wait_for_open_port("1234")
+    *      <exporterName>.succeed("curl -sSf 'localhost:1234/metrics'")
+    *      <exporterName>.shutdown()
+    *    '';
+  */
+
+  exporterTests = {
+    apcupsd = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider = {
+        services.apcupsd.enable = true;
+      };
+      exporterTest = ''
+        wait_for_unit("apcupsd.service")
+        wait_for_open_port(3551)
+        wait_for_unit("prometheus-apcupsd-exporter.service")
+        wait_for_open_port(9162)
+        succeed("curl -sSf http://localhost:9162/metrics | grep 'apcupsd_info'")
+      '';
+    };
+
+    artifactory = {
+      exporterConfig = {
+        enable = true;
+        artiUsername = "artifactory-username";
+        artiPassword = "artifactory-password";
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-artifactory-exporter.service")
+        wait_for_open_port(9531)
+        succeed(
+            "curl -sSf http://localhost:9531/metrics | grep 'artifactory_up'"
+        )
+      '';
+    };
+
+    bind = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider = {
+        services.bind.enable = true;
+        services.bind.extraConfig = ''
+          statistics-channels {
+            inet 127.0.0.1 port 8053 allow { localhost; };
+          };
+        '';
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-bind-exporter.service")
+        wait_for_open_port(9119)
+        succeed(
+            "curl -sSf http://localhost:9119/metrics | grep 'bind_query_recursions_total 0'"
+        )
+      '';
+    };
+
+    bird = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider = {
+        services.bird2.enable = true;
+        services.bird2.config = ''
+          router id 127.0.0.1;
+
+          protocol kernel MyObviousTestString {
+            ipv4 {
+              import all;
+              export none;
+            };
+          }
+
+          protocol device {
+          }
+        '';
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-bird-exporter.service")
+        wait_for_open_port(9324)
+        wait_until_succeeds(
+            "curl -sSf http://localhost:9324/metrics | grep 'MyObviousTestString'"
+        )
+      '';
+    };
+
+    bitcoin = {
+      exporterConfig = {
+        enable = true;
+        rpcUser = "bitcoinrpc";
+        rpcPasswordFile = pkgs.writeText "password" "hunter2";
+      };
+      metricProvider = {
+        services.bitcoind.default.enable = true;
+        services.bitcoind.default.rpc.users.bitcoinrpc.passwordHMAC = "e8fe33f797e698ac258c16c8d7aadfbe$872bdb8f4d787367c26bcfd75e6c23c4f19d44a69f5d1ad329e5adf3f82710f7";
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-bitcoin-exporter.service")
+        wait_for_unit("bitcoind-default.service")
+        wait_for_open_port(9332)
+        succeed("curl -sSf http://localhost:9332/metrics | grep '^bitcoin_blocks '")
+      '';
+    };
+
+    blackbox = {
+      exporterConfig = {
+        enable = true;
+        configFile = pkgs.writeText "config.yml" (builtins.toJSON {
+          modules.icmp_v6 = {
+            prober = "icmp";
+            icmp.preferred_ip_protocol = "ip6";
+          };
+        });
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-blackbox-exporter.service")
+        wait_for_open_port(9115)
+        succeed(
+            "curl -sSf 'http://localhost:9115/probe?target=localhost&module=icmp_v6' | grep 'probe_success 1'"
+        )
+      '';
+    };
+
+    collectd = {
+      exporterConfig = {
+        enable = true;
+        extraFlags = [ "--web.collectd-push-path /collectd" ];
+      };
+      exporterTest = let postData = replaceChars [ "\n" ] [ "" ] ''
+        [{
+          "values":[23],
+          "dstypes":["gauge"],
+          "type":"gauge",
+          "interval":1000,
+          "host":"testhost",
+          "plugin":"testplugin",
+          "time":DATE
+        }]
+      ''; in
+        ''
+          wait_for_unit("prometheus-collectd-exporter.service")
+          wait_for_open_port(9103)
+          succeed(
+              'echo \'${postData}\'> /tmp/data.json'
+          )
+          succeed('sed -ie "s DATE $(date +%s) " /tmp/data.json')
+          succeed(
+              "curl -sSfH 'Content-Type: application/json' -X POST --data @/tmp/data.json localhost:9103/collectd"
+          )
+          succeed(
+              "curl -sSf localhost:9103/metrics | grep 'collectd_testplugin_gauge{instance=\"testhost\"} 23'"
+          )
+        '';
+    };
+
+    dnsmasq = {
+      exporterConfig = {
+        enable = true;
+        leasesPath = "/var/lib/dnsmasq/dnsmasq.leases";
+      };
+      metricProvider = {
+        services.dnsmasq.enable = true;
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-dnsmasq-exporter.service")
+        wait_for_open_port(9153)
+        succeed("curl -sSf http://localhost:9153/metrics | grep 'dnsmasq_leases 0'")
+      '';
+    };
+
+    # Access to WHOIS server is required to properly test this exporter, so
+    # just perform basic sanity check that the exporter is running and returns
+    # a failure.
+    domain = {
+      exporterConfig = {
+        enable = true;
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-domain-exporter.service")
+        wait_for_open_port(9222)
+        succeed(
+            "curl -sSf 'http://localhost:9222/probe?target=nixos.org' | grep 'domain_probe_success 0'"
+        )
+      '';
+    };
+
+    dovecot = {
+      exporterConfig = {
+        enable = true;
+        scopes = [ "global" ];
+        socketPath = "/var/run/dovecot2/old-stats";
+        user = "root"; # <- don't use user root in production
+      };
+      metricProvider = {
+        services.dovecot2.enable = true;
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-dovecot-exporter.service")
+        wait_for_open_port(9166)
+        succeed(
+            "curl -sSf http://localhost:9166/metrics | grep 'dovecot_up{scope=\"global\"} 1'"
+        )
+      '';
+    };
+
+    fastly = {
+      exporterConfig = {
+        enable = true;
+        tokenPath = pkgs.writeText "token" "abc123";
+      };
+
+      # noop: fastly's exporter can't start without first talking to fastly
+      # see: https://github.com/peterbourgon/fastly-exporter/issues/87
+      exporterTest = ''
+        succeed("true");
+      '';
+    };
+
+    fritzbox = {
+      # TODO add proper test case
+      exporterConfig = {
+        enable = true;
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-fritzbox-exporter.service")
+        wait_for_open_port(9133)
+        succeed(
+            "curl -sSf http://localhost:9133/metrics | grep 'fritzbox_exporter_collect_errors 0'"
+        )
+      '';
+    };
+
+    influxdb = {
+      exporterConfig = {
+        enable = true;
+        sampleExpiry = "3s";
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-influxdb-exporter.service")
+        wait_for_open_port(9122)
+        succeed(
+          "curl -XPOST http://localhost:9122/write --data-binary 'influxdb_exporter,distro=nixos,added_in=21.09 value=1'"
+        )
+        succeed(
+          "curl -sSf http://localhost:9122/metrics | grep 'nixos'"
+        )
+        execute("sleep 5")
+        fail(
+          "curl -sSf http://localhost:9122/metrics | grep 'nixos'"
+        )
+      '';
+    };
+
+    jitsi = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider = {
+        systemd.services.prometheus-jitsi-exporter.after = [ "jitsi-videobridge2.service" ];
+        services.jitsi-videobridge = {
+          enable = true;
+          apis = [ "colibri" "rest" ];
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("jitsi-videobridge2.service")
+        wait_for_open_port(8080)
+        wait_for_unit("prometheus-jitsi-exporter.service")
+        wait_for_open_port(9700)
+        wait_until_succeeds(
+            'journalctl -eu prometheus-jitsi-exporter.service -o cat | grep "key=participants"'
+        )
+        succeed("curl -sSf 'localhost:9700/metrics' | grep 'jitsi_participants 0'")
+      '';
+    };
+
+    json = {
+      exporterConfig = {
+        enable = true;
+        url = "http://localhost";
+        configFile = pkgs.writeText "json-exporter-conf.json" (builtins.toJSON {
+          metrics = [
+            { name = "json_test_metric"; path = "{ .test }"; }
+          ];
+        });
+      };
+      metricProvider = {
+        systemd.services.prometheus-json-exporter.after = [ "nginx.service" ];
+        services.nginx = {
+          enable = true;
+          virtualHosts.localhost.locations."/".extraConfig = ''
+            return 200 "{\"test\":1}";
+          '';
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("nginx.service")
+        wait_for_open_port(80)
+        wait_for_unit("prometheus-json-exporter.service")
+        wait_for_open_port(7979)
+        succeed(
+            "curl -sSf 'localhost:7979/probe?target=http://localhost' | grep 'json_test_metric 1'"
+        )
+      '';
+    };
+
+    kea = let
+      controlSocketPath = "/run/kea/dhcp6.sock";
+    in
+    {
+      exporterConfig = {
+        enable = true;
+        controlSocketPaths = [
+          controlSocketPath
+        ];
+      };
+      metricProvider = {
+        systemd.services.prometheus-kea-exporter.after = [ "kea-dhcp6-server.service" ];
+
+        services.kea = {
+          dhcp6 = {
+            enable = true;
+            settings = {
+              control-socket = {
+                socket-type = "unix";
+                socket-name = controlSocketPath;
+              };
+            };
+          };
+        };
+      };
+
+      exporterTest = ''
+        wait_for_unit("kea-dhcp6-server.service")
+        wait_for_file("${controlSocketPath}")
+        wait_for_unit("prometheus-kea-exporter.service")
+        wait_for_open_port(9547)
+        succeed(
+            "curl --fail localhost:9547/metrics | grep 'packets_received_total'"
+        )
+      '';
+    };
+
+    knot = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider = {
+        services.knot = {
+          enable = true;
+          extraArgs = [ "-v" ];
+          extraConfig = ''
+            server:
+              listen: 127.0.0.1@53
+
+            template:
+              - id: default
+                global-module: mod-stats
+                dnssec-signing: off
+                zonefile-sync: -1
+                journal-db: /var/lib/knot/journal
+                kasp-db: /var/lib/knot/kasp
+                timer-db: /var/lib/knot/timer
+                zonefile-load: difference
+                storage: ${pkgs.buildEnv {
+                  name = "foo";
+                  paths = [
+                    (pkgs.writeTextDir "test.zone" ''
+                      @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800
+                      @       NS      ns1
+                      @       NS      ns2
+                      ns1     A       192.168.0.1
+                    '')
+                  ];
+                }}
+
+            mod-stats:
+              - id: custom
+                edns-presence: on
+                query-type: on
+
+            zone:
+              - domain: test
+                file: test.zone
+                module: mod-stats/custom
+          '';
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("knot.service")
+        wait_for_unit("prometheus-knot-exporter.service")
+        wait_for_open_port(9433)
+        succeed("curl -sSf 'localhost:9433' | grep 'knot_server_zone_count 1.0'")
+      '';
+    };
+
+    keylight = {
+      # A hardware device is required to properly test this exporter, so just
+      # perform a couple of basic sanity checks that the exporter is running
+      # and requires a target, but cannot reach a specified target.
+      exporterConfig = {
+        enable = true;
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-keylight-exporter.service")
+        wait_for_open_port(9288)
+        succeed(
+            "curl -sS --write-out '%{http_code}' -o /dev/null http://localhost:9288/metrics | grep '400'"
+        )
+        succeed(
+            "curl -sS --write-out '%{http_code}' -o /dev/null http://localhost:9288/metrics?target=nosuchdevice | grep '500'"
+        )
+      '';
+    };
+
+    lnd = {
+      exporterConfig = {
+        enable = true;
+        lndTlsPath = "/var/lib/lnd/tls.cert";
+        lndMacaroonDir = "/var/lib/lnd";
+        extraFlags = [ "--lnd.network=regtest" ];
+      };
+      metricProvider = {
+        systemd.services.prometheus-lnd-exporter.serviceConfig.RestartSec = 15;
+        systemd.services.prometheus-lnd-exporter.after = [ "lnd.service" ];
+        services.bitcoind.regtest = {
+          enable = true;
+          extraConfig = ''
+            rpcauth=bitcoinrpc:e8fe33f797e698ac258c16c8d7aadfbe$872bdb8f4d787367c26bcfd75e6c23c4f19d44a69f5d1ad329e5adf3f82710f7
+            zmqpubrawblock=tcp://127.0.0.1:28332
+            zmqpubrawtx=tcp://127.0.0.1:28333
+          '';
+          extraCmdlineOptions = [ "-regtest" ];
+        };
+        systemd.services.lnd = {
+          serviceConfig.ExecStart = ''
+            ${pkgs.lnd}/bin/lnd \
+              --datadir=/var/lib/lnd \
+              --tlscertpath=/var/lib/lnd/tls.cert \
+              --tlskeypath=/var/lib/lnd/tls.key \
+              --logdir=/var/log/lnd \
+              --bitcoin.active \
+              --bitcoin.regtest \
+              --bitcoin.node=bitcoind \
+              --bitcoind.rpcuser=bitcoinrpc \
+              --bitcoind.rpcpass=hunter2 \
+              --bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332 \
+              --bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333 \
+              --readonlymacaroonpath=/var/lib/lnd/readonly.macaroon
+          '';
+          serviceConfig.StateDirectory = "lnd";
+          wantedBy = [ "multi-user.target" ];
+          after = [ "network.target" ];
+        };
+        # initialize wallet, creates macaroon needed by exporter
+        systemd.services.lnd.postStart = ''
+          ${pkgs.curl}/bin/curl \
+            --retry 20 \
+            --retry-delay 1 \
+            --retry-connrefused \
+            --cacert /var/lib/lnd/tls.cert \
+            -X GET \
+            https://localhost:8080/v1/genseed | ${pkgs.jq}/bin/jq -c '.cipher_seed_mnemonic' > /tmp/seed
+          ${pkgs.curl}/bin/curl \
+            --retry 20 \
+            --retry-delay 1 \
+            --retry-connrefused \
+            --cacert /var/lib/lnd/tls.cert \
+            -X POST \
+            -d "{\"wallet_password\": \"asdfasdfasdf\", \"cipher_seed_mnemonic\": $(cat /tmp/seed | tr -d '\n')}" \
+            https://localhost:8080/v1/initwallet
+        '';
+      };
+      exporterTest = ''
+        wait_for_unit("lnd.service")
+        wait_for_open_port(10009)
+        wait_for_unit("prometheus-lnd-exporter.service")
+        wait_for_open_port(9092)
+        succeed("curl -sSf localhost:9092/metrics | grep '^lnd_peer_count'")
+      '';
+    };
+
+    mail = {
+      exporterConfig = {
+        enable = true;
+        configuration = {
+          monitoringInterval = "2s";
+          mailCheckTimeout = "10s";
+          servers = [{
+            name = "testserver";
+            server = "localhost";
+            port = 25;
+            from = "mail-exporter@localhost";
+            to = "mail-exporter@localhost";
+            detectionDir = "/var/spool/mail/mail-exporter/new";
+          }];
+        };
+      };
+      metricProvider = {
+        services.postfix.enable = true;
+        systemd.services.prometheus-mail-exporter = {
+          after = [ "postfix.service" ];
+          requires = [ "postfix.service" ];
+          preStart = ''
+            mkdir -p -m 0700 mail-exporter/new
+          '';
+          serviceConfig = {
+            ProtectHome = true;
+            ReadOnlyPaths = "/";
+            ReadWritePaths = "/var/spool/mail";
+            WorkingDirectory = "/var/spool/mail";
+          };
+        };
+        users.users.mailexporter = {
+          isSystemUser = true;
+          group = "mailexporter";
+        };
+        users.groups.mailexporter = {};
+      };
+      exporterTest = ''
+        wait_for_unit("postfix.service")
+        wait_for_unit("prometheus-mail-exporter.service")
+        wait_for_open_port(9225)
+        wait_until_succeeds(
+            "curl -sSf http://localhost:9225/metrics | grep 'mail_deliver_success{configname=\"testserver\"} 1'"
+        )
+      '';
+    };
+
+    mikrotik = {
+      exporterConfig = {
+        enable = true;
+        extraFlags = [ "-timeout=1s" ];
+        configuration = {
+          devices = [
+            {
+              name = "router";
+              address = "192.168.42.48";
+              user = "prometheus";
+              password = "shh";
+            }
+          ];
+          features = {
+            bgp = true;
+            dhcp = true;
+            dhcpl = true;
+            dhcpv6 = true;
+            health = true;
+            routes = true;
+            poe = true;
+            pools = true;
+            optics = true;
+            w60g = true;
+            wlansta = true;
+            wlanif = true;
+            monitor = true;
+            ipsec = true;
+          };
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-mikrotik-exporter.service")
+        wait_for_open_port(9436)
+        succeed(
+            "curl -sSf http://localhost:9436/metrics | grep 'mikrotik_scrape_collector_success{device=\"router\"} 0'"
+        )
+      '';
+    };
+
+    modemmanager = {
+      exporterConfig = {
+        enable = true;
+        refreshRate = "10s";
+      };
+      metricProvider = {
+        # ModemManager is installed when NetworkManager is enabled. Ensure it is
+        # started and is wanted by NM and the exporter to start everything up
+        # in the right order.
+        networking.networkmanager.enable = true;
+        systemd.services.ModemManager = {
+          enable = true;
+          wantedBy = [ "NetworkManager.service" "prometheus-modemmanager-exporter.service" ];
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("ModemManager.service")
+        wait_for_unit("prometheus-modemmanager-exporter.service")
+        wait_for_open_port(9539)
+        succeed(
+            "curl -sSf http://localhost:9539/metrics | grep 'modemmanager_info'"
+        )
+      '';
+    };
+
+    nextcloud = {
+      exporterConfig = {
+        enable = true;
+        passwordFile = "/var/nextcloud-pwfile";
+        url = "http://localhost";
+      };
+      metricProvider = {
+        systemd.services.nc-pwfile =
+          let
+            passfile = (pkgs.writeText "pwfile" "snakeoilpw");
+          in
+          {
+            requiredBy = [ "prometheus-nextcloud-exporter.service" ];
+            before = [ "prometheus-nextcloud-exporter.service" ];
+            serviceConfig.ExecStart = ''
+              ${pkgs.coreutils}/bin/install -o nextcloud-exporter -m 0400 ${passfile} /var/nextcloud-pwfile
+            '';
+          };
+        services.nginx = {
+          enable = true;
+          virtualHosts."localhost" = {
+            basicAuth.nextcloud-exporter = "snakeoilpw";
+            locations."/" = {
+              root = "${pkgs.prometheus-nextcloud-exporter.src}/serverinfo/testdata";
+              tryFiles = "/negative-space.json =404";
+            };
+          };
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("nginx.service")
+        wait_for_unit("prometheus-nextcloud-exporter.service")
+        wait_for_open_port(9205)
+        succeed("curl -sSf http://localhost:9205/metrics | grep 'nextcloud_up 1'")
+      '';
+    };
+
+    nginx = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider = {
+        services.nginx = {
+          enable = true;
+          statusPage = true;
+          virtualHosts."test".extraConfig = "return 204;";
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("nginx.service")
+        wait_for_unit("prometheus-nginx-exporter.service")
+        wait_for_open_port(9113)
+        succeed("curl -sSf http://localhost:9113/metrics | grep 'nginx_up 1'")
+      '';
+    };
+
+    nginxlog = {
+      exporterConfig = {
+        enable = true;
+        group = "nginx";
+        settings = {
+          namespaces = [
+            {
+              name = "filelogger";
+              source = {
+                files = [ "/var/log/nginx/filelogger.access.log" ];
+              };
+            }
+            {
+              name = "syslogger";
+              source = {
+                syslog = {
+                  listen_address = "udp://127.0.0.1:10000";
+                  format = "rfc3164";
+                  tags = [ "nginx" ];
+                };
+              };
+            }
+          ];
+        };
+      };
+      metricProvider = {
+        services.nginx = {
+          enable = true;
+          httpConfig = ''
+            server {
+              listen 80;
+              server_name filelogger.local;
+              access_log /var/log/nginx/filelogger.access.log;
+            }
+            server {
+              listen 81;
+              server_name syslogger.local;
+              access_log syslog:server=127.0.0.1:10000,tag=nginx,severity=info;
+            }
+          '';
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("nginx.service")
+        wait_for_unit("prometheus-nginxlog-exporter.service")
+        wait_for_open_port(9117)
+        wait_for_open_port(80)
+        wait_for_open_port(81)
+        succeed("curl http://localhost")
+        execute("sleep 1")
+        succeed(
+            "curl -sSf http://localhost:9117/metrics | grep 'filelogger_http_response_count_total' | grep 1"
+        )
+        succeed("curl http://localhost:81")
+        execute("sleep 1")
+        succeed(
+            "curl -sSf http://localhost:9117/metrics | grep 'syslogger_http_response_count_total' | grep 1"
+        )
+      '';
+    };
+
+    node = {
+      exporterConfig = {
+        enable = true;
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-node-exporter.service")
+        wait_for_open_port(9100)
+        succeed(
+            "curl -sSf http://localhost:9100/metrics | grep 'node_exporter_build_info{.\\+} 1'"
+        )
+      '';
+    };
+
+    openldap = {
+      exporterConfig = {
+        enable = true;
+        ldapCredentialFile = "${pkgs.writeText "exporter.yml" ''
+          ldapUser: "cn=root,dc=example"
+          ldapPass: "notapassword"
+        ''}";
+      };
+      metricProvider = {
+        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/db/openldap";
+                olcSuffix = "dc=example";
+                olcRootDN = {
+                  # cn=root,dc=example
+                  base64 = "Y249cm9vdCxkYz1leGFtcGxl";
+                };
+                olcRootPW = {
+                  path = "${pkgs.writeText "rootpw" "notapassword"}";
+                };
+              };
+            };
+            "olcDatabase={2}monitor".attrs = {
+              objectClass = [ "olcDatabaseConfig" ];
+              olcDatabase = "{2}monitor";
+              olcAccess = [ "to dn.subtree=cn=monitor by users read" ];
+            };
+          };
+          declarativeContents."dc=example" = ''
+            dn: dc=example
+            objectClass: domain
+            dc: example
+
+            dn: ou=users,dc=example
+            objectClass: organizationalUnit
+            ou: users
+          '';
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-openldap-exporter.service")
+        wait_for_open_port(389)
+        wait_for_open_port(9330)
+        wait_until_succeeds(
+            "curl -sSf http://localhost:9330/metrics | grep 'openldap_scrape{result=\"ok\"} 1'"
+        )
+      '';
+    };
+
+    openvpn = {
+      exporterConfig = {
+        enable = true;
+        group = "openvpn";
+        statusPaths = [ "/run/openvpn-test" ];
+      };
+      metricProvider = {
+        users.groups.openvpn = { };
+        services.openvpn.servers.test = {
+          config = ''
+            dev tun
+            status /run/openvpn-test
+            status-version 3
+          '';
+          up = "chmod g+r /run/openvpn-test";
+        };
+        systemd.services."openvpn-test".serviceConfig.Group = "openvpn";
+      };
+      exporterTest = ''
+        wait_for_unit("openvpn-test.service")
+        wait_for_unit("prometheus-openvpn-exporter.service")
+        succeed("curl -sSf http://localhost:9176/metrics | grep 'openvpn_up{.*} 1'")
+      '';
+    };
+
+    postfix = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider = {
+        services.postfix.enable = true;
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-postfix-exporter.service")
+        wait_for_file("/var/lib/postfix/queue/public/showq")
+        wait_for_open_port(9154)
+        wait_until_succeeds(
+            "curl -sSf http://localhost:9154/metrics | grep 'postfix_up{path=\"/var/lib/postfix/queue/public/showq\"} 1'"
+        )
+        succeed(
+            "curl -sSf http://localhost:9154/metrics | grep 'postfix_smtpd_connects_total 0'"
+        )
+        succeed("curl -sSf http://localhost:9154/metrics | grep 'postfix_up{.*} 1'")
+      '';
+    };
+
+    postgres = {
+      exporterConfig = {
+        enable = true;
+        runAsLocalSuperUser = true;
+      };
+      metricProvider = {
+        services.postgresql.enable = true;
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-postgres-exporter.service")
+        wait_for_open_port(9187)
+        wait_for_unit("postgresql.service")
+        succeed(
+            "curl -sSf http://localhost:9187/metrics | grep 'pg_exporter_last_scrape_error 0'"
+        )
+        succeed("curl -sSf http://localhost:9187/metrics | grep 'pg_up 1'")
+        systemctl("stop postgresql.service")
+        succeed(
+            "curl -sSf http://localhost:9187/metrics | grep -v 'pg_exporter_last_scrape_error 0'"
+        )
+        succeed("curl -sSf http://localhost:9187/metrics | grep 'pg_up 0'")
+        systemctl("start postgresql.service")
+        wait_for_unit("postgresql.service")
+        succeed(
+            "curl -sSf http://localhost:9187/metrics | grep 'pg_exporter_last_scrape_error 0'"
+        )
+        succeed("curl -sSf http://localhost:9187/metrics | grep 'pg_up 1'")
+      '';
+    };
+
+    process = {
+      exporterConfig = {
+        enable = true;
+        settings.process_names = [
+          # Remove nix store path from process name
+          { name = "{{.Matches.Wrapped}} {{ .Matches.Args }}"; cmdline = [ "^/nix/store[^ ]*/(?P<Wrapped>[^ /]*) (?P<Args>.*)" ]; }
+        ];
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-process-exporter.service")
+        wait_for_open_port(9256)
+        wait_until_succeeds(
+            "curl -sSf localhost:9256/metrics | grep -q '{}'".format(
+                'namedprocess_namegroup_cpu_seconds_total{groupname="process-exporter '
+            )
+        )
+      '';
+    };
+
+    pve = let
+      pveExporterEnvFile = pkgs.writeTextFile {
+        name = "pve.env";
+        text = ''
+          PVE_USER="test_user@pam"
+          PVE_PASSWORD="hunter3"
+          PVE_VERIFY_SSL="false"
+        '';
+      };
+    in {
+      exporterConfig = {
+        enable = true;
+        environmentFile = pveExporterEnvFile;
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-pve-exporter.service")
+        wait_for_open_port(9221)
+        wait_until_succeeds("curl localhost:9221")
+      '';
+    };
+
+    py-air-control = {
+      nodeName = "py_air_control";
+      exporterConfig = {
+        enable = true;
+        deviceHostname = "127.0.0.1";
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-py-air-control-exporter.service")
+        wait_for_open_port(9896)
+        succeed(
+            "curl -sSf http://localhost:9896/metrics | grep 'py_air_control_sampling_error_total'"
+        )
+      '';
+    };
+
+    redis = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider.services.redis.servers."".enable = true;
+      exporterTest = ''
+        wait_for_unit("redis.service")
+        wait_for_unit("prometheus-redis-exporter.service")
+        wait_for_open_port(6379)
+        wait_for_open_port(9121)
+        wait_until_succeeds("curl -sSf localhost:9121/metrics | grep 'redis_up 1'")
+      '';
+    };
+
+    rspamd = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider = {
+        services.rspamd.enable = true;
+      };
+      exporterTest = ''
+        wait_for_unit("rspamd.service")
+        wait_for_unit("prometheus-rspamd-exporter.service")
+        wait_for_open_port(11334)
+        wait_for_open_port(7980)
+        wait_until_succeeds(
+            "curl -sSf 'localhost:7980/probe?target=http://localhost:11334/stat' | grep 'rspamd_scanned{host=\"rspamd\"} 0'"
+        )
+      '';
+    };
+
+    rtl_433 = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider = {
+        # Mock rtl_433 binary to return a dummy metric stream.
+        nixpkgs.overlays = [
+          (self: super: {
+            rtl_433 = self.runCommand "rtl_433" { } ''
+              mkdir -p "$out/bin"
+              cat <<EOF > "$out/bin/rtl_433"
+              #!/bin/sh
+              while true; do
+                printf '{"time" : "2020-04-26 13:37:42", "model" : "zopieux", "id" : 55, "channel" : 3, "temperature_C" : 18.000}\n'
+                sleep 4
+              done
+              EOF
+              chmod +x "$out/bin/rtl_433"
+            '';
+          })
+        ];
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-rtl_433-exporter.service")
+        wait_for_open_port(9550)
+        wait_until_succeeds(
+            "curl -sSf localhost:9550/metrics | grep '{}'".format(
+                'rtl_433_temperature_celsius{channel="3",id="55",location="",model="zopieux"} 18'
+            )
+        )
+      '';
+    };
+
+    script = {
+      exporterConfig = {
+        enable = true;
+        settings.scripts = [
+          { name = "success"; script = "sleep 1"; }
+        ];
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-script-exporter.service")
+        wait_for_open_port(9172)
+        wait_until_succeeds(
+            "curl -sSf 'localhost:9172/probe?name=success' | grep -q '{}'".format(
+                'script_success{script="success"} 1'
+            )
+        )
+      '';
+    };
+
+    smartctl = {
+      exporterConfig = {
+        enable = true;
+        devices = [
+          "/dev/vda"
+        ];
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-smartctl-exporter.service")
+        wait_for_open_port("9633")
+        wait_until_succeeds(
+          "curl -sSf 'localhost:9633/metrics'"
+        )
+        wait_until_succeeds(
+            'journalctl -eu prometheus-smartctl-exporter.service -o cat | grep "/dev/vda: Unable to detect device type"'
+        )
+      '';
+    };
+
+    smokeping = {
+      exporterConfig = {
+        enable = true;
+        hosts = [ "127.0.0.1" ];
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-smokeping-exporter.service")
+        wait_for_open_port(9374)
+        wait_until_succeeds(
+            "curl -sSf localhost:9374/metrics | grep '{}' | grep -v ' 0$'".format(
+                'smokeping_requests_total{host="127.0.0.1",ip="127.0.0.1"} '
+            )
+        )
+        wait_until_succeeds(
+            "curl -sSf localhost:9374/metrics | grep '{}'".format(
+                'smokeping_response_ttl{host="127.0.0.1",ip="127.0.0.1"}'
+            )
+        )
+      '';
+    };
+
+    snmp = {
+      exporterConfig = {
+        enable = true;
+        configuration.default = {
+          version = 2;
+          auth.community = "public";
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-snmp-exporter.service")
+        wait_for_open_port(9116)
+        succeed("curl -sSf localhost:9116/metrics | grep 'snmp_request_errors_total 0'")
+      '';
+    };
+
+    sql = {
+      exporterConfig = {
+        configuration.jobs.points = {
+          interval = "1m";
+          connections = [
+            "postgres://prometheus-sql-exporter@/data?host=/run/postgresql&sslmode=disable"
+          ];
+          queries = {
+            points = {
+              labels = [ "name" ];
+              help = "Amount of points accumulated per person";
+              values = [ "amount" ];
+              query = "SELECT SUM(amount) as amount, name FROM points GROUP BY name";
+            };
+          };
+        };
+        enable = true;
+        user = "prometheus-sql-exporter";
+      };
+      metricProvider = {
+        services.postgresql = {
+          enable = true;
+          initialScript = builtins.toFile "init.sql" ''
+            CREATE DATABASE data;
+            \c data;
+            CREATE TABLE points (amount INT, name TEXT);
+            INSERT INTO points(amount, name) VALUES (1, 'jack');
+            INSERT INTO points(amount, name) VALUES (2, 'jill');
+            INSERT INTO points(amount, name) VALUES (3, 'jack');
+
+            CREATE USER "prometheus-sql-exporter";
+            GRANT ALL PRIVILEGES ON DATABASE data TO "prometheus-sql-exporter";
+            GRANT SELECT ON points TO "prometheus-sql-exporter";
+          '';
+        };
+        systemd.services.prometheus-sql-exporter.after = [ "postgresql.service" ];
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-sql-exporter.service")
+        wait_for_open_port(9237)
+        succeed("curl http://localhost:9237/metrics | grep -c 'sql_points{' | grep 2")
+      '';
+    };
+
+    surfboard = {
+      exporterConfig = {
+        enable = true;
+        modemAddress = "localhost";
+      };
+      metricProvider = {
+        systemd.services.prometheus-surfboard-exporter.after = [ "nginx.service" ];
+        services.nginx = {
+          enable = true;
+          virtualHosts.localhost.locations."/cgi-bin/status".extraConfig = ''
+            return 204;
+          '';
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("nginx.service")
+        wait_for_open_port(80)
+        wait_for_unit("prometheus-surfboard-exporter.service")
+        wait_for_open_port(9239)
+        succeed("curl -sSf localhost:9239/metrics | grep 'surfboard_up 1'")
+      '';
+    };
+
+    systemd = {
+      exporterConfig = {
+        enable = true;
+
+        extraFlags = [
+          "--collector.enable-restart-count"
+        ];
+      };
+      metricProvider = { };
+      exporterTest = ''
+        wait_for_unit("prometheus-systemd-exporter.service")
+        wait_for_open_port(9558)
+        succeed(
+            "curl -sSf localhost:9558/metrics | grep '{}'".format(
+                'systemd_unit_state{name="basic.target",state="active",type="target"} 1'
+            )
+        )
+        succeed(
+            "curl -sSf localhost:9558/metrics | grep '{}'".format(
+                'systemd_service_restart_total{state="prometheus-systemd-exporter.service"} 0'
+            )
+        )
+      '';
+    };
+
+    tor = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider = {
+        # Note: this does not connect the test environment to the Tor network.
+        # Client, relay, bridge or exit connectivity are disabled by default.
+        services.tor.enable = true;
+        services.tor.settings.ControlPort = 9051;
+      };
+      exporterTest = ''
+        wait_for_unit("tor.service")
+        wait_for_open_port(9051)
+        wait_for_unit("prometheus-tor-exporter.service")
+        wait_for_open_port(9130)
+        succeed("curl -sSf localhost:9130/metrics | grep 'tor_version{.\\+} 1'")
+      '';
+    };
+
+    unifi-poller = {
+      nodeName = "unifi_poller";
+      exporterConfig.enable = true;
+      exporterConfig.controllers = [{ }];
+      exporterTest = ''
+        wait_for_unit("prometheus-unifi-poller-exporter.service")
+        wait_for_open_port(9130)
+        succeed(
+            "curl -sSf localhost:9130/metrics | grep 'unifipoller_build_info{.\\+} 1'"
+        )
+      '';
+    };
+
+    unbound = {
+      exporterConfig = {
+        enable = true;
+        fetchType = "uds";
+        controlInterface = "/run/unbound/unbound.ctl";
+      };
+      metricProvider = {
+        services.unbound = {
+          enable = true;
+          localControlSocketPath = "/run/unbound/unbound.ctl";
+        };
+        systemd.services.prometheus-unbound-exporter.serviceConfig = {
+          SupplementaryGroups = [ "unbound" ];
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("unbound.service")
+        wait_for_unit("prometheus-unbound-exporter.service")
+        wait_for_open_port(9167)
+        succeed("curl -sSf localhost:9167/metrics | grep 'unbound_up 1'")
+      '';
+    };
+
+    varnish = {
+      exporterConfig = {
+        enable = true;
+        instance = "/var/spool/varnish/varnish";
+        group = "varnish";
+      };
+      metricProvider = {
+        systemd.services.prometheus-varnish-exporter.after = [
+          "varnish.service"
+        ];
+        services.varnish = {
+          enable = true;
+          config = ''
+            vcl 4.0;
+            backend default {
+              .host = "127.0.0.1";
+              .port = "80";
+            }
+          '';
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-varnish-exporter.service")
+        wait_for_open_port(6081)
+        wait_for_open_port(9131)
+        succeed("curl -sSf http://localhost:9131/metrics | grep 'varnish_up 1'")
+      '';
+    };
+
+    wireguard = let snakeoil = import ./wireguard/snakeoil-keys.nix; in
+      {
+        exporterConfig.enable = true;
+        metricProvider = {
+          networking.wireguard.interfaces.wg0 = {
+            ips = [ "10.23.42.1/32" "fc00::1/128" ];
+            listenPort = 23542;
+
+            inherit (snakeoil.peer0) privateKey;
+
+            peers = singleton {
+              allowedIPs = [ "10.23.42.2/32" "fc00::2/128" ];
+
+              inherit (snakeoil.peer1) publicKey;
+            };
+          };
+          systemd.services.prometheus-wireguard-exporter.after = [ "wireguard-wg0.service" ];
+        };
+        exporterTest = ''
+          wait_for_unit("prometheus-wireguard-exporter.service")
+          wait_for_open_port(9586)
+          wait_until_succeeds(
+              "curl -sSf http://localhost:9586/metrics | grep '${snakeoil.peer1.publicKey}'"
+          )
+        '';
+      };
+  };
+in
+mapAttrs
+  (exporter: testConfig: (makeTest (
+    let
+      nodeName = testConfig.nodeName or exporter;
+
+    in
+    {
+      name = "prometheus-${exporter}-exporter";
+
+      nodes.${nodeName} = mkMerge [{
+        services.prometheus.exporters.${exporter} = testConfig.exporterConfig;
+      } testConfig.metricProvider or { }];
+
+      testScript = ''
+        ${nodeName}.start()
+        ${concatStringsSep "\n" (map (line:
+          if (builtins.substring 0 1 line == " " || builtins.substring 0 1 line == ")")
+          then line
+          else "${nodeName}.${line}"
+        ) (splitString "\n" (removeSuffix "\n" testConfig.exporterTest)))}
+        ${nodeName}.shutdown()
+      '';
+
+      meta = with maintainers; {
+        maintainers = [ willibutz elseym ];
+      };
+    }
+  )))
+  exporterTests
diff --git a/nixos/tests/prometheus.nix b/nixos/tests/prometheus.nix
new file mode 100644
index 00000000000..a075cfc1f1b
--- /dev/null
+++ b/nixos/tests/prometheus.nix
@@ -0,0 +1,339 @@
+let
+  grpcPort   = 19090;
+  queryPort  =  9090;
+  minioPort  =  9000;
+  pushgwPort =  9091;
+
+  s3 = {
+    accessKey = "BKIKJAA5BMMU2RHO6IBB";
+    secretKey = "V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12";
+  };
+
+  objstore.config = {
+    type = "S3";
+    config = {
+      bucket = "thanos-bucket";
+      endpoint = "s3:${toString minioPort}";
+      region =  "us-east-1";
+      access_key = s3.accessKey;
+      secret_key = s3.secretKey;
+      insecure = true;
+      signature_version2 = false;
+      put_user_metadata = {};
+      http_config = {
+        idle_conn_timeout = "0s";
+        insecure_skip_verify = false;
+      };
+      trace = {
+        enable = false;
+      };
+    };
+  };
+
+in import ./make-test-python.nix {
+  name = "prometheus";
+
+  nodes = {
+    prometheus = { pkgs, ... }: {
+      virtualisation.diskSize = 2 * 1024;
+      virtualisation.memorySize = 2048;
+      environment.systemPackages = [ pkgs.jq ];
+      networking.firewall.allowedTCPPorts = [ grpcPort ];
+      services.prometheus = {
+        enable = true;
+        enableReload = true;
+        scrapeConfigs = [
+          {
+            job_name = "prometheus";
+            static_configs = [
+              {
+                targets = [ "127.0.0.1:${toString queryPort}" ];
+                labels = { instance = "localhost"; };
+              }
+            ];
+          }
+          {
+            job_name = "pushgateway";
+            scrape_interval = "1s";
+            static_configs = [
+              {
+                targets = [ "127.0.0.1:${toString pushgwPort}" ];
+              }
+            ];
+          }
+        ];
+        rules = [
+          ''
+            groups:
+              - name: test
+                rules:
+                  - record: testrule
+                    expr: count(up{job="prometheus"})
+          ''
+        ];
+        globalConfig = {
+          external_labels = {
+            some_label = "required by thanos";
+          };
+        };
+        extraFlags = [
+          # Required by thanos
+          "--storage.tsdb.min-block-duration=5s"
+          "--storage.tsdb.max-block-duration=5s"
+        ];
+      };
+      services.prometheus.pushgateway = {
+        enable = true;
+        web.listen-address = ":${toString pushgwPort}";
+        persistMetrics = true;
+        persistence.interval = "1s";
+        stateDir = "prometheus-pushgateway";
+      };
+      services.thanos = {
+        sidecar = {
+          enable = true;
+          grpc-address = "0.0.0.0:${toString grpcPort}";
+          inherit objstore;
+        };
+
+        # TODO: Add some tests for these services:
+        #rule = {
+        #  enable = true;
+        #  http-address = "0.0.0.0:19194";
+        #  grpc-address = "0.0.0.0:19193";
+        #  query.addresses = [
+        #    "localhost:19191"
+        #  ];
+        #  labels = {
+        #    just = "some";
+        #    nice = "labels";
+        #  };
+        #};
+        #
+        #receive = {
+        #  http-address = "0.0.0.0:19195";
+        #  enable = true;
+        #  labels = {
+        #    just = "some";
+        #    nice = "labels";
+        #  };
+        #};
+      };
+      # Adds a "specialisation" of the above config which allows us to
+      # "switch" to it and see if the services.prometheus.enableReload
+      # functionality actually reloads the prometheus service instead of
+      # restarting it.
+      specialisation = {
+        "prometheus-config-change" = {
+          configuration = {
+            environment.systemPackages = [ pkgs.yq ];
+
+            # This configuration just adds a new prometheus job
+            # to scrape the node_exporter metrics of the s3 machine.
+            services.prometheus = {
+              scrapeConfigs = [
+                {
+                  job_name = "s3-node_exporter";
+                  static_configs = [
+                    {
+                      targets = [ "s3:9100" ];
+                    }
+                  ];
+                }
+              ];
+            };
+          };
+        };
+      };
+    };
+
+    query = { pkgs, ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+      services.thanos.query = {
+        enable = true;
+        http-address = "0.0.0.0:${toString queryPort}";
+        store.addresses = [
+          "prometheus:${toString grpcPort}"
+        ];
+      };
+    };
+
+    store = { pkgs, ... }: {
+      virtualisation.diskSize = 2 * 1024;
+      virtualisation.memorySize = 2048;
+      environment.systemPackages = with pkgs; [ jq thanos ];
+      services.thanos.store = {
+        enable = true;
+        http-address = "0.0.0.0:10902";
+        grpc-address = "0.0.0.0:${toString grpcPort}";
+        inherit objstore;
+        sync-block-duration = "1s";
+      };
+      services.thanos.compact = {
+        enable = true;
+        http-address = "0.0.0.0:10903";
+        inherit objstore;
+        consistency-delay = "5s";
+      };
+      services.thanos.query = {
+        enable = true;
+        http-address = "0.0.0.0:${toString queryPort}";
+        store.addresses = [
+          "localhost:${toString grpcPort}"
+        ];
+      };
+    };
+
+    s3 = { pkgs, ... } : {
+      # Minio requires at least 1GiB of free disk space to run.
+      virtualisation = {
+        diskSize = 2 * 1024;
+      };
+      networking.firewall.allowedTCPPorts = [ minioPort ];
+
+      services.minio = {
+        enable = true;
+        inherit (s3) accessKey secretKey;
+      };
+
+      environment.systemPackages = [ pkgs.minio-client ];
+
+      services.prometheus.exporters.node = {
+        enable = true;
+        openFirewall = true;
+      };
+    };
+  };
+
+  testScript = { nodes, ... } : ''
+    import json
+
+    # Before starting the other machines we first make sure that our S3 service is online
+    # and has a bucket added for thanos:
+    s3.start()
+    s3.wait_for_unit("minio.service")
+    s3.wait_for_open_port(${toString minioPort})
+    s3.succeed(
+        "mc config host add minio "
+        + "http://localhost:${toString minioPort} "
+        + "${s3.accessKey} ${s3.secretKey} --api s3v4",
+        "mc mb minio/thanos-bucket",
+    )
+
+    # Now that s3 has started we can start the other machines:
+    for machine in prometheus, query, store:
+        machine.start()
+
+    # Check if prometheus responds to requests:
+    prometheus.wait_for_unit("prometheus.service")
+
+    prometheus.wait_for_open_port(${toString queryPort})
+    prometheus.succeed("curl -sf http://127.0.0.1:${toString queryPort}/metrics")
+
+    # Let's test if pushing a metric to the pushgateway succeeds:
+    prometheus.wait_for_unit("pushgateway.service")
+    prometheus.succeed(
+        "echo 'some_metric 3.14' | "
+        + "curl -f --data-binary \@- "
+        + "http://127.0.0.1:${toString pushgwPort}/metrics/job/some_job"
+    )
+
+    # Now check whether that metric gets ingested by prometheus.
+    # Since we'll check for the metric several times on different machines
+    # we abstract the test using the following function:
+
+    # Function to check if the metric "some_metric" has been received and returns the correct value.
+    def wait_for_metric(machine):
+        return machine.wait_until_succeeds(
+            "curl -sf 'http://127.0.0.1:${toString queryPort}/api/v1/query?query=some_metric' | "
+            + "jq '.data.result[0].value[1]' | grep '\"3.14\"'"
+        )
+
+
+    wait_for_metric(prometheus)
+
+    # Let's test if the pushgateway persists metrics to the configured location.
+    prometheus.wait_until_succeeds("test -e /var/lib/prometheus-pushgateway/metrics")
+
+    # Test thanos
+    prometheus.wait_for_unit("thanos-sidecar.service")
+
+    # Test if the Thanos query service can correctly retrieve the metric that was send above.
+    query.wait_for_unit("thanos-query.service")
+    wait_for_metric(query)
+
+    # Test if the Thanos sidecar has correctly uploaded its TSDB to S3, if the
+    # Thanos storage service has correctly downloaded it from S3 and if the Thanos
+    # query service running on $store can correctly retrieve the metric:
+    store.wait_for_unit("thanos-store.service")
+    wait_for_metric(store)
+
+    store.wait_for_unit("thanos-compact.service")
+
+    # Test if the Thanos bucket command is able to retrieve blocks from the S3 bucket
+    # and check if the blocks have the correct labels:
+    store.succeed(
+        "thanos tools bucket ls "
+        + "--objstore.config-file=${nodes.store.config.services.thanos.store.objstore.config-file} "
+        + "--output=json | "
+        + "jq .thanos.labels.some_label | "
+        + "grep 'required by thanos'"
+    )
+
+    # Check if switching to a NixOS configuration that changes the prometheus
+    # configuration reloads (instead of restarts) prometheus before the switch
+    # finishes successfully:
+    with subtest("config change reloads prometheus"):
+        # We check if prometheus has finished reloading by looking for the message
+        # "Completed loading of configuration file" in the journal between the start
+        # and finish of switching to the new NixOS configuration.
+        #
+        # To mark the start we record the journal cursor before starting the switch:
+        cursor_before_switching = json.loads(
+            prometheus.succeed("journalctl -n1 -o json --output-fields=__CURSOR")
+        )["__CURSOR"]
+
+        # Now we switch:
+        prometheus_config_change = prometheus.succeed(
+            "readlink /run/current-system/specialisation/prometheus-config-change"
+        ).strip()
+        prometheus.succeed(prometheus_config_change + "/bin/switch-to-configuration test")
+
+        # Next we retrieve all logs since the start of switching:
+        logs_after_starting_switching = prometheus.succeed(
+            """
+              journalctl --after-cursor='{cursor_before_switching}' -o json --output-fields=MESSAGE
+            """.format(
+                cursor_before_switching=cursor_before_switching
+            )
+        )
+
+        # Finally we check if the message "Completed loading of configuration file"
+        # occurs before the "finished switching to system configuration" message:
+        finished_switching_msg = (
+            "finished switching to system configuration " + prometheus_config_change
+        )
+        reloaded_before_switching_finished = False
+        finished_switching = False
+        for log_line in logs_after_starting_switching.split("\n"):
+            msg = json.loads(log_line)["MESSAGE"]
+            if "Completed loading of configuration file" in msg:
+                reloaded_before_switching_finished = True
+            if msg == finished_switching_msg:
+                finished_switching = True
+                break
+
+        assert reloaded_before_switching_finished
+        assert finished_switching
+
+        # Check if the reloaded config includes the new s3-node_exporter job:
+        prometheus.succeed(
+          """
+            curl -sf http://127.0.0.1:${toString queryPort}/api/v1/status/config \
+              | jq -r .data.yaml \
+              | yq '.scrape_configs | any(.job_name == "s3-node_exporter")' \
+              | grep true
+          """
+        )
+  '';
+}
diff --git a/nixos/tests/prowlarr.nix b/nixos/tests/prowlarr.nix
new file mode 100644
index 00000000000..4cbca107568
--- /dev/null
+++ b/nixos/tests/prowlarr.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "prowlarr";
+  meta.maintainers = with maintainers; [ jdreaver ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    { services.prowlarr.enable = true; };
+
+  testScript = ''
+    machine.wait_for_unit("prowlarr.service")
+    machine.wait_for_open_port("9696")
+    machine.succeed("curl --fail http://localhost:9696/")
+  '';
+})
diff --git a/nixos/tests/proxy.nix b/nixos/tests/proxy.nix
new file mode 100644
index 00000000000..f8a3d576903
--- /dev/null
+++ b/nixos/tests/proxy.nix
@@ -0,0 +1,90 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+let
+  backend = { pkgs, ... }: {
+    services.httpd = {
+      enable = true;
+      adminAddr = "foo@example.org";
+      virtualHosts.localhost.documentRoot = "${pkgs.valgrind.doc}/share/doc/valgrind/html";
+    };
+    networking.firewall.allowedTCPPorts = [ 80 ];
+  };
+in {
+  name = "proxy";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eelco ];
+  };
+
+  nodes = {
+    proxy = { nodes, ... }: {
+      services.httpd = {
+        enable = true;
+        adminAddr = "bar@example.org";
+        extraModules = [ "proxy_balancer" "lbmethod_byrequests" ];
+        extraConfig = ''
+          ExtendedStatus on
+        '';
+        virtualHosts.localhost = {
+          extraConfig = ''
+            <Location /server-status>
+              Require all granted
+              SetHandler server-status
+            </Location>
+
+            <Proxy balancer://cluster>
+              Require all granted
+              BalancerMember http://${nodes.backend1.config.networking.hostName} retry=0
+              BalancerMember http://${nodes.backend2.config.networking.hostName} retry=0
+            </Proxy>
+
+            ProxyStatus       full
+            ProxyPass         /server-status !
+            ProxyPass         /       balancer://cluster/
+            ProxyPassReverse  /       balancer://cluster/
+
+            # For testing; don't want to wait forever for dead backend servers.
+            ProxyTimeout      5
+          '';
+        };
+      };
+      networking.firewall.allowedTCPPorts = [ 80 ];
+    };
+
+    backend1 = backend;
+    backend2 = backend;
+
+    client = { ... }: { };
+  };
+
+  testScript = ''
+    start_all()
+
+    proxy.wait_for_unit("httpd")
+    backend1.wait_for_unit("httpd")
+    backend2.wait_for_unit("httpd")
+    client.wait_for_unit("network.target")
+
+    # With the back-ends up, the proxy should work.
+    client.succeed("curl --fail http://proxy/")
+
+    client.succeed("curl --fail http://proxy/server-status")
+
+    # Block the first back-end.
+    backend1.block()
+
+    # The proxy should still work.
+    client.succeed("curl --fail http://proxy/")
+    client.succeed("curl --fail http://proxy/")
+
+    # Block the second back-end.
+    backend2.block()
+
+    # Now the proxy should fail as well.
+    client.fail("curl --fail http://proxy/")
+
+    # But if the second back-end comes back, the proxy should start
+    # working again.
+    backend2.unblock()
+    client.succeed("curl --fail http://proxy/")
+  '';
+})
diff --git a/nixos/tests/pt2-clone.nix b/nixos/tests/pt2-clone.nix
new file mode 100644
index 00000000000..364920c3987
--- /dev/null
+++ b/nixos/tests/pt2-clone.nix
@@ -0,0 +1,35 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "pt2-clone";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fgaz ];
+  };
+
+  machine = { config, pkgs, ... }: {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    sound.enable = true;
+    environment.systemPackages = [ pkgs.pt2-clone ];
+  };
+
+  enableOCR = true;
+
+  testScript =
+    ''
+      machine.wait_for_x()
+      # Add a dummy sound card, or the program won't start
+      machine.execute("modprobe snd-dummy")
+
+      machine.execute("pt2-clone >&2 &")
+
+      machine.wait_for_window(r"ProTracker")
+      machine.sleep(5)
+      # One of the few words that actually get recognized
+      if "LENGTH" not in machine.get_screen_text():
+          raise Exception("Program did not start successfully")
+      machine.screenshot("screen")
+    '';
+})
+
diff --git a/nixos/tests/pulseaudio.nix b/nixos/tests/pulseaudio.nix
new file mode 100644
index 00000000000..4e2ce679acd
--- /dev/null
+++ b/nixos/tests/pulseaudio.nix
@@ -0,0 +1,71 @@
+let
+  mkTest = { systemWide ? false }:
+    import ./make-test-python.nix ({ pkgs, lib, ... }:
+      let
+        testFile = pkgs.fetchurl {
+          url =
+            "https://file-examples-com.github.io/uploads/2017/11/file_example_MP3_700KB.mp3";
+          hash = "sha256-+iggJW8s0/LfA/okfXsB550/55Q0Sq3OoIzuBrzOPJQ=";
+        };
+
+        makeTestPlay = key:
+          { sox, alsa-utils }:
+          pkgs.writeScriptBin key ''
+            set -euxo pipefail
+            ${sox}/bin/play ${testFile}
+            ${sox}/bin/sox ${testFile} -t wav - | ${alsa-utils}/bin/aplay
+            touch /tmp/${key}_success
+          '';
+
+        testers = builtins.mapAttrs makeTestPlay {
+          testPlay = { inherit (pkgs) sox alsa-utils; };
+          testPlay32 = { inherit (pkgs.pkgsi686Linux) sox alsa-utils; };
+        };
+      in {
+        name = "pulseaudio${lib.optionalString systemWide "-systemWide"}";
+        meta = with pkgs.lib.maintainers; {
+          maintainers = [ synthetica ] ++ pkgs.pulseaudio.meta.maintainers;
+        };
+
+        machine = { ... }:
+
+          {
+            imports = [ ./common/wayland-cage.nix ];
+            hardware.pulseaudio = {
+              enable = true;
+              support32Bit = true;
+              inherit systemWide;
+            };
+
+            environment.systemPackages = [ testers.testPlay pkgs.pavucontrol ]
+              ++ lib.optional pkgs.stdenv.isx86_64 testers.testPlay32;
+          } // lib.optionalAttrs systemWide {
+            users.users.alice.extraGroups = [ "audio" ];
+            systemd.services.pulseaudio.wantedBy = [ "multi-user.target" ];
+          };
+
+        enableOCR = true;
+
+        testScript = { ... }: ''
+          machine.wait_until_succeeds("pgrep xterm")
+          machine.wait_for_text("alice@machine")
+
+          machine.send_chars("testPlay \n")
+          machine.wait_for_file("/tmp/testPlay_success")
+          ${lib.optionalString pkgs.stdenv.isx86_64 ''
+            machine.send_chars("testPlay32 \n")
+            machine.wait_for_file("/tmp/testPlay32_success")
+          ''}
+          machine.screenshot("testPlay")
+
+          # Pavucontrol only loads when Pulseaudio is running. If it isn't, the
+          # text "Playback" (one of the tabs) will never show.
+          machine.send_chars("pavucontrol\n")
+          machine.wait_for_text("Playback")
+          machine.screenshot("Pavucontrol")
+        '';
+      });
+in builtins.mapAttrs (key: val: mkTest val) {
+  user = { systemWide = false; };
+  system = { systemWide = true; };
+}
diff --git a/nixos/tests/qboot.nix b/nixos/tests/qboot.nix
new file mode 100644
index 00000000000..12aef6decfa
--- /dev/null
+++ b/nixos/tests/qboot.nix
@@ -0,0 +1,13 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "qboot";
+
+  machine = { ... }: {
+    virtualisation.bios = pkgs.qboot;
+  };
+
+  testScript =
+    ''
+      start_all()
+      machine.wait_for_unit("multi-user.target")
+    '';
+})
diff --git a/nixos/tests/quorum.nix b/nixos/tests/quorum.nix
new file mode 100644
index 00000000000..31669eb7fc3
--- /dev/null
+++ b/nixos/tests/quorum.nix
@@ -0,0 +1,102 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+let
+  keystore =  {
+    address = "9377bc3936de934c497e22917b81aa8774ac3bb0";
+    crypto = {
+      cipher = "aes-128-ctr";
+      ciphertext = "ad8341d8ef225650403fd366c955f41095e438dd966a3c84b3d406818c1e366c";
+      cipherparams = {
+        iv = "2a09f7a72fd6dff7c43150ff437e6ac2";
+      };
+      kdf = "scrypt";
+      kdfparams = {
+        dklen = 32;
+        n = 262144;
+        p = 1;
+        r = 8;
+        salt = "d1a153845bb80cd6274c87c5bac8ac09fdfac5ff131a6f41b5ed319667f12027";
+      };
+      mac = "a9621ad88fa1d042acca6fc2fcd711f7e05bfbadea3f30f379235570c8e270d3";
+    };
+    id = "89e847a3-1527-42f6-a321-77de0a14ce02";
+    version = 3;
+  };
+  keystore-file = pkgs.writeText "keystore-file" (builtins.toJSON keystore);
+in
+{
+  name = "quorum";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mmahut ];
+  };
+
+  nodes = {
+    machine = { ... }: {
+      services.quorum = {
+        enable = true;
+        permissioned = false;
+        staticNodes = [ "enode://dd333ec28f0a8910c92eb4d336461eea1c20803eed9cf2c056557f986e720f8e693605bba2f4e8f289b1162e5ac7c80c914c7178130711e393ca76abc1d92f57@0.0.0.0:30303?discport=0" ];
+        genesis = {
+          alloc = {
+            "189d23d201b03ae1cf9113672df29a5d672aefa3" = {
+              balance = "0x446c3b15f9926687d2c40534fdb564000000000000";
+            };
+            "44b07d2c28b8ed8f02b45bd84ac7d9051b3349e6" = {
+              balance = "0x446c3b15f9926687d2c40534fdb564000000000000";
+            };
+            "4c1ccd426833b9782729a212c857f2f03b7b4c0d" = {
+              balance = "0x446c3b15f9926687d2c40534fdb564000000000000";
+            };
+            "7ae555d0f6faad7930434abdaac2274fd86ab516" = {
+              balance = "0x446c3b15f9926687d2c40534fdb564000000000000";
+            };
+            c1056df7c02b6f1a353052eaf0533cc7cb743b52 = {
+              balance = "0x446c3b15f9926687d2c40534fdb564000000000000";
+            };
+          };
+          coinbase = "0x0000000000000000000000000000000000000000";
+          config = {
+            byzantiumBlock = 1;
+            chainId = 10;
+            eip150Block = 1;
+            eip150Hash =
+              "0x0000000000000000000000000000000000000000000000000000000000000000";
+            eip155Block = 1;
+            eip158Block = 1;
+            isQuorum = true;
+            istanbul = {
+              epoch = 30000;
+              policy = 0;
+            };
+          };
+        difficulty = "0x1";
+        extraData =
+          "0x0000000000000000000000000000000000000000000000000000000000000000f8aff869944c1ccd426833b9782729a212c857f2f03b7b4c0d94189d23d201b03ae1cf9113672df29a5d672aefa39444b07d2c28b8ed8f02b45bd84ac7d9051b3349e694c1056df7c02b6f1a353052eaf0533cc7cb743b52947ae555d0f6faad7930434abdaac2274fd86ab516b8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0";
+        gasLimit = "0xe0000000";
+        gasUsed = "0x0";
+        mixHash =
+          "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365";
+        nonce = "0x0";
+        number = "0x0";
+        parentHash =
+          "0x0000000000000000000000000000000000000000000000000000000000000000";
+        timestamp = "0x5cffc201";
+      };
+     };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    machine.succeed("mkdir -p /var/lib/quorum/keystore")
+    machine.succeed(
+        'cp ${keystore-file} /var/lib/quorum/keystore/UTC--2020-03-23T11-08-34.144812212Z--${keystore.address}'
+    )
+    machine.succeed(
+        "echo fe2725c4e8f7617764b845e8d939a65c664e7956eb47ed7d934573f16488efc1 > /var/lib/quorum/nodekey"
+    )
+    machine.succeed("systemctl restart quorum")
+    machine.wait_for_unit("quorum.service")
+    machine.sleep(15)
+    machine.succeed('geth attach /var/lib/quorum/geth.ipc --exec "eth.accounts" | grep ${keystore.address}')
+  '';
+})
diff --git a/nixos/tests/rabbitmq.nix b/nixos/tests/rabbitmq.nix
new file mode 100644
index 00000000000..03f1fa46d29
--- /dev/null
+++ b/nixos/tests/rabbitmq.nix
@@ -0,0 +1,27 @@
+# This test runs rabbitmq and checks if rabbitmq is up and running.
+
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "rabbitmq";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eelco offline ];
+  };
+
+  machine = {
+    services.rabbitmq = {
+      enable = true;
+      managementPlugin.enable = true;
+    };
+    # Ensure there is sufficient extra disk space for rabbitmq to be happy
+    virtualisation.diskSize = 1024;
+  };
+
+  testScript = ''
+    machine.start()
+
+    machine.wait_for_unit("rabbitmq.service")
+    machine.wait_until_succeeds(
+        'su -s ${pkgs.runtimeShell} rabbitmq -c "rabbitmqctl status"'
+    )
+    machine.wait_for_open_port("15672")
+  '';
+})
diff --git a/nixos/tests/radarr.nix b/nixos/tests/radarr.nix
new file mode 100644
index 00000000000..ed90025ac42
--- /dev/null
+++ b/nixos/tests/radarr.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "radarr";
+  meta.maintainers = with maintainers; [ etu ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    { services.radarr.enable = true; };
+
+  testScript = ''
+    machine.wait_for_unit("radarr.service")
+    machine.wait_for_open_port("7878")
+    machine.succeed("curl --fail http://localhost:7878/")
+  '';
+})
diff --git a/nixos/tests/radicale.nix b/nixos/tests/radicale.nix
new file mode 100644
index 00000000000..5101628a682
--- /dev/null
+++ b/nixos/tests/radicale.nix
@@ -0,0 +1,95 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+
+let
+  user = "someuser";
+  password = "some_password";
+  port = "5232";
+  filesystem_folder = "/data/radicale";
+
+  cli = "${pkgs.calendar-cli}/bin/calendar-cli --caldav-user ${user} --caldav-pass ${password}";
+in {
+  name = "radicale3";
+  meta.maintainers = with lib.maintainers; [ dotlambda ];
+
+  machine = { pkgs, ... }: {
+    services.radicale = {
+      enable = true;
+      settings = {
+        auth = {
+          type = "htpasswd";
+          htpasswd_filename = "/etc/radicale/users";
+          htpasswd_encryption = "bcrypt";
+        };
+        storage = {
+          inherit filesystem_folder;
+          hook = "git add -A && (git diff --cached --quiet || git commit -m 'Changes by '%(user)s)";
+        };
+        logging.level = "info";
+      };
+      rights = {
+        principal = {
+          user = ".+";
+          collection = "{user}";
+          permissions = "RW";
+        };
+        calendars = {
+          user = ".+";
+          collection = "{user}/[^/]+";
+          permissions = "rw";
+        };
+      };
+    };
+    systemd.services.radicale.path = [ pkgs.git ];
+    environment.systemPackages = [ pkgs.git ];
+    systemd.tmpfiles.rules = [ "d ${filesystem_folder} 0750 radicale radicale -" ];
+    # WARNING: DON'T DO THIS IN PRODUCTION!
+    # This puts unhashed secrets directly into the Nix store for ease of testing.
+    environment.etc."radicale/users".source = pkgs.runCommand "htpasswd" {} ''
+      ${pkgs.apacheHttpd}/bin/htpasswd -bcB "$out" ${user} ${password}
+    '';
+  };
+  testScript = ''
+    machine.wait_for_unit("radicale.service")
+    machine.wait_for_open_port(${port})
+
+    machine.succeed("sudo -u radicale git -C ${filesystem_folder} init")
+    machine.succeed(
+        "sudo -u radicale git -C ${filesystem_folder} config --local user.email radicale@example.com"
+    )
+    machine.succeed(
+        "sudo -u radicale git -C ${filesystem_folder} config --local user.name radicale"
+    )
+
+    with subtest("Test calendar and event creation"):
+        machine.succeed(
+            "${cli} --caldav-url http://localhost:${port}/${user} calendar create cal"
+        )
+        machine.succeed("test -d ${filesystem_folder}/collection-root/${user}/cal")
+        machine.succeed('test -z "$(ls ${filesystem_folder}/collection-root/${user}/cal)"')
+        machine.succeed(
+            "${cli} --caldav-url http://localhost:${port}/${user}/cal calendar add 2021-04-23 testevent"
+        )
+        machine.succeed('test -n "$(ls ${filesystem_folder}/collection-root/${user}/cal)"')
+        (status, stdout) = machine.execute(
+            "sudo -u radicale git -C ${filesystem_folder} log --format=oneline | wc -l"
+        )
+        assert status == 0, "git log failed"
+        assert stdout == "3\n", "there should be exactly 3 commits"
+
+    with subtest("Test rights file"):
+        machine.fail(
+            "${cli} --caldav-url http://localhost:${port}/${user} calendar create sub/cal"
+        )
+        machine.fail(
+            "${cli} --caldav-url http://localhost:${port}/otheruser calendar create cal"
+        )
+
+    with subtest("Test web interface"):
+        machine.succeed("curl --fail http://${user}:${password}@localhost:${port}/.web/")
+
+    with subtest("Test security"):
+        output = machine.succeed("systemd-analyze security radicale.service")
+        machine.log(output)
+        assert output[-9:-1] == "SAFE :-}"
+  '';
+})
diff --git a/nixos/tests/rasdaemon.nix b/nixos/tests/rasdaemon.nix
new file mode 100644
index 00000000000..e4bd8d96a8d
--- /dev/null
+++ b/nixos/tests/rasdaemon.nix
@@ -0,0 +1,34 @@
+import ./make-test-python.nix ({ pkgs, ... } : {
+  name = "rasdaemon";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ evils ];
+  };
+
+  machine = { pkgs, ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+    hardware.rasdaemon = {
+      enable = true;
+      # should be enabled by default, just making sure
+      record = true;
+      # nonsense label
+      labels = ''
+        vendor: none
+          product: none
+          model: none
+            DIMM_0: 0.0.0;
+      '';
+    };
+  };
+
+  testScript =
+    ''
+      start_all()
+      machine.wait_for_unit("multi-user.target")
+      # confirm rasdaemon is running and has a valid database
+      # some disk errors detected in qemu for some reason ¯\_(ツ)_/¯
+      machine.succeed("ras-mc-ctl --errors | tee /dev/stderr | grep -q 'No .* errors.'")
+      # confirm the supplied labels text made it into the system
+      machine.succeed("grep -q 'vendor: none' /etc/ras/dimm_labels.d/labels >&2")
+      machine.shutdown()
+    '';
+})
diff --git a/nixos/tests/redis.nix b/nixos/tests/redis.nix
new file mode 100644
index 00000000000..7b70c239ad6
--- /dev/null
+++ b/nixos/tests/redis.nix
@@ -0,0 +1,46 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+{
+  name = "redis";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ flokli ];
+  };
+
+  nodes = {
+    machine =
+      { pkgs, lib, ... }: with lib;
+
+      {
+        services.redis.servers."".enable = true;
+        services.redis.servers."test".enable = true;
+
+        users.users = listToAttrs (map (suffix: nameValuePair "member${suffix}" {
+          createHome = false;
+          description = "A member of the redis${suffix} group";
+          isNormalUser = true;
+          extraGroups = [ "redis${suffix}" ];
+        }) ["" "-test"]);
+      };
+  };
+
+  testScript = { nodes, ... }: let
+    inherit (nodes.machine.config.services) redis;
+    in ''
+    start_all()
+    machine.wait_for_unit("redis")
+    machine.wait_for_unit("redis-test")
+
+    # The unnamed Redis server still opens a port for backward-compatibility
+    machine.wait_for_open_port("6379")
+
+    machine.wait_for_file("${redis.servers."".unixSocket}")
+    machine.wait_for_file("${redis.servers."test".unixSocket}")
+
+    # The unix socket is accessible to the redis group
+    machine.succeed('su member -c "redis-cli ping | grep PONG"')
+    machine.succeed('su member-test -c "redis-cli ping | grep PONG"')
+
+    machine.succeed("redis-cli ping | grep PONG")
+    machine.succeed("redis-cli -s ${redis.servers."".unixSocket} ping | grep PONG")
+    machine.succeed("redis-cli -s ${redis.servers."test".unixSocket} ping | grep PONG")
+  '';
+})
diff --git a/nixos/tests/redmine.nix b/nixos/tests/redmine.nix
new file mode 100644
index 00000000000..3866a1f528c
--- /dev/null
+++ b/nixos/tests/redmine.nix
@@ -0,0 +1,44 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  redmineTest = { name, type }: makeTest {
+    name = "redmine-${name}";
+    machine = { config, pkgs, ... }: {
+      services.redmine = {
+        enable = true;
+        package = pkgs.redmine;
+        database.type = type;
+        plugins = {
+          redmine_env_auth = pkgs.fetchurl {
+            url = "https://github.com/Intera/redmine_env_auth/archive/0.7.zip";
+            sha256 = "1xb8lyarc7mpi86yflnlgyllh9hfwb9z304f19dx409gqpia99sc";
+          };
+        };
+        themes = {
+          dkuk-redmine_alex_skin = pkgs.fetchurl {
+            url = "https://bitbucket.org/dkuk/redmine_alex_skin/get/1842ef675ef3.zip";
+            sha256 = "0hrin9lzyi50k4w2bd2b30vrf1i4fi1c0gyas5801wn8i7kpm9yl";
+          };
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+      machine.wait_for_unit("redmine.service")
+      machine.wait_for_open_port(3000)
+      machine.succeed("curl --fail http://localhost:3000/")
+    '';
+  } // {
+    meta.maintainers = [ maintainers.aanderse ];
+  };
+in {
+  mysql = redmineTest { name = "mysql"; type = "mysql2"; };
+  pgsql = redmineTest { name = "pgsql"; type = "postgresql"; };
+}
diff --git a/nixos/tests/resolv.nix b/nixos/tests/resolv.nix
new file mode 100644
index 00000000000..f0aa7e42aaf
--- /dev/null
+++ b/nixos/tests/resolv.nix
@@ -0,0 +1,46 @@
+# Test whether DNS resolving returns multiple records and all address families.
+import ./make-test-python.nix ({ pkgs, ... } : {
+  name = "resolv";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ckauhaus ];
+  };
+
+  nodes.resolv = { ... }: {
+    networking.extraHosts = ''
+      # IPv4 only
+      192.0.2.1 host-ipv4.example.net
+      192.0.2.2 host-ipv4.example.net
+      # IP6 only
+      2001:db8::2:1 host-ipv6.example.net
+      2001:db8::2:2 host-ipv6.example.net
+      # dual stack
+      192.0.2.1 host-dual.example.net
+      192.0.2.2 host-dual.example.net
+      2001:db8::2:1 host-dual.example.net
+      2001:db8::2:2 host-dual.example.net
+    '';
+  };
+
+  testScript = ''
+    def addrs_in(hostname, addrs):
+        res = resolv.succeed("getent ahosts {}".format(hostname))
+        for addr in addrs:
+            assert addr in res, "Expected output '{}' not found in\n{}".format(addr, res)
+
+
+    start_all()
+    resolv.wait_for_unit("nscd")
+
+    ipv4 = ["192.0.2.1", "192.0.2.2"]
+    ipv6 = ["2001:db8::2:1", "2001:db8::2:2"]
+
+    with subtest("IPv4 resolves"):
+        addrs_in("host-ipv4.example.net", ipv4)
+
+    with subtest("IPv6 resolves"):
+        addrs_in("host-ipv6.example.net", ipv6)
+
+    with subtest("Dual stack resolves"):
+        addrs_in("host-dual.example.net", ipv4 + ipv6)
+  '';
+})
diff --git a/nixos/tests/restart-by-activation-script.nix b/nixos/tests/restart-by-activation-script.nix
new file mode 100644
index 00000000000..0eec292ea9e
--- /dev/null
+++ b/nixos/tests/restart-by-activation-script.nix
@@ -0,0 +1,73 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "restart-by-activation-script";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ das_j ];
+  };
+
+  machine = { pkgs, ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+
+    systemd.services.restart-me = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStart = "${pkgs.coreutils}/bin/true";
+      };
+    };
+
+    systemd.services.reload-me = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = rec {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStart = "${pkgs.coreutils}/bin/true";
+        ExecReload = ExecStart;
+      };
+    };
+
+    system.activationScripts.test = {
+      supportsDryActivation = true;
+      text = ''
+        if [ -e /test-the-activation-script ]; then
+          if [ "$NIXOS_ACTION" != dry-activate ]; then
+            touch /activation-was-run
+            echo restart-me.service > /run/nixos/activation-restart-list
+            echo reload-me.service > /run/nixos/activation-reload-list
+          else
+            echo restart-me.service > /run/nixos/dry-activation-restart-list
+            echo reload-me.service > /run/nixos/dry-activation-reload-list
+          fi
+        fi
+      '';
+    };
+  };
+
+  testScript = /* python */ ''
+    machine.wait_for_unit("multi-user.target")
+
+    with subtest("nothing happens when the activation script does nothing"):
+        out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate 2>&1")
+        assert 'restart' not in out
+        assert 'reload' not in out
+        out = machine.succeed("/run/current-system/bin/switch-to-configuration test")
+        assert 'restart' not in out
+        assert 'reload' not in out
+
+    machine.succeed("touch /test-the-activation-script")
+
+    with subtest("dry activation"):
+        out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate 2>&1")
+        assert 'would restart the following units: restart-me.service' in out
+        assert 'would reload the following units: reload-me.service' in out
+        machine.fail("test -f /run/nixos/dry-activation-restart-list")
+        machine.fail("test -f /run/nixos/dry-activation-reload-list")
+
+    with subtest("real activation"):
+        out = machine.succeed("/run/current-system/bin/switch-to-configuration test 2>&1")
+        assert 'restarting the following units: restart-me.service' in out
+        assert 'reloading the following units: reload-me.service' in out
+        machine.fail("test -f /run/nixos/activation-restart-list")
+        machine.fail("test -f /run/nixos/activation-reload-list")
+  '';
+})
diff --git a/nixos/tests/restic.nix b/nixos/tests/restic.nix
new file mode 100644
index 00000000000..16979eab821
--- /dev/null
+++ b/nixos/tests/restic.nix
@@ -0,0 +1,96 @@
+import ./make-test-python.nix (
+  { pkgs, ... }:
+
+    let
+      password = "some_password";
+      repository = "/tmp/restic-backup";
+      rcloneRepository = "rclone:local:/tmp/restic-rclone-backup";
+
+      passwordFile = "${pkgs.writeText "password" "correcthorsebatterystaple"}";
+      initialize = true;
+      paths = [ "/opt" ];
+      pruneOpts = [
+        "--keep-daily 2"
+        "--keep-weekly 1"
+        "--keep-monthly 1"
+        "--keep-yearly 99"
+      ];
+    in
+      {
+        name = "restic";
+
+        meta = with pkgs.lib.maintainers; {
+          maintainers = [ bbigras i077 ];
+        };
+
+        nodes = {
+          server =
+            { pkgs, ... }:
+              {
+                services.restic.backups = {
+                  remotebackup = {
+                    inherit repository passwordFile initialize paths pruneOpts;
+                  };
+                  rclonebackup = {
+                    repository = rcloneRepository;
+                    rcloneConfig = {
+                      type = "local";
+                      one_file_system = true;
+                    };
+
+                    # This gets overridden by rcloneConfig.type
+                    rcloneConfigFile = pkgs.writeText "rclone.conf" ''
+                      [local]
+                      type=ftp
+                    '';
+                    inherit passwordFile initialize paths pruneOpts;
+                  };
+                  remoteprune = {
+                    inherit repository passwordFile;
+                    pruneOpts = [ "--keep-last 1" ];
+                  };
+                };
+
+                environment.sessionVariables.RCLONE_CONFIG_LOCAL_TYPE = "local";
+              };
+        };
+
+        testScript = ''
+          server.start()
+          server.wait_for_unit("dbus.socket")
+          server.fail(
+              "${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots",
+              "${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots",
+          )
+          server.succeed(
+              "mkdir -p /opt",
+              "touch /opt/some_file",
+              "mkdir -p /tmp/restic-rclone-backup",
+              "timedatectl set-time '2016-12-13 13:45'",
+              "systemctl start restic-backups-remotebackup.service",
+              "systemctl start restic-backups-rclonebackup.service",
+              '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"',
+              '${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"',
+              "timedatectl set-time '2017-12-13 13:45'",
+              "systemctl start restic-backups-remotebackup.service",
+              "systemctl start restic-backups-rclonebackup.service",
+              "timedatectl set-time '2018-12-13 13:45'",
+              "systemctl start restic-backups-remotebackup.service",
+              "systemctl start restic-backups-rclonebackup.service",
+              "timedatectl set-time '2018-12-14 13:45'",
+              "systemctl start restic-backups-remotebackup.service",
+              "systemctl start restic-backups-rclonebackup.service",
+              "timedatectl set-time '2018-12-15 13:45'",
+              "systemctl start restic-backups-remotebackup.service",
+              "systemctl start restic-backups-rclonebackup.service",
+              "timedatectl set-time '2018-12-16 13:45'",
+              "systemctl start restic-backups-remotebackup.service",
+              "systemctl start restic-backups-rclonebackup.service",
+              '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^4 snapshot"',
+              '${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots -c | grep -e "^4 snapshot"',
+              "systemctl start restic-backups-remoteprune.service",
+              '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"',
+          )
+        '';
+      }
+)
diff --git a/nixos/tests/retroarch.nix b/nixos/tests/retroarch.nix
new file mode 100644
index 00000000000..4c96f9eabc8
--- /dev/null
+++ b/nixos/tests/retroarch.nix
@@ -0,0 +1,49 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+  {
+    name = "retroarch";
+    meta = with pkgs.lib.maintainers; { maintainers = [ j0hax ]; };
+
+    machine = { ... }:
+
+      {
+        imports = [ ./common/user-account.nix ];
+        services.xserver.enable = true;
+        services.xserver.desktopManager.retroarch = {
+          enable = true;
+          package = pkgs.retroarchFull;
+        };
+        services.xserver.displayManager = {
+          sddm.enable = true;
+          defaultSession = "RetroArch";
+          autoLogin = {
+            enable = true;
+            user = "alice";
+          };
+        };
+      };
+
+    testScript = { nodes, ... }:
+      let
+        user = nodes.machine.config.users.users.alice;
+        xdo = "${pkgs.xdotool}/bin/xdotool";
+      in ''
+        with subtest("Wait for login"):
+            start_all()
+            machine.wait_for_file("${user.home}/.Xauthority")
+            machine.succeed("xauth merge ${user.home}/.Xauthority")
+
+        with subtest("Check RetroArch started"):
+            machine.wait_until_succeeds("pgrep retroarch")
+            machine.wait_for_window("^RetroArch ")
+
+        with subtest("Check configuration created"):
+            machine.wait_for_file("${user.home}/.config/retroarch/retroarch.cfg")
+
+        with subtest("Wait to get a screenshot"):
+            machine.execute(
+                "${xdo} key Alt+F1 sleep 10"
+            )
+            machine.screenshot("screen")
+      '';
+  })
diff --git a/nixos/tests/riak.nix b/nixos/tests/riak.nix
new file mode 100644
index 00000000000..3dd4e333d66
--- /dev/null
+++ b/nixos/tests/riak.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "riak";
+  meta = with lib.maintainers; {
+    maintainers = [ Br1ght0ne ];
+  };
+
+  machine = {
+    services.riak.enable = true;
+    services.riak.package = pkgs.riak;
+  };
+
+  testScript = ''
+    machine.start()
+
+    machine.wait_for_unit("riak")
+    machine.wait_until_succeeds("riak ping 2>&1")
+  '';
+})
diff --git a/nixos/tests/robustirc-bridge.nix b/nixos/tests/robustirc-bridge.nix
new file mode 100644
index 00000000000..8493fd62821
--- /dev/null
+++ b/nixos/tests/robustirc-bridge.nix
@@ -0,0 +1,29 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "robustirc-bridge";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ hax404 ];
+  };
+
+  nodes =
+    { bridge =
+      { services.robustirc-bridge = {
+          enable = true;
+          extraFlags = [
+            "-listen localhost:6667"
+            "-network example.com"
+          ];
+        };
+      };
+    };
+
+    testScript =
+    ''
+      start_all()
+
+      bridge.wait_for_unit("robustirc-bridge.service")
+      bridge.wait_for_open_port(1080)
+      bridge.wait_for_open_port(6667)
+    '';
+})
diff --git a/nixos/tests/roundcube.nix b/nixos/tests/roundcube.nix
new file mode 100644
index 00000000000..763f10a7a2d
--- /dev/null
+++ b/nixos/tests/roundcube.nix
@@ -0,0 +1,31 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "roundcube";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ globin ];
+  };
+
+  nodes = {
+    roundcube = { config, pkgs, ... }: {
+      services.roundcube = {
+        enable = true;
+        hostName = "roundcube";
+        database.password = "not production";
+        package = pkgs.roundcube.withPlugins (plugins: [ plugins.persistent_login ]);
+        plugins = [ "persistent_login" ];
+        dicts = with pkgs.aspellDicts; [ en fr de ];
+      };
+      services.nginx.virtualHosts.roundcube = {
+        forceSSL = false;
+        enableACME = false;
+      };
+    };
+  };
+
+  testScript = ''
+    roundcube.start
+    roundcube.wait_for_unit("postgresql.service")
+    roundcube.wait_for_unit("phpfpm-roundcube.service")
+    roundcube.wait_for_unit("nginx.service")
+    roundcube.succeed("curl -sSfL http://roundcube/ | grep 'Keep me logged in'")
+  '';
+})
diff --git a/nixos/tests/rspamd.nix b/nixos/tests/rspamd.nix
new file mode 100644
index 00000000000..f0ccfe7ea0e
--- /dev/null
+++ b/nixos/tests/rspamd.nix
@@ -0,0 +1,313 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  initMachine = ''
+    start_all()
+    machine.wait_for_unit("rspamd.service")
+    machine.succeed("id rspamd >/dev/null")
+  '';
+  checkSocket = socket: user: group: mode: ''
+    machine.succeed(
+        "ls ${socket} >/dev/null",
+        '[[ "$(stat -c %U ${socket})" == "${user}" ]]',
+        '[[ "$(stat -c %G ${socket})" == "${group}" ]]',
+        '[[ "$(stat -c %a ${socket})" == "${mode}" ]]',
+    )
+  '';
+  simple = name: enableIPv6: makeTest {
+    name = "rspamd-${name}";
+    machine = {
+      services.rspamd.enable = true;
+      networking.enableIPv6 = enableIPv6;
+    };
+    testScript = ''
+      start_all()
+      machine.wait_for_unit("multi-user.target")
+      machine.wait_for_open_port(11334)
+      machine.wait_for_unit("rspamd.service")
+      machine.succeed("id rspamd >/dev/null")
+      ${checkSocket "/run/rspamd/rspamd.sock" "rspamd" "rspamd" "660" }
+      machine.sleep(10)
+      machine.log(machine.succeed("cat /etc/rspamd/rspamd.conf"))
+      machine.log(
+          machine.succeed("grep 'CONFDIR/worker-controller.inc' /etc/rspamd/rspamd.conf")
+      )
+      machine.log(machine.succeed("grep 'CONFDIR/worker-normal.inc' /etc/rspamd/rspamd.conf"))
+      machine.log(machine.succeed("systemctl cat rspamd.service"))
+      machine.log(machine.succeed("curl http://localhost:11334/auth"))
+      machine.log(machine.succeed("curl http://127.0.0.1:11334/auth"))
+      ${optionalString enableIPv6 ''machine.log(machine.succeed("curl http://[::1]:11334/auth"))''}
+      # would not reformat
+    '';
+  };
+in
+{
+  simple = simple "simple" true;
+  ipv4only = simple "ipv4only" false;
+  deprecated = makeTest {
+    name = "rspamd-deprecated";
+    machine = {
+      services.rspamd = {
+        enable = true;
+        workers.normal.bindSockets = [{
+          socket = "/run/rspamd/rspamd.sock";
+          mode = "0600";
+          owner = "rspamd";
+          group = "rspamd";
+        }];
+        workers.controller.bindSockets = [{
+          socket = "/run/rspamd/rspamd-worker.sock";
+          mode = "0666";
+          owner = "rspamd";
+          group = "rspamd";
+        }];
+      };
+    };
+
+    testScript = ''
+      ${initMachine}
+      machine.wait_for_file("/run/rspamd/rspamd.sock")
+      ${checkSocket "/run/rspamd/rspamd.sock" "rspamd" "rspamd" "600" }
+      ${checkSocket "/run/rspamd/rspamd-worker.sock" "rspamd" "rspamd" "666" }
+      machine.log(machine.succeed("cat /etc/rspamd/rspamd.conf"))
+      machine.log(
+          machine.succeed("grep 'CONFDIR/worker-controller.inc' /etc/rspamd/rspamd.conf")
+      )
+      machine.log(machine.succeed("grep 'CONFDIR/worker-normal.inc' /etc/rspamd/rspamd.conf"))
+      machine.log(machine.succeed("rspamc -h /run/rspamd/rspamd-worker.sock stat"))
+      machine.log(
+          machine.succeed(
+              "curl --unix-socket /run/rspamd/rspamd-worker.sock http://localhost/ping"
+          )
+      )
+    '';
+  };
+
+  bindports = makeTest {
+    name = "rspamd-bindports";
+    machine = {
+      services.rspamd = {
+        enable = true;
+        workers.normal.bindSockets = [{
+          socket = "/run/rspamd/rspamd.sock";
+          mode = "0600";
+          owner = "rspamd";
+          group = "rspamd";
+        }];
+        workers.controller.bindSockets = [{
+          socket = "/run/rspamd/rspamd-worker.sock";
+          mode = "0666";
+          owner = "rspamd";
+          group = "rspamd";
+        }];
+        workers.controller2 = {
+          type = "controller";
+          bindSockets = [ "0.0.0.0:11335" ];
+          extraConfig = ''
+            static_dir = "''${WWWDIR}";
+            secure_ip = null;
+            password = "verysecretpassword";
+          '';
+        };
+      };
+    };
+
+    testScript = ''
+      ${initMachine}
+      machine.wait_for_file("/run/rspamd/rspamd.sock")
+      ${checkSocket "/run/rspamd/rspamd.sock" "rspamd" "rspamd" "600" }
+      ${checkSocket "/run/rspamd/rspamd-worker.sock" "rspamd" "rspamd" "666" }
+      machine.log(machine.succeed("cat /etc/rspamd/rspamd.conf"))
+      machine.log(
+          machine.succeed("grep 'CONFDIR/worker-controller.inc' /etc/rspamd/rspamd.conf")
+      )
+      machine.log(machine.succeed("grep 'CONFDIR/worker-normal.inc' /etc/rspamd/rspamd.conf"))
+      machine.log(
+          machine.succeed(
+              "grep 'LOCAL_CONFDIR/override.d/worker-controller2.inc' /etc/rspamd/rspamd.conf"
+          )
+      )
+      machine.log(
+          machine.succeed(
+              "grep 'verysecretpassword' /etc/rspamd/override.d/worker-controller2.inc"
+          )
+      )
+      machine.wait_until_succeeds(
+          "journalctl -u rspamd | grep -i 'starting controller process' >&2"
+      )
+      machine.log(machine.succeed("rspamc -h /run/rspamd/rspamd-worker.sock stat"))
+      machine.log(
+          machine.succeed(
+              "curl --unix-socket /run/rspamd/rspamd-worker.sock http://localhost/ping"
+          )
+      )
+      machine.log(machine.succeed("curl http://localhost:11335/ping"))
+    '';
+  };
+  customLuaRules = makeTest {
+    name = "rspamd-custom-lua-rules";
+    machine = {
+      environment.etc."tests/no-muh.eml".text = ''
+        From: Sheep1<bah@example.com>
+        To: Sheep2<mah@example.com>
+        Subject: Evil cows
+
+        I find cows to be evil don't you?
+      '';
+      environment.etc."tests/muh.eml".text = ''
+        From: Cow<cow@example.com>
+        To: Sheep2<mah@example.com>
+        Subject: Evil cows
+
+        Cows are majestic creatures don't Muh agree?
+      '';
+      services.rspamd = {
+        enable = true;
+        locals = {
+          "antivirus.conf" = mkIf false { text = ''
+              clamav {
+                action = "reject";
+                symbol = "CLAM_VIRUS";
+                type = "clamav";
+                log_clean = true;
+                servers = "/run/clamav/clamd.ctl";
+              }
+            '';};
+          "redis.conf" = {
+            enable = false;
+            text = ''
+              servers = "127.0.0.1";
+            '';
+          };
+          "groups.conf".text = ''
+            group "cows" {
+              symbol {
+                NO_MUH = {
+                  weight = 1.0;
+                  description = "Mails should not muh";
+                }
+              }
+            }
+          '';
+        };
+        localLuaRules = pkgs.writeText "rspamd.local.lua" ''
+          local rspamd_logger = require "rspamd_logger"
+          rspamd_config.NO_MUH = {
+            callback = function (task)
+              local parts = task:get_text_parts()
+              if parts then
+                for _,part in ipairs(parts) do
+                  local content = tostring(part:get_content())
+                  rspamd_logger.infox(rspamd_config, 'Found content %s', content)
+                  local found = string.find(content, "Muh");
+                  rspamd_logger.infox(rspamd_config, 'Found muh %s', tostring(found))
+                  if found then
+                    return true
+                  end
+                end
+              end
+              return false
+            end,
+            score = 5.0,
+            description = 'Allow no cows',
+            group = "cows",
+          }
+          rspamd_logger.infox(rspamd_config, 'Work dammit!!!')
+        '';
+      };
+    };
+    testScript = ''
+      ${initMachine}
+      machine.wait_for_open_port(11334)
+      machine.log(machine.succeed("cat /etc/rspamd/rspamd.conf"))
+      machine.log(machine.succeed("cat /etc/rspamd/rspamd.local.lua"))
+      machine.log(machine.succeed("cat /etc/rspamd/local.d/groups.conf"))
+      # Verify that redis.conf was not written
+      machine.fail("cat /etc/rspamd/local.d/redis.conf >&2")
+      # Verify that antivirus.conf was not written
+      machine.fail("cat /etc/rspamd/local.d/antivirus.conf >&2")
+      ${checkSocket "/run/rspamd/rspamd.sock" "rspamd" "rspamd" "660" }
+      machine.log(
+          machine.succeed("curl --unix-socket /run/rspamd/rspamd.sock http://localhost/ping")
+      )
+      machine.log(machine.succeed("rspamc -h 127.0.0.1:11334 stat"))
+      machine.log(machine.succeed("cat /etc/tests/no-muh.eml | rspamc -h 127.0.0.1:11334"))
+      machine.log(
+          machine.succeed("cat /etc/tests/muh.eml | rspamc -h 127.0.0.1:11334 symbols")
+      )
+      machine.wait_until_succeeds("journalctl -u rspamd | grep -i muh >&2")
+      machine.log(
+          machine.fail(
+              "cat /etc/tests/no-muh.eml | rspamc -h 127.0.0.1:11334 symbols | grep NO_MUH"
+          )
+      )
+      machine.log(
+          machine.succeed(
+              "cat /etc/tests/muh.eml | rspamc -h 127.0.0.1:11334 symbols | grep NO_MUH"
+          )
+      )
+    '';
+  };
+  postfixIntegration = makeTest {
+    name = "rspamd-postfix-integration";
+    machine = {
+      environment.systemPackages = with pkgs; [ msmtp ];
+      environment.etc."tests/gtube.eml".text = ''
+        From: Sheep1<bah@example.com>
+        To: Sheep2<tester@example.com>
+        Subject: Evil cows
+
+        I find cows to be evil don't you?
+
+        XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X
+      '';
+      environment.etc."tests/example.eml".text = ''
+        From: Sheep1<bah@example.com>
+        To: Sheep2<tester@example.com>
+        Subject: Evil cows
+
+        I find cows to be evil don't you?
+      '';
+      users.users.tester = {
+        isNormalUser = true;
+        password = "test";
+      };
+      services.postfix = {
+        enable = true;
+        destination = ["example.com"];
+      };
+      services.rspamd = {
+        enable = true;
+        postfix.enable = true;
+        workers.rspamd_proxy.type = "rspamd_proxy";
+      };
+    };
+    testScript = ''
+      ${initMachine}
+      machine.wait_for_open_port(11334)
+      machine.wait_for_open_port(25)
+      ${checkSocket "/run/rspamd/rspamd-milter.sock" "rspamd" "postfix" "660" }
+      machine.log(machine.succeed("rspamc -h 127.0.0.1:11334 stat"))
+      machine.log(
+          machine.succeed(
+              "msmtp --host=localhost -t --read-envelope-from < /etc/tests/example.eml"
+          )
+      )
+      machine.log(
+          machine.fail(
+              "msmtp --host=localhost -t --read-envelope-from < /etc/tests/gtube.eml"
+          )
+      )
+
+      machine.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
+      machine.fail("journalctl -u postfix | grep -i error >&2")
+      machine.fail("journalctl -u postfix | grep -i warning >&2")
+    '';
+  };
+}
diff --git a/nixos/tests/rss2email.nix b/nixos/tests/rss2email.nix
new file mode 100644
index 00000000000..f32326feb50
--- /dev/null
+++ b/nixos/tests/rss2email.nix
@@ -0,0 +1,66 @@
+import ./make-test-python.nix {
+  name = "rss2email";
+
+  nodes = {
+    server = { pkgs, ... }: {
+      imports = [ common/user-account.nix ];
+      services.nginx = {
+        enable = true;
+        virtualHosts."127.0.0.1".root = ./common/webroot;
+      };
+      services.rss2email = {
+        enable = true;
+        to = "alice@localhost";
+        interval = "1";
+        config.from = "test@example.org";
+        feeds = {
+          nixos = { url = "http://127.0.0.1/news-rss.xml"; };
+        };
+      };
+      services.opensmtpd = {
+        enable = true;
+        extraServerArgs = [ "-v" ];
+        serverConfiguration = ''
+          listen on 127.0.0.1
+          action dovecot_deliver mda \
+            "${pkgs.dovecot}/libexec/dovecot/deliver -d %{user.username}"
+          match from any for local action dovecot_deliver
+        '';
+      };
+      services.dovecot2 = {
+        enable = true;
+        enableImap = true;
+        mailLocation = "maildir:~/mail";
+        protocols = [ "imap" ];
+      };
+      environment.systemPackages = let
+        checkMailLanded = pkgs.writeScriptBin "check-mail-landed" ''
+          #!${pkgs.python3.interpreter}
+          import imaplib
+
+          with imaplib.IMAP4('127.0.0.1', 143) as imap:
+            imap.login('alice', 'foobar')
+            imap.select()
+            status, refs = imap.search(None, 'ALL')
+            print("=====> Result of search for all:", status, refs)
+            assert status == 'OK'
+            assert len(refs) > 0
+            status, msg = imap.fetch(refs[0], 'BODY[TEXT]')
+            assert status == 'OK'
+        '';
+      in [ pkgs.opensmtpd checkMailLanded ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    server.wait_for_unit("network-online.target")
+    server.wait_for_unit("opensmtpd")
+    server.wait_for_unit("dovecot2")
+    server.wait_for_unit("nginx")
+    server.wait_for_unit("rss2email")
+
+    server.wait_until_succeeds("check-mail-landed >&2")
+  '';
+}
diff --git a/nixos/tests/rstudio-server.nix b/nixos/tests/rstudio-server.nix
new file mode 100644
index 00000000000..c7ac7670fbd
--- /dev/null
+++ b/nixos/tests/rstudio-server.nix
@@ -0,0 +1,30 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+  {
+    name = "rstudio-server-test";
+    meta.maintainers = with pkgs.lib.maintainers; [ jbedo cfhammill ];
+
+    nodes.machine = { config, lib, pkgs, ... }: {
+      services.rstudio-server.enable = true;
+    };
+
+    nodes.customPackageMachine = { config, lib, pkgs, ... }: {
+      services.rstudio-server = {
+        enable = true;
+        package = pkgs.rstudioServerWrapper.override { packages = [ pkgs.rPackages.ggplot2 ]; };
+      };
+    };
+
+    users.testuser = {
+      uid = 1000;
+      group = "testgroup";
+    };
+    groups.testgroup.gid = 1000;
+
+    testScript = ''
+      machine.wait_for_unit("rstudio-server.service")
+      machine.succeed("curl -f -vvv -s http://127.0.0.1:8787")
+
+      customPackageMachine.wait_for_unit("rstudio-server.service")
+      customPackageMachine.succeed("curl -f -vvv -s http://127.0.0.1:8787")
+    '';
+  })
diff --git a/nixos/tests/rsyncd.nix b/nixos/tests/rsyncd.nix
new file mode 100644
index 00000000000..44464e42f28
--- /dev/null
+++ b/nixos/tests/rsyncd.nix
@@ -0,0 +1,36 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "rsyncd";
+  meta.maintainers = with pkgs.lib.maintainers; [ ehmry ];
+
+  nodes = let
+    mkNode = socketActivated:
+      { config, ... }: {
+        networking.firewall.allowedTCPPorts = [ config.services.rsyncd.port ];
+        services.rsyncd = {
+          enable = true;
+          inherit socketActivated;
+          settings = {
+            global = {
+              "reverse lookup" = false;
+              "forward lookup" = false;
+            };
+            tmp = {
+              path = "/nix/store";
+              comment = "test module";
+            };
+          };
+        };
+      };
+  in {
+    a = mkNode false;
+    b = mkNode true;
+  };
+
+  testScript = ''
+    start_all()
+    a.wait_for_unit("rsync")
+    b.wait_for_unit("sockets.target")
+    b.succeed("rsync a::")
+    a.succeed("rsync b::")
+  '';
+})
diff --git a/nixos/tests/rsyslogd.nix b/nixos/tests/rsyslogd.nix
new file mode 100644
index 00000000000..f35db3bd44b
--- /dev/null
+++ b/nixos/tests/rsyslogd.nix
@@ -0,0 +1,40 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+{
+  test1 = makeTest {
+    name = "rsyslogd-test1";
+    meta.maintainers = [ pkgs.lib.maintainers.aanderse ];
+
+    machine = { config, pkgs, ... }: {
+      services.rsyslogd.enable = true;
+      services.journald.forwardToSyslog = false;
+    };
+
+    # ensure rsyslogd isn't receiving messages from journald if explicitly disabled
+    testScript = ''
+      machine.wait_for_unit("default.target")
+      machine.fail("test -f /var/log/messages")
+    '';
+  };
+
+  test2 = makeTest {
+    name = "rsyslogd-test2";
+    meta.maintainers = [ pkgs.lib.maintainers.aanderse ];
+
+    machine = { config, pkgs, ... }: {
+      services.rsyslogd.enable = true;
+    };
+
+    # ensure rsyslogd is receiving messages from journald
+    testScript = ''
+      machine.wait_for_unit("default.target")
+      machine.succeed("test -f /var/log/messages")
+    '';
+  };
+}
diff --git a/nixos/tests/rxe.nix b/nixos/tests/rxe.nix
new file mode 100644
index 00000000000..10753c4ed0c
--- /dev/null
+++ b/nixos/tests/rxe.nix
@@ -0,0 +1,47 @@
+import ./make-test-python.nix ({ ... } :
+
+let
+  node = { pkgs, ... } : {
+    networking = {
+      firewall = {
+        allowedUDPPorts = [ 4791 ]; # open RoCE port
+        allowedTCPPorts = [ 4800 ]; # port for test utils
+      };
+      rxe = {
+        enable = true;
+        interfaces = [ "eth1" ];
+      };
+    };
+
+    environment.systemPackages = with pkgs; [ rdma-core screen ];
+  };
+
+in {
+  name = "rxe";
+
+  nodes = {
+    server = node;
+    client = node;
+  };
+
+  testScript = ''
+    # Test if rxe interface comes up
+    server.wait_for_unit("default.target")
+    server.succeed("systemctl status rxe.service")
+    server.succeed("ibv_devices | grep rxe_eth1")
+
+    client.wait_for_unit("default.target")
+
+    # ping pong tests
+    for proto in "rc", "uc", "ud", "srq":
+        server.succeed(
+            "screen -dmS {0}_pingpong ibv_{0}_pingpong -p 4800 -s 1024 -g0".format(proto)
+        )
+        client.succeed("sleep 2; ibv_{}_pingpong -p 4800 -s 1024 -g0 server".format(proto))
+
+    server.succeed("screen -dmS rping rping -s -a server -C 10")
+    client.succeed("sleep 2; rping -c -a server -C 10")
+  '';
+})
+
+
diff --git a/nixos/tests/sabnzbd.nix b/nixos/tests/sabnzbd.nix
new file mode 100644
index 00000000000..fb35b212b49
--- /dev/null
+++ b/nixos/tests/sabnzbd.nix
@@ -0,0 +1,22 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "sabnzbd";
+  meta = with pkgs.lib; {
+    maintainers = with maintainers; [ jojosch ];
+  };
+
+  machine = { pkgs, ... }: {
+    services.sabnzbd = {
+      enable = true;
+    };
+
+    # unrar is unfree
+    nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "unrar" ];
+  };
+
+  testScript = ''
+    machine.wait_for_unit("sabnzbd.service")
+    machine.wait_until_succeeds(
+        "curl --fail -L http://localhost:8080/"
+    )
+  '';
+})
diff --git a/nixos/tests/samba-wsdd.nix b/nixos/tests/samba-wsdd.nix
new file mode 100644
index 00000000000..0e3185b0c68
--- /dev/null
+++ b/nixos/tests/samba-wsdd.nix
@@ -0,0 +1,44 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "samba-wsdd";
+  meta.maintainers = with pkgs.lib.maintainers; [ izorkin ];
+
+  nodes = {
+    client_wsdd = { pkgs, ... }: {
+      services.samba-wsdd = {
+        enable = true;
+        interface = "eth1";
+        workgroup = "WORKGROUP";
+        hostname = "CLIENT-WSDD";
+        discovery = true;
+        extraOptions = [ "--no-host" ];
+      };
+      networking.firewall.allowedTCPPorts = [ 5357 ];
+      networking.firewall.allowedUDPPorts = [ 3702 ];
+    };
+
+    server_wsdd = { ... }: {
+      services.samba-wsdd = {
+        enable = true;
+        interface = "eth1";
+        workgroup = "WORKGROUP";
+        hostname = "SERVER-WSDD";
+      };
+      networking.firewall.allowedTCPPorts = [ 5357 ];
+      networking.firewall.allowedUDPPorts = [ 3702 ];
+    };
+  };
+
+  testScript = ''
+    client_wsdd.start()
+    client_wsdd.wait_for_unit("samba-wsdd")
+
+    server_wsdd.start()
+    server_wsdd.wait_for_unit("samba-wsdd")
+
+    client_wsdd.wait_until_succeeds(
+        "echo list | ${pkgs.libressl.nc}/bin/nc -N -U /run/wsdd/wsdd.sock | grep -i SERVER-WSDD"
+    )
+  '';
+})
diff --git a/nixos/tests/samba.nix b/nixos/tests/samba.nix
new file mode 100644
index 00000000000..252c3dd9c76
--- /dev/null
+++ b/nixos/tests/samba.nix
@@ -0,0 +1,46 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "samba";
+
+  meta.maintainers = [ pkgs.lib.maintainers.eelco ];
+
+  nodes =
+    { client =
+        { pkgs, ... }:
+        { virtualisation.fileSystems =
+            { "/public" = {
+                fsType = "cifs";
+                device = "//server/public";
+                options = [ "guest" ];
+              };
+            };
+        };
+
+      server =
+        { ... }:
+        { services.samba.enable = true;
+          services.samba.openFirewall = true;
+          services.samba.shares.public =
+            { path = "/public";
+              "read only" = true;
+              browseable = "yes";
+              "guest ok" = "yes";
+              comment = "Public samba share.";
+            };
+        };
+    };
+
+  # client# [    4.542997] mount[777]: sh: systemd-ask-password: command not found
+
+  testScript =
+    ''
+      server.start()
+      server.wait_for_unit("samba.target")
+      server.succeed("mkdir -p /public; echo bar > /public/foo")
+
+      client.start()
+      client.wait_for_unit("remote-fs.target")
+      client.succeed("[[ $(cat /public/foo) = bar ]]")
+    '';
+})
diff --git a/nixos/tests/sanoid.nix b/nixos/tests/sanoid.nix
new file mode 100644
index 00000000000..3bdbe0a8d8d
--- /dev/null
+++ b/nixos/tests/sanoid.nix
@@ -0,0 +1,112 @@
+import ./make-test-python.nix ({ pkgs, ... }: let
+  inherit (import ./ssh-keys.nix pkgs)
+    snakeOilPrivateKey snakeOilPublicKey;
+
+  commonConfig = { pkgs, ... }: {
+    virtualisation.emptyDiskImages = [ 2048 ];
+    boot.supportedFilesystems = [ "zfs" ];
+    environment.systemPackages = [ pkgs.parted ];
+  };
+in {
+  name = "sanoid";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lopsided98 ];
+  };
+
+  nodes = {
+    source = { ... }: {
+      imports = [ commonConfig ];
+      networking.hostId = "daa82e91";
+
+      programs.ssh.extraConfig = ''
+        UserKnownHostsFile=/dev/null
+        StrictHostKeyChecking=no
+      '';
+
+      services.sanoid = {
+        enable = true;
+        templates.test = {
+          hourly = 12;
+          daily = 1;
+          monthly = 1;
+          yearly = 1;
+
+          autosnap = true;
+        };
+        datasets."pool/sanoid".use_template = [ "test" ];
+        extraArgs = [ "--verbose" ];
+      };
+
+      services.syncoid = {
+        enable = true;
+        sshKey = "/var/lib/syncoid/id_ecdsa";
+        commands = {
+          # Sync snapshot taken by sanoid
+          "pool/sanoid" = {
+            target = "root@target:pool/sanoid";
+            extraArgs = [ "--no-sync-snap" "--create-bookmark" ];
+          };
+          # Take snapshot and sync
+          "pool/syncoid".target = "root@target:pool/syncoid";
+        };
+      };
+    };
+    target = { ... }: {
+      imports = [ commonConfig ];
+      networking.hostId = "dcf39d36";
+
+      services.openssh.enable = true;
+      users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
+    };
+  };
+
+  testScript = ''
+    source.succeed(
+        "mkdir /mnt",
+        "parted --script /dev/vdb -- mklabel msdos mkpart primary 1024M -1s",
+        "udevadm settle",
+        "zpool create pool -R /mnt /dev/vdb1",
+        "zfs create pool/sanoid",
+        "zfs create pool/syncoid",
+        "udevadm settle",
+    )
+    target.succeed(
+        "mkdir /mnt",
+        "parted --script /dev/vdb -- mklabel msdos mkpart primary 1024M -1s",
+        "udevadm settle",
+        "zpool create pool -R /mnt /dev/vdb1",
+        "udevadm settle",
+    )
+
+    source.succeed(
+        "mkdir -m 700 -p /var/lib/syncoid",
+        "cat '${snakeOilPrivateKey}' > /var/lib/syncoid/id_ecdsa",
+        "chmod 600 /var/lib/syncoid/id_ecdsa",
+        "chown -R syncoid:syncoid /var/lib/syncoid/",
+    )
+
+    assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set before snapshotting"
+    assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set before snapshotting"
+    assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set before snapshotting"
+
+    # Take snapshot with sanoid
+    source.succeed("touch /mnt/pool/sanoid/test.txt")
+    source.systemctl("start --wait sanoid.service")
+
+    assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set after snapshotting"
+    assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set after snapshotting"
+    assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set after snapshotting"
+
+    # Sync snapshots
+    target.wait_for_open_port(22)
+    source.succeed("touch /mnt/pool/syncoid/test.txt")
+    source.systemctl("start --wait syncoid-pool-sanoid.service")
+    target.succeed("cat /mnt/pool/sanoid/test.txt")
+    source.systemctl("start --wait syncoid-pool-syncoid.service")
+    target.succeed("cat /mnt/pool/syncoid/test.txt")
+
+    assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set after syncing snapshots"
+    assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set after syncing snapshots"
+    assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set after syncing snapshots"
+  '';
+})
diff --git a/nixos/tests/sddm.nix b/nixos/tests/sddm.nix
new file mode 100644
index 00000000000..d7c65fa33d6
--- /dev/null
+++ b/nixos/tests/sddm.nix
@@ -0,0 +1,69 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+
+let
+  inherit (pkgs) lib;
+
+  tests = {
+    default = {
+      name = "sddm";
+
+      machine = { ... }: {
+        imports = [ ./common/user-account.nix ];
+        services.xserver.enable = true;
+        services.xserver.displayManager.sddm.enable = true;
+        services.xserver.displayManager.defaultSession = "none+icewm";
+        services.xserver.windowManager.icewm.enable = true;
+      };
+
+      enableOCR = true;
+
+      testScript = { nodes, ... }: let
+        user = nodes.machine.config.users.users.alice;
+      in ''
+        start_all()
+        machine.wait_for_text("(?i)select your user")
+        machine.screenshot("sddm")
+        machine.send_chars("${user.password}\n")
+        machine.wait_for_file("${user.home}/.Xauthority")
+        machine.succeed("xauth merge ${user.home}/.Xauthority")
+        machine.wait_for_window("^IceWM ")
+      '';
+    };
+
+    autoLogin = {
+      name = "sddm-autologin";
+      meta = with pkgs.lib.maintainers; {
+        maintainers = [ ttuegel ];
+      };
+
+      machine = { ... }: {
+        imports = [ ./common/user-account.nix ];
+        services.xserver.enable = true;
+        services.xserver.displayManager = {
+          sddm.enable = true;
+          autoLogin = {
+            enable = true;
+            user = "alice";
+          };
+        };
+        services.xserver.displayManager.defaultSession = "none+icewm";
+        services.xserver.windowManager.icewm.enable = true;
+      };
+
+      testScript = { nodes, ... }: let
+        user = nodes.machine.config.users.users.alice;
+      in ''
+        start_all()
+        machine.wait_for_file("${user.home}/.Xauthority")
+        machine.succeed("xauth merge ${user.home}/.Xauthority")
+        machine.wait_for_window("^IceWM ")
+      '';
+    };
+  };
+in
+  lib.mapAttrs (lib.const makeTest) tests
diff --git a/nixos/tests/seafile.nix b/nixos/tests/seafile.nix
new file mode 100644
index 00000000000..6eec8b1fbe5
--- /dev/null
+++ b/nixos/tests/seafile.nix
@@ -0,0 +1,121 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+  let
+    client = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.seafile-shared pkgs.curl ];
+    };
+  in {
+    name = "seafile";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ kampfschlaefer schmittlauch ];
+    };
+
+    nodes = {
+      server = { config, pkgs, ... }: {
+        services.seafile = {
+          enable = true;
+          ccnetSettings.General.SERVICE_URL = "http://server";
+          adminEmail = "admin@example.com";
+          initialAdminPassword = "seafile_password";
+        };
+        services.nginx = {
+          enable = true;
+          virtualHosts."server" = {
+            locations."/".proxyPass = "http://unix:/run/seahub/gunicorn.sock";
+            locations."/seafhttp" = {
+              proxyPass = "http://127.0.0.1:8082";
+              extraConfig = ''
+                rewrite ^/seafhttp(.*)$ $1 break;
+                client_max_body_size 0;
+                proxy_connect_timeout  36000s;
+                proxy_read_timeout  36000s;
+                proxy_send_timeout  36000s;
+                send_timeout  36000s;
+                proxy_http_version 1.1;
+              '';
+            };
+          };
+        };
+        networking.firewall = { allowedTCPPorts = [ 80 ]; };
+      };
+      client1 = client pkgs;
+      client2 = client pkgs;
+    };
+
+    testScript = ''
+      start_all()
+
+      with subtest("start seaf-server"):
+          server.wait_for_unit("seaf-server.service")
+          server.wait_for_file("/run/seafile/seafile.sock")
+
+      with subtest("start seahub"):
+          server.wait_for_unit("seahub.service")
+          server.wait_for_unit("nginx.service")
+          server.wait_for_file("/run/seahub/gunicorn.sock")
+
+      with subtest("client1 fetch seahub page"):
+          client1.succeed("curl -L http://server | grep 'Log In' >&2")
+
+      with subtest("client1 connect"):
+          client1.wait_for_unit("default.target")
+          client1.succeed("seaf-cli init -d . >&2")
+          client1.succeed("seaf-cli start >&2")
+          client1.succeed(
+              "seaf-cli list-remote -s http://server -u admin\@example.com -p seafile_password >&2"
+          )
+
+          libid = client1.succeed(
+              'seaf-cli create -s http://server -n test01 -u admin\@example.com -p seafile_password -t "first test library"'
+          ).strip()
+
+          client1.succeed(
+              "seaf-cli list-remote -s http://server -u admin\@example.com -p seafile_password |grep test01"
+          )
+          client1.fail(
+              "seaf-cli list-remote -s http://server -u admin\@example.com -p seafile_password |grep test02"
+          )
+
+          client1.succeed(
+              f"seaf-cli download -l {libid} -s http://server -u admin\@example.com -p seafile_password -d . >&2"
+          )
+
+          client1.sleep(3)
+
+          client1.succeed("seaf-cli status |grep synchronized >&2")
+
+          client1.succeed("ls -la >&2")
+          client1.succeed("ls -la test01 >&2")
+
+          client1.execute("echo bla > test01/first_file")
+
+          client1.sleep(2)
+
+          client1.succeed("seaf-cli status |grep synchronized >&2")
+
+      with subtest("client2 sync"):
+          client2.wait_for_unit("default.target")
+
+          client2.succeed("seaf-cli init -d . >&2")
+          client2.succeed("seaf-cli start >&2")
+
+          client2.succeed(
+              "seaf-cli list-remote -s http://server -u admin\@example.com -p seafile_password >&2"
+          )
+
+          libid = client2.succeed(
+              "seaf-cli list-remote -s http://server -u admin\@example.com -p seafile_password |grep test01 |cut -d' ' -f 2"
+          ).strip()
+
+          client2.succeed(
+              f"seaf-cli download -l {libid} -s http://server -u admin\@example.com -p seafile_password -d . >&2"
+          )
+
+          client2.sleep(3)
+
+          client2.succeed("seaf-cli status |grep synchronized >&2")
+
+          client2.succeed("ls -la test01 >&2")
+
+          client2.succeed('[ `cat test01/first_file` = "bla" ]')
+    '';
+  })
diff --git a/nixos/tests/searx.nix b/nixos/tests/searx.nix
new file mode 100644
index 00000000000..2f808cb6526
--- /dev/null
+++ b/nixos/tests/searx.nix
@@ -0,0 +1,114 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+{
+  name = "searx";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ rnhmjoj ];
+  };
+
+  # basic setup: searx running the built-in webserver
+  nodes.base = { ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+
+    services.searx = {
+      enable = true;
+      environmentFile = pkgs.writeText "secrets" ''
+        WOLFRAM_API_KEY  = sometoken
+        SEARX_SECRET_KEY = somesecret
+      '';
+
+      settings.server =
+        { port = "8080";
+          bind_address = "0.0.0.0";
+          secret_key = "@SEARX_SECRET_KEY@";
+        };
+      settings.engines = [
+        { name = "wolframalpha";
+          api_key = "@WOLFRAM_API_KEY@";
+          engine = "wolframalpha_api";
+        }
+        { name = "startpage";
+          shortcut = "start";
+        }
+      ];
+    };
+
+  };
+
+  # fancy setup: run in uWSGI and use nginx as proxy
+  nodes.fancy = { ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+
+    services.searx = {
+      enable = true;
+      # searx refuses to run if unchanged
+      settings.server.secret_key = "somesecret";
+
+      runInUwsgi = true;
+      uwsgiConfig = {
+        # serve using the uwsgi protocol
+        socket = "/run/searx/uwsgi.sock";
+        chmod-socket = "660";
+
+        # use /searx as url "mountpoint"
+        mount = "/searx=searx.webapp:application";
+        module = "";
+        manage-script-name = true;
+      };
+    };
+
+    # use nginx as reverse proxy
+    services.nginx.enable = true;
+    services.nginx.virtualHosts.localhost = {
+      locations."/searx".extraConfig =
+        ''
+          include ${pkgs.nginx}/conf/uwsgi_params;
+          uwsgi_pass unix:/run/searx/uwsgi.sock;
+        '';
+      locations."/searx/static/".alias = "${pkgs.searx}/share/static/";
+    };
+
+    # allow nginx access to the searx socket
+    users.users.nginx.extraGroups = [ "searx" ];
+
+  };
+
+  testScript =
+    ''
+      base.start()
+
+      with subtest("Settings have been merged"):
+          base.wait_for_unit("searx-init")
+          base.wait_for_file("/run/searx/settings.yml")
+          output = base.succeed(
+              "${pkgs.yq-go}/bin/yq eval"
+              " '.engines[] | select(.name==\"startpage\") | .shortcut'"
+              " /run/searx/settings.yml"
+          ).strip()
+          assert output == "start", "Settings not merged"
+
+      with subtest("Environment variables have been substituted"):
+          base.succeed("grep -q somesecret /run/searx/settings.yml")
+          base.succeed("grep -q sometoken /run/searx/settings.yml")
+          base.copy_from_vm("/run/searx/settings.yml")
+
+      with subtest("Basic setup is working"):
+          base.wait_for_open_port(8080)
+          base.wait_for_unit("searx")
+          base.succeed(
+              "${pkgs.curl}/bin/curl --fail http://localhost:8080"
+          )
+          base.shutdown()
+
+      with subtest("Nginx+uWSGI setup is working"):
+          fancy.start()
+          fancy.wait_for_open_port(80)
+          fancy.wait_for_unit("uwsgi")
+          fancy.succeed(
+              "${pkgs.curl}/bin/curl --fail http://localhost/searx >&2"
+          )
+          fancy.succeed(
+              "${pkgs.curl}/bin/curl --fail http://localhost/searx/static/themes/oscar/js/bootstrap.min.js >&2"
+          )
+    '';
+})
diff --git a/nixos/tests/service-runner.nix b/nixos/tests/service-runner.nix
new file mode 100644
index 00000000000..79d96f739a6
--- /dev/null
+++ b/nixos/tests/service-runner.nix
@@ -0,0 +1,36 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "service-runner";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ roberth ];
+  };
+
+  nodes = {
+    machine = { pkgs, lib, ... }: {
+      services.nginx.enable = true;
+      services.nginx.virtualHosts.machine.root = pkgs.runCommand "webroot" {} ''
+        mkdir $out
+        echo 'yay' >$out/index.html
+      '';
+      systemd.services.nginx.enable = false;
+    };
+
+  };
+
+  testScript = { nodes, ... }: ''
+    url = "http://localhost/index.html"
+
+    with subtest("check systemd.services.nginx.runner"):
+        machine.fail(f"curl {url}")
+        machine.succeed(
+            """
+            mkdir -p /run/nginx /var/log/nginx /var/cache/nginx
+            ${nodes.machine.config.systemd.services.nginx.runner} >&2 &
+            echo $!>my-nginx.pid
+            """
+        )
+        machine.wait_for_open_port(80)
+        machine.succeed(f"curl -f {url}")
+        machine.succeed("kill -INT $(cat my-nginx.pid)")
+        machine.wait_for_closed_port(80)
+  '';
+})
diff --git a/nixos/tests/shadow.nix b/nixos/tests/shadow.nix
new file mode 100644
index 00000000000..dd2a575b193
--- /dev/null
+++ b/nixos/tests/shadow.nix
@@ -0,0 +1,119 @@
+let
+  password1 = "foobar";
+  password2 = "helloworld";
+  password3 = "bazqux";
+  password4 = "asdf123";
+in import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "shadow";
+  meta = with pkgs.lib.maintainers; { maintainers = [ nequissimus ]; };
+
+  nodes.shadow = { pkgs, ... }: {
+    environment.systemPackages = [ pkgs.shadow ];
+
+    users = {
+      mutableUsers = true;
+      users.emma = {
+        isNormalUser = true;
+        password = password1;
+        shell = pkgs.bash;
+      };
+      users.layla = {
+        isNormalUser = true;
+        password = password2;
+        shell = pkgs.shadow;
+      };
+      users.ash = {
+        isNormalUser = true;
+        password = password4;
+        shell = pkgs.bash;
+      };
+    };
+  };
+
+  testScript = ''
+    shadow.wait_for_unit("multi-user.target")
+    shadow.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
+
+    with subtest("Normal login"):
+        shadow.send_key("alt-f2")
+        shadow.wait_until_succeeds("[ $(fgconsole) = 2 ]")
+        shadow.wait_for_unit("getty@tty2.service")
+        shadow.wait_until_succeeds("pgrep -f 'agetty.*tty2'")
+        shadow.wait_until_tty_matches(2, "login: ")
+        shadow.send_chars("emma\n")
+        shadow.wait_until_tty_matches(2, "login: emma")
+        shadow.wait_until_succeeds("pgrep login")
+        shadow.sleep(2)
+        shadow.send_chars("${password1}\n")
+        shadow.send_chars("whoami > /tmp/1\n")
+        shadow.wait_for_file("/tmp/1")
+        assert "emma" in shadow.succeed("cat /tmp/1")
+
+    with subtest("Switch user"):
+        shadow.send_chars("su - ash\n")
+        shadow.sleep(2)
+        shadow.send_chars("${password4}\n")
+        shadow.sleep(2)
+        shadow.send_chars("whoami > /tmp/3\n")
+        shadow.wait_for_file("/tmp/3")
+        assert "ash" in shadow.succeed("cat /tmp/3")
+
+    with subtest("Change password"):
+        shadow.send_key("alt-f3")
+        shadow.wait_until_succeeds("[ $(fgconsole) = 3 ]")
+        shadow.wait_for_unit("getty@tty3.service")
+        shadow.wait_until_succeeds("pgrep -f 'agetty.*tty3'")
+        shadow.wait_until_tty_matches(3, "login: ")
+        shadow.send_chars("emma\n")
+        shadow.wait_until_tty_matches(3, "login: emma")
+        shadow.wait_until_succeeds("pgrep login")
+        shadow.sleep(2)
+        shadow.send_chars("${password1}\n")
+        shadow.send_chars("passwd\n")
+        shadow.sleep(2)
+        shadow.send_chars("${password1}\n")
+        shadow.sleep(2)
+        shadow.send_chars("${password3}\n")
+        shadow.sleep(2)
+        shadow.send_chars("${password3}\n")
+        shadow.sleep(2)
+        shadow.send_key("alt-f4")
+        shadow.wait_until_succeeds("[ $(fgconsole) = 4 ]")
+        shadow.wait_for_unit("getty@tty4.service")
+        shadow.wait_until_succeeds("pgrep -f 'agetty.*tty4'")
+        shadow.wait_until_tty_matches(4, "login: ")
+        shadow.send_chars("emma\n")
+        shadow.wait_until_tty_matches(4, "login: emma")
+        shadow.wait_until_succeeds("pgrep login")
+        shadow.sleep(2)
+        shadow.send_chars("${password1}\n")
+        shadow.wait_until_tty_matches(4, "Login incorrect")
+        shadow.wait_until_tty_matches(4, "login:")
+        shadow.send_chars("emma\n")
+        shadow.wait_until_tty_matches(4, "login: emma")
+        shadow.wait_until_succeeds("pgrep login")
+        shadow.sleep(2)
+        shadow.send_chars("${password3}\n")
+        shadow.send_chars("whoami > /tmp/2\n")
+        shadow.wait_for_file("/tmp/2")
+        assert "emma" in shadow.succeed("cat /tmp/2")
+
+    with subtest("Groups"):
+        assert "foobar" not in shadow.succeed("groups emma")
+        shadow.succeed("groupadd foobar")
+        shadow.succeed("usermod -a -G foobar emma")
+        assert "foobar" in shadow.succeed("groups emma")
+
+    with subtest("nologin shell"):
+        shadow.send_key("alt-f5")
+        shadow.wait_until_succeeds("[ $(fgconsole) = 5 ]")
+        shadow.wait_for_unit("getty@tty5.service")
+        shadow.wait_until_succeeds("pgrep -f 'agetty.*tty5'")
+        shadow.wait_until_tty_matches(5, "login: ")
+        shadow.send_chars("layla\n")
+        shadow.wait_until_tty_matches(5, "login: layla")
+        shadow.wait_until_succeeds("pgrep login")
+        shadow.send_chars("${password2}\n")
+        shadow.wait_until_tty_matches(5, "login:")
+  '';
+})
diff --git a/nixos/tests/shadowsocks/common.nix b/nixos/tests/shadowsocks/common.nix
new file mode 100644
index 00000000000..8cbbc3f2068
--- /dev/null
+++ b/nixos/tests/shadowsocks/common.nix
@@ -0,0 +1,84 @@
+{ name
+, plugin ? null
+, pluginOpts ? ""
+}:
+
+import ../make-test-python.nix ({ pkgs, lib, ... }: {
+    inherit name;
+    meta = {
+      maintainers = with lib.maintainers; [ hmenke ];
+    };
+
+    nodes = {
+      server = {
+        boot.kernel.sysctl."net.ipv4.ip_forward" = "1";
+        networking.useDHCP = false;
+        networking.interfaces.eth1.ipv4.addresses = [
+          { address = "192.168.0.1"; prefixLength = 24; }
+        ];
+        networking.firewall.rejectPackets = true;
+        networking.firewall.allowedTCPPorts = [ 8488 ];
+        networking.firewall.allowedUDPPorts = [ 8488 ];
+        services.shadowsocks = {
+          enable = true;
+          encryptionMethod = "chacha20-ietf-poly1305";
+          password = "pa$$w0rd";
+          localAddress = [ "0.0.0.0" ];
+          port = 8488;
+          fastOpen = false;
+          mode = "tcp_and_udp";
+        } // lib.optionalAttrs (plugin != null) {
+          inherit plugin;
+          pluginOpts = "server;${pluginOpts}";
+        };
+        services.nginx = {
+          enable = true;
+          virtualHosts.server = {
+            locations."/".root = pkgs.writeTextDir "index.html" "It works!";
+          };
+        };
+      };
+
+      client = {
+        networking.useDHCP = false;
+        networking.interfaces.eth1.ipv4.addresses = [
+          { address = "192.168.0.2"; prefixLength = 24; }
+        ];
+        systemd.services.shadowsocks-client = {
+          description = "connect to shadowsocks";
+          after = [ "network.target" ];
+          wantedBy = [ "multi-user.target" ];
+          path = with pkgs; [ shadowsocks-libev ];
+          script = ''
+            exec ss-local \
+                -s 192.168.0.1 \
+                -p 8488 \
+                -l 1080 \
+                -k 'pa$$w0rd' \
+                -m chacha20-ietf-poly1305 \
+                -a nobody \
+                ${lib.optionalString (plugin != null) ''
+                  --plugin "${plugin}" --plugin-opts "${pluginOpts}"
+                ''}
+          '';
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      server.wait_for_unit("shadowsocks-libev.service")
+      client.wait_for_unit("shadowsocks-client.service")
+
+      client.fail(
+          "${pkgs.curl}/bin/curl 192.168.0.1:80"
+      )
+
+      msg = client.succeed(
+          "${pkgs.curl}/bin/curl --socks5 localhost:1080 192.168.0.1:80"
+      )
+      assert msg == "It works!", "Could not connect through shadowsocks"
+    '';
+  }
+)
diff --git a/nixos/tests/shadowsocks/default.nix b/nixos/tests/shadowsocks/default.nix
new file mode 100644
index 00000000000..37a8c3c9d0d
--- /dev/null
+++ b/nixos/tests/shadowsocks/default.nix
@@ -0,0 +1,16 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../../.. { inherit system config; }
+}:
+
+{
+  "basic" = import ./common.nix {
+    name = "basic";
+  };
+
+  "v2ray-plugin" = import ./common.nix {
+    name = "v2ray-plugin";
+    plugin = "${pkgs.shadowsocks-v2ray-plugin}/bin/v2ray-plugin";
+    pluginOpts = "host=nixos.org";
+  };
+}
diff --git a/nixos/tests/shattered-pixel-dungeon.nix b/nixos/tests/shattered-pixel-dungeon.nix
new file mode 100644
index 00000000000..d4e5de22ab9
--- /dev/null
+++ b/nixos/tests/shattered-pixel-dungeon.nix
@@ -0,0 +1,30 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "shattered-pixel-dungeon";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fgaz ];
+  };
+
+  machine = { config, pkgs, ... }: {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    sound.enable = true;
+    environment.systemPackages = [ pkgs.shattered-pixel-dungeon ];
+  };
+
+  enableOCR = true;
+
+  testScript =
+    ''
+      machine.wait_for_x()
+      machine.execute("shattered-pixel-dungeon >&2 &")
+      machine.wait_for_window(r"Shattered Pixel Dungeon")
+      machine.sleep(5)
+      if "Enter" not in machine.get_screen_text():
+          raise Exception("Program did not start successfully")
+      machine.screenshot("screen")
+    '';
+})
+
diff --git a/nixos/tests/shiori.nix b/nixos/tests/shiori.nix
new file mode 100644
index 00000000000..6c59c394009
--- /dev/null
+++ b/nixos/tests/shiori.nix
@@ -0,0 +1,80 @@
+import ./make-test-python.nix ({ pkgs, lib, ...}:
+
+{
+  name = "shiori";
+  meta.maintainers = with lib.maintainers; [ minijackson ];
+
+  machine =
+    { ... }:
+    { services.shiori.enable = true; };
+
+  testScript = let
+    authJSON = pkgs.writeText "auth.json" (builtins.toJSON {
+      username = "shiori";
+      password = "gopher";
+      owner = true;
+    });
+
+  insertBookmark = {
+    url = "http://example.org";
+    title = "Example Bookmark";
+  };
+
+  insertBookmarkJSON = pkgs.writeText "insertBookmark.json" (builtins.toJSON insertBookmark);
+  in ''
+    import json
+
+    machine.wait_for_unit("shiori.service")
+    machine.wait_for_open_port(8080)
+    machine.succeed("curl --fail http://localhost:8080/")
+    machine.succeed("curl --fail --location http://localhost:8080/ | grep -i shiori")
+
+    with subtest("login"):
+        auth_json = machine.succeed(
+            "curl --fail --location http://localhost:8080/api/login "
+            "-X POST -H 'Content-Type:application/json' -d @${authJSON}"
+        )
+        auth_ret = json.loads(auth_json)
+        session_id = auth_ret["session"]
+
+    with subtest("bookmarks"):
+        with subtest("first use no bookmarks"):
+            bookmarks_json = machine.succeed(
+                (
+                    "curl --fail --location http://localhost:8080/api/bookmarks "
+                    "-H 'X-Session-Id:{}'"
+                ).format(session_id)
+            )
+
+            if json.loads(bookmarks_json)["bookmarks"] != []:
+                raise Exception("Shiori have a bookmark on first use")
+
+        with subtest("insert bookmark"):
+            machine.succeed(
+                (
+                    "curl --fail --location http://localhost:8080/api/bookmarks "
+                    "-X POST -H 'X-Session-Id:{}' "
+                    "-H 'Content-Type:application/json' -d @${insertBookmarkJSON}"
+                ).format(session_id)
+            )
+
+        with subtest("get inserted bookmark"):
+            bookmarks_json = machine.succeed(
+                (
+                    "curl --fail --location http://localhost:8080/api/bookmarks "
+                    "-H 'X-Session-Id:{}'"
+                ).format(session_id)
+            )
+
+            bookmarks = json.loads(bookmarks_json)["bookmarks"]
+            if len(bookmarks) != 1:
+                raise Exception("Shiori didn't save the bookmark")
+
+            bookmark = bookmarks[0]
+            if (
+                bookmark["url"] != "${insertBookmark.url}"
+                or bookmark["title"] != "${insertBookmark.title}"
+            ):
+                raise Exception("Inserted bookmark doesn't have same URL or title")
+  '';
+})
diff --git a/nixos/tests/signal-desktop.nix b/nixos/tests/signal-desktop.nix
new file mode 100644
index 00000000000..8c723062992
--- /dev/null
+++ b/nixos/tests/signal-desktop.nix
@@ -0,0 +1,69 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+let
+  sqlcipher-signal = pkgs.writeShellScriptBin "sqlcipher" ''
+    set -eu
+
+    readonly CFG=~/.config/Signal/config.json
+    readonly KEY="$(${pkgs.jq}/bin/jq --raw-output '.key' $CFG)"
+    readonly DB="$1"
+    readonly SQL="SELECT * FROM sqlite_master where type='table'"
+    ${pkgs.sqlcipher}/bin/sqlcipher "$DB" "PRAGMA key = \"x'$KEY'\"; $SQL"
+  '';
+in {
+  name = "signal-desktop";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ flokli primeos ];
+  };
+
+  machine = { ... }:
+
+  {
+    imports = [
+      ./common/user-account.nix
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    test-support.displayManager.auto.user = "alice";
+    environment.systemPackages = with pkgs; [
+      signal-desktop file sqlite sqlcipher-signal
+    ];
+  };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+  in ''
+    start_all()
+    machine.wait_for_x()
+
+    # start signal desktop
+    machine.execute("su - alice -c signal-desktop >&2 &")
+
+    # Wait for the Signal window to appear. Since usually the tests
+    # are run sandboxed and therfore with no internet, we can not wait
+    # for the message "Link your phone ...". Nor should we wait for
+    # the "Failed to connect to server" message, because when manually
+    # running this test it will be not sandboxed.
+    machine.wait_for_text("Signal")
+    machine.wait_for_text("File Edit View Window Help")
+    machine.screenshot("signal_desktop")
+
+    # Test if the database is encrypted to prevent these issues:
+    # - https://github.com/NixOS/nixpkgs/issues/108772
+    # - https://github.com/NixOS/nixpkgs/pull/117555
+    print(machine.succeed("su - alice -c 'file ~/.config/Signal/sql/db.sqlite'"))
+    machine.fail(
+        "su - alice -c 'file ~/.config/Signal/sql/db.sqlite' | grep -e SQLite -e database"
+    )
+    # Only SQLCipher should be able to read the encrypted DB:
+    machine.fail(
+        "su - alice -c 'sqlite3 ~/.config/Signal/sql/db.sqlite .databases'"
+    )
+    print(machine.succeed(
+        "su - alice -c 'sqlcipher ~/.config/Signal/sql/db.sqlite'"
+    ))
+  '';
+})
diff --git a/nixos/tests/simple.nix b/nixos/tests/simple.nix
new file mode 100644
index 00000000000..b4d90f750ec
--- /dev/null
+++ b/nixos/tests/simple.nix
@@ -0,0 +1,17 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "simple";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eelco ];
+  };
+
+  machine = { ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+  };
+
+  testScript =
+    ''
+      start_all()
+      machine.wait_for_unit("multi-user.target")
+      machine.shutdown()
+    '';
+})
diff --git a/nixos/tests/slurm.nix b/nixos/tests/slurm.nix
new file mode 100644
index 00000000000..a6b02e970b0
--- /dev/null
+++ b/nixos/tests/slurm.nix
@@ -0,0 +1,168 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+let
+    slurmconfig = {
+      services.slurm = {
+        controlMachine = "control";
+        nodeName = [ "node[1-3] CPUs=1 State=UNKNOWN" ];
+        partitionName = [ "debug Nodes=node[1-3] Default=YES MaxTime=INFINITE State=UP" ];
+        extraConfig = ''
+          AccountingStorageHost=dbd
+          AccountingStorageType=accounting_storage/slurmdbd
+        '';
+      };
+      environment.systemPackages = [ mpitest ];
+      networking.firewall.enable = false;
+      systemd.tmpfiles.rules = [
+        "f /etc/munge/munge.key 0400 munge munge - mungeverryweakkeybuteasytointegratoinatest"
+      ];
+    };
+
+    mpitest = let
+      mpitestC = pkgs.writeText "mpitest.c" ''
+        #include <stdio.h>
+        #include <stdlib.h>
+        #include <mpi.h>
+
+        int
+        main (int argc, char *argv[])
+        {
+          int rank, size, length;
+          char name[512];
+
+          MPI_Init (&argc, &argv);
+          MPI_Comm_rank (MPI_COMM_WORLD, &rank);
+          MPI_Comm_size (MPI_COMM_WORLD, &size);
+          MPI_Get_processor_name (name, &length);
+
+          if ( rank == 0 ) printf("size=%d\n", size);
+
+          printf ("%s: hello world from process %d of %d\n", name, rank, size);
+
+          MPI_Finalize ();
+
+          return EXIT_SUCCESS;
+        }
+      '';
+    in pkgs.runCommand "mpitest" {} ''
+      mkdir -p $out/bin
+      ${pkgs.openmpi}/bin/mpicc ${mpitestC} -o $out/bin/mpitest
+    '';
+in {
+  name = "slurm";
+
+  meta.maintainers = [ lib.maintainers.markuskowa ];
+
+  nodes =
+    let
+    computeNode =
+      { ...}:
+      {
+        imports = [ slurmconfig ];
+        # TODO slurmd port and slurmctld port should be configurations and
+        # automatically allowed by the  firewall.
+        services.slurm = {
+          client.enable = true;
+        };
+      };
+    in {
+
+    control =
+      { ...}:
+      {
+        imports = [ slurmconfig ];
+        services.slurm = {
+          server.enable = true;
+        };
+      };
+
+    submit =
+      { ...}:
+      {
+        imports = [ slurmconfig ];
+        services.slurm = {
+          enableStools = true;
+        };
+      };
+
+    dbd =
+      { pkgs, ... } :
+      let
+        passFile = pkgs.writeText "dbdpassword" "password123";
+      in {
+        networking.firewall.enable = false;
+        systemd.tmpfiles.rules = [
+          "f /etc/munge/munge.key 0400 munge munge - mungeverryweakkeybuteasytointegratoinatest"
+        ];
+        services.slurm.dbdserver = {
+          enable = true;
+          storagePassFile = "${passFile}";
+        };
+        services.mysql = {
+          enable = true;
+          package = pkgs.mariadb;
+          initialScript = pkgs.writeText "mysql-init.sql" ''
+            CREATE USER 'slurm'@'localhost' IDENTIFIED BY 'password123';
+            GRANT ALL PRIVILEGES ON slurm_acct_db.* TO 'slurm'@'localhost';
+          '';
+          ensureDatabases = [ "slurm_acct_db" ];
+          ensureUsers = [{
+            ensurePermissions = { "slurm_acct_db.*" = "ALL PRIVILEGES"; };
+            name = "slurm";
+          }];
+          settings.mysqld = {
+            # recommendations from: https://slurm.schedmd.com/accounting.html#mysql-configuration
+            innodb_buffer_pool_size="1024M";
+            innodb_log_file_size="64M";
+            innodb_lock_wait_timeout=900;
+          };
+        };
+      };
+
+    node1 = computeNode;
+    node2 = computeNode;
+    node3 = computeNode;
+  };
+
+
+  testScript =
+  ''
+  start_all()
+
+  # Make sure DBD is up after DB initialzation
+  with subtest("can_start_slurmdbd"):
+      dbd.succeed("systemctl restart slurmdbd")
+      dbd.wait_for_unit("slurmdbd.service")
+      dbd.wait_for_open_port(6819)
+
+  # there needs to be an entry for the current
+  # cluster in the database before slurmctld is restarted
+  with subtest("add_account"):
+      control.succeed("sacctmgr -i add cluster default")
+      # check for cluster entry
+      control.succeed("sacctmgr list cluster | awk '{ print $1 }' | grep default")
+
+  with subtest("can_start_slurmctld"):
+      control.succeed("systemctl restart slurmctld")
+      control.wait_for_unit("slurmctld.service")
+
+  with subtest("can_start_slurmd"):
+      for node in [node1, node2, node3]:
+          node.succeed("systemctl restart slurmd.service")
+          node.wait_for_unit("slurmd")
+
+  # Test that the cluster works and can distribute jobs;
+
+  with subtest("run_distributed_command"):
+      # Run `hostname` on 3 nodes of the partition (so on all the 3 nodes).
+      # The output must contain the 3 different names
+      submit.succeed("srun -N 3 hostname | sort | uniq | wc -l | xargs test 3 -eq")
+
+      with subtest("check_slurm_dbd"):
+          # find the srun job from above in the database
+          control.succeed("sleep 5")
+          control.succeed("sacct | grep hostname")
+
+  with subtest("run_PMIx_mpitest"):
+      submit.succeed("srun -N 3 --mpi=pmix mpitest | grep size=3")
+  '';
+})
diff --git a/nixos/tests/smokeping.nix b/nixos/tests/smokeping.nix
new file mode 100644
index 00000000000..ccacf60cfe4
--- /dev/null
+++ b/nixos/tests/smokeping.nix
@@ -0,0 +1,34 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "smokeping";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ cransom ];
+  };
+
+  nodes = {
+    sm =
+      { ... }:
+      {
+        networking.domain = "example.com"; # FQDN: sm.example.com
+        services.smokeping = {
+          enable = true;
+          port = 8081;
+          mailHost = "127.0.0.2";
+          probeConfig = ''
+            + FPing
+            binary = /run/wrappers/bin/fping
+            offset = 0%
+          '';
+        };
+      };
+  };
+
+  testScript = ''
+    start_all()
+    sm.wait_for_unit("smokeping")
+    sm.wait_for_unit("thttpd")
+    sm.wait_for_file("/var/lib/smokeping/data/Local/LocalMachine.rrd")
+    sm.succeed("curl -s -f localhost:8081/smokeping.fcgi?target=Local")
+    sm.succeed("ls /var/lib/smokeping/cache/Local/LocalMachine_mini.png")
+    sm.succeed("ls /var/lib/smokeping/cache/index.html")
+  '';
+})
diff --git a/nixos/tests/snapcast.nix b/nixos/tests/snapcast.nix
new file mode 100644
index 00000000000..30b8343e2ff
--- /dev/null
+++ b/nixos/tests/snapcast.nix
@@ -0,0 +1,89 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+let
+  port = 10004;
+  tcpPort = 10005;
+  httpPort = 10080;
+  tcpStreamPort = 10006;
+  bufferSize = 742;
+in {
+  name = "snapcast";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ hexa ];
+  };
+
+  nodes = {
+    server = {
+      services.snapserver = {
+        enable = true;
+        port = port;
+        tcp.port = tcpPort;
+        http.port = httpPort;
+        buffer = bufferSize;
+        streams = {
+          mpd = {
+            type = "pipe";
+            location = "/run/snapserver/mpd";
+            query.mode = "create";
+          };
+          bluetooth = {
+            type = "pipe";
+            location = "/run/snapserver/bluetooth";
+          };
+          tcp = {
+            type = "tcp";
+            location = "127.0.0.1:${toString tcpStreamPort}";
+          };
+          meta = {
+            type = "meta";
+            location = "/mpd/bluetooth/tcp";
+          };
+        };
+      };
+      environment.systemPackages = [ pkgs.snapcast ];
+    };
+    client = {
+      environment.systemPackages = [ pkgs.snapcast ];
+    };
+  };
+
+  testScript = ''
+    import json
+
+    get_rpc_version = {"id": "1", "jsonrpc": "2.0", "method": "Server.GetRPCVersion"}
+
+    start_all()
+
+    server.wait_for_unit("snapserver.service")
+    server.wait_until_succeeds("ss -ntl | grep -q ${toString port}")
+    server.wait_until_succeeds("ss -ntl | grep -q ${toString tcpPort}")
+    server.wait_until_succeeds("ss -ntl | grep -q ${toString httpPort}")
+    server.wait_until_succeeds("ss -ntl | grep -q ${toString tcpStreamPort}")
+
+    with subtest("check that pipes are created"):
+        server.succeed("test -p /run/snapserver/mpd")
+        server.succeed("test -p /run/snapserver/bluetooth")
+
+    with subtest("test tcp json-rpc"):
+        server.succeed(f"echo '{json.dumps(get_rpc_version)}' | nc -w 1 localhost ${toString tcpPort}")
+
+    with subtest("test http json-rpc"):
+        server.succeed(
+            "curl --fail http://localhost:${toString httpPort}/jsonrpc -d '{json.dumps(get_rpc_version)}'"
+        )
+
+    with subtest("test a ipv6 connection"):
+        server.execute("systemd-run --unit=snapcast-local-client snapclient -h ::1 -p ${toString port}")
+        server.wait_until_succeeds(
+            "journalctl -o cat -u snapserver.service | grep -q 'Hello from'"
+        )
+        server.wait_until_succeeds("journalctl -o cat -u snapcast-local-client | grep -q 'buffer: ${toString bufferSize}'")
+
+    with subtest("test a connection"):
+        client.execute("systemd-run --unit=snapcast-client snapclient -h server -p ${toString port}")
+        server.wait_until_succeeds(
+            "journalctl -o cat -u snapserver.service | grep -q 'Hello from'"
+        )
+        client.wait_until_succeeds("journalctl -o cat -u snapcast-client | grep -q 'buffer: ${toString bufferSize}'")
+  '';
+})
diff --git a/nixos/tests/snapper.nix b/nixos/tests/snapper.nix
new file mode 100644
index 00000000000..098d8d9d72f
--- /dev/null
+++ b/nixos/tests/snapper.nix
@@ -0,0 +1,35 @@
+import ./make-test-python.nix ({ ... }:
+{
+  name = "snapper";
+
+  machine = { pkgs, lib, ... }: {
+    boot.initrd.postDeviceCommands = ''
+      ${pkgs.btrfs-progs}/bin/mkfs.btrfs -f -L aux /dev/vdb
+    '';
+
+    virtualisation.emptyDiskImages = [ 4096 ];
+
+    virtualisation.fileSystems = {
+      "/home" = {
+        device = "/dev/disk/by-label/aux";
+        fsType = "btrfs";
+      };
+    };
+    services.snapper.configs.home.subvolume = "/home";
+    services.snapper.filters = "/nix";
+  };
+
+  testScript = ''
+    machine.succeed("btrfs subvolume create /home/.snapshots")
+    machine.succeed("snapper -c home list")
+    machine.succeed("snapper -c home create --description empty")
+    machine.succeed("echo test > /home/file")
+    machine.succeed("snapper -c home create --description file")
+    machine.succeed("snapper -c home status 1..2")
+    machine.succeed("snapper -c home undochange 1..2")
+    machine.fail("ls /home/file")
+    machine.succeed("snapper -c home delete 2")
+    machine.succeed("systemctl --wait start snapper-timeline.service")
+    machine.succeed("systemctl --wait start snapper-cleanup.service")
+  '';
+})
diff --git a/nixos/tests/soapui.nix b/nixos/tests/soapui.nix
new file mode 100644
index 00000000000..76a87ed5efa
--- /dev/null
+++ b/nixos/tests/soapui.nix
@@ -0,0 +1,24 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "soapui";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ asbachb ];
+  };
+
+  machine = { config, pkgs, ... }: {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+
+    environment.systemPackages = [ pkgs.soapui ];
+  };
+
+  testScript = ''
+    machine.wait_for_x()
+    machine.succeed("soapui >&2 &")
+    machine.wait_for_window(r"SoapUI \d+\.\d+\.\d+")
+    machine.sleep(1)
+    machine.screenshot("soapui")
+  '';
+})
diff --git a/nixos/tests/sogo.nix b/nixos/tests/sogo.nix
new file mode 100644
index 00000000000..acdad8d0f47
--- /dev/null
+++ b/nixos/tests/sogo.nix
@@ -0,0 +1,58 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "sogo";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ajs124 das_j ];
+  };
+
+  nodes = {
+    sogo = { config, pkgs, ... }: {
+      services.nginx.enable = true;
+
+      services.mysql = {
+        enable = true;
+        package = pkgs.mariadb;
+        ensureDatabases = [ "sogo" ];
+        ensureUsers = [{
+          name = "sogo";
+          ensurePermissions = {
+            "sogo.*" = "ALL PRIVILEGES";
+          };
+        }];
+      };
+
+      services.sogo = {
+        enable = true;
+        timezone = "Europe/Berlin";
+        extraConfig = ''
+          WOWorkersCount = 1;
+
+          SOGoUserSources = (
+            {
+              type = sql;
+              userPasswordAlgorithm = md5;
+              viewURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_users";
+              canAuthenticate = YES;
+              id = users;
+            }
+          );
+
+          SOGoProfileURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_user_profile";
+          OCSFolderInfoURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_folder_info";
+          OCSSessionsFolderURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_sessions_folder";
+          OCSEMailAlarmsFolderURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_alarms_folder";
+          OCSStoreURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_store";
+          OCSAclURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_acl";
+          OCSCacheFolderURL = "mysql://sogo@%2Frun%2Fmysqld%2Fmysqld.sock/sogo/sogo_cache_folder";
+        '';
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    sogo.wait_for_unit("multi-user.target")
+    sogo.wait_for_open_port(20000)
+    sogo.wait_for_open_port(80)
+    sogo.succeed("curl -sSfL http://sogo/SOGo")
+  '';
+})
diff --git a/nixos/tests/solanum.nix b/nixos/tests/solanum.nix
new file mode 100644
index 00000000000..1ecf91bce40
--- /dev/null
+++ b/nixos/tests/solanum.nix
@@ -0,0 +1,97 @@
+let
+  clients = [
+    "ircclient1"
+    "ircclient2"
+  ];
+  server = "solanum";
+  ircPort = 6667;
+  channel = "nixos-cat";
+  iiDir = "/tmp/irc";
+in
+
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "solanum";
+  nodes = {
+    "${server}" = {
+      networking.firewall.allowedTCPPorts = [ ircPort ];
+      services.solanum = {
+        enable = true;
+        motd = ''
+          The default MOTD doesn't contain the word "nixos" in it.
+          This one does.
+        '';
+      };
+    };
+  } // lib.listToAttrs (builtins.map (client: lib.nameValuePair client {
+    imports = [
+      ./common/user-account.nix
+    ];
+
+    systemd.services.ii = {
+      requires = [ "network.target" ];
+      wantedBy = [ "default.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        ExecPreStartPre = "mkdir -p ${iiDir}";
+        ExecStart = ''
+          ${lib.getBin pkgs.ii}/bin/ii -n ${client} -s ${server} -i ${iiDir}
+        '';
+        User = "alice";
+      };
+    };
+  }) clients);
+
+  testScript =
+    let
+      msg = client: "Hello, my name is ${client}";
+      clientScript = client: [
+        ''
+          ${client}.wait_for_unit("network.target")
+          ${client}.systemctl("start ii")
+          ${client}.wait_for_unit("ii")
+          ${client}.wait_for_file("${iiDir}/${server}/out")
+        ''
+        # look for the custom text in the MOTD.
+        ''
+          ${client}.wait_until_succeeds("grep 'nixos' ${iiDir}/${server}/out")
+        ''
+        # wait until first PING from server arrives before joining,
+        # so we don't try it too early
+        ''
+          ${client}.wait_until_succeeds("grep 'PING' ${iiDir}/${server}/out")
+        ''
+        # join ${channel}
+        ''
+          ${client}.succeed("echo '/j #${channel}' > ${iiDir}/${server}/in")
+          ${client}.wait_for_file("${iiDir}/${server}/#${channel}/in")
+        ''
+        # send a greeting
+        ''
+          ${client}.succeed(
+              "echo '${msg client}' > ${iiDir}/${server}/#${channel}/in"
+          )
+        ''
+        # check that all greetings arrived on all clients
+      ] ++ builtins.map (other: ''
+        ${client}.succeed(
+            "grep '${msg other}$' ${iiDir}/${server}/#${channel}/out"
+        )
+      '') clients;
+
+      # foldl', but requires a non-empty list instead of a start value
+      reduce = f: list:
+        builtins.foldl' f (builtins.head list) (builtins.tail list);
+    in ''
+      start_all()
+      ${server}.systemctl("status solanum")
+      ${server}.wait_for_open_port(${toString ircPort})
+
+      # run clientScript for all clients so that every list
+      # entry is executed by every client before advancing
+      # to the next one.
+    '' + lib.concatStrings
+      (reduce
+        (lib.zipListsWith (cs: c: cs + c))
+        (builtins.map clientScript clients));
+})
diff --git a/nixos/tests/solr.nix b/nixos/tests/solr.nix
new file mode 100644
index 00000000000..86efe87c707
--- /dev/null
+++ b/nixos/tests/solr.nix
@@ -0,0 +1,56 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "solr";
+  meta.maintainers = [ pkgs.lib.maintainers.aanderse ];
+
+  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/sonarr.nix b/nixos/tests/sonarr.nix
new file mode 100644
index 00000000000..764a4d05b38
--- /dev/null
+++ b/nixos/tests/sonarr.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "sonarr";
+  meta.maintainers = with maintainers; [ etu ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    { services.sonarr.enable = true; };
+
+  testScript = ''
+    machine.wait_for_unit("sonarr.service")
+    machine.wait_for_open_port("8989")
+    machine.succeed("curl --fail http://localhost:8989/")
+  '';
+})
diff --git a/nixos/tests/sourcehut.nix b/nixos/tests/sourcehut.nix
new file mode 100644
index 00000000000..55757e35f9b
--- /dev/null
+++ b/nixos/tests/sourcehut.nix
@@ -0,0 +1,212 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+let
+  domain = "sourcehut.localdomain";
+
+  # Note that wildcard certificates just under the TLD (eg. *.com)
+  # would be rejected by clients like curl.
+  tls-cert = pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
+    openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -days 36500 \
+      -subj '/CN=${domain}' -extensions v3_req \
+      -addext 'subjectAltName = DNS:*.${domain}'
+    install -D -t $out key.pem cert.pem
+  '';
+
+  images = {
+    nixos.unstable.x86_64 =
+      let
+        systemConfig = { pkgs, ... }: {
+          # passwordless ssh server
+          services.openssh = {
+            enable = true;
+            permitRootLogin = "yes";
+            extraConfig = "PermitEmptyPasswords yes";
+          };
+
+          users = {
+            mutableUsers = false;
+            # build user
+            extraUsers."build" = {
+              isNormalUser = true;
+              uid = 1000;
+              extraGroups = [ "wheel" ];
+              password = "";
+            };
+            users.root.password = "";
+          };
+
+          security.sudo.wheelNeedsPassword = false;
+          nix.trustedUsers = [ "root" "build" ];
+          documentation.nixos.enable = false;
+
+          # builds.sr.ht-image-specific network settings
+          networking = {
+            hostName = "build";
+            dhcpcd.enable = false;
+            defaultGateway.address = "10.0.2.2";
+            usePredictableInterfaceNames = false;
+            interfaces."eth0".ipv4.addresses = [{
+              address = "10.0.2.15";
+              prefixLength = 25;
+            }];
+            enableIPv6 = false;
+            nameservers = [
+              # OpenNIC anycast
+              "185.121.177.177"
+              "169.239.202.202"
+              # Google
+              "8.8.8.8"
+            ];
+            firewall.allowedTCPPorts = [ 22 ];
+          };
+
+          environment.systemPackages = [
+            pkgs.gitMinimal
+            #pkgs.mercurial
+            pkgs.curl
+            pkgs.gnupg
+          ];
+        };
+        qemuConfig = { pkgs, ... }: {
+          imports = [ systemConfig ];
+          fileSystems."/".device = "/dev/disk/by-label/nixos";
+          boot.initrd.availableKernelModules = [
+            "ahci"
+            "ehci_pci"
+            "sd_mod"
+            "usb_storage"
+            "usbhid"
+            "virtio_balloon"
+            "virtio_blk"
+            "virtio_pci"
+            "virtio_ring"
+            "xhci_pci"
+          ];
+          boot.loader = {
+            grub = {
+              version = 2;
+              device = "/dev/vda";
+            };
+            timeout = 0;
+          };
+        };
+        config = (import (pkgs.path + "/nixos/lib/eval-config.nix") {
+          inherit pkgs; modules = [ qemuConfig ];
+          system = "x86_64-linux";
+        }).config;
+      in
+      import (pkgs.path + "/nixos/lib/make-disk-image.nix") {
+        inherit pkgs lib config;
+        diskSize = 16000;
+        format = "qcow2-compressed";
+        contents = [
+          { source = pkgs.writeText "gitconfig" ''
+              [user]
+                name = builds.sr.ht
+                email = build@sr.ht
+            '';
+            target = "/home/build/.gitconfig";
+            user = "build";
+            group = "users";
+            mode = "644";
+          }
+        ];
+      };
+  };
+
+in
+{
+  name = "sourcehut";
+
+  meta.maintainers = [ pkgs.lib.maintainers.tomberek ];
+
+  machine = { config, pkgs, nodes, ... }: {
+    # buildsrht needs space
+    virtualisation.diskSize = 4 * 1024;
+    virtualisation.memorySize = 2 * 1024;
+    networking.domain = domain;
+    networking.extraHosts = ''
+      ${config.networking.primaryIPAddress} builds.${domain}
+      ${config.networking.primaryIPAddress} git.${domain}
+      ${config.networking.primaryIPAddress} meta.${domain}
+    '';
+
+    services.sourcehut = {
+      enable = true;
+      services = [
+        "builds"
+        "git"
+        "meta"
+      ];
+      nginx.enable = true;
+      nginx.virtualHost = {
+        forceSSL = true;
+        sslCertificate = "${tls-cert}/cert.pem";
+        sslCertificateKey = "${tls-cert}/key.pem";
+      };
+      postgresql.enable = true;
+      redis.enable = true;
+
+      meta.enable = true;
+      builds = {
+        enable = true;
+        # FIXME: see why it does not seem to activate fully.
+        #enableWorker = true;
+        inherit images;
+      };
+      git.enable = true;
+
+      settings."sr.ht" = {
+        global-domain = config.networking.domain;
+        service-key = pkgs.writeText "service-key" "8b327279b77e32a3620e2fc9aabce491cc46e7d821fd6713b2a2e650ce114d01";
+        network-key = pkgs.writeText "network-key" "cEEmc30BRBGkgQZcHFksiG7hjc6_dK1XR2Oo5Jb9_nQ=";
+      };
+      settings."builds.sr.ht" = {
+        oauth-client-secret = pkgs.writeText "buildsrht-oauth-client-secret" "2260e9c4d9b8dcedcef642860e0504bc";
+        oauth-client-id = "299db9f9c2013170";
+      };
+      settings."git.sr.ht" = {
+        oauth-client-secret = pkgs.writeText "gitsrht-oauth-client-secret" "3597288dc2c716e567db5384f493b09d";
+        oauth-client-id = "d07cb713d920702e";
+      };
+      settings.webhooks.private-key = pkgs.writeText "webhook-key" "Ra3IjxgFiwG9jxgp4WALQIZw/BMYt30xWiOsqD0J7EA=";
+    };
+
+    networking.firewall.allowedTCPPorts = [ 443 ];
+    security.pki.certificateFiles = [ "${tls-cert}/cert.pem" ];
+    services.nginx = {
+      enable = true;
+      recommendedGzipSettings = true;
+      recommendedOptimisation = true;
+      recommendedTlsSettings = true;
+      recommendedProxySettings = true;
+    };
+
+    services.postgresql = {
+      enable = true;
+      enableTCPIP = false;
+      settings.unix_socket_permissions = "0770";
+    };
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("multi-user.target")
+
+    # Testing metasrht
+    machine.wait_for_unit("metasrht-api.service")
+    machine.wait_for_unit("metasrht.service")
+    machine.wait_for_open_port(5000)
+    machine.succeed("curl -sL http://localhost:5000 | grep meta.${domain}")
+    machine.succeed("curl -sL http://meta.${domain} | grep meta.${domain}")
+
+    # Testing buildsrht
+    machine.wait_for_unit("buildsrht.service")
+    machine.wait_for_open_port(5002)
+    machine.succeed("curl -sL http://localhost:5002 | grep builds.${domain}")
+    #machine.wait_for_unit("buildsrht-worker.service")
+
+    # Testing gitsrht
+    machine.wait_for_unit("gitsrht.service")
+    machine.succeed("curl -sL http://git.${domain} | grep git.${domain}")
+  '';
+})
diff --git a/nixos/tests/spacecookie.nix b/nixos/tests/spacecookie.nix
new file mode 100644
index 00000000000..a640657d8a6
--- /dev/null
+++ b/nixos/tests/spacecookie.nix
@@ -0,0 +1,56 @@
+let
+  gopherRoot   = "/tmp/gopher";
+  gopherHost   = "gopherd";
+  gopherClient = "client";
+  fileContent  = "Hello Gopher!\n";
+  fileName     = "file.txt";
+in
+  import ./make-test-python.nix ({...}: {
+    name = "spacecookie";
+    nodes = {
+      ${gopherHost} = {
+        systemd.services.spacecookie = {
+          preStart = ''
+            mkdir -p ${gopherRoot}/directory
+            printf "%s" "${fileContent}" > ${gopherRoot}/${fileName}
+          '';
+        };
+
+        services.spacecookie = {
+          enable = true;
+          openFirewall = true;
+          settings = {
+            root = gopherRoot;
+            hostname = gopherHost;
+          };
+        };
+      };
+
+      ${gopherClient} = {};
+    };
+
+    testScript = ''
+      start_all()
+
+      # with daemon type notify, the unit being started
+      # should also mean the port is open
+      ${gopherHost}.wait_for_unit("spacecookie.service")
+      ${gopherClient}.wait_for_unit("network.target")
+
+      fileResponse = ${gopherClient}.succeed("curl -f -s gopher://${gopherHost}/0/${fileName}")
+
+      # the file response should return our created file exactly
+      if not (fileResponse == "${builtins.replaceStrings [ "\n" ] [ "\\n" ] fileContent}"):
+          raise Exception("Unexpected file response")
+
+      # sanity check on the directory listing: we serve a directory and a file
+      # via gopher, so the directory listing should have exactly two entries,
+      # one with gopher file type 0 (file) and one with file type 1 (directory).
+      dirResponse = ${gopherClient}.succeed("curl -f -s gopher://${gopherHost}")
+      dirEntries = [l[0] for l in dirResponse.split("\n") if len(l) > 0]
+      dirEntries.sort()
+
+      if not (["0", "1"] == dirEntries):
+          raise Exception("Unexpected directory response")
+    '';
+  })
diff --git a/nixos/tests/spark/default.nix b/nixos/tests/spark/default.nix
new file mode 100644
index 00000000000..025c5a5222e
--- /dev/null
+++ b/nixos/tests/spark/default.nix
@@ -0,0 +1,27 @@
+import ../make-test-python.nix ({...}: {
+  name = "spark";
+
+  nodes = {
+    worker = { nodes, pkgs, ... }: {
+      services.spark.worker = {
+        enable = true;
+        master = "master:7077";
+      };
+    };
+    master = { config, pkgs, ... }: {
+      services.spark.master = {
+        enable = true;
+        bind = "0.0.0.0";
+      };
+      networking.firewall.allowedTCPPorts = [ 22 7077 8080 ];
+    };
+  };
+
+  testScript = ''
+    master.wait_for_unit("spark-master.service")
+    worker.wait_for_unit("spark-worker.service")
+    worker.copy_from_host( "${./spark_sample.py}", "/spark_sample.py" )
+    assert "<title>Spark Master at spark://" in worker.succeed("curl -sSfkL http://master:8080/")
+    worker.succeed("spark-submit --master spark://master:7077 --executor-memory 512m --executor-cores 1 /spark_sample.py")
+  '';
+})
diff --git a/nixos/tests/spark/spark_sample.py b/nixos/tests/spark/spark_sample.py
new file mode 100644
index 00000000000..c4939451eae
--- /dev/null
+++ b/nixos/tests/spark/spark_sample.py
@@ -0,0 +1,40 @@
+from pyspark.sql import Row, SparkSession
+from pyspark.sql import functions as F
+from pyspark.sql.functions import udf
+from pyspark.sql.types import *
+from pyspark.sql.functions import explode
+
+def explode_col(weight):
+    return int(weight//10) * [10.0] + ([] if weight%10==0 else [weight%10])
+
+spark = SparkSession.builder.getOrCreate()
+
+dataSchema = [
+    StructField("feature_1", FloatType()),
+    StructField("feature_2", FloatType()),
+    StructField("bias_weight", FloatType())
+]
+
+data = [
+    Row(0.1, 0.2, 10.32),
+    Row(0.32, 1.43, 12.8),
+    Row(1.28, 1.12, 0.23)
+]
+
+df = spark.createDataFrame(spark.sparkContext.parallelize(data), StructType(dataSchema))
+
+normalizing_constant = 100
+sum_bias_weight = df.select(F.sum('bias_weight')).collect()[0][0]
+normalizing_factor = normalizing_constant / sum_bias_weight
+df = df.withColumn('normalized_bias_weight', df.bias_weight * normalizing_factor)
+df = df.drop('bias_weight')
+df = df.withColumnRenamed('normalized_bias_weight', 'bias_weight')
+
+my_udf = udf(lambda x: explode_col(x), ArrayType(FloatType()))
+df1 = df.withColumn('explode_val', my_udf(df.bias_weight))
+df1 = df1.withColumn("explode_val_1", explode(df1.explode_val)).drop("explode_val")
+df1 = df1.drop('bias_weight').withColumnRenamed('explode_val_1', 'bias_weight')
+
+df1.show()
+
+assert(df1.count() == 12)
diff --git a/nixos/tests/specialisation.nix b/nixos/tests/specialisation.nix
new file mode 100644
index 00000000000..b8d4b8279f4
--- /dev/null
+++ b/nixos/tests/specialisation.nix
@@ -0,0 +1,43 @@
+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/ssh-keys.nix b/nixos/tests/ssh-keys.nix
new file mode 100644
index 00000000000..df9ff38a3b2
--- /dev/null
+++ b/nixos/tests/ssh-keys.nix
@@ -0,0 +1,15 @@
+pkgs:
+{ snakeOilPrivateKey = pkgs.writeText "privkey.snakeoil" ''
+    -----BEGIN EC PRIVATE KEY-----
+    MHcCAQEEIHQf/khLvYrQ8IOika5yqtWvI0oquHlpRLTZiJy5dRJmoAoGCCqGSM49
+    AwEHoUQDQgAEKF0DYGbBwbj06tA3fd/+yP44cvmwmHBWXZCKbS+RQlAKvLXMWkpN
+    r1lwMyJZoSGgBHoUahoYjTh9/sJL7XLJtA==
+    -----END EC PRIVATE KEY-----
+  '';
+
+  snakeOilPublicKey = pkgs.lib.concatStrings [
+    "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHA"
+    "yNTYAAABBBChdA2BmwcG49OrQN33f/sj+OHL5sJhwVl2Qim0vkUJQCry1zFpKTa"
+    "9ZcDMiWaEhoAR6FGoaGI04ff7CS+1yybQ= snakeoil"
+  ];
+}
diff --git a/nixos/tests/sslh.nix b/nixos/tests/sslh.nix
new file mode 100644
index 00000000000..17094606e8e
--- /dev/null
+++ b/nixos/tests/sslh.nix
@@ -0,0 +1,83 @@
+import ./make-test-python.nix {
+  name = "sslh";
+
+  nodes = {
+    server = { pkgs, lib, ... }: {
+      networking.firewall.allowedTCPPorts = [ 443 ];
+      networking.interfaces.eth1.ipv6.addresses = [
+        {
+          address = "fe00:aa:bb:cc::2";
+          prefixLength = 64;
+        }
+      ];
+      # sslh is really slow when reverse dns does not work
+      networking.hosts = {
+        "fe00:aa:bb:cc::2" = [ "server" ];
+        "fe00:aa:bb:cc::1" = [ "client" ];
+      };
+      services.sslh = {
+        enable = true;
+        transparent = true;
+        appendConfig = ''
+          protocols:
+          (
+            { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; probe: "builtin"; },
+            { name: "http"; host: "localhost"; port: "80"; probe: "builtin"; },
+          );
+        '';
+      };
+      services.openssh.enable = true;
+      users.users.root.openssh.authorizedKeys.keyFiles = [ ./initrd-network-ssh/id_ed25519.pub ];
+      services.nginx = {
+        enable = true;
+        virtualHosts."localhost" = {
+          addSSL = false;
+          default = true;
+          root = pkgs.runCommand "testdir" {} ''
+            mkdir "$out"
+            echo hello world > "$out/index.html"
+          '';
+        };
+      };
+    };
+    client = { ... }: {
+      networking.interfaces.eth1.ipv6.addresses = [
+        {
+          address = "fe00:aa:bb:cc::1";
+          prefixLength = 64;
+        }
+      ];
+      networking.hosts."fe00:aa:bb:cc::2" = [ "server" ];
+      environment.etc.sshKey = {
+        source = ./initrd-network-ssh/id_ed25519; # dont use this anywhere else
+        mode = "0600";
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    server.wait_for_unit("sslh.service")
+    server.wait_for_unit("nginx.service")
+    server.wait_for_unit("sshd.service")
+    server.wait_for_open_port(80)
+    server.wait_for_open_port(443)
+    server.wait_for_open_port(22)
+
+    for arg in ["-6", "-4"]:
+        client.wait_until_succeeds(f"ping {arg} -c1 server")
+
+        # check that ssh through sslh works
+        client.succeed(
+            f"ssh {arg} -p 443 -i /etc/sshKey -o StrictHostKeyChecking=accept-new server 'echo $SSH_CONNECTION > /tmp/foo{arg}'"
+        )
+
+        # check that 1/ the above ssh command had an effect 2/ transparent proxying really works
+        ip = "fe00:aa:bb:cc::1" if arg == "-6" else "192.168.1."
+        server.succeed(f"grep '{ip}' /tmp/foo{arg}")
+
+        # check that http through sslh works
+        assert client.succeed(f"curl -f {arg} http://server:443").strip() == "hello world"
+  '';
+}
diff --git a/nixos/tests/sssd-ldap.nix b/nixos/tests/sssd-ldap.nix
new file mode 100644
index 00000000000..5c58eaef714
--- /dev/null
+++ b/nixos/tests/sssd-ldap.nix
@@ -0,0 +1,94 @@
+let
+  dbDomain = "example.org";
+  dbSuffix = "dc=example,dc=org";
+
+  ldapRootUser = "admin";
+  ldapRootPassword = "foobar";
+
+  testUser = "alice";
+in import ./make-test-python.nix ({pkgs, ...}: {
+  name = "sssd-ldap";
+
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ bbigras ];
+  };
+
+  machine = { pkgs, ... }: {
+    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/db/openldap";
+              olcSuffix = dbSuffix;
+              olcRootDN = "cn=${ldapRootUser},${dbSuffix}";
+              olcRootPW = ldapRootPassword;
+            };
+          };
+        };
+      };
+      declarativeContents = {
+        ${dbSuffix} = ''
+          dn: ${dbSuffix}
+          objectClass: top
+          objectClass: dcObject
+          objectClass: organization
+          o: ${dbDomain}
+
+          dn: ou=posix,${dbSuffix}
+          objectClass: top
+          objectClass: organizationalUnit
+
+          dn: ou=accounts,ou=posix,${dbSuffix}
+          objectClass: top
+          objectClass: organizationalUnit
+
+          dn: uid=${testUser},ou=accounts,ou=posix,${dbSuffix}
+          objectClass: person
+          objectClass: posixAccount
+          # userPassword: somePasswordHash
+          homeDirectory: /home/${testUser}
+          uidNumber: 1234
+          gidNumber: 1234
+          cn: ""
+          sn: ""
+        '';
+      };
+    };
+
+    services.sssd = {
+      enable = true;
+      config = ''
+        [sssd]
+        config_file_version = 2
+        services = nss, pam, sudo
+        domains = ${dbDomain}
+
+        [domain/${dbDomain}]
+        auth_provider = ldap
+        id_provider = ldap
+        ldap_uri = ldap://127.0.0.1:389
+        ldap_search_base = ${dbSuffix}
+        ldap_default_bind_dn = cn=${ldapRootUser},${dbSuffix}
+        ldap_default_authtok_type = password
+        ldap_default_authtok = ${ldapRootPassword}
+      '';
+    };
+  };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("openldap.service")
+    machine.wait_for_unit("sssd.service")
+    machine.succeed("getent passwd ${testUser}")
+  '';
+})
diff --git a/nixos/tests/sssd.nix b/nixos/tests/sssd.nix
new file mode 100644
index 00000000000..5c1abdca6ae
--- /dev/null
+++ b/nixos/tests/sssd.nix
@@ -0,0 +1,17 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "sssd";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ bbigras ];
+  };
+  machine = { pkgs, ... }: {
+    services.sssd.enable = true;
+  };
+
+  testScript = ''
+      start_all()
+      machine.wait_for_unit("multi-user.target")
+      machine.wait_for_unit("sssd.service")
+    '';
+})
diff --git a/nixos/tests/starship.nix b/nixos/tests/starship.nix
new file mode 100644
index 00000000000..33e9a72f700
--- /dev/null
+++ b/nixos/tests/starship.nix
@@ -0,0 +1,42 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "starship";
+  meta.maintainers = pkgs.starship.meta.maintainers;
+
+  machine = {
+    programs = {
+      fish.enable = true;
+      zsh.enable = true;
+
+      starship = {
+        enable = true;
+        settings.format = "<starship>";
+      };
+    };
+
+    environment.systemPackages = map
+      (shell: pkgs.writeScriptBin "expect-${shell}" ''
+        #!${pkgs.expect}/bin/expect -f
+
+        spawn env TERM=xterm ${shell} -i
+
+        expect "<starship>" {
+          send "exit\n"
+        } timeout {
+          send_user "\n${shell} failed to display Starship\n"
+          exit 1
+        }
+
+        expect eof
+      '')
+      [ "bash" "fish" "zsh" ];
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("default.target")
+
+    machine.succeed("expect-bash")
+    machine.succeed("expect-fish")
+    machine.succeed("expect-zsh")
+  '';
+})
diff --git a/nixos/tests/step-ca.nix b/nixos/tests/step-ca.nix
new file mode 100644
index 00000000000..b22bcb060f2
--- /dev/null
+++ b/nixos/tests/step-ca.nix
@@ -0,0 +1,76 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+  let
+    test-certificates = pkgs.runCommandLocal "test-certificates" { } ''
+      mkdir -p $out
+      echo insecure-root-password > $out/root-password-file
+      echo insecure-intermediate-password > $out/intermediate-password-file
+      ${pkgs.step-cli}/bin/step certificate create "Example Root CA" $out/root_ca.crt $out/root_ca.key --password-file=$out/root-password-file --profile root-ca
+      ${pkgs.step-cli}/bin/step certificate create "Example Intermediate CA 1" $out/intermediate_ca.crt $out/intermediate_ca.key --password-file=$out/intermediate-password-file --ca-password-file=$out/root-password-file --profile intermediate-ca --ca $out/root_ca.crt --ca-key $out/root_ca.key
+    '';
+  in
+  {
+    nodes =
+      {
+        caserver =
+          { config, pkgs, ... }: {
+            services.step-ca = {
+              enable = true;
+              address = "0.0.0.0";
+              port = 8443;
+              openFirewall = true;
+              intermediatePasswordFile = "${test-certificates}/intermediate-password-file";
+              settings = {
+                dnsNames = [ "caserver" ];
+                root = "${test-certificates}/root_ca.crt";
+                crt = "${test-certificates}/intermediate_ca.crt";
+                key = "${test-certificates}/intermediate_ca.key";
+                db = {
+                  type = "badger";
+                  dataSource = "/var/lib/step-ca/db";
+                };
+                authority = {
+                  provisioners = [
+                    {
+                      type = "ACME";
+                      name = "acme";
+                    }
+                  ];
+                };
+              };
+            };
+          };
+
+        caclient =
+          { config, pkgs, ... }: {
+            security.acme.server = "https://caserver:8443/acme/acme/directory";
+            security.acme.email = "root@example.org";
+            security.acme.acceptTerms = true;
+
+            security.pki.certificateFiles = [ "${test-certificates}/root_ca.crt" ];
+
+            networking.firewall.allowedTCPPorts = [ 80 443 ];
+
+            services.nginx = {
+              enable = true;
+              virtualHosts = {
+                "caclient" = {
+                  forceSSL = true;
+                  enableACME = true;
+                };
+              };
+            };
+          };
+
+        catester = { config, pkgs, ... }: {
+          security.pki.certificateFiles = [ "${test-certificates}/root_ca.crt" ];
+        };
+      };
+
+    testScript =
+      ''
+        catester.start()
+        caserver.wait_for_unit("step-ca.service")
+        caclient.wait_for_unit("acme-finished-caclient.target")
+        catester.succeed("curl https://caclient/ | grep \"Welcome to nginx!\"")
+      '';
+  })
diff --git a/nixos/tests/strongswan-swanctl.nix b/nixos/tests/strongswan-swanctl.nix
new file mode 100644
index 00000000000..0cf181ee62a
--- /dev/null
+++ b/nixos/tests/strongswan-swanctl.nix
@@ -0,0 +1,148 @@
+# This strongswan-swanctl test is based on:
+# https://www.strongswan.org/testing/testresults/swanctl/rw-psk-ipv4/index.html
+# https://github.com/strongswan/strongswan/tree/master/testing/tests/swanctl/rw-psk-ipv4
+#
+# The roadwarrior carol sets up a connection to gateway moon. The authentication
+# is based on pre-shared keys and IPv4 addresses. Upon the successful
+# establishment of the IPsec tunnels, the specified updown script automatically
+# inserts iptables-based firewall rules that let pass the tunneled traffic. In
+# order to test both tunnel and firewall, carol pings the client alice behind
+# the gateway moon.
+#
+#     alice                       moon                        carol
+#      eth1------vlan_0------eth1        eth2------vlan_1------eth1
+#   192.168.0.1         192.168.0.3  192.168.1.3           192.168.1.2
+#
+# See the NixOS manual for how to run this test:
+# https://nixos.org/nixos/manual/index.html#sec-running-nixos-tests-interactively
+
+import ./make-test-python.nix ({ pkgs, ...} :
+
+let
+  allowESP = "iptables --insert INPUT --protocol ESP --jump ACCEPT";
+
+  # Shared VPN settings:
+  vlan0         = "192.168.0.0/24";
+  carolIp       = "192.168.1.2";
+  moonIp        = "192.168.1.3";
+  version       = 2;
+  secret        = "0sFpZAZqEN6Ti9sqt4ZP5EWcqx";
+  esp_proposals = [ "aes128gcm128-x25519" ];
+  proposals     = [ "aes128-sha256-x25519" ];
+in {
+  name = "strongswan-swanctl";
+  meta.maintainers = with pkgs.lib.maintainers; [ basvandijk ];
+  nodes = {
+
+    alice = { ... } : {
+      virtualisation.vlans = [ 0 ];
+      networking = {
+        dhcpcd.enable = false;
+        defaultGateway = "192.168.0.3";
+      };
+    };
+
+    moon = { config, ...} :
+      let strongswan = config.services.strongswan-swanctl.package;
+      in {
+        virtualisation.vlans = [ 0 1 ];
+        networking = {
+          dhcpcd.enable = false;
+          firewall = {
+            allowedUDPPorts = [ 4500 500 ];
+            extraCommands = allowESP;
+          };
+          nat = {
+            enable             = true;
+            internalIPs        = [ vlan0 ];
+            internalInterfaces = [ "eth1" ];
+            externalIP         = moonIp;
+            externalInterface  = "eth2";
+          };
+        };
+        environment.systemPackages = [ strongswan ];
+        services.strongswan-swanctl = {
+          enable = true;
+          swanctl = {
+            connections = {
+              rw = {
+                local_addrs = [ moonIp ];
+                local.main = {
+                  auth = "psk";
+                };
+                remote.main = {
+                  auth = "psk";
+                };
+                children = {
+                  net = {
+                    local_ts = [ vlan0 ];
+                    updown = "${strongswan}/libexec/ipsec/_updown iptables";
+                    inherit esp_proposals;
+                  };
+                };
+                inherit version;
+                inherit proposals;
+              };
+            };
+            secrets = {
+              ike.carol = {
+                id.main = carolIp;
+                inherit secret;
+              };
+            };
+          };
+        };
+      };
+
+    carol = { config, ...} :
+      let strongswan = config.services.strongswan-swanctl.package;
+      in {
+        virtualisation.vlans = [ 1 ];
+        networking = {
+          dhcpcd.enable = false;
+          firewall.extraCommands = allowESP;
+        };
+        environment.systemPackages = [ strongswan ];
+        services.strongswan-swanctl = {
+          enable = true;
+          swanctl = {
+            connections = {
+              home = {
+                local_addrs = [ carolIp ];
+                remote_addrs = [ moonIp ];
+                local.main = {
+                  auth = "psk";
+                  id = carolIp;
+                };
+                remote.main = {
+                  auth = "psk";
+                  id = moonIp;
+                };
+                children = {
+                  home = {
+                    remote_ts = [ vlan0 ];
+                    start_action = "trap";
+                    updown = "${strongswan}/libexec/ipsec/_updown iptables";
+                    inherit esp_proposals;
+                  };
+                };
+                inherit version;
+                inherit proposals;
+              };
+            };
+            secrets = {
+              ike.moon = {
+                id.main = moonIp;
+                inherit secret;
+              };
+            };
+          };
+        };
+      };
+
+  };
+  testScript = ''
+    start_all()
+    carol.wait_until_succeeds("ping -c 1 alice")
+  '';
+})
diff --git a/nixos/tests/sudo.nix b/nixos/tests/sudo.nix
new file mode 100644
index 00000000000..661fe9989e7
--- /dev/null
+++ b/nixos/tests/sudo.nix
@@ -0,0 +1,106 @@
+# Some tests to ensure sudo is working properly.
+
+let
+  password = "helloworld";
+
+in
+  import ./make-test-python.nix ({ pkgs, ...} : {
+    name = "sudo";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ lschuermann ];
+    };
+
+    nodes.machine =
+      { lib, ... }:
+      with lib;
+      {
+        users.groups = { foobar = {}; barfoo = {}; baz = { gid = 1337; }; };
+        users.users = {
+          test0 = { isNormalUser = true; extraGroups = [ "wheel" ]; };
+          test1 = { isNormalUser = true; password = password; };
+          test2 = { isNormalUser = true; extraGroups = [ "foobar" ]; password = password; };
+          test3 = { isNormalUser = true; extraGroups = [ "barfoo" ]; };
+          test4 = { isNormalUser = true; extraGroups = [ "baz" ]; };
+          test5 = { isNormalUser = true; };
+        };
+
+        security.sudo = {
+          enable = true;
+          wheelNeedsPassword = false;
+
+          extraConfig = ''
+            Defaults lecture="never"
+          '';
+
+          extraRules = [
+            # SUDOERS SYNTAX CHECK (Test whether the module produces a valid output;
+            # errors being detected by the visudo checks.
+
+            # These should not create any entries
+            { users = [ "notest1" ]; commands = [ ]; }
+            { commands = [ { command = "ALL"; options = [ ]; } ]; }
+
+            # Test defining commands with the options syntax, though not setting any options
+            { users = [ "notest2" ]; commands = [ { command = "ALL"; options = [ ]; } ]; }
+
+
+            # CONFIGURATION FOR TEST CASES
+            { users = [ "test1" ]; groups = [ "foobar" ]; commands = [ "ALL" ]; }
+            { groups = [ "barfoo" 1337 ]; commands = [ { command = "ALL"; options = [ "NOPASSWD" "NOSETENV" ]; } ]; }
+            { users = [ "test5" ]; commands = [ { command = "ALL"; options = [ "NOPASSWD" "SETENV" ]; } ]; runAs = "test1:barfoo"; }
+          ];
+        };
+      };
+
+    nodes.strict = { ... }: {
+      users.users = {
+        admin = { isNormalUser = true; extraGroups = [ "wheel" ]; };
+        noadmin = { isNormalUser = true; };
+      };
+
+      security.sudo = {
+        enable = true;
+        wheelNeedsPassword = false;
+        execWheelOnly = true;
+      };
+    };
+
+    testScript =
+      ''
+        with subtest("users in wheel group should have passwordless sudo"):
+            machine.succeed('su - test0 -c "sudo -u root true"')
+
+        with subtest("test1 user should have sudo with password"):
+            machine.succeed('su - test1 -c "echo ${password} | sudo -S -u root true"')
+
+        with subtest("test1 user should not be able to use sudo without password"):
+            machine.fail('su - test1 -c "sudo -n -u root true"')
+
+        with subtest("users in group 'foobar' should be able to use sudo with password"):
+            machine.succeed('su - test2 -c "echo ${password} | sudo -S -u root true"')
+
+        with subtest("users in group 'barfoo' should be able to use sudo without password"):
+            machine.succeed("sudo -u test3 sudo -n -u root true")
+
+        with subtest("users in group 'baz' (GID 1337)"):
+            machine.succeed("sudo -u test4 sudo -n -u root echo true")
+
+        with subtest("test5 user should be able to run commands under test1"):
+            machine.succeed("sudo -u test5 sudo -n -u test1 true")
+
+        with subtest("test5 user should not be able to run commands under root"):
+            machine.fail("sudo -u test5 sudo -n -u root true")
+
+        with subtest("test5 user should be able to keep their environment"):
+            machine.succeed("sudo -u test5 sudo -n -E -u test1 true")
+
+        with subtest("users in group 'barfoo' should not be able to keep their environment"):
+            machine.fail("sudo -u test3 sudo -n -E -u root true")
+
+        with subtest("users in wheel should be able to run sudo despite execWheelOnly"):
+            strict.succeed('su - admin -c "sudo -u root true"')
+
+        with subtest("non-wheel users should be unable to run sudo thanks to execWheelOnly"):
+            strict.fail('su - noadmin -c "sudo --help"')
+      '';
+  })
diff --git a/nixos/tests/sway.nix b/nixos/tests/sway.nix
new file mode 100644
index 00000000000..1e9e146c4b6
--- /dev/null
+++ b/nixos/tests/sway.nix
@@ -0,0 +1,138 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "sway";
+  meta = {
+    maintainers = with lib.maintainers; [ primeos synthetica ];
+  };
+
+  machine = { config, ... }: {
+    # Automatically login on tty1 as a normal user:
+    imports = [ ./common/user-account.nix ];
+    services.getty.autologinUser = "alice";
+
+    environment = {
+      # For glinfo and wayland-info:
+      systemPackages = with pkgs; [ mesa-demos wayland-utils alacritty ];
+      # Use a fixed SWAYSOCK path (for swaymsg):
+      variables = {
+        "SWAYSOCK" = "/tmp/sway-ipc.sock";
+        # TODO: Investigate if we can get hardware acceleration to work (via
+        # virtio-gpu and Virgil). We currently have to use the Pixman software
+        # renderer since the GLES2 renderer doesn't work inside the VM (even
+        # with WLR_RENDERER_ALLOW_SOFTWARE):
+        # "WLR_RENDERER_ALLOW_SOFTWARE" = "1";
+        "WLR_RENDERER" = "pixman";
+      };
+      # For convenience:
+      shellAliases = {
+        test-x11 = "glinfo | tee /tmp/test-x11.out && touch /tmp/test-x11-exit-ok";
+        test-wayland = "wayland-info | tee /tmp/test-wayland.out && touch /tmp/test-wayland-exit-ok";
+      };
+
+      # To help with OCR:
+      etc."xdg/foot/foot.ini".text = lib.generators.toINI { } {
+        main = {
+          font = "inconsolata:size=14";
+        };
+        colors = rec {
+          foreground = "000000";
+          background = "ffffff";
+          regular2 = foreground;
+        };
+      };
+    };
+
+    fonts.fonts = [ pkgs.inconsolata ];
+
+    # Automatically configure and start Sway when logging in on tty1:
+    programs.bash.loginShellInit = ''
+      if [ "$(tty)" = "/dev/tty1" ]; then
+        set -e
+
+        mkdir -p ~/.config/sway
+        sed s/Mod4/Mod1/ /etc/sway/config > ~/.config/sway/config
+
+        sway --validate
+        sway && touch /tmp/sway-exit-ok
+      fi
+    '';
+
+    programs.sway.enable = true;
+
+    # To test pinentry via gpg-agent:
+    programs.gnupg.agent.enable = true;
+
+    # Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
+    virtualisation.qemu.options = [ "-vga none -device virtio-gpu-pci" ];
+  };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }: ''
+    import shlex
+
+    def swaymsg(command: str, succeed=True):
+        with machine.nested(f"sending swaymsg {command!r}" + " (allowed to fail)" * (not succeed)):
+          (machine.succeed if succeed else machine.execute)(
+            f"su - alice -c {shlex.quote('swaymsg -- ' + command)}"
+          )
+
+    start_all()
+    machine.wait_for_unit("multi-user.target")
+
+    # To check the version:
+    print(machine.succeed("sway --version"))
+
+    # Wait for Sway to complete startup:
+    machine.wait_for_file("/run/user/1000/wayland-1")
+    machine.wait_for_file("/tmp/sway-ipc.sock")
+
+    # Test XWayland (foot does not support X):
+    swaymsg("exec WINIT_UNIX_BACKEND=x11 WAYLAND_DISPLAY=invalid alacritty")
+    machine.wait_for_text("alice@machine")
+    machine.send_chars("test-x11\n")
+    machine.wait_for_file("/tmp/test-x11-exit-ok")
+    print(machine.succeed("cat /tmp/test-x11.out"))
+    machine.copy_from_vm("/tmp/test-x11.out")
+    machine.screenshot("alacritty_glinfo")
+    machine.succeed("pkill alacritty")
+
+    # Start a terminal (foot) on workspace 3:
+    machine.send_key("alt-3")
+    machine.sleep(3)
+    machine.send_key("alt-ret")
+    machine.wait_for_text("alice@machine")
+    machine.send_chars("test-wayland\n")
+    machine.wait_for_file("/tmp/test-wayland-exit-ok")
+    print(machine.succeed("cat /tmp/test-wayland.out"))
+    machine.copy_from_vm("/tmp/test-wayland.out")
+    machine.screenshot("foot_wayland_info")
+    machine.send_key("alt-shift-q")
+    machine.wait_until_fails("pgrep foot")
+
+    # Test gpg-agent starting pinentry-gnome3 via D-Bus (tests if
+    # $WAYLAND_DISPLAY is correctly imported into the D-Bus user env):
+    swaymsg("exec gpg --no-tty --yes --quick-generate-key test")
+    machine.wait_until_succeeds("pgrep --exact gpg")
+    machine.wait_for_text("Passphrase")
+    machine.screenshot("gpg_pinentry")
+    machine.send_key("alt-shift-q")
+    machine.wait_until_fails("pgrep --exact gpg")
+
+    # Test swaynag:
+    machine.send_key("alt-shift-e")
+    machine.wait_for_text("You pressed the exit shortcut.")
+    machine.screenshot("sway_exit")
+
+    swaymsg("exec swaylock")
+    machine.wait_until_succeeds("pgrep -x swaylock")
+    machine.sleep(3)
+    machine.send_chars("${nodes.machine.config.users.users.alice.password}")
+    machine.send_key("ret")
+    machine.wait_until_fails("pgrep -x swaylock")
+
+    # Exit Sway and verify process exit status 0:
+    swaymsg("exit", succeed=False)
+    machine.wait_until_fails("pgrep -x sway")
+    machine.wait_for_file("/tmp/sway-exit-ok")
+  '';
+})
diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix
new file mode 100644
index 00000000000..93eee4babc2
--- /dev/null
+++ b/nixos/tests/switch-test.nix
@@ -0,0 +1,1018 @@
+# Test configuration switching.
+
+import ./make-test-python.nix ({ pkgs, ...} : let
+
+  # Simple service that can either be socket-activated or that will
+  # listen on port 1234 if not socket-activated.
+  # A connection to the socket causes 'hello' to be written to the client.
+  socketTest = pkgs.writeScript "socket-test.py" /* python */ ''
+    #!${pkgs.python3}/bin/python3
+
+    from socketserver import TCPServer, StreamRequestHandler
+    import socket
+    import os
+
+
+    class Handler(StreamRequestHandler):
+        def handle(self):
+            self.wfile.write("hello".encode("utf-8"))
+
+
+    class Server(TCPServer):
+        def __init__(self, server_address, handler_cls):
+            listenFds = os.getenv('LISTEN_FDS')
+            if listenFds is None or int(listenFds) < 1:
+                print(f'Binding to {server_address}')
+                TCPServer.__init__(
+                        self, server_address, handler_cls, bind_and_activate=True)
+            else:
+                TCPServer.__init__(
+                        self, server_address, handler_cls, bind_and_activate=False)
+                # Override socket
+                print(f'Got activated by {os.getenv("LISTEN_FDNAMES")} '
+                      f'with {listenFds} FDs')
+                self.socket = socket.fromfd(3, self.address_family,
+                                            self.socket_type)
+
+
+    if __name__ == "__main__":
+        server = Server(("localhost", 1234), Handler)
+        server.serve_forever()
+  '';
+
+in {
+  name = "switch-test";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ gleber das_j ];
+  };
+
+  nodes = {
+    machine = { pkgs, lib, ... }: {
+      environment.systemPackages = [ pkgs.socat ]; # for the socket activation stuff
+      users.mutableUsers = false;
+
+      specialisation = rec {
+        simpleService.configuration = {
+          systemd.services.test = {
+            wantedBy = [ "multi-user.target" ];
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+              ExecStart = "${pkgs.coreutils}/bin/true";
+              ExecReload = "${pkgs.coreutils}/bin/true";
+            };
+          };
+        };
+
+        simpleServiceDifferentDescription.configuration = {
+          imports = [ simpleService.configuration ];
+          systemd.services.test.description = "Test unit";
+        };
+
+        simpleServiceModified.configuration = {
+          imports = [ simpleService.configuration ];
+          systemd.services.test.serviceConfig.X-Test = true;
+        };
+
+        simpleServiceNostop.configuration = {
+          imports = [ simpleService.configuration ];
+          systemd.services.test.stopIfChanged = false;
+        };
+
+        simpleServiceReload.configuration = {
+          imports = [ simpleService.configuration ];
+          systemd.services.test = {
+            reloadIfChanged = true;
+            serviceConfig.ExecReload = "${pkgs.coreutils}/bin/true";
+          };
+        };
+
+        simpleServiceNorestart.configuration = {
+          imports = [ simpleService.configuration ];
+          systemd.services.test.restartIfChanged = false;
+        };
+
+        simpleServiceFailing.configuration = {
+          imports = [ simpleServiceModified.configuration ];
+          systemd.services.test.serviceConfig.ExecStart = lib.mkForce "${pkgs.coreutils}/bin/false";
+        };
+
+        autorestartService.configuration = {
+          # A service that immediately goes into restarting (but without failing)
+          systemd.services.autorestart = {
+            wantedBy = [ "multi-user.target" ];
+            serviceConfig = {
+              Type = "simple";
+              Restart = "always";
+              RestartSec = "20y"; # Should be long enough
+              ExecStart = "${pkgs.coreutils}/bin/true";
+            };
+          };
+        };
+
+        autorestartServiceFailing.configuration = {
+          imports = [ autorestartService.configuration ];
+          systemd.services.autorestart.serviceConfig = {
+            ExecStart = lib.mkForce "${pkgs.coreutils}/bin/false";
+          };
+        };
+
+        simpleServiceWithExtraSection.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.packages = [ (pkgs.writeTextFile {
+            name = "systemd-extra-section";
+            destination = "/etc/systemd/system/test.service";
+            text = ''
+              [X-Test]
+              X-Test-Value=a
+            '';
+          }) ];
+        };
+
+        simpleServiceWithExtraSectionOtherName.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.packages = [ (pkgs.writeTextFile {
+            name = "systemd-extra-section";
+            destination = "/etc/systemd/system/test.service";
+            text = ''
+              [X-Test2]
+              X-Test-Value=a
+            '';
+          }) ];
+        };
+
+        simpleServiceWithInstallSection.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.packages = [ (pkgs.writeTextFile {
+            name = "systemd-extra-section";
+            destination = "/etc/systemd/system/test.service";
+            text = ''
+              [Install]
+              WantedBy=multi-user.target
+            '';
+          }) ];
+        };
+
+        simpleServiceWithExtraKey.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.serviceConfig."X-Test" = "test";
+        };
+
+        simpleServiceWithExtraKeyOtherValue.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.serviceConfig."X-Test" = "test2";
+        };
+
+        simpleServiceWithExtraKeyOtherName.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.serviceConfig."X-Test2" = "test";
+        };
+
+        simpleServiceReloadTrigger.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.reloadTriggers = [ "/dev/null" ];
+        };
+
+        simpleServiceReloadTriggerModified.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.reloadTriggers = [ "/dev/zero" ];
+        };
+
+        simpleServiceReloadTriggerModifiedAndSomethingElse.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test = {
+            reloadTriggers = [ "/dev/zero" ];
+            serviceConfig."X-Test" = "test";
+          };
+        };
+
+        simpleServiceReloadTriggerModifiedSomethingElse.configuration = {
+          imports = [ simpleServiceNostop.configuration ];
+          systemd.services.test.serviceConfig."X-Test" = "test";
+        };
+
+        unitWithBackslash.configuration = {
+          systemd.services."escaped\\x2ddash" = {
+            wantedBy = [ "multi-user.target" ];
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+              ExecStart = "${pkgs.coreutils}/bin/true";
+              ExecReload = "${pkgs.coreutils}/bin/true";
+            };
+          };
+        };
+
+        unitWithBackslashModified.configuration = {
+          imports = [ unitWithBackslash.configuration ];
+          systemd.services."escaped\\x2ddash".serviceConfig.X-Test = "test";
+        };
+
+        unitWithRequirement.configuration = {
+          systemd.services.required-service = {
+            wantedBy = [ "multi-user.target" ];
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+              ExecStart = "${pkgs.coreutils}/bin/true";
+              ExecReload = "${pkgs.coreutils}/bin/true";
+            };
+          };
+          systemd.services.test-service = {
+            wantedBy = [ "multi-user.target" ];
+            requires = [ "required-service.service" ];
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+              ExecStart = "${pkgs.coreutils}/bin/true";
+              ExecReload = "${pkgs.coreutils}/bin/true";
+            };
+          };
+        };
+
+        unitWithRequirementModified.configuration = {
+          imports = [ unitWithRequirement.configuration ];
+          systemd.services.required-service.serviceConfig.X-Test = "test";
+          systemd.services.test-service.reloadTriggers = [ "test" ];
+        };
+
+        unitWithRequirementModifiedNostart.configuration = {
+          imports = [ unitWithRequirement.configuration ];
+          systemd.services.test-service.unitConfig.RefuseManualStart = true;
+        };
+
+        restart-and-reload-by-activation-script.configuration = {
+          systemd.services = rec {
+            simple-service = {
+              # No wantedBy so we can check if the activation script restart triggers them
+              serviceConfig = {
+                Type = "oneshot";
+                RemainAfterExit = true;
+                ExecStart = "${pkgs.coreutils}/bin/true";
+                ExecReload = "${pkgs.coreutils}/bin/true";
+              };
+            };
+
+            simple-restart-service = simple-service // {
+              stopIfChanged = false;
+            };
+
+            simple-reload-service = simple-service // {
+              reloadIfChanged = true;
+            };
+
+            no-restart-service = simple-service // {
+              restartIfChanged = false;
+            };
+
+            reload-triggers = simple-service // {
+              wantedBy = [ "multi-user.target" ];
+            };
+
+            reload-triggers-and-restart-by-as = simple-service;
+
+            reload-triggers-and-restart = simple-service // {
+              stopIfChanged = false; # easier to check for this
+              wantedBy = [ "multi-user.target" ];
+            };
+          };
+
+          system.activationScripts.restart-and-reload-test = {
+            supportsDryActivation = true;
+            deps = [];
+            text = ''
+              if [ "$NIXOS_ACTION" = dry-activate ]; then
+                f=/run/nixos/dry-activation-restart-list
+                g=/run/nixos/dry-activation-reload-list
+              else
+                f=/run/nixos/activation-restart-list
+                g=/run/nixos/activation-reload-list
+              fi
+              cat <<EOF >> "$f"
+              simple-service.service
+              simple-restart-service.service
+              simple-reload-service.service
+              no-restart-service.service
+              reload-triggers-and-restart-by-as.service
+              EOF
+
+              cat <<EOF >> "$g"
+              reload-triggers.service
+              reload-triggers-and-restart-by-as.service
+              reload-triggers-and-restart.service
+              EOF
+            '';
+          };
+        };
+
+        restart-and-reload-by-activation-script-modified.configuration = {
+          imports = [ restart-and-reload-by-activation-script.configuration ];
+          systemd.services.reload-triggers-and-restart.serviceConfig.X-Modified = "test";
+        };
+
+        simple-socket.configuration = {
+          systemd.services.socket-activated = {
+            description = "A socket-activated service";
+            stopIfChanged = lib.mkDefault false;
+            serviceConfig = {
+              ExecStart = socketTest;
+              ExecReload = "${pkgs.coreutils}/bin/true";
+            };
+          };
+          systemd.sockets.socket-activated = {
+            wantedBy = [ "sockets.target" ];
+            listenStreams = [ "/run/test.sock" ];
+            socketConfig.SocketMode = lib.mkDefault "0777";
+          };
+        };
+
+        simple-socket-service-modified.configuration = {
+          imports = [ simple-socket.configuration ];
+          systemd.services.socket-activated.serviceConfig.X-Test = "test";
+        };
+
+        simple-socket-stop-if-changed.configuration = {
+          imports = [ simple-socket.configuration ];
+          systemd.services.socket-activated.stopIfChanged = true;
+        };
+
+        simple-socket-stop-if-changed-and-reloadtrigger.configuration = {
+          imports = [ simple-socket.configuration ];
+          systemd.services.socket-activated = {
+            stopIfChanged = true;
+            reloadTriggers = [ "test" ];
+          };
+        };
+
+        mount.configuration = {
+          systemd.mounts = [
+            {
+              description = "Testmount";
+              what = "tmpfs";
+              type = "tmpfs";
+              where = "/testmount";
+              options = "size=1M";
+              wantedBy = [ "local-fs.target" ];
+            }
+          ];
+        };
+
+        mountModified.configuration = {
+          systemd.mounts = [
+            {
+              description = "Testmount";
+              what = "tmpfs";
+              type = "tmpfs";
+              where = "/testmount";
+              options = "size=10M";
+              wantedBy = [ "local-fs.target" ];
+            }
+          ];
+        };
+
+        timer.configuration = {
+          systemd.timers.test-timer = {
+            wantedBy = [ "timers.target" ];
+            timerConfig.OnCalendar = "@1395716396"; # chosen by fair dice roll
+          };
+          systemd.services.test-timer = {
+            serviceConfig = {
+              Type = "oneshot";
+              ExecStart = "${pkgs.coreutils}/bin/true";
+            };
+          };
+        };
+
+        timerModified.configuration = {
+          imports = [ timer.configuration ];
+          systemd.timers.test-timer.timerConfig.OnCalendar = lib.mkForce "Fri 2012-11-23 16:00:00";
+        };
+
+        hybridSleepModified.configuration = {
+          systemd.targets.hybrid-sleep.unitConfig.X-Test = true;
+        };
+
+        target.configuration = {
+          systemd.targets.test-target.wantedBy = [ "multi-user.target" ];
+          # We use this service to figure out whether the target was modified.
+          # This is the only way because targets are filtered and therefore not
+          # printed when they are started/stopped.
+          systemd.services.test-service = {
+            bindsTo = [ "test-target.target" ];
+            serviceConfig.ExecStart = "${pkgs.coreutils}/bin/sleep infinity";
+          };
+        };
+
+        targetModified.configuration = {
+          imports = [ target.configuration ];
+          systemd.targets.test-target.unitConfig.X-Test = true;
+        };
+
+        targetModifiedStopOnReconfig.configuration = {
+          imports = [ target.configuration ];
+          systemd.targets.test-target.unitConfig.X-StopOnReconfiguration = true;
+        };
+
+        path.configuration = {
+          systemd.paths.test-watch = {
+            wantedBy = [ "paths.target" ];
+            pathConfig.PathExists = "/testpath";
+          };
+          systemd.services.test-watch = {
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+              ExecStart = "${pkgs.coreutils}/bin/touch /testpath-modified";
+            };
+          };
+        };
+
+        pathModified.configuration = {
+          imports = [ path.configuration ];
+          systemd.paths.test-watch.pathConfig.PathExists = lib.mkForce "/testpath2";
+        };
+
+        slice.configuration = {
+          systemd.slices.testslice.sliceConfig.MemoryMax = "1"; # don't allow memory allocation
+          systemd.services.testservice = {
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+              ExecStart = "${pkgs.coreutils}/bin/true";
+              Slice = "testslice.slice";
+            };
+          };
+        };
+
+        sliceModified.configuration = {
+          imports = [ slice.configuration ];
+          systemd.slices.testslice.sliceConfig.MemoryMax = lib.mkForce null;
+        };
+      };
+    };
+
+    other = {
+      users.mutableUsers = true;
+    };
+  };
+
+  testScript = { nodes, ... }: let
+    originalSystem = nodes.machine.config.system.build.toplevel;
+    otherSystem = nodes.other.config.system.build.toplevel;
+    machine = nodes.machine.config.system.build.toplevel;
+
+    # Ensures failures pass through using pipefail, otherwise failing to
+    # switch-to-configuration is hidden by the success of `tee`.
+    stderrRunner = pkgs.writeScript "stderr-runner" ''
+      #! ${pkgs.runtimeShell}
+      set -e
+      set -o pipefail
+      exec env -i "$@" | tee /dev/stderr
+    '';
+  in /* python */ ''
+    def switch_to_specialisation(system, name, action="test", fail=False):
+        if name == "":
+            stc = f"{system}/bin/switch-to-configuration"
+        else:
+            stc = f"{system}/specialisation/{name}/bin/switch-to-configuration"
+        out = machine.fail(f"{stc} {action} 2>&1") if fail \
+            else machine.succeed(f"{stc} {action} 2>&1")
+        assert_lacks(out, "switch-to-configuration line")  # Perl warnings
+        return out
+
+    def assert_contains(haystack, needle):
+        if needle not in haystack:
+            print("The haystack that will cause the following exception is:")
+            print("---")
+            print(haystack)
+            print("---")
+            raise Exception(f"Expected string '{needle}' was not found")
+
+    def assert_lacks(haystack, needle):
+        if needle in haystack:
+            print("The haystack that will cause the following exception is:")
+            print("---")
+            print(haystack, end="")
+            print("---")
+            raise Exception(f"Unexpected string '{needle}' was found")
+
+
+    machine.wait_for_unit("multi-user.target")
+
+    machine.succeed(
+        "${stderrRunner} ${originalSystem}/bin/switch-to-configuration test"
+    )
+    machine.succeed(
+        "${stderrRunner} ${otherSystem}/bin/switch-to-configuration test"
+    )
+
+    with subtest("services"):
+        switch_to_specialisation("${machine}", "")
+        # Nothing happens when nothing is changed
+        out = switch_to_specialisation("${machine}", "")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Start a simple service
+        out = switch_to_specialisation("${machine}", "simpleService")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: dbus.service\n")  # huh
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: test.service\n")
+
+        # Not changing anything doesn't do anything
+        out = switch_to_specialisation("${machine}", "simpleService")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Only changing the description does nothing
+        out = switch_to_specialisation("${machine}", "simpleServiceDifferentDescription")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Restart the simple service
+        out = switch_to_specialisation("${machine}", "simpleServiceModified")
+        assert_contains(out, "stopping the following units: test.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_contains(out, "\nstarting the following units: test.service\n")
+        assert_lacks(out, "the following new units were started:")
+
+        # Restart the service with stopIfChanged=false
+        out = switch_to_specialisation("${machine}", "simpleServiceNostop")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Reload the service with reloadIfChanged=true
+        out = switch_to_specialisation("${machine}", "simpleServiceReload")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: test.service\n")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Nothing happens when restartIfChanged=false
+        out = switch_to_specialisation("${machine}", "simpleServiceNorestart")
+        assert_lacks(out, "stopping the following units:")
+        assert_contains(out, "NOT restarting the following changed units: test.service\n")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Dry mode shows different messages
+        out = switch_to_specialisation("${machine}", "simpleService", action="dry-activate")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_contains(out, "would start the following units: test.service\n")
+
+        # Ensure \ works in unit names
+        out = switch_to_specialisation("${machine}", "unitWithBackslash")
+        assert_contains(out, "stopping the following units: test.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: escaped\\x2ddash.service\n")
+
+        out = switch_to_specialisation("${machine}", "unitWithBackslashModified")
+        assert_contains(out, "stopping the following units: escaped\\x2ddash.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_contains(out, "\nstarting the following units: escaped\\x2ddash.service\n")
+        assert_lacks(out, "the following new units were started:")
+
+        # Ensure units that require changed units are properly reloaded
+        out = switch_to_specialisation("${machine}", "unitWithRequirement")
+        assert_contains(out, "stopping the following units: escaped\\x2ddash.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: required-service.service, test-service.service\n")
+
+        out = switch_to_specialisation("${machine}", "unitWithRequirementModified")
+        assert_contains(out, "stopping the following units: required-service.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_contains(out, "\nstarting the following units: required-service.service, test-service.service\n")
+        assert_lacks(out, "the following new units were started:")
+
+        # Unless the unit asks to be not restarted
+        out = switch_to_specialisation("${machine}", "unitWithRequirementModifiedNostart")
+        assert_contains(out, "stopping the following units: required-service.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_contains(out, "\nstarting the following units: required-service.service\n")
+        assert_lacks(out, "the following new units were started:")
+
+    with subtest("failing units"):
+        # Let the simple service fail
+        switch_to_specialisation("${machine}", "simpleServiceModified")
+        out = switch_to_specialisation("${machine}", "simpleServiceFailing", fail=True)
+        assert_contains(out, "stopping the following units: test.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_contains(out, "\nstarting the following units: test.service\n")
+        assert_lacks(out, "the following new units were started:")
+        assert_contains(out, "warning: the following units failed: test.service\n")
+        assert_contains(out, "Main PID:")  # output of systemctl
+
+        # A unit that gets into autorestart without failing is not treated as failed
+        out = switch_to_specialisation("${machine}", "autorestartService")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: autorestart.service\n")
+        machine.systemctl('stop autorestart.service')  # cancel the 20y timer
+
+        # Switching to the same system should do nothing (especially not treat the unit as failed)
+        out = switch_to_specialisation("${machine}", "autorestartService")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: autorestart.service\n")
+        machine.systemctl('stop autorestart.service')  # cancel the 20y timer
+
+        # If systemd thinks the unit has failed and is in autorestart, we should show it as failed
+        out = switch_to_specialisation("${machine}", "autorestartServiceFailing", fail=True)
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        assert_contains(out, "warning: the following units failed: autorestart.service\n")
+        assert_contains(out, "Main PID:")  # output of systemctl
+
+    with subtest("unit file parser"):
+        # Switch to a well-known state
+        switch_to_specialisation("${machine}", "simpleServiceNostop")
+
+        # Add a section
+        out = switch_to_specialisation("${machine}", "simpleServiceWithExtraSection")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Rename it
+        out = switch_to_specialisation("${machine}", "simpleServiceWithExtraSectionOtherName")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Remove it
+        out = switch_to_specialisation("${machine}", "simpleServiceNostop")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # [Install] section is ignored
+        out = switch_to_specialisation("${machine}", "simpleServiceWithInstallSection")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Add a key
+        out = switch_to_specialisation("${machine}", "simpleServiceWithExtraKey")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Change its value
+        out = switch_to_specialisation("${machine}", "simpleServiceWithExtraKeyOtherValue")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Rename it
+        out = switch_to_specialisation("${machine}", "simpleServiceWithExtraKeyOtherName")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Remove it
+        out = switch_to_specialisation("${machine}", "simpleServiceNostop")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Add a reload trigger
+        out = switch_to_specialisation("${machine}", "simpleServiceReloadTrigger")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: test.service\n")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Modify the reload trigger
+        out = switch_to_specialisation("${machine}", "simpleServiceReloadTriggerModified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: test.service\n")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Modify the reload trigger and something else
+        out = switch_to_specialisation("${machine}", "simpleServiceReloadTriggerModifiedAndSomethingElse")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Remove the reload trigger
+        out = switch_to_specialisation("${machine}", "simpleServiceReloadTriggerModifiedSomethingElse")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+    with subtest("restart and reload by activation script"):
+        switch_to_specialisation("${machine}", "simpleServiceNorestart")
+        out = switch_to_specialisation("${machine}", "restart-and-reload-by-activation-script")
+        assert_contains(out, "stopping the following units: test.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "restarting the following units:")
+        assert_contains(out, "\nstarting the following units: no-restart-service.service, reload-triggers-and-restart-by-as.service, simple-reload-service.service, simple-restart-service.service, simple-service.service\n")
+        assert_contains(out, "the following new units were started: no-restart-service.service, reload-triggers-and-restart-by-as.service, reload-triggers-and-restart.service, reload-triggers.service, simple-reload-service.service, simple-restart-service.service, simple-service.service\n")
+        # Switch to the same system where the example services get restarted
+        # and reloaded by the activation script
+        out = switch_to_specialisation("${machine}", "restart-and-reload-by-activation-script")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: reload-triggers-and-restart.service, reload-triggers.service, simple-reload-service.service\n")
+        assert_contains(out, "restarting the following units: reload-triggers-and-restart-by-as.service, simple-restart-service.service, simple-service.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        # Switch to the same system and see if the service gets restarted when it's modified
+        # while the fact that it's supposed to be reloaded by the activation script is ignored.
+        out = switch_to_specialisation("${machine}", "restart-and-reload-by-activation-script-modified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: reload-triggers.service, simple-reload-service.service\n")
+        assert_contains(out, "restarting the following units: reload-triggers-and-restart-by-as.service, reload-triggers-and-restart.service, simple-restart-service.service, simple-service.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        # The same, but in dry mode
+        out = switch_to_specialisation("${machine}", "restart-and-reload-by-activation-script", action="dry-activate")
+        assert_lacks(out, "would stop the following units:")
+        assert_lacks(out, "would NOT stop the following changed units:")
+        assert_contains(out, "would reload the following units: reload-triggers.service, simple-reload-service.service\n")
+        assert_contains(out, "would restart the following units: reload-triggers-and-restart-by-as.service, reload-triggers-and-restart.service, simple-restart-service.service, simple-service.service\n")
+        assert_lacks(out, "\nwould start the following units:")
+
+    with subtest("socket-activated services"):
+        # Socket-activated services don't get started, just the socket
+        machine.fail("[ -S /run/test.sock ]")
+        out = switch_to_specialisation("${machine}", "simple-socket")
+        # assert_lacks(out, "stopping the following units:") not relevant
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: socket-activated.socket\n")
+        machine.succeed("[ -S /run/test.sock ]")
+
+        # Changing a non-activated service does nothing
+        out = switch_to_specialisation("${machine}", "simple-socket-service-modified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        machine.succeed("[ -S /run/test.sock ]")
+        # The unit is properly activated when the socket is accessed
+        if machine.succeed("socat - UNIX-CONNECT:/run/test.sock") != "hello":
+            raise Exception("Socket was not properly activated")  # idk how that would happen tbh
+
+        # Changing an activated service with stopIfChanged=false restarts the service
+        out = switch_to_specialisation("${machine}", "simple-socket")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: socket-activated.service\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        machine.succeed("[ -S /run/test.sock ]")
+        # Socket-activation of the unit still works
+        if machine.succeed("socat - UNIX-CONNECT:/run/test.sock") != "hello":
+            raise Exception("Socket was not properly activated after the service was restarted")
+
+        # Changing an activated service with stopIfChanged=true stops the service and
+        # socket and starts the socket
+        out = switch_to_specialisation("${machine}", "simple-socket-stop-if-changed")
+        assert_contains(out, "stopping the following units: socket-activated.service, socket-activated.socket\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_contains(out, "\nstarting the following units: socket-activated.socket\n")
+        assert_lacks(out, "the following new units were started:")
+        machine.succeed("[ -S /run/test.sock ]")
+        # Socket-activation of the unit still works
+        if machine.succeed("socat - UNIX-CONNECT:/run/test.sock") != "hello":
+            raise Exception("Socket was not properly activated after the service was restarted")
+
+        # Changing a reload trigger of a socket-activated unit only reloads it
+        out = switch_to_specialisation("${machine}", "simple-socket-stop-if-changed-and-reloadtrigger")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: socket-activated.service\n")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units: socket-activated.socket")
+        assert_lacks(out, "the following new units were started:")
+        machine.succeed("[ -S /run/test.sock ]")
+        # Socket-activation of the unit still works
+        if machine.succeed("socat - UNIX-CONNECT:/run/test.sock") != "hello":
+            raise Exception("Socket was not properly activated after the service was restarted")
+
+    with subtest("mounts"):
+        switch_to_specialisation("${machine}", "mount")
+        out = machine.succeed("mount | grep 'on /testmount'")
+        assert_contains(out, "size=1024k")
+        out = switch_to_specialisation("${machine}", "mountModified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: testmount.mount\n")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        # It changed
+        out = machine.succeed("mount | grep 'on /testmount'")
+        assert_contains(out, "size=10240k")
+
+    with subtest("timers"):
+        switch_to_specialisation("${machine}", "timer")
+        out = machine.succeed("systemctl show test-timer.timer")
+        assert_contains(out, "OnCalendar=2014-03-25 02:59:56 UTC")
+        out = switch_to_specialisation("${machine}", "timerModified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: test-timer.timer\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        # It changed
+        out = machine.succeed("systemctl show test-timer.timer")
+        assert_contains(out, "OnCalendar=Fri 2012-11-23 16:00:00")
+
+    with subtest("targets"):
+        # Modifying some special targets like hybrid-sleep.target does nothing
+        out = switch_to_specialisation("${machine}", "hybridSleepModified")
+        assert_contains(out, "stopping the following units: test-timer.timer\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+
+        # Adding a new target starts it
+        out = switch_to_specialisation("${machine}", "target")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: test-target.target\n")
+
+        # Changing a target doesn't print anything because the unit is filtered
+        machine.systemctl("start test-service.service")
+        out = switch_to_specialisation("${machine}", "targetModified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        machine.succeed("systemctl is-active test-service.service")  # target was not restarted
+
+        # With X-StopOnReconfiguration, the target gets stopped and started
+        out = switch_to_specialisation("${machine}", "targetModifiedStopOnReconfig")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        machine.fail("systemctl is-active test-service.servce")  # target was restarted
+
+        # Remove the target by switching to the old specialisation
+        out = switch_to_specialisation("${machine}", "timerModified")
+        assert_contains(out, "stopping the following units: test-target.target\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: test-timer.timer\n")
+
+    with subtest("paths"):
+        out = switch_to_specialisation("${machine}", "path")
+        assert_contains(out, "stopping the following units: test-timer.timer\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: test-watch.path\n")
+        machine.fail("test -f /testpath-modified")
+
+        # touch the file, unit should be triggered
+        machine.succeed("touch /testpath")
+        machine.wait_until_succeeds("test -f /testpath-modified")
+        machine.succeed("rm /testpath /testpath-modified")
+        machine.systemctl("stop test-watch.service")
+        switch_to_specialisation("${machine}", "pathModified")
+        machine.succeed("touch /testpath")
+        machine.fail("test -f /testpath-modified")
+        machine.succeed("touch /testpath2")
+        machine.wait_until_succeeds("test -f /testpath-modified")
+
+    # This test ensures that changes to slice configuration get applied.
+    # We test this by having a slice that allows no memory allocation at
+    # all and starting a service within it. If the service crashes, the slice
+    # is applied and if we modify the slice to allow memory allocation, the
+    # service should successfully start.
+    with subtest("slices"):
+        machine.succeed("echo 0 > /proc/sys/vm/panic_on_oom")  # allow OOMing
+        out = switch_to_specialisation("${machine}", "slice")
+        # assert_lacks(out, "stopping the following units:") not relevant
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        machine.fail("systemctl start testservice.service")
+
+        out = switch_to_specialisation("${machine}", "sliceModified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        machine.succeed("systemctl start testservice.service")
+        machine.succeed("echo 1 > /proc/sys/vm/panic_on_oom")  # disallow OOMing
+  '';
+})
diff --git a/nixos/tests/sympa.nix b/nixos/tests/sympa.nix
new file mode 100644
index 00000000000..aad7c95b6c9
--- /dev/null
+++ b/nixos/tests/sympa.nix
@@ -0,0 +1,35 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "sympa";
+  meta.maintainers = with lib.maintainers; [ mmilata ];
+
+  machine =
+    { ... }:
+    {
+
+      services.sympa = {
+        enable = true;
+        domains = {
+          "lists.example.org" = {
+            webHost = "localhost";
+          };
+        };
+        listMasters = [ "joe@example.org" ];
+        web.enable = true;
+        web.https = false;
+        database = {
+          type = "PostgreSQL";
+          createLocally = true;
+        };
+      };
+    };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("sympa.service")
+    machine.wait_for_unit("wwsympa.service")
+    assert "Mailing lists service" in machine.succeed(
+        "curl --fail --insecure -L http://localhost/"
+    )
+  '';
+})
diff --git a/nixos/tests/syncthing-init.nix b/nixos/tests/syncthing-init.nix
new file mode 100644
index 00000000000..8b60ad7faf0
--- /dev/null
+++ b/nixos/tests/syncthing-init.nix
@@ -0,0 +1,31 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: let
+
+  testId = "7CFNTQM-IMTJBHJ-3UWRDIU-ZGQJFR6-VCXZ3NB-XUH3KZO-N52ITXR-LAIYUAU";
+
+in {
+  name = "syncthing-init";
+  meta.maintainers = with pkgs.lib.maintainers; [ lassulus ];
+
+  machine = {
+    services.syncthing = {
+      enable = true;
+      devices.testDevice = {
+        id = testId;
+      };
+      folders.testFolder = {
+        path = "/tmp/test";
+        devices = [ "testDevice" ];
+      };
+      extraOptions.gui.user = "guiUser";
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("syncthing-init.service")
+    config = machine.succeed("cat /var/lib/syncthing/.config/syncthing/config.xml")
+
+    assert "testFolder" in config
+    assert "${testId}" in config
+    assert "guiUser" in config
+  '';
+})
diff --git a/nixos/tests/syncthing-relay.nix b/nixos/tests/syncthing-relay.nix
new file mode 100644
index 00000000000..a0233c969ec
--- /dev/null
+++ b/nixos/tests/syncthing-relay.nix
@@ -0,0 +1,26 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "syncthing-relay";
+  meta.maintainers = with pkgs.lib.maintainers; [ delroth ];
+
+  machine = {
+    environment.systemPackages = [ pkgs.jq ];
+    services.syncthing.relay = {
+      enable = true;
+      providedBy = "nixos-test";
+      pools = [];  # Don't connect to any pool while testing.
+      port = 12345;
+      statusPort = 12346;
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("syncthing-relay.service")
+    machine.wait_for_open_port(12345)
+    machine.wait_for_open_port(12346)
+
+    out = machine.succeed(
+        "curl -sSf http://localhost:12346/status | jq -r '.options.\"provided-by\"'"
+    )
+    assert "nixos-test" in out
+  '';
+})
diff --git a/nixos/tests/syncthing.nix b/nixos/tests/syncthing.nix
new file mode 100644
index 00000000000..aff1d874413
--- /dev/null
+++ b/nixos/tests/syncthing.nix
@@ -0,0 +1,65 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "syncthing";
+  meta.maintainers = with pkgs.lib.maintainers; [ chkno ];
+
+  nodes = rec {
+    a = {
+      environment.systemPackages = with pkgs; [ curl libxml2 syncthing ];
+      services.syncthing = {
+        enable = true;
+        openDefaultPorts = true;
+      };
+    };
+    b = a;
+  };
+
+  testScript = ''
+    import json
+    import shlex
+
+    confdir = "/var/lib/syncthing/.config/syncthing"
+
+
+    def addPeer(host, name, deviceID):
+        APIKey = host.succeed(
+            "xmllint --xpath 'string(configuration/gui/apikey)' %s/config.xml" % confdir
+        ).strip()
+        oldConf = host.succeed(
+            "curl -Ssf -H 'X-API-Key: %s' 127.0.0.1:8384/rest/config" % APIKey
+        )
+        conf = json.loads(oldConf)
+        conf["devices"].append({"deviceID": deviceID, "id": name})
+        conf["folders"].append(
+            {
+                "devices": [{"deviceID": deviceID}],
+                "id": "foo",
+                "path": "/var/lib/syncthing/foo",
+                "rescanIntervalS": 1,
+            }
+        )
+        newConf = json.dumps(conf)
+        host.succeed(
+            "curl -Ssf -H 'X-API-Key: %s' 127.0.0.1:8384/rest/config -X PUT -d %s"
+            % (APIKey, shlex.quote(newConf))
+        )
+
+
+    start_all()
+    a.wait_for_unit("syncthing.service")
+    b.wait_for_unit("syncthing.service")
+    a.wait_for_open_port(22000)
+    b.wait_for_open_port(22000)
+
+    aDeviceID = a.succeed("syncthing -home=%s -device-id" % confdir).strip()
+    bDeviceID = b.succeed("syncthing -home=%s -device-id" % confdir).strip()
+    addPeer(a, "b", bDeviceID)
+    addPeer(b, "a", aDeviceID)
+
+    a.wait_for_file("/var/lib/syncthing/foo")
+    b.wait_for_file("/var/lib/syncthing/foo")
+    a.succeed("echo a2b > /var/lib/syncthing/foo/a2b")
+    b.succeed("echo b2a > /var/lib/syncthing/foo/b2a")
+    a.wait_for_file("/var/lib/syncthing/foo/b2a")
+    b.wait_for_file("/var/lib/syncthing/foo/a2b")
+  '';
+})
diff --git a/nixos/tests/systemd-analyze.nix b/nixos/tests/systemd-analyze.nix
new file mode 100644
index 00000000000..186f5aee7b8
--- /dev/null
+++ b/nixos/tests/systemd-analyze.nix
@@ -0,0 +1,46 @@
+import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }:
+
+{
+  name = "systemd-analyze";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ raskin ];
+  };
+
+  machine =
+    { pkgs, lib, ... }:
+    { boot.kernelPackages = lib.mkIf latestKernel pkgs.linuxPackages_latest;
+      sound.enable = true; # needed for the factl test, /dev/snd/* exists without them but udev doesn't care then
+    };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+
+    # We create a special output directory to copy it as a whole
+    with subtest("Prepare output dir"):
+        machine.succeed("mkdir systemd-analyze")
+
+
+    # Save the output into a file with given name inside the common
+    # output directory
+    def run_systemd_analyze(args, name):
+        tgt_dir = "systemd-analyze"
+        machine.succeed(
+            "systemd-analyze {} > {}/{} 2> {}/{}.err".format(
+                " ".join(args), tgt_dir, name, tgt_dir, name
+            )
+        )
+
+
+    with subtest("Print statistics"):
+        run_systemd_analyze(["blame"], "blame.txt")
+        run_systemd_analyze(["critical-chain"], "critical-chain.txt")
+        run_systemd_analyze(["dot"], "dependencies.dot")
+        run_systemd_analyze(["plot"], "systemd-analyze.svg")
+
+    # We copy the main graph into the $out (toplevel), and we also copy
+    # the entire output directory with additional data
+    with subtest("Copying the resulting data into $out"):
+        machine.copy_from_vm("systemd-analyze/", "")
+        machine.copy_from_vm("systemd-analyze/systemd-analyze.svg", "")
+  '';
+})
diff --git a/nixos/tests/systemd-binfmt.nix b/nixos/tests/systemd-binfmt.nix
new file mode 100644
index 00000000000..a3a6efac3e4
--- /dev/null
+++ b/nixos/tests/systemd-binfmt.nix
@@ -0,0 +1,90 @@
+# Teach the kernel how to run armv7l and aarch64-linux binaries,
+# and run GNU Hello for these architectures.
+
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+
+let
+  expectArgv0 = xpkgs: xpkgs.runCommandCC "expect-argv0" {
+    src = pkgs.writeText "expect-argv0.c" ''
+      #include <stdio.h>
+      #include <string.h>
+
+      int main(int argc, char **argv) {
+        fprintf(stderr, "Our argv[0] is %s\n", argv[0]);
+
+        if (strcmp(argv[0], argv[1])) {
+          fprintf(stderr, "ERROR: argv[0] is %s, should be %s\n", argv[0], argv[1]);
+          return 1;
+        }
+
+        return 0;
+      }
+    '';
+  } ''
+    $CC -o $out $src
+  '';
+in {
+  basic = makeTest {
+    name = "systemd-binfmt";
+    machine = {
+      boot.binfmt.emulatedSystems = [
+        "armv7l-linux"
+        "aarch64-linux"
+      ];
+    };
+
+    testScript = let
+      helloArmv7l = pkgs.pkgsCross.armv7l-hf-multiplatform.hello;
+      helloAarch64 = pkgs.pkgsCross.aarch64-multiplatform.hello;
+    in ''
+      machine.start()
+
+      assert "world" in machine.succeed(
+          "${helloArmv7l}/bin/hello"
+      )
+
+      assert "world" in machine.succeed(
+          "${helloAarch64}/bin/hello"
+      )
+    '';
+  };
+
+  preserveArgvZero = makeTest {
+    name = "systemd-binfmt-preserve-argv0";
+    machine = {
+      boot.binfmt.emulatedSystems = [
+        "aarch64-linux"
+      ];
+    };
+    testScript = let
+      testAarch64 = expectArgv0 pkgs.pkgsCross.aarch64-multiplatform;
+    in ''
+      machine.start()
+      machine.succeed("exec -a meow ${testAarch64} meow")
+    '';
+  };
+
+  ldPreload = makeTest {
+    name = "systemd-binfmt-ld-preload";
+    machine = {
+      boot.binfmt.emulatedSystems = [
+        "aarch64-linux"
+      ];
+    };
+    testScript = let
+      helloAarch64 = pkgs.pkgsCross.aarch64-multiplatform.hello;
+      libredirectAarch64 = pkgs.pkgsCross.aarch64-multiplatform.libredirect;
+    in ''
+      machine.start()
+
+      assert "error" not in machine.succeed(
+          "LD_PRELOAD='${libredirectAarch64}/lib/libredirect.so' ${helloAarch64}/bin/hello 2>&1"
+      ).lower()
+    '';
+  };
+}
diff --git a/nixos/tests/systemd-boot.nix b/nixos/tests/systemd-boot.nix
new file mode 100644
index 00000000000..51cfd82e6c4
--- /dev/null
+++ b/nixos/tests/systemd-boot.nix
@@ -0,0 +1,254 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  common = {
+    virtualisation.useBootLoader = true;
+    virtualisation.useEFIBoot = true;
+    boot.loader.systemd-boot.enable = true;
+    boot.loader.efi.canTouchEfiVariables = true;
+    environment.systemPackages = [ pkgs.efibootmgr ];
+  };
+in
+{
+  basic = makeTest {
+    name = "systemd-boot";
+    meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer ];
+
+    machine = common;
+
+    testScript = ''
+      machine.start()
+      machine.wait_for_unit("multi-user.target")
+
+      machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
+
+      # Ensure we actually booted using systemd-boot
+      # Magic number is the vendor UUID used by systemd-boot.
+      machine.succeed(
+          "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
+      )
+
+      # "bootctl install" should have created an EFI entry
+      machine.succeed('efibootmgr | grep "Linux Boot Manager"')
+    '';
+  };
+
+  # Check that specialisations create corresponding boot entries.
+  specialisation = makeTest {
+    name = "systemd-boot-specialisation";
+    meta.maintainers = with pkgs.lib.maintainers; [ lukegb ];
+
+    machine = { pkgs, lib, ... }: {
+      imports = [ common ];
+      specialisation.something.configuration = {};
+    };
+
+    testScript = ''
+      machine.start()
+      machine.wait_for_unit("multi-user.target")
+
+      machine.succeed(
+          "test -e /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
+      )
+      machine.succeed(
+          "grep -q 'title NixOS (something)' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
+      )
+    '';
+  };
+
+  # Boot without having created an EFI entry--instead using default "/EFI/BOOT/BOOTX64.EFI"
+  fallback = makeTest {
+    name = "systemd-boot-fallback";
+    meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer ];
+
+    machine = { pkgs, lib, ... }: {
+      imports = [ common ];
+      boot.loader.efi.canTouchEfiVariables = mkForce false;
+    };
+
+    testScript = ''
+      machine.start()
+      machine.wait_for_unit("multi-user.target")
+
+      machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
+
+      # Ensure we actually booted using systemd-boot
+      # Magic number is the vendor UUID used by systemd-boot.
+      machine.succeed(
+          "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
+      )
+
+      # "bootctl install" should _not_ have created an EFI entry
+      machine.fail('efibootmgr | grep "Linux Boot Manager"')
+    '';
+  };
+
+  update = makeTest {
+    name = "systemd-boot-update";
+    meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer ];
+
+    machine = common;
+
+    testScript = ''
+      machine.succeed("mount -o remount,rw /boot")
+
+      # Replace version inside sd-boot with something older. See magic[] string in systemd src/boot/efi/boot.c
+      machine.succeed(
+          """
+        find /boot -iname '*.efi' -print0 | \
+        xargs -0 -I '{}' sed -i 's/#### LoaderInfo: systemd-boot .* ####/#### LoaderInfo: systemd-boot 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
+    '';
+  };
+
+  memtest86 = makeTest {
+    name = "systemd-boot-memtest86";
+    meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
+
+    machine = { pkgs, lib, ... }: {
+      imports = [ common ];
+      boot.loader.systemd-boot.memtest86.enable = true;
+      nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
+        "memtest86-efi"
+      ];
+    };
+
+    testScript = ''
+      machine.succeed("test -e /boot/loader/entries/memtest86.conf")
+      machine.succeed("test -e /boot/efi/memtest86/BOOTX64.efi")
+    '';
+  };
+
+  netbootxyz = makeTest {
+    name = "systemd-boot-netbootxyz";
+    meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
+
+    machine = { pkgs, lib, ... }: {
+      imports = [ common ];
+      boot.loader.systemd-boot.netbootxyz.enable = true;
+    };
+
+    testScript = ''
+      machine.succeed("test -e /boot/loader/entries/o_netbootxyz.conf")
+      machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi")
+    '';
+  };
+
+  entryFilename = makeTest {
+    name = "systemd-boot-entry-filename";
+    meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
+
+    machine = { pkgs, lib, ... }: {
+      imports = [ common ];
+      boot.loader.systemd-boot.memtest86.enable = true;
+      boot.loader.systemd-boot.memtest86.entryFilename = "apple.conf";
+      nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
+        "memtest86-efi"
+      ];
+    };
+
+    testScript = ''
+      machine.fail("test -e /boot/loader/entries/memtest86.conf")
+      machine.succeed("test -e /boot/loader/entries/apple.conf")
+      machine.succeed("test -e /boot/efi/memtest86/BOOTX64.efi")
+    '';
+  };
+
+  extraEntries = makeTest {
+    name = "systemd-boot-extra-entries";
+    meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
+
+    machine = { pkgs, lib, ... }: {
+      imports = [ common ];
+      boot.loader.systemd-boot.extraEntries = {
+        "banana.conf" = ''
+          title banana
+        '';
+      };
+    };
+
+    testScript = ''
+      machine.succeed("test -e /boot/loader/entries/banana.conf")
+      machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/banana.conf")
+    '';
+  };
+
+  extraFiles = makeTest {
+    name = "systemd-boot-extra-files";
+    meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
+
+    machine = { pkgs, lib, ... }: {
+      imports = [ common ];
+      boot.loader.systemd-boot.extraFiles = {
+        "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
+      };
+    };
+
+    testScript = ''
+      machine.succeed("test -e /boot/efi/fruits/tomato.efi")
+      machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
+    '';
+  };
+
+  switch-test = makeTest {
+    name = "systemd-boot-switch-test";
+    meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
+
+    nodes = {
+      inherit common;
+
+      machine = { pkgs, ... }: {
+        imports = [ common ];
+        boot.loader.systemd-boot.extraFiles = {
+          "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
+        };
+      };
+
+      with_netbootxyz = { pkgs, ... }: {
+        imports = [ common ];
+        boot.loader.systemd-boot.netbootxyz.enable = true;
+      };
+    };
+
+    testScript = { nodes, ... }: let
+      originalSystem = nodes.machine.config.system.build.toplevel;
+      baseSystem = nodes.common.config.system.build.toplevel;
+      finalSystem = nodes.with_netbootxyz.config.system.build.toplevel;
+    in ''
+      machine.succeed("test -e /boot/efi/fruits/tomato.efi")
+      machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
+
+      with subtest("remove files when no longer needed"):
+          machine.succeed("${baseSystem}/bin/switch-to-configuration boot")
+          machine.fail("test -e /boot/efi/fruits/tomato.efi")
+          machine.fail("test -d /boot/efi/fruits")
+          machine.succeed("test -d /boot/efi/nixos/.extra-files")
+          machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
+          machine.fail("test -d /boot/efi/nixos/.extra-files/efi/fruits")
+
+      with subtest("files are added back when needed again"):
+          machine.succeed("${originalSystem}/bin/switch-to-configuration boot")
+          machine.succeed("test -e /boot/efi/fruits/tomato.efi")
+          machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
+
+      with subtest("simultaneously removing and adding files works"):
+          machine.succeed("${finalSystem}/bin/switch-to-configuration boot")
+          machine.fail("test -e /boot/efi/fruits/tomato.efi")
+          machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
+          machine.succeed("test -e /boot/loader/entries/o_netbootxyz.conf")
+          machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi")
+          machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/o_netbootxyz.conf")
+          machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/netbootxyz/netboot.xyz.efi")
+    '';
+  };
+}
diff --git a/nixos/tests/systemd-confinement.nix b/nixos/tests/systemd-confinement.nix
new file mode 100644
index 00000000000..3181af309a6
--- /dev/null
+++ b/nixos/tests/systemd-confinement.nix
@@ -0,0 +1,184 @@
+import ./make-test-python.nix {
+  name = "systemd-confinement";
+
+  machine = { pkgs, lib, ... }: let
+    testServer = pkgs.writeScript "testserver.sh" ''
+      #!${pkgs.runtimeShell}
+      export PATH=${lib.escapeShellArg "${pkgs.coreutils}/bin"}
+      ${lib.escapeShellArg pkgs.runtimeShell} 2>&1
+      echo "exit-status:$?"
+    '';
+
+    testClient = pkgs.writeScriptBin "chroot-exec" ''
+      #!${pkgs.runtimeShell} -e
+      output="$(echo "$@" | nc -NU "/run/test$(< /teststep).sock")"
+      ret="$(echo "$output" | sed -nre '$s/^exit-status:([0-9]+)$/\1/p')"
+      echo "$output" | head -n -1
+      exit "''${ret:-1}"
+    '';
+
+    mkTestStep = num: {
+      testScript,
+      config ? {},
+      serviceName ? "test${toString num}",
+    }: {
+      systemd.sockets.${serviceName} = {
+        description = "Socket for Test Service ${toString num}";
+        wantedBy = [ "sockets.target" ];
+        socketConfig.ListenStream = "/run/test${toString num}.sock";
+        socketConfig.Accept = true;
+      };
+
+      systemd.services."${serviceName}@" = {
+        description = "Confined Test Service ${toString num}";
+        confinement = (config.confinement or {}) // { enable = true; };
+        serviceConfig = (config.serviceConfig or {}) // {
+          ExecStart = testServer;
+          StandardInput = "socket";
+        };
+      } // removeAttrs config [ "confinement" "serviceConfig" ];
+
+      __testSteps = lib.mkOrder num (''
+        machine.succeed("echo ${toString num} > /teststep")
+      '' + testScript);
+    };
+
+  in {
+    imports = lib.imap1 mkTestStep [
+      { config.confinement.mode = "chroot-only";
+        testScript = ''
+          with subtest("chroot-only confinement"):
+              paths = machine.succeed('chroot-exec ls -1 / | paste -sd,').strip()
+              assert_eq(paths, "bin,nix,run")
+              uid = machine.succeed('chroot-exec id -u').strip()
+              assert_eq(uid, "0")
+              machine.succeed("chroot-exec chown 65534 /bin")
+        '';
+      }
+      { testScript = ''
+          with subtest("full confinement with APIVFS"):
+              machine.fail("chroot-exec ls -l /etc")
+              machine.fail("chroot-exec chown 65534 /bin")
+              assert_eq(machine.succeed('chroot-exec id -u').strip(), "0")
+              machine.succeed("chroot-exec chown 0 /bin")
+        '';
+      }
+      { config.serviceConfig.BindReadOnlyPaths = [ "/etc" ];
+        testScript = ''
+          with subtest("check existence of bind-mounted /etc"):
+              passwd = machine.succeed('chroot-exec cat /etc/passwd').strip()
+              assert len(passwd) > 0, "/etc/passwd must not be empty"
+        '';
+      }
+      { config.serviceConfig.User = "chroot-testuser";
+        config.serviceConfig.Group = "chroot-testgroup";
+        testScript = ''
+          with subtest("check if User/Group really runs as non-root"):
+              machine.succeed("chroot-exec ls -l /dev")
+              uid = machine.succeed('chroot-exec id -u').strip()
+              assert uid != "0", "UID of chroot-testuser shouldn't be 0"
+              machine.fail("chroot-exec touch /bin/test")
+        '';
+      }
+      (let
+        symlink = pkgs.runCommand "symlink" {
+          target = pkgs.writeText "symlink-target" "got me\n";
+        } "ln -s \"$target\" \"$out\"";
+      in {
+        config.confinement.packages = lib.singleton symlink;
+        testScript = ''
+          with subtest("check if symlinks are properly bind-mounted"):
+              machine.fail("chroot-exec test -e /etc")
+              text = machine.succeed('chroot-exec cat ${symlink}').strip()
+              assert_eq(text, "got me")
+        '';
+      })
+      { config.serviceConfig.User = "chroot-testuser";
+        config.serviceConfig.Group = "chroot-testgroup";
+        config.serviceConfig.StateDirectory = "testme";
+        testScript = ''
+          with subtest("check if StateDirectory works"):
+              machine.succeed("chroot-exec touch /tmp/canary")
+              machine.succeed('chroot-exec "echo works > /var/lib/testme/foo"')
+              machine.succeed('test "$(< /var/lib/testme/foo)" = works')
+              machine.succeed("test ! -e /tmp/canary")
+        '';
+      }
+      { testScript = ''
+          with subtest("check if /bin/sh works"):
+              machine.succeed(
+                  "chroot-exec test -e /bin/sh",
+                  'test "$(chroot-exec \'/bin/sh -c "echo bar"\')" = bar',
+              )
+        '';
+      }
+      { config.confinement.binSh = null;
+        testScript = ''
+          with subtest("check if suppressing /bin/sh works"):
+              machine.succeed("chroot-exec test ! -e /bin/sh")
+              machine.succeed('test "$(chroot-exec \'/bin/sh -c "echo foo"\')" != foo')
+        '';
+      }
+      { config.confinement.binSh = "${pkgs.hello}/bin/hello";
+        testScript = ''
+          with subtest("check if we can set /bin/sh to something different"):
+              machine.succeed("chroot-exec test -e /bin/sh")
+              machine.succeed('test "$(chroot-exec /bin/sh -g foo)" = foo')
+        '';
+      }
+      { config.environment.FOOBAR = pkgs.writeText "foobar" "eek\n";
+        testScript = ''
+          with subtest("check if only Exec* dependencies are included"):
+              machine.succeed('test "$(chroot-exec \'cat "$FOOBAR"\')" != eek')
+        '';
+      }
+      { config.environment.FOOBAR = pkgs.writeText "foobar" "eek\n";
+        config.confinement.fullUnit = true;
+        testScript = ''
+          with subtest("check if all unit dependencies are included"):
+              machine.succeed('test "$(chroot-exec \'cat "$FOOBAR"\')" = eek')
+        '';
+      }
+      { serviceName = "shipped-unitfile";
+        config.confinement.mode = "chroot-only";
+        testScript = ''
+          with subtest("check if shipped unit file still works"):
+              machine.succeed(
+                  'chroot-exec \'kill -9 $$ 2>&1 || :\' | '
+                  'grep -q "Too many levels of symbolic links"'
+              )
+        '';
+      }
+    ];
+
+    options.__testSteps = lib.mkOption {
+      type = lib.types.lines;
+      description = "All of the test steps combined as a single script.";
+    };
+
+    config.environment.systemPackages = lib.singleton testClient;
+    config.systemd.packages = lib.singleton (pkgs.writeTextFile {
+      name = "shipped-unitfile";
+      destination = "/etc/systemd/system/shipped-unitfile@.service";
+      text = ''
+        [Service]
+        SystemCallFilter=~kill
+        SystemCallErrorNumber=ELOOP
+      '';
+    });
+
+    config.users.groups.chroot-testgroup = {};
+    config.users.users.chroot-testuser = {
+      isSystemUser = true;
+      description = "Chroot Test User";
+      group = "chroot-testgroup";
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+    def assert_eq(a, b):
+        assert a == b, f"{a} != {b}"
+
+    machine.wait_for_unit("multi-user.target")
+  '' + nodes.machine.config.__testSteps;
+}
diff --git a/nixos/tests/systemd-cryptenroll.nix b/nixos/tests/systemd-cryptenroll.nix
new file mode 100644
index 00000000000..49634ef6567
--- /dev/null
+++ b/nixos/tests/systemd-cryptenroll.nix
@@ -0,0 +1,54 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "systemd-cryptenroll";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ymatsiuk ];
+  };
+
+  machine = { pkgs, lib, ... }: {
+    environment.systemPackages = [ pkgs.cryptsetup ];
+    virtualisation = {
+      emptyDiskImages = [ 512 ];
+      qemu.options = [
+        "-chardev socket,id=chrtpm,path=/tmp/swtpm-sock"
+        "-tpmdev emulator,id=tpm0,chardev=chrtpm"
+        "-device tpm-tis,tpmdev=tpm0"
+      ];
+    };
+  };
+
+  testScript = ''
+    import subprocess
+    import tempfile
+
+    def start_swtpm(tpmstate):
+        subprocess.Popen(["${pkgs.swtpm}/bin/swtpm", "socket", "--tpmstate", "dir="+tpmstate, "--ctrl", "type=unixio,path=/tmp/swtpm-sock", "--log", "level=0", "--tpm2"])
+
+    with tempfile.TemporaryDirectory() as tpmstate:
+        start_swtpm(tpmstate)
+        machine.start()
+
+        # Verify the TPM device is available and accessible by systemd-cryptenroll
+        machine.succeed("test -e /dev/tpm0")
+        machine.succeed("test -e /dev/tpmrm0")
+        machine.succeed("systemd-cryptenroll --tpm2-device=list")
+
+        # Create LUKS partition
+        machine.succeed("echo -n lukspass | cryptsetup luksFormat -q /dev/vdb -")
+        # Enroll new LUKS key and bind it to Secure Boot state
+        # For more details on PASSWORD variable, check the following issue:
+        # https://github.com/systemd/systemd/issues/20955
+        machine.succeed("PASSWORD=lukspass systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 /dev/vdb")
+        # Add LUKS partition to /etc/crypttab to test auto unlock
+        machine.succeed("echo 'luks /dev/vdb - tpm2-device=auto' >> /etc/crypttab")
+        machine.shutdown()
+
+        start_swtpm(tpmstate)
+        machine.start()
+
+        # Test LUKS partition automatic unlock on boot
+        machine.wait_for_unit("systemd-cryptsetup@luks.service")
+        # Wipe TPM2 slot
+        machine.succeed("systemd-cryptenroll --wipe-slot=tpm2 /dev/vdb")
+  '';
+})
+
diff --git a/nixos/tests/systemd-escaping.nix b/nixos/tests/systemd-escaping.nix
new file mode 100644
index 00000000000..7f93eb5e4f7
--- /dev/null
+++ b/nixos/tests/systemd-escaping.nix
@@ -0,0 +1,45 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+  echoAll = pkgs.writeScript "echo-all" ''
+    #! ${pkgs.runtimeShell}
+    for s in "$@"; do
+      printf '%s\n' "$s"
+    done
+  '';
+  # deliberately using a local empty file instead of pkgs.emptyFile to have
+  # a non-store path in the test
+  args = [ "a%Nything" "lang=\${LANG}" ";" "/bin/sh -c date" ./empty-file 4.2 23 ];
+in
+{
+  name = "systemd-escaping";
+
+  machine = { pkgs, lib, utils, ... }: {
+    systemd.services.echo =
+      assert !(builtins.tryEval (utils.escapeSystemdExecArgs [ [] ])).success;
+      assert !(builtins.tryEval (utils.escapeSystemdExecArgs [ {} ])).success;
+      assert !(builtins.tryEval (utils.escapeSystemdExecArgs [ null ])).success;
+      assert !(builtins.tryEval (utils.escapeSystemdExecArgs [ false ])).success;
+      assert !(builtins.tryEval (utils.escapeSystemdExecArgs [ (_:_) ])).success;
+      { description = "Echo to the journal";
+        serviceConfig.Type = "oneshot";
+        serviceConfig.ExecStart = ''
+          ${echoAll} ${utils.escapeSystemdExecArgs args}
+        '';
+      };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed("systemctl start echo.service")
+    # skip the first 'Starting <service> ...' line
+    logs = machine.succeed("journalctl -u echo.service -o cat").splitlines()[1:]
+    assert "a%Nything" == logs[0]
+    assert "lang=''${LANG}" == logs[1]
+    assert ";" == logs[2]
+    assert "/bin/sh -c date" == logs[3]
+    assert "/nix/store/ij3gw72f4n5z4dz6nnzl1731p9kmjbwr-empty-file" == logs[4]
+    assert "4.2" in logs[5] # toString produces extra fractional digits!
+    assert "23" == logs[6]
+  '';
+})
diff --git a/nixos/tests/systemd-journal.nix b/nixos/tests/systemd-journal.nix
new file mode 100644
index 00000000000..6ab7c724631
--- /dev/null
+++ b/nixos/tests/systemd-journal.nix
@@ -0,0 +1,22 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "systemd-journal";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lewo ];
+  };
+
+  machine = { pkgs, lib, ... }: {
+    services.journald.enableHttpGateway = true;
+  };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+
+    machine.succeed("journalctl --grep=systemd")
+
+    machine.succeed(
+        "${pkgs.curl}/bin/curl -s localhost:19531/machine | ${pkgs.jq}/bin/jq -e '.hostname == \"machine\"'"
+    )
+  '';
+})
diff --git a/nixos/tests/systemd-machinectl.nix b/nixos/tests/systemd-machinectl.nix
new file mode 100644
index 00000000000..4fc5864357c
--- /dev/null
+++ b/nixos/tests/systemd-machinectl.nix
@@ -0,0 +1,85 @@
+import ./make-test-python.nix (
+  let
+
+    container = {
+      # We re-use the NixOS container option ...
+      boot.isContainer = true;
+      # ... and revert unwanted defaults
+      networking.useHostResolvConf = false;
+
+      # use networkd to obtain systemd network setup
+      networking.useNetworkd = true;
+      networking.useDHCP = false;
+
+      # systemd-nspawn expects /sbin/init
+      boot.loader.initScript.enable = true;
+
+      imports = [ ../modules/profiles/minimal.nix ];
+    };
+
+    containerSystem = (import ../lib/eval-config.nix {
+      modules = [ container ];
+    }).config.system.build.toplevel;
+
+    containerName = "container";
+    containerRoot = "/var/lib/machines/${containerName}";
+
+  in
+  {
+    name = "systemd-machinectl";
+
+    machine = { lib, ... }: {
+      # use networkd to obtain systemd network setup
+      networking.useNetworkd = true;
+      networking.useDHCP = false;
+      services.resolved.enable = false;
+
+      # open DHCP server on interface to container
+      networking.firewall.trustedInterfaces = [ "ve-+" ];
+
+      # do not try to access cache.nixos.org
+      nix.settings.substituters = lib.mkForce [ ];
+
+      virtualisation.additionalPaths = [ containerSystem ];
+    };
+
+    testScript = ''
+      start_all()
+      machine.wait_for_unit("default.target");
+
+      # Install container
+      machine.succeed("mkdir -p ${containerRoot}");
+      # Workaround for nixos-install
+      machine.succeed("chmod o+rx /var/lib/machines");
+      machine.succeed("nixos-install --root ${containerRoot} --system ${containerSystem} --no-channel-copy --no-root-passwd");
+
+      # Allow systemd-nspawn to apply user namespace on immutable files
+      machine.succeed("chattr -i ${containerRoot}/var/empty");
+
+      # Test machinectl start
+      machine.succeed("machinectl start ${containerName}");
+      machine.wait_until_succeeds("systemctl -M ${containerName} is-active default.target");
+
+      # Test systemd-nspawn network configuration
+      machine.succeed("ping -n -c 1 ${containerName}");
+
+      # Test systemd-nspawn uses a user namespace
+      machine.succeed("test `stat ${containerRoot}/var/empty -c %u%g` != 00");
+
+      # Test systemd-nspawn reboot
+      machine.succeed("machinectl shell ${containerName} /run/current-system/sw/bin/reboot");
+      machine.wait_until_succeeds("systemctl -M ${containerName} is-active default.target");
+
+      # Test machinectl reboot
+      machine.succeed("machinectl reboot ${containerName}");
+      machine.wait_until_succeeds("systemctl -M ${containerName} is-active default.target");
+
+      # Test machinectl stop
+      machine.succeed("machinectl stop ${containerName}");
+
+      # Show to to delete the container
+      machine.succeed("chattr -i ${containerRoot}/var/empty");
+      machine.succeed("rm -rf ${containerRoot}");
+    '';
+  }
+)
diff --git a/nixos/tests/systemd-networkd-dhcpserver-static-leases.nix b/nixos/tests/systemd-networkd-dhcpserver-static-leases.nix
new file mode 100644
index 00000000000..a8254a15801
--- /dev/null
+++ b/nixos/tests/systemd-networkd-dhcpserver-static-leases.nix
@@ -0,0 +1,81 @@
+# In contrast to systemd-networkd-dhcpserver, this test configures
+# the router with a static DHCP lease for the client's MAC address.
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "systemd-networkd-dhcpserver-static-leases";
+  meta = with lib.maintainers; {
+    maintainers = [ veehaitch tomfitzhenry ];
+  };
+  nodes = {
+    router = {
+      virtualisation.vlans = [ 1 ];
+      systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
+      networking = {
+        useNetworkd = true;
+        useDHCP = false;
+        firewall.enable = false;
+      };
+      systemd.network = {
+        networks = {
+          # systemd-networkd will load the first network unit file
+          # that matches, ordered lexiographically by filename.
+          # /etc/systemd/network/{40-eth1,99-main}.network already
+          # exists. This network unit must be loaded for the test,
+          # however, hence why this network is named such.
+          "01-eth1" = {
+            name = "eth1";
+            networkConfig = {
+              DHCPServer = true;
+              Address = "10.0.0.1/24";
+            };
+            dhcpServerStaticLeases = [{
+              dhcpServerStaticLeaseConfig = {
+                MACAddress = "02:de:ad:be:ef:01";
+                Address = "10.0.0.10";
+              };
+            }];
+          };
+        };
+      };
+    };
+
+    client = {
+      virtualisation.vlans = [ 1 ];
+      systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
+      networking = {
+        useNetworkd = true;
+        useDHCP = false;
+        firewall.enable = false;
+        interfaces.eth1 = {
+          useDHCP = true;
+          macAddress = "02:de:ad:be:ef:01";
+        };
+      };
+
+      # This setting is important to have the router assign the
+      # configured lease based on the client's MAC address. Also see:
+      # https://github.com/systemd/systemd/issues/21368#issuecomment-982193546
+      systemd.network.networks."40-eth1".dhcpV4Config.ClientIdentifier = "mac";
+    };
+  };
+  testScript = ''
+    start_all()
+
+    with subtest("check router network configuration"):
+      router.wait_for_unit("systemd-networkd-wait-online.service")
+      eth1_status = router.succeed("networkctl status eth1")
+      assert "Network File: /etc/systemd/network/01-eth1.network" in eth1_status, \
+        "The router interface eth1 is not using the expected network file"
+      assert "10.0.0.1" in eth1_status, "Did not find expected router IPv4"
+
+    with subtest("check client network configuration"):
+      client.wait_for_unit("systemd-networkd-wait-online.service")
+      eth1_status = client.succeed("networkctl status eth1")
+      assert "Network File: /etc/systemd/network/40-eth1.network" in eth1_status, \
+        "The client interface eth1 is not using the expected network file"
+      assert "10.0.0.10" in eth1_status, "Did not find expected client IPv4"
+
+    with subtest("router and client can reach each other"):
+      client.wait_until_succeeds("ping -c 5 10.0.0.1")
+      router.wait_until_succeeds("ping -c 5 10.0.0.10")
+  '';
+})
diff --git a/nixos/tests/systemd-networkd-dhcpserver.nix b/nixos/tests/systemd-networkd-dhcpserver.nix
new file mode 100644
index 00000000000..b52c1499718
--- /dev/null
+++ b/nixos/tests/systemd-networkd-dhcpserver.nix
@@ -0,0 +1,58 @@
+# This test predominantly tests systemd-networkd DHCP server, by
+# setting up a DHCP server and client, and ensuring they are mutually
+# reachable via the DHCP allocated address.
+import ./make-test-python.nix ({pkgs, ...}: {
+  name = "systemd-networkd-dhcpserver";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ tomfitzhenry ];
+  };
+  nodes = {
+    router = { config, pkgs, ... }: {
+      virtualisation.vlans = [ 1 ];
+      systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
+      networking = {
+        useNetworkd = true;
+        useDHCP = false;
+        firewall.enable = false;
+      };
+      systemd.network = {
+        networks = {
+          # systemd-networkd will load the first network unit file
+          # that matches, ordered lexiographically by filename.
+          # /etc/systemd/network/{40-eth1,99-main}.network already
+          # exists. This network unit must be loaded for the test,
+          # however, hence why this network is named such.
+          "01-eth1" = {
+            name = "eth1";
+            networkConfig = {
+              DHCPServer = true;
+              Address = "10.0.0.1/24";
+            };
+            dhcpServerConfig = {
+              PoolOffset = 100;
+              PoolSize = 1;
+            };
+          };
+        };
+      };
+    };
+
+    client = { config, pkgs, ... }: {
+      virtualisation.vlans = [ 1 ];
+      systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
+      networking = {
+        useNetworkd = true;
+        useDHCP = false;
+        firewall.enable = false;
+        interfaces.eth1.useDHCP = true;
+      };
+    };
+  };
+  testScript = { ... }: ''
+    start_all()
+    router.wait_for_unit("systemd-networkd-wait-online.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.100")
+  '';
+})
diff --git a/nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix b/nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix
new file mode 100644
index 00000000000..37a89fc21e4
--- /dev/null
+++ b/nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix
@@ -0,0 +1,284 @@
+# This test verifies that we can request and assign IPv6 prefixes from upstream
+# (e.g. ISP) routers.
+# The setup consits of three VMs. One for the ISP, as your residential router
+# and the third as a client machine in the residential network.
+#
+# There are two VLANs in this test:
+# - VLAN 1 is the connection between the ISP and the router
+# - VLAN 2 is the connection between the router and the client
+
+import ./make-test-python.nix ({pkgs, ...}: {
+  name = "systemd-networkd-ipv6-prefix-delegation";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ andir ];
+  };
+  nodes = {
+
+    # The ISP's routers job is to delegate IPv6 prefixes via DHCPv6. Like with
+    # regular IPv6 auto-configuration it will also emit IPv6 router
+    # advertisements (RAs). Those RA's will not carry a prefix but in contrast
+    # just set the "Other" flag to indicate to the receiving nodes that they
+    # should attempt DHCPv6.
+    #
+    # Note: On the ISPs device we don't really care if we are using networkd in
+    # this example. That being said we can't use it (yet) as networkd doesn't
+    # implement the serving side of DHCPv6. We will use ISC's well aged dhcpd6
+    # for that task.
+    isp = { lib, pkgs, ... }: {
+      virtualisation.vlans = [ 1 ];
+      networking = {
+        useDHCP = false;
+        firewall.enable = false;
+        interfaces.eth1.ipv4.addresses = lib.mkForce []; # no need for legacy IP
+        interfaces.eth1.ipv6.addresses = lib.mkForce [
+          { address = "2001:DB8::1"; prefixLength = 64; }
+        ];
+      };
+
+      # Since we want to program the routes that we delegate to the "customer"
+      # into our routing table we must give dhcpd the required privs.
+      systemd.services.dhcpd6.serviceConfig.AmbientCapabilities =
+        [ "CAP_NET_ADMIN" ];
+
+      services = {
+        # Configure the DHCPv6 server
+        #
+        # We will hand out /48 prefixes from the subnet 2001:DB8:F000::/36.
+        # That gives us ~8k prefixes. That should be enough for this test.
+        #
+        # Since (usually) you will not receive a prefix with the router
+        # advertisements we also hand out /128 leases from the range
+        # 2001:DB8:0000:0000:FFFF::/112.
+        dhcpd6 = {
+          enable = true;
+          interfaces = [ "eth1" ];
+          extraConfig = ''
+            subnet6 2001:DB8::/36 {
+              range6 2001:DB8:0000:0000:FFFF:: 2001:DB8:0000:0000:FFFF::FFFF;
+              prefix6 2001:DB8:F000:: 2001:DB8:FFFF:: /48;
+            }
+
+            # This is the secret sauce. We have to extract the prefix and the
+            # next hop when commiting the lease to the database.  dhcpd6
+            # (rightfully) has not concept of adding routes to the systems
+            # routing table. It really depends on the setup.
+            #
+            # In a production environment your DHCPv6 server is likely not the
+            # router. You might want to consider BGP, custom NetConf calls, …
+            # in those cases.
+            on commit {
+              set IP = pick-first-value(binary-to-ascii(16, 16, ":", substring(option dhcp6.ia-na, 16, 16)), "n/a");
+              set Prefix = pick-first-value(binary-to-ascii(16, 16, ":", suffix(option dhcp6.ia-pd, 16)), "n/a");
+              set PrefixLength = pick-first-value(binary-to-ascii(10, 8, ":", substring(suffix(option dhcp6.ia-pd, 17), 0, 1)), "n/a");
+              log(concat(IP, " ", Prefix, " ", PrefixLength));
+              execute("${pkgs.iproute2}/bin/ip", "-6", "route", "replace", concat(Prefix,"/",PrefixLength), "via", IP);
+            }
+          '';
+        };
+
+        # Finally we have to set up the router advertisements. While we could be
+        # using networkd or bird for this task `radvd` is probably the most
+        # venerable of them all. It was made explicitly for this purpose and
+        # the configuration is much more straightforward than what networkd
+        # requires.
+        # As outlined above we will have to set the `Managed` flag as otherwise
+        # the clients will not know if they should do DHCPv6. (Some do
+        # anyway/always)
+        radvd = {
+          enable = true;
+          config = ''
+            interface eth1 {
+              AdvSendAdvert on;
+              AdvManagedFlag on;
+              AdvOtherConfigFlag off; # we don't really have DNS or NTP or anything like that to distribute
+              prefix ::/64 {
+                AdvOnLink on;
+                AdvAutonomous on;
+              };
+            };
+          '';
+        };
+
+      };
+    };
+
+    # This will be our (residential) router that receives the IPv6 prefix (IA_PD)
+    # and /128 (IA_NA) allocation.
+    #
+    # Here we will actually start using networkd.
+    router = {
+      virtualisation.vlans = [ 1 2 ];
+      systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
+
+      boot.kernel.sysctl = {
+        # we want to forward packets from the ISP to the client and back.
+        "net.ipv6.conf.all.forwarding" = 1;
+      };
+
+      networking = {
+        useNetworkd = true;
+        useDHCP = false;
+        # Consider enabling this in production and generating firewall rules
+        # for fowarding/input from the configured interfaces so you do not have
+        # to manage multiple places
+        firewall.enable = false;
+      };
+
+      systemd.network = {
+        networks = {
+          # systemd-networkd will load the first network unit file
+          # that matches, ordered lexiographically by filename.
+          # /etc/systemd/network/{40-eth1,99-main}.network already
+          # exists. This network unit must be loaded for the test,
+          # however, hence why this network is named such.
+
+          # Configuration of the interface to the ISP.
+          # We must request accept RAs and request the PD prefix.
+          "01-eth1" = {
+            name = "eth1";
+            networkConfig = {
+              Description = "ISP interface";
+              IPv6AcceptRA = true;
+              #DHCP = false; # no need for legacy IP
+            };
+            linkConfig = {
+              # We care about this interface when talking about being "online".
+              # If this interface is in the `routable` state we can reach
+              # others and they should be able to reach us.
+              RequiredForOnline = "routable";
+            };
+            # This configures the DHCPv6 client part towards the ISPs DHCPv6 server.
+            dhcpV6Config = {
+              # We have to include a request for a prefix in our DHCPv6 client
+              # request packets.
+              # Otherwise the upstream DHCPv6 server wouldn't know if we want a
+              # prefix or not.  Note: On some installation it makes sense to
+              # always force that option on the DHPCv6 server since there are
+              # certain CPEs that are just not setting this field but happily
+              # accept the delegated prefix.
+              PrefixDelegationHint  = "::/48";
+            };
+            ipv6SendRAConfig = {
+              # Let networkd know that we would very much like to use DHCPv6
+              # to obtain the "managed" information. Not sure why they can't
+              # just take that from the upstream RAs.
+              Managed = true;
+            };
+          };
+
+          # Interface to the client. Here we should redistribute a /64 from
+          # the prefix we received from the ISP.
+          "01-eth2" = {
+            name = "eth2";
+            networkConfig = {
+              Description = "Client interface";
+              # The client shouldn't be allowed to send us RAs, that would be weird.
+              IPv6AcceptRA = false;
+
+              # Delegate prefixes from the DHCPv6 PD pool.
+              DHCPv6PrefixDelegation = true;
+              IPv6SendRA = true;
+            };
+
+            # In a production environment you should consider setting these as well:
+            # ipv6SendRAConfig = {
+              #EmitDNS = true;
+              #EmitDomains = true;
+              #DNS= = "fe80::1"; # or whatever "well known" IP your router will have on the inside.
+            # };
+
+            # This adds a "random" ULA prefix to the interface that is being
+            # advertised to the clients.
+            # Not used in this test.
+            # ipv6Prefixes = [
+            #   {
+            #     ipv6PrefixConfig = {
+            #       AddressAutoconfiguration = true;
+            #       PreferredLifetimeSec = 1800;
+            #       ValidLifetimeSec = 1800;
+            #     };
+            #   }
+            # ];
+          };
+
+          # finally we are going to add a static IPv6 unique local address to
+          # the "lo" interface.  This will serve as ICMPv6 echo target to
+          # verify connectivity from the client to the router.
+          "01-lo" = {
+            name = "lo";
+            addresses = [
+              { addressConfig.Address = "FD42::1/128"; }
+            ];
+          };
+        };
+      };
+
+      # make the network-online target a requirement, we wait for it in our test script
+      systemd.targets.network-online.wantedBy = [ "multi-user.target" ];
+    };
+
+    # This is the client behind the router. We should be receving router
+    # advertisements for both the ULA and the delegated prefix.
+    # All we have to do is boot with the default (networkd) configuration.
+    client = {
+      virtualisation.vlans = [ 2 ];
+      systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
+      networking = {
+        useNetworkd = true;
+        useDHCP = false;
+      };
+
+      # make the network-online target a requirement, we wait for it in our test script
+      systemd.targets.network-online.wantedBy = [ "multi-user.target" ];
+    };
+  };
+
+  testScript = ''
+    # First start the router and wait for it it reach a state where we are
+    # certain networkd is up and it is able to send out RAs
+    router.start()
+    router.wait_for_unit("systemd-networkd.service")
+
+    # After that we can boot the client and wait for the network online target.
+    # Since we only care about IPv6 that should not involve waiting for legacy
+    # IP leases.
+    client.start()
+    client.wait_for_unit("network-online.target")
+
+    # the static address on the router should not be reachable
+    client.wait_until_succeeds("ping -6 -c 1 FD42::1")
+
+    # the global IP of the ISP router should still not be a reachable
+    router.fail("ping -6 -c 1 2001:DB8::1")
+
+    # Once we have internal connectivity boot up the ISP
+    isp.start()
+
+    # Since for the ISP "being online" should have no real meaning we just
+    # wait for the target where all the units have been started.
+    # It probably still takes a few more seconds for all the RA timers to be
+    # fired etc..
+    isp.wait_for_unit("multi-user.target")
+
+    # wait until the uplink interface has a good status
+    router.wait_for_unit("network-online.target")
+    router.wait_until_succeeds("ping -6 -c1 2001:DB8::1")
+
+    # shortly after that the client should have received it's global IPv6
+    # address and thus be able to ping the ISP
+    client.wait_until_succeeds("ping -6 -c1 2001:DB8::1")
+
+    # verify that we got a globally scoped address in eth1 from the
+    # documentation prefix
+    ip_output = client.succeed("ip --json -6 address show dev eth1")
+
+    import json
+
+    ip_json = json.loads(ip_output)[0]
+    assert any(
+        addr["local"].upper().startswith("2001:DB8:")
+        for addr in ip_json["addr_info"]
+        if addr["scope"] == "global"
+    )
+  '';
+})
diff --git a/nixos/tests/systemd-networkd-vrf.nix b/nixos/tests/systemd-networkd-vrf.nix
new file mode 100644
index 00000000000..8a1580fc2ad
--- /dev/null
+++ b/nixos/tests/systemd-networkd-vrf.nix
@@ -0,0 +1,223 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: let
+  inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
+in {
+  name = "systemd-networkd-vrf";
+  meta.maintainers = with lib.maintainers; [ ma27 ];
+
+  nodes = {
+    client = { pkgs, ... }: {
+      virtualisation.vlans = [ 1 2 ];
+
+      networking = {
+        useDHCP = false;
+        useNetworkd = true;
+        firewall.checkReversePath = "loose";
+      };
+
+      systemd.network = {
+        enable = true;
+
+        netdevs."10-vrf1" = {
+          netdevConfig = {
+            Kind = "vrf";
+            Name = "vrf1";
+            MTUBytes = "1300";
+          };
+          vrfConfig.Table = 23;
+        };
+        netdevs."10-vrf2" = {
+          netdevConfig = {
+            Kind = "vrf";
+            Name = "vrf2";
+            MTUBytes = "1300";
+          };
+          vrfConfig.Table = 42;
+        };
+
+        networks."10-vrf1" = {
+          matchConfig.Name = "vrf1";
+          networkConfig.IPForward = "yes";
+          routes = [
+            { routeConfig = { Destination = "192.168.1.2"; Metric = 100; }; }
+          ];
+        };
+        networks."10-vrf2" = {
+          matchConfig.Name = "vrf2";
+          networkConfig.IPForward = "yes";
+          routes = [
+            { routeConfig = { Destination = "192.168.2.3"; Metric = 100; }; }
+          ];
+        };
+
+        networks."10-eth1" = {
+          matchConfig.Name = "eth1";
+          linkConfig.RequiredForOnline = "no";
+          networkConfig = {
+            VRF = "vrf1";
+            Address = "192.168.1.1";
+            IPForward = "yes";
+          };
+        };
+        networks."10-eth2" = {
+          matchConfig.Name = "eth2";
+          linkConfig.RequiredForOnline = "no";
+          networkConfig = {
+            VRF = "vrf2";
+            Address = "192.168.2.1";
+            IPForward = "yes";
+          };
+        };
+      };
+    };
+
+    node1 = { pkgs, ... }: {
+      virtualisation.vlans = [ 1 ];
+      networking = {
+        useDHCP = false;
+        useNetworkd = true;
+      };
+
+      services.openssh.enable = true;
+      users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
+
+      systemd.network = {
+        enable = true;
+
+        networks."10-eth1" = {
+          matchConfig.Name = "eth1";
+          linkConfig.RequiredForOnline = "no";
+          networkConfig = {
+            Address = "192.168.1.2";
+            IPForward = "yes";
+          };
+        };
+      };
+    };
+
+    node2 = { pkgs, ... }: {
+      virtualisation.vlans = [ 2 ];
+      networking = {
+        useDHCP = false;
+        useNetworkd = true;
+      };
+
+      systemd.network = {
+        enable = true;
+
+        networks."10-eth2" = {
+          matchConfig.Name = "eth2";
+          linkConfig.RequiredForOnline = "no";
+          networkConfig = {
+            Address = "192.168.2.3";
+            IPForward = "yes";
+          };
+        };
+      };
+    };
+
+    node3 = { pkgs, ... }: {
+      virtualisation.vlans = [ 2 ];
+      networking = {
+        useDHCP = false;
+        useNetworkd = true;
+      };
+
+      systemd.network = {
+        enable = true;
+
+        networks."10-eth2" = {
+          matchConfig.Name = "eth2";
+          linkConfig.RequiredForOnline = "no";
+          networkConfig = {
+            Address = "192.168.2.4";
+            IPForward = "yes";
+          };
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    def compare_tables(expected, actual):
+        assert (
+            expected == actual
+        ), """
+        Routing tables don't match!
+        Expected:
+          {}
+        Actual:
+          {}
+        """.format(
+            expected, actual
+        )
+
+
+    start_all()
+
+    client.wait_for_unit("network.target")
+    node1.wait_for_unit("network.target")
+    node2.wait_for_unit("network.target")
+    node3.wait_for_unit("network.target")
+
+    # NOTE: please keep in mind that the trailing whitespaces in the following strings
+    # are intentional as the output is compared against the raw `iproute2`-output.
+    # editorconfig-checker-disable
+    client_ipv4_table = """
+    192.168.1.2 dev vrf1 proto static metric 100 
+    192.168.2.3 dev vrf2 proto static metric 100
+    """.strip()
+    vrf1_table = """
+    broadcast 192.168.1.0 dev eth1 proto kernel scope link src 192.168.1.1 
+    192.168.1.0/24 dev eth1 proto kernel scope link src 192.168.1.1 
+    local 192.168.1.1 dev eth1 proto kernel scope host src 192.168.1.1 
+    broadcast 192.168.1.255 dev eth1 proto kernel scope link src 192.168.1.1
+    """.strip()
+    vrf2_table = """
+    broadcast 192.168.2.0 dev eth2 proto kernel scope link src 192.168.2.1 
+    192.168.2.0/24 dev eth2 proto kernel scope link src 192.168.2.1 
+    local 192.168.2.1 dev eth2 proto kernel scope host src 192.168.2.1 
+    broadcast 192.168.2.255 dev eth2 proto kernel scope link src 192.168.2.1
+    """.strip()
+    # editorconfig-checker-enable
+
+    # Check that networkd properly configures the main routing table
+    # and the routing tables for the VRF.
+    with subtest("check vrf routing tables"):
+        compare_tables(
+            client_ipv4_table, client.succeed("ip -4 route list | head -n2").strip()
+        )
+        compare_tables(
+            vrf1_table, client.succeed("ip -4 route list table 23 | head -n4").strip()
+        )
+        compare_tables(
+            vrf2_table, client.succeed("ip -4 route list table 42 | head -n4").strip()
+        )
+
+    # Ensure that other nodes are reachable via ICMP through the VRF.
+    with subtest("icmp through vrf works"):
+        client.succeed("ping -c5 192.168.1.2")
+        client.succeed("ping -c5 192.168.2.3")
+
+    # Test whether TCP through a VRF IP is possible.
+    with subtest("tcp traffic through vrf works"):
+        node1.wait_for_open_port(22)
+        client.succeed(
+            "cat ${snakeOilPrivateKey} > privkey.snakeoil"
+        )
+        client.succeed("chmod 600 privkey.snakeoil")
+        client.succeed(
+            "ulimit -l 2048; ip vrf exec vrf1 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil root@192.168.1.2 true"
+        )
+
+    # Only configured routes through the VRF from the main routing table should
+    # work. Additional IPs are only reachable when binding to the vrf interface.
+    with subtest("only routes from main routing table work by default"):
+        client.fail("ping -c5 192.168.2.4")
+        client.succeed("ping -I vrf2 -c5 192.168.2.4")
+
+    client.shutdown()
+    node1.shutdown()
+    node2.shutdown()
+    node3.shutdown()
+  '';
+})
diff --git a/nixos/tests/systemd-networkd.nix b/nixos/tests/systemd-networkd.nix
new file mode 100644
index 00000000000..7faeae3704e
--- /dev/null
+++ b/nixos/tests/systemd-networkd.nix
@@ -0,0 +1,113 @@
+let generateNodeConf = { lib, pkgs, config, privk, pubk, peerId, nodeId, ...}: {
+      imports = [ common/user-account.nix ];
+      systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
+      networking.useNetworkd = true;
+      networking.useDHCP = false;
+      networking.firewall.enable = false;
+      virtualisation.vlans = [ 1 ];
+      environment.systemPackages = with pkgs; [ wireguard-tools ];
+      systemd.network = {
+        enable = true;
+        netdevs = {
+          "90-wg0" = {
+            netdevConfig = { Kind = "wireguard"; Name = "wg0"; };
+            wireguardConfig = {
+              # NOTE: we're storing the wireguard private key in the
+              #       store for this test. Do not do this in the real
+              #       world. Keep in mind the nix store is
+              #       world-readable.
+              PrivateKeyFile = pkgs.writeText "wg0-priv" privk;
+              ListenPort = 51820;
+              FirewallMark = 42;
+            };
+            wireguardPeers = [ {wireguardPeerConfig={
+              Endpoint = "192.168.1.${peerId}:51820";
+              PublicKey = pubk;
+              PresharedKeyFile = pkgs.writeText "psk.key" "yTL3sCOL33Wzi6yCnf9uZQl/Z8laSE+zwpqOHC4HhFU=";
+              AllowedIPs = [ "10.0.0.${peerId}/32" ];
+              PersistentKeepalive = 15;
+            };}];
+          };
+        };
+        networks = {
+          "99-nope" = {
+            matchConfig.Name = "eth*";
+            linkConfig.Unmanaged = true;
+          };
+          "90-wg0" = {
+            matchConfig = { Name = "wg0"; };
+            address = [ "10.0.0.${nodeId}/32" ];
+            routes = [
+              { routeConfig = { Gateway = "10.0.0.${nodeId}"; Destination = "10.0.0.0/24"; }; }
+            ];
+          };
+          "30-eth1" = {
+            matchConfig = { Name = "eth1"; };
+            address = [
+              "192.168.1.${nodeId}/24"
+              "fe80::${nodeId}/64"
+            ];
+            routingPolicyRules = [
+              { routingPolicyRuleConfig = { Table = 10; IncomingInterface = "eth1"; Family = "both"; };}
+              { routingPolicyRuleConfig = { Table = 20; OutgoingInterface = "eth1"; };}
+              { routingPolicyRuleConfig = { Table = 30; From = "192.168.1.1"; To = "192.168.1.2"; SourcePort = 666 ; DestinationPort = 667; };}
+              { routingPolicyRuleConfig = { Table = 40; IPProtocol = "tcp"; InvertRule = true; };}
+              { routingPolicyRuleConfig = { Table = 50; IncomingInterface = "eth1"; Family = "ipv4"; };}
+            ];
+          };
+        };
+      };
+    };
+in import ./make-test-python.nix ({pkgs, ... }: {
+  name = "networkd";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ninjatrappeur ];
+  };
+  nodes = {
+    node1 = { pkgs, ... }@attrs:
+    let localConf = {
+        privk = "GDiXWlMQKb379XthwX0haAbK6hTdjblllpjGX0heP00=";
+        pubk = "iRxpqj42nnY0Qz8MAQbSm7bXxXP5hkPqWYIULmvW+EE=";
+        nodeId = "1";
+        peerId = "2";
+    };
+    in generateNodeConf (attrs // localConf);
+
+    node2 = { pkgs, ... }@attrs:
+    let localConf = {
+        privk = "eHxSI2jwX/P4AOI0r8YppPw0+4NZnjOxfbS5mt06K2k=";
+        pubk = "27s0OvaBBdHoJYkH9osZpjpgSOVNw+RaKfboT/Sfq0g=";
+        nodeId = "2";
+        peerId = "1";
+    };
+    in generateNodeConf (attrs // localConf);
+  };
+testScript = ''
+    start_all()
+    node1.wait_for_unit("systemd-networkd-wait-online.service")
+    node2.wait_for_unit("systemd-networkd-wait-online.service")
+
+    # ================================
+    # Wireguard
+    # ================================
+    node1.succeed("ping -c 5 10.0.0.2")
+    node2.succeed("ping -c 5 10.0.0.1")
+    # Is the fwmark set?
+    node2.succeed("wg | grep -q 42")
+
+    # ================================
+    # Routing Policies
+    # ================================
+    # Testing all the routingPolicyRuleConfig members:
+    # Table + IncomingInterface
+    node1.succeed("sudo ip rule | grep 'from all iif eth1 lookup 10'")
+    # OutgoingInterface
+    node1.succeed("sudo ip rule | grep 'from all oif eth1 lookup 20'")
+    # From + To + SourcePort + DestinationPort
+    node1.succeed(
+        "sudo ip rule | grep 'from 192.168.1.1 to 192.168.1.2 sport 666 dport 667 lookup 30'"
+    )
+    # IPProtocol + InvertRule
+    node1.succeed("sudo ip rule | grep 'not from all ipproto tcp lookup 40'")
+'';
+})
diff --git a/nixos/tests/systemd-nspawn.nix b/nixos/tests/systemd-nspawn.nix
new file mode 100644
index 00000000000..5bf55060d2e
--- /dev/null
+++ b/nixos/tests/systemd-nspawn.nix
@@ -0,0 +1,60 @@
+import ./make-test-python.nix ({pkgs, lib, ...}:
+let
+  gpgKeyring = (pkgs.runCommand "gpg-keyring" { buildInputs = [ pkgs.gnupg ]; } ''
+    mkdir -p $out
+    export GNUPGHOME=$out
+    cat > foo <<EOF
+      %echo Generating a basic OpenPGP key
+      %no-protection
+      Key-Type: DSA
+      Key-Length: 1024
+      Subkey-Type: ELG-E
+      Subkey-Length: 1024
+      Name-Real: Joe Tester
+      Name-Email: joe@foo.bar
+      Expire-Date: 0
+      # Do a commit here, so that we can later print "done"
+      %commit
+      %echo done
+    EOF
+    gpg --batch --generate-key foo
+    rm $out/S.gpg-agent $out/S.gpg-agent.*
+    gpg --export joe@foo.bar -a > $out/pubkey.gpg
+  '');
+
+  nspawnImages = (pkgs.runCommand "localhost" { buildInputs = [ pkgs.coreutils pkgs.gnupg ]; } ''
+    mkdir -p $out
+    cd $out
+    dd if=/dev/urandom of=$out/testimage.raw bs=$((1024*1024+7)) count=5
+    sha256sum testimage.raw > SHA256SUMS
+    export GNUPGHOME="$(mktemp -d)"
+    cp -R ${gpgKeyring}/* $GNUPGHOME
+    gpg --batch --sign --detach-sign --output SHA256SUMS.gpg SHA256SUMS
+  '');
+in {
+  name = "systemd-nspawn";
+
+  nodes = {
+    server = { pkgs, ... }: {
+      networking.firewall.allowedTCPPorts = [ 80 ];
+      services.nginx = {
+        enable = true;
+        virtualHosts."server".root = nspawnImages;
+      };
+    };
+    client = { pkgs, ... }: {
+      environment.etc."systemd/import-pubring.gpg".source = "${gpgKeyring}/pubkey.gpg";
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    server.wait_for_unit("nginx.service")
+    client.wait_for_unit("network-online.target")
+    client.succeed("machinectl pull-raw --verify=signature http://server/testimage.raw")
+    client.succeed(
+        "cmp /var/lib/machines/testimage.raw ${nspawnImages}/testimage.raw"
+    )
+  '';
+})
diff --git a/nixos/tests/systemd-timesyncd.nix b/nixos/tests/systemd-timesyncd.nix
new file mode 100644
index 00000000000..ad5b9a47383
--- /dev/null
+++ b/nixos/tests/systemd-timesyncd.nix
@@ -0,0 +1,52 @@
+# Regression test for systemd-timesync having moved the state directory without
+# upstream providing a migration path. https://github.com/systemd/systemd/issues/12131
+
+import ./make-test-python.nix (let
+  common = { lib, ... }: {
+    # override the `false` value from the qemu-vm base profile
+    services.timesyncd.enable = lib.mkForce true;
+  };
+  mkVM = conf: { imports = [ conf common ]; };
+in {
+  name = "systemd-timesyncd";
+  nodes = {
+    current = mkVM {};
+    pre1909 = mkVM ({lib, ... }: with lib; {
+      # create the path that should be migrated by our activation script when
+      # upgrading to a newer nixos version
+      system.stateVersion = "19.03";
+      system.activationScripts.simulate-old-timesync-state-dir = mkBefore ''
+        rm -f /var/lib/systemd/timesync
+        mkdir -p /var/lib/systemd /var/lib/private/systemd/timesync
+        ln -s /var/lib/private/systemd/timesync /var/lib/systemd/timesync
+        chown systemd-timesync: /var/lib/private/systemd/timesync
+      '';
+    });
+  };
+
+  testScript = ''
+    start_all()
+    current.succeed("systemctl status systemd-timesyncd.service")
+    # on a new install with a recent systemd there should not be any
+    # leftovers from the dynamic user mess
+    current.succeed("test -e /var/lib/systemd/timesync")
+    current.succeed("test ! -L /var/lib/systemd/timesync")
+
+    # timesyncd should be running on the upgrading system since we fixed the
+    # file bits in the activation script
+    pre1909.succeed("systemctl status systemd-timesyncd.service")
+
+    # the path should be gone after the migration
+    pre1909.succeed("test ! -e /var/lib/private/systemd/timesync")
+
+    # and the new path should no longer be a symlink
+    pre1909.succeed("test -e /var/lib/systemd/timesync")
+    pre1909.succeed("test ! -L /var/lib/systemd/timesync")
+
+    # after a restart things should still work and not fail in the activation
+    # scripts and cause the boot to fail..
+    pre1909.shutdown()
+    pre1909.start()
+    pre1909.succeed("systemctl status systemd-timesyncd.service")
+  '';
+})
diff --git a/nixos/tests/systemd-unit-path.nix b/nixos/tests/systemd-unit-path.nix
new file mode 100644
index 00000000000..5998a187188
--- /dev/null
+++ b/nixos/tests/systemd-unit-path.nix
@@ -0,0 +1,47 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+  exampleScript = pkgs.writeTextFile {
+    name = "example.sh";
+    text = ''
+      #! ${pkgs.runtimeShell} -e
+
+      while true; do
+          echo "Example script running" >&2
+          ${pkgs.coreutils}/bin/sleep 1
+      done
+    '';
+    executable = true;
+  };
+
+  unitFile = pkgs.writeTextFile {
+    name = "example.service";
+    text = ''
+      [Unit]
+      Description=Example systemd service unit file
+
+      [Service]
+      ExecStart=${exampleScript}
+
+      [Install]
+      WantedBy=multi-user.target
+    '';
+  };
+in
+{
+  name = "systemd-unit-path";
+
+  machine = { pkgs, lib, ... }: {
+    boot.extraSystemdUnitPaths = [ "/etc/systemd-rw/system" ];
+  };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed("mkdir -p /etc/systemd-rw/system")
+    machine.succeed(
+        "cp ${unitFile} /etc/systemd-rw/system/example.service"
+    )
+    machine.succeed("systemctl start example.service")
+    machine.succeed("systemctl status example.service | grep 'Active: active'")
+  '';
+})
diff --git a/nixos/tests/systemd.nix b/nixos/tests/systemd.nix
new file mode 100644
index 00000000000..f86daa5eea9
--- /dev/null
+++ b/nixos/tests/systemd.nix
@@ -0,0 +1,196 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "systemd";
+
+  machine = { lib, ... }: {
+    imports = [ common/user-account.nix common/x11.nix ];
+
+    virtualisation.emptyDiskImages = [ 512 512 ];
+
+    environment.systemPackages = [ pkgs.cryptsetup ];
+
+    virtualisation.fileSystems = {
+      "/test-x-initrd-mount" = {
+        device = "/dev/vdb";
+        fsType = "ext2";
+        autoFormat = true;
+        noCheck = true;
+        options = [ "x-initrd.mount" ];
+      };
+    };
+
+    systemd.extraConfig = "DefaultEnvironment=\"XXX_SYSTEM=foo\"";
+    systemd.user.extraConfig = "DefaultEnvironment=\"XXX_USER=bar\"";
+    services.journald.extraConfig = "Storage=volatile";
+    test-support.displayManager.auto.user = "alice";
+
+    systemd.shutdown.test = pkgs.writeScript "test.shutdown" ''
+      #!${pkgs.runtimeShell}
+      PATH=${lib.makeBinPath (with pkgs; [ util-linux coreutils ])}
+      mount -t 9p shared -o trans=virtio,version=9p2000.L /tmp/shared
+      touch /tmp/shared/shutdown-test
+      umount /tmp/shared
+    '';
+
+    systemd.services.oncalendar-test = {
+      description = "calendar test";
+      # Japan does not have DST which makes the test a little bit simpler
+      startAt = "Wed 10:00 Asia/Tokyo";
+      script = "true";
+    };
+
+    systemd.services.testservice1 = {
+      description = "Test Service 1";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig.Type = "oneshot";
+      script = ''
+        if [ "$XXX_SYSTEM" = foo ]; then
+          touch /system_conf_read
+        fi
+      '';
+    };
+
+    systemd.user.services.testservice2 = {
+      description = "Test Service 2";
+      wantedBy = [ "default.target" ];
+      serviceConfig.Type = "oneshot";
+      script = ''
+        if [ "$XXX_USER" = bar ]; then
+          touch "$HOME/user_conf_read"
+        fi
+      '';
+    };
+
+    systemd.watchdog = {
+      device = "/dev/watchdog";
+      runtimeTime = "30s";
+      rebootTime = "10min";
+      kexecTime = "5min";
+    };
+  };
+
+  testScript = ''
+    import re
+    import subprocess
+
+    machine.wait_for_x()
+    # wait for user services
+    machine.wait_for_unit("default.target", "alice")
+
+    # Regression test for https://github.com/NixOS/nixpkgs/issues/105049
+    with subtest("systemd reads timezone database in /etc/zoneinfo"):
+        timer = machine.succeed("TZ=UTC systemctl show --property=TimersCalendar oncalendar-test.timer")
+        assert re.search("next_elapse=Wed ....-..-.. 01:00:00 UTC", timer), f"got {timer.strip()}"
+
+    # Regression test for https://github.com/NixOS/nixpkgs/issues/35415
+    with subtest("configuration files are recognized by systemd"):
+        machine.succeed("test -e /system_conf_read")
+        machine.succeed("test -e /home/alice/user_conf_read")
+        machine.succeed("test -z $(ls -1 /var/log/journal)")
+
+    # Regression test for https://github.com/NixOS/nixpkgs/issues/50273
+    with subtest("DynamicUser actually allocates a user"):
+        assert "iamatest" in machine.succeed(
+            "systemd-run --pty --property=Type=oneshot --property=DynamicUser=yes --property=User=iamatest whoami"
+        )
+
+    with subtest("regression test for https://bugs.freedesktop.org/show_bug.cgi?id=77507"):
+        retcode, output = machine.execute("systemctl status testservice1.service")
+        assert retcode in [0, 3]  # https://bugs.freedesktop.org/show_bug.cgi?id=77507
+
+    # Regression test for https://github.com/NixOS/nixpkgs/issues/35268
+    with subtest("file system with x-initrd.mount is not unmounted"):
+        machine.succeed("mountpoint -q /test-x-initrd-mount")
+        machine.shutdown()
+
+        subprocess.check_call(
+            [
+                "qemu-img",
+                "convert",
+                "-O",
+                "raw",
+                "vm-state-machine/empty0.qcow2",
+                "x-initrd-mount.raw",
+            ]
+        )
+        extinfo = subprocess.check_output(
+            [
+                "${pkgs.e2fsprogs}/bin/dumpe2fs",
+                "x-initrd-mount.raw",
+            ]
+        ).decode("utf-8")
+        assert (
+            re.search(r"^Filesystem state: *clean$", extinfo, re.MULTILINE) is not None
+        ), ("File system was not cleanly unmounted: " + extinfo)
+
+    # Regression test for https://github.com/NixOS/nixpkgs/pull/91232
+    with subtest("setting transient hostnames works"):
+        machine.succeed("hostnamectl set-hostname --transient machine-transient")
+        machine.fail("hostnamectl set-hostname machine-all")
+
+    with subtest("systemd-shutdown works"):
+        machine.shutdown()
+        machine.wait_for_unit("multi-user.target")
+        machine.succeed("test -e /tmp/shared/shutdown-test")
+
+    # Test settings from /etc/sysctl.d/50-default.conf are applied
+    with subtest("systemd sysctl settings are applied"):
+        machine.wait_for_unit("multi-user.target")
+        assert "fq_codel" in machine.succeed("sysctl net.core.default_qdisc")
+
+    # Test systemd is configured to manage a watchdog
+    with subtest("systemd manages hardware watchdog"):
+        machine.wait_for_unit("multi-user.target")
+
+        # It seems that the device's path doesn't appear in 'systemctl show' so
+        # check it separately.
+        assert "WatchdogDevice=/dev/watchdog" in machine.succeed(
+            "cat /etc/systemd/system.conf"
+        )
+
+        output = machine.succeed("systemctl show | grep Watchdog")
+        # assert "RuntimeWatchdogUSec=30s" in output
+        # for some reason RuntimeWatchdogUSec, doesn't seem to be updated in here.
+        assert "RebootWatchdogUSec=10min" in output
+        assert "KExecWatchdogUSec=5min" in output
+
+    # Test systemd cryptsetup support
+    with subtest("systemd successfully reads /etc/crypttab and unlocks volumes"):
+        # create a luks volume and put a filesystem on it
+        machine.succeed(
+            "echo -n supersecret | cryptsetup luksFormat -q /dev/vdc -",
+            "echo -n supersecret | cryptsetup luksOpen --key-file - /dev/vdc foo",
+            "mkfs.ext3 /dev/mapper/foo",
+        )
+
+        # create a keyfile and /etc/crypttab
+        machine.succeed("echo -n supersecret > /var/lib/luks-keyfile")
+        machine.succeed("chmod 600 /var/lib/luks-keyfile")
+        machine.succeed("echo 'luks1 /dev/vdc /var/lib/luks-keyfile luks' > /etc/crypttab")
+
+        # after a reboot, systemd should unlock the volume and we should be able to mount it
+        machine.shutdown()
+        machine.succeed("systemctl status systemd-cryptsetup@luks1.service")
+        machine.succeed("mkdir -p /tmp/luks1")
+        machine.succeed("mount /dev/mapper/luks1 /tmp/luks1")
+
+    # Do some IP traffic
+    output_ping = machine.succeed(
+        "systemd-run --wait -- /run/wrappers/bin/ping -c 1 127.0.0.1 2>&1"
+    )
+
+    with subtest("systemd reports accounting data on system.slice"):
+        output = machine.succeed("systemctl status system.slice")
+        assert "CPU:" in output
+        assert "Memory:" in output
+
+        assert "IP:" in output
+        assert "0B in, 0B out" not in output
+
+        assert "IO:" in output
+        assert "0B read, 0B written" not in output
+
+    with subtest("systemd per-unit accounting works"):
+        assert "IP traffic received: 84B" in output_ping
+        assert "IP traffic sent: 84B" in output_ping
+  '';
+})
diff --git a/nixos/tests/taskserver.nix b/nixos/tests/taskserver.nix
new file mode 100644
index 00000000000..f34782c7059
--- /dev/null
+++ b/nixos/tests/taskserver.nix
@@ -0,0 +1,282 @@
+import ./make-test-python.nix ({ pkgs, ... }: let
+  snakeOil = pkgs.runCommand "snakeoil-certs" {
+    outputs = [ "out" "cacert" "cert" "key" "crl" ];
+    buildInputs = [ pkgs.gnutls.bin ];
+    caTemplate = pkgs.writeText "snakeoil-ca.template" ''
+      cn = server
+      expiration_days = -1
+      cert_signing_key
+      ca
+    '';
+    certTemplate = pkgs.writeText "snakeoil-cert.template" ''
+      cn = server
+      expiration_days = -1
+      tls_www_server
+      encryption_key
+      signing_key
+    '';
+    crlTemplate = pkgs.writeText "snakeoil-crl.template" ''
+      expiration_days = -1
+    '';
+    userCertTemplate = pkgs.writeText "snakeoil-user-cert.template" ''
+      organization = snakeoil
+      cn = server
+      expiration_days = -1
+      tls_www_client
+      encryption_key
+      signing_key
+    '';
+  } ''
+    certtool -p --bits 4096 --outfile ca.key
+    certtool -s --template "$caTemplate" --load-privkey ca.key \
+                --outfile "$cacert"
+    certtool -p --bits 4096 --outfile "$key"
+    certtool -c --template "$certTemplate" \
+                --load-ca-privkey ca.key \
+                --load-ca-certificate "$cacert" \
+                --load-privkey "$key" \
+                --outfile "$cert"
+    certtool --generate-crl --template "$crlTemplate" \
+                            --load-ca-privkey ca.key \
+                            --load-ca-certificate "$cacert" \
+                            --outfile "$crl"
+
+    mkdir "$out"
+
+    # Stripping key information before the actual PEM-encoded values is solely
+    # to make test output a bit less verbose when copying the client key to the
+    # actual client.
+    certtool -p --bits 4096 | sed -n \
+      -e '/^----* *BEGIN/,/^----* *END/p' > "$out/alice.key"
+
+    certtool -c --template "$userCertTemplate" \
+                --load-privkey "$out/alice.key" \
+                --load-ca-privkey ca.key \
+                --load-ca-certificate "$cacert" \
+                --outfile "$out/alice.cert"
+  '';
+
+in {
+  name = "taskserver";
+
+  nodes = rec {
+    server = {
+      services.taskserver.enable = true;
+      services.taskserver.listenHost = "::";
+      services.taskserver.fqdn = "server";
+      services.taskserver.organisations = {
+        testOrganisation.users = [ "alice" "foo" ];
+        anotherOrganisation.users = [ "bob" ];
+      };
+    };
+
+    # New generation of the server with manual config
+    newServer = { lib, nodes, ... }: {
+      imports = [ server ];
+      services.taskserver.pki.manual = {
+        ca.cert = snakeOil.cacert;
+        server.cert = snakeOil.cert;
+        server.key = snakeOil.key;
+        server.crl = snakeOil.crl;
+      };
+      # This is to avoid assigning a different network address to the new
+      # generation.
+      networking = lib.mapAttrs (lib.const lib.mkForce) {
+        interfaces.eth1.ipv4 = nodes.server.config.networking.interfaces.eth1.ipv4;
+        inherit (nodes.server.config.networking)
+          hostName primaryIPAddress extraHosts;
+      };
+    };
+
+    client1 = { pkgs, ... }: {
+      environment.systemPackages = [ pkgs.taskwarrior pkgs.gnutls ];
+      users.users.alice.isNormalUser = true;
+      users.users.bob.isNormalUser = true;
+      users.users.foo.isNormalUser = true;
+      users.users.bar.isNormalUser = true;
+    };
+
+    client2 = client1;
+  };
+
+  testScript = { nodes, ... }: let
+    cfg = nodes.server.config.services.taskserver;
+    portStr = toString cfg.listenPort;
+    newServerSystem = nodes.newServer.config.system.build.toplevel;
+    switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test";
+  in ''
+    from shlex import quote
+
+
+    def su(user, cmd):
+        return f"su - {user} -c {quote(cmd)}"
+
+
+    def no_extra_init(client, org, user):
+        pass
+
+
+    def setup_clients_for(org, user, extra_init=no_extra_init):
+        for client in [client1, client2]:
+            with client.nested(f"initialize client for user {user}"):
+                client.succeed(
+                    su(user, f"rm -rf /home/{user}/.task"),
+                    su(user, "task rc.confirmation=no config confirmation no"),
+                )
+
+                exportinfo = server.succeed(f"nixos-taskserver user export {org} {user}")
+
+                with client.nested("importing taskwarrior configuration"):
+                    client.succeed(su(user, f"eval {quote(exportinfo)} >&2"))
+
+                extra_init(client, org, user)
+
+                client.succeed(su(user, "task config taskd.server server:${portStr} >&2"))
+
+                client.succeed(su(user, "task sync init >&2"))
+
+
+    def restart_server():
+        server.systemctl("restart taskserver.service")
+        server.wait_for_open_port(${portStr})
+
+
+    def re_add_imperative_user():
+        with server.nested("(re-)add imperative user bar"):
+            server.execute("nixos-taskserver org remove imperativeOrg")
+            server.succeed(
+                "nixos-taskserver org add imperativeOrg",
+                "nixos-taskserver user add imperativeOrg bar",
+            )
+            setup_clients_for("imperativeOrg", "bar")
+
+
+    def test_sync(user):
+        with subtest(f"sync for user {user}"):
+            client1.succeed(su(user, "task add foo >&2"))
+            client1.succeed(su(user, "task sync >&2"))
+            client2.fail(su(user, "task list >&2"))
+            client2.succeed(su(user, "task sync >&2"))
+            client2.succeed(su(user, "task list >&2"))
+
+
+    def check_client_cert(user):
+        # debug level 3 is a workaround for gnutls issue https://gitlab.com/gnutls/gnutls/-/issues/1040
+        cmd = (
+            f"gnutls-cli -d 3"
+            f" --x509cafile=/home/{user}/.task/keys/ca.cert"
+            f" --x509keyfile=/home/{user}/.task/keys/private.key"
+            f" --x509certfile=/home/{user}/.task/keys/public.cert"
+            f" --port=${portStr} server < /dev/null"
+        )
+        return su(user, cmd)
+
+
+    # Explicitly start the VMs so that we don't accidentally start newServer
+    server.start()
+    client1.start()
+    client2.start()
+
+    server.wait_for_unit("taskserver.service")
+
+    server.succeed(
+        "nixos-taskserver user list testOrganisation | grep -qxF alice",
+        "nixos-taskserver user list testOrganisation | grep -qxF foo",
+        "nixos-taskserver user list anotherOrganisation | grep -qxF bob",
+    )
+
+    server.wait_for_open_port(${portStr})
+
+    client1.wait_for_unit("multi-user.target")
+    client2.wait_for_unit("multi-user.target")
+
+    setup_clients_for("testOrganisation", "alice")
+    setup_clients_for("testOrganisation", "foo")
+    setup_clients_for("anotherOrganisation", "bob")
+
+    for user in ["alice", "bob", "foo"]:
+        test_sync(user)
+
+    server.fail("nixos-taskserver user add imperativeOrg bar")
+    re_add_imperative_user()
+
+    test_sync("bar")
+
+    with subtest("checking certificate revocation of user bar"):
+        client1.succeed(check_client_cert("bar"))
+
+        server.succeed("nixos-taskserver user remove imperativeOrg bar")
+        restart_server()
+
+        client1.fail(check_client_cert("bar"))
+
+        client1.succeed(su("bar", "task add destroy everything >&2"))
+        client1.fail(su("bar", "task sync >&2"))
+
+    re_add_imperative_user()
+
+    with subtest("checking certificate revocation of org imperativeOrg"):
+        client1.succeed(check_client_cert("bar"))
+
+        server.succeed("nixos-taskserver org remove imperativeOrg")
+        restart_server()
+
+        client1.fail(check_client_cert("bar"))
+
+        client1.succeed(su("bar", "task add destroy even more >&2"))
+        client1.fail(su("bar", "task sync >&2"))
+
+    re_add_imperative_user()
+
+    with subtest("check whether declarative config overrides user bar"):
+        restart_server()
+        test_sync("bar")
+
+
+    def init_manual_config(client, org, user):
+        cfgpath = f"/home/{user}/.task"
+
+        client.copy_from_host(
+            "${snakeOil.cacert}",
+            f"{cfgpath}/ca.cert",
+        )
+        for file in ["alice.key", "alice.cert"]:
+            client.copy_from_host(
+                f"${snakeOil}/{file}",
+                f"{cfgpath}/{file}",
+            )
+
+        for file in [f"{user}.key", f"{user}.cert"]:
+            client.copy_from_host(
+                f"${snakeOil}/{file}",
+                f"{cfgpath}/{file}",
+            )
+
+        client.succeed(
+            su("alice", f"task config taskd.ca {cfgpath}/ca.cert"),
+            su("alice", f"task config taskd.key {cfgpath}/{user}.key"),
+            su(user, f"task config taskd.certificate {cfgpath}/{user}.cert"),
+        )
+
+
+    with subtest("check manual configuration"):
+        # Remove the keys from automatic CA creation, to make sure the new
+        # generation doesn't use keys from before.
+        server.succeed("rm -rf ${cfg.dataDir}/keys/* >&2")
+
+        server.succeed(
+            "${switchToNewServer} >&2"
+        )
+        server.wait_for_unit("taskserver.service")
+        server.wait_for_open_port(${portStr})
+
+        server.succeed(
+            "nixos-taskserver org add manualOrg",
+            "nixos-taskserver user add manualOrg alice",
+        )
+
+        setup_clients_for("manualOrg", "alice", init_manual_config)
+
+        test_sync("alice")
+  '';
+})
diff --git a/nixos/tests/teeworlds.nix b/nixos/tests/teeworlds.nix
new file mode 100644
index 00000000000..ac2c996955c
--- /dev/null
+++ b/nixos/tests/teeworlds.nix
@@ -0,0 +1,55 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+  client =
+    { pkgs, ... }:
+
+    { imports = [ ./common/x11.nix ];
+      environment.systemPackages = [ pkgs.teeworlds ];
+    };
+
+in {
+  name = "teeworlds";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ hax404 ];
+  };
+
+  nodes =
+    { server =
+      { services.teeworlds = {
+          enable = true;
+          openPorts = true;
+        };
+      };
+
+      client1 = client;
+      client2 = client;
+    };
+
+    testScript =
+    ''
+      start_all()
+
+      server.wait_for_unit("teeworlds.service")
+      server.wait_until_succeeds("ss --numeric --udp --listening | grep -q 8303")
+
+      client1.wait_for_x()
+      client2.wait_for_x()
+
+      client1.execute("teeworlds 'player_name Alice;connect server' >&2 &")
+      server.wait_until_succeeds(
+          'journalctl -u teeworlds -e | grep --extended-regexp -q "team_join player=\'[0-9]:Alice"'
+      )
+
+      client2.execute("teeworlds 'player_name Bob;connect server' >&2 &")
+      server.wait_until_succeeds(
+          'journalctl -u teeworlds -e | grep --extended-regexp -q "team_join player=\'[0-9]:Bob"'
+      )
+
+      server.sleep(10)  # wait for a while to get a nice screenshot
+
+      client1.screenshot("screen_client1")
+      client2.screenshot("screen_client2")
+    '';
+
+})
diff --git a/nixos/tests/telegraf.nix b/nixos/tests/telegraf.nix
new file mode 100644
index 00000000000..d99680ce2c3
--- /dev/null
+++ b/nixos/tests/telegraf.nix
@@ -0,0 +1,33 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "telegraf";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mic92 ];
+  };
+
+  machine = { ... }: {
+    services.telegraf.enable = true;
+    services.telegraf.environmentFiles = [(pkgs.writeText "secrets" ''
+      SECRET=example
+    '')];
+    services.telegraf.extraConfig = {
+      agent.interval = "1s";
+      agent.flush_interval = "1s";
+      inputs.exec = {
+        commands = [
+          "${pkgs.runtimeShell} -c 'echo $SECRET,tag=a i=42i'"
+        ];
+        timeout = "5s";
+        data_format = "influx";
+      };
+      outputs.file.files = ["/tmp/metrics.out"];
+      outputs.file.data_format = "influx";
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("telegraf.service")
+    machine.wait_until_succeeds("grep -q example /tmp/metrics.out")
+  '';
+})
diff --git a/nixos/tests/teleport.nix b/nixos/tests/teleport.nix
new file mode 100644
index 00000000000..15b16e44409
--- /dev/null
+++ b/nixos/tests/teleport.nix
@@ -0,0 +1,99 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+
+let
+  minimal = { config, ... }: {
+    services.teleport.enable = true;
+  };
+
+  client = { config, ... }: {
+    services.teleport = {
+      enable = true;
+      settings = {
+        teleport = {
+          nodename = "client";
+          advertise_ip = "192.168.1.20";
+          auth_token = "8d1957b2-2ded-40e6-8297-d48156a898a9";
+          auth_servers = [ "192.168.1.10:3025" ];
+          log.severity = "DEBUG";
+        };
+        ssh_service = {
+          enabled = true;
+          labels = {
+            role = "client";
+          };
+        };
+        proxy_service.enabled = false;
+        auth_service.enabled = false;
+      };
+    };
+    networking.interfaces.eth1.ipv4.addresses = [{
+      address = "192.168.1.20";
+      prefixLength = 24;
+    }];
+  };
+
+  server = { config, ... }: {
+    services.teleport = {
+      enable = true;
+      settings = {
+        teleport = {
+          nodename = "server";
+          advertise_ip = "192.168.1.10";
+        };
+        ssh_service.enabled = true;
+        proxy_service.enabled = true;
+        auth_service = {
+          enabled = true;
+          tokens = [ "node:8d1957b2-2ded-40e6-8297-d48156a898a9" ];
+        };
+      };
+      diag.enable = true;
+      insecure.enable = true;
+    };
+    networking = {
+      firewall.allowedTCPPorts = [ 3025 ];
+      interfaces.eth1.ipv4.addresses = [{
+        address = "192.168.1.10";
+        prefixLength = 24;
+      }];
+    };
+  };
+in
+{
+  minimal = makeTest {
+    # minimal setup should always work
+    name = "teleport-minimal-setup";
+    meta.maintainers = with pkgs.lib.maintainers; [ ymatsiuk ];
+    nodes = { inherit minimal; };
+
+    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; };
+
+    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.'")
+    '';
+  };
+}
diff --git a/nixos/tests/terminal-emulators.nix b/nixos/tests/terminal-emulators.nix
new file mode 100644
index 00000000000..60161b80b96
--- /dev/null
+++ b/nixos/tests/terminal-emulators.nix
@@ -0,0 +1,207 @@
+# Terminal emulators all present a pretty similar interface.
+# That gives us an opportunity to easily test their basic functionality with a single codebase.
+#
+# There are two tests run on each terminal emulator
+# - can it successfully execute a command passed on the cmdline?
+# - can it successfully display a colour?
+# the latter is used as a proxy for "can it display text?", without going through all the intricacies of OCR.
+#
+# 256-colour terminal mode is used to display the test colour, since it has a universally-applicable palette (unlike 8- and 16- colour, where the colours are implementation-defined), and it is widely supported (unlike 24-bit colour).
+#
+# Future work:
+# - Wayland support (both for testing the existing terminals, and for testing wayland-only terminals like foot and havoc)
+# - Test keyboard input? (skipped for now, to eliminate the possibility of race conditions and focus issues)
+
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let tests = {
+      alacritty.pkg = p: p.alacritty;
+
+      contour.pkg = p: p.contour;
+      contour.cmd = "contour $command";
+
+      cool-retro-term.pkg = p: p.cool-retro-term;
+      cool-retro-term.colourTest = false; # broken by gloss effect
+
+      ctx.pkg = p: p.ctx;
+      ctx.pinkValue = "#FE0065";
+
+      darktile.pkg = p: p.darktile;
+
+      eterm.pkg = p: p.eterm;
+      eterm.executable = "Eterm";
+      eterm.pinkValue = "#D40055";
+
+      germinal.pkg = p: p.germinal;
+
+      gnome-terminal.pkg = p: p.gnome.gnome-terminal;
+
+      guake.pkg = p: p.guake;
+      guake.cmd = "SHELL=$command guake --show";
+      guake.kill = true;
+
+      hyper.pkg = p: p.hyper;
+
+      kermit.pkg = p: p.kermit-terminal;
+
+      kgx.pkg = p: p.kgx;
+      kgx.cmd = "kgx -e $command";
+      kgx.kill = true;
+
+      kitty.pkg = p: p.kitty;
+      kitty.cmd = "kitty $command";
+
+      konsole.pkg = p: p.plasma5Packages.konsole;
+
+      lxterminal.pkg = p: p.lxterminal;
+
+      mate-terminal.pkg = p: p.mate.mate-terminal;
+      mate-terminal.cmd = "SHELL=$command mate-terminal --disable-factory"; # factory mode uses dbus, and we don't have a proper dbus session set up
+
+      mlterm.pkg = p: p.mlterm;
+
+      mrxvt.pkg = p: p.mrxvt;
+
+      qterminal.pkg = p: p.lxqt.qterminal;
+      qterminal.kill = true;
+
+      roxterm.pkg = p: p.roxterm;
+      roxterm.cmd = "roxterm -e $command";
+
+      sakura.pkg = p: p.sakura;
+
+      st.pkg = p: p.st;
+      st.kill = true;
+
+      stupidterm.pkg = p: p.stupidterm;
+      stupidterm.cmd = "stupidterm -- $command";
+
+      terminator.pkg = p: p.terminator;
+      terminator.cmd = "terminator -e $command";
+
+      terminology.pkg = p: p.enlightenment.terminology;
+      terminology.cmd = "SHELL=$command terminology --no-wizard=true";
+      terminology.colourTest = false; # broken by gloss effect
+
+      termite.pkg = p: p.termite;
+
+      termonad.pkg = p: p.termonad;
+
+      tilda.pkg = p: p.tilda;
+
+      tilix.pkg = p: p.tilix;
+      tilix.cmd = "tilix -e $command";
+
+      urxvt.pkg = p: p.rxvt-unicode;
+
+      wayst.pkg = p: p.wayst;
+      wayst.pinkValue = "#FF0066";
+
+      wezterm.pkg = p: p.wezterm;
+
+      xfce4-terminal.pkg = p: p.xfce.xfce4-terminal;
+
+      xterm.pkg = p: p.xterm;
+    };
+in mapAttrs (name: { pkg, executable ? name, cmd ? "SHELL=$command ${executable}", colourTest ? true, pinkValue ? "#FF0087", kill ? false }: makeTest
+{
+  name = "terminal-emulator-${name}";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ jjjollyjim ];
+  };
+
+  machine = { pkgsInner, ... }:
+
+  {
+    imports = [ ./common/x11.nix ./common/user-account.nix ];
+
+    # Hyper (and any other electron-based terminals) won't run as root
+    test-support.displayManager.auto.user = "alice";
+
+    environment.systemPackages = [
+      (pkg pkgs)
+      (pkgs.writeShellScriptBin "report-success" ''
+        echo 1 > /tmp/term-ran-successfully
+        ${optionalString kill "pkill ${executable}"}
+      '')
+      (pkgs.writeShellScriptBin "display-colour" ''
+        # A 256-colour background colour code for pink, then spaces.
+        #
+        # Background is used rather than foreground to minimize the effect of anti-aliasing.
+        #
+        # Keep adding more in case the window is partially offscreen to the left or requires
+        # a change to correctly redraw after initialising the window (as with ctx).
+
+        while :
+        do
+            echo -ne "\e[48;5;198m                   "
+            sleep 0.5
+        done
+        sleep infinity
+      '')
+      (pkgs.writeShellScriptBin "run-in-this-term" "sudo -u alice run-in-this-term-wrapped $1")
+
+      (pkgs.writeShellScriptBin "run-in-this-term-wrapped" "command=\"$(which \"$1\")\"; ${cmd}")
+    ];
+
+    # Helpful reminder to add this test to passthru.tests
+    warnings = if !((pkg pkgs) ? "passthru" && (pkg pkgs).passthru ? "tests") then [ "The package for ${name} doesn't have a passthru.tests" ] else [ ];
+  };
+
+  # We need imagemagick, though not tesseract
+  enableOCR = true;
+
+  testScript = { nodes, ... }: let
+  in ''
+    with subtest("wait for x"):
+        start_all()
+        machine.wait_for_x()
+
+    with subtest("have the terminal run a command"):
+        # We run this command synchronously, so we can be certain the exit codes are happy
+        machine.${if kill then "execute" else "succeed"}("run-in-this-term report-success")
+        machine.wait_for_file("/tmp/term-ran-successfully")
+    ${optionalString colourTest ''
+
+    import tempfile
+    import subprocess
+
+
+    def check_for_pink(final=False) -> bool:
+        with tempfile.NamedTemporaryFile() as tmpin:
+            machine.send_monitor_command("screendump {}".format(tmpin.name))
+
+            cmd = 'convert {} -define histogram:unique-colors=true -format "%c" histogram:info:'.format(
+                tmpin.name
+            )
+            ret = subprocess.run(cmd, shell=True, capture_output=True)
+            if ret.returncode != 0:
+                raise Exception(
+                    "image analysis failed with exit code {}".format(ret.returncode)
+                )
+
+            text = ret.stdout.decode("utf-8")
+            return "${pinkValue}" in text
+
+
+    with subtest("ensuring no pink is present without the terminal"):
+        assert (
+            check_for_pink() == False
+        ), "Pink was present on the screen before we even launched a terminal!"
+
+    with subtest("have the terminal display a colour"):
+        # We run this command in the background
+        machine.shell.send(b"(run-in-this-term display-colour |& systemd-cat -t terminal) &\n")
+
+        with machine.nested("Waiting for the screen to have pink on it:"):
+            retry(check_for_pink)
+  ''}'';
+}
+
+  ) tests
diff --git a/nixos/tests/thelounge.nix b/nixos/tests/thelounge.nix
new file mode 100644
index 00000000000..e9b85685bf2
--- /dev/null
+++ b/nixos/tests/thelounge.nix
@@ -0,0 +1,29 @@
+import ./make-test-python.nix {
+  nodes = {
+    private = { config, pkgs, ... }: {
+      services.thelounge = {
+        enable = true;
+        plugins = [ pkgs.theLoungePlugins.themes.solarized ];
+      };
+    };
+
+    public = { config, pkgs, ... }: {
+      services.thelounge = {
+        enable = true;
+        public = true;
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    for machine in machines:
+      machine.wait_for_unit("thelounge.service")
+      machine.wait_for_open_port(9000)
+
+    private.wait_until_succeeds("journalctl -u thelounge.service | grep thelounge-theme-solarized")
+    private.wait_until_succeeds("journalctl -u thelounge.service | grep 'in private mode'")
+    public.wait_until_succeeds("journalctl -u thelounge.service | grep 'in public mode'")
+  '';
+}
diff --git a/nixos/tests/tiddlywiki.nix b/nixos/tests/tiddlywiki.nix
new file mode 100644
index 00000000000..822711b8939
--- /dev/null
+++ b/nixos/tests/tiddlywiki.nix
@@ -0,0 +1,69 @@
+import ./make-test-python.nix ({ ... }: {
+  name = "tiddlywiki";
+  nodes = {
+    default = {
+      services.tiddlywiki.enable = true;
+    };
+    configured = {
+      boot.postBootCommands = ''
+        echo "username,password
+        somelogin,somesecret" > /var/lib/wikiusers.csv
+      '';
+      services.tiddlywiki = {
+        enable = true;
+        listenOptions = {
+          port = 3000;
+          credentials="../wikiusers.csv";
+          readers="(authenticated)";
+        };
+      };
+    };
+  };
+
+  testScript =
+    ''
+      start_all()
+
+      with subtest("by default works without configuration"):
+          default.wait_for_unit("tiddlywiki.service")
+
+      with subtest("by default available on port 8080 without auth"):
+          default.wait_for_unit("tiddlywiki.service")
+          default.wait_for_open_port(8080)
+          # we output to /dev/null here to avoid a python UTF-8 decode error
+          # but the check will still fail if the service doesn't respond
+          default.succeed("curl --fail -o /dev/null 127.0.0.1:8080")
+
+      with subtest("by default creates empty wiki"):
+          default.succeed("test -f /var/lib/tiddlywiki/tiddlywiki.info")
+
+      with subtest("configured on port 3000 with basic auth"):
+          configured.wait_for_unit("tiddlywiki.service")
+          configured.wait_for_open_port(3000)
+          configured.fail("curl --fail -o /dev/null 127.0.0.1:3000")
+          configured.succeed(
+              "curl --fail -o /dev/null 127.0.0.1:3000 --user somelogin:somesecret"
+          )
+
+      with subtest("restart preserves changes"):
+          # given running wiki
+          default.wait_for_unit("tiddlywiki.service")
+          # with some changes
+          default.succeed(
+              'curl --fail --request PUT --header \'X-Requested-With:TiddlyWiki\' \
+              --data \'{ "title": "title", "text": "content" }\' \
+              --url 127.0.0.1:8080/recipes/default/tiddlers/somepage '
+          )
+          default.succeed("sleep 2")
+
+          # when wiki is cycled
+          default.systemctl("restart tiddlywiki.service")
+          default.wait_for_unit("tiddlywiki.service")
+          default.wait_for_open_port(8080)
+
+          # the change is preserved
+          default.succeed(
+              "curl --fail -o /dev/null 127.0.0.1:8080/recipes/default/tiddlers/somepage"
+          )
+    '';
+})
diff --git a/nixos/tests/tigervnc.nix b/nixos/tests/tigervnc.nix
new file mode 100644
index 00000000000..ed575682d93
--- /dev/null
+++ b/nixos/tests/tigervnc.nix
@@ -0,0 +1,53 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+makeTest {
+  name = "tigervnc";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lheckemann ];
+  };
+
+  nodes = {
+    server = { pkgs, ...}: {
+      environment.systemPackages = with pkgs; [
+        tigervnc # for Xvnc
+        xorg.xwininfo
+        imagemagickBig # for display with working label: support
+      ];
+      networking.firewall.allowedTCPPorts = [ 5901 ];
+    };
+
+    client = { pkgs, ... }: {
+      imports = [ ./common/x11.nix ];
+      # for vncviewer
+      environment.systemPackages = [ pkgs.tigervnc ];
+    };
+  };
+
+  enableOCR = true;
+
+  testScript = ''
+    start_all()
+
+    for host in [server, client]:
+        host.succeed("echo foobar | vncpasswd -f > vncpasswd")
+
+    server.succeed("Xvnc -geometry 720x576 :1 -PasswordFile vncpasswd >&2 &")
+    server.wait_until_succeeds("nc -z localhost 5901", timeout=10)
+    server.succeed("DISPLAY=:1 xwininfo -root | grep 720x576")
+    server.execute("DISPLAY=:1 display -size 360x200 -font sans -gravity south label:'HELLO VNC WORLD' >&2 &")
+
+    client.wait_for_x()
+    client.execute("vncviewer server:1 -PasswordFile vncpasswd >&2 &")
+    client.wait_for_window(r"VNC")
+    client.screenshot("screenshot")
+    text = client.get_screen_text()
+    # Displayed text
+    assert 'HELLO VNC WORLD' in text
+    # Client window title
+    assert 'TigerVNC' in text
+  '';
+}
diff --git a/nixos/tests/timezone.nix b/nixos/tests/timezone.nix
new file mode 100644
index 00000000000..7fc9a5058ee
--- /dev/null
+++ b/nixos/tests/timezone.nix
@@ -0,0 +1,50 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "timezone";
+  meta.maintainers = with pkgs.lib.maintainers; [ lheckemann ];
+
+  nodes = {
+    node_eutz = { pkgs, ... }: {
+      time.timeZone = "Europe/Amsterdam";
+    };
+
+    node_nulltz = { pkgs, ... }: {
+      time.timeZone = null;
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+      node_eutz.wait_for_unit("dbus.socket")
+
+      with subtest("static - Ensure timezone change gives the correct result"):
+          node_eutz.fail("timedatectl set-timezone Asia/Tokyo")
+          date_result = node_eutz.succeed('date -d @0 "+%Y-%m-%d %H:%M:%S"')
+          assert date_result == "1970-01-01 01:00:00\n", "Timezone seems to be wrong"
+
+      node_nulltz.wait_for_unit("dbus.socket")
+
+      with subtest("imperative - Ensure timezone defaults to UTC"):
+          date_result = node_nulltz.succeed('date -d @0 "+%Y-%m-%d %H:%M:%S"')
+          print(date_result)
+          assert (
+              date_result == "1970-01-01 00:00:00\n"
+          ), "Timezone seems to be wrong (not UTC)"
+
+      with subtest("imperative - Ensure timezone adjustment produces expected result"):
+          node_nulltz.succeed("timedatectl set-timezone Asia/Tokyo")
+
+          # Adjustment should be taken into account
+          date_result = node_nulltz.succeed('date -d @0 "+%Y-%m-%d %H:%M:%S"')
+          print(date_result)
+          assert date_result == "1970-01-01 09:00:00\n", "Timezone was not adjusted"
+
+      with subtest("imperative - Ensure timezone adjustment persists across reboot"):
+          # Adjustment should persist across a reboot
+          node_nulltz.shutdown()
+          node_nulltz.wait_for_unit("dbus.socket")
+          date_result = node_nulltz.succeed('date -d @0 "+%Y-%m-%d %H:%M:%S"')
+          print(date_result)
+          assert (
+              date_result == "1970-01-01 09:00:00\n"
+          ), "Timezone adjustment was not persisted"
+  '';
+})
diff --git a/nixos/tests/tinc/default.nix b/nixos/tests/tinc/default.nix
new file mode 100644
index 00000000000..31b675ad35c
--- /dev/null
+++ b/nixos/tests/tinc/default.nix
@@ -0,0 +1,139 @@
+import ../make-test-python.nix ({ lib, ... }:
+  let
+    snakeoil-keys = import ./snakeoil-keys.nix;
+
+    hosts = lib.attrNames snakeoil-keys;
+
+    subnetOf = name: config:
+      let
+        subnets = config.services.tinc.networks.myNetwork.hostSettings.${name}.subnets;
+      in
+      (builtins.head subnets).address;
+
+    makeTincHost = name: { subnet, extraConfig ? { } }: lib.mkMerge [
+      {
+        subnets = [{ address = subnet; }];
+        settings = {
+          Ed25519PublicKey = snakeoil-keys.${name}.ed25519Public;
+        };
+        rsaPublicKey = snakeoil-keys.${name}.rsaPublic;
+      }
+      extraConfig
+    ];
+
+    makeTincNode = { config, ... }: name: extraConfig: lib.mkMerge [
+      {
+        services.tinc.networks.myNetwork = {
+          inherit name;
+          rsaPrivateKeyFile =
+            builtins.toFile "rsa.priv" snakeoil-keys.${name}.rsaPrivate;
+          ed25519PrivateKeyFile =
+            builtins.toFile "ed25519.priv" snakeoil-keys.${name}.ed25519Private;
+
+          hostSettings = lib.mapAttrs makeTincHost {
+            static = {
+              subnet = "10.0.0.11";
+              # Only specify the addresses in the node's vlans, Tinc does not
+              # seem to try each one, unlike the documentation suggests...
+              extraConfig.addresses = map
+                (vlan: { address = "192.168.${toString vlan}.11"; port = 655; })
+                config.virtualisation.vlans;
+            };
+            dynamic1 = { subnet = "10.0.0.21"; };
+            dynamic2 = { subnet = "10.0.0.22"; };
+          };
+        };
+
+        networking.useDHCP = false;
+
+        networking.interfaces."tinc.myNetwork" = {
+          virtual = true;
+          virtualType = "tun";
+          ipv4.addresses = [{
+            address = subnetOf name config;
+            prefixLength = 24;
+          }];
+        };
+
+        # Prevents race condition between NixOS service and tinc creating the
+        # interface.
+        # See: https://github.com/NixOS/nixpkgs/issues/27070
+        systemd.services."tinc.myNetwork" = {
+          after = [ "network-addresses-tinc.myNetwork.service" ];
+          requires = [ "network-addresses-tinc.myNetwork.service" ];
+        };
+
+        networking.firewall.allowedTCPPorts = [ 655 ];
+        networking.firewall.allowedUDPPorts = [ 655 ];
+      }
+      extraConfig
+    ];
+
+  in
+  {
+    name = "tinc";
+    meta.maintainers = with lib.maintainers; [ minijackson ];
+
+    nodes = {
+
+      static = { ... } @ args:
+        makeTincNode args "static" {
+          virtualisation.vlans = [ 1 2 ];
+
+          networking.interfaces.eth1.ipv4.addresses = [{
+            address = "192.168.1.11";
+            prefixLength = 24;
+          }];
+
+          networking.interfaces.eth2.ipv4.addresses = [{
+            address = "192.168.2.11";
+            prefixLength = 24;
+          }];
+        };
+
+
+      dynamic1 = { ... } @ args:
+        makeTincNode args "dynamic1" {
+          virtualisation.vlans = [ 1 ];
+        };
+
+      dynamic2 = { ... } @ args:
+        makeTincNode args "dynamic2" {
+          virtualisation.vlans = [ 2 ];
+        };
+
+    };
+
+    testScript = ''
+      start_all()
+
+      static.wait_for_unit("tinc.myNetwork.service")
+      dynamic1.wait_for_unit("tinc.myNetwork.service")
+      dynamic2.wait_for_unit("tinc.myNetwork.service")
+
+      # Static is accessible by the other hosts
+      dynamic1.succeed("ping -c5 192.168.1.11")
+      dynamic2.succeed("ping -c5 192.168.2.11")
+
+      # The other hosts are in separate vlans
+      dynamic1.fail("ping -c5 192.168.2.11")
+      dynamic2.fail("ping -c5 192.168.1.11")
+
+      # Each host can ping themselves through Tinc
+      static.succeed("ping -c5 10.0.0.11")
+      dynamic1.succeed("ping -c5 10.0.0.21")
+      dynamic2.succeed("ping -c5 10.0.0.22")
+
+      # Static is accessible by the other hosts through Tinc
+      dynamic1.succeed("ping -c5 10.0.0.11")
+      dynamic2.succeed("ping -c5 10.0.0.11")
+
+      # Static can access the other hosts through Tinc
+      static.succeed("ping -c5 10.0.0.21")
+      static.succeed("ping -c5 10.0.0.22")
+
+      # The other hosts in separate vlans can access each other through Tinc
+      dynamic1.succeed("ping -c5 10.0.0.22")
+      dynamic2.succeed("ping -c5 10.0.0.21")
+    '';
+  })
diff --git a/nixos/tests/tinc/snakeoil-keys.nix b/nixos/tests/tinc/snakeoil-keys.nix
new file mode 100644
index 00000000000..650e57d61d4
--- /dev/null
+++ b/nixos/tests/tinc/snakeoil-keys.nix
@@ -0,0 +1,157 @@
+{
+  static = {
+    ed25519Private = ''
+      -----BEGIN ED25519 PRIVATE KEY-----
+      IPR+ur5LfVdm6VlR1+FGIkbkL8Enkb9sejBa/JP6tXkg/vHoraIp70srb6jAUFm5
+      3YbCJiBjLW3dy16qM5PovBoWtr5hoqYYA9dFLOys8FBUFFsIGfKhnbk7g25iwxbO
+      -----END ED25519 PRIVATE KEY-----
+    '';
+
+    ed25519Public = "AqV7aeIqKGGQfXxijMLfRAVRBLixnS45G5OoduIc8mD";
+
+    rsaPrivate = ''
+      -----BEGIN RSA PRIVATE KEY-----
+      MIIEpAIBAAKCAQEAxDHl0TIhhT2yH5rT+Q7MLnj+Ir8bbs3uaPqnzcxWzN1EfVP8
+      TWt5fSTrF2Dc78Kyu5ZNALrp7tUj0GZAegp1YeYJ28p3qTwCveywtCwbB4dI987S
+      yJwq95kE9aoyLa+cT99VwSTdb2YowQv2tWj/idxE3oJ+qZjy9tE5mysXm7jmTQDx
+      +U0XmNe6MHjKXc01Ener41u0ykJLeUfdgJ1zEyM2rQGtaHpIXfMT6kmxCaMcAMLg
+      YFpI38/1pQGQtROKdGOaUomx2m058bkMsJhTiBjESiLRDElRGxmMJ732crGJP0GR
+      ChJkaX/CnxHq7R0daZfwoTVHRu6N7WDbFQL5twIDAQABAoIBAQCM/fLTIHyYXRr5
+      vXFhxXGUYBz56W6UdWdEiAU5TwR92vFSQ53IIVlARtyvg0ui/b8mMcAKq0hb+03u
+      gN0LFyL+BKvHCLxvoRGzXTorcJrIET+t3jL6OchjANNgnDvNOytQ9wWQdKaxXLAi
+      8y8LdXZWozXW1d6ikKjiGL+WNCSWIcq83ktSJZcohihptU9Un16FYQzdolSC8RtI
+      XyT7i1ye6hW/wJTJxqZ4taX3EPat85kXS234VGSqg9bb2A1yE+U8Rq37bf8AKldJ
+      NUQB3JyxnkYGJcqvzDmz139+744VWxDRvXDA5vU29LC6f8bGBvwEttD98QW+pgmB
+      1NBU1Uo5AoGBAOzUk6k74h1RarwXaftjh/9Pures0CfNNnrkJApzFCh4bAoHNxq6
+      SSXqLcc/vvX2+YaZ72nn5YTo+JLQP6evM9oUaqRMAxa3nzoNCtF8U2r48UWmoUQE
+      aZCYbD3m7IVWFacCKRVaVTMZMTTicypSnXcbCSIEH8PRs9+L4jkHgql9AoGBANQT
+      TZECVhIaQnyRiKWlUE8G1QKzXIxjmfyirBe+ftlIG2XMXasAtQ4VRxpnorgqUnIH
+      BVrIbvRx21zlqwZbrZvyb1jHWRoyi1cqBPijpYBUm5LbV2jgHPhnfhRVqdD4CDKj
+      NQzIQrNymFaMWAoOQv/DE3g+Txr0fm9Ztu8ZRXZDAoGAHh3SQT0aPfwyhIS9t3gq
+      vS7YYa8aMVWJTgthAessbxERPB06xq1Vy/qBo8rZb9HeXV2J8n/I0iQGKDVPQvWm
+      tF7QSOBZrDPhjbJG4+jZesr5c5ADBfFBs1+OtDh/b11JF5nQu6RnHT5g4YbCemlT
+      GOhZOvgnSfGK3CyfsfzggskCgYEAmpKDK5kPUNxw70hH16v5L9Bj+zbt0qlZ+Ag8
+      9IV1ATuMNJNTBitay6v4iidVM3QtaUzyuytxq5s87qW7FMRHcm2ueH+70ttaMiq/
+      OtZT74g7aDuUpy0KEIemHn4dauENYJMSPIHOE+sHW7WpCZNBhBcUHsUTdSsU6GX0
+      bqr1tO8CgYBpZdR2OoX/rn8nwjmtBOH38aPnCpaAfdI2Eq2Lg6DjksP6TBt53a+R
+      m1lk6Kt37BPPZQ85SBr7ywvDgUzfoD7uSmHujF2JUHPsdrg9nx7pNIGlW6DlS9OU
+      oNXGAJ/6/y6F8uDbToUfrwFq5tKMypEEa32kFtxb9f0XQ5fSgHrBEw==
+      -----END RSA PRIVATE KEY-----
+    '';
+
+    rsaPublic = ''
+      -----BEGIN RSA PUBLIC KEY-----
+      MIIBCgKCAQEAxDHl0TIhhT2yH5rT+Q7MLnj+Ir8bbs3uaPqnzcxWzN1EfVP8TWt5
+      fSTrF2Dc78Kyu5ZNALrp7tUj0GZAegp1YeYJ28p3qTwCveywtCwbB4dI987SyJwq
+      95kE9aoyLa+cT99VwSTdb2YowQv2tWj/idxE3oJ+qZjy9tE5mysXm7jmTQDx+U0X
+      mNe6MHjKXc01Ener41u0ykJLeUfdgJ1zEyM2rQGtaHpIXfMT6kmxCaMcAMLgYFpI
+      38/1pQGQtROKdGOaUomx2m058bkMsJhTiBjESiLRDElRGxmMJ732crGJP0GRChJk
+      aX/CnxHq7R0daZfwoTVHRu6N7WDbFQL5twIDAQAB
+      -----END RSA PUBLIC KEY-----
+    '';
+  };
+
+  dynamic1 = {
+    ed25519Private = ''
+      -----BEGIN ED25519 PRIVATE KEY-----
+      wHNC2IMXfYtL4ehdsCX154HBvlIZYEiTOnXtckWMUtEAiX9fu7peyBkp9q+yOy9c
+      xsNyssLL78lt0GoweCxlu3Sza2oBQAcwb+6tuv7P/bqzcG005uCwquyCz8LVymXA
+      -----END ED25519 PRIVATE KEY-----
+    '';
+
+    ed25519Public = "t0smNaAEAH8mver77+z/m6MnBNdurAsqrswM/Sls5FA";
+
+    rsaPrivate = ''
+      -----BEGIN RSA PRIVATE KEY-----
+      MIIEpAIBAAKCAQEApukYNGFNWvVlmx75LyOE7MEcd/ViV+yEyk+4cIBXYJ3Ouw+/
+      oEuh8ghQfsiUtbUPR6hPYhX2ZV8XGhuU2nAXVQV0sfZ8pdkbHQ6wHUqFcUIQAVvS
+      Wpm2DvZM8jkbCPP64/x5nukPwQ8VoNnb62rWGzbcj7rOeb7ndMK0TpX5Wwv8F297
+      nKTNCEDbK3DLTj3VD+QGnw6AoEt5i44vViAWZBXuHLHWTDC0Nq8GG+9TKODkEwt5
+      4dgN2X9f+WTVAYhZT3SayHLqIFIMQunN89RpWwhHSW+JIRfAfuT1TbP+wA5ptDeI
+      ktCkJwWyv4hK6l800BJ9GW1nbId5LPa58ipaVwIDAQABAoIBAHcw3WgKVAMwWm57
+      n9ZZtwKapInFYYUIEYungj5UaBFGn+pVRLJjUDJWXaUr94YK1e6F8qpIpLufPBAY
+      wiN7CC5exwaOzlRgxUvqwTkpjkFiu6s8tuqb+baVjD0tKnEqSW+lS/R+2hEzhG5p
+      JPLoSB0HAFpjPC8UdJSctcWos3if3mvOGkGCKyTkrwaJgECDfD+lZ+NBIAiYLSps
+      jWLE+XlY1+nfPdLUQ+TRSv3IikJ/CWbvJLl9EE1tKhkY564KytwZrkIdJlc7NyRO
+      HpzhyMzHu1GLsr+OsBZByNNUxEPU+bzkDQluRXUSIUs9zZoBiCQr3o04qGPTEX9n
+      pNU60gECgYEA3Uf+c80eqzjDxv+O0YzC+9x6A+yMrV56siGkKRPMlrSqjX7iE2Yg
+      tUjD25kEvtaFuB3f/7zp3h4O/VLZgXreRtXHvdrfoyyJGHvHIyCGm8sw8CEWsKo4
+      1LgZUzdPJRkXJq1zOgS0r1xsA1UDC4s02Ww2HwNeVWtmLUyCpA+B/ccCgYEAwRk9
+      tbe82eq1a85zZiPVXP2qvDH5+Vz9YiMky8xsBnoxmz2siR+NdvWBLcE2VDIY8MK1
+      9a1dz2a7cAHQBrtWtACFVY4zvr69DumApjbQRClDYpJ42tp2VbzlMcUDIoKudRQV
+      CObhrE4w4yfVizXFyH9+4Tsg5NzVYuGg9fUJ/vECgYEAoRz7KouNqfMhsLF/5hkM
+      Gt9zw4mm/9ALm8kcwn/U9WHD0FQy/Rbd98BsQmaOavi80cqGvqhoyz2tgkqhbUHt
+      tzuOPDCxphgWFcqBupTDDYoLLruYzraRvGfyoIFj0coL7jBZ9kNY31l2l5J9LhmE
+      OE4utbP5Kk6RTagocpWL+x8CgYB48CwcIcWf3kZeDOFtuUeqhB1o3Qwox7rSuhwT
+      oCaQL/vdtNTY1PAu7zhGxdoXBYFlWS3JfxlgCoGedyQo8zAscJ8RpIx4DNIwAsLW
+      V0I9TnKry/zxZR30OOh7MV7zQFGvdjJubtwspJQt0QcHt1f2aRO4UOYbMMxcr9+1
+      7BCkoQKBgQDBEtg1hx9zYGg1WN2TBSvh6NShi9S23r6IZ3Up8vz6Z2rcwB3UuhKi
+      xluI2ZFwM9s+7UOpaGC+hnc1aMHDEguYOPXoIzvebbYAdN4AkrsJ5d0r1GoEe64E
+      UXxrfuv5LeJ/vkUgWof+U3/jGOVvrjzi5y1xOC0r3kiSpMa85s1dhQ==
+      -----END RSA PRIVATE KEY-----
+    '';
+
+    rsaPublic = ''
+      -----BEGIN RSA PUBLIC KEY-----
+      MIIBCgKCAQEApukYNGFNWvVlmx75LyOE7MEcd/ViV+yEyk+4cIBXYJ3Ouw+/oEuh
+      8ghQfsiUtbUPR6hPYhX2ZV8XGhuU2nAXVQV0sfZ8pdkbHQ6wHUqFcUIQAVvSWpm2
+      DvZM8jkbCPP64/x5nukPwQ8VoNnb62rWGzbcj7rOeb7ndMK0TpX5Wwv8F297nKTN
+      CEDbK3DLTj3VD+QGnw6AoEt5i44vViAWZBXuHLHWTDC0Nq8GG+9TKODkEwt54dgN
+      2X9f+WTVAYhZT3SayHLqIFIMQunN89RpWwhHSW+JIRfAfuT1TbP+wA5ptDeIktCk
+      JwWyv4hK6l800BJ9GW1nbId5LPa58ipaVwIDAQAB
+      -----END RSA PUBLIC KEY-----
+    '';
+  };
+
+  dynamic2 = {
+    ed25519Private = ''
+      -----BEGIN ED25519 PRIVATE KEY-----
+      oUx9JdIstZLMj3ZPD8mP3ITsUscCTIXhNF3VKFUVi/ma5uk50/1vrEohfDraiMxj
+      gAWthpkhnFzUbp+YlOHE7/Z3h1a/br2/tk8DoZ5PV6ufoV1MaBlGdu+TZgeZou0t
+      -----END ED25519 PRIVATE KEY-----
+    '';
+
+    ed25519Public = "f2dYt2/2q9fLJ/AaW+Tlu7HaVNjWQpRnr/UGoXGqLdL";
+
+    rsaPrivate = ''
+      -----BEGIN RSA PRIVATE KEY-----
+      MIIEpAIBAAKCAQEAtQfijPX3BwOAs2Y0EuNjcBmsI90uYqNAonrFgTtcVwERIVE6
+      p6alSEakazhByujBg3jI8oPKC8eO0IJ7x/BWcgxqaw8hsPfJZFnRlwEcU5kK4c+j
+      UNS+hJOXp0x97T1edLpSFHDK9bZ2necblHKG5MsI4UsxEa+CZ0yoIybwWCDmYuya
+      PvE7CeNNa+CIOUbtPVoN4p/aBj0vZeerNBBuodNkglKRxj4l9wD9uOx4S9sdK5lu
+      q/rkxlViBoXRAshT+G2d/u/7/WPoiKB3QJcF33z8UfrlsTRnDDqOMSGisTPSv2LK
+      4QLN4hWOGXAYQqZcxTkvvjl62mCDuoy0TM+CKQIDAQABAoIBAFKpMAxXf52nPswr
+      /dkmFVCpmE2kADsv+iJ21tpkpYxgw1aoRZUp5cyz3P3MaVZio4IJ1A/Ql6B7Vb3l
+      5ulr170p6CnMdgDdlAsLbEV8T1foyOxFKHiPPBNDZXsR1WpPnGLGdRY6TqKV12HQ
+      lmpZRTkRcJOXBufhcTUD7r5mWFaUoZ7so6VxR4L4Tzcgv1Rl4S6jgnHOQdO6lj47
+      BaPjpBb+hplJ4wsRm91dQ7JApYq25XZwyxnBwQ2zAwb46wsuFxDPHlSc4wU7qTt6
+      x2omm33Xy2cm8L1XQhrassZzldSnAyaLBh9DC3+vFPLODDxdz5M2kpHujYYctRhv
+      CICMYJUCgYEA7mWVYuw0S8FNjaLx6n9Q1hr9d9vAFDd3NEaegH586xvhYNxf6n+C
+      2zZloVLEsX0UnBU/6ZtLAUfxUIqlvDS2r1VjSYG5SNxM6/vyGl17Niu1jC8nzf7M
+      V1WtDCHhT4ikZCuNkAldtgI7CXVdCVO/fTqVhjk4hDblJo7VsCZSZysCgYEAwmXp
+      TwlDHapDqA8UxClZuxS8k+2hthny3ihRPCuT34yqAz074zYG97ZBKwIa4Lm1vnkc
+      mwU7yR2aK7IYeU4ScfWm1mLjkW5iaNV/sG7iTz/RP4mBAs3KSGmuhhz8sFWcXByU
+      IZyvMJvC+FpgJQJn/Xc8ZmdImvXlZd6k8v4/kfsCgYEA6VzFPB2OH63slb4w42SX
+      o86t2dtiDigxZxnN5GhtLdSP7borpigF10JLf/y+kCOpvhRLCQk8Bdf/z+C41iAf
+      yEhktbrnvfvwzHxHhSmHCAMHZ19trodCTiePCrZLkQhoK6o6nAmfEyDh26NoXE3/
+      v71OSyLOQRZfgDwHz7PjrBsCgYAe0zojpjxWP+FqjLmmQUhROgCNFGlIDuVMBOic
+      uexAznVG/ja42KBSNzwuLa9FYy1Gfr3idvn78g24UA1BbvfNyj4iUJv1O6OvK+uL
+      dom8N0pe4NbsMuWYhel+qqoG7AxXLtDuY4IEGy7XYr1MIQ2MS5PwSQBiUguGE7/k
+      KBy8cQKBgQCyC9R8VWJxQLqJxZGa9Ful01bSuntB5OLRfEjFCCuGiY/3Vj+mCiQL
+      GOfMOi2jrcnSNgUm0uevmiFCq9m7QiPiAcSYKXPWhsz/55jJIGcZy8bwyhZ2s2Mg
+      BGeZgj4RFORidqkt5g/KJz0+Wp6Ks4sLoCvOzkpeXvLzFVyzGkihrw==
+      -----END RSA PRIVATE KEY-----
+    '';
+
+    rsaPublic = ''
+      -----BEGIN RSA PUBLIC KEY-----
+      MIIBCgKCAQEAtQfijPX3BwOAs2Y0EuNjcBmsI90uYqNAonrFgTtcVwERIVE6p6al
+      SEakazhByujBg3jI8oPKC8eO0IJ7x/BWcgxqaw8hsPfJZFnRlwEcU5kK4c+jUNS+
+      hJOXp0x97T1edLpSFHDK9bZ2necblHKG5MsI4UsxEa+CZ0yoIybwWCDmYuyaPvE7
+      CeNNa+CIOUbtPVoN4p/aBj0vZeerNBBuodNkglKRxj4l9wD9uOx4S9sdK5luq/rk
+      xlViBoXRAshT+G2d/u/7/WPoiKB3QJcF33z8UfrlsTRnDDqOMSGisTPSv2LK4QLN
+      4hWOGXAYQqZcxTkvvjl62mCDuoy0TM+CKQIDAQAB
+      -----END RSA PUBLIC KEY-----
+    '';
+  };
+}
diff --git a/nixos/tests/tinydns.nix b/nixos/tests/tinydns.nix
new file mode 100644
index 00000000000..124508bc004
--- /dev/null
+++ b/nixos/tests/tinydns.nix
@@ -0,0 +1,40 @@
+import ./make-test-python.nix ({ lib, ...} : {
+  name = "tinydns";
+  meta = {
+    maintainers = with lib.maintainers; [ basvandijk ];
+  };
+  nodes = {
+    nameserver = { config, lib, ... } : let
+      ip = (lib.head config.networking.interfaces.eth1.ipv4.addresses).address;
+    in {
+      networking.nameservers = [ ip ];
+      services.tinydns = {
+        enable = true;
+        inherit ip;
+        data = ''
+          .foo.bar:${ip}
+          +.bla.foo.bar:1.2.3.4:300
+        '';
+      };
+    };
+  };
+  testScript = ''
+    nameserver.start()
+    nameserver.wait_for_unit("tinydns.service")
+
+    # We query tinydns a few times to trigger the bug:
+    #
+    #   nameserver # [    6.105872] mmap: tinydns (842): VmData 331776 exceed data ulimit 300000. Update limits or use boot option ignore_rlimit_data.
+    #
+    # which was reported in https://github.com/NixOS/nixpkgs/issues/119066.
+    # Without the patch <nixpkgs/pkgs/tools/networking/djbdns/softlimit.patch>
+    # it fails on the 10th iteration.
+    nameserver.succeed(
+        """
+          for i in {1..100}; do
+            host bla.foo.bar 192.168.1.1 | grep '1\.2\.3\.4'
+          done
+        """
+    )
+  '';
+})
diff --git a/nixos/tests/tinywl.nix b/nixos/tests/tinywl.nix
new file mode 100644
index 00000000000..8fb87b53330
--- /dev/null
+++ b/nixos/tests/tinywl.nix
@@ -0,0 +1,57 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+  {
+    name = "tinywl";
+    meta = {
+      maintainers = with lib.maintainers; [ primeos ];
+    };
+
+    machine = { config, ... }: {
+      # Automatically login on tty1 as a normal user:
+      imports = [ ./common/user-account.nix ];
+      services.getty.autologinUser = "alice";
+      security.polkit.enable = true;
+
+      environment = {
+        systemPackages = with pkgs; [ tinywl foot wayland-utils ];
+      };
+
+      # Automatically start TinyWL when logging in on tty1:
+      programs.bash.loginShellInit = ''
+        if [ "$(tty)" = "/dev/tty1" ]; then
+          set -e
+          test ! -e /tmp/tinywl.log # Only start tinywl once
+          readonly TEST_CMD="wayland-info |& tee /tmp/test-wayland.out && touch /tmp/test-wayland-exit-ok; read"
+          readonly FOOT_CMD="foot sh -c '$TEST_CMD'"
+          tinywl -s "$FOOT_CMD" |& tee /tmp/tinywl.log
+          touch /tmp/tinywl-exit-ok
+        fi
+      '';
+
+      # Switch to a different GPU driver (default: -vga std), otherwise TinyWL segfaults:
+      virtualisation.qemu.options = [ "-vga none -device virtio-gpu-pci" ];
+    };
+
+    testScript = { nodes, ... }: ''
+      start_all()
+      machine.wait_for_unit("multi-user.target")
+
+      # Wait for complete startup:
+      machine.wait_until_succeeds("pgrep tinywl")
+      machine.wait_for_file("/run/user/1000/wayland-0")
+      machine.wait_until_succeeds("pgrep foot")
+      machine.wait_for_file("/tmp/test-wayland-exit-ok")
+
+      # Make a screenshot and save the result:
+      machine.screenshot("tinywl_foot")
+      print(machine.succeed("cat /tmp/test-wayland.out"))
+      machine.copy_from_vm("/tmp/test-wayland.out")
+
+      # Terminate cleanly:
+      machine.send_key("alt-esc")
+      machine.wait_until_fails("pgrep foot")
+      machine.wait_until_fails("pgrep tinywl")
+      machine.wait_for_file("/tmp/tinywl-exit-ok")
+      machine.copy_from_vm("/tmp/tinywl.log")
+    '';
+  })
diff --git a/nixos/tests/tomcat.nix b/nixos/tests/tomcat.nix
new file mode 100644
index 00000000000..e383f224e3d
--- /dev/null
+++ b/nixos/tests/tomcat.nix
@@ -0,0 +1,21 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "tomcat";
+
+  machine = { pkgs, ... }: {
+    services.tomcat.enable = true;
+  };
+
+  testScript = ''
+    machine.wait_for_unit("tomcat.service")
+    machine.wait_for_open_port(8080)
+    machine.wait_for_file("/var/tomcat/webapps/examples");
+    machine.succeed(
+        "curl --fail http://localhost:8080/examples/servlets/servlet/HelloWorldExample | grep 'Hello World!'"
+    )
+    machine.succeed(
+        "curl --fail http://localhost:8080/examples/jsp/jsp2/simpletag/hello.jsp | grep 'Hello, world!'"
+    )
+  '';
+})
diff --git a/nixos/tests/tor.nix b/nixos/tests/tor.nix
new file mode 100644
index 00000000000..c061f59226c
--- /dev/null
+++ b/nixos/tests/tor.nix
@@ -0,0 +1,30 @@
+import ./make-test-python.nix ({ lib, ... }: with lib;
+
+rec {
+  name = "tor";
+  meta.maintainers = with maintainers; [ joachifm ];
+
+  common =
+    { ... }:
+    { boot.kernelParams = [ "audit=0" "apparmor=0" "quiet" ];
+      networking.firewall.enable = false;
+      networking.useDHCP = false;
+    };
+
+  nodes.client =
+    { pkgs, ... }:
+    { imports = [ common ];
+      environment.systemPackages = with pkgs; [ netcat ];
+      services.tor.enable = true;
+      services.tor.client.enable = true;
+      services.tor.settings.ControlPort = 9051;
+    };
+
+  testScript = ''
+    client.wait_for_unit("tor.service")
+    client.wait_for_open_port(9051)
+    assert "514 Authentication required." in client.succeed(
+        "echo GETINFO version | nc 127.0.0.1 9051"
+    )
+  '';
+})
diff --git a/nixos/tests/traefik.nix b/nixos/tests/traefik.nix
new file mode 100644
index 00000000000..1d6c0a479ef
--- /dev/null
+++ b/nixos/tests/traefik.nix
@@ -0,0 +1,89 @@
+# Test Traefik as a reverse proxy of a local web service
+# and a Docker container.
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "traefik";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ joko ];
+  };
+
+  nodes = {
+    client = { config, pkgs, ... }: {
+      environment.systemPackages = [ pkgs.curl ];
+    };
+    traefik = { config, pkgs, ... }: {
+      virtualisation.oci-containers.containers.nginx = {
+        extraOptions = [
+          "-l" "traefik.enable=true"
+          "-l" "traefik.http.routers.nginx.entrypoints=web"
+          "-l" "traefik.http.routers.nginx.rule=Host(`nginx.traefik.test`)"
+        ];
+        image = "nginx-container";
+        imageFile = pkgs.dockerTools.examples.nginx;
+      };
+
+      networking.firewall.allowedTCPPorts = [ 80 ];
+
+      services.traefik = {
+        enable = true;
+
+        dynamicConfigOptions = {
+          http.routers.simplehttp = {
+            rule = "Host(`simplehttp.traefik.test`)";
+            entryPoints = [ "web" ];
+            service = "simplehttp";
+          };
+
+          http.services.simplehttp = {
+            loadBalancer.servers = [{
+              url = "http://127.0.0.1:8000";
+            }];
+          };
+        };
+
+        staticConfigOptions = {
+          global = {
+            checkNewVersion = false;
+            sendAnonymousUsage = false;
+          };
+
+          entryPoints.web.address = ":80";
+
+          providers.docker.exposedByDefault = false;
+        };
+      };
+
+      systemd.services.simplehttp = {
+        script = "${pkgs.python3}/bin/python -m http.server 8000";
+        serviceConfig.Type = "simple";
+        wantedBy = [ "multi-user.target" ];
+      };
+
+      users.users.traefik.extraGroups = [ "docker" ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    traefik.wait_for_unit("docker-nginx.service")
+    traefik.wait_until_succeeds("docker ps | grep nginx-container")
+    traefik.wait_for_unit("simplehttp.service")
+    traefik.wait_for_unit("traefik.service")
+    traefik.wait_for_open_port(80)
+    traefik.wait_for_unit("multi-user.target")
+
+    client.wait_for_unit("multi-user.target")
+
+    client.wait_until_succeeds("curl -sSf -H Host:nginx.traefik.test http://traefik/")
+
+    with subtest("Check that a container can be reached via Traefik"):
+        assert "Hello from NGINX" in client.succeed(
+            "curl -sSf -H Host:nginx.traefik.test http://traefik/"
+        )
+
+    with subtest("Check that dynamic configuration works"):
+        assert "Directory listing for " in client.succeed(
+            "curl -sSf -H Host:simplehttp.traefik.test http://traefik/"
+        )
+  '';
+})
diff --git a/nixos/tests/trafficserver.nix b/nixos/tests/trafficserver.nix
new file mode 100644
index 00000000000..983ded4f172
--- /dev/null
+++ b/nixos/tests/trafficserver.nix
@@ -0,0 +1,177 @@
+# verifies:
+#   1. Traffic Server is able to start
+#   2. Traffic Server spawns traffic_crashlog upon startup
+#   3. Traffic Server proxies HTTP requests according to URL remapping rules
+#      in 'services.trafficserver.remap'
+#   4. Traffic Server applies per-map settings specified with the conf_remap
+#      plugin
+#   5. Traffic Server caches HTTP responses
+#   6. Traffic Server processes HTTP PUSH requests
+#   7. Traffic Server can load the healthchecks plugin
+#   8. Traffic Server logs HTTP traffic as configured
+#
+# uses:
+#   - bin/traffic_manager
+#   - bin/traffic_server
+#   - bin/traffic_crashlog
+#   - bin/traffic_cache_tool
+#   - bin/traffic_ctl
+#   - bin/traffic_logcat
+#   - bin/traffic_logstats
+#   - bin/tspush
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "trafficserver";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ midchildan ];
+  };
+
+  nodes = {
+    ats = { pkgs, lib, config, ... }: let
+      user = config.users.users.trafficserver.name;
+      group = config.users.groups.trafficserver.name;
+      healthchecks = pkgs.writeText "healthchecks.conf" ''
+        /status /tmp/ats.status text/plain 200 500
+      '';
+    in {
+      services.trafficserver.enable = true;
+
+      services.trafficserver.records = {
+        proxy.config.http.server_ports = "80 80:ipv6";
+        proxy.config.hostdb.host_file.path = "/etc/hosts";
+        proxy.config.log.max_space_mb_headroom = 0;
+        proxy.config.http.push_method_enabled = 1;
+
+        # check that cache storage is usable before accepting traffic
+        proxy.config.http.wait_for_cache = 2;
+      };
+
+      services.trafficserver.plugins = [
+        { path = "healthchecks.so"; arg = toString healthchecks; }
+        { path = "xdebug.so"; }
+      ];
+
+      services.trafficserver.remap = ''
+        map http://httpbin.test http://httpbin
+        map http://pristine-host-hdr.test http://httpbin \
+          @plugin=conf_remap.so \
+          @pparam=proxy.config.url_remap.pristine_host_hdr=1
+        map http://ats/tspush http://httpbin/cache \
+          @plugin=conf_remap.so \
+          @pparam=proxy.config.http.cache.required_headers=0
+      '';
+
+      services.trafficserver.storage = ''
+        /dev/vdb volume=1
+      '';
+
+      networking.firewall.allowedTCPPorts = [ 80 ];
+      virtualisation.emptyDiskImages = [ 256 ];
+      services.udev.extraRules = ''
+        KERNEL=="vdb", OWNER="${user}", GROUP="${group}"
+      '';
+    };
+
+    httpbin = { pkgs, lib, ... }: let
+      python = pkgs.python3.withPackages
+        (ps: with ps; [ httpbin gunicorn gevent ]);
+    in {
+      systemd.services.httpbin = {
+        enable = true;
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          ExecStart = "${python}/bin/gunicorn -b 0.0.0.0:80 httpbin:app -k gevent";
+        };
+      };
+
+      networking.firewall.allowedTCPPorts = [ 80 ];
+    };
+
+    client = { pkgs, lib, ... }: {
+      environment.systemPackages = with pkgs; [ curl ];
+    };
+  };
+
+  testScript = { nodes, ... }: let
+    sampleFile = pkgs.writeText "sample.txt" ''
+      It's the season of White Album.
+    '';
+  in ''
+    import json
+    import re
+
+    ats.wait_for_unit("trafficserver")
+    ats.wait_for_open_port(80)
+    httpbin.wait_for_unit("httpbin")
+    httpbin.wait_for_open_port(80)
+    client.wait_for_unit("network-online.target")
+
+    with subtest("Traffic Server is running"):
+        out = ats.succeed("traffic_ctl server status")
+        assert out.strip() == "Proxy -- on"
+
+    with subtest("traffic_crashlog is running"):
+        ats.succeed("pgrep -f traffic_crashlog")
+
+    with subtest("basic remapping works"):
+        out = client.succeed("curl -vv -H 'Host: httpbin.test' http://ats/headers")
+        assert json.loads(out)["headers"]["Host"] == "httpbin"
+
+    with subtest("conf_remap plugin works"):
+        out = client.succeed(
+            "curl -vv -H 'Host: pristine-host-hdr.test' http://ats/headers"
+        )
+        assert json.loads(out)["headers"]["Host"] == "pristine-host-hdr.test"
+
+    with subtest("caching works"):
+        out = client.succeed(
+            "curl -vv -D - -H 'Host: httpbin.test' -H 'X-Debug: X-Cache' http://ats/cache/60 -o /dev/null"
+        )
+        assert "X-Cache: miss" in out
+
+        out = client.succeed(
+            "curl -vv -D - -H 'Host: httpbin.test' -H 'X-Debug: X-Cache' http://ats/cache/60 -o /dev/null"
+        )
+        assert "X-Cache: hit-fresh" in out
+
+    with subtest("pushing to cache works"):
+        url = "http://ats/tspush"
+
+        ats.succeed(f"echo {url} > /tmp/urls.txt")
+        out = ats.succeed(
+            f"tspush -f '${sampleFile}' -u {url}"
+        )
+        assert "HTTP/1.0 201 Created" in out, "cache push failed"
+
+        out = ats.succeed(
+            "traffic_cache_tool --spans /etc/trafficserver/storage.config find --input /tmp/urls.txt"
+        )
+        assert "Span: /dev/vdb" in out, "cache not stored on disk"
+
+        out = client.succeed(f"curl {url}").strip()
+        expected = (
+            open("${sampleFile}").read().strip()
+        )
+        assert out == expected, "cache content mismatch"
+
+    with subtest("healthcheck plugin works"):
+        out = client.succeed("curl -vv http://ats/status -o /dev/null -w '%{http_code}'")
+        assert out.strip() == "500"
+
+        ats.succeed("touch /tmp/ats.status")
+
+        out = client.succeed("curl -vv http://ats/status -o /dev/null -w '%{http_code}'")
+        assert out.strip() == "200"
+
+    with subtest("logging works"):
+        access_log_path = "/var/log/trafficserver/squid.blog"
+        ats.wait_for_file(access_log_path)
+
+        out = ats.succeed(f"traffic_logcat {access_log_path}").split("\n")[0]
+        expected = "^\S+ \S+ \S+ TCP_MISS/200 \S+ GET http://httpbin/headers - DIRECT/httpbin application/json$"
+        assert re.fullmatch(expected, out) is not None, "no matching logs"
+
+        out = json.loads(ats.succeed(f"traffic_logstats -jf {access_log_path}"))
+        assert out["total"]["error.total"]["req"] == "0", "unexpected log stat"
+  '';
+})
diff --git a/nixos/tests/transmission.nix b/nixos/tests/transmission.nix
new file mode 100644
index 00000000000..7e2648804de
--- /dev/null
+++ b/nixos/tests/transmission.nix
@@ -0,0 +1,23 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "transmission";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ coconnor ];
+  };
+
+  machine = { ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+
+    networking.firewall.allowedTCPPorts = [ 9091 ];
+
+    security.apparmor.enable = true;
+
+    services.transmission.enable = true;
+  };
+
+  testScript =
+    ''
+      start_all()
+      machine.wait_for_unit("transmission")
+      machine.shutdown()
+    '';
+})
diff --git a/nixos/tests/trezord.nix b/nixos/tests/trezord.nix
new file mode 100644
index 00000000000..fb60cb4aff1
--- /dev/null
+++ b/nixos/tests/trezord.nix
@@ -0,0 +1,19 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "trezord";
+  meta = with pkgs.lib; {
+    maintainers = with maintainers; [ mmahut _1000101 ];
+  };
+  nodes = {
+    machine = { ... }: {
+      services.trezord.enable = true;
+      services.trezord.emulator.enable = true;
+    };
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("trezord.service")
+    machine.wait_for_open_port(21325)
+    machine.wait_until_succeeds("curl -fL http://localhost:21325/status/ | grep Version")
+  '';
+})
diff --git a/nixos/tests/trickster.nix b/nixos/tests/trickster.nix
new file mode 100644
index 00000000000..acb2e735c39
--- /dev/null
+++ b/nixos/tests/trickster.nix
@@ -0,0 +1,37 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "trickster";
+  meta = with pkgs.lib; {
+    maintainers = with maintainers; [ _1000101 ];
+  };
+
+  nodes = {
+    prometheus = { ... }: {
+      services.prometheus.enable = true;
+      networking.firewall.allowedTCPPorts = [ 9090 ];
+    };
+    trickster = { ... }: {
+      services.trickster.enable = true;
+    };
+  };
+
+  testScript = ''
+    start_all()
+    prometheus.wait_for_unit("prometheus.service")
+    prometheus.wait_for_open_port(9090)
+    prometheus.wait_until_succeeds(
+        "curl -fL http://localhost:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
+    )
+    trickster.wait_for_unit("trickster.service")
+    trickster.wait_for_open_port(8082)
+    trickster.wait_for_open_port(9090)
+    trickster.wait_until_succeeds(
+        "curl -fL http://localhost:8082/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
+    )
+    trickster.wait_until_succeeds(
+        "curl -fL http://prometheus:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
+    )
+    trickster.wait_until_succeeds(
+        "curl -fL http://localhost:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
+    )
+  '';
+})
diff --git a/nixos/tests/trilium-server.nix b/nixos/tests/trilium-server.nix
new file mode 100644
index 00000000000..6346575b33d
--- /dev/null
+++ b/nixos/tests/trilium-server.nix
@@ -0,0 +1,53 @@
+import ./make-test-python.nix ({ ... }: {
+  name = "trilium-server";
+  nodes = {
+    default = {
+      services.trilium-server.enable = true;
+    };
+    configured = {
+      services.trilium-server = {
+        enable = true;
+        dataDir = "/data/trilium";
+      };
+    };
+
+    nginx = {
+      services.trilium-server = {
+        enable = true;
+        nginx.enable = true;
+        nginx.hostName = "trilium.example.com";
+      };
+    };
+  };
+
+  testScript =
+    ''
+      start_all()
+
+      with subtest("by default works without configuration"):
+          default.wait_for_unit("trilium-server.service")
+
+      with subtest("by default available on port 8080"):
+          default.wait_for_unit("trilium-server.service")
+          default.wait_for_open_port(8080)
+          # we output to /dev/null here to avoid a python UTF-8 decode error
+          # but the check will still fail if the service doesn't respond
+          default.succeed("curl --fail -o /dev/null 127.0.0.1:8080")
+
+      with subtest("by default creates empty document"):
+          default.wait_for_unit("trilium-server.service")
+          default.succeed("test -f /var/lib/trilium/document.db")
+
+      with subtest("configured with custom data store"):
+          configured.wait_for_unit("trilium-server.service")
+          configured.succeed("test -f /data/trilium/document.db")
+
+      with subtest("nginx with custom host name"):
+          nginx.wait_for_unit("trilium-server.service")
+          nginx.wait_for_unit("nginx.service")
+
+          nginx.succeed(
+              "curl --resolve 'trilium.example.com:80:127.0.0.1' http://trilium.example.com/"
+          )
+    '';
+})
diff --git a/nixos/tests/tsm-client-gui.nix b/nixos/tests/tsm-client-gui.nix
new file mode 100644
index 00000000000..e4bcd344a89
--- /dev/null
+++ b/nixos/tests/tsm-client-gui.nix
@@ -0,0 +1,57 @@
+# The tsm-client GUI first tries to connect to a server.
+# We can't simulate a server, so we just check if
+# it reports the correct connection failure error.
+# After that the test persuades the GUI
+# to show its main application window
+# and verifies some configuration information.
+
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "tsm-client";
+
+  enableOCR = true;
+
+  machine = { pkgs, ... }: {
+    imports = [ ./common/x11.nix ];
+    programs.tsmClient = {
+      enable = true;
+      package = pkgs.tsm-client-withGui;
+      defaultServername = "testserver";
+      servers.testserver = {
+        # 192.0.0.8 is a "dummy address" according to RFC 7600
+        server = "192.0.0.8";
+        node = "SOME-NODE";
+        passwdDir = "/tmp";
+      };
+    };
+  };
+
+  testScript = ''
+    machine.succeed("which dsmj")  # fail early if this is missing
+    machine.wait_for_x()
+    machine.execute("DSM_LOG=/tmp dsmj -optfile=/dev/null >&2 &")
+
+    # does it report the "TCP/IP connection failure" error code?
+    machine.wait_for_window("IBM Spectrum Protect")
+    machine.wait_for_text("ANS2610S")
+    machine.send_key("esc")
+
+    # it asks to continue to restore a local backupset now;
+    # "yes" (return) leads to the main application window
+    machine.wait_for_text("backupset")
+    machine.send_key("ret")
+
+    # main window: navigate to "Connection Information"
+    machine.wait_for_text("Welcome")
+    machine.send_key("alt-f")  # "File" menu
+    machine.send_key("c")  # "Connection Information"
+
+    # "Connection Information" dialog box
+    machine.wait_for_window("Connection Information")
+    machine.wait_for_text("SOME-NODE")
+    machine.wait_for_text("${pkgs.tsm-client.passthru.unwrapped.version}")
+
+    machine.shutdown()
+  '';
+
+  meta.maintainers = [ lib.maintainers.yarny ];
+})
diff --git a/nixos/tests/tuptime.nix b/nixos/tests/tuptime.nix
new file mode 100644
index 00000000000..6d37e306983
--- /dev/null
+++ b/nixos/tests/tuptime.nix
@@ -0,0 +1,29 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "tuptime";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ evils ];
+  };
+
+  machine = { pkgs, ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+    services.tuptime.enable = true;
+  };
+
+  testScript =
+    ''
+      # see if it starts
+      start_all()
+      machine.wait_for_unit("multi-user.target")
+      machine.succeed("tuptime | grep 'System startups:[[:blank:]]*1'")
+      machine.succeed("tuptime | grep 'System uptime:[[:blank:]]*100.0%'")
+      machine.shutdown()
+
+      # restart machine and see if it correctly reports the reboot
+      machine.start()
+      machine.wait_for_unit("multi-user.target")
+      machine.succeed("tuptime | grep 'System startups:[[:blank:]]*2'")
+      machine.succeed("tuptime | grep 'System shutdowns:[[:blank:]]*1 ok'")
+      machine.shutdown()
+    '';
+})
+
diff --git a/nixos/tests/turbovnc-headless-server.nix b/nixos/tests/turbovnc-headless-server.nix
new file mode 100644
index 00000000000..7d705c56ecf
--- /dev/null
+++ b/nixos/tests/turbovnc-headless-server.nix
@@ -0,0 +1,172 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "turbovnc-headless-server";
+  meta = {
+    maintainers = with lib.maintainers; [ nh2 ];
+  };
+
+  machine = { pkgs, ... }: {
+
+    environment.systemPackages = with pkgs; [
+      glxinfo
+      procps # for `pkill`, `pidof` in the test
+      scrot # for screenshotting Xorg
+      turbovnc
+    ];
+
+    programs.turbovnc.ensureHeadlessSoftwareOpenGL = true;
+
+    networking.firewall = {
+      # Reject instead of drop, for failures instead of hangs.
+      rejectPackets = true;
+      allowedTCPPorts = [
+        5900 # VNC :0, for seeing what's going on in the server
+      ];
+    };
+
+    # 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";
+    users.extraUsers.root.password = "";
+    users.mutableUsers = false;
+  };
+
+  testScript = ''
+    def wait_until_terminated_or_succeeds(
+        termination_check_shell_command,
+        success_check_shell_command,
+        get_detail_message_fn,
+        retries=60,
+        retry_sleep=0.5,
+    ):
+        def check_success():
+            command_exit_code, _output = machine.execute(success_check_shell_command)
+            return command_exit_code == 0
+
+        for _ in range(retries):
+            exit_check_exit_code, _output = machine.execute(termination_check_shell_command)
+            is_terminated = exit_check_exit_code != 0
+            if is_terminated:
+                if check_success():
+                    return
+                else:
+                    details = get_detail_message_fn()
+                    raise Exception(
+                        f"termination check ({termination_check_shell_command}) triggered without command succeeding ({success_check_shell_command}); details: {details}"
+                    )
+            else:
+                if check_success():
+                    return
+            import time
+            time.sleep(retry_sleep)
+
+        if not check_success():
+            details = get_detail_message_fn()
+            raise Exception(
+                f"action timed out ({success_check_shell_command}); details: {details}"
+            )
+
+
+    # Below we use the pattern:
+    #     (cmd | tee stdout.log) 3>&1 1>&2 2>&3 | tee stderr.log
+    # to capture both stderr and stdout while also teeing them, see:
+    # https://unix.stackexchange.com/questions/6430/how-to-redirect-stderr-and-stdout-to-different-files-and-also-display-in-termina/6431#6431
+
+
+    # Starts headless VNC server, backgrounding it.
+    def start_xvnc():
+        xvnc_command = " ".join(
+            [
+                "Xvnc",
+                ":0",
+                "-iglx",
+                "-auth /root/.Xauthority",
+                "-geometry 1240x900",
+                "-depth 24",
+                "-rfbwait 5000",
+                "-deferupdate 1",
+                "-verbose",
+                "-securitytypes none",
+                # We don't enforce localhost listening such that we
+                # can connect from outside the VM using
+                #     env QEMU_NET_OPTS=hostfwd=tcp::5900-:5900 $(nix-build nixos/tests/turbovnc-headless-server.nix -A driver)/bin/nixos-test-driver
+                # for testing purposes, and so that we can in the future
+                # add another test case that connects the TurboVNC client.
+                # "-localhost",
+            ]
+        )
+        machine.execute(
+            # Note trailing & for backgrounding.
+            f"({xvnc_command} | tee /tmp/Xvnc.stdout) 3>&1 1>&2 2>&3 | tee /tmp/Xvnc.stderr >&2 &",
+        )
+
+
+    # Waits until the server log message that tells us that GLX is ready
+    # (requires `-verbose` above), avoiding screenshoting racing below.
+    def wait_until_xvnc_glx_ready():
+        machine.wait_until_succeeds("test -f /tmp/Xvnc.stderr")
+        wait_until_terminated_or_succeeds(
+            termination_check_shell_command="pidof Xvnc",
+            success_check_shell_command="grep 'GLX: Initialized DRISWRAST' /tmp/Xvnc.stderr",
+            get_detail_message_fn=lambda: "Contents of /tmp/Xvnc.stderr:\n"
+            + machine.succeed("cat /tmp/Xvnc.stderr"),
+        )
+
+
+    # Checks that we detect glxgears failing when
+    # `LIBGL_DRIVERS_PATH=/nonexistent` is set
+    # (in which case software rendering should not work).
+    def test_glxgears_failing_with_bad_driver_path():
+        machine.execute(
+            # Note trailing & for backgrounding.
+            "(env DISPLAY=:0 LIBGL_DRIVERS_PATH=/nonexistent glxgears -info | tee /tmp/glxgears-should-fail.stdout) 3>&1 1>&2 2>&3 | tee /tmp/glxgears-should-fail.stderr >&2 &"
+        )
+        machine.wait_until_succeeds("test -f /tmp/glxgears-should-fail.stderr")
+        wait_until_terminated_or_succeeds(
+            termination_check_shell_command="pidof glxgears",
+            success_check_shell_command="grep 'libGL error: failed to load driver: swrast' /tmp/glxgears-should-fail.stderr",
+            get_detail_message_fn=lambda: "Contents of /tmp/glxgears-should-fail.stderr:\n"
+            + machine.succeed("cat /tmp/glxgears-should-fail.stderr"),
+        )
+        machine.wait_until_fails("pidof glxgears")
+
+
+    # Starts glxgears, backgrounding it. Waits until it prints the `GL_RENDERER`.
+    # Does not quit glxgears.
+    def test_glxgears_prints_renderer():
+        machine.execute(
+            # Note trailing & for backgrounding.
+            "(env DISPLAY=:0 glxgears -info | tee /tmp/glxgears.stdout) 3>&1 1>&2 2>&3 | tee /tmp/glxgears.stderr >&2 &"
+        )
+        machine.wait_until_succeeds("test -f /tmp/glxgears.stderr")
+        wait_until_terminated_or_succeeds(
+            termination_check_shell_command="pidof glxgears",
+            success_check_shell_command="grep 'GL_RENDERER' /tmp/glxgears.stdout",
+            get_detail_message_fn=lambda: "Contents of /tmp/glxgears.stderr:\n"
+            + machine.succeed("cat /tmp/glxgears.stderr"),
+        )
+
+
+    with subtest("Start Xvnc"):
+        start_xvnc()
+        wait_until_xvnc_glx_ready()
+
+    with subtest("Ensure bad driver path makes glxgears fail"):
+        test_glxgears_failing_with_bad_driver_path()
+
+    with subtest("Run 3D application (glxgears)"):
+        test_glxgears_prints_renderer()
+
+        # Take screenshot; should display the glxgears.
+        machine.succeed("scrot --display :0 /tmp/glxgears.png")
+
+    # Copy files down.
+    machine.copy_from_vm("/tmp/glxgears.png")
+    machine.copy_from_vm("/tmp/glxgears.stdout")
+    machine.copy_from_vm("/tmp/glxgears-should-fail.stdout")
+    machine.copy_from_vm("/tmp/glxgears-should-fail.stderr")
+    machine.copy_from_vm("/tmp/Xvnc.stdout")
+    machine.copy_from_vm("/tmp/Xvnc.stderr")
+  '';
+
+})
diff --git a/nixos/tests/tuxguitar.nix b/nixos/tests/tuxguitar.nix
new file mode 100644
index 00000000000..63a7b6c7dec
--- /dev/null
+++ b/nixos/tests/tuxguitar.nix
@@ -0,0 +1,24 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "tuxguitar";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ asbachb ];
+  };
+
+  machine = { config, pkgs, ... }: {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+
+    environment.systemPackages = [ pkgs.tuxguitar ];
+  };
+
+  testScript = ''
+    machine.wait_for_x()
+    machine.succeed("tuxguitar >&2 &")
+    machine.wait_for_window("TuxGuitar - Untitled.tg")
+    machine.sleep(1)
+    machine.screenshot("tuxguitar")
+  '';
+})
diff --git a/nixos/tests/txredisapi.nix b/nixos/tests/txredisapi.nix
new file mode 100644
index 00000000000..7c6b36a5c47
--- /dev/null
+++ b/nixos/tests/txredisapi.nix
@@ -0,0 +1,29 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+{
+  name = "txredisapi";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ dandellion ];
+  };
+
+  nodes = {
+    machine =
+      { pkgs, ... }:
+
+      {
+        services.redis.servers."".enable = true;
+
+        environment.systemPackages = with pkgs; [ (python38.withPackages (ps: [ ps.twisted ps.txredisapi ps.mock ]))];
+      };
+  };
+
+  testScript = { nodes, ... }: let
+    inherit (nodes.machine.config.services) redis;
+    in ''
+    start_all()
+    machine.wait_for_unit("redis")
+    machine.wait_for_file("${redis.servers."".unixSocket}")
+    machine.succeed("ln -s ${redis.servers."".unixSocket} /tmp/redis.sock")
+
+    tests = machine.succeed("PYTHONPATH=\"${pkgs.python3Packages.txredisapi.src}\" python -m twisted.trial ${pkgs.python3Packages.txredisapi.src}/tests")
+  '';
+})
diff --git a/nixos/tests/ucarp.nix b/nixos/tests/ucarp.nix
new file mode 100644
index 00000000000..1f60f770d3a
--- /dev/null
+++ b/nixos/tests/ucarp.nix
@@ -0,0 +1,66 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} :
+
+let
+  addrShared = "192.168.0.1";
+  addrHostA = "192.168.0.10";
+  addrHostB = "192.168.0.11";
+
+  mkUcarpHost = addr: { config, pkgs, lib, ... }: {
+    networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
+      { address = addr; prefixLength = 24; }
+    ];
+
+    networking.ucarp = {
+      enable = true;
+      interface = "eth1";
+      srcIp = addr;
+      vhId = 1;
+      passwordFile = "${pkgs.writeText "ucarp-pass" "secure"}";
+      addr = addrShared;
+      upscript = pkgs.writeScript "upscript" ''
+        #!/bin/sh
+        ${pkgs.iproute2}/bin/ip addr add "$2"/24 dev "$1"
+      '';
+      downscript = pkgs.writeScript "downscript" ''
+        #!/bin/sh
+        ${pkgs.iproute2}/bin/ip addr del "$2"/24 dev "$1"
+      '';
+    };
+  };
+in {
+  name = "ucarp";
+  meta.maintainers = with lib.maintainers; [ oxzi ];
+
+  nodes = {
+    hostA = mkUcarpHost addrHostA;
+    hostB = mkUcarpHost addrHostB;
+  };
+
+  testScript = ''
+    def is_master(host):
+      ipOutput = host.succeed("ip addr show dev eth1")
+      return "inet ${addrShared}/24" in ipOutput
+
+
+    start_all()
+
+    # First, let both hosts start and let a master node be selected
+    for host, peer in [(hostA, "${addrHostB}"), (hostB, "${addrHostA}")]:
+      host.wait_for_unit("ucarp.service")
+      host.succeed(f"ping -c 1 {peer}")
+
+    hostA.sleep(5)
+
+    hostA_master, hostB_master = is_master(hostA), is_master(hostB)
+    assert hostA_master != hostB_master, "only one master node is allowed"
+
+    master_host = hostA if hostA_master else hostB
+    backup_host = hostB if hostA_master else hostA
+
+    # Let's crash the master host and let the backup take over
+    master_host.crash()
+
+    backup_host.sleep(5)
+    assert is_master(backup_host), "backup did not take over"
+  '';
+})
diff --git a/nixos/tests/udisks2.nix b/nixos/tests/udisks2.nix
new file mode 100644
index 00000000000..6c4b71aaa2e
--- /dev/null
+++ b/nixos/tests/udisks2.nix
@@ -0,0 +1,69 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+
+  stick = pkgs.fetchurl {
+    url = "https://nixos.org/~eelco/nix/udisks-test.img.xz";
+    sha256 = "0was1xgjkjad91nipzclaz5biv3m4b2nk029ga6nk7iklwi19l8b";
+  };
+
+in
+
+{
+  name = "udisks2";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ eelco ];
+  };
+
+  machine =
+    { ... }:
+    { services.udisks2.enable = true;
+      imports = [ ./common/user-account.nix ];
+
+      security.polkit.extraConfig =
+        ''
+          polkit.addRule(function(action, subject) {
+            if (subject.user == "alice") return "yes";
+          });
+        '';
+    };
+
+  testScript =
+    ''
+      import lzma
+
+      with lzma.open(
+          "${stick}"
+      ) as data, open(machine.state_dir / "usbstick.img", "wb") as stick:
+          stick.write(data.read())
+
+      machine.succeed("udisksctl info -b /dev/vda >&2")
+      machine.fail("udisksctl info -b /dev/sda1")
+
+      # Attach a USB stick and wait for it to show up.
+      machine.send_monitor_command(
+          f"drive_add 0 id=stick,if=none,file={stick.name},format=raw"
+      )
+      machine.send_monitor_command("device_add usb-storage,id=stick,drive=stick")
+      machine.wait_until_succeeds("udisksctl info -b /dev/sda1")
+      machine.succeed("udisksctl info -b /dev/sda1 | grep 'IdLabel:.*USBSTICK'")
+
+      # Mount the stick as a non-root user and do some stuff with it.
+      machine.succeed("su - alice -c 'udisksctl info -b /dev/sda1'")
+      machine.succeed("su - alice -c 'udisksctl mount -b /dev/sda1'")
+      machine.succeed(
+          "su - alice -c 'cat /run/media/alice/USBSTICK/test.txt' | grep -q 'Hello World'"
+      )
+      machine.succeed("su - alice -c 'echo foo > /run/media/alice/USBSTICK/bar.txt'")
+
+      # Unmounting the stick should make the mountpoint disappear.
+      machine.succeed("su - alice -c 'udisksctl unmount -b /dev/sda1'")
+      machine.fail("[ -d /run/media/alice/USBSTICK ]")
+
+      # Remove the USB stick.
+      machine.send_monitor_command("device_del stick")
+      machine.wait_until_fails("udisksctl info -b /dev/sda1")
+      machine.fail("[ -e /dev/sda ]")
+    '';
+
+})
diff --git a/nixos/tests/unbound.nix b/nixos/tests/unbound.nix
new file mode 100644
index 00000000000..576287a9fe5
--- /dev/null
+++ b/nixos/tests/unbound.nix
@@ -0,0 +1,315 @@
+/*
+ Test that our unbound module indeed works as most users would expect.
+ There are a few settings that we must consider when modifying the test. The
+ ususal use-cases for unbound are
+   * running a recursive DNS resolver on the local machine
+   * running a recursive DNS resolver on the local machine, forwarding to a local DNS server via UDP/53 & TCP/53
+   * running a recursive DNS resolver on the local machine, forwarding to a local DNS server via TCP/853 (DoT)
+   * running a recursive DNS resolver on a machine in the network awaiting input from clients over TCP/53 & UDP/53
+   * running a recursive DNS resolver on a machine in the network awaiting input from clients over TCP/853 (DoT)
+
+ In the below test setup we are trying to implement all of those use cases.
+
+ Another aspect that we cover is access to the local control UNIX socket. It
+ can optionally be enabled and users can optionally be in a group to gain
+ access. Users that are not in the group (except for root) should not have
+ access to that socket. Also, when there is no socket configured, users
+ shouldn't be able to access the control socket at all. Not even root.
+*/
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+  let
+    # common client configuration that we can just use for the multitude of
+    # clients we are constructing
+    common = { lib, pkgs, ... }: {
+      config = {
+        environment.systemPackages = [ pkgs.knot-dns ];
+
+        # disable the root anchor update as we do not have internet access during
+        # the test execution
+        services.unbound.enableRootTrustAnchor = false;
+
+        # we want to test the full-variant of the package to also get DoH support
+        services.unbound.package = pkgs.unbound-full;
+      };
+    };
+
+    cert = pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
+      openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=dns.example.local'
+      mkdir -p $out
+      cp key.pem cert.pem $out
+    '';
+  in
+  {
+    name = "unbound";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ andir ];
+    };
+
+    nodes = {
+
+      # The server that actually serves our zones, this tests unbounds authoriative mode
+      authoritative = { lib, pkgs, config, ... }: {
+        imports = [ common ];
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
+          { address = "192.168.0.1"; prefixLength = 24; }
+        ];
+        networking.interfaces.eth1.ipv6.addresses = lib.mkForce [
+          { address = "fd21::1"; prefixLength = 64; }
+        ];
+        networking.firewall.allowedTCPPorts = [ 53 ];
+        networking.firewall.allowedUDPPorts = [ 53 ];
+
+        services.unbound = {
+          enable = true;
+          settings = {
+            server = {
+              interface = [ "192.168.0.1" "fd21::1" "::1" "127.0.0.1" ];
+              access-control = [ "192.168.0.0/24 allow" "fd21::/64 allow" "::1 allow" "127.0.0.0/8 allow" ];
+              local-data = [
+                ''"example.local. IN A 1.2.3.4"''
+                ''"example.local. IN AAAA abcd::eeff"''
+              ];
+            };
+          };
+        };
+      };
+
+      # The resolver that knows that fowards (only) to the authoritative server
+      # and listens on UDP/53, TCP/53 & TCP/853.
+      resolver = { lib, nodes, ... }: {
+        imports = [ common ];
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
+          { address = "192.168.0.2"; prefixLength = 24; }
+        ];
+        networking.interfaces.eth1.ipv6.addresses = lib.mkForce [
+          { address = "fd21::2"; prefixLength = 64; }
+        ];
+        networking.firewall.allowedTCPPorts = [
+          53 # regular DNS
+          853 # DNS over TLS
+          443 # DNS over HTTPS
+        ];
+        networking.firewall.allowedUDPPorts = [ 53 ];
+
+        services.unbound = {
+          enable = true;
+          settings = {
+            server = {
+              interface = [ "::1" "127.0.0.1" "192.168.0.2" "fd21::2"
+                            "192.168.0.2@853" "fd21::2@853" "::1@853" "127.0.0.1@853"
+                            "192.168.0.2@443" "fd21::2@443" "::1@443" "127.0.0.1@443" ];
+              access-control = [ "192.168.0.0/24 allow" "fd21::/64 allow" "::1 allow" "127.0.0.0/8 allow" ];
+              tls-service-pem = "${cert}/cert.pem";
+              tls-service-key = "${cert}/key.pem";
+            };
+            forward-zone = [
+              {
+                name = ".";
+                forward-addr = [
+                  (lib.head nodes.authoritative.config.networking.interfaces.eth1.ipv6.addresses).address
+                  (lib.head nodes.authoritative.config.networking.interfaces.eth1.ipv4.addresses).address
+                ];
+              }
+            ];
+          };
+        };
+      };
+
+      # machine that runs a local unbound that will be reconfigured during test execution
+      local_resolver = { lib, nodes, config, ... }: {
+        imports = [ common ];
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
+          { address = "192.168.0.3"; prefixLength = 24; }
+        ];
+        networking.interfaces.eth1.ipv6.addresses = lib.mkForce [
+          { address = "fd21::3"; prefixLength = 64; }
+        ];
+        networking.firewall.allowedTCPPorts = [
+          53 # regular DNS
+        ];
+        networking.firewall.allowedUDPPorts = [ 53 ];
+
+        services.unbound = {
+          enable = true;
+          settings = {
+            server = {
+              interface = [ "::1" "127.0.0.1" ];
+              access-control = [ "::1 allow" "127.0.0.0/8 allow" ];
+            };
+            include = "/etc/unbound/extra*.conf";
+          };
+          localControlSocketPath = "/run/unbound/unbound.ctl";
+        };
+
+        users.users = {
+          # user that is permitted to access the unix socket
+          someuser = {
+            isSystemUser = true;
+            group = "someuser";
+            extraGroups = [
+              config.users.users.unbound.group
+            ];
+          };
+
+          # user that is not permitted to access the unix socket
+          unauthorizeduser = {
+            isSystemUser = true;
+            group = "unauthorizeduser";
+          };
+
+        };
+        users.groups = {
+          someuser = {};
+          unauthorizeduser = {};
+        };
+
+        # Used for testing configuration reloading
+        environment.etc = {
+          "unbound-extra1.conf".text = ''
+            forward-zone:
+            name: "example.local."
+            forward-addr: ${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv6.addresses).address}
+            forward-addr: ${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv4.addresses).address}
+          '';
+          "unbound-extra2.conf".text = ''
+            auth-zone:
+              name: something.local.
+              zonefile: ${pkgs.writeText "zone" ''
+                something.local. IN A 3.4.5.6
+              ''}
+          '';
+        };
+      };
+
+
+      # plain node that only has network access and doesn't run any part of the
+      # resolver software locally
+      client = { lib, nodes, ... }: {
+        imports = [ common ];
+        networking.nameservers = [
+          (lib.head nodes.resolver.config.networking.interfaces.eth1.ipv6.addresses).address
+          (lib.head nodes.resolver.config.networking.interfaces.eth1.ipv4.addresses).address
+        ];
+        networking.interfaces.eth1.ipv4.addresses = [
+          { address = "192.168.0.10"; prefixLength = 24; }
+        ];
+        networking.interfaces.eth1.ipv6.addresses = [
+          { address = "fd21::10"; prefixLength = 64; }
+        ];
+      };
+    };
+
+    testScript = { nodes, ... }: ''
+      import typing
+
+      zone = "example.local."
+      records = [("AAAA", "abcd::eeff"), ("A", "1.2.3.4")]
+
+
+      def query(
+          machine,
+          host: str,
+          query_type: str,
+          query: str,
+          expected: typing.Optional[str] = None,
+          args: typing.Optional[typing.List[str]] = None,
+      ):
+          """
+          Execute a single query and compare the result with expectation
+          """
+          text_args = ""
+          if args:
+              text_args = " ".join(args)
+
+          out = machine.succeed(
+              f"kdig {text_args} {query} {query_type} @{host} +short"
+          ).strip()
+          machine.log(f"{host} replied with {out}")
+          if expected:
+              assert expected == out, f"Expected `{expected}` but got `{out}`"
+
+
+      def test(machine, remotes, /, doh=False, zone=zone, records=records, args=[]):
+          """
+          Run queries for the given remotes on the given machine.
+          """
+          for query_type, expected in records:
+              for remote in remotes:
+                  query(machine, remote, query_type, zone, expected, args)
+                  query(machine, remote, query_type, zone, expected, ["+tcp"] + args)
+                  if doh:
+                      query(
+                          machine,
+                          remote,
+                          query_type,
+                          zone,
+                          expected,
+                          ["+tcp", "+tls"] + args,
+                      )
+                      query(
+                          machine,
+                          remote,
+                          query_type,
+                          zone,
+                          expected,
+                          ["+https"] + args,
+                      )
+
+
+      client.start()
+      authoritative.wait_for_unit("unbound.service")
+
+      # verify that we can resolve locally
+      with subtest("test the authoritative servers local responses"):
+          test(authoritative, ["::1", "127.0.0.1"])
+
+      resolver.wait_for_unit("unbound.service")
+
+      with subtest("root is unable to use unbounc-control when the socket is not configured"):
+          resolver.succeed("which unbound-control")  # the binary must exist
+          resolver.fail("unbound-control list_forwards")  # the invocation must fail
+
+      # verify that the resolver is able to resolve on all the local protocols
+      with subtest("test that the resolver resolves on all protocols and transports"):
+          test(resolver, ["::1", "127.0.0.1"], doh=True)
+
+      resolver.wait_for_unit("multi-user.target")
+
+      with subtest("client should be able to query the resolver"):
+          test(client, ["${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv6.addresses).address}", "${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv4.addresses).address}"], doh=True)
+
+      # discard the client we do not need anymore
+      client.shutdown()
+
+      local_resolver.wait_for_unit("multi-user.target")
+
+      # link a new config file to /etc/unbound/extra.conf
+      local_resolver.succeed("ln -s /etc/unbound-extra1.conf /etc/unbound/extra1.conf")
+
+      # reload the server & ensure the forwarding works
+      with subtest("test that the local resolver resolves on all protocols and transports"):
+          local_resolver.succeed("systemctl reload unbound")
+          print(local_resolver.succeed("journalctl -u unbound -n 1000"))
+          test(local_resolver, ["::1", "127.0.0.1"], args=["+timeout=60"])
+
+      with subtest("test that we can use the unbound control socket"):
+          out = local_resolver.succeed(
+              "sudo -u someuser -- unbound-control list_forwards"
+          ).strip()
+
+          # Thank you black! Can't really break this line into a readable version.
+          expected = "example.local. IN forward ${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv6.addresses).address} ${(lib.head nodes.resolver.config.networking.interfaces.eth1.ipv4.addresses).address}"
+          assert out == expected, f"Expected `{expected}` but got `{out}` instead."
+          local_resolver.fail("sudo -u unauthorizeduser -- unbound-control list_forwards")
+
+
+      # link a new config file to /etc/unbound/extra.conf
+      local_resolver.succeed("ln -sf /etc/unbound-extra2.conf /etc/unbound/extra2.conf")
+
+      # reload the server & ensure the new local zone works
+      with subtest("test that we can query the new local zone"):
+          local_resolver.succeed("unbound-control reload")
+          r = [("A", "3.4.5.6")]
+          test(local_resolver, ["::1", "127.0.0.1"], zone="something.local.", records=r)
+    '';
+  })
diff --git a/nixos/tests/unifi.nix b/nixos/tests/unifi.nix
new file mode 100644
index 00000000000..9dc7e5d04bd
--- /dev/null
+++ b/nixos/tests/unifi.nix
@@ -0,0 +1,36 @@
+# Test UniFi controller
+
+{ system ? builtins.currentSystem
+, config ? { allowUnfree = true; }
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  makeAppTest = unifi: makeTest {
+    name = "unifi-controller-${unifi.version}";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ patryk27 zhaofengli ];
+    };
+
+    nodes.server = {
+      services.unifi = {
+        enable = true;
+        unifiPackage = unifi;
+        openFirewall = false;
+      };
+    };
+
+    testScript = ''
+      server.wait_for_unit("unifi.service")
+      server.wait_until_succeeds("curl -Lk https://localhost:8443 >&2", timeout=300)
+    '';
+  };
+in with pkgs; {
+  unifiLTS = makeAppTest unifiLTS;
+  unifi5 = makeAppTest unifi5;
+  unifi6 = makeAppTest unifi6;
+  unifi7 = makeAppTest unifi7;
+}
diff --git a/nixos/tests/upnp.nix b/nixos/tests/upnp.nix
new file mode 100644
index 00000000000..451c8607d0e
--- /dev/null
+++ b/nixos/tests/upnp.nix
@@ -0,0 +1,96 @@
+# This tests whether UPnP port mappings can be created using Miniupnpd
+# and Miniupnpc.
+# It runs a Miniupnpd service on one machine, and verifies
+# a client can indeed create a port mapping using Miniupnpc. If
+# this succeeds an external client will try to connect to the port
+# mapping.
+
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+  internalRouterAddress = "192.168.3.1";
+  internalClient1Address = "192.168.3.2";
+  externalRouterAddress = "80.100.100.1";
+  externalClient2Address = "80.100.100.2";
+in
+{
+  name = "upnp";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ bobvanderlinden ];
+  };
+
+  nodes =
+    {
+      router =
+        { pkgs, nodes, ... }:
+        { virtualisation.vlans = [ 1 2 ];
+          networking.nat.enable = true;
+          networking.nat.internalInterfaces = [ "eth2" ];
+          networking.nat.externalInterface = "eth1";
+          networking.firewall.enable = true;
+          networking.firewall.trustedInterfaces = [ "eth2" ];
+          networking.interfaces.eth1.ipv4.addresses = [
+            { address = externalRouterAddress; prefixLength = 24; }
+          ];
+          networking.interfaces.eth2.ipv4.addresses = [
+            { address = internalRouterAddress; prefixLength = 24; }
+          ];
+          services.miniupnpd = {
+            enable = true;
+            externalInterface = "eth1";
+            internalIPs = [ "eth2" ];
+            appendConfig = ''
+              ext_ip=${externalRouterAddress}
+            '';
+          };
+        };
+
+      client1 =
+        { pkgs, nodes, ... }:
+        { environment.systemPackages = [ pkgs.miniupnpc_2 pkgs.netcat ];
+          virtualisation.vlans = [ 2 ];
+          networking.defaultGateway = internalRouterAddress;
+          networking.interfaces.eth1.ipv4.addresses = [
+            { address = internalClient1Address; prefixLength = 24; }
+          ];
+          networking.firewall.enable = false;
+
+          services.httpd.enable = true;
+          services.httpd.virtualHosts.localhost = {
+            listen = [{ ip = "*"; port = 9000; }];
+            adminAddr = "foo@example.org";
+            documentRoot = "/tmp";
+          };
+        };
+
+      client2 =
+        { pkgs, ... }:
+        { environment.systemPackages = [ pkgs.miniupnpc_2 ];
+          virtualisation.vlans = [ 1 ];
+          networking.interfaces.eth1.ipv4.addresses = [
+            { address = externalClient2Address; prefixLength = 24; }
+          ];
+          networking.firewall.enable = false;
+        };
+    };
+
+  testScript =
+    { nodes, ... }:
+    ''
+      start_all()
+
+      # Wait for network and miniupnpd.
+      router.wait_for_unit("network-online.target")
+      # $router.wait_for_unit("nat")
+      router.wait_for_unit("firewall.service")
+      router.wait_for_unit("miniupnpd")
+
+      client1.wait_for_unit("network-online.target")
+
+      client1.succeed("upnpc -a ${internalClient1Address} 9000 9000 TCP")
+
+      client1.wait_for_unit("httpd")
+      client2.wait_until_succeeds("curl -f http://${externalRouterAddress}:9000/")
+    '';
+
+})
diff --git a/nixos/tests/usbguard.nix b/nixos/tests/usbguard.nix
new file mode 100644
index 00000000000..bb707bdbf70
--- /dev/null
+++ b/nixos/tests/usbguard.nix
@@ -0,0 +1,62 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "usbguard";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ tnias ];
+  };
+
+  machine =
+    { ... }:
+    {
+      services.usbguard = {
+        enable = true;
+        IPCAllowedUsers = [ "alice" "root" ];
+
+        # As virtual USB devices get attached to the "QEMU USB Hub" we need to
+        # allow Hubs. Otherwise we would have to explicitly allow them too.
+        rules = ''
+          allow with-interface equals { 09:00:00 }
+        '';
+      };
+      imports = [ ./common/user-account.nix ];
+    };
+
+  testScript = ''
+    # create a blank disk image for our fake USB stick
+    with open(machine.state_dir / "usbstick.img", "wb") as stick:
+        stick.write(b"\x00" * (1024 * 1024))
+
+    # wait for machine to have started and the usbguard service to be up
+    machine.wait_for_unit("usbguard.service")
+
+    with subtest("IPC access control"):
+        # User "alice" is allowed to access the IPC interface
+        machine.succeed("su alice -c 'usbguard list-devices'")
+
+        # User "bob" is not allowed to access the IPC interface
+        machine.fail("su bob -c 'usbguard list-devices'")
+
+    with subtest("check basic functionality"):
+        # at this point we expect that no USB HDD is connected
+        machine.fail("usbguard list-devices | grep -E 'QEMU USB HARDDRIVE'")
+
+        # insert usb device
+        machine.send_monitor_command(
+            f"drive_add 0 id=stick,if=none,file={stick.name},format=raw"
+        )
+        machine.send_monitor_command("device_add usb-storage,id=stick,drive=stick")
+
+        # the attached USB HDD should show up after a short while
+        machine.wait_until_succeeds("usbguard list-devices | grep -E 'QEMU USB HARDDRIVE'")
+
+        # at this point there should be a **blocked** USB HDD
+        machine.succeed("usbguard list-devices | grep -E 'block.*QEMU USB HARDDRIVE'")
+        machine.fail("usbguard list-devices | grep -E ' allow .*QEMU USB HARDDRIVE'")
+
+        # allow storage devices
+        machine.succeed("usbguard allow-device 'with-interface { 08:*:* }'")
+
+        # at this point there should be an **allowed** USB HDD
+        machine.succeed("usbguard list-devices | grep -E ' allow .*QEMU USB HARDDRIVE'")
+        machine.fail("usbguard list-devices | grep -E ' block .*QEMU USB HARDDRIVE'")
+  '';
+})
diff --git a/nixos/tests/user-activation-scripts.nix b/nixos/tests/user-activation-scripts.nix
new file mode 100644
index 00000000000..0de8664c5ef
--- /dev/null
+++ b/nixos/tests/user-activation-scripts.nix
@@ -0,0 +1,33 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "user-activation-scripts";
+  meta = with lib.maintainers; { maintainers = [ chkno ]; };
+
+  machine = {
+    system.userActivationScripts.foo = "mktemp ~/user-activation-ran.XXXXXX";
+    users.users.alice = {
+      initialPassword = "pass1";
+      isNormalUser = true;
+    };
+  };
+
+  testScript = ''
+    def verify_user_activation_run_count(n):
+        machine.succeed(
+            '[[ "$(find /home/alice/ -name user-activation-ran.\\* | wc -l)" == %s ]]' % n
+        )
+
+
+    machine.wait_for_unit("multi-user.target")
+    machine.wait_for_unit("getty@tty1.service")
+    machine.wait_until_tty_matches(1, "login: ")
+    machine.send_chars("alice\n")
+    machine.wait_until_tty_matches(1, "Password: ")
+    machine.send_chars("pass1\n")
+    machine.send_chars("touch login-ok\n")
+    machine.wait_for_file("/home/alice/login-ok")
+    verify_user_activation_run_count(1)
+
+    machine.succeed("/run/current-system/bin/switch-to-configuration test")
+    verify_user_activation_run_count(2)
+  '';
+})
diff --git a/nixos/tests/uwsgi.nix b/nixos/tests/uwsgi.nix
new file mode 100644
index 00000000000..80dcde324aa
--- /dev/null
+++ b/nixos/tests/uwsgi.nix
@@ -0,0 +1,81 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+{
+  name = "uwsgi";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lnl7 ];
+  };
+
+  machine = { pkgs, ... }: {
+    users.users.hello  =
+      { isSystemUser = true;
+        group = "hello";
+      };
+    users.groups.hello = { };
+
+    services.uwsgi = {
+      enable = true;
+      plugins = [ "python3" "php" ];
+      capabilities = [ "CAP_NET_BIND_SERVICE" ];
+      instance.type = "emperor";
+
+      instance.vassals.hello = {
+        type = "normal";
+        immediate-uid = "hello";
+        immediate-gid = "hello";
+        module = "wsgi:application";
+        http = ":80";
+        cap = "net_bind_service";
+        pythonPackages = self: [ self.flask ];
+        chdir = pkgs.writeTextDir "wsgi.py" ''
+          from flask import Flask
+          import subprocess
+          application = Flask(__name__)
+
+          @application.route("/")
+          def hello():
+              return "Hello, World!"
+
+          @application.route("/whoami")
+          def whoami():
+              whoami = "${pkgs.coreutils}/bin/whoami"
+              proc = subprocess.run(whoami, capture_output=True)
+              return proc.stdout.decode().strip()
+        '';
+      };
+
+      instance.vassals.php = {
+        type = "normal";
+        master = true;
+        workers = 2;
+        http-socket = ":8000";
+        http-socket-modifier1 = 14;
+        php-index = "index.php";
+        php-docroot = pkgs.writeTextDir "index.php" ''
+          <?php echo "Hello World\n"; ?>
+        '';
+      };
+    };
+  };
+
+  testScript =
+    ''
+      machine.wait_for_unit("multi-user.target")
+      machine.wait_for_unit("uwsgi.service")
+
+      with subtest("uWSGI has started"):
+          machine.wait_for_unit("uwsgi.service")
+
+      with subtest("Vassal can bind on port <1024"):
+          machine.wait_for_open_port(80)
+          hello = machine.succeed("curl -f http://machine").strip()
+          assert "Hello, World!" in hello, f"Excepted 'Hello, World!', got '{hello}'"
+
+      with subtest("Vassal is running as dedicated user"):
+          username = machine.succeed("curl -f http://machine/whoami").strip()
+          assert username == "hello", f"Excepted 'hello', got '{username}'"
+
+      with subtest("PHP plugin is working"):
+          machine.wait_for_open_port(8000)
+          assert "Hello World" in machine.succeed("curl -fv http://machine:8000")
+    '';
+})
diff --git a/nixos/tests/v2ray.nix b/nixos/tests/v2ray.nix
new file mode 100644
index 00000000000..4808e149d31
--- /dev/null
+++ b/nixos/tests/v2ray.nix
@@ -0,0 +1,83 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: let
+
+  v2rayUser = {
+    # A random UUID.
+    id = "a6a46834-2150-45f8-8364-0f6f6ab32384";
+    alterId = 0; # Non-zero support will be disabled in the future.
+  };
+
+  # 1080 [http proxy] -> 1081 [vmess] -> direct
+  v2rayConfig = {
+    inbounds = [
+      {
+        tag = "http_in";
+        port = 1080;
+        listen = "127.0.0.1";
+        protocol = "http";
+      }
+      {
+        tag = "vmess_in";
+        port = 1081;
+        listen = "127.0.0.1";
+        protocol = "vmess";
+        settings.clients = [v2rayUser];
+      }
+    ];
+    outbounds = [
+      {
+        tag = "vmess_out";
+        protocol = "vmess";
+        settings.vnext = [{
+          address = "127.0.0.1";
+          port = 1081;
+          users = [v2rayUser];
+        }];
+      }
+      {
+        tag = "direct";
+        protocol = "freedom";
+      }
+    ];
+    routing.rules = [
+      {
+        type = "field";
+        inboundTag = "http_in";
+        outboundTag = "vmess_out";
+      }
+      {
+        type = "field";
+        inboundTag = "vmess_in";
+        outboundTag = "direct";
+      }
+    ];
+  };
+
+in {
+  name = "v2ray";
+  meta = with lib.maintainers; {
+    maintainers = [ servalcatty ];
+  };
+  machine = { pkgs, ... }: {
+    environment.systemPackages = [ pkgs.curl ];
+    services.v2ray = {
+      enable = true;
+      config = v2rayConfig;
+    };
+    services.httpd = {
+      enable = true;
+      adminAddr = "foo@example.org";
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("httpd.service")
+    machine.wait_for_unit("v2ray.service")
+    machine.wait_for_open_port(80)
+    machine.wait_for_open_port(1080)
+    machine.succeed(
+        "curl --fail --max-time 10 --proxy http://localhost:1080 http://localhost"
+    )
+  '';
+})
diff --git a/nixos/tests/vault-postgresql.nix b/nixos/tests/vault-postgresql.nix
new file mode 100644
index 00000000000..2847af13cbf
--- /dev/null
+++ b/nixos/tests/vault-postgresql.nix
@@ -0,0 +1,69 @@
+/* This test checks that
+    - multiple config files can be loaded
+    - the storage backend can be in a file outside the nix store
+      as is required for security (required because while confidentiality is
+      always covered, availability isn't)
+    - the postgres integration works
+ */
+import ./make-test-python.nix ({ pkgs, ... }:
+{
+  name = "vault-postgresql";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lnl7 roberth ];
+  };
+  machine = { lib, pkgs, ... }: {
+    environment.systemPackages = [ pkgs.vault ];
+    environment.variables.VAULT_ADDR = "http://127.0.0.1:8200";
+    services.vault.enable = true;
+    services.vault.extraSettingsPaths = [ "/run/vault.hcl" ];
+
+    systemd.services.vault = {
+      after = [
+        "postgresql.service"
+      ];
+      # Try for about 10 minutes rather than the default of 5 attempts.
+      serviceConfig.RestartSec = 1;
+      serviceConfig.StartLimitBurst = 600;
+    };
+    # systemd.services.vault.unitConfig.RequiresMountsFor = "/run/keys/";
+
+    services.postgresql.enable = true;
+    services.postgresql.initialScript = pkgs.writeText "init.psql" ''
+      CREATE USER vaultuser WITH ENCRYPTED PASSWORD 'thisisthepass';
+      GRANT CONNECT ON DATABASE postgres TO vaultuser;
+
+      -- https://www.vaultproject.io/docs/configuration/storage/postgresql
+      CREATE TABLE vault_kv_store (
+        parent_path TEXT COLLATE "C" NOT NULL,
+        path        TEXT COLLATE "C",
+        key         TEXT COLLATE "C",
+        value       BYTEA,
+        CONSTRAINT pkey PRIMARY KEY (path, key)
+      );
+      CREATE INDEX parent_path_idx ON vault_kv_store (parent_path);
+
+      GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO vaultuser;
+    '';
+  };
+
+  testScript =
+    ''
+      secretConfig = """
+          storage "postgresql" {
+            connection_url = "postgres://vaultuser:thisisthepass@localhost/postgres?sslmode=disable"
+          }
+          """
+
+      start_all()
+
+      machine.wait_for_unit("multi-user.target")
+      machine.succeed("cat >/root/vault.hcl <<EOF\n%s\nEOF\n" % secretConfig)
+      machine.succeed(
+          "install --owner vault --mode 0400 /root/vault.hcl /run/vault.hcl; rm /root/vault.hcl"
+      )
+      machine.wait_for_unit("vault.service")
+      machine.wait_for_open_port(8200)
+      machine.succeed("vault operator init")
+      machine.succeed("vault status || test $? -eq 2")
+    '';
+})
diff --git a/nixos/tests/vault.nix b/nixos/tests/vault.nix
new file mode 100644
index 00000000000..e86acd5b593
--- /dev/null
+++ b/nixos/tests/vault.nix
@@ -0,0 +1,25 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+{
+  name = "vault";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lnl7 ];
+  };
+  machine = { pkgs, ... }: {
+    environment.systemPackages = [ pkgs.vault ];
+    environment.variables.VAULT_ADDR = "http://127.0.0.1:8200";
+    services.vault.enable = true;
+  };
+
+  testScript =
+    ''
+      start_all()
+
+      machine.wait_for_unit("multi-user.target")
+      machine.wait_for_unit("vault.service")
+      machine.wait_for_open_port(8200)
+      machine.succeed("vault operator init")
+      # vault now returns exit code 2 for sealed vaults
+      machine.fail("vault status")
+      machine.succeed("vault status || test $? -eq 2")
+    '';
+})
diff --git a/nixos/tests/vaultwarden.nix b/nixos/tests/vaultwarden.nix
new file mode 100644
index 00000000000..56f1d245d50
--- /dev/null
+++ b/nixos/tests/vaultwarden.nix
@@ -0,0 +1,188 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+# These tests will:
+#  * Set up a vaultwarden server
+#  * Have Firefox use the web vault to create an account, log in, and save a password to the valut
+#  * Have the bw cli log in and read that password from the vault
+#
+# Note that Firefox must be on the same machine as the server for WebCrypto APIs to be available (or HTTPS must be configured)
+#
+# The same tests should work without modification on the official bitwarden server, if we ever package that.
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+let
+  backends = [ "sqlite" "mysql" "postgresql" ];
+
+  dbPassword = "please_dont_hack";
+
+  userEmail = "meow@example.com";
+  userPassword = "also_super_secret_ZJWpBKZi668QGt"; # Must be complex to avoid interstitial warning on the signup page
+
+  storedPassword = "seeeecret";
+
+  makeVaultwardenTest = backend: makeTest {
+    name = "vaultwarden-${backend}";
+    meta = {
+      maintainers = with pkgs.lib.maintainers; [ jjjollyjim ];
+    };
+
+    nodes = {
+      server = { pkgs, ... }:
+        let backendConfig = {
+          mysql = {
+            services.mysql = {
+              enable = true;
+              initialScript = pkgs.writeText "mysql-init.sql" ''
+                CREATE DATABASE bitwarden;
+                CREATE USER 'bitwardenuser'@'localhost' IDENTIFIED BY '${dbPassword}';
+                GRANT ALL ON `bitwarden`.* TO 'bitwardenuser'@'localhost';
+                FLUSH PRIVILEGES;
+              '';
+              package = pkgs.mariadb;
+            };
+
+            services.vaultwarden.config.databaseUrl = "mysql://bitwardenuser:${dbPassword}@localhost/bitwarden";
+
+            systemd.services.vaultwarden.after = [ "mysql.service" ];
+          };
+
+          postgresql = {
+            services.postgresql = {
+              enable = true;
+              initialScript = pkgs.writeText "postgresql-init.sql" ''
+                CREATE DATABASE bitwarden;
+                CREATE USER bitwardenuser WITH PASSWORD '${dbPassword}';
+                GRANT ALL PRIVILEGES ON DATABASE bitwarden TO bitwardenuser;
+              '';
+            };
+
+            services.vaultwarden.config.databaseUrl = "postgresql://bitwardenuser:${dbPassword}@localhost/bitwarden";
+
+            systemd.services.vaultwarden.after = [ "postgresql.service" ];
+          };
+
+          sqlite = { };
+        };
+        in
+        mkMerge [
+          backendConfig.${backend}
+          {
+            services.vaultwarden = {
+              enable = true;
+              dbBackend = backend;
+              config.rocketPort = 80;
+            };
+
+            networking.firewall.allowedTCPPorts = [ 80 ];
+
+            environment.systemPackages =
+              let
+                testRunner = pkgs.writers.writePython3Bin "test-runner"
+                  {
+                    libraries = [ pkgs.python3Packages.selenium ];
+                  } ''
+                  from selenium.webdriver import Firefox
+                  from selenium.webdriver.firefox.options import Options
+                  from selenium.webdriver.support.ui import WebDriverWait
+                  from selenium.webdriver.support import expected_conditions as EC
+
+                  options = Options()
+                  options.add_argument('--headless')
+                  driver = Firefox(options=options)
+
+                  driver.implicitly_wait(20)
+                  driver.get('http://localhost/#/register')
+
+                  wait = WebDriverWait(driver, 10)
+
+                  wait.until(EC.title_contains("Create Account"))
+
+                  driver.find_element_by_css_selector('input#email').send_keys(
+                    '${userEmail}'
+                  )
+                  driver.find_element_by_css_selector('input#name').send_keys(
+                    'A Cat'
+                  )
+                  driver.find_element_by_css_selector('input#masterPassword').send_keys(
+                    '${userPassword}'
+                  )
+                  driver.find_element_by_css_selector('input#masterPasswordRetype').send_keys(
+                    '${userPassword}'
+                  )
+                  driver.find_element_by_css_selector('input#acceptPolicies').click()
+
+                  driver.find_element_by_xpath("//button[contains(., 'Submit')]").click()
+
+                  wait.until_not(EC.title_contains("Create Account"))
+
+                  driver.find_element_by_css_selector('input#masterPassword').send_keys(
+                    '${userPassword}'
+                  )
+                  driver.find_element_by_xpath("//button[contains(., 'Log In')]").click()
+
+                  wait.until(EC.title_contains("My Vault"))
+
+                  driver.find_element_by_xpath("//button[contains(., 'Add Item')]").click()
+
+                  driver.find_element_by_css_selector('input#name').send_keys(
+                    'secrets'
+                  )
+                  driver.find_element_by_css_selector('input#loginPassword').send_keys(
+                    '${storedPassword}'
+                  )
+
+                  driver.find_element_by_xpath("//button[contains(., 'Save')]").click()
+                '';
+              in
+              [ pkgs.firefox-unwrapped pkgs.geckodriver testRunner ];
+
+          }
+        ];
+
+      client = { pkgs, ... }:
+        {
+          environment.systemPackages = [ pkgs.bitwarden-cli ];
+        };
+    };
+
+    testScript = ''
+      start_all()
+      server.wait_for_unit("vaultwarden.service")
+      server.wait_for_open_port(80)
+
+      with subtest("configure the cli"):
+          client.succeed("bw --nointeraction config server http://server")
+
+      with subtest("can't login to nonexistant account"):
+          client.fail(
+              "bw --nointeraction --raw login ${userEmail} ${userPassword}"
+          )
+
+      with subtest("use the web interface to sign up, log in, and save a password"):
+          server.succeed("PYTHONUNBUFFERED=1 test-runner | systemd-cat -t test-runner")
+
+      with subtest("log in with the cli"):
+          key = client.succeed(
+              "bw --nointeraction --raw login ${userEmail} ${userPassword}"
+          ).strip()
+
+      with subtest("sync with the cli"):
+          client.succeed(f"bw --nointeraction --raw --session {key} sync -f")
+
+      with subtest("get the password with the cli"):
+          password = client.succeed(
+              f"bw --nointeraction --raw --session {key} list items | ${pkgs.jq}/bin/jq -r .[].login.password"
+          )
+          assert password.strip() == "${storedPassword}"
+    '';
+  };
+in
+builtins.listToAttrs (
+  map
+    (backend: { name = backend; value = makeVaultwardenTest backend; })
+    backends
+)
diff --git a/nixos/tests/vector.nix b/nixos/tests/vector.nix
new file mode 100644
index 00000000000..583e60ddc56
--- /dev/null
+++ b/nixos/tests/vector.nix
@@ -0,0 +1,37 @@
+{ system ? builtins.currentSystem, config ? { }
+, pkgs ? import ../.. { inherit system config; } }:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+{
+  test1 = makeTest {
+    name = "vector-test1";
+    meta.maintainers = [ pkgs.lib.maintainers.happysalada ];
+
+    machine = { config, pkgs, ... }: {
+      services.vector = {
+        enable = true;
+        journaldAccess = true;
+        settings = {
+          sources.journald.type = "journald";
+
+          sinks = {
+            file = {
+              type = "file";
+              inputs = [ "journald" ];
+              path = "/var/lib/vector/logs.log";
+              encoding = { codec = "ndjson"; };
+            };
+          };
+        };
+      };
+    };
+
+    # ensure vector is forwarding the messages appropriately
+    testScript = ''
+      machine.wait_for_unit("vector.service")
+      machine.succeed("test -f /var/lib/vector/logs.log")
+    '';
+  };
+}
diff --git a/nixos/tests/vengi-tools.nix b/nixos/tests/vengi-tools.nix
new file mode 100644
index 00000000000..6b90542887d
--- /dev/null
+++ b/nixos/tests/vengi-tools.nix
@@ -0,0 +1,29 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "vengi-tools";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fgaz ];
+  };
+
+  machine = { config, pkgs, ... }: {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    environment.systemPackages = [ pkgs.vengi-tools ];
+  };
+
+  enableOCR = true;
+
+  testScript =
+    ''
+      machine.wait_for_x()
+      machine.execute("vengi-voxedit >&2 &")
+      machine.wait_for_window("voxedit")
+      # OCR on voxedit's window is very expensive, so we avoid wasting a try
+      # by letting the window load fully first
+      machine.sleep(15)
+      machine.wait_for_text("Palette")
+      machine.screenshot("screen")
+    '';
+})
diff --git a/nixos/tests/victoriametrics.nix b/nixos/tests/victoriametrics.nix
new file mode 100644
index 00000000000..5e364b67bf8
--- /dev/null
+++ b/nixos/tests/victoriametrics.nix
@@ -0,0 +1,33 @@
+# This test runs influxdb and checks if influxdb is up and running
+
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "victoriametrics";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ yorickvp ];
+  };
+
+  nodes = {
+    one = { ... }: {
+      services.victoriametrics.enable = true;
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    one.wait_for_unit("victoriametrics.service")
+
+    # write some points and run simple query
+    out = one.succeed(
+        "curl -f -d 'measurement,tag1=value1,tag2=value2 field1=123,field2=1.23' -X POST 'http://localhost:8428/write'"
+    )
+    cmd = (
+        """curl -f -s -G 'http://localhost:8428/api/v1/export' -d 'match={__name__!=""}'"""
+    )
+    # data takes a while to appear
+    one.wait_until_succeeds(f"[[ $({cmd} | wc -l) -ne 0 ]]")
+    out = one.succeed(cmd)
+    assert '"values":[123]' in out
+    assert '"values":[1.23]' in out
+  '';
+})
diff --git a/nixos/tests/vikunja.nix b/nixos/tests/vikunja.nix
new file mode 100644
index 00000000000..bd884b37f4f
--- /dev/null
+++ b/nixos/tests/vikunja.nix
@@ -0,0 +1,65 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "vikunja";
+
+  meta = with lib.maintainers; {
+    maintainers = [ em0lar ];
+  };
+
+  nodes = {
+    vikunjaSqlite = { ... }: {
+      services.vikunja = {
+        enable = true;
+        database = {
+          type = "sqlite";
+        };
+        frontendScheme = "http";
+        frontendHostname = "localhost";
+      };
+      services.nginx.enable = true;
+    };
+    vikunjaPostgresql = { pkgs, ... }: {
+      services.vikunja = {
+        enable = true;
+        database = {
+          type = "postgres";
+          user = "vikunja-api";
+          database = "vikunja-api";
+          host = "/run/postgresql";
+        };
+        frontendScheme = "http";
+        frontendHostname = "localhost";
+      };
+      services.postgresql = {
+        enable = true;
+        ensureDatabases = [ "vikunja-api" ];
+        ensureUsers = [
+          { name = "vikunja-api";
+            ensurePermissions = { "DATABASE \"vikunja-api\"" = "ALL PRIVILEGES"; };
+          }
+        ];
+      };
+      services.nginx.enable = true;
+    };
+  };
+
+  testScript =
+    ''
+      vikunjaSqlite.wait_for_unit("vikunja-api.service")
+      vikunjaSqlite.wait_for_open_port(3456)
+      vikunjaSqlite.succeed("curl --fail http://localhost:3456/api/v1/info")
+
+      vikunjaSqlite.wait_for_unit("nginx.service")
+      vikunjaSqlite.wait_for_open_port(80)
+      vikunjaSqlite.succeed("curl --fail http://localhost/api/v1/info")
+      vikunjaSqlite.succeed("curl --fail http://localhost")
+
+      vikunjaPostgresql.wait_for_unit("vikunja-api.service")
+      vikunjaPostgresql.wait_for_open_port(3456)
+      vikunjaPostgresql.succeed("curl --fail http://localhost:3456/api/v1/info")
+
+      vikunjaPostgresql.wait_for_unit("nginx.service")
+      vikunjaPostgresql.wait_for_open_port(80)
+      vikunjaPostgresql.succeed("curl --fail http://localhost/api/v1/info")
+      vikunjaPostgresql.succeed("curl --fail http://localhost")
+    '';
+})
diff --git a/nixos/tests/virtualbox.nix b/nixos/tests/virtualbox.nix
new file mode 100644
index 00000000000..f15412d365f
--- /dev/null
+++ b/nixos/tests/virtualbox.nix
@@ -0,0 +1,531 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; },
+  debug ? false,
+  enableUnfree ? false,
+  # Nested KVM virtualization (https://www.linux-kvm.org/page/Nested_Guests)
+  # requires a modprobe flag on the build machine: (kvm-amd for AMD CPUs)
+  #   boot.extraModprobeConfig = "options kvm-intel nested=Y";
+  # Without this VirtualBox will use SW virtualization and will only be able
+  # to run 32-bit guests.
+  useKvmNestedVirt ? false,
+  # Whether to run 64-bit guests instead of 32-bit. Requires nested KVM.
+  use64bitGuest ? false
+}:
+
+assert use64bitGuest -> useKvmNestedVirt;
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  testVMConfig = vmName: attrs: { config, pkgs, lib, ... }: let
+    guestAdditions = pkgs.linuxPackages.virtualboxGuestAdditions;
+
+    miniInit = ''
+      #!${pkgs.runtimeShell} -xe
+      export PATH="${lib.makeBinPath [ pkgs.coreutils pkgs.util-linux ]}"
+
+      mkdir -p /run/dbus
+      cat > /etc/passwd <<EOF
+      root:x:0:0::/root:/bin/false
+      messagebus:x:1:1::/run/dbus:/bin/false
+      EOF
+      cat > /etc/group <<EOF
+      root:x:0:
+      messagebus:x:1:
+      EOF
+
+      "${pkgs.dbus.daemon}/bin/dbus-daemon" --fork \
+        --config-file="${pkgs.dbus.daemon}/share/dbus-1/system.conf"
+
+      ${guestAdditions}/bin/VBoxService
+      ${(attrs.vmScript or (const "")) pkgs}
+
+      i=0
+      while [ ! -e /mnt-root/shutdown ]; do
+        sleep 10
+        i=$(($i + 10))
+        [ $i -le 120 ] || fail
+      done
+
+      rm -f /mnt-root/boot-done /mnt-root/shutdown
+    '';
+  in {
+    boot.kernelParams = [
+      "console=tty0" "console=ttyS0" "ignore_loglevel"
+      "boot.trace" "panic=1" "boot.panic_on_fail"
+      "init=${pkgs.writeScript "mini-init.sh" miniInit}"
+    ];
+
+    fileSystems."/" = {
+      device = "vboxshare";
+      fsType = "vboxsf";
+    };
+
+    virtualisation.virtualbox.guest.enable = true;
+
+    boot.initrd.kernelModules = [
+      "af_packet" "vboxsf"
+      "virtio" "virtio_pci" "virtio_ring" "virtio_net" "vboxguest"
+    ];
+
+    boot.initrd.extraUtilsCommands = ''
+      copy_bin_and_libs "${guestAdditions}/bin/mount.vboxsf"
+      copy_bin_and_libs "${pkgs.util-linux}/bin/unshare"
+      ${(attrs.extraUtilsCommands or (const "")) pkgs}
+    '';
+
+    boot.initrd.postMountCommands = ''
+      touch /mnt-root/boot-done
+      hostname "${vmName}"
+      mkdir -p /nix/store
+      unshare -m ${escapeShellArg pkgs.runtimeShell} -c '
+        mount -t vboxsf nixstore /nix/store
+        exec "$stage2Init"
+      '
+      poweroff -f
+    '';
+
+    system.requiredKernelConfig = with config.lib.kernelConfig; [
+      (isYes "SERIAL_8250_CONSOLE")
+      (isYes "SERIAL_8250")
+    ];
+
+    networking.usePredictableInterfaceNames = false;
+  };
+
+  mkLog = logfile: tag: let
+    rotated = map (i: "${logfile}.${toString i}") (range 1 9);
+    all = concatMapStringsSep " " (f: "\"${f}\"") ([logfile] ++ rotated);
+    logcmd = "tail -F ${all} 2> /dev/null | logger -t \"${tag}\"";
+  in if debug then "machine.execute(ru('${logcmd} & disown'))" else "pass";
+
+  testVM = vmName: vmScript: let
+    cfg = (import ../lib/eval-config.nix {
+      system = if use64bitGuest then "x86_64-linux" else "i686-linux";
+      modules = [
+        ../modules/profiles/minimal.nix
+        (testVMConfig vmName vmScript)
+      ];
+    }).config;
+  in pkgs.vmTools.runInLinuxVM (pkgs.runCommand "virtualbox-image" {
+    preVM = ''
+      mkdir -p "$out"
+      diskImage="$(pwd)/qimage"
+      ${pkgs.vmTools.qemu}/bin/qemu-img create -f raw "$diskImage" 100M
+    '';
+
+    postVM = ''
+      echo "creating VirtualBox disk image..."
+      ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -O vdi \
+        "$diskImage" "$out/disk.vdi"
+    '';
+
+    buildInputs = [ pkgs.util-linux pkgs.perl ];
+  } ''
+    ${pkgs.parted}/sbin/parted --script /dev/vda mklabel msdos
+    ${pkgs.parted}/sbin/parted --script /dev/vda -- mkpart primary ext2 1M -1s
+    ${pkgs.e2fsprogs}/sbin/mkfs.ext4 /dev/vda1
+    ${pkgs.e2fsprogs}/sbin/tune2fs -c 0 -i 0 /dev/vda1
+    mkdir /mnt
+    mount /dev/vda1 /mnt
+    cp "${cfg.system.build.kernel}/bzImage" /mnt/linux
+    cp "${cfg.system.build.initialRamdisk}/initrd" /mnt/initrd
+
+    ${pkgs.grub2}/bin/grub-install --boot-directory=/mnt /dev/vda
+
+    cat > /mnt/grub/grub.cfg <<GRUB
+    set root=hd0,1
+    linux /linux ${concatStringsSep " " cfg.boot.kernelParams}
+    initrd /initrd
+    boot
+    GRUB
+    umount /mnt
+  '');
+
+  createVM = name: attrs: let
+    mkFlags = concatStringsSep " ";
+
+    sharePath = "/home/alice/vboxshare-${name}";
+
+    createFlags = mkFlags [
+      "--ostype ${if use64bitGuest then "Linux26_64" else "Linux26"}"
+      "--register"
+    ];
+
+    vmFlags = mkFlags ([
+      "--uart1 0x3F8 4"
+      "--uartmode1 client /run/virtualbox-log-${name}.sock"
+      "--memory 768"
+      "--audio none"
+    ] ++ (attrs.vmFlags or []));
+
+    controllerFlags = mkFlags [
+      "--name SATA"
+      "--add sata"
+      "--bootable on"
+      "--hostiocache on"
+    ];
+
+    diskFlags = mkFlags [
+      "--storagectl SATA"
+      "--port 0"
+      "--device 0"
+      "--type hdd"
+      "--mtype immutable"
+      "--medium ${testVM name attrs}/disk.vdi"
+    ];
+
+    sharedFlags = mkFlags [
+      "--name vboxshare"
+      "--hostpath ${sharePath}"
+    ];
+
+    nixstoreFlags = mkFlags [
+      "--name nixstore"
+      "--hostpath /nix/store"
+      "--readonly"
+    ];
+  in {
+    machine = {
+      systemd.sockets."vboxtestlog-${name}" = {
+        description = "VirtualBox Test Machine Log Socket For ${name}";
+        wantedBy = [ "sockets.target" ];
+        before = [ "multi-user.target" ];
+        socketConfig.ListenStream = "/run/virtualbox-log-${name}.sock";
+        socketConfig.Accept = true;
+      };
+
+      systemd.services."vboxtestlog-${name}@" = {
+        description = "VirtualBox Test Machine Log For ${name}";
+        serviceConfig.StandardInput = "socket";
+        serviceConfig.SyslogIdentifier = "GUEST-${name}";
+        serviceConfig.ExecStart = "${pkgs.coreutils}/bin/cat";
+      };
+    };
+
+    testSubs = ''
+
+
+      ${name}_sharepath = "${sharePath}"
+
+
+      def check_running_${name}():
+          cmd = "VBoxManage list runningvms | grep -q '^\"${name}\"'"
+          (status, _) = machine.execute(ru(cmd))
+          return status == 0
+
+
+      def cleanup_${name}():
+          if check_running_${name}():
+              machine.execute(ru("VBoxManage controlvm ${name} poweroff"))
+          machine.succeed("rm -rf ${sharePath}")
+          machine.succeed("mkdir -p ${sharePath}")
+          machine.succeed("chown alice.users ${sharePath}")
+
+
+      def create_vm_${name}():
+          vbm("createvm --name ${name} ${createFlags}")
+          vbm("modifyvm ${name} ${vmFlags}")
+          vbm("setextradata ${name} VBoxInternal/PDM/HaltOnReset 1")
+          vbm("storagectl ${name} ${controllerFlags}")
+          vbm("storageattach ${name} ${diskFlags}")
+          vbm("sharedfolder add ${name} ${sharedFlags}")
+          vbm("sharedfolder add ${name} ${nixstoreFlags}")
+          cleanup_${name}()
+
+          ${mkLog "$HOME/VirtualBox VMs/${name}/Logs/VBox.log" "HOST-${name}"}
+
+
+      def destroy_vm_${name}():
+          cleanup_${name}()
+          vbm("unregistervm ${name} --delete")
+
+
+      def wait_for_vm_boot_${name}():
+          machine.execute(
+              ru(
+                  "set -e; i=0; "
+                  "while ! test -e ${sharePath}/boot-done; do "
+                  "sleep 10; i=$(($i + 10)); [ $i -le 3600 ]; "
+                  "VBoxManage list runningvms | grep -q '^\"${name}\"'; "
+                  "done"
+              )
+          )
+
+
+      def wait_for_ip_${name}(interface):
+          property = f"/VirtualBox/GuestInfo/Net/{interface}/V4/IP"
+          getip = f"VBoxManage guestproperty get ${name} {property} | sed -n -e 's/^Value: //p'"
+
+          ip = machine.succeed(
+              ru(
+                  "for i in $(seq 1000); do "
+                  f'if ipaddr="$({getip})" && [ -n "$ipaddr" ]; then '
+                  'echo "$ipaddr"; exit 0; '
+                  "fi; "
+                  "sleep 1; "
+                  "done; "
+                  "echo 'Could not get IPv4 address for ${name}!' >&2; "
+                  "exit 1"
+              )
+          ).strip()
+          return ip
+
+
+      def wait_for_startup_${name}(nudge=lambda: None):
+          for _ in range(0, 130, 10):
+              machine.sleep(10)
+              if check_running_${name}():
+                  return
+              nudge()
+          raise Exception("VirtualBox VM didn't start up within 2 minutes")
+
+
+      def wait_for_shutdown_${name}():
+          for _ in range(0, 130, 10):
+              machine.sleep(10)
+              if not check_running_${name}():
+                  return
+          raise Exception("VirtualBox VM didn't shut down within 2 minutes")
+
+
+      def shutdown_vm_${name}():
+          machine.succeed(ru("touch ${sharePath}/shutdown"))
+          machine.execute(
+              "set -e; i=0; "
+              "while test -e ${sharePath}/shutdown "
+              "        -o -e ${sharePath}/boot-done; do "
+              "sleep 1; i=$(($i + 1)); [ $i -le 3600 ]; "
+              "done"
+          )
+          wait_for_shutdown_${name}()
+    '';
+  };
+
+  hostonlyVMFlags = [
+    "--nictype1 virtio"
+    "--nictype2 virtio"
+    "--nic2 hostonly"
+    "--hostonlyadapter2 vboxnet0"
+  ];
+
+  # The VirtualBox Oracle Extension Pack lets you use USB 3.0 (xHCI).
+  enableExtensionPackVMFlags = [
+    "--usbxhci on"
+  ];
+
+  dhcpScript = pkgs: ''
+    ${pkgs.dhcp}/bin/dhclient \
+      -lf /run/dhcp.leases \
+      -pf /run/dhclient.pid \
+      -v eth0 eth1
+
+    otherIP="$(${pkgs.netcat}/bin/nc -l 1234 || :)"
+    ${pkgs.iputils}/bin/ping -I eth1 -c1 "$otherIP"
+    echo "$otherIP reachable" | ${pkgs.netcat}/bin/nc -l 5678 || :
+  '';
+
+  sysdDetectVirt = pkgs: ''
+    ${pkgs.systemd}/bin/systemd-detect-virt > /mnt-root/result
+  '';
+
+  vboxVMs = mapAttrs createVM {
+    simple = {};
+
+    detectvirt.vmScript = sysdDetectVirt;
+
+    test1.vmFlags = hostonlyVMFlags;
+    test1.vmScript = dhcpScript;
+
+    test2.vmFlags = hostonlyVMFlags;
+    test2.vmScript = dhcpScript;
+
+    headless.virtualisation.virtualbox.headless = true;
+    headless.services.xserver.enable = false;
+  };
+
+  vboxVMsWithExtpack = mapAttrs createVM {
+    testExtensionPack.vmFlags = enableExtensionPackVMFlags;
+  };
+
+  mkVBoxTest = useExtensionPack: vms: name: testScript: makeTest {
+    name = "virtualbox-${name}";
+
+    machine = { lib, config, ... }: {
+      imports = let
+        mkVMConf = name: val: val.machine // { key = "${name}-config"; };
+        vmConfigs = mapAttrsToList mkVMConf vms;
+      in [ ./common/user-account.nix ./common/x11.nix ] ++ vmConfigs;
+      virtualisation.memorySize = 2048;
+      virtualisation.qemu.options =
+        if useKvmNestedVirt then ["-cpu" "kvm64,vmx=on"] else [];
+      virtualisation.virtualbox.host.enable = true;
+      test-support.displayManager.auto.user = "alice";
+      users.users.alice.extraGroups = let
+        inherit (config.virtualisation.virtualbox.host) enableHardening;
+      in lib.mkIf enableHardening (lib.singleton "vboxusers");
+      virtualisation.virtualbox.host.enableExtensionPack = useExtensionPack;
+      nixpkgs.config.allowUnfree = useExtensionPack;
+    };
+
+    testScript = ''
+      from shlex import quote
+      ${concatStrings (mapAttrsToList (_: getAttr "testSubs") vms)}
+
+      def ru(cmd: str) -> str:
+          return f"su - alice -c {quote(cmd)}"
+
+
+      def vbm(cmd: str) -> str:
+          return machine.succeed(ru(f"VBoxManage {cmd}"))
+
+
+      def remove_uuids(output: str) -> str:
+          return "\n".join(
+              [line for line in (output or "").splitlines() if not line.startswith("UUID:")]
+          )
+
+
+      machine.wait_for_x()
+
+      ${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"}
+
+      ${testScript}
+      # (keep black happy)
+    '';
+
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ aszlig cdepillabout ];
+    };
+  };
+
+  unfreeTests = mapAttrs (mkVBoxTest true vboxVMsWithExtpack) {
+    enable-extension-pack = ''
+      create_vm_testExtensionPack()
+      vbm("startvm testExtensionPack")
+      wait_for_startup_testExtensionPack()
+      machine.screenshot("cli_started")
+      wait_for_vm_boot_testExtensionPack()
+      machine.screenshot("cli_booted")
+
+      with machine.nested("Checking for privilege escalation"):
+          machine.fail("test -e '/root/VirtualBox VMs'")
+          machine.fail("test -e '/root/.config/VirtualBox'")
+          machine.succeed("test -e '/home/alice/VirtualBox VMs'")
+
+      shutdown_vm_testExtensionPack()
+      destroy_vm_testExtensionPack()
+    '';
+  };
+
+in mapAttrs (mkVBoxTest false vboxVMs) {
+  simple-gui = ''
+    # Home to select Tools, down to move to the VM, enter to start it.
+    def send_vm_startup():
+        machine.send_key("home")
+        machine.send_key("down")
+        machine.send_key("ret")
+
+
+    create_vm_simple()
+    machine.succeed(ru("VirtualBox >&2 &"))
+    machine.wait_until_succeeds(ru("xprop -name 'Oracle VM VirtualBox Manager'"))
+    machine.sleep(5)
+    machine.screenshot("gui_manager_started")
+    send_vm_startup()
+    machine.screenshot("gui_manager_sent_startup")
+    wait_for_startup_simple(send_vm_startup)
+    machine.screenshot("gui_started")
+    wait_for_vm_boot_simple()
+    machine.screenshot("gui_booted")
+    shutdown_vm_simple()
+    machine.sleep(5)
+    machine.screenshot("gui_stopped")
+    machine.send_key("ctrl-q")
+    machine.sleep(5)
+    machine.screenshot("gui_manager_stopped")
+    destroy_vm_simple()
+  '';
+
+  simple-cli = ''
+    create_vm_simple()
+    vbm("startvm simple")
+    wait_for_startup_simple()
+    machine.screenshot("cli_started")
+    wait_for_vm_boot_simple()
+    machine.screenshot("cli_booted")
+
+    with machine.nested("Checking for privilege escalation"):
+        machine.fail("test -e '/root/VirtualBox VMs'")
+        machine.fail("test -e '/root/.config/VirtualBox'")
+        machine.succeed("test -e '/home/alice/VirtualBox VMs'")
+
+    shutdown_vm_simple()
+    destroy_vm_simple()
+  '';
+
+  headless = ''
+    create_vm_headless()
+    machine.succeed(ru("VBoxHeadless --startvm headless & disown %1"))
+    wait_for_startup_headless()
+    wait_for_vm_boot_headless()
+    shutdown_vm_headless()
+    destroy_vm_headless()
+  '';
+
+  host-usb-permissions = ''
+    user_usb = remove_uuids(vbm("list usbhost"))
+    print(user_usb, file=sys.stderr)
+    root_usb = remove_uuids(machine.succeed("VBoxManage list usbhost"))
+    print(root_usb, file=sys.stderr)
+
+    if user_usb != root_usb:
+        raise Exception("USB host devices differ for root and normal user")
+    if "<none>" in user_usb:
+        raise Exception("No USB host devices found")
+  '';
+
+  systemd-detect-virt = ''
+    create_vm_detectvirt()
+    vbm("startvm detectvirt")
+    wait_for_startup_detectvirt()
+    wait_for_vm_boot_detectvirt()
+    shutdown_vm_detectvirt()
+    result = machine.succeed(f"cat '{detectvirt_sharepath}/result'").strip()
+    destroy_vm_detectvirt()
+    if result != "oracle":
+        raise Exception(f'systemd-detect-virt returned "{result}" instead of "oracle"')
+  '';
+
+  net-hostonlyif = ''
+    create_vm_test1()
+    create_vm_test2()
+
+    vbm("startvm test1")
+    wait_for_startup_test1()
+    wait_for_vm_boot_test1()
+
+    vbm("startvm test2")
+    wait_for_startup_test2()
+    wait_for_vm_boot_test2()
+
+    machine.screenshot("net_booted")
+
+    test1_ip = wait_for_ip_test1(1)
+    test2_ip = wait_for_ip_test2(1)
+
+    machine.succeed(f"echo '{test2_ip}' | nc -N '{test1_ip}' 1234")
+    machine.succeed(f"echo '{test1_ip}' | nc -N '{test2_ip}' 1234")
+
+    machine.wait_until_succeeds(f"nc -N '{test1_ip}' 5678 < /dev/null >&2")
+    machine.wait_until_succeeds(f"nc -N '{test2_ip}' 5678 < /dev/null >&2")
+
+    shutdown_vm_test1()
+    shutdown_vm_test2()
+
+    destroy_vm_test1()
+    destroy_vm_test2()
+  '';
+} // (if enableUnfree then unfreeTests else {})
diff --git a/nixos/tests/vscodium.nix b/nixos/tests/vscodium.nix
new file mode 100644
index 00000000000..688ddfe07e3
--- /dev/null
+++ b/nixos/tests/vscodium.nix
@@ -0,0 +1,78 @@
+let
+  tests = {
+    wayland = { pkgs, ... }: {
+      imports = [ ./common/wayland-cage.nix ];
+
+      services.cage.program = "${pkgs.vscodium}/bin/codium";
+
+      environment.variables.NIXOS_OZONE_WL = "1";
+      environment.variables.DISPLAY = "do not use";
+
+      fonts.fonts = with pkgs; [ dejavu_fonts ];
+    };
+    xorg = { pkgs, ... }: {
+      imports = [ ./common/user-account.nix ./common/x11.nix ];
+
+      virtualisation.memorySize = 2047;
+      services.xserver.enable = true;
+      services.xserver.displayManager.sessionCommands = ''
+        ${pkgs.vscodium}/bin/codium
+      '';
+      test-support.displayManager.auto.user = "alice";
+    };
+  };
+
+  mkTest = name: machine:
+    import ./make-test-python.nix ({ pkgs, ... }: {
+      inherit name;
+
+      nodes = { "${name}" = machine; };
+
+      meta = with pkgs.lib.maintainers; {
+        maintainers = [ synthetica turion ];
+      };
+      enableOCR = true;
+      testScript = ''
+        @polling_condition
+        def codium_running():
+            machine.succeed('pgrep -x codium')
+
+
+        start_all()
+
+        machine.wait_for_unit('graphical.target')
+        machine.wait_until_succeeds('pgrep -x codium')
+
+        with codium_running:
+            # Wait until vscodium is visible. "File" is in the menu bar.
+            machine.wait_for_text('Get Started')
+            machine.screenshot('start_screen')
+
+            test_string = 'testfile'
+
+            # Create a new file
+            machine.send_key('ctrl-n')
+            machine.wait_for_text('Untitled')
+            machine.screenshot('empty_editor')
+
+            # Type a string
+            machine.send_chars(test_string)
+            machine.wait_for_text(test_string)
+            machine.screenshot('editor')
+
+            # Save the file
+            machine.send_key('ctrl-s')
+            machine.wait_for_text('Save')
+            machine.screenshot('save_window')
+            machine.send_key('ret')
+
+            # (the default filename is the first line of the file)
+            machine.wait_for_file(f'/home/alice/{test_string}')
+
+        machine.send_key('ctrl-q')
+        machine.wait_until_fails('pgrep -x codium')
+      '';
+    });
+
+in
+builtins.mapAttrs (k: v: mkTest k v { }) tests
diff --git a/nixos/tests/vsftpd.nix b/nixos/tests/vsftpd.nix
new file mode 100644
index 00000000000..4bea27f0eb1
--- /dev/null
+++ b/nixos/tests/vsftpd.nix
@@ -0,0 +1,42 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "vsftpd";
+
+  nodes = {
+    server = {
+      services.vsftpd = {
+        enable = true;
+        userlistDeny = false;
+        localUsers = true;
+        userlist = [ "ftp-test-user" ];
+        writeEnable = true;
+        localRoot = "/tmp";
+      };
+      networking.firewall.enable = false;
+
+      users = {
+        users.ftp-test-user = {
+          isSystemUser = true;
+          password = "ftp-test-password";
+          group = "ftp-test-group";
+        };
+        groups.ftp-test-group = {};
+      };
+    };
+
+    client = {};
+  };
+
+  testScript = ''
+    client.start()
+    server.wait_for_unit("vsftpd")
+    server.wait_for_open_port("21")
+
+    client.succeed("curl -u ftp-test-user:ftp-test-password ftp://server")
+    client.succeed('echo "this is a test" > /tmp/test.file.up')
+    client.succeed("curl -v -T /tmp/test.file.up -u ftp-test-user:ftp-test-password ftp://server")
+    client.succeed("curl -u ftp-test-user:ftp-test-password ftp://server/test.file.up > /tmp/test.file.down")
+    client.succeed("diff /tmp/test.file.up /tmp/test.file.down")
+    assert client.succeed("cat /tmp/test.file.up") == server.succeed("cat /tmp/test.file.up")
+    assert client.succeed("cat /tmp/test.file.down") == server.succeed("cat /tmp/test.file.up")
+  '';
+})
diff --git a/nixos/tests/wasabibackend.nix b/nixos/tests/wasabibackend.nix
new file mode 100644
index 00000000000..75730fe24d0
--- /dev/null
+++ b/nixos/tests/wasabibackend.nix
@@ -0,0 +1,38 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "wasabibackend";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ mmahut ];
+  };
+
+  nodes = {
+    machine = { ... }: {
+      services.wasabibackend = {
+        enable = true;
+        network = "testnet";
+        rpc = {
+          user = "alice";
+          port = 18332;
+        };
+      };
+      services.bitcoind."testnet" = {
+        enable = true;
+        testnet = true;
+        rpc.users = {
+          alice.passwordHMAC = "e7096bc21da60b29ecdbfcdb2c3acc62$f948e61cb587c399358ed99c6ed245a41460b4bf75125d8330c9f6fcc13d7ae7";
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("wasabibackend.service")
+    machine.wait_until_succeeds(
+        "grep 'Wasabi Backend started' /var/lib/wasabibackend/.walletwasabi/backend/Logs.txt"
+    )
+    machine.sleep(5)
+    machine.succeed(
+        "grep 'Config is successfully initialized' /var/lib/wasabibackend/.walletwasabi/backend/Logs.txt"
+    )
+  '';
+})
diff --git a/nixos/tests/web-apps/mastodon.nix b/nixos/tests/web-apps/mastodon.nix
new file mode 100644
index 00000000000..279a1c59169
--- /dev/null
+++ b/nixos/tests/web-apps/mastodon.nix
@@ -0,0 +1,170 @@
+import ../make-test-python.nix ({pkgs, ...}:
+let
+  test-certificates = pkgs.runCommandLocal "test-certificates" { } ''
+    mkdir -p $out
+    echo insecure-root-password > $out/root-password-file
+    echo insecure-intermediate-password > $out/intermediate-password-file
+    ${pkgs.step-cli}/bin/step certificate create "Example Root CA" $out/root_ca.crt $out/root_ca.key --password-file=$out/root-password-file --profile root-ca
+    ${pkgs.step-cli}/bin/step certificate create "Example Intermediate CA 1" $out/intermediate_ca.crt $out/intermediate_ca.key --password-file=$out/intermediate-password-file --ca-password-file=$out/root-password-file --profile intermediate-ca --ca $out/root_ca.crt --ca-key $out/root_ca.key
+  '';
+
+  hosts = ''
+    192.168.2.10 ca.local
+    192.168.2.11 mastodon.local
+  '';
+
+in
+{
+  name = "mastodon";
+  meta.maintainers = with pkgs.lib.maintainers; [ erictapen izorkin ];
+
+  nodes = {
+    ca = { pkgs, ... }: {
+      networking = {
+        interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.10"; prefixLength = 24; }
+          ];
+        };
+        extraHosts = hosts;
+      };
+      services.step-ca = {
+        enable = true;
+        address = "0.0.0.0";
+        port = 8443;
+        openFirewall = true;
+        intermediatePasswordFile = "${test-certificates}/intermediate-password-file";
+        settings = {
+          dnsNames = [ "ca.local" ];
+          root = "${test-certificates}/root_ca.crt";
+          crt = "${test-certificates}/intermediate_ca.crt";
+          key = "${test-certificates}/intermediate_ca.key";
+          db = {
+            type = "badger";
+            dataSource = "/var/lib/step-ca/db";
+          };
+          authority = {
+            provisioners = [
+              {
+                type = "ACME";
+                name = "acme";
+              }
+            ];
+          };
+        };
+      };
+    };
+
+    server = { pkgs, ... }: {
+      networking = {
+        interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.11"; prefixLength = 24; }
+          ];
+        };
+        extraHosts = hosts;
+        firewall.allowedTCPPorts = [ 80 443 ];
+      };
+
+      security = {
+        acme = {
+          acceptTerms = true;
+          defaults.server = "https://ca.local:8443/acme/acme/directory";
+          defaults.email = "mastodon@mastodon.local";
+        };
+        pki.certificateFiles = [ "${test-certificates}/root_ca.crt" ];
+      };
+
+      services.redis.servers.mastodon = {
+        enable = true;
+        bind = "127.0.0.1";
+        port = 31637;
+      };
+
+      services.mastodon = {
+        enable = true;
+        configureNginx = true;
+        localDomain = "mastodon.local";
+        enableUnixSocket = false;
+        redis = {
+          createLocally = true;
+          host = "127.0.0.1";
+          port = 31637;
+        };
+        database = {
+          createLocally = true;
+          host = "/run/postgresql";
+          port = 5432;
+        };
+        smtp = {
+          createLocally = false;
+          fromAddress = "mastodon@mastodon.local";
+        };
+        extraConfig = {
+          EMAIL_DOMAIN_ALLOWLIST = "example.com";
+        };
+      };
+    };
+
+    client = { pkgs, ... }: {
+      environment.systemPackages = [ pkgs.jq ];
+      networking = {
+        interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.12"; prefixLength = 24; }
+          ];
+        };
+        extraHosts = hosts;
+      };
+
+      security = {
+        pki.certificateFiles = [ "${test-certificates}/root_ca.crt" ];
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    ca.wait_for_unit("step-ca.service")
+    ca.wait_for_open_port(8443)
+
+    server.wait_for_unit("nginx.service")
+    server.wait_for_unit("redis-mastodon.service")
+    server.wait_for_unit("postgresql.service")
+    server.wait_for_unit("mastodon-sidekiq.service")
+    server.wait_for_unit("mastodon-streaming.service")
+    server.wait_for_unit("mastodon-web.service")
+    server.wait_for_open_port(55000)
+    server.wait_for_open_port(55001)
+
+    # Check Mastodon version from remote client
+    client.succeed("curl --fail https://mastodon.local/api/v1/instance | jq -r '.version' | grep '${pkgs.mastodon.version}'")
+
+    # Check using admin CLI
+    # Check Mastodon version
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl version' | grep '${pkgs.mastodon.version}'")
+
+    # Manage accounts
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl email_domain_blocks add example.com'")
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl email_domain_blocks list' | grep 'example.com'")
+    server.fail("su - mastodon -s /bin/sh -c 'mastodon-env tootctl email_domain_blocks list' | grep 'mastodon.local'")
+    server.fail("su - mastodon -s /bin/sh -c 'mastodon-env tootctl accounts create alice --email=alice@example.com'")
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl email_domain_blocks remove example.com'")
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl accounts create bob --email=bob@example.com'")
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl accounts approve bob'")
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl accounts delete bob'")
+
+    # Manage IP access
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl ip_blocks add 192.168.0.0/16 --severity=no_access'")
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl ip_blocks export' | grep '192.168.0.0/16'")
+    server.fail("su - mastodon -s /bin/sh -c 'mastodon-env tootctl p_blocks export' | grep '172.16.0.0/16'")
+    client.fail("curl --fail https://mastodon.local/about")
+    server.succeed("su - mastodon -s /bin/sh -c 'mastodon-env tootctl ip_blocks remove 192.168.0.0/16'")
+    client.succeed("curl --fail https://mastodon.local/about")
+
+    ca.shutdown()
+    server.shutdown()
+    client.shutdown()
+  '';
+})
diff --git a/nixos/tests/web-apps/peertube.nix b/nixos/tests/web-apps/peertube.nix
new file mode 100644
index 00000000000..706c598338e
--- /dev/null
+++ b/nixos/tests/web-apps/peertube.nix
@@ -0,0 +1,130 @@
+import ../make-test-python.nix ({pkgs, ...}:
+{
+  name = "peertube";
+  meta.maintainers = with pkgs.lib.maintainers; [ izorkin ];
+
+  nodes = {
+    database = {
+      networking = {
+       interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.10"; prefixLength = 24; }
+          ];
+        };
+        firewall.allowedTCPPorts = [ 5432 6379 ];
+      };
+
+      services.postgresql = {
+        enable = true;
+        enableTCPIP = true;
+        authentication = ''
+          hostnossl peertube_local peertube_test 192.168.2.11/32 md5
+        '';
+        initialScript = pkgs.writeText "postgresql_init.sql" ''
+          CREATE ROLE peertube_test LOGIN PASSWORD '0gUN0C1mgST6czvjZ8T9';
+          CREATE DATABASE peertube_local TEMPLATE template0 ENCODING UTF8;
+          GRANT ALL PRIVILEGES ON DATABASE peertube_local TO peertube_test;
+          \connect peertube_local
+          CREATE EXTENSION IF NOT EXISTS pg_trgm;
+          CREATE EXTENSION IF NOT EXISTS unaccent;
+        '';
+      };
+
+      services.redis = {
+        enable = true;
+        bind = "0.0.0.0";
+        requirePass = "turrQfaQwnanGbcsdhxy";
+      };
+    };
+
+    server = { pkgs, ... }: {
+      environment = {
+        etc = {
+          "peertube/password-posgressql-db".text = ''
+            0gUN0C1mgST6czvjZ8T9
+          '';
+          "peertube/password-redis-db".text = ''
+            turrQfaQwnanGbcsdhxy
+          '';
+        };
+      };
+
+      networking = {
+        interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.11"; prefixLength = 24; }
+          ];
+        };
+        extraHosts = ''
+          192.168.2.11 peertube.local
+        '';
+        firewall.allowedTCPPorts = [ 9000 ];
+      };
+
+      services.peertube = {
+        enable = true;
+        localDomain = "peertube.local";
+        enableWebHttps = false;
+
+        database = {
+          host = "192.168.2.10";
+          name = "peertube_local";
+          user = "peertube_test";
+          passwordFile = "/etc/peertube/password-posgressql-db";
+        };
+
+        redis = {
+          host = "192.168.2.10";
+          passwordFile = "/etc/peertube/password-redis-db";
+        };
+
+        settings = {
+          listen = {
+            hostname = "0.0.0.0";
+          };
+          instance = {
+            name = "PeerTube Test Server";
+          };
+        };
+      };
+    };
+
+    client = {
+      environment.systemPackages = [ pkgs.jq ];
+      networking = {
+       interfaces.eth1 = {
+          ipv4.addresses = [
+            { address = "192.168.2.12"; prefixLength = 24; }
+          ];
+        };
+        extraHosts = ''
+          192.168.2.11 peertube.local
+        '';
+      };
+    };
+
+  };
+
+  testScript = ''
+    start_all()
+
+    database.wait_for_unit("postgresql.service")
+    database.wait_for_unit("redis.service")
+
+    database.wait_for_open_port(5432)
+    database.wait_for_open_port(6379)
+
+    server.wait_for_unit("peertube.service")
+    server.wait_for_open_port(9000)
+
+    # Check if PeerTube is running
+    client.succeed("curl --fail http://peertube.local:9000/api/v1/config/about | jq -r '.instance.name' | grep 'PeerTube\ Test\ Server'")
+
+    # Check PeerTube CLI version
+    assert "${pkgs.peertube.version}" in server.succeed('su - peertube -s /bin/sh -c "peertube --version"')
+
+    client.shutdown()
+    server.shutdown()
+    database.shutdown()
+  '';
+})
diff --git a/nixos/tests/web-servers/agate.nix b/nixos/tests/web-servers/agate.nix
new file mode 100644
index 00000000000..e364e134cfd
--- /dev/null
+++ b/nixos/tests/web-servers/agate.nix
@@ -0,0 +1,29 @@
+import ../make-test-python.nix (
+  { pkgs, lib, ... }:
+  {
+    name = "agate";
+    meta = with lib.maintainers; { maintainers = [ jk ]; };
+
+    nodes = {
+      geminiserver = { pkgs, ... }: {
+        services.agate = {
+          enable = true;
+          hostnames = [ "localhost" ];
+          contentDir = pkgs.writeTextDir "index.gmi" ''
+            # Hello NixOS!
+          '';
+        };
+      };
+    };
+
+    testScript = { nodes, ... }: ''
+      geminiserver.wait_for_unit("agate")
+      geminiserver.wait_for_open_port(1965)
+
+      with subtest("check is serving over gemini"):
+        response = geminiserver.succeed("${pkgs.gmni}/bin/gmni -j once -i -N gemini://localhost:1965")
+        print(response)
+        assert "Hello NixOS!" in response
+    '';
+  }
+)
diff --git a/nixos/tests/web-servers/unit-php.nix b/nixos/tests/web-servers/unit-php.nix
new file mode 100644
index 00000000000..00512b506cc
--- /dev/null
+++ b/nixos/tests/web-servers/unit-php.nix
@@ -0,0 +1,47 @@
+import ../make-test-python.nix ({pkgs, ...}:
+let
+  testdir = pkgs.writeTextDir "www/info.php" "<?php phpinfo();";
+
+in {
+  name = "unit-php-test";
+  meta.maintainers = with pkgs.lib.maintainers; [ izorkin ];
+
+  machine = { config, lib, pkgs, ... }: {
+    services.unit = {
+      enable = true;
+      config = pkgs.lib.strings.toJSON {
+        listeners."*:9080".application = "php_80";
+        applications.php_80 = {
+          type = "php 8.0";
+          processes = 1;
+          user = "testuser";
+          group = "testgroup";
+          root = "${testdir}/www";
+          index = "info.php";
+          options.file = "${pkgs.unit.usedPhp80}/lib/php.ini";
+        };
+      };
+    };
+    users = {
+      users.testuser = {
+        isSystemUser = true;
+        uid = 1080;
+        group = "testgroup";
+      };
+      groups.testgroup = {
+        gid = 1080;
+      };
+    };
+  };
+  testScript = ''
+    machine.wait_for_unit("unit.service")
+
+    # Check so we get an evaluated PHP back
+    response = machine.succeed("curl -f -vvv -s http://127.0.0.1:9080/")
+    assert "PHP Version ${pkgs.unit.usedPhp80.version}" in response, "PHP version not detected"
+
+    # Check so we have database and some other extensions loaded
+    for ext in ["json", "opcache", "pdo_mysql", "pdo_pgsql", "pdo_sqlite"]:
+        assert ext in response, f"Missing {ext} extension"
+  '';
+})
diff --git a/nixos/tests/wiki-js.nix b/nixos/tests/wiki-js.nix
new file mode 100644
index 00000000000..783887d2dca
--- /dev/null
+++ b/nixos/tests/wiki-js.nix
@@ -0,0 +1,152 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} : {
+  name = "wiki-js";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ma27 ];
+  };
+
+  machine = { pkgs, ... }: {
+    virtualisation.memorySize = 2048;
+    services.wiki-js = {
+      enable = true;
+      settings.db.host = "/run/postgresql";
+      settings.db.user = "wiki-js";
+      settings.logLevel = "debug";
+    };
+    services.postgresql = {
+      enable = true;
+      ensureDatabases = [ "wiki" ];
+      ensureUsers = [
+        { name = "wiki-js";
+          ensurePermissions."DATABASE wiki" = "ALL PRIVILEGES";
+        }
+      ];
+    };
+    systemd.services.wiki-js = {
+      requires = [ "postgresql.service" ];
+      after = [ "postgresql.service" ];
+    };
+    environment.systemPackages = with pkgs; [ jq ];
+  };
+
+  testScript = let
+    payloads.finalize = pkgs.writeText "finalize.json" (builtins.toJSON {
+      adminEmail = "webmaster@example.com";
+      adminPassword = "notapassword";
+      adminPasswordConfirm = "notapassword";
+      siteUrl = "http://localhost:3000";
+      telemetry = false;
+    });
+    payloads.login = pkgs.writeText "login.json" (builtins.toJSON [{
+      operationName = null;
+      extensions = {};
+      query = ''
+        mutation ($username: String!, $password: String!, $strategy: String!) {
+          authentication {
+            login(username: $username, password: $password, strategy: $strategy) {
+              responseResult {
+                succeeded
+                errorCode
+                slug
+                message
+                __typename
+              }
+              jwt
+              mustChangePwd
+              mustProvideTFA
+              mustSetupTFA
+              continuationToken
+              redirect
+              tfaQRImage
+              __typename
+            }
+            __typename
+          }
+        }
+      '';
+      variables = {
+        password = "notapassword";
+        strategy = "local";
+        username = "webmaster@example.com";
+      };
+    }]);
+    payloads.content = pkgs.writeText "content.json" (builtins.toJSON [{
+      extensions = {};
+      operationName = null;
+      query = ''
+        mutation ($content: String!, $description: String!, $editor: String!, $isPrivate: Boolean!, $isPublished: Boolean!, $locale: String!, $path: String!, $publishEndDate: Date, $publishStartDate: Date, $scriptCss: String, $scriptJs: String, $tags: [String]!, $title: String!) {
+          pages {
+            create(content: $content, description: $description, editor: $editor, isPrivate: $isPrivate, isPublished: $isPublished, locale: $locale, path: $path, publishEndDate: $publishEndDate, publishStartDate: $publishStartDate, scriptCss: $scriptCss, scriptJs: $scriptJs, tags: $tags, title: $title) {
+              responseResult {
+                succeeded
+                errorCode
+                slug
+                message
+                __typename
+              }
+              page {
+                id
+                updatedAt
+                __typename
+              }
+              __typename
+            }
+            __typename
+          }
+        }
+      '';
+      variables = {
+        content = "# Header\n\nHello world!";
+        description = "";
+        editor = "markdown";
+        isPrivate = false;
+        isPublished = true;
+        locale = "en";
+        path = "home";
+        publishEndDate = "";
+        publishStartDate = "";
+        scriptCss = "";
+        scriptJs = "";
+        tags = [];
+        title = "Hello world";
+      };
+    }]);
+  in ''
+    machine.start()
+    machine.wait_for_unit("multi-user.target")
+    machine.wait_for_open_port(3000)
+
+    machine.succeed("curl -sSf localhost:3000")
+
+    with subtest("Setup"):
+        result = machine.succeed(
+            "curl -sSf localhost:3000/finalize -X POST -d "
+            + "@${payloads.finalize} -H 'Content-Type: application/json' "
+            + "| jq .ok | xargs echo"
+        )
+        assert result.strip() == "true", f"Expected true, got {result}"
+
+        # During the setup the service gets restarted, so we use this
+        # to check if the setup is done.
+        machine.wait_until_fails("curl -sSf localhost:3000")
+        machine.wait_until_succeeds("curl -sSf localhost:3000")
+
+    with subtest("Base functionality"):
+        auth = machine.succeed(
+            "curl -sSf localhost:3000/graphql -X POST "
+            + "-d @${payloads.login} -H 'Content-Type: application/json' "
+            + "| jq '.[0].data.authentication.login.jwt' | xargs echo"
+        ).strip()
+
+        assert auth
+
+        create = machine.succeed(
+            "curl -sSf localhost:3000/graphql -X POST "
+            + "-d @${payloads.content} -H 'Content-Type: application/json' "
+            + f"-H 'Authorization: Bearer {auth}' "
+            + "| jq '.[0].data.pages.create.responseResult.succeeded'|xargs echo"
+        )
+        assert create.strip() == "true", f"Expected true, got {create}"
+
+    machine.shutdown()
+  '';
+})
diff --git a/nixos/tests/wine.nix b/nixos/tests/wine.nix
new file mode 100644
index 00000000000..8135cb90a59
--- /dev/null
+++ b/nixos/tests/wine.nix
@@ -0,0 +1,48 @@
+{ system ? builtins.currentSystem
+, pkgs ? import ../.. { inherit system; config = { }; }
+}:
+
+let
+  inherit (pkgs.lib) concatMapStrings listToAttrs optionals optionalString;
+  inherit (import ../lib/testing-python.nix { inherit system pkgs; }) makeTest;
+
+  hello32 = "${pkgs.pkgsCross.mingw32.hello}/bin/hello.exe";
+  hello64 = "${pkgs.pkgsCross.mingwW64.hello}/bin/hello.exe";
+
+  makeWineTest = packageSet: exes: variant: rec {
+    name = "${packageSet}-${variant}";
+    value = makeTest {
+      inherit name;
+      meta = with pkgs.lib.maintainers; { maintainers = [ chkno ]; };
+
+      machine = { pkgs, ... }: {
+        environment.systemPackages = [ pkgs."${packageSet}"."${variant}" ];
+        virtualisation.diskSize = 800;
+      };
+
+      testScript = ''
+        machine.wait_for_unit("multi-user.target")
+        ${concatMapStrings (exe: ''
+          greeting = machine.succeed(
+              "bash -c 'wine ${exe} 2> >(tee wine-stderr >&2)'"
+          )
+          assert 'Hello, world!' in greeting
+        ''
+        # only the full version contains Gecko, but the error is not printed reliably in other variants
+        + optionalString (variant == "full") ''
+          machine.fail(
+              "fgrep 'Could not find Wine Gecko. HTML rendering will be disabled.' wine-stderr"
+          )
+        '') exes}
+      '';
+    };
+  };
+
+  variants = [ "base" "full" "minimal" "staging" "unstable" "wayland" ];
+
+in
+listToAttrs (
+  map (makeWineTest "winePackages" [ hello32 ]) variants
+  ++ optionals pkgs.stdenv.is64bit
+    (map (makeWineTest "wineWowPackages" [ hello32 hello64 ]) variants)
+)
diff --git a/nixos/tests/wireguard/basic.nix b/nixos/tests/wireguard/basic.nix
new file mode 100644
index 00000000000..36ab226cde0
--- /dev/null
+++ b/nixos/tests/wireguard/basic.nix
@@ -0,0 +1,74 @@
+{ kernelPackages ? null }:
+import ../make-test-python.nix ({ pkgs, lib, ...} :
+  let
+    wg-snakeoil-keys = import ./snakeoil-keys.nix;
+    peer = (import ./make-peer.nix) { inherit lib; };
+  in
+  {
+    name = "wireguard";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ ma27 ];
+    };
+
+    nodes = {
+      peer0 = peer {
+        ip4 = "192.168.0.1";
+        ip6 = "fd00::1";
+        extraConfig = {
+          boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
+          networking.firewall.allowedUDPPorts = [ 23542 ];
+          networking.wireguard.interfaces.wg0 = {
+            ips = [ "10.23.42.1/32" "fc00::1/128" ];
+            listenPort = 23542;
+
+            inherit (wg-snakeoil-keys.peer0) privateKey;
+
+            peers = lib.singleton {
+              allowedIPs = [ "10.23.42.2/32" "fc00::2/128" ];
+
+              inherit (wg-snakeoil-keys.peer1) publicKey;
+            };
+          };
+        };
+      };
+
+      peer1 = peer {
+        ip4 = "192.168.0.2";
+        ip6 = "fd00::2";
+        extraConfig = {
+          boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
+          networking.wireguard.interfaces.wg0 = {
+            ips = [ "10.23.42.2/32" "fc00::2/128" ];
+            listenPort = 23542;
+            allowedIPsAsRoutes = false;
+
+            inherit (wg-snakeoil-keys.peer1) privateKey;
+
+            peers = lib.singleton {
+              allowedIPs = [ "0.0.0.0/0" "::/0" ];
+              endpoint = "192.168.0.1:23542";
+              persistentKeepalive = 25;
+
+              inherit (wg-snakeoil-keys.peer0) publicKey;
+            };
+
+            postSetup = let inherit (pkgs) iproute2; in ''
+              ${iproute2}/bin/ip route replace 10.23.42.1/32 dev wg0
+              ${iproute2}/bin/ip route replace fc00::1/128 dev wg0
+            '';
+          };
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      peer0.wait_for_unit("wireguard-wg0.service")
+      peer1.wait_for_unit("wireguard-wg0.service")
+
+      peer1.succeed("ping -c5 fc00::1")
+      peer1.succeed("ping -c5 10.23.42.1")
+    '';
+  }
+)
diff --git a/nixos/tests/wireguard/default.nix b/nixos/tests/wireguard/default.nix
new file mode 100644
index 00000000000..dedb321ff2e
--- /dev/null
+++ b/nixos/tests/wireguard/default.nix
@@ -0,0 +1,27 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../../.. { inherit system config; }
+, kernelVersionsToTest ? [ "5.4" "latest" ]
+}:
+
+with pkgs.lib;
+
+let
+  tests = let callTest = p: flip (import p) { inherit system pkgs; }; in {
+    basic = callTest ./basic.nix;
+    namespaces = callTest ./namespaces.nix;
+    wg-quick = callTest ./wg-quick.nix;
+    generated = callTest ./generated.nix;
+  };
+in
+
+listToAttrs (
+  flip concatMap kernelVersionsToTest (version:
+    let
+      v' = replaceStrings [ "." ] [ "_" ] version;
+    in
+    flip mapAttrsToList tests (name: test:
+      nameValuePair "wireguard-${name}-linux-${v'}" (test { kernelPackages = pkgs."linuxPackages_${v'}"; })
+    )
+  )
+)
diff --git a/nixos/tests/wireguard/generated.nix b/nixos/tests/wireguard/generated.nix
new file mode 100644
index 00000000000..84a35d29b45
--- /dev/null
+++ b/nixos/tests/wireguard/generated.nix
@@ -0,0 +1,64 @@
+{ kernelPackages ? null }:
+import ../make-test-python.nix ({ pkgs, lib, ... } : {
+  name = "wireguard-generated";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ma27 grahamc ];
+  };
+
+  nodes = {
+    peer1 = {
+      boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
+      networking.firewall.allowedUDPPorts = [ 12345 ];
+      networking.wireguard.interfaces.wg0 = {
+        ips = [ "10.10.10.1/24" ];
+        listenPort = 12345;
+        privateKeyFile = "/etc/wireguard/private";
+        generatePrivateKeyFile = true;
+
+      };
+    };
+
+    peer2 = {
+      boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
+      networking.firewall.allowedUDPPorts = [ 12345 ];
+      networking.wireguard.interfaces.wg0 = {
+        ips = [ "10.10.10.2/24" ];
+        listenPort = 12345;
+        privateKeyFile = "/etc/wireguard/private";
+        generatePrivateKeyFile = true;
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    peer1.wait_for_unit("wireguard-wg0.service")
+    peer2.wait_for_unit("wireguard-wg0.service")
+
+    retcode, peer1pubkey = peer1.execute("wg pubkey < /etc/wireguard/private")
+    if retcode != 0:
+        raise Exception("Could not read public key from peer1")
+
+    retcode, peer2pubkey = peer2.execute("wg pubkey < /etc/wireguard/private")
+    if retcode != 0:
+        raise Exception("Could not read public key from peer2")
+
+    peer1.succeed(
+        "wg set wg0 peer {} allowed-ips 10.10.10.2/32 endpoint 192.168.1.2:12345 persistent-keepalive 1".format(
+            peer2pubkey.strip()
+        )
+    )
+    peer1.succeed("ip route replace 10.10.10.2/32 dev wg0 table main")
+
+    peer2.succeed(
+        "wg set wg0 peer {} allowed-ips 10.10.10.1/32 endpoint 192.168.1.1:12345 persistent-keepalive 1".format(
+            peer1pubkey.strip()
+        )
+    )
+    peer2.succeed("ip route replace 10.10.10.1/32 dev wg0 table main")
+
+    peer1.succeed("ping -c1 10.10.10.2")
+    peer2.succeed("ping -c1 10.10.10.1")
+  '';
+})
diff --git a/nixos/tests/wireguard/make-peer.nix b/nixos/tests/wireguard/make-peer.nix
new file mode 100644
index 00000000000..d2740549738
--- /dev/null
+++ b/nixos/tests/wireguard/make-peer.nix
@@ -0,0 +1,23 @@
+{ lib, ... }: { ip4, ip6, extraConfig }:
+lib.mkMerge [
+  {
+    boot.kernel.sysctl = {
+      "net.ipv6.conf.all.forwarding" = "1";
+      "net.ipv6.conf.default.forwarding" = "1";
+      "net.ipv4.ip_forward" = "1";
+    };
+
+    networking.useDHCP = false;
+    networking.interfaces.eth1 = {
+      ipv4.addresses = [{
+        address = ip4;
+        prefixLength = 24;
+      }];
+      ipv6.addresses = [{
+        address = ip6;
+        prefixLength = 64;
+      }];
+    };
+  }
+  extraConfig
+]
diff --git a/nixos/tests/wireguard/namespaces.nix b/nixos/tests/wireguard/namespaces.nix
new file mode 100644
index 00000000000..93dc84a8768
--- /dev/null
+++ b/nixos/tests/wireguard/namespaces.nix
@@ -0,0 +1,84 @@
+{ kernelPackages ? null }:
+
+let
+  listenPort = 12345;
+  socketNamespace = "foo";
+  interfaceNamespace = "bar";
+  node = {
+    networking.wireguard.interfaces.wg0 = {
+      listenPort = listenPort;
+      ips = [ "10.10.10.1/24" ];
+      privateKeyFile = "/etc/wireguard/private";
+      generatePrivateKeyFile = true;
+    };
+  };
+
+in
+
+import ../make-test-python.nix ({ pkgs, lib, ... } : {
+  name = "wireguard-with-namespaces";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ asymmetric ];
+  };
+
+  nodes = {
+    # interface should be created in the socketNamespace
+    # and not moved from there
+    peer0 = pkgs.lib.attrsets.recursiveUpdate node {
+      boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
+      networking.wireguard.interfaces.wg0 = {
+        preSetup = ''
+          ip netns add ${socketNamespace}
+        '';
+        inherit socketNamespace;
+      };
+    };
+    # interface should be created in the init namespace
+    # and moved to the interfaceNamespace
+    peer1 = pkgs.lib.attrsets.recursiveUpdate node {
+      boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
+      networking.wireguard.interfaces.wg0 = {
+        preSetup = ''
+          ip netns add ${interfaceNamespace}
+        '';
+        inherit interfaceNamespace;
+      };
+    };
+    # interface should be created in the socketNamespace
+    # and moved to the interfaceNamespace
+    peer2 = pkgs.lib.attrsets.recursiveUpdate node {
+      boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
+      networking.wireguard.interfaces.wg0 = {
+        preSetup = ''
+          ip netns add ${socketNamespace}
+          ip netns add ${interfaceNamespace}
+        '';
+        inherit socketNamespace interfaceNamespace;
+      };
+    };
+    # interface should be created in the socketNamespace
+    # and moved to the init namespace
+    peer3 = pkgs.lib.attrsets.recursiveUpdate node {
+      boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
+      networking.wireguard.interfaces.wg0 = {
+        preSetup = ''
+          ip netns add ${socketNamespace}
+        '';
+        inherit socketNamespace;
+        interfaceNamespace = "init";
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    for machine in peer0, peer1, peer2, peer3:
+        machine.wait_for_unit("wireguard-wg0.service")
+
+    peer0.succeed("ip -n ${socketNamespace} link show wg0")
+    peer1.succeed("ip -n ${interfaceNamespace} link show wg0")
+    peer2.succeed("ip -n ${interfaceNamespace} link show wg0")
+    peer3.succeed("ip link show wg0")
+  '';
+})
diff --git a/nixos/tests/wireguard/snakeoil-keys.nix b/nixos/tests/wireguard/snakeoil-keys.nix
new file mode 100644
index 00000000000..55ad582d405
--- /dev/null
+++ b/nixos/tests/wireguard/snakeoil-keys.nix
@@ -0,0 +1,11 @@
+{
+  peer0 = {
+    privateKey = "OPuVRS2T0/AtHDp3PXkNuLQYDiqJaBEEnYe42BSnJnQ=";
+    publicKey = "IujkG119YPr2cVQzJkSLYCdjpHIDjvr/qH1w1tdKswY=";
+  };
+
+  peer1 = {
+    privateKey = "uO8JVo/sanx2DOM0L9GUEtzKZ82RGkRnYgpaYc7iXmg=";
+    publicKey = "Ks9yRJIi/0vYgRmn14mIOQRwkcUGBujYINbMpik2SBI=";
+  };
+}
diff --git a/nixos/tests/wireguard/wg-quick.nix b/nixos/tests/wireguard/wg-quick.nix
new file mode 100644
index 00000000000..961c2e15c30
--- /dev/null
+++ b/nixos/tests/wireguard/wg-quick.nix
@@ -0,0 +1,67 @@
+{ kernelPackages ? null }:
+
+import ../make-test-python.nix ({ pkgs, lib, ... }:
+  let
+    wg-snakeoil-keys = import ./snakeoil-keys.nix;
+    peer = (import ./make-peer.nix) { inherit lib; };
+  in
+  {
+    name = "wg-quick";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ d-xo ];
+    };
+
+    nodes = {
+      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;
+
+            inherit (wg-snakeoil-keys.peer0) privateKey;
+
+            peers = lib.singleton {
+              allowedIPs = [ "10.23.42.2/32" "fc00::2/128" ];
+
+              inherit (wg-snakeoil-keys.peer1) publicKey;
+            };
+          };
+        };
+      };
+
+      peer1 = peer {
+        ip4 = "192.168.0.2";
+        ip6 = "fd00::2";
+        extraConfig = {
+          boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
+          networking.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;
+
+              inherit (wg-snakeoil-keys.peer0) publicKey;
+            };
+          };
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      peer0.wait_for_unit("wg-quick-wg0.service")
+      peer1.wait_for_unit("wg-quick-wg0.service")
+
+      peer1.succeed("ping -c5 fc00::1")
+      peer1.succeed("ping -c5 10.23.42.1")
+    '';
+  }
+)
diff --git a/nixos/tests/without-nix.nix b/nixos/tests/without-nix.nix
new file mode 100644
index 00000000000..2fc00b04144
--- /dev/null
+++ b/nixos/tests/without-nix.nix
@@ -0,0 +1,23 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "without-nix";
+  meta = with lib.maintainers; {
+    maintainers = [ ericson2314 ];
+  };
+
+  nixpkgs.overlays = [
+    (self: super: {
+      nix = throw "don't want to use this";
+    })
+  ];
+
+  nodes.machine = { ... }: {
+    nix.enable = false;
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.succeed("which which")
+    machine.fail("which nix")
+  '';
+})
diff --git a/nixos/tests/wmderland.nix b/nixos/tests/wmderland.nix
new file mode 100644
index 00000000000..6de0cd9212e
--- /dev/null
+++ b/nixos/tests/wmderland.nix
@@ -0,0 +1,54 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "wmderland";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ takagiy ];
+  };
+
+  machine = { lib, ... }: {
+    imports = [ ./common/x11.nix ./common/user-account.nix ];
+    test-support.displayManager.auto.user = "alice";
+    services.xserver.displayManager.defaultSession = lib.mkForce "none+wmderland";
+    services.xserver.windowManager.wmderland.enable = true;
+
+    systemd.services.setupWmderlandConfig = {
+      wantedBy = [ "multi-user.target" ];
+      before = [ "multi-user.target" ];
+      environment = {
+        HOME = "/home/alice";
+      };
+      unitConfig = {
+        type = "oneshot";
+        RemainAfterExit = true;
+        user = "alice";
+      };
+      script = let
+        config = pkgs.writeText "config" ''
+             set $Mod = Mod1
+             bindsym $Mod+Return exec ${pkgs.xterm}/bin/xterm -cm -pc
+        '';
+      in ''
+        mkdir -p $HOME/.config/wmderland
+        cp ${config} $HOME/.config/wmderland/config
+      '';
+    };
+  };
+
+  testScript = { ... }: ''
+    with subtest("ensure x starts"):
+        machine.wait_for_x()
+        machine.wait_for_file("/home/alice/.Xauthority")
+        machine.succeed("xauth merge ~alice/.Xauthority")
+
+    with subtest("ensure we can open a new terminal"):
+        machine.send_key("alt-ret")
+        machine.wait_until_succeeds("pgrep xterm")
+        machine.wait_for_window(r"alice.*?machine")
+        machine.screenshot("terminal")
+
+    with subtest("ensure we can communicate through ipc with wmderlandc"):
+        # Kills the previously open xterm
+        machine.succeed("pgrep xterm")
+        machine.execute("DISPLAY=:0 wmderlandc kill")
+        machine.fail("pgrep xterm")
+  '';
+})
diff --git a/nixos/tests/wordpress.nix b/nixos/tests/wordpress.nix
new file mode 100644
index 00000000000..416a20aa7fe
--- /dev/null
+++ b/nixos/tests/wordpress.nix
@@ -0,0 +1,90 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+  name = "wordpress";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [
+      flokli
+      grahamc # under duress!
+      mmilata
+    ];
+  };
+
+  nodes = {
+    wp_httpd = { ... }: {
+      services.httpd.adminAddr = "webmaster@site.local";
+      services.httpd.logPerVirtualHost = true;
+
+      services.wordpress.sites = {
+        "site1.local" = {
+          database.tablePrefix = "site1_";
+        };
+        "site2.local" = {
+          database.tablePrefix = "site2_";
+        };
+      };
+
+      networking.firewall.allowedTCPPorts = [ 80 ];
+      networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ];
+    };
+
+    wp_nginx = { ... }: {
+      services.wordpress.webserver = "nginx";
+      services.wordpress.sites = {
+        "site1.local" = {
+          database.tablePrefix = "site1_";
+        };
+        "site2.local" = {
+          database.tablePrefix = "site2_";
+        };
+      };
+
+      networking.firewall.allowedTCPPorts = [ 80 ];
+      networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ];
+    };
+
+    wp_caddy = { ... }: {
+      services.wordpress.webserver = "caddy";
+      services.wordpress.sites = {
+        "site1.local" = {
+          database.tablePrefix = "site1_";
+        };
+        "site2.local" = {
+          database.tablePrefix = "site2_";
+        };
+      };
+
+      networking.firewall.allowedTCPPorts = [ 80 ];
+      networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ];
+    };
+  };
+
+  testScript = ''
+    import re
+
+    start_all()
+
+    wp_httpd.wait_for_unit("httpd")
+    wp_nginx.wait_for_unit("nginx")
+    wp_caddy.wait_for_unit("caddy")
+
+    site_names = ["site1.local", "site2.local"]
+
+    for machine in (wp_httpd, wp_nginx, wp_caddy):
+        for site_name in site_names:
+            machine.wait_for_unit(f"phpfpm-wordpress-{site_name}")
+
+            with subtest("website returns welcome screen"):
+                assert "Welcome to the famous" in machine.succeed(f"curl -L {site_name}")
+
+            with subtest("wordpress-init went through"):
+                info = machine.get_unit_info(f"wordpress-init-{site_name}")
+                assert info["Result"] == "success"
+
+            with subtest("secret keys are set"):
+                pattern = re.compile(r"^define.*NONCE_SALT.{64,};$", re.MULTILINE)
+                assert pattern.search(
+                    machine.succeed(f"cat /var/lib/wordpress/{site_name}/secret-keys.php")
+                )
+  '';
+})
diff --git a/nixos/tests/wpa_supplicant.nix b/nixos/tests/wpa_supplicant.nix
new file mode 100644
index 00000000000..40d934b8e1d
--- /dev/null
+++ b/nixos/tests/wpa_supplicant.nix
@@ -0,0 +1,96 @@
+import ./make-test-python.nix ({ pkgs, lib, ...}:
+{
+  name = "wpa_supplicant";
+  meta = with lib.maintainers; {
+    maintainers = [ rnhmjoj ];
+  };
+
+  machine = { ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+
+    # add a virtual wlan interface
+    boot.kernelModules = [ "mac80211_hwsim" ];
+
+    # wireless access point
+    services.hostapd = {
+      enable = true;
+      wpa = true;
+      interface = "wlan0";
+      ssid = "nixos-test";
+      wpaPassphrase = "reproducibility";
+    };
+
+    # wireless client
+    networking.wireless = {
+      # the override is needed because the wifi is
+      # disabled with mkVMOverride in qemu-vm.nix.
+      enable = lib.mkOverride 0 true;
+      userControlled.enable = true;
+      interfaces = [ "wlan1" ];
+      fallbackToWPA2 = true;
+
+      networks = {
+        # test WPA2 fallback
+        mixed-wpa = {
+          psk = "password";
+          authProtocols = [ "WPA-PSK" "SAE" ];
+        };
+        sae-only = {
+          psk = "password";
+          authProtocols = [ "SAE" ];
+        };
+
+        # test network
+        nixos-test.psk = "@PSK_NIXOS_TEST@";
+
+        # secrets substitution test cases
+        test1.psk = "@PSK_VALID@";              # should be replaced
+        test2.psk = "@PSK_SPECIAL@";            # should be replaced
+        test3.psk = "@PSK_MISSING@";            # should not be replaced
+        test4.psk = "P@ssowrdWithSome@tSymbol"; # should not be replaced
+      };
+
+      # secrets
+      environmentFile = pkgs.writeText "wpa-secrets" ''
+        PSK_NIXOS_TEST="reproducibility"
+        PSK_VALID="S0m3BadP4ssw0rd";
+        # taken from https://github.com/minimaxir/big-list-of-naughty-strings
+        PSK_SPECIAL=",./;'[]\-= <>?:\"{}|_+ !@#$%^\&*()`~";
+      '';
+    };
+
+  };
+
+  testScript =
+    ''
+      config_file = "/run/wpa_supplicant/wpa_supplicant.conf"
+
+      with subtest("Configuration file is inaccessible to other users"):
+          machine.wait_for_file(config_file)
+          machine.fail(f"sudo -u nobody ls {config_file}")
+
+      with subtest("Secrets variables have been substituted"):
+          machine.fail(f"grep -q @PSK_VALID@ {config_file}")
+          machine.fail(f"grep -q @PSK_SPECIAL@ {config_file}")
+          machine.succeed(f"grep -q @PSK_MISSING@ {config_file}")
+          machine.succeed(f"grep -q P@ssowrdWithSome@tSymbol {config_file}")
+
+      with subtest("WPA2 fallbacks have been generated"):
+          assert int(machine.succeed(f"grep -c sae-only {config_file}")) == 1
+          assert int(machine.succeed(f"grep -c mixed-wpa {config_file}")) == 2
+
+      # save file for manual inspection
+      machine.copy_from_vm(config_file)
+
+      with subtest("Daemon is running and accepting connections"):
+          machine.wait_for_unit("wpa_supplicant-wlan1.service")
+          status = machine.succeed("wpa_cli -i wlan1 status")
+          assert "Failed to connect" not in status, \
+                 "Failed to connect to the daemon"
+
+      with subtest("Daemon can connect to the access point"):
+          machine.wait_until_succeeds(
+            "wpa_cli -i wlan1 status | grep -q wpa_state=COMPLETED"
+          )
+    '';
+})
diff --git a/nixos/tests/xandikos.nix b/nixos/tests/xandikos.nix
new file mode 100644
index 00000000000..69d78ee21e7
--- /dev/null
+++ b/nixos/tests/xandikos.nix
@@ -0,0 +1,70 @@
+import ./make-test-python.nix (
+  { pkgs, lib, ... }:
+
+    {
+      name = "xandikos";
+
+      meta.maintainers = with lib.maintainers; [ _0x4A6F ];
+
+      nodes = {
+        xandikos_client = {};
+        xandikos_default = {
+          networking.firewall.allowedTCPPorts = [ 8080 ];
+          services.xandikos.enable = true;
+        };
+        xandikos_proxy = {
+          networking.firewall.allowedTCPPorts = [ 80 8080 ];
+          services.xandikos.enable = true;
+          services.xandikos.address = "localhost";
+          services.xandikos.port = 8080;
+          services.xandikos.routePrefix = "/xandikos-prefix/";
+          services.xandikos.extraOptions = [
+            "--defaults"
+          ];
+          services.nginx = {
+            enable = true;
+            recommendedProxySettings = true;
+            virtualHosts."xandikos" = {
+              serverName = "xandikos.local";
+              basicAuth.xandikos = "snakeOilPassword";
+              locations."/xandikos/" = {
+                proxyPass = "http://localhost:8080/xandikos-prefix/";
+              };
+            };
+          };
+        };
+      };
+
+      testScript = ''
+        start_all()
+
+        with subtest("Xandikos default"):
+            xandikos_default.wait_for_unit("multi-user.target")
+            xandikos_default.wait_for_unit("xandikos.service")
+            xandikos_default.wait_for_open_port(8080)
+            xandikos_default.succeed("curl --fail http://localhost:8080/")
+            xandikos_default.succeed(
+                "curl -s --fail --location http://localhost:8080/ | grep -i Xandikos"
+            )
+            xandikos_client.wait_for_unit("network.target")
+            xandikos_client.fail("curl --fail http://xandikos_default:8080/")
+
+        with subtest("Xandikos proxy"):
+            xandikos_proxy.wait_for_unit("multi-user.target")
+            xandikos_proxy.wait_for_unit("xandikos.service")
+            xandikos_proxy.wait_for_open_port(8080)
+            xandikos_proxy.succeed("curl --fail http://localhost:8080/")
+            xandikos_proxy.succeed(
+                "curl -s --fail --location http://localhost:8080/ | grep -i Xandikos"
+            )
+            xandikos_client.wait_for_unit("network.target")
+            xandikos_client.fail("curl --fail http://xandikos_proxy:8080/")
+            xandikos_client.succeed(
+                "curl -s --fail -u xandikos:snakeOilPassword -H 'Host: xandikos.local' http://xandikos_proxy/xandikos/ | grep -i Xandikos"
+            )
+            xandikos_client.succeed(
+                "curl -s --fail -u xandikos:snakeOilPassword -H 'Host: xandikos.local' http://xandikos_proxy/xandikos/user/ | grep -i Xandikos"
+            )
+      '';
+    }
+)
diff --git a/nixos/tests/xautolock.nix b/nixos/tests/xautolock.nix
new file mode 100644
index 00000000000..529567e0797
--- /dev/null
+++ b/nixos/tests/xautolock.nix
@@ -0,0 +1,24 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+with lib;
+
+{
+  name = "xautolock";
+  meta.maintainers = with pkgs.lib.maintainers; [ ];
+
+  nodes.machine = {
+    imports = [ ./common/x11.nix ./common/user-account.nix ];
+
+    test-support.displayManager.auto.user = "bob";
+    services.xserver.xautolock.enable = true;
+    services.xserver.xautolock.time = 1;
+  };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_x()
+    machine.fail("pgrep xlock")
+    machine.sleep(120)
+    machine.succeed("pgrep xlock")
+  '';
+})
diff --git a/nixos/tests/xfce.nix b/nixos/tests/xfce.nix
new file mode 100644
index 00000000000..9051deebae7
--- /dev/null
+++ b/nixos/tests/xfce.nix
@@ -0,0 +1,45 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "xfce";
+
+  machine =
+    { pkgs, ... }:
+
+    {
+      imports = [
+        ./common/user-account.nix
+      ];
+
+      services.xserver.enable = true;
+
+      services.xserver.displayManager = {
+        lightdm.enable = true;
+        autoLogin = {
+          enable = true;
+          user = "alice";
+        };
+      };
+
+      services.xserver.desktopManager.xfce.enable = true;
+
+      hardware.pulseaudio.enable = true; # needed for the factl test, /dev/snd/* exists without them but udev doesn't care then
+
+    };
+
+  testScript = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+  in ''
+      machine.wait_for_x()
+      machine.wait_for_file("${user.home}/.Xauthority")
+      machine.succeed("xauth merge ${user.home}/.Xauthority")
+      machine.wait_for_window("xfce4-panel")
+      machine.sleep(10)
+
+      # Check that logging in has given the user ownership of devices.
+      machine.succeed("getfacl -p /dev/snd/timer | grep -q ${user.name}")
+
+      machine.succeed("su - ${user.name} -c 'DISPLAY=:0.0 xfce4-terminal >&2 &'")
+      machine.wait_for_window("Terminal")
+      machine.sleep(10)
+      machine.screenshot("screen")
+    '';
+})
diff --git a/nixos/tests/xmonad.nix b/nixos/tests/xmonad.nix
new file mode 100644
index 00000000000..a2fb38e53bd
--- /dev/null
+++ b/nixos/tests/xmonad.nix
@@ -0,0 +1,114 @@
+import ./make-test-python.nix ({ pkgs, ...}:
+
+let
+  mkConfig = name: keys: ''
+    import XMonad
+    import XMonad.Operations (restart)
+    import XMonad.Util.EZConfig
+    import XMonad.Util.SessionStart
+    import Control.Monad (when)
+    import Text.Printf (printf)
+    import System.Posix.Process (executeFile)
+    import System.Info (arch,os)
+    import System.Environment (getArgs)
+    import System.FilePath ((</>))
+
+    main = launch $ def { startupHook = startup } `additionalKeysP` myKeys
+
+    startup = isSessionStart >>= \sessInit ->
+      spawn "touch /tmp/${name}"
+        >> if sessInit then setSessionStarted else spawn "xterm"
+
+    myKeys = [${builtins.concatStringsSep ", " keys}]
+
+    compiledConfig = printf "xmonad-%s-%s" arch os
+
+    compileRestart resume =
+      whenX (recompile True) $
+        when resume writeStateToFile
+          *> catchIO
+            ( do
+                dir <- getXMonadDataDir
+                args <- getArgs
+                executeFile (dir </> compiledConfig) False args Nothing
+            )
+  '';
+
+  oldKeys =
+    [ ''("M-C-x", spawn "xterm")''
+      ''("M-q", restart "xmonad" True)''
+      ''("M-C-q", compileRestart True)''
+      ''("M-C-t", spawn "touch /tmp/somefile")'' # create somefile
+    ];
+
+  newKeys =
+    [ ''("M-C-x", spawn "xterm")''
+      ''("M-q", restart "xmonad" True)''
+      ''("M-C-q", compileRestart True)''
+      ''("M-C-r", spawn "rm /tmp/somefile")'' # delete somefile
+    ];
+
+  newConfig = pkgs.writeText "xmonad.hs" (mkConfig "newXMonad" newKeys);
+in {
+  name = "xmonad";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ nequissimus ivanbrennan ];
+  };
+
+  machine = { pkgs, ... }: {
+    imports = [ ./common/x11.nix ./common/user-account.nix ];
+    test-support.displayManager.auto.user = "alice";
+    services.xserver.displayManager.defaultSession = "none+xmonad";
+    services.xserver.windowManager.xmonad = {
+      enable = true;
+      enableConfiguredRecompile = true;
+      enableContribAndExtras = true;
+      extraPackages = with pkgs.haskellPackages; haskellPackages: [ xmobar ];
+      config = mkConfig "oldXMonad" oldKeys;
+    };
+  };
+
+  testScript = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+  in ''
+    machine.wait_for_x()
+    machine.wait_for_file("${user.home}/.Xauthority")
+    machine.succeed("xauth merge ${user.home}/.Xauthority")
+    machine.send_key("alt-ctrl-x")
+    machine.wait_for_window("${user.name}.*machine")
+    machine.sleep(1)
+    machine.screenshot("terminal1")
+    machine.succeed("rm /tmp/oldXMonad")
+    machine.send_key("alt-q")
+    machine.wait_for_file("/tmp/oldXMonad")
+    machine.wait_for_window("${user.name}.*machine")
+    machine.sleep(1)
+    machine.screenshot("terminal2")
+
+    # /tmp/somefile should not exist yet
+    machine.fail("stat /tmp/somefile")
+
+    # original config has a keybinding that creates somefile
+    machine.send_key("alt-ctrl-t")
+    machine.wait_for_file("/tmp/somefile")
+
+    # set up the new config
+    machine.succeed("mkdir -p ${user.home}/.xmonad")
+    machine.copy_from_host("${newConfig}", "${user.home}/.xmonad/xmonad.hs")
+
+    # recompile xmonad using the new config
+    machine.send_key("alt-ctrl-q")
+    machine.wait_for_file("/tmp/newXMonad")
+
+    # new config has a keybinding that deletes somefile
+    machine.send_key("alt-ctrl-r")
+    machine.wait_until_fails("stat /tmp/somefile", timeout=30)
+
+    # restart with the old config, and confirm the old keybinding is back
+    machine.succeed("rm /tmp/oldXMonad")
+    machine.send_key("alt-q")
+    machine.wait_for_file("/tmp/oldXMonad")
+    machine.send_key("alt-ctrl-t")
+    machine.wait_for_file("/tmp/somefile")
+  '';
+})
diff --git a/nixos/tests/xmpp/ejabberd.nix b/nixos/tests/xmpp/ejabberd.nix
new file mode 100644
index 00000000000..7926fe80de2
--- /dev/null
+++ b/nixos/tests/xmpp/ejabberd.nix
@@ -0,0 +1,278 @@
+import ../make-test-python.nix ({ pkgs, ... }: {
+  name = "ejabberd";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ajs124 ];
+  };
+  nodes = {
+    client = { nodes, pkgs, ... }: {
+      networking.extraHosts = ''
+        ${nodes.server.config.networking.primaryIPAddress} example.com
+      '';
+
+      environment.systemPackages = [
+        (pkgs.callPackage ./xmpp-sendmessage.nix { connectTo = nodes.server.config.networking.primaryIPAddress; })
+      ];
+    };
+    server = { config, pkgs, ... }: {
+      networking.extraHosts = ''
+        ${config.networking.primaryIPAddress} example.com
+      '';
+
+      services.ejabberd = {
+        enable = true;
+        configFile = "/etc/ejabberd.yml";
+      };
+
+      environment.etc."ejabberd.yml" = {
+        user = "ejabberd";
+        mode = "0600";
+        text = ''
+          loglevel: 3
+
+          hosts:
+            - "example.com"
+
+          listen:
+            -
+              port: 5222
+              module: ejabberd_c2s
+              zlib: false
+              max_stanza_size: 65536
+              shaper: c2s_shaper
+              access: c2s
+            -
+              port: 5269
+              ip: "::"
+              module: ejabberd_s2s_in
+            -
+              port: 5347
+              ip: "127.0.0.1"
+              module: ejabberd_service
+              access: local
+              shaper: fast
+            -
+              port: 5444
+              module: ejabberd_http
+              request_handlers:
+                "/upload": mod_http_upload
+
+          ## Disabling digest-md5 SASL authentication. digest-md5 requires plain-text
+          ## password storage (see auth_password_format option).
+          disable_sasl_mechanisms: "digest-md5"
+
+          ## Outgoing S2S options
+          ## Preferred address families (which to try first) and connect timeout
+          ## in seconds.
+          outgoing_s2s_families:
+             - ipv4
+             - ipv6
+
+          ## auth_method: Method used to authenticate the users.
+          ## The default method is the internal.
+          ## If you want to use a different method,
+          ## comment this line and enable the correct ones.
+          auth_method: internal
+
+          ## Store the plain passwords or hashed for SCRAM:
+          ## auth_password_format: plain
+          auth_password_format: scram
+
+          ###'  TRAFFIC SHAPERS
+          shaper:
+            # in B/s
+            normal: 1000000
+            fast: 50000000
+
+          ## This option specifies the maximum number of elements in the queue
+          ## of the FSM. Refer to the documentation for details.
+          max_fsm_queue: 1000
+
+          ###'   ACCESS CONTROL LISTS
+          acl:
+            ## The 'admin' ACL grants administrative privileges to XMPP accounts.
+            ## You can put here as many accounts as you want.
+            admin:
+               user:
+                 - "root": "example.com"
+
+            ## Local users: don't modify this.
+            local:
+              user_regexp: ""
+
+            ## Loopback network
+            loopback:
+              ip:
+                - "127.0.0.0/8"
+                - "::1/128"
+                - "::FFFF:127.0.0.1/128"
+
+          ###'  SHAPER RULES
+          shaper_rules:
+            ## Maximum number of simultaneous sessions allowed for a single user:
+            max_user_sessions: 10
+            ## Maximum number of offline messages that users can have:
+            max_user_offline_messages:
+              - 5000: admin
+              - 1024
+            ## For C2S connections, all users except admins use the "normal" shaper
+            c2s_shaper:
+              - none: admin
+              - normal
+            ## All S2S connections use the "fast" shaper
+            s2s_shaper: fast
+
+          ###'  ACCESS RULES
+          access_rules:
+            ## This rule allows access only for local users:
+            local:
+              - allow: local
+            ## Only non-blocked users can use c2s connections:
+            c2s:
+              - deny: blocked
+              - allow
+            ## Only admins can send announcement messages:
+            announce:
+              - allow: admin
+            ## Only admins can use the configuration interface:
+            configure:
+              - allow: admin
+            ## Only accounts of the local ejabberd server can create rooms:
+            muc_create:
+              - allow: local
+            ## Only accounts on the local ejabberd server can create Pubsub nodes:
+            pubsub_createnode:
+              - allow: local
+            ## In-band registration allows registration of any possible username.
+            ## To disable in-band registration, replace 'allow' with 'deny'.
+            register:
+              - allow
+            ## Only allow to register from localhost
+            trusted_network:
+              - allow: loopback
+
+          ## ===============
+          ## API PERMISSIONS
+          ## ===============
+          ##
+          ## This section allows you to define who and using what method
+          ## can execute commands offered by ejabberd.
+          ##
+          ## By default "console commands" section allow executing all commands
+          ## issued using ejabberdctl command, and "admin access" section allows
+          ## users in admin acl that connect from 127.0.0.1 to  execute all
+          ## commands except start and stop with any available access method
+          ## (ejabberdctl, http-api, xmlrpc depending what is enabled on server).
+          ##
+          ## If you remove "console commands" there will be one added by
+          ## default allowing executing all commands, but if you just change
+          ## permissions in it, version from config file will be used instead
+          ## of default one.
+          ##
+          api_permissions:
+            "console commands":
+              from:
+                - ejabberd_ctl
+              who: all
+              what: "*"
+
+          language: "en"
+
+          ###'  MODULES
+          ## Modules enabled in all ejabberd virtual hosts.
+          modules:
+            mod_adhoc: {}
+            mod_announce: # recommends mod_adhoc
+              access: announce
+            mod_blocking: {} # requires mod_privacy
+            mod_caps: {}
+            mod_carboncopy: {}
+            mod_client_state: {}
+            mod_configure: {} # requires mod_adhoc
+            ## mod_delegation: {} # for xep0356
+            mod_disco: {}
+            #mod_irc:
+            #  host: "irc.@HOST@"
+            #  default_encoding: "utf-8"
+            ## mod_bosh: {}
+            ## mod_http_fileserver:
+            ##   docroot: "/var/www"
+            ##   accesslog: "/var/log/ejabberd/access.log"
+            mod_http_upload:
+              thumbnail: false # otherwise needs the identify command from ImageMagick installed
+              put_url: "http://@HOST@:5444/upload"
+            ##   # docroot: "@HOME@/upload"
+            #mod_http_upload_quota:
+            #  max_days: 14
+            mod_last: {}
+            ## XEP-0313: Message Archive Management
+            ## You might want to setup a SQL backend for MAM because the mnesia database is
+            ## limited to 2GB which might be exceeded on large servers
+            mod_mam: {}
+            mod_muc:
+              host: "muc.@HOST@"
+              access:
+                - allow
+              access_admin:
+                - allow: admin
+              access_create: muc_create
+              access_persistent: muc_create
+            mod_muc_admin: {}
+            mod_muc_log: {}
+            mod_offline:
+              access_max_user_messages: max_user_offline_messages
+            mod_ping: {}
+            ## mod_pres_counter:
+            ##   count: 5
+            ##   interval: 60
+            mod_privacy: {}
+            mod_private: {}
+            mod_roster:
+                versioning: true
+            mod_shared_roster: {}
+            mod_stats: {}
+            mod_time: {}
+            mod_vcard:
+              search: false
+            mod_vcard_xupdate: {}
+            ## Convert all avatars posted by Android clients from WebP to JPEG
+            mod_avatar: {}
+            #  convert:
+            #    webp: jpeg
+            mod_version: {}
+            mod_stream_mgmt: {}
+            ##   The module for S2S dialback (XEP-0220). Please note that you cannot
+            ##   rely solely on dialback if you want to federate with other servers,
+            ##   because a lot of servers have dialback disabled and instead rely on
+            ##   PKIX authentication. Make sure you have proper certificates installed
+            ##   and check your accessibility at https://check.messaging.one/
+            mod_s2s_dialback: {}
+            mod_pubsub:
+              plugins:
+                - "pep"
+            mod_push: {}
+        '';
+      };
+
+      networking.firewall.enable = false;
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+    ejabberd_prefix = "su ejabberd -s $(which ejabberdctl) "
+
+    server.wait_for_unit("ejabberd.service")
+
+    assert "status: started" in server.succeed(ejabberd_prefix + "status")
+
+    server.succeed(
+        ejabberd_prefix + "register azurediamond example.com hunter2",
+        ejabberd_prefix + "register cthon98 example.com nothunter2",
+    )
+    server.fail(ejabberd_prefix + "register asdf wrong.domain")
+    client.succeed("send-message")
+    server.succeed(
+        ejabberd_prefix + "unregister cthon98 example.com",
+        ejabberd_prefix + "unregister azurediamond example.com",
+    )
+  '';
+})
diff --git a/nixos/tests/xmpp/prosody-mysql.nix b/nixos/tests/xmpp/prosody-mysql.nix
new file mode 100644
index 00000000000..40f3e308a04
--- /dev/null
+++ b/nixos/tests/xmpp/prosody-mysql.nix
@@ -0,0 +1,124 @@
+let
+  cert = pkgs: pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
+    openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=example.com/CN=uploads.example.com/CN=conference.example.com' -days 36500
+    mkdir -p $out
+    cp key.pem cert.pem $out
+  '';
+  createUsers = pkgs: pkgs.writeScriptBin "create-prosody-users" ''
+    #!${pkgs.bash}/bin/bash
+    set -e
+
+    # Creates and set password for the 2 xmpp test users.
+    #
+    # Doing that in a bash script instead of doing that in the test
+    # script allow us to easily provision the users when running that
+    # test interactively.
+
+    prosodyctl register cthon98 example.com nothunter2
+    prosodyctl register azurediamond example.com hunter2
+  '';
+  delUsers = pkgs: pkgs.writeScriptBin "delete-prosody-users" ''
+    #!${pkgs.bash}/bin/bash
+    set -e
+
+    # Deletes the test users.
+    #
+    # Doing that in a bash script instead of doing that in the test
+    # script allow us to easily provision the users when running that
+    # test interactively.
+
+    prosodyctl deluser cthon98@example.com
+    prosodyctl deluser azurediamond@example.com
+  '';
+in import ../make-test-python.nix {
+  name = "prosody-mysql";
+  nodes = {
+    client = { nodes, pkgs, config, ... }: {
+      security.pki.certificateFiles = [ "${cert pkgs}/cert.pem" ];
+      console.keyMap = "fr-bepo";
+      networking.extraHosts = ''
+        ${nodes.server.config.networking.primaryIPAddress} example.com
+        ${nodes.server.config.networking.primaryIPAddress} conference.example.com
+        ${nodes.server.config.networking.primaryIPAddress} uploads.example.com
+      '';
+      environment.systemPackages = [
+        (pkgs.callPackage ./xmpp-sendmessage.nix { connectTo = nodes.server.config.networking.primaryIPAddress; })
+      ];
+    };
+    server = { config, pkgs, ... }: {
+      nixpkgs.overlays = [
+        (self: super: {
+          prosody = super.prosody.override {
+            withExtraLuaPackages = p: [ p.luadbi-mysql ];
+          };
+        })
+      ];
+      security.pki.certificateFiles = [ "${cert pkgs}/cert.pem" ];
+      console.keyMap = "fr-bepo";
+      networking.extraHosts = ''
+        ${config.networking.primaryIPAddress} example.com
+        ${config.networking.primaryIPAddress} conference.example.com
+        ${config.networking.primaryIPAddress} uploads.example.com
+      '';
+      networking.firewall.enable = false;
+      environment.systemPackages = [
+        (createUsers pkgs)
+        (delUsers pkgs)
+      ];
+      services.prosody = {
+        enable = true;
+        ssl.cert = "${cert pkgs}/cert.pem";
+        ssl.key = "${cert pkgs}/key.pem";
+        virtualHosts.example = {
+          domain = "example.com";
+          enabled = true;
+          ssl.cert = "${cert pkgs}/cert.pem";
+          ssl.key = "${cert pkgs}/key.pem";
+        };
+        muc = [
+          {
+            domain = "conference.example.com";
+          }
+        ];
+        uploadHttp = {
+          domain = "uploads.example.com";
+        };
+        extraConfig = ''
+          storage = "sql"
+          sql = {
+            driver = "MySQL";
+            database = "prosody";
+            host = "mysql";
+            port = 3306;
+            username = "prosody";
+            password = "password123";
+          };
+        '';
+      };
+    };
+    mysql = { config, pkgs, ... }: {
+      networking.firewall.enable = false;
+      services.mysql = {
+        enable = true;
+        initialScript = pkgs.writeText "mysql_init.sql" ''
+          CREATE DATABASE prosody;
+          CREATE USER 'prosody'@'server' IDENTIFIED BY 'password123';
+          GRANT ALL PRIVILEGES ON prosody.* TO 'prosody'@'server';
+          FLUSH PRIVILEGES;
+        '';
+        package = pkgs.mariadb;
+      };
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+    # Check with mysql storage
+    mysql.wait_for_unit("mysql.service")
+    server.wait_for_unit("prosody.service")
+    server.succeed('prosodyctl status | grep "Prosody is running"')
+
+    server.succeed("create-prosody-users")
+    client.succeed("send-message")
+    server.succeed("delete-prosody-users")
+  '';
+}
diff --git a/nixos/tests/xmpp/prosody.nix b/nixos/tests/xmpp/prosody.nix
new file mode 100644
index 00000000000..14eab56fb82
--- /dev/null
+++ b/nixos/tests/xmpp/prosody.nix
@@ -0,0 +1,92 @@
+let
+  cert = pkgs: pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
+    openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=example.com/CN=uploads.example.com/CN=conference.example.com' -days 36500
+    mkdir -p $out
+    cp key.pem cert.pem $out
+  '';
+  createUsers = pkgs: pkgs.writeScriptBin "create-prosody-users" ''
+    #!${pkgs.bash}/bin/bash
+    set -e
+
+    # Creates and set password for the 2 xmpp test users.
+    #
+    # Doing that in a bash script instead of doing that in the test
+    # script allow us to easily provision the users when running that
+    # test interactively.
+
+    prosodyctl register cthon98 example.com nothunter2
+    prosodyctl register azurediamond example.com hunter2
+  '';
+  delUsers = pkgs: pkgs.writeScriptBin "delete-prosody-users" ''
+    #!${pkgs.bash}/bin/bash
+    set -e
+
+    # Deletes the test users.
+    #
+    # Doing that in a bash script instead of doing that in the test
+    # script allow us to easily provision the users when running that
+    # test interactively.
+
+    prosodyctl deluser cthon98@example.com
+    prosodyctl deluser azurediamond@example.com
+  '';
+in import ../make-test-python.nix {
+  name = "prosody";
+  nodes = {
+    client = { nodes, pkgs, config, ... }: {
+      security.pki.certificateFiles = [ "${cert pkgs}/cert.pem" ];
+      console.keyMap = "fr-bepo";
+      networking.extraHosts = ''
+        ${nodes.server.config.networking.primaryIPAddress} example.com
+        ${nodes.server.config.networking.primaryIPAddress} conference.example.com
+        ${nodes.server.config.networking.primaryIPAddress} uploads.example.com
+      '';
+      environment.systemPackages = [
+        (pkgs.callPackage ./xmpp-sendmessage.nix { connectTo = nodes.server.config.networking.primaryIPAddress; })
+      ];
+    };
+    server = { config, pkgs, ... }: {
+      security.pki.certificateFiles = [ "${cert pkgs}/cert.pem" ];
+      console.keyMap = "fr-bepo";
+      networking.extraHosts = ''
+        ${config.networking.primaryIPAddress} example.com
+        ${config.networking.primaryIPAddress} conference.example.com
+        ${config.networking.primaryIPAddress} uploads.example.com
+      '';
+      networking.firewall.enable = false;
+      environment.systemPackages = [
+        (createUsers pkgs)
+        (delUsers pkgs)
+      ];
+      services.prosody = {
+        enable = true;
+        ssl.cert = "${cert pkgs}/cert.pem";
+        ssl.key = "${cert pkgs}/key.pem";
+        virtualHosts.example = {
+          domain = "example.com";
+          enabled = true;
+          ssl.cert = "${cert pkgs}/cert.pem";
+          ssl.key = "${cert pkgs}/key.pem";
+        };
+        muc = [
+          {
+            domain = "conference.example.com";
+          }
+        ];
+        uploadHttp = {
+          domain = "uploads.example.com";
+        };
+      };
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+    # Check with sqlite storage
+    server.wait_for_unit("prosody.service")
+    server.succeed('prosodyctl status | grep "Prosody is running"')
+
+    server.succeed("create-prosody-users")
+    client.succeed("send-message")
+    server.succeed("delete-prosody-users")
+  '';
+}
diff --git a/nixos/tests/xmpp/xmpp-sendmessage.nix b/nixos/tests/xmpp/xmpp-sendmessage.nix
new file mode 100644
index 00000000000..47a77f524c6
--- /dev/null
+++ b/nixos/tests/xmpp/xmpp-sendmessage.nix
@@ -0,0 +1,87 @@
+{ writeScriptBin, writeText, python3, connectTo ? "localhost" }:
+let
+  dummyFile = writeText "dummy-file" ''
+    Dear dog,
+
+    Please find this *really* important attachment.
+
+    Yours truly,
+    John
+  '';
+in writeScriptBin "send-message" ''
+#!${(python3.withPackages (ps: [ ps.slixmpp ])).interpreter}
+import logging
+import sys
+from types import MethodType
+
+from slixmpp import ClientXMPP
+from slixmpp.exceptions import IqError, IqTimeout
+
+
+class CthonTest(ClientXMPP):
+
+    def __init__(self, jid, password):
+        ClientXMPP.__init__(self, jid, password)
+        self.add_event_handler("session_start", self.session_start)
+        self.test_succeeded = False
+
+    async def session_start(self, event):
+        try:
+            # Exceptions in event handlers are printed to stderr but not
+            # propagated, they do not make the script terminate with a non-zero
+            # exit code. We use the `test_succeeded` flag as a workaround and
+            # check it later at the end of the script to exit with a proper
+            # exit code.
+            # Additionally, this flag ensures that this event handler has been
+            # actually run by ClientXMPP, which may well not be the case.
+            await self.test_xmpp_server()
+            self.test_succeeded = True
+        finally:
+            # Even if an exception happens in `test_xmpp_server()`, we still
+            # need to disconnect explicitly, otherwise the process will hang
+            # forever.
+            self.disconnect(wait=True)
+
+    async def test_xmpp_server(self):
+        log = logging.getLogger(__name__)
+        self.send_presence()
+        self.get_roster()
+        # Sending a test message
+        self.send_message(mto="azurediamond@example.com", mbody="Hello, this is dog.", mtype="chat")
+        log.info('Message sent')
+
+        # Test http upload (XEP_0363)
+        def timeout_callback(arg):
+            log.error("ERROR: Cannot upload file. XEP_0363 seems broken")
+            sys.exit(1)
+        try:
+            url = await self['xep_0363'].upload_file("${dummyFile}",timeout=10, timeout_callback=timeout_callback)
+        except:
+            log.error("ERROR: Cannot run upload command. XEP_0363 seems broken")
+            sys.exit(1)
+        log.info('Upload success!')
+
+        # Test MUC
+        # TODO: use join_muc_wait() after slixmpp 1.8.0 is released.
+        self.plugin['xep_0045'].join_muc('testMucRoom', 'cthon98')
+        log.info('MUC join success!')
+        log.info('XMPP SCRIPT TEST SUCCESS')
+
+
+if __name__ == '__main__':
+    logging.basicConfig(level=logging.DEBUG,
+                        format='%(levelname)-8s %(message)s')
+
+    ct = CthonTest('cthon98@example.com', 'nothunter2')
+    ct.register_plugin('xep_0071')
+    ct.register_plugin('xep_0128')
+    # HTTP Upload
+    ct.register_plugin('xep_0363')
+    # MUC
+    ct.register_plugin('xep_0045')
+    ct.connect(("server", 5222))
+    ct.process(forever=False)
+
+    if not ct.test_succeeded:
+        sys.exit(1)
+''
diff --git a/nixos/tests/xrdp.nix b/nixos/tests/xrdp.nix
new file mode 100644
index 00000000000..0e1d521c5ac
--- /dev/null
+++ b/nixos/tests/xrdp.nix
@@ -0,0 +1,47 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "xrdp";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ volth ];
+  };
+
+  nodes = {
+    server = { pkgs, ... }: {
+      imports = [ ./common/user-account.nix ];
+      services.xrdp.enable = true;
+      services.xrdp.defaultWindowManager = "${pkgs.xterm}/bin/xterm";
+      networking.firewall.allowedTCPPorts = [ 3389 ];
+    };
+
+    client = { pkgs, ... }: {
+      imports = [ ./common/x11.nix ./common/user-account.nix ];
+      test-support.displayManager.auto.user = "alice";
+      environment.systemPackages = [ pkgs.freerdp ];
+      services.xrdp.enable = true;
+      services.xrdp.defaultWindowManager = "${pkgs.icewm}/bin/icewm";
+    };
+  };
+
+  testScript = { nodes, ... }: let
+    user = nodes.client.config.users.users.alice;
+  in ''
+    start_all()
+
+    client.wait_for_x()
+    client.wait_for_file("${user.home}/.Xauthority")
+    client.succeed("xauth merge ${user.home}/.Xauthority")
+
+    client.sleep(5)
+
+    client.execute("xterm >&2 &")
+    client.sleep(1)
+    client.send_chars("xfreerdp /cert-tofu /w:640 /h:480 /v:127.0.0.1 /u:${user.name} /p:${user.password}\n")
+    client.sleep(5)
+    client.screenshot("localrdp")
+
+    client.execute("xterm >&2 &")
+    client.sleep(1)
+    client.send_chars("xfreerdp /cert-tofu /w:640 /h:480 /v:server /u:${user.name} /p:${user.password}\n")
+    client.sleep(5)
+    client.screenshot("remoterdp")
+  '';
+})
diff --git a/nixos/tests/xss-lock.nix b/nixos/tests/xss-lock.nix
new file mode 100644
index 00000000000..c927d9274e6
--- /dev/null
+++ b/nixos/tests/xss-lock.nix
@@ -0,0 +1,44 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+with lib;
+
+{
+  name = "xss-lock";
+  meta.maintainers = with pkgs.lib.maintainers; [ ];
+
+  nodes = {
+    simple = {
+      imports = [ ./common/x11.nix ./common/user-account.nix ];
+      programs.xss-lock.enable = true;
+      test-support.displayManager.auto.user = "alice";
+    };
+
+    custom_lockcmd = { pkgs, ... }: {
+      imports = [ ./common/x11.nix ./common/user-account.nix ];
+      test-support.displayManager.auto.user = "alice";
+
+      programs.xss-lock = {
+        enable = true;
+        extraOptions = [ "-n" "${pkgs.libnotify}/bin/notify-send 'About to sleep!'"];
+        lockerCommand = "${pkgs.xlockmore}/bin/xlock -mode ant";
+      };
+    };
+  };
+
+  testScript = ''
+    def perform_xsslock_test(machine, lockCmd):
+        machine.start()
+        machine.wait_for_x()
+        machine.wait_for_unit("xss-lock.service", "alice")
+        machine.fail(f"pgrep {lockCmd}")
+        machine.succeed("su -l alice -c 'xset dpms force standby'")
+        machine.wait_until_succeeds(f"pgrep {lockCmd}")
+
+
+    with subtest("simple"):
+        perform_xsslock_test(simple, "i3lock")
+
+    with subtest("custom_cmd"):
+        perform_xsslock_test(custom_lockcmd, "xlock")
+  '';
+})
diff --git a/nixos/tests/xterm.nix b/nixos/tests/xterm.nix
new file mode 100644
index 00000000000..4ee31139ab5
--- /dev/null
+++ b/nixos/tests/xterm.nix
@@ -0,0 +1,23 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "xterm";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ nequissimus ];
+  };
+
+  machine = { pkgs, ... }:
+    {
+      imports = [ ./common/x11.nix ];
+      services.xserver.desktopManager.xterm.enable = false;
+    };
+
+  testScript =
+    ''
+      machine.wait_for_x()
+      machine.succeed("DISPLAY=:0 xterm -title testterm -class testterm -fullscreen >&2 &")
+      machine.sleep(2)
+      machine.send_chars("echo $XTERM_VERSION >> /tmp/xterm_version\n")
+      machine.wait_for_file("/tmp/xterm_version")
+      assert "${pkgs.xterm.version}" in machine.succeed("cat /tmp/xterm_version")
+      machine.screenshot("window")
+    '';
+})
diff --git a/nixos/tests/xxh.nix b/nixos/tests/xxh.nix
new file mode 100644
index 00000000000..3af8e53779e
--- /dev/null
+++ b/nixos/tests/xxh.nix
@@ -0,0 +1,67 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+  let
+    inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
+    xxh-shell-zsh = pkgs.stdenv.mkDerivation {
+      pname = "xxh-shell-zsh";
+      version = "";
+      src = pkgs.fetchFromGitHub {
+        owner = "xxh";
+        repo = "xxh-shell-zsh";
+        # gets rarely updated, we can then just replace the hash
+        rev = "91e1f84f8d6e0852c3235d4813f341230cac439f";
+        sha256 = "sha256-Y1FrIRxTd0yooK+ZzKcCd6bLSy5E2fRXYAzrIsm7rIc=";
+      };
+
+      postPatch = ''
+        substituteInPlace build.sh \
+          --replace "echo Install wget or curl" "cp ${zsh-portable-binary} zsh-5.8-linux-x86_64.tar.gz" \
+          --replace "command -v curl" "command -v this-should-not-trigger"
+      '';
+
+      installPhase = ''
+        mkdir -p $out
+        mv * $out/
+      '';
+    };
+
+    zsh-portable-binary = pkgs.fetchurl {
+      # kept in sync with https://github.com/xxh/xxh-shell-zsh/tree/master/build.sh#L27
+      url = "https://github.com/romkatv/zsh-bin/releases/download/v3.0.1/zsh-5.8-linux-x86_64.tar.gz";
+      sha256 = "sha256-i8flMd2Isc0uLoeYQNDnOGb/kK3oTFVqQgIx7aOAIIo=";
+    };
+  in
+  {
+    name = "xxh";
+    meta = with lib.maintainers; {
+      maintainers = [ lom ];
+    };
+
+    nodes = {
+      server = { ... }: {
+        services.openssh.enable = true;
+        users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
+      };
+
+      client = { ... }: {
+        programs.zsh.enable = true;
+        users.users.root.shell = pkgs.zsh;
+        environment.systemPackages = with pkgs; [ xxh git ];
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      client.succeed("mkdir -m 700 /root/.ssh")
+
+      client.succeed(
+         "cat ${snakeOilPrivateKey} > /root/.ssh/id_ecdsa"
+      )
+      client.succeed("chmod 600 /root/.ssh/id_ecdsa")
+
+      server.wait_for_unit("sshd")
+
+      client.succeed("xxh server -i /root/.ssh/id_ecdsa +hc \'echo $0\' +i +s zsh +I xxh-shell-zsh+path+${xxh-shell-zsh} | grep -Fq '/root/.xxh/.xxh/shells/xxh-shell-zsh/build/zsh-bin/bin/zsh'")
+    '';
+  })
diff --git a/nixos/tests/yabar.nix b/nixos/tests/yabar.nix
new file mode 100644
index 00000000000..c2431e556c3
--- /dev/null
+++ b/nixos/tests/yabar.nix
@@ -0,0 +1,33 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+with lib;
+
+{
+  name = "yabar";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ];
+  };
+
+  machine = {
+    imports = [ ./common/x11.nix ./common/user-account.nix ];
+
+    test-support.displayManager.auto.user = "bob";
+
+    programs.yabar.enable = true;
+    programs.yabar.bars = {
+      top.indicators.date.exec = "YABAR_DATE";
+    };
+  };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_x()
+
+    # confirm proper startup
+    machine.wait_for_unit("yabar.service", "bob")
+    machine.sleep(10)
+    machine.wait_for_unit("yabar.service", "bob")
+
+    machine.screenshot("top_bar")
+  '';
+})
diff --git a/nixos/tests/yggdrasil.nix b/nixos/tests/yggdrasil.nix
new file mode 100644
index 00000000000..b409d9ed785
--- /dev/null
+++ b/nixos/tests/yggdrasil.nix
@@ -0,0 +1,162 @@
+let
+  aliceIp6 = "202:b70:9b0b:cf34:f93c:8f18:bbfd:7034";
+  aliceKeys = {
+    PublicKey = "3e91ec9e861960d86e1ce88051f97c435bdf2859640ab681dfa906eb45ad5182";
+    PrivateKey = "a867f9e078e4ce58d310cf5acd4622d759e2a21df07e1d6fc380a2a26489480d3e91ec9e861960d86e1ce88051f97c435bdf2859640ab681dfa906eb45ad5182";
+  };
+  bobIp6 = "202:a483:73a4:9f2d:a559:4a19:bc9:8458";
+  bobPrefix = "302:a483:73a4:9f2d";
+  bobConfig = {
+    InterfacePeers = {
+      eth1 = [ "tcp://192.168.1.200:12345" ];
+    };
+    MulticastInterfaces = [ "eth1" ];
+    LinkLocalTCPPort = 54321;
+    PublicKey = "2b6f918b6c1a4b54d6bcde86cf74e074fb32ead4ee439b7930df2aa60c825186";
+    PrivateKey = "0c4a24acd3402722ce9277ed179f4a04b895b49586493c25fbaed60653d857d62b6f918b6c1a4b54d6bcde86cf74e074fb32ead4ee439b7930df2aa60c825186";
+  };
+  danIp6 = bobPrefix + "::2";
+
+in import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "yggdrasil";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ gazally ];
+  };
+
+  nodes = rec {
+    # Alice is listening for peerings on a specified port,
+    # but has multicast peering disabled.  Alice has part of her
+    # yggdrasil config in Nix and part of it in a file.
+    alice =
+      { ... }:
+      {
+        networking = {
+          interfaces.eth1.ipv4.addresses = [{
+            address = "192.168.1.200";
+            prefixLength = 24;
+          }];
+          firewall.allowedTCPPorts = [ 80 12345 ];
+        };
+        services.httpd.enable = true;
+        services.httpd.adminAddr = "foo@example.org";
+
+        services.yggdrasil = {
+          enable = true;
+          config = {
+            Listen = ["tcp://0.0.0.0:12345"];
+            MulticastInterfaces = [ ];
+          };
+          configFile = toString (pkgs.writeTextFile {
+                         name = "yggdrasil-alice-conf";
+                         text = builtins.toJSON aliceKeys;
+                       });
+        };
+      };
+
+    # Bob is set up to peer with Alice, and also to do local multicast
+    # peering.  Bob's yggdrasil config is in a file.
+    bob =
+      { ... }:
+      {
+        networking.firewall.allowedTCPPorts = [ 54321 ];
+        services.yggdrasil = {
+          enable = true;
+          openMulticastPort = true;
+          configFile = toString (pkgs.writeTextFile {
+                         name = "yggdrasil-bob-conf";
+                         text = builtins.toJSON bobConfig;
+                       });
+        };
+
+        boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1;
+
+        networking = {
+          bridges.br0.interfaces = [ ];
+          interfaces.br0 = {
+            ipv6.addresses = [{
+              address = bobPrefix + "::1";
+              prefixLength = 64;
+            }];
+          };
+        };
+
+        # dan is a node inside a container running on bob's host.
+        containers.dan = {
+          autoStart = true;
+          privateNetwork = true;
+          hostBridge = "br0";
+          config = { config, pkgs, ... }: {
+            networking.interfaces.eth0.ipv6 = {
+              addresses = [{
+                address = bobPrefix + "::2";
+                prefixLength = 64;
+              }];
+              routes = [{
+                address = "200::";
+                prefixLength = 7;
+                via = bobPrefix + "::1";
+              }];
+            };
+            services.httpd.enable = true;
+            services.httpd.adminAddr = "foo@example.org";
+            networking.firewall.allowedTCPPorts = [ 80 ];
+          };
+        };
+      };
+
+    # Carol only does local peering.  Carol's yggdrasil config is all Nix.
+    carol =
+      { ... }:
+      {
+        networking.firewall.allowedTCPPorts = [ 43210 ];
+        services.yggdrasil = {
+          enable = true;
+          denyDhcpcdInterfaces = [ "ygg0" ];
+          config = {
+            IfTAPMode = true;
+            IfName = "ygg0";
+            MulticastInterfaces = [ "eth1" ];
+            LinkLocalTCPPort = 43210;
+          };
+          persistentKeys = true;
+        };
+      };
+    };
+
+  testScript =
+    ''
+      import re
+
+      # Give Alice a head start so she is ready when Bob calls.
+      alice.start()
+      alice.wait_for_unit("yggdrasil.service")
+
+      bob.start()
+      carol.start()
+      bob.wait_for_unit("default.target")
+      carol.wait_for_unit("yggdrasil.service")
+
+      ip_addr_show = "ip -o -6 addr show dev ygg0 scope global"
+      carol.wait_until_succeeds(f"[ `{ip_addr_show} | grep -v tentative | wc -l` -ge 1 ]")
+      carol_ip6 = re.split(" +|/", carol.succeed(ip_addr_show))[3]
+
+      # If Alice can talk to Carol, then Bob's outbound peering and Carol's
+      # local peering have succeeded and everybody is connected.
+      alice.wait_until_succeeds(f"ping -c 1 {carol_ip6}")
+      alice.succeed("ping -c 1 ${bobIp6}")
+
+      bob.succeed("ping -c 1 ${aliceIp6}")
+      bob.succeed(f"ping -c 1 {carol_ip6}")
+
+      carol.succeed("ping -c 1 ${aliceIp6}")
+      carol.succeed("ping -c 1 ${bobIp6}")
+      carol.succeed("ping -c 1 ${bobPrefix}::1")
+      carol.succeed("ping -c 8 ${danIp6}")
+
+      carol.fail("journalctl -u dhcpcd | grep ygg0")
+
+      alice.wait_for_unit("httpd.service")
+      carol.succeed("curl --fail -g http://[${aliceIp6}]")
+      carol.succeed("curl --fail -g http://[${danIp6}]")
+    '';
+})
diff --git a/nixos/tests/zammad.nix b/nixos/tests/zammad.nix
new file mode 100644
index 00000000000..4e466f6e3b9
--- /dev/null
+++ b/nixos/tests/zammad.nix
@@ -0,0 +1,60 @@
+import ./make-test-python.nix (
+  { lib, pkgs, ... }:
+
+  {
+    name = "zammad";
+
+    meta.maintainers = with lib.maintainers; [ garbas taeer ];
+
+    nodes.machine = { config, ... }: {
+      services.zammad.enable = true;
+      services.zammad.secretKeyBaseFile = pkgs.writeText "secret" ''
+        52882ef142066e09ab99ce816ba72522e789505caba224a52d750ec7dc872c2c371b2fd19f16b25dfbdd435a4dd46cb3df9f82eb63fafad715056bdfe25740d6
+      '';
+
+      systemd.services.zammad-locale-cheat =
+        let cfg = config.services.zammad; in
+        {
+          serviceConfig = {
+            Type = "simple";
+            Restart = "always";
+
+            User = "zammad";
+            Group = "zammad";
+            PrivateTmp = true;
+            StateDirectory = "zammad";
+            WorkingDirectory = cfg.dataDir;
+          };
+          wantedBy = [ "zammad-web.service" ];
+          description = "Hack in the locale files so zammad doesn't try to access the internet";
+          script = ''
+            mkdir -p ./config/translations
+            VERSION=$(cat ${cfg.package}/VERSION)
+
+            # If these files are not in place, zammad will try to access the internet.
+            # For the test, we only need to supply en-us.
+            echo '[{"locale":"en-us","alias":"en","name":"English (United States)","active":true,"dir":"ltr"}]' \
+              > ./config/locales-$VERSION.yml
+            echo '[{"locale":"en-us","format":"time","source":"date","target":"mm/dd/yyyy","target_initial":"mm/dd/yyyy"},{"locale":"en-us","format":"time","source":"timestamp","target":"mm/dd/yyyy HH:MM","target_initial":"mm/dd/yyyy HH:MM"}]' \
+              > ./config/translations/en-us-$VERSION.yml
+          '';
+        };
+    };
+
+    testScript = ''
+      start_all()
+      machine.wait_for_unit("postgresql.service")
+      machine.wait_for_unit("zammad-web.service")
+      machine.wait_for_unit("zammad-websocket.service")
+      machine.wait_for_unit("zammad-scheduler.service")
+      # wait for zammad to fully come up
+      machine.sleep(120)
+
+      # without the grep the command does not produce valid utf-8 for some reason
+      with subtest("welcome screen loads"):
+          machine.succeed(
+              "curl -sSfL http://localhost:3000/ | grep '<title>Zammad Helpdesk</title>'"
+          )
+    '';
+  }
+)
diff --git a/nixos/tests/zfs.nix b/nixos/tests/zfs.nix
new file mode 100644
index 00000000000..d25090403e5
--- /dev/null
+++ b/nixos/tests/zfs.nix
@@ -0,0 +1,130 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+
+let
+
+  makeZfsTest = name:
+    { kernelPackage ? if enableUnstable then pkgs.linuxPackages_latest else pkgs.linuxPackages
+    , enableUnstable ? false
+    , extraTest ? ""
+    }:
+    makeTest {
+      name = "zfs-" + name;
+      meta = with pkgs.lib.maintainers; {
+        maintainers = [ adisbladis ];
+      };
+
+      machine = { pkgs, lib, ... }:
+        let
+          usersharePath = "/var/lib/samba/usershares";
+        in {
+        virtualisation.emptyDiskImages = [ 4096 ];
+        networking.hostId = "deadbeef";
+        boot.kernelPackages = kernelPackage;
+        boot.supportedFilesystems = [ "zfs" ];
+        boot.zfs.enableUnstable = enableUnstable;
+
+        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}";
+
+        environment.systemPackages = [ pkgs.parted ];
+
+        # Setup regular fileSystems machinery to ensure forceImportAll can be
+        # tested via the regular service units.
+        virtualisation.fileSystems = {
+          "/forcepool" = {
+            device = "forcepool";
+            fsType = "zfs";
+            options = [ "noauto" ];
+          };
+        };
+
+        # forcepool doesn't exist at first boot, and we need to manually test
+        # the import after tweaking the hostId.
+        systemd.services.zfs-import-forcepool.wantedBy = lib.mkVMOverride [];
+        systemd.targets.zfs.wantedBy = lib.mkVMOverride [];
+        boot.zfs.forceImportAll = true;
+        # /dev/disk/by-id doesn't get populated in the NixOS test framework
+        boot.zfs.devNodes = "/dev/disk/by-uuid";
+      };
+
+      testScript = ''
+        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",
+        )
+
+        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("boot.zfs.forceImportAll works"):
+            machine.succeed(
+                "rm /etc/hostid",
+                "zgenhostid deadcafe",
+                "zpool create forcepool /dev/vdb1 -O mountpoint=legacy",
+            )
+            machine.shutdown()
+            machine.start()
+            machine.succeed("udevadm settle")
+            machine.fail("zpool import forcepool")
+            machine.succeed(
+                "systemctl start zfs-import-forcepool.service",
+                "mount -t zfs forcepool /tmp/mnt",
+            )
+      '' + extraTest;
+
+    };
+
+
+in {
+
+  stable = makeZfsTest "stable" { };
+
+  unstable = makeZfsTest "unstable" {
+    enableUnstable = true;
+  };
+
+  installer = (import ./installer.nix { }).zfsroot;
+}
diff --git a/nixos/tests/zigbee2mqtt.nix b/nixos/tests/zigbee2mqtt.nix
new file mode 100644
index 00000000000..98aadbb699b
--- /dev/null
+++ b/nixos/tests/zigbee2mqtt.nix
@@ -0,0 +1,23 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+  {
+    machine = { pkgs, ... }:
+      {
+        services.zigbee2mqtt = {
+          enable = true;
+        };
+
+        systemd.services.zigbee2mqtt.serviceConfig.DevicePolicy = lib.mkForce "auto";
+      };
+
+    testScript = ''
+      machine.wait_for_unit("zigbee2mqtt.service")
+      machine.wait_until_fails("systemctl status zigbee2mqtt.service")
+      machine.succeed(
+          "journalctl -eu zigbee2mqtt | grep \"Error: Error while opening serialport 'Error: Error: No such file or directory, cannot open /dev/ttyACM0'\""
+      )
+
+      machine.log(machine.succeed("systemd-analyze security zigbee2mqtt.service"))
+    '';
+  }
+)
diff --git a/nixos/tests/zoneminder.nix b/nixos/tests/zoneminder.nix
new file mode 100644
index 00000000000..a4e1a05ec0e
--- /dev/null
+++ b/nixos/tests/zoneminder.nix
@@ -0,0 +1,23 @@
+import ./make-test-python.nix ({ lib, ...}:
+
+{
+  name = "zoneminder";
+  meta.maintainers = with lib.maintainers; [ danielfullmer ];
+
+  machine = { ... }:
+  {
+    services.zoneminder = {
+      enable = true;
+      database.createLocally = true;
+      database.username = "zoneminder";
+    };
+    time.timeZone = "America/New_York";
+  };
+
+  testScript = ''
+    machine.wait_for_unit("zoneminder.service")
+    machine.wait_for_unit("nginx.service")
+    machine.wait_for_open_port(8095)
+    machine.succeed("curl --fail http://localhost:8095/")
+  '';
+})
diff --git a/nixos/tests/zookeeper.nix b/nixos/tests/zookeeper.nix
new file mode 100644
index 00000000000..0ee2673886a
--- /dev/null
+++ b/nixos/tests/zookeeper.nix
@@ -0,0 +1,46 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+let
+
+  perlEnv = pkgs.perl.withPackages (p: [p.NetZooKeeper]);
+
+in {
+  name = "zookeeper";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ nequissimus ztzg ];
+  };
+
+  nodes = {
+    server = { ... }: {
+      services.zookeeper = {
+        enable = true;
+      };
+
+      networking.firewall.allowedTCPPorts = [ 2181 ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    server.wait_for_unit("zookeeper")
+    server.wait_for_unit("network.target")
+    server.wait_for_open_port(2181)
+
+    server.wait_until_succeeds(
+        "${pkgs.zookeeper}/bin/zkCli.sh -server localhost:2181 create /foo bar"
+    )
+    server.wait_until_succeeds(
+        "${pkgs.zookeeper}/bin/zkCli.sh -server localhost:2181 set /foo hello"
+    )
+    server.wait_until_succeeds(
+        "${pkgs.zookeeper}/bin/zkCli.sh -server localhost:2181 get /foo | grep hello"
+    )
+
+    server.wait_until_succeeds(
+        "${perlEnv}/bin/perl -E 'use Net::ZooKeeper qw(:acls); $z=Net::ZooKeeper->new(q(localhost:2181)); $z->create(qw(/perl foo acl), ZOO_OPEN_ACL_UNSAFE) || die $z->get_error()'"
+    )
+    server.wait_until_succeeds(
+        "${perlEnv}/bin/perl -E 'use Net::ZooKeeper qw(:acls); $z=Net::ZooKeeper->new(q(localhost:2181)); $z->get(qw(/perl)) eq qw(foo) || die $z->get_error()'"
+    )
+  '';
+})
diff --git a/nixos/tests/zsh-history.nix b/nixos/tests/zsh-history.nix
new file mode 100644
index 00000000000..35568779840
--- /dev/null
+++ b/nixos/tests/zsh-history.nix
@@ -0,0 +1,35 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "zsh-history";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ ];
+  };
+
+  nodes.default = { ... }: {
+    programs = {
+      zsh.enable = true;
+    };
+    environment.systemPackages = [ pkgs.zsh-history ];
+    programs.zsh.interactiveShellInit = ''
+      source ${pkgs.zsh-history.out}/share/zsh/init.zsh
+    '';
+    users.users.root.shell = "${pkgs.zsh}/bin/zsh";
+  };
+
+  testScript = ''
+    start_all()
+    default.wait_for_unit("multi-user.target")
+    default.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
+
+    # Login
+    default.wait_until_tty_matches(1, "login: ")
+    default.send_chars("root\n")
+    default.wait_until_tty_matches(1, r"\nroot@default\b")
+
+    # Generate some history
+    default.send_chars("echo foobar\n")
+    default.wait_until_tty_matches(1, "foobar")
+
+    # Ensure that command was recorded in history
+    default.succeed("/run/current-system/sw/bin/history list | grep -q foobar")
+  '';
+})